Coverage for /pythoncovmergedfiles/medio/medio/src/aiohttp/aiohttp/helpers.py: 38%
550 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
1"""Various helper functions"""
3import asyncio
4import base64
5import binascii
6import contextlib
7import dataclasses
8import datetime
9import enum
10import functools
11import inspect
12import netrc
13import os
14import platform
15import re
16import sys
17import time
18import warnings
19import weakref
20from collections import namedtuple
21from contextlib import suppress
22from email.parser import HeaderParser
23from email.utils import parsedate
24from http.cookies import SimpleCookie
25from math import ceil
26from pathlib import Path
27from types import TracebackType
28from typing import (
29 Any,
30 Callable,
31 ContextManager,
32 Dict,
33 Generator,
34 Generic,
35 Iterable,
36 Iterator,
37 List,
38 Mapping,
39 Optional,
40 Pattern,
41 Protocol,
42 Tuple,
43 Type,
44 TypeVar,
45 Union,
46 final,
47 get_args,
48 overload,
49)
50from urllib.parse import quote
51from urllib.request import getproxies, proxy_bypass
53from multidict import CIMultiDict, MultiDict, MultiDictProxy
54from yarl import URL
56from . import hdrs
57from .log import client_logger
58from .typedefs import PathLike # noqa
60if sys.version_info >= (3, 11):
61 import asyncio as async_timeout
62else:
63 import async_timeout
65__all__ = ("BasicAuth", "ChainMapProxy", "ETag")
67PY_310 = sys.version_info >= (3, 10)
69COOKIE_MAX_LENGTH = 4096
71_T = TypeVar("_T")
72_S = TypeVar("_S")
74_SENTINEL = enum.Enum("_SENTINEL", "sentinel")
75sentinel = _SENTINEL.sentinel
77NO_EXTENSIONS = bool(os.environ.get("AIOHTTP_NO_EXTENSIONS"))
79DEBUG = sys.flags.dev_mode or (
80 not sys.flags.ignore_environment and bool(os.environ.get("PYTHONASYNCIODEBUG"))
81)
84CHAR = {chr(i) for i in range(0, 128)}
85CTL = {chr(i) for i in range(0, 32)} | {
86 chr(127),
87}
88SEPARATORS = {
89 "(",
90 ")",
91 "<",
92 ">",
93 "@",
94 ",",
95 ";",
96 ":",
97 "\\",
98 '"',
99 "/",
100 "[",
101 "]",
102 "?",
103 "=",
104 "{",
105 "}",
106 " ",
107 chr(9),
108}
109TOKEN = CHAR ^ CTL ^ SEPARATORS
112class noop:
113 def __await__(self) -> Generator[None, None, None]:
114 yield
117json_re = re.compile(r"(?:application/|[\w.-]+/[\w.+-]+?\+)json$", re.IGNORECASE)
120class BasicAuth(namedtuple("BasicAuth", ["login", "password", "encoding"])):
121 """Http basic authentication helper."""
123 def __new__(
124 cls, login: str, password: str = "", encoding: str = "latin1"
125 ) -> "BasicAuth":
126 if login is None:
127 raise ValueError("None is not allowed as login value")
129 if password is None:
130 raise ValueError("None is not allowed as password value")
132 if ":" in login:
133 raise ValueError('A ":" is not allowed in login (RFC 1945#section-11.1)')
135 return super().__new__(cls, login, password, encoding)
137 @classmethod
138 def decode(cls, auth_header: str, encoding: str = "latin1") -> "BasicAuth":
139 """Create a BasicAuth object from an Authorization HTTP header."""
140 try:
141 auth_type, encoded_credentials = auth_header.split(" ", 1)
142 except ValueError:
143 raise ValueError("Could not parse authorization header.")
145 if auth_type.lower() != "basic":
146 raise ValueError("Unknown authorization method %s" % auth_type)
148 try:
149 decoded = base64.b64decode(
150 encoded_credentials.encode("ascii"), validate=True
151 ).decode(encoding)
152 except binascii.Error:
153 raise ValueError("Invalid base64 encoding.")
155 try:
156 # RFC 2617 HTTP Authentication
157 # https://www.ietf.org/rfc/rfc2617.txt
158 # the colon must be present, but the username and password may be
159 # otherwise blank.
160 username, password = decoded.split(":", 1)
161 except ValueError:
162 raise ValueError("Invalid credentials.")
164 return cls(username, password, encoding=encoding)
166 @classmethod
167 def from_url(cls, url: URL, *, encoding: str = "latin1") -> Optional["BasicAuth"]:
168 """Create BasicAuth from url."""
169 if not isinstance(url, URL):
170 raise TypeError("url should be yarl.URL instance")
171 if url.user is None:
172 return None
173 return cls(url.user, url.password or "", encoding=encoding)
175 def encode(self) -> str:
176 """Encode credentials."""
177 creds = (f"{self.login}:{self.password}").encode(self.encoding)
178 return "Basic %s" % base64.b64encode(creds).decode(self.encoding)
181def strip_auth_from_url(url: URL) -> Tuple[URL, Optional[BasicAuth]]:
182 auth = BasicAuth.from_url(url)
183 if auth is None:
184 return url, None
185 else:
186 return url.with_user(None), auth
189def netrc_from_env() -> Optional[netrc.netrc]:
190 """Load netrc from file.
192 Attempt to load it from the path specified by the env-var
193 NETRC or in the default location in the user's home directory.
195 Returns None if it couldn't be found or fails to parse.
196 """
197 netrc_env = os.environ.get("NETRC")
199 if netrc_env is not None:
200 netrc_path = Path(netrc_env)
201 else:
202 try:
203 home_dir = Path.home()
204 except RuntimeError as e: # pragma: no cover
205 # if pathlib can't resolve home, it may raise a RuntimeError
206 client_logger.debug(
207 "Could not resolve home directory when "
208 "trying to look for .netrc file: %s",
209 e,
210 )
211 return None
213 netrc_path = home_dir / (
214 "_netrc" if platform.system() == "Windows" else ".netrc"
215 )
217 try:
218 return netrc.netrc(str(netrc_path))
219 except netrc.NetrcParseError as e:
220 client_logger.warning("Could not parse .netrc file: %s", e)
221 except OSError as e:
222 netrc_exists = False
223 with contextlib.suppress(OSError):
224 netrc_exists = netrc_path.is_file()
225 # we couldn't read the file (doesn't exist, permissions, etc.)
226 if netrc_env or netrc_exists:
227 # only warn if the environment wanted us to load it,
228 # or it appears like the default file does actually exist
229 client_logger.warning("Could not read .netrc file: %s", e)
231 return None
234@dataclasses.dataclass(frozen=True)
235class ProxyInfo:
236 proxy: URL
237 proxy_auth: Optional[BasicAuth]
240def basicauth_from_netrc(netrc_obj: Optional[netrc.netrc], host: str) -> BasicAuth:
241 """
242 Return :py:class:`~aiohttp.BasicAuth` credentials for ``host`` from ``netrc_obj``.
244 :raises LookupError: if ``netrc_obj`` is :py:data:`None` or if no
245 entry is found for the ``host``.
246 """
247 if netrc_obj is None:
248 raise LookupError("No .netrc file found")
249 auth_from_netrc = netrc_obj.authenticators(host)
251 if auth_from_netrc is None:
252 raise LookupError(f"No entry for {host!s} found in the `.netrc` file.")
253 login, account, password = auth_from_netrc
255 # TODO(PY311): username = login or account
256 # Up to python 3.10, account could be None if not specified,
257 # and login will be empty string if not specified. From 3.11,
258 # login and account will be empty string if not specified.
259 username = login if (login or account is None) else account
261 # TODO(PY311): Remove this, as password will be empty string
262 # if not specified
263 if password is None:
264 password = ""
266 return BasicAuth(username, password)
269def proxies_from_env() -> Dict[str, ProxyInfo]:
270 proxy_urls = {
271 k: URL(v)
272 for k, v in getproxies().items()
273 if k in ("http", "https", "ws", "wss")
274 }
275 netrc_obj = netrc_from_env()
276 stripped = {k: strip_auth_from_url(v) for k, v in proxy_urls.items()}
277 ret = {}
278 for proto, val in stripped.items():
279 proxy, auth = val
280 if proxy.scheme in ("https", "wss"):
281 client_logger.warning(
282 "%s proxies %s are not supported, ignoring", proxy.scheme.upper(), proxy
283 )
284 continue
285 if netrc_obj and auth is None:
286 if proxy.host is not None:
287 try:
288 auth = basicauth_from_netrc(netrc_obj, proxy.host)
289 except LookupError:
290 auth = None
291 ret[proto] = ProxyInfo(proxy, auth)
292 return ret
295def get_env_proxy_for_url(url: URL) -> Tuple[URL, Optional[BasicAuth]]:
296 """Get a permitted proxy for the given URL from the env."""
297 if url.host is not None and proxy_bypass(url.host):
298 raise LookupError(f"Proxying is disallowed for `{url.host!r}`")
300 proxies_in_env = proxies_from_env()
301 try:
302 proxy_info = proxies_in_env[url.scheme]
303 except KeyError:
304 raise LookupError(f"No proxies found for `{url!s}` in the env")
305 else:
306 return proxy_info.proxy, proxy_info.proxy_auth
309@dataclasses.dataclass(frozen=True)
310class MimeType:
311 type: str
312 subtype: str
313 suffix: str
314 parameters: "MultiDictProxy[str]"
317@functools.lru_cache(maxsize=56)
318def parse_mimetype(mimetype: str) -> MimeType:
319 """Parses a MIME type into its components.
321 mimetype is a MIME type string.
323 Returns a MimeType object.
325 Example:
327 >>> parse_mimetype('text/html; charset=utf-8')
328 MimeType(type='text', subtype='html', suffix='',
329 parameters={'charset': 'utf-8'})
331 """
332 if not mimetype:
333 return MimeType(
334 type="", subtype="", suffix="", parameters=MultiDictProxy(MultiDict())
335 )
337 parts = mimetype.split(";")
338 params: MultiDict[str] = MultiDict()
339 for item in parts[1:]:
340 if not item:
341 continue
342 key, _, value = item.partition("=")
343 params.add(key.lower().strip(), value.strip(' "'))
345 fulltype = parts[0].strip().lower()
346 if fulltype == "*":
347 fulltype = "*/*"
349 mtype, _, stype = fulltype.partition("/")
350 stype, _, suffix = stype.partition("+")
352 return MimeType(
353 type=mtype, subtype=stype, suffix=suffix, parameters=MultiDictProxy(params)
354 )
357def guess_filename(obj: Any, default: Optional[str] = None) -> Optional[str]:
358 name = getattr(obj, "name", None)
359 if name and isinstance(name, str) and name[0] != "<" and name[-1] != ">":
360 return Path(name).name
361 return default
364not_qtext_re = re.compile(r"[^\041\043-\133\135-\176]")
365QCONTENT = {chr(i) for i in range(0x20, 0x7F)} | {"\t"}
368def quoted_string(content: str) -> str:
369 """Return 7-bit content as quoted-string.
371 Format content into a quoted-string as defined in RFC5322 for
372 Internet Message Format. Notice that this is not the 8-bit HTTP
373 format, but the 7-bit email format. Content must be in usascii or
374 a ValueError is raised.
375 """
376 if not (QCONTENT > set(content)):
377 raise ValueError(f"bad content for quoted-string {content!r}")
378 return not_qtext_re.sub(lambda x: "\\" + x.group(0), content)
381def content_disposition_header(
382 disptype: str, quote_fields: bool = True, _charset: str = "utf-8", **params: str
383) -> str:
384 """Sets ``Content-Disposition`` header for MIME.
386 This is the MIME payload Content-Disposition header from RFC 2183
387 and RFC 7579 section 4.2, not the HTTP Content-Disposition from
388 RFC 6266.
390 disptype is a disposition type: inline, attachment, form-data.
391 Should be valid extension token (see RFC 2183)
393 quote_fields performs value quoting to 7-bit MIME headers
394 according to RFC 7578. Set to quote_fields to False if recipient
395 can take 8-bit file names and field values.
397 _charset specifies the charset to use when quote_fields is True.
399 params is a dict with disposition params.
400 """
401 if not disptype or not (TOKEN > set(disptype)):
402 raise ValueError("bad content disposition type {!r}" "".format(disptype))
404 value = disptype
405 if params:
406 lparams = []
407 for key, val in params.items():
408 if not key or not (TOKEN > set(key)):
409 raise ValueError(
410 "bad content disposition parameter" " {!r}={!r}".format(key, val)
411 )
412 if quote_fields:
413 if key.lower() == "filename":
414 qval = quote(val, "", encoding=_charset)
415 lparams.append((key, '"%s"' % qval))
416 else:
417 try:
418 qval = quoted_string(val)
419 except ValueError:
420 qval = "".join(
421 (_charset, "''", quote(val, "", encoding=_charset))
422 )
423 lparams.append((key + "*", qval))
424 else:
425 lparams.append((key, '"%s"' % qval))
426 else:
427 qval = val.replace("\\", "\\\\").replace('"', '\\"')
428 lparams.append((key, '"%s"' % qval))
429 sparams = "; ".join("=".join(pair) for pair in lparams)
430 value = "; ".join((value, sparams))
431 return value
434def is_expected_content_type(
435 response_content_type: str, expected_content_type: str
436) -> bool:
437 """Checks if received content type is processable as an expected one.
439 Both arguments should be given without parameters.
440 """
441 if expected_content_type == "application/json":
442 return json_re.match(response_content_type) is not None
443 return expected_content_type in response_content_type
446class _TSelf(Protocol, Generic[_T]):
447 _cache: Dict[str, _T]
450class reify(Generic[_T]):
451 """Use as a class method decorator.
453 It operates almost exactly like
454 the Python `@property` decorator, but it puts the result of the
455 method it decorates into the instance dict after the first call,
456 effectively replacing the function it decorates with an instance
457 variable. It is, in Python parlance, a data descriptor.
458 """
460 def __init__(self, wrapped: Callable[..., _T]) -> None:
461 self.wrapped = wrapped
462 self.__doc__ = wrapped.__doc__
463 self.name = wrapped.__name__
465 def __get__(self, inst: _TSelf[_T], owner: Optional[Type[Any]] = None) -> _T:
466 try:
467 try:
468 return inst._cache[self.name]
469 except KeyError:
470 val = self.wrapped(inst)
471 inst._cache[self.name] = val
472 return val
473 except AttributeError:
474 if inst is None:
475 return self
476 raise
478 def __set__(self, inst: _TSelf[_T], value: _T) -> None:
479 raise AttributeError("reified property is read-only")
482reify_py = reify
484try:
485 from ._helpers import reify as reify_c
487 if not NO_EXTENSIONS:
488 reify = reify_c # type: ignore[misc,assignment]
489except ImportError:
490 pass
492_ipv4_pattern = (
493 r"^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}"
494 r"(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$"
495)
496_ipv6_pattern = (
497 r"^(?:(?:(?:[A-F0-9]{1,4}:){6}|(?=(?:[A-F0-9]{0,4}:){0,6}"
498 r"(?:[0-9]{1,3}\.){3}[0-9]{1,3}$)(([0-9A-F]{1,4}:){0,5}|:)"
499 r"((:[0-9A-F]{1,4}){1,5}:|:)|::(?:[A-F0-9]{1,4}:){5})"
500 r"(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){3}"
501 r"(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])|(?:[A-F0-9]{1,4}:){7}"
502 r"[A-F0-9]{1,4}|(?=(?:[A-F0-9]{0,4}:){0,7}[A-F0-9]{0,4}$)"
503 r"(([0-9A-F]{1,4}:){1,7}|:)((:[0-9A-F]{1,4}){1,7}|:)|(?:[A-F0-9]{1,4}:){7}"
504 r":|:(:[A-F0-9]{1,4}){7})$"
505)
506_ipv4_regex = re.compile(_ipv4_pattern)
507_ipv6_regex = re.compile(_ipv6_pattern, flags=re.IGNORECASE)
508_ipv4_regexb = re.compile(_ipv4_pattern.encode("ascii"))
509_ipv6_regexb = re.compile(_ipv6_pattern.encode("ascii"), flags=re.IGNORECASE)
512def _is_ip_address(
513 regex: Pattern[str], regexb: Pattern[bytes], host: Optional[Union[str, bytes]]
514) -> bool:
515 if host is None:
516 return False
517 if isinstance(host, str):
518 return bool(regex.match(host))
519 elif isinstance(host, (bytes, bytearray, memoryview)):
520 return bool(regexb.match(host))
521 else:
522 raise TypeError(f"{host} [{type(host)}] is not a str or bytes")
525is_ipv4_address = functools.partial(_is_ip_address, _ipv4_regex, _ipv4_regexb)
526is_ipv6_address = functools.partial(_is_ip_address, _ipv6_regex, _ipv6_regexb)
529def is_ip_address(host: Optional[Union[str, bytes, bytearray, memoryview]]) -> bool:
530 return is_ipv4_address(host) or is_ipv6_address(host)
533def next_whole_second() -> datetime.datetime:
534 """Return current time rounded up to the next whole second."""
535 return datetime.datetime.now(datetime.timezone.utc).replace(
536 microsecond=0
537 ) + datetime.timedelta(seconds=0)
540_cached_current_datetime: Optional[int] = None
541_cached_formatted_datetime = ""
544def rfc822_formatted_time() -> str:
545 global _cached_current_datetime
546 global _cached_formatted_datetime
548 now = int(time.time())
549 if now != _cached_current_datetime:
550 # Weekday and month names for HTTP date/time formatting;
551 # always English!
552 # Tuples are constants stored in codeobject!
553 _weekdayname = ("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun")
554 _monthname = (
555 "", # Dummy so we can use 1-based month numbers
556 "Jan",
557 "Feb",
558 "Mar",
559 "Apr",
560 "May",
561 "Jun",
562 "Jul",
563 "Aug",
564 "Sep",
565 "Oct",
566 "Nov",
567 "Dec",
568 )
570 year, month, day, hh, mm, ss, wd, *tail = time.gmtime(now)
571 _cached_formatted_datetime = "%s, %02d %3s %4d %02d:%02d:%02d GMT" % (
572 _weekdayname[wd],
573 day,
574 _monthname[month],
575 year,
576 hh,
577 mm,
578 ss,
579 )
580 _cached_current_datetime = now
581 return _cached_formatted_datetime
584def _weakref_handle(info: "Tuple[weakref.ref[object], str]") -> None:
585 ref, name = info
586 ob = ref()
587 if ob is not None:
588 with suppress(Exception):
589 getattr(ob, name)()
592def weakref_handle(
593 ob: object,
594 name: str,
595 timeout: Optional[float],
596 loop: asyncio.AbstractEventLoop,
597 timeout_ceil_threshold: float = 5,
598) -> Optional[asyncio.TimerHandle]:
599 if timeout is not None and timeout > 0:
600 when = loop.time() + timeout
601 if timeout >= timeout_ceil_threshold:
602 when = ceil(when)
604 return loop.call_at(when, _weakref_handle, (weakref.ref(ob), name))
605 return None
608def call_later(
609 cb: Callable[[], Any],
610 timeout: Optional[float],
611 loop: asyncio.AbstractEventLoop,
612 timeout_ceil_threshold: float = 5,
613) -> Optional[asyncio.TimerHandle]:
614 if timeout is not None and timeout > 0:
615 when = loop.time() + timeout
616 if timeout > timeout_ceil_threshold:
617 when = ceil(when)
618 return loop.call_at(when, cb)
619 return None
622class TimeoutHandle:
623 """Timeout handle"""
625 def __init__(
626 self,
627 loop: asyncio.AbstractEventLoop,
628 timeout: Optional[float],
629 ceil_threshold: float = 5,
630 ) -> None:
631 self._timeout = timeout
632 self._loop = loop
633 self._ceil_threshold = ceil_threshold
634 self._callbacks: List[
635 Tuple[Callable[..., None], Tuple[Any, ...], Dict[str, Any]]
636 ] = []
638 def register(
639 self, callback: Callable[..., None], *args: Any, **kwargs: Any
640 ) -> None:
641 self._callbacks.append((callback, args, kwargs))
643 def close(self) -> None:
644 self._callbacks.clear()
646 def start(self) -> Optional[asyncio.Handle]:
647 timeout = self._timeout
648 if timeout is not None and timeout > 0:
649 when = self._loop.time() + timeout
650 if timeout >= self._ceil_threshold:
651 when = ceil(when)
652 return self._loop.call_at(when, self.__call__)
653 else:
654 return None
656 def timer(self) -> "BaseTimerContext":
657 if self._timeout is not None and self._timeout > 0:
658 timer = TimerContext(self._loop)
659 self.register(timer.timeout)
660 return timer
661 else:
662 return TimerNoop()
664 def __call__(self) -> None:
665 for cb, args, kwargs in self._callbacks:
666 with suppress(Exception):
667 cb(*args, **kwargs)
669 self._callbacks.clear()
672class BaseTimerContext(ContextManager["BaseTimerContext"]):
673 def assert_timeout(self) -> None:
674 """Raise TimeoutError if timeout has been exceeded."""
677class TimerNoop(BaseTimerContext):
678 def __enter__(self) -> BaseTimerContext:
679 return self
681 def __exit__(
682 self,
683 exc_type: Optional[Type[BaseException]],
684 exc_val: Optional[BaseException],
685 exc_tb: Optional[TracebackType],
686 ) -> None:
687 return
690class TimerContext(BaseTimerContext):
691 """Low resolution timeout context manager"""
693 def __init__(self, loop: asyncio.AbstractEventLoop) -> None:
694 self._loop = loop
695 self._tasks: List[asyncio.Task[Any]] = []
696 self._cancelled = False
698 def assert_timeout(self) -> None:
699 """Raise TimeoutError if timer has already been cancelled."""
700 if self._cancelled:
701 raise asyncio.TimeoutError from None
703 def __enter__(self) -> BaseTimerContext:
704 task = asyncio.current_task(loop=self._loop)
706 if task is None:
707 raise RuntimeError(
708 "Timeout context manager should be used " "inside a task"
709 )
711 if self._cancelled:
712 raise asyncio.TimeoutError from None
714 self._tasks.append(task)
715 return self
717 def __exit__(
718 self,
719 exc_type: Optional[Type[BaseException]],
720 exc_val: Optional[BaseException],
721 exc_tb: Optional[TracebackType],
722 ) -> Optional[bool]:
723 if self._tasks:
724 self._tasks.pop() # type: ignore[unused-awaitable]
726 if exc_type is asyncio.CancelledError and self._cancelled:
727 raise asyncio.TimeoutError from None
728 return None
730 def timeout(self) -> None:
731 if not self._cancelled:
732 for task in set(self._tasks):
733 task.cancel()
735 self._cancelled = True
738def ceil_timeout(
739 delay: Optional[float], ceil_threshold: float = 5
740) -> async_timeout.Timeout:
741 if delay is None or delay <= 0:
742 return async_timeout.timeout(None)
744 loop = asyncio.get_running_loop()
745 now = loop.time()
746 when = now + delay
747 if delay > ceil_threshold:
748 when = ceil(when)
749 return async_timeout.timeout_at(when)
752class HeadersMixin:
753 __slots__ = ("_content_type", "_content_dict", "_stored_content_type")
755 def __init__(self) -> None:
756 super().__init__()
757 self._content_type: Optional[str] = None
758 self._content_dict: Optional[Dict[str, str]] = None
759 self._stored_content_type: Union[str, _SENTINEL] = sentinel
761 def _parse_content_type(self, raw: str) -> None:
762 self._stored_content_type = raw
763 if raw is None:
764 # default value according to RFC 2616
765 self._content_type = "application/octet-stream"
766 self._content_dict = {}
767 else:
768 msg = HeaderParser().parsestr("Content-Type: " + raw)
769 self._content_type = msg.get_content_type()
770 params = msg.get_params(())
771 self._content_dict = dict(params[1:]) # First element is content type again
773 @property
774 def content_type(self) -> str:
775 """The value of content part for Content-Type HTTP header."""
776 raw = self._headers.get(hdrs.CONTENT_TYPE) # type: ignore[attr-defined]
777 if self._stored_content_type != raw:
778 self._parse_content_type(raw)
779 return self._content_type # type: ignore[return-value]
781 @property
782 def charset(self) -> Optional[str]:
783 """The value of charset part for Content-Type HTTP header."""
784 raw = self._headers.get(hdrs.CONTENT_TYPE) # type: ignore[attr-defined]
785 if self._stored_content_type != raw:
786 self._parse_content_type(raw)
787 return self._content_dict.get("charset") # type: ignore[union-attr]
789 @property
790 def content_length(self) -> Optional[int]:
791 """The value of Content-Length HTTP header."""
792 content_length = self._headers.get( # type: ignore[attr-defined]
793 hdrs.CONTENT_LENGTH
794 )
796 if content_length is not None:
797 return int(content_length)
798 else:
799 return None
802def set_result(fut: "asyncio.Future[_T]", result: _T) -> None:
803 if not fut.done():
804 fut.set_result(result)
807def set_exception(fut: "asyncio.Future[_T]", exc: BaseException) -> None:
808 if not fut.done():
809 fut.set_exception(exc)
812@functools.total_ordering
813class AppKey(Generic[_T]):
814 """Keys for static typing support in Application."""
816 __slots__ = ("_name", "_t", "__orig_class__")
818 # This may be set by Python when instantiating with a generic type. We need to
819 # support this, in order to support types that are not concrete classes,
820 # like Iterable, which can't be passed as the second parameter to __init__.
821 __orig_class__: Type[object]
823 def __init__(self, name: str, t: Optional[Type[_T]] = None):
824 # Prefix with module name to help deduplicate key names.
825 frame = inspect.currentframe()
826 while frame:
827 if frame.f_code.co_name == "<module>":
828 module: str = frame.f_globals["__name__"]
829 break
830 frame = frame.f_back
831 else:
832 raise RuntimeError("Failed to get module name.")
834 # https://github.com/python/mypy/issues/14209
835 self._name = module + "." + name # type: ignore[possibly-undefined]
836 self._t = t
838 def __lt__(self, other: object) -> bool:
839 if isinstance(other, AppKey):
840 return self._name < other._name
841 return True # Order AppKey above other types.
843 def __repr__(self) -> str:
844 t = self._t
845 if t is None:
846 with suppress(AttributeError):
847 # Set to type arg.
848 t = get_args(self.__orig_class__)[0]
850 if t is None:
851 t_repr = "<<Unkown>>"
852 elif isinstance(t, type):
853 if t.__module__ == "builtins":
854 t_repr = t.__qualname__
855 else:
856 t_repr = f"{t.__module__}.{t.__qualname__}"
857 else:
858 t_repr = repr(t)
859 return f"<AppKey({self._name}, type={t_repr})>"
862@final
863class ChainMapProxy(Mapping[Union[str, AppKey[Any]], Any]):
864 __slots__ = ("_maps",)
866 def __init__(self, maps: Iterable[Mapping[Union[str, AppKey[Any]], Any]]) -> None:
867 self._maps = tuple(maps)
869 def __init_subclass__(cls) -> None:
870 raise TypeError(
871 "Inheritance class {} from ChainMapProxy "
872 "is forbidden".format(cls.__name__)
873 )
875 @overload # type: ignore[override]
876 def __getitem__(self, key: AppKey[_T]) -> _T:
877 ...
879 @overload
880 def __getitem__(self, key: str) -> Any:
881 ...
883 def __getitem__(self, key: Union[str, AppKey[_T]]) -> Any:
884 for mapping in self._maps:
885 try:
886 return mapping[key]
887 except KeyError:
888 pass
889 raise KeyError(key)
891 @overload # type: ignore[override]
892 def get(self, key: AppKey[_T], default: _S) -> Union[_T, _S]:
893 ...
895 @overload
896 def get(self, key: AppKey[_T], default: None = ...) -> Optional[_T]:
897 ...
899 @overload
900 def get(self, key: str, default: Any = ...) -> Any:
901 ...
903 def get(self, key: Union[str, AppKey[_T]], default: Any = None) -> Any:
904 try:
905 return self[key]
906 except KeyError:
907 return default
909 def __len__(self) -> int:
910 # reuses stored hash values if possible
911 return len(set().union(*self._maps))
913 def __iter__(self) -> Iterator[Union[str, AppKey[Any]]]:
914 d: Dict[Union[str, AppKey[Any]], Any] = {}
915 for mapping in reversed(self._maps):
916 # reuses stored hash values if possible
917 d.update(mapping)
918 return iter(d)
920 def __contains__(self, key: object) -> bool:
921 return any(key in m for m in self._maps)
923 def __bool__(self) -> bool:
924 return any(self._maps)
926 def __repr__(self) -> str:
927 content = ", ".join(map(repr, self._maps))
928 return f"ChainMapProxy({content})"
931class CookieMixin:
932 # The `_cookies` slots is not defined here because non-empty slots cannot
933 # be combined with an Exception base class, as is done in HTTPException.
934 # CookieMixin subclasses with slots should define the `_cookies`
935 # slot themselves.
936 __slots__ = ()
938 def __init__(self) -> None:
939 super().__init__()
940 # Mypy doesn't like that _cookies isn't in __slots__.
941 # See the comment on this class's __slots__ for why this is OK.
942 self._cookies: SimpleCookie[str] = SimpleCookie() # type: ignore[misc]
944 @property
945 def cookies(self) -> "SimpleCookie[str]":
946 return self._cookies
948 def set_cookie(
949 self,
950 name: str,
951 value: str,
952 *,
953 expires: Optional[str] = None,
954 domain: Optional[str] = None,
955 max_age: Optional[Union[int, str]] = None,
956 path: str = "/",
957 secure: Optional[bool] = None,
958 httponly: Optional[bool] = None,
959 version: Optional[str] = None,
960 samesite: Optional[str] = None,
961 ) -> None:
962 """Set or update response cookie.
964 Sets new cookie or updates existent with new value.
965 Also updates only those params which are not None.
966 """
967 old = self._cookies.get(name)
968 if old is not None and old.coded_value == "":
969 # deleted cookie
970 self._cookies.pop(name, None)
972 self._cookies[name] = value
973 c = self._cookies[name]
975 if expires is not None:
976 c["expires"] = expires
977 elif c.get("expires") == "Thu, 01 Jan 1970 00:00:00 GMT":
978 del c["expires"]
980 if domain is not None:
981 c["domain"] = domain
983 if max_age is not None:
984 c["max-age"] = str(max_age)
985 elif "max-age" in c:
986 del c["max-age"]
988 c["path"] = path
990 if secure is not None:
991 c["secure"] = secure
992 if httponly is not None:
993 c["httponly"] = httponly
994 if version is not None:
995 c["version"] = version
996 if samesite is not None:
997 c["samesite"] = samesite
999 if DEBUG:
1000 cookie_length = len(c.output(header="")[1:])
1001 if cookie_length > COOKIE_MAX_LENGTH:
1002 warnings.warn(
1003 "The size of is too large, it might get ignored by the client.",
1004 UserWarning,
1005 stacklevel=2,
1006 )
1008 def del_cookie(
1009 self, name: str, *, domain: Optional[str] = None, path: str = "/"
1010 ) -> None:
1011 """Delete cookie.
1013 Creates new empty expired cookie.
1014 """
1015 # TODO: do we need domain/path here?
1016 self._cookies.pop(name, None)
1017 self.set_cookie(
1018 name,
1019 "",
1020 max_age=0,
1021 expires="Thu, 01 Jan 1970 00:00:00 GMT",
1022 domain=domain,
1023 path=path,
1024 )
1027def populate_with_cookies(
1028 headers: "CIMultiDict[str]", cookies: "SimpleCookie[str]"
1029) -> None:
1030 for cookie in cookies.values():
1031 value = cookie.output(header="")[1:]
1032 headers.add(hdrs.SET_COOKIE, value)
1035# https://tools.ietf.org/html/rfc7232#section-2.3
1036_ETAGC = r"[!#-}\x80-\xff]+"
1037_ETAGC_RE = re.compile(_ETAGC)
1038_QUOTED_ETAG = rf'(W/)?"({_ETAGC})"'
1039QUOTED_ETAG_RE = re.compile(_QUOTED_ETAG)
1040LIST_QUOTED_ETAG_RE = re.compile(rf"({_QUOTED_ETAG})(?:\s*,\s*|$)|(.)")
1042ETAG_ANY = "*"
1045@dataclasses.dataclass(frozen=True)
1046class ETag:
1047 value: str
1048 is_weak: bool = False
1051def validate_etag_value(value: str) -> None:
1052 if value != ETAG_ANY and not _ETAGC_RE.fullmatch(value):
1053 raise ValueError(
1054 f"Value {value!r} is not a valid etag. Maybe it contains '\"'?"
1055 )
1058def parse_http_date(date_str: Optional[str]) -> Optional[datetime.datetime]:
1059 """Process a date string, return a datetime object"""
1060 if date_str is not None:
1061 timetuple = parsedate(date_str)
1062 if timetuple is not None:
1063 with suppress(ValueError):
1064 return datetime.datetime(*timetuple[:6], tzinfo=datetime.timezone.utc)
1065 return None