diff --git a/micropython/uaiohttpclient/clientsession_example.py b/micropython/uaiohttpclient/clientsession_example.py new file mode 100644 index 000000000..d03cf0b8e --- /dev/null +++ b/micropython/uaiohttpclient/clientsession_example.py @@ -0,0 +1,18 @@ +import uaiohttpclient as aiohttp +import asyncio + + +async def fetch(client): + async with client.get("http://micropython.org") as resp: + assert resp.status == 200 + return await resp.text() + + +async def main(): + async with aiohttp.ClientSession() as client: + html = await fetch(client) + print(html.decode()) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/micropython/uaiohttpclient/clientsession_methods_example.py b/micropython/uaiohttpclient/clientsession_methods_example.py new file mode 100644 index 000000000..3ea83d731 --- /dev/null +++ b/micropython/uaiohttpclient/clientsession_methods_example.py @@ -0,0 +1,22 @@ +import uaiohttpclient as aiohttp +import asyncio + + +async def main(): + async with aiohttp.ClientSession("http://httpbin.org") as session: + async with session.get("/get") as resp: + assert resp.status == 200 + rget = await resp.text() + print(f"GET: {rget.decode()}") + async with session.post("/post", json={"foo": "bar"}) as resp: + assert resp.status == 200 + rpost = await resp.text() + print(f"POST: {rpost.decode()}") + async with session.put("/put", data=b"data") as resp: + assert resp.status == 200 + rput = await resp.json() + print("PUT: ", rput) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/micropython/uaiohttpclient/uaiohttpclient.py b/micropython/uaiohttpclient/uaiohttpclient.py index 25b2e62a9..d1afb2b21 100644 --- a/micropython/uaiohttpclient/uaiohttpclient.py +++ b/micropython/uaiohttpclient/uaiohttpclient.py @@ -1,4 +1,5 @@ import uasyncio as asyncio +import json as _json class ClientResponse: @@ -8,6 +9,12 @@ def __init__(self, reader): def read(self, sz=-1): return (yield from self.content.read(sz)) + def text(self, sz=-1): + return self.read(sz=sz) + + async def json(self): + return _json.loads(await self.read()) + def __repr__(self): return "" % (self.status, self.headers) @@ -40,15 +47,186 @@ def __repr__(self): return "" % (self.status, self.headers) +class _RequestContextManager: + def __init__(self, client, request_co): + self.reqco = request_co + self.client = client + + async def __aenter__(self): + return await self.reqco + + async def __aexit__(self, *args): + await self.client._reader.aclose() + return await asyncio.sleep(0) + + +class ClientSession: + def __init__(self, base_url=""): + self._reader = None + self._base_url = base_url + + async def __aenter__(self): + return self + + async def __aexit__(self, *args): + return await asyncio.sleep(0) + + def request(self, method, url, data=None, json=None, ssl=None): + return _RequestContextManager( + self, + self._request(method, self._base_url + url, data=data, json=json, ssl=ssl), + ) + + async def _request(self, method, url, data=None, json=None, ssl=None): + redir_cnt = 0 + redir_url = None + while redir_cnt < 2: + reader = await self.request_raw(method, url, data, json, ssl) + headers = [] + sline = await reader.readline() + sline = sline.split(None, 2) + status = int(sline[1]) + chunked = False + while True: + line = await reader.readline() + if not line or line == b"\r\n": + break + headers.append(line) + if line.startswith(b"Transfer-Encoding:"): + if b"chunked" in line: + chunked = True + elif line.startswith(b"Location:"): + url = line.rstrip().split(None, 1)[1].decode("latin-1") + + if 301 <= status <= 303: + redir_cnt += 1 + await reader.aclose() + continue + break + + if chunked: + resp = ChunkedClientResponse(reader) + else: + resp = ClientResponse(reader) + resp.status = status + resp.headers = headers + self._reader = reader + return resp + + async def request_raw(self, method, url, data=None, json=None, ssl=None): + if json and isinstance(json, dict): + data = _json.dumps(json) + if data is not None and method == "GET": + method = "POST" + try: + proto, dummy, host, path = url.split("/", 3) + except ValueError: + proto, dummy, host = url.split("/", 2) + path = "" + + if proto == "http:": + port = 80 + elif proto == "https:": + port = 443 + if ssl is None: + ssl = True + else: + raise ValueError("Unsupported protocol: " + proto) + + if ":" in host: + host, port = host.split(":", 1) + port = int(port) + + reader, writer = await asyncio.open_connection(host, port, ssl=ssl) + + # Use protocol 1.0, because 1.1 always allows to use chunked transfer-encoding + # But explicitly set Connection: close, even though this should be default for 1.0, + # because some servers misbehave w/o it. + if not data: + query = ( + "%s /%s HTTP/1.0\r\nHost: %s\r\nConnection: close\r\nUser-Agent: compat\r\n\r\n" + % ( + method, + path, + host, + ) + ) + else: + query = ( + """%s /%s HTTP/1.0\r\nHost: %s\r\n%sContent-Length: %s\r\n\r\n%s\r\nConnection: close\r\nUser-Agent: compat\r\n\r\n""" + % ( + method, + path, + host, + "Content-Type: application/json\r\n" if json else "", + str(len(str(data))), + data, + ) + ) + + await writer.awrite(query.encode("latin-1")) + # yield from writer.aclose() + return reader + + def get(self, url, ssl=None): + return _RequestContextManager(self, self._request("GET", self._base_url + url, ssl=ssl)) + + def post(self, url, data=None, json=None, ssl=None): + return _RequestContextManager( + self, + self._request("POST", self._base_url + url, data=data, json=json, ssl=ssl), + ) + + def put(self, url, data=None, json=None, ssl=None): + return _RequestContextManager( + self, + self._request("PUT", self._base_url + url, data=data, json=json, ssl=ssl), + ) + + def patch(self, url, data=None, json=None, ssl=None): + return _RequestContextManager( + self, + self._request("PATCH", self._base_url + url, data=data, json=json, ssl=ssl), + ) + + def delete(self, url, ssl=None): + return _RequestContextManager( + self, + self._request("DELETE", self._base_url + url, ssl=ssl), + ) + + def head(self, url, ssl=None): + return _RequestContextManager( + self, + self._request("HEAD", self._base_url + url, ssl=ssl), + ) + + def options(self, url, ssl=None): + return _RequestContextManager( + self, + self._request("OPTIONS", self._base_url + url, ssl=ssl), + ) + + def request_raw(method, url): try: proto, dummy, host, path = url.split("/", 3) except ValueError: proto, dummy, host = url.split("/", 2) path = "" - if proto != "http:": + + if proto == "http:": + port = 80 + elif proto == "https:": + port = 443 + else: raise ValueError("Unsupported protocol: " + proto) - reader, writer = yield from asyncio.open_connection(host, 80) + + if ":" in host: + host, port = host.split(":", 1) + port = int(port) + + reader, writer = yield from asyncio.open_connection(host, port, ssl=proto == "https:") # Use protocol 1.0, because 1.1 always allows to use chunked transfer-encoding # But explicitly set Connection: close, even though this should be default for 1.0, # because some servers misbehave w/o it.