Coverage for /pythoncovmergedfiles/medio/medio/src/aiohttp/aiohttp/web_response.py: 25%
430 statements
« prev ^ index » next coverage.py v7.3.1, created at 2023-09-27 06:09 +0000
« prev ^ index » next coverage.py v7.3.1, created at 2023-09-27 06:09 +0000
1import asyncio
2import collections.abc
3import datetime
4import enum
5import json
6import math
7import time
8import warnings
9from concurrent.futures import Executor
10from http import HTTPStatus
11from typing import (
12 TYPE_CHECKING,
13 Any,
14 Dict,
15 Iterator,
16 MutableMapping,
17 Optional,
18 Union,
19 cast,
20)
22from multidict import CIMultiDict, istr
24from . import hdrs, payload
25from .abc import AbstractStreamWriter
26from .compression_utils import ZLibCompressor
27from .helpers import (
28 ETAG_ANY,
29 QUOTED_ETAG_RE,
30 CookieMixin,
31 ETag,
32 HeadersMixin,
33 parse_http_date,
34 populate_with_cookies,
35 rfc822_formatted_time,
36 sentinel,
37 validate_etag_value,
38)
39from .http import SERVER_SOFTWARE, HttpVersion10, HttpVersion11
40from .payload import Payload
41from .typedefs import JSONEncoder, LooseHeaders
43__all__ = ("ContentCoding", "StreamResponse", "Response", "json_response")
46if TYPE_CHECKING: # pragma: no cover
47 from .web_request import BaseRequest
49 BaseClass = MutableMapping[str, Any]
50else:
51 BaseClass = collections.abc.MutableMapping
54class ContentCoding(enum.Enum):
55 # The content codings that we have support for.
56 #
57 # Additional registered codings are listed at:
58 # https://www.iana.org/assignments/http-parameters/http-parameters.xhtml#content-coding
59 deflate = "deflate"
60 gzip = "gzip"
61 identity = "identity"
64############################################################
65# HTTP Response classes
66############################################################
69class StreamResponse(BaseClass, HeadersMixin, CookieMixin):
70 __slots__ = (
71 "_length_check",
72 "_body",
73 "_keep_alive",
74 "_chunked",
75 "_compression",
76 "_compression_force",
77 "_req",
78 "_payload_writer",
79 "_eof_sent",
80 "_body_length",
81 "_state",
82 "_headers",
83 "_status",
84 "_reason",
85 "_cookies",
86 "__weakref__",
87 )
89 def __init__(
90 self,
91 *,
92 status: int = 200,
93 reason: Optional[str] = None,
94 headers: Optional[LooseHeaders] = None,
95 ) -> None:
96 super().__init__()
97 self._length_check = True
98 self._body = None
99 self._keep_alive: Optional[bool] = None
100 self._chunked = False
101 self._compression = False
102 self._compression_force: Optional[ContentCoding] = None
104 self._req: Optional[BaseRequest] = None
105 self._payload_writer: Optional[AbstractStreamWriter] = None
106 self._eof_sent = False
107 self._body_length = 0
108 self._state: Dict[str, Any] = {}
110 if headers is not None:
111 self._headers: CIMultiDict[str] = CIMultiDict(headers)
112 else:
113 self._headers = CIMultiDict()
115 self.set_status(status, reason)
117 @property
118 def prepared(self) -> bool:
119 return self._payload_writer is not None
121 @property
122 def task(self) -> "Optional[asyncio.Task[None]]":
123 if self._req:
124 return self._req.task
125 else:
126 return None
128 @property
129 def status(self) -> int:
130 return self._status
132 @property
133 def chunked(self) -> bool:
134 return self._chunked
136 @property
137 def compression(self) -> bool:
138 return self._compression
140 @property
141 def reason(self) -> str:
142 return self._reason
144 def set_status(
145 self,
146 status: int,
147 reason: Optional[str] = None,
148 ) -> None:
149 assert not self.prepared, (
150 "Cannot change the response status code after " "the headers have been sent"
151 )
152 self._status = int(status)
153 if reason is None:
154 try:
155 reason = HTTPStatus(self._status).phrase
156 except ValueError:
157 reason = ""
158 self._reason = reason
160 @property
161 def keep_alive(self) -> Optional[bool]:
162 return self._keep_alive
164 def force_close(self) -> None:
165 self._keep_alive = False
167 @property
168 def body_length(self) -> int:
169 return self._body_length
171 def enable_chunked_encoding(self) -> None:
172 """Enables automatic chunked transfer encoding."""
173 self._chunked = True
175 if hdrs.CONTENT_LENGTH in self._headers:
176 raise RuntimeError(
177 "You can't enable chunked encoding when " "a content length is set"
178 )
180 def enable_compression(self, force: Optional[ContentCoding] = None) -> None:
181 """Enables response compression encoding."""
182 # Backwards compatibility for when force was a bool <0.17.
183 self._compression = True
184 self._compression_force = force
186 @property
187 def headers(self) -> "CIMultiDict[str]":
188 return self._headers
190 @property
191 def content_length(self) -> Optional[int]:
192 # Just a placeholder for adding setter
193 return super().content_length
195 @content_length.setter
196 def content_length(self, value: Optional[int]) -> None:
197 if value is not None:
198 value = int(value)
199 if self._chunked:
200 raise RuntimeError(
201 "You can't set content length when " "chunked encoding is enable"
202 )
203 self._headers[hdrs.CONTENT_LENGTH] = str(value)
204 else:
205 self._headers.pop(hdrs.CONTENT_LENGTH, None)
207 @property
208 def content_type(self) -> str:
209 # Just a placeholder for adding setter
210 return super().content_type
212 @content_type.setter
213 def content_type(self, value: str) -> None:
214 self.content_type # read header values if needed
215 self._content_type = str(value)
216 self._generate_content_type_header()
218 @property
219 def charset(self) -> Optional[str]:
220 # Just a placeholder for adding setter
221 return super().charset
223 @charset.setter
224 def charset(self, value: Optional[str]) -> None:
225 ctype = self.content_type # read header values if needed
226 if ctype == "application/octet-stream":
227 raise RuntimeError(
228 "Setting charset for application/octet-stream "
229 "doesn't make sense, setup content_type first"
230 )
231 assert self._content_dict is not None
232 if value is None:
233 self._content_dict.pop("charset", None)
234 else:
235 self._content_dict["charset"] = str(value).lower()
236 self._generate_content_type_header()
238 @property
239 def last_modified(self) -> Optional[datetime.datetime]:
240 """The value of Last-Modified HTTP header, or None.
242 This header is represented as a `datetime` object.
243 """
244 return parse_http_date(self._headers.get(hdrs.LAST_MODIFIED))
246 @last_modified.setter
247 def last_modified(
248 self, value: Optional[Union[int, float, datetime.datetime, str]]
249 ) -> None:
250 if value is None:
251 self._headers.pop(hdrs.LAST_MODIFIED, None)
252 elif isinstance(value, (int, float)):
253 self._headers[hdrs.LAST_MODIFIED] = time.strftime(
254 "%a, %d %b %Y %H:%M:%S GMT", time.gmtime(math.ceil(value))
255 )
256 elif isinstance(value, datetime.datetime):
257 self._headers[hdrs.LAST_MODIFIED] = time.strftime(
258 "%a, %d %b %Y %H:%M:%S GMT", value.utctimetuple()
259 )
260 elif isinstance(value, str):
261 self._headers[hdrs.LAST_MODIFIED] = value
263 @property
264 def etag(self) -> Optional[ETag]:
265 quoted_value = self._headers.get(hdrs.ETAG)
266 if not quoted_value:
267 return None
268 elif quoted_value == ETAG_ANY:
269 return ETag(value=ETAG_ANY)
270 match = QUOTED_ETAG_RE.fullmatch(quoted_value)
271 if not match:
272 return None
273 is_weak, value = match.group(1, 2)
274 return ETag(
275 is_weak=bool(is_weak),
276 value=value,
277 )
279 @etag.setter
280 def etag(self, value: Optional[Union[ETag, str]]) -> None:
281 if value is None:
282 self._headers.pop(hdrs.ETAG, None)
283 elif (isinstance(value, str) and value == ETAG_ANY) or (
284 isinstance(value, ETag) and value.value == ETAG_ANY
285 ):
286 self._headers[hdrs.ETAG] = ETAG_ANY
287 elif isinstance(value, str):
288 validate_etag_value(value)
289 self._headers[hdrs.ETAG] = f'"{value}"'
290 elif isinstance(value, ETag) and isinstance(value.value, str): # type: ignore[redundant-expr]
291 validate_etag_value(value.value)
292 hdr_value = f'W/"{value.value}"' if value.is_weak else f'"{value.value}"'
293 self._headers[hdrs.ETAG] = hdr_value
294 else:
295 raise ValueError(
296 f"Unsupported etag type: {type(value)}. "
297 f"etag must be str, ETag or None"
298 )
300 def _generate_content_type_header(
301 self, CONTENT_TYPE: istr = hdrs.CONTENT_TYPE
302 ) -> None:
303 assert self._content_dict is not None
304 assert self._content_type is not None
305 params = "; ".join(f"{k}={v}" for k, v in self._content_dict.items())
306 if params:
307 ctype = self._content_type + "; " + params
308 else:
309 ctype = self._content_type
310 self._headers[CONTENT_TYPE] = ctype
312 async def _do_start_compression(self, coding: ContentCoding) -> None:
313 if coding != ContentCoding.identity:
314 assert self._payload_writer is not None
315 self._headers[hdrs.CONTENT_ENCODING] = coding.value
316 self._payload_writer.enable_compression(coding.value)
317 # Compressed payload may have different content length,
318 # remove the header
319 self._headers.popall(hdrs.CONTENT_LENGTH, None)
321 async def _start_compression(self, request: "BaseRequest") -> None:
322 if self._compression_force:
323 await self._do_start_compression(self._compression_force)
324 else:
325 accept_encoding = request.headers.get(hdrs.ACCEPT_ENCODING, "").lower()
326 for coding in ContentCoding:
327 if coding.value in accept_encoding:
328 await self._do_start_compression(coding)
329 return
331 async def prepare(self, request: "BaseRequest") -> Optional[AbstractStreamWriter]:
332 if self._eof_sent:
333 return None
334 if self._payload_writer is not None:
335 return self._payload_writer
337 return await self._start(request)
339 async def _start(self, request: "BaseRequest") -> AbstractStreamWriter:
340 self._req = request
341 writer = self._payload_writer = request._payload_writer
343 await self._prepare_headers()
344 await request._prepare_hook(self)
345 await self._write_headers()
347 return writer
349 async def _prepare_headers(self) -> None:
350 request = self._req
351 assert request is not None
352 writer = self._payload_writer
353 assert writer is not None
354 keep_alive = self._keep_alive
355 if keep_alive is None:
356 keep_alive = request.keep_alive
357 self._keep_alive = keep_alive
359 version = request.version
361 headers = self._headers
362 populate_with_cookies(headers, self.cookies)
364 if self._compression:
365 await self._start_compression(request)
367 if self._chunked:
368 if version != HttpVersion11:
369 raise RuntimeError(
370 "Using chunked encoding is forbidden "
371 "for HTTP/{0.major}.{0.minor}".format(request.version)
372 )
373 writer.enable_chunking()
374 headers[hdrs.TRANSFER_ENCODING] = "chunked"
375 if hdrs.CONTENT_LENGTH in headers:
376 del headers[hdrs.CONTENT_LENGTH]
377 elif self._length_check:
378 writer.length = self.content_length
379 if writer.length is None:
380 if version >= HttpVersion11 and self.status != 204:
381 writer.enable_chunking()
382 headers[hdrs.TRANSFER_ENCODING] = "chunked"
383 if hdrs.CONTENT_LENGTH in headers:
384 del headers[hdrs.CONTENT_LENGTH]
385 else:
386 keep_alive = False
387 # HTTP 1.1: https://tools.ietf.org/html/rfc7230#section-3.3.2
388 # HTTP 1.0: https://tools.ietf.org/html/rfc1945#section-10.4
389 elif version >= HttpVersion11 and self.status in (100, 101, 102, 103, 204):
390 del headers[hdrs.CONTENT_LENGTH]
392 if self.status not in (204, 304):
393 headers.setdefault(hdrs.CONTENT_TYPE, "application/octet-stream")
394 headers.setdefault(hdrs.DATE, rfc822_formatted_time())
395 headers.setdefault(hdrs.SERVER, SERVER_SOFTWARE)
397 # connection header
398 if hdrs.CONNECTION not in headers:
399 if keep_alive:
400 if version == HttpVersion10:
401 headers[hdrs.CONNECTION] = "keep-alive"
402 else:
403 if version == HttpVersion11:
404 headers[hdrs.CONNECTION] = "close"
406 async def _write_headers(self) -> None:
407 request = self._req
408 assert request is not None
409 writer = self._payload_writer
410 assert writer is not None
411 # status line
412 version = request.version
413 status_line = "HTTP/{}.{} {} {}".format(
414 version[0], version[1], self._status, self._reason
415 )
416 await writer.write_headers(status_line, self._headers)
418 async def write(self, data: bytes) -> None:
419 assert isinstance(
420 data, (bytes, bytearray, memoryview)
421 ), "data argument must be byte-ish (%r)" % type(data)
423 if self._eof_sent:
424 raise RuntimeError("Cannot call write() after write_eof()")
425 if self._payload_writer is None:
426 raise RuntimeError("Cannot call write() before prepare()")
428 await self._payload_writer.write(data)
430 async def drain(self) -> None:
431 assert not self._eof_sent, "EOF has already been sent"
432 assert self._payload_writer is not None, "Response has not been started"
433 warnings.warn(
434 "drain method is deprecated, use await resp.write()",
435 DeprecationWarning,
436 stacklevel=2,
437 )
438 await self._payload_writer.drain()
440 async def write_eof(self, data: bytes = b"") -> None:
441 assert isinstance(
442 data, (bytes, bytearray, memoryview)
443 ), "data argument must be byte-ish (%r)" % type(data)
445 if self._eof_sent:
446 return
448 assert self._payload_writer is not None, "Response has not been started"
450 await self._payload_writer.write_eof(data)
451 self._eof_sent = True
452 self._req = None
453 self._body_length = self._payload_writer.output_size
454 self._payload_writer = None
456 def __repr__(self) -> str:
457 if self._eof_sent:
458 info = "eof"
459 elif self.prepared:
460 assert self._req is not None
461 info = f"{self._req.method} {self._req.path} "
462 else:
463 info = "not prepared"
464 return f"<{self.__class__.__name__} {self.reason} {info}>"
466 def __getitem__(self, key: str) -> Any:
467 return self._state[key]
469 def __setitem__(self, key: str, value: Any) -> None:
470 self._state[key] = value
472 def __delitem__(self, key: str) -> None:
473 del self._state[key]
475 def __len__(self) -> int:
476 return len(self._state)
478 def __iter__(self) -> Iterator[str]:
479 return iter(self._state)
481 def __hash__(self) -> int:
482 return hash(id(self))
484 def __eq__(self, other: object) -> bool:
485 return self is other
488class Response(StreamResponse):
489 __slots__ = (
490 "_body_payload",
491 "_compressed_body",
492 "_zlib_executor_size",
493 "_zlib_executor",
494 )
496 def __init__(
497 self,
498 *,
499 body: Any = None,
500 status: int = 200,
501 reason: Optional[str] = None,
502 text: Optional[str] = None,
503 headers: Optional[LooseHeaders] = None,
504 content_type: Optional[str] = None,
505 charset: Optional[str] = None,
506 zlib_executor_size: Optional[int] = None,
507 zlib_executor: Optional[Executor] = None,
508 ) -> None:
509 if body is not None and text is not None:
510 raise ValueError("body and text are not allowed together")
512 if headers is None:
513 real_headers: CIMultiDict[str] = CIMultiDict()
514 elif not isinstance(headers, CIMultiDict):
515 real_headers = CIMultiDict(headers)
516 else:
517 real_headers = headers # = cast('CIMultiDict[str]', headers)
519 if content_type is not None and "charset" in content_type:
520 raise ValueError("charset must not be in content_type " "argument")
522 if text is not None:
523 if hdrs.CONTENT_TYPE in real_headers:
524 if content_type or charset:
525 raise ValueError(
526 "passing both Content-Type header and "
527 "content_type or charset params "
528 "is forbidden"
529 )
530 else:
531 # fast path for filling headers
532 if not isinstance(text, str):
533 raise TypeError("text argument must be str (%r)" % type(text))
534 if content_type is None:
535 content_type = "text/plain"
536 if charset is None:
537 charset = "utf-8"
538 real_headers[hdrs.CONTENT_TYPE] = content_type + "; charset=" + charset
539 body = text.encode(charset)
540 text = None
541 else:
542 if hdrs.CONTENT_TYPE in real_headers:
543 if content_type is not None or charset is not None:
544 raise ValueError(
545 "passing both Content-Type header and "
546 "content_type or charset params "
547 "is forbidden"
548 )
549 else:
550 if content_type is not None:
551 if charset is not None:
552 content_type += "; charset=" + charset
553 real_headers[hdrs.CONTENT_TYPE] = content_type
555 super().__init__(status=status, reason=reason, headers=real_headers)
557 if text is not None:
558 self.text = text
559 else:
560 self.body = body
562 self._compressed_body: Optional[bytes] = None
563 self._zlib_executor_size = zlib_executor_size
564 self._zlib_executor = zlib_executor
566 @property
567 def body(self) -> Optional[Union[bytes, Payload]]:
568 return self._body
570 @body.setter
571 def body(self, body: bytes) -> None:
572 if body is None:
573 self._body: Optional[bytes] = None
574 self._body_payload: bool = False
575 elif isinstance(body, (bytes, bytearray)):
576 self._body = body
577 self._body_payload = False
578 else:
579 try:
580 self._body = body = payload.PAYLOAD_REGISTRY.get(body)
581 except payload.LookupError:
582 raise ValueError("Unsupported body type %r" % type(body))
584 self._body_payload = True
586 headers = self._headers
588 # set content-type
589 if hdrs.CONTENT_TYPE not in headers:
590 headers[hdrs.CONTENT_TYPE] = body.content_type
592 # copy payload headers
593 if body.headers:
594 for key, value in body.headers.items():
595 if key not in headers:
596 headers[key] = value
598 self._compressed_body = None
600 @property
601 def text(self) -> Optional[str]:
602 if self._body is None:
603 return None
604 return self._body.decode(self.charset or "utf-8")
606 @text.setter
607 def text(self, text: str) -> None:
608 assert isinstance(text, str), "text argument must be str (%r)" % type(text)
610 if self.content_type == "application/octet-stream":
611 self.content_type = "text/plain"
612 if self.charset is None:
613 self.charset = "utf-8"
615 self._body = text.encode(self.charset)
616 self._body_payload = False
617 self._compressed_body = None
619 @property
620 def content_length(self) -> Optional[int]:
621 if self._chunked:
622 return None
624 if hdrs.CONTENT_LENGTH in self._headers:
625 return super().content_length
627 if self._compressed_body is not None:
628 # Return length of the compressed body
629 return len(self._compressed_body)
630 elif self._body_payload:
631 # A payload without content length, or a compressed payload
632 return None
633 elif self._body is not None:
634 return len(self._body)
635 else:
636 return 0
638 @content_length.setter
639 def content_length(self, value: Optional[int]) -> None:
640 raise RuntimeError("Content length is set automatically")
642 async def write_eof(self, data: bytes = b"") -> None:
643 if self._eof_sent:
644 return
645 if self._compressed_body is None:
646 body: Optional[Union[bytes, Payload]] = self._body
647 else:
648 body = self._compressed_body
649 assert not data, f"data arg is not supported, got {data!r}"
650 assert self._req is not None
651 assert self._payload_writer is not None
652 if body is not None:
653 if self._req._method == hdrs.METH_HEAD or self._status in [204, 304]:
654 await super().write_eof()
655 elif self._body_payload:
656 payload = cast(Payload, body)
657 await payload.write(self._payload_writer)
658 await super().write_eof()
659 else:
660 await super().write_eof(cast(bytes, body))
661 else:
662 await super().write_eof()
664 async def _start(self, request: "BaseRequest") -> AbstractStreamWriter:
665 if not self._chunked and hdrs.CONTENT_LENGTH not in self._headers:
666 if self._body_payload:
667 size = cast(Payload, self._body).size
668 if size is not None:
669 self._headers[hdrs.CONTENT_LENGTH] = str(size)
670 else:
671 body_len = len(self._body) if self._body else "0"
672 self._headers[hdrs.CONTENT_LENGTH] = str(body_len)
674 return await super()._start(request)
676 async def _do_start_compression(self, coding: ContentCoding) -> None:
677 if self._body_payload or self._chunked:
678 return await super()._do_start_compression(coding)
680 if coding != ContentCoding.identity:
681 # Instead of using _payload_writer.enable_compression,
682 # compress the whole body
683 compressor = ZLibCompressor(
684 encoding=str(coding.value),
685 max_sync_chunk_size=self._zlib_executor_size,
686 executor=self._zlib_executor,
687 )
688 assert self._body is not None
689 if self._zlib_executor_size is None and len(self._body) > 1024 * 1024:
690 warnings.warn(
691 "Synchronous compression of large response bodies "
692 f"({len(self._body)} bytes) might block the async event loop. "
693 "Consider providing a custom value to zlib_executor_size/"
694 "zlib_executor response properties or disabling compression on it."
695 )
696 self._compressed_body = (
697 await compressor.compress(self._body) + compressor.flush()
698 )
699 assert self._compressed_body is not None
701 self._headers[hdrs.CONTENT_ENCODING] = coding.value
702 self._headers[hdrs.CONTENT_LENGTH] = str(len(self._compressed_body))
705def json_response(
706 data: Any = sentinel,
707 *,
708 text: Optional[str] = None,
709 body: Optional[bytes] = None,
710 status: int = 200,
711 reason: Optional[str] = None,
712 headers: Optional[LooseHeaders] = None,
713 content_type: str = "application/json",
714 dumps: JSONEncoder = json.dumps,
715) -> Response:
716 if data is not sentinel:
717 if text or body:
718 raise ValueError("only one of data, text, or body should be specified")
719 else:
720 text = dumps(data)
721 return Response(
722 text=text,
723 body=body,
724 status=status,
725 reason=reason,
726 headers=headers,
727 content_type=content_type,
728 )