refactor: payments providers
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import Any, Callable, Dict, Mapping, Optional, Tuple
|
||||
|
||||
from aiohttp import ClientSession, ClientTimeout
|
||||
|
||||
SuccessCheck = Callable[[int, Any], bool]
|
||||
|
||||
|
||||
def http_ok(status: int, _body: Any) -> bool:
|
||||
"""Default success criterion — HTTP 200 with any body."""
|
||||
return status == 200
|
||||
|
||||
|
||||
async def post_json_request(
|
||||
session: ClientSession,
|
||||
url: str,
|
||||
*,
|
||||
body: Any,
|
||||
headers: Optional[Mapping[str, str]] = None,
|
||||
log_prefix: str,
|
||||
is_success: SuccessCheck = http_ok,
|
||||
) -> Tuple[bool, Dict[str, Any]]:
|
||||
"""Centralized JSON-POST every HTTP-API provider used to inline ~25 lines for.
|
||||
|
||||
On transport failure, JSON decode failure, or rejected ``is_success`` check,
|
||||
returns ``(False, {"status": ..., "message": ..., "raw": ...?})`` so callers
|
||||
can decide what to do (typically: mark the payment as ``failed_creation``).
|
||||
"""
|
||||
try:
|
||||
async with session.post(
|
||||
url,
|
||||
json=body,
|
||||
headers=dict(headers) if headers else None,
|
||||
) as response:
|
||||
response_text = await response.text()
|
||||
try:
|
||||
response_data = json.loads(response_text) if response_text else {}
|
||||
except json.JSONDecodeError:
|
||||
logging.error("%s: invalid JSON response: %s", log_prefix, response_text)
|
||||
return False, {
|
||||
"status": response.status,
|
||||
"message": "invalid_json",
|
||||
"raw": response_text,
|
||||
}
|
||||
if not is_success(response.status, response_data):
|
||||
logging.error(
|
||||
"%s: API returned error (status=%s, body=%s)",
|
||||
log_prefix,
|
||||
response.status,
|
||||
response_data,
|
||||
)
|
||||
return False, {"status": response.status, "message": response_data}
|
||||
return True, response_data
|
||||
except Exception as exc:
|
||||
logging.exception("%s: request failed.", log_prefix)
|
||||
return False, {"message": str(exc)}
|
||||
|
||||
|
||||
def first_value(data: Optional[Mapping[str, Any]], *keys: str) -> Optional[str]:
|
||||
"""Return the first non-empty value among ``keys`` (cast to ``str``)."""
|
||||
if not data:
|
||||
return None
|
||||
for key in keys:
|
||||
value = data.get(key)
|
||||
if value:
|
||||
return str(value)
|
||||
return None
|
||||
|
||||
|
||||
class HttpClientMixin:
|
||||
"""Shared lazy ``aiohttp.ClientSession`` lifecycle for provider services.
|
||||
|
||||
Each subclass calls ``self._init_http_client(total_timeout=...)`` from
|
||||
``__init__`` and inherits ``_get_session`` / ``close``. The session is
|
||||
created on first use and recreated transparently if it was closed.
|
||||
"""
|
||||
|
||||
_timeout: ClientTimeout
|
||||
_session: Optional[ClientSession]
|
||||
|
||||
def _init_http_client(self, *, total_timeout: float = 20.0) -> None:
|
||||
self._timeout = ClientTimeout(total=total_timeout)
|
||||
self._session = None
|
||||
|
||||
async def _get_session(self) -> ClientSession:
|
||||
if self._session is None or self._session.closed:
|
||||
self._session = ClientSession(timeout=self._timeout)
|
||||
return self._session
|
||||
|
||||
async def close(self) -> None:
|
||||
if self._session and not self._session.closed:
|
||||
await self._session.close()
|
||||
Reference in New Issue
Block a user