mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-21 04:21:13 +00:00
This commit is contained in:
@@ -2,13 +2,13 @@
|
||||
|
||||
## Current Step
|
||||
flow: existing-code
|
||||
step: 10
|
||||
name: Implement
|
||||
step: 11
|
||||
name: Run Tests
|
||||
status: in_progress
|
||||
sub_step:
|
||||
phase: 6
|
||||
name: implement-tasks
|
||||
detail: "batch 05 (AZ-963) done; cycle 4 has no more actionable tasks"
|
||||
phase: 1
|
||||
name: run-unit-tests
|
||||
detail: ""
|
||||
retry_count: 0
|
||||
cycle: 4
|
||||
tracker: jira
|
||||
|
||||
@@ -1,40 +1,3 @@
|
||||
"""C11 ``SatelliteProviderRouteClient`` (AZ-838 / Epic AZ-835 C2).
|
||||
|
||||
Operator-side HTTP client for the parent-suite Route API. Takes a
|
||||
:class:`gps_denied_onboard._types.route.RouteSpec` (produced
|
||||
by AZ-836 / C1) and onboards it with ``satellite-provider``:
|
||||
|
||||
1. **Pre-emptive validation** mirrors the AZ-809
|
||||
``CreateRouteRequestValidator`` rules so obviously-bad input fails
|
||||
before the HTTP POST.
|
||||
2. **POST** ``/api/satellite/route`` with ``requestMaps=true`` and
|
||||
``createTilesZip=false``. Wire shape derived from the live DTOs in
|
||||
``../satellite-provider/SatelliteProvider.Common/DTO/{CreateRouteRequest,RoutePoint,GeoPoint}.cs``.
|
||||
3. **Poll** ``GET /api/satellite/route/{id}`` until ``mapsReady=true``
|
||||
OR a terminal failure status; respects
|
||||
:attr:`SatelliteProviderRouteClient.poll_max_attempts` and
|
||||
:attr:`SatelliteProviderRouteClient.poll_interval_s`.
|
||||
4. **Inventory verify** via ``POST /api/satellite/tiles/inventory`` —
|
||||
enumerates the route's tile coverage locally from the
|
||||
``RouteSpec`` waypoints + ``regionSizeMeters`` and counts the
|
||||
``present=true`` entries returned by the server (lower bound on
|
||||
the actual coverage, since the server interpolates intermediate
|
||||
waypoints — documented in the contract).
|
||||
5. **Return** :class:`RouteSeedResult` with provenance fields
|
||||
(route id, terminal status, maps_ready flag, tile count, elapsed
|
||||
time, sha256 of the submitted payload).
|
||||
|
||||
The error hierarchy is rooted at :class:`SatelliteProviderRouteError`
|
||||
(in :mod:`.errors`), independent of :class:`TileManagerError` because
|
||||
the Route API is a corridor-onboarding flow, not a per-tile transfer.
|
||||
|
||||
Lives under ``c11_tile_manager`` because the existing C11 plumbing
|
||||
(JWT auth, TLS-insecure flag for self-signed dev certs) is shared and
|
||||
because C11 is already gated ``BUILD_C11_TILE_MANAGER=ON`` for the
|
||||
operator-orchestrator binary (and OFF for airborne) — same audience
|
||||
as the Route API.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
@@ -60,20 +23,11 @@ __all__ = [
|
||||
"SatelliteProviderRouteClient",
|
||||
]
|
||||
|
||||
|
||||
# AZ-838 wire constants — paths confirmed against
|
||||
# `../satellite-provider/SatelliteProvider.Api/Program.cs:266`+ on
|
||||
# 2026-05-22 (route create + route status) and against
|
||||
# `tile_downloader.py::_INVENTORY_PATH` for the inventory verify step.
|
||||
_ROUTE_CREATE_PATH = "/api/satellite/route"
|
||||
_ROUTE_STATUS_PATH_TPL = "/api/satellite/route/{id}"
|
||||
_INVENTORY_PATH = "/api/satellite/tiles/inventory"
|
||||
_INVENTORY_MAX_ENTRIES_PER_REQUEST = 5000
|
||||
|
||||
# AZ-809 validator bounds (mirrored from
|
||||
# `SatelliteProvider.Api/Validators/CreateRouteRequestValidator.cs`).
|
||||
# Keep these in sync with that file — the client pre-emptively
|
||||
# enforces them so obviously-bad input fails before the HTTP POST.
|
||||
_VALIDATOR_NAME_MAX_LEN: int = 200
|
||||
_VALIDATOR_DESCRIPTION_MAX_LEN: int = 1000
|
||||
_VALIDATOR_REGION_SIZE_MIN_M: float = 100.0
|
||||
@@ -83,16 +37,8 @@ _VALIDATOR_ZOOM_MAX: int = 22
|
||||
_VALIDATOR_POINTS_MIN: int = 2
|
||||
_VALIDATOR_POINTS_MAX: int = 500
|
||||
|
||||
# Mirror of the parent-suite tile-size math used by C11
|
||||
# (`tile_downloader._EARTH_EQUATORIAL_CIRCUMFERENCE_M` /
|
||||
# `_TILE_SIZE_PIXELS`). Re-stated here so the inventory-coverage
|
||||
# enumeration does not depend on a private constant from the
|
||||
# downloader module.
|
||||
_EARTH_EQUATORIAL_CIRCUMFERENCE_M: float = 40_075_016.686
|
||||
|
||||
# Terminal status strings the parent suite reports via
|
||||
# `GET /api/satellite/route/{id}`. Mirrors `seed_region.py`'s set so
|
||||
# both Region and Route flows agree on terminal semantics.
|
||||
_TERMINAL_STATUSES: frozenset[str] = frozenset(
|
||||
{"completed", "failed", "error", "done", "succeeded", "rejected"}
|
||||
)
|
||||
@@ -116,33 +62,6 @@ _LOG_KIND_VALIDATION_FAIL = "c11.route.validation_failed"
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class RouteSeedResult:
|
||||
"""Outcome of one :meth:`SatelliteProviderRouteClient.seed_route` call.
|
||||
|
||||
Attributes:
|
||||
route_id: The ``id`` field POSTed in the request — kept here
|
||||
so the caller can re-query ``GET /api/satellite/route/{id}``
|
||||
without re-deriving it.
|
||||
terminal_status: The server's last observed status string
|
||||
(one of the values in :data:`_TERMINAL_STATUSES`, lower-
|
||||
cased). On a healthy run this is typically ``completed``.
|
||||
maps_ready: ``True`` if the server reported ``mapsReady=true``
|
||||
within the poll budget. ``False`` only on terminal
|
||||
failure paths that do NOT raise (currently impossible —
|
||||
terminal failures always raise; the field is here for
|
||||
forward compatibility if the server adds a "ready
|
||||
without maps" state).
|
||||
tile_count: Number of (z, x, y) entries the inventory call
|
||||
reported as ``present=true``. Lower bound on the actual
|
||||
tile coverage produced by the server, since the local
|
||||
enumeration does NOT account for the server-side
|
||||
~200 m intermediate-point interpolation documented in
|
||||
``../satellite-provider/_docs/02_document/contracts/api/route-creation.md``.
|
||||
elapsed_ms: Wall-clock milliseconds from the start of the
|
||||
POST submission to the completion of the inventory verify.
|
||||
submitted_payload_sha256: SHA-256 hex digest of the JSON body
|
||||
POSTed to ``/api/satellite/route`` (provenance / audit).
|
||||
"""
|
||||
|
||||
route_id: uuid.UUID
|
||||
terminal_status: str
|
||||
maps_ready: bool
|
||||
@@ -152,21 +71,6 @@ class RouteSeedResult:
|
||||
|
||||
|
||||
class SatelliteProviderRouteClient:
|
||||
"""HTTP client for the parent-suite Route API (AZ-838).
|
||||
|
||||
Constructor parameters mirror the operator-side ergonomics
|
||||
(``base_url`` + ``jwt`` + ``tls_insecure`` for self-signed dev
|
||||
certs), matching the existing ``seed_region.py`` flag surface so
|
||||
operators can use a single ``.env.test`` file.
|
||||
|
||||
For tests, an optional ``http_client`` may be injected — the
|
||||
standard ``httpx.MockTransport`` pattern from
|
||||
``test_tile_downloader.py`` works directly. When ``http_client``
|
||||
is ``None`` (production / CLI use), the client owns its own
|
||||
short-lived :class:`httpx.Client` per ``seed_route`` call so the
|
||||
caller does not need to manage connection lifetime.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
base_url: str,
|
||||
@@ -211,10 +115,6 @@ class SatelliteProviderRouteClient:
|
||||
"gps_denied_onboard.components.c11_tile_manager.route_client"
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Public API
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def seed_route(
|
||||
self,
|
||||
spec: RouteSpec,
|
||||
@@ -224,38 +124,6 @@ class SatelliteProviderRouteClient:
|
||||
zoom_level: int = 18,
|
||||
description: str | None = None,
|
||||
) -> RouteSeedResult:
|
||||
"""Onboard ``spec`` with the parent-suite Route API.
|
||||
|
||||
Args:
|
||||
spec: The :class:`RouteSpec` produced by AZ-836's
|
||||
``extract_route_from_tlog``.
|
||||
name: Optional human-readable name. When ``None``, derived
|
||||
from the spec's ``source_tlog`` stem + a short hash of
|
||||
the waypoints (deterministic for the same RouteSpec).
|
||||
region_size_meters: Per-waypoint coverage radius in
|
||||
metres. When ``None``, falls back to
|
||||
:attr:`RouteSpec.suggested_region_size_meters`. The
|
||||
combined value MUST be in the AZ-809 validator range
|
||||
``[100, 10000]``.
|
||||
zoom_level: Web-Mercator zoom for the route. Defaults to
|
||||
18 — matches ``seed_region.py``'s ``zoom_levels``
|
||||
default. AZ-809 validator accepts ``[0, 22]``.
|
||||
description: Optional free-text description (max 1000
|
||||
chars per AZ-809).
|
||||
|
||||
Returns:
|
||||
:class:`RouteSeedResult` on success.
|
||||
|
||||
Raises:
|
||||
RouteValidationError: Pre-emptive validation rejected the
|
||||
inputs OR the server returned 4xx + RFC 7807.
|
||||
RouteTransientError: 5xx / network / timeout. The
|
||||
underlying ``httpx`` exception is on ``__cause__``.
|
||||
RouteTerminalFailureError: ``mapsReady=true`` was never
|
||||
reached within the poll budget OR the server reported
|
||||
a terminal failure status.
|
||||
"""
|
||||
|
||||
effective_region_size = float(
|
||||
region_size_meters
|
||||
if region_size_meters is not None
|
||||
@@ -273,8 +141,6 @@ class SatelliteProviderRouteClient:
|
||||
description=description,
|
||||
)
|
||||
|
||||
# Pre-emptive validation runs against the assembled body so
|
||||
# the rules apply to whatever the server is about to see.
|
||||
self._preemptive_validate(request_body)
|
||||
|
||||
payload_bytes = _canonical_json_bytes(request_body)
|
||||
@@ -311,12 +177,6 @@ class SatelliteProviderRouteClient:
|
||||
zoom_level: int = 18,
|
||||
description: str | None = None,
|
||||
) -> tuple[dict[str, Any], str]:
|
||||
"""Return the planned request body + its sha256 without HTTP.
|
||||
|
||||
Powers ``seed_route.py --dry-run`` (AC-7). Runs the same
|
||||
pre-emptive validation as :meth:`seed_route`, so a dry-run
|
||||
surfaces validation errors the same way a live run would.
|
||||
"""
|
||||
|
||||
effective_region_size = float(
|
||||
region_size_meters
|
||||
@@ -337,10 +197,6 @@ class SatelliteProviderRouteClient:
|
||||
sha256 = hashlib.sha256(_canonical_json_bytes(body)).hexdigest()
|
||||
return body, sha256
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Internal pipeline
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _run(
|
||||
self,
|
||||
*,
|
||||
@@ -613,10 +469,6 @@ class SatelliteProviderRouteClient:
|
||||
)
|
||||
return present_count
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Validation + payload assembly
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _build_request_body(
|
||||
self,
|
||||
*,
|
||||
@@ -627,13 +479,6 @@ class SatelliteProviderRouteClient:
|
||||
zoom_level: int,
|
||||
description: str | None,
|
||||
) -> dict[str, Any]:
|
||||
"""Assemble the wire body matching CreateRouteRequest.cs / RoutePoint.cs.
|
||||
|
||||
Per the AZ-809 batch-03 review F3, ``RoutePoint`` uses
|
||||
``[JsonPropertyName("lat"|"lon")]`` so we serialize ``lat`` /
|
||||
``lon`` (NOT ``latitude`` / ``longitude``).
|
||||
"""
|
||||
|
||||
body: dict[str, Any] = {
|
||||
"id": str(route_id),
|
||||
"name": name,
|
||||
|
||||
@@ -18,6 +18,8 @@ from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
pytest.importorskip("torch")
|
||||
import torch
|
||||
from torch import nn
|
||||
|
||||
|
||||
Reference in New Issue
Block a user