Skip to content

Commit 7cdf708

Browse files
Carglglzdpgeorge
authored andcommitted
aiohttp: Add new aiohttp package.
Implement `aiohttp` with `ClientSession`, websockets and `SSLContext` support. Only client is implemented and API is mostly compatible with CPython `aiohttp`. Signed-off-by: Carlos Gil <carlosgilglez@gmail.com>
1 parent 57ce3ba commit 7cdf708

File tree

12 files changed

+799
-0
lines changed

12 files changed

+799
-0
lines changed

python-ecosys/aiohttp/README.md

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
aiohttp is an HTTP client module for MicroPython asyncio module,
2+
with API mostly compatible with CPython [aiohttp](https://github.com/aio-libs/aiohttp)
3+
module.
4+
5+
> [!NOTE]
6+
> Only client is implemented.
7+
8+
See `examples/client.py`
9+
```py
10+
import aiohttp
11+
import asyncio
12+
13+
async def main():
14+
15+
async with aiohttp.ClientSession() as session:
16+
async with session.get('http://micropython.org') as response:
17+
18+
print("Status:", response.status)
19+
print("Content-Type:", response.headers['Content-Type'])
20+
21+
html = await response.text()
22+
print("Body:", html[:15], "...")
23+
24+
asyncio.run(main())
25+
```
26+
```
27+
$ micropython examples/client.py
28+
Status: 200
29+
Content-Type: text/html; charset=utf-8
30+
Body: <!DOCTYPE html> ...
31+
32+
```
+264
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
# MicroPython aiohttp library
2+
# MIT license; Copyright (c) 2023 Carlos Gil
3+
4+
import asyncio
5+
import json as _json
6+
from .aiohttp_ws import (
7+
_WSRequestContextManager,
8+
ClientWebSocketResponse,
9+
WebSocketClient,
10+
WSMsgType,
11+
)
12+
13+
HttpVersion10 = "HTTP/1.0"
14+
HttpVersion11 = "HTTP/1.1"
15+
16+
17+
class ClientResponse:
18+
def __init__(self, reader):
19+
self.content = reader
20+
21+
def _decode(self, data):
22+
c_encoding = self.headers.get("Content-Encoding")
23+
if c_encoding in ("gzip", "deflate", "gzip,deflate"):
24+
try:
25+
import deflate, io
26+
27+
if c_encoding == "deflate":
28+
with deflate.DeflateIO(io.BytesIO(data), deflate.ZLIB) as d:
29+
return d.read()
30+
elif c_encoding == "gzip":
31+
with deflate.DeflateIO(io.BytesIO(data), deflate.GZIP, 15) as d:
32+
return d.read()
33+
except ImportError:
34+
print("WARNING: deflate module required")
35+
return data
36+
37+
async def read(self, sz=-1):
38+
return self._decode(await self.content.read(sz))
39+
40+
async def text(self, encoding="utf-8"):
41+
return (await self.read(sz=-1)).decode(encoding)
42+
43+
async def json(self):
44+
return _json.loads(await self.read())
45+
46+
def __repr__(self):
47+
return "<ClientResponse %d %s>" % (self.status, self.headers)
48+
49+
50+
class ChunkedClientResponse(ClientResponse):
51+
def __init__(self, reader):
52+
self.content = reader
53+
self.chunk_size = 0
54+
55+
async def read(self, sz=4 * 1024 * 1024):
56+
if self.chunk_size == 0:
57+
l = await self.content.readline()
58+
l = l.split(b";", 1)[0]
59+
self.chunk_size = int(l, 16)
60+
if self.chunk_size == 0:
61+
# End of message
62+
sep = await self.content.read(2)
63+
assert sep == b"\r\n"
64+
return b""
65+
data = await self.content.read(min(sz, self.chunk_size))
66+
self.chunk_size -= len(data)
67+
if self.chunk_size == 0:
68+
sep = await self.content.read(2)
69+
assert sep == b"\r\n"
70+
return self._decode(data)
71+
72+
def __repr__(self):
73+
return "<ChunkedClientResponse %d %s>" % (self.status, self.headers)
74+
75+
76+
class _RequestContextManager:
77+
def __init__(self, client, request_co):
78+
self.reqco = request_co
79+
self.client = client
80+
81+
async def __aenter__(self):
82+
return await self.reqco
83+
84+
async def __aexit__(self, *args):
85+
await self.client._reader.aclose()
86+
return await asyncio.sleep(0)
87+
88+
89+
class ClientSession:
90+
def __init__(self, base_url="", headers={}, version=HttpVersion10):
91+
self._reader = None
92+
self._base_url = base_url
93+
self._base_headers = {"Connection": "close", "User-Agent": "compat"}
94+
self._base_headers.update(**headers)
95+
self._http_version = version
96+
97+
async def __aenter__(self):
98+
return self
99+
100+
async def __aexit__(self, *args):
101+
return await asyncio.sleep(0)
102+
103+
# TODO: Implement timeouts
104+
105+
async def _request(self, method, url, data=None, json=None, ssl=None, params=None, headers={}):
106+
redir_cnt = 0
107+
redir_url = None
108+
while redir_cnt < 2:
109+
reader = await self.request_raw(method, url, data, json, ssl, params, headers)
110+
_headers = []
111+
sline = await reader.readline()
112+
sline = sline.split(None, 2)
113+
status = int(sline[1])
114+
chunked = False
115+
while True:
116+
line = await reader.readline()
117+
if not line or line == b"\r\n":
118+
break
119+
_headers.append(line)
120+
if line.startswith(b"Transfer-Encoding:"):
121+
if b"chunked" in line:
122+
chunked = True
123+
elif line.startswith(b"Location:"):
124+
url = line.rstrip().split(None, 1)[1].decode("latin-1")
125+
126+
if 301 <= status <= 303:
127+
redir_cnt += 1
128+
await reader.aclose()
129+
continue
130+
break
131+
132+
if chunked:
133+
resp = ChunkedClientResponse(reader)
134+
else:
135+
resp = ClientResponse(reader)
136+
resp.status = status
137+
resp.headers = _headers
138+
resp.url = url
139+
if params:
140+
resp.url += "?" + "&".join(f"{k}={params[k]}" for k in sorted(params))
141+
try:
142+
resp.headers = {
143+
val.split(":", 1)[0]: val.split(":", 1)[-1].strip()
144+
for val in [hed.decode().strip() for hed in _headers]
145+
}
146+
except Exception:
147+
pass
148+
self._reader = reader
149+
return resp
150+
151+
async def request_raw(
152+
self,
153+
method,
154+
url,
155+
data=None,
156+
json=None,
157+
ssl=None,
158+
params=None,
159+
headers={},
160+
is_handshake=False,
161+
version=None,
162+
):
163+
if json and isinstance(json, dict):
164+
data = _json.dumps(json)
165+
if data is not None and method == "GET":
166+
method = "POST"
167+
if params:
168+
url += "?" + "&".join(f"{k}={params[k]}" for k in sorted(params))
169+
try:
170+
proto, dummy, host, path = url.split("/", 3)
171+
except ValueError:
172+
proto, dummy, host = url.split("/", 2)
173+
path = ""
174+
175+
if proto == "http:":
176+
port = 80
177+
elif proto == "https:":
178+
port = 443
179+
if ssl is None:
180+
ssl = True
181+
else:
182+
raise ValueError("Unsupported protocol: " + proto)
183+
184+
if ":" in host:
185+
host, port = host.split(":", 1)
186+
port = int(port)
187+
188+
reader, writer = await asyncio.open_connection(host, port, ssl=ssl)
189+
190+
# Use protocol 1.0, because 1.1 always allows to use chunked transfer-encoding
191+
# But explicitly set Connection: close, even though this should be default for 1.0,
192+
# because some servers misbehave w/o it.
193+
if version is None:
194+
version = self._http_version
195+
if "Host" not in headers:
196+
headers.update(Host=host)
197+
if not data:
198+
query = "%s /%s %s\r\n%s\r\n" % (
199+
method,
200+
path,
201+
version,
202+
"\r\n".join(f"{k}: {v}" for k, v in headers.items()) + "\r\n" if headers else "",
203+
)
204+
else:
205+
headers.update(**{"Content-Length": len(str(data))})
206+
if json:
207+
headers.update(**{"Content-Type": "application/json"})
208+
query = """%s /%s %s\r\n%s\r\n%s\r\n\r\n""" % (
209+
method,
210+
path,
211+
version,
212+
"\r\n".join(f"{k}: {v}" for k, v in headers.items()) + "\r\n",
213+
data,
214+
)
215+
if not is_handshake:
216+
await writer.awrite(query.encode("latin-1"))
217+
return reader
218+
else:
219+
await writer.awrite(query.encode())
220+
return reader, writer
221+
222+
def request(self, method, url, data=None, json=None, ssl=None, params=None, headers={}):
223+
return _RequestContextManager(
224+
self,
225+
self._request(
226+
method,
227+
self._base_url + url,
228+
data=data,
229+
json=json,
230+
ssl=ssl,
231+
params=params,
232+
headers=dict(**self._base_headers, **headers),
233+
),
234+
)
235+
236+
def get(self, url, **kwargs):
237+
return self.request("GET", url, **kwargs)
238+
239+
def post(self, url, **kwargs):
240+
return self.request("POST", url, **kwargs)
241+
242+
def put(self, url, **kwargs):
243+
return self.request("PUT", url, **kwargs)
244+
245+
def patch(self, url, **kwargs):
246+
return self.request("PATCH", url, **kwargs)
247+
248+
def delete(self, url, **kwargs):
249+
return self.request("DELETE", url, **kwargs)
250+
251+
def head(self, url, **kwargs):
252+
return self.request("HEAD", url, **kwargs)
253+
254+
def options(self, url, **kwargs):
255+
return self.request("OPTIONS", url, **kwargs)
256+
257+
def ws_connect(self, url, ssl=None):
258+
return _WSRequestContextManager(self, self._ws_connect(url, ssl=ssl))
259+
260+
async def _ws_connect(self, url, ssl=None):
261+
ws_client = WebSocketClient(None)
262+
await ws_client.connect(url, ssl=ssl, handshake_request=self.request_raw)
263+
self._reader = ws_client.reader
264+
return ClientWebSocketResponse(ws_client)

0 commit comments

Comments
 (0)