WIP
This commit is contained in:
@ -75,3 +75,15 @@ workers = 4
|
||||
profile = "black"
|
||||
line_length = 160
|
||||
skip = ["migrations", "env", "venv", ".venv", ".git", "media"]
|
||||
|
||||
|
||||
[tool.mypy]
|
||||
python_version = "3.11"
|
||||
exclude = ['^\.?venv/',]
|
||||
plugins = ["pydantic.mypy"]
|
||||
warn_unused_configs = true
|
||||
disallow_untyped_defs = true
|
||||
implicit_optional = true
|
||||
warn_redundant_casts = true
|
||||
warn_no_return = false
|
||||
ignore_missing_imports = false
|
||||
|
@ -7,12 +7,14 @@ pydantic~=2.6.3
|
||||
black~=24.2
|
||||
flake8~=7.0
|
||||
isort~=5.13
|
||||
mypy~=1.9
|
||||
|
||||
# Version and package support
|
||||
twine~=5.0
|
||||
bump-my-version~=0.18
|
||||
build~=1.1
|
||||
pur~=7.3
|
||||
setuptools~=69.1
|
||||
|
||||
# Testing and debugging
|
||||
pytest~=8.0
|
||||
|
@ -1,5 +1,5 @@
|
||||
import logging
|
||||
from abc import ABC, abstractmethod
|
||||
from abc import ABC
|
||||
from typing import Any
|
||||
|
||||
import requests
|
||||
@ -38,27 +38,28 @@ class BaseAPIClient(ABC):
|
||||
def _request(self, method: str, endpoint: str, **kwargs: Any) -> requests.Response:
|
||||
full_url = self._make_full_url(endpoint)
|
||||
|
||||
self._debug(f'Preparing request {method} "{full_url}"', extra={"method": method, "full_url": full_url, "kwargs": kwargs})
|
||||
# Create the Request.
|
||||
req = requests.Request(
|
||||
method=method.upper(),
|
||||
url=full_url,
|
||||
headers=kwargs.get("headers"),
|
||||
files=kwargs.get("headers"),
|
||||
data=kwargs.get("headers") or {},
|
||||
json=kwargs.get("headers"),
|
||||
params=kwargs.get("headers") or {},
|
||||
auth=kwargs.get("headers"),
|
||||
cookies=kwargs.get("headers"),
|
||||
hooks=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"),
|
||||
)
|
||||
request = self._session.prepare_request(method=method, url=full_url, **kwargs)
|
||||
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={shortify_log_value(kwargs.get('json', kwargs.get('data', {})))}",
|
||||
f"Sending request with payload={prepared_request.body}",
|
||||
extra={"payload": shortify_log_value(kwargs.get("json", kwargs.get("data", {})))},
|
||||
)
|
||||
response = self._session.send(request, auth=self.prepare_authentication)
|
||||
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})
|
||||
@ -66,9 +67,9 @@ class BaseAPIClient(ABC):
|
||||
response.raise_for_status()
|
||||
return response
|
||||
|
||||
@abstractmethod
|
||||
def prepare_authentication(self, request: requests.Request) -> requests.Request:
|
||||
raise NotImplementedError()
|
||||
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._logger.info(f"[{self._nonce}] {msg}", *args, **kwargs)
|
||||
|
@ -1,38 +0,0 @@
|
||||
import httpx
|
||||
from httpx import Timeout
|
||||
|
||||
from api_client.utils.logging import get_logger
|
||||
|
||||
LOGGER = get_logger("httpx")
|
||||
|
||||
|
||||
class HTTPXClientWrapper:
|
||||
"""httpx client wrapper - logic copied from https://stackoverflow.com/a/74397436"""
|
||||
|
||||
async_client: httpx.AsyncClient | None = None
|
||||
|
||||
def __init__(self, UA_string: str = httpx._client.USER_AGENT):
|
||||
self.user_agent = UA_string
|
||||
|
||||
def start(self) -> None:
|
||||
"""Instantiate the client. Call from the FastAPI startup hook."""
|
||||
self.async_client = httpx.AsyncClient(headers={"user-agent": self.user_agent}, timeout=Timeout(120))
|
||||
LOGGER.debug(f"httpx AsyncClient instantiated. Id {id(self.async_client)}")
|
||||
|
||||
async def stop(self) -> None:
|
||||
"""Gracefully shutdown. Call from FastAPI shutdown hook."""
|
||||
if self.async_client is None:
|
||||
raise AssertionError("async_client is None!")
|
||||
LOGGER.debug(f"httpx async_client.is_closed(): {self.async_client.is_closed} - Now close it. Id (will be unchanged): {id(self.async_client)}")
|
||||
await self.async_client.aclose()
|
||||
LOGGER.debug(f"httpx async_client.is_closed(): {self.async_client.is_closed}. Id (will be unchanged): {id(self.async_client)}")
|
||||
self.async_client = None
|
||||
LOGGER.debug("httpx AsyncClient closed")
|
||||
|
||||
def __call__(self) -> httpx.AsyncClient:
|
||||
"""Calling the instantiated HTTPXClientWrapper returns the wrapped singleton."""
|
||||
# Ensure we don't use it if not started / running
|
||||
if self.async_client is None:
|
||||
raise AssertionError("async_client is None!")
|
||||
LOGGER.debug(f"httpx async_client.is_closed(): {self.async_client.is_closed}. Id (will be unchanged): {id(self.async_client)}")
|
||||
return self.async_client
|
@ -5,7 +5,7 @@ from dataclasses import asdict
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
from api_client.utils.parse import ServiceJSONEncoder
|
||||
from .parse import APIClientJSONEncoder
|
||||
|
||||
|
||||
def get_logger(name: str = "service") -> logging.Logger:
|
||||
@ -28,7 +28,7 @@ def shortify_log_value(dct: Any) -> str:
|
||||
dct = asdict(dct)
|
||||
if not isinstance(dct, dict):
|
||||
return str(dct)
|
||||
return json.dumps(_shorten_dict_keys(dct), cls=ServiceJSONEncoder)
|
||||
return json.dumps(_shorten_dict_keys(dct), cls=APIClientJSONEncoder)
|
||||
|
||||
|
||||
def get_nonce() -> str:
|
||||
|
@ -7,103 +7,11 @@ from typing import Any, NoReturn
|
||||
from pydantic import BaseModel
|
||||
|
||||
__all__ = [
|
||||
"str_as_date",
|
||||
"str_as_date_or_none",
|
||||
"as_decimal",
|
||||
"as_decimal_or_none",
|
||||
"extract_dates_from_str",
|
||||
"extract_date_from_str",
|
||||
"ServiceJSONEncoder",
|
||||
"APIClientJSONEncoder",
|
||||
]
|
||||
|
||||
|
||||
def str_as_date(date_str: str, date_format: str = "%Y-%m-%d") -> datetime.date | NoReturn:
|
||||
try:
|
||||
datetime_object = datetime.datetime.strptime(date_str, date_format)
|
||||
return datetime_object.date()
|
||||
except (ValueError, TypeError):
|
||||
raise TypeError(f"Unable to cast {date_str=} to datetime.date!")
|
||||
|
||||
|
||||
def str_as_date_or_none(date_str: str | None, date_format: str = "%Y-%m-%d") -> datetime.date | None:
|
||||
if not isinstance(date_str, str):
|
||||
return None
|
||||
try:
|
||||
return str_as_date(date_str, date_format)
|
||||
except TypeError:
|
||||
return None
|
||||
|
||||
|
||||
def as_decimal(decimal_str: str | int | float) -> decimal.Decimal | NoReturn:
|
||||
try:
|
||||
return decimal.Decimal(str(decimal_str))
|
||||
except (decimal.DecimalException, TypeError):
|
||||
raise TypeError(f"Unable to cast {decimal_str=} to Decimal value!")
|
||||
|
||||
|
||||
def as_decimal_or_none(decimal_str: str | int | float | None) -> decimal.Decimal | None:
|
||||
if not isinstance(decimal_str, (str | int | float)):
|
||||
return None
|
||||
try:
|
||||
return as_decimal(decimal_str)
|
||||
except TypeError:
|
||||
return None
|
||||
|
||||
|
||||
def _date_from_re_search(pattern: re.Pattern, test_str: str) -> datetime.date | tuple[datetime.date, datetime.date] | None:
|
||||
if match := pattern.search(test_str):
|
||||
if "year" in match.re.groupindex:
|
||||
try:
|
||||
return datetime.date(int(match.group("year")), int(match.group("month")), int(match.group("day")))
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
elif "year1" in match.re.groupindex:
|
||||
try:
|
||||
return (
|
||||
datetime.date(int(match.group("year1")), int(match.group("month1")), int(match.group("day1"))),
|
||||
datetime.date(int(match.group("year2")), int(match.group("month2")), int(match.group("day2"))),
|
||||
)
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def extract_date_from_str(input_str: str) -> datetime.date | None:
|
||||
# date_time_re = r"(\d{0,4})(\-)(\d{0,2})(\-)(\d{0,2}) (\d{0,2})(:)(\d{0,2})(:)(\d{0,2})"
|
||||
# date_time_re = r"(\d{0,2})(\.)(\d{0,2})(\.)(\d{0,4}) (\d{0,2})(:)(\d{0,2})(:)(\d{0,2})"
|
||||
# date_time_re = r"(\d{0,2})(\.)(\d{0,2})(\.)(\d{0,4}) / (\d{0,2})(:)(\d{0,2})"
|
||||
# date_time_re = r"(\d{0,2})(\.)(\d{0,2})(\.)(\d{0,4})"
|
||||
|
||||
for pattern in (
|
||||
re.compile(r"(?P<day>\d{2})\.(?P<month>\d{2})\.(?P<year>\d{4}) / (?P<time>\d{2}:\d{2}:\d{2})"),
|
||||
re.compile(r"(?P<day>\d{2})\.(?P<month>\d{2})\.(?P<year>\d{4}) (?P<time>\d{2}:\d{2}:\d{2})"),
|
||||
re.compile(r"(?P<day>\d{2})\.(?P<month>\d{2})\.(?P<year>\d{4})"),
|
||||
re.compile(r"(?P<year>\d{4})(.|-)(?P<month>\d{1,2})(.|-)(?P<day>\d{1,2})"),
|
||||
):
|
||||
if match := _date_from_re_search(pattern, input_str):
|
||||
assert isinstance(match, datetime.date) # sanity check
|
||||
return match
|
||||
return None
|
||||
|
||||
|
||||
def extract_dates_from_str(input_str: str) -> tuple[datetime.date, datetime.date] | None:
|
||||
# period_re = re.search(r" (\d{0,2}\.\d{0,2}\.\d{0,4}) - (\d{0,2})\.(\d{0,2})\.(\d{0,4})")
|
||||
# period_re = re.search(r".* (\d{0,2}\.\d{0,2}\.\d{0,4}) \W* (\d{0,2}\.\d{0,2}\.\d{0,4})", text)
|
||||
# period_re = re.search(r".* (\d{0,2}\.\d{0,2}\.\d{0,4}) .* (\d{0,2}\.\d{0,2}\.\d{0,4})", line)
|
||||
# r".* (\d{0,2}\.\d{0,2}\.\d{0,4}) .* (\d{0,2}\.\d{0,2}\.\d{0,4})"
|
||||
# period_re = re.search(r"(\d{4}-\d{2}-\d{2}) - (\d{4}-\d{2}-\d{2})", line)
|
||||
for pattern in (
|
||||
re.compile(r"(?P<day1>\d{1,2})\.(?P<month1>\d{1,2})\.(?P<year1>\d{4}) - (?P<day2>\d{1,2})\.(?P<month2>\d{1,2})\.(?P<year2>\d{4})"),
|
||||
re.compile(r"(?P<day1>\d{1,2})\.(?P<month1>\d{1,2})\.(?P<year1>\d{4})[\s\w\W]+(?P<day2>\d{1,2})\.(?P<month2>\d{1,2})\.(?P<year2>\d{4})"),
|
||||
re.compile(r"(?P<year1>\d{4})(.|-)(?P<month1>\d{1,2})(.|-)(?P<day1>\d{1,2}) - (?P<year2>\d{4})(.|-)(?P<month2>\d{1,2})(.|-)(?P<day2>\d{1,2})"),
|
||||
):
|
||||
if match := _date_from_re_search(pattern, input_str):
|
||||
assert not isinstance(match, datetime.date) # sanity check
|
||||
return match
|
||||
return None
|
||||
|
||||
|
||||
class ServiceJSONEncoder(json.JSONEncoder):
|
||||
class APIClientJSONEncoder(json.JSONEncoder):
|
||||
def default(self, obj: Any) -> Any:
|
||||
try:
|
||||
if isinstance(obj, decimal.Decimal):
|
||||
|
Reference in New Issue
Block a user