Files
wyndham-ARR/arr_ingestion/direct_service.py
2026-07-29 16:38:05 +08:00

125 lines
3.1 KiB
Python

"""One-call direct structured-result orchestration owned by ARR."""
from __future__ import annotations
from typing import Any, Protocol
from arr_ingestion.contracts import IngestionError
from arr_ingestion.direct_contracts import (
DirectSubmissionReceipt,
DirectSubmissionRequest,
ReceivedDirectSubmission,
SubmissionGrant,
VerifiedDirectSubmission,
)
class DirectSubmissionRepository(Protocol):
def assert_ready(self) -> None:
...
def issue_grant(
self,
job_id: str,
attempt_no: int,
*,
ttl_seconds: int,
) -> SubmissionGrant:
...
def receive(
self,
request: DirectSubmissionRequest,
) -> ReceivedDirectSubmission:
...
def commit(
self,
verified: VerifiedDirectSubmission,
) -> DirectSubmissionReceipt:
...
def reject(
self,
submission: ReceivedDirectSubmission,
failure_code: str,
) -> None:
...
def expire_stale(self, *, limit: int) -> int:
...
class DirectSubmissionValidator(Protocol):
def validate(
self,
submission: ReceivedDirectSubmission,
) -> VerifiedDirectSubmission:
...
_RETRYABLE_VALIDATION_CODES = frozenset(
{
"ARTIFACT_NOT_FOUND",
"ARTIFACT_UNREADABLE",
"DIRECT_REPLAY_UNAVAILABLE",
"OBJECT_STORE_UNAVAILABLE",
}
)
class DirectSubmissionService:
"""Persist, replay, and atomically commit one complete structured payload."""
def __init__(
self,
validator: DirectSubmissionValidator,
repository: DirectSubmissionRepository,
) -> None:
self._validator = validator
self._repository = repository
def issue_grant(
self,
job_id: str,
attempt_no: int,
*,
ttl_seconds: int = 900,
) -> SubmissionGrant:
return self._repository.issue_grant(
job_id,
attempt_no,
ttl_seconds=ttl_seconds,
)
def assert_ready(self) -> None:
self._repository.assert_ready()
def submit(self, raw_request: Any) -> DirectSubmissionReceipt:
request = DirectSubmissionRequest.from_dict(raw_request)
submission = self._repository.receive(request)
if submission.receipt is not None:
return submission.receipt
try:
verified = self._validator.validate(submission)
except IngestionError as error:
if (
not error.retryable
and error.code not in _RETRYABLE_VALIDATION_CODES
):
self._repository.reject(submission, error.code)
raise
return self._repository.commit(verified)
def expire_stale(self, *, limit: int = 100) -> int:
if (
not isinstance(limit, int)
or isinstance(limit, bool)
or not 1 <= limit <= 1000
):
raise IngestionError(
"DIRECT_SUBMISSION_INVALID",
"direct submission expiry limit is invalid",
)
return self._repository.expire_stale(limit=limit)