Добавлен поллер накладной
This commit is contained in:
@@ -19,13 +19,17 @@ from app.adapters.delivery_providers.cdek.mapper import (
|
||||
map_cdek_response_for_tariff_code,
|
||||
)
|
||||
from app.adapters.delivery_providers.cdek.order_mapper import (
|
||||
CDEKOrderInfo,
|
||||
CDEKOrderMappingError,
|
||||
CDEKOrderRegistrationResult,
|
||||
CDEKWaybillInfo,
|
||||
centimeters_string_to_int,
|
||||
kilograms_string_to_grams,
|
||||
map_cdek_existing_order_response,
|
||||
map_cdek_order_info_response,
|
||||
map_cdek_order_request,
|
||||
map_cdek_order_response,
|
||||
map_cdek_waybill_info_response,
|
||||
resolve_cdek_city_code,
|
||||
)
|
||||
from app.cities import cities_map
|
||||
@@ -61,6 +65,7 @@ class CDEKClient:
|
||||
normalized_base_url = base_url.rstrip("/")
|
||||
self._tariff_url = f"{normalized_base_url}/calculator/tarifflist"
|
||||
self._orders_url = f"{normalized_base_url}/orders"
|
||||
self._print_orders_url = f"{normalized_base_url}/print/orders"
|
||||
self._timeout_seconds = timeout_seconds
|
||||
self._retry_attempts = retry_attempts
|
||||
self._retry_backoff_seconds = retry_backoff_seconds
|
||||
@@ -222,6 +227,85 @@ class CDEKClient:
|
||||
|
||||
raise CDEKClientError("CDEK order registration failed unexpectedly.")
|
||||
|
||||
async def get_order(self, cdek_order_uuid: str) -> CDEKOrderInfo:
|
||||
url = f"{self._orders_url}/{cdek_order_uuid}"
|
||||
raw_payload = await self._get_json(
|
||||
url,
|
||||
failure_message="CDEK get order failed",
|
||||
)
|
||||
try:
|
||||
return map_cdek_order_info_response(raw_payload)
|
||||
except CDEKOrderMappingError as exc:
|
||||
raise CDEKClientError(
|
||||
"CDEK get order response payload is invalid."
|
||||
) from exc
|
||||
|
||||
async def get_waybill(self, cdek_waybill_uuid: str) -> CDEKWaybillInfo:
|
||||
url = f"{self._print_orders_url}/{cdek_waybill_uuid}"
|
||||
raw_payload = await self._get_json(
|
||||
url,
|
||||
failure_message="CDEK get waybill failed",
|
||||
)
|
||||
try:
|
||||
return map_cdek_waybill_info_response(raw_payload)
|
||||
except CDEKOrderMappingError as exc:
|
||||
raise CDEKClientError(
|
||||
"CDEK get waybill response payload is invalid."
|
||||
) from exc
|
||||
|
||||
async def _get_json(self, url: str, *, failure_message: str) -> dict[str, Any]:
|
||||
for attempt in range(self._retry_attempts + 1):
|
||||
try:
|
||||
token = await self._auth_client.get_access_token()
|
||||
response = await self._http_client.get(
|
||||
url,
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=self._timeout_seconds,
|
||||
)
|
||||
except (httpx.TimeoutException, httpx.TransportError) as exc:
|
||||
if attempt < self._retry_attempts:
|
||||
await self._sleep(self._retry_delay(attempt))
|
||||
continue
|
||||
raise CDEKClientError(
|
||||
f"{failure_message} after retry attempts."
|
||||
) from exc
|
||||
|
||||
if self._should_retry(response.status_code):
|
||||
if attempt < self._retry_attempts:
|
||||
await self._sleep(self._retry_delay(attempt))
|
||||
continue
|
||||
raise CDEKClientError(
|
||||
f"{failure_message} with retriable status "
|
||||
f"{response.status_code}."
|
||||
)
|
||||
|
||||
if 400 <= response.status_code < 500:
|
||||
log.warning(
|
||||
"cdek_get_request_rejected",
|
||||
url=url,
|
||||
status_code=response.status_code,
|
||||
response_body=_response_text_or_none(response),
|
||||
)
|
||||
raise CDEKRequestError(
|
||||
f"{failure_message} with status {response.status_code}."
|
||||
)
|
||||
|
||||
try:
|
||||
response.raise_for_status()
|
||||
raw_payload = response.json()
|
||||
except (httpx.HTTPError, TypeError, ValueError) as exc:
|
||||
raise CDEKClientError(
|
||||
f"{failure_message}: invalid response payload."
|
||||
) from exc
|
||||
|
||||
if not isinstance(raw_payload, dict):
|
||||
raise CDEKClientError(
|
||||
f"{failure_message}: payload must be a JSON object."
|
||||
)
|
||||
return raw_payload
|
||||
|
||||
raise CDEKClientError(f"{failure_message} unexpectedly.")
|
||||
|
||||
def _retry_delay(self, attempt: int) -> float:
|
||||
return self._retry_backoff_seconds * (attempt + 1)
|
||||
|
||||
@@ -361,6 +445,12 @@ class CDEKProvider(DeliveryProvider):
|
||||
) -> CDEKOrderRegistrationResult:
|
||||
return await self._client.register_order(request)
|
||||
|
||||
async def get_order(self, cdek_order_uuid: str) -> CDEKOrderInfo:
|
||||
return await self._client.get_order(cdek_order_uuid)
|
||||
|
||||
async def get_waybill(self, cdek_waybill_uuid: str) -> CDEKWaybillInfo:
|
||||
return await self._client.get_waybill(cdek_waybill_uuid)
|
||||
|
||||
|
||||
def _response_json_or_none(response: httpx.Response) -> object | None:
|
||||
try:
|
||||
|
||||
@@ -29,6 +29,19 @@ class CDEKOrderRegistrationResult:
|
||||
waybill_url: str | None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CDEKOrderInfo:
|
||||
order_uuid: str
|
||||
status_code: str | None
|
||||
waybill_uuid: str | None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CDEKWaybillInfo:
|
||||
waybill_uuid: str
|
||||
url: str | None
|
||||
|
||||
|
||||
def map_cdek_order_request(request: InitPaymentRequest) -> dict[str, Any]:
|
||||
payload: dict[str, Any] = {
|
||||
"number": request.order_uuid,
|
||||
@@ -89,6 +102,65 @@ def map_cdek_existing_order_response(
|
||||
)
|
||||
|
||||
|
||||
def map_cdek_order_info_response(payload: dict[str, Any]) -> CDEKOrderInfo:
|
||||
entity = payload.get("entity")
|
||||
if not isinstance(entity, dict):
|
||||
raise CDEKOrderMappingError(
|
||||
"CDEK order info response must include entity object."
|
||||
)
|
||||
|
||||
order_uuid = entity.get("uuid")
|
||||
if not isinstance(order_uuid, str) or not order_uuid:
|
||||
raise CDEKOrderMappingError(
|
||||
"CDEK order info response must include entity.uuid."
|
||||
)
|
||||
|
||||
status_code = _latest_status_code(entity.get("statuses"))
|
||||
waybill_uuid, _ = _extract_waybill(payload)
|
||||
return CDEKOrderInfo(
|
||||
order_uuid=order_uuid,
|
||||
status_code=status_code,
|
||||
waybill_uuid=waybill_uuid,
|
||||
)
|
||||
|
||||
|
||||
def map_cdek_waybill_info_response(payload: dict[str, Any]) -> CDEKWaybillInfo:
|
||||
entity = payload.get("entity")
|
||||
if not isinstance(entity, dict):
|
||||
raise CDEKOrderMappingError(
|
||||
"CDEK waybill info response must include entity object."
|
||||
)
|
||||
|
||||
waybill_uuid = entity.get("uuid")
|
||||
if not isinstance(waybill_uuid, str) or not waybill_uuid:
|
||||
raise CDEKOrderMappingError(
|
||||
"CDEK waybill info response must include entity.uuid."
|
||||
)
|
||||
|
||||
url_value = entity.get("url")
|
||||
waybill_url = url_value if isinstance(url_value, str) and url_value else None
|
||||
return CDEKWaybillInfo(waybill_uuid=waybill_uuid, url=waybill_url)
|
||||
|
||||
|
||||
def _latest_status_code(statuses: object) -> str | None:
|
||||
if not isinstance(statuses, list) or not statuses:
|
||||
return None
|
||||
dated: list[tuple[str, str]] = []
|
||||
for entry in statuses:
|
||||
if not isinstance(entry, dict):
|
||||
continue
|
||||
code = entry.get("code")
|
||||
if not isinstance(code, str) or not code:
|
||||
continue
|
||||
date_time = entry.get("date_time")
|
||||
date_time_key = date_time if isinstance(date_time, str) else ""
|
||||
dated.append((date_time_key, code))
|
||||
if not dated:
|
||||
return None
|
||||
dated.sort(key=lambda item: item[0])
|
||||
return dated[-1][1]
|
||||
|
||||
|
||||
def _extract_waybill(payload: dict[str, Any]) -> tuple[str | None, str | None]:
|
||||
related_entities = payload.get("related_entities")
|
||||
if not isinstance(related_entities, list):
|
||||
|
||||
@@ -116,6 +116,11 @@ class ObservabilityConfig(BaseModel):
|
||||
otlp_insecure: bool = True
|
||||
|
||||
|
||||
class WaybillPollerConfig(BaseModel):
|
||||
interval_seconds: float = Field(default=30.0, gt=0)
|
||||
batch_size: int = Field(default=50, gt=0)
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
model_config = SettingsConfigDict(
|
||||
extra="ignore",
|
||||
@@ -132,6 +137,7 @@ class Settings(BaseSettings):
|
||||
default_factory=AddressSuggestionsConfig
|
||||
)
|
||||
observability: ObservabilityConfig
|
||||
waybill_poller: WaybillPollerConfig = Field(default_factory=WaybillPollerConfig)
|
||||
|
||||
@classmethod
|
||||
def settings_customise_sources(
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
"""Pure rules for CDEK order polling lifecycle."""
|
||||
|
||||
TERMINAL_ORDER_STATUSES: frozenset[str] = frozenset(
|
||||
{"INVALID", "DELIVERED", "NOT_DELIVERED", "CANCELLED"}
|
||||
)
|
||||
|
||||
|
||||
def is_terminal_order_status(code: str | None) -> bool:
|
||||
if code is None:
|
||||
return False
|
||||
return code in TERMINAL_ORDER_STATUSES
|
||||
@@ -41,8 +41,13 @@ class Order(Base):
|
||||
payment_status: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||||
tbank_payment_id: Mapped[int | None] = mapped_column(BigInteger, nullable=True)
|
||||
cdek_order_uuid: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
||||
cdek_order_status: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||||
cdek_waybill_uuid: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
||||
cdek_waybill_url: Mapped[str | None] = mapped_column(String(2048), nullable=True)
|
||||
cdek_polled_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True),
|
||||
nullable=True,
|
||||
)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True),
|
||||
server_default=func.now(),
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
"""PostgreSQL order repository."""
|
||||
|
||||
from collections.abc import Sequence
|
||||
from contextlib import AbstractAsyncContextManager
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
||||
|
||||
from app.domain.cdek_polling import TERMINAL_ORDER_STATUSES
|
||||
from app.repositories.order.models import Order
|
||||
|
||||
|
||||
@@ -72,15 +75,72 @@ class OrderRepository:
|
||||
session: AsyncSession,
|
||||
order_uuid: str,
|
||||
cdek_order_uuid: str,
|
||||
cdek_waybill_uuid: str | None = None,
|
||||
cdek_waybill_url: str | None = None,
|
||||
) -> Order | None:
|
||||
order = await self.get_order_by_order_uuid(session, order_uuid)
|
||||
if order is None:
|
||||
return None
|
||||
|
||||
order.cdek_order_uuid = cdek_order_uuid
|
||||
order.cdek_waybill_uuid = cdek_waybill_uuid
|
||||
order.cdek_waybill_url = cdek_waybill_url
|
||||
await session.flush()
|
||||
return order
|
||||
|
||||
async def list_orders_pending_waybill(
|
||||
self,
|
||||
session: AsyncSession,
|
||||
*,
|
||||
limit: int,
|
||||
) -> Sequence[Order]:
|
||||
statement = (
|
||||
select(Order)
|
||||
.where(
|
||||
Order.cdek_order_uuid.is_not(None),
|
||||
Order.cdek_waybill_url.is_(None),
|
||||
(
|
||||
Order.cdek_order_status.is_(None)
|
||||
| Order.cdek_order_status.not_in(TERMINAL_ORDER_STATUSES)
|
||||
),
|
||||
)
|
||||
.order_by(Order.cdek_polled_at.asc().nulls_first())
|
||||
.limit(limit)
|
||||
.with_for_update(skip_locked=True)
|
||||
)
|
||||
result = await session.execute(statement)
|
||||
return result.scalars().all()
|
||||
|
||||
async def record_order_poll(
|
||||
self,
|
||||
session: AsyncSession,
|
||||
*,
|
||||
order_uuid: str,
|
||||
order_status: str | None,
|
||||
waybill_uuid: str | None,
|
||||
polled_at: datetime,
|
||||
) -> Order | None:
|
||||
order = await self.get_order_by_order_uuid(session, order_uuid)
|
||||
if order is None:
|
||||
return None
|
||||
|
||||
order.cdek_order_status = order_status
|
||||
if waybill_uuid is not None and order.cdek_waybill_uuid is None:
|
||||
order.cdek_waybill_uuid = waybill_uuid
|
||||
order.cdek_polled_at = polled_at
|
||||
await session.flush()
|
||||
return order
|
||||
|
||||
async def record_waybill_poll(
|
||||
self,
|
||||
session: AsyncSession,
|
||||
*,
|
||||
order_uuid: str,
|
||||
waybill_url: str | None,
|
||||
polled_at: datetime,
|
||||
) -> Order | None:
|
||||
order = await self.get_order_by_order_uuid(session, order_uuid)
|
||||
if order is None:
|
||||
return None
|
||||
|
||||
if waybill_url is not None and order.cdek_waybill_url is None:
|
||||
order.cdek_waybill_url = waybill_url
|
||||
order.cdek_polled_at = polled_at
|
||||
await session.flush()
|
||||
return order
|
||||
|
||||
@@ -141,8 +141,6 @@ class OrderRepositoryProtocol(Protocol):
|
||||
session: object,
|
||||
order_uuid: str,
|
||||
cdek_order_uuid: str,
|
||||
cdek_waybill_uuid: str | None = None,
|
||||
cdek_waybill_url: str | None = None,
|
||||
) -> object | None: ...
|
||||
|
||||
|
||||
@@ -411,8 +409,6 @@ class AggregatorService:
|
||||
await self._save_cdek_order_uuid(
|
||||
order_uuid=notification.OrderId,
|
||||
cdek_order_uuid=registration_result.order_uuid,
|
||||
cdek_waybill_uuid=registration_result.waybill_uuid,
|
||||
cdek_waybill_url=registration_result.waybill_url,
|
||||
)
|
||||
return "OK"
|
||||
|
||||
@@ -492,8 +488,6 @@ class AggregatorService:
|
||||
*,
|
||||
order_uuid: str,
|
||||
cdek_order_uuid: str,
|
||||
cdek_waybill_uuid: str | None,
|
||||
cdek_waybill_url: str | None,
|
||||
) -> None:
|
||||
if self._order_repository is None:
|
||||
raise TBankPaymentNotificationProcessingError(
|
||||
@@ -506,8 +500,6 @@ class AggregatorService:
|
||||
session,
|
||||
order_uuid,
|
||||
cdek_order_uuid,
|
||||
cdek_waybill_uuid,
|
||||
cdek_waybill_url,
|
||||
)
|
||||
if order is None:
|
||||
logger.warning(
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
"""Background service that polls CDEK for waybill updates."""
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable, Sequence
|
||||
from contextlib import AbstractAsyncContextManager
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from typing import Protocol
|
||||
|
||||
import structlog
|
||||
|
||||
from app.adapters.delivery_providers.cdek.order_mapper import (
|
||||
CDEKOrderInfo,
|
||||
CDEKWaybillInfo,
|
||||
)
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class CDEKOrderInfoAdapterProtocol(Protocol):
|
||||
async def get_order(self, cdek_order_uuid: str) -> CDEKOrderInfo: ...
|
||||
|
||||
|
||||
class CDEKWaybillInfoAdapterProtocol(Protocol):
|
||||
async def get_waybill(self, cdek_waybill_uuid: str) -> CDEKWaybillInfo: ...
|
||||
|
||||
|
||||
class OrderRecord(Protocol):
|
||||
order_uuid: str
|
||||
cdek_order_uuid: str | None
|
||||
cdek_waybill_uuid: str | None
|
||||
|
||||
|
||||
class WaybillPollerRepositoryProtocol(Protocol):
|
||||
def session(self) -> AbstractAsyncContextManager[object]: ...
|
||||
|
||||
async def list_orders_pending_waybill(
|
||||
self, session: object, *, limit: int
|
||||
) -> Sequence[OrderRecord]: ...
|
||||
|
||||
async def record_order_poll(
|
||||
self,
|
||||
session: object,
|
||||
*,
|
||||
order_uuid: str,
|
||||
order_status: str | None,
|
||||
waybill_uuid: str | None,
|
||||
polled_at: datetime,
|
||||
) -> object | None: ...
|
||||
|
||||
async def record_waybill_poll(
|
||||
self,
|
||||
session: object,
|
||||
*,
|
||||
order_uuid: str,
|
||||
waybill_url: str | None,
|
||||
polled_at: datetime,
|
||||
) -> object | None: ...
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PollBatchSummary:
|
||||
processed: int
|
||||
succeeded: int
|
||||
failed: int
|
||||
|
||||
|
||||
class WaybillPollerService:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
order_repository: WaybillPollerRepositoryProtocol,
|
||||
order_info_adapter: CDEKOrderInfoAdapterProtocol,
|
||||
waybill_info_adapter: CDEKWaybillInfoAdapterProtocol,
|
||||
batch_size: int,
|
||||
datetime_now: Callable[[], datetime] = lambda: datetime.now(timezone.utc),
|
||||
) -> None:
|
||||
self._repository = order_repository
|
||||
self._order_info_adapter = order_info_adapter
|
||||
self._waybill_info_adapter = waybill_info_adapter
|
||||
self._batch_size = batch_size
|
||||
self._datetime_now = datetime_now
|
||||
|
||||
async def poll_once(self) -> PollBatchSummary:
|
||||
async with self._repository.session() as session:
|
||||
orders = await self._repository.list_orders_pending_waybill(
|
||||
session, limit=self._batch_size
|
||||
)
|
||||
succeeded = 0
|
||||
failed = 0
|
||||
for order in orders:
|
||||
try:
|
||||
await self._handle_order(session, order)
|
||||
succeeded += 1
|
||||
except Exception:
|
||||
failed += 1
|
||||
logger.exception(
|
||||
"waybill_poll_order_failed",
|
||||
order_uuid=order.order_uuid,
|
||||
cdek_order_uuid=order.cdek_order_uuid,
|
||||
cdek_waybill_uuid=order.cdek_waybill_uuid,
|
||||
)
|
||||
return PollBatchSummary(
|
||||
processed=len(orders),
|
||||
succeeded=succeeded,
|
||||
failed=failed,
|
||||
)
|
||||
|
||||
async def run_forever(
|
||||
self,
|
||||
*,
|
||||
interval_seconds: float,
|
||||
stop_event: asyncio.Event,
|
||||
) -> None:
|
||||
while not stop_event.is_set():
|
||||
try:
|
||||
summary = await self.poll_once()
|
||||
logger.info(
|
||||
"waybill_poll_tick",
|
||||
processed=summary.processed,
|
||||
succeeded=summary.succeeded,
|
||||
failed=summary.failed,
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("waybill_poll_tick_failed")
|
||||
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
stop_event.wait(), timeout=interval_seconds
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
continue
|
||||
|
||||
async def _handle_order(self, session: object, order: OrderRecord) -> None:
|
||||
polled_at = self._datetime_now()
|
||||
if order.cdek_waybill_uuid is None:
|
||||
cdek_order_uuid = order.cdek_order_uuid
|
||||
if cdek_order_uuid is None:
|
||||
return
|
||||
info = await self._order_info_adapter.get_order(cdek_order_uuid)
|
||||
await self._repository.record_order_poll(
|
||||
session,
|
||||
order_uuid=order.order_uuid,
|
||||
order_status=info.status_code,
|
||||
waybill_uuid=info.waybill_uuid,
|
||||
polled_at=polled_at,
|
||||
)
|
||||
logger.info(
|
||||
"waybill_poll_order_result",
|
||||
order_uuid=order.order_uuid,
|
||||
cdek_order_uuid=cdek_order_uuid,
|
||||
cdek_order_status=info.status_code,
|
||||
cdek_waybill_uuid=info.waybill_uuid,
|
||||
)
|
||||
return
|
||||
|
||||
waybill = await self._waybill_info_adapter.get_waybill(order.cdek_waybill_uuid)
|
||||
await self._repository.record_waybill_poll(
|
||||
session,
|
||||
order_uuid=order.order_uuid,
|
||||
waybill_url=waybill.url,
|
||||
polled_at=polled_at,
|
||||
)
|
||||
logger.info(
|
||||
"waybill_poll_waybill_result",
|
||||
order_uuid=order.order_uuid,
|
||||
cdek_waybill_uuid=order.cdek_waybill_uuid,
|
||||
cdek_waybill_url=waybill.url,
|
||||
)
|
||||
@@ -0,0 +1,81 @@
|
||||
"""Background worker that polls CDEK for waybill updates."""
|
||||
|
||||
import asyncio
|
||||
import signal
|
||||
|
||||
import httpx
|
||||
import structlog
|
||||
|
||||
from app.adapters.delivery_providers.cdek.auth import CDEKAuthClient
|
||||
from app.adapters.delivery_providers.cdek.client import CDEKClient
|
||||
from app.adapters.postgres.engine import (
|
||||
create_postgres_engine,
|
||||
create_postgres_session_factory,
|
||||
)
|
||||
from app.config import Settings, get_settings
|
||||
from app.repositories.order import OrderRepository
|
||||
from app.runtime.logging import configure_logging
|
||||
from app.services.waybill_poller import WaybillPollerService
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
async def _run(settings: Settings, stop_event: asyncio.Event) -> None:
|
||||
http_client = httpx.AsyncClient(timeout=settings.adapter.cdek_timeout_seconds)
|
||||
auth_client = CDEKAuthClient(
|
||||
http_client=http_client,
|
||||
base_url=settings.adapter.cdek_base_url,
|
||||
client_id=settings.adapter.cdek_client_id,
|
||||
client_secret=settings.adapter.cdek_client_secret,
|
||||
timeout_seconds=settings.adapter.cdek_timeout_seconds,
|
||||
)
|
||||
cdek_client = CDEKClient(
|
||||
http_client=http_client,
|
||||
auth_client=auth_client,
|
||||
base_url=settings.adapter.cdek_base_url,
|
||||
timeout_seconds=settings.adapter.cdek_timeout_seconds,
|
||||
retry_attempts=settings.adapter.cdek_retry_attempts,
|
||||
retry_backoff_seconds=settings.adapter.cdek_retry_backoff_seconds,
|
||||
)
|
||||
engine = create_postgres_engine(settings.postgres)
|
||||
session_factory = create_postgres_session_factory(engine)
|
||||
repository = OrderRepository(session_factory=session_factory)
|
||||
service = WaybillPollerService(
|
||||
order_repository=repository,
|
||||
order_info_adapter=cdek_client,
|
||||
waybill_info_adapter=cdek_client,
|
||||
batch_size=settings.waybill_poller.batch_size,
|
||||
)
|
||||
logger.info(
|
||||
"waybill_poller_started",
|
||||
interval_seconds=settings.waybill_poller.interval_seconds,
|
||||
batch_size=settings.waybill_poller.batch_size,
|
||||
)
|
||||
try:
|
||||
await service.run_forever(
|
||||
interval_seconds=settings.waybill_poller.interval_seconds,
|
||||
stop_event=stop_event,
|
||||
)
|
||||
finally:
|
||||
await http_client.aclose()
|
||||
await engine.dispose()
|
||||
logger.info("waybill_poller_stopped")
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
configure_logging()
|
||||
settings = get_settings()
|
||||
stop_event = asyncio.Event()
|
||||
|
||||
loop = asyncio.get_running_loop()
|
||||
for sig in (signal.SIGTERM, signal.SIGINT):
|
||||
try:
|
||||
loop.add_signal_handler(sig, stop_event.set)
|
||||
except NotImplementedError:
|
||||
pass
|
||||
|
||||
await _run(settings, stop_event)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Reference in New Issue
Block a user