Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 20 additions & 9 deletions controls/sae_2025_ws/src/integration/backend/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,22 @@ def _normalize_fleet_file(value: object) -> str:
return Path(raw.replace("\\", "/")).name.strip()


def _optional_float(value: object, *, field: str) -> float | None:
if value in (None, ""):
return None
if isinstance(value, bool) or not isinstance(value, int | float | str):
raise ValueError(f"{field} must be numeric when provided.")
return float(value)


def _optional_int(value: object, *, field: str) -> int | None:
if value in (None, ""):
return None
if isinstance(value, bool) or not isinstance(value, int | float | str):
raise ValueError(f"{field} must be an integer when provided.")
return int(value)


@dataclass(slots=True)
class OperatorConfig:
github_repo: str
Expand Down Expand Up @@ -608,21 +624,16 @@ def from_dict(cls, data: dict[str, object] | None) -> "BuildSourceRecord":
name=str(payload.get("name", "")).strip(),
date=str(payload.get("date", "")).strip(),
download_url=str(payload.get("download_url", "")).strip(),
size_mb=(
float(payload["size_mb"])
if payload.get("size_mb") not in (None, "")
else None
),
size_mb=_optional_float(payload.get("size_mb"), field="size_mb"),
branch=str(payload.get("branch", "")).strip(),
artifact_name=str(payload.get("artifact_name", "")).strip(),
workflow_name=str(payload.get("workflow_name", "")).strip(),
workflow_event=str(payload.get("workflow_event", "")).strip(),
workflow_conclusion=str(payload.get("workflow_conclusion", "")).strip(),
local_artifact_path=str(payload.get("local_artifact_path", "")).strip(),
local_artifact_size_bytes=(
int(payload["local_artifact_size_bytes"])
if payload.get("local_artifact_size_bytes") not in (None, "")
else None
local_artifact_size_bytes=_optional_int(
payload.get("local_artifact_size_bytes"),
field="local_artifact_size_bytes",
),
codebase_root=str(payload.get("codebase_root", "")).strip(),
fleet_file=fleet_file,
Expand Down
33 changes: 26 additions & 7 deletions controls/sae_2025_ws/src/integration/backend/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@

def _require_zeroconf():
try:
from zeroconf import IPVersion, ServiceBrowser, ServiceListener, Zeroconf
from zeroconf import IPVersion, ServiceBrowser, ServiceListener, Zeroconf # pyright: ignore[reportMissingImports]
except ModuleNotFoundError as exc:
raise RuntimeError(
"Missing Python dependency `zeroconf`. "
Expand Down Expand Up @@ -91,10 +91,29 @@ def _snapshot_key(hostname: str) -> str:
return normalized or (hostname or "").strip().lower()


def _float_snapshot_value(
entry: dict[str, object], key: str, *, default: float = 0.0
) -> float:
value = entry.get(key, default)
if isinstance(value, bool) or not isinstance(value, int | float | str):
return default
try:
return float(value)
except ValueError:
return default


def _string_sequence_value(entry: dict[str, object], key: str) -> list[str]:
value = entry.get(key, [])
if not isinstance(value, list | tuple | set):
return []
return [str(item) for item in value if str(item).strip()]


def _mark_snapshot_stale(now_monotonic: float | None = None) -> None:
now_value = now_monotonic if now_monotonic is not None else time.monotonic()
for key, entry in list(_DISCOVERY_SNAPSHOT.items()):
last_seen = float(entry.get("last_seen_monotonic", 0.0) or 0.0)
last_seen = _float_snapshot_value(entry, "last_seen_monotonic")
age = (
now_value - last_seen if last_seen else _DISCOVERY_STALE_WINDOW_SECONDS + 1
)
Expand All @@ -107,7 +126,7 @@ def _mark_snapshot_stale(now_monotonic: float | None = None) -> None:
def _prune_snapshot(now_monotonic: float | None = None) -> None:
now_value = now_monotonic if now_monotonic is not None else time.monotonic()
for key, entry in list(_DISCOVERY_SNAPSHOT.items()):
last_seen = float(entry.get("last_seen_monotonic", 0.0) or 0.0)
last_seen = _float_snapshot_value(entry, "last_seen_monotonic")
age = (
now_value - last_seen if last_seen else _DISCOVERY_STALE_WINDOW_SECONDS + 1
)
Expand Down Expand Up @@ -142,7 +161,7 @@ def _update_snapshot(discovered: list[_DiscoveredService]) -> None:
if key in fresh_keys:
entry["discovery_stale"] = False
continue
last_seen = float(entry.get("last_seen_monotonic", 0.0) or 0.0)
last_seen = _float_snapshot_value(entry, "last_seen_monotonic")
age = (
now_monotonic - last_seen
if last_seen
Expand All @@ -169,7 +188,7 @@ def _snapshot_cards(ctx: AppContext) -> list[dict[str, object]]:
entry.get("hardware_id") or _snapshot_key(hostname) or hostname
),
"hostname": hostname,
"addresses": list(entry.get("addresses", []) or []),
"addresses": _string_sequence_value(entry, "addresses"),
"service_name": entry.get("service_name"),
"service_type": entry.get("service_type"),
"last_seen_at": entry.get("last_seen_at"),
Expand Down Expand Up @@ -264,7 +283,7 @@ def _resolve_socket_addresses(hostname: str) -> set[str]:
for info in socket.getaddrinfo(hostname, None, family=socket.AF_UNSPEC):
sockaddr = info[4]
if isinstance(sockaddr, tuple) and sockaddr:
addresses.add(sockaddr[0])
addresses.add(str(sockaddr[0]))
except OSError:
return set()
return addresses
Expand Down Expand Up @@ -300,7 +319,7 @@ def add_service(self, zeroconf, service_type: str, name: str) -> None:
self.found[key] = record
addresses = info.parsed_addresses(IPVersion.All) or []
for address in addresses:
record.addresses.add(address)
record.addresses.add(str(address))

zeroconf = Zeroconf(ip_version=IPVersion.All)
listener = Listener(zeroconf)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from ..models import (
ConfigResponse,
MessageResponse,
OperatorConfigPayload,
)


Expand All @@ -17,7 +18,9 @@ def build_router(ctx: AppContext) -> APIRouter:
@router.get("", response_model=ConfigResponse)
async def get_config() -> ConfigResponse:
return ConfigResponse(
config=ctx.operator_config.to_safe_dict(),
config=OperatorConfigPayload.model_validate(
ctx.operator_config.to_safe_dict()
),
)

@router.post("", response_model=MessageResponse)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from fastapi import APIRouter

from ..context import AppContext
from ..models import HardwareDiscoveryResponse
from ..models import HardwareDiscoveryResponse, LiveHardwareDeviceResponse


def build_router(ctx: AppContext) -> APIRouter:
Expand All @@ -15,7 +15,13 @@ async def live_hardware() -> HardwareDiscoveryResponse:

try:
devices = await discovery.live_hardware_cards(ctx)
return HardwareDiscoveryResponse(success=True, devices=devices)
return HardwareDiscoveryResponse(
success=True,
devices=[
LiveHardwareDeviceResponse.model_validate(device)
for device in devices
],
)
except Exception as exc:
return HardwareDiscoveryResponse(success=False, error=str(exc))

Expand Down
26 changes: 19 additions & 7 deletions controls/sae_2025_ws/src/integration/backend/routers/inventory.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,20 +109,32 @@ async def import_inventory(
replace_existing=str(replace_existing or "").strip().lower()
in {"1", "true", "yes", "on"},
)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
except json.JSONDecodeError as exc:
raise HTTPException(
status_code=400,
detail="Inventory import file must be valid JSON.",
) from exc
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc

imported_count_value = summary.get("targets_imported", 0)
imported_count = (
imported_count_value if isinstance(imported_count_value, int) else 0
)
target_ids_value = summary.get("target_ids", [])
target_ids = (
[str(target_id) for target_id in target_ids_value]
if isinstance(target_ids_value, list)
else []
)
active_target_id_value = summary.get("active_target_id")
active_target_id = (
active_target_id_value if isinstance(active_target_id_value, str) else None
)

return InventoryMutationResponse(
output=(
f"Imported {summary['targets_imported']} target(s): "
+ ", ".join(summary["target_ids"])
),
active_target_id=summary["active_target_id"],
output=(f"Imported {imported_count} target(s): " + ", ".join(target_ids)),
active_target_id=active_target_id,
)

return router
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
from importlib import import_module
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
from . import deploy as deploy
from . import fleet as fleet
from . import mission as mission
from . import wifi as wifi

__all__ = ["wifi", "deploy", "mission", "fleet"]

Expand All @@ -10,7 +17,7 @@
}


def __getattr__(name: str):
def __getattr__(name: str) -> Any:
module_path = _EXPORTS.get(name)
if module_path is None:
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
Expand Down
Loading
Loading