Coverage for /pythoncovmergedfiles/medio/medio/src/aiohttp/aiohttp/helpers.py: 37%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

576 statements  

1"""Various helper functions""" 

2 

3import asyncio 

4import base64 

5import contextlib 

6import dataclasses 

7import datetime 

8import enum 

9import functools 

10import inspect 

11import netrc 

12import os 

13import platform 

14import re 

15import sys 

16import time 

17import warnings 

18import weakref 

19from collections.abc import Callable, Iterable, Iterator, Mapping 

20from contextlib import suppress 

21from email.message import EmailMessage 

22from email.parser import HeaderParser 

23from email.policy import HTTP 

24from email.utils import parsedate 

25from http.cookies import SimpleCookie 

26from math import ceil 

27from pathlib import Path 

28from types import MappingProxyType, TracebackType 

29from typing import ( 

30 TYPE_CHECKING, 

31 Any, 

32 ContextManager, 

33 Generic, 

34 Protocol, 

35 TypeVar, 

36 Union, 

37 final, 

38 get_args, 

39 overload, 

40) 

41from urllib.parse import quote 

42from urllib.request import getproxies, proxy_bypass 

43 

44from multidict import CIMultiDict, MultiDict, MultiDictProxy 

45from propcache.api import under_cached_property as reify 

46from yarl import URL 

47 

48from . import hdrs 

49from .log import client_logger 

50from .typedefs import PathLike # noqa 

51 

52if sys.version_info >= (3, 11): 

53 import asyncio as async_timeout 

54else: 

55 import async_timeout 

56 

57if TYPE_CHECKING: 

58 from dataclasses import dataclass as frozen_dataclass_decorator 

59else: 

60 frozen_dataclass_decorator = functools.partial( 

61 dataclasses.dataclass, frozen=True, slots=True 

62 ) 

63 

64__all__ = ("ChainMapProxy", "ETag", "frozen_dataclass_decorator", "reify") 

65 

66# This is the default size/limit for several operations. 

67# Matches the max size we receive from sockets: 

68# https://github.com/python/cpython/blob/1857a40807daeae3a1bf5efb682de9c9ae6df845/Lib/asyncio/selector_events.py#L766 

69DEFAULT_CHUNK_SIZE = 2**18 # 256 KiB 

70COOKIE_MAX_LENGTH = 4096 

71_QUOTED_PAIR_SUB = re.compile(r"\\(.)") 

72_QUOTED_STRING = r'"(?:[^"\\]|\\.)*"' 

73_ESCAPED_COMMENT = r"(?:[^()\\]|\\.)*" 

74# Matches one element in a comma-separated header list. 

75# Group 1: content of a top-level quoted-string (quotes stripped). 

76# Group 2: an unquoted element (may contain parameter quoted-strings / comments). 

77_LIST_ELEMENT_RE = re.compile( 

78 rf""" 

79 [ \t]* 

80 (?: 

81 "( (?:[^"\\]|\\.)* )" # group 1: top-level quoted-string 

82 | ( # group 2: unquoted element 

83 (?: 

84 (?<=[^\s]=) {_QUOTED_STRING} # parameter quoted value 

85 | (?<=\s) \( {_ESCAPED_COMMENT} \) # comment 

86 | [^,] # any non-comma character 

87 )+? 

88 ) 

89 ) 

90 [ \t]* (?:,|\Z) 

91 """, 

92 re.VERBOSE, 

93) 

94# Finds parameter quoted-strings and comments inside an unquoted element for unescaping. 

95_PROTECTED_RE = re.compile( 

96 rf""" 

97 (?<=[^\s]=) {_QUOTED_STRING} # parameter quoted-string 

98 | (?<=\s) \( {_ESCAPED_COMMENT} \) # comment 

99 """, 

100 re.VERBOSE, 

101) 

102 

103_T = TypeVar("_T") 

104_S = TypeVar("_S") 

105 

106_SENTINEL = enum.Enum("_SENTINEL", "sentinel") 

107sentinel = _SENTINEL.sentinel 

108 

109NO_EXTENSIONS = bool(os.environ.get("AIOHTTP_NO_EXTENSIONS")) 

110 

111# https://datatracker.ietf.org/doc/html/rfc9112#section-6.3-2.1 

112EMPTY_BODY_STATUS_CODES = frozenset((204, 304, *range(100, 200))) 

113# https://datatracker.ietf.org/doc/html/rfc9112#section-6.3-2.1 

114# https://datatracker.ietf.org/doc/html/rfc9112#section-6.3-2.2 

115EMPTY_BODY_METHODS = frozenset({hdrs.METH_HEAD}) 

116 

117DEBUG = sys.flags.dev_mode or ( 

118 not sys.flags.ignore_environment and bool(os.environ.get("PYTHONASYNCIODEBUG")) 

119) 

120 

121 

122EMPTY_SCHEMA_SET = frozenset({""}) 

123HTTP_SCHEMA_SET = frozenset({"http", "https"}) 

124WS_SCHEMA_SET = frozenset({"ws", "wss"}) 

125HTTP_AND_EMPTY_SCHEMA_SET = HTTP_SCHEMA_SET | EMPTY_SCHEMA_SET 

126HIGH_LEVEL_SCHEMA_SET = HTTP_AND_EMPTY_SCHEMA_SET | WS_SCHEMA_SET 

127 

128 

129CHAR = {chr(i) for i in range(0, 128)} 

130CTL = {chr(i) for i in range(0, 32)} | { 

131 chr(127), 

132} 

133SEPARATORS = { 

134 "(", 

135 ")", 

136 "<", 

137 ">", 

138 "@", 

139 ",", 

140 ";", 

141 ":", 

142 "\\", 

143 '"', 

144 "/", 

145 "[", 

146 "]", 

147 "?", 

148 "=", 

149 "{", 

150 "}", 

151 " ", 

152 chr(9), 

153} 

154TOKEN = CHAR ^ CTL ^ SEPARATORS 

155 

156 

157json_re = re.compile(r"^(?:application/|[\w.-]+/[\w.+-]+?\+)json$", re.IGNORECASE) 

158 

159 

160def encode_basic_auth(login: str, password: str = "", encoding: str = "utf-8") -> str: 

161 """Encode HTTP Basic Authentication credentials as an Authorization header value. 

162 

163 Returns a string of the form ``"Basic <base64>"`` suitable for use as the 

164 value of the ``Authorization`` (or ``Proxy-Authorization``) header. 

165 """ 

166 if ":" in login: 

167 raise ValueError('A ":" is not allowed in login (RFC 7617#section-2)') 

168 creds = f"{login}:{password}".encode(encoding) 

169 return "Basic " + base64.b64encode(creds).decode(encoding) 

170 

171 

172def strip_auth_from_url(url: URL) -> tuple[URL, str | None]: 

173 """Strip user/password from a URL and return the Authorization header value. 

174 

175 Returns a tuple of ``(url_without_credentials, authorization_header_value)``. 

176 The header value is ``None`` if no credentials were present. 

177 """ 

178 # Check raw_user and raw_password first as yarl is likely 

179 # to already have these values parsed from the netloc in the cache. 

180 if url.raw_user is None and url.raw_password is None: 

181 return url, None 

182 return url.with_user(None), encode_basic_auth(url.user or "", url.password or "") 

183 

184 

185def netrc_from_env() -> netrc.netrc | None: 

186 """Load netrc from file. 

187 

188 Attempt to load it from the path specified by the env-var 

189 NETRC or in the default location in the user's home directory. 

190 

191 Returns None if it couldn't be found or fails to parse. 

192 """ 

193 netrc_env = os.environ.get("NETRC") 

194 

195 if netrc_env is not None: 

196 netrc_path = Path(netrc_env) 

197 else: 

198 try: 

199 home_dir = Path.home() 

200 except RuntimeError as e: 

201 # if pathlib can't resolve home, it may raise a RuntimeError 

202 client_logger.debug( 

203 "Could not resolve home directory when " 

204 "trying to look for .netrc file: %s", 

205 e, 

206 ) 

207 return None 

208 

209 netrc_path = home_dir / ( 

210 "_netrc" if platform.system() == "Windows" else ".netrc" 

211 ) 

212 

213 try: 

214 return netrc.netrc(str(netrc_path)) 

215 except netrc.NetrcParseError as e: 

216 client_logger.warning("Could not parse .netrc file: %s", e) 

217 except OSError as e: 

218 netrc_exists = False 

219 with contextlib.suppress(OSError): 

220 netrc_exists = netrc_path.is_file() 

221 # we couldn't read the file (doesn't exist, permissions, etc.) 

222 if netrc_env or netrc_exists: 

223 # only warn if the environment wanted us to load it, 

224 # or it appears like the default file does actually exist 

225 client_logger.warning("Could not read .netrc file: %s", e) 

226 

227 return None 

228 

229 

230@frozen_dataclass_decorator 

231class ProxyInfo: 

232 proxy: URL 

233 proxy_auth: str | None 

234 

235 

236def _auth_header_from_netrc(netrc_obj: netrc.netrc | None, host: str) -> str: 

237 """Return a ``Proxy-Authorization`` header value for ``host`` from netrc. 

238 

239 :raises LookupError: if ``netrc_obj`` is :py:data:`None` or if no 

240 entry is found for the ``host``. 

241 """ 

242 if netrc_obj is None: 

243 raise LookupError("No .netrc file found") 

244 auth_from_netrc = netrc_obj.authenticators(host) 

245 

246 if auth_from_netrc is None: 

247 raise LookupError(f"No entry for {host!s} found in the `.netrc` file.") 

248 login, account, password = auth_from_netrc 

249 

250 # TODO(PY311): username = login or account 

251 # Up to python 3.10, account could be None if not specified, 

252 # and login will be empty string if not specified. From 3.11, 

253 # login and account will be empty string if not specified. 

254 username = login if (login or account is None) else account 

255 

256 # TODO(PY311): Remove this, as password will be empty string 

257 # if not specified 

258 if password is None: 

259 password = "" # type: ignore[unreachable] 

260 

261 return encode_basic_auth(username, password) 

262 

263 

264def proxies_from_env() -> dict[str, ProxyInfo]: 

265 proxy_urls = { 

266 k: URL(v) 

267 for k, v in getproxies().items() 

268 if k in ("http", "https", "ws", "wss") 

269 } 

270 netrc_obj = netrc_from_env() 

271 stripped = {k: strip_auth_from_url(v) for k, v in proxy_urls.items()} 

272 ret = {} 

273 for proto, val in stripped.items(): 

274 proxy, auth = val 

275 if proxy.scheme in ("https", "wss"): 

276 client_logger.warning( 

277 "%s proxies %s are not supported, ignoring", proxy.scheme.upper(), proxy 

278 ) 

279 continue 

280 if netrc_obj and auth is None: 

281 if proxy.host is not None: 

282 try: 

283 auth = _auth_header_from_netrc(netrc_obj, proxy.host) 

284 except LookupError: 

285 auth = None 

286 ret[proto] = ProxyInfo(proxy, auth) 

287 return ret 

288 

289 

290def get_env_proxy_for_url(url: URL) -> tuple[URL, str | None]: 

291 """Get a permitted proxy for the given URL from the env.""" 

292 if url.host is not None and proxy_bypass(url.host): 

293 raise LookupError(f"Proxying is disallowed for `{url.host!r}`") 

294 

295 proxies_in_env = proxies_from_env() 

296 try: 

297 proxy_info = proxies_in_env[url.scheme] 

298 except KeyError: 

299 raise LookupError(f"No proxies found for `{url!s}` in the env") 

300 else: 

301 return proxy_info.proxy, proxy_info.proxy_auth 

302 

303 

304@frozen_dataclass_decorator 

305class MimeType: 

306 type: str 

307 subtype: str 

308 suffix: str 

309 parameters: "MultiDictProxy[str]" 

310 

311 

312@functools.lru_cache(maxsize=56) 

313def parse_mimetype(mimetype: str) -> MimeType: 

314 """Parses a MIME type into its components. 

315 

316 mimetype is a MIME type string. 

317 

318 Returns a MimeType object. 

319 

320 Example: 

321 

322 >>> parse_mimetype('text/html; charset=utf-8') 

323 MimeType(type='text', subtype='html', suffix='', 

324 parameters={'charset': 'utf-8'}) 

325 

326 """ 

327 if not mimetype: 

328 return MimeType( 

329 type="", subtype="", suffix="", parameters=MultiDictProxy(MultiDict()) 

330 ) 

331 

332 parts = mimetype.split(";") 

333 params: MultiDict[str] = MultiDict() 

334 for item in parts[1:]: 

335 if not item.strip(): 

336 continue 

337 key, _, value = item.partition("=") 

338 params.add(key.lower().strip(), value.strip(' "')) 

339 

340 fulltype = parts[0].strip().lower() 

341 if fulltype == "*": 

342 fulltype = "*/*" 

343 

344 mtype, _, stype = fulltype.partition("/") 

345 stype, _, suffix = stype.partition("+") 

346 

347 return MimeType( 

348 type=mtype, subtype=stype, suffix=suffix, parameters=MultiDictProxy(params) 

349 ) 

350 

351 

352class EnsureOctetStream(EmailMessage): 

353 def __init__(self) -> None: 

354 super().__init__() 

355 # https://www.rfc-editor.org/rfc/rfc9110#section-8.3-5 

356 self.set_default_type("application/octet-stream") 

357 

358 def get_content_type(self) -> str: 

359 """Re-implementation from Message 

360 

361 Returns application/octet-stream in place of plain/text when 

362 value is wrong. 

363 

364 The way this class is used guarantees that content-type will 

365 be present so simplify the checks wrt to the base implementation. 

366 """ 

367 value = self.get("content-type", "").lower() 

368 

369 # Based on the implementation of _splitparam in the standard library 

370 ctype, _, _ = value.partition(";") 

371 ctype = ctype.strip() 

372 if ctype.count("/") != 1: 

373 return self.get_default_type() 

374 return ctype 

375 

376 

377@functools.lru_cache(maxsize=56) 

378def parse_content_type(raw: str) -> tuple[str, MappingProxyType[str, str]]: 

379 """Parse Content-Type header. 

380 

381 Returns a tuple of the parsed content type and a 

382 MappingProxyType of parameters. The default returned value 

383 is `application/octet-stream` 

384 """ 

385 msg = HeaderParser(EnsureOctetStream, policy=HTTP).parsestr(f"Content-Type: {raw}") 

386 content_type = msg.get_content_type() 

387 params = msg.get_params(()) 

388 content_dict = dict(params[1:]) # First element is content type again 

389 return content_type, MappingProxyType(content_dict) 

390 

391 

392def guess_filename(obj: Any, default: str | None = None) -> str | None: 

393 name = getattr(obj, "name", None) 

394 if name and isinstance(name, str) and name[0] != "<" and name[-1] != ">": 

395 return Path(name).name 

396 return default 

397 

398 

399not_qtext_re = re.compile(r"[^\041\043-\133\135-\176]") 

400QCONTENT = {chr(i) for i in range(0x20, 0x7F)} | {"\t"} 

401 

402 

403def quoted_string(content: str) -> str: 

404 """Return 7-bit content as quoted-string. 

405 

406 Format content into a quoted-string as defined in RFC5322 for 

407 Internet Message Format. Notice that this is not the 8-bit HTTP 

408 format, but the 7-bit email format. Content must be in usascii or 

409 a ValueError is raised. 

410 """ 

411 if not (QCONTENT > set(content)): 

412 raise ValueError(f"bad content for quoted-string {content!r}") 

413 return not_qtext_re.sub(lambda x: "\\" + x.group(0), content) 

414 

415 

416def content_disposition_header( 

417 disptype: str, 

418 quote_fields: bool = True, 

419 _charset: str = "utf-8", 

420 params: dict[str, str] | None = None, 

421) -> str: 

422 """Sets ``Content-Disposition`` header for MIME. 

423 

424 This is the MIME payload Content-Disposition header from RFC 2183 

425 and RFC 7579 section 4.2, not the HTTP Content-Disposition from 

426 RFC 6266. 

427 

428 disptype is a disposition type: inline, attachment, form-data. 

429 Should be valid extension token (see RFC 2183) 

430 

431 quote_fields performs value quoting to 7-bit MIME headers 

432 according to RFC 7578. Set to quote_fields to False if recipient 

433 can take 8-bit file names and field values. 

434 

435 _charset specifies the charset to use when quote_fields is True. 

436 

437 params is a dict with disposition params. 

438 """ 

439 if not disptype or not (TOKEN > set(disptype)): 

440 raise ValueError(f"bad content disposition type {disptype!r}") 

441 

442 value = disptype 

443 if params: 

444 lparams = [] 

445 for key, val in params.items(): 

446 if not key or not (TOKEN > set(key)): 

447 raise ValueError(f"bad content disposition parameter {key!r}={val!r}") 

448 if quote_fields: 

449 if key.lower() == "filename": 

450 qval = quote(val, "", encoding=_charset) 

451 lparams.append((key, '"%s"' % qval)) 

452 else: 

453 try: 

454 qval = quoted_string(val) 

455 except ValueError: 

456 qval = "".join( 

457 (_charset, "''", quote(val, "", encoding=_charset)) 

458 ) 

459 lparams.append((key + "*", qval)) 

460 else: 

461 lparams.append((key, '"%s"' % qval)) 

462 else: 

463 qval = val.replace("\\", "\\\\").replace('"', '\\"') 

464 lparams.append((key, '"%s"' % qval)) 

465 sparams = "; ".join("=".join(pair) for pair in lparams) 

466 value = "; ".join((value, sparams)) 

467 return value 

468 

469 

470def is_expected_content_type( 

471 response_content_type: str, expected_content_type: str 

472) -> bool: 

473 """Checks if received content type is processable as an expected one. 

474 

475 Both arguments should be given without parameters. 

476 """ 

477 if expected_content_type == "application/json": 

478 return json_re.match(response_content_type) is not None 

479 return expected_content_type in response_content_type 

480 

481 

482def is_ip_address(host: str | None) -> bool: 

483 """Check if host looks like an IP Address. 

484 

485 This check is only meant as a heuristic to ensure that 

486 a host is not a domain name. 

487 """ 

488 if not host: 

489 return False 

490 # For a host to be an ipv4 address, it must be all numeric. 

491 # The host must contain a colon to be an IPv6 address. 

492 return ":" in host or host.replace(".", "").isdigit() 

493 

494 

495def is_canonical_ipv4_address(host: str) -> bool: 

496 """Check if host is a canonical dotted-quad IPv4 address. 

497 

498 Rejects the legacy numeric forms that ``socket`` still accepts and 

499 maps onto an address, e.g. ``2130706433``, ``017700000001``, ``127.1``. 

500 """ 

501 parts = host.split(".") 

502 if len(parts) != 4: 

503 return False 

504 for part in parts: 

505 # Each octet must be 1-3 ASCII digits; reject unicode digits 

506 # (which ``str.isdigit`` accepts but ``int`` may not), octal 

507 # leading zeros, and values above 255. 

508 if not (1 <= len(part) <= 3) or not part.isascii() or not part.isdigit(): 

509 return False 

510 if part[0] == "0" and len(part) != 1: 

511 return False 

512 if int(part) > 255: 

513 return False 

514 return True 

515 

516 

517_cached_current_datetime: int | None = None 

518_cached_formatted_datetime = "" 

519 

520 

521def rfc822_formatted_time() -> str: 

522 global _cached_current_datetime 

523 global _cached_formatted_datetime 

524 

525 now = int(time.time()) 

526 if now != _cached_current_datetime: 

527 # Weekday and month names for HTTP date/time formatting; 

528 # always English! 

529 # Tuples are constants stored in codeobject! 

530 _weekdayname = ("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun") 

531 _monthname = ( 

532 "", # Dummy so we can use 1-based month numbers 

533 "Jan", 

534 "Feb", 

535 "Mar", 

536 "Apr", 

537 "May", 

538 "Jun", 

539 "Jul", 

540 "Aug", 

541 "Sep", 

542 "Oct", 

543 "Nov", 

544 "Dec", 

545 ) 

546 

547 year, month, day, hh, mm, ss, wd, *tail = time.gmtime(now) 

548 _cached_formatted_datetime = "%s, %02d %3s %4d %02d:%02d:%02d GMT" % ( 

549 _weekdayname[wd], 

550 day, 

551 _monthname[month], 

552 year, 

553 hh, 

554 mm, 

555 ss, 

556 ) 

557 _cached_current_datetime = now 

558 return _cached_formatted_datetime 

559 

560 

561def _weakref_handle(info: "tuple[weakref.ref[object], str]") -> None: 

562 ref, name = info 

563 ob = ref() 

564 if ob is not None: 

565 with suppress(Exception): 

566 getattr(ob, name)() 

567 

568 

569def weakref_handle( 

570 ob: object, 

571 name: str, 

572 timeout: float | None, 

573 loop: asyncio.AbstractEventLoop, 

574 timeout_ceil_threshold: float = 5, 

575) -> asyncio.TimerHandle | None: 

576 if timeout is not None and timeout > 0: 

577 when = loop.time() + timeout 

578 if timeout >= timeout_ceil_threshold: 

579 when = ceil(when) 

580 

581 return loop.call_at(when, _weakref_handle, (weakref.ref(ob), name)) 

582 return None 

583 

584 

585def call_later( 

586 cb: Callable[[], Any], 

587 timeout: float | None, 

588 loop: asyncio.AbstractEventLoop, 

589 timeout_ceil_threshold: float = 5, 

590) -> asyncio.TimerHandle | None: 

591 if timeout is None or timeout <= 0: 

592 return None 

593 now = loop.time() 

594 when = calculate_timeout_when(now, timeout, timeout_ceil_threshold) 

595 return loop.call_at(when, cb) 

596 

597 

598def calculate_timeout_when( 

599 loop_time: float, 

600 timeout: float, 

601 timeout_ceiling_threshold: float, 

602) -> float: 

603 """Calculate when to execute a timeout.""" 

604 when = loop_time + timeout 

605 if timeout > timeout_ceiling_threshold: 

606 return ceil(when) 

607 return when 

608 

609 

610class TimeoutHandle: 

611 """Timeout handle""" 

612 

613 __slots__ = ("_timeout", "_loop", "_ceil_threshold", "_callbacks") 

614 

615 def __init__( 

616 self, 

617 loop: asyncio.AbstractEventLoop, 

618 timeout: float | None, 

619 ceil_threshold: float = 5, 

620 ) -> None: 

621 self._timeout = timeout 

622 self._loop = loop 

623 self._ceil_threshold = ceil_threshold 

624 self._callbacks: list[ 

625 tuple[Callable[..., None], tuple[Any, ...], dict[str, Any]] 

626 ] = [] 

627 

628 def register( 

629 self, callback: Callable[..., None], *args: Any, **kwargs: Any 

630 ) -> None: 

631 self._callbacks.append((callback, args, kwargs)) 

632 

633 def close(self) -> None: 

634 self._callbacks.clear() 

635 

636 def start(self) -> asyncio.TimerHandle | None: 

637 timeout = self._timeout 

638 if timeout is not None and timeout > 0: 

639 when = self._loop.time() + timeout 

640 if timeout >= self._ceil_threshold: 

641 when = ceil(when) 

642 return self._loop.call_at(when, self.__call__) 

643 else: 

644 return None 

645 

646 def timer(self) -> "BaseTimerContext": 

647 if self._timeout is not None and self._timeout > 0: 

648 timer = TimerContext(self._loop) 

649 self.register(timer.timeout) 

650 return timer 

651 else: 

652 return TimerNoop() 

653 

654 def __call__(self) -> None: 

655 for cb, args, kwargs in self._callbacks: 

656 with suppress(Exception): 

657 cb(*args, **kwargs) 

658 

659 self._callbacks.clear() 

660 

661 

662class BaseTimerContext(ContextManager["BaseTimerContext"]): 

663 

664 __slots__ = () 

665 

666 def assert_timeout(self) -> None: 

667 """Raise TimeoutError if timeout has been exceeded.""" 

668 

669 

670class TimerNoop(BaseTimerContext): 

671 

672 __slots__ = () 

673 

674 def __enter__(self) -> BaseTimerContext: 

675 return self 

676 

677 def __exit__( 

678 self, 

679 exc_type: type[BaseException] | None, 

680 exc_val: BaseException | None, 

681 exc_tb: TracebackType | None, 

682 ) -> None: 

683 return 

684 

685 

686class TimerContext(BaseTimerContext): 

687 """Low resolution timeout context manager""" 

688 

689 __slots__ = ("_loop", "_tasks", "_cancelled", "_cancelling") 

690 

691 def __init__(self, loop: asyncio.AbstractEventLoop) -> None: 

692 self._loop = loop 

693 self._tasks: list[asyncio.Task[Any]] = [] 

694 self._cancelled = False 

695 self._cancelling = 0 

696 

697 def assert_timeout(self) -> None: 

698 """Raise TimeoutError if timer has already been cancelled.""" 

699 if self._cancelled: 

700 raise asyncio.TimeoutError from None 

701 

702 def __enter__(self) -> BaseTimerContext: 

703 task = asyncio.current_task(loop=self._loop) 

704 if task is None: 

705 raise RuntimeError("Timeout context manager should be used inside a task") 

706 

707 if sys.version_info >= (3, 11): 

708 # Remember if the task was already cancelling 

709 # so when we __exit__ we can decide if we should 

710 # raise asyncio.TimeoutError or let the cancellation propagate 

711 self._cancelling = task.cancelling() 

712 

713 if self._cancelled: 

714 raise asyncio.TimeoutError from None 

715 

716 self._tasks.append(task) 

717 return self 

718 

719 def __exit__( 

720 self, 

721 exc_type: type[BaseException] | None, 

722 exc_val: BaseException | None, 

723 exc_tb: TracebackType | None, 

724 ) -> bool | None: 

725 enter_task: asyncio.Task[Any] | None = None 

726 if self._tasks: 

727 enter_task = self._tasks.pop() 

728 

729 if exc_type is asyncio.CancelledError and self._cancelled: 

730 assert enter_task is not None 

731 # The timeout was hit, and the task was cancelled 

732 # so we need to uncancel the last task that entered the context manager 

733 # since the cancellation should not leak out of the context manager 

734 if sys.version_info >= (3, 11): 

735 # If the task was already cancelling don't raise 

736 # asyncio.TimeoutError and instead return None 

737 # to allow the cancellation to propagate 

738 if enter_task.uncancel() > self._cancelling: 

739 return None 

740 raise asyncio.TimeoutError from exc_val 

741 return None 

742 

743 def timeout(self) -> None: 

744 if not self._cancelled: 

745 for task in set(self._tasks): 

746 task.cancel() 

747 

748 self._cancelled = True 

749 

750 

751def ceil_timeout( 

752 delay: float | None, ceil_threshold: float = 5 

753) -> async_timeout.Timeout: 

754 if delay is None or delay <= 0: 

755 return async_timeout.timeout(None) 

756 

757 loop = asyncio.get_running_loop() 

758 now = loop.time() 

759 when = now + delay 

760 if delay > ceil_threshold: 

761 when = ceil(when) 

762 return async_timeout.timeout_at(when) 

763 

764 

765class HeadersDictProxy(Mapping[str, str]): 

766 def __init__(self, md: CIMultiDict[str]): 

767 self._md = md 

768 

769 def getall(self, key: str) -> tuple[str, ...]: 

770 val = self.get(key, "") 

771 unescape = _QUOTED_PAIR_SUB.sub 

772 values = [] 

773 for m in _LIST_ELEMENT_RE.finditer(val): 

774 qs = m.group(1) 

775 if qs is not None: 

776 values.append(unescape(r"\1", qs)) 

777 else: 

778 raw = m.group(2).strip() 

779 if raw: 

780 values.append( 

781 _PROTECTED_RE.sub(lambda p: unescape(r"\1", p.group()), raw) 

782 ) 

783 return tuple(values) 

784 

785 def __eq__(self, other: object) -> bool: 

786 return self._md.__eq__(other) 

787 

788 def __getitem__(self, key: str) -> str: 

789 return ", ".join(self._md.getall(key)) 

790 

791 def __iter__(self) -> Iterator[str]: 

792 # We need to deduplicate keys from MultiDict 

793 # But, we also need to retain ordering 

794 seen = set() 

795 for k in self._md.__iter__(): 

796 if k in seen: 

797 continue 

798 seen.add(k) 

799 yield k 

800 

801 def __len__(self) -> int: 

802 return len(set(self._md.keys())) 

803 

804 def __repr__(self) -> str: 

805 body = ", ".join(f"'{k}': {v!r}" for k, v in self.items()) 

806 return f"<{self.__class__.__name__}({body})>" 

807 

808 

809class HeadersMixin: 

810 """Mixin for handling headers.""" 

811 

812 _headers: Mapping[str, str] 

813 _content_type: str | None = None 

814 _content_dict: dict[str, str] | None = None 

815 _stored_content_type: str | None | _SENTINEL = sentinel 

816 

817 def _parse_content_type(self, raw: str | None) -> None: 

818 self._stored_content_type = raw 

819 if raw is None: 

820 # default value according to RFC 2616 

821 self._content_type = "application/octet-stream" 

822 self._content_dict = {} 

823 else: 

824 content_type, content_mapping_proxy = parse_content_type(raw) 

825 self._content_type = content_type 

826 # _content_dict needs to be mutable so we can update it 

827 self._content_dict = content_mapping_proxy.copy() 

828 

829 @property 

830 def content_type(self) -> str: 

831 """The value of content part for Content-Type HTTP header.""" 

832 raw = self._headers.get(hdrs.CONTENT_TYPE) 

833 if self._stored_content_type != raw: 

834 self._parse_content_type(raw) 

835 assert self._content_type is not None 

836 return self._content_type 

837 

838 @property 

839 def charset(self) -> str | None: 

840 """The value of charset part for Content-Type HTTP header.""" 

841 raw = self._headers.get(hdrs.CONTENT_TYPE) 

842 if self._stored_content_type != raw: 

843 self._parse_content_type(raw) 

844 assert self._content_dict is not None 

845 return self._content_dict.get("charset") 

846 

847 @property 

848 def content_length(self) -> int | None: 

849 """The value of Content-Length HTTP header.""" 

850 content_length = self._headers.get(hdrs.CONTENT_LENGTH) 

851 return None if content_length is None else int(content_length) 

852 

853 

854def set_result(fut: "asyncio.Future[_T]", result: _T) -> None: 

855 if not fut.done(): 

856 fut.set_result(result) 

857 

858 

859_EXC_SENTINEL = BaseException() 

860 

861 

862class ErrorableProtocol(Protocol): 

863 def set_exception( 

864 self, 

865 exc: type[BaseException] | BaseException, 

866 exc_cause: BaseException = ..., 

867 ) -> None: ... 

868 

869 

870def set_exception( 

871 fut: Union["asyncio.Future[_T]", ErrorableProtocol], 

872 exc: type[BaseException] | BaseException, 

873 exc_cause: BaseException = _EXC_SENTINEL, 

874) -> None: 

875 """Set future exception. 

876 

877 If the future is marked as complete, this function is a no-op. 

878 

879 :param exc_cause: An exception that is a direct cause of ``exc``. 

880 Only set if provided. 

881 """ 

882 if asyncio.isfuture(fut) and fut.done(): 

883 return 

884 

885 exc_is_sentinel = exc_cause is _EXC_SENTINEL 

886 exc_causes_itself = exc is exc_cause 

887 if not exc_is_sentinel and not exc_causes_itself: 

888 exc.__cause__ = exc_cause 

889 

890 fut.set_exception(exc) 

891 

892 

893@functools.total_ordering 

894class BaseKey(Generic[_T]): 

895 """Base for concrete context storage key classes. 

896 

897 Each storage is provided with its own sub-class for the sake of some additional type safety. 

898 """ 

899 

900 __slots__ = ("_name", "_t", "__orig_class__") 

901 

902 # This may be set by Python when instantiating with a generic type. We need to 

903 # support this, in order to support types that are not concrete classes, 

904 # like Iterable, which can't be passed as the second parameter to __init__. 

905 __orig_class__: type[object] 

906 

907 # TODO(PY314): Change Type to TypeForm (this should resolve unreachable below). 

908 def __init__(self, name: str, t: type[_T] | None = None): 

909 # Prefix with module name to help deduplicate key names. 

910 frame = inspect.currentframe() 

911 while frame: 

912 if frame.f_code.co_name == "<module>": 

913 module: str = frame.f_globals["__name__"] 

914 break 

915 frame = frame.f_back 

916 else: 

917 raise RuntimeError("Failed to get module name.") 

918 

919 # https://github.com/python/mypy/issues/14209 

920 self._name = module + "." + name # type: ignore[possibly-undefined] 

921 self._t = t 

922 

923 def __lt__(self, other: object) -> bool: 

924 if isinstance(other, BaseKey): 

925 return self._name < other._name 

926 return True # Order BaseKey above other types. 

927 

928 def __repr__(self) -> str: 

929 t = self._t 

930 if t is None: 

931 with suppress(AttributeError): 

932 # Set to type arg. 

933 t = get_args(self.__orig_class__)[0] 

934 

935 if t is None: 

936 t_repr = "<<Unknown>>" 

937 elif isinstance(t, type): 

938 if t.__module__ == "builtins": 

939 t_repr = t.__qualname__ 

940 else: 

941 t_repr = f"{t.__module__}.{t.__qualname__}" 

942 else: 

943 t_repr = repr(t) # type: ignore[unreachable] 

944 return f"<{self.__class__.__name__}({self._name}, type={t_repr})>" 

945 

946 

947class AppKey(BaseKey[_T]): 

948 """Keys for static typing support in Application.""" 

949 

950 

951class RequestKey(BaseKey[_T]): 

952 """Keys for static typing support in Request.""" 

953 

954 

955class ResponseKey(BaseKey[_T]): 

956 """Keys for static typing support in Response.""" 

957 

958 

959@final 

960class ChainMapProxy(Mapping[str | AppKey[Any], Any]): 

961 __slots__ = ("_maps",) 

962 

963 def __init__(self, maps: Iterable[Mapping[str | AppKey[Any], Any]]) -> None: 

964 self._maps = tuple(maps) 

965 

966 def __init_subclass__(cls) -> None: 

967 raise TypeError( 

968 f"Inheritance class {cls.__name__} from ChainMapProxy is forbidden" 

969 ) 

970 

971 @overload # type: ignore[override] 

972 def __getitem__(self, key: AppKey[_T]) -> _T: ... 

973 

974 @overload 

975 def __getitem__(self, key: str) -> Any: ... 

976 

977 def __getitem__(self, key: str | AppKey[_T]) -> Any: 

978 for mapping in self._maps: 

979 try: 

980 return mapping[key] 

981 except KeyError: 

982 pass 

983 raise KeyError(key) 

984 

985 @overload # type: ignore[override] 

986 def get(self, key: AppKey[_T], default: _S) -> _T | _S: ... 

987 

988 @overload 

989 def get(self, key: AppKey[_T], default: None = ...) -> _T | None: ... 

990 

991 @overload 

992 def get(self, key: str, default: Any = ...) -> Any: ... 

993 

994 def get(self, key: str | AppKey[_T], default: Any = None) -> Any: 

995 try: 

996 return self[key] 

997 except KeyError: 

998 return default 

999 

1000 def __len__(self) -> int: 

1001 # reuses stored hash values if possible 

1002 return len(set().union(*self._maps)) 

1003 

1004 def __iter__(self) -> Iterator[str | AppKey[Any]]: 

1005 d: dict[str | AppKey[Any], Any] = {} 

1006 for mapping in reversed(self._maps): 

1007 # reuses stored hash values if possible 

1008 d.update(mapping) 

1009 return iter(d) 

1010 

1011 def __contains__(self, key: object) -> bool: 

1012 return any(key in m for m in self._maps) 

1013 

1014 def __bool__(self) -> bool: 

1015 return any(self._maps) 

1016 

1017 def __repr__(self) -> str: 

1018 content = ", ".join(map(repr, self._maps)) 

1019 return f"ChainMapProxy({content})" 

1020 

1021 

1022class CookieMixin: 

1023 """Mixin for handling cookies.""" 

1024 

1025 _cookies: SimpleCookie | None = None 

1026 

1027 @property 

1028 def cookies(self) -> SimpleCookie: 

1029 if self._cookies is None: 

1030 self._cookies = SimpleCookie() 

1031 return self._cookies 

1032 

1033 def set_cookie( 

1034 self, 

1035 name: str, 

1036 value: str, 

1037 *, 

1038 expires: str | None = None, 

1039 domain: str | None = None, 

1040 max_age: int | str | None = None, 

1041 path: str = "/", 

1042 secure: bool | None = None, 

1043 httponly: bool | None = None, 

1044 samesite: str | None = None, 

1045 partitioned: bool | None = None, 

1046 ) -> None: 

1047 """Set or update response cookie. 

1048 

1049 Sets new cookie or updates existent with new value. 

1050 Also updates only those params which are not None. 

1051 """ 

1052 if self._cookies is None: 

1053 self._cookies = SimpleCookie() 

1054 

1055 self._cookies[name] = value 

1056 c = self._cookies[name] 

1057 

1058 if expires is not None: 

1059 c["expires"] = expires 

1060 elif c.get("expires") == "Thu, 01 Jan 1970 00:00:00 GMT": 

1061 del c["expires"] 

1062 

1063 if domain is not None: 

1064 c["domain"] = domain 

1065 

1066 if max_age is not None: 

1067 c["max-age"] = str(max_age) 

1068 elif "max-age" in c: 

1069 del c["max-age"] 

1070 

1071 c["path"] = path 

1072 

1073 if secure is not None: 

1074 c["secure"] = secure 

1075 if httponly is not None: 

1076 c["httponly"] = httponly 

1077 if samesite is not None: 

1078 c["samesite"] = samesite 

1079 

1080 if partitioned is not None: 

1081 c["partitioned"] = partitioned 

1082 

1083 if DEBUG: 

1084 cookie_length = len(c.output(header="")[1:]) 

1085 if cookie_length > COOKIE_MAX_LENGTH: 

1086 warnings.warn( 

1087 "The size of is too large, it might get ignored by the client.", 

1088 UserWarning, 

1089 stacklevel=2, 

1090 ) 

1091 

1092 def del_cookie( 

1093 self, 

1094 name: str, 

1095 *, 

1096 domain: str | None = None, 

1097 path: str = "/", 

1098 secure: bool | None = None, 

1099 httponly: bool | None = None, 

1100 samesite: str | None = None, 

1101 ) -> None: 

1102 """Delete cookie. 

1103 

1104 Creates new empty expired cookie. 

1105 """ 

1106 # TODO: do we need domain/path here? 

1107 if self._cookies is not None: 

1108 self._cookies.pop(name, None) 

1109 self.set_cookie( 

1110 name, 

1111 "", 

1112 max_age=0, 

1113 expires="Thu, 01 Jan 1970 00:00:00 GMT", 

1114 domain=domain, 

1115 path=path, 

1116 secure=secure, 

1117 httponly=httponly, 

1118 samesite=samesite, 

1119 ) 

1120 

1121 

1122def populate_with_cookies(headers: "CIMultiDict[str]", cookies: SimpleCookie) -> None: 

1123 for cookie in cookies.values(): 

1124 value = cookie.output(header="")[1:] 

1125 headers.add(hdrs.SET_COOKIE, value) 

1126 

1127 

1128# https://tools.ietf.org/html/rfc7232#section-2.3 

1129_ETAGC = r"[!\x23-\x7E\x80-\xff]+" 

1130_ETAGC_RE = re.compile(_ETAGC) 

1131_QUOTED_ETAG = rf'(W/)?"({_ETAGC})"' 

1132QUOTED_ETAG_RE = re.compile(_QUOTED_ETAG) 

1133LIST_QUOTED_ETAG_RE = re.compile(rf"({_QUOTED_ETAG})(?:\s*,\s*|$)|(.)") 

1134 

1135ETAG_ANY = "*" 

1136 

1137 

1138@frozen_dataclass_decorator 

1139class ETag: 

1140 value: str 

1141 is_weak: bool = False 

1142 

1143 

1144def validate_etag_value(value: str) -> None: 

1145 if value != ETAG_ANY and not _ETAGC_RE.fullmatch(value): 

1146 raise ValueError( 

1147 f"Value {value!r} is not a valid etag. Maybe it contains '\"'?" 

1148 ) 

1149 

1150 

1151def parse_http_date(date_str: str | None) -> datetime.datetime | None: 

1152 """Process a date string, return a datetime object""" 

1153 if date_str is not None: 

1154 timetuple = parsedate(date_str) 

1155 if timetuple is not None: 

1156 with suppress(ValueError): 

1157 return datetime.datetime(*timetuple[:6], tzinfo=datetime.timezone.utc) 

1158 return None 

1159 

1160 

1161@functools.lru_cache 

1162def must_be_empty_body(method: str, code: int) -> bool: 

1163 """Check if a request must return an empty body.""" 

1164 return ( 

1165 code in EMPTY_BODY_STATUS_CODES 

1166 or method in EMPTY_BODY_METHODS 

1167 or (200 <= code < 300 and method == hdrs.METH_CONNECT) 

1168 ) 

1169 

1170 

1171def should_remove_content_length(method: str, code: int) -> bool: 

1172 """Check if a Content-Length header should be removed. 

1173 

1174 This should always be a subset of must_be_empty_body 

1175 """ 

1176 # https://www.rfc-editor.org/rfc/rfc9110.html#section-8.6-8 

1177 # https://www.rfc-editor.org/rfc/rfc9110.html#section-15.4.5-4 

1178 return code in EMPTY_BODY_STATUS_CODES or ( 

1179 200 <= code < 300 and method == hdrs.METH_CONNECT 

1180 )