mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-21 10:11:12 +00:00
7d53cef0cf
ci/woodpecker/push/02-build-push Pipeline failed
New replay_api component: FastAPI service wrapping the offline
gps-denied-replay pipeline. POST tlog+video (multipart) → either
sync 200 with result/map/report URLs, or async 202 + job id with
/jobs/{id} polling. Magic-byte validation, bearer auth, in-memory
JobRegistry with concurrency + queue caps (429 on overflow).
Helper accuracy_report.py promoted from tests/ to src/ because the
API needs the Markdown report writer at runtime; all AZ-699 imports
re-pointed. OpenAPI spec exported to docs.
18/18 unit tests pass (AC-1 sync, AC-2 async, AC-3 state machine,
AC-5 auth, AC-6 health, AC-8 concurrency, AC-9 magic-byte). Full
unit suite: 2251 pass, 86 skip, 1 pre-existing C12 cold-start flake
(unchanged). mypy --strict clean on the new surface.
Co-authored-by: Cursor <cursoragent@cursor.com>
87 lines
2.2 KiB
Python
87 lines
2.2 KiB
Python
"""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
|