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.

968 lines
36 KiB

4 years ago
  1. """HTTP Client for asyncio."""
  2. import asyncio
  3. import base64
  4. import hashlib
  5. import json
  6. import os
  7. import sys
  8. import traceback
  9. import warnings
  10. from collections.abc import Coroutine as CoroutineABC
  11. from typing import Any, Generator, Optional, Tuple
  12. import attr
  13. from multidict import CIMultiDict, MultiDict, MultiDictProxy, istr
  14. from yarl import URL
  15. from . import client_exceptions, client_reqrep
  16. from . import connector as connector_mod
  17. from . import hdrs, http, payload
  18. from .client_exceptions import * # noqa
  19. from .client_exceptions import (ClientError, ClientOSError, InvalidURL,
  20. ServerTimeoutError, TooManyRedirects,
  21. WSServerHandshakeError)
  22. from .client_reqrep import * # noqa
  23. from .client_reqrep import ClientRequest, ClientResponse, _merge_ssl_params
  24. from .client_ws import ClientWebSocketResponse
  25. from .connector import * # noqa
  26. from .connector import BaseConnector, TCPConnector
  27. from .cookiejar import CookieJar
  28. from .helpers import (DEBUG, PY_36, CeilTimeout, TimeoutHandle,
  29. proxies_from_env, sentinel, strip_auth_from_url)
  30. from .http import WS_KEY, WebSocketReader, WebSocketWriter
  31. from .http_websocket import WSHandshakeError, ws_ext_gen, ws_ext_parse
  32. from .streams import FlowControlDataQueue
  33. from .tcp_helpers import tcp_cork, tcp_nodelay
  34. from .tracing import Trace
  35. from .typedefs import StrOrURL
  36. __all__ = (client_exceptions.__all__ + # noqa
  37. client_reqrep.__all__ + # noqa
  38. connector_mod.__all__ + # noqa
  39. ('ClientSession', 'ClientTimeout',
  40. 'ClientWebSocketResponse', 'request'))
  41. @attr.s(frozen=True, slots=True)
  42. class ClientTimeout:
  43. total = attr.ib(type=float, default=None)
  44. connect = attr.ib(type=float, default=None)
  45. sock_read = attr.ib(type=float, default=None)
  46. sock_connect = attr.ib(type=float, default=None)
  47. # pool_queue_timeout = attr.ib(type=float, default=None)
  48. # dns_resolution_timeout = attr.ib(type=float, default=None)
  49. # socket_connect_timeout = attr.ib(type=float, default=None)
  50. # connection_acquiring_timeout = attr.ib(type=float, default=None)
  51. # new_connection_timeout = attr.ib(type=float, default=None)
  52. # http_header_timeout = attr.ib(type=float, default=None)
  53. # response_body_timeout = attr.ib(type=float, default=None)
  54. # to create a timeout specific for a single request, either
  55. # - create a completely new one to overwrite the default
  56. # - or use http://www.attrs.org/en/stable/api.html#attr.evolve
  57. # to overwrite the defaults
  58. # 5 Minute default read timeout
  59. DEFAULT_TIMEOUT = ClientTimeout(total=5*60)
  60. class ClientSession:
  61. """First-class interface for making HTTP requests."""
  62. ATTRS = frozenset([
  63. '_source_traceback', '_connector',
  64. 'requote_redirect_url', '_loop', '_cookie_jar',
  65. '_connector_owner', '_default_auth',
  66. '_version', '_json_serialize',
  67. '_timeout', '_raise_for_status', '_auto_decompress',
  68. '_trust_env', '_default_headers', '_skip_auto_headers',
  69. '_request_class', '_response_class',
  70. '_ws_response_class', '_trace_configs'])
  71. _source_traceback = None
  72. _connector = None
  73. requote_redirect_url = True
  74. def __init__(self, *, connector=None, loop=None, cookies=None,
  75. headers=None, skip_auto_headers=None,
  76. auth=None, json_serialize=json.dumps,
  77. request_class=ClientRequest, response_class=ClientResponse,
  78. ws_response_class=ClientWebSocketResponse,
  79. version=http.HttpVersion11,
  80. cookie_jar=None, connector_owner=True,
  81. raise_for_status=False,
  82. read_timeout=sentinel, conn_timeout=None,
  83. timeout=sentinel,
  84. auto_decompress=True, trust_env=False,
  85. trace_configs=None):
  86. implicit_loop = False
  87. if loop is None:
  88. if connector is not None:
  89. loop = connector._loop
  90. else:
  91. implicit_loop = True
  92. loop = asyncio.get_event_loop()
  93. if connector is None:
  94. connector = TCPConnector(loop=loop)
  95. if connector._loop is not loop:
  96. raise RuntimeError(
  97. "Session and connector has to use same event loop")
  98. self._loop = loop
  99. if loop.get_debug():
  100. self._source_traceback = traceback.extract_stack(sys._getframe(1))
  101. if implicit_loop and not loop.is_running():
  102. warnings.warn("Creating a client session outside of coroutine is "
  103. "a very dangerous idea",
  104. stacklevel=2)
  105. context = {'client_session': self,
  106. 'message': 'Creating a client session outside '
  107. 'of coroutine'}
  108. if self._source_traceback is not None:
  109. context['source_traceback'] = self._source_traceback
  110. loop.call_exception_handler(context)
  111. if cookie_jar is None:
  112. cookie_jar = CookieJar(loop=loop)
  113. self._cookie_jar = cookie_jar
  114. if cookies is not None:
  115. self._cookie_jar.update_cookies(cookies)
  116. self._connector = connector # type: BaseConnector
  117. self._connector_owner = connector_owner
  118. self._default_auth = auth
  119. self._version = version
  120. self._json_serialize = json_serialize
  121. if timeout is not sentinel:
  122. self._timeout = timeout
  123. if read_timeout is not sentinel:
  124. raise ValueError("read_timeout and timeout parameters "
  125. "conflict, please setup "
  126. "timeout.read")
  127. if conn_timeout is not None:
  128. raise ValueError("conn_timeout and timeout parameters "
  129. "conflict, please setup "
  130. "timeout.connect")
  131. else:
  132. self._timeout = DEFAULT_TIMEOUT
  133. if read_timeout is not sentinel:
  134. self._timeout = attr.evolve(self._timeout, total=read_timeout)
  135. if conn_timeout is not None:
  136. self._timeout = attr.evolve(self._timeout,
  137. connect=conn_timeout)
  138. self._raise_for_status = raise_for_status
  139. self._auto_decompress = auto_decompress
  140. self._trust_env = trust_env
  141. # Convert to list of tuples
  142. if headers:
  143. headers = CIMultiDict(headers)
  144. else:
  145. headers = CIMultiDict()
  146. self._default_headers = headers
  147. if skip_auto_headers is not None:
  148. self._skip_auto_headers = frozenset([istr(i)
  149. for i in skip_auto_headers])
  150. else:
  151. self._skip_auto_headers = frozenset()
  152. self._request_class = request_class
  153. self._response_class = response_class
  154. self._ws_response_class = ws_response_class
  155. self._trace_configs = trace_configs or []
  156. for trace_config in self._trace_configs:
  157. trace_config.freeze()
  158. def __init_subclass__(cls):
  159. warnings.warn("Inheritance class {} from ClientSession "
  160. "is discouraged".format(cls.__name__),
  161. DeprecationWarning,
  162. stacklevel=2)
  163. if DEBUG:
  164. def __setattr__(self, name, val):
  165. if name not in self.ATTRS:
  166. warnings.warn("Setting custom ClientSession.{} attribute "
  167. "is discouraged".format(name),
  168. DeprecationWarning,
  169. stacklevel=2)
  170. super().__setattr__(name, val)
  171. def __del__(self, _warnings=warnings):
  172. if not self.closed:
  173. if PY_36:
  174. kwargs = {'source': self}
  175. else:
  176. kwargs = {}
  177. _warnings.warn("Unclosed client session {!r}".format(self),
  178. ResourceWarning,
  179. **kwargs)
  180. context = {'client_session': self,
  181. 'message': 'Unclosed client session'}
  182. if self._source_traceback is not None:
  183. context['source_traceback'] = self._source_traceback
  184. self._loop.call_exception_handler(context)
  185. def request(self,
  186. method: str,
  187. url: StrOrURL,
  188. **kwargs) -> '_RequestContextManager':
  189. """Perform HTTP request."""
  190. return _RequestContextManager(self._request(method, url, **kwargs))
  191. async def _request(self, method, url, *,
  192. params=None,
  193. data=None,
  194. json=None,
  195. headers=None,
  196. skip_auto_headers=None,
  197. auth=None,
  198. allow_redirects=True,
  199. max_redirects=10,
  200. compress=None,
  201. chunked=None,
  202. expect100=False,
  203. raise_for_status=None,
  204. read_until_eof=True,
  205. proxy=None,
  206. proxy_auth=None,
  207. timeout=sentinel,
  208. verify_ssl=None,
  209. fingerprint=None,
  210. ssl_context=None,
  211. ssl=None,
  212. proxy_headers=None,
  213. trace_request_ctx=None):
  214. # NOTE: timeout clamps existing connect and read timeouts. We cannot
  215. # set the default to None because we need to detect if the user wants
  216. # to use the existing timeouts by setting timeout to None.
  217. if self.closed:
  218. raise RuntimeError('Session is closed')
  219. ssl = _merge_ssl_params(ssl, verify_ssl, ssl_context, fingerprint)
  220. if data is not None and json is not None:
  221. raise ValueError(
  222. 'data and json parameters can not be used at the same time')
  223. elif json is not None:
  224. data = payload.JsonPayload(json, dumps=self._json_serialize)
  225. if not isinstance(chunked, bool) and chunked is not None:
  226. warnings.warn(
  227. 'Chunk size is deprecated #1615', DeprecationWarning)
  228. redirects = 0
  229. history = []
  230. version = self._version
  231. # Merge with default headers and transform to CIMultiDict
  232. headers = self._prepare_headers(headers)
  233. proxy_headers = self._prepare_headers(proxy_headers)
  234. try:
  235. url = URL(url)
  236. except ValueError:
  237. raise InvalidURL(url)
  238. skip_headers = set(self._skip_auto_headers)
  239. if skip_auto_headers is not None:
  240. for i in skip_auto_headers:
  241. skip_headers.add(istr(i))
  242. if proxy is not None:
  243. try:
  244. proxy = URL(proxy)
  245. except ValueError:
  246. raise InvalidURL(proxy)
  247. if timeout is sentinel:
  248. timeout = self._timeout
  249. else:
  250. if not isinstance(timeout, ClientTimeout):
  251. timeout = ClientTimeout(total=timeout)
  252. # timeout is cumulative for all request operations
  253. # (request, redirects, responses, data consuming)
  254. tm = TimeoutHandle(self._loop, timeout.total)
  255. handle = tm.start()
  256. traces = [
  257. Trace(
  258. self,
  259. trace_config,
  260. trace_config.trace_config_ctx(
  261. trace_request_ctx=trace_request_ctx)
  262. )
  263. for trace_config in self._trace_configs
  264. ]
  265. for trace in traces:
  266. await trace.send_request_start(
  267. method,
  268. url,
  269. headers
  270. )
  271. timer = tm.timer()
  272. try:
  273. with timer:
  274. while True:
  275. url, auth_from_url = strip_auth_from_url(url)
  276. if auth and auth_from_url:
  277. raise ValueError("Cannot combine AUTH argument with "
  278. "credentials encoded in URL")
  279. if auth is None:
  280. auth = auth_from_url
  281. if auth is None:
  282. auth = self._default_auth
  283. # It would be confusing if we support explicit
  284. # Authorization header with auth argument
  285. if (headers is not None and
  286. auth is not None and
  287. hdrs.AUTHORIZATION in headers):
  288. raise ValueError("Cannot combine AUTHORIZATION header "
  289. "with AUTH argument or credentials "
  290. "encoded in URL")
  291. cookies = self._cookie_jar.filter_cookies(url)
  292. if proxy is not None:
  293. proxy = URL(proxy)
  294. elif self._trust_env:
  295. for scheme, proxy_info in proxies_from_env().items():
  296. if scheme == url.scheme:
  297. proxy = proxy_info.proxy
  298. proxy_auth = proxy_info.proxy_auth
  299. break
  300. req = self._request_class(
  301. method, url, params=params, headers=headers,
  302. skip_auto_headers=skip_headers, data=data,
  303. cookies=cookies, auth=auth, version=version,
  304. compress=compress, chunked=chunked,
  305. expect100=expect100, loop=self._loop,
  306. response_class=self._response_class,
  307. proxy=proxy, proxy_auth=proxy_auth, timer=timer,
  308. session=self,
  309. ssl=ssl, proxy_headers=proxy_headers, traces=traces)
  310. # connection timeout
  311. try:
  312. with CeilTimeout(self._timeout.connect,
  313. loop=self._loop):
  314. conn = await self._connector.connect(
  315. req,
  316. traces=traces,
  317. timeout=timeout
  318. )
  319. except asyncio.TimeoutError as exc:
  320. raise ServerTimeoutError(
  321. 'Connection timeout '
  322. 'to host {0}'.format(url)) from exc
  323. tcp_nodelay(conn.transport, True)
  324. tcp_cork(conn.transport, False)
  325. conn.protocol.set_response_params(
  326. timer=timer,
  327. skip_payload=method.upper() == 'HEAD',
  328. read_until_eof=read_until_eof,
  329. auto_decompress=self._auto_decompress,
  330. read_timeout=timeout.sock_read)
  331. try:
  332. try:
  333. resp = await req.send(conn)
  334. try:
  335. await resp.start(conn)
  336. except BaseException:
  337. resp.close()
  338. raise
  339. except BaseException:
  340. conn.close()
  341. raise
  342. except ClientError:
  343. raise
  344. except OSError as exc:
  345. raise ClientOSError(*exc.args) from exc
  346. self._cookie_jar.update_cookies(resp.cookies, resp.url)
  347. # redirects
  348. if resp.status in (
  349. 301, 302, 303, 307, 308) and allow_redirects:
  350. for trace in traces:
  351. await trace.send_request_redirect(
  352. method,
  353. url,
  354. headers,
  355. resp
  356. )
  357. redirects += 1
  358. history.append(resp)
  359. if max_redirects and redirects >= max_redirects:
  360. resp.close()
  361. raise TooManyRedirects(
  362. history[0].request_info, tuple(history))
  363. # For 301 and 302, mimic IE, now changed in RFC
  364. # https://github.com/kennethreitz/requests/pull/269
  365. if (resp.status == 303 and
  366. resp.method != hdrs.METH_HEAD) \
  367. or (resp.status in (301, 302) and
  368. resp.method == hdrs.METH_POST):
  369. method = hdrs.METH_GET
  370. data = None
  371. if headers.get(hdrs.CONTENT_LENGTH):
  372. headers.pop(hdrs.CONTENT_LENGTH)
  373. r_url = (resp.headers.get(hdrs.LOCATION) or
  374. resp.headers.get(hdrs.URI))
  375. if r_url is None:
  376. # see github.com/aio-libs/aiohttp/issues/2022
  377. break
  378. else:
  379. # reading from correct redirection
  380. # response is forbidden
  381. resp.release()
  382. try:
  383. r_url = URL(
  384. r_url, encoded=not self.requote_redirect_url)
  385. except ValueError:
  386. raise InvalidURL(r_url)
  387. scheme = r_url.scheme
  388. if scheme not in ('http', 'https', ''):
  389. resp.close()
  390. raise ValueError(
  391. 'Can redirect only to http or https')
  392. elif not scheme:
  393. r_url = url.join(r_url)
  394. if url.origin() != r_url.origin():
  395. auth = None
  396. headers.pop(hdrs.AUTHORIZATION, None)
  397. url = r_url
  398. params = None
  399. resp.release()
  400. continue
  401. break
  402. # check response status
  403. if raise_for_status is None:
  404. raise_for_status = self._raise_for_status
  405. if raise_for_status:
  406. resp.raise_for_status()
  407. # register connection
  408. if handle is not None:
  409. if resp.connection is not None:
  410. resp.connection.add_callback(handle.cancel)
  411. else:
  412. handle.cancel()
  413. resp._history = tuple(history)
  414. for trace in traces:
  415. await trace.send_request_end(
  416. method,
  417. url,
  418. headers,
  419. resp
  420. )
  421. return resp
  422. except BaseException as e:
  423. # cleanup timer
  424. tm.close()
  425. if handle:
  426. handle.cancel()
  427. handle = None
  428. for trace in traces:
  429. await trace.send_request_exception(
  430. method,
  431. url,
  432. headers,
  433. e
  434. )
  435. raise
  436. def ws_connect(self, url: StrOrURL, *,
  437. protocols=(),
  438. timeout=10.0,
  439. receive_timeout=None,
  440. autoclose=True,
  441. autoping=True,
  442. heartbeat=None,
  443. auth=None,
  444. origin=None,
  445. headers=None,
  446. proxy=None,
  447. proxy_auth=None,
  448. ssl=None,
  449. verify_ssl=None,
  450. fingerprint=None,
  451. ssl_context=None,
  452. proxy_headers=None,
  453. compress=0,
  454. max_msg_size=4*1024*1024):
  455. """Initiate websocket connection."""
  456. return _WSRequestContextManager(
  457. self._ws_connect(url,
  458. protocols=protocols,
  459. timeout=timeout,
  460. receive_timeout=receive_timeout,
  461. autoclose=autoclose,
  462. autoping=autoping,
  463. heartbeat=heartbeat,
  464. auth=auth,
  465. origin=origin,
  466. headers=headers,
  467. proxy=proxy,
  468. proxy_auth=proxy_auth,
  469. ssl=ssl,
  470. verify_ssl=verify_ssl,
  471. fingerprint=fingerprint,
  472. ssl_context=ssl_context,
  473. proxy_headers=proxy_headers,
  474. compress=compress,
  475. max_msg_size=max_msg_size))
  476. async def _ws_connect(self, url, *,
  477. protocols=(),
  478. timeout=10.0,
  479. receive_timeout=None,
  480. autoclose=True,
  481. autoping=True,
  482. heartbeat=None,
  483. auth=None,
  484. origin=None,
  485. headers=None,
  486. proxy=None,
  487. proxy_auth=None,
  488. ssl=None,
  489. verify_ssl=None,
  490. fingerprint=None,
  491. ssl_context=None,
  492. proxy_headers=None,
  493. compress=0,
  494. max_msg_size=4*1024*1024):
  495. if headers is None:
  496. headers = CIMultiDict()
  497. default_headers = {
  498. hdrs.UPGRADE: hdrs.WEBSOCKET,
  499. hdrs.CONNECTION: hdrs.UPGRADE,
  500. hdrs.SEC_WEBSOCKET_VERSION: '13',
  501. }
  502. for key, value in default_headers.items():
  503. if key not in headers:
  504. headers[key] = value
  505. sec_key = base64.b64encode(os.urandom(16))
  506. headers[hdrs.SEC_WEBSOCKET_KEY] = sec_key.decode()
  507. if protocols:
  508. headers[hdrs.SEC_WEBSOCKET_PROTOCOL] = ','.join(protocols)
  509. if origin is not None:
  510. headers[hdrs.ORIGIN] = origin
  511. if compress:
  512. extstr = ws_ext_gen(compress=compress)
  513. headers[hdrs.SEC_WEBSOCKET_EXTENSIONS] = extstr
  514. ssl = _merge_ssl_params(ssl, verify_ssl, ssl_context, fingerprint)
  515. # send request
  516. resp = await self.get(url, headers=headers,
  517. read_until_eof=False,
  518. auth=auth,
  519. proxy=proxy,
  520. proxy_auth=proxy_auth,
  521. ssl=ssl,
  522. proxy_headers=proxy_headers)
  523. try:
  524. # check handshake
  525. if resp.status != 101:
  526. raise WSServerHandshakeError(
  527. resp.request_info,
  528. resp.history,
  529. message='Invalid response status',
  530. status=resp.status,
  531. headers=resp.headers)
  532. if resp.headers.get(hdrs.UPGRADE, '').lower() != 'websocket':
  533. raise WSServerHandshakeError(
  534. resp.request_info,
  535. resp.history,
  536. message='Invalid upgrade header',
  537. status=resp.status,
  538. headers=resp.headers)
  539. if resp.headers.get(hdrs.CONNECTION, '').lower() != 'upgrade':
  540. raise WSServerHandshakeError(
  541. resp.request_info,
  542. resp.history,
  543. message='Invalid connection header',
  544. status=resp.status,
  545. headers=resp.headers)
  546. # key calculation
  547. key = resp.headers.get(hdrs.SEC_WEBSOCKET_ACCEPT, '')
  548. match = base64.b64encode(
  549. hashlib.sha1(sec_key + WS_KEY).digest()).decode()
  550. if key != match:
  551. raise WSServerHandshakeError(
  552. resp.request_info,
  553. resp.history,
  554. message='Invalid challenge response',
  555. status=resp.status,
  556. headers=resp.headers)
  557. # websocket protocol
  558. protocol = None
  559. if protocols and hdrs.SEC_WEBSOCKET_PROTOCOL in resp.headers:
  560. resp_protocols = [
  561. proto.strip() for proto in
  562. resp.headers[hdrs.SEC_WEBSOCKET_PROTOCOL].split(',')]
  563. for proto in resp_protocols:
  564. if proto in protocols:
  565. protocol = proto
  566. break
  567. # websocket compress
  568. notakeover = False
  569. if compress:
  570. compress_hdrs = resp.headers.get(hdrs.SEC_WEBSOCKET_EXTENSIONS)
  571. if compress_hdrs:
  572. try:
  573. compress, notakeover = ws_ext_parse(compress_hdrs)
  574. except WSHandshakeError as exc:
  575. raise WSServerHandshakeError(
  576. resp.request_info,
  577. resp.history,
  578. message=exc.args[0],
  579. status=resp.status,
  580. headers=resp.headers)
  581. else:
  582. compress = 0
  583. notakeover = False
  584. proto = resp.connection.protocol
  585. transport = resp.connection.transport
  586. reader = FlowControlDataQueue(
  587. proto, limit=2 ** 16, loop=self._loop)
  588. proto.set_parser(WebSocketReader(reader, max_msg_size), reader)
  589. tcp_nodelay(transport, True)
  590. writer = WebSocketWriter(
  591. proto, transport, use_mask=True,
  592. compress=compress, notakeover=notakeover)
  593. except BaseException:
  594. resp.close()
  595. raise
  596. else:
  597. return self._ws_response_class(reader,
  598. writer,
  599. protocol,
  600. resp,
  601. timeout,
  602. autoclose,
  603. autoping,
  604. self._loop,
  605. receive_timeout=receive_timeout,
  606. heartbeat=heartbeat,
  607. compress=compress,
  608. client_notakeover=notakeover)
  609. def _prepare_headers(self, headers):
  610. """ Add default headers and transform it to CIMultiDict
  611. """
  612. # Convert headers to MultiDict
  613. result = CIMultiDict(self._default_headers)
  614. if headers:
  615. if not isinstance(headers, (MultiDictProxy, MultiDict)):
  616. headers = CIMultiDict(headers)
  617. added_names = set()
  618. for key, value in headers.items():
  619. if key in added_names:
  620. result.add(key, value)
  621. else:
  622. result[key] = value
  623. added_names.add(key)
  624. return result
  625. def get(self, url: StrOrURL, *, allow_redirects: bool=True,
  626. **kwargs) -> '_RequestContextManager':
  627. """Perform HTTP GET request."""
  628. return _RequestContextManager(
  629. self._request(hdrs.METH_GET, url,
  630. allow_redirects=allow_redirects,
  631. **kwargs))
  632. def options(self, url: StrOrURL, *, allow_redirects: bool=True,
  633. **kwargs) -> '_RequestContextManager':
  634. """Perform HTTP OPTIONS request."""
  635. return _RequestContextManager(
  636. self._request(hdrs.METH_OPTIONS, url,
  637. allow_redirects=allow_redirects,
  638. **kwargs))
  639. def head(self, url: StrOrURL, *, allow_redirects: bool=False,
  640. **kwargs) -> '_RequestContextManager':
  641. """Perform HTTP HEAD request."""
  642. return _RequestContextManager(
  643. self._request(hdrs.METH_HEAD, url,
  644. allow_redirects=allow_redirects,
  645. **kwargs))
  646. def post(self, url: StrOrURL,
  647. *, data: Any=None, **kwargs) -> '_RequestContextManager':
  648. """Perform HTTP POST request."""
  649. return _RequestContextManager(
  650. self._request(hdrs.METH_POST, url,
  651. data=data,
  652. **kwargs))
  653. def put(self, url: StrOrURL,
  654. *, data: Any=None, **kwargs) -> '_RequestContextManager':
  655. """Perform HTTP PUT request."""
  656. return _RequestContextManager(
  657. self._request(hdrs.METH_PUT, url,
  658. data=data,
  659. **kwargs))
  660. def patch(self, url: StrOrURL,
  661. *, data: Any=None, **kwargs) -> '_RequestContextManager':
  662. """Perform HTTP PATCH request."""
  663. return _RequestContextManager(
  664. self._request(hdrs.METH_PATCH, url,
  665. data=data,
  666. **kwargs))
  667. def delete(self, url: StrOrURL, **kwargs) -> '_RequestContextManager':
  668. """Perform HTTP DELETE request."""
  669. return _RequestContextManager(
  670. self._request(hdrs.METH_DELETE, url,
  671. **kwargs))
  672. async def close(self) -> None:
  673. """Close underlying connector.
  674. Release all acquired resources.
  675. """
  676. if not self.closed:
  677. if self._connector is not None and self._connector_owner:
  678. self._connector.close()
  679. self._connector = None
  680. @property
  681. def closed(self) -> bool:
  682. """Is client session closed.
  683. A readonly property.
  684. """
  685. return self._connector is None or self._connector.closed
  686. @property
  687. def connector(self) -> Optional[BaseConnector]:
  688. """Connector instance used for the session."""
  689. return self._connector
  690. @property
  691. def cookie_jar(self) -> CookieJar:
  692. """The session cookies."""
  693. return self._cookie_jar
  694. @property
  695. def version(self) -> Tuple[int, int]:
  696. """The session HTTP protocol version."""
  697. return self._version
  698. @property
  699. def loop(self) -> asyncio.AbstractEventLoop:
  700. """Session's loop."""
  701. return self._loop
  702. def detach(self) -> None:
  703. """Detach connector from session without closing the former.
  704. Session is switched to closed state anyway.
  705. """
  706. self._connector = None
  707. def __enter__(self):
  708. raise TypeError("Use async with instead")
  709. def __exit__(self, exc_type, exc_val, exc_tb):
  710. # __exit__ should exist in pair with __enter__ but never executed
  711. pass # pragma: no cover
  712. async def __aenter__(self) -> 'ClientSession':
  713. return self
  714. async def __aexit__(self, exc_type, exc_val, exc_tb):
  715. await self.close()
  716. class _BaseRequestContextManager(CoroutineABC):
  717. __slots__ = ('_coro', '_resp')
  718. def __init__(self, coro):
  719. self._coro = coro
  720. def send(self, arg):
  721. return self._coro.send(arg)
  722. def throw(self, arg):
  723. return self._coro.throw(arg)
  724. def close(self):
  725. return self._coro.close()
  726. def __await__(self) -> Generator[Any, None, ClientResponse]:
  727. ret = self._coro.__await__()
  728. return ret
  729. def __iter__(self):
  730. return self.__await__()
  731. async def __aenter__(self) -> ClientResponse:
  732. self._resp = await self._coro
  733. return self._resp
  734. class _RequestContextManager(_BaseRequestContextManager):
  735. async def __aexit__(self, exc_type, exc, tb):
  736. # We're basing behavior on the exception as it can be caused by
  737. # user code unrelated to the status of the connection. If you
  738. # would like to close a connection you must do that
  739. # explicitly. Otherwise connection error handling should kick in
  740. # and close/recycle the connection as required.
  741. self._resp.release()
  742. class _WSRequestContextManager(_BaseRequestContextManager):
  743. async def __aexit__(self, exc_type, exc, tb):
  744. await self._resp.close()
  745. class _SessionRequestContextManager:
  746. __slots__ = ('_coro', '_resp', '_session')
  747. def __init__(self, coro, session):
  748. self._coro = coro
  749. self._resp = None
  750. self._session = session
  751. async def __aenter__(self) -> ClientResponse:
  752. self._resp = await self._coro
  753. return self._resp
  754. async def __aexit__(self, exc_type, exc_val, exc_tb):
  755. self._resp.close()
  756. await self._session.close()
  757. def request(method, url, *,
  758. params=None,
  759. data=None,
  760. json=None,
  761. headers=None,
  762. skip_auto_headers=None,
  763. cookies=None,
  764. auth=None,
  765. allow_redirects=True,
  766. max_redirects=10,
  767. version=http.HttpVersion11,
  768. compress=None,
  769. chunked=None,
  770. expect100=False,
  771. connector=None,
  772. loop=None,
  773. read_until_eof=True,
  774. proxy=None,
  775. proxy_auth=None) -> _SessionRequestContextManager:
  776. """Constructs and sends a request. Returns response object.
  777. method - HTTP method
  778. url - request url
  779. params - (optional) Dictionary or bytes to be sent in the query
  780. string of the new request
  781. data - (optional) Dictionary, bytes, or file-like object to
  782. send in the body of the request
  783. json - (optional) Any json compatibile python object
  784. headers - (optional) Dictionary of HTTP Headers to send with
  785. the request
  786. cookies - (optional) Dict object to send with the request
  787. auth - (optional) BasicAuth named tuple represent HTTP Basic Auth
  788. auth - aiohttp.helpers.BasicAuth
  789. allow_redirects - (optional) If set to False, do not follow
  790. redirects
  791. version - Request HTTP version.
  792. compress - Set to True if request has to be compressed
  793. with deflate encoding.
  794. chunked - Set to chunk size for chunked transfer encoding.
  795. expect100 - Expect 100-continue response from server.
  796. connector - BaseConnector sub-class instance to support
  797. connection pooling.
  798. read_until_eof - Read response until eof if response
  799. does not have Content-Length header.
  800. loop - Optional event loop.
  801. Usage::
  802. >>> import aiohttp
  803. >>> resp = await aiohttp.request('GET', 'http://python.org/')
  804. >>> resp
  805. <ClientResponse(python.org/) [200]>
  806. >>> data = await resp.read()
  807. """
  808. connector_owner = False
  809. if connector is None:
  810. connector_owner = True
  811. connector = TCPConnector(loop=loop, force_close=True)
  812. session = ClientSession(
  813. loop=loop, cookies=cookies, version=version,
  814. connector=connector, connector_owner=connector_owner)
  815. return _SessionRequestContextManager(
  816. session._request(method, url,
  817. params=params,
  818. data=data,
  819. json=json,
  820. headers=headers,
  821. skip_auto_headers=skip_auto_headers,
  822. auth=auth,
  823. allow_redirects=allow_redirects,
  824. max_redirects=max_redirects,
  825. compress=compress,
  826. chunked=chunked,
  827. expect100=expect100,
  828. read_until_eof=read_until_eof,
  829. proxy=proxy,
  830. proxy_auth=proxy_auth,),
  831. session)