salmankhanpm commited on
Commit
b749c39
·
verified ·
1 Parent(s): c896de3

Add files using upload-large-folder tool

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
aiohttp-3.13.3.dist-info/INSTALLER ADDED
@@ -0,0 +1 @@
 
 
1
+ uv
aiohttp-3.13.3.dist-info/METADATA ADDED
@@ -0,0 +1,262 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Metadata-Version: 2.4
2
+ Name: aiohttp
3
+ Version: 3.13.3
4
+ Summary: Async http client/server framework (asyncio)
5
+ Maintainer-email: aiohttp team <team@aiohttp.org>
6
+ License: Apache-2.0 AND MIT
7
+ Project-URL: Homepage, https://github.com/aio-libs/aiohttp
8
+ Project-URL: Chat: Matrix, https://matrix.to/#/#aio-libs:matrix.org
9
+ Project-URL: Chat: Matrix Space, https://matrix.to/#/#aio-libs-space:matrix.org
10
+ Project-URL: CI: GitHub Actions, https://github.com/aio-libs/aiohttp/actions?query=workflow%3ACI
11
+ Project-URL: Coverage: codecov, https://codecov.io/github/aio-libs/aiohttp
12
+ Project-URL: Docs: Changelog, https://docs.aiohttp.org/en/stable/changes.html
13
+ Project-URL: Docs: RTD, https://docs.aiohttp.org
14
+ Project-URL: GitHub: issues, https://github.com/aio-libs/aiohttp/issues
15
+ Project-URL: GitHub: repo, https://github.com/aio-libs/aiohttp
16
+ Classifier: Development Status :: 5 - Production/Stable
17
+ Classifier: Framework :: AsyncIO
18
+ Classifier: Intended Audience :: Developers
19
+ Classifier: Operating System :: POSIX
20
+ Classifier: Operating System :: MacOS :: MacOS X
21
+ Classifier: Operating System :: Microsoft :: Windows
22
+ Classifier: Programming Language :: Python
23
+ Classifier: Programming Language :: Python :: 3
24
+ Classifier: Programming Language :: Python :: 3.9
25
+ Classifier: Programming Language :: Python :: 3.10
26
+ Classifier: Programming Language :: Python :: 3.11
27
+ Classifier: Programming Language :: Python :: 3.12
28
+ Classifier: Programming Language :: Python :: 3.13
29
+ Classifier: Programming Language :: Python :: 3.14
30
+ Classifier: Topic :: Internet :: WWW/HTTP
31
+ Requires-Python: >=3.9
32
+ Description-Content-Type: text/x-rst
33
+ License-File: LICENSE.txt
34
+ License-File: vendor/llhttp/LICENSE
35
+ Requires-Dist: aiohappyeyeballs>=2.5.0
36
+ Requires-Dist: aiosignal>=1.4.0
37
+ Requires-Dist: async-timeout<6.0,>=4.0; python_version < "3.11"
38
+ Requires-Dist: attrs>=17.3.0
39
+ Requires-Dist: frozenlist>=1.1.1
40
+ Requires-Dist: multidict<7.0,>=4.5
41
+ Requires-Dist: propcache>=0.2.0
42
+ Requires-Dist: yarl<2.0,>=1.17.0
43
+ Provides-Extra: speedups
44
+ Requires-Dist: aiodns>=3.3.0; extra == "speedups"
45
+ Requires-Dist: Brotli>=1.2; platform_python_implementation == "CPython" and extra == "speedups"
46
+ Requires-Dist: brotlicffi>=1.2; platform_python_implementation != "CPython" and extra == "speedups"
47
+ Requires-Dist: backports.zstd; (platform_python_implementation == "CPython" and python_version < "3.14") and extra == "speedups"
48
+ Dynamic: license-file
49
+
50
+ ==================================
51
+ Async http client/server framework
52
+ ==================================
53
+
54
+ .. image:: https://raw.githubusercontent.com/aio-libs/aiohttp/master/docs/aiohttp-plain.svg
55
+ :height: 64px
56
+ :width: 64px
57
+ :alt: aiohttp logo
58
+
59
+ |
60
+
61
+ .. image:: https://github.com/aio-libs/aiohttp/workflows/CI/badge.svg
62
+ :target: https://github.com/aio-libs/aiohttp/actions?query=workflow%3ACI
63
+ :alt: GitHub Actions status for master branch
64
+
65
+ .. image:: https://codecov.io/gh/aio-libs/aiohttp/branch/master/graph/badge.svg
66
+ :target: https://codecov.io/gh/aio-libs/aiohttp
67
+ :alt: codecov.io status for master branch
68
+
69
+ .. image:: https://badge.fury.io/py/aiohttp.svg
70
+ :target: https://pypi.org/project/aiohttp
71
+ :alt: Latest PyPI package version
72
+
73
+ .. image:: https://img.shields.io/pypi/dm/aiohttp
74
+ :target: https://pypistats.org/packages/aiohttp
75
+ :alt: Downloads count
76
+
77
+ .. image:: https://readthedocs.org/projects/aiohttp/badge/?version=latest
78
+ :target: https://docs.aiohttp.org/
79
+ :alt: Latest Read The Docs
80
+
81
+ .. image:: https://img.shields.io/endpoint?url=https://codspeed.io/badge.json
82
+ :target: https://codspeed.io/aio-libs/aiohttp
83
+ :alt: Codspeed.io status for aiohttp
84
+
85
+
86
+ Key Features
87
+ ============
88
+
89
+ - Supports both client and server side of HTTP protocol.
90
+ - Supports both client and server Web-Sockets out-of-the-box and avoids
91
+ Callback Hell.
92
+ - Provides Web-server with middleware and pluggable routing.
93
+
94
+
95
+ Getting started
96
+ ===============
97
+
98
+ Client
99
+ ------
100
+
101
+ To get something from the web:
102
+
103
+ .. code-block:: python
104
+
105
+ import aiohttp
106
+ import asyncio
107
+
108
+ async def main():
109
+
110
+ async with aiohttp.ClientSession() as session:
111
+ async with session.get('http://python.org') as response:
112
+
113
+ print("Status:", response.status)
114
+ print("Content-type:", response.headers['content-type'])
115
+
116
+ html = await response.text()
117
+ print("Body:", html[:15], "...")
118
+
119
+ asyncio.run(main())
120
+
121
+ This prints:
122
+
123
+ .. code-block::
124
+
125
+ Status: 200
126
+ Content-type: text/html; charset=utf-8
127
+ Body: <!doctype html> ...
128
+
129
+ Coming from `requests <https://requests.readthedocs.io/>`_ ? Read `why we need so many lines <https://aiohttp.readthedocs.io/en/latest/http_request_lifecycle.html>`_.
130
+
131
+ Server
132
+ ------
133
+
134
+ An example using a simple server:
135
+
136
+ .. code-block:: python
137
+
138
+ # examples/server_simple.py
139
+ from aiohttp import web
140
+
141
+ async def handle(request):
142
+ name = request.match_info.get('name', "Anonymous")
143
+ text = "Hello, " + name
144
+ return web.Response(text=text)
145
+
146
+ async def wshandle(request):
147
+ ws = web.WebSocketResponse()
148
+ await ws.prepare(request)
149
+
150
+ async for msg in ws:
151
+ if msg.type == web.WSMsgType.text:
152
+ await ws.send_str("Hello, {}".format(msg.data))
153
+ elif msg.type == web.WSMsgType.binary:
154
+ await ws.send_bytes(msg.data)
155
+ elif msg.type == web.WSMsgType.close:
156
+ break
157
+
158
+ return ws
159
+
160
+
161
+ app = web.Application()
162
+ app.add_routes([web.get('/', handle),
163
+ web.get('/echo', wshandle),
164
+ web.get('/{name}', handle)])
165
+
166
+ if __name__ == '__main__':
167
+ web.run_app(app)
168
+
169
+
170
+ Documentation
171
+ =============
172
+
173
+ https://aiohttp.readthedocs.io/
174
+
175
+
176
+ Demos
177
+ =====
178
+
179
+ https://github.com/aio-libs/aiohttp-demos
180
+
181
+
182
+ External links
183
+ ==============
184
+
185
+ * `Third party libraries
186
+ <http://aiohttp.readthedocs.io/en/latest/third_party.html>`_
187
+ * `Built with aiohttp
188
+ <http://aiohttp.readthedocs.io/en/latest/built_with.html>`_
189
+ * `Powered by aiohttp
190
+ <http://aiohttp.readthedocs.io/en/latest/powered_by.html>`_
191
+
192
+ Feel free to make a Pull Request for adding your link to these pages!
193
+
194
+
195
+ Communication channels
196
+ ======================
197
+
198
+ *aio-libs Discussions*: https://github.com/aio-libs/aiohttp/discussions
199
+
200
+ *Matrix*: `#aio-libs:matrix.org <https://matrix.to/#/#aio-libs:matrix.org>`_
201
+
202
+ We support `Stack Overflow
203
+ <https://stackoverflow.com/questions/tagged/aiohttp>`_.
204
+ Please add *aiohttp* tag to your question there.
205
+
206
+ Requirements
207
+ ============
208
+
209
+ - attrs_
210
+ - multidict_
211
+ - yarl_
212
+ - frozenlist_
213
+
214
+ Optionally you may install the aiodns_ library (highly recommended for sake of speed).
215
+
216
+ .. _aiodns: https://pypi.python.org/pypi/aiodns
217
+ .. _attrs: https://github.com/python-attrs/attrs
218
+ .. _multidict: https://pypi.python.org/pypi/multidict
219
+ .. _frozenlist: https://pypi.org/project/frozenlist/
220
+ .. _yarl: https://pypi.python.org/pypi/yarl
221
+ .. _async-timeout: https://pypi.python.org/pypi/async_timeout
222
+
223
+ License
224
+ =======
225
+
226
+ ``aiohttp`` is offered under the Apache 2 license.
227
+
228
+
229
+ Keepsafe
230
+ ========
231
+
232
+ The aiohttp community would like to thank Keepsafe
233
+ (https://www.getkeepsafe.com) for its support in the early days of
234
+ the project.
235
+
236
+
237
+ Source code
238
+ ===========
239
+
240
+ The latest developer version is available in a GitHub repository:
241
+ https://github.com/aio-libs/aiohttp
242
+
243
+ Benchmarks
244
+ ==========
245
+
246
+ If you are interested in efficiency, the AsyncIO community maintains a
247
+ list of benchmarks on the official wiki:
248
+ https://github.com/python/asyncio/wiki/Benchmarks
249
+
250
+ --------
251
+
252
+ .. image:: https://img.shields.io/matrix/aio-libs:matrix.org?label=Discuss%20on%20Matrix%20at%20%23aio-libs%3Amatrix.org&logo=matrix&server_fqdn=matrix.org&style=flat
253
+ :target: https://matrix.to/#/%23aio-libs:matrix.org
254
+ :alt: Matrix Room — #aio-libs:matrix.org
255
+
256
+ .. image:: https://img.shields.io/matrix/aio-libs-space:matrix.org?label=Discuss%20on%20Matrix%20at%20%23aio-libs-space%3Amatrix.org&logo=matrix&server_fqdn=matrix.org&style=flat
257
+ :target: https://matrix.to/#/%23aio-libs-space:matrix.org
258
+ :alt: Matrix Space — #aio-libs-space:matrix.org
259
+
260
+ .. image:: https://insights.linuxfoundation.org/api/badge/health-score?project=aiohttp
261
+ :target: https://insights.linuxfoundation.org/project/aiohttp
262
+ :alt: LFX Health Score
aiohttp-3.13.3.dist-info/RECORD ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ aiohttp-3.13.3.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2
2
+ aiohttp-3.13.3.dist-info/METADATA,sha256=CQROZCStho-eb7xiFIuAzj30JuupEU_jHpYDFiG_HhM,8145
3
+ aiohttp-3.13.3.dist-info/RECORD,,
4
+ aiohttp-3.13.3.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ aiohttp-3.13.3.dist-info/WHEEL,sha256=DxRnWQz-Kp9-4a4hdDHsSv0KUC3H7sN9Nbef3-8RjXU,190
6
+ aiohttp-3.13.3.dist-info/licenses/LICENSE.txt,sha256=n4DQ2311WpQdtFchcsJw7L2PCCuiFd3QlZhZQu2Uqes,588
7
+ aiohttp-3.13.3.dist-info/licenses/vendor/llhttp/LICENSE,sha256=68qFTgE0zSVtZzYnwgSZ9CV363S6zwi58ltianPJEnc,1105
8
+ aiohttp-3.13.3.dist-info/top_level.txt,sha256=iv-JIaacmTl-hSho3QmphcKnbRRYx1st47yjz_178Ro,8
9
+ aiohttp/.hash/_cparser.pxd.hash,sha256=pjs-sEXNw_eijXGAedwG-BHnlFp8B7sOCgUagIWaU2A,121
10
+ aiohttp/.hash/_find_header.pxd.hash,sha256=_mbpD6vM-CVCKq3ulUvsOAz5Wdo88wrDzfpOsMQaMNA,125
11
+ aiohttp/.hash/_http_parser.pyx.hash,sha256=RKkD9x-EhXksvXrpCaTNWYtffb52urLvuTnxbTN2Lmw,125
12
+ aiohttp/.hash/_http_writer.pyx.hash,sha256=9txOh7t7c3y-vLmiuEY5dltmXvEo0CYyU4U853yyv9E,125
13
+ aiohttp/.hash/hdrs.py.hash,sha256=v6IaKbsxjsdQxBzhb5AjP0x_9G3rUe84D7avf7AI4cs,116
14
+ aiohttp/__init__.py,sha256=QWssFaD-DaFFcwP36lLUQzRmlSZ5KxivJBU-yg5C1wg,8302
15
+ aiohttp/_cookie_helpers.py,sha256=_p7y-B8OCAk7FLjByiuwFIpDLGuNoJn3_vixzymAFnE,13659
16
+ aiohttp/_cparser.pxd,sha256=UnbUYCHg4NdXfgyRVYAMv2KTLWClB4P-xCrvtj_r7ew,4295
17
+ aiohttp/_find_header.pxd,sha256=0GfwFCPN2zxEKTO1_MA5sYq2UfzsG8kcV3aTqvwlz3g,68
18
+ aiohttp/_headers.pxi,sha256=n701k28dVPjwRnx5j6LpJhLTfj7dqu2vJt7f0O60Oyg,2007
19
+ aiohttp/_http_parser.cpython-312-x86_64-linux-gnu.so,sha256=WZP45rtTvKwOq_1uXO_1L84Kz6I0AnqYZn0b5L-6HkA,2833152
20
+ aiohttp/_http_parser.pyx,sha256=-YI8YIY4uKd_7Bwr0o3FwEPwjHdexZ5-Ji3XS067c4Q,28261
21
+ aiohttp/_http_writer.cpython-312-x86_64-linux-gnu.so,sha256=mWC-4rsbntVD1V5ZEKEpSW_sm63V0XRe1fDR0lygipo,539144
22
+ aiohttp/_http_writer.pyx,sha256=VlFEBM6HoVv8a0AAJtc6JwFlsv2-cDE8-gB94p3dfhQ,4664
23
+ aiohttp/_websocket/.hash/mask.pxd.hash,sha256=Y0zBddk_ck3pi9-BFzMcpkcvCKvwvZ4GTtZFb9u1nxQ,128
24
+ aiohttp/_websocket/.hash/mask.pyx.hash,sha256=90owpXYM8_kIma4KUcOxhWSk-Uv4NVMBoCYeFM1B3d0,128
25
+ aiohttp/_websocket/.hash/reader_c.pxd.hash,sha256=5xf3oobk6vx4xbJm-xtZ1_QufB8fYFtLQV2MNdqUc1w,132
26
+ aiohttp/_websocket/__init__.py,sha256=Mar3R9_vBN_Ea4lsW7iTAVXD7OKswKPGqF5xgSyt77k,44
27
+ aiohttp/_websocket/helpers.py,sha256=P-XLv8IUaihKzDenVUqfKU5DJbWE5HvG8uhvUZK8Ic4,5038
28
+ aiohttp/_websocket/mask.cpython-312-x86_64-linux-gnu.so,sha256=EpRwPJm1K1yavMCd9llAWdT4AsqKx_QEN0rb0eJH_Kc,263512
29
+ aiohttp/_websocket/mask.pxd,sha256=sBmZ1Amym9kW4Ge8lj1fLZ7mPPya4LzLdpkQExQXv5M,112
30
+ aiohttp/_websocket/mask.pyx,sha256=BHjOtV0O0w7xp9p0LNADRJvGmgfPn9sGeJvSs0fL__4,1397
31
+ aiohttp/_websocket/models.py,sha256=XAzjs_8JYszWXIgZ6R3ZRrF-tX9Q_6LiD49WRYojopM,2121
32
+ aiohttp/_websocket/reader.py,sha256=eC4qS0c5sOeQ2ebAHLaBpIaTVFaSKX79pY2xvh3Pqyw,1030
33
+ aiohttp/_websocket/reader_c.cpython-312-x86_64-linux-gnu.so,sha256=GYd-y-IkGkOIp3vmma5BmZgmG9Py9fLTybcmMWyHNf0,1822128
34
+ aiohttp/_websocket/reader_c.pxd,sha256=nl_njtDrzlQU0rjgGGjZDB-swguE0tX_bCPobkShVa4,2625
35
+ aiohttp/_websocket/reader_c.py,sha256=V5YtZ2gj2BjE2Q-W9sR_MdAl1VAm1pB7ZjozVJcOpbg,18868
36
+ aiohttp/_websocket/reader_py.py,sha256=V5YtZ2gj2BjE2Q-W9sR_MdAl1VAm1pB7ZjozVJcOpbg,18868
37
+ aiohttp/_websocket/writer.py,sha256=2OvSktPmNh_g20h1cXJt2Xu8u6IvswnPjdur7OwBbJk,11261
38
+ aiohttp/abc.py,sha256=M66F4S6m00bIEn7y4ha_XLTMDmVQ9dPihfOVB0pGfOo,7149
39
+ aiohttp/base_protocol.py,sha256=Tp8cxUPQvv9kUPk3w6lAzk6d2MAzV3scwI_3Go3C47c,3025
40
+ aiohttp/client.py,sha256=fOQfwcIUL1NGAVRV4DDj6-wipBzeD8KZpmzhO-LLKp4,58357
41
+ aiohttp/client_exceptions.py,sha256=uyKbxI2peZhKl7lELBMx3UeusNkfpemPWpGFq0r6JeM,11367
42
+ aiohttp/client_middleware_digest_auth.py,sha256=G5JM9YtzL9AWklz6NP28xEOBeAvrAZgDzU657JqO4qs,17627
43
+ aiohttp/client_middlewares.py,sha256=kP5N9CMzQPMGPIEydeVUiLUTLsw8Vl8Gr4qAWYdu3vM,1918
44
+ aiohttp/client_proto.py,sha256=56_WtLStZGBFPYKzgEgY6v24JkhV1y6JEmmuxeJT2So,12110
45
+ aiohttp/client_reqrep.py,sha256=eEREDrZ0M8ZFTt1wjHduR-P8_sm40K65gNz-iMGYask,53391
46
+ aiohttp/client_ws.py,sha256=1CIjIXwyzOMIYw6AjUES4-qUwbyVHW1seJKQfg_Rta8,15109
47
+ aiohttp/compression_utils.py,sha256=hJ2LXhN2OWukFHm5b78TJFGKcAiL2kthi9Sf5PRYO-U,11738
48
+ aiohttp/connector.py,sha256=vT22BNuCDtbadE1Uq7HC7zpOWCHMxI4n3PtCz7zZZkw,69004
49
+ aiohttp/cookiejar.py,sha256=e28ZMQwJ5P0vbPX1OX4Se7-k3zeGvocFEqzGhwpG53k,18922
50
+ aiohttp/formdata.py,sha256=xqYMbUo1qoLYPuzY92XeR4pyEe-w-DNcToARDF3GUhA,6384
51
+ aiohttp/hdrs.py,sha256=2rj5MyA-6yRdYPhW5UKkW4iNWhEAlGIOSBH5D4FmKNE,5111
52
+ aiohttp/helpers.py,sha256=Q1307PCEnWz4RP8crUw8dk58c0YF2Ei3JywkKfRxz5E,30629
53
+ aiohttp/http.py,sha256=8o8j8xH70OWjnfTWA9V44NR785QPxEPrUtzMXiAVpwc,1842
54
+ aiohttp/http_exceptions.py,sha256=BjIxD4LtrQgytqoR5lOI9zAttNmSygRgksUsMRy7sss,3069
55
+ aiohttp/http_parser.py,sha256=z6djZDOUs7hdPzplTEsAVyz0of-rQAwT7xz8OpXhnuY,38177
56
+ aiohttp/http_websocket.py,sha256=8VXFKw6KQUEmPg48GtRMB37v0gTK7A0inoxXuDxMZEc,842
57
+ aiohttp/http_writer.py,sha256=fbRtKPYSqRbtAdr_gqpjF2-4sI1ESL8dPDF-xY_mAMY,12446
58
+ aiohttp/log.py,sha256=BbNKx9e3VMIm0xYjZI0IcBBoS7wjdeIeSaiJE7-qK2g,325
59
+ aiohttp/multipart.py,sha256=326npYdWxYI3raoRfmpBeUV_ef3-LRn8sV9WqcIOoPk,40482
60
+ aiohttp/payload.py,sha256=O6nsYNULL7AeM2cyJ6TYX73ncVnL5xJwt5AegxwMKqw,40874
61
+ aiohttp/payload_streamer.py,sha256=ZzEYyfzcjGWkVkK3XR2pBthSCSIykYvY3Wr5cGQ2eTc,2211
62
+ aiohttp/py.typed,sha256=sow9soTwP9T_gEAQSVh7Gb8855h04Nwmhs2We-JRgZM,7
63
+ aiohttp/pytest_plugin.py,sha256=z4XwqmsKdyJCKxbGiA5kFf90zcedvomqk4RqjZbhKNk,12901
64
+ aiohttp/resolver.py,sha256=gsrfUpFf8iHlcHfJvY-1fiBHW3PRvRVNb5lNZBg3zlY,10031
65
+ aiohttp/streams.py,sha256=rlwL7ek6CkMMYil_e_EokWv26uHmtzi3lKqlnLNrXCc,23666
66
+ aiohttp/tcp_helpers.py,sha256=BSadqVWaBpMFDRWnhaaR941N9MiDZ7bdTrxgCb0CW-M,961
67
+ aiohttp/test_utils.py,sha256=ZJSzZWjC76KSbtwddTKcP6vHpUl_ozfAf3F93ewmHRU,23016
68
+ aiohttp/tracing.py,sha256=-6aaW6l0J9uJD45LzR4cijYH0j62pt0U_nn_aVzFku4,14558
69
+ aiohttp/typedefs.py,sha256=wUlqwe9Mw9W8jT3HsYJcYk00qP3EMPz3nTkYXmeNN48,1657
70
+ aiohttp/web.py,sha256=JzSNmejg5G6YeFAnkIgZfytqbU86sNu844yYKmoUpqs,17852
71
+ aiohttp/web_app.py,sha256=lGU_aAMN-h3wy-LTTHi6SeKH8ydt1G51BXcCspgD5ZA,19452
72
+ aiohttp/web_exceptions.py,sha256=7nIuiwhZ39vJJ9KrWqArA5QcWbUdqkz2CLwEpJapeN8,10360
73
+ aiohttp/web_fileresponse.py,sha256=Xzau8EMrWNrFg3u46h4UEteg93G4zYq94CU6vy0HiqE,16362
74
+ aiohttp/web_log.py,sha256=rX5D7xLOX2B6BMdiZ-chme_KfJfW5IXEoFwLfkfkajs,7865
75
+ aiohttp/web_middlewares.py,sha256=sFI0AgeNjdyAjuz92QtMIpngmJSOxrqe2Jfbs4BNUu0,4165
76
+ aiohttp/web_protocol.py,sha256=6s9dMzmaqW77bzM1T111uGNSLFo6gNmfDg7XzYnA8xk,27010
77
+ aiohttp/web_request.py,sha256=KqrOp6AeWB5e6tKrG55Lo7Zbwq49DxdrKniuW2t2u04,29849
78
+ aiohttp/web_response.py,sha256=PKcziNU4LmftXqKVvoRMrAbOeVClpSN-iznHsiWezmU,29341
79
+ aiohttp/web_routedef.py,sha256=VT1GAx6BrawoDh5RwBwBu5wSABSqgWwAe74AUCyZAEo,6110
80
+ aiohttp/web_runner.py,sha256=v1G1nKiOOQgFnTSR4IMc6I9ReEFDMaHtMLvO_roDM-A,11786
81
+ aiohttp/web_server.py,sha256=-9WDKUAiR9ll-rSdwXSqG6YjaoW79d1R4y0BGSqgUMA,2888
82
+ aiohttp/web_urldispatcher.py,sha256=JM-TlriKCNbTLNL43Ra9sdZ0zChxZmIEYQM6ZpbyjI4,44290
83
+ aiohttp/web_ws.py,sha256=lItgmyatkXh0M6EY7JoZnSZkUl6R0wv8B88X4ILqQbU,22739
84
+ aiohttp/worker.py,sha256=zT0iWN5Xze194bO6_VjHou0x7lR_k0MviN6Kadnk22g,8152
aiohttp-3.13.3.dist-info/REQUESTED ADDED
File without changes
aiohttp-3.13.3.dist-info/WHEEL ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: false
4
+ Tag: cp312-cp312-manylinux_2_17_x86_64
5
+ Tag: cp312-cp312-manylinux2014_x86_64
6
+ Tag: cp312-cp312-manylinux_2_28_x86_64
7
+
aiohttp-3.13.3.dist-info/top_level.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ aiohttp
aiohttp/_cookie_helpers.py ADDED
@@ -0,0 +1,338 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Internal cookie handling helpers.
3
+
4
+ This module contains internal utilities for cookie parsing and manipulation.
5
+ These are not part of the public API and may change without notice.
6
+ """
7
+
8
+ import re
9
+ from http.cookies import Morsel
10
+ from typing import List, Optional, Sequence, Tuple, cast
11
+
12
+ from .log import internal_logger
13
+
14
+ __all__ = (
15
+ "parse_set_cookie_headers",
16
+ "parse_cookie_header",
17
+ "preserve_morsel_with_coded_value",
18
+ )
19
+
20
+ # Cookie parsing constants
21
+ # Allow more characters in cookie names to handle real-world cookies
22
+ # that don't strictly follow RFC standards (fixes #2683)
23
+ # RFC 6265 defines cookie-name token as per RFC 2616 Section 2.2,
24
+ # but many servers send cookies with characters like {} [] () etc.
25
+ # This makes the cookie parser more tolerant of real-world cookies
26
+ # while still providing some validation to catch obviously malformed names.
27
+ _COOKIE_NAME_RE = re.compile(r"^[!#$%&\'()*+\-./0-9:<=>?@A-Z\[\]^_`a-z{|}~]+$")
28
+ _COOKIE_KNOWN_ATTRS = frozenset( # AKA Morsel._reserved
29
+ (
30
+ "path",
31
+ "domain",
32
+ "max-age",
33
+ "expires",
34
+ "secure",
35
+ "httponly",
36
+ "samesite",
37
+ "partitioned",
38
+ "version",
39
+ "comment",
40
+ )
41
+ )
42
+ _COOKIE_BOOL_ATTRS = frozenset( # AKA Morsel._flags
43
+ ("secure", "httponly", "partitioned")
44
+ )
45
+
46
+ # SimpleCookie's pattern for parsing cookies with relaxed validation
47
+ # Based on http.cookies pattern but extended to allow more characters in cookie names
48
+ # to handle real-world cookies (fixes #2683)
49
+ _COOKIE_PATTERN = re.compile(
50
+ r"""
51
+ \s* # Optional whitespace at start of cookie
52
+ (?P<key> # Start of group 'key'
53
+ # aiohttp has extended to include [] for compatibility with real-world cookies
54
+ [\w\d!#%&'~_`><@,:/\$\*\+\-\.\^\|\)\(\?\}\{\[\]]+ # Any word of at least one letter
55
+ ) # End of group 'key'
56
+ ( # Optional group: there may not be a value.
57
+ \s*=\s* # Equal Sign
58
+ (?P<val> # Start of group 'val'
59
+ "(?:[^\\"]|\\.)*" # Any double-quoted string (properly closed)
60
+ | # or
61
+ "[^";]* # Unmatched opening quote (differs from SimpleCookie - issue #7993)
62
+ | # or
63
+ # Special case for "expires" attr - RFC 822, RFC 850, RFC 1036, RFC 1123
64
+ (\w{3,6}day|\w{3}),\s # Day of the week or abbreviated day (with comma)
65
+ [\w\d\s-]{9,11}\s[\d:]{8}\s # Date and time in specific format
66
+ (GMT|[+-]\d{4}) # Timezone: GMT or RFC 2822 offset like -0000, +0100
67
+ # NOTE: RFC 2822 timezone support is an aiohttp extension
68
+ # for issue #4493 - SimpleCookie does NOT support this
69
+ | # or
70
+ # ANSI C asctime() format: "Wed Jun 9 10:18:14 2021"
71
+ # NOTE: This is an aiohttp extension for issue #4327 - SimpleCookie does NOT support this format
72
+ \w{3}\s+\w{3}\s+[\s\d]\d\s+\d{2}:\d{2}:\d{2}\s+\d{4}
73
+ | # or
74
+ [\w\d!#%&'~_`><@,:/\$\*\+\-\.\^\|\)\(\?\}\{\=\[\]]* # Any word or empty string
75
+ ) # End of group 'val'
76
+ )? # End of optional value group
77
+ \s* # Any number of spaces.
78
+ (\s+|;|$) # Ending either at space, semicolon, or EOS.
79
+ """,
80
+ re.VERBOSE | re.ASCII,
81
+ )
82
+
83
+
84
+ def preserve_morsel_with_coded_value(cookie: Morsel[str]) -> Morsel[str]:
85
+ """
86
+ Preserve a Morsel's coded_value exactly as received from the server.
87
+
88
+ This function ensures that cookie encoding is preserved exactly as sent by
89
+ the server, which is critical for compatibility with old servers that have
90
+ strict requirements about cookie formats.
91
+
92
+ This addresses the issue described in https://github.com/aio-libs/aiohttp/pull/1453
93
+ where Python's SimpleCookie would re-encode cookies, breaking authentication
94
+ with certain servers.
95
+
96
+ Args:
97
+ cookie: A Morsel object from SimpleCookie
98
+
99
+ Returns:
100
+ A Morsel object with preserved coded_value
101
+
102
+ """
103
+ mrsl_val = cast("Morsel[str]", cookie.get(cookie.key, Morsel()))
104
+ # We use __setstate__ instead of the public set() API because it allows us to
105
+ # bypass validation and set already validated state. This is more stable than
106
+ # setting protected attributes directly and unlikely to change since it would
107
+ # break pickling.
108
+ mrsl_val.__setstate__( # type: ignore[attr-defined]
109
+ {"key": cookie.key, "value": cookie.value, "coded_value": cookie.coded_value}
110
+ )
111
+ return mrsl_val
112
+
113
+
114
+ _unquote_sub = re.compile(r"\\(?:([0-3][0-7][0-7])|(.))").sub
115
+
116
+
117
+ def _unquote_replace(m: re.Match[str]) -> str:
118
+ """
119
+ Replace function for _unquote_sub regex substitution.
120
+
121
+ Handles escaped characters in cookie values:
122
+ - Octal sequences are converted to their character representation
123
+ - Other escaped characters are unescaped by removing the backslash
124
+ """
125
+ if m[1]:
126
+ return chr(int(m[1], 8))
127
+ return m[2]
128
+
129
+
130
+ def _unquote(value: str) -> str:
131
+ """
132
+ Unquote a cookie value.
133
+
134
+ Vendored from http.cookies._unquote to ensure compatibility.
135
+
136
+ Note: The original implementation checked for None, but we've removed
137
+ that check since all callers already ensure the value is not None.
138
+ """
139
+ # If there aren't any doublequotes,
140
+ # then there can't be any special characters. See RFC 2109.
141
+ if len(value) < 2:
142
+ return value
143
+ if value[0] != '"' or value[-1] != '"':
144
+ return value
145
+
146
+ # We have to assume that we must decode this string.
147
+ # Down to work.
148
+
149
+ # Remove the "s
150
+ value = value[1:-1]
151
+
152
+ # Check for special sequences. Examples:
153
+ # \012 --> \n
154
+ # \" --> "
155
+ #
156
+ return _unquote_sub(_unquote_replace, value)
157
+
158
+
159
+ def parse_cookie_header(header: str) -> List[Tuple[str, Morsel[str]]]:
160
+ """
161
+ Parse a Cookie header according to RFC 6265 Section 5.4.
162
+
163
+ Cookie headers contain only name-value pairs separated by semicolons.
164
+ There are no attributes in Cookie headers - even names that match
165
+ attribute names (like 'path' or 'secure') should be treated as cookies.
166
+
167
+ This parser uses the same regex-based approach as parse_set_cookie_headers
168
+ to properly handle quoted values that may contain semicolons. When the
169
+ regex fails to match a malformed cookie, it falls back to simple parsing
170
+ to ensure subsequent cookies are not lost
171
+ https://github.com/aio-libs/aiohttp/issues/11632
172
+
173
+ Args:
174
+ header: The Cookie header value to parse
175
+
176
+ Returns:
177
+ List of (name, Morsel) tuples for compatibility with SimpleCookie.update()
178
+ """
179
+ if not header:
180
+ return []
181
+
182
+ cookies: List[Tuple[str, Morsel[str]]] = []
183
+ morsel: Morsel[str]
184
+ i = 0
185
+ n = len(header)
186
+
187
+ invalid_names = []
188
+ while i < n:
189
+ # Use the same pattern as parse_set_cookie_headers to find cookies
190
+ match = _COOKIE_PATTERN.match(header, i)
191
+ if not match:
192
+ # Fallback for malformed cookies https://github.com/aio-libs/aiohttp/issues/11632
193
+ # Find next semicolon to skip or attempt simple key=value parsing
194
+ next_semi = header.find(";", i)
195
+ eq_pos = header.find("=", i)
196
+
197
+ # Try to extract key=value if '=' comes before ';'
198
+ if eq_pos != -1 and (next_semi == -1 or eq_pos < next_semi):
199
+ end_pos = next_semi if next_semi != -1 else n
200
+ key = header[i:eq_pos].strip()
201
+ value = header[eq_pos + 1 : end_pos].strip()
202
+
203
+ # Validate the name (same as regex path)
204
+ if not _COOKIE_NAME_RE.match(key):
205
+ invalid_names.append(key)
206
+ else:
207
+ morsel = Morsel()
208
+ morsel.__setstate__( # type: ignore[attr-defined]
209
+ {"key": key, "value": _unquote(value), "coded_value": value}
210
+ )
211
+ cookies.append((key, morsel))
212
+
213
+ # Move to next cookie or end
214
+ i = next_semi + 1 if next_semi != -1 else n
215
+ continue
216
+
217
+ key = match.group("key")
218
+ value = match.group("val") or ""
219
+ i = match.end(0)
220
+
221
+ # Validate the name
222
+ if not key or not _COOKIE_NAME_RE.match(key):
223
+ invalid_names.append(key)
224
+ continue
225
+
226
+ # Create new morsel
227
+ morsel = Morsel()
228
+ # Preserve the original value as coded_value (with quotes if present)
229
+ # We use __setstate__ instead of the public set() API because it allows us to
230
+ # bypass validation and set already validated state. This is more stable than
231
+ # setting protected attributes directly and unlikely to change since it would
232
+ # break pickling.
233
+ morsel.__setstate__( # type: ignore[attr-defined]
234
+ {"key": key, "value": _unquote(value), "coded_value": value}
235
+ )
236
+
237
+ cookies.append((key, morsel))
238
+
239
+ if invalid_names:
240
+ internal_logger.debug(
241
+ "Cannot load cookie. Illegal cookie names: %r", invalid_names
242
+ )
243
+
244
+ return cookies
245
+
246
+
247
+ def parse_set_cookie_headers(headers: Sequence[str]) -> List[Tuple[str, Morsel[str]]]:
248
+ """
249
+ Parse cookie headers using a vendored version of SimpleCookie parsing.
250
+
251
+ This implementation is based on SimpleCookie.__parse_string to ensure
252
+ compatibility with how SimpleCookie parses cookies, including handling
253
+ of malformed cookies with missing semicolons.
254
+
255
+ This function is used for both Cookie and Set-Cookie headers in order to be
256
+ forgiving. Ideally we would have followed RFC 6265 Section 5.2 (for Cookie
257
+ headers) and RFC 6265 Section 4.2.1 (for Set-Cookie headers), but the
258
+ real world data makes it impossible since we need to be a bit more forgiving.
259
+
260
+ NOTE: This implementation differs from SimpleCookie in handling unmatched quotes.
261
+ SimpleCookie will stop parsing when it encounters a cookie value with an unmatched
262
+ quote (e.g., 'cookie="value'), causing subsequent cookies to be silently dropped.
263
+ This implementation handles unmatched quotes more gracefully to prevent cookie loss.
264
+ See https://github.com/aio-libs/aiohttp/issues/7993
265
+ """
266
+ parsed_cookies: List[Tuple[str, Morsel[str]]] = []
267
+
268
+ for header in headers:
269
+ if not header:
270
+ continue
271
+
272
+ # Parse cookie string using SimpleCookie's algorithm
273
+ i = 0
274
+ n = len(header)
275
+ current_morsel: Optional[Morsel[str]] = None
276
+ morsel_seen = False
277
+
278
+ while 0 <= i < n:
279
+ # Start looking for a cookie
280
+ match = _COOKIE_PATTERN.match(header, i)
281
+ if not match:
282
+ # No more cookies
283
+ break
284
+
285
+ key, value = match.group("key"), match.group("val")
286
+ i = match.end(0)
287
+ lower_key = key.lower()
288
+
289
+ if key[0] == "$":
290
+ if not morsel_seen:
291
+ # We ignore attributes which pertain to the cookie
292
+ # mechanism as a whole, such as "$Version".
293
+ continue
294
+ # Process as attribute
295
+ if current_morsel is not None:
296
+ attr_lower_key = lower_key[1:]
297
+ if attr_lower_key in _COOKIE_KNOWN_ATTRS:
298
+ current_morsel[attr_lower_key] = value or ""
299
+ elif lower_key in _COOKIE_KNOWN_ATTRS:
300
+ if not morsel_seen:
301
+ # Invalid cookie string - attribute before cookie
302
+ break
303
+ if lower_key in _COOKIE_BOOL_ATTRS:
304
+ # Boolean attribute with any value should be True
305
+ if current_morsel is not None and current_morsel.isReservedKey(key):
306
+ current_morsel[lower_key] = True
307
+ elif value is None:
308
+ # Invalid cookie string - non-boolean attribute without value
309
+ break
310
+ elif current_morsel is not None:
311
+ # Regular attribute with value
312
+ current_morsel[lower_key] = _unquote(value)
313
+ elif value is not None:
314
+ # This is a cookie name=value pair
315
+ # Validate the name
316
+ if key in _COOKIE_KNOWN_ATTRS or not _COOKIE_NAME_RE.match(key):
317
+ internal_logger.warning(
318
+ "Can not load cookies: Illegal cookie name %r", key
319
+ )
320
+ current_morsel = None
321
+ else:
322
+ # Create new morsel
323
+ current_morsel = Morsel()
324
+ # Preserve the original value as coded_value (with quotes if present)
325
+ # We use __setstate__ instead of the public set() API because it allows us to
326
+ # bypass validation and set already validated state. This is more stable than
327
+ # setting protected attributes directly and unlikely to change since it would
328
+ # break pickling.
329
+ current_morsel.__setstate__( # type: ignore[attr-defined]
330
+ {"key": key, "value": _unquote(value), "coded_value": value}
331
+ )
332
+ parsed_cookies.append((key, current_morsel))
333
+ morsel_seen = True
334
+ else:
335
+ # Invalid cookie string - no value for non-attribute
336
+ break
337
+
338
+ return parsed_cookies
aiohttp/_cparser.pxd ADDED
@@ -0,0 +1,158 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from libc.stdint cimport int32_t, uint8_t, uint16_t, uint64_t
2
+
3
+
4
+ cdef extern from "llhttp.h":
5
+
6
+ struct llhttp__internal_s:
7
+ int32_t _index
8
+ void* _span_pos0
9
+ void* _span_cb0
10
+ int32_t error
11
+ const char* reason
12
+ const char* error_pos
13
+ void* data
14
+ void* _current
15
+ uint64_t content_length
16
+ uint8_t type
17
+ uint8_t method
18
+ uint8_t http_major
19
+ uint8_t http_minor
20
+ uint8_t header_state
21
+ uint8_t lenient_flags
22
+ uint8_t upgrade
23
+ uint8_t finish
24
+ uint16_t flags
25
+ uint16_t status_code
26
+ void* settings
27
+
28
+ ctypedef llhttp__internal_s llhttp__internal_t
29
+ ctypedef llhttp__internal_t llhttp_t
30
+
31
+ ctypedef int (*llhttp_data_cb)(llhttp_t*, const char *at, size_t length) except -1
32
+ ctypedef int (*llhttp_cb)(llhttp_t*) except -1
33
+
34
+ struct llhttp_settings_s:
35
+ llhttp_cb on_message_begin
36
+ llhttp_data_cb on_url
37
+ llhttp_data_cb on_status
38
+ llhttp_data_cb on_header_field
39
+ llhttp_data_cb on_header_value
40
+ llhttp_cb on_headers_complete
41
+ llhttp_data_cb on_body
42
+ llhttp_cb on_message_complete
43
+ llhttp_cb on_chunk_header
44
+ llhttp_cb on_chunk_complete
45
+
46
+ llhttp_cb on_url_complete
47
+ llhttp_cb on_status_complete
48
+ llhttp_cb on_header_field_complete
49
+ llhttp_cb on_header_value_complete
50
+
51
+ ctypedef llhttp_settings_s llhttp_settings_t
52
+
53
+ enum llhttp_errno:
54
+ HPE_OK,
55
+ HPE_INTERNAL,
56
+ HPE_STRICT,
57
+ HPE_LF_EXPECTED,
58
+ HPE_UNEXPECTED_CONTENT_LENGTH,
59
+ HPE_CLOSED_CONNECTION,
60
+ HPE_INVALID_METHOD,
61
+ HPE_INVALID_URL,
62
+ HPE_INVALID_CONSTANT,
63
+ HPE_INVALID_VERSION,
64
+ HPE_INVALID_HEADER_TOKEN,
65
+ HPE_INVALID_CONTENT_LENGTH,
66
+ HPE_INVALID_CHUNK_SIZE,
67
+ HPE_INVALID_STATUS,
68
+ HPE_INVALID_EOF_STATE,
69
+ HPE_INVALID_TRANSFER_ENCODING,
70
+ HPE_CB_MESSAGE_BEGIN,
71
+ HPE_CB_HEADERS_COMPLETE,
72
+ HPE_CB_MESSAGE_COMPLETE,
73
+ HPE_CB_CHUNK_HEADER,
74
+ HPE_CB_CHUNK_COMPLETE,
75
+ HPE_PAUSED,
76
+ HPE_PAUSED_UPGRADE,
77
+ HPE_USER
78
+
79
+ ctypedef llhttp_errno llhttp_errno_t
80
+
81
+ enum llhttp_flags:
82
+ F_CHUNKED,
83
+ F_CONTENT_LENGTH
84
+
85
+ enum llhttp_type:
86
+ HTTP_REQUEST,
87
+ HTTP_RESPONSE,
88
+ HTTP_BOTH
89
+
90
+ enum llhttp_method:
91
+ HTTP_DELETE,
92
+ HTTP_GET,
93
+ HTTP_HEAD,
94
+ HTTP_POST,
95
+ HTTP_PUT,
96
+ HTTP_CONNECT,
97
+ HTTP_OPTIONS,
98
+ HTTP_TRACE,
99
+ HTTP_COPY,
100
+ HTTP_LOCK,
101
+ HTTP_MKCOL,
102
+ HTTP_MOVE,
103
+ HTTP_PROPFIND,
104
+ HTTP_PROPPATCH,
105
+ HTTP_SEARCH,
106
+ HTTP_UNLOCK,
107
+ HTTP_BIND,
108
+ HTTP_REBIND,
109
+ HTTP_UNBIND,
110
+ HTTP_ACL,
111
+ HTTP_REPORT,
112
+ HTTP_MKACTIVITY,
113
+ HTTP_CHECKOUT,
114
+ HTTP_MERGE,
115
+ HTTP_MSEARCH,
116
+ HTTP_NOTIFY,
117
+ HTTP_SUBSCRIBE,
118
+ HTTP_UNSUBSCRIBE,
119
+ HTTP_PATCH,
120
+ HTTP_PURGE,
121
+ HTTP_MKCALENDAR,
122
+ HTTP_LINK,
123
+ HTTP_UNLINK,
124
+ HTTP_SOURCE,
125
+ HTTP_PRI,
126
+ HTTP_DESCRIBE,
127
+ HTTP_ANNOUNCE,
128
+ HTTP_SETUP,
129
+ HTTP_PLAY,
130
+ HTTP_PAUSE,
131
+ HTTP_TEARDOWN,
132
+ HTTP_GET_PARAMETER,
133
+ HTTP_SET_PARAMETER,
134
+ HTTP_REDIRECT,
135
+ HTTP_RECORD,
136
+ HTTP_FLUSH
137
+
138
+ ctypedef llhttp_method llhttp_method_t;
139
+
140
+ void llhttp_settings_init(llhttp_settings_t* settings)
141
+ void llhttp_init(llhttp_t* parser, llhttp_type type,
142
+ const llhttp_settings_t* settings)
143
+
144
+ llhttp_errno_t llhttp_execute(llhttp_t* parser, const char* data, size_t len)
145
+
146
+ int llhttp_should_keep_alive(const llhttp_t* parser)
147
+
148
+ void llhttp_resume_after_upgrade(llhttp_t* parser)
149
+
150
+ llhttp_errno_t llhttp_get_errno(const llhttp_t* parser)
151
+ const char* llhttp_get_error_reason(const llhttp_t* parser)
152
+ const char* llhttp_get_error_pos(const llhttp_t* parser)
153
+
154
+ const char* llhttp_method_name(llhttp_method_t method)
155
+
156
+ void llhttp_set_lenient_headers(llhttp_t* parser, int enabled)
157
+ void llhttp_set_lenient_optional_cr_before_lf(llhttp_t* parser, int enabled)
158
+ void llhttp_set_lenient_spaces_after_chunk_size(llhttp_t* parser, int enabled)
aiohttp/abc.py ADDED
@@ -0,0 +1,268 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import logging
3
+ import socket
4
+ from abc import ABC, abstractmethod
5
+ from collections.abc import Sized
6
+ from http.cookies import BaseCookie, Morsel
7
+ from typing import (
8
+ TYPE_CHECKING,
9
+ Any,
10
+ Awaitable,
11
+ Callable,
12
+ Dict,
13
+ Generator,
14
+ Iterable,
15
+ List,
16
+ Optional,
17
+ Sequence,
18
+ Tuple,
19
+ TypedDict,
20
+ Union,
21
+ )
22
+
23
+ from multidict import CIMultiDict
24
+ from yarl import URL
25
+
26
+ from ._cookie_helpers import parse_set_cookie_headers
27
+ from .typedefs import LooseCookies
28
+
29
+ if TYPE_CHECKING:
30
+ from .web_app import Application
31
+ from .web_exceptions import HTTPException
32
+ from .web_request import BaseRequest, Request
33
+ from .web_response import StreamResponse
34
+ else:
35
+ BaseRequest = Request = Application = StreamResponse = None
36
+ HTTPException = None
37
+
38
+
39
+ class AbstractRouter(ABC):
40
+ def __init__(self) -> None:
41
+ self._frozen = False
42
+
43
+ def post_init(self, app: Application) -> None:
44
+ """Post init stage.
45
+
46
+ Not an abstract method for sake of backward compatibility,
47
+ but if the router wants to be aware of the application
48
+ it can override this.
49
+ """
50
+
51
+ @property
52
+ def frozen(self) -> bool:
53
+ return self._frozen
54
+
55
+ def freeze(self) -> None:
56
+ """Freeze router."""
57
+ self._frozen = True
58
+
59
+ @abstractmethod
60
+ async def resolve(self, request: Request) -> "AbstractMatchInfo":
61
+ """Return MATCH_INFO for given request"""
62
+
63
+
64
+ class AbstractMatchInfo(ABC):
65
+
66
+ __slots__ = ()
67
+
68
+ @property # pragma: no branch
69
+ @abstractmethod
70
+ def handler(self) -> Callable[[Request], Awaitable[StreamResponse]]:
71
+ """Execute matched request handler"""
72
+
73
+ @property
74
+ @abstractmethod
75
+ def expect_handler(
76
+ self,
77
+ ) -> Callable[[Request], Awaitable[Optional[StreamResponse]]]:
78
+ """Expect handler for 100-continue processing"""
79
+
80
+ @property # pragma: no branch
81
+ @abstractmethod
82
+ def http_exception(self) -> Optional[HTTPException]:
83
+ """HTTPException instance raised on router's resolving, or None"""
84
+
85
+ @abstractmethod # pragma: no branch
86
+ def get_info(self) -> Dict[str, Any]:
87
+ """Return a dict with additional info useful for introspection"""
88
+
89
+ @property # pragma: no branch
90
+ @abstractmethod
91
+ def apps(self) -> Tuple[Application, ...]:
92
+ """Stack of nested applications.
93
+
94
+ Top level application is left-most element.
95
+
96
+ """
97
+
98
+ @abstractmethod
99
+ def add_app(self, app: Application) -> None:
100
+ """Add application to the nested apps stack."""
101
+
102
+ @abstractmethod
103
+ def freeze(self) -> None:
104
+ """Freeze the match info.
105
+
106
+ The method is called after route resolution.
107
+
108
+ After the call .add_app() is forbidden.
109
+
110
+ """
111
+
112
+
113
+ class AbstractView(ABC):
114
+ """Abstract class based view."""
115
+
116
+ def __init__(self, request: Request) -> None:
117
+ self._request = request
118
+
119
+ @property
120
+ def request(self) -> Request:
121
+ """Request instance."""
122
+ return self._request
123
+
124
+ @abstractmethod
125
+ def __await__(self) -> Generator[None, None, StreamResponse]:
126
+ """Execute the view handler."""
127
+
128
+
129
+ class ResolveResult(TypedDict):
130
+ """Resolve result.
131
+
132
+ This is the result returned from an AbstractResolver's
133
+ resolve method.
134
+
135
+ :param hostname: The hostname that was provided.
136
+ :param host: The IP address that was resolved.
137
+ :param port: The port that was resolved.
138
+ :param family: The address family that was resolved.
139
+ :param proto: The protocol that was resolved.
140
+ :param flags: The flags that were resolved.
141
+ """
142
+
143
+ hostname: str
144
+ host: str
145
+ port: int
146
+ family: int
147
+ proto: int
148
+ flags: int
149
+
150
+
151
+ class AbstractResolver(ABC):
152
+ """Abstract DNS resolver."""
153
+
154
+ @abstractmethod
155
+ async def resolve(
156
+ self, host: str, port: int = 0, family: socket.AddressFamily = socket.AF_INET
157
+ ) -> List[ResolveResult]:
158
+ """Return IP address for given hostname"""
159
+
160
+ @abstractmethod
161
+ async def close(self) -> None:
162
+ """Release resolver"""
163
+
164
+
165
+ if TYPE_CHECKING:
166
+ IterableBase = Iterable[Morsel[str]]
167
+ else:
168
+ IterableBase = Iterable
169
+
170
+
171
+ ClearCookiePredicate = Callable[["Morsel[str]"], bool]
172
+
173
+
174
+ class AbstractCookieJar(Sized, IterableBase):
175
+ """Abstract Cookie Jar."""
176
+
177
+ def __init__(self, *, loop: Optional[asyncio.AbstractEventLoop] = None) -> None:
178
+ self._loop = loop or asyncio.get_running_loop()
179
+
180
+ @property
181
+ @abstractmethod
182
+ def quote_cookie(self) -> bool:
183
+ """Return True if cookies should be quoted."""
184
+
185
+ @abstractmethod
186
+ def clear(self, predicate: Optional[ClearCookiePredicate] = None) -> None:
187
+ """Clear all cookies if no predicate is passed."""
188
+
189
+ @abstractmethod
190
+ def clear_domain(self, domain: str) -> None:
191
+ """Clear all cookies for domain and all subdomains."""
192
+
193
+ @abstractmethod
194
+ def update_cookies(self, cookies: LooseCookies, response_url: URL = URL()) -> None:
195
+ """Update cookies."""
196
+
197
+ def update_cookies_from_headers(
198
+ self, headers: Sequence[str], response_url: URL
199
+ ) -> None:
200
+ """Update cookies from raw Set-Cookie headers."""
201
+ if headers and (cookies_to_update := parse_set_cookie_headers(headers)):
202
+ self.update_cookies(cookies_to_update, response_url)
203
+
204
+ @abstractmethod
205
+ def filter_cookies(self, request_url: URL) -> "BaseCookie[str]":
206
+ """Return the jar's cookies filtered by their attributes."""
207
+
208
+
209
+ class AbstractStreamWriter(ABC):
210
+ """Abstract stream writer."""
211
+
212
+ buffer_size: int = 0
213
+ output_size: int = 0
214
+ length: Optional[int] = 0
215
+
216
+ @abstractmethod
217
+ async def write(self, chunk: Union[bytes, bytearray, memoryview]) -> None:
218
+ """Write chunk into stream."""
219
+
220
+ @abstractmethod
221
+ async def write_eof(self, chunk: bytes = b"") -> None:
222
+ """Write last chunk."""
223
+
224
+ @abstractmethod
225
+ async def drain(self) -> None:
226
+ """Flush the write buffer."""
227
+
228
+ @abstractmethod
229
+ def enable_compression(
230
+ self, encoding: str = "deflate", strategy: Optional[int] = None
231
+ ) -> None:
232
+ """Enable HTTP body compression"""
233
+
234
+ @abstractmethod
235
+ def enable_chunking(self) -> None:
236
+ """Enable HTTP chunked mode"""
237
+
238
+ @abstractmethod
239
+ async def write_headers(
240
+ self, status_line: str, headers: "CIMultiDict[str]"
241
+ ) -> None:
242
+ """Write HTTP headers"""
243
+
244
+ def send_headers(self) -> None:
245
+ """Force sending buffered headers if not already sent.
246
+
247
+ Required only if write_headers() buffers headers instead of sending immediately.
248
+ For backwards compatibility, this method does nothing by default.
249
+ """
250
+
251
+
252
+ class AbstractAccessLogger(ABC):
253
+ """Abstract writer to access log."""
254
+
255
+ __slots__ = ("logger", "log_format")
256
+
257
+ def __init__(self, logger: logging.Logger, log_format: str) -> None:
258
+ self.logger = logger
259
+ self.log_format = log_format
260
+
261
+ @abstractmethod
262
+ def log(self, request: BaseRequest, response: StreamResponse, time: float) -> None:
263
+ """Emit log to logger."""
264
+
265
+ @property
266
+ def enabled(self) -> bool:
267
+ """Check if logger is enabled."""
268
+ return True
aiohttp/client_exceptions.py ADDED
@@ -0,0 +1,421 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """HTTP related errors."""
2
+
3
+ import asyncio
4
+ import warnings
5
+ from typing import TYPE_CHECKING, Optional, Tuple, Union
6
+
7
+ from multidict import MultiMapping
8
+
9
+ from .typedefs import StrOrURL
10
+
11
+ if TYPE_CHECKING:
12
+ import ssl
13
+
14
+ SSLContext = ssl.SSLContext
15
+ else:
16
+ try:
17
+ import ssl
18
+
19
+ SSLContext = ssl.SSLContext
20
+ except ImportError: # pragma: no cover
21
+ ssl = SSLContext = None # type: ignore[assignment]
22
+
23
+ if TYPE_CHECKING:
24
+ from .client_reqrep import ClientResponse, ConnectionKey, Fingerprint, RequestInfo
25
+ from .http_parser import RawResponseMessage
26
+ else:
27
+ RequestInfo = ClientResponse = ConnectionKey = RawResponseMessage = None
28
+
29
+ __all__ = (
30
+ "ClientError",
31
+ "ClientConnectionError",
32
+ "ClientConnectionResetError",
33
+ "ClientOSError",
34
+ "ClientConnectorError",
35
+ "ClientProxyConnectionError",
36
+ "ClientSSLError",
37
+ "ClientConnectorDNSError",
38
+ "ClientConnectorSSLError",
39
+ "ClientConnectorCertificateError",
40
+ "ConnectionTimeoutError",
41
+ "SocketTimeoutError",
42
+ "ServerConnectionError",
43
+ "ServerTimeoutError",
44
+ "ServerDisconnectedError",
45
+ "ServerFingerprintMismatch",
46
+ "ClientResponseError",
47
+ "ClientHttpProxyError",
48
+ "WSServerHandshakeError",
49
+ "ContentTypeError",
50
+ "ClientPayloadError",
51
+ "InvalidURL",
52
+ "InvalidUrlClientError",
53
+ "RedirectClientError",
54
+ "NonHttpUrlClientError",
55
+ "InvalidUrlRedirectClientError",
56
+ "NonHttpUrlRedirectClientError",
57
+ "WSMessageTypeError",
58
+ )
59
+
60
+
61
+ class ClientError(Exception):
62
+ """Base class for client connection errors."""
63
+
64
+
65
+ class ClientResponseError(ClientError):
66
+ """Base class for exceptions that occur after getting a response.
67
+
68
+ request_info: An instance of RequestInfo.
69
+ history: A sequence of responses, if redirects occurred.
70
+ status: HTTP status code.
71
+ message: Error message.
72
+ headers: Response headers.
73
+ """
74
+
75
+ def __init__(
76
+ self,
77
+ request_info: RequestInfo,
78
+ history: Tuple[ClientResponse, ...],
79
+ *,
80
+ code: Optional[int] = None,
81
+ status: Optional[int] = None,
82
+ message: str = "",
83
+ headers: Optional[MultiMapping[str]] = None,
84
+ ) -> None:
85
+ self.request_info = request_info
86
+ if code is not None:
87
+ if status is not None:
88
+ raise ValueError(
89
+ "Both code and status arguments are provided; "
90
+ "code is deprecated, use status instead"
91
+ )
92
+ warnings.warn(
93
+ "code argument is deprecated, use status instead",
94
+ DeprecationWarning,
95
+ stacklevel=2,
96
+ )
97
+ if status is not None:
98
+ self.status = status
99
+ elif code is not None:
100
+ self.status = code
101
+ else:
102
+ self.status = 0
103
+ self.message = message
104
+ self.headers = headers
105
+ self.history = history
106
+ self.args = (request_info, history)
107
+
108
+ def __str__(self) -> str:
109
+ return "{}, message={!r}, url={!r}".format(
110
+ self.status,
111
+ self.message,
112
+ str(self.request_info.real_url),
113
+ )
114
+
115
+ def __repr__(self) -> str:
116
+ args = f"{self.request_info!r}, {self.history!r}"
117
+ if self.status != 0:
118
+ args += f", status={self.status!r}"
119
+ if self.message != "":
120
+ args += f", message={self.message!r}"
121
+ if self.headers is not None:
122
+ args += f", headers={self.headers!r}"
123
+ return f"{type(self).__name__}({args})"
124
+
125
+ @property
126
+ def code(self) -> int:
127
+ warnings.warn(
128
+ "code property is deprecated, use status instead",
129
+ DeprecationWarning,
130
+ stacklevel=2,
131
+ )
132
+ return self.status
133
+
134
+ @code.setter
135
+ def code(self, value: int) -> None:
136
+ warnings.warn(
137
+ "code property is deprecated, use status instead",
138
+ DeprecationWarning,
139
+ stacklevel=2,
140
+ )
141
+ self.status = value
142
+
143
+
144
+ class ContentTypeError(ClientResponseError):
145
+ """ContentType found is not valid."""
146
+
147
+
148
+ class WSServerHandshakeError(ClientResponseError):
149
+ """websocket server handshake error."""
150
+
151
+
152
+ class ClientHttpProxyError(ClientResponseError):
153
+ """HTTP proxy error.
154
+
155
+ Raised in :class:`aiohttp.connector.TCPConnector` if
156
+ proxy responds with status other than ``200 OK``
157
+ on ``CONNECT`` request.
158
+ """
159
+
160
+
161
+ class TooManyRedirects(ClientResponseError):
162
+ """Client was redirected too many times."""
163
+
164
+
165
+ class ClientConnectionError(ClientError):
166
+ """Base class for client socket errors."""
167
+
168
+
169
+ class ClientConnectionResetError(ClientConnectionError, ConnectionResetError):
170
+ """ConnectionResetError"""
171
+
172
+
173
+ class ClientOSError(ClientConnectionError, OSError):
174
+ """OSError error."""
175
+
176
+
177
+ class ClientConnectorError(ClientOSError):
178
+ """Client connector error.
179
+
180
+ Raised in :class:`aiohttp.connector.TCPConnector` if
181
+ a connection can not be established.
182
+ """
183
+
184
+ def __init__(self, connection_key: ConnectionKey, os_error: OSError) -> None:
185
+ self._conn_key = connection_key
186
+ self._os_error = os_error
187
+ super().__init__(os_error.errno, os_error.strerror)
188
+ self.args = (connection_key, os_error)
189
+
190
+ @property
191
+ def os_error(self) -> OSError:
192
+ return self._os_error
193
+
194
+ @property
195
+ def host(self) -> str:
196
+ return self._conn_key.host
197
+
198
+ @property
199
+ def port(self) -> Optional[int]:
200
+ return self._conn_key.port
201
+
202
+ @property
203
+ def ssl(self) -> Union[SSLContext, bool, "Fingerprint"]:
204
+ return self._conn_key.ssl
205
+
206
+ def __str__(self) -> str:
207
+ return "Cannot connect to host {0.host}:{0.port} ssl:{1} [{2}]".format(
208
+ self, "default" if self.ssl is True else self.ssl, self.strerror
209
+ )
210
+
211
+ # OSError.__reduce__ does too much black magick
212
+ __reduce__ = BaseException.__reduce__
213
+
214
+
215
+ class ClientConnectorDNSError(ClientConnectorError):
216
+ """DNS resolution failed during client connection.
217
+
218
+ Raised in :class:`aiohttp.connector.TCPConnector` if
219
+ DNS resolution fails.
220
+ """
221
+
222
+
223
+ class ClientProxyConnectionError(ClientConnectorError):
224
+ """Proxy connection error.
225
+
226
+ Raised in :class:`aiohttp.connector.TCPConnector` if
227
+ connection to proxy can not be established.
228
+ """
229
+
230
+
231
+ class UnixClientConnectorError(ClientConnectorError):
232
+ """Unix connector error.
233
+
234
+ Raised in :py:class:`aiohttp.connector.UnixConnector`
235
+ if connection to unix socket can not be established.
236
+ """
237
+
238
+ def __init__(
239
+ self, path: str, connection_key: ConnectionKey, os_error: OSError
240
+ ) -> None:
241
+ self._path = path
242
+ super().__init__(connection_key, os_error)
243
+
244
+ @property
245
+ def path(self) -> str:
246
+ return self._path
247
+
248
+ def __str__(self) -> str:
249
+ return "Cannot connect to unix socket {0.path} ssl:{1} [{2}]".format(
250
+ self, "default" if self.ssl is True else self.ssl, self.strerror
251
+ )
252
+
253
+
254
+ class ServerConnectionError(ClientConnectionError):
255
+ """Server connection errors."""
256
+
257
+
258
+ class ServerDisconnectedError(ServerConnectionError):
259
+ """Server disconnected."""
260
+
261
+ def __init__(self, message: Union[RawResponseMessage, str, None] = None) -> None:
262
+ if message is None:
263
+ message = "Server disconnected"
264
+
265
+ self.args = (message,)
266
+ self.message = message
267
+
268
+
269
+ class ServerTimeoutError(ServerConnectionError, asyncio.TimeoutError):
270
+ """Server timeout error."""
271
+
272
+
273
+ class ConnectionTimeoutError(ServerTimeoutError):
274
+ """Connection timeout error."""
275
+
276
+
277
+ class SocketTimeoutError(ServerTimeoutError):
278
+ """Socket timeout error."""
279
+
280
+
281
+ class ServerFingerprintMismatch(ServerConnectionError):
282
+ """SSL certificate does not match expected fingerprint."""
283
+
284
+ def __init__(self, expected: bytes, got: bytes, host: str, port: int) -> None:
285
+ self.expected = expected
286
+ self.got = got
287
+ self.host = host
288
+ self.port = port
289
+ self.args = (expected, got, host, port)
290
+
291
+ def __repr__(self) -> str:
292
+ return "<{} expected={!r} got={!r} host={!r} port={!r}>".format(
293
+ self.__class__.__name__, self.expected, self.got, self.host, self.port
294
+ )
295
+
296
+
297
+ class ClientPayloadError(ClientError):
298
+ """Response payload error."""
299
+
300
+
301
+ class InvalidURL(ClientError, ValueError):
302
+ """Invalid URL.
303
+
304
+ URL used for fetching is malformed, e.g. it doesn't contains host
305
+ part.
306
+ """
307
+
308
+ # Derive from ValueError for backward compatibility
309
+
310
+ def __init__(self, url: StrOrURL, description: Union[str, None] = None) -> None:
311
+ # The type of url is not yarl.URL because the exception can be raised
312
+ # on URL(url) call
313
+ self._url = url
314
+ self._description = description
315
+
316
+ if description:
317
+ super().__init__(url, description)
318
+ else:
319
+ super().__init__(url)
320
+
321
+ @property
322
+ def url(self) -> StrOrURL:
323
+ return self._url
324
+
325
+ @property
326
+ def description(self) -> "str | None":
327
+ return self._description
328
+
329
+ def __repr__(self) -> str:
330
+ return f"<{self.__class__.__name__} {self}>"
331
+
332
+ def __str__(self) -> str:
333
+ if self._description:
334
+ return f"{self._url} - {self._description}"
335
+ return str(self._url)
336
+
337
+
338
+ class InvalidUrlClientError(InvalidURL):
339
+ """Invalid URL client error."""
340
+
341
+
342
+ class RedirectClientError(ClientError):
343
+ """Client redirect error."""
344
+
345
+
346
+ class NonHttpUrlClientError(ClientError):
347
+ """Non http URL client error."""
348
+
349
+
350
+ class InvalidUrlRedirectClientError(InvalidUrlClientError, RedirectClientError):
351
+ """Invalid URL redirect client error."""
352
+
353
+
354
+ class NonHttpUrlRedirectClientError(NonHttpUrlClientError, RedirectClientError):
355
+ """Non http URL redirect client error."""
356
+
357
+
358
+ class ClientSSLError(ClientConnectorError):
359
+ """Base error for ssl.*Errors."""
360
+
361
+
362
+ if ssl is not None:
363
+ cert_errors = (ssl.CertificateError,)
364
+ cert_errors_bases = (
365
+ ClientSSLError,
366
+ ssl.CertificateError,
367
+ )
368
+
369
+ ssl_errors = (ssl.SSLError,)
370
+ ssl_error_bases = (ClientSSLError, ssl.SSLError)
371
+ else: # pragma: no cover
372
+ cert_errors = tuple()
373
+ cert_errors_bases = (
374
+ ClientSSLError,
375
+ ValueError,
376
+ )
377
+
378
+ ssl_errors = tuple()
379
+ ssl_error_bases = (ClientSSLError,)
380
+
381
+
382
+ class ClientConnectorSSLError(*ssl_error_bases): # type: ignore[misc]
383
+ """Response ssl error."""
384
+
385
+
386
+ class ClientConnectorCertificateError(*cert_errors_bases): # type: ignore[misc]
387
+ """Response certificate error."""
388
+
389
+ def __init__(
390
+ self, connection_key: ConnectionKey, certificate_error: Exception
391
+ ) -> None:
392
+ self._conn_key = connection_key
393
+ self._certificate_error = certificate_error
394
+ self.args = (connection_key, certificate_error)
395
+
396
+ @property
397
+ def certificate_error(self) -> Exception:
398
+ return self._certificate_error
399
+
400
+ @property
401
+ def host(self) -> str:
402
+ return self._conn_key.host
403
+
404
+ @property
405
+ def port(self) -> Optional[int]:
406
+ return self._conn_key.port
407
+
408
+ @property
409
+ def ssl(self) -> bool:
410
+ return self._conn_key.is_ssl
411
+
412
+ def __str__(self) -> str:
413
+ return (
414
+ "Cannot connect to host {0.host}:{0.port} ssl:{0.ssl} "
415
+ "[{0.certificate_error.__class__.__name__}: "
416
+ "{0.certificate_error.args}]".format(self)
417
+ )
418
+
419
+
420
+ class WSMessageTypeError(TypeError):
421
+ """WebSocket message type is not valid."""
aiohttp/client_middleware_digest_auth.py ADDED
@@ -0,0 +1,480 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Digest authentication middleware for aiohttp client.
3
+
4
+ This middleware implements HTTP Digest Authentication according to RFC 7616,
5
+ providing a more secure alternative to Basic Authentication. It supports all
6
+ standard hash algorithms including MD5, SHA, SHA-256, SHA-512 and their session
7
+ variants, as well as both 'auth' and 'auth-int' quality of protection (qop) options.
8
+ """
9
+
10
+ import hashlib
11
+ import os
12
+ import re
13
+ import sys
14
+ import time
15
+ from typing import (
16
+ Callable,
17
+ Dict,
18
+ Final,
19
+ FrozenSet,
20
+ List,
21
+ Literal,
22
+ Tuple,
23
+ TypedDict,
24
+ Union,
25
+ )
26
+
27
+ from yarl import URL
28
+
29
+ from . import hdrs
30
+ from .client_exceptions import ClientError
31
+ from .client_middlewares import ClientHandlerType
32
+ from .client_reqrep import ClientRequest, ClientResponse
33
+ from .payload import Payload
34
+
35
+
36
+ class DigestAuthChallenge(TypedDict, total=False):
37
+ realm: str
38
+ nonce: str
39
+ qop: str
40
+ algorithm: str
41
+ opaque: str
42
+ domain: str
43
+ stale: str
44
+
45
+
46
+ DigestFunctions: Dict[str, Callable[[bytes], "hashlib._Hash"]] = {
47
+ "MD5": hashlib.md5,
48
+ "MD5-SESS": hashlib.md5,
49
+ "SHA": hashlib.sha1,
50
+ "SHA-SESS": hashlib.sha1,
51
+ "SHA256": hashlib.sha256,
52
+ "SHA256-SESS": hashlib.sha256,
53
+ "SHA-256": hashlib.sha256,
54
+ "SHA-256-SESS": hashlib.sha256,
55
+ "SHA512": hashlib.sha512,
56
+ "SHA512-SESS": hashlib.sha512,
57
+ "SHA-512": hashlib.sha512,
58
+ "SHA-512-SESS": hashlib.sha512,
59
+ }
60
+
61
+
62
+ # Compile the regex pattern once at module level for performance
63
+ _HEADER_PAIRS_PATTERN = re.compile(
64
+ r'(?:^|\s|,\s*)(\w+)\s*=\s*(?:"((?:[^"\\]|\\.)*)"|([^\s,]+))'
65
+ if sys.version_info < (3, 11)
66
+ else r'(?:^|\s|,\s*)((?>\w+))\s*=\s*(?:"((?:[^"\\]|\\.)*)"|([^\s,]+))'
67
+ # +------------|--------|--|-|-|--|----|------|----|--||-----|-> Match valid start/sep
68
+ # +--------|--|-|-|--|----|------|----|--||-----|-> alphanumeric key (atomic
69
+ # | | | | | | | | || | group reduces backtracking)
70
+ # +--|-|-|--|----|------|----|--||-----|-> maybe whitespace
71
+ # | | | | | | | || |
72
+ # +-|-|--|----|------|----|--||-----|-> = (delimiter)
73
+ # +-|--|----|------|----|--||-----|-> maybe whitespace
74
+ # | | | | | || |
75
+ # +--|----|------|----|--||-----|-> group quoted or unquoted
76
+ # | | | | || |
77
+ # +----|------|----|--||-----|-> if quoted...
78
+ # +------|----|--||-----|-> anything but " or \
79
+ # +----|--||-----|-> escaped characters allowed
80
+ # +--||-----|-> or can be empty string
81
+ # || |
82
+ # +|-----|-> if unquoted...
83
+ # +-----|-> anything but , or <space>
84
+ # +-> at least one char req'd
85
+ )
86
+
87
+
88
+ # RFC 7616: Challenge parameters to extract
89
+ CHALLENGE_FIELDS: Final[
90
+ Tuple[
91
+ Literal["realm", "nonce", "qop", "algorithm", "opaque", "domain", "stale"], ...
92
+ ]
93
+ ] = (
94
+ "realm",
95
+ "nonce",
96
+ "qop",
97
+ "algorithm",
98
+ "opaque",
99
+ "domain",
100
+ "stale",
101
+ )
102
+
103
+ # Supported digest authentication algorithms
104
+ # Use a tuple of sorted keys for predictable documentation and error messages
105
+ SUPPORTED_ALGORITHMS: Final[Tuple[str, ...]] = tuple(sorted(DigestFunctions.keys()))
106
+
107
+ # RFC 7616: Fields that require quoting in the Digest auth header
108
+ # These fields must be enclosed in double quotes in the Authorization header.
109
+ # Algorithm, qop, and nc are never quoted per RFC specifications.
110
+ # This frozen set is used by the template-based header construction to
111
+ # automatically determine which fields need quotes.
112
+ QUOTED_AUTH_FIELDS: Final[FrozenSet[str]] = frozenset(
113
+ {"username", "realm", "nonce", "uri", "response", "opaque", "cnonce"}
114
+ )
115
+
116
+
117
+ def escape_quotes(value: str) -> str:
118
+ """Escape double quotes for HTTP header values."""
119
+ return value.replace('"', '\\"')
120
+
121
+
122
+ def unescape_quotes(value: str) -> str:
123
+ """Unescape double quotes in HTTP header values."""
124
+ return value.replace('\\"', '"')
125
+
126
+
127
+ def parse_header_pairs(header: str) -> Dict[str, str]:
128
+ """
129
+ Parse key-value pairs from WWW-Authenticate or similar HTTP headers.
130
+
131
+ This function handles the complex format of WWW-Authenticate header values,
132
+ supporting both quoted and unquoted values, proper handling of commas in
133
+ quoted values, and whitespace variations per RFC 7616.
134
+
135
+ Examples of supported formats:
136
+ - key1="value1", key2=value2
137
+ - key1 = "value1" , key2="value, with, commas"
138
+ - key1=value1,key2="value2"
139
+ - realm="example.com", nonce="12345", qop="auth"
140
+
141
+ Args:
142
+ header: The header value string to parse
143
+
144
+ Returns:
145
+ Dictionary mapping parameter names to their values
146
+ """
147
+ return {
148
+ stripped_key: unescape_quotes(quoted_val) if quoted_val else unquoted_val
149
+ for key, quoted_val, unquoted_val in _HEADER_PAIRS_PATTERN.findall(header)
150
+ if (stripped_key := key.strip())
151
+ }
152
+
153
+
154
+ class DigestAuthMiddleware:
155
+ """
156
+ HTTP digest authentication middleware for aiohttp client.
157
+
158
+ This middleware intercepts 401 Unauthorized responses containing a Digest
159
+ authentication challenge, calculates the appropriate digest credentials,
160
+ and automatically retries the request with the proper Authorization header.
161
+
162
+ Features:
163
+ - Handles all aspects of Digest authentication handshake automatically
164
+ - Supports all standard hash algorithms:
165
+ - MD5, MD5-SESS
166
+ - SHA, SHA-SESS
167
+ - SHA256, SHA256-SESS, SHA-256, SHA-256-SESS
168
+ - SHA512, SHA512-SESS, SHA-512, SHA-512-SESS
169
+ - Supports 'auth' and 'auth-int' quality of protection modes
170
+ - Properly handles quoted strings and parameter parsing
171
+ - Includes replay attack protection with client nonce count tracking
172
+ - Supports preemptive authentication per RFC 7616 Section 3.6
173
+
174
+ Standards compliance:
175
+ - RFC 7616: HTTP Digest Access Authentication (primary reference)
176
+ - RFC 2617: HTTP Authentication (deprecated by RFC 7616)
177
+ - RFC 1945: Section 11.1 (username restrictions)
178
+
179
+ Implementation notes:
180
+ The core digest calculation is inspired by the implementation in
181
+ https://github.com/requests/requests/blob/v2.18.4/requests/auth.py
182
+ with added support for modern digest auth features and error handling.
183
+ """
184
+
185
+ def __init__(
186
+ self,
187
+ login: str,
188
+ password: str,
189
+ preemptive: bool = True,
190
+ ) -> None:
191
+ if login is None:
192
+ raise ValueError("None is not allowed as login value")
193
+
194
+ if password is None:
195
+ raise ValueError("None is not allowed as password value")
196
+
197
+ if ":" in login:
198
+ raise ValueError('A ":" is not allowed in username (RFC 1945#section-11.1)')
199
+
200
+ self._login_str: Final[str] = login
201
+ self._login_bytes: Final[bytes] = login.encode("utf-8")
202
+ self._password_bytes: Final[bytes] = password.encode("utf-8")
203
+
204
+ self._last_nonce_bytes = b""
205
+ self._nonce_count = 0
206
+ self._challenge: DigestAuthChallenge = {}
207
+ self._preemptive: bool = preemptive
208
+ # Set of URLs defining the protection space
209
+ self._protection_space: List[str] = []
210
+
211
+ async def _encode(
212
+ self, method: str, url: URL, body: Union[Payload, Literal[b""]]
213
+ ) -> str:
214
+ """
215
+ Build digest authorization header for the current challenge.
216
+
217
+ Args:
218
+ method: The HTTP method (GET, POST, etc.)
219
+ url: The request URL
220
+ body: The request body (used for qop=auth-int)
221
+
222
+ Returns:
223
+ A fully formatted Digest authorization header string
224
+
225
+ Raises:
226
+ ClientError: If the challenge is missing required parameters or
227
+ contains unsupported values
228
+
229
+ """
230
+ challenge = self._challenge
231
+ if "realm" not in challenge:
232
+ raise ClientError(
233
+ "Malformed Digest auth challenge: Missing 'realm' parameter"
234
+ )
235
+
236
+ if "nonce" not in challenge:
237
+ raise ClientError(
238
+ "Malformed Digest auth challenge: Missing 'nonce' parameter"
239
+ )
240
+
241
+ # Empty realm values are allowed per RFC 7616 (SHOULD, not MUST, contain host name)
242
+ realm = challenge["realm"]
243
+ nonce = challenge["nonce"]
244
+
245
+ # Empty nonce values are not allowed as they are security-critical for replay protection
246
+ if not nonce:
247
+ raise ClientError(
248
+ "Security issue: Digest auth challenge contains empty 'nonce' value"
249
+ )
250
+
251
+ qop_raw = challenge.get("qop", "")
252
+ # Preserve original algorithm case for response while using uppercase for processing
253
+ algorithm_original = challenge.get("algorithm", "MD5")
254
+ algorithm = algorithm_original.upper()
255
+ opaque = challenge.get("opaque", "")
256
+
257
+ # Convert string values to bytes once
258
+ nonce_bytes = nonce.encode("utf-8")
259
+ realm_bytes = realm.encode("utf-8")
260
+ path = URL(url).path_qs
261
+
262
+ # Process QoP
263
+ qop = ""
264
+ qop_bytes = b""
265
+ if qop_raw:
266
+ valid_qops = {"auth", "auth-int"}.intersection(
267
+ {q.strip() for q in qop_raw.split(",") if q.strip()}
268
+ )
269
+ if not valid_qops:
270
+ raise ClientError(
271
+ f"Digest auth error: Unsupported Quality of Protection (qop) value(s): {qop_raw}"
272
+ )
273
+
274
+ qop = "auth-int" if "auth-int" in valid_qops else "auth"
275
+ qop_bytes = qop.encode("utf-8")
276
+
277
+ if algorithm not in DigestFunctions:
278
+ raise ClientError(
279
+ f"Digest auth error: Unsupported hash algorithm: {algorithm}. "
280
+ f"Supported algorithms: {', '.join(SUPPORTED_ALGORITHMS)}"
281
+ )
282
+ hash_fn: Final = DigestFunctions[algorithm]
283
+
284
+ def H(x: bytes) -> bytes:
285
+ """RFC 7616 Section 3: Hash function H(data) = hex(hash(data))."""
286
+ return hash_fn(x).hexdigest().encode()
287
+
288
+ def KD(s: bytes, d: bytes) -> bytes:
289
+ """RFC 7616 Section 3: KD(secret, data) = H(concat(secret, ":", data))."""
290
+ return H(b":".join((s, d)))
291
+
292
+ # Calculate A1 and A2
293
+ A1 = b":".join((self._login_bytes, realm_bytes, self._password_bytes))
294
+ A2 = f"{method.upper()}:{path}".encode()
295
+ if qop == "auth-int":
296
+ if isinstance(body, Payload): # will always be empty bytes unless Payload
297
+ entity_bytes = await body.as_bytes() # Get bytes from Payload
298
+ else:
299
+ entity_bytes = body
300
+ entity_hash = H(entity_bytes)
301
+ A2 = b":".join((A2, entity_hash))
302
+
303
+ HA1 = H(A1)
304
+ HA2 = H(A2)
305
+
306
+ # Nonce count handling
307
+ if nonce_bytes == self._last_nonce_bytes:
308
+ self._nonce_count += 1
309
+ else:
310
+ self._nonce_count = 1
311
+
312
+ self._last_nonce_bytes = nonce_bytes
313
+ ncvalue = f"{self._nonce_count:08x}"
314
+ ncvalue_bytes = ncvalue.encode("utf-8")
315
+
316
+ # Generate client nonce
317
+ cnonce = hashlib.sha1(
318
+ b"".join(
319
+ [
320
+ str(self._nonce_count).encode("utf-8"),
321
+ nonce_bytes,
322
+ time.ctime().encode("utf-8"),
323
+ os.urandom(8),
324
+ ]
325
+ )
326
+ ).hexdigest()[:16]
327
+ cnonce_bytes = cnonce.encode("utf-8")
328
+
329
+ # Special handling for session-based algorithms
330
+ if algorithm.upper().endswith("-SESS"):
331
+ HA1 = H(b":".join((HA1, nonce_bytes, cnonce_bytes)))
332
+
333
+ # Calculate the response digest
334
+ if qop:
335
+ noncebit = b":".join(
336
+ (nonce_bytes, ncvalue_bytes, cnonce_bytes, qop_bytes, HA2)
337
+ )
338
+ response_digest = KD(HA1, noncebit)
339
+ else:
340
+ response_digest = KD(HA1, b":".join((nonce_bytes, HA2)))
341
+
342
+ # Define a dict mapping of header fields to their values
343
+ # Group fields into always-present, optional, and qop-dependent
344
+ header_fields = {
345
+ # Always present fields
346
+ "username": escape_quotes(self._login_str),
347
+ "realm": escape_quotes(realm),
348
+ "nonce": escape_quotes(nonce),
349
+ "uri": path,
350
+ "response": response_digest.decode(),
351
+ "algorithm": algorithm_original,
352
+ }
353
+
354
+ # Optional fields
355
+ if opaque:
356
+ header_fields["opaque"] = escape_quotes(opaque)
357
+
358
+ # QoP-dependent fields
359
+ if qop:
360
+ header_fields["qop"] = qop
361
+ header_fields["nc"] = ncvalue
362
+ header_fields["cnonce"] = cnonce
363
+
364
+ # Build header using templates for each field type
365
+ pairs: List[str] = []
366
+ for field, value in header_fields.items():
367
+ if field in QUOTED_AUTH_FIELDS:
368
+ pairs.append(f'{field}="{value}"')
369
+ else:
370
+ pairs.append(f"{field}={value}")
371
+
372
+ return f"Digest {', '.join(pairs)}"
373
+
374
+ def _in_protection_space(self, url: URL) -> bool:
375
+ """
376
+ Check if the given URL is within the current protection space.
377
+
378
+ According to RFC 7616, a URI is in the protection space if any URI
379
+ in the protection space is a prefix of it (after both have been made absolute).
380
+ """
381
+ request_str = str(url)
382
+ for space_str in self._protection_space:
383
+ # Check if request starts with space URL
384
+ if not request_str.startswith(space_str):
385
+ continue
386
+ # Exact match or space ends with / (proper directory prefix)
387
+ if len(request_str) == len(space_str) or space_str[-1] == "/":
388
+ return True
389
+ # Check next char is / to ensure proper path boundary
390
+ if request_str[len(space_str)] == "/":
391
+ return True
392
+ return False
393
+
394
+ def _authenticate(self, response: ClientResponse) -> bool:
395
+ """
396
+ Takes the given response and tries digest-auth, if needed.
397
+
398
+ Returns true if the original request must be resent.
399
+ """
400
+ if response.status != 401:
401
+ return False
402
+
403
+ auth_header = response.headers.get("www-authenticate", "")
404
+ if not auth_header:
405
+ return False # No authentication header present
406
+
407
+ method, sep, headers = auth_header.partition(" ")
408
+ if not sep:
409
+ # No space found in www-authenticate header
410
+ return False # Malformed auth header, missing scheme separator
411
+
412
+ if method.lower() != "digest":
413
+ # Not a digest auth challenge (could be Basic, Bearer, etc.)
414
+ return False
415
+
416
+ if not headers:
417
+ # We have a digest scheme but no parameters
418
+ return False # Malformed digest header, missing parameters
419
+
420
+ # We have a digest auth header with content
421
+ if not (header_pairs := parse_header_pairs(headers)):
422
+ # Failed to parse any key-value pairs
423
+ return False # Malformed digest header, no valid parameters
424
+
425
+ # Extract challenge parameters
426
+ self._challenge = {}
427
+ for field in CHALLENGE_FIELDS:
428
+ if value := header_pairs.get(field):
429
+ self._challenge[field] = value
430
+
431
+ # Update protection space based on domain parameter or default to origin
432
+ origin = response.url.origin()
433
+
434
+ if domain := self._challenge.get("domain"):
435
+ # Parse space-separated list of URIs
436
+ self._protection_space = []
437
+ for uri in domain.split():
438
+ # Remove quotes if present
439
+ uri = uri.strip('"')
440
+ if uri.startswith("/"):
441
+ # Path-absolute, relative to origin
442
+ self._protection_space.append(str(origin.join(URL(uri))))
443
+ else:
444
+ # Absolute URI
445
+ self._protection_space.append(str(URL(uri)))
446
+ else:
447
+ # No domain specified, protection space is entire origin
448
+ self._protection_space = [str(origin)]
449
+
450
+ # Return True only if we found at least one challenge parameter
451
+ return bool(self._challenge)
452
+
453
+ async def __call__(
454
+ self, request: ClientRequest, handler: ClientHandlerType
455
+ ) -> ClientResponse:
456
+ """Run the digest auth middleware."""
457
+ response = None
458
+ for retry_count in range(2):
459
+ # Apply authorization header if:
460
+ # 1. This is a retry after 401 (retry_count > 0), OR
461
+ # 2. Preemptive auth is enabled AND we have a challenge AND the URL is in protection space
462
+ if retry_count > 0 or (
463
+ self._preemptive
464
+ and self._challenge
465
+ and self._in_protection_space(request.url)
466
+ ):
467
+ request.headers[hdrs.AUTHORIZATION] = await self._encode(
468
+ request.method, request.url, request.body
469
+ )
470
+
471
+ # Send the request
472
+ response = await handler(request)
473
+
474
+ # Check if we need to authenticate
475
+ if not self._authenticate(response):
476
+ break
477
+
478
+ # At this point, response is guaranteed to be defined
479
+ assert response is not None
480
+ return response
aiohttp/client_reqrep.py ADDED
@@ -0,0 +1,1536 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import codecs
3
+ import contextlib
4
+ import functools
5
+ import io
6
+ import re
7
+ import sys
8
+ import traceback
9
+ import warnings
10
+ from collections.abc import Mapping
11
+ from hashlib import md5, sha1, sha256
12
+ from http.cookies import Morsel, SimpleCookie
13
+ from types import MappingProxyType, TracebackType
14
+ from typing import (
15
+ TYPE_CHECKING,
16
+ Any,
17
+ Callable,
18
+ Dict,
19
+ Iterable,
20
+ List,
21
+ Literal,
22
+ NamedTuple,
23
+ Optional,
24
+ Tuple,
25
+ Type,
26
+ Union,
27
+ )
28
+
29
+ import attr
30
+ from multidict import CIMultiDict, CIMultiDictProxy, MultiDict, MultiDictProxy
31
+ from yarl import URL
32
+
33
+ from . import hdrs, helpers, http, multipart, payload
34
+ from ._cookie_helpers import (
35
+ parse_cookie_header,
36
+ parse_set_cookie_headers,
37
+ preserve_morsel_with_coded_value,
38
+ )
39
+ from .abc import AbstractStreamWriter
40
+ from .client_exceptions import (
41
+ ClientConnectionError,
42
+ ClientOSError,
43
+ ClientResponseError,
44
+ ContentTypeError,
45
+ InvalidURL,
46
+ ServerFingerprintMismatch,
47
+ )
48
+ from .compression_utils import HAS_BROTLI, HAS_ZSTD
49
+ from .formdata import FormData
50
+ from .helpers import (
51
+ _SENTINEL,
52
+ BaseTimerContext,
53
+ BasicAuth,
54
+ HeadersMixin,
55
+ TimerNoop,
56
+ noop,
57
+ reify,
58
+ sentinel,
59
+ set_exception,
60
+ set_result,
61
+ )
62
+ from .http import (
63
+ SERVER_SOFTWARE,
64
+ HttpVersion,
65
+ HttpVersion10,
66
+ HttpVersion11,
67
+ StreamWriter,
68
+ )
69
+ from .streams import StreamReader
70
+ from .typedefs import (
71
+ DEFAULT_JSON_DECODER,
72
+ JSONDecoder,
73
+ LooseCookies,
74
+ LooseHeaders,
75
+ Query,
76
+ RawHeaders,
77
+ )
78
+
79
+ if TYPE_CHECKING:
80
+ import ssl
81
+ from ssl import SSLContext
82
+ else:
83
+ try:
84
+ import ssl
85
+ from ssl import SSLContext
86
+ except ImportError: # pragma: no cover
87
+ ssl = None # type: ignore[assignment]
88
+ SSLContext = object # type: ignore[misc,assignment]
89
+
90
+
91
+ __all__ = ("ClientRequest", "ClientResponse", "RequestInfo", "Fingerprint")
92
+
93
+
94
+ if TYPE_CHECKING:
95
+ from .client import ClientSession
96
+ from .connector import Connection
97
+ from .tracing import Trace
98
+
99
+
100
+ _CONNECTION_CLOSED_EXCEPTION = ClientConnectionError("Connection closed")
101
+ _CONTAINS_CONTROL_CHAR_RE = re.compile(r"[^-!#$%&'*+.^_`|~0-9a-zA-Z]")
102
+ json_re = re.compile(r"^application/(?:[\w.+-]+?\+)?json")
103
+
104
+
105
+ def _gen_default_accept_encoding() -> str:
106
+ encodings = [
107
+ "gzip",
108
+ "deflate",
109
+ ]
110
+ if HAS_BROTLI:
111
+ encodings.append("br")
112
+ if HAS_ZSTD:
113
+ encodings.append("zstd")
114
+ return ", ".join(encodings)
115
+
116
+
117
+ @attr.s(auto_attribs=True, frozen=True, slots=True)
118
+ class ContentDisposition:
119
+ type: Optional[str]
120
+ parameters: "MappingProxyType[str, str]"
121
+ filename: Optional[str]
122
+
123
+
124
+ class _RequestInfo(NamedTuple):
125
+ url: URL
126
+ method: str
127
+ headers: "CIMultiDictProxy[str]"
128
+ real_url: URL
129
+
130
+
131
+ class RequestInfo(_RequestInfo):
132
+
133
+ def __new__(
134
+ cls,
135
+ url: URL,
136
+ method: str,
137
+ headers: "CIMultiDictProxy[str]",
138
+ real_url: Union[URL, _SENTINEL] = sentinel,
139
+ ) -> "RequestInfo":
140
+ """Create a new RequestInfo instance.
141
+
142
+ For backwards compatibility, the real_url parameter is optional.
143
+ """
144
+ return tuple.__new__(
145
+ cls, (url, method, headers, url if real_url is sentinel else real_url)
146
+ )
147
+
148
+
149
+ class Fingerprint:
150
+ HASHFUNC_BY_DIGESTLEN = {
151
+ 16: md5,
152
+ 20: sha1,
153
+ 32: sha256,
154
+ }
155
+
156
+ def __init__(self, fingerprint: bytes) -> None:
157
+ digestlen = len(fingerprint)
158
+ hashfunc = self.HASHFUNC_BY_DIGESTLEN.get(digestlen)
159
+ if not hashfunc:
160
+ raise ValueError("fingerprint has invalid length")
161
+ elif hashfunc is md5 or hashfunc is sha1:
162
+ raise ValueError("md5 and sha1 are insecure and not supported. Use sha256.")
163
+ self._hashfunc = hashfunc
164
+ self._fingerprint = fingerprint
165
+
166
+ @property
167
+ def fingerprint(self) -> bytes:
168
+ return self._fingerprint
169
+
170
+ def check(self, transport: asyncio.Transport) -> None:
171
+ if not transport.get_extra_info("sslcontext"):
172
+ return
173
+ sslobj = transport.get_extra_info("ssl_object")
174
+ cert = sslobj.getpeercert(binary_form=True)
175
+ got = self._hashfunc(cert).digest()
176
+ if got != self._fingerprint:
177
+ host, port, *_ = transport.get_extra_info("peername")
178
+ raise ServerFingerprintMismatch(self._fingerprint, got, host, port)
179
+
180
+
181
+ if ssl is not None:
182
+ SSL_ALLOWED_TYPES = (ssl.SSLContext, bool, Fingerprint, type(None))
183
+ else: # pragma: no cover
184
+ SSL_ALLOWED_TYPES = (bool, type(None))
185
+
186
+
187
+ def _merge_ssl_params(
188
+ ssl: Union["SSLContext", bool, Fingerprint],
189
+ verify_ssl: Optional[bool],
190
+ ssl_context: Optional["SSLContext"],
191
+ fingerprint: Optional[bytes],
192
+ ) -> Union["SSLContext", bool, Fingerprint]:
193
+ if ssl is None:
194
+ ssl = True # Double check for backwards compatibility
195
+ if verify_ssl is not None and not verify_ssl:
196
+ warnings.warn(
197
+ "verify_ssl is deprecated, use ssl=False instead",
198
+ DeprecationWarning,
199
+ stacklevel=3,
200
+ )
201
+ if ssl is not True:
202
+ raise ValueError(
203
+ "verify_ssl, ssl_context, fingerprint and ssl "
204
+ "parameters are mutually exclusive"
205
+ )
206
+ else:
207
+ ssl = False
208
+ if ssl_context is not None:
209
+ warnings.warn(
210
+ "ssl_context is deprecated, use ssl=context instead",
211
+ DeprecationWarning,
212
+ stacklevel=3,
213
+ )
214
+ if ssl is not True:
215
+ raise ValueError(
216
+ "verify_ssl, ssl_context, fingerprint and ssl "
217
+ "parameters are mutually exclusive"
218
+ )
219
+ else:
220
+ ssl = ssl_context
221
+ if fingerprint is not None:
222
+ warnings.warn(
223
+ "fingerprint is deprecated, use ssl=Fingerprint(fingerprint) instead",
224
+ DeprecationWarning,
225
+ stacklevel=3,
226
+ )
227
+ if ssl is not True:
228
+ raise ValueError(
229
+ "verify_ssl, ssl_context, fingerprint and ssl "
230
+ "parameters are mutually exclusive"
231
+ )
232
+ else:
233
+ ssl = Fingerprint(fingerprint)
234
+ if not isinstance(ssl, SSL_ALLOWED_TYPES):
235
+ raise TypeError(
236
+ "ssl should be SSLContext, bool, Fingerprint or None, "
237
+ "got {!r} instead.".format(ssl)
238
+ )
239
+ return ssl
240
+
241
+
242
+ _SSL_SCHEMES = frozenset(("https", "wss"))
243
+
244
+
245
+ # ConnectionKey is a NamedTuple because it is used as a key in a dict
246
+ # and a set in the connector. Since a NamedTuple is a tuple it uses
247
+ # the fast native tuple __hash__ and __eq__ implementation in CPython.
248
+ class ConnectionKey(NamedTuple):
249
+ # the key should contain an information about used proxy / TLS
250
+ # to prevent reusing wrong connections from a pool
251
+ host: str
252
+ port: Optional[int]
253
+ is_ssl: bool
254
+ ssl: Union[SSLContext, bool, Fingerprint]
255
+ proxy: Optional[URL]
256
+ proxy_auth: Optional[BasicAuth]
257
+ proxy_headers_hash: Optional[int] # hash(CIMultiDict)
258
+
259
+
260
+ def _is_expected_content_type(
261
+ response_content_type: str, expected_content_type: str
262
+ ) -> bool:
263
+ if expected_content_type == "application/json":
264
+ return json_re.match(response_content_type) is not None
265
+ return expected_content_type in response_content_type
266
+
267
+
268
+ def _warn_if_unclosed_payload(payload: payload.Payload, stacklevel: int = 2) -> None:
269
+ """Warn if the payload is not closed.
270
+
271
+ Callers must check that the body is a Payload before calling this method.
272
+
273
+ Args:
274
+ payload: The payload to check
275
+ stacklevel: Stack level for the warning (default 2 for direct callers)
276
+ """
277
+ if not payload.autoclose and not payload.consumed:
278
+ warnings.warn(
279
+ "The previous request body contains unclosed resources. "
280
+ "Use await request.update_body() instead of setting request.body "
281
+ "directly to properly close resources and avoid leaks.",
282
+ ResourceWarning,
283
+ stacklevel=stacklevel,
284
+ )
285
+
286
+
287
+ class ClientResponse(HeadersMixin):
288
+
289
+ # Some of these attributes are None when created,
290
+ # but will be set by the start() method.
291
+ # As the end user will likely never see the None values, we cheat the types below.
292
+ # from the Status-Line of the response
293
+ version: Optional[HttpVersion] = None # HTTP-Version
294
+ status: int = None # type: ignore[assignment] # Status-Code
295
+ reason: Optional[str] = None # Reason-Phrase
296
+
297
+ content: StreamReader = None # type: ignore[assignment] # Payload stream
298
+ _body: Optional[bytes] = None
299
+ _headers: CIMultiDictProxy[str] = None # type: ignore[assignment]
300
+ _history: Tuple["ClientResponse", ...] = ()
301
+ _raw_headers: RawHeaders = None # type: ignore[assignment]
302
+
303
+ _connection: Optional["Connection"] = None # current connection
304
+ _cookies: Optional[SimpleCookie] = None
305
+ _raw_cookie_headers: Optional[Tuple[str, ...]] = None
306
+ _continue: Optional["asyncio.Future[bool]"] = None
307
+ _source_traceback: Optional[traceback.StackSummary] = None
308
+ _session: Optional["ClientSession"] = None
309
+ # set up by ClientRequest after ClientResponse object creation
310
+ # post-init stage allows to not change ctor signature
311
+ _closed = True # to allow __del__ for non-initialized properly response
312
+ _released = False
313
+ _in_context = False
314
+
315
+ _resolve_charset: Callable[["ClientResponse", bytes], str] = lambda *_: "utf-8"
316
+
317
+ __writer: Optional["asyncio.Task[None]"] = None
318
+
319
+ def __init__(
320
+ self,
321
+ method: str,
322
+ url: URL,
323
+ *,
324
+ writer: "Optional[asyncio.Task[None]]",
325
+ continue100: Optional["asyncio.Future[bool]"],
326
+ timer: BaseTimerContext,
327
+ request_info: RequestInfo,
328
+ traces: List["Trace"],
329
+ loop: asyncio.AbstractEventLoop,
330
+ session: "ClientSession",
331
+ ) -> None:
332
+ # URL forbids subclasses, so a simple type check is enough.
333
+ assert type(url) is URL
334
+
335
+ self.method = method
336
+
337
+ self._real_url = url
338
+ self._url = url.with_fragment(None) if url.raw_fragment else url
339
+ if writer is not None:
340
+ self._writer = writer
341
+ if continue100 is not None:
342
+ self._continue = continue100
343
+ self._request_info = request_info
344
+ self._timer = timer if timer is not None else TimerNoop()
345
+ self._cache: Dict[str, Any] = {}
346
+ self._traces = traces
347
+ self._loop = loop
348
+ # Save reference to _resolve_charset, so that get_encoding() will still
349
+ # work after the response has finished reading the body.
350
+ # TODO: Fix session=None in tests (see ClientRequest.__init__).
351
+ if session is not None:
352
+ # store a reference to session #1985
353
+ self._session = session
354
+ self._resolve_charset = session._resolve_charset
355
+ if loop.get_debug():
356
+ self._source_traceback = traceback.extract_stack(sys._getframe(1))
357
+
358
+ def __reset_writer(self, _: object = None) -> None:
359
+ self.__writer = None
360
+
361
+ @property
362
+ def _writer(self) -> Optional["asyncio.Task[None]"]:
363
+ """The writer task for streaming data.
364
+
365
+ _writer is only provided for backwards compatibility
366
+ for subclasses that may need to access it.
367
+ """
368
+ return self.__writer
369
+
370
+ @_writer.setter
371
+ def _writer(self, writer: Optional["asyncio.Task[None]"]) -> None:
372
+ """Set the writer task for streaming data."""
373
+ if self.__writer is not None:
374
+ self.__writer.remove_done_callback(self.__reset_writer)
375
+ self.__writer = writer
376
+ if writer is None:
377
+ return
378
+ if writer.done():
379
+ # The writer is already done, so we can clear it immediately.
380
+ self.__writer = None
381
+ else:
382
+ writer.add_done_callback(self.__reset_writer)
383
+
384
+ @property
385
+ def cookies(self) -> SimpleCookie:
386
+ if self._cookies is None:
387
+ if self._raw_cookie_headers is not None:
388
+ # Parse cookies for response.cookies (SimpleCookie for backward compatibility)
389
+ cookies = SimpleCookie()
390
+ # Use parse_set_cookie_headers for more lenient parsing that handles
391
+ # malformed cookies better than SimpleCookie.load
392
+ cookies.update(parse_set_cookie_headers(self._raw_cookie_headers))
393
+ self._cookies = cookies
394
+ else:
395
+ self._cookies = SimpleCookie()
396
+ return self._cookies
397
+
398
+ @cookies.setter
399
+ def cookies(self, cookies: SimpleCookie) -> None:
400
+ self._cookies = cookies
401
+ # Generate raw cookie headers from the SimpleCookie
402
+ if cookies:
403
+ self._raw_cookie_headers = tuple(
404
+ morsel.OutputString() for morsel in cookies.values()
405
+ )
406
+ else:
407
+ self._raw_cookie_headers = None
408
+
409
+ @reify
410
+ def url(self) -> URL:
411
+ return self._url
412
+
413
+ @reify
414
+ def url_obj(self) -> URL:
415
+ warnings.warn("Deprecated, use .url #1654", DeprecationWarning, stacklevel=2)
416
+ return self._url
417
+
418
+ @reify
419
+ def real_url(self) -> URL:
420
+ return self._real_url
421
+
422
+ @reify
423
+ def host(self) -> str:
424
+ assert self._url.host is not None
425
+ return self._url.host
426
+
427
+ @reify
428
+ def headers(self) -> "CIMultiDictProxy[str]":
429
+ return self._headers
430
+
431
+ @reify
432
+ def raw_headers(self) -> RawHeaders:
433
+ return self._raw_headers
434
+
435
+ @reify
436
+ def request_info(self) -> RequestInfo:
437
+ return self._request_info
438
+
439
+ @reify
440
+ def content_disposition(self) -> Optional[ContentDisposition]:
441
+ raw = self._headers.get(hdrs.CONTENT_DISPOSITION)
442
+ if raw is None:
443
+ return None
444
+ disposition_type, params_dct = multipart.parse_content_disposition(raw)
445
+ params = MappingProxyType(params_dct)
446
+ filename = multipart.content_disposition_filename(params)
447
+ return ContentDisposition(disposition_type, params, filename)
448
+
449
+ def __del__(self, _warnings: Any = warnings) -> None:
450
+ if self._closed:
451
+ return
452
+
453
+ if self._connection is not None:
454
+ self._connection.release()
455
+ self._cleanup_writer()
456
+
457
+ if self._loop.get_debug():
458
+ kwargs = {"source": self}
459
+ _warnings.warn(f"Unclosed response {self!r}", ResourceWarning, **kwargs)
460
+ context = {"client_response": self, "message": "Unclosed response"}
461
+ if self._source_traceback:
462
+ context["source_traceback"] = self._source_traceback
463
+ self._loop.call_exception_handler(context)
464
+
465
+ def __repr__(self) -> str:
466
+ out = io.StringIO()
467
+ ascii_encodable_url = str(self.url)
468
+ if self.reason:
469
+ ascii_encodable_reason = self.reason.encode(
470
+ "ascii", "backslashreplace"
471
+ ).decode("ascii")
472
+ else:
473
+ ascii_encodable_reason = "None"
474
+ print(
475
+ "<ClientResponse({}) [{} {}]>".format(
476
+ ascii_encodable_url, self.status, ascii_encodable_reason
477
+ ),
478
+ file=out,
479
+ )
480
+ print(self.headers, file=out)
481
+ return out.getvalue()
482
+
483
+ @property
484
+ def connection(self) -> Optional["Connection"]:
485
+ return self._connection
486
+
487
+ @reify
488
+ def history(self) -> Tuple["ClientResponse", ...]:
489
+ """A sequence of of responses, if redirects occurred."""
490
+ return self._history
491
+
492
+ @reify
493
+ def links(self) -> "MultiDictProxy[MultiDictProxy[Union[str, URL]]]":
494
+ links_str = ", ".join(self.headers.getall("link", []))
495
+
496
+ if not links_str:
497
+ return MultiDictProxy(MultiDict())
498
+
499
+ links: MultiDict[MultiDictProxy[Union[str, URL]]] = MultiDict()
500
+
501
+ for val in re.split(r",(?=\s*<)", links_str):
502
+ match = re.match(r"\s*<(.*)>(.*)", val)
503
+ if match is None: # pragma: no cover
504
+ # the check exists to suppress mypy error
505
+ continue
506
+ url, params_str = match.groups()
507
+ params = params_str.split(";")[1:]
508
+
509
+ link: MultiDict[Union[str, URL]] = MultiDict()
510
+
511
+ for param in params:
512
+ match = re.match(r"^\s*(\S*)\s*=\s*(['\"]?)(.*?)(\2)\s*$", param, re.M)
513
+ if match is None: # pragma: no cover
514
+ # the check exists to suppress mypy error
515
+ continue
516
+ key, _, value, _ = match.groups()
517
+
518
+ link.add(key, value)
519
+
520
+ key = link.get("rel", url)
521
+
522
+ link.add("url", self.url.join(URL(url)))
523
+
524
+ links.add(str(key), MultiDictProxy(link))
525
+
526
+ return MultiDictProxy(links)
527
+
528
+ async def start(self, connection: "Connection") -> "ClientResponse":
529
+ """Start response processing."""
530
+ self._closed = False
531
+ self._protocol = connection.protocol
532
+ self._connection = connection
533
+
534
+ with self._timer:
535
+ while True:
536
+ # read response
537
+ try:
538
+ protocol = self._protocol
539
+ message, payload = await protocol.read() # type: ignore[union-attr]
540
+ except http.HttpProcessingError as exc:
541
+ raise ClientResponseError(
542
+ self.request_info,
543
+ self.history,
544
+ status=exc.code,
545
+ message=exc.message,
546
+ headers=exc.headers,
547
+ ) from exc
548
+
549
+ if message.code < 100 or message.code > 199 or message.code == 101:
550
+ break
551
+
552
+ if self._continue is not None:
553
+ set_result(self._continue, True)
554
+ self._continue = None
555
+
556
+ # payload eof handler
557
+ payload.on_eof(self._response_eof)
558
+
559
+ # response status
560
+ self.version = message.version
561
+ self.status = message.code
562
+ self.reason = message.reason
563
+
564
+ # headers
565
+ self._headers = message.headers # type is CIMultiDictProxy
566
+ self._raw_headers = message.raw_headers # type is Tuple[bytes, bytes]
567
+
568
+ # payload
569
+ self.content = payload
570
+
571
+ # cookies
572
+ if cookie_hdrs := self.headers.getall(hdrs.SET_COOKIE, ()):
573
+ # Store raw cookie headers for CookieJar
574
+ self._raw_cookie_headers = tuple(cookie_hdrs)
575
+ return self
576
+
577
+ def _response_eof(self) -> None:
578
+ if self._closed:
579
+ return
580
+
581
+ # protocol could be None because connection could be detached
582
+ protocol = self._connection and self._connection.protocol
583
+ if protocol is not None and protocol.upgraded:
584
+ return
585
+
586
+ self._closed = True
587
+ self._cleanup_writer()
588
+ self._release_connection()
589
+
590
+ @property
591
+ def closed(self) -> bool:
592
+ return self._closed
593
+
594
+ def close(self) -> None:
595
+ if not self._released:
596
+ self._notify_content()
597
+
598
+ self._closed = True
599
+ if self._loop is None or self._loop.is_closed():
600
+ return
601
+
602
+ self._cleanup_writer()
603
+ if self._connection is not None:
604
+ self._connection.close()
605
+ self._connection = None
606
+
607
+ def release(self) -> Any:
608
+ if not self._released:
609
+ self._notify_content()
610
+
611
+ self._closed = True
612
+
613
+ self._cleanup_writer()
614
+ self._release_connection()
615
+ return noop()
616
+
617
+ @property
618
+ def ok(self) -> bool:
619
+ """Returns ``True`` if ``status`` is less than ``400``, ``False`` if not.
620
+
621
+ This is **not** a check for ``200 OK`` but a check that the response
622
+ status is under 400.
623
+ """
624
+ return 400 > self.status
625
+
626
+ def raise_for_status(self) -> None:
627
+ if not self.ok:
628
+ # reason should always be not None for a started response
629
+ assert self.reason is not None
630
+
631
+ # If we're in a context we can rely on __aexit__() to release as the
632
+ # exception propagates.
633
+ if not self._in_context:
634
+ self.release()
635
+
636
+ raise ClientResponseError(
637
+ self.request_info,
638
+ self.history,
639
+ status=self.status,
640
+ message=self.reason,
641
+ headers=self.headers,
642
+ )
643
+
644
+ def _release_connection(self) -> None:
645
+ if self._connection is not None:
646
+ if self.__writer is None:
647
+ self._connection.release()
648
+ self._connection = None
649
+ else:
650
+ self.__writer.add_done_callback(lambda f: self._release_connection())
651
+
652
+ async def _wait_released(self) -> None:
653
+ if self.__writer is not None:
654
+ try:
655
+ await self.__writer
656
+ except asyncio.CancelledError:
657
+ if (
658
+ sys.version_info >= (3, 11)
659
+ and (task := asyncio.current_task())
660
+ and task.cancelling()
661
+ ):
662
+ raise
663
+ self._release_connection()
664
+
665
+ def _cleanup_writer(self) -> None:
666
+ if self.__writer is not None:
667
+ self.__writer.cancel()
668
+ self._session = None
669
+
670
+ def _notify_content(self) -> None:
671
+ content = self.content
672
+ if content and content.exception() is None:
673
+ set_exception(content, _CONNECTION_CLOSED_EXCEPTION)
674
+ self._released = True
675
+
676
+ async def wait_for_close(self) -> None:
677
+ if self.__writer is not None:
678
+ try:
679
+ await self.__writer
680
+ except asyncio.CancelledError:
681
+ if (
682
+ sys.version_info >= (3, 11)
683
+ and (task := asyncio.current_task())
684
+ and task.cancelling()
685
+ ):
686
+ raise
687
+ self.release()
688
+
689
+ async def read(self) -> bytes:
690
+ """Read response payload."""
691
+ if self._body is None:
692
+ try:
693
+ self._body = await self.content.read()
694
+ for trace in self._traces:
695
+ await trace.send_response_chunk_received(
696
+ self.method, self.url, self._body
697
+ )
698
+ except BaseException:
699
+ self.close()
700
+ raise
701
+ elif self._released: # Response explicitly released
702
+ raise ClientConnectionError("Connection closed")
703
+
704
+ protocol = self._connection and self._connection.protocol
705
+ if protocol is None or not protocol.upgraded:
706
+ await self._wait_released() # Underlying connection released
707
+ return self._body
708
+
709
+ def get_encoding(self) -> str:
710
+ ctype = self.headers.get(hdrs.CONTENT_TYPE, "").lower()
711
+ mimetype = helpers.parse_mimetype(ctype)
712
+
713
+ encoding = mimetype.parameters.get("charset")
714
+ if encoding:
715
+ with contextlib.suppress(LookupError, ValueError):
716
+ return codecs.lookup(encoding).name
717
+
718
+ if mimetype.type == "application" and (
719
+ mimetype.subtype == "json" or mimetype.subtype == "rdap"
720
+ ):
721
+ # RFC 7159 states that the default encoding is UTF-8.
722
+ # RFC 7483 defines application/rdap+json
723
+ return "utf-8"
724
+
725
+ if self._body is None:
726
+ raise RuntimeError(
727
+ "Cannot compute fallback encoding of a not yet read body"
728
+ )
729
+
730
+ return self._resolve_charset(self, self._body)
731
+
732
+ async def text(self, encoding: Optional[str] = None, errors: str = "strict") -> str:
733
+ """Read response payload and decode."""
734
+ if self._body is None:
735
+ await self.read()
736
+
737
+ if encoding is None:
738
+ encoding = self.get_encoding()
739
+
740
+ return self._body.decode(encoding, errors=errors) # type: ignore[union-attr]
741
+
742
+ async def json(
743
+ self,
744
+ *,
745
+ encoding: Optional[str] = None,
746
+ loads: JSONDecoder = DEFAULT_JSON_DECODER,
747
+ content_type: Optional[str] = "application/json",
748
+ ) -> Any:
749
+ """Read and decodes JSON response."""
750
+ if self._body is None:
751
+ await self.read()
752
+
753
+ if content_type:
754
+ ctype = self.headers.get(hdrs.CONTENT_TYPE, "").lower()
755
+ if not _is_expected_content_type(ctype, content_type):
756
+ raise ContentTypeError(
757
+ self.request_info,
758
+ self.history,
759
+ status=self.status,
760
+ message=(
761
+ "Attempt to decode JSON with unexpected mimetype: %s" % ctype
762
+ ),
763
+ headers=self.headers,
764
+ )
765
+
766
+ stripped = self._body.strip() # type: ignore[union-attr]
767
+ if not stripped:
768
+ return None
769
+
770
+ if encoding is None:
771
+ encoding = self.get_encoding()
772
+
773
+ return loads(stripped.decode(encoding))
774
+
775
+ async def __aenter__(self) -> "ClientResponse":
776
+ self._in_context = True
777
+ return self
778
+
779
+ async def __aexit__(
780
+ self,
781
+ exc_type: Optional[Type[BaseException]],
782
+ exc_val: Optional[BaseException],
783
+ exc_tb: Optional[TracebackType],
784
+ ) -> None:
785
+ self._in_context = False
786
+ # similar to _RequestContextManager, we do not need to check
787
+ # for exceptions, response object can close connection
788
+ # if state is broken
789
+ self.release()
790
+ await self.wait_for_close()
791
+
792
+
793
+ class ClientRequest:
794
+ GET_METHODS = {
795
+ hdrs.METH_GET,
796
+ hdrs.METH_HEAD,
797
+ hdrs.METH_OPTIONS,
798
+ hdrs.METH_TRACE,
799
+ }
800
+ POST_METHODS = {hdrs.METH_PATCH, hdrs.METH_POST, hdrs.METH_PUT}
801
+ ALL_METHODS = GET_METHODS.union(POST_METHODS).union({hdrs.METH_DELETE})
802
+
803
+ DEFAULT_HEADERS = {
804
+ hdrs.ACCEPT: "*/*",
805
+ hdrs.ACCEPT_ENCODING: _gen_default_accept_encoding(),
806
+ }
807
+
808
+ # Type of body depends on PAYLOAD_REGISTRY, which is dynamic.
809
+ _body: Union[None, payload.Payload] = None
810
+ auth = None
811
+ response = None
812
+
813
+ __writer: Optional["asyncio.Task[None]"] = None # async task for streaming data
814
+
815
+ # These class defaults help create_autospec() work correctly.
816
+ # If autospec is improved in future, maybe these can be removed.
817
+ url = URL()
818
+ method = "GET"
819
+
820
+ _continue = None # waiter future for '100 Continue' response
821
+
822
+ _skip_auto_headers: Optional["CIMultiDict[None]"] = None
823
+
824
+ # N.B.
825
+ # Adding __del__ method with self._writer closing doesn't make sense
826
+ # because _writer is instance method, thus it keeps a reference to self.
827
+ # Until writer has finished finalizer will not be called.
828
+
829
+ def __init__(
830
+ self,
831
+ method: str,
832
+ url: URL,
833
+ *,
834
+ params: Query = None,
835
+ headers: Optional[LooseHeaders] = None,
836
+ skip_auto_headers: Optional[Iterable[str]] = None,
837
+ data: Any = None,
838
+ cookies: Optional[LooseCookies] = None,
839
+ auth: Optional[BasicAuth] = None,
840
+ version: http.HttpVersion = http.HttpVersion11,
841
+ compress: Union[str, bool, None] = None,
842
+ chunked: Optional[bool] = None,
843
+ expect100: bool = False,
844
+ loop: Optional[asyncio.AbstractEventLoop] = None,
845
+ response_class: Optional[Type["ClientResponse"]] = None,
846
+ proxy: Optional[URL] = None,
847
+ proxy_auth: Optional[BasicAuth] = None,
848
+ timer: Optional[BaseTimerContext] = None,
849
+ session: Optional["ClientSession"] = None,
850
+ ssl: Union[SSLContext, bool, Fingerprint] = True,
851
+ proxy_headers: Optional[LooseHeaders] = None,
852
+ traces: Optional[List["Trace"]] = None,
853
+ trust_env: bool = False,
854
+ server_hostname: Optional[str] = None,
855
+ ):
856
+ if loop is None:
857
+ loop = asyncio.get_event_loop()
858
+ if match := _CONTAINS_CONTROL_CHAR_RE.search(method):
859
+ raise ValueError(
860
+ f"Method cannot contain non-token characters {method!r} "
861
+ f"(found at least {match.group()!r})"
862
+ )
863
+ # URL forbids subclasses, so a simple type check is enough.
864
+ assert type(url) is URL, url
865
+ if proxy is not None:
866
+ assert type(proxy) is URL, proxy
867
+ # FIXME: session is None in tests only, need to fix tests
868
+ # assert session is not None
869
+ if TYPE_CHECKING:
870
+ assert session is not None
871
+ self._session = session
872
+ if params:
873
+ url = url.extend_query(params)
874
+ self.original_url = url
875
+ self.url = url.with_fragment(None) if url.raw_fragment else url
876
+ self.method = method.upper()
877
+ self.chunked = chunked
878
+ self.compress = compress
879
+ self.loop = loop
880
+ self.length = None
881
+ if response_class is None:
882
+ real_response_class = ClientResponse
883
+ else:
884
+ real_response_class = response_class
885
+ self.response_class: Type[ClientResponse] = real_response_class
886
+ self._timer = timer if timer is not None else TimerNoop()
887
+ self._ssl = ssl if ssl is not None else True
888
+ self.server_hostname = server_hostname
889
+
890
+ if loop.get_debug():
891
+ self._source_traceback = traceback.extract_stack(sys._getframe(1))
892
+
893
+ self.update_version(version)
894
+ self.update_host(url)
895
+ self.update_headers(headers)
896
+ self.update_auto_headers(skip_auto_headers)
897
+ self.update_cookies(cookies)
898
+ self.update_content_encoding(data)
899
+ self.update_auth(auth, trust_env)
900
+ self.update_proxy(proxy, proxy_auth, proxy_headers)
901
+
902
+ self.update_body_from_data(data)
903
+ if data is not None or self.method not in self.GET_METHODS:
904
+ self.update_transfer_encoding()
905
+ self.update_expect_continue(expect100)
906
+ self._traces = [] if traces is None else traces
907
+
908
+ def __reset_writer(self, _: object = None) -> None:
909
+ self.__writer = None
910
+
911
+ def _get_content_length(self) -> Optional[int]:
912
+ """Extract and validate Content-Length header value.
913
+
914
+ Returns parsed Content-Length value or None if not set.
915
+ Raises ValueError if header exists but cannot be parsed as an integer.
916
+ """
917
+ if hdrs.CONTENT_LENGTH not in self.headers:
918
+ return None
919
+
920
+ content_length_hdr = self.headers[hdrs.CONTENT_LENGTH]
921
+ try:
922
+ return int(content_length_hdr)
923
+ except ValueError:
924
+ raise ValueError(
925
+ f"Invalid Content-Length header: {content_length_hdr}"
926
+ ) from None
927
+
928
+ @property
929
+ def skip_auto_headers(self) -> CIMultiDict[None]:
930
+ return self._skip_auto_headers or CIMultiDict()
931
+
932
+ @property
933
+ def _writer(self) -> Optional["asyncio.Task[None]"]:
934
+ return self.__writer
935
+
936
+ @_writer.setter
937
+ def _writer(self, writer: "asyncio.Task[None]") -> None:
938
+ if self.__writer is not None:
939
+ self.__writer.remove_done_callback(self.__reset_writer)
940
+ self.__writer = writer
941
+ writer.add_done_callback(self.__reset_writer)
942
+
943
+ def is_ssl(self) -> bool:
944
+ return self.url.scheme in _SSL_SCHEMES
945
+
946
+ @property
947
+ def ssl(self) -> Union["SSLContext", bool, Fingerprint]:
948
+ return self._ssl
949
+
950
+ @property
951
+ def connection_key(self) -> ConnectionKey:
952
+ if proxy_headers := self.proxy_headers:
953
+ h: Optional[int] = hash(tuple(proxy_headers.items()))
954
+ else:
955
+ h = None
956
+ url = self.url
957
+ return tuple.__new__(
958
+ ConnectionKey,
959
+ (
960
+ url.raw_host or "",
961
+ url.port,
962
+ url.scheme in _SSL_SCHEMES,
963
+ self._ssl,
964
+ self.proxy,
965
+ self.proxy_auth,
966
+ h,
967
+ ),
968
+ )
969
+
970
+ @property
971
+ def host(self) -> str:
972
+ ret = self.url.raw_host
973
+ assert ret is not None
974
+ return ret
975
+
976
+ @property
977
+ def port(self) -> Optional[int]:
978
+ return self.url.port
979
+
980
+ @property
981
+ def body(self) -> Union[payload.Payload, Literal[b""]]:
982
+ """Request body."""
983
+ # empty body is represented as bytes for backwards compatibility
984
+ return self._body or b""
985
+
986
+ @body.setter
987
+ def body(self, value: Any) -> None:
988
+ """Set request body with warning for non-autoclose payloads.
989
+
990
+ WARNING: This setter must be called from within an event loop and is not
991
+ thread-safe. Setting body outside of an event loop may raise RuntimeError
992
+ when closing file-based payloads.
993
+
994
+ DEPRECATED: Direct assignment to body is deprecated and will be removed
995
+ in a future version. Use await update_body() instead for proper resource
996
+ management.
997
+ """
998
+ # Close existing payload if present
999
+ if self._body is not None:
1000
+ # Warn if the payload needs manual closing
1001
+ # stacklevel=3: user code -> body setter -> _warn_if_unclosed_payload
1002
+ _warn_if_unclosed_payload(self._body, stacklevel=3)
1003
+ # NOTE: In the future, when we remove sync close support,
1004
+ # this setter will need to be removed and only the async
1005
+ # update_body() method will be available. For now, we call
1006
+ # _close() for backwards compatibility.
1007
+ self._body._close()
1008
+ self._update_body(value)
1009
+
1010
+ @property
1011
+ def request_info(self) -> RequestInfo:
1012
+ headers: CIMultiDictProxy[str] = CIMultiDictProxy(self.headers)
1013
+ # These are created on every request, so we use a NamedTuple
1014
+ # for performance reasons. We don't use the RequestInfo.__new__
1015
+ # method because it has a different signature which is provided
1016
+ # for backwards compatibility only.
1017
+ return tuple.__new__(
1018
+ RequestInfo, (self.url, self.method, headers, self.original_url)
1019
+ )
1020
+
1021
+ @property
1022
+ def session(self) -> "ClientSession":
1023
+ """Return the ClientSession instance.
1024
+
1025
+ This property provides access to the ClientSession that initiated
1026
+ this request, allowing middleware to make additional requests
1027
+ using the same session.
1028
+ """
1029
+ return self._session
1030
+
1031
+ def update_host(self, url: URL) -> None:
1032
+ """Update destination host, port and connection type (ssl)."""
1033
+ # get host/port
1034
+ if not url.raw_host:
1035
+ raise InvalidURL(url)
1036
+
1037
+ # basic auth info
1038
+ if url.raw_user or url.raw_password:
1039
+ self.auth = helpers.BasicAuth(url.user or "", url.password or "")
1040
+
1041
+ def update_version(self, version: Union[http.HttpVersion, str]) -> None:
1042
+ """Convert request version to two elements tuple.
1043
+
1044
+ parser HTTP version '1.1' => (1, 1)
1045
+ """
1046
+ if isinstance(version, str):
1047
+ v = [part.strip() for part in version.split(".", 1)]
1048
+ try:
1049
+ version = http.HttpVersion(int(v[0]), int(v[1]))
1050
+ except ValueError:
1051
+ raise ValueError(
1052
+ f"Can not parse http version number: {version}"
1053
+ ) from None
1054
+ self.version = version
1055
+
1056
+ def update_headers(self, headers: Optional[LooseHeaders]) -> None:
1057
+ """Update request headers."""
1058
+ self.headers: CIMultiDict[str] = CIMultiDict()
1059
+
1060
+ # Build the host header
1061
+ host = self.url.host_port_subcomponent
1062
+
1063
+ # host_port_subcomponent is None when the URL is a relative URL.
1064
+ # but we know we do not have a relative URL here.
1065
+ assert host is not None
1066
+ self.headers[hdrs.HOST] = host
1067
+
1068
+ if not headers:
1069
+ return
1070
+
1071
+ if isinstance(headers, (dict, MultiDictProxy, MultiDict)):
1072
+ headers = headers.items()
1073
+
1074
+ for key, value in headers: # type: ignore[misc]
1075
+ # A special case for Host header
1076
+ if key in hdrs.HOST_ALL:
1077
+ self.headers[key] = value
1078
+ else:
1079
+ self.headers.add(key, value)
1080
+
1081
+ def update_auto_headers(self, skip_auto_headers: Optional[Iterable[str]]) -> None:
1082
+ if skip_auto_headers is not None:
1083
+ self._skip_auto_headers = CIMultiDict(
1084
+ (hdr, None) for hdr in sorted(skip_auto_headers)
1085
+ )
1086
+ used_headers = self.headers.copy()
1087
+ used_headers.extend(self._skip_auto_headers) # type: ignore[arg-type]
1088
+ else:
1089
+ # Fast path when there are no headers to skip
1090
+ # which is the most common case.
1091
+ used_headers = self.headers
1092
+
1093
+ for hdr, val in self.DEFAULT_HEADERS.items():
1094
+ if hdr not in used_headers:
1095
+ self.headers[hdr] = val
1096
+
1097
+ if hdrs.USER_AGENT not in used_headers:
1098
+ self.headers[hdrs.USER_AGENT] = SERVER_SOFTWARE
1099
+
1100
+ def update_cookies(self, cookies: Optional[LooseCookies]) -> None:
1101
+ """Update request cookies header."""
1102
+ if not cookies:
1103
+ return
1104
+
1105
+ c = SimpleCookie()
1106
+ if hdrs.COOKIE in self.headers:
1107
+ # parse_cookie_header for RFC 6265 compliant Cookie header parsing
1108
+ c.update(parse_cookie_header(self.headers.get(hdrs.COOKIE, "")))
1109
+ del self.headers[hdrs.COOKIE]
1110
+
1111
+ if isinstance(cookies, Mapping):
1112
+ iter_cookies = cookies.items()
1113
+ else:
1114
+ iter_cookies = cookies # type: ignore[assignment]
1115
+ for name, value in iter_cookies:
1116
+ if isinstance(value, Morsel):
1117
+ # Use helper to preserve coded_value exactly as sent by server
1118
+ c[name] = preserve_morsel_with_coded_value(value)
1119
+ else:
1120
+ c[name] = value # type: ignore[assignment]
1121
+
1122
+ self.headers[hdrs.COOKIE] = c.output(header="", sep=";").strip()
1123
+
1124
+ def update_content_encoding(self, data: Any) -> None:
1125
+ """Set request content encoding."""
1126
+ if not data:
1127
+ # Don't compress an empty body.
1128
+ self.compress = None
1129
+ return
1130
+
1131
+ if self.headers.get(hdrs.CONTENT_ENCODING):
1132
+ if self.compress:
1133
+ raise ValueError(
1134
+ "compress can not be set if Content-Encoding header is set"
1135
+ )
1136
+ elif self.compress:
1137
+ if not isinstance(self.compress, str):
1138
+ self.compress = "deflate"
1139
+ self.headers[hdrs.CONTENT_ENCODING] = self.compress
1140
+ self.chunked = True # enable chunked, no need to deal with length
1141
+
1142
+ def update_transfer_encoding(self) -> None:
1143
+ """Analyze transfer-encoding header."""
1144
+ te = self.headers.get(hdrs.TRANSFER_ENCODING, "").lower()
1145
+
1146
+ if "chunked" in te:
1147
+ if self.chunked:
1148
+ raise ValueError(
1149
+ "chunked can not be set "
1150
+ 'if "Transfer-Encoding: chunked" header is set'
1151
+ )
1152
+
1153
+ elif self.chunked:
1154
+ if hdrs.CONTENT_LENGTH in self.headers:
1155
+ raise ValueError(
1156
+ "chunked can not be set if Content-Length header is set"
1157
+ )
1158
+
1159
+ self.headers[hdrs.TRANSFER_ENCODING] = "chunked"
1160
+
1161
+ def update_auth(self, auth: Optional[BasicAuth], trust_env: bool = False) -> None:
1162
+ """Set basic auth."""
1163
+ if auth is None:
1164
+ auth = self.auth
1165
+ if auth is None:
1166
+ return
1167
+
1168
+ if not isinstance(auth, helpers.BasicAuth):
1169
+ raise TypeError("BasicAuth() tuple is required instead")
1170
+
1171
+ self.headers[hdrs.AUTHORIZATION] = auth.encode()
1172
+
1173
+ def update_body_from_data(self, body: Any, _stacklevel: int = 3) -> None:
1174
+ """Update request body from data."""
1175
+ if self._body is not None:
1176
+ _warn_if_unclosed_payload(self._body, stacklevel=_stacklevel)
1177
+
1178
+ if body is None:
1179
+ self._body = None
1180
+ # Set Content-Length to 0 when body is None for methods that expect a body
1181
+ if (
1182
+ self.method not in self.GET_METHODS
1183
+ and not self.chunked
1184
+ and hdrs.CONTENT_LENGTH not in self.headers
1185
+ ):
1186
+ self.headers[hdrs.CONTENT_LENGTH] = "0"
1187
+ return
1188
+
1189
+ # FormData
1190
+ maybe_payload = body() if isinstance(body, FormData) else body
1191
+
1192
+ try:
1193
+ body_payload = payload.PAYLOAD_REGISTRY.get(maybe_payload, disposition=None)
1194
+ except payload.LookupError:
1195
+ body_payload = FormData(maybe_payload)() # type: ignore[arg-type]
1196
+
1197
+ self._body = body_payload
1198
+ # enable chunked encoding if needed
1199
+ if not self.chunked and hdrs.CONTENT_LENGTH not in self.headers:
1200
+ if (size := body_payload.size) is not None:
1201
+ self.headers[hdrs.CONTENT_LENGTH] = str(size)
1202
+ else:
1203
+ self.chunked = True
1204
+
1205
+ # copy payload headers
1206
+ assert body_payload.headers
1207
+ headers = self.headers
1208
+ skip_headers = self._skip_auto_headers
1209
+ for key, value in body_payload.headers.items():
1210
+ if key in headers or (skip_headers is not None and key in skip_headers):
1211
+ continue
1212
+ headers[key] = value
1213
+
1214
+ def _update_body(self, body: Any) -> None:
1215
+ """Update request body after its already been set."""
1216
+ # Remove existing Content-Length header since body is changing
1217
+ if hdrs.CONTENT_LENGTH in self.headers:
1218
+ del self.headers[hdrs.CONTENT_LENGTH]
1219
+
1220
+ # Remove existing Transfer-Encoding header to avoid conflicts
1221
+ if self.chunked and hdrs.TRANSFER_ENCODING in self.headers:
1222
+ del self.headers[hdrs.TRANSFER_ENCODING]
1223
+
1224
+ # Now update the body using the existing method
1225
+ # Called from _update_body, add 1 to stacklevel from caller
1226
+ self.update_body_from_data(body, _stacklevel=4)
1227
+
1228
+ # Update transfer encoding headers if needed (same logic as __init__)
1229
+ if body is not None or self.method not in self.GET_METHODS:
1230
+ self.update_transfer_encoding()
1231
+
1232
+ async def update_body(self, body: Any) -> None:
1233
+ """
1234
+ Update request body and close previous payload if needed.
1235
+
1236
+ This method safely updates the request body by first closing any existing
1237
+ payload to prevent resource leaks, then setting the new body.
1238
+
1239
+ IMPORTANT: Always use this method instead of setting request.body directly.
1240
+ Direct assignment to request.body will leak resources if the previous body
1241
+ contains file handles, streams, or other resources that need cleanup.
1242
+
1243
+ Args:
1244
+ body: The new body content. Can be:
1245
+ - bytes/bytearray: Raw binary data
1246
+ - str: Text data (will be encoded using charset from Content-Type)
1247
+ - FormData: Form data that will be encoded as multipart/form-data
1248
+ - Payload: A pre-configured payload object
1249
+ - AsyncIterable: An async iterable of bytes chunks
1250
+ - File-like object: Will be read and sent as binary data
1251
+ - None: Clears the body
1252
+
1253
+ Usage:
1254
+ # CORRECT: Use update_body
1255
+ await request.update_body(b"new request data")
1256
+
1257
+ # WRONG: Don't set body directly
1258
+ # request.body = b"new request data" # This will leak resources!
1259
+
1260
+ # Update with form data
1261
+ form_data = FormData()
1262
+ form_data.add_field('field', 'value')
1263
+ await request.update_body(form_data)
1264
+
1265
+ # Clear body
1266
+ await request.update_body(None)
1267
+
1268
+ Note:
1269
+ This method is async because it may need to close file handles or
1270
+ other resources associated with the previous payload. Always await
1271
+ this method to ensure proper cleanup.
1272
+
1273
+ Warning:
1274
+ Setting request.body directly is highly discouraged and can lead to:
1275
+ - Resource leaks (unclosed file handles, streams)
1276
+ - Memory leaks (unreleased buffers)
1277
+ - Unexpected behavior with streaming payloads
1278
+
1279
+ It is not recommended to change the payload type in middleware. If the
1280
+ body was already set (e.g., as bytes), it's best to keep the same type
1281
+ rather than converting it (e.g., to str) as this may result in unexpected
1282
+ behavior.
1283
+
1284
+ See Also:
1285
+ - update_body_from_data: Synchronous body update without cleanup
1286
+ - body property: Direct body access (STRONGLY DISCOURAGED)
1287
+
1288
+ """
1289
+ # Close existing payload if it exists and needs closing
1290
+ if self._body is not None:
1291
+ await self._body.close()
1292
+ self._update_body(body)
1293
+
1294
+ def update_expect_continue(self, expect: bool = False) -> None:
1295
+ if expect:
1296
+ self.headers[hdrs.EXPECT] = "100-continue"
1297
+ elif (
1298
+ hdrs.EXPECT in self.headers
1299
+ and self.headers[hdrs.EXPECT].lower() == "100-continue"
1300
+ ):
1301
+ expect = True
1302
+
1303
+ if expect:
1304
+ self._continue = self.loop.create_future()
1305
+
1306
+ def update_proxy(
1307
+ self,
1308
+ proxy: Optional[URL],
1309
+ proxy_auth: Optional[BasicAuth],
1310
+ proxy_headers: Optional[LooseHeaders],
1311
+ ) -> None:
1312
+ self.proxy = proxy
1313
+ if proxy is None:
1314
+ self.proxy_auth = None
1315
+ self.proxy_headers = None
1316
+ return
1317
+
1318
+ if proxy_auth and not isinstance(proxy_auth, helpers.BasicAuth):
1319
+ raise ValueError("proxy_auth must be None or BasicAuth() tuple")
1320
+ self.proxy_auth = proxy_auth
1321
+
1322
+ if proxy_headers is not None and not isinstance(
1323
+ proxy_headers, (MultiDict, MultiDictProxy)
1324
+ ):
1325
+ proxy_headers = CIMultiDict(proxy_headers)
1326
+ self.proxy_headers = proxy_headers
1327
+
1328
+ async def write_bytes(
1329
+ self,
1330
+ writer: AbstractStreamWriter,
1331
+ conn: "Connection",
1332
+ content_length: Optional[int] = None,
1333
+ ) -> None:
1334
+ """
1335
+ Write the request body to the connection stream.
1336
+
1337
+ This method handles writing different types of request bodies:
1338
+ 1. Payload objects (using their specialized write_with_length method)
1339
+ 2. Bytes/bytearray objects
1340
+ 3. Iterable body content
1341
+
1342
+ Args:
1343
+ writer: The stream writer to write the body to
1344
+ conn: The connection being used for this request
1345
+ content_length: Optional maximum number of bytes to write from the body
1346
+ (None means write the entire body)
1347
+
1348
+ The method properly handles:
1349
+ - Waiting for 100-Continue responses if required
1350
+ - Content length constraints for chunked encoding
1351
+ - Error handling for network issues, cancellation, and other exceptions
1352
+ - Signaling EOF and timeout management
1353
+
1354
+ Raises:
1355
+ ClientOSError: When there's an OS-level error writing the body
1356
+ ClientConnectionError: When there's a general connection error
1357
+ asyncio.CancelledError: When the operation is cancelled
1358
+
1359
+ """
1360
+ # 100 response
1361
+ if self._continue is not None:
1362
+ # Force headers to be sent before waiting for 100-continue
1363
+ writer.send_headers()
1364
+ await writer.drain()
1365
+ await self._continue
1366
+
1367
+ protocol = conn.protocol
1368
+ assert protocol is not None
1369
+ try:
1370
+ # This should be a rare case but the
1371
+ # self._body can be set to None while
1372
+ # the task is being started or we wait above
1373
+ # for the 100-continue response.
1374
+ # The more likely case is we have an empty
1375
+ # payload, but 100-continue is still expected.
1376
+ if self._body is not None:
1377
+ await self._body.write_with_length(writer, content_length)
1378
+ except OSError as underlying_exc:
1379
+ reraised_exc = underlying_exc
1380
+
1381
+ # Distinguish between timeout and other OS errors for better error reporting
1382
+ exc_is_not_timeout = underlying_exc.errno is not None or not isinstance(
1383
+ underlying_exc, asyncio.TimeoutError
1384
+ )
1385
+ if exc_is_not_timeout:
1386
+ reraised_exc = ClientOSError(
1387
+ underlying_exc.errno,
1388
+ f"Can not write request body for {self.url !s}",
1389
+ )
1390
+
1391
+ set_exception(protocol, reraised_exc, underlying_exc)
1392
+ except asyncio.CancelledError:
1393
+ # Body hasn't been fully sent, so connection can't be reused
1394
+ conn.close()
1395
+ raise
1396
+ except Exception as underlying_exc:
1397
+ set_exception(
1398
+ protocol,
1399
+ ClientConnectionError(
1400
+ "Failed to send bytes into the underlying connection "
1401
+ f"{conn !s}: {underlying_exc!r}",
1402
+ ),
1403
+ underlying_exc,
1404
+ )
1405
+ else:
1406
+ # Successfully wrote the body, signal EOF and start response timeout
1407
+ await writer.write_eof()
1408
+ protocol.start_timeout()
1409
+
1410
+ async def send(self, conn: "Connection") -> "ClientResponse":
1411
+ # Specify request target:
1412
+ # - CONNECT request must send authority form URI
1413
+ # - not CONNECT proxy must send absolute form URI
1414
+ # - most common is origin form URI
1415
+ if self.method == hdrs.METH_CONNECT:
1416
+ connect_host = self.url.host_subcomponent
1417
+ assert connect_host is not None
1418
+ path = f"{connect_host}:{self.url.port}"
1419
+ elif self.proxy and not self.is_ssl():
1420
+ path = str(self.url)
1421
+ else:
1422
+ path = self.url.raw_path_qs
1423
+
1424
+ protocol = conn.protocol
1425
+ assert protocol is not None
1426
+ writer = StreamWriter(
1427
+ protocol,
1428
+ self.loop,
1429
+ on_chunk_sent=(
1430
+ functools.partial(self._on_chunk_request_sent, self.method, self.url)
1431
+ if self._traces
1432
+ else None
1433
+ ),
1434
+ on_headers_sent=(
1435
+ functools.partial(self._on_headers_request_sent, self.method, self.url)
1436
+ if self._traces
1437
+ else None
1438
+ ),
1439
+ )
1440
+
1441
+ if self.compress:
1442
+ writer.enable_compression(self.compress) # type: ignore[arg-type]
1443
+
1444
+ if self.chunked is not None:
1445
+ writer.enable_chunking()
1446
+
1447
+ # set default content-type
1448
+ if (
1449
+ self.method in self.POST_METHODS
1450
+ and (
1451
+ self._skip_auto_headers is None
1452
+ or hdrs.CONTENT_TYPE not in self._skip_auto_headers
1453
+ )
1454
+ and hdrs.CONTENT_TYPE not in self.headers
1455
+ ):
1456
+ self.headers[hdrs.CONTENT_TYPE] = "application/octet-stream"
1457
+
1458
+ v = self.version
1459
+ if hdrs.CONNECTION not in self.headers:
1460
+ if conn._connector.force_close:
1461
+ if v == HttpVersion11:
1462
+ self.headers[hdrs.CONNECTION] = "close"
1463
+ elif v == HttpVersion10:
1464
+ self.headers[hdrs.CONNECTION] = "keep-alive"
1465
+
1466
+ # status + headers
1467
+ status_line = f"{self.method} {path} HTTP/{v.major}.{v.minor}"
1468
+
1469
+ # Buffer headers for potential coalescing with body
1470
+ await writer.write_headers(status_line, self.headers)
1471
+
1472
+ task: Optional["asyncio.Task[None]"]
1473
+ if self._body or self._continue is not None or protocol.writing_paused:
1474
+ coro = self.write_bytes(writer, conn, self._get_content_length())
1475
+ if sys.version_info >= (3, 12):
1476
+ # Optimization for Python 3.12, try to write
1477
+ # bytes immediately to avoid having to schedule
1478
+ # the task on the event loop.
1479
+ task = asyncio.Task(coro, loop=self.loop, eager_start=True)
1480
+ else:
1481
+ task = self.loop.create_task(coro)
1482
+ if task.done():
1483
+ task = None
1484
+ else:
1485
+ self._writer = task
1486
+ else:
1487
+ # We have nothing to write because
1488
+ # - there is no body
1489
+ # - the protocol does not have writing paused
1490
+ # - we are not waiting for a 100-continue response
1491
+ protocol.start_timeout()
1492
+ writer.set_eof()
1493
+ task = None
1494
+ response_class = self.response_class
1495
+ assert response_class is not None
1496
+ self.response = response_class(
1497
+ self.method,
1498
+ self.original_url,
1499
+ writer=task,
1500
+ continue100=self._continue,
1501
+ timer=self._timer,
1502
+ request_info=self.request_info,
1503
+ traces=self._traces,
1504
+ loop=self.loop,
1505
+ session=self._session,
1506
+ )
1507
+ return self.response
1508
+
1509
+ async def close(self) -> None:
1510
+ if self.__writer is not None:
1511
+ try:
1512
+ await self.__writer
1513
+ except asyncio.CancelledError:
1514
+ if (
1515
+ sys.version_info >= (3, 11)
1516
+ and (task := asyncio.current_task())
1517
+ and task.cancelling()
1518
+ ):
1519
+ raise
1520
+
1521
+ def terminate(self) -> None:
1522
+ if self.__writer is not None:
1523
+ if not self.loop.is_closed():
1524
+ self.__writer.cancel()
1525
+ self.__writer.remove_done_callback(self.__reset_writer)
1526
+ self.__writer = None
1527
+
1528
+ async def _on_chunk_request_sent(self, method: str, url: URL, chunk: bytes) -> None:
1529
+ for trace in self._traces:
1530
+ await trace.send_request_chunk_sent(method, url, chunk)
1531
+
1532
+ async def _on_headers_request_sent(
1533
+ self, method: str, url: URL, headers: "CIMultiDict[str]"
1534
+ ) -> None:
1535
+ for trace in self._traces:
1536
+ await trace.send_request_headers(method, url, headers)
aiohttp/connector.py ADDED
@@ -0,0 +1,1842 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import functools
3
+ import random
4
+ import socket
5
+ import sys
6
+ import traceback
7
+ import warnings
8
+ from collections import OrderedDict, defaultdict, deque
9
+ from contextlib import suppress
10
+ from http import HTTPStatus
11
+ from itertools import chain, cycle, islice
12
+ from time import monotonic
13
+ from types import TracebackType
14
+ from typing import (
15
+ TYPE_CHECKING,
16
+ Any,
17
+ Awaitable,
18
+ Callable,
19
+ DefaultDict,
20
+ Deque,
21
+ Dict,
22
+ Iterator,
23
+ List,
24
+ Literal,
25
+ Optional,
26
+ Sequence,
27
+ Set,
28
+ Tuple,
29
+ Type,
30
+ Union,
31
+ cast,
32
+ )
33
+
34
+ import aiohappyeyeballs
35
+ from aiohappyeyeballs import AddrInfoType, SocketFactoryType
36
+
37
+ from . import hdrs, helpers
38
+ from .abc import AbstractResolver, ResolveResult
39
+ from .client_exceptions import (
40
+ ClientConnectionError,
41
+ ClientConnectorCertificateError,
42
+ ClientConnectorDNSError,
43
+ ClientConnectorError,
44
+ ClientConnectorSSLError,
45
+ ClientHttpProxyError,
46
+ ClientProxyConnectionError,
47
+ ServerFingerprintMismatch,
48
+ UnixClientConnectorError,
49
+ cert_errors,
50
+ ssl_errors,
51
+ )
52
+ from .client_proto import ResponseHandler
53
+ from .client_reqrep import ClientRequest, Fingerprint, _merge_ssl_params
54
+ from .helpers import (
55
+ _SENTINEL,
56
+ ceil_timeout,
57
+ is_ip_address,
58
+ noop,
59
+ sentinel,
60
+ set_exception,
61
+ set_result,
62
+ )
63
+ from .log import client_logger
64
+ from .resolver import DefaultResolver
65
+
66
+ if sys.version_info >= (3, 12):
67
+ from collections.abc import Buffer
68
+ else:
69
+ Buffer = Union[bytes, bytearray, "memoryview[int]", "memoryview[bytes]"]
70
+
71
+ if TYPE_CHECKING:
72
+ import ssl
73
+
74
+ SSLContext = ssl.SSLContext
75
+ else:
76
+ try:
77
+ import ssl
78
+
79
+ SSLContext = ssl.SSLContext
80
+ except ImportError: # pragma: no cover
81
+ ssl = None # type: ignore[assignment]
82
+ SSLContext = object # type: ignore[misc,assignment]
83
+
84
+ EMPTY_SCHEMA_SET = frozenset({""})
85
+ HTTP_SCHEMA_SET = frozenset({"http", "https"})
86
+ WS_SCHEMA_SET = frozenset({"ws", "wss"})
87
+
88
+ HTTP_AND_EMPTY_SCHEMA_SET = HTTP_SCHEMA_SET | EMPTY_SCHEMA_SET
89
+ HIGH_LEVEL_SCHEMA_SET = HTTP_AND_EMPTY_SCHEMA_SET | WS_SCHEMA_SET
90
+
91
+ NEEDS_CLEANUP_CLOSED = (3, 13, 0) <= sys.version_info < (
92
+ 3,
93
+ 13,
94
+ 1,
95
+ ) or sys.version_info < (3, 12, 7)
96
+ # Cleanup closed is no longer needed after https://github.com/python/cpython/pull/118960
97
+ # which first appeared in Python 3.12.7 and 3.13.1
98
+
99
+
100
+ __all__ = (
101
+ "BaseConnector",
102
+ "TCPConnector",
103
+ "UnixConnector",
104
+ "NamedPipeConnector",
105
+ "AddrInfoType",
106
+ "SocketFactoryType",
107
+ )
108
+
109
+
110
+ if TYPE_CHECKING:
111
+ from .client import ClientTimeout
112
+ from .client_reqrep import ConnectionKey
113
+ from .tracing import Trace
114
+
115
+
116
+ class _DeprecationWaiter:
117
+ __slots__ = ("_awaitable", "_awaited")
118
+
119
+ def __init__(self, awaitable: Awaitable[Any]) -> None:
120
+ self._awaitable = awaitable
121
+ self._awaited = False
122
+
123
+ def __await__(self) -> Any:
124
+ self._awaited = True
125
+ return self._awaitable.__await__()
126
+
127
+ def __del__(self) -> None:
128
+ if not self._awaited:
129
+ warnings.warn(
130
+ "Connector.close() is a coroutine, "
131
+ "please use await connector.close()",
132
+ DeprecationWarning,
133
+ )
134
+
135
+
136
+ async def _wait_for_close(waiters: List[Awaitable[object]]) -> None:
137
+ """Wait for all waiters to finish closing."""
138
+ results = await asyncio.gather(*waiters, return_exceptions=True)
139
+ for res in results:
140
+ if isinstance(res, Exception):
141
+ client_logger.debug("Error while closing connector: %r", res)
142
+
143
+
144
+ class Connection:
145
+
146
+ _source_traceback = None
147
+
148
+ def __init__(
149
+ self,
150
+ connector: "BaseConnector",
151
+ key: "ConnectionKey",
152
+ protocol: ResponseHandler,
153
+ loop: asyncio.AbstractEventLoop,
154
+ ) -> None:
155
+ self._key = key
156
+ self._connector = connector
157
+ self._loop = loop
158
+ self._protocol: Optional[ResponseHandler] = protocol
159
+ self._callbacks: List[Callable[[], None]] = []
160
+
161
+ if loop.get_debug():
162
+ self._source_traceback = traceback.extract_stack(sys._getframe(1))
163
+
164
+ def __repr__(self) -> str:
165
+ return f"Connection<{self._key}>"
166
+
167
+ def __del__(self, _warnings: Any = warnings) -> None:
168
+ if self._protocol is not None:
169
+ kwargs = {"source": self}
170
+ _warnings.warn(f"Unclosed connection {self!r}", ResourceWarning, **kwargs)
171
+ if self._loop.is_closed():
172
+ return
173
+
174
+ self._connector._release(self._key, self._protocol, should_close=True)
175
+
176
+ context = {"client_connection": self, "message": "Unclosed connection"}
177
+ if self._source_traceback is not None:
178
+ context["source_traceback"] = self._source_traceback
179
+ self._loop.call_exception_handler(context)
180
+
181
+ def __bool__(self) -> Literal[True]:
182
+ """Force subclasses to not be falsy, to make checks simpler."""
183
+ return True
184
+
185
+ @property
186
+ def loop(self) -> asyncio.AbstractEventLoop:
187
+ warnings.warn(
188
+ "connector.loop property is deprecated", DeprecationWarning, stacklevel=2
189
+ )
190
+ return self._loop
191
+
192
+ @property
193
+ def transport(self) -> Optional[asyncio.Transport]:
194
+ if self._protocol is None:
195
+ return None
196
+ return self._protocol.transport
197
+
198
+ @property
199
+ def protocol(self) -> Optional[ResponseHandler]:
200
+ return self._protocol
201
+
202
+ def add_callback(self, callback: Callable[[], None]) -> None:
203
+ if callback is not None:
204
+ self._callbacks.append(callback)
205
+
206
+ def _notify_release(self) -> None:
207
+ callbacks, self._callbacks = self._callbacks[:], []
208
+
209
+ for cb in callbacks:
210
+ with suppress(Exception):
211
+ cb()
212
+
213
+ def close(self) -> None:
214
+ self._notify_release()
215
+
216
+ if self._protocol is not None:
217
+ self._connector._release(self._key, self._protocol, should_close=True)
218
+ self._protocol = None
219
+
220
+ def release(self) -> None:
221
+ self._notify_release()
222
+
223
+ if self._protocol is not None:
224
+ self._connector._release(self._key, self._protocol)
225
+ self._protocol = None
226
+
227
+ @property
228
+ def closed(self) -> bool:
229
+ return self._protocol is None or not self._protocol.is_connected()
230
+
231
+
232
+ class _ConnectTunnelConnection(Connection):
233
+ """Special connection wrapper for CONNECT tunnels that must never be pooled.
234
+
235
+ This connection wraps the proxy connection that will be upgraded with TLS.
236
+ It must never be released to the pool because:
237
+ 1. Its 'closed' future will never complete, causing session.close() to hang
238
+ 2. It represents an intermediate state, not a reusable connection
239
+ 3. The real connection (with TLS) will be created separately
240
+ """
241
+
242
+ def release(self) -> None:
243
+ """Do nothing - don't pool or close the connection.
244
+
245
+ These connections are an intermediate state during the CONNECT tunnel
246
+ setup and will be cleaned up naturally after the TLS upgrade. If they
247
+ were to be pooled, they would never be properly closed, causing
248
+ session.close() to wait forever for their 'closed' future.
249
+ """
250
+
251
+
252
+ class _TransportPlaceholder:
253
+ """placeholder for BaseConnector.connect function"""
254
+
255
+ __slots__ = ("closed", "transport")
256
+
257
+ def __init__(self, closed_future: asyncio.Future[Optional[Exception]]) -> None:
258
+ """Initialize a placeholder for a transport."""
259
+ self.closed = closed_future
260
+ self.transport = None
261
+
262
+ def close(self) -> None:
263
+ """Close the placeholder."""
264
+
265
+ def abort(self) -> None:
266
+ """Abort the placeholder (does nothing)."""
267
+
268
+
269
+ class BaseConnector:
270
+ """Base connector class.
271
+
272
+ keepalive_timeout - (optional) Keep-alive timeout.
273
+ force_close - Set to True to force close and do reconnect
274
+ after each request (and between redirects).
275
+ limit - The total number of simultaneous connections.
276
+ limit_per_host - Number of simultaneous connections to one host.
277
+ enable_cleanup_closed - Enables clean-up closed ssl transports.
278
+ Disabled by default.
279
+ timeout_ceil_threshold - Trigger ceiling of timeout values when
280
+ it's above timeout_ceil_threshold.
281
+ loop - Optional event loop.
282
+ """
283
+
284
+ _closed = True # prevent AttributeError in __del__ if ctor was failed
285
+ _source_traceback = None
286
+
287
+ # abort transport after 2 seconds (cleanup broken connections)
288
+ _cleanup_closed_period = 2.0
289
+
290
+ allowed_protocol_schema_set = HIGH_LEVEL_SCHEMA_SET
291
+
292
+ def __init__(
293
+ self,
294
+ *,
295
+ keepalive_timeout: Union[object, None, float] = sentinel,
296
+ force_close: bool = False,
297
+ limit: int = 100,
298
+ limit_per_host: int = 0,
299
+ enable_cleanup_closed: bool = False,
300
+ loop: Optional[asyncio.AbstractEventLoop] = None,
301
+ timeout_ceil_threshold: float = 5,
302
+ ) -> None:
303
+
304
+ if force_close:
305
+ if keepalive_timeout is not None and keepalive_timeout is not sentinel:
306
+ raise ValueError(
307
+ "keepalive_timeout cannot be set if force_close is True"
308
+ )
309
+ else:
310
+ if keepalive_timeout is sentinel:
311
+ keepalive_timeout = 15.0
312
+
313
+ loop = loop or asyncio.get_running_loop()
314
+ self._timeout_ceil_threshold = timeout_ceil_threshold
315
+
316
+ self._closed = False
317
+ if loop.get_debug():
318
+ self._source_traceback = traceback.extract_stack(sys._getframe(1))
319
+
320
+ # Connection pool of reusable connections.
321
+ # We use a deque to store connections because it has O(1) popleft()
322
+ # and O(1) append() operations to implement a FIFO queue.
323
+ self._conns: DefaultDict[
324
+ ConnectionKey, Deque[Tuple[ResponseHandler, float]]
325
+ ] = defaultdict(deque)
326
+ self._limit = limit
327
+ self._limit_per_host = limit_per_host
328
+ self._acquired: Set[ResponseHandler] = set()
329
+ self._acquired_per_host: DefaultDict[ConnectionKey, Set[ResponseHandler]] = (
330
+ defaultdict(set)
331
+ )
332
+ self._keepalive_timeout = cast(float, keepalive_timeout)
333
+ self._force_close = force_close
334
+
335
+ # {host_key: FIFO list of waiters}
336
+ # The FIFO is implemented with an OrderedDict with None keys because
337
+ # python does not have an ordered set.
338
+ self._waiters: DefaultDict[
339
+ ConnectionKey, OrderedDict[asyncio.Future[None], None]
340
+ ] = defaultdict(OrderedDict)
341
+
342
+ self._loop = loop
343
+ self._factory = functools.partial(ResponseHandler, loop=loop)
344
+
345
+ # start keep-alive connection cleanup task
346
+ self._cleanup_handle: Optional[asyncio.TimerHandle] = None
347
+
348
+ # start cleanup closed transports task
349
+ self._cleanup_closed_handle: Optional[asyncio.TimerHandle] = None
350
+
351
+ if enable_cleanup_closed and not NEEDS_CLEANUP_CLOSED:
352
+ warnings.warn(
353
+ "enable_cleanup_closed ignored because "
354
+ "https://github.com/python/cpython/pull/118960 is fixed "
355
+ f"in Python version {sys.version_info}",
356
+ DeprecationWarning,
357
+ stacklevel=2,
358
+ )
359
+ enable_cleanup_closed = False
360
+
361
+ self._cleanup_closed_disabled = not enable_cleanup_closed
362
+ self._cleanup_closed_transports: List[Optional[asyncio.Transport]] = []
363
+ self._placeholder_future: asyncio.Future[Optional[Exception]] = (
364
+ loop.create_future()
365
+ )
366
+ self._placeholder_future.set_result(None)
367
+ self._cleanup_closed()
368
+
369
+ def __del__(self, _warnings: Any = warnings) -> None:
370
+ if self._closed:
371
+ return
372
+ if not self._conns:
373
+ return
374
+
375
+ conns = [repr(c) for c in self._conns.values()]
376
+
377
+ self._close()
378
+
379
+ kwargs = {"source": self}
380
+ _warnings.warn(f"Unclosed connector {self!r}", ResourceWarning, **kwargs)
381
+ context = {
382
+ "connector": self,
383
+ "connections": conns,
384
+ "message": "Unclosed connector",
385
+ }
386
+ if self._source_traceback is not None:
387
+ context["source_traceback"] = self._source_traceback
388
+ self._loop.call_exception_handler(context)
389
+
390
+ def __enter__(self) -> "BaseConnector":
391
+ warnings.warn(
392
+ '"with Connector():" is deprecated, '
393
+ 'use "async with Connector():" instead',
394
+ DeprecationWarning,
395
+ )
396
+ return self
397
+
398
+ def __exit__(self, *exc: Any) -> None:
399
+ self._close()
400
+
401
+ async def __aenter__(self) -> "BaseConnector":
402
+ return self
403
+
404
+ async def __aexit__(
405
+ self,
406
+ exc_type: Optional[Type[BaseException]] = None,
407
+ exc_value: Optional[BaseException] = None,
408
+ exc_traceback: Optional[TracebackType] = None,
409
+ ) -> None:
410
+ await self.close()
411
+
412
+ @property
413
+ def force_close(self) -> bool:
414
+ """Ultimately close connection on releasing if True."""
415
+ return self._force_close
416
+
417
+ @property
418
+ def limit(self) -> int:
419
+ """The total number for simultaneous connections.
420
+
421
+ If limit is 0 the connector has no limit.
422
+ The default limit size is 100.
423
+ """
424
+ return self._limit
425
+
426
+ @property
427
+ def limit_per_host(self) -> int:
428
+ """The limit for simultaneous connections to the same endpoint.
429
+
430
+ Endpoints are the same if they are have equal
431
+ (host, port, is_ssl) triple.
432
+ """
433
+ return self._limit_per_host
434
+
435
+ def _cleanup(self) -> None:
436
+ """Cleanup unused transports."""
437
+ if self._cleanup_handle:
438
+ self._cleanup_handle.cancel()
439
+ # _cleanup_handle should be unset, otherwise _release() will not
440
+ # recreate it ever!
441
+ self._cleanup_handle = None
442
+
443
+ now = monotonic()
444
+ timeout = self._keepalive_timeout
445
+
446
+ if self._conns:
447
+ connections = defaultdict(deque)
448
+ deadline = now - timeout
449
+ for key, conns in self._conns.items():
450
+ alive: Deque[Tuple[ResponseHandler, float]] = deque()
451
+ for proto, use_time in conns:
452
+ if proto.is_connected() and use_time - deadline >= 0:
453
+ alive.append((proto, use_time))
454
+ continue
455
+ transport = proto.transport
456
+ proto.close()
457
+ if not self._cleanup_closed_disabled and key.is_ssl:
458
+ self._cleanup_closed_transports.append(transport)
459
+
460
+ if alive:
461
+ connections[key] = alive
462
+
463
+ self._conns = connections
464
+
465
+ if self._conns:
466
+ self._cleanup_handle = helpers.weakref_handle(
467
+ self,
468
+ "_cleanup",
469
+ timeout,
470
+ self._loop,
471
+ timeout_ceil_threshold=self._timeout_ceil_threshold,
472
+ )
473
+
474
+ def _cleanup_closed(self) -> None:
475
+ """Double confirmation for transport close.
476
+
477
+ Some broken ssl servers may leave socket open without proper close.
478
+ """
479
+ if self._cleanup_closed_handle:
480
+ self._cleanup_closed_handle.cancel()
481
+
482
+ for transport in self._cleanup_closed_transports:
483
+ if transport is not None:
484
+ transport.abort()
485
+
486
+ self._cleanup_closed_transports = []
487
+
488
+ if not self._cleanup_closed_disabled:
489
+ self._cleanup_closed_handle = helpers.weakref_handle(
490
+ self,
491
+ "_cleanup_closed",
492
+ self._cleanup_closed_period,
493
+ self._loop,
494
+ timeout_ceil_threshold=self._timeout_ceil_threshold,
495
+ )
496
+
497
+ def close(self, *, abort_ssl: bool = False) -> Awaitable[None]:
498
+ """Close all opened transports.
499
+
500
+ :param abort_ssl: If True, SSL connections will be aborted immediately
501
+ without performing the shutdown handshake. This provides
502
+ faster cleanup at the cost of less graceful disconnection.
503
+ """
504
+ if not (waiters := self._close(abort_ssl=abort_ssl)):
505
+ # If there are no connections to close, we can return a noop
506
+ # awaitable to avoid scheduling a task on the event loop.
507
+ return _DeprecationWaiter(noop())
508
+ coro = _wait_for_close(waiters)
509
+ if sys.version_info >= (3, 12):
510
+ # Optimization for Python 3.12, try to close connections
511
+ # immediately to avoid having to schedule the task on the event loop.
512
+ task = asyncio.Task(coro, loop=self._loop, eager_start=True)
513
+ else:
514
+ task = self._loop.create_task(coro)
515
+ return _DeprecationWaiter(task)
516
+
517
+ def _close(self, *, abort_ssl: bool = False) -> List[Awaitable[object]]:
518
+ waiters: List[Awaitable[object]] = []
519
+
520
+ if self._closed:
521
+ return waiters
522
+
523
+ self._closed = True
524
+
525
+ try:
526
+ if self._loop.is_closed():
527
+ return waiters
528
+
529
+ # cancel cleanup task
530
+ if self._cleanup_handle:
531
+ self._cleanup_handle.cancel()
532
+
533
+ # cancel cleanup close task
534
+ if self._cleanup_closed_handle:
535
+ self._cleanup_closed_handle.cancel()
536
+
537
+ for data in self._conns.values():
538
+ for proto, _ in data:
539
+ if (
540
+ abort_ssl
541
+ and proto.transport
542
+ and proto.transport.get_extra_info("sslcontext") is not None
543
+ ):
544
+ proto.abort()
545
+ else:
546
+ proto.close()
547
+ if closed := proto.closed:
548
+ waiters.append(closed)
549
+
550
+ for proto in self._acquired:
551
+ if (
552
+ abort_ssl
553
+ and proto.transport
554
+ and proto.transport.get_extra_info("sslcontext") is not None
555
+ ):
556
+ proto.abort()
557
+ else:
558
+ proto.close()
559
+ if closed := proto.closed:
560
+ waiters.append(closed)
561
+
562
+ for transport in self._cleanup_closed_transports:
563
+ if transport is not None:
564
+ transport.abort()
565
+
566
+ return waiters
567
+
568
+ finally:
569
+ self._conns.clear()
570
+ self._acquired.clear()
571
+ for keyed_waiters in self._waiters.values():
572
+ for keyed_waiter in keyed_waiters:
573
+ keyed_waiter.cancel()
574
+ self._waiters.clear()
575
+ self._cleanup_handle = None
576
+ self._cleanup_closed_transports.clear()
577
+ self._cleanup_closed_handle = None
578
+
579
+ @property
580
+ def closed(self) -> bool:
581
+ """Is connector closed.
582
+
583
+ A readonly property.
584
+ """
585
+ return self._closed
586
+
587
+ def _available_connections(self, key: "ConnectionKey") -> int:
588
+ """
589
+ Return number of available connections.
590
+
591
+ The limit, limit_per_host and the connection key are taken into account.
592
+
593
+ If it returns less than 1 means that there are no connections
594
+ available.
595
+ """
596
+ # check total available connections
597
+ # If there are no limits, this will always return 1
598
+ total_remain = 1
599
+
600
+ if self._limit and (total_remain := self._limit - len(self._acquired)) <= 0:
601
+ return total_remain
602
+
603
+ # check limit per host
604
+ if host_remain := self._limit_per_host:
605
+ if acquired := self._acquired_per_host.get(key):
606
+ host_remain -= len(acquired)
607
+ if total_remain > host_remain:
608
+ return host_remain
609
+
610
+ return total_remain
611
+
612
+ def _update_proxy_auth_header_and_build_proxy_req(
613
+ self, req: ClientRequest
614
+ ) -> ClientRequest:
615
+ """Set Proxy-Authorization header for non-SSL proxy requests and builds the proxy request for SSL proxy requests."""
616
+ url = req.proxy
617
+ assert url is not None
618
+ headers: Dict[str, str] = {}
619
+ if req.proxy_headers is not None:
620
+ headers = req.proxy_headers # type: ignore[assignment]
621
+ headers[hdrs.HOST] = req.headers[hdrs.HOST]
622
+ proxy_req = ClientRequest(
623
+ hdrs.METH_GET,
624
+ url,
625
+ headers=headers,
626
+ auth=req.proxy_auth,
627
+ loop=self._loop,
628
+ ssl=req.ssl,
629
+ )
630
+ auth = proxy_req.headers.pop(hdrs.AUTHORIZATION, None)
631
+ if auth is not None:
632
+ if not req.is_ssl():
633
+ req.headers[hdrs.PROXY_AUTHORIZATION] = auth
634
+ else:
635
+ proxy_req.headers[hdrs.PROXY_AUTHORIZATION] = auth
636
+ return proxy_req
637
+
638
+ async def connect(
639
+ self, req: ClientRequest, traces: List["Trace"], timeout: "ClientTimeout"
640
+ ) -> Connection:
641
+ """Get from pool or create new connection."""
642
+ key = req.connection_key
643
+ if (conn := await self._get(key, traces)) is not None:
644
+ # If we do not have to wait and we can get a connection from the pool
645
+ # we can avoid the timeout ceil logic and directly return the connection
646
+ if req.proxy:
647
+ self._update_proxy_auth_header_and_build_proxy_req(req)
648
+ return conn
649
+
650
+ async with ceil_timeout(timeout.connect, timeout.ceil_threshold):
651
+ if self._available_connections(key) <= 0:
652
+ await self._wait_for_available_connection(key, traces)
653
+ if (conn := await self._get(key, traces)) is not None:
654
+ if req.proxy:
655
+ self._update_proxy_auth_header_and_build_proxy_req(req)
656
+ return conn
657
+
658
+ placeholder = cast(
659
+ ResponseHandler, _TransportPlaceholder(self._placeholder_future)
660
+ )
661
+ self._acquired.add(placeholder)
662
+ if self._limit_per_host:
663
+ self._acquired_per_host[key].add(placeholder)
664
+
665
+ try:
666
+ # Traces are done inside the try block to ensure that the
667
+ # that the placeholder is still cleaned up if an exception
668
+ # is raised.
669
+ if traces:
670
+ for trace in traces:
671
+ await trace.send_connection_create_start()
672
+ proto = await self._create_connection(req, traces, timeout)
673
+ if traces:
674
+ for trace in traces:
675
+ await trace.send_connection_create_end()
676
+ except BaseException:
677
+ self._release_acquired(key, placeholder)
678
+ raise
679
+ else:
680
+ if self._closed:
681
+ proto.close()
682
+ raise ClientConnectionError("Connector is closed.")
683
+
684
+ # The connection was successfully created, drop the placeholder
685
+ # and add the real connection to the acquired set. There should
686
+ # be no awaits after the proto is added to the acquired set
687
+ # to ensure that the connection is not left in the acquired set
688
+ # on cancellation.
689
+ self._acquired.remove(placeholder)
690
+ self._acquired.add(proto)
691
+ if self._limit_per_host:
692
+ acquired_per_host = self._acquired_per_host[key]
693
+ acquired_per_host.remove(placeholder)
694
+ acquired_per_host.add(proto)
695
+ return Connection(self, key, proto, self._loop)
696
+
697
+ async def _wait_for_available_connection(
698
+ self, key: "ConnectionKey", traces: List["Trace"]
699
+ ) -> None:
700
+ """Wait for an available connection slot."""
701
+ # We loop here because there is a race between
702
+ # the connection limit check and the connection
703
+ # being acquired. If the connection is acquired
704
+ # between the check and the await statement, we
705
+ # need to loop again to check if the connection
706
+ # slot is still available.
707
+ attempts = 0
708
+ while True:
709
+ fut: asyncio.Future[None] = self._loop.create_future()
710
+ keyed_waiters = self._waiters[key]
711
+ keyed_waiters[fut] = None
712
+ if attempts:
713
+ # If we have waited before, we need to move the waiter
714
+ # to the front of the queue as otherwise we might get
715
+ # starved and hit the timeout.
716
+ keyed_waiters.move_to_end(fut, last=False)
717
+
718
+ try:
719
+ # Traces happen in the try block to ensure that the
720
+ # the waiter is still cleaned up if an exception is raised.
721
+ if traces:
722
+ for trace in traces:
723
+ await trace.send_connection_queued_start()
724
+ await fut
725
+ if traces:
726
+ for trace in traces:
727
+ await trace.send_connection_queued_end()
728
+ finally:
729
+ # pop the waiter from the queue if its still
730
+ # there and not already removed by _release_waiter
731
+ keyed_waiters.pop(fut, None)
732
+ if not self._waiters.get(key, True):
733
+ del self._waiters[key]
734
+
735
+ if self._available_connections(key) > 0:
736
+ break
737
+ attempts += 1
738
+
739
+ async def _get(
740
+ self, key: "ConnectionKey", traces: List["Trace"]
741
+ ) -> Optional[Connection]:
742
+ """Get next reusable connection for the key or None.
743
+
744
+ The connection will be marked as acquired.
745
+ """
746
+ if (conns := self._conns.get(key)) is None:
747
+ return None
748
+
749
+ t1 = monotonic()
750
+ while conns:
751
+ proto, t0 = conns.popleft()
752
+ # We will we reuse the connection if its connected and
753
+ # the keepalive timeout has not been exceeded
754
+ if proto.is_connected() and t1 - t0 <= self._keepalive_timeout:
755
+ if not conns:
756
+ # The very last connection was reclaimed: drop the key
757
+ del self._conns[key]
758
+ self._acquired.add(proto)
759
+ if self._limit_per_host:
760
+ self._acquired_per_host[key].add(proto)
761
+ if traces:
762
+ for trace in traces:
763
+ try:
764
+ await trace.send_connection_reuseconn()
765
+ except BaseException:
766
+ self._release_acquired(key, proto)
767
+ raise
768
+ return Connection(self, key, proto, self._loop)
769
+
770
+ # Connection cannot be reused, close it
771
+ transport = proto.transport
772
+ proto.close()
773
+ # only for SSL transports
774
+ if not self._cleanup_closed_disabled and key.is_ssl:
775
+ self._cleanup_closed_transports.append(transport)
776
+
777
+ # No more connections: drop the key
778
+ del self._conns[key]
779
+ return None
780
+
781
+ def _release_waiter(self) -> None:
782
+ """
783
+ Iterates over all waiters until one to be released is found.
784
+
785
+ The one to be released is not finished and
786
+ belongs to a host that has available connections.
787
+ """
788
+ if not self._waiters:
789
+ return
790
+
791
+ # Having the dict keys ordered this avoids to iterate
792
+ # at the same order at each call.
793
+ queues = list(self._waiters)
794
+ random.shuffle(queues)
795
+
796
+ for key in queues:
797
+ if self._available_connections(key) < 1:
798
+ continue
799
+
800
+ waiters = self._waiters[key]
801
+ while waiters:
802
+ waiter, _ = waiters.popitem(last=False)
803
+ if not waiter.done():
804
+ waiter.set_result(None)
805
+ return
806
+
807
+ def _release_acquired(self, key: "ConnectionKey", proto: ResponseHandler) -> None:
808
+ """Release acquired connection."""
809
+ if self._closed:
810
+ # acquired connection is already released on connector closing
811
+ return
812
+
813
+ self._acquired.discard(proto)
814
+ if self._limit_per_host and (conns := self._acquired_per_host.get(key)):
815
+ conns.discard(proto)
816
+ if not conns:
817
+ del self._acquired_per_host[key]
818
+ self._release_waiter()
819
+
820
+ def _release(
821
+ self,
822
+ key: "ConnectionKey",
823
+ protocol: ResponseHandler,
824
+ *,
825
+ should_close: bool = False,
826
+ ) -> None:
827
+ if self._closed:
828
+ # acquired connection is already released on connector closing
829
+ return
830
+
831
+ self._release_acquired(key, protocol)
832
+
833
+ if self._force_close or should_close or protocol.should_close:
834
+ transport = protocol.transport
835
+ protocol.close()
836
+
837
+ if key.is_ssl and not self._cleanup_closed_disabled:
838
+ self._cleanup_closed_transports.append(transport)
839
+ return
840
+
841
+ self._conns[key].append((protocol, monotonic()))
842
+
843
+ if self._cleanup_handle is None:
844
+ self._cleanup_handle = helpers.weakref_handle(
845
+ self,
846
+ "_cleanup",
847
+ self._keepalive_timeout,
848
+ self._loop,
849
+ timeout_ceil_threshold=self._timeout_ceil_threshold,
850
+ )
851
+
852
+ async def _create_connection(
853
+ self, req: ClientRequest, traces: List["Trace"], timeout: "ClientTimeout"
854
+ ) -> ResponseHandler:
855
+ raise NotImplementedError()
856
+
857
+
858
+ class _DNSCacheTable:
859
+ def __init__(self, ttl: Optional[float] = None) -> None:
860
+ self._addrs_rr: Dict[Tuple[str, int], Tuple[Iterator[ResolveResult], int]] = {}
861
+ self._timestamps: Dict[Tuple[str, int], float] = {}
862
+ self._ttl = ttl
863
+
864
+ def __contains__(self, host: object) -> bool:
865
+ return host in self._addrs_rr
866
+
867
+ def add(self, key: Tuple[str, int], addrs: List[ResolveResult]) -> None:
868
+ self._addrs_rr[key] = (cycle(addrs), len(addrs))
869
+
870
+ if self._ttl is not None:
871
+ self._timestamps[key] = monotonic()
872
+
873
+ def remove(self, key: Tuple[str, int]) -> None:
874
+ self._addrs_rr.pop(key, None)
875
+
876
+ if self._ttl is not None:
877
+ self._timestamps.pop(key, None)
878
+
879
+ def clear(self) -> None:
880
+ self._addrs_rr.clear()
881
+ self._timestamps.clear()
882
+
883
+ def next_addrs(self, key: Tuple[str, int]) -> List[ResolveResult]:
884
+ loop, length = self._addrs_rr[key]
885
+ addrs = list(islice(loop, length))
886
+ # Consume one more element to shift internal state of `cycle`
887
+ next(loop)
888
+ return addrs
889
+
890
+ def expired(self, key: Tuple[str, int]) -> bool:
891
+ if self._ttl is None:
892
+ return False
893
+
894
+ return self._timestamps[key] + self._ttl < monotonic()
895
+
896
+
897
+ def _make_ssl_context(verified: bool) -> SSLContext:
898
+ """Create SSL context.
899
+
900
+ This method is not async-friendly and should be called from a thread
901
+ because it will load certificates from disk and do other blocking I/O.
902
+ """
903
+ if ssl is None:
904
+ # No ssl support
905
+ return None
906
+ if verified:
907
+ sslcontext = ssl.create_default_context()
908
+ else:
909
+ sslcontext = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
910
+ sslcontext.options |= ssl.OP_NO_SSLv2
911
+ sslcontext.options |= ssl.OP_NO_SSLv3
912
+ sslcontext.check_hostname = False
913
+ sslcontext.verify_mode = ssl.CERT_NONE
914
+ sslcontext.options |= ssl.OP_NO_COMPRESSION
915
+ sslcontext.set_default_verify_paths()
916
+ sslcontext.set_alpn_protocols(("http/1.1",))
917
+ return sslcontext
918
+
919
+
920
+ # The default SSLContext objects are created at import time
921
+ # since they do blocking I/O to load certificates from disk,
922
+ # and imports should always be done before the event loop starts
923
+ # or in a thread.
924
+ _SSL_CONTEXT_VERIFIED = _make_ssl_context(True)
925
+ _SSL_CONTEXT_UNVERIFIED = _make_ssl_context(False)
926
+
927
+
928
+ class TCPConnector(BaseConnector):
929
+ """TCP connector.
930
+
931
+ verify_ssl - Set to True to check ssl certifications.
932
+ fingerprint - Pass the binary sha256
933
+ digest of the expected certificate in DER format to verify
934
+ that the certificate the server presents matches. See also
935
+ https://en.wikipedia.org/wiki/HTTP_Public_Key_Pinning
936
+ resolver - Enable DNS lookups and use this
937
+ resolver
938
+ use_dns_cache - Use memory cache for DNS lookups.
939
+ ttl_dns_cache - Max seconds having cached a DNS entry, None forever.
940
+ family - socket address family
941
+ local_addr - local tuple of (host, port) to bind socket to
942
+
943
+ keepalive_timeout - (optional) Keep-alive timeout.
944
+ force_close - Set to True to force close and do reconnect
945
+ after each request (and between redirects).
946
+ limit - The total number of simultaneous connections.
947
+ limit_per_host - Number of simultaneous connections to one host.
948
+ enable_cleanup_closed - Enables clean-up closed ssl transports.
949
+ Disabled by default.
950
+ happy_eyeballs_delay - This is the “Connection Attempt Delay”
951
+ as defined in RFC 8305. To disable
952
+ the happy eyeballs algorithm, set to None.
953
+ interleave - “First Address Family Count” as defined in RFC 8305
954
+ loop - Optional event loop.
955
+ socket_factory - A SocketFactoryType function that, if supplied,
956
+ will be used to create sockets given an
957
+ AddrInfoType.
958
+ ssl_shutdown_timeout - DEPRECATED. Will be removed in aiohttp 4.0.
959
+ Grace period for SSL shutdown handshake on TLS
960
+ connections. Default is 0 seconds (immediate abort).
961
+ This parameter allowed for a clean SSL shutdown by
962
+ notifying the remote peer of connection closure,
963
+ while avoiding excessive delays during connector cleanup.
964
+ Note: Only takes effect on Python 3.11+.
965
+ """
966
+
967
+ allowed_protocol_schema_set = HIGH_LEVEL_SCHEMA_SET | frozenset({"tcp"})
968
+
969
+ def __init__(
970
+ self,
971
+ *,
972
+ verify_ssl: bool = True,
973
+ fingerprint: Optional[bytes] = None,
974
+ use_dns_cache: bool = True,
975
+ ttl_dns_cache: Optional[int] = 10,
976
+ family: socket.AddressFamily = socket.AddressFamily.AF_UNSPEC,
977
+ ssl_context: Optional[SSLContext] = None,
978
+ ssl: Union[bool, Fingerprint, SSLContext] = True,
979
+ local_addr: Optional[Tuple[str, int]] = None,
980
+ resolver: Optional[AbstractResolver] = None,
981
+ keepalive_timeout: Union[None, float, object] = sentinel,
982
+ force_close: bool = False,
983
+ limit: int = 100,
984
+ limit_per_host: int = 0,
985
+ enable_cleanup_closed: bool = False,
986
+ loop: Optional[asyncio.AbstractEventLoop] = None,
987
+ timeout_ceil_threshold: float = 5,
988
+ happy_eyeballs_delay: Optional[float] = 0.25,
989
+ interleave: Optional[int] = None,
990
+ socket_factory: Optional[SocketFactoryType] = None,
991
+ ssl_shutdown_timeout: Union[_SENTINEL, None, float] = sentinel,
992
+ ):
993
+ super().__init__(
994
+ keepalive_timeout=keepalive_timeout,
995
+ force_close=force_close,
996
+ limit=limit,
997
+ limit_per_host=limit_per_host,
998
+ enable_cleanup_closed=enable_cleanup_closed,
999
+ loop=loop,
1000
+ timeout_ceil_threshold=timeout_ceil_threshold,
1001
+ )
1002
+
1003
+ self._ssl = _merge_ssl_params(ssl, verify_ssl, ssl_context, fingerprint)
1004
+
1005
+ self._resolver: AbstractResolver
1006
+ if resolver is None:
1007
+ self._resolver = DefaultResolver(loop=self._loop)
1008
+ self._resolver_owner = True
1009
+ else:
1010
+ self._resolver = resolver
1011
+ self._resolver_owner = False
1012
+
1013
+ self._use_dns_cache = use_dns_cache
1014
+ self._cached_hosts = _DNSCacheTable(ttl=ttl_dns_cache)
1015
+ self._throttle_dns_futures: Dict[
1016
+ Tuple[str, int], Set["asyncio.Future[None]"]
1017
+ ] = {}
1018
+ self._family = family
1019
+ self._local_addr_infos = aiohappyeyeballs.addr_to_addr_infos(local_addr)
1020
+ self._happy_eyeballs_delay = happy_eyeballs_delay
1021
+ self._interleave = interleave
1022
+ self._resolve_host_tasks: Set["asyncio.Task[List[ResolveResult]]"] = set()
1023
+ self._socket_factory = socket_factory
1024
+ self._ssl_shutdown_timeout: Optional[float]
1025
+ # Handle ssl_shutdown_timeout with warning for Python < 3.11
1026
+ if ssl_shutdown_timeout is sentinel:
1027
+ self._ssl_shutdown_timeout = 0
1028
+ else:
1029
+ # Deprecation warning for ssl_shutdown_timeout parameter
1030
+ warnings.warn(
1031
+ "The ssl_shutdown_timeout parameter is deprecated and will be removed in aiohttp 4.0",
1032
+ DeprecationWarning,
1033
+ stacklevel=2,
1034
+ )
1035
+ if (
1036
+ sys.version_info < (3, 11)
1037
+ and ssl_shutdown_timeout is not None
1038
+ and ssl_shutdown_timeout != 0
1039
+ ):
1040
+ warnings.warn(
1041
+ f"ssl_shutdown_timeout={ssl_shutdown_timeout} is ignored on Python < 3.11; "
1042
+ "only ssl_shutdown_timeout=0 is supported. The timeout will be ignored.",
1043
+ RuntimeWarning,
1044
+ stacklevel=2,
1045
+ )
1046
+ self._ssl_shutdown_timeout = ssl_shutdown_timeout
1047
+
1048
+ def _close(self, *, abort_ssl: bool = False) -> List[Awaitable[object]]:
1049
+ """Close all ongoing DNS calls."""
1050
+ for fut in chain.from_iterable(self._throttle_dns_futures.values()):
1051
+ fut.cancel()
1052
+
1053
+ waiters = super()._close(abort_ssl=abort_ssl)
1054
+
1055
+ for t in self._resolve_host_tasks:
1056
+ t.cancel()
1057
+ waiters.append(t)
1058
+
1059
+ return waiters
1060
+
1061
+ async def close(self, *, abort_ssl: bool = False) -> None:
1062
+ """
1063
+ Close all opened transports.
1064
+
1065
+ :param abort_ssl: If True, SSL connections will be aborted immediately
1066
+ without performing the shutdown handshake. If False (default),
1067
+ the behavior is determined by ssl_shutdown_timeout:
1068
+ - If ssl_shutdown_timeout=0: connections are aborted
1069
+ - If ssl_shutdown_timeout>0: graceful shutdown is performed
1070
+ """
1071
+ if self._resolver_owner:
1072
+ await self._resolver.close()
1073
+ # Use abort_ssl param if explicitly set, otherwise use ssl_shutdown_timeout default
1074
+ await super().close(abort_ssl=abort_ssl or self._ssl_shutdown_timeout == 0)
1075
+
1076
+ @property
1077
+ def family(self) -> int:
1078
+ """Socket family like AF_INET."""
1079
+ return self._family
1080
+
1081
+ @property
1082
+ def use_dns_cache(self) -> bool:
1083
+ """True if local DNS caching is enabled."""
1084
+ return self._use_dns_cache
1085
+
1086
+ def clear_dns_cache(
1087
+ self, host: Optional[str] = None, port: Optional[int] = None
1088
+ ) -> None:
1089
+ """Remove specified host/port or clear all dns local cache."""
1090
+ if host is not None and port is not None:
1091
+ self._cached_hosts.remove((host, port))
1092
+ elif host is not None or port is not None:
1093
+ raise ValueError("either both host and port or none of them are allowed")
1094
+ else:
1095
+ self._cached_hosts.clear()
1096
+
1097
+ async def _resolve_host(
1098
+ self, host: str, port: int, traces: Optional[Sequence["Trace"]] = None
1099
+ ) -> List[ResolveResult]:
1100
+ """Resolve host and return list of addresses."""
1101
+ if is_ip_address(host):
1102
+ return [
1103
+ {
1104
+ "hostname": host,
1105
+ "host": host,
1106
+ "port": port,
1107
+ "family": self._family,
1108
+ "proto": 0,
1109
+ "flags": 0,
1110
+ }
1111
+ ]
1112
+
1113
+ if not self._use_dns_cache:
1114
+
1115
+ if traces:
1116
+ for trace in traces:
1117
+ await trace.send_dns_resolvehost_start(host)
1118
+
1119
+ res = await self._resolver.resolve(host, port, family=self._family)
1120
+
1121
+ if traces:
1122
+ for trace in traces:
1123
+ await trace.send_dns_resolvehost_end(host)
1124
+
1125
+ return res
1126
+
1127
+ key = (host, port)
1128
+ if key in self._cached_hosts and not self._cached_hosts.expired(key):
1129
+ # get result early, before any await (#4014)
1130
+ result = self._cached_hosts.next_addrs(key)
1131
+
1132
+ if traces:
1133
+ for trace in traces:
1134
+ await trace.send_dns_cache_hit(host)
1135
+ return result
1136
+
1137
+ futures: Set["asyncio.Future[None]"]
1138
+ #
1139
+ # If multiple connectors are resolving the same host, we wait
1140
+ # for the first one to resolve and then use the result for all of them.
1141
+ # We use a throttle to ensure that we only resolve the host once
1142
+ # and then use the result for all the waiters.
1143
+ #
1144
+ if key in self._throttle_dns_futures:
1145
+ # get futures early, before any await (#4014)
1146
+ futures = self._throttle_dns_futures[key]
1147
+ future: asyncio.Future[None] = self._loop.create_future()
1148
+ futures.add(future)
1149
+ if traces:
1150
+ for trace in traces:
1151
+ await trace.send_dns_cache_hit(host)
1152
+ try:
1153
+ await future
1154
+ finally:
1155
+ futures.discard(future)
1156
+ return self._cached_hosts.next_addrs(key)
1157
+
1158
+ # update dict early, before any await (#4014)
1159
+ self._throttle_dns_futures[key] = futures = set()
1160
+ # In this case we need to create a task to ensure that we can shield
1161
+ # the task from cancellation as cancelling this lookup should not cancel
1162
+ # the underlying lookup or else the cancel event will get broadcast to
1163
+ # all the waiters across all connections.
1164
+ #
1165
+ coro = self._resolve_host_with_throttle(key, host, port, futures, traces)
1166
+ loop = asyncio.get_running_loop()
1167
+ if sys.version_info >= (3, 12):
1168
+ # Optimization for Python 3.12, try to send immediately
1169
+ resolved_host_task = asyncio.Task(coro, loop=loop, eager_start=True)
1170
+ else:
1171
+ resolved_host_task = loop.create_task(coro)
1172
+
1173
+ if not resolved_host_task.done():
1174
+ self._resolve_host_tasks.add(resolved_host_task)
1175
+ resolved_host_task.add_done_callback(self._resolve_host_tasks.discard)
1176
+
1177
+ try:
1178
+ return await asyncio.shield(resolved_host_task)
1179
+ except asyncio.CancelledError:
1180
+
1181
+ def drop_exception(fut: "asyncio.Future[List[ResolveResult]]") -> None:
1182
+ with suppress(Exception, asyncio.CancelledError):
1183
+ fut.result()
1184
+
1185
+ resolved_host_task.add_done_callback(drop_exception)
1186
+ raise
1187
+
1188
+ async def _resolve_host_with_throttle(
1189
+ self,
1190
+ key: Tuple[str, int],
1191
+ host: str,
1192
+ port: int,
1193
+ futures: Set["asyncio.Future[None]"],
1194
+ traces: Optional[Sequence["Trace"]],
1195
+ ) -> List[ResolveResult]:
1196
+ """Resolve host and set result for all waiters.
1197
+
1198
+ This method must be run in a task and shielded from cancellation
1199
+ to avoid cancelling the underlying lookup.
1200
+ """
1201
+ try:
1202
+ if traces:
1203
+ for trace in traces:
1204
+ await trace.send_dns_cache_miss(host)
1205
+
1206
+ for trace in traces:
1207
+ await trace.send_dns_resolvehost_start(host)
1208
+
1209
+ addrs = await self._resolver.resolve(host, port, family=self._family)
1210
+ if traces:
1211
+ for trace in traces:
1212
+ await trace.send_dns_resolvehost_end(host)
1213
+
1214
+ self._cached_hosts.add(key, addrs)
1215
+ for fut in futures:
1216
+ set_result(fut, None)
1217
+ except BaseException as e:
1218
+ # any DNS exception is set for the waiters to raise the same exception.
1219
+ # This coro is always run in task that is shielded from cancellation so
1220
+ # we should never be propagating cancellation here.
1221
+ for fut in futures:
1222
+ set_exception(fut, e)
1223
+ raise
1224
+ finally:
1225
+ self._throttle_dns_futures.pop(key)
1226
+
1227
+ return self._cached_hosts.next_addrs(key)
1228
+
1229
+ async def _create_connection(
1230
+ self, req: ClientRequest, traces: List["Trace"], timeout: "ClientTimeout"
1231
+ ) -> ResponseHandler:
1232
+ """Create connection.
1233
+
1234
+ Has same keyword arguments as BaseEventLoop.create_connection.
1235
+ """
1236
+ if req.proxy:
1237
+ _, proto = await self._create_proxy_connection(req, traces, timeout)
1238
+ else:
1239
+ _, proto = await self._create_direct_connection(req, traces, timeout)
1240
+
1241
+ return proto
1242
+
1243
+ def _get_ssl_context(self, req: ClientRequest) -> Optional[SSLContext]:
1244
+ """Logic to get the correct SSL context
1245
+
1246
+ 0. if req.ssl is false, return None
1247
+
1248
+ 1. if ssl_context is specified in req, use it
1249
+ 2. if _ssl_context is specified in self, use it
1250
+ 3. otherwise:
1251
+ 1. if verify_ssl is not specified in req, use self.ssl_context
1252
+ (will generate a default context according to self.verify_ssl)
1253
+ 2. if verify_ssl is True in req, generate a default SSL context
1254
+ 3. if verify_ssl is False in req, generate a SSL context that
1255
+ won't verify
1256
+ """
1257
+ if not req.is_ssl():
1258
+ return None
1259
+
1260
+ if ssl is None: # pragma: no cover
1261
+ raise RuntimeError("SSL is not supported.")
1262
+ sslcontext = req.ssl
1263
+ if isinstance(sslcontext, ssl.SSLContext):
1264
+ return sslcontext
1265
+ if sslcontext is not True:
1266
+ # not verified or fingerprinted
1267
+ return _SSL_CONTEXT_UNVERIFIED
1268
+ sslcontext = self._ssl
1269
+ if isinstance(sslcontext, ssl.SSLContext):
1270
+ return sslcontext
1271
+ if sslcontext is not True:
1272
+ # not verified or fingerprinted
1273
+ return _SSL_CONTEXT_UNVERIFIED
1274
+ return _SSL_CONTEXT_VERIFIED
1275
+
1276
+ def _get_fingerprint(self, req: ClientRequest) -> Optional["Fingerprint"]:
1277
+ ret = req.ssl
1278
+ if isinstance(ret, Fingerprint):
1279
+ return ret
1280
+ ret = self._ssl
1281
+ if isinstance(ret, Fingerprint):
1282
+ return ret
1283
+ return None
1284
+
1285
+ async def _wrap_create_connection(
1286
+ self,
1287
+ *args: Any,
1288
+ addr_infos: List[AddrInfoType],
1289
+ req: ClientRequest,
1290
+ timeout: "ClientTimeout",
1291
+ client_error: Type[Exception] = ClientConnectorError,
1292
+ **kwargs: Any,
1293
+ ) -> Tuple[asyncio.Transport, ResponseHandler]:
1294
+ try:
1295
+ async with ceil_timeout(
1296
+ timeout.sock_connect, ceil_threshold=timeout.ceil_threshold
1297
+ ):
1298
+ sock = await aiohappyeyeballs.start_connection(
1299
+ addr_infos=addr_infos,
1300
+ local_addr_infos=self._local_addr_infos,
1301
+ happy_eyeballs_delay=self._happy_eyeballs_delay,
1302
+ interleave=self._interleave,
1303
+ loop=self._loop,
1304
+ socket_factory=self._socket_factory,
1305
+ )
1306
+ # Add ssl_shutdown_timeout for Python 3.11+ when SSL is used
1307
+ if (
1308
+ kwargs.get("ssl")
1309
+ and self._ssl_shutdown_timeout
1310
+ and sys.version_info >= (3, 11)
1311
+ ):
1312
+ kwargs["ssl_shutdown_timeout"] = self._ssl_shutdown_timeout
1313
+ return await self._loop.create_connection(*args, **kwargs, sock=sock)
1314
+ except cert_errors as exc:
1315
+ raise ClientConnectorCertificateError(req.connection_key, exc) from exc
1316
+ except ssl_errors as exc:
1317
+ raise ClientConnectorSSLError(req.connection_key, exc) from exc
1318
+ except OSError as exc:
1319
+ if exc.errno is None and isinstance(exc, asyncio.TimeoutError):
1320
+ raise
1321
+ raise client_error(req.connection_key, exc) from exc
1322
+
1323
+ async def _wrap_existing_connection(
1324
+ self,
1325
+ *args: Any,
1326
+ req: ClientRequest,
1327
+ timeout: "ClientTimeout",
1328
+ client_error: Type[Exception] = ClientConnectorError,
1329
+ **kwargs: Any,
1330
+ ) -> Tuple[asyncio.Transport, ResponseHandler]:
1331
+ try:
1332
+ async with ceil_timeout(
1333
+ timeout.sock_connect, ceil_threshold=timeout.ceil_threshold
1334
+ ):
1335
+ return await self._loop.create_connection(*args, **kwargs)
1336
+ except cert_errors as exc:
1337
+ raise ClientConnectorCertificateError(req.connection_key, exc) from exc
1338
+ except ssl_errors as exc:
1339
+ raise ClientConnectorSSLError(req.connection_key, exc) from exc
1340
+ except OSError as exc:
1341
+ if exc.errno is None and isinstance(exc, asyncio.TimeoutError):
1342
+ raise
1343
+ raise client_error(req.connection_key, exc) from exc
1344
+
1345
+ def _fail_on_no_start_tls(self, req: "ClientRequest") -> None:
1346
+ """Raise a :py:exc:`RuntimeError` on missing ``start_tls()``.
1347
+
1348
+ It is necessary for TLS-in-TLS so that it is possible to
1349
+ send HTTPS queries through HTTPS proxies.
1350
+
1351
+ This doesn't affect regular HTTP requests, though.
1352
+ """
1353
+ if not req.is_ssl():
1354
+ return
1355
+
1356
+ proxy_url = req.proxy
1357
+ assert proxy_url is not None
1358
+ if proxy_url.scheme != "https":
1359
+ return
1360
+
1361
+ self._check_loop_for_start_tls()
1362
+
1363
+ def _check_loop_for_start_tls(self) -> None:
1364
+ try:
1365
+ self._loop.start_tls
1366
+ except AttributeError as attr_exc:
1367
+ raise RuntimeError(
1368
+ "An HTTPS request is being sent through an HTTPS proxy. "
1369
+ "This needs support for TLS in TLS but it is not implemented "
1370
+ "in your runtime for the stdlib asyncio.\n\n"
1371
+ "Please upgrade to Python 3.11 or higher. For more details, "
1372
+ "please see:\n"
1373
+ "* https://bugs.python.org/issue37179\n"
1374
+ "* https://github.com/python/cpython/pull/28073\n"
1375
+ "* https://docs.aiohttp.org/en/stable/"
1376
+ "client_advanced.html#proxy-support\n"
1377
+ "* https://github.com/aio-libs/aiohttp/discussions/6044\n",
1378
+ ) from attr_exc
1379
+
1380
+ def _loop_supports_start_tls(self) -> bool:
1381
+ try:
1382
+ self._check_loop_for_start_tls()
1383
+ except RuntimeError:
1384
+ return False
1385
+ else:
1386
+ return True
1387
+
1388
+ def _warn_about_tls_in_tls(
1389
+ self,
1390
+ underlying_transport: asyncio.Transport,
1391
+ req: ClientRequest,
1392
+ ) -> None:
1393
+ """Issue a warning if the requested URL has HTTPS scheme."""
1394
+ if req.request_info.url.scheme != "https":
1395
+ return
1396
+
1397
+ # Check if uvloop is being used, which supports TLS in TLS,
1398
+ # otherwise assume that asyncio's native transport is being used.
1399
+ if type(underlying_transport).__module__.startswith("uvloop"):
1400
+ return
1401
+
1402
+ # Support in asyncio was added in Python 3.11 (bpo-44011)
1403
+ asyncio_supports_tls_in_tls = sys.version_info >= (3, 11) or getattr(
1404
+ underlying_transport,
1405
+ "_start_tls_compatible",
1406
+ False,
1407
+ )
1408
+
1409
+ if asyncio_supports_tls_in_tls:
1410
+ return
1411
+
1412
+ warnings.warn(
1413
+ "An HTTPS request is being sent through an HTTPS proxy. "
1414
+ "This support for TLS in TLS is known to be disabled "
1415
+ "in the stdlib asyncio (Python <3.11). This is why you'll probably see "
1416
+ "an error in the log below.\n\n"
1417
+ "It is possible to enable it via monkeypatching. "
1418
+ "For more details, see:\n"
1419
+ "* https://bugs.python.org/issue37179\n"
1420
+ "* https://github.com/python/cpython/pull/28073\n\n"
1421
+ "You can temporarily patch this as follows:\n"
1422
+ "* https://docs.aiohttp.org/en/stable/client_advanced.html#proxy-support\n"
1423
+ "* https://github.com/aio-libs/aiohttp/discussions/6044\n",
1424
+ RuntimeWarning,
1425
+ source=self,
1426
+ # Why `4`? At least 3 of the calls in the stack originate
1427
+ # from the methods in this class.
1428
+ stacklevel=3,
1429
+ )
1430
+
1431
+ async def _start_tls_connection(
1432
+ self,
1433
+ underlying_transport: asyncio.Transport,
1434
+ req: ClientRequest,
1435
+ timeout: "ClientTimeout",
1436
+ client_error: Type[Exception] = ClientConnectorError,
1437
+ ) -> Tuple[asyncio.BaseTransport, ResponseHandler]:
1438
+ """Wrap the raw TCP transport with TLS."""
1439
+ tls_proto = self._factory() # Create a brand new proto for TLS
1440
+ sslcontext = self._get_ssl_context(req)
1441
+ if TYPE_CHECKING:
1442
+ # _start_tls_connection is unreachable in the current code path
1443
+ # if sslcontext is None.
1444
+ assert sslcontext is not None
1445
+
1446
+ try:
1447
+ async with ceil_timeout(
1448
+ timeout.sock_connect, ceil_threshold=timeout.ceil_threshold
1449
+ ):
1450
+ try:
1451
+ # ssl_shutdown_timeout is only available in Python 3.11+
1452
+ if sys.version_info >= (3, 11) and self._ssl_shutdown_timeout:
1453
+ tls_transport = await self._loop.start_tls(
1454
+ underlying_transport,
1455
+ tls_proto,
1456
+ sslcontext,
1457
+ server_hostname=req.server_hostname or req.host,
1458
+ ssl_handshake_timeout=timeout.total,
1459
+ ssl_shutdown_timeout=self._ssl_shutdown_timeout,
1460
+ )
1461
+ else:
1462
+ tls_transport = await self._loop.start_tls(
1463
+ underlying_transport,
1464
+ tls_proto,
1465
+ sslcontext,
1466
+ server_hostname=req.server_hostname or req.host,
1467
+ ssl_handshake_timeout=timeout.total,
1468
+ )
1469
+ except BaseException:
1470
+ # We need to close the underlying transport since
1471
+ # `start_tls()` probably failed before it had a
1472
+ # chance to do this:
1473
+ if self._ssl_shutdown_timeout == 0:
1474
+ underlying_transport.abort()
1475
+ else:
1476
+ underlying_transport.close()
1477
+ raise
1478
+ if isinstance(tls_transport, asyncio.Transport):
1479
+ fingerprint = self._get_fingerprint(req)
1480
+ if fingerprint:
1481
+ try:
1482
+ fingerprint.check(tls_transport)
1483
+ except ServerFingerprintMismatch:
1484
+ tls_transport.close()
1485
+ if not self._cleanup_closed_disabled:
1486
+ self._cleanup_closed_transports.append(tls_transport)
1487
+ raise
1488
+ except cert_errors as exc:
1489
+ raise ClientConnectorCertificateError(req.connection_key, exc) from exc
1490
+ except ssl_errors as exc:
1491
+ raise ClientConnectorSSLError(req.connection_key, exc) from exc
1492
+ except OSError as exc:
1493
+ if exc.errno is None and isinstance(exc, asyncio.TimeoutError):
1494
+ raise
1495
+ raise client_error(req.connection_key, exc) from exc
1496
+ except TypeError as type_err:
1497
+ # Example cause looks like this:
1498
+ # TypeError: transport <asyncio.sslproto._SSLProtocolTransport
1499
+ # object at 0x7f760615e460> is not supported by start_tls()
1500
+
1501
+ raise ClientConnectionError(
1502
+ "Cannot initialize a TLS-in-TLS connection to host "
1503
+ f"{req.host!s}:{req.port:d} through an underlying connection "
1504
+ f"to an HTTPS proxy {req.proxy!s} ssl:{req.ssl or 'default'} "
1505
+ f"[{type_err!s}]"
1506
+ ) from type_err
1507
+ else:
1508
+ if tls_transport is None:
1509
+ msg = "Failed to start TLS (possibly caused by closing transport)"
1510
+ raise client_error(req.connection_key, OSError(msg))
1511
+ tls_proto.connection_made(
1512
+ tls_transport
1513
+ ) # Kick the state machine of the new TLS protocol
1514
+
1515
+ return tls_transport, tls_proto
1516
+
1517
+ def _convert_hosts_to_addr_infos(
1518
+ self, hosts: List[ResolveResult]
1519
+ ) -> List[AddrInfoType]:
1520
+ """Converts the list of hosts to a list of addr_infos.
1521
+
1522
+ The list of hosts is the result of a DNS lookup. The list of
1523
+ addr_infos is the result of a call to `socket.getaddrinfo()`.
1524
+ """
1525
+ addr_infos: List[AddrInfoType] = []
1526
+ for hinfo in hosts:
1527
+ host = hinfo["host"]
1528
+ is_ipv6 = ":" in host
1529
+ family = socket.AF_INET6 if is_ipv6 else socket.AF_INET
1530
+ if self._family and self._family != family:
1531
+ continue
1532
+ addr = (host, hinfo["port"], 0, 0) if is_ipv6 else (host, hinfo["port"])
1533
+ addr_infos.append(
1534
+ (family, socket.SOCK_STREAM, socket.IPPROTO_TCP, "", addr)
1535
+ )
1536
+ return addr_infos
1537
+
1538
+ async def _create_direct_connection(
1539
+ self,
1540
+ req: ClientRequest,
1541
+ traces: List["Trace"],
1542
+ timeout: "ClientTimeout",
1543
+ *,
1544
+ client_error: Type[Exception] = ClientConnectorError,
1545
+ ) -> Tuple[asyncio.Transport, ResponseHandler]:
1546
+ sslcontext = self._get_ssl_context(req)
1547
+ fingerprint = self._get_fingerprint(req)
1548
+
1549
+ host = req.url.raw_host
1550
+ assert host is not None
1551
+ # Replace multiple trailing dots with a single one.
1552
+ # A trailing dot is only present for fully-qualified domain names.
1553
+ # See https://github.com/aio-libs/aiohttp/pull/7364.
1554
+ if host.endswith(".."):
1555
+ host = host.rstrip(".") + "."
1556
+ port = req.port
1557
+ assert port is not None
1558
+ try:
1559
+ # Cancelling this lookup should not cancel the underlying lookup
1560
+ # or else the cancel event will get broadcast to all the waiters
1561
+ # across all connections.
1562
+ hosts = await self._resolve_host(host, port, traces=traces)
1563
+ except OSError as exc:
1564
+ if exc.errno is None and isinstance(exc, asyncio.TimeoutError):
1565
+ raise
1566
+ # in case of proxy it is not ClientProxyConnectionError
1567
+ # it is problem of resolving proxy ip itself
1568
+ raise ClientConnectorDNSError(req.connection_key, exc) from exc
1569
+
1570
+ last_exc: Optional[Exception] = None
1571
+ addr_infos = self._convert_hosts_to_addr_infos(hosts)
1572
+ while addr_infos:
1573
+ # Strip trailing dots, certificates contain FQDN without dots.
1574
+ # See https://github.com/aio-libs/aiohttp/issues/3636
1575
+ server_hostname = (
1576
+ (req.server_hostname or host).rstrip(".") if sslcontext else None
1577
+ )
1578
+
1579
+ try:
1580
+ transp, proto = await self._wrap_create_connection(
1581
+ self._factory,
1582
+ timeout=timeout,
1583
+ ssl=sslcontext,
1584
+ addr_infos=addr_infos,
1585
+ server_hostname=server_hostname,
1586
+ req=req,
1587
+ client_error=client_error,
1588
+ )
1589
+ except (ClientConnectorError, asyncio.TimeoutError) as exc:
1590
+ last_exc = exc
1591
+ aiohappyeyeballs.pop_addr_infos_interleave(addr_infos, self._interleave)
1592
+ continue
1593
+
1594
+ if req.is_ssl() and fingerprint:
1595
+ try:
1596
+ fingerprint.check(transp)
1597
+ except ServerFingerprintMismatch as exc:
1598
+ transp.close()
1599
+ if not self._cleanup_closed_disabled:
1600
+ self._cleanup_closed_transports.append(transp)
1601
+ last_exc = exc
1602
+ # Remove the bad peer from the list of addr_infos
1603
+ sock: socket.socket = transp.get_extra_info("socket")
1604
+ bad_peer = sock.getpeername()
1605
+ aiohappyeyeballs.remove_addr_infos(addr_infos, bad_peer)
1606
+ continue
1607
+
1608
+ return transp, proto
1609
+ else:
1610
+ assert last_exc is not None
1611
+ raise last_exc
1612
+
1613
+ async def _create_proxy_connection(
1614
+ self, req: ClientRequest, traces: List["Trace"], timeout: "ClientTimeout"
1615
+ ) -> Tuple[asyncio.BaseTransport, ResponseHandler]:
1616
+ self._fail_on_no_start_tls(req)
1617
+ runtime_has_start_tls = self._loop_supports_start_tls()
1618
+ proxy_req = self._update_proxy_auth_header_and_build_proxy_req(req)
1619
+
1620
+ # create connection to proxy server
1621
+ transport, proto = await self._create_direct_connection(
1622
+ proxy_req, [], timeout, client_error=ClientProxyConnectionError
1623
+ )
1624
+
1625
+ if req.is_ssl():
1626
+ if runtime_has_start_tls:
1627
+ self._warn_about_tls_in_tls(transport, req)
1628
+
1629
+ # For HTTPS requests over HTTP proxy
1630
+ # we must notify proxy to tunnel connection
1631
+ # so we send CONNECT command:
1632
+ # CONNECT www.python.org:443 HTTP/1.1
1633
+ # Host: www.python.org
1634
+ #
1635
+ # next we must do TLS handshake and so on
1636
+ # to do this we must wrap raw socket into secure one
1637
+ # asyncio handles this perfectly
1638
+ proxy_req.method = hdrs.METH_CONNECT
1639
+ proxy_req.url = req.url
1640
+ key = req.connection_key._replace(
1641
+ proxy=None, proxy_auth=None, proxy_headers_hash=None
1642
+ )
1643
+ conn = _ConnectTunnelConnection(self, key, proto, self._loop)
1644
+ proxy_resp = await proxy_req.send(conn)
1645
+ try:
1646
+ protocol = conn._protocol
1647
+ assert protocol is not None
1648
+
1649
+ # read_until_eof=True will ensure the connection isn't closed
1650
+ # once the response is received and processed allowing
1651
+ # START_TLS to work on the connection below.
1652
+ protocol.set_response_params(
1653
+ read_until_eof=runtime_has_start_tls,
1654
+ timeout_ceil_threshold=self._timeout_ceil_threshold,
1655
+ )
1656
+ resp = await proxy_resp.start(conn)
1657
+ except BaseException:
1658
+ proxy_resp.close()
1659
+ conn.close()
1660
+ raise
1661
+ else:
1662
+ conn._protocol = None
1663
+ try:
1664
+ if resp.status != 200:
1665
+ message = resp.reason
1666
+ if message is None:
1667
+ message = HTTPStatus(resp.status).phrase
1668
+ raise ClientHttpProxyError(
1669
+ proxy_resp.request_info,
1670
+ resp.history,
1671
+ status=resp.status,
1672
+ message=message,
1673
+ headers=resp.headers,
1674
+ )
1675
+ if not runtime_has_start_tls:
1676
+ rawsock = transport.get_extra_info("socket", default=None)
1677
+ if rawsock is None:
1678
+ raise RuntimeError(
1679
+ "Transport does not expose socket instance"
1680
+ )
1681
+ # Duplicate the socket, so now we can close proxy transport
1682
+ rawsock = rawsock.dup()
1683
+ except BaseException:
1684
+ # It shouldn't be closed in `finally` because it's fed to
1685
+ # `loop.start_tls()` and the docs say not to touch it after
1686
+ # passing there.
1687
+ transport.close()
1688
+ raise
1689
+ finally:
1690
+ if not runtime_has_start_tls:
1691
+ transport.close()
1692
+
1693
+ if not runtime_has_start_tls:
1694
+ # HTTP proxy with support for upgrade to HTTPS
1695
+ sslcontext = self._get_ssl_context(req)
1696
+ return await self._wrap_existing_connection(
1697
+ self._factory,
1698
+ timeout=timeout,
1699
+ ssl=sslcontext,
1700
+ sock=rawsock,
1701
+ server_hostname=req.host,
1702
+ req=req,
1703
+ )
1704
+
1705
+ return await self._start_tls_connection(
1706
+ # Access the old transport for the last time before it's
1707
+ # closed and forgotten forever:
1708
+ transport,
1709
+ req=req,
1710
+ timeout=timeout,
1711
+ )
1712
+ finally:
1713
+ proxy_resp.close()
1714
+
1715
+ return transport, proto
1716
+
1717
+
1718
+ class UnixConnector(BaseConnector):
1719
+ """Unix socket connector.
1720
+
1721
+ path - Unix socket path.
1722
+ keepalive_timeout - (optional) Keep-alive timeout.
1723
+ force_close - Set to True to force close and do reconnect
1724
+ after each request (and between redirects).
1725
+ limit - The total number of simultaneous connections.
1726
+ limit_per_host - Number of simultaneous connections to one host.
1727
+ loop - Optional event loop.
1728
+ """
1729
+
1730
+ allowed_protocol_schema_set = HIGH_LEVEL_SCHEMA_SET | frozenset({"unix"})
1731
+
1732
+ def __init__(
1733
+ self,
1734
+ path: str,
1735
+ force_close: bool = False,
1736
+ keepalive_timeout: Union[object, float, None] = sentinel,
1737
+ limit: int = 100,
1738
+ limit_per_host: int = 0,
1739
+ loop: Optional[asyncio.AbstractEventLoop] = None,
1740
+ ) -> None:
1741
+ super().__init__(
1742
+ force_close=force_close,
1743
+ keepalive_timeout=keepalive_timeout,
1744
+ limit=limit,
1745
+ limit_per_host=limit_per_host,
1746
+ loop=loop,
1747
+ )
1748
+ self._path = path
1749
+
1750
+ @property
1751
+ def path(self) -> str:
1752
+ """Path to unix socket."""
1753
+ return self._path
1754
+
1755
+ async def _create_connection(
1756
+ self, req: ClientRequest, traces: List["Trace"], timeout: "ClientTimeout"
1757
+ ) -> ResponseHandler:
1758
+ try:
1759
+ async with ceil_timeout(
1760
+ timeout.sock_connect, ceil_threshold=timeout.ceil_threshold
1761
+ ):
1762
+ _, proto = await self._loop.create_unix_connection(
1763
+ self._factory, self._path
1764
+ )
1765
+ except OSError as exc:
1766
+ if exc.errno is None and isinstance(exc, asyncio.TimeoutError):
1767
+ raise
1768
+ raise UnixClientConnectorError(self.path, req.connection_key, exc) from exc
1769
+
1770
+ return proto
1771
+
1772
+
1773
+ class NamedPipeConnector(BaseConnector):
1774
+ """Named pipe connector.
1775
+
1776
+ Only supported by the proactor event loop.
1777
+ See also: https://docs.python.org/3/library/asyncio-eventloop.html
1778
+
1779
+ path - Windows named pipe path.
1780
+ keepalive_timeout - (optional) Keep-alive timeout.
1781
+ force_close - Set to True to force close and do reconnect
1782
+ after each request (and between redirects).
1783
+ limit - The total number of simultaneous connections.
1784
+ limit_per_host - Number of simultaneous connections to one host.
1785
+ loop - Optional event loop.
1786
+ """
1787
+
1788
+ allowed_protocol_schema_set = HIGH_LEVEL_SCHEMA_SET | frozenset({"npipe"})
1789
+
1790
+ def __init__(
1791
+ self,
1792
+ path: str,
1793
+ force_close: bool = False,
1794
+ keepalive_timeout: Union[object, float, None] = sentinel,
1795
+ limit: int = 100,
1796
+ limit_per_host: int = 0,
1797
+ loop: Optional[asyncio.AbstractEventLoop] = None,
1798
+ ) -> None:
1799
+ super().__init__(
1800
+ force_close=force_close,
1801
+ keepalive_timeout=keepalive_timeout,
1802
+ limit=limit,
1803
+ limit_per_host=limit_per_host,
1804
+ loop=loop,
1805
+ )
1806
+ if not isinstance(
1807
+ self._loop,
1808
+ asyncio.ProactorEventLoop, # type: ignore[attr-defined]
1809
+ ):
1810
+ raise RuntimeError(
1811
+ "Named Pipes only available in proactor loop under windows"
1812
+ )
1813
+ self._path = path
1814
+
1815
+ @property
1816
+ def path(self) -> str:
1817
+ """Path to the named pipe."""
1818
+ return self._path
1819
+
1820
+ async def _create_connection(
1821
+ self, req: ClientRequest, traces: List["Trace"], timeout: "ClientTimeout"
1822
+ ) -> ResponseHandler:
1823
+ try:
1824
+ async with ceil_timeout(
1825
+ timeout.sock_connect, ceil_threshold=timeout.ceil_threshold
1826
+ ):
1827
+ _, proto = await self._loop.create_pipe_connection( # type: ignore[attr-defined]
1828
+ self._factory, self._path
1829
+ )
1830
+ # the drain is required so that the connection_made is called
1831
+ # and transport is set otherwise it is not set before the
1832
+ # `assert conn.transport is not None`
1833
+ # in client.py's _request method
1834
+ await asyncio.sleep(0)
1835
+ # other option is to manually set transport like
1836
+ # `proto.transport = trans`
1837
+ except OSError as exc:
1838
+ if exc.errno is None and isinstance(exc, asyncio.TimeoutError):
1839
+ raise
1840
+ raise ClientConnectorError(req.connection_key, exc) from exc
1841
+
1842
+ return cast(ResponseHandler, proto)
aiohttp/cookiejar.py ADDED
@@ -0,0 +1,522 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import calendar
3
+ import contextlib
4
+ import datetime
5
+ import heapq
6
+ import itertools
7
+ import os # noqa
8
+ import pathlib
9
+ import pickle
10
+ import re
11
+ import time
12
+ import warnings
13
+ from collections import defaultdict
14
+ from collections.abc import Mapping
15
+ from http.cookies import BaseCookie, Morsel, SimpleCookie
16
+ from typing import (
17
+ DefaultDict,
18
+ Dict,
19
+ Iterable,
20
+ Iterator,
21
+ List,
22
+ Optional,
23
+ Set,
24
+ Tuple,
25
+ Union,
26
+ )
27
+
28
+ from yarl import URL
29
+
30
+ from ._cookie_helpers import preserve_morsel_with_coded_value
31
+ from .abc import AbstractCookieJar, ClearCookiePredicate
32
+ from .helpers import is_ip_address
33
+ from .typedefs import LooseCookies, PathLike, StrOrURL
34
+
35
+ __all__ = ("CookieJar", "DummyCookieJar")
36
+
37
+
38
+ CookieItem = Union[str, "Morsel[str]"]
39
+
40
+ # We cache these string methods here as their use is in performance critical code.
41
+ _FORMAT_PATH = "{}/{}".format
42
+ _FORMAT_DOMAIN_REVERSED = "{1}.{0}".format
43
+
44
+ # The minimum number of scheduled cookie expirations before we start cleaning up
45
+ # the expiration heap. This is a performance optimization to avoid cleaning up the
46
+ # heap too often when there are only a few scheduled expirations.
47
+ _MIN_SCHEDULED_COOKIE_EXPIRATION = 100
48
+ _SIMPLE_COOKIE = SimpleCookie()
49
+
50
+
51
+ class CookieJar(AbstractCookieJar):
52
+ """Implements cookie storage adhering to RFC 6265."""
53
+
54
+ DATE_TOKENS_RE = re.compile(
55
+ r"[\x09\x20-\x2F\x3B-\x40\x5B-\x60\x7B-\x7E]*"
56
+ r"(?P<token>[\x00-\x08\x0A-\x1F\d:a-zA-Z\x7F-\xFF]+)"
57
+ )
58
+
59
+ DATE_HMS_TIME_RE = re.compile(r"(\d{1,2}):(\d{1,2}):(\d{1,2})")
60
+
61
+ DATE_DAY_OF_MONTH_RE = re.compile(r"(\d{1,2})")
62
+
63
+ DATE_MONTH_RE = re.compile(
64
+ "(jan)|(feb)|(mar)|(apr)|(may)|(jun)|(jul)|(aug)|(sep)|(oct)|(nov)|(dec)",
65
+ re.I,
66
+ )
67
+
68
+ DATE_YEAR_RE = re.compile(r"(\d{2,4})")
69
+
70
+ # calendar.timegm() fails for timestamps after datetime.datetime.max
71
+ # Minus one as a loss of precision occurs when timestamp() is called.
72
+ MAX_TIME = (
73
+ int(datetime.datetime.max.replace(tzinfo=datetime.timezone.utc).timestamp()) - 1
74
+ )
75
+ try:
76
+ calendar.timegm(time.gmtime(MAX_TIME))
77
+ except (OSError, ValueError):
78
+ # Hit the maximum representable time on Windows
79
+ # https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/localtime-localtime32-localtime64
80
+ # Throws ValueError on PyPy 3.9, OSError elsewhere
81
+ MAX_TIME = calendar.timegm((3000, 12, 31, 23, 59, 59, -1, -1, -1))
82
+ except OverflowError:
83
+ # #4515: datetime.max may not be representable on 32-bit platforms
84
+ MAX_TIME = 2**31 - 1
85
+ # Avoid minuses in the future, 3x faster
86
+ SUB_MAX_TIME = MAX_TIME - 1
87
+
88
+ def __init__(
89
+ self,
90
+ *,
91
+ unsafe: bool = False,
92
+ quote_cookie: bool = True,
93
+ treat_as_secure_origin: Union[StrOrURL, List[StrOrURL], None] = None,
94
+ loop: Optional[asyncio.AbstractEventLoop] = None,
95
+ ) -> None:
96
+ super().__init__(loop=loop)
97
+ self._cookies: DefaultDict[Tuple[str, str], SimpleCookie] = defaultdict(
98
+ SimpleCookie
99
+ )
100
+ self._morsel_cache: DefaultDict[Tuple[str, str], Dict[str, Morsel[str]]] = (
101
+ defaultdict(dict)
102
+ )
103
+ self._host_only_cookies: Set[Tuple[str, str]] = set()
104
+ self._unsafe = unsafe
105
+ self._quote_cookie = quote_cookie
106
+ if treat_as_secure_origin is None:
107
+ treat_as_secure_origin = []
108
+ elif isinstance(treat_as_secure_origin, URL):
109
+ treat_as_secure_origin = [treat_as_secure_origin.origin()]
110
+ elif isinstance(treat_as_secure_origin, str):
111
+ treat_as_secure_origin = [URL(treat_as_secure_origin).origin()]
112
+ else:
113
+ treat_as_secure_origin = [
114
+ URL(url).origin() if isinstance(url, str) else url.origin()
115
+ for url in treat_as_secure_origin
116
+ ]
117
+ self._treat_as_secure_origin = treat_as_secure_origin
118
+ self._expire_heap: List[Tuple[float, Tuple[str, str, str]]] = []
119
+ self._expirations: Dict[Tuple[str, str, str], float] = {}
120
+
121
+ @property
122
+ def quote_cookie(self) -> bool:
123
+ return self._quote_cookie
124
+
125
+ def save(self, file_path: PathLike) -> None:
126
+ file_path = pathlib.Path(file_path)
127
+ with file_path.open(mode="wb") as f:
128
+ pickle.dump(self._cookies, f, pickle.HIGHEST_PROTOCOL)
129
+
130
+ def load(self, file_path: PathLike) -> None:
131
+ file_path = pathlib.Path(file_path)
132
+ with file_path.open(mode="rb") as f:
133
+ self._cookies = pickle.load(f)
134
+
135
+ def clear(self, predicate: Optional[ClearCookiePredicate] = None) -> None:
136
+ if predicate is None:
137
+ self._expire_heap.clear()
138
+ self._cookies.clear()
139
+ self._morsel_cache.clear()
140
+ self._host_only_cookies.clear()
141
+ self._expirations.clear()
142
+ return
143
+
144
+ now = time.time()
145
+ to_del = [
146
+ key
147
+ for (domain, path), cookie in self._cookies.items()
148
+ for name, morsel in cookie.items()
149
+ if (
150
+ (key := (domain, path, name)) in self._expirations
151
+ and self._expirations[key] <= now
152
+ )
153
+ or predicate(morsel)
154
+ ]
155
+ if to_del:
156
+ self._delete_cookies(to_del)
157
+
158
+ def clear_domain(self, domain: str) -> None:
159
+ self.clear(lambda x: self._is_domain_match(domain, x["domain"]))
160
+
161
+ def __iter__(self) -> "Iterator[Morsel[str]]":
162
+ self._do_expiration()
163
+ for val in self._cookies.values():
164
+ yield from val.values()
165
+
166
+ def __len__(self) -> int:
167
+ """Return number of cookies.
168
+
169
+ This function does not iterate self to avoid unnecessary expiration
170
+ checks.
171
+ """
172
+ return sum(len(cookie.values()) for cookie in self._cookies.values())
173
+
174
+ def _do_expiration(self) -> None:
175
+ """Remove expired cookies."""
176
+ if not (expire_heap_len := len(self._expire_heap)):
177
+ return
178
+
179
+ # If the expiration heap grows larger than the number expirations
180
+ # times two, we clean it up to avoid keeping expired entries in
181
+ # the heap and consuming memory. We guard this with a minimum
182
+ # threshold to avoid cleaning up the heap too often when there are
183
+ # only a few scheduled expirations.
184
+ if (
185
+ expire_heap_len > _MIN_SCHEDULED_COOKIE_EXPIRATION
186
+ and expire_heap_len > len(self._expirations) * 2
187
+ ):
188
+ # Remove any expired entries from the expiration heap
189
+ # that do not match the expiration time in the expirations
190
+ # as it means the cookie has been re-added to the heap
191
+ # with a different expiration time.
192
+ self._expire_heap = [
193
+ entry
194
+ for entry in self._expire_heap
195
+ if self._expirations.get(entry[1]) == entry[0]
196
+ ]
197
+ heapq.heapify(self._expire_heap)
198
+
199
+ now = time.time()
200
+ to_del: List[Tuple[str, str, str]] = []
201
+ # Find any expired cookies and add them to the to-delete list
202
+ while self._expire_heap:
203
+ when, cookie_key = self._expire_heap[0]
204
+ if when > now:
205
+ break
206
+ heapq.heappop(self._expire_heap)
207
+ # Check if the cookie hasn't been re-added to the heap
208
+ # with a different expiration time as it will be removed
209
+ # later when it reaches the top of the heap and its
210
+ # expiration time is met.
211
+ if self._expirations.get(cookie_key) == when:
212
+ to_del.append(cookie_key)
213
+
214
+ if to_del:
215
+ self._delete_cookies(to_del)
216
+
217
+ def _delete_cookies(self, to_del: List[Tuple[str, str, str]]) -> None:
218
+ for domain, path, name in to_del:
219
+ self._host_only_cookies.discard((domain, name))
220
+ self._cookies[(domain, path)].pop(name, None)
221
+ self._morsel_cache[(domain, path)].pop(name, None)
222
+ self._expirations.pop((domain, path, name), None)
223
+
224
+ def _expire_cookie(self, when: float, domain: str, path: str, name: str) -> None:
225
+ cookie_key = (domain, path, name)
226
+ if self._expirations.get(cookie_key) == when:
227
+ # Avoid adding duplicates to the heap
228
+ return
229
+ heapq.heappush(self._expire_heap, (when, cookie_key))
230
+ self._expirations[cookie_key] = when
231
+
232
+ def update_cookies(self, cookies: LooseCookies, response_url: URL = URL()) -> None:
233
+ """Update cookies."""
234
+ hostname = response_url.raw_host
235
+
236
+ if not self._unsafe and is_ip_address(hostname):
237
+ # Don't accept cookies from IPs
238
+ return
239
+
240
+ if isinstance(cookies, Mapping):
241
+ cookies = cookies.items()
242
+
243
+ for name, cookie in cookies:
244
+ if not isinstance(cookie, Morsel):
245
+ tmp = SimpleCookie()
246
+ tmp[name] = cookie # type: ignore[assignment]
247
+ cookie = tmp[name]
248
+
249
+ domain = cookie["domain"]
250
+
251
+ # ignore domains with trailing dots
252
+ if domain and domain[-1] == ".":
253
+ domain = ""
254
+ del cookie["domain"]
255
+
256
+ if not domain and hostname is not None:
257
+ # Set the cookie's domain to the response hostname
258
+ # and set its host-only-flag
259
+ self._host_only_cookies.add((hostname, name))
260
+ domain = cookie["domain"] = hostname
261
+
262
+ if domain and domain[0] == ".":
263
+ # Remove leading dot
264
+ domain = domain[1:]
265
+ cookie["domain"] = domain
266
+
267
+ if hostname and not self._is_domain_match(domain, hostname):
268
+ # Setting cookies for different domains is not allowed
269
+ continue
270
+
271
+ path = cookie["path"]
272
+ if not path or path[0] != "/":
273
+ # Set the cookie's path to the response path
274
+ path = response_url.path
275
+ if not path.startswith("/"):
276
+ path = "/"
277
+ else:
278
+ # Cut everything from the last slash to the end
279
+ path = "/" + path[1 : path.rfind("/")]
280
+ cookie["path"] = path
281
+ path = path.rstrip("/")
282
+
283
+ if max_age := cookie["max-age"]:
284
+ try:
285
+ delta_seconds = int(max_age)
286
+ max_age_expiration = min(time.time() + delta_seconds, self.MAX_TIME)
287
+ self._expire_cookie(max_age_expiration, domain, path, name)
288
+ except ValueError:
289
+ cookie["max-age"] = ""
290
+
291
+ elif expires := cookie["expires"]:
292
+ if expire_time := self._parse_date(expires):
293
+ self._expire_cookie(expire_time, domain, path, name)
294
+ else:
295
+ cookie["expires"] = ""
296
+
297
+ key = (domain, path)
298
+ if self._cookies[key].get(name) != cookie:
299
+ # Don't blow away the cache if the same
300
+ # cookie gets set again
301
+ self._cookies[key][name] = cookie
302
+ self._morsel_cache[key].pop(name, None)
303
+
304
+ self._do_expiration()
305
+
306
+ def filter_cookies(self, request_url: URL = URL()) -> "BaseCookie[str]":
307
+ """Returns this jar's cookies filtered by their attributes."""
308
+ # We always use BaseCookie now since all
309
+ # cookies set on on filtered are fully constructed
310
+ # Morsels, not just names and values.
311
+ filtered: BaseCookie[str] = BaseCookie()
312
+ if not self._cookies:
313
+ # Skip do_expiration() if there are no cookies.
314
+ return filtered
315
+ self._do_expiration()
316
+ if not self._cookies:
317
+ # Skip rest of function if no non-expired cookies.
318
+ return filtered
319
+ if type(request_url) is not URL:
320
+ warnings.warn(
321
+ "filter_cookies expects yarl.URL instances only,"
322
+ f"and will stop working in 4.x, got {type(request_url)}",
323
+ DeprecationWarning,
324
+ stacklevel=2,
325
+ )
326
+ request_url = URL(request_url)
327
+ hostname = request_url.raw_host or ""
328
+
329
+ is_not_secure = request_url.scheme not in ("https", "wss")
330
+ if is_not_secure and self._treat_as_secure_origin:
331
+ request_origin = URL()
332
+ with contextlib.suppress(ValueError):
333
+ request_origin = request_url.origin()
334
+ is_not_secure = request_origin not in self._treat_as_secure_origin
335
+
336
+ # Send shared cookie
337
+ key = ("", "")
338
+ for c in self._cookies[key].values():
339
+ # Check cache first
340
+ if c.key in self._morsel_cache[key]:
341
+ filtered[c.key] = self._morsel_cache[key][c.key]
342
+ continue
343
+
344
+ # Build and cache the morsel
345
+ mrsl_val = self._build_morsel(c)
346
+ self._morsel_cache[key][c.key] = mrsl_val
347
+ filtered[c.key] = mrsl_val
348
+
349
+ if is_ip_address(hostname):
350
+ if not self._unsafe:
351
+ return filtered
352
+ domains: Iterable[str] = (hostname,)
353
+ else:
354
+ # Get all the subdomains that might match a cookie (e.g. "foo.bar.com", "bar.com", "com")
355
+ domains = itertools.accumulate(
356
+ reversed(hostname.split(".")), _FORMAT_DOMAIN_REVERSED
357
+ )
358
+
359
+ # Get all the path prefixes that might match a cookie (e.g. "", "/foo", "/foo/bar")
360
+ paths = itertools.accumulate(request_url.path.split("/"), _FORMAT_PATH)
361
+ # Create every combination of (domain, path) pairs.
362
+ pairs = itertools.product(domains, paths)
363
+
364
+ path_len = len(request_url.path)
365
+ # Point 2: https://www.rfc-editor.org/rfc/rfc6265.html#section-5.4
366
+ for p in pairs:
367
+ if p not in self._cookies:
368
+ continue
369
+ for name, cookie in self._cookies[p].items():
370
+ domain = cookie["domain"]
371
+
372
+ if (domain, name) in self._host_only_cookies and domain != hostname:
373
+ continue
374
+
375
+ # Skip edge case when the cookie has a trailing slash but request doesn't.
376
+ if len(cookie["path"]) > path_len:
377
+ continue
378
+
379
+ if is_not_secure and cookie["secure"]:
380
+ continue
381
+
382
+ # We already built the Morsel so reuse it here
383
+ if name in self._morsel_cache[p]:
384
+ filtered[name] = self._morsel_cache[p][name]
385
+ continue
386
+
387
+ # Build and cache the morsel
388
+ mrsl_val = self._build_morsel(cookie)
389
+ self._morsel_cache[p][name] = mrsl_val
390
+ filtered[name] = mrsl_val
391
+
392
+ return filtered
393
+
394
+ def _build_morsel(self, cookie: Morsel[str]) -> Morsel[str]:
395
+ """Build a morsel for sending, respecting quote_cookie setting."""
396
+ if self._quote_cookie and cookie.coded_value and cookie.coded_value[0] == '"':
397
+ return preserve_morsel_with_coded_value(cookie)
398
+ morsel: Morsel[str] = Morsel()
399
+ if self._quote_cookie:
400
+ value, coded_value = _SIMPLE_COOKIE.value_encode(cookie.value)
401
+ else:
402
+ coded_value = value = cookie.value
403
+ # We use __setstate__ instead of the public set() API because it allows us to
404
+ # bypass validation and set already validated state. This is more stable than
405
+ # setting protected attributes directly and unlikely to change since it would
406
+ # break pickling.
407
+ morsel.__setstate__({"key": cookie.key, "value": value, "coded_value": coded_value}) # type: ignore[attr-defined]
408
+ return morsel
409
+
410
+ @staticmethod
411
+ def _is_domain_match(domain: str, hostname: str) -> bool:
412
+ """Implements domain matching adhering to RFC 6265."""
413
+ if hostname == domain:
414
+ return True
415
+
416
+ if not hostname.endswith(domain):
417
+ return False
418
+
419
+ non_matching = hostname[: -len(domain)]
420
+
421
+ if not non_matching.endswith("."):
422
+ return False
423
+
424
+ return not is_ip_address(hostname)
425
+
426
+ @classmethod
427
+ def _parse_date(cls, date_str: str) -> Optional[int]:
428
+ """Implements date string parsing adhering to RFC 6265."""
429
+ if not date_str:
430
+ return None
431
+
432
+ found_time = False
433
+ found_day = False
434
+ found_month = False
435
+ found_year = False
436
+
437
+ hour = minute = second = 0
438
+ day = 0
439
+ month = 0
440
+ year = 0
441
+
442
+ for token_match in cls.DATE_TOKENS_RE.finditer(date_str):
443
+
444
+ token = token_match.group("token")
445
+
446
+ if not found_time:
447
+ time_match = cls.DATE_HMS_TIME_RE.match(token)
448
+ if time_match:
449
+ found_time = True
450
+ hour, minute, second = (int(s) for s in time_match.groups())
451
+ continue
452
+
453
+ if not found_day:
454
+ day_match = cls.DATE_DAY_OF_MONTH_RE.match(token)
455
+ if day_match:
456
+ found_day = True
457
+ day = int(day_match.group())
458
+ continue
459
+
460
+ if not found_month:
461
+ month_match = cls.DATE_MONTH_RE.match(token)
462
+ if month_match:
463
+ found_month = True
464
+ assert month_match.lastindex is not None
465
+ month = month_match.lastindex
466
+ continue
467
+
468
+ if not found_year:
469
+ year_match = cls.DATE_YEAR_RE.match(token)
470
+ if year_match:
471
+ found_year = True
472
+ year = int(year_match.group())
473
+
474
+ if 70 <= year <= 99:
475
+ year += 1900
476
+ elif 0 <= year <= 69:
477
+ year += 2000
478
+
479
+ if False in (found_day, found_month, found_year, found_time):
480
+ return None
481
+
482
+ if not 1 <= day <= 31:
483
+ return None
484
+
485
+ if year < 1601 or hour > 23 or minute > 59 or second > 59:
486
+ return None
487
+
488
+ return calendar.timegm((year, month, day, hour, minute, second, -1, -1, -1))
489
+
490
+
491
+ class DummyCookieJar(AbstractCookieJar):
492
+ """Implements a dummy cookie storage.
493
+
494
+ It can be used with the ClientSession when no cookie processing is needed.
495
+
496
+ """
497
+
498
+ def __init__(self, *, loop: Optional[asyncio.AbstractEventLoop] = None) -> None:
499
+ super().__init__(loop=loop)
500
+
501
+ def __iter__(self) -> "Iterator[Morsel[str]]":
502
+ while False:
503
+ yield None
504
+
505
+ def __len__(self) -> int:
506
+ return 0
507
+
508
+ @property
509
+ def quote_cookie(self) -> bool:
510
+ return True
511
+
512
+ def clear(self, predicate: Optional[ClearCookiePredicate] = None) -> None:
513
+ pass
514
+
515
+ def clear_domain(self, domain: str) -> None:
516
+ pass
517
+
518
+ def update_cookies(self, cookies: LooseCookies, response_url: URL = URL()) -> None:
519
+ pass
520
+
521
+ def filter_cookies(self, request_url: URL) -> "BaseCookie[str]":
522
+ return SimpleCookie()
aiohttp/formdata.py ADDED
@@ -0,0 +1,179 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import io
2
+ import warnings
3
+ from typing import Any, Iterable, List, Optional
4
+ from urllib.parse import urlencode
5
+
6
+ from multidict import MultiDict, MultiDictProxy
7
+
8
+ from . import hdrs, multipart, payload
9
+ from .helpers import guess_filename
10
+ from .payload import Payload
11
+
12
+ __all__ = ("FormData",)
13
+
14
+
15
+ class FormData:
16
+ """Helper class for form body generation.
17
+
18
+ Supports multipart/form-data and application/x-www-form-urlencoded.
19
+ """
20
+
21
+ def __init__(
22
+ self,
23
+ fields: Iterable[Any] = (),
24
+ quote_fields: bool = True,
25
+ charset: Optional[str] = None,
26
+ *,
27
+ default_to_multipart: bool = False,
28
+ ) -> None:
29
+ self._writer = multipart.MultipartWriter("form-data")
30
+ self._fields: List[Any] = []
31
+ self._is_multipart = default_to_multipart
32
+ self._quote_fields = quote_fields
33
+ self._charset = charset
34
+
35
+ if isinstance(fields, dict):
36
+ fields = list(fields.items())
37
+ elif not isinstance(fields, (list, tuple)):
38
+ fields = (fields,)
39
+ self.add_fields(*fields)
40
+
41
+ @property
42
+ def is_multipart(self) -> bool:
43
+ return self._is_multipart
44
+
45
+ def add_field(
46
+ self,
47
+ name: str,
48
+ value: Any,
49
+ *,
50
+ content_type: Optional[str] = None,
51
+ filename: Optional[str] = None,
52
+ content_transfer_encoding: Optional[str] = None,
53
+ ) -> None:
54
+
55
+ if isinstance(value, io.IOBase):
56
+ self._is_multipart = True
57
+ elif isinstance(value, (bytes, bytearray, memoryview)):
58
+ msg = (
59
+ "In v4, passing bytes will no longer create a file field. "
60
+ "Please explicitly use the filename parameter or pass a BytesIO object."
61
+ )
62
+ if filename is None and content_transfer_encoding is None:
63
+ warnings.warn(msg, DeprecationWarning)
64
+ filename = name
65
+
66
+ type_options: MultiDict[str] = MultiDict({"name": name})
67
+ if filename is not None and not isinstance(filename, str):
68
+ raise TypeError("filename must be an instance of str. Got: %s" % filename)
69
+ if filename is None and isinstance(value, io.IOBase):
70
+ filename = guess_filename(value, name)
71
+ if filename is not None:
72
+ type_options["filename"] = filename
73
+ self._is_multipart = True
74
+
75
+ headers = {}
76
+ if content_type is not None:
77
+ if not isinstance(content_type, str):
78
+ raise TypeError(
79
+ "content_type must be an instance of str. Got: %s" % content_type
80
+ )
81
+ headers[hdrs.CONTENT_TYPE] = content_type
82
+ self._is_multipart = True
83
+ if content_transfer_encoding is not None:
84
+ if not isinstance(content_transfer_encoding, str):
85
+ raise TypeError(
86
+ "content_transfer_encoding must be an instance"
87
+ " of str. Got: %s" % content_transfer_encoding
88
+ )
89
+ msg = (
90
+ "content_transfer_encoding is deprecated. "
91
+ "To maintain compatibility with v4 please pass a BytesPayload."
92
+ )
93
+ warnings.warn(msg, DeprecationWarning)
94
+ self._is_multipart = True
95
+
96
+ self._fields.append((type_options, headers, value))
97
+
98
+ def add_fields(self, *fields: Any) -> None:
99
+ to_add = list(fields)
100
+
101
+ while to_add:
102
+ rec = to_add.pop(0)
103
+
104
+ if isinstance(rec, io.IOBase):
105
+ k = guess_filename(rec, "unknown")
106
+ self.add_field(k, rec) # type: ignore[arg-type]
107
+
108
+ elif isinstance(rec, (MultiDictProxy, MultiDict)):
109
+ to_add.extend(rec.items())
110
+
111
+ elif isinstance(rec, (list, tuple)) and len(rec) == 2:
112
+ k, fp = rec
113
+ self.add_field(k, fp)
114
+
115
+ else:
116
+ raise TypeError(
117
+ "Only io.IOBase, multidict and (name, file) "
118
+ "pairs allowed, use .add_field() for passing "
119
+ "more complex parameters, got {!r}".format(rec)
120
+ )
121
+
122
+ def _gen_form_urlencoded(self) -> payload.BytesPayload:
123
+ # form data (x-www-form-urlencoded)
124
+ data = []
125
+ for type_options, _, value in self._fields:
126
+ data.append((type_options["name"], value))
127
+
128
+ charset = self._charset if self._charset is not None else "utf-8"
129
+
130
+ if charset == "utf-8":
131
+ content_type = "application/x-www-form-urlencoded"
132
+ else:
133
+ content_type = "application/x-www-form-urlencoded; charset=%s" % charset
134
+
135
+ return payload.BytesPayload(
136
+ urlencode(data, doseq=True, encoding=charset).encode(),
137
+ content_type=content_type,
138
+ )
139
+
140
+ def _gen_form_data(self) -> multipart.MultipartWriter:
141
+ """Encode a list of fields using the multipart/form-data MIME format"""
142
+ for dispparams, headers, value in self._fields:
143
+ try:
144
+ if hdrs.CONTENT_TYPE in headers:
145
+ part = payload.get_payload(
146
+ value,
147
+ content_type=headers[hdrs.CONTENT_TYPE],
148
+ headers=headers,
149
+ encoding=self._charset,
150
+ )
151
+ else:
152
+ part = payload.get_payload(
153
+ value, headers=headers, encoding=self._charset
154
+ )
155
+ except Exception as exc:
156
+ raise TypeError(
157
+ "Can not serialize value type: %r\n "
158
+ "headers: %r\n value: %r" % (type(value), headers, value)
159
+ ) from exc
160
+
161
+ if dispparams:
162
+ part.set_content_disposition(
163
+ "form-data", quote_fields=self._quote_fields, **dispparams
164
+ )
165
+ # FIXME cgi.FieldStorage doesn't likes body parts with
166
+ # Content-Length which were sent via chunked transfer encoding
167
+ assert part.headers is not None
168
+ part.headers.popall(hdrs.CONTENT_LENGTH, None)
169
+
170
+ self._writer.append_payload(part)
171
+
172
+ self._fields.clear()
173
+ return self._writer
174
+
175
+ def __call__(self) -> Payload:
176
+ if self._is_multipart:
177
+ return self._gen_form_data()
178
+ else:
179
+ return self._gen_form_urlencoded()
aiohttp/hdrs.py ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """HTTP Headers constants."""
2
+
3
+ # After changing the file content call ./tools/gen.py
4
+ # to regenerate the headers parser
5
+ import itertools
6
+ from typing import Final, Set
7
+
8
+ from multidict import istr
9
+
10
+ METH_ANY: Final[str] = "*"
11
+ METH_CONNECT: Final[str] = "CONNECT"
12
+ METH_HEAD: Final[str] = "HEAD"
13
+ METH_GET: Final[str] = "GET"
14
+ METH_DELETE: Final[str] = "DELETE"
15
+ METH_OPTIONS: Final[str] = "OPTIONS"
16
+ METH_PATCH: Final[str] = "PATCH"
17
+ METH_POST: Final[str] = "POST"
18
+ METH_PUT: Final[str] = "PUT"
19
+ METH_TRACE: Final[str] = "TRACE"
20
+
21
+ METH_ALL: Final[Set[str]] = {
22
+ METH_CONNECT,
23
+ METH_HEAD,
24
+ METH_GET,
25
+ METH_DELETE,
26
+ METH_OPTIONS,
27
+ METH_PATCH,
28
+ METH_POST,
29
+ METH_PUT,
30
+ METH_TRACE,
31
+ }
32
+
33
+ ACCEPT: Final[istr] = istr("Accept")
34
+ ACCEPT_CHARSET: Final[istr] = istr("Accept-Charset")
35
+ ACCEPT_ENCODING: Final[istr] = istr("Accept-Encoding")
36
+ ACCEPT_LANGUAGE: Final[istr] = istr("Accept-Language")
37
+ ACCEPT_RANGES: Final[istr] = istr("Accept-Ranges")
38
+ ACCESS_CONTROL_MAX_AGE: Final[istr] = istr("Access-Control-Max-Age")
39
+ ACCESS_CONTROL_ALLOW_CREDENTIALS: Final[istr] = istr("Access-Control-Allow-Credentials")
40
+ ACCESS_CONTROL_ALLOW_HEADERS: Final[istr] = istr("Access-Control-Allow-Headers")
41
+ ACCESS_CONTROL_ALLOW_METHODS: Final[istr] = istr("Access-Control-Allow-Methods")
42
+ ACCESS_CONTROL_ALLOW_ORIGIN: Final[istr] = istr("Access-Control-Allow-Origin")
43
+ ACCESS_CONTROL_EXPOSE_HEADERS: Final[istr] = istr("Access-Control-Expose-Headers")
44
+ ACCESS_CONTROL_REQUEST_HEADERS: Final[istr] = istr("Access-Control-Request-Headers")
45
+ ACCESS_CONTROL_REQUEST_METHOD: Final[istr] = istr("Access-Control-Request-Method")
46
+ AGE: Final[istr] = istr("Age")
47
+ ALLOW: Final[istr] = istr("Allow")
48
+ AUTHORIZATION: Final[istr] = istr("Authorization")
49
+ CACHE_CONTROL: Final[istr] = istr("Cache-Control")
50
+ CONNECTION: Final[istr] = istr("Connection")
51
+ CONTENT_DISPOSITION: Final[istr] = istr("Content-Disposition")
52
+ CONTENT_ENCODING: Final[istr] = istr("Content-Encoding")
53
+ CONTENT_LANGUAGE: Final[istr] = istr("Content-Language")
54
+ CONTENT_LENGTH: Final[istr] = istr("Content-Length")
55
+ CONTENT_LOCATION: Final[istr] = istr("Content-Location")
56
+ CONTENT_MD5: Final[istr] = istr("Content-MD5")
57
+ CONTENT_RANGE: Final[istr] = istr("Content-Range")
58
+ CONTENT_TRANSFER_ENCODING: Final[istr] = istr("Content-Transfer-Encoding")
59
+ CONTENT_TYPE: Final[istr] = istr("Content-Type")
60
+ COOKIE: Final[istr] = istr("Cookie")
61
+ DATE: Final[istr] = istr("Date")
62
+ DESTINATION: Final[istr] = istr("Destination")
63
+ DIGEST: Final[istr] = istr("Digest")
64
+ ETAG: Final[istr] = istr("Etag")
65
+ EXPECT: Final[istr] = istr("Expect")
66
+ EXPIRES: Final[istr] = istr("Expires")
67
+ FORWARDED: Final[istr] = istr("Forwarded")
68
+ FROM: Final[istr] = istr("From")
69
+ HOST: Final[istr] = istr("Host")
70
+ IF_MATCH: Final[istr] = istr("If-Match")
71
+ IF_MODIFIED_SINCE: Final[istr] = istr("If-Modified-Since")
72
+ IF_NONE_MATCH: Final[istr] = istr("If-None-Match")
73
+ IF_RANGE: Final[istr] = istr("If-Range")
74
+ IF_UNMODIFIED_SINCE: Final[istr] = istr("If-Unmodified-Since")
75
+ KEEP_ALIVE: Final[istr] = istr("Keep-Alive")
76
+ LAST_EVENT_ID: Final[istr] = istr("Last-Event-ID")
77
+ LAST_MODIFIED: Final[istr] = istr("Last-Modified")
78
+ LINK: Final[istr] = istr("Link")
79
+ LOCATION: Final[istr] = istr("Location")
80
+ MAX_FORWARDS: Final[istr] = istr("Max-Forwards")
81
+ ORIGIN: Final[istr] = istr("Origin")
82
+ PRAGMA: Final[istr] = istr("Pragma")
83
+ PROXY_AUTHENTICATE: Final[istr] = istr("Proxy-Authenticate")
84
+ PROXY_AUTHORIZATION: Final[istr] = istr("Proxy-Authorization")
85
+ RANGE: Final[istr] = istr("Range")
86
+ REFERER: Final[istr] = istr("Referer")
87
+ RETRY_AFTER: Final[istr] = istr("Retry-After")
88
+ SEC_WEBSOCKET_ACCEPT: Final[istr] = istr("Sec-WebSocket-Accept")
89
+ SEC_WEBSOCKET_VERSION: Final[istr] = istr("Sec-WebSocket-Version")
90
+ SEC_WEBSOCKET_PROTOCOL: Final[istr] = istr("Sec-WebSocket-Protocol")
91
+ SEC_WEBSOCKET_EXTENSIONS: Final[istr] = istr("Sec-WebSocket-Extensions")
92
+ SEC_WEBSOCKET_KEY: Final[istr] = istr("Sec-WebSocket-Key")
93
+ SEC_WEBSOCKET_KEY1: Final[istr] = istr("Sec-WebSocket-Key1")
94
+ SERVER: Final[istr] = istr("Server")
95
+ SET_COOKIE: Final[istr] = istr("Set-Cookie")
96
+ TE: Final[istr] = istr("TE")
97
+ TRAILER: Final[istr] = istr("Trailer")
98
+ TRANSFER_ENCODING: Final[istr] = istr("Transfer-Encoding")
99
+ UPGRADE: Final[istr] = istr("Upgrade")
100
+ URI: Final[istr] = istr("URI")
101
+ USER_AGENT: Final[istr] = istr("User-Agent")
102
+ VARY: Final[istr] = istr("Vary")
103
+ VIA: Final[istr] = istr("Via")
104
+ WANT_DIGEST: Final[istr] = istr("Want-Digest")
105
+ WARNING: Final[istr] = istr("Warning")
106
+ WWW_AUTHENTICATE: Final[istr] = istr("WWW-Authenticate")
107
+ X_FORWARDED_FOR: Final[istr] = istr("X-Forwarded-For")
108
+ X_FORWARDED_HOST: Final[istr] = istr("X-Forwarded-Host")
109
+ X_FORWARDED_PROTO: Final[istr] = istr("X-Forwarded-Proto")
110
+
111
+ # These are the upper/lower case variants of the headers/methods
112
+ # Example: {'hOst', 'host', 'HoST', 'HOSt', 'hOsT', 'HosT', 'hoSt', ...}
113
+ METH_HEAD_ALL: Final = frozenset(
114
+ map("".join, itertools.product(*zip(METH_HEAD.upper(), METH_HEAD.lower())))
115
+ )
116
+ METH_CONNECT_ALL: Final = frozenset(
117
+ map("".join, itertools.product(*zip(METH_CONNECT.upper(), METH_CONNECT.lower())))
118
+ )
119
+ HOST_ALL: Final = frozenset(
120
+ map("".join, itertools.product(*zip(HOST.upper(), HOST.lower())))
121
+ )
aiohttp/helpers.py ADDED
@@ -0,0 +1,986 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Various helper functions"""
2
+
3
+ import asyncio
4
+ import base64
5
+ import binascii
6
+ import contextlib
7
+ import datetime
8
+ import enum
9
+ import functools
10
+ import inspect
11
+ import netrc
12
+ import os
13
+ import platform
14
+ import re
15
+ import sys
16
+ import time
17
+ import weakref
18
+ from collections import namedtuple
19
+ from contextlib import suppress
20
+ from email.message import EmailMessage
21
+ from email.parser import HeaderParser
22
+ from email.policy import HTTP
23
+ from email.utils import parsedate
24
+ from math import ceil
25
+ from pathlib import Path
26
+ from types import MappingProxyType, TracebackType
27
+ from typing import (
28
+ Any,
29
+ Callable,
30
+ ContextManager,
31
+ Dict,
32
+ Generator,
33
+ Generic,
34
+ Iterable,
35
+ Iterator,
36
+ List,
37
+ Mapping,
38
+ Optional,
39
+ Protocol,
40
+ Tuple,
41
+ Type,
42
+ TypeVar,
43
+ Union,
44
+ get_args,
45
+ overload,
46
+ )
47
+ from urllib.parse import quote
48
+ from urllib.request import getproxies, proxy_bypass
49
+
50
+ import attr
51
+ from multidict import MultiDict, MultiDictProxy, MultiMapping
52
+ from propcache.api import under_cached_property as reify
53
+ from yarl import URL
54
+
55
+ from . import hdrs
56
+ from .log import client_logger
57
+
58
+ if sys.version_info >= (3, 11):
59
+ import asyncio as async_timeout
60
+ else:
61
+ import async_timeout
62
+
63
+ __all__ = ("BasicAuth", "ChainMapProxy", "ETag", "reify")
64
+
65
+ IS_MACOS = platform.system() == "Darwin"
66
+ IS_WINDOWS = platform.system() == "Windows"
67
+
68
+ PY_310 = sys.version_info >= (3, 10)
69
+ PY_311 = sys.version_info >= (3, 11)
70
+
71
+
72
+ _T = TypeVar("_T")
73
+ _S = TypeVar("_S")
74
+
75
+ _SENTINEL = enum.Enum("_SENTINEL", "sentinel")
76
+ sentinel = _SENTINEL.sentinel
77
+
78
+ NO_EXTENSIONS = bool(os.environ.get("AIOHTTP_NO_EXTENSIONS"))
79
+
80
+ # https://datatracker.ietf.org/doc/html/rfc9112#section-6.3-2.1
81
+ EMPTY_BODY_STATUS_CODES = frozenset((204, 304, *range(100, 200)))
82
+ # https://datatracker.ietf.org/doc/html/rfc9112#section-6.3-2.1
83
+ # https://datatracker.ietf.org/doc/html/rfc9112#section-6.3-2.2
84
+ EMPTY_BODY_METHODS = hdrs.METH_HEAD_ALL
85
+
86
+ DEBUG = sys.flags.dev_mode or (
87
+ not sys.flags.ignore_environment and bool(os.environ.get("PYTHONASYNCIODEBUG"))
88
+ )
89
+
90
+
91
+ CHAR = {chr(i) for i in range(0, 128)}
92
+ CTL = {chr(i) for i in range(0, 32)} | {
93
+ chr(127),
94
+ }
95
+ SEPARATORS = {
96
+ "(",
97
+ ")",
98
+ "<",
99
+ ">",
100
+ "@",
101
+ ",",
102
+ ";",
103
+ ":",
104
+ "\\",
105
+ '"',
106
+ "/",
107
+ "[",
108
+ "]",
109
+ "?",
110
+ "=",
111
+ "{",
112
+ "}",
113
+ " ",
114
+ chr(9),
115
+ }
116
+ TOKEN = CHAR ^ CTL ^ SEPARATORS
117
+
118
+
119
+ class noop:
120
+ def __await__(self) -> Generator[None, None, None]:
121
+ yield
122
+
123
+
124
+ class BasicAuth(namedtuple("BasicAuth", ["login", "password", "encoding"])):
125
+ """Http basic authentication helper."""
126
+
127
+ def __new__(
128
+ cls, login: str, password: str = "", encoding: str = "latin1"
129
+ ) -> "BasicAuth":
130
+ if login is None:
131
+ raise ValueError("None is not allowed as login value")
132
+
133
+ if password is None:
134
+ raise ValueError("None is not allowed as password value")
135
+
136
+ if ":" in login:
137
+ raise ValueError('A ":" is not allowed in login (RFC 1945#section-11.1)')
138
+
139
+ return super().__new__(cls, login, password, encoding)
140
+
141
+ @classmethod
142
+ def decode(cls, auth_header: str, encoding: str = "latin1") -> "BasicAuth":
143
+ """Create a BasicAuth object from an Authorization HTTP header."""
144
+ try:
145
+ auth_type, encoded_credentials = auth_header.split(" ", 1)
146
+ except ValueError:
147
+ raise ValueError("Could not parse authorization header.")
148
+
149
+ if auth_type.lower() != "basic":
150
+ raise ValueError("Unknown authorization method %s" % auth_type)
151
+
152
+ try:
153
+ decoded = base64.b64decode(
154
+ encoded_credentials.encode("ascii"), validate=True
155
+ ).decode(encoding)
156
+ except binascii.Error:
157
+ raise ValueError("Invalid base64 encoding.")
158
+
159
+ try:
160
+ # RFC 2617 HTTP Authentication
161
+ # https://www.ietf.org/rfc/rfc2617.txt
162
+ # the colon must be present, but the username and password may be
163
+ # otherwise blank.
164
+ username, password = decoded.split(":", 1)
165
+ except ValueError:
166
+ raise ValueError("Invalid credentials.")
167
+
168
+ return cls(username, password, encoding=encoding)
169
+
170
+ @classmethod
171
+ def from_url(cls, url: URL, *, encoding: str = "latin1") -> Optional["BasicAuth"]:
172
+ """Create BasicAuth from url."""
173
+ if not isinstance(url, URL):
174
+ raise TypeError("url should be yarl.URL instance")
175
+ # Check raw_user and raw_password first as yarl is likely
176
+ # to already have these values parsed from the netloc in the cache.
177
+ if url.raw_user is None and url.raw_password is None:
178
+ return None
179
+ return cls(url.user or "", url.password or "", encoding=encoding)
180
+
181
+ def encode(self) -> str:
182
+ """Encode credentials."""
183
+ creds = (f"{self.login}:{self.password}").encode(self.encoding)
184
+ return "Basic %s" % base64.b64encode(creds).decode(self.encoding)
185
+
186
+
187
+ def strip_auth_from_url(url: URL) -> Tuple[URL, Optional[BasicAuth]]:
188
+ """Remove user and password from URL if present and return BasicAuth object."""
189
+ # Check raw_user and raw_password first as yarl is likely
190
+ # to already have these values parsed from the netloc in the cache.
191
+ if url.raw_user is None and url.raw_password is None:
192
+ return url, None
193
+ return url.with_user(None), BasicAuth(url.user or "", url.password or "")
194
+
195
+
196
+ def netrc_from_env() -> Optional[netrc.netrc]:
197
+ """Load netrc from file.
198
+
199
+ Attempt to load it from the path specified by the env-var
200
+ NETRC or in the default location in the user's home directory.
201
+
202
+ Returns None if it couldn't be found or fails to parse.
203
+ """
204
+ netrc_env = os.environ.get("NETRC")
205
+
206
+ if netrc_env is not None:
207
+ netrc_path = Path(netrc_env)
208
+ else:
209
+ try:
210
+ home_dir = Path.home()
211
+ except RuntimeError as e: # pragma: no cover
212
+ # if pathlib can't resolve home, it may raise a RuntimeError
213
+ client_logger.debug(
214
+ "Could not resolve home directory when "
215
+ "trying to look for .netrc file: %s",
216
+ e,
217
+ )
218
+ return None
219
+
220
+ netrc_path = home_dir / ("_netrc" if IS_WINDOWS else ".netrc")
221
+
222
+ try:
223
+ return netrc.netrc(str(netrc_path))
224
+ except netrc.NetrcParseError as e:
225
+ client_logger.warning("Could not parse .netrc file: %s", e)
226
+ except OSError as e:
227
+ netrc_exists = False
228
+ with contextlib.suppress(OSError):
229
+ netrc_exists = netrc_path.is_file()
230
+ # we couldn't read the file (doesn't exist, permissions, etc.)
231
+ if netrc_env or netrc_exists:
232
+ # only warn if the environment wanted us to load it,
233
+ # or it appears like the default file does actually exist
234
+ client_logger.warning("Could not read .netrc file: %s", e)
235
+
236
+ return None
237
+
238
+
239
+ @attr.s(auto_attribs=True, frozen=True, slots=True)
240
+ class ProxyInfo:
241
+ proxy: URL
242
+ proxy_auth: Optional[BasicAuth]
243
+
244
+
245
+ def basicauth_from_netrc(netrc_obj: Optional[netrc.netrc], host: str) -> BasicAuth:
246
+ """
247
+ Return :py:class:`~aiohttp.BasicAuth` credentials for ``host`` from ``netrc_obj``.
248
+
249
+ :raises LookupError: if ``netrc_obj`` is :py:data:`None` or if no
250
+ entry is found for the ``host``.
251
+ """
252
+ if netrc_obj is None:
253
+ raise LookupError("No .netrc file found")
254
+ auth_from_netrc = netrc_obj.authenticators(host)
255
+
256
+ if auth_from_netrc is None:
257
+ raise LookupError(f"No entry for {host!s} found in the `.netrc` file.")
258
+ login, account, password = auth_from_netrc
259
+
260
+ # TODO(PY311): username = login or account
261
+ # Up to python 3.10, account could be None if not specified,
262
+ # and login will be empty string if not specified. From 3.11,
263
+ # login and account will be empty string if not specified.
264
+ username = login if (login or account is None) else account
265
+
266
+ # TODO(PY311): Remove this, as password will be empty string
267
+ # if not specified
268
+ if password is None:
269
+ password = ""
270
+
271
+ return BasicAuth(username, password)
272
+
273
+
274
+ def proxies_from_env() -> Dict[str, ProxyInfo]:
275
+ proxy_urls = {
276
+ k: URL(v)
277
+ for k, v in getproxies().items()
278
+ if k in ("http", "https", "ws", "wss")
279
+ }
280
+ netrc_obj = netrc_from_env()
281
+ stripped = {k: strip_auth_from_url(v) for k, v in proxy_urls.items()}
282
+ ret = {}
283
+ for proto, val in stripped.items():
284
+ proxy, auth = val
285
+ if proxy.scheme in ("https", "wss"):
286
+ client_logger.warning(
287
+ "%s proxies %s are not supported, ignoring", proxy.scheme.upper(), proxy
288
+ )
289
+ continue
290
+ if netrc_obj and auth is None:
291
+ if proxy.host is not None:
292
+ try:
293
+ auth = basicauth_from_netrc(netrc_obj, proxy.host)
294
+ except LookupError:
295
+ auth = None
296
+ ret[proto] = ProxyInfo(proxy, auth)
297
+ return ret
298
+
299
+
300
+ def get_env_proxy_for_url(url: URL) -> Tuple[URL, Optional[BasicAuth]]:
301
+ """Get a permitted proxy for the given URL from the env."""
302
+ if url.host is not None and proxy_bypass(url.host):
303
+ raise LookupError(f"Proxying is disallowed for `{url.host!r}`")
304
+
305
+ proxies_in_env = proxies_from_env()
306
+ try:
307
+ proxy_info = proxies_in_env[url.scheme]
308
+ except KeyError:
309
+ raise LookupError(f"No proxies found for `{url!s}` in the env")
310
+ else:
311
+ return proxy_info.proxy, proxy_info.proxy_auth
312
+
313
+
314
+ @attr.s(auto_attribs=True, frozen=True, slots=True)
315
+ class MimeType:
316
+ type: str
317
+ subtype: str
318
+ suffix: str
319
+ parameters: "MultiDictProxy[str]"
320
+
321
+
322
+ @functools.lru_cache(maxsize=56)
323
+ def parse_mimetype(mimetype: str) -> MimeType:
324
+ """Parses a MIME type into its components.
325
+
326
+ mimetype is a MIME type string.
327
+
328
+ Returns a MimeType object.
329
+
330
+ Example:
331
+
332
+ >>> parse_mimetype('text/html; charset=utf-8')
333
+ MimeType(type='text', subtype='html', suffix='',
334
+ parameters={'charset': 'utf-8'})
335
+
336
+ """
337
+ if not mimetype:
338
+ return MimeType(
339
+ type="", subtype="", suffix="", parameters=MultiDictProxy(MultiDict())
340
+ )
341
+
342
+ parts = mimetype.split(";")
343
+ params: MultiDict[str] = MultiDict()
344
+ for item in parts[1:]:
345
+ if not item:
346
+ continue
347
+ key, _, value = item.partition("=")
348
+ params.add(key.lower().strip(), value.strip(' "'))
349
+
350
+ fulltype = parts[0].strip().lower()
351
+ if fulltype == "*":
352
+ fulltype = "*/*"
353
+
354
+ mtype, _, stype = fulltype.partition("/")
355
+ stype, _, suffix = stype.partition("+")
356
+
357
+ return MimeType(
358
+ type=mtype, subtype=stype, suffix=suffix, parameters=MultiDictProxy(params)
359
+ )
360
+
361
+
362
+ class EnsureOctetStream(EmailMessage):
363
+ def __init__(self) -> None:
364
+ super().__init__()
365
+ # https://www.rfc-editor.org/rfc/rfc9110#section-8.3-5
366
+ self.set_default_type("application/octet-stream")
367
+
368
+ def get_content_type(self) -> str:
369
+ """Re-implementation from Message
370
+
371
+ Returns application/octet-stream in place of plain/text when
372
+ value is wrong.
373
+
374
+ The way this class is used guarantees that content-type will
375
+ be present so simplify the checks wrt to the base implementation.
376
+ """
377
+ value = self.get("content-type", "").lower()
378
+
379
+ # Based on the implementation of _splitparam in the standard library
380
+ ctype, _, _ = value.partition(";")
381
+ ctype = ctype.strip()
382
+ if ctype.count("/") != 1:
383
+ return self.get_default_type()
384
+ return ctype
385
+
386
+
387
+ @functools.lru_cache(maxsize=56)
388
+ def parse_content_type(raw: str) -> Tuple[str, MappingProxyType[str, str]]:
389
+ """Parse Content-Type header.
390
+
391
+ Returns a tuple of the parsed content type and a
392
+ MappingProxyType of parameters. The default returned value
393
+ is `application/octet-stream`
394
+ """
395
+ msg = HeaderParser(EnsureOctetStream, policy=HTTP).parsestr(f"Content-Type: {raw}")
396
+ content_type = msg.get_content_type()
397
+ params = msg.get_params(())
398
+ content_dict = dict(params[1:]) # First element is content type again
399
+ return content_type, MappingProxyType(content_dict)
400
+
401
+
402
+ def guess_filename(obj: Any, default: Optional[str] = None) -> Optional[str]:
403
+ name = getattr(obj, "name", None)
404
+ if name and isinstance(name, str) and name[0] != "<" and name[-1] != ">":
405
+ return Path(name).name
406
+ return default
407
+
408
+
409
+ not_qtext_re = re.compile(r"[^\041\043-\133\135-\176]")
410
+ QCONTENT = {chr(i) for i in range(0x20, 0x7F)} | {"\t"}
411
+
412
+
413
+ def quoted_string(content: str) -> str:
414
+ """Return 7-bit content as quoted-string.
415
+
416
+ Format content into a quoted-string as defined in RFC5322 for
417
+ Internet Message Format. Notice that this is not the 8-bit HTTP
418
+ format, but the 7-bit email format. Content must be in usascii or
419
+ a ValueError is raised.
420
+ """
421
+ if not (QCONTENT > set(content)):
422
+ raise ValueError(f"bad content for quoted-string {content!r}")
423
+ return not_qtext_re.sub(lambda x: "\\" + x.group(0), content)
424
+
425
+
426
+ def content_disposition_header(
427
+ disptype: str, quote_fields: bool = True, _charset: str = "utf-8", **params: str
428
+ ) -> str:
429
+ """Sets ``Content-Disposition`` header for MIME.
430
+
431
+ This is the MIME payload Content-Disposition header from RFC 2183
432
+ and RFC 7579 section 4.2, not the HTTP Content-Disposition from
433
+ RFC 6266.
434
+
435
+ disptype is a disposition type: inline, attachment, form-data.
436
+ Should be valid extension token (see RFC 2183)
437
+
438
+ quote_fields performs value quoting to 7-bit MIME headers
439
+ according to RFC 7578. Set to quote_fields to False if recipient
440
+ can take 8-bit file names and field values.
441
+
442
+ _charset specifies the charset to use when quote_fields is True.
443
+
444
+ params is a dict with disposition params.
445
+ """
446
+ if not disptype or not (TOKEN > set(disptype)):
447
+ raise ValueError(f"bad content disposition type {disptype!r}")
448
+
449
+ value = disptype
450
+ if params:
451
+ lparams = []
452
+ for key, val in params.items():
453
+ if not key or not (TOKEN > set(key)):
454
+ raise ValueError(f"bad content disposition parameter {key!r}={val!r}")
455
+ if quote_fields:
456
+ if key.lower() == "filename":
457
+ qval = quote(val, "", encoding=_charset)
458
+ lparams.append((key, '"%s"' % qval))
459
+ else:
460
+ try:
461
+ qval = quoted_string(val)
462
+ except ValueError:
463
+ qval = "".join(
464
+ (_charset, "''", quote(val, "", encoding=_charset))
465
+ )
466
+ lparams.append((key + "*", qval))
467
+ else:
468
+ lparams.append((key, '"%s"' % qval))
469
+ else:
470
+ qval = val.replace("\\", "\\\\").replace('"', '\\"')
471
+ lparams.append((key, '"%s"' % qval))
472
+ sparams = "; ".join("=".join(pair) for pair in lparams)
473
+ value = "; ".join((value, sparams))
474
+ return value
475
+
476
+
477
+ def is_ip_address(host: Optional[str]) -> bool:
478
+ """Check if host looks like an IP Address.
479
+
480
+ This check is only meant as a heuristic to ensure that
481
+ a host is not a domain name.
482
+ """
483
+ if not host:
484
+ return False
485
+ # For a host to be an ipv4 address, it must be all numeric.
486
+ # The host must contain a colon to be an IPv6 address.
487
+ return ":" in host or host.replace(".", "").isdigit()
488
+
489
+
490
+ _cached_current_datetime: Optional[int] = None
491
+ _cached_formatted_datetime = ""
492
+
493
+
494
+ def rfc822_formatted_time() -> str:
495
+ global _cached_current_datetime
496
+ global _cached_formatted_datetime
497
+
498
+ now = int(time.time())
499
+ if now != _cached_current_datetime:
500
+ # Weekday and month names for HTTP date/time formatting;
501
+ # always English!
502
+ # Tuples are constants stored in codeobject!
503
+ _weekdayname = ("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun")
504
+ _monthname = (
505
+ "", # Dummy so we can use 1-based month numbers
506
+ "Jan",
507
+ "Feb",
508
+ "Mar",
509
+ "Apr",
510
+ "May",
511
+ "Jun",
512
+ "Jul",
513
+ "Aug",
514
+ "Sep",
515
+ "Oct",
516
+ "Nov",
517
+ "Dec",
518
+ )
519
+
520
+ year, month, day, hh, mm, ss, wd, *tail = time.gmtime(now)
521
+ _cached_formatted_datetime = "%s, %02d %3s %4d %02d:%02d:%02d GMT" % (
522
+ _weekdayname[wd],
523
+ day,
524
+ _monthname[month],
525
+ year,
526
+ hh,
527
+ mm,
528
+ ss,
529
+ )
530
+ _cached_current_datetime = now
531
+ return _cached_formatted_datetime
532
+
533
+
534
+ def _weakref_handle(info: "Tuple[weakref.ref[object], str]") -> None:
535
+ ref, name = info
536
+ ob = ref()
537
+ if ob is not None:
538
+ with suppress(Exception):
539
+ getattr(ob, name)()
540
+
541
+
542
+ def weakref_handle(
543
+ ob: object,
544
+ name: str,
545
+ timeout: float,
546
+ loop: asyncio.AbstractEventLoop,
547
+ timeout_ceil_threshold: float = 5,
548
+ ) -> Optional[asyncio.TimerHandle]:
549
+ if timeout is not None and timeout > 0:
550
+ when = loop.time() + timeout
551
+ if timeout >= timeout_ceil_threshold:
552
+ when = ceil(when)
553
+
554
+ return loop.call_at(when, _weakref_handle, (weakref.ref(ob), name))
555
+ return None
556
+
557
+
558
+ def call_later(
559
+ cb: Callable[[], Any],
560
+ timeout: float,
561
+ loop: asyncio.AbstractEventLoop,
562
+ timeout_ceil_threshold: float = 5,
563
+ ) -> Optional[asyncio.TimerHandle]:
564
+ if timeout is None or timeout <= 0:
565
+ return None
566
+ now = loop.time()
567
+ when = calculate_timeout_when(now, timeout, timeout_ceil_threshold)
568
+ return loop.call_at(when, cb)
569
+
570
+
571
+ def calculate_timeout_when(
572
+ loop_time: float,
573
+ timeout: float,
574
+ timeout_ceiling_threshold: float,
575
+ ) -> float:
576
+ """Calculate when to execute a timeout."""
577
+ when = loop_time + timeout
578
+ if timeout > timeout_ceiling_threshold:
579
+ return ceil(when)
580
+ return when
581
+
582
+
583
+ class TimeoutHandle:
584
+ """Timeout handle"""
585
+
586
+ __slots__ = ("_timeout", "_loop", "_ceil_threshold", "_callbacks")
587
+
588
+ def __init__(
589
+ self,
590
+ loop: asyncio.AbstractEventLoop,
591
+ timeout: Optional[float],
592
+ ceil_threshold: float = 5,
593
+ ) -> None:
594
+ self._timeout = timeout
595
+ self._loop = loop
596
+ self._ceil_threshold = ceil_threshold
597
+ self._callbacks: List[
598
+ Tuple[Callable[..., None], Tuple[Any, ...], Dict[str, Any]]
599
+ ] = []
600
+
601
+ def register(
602
+ self, callback: Callable[..., None], *args: Any, **kwargs: Any
603
+ ) -> None:
604
+ self._callbacks.append((callback, args, kwargs))
605
+
606
+ def close(self) -> None:
607
+ self._callbacks.clear()
608
+
609
+ def start(self) -> Optional[asyncio.TimerHandle]:
610
+ timeout = self._timeout
611
+ if timeout is not None and timeout > 0:
612
+ when = self._loop.time() + timeout
613
+ if timeout >= self._ceil_threshold:
614
+ when = ceil(when)
615
+ return self._loop.call_at(when, self.__call__)
616
+ else:
617
+ return None
618
+
619
+ def timer(self) -> "BaseTimerContext":
620
+ if self._timeout is not None and self._timeout > 0:
621
+ timer = TimerContext(self._loop)
622
+ self.register(timer.timeout)
623
+ return timer
624
+ else:
625
+ return TimerNoop()
626
+
627
+ def __call__(self) -> None:
628
+ for cb, args, kwargs in self._callbacks:
629
+ with suppress(Exception):
630
+ cb(*args, **kwargs)
631
+
632
+ self._callbacks.clear()
633
+
634
+
635
+ class BaseTimerContext(ContextManager["BaseTimerContext"]):
636
+
637
+ __slots__ = ()
638
+
639
+ def assert_timeout(self) -> None:
640
+ """Raise TimeoutError if timeout has been exceeded."""
641
+
642
+
643
+ class TimerNoop(BaseTimerContext):
644
+
645
+ __slots__ = ()
646
+
647
+ def __enter__(self) -> BaseTimerContext:
648
+ return self
649
+
650
+ def __exit__(
651
+ self,
652
+ exc_type: Optional[Type[BaseException]],
653
+ exc_val: Optional[BaseException],
654
+ exc_tb: Optional[TracebackType],
655
+ ) -> None:
656
+ return
657
+
658
+
659
+ class TimerContext(BaseTimerContext):
660
+ """Low resolution timeout context manager"""
661
+
662
+ __slots__ = ("_loop", "_tasks", "_cancelled", "_cancelling")
663
+
664
+ def __init__(self, loop: asyncio.AbstractEventLoop) -> None:
665
+ self._loop = loop
666
+ self._tasks: List[asyncio.Task[Any]] = []
667
+ self._cancelled = False
668
+ self._cancelling = 0
669
+
670
+ def assert_timeout(self) -> None:
671
+ """Raise TimeoutError if timer has already been cancelled."""
672
+ if self._cancelled:
673
+ raise asyncio.TimeoutError from None
674
+
675
+ def __enter__(self) -> BaseTimerContext:
676
+ task = asyncio.current_task(loop=self._loop)
677
+ if task is None:
678
+ raise RuntimeError("Timeout context manager should be used inside a task")
679
+
680
+ if sys.version_info >= (3, 11):
681
+ # Remember if the task was already cancelling
682
+ # so when we __exit__ we can decide if we should
683
+ # raise asyncio.TimeoutError or let the cancellation propagate
684
+ self._cancelling = task.cancelling()
685
+
686
+ if self._cancelled:
687
+ raise asyncio.TimeoutError from None
688
+
689
+ self._tasks.append(task)
690
+ return self
691
+
692
+ def __exit__(
693
+ self,
694
+ exc_type: Optional[Type[BaseException]],
695
+ exc_val: Optional[BaseException],
696
+ exc_tb: Optional[TracebackType],
697
+ ) -> Optional[bool]:
698
+ enter_task: Optional[asyncio.Task[Any]] = None
699
+ if self._tasks:
700
+ enter_task = self._tasks.pop()
701
+
702
+ if exc_type is asyncio.CancelledError and self._cancelled:
703
+ assert enter_task is not None
704
+ # The timeout was hit, and the task was cancelled
705
+ # so we need to uncancel the last task that entered the context manager
706
+ # since the cancellation should not leak out of the context manager
707
+ if sys.version_info >= (3, 11):
708
+ # If the task was already cancelling don't raise
709
+ # asyncio.TimeoutError and instead return None
710
+ # to allow the cancellation to propagate
711
+ if enter_task.uncancel() > self._cancelling:
712
+ return None
713
+ raise asyncio.TimeoutError from exc_val
714
+ return None
715
+
716
+ def timeout(self) -> None:
717
+ if not self._cancelled:
718
+ for task in set(self._tasks):
719
+ task.cancel()
720
+
721
+ self._cancelled = True
722
+
723
+
724
+ def ceil_timeout(
725
+ delay: Optional[float], ceil_threshold: float = 5
726
+ ) -> async_timeout.Timeout:
727
+ if delay is None or delay <= 0:
728
+ return async_timeout.timeout(None)
729
+
730
+ loop = asyncio.get_running_loop()
731
+ now = loop.time()
732
+ when = now + delay
733
+ if delay > ceil_threshold:
734
+ when = ceil(when)
735
+ return async_timeout.timeout_at(when)
736
+
737
+
738
+ class HeadersMixin:
739
+ """Mixin for handling headers."""
740
+
741
+ ATTRS = frozenset(["_content_type", "_content_dict", "_stored_content_type"])
742
+
743
+ _headers: MultiMapping[str]
744
+ _content_type: Optional[str] = None
745
+ _content_dict: Optional[Dict[str, str]] = None
746
+ _stored_content_type: Union[str, None, _SENTINEL] = sentinel
747
+
748
+ def _parse_content_type(self, raw: Optional[str]) -> None:
749
+ self._stored_content_type = raw
750
+ if raw is None:
751
+ # default value according to RFC 2616
752
+ self._content_type = "application/octet-stream"
753
+ self._content_dict = {}
754
+ else:
755
+ content_type, content_mapping_proxy = parse_content_type(raw)
756
+ self._content_type = content_type
757
+ # _content_dict needs to be mutable so we can update it
758
+ self._content_dict = content_mapping_proxy.copy()
759
+
760
+ @property
761
+ def content_type(self) -> str:
762
+ """The value of content part for Content-Type HTTP header."""
763
+ raw = self._headers.get(hdrs.CONTENT_TYPE)
764
+ if self._stored_content_type != raw:
765
+ self._parse_content_type(raw)
766
+ assert self._content_type is not None
767
+ return self._content_type
768
+
769
+ @property
770
+ def charset(self) -> Optional[str]:
771
+ """The value of charset part for Content-Type HTTP header."""
772
+ raw = self._headers.get(hdrs.CONTENT_TYPE)
773
+ if self._stored_content_type != raw:
774
+ self._parse_content_type(raw)
775
+ assert self._content_dict is not None
776
+ return self._content_dict.get("charset")
777
+
778
+ @property
779
+ def content_length(self) -> Optional[int]:
780
+ """The value of Content-Length HTTP header."""
781
+ content_length = self._headers.get(hdrs.CONTENT_LENGTH)
782
+ return None if content_length is None else int(content_length)
783
+
784
+
785
+ def set_result(fut: "asyncio.Future[_T]", result: _T) -> None:
786
+ if not fut.done():
787
+ fut.set_result(result)
788
+
789
+
790
+ _EXC_SENTINEL = BaseException()
791
+
792
+
793
+ class ErrorableProtocol(Protocol):
794
+ def set_exception(
795
+ self,
796
+ exc: BaseException,
797
+ exc_cause: BaseException = ...,
798
+ ) -> None: ... # pragma: no cover
799
+
800
+
801
+ def set_exception(
802
+ fut: "asyncio.Future[_T] | ErrorableProtocol",
803
+ exc: BaseException,
804
+ exc_cause: BaseException = _EXC_SENTINEL,
805
+ ) -> None:
806
+ """Set future exception.
807
+
808
+ If the future is marked as complete, this function is a no-op.
809
+
810
+ :param exc_cause: An exception that is a direct cause of ``exc``.
811
+ Only set if provided.
812
+ """
813
+ if asyncio.isfuture(fut) and fut.done():
814
+ return
815
+
816
+ exc_is_sentinel = exc_cause is _EXC_SENTINEL
817
+ exc_causes_itself = exc is exc_cause
818
+ if not exc_is_sentinel and not exc_causes_itself:
819
+ exc.__cause__ = exc_cause
820
+
821
+ fut.set_exception(exc)
822
+
823
+
824
+ @functools.total_ordering
825
+ class AppKey(Generic[_T]):
826
+ """Keys for static typing support in Application."""
827
+
828
+ __slots__ = ("_name", "_t", "__orig_class__")
829
+
830
+ # This may be set by Python when instantiating with a generic type. We need to
831
+ # support this, in order to support types that are not concrete classes,
832
+ # like Iterable, which can't be passed as the second parameter to __init__.
833
+ __orig_class__: Type[object]
834
+
835
+ def __init__(self, name: str, t: Optional[Type[_T]] = None):
836
+ # Prefix with module name to help deduplicate key names.
837
+ frame = inspect.currentframe()
838
+ while frame:
839
+ if frame.f_code.co_name == "<module>":
840
+ module: str = frame.f_globals["__name__"]
841
+ break
842
+ frame = frame.f_back
843
+
844
+ self._name = module + "." + name
845
+ self._t = t
846
+
847
+ def __lt__(self, other: object) -> bool:
848
+ if isinstance(other, AppKey):
849
+ return self._name < other._name
850
+ return True # Order AppKey above other types.
851
+
852
+ def __repr__(self) -> str:
853
+ t = self._t
854
+ if t is None:
855
+ with suppress(AttributeError):
856
+ # Set to type arg.
857
+ t = get_args(self.__orig_class__)[0]
858
+
859
+ if t is None:
860
+ t_repr = "<<Unknown>>"
861
+ elif isinstance(t, type):
862
+ if t.__module__ == "builtins":
863
+ t_repr = t.__qualname__
864
+ else:
865
+ t_repr = f"{t.__module__}.{t.__qualname__}"
866
+ else:
867
+ t_repr = repr(t)
868
+ return f"<AppKey({self._name}, type={t_repr})>"
869
+
870
+
871
+ class ChainMapProxy(Mapping[Union[str, AppKey[Any]], Any]):
872
+ __slots__ = ("_maps",)
873
+
874
+ def __init__(self, maps: Iterable[Mapping[Union[str, AppKey[Any]], Any]]) -> None:
875
+ self._maps = tuple(maps)
876
+
877
+ def __init_subclass__(cls) -> None:
878
+ raise TypeError(
879
+ "Inheritance class {} from ChainMapProxy "
880
+ "is forbidden".format(cls.__name__)
881
+ )
882
+
883
+ @overload # type: ignore[override]
884
+ def __getitem__(self, key: AppKey[_T]) -> _T: ...
885
+
886
+ @overload
887
+ def __getitem__(self, key: str) -> Any: ...
888
+
889
+ def __getitem__(self, key: Union[str, AppKey[_T]]) -> Any:
890
+ for mapping in self._maps:
891
+ try:
892
+ return mapping[key]
893
+ except KeyError:
894
+ pass
895
+ raise KeyError(key)
896
+
897
+ @overload # type: ignore[override]
898
+ def get(self, key: AppKey[_T], default: _S) -> Union[_T, _S]: ...
899
+
900
+ @overload
901
+ def get(self, key: AppKey[_T], default: None = ...) -> Optional[_T]: ...
902
+
903
+ @overload
904
+ def get(self, key: str, default: Any = ...) -> Any: ...
905
+
906
+ def get(self, key: Union[str, AppKey[_T]], default: Any = None) -> Any:
907
+ try:
908
+ return self[key]
909
+ except KeyError:
910
+ return default
911
+
912
+ def __len__(self) -> int:
913
+ # reuses stored hash values if possible
914
+ return len(set().union(*self._maps))
915
+
916
+ def __iter__(self) -> Iterator[Union[str, AppKey[Any]]]:
917
+ d: Dict[Union[str, AppKey[Any]], Any] = {}
918
+ for mapping in reversed(self._maps):
919
+ # reuses stored hash values if possible
920
+ d.update(mapping)
921
+ return iter(d)
922
+
923
+ def __contains__(self, key: object) -> bool:
924
+ return any(key in m for m in self._maps)
925
+
926
+ def __bool__(self) -> bool:
927
+ return any(self._maps)
928
+
929
+ def __repr__(self) -> str:
930
+ content = ", ".join(map(repr, self._maps))
931
+ return f"ChainMapProxy({content})"
932
+
933
+
934
+ # https://tools.ietf.org/html/rfc7232#section-2.3
935
+ _ETAGC = r"[!\x23-\x7E\x80-\xff]+"
936
+ _ETAGC_RE = re.compile(_ETAGC)
937
+ _QUOTED_ETAG = rf'(W/)?"({_ETAGC})"'
938
+ QUOTED_ETAG_RE = re.compile(_QUOTED_ETAG)
939
+ LIST_QUOTED_ETAG_RE = re.compile(rf"({_QUOTED_ETAG})(?:\s*,\s*|$)|(.)")
940
+
941
+ ETAG_ANY = "*"
942
+
943
+
944
+ @attr.s(auto_attribs=True, frozen=True, slots=True)
945
+ class ETag:
946
+ value: str
947
+ is_weak: bool = False
948
+
949
+
950
+ def validate_etag_value(value: str) -> None:
951
+ if value != ETAG_ANY and not _ETAGC_RE.fullmatch(value):
952
+ raise ValueError(
953
+ f"Value {value!r} is not a valid etag. Maybe it contains '\"'?"
954
+ )
955
+
956
+
957
+ def parse_http_date(date_str: Optional[str]) -> Optional[datetime.datetime]:
958
+ """Process a date string, return a datetime object"""
959
+ if date_str is not None:
960
+ timetuple = parsedate(date_str)
961
+ if timetuple is not None:
962
+ with suppress(ValueError):
963
+ return datetime.datetime(*timetuple[:6], tzinfo=datetime.timezone.utc)
964
+ return None
965
+
966
+
967
+ @functools.lru_cache
968
+ def must_be_empty_body(method: str, code: int) -> bool:
969
+ """Check if a request must return an empty body."""
970
+ return (
971
+ code in EMPTY_BODY_STATUS_CODES
972
+ or method in EMPTY_BODY_METHODS
973
+ or (200 <= code < 300 and method in hdrs.METH_CONNECT_ALL)
974
+ )
975
+
976
+
977
+ def should_remove_content_length(method: str, code: int) -> bool:
978
+ """Check if a Content-Length header should be removed.
979
+
980
+ This should always be a subset of must_be_empty_body
981
+ """
982
+ # https://www.rfc-editor.org/rfc/rfc9110.html#section-8.6-8
983
+ # https://www.rfc-editor.org/rfc/rfc9110.html#section-15.4.5-4
984
+ return code in EMPTY_BODY_STATUS_CODES or (
985
+ 200 <= code < 300 and method in hdrs.METH_CONNECT_ALL
986
+ )
aiohttp/http_parser.py ADDED
@@ -0,0 +1,1086 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import abc
2
+ import asyncio
3
+ import re
4
+ import string
5
+ from contextlib import suppress
6
+ from enum import IntEnum
7
+ from typing import (
8
+ Any,
9
+ ClassVar,
10
+ Final,
11
+ Generic,
12
+ List,
13
+ Literal,
14
+ NamedTuple,
15
+ Optional,
16
+ Pattern,
17
+ Set,
18
+ Tuple,
19
+ Type,
20
+ TypeVar,
21
+ Union,
22
+ )
23
+
24
+ from multidict import CIMultiDict, CIMultiDictProxy, istr
25
+ from yarl import URL
26
+
27
+ from . import hdrs
28
+ from .base_protocol import BaseProtocol
29
+ from .compression_utils import (
30
+ DEFAULT_MAX_DECOMPRESS_SIZE,
31
+ HAS_BROTLI,
32
+ HAS_ZSTD,
33
+ BrotliDecompressor,
34
+ ZLibDecompressor,
35
+ ZSTDDecompressor,
36
+ )
37
+ from .helpers import (
38
+ _EXC_SENTINEL,
39
+ DEBUG,
40
+ EMPTY_BODY_METHODS,
41
+ EMPTY_BODY_STATUS_CODES,
42
+ NO_EXTENSIONS,
43
+ BaseTimerContext,
44
+ set_exception,
45
+ )
46
+ from .http_exceptions import (
47
+ BadHttpMessage,
48
+ BadHttpMethod,
49
+ BadStatusLine,
50
+ ContentEncodingError,
51
+ ContentLengthError,
52
+ DecompressSizeError,
53
+ InvalidHeader,
54
+ InvalidURLError,
55
+ LineTooLong,
56
+ TransferEncodingError,
57
+ )
58
+ from .http_writer import HttpVersion, HttpVersion10
59
+ from .streams import EMPTY_PAYLOAD, StreamReader
60
+ from .typedefs import RawHeaders
61
+
62
+ __all__ = (
63
+ "HeadersParser",
64
+ "HttpParser",
65
+ "HttpRequestParser",
66
+ "HttpResponseParser",
67
+ "RawRequestMessage",
68
+ "RawResponseMessage",
69
+ )
70
+
71
+ _SEP = Literal[b"\r\n", b"\n"]
72
+
73
+ ASCIISET: Final[Set[str]] = set(string.printable)
74
+
75
+ # See https://www.rfc-editor.org/rfc/rfc9110.html#name-overview
76
+ # and https://www.rfc-editor.org/rfc/rfc9110.html#name-tokens
77
+ #
78
+ # method = token
79
+ # tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." /
80
+ # "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA
81
+ # token = 1*tchar
82
+ _TCHAR_SPECIALS: Final[str] = re.escape("!#$%&'*+-.^_`|~")
83
+ TOKENRE: Final[Pattern[str]] = re.compile(f"[0-9A-Za-z{_TCHAR_SPECIALS}]+")
84
+ VERSRE: Final[Pattern[str]] = re.compile(r"HTTP/(\d)\.(\d)", re.ASCII)
85
+ DIGITS: Final[Pattern[str]] = re.compile(r"\d+", re.ASCII)
86
+ HEXDIGITS: Final[Pattern[bytes]] = re.compile(rb"[0-9a-fA-F]+")
87
+
88
+
89
+ class RawRequestMessage(NamedTuple):
90
+ method: str
91
+ path: str
92
+ version: HttpVersion
93
+ headers: "CIMultiDictProxy[str]"
94
+ raw_headers: RawHeaders
95
+ should_close: bool
96
+ compression: Optional[str]
97
+ upgrade: bool
98
+ chunked: bool
99
+ url: URL
100
+
101
+
102
+ class RawResponseMessage(NamedTuple):
103
+ version: HttpVersion
104
+ code: int
105
+ reason: str
106
+ headers: CIMultiDictProxy[str]
107
+ raw_headers: RawHeaders
108
+ should_close: bool
109
+ compression: Optional[str]
110
+ upgrade: bool
111
+ chunked: bool
112
+
113
+
114
+ _MsgT = TypeVar("_MsgT", RawRequestMessage, RawResponseMessage)
115
+
116
+
117
+ class ParseState(IntEnum):
118
+
119
+ PARSE_NONE = 0
120
+ PARSE_LENGTH = 1
121
+ PARSE_CHUNKED = 2
122
+ PARSE_UNTIL_EOF = 3
123
+
124
+
125
+ class ChunkState(IntEnum):
126
+ PARSE_CHUNKED_SIZE = 0
127
+ PARSE_CHUNKED_CHUNK = 1
128
+ PARSE_CHUNKED_CHUNK_EOF = 2
129
+ PARSE_MAYBE_TRAILERS = 3
130
+ PARSE_TRAILERS = 4
131
+
132
+
133
+ class HeadersParser:
134
+ def __init__(
135
+ self,
136
+ max_line_size: int = 8190,
137
+ max_headers: int = 32768,
138
+ max_field_size: int = 8190,
139
+ lax: bool = False,
140
+ ) -> None:
141
+ self.max_line_size = max_line_size
142
+ self.max_headers = max_headers
143
+ self.max_field_size = max_field_size
144
+ self._lax = lax
145
+
146
+ def parse_headers(
147
+ self, lines: List[bytes]
148
+ ) -> Tuple["CIMultiDictProxy[str]", RawHeaders]:
149
+ headers: CIMultiDict[str] = CIMultiDict()
150
+ # note: "raw" does not mean inclusion of OWS before/after the field value
151
+ raw_headers = []
152
+
153
+ lines_idx = 0
154
+ line = lines[lines_idx]
155
+ line_count = len(lines)
156
+
157
+ while line:
158
+ # Parse initial header name : value pair.
159
+ try:
160
+ bname, bvalue = line.split(b":", 1)
161
+ except ValueError:
162
+ raise InvalidHeader(line) from None
163
+
164
+ if len(bname) == 0:
165
+ raise InvalidHeader(bname)
166
+
167
+ # https://www.rfc-editor.org/rfc/rfc9112.html#section-5.1-2
168
+ if {bname[0], bname[-1]} & {32, 9}: # {" ", "\t"}
169
+ raise InvalidHeader(line)
170
+
171
+ bvalue = bvalue.lstrip(b" \t")
172
+ if len(bname) > self.max_field_size:
173
+ raise LineTooLong(
174
+ "request header name {}".format(
175
+ bname.decode("utf8", "backslashreplace")
176
+ ),
177
+ str(self.max_field_size),
178
+ str(len(bname)),
179
+ )
180
+ name = bname.decode("utf-8", "surrogateescape")
181
+ if not TOKENRE.fullmatch(name):
182
+ raise InvalidHeader(bname)
183
+
184
+ header_length = len(bvalue)
185
+
186
+ # next line
187
+ lines_idx += 1
188
+ line = lines[lines_idx]
189
+
190
+ # consume continuation lines
191
+ continuation = self._lax and line and line[0] in (32, 9) # (' ', '\t')
192
+
193
+ # Deprecated: https://www.rfc-editor.org/rfc/rfc9112.html#name-obsolete-line-folding
194
+ if continuation:
195
+ bvalue_lst = [bvalue]
196
+ while continuation:
197
+ header_length += len(line)
198
+ if header_length > self.max_field_size:
199
+ raise LineTooLong(
200
+ "request header field {}".format(
201
+ bname.decode("utf8", "backslashreplace")
202
+ ),
203
+ str(self.max_field_size),
204
+ str(header_length),
205
+ )
206
+ bvalue_lst.append(line)
207
+
208
+ # next line
209
+ lines_idx += 1
210
+ if lines_idx < line_count:
211
+ line = lines[lines_idx]
212
+ if line:
213
+ continuation = line[0] in (32, 9) # (' ', '\t')
214
+ else:
215
+ line = b""
216
+ break
217
+ bvalue = b"".join(bvalue_lst)
218
+ else:
219
+ if header_length > self.max_field_size:
220
+ raise LineTooLong(
221
+ "request header field {}".format(
222
+ bname.decode("utf8", "backslashreplace")
223
+ ),
224
+ str(self.max_field_size),
225
+ str(header_length),
226
+ )
227
+
228
+ bvalue = bvalue.strip(b" \t")
229
+ value = bvalue.decode("utf-8", "surrogateescape")
230
+
231
+ # https://www.rfc-editor.org/rfc/rfc9110.html#section-5.5-5
232
+ if "\n" in value or "\r" in value or "\x00" in value:
233
+ raise InvalidHeader(bvalue)
234
+
235
+ headers.add(name, value)
236
+ raw_headers.append((bname, bvalue))
237
+
238
+ return (CIMultiDictProxy(headers), tuple(raw_headers))
239
+
240
+
241
+ def _is_supported_upgrade(headers: CIMultiDictProxy[str]) -> bool:
242
+ """Check if the upgrade header is supported."""
243
+ u = headers.get(hdrs.UPGRADE, "")
244
+ # .lower() can transform non-ascii characters.
245
+ return u.isascii() and u.lower() in {"tcp", "websocket"}
246
+
247
+
248
+ class HttpParser(abc.ABC, Generic[_MsgT]):
249
+ lax: ClassVar[bool] = False
250
+
251
+ def __init__(
252
+ self,
253
+ protocol: Optional[BaseProtocol] = None,
254
+ loop: Optional[asyncio.AbstractEventLoop] = None,
255
+ limit: int = 2**16,
256
+ max_line_size: int = 8190,
257
+ max_headers: int = 32768,
258
+ max_field_size: int = 8190,
259
+ timer: Optional[BaseTimerContext] = None,
260
+ code: Optional[int] = None,
261
+ method: Optional[str] = None,
262
+ payload_exception: Optional[Type[BaseException]] = None,
263
+ response_with_body: bool = True,
264
+ read_until_eof: bool = False,
265
+ auto_decompress: bool = True,
266
+ ) -> None:
267
+ self.protocol = protocol
268
+ self.loop = loop
269
+ self.max_line_size = max_line_size
270
+ self.max_headers = max_headers
271
+ self.max_field_size = max_field_size
272
+ self.timer = timer
273
+ self.code = code
274
+ self.method = method
275
+ self.payload_exception = payload_exception
276
+ self.response_with_body = response_with_body
277
+ self.read_until_eof = read_until_eof
278
+
279
+ self._lines: List[bytes] = []
280
+ self._tail = b""
281
+ self._upgraded = False
282
+ self._payload = None
283
+ self._payload_parser: Optional[HttpPayloadParser] = None
284
+ self._auto_decompress = auto_decompress
285
+ self._limit = limit
286
+ self._headers_parser = HeadersParser(
287
+ max_line_size, max_headers, max_field_size, self.lax
288
+ )
289
+
290
+ @abc.abstractmethod
291
+ def parse_message(self, lines: List[bytes]) -> _MsgT: ...
292
+
293
+ @abc.abstractmethod
294
+ def _is_chunked_te(self, te: str) -> bool: ...
295
+
296
+ def feed_eof(self) -> Optional[_MsgT]:
297
+ if self._payload_parser is not None:
298
+ self._payload_parser.feed_eof()
299
+ self._payload_parser = None
300
+ else:
301
+ # try to extract partial message
302
+ if self._tail:
303
+ self._lines.append(self._tail)
304
+
305
+ if self._lines:
306
+ if self._lines[-1] != "\r\n":
307
+ self._lines.append(b"")
308
+ with suppress(Exception):
309
+ return self.parse_message(self._lines)
310
+ return None
311
+
312
+ def feed_data(
313
+ self,
314
+ data: bytes,
315
+ SEP: _SEP = b"\r\n",
316
+ EMPTY: bytes = b"",
317
+ CONTENT_LENGTH: istr = hdrs.CONTENT_LENGTH,
318
+ METH_CONNECT: str = hdrs.METH_CONNECT,
319
+ SEC_WEBSOCKET_KEY1: istr = hdrs.SEC_WEBSOCKET_KEY1,
320
+ ) -> Tuple[List[Tuple[_MsgT, StreamReader]], bool, bytes]:
321
+
322
+ messages = []
323
+
324
+ if self._tail:
325
+ data, self._tail = self._tail + data, b""
326
+
327
+ data_len = len(data)
328
+ start_pos = 0
329
+ loop = self.loop
330
+
331
+ should_close = False
332
+ while start_pos < data_len:
333
+
334
+ # read HTTP message (request/response line + headers), \r\n\r\n
335
+ # and split by lines
336
+ if self._payload_parser is None and not self._upgraded:
337
+ pos = data.find(SEP, start_pos)
338
+ # consume \r\n
339
+ if pos == start_pos and not self._lines:
340
+ start_pos = pos + len(SEP)
341
+ continue
342
+
343
+ if pos >= start_pos:
344
+ if should_close:
345
+ raise BadHttpMessage("Data after `Connection: close`")
346
+
347
+ # line found
348
+ line = data[start_pos:pos]
349
+ if SEP == b"\n": # For lax response parsing
350
+ line = line.rstrip(b"\r")
351
+ self._lines.append(line)
352
+ start_pos = pos + len(SEP)
353
+
354
+ # \r\n\r\n found
355
+ if self._lines[-1] == EMPTY:
356
+ try:
357
+ msg: _MsgT = self.parse_message(self._lines)
358
+ finally:
359
+ self._lines.clear()
360
+
361
+ def get_content_length() -> Optional[int]:
362
+ # payload length
363
+ length_hdr = msg.headers.get(CONTENT_LENGTH)
364
+ if length_hdr is None:
365
+ return None
366
+
367
+ # Shouldn't allow +/- or other number formats.
368
+ # https://www.rfc-editor.org/rfc/rfc9110#section-8.6-2
369
+ # msg.headers is already stripped of leading/trailing wsp
370
+ if not DIGITS.fullmatch(length_hdr):
371
+ raise InvalidHeader(CONTENT_LENGTH)
372
+
373
+ return int(length_hdr)
374
+
375
+ length = get_content_length()
376
+ # do not support old websocket spec
377
+ if SEC_WEBSOCKET_KEY1 in msg.headers:
378
+ raise InvalidHeader(SEC_WEBSOCKET_KEY1)
379
+
380
+ self._upgraded = msg.upgrade and _is_supported_upgrade(
381
+ msg.headers
382
+ )
383
+
384
+ method = getattr(msg, "method", self.method)
385
+ # code is only present on responses
386
+ code = getattr(msg, "code", 0)
387
+
388
+ assert self.protocol is not None
389
+ # calculate payload
390
+ empty_body = code in EMPTY_BODY_STATUS_CODES or bool(
391
+ method and method in EMPTY_BODY_METHODS
392
+ )
393
+ if not empty_body and (
394
+ ((length is not None and length > 0) or msg.chunked)
395
+ and not self._upgraded
396
+ ):
397
+ payload = StreamReader(
398
+ self.protocol,
399
+ timer=self.timer,
400
+ loop=loop,
401
+ limit=self._limit,
402
+ )
403
+ payload_parser = HttpPayloadParser(
404
+ payload,
405
+ length=length,
406
+ chunked=msg.chunked,
407
+ method=method,
408
+ compression=msg.compression,
409
+ code=self.code,
410
+ response_with_body=self.response_with_body,
411
+ auto_decompress=self._auto_decompress,
412
+ lax=self.lax,
413
+ headers_parser=self._headers_parser,
414
+ )
415
+ if not payload_parser.done:
416
+ self._payload_parser = payload_parser
417
+ elif method == METH_CONNECT:
418
+ assert isinstance(msg, RawRequestMessage)
419
+ payload = StreamReader(
420
+ self.protocol,
421
+ timer=self.timer,
422
+ loop=loop,
423
+ limit=self._limit,
424
+ )
425
+ self._upgraded = True
426
+ self._payload_parser = HttpPayloadParser(
427
+ payload,
428
+ method=msg.method,
429
+ compression=msg.compression,
430
+ auto_decompress=self._auto_decompress,
431
+ lax=self.lax,
432
+ headers_parser=self._headers_parser,
433
+ )
434
+ elif not empty_body and length is None and self.read_until_eof:
435
+ payload = StreamReader(
436
+ self.protocol,
437
+ timer=self.timer,
438
+ loop=loop,
439
+ limit=self._limit,
440
+ )
441
+ payload_parser = HttpPayloadParser(
442
+ payload,
443
+ length=length,
444
+ chunked=msg.chunked,
445
+ method=method,
446
+ compression=msg.compression,
447
+ code=self.code,
448
+ response_with_body=self.response_with_body,
449
+ auto_decompress=self._auto_decompress,
450
+ lax=self.lax,
451
+ headers_parser=self._headers_parser,
452
+ )
453
+ if not payload_parser.done:
454
+ self._payload_parser = payload_parser
455
+ else:
456
+ payload = EMPTY_PAYLOAD
457
+
458
+ messages.append((msg, payload))
459
+ should_close = msg.should_close
460
+ else:
461
+ self._tail = data[start_pos:]
462
+ data = EMPTY
463
+ break
464
+
465
+ # no parser, just store
466
+ elif self._payload_parser is None and self._upgraded:
467
+ assert not self._lines
468
+ break
469
+
470
+ # feed payload
471
+ elif data and start_pos < data_len:
472
+ assert not self._lines
473
+ assert self._payload_parser is not None
474
+ try:
475
+ eof, data = self._payload_parser.feed_data(data[start_pos:], SEP)
476
+ except BaseException as underlying_exc:
477
+ reraised_exc = underlying_exc
478
+ if self.payload_exception is not None:
479
+ reraised_exc = self.payload_exception(str(underlying_exc))
480
+
481
+ set_exception(
482
+ self._payload_parser.payload,
483
+ reraised_exc,
484
+ underlying_exc,
485
+ )
486
+
487
+ eof = True
488
+ data = b""
489
+ if isinstance(
490
+ underlying_exc, (InvalidHeader, TransferEncodingError)
491
+ ):
492
+ raise
493
+
494
+ if eof:
495
+ start_pos = 0
496
+ data_len = len(data)
497
+ self._payload_parser = None
498
+ continue
499
+ else:
500
+ break
501
+
502
+ if data and start_pos < data_len:
503
+ data = data[start_pos:]
504
+ else:
505
+ data = EMPTY
506
+
507
+ return messages, self._upgraded, data
508
+
509
+ def parse_headers(
510
+ self, lines: List[bytes]
511
+ ) -> Tuple[
512
+ "CIMultiDictProxy[str]", RawHeaders, Optional[bool], Optional[str], bool, bool
513
+ ]:
514
+ """Parses RFC 5322 headers from a stream.
515
+
516
+ Line continuations are supported. Returns list of header name
517
+ and value pairs. Header name is in upper case.
518
+ """
519
+ headers, raw_headers = self._headers_parser.parse_headers(lines)
520
+ close_conn = None
521
+ encoding = None
522
+ upgrade = False
523
+ chunked = False
524
+
525
+ # https://www.rfc-editor.org/rfc/rfc9110.html#section-5.5-6
526
+ # https://www.rfc-editor.org/rfc/rfc9110.html#name-collected-abnf
527
+ singletons = (
528
+ hdrs.CONTENT_LENGTH,
529
+ hdrs.CONTENT_LOCATION,
530
+ hdrs.CONTENT_RANGE,
531
+ hdrs.CONTENT_TYPE,
532
+ hdrs.ETAG,
533
+ hdrs.HOST,
534
+ hdrs.MAX_FORWARDS,
535
+ hdrs.SERVER,
536
+ hdrs.TRANSFER_ENCODING,
537
+ hdrs.USER_AGENT,
538
+ )
539
+ bad_hdr = next((h for h in singletons if len(headers.getall(h, ())) > 1), None)
540
+ if bad_hdr is not None:
541
+ raise BadHttpMessage(f"Duplicate '{bad_hdr}' header found.")
542
+
543
+ # keep-alive
544
+ conn = headers.get(hdrs.CONNECTION)
545
+ if conn:
546
+ v = conn.lower()
547
+ if v == "close":
548
+ close_conn = True
549
+ elif v == "keep-alive":
550
+ close_conn = False
551
+ # https://www.rfc-editor.org/rfc/rfc9110.html#name-101-switching-protocols
552
+ elif v == "upgrade" and headers.get(hdrs.UPGRADE):
553
+ upgrade = True
554
+
555
+ # encoding
556
+ enc = headers.get(hdrs.CONTENT_ENCODING, "")
557
+ if enc.isascii() and enc.lower() in {"gzip", "deflate", "br", "zstd"}:
558
+ encoding = enc
559
+
560
+ # chunking
561
+ te = headers.get(hdrs.TRANSFER_ENCODING)
562
+ if te is not None:
563
+ if self._is_chunked_te(te):
564
+ chunked = True
565
+
566
+ if hdrs.CONTENT_LENGTH in headers:
567
+ raise BadHttpMessage(
568
+ "Transfer-Encoding can't be present with Content-Length",
569
+ )
570
+
571
+ return (headers, raw_headers, close_conn, encoding, upgrade, chunked)
572
+
573
+ def set_upgraded(self, val: bool) -> None:
574
+ """Set connection upgraded (to websocket) mode.
575
+
576
+ :param bool val: new state.
577
+ """
578
+ self._upgraded = val
579
+
580
+
581
+ class HttpRequestParser(HttpParser[RawRequestMessage]):
582
+ """Read request status line.
583
+
584
+ Exception .http_exceptions.BadStatusLine
585
+ could be raised in case of any errors in status line.
586
+ Returns RawRequestMessage.
587
+ """
588
+
589
+ def parse_message(self, lines: List[bytes]) -> RawRequestMessage:
590
+ # request line
591
+ line = lines[0].decode("utf-8", "surrogateescape")
592
+ try:
593
+ method, path, version = line.split(" ", maxsplit=2)
594
+ except ValueError:
595
+ raise BadHttpMethod(line) from None
596
+
597
+ if len(path) > self.max_line_size:
598
+ raise LineTooLong(
599
+ "Status line is too long", str(self.max_line_size), str(len(path))
600
+ )
601
+
602
+ # method
603
+ if not TOKENRE.fullmatch(method):
604
+ raise BadHttpMethod(method)
605
+
606
+ # version
607
+ match = VERSRE.fullmatch(version)
608
+ if match is None:
609
+ raise BadStatusLine(line)
610
+ version_o = HttpVersion(int(match.group(1)), int(match.group(2)))
611
+
612
+ if method == "CONNECT":
613
+ # authority-form,
614
+ # https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.3
615
+ url = URL.build(authority=path, encoded=True)
616
+ elif path.startswith("/"):
617
+ # origin-form,
618
+ # https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.1
619
+ path_part, _hash_separator, url_fragment = path.partition("#")
620
+ path_part, _question_mark_separator, qs_part = path_part.partition("?")
621
+
622
+ # NOTE: `yarl.URL.build()` is used to mimic what the Cython-based
623
+ # NOTE: parser does, otherwise it results into the same
624
+ # NOTE: HTTP Request-Line input producing different
625
+ # NOTE: `yarl.URL()` objects
626
+ url = URL.build(
627
+ path=path_part,
628
+ query_string=qs_part,
629
+ fragment=url_fragment,
630
+ encoded=True,
631
+ )
632
+ elif path == "*" and method == "OPTIONS":
633
+ # asterisk-form,
634
+ url = URL(path, encoded=True)
635
+ else:
636
+ # absolute-form for proxy maybe,
637
+ # https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.2
638
+ url = URL(path, encoded=True)
639
+ if url.scheme == "":
640
+ # not absolute-form
641
+ raise InvalidURLError(
642
+ path.encode(errors="surrogateescape").decode("latin1")
643
+ )
644
+
645
+ # read headers
646
+ (
647
+ headers,
648
+ raw_headers,
649
+ close,
650
+ compression,
651
+ upgrade,
652
+ chunked,
653
+ ) = self.parse_headers(lines[1:])
654
+
655
+ if close is None: # then the headers weren't set in the request
656
+ if version_o <= HttpVersion10: # HTTP 1.0 must asks to not close
657
+ close = True
658
+ else: # HTTP 1.1 must ask to close.
659
+ close = False
660
+
661
+ return RawRequestMessage(
662
+ method,
663
+ path,
664
+ version_o,
665
+ headers,
666
+ raw_headers,
667
+ close,
668
+ compression,
669
+ upgrade,
670
+ chunked,
671
+ url,
672
+ )
673
+
674
+ def _is_chunked_te(self, te: str) -> bool:
675
+ te = te.rsplit(",", maxsplit=1)[-1].strip(" \t")
676
+ # .lower() transforms some non-ascii chars, so must check first.
677
+ if te.isascii() and te.lower() == "chunked":
678
+ return True
679
+ # https://www.rfc-editor.org/rfc/rfc9112#section-6.3-2.4.3
680
+ raise BadHttpMessage("Request has invalid `Transfer-Encoding`")
681
+
682
+
683
+ class HttpResponseParser(HttpParser[RawResponseMessage]):
684
+ """Read response status line and headers.
685
+
686
+ BadStatusLine could be raised in case of any errors in status line.
687
+ Returns RawResponseMessage.
688
+ """
689
+
690
+ # Lax mode should only be enabled on response parser.
691
+ lax = not DEBUG
692
+
693
+ def feed_data(
694
+ self,
695
+ data: bytes,
696
+ SEP: Optional[_SEP] = None,
697
+ *args: Any,
698
+ **kwargs: Any,
699
+ ) -> Tuple[List[Tuple[RawResponseMessage, StreamReader]], bool, bytes]:
700
+ if SEP is None:
701
+ SEP = b"\r\n" if DEBUG else b"\n"
702
+ return super().feed_data(data, SEP, *args, **kwargs)
703
+
704
+ def parse_message(self, lines: List[bytes]) -> RawResponseMessage:
705
+ line = lines[0].decode("utf-8", "surrogateescape")
706
+ try:
707
+ version, status = line.split(maxsplit=1)
708
+ except ValueError:
709
+ raise BadStatusLine(line) from None
710
+
711
+ try:
712
+ status, reason = status.split(maxsplit=1)
713
+ except ValueError:
714
+ status = status.strip()
715
+ reason = ""
716
+
717
+ if len(reason) > self.max_line_size:
718
+ raise LineTooLong(
719
+ "Status line is too long", str(self.max_line_size), str(len(reason))
720
+ )
721
+
722
+ # version
723
+ match = VERSRE.fullmatch(version)
724
+ if match is None:
725
+ raise BadStatusLine(line)
726
+ version_o = HttpVersion(int(match.group(1)), int(match.group(2)))
727
+
728
+ # The status code is a three-digit ASCII number, no padding
729
+ if len(status) != 3 or not DIGITS.fullmatch(status):
730
+ raise BadStatusLine(line)
731
+ status_i = int(status)
732
+
733
+ # read headers
734
+ (
735
+ headers,
736
+ raw_headers,
737
+ close,
738
+ compression,
739
+ upgrade,
740
+ chunked,
741
+ ) = self.parse_headers(lines[1:])
742
+
743
+ if close is None:
744
+ if version_o <= HttpVersion10:
745
+ close = True
746
+ # https://www.rfc-editor.org/rfc/rfc9112.html#name-message-body-length
747
+ elif 100 <= status_i < 200 or status_i in {204, 304}:
748
+ close = False
749
+ elif hdrs.CONTENT_LENGTH in headers or hdrs.TRANSFER_ENCODING in headers:
750
+ close = False
751
+ else:
752
+ # https://www.rfc-editor.org/rfc/rfc9112.html#section-6.3-2.8
753
+ close = True
754
+
755
+ return RawResponseMessage(
756
+ version_o,
757
+ status_i,
758
+ reason.strip(),
759
+ headers,
760
+ raw_headers,
761
+ close,
762
+ compression,
763
+ upgrade,
764
+ chunked,
765
+ )
766
+
767
+ def _is_chunked_te(self, te: str) -> bool:
768
+ # https://www.rfc-editor.org/rfc/rfc9112#section-6.3-2.4.2
769
+ return te.rsplit(",", maxsplit=1)[-1].strip(" \t").lower() == "chunked"
770
+
771
+
772
+ class HttpPayloadParser:
773
+ def __init__(
774
+ self,
775
+ payload: StreamReader,
776
+ length: Optional[int] = None,
777
+ chunked: bool = False,
778
+ compression: Optional[str] = None,
779
+ code: Optional[int] = None,
780
+ method: Optional[str] = None,
781
+ response_with_body: bool = True,
782
+ auto_decompress: bool = True,
783
+ lax: bool = False,
784
+ *,
785
+ headers_parser: HeadersParser,
786
+ ) -> None:
787
+ self._length = 0
788
+ self._type = ParseState.PARSE_UNTIL_EOF
789
+ self._chunk = ChunkState.PARSE_CHUNKED_SIZE
790
+ self._chunk_size = 0
791
+ self._chunk_tail = b""
792
+ self._auto_decompress = auto_decompress
793
+ self._lax = lax
794
+ self._headers_parser = headers_parser
795
+ self._trailer_lines: list[bytes] = []
796
+ self.done = False
797
+
798
+ # payload decompression wrapper
799
+ if response_with_body and compression and self._auto_decompress:
800
+ real_payload: Union[StreamReader, DeflateBuffer] = DeflateBuffer(
801
+ payload, compression
802
+ )
803
+ else:
804
+ real_payload = payload
805
+
806
+ # payload parser
807
+ if not response_with_body:
808
+ # don't parse payload if it's not expected to be received
809
+ self._type = ParseState.PARSE_NONE
810
+ real_payload.feed_eof()
811
+ self.done = True
812
+ elif chunked:
813
+ self._type = ParseState.PARSE_CHUNKED
814
+ elif length is not None:
815
+ self._type = ParseState.PARSE_LENGTH
816
+ self._length = length
817
+ if self._length == 0:
818
+ real_payload.feed_eof()
819
+ self.done = True
820
+
821
+ self.payload = real_payload
822
+
823
+ def feed_eof(self) -> None:
824
+ if self._type == ParseState.PARSE_UNTIL_EOF:
825
+ self.payload.feed_eof()
826
+ elif self._type == ParseState.PARSE_LENGTH:
827
+ raise ContentLengthError(
828
+ "Not enough data to satisfy content length header."
829
+ )
830
+ elif self._type == ParseState.PARSE_CHUNKED:
831
+ raise TransferEncodingError(
832
+ "Not enough data to satisfy transfer length header."
833
+ )
834
+
835
+ def feed_data(
836
+ self, chunk: bytes, SEP: _SEP = b"\r\n", CHUNK_EXT: bytes = b";"
837
+ ) -> Tuple[bool, bytes]:
838
+ # Read specified amount of bytes
839
+ if self._type == ParseState.PARSE_LENGTH:
840
+ required = self._length
841
+ chunk_len = len(chunk)
842
+
843
+ if required >= chunk_len:
844
+ self._length = required - chunk_len
845
+ self.payload.feed_data(chunk, chunk_len)
846
+ if self._length == 0:
847
+ self.payload.feed_eof()
848
+ return True, b""
849
+ else:
850
+ self._length = 0
851
+ self.payload.feed_data(chunk[:required], required)
852
+ self.payload.feed_eof()
853
+ return True, chunk[required:]
854
+
855
+ # Chunked transfer encoding parser
856
+ elif self._type == ParseState.PARSE_CHUNKED:
857
+ if self._chunk_tail:
858
+ chunk = self._chunk_tail + chunk
859
+ self._chunk_tail = b""
860
+
861
+ while chunk:
862
+
863
+ # read next chunk size
864
+ if self._chunk == ChunkState.PARSE_CHUNKED_SIZE:
865
+ pos = chunk.find(SEP)
866
+ if pos >= 0:
867
+ i = chunk.find(CHUNK_EXT, 0, pos)
868
+ if i >= 0:
869
+ size_b = chunk[:i] # strip chunk-extensions
870
+ # Verify no LF in the chunk-extension
871
+ if b"\n" in (ext := chunk[i:pos]):
872
+ exc = TransferEncodingError(
873
+ f"Unexpected LF in chunk-extension: {ext!r}"
874
+ )
875
+ set_exception(self.payload, exc)
876
+ raise exc
877
+ else:
878
+ size_b = chunk[:pos]
879
+
880
+ if self._lax: # Allow whitespace in lax mode.
881
+ size_b = size_b.strip()
882
+
883
+ if not re.fullmatch(HEXDIGITS, size_b):
884
+ exc = TransferEncodingError(
885
+ chunk[:pos].decode("ascii", "surrogateescape")
886
+ )
887
+ set_exception(self.payload, exc)
888
+ raise exc
889
+ size = int(bytes(size_b), 16)
890
+
891
+ chunk = chunk[pos + len(SEP) :]
892
+ if size == 0: # eof marker
893
+ self._chunk = ChunkState.PARSE_TRAILERS
894
+ if self._lax and chunk.startswith(b"\r"):
895
+ chunk = chunk[1:]
896
+ else:
897
+ self._chunk = ChunkState.PARSE_CHUNKED_CHUNK
898
+ self._chunk_size = size
899
+ self.payload.begin_http_chunk_receiving()
900
+ else:
901
+ self._chunk_tail = chunk
902
+ return False, b""
903
+
904
+ # read chunk and feed buffer
905
+ if self._chunk == ChunkState.PARSE_CHUNKED_CHUNK:
906
+ required = self._chunk_size
907
+ chunk_len = len(chunk)
908
+
909
+ if required > chunk_len:
910
+ self._chunk_size = required - chunk_len
911
+ self.payload.feed_data(chunk, chunk_len)
912
+ return False, b""
913
+ else:
914
+ self._chunk_size = 0
915
+ self.payload.feed_data(chunk[:required], required)
916
+ chunk = chunk[required:]
917
+ self._chunk = ChunkState.PARSE_CHUNKED_CHUNK_EOF
918
+ self.payload.end_http_chunk_receiving()
919
+
920
+ # toss the CRLF at the end of the chunk
921
+ if self._chunk == ChunkState.PARSE_CHUNKED_CHUNK_EOF:
922
+ if self._lax and chunk.startswith(b"\r"):
923
+ chunk = chunk[1:]
924
+ if chunk[: len(SEP)] == SEP:
925
+ chunk = chunk[len(SEP) :]
926
+ self._chunk = ChunkState.PARSE_CHUNKED_SIZE
927
+ else:
928
+ self._chunk_tail = chunk
929
+ return False, b""
930
+
931
+ if self._chunk == ChunkState.PARSE_TRAILERS:
932
+ pos = chunk.find(SEP)
933
+ if pos < 0: # No line found
934
+ self._chunk_tail = chunk
935
+ return False, b""
936
+
937
+ line = chunk[:pos]
938
+ chunk = chunk[pos + len(SEP) :]
939
+ if SEP == b"\n": # For lax response parsing
940
+ line = line.rstrip(b"\r")
941
+ self._trailer_lines.append(line)
942
+
943
+ # \r\n\r\n found, end of stream
944
+ if self._trailer_lines[-1] == b"":
945
+ # Headers and trailers are defined the same way,
946
+ # so we reuse the HeadersParser here.
947
+ try:
948
+ trailers, raw_trailers = self._headers_parser.parse_headers(
949
+ self._trailer_lines
950
+ )
951
+ finally:
952
+ self._trailer_lines.clear()
953
+ self.payload.feed_eof()
954
+ return True, chunk
955
+
956
+ # Read all bytes until eof
957
+ elif self._type == ParseState.PARSE_UNTIL_EOF:
958
+ self.payload.feed_data(chunk, len(chunk))
959
+
960
+ return False, b""
961
+
962
+
963
+ class DeflateBuffer:
964
+ """DeflateStream decompress stream and feed data into specified stream."""
965
+
966
+ decompressor: Any
967
+
968
+ def __init__(
969
+ self,
970
+ out: StreamReader,
971
+ encoding: Optional[str],
972
+ max_decompress_size: int = DEFAULT_MAX_DECOMPRESS_SIZE,
973
+ ) -> None:
974
+ self.out = out
975
+ self.size = 0
976
+ out.total_compressed_bytes = self.size
977
+ self.encoding = encoding
978
+ self._started_decoding = False
979
+
980
+ self.decompressor: Union[BrotliDecompressor, ZLibDecompressor, ZSTDDecompressor]
981
+ if encoding == "br":
982
+ if not HAS_BROTLI: # pragma: no cover
983
+ raise ContentEncodingError(
984
+ "Can not decode content-encoding: brotli (br). "
985
+ "Please install `Brotli`"
986
+ )
987
+ self.decompressor = BrotliDecompressor()
988
+ elif encoding == "zstd":
989
+ if not HAS_ZSTD:
990
+ raise ContentEncodingError(
991
+ "Can not decode content-encoding: zstandard (zstd). "
992
+ "Please install `backports.zstd`"
993
+ )
994
+ self.decompressor = ZSTDDecompressor()
995
+ else:
996
+ self.decompressor = ZLibDecompressor(encoding=encoding)
997
+
998
+ self._max_decompress_size = max_decompress_size
999
+
1000
+ def set_exception(
1001
+ self,
1002
+ exc: BaseException,
1003
+ exc_cause: BaseException = _EXC_SENTINEL,
1004
+ ) -> None:
1005
+ set_exception(self.out, exc, exc_cause)
1006
+
1007
+ def feed_data(self, chunk: bytes, size: int) -> None:
1008
+ if not size:
1009
+ return
1010
+
1011
+ self.size += size
1012
+ self.out.total_compressed_bytes = self.size
1013
+
1014
+ # RFC1950
1015
+ # bits 0..3 = CM = 0b1000 = 8 = "deflate"
1016
+ # bits 4..7 = CINFO = 1..7 = windows size.
1017
+ if (
1018
+ not self._started_decoding
1019
+ and self.encoding == "deflate"
1020
+ and chunk[0] & 0xF != 8
1021
+ ):
1022
+ # Change the decoder to decompress incorrectly compressed data
1023
+ # Actually we should issue a warning about non-RFC-compliant data.
1024
+ self.decompressor = ZLibDecompressor(
1025
+ encoding=self.encoding, suppress_deflate_header=True
1026
+ )
1027
+
1028
+ try:
1029
+ # Decompress with limit + 1 so we can detect if output exceeds limit
1030
+ chunk = self.decompressor.decompress_sync(
1031
+ chunk, max_length=self._max_decompress_size + 1
1032
+ )
1033
+ except Exception:
1034
+ raise ContentEncodingError(
1035
+ "Can not decode content-encoding: %s" % self.encoding
1036
+ )
1037
+
1038
+ self._started_decoding = True
1039
+
1040
+ # Check if decompression limit was exceeded
1041
+ if len(chunk) > self._max_decompress_size:
1042
+ raise DecompressSizeError(
1043
+ "Decompressed data exceeds the configured limit of %d bytes"
1044
+ % self._max_decompress_size
1045
+ )
1046
+
1047
+ if chunk:
1048
+ self.out.feed_data(chunk, len(chunk))
1049
+
1050
+ def feed_eof(self) -> None:
1051
+ chunk = self.decompressor.flush()
1052
+
1053
+ if chunk or self.size > 0:
1054
+ self.out.feed_data(chunk, len(chunk))
1055
+ if self.encoding == "deflate" and not self.decompressor.eof:
1056
+ raise ContentEncodingError("deflate")
1057
+
1058
+ self.out.feed_eof()
1059
+
1060
+ def begin_http_chunk_receiving(self) -> None:
1061
+ self.out.begin_http_chunk_receiving()
1062
+
1063
+ def end_http_chunk_receiving(self) -> None:
1064
+ self.out.end_http_chunk_receiving()
1065
+
1066
+
1067
+ HttpRequestParserPy = HttpRequestParser
1068
+ HttpResponseParserPy = HttpResponseParser
1069
+ RawRequestMessagePy = RawRequestMessage
1070
+ RawResponseMessagePy = RawResponseMessage
1071
+
1072
+ try:
1073
+ if not NO_EXTENSIONS:
1074
+ from ._http_parser import ( # type: ignore[import-not-found,no-redef]
1075
+ HttpRequestParser,
1076
+ HttpResponseParser,
1077
+ RawRequestMessage,
1078
+ RawResponseMessage,
1079
+ )
1080
+
1081
+ HttpRequestParserC = HttpRequestParser
1082
+ HttpResponseParserC = HttpResponseParser
1083
+ RawRequestMessageC = RawRequestMessage
1084
+ RawResponseMessageC = RawResponseMessage
1085
+ except ImportError: # pragma: no cover
1086
+ pass
aiohttp/http_writer.py ADDED
@@ -0,0 +1,378 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Http related parsers and protocol."""
2
+
3
+ import asyncio
4
+ import sys
5
+ from typing import ( # noqa
6
+ TYPE_CHECKING,
7
+ Any,
8
+ Awaitable,
9
+ Callable,
10
+ Iterable,
11
+ List,
12
+ NamedTuple,
13
+ Optional,
14
+ Union,
15
+ )
16
+
17
+ from multidict import CIMultiDict
18
+
19
+ from .abc import AbstractStreamWriter
20
+ from .base_protocol import BaseProtocol
21
+ from .client_exceptions import ClientConnectionResetError
22
+ from .compression_utils import ZLibCompressor
23
+ from .helpers import NO_EXTENSIONS
24
+
25
+ __all__ = ("StreamWriter", "HttpVersion", "HttpVersion10", "HttpVersion11")
26
+
27
+
28
+ MIN_PAYLOAD_FOR_WRITELINES = 2048
29
+ IS_PY313_BEFORE_313_2 = (3, 13, 0) <= sys.version_info < (3, 13, 2)
30
+ IS_PY_BEFORE_312_9 = sys.version_info < (3, 12, 9)
31
+ SKIP_WRITELINES = IS_PY313_BEFORE_313_2 or IS_PY_BEFORE_312_9
32
+ # writelines is not safe for use
33
+ # on Python 3.12+ until 3.12.9
34
+ # on Python 3.13+ until 3.13.2
35
+ # and on older versions it not any faster than write
36
+ # CVE-2024-12254: https://github.com/python/cpython/pull/127656
37
+
38
+
39
+ class HttpVersion(NamedTuple):
40
+ major: int
41
+ minor: int
42
+
43
+
44
+ HttpVersion10 = HttpVersion(1, 0)
45
+ HttpVersion11 = HttpVersion(1, 1)
46
+
47
+
48
+ _T_OnChunkSent = Optional[Callable[[bytes], Awaitable[None]]]
49
+ _T_OnHeadersSent = Optional[Callable[["CIMultiDict[str]"], Awaitable[None]]]
50
+
51
+
52
+ class StreamWriter(AbstractStreamWriter):
53
+
54
+ length: Optional[int] = None
55
+ chunked: bool = False
56
+ _eof: bool = False
57
+ _compress: Optional[ZLibCompressor] = None
58
+
59
+ def __init__(
60
+ self,
61
+ protocol: BaseProtocol,
62
+ loop: asyncio.AbstractEventLoop,
63
+ on_chunk_sent: _T_OnChunkSent = None,
64
+ on_headers_sent: _T_OnHeadersSent = None,
65
+ ) -> None:
66
+ self._protocol = protocol
67
+ self.loop = loop
68
+ self._on_chunk_sent: _T_OnChunkSent = on_chunk_sent
69
+ self._on_headers_sent: _T_OnHeadersSent = on_headers_sent
70
+ self._headers_buf: Optional[bytes] = None
71
+ self._headers_written: bool = False
72
+
73
+ @property
74
+ def transport(self) -> Optional[asyncio.Transport]:
75
+ return self._protocol.transport
76
+
77
+ @property
78
+ def protocol(self) -> BaseProtocol:
79
+ return self._protocol
80
+
81
+ def enable_chunking(self) -> None:
82
+ self.chunked = True
83
+
84
+ def enable_compression(
85
+ self, encoding: str = "deflate", strategy: Optional[int] = None
86
+ ) -> None:
87
+ self._compress = ZLibCompressor(encoding=encoding, strategy=strategy)
88
+
89
+ def _write(self, chunk: Union[bytes, bytearray, memoryview]) -> None:
90
+ size = len(chunk)
91
+ self.buffer_size += size
92
+ self.output_size += size
93
+ transport = self._protocol.transport
94
+ if transport is None or transport.is_closing():
95
+ raise ClientConnectionResetError("Cannot write to closing transport")
96
+ transport.write(chunk)
97
+
98
+ def _writelines(self, chunks: Iterable[bytes]) -> None:
99
+ size = 0
100
+ for chunk in chunks:
101
+ size += len(chunk)
102
+ self.buffer_size += size
103
+ self.output_size += size
104
+ transport = self._protocol.transport
105
+ if transport is None or transport.is_closing():
106
+ raise ClientConnectionResetError("Cannot write to closing transport")
107
+ if SKIP_WRITELINES or size < MIN_PAYLOAD_FOR_WRITELINES:
108
+ transport.write(b"".join(chunks))
109
+ else:
110
+ transport.writelines(chunks)
111
+
112
+ def _write_chunked_payload(
113
+ self, chunk: Union[bytes, bytearray, "memoryview[int]", "memoryview[bytes]"]
114
+ ) -> None:
115
+ """Write a chunk with proper chunked encoding."""
116
+ chunk_len_pre = f"{len(chunk):x}\r\n".encode("ascii")
117
+ self._writelines((chunk_len_pre, chunk, b"\r\n"))
118
+
119
+ def _send_headers_with_payload(
120
+ self,
121
+ chunk: Union[bytes, bytearray, "memoryview[int]", "memoryview[bytes]"],
122
+ is_eof: bool,
123
+ ) -> None:
124
+ """Send buffered headers with payload, coalescing into single write."""
125
+ # Mark headers as written
126
+ self._headers_written = True
127
+ headers_buf = self._headers_buf
128
+ self._headers_buf = None
129
+
130
+ if TYPE_CHECKING:
131
+ # Safe because callers (write() and write_eof()) only invoke this method
132
+ # after checking that self._headers_buf is truthy
133
+ assert headers_buf is not None
134
+
135
+ if not self.chunked:
136
+ # Non-chunked: coalesce headers with body
137
+ if chunk:
138
+ self._writelines((headers_buf, chunk))
139
+ else:
140
+ self._write(headers_buf)
141
+ return
142
+
143
+ # Coalesce headers with chunked data
144
+ if chunk:
145
+ chunk_len_pre = f"{len(chunk):x}\r\n".encode("ascii")
146
+ if is_eof:
147
+ self._writelines((headers_buf, chunk_len_pre, chunk, b"\r\n0\r\n\r\n"))
148
+ else:
149
+ self._writelines((headers_buf, chunk_len_pre, chunk, b"\r\n"))
150
+ elif is_eof:
151
+ self._writelines((headers_buf, b"0\r\n\r\n"))
152
+ else:
153
+ self._write(headers_buf)
154
+
155
+ async def write(
156
+ self,
157
+ chunk: Union[bytes, bytearray, memoryview],
158
+ *,
159
+ drain: bool = True,
160
+ LIMIT: int = 0x10000,
161
+ ) -> None:
162
+ """
163
+ Writes chunk of data to a stream.
164
+
165
+ write_eof() indicates end of stream.
166
+ writer can't be used after write_eof() method being called.
167
+ write() return drain future.
168
+ """
169
+ if self._on_chunk_sent is not None:
170
+ await self._on_chunk_sent(chunk)
171
+
172
+ if isinstance(chunk, memoryview):
173
+ if chunk.nbytes != len(chunk):
174
+ # just reshape it
175
+ chunk = chunk.cast("c")
176
+
177
+ if self._compress is not None:
178
+ chunk = await self._compress.compress(chunk)
179
+ if not chunk:
180
+ return
181
+
182
+ if self.length is not None:
183
+ chunk_len = len(chunk)
184
+ if self.length >= chunk_len:
185
+ self.length = self.length - chunk_len
186
+ else:
187
+ chunk = chunk[: self.length]
188
+ self.length = 0
189
+ if not chunk:
190
+ return
191
+
192
+ # Handle buffered headers for small payload optimization
193
+ if self._headers_buf and not self._headers_written:
194
+ self._send_headers_with_payload(chunk, False)
195
+ if drain and self.buffer_size > LIMIT:
196
+ self.buffer_size = 0
197
+ await self.drain()
198
+ return
199
+
200
+ if chunk:
201
+ if self.chunked:
202
+ self._write_chunked_payload(chunk)
203
+ else:
204
+ self._write(chunk)
205
+
206
+ if drain and self.buffer_size > LIMIT:
207
+ self.buffer_size = 0
208
+ await self.drain()
209
+
210
+ async def write_headers(
211
+ self, status_line: str, headers: "CIMultiDict[str]"
212
+ ) -> None:
213
+ """Write headers to the stream."""
214
+ if self._on_headers_sent is not None:
215
+ await self._on_headers_sent(headers)
216
+ # status + headers
217
+ buf = _serialize_headers(status_line, headers)
218
+ self._headers_written = False
219
+ self._headers_buf = buf
220
+
221
+ def send_headers(self) -> None:
222
+ """Force sending buffered headers if not already sent."""
223
+ if not self._headers_buf or self._headers_written:
224
+ return
225
+
226
+ self._headers_written = True
227
+ headers_buf = self._headers_buf
228
+ self._headers_buf = None
229
+
230
+ if TYPE_CHECKING:
231
+ # Safe because we only enter this block when self._headers_buf is truthy
232
+ assert headers_buf is not None
233
+
234
+ self._write(headers_buf)
235
+
236
+ def set_eof(self) -> None:
237
+ """Indicate that the message is complete."""
238
+ if self._eof:
239
+ return
240
+
241
+ # If headers haven't been sent yet, send them now
242
+ # This handles the case where there's no body at all
243
+ if self._headers_buf and not self._headers_written:
244
+ self._headers_written = True
245
+ headers_buf = self._headers_buf
246
+ self._headers_buf = None
247
+
248
+ if TYPE_CHECKING:
249
+ # Safe because we only enter this block when self._headers_buf is truthy
250
+ assert headers_buf is not None
251
+
252
+ # Combine headers and chunked EOF marker in a single write
253
+ if self.chunked:
254
+ self._writelines((headers_buf, b"0\r\n\r\n"))
255
+ else:
256
+ self._write(headers_buf)
257
+ elif self.chunked and self._headers_written:
258
+ # Headers already sent, just send the final chunk marker
259
+ self._write(b"0\r\n\r\n")
260
+
261
+ self._eof = True
262
+
263
+ async def write_eof(self, chunk: bytes = b"") -> None:
264
+ if self._eof:
265
+ return
266
+
267
+ if chunk and self._on_chunk_sent is not None:
268
+ await self._on_chunk_sent(chunk)
269
+
270
+ # Handle body/compression
271
+ if self._compress:
272
+ chunks: List[bytes] = []
273
+ chunks_len = 0
274
+ if chunk and (compressed_chunk := await self._compress.compress(chunk)):
275
+ chunks_len = len(compressed_chunk)
276
+ chunks.append(compressed_chunk)
277
+
278
+ flush_chunk = self._compress.flush()
279
+ chunks_len += len(flush_chunk)
280
+ chunks.append(flush_chunk)
281
+ assert chunks_len
282
+
283
+ # Send buffered headers with compressed data if not yet sent
284
+ if self._headers_buf and not self._headers_written:
285
+ self._headers_written = True
286
+ headers_buf = self._headers_buf
287
+ self._headers_buf = None
288
+
289
+ if self.chunked:
290
+ # Coalesce headers with compressed chunked data
291
+ chunk_len_pre = f"{chunks_len:x}\r\n".encode("ascii")
292
+ self._writelines(
293
+ (headers_buf, chunk_len_pre, *chunks, b"\r\n0\r\n\r\n")
294
+ )
295
+ else:
296
+ # Coalesce headers with compressed data
297
+ self._writelines((headers_buf, *chunks))
298
+ await self.drain()
299
+ self._eof = True
300
+ return
301
+
302
+ # Headers already sent, just write compressed data
303
+ if self.chunked:
304
+ chunk_len_pre = f"{chunks_len:x}\r\n".encode("ascii")
305
+ self._writelines((chunk_len_pre, *chunks, b"\r\n0\r\n\r\n"))
306
+ elif len(chunks) > 1:
307
+ self._writelines(chunks)
308
+ else:
309
+ self._write(chunks[0])
310
+ await self.drain()
311
+ self._eof = True
312
+ return
313
+
314
+ # No compression - send buffered headers if not yet sent
315
+ if self._headers_buf and not self._headers_written:
316
+ # Use helper to send headers with payload
317
+ self._send_headers_with_payload(chunk, True)
318
+ await self.drain()
319
+ self._eof = True
320
+ return
321
+
322
+ # Handle remaining body
323
+ if self.chunked:
324
+ if chunk:
325
+ # Write final chunk with EOF marker
326
+ self._writelines(
327
+ (f"{len(chunk):x}\r\n".encode("ascii"), chunk, b"\r\n0\r\n\r\n")
328
+ )
329
+ else:
330
+ self._write(b"0\r\n\r\n")
331
+ await self.drain()
332
+ self._eof = True
333
+ return
334
+
335
+ if chunk:
336
+ self._write(chunk)
337
+ await self.drain()
338
+
339
+ self._eof = True
340
+
341
+ async def drain(self) -> None:
342
+ """Flush the write buffer.
343
+
344
+ The intended use is to write
345
+
346
+ await w.write(data)
347
+ await w.drain()
348
+ """
349
+ protocol = self._protocol
350
+ if protocol.transport is not None and protocol._paused:
351
+ await protocol._drain_helper()
352
+
353
+
354
+ def _safe_header(string: str) -> str:
355
+ if "\r" in string or "\n" in string:
356
+ raise ValueError(
357
+ "Newline or carriage return detected in headers. "
358
+ "Potential header injection attack."
359
+ )
360
+ return string
361
+
362
+
363
+ def _py_serialize_headers(status_line: str, headers: "CIMultiDict[str]") -> bytes:
364
+ headers_gen = (_safe_header(k) + ": " + _safe_header(v) for k, v in headers.items())
365
+ line = status_line + "\r\n" + "\r\n".join(headers_gen) + "\r\n\r\n"
366
+ return line.encode("utf-8")
367
+
368
+
369
+ _serialize_headers = _py_serialize_headers
370
+
371
+ try:
372
+ import aiohttp._http_writer as _http_writer # type: ignore[import-not-found]
373
+
374
+ _c_serialize_headers = _http_writer._serialize_headers
375
+ if not NO_EXTENSIONS:
376
+ _serialize_headers = _c_serialize_headers
377
+ except ImportError:
378
+ pass
aiohttp/payload_streamer.py ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Payload implementation for coroutines as data provider.
3
+
4
+ As a simple case, you can upload data from file::
5
+
6
+ @aiohttp.streamer
7
+ async def file_sender(writer, file_name=None):
8
+ with open(file_name, 'rb') as f:
9
+ chunk = f.read(2**16)
10
+ while chunk:
11
+ await writer.write(chunk)
12
+
13
+ chunk = f.read(2**16)
14
+
15
+ Then you can use `file_sender` like this:
16
+
17
+ async with session.post('http://httpbin.org/post',
18
+ data=file_sender(file_name='huge_file')) as resp:
19
+ print(await resp.text())
20
+
21
+ ..note:: Coroutine must accept `writer` as first argument
22
+
23
+ """
24
+
25
+ import types
26
+ import warnings
27
+ from typing import Any, Awaitable, Callable, Dict, Tuple
28
+
29
+ from .abc import AbstractStreamWriter
30
+ from .payload import Payload, payload_type
31
+
32
+ __all__ = ("streamer",)
33
+
34
+
35
+ class _stream_wrapper:
36
+ def __init__(
37
+ self,
38
+ coro: Callable[..., Awaitable[None]],
39
+ args: Tuple[Any, ...],
40
+ kwargs: Dict[str, Any],
41
+ ) -> None:
42
+ self.coro = types.coroutine(coro)
43
+ self.args = args
44
+ self.kwargs = kwargs
45
+
46
+ async def __call__(self, writer: AbstractStreamWriter) -> None:
47
+ await self.coro(writer, *self.args, **self.kwargs)
48
+
49
+
50
+ class streamer:
51
+ def __init__(self, coro: Callable[..., Awaitable[None]]) -> None:
52
+ warnings.warn(
53
+ "@streamer is deprecated, use async generators instead",
54
+ DeprecationWarning,
55
+ stacklevel=2,
56
+ )
57
+ self.coro = coro
58
+
59
+ def __call__(self, *args: Any, **kwargs: Any) -> _stream_wrapper:
60
+ return _stream_wrapper(self.coro, args, kwargs)
61
+
62
+
63
+ @payload_type(_stream_wrapper)
64
+ class StreamWrapperPayload(Payload):
65
+ async def write(self, writer: AbstractStreamWriter) -> None:
66
+ await self._value(writer)
67
+
68
+ def decode(self, encoding: str = "utf-8", errors: str = "strict") -> str:
69
+ raise TypeError("Unable to decode.")
70
+
71
+
72
+ @payload_type(streamer)
73
+ class StreamPayload(StreamWrapperPayload):
74
+ def __init__(self, value: Any, *args: Any, **kwargs: Any) -> None:
75
+ super().__init__(value(), *args, **kwargs)
76
+
77
+ async def write(self, writer: AbstractStreamWriter) -> None:
78
+ await self._value(writer)
aiohttp/py.typed ADDED
@@ -0,0 +1 @@
 
 
1
+ Marker
aiohttp/pytest_plugin.py ADDED
@@ -0,0 +1,444 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import contextlib
3
+ import inspect
4
+ import warnings
5
+ from typing import (
6
+ Any,
7
+ Awaitable,
8
+ Callable,
9
+ Dict,
10
+ Iterator,
11
+ Optional,
12
+ Protocol,
13
+ Union,
14
+ overload,
15
+ )
16
+
17
+ import pytest
18
+
19
+ from .test_utils import (
20
+ BaseTestServer,
21
+ RawTestServer,
22
+ TestClient,
23
+ TestServer,
24
+ loop_context,
25
+ setup_test_loop,
26
+ teardown_test_loop,
27
+ unused_port as _unused_port,
28
+ )
29
+ from .web import Application, BaseRequest, Request
30
+ from .web_protocol import _RequestHandler
31
+
32
+ try:
33
+ import uvloop
34
+ except ImportError: # pragma: no cover
35
+ uvloop = None # type: ignore[assignment]
36
+
37
+
38
+ class AiohttpClient(Protocol):
39
+ @overload
40
+ async def __call__(
41
+ self,
42
+ __param: Application,
43
+ *,
44
+ server_kwargs: Optional[Dict[str, Any]] = None,
45
+ **kwargs: Any,
46
+ ) -> TestClient[Request, Application]: ...
47
+ @overload
48
+ async def __call__(
49
+ self,
50
+ __param: BaseTestServer,
51
+ *,
52
+ server_kwargs: Optional[Dict[str, Any]] = None,
53
+ **kwargs: Any,
54
+ ) -> TestClient[BaseRequest, None]: ...
55
+
56
+
57
+ class AiohttpServer(Protocol):
58
+ def __call__(
59
+ self, app: Application, *, port: Optional[int] = None, **kwargs: Any
60
+ ) -> Awaitable[TestServer]: ...
61
+
62
+
63
+ class AiohttpRawServer(Protocol):
64
+ def __call__(
65
+ self, handler: _RequestHandler, *, port: Optional[int] = None, **kwargs: Any
66
+ ) -> Awaitable[RawTestServer]: ...
67
+
68
+
69
+ def pytest_addoption(parser): # type: ignore[no-untyped-def]
70
+ parser.addoption(
71
+ "--aiohttp-fast",
72
+ action="store_true",
73
+ default=False,
74
+ help="run tests faster by disabling extra checks",
75
+ )
76
+ parser.addoption(
77
+ "--aiohttp-loop",
78
+ action="store",
79
+ default="pyloop",
80
+ help="run tests with specific loop: pyloop, uvloop or all",
81
+ )
82
+ parser.addoption(
83
+ "--aiohttp-enable-loop-debug",
84
+ action="store_true",
85
+ default=False,
86
+ help="enable event loop debug mode",
87
+ )
88
+
89
+
90
+ def pytest_fixture_setup(fixturedef): # type: ignore[no-untyped-def]
91
+ """Set up pytest fixture.
92
+
93
+ Allow fixtures to be coroutines. Run coroutine fixtures in an event loop.
94
+ """
95
+ func = fixturedef.func
96
+
97
+ if inspect.isasyncgenfunction(func):
98
+ # async generator fixture
99
+ is_async_gen = True
100
+ elif inspect.iscoroutinefunction(func):
101
+ # regular async fixture
102
+ is_async_gen = False
103
+ else:
104
+ # not an async fixture, nothing to do
105
+ return
106
+
107
+ strip_request = False
108
+ if "request" not in fixturedef.argnames:
109
+ fixturedef.argnames += ("request",)
110
+ strip_request = True
111
+
112
+ def wrapper(*args, **kwargs): # type: ignore[no-untyped-def]
113
+ request = kwargs["request"]
114
+ if strip_request:
115
+ del kwargs["request"]
116
+
117
+ # if neither the fixture nor the test use the 'loop' fixture,
118
+ # 'getfixturevalue' will fail because the test is not parameterized
119
+ # (this can be removed someday if 'loop' is no longer parameterized)
120
+ if "loop" not in request.fixturenames:
121
+ raise Exception(
122
+ "Asynchronous fixtures must depend on the 'loop' fixture or "
123
+ "be used in tests depending from it."
124
+ )
125
+
126
+ _loop = request.getfixturevalue("loop")
127
+
128
+ if is_async_gen:
129
+ # for async generators, we need to advance the generator once,
130
+ # then advance it again in a finalizer
131
+ gen = func(*args, **kwargs)
132
+
133
+ def finalizer(): # type: ignore[no-untyped-def]
134
+ try:
135
+ return _loop.run_until_complete(gen.__anext__())
136
+ except StopAsyncIteration:
137
+ pass
138
+
139
+ request.addfinalizer(finalizer)
140
+ return _loop.run_until_complete(gen.__anext__())
141
+ else:
142
+ return _loop.run_until_complete(func(*args, **kwargs))
143
+
144
+ fixturedef.func = wrapper
145
+
146
+
147
+ @pytest.fixture
148
+ def fast(request): # type: ignore[no-untyped-def]
149
+ """--fast config option"""
150
+ return request.config.getoption("--aiohttp-fast")
151
+
152
+
153
+ @pytest.fixture
154
+ def loop_debug(request): # type: ignore[no-untyped-def]
155
+ """--enable-loop-debug config option"""
156
+ return request.config.getoption("--aiohttp-enable-loop-debug")
157
+
158
+
159
+ @contextlib.contextmanager
160
+ def _runtime_warning_context(): # type: ignore[no-untyped-def]
161
+ """Context manager which checks for RuntimeWarnings.
162
+
163
+ This exists specifically to
164
+ avoid "coroutine 'X' was never awaited" warnings being missed.
165
+
166
+ If RuntimeWarnings occur in the context a RuntimeError is raised.
167
+ """
168
+ with warnings.catch_warnings(record=True) as _warnings:
169
+ yield
170
+ rw = [
171
+ "{w.filename}:{w.lineno}:{w.message}".format(w=w)
172
+ for w in _warnings
173
+ if w.category == RuntimeWarning
174
+ ]
175
+ if rw:
176
+ raise RuntimeError(
177
+ "{} Runtime Warning{},\n{}".format(
178
+ len(rw), "" if len(rw) == 1 else "s", "\n".join(rw)
179
+ )
180
+ )
181
+
182
+
183
+ @contextlib.contextmanager
184
+ def _passthrough_loop_context(loop, fast=False): # type: ignore[no-untyped-def]
185
+ """Passthrough loop context.
186
+
187
+ Sets up and tears down a loop unless one is passed in via the loop
188
+ argument when it's passed straight through.
189
+ """
190
+ if loop:
191
+ # loop already exists, pass it straight through
192
+ yield loop
193
+ else:
194
+ # this shadows loop_context's standard behavior
195
+ loop = setup_test_loop()
196
+ yield loop
197
+ teardown_test_loop(loop, fast=fast)
198
+
199
+
200
+ def pytest_pycollect_makeitem(collector, name, obj): # type: ignore[no-untyped-def]
201
+ """Fix pytest collecting for coroutines."""
202
+ if collector.funcnamefilter(name) and inspect.iscoroutinefunction(obj):
203
+ return list(collector._genfunctions(name, obj))
204
+
205
+
206
+ def pytest_pyfunc_call(pyfuncitem): # type: ignore[no-untyped-def]
207
+ """Run coroutines in an event loop instead of a normal function call."""
208
+ fast = pyfuncitem.config.getoption("--aiohttp-fast")
209
+ if inspect.iscoroutinefunction(pyfuncitem.function):
210
+ existing_loop = (
211
+ pyfuncitem.funcargs.get("proactor_loop")
212
+ or pyfuncitem.funcargs.get("selector_loop")
213
+ or pyfuncitem.funcargs.get("uvloop_loop")
214
+ or pyfuncitem.funcargs.get("loop", None)
215
+ )
216
+
217
+ with _runtime_warning_context():
218
+ with _passthrough_loop_context(existing_loop, fast=fast) as _loop:
219
+ testargs = {
220
+ arg: pyfuncitem.funcargs[arg]
221
+ for arg in pyfuncitem._fixtureinfo.argnames
222
+ }
223
+ _loop.run_until_complete(pyfuncitem.obj(**testargs))
224
+
225
+ return True
226
+
227
+
228
+ def pytest_generate_tests(metafunc): # type: ignore[no-untyped-def]
229
+ if "loop_factory" not in metafunc.fixturenames:
230
+ return
231
+
232
+ loops = metafunc.config.option.aiohttp_loop
233
+ avail_factories: dict[str, Callable[[], asyncio.AbstractEventLoop]]
234
+ avail_factories = {"pyloop": asyncio.new_event_loop}
235
+
236
+ if uvloop is not None: # pragma: no cover
237
+ avail_factories["uvloop"] = uvloop.new_event_loop
238
+
239
+ if loops == "all":
240
+ loops = "pyloop,uvloop?"
241
+
242
+ factories = {} # type: ignore[var-annotated]
243
+ for name in loops.split(","):
244
+ required = not name.endswith("?")
245
+ name = name.strip(" ?")
246
+ if name not in avail_factories: # pragma: no cover
247
+ if required:
248
+ raise ValueError(
249
+ "Unknown loop '%s', available loops: %s"
250
+ % (name, list(factories.keys()))
251
+ )
252
+ else:
253
+ continue
254
+ factories[name] = avail_factories[name]
255
+ metafunc.parametrize(
256
+ "loop_factory", list(factories.values()), ids=list(factories.keys())
257
+ )
258
+
259
+
260
+ @pytest.fixture
261
+ def loop(
262
+ loop_factory: Callable[[], asyncio.AbstractEventLoop],
263
+ fast: bool,
264
+ loop_debug: bool,
265
+ ) -> Iterator[asyncio.AbstractEventLoop]:
266
+ """Return an instance of the event loop."""
267
+ with loop_context(loop_factory, fast=fast) as _loop:
268
+ if loop_debug:
269
+ _loop.set_debug(True) # pragma: no cover
270
+ asyncio.set_event_loop(_loop)
271
+ yield _loop
272
+
273
+
274
+ @pytest.fixture
275
+ def proactor_loop() -> Iterator[asyncio.AbstractEventLoop]:
276
+ factory = asyncio.ProactorEventLoop # type: ignore[attr-defined]
277
+
278
+ with loop_context(factory) as _loop:
279
+ asyncio.set_event_loop(_loop)
280
+ yield _loop
281
+
282
+
283
+ @pytest.fixture
284
+ def unused_port(aiohttp_unused_port: Callable[[], int]) -> Callable[[], int]:
285
+ warnings.warn(
286
+ "Deprecated, use aiohttp_unused_port fixture instead",
287
+ DeprecationWarning,
288
+ stacklevel=2,
289
+ )
290
+ return aiohttp_unused_port
291
+
292
+
293
+ @pytest.fixture
294
+ def aiohttp_unused_port() -> Callable[[], int]:
295
+ """Return a port that is unused on the current host."""
296
+ return _unused_port
297
+
298
+
299
+ @pytest.fixture
300
+ def aiohttp_server(loop: asyncio.AbstractEventLoop) -> Iterator[AiohttpServer]:
301
+ """Factory to create a TestServer instance, given an app.
302
+
303
+ aiohttp_server(app, **kwargs)
304
+ """
305
+ servers = []
306
+
307
+ async def go(
308
+ app: Application,
309
+ *,
310
+ host: str = "127.0.0.1",
311
+ port: Optional[int] = None,
312
+ **kwargs: Any,
313
+ ) -> TestServer:
314
+ server = TestServer(app, host=host, port=port)
315
+ await server.start_server(loop=loop, **kwargs)
316
+ servers.append(server)
317
+ return server
318
+
319
+ yield go
320
+
321
+ async def finalize() -> None:
322
+ while servers:
323
+ await servers.pop().close()
324
+
325
+ loop.run_until_complete(finalize())
326
+
327
+
328
+ @pytest.fixture
329
+ def test_server(aiohttp_server): # type: ignore[no-untyped-def] # pragma: no cover
330
+ warnings.warn(
331
+ "Deprecated, use aiohttp_server fixture instead",
332
+ DeprecationWarning,
333
+ stacklevel=2,
334
+ )
335
+ return aiohttp_server
336
+
337
+
338
+ @pytest.fixture
339
+ def aiohttp_raw_server(loop: asyncio.AbstractEventLoop) -> Iterator[AiohttpRawServer]:
340
+ """Factory to create a RawTestServer instance, given a web handler.
341
+
342
+ aiohttp_raw_server(handler, **kwargs)
343
+ """
344
+ servers = []
345
+
346
+ async def go(
347
+ handler: _RequestHandler, *, port: Optional[int] = None, **kwargs: Any
348
+ ) -> RawTestServer:
349
+ server = RawTestServer(handler, port=port)
350
+ await server.start_server(loop=loop, **kwargs)
351
+ servers.append(server)
352
+ return server
353
+
354
+ yield go
355
+
356
+ async def finalize() -> None:
357
+ while servers:
358
+ await servers.pop().close()
359
+
360
+ loop.run_until_complete(finalize())
361
+
362
+
363
+ @pytest.fixture
364
+ def raw_test_server( # type: ignore[no-untyped-def] # pragma: no cover
365
+ aiohttp_raw_server,
366
+ ):
367
+ warnings.warn(
368
+ "Deprecated, use aiohttp_raw_server fixture instead",
369
+ DeprecationWarning,
370
+ stacklevel=2,
371
+ )
372
+ return aiohttp_raw_server
373
+
374
+
375
+ @pytest.fixture
376
+ def aiohttp_client(loop: asyncio.AbstractEventLoop) -> Iterator[AiohttpClient]:
377
+ """Factory to create a TestClient instance.
378
+
379
+ aiohttp_client(app, **kwargs)
380
+ aiohttp_client(server, **kwargs)
381
+ aiohttp_client(raw_server, **kwargs)
382
+ """
383
+ clients = []
384
+
385
+ @overload
386
+ async def go(
387
+ __param: Application,
388
+ *,
389
+ server_kwargs: Optional[Dict[str, Any]] = None,
390
+ **kwargs: Any,
391
+ ) -> TestClient[Request, Application]: ...
392
+
393
+ @overload
394
+ async def go(
395
+ __param: BaseTestServer,
396
+ *,
397
+ server_kwargs: Optional[Dict[str, Any]] = None,
398
+ **kwargs: Any,
399
+ ) -> TestClient[BaseRequest, None]: ...
400
+
401
+ async def go(
402
+ __param: Union[Application, BaseTestServer],
403
+ *args: Any,
404
+ server_kwargs: Optional[Dict[str, Any]] = None,
405
+ **kwargs: Any,
406
+ ) -> TestClient[Any, Any]:
407
+ if isinstance(__param, Callable) and not isinstance( # type: ignore[arg-type]
408
+ __param, (Application, BaseTestServer)
409
+ ):
410
+ __param = __param(loop, *args, **kwargs)
411
+ kwargs = {}
412
+ else:
413
+ assert not args, "args should be empty"
414
+
415
+ if isinstance(__param, Application):
416
+ server_kwargs = server_kwargs or {}
417
+ server = TestServer(__param, loop=loop, **server_kwargs)
418
+ client = TestClient(server, loop=loop, **kwargs)
419
+ elif isinstance(__param, BaseTestServer):
420
+ client = TestClient(__param, loop=loop, **kwargs)
421
+ else:
422
+ raise ValueError("Unknown argument type: %r" % type(__param))
423
+
424
+ await client.start_server()
425
+ clients.append(client)
426
+ return client
427
+
428
+ yield go
429
+
430
+ async def finalize() -> None:
431
+ while clients:
432
+ await clients.pop().close()
433
+
434
+ loop.run_until_complete(finalize())
435
+
436
+
437
+ @pytest.fixture
438
+ def test_client(aiohttp_client): # type: ignore[no-untyped-def] # pragma: no cover
439
+ warnings.warn(
440
+ "Deprecated, use aiohttp_client fixture instead",
441
+ DeprecationWarning,
442
+ stacklevel=2,
443
+ )
444
+ return aiohttp_client
aiohttp/resolver.py ADDED
@@ -0,0 +1,274 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import socket
3
+ import weakref
4
+ from typing import Any, Dict, Final, List, Optional, Tuple, Type, Union
5
+
6
+ from .abc import AbstractResolver, ResolveResult
7
+
8
+ __all__ = ("ThreadedResolver", "AsyncResolver", "DefaultResolver")
9
+
10
+
11
+ try:
12
+ import aiodns
13
+
14
+ aiodns_default = hasattr(aiodns.DNSResolver, "getaddrinfo")
15
+ except ImportError: # pragma: no cover
16
+ aiodns = None # type: ignore[assignment]
17
+ aiodns_default = False
18
+
19
+
20
+ _NUMERIC_SOCKET_FLAGS = socket.AI_NUMERICHOST | socket.AI_NUMERICSERV
21
+ _NAME_SOCKET_FLAGS = socket.NI_NUMERICHOST | socket.NI_NUMERICSERV
22
+ _AI_ADDRCONFIG = socket.AI_ADDRCONFIG
23
+ if hasattr(socket, "AI_MASK"):
24
+ _AI_ADDRCONFIG &= socket.AI_MASK
25
+
26
+
27
+ class ThreadedResolver(AbstractResolver):
28
+ """Threaded resolver.
29
+
30
+ Uses an Executor for synchronous getaddrinfo() calls.
31
+ concurrent.futures.ThreadPoolExecutor is used by default.
32
+ """
33
+
34
+ def __init__(self, loop: Optional[asyncio.AbstractEventLoop] = None) -> None:
35
+ self._loop = loop or asyncio.get_running_loop()
36
+
37
+ async def resolve(
38
+ self, host: str, port: int = 0, family: socket.AddressFamily = socket.AF_INET
39
+ ) -> List[ResolveResult]:
40
+ infos = await self._loop.getaddrinfo(
41
+ host,
42
+ port,
43
+ type=socket.SOCK_STREAM,
44
+ family=family,
45
+ flags=_AI_ADDRCONFIG,
46
+ )
47
+
48
+ hosts: List[ResolveResult] = []
49
+ for family, _, proto, _, address in infos:
50
+ if family == socket.AF_INET6:
51
+ if len(address) < 3:
52
+ # IPv6 is not supported by Python build,
53
+ # or IPv6 is not enabled in the host
54
+ continue
55
+ if address[3]:
56
+ # This is essential for link-local IPv6 addresses.
57
+ # LL IPv6 is a VERY rare case. Strictly speaking, we should use
58
+ # getnameinfo() unconditionally, but performance makes sense.
59
+ resolved_host, _port = await self._loop.getnameinfo(
60
+ address, _NAME_SOCKET_FLAGS
61
+ )
62
+ port = int(_port)
63
+ else:
64
+ resolved_host, port = address[:2]
65
+ else: # IPv4
66
+ assert family == socket.AF_INET
67
+ resolved_host, port = address # type: ignore[misc]
68
+ hosts.append(
69
+ ResolveResult(
70
+ hostname=host,
71
+ host=resolved_host,
72
+ port=port,
73
+ family=family,
74
+ proto=proto,
75
+ flags=_NUMERIC_SOCKET_FLAGS,
76
+ )
77
+ )
78
+
79
+ return hosts
80
+
81
+ async def close(self) -> None:
82
+ pass
83
+
84
+
85
+ class AsyncResolver(AbstractResolver):
86
+ """Use the `aiodns` package to make asynchronous DNS lookups"""
87
+
88
+ def __init__(
89
+ self,
90
+ loop: Optional[asyncio.AbstractEventLoop] = None,
91
+ *args: Any,
92
+ **kwargs: Any,
93
+ ) -> None:
94
+ if aiodns is None:
95
+ raise RuntimeError("Resolver requires aiodns library")
96
+
97
+ self._loop = loop or asyncio.get_running_loop()
98
+ self._manager: Optional[_DNSResolverManager] = None
99
+ # If custom args are provided, create a dedicated resolver instance
100
+ # This means each AsyncResolver with custom args gets its own
101
+ # aiodns.DNSResolver instance
102
+ if args or kwargs:
103
+ self._resolver = aiodns.DNSResolver(*args, **kwargs)
104
+ return
105
+ # Use the shared resolver from the manager for default arguments
106
+ self._manager = _DNSResolverManager()
107
+ self._resolver = self._manager.get_resolver(self, self._loop)
108
+
109
+ if not hasattr(self._resolver, "gethostbyname"):
110
+ # aiodns 1.1 is not available, fallback to DNSResolver.query
111
+ self.resolve = self._resolve_with_query # type: ignore
112
+
113
+ async def resolve(
114
+ self, host: str, port: int = 0, family: socket.AddressFamily = socket.AF_INET
115
+ ) -> List[ResolveResult]:
116
+ try:
117
+ resp = await self._resolver.getaddrinfo(
118
+ host,
119
+ port=port,
120
+ type=socket.SOCK_STREAM,
121
+ family=family,
122
+ flags=_AI_ADDRCONFIG,
123
+ )
124
+ except aiodns.error.DNSError as exc:
125
+ msg = exc.args[1] if len(exc.args) >= 1 else "DNS lookup failed"
126
+ raise OSError(None, msg) from exc
127
+ hosts: List[ResolveResult] = []
128
+ for node in resp.nodes:
129
+ address: Union[Tuple[bytes, int], Tuple[bytes, int, int, int]] = node.addr
130
+ family = node.family
131
+ if family == socket.AF_INET6:
132
+ if len(address) > 3 and address[3]:
133
+ # This is essential for link-local IPv6 addresses.
134
+ # LL IPv6 is a VERY rare case. Strictly speaking, we should use
135
+ # getnameinfo() unconditionally, but performance makes sense.
136
+ result = await self._resolver.getnameinfo(
137
+ (address[0].decode("ascii"), *address[1:]),
138
+ _NAME_SOCKET_FLAGS,
139
+ )
140
+ resolved_host = result.node
141
+ else:
142
+ resolved_host = address[0].decode("ascii")
143
+ port = address[1]
144
+ else: # IPv4
145
+ assert family == socket.AF_INET
146
+ resolved_host = address[0].decode("ascii")
147
+ port = address[1]
148
+ hosts.append(
149
+ ResolveResult(
150
+ hostname=host,
151
+ host=resolved_host,
152
+ port=port,
153
+ family=family,
154
+ proto=0,
155
+ flags=_NUMERIC_SOCKET_FLAGS,
156
+ )
157
+ )
158
+
159
+ if not hosts:
160
+ raise OSError(None, "DNS lookup failed")
161
+
162
+ return hosts
163
+
164
+ async def _resolve_with_query(
165
+ self, host: str, port: int = 0, family: int = socket.AF_INET
166
+ ) -> List[Dict[str, Any]]:
167
+ qtype: Final = "AAAA" if family == socket.AF_INET6 else "A"
168
+
169
+ try:
170
+ resp = await self._resolver.query(host, qtype)
171
+ except aiodns.error.DNSError as exc:
172
+ msg = exc.args[1] if len(exc.args) >= 1 else "DNS lookup failed"
173
+ raise OSError(None, msg) from exc
174
+
175
+ hosts = []
176
+ for rr in resp:
177
+ hosts.append(
178
+ {
179
+ "hostname": host,
180
+ "host": rr.host,
181
+ "port": port,
182
+ "family": family,
183
+ "proto": 0,
184
+ "flags": socket.AI_NUMERICHOST,
185
+ }
186
+ )
187
+
188
+ if not hosts:
189
+ raise OSError(None, "DNS lookup failed")
190
+
191
+ return hosts
192
+
193
+ async def close(self) -> None:
194
+ if self._manager:
195
+ # Release the resolver from the manager if using the shared resolver
196
+ self._manager.release_resolver(self, self._loop)
197
+ self._manager = None # Clear reference to manager
198
+ self._resolver = None # type: ignore[assignment] # Clear reference to resolver
199
+ return
200
+ # Otherwise cancel our dedicated resolver
201
+ if self._resolver is not None:
202
+ self._resolver.cancel()
203
+ self._resolver = None # type: ignore[assignment] # Clear reference
204
+
205
+
206
+ class _DNSResolverManager:
207
+ """Manager for aiodns.DNSResolver objects.
208
+
209
+ This class manages shared aiodns.DNSResolver instances
210
+ with no custom arguments across different event loops.
211
+ """
212
+
213
+ _instance: Optional["_DNSResolverManager"] = None
214
+
215
+ def __new__(cls) -> "_DNSResolverManager":
216
+ if cls._instance is None:
217
+ cls._instance = super().__new__(cls)
218
+ cls._instance._init()
219
+ return cls._instance
220
+
221
+ def _init(self) -> None:
222
+ # Use WeakKeyDictionary to allow event loops to be garbage collected
223
+ self._loop_data: weakref.WeakKeyDictionary[
224
+ asyncio.AbstractEventLoop,
225
+ tuple["aiodns.DNSResolver", weakref.WeakSet["AsyncResolver"]],
226
+ ] = weakref.WeakKeyDictionary()
227
+
228
+ def get_resolver(
229
+ self, client: "AsyncResolver", loop: asyncio.AbstractEventLoop
230
+ ) -> "aiodns.DNSResolver":
231
+ """Get or create the shared aiodns.DNSResolver instance for a specific event loop.
232
+
233
+ Args:
234
+ client: The AsyncResolver instance requesting the resolver.
235
+ This is required to track resolver usage.
236
+ loop: The event loop to use for the resolver.
237
+ """
238
+ # Create a new resolver and client set for this loop if it doesn't exist
239
+ if loop not in self._loop_data:
240
+ resolver = aiodns.DNSResolver(loop=loop)
241
+ client_set: weakref.WeakSet["AsyncResolver"] = weakref.WeakSet()
242
+ self._loop_data[loop] = (resolver, client_set)
243
+ else:
244
+ # Get the existing resolver and client set
245
+ resolver, client_set = self._loop_data[loop]
246
+
247
+ # Register this client with the loop
248
+ client_set.add(client)
249
+ return resolver
250
+
251
+ def release_resolver(
252
+ self, client: "AsyncResolver", loop: asyncio.AbstractEventLoop
253
+ ) -> None:
254
+ """Release the resolver for an AsyncResolver client when it's closed.
255
+
256
+ Args:
257
+ client: The AsyncResolver instance to release.
258
+ loop: The event loop the resolver was using.
259
+ """
260
+ # Remove client from its loop's tracking
261
+ current_loop_data = self._loop_data.get(loop)
262
+ if current_loop_data is None:
263
+ return
264
+ resolver, client_set = current_loop_data
265
+ client_set.discard(client)
266
+ # If no more clients for this loop, cancel and remove its resolver
267
+ if not client_set:
268
+ if resolver is not None:
269
+ resolver.cancel()
270
+ del self._loop_data[loop]
271
+
272
+
273
+ _DefaultType = Type[Union[AsyncResolver, ThreadedResolver]]
274
+ DefaultResolver: _DefaultType = AsyncResolver if aiodns_default else ThreadedResolver
aiohttp/tcp_helpers.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Helper methods to tune a TCP connection"""
2
+
3
+ import asyncio
4
+ import socket
5
+ from contextlib import suppress
6
+ from typing import Optional # noqa
7
+
8
+ __all__ = ("tcp_keepalive", "tcp_nodelay")
9
+
10
+
11
+ if hasattr(socket, "SO_KEEPALIVE"):
12
+
13
+ def tcp_keepalive(transport: asyncio.Transport) -> None:
14
+ sock = transport.get_extra_info("socket")
15
+ if sock is not None:
16
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
17
+
18
+ else:
19
+
20
+ def tcp_keepalive(transport: asyncio.Transport) -> None: # pragma: no cover
21
+ pass
22
+
23
+
24
+ def tcp_nodelay(transport: asyncio.Transport, value: bool) -> None:
25
+ sock = transport.get_extra_info("socket")
26
+
27
+ if sock is None:
28
+ return
29
+
30
+ if sock.family not in (socket.AF_INET, socket.AF_INET6):
31
+ return
32
+
33
+ value = bool(value)
34
+
35
+ # socket may be closed already, on windows OSError get raised
36
+ with suppress(OSError):
37
+ sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, value)
aiohttp/typedefs.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ from typing import (
4
+ TYPE_CHECKING,
5
+ Any,
6
+ Awaitable,
7
+ Callable,
8
+ Iterable,
9
+ Mapping,
10
+ Protocol,
11
+ Tuple,
12
+ Union,
13
+ )
14
+
15
+ from multidict import CIMultiDict, CIMultiDictProxy, MultiDict, MultiDictProxy, istr
16
+ from yarl import URL, Query as _Query
17
+
18
+ Query = _Query
19
+
20
+ DEFAULT_JSON_ENCODER = json.dumps
21
+ DEFAULT_JSON_DECODER = json.loads
22
+
23
+ if TYPE_CHECKING:
24
+ _CIMultiDict = CIMultiDict[str]
25
+ _CIMultiDictProxy = CIMultiDictProxy[str]
26
+ _MultiDict = MultiDict[str]
27
+ _MultiDictProxy = MultiDictProxy[str]
28
+ from http.cookies import BaseCookie, Morsel
29
+
30
+ from .web import Request, StreamResponse
31
+ else:
32
+ _CIMultiDict = CIMultiDict
33
+ _CIMultiDictProxy = CIMultiDictProxy
34
+ _MultiDict = MultiDict
35
+ _MultiDictProxy = MultiDictProxy
36
+
37
+ Byteish = Union[bytes, bytearray, memoryview]
38
+ JSONEncoder = Callable[[Any], str]
39
+ JSONDecoder = Callable[[str], Any]
40
+ LooseHeaders = Union[
41
+ Mapping[str, str],
42
+ Mapping[istr, str],
43
+ _CIMultiDict,
44
+ _CIMultiDictProxy,
45
+ Iterable[Tuple[Union[str, istr], str]],
46
+ ]
47
+ RawHeaders = Tuple[Tuple[bytes, bytes], ...]
48
+ StrOrURL = Union[str, URL]
49
+
50
+ LooseCookiesMappings = Mapping[str, Union[str, "BaseCookie[str]", "Morsel[Any]"]]
51
+ LooseCookiesIterables = Iterable[
52
+ Tuple[str, Union[str, "BaseCookie[str]", "Morsel[Any]"]]
53
+ ]
54
+ LooseCookies = Union[
55
+ LooseCookiesMappings,
56
+ LooseCookiesIterables,
57
+ "BaseCookie[str]",
58
+ ]
59
+
60
+ Handler = Callable[["Request"], Awaitable["StreamResponse"]]
61
+
62
+
63
+ class Middleware(Protocol):
64
+ def __call__(
65
+ self, request: "Request", handler: Handler
66
+ ) -> Awaitable["StreamResponse"]: ...
67
+
68
+
69
+ PathLike = Union[str, "os.PathLike[str]"]
aiohttp/web_fileresponse.py ADDED
@@ -0,0 +1,418 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import io
3
+ import os
4
+ import pathlib
5
+ import sys
6
+ from contextlib import suppress
7
+ from enum import Enum, auto
8
+ from mimetypes import MimeTypes
9
+ from stat import S_ISREG
10
+ from types import MappingProxyType
11
+ from typing import ( # noqa
12
+ IO,
13
+ TYPE_CHECKING,
14
+ Any,
15
+ Awaitable,
16
+ Callable,
17
+ Final,
18
+ Iterator,
19
+ List,
20
+ Optional,
21
+ Set,
22
+ Tuple,
23
+ Union,
24
+ cast,
25
+ )
26
+
27
+ from . import hdrs
28
+ from .abc import AbstractStreamWriter
29
+ from .helpers import ETAG_ANY, ETag, must_be_empty_body
30
+ from .typedefs import LooseHeaders, PathLike
31
+ from .web_exceptions import (
32
+ HTTPForbidden,
33
+ HTTPNotFound,
34
+ HTTPNotModified,
35
+ HTTPPartialContent,
36
+ HTTPPreconditionFailed,
37
+ HTTPRequestRangeNotSatisfiable,
38
+ )
39
+ from .web_response import StreamResponse
40
+
41
+ __all__ = ("FileResponse",)
42
+
43
+ if TYPE_CHECKING:
44
+ from .web_request import BaseRequest
45
+
46
+
47
+ _T_OnChunkSent = Optional[Callable[[bytes], Awaitable[None]]]
48
+
49
+
50
+ NOSENDFILE: Final[bool] = bool(os.environ.get("AIOHTTP_NOSENDFILE"))
51
+
52
+ CONTENT_TYPES: Final[MimeTypes] = MimeTypes()
53
+
54
+ # File extension to IANA encodings map that will be checked in the order defined.
55
+ ENCODING_EXTENSIONS = MappingProxyType(
56
+ {ext: CONTENT_TYPES.encodings_map[ext] for ext in (".br", ".gz")}
57
+ )
58
+
59
+ FALLBACK_CONTENT_TYPE = "application/octet-stream"
60
+
61
+ # Provide additional MIME type/extension pairs to be recognized.
62
+ # https://en.wikipedia.org/wiki/List_of_archive_formats#Compression_only
63
+ ADDITIONAL_CONTENT_TYPES = MappingProxyType(
64
+ {
65
+ "application/gzip": ".gz",
66
+ "application/x-brotli": ".br",
67
+ "application/x-bzip2": ".bz2",
68
+ "application/x-compress": ".Z",
69
+ "application/x-xz": ".xz",
70
+ }
71
+ )
72
+
73
+
74
+ class _FileResponseResult(Enum):
75
+ """The result of the file response."""
76
+
77
+ SEND_FILE = auto() # Ie a regular file to send
78
+ NOT_ACCEPTABLE = auto() # Ie a socket, or non-regular file
79
+ PRE_CONDITION_FAILED = auto() # Ie If-Match or If-None-Match failed
80
+ NOT_MODIFIED = auto() # 304 Not Modified
81
+
82
+
83
+ # Add custom pairs and clear the encodings map so guess_type ignores them.
84
+ CONTENT_TYPES.encodings_map.clear()
85
+ for content_type, extension in ADDITIONAL_CONTENT_TYPES.items():
86
+ CONTENT_TYPES.add_type(content_type, extension)
87
+
88
+
89
+ _CLOSE_FUTURES: Set[asyncio.Future[None]] = set()
90
+
91
+
92
+ class FileResponse(StreamResponse):
93
+ """A response object can be used to send files."""
94
+
95
+ def __init__(
96
+ self,
97
+ path: PathLike,
98
+ chunk_size: int = 256 * 1024,
99
+ status: int = 200,
100
+ reason: Optional[str] = None,
101
+ headers: Optional[LooseHeaders] = None,
102
+ ) -> None:
103
+ super().__init__(status=status, reason=reason, headers=headers)
104
+
105
+ self._path = pathlib.Path(path)
106
+ self._chunk_size = chunk_size
107
+
108
+ def _seek_and_read(self, fobj: IO[Any], offset: int, chunk_size: int) -> bytes:
109
+ fobj.seek(offset)
110
+ return fobj.read(chunk_size) # type: ignore[no-any-return]
111
+
112
+ async def _sendfile_fallback(
113
+ self, writer: AbstractStreamWriter, fobj: IO[Any], offset: int, count: int
114
+ ) -> AbstractStreamWriter:
115
+ # To keep memory usage low,fobj is transferred in chunks
116
+ # controlled by the constructor's chunk_size argument.
117
+
118
+ chunk_size = self._chunk_size
119
+ loop = asyncio.get_event_loop()
120
+ chunk = await loop.run_in_executor(
121
+ None, self._seek_and_read, fobj, offset, chunk_size
122
+ )
123
+ while chunk:
124
+ await writer.write(chunk)
125
+ count = count - chunk_size
126
+ if count <= 0:
127
+ break
128
+ chunk = await loop.run_in_executor(None, fobj.read, min(chunk_size, count))
129
+
130
+ await writer.drain()
131
+ return writer
132
+
133
+ async def _sendfile(
134
+ self, request: "BaseRequest", fobj: IO[Any], offset: int, count: int
135
+ ) -> AbstractStreamWriter:
136
+ writer = await super().prepare(request)
137
+ assert writer is not None
138
+
139
+ if NOSENDFILE or self.compression:
140
+ return await self._sendfile_fallback(writer, fobj, offset, count)
141
+
142
+ loop = request._loop
143
+ transport = request.transport
144
+ assert transport is not None
145
+
146
+ try:
147
+ await loop.sendfile(transport, fobj, offset, count)
148
+ except NotImplementedError:
149
+ return await self._sendfile_fallback(writer, fobj, offset, count)
150
+
151
+ await super().write_eof()
152
+ return writer
153
+
154
+ @staticmethod
155
+ def _etag_match(etag_value: str, etags: Tuple[ETag, ...], *, weak: bool) -> bool:
156
+ if len(etags) == 1 and etags[0].value == ETAG_ANY:
157
+ return True
158
+ return any(
159
+ etag.value == etag_value for etag in etags if weak or not etag.is_weak
160
+ )
161
+
162
+ async def _not_modified(
163
+ self, request: "BaseRequest", etag_value: str, last_modified: float
164
+ ) -> Optional[AbstractStreamWriter]:
165
+ self.set_status(HTTPNotModified.status_code)
166
+ self._length_check = False
167
+ self.etag = etag_value
168
+ self.last_modified = last_modified
169
+ # Delete any Content-Length headers provided by user. HTTP 304
170
+ # should always have empty response body
171
+ return await super().prepare(request)
172
+
173
+ async def _precondition_failed(
174
+ self, request: "BaseRequest"
175
+ ) -> Optional[AbstractStreamWriter]:
176
+ self.set_status(HTTPPreconditionFailed.status_code)
177
+ self.content_length = 0
178
+ return await super().prepare(request)
179
+
180
+ def _make_response(
181
+ self, request: "BaseRequest", accept_encoding: str
182
+ ) -> Tuple[
183
+ _FileResponseResult, Optional[io.BufferedReader], os.stat_result, Optional[str]
184
+ ]:
185
+ """Return the response result, io object, stat result, and encoding.
186
+
187
+ If an uncompressed file is returned, the encoding is set to
188
+ :py:data:`None`.
189
+
190
+ This method should be called from a thread executor
191
+ since it calls os.stat which may block.
192
+ """
193
+ file_path, st, file_encoding = self._get_file_path_stat_encoding(
194
+ accept_encoding
195
+ )
196
+ if not file_path:
197
+ return _FileResponseResult.NOT_ACCEPTABLE, None, st, None
198
+
199
+ etag_value = f"{st.st_mtime_ns:x}-{st.st_size:x}"
200
+
201
+ # https://www.rfc-editor.org/rfc/rfc9110#section-13.1.1-2
202
+ if (ifmatch := request.if_match) is not None and not self._etag_match(
203
+ etag_value, ifmatch, weak=False
204
+ ):
205
+ return _FileResponseResult.PRE_CONDITION_FAILED, None, st, file_encoding
206
+
207
+ if (
208
+ (unmodsince := request.if_unmodified_since) is not None
209
+ and ifmatch is None
210
+ and st.st_mtime > unmodsince.timestamp()
211
+ ):
212
+ return _FileResponseResult.PRE_CONDITION_FAILED, None, st, file_encoding
213
+
214
+ # https://www.rfc-editor.org/rfc/rfc9110#section-13.1.2-2
215
+ if (ifnonematch := request.if_none_match) is not None and self._etag_match(
216
+ etag_value, ifnonematch, weak=True
217
+ ):
218
+ return _FileResponseResult.NOT_MODIFIED, None, st, file_encoding
219
+
220
+ if (
221
+ (modsince := request.if_modified_since) is not None
222
+ and ifnonematch is None
223
+ and st.st_mtime <= modsince.timestamp()
224
+ ):
225
+ return _FileResponseResult.NOT_MODIFIED, None, st, file_encoding
226
+
227
+ fobj = file_path.open("rb")
228
+ with suppress(OSError):
229
+ # fstat() may not be available on all platforms
230
+ # Once we open the file, we want the fstat() to ensure
231
+ # the file has not changed between the first stat()
232
+ # and the open().
233
+ st = os.stat(fobj.fileno())
234
+ return _FileResponseResult.SEND_FILE, fobj, st, file_encoding
235
+
236
+ def _get_file_path_stat_encoding(
237
+ self, accept_encoding: str
238
+ ) -> Tuple[Optional[pathlib.Path], os.stat_result, Optional[str]]:
239
+ file_path = self._path
240
+ for file_extension, file_encoding in ENCODING_EXTENSIONS.items():
241
+ if file_encoding not in accept_encoding:
242
+ continue
243
+
244
+ compressed_path = file_path.with_suffix(file_path.suffix + file_extension)
245
+ with suppress(OSError):
246
+ # Do not follow symlinks and ignore any non-regular files.
247
+ st = compressed_path.lstat()
248
+ if S_ISREG(st.st_mode):
249
+ return compressed_path, st, file_encoding
250
+
251
+ # Fallback to the uncompressed file
252
+ st = file_path.stat()
253
+ return file_path if S_ISREG(st.st_mode) else None, st, None
254
+
255
+ async def prepare(self, request: "BaseRequest") -> Optional[AbstractStreamWriter]:
256
+ loop = asyncio.get_running_loop()
257
+ # Encoding comparisons should be case-insensitive
258
+ # https://www.rfc-editor.org/rfc/rfc9110#section-8.4.1
259
+ accept_encoding = request.headers.get(hdrs.ACCEPT_ENCODING, "").lower()
260
+ try:
261
+ response_result, fobj, st, file_encoding = await loop.run_in_executor(
262
+ None, self._make_response, request, accept_encoding
263
+ )
264
+ except PermissionError:
265
+ self.set_status(HTTPForbidden.status_code)
266
+ return await super().prepare(request)
267
+ except OSError:
268
+ # Most likely to be FileNotFoundError or OSError for circular
269
+ # symlinks in python >= 3.13, so respond with 404.
270
+ self.set_status(HTTPNotFound.status_code)
271
+ return await super().prepare(request)
272
+
273
+ # Forbid special files like sockets, pipes, devices, etc.
274
+ if response_result is _FileResponseResult.NOT_ACCEPTABLE:
275
+ self.set_status(HTTPForbidden.status_code)
276
+ return await super().prepare(request)
277
+
278
+ if response_result is _FileResponseResult.PRE_CONDITION_FAILED:
279
+ return await self._precondition_failed(request)
280
+
281
+ if response_result is _FileResponseResult.NOT_MODIFIED:
282
+ etag_value = f"{st.st_mtime_ns:x}-{st.st_size:x}"
283
+ last_modified = st.st_mtime
284
+ return await self._not_modified(request, etag_value, last_modified)
285
+
286
+ assert fobj is not None
287
+ try:
288
+ return await self._prepare_open_file(request, fobj, st, file_encoding)
289
+ finally:
290
+ # We do not await here because we do not want to wait
291
+ # for the executor to finish before returning the response
292
+ # so the connection can begin servicing another request
293
+ # as soon as possible.
294
+ close_future = loop.run_in_executor(None, fobj.close)
295
+ # Hold a strong reference to the future to prevent it from being
296
+ # garbage collected before it completes.
297
+ _CLOSE_FUTURES.add(close_future)
298
+ close_future.add_done_callback(_CLOSE_FUTURES.remove)
299
+
300
+ async def _prepare_open_file(
301
+ self,
302
+ request: "BaseRequest",
303
+ fobj: io.BufferedReader,
304
+ st: os.stat_result,
305
+ file_encoding: Optional[str],
306
+ ) -> Optional[AbstractStreamWriter]:
307
+ status = self._status
308
+ file_size: int = st.st_size
309
+ file_mtime: float = st.st_mtime
310
+ count: int = file_size
311
+ start: Optional[int] = None
312
+
313
+ if (ifrange := request.if_range) is None or file_mtime <= ifrange.timestamp():
314
+ # If-Range header check:
315
+ # condition = cached date >= last modification date
316
+ # return 206 if True else 200.
317
+ # if False:
318
+ # Range header would not be processed, return 200
319
+ # if True but Range header missing
320
+ # return 200
321
+ try:
322
+ rng = request.http_range
323
+ start = rng.start
324
+ end: Optional[int] = rng.stop
325
+ except ValueError:
326
+ # https://tools.ietf.org/html/rfc7233:
327
+ # A server generating a 416 (Range Not Satisfiable) response to
328
+ # a byte-range request SHOULD send a Content-Range header field
329
+ # with an unsatisfied-range value.
330
+ # The complete-length in a 416 response indicates the current
331
+ # length of the selected representation.
332
+ #
333
+ # Will do the same below. Many servers ignore this and do not
334
+ # send a Content-Range header with HTTP 416
335
+ self._headers[hdrs.CONTENT_RANGE] = f"bytes */{file_size}"
336
+ self.set_status(HTTPRequestRangeNotSatisfiable.status_code)
337
+ return await super().prepare(request)
338
+
339
+ # If a range request has been made, convert start, end slice
340
+ # notation into file pointer offset and count
341
+ if start is not None:
342
+ if start < 0 and end is None: # return tail of file
343
+ start += file_size
344
+ if start < 0:
345
+ # if Range:bytes=-1000 in request header but file size
346
+ # is only 200, there would be trouble without this
347
+ start = 0
348
+ count = file_size - start
349
+ else:
350
+ # rfc7233:If the last-byte-pos value is
351
+ # absent, or if the value is greater than or equal to
352
+ # the current length of the representation data,
353
+ # the byte range is interpreted as the remainder
354
+ # of the representation (i.e., the server replaces the
355
+ # value of last-byte-pos with a value that is one less than
356
+ # the current length of the selected representation).
357
+ count = (
358
+ min(end if end is not None else file_size, file_size) - start
359
+ )
360
+
361
+ if start >= file_size:
362
+ # HTTP 416 should be returned in this case.
363
+ #
364
+ # According to https://tools.ietf.org/html/rfc7233:
365
+ # If a valid byte-range-set includes at least one
366
+ # byte-range-spec with a first-byte-pos that is less than
367
+ # the current length of the representation, or at least one
368
+ # suffix-byte-range-spec with a non-zero suffix-length,
369
+ # then the byte-range-set is satisfiable. Otherwise, the
370
+ # byte-range-set is unsatisfiable.
371
+ self._headers[hdrs.CONTENT_RANGE] = f"bytes */{file_size}"
372
+ self.set_status(HTTPRequestRangeNotSatisfiable.status_code)
373
+ return await super().prepare(request)
374
+
375
+ status = HTTPPartialContent.status_code
376
+ # Even though you are sending the whole file, you should still
377
+ # return a HTTP 206 for a Range request.
378
+ self.set_status(status)
379
+
380
+ # If the Content-Type header is not already set, guess it based on the
381
+ # extension of the request path. The encoding returned by guess_type
382
+ # can be ignored since the map was cleared above.
383
+ if hdrs.CONTENT_TYPE not in self._headers:
384
+ if sys.version_info >= (3, 13):
385
+ guesser = CONTENT_TYPES.guess_file_type
386
+ else:
387
+ guesser = CONTENT_TYPES.guess_type
388
+ self.content_type = guesser(self._path)[0] or FALLBACK_CONTENT_TYPE
389
+
390
+ if file_encoding:
391
+ self._headers[hdrs.CONTENT_ENCODING] = file_encoding
392
+ self._headers[hdrs.VARY] = hdrs.ACCEPT_ENCODING
393
+ # Disable compression if we are already sending
394
+ # a compressed file since we don't want to double
395
+ # compress.
396
+ self._compression = False
397
+
398
+ self.etag = f"{st.st_mtime_ns:x}-{st.st_size:x}"
399
+ self.last_modified = file_mtime
400
+ self.content_length = count
401
+
402
+ self._headers[hdrs.ACCEPT_RANGES] = "bytes"
403
+
404
+ if status == HTTPPartialContent.status_code:
405
+ real_start = start
406
+ assert real_start is not None
407
+ self._headers[hdrs.CONTENT_RANGE] = "bytes {}-{}/{}".format(
408
+ real_start, real_start + count - 1, file_size
409
+ )
410
+
411
+ # If we are sending 0 bytes calling sendfile() will throw a ValueError
412
+ if count == 0 or must_be_empty_body(request.method, status):
413
+ return await super().prepare(request)
414
+
415
+ # be aware that start could be None or int=0 here.
416
+ offset = start or 0
417
+
418
+ return await self._sendfile(request, fobj, offset, count)
aiohttp/web_log.py ADDED
@@ -0,0 +1,216 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import datetime
2
+ import functools
3
+ import logging
4
+ import os
5
+ import re
6
+ import time as time_mod
7
+ from collections import namedtuple
8
+ from typing import Any, Callable, Dict, Iterable, List, Tuple # noqa
9
+
10
+ from .abc import AbstractAccessLogger
11
+ from .web_request import BaseRequest
12
+ from .web_response import StreamResponse
13
+
14
+ KeyMethod = namedtuple("KeyMethod", "key method")
15
+
16
+
17
+ class AccessLogger(AbstractAccessLogger):
18
+ """Helper object to log access.
19
+
20
+ Usage:
21
+ log = logging.getLogger("spam")
22
+ log_format = "%a %{User-Agent}i"
23
+ access_logger = AccessLogger(log, log_format)
24
+ access_logger.log(request, response, time)
25
+
26
+ Format:
27
+ %% The percent sign
28
+ %a Remote IP-address (IP-address of proxy if using reverse proxy)
29
+ %t Time when the request was started to process
30
+ %P The process ID of the child that serviced the request
31
+ %r First line of request
32
+ %s Response status code
33
+ %b Size of response in bytes, including HTTP headers
34
+ %T Time taken to serve the request, in seconds
35
+ %Tf Time taken to serve the request, in seconds with floating fraction
36
+ in .06f format
37
+ %D Time taken to serve the request, in microseconds
38
+ %{FOO}i request.headers['FOO']
39
+ %{FOO}o response.headers['FOO']
40
+ %{FOO}e os.environ['FOO']
41
+
42
+ """
43
+
44
+ LOG_FORMAT_MAP = {
45
+ "a": "remote_address",
46
+ "t": "request_start_time",
47
+ "P": "process_id",
48
+ "r": "first_request_line",
49
+ "s": "response_status",
50
+ "b": "response_size",
51
+ "T": "request_time",
52
+ "Tf": "request_time_frac",
53
+ "D": "request_time_micro",
54
+ "i": "request_header",
55
+ "o": "response_header",
56
+ }
57
+
58
+ LOG_FORMAT = '%a %t "%r" %s %b "%{Referer}i" "%{User-Agent}i"'
59
+ FORMAT_RE = re.compile(r"%(\{([A-Za-z0-9\-_]+)\}([ioe])|[atPrsbOD]|Tf?)")
60
+ CLEANUP_RE = re.compile(r"(%[^s])")
61
+ _FORMAT_CACHE: Dict[str, Tuple[str, List[KeyMethod]]] = {}
62
+
63
+ def __init__(self, logger: logging.Logger, log_format: str = LOG_FORMAT) -> None:
64
+ """Initialise the logger.
65
+
66
+ logger is a logger object to be used for logging.
67
+ log_format is a string with apache compatible log format description.
68
+
69
+ """
70
+ super().__init__(logger, log_format=log_format)
71
+
72
+ _compiled_format = AccessLogger._FORMAT_CACHE.get(log_format)
73
+ if not _compiled_format:
74
+ _compiled_format = self.compile_format(log_format)
75
+ AccessLogger._FORMAT_CACHE[log_format] = _compiled_format
76
+
77
+ self._log_format, self._methods = _compiled_format
78
+
79
+ def compile_format(self, log_format: str) -> Tuple[str, List[KeyMethod]]:
80
+ """Translate log_format into form usable by modulo formatting
81
+
82
+ All known atoms will be replaced with %s
83
+ Also methods for formatting of those atoms will be added to
84
+ _methods in appropriate order
85
+
86
+ For example we have log_format = "%a %t"
87
+ This format will be translated to "%s %s"
88
+ Also contents of _methods will be
89
+ [self._format_a, self._format_t]
90
+ These method will be called and results will be passed
91
+ to translated string format.
92
+
93
+ Each _format_* method receive 'args' which is list of arguments
94
+ given to self.log
95
+
96
+ Exceptions are _format_e, _format_i and _format_o methods which
97
+ also receive key name (by functools.partial)
98
+
99
+ """
100
+ # list of (key, method) tuples, we don't use an OrderedDict as users
101
+ # can repeat the same key more than once
102
+ methods = list()
103
+
104
+ for atom in self.FORMAT_RE.findall(log_format):
105
+ if atom[1] == "":
106
+ format_key1 = self.LOG_FORMAT_MAP[atom[0]]
107
+ m = getattr(AccessLogger, "_format_%s" % atom[0])
108
+ key_method = KeyMethod(format_key1, m)
109
+ else:
110
+ format_key2 = (self.LOG_FORMAT_MAP[atom[2]], atom[1])
111
+ m = getattr(AccessLogger, "_format_%s" % atom[2])
112
+ key_method = KeyMethod(format_key2, functools.partial(m, atom[1]))
113
+
114
+ methods.append(key_method)
115
+
116
+ log_format = self.FORMAT_RE.sub(r"%s", log_format)
117
+ log_format = self.CLEANUP_RE.sub(r"%\1", log_format)
118
+ return log_format, methods
119
+
120
+ @staticmethod
121
+ def _format_i(
122
+ key: str, request: BaseRequest, response: StreamResponse, time: float
123
+ ) -> str:
124
+ if request is None:
125
+ return "(no headers)"
126
+
127
+ # suboptimal, make istr(key) once
128
+ return request.headers.get(key, "-")
129
+
130
+ @staticmethod
131
+ def _format_o(
132
+ key: str, request: BaseRequest, response: StreamResponse, time: float
133
+ ) -> str:
134
+ # suboptimal, make istr(key) once
135
+ return response.headers.get(key, "-")
136
+
137
+ @staticmethod
138
+ def _format_a(request: BaseRequest, response: StreamResponse, time: float) -> str:
139
+ if request is None:
140
+ return "-"
141
+ ip = request.remote
142
+ return ip if ip is not None else "-"
143
+
144
+ @staticmethod
145
+ def _format_t(request: BaseRequest, response: StreamResponse, time: float) -> str:
146
+ tz = datetime.timezone(datetime.timedelta(seconds=-time_mod.timezone))
147
+ now = datetime.datetime.now(tz)
148
+ start_time = now - datetime.timedelta(seconds=time)
149
+ return start_time.strftime("[%d/%b/%Y:%H:%M:%S %z]")
150
+
151
+ @staticmethod
152
+ def _format_P(request: BaseRequest, response: StreamResponse, time: float) -> str:
153
+ return "<%s>" % os.getpid()
154
+
155
+ @staticmethod
156
+ def _format_r(request: BaseRequest, response: StreamResponse, time: float) -> str:
157
+ if request is None:
158
+ return "-"
159
+ return "{} {} HTTP/{}.{}".format(
160
+ request.method,
161
+ request.path_qs,
162
+ request.version.major,
163
+ request.version.minor,
164
+ )
165
+
166
+ @staticmethod
167
+ def _format_s(request: BaseRequest, response: StreamResponse, time: float) -> int:
168
+ return response.status
169
+
170
+ @staticmethod
171
+ def _format_b(request: BaseRequest, response: StreamResponse, time: float) -> int:
172
+ return response.body_length
173
+
174
+ @staticmethod
175
+ def _format_T(request: BaseRequest, response: StreamResponse, time: float) -> str:
176
+ return str(round(time))
177
+
178
+ @staticmethod
179
+ def _format_Tf(request: BaseRequest, response: StreamResponse, time: float) -> str:
180
+ return "%06f" % time
181
+
182
+ @staticmethod
183
+ def _format_D(request: BaseRequest, response: StreamResponse, time: float) -> str:
184
+ return str(round(time * 1000000))
185
+
186
+ def _format_line(
187
+ self, request: BaseRequest, response: StreamResponse, time: float
188
+ ) -> Iterable[Tuple[str, Callable[[BaseRequest, StreamResponse, float], str]]]:
189
+ return [(key, method(request, response, time)) for key, method in self._methods]
190
+
191
+ @property
192
+ def enabled(self) -> bool:
193
+ """Check if logger is enabled."""
194
+ # Avoid formatting the log line if it will not be emitted.
195
+ return self.logger.isEnabledFor(logging.INFO)
196
+
197
+ def log(self, request: BaseRequest, response: StreamResponse, time: float) -> None:
198
+ try:
199
+ fmt_info = self._format_line(request, response, time)
200
+
201
+ values = list()
202
+ extra = dict()
203
+ for key, value in fmt_info:
204
+ values.append(value)
205
+
206
+ if key.__class__ is str:
207
+ extra[key] = value
208
+ else:
209
+ k1, k2 = key # type: ignore[misc]
210
+ dct = extra.get(k1, {}) # type: ignore[var-annotated,has-type]
211
+ dct[k2] = value # type: ignore[index,has-type]
212
+ extra[k1] = dct # type: ignore[has-type,assignment]
213
+
214
+ self.logger.info(self._log_format % tuple(values), extra=extra)
215
+ except Exception:
216
+ self.logger.exception("Error in logging")
aiohttp/web_routedef.py ADDED
@@ -0,0 +1,214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import abc
2
+ import os # noqa
3
+ from typing import (
4
+ TYPE_CHECKING,
5
+ Any,
6
+ Callable,
7
+ Dict,
8
+ Iterator,
9
+ List,
10
+ Optional,
11
+ Sequence,
12
+ Type,
13
+ Union,
14
+ overload,
15
+ )
16
+
17
+ import attr
18
+
19
+ from . import hdrs
20
+ from .abc import AbstractView
21
+ from .typedefs import Handler, PathLike
22
+
23
+ if TYPE_CHECKING:
24
+ from .web_request import Request
25
+ from .web_response import StreamResponse
26
+ from .web_urldispatcher import AbstractRoute, UrlDispatcher
27
+ else:
28
+ Request = StreamResponse = UrlDispatcher = AbstractRoute = None
29
+
30
+
31
+ __all__ = (
32
+ "AbstractRouteDef",
33
+ "RouteDef",
34
+ "StaticDef",
35
+ "RouteTableDef",
36
+ "head",
37
+ "options",
38
+ "get",
39
+ "post",
40
+ "patch",
41
+ "put",
42
+ "delete",
43
+ "route",
44
+ "view",
45
+ "static",
46
+ )
47
+
48
+
49
+ class AbstractRouteDef(abc.ABC):
50
+ @abc.abstractmethod
51
+ def register(self, router: UrlDispatcher) -> List[AbstractRoute]:
52
+ pass # pragma: no cover
53
+
54
+
55
+ _HandlerType = Union[Type[AbstractView], Handler]
56
+
57
+
58
+ @attr.s(auto_attribs=True, frozen=True, repr=False, slots=True)
59
+ class RouteDef(AbstractRouteDef):
60
+ method: str
61
+ path: str
62
+ handler: _HandlerType
63
+ kwargs: Dict[str, Any]
64
+
65
+ def __repr__(self) -> str:
66
+ info = []
67
+ for name, value in sorted(self.kwargs.items()):
68
+ info.append(f", {name}={value!r}")
69
+ return "<RouteDef {method} {path} -> {handler.__name__!r}{info}>".format(
70
+ method=self.method, path=self.path, handler=self.handler, info="".join(info)
71
+ )
72
+
73
+ def register(self, router: UrlDispatcher) -> List[AbstractRoute]:
74
+ if self.method in hdrs.METH_ALL:
75
+ reg = getattr(router, "add_" + self.method.lower())
76
+ return [reg(self.path, self.handler, **self.kwargs)]
77
+ else:
78
+ return [
79
+ router.add_route(self.method, self.path, self.handler, **self.kwargs)
80
+ ]
81
+
82
+
83
+ @attr.s(auto_attribs=True, frozen=True, repr=False, slots=True)
84
+ class StaticDef(AbstractRouteDef):
85
+ prefix: str
86
+ path: PathLike
87
+ kwargs: Dict[str, Any]
88
+
89
+ def __repr__(self) -> str:
90
+ info = []
91
+ for name, value in sorted(self.kwargs.items()):
92
+ info.append(f", {name}={value!r}")
93
+ return "<StaticDef {prefix} -> {path}{info}>".format(
94
+ prefix=self.prefix, path=self.path, info="".join(info)
95
+ )
96
+
97
+ def register(self, router: UrlDispatcher) -> List[AbstractRoute]:
98
+ resource = router.add_static(self.prefix, self.path, **self.kwargs)
99
+ routes = resource.get_info().get("routes", {})
100
+ return list(routes.values())
101
+
102
+
103
+ def route(method: str, path: str, handler: _HandlerType, **kwargs: Any) -> RouteDef:
104
+ return RouteDef(method, path, handler, kwargs)
105
+
106
+
107
+ def head(path: str, handler: _HandlerType, **kwargs: Any) -> RouteDef:
108
+ return route(hdrs.METH_HEAD, path, handler, **kwargs)
109
+
110
+
111
+ def options(path: str, handler: _HandlerType, **kwargs: Any) -> RouteDef:
112
+ return route(hdrs.METH_OPTIONS, path, handler, **kwargs)
113
+
114
+
115
+ def get(
116
+ path: str,
117
+ handler: _HandlerType,
118
+ *,
119
+ name: Optional[str] = None,
120
+ allow_head: bool = True,
121
+ **kwargs: Any,
122
+ ) -> RouteDef:
123
+ return route(
124
+ hdrs.METH_GET, path, handler, name=name, allow_head=allow_head, **kwargs
125
+ )
126
+
127
+
128
+ def post(path: str, handler: _HandlerType, **kwargs: Any) -> RouteDef:
129
+ return route(hdrs.METH_POST, path, handler, **kwargs)
130
+
131
+
132
+ def put(path: str, handler: _HandlerType, **kwargs: Any) -> RouteDef:
133
+ return route(hdrs.METH_PUT, path, handler, **kwargs)
134
+
135
+
136
+ def patch(path: str, handler: _HandlerType, **kwargs: Any) -> RouteDef:
137
+ return route(hdrs.METH_PATCH, path, handler, **kwargs)
138
+
139
+
140
+ def delete(path: str, handler: _HandlerType, **kwargs: Any) -> RouteDef:
141
+ return route(hdrs.METH_DELETE, path, handler, **kwargs)
142
+
143
+
144
+ def view(path: str, handler: Type[AbstractView], **kwargs: Any) -> RouteDef:
145
+ return route(hdrs.METH_ANY, path, handler, **kwargs)
146
+
147
+
148
+ def static(prefix: str, path: PathLike, **kwargs: Any) -> StaticDef:
149
+ return StaticDef(prefix, path, kwargs)
150
+
151
+
152
+ _Deco = Callable[[_HandlerType], _HandlerType]
153
+
154
+
155
+ class RouteTableDef(Sequence[AbstractRouteDef]):
156
+ """Route definition table"""
157
+
158
+ def __init__(self) -> None:
159
+ self._items: List[AbstractRouteDef] = []
160
+
161
+ def __repr__(self) -> str:
162
+ return f"<RouteTableDef count={len(self._items)}>"
163
+
164
+ @overload
165
+ def __getitem__(self, index: int) -> AbstractRouteDef: ...
166
+
167
+ @overload
168
+ def __getitem__(self, index: slice) -> List[AbstractRouteDef]: ...
169
+
170
+ def __getitem__(self, index): # type: ignore[no-untyped-def]
171
+ return self._items[index]
172
+
173
+ def __iter__(self) -> Iterator[AbstractRouteDef]:
174
+ return iter(self._items)
175
+
176
+ def __len__(self) -> int:
177
+ return len(self._items)
178
+
179
+ def __contains__(self, item: object) -> bool:
180
+ return item in self._items
181
+
182
+ def route(self, method: str, path: str, **kwargs: Any) -> _Deco:
183
+ def inner(handler: _HandlerType) -> _HandlerType:
184
+ self._items.append(RouteDef(method, path, handler, kwargs))
185
+ return handler
186
+
187
+ return inner
188
+
189
+ def head(self, path: str, **kwargs: Any) -> _Deco:
190
+ return self.route(hdrs.METH_HEAD, path, **kwargs)
191
+
192
+ def get(self, path: str, **kwargs: Any) -> _Deco:
193
+ return self.route(hdrs.METH_GET, path, **kwargs)
194
+
195
+ def post(self, path: str, **kwargs: Any) -> _Deco:
196
+ return self.route(hdrs.METH_POST, path, **kwargs)
197
+
198
+ def put(self, path: str, **kwargs: Any) -> _Deco:
199
+ return self.route(hdrs.METH_PUT, path, **kwargs)
200
+
201
+ def patch(self, path: str, **kwargs: Any) -> _Deco:
202
+ return self.route(hdrs.METH_PATCH, path, **kwargs)
203
+
204
+ def delete(self, path: str, **kwargs: Any) -> _Deco:
205
+ return self.route(hdrs.METH_DELETE, path, **kwargs)
206
+
207
+ def options(self, path: str, **kwargs: Any) -> _Deco:
208
+ return self.route(hdrs.METH_OPTIONS, path, **kwargs)
209
+
210
+ def view(self, path: str, **kwargs: Any) -> _Deco:
211
+ return self.route(hdrs.METH_ANY, path, **kwargs)
212
+
213
+ def static(self, prefix: str, path: PathLike, **kwargs: Any) -> None:
214
+ self._items.append(StaticDef(prefix, path, kwargs))
bin/accelerate ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/home/zeus/miniconda3/envs/cloudspace/bin/python
2
+ # -*- coding: utf-8 -*-
3
+ import sys
4
+ from accelerate.commands.accelerate_cli import main
5
+ if __name__ == "__main__":
6
+ if sys.argv[0].endswith("-script.pyw"):
7
+ sys.argv[0] = sys.argv[0][:-11]
8
+ elif sys.argv[0].endswith(".exe"):
9
+ sys.argv[0] = sys.argv[0][:-4]
10
+ sys.exit(main())
bin/accelerate-config ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/home/zeus/miniconda3/envs/cloudspace/bin/python
2
+ # -*- coding: utf-8 -*-
3
+ import sys
4
+ from accelerate.commands.config import main
5
+ if __name__ == "__main__":
6
+ if sys.argv[0].endswith("-script.pyw"):
7
+ sys.argv[0] = sys.argv[0][:-11]
8
+ elif sys.argv[0].endswith(".exe"):
9
+ sys.argv[0] = sys.argv[0][:-4]
10
+ sys.exit(main())
bin/accelerate-estimate-memory ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/home/zeus/miniconda3/envs/cloudspace/bin/python
2
+ # -*- coding: utf-8 -*-
3
+ import sys
4
+ from accelerate.commands.estimate import main
5
+ if __name__ == "__main__":
6
+ if sys.argv[0].endswith("-script.pyw"):
7
+ sys.argv[0] = sys.argv[0][:-11]
8
+ elif sys.argv[0].endswith(".exe"):
9
+ sys.argv[0] = sys.argv[0][:-4]
10
+ sys.exit(main())
bin/accelerate-launch ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/home/zeus/miniconda3/envs/cloudspace/bin/python
2
+ # -*- coding: utf-8 -*-
3
+ import sys
4
+ from accelerate.commands.launch import main
5
+ if __name__ == "__main__":
6
+ if sys.argv[0].endswith("-script.pyw"):
7
+ sys.argv[0] = sys.argv[0][:-11]
8
+ elif sys.argv[0].endswith(".exe"):
9
+ sys.argv[0] = sys.argv[0][:-4]
10
+ sys.exit(main())
bin/accelerate-merge-weights ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/home/zeus/miniconda3/envs/cloudspace/bin/python
2
+ # -*- coding: utf-8 -*-
3
+ import sys
4
+ from accelerate.commands.merge import main
5
+ if __name__ == "__main__":
6
+ if sys.argv[0].endswith("-script.pyw"):
7
+ sys.argv[0] = sys.argv[0][:-11]
8
+ elif sys.argv[0].endswith(".exe"):
9
+ sys.argv[0] = sys.argv[0][:-4]
10
+ sys.exit(main())
bin/datasets-cli ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/home/zeus/miniconda3/envs/cloudspace/bin/python
2
+ # -*- coding: utf-8 -*-
3
+ import sys
4
+ from datasets.commands.datasets_cli import main
5
+ if __name__ == "__main__":
6
+ if sys.argv[0].endswith("-script.pyw"):
7
+ sys.argv[0] = sys.argv[0][:-11]
8
+ elif sys.argv[0].endswith(".exe"):
9
+ sys.argv[0] = sys.argv[0][:-4]
10
+ sys.exit(main())
bin/diffusers-cli ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/home/zeus/miniconda3/envs/cloudspace/bin/python
2
+ # -*- coding: utf-8 -*-
3
+ import sys
4
+ from diffusers.commands.diffusers_cli import main
5
+ if __name__ == "__main__":
6
+ if sys.argv[0].endswith("-script.pyw"):
7
+ sys.argv[0] = sys.argv[0][:-11]
8
+ elif sys.argv[0].endswith(".exe"):
9
+ sys.argv[0] = sys.argv[0][:-4]
10
+ sys.exit(main())
bin/f2py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/home/zeus/miniconda3/envs/cloudspace/bin/python
2
+ # -*- coding: utf-8 -*-
3
+ import sys
4
+ from numpy.f2py.f2py2e import main
5
+ if __name__ == "__main__":
6
+ if sys.argv[0].endswith("-script.pyw"):
7
+ sys.argv[0] = sys.argv[0][:-11]
8
+ elif sys.argv[0].endswith(".exe"):
9
+ sys.argv[0] = sys.argv[0][:-4]
10
+ sys.exit(main())
bin/get_gprof ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/home/zeus/miniconda3/envs/cloudspace/bin/python
2
+ #
3
+ # Author: Mike McKerns (mmckerns @caltech and @uqfoundation)
4
+ # Copyright (c) 2008-2016 California Institute of Technology.
5
+ # Copyright (c) 2016-2025 The Uncertainty Quantification Foundation.
6
+ # License: 3-clause BSD. The full license text is available at:
7
+ # - https://github.com/uqfoundation/dill/blob/master/LICENSE
8
+ '''
9
+ build profile graph for the given instance
10
+
11
+ running:
12
+ $ get_gprof <args> <instance>
13
+
14
+ executes:
15
+ gprof2dot -f pstats <args> <type>.prof | dot -Tpng -o <type>.call.png
16
+
17
+ where:
18
+ <args> are arguments for gprof2dot, such as "-n 5 -e 5"
19
+ <instance> is code to create the instance to profile
20
+ <type> is the class of the instance (i.e. type(instance))
21
+
22
+ For example:
23
+ $ get_gprof -n 5 -e 1 "import numpy; numpy.array([1,2])"
24
+
25
+ will create 'ndarray.call.png' with the profile graph for numpy.array([1,2]),
26
+ where '-n 5' eliminates nodes below 5% threshold, similarly '-e 1' eliminates
27
+ edges below 1% threshold
28
+ '''
29
+
30
+ if __name__ == "__main__":
31
+ import sys
32
+ if len(sys.argv) < 2:
33
+ print ("Please provide an object instance (e.g. 'import math; math.pi')")
34
+ sys.exit()
35
+ # grab args for gprof2dot
36
+ args = sys.argv[1:-1]
37
+ args = ' '.join(args)
38
+ # last arg builds the object
39
+ obj = sys.argv[-1]
40
+ obj = obj.split(';')
41
+ # multi-line prep for generating an instance
42
+ for line in obj[:-1]:
43
+ exec(line)
44
+ # one-line generation of an instance
45
+ try:
46
+ obj = eval(obj[-1])
47
+ except Exception:
48
+ print ("Error processing object instance")
49
+ sys.exit()
50
+
51
+ # get object 'name'
52
+ objtype = type(obj)
53
+ name = getattr(objtype, '__name__', getattr(objtype, '__class__', objtype))
54
+
55
+ # profile dumping an object
56
+ import dill
57
+ import os
58
+ import cProfile
59
+ #name = os.path.splitext(os.path.basename(__file__))[0]
60
+ cProfile.run("dill.dumps(obj)", filename="%s.prof" % name)
61
+ msg = "gprof2dot -f pstats %s %s.prof | dot -Tpng -o %s.call.png" % (args, name, name)
62
+ try:
63
+ res = os.system(msg)
64
+ except Exception:
65
+ print ("Please verify install of 'gprof2dot' to view profile graphs")
66
+ if res:
67
+ print ("Please verify install of 'gprof2dot' to view profile graphs")
68
+
69
+ # get stats
70
+ f_prof = "%s.prof" % name
71
+ import pstats
72
+ stats = pstats.Stats(f_prof, stream=sys.stdout)
73
+ stats.strip_dirs().sort_stats('cumtime')
74
+ stats.print_stats(20) #XXX: save to file instead of print top 20?
75
+ os.remove(f_prof)
bin/get_objgraph ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/home/zeus/miniconda3/envs/cloudspace/bin/python
2
+ #
3
+ # Author: Mike McKerns (mmckerns @caltech and @uqfoundation)
4
+ # Copyright (c) 2008-2016 California Institute of Technology.
5
+ # Copyright (c) 2016-2025 The Uncertainty Quantification Foundation.
6
+ # License: 3-clause BSD. The full license text is available at:
7
+ # - https://github.com/uqfoundation/dill/blob/master/LICENSE
8
+ """
9
+ display the reference paths for objects in ``dill.types`` or a .pkl file
10
+
11
+ Notes:
12
+ the generated image is useful in showing the pointer references in
13
+ objects that are or can be pickled. Any object in ``dill.objects``
14
+ listed in ``dill.load_types(picklable=True, unpicklable=True)`` works.
15
+
16
+ Examples::
17
+
18
+ $ get_objgraph ArrayType
19
+ Image generated as ArrayType.png
20
+ """
21
+
22
+ import dill as pickle
23
+ #pickle.debug.trace(True)
24
+ #import pickle
25
+
26
+ # get all objects for testing
27
+ from dill import load_types
28
+ load_types(pickleable=True,unpickleable=True)
29
+ from dill import objects
30
+
31
+ if __name__ == "__main__":
32
+ import sys
33
+ if len(sys.argv) != 2:
34
+ print ("Please provide exactly one file or type name (e.g. 'IntType')")
35
+ msg = "\n"
36
+ for objtype in list(objects.keys())[:40]:
37
+ msg += objtype + ', '
38
+ print (msg + "...")
39
+ else:
40
+ objtype = str(sys.argv[-1])
41
+ try:
42
+ obj = objects[objtype]
43
+ except KeyError:
44
+ obj = pickle.load(open(objtype,'rb'))
45
+ import os
46
+ objtype = os.path.splitext(objtype)[0]
47
+ try:
48
+ import objgraph
49
+ objgraph.show_refs(obj, filename=objtype+'.png')
50
+ except ImportError:
51
+ print ("Please install 'objgraph' to view object graphs")
52
+
53
+
54
+ # EOF
bin/hf ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/home/zeus/miniconda3/envs/cloudspace/bin/python
2
+ # -*- coding: utf-8 -*-
3
+ import sys
4
+ from huggingface_hub.cli.hf import main
5
+ if __name__ == "__main__":
6
+ if sys.argv[0].endswith("-script.pyw"):
7
+ sys.argv[0] = sys.argv[0][:-11]
8
+ elif sys.argv[0].endswith(".exe"):
9
+ sys.argv[0] = sys.argv[0][:-4]
10
+ sys.exit(main())
bin/httpx ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/home/zeus/miniconda3/envs/cloudspace/bin/python
2
+ # -*- coding: utf-8 -*-
3
+ import sys
4
+ from httpx import main
5
+ if __name__ == "__main__":
6
+ if sys.argv[0].endswith("-script.pyw"):
7
+ sys.argv[0] = sys.argv[0][:-11]
8
+ elif sys.argv[0].endswith(".exe"):
9
+ sys.argv[0] = sys.argv[0][:-4]
10
+ sys.exit(main())
bin/isympy ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/home/zeus/miniconda3/envs/cloudspace/bin/python
2
+ # -*- coding: utf-8 -*-
3
+ import sys
4
+ from isympy import main
5
+ if __name__ == "__main__":
6
+ if sys.argv[0].endswith("-script.pyw"):
7
+ sys.argv[0] = sys.argv[0][:-11]
8
+ elif sys.argv[0].endswith(".exe"):
9
+ sys.argv[0] = sys.argv[0][:-4]
10
+ sys.exit(main())
bin/markdown-it ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/home/zeus/miniconda3/envs/cloudspace/bin/python
2
+ # -*- coding: utf-8 -*-
3
+ import sys
4
+ from markdown_it.cli.parse import main
5
+ if __name__ == "__main__":
6
+ if sys.argv[0].endswith("-script.pyw"):
7
+ sys.argv[0] = sys.argv[0][:-11]
8
+ elif sys.argv[0].endswith(".exe"):
9
+ sys.argv[0] = sys.argv[0][:-4]
10
+ sys.exit(main())
bin/normalizer ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/home/zeus/miniconda3/envs/cloudspace/bin/python
2
+ # -*- coding: utf-8 -*-
3
+ import sys
4
+ from charset_normalizer.cli import cli_detect
5
+ if __name__ == "__main__":
6
+ if sys.argv[0].endswith("-script.pyw"):
7
+ sys.argv[0] = sys.argv[0][:-11]
8
+ elif sys.argv[0].endswith(".exe"):
9
+ sys.argv[0] = sys.argv[0][:-4]
10
+ sys.exit(cli_detect())
bin/proton ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/home/zeus/miniconda3/envs/cloudspace/bin/python
2
+ # -*- coding: utf-8 -*-
3
+ import sys
4
+ from triton.profiler.proton import main
5
+ if __name__ == "__main__":
6
+ if sys.argv[0].endswith("-script.pyw"):
7
+ sys.argv[0] = sys.argv[0][:-11]
8
+ elif sys.argv[0].endswith(".exe"):
9
+ sys.argv[0] = sys.argv[0][:-4]
10
+ sys.exit(main())
bin/proton-viewer ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/home/zeus/miniconda3/envs/cloudspace/bin/python
2
+ # -*- coding: utf-8 -*-
3
+ import sys
4
+ from triton.profiler.viewer import main
5
+ if __name__ == "__main__":
6
+ if sys.argv[0].endswith("-script.pyw"):
7
+ sys.argv[0] = sys.argv[0][:-11]
8
+ elif sys.argv[0].endswith(".exe"):
9
+ sys.argv[0] = sys.argv[0][:-4]
10
+ sys.exit(main())
bin/pygmentize ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/home/zeus/miniconda3/envs/cloudspace/bin/python
2
+ # -*- coding: utf-8 -*-
3
+ import sys
4
+ from pygments.cmdline import main
5
+ if __name__ == "__main__":
6
+ if sys.argv[0].endswith("-script.pyw"):
7
+ sys.argv[0] = sys.argv[0][:-11]
8
+ elif sys.argv[0].endswith(".exe"):
9
+ sys.argv[0] = sys.argv[0][:-4]
10
+ sys.exit(main())
bin/tiny-agents ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/home/zeus/miniconda3/envs/cloudspace/bin/python
2
+ # -*- coding: utf-8 -*-
3
+ import sys
4
+ from huggingface_hub.inference._mcp.cli import app
5
+ if __name__ == "__main__":
6
+ if sys.argv[0].endswith("-script.pyw"):
7
+ sys.argv[0] = sys.argv[0][:-11]
8
+ elif sys.argv[0].endswith(".exe"):
9
+ sys.argv[0] = sys.argv[0][:-4]
10
+ sys.exit(app())
bin/torchfrtrace ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/home/zeus/miniconda3/envs/cloudspace/bin/python
2
+ # -*- coding: utf-8 -*-
3
+ import sys
4
+ from torch.distributed.flight_recorder.fr_trace import main
5
+ if __name__ == "__main__":
6
+ if sys.argv[0].endswith("-script.pyw"):
7
+ sys.argv[0] = sys.argv[0][:-11]
8
+ elif sys.argv[0].endswith(".exe"):
9
+ sys.argv[0] = sys.argv[0][:-4]
10
+ sys.exit(main())
bin/torchrun ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/home/zeus/miniconda3/envs/cloudspace/bin/python
2
+ # -*- coding: utf-8 -*-
3
+ import sys
4
+ from torch.distributed.run import main
5
+ if __name__ == "__main__":
6
+ if sys.argv[0].endswith("-script.pyw"):
7
+ sys.argv[0] = sys.argv[0][:-11]
8
+ elif sys.argv[0].endswith(".exe"):
9
+ sys.argv[0] = sys.argv[0][:-4]
10
+ sys.exit(main())
bin/tqdm ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/home/zeus/miniconda3/envs/cloudspace/bin/python
2
+ # -*- coding: utf-8 -*-
3
+ import sys
4
+ from tqdm.cli import main
5
+ if __name__ == "__main__":
6
+ if sys.argv[0].endswith("-script.pyw"):
7
+ sys.argv[0] = sys.argv[0][:-11]
8
+ elif sys.argv[0].endswith(".exe"):
9
+ sys.argv[0] = sys.argv[0][:-4]
10
+ sys.exit(main())