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:
@ -3,7 +3,7 @@
|
|||||||
root = true
|
root = true
|
||||||
|
|
||||||
[*]
|
[*]
|
||||||
max_line_length = 160
|
max_line_length = 120
|
||||||
indent_style = space
|
indent_style = space
|
||||||
indent_size = 4
|
indent_size = 4
|
||||||
charset = utf-8
|
charset = utf-8
|
||||||
|
4
.flake8
4
.flake8
@ -1,4 +1,4 @@
|
|||||||
[flake8]
|
[flake8]
|
||||||
extend-ignore = E203, E704, W605, E701
|
extend-ignore = E203, E704, W605, E701
|
||||||
exclude = .git,__pycache__,venv,migrations
|
exclude = .git,__pycache__,venv
|
||||||
max-line-length = 160
|
max-line-length = 120
|
||||||
|
@ -25,7 +25,7 @@ classifiers = [
|
|||||||
"Operating System :: OS Independent",
|
"Operating System :: OS Independent",
|
||||||
]
|
]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"requests~=2.31",
|
"httpx[http2]~=0.27.0",
|
||||||
"pydantic~=2.6",
|
"pydantic~=2.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -42,7 +42,7 @@ version = { attr = "api_client.__version__" }
|
|||||||
|
|
||||||
|
|
||||||
[tool.bumpversion]
|
[tool.bumpversion]
|
||||||
current_version = "1.0.1"
|
current_version = "1.1.0"
|
||||||
commit = true
|
commit = true
|
||||||
tag = true
|
tag = true
|
||||||
tag_name = "v{new_version}"
|
tag_name = "v{new_version}"
|
||||||
@ -58,7 +58,7 @@ filename = "src/api_client/__init__.py"
|
|||||||
|
|
||||||
|
|
||||||
[tool.black]
|
[tool.black]
|
||||||
line-length = 160
|
line-length = 120
|
||||||
target-version = ['py311']
|
target-version = ['py311']
|
||||||
include = '\.pyi?$'
|
include = '\.pyi?$'
|
||||||
extend-exclude = '''(
|
extend-exclude = '''(
|
||||||
@ -69,7 +69,7 @@ workers = 4
|
|||||||
|
|
||||||
[tool.isort]
|
[tool.isort]
|
||||||
profile = "black"
|
profile = "black"
|
||||||
line_length = 160
|
line_length = 120
|
||||||
skip = ["migrations", "env", "venv", ".venv", ".git", "media"]
|
skip = ["migrations", "env", "venv", ".venv", ".git", "media"]
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
## Requirements for development of library
|
## Requirements for development of library
|
||||||
# Base requirements
|
# Base requirements
|
||||||
requests==2.31.0
|
httpx[http2]==0.27.0
|
||||||
pydantic==2.6.3
|
pydantic==2.6.3
|
||||||
|
|
||||||
# Code style
|
# Code style
|
||||||
@ -10,7 +10,6 @@ isort==5.13.2
|
|||||||
|
|
||||||
# Type check
|
# Type check
|
||||||
mypy==1.9
|
mypy==1.9
|
||||||
types-requests==2.31.0.20240218
|
|
||||||
|
|
||||||
# Version and package support
|
# Version and package support
|
||||||
twine==5.0
|
twine==5.0
|
||||||
|
@ -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"
|
__version__ = "1.1.0"
|
||||||
__all__ = ["BaseAPIClient"]
|
__all__ = ["SyncClient", "AsyncClient"]
|
||||||
|
125
src/api_client/_base.py
Normal file
125
src/api_client/_base.py
Normal 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
|
||||||
|
)
|
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