Skip to content

Commit 2f8238f

Browse files
committed
micropython/aiohttp: Add 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 0620d02 commit 2f8238f

File tree

12 files changed

+863
-0
lines changed

12 files changed

+863
-0
lines changed

python-ecosys/aiohttp/README.md

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

0 commit comments

Comments
 (0)