mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-21 07:01:14 +00:00
This commit is contained in:
@@ -2,13 +2,13 @@
|
|||||||
|
|
||||||
## Current Step
|
## Current Step
|
||||||
flow: existing-code
|
flow: existing-code
|
||||||
step: 10
|
step: 11
|
||||||
name: Implement
|
name: Run Tests
|
||||||
status: in_progress
|
status: in_progress
|
||||||
sub_step:
|
sub_step:
|
||||||
phase: 6
|
phase: 1
|
||||||
name: implement-tasks
|
name: run-unit-tests
|
||||||
detail: "batch 05 (AZ-963) done; cycle 4 has no more actionable tasks"
|
detail: ""
|
||||||
retry_count: 0
|
retry_count: 0
|
||||||
cycle: 4
|
cycle: 4
|
||||||
tracker: jira
|
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
|
from __future__ import annotations
|
||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
@@ -60,20 +23,11 @@ __all__ = [
|
|||||||
"SatelliteProviderRouteClient",
|
"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_CREATE_PATH = "/api/satellite/route"
|
||||||
_ROUTE_STATUS_PATH_TPL = "/api/satellite/route/{id}"
|
_ROUTE_STATUS_PATH_TPL = "/api/satellite/route/{id}"
|
||||||
_INVENTORY_PATH = "/api/satellite/tiles/inventory"
|
_INVENTORY_PATH = "/api/satellite/tiles/inventory"
|
||||||
_INVENTORY_MAX_ENTRIES_PER_REQUEST = 5000
|
_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_NAME_MAX_LEN: int = 200
|
||||||
_VALIDATOR_DESCRIPTION_MAX_LEN: int = 1000
|
_VALIDATOR_DESCRIPTION_MAX_LEN: int = 1000
|
||||||
_VALIDATOR_REGION_SIZE_MIN_M: float = 100.0
|
_VALIDATOR_REGION_SIZE_MIN_M: float = 100.0
|
||||||
@@ -83,16 +37,8 @@ _VALIDATOR_ZOOM_MAX: int = 22
|
|||||||
_VALIDATOR_POINTS_MIN: int = 2
|
_VALIDATOR_POINTS_MIN: int = 2
|
||||||
_VALIDATOR_POINTS_MAX: int = 500
|
_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
|
_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(
|
_TERMINAL_STATUSES: frozenset[str] = frozenset(
|
||||||
{"completed", "failed", "error", "done", "succeeded", "rejected"}
|
{"completed", "failed", "error", "done", "succeeded", "rejected"}
|
||||||
)
|
)
|
||||||
@@ -116,33 +62,6 @@ _LOG_KIND_VALIDATION_FAIL = "c11.route.validation_failed"
|
|||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
@dataclass(frozen=True, slots=True)
|
||||||
class RouteSeedResult:
|
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
|
route_id: uuid.UUID
|
||||||
terminal_status: str
|
terminal_status: str
|
||||||
maps_ready: bool
|
maps_ready: bool
|
||||||
@@ -152,21 +71,6 @@ class RouteSeedResult:
|
|||||||
|
|
||||||
|
|
||||||
class SatelliteProviderRouteClient:
|
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__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
base_url: str,
|
base_url: str,
|
||||||
@@ -211,10 +115,6 @@ class SatelliteProviderRouteClient:
|
|||||||
"gps_denied_onboard.components.c11_tile_manager.route_client"
|
"gps_denied_onboard.components.c11_tile_manager.route_client"
|
||||||
)
|
)
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# Public API
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
|
|
||||||
def seed_route(
|
def seed_route(
|
||||||
self,
|
self,
|
||||||
spec: RouteSpec,
|
spec: RouteSpec,
|
||||||
@@ -224,38 +124,6 @@ class SatelliteProviderRouteClient:
|
|||||||
zoom_level: int = 18,
|
zoom_level: int = 18,
|
||||||
description: str | None = None,
|
description: str | None = None,
|
||||||
) -> RouteSeedResult:
|
) -> 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(
|
effective_region_size = float(
|
||||||
region_size_meters
|
region_size_meters
|
||||||
if region_size_meters is not None
|
if region_size_meters is not None
|
||||||
@@ -273,8 +141,6 @@ class SatelliteProviderRouteClient:
|
|||||||
description=description,
|
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)
|
self._preemptive_validate(request_body)
|
||||||
|
|
||||||
payload_bytes = _canonical_json_bytes(request_body)
|
payload_bytes = _canonical_json_bytes(request_body)
|
||||||
@@ -311,12 +177,6 @@ class SatelliteProviderRouteClient:
|
|||||||
zoom_level: int = 18,
|
zoom_level: int = 18,
|
||||||
description: str | None = None,
|
description: str | None = None,
|
||||||
) -> tuple[dict[str, Any], str]:
|
) -> 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(
|
effective_region_size = float(
|
||||||
region_size_meters
|
region_size_meters
|
||||||
@@ -337,10 +197,6 @@ class SatelliteProviderRouteClient:
|
|||||||
sha256 = hashlib.sha256(_canonical_json_bytes(body)).hexdigest()
|
sha256 = hashlib.sha256(_canonical_json_bytes(body)).hexdigest()
|
||||||
return body, sha256
|
return body, sha256
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# Internal pipeline
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
|
|
||||||
def _run(
|
def _run(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
@@ -613,10 +469,6 @@ class SatelliteProviderRouteClient:
|
|||||||
)
|
)
|
||||||
return present_count
|
return present_count
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# Validation + payload assembly
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
|
|
||||||
def _build_request_body(
|
def _build_request_body(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
@@ -627,13 +479,6 @@ class SatelliteProviderRouteClient:
|
|||||||
zoom_level: int,
|
zoom_level: int,
|
||||||
description: str | None,
|
description: str | None,
|
||||||
) -> dict[str, Any]:
|
) -> 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] = {
|
body: dict[str, Any] = {
|
||||||
"id": str(route_id),
|
"id": str(route_id),
|
||||||
"name": name,
|
"name": name,
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ from pathlib import Path
|
|||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
pytest.importorskip("torch")
|
||||||
import torch
|
import torch
|
||||||
from torch import nn
|
from torch import nn
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user