v1.1.0 feature/async-httpx (#2)

Reviewed-on: #2
Co-authored-by: Ēriks Karls <git@72.lv>
Co-committed-by: Ēriks Karls <git@72.lv>
This commit is contained in:
2024-03-10 12:22:05 +02:00
committed by eriks
parent 9ba78973b0
commit 7b831e9cd9
9 changed files with 230 additions and 119 deletions

View File

@ -3,7 +3,7 @@
root = true
[*]
max_line_length = 160
max_line_length = 120
indent_style = space
indent_size = 4
charset = utf-8

View File

@ -1,4 +1,4 @@
[flake8]
extend-ignore = E203, E704, W605, E701
exclude = .git,__pycache__,venv,migrations
max-line-length = 160
exclude = .git,__pycache__,venv
max-line-length = 120

View File

@ -25,7 +25,7 @@ classifiers = [
"Operating System :: OS Independent",
]
dependencies = [
"requests~=2.31",
"httpx[http2]~=0.27.0",
"pydantic~=2.6",
]
@ -42,7 +42,7 @@ version = { attr = "api_client.__version__" }
[tool.bumpversion]
current_version = "1.0.1"
current_version = "1.1.0"
commit = true
tag = true
tag_name = "v{new_version}"
@ -58,7 +58,7 @@ filename = "src/api_client/__init__.py"
[tool.black]
line-length = 160
line-length = 120
target-version = ['py311']
include = '\.pyi?$'
extend-exclude = '''(
@ -69,7 +69,7 @@ workers = 4
[tool.isort]
profile = "black"
line_length = 160
line_length = 120
skip = ["migrations", "env", "venv", ".venv", ".git", "media"]

View File

@ -1,6 +1,6 @@
## Requirements for development of library
# Base requirements
requests==2.31.0
httpx[http2]==0.27.0
pydantic==2.6.3
# Code style
@ -10,7 +10,6 @@ isort==5.13.2
# Type check
mypy==1.9
types-requests==2.31.0.20240218
# Version and package support
twine==5.0

View File

@ -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"]
__version__ = "1.1.0"
__all__ = ["SyncClient", "AsyncClient"]

125
src/api_client/_base.py Normal file
View File

@ -0,0 +1,125 @@
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
)

View 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

View File

@ -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)

View 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