Add Async client, migrate from requests to httpx
This commit is contained in:
@ -1,4 +1,5 @@
|
||||
from .client import BaseAPIClient
|
||||
from .async_client import BaseAPIClient as AsyncClient
|
||||
from .sync_client import BaseAPIClient as SyncClient
|
||||
|
||||
__version__ = "1.0.1"
|
||||
__all__ = ["BaseAPIClient"]
|
||||
__all__ = ["SyncClient", "AsyncClient"]
|
||||
|
116
src/api_client/_base.py
Normal file
116
src/api_client/_base.py
Normal file
@ -0,0 +1,116 @@
|
||||
import logging
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any, Coroutine
|
||||
|
||||
import httpx
|
||||
from pydantic import HttpUrl
|
||||
|
||||
from .utils import get_logger
|
||||
from .utils.logs import get_nonce, shortify_log_value
|
||||
|
||||
_TypeSyncAsyncResponse = httpx.Response | Coroutine[Any, Any, httpx.Response]
|
||||
|
||||
|
||||
class BaseClient(ABC):
|
||||
base_url: HttpUrl
|
||||
_session: httpx._client.BaseClient
|
||||
_nonce: str
|
||||
_logger: logging.Logger
|
||||
|
||||
def __init__(self, nonce: str = "", session: httpx._client.BaseClient | None = None):
|
||||
self._nonce = nonce or get_nonce()
|
||||
self._session = session or httpx.Client()
|
||||
self._logger = get_logger("APIClient")
|
||||
|
||||
@abstractmethod
|
||||
def get(self, endpoint: str, *, params: Any = None, **kwargs: Any) -> _TypeSyncAsyncResponse:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def post(self, endpoint: str, *, json: Any = None, data: Any = None, **kwargs: Any) -> _TypeSyncAsyncResponse:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def put(self, endpoint: str, *, json: Any = None, data: Any = None, **kwargs: Any) -> _TypeSyncAsyncResponse:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def patch(self, endpoint: str, *, json: Any = None, data: Any = None, **kwargs: Any) -> _TypeSyncAsyncResponse:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def delete(self, endpoint: str, *, params: Any = None, **kwargs: Any) -> _TypeSyncAsyncResponse:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def head(self, endpoint: str, *, params: Any = None, **kwargs: Any) -> _TypeSyncAsyncResponse:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def options(self, endpoint: str, *, params: Any = None, **kwargs: Any) -> _TypeSyncAsyncResponse:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def trace(self, endpoint: str, *, params: Any = None, **kwargs: Any) -> _TypeSyncAsyncResponse:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def connect(self, endpoint: str, *, params: Any = None, **kwargs: Any) -> _TypeSyncAsyncResponse:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def _request(self, method: str, endpoint: str, **kwargs: Any) -> _TypeSyncAsyncResponse:
|
||||
pass
|
||||
|
||||
def _make_full_url(self, endpoint: str) -> str:
|
||||
return f"{self.base_url}{endpoint}"
|
||||
|
||||
def prepare_authentication(self, request: httpx.Request) -> httpx.Request:
|
||||
return request
|
||||
|
||||
def _prepare_request(self, method: str, endpoint: str, **kwargs: Any) -> httpx.Request:
|
||||
full_url = self._make_full_url(endpoint)
|
||||
|
||||
self._debug_log_request(method, full_url)
|
||||
request: httpx.Request = self._session.build_request(method=method, url=full_url, **kwargs)
|
||||
self._debug_log_prepared_request(request)
|
||||
self._debug(f"Prepared {request.method} request to '{request.url}'", extra=request.__dict__)
|
||||
|
||||
self._info_log_request_sending(request, kwargs.get("content") or kwargs.get("files") or kwargs.get("data") or kwargs.get("json"))
|
||||
return request
|
||||
|
||||
def _debug_log_request(self, method: str, full_url: str) -> None:
|
||||
self._debug(f"Preparing {method} request for '{full_url}'")
|
||||
|
||||
def _debug_log_prepared_request(self, request: httpx.Request) -> None:
|
||||
self._debug(f"Prepared {request.method} request to '{request.url}'", extra=request.__dict__)
|
||||
|
||||
def _info_log_request_sending(self, request: httpx.Request, log_payload: Any) -> None:
|
||||
self._info(
|
||||
f"Sending {request.method} request to '{request.url}' with payload={shortify_log_value(log_payload)!r}",
|
||||
extra={"payload": shortify_log_value(log_payload)},
|
||||
)
|
||||
|
||||
def _info_log_response(self, response: httpx.Response) -> None:
|
||||
str_repr_content = response.content.decode("utf8")[:500]
|
||||
self._info(f"Response {response.status_code=} {str_repr_content=}", extra={"status_code": response.status_code, "content": str_repr_content})
|
||||
|
||||
def _info(self, msg: str, *args: Any, **kwargs: Any) -> None:
|
||||
self._log("INFO", f"[{self._nonce}] {msg}", *args, **kwargs)
|
||||
|
||||
def _debug(self, msg: str, *args: Any, **kwargs: Any) -> None:
|
||||
self._log("DEBUG", f"[{self._nonce}] {msg}", *args, **kwargs)
|
||||
|
||||
def _warn(self, msg: str, *args: Any, **kwargs: Any) -> None:
|
||||
self._log("WARNING", f"[{self._nonce}] {msg}", *args, **kwargs)
|
||||
|
||||
def _error(self, msg: str, *args: Any, **kwargs: Any) -> None:
|
||||
self._log("ERROR", f"[{self._nonce}] {msg}", *args, **kwargs)
|
||||
|
||||
def _critical(self, msg: str, *args: Any, **kwargs: Any) -> None:
|
||||
self._log("CRITICAL", f"[{self._nonce}] {msg}", *args, **kwargs)
|
||||
|
||||
def _log(self, level: str, msg: object, *args: object, stacklevel: int = 4, extra: dict | None = None, **kwargs: Any) -> None:
|
||||
extra = extra or {}
|
||||
extra.update(nonce=self._nonce)
|
||||
self._logger.log(logging.getLevelNamesMapping()[level], msg, *args, stacklevel=stacklevel, extra=extra, **kwargs)
|
46
src/api_client/async_client.py
Normal file
46
src/api_client/async_client.py
Normal file
@ -0,0 +1,46 @@
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
|
||||
from ._base import BaseClient
|
||||
|
||||
|
||||
class BaseAPIClient(BaseClient):
|
||||
_session: httpx.AsyncClient
|
||||
|
||||
def __init__(self, nonce: str = "", session: httpx.AsyncClient | None = None):
|
||||
super().__init__(nonce, session or httpx.AsyncClient(http2=True))
|
||||
|
||||
async def get(self, endpoint: str, *, params: Any = None, **kwargs: Any) -> httpx.Response:
|
||||
return await self._request("GET", endpoint, params=params, **kwargs)
|
||||
|
||||
async def post(self, endpoint: str, *, json: Any = None, data: Any = None, **kwargs: Any) -> httpx.Response:
|
||||
return await self._request("POST", endpoint, json=json, data=data, **kwargs)
|
||||
|
||||
async def put(self, endpoint: str, *, json: Any = None, data: Any = None, **kwargs: Any) -> httpx.Response:
|
||||
return await self._request("PUT", endpoint, json=json, data=data, **kwargs)
|
||||
|
||||
async def patch(self, endpoint: str, *, json: Any = None, data: Any = None, **kwargs: Any) -> httpx.Response:
|
||||
return await self._request("GET", endpoint, json=json, data=data, **kwargs)
|
||||
|
||||
async def delete(self, endpoint: str, *, params: Any = None, **kwargs: Any) -> httpx.Response:
|
||||
return await self._request("GET", endpoint, params=params, **kwargs)
|
||||
|
||||
async def head(self, endpoint: str, *, params: Any = None, **kwargs: Any) -> httpx.Response:
|
||||
return await self._request("GET", endpoint, params=params, **kwargs)
|
||||
|
||||
async def options(self, endpoint: str, *, params: Any = None, **kwargs: Any) -> httpx.Response:
|
||||
return await self._request("GET", endpoint, params=params, **kwargs)
|
||||
|
||||
async def trace(self, endpoint: str, *, params: Any = None, **kwargs: Any) -> httpx.Response:
|
||||
return await self._request("GET", endpoint, params=params, **kwargs)
|
||||
|
||||
async def connect(self, endpoint: str, *, params: Any = None, **kwargs: Any) -> httpx.Response:
|
||||
return await self._request("GET", endpoint, params=params, **kwargs)
|
||||
|
||||
async def _request(self, method: str, endpoint: str, **kwargs: Any) -> httpx.Response:
|
||||
request = self._prepare_request(method, endpoint, **kwargs)
|
||||
response = await self._session.send(request, auth=self.prepare_authentication)
|
||||
self._info_log_response(response)
|
||||
|
||||
return response
|
@ -1,107 +0,0 @@
|
||||
import logging
|
||||
from abc import ABC
|
||||
from typing import Any
|
||||
|
||||
import requests
|
||||
from pydantic import HttpUrl
|
||||
|
||||
from .utils import get_logger
|
||||
from .utils.logs import get_nonce, shortify_log_value
|
||||
|
||||
|
||||
class BaseAPIClient(ABC):
|
||||
base_url: HttpUrl
|
||||
_session: requests.Session
|
||||
_nonce: str
|
||||
_logger: logging.Logger
|
||||
|
||||
def __init__(self, nonce: str = ""):
|
||||
self._nonce = nonce or get_nonce()
|
||||
self._session = requests.Session()
|
||||
self._logger = get_logger("APIClient")
|
||||
|
||||
def get(self, endpoint: str, *, params: Any = None, **kwargs: Any) -> requests.Response:
|
||||
return self._request("GET", endpoint, params=params, **kwargs)
|
||||
|
||||
def post(self, endpoint: str, *, json: Any = None, data: Any = None, **kwargs: Any) -> requests.Response:
|
||||
return self._request("POST", endpoint, json=json, data=data, **kwargs)
|
||||
|
||||
def put(self, endpoint: str, *, json: Any = None, data: Any = None, **kwargs: Any) -> requests.Response:
|
||||
return self._request("PUT", endpoint, json=json, data=data, **kwargs)
|
||||
|
||||
def patch(self, endpoint: str, *, json: Any = None, data: Any = None, **kwargs: Any) -> requests.Response:
|
||||
return self._request("PATCH", endpoint, json=json, data=data, **kwargs)
|
||||
|
||||
def delete(self, endpoint: str, *, params: Any = None, **kwargs: Any) -> requests.Response:
|
||||
return self._request("DELETE", endpoint, params=params, **kwargs)
|
||||
|
||||
def head(self, endpoint: str, *, params: Any = None, **kwargs: Any) -> requests.Response:
|
||||
return self._request("HEAD", endpoint, params=params, **kwargs)
|
||||
|
||||
def options(self, endpoint: str, *, params: Any = None, **kwargs: Any) -> requests.Response:
|
||||
return self._request("OPTIONS", endpoint, params=params, **kwargs)
|
||||
|
||||
def trace(self, endpoint: str, *, params: Any = None, **kwargs: Any) -> requests.Response:
|
||||
return self._request("TRACE", endpoint, params=params, **kwargs)
|
||||
|
||||
def connect(self, endpoint: str, *, params: Any = None, **kwargs: Any) -> requests.Response:
|
||||
return self._request("CONNECT", endpoint, params=params, **kwargs)
|
||||
|
||||
def _make_full_url(self, endpoint: str) -> str:
|
||||
return f"{self.base_url}{endpoint}"
|
||||
|
||||
def _request(self, method: str, endpoint: str, **kwargs: Any) -> requests.Response:
|
||||
full_url = self._make_full_url(endpoint)
|
||||
|
||||
# Create the Request.
|
||||
req = requests.Request(
|
||||
method=method.upper(),
|
||||
url=full_url,
|
||||
headers=kwargs.get("headers"),
|
||||
files=kwargs.get("files"),
|
||||
data=kwargs.get("data"),
|
||||
json=kwargs.get("json"),
|
||||
params=kwargs.get("params"),
|
||||
auth=kwargs.get("auth"),
|
||||
cookies=kwargs.get("cookies"),
|
||||
hooks=kwargs.get("hooks"),
|
||||
)
|
||||
self.prepare_authentication(req)
|
||||
self._debug(f'Preparing {req.method} request to "{req.url}"', extra=req.__dict__)
|
||||
prepared_request: requests.PreparedRequest = self._session.prepare_request(req)
|
||||
|
||||
self._info(
|
||||
f"Sending request with payload={prepared_request.body!r}",
|
||||
extra={"payload": shortify_log_value(kwargs.get("json", kwargs.get("data", {})))},
|
||||
)
|
||||
response = self._session.send(prepared_request)
|
||||
|
||||
str_repr_content = response.content.decode("utf8")[:500]
|
||||
self._info(f"Response {response.status_code=} {str_repr_content=}", extra={"status_code": response.status_code, "content": str_repr_content})
|
||||
|
||||
response.raise_for_status()
|
||||
return response
|
||||
|
||||
def prepare_authentication(self, request: requests.Request) -> None:
|
||||
"""Do auth setup in-place"""
|
||||
pass
|
||||
|
||||
def _info(self, msg: str, *args: Any, **kwargs: Any) -> None:
|
||||
self._log("INFO", f"[{self._nonce}] {msg}", *args, **kwargs)
|
||||
|
||||
def _debug(self, msg: str, *args: Any, **kwargs: Any) -> None:
|
||||
self._log("DEBUG", f"[{self._nonce}] {msg}", *args, **kwargs)
|
||||
|
||||
def _warn(self, msg: str, *args: Any, **kwargs: Any) -> None:
|
||||
self._log("WARNING", f"[{self._nonce}] {msg}", *args, **kwargs)
|
||||
|
||||
def _error(self, msg: str, *args: Any, **kwargs: Any) -> None:
|
||||
self._log("ERROR", f"[{self._nonce}] {msg}", *args, **kwargs)
|
||||
|
||||
def _critical(self, msg: str, *args: Any, **kwargs: Any) -> None:
|
||||
self._log("CRITICAL", f"[{self._nonce}] {msg}", *args, **kwargs)
|
||||
|
||||
def _log(self, level: str, msg: object, *args: object, stacklevel: int = 3, extra: dict | None = None, **kwargs: Any) -> None:
|
||||
extra = extra or {}
|
||||
extra.update(nonce=self._nonce)
|
||||
self._logger.log(logging.getLevelNamesMapping()[level], msg, *args, stacklevel=stacklevel, extra=extra, **kwargs)
|
47
src/api_client/sync_client.py
Normal file
47
src/api_client/sync_client.py
Normal file
@ -0,0 +1,47 @@
|
||||
from abc import ABC
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
|
||||
from ._base import BaseClient
|
||||
|
||||
|
||||
class BaseAPIClient(BaseClient, ABC):
|
||||
_session: httpx.Client
|
||||
|
||||
def __init__(self, nonce: str = "", session: httpx.Client | None = None):
|
||||
super().__init__(nonce, session or httpx.Client(http2=True))
|
||||
|
||||
def get(self, endpoint: str, *, params: Any = None, **kwargs: Any) -> httpx.Response:
|
||||
return self._request("GET", endpoint, params=params, **kwargs)
|
||||
|
||||
def post(self, endpoint: str, *, json: Any = None, data: Any = None, **kwargs: Any) -> httpx.Response:
|
||||
return self._request("POST", endpoint, json=json, data=data, **kwargs)
|
||||
|
||||
def put(self, endpoint: str, *, json: Any = None, data: Any = None, **kwargs: Any) -> httpx.Response:
|
||||
return self._request("PUT", endpoint, json=json, data=data, **kwargs)
|
||||
|
||||
def patch(self, endpoint: str, *, json: Any = None, data: Any = None, **kwargs: Any) -> httpx.Response:
|
||||
return self._request("PATCH", endpoint, json=json, data=data, **kwargs)
|
||||
|
||||
def delete(self, endpoint: str, *, params: Any = None, **kwargs: Any) -> httpx.Response:
|
||||
return self._request("DELETE", endpoint, params=params, **kwargs)
|
||||
|
||||
def head(self, endpoint: str, *, params: Any = None, **kwargs: Any) -> httpx.Response:
|
||||
return self._request("HEAD", endpoint, params=params, **kwargs)
|
||||
|
||||
def options(self, endpoint: str, *, params: Any = None, **kwargs: Any) -> httpx.Response:
|
||||
return self._request("OPTIONS", endpoint, params=params, **kwargs)
|
||||
|
||||
def trace(self, endpoint: str, *, params: Any = None, **kwargs: Any) -> httpx.Response:
|
||||
return self._request("TRACE", endpoint, params=params, **kwargs)
|
||||
|
||||
def connect(self, endpoint: str, *, params: Any = None, **kwargs: Any) -> httpx.Response:
|
||||
return self._request("CONNECT", endpoint, params=params, **kwargs)
|
||||
|
||||
def _request(self, method: str, endpoint: str, **kwargs: Any) -> httpx.Response:
|
||||
request = self._prepare_request(method, endpoint, **kwargs)
|
||||
response = self._session.send(request, auth=self.prepare_authentication)
|
||||
self._info_log_response(response)
|
||||
|
||||
return response
|
Reference in New Issue
Block a user