improved logging
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from python_app.models.run_config_schema import (
|
||||
@@ -13,6 +14,8 @@ from python_app.models.run_config_schema import (
|
||||
SwitchModel,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Wire-format bounds shared with the C++ pipeline. The ring header stores the
|
||||
# slot size as a uint32, and capacity * slot_size must address into a single
|
||||
# shared-memory mapping, so reject values the C++ side cannot represent.
|
||||
@@ -26,9 +29,10 @@ _MAX_COMBOS = 4096
|
||||
def _require_int(payload: dict[str, Any], key: str, default: int) -> int:
|
||||
"""Read a strict JSON integer, treating an explicit ``null`` as 'use default'.
|
||||
|
||||
Accept only a genuine JSON integer (not bool, not float, not numeric string):
|
||||
silently truncating ``5.7`` or parsing ``"5"`` would hide a malformed config.
|
||||
Mirrors ``run_config_codec._read_int`` so every config integer reads identically.
|
||||
Accepts only a genuine JSON integer (not bool, not float, not numeric string),
|
||||
because silently truncating ``5.7`` or parsing ``"5"`` would hide a malformed
|
||||
config. Mirrors ``run_config_codec._read_int`` so every config integer reads
|
||||
identically.
|
||||
"""
|
||||
value = payload.get(key, default)
|
||||
if value is None: # explicit JSON null -> use the default, never coerce
|
||||
@@ -63,7 +67,7 @@ def load_switch_payload(
|
||||
target: SwitchModel,
|
||||
) -> None:
|
||||
"""Populate switch model from payload preserving defaults for missing values."""
|
||||
# #53: scalar reads reject array/object JSON types as ValueError (not TypeError).
|
||||
# Scalar reads reject array/object JSON types as ValueError (not TypeError).
|
||||
target.name = _require_str(payload, "name", target.name)
|
||||
target.driver_mode = _require_str(payload, "driver_mode", target.driver_mode)
|
||||
target.driver = _require_str(payload, "driver", target.driver)
|
||||
@@ -81,7 +85,7 @@ def load_control_button_payload(
|
||||
target: ControlButtonModel,
|
||||
) -> None:
|
||||
"""Populate control-button model from payload preserving defaults."""
|
||||
# #53: scalar reads reject array/object JSON types as ValueError (not TypeError).
|
||||
# Scalar reads reject array/object JSON types as ValueError (not TypeError).
|
||||
target.enabled = _require_bool(payload, "enabled", target.enabled)
|
||||
target.gpio_chip = _require_str(payload, "gpio_chip", target.gpio_chip)
|
||||
target.pin = _require_int(payload, "pin", target.pin)
|
||||
@@ -92,38 +96,50 @@ def load_control_button_payload(
|
||||
|
||||
|
||||
def load_ring_payload(payload: dict[str, Any], target: RingEndpointModel) -> None:
|
||||
"""Populate ring endpoint model from payload preserving defaults."""
|
||||
# #53: scalar reads reject array/object JSON types as ValueError (not TypeError).
|
||||
"""Populate ring endpoint model from payload preserving defaults.
|
||||
|
||||
Validates the resulting ring sizing before returning, so a bad config fails
|
||||
here (on GUI save and at config load) instead of crashing the C++ ring
|
||||
allocator at boot.
|
||||
"""
|
||||
# Scalar reads reject array/object JSON types as ValueError (not TypeError).
|
||||
target.name = _require_str(payload, "name", target.name)
|
||||
target.capacity = _require_int(payload, "capacity", target.capacity)
|
||||
target.slot_size_bytes = _require_int(payload, "slot_size_bytes", target.slot_size_bytes)
|
||||
# #36: enforce ring sizing in Python so a bad config fails here (in GUI/save and
|
||||
# at config load) instead of crashing the C++ ring allocator at boot.
|
||||
validate_ring_endpoint(target)
|
||||
|
||||
|
||||
def validate_ring_endpoint(ring: RingEndpointModel) -> None:
|
||||
"""Validate ring sizing against the constraints the C++ allocator requires."""
|
||||
field = ring.name or "ring"
|
||||
"""Validate ring sizing against the constraints the C++ allocator requires.
|
||||
|
||||
Raises ValueError naming the offending ring when capacity or slot size is
|
||||
non-positive, the slot size overflows the uint32 wire field, or the segment
|
||||
would exceed the maximum single mapping.
|
||||
"""
|
||||
ring_name = ring.name or "ring"
|
||||
if ring.capacity <= 0:
|
||||
raise ValueError(f"rings.{field}.capacity must be > 0")
|
||||
raise ValueError(f"rings.{ring_name}.capacity must be > 0")
|
||||
if ring.slot_size_bytes <= 0:
|
||||
raise ValueError(f"rings.{field}.slot_size_bytes must be > 0")
|
||||
raise ValueError(f"rings.{ring_name}.slot_size_bytes must be > 0")
|
||||
if ring.slot_size_bytes > _UINT32_MAX:
|
||||
raise ValueError(f"rings.{field}.slot_size_bytes exceeds the uint32 wire limit")
|
||||
raise ValueError(f"rings.{ring_name}.slot_size_bytes exceeds the uint32 wire limit")
|
||||
# Overflow-safe: compare against the ceiling without ever forming the full
|
||||
# product, so an attacker-sized capacity cannot wrap a fixed-width index.
|
||||
# product, so an oversized capacity cannot wrap a fixed-width index.
|
||||
if ring.capacity > _RING_SEGMENT_MAX_BYTES // ring.slot_size_bytes:
|
||||
raise ValueError(
|
||||
f"rings.{field} capacity * slot_size_bytes exceeds the maximum ring segment size"
|
||||
f"rings.{ring_name} capacity * slot_size_bytes exceeds the maximum ring segment size"
|
||||
)
|
||||
|
||||
|
||||
def validate_sweep_model(sweep: RadarSweepModel) -> None:
|
||||
"""Validate radar sweep bounds in Python so a bad sweep fails in the GUI/save
|
||||
and at config load rather than aborting the C++ acquisition process at boot.
|
||||
"""Validate radar sweep bounds (point count and frequency span).
|
||||
|
||||
Runs in Python so a bad sweep fails on GUI save and at config load rather
|
||||
than aborting the C++ acquisition process at boot. Raises ValueError when
|
||||
``points`` is non-integral or non-positive, or when ``stop_hz`` does not
|
||||
exceed ``start_hz``.
|
||||
"""
|
||||
# #36: points must be a positive, integral count of frequency samples.
|
||||
# points must be a positive, integral count of frequency samples.
|
||||
points = sweep.points
|
||||
if isinstance(points, bool) or not isinstance(points, int):
|
||||
raise ValueError("radar.sweep.points must be an integer")
|
||||
@@ -142,9 +158,11 @@ def validate_gpr_model(
|
||||
) -> None:
|
||||
"""Validate stable GPR config against current switch dimensions.
|
||||
|
||||
When ``sweep`` is supplied (load and GUI/save paths share this chokepoint),
|
||||
its bounds are validated here too so #36 sweep failures surface alongside the
|
||||
GPR checks instead of as a C++ boot crash.
|
||||
Checks that the relative permittivity is positive and that every tx/rx
|
||||
geometry entry indexes a valid, non-duplicate switch position. When ``sweep``
|
||||
is supplied (the load and GUI/save paths share this chokepoint), its bounds
|
||||
are validated here too so sweep failures surface alongside the GPR checks
|
||||
instead of as a C++ boot crash.
|
||||
"""
|
||||
if sweep is not None:
|
||||
validate_sweep_model(sweep)
|
||||
@@ -200,7 +218,12 @@ def validate_combos(
|
||||
|
||||
|
||||
def parse_combos_from_text(text: str) -> list[ComboModel]:
|
||||
"""Parse UI combos string in `input:output,input:output` format."""
|
||||
"""Parse a UI combos string in ``input:output,input:output`` format.
|
||||
|
||||
Returns an empty list for blank input. Raises ValueError (naming the
|
||||
offending pair) on malformed syntax, empty sides, non-integer values, more
|
||||
than ``_MAX_COMBOS`` entries, or a non-blank string that yields no combos.
|
||||
"""
|
||||
cleaned = text.strip()
|
||||
if not cleaned:
|
||||
return []
|
||||
@@ -213,13 +236,13 @@ def parse_combos_from_text(text: str) -> list[ComboModel]:
|
||||
if ":" not in pair:
|
||||
raise ValueError(f"Invalid combo syntax: {pair!r}. Expected input:output")
|
||||
|
||||
# #57: cap the combo count so a pathological string cannot expand into a
|
||||
# list large enough to stall the GUI or the acquisition loop.
|
||||
# Cap the combo count so a pathological string cannot expand into a list
|
||||
# large enough to stall the GUI or the acquisition loop.
|
||||
if len(combos) >= _MAX_COMBOS:
|
||||
raise ValueError(f"Too many combos: limit is {_MAX_COMBOS}")
|
||||
|
||||
input_text, output_text = (side.strip() for side in pair.split(":", 1))
|
||||
# #57: reject empty sides and re-raise non-integer values naming the pair/side.
|
||||
# Reject empty sides and re-raise non-integer values naming the pair/side.
|
||||
if not input_text:
|
||||
raise ValueError(f"Invalid combo {pair!r}: input side is empty")
|
||||
if not output_text:
|
||||
@@ -236,4 +259,5 @@ def parse_combos_from_text(text: str) -> list[ComboModel]:
|
||||
|
||||
if not combos:
|
||||
raise ValueError("No valid combos were provided")
|
||||
logger.debug("Parsed %d combo(s) from UI text", len(combos))
|
||||
return combos
|
||||
|
||||
Reference in New Issue
Block a user