Skip to content

Commit 959e028

Browse files
add aiohttp (again?!)
1 parent c923ad3 commit 959e028

File tree

4 files changed

+555
-0
lines changed

4 files changed

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

0 commit comments

Comments
 (0)