"""AZ-701 — typed HTTP error families for the replay_api service. Every error has a stable ``error_code`` (string) the contract pins in ``_docs/02_document/contracts/replay_api/replay_api_protocol.md``. The handler layer translates these into JSON responses; the business layer raises them without knowing about HTTP. """ from __future__ import annotations from typing import Any __all__ = [ "ConcurrencyLimitReachedError", "JobNotCompleteError", "JobNotFoundError", "MultipartMissingFieldError", "PayloadTooLargeError", "ReplayApiError", "ReplayRunnerError", "UnauthorizedError", "UnsupportedFileKindError", ] class ReplayApiError(Exception): """Base for every typed replay_api error. Subclasses pin a stable ``error_code`` and HTTP ``status_code``; the handler layer reads both to build a JSON response. """ error_code: str = "replay_api_error" status_code: int = 500 def __init__(self, message: str, details: dict[str, Any] | None = None) -> None: super().__init__(message) self.message = message self.details = details or {} class UnsupportedFileKindError(ReplayApiError): error_code = "unsupported_file_kind" status_code = 400 class MultipartMissingFieldError(ReplayApiError): error_code = "multipart_missing_field" status_code = 400 class UnauthorizedError(ReplayApiError): error_code = "unauthorized" status_code = 401 class JobNotFoundError(ReplayApiError): error_code = "job_not_found" status_code = 404 class JobNotCompleteError(ReplayApiError): error_code = "job_not_complete" status_code = 409 class PayloadTooLargeError(ReplayApiError): error_code = "payload_too_large" status_code = 413 class ConcurrencyLimitReachedError(ReplayApiError): """Raised when the queue is full. Note: per-spec, hitting just the running-job concurrency limit does NOT raise this — those jobs queue normally. The 429 case is "queue itself is full" only. """ error_code = "concurrency_limit_reached" status_code = 429 class ReplayRunnerError(ReplayApiError): error_code = "replay_runner_failed" status_code = 500