You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

789 lines
24 KiB

4 years ago
  1. """Various helper functions"""
  2. import asyncio
  3. import base64
  4. import binascii
  5. import cgi
  6. import datetime
  7. import functools
  8. import inspect
  9. import netrc
  10. import os
  11. import re
  12. import sys
  13. import time
  14. import weakref
  15. from collections import namedtuple
  16. from collections.abc import Mapping as ABCMapping
  17. from contextlib import suppress
  18. from math import ceil
  19. from pathlib import Path
  20. from typing import Any, Dict, List, Optional, Tuple # noqa
  21. from urllib.parse import quote
  22. from urllib.request import getproxies
  23. import async_timeout
  24. import attr
  25. from multidict import MultiDict
  26. from yarl import URL
  27. from . import hdrs
  28. from .abc import AbstractAccessLogger
  29. from .log import client_logger
  30. __all__ = ('BasicAuth', 'ChainMapProxy')
  31. PY_36 = sys.version_info >= (3, 6)
  32. PY_37 = sys.version_info >= (3, 7)
  33. if not PY_37:
  34. import idna_ssl
  35. idna_ssl.patch_match_hostname()
  36. sentinel = object() # type: Any
  37. NO_EXTENSIONS = bool(os.environ.get('AIOHTTP_NO_EXTENSIONS')) # type: bool
  38. # N.B. sys.flags.dev_mode is available on Python 3.7+, use getattr
  39. # for compatibility with older versions
  40. DEBUG = (getattr(sys.flags, 'dev_mode', False) or
  41. (not sys.flags.ignore_environment and
  42. bool(os.environ.get('PYTHONASYNCIODEBUG'))))
  43. CHAR = set(chr(i) for i in range(0, 128))
  44. CTL = set(chr(i) for i in range(0, 32)) | {chr(127), }
  45. SEPARATORS = {'(', ')', '<', '>', '@', ',', ';', ':', '\\', '"', '/', '[', ']',
  46. '?', '=', '{', '}', ' ', chr(9)}
  47. TOKEN = CHAR ^ CTL ^ SEPARATORS
  48. coroutines = asyncio.coroutines
  49. old_debug = coroutines._DEBUG # type: ignore
  50. # prevent "coroutine noop was never awaited" warning.
  51. coroutines._DEBUG = False # type: ignore
  52. @asyncio.coroutine
  53. def noop(*args, **kwargs):
  54. return
  55. coroutines._DEBUG = old_debug # type: ignore
  56. class BasicAuth(namedtuple('BasicAuth', ['login', 'password', 'encoding'])):
  57. """Http basic authentication helper."""
  58. def __new__(cls, login: str,
  59. password: str='',
  60. encoding: str='latin1') -> 'BasicAuth':
  61. if login is None:
  62. raise ValueError('None is not allowed as login value')
  63. if password is None:
  64. raise ValueError('None is not allowed as password value')
  65. if ':' in login:
  66. raise ValueError(
  67. 'A ":" is not allowed in login (RFC 1945#section-11.1)')
  68. return super().__new__(cls, login, password, encoding)
  69. @classmethod
  70. def decode(cls, auth_header: str, encoding: str='latin1') -> 'BasicAuth':
  71. """Create a BasicAuth object from an Authorization HTTP header."""
  72. split = auth_header.strip().split(' ')
  73. if len(split) == 2:
  74. if split[0].strip().lower() != 'basic':
  75. raise ValueError('Unknown authorization method %s' % split[0])
  76. to_decode = split[1]
  77. else:
  78. raise ValueError('Could not parse authorization header.')
  79. try:
  80. username, _, password = base64.b64decode(
  81. to_decode.encode('ascii')
  82. ).decode(encoding).partition(':')
  83. except binascii.Error:
  84. raise ValueError('Invalid base64 encoding.')
  85. return cls(username, password, encoding=encoding)
  86. @classmethod
  87. def from_url(cls, url: URL,
  88. *, encoding: str='latin1') -> Optional['BasicAuth']:
  89. """Create BasicAuth from url."""
  90. if not isinstance(url, URL):
  91. raise TypeError("url should be yarl.URL instance")
  92. if url.user is None:
  93. return None
  94. return cls(url.user, url.password or '', encoding=encoding)
  95. def encode(self) -> str:
  96. """Encode credentials."""
  97. creds = ('%s:%s' % (self.login, self.password)).encode(self.encoding)
  98. return 'Basic %s' % base64.b64encode(creds).decode(self.encoding)
  99. def strip_auth_from_url(url: URL) -> Tuple[URL, Optional[BasicAuth]]:
  100. auth = BasicAuth.from_url(url)
  101. if auth is None:
  102. return url, None
  103. else:
  104. return url.with_user(None), auth
  105. def netrc_from_env():
  106. netrc_obj = None
  107. netrc_path = os.environ.get('NETRC')
  108. try:
  109. if netrc_path is not None:
  110. netrc_path = Path(netrc_path)
  111. else:
  112. home_dir = Path.home()
  113. if os.name == 'nt': # pragma: no cover
  114. netrc_path = home_dir.joinpath('_netrc')
  115. else:
  116. netrc_path = home_dir.joinpath('.netrc')
  117. if netrc_path and netrc_path.is_file():
  118. try:
  119. netrc_obj = netrc.netrc(str(netrc_path))
  120. except (netrc.NetrcParseError, OSError) as e:
  121. client_logger.warning(".netrc file parses fail: %s", e)
  122. if netrc_obj is None:
  123. client_logger.warning("could't find .netrc file")
  124. except RuntimeError as e: # pragma: no cover
  125. """ handle error raised by pathlib """
  126. client_logger.warning("could't find .netrc file: %s", e)
  127. return netrc_obj
  128. @attr.s(frozen=True, slots=True)
  129. class ProxyInfo:
  130. proxy = attr.ib(type=str)
  131. proxy_auth = attr.ib(type=BasicAuth)
  132. def proxies_from_env():
  133. proxy_urls = {k: URL(v) for k, v in getproxies().items()
  134. if k in ('http', 'https')}
  135. netrc_obj = netrc_from_env()
  136. stripped = {k: strip_auth_from_url(v) for k, v in proxy_urls.items()}
  137. ret = {}
  138. for proto, val in stripped.items():
  139. proxy, auth = val
  140. if proxy.scheme == 'https':
  141. client_logger.warning(
  142. "HTTPS proxies %s are not supported, ignoring", proxy)
  143. continue
  144. if netrc_obj and auth is None:
  145. auth_from_netrc = netrc_obj.authenticators(proxy.host)
  146. if auth_from_netrc is not None:
  147. # auth_from_netrc is a (`user`, `account`, `password`) tuple,
  148. # `user` and `account` both can be username,
  149. # if `user` is None, use `account`
  150. *logins, password = auth_from_netrc
  151. auth = BasicAuth(logins[0] if logins[0] else logins[-1],
  152. password)
  153. ret[proto] = ProxyInfo(proxy, auth)
  154. return ret
  155. def current_task(loop=None):
  156. if PY_37:
  157. return asyncio.current_task(loop=loop)
  158. else:
  159. return asyncio.Task.current_task(loop=loop)
  160. def isasyncgenfunction(obj):
  161. if hasattr(inspect, 'isasyncgenfunction'):
  162. return inspect.isasyncgenfunction(obj)
  163. return False
  164. @attr.s(frozen=True, slots=True)
  165. class MimeType:
  166. type = attr.ib(type=str)
  167. subtype = attr.ib(type=str)
  168. suffix = attr.ib(type=str)
  169. parameters = attr.ib(type=MultiDict)
  170. def parse_mimetype(mimetype):
  171. """Parses a MIME type into its components.
  172. mimetype is a MIME type string.
  173. Returns a MimeType object.
  174. Example:
  175. >>> parse_mimetype('text/html; charset=utf-8')
  176. MimeType(type='text', subtype='html', suffix='',
  177. parameters={'charset': 'utf-8'})
  178. """
  179. if not mimetype:
  180. return MimeType(type='', subtype='', suffix='', parameters={})
  181. parts = mimetype.split(';')
  182. params = []
  183. for item in parts[1:]:
  184. if not item:
  185. continue
  186. key, value = item.split('=', 1) if '=' in item else (item, '')
  187. params.append((key.lower().strip(), value.strip(' "')))
  188. params = MultiDict(params)
  189. fulltype = parts[0].strip().lower()
  190. if fulltype == '*':
  191. fulltype = '*/*'
  192. mtype, stype = fulltype.split('/', 1) \
  193. if '/' in fulltype else (fulltype, '')
  194. stype, suffix = stype.split('+', 1) if '+' in stype else (stype, '')
  195. return MimeType(type=mtype, subtype=stype, suffix=suffix,
  196. parameters=params)
  197. def guess_filename(obj, default=None):
  198. name = getattr(obj, 'name', None)
  199. if name and isinstance(name, str) and name[0] != '<' and name[-1] != '>':
  200. return Path(name).name
  201. return default
  202. def content_disposition_header(disptype, quote_fields=True, **params):
  203. """Sets ``Content-Disposition`` header.
  204. disptype is a disposition type: inline, attachment, form-data.
  205. Should be valid extension token (see RFC 2183)
  206. params is a dict with disposition params.
  207. """
  208. if not disptype or not (TOKEN > set(disptype)):
  209. raise ValueError('bad content disposition type {!r}'
  210. ''.format(disptype))
  211. value = disptype
  212. if params:
  213. lparams = []
  214. for key, val in params.items():
  215. if not key or not (TOKEN > set(key)):
  216. raise ValueError('bad content disposition parameter'
  217. ' {!r}={!r}'.format(key, val))
  218. qval = quote(val, '') if quote_fields else val
  219. lparams.append((key, '"%s"' % qval))
  220. if key == 'filename':
  221. lparams.append(('filename*', "utf-8''" + qval))
  222. sparams = '; '.join('='.join(pair) for pair in lparams)
  223. value = '; '.join((value, sparams))
  224. return value
  225. KeyMethod = namedtuple('KeyMethod', 'key method')
  226. class AccessLogger(AbstractAccessLogger):
  227. """Helper object to log access.
  228. Usage:
  229. log = logging.getLogger("spam")
  230. log_format = "%a %{User-Agent}i"
  231. access_logger = AccessLogger(log, log_format)
  232. access_logger.log(request, response, time)
  233. Format:
  234. %% The percent sign
  235. %a Remote IP-address (IP-address of proxy if using reverse proxy)
  236. %t Time when the request was started to process
  237. %P The process ID of the child that serviced the request
  238. %r First line of request
  239. %s Response status code
  240. %b Size of response in bytes, including HTTP headers
  241. %T Time taken to serve the request, in seconds
  242. %Tf Time taken to serve the request, in seconds with floating fraction
  243. in .06f format
  244. %D Time taken to serve the request, in microseconds
  245. %{FOO}i request.headers['FOO']
  246. %{FOO}o response.headers['FOO']
  247. %{FOO}e os.environ['FOO']
  248. """
  249. LOG_FORMAT_MAP = {
  250. 'a': 'remote_address',
  251. 't': 'request_start_time',
  252. 'P': 'process_id',
  253. 'r': 'first_request_line',
  254. 's': 'response_status',
  255. 'b': 'response_size',
  256. 'T': 'request_time',
  257. 'Tf': 'request_time_frac',
  258. 'D': 'request_time_micro',
  259. 'i': 'request_header',
  260. 'o': 'response_header',
  261. }
  262. LOG_FORMAT = '%a %t "%r" %s %b "%{Referer}i" "%{User-Agent}i"'
  263. FORMAT_RE = re.compile(r'%(\{([A-Za-z0-9\-_]+)\}([ioe])|[atPrsbOD]|Tf?)')
  264. CLEANUP_RE = re.compile(r'(%[^s])')
  265. _FORMAT_CACHE = {} # type: Dict[str, Tuple[str, List[KeyMethod]]]
  266. def __init__(self, logger, log_format=LOG_FORMAT):
  267. """Initialise the logger.
  268. logger is a logger object to be used for logging.
  269. log_format is an string with apache compatible log format description.
  270. """
  271. super().__init__(logger, log_format=log_format)
  272. _compiled_format = AccessLogger._FORMAT_CACHE.get(log_format)
  273. if not _compiled_format:
  274. _compiled_format = self.compile_format(log_format)
  275. AccessLogger._FORMAT_CACHE[log_format] = _compiled_format
  276. self._log_format, self._methods = _compiled_format
  277. def compile_format(self, log_format):
  278. """Translate log_format into form usable by modulo formatting
  279. All known atoms will be replaced with %s
  280. Also methods for formatting of those atoms will be added to
  281. _methods in appropriate order
  282. For example we have log_format = "%a %t"
  283. This format will be translated to "%s %s"
  284. Also contents of _methods will be
  285. [self._format_a, self._format_t]
  286. These method will be called and results will be passed
  287. to translated string format.
  288. Each _format_* method receive 'args' which is list of arguments
  289. given to self.log
  290. Exceptions are _format_e, _format_i and _format_o methods which
  291. also receive key name (by functools.partial)
  292. """
  293. # list of (key, method) tuples, we don't use an OrderedDict as users
  294. # can repeat the same key more than once
  295. methods = list()
  296. for atom in self.FORMAT_RE.findall(log_format):
  297. if atom[1] == '':
  298. format_key = self.LOG_FORMAT_MAP[atom[0]]
  299. m = getattr(AccessLogger, '_format_%s' % atom[0])
  300. else:
  301. format_key = (self.LOG_FORMAT_MAP[atom[2]], atom[1])
  302. m = getattr(AccessLogger, '_format_%s' % atom[2])
  303. m = functools.partial(m, atom[1])
  304. methods.append(KeyMethod(format_key, m))
  305. log_format = self.FORMAT_RE.sub(r'%s', log_format)
  306. log_format = self.CLEANUP_RE.sub(r'%\1', log_format)
  307. return log_format, methods
  308. @staticmethod
  309. def _format_i(key, request, response, time):
  310. if request is None:
  311. return '(no headers)'
  312. # suboptimal, make istr(key) once
  313. return request.headers.get(key, '-')
  314. @staticmethod
  315. def _format_o(key, request, response, time):
  316. # suboptimal, make istr(key) once
  317. return response.headers.get(key, '-')
  318. @staticmethod
  319. def _format_a(request, response, time):
  320. if request is None:
  321. return '-'
  322. ip = request.remote
  323. return ip if ip is not None else '-'
  324. @staticmethod
  325. def _format_t(request, response, time):
  326. now = datetime.datetime.utcnow()
  327. start_time = now - datetime.timedelta(seconds=time)
  328. return start_time.strftime('[%d/%b/%Y:%H:%M:%S +0000]')
  329. @staticmethod
  330. def _format_P(request, response, time):
  331. return "<%s>" % os.getpid()
  332. @staticmethod
  333. def _format_r(request, response, time):
  334. if request is None:
  335. return '-'
  336. return '%s %s HTTP/%s.%s' % tuple((request.method,
  337. request.path_qs) + request.version)
  338. @staticmethod
  339. def _format_s(request, response, time):
  340. return response.status
  341. @staticmethod
  342. def _format_b(request, response, time):
  343. return response.body_length
  344. @staticmethod
  345. def _format_T(request, response, time):
  346. return round(time)
  347. @staticmethod
  348. def _format_Tf(request, response, time):
  349. return '%06f' % time
  350. @staticmethod
  351. def _format_D(request, response, time):
  352. return round(time * 1000000)
  353. def _format_line(self, request, response, time):
  354. return ((key, method(request, response, time))
  355. for key, method in self._methods)
  356. def log(self, request, response, time):
  357. try:
  358. fmt_info = self._format_line(request, response, time)
  359. values = list()
  360. extra = dict()
  361. for key, value in fmt_info:
  362. values.append(value)
  363. if key.__class__ is str:
  364. extra[key] = value
  365. else:
  366. k1, k2 = key
  367. dct = extra.get(k1, {})
  368. dct[k2] = value
  369. extra[k1] = dct
  370. self.logger.info(self._log_format % tuple(values), extra=extra)
  371. except Exception:
  372. self.logger.exception("Error in logging")
  373. class reify:
  374. """Use as a class method decorator. It operates almost exactly like
  375. the Python `@property` decorator, but it puts the result of the
  376. method it decorates into the instance dict after the first call,
  377. effectively replacing the function it decorates with an instance
  378. variable. It is, in Python parlance, a data descriptor.
  379. """
  380. def __init__(self, wrapped):
  381. self.wrapped = wrapped
  382. self.__doc__ = wrapped.__doc__
  383. self.name = wrapped.__name__
  384. def __get__(self, inst, owner):
  385. try:
  386. try:
  387. return inst._cache[self.name]
  388. except KeyError:
  389. val = self.wrapped(inst)
  390. inst._cache[self.name] = val
  391. return val
  392. except AttributeError:
  393. if inst is None:
  394. return self
  395. raise
  396. def __set__(self, inst, value):
  397. raise AttributeError("reified property is read-only")
  398. reify_py = reify
  399. try:
  400. from ._helpers import reify as reify_c
  401. if not NO_EXTENSIONS:
  402. reify = reify_c # type: ignore
  403. except ImportError:
  404. pass
  405. _ipv4_pattern = (r'^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}'
  406. r'(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$')
  407. _ipv6_pattern = (
  408. r'^(?:(?:(?:[A-F0-9]{1,4}:){6}|(?=(?:[A-F0-9]{0,4}:){0,6}'
  409. r'(?:[0-9]{1,3}\.){3}[0-9]{1,3}$)(([0-9A-F]{1,4}:){0,5}|:)'
  410. r'((:[0-9A-F]{1,4}){1,5}:|:)|::(?:[A-F0-9]{1,4}:){5})'
  411. r'(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){3}'
  412. r'(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])|(?:[A-F0-9]{1,4}:){7}'
  413. r'[A-F0-9]{1,4}|(?=(?:[A-F0-9]{0,4}:){0,7}[A-F0-9]{0,4}$)'
  414. r'(([0-9A-F]{1,4}:){1,7}|:)((:[0-9A-F]{1,4}){1,7}|:)|(?:[A-F0-9]{1,4}:){7}'
  415. r':|:(:[A-F0-9]{1,4}){7})$')
  416. _ipv4_regex = re.compile(_ipv4_pattern)
  417. _ipv6_regex = re.compile(_ipv6_pattern, flags=re.IGNORECASE)
  418. _ipv4_regexb = re.compile(_ipv4_pattern.encode('ascii'))
  419. _ipv6_regexb = re.compile(_ipv6_pattern.encode('ascii'), flags=re.IGNORECASE)
  420. def is_ip_address(host):
  421. if host is None:
  422. return False
  423. if isinstance(host, str):
  424. if _ipv4_regex.match(host) or _ipv6_regex.match(host):
  425. return True
  426. else:
  427. return False
  428. elif isinstance(host, (bytes, bytearray, memoryview)):
  429. if _ipv4_regexb.match(host) or _ipv6_regexb.match(host):
  430. return True
  431. else:
  432. return False
  433. else:
  434. raise TypeError("{} [{}] is not a str or bytes"
  435. .format(host, type(host)))
  436. _cached_current_datetime = None
  437. _cached_formatted_datetime = None
  438. def rfc822_formatted_time():
  439. global _cached_current_datetime
  440. global _cached_formatted_datetime
  441. now = int(time.time())
  442. if now != _cached_current_datetime:
  443. # Weekday and month names for HTTP date/time formatting;
  444. # always English!
  445. # Tuples are constants stored in codeobject!
  446. _weekdayname = ("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun")
  447. _monthname = ("", # Dummy so we can use 1-based month numbers
  448. "Jan", "Feb", "Mar", "Apr", "May", "Jun",
  449. "Jul", "Aug", "Sep", "Oct", "Nov", "Dec")
  450. year, month, day, hh, mm, ss, wd, y, z = time.gmtime(now)
  451. _cached_formatted_datetime = "%s, %02d %3s %4d %02d:%02d:%02d GMT" % (
  452. _weekdayname[wd], day, _monthname[month], year, hh, mm, ss
  453. )
  454. _cached_current_datetime = now
  455. return _cached_formatted_datetime
  456. def _weakref_handle(info):
  457. ref, name = info
  458. ob = ref()
  459. if ob is not None:
  460. with suppress(Exception):
  461. getattr(ob, name)()
  462. def weakref_handle(ob, name, timeout, loop, ceil_timeout=True):
  463. if timeout is not None and timeout > 0:
  464. when = loop.time() + timeout
  465. if ceil_timeout:
  466. when = ceil(when)
  467. return loop.call_at(when, _weakref_handle, (weakref.ref(ob), name))
  468. def call_later(cb, timeout, loop):
  469. if timeout is not None and timeout > 0:
  470. when = ceil(loop.time() + timeout)
  471. return loop.call_at(when, cb)
  472. class TimeoutHandle:
  473. """ Timeout handle """
  474. def __init__(self, loop, timeout):
  475. self._timeout = timeout
  476. self._loop = loop
  477. self._callbacks = []
  478. def register(self, callback, *args, **kwargs):
  479. self._callbacks.append((callback, args, kwargs))
  480. def close(self):
  481. self._callbacks.clear()
  482. def start(self):
  483. if self._timeout is not None and self._timeout > 0:
  484. at = ceil(self._loop.time() + self._timeout)
  485. return self._loop.call_at(at, self.__call__)
  486. def timer(self):
  487. if self._timeout is not None and self._timeout > 0:
  488. timer = TimerContext(self._loop)
  489. self.register(timer.timeout)
  490. else:
  491. timer = TimerNoop()
  492. return timer
  493. def __call__(self):
  494. for cb, args, kwargs in self._callbacks:
  495. with suppress(Exception):
  496. cb(*args, **kwargs)
  497. self._callbacks.clear()
  498. class TimerNoop:
  499. def __enter__(self):
  500. return self
  501. def __exit__(self, exc_type, exc_val, exc_tb):
  502. return False
  503. class TimerContext:
  504. """ Low resolution timeout context manager """
  505. def __init__(self, loop):
  506. self._loop = loop
  507. self._tasks = []
  508. self._cancelled = False
  509. def __enter__(self):
  510. task = current_task(loop=self._loop)
  511. if task is None:
  512. raise RuntimeError('Timeout context manager should be used '
  513. 'inside a task')
  514. if self._cancelled:
  515. task.cancel()
  516. raise asyncio.TimeoutError from None
  517. self._tasks.append(task)
  518. return self
  519. def __exit__(self, exc_type, exc_val, exc_tb):
  520. if self._tasks:
  521. self._tasks.pop()
  522. if exc_type is asyncio.CancelledError and self._cancelled:
  523. raise asyncio.TimeoutError from None
  524. def timeout(self):
  525. if not self._cancelled:
  526. for task in set(self._tasks):
  527. task.cancel()
  528. self._cancelled = True
  529. class CeilTimeout(async_timeout.timeout):
  530. def __enter__(self):
  531. if self._timeout is not None:
  532. self._task = current_task(loop=self._loop)
  533. if self._task is None:
  534. raise RuntimeError(
  535. 'Timeout context manager should be used inside a task')
  536. self._cancel_handler = self._loop.call_at(
  537. ceil(self._loop.time() + self._timeout), self._cancel_task)
  538. return self
  539. class HeadersMixin:
  540. ATTRS = frozenset([
  541. '_content_type', '_content_dict', '_stored_content_type'])
  542. _content_type = None
  543. _content_dict = None
  544. _stored_content_type = sentinel
  545. def _parse_content_type(self, raw):
  546. self._stored_content_type = raw
  547. if raw is None:
  548. # default value according to RFC 2616
  549. self._content_type = 'application/octet-stream'
  550. self._content_dict = {}
  551. else:
  552. self._content_type, self._content_dict = cgi.parse_header(raw)
  553. @property
  554. def content_type(self):
  555. """The value of content part for Content-Type HTTP header."""
  556. raw = self._headers.get(hdrs.CONTENT_TYPE)
  557. if self._stored_content_type != raw:
  558. self._parse_content_type(raw)
  559. return self._content_type
  560. @property
  561. def charset(self):
  562. """The value of charset part for Content-Type HTTP header."""
  563. raw = self._headers.get(hdrs.CONTENT_TYPE)
  564. if self._stored_content_type != raw:
  565. self._parse_content_type(raw)
  566. return self._content_dict.get('charset')
  567. @property
  568. def content_length(self):
  569. """The value of Content-Length HTTP header."""
  570. content_length = self._headers.get(hdrs.CONTENT_LENGTH)
  571. if content_length:
  572. return int(content_length)
  573. def set_result(fut, result):
  574. if not fut.done():
  575. fut.set_result(result)
  576. def set_exception(fut, exc):
  577. if not fut.done():
  578. fut.set_exception(exc)
  579. class ChainMapProxy(ABCMapping):
  580. __slots__ = ('_maps',)
  581. def __init__(self, maps):
  582. self._maps = tuple(maps)
  583. def __init_subclass__(cls):
  584. raise TypeError("Inheritance class {} from ChainMapProxy "
  585. "is forbidden".format(cls.__name__))
  586. def __getitem__(self, key):
  587. for mapping in self._maps:
  588. try:
  589. return mapping[key]
  590. except KeyError:
  591. pass
  592. raise KeyError(key)
  593. def get(self, key, default=None):
  594. return self[key] if key in self else default
  595. def __len__(self):
  596. # reuses stored hash values if possible
  597. return len(set().union(*self._maps))
  598. def __iter__(self):
  599. d = {}
  600. for mapping in reversed(self._maps):
  601. # reuses stored hash values if possible
  602. d.update(mapping)
  603. return iter(d)
  604. def __contains__(self, key):
  605. return any(key in m for m in self._maps)
  606. def __bool__(self):
  607. return any(self._maps)
  608. def __repr__(self):
  609. content = ", ".join(map(repr, self._maps))
  610. return 'ChainMapProxy({})'.format(content)