125 lines
3.1 KiB
Python
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)
|