"""C11 TileManager config block (AZ-316, AZ-319, AZ-320). Registered into ``config.components['c11_tile_manager']`` by the package ``__init__.py``. Three composition-root factories read this block: * :func:`gps_denied_onboard.runtime_root.c11_factory.build_tile_uploader` reads the ``upload_*`` fields, ``companion_id``, and the AZ-320 ``retry`` block (``disable_retry_decorator`` + the per-tile / per-call retry knobs) to drive AZ-319 + the optional AZ-320 decorator. * :func:`gps_denied_onboard.runtime_root.c11_factory.build_tile_downloader` reads the ``satellite_provider_url``, ``service_api_key``, and ``download_*`` fields to drive AZ-316. All defaults are conservative no-op values so unit tests / replay runs that do not exercise C11 keep working without YAML; the factory raises :class:`ConfigError` when an empty production-required field (``service_api_key``, ``companion_id``, etc.) is observed in operator wiring. """ from __future__ import annotations from dataclasses import dataclass, field from gps_denied_onboard.config.schema import ConfigError __all__ = ["C11Config", "C11RetryConfig"] _DEFAULT_BATCH_SIZE: int = 25 _DEFAULT_HTTP_TIMEOUT_S: float = 30.0 _DEFAULT_MAX_RETRY_AFTER_S: int = 600 _MAX_BATCH_SIZE: int = 200 _DEFAULT_DOWNLOAD_RESOLUTION_FLOOR: float = 0.5 _DEFAULT_DOWNLOAD_MAX_5XX_RETRIES: int = 4 _MIN_DOWNLOAD_RETRIES: int = 1 _MAX_DOWNLOAD_RETRIES: int = 16 _DEFAULT_MAX_IN_CALL_RETRIES: int = 3 _DEFAULT_MAX_PER_TILE_ATTEMPTS: int = 5 _DEFAULT_RETRY_BACKOFF_BASE_S: float = 2.0 _DEFAULT_RETRY_BACKOFF_CAP_S: float = 60.0 @dataclass(frozen=True) class C11RetryConfig: """C11 ``IdempotentRetryTileUploader`` knobs (AZ-320). * ``max_in_call_retries`` — bounded loop count for partial-success re-invocations of the wrapped uploader within a single call. * ``max_per_tile_attempts`` — terminal threshold per tile across ALL calls; exceeding the threshold moves the tile to :class:`VotingStatus.UPLOAD_GIVEUP` (a human-decision boundary — automated promotion back to ``PENDING`` is forbidden). * ``backoff_base_s`` — base of the exponential backoff used between in-call retries (``base ** retries_used``). * ``backoff_cap_s`` — upper bound on each individual backoff sleep; also used as the operator hint for ``next_retry_at_s`` when the in-call budget is exhausted. """ max_in_call_retries: int = _DEFAULT_MAX_IN_CALL_RETRIES max_per_tile_attempts: int = _DEFAULT_MAX_PER_TILE_ATTEMPTS backoff_base_s: float = _DEFAULT_RETRY_BACKOFF_BASE_S backoff_cap_s: float = _DEFAULT_RETRY_BACKOFF_CAP_S def __post_init__(self) -> None: if self.max_in_call_retries < 0: raise ConfigError( "C11RetryConfig.max_in_call_retries must be >= 0; " f"got {self.max_in_call_retries}" ) if self.max_per_tile_attempts <= 0: raise ConfigError( "C11RetryConfig.max_per_tile_attempts must be > 0; " f"got {self.max_per_tile_attempts}" ) if self.backoff_base_s <= 0: raise ConfigError( "C11RetryConfig.backoff_base_s must be > 0; " f"got {self.backoff_base_s}" ) if self.backoff_cap_s <= 0: raise ConfigError( "C11RetryConfig.backoff_cap_s must be > 0; " f"got {self.backoff_cap_s}" ) @dataclass(frozen=True) class C11Config: """Per-component config for C11 tile manager (upload + download paths). Upload-side fields (AZ-319): * ``satellite_provider_ingest_url`` — base URL for the upload endpoint; ``HttpTileUploader`` appends ``/api/satellite/tiles/ingest``. Empty → upload factory raises :class:`ConfigError`. * ``upload_batch_size`` — tiles per multipart POST. * ``upload_http_timeout_s`` — per-request timeout (seconds). * ``upload_max_retry_after_s`` — cumulative 429 ``Retry-After`` cap before :class:`RateLimitedError`. * ``companion_id`` — stable per-companion id for D-PROJ-2 voting. Download-side fields (AZ-316): * ``satellite_provider_url`` — base URL for the GET surface; ``HttpTileDownloader`` appends per-tile / list paths. * ``service_api_key`` — bearer token for authenticated GETs; logged ONLY redacted (``Bearer ***``). Empty → download factory raises :class:`ConfigError`. * ``download_http_timeout_s`` — per-request timeout (seconds). * ``download_max_5xx_retries`` — exponential-backoff cap before :class:`SatelliteProviderError`. * ``download_max_retry_after_s`` — cumulative 429 ``Retry-After`` cap before :class:`RateLimitedError`. * ``download_resolution_floor_m_per_px`` — RESTRICT-SAT-4 lower bound for the C11 boundary check; defaults to 0.5 m/px. """ satellite_provider_ingest_url: str = "" upload_batch_size: int = _DEFAULT_BATCH_SIZE upload_http_timeout_s: float = _DEFAULT_HTTP_TIMEOUT_S upload_max_retry_after_s: int = _DEFAULT_MAX_RETRY_AFTER_S companion_id: str = "" satellite_provider_url: str = "" service_api_key: str = "" download_http_timeout_s: float = _DEFAULT_HTTP_TIMEOUT_S download_max_5xx_retries: int = _DEFAULT_DOWNLOAD_MAX_5XX_RETRIES download_max_retry_after_s: int = _DEFAULT_MAX_RETRY_AFTER_S download_resolution_floor_m_per_px: float = _DEFAULT_DOWNLOAD_RESOLUTION_FLOOR disable_retry_decorator: bool = False retry: C11RetryConfig = field(default_factory=C11RetryConfig) def __post_init__(self) -> None: if not 1 <= self.upload_batch_size <= _MAX_BATCH_SIZE: raise ConfigError( "C11Config.upload_batch_size must be in " f"[1, {_MAX_BATCH_SIZE}]; got {self.upload_batch_size}" ) if self.upload_http_timeout_s <= 0: raise ConfigError( "C11Config.upload_http_timeout_s must be > 0; " f"got {self.upload_http_timeout_s}" ) if self.upload_max_retry_after_s <= 0: raise ConfigError( "C11Config.upload_max_retry_after_s must be > 0; " f"got {self.upload_max_retry_after_s}" ) if self.download_http_timeout_s <= 0: raise ConfigError( "C11Config.download_http_timeout_s must be > 0; " f"got {self.download_http_timeout_s}" ) if not _MIN_DOWNLOAD_RETRIES <= self.download_max_5xx_retries <= _MAX_DOWNLOAD_RETRIES: raise ConfigError( "C11Config.download_max_5xx_retries must be in " f"[{_MIN_DOWNLOAD_RETRIES}, {_MAX_DOWNLOAD_RETRIES}]; " f"got {self.download_max_5xx_retries}" ) if self.download_max_retry_after_s <= 0: raise ConfigError( "C11Config.download_max_retry_after_s must be > 0; " f"got {self.download_max_retry_after_s}" ) if self.download_resolution_floor_m_per_px <= 0: raise ConfigError( "C11Config.download_resolution_floor_m_per_px must be > 0; " f"got {self.download_resolution_floor_m_per_px}" ) if not isinstance(self.retry, C11RetryConfig): raise ConfigError( "C11Config.retry must be a C11RetryConfig; got " f"{type(self.retry).__name__}" )