Source code for snug.clients

"""Funtions for dealing with for HTTP clients in a unified manner"""
import asyncio
import sys
import urllib.request
from functools import partial, singledispatch
from http.client import HTTPResponse
from io import BytesIO
from itertools import starmap
from urllib.error import HTTPError
from urllib.parse import urlencode

from .http import Response

__all__ = ["send", "send_async"]


_ASYNCIO_USER_AGENT = "Python-asyncio/3.{}".format(sys.version_info.minor)


[docs]@singledispatch def send(client, request): """Given a client, send a :class:`~snug.http.Request`, returning a :class:`~snug.http.Response`. A :func:`~functools.singledispatch` function. Parameters ---------- client: any registered client type The client with which to send the request. Client types registered by default: * :class:`urllib.request.OpenerDirector` (e.g. from :func:`~urllib.request.build_opener`) * :class:`requests.Session` (if `requests <http://docs.python-requests.org/>`_ is installed) request: Request The request to send Returns ------- Response the resulting response Example of registering a new HTTP client: >>> @send.register(MyClientClass) ... def _send(client, request: Request) -> Response: ... r = client.send(request) ... return Response(r.status, r.read(), headers=r.get_headers()) """ raise TypeError("client {!r} not registered".format(client))
[docs]@singledispatch def send_async(client, request): """Given a client, send a :class:`~snug.http.Request`, returning an awaitable :class:`~snug.http.Response`. A :func:`~functools.singledispatch` function. Parameters ---------- client: any registered client type The client with which to send the request. Client types supported by default: * :class:`asyncio.AbstractEventLoop` (e.g. from :func:`~asyncio.get_event_loop`) * :class:`aiohttp.ClientSession` (if `aiohttp <http://aiohttp.readthedocs.io/>`_ is installed) request: Request The request to send Returns ------- Response the resulting response Example of registering a new HTTP client: >>> @send_async.register(MyClientClass) ... async def _send(client, request: Request) -> Response: ... r = await client.send(request) ... return Response(r.status, r.read(), headers=r.get_headers()) """ raise TypeError("client {!r} not registered".format(client))
@send.register(urllib.request.OpenerDirector) def _urllib_send(opener, req, **kwargs): """Send a request with an :mod:`urllib` opener""" if req.content and not any( h.lower() == "content-type" for h in req.headers ): req = req.with_headers({"Content-Type": "application/octet-stream"}) url = req.url + "?" + urlencode(req.params) raw_req = urllib.request.Request(url, req.content, headers=req.headers) raw_req.method = req.method try: res = opener.open(raw_req, **kwargs) except HTTPError as http_err: res = http_err return Response(res.getcode(), content=res.read(), headers=res.headers) class _SocketAdaptor: def __init__(self, io): self._file = io def makefile(self, *args, **kwargs): return self._file @send_async.register(type(None)) async def _asyncio_send(_, req, *, timeout=10, max_redirects=10): """A rudimentary HTTP client using :mod:`asyncio`""" if not any(h.lower() == "user-agent" for h in req.headers): req = req.with_headers({"User-Agent": _ASYNCIO_USER_AGENT}) url = urllib.parse.urlsplit( req.url + "?" + urllib.parse.urlencode(req.params) ) open_ = partial(asyncio.open_connection, url.hostname) connect = open_(443, ssl=True) if url.scheme == "https" else open_(80) reader, writer = await connect try: headers = "\r\n".join( [ "{} {} HTTP/1.1".format( req.method, url.path + "?" + url.query ), "Host: " + url.hostname, "Connection: close", "Content-Length: {}".format(len(req.content or b"")), "\r\n".join(starmap("{}: {}".format, req.headers.items())), ] ) writer.write( b"\r\n".join([headers.encode("latin-1"), b"", req.content or b""]) ) response_bytes = BytesIO( await asyncio.wait_for(reader.read(), timeout=timeout) ) finally: writer.close() resp = HTTPResponse( _SocketAdaptor(response_bytes), method=req.method, url=req.url ) resp.begin() status = resp.getcode() if 300 <= status < 400 and "Location" in resp.headers and max_redirects: new_url = urllib.parse.urljoin(req.url, resp.headers["Location"]) return await _asyncio_send( None, req.replace(url=new_url), timeout=timeout, max_redirects=max_redirects - 1, ) return Response(status, content=resp.read(), headers=resp.headers) try: import requests except ImportError: # pragma: no cover pass else: @send.register(requests.Session) def _requests_send(session, req): """send a request with the `requests` library""" res = session.request( req.method, req.url, data=req.content, params=req.params, headers=req.headers, ) return Response(res.status_code, res.content, headers=res.headers) try: import aiohttp except ImportError: # pragma: no cover pass else: @send_async.register(aiohttp.ClientSession) async def _aiohttp_send(session, req): """send a request with the `aiohttp` library""" async with session.request( req.method, req.url, params=req.params, data=req.content, headers=req.headers, ) as resp: return Response( resp.status, content=await resp.read(), headers=resp.headers ) try: import httpx except ImportError: # pragma: no cover pass else: @send.register(httpx.Client) def _httpx_send_sync(client, req): """send a request with the `httpx` library""" res = client.request( req.method, req.url, params=req.params, content=req.content, headers=req.headers, ) return Response(res.status_code, res.content, headers=res.headers) @send_async.register(httpx.AsyncClient) async def _httpx_send_async(client, req): """send a request with the `httpx` library""" res = await client.request( req.method, req.url, params=req.params, content=req.content, headers=req.headers, ) return Response(res.status_code, res.content, headers=res.headers)