mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-22 13:41:14 +00:00
[AZ-701] HTTP replay API service (FastAPI + magic-byte upload validation)
ci/woodpecker/push/02-build-push Pipeline failed
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>
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user