Back to report index

Step 1 trajectorydetect ad40da4: AI3D-379 Review fixes: normalize accepts TrajectoryConfig, JSON/model defaults parity test, README deps

Miroslav Simko <ms@iolabs.ch> 2026-09-02T09:08:03+02:00

Commit #12 · 9 snippets

 README.md                                           |  4 ++--
 src/iolabs_point_cloud_trajectory_detect/_config.py | 10 ++++++++--
 tests/test_config.py                                | 21 +++++++++++++++++++++
 3 files changed, 31 insertions(+), 4 deletions(-)
Importance #1: src/iolabs_point_cloud_trajectory_detect/_config.py @@ -1,7 +1,8 @@
1from __future__ import annotations1from __future__ import annotations
22
3import logging3import logging
4from collections.abc import Mapping
4from pathlib import Path5from pathlib import Path
5from typing import Any6from typing import Any
67
7from iolabs.common import config_loader8from iolabs.common import config_loader
Importance #2: src/iolabs_point_cloud_trajectory_detect/_config.py @@ -30,20 +31,25 @@
30class TrajectoryConfigError(config_loader.ConfigError):31class TrajectoryConfigError(config_loader.ConfigError):
31 """Raised when trajectory config contains unsupported keys or values."""32 """Raised when trajectory config contains unsupported keys or values."""
3233
3334
34def normalize_trajectory_config(raw_config: dict[str, Any]) -> dict[str, Any]:35def normalize_trajectory_config(
36 raw_config: Mapping[str, Any] | TrajectoryConfig,
37) -> dict[str, Any]:
35 """Validate *raw_config* and fill field defaults.38 """Validate *raw_config* and fill field defaults.
3639
37 Args:40 Args:
38 raw_config: A raw or partial trajectory config mapping.41 raw_config: A raw or partial trajectory config mapping, or an already
42 validated :class:`TrajectoryConfig`.
3943
40 Returns:44 Returns:
41 A plain dict of the validated config.45 A plain dict of the validated config.
4246
43 Raises:47 Raises:
44 TrajectoryConfigError: Unknown keys or values that cannot be coerced.48 TrajectoryConfigError: Unknown keys or values that cannot be coerced.
45 """49 """
50 if isinstance(raw_config, TrajectoryConfig):
51 return raw_config.model_dump()
46 logger.debug("Normalizing trajectory config keys=%s", sorted(raw_config))52 logger.debug("Normalizing trajectory config keys=%s", sorted(raw_config))
47 return config_loader.validate_config(53 return config_loader.validate_config(
48 TrajectoryConfig,54 TrajectoryConfig,
49 raw_config,55 raw_config,
Importance #3: tests/test_config.py @@ -8,9 +8,11 @@
88
9sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))9sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
1010
11from iolabs.common import config_loader # noqa: E40211from iolabs.common import config_loader # noqa: E402
12from iolabs_point_cloud_trajectory_detect import _config # noqa: E402
12from iolabs_point_cloud_trajectory_detect._config import ( # noqa: E40213from iolabs_point_cloud_trajectory_detect._config import ( # noqa: E402
14 TrajectoryConfig,
13 TrajectoryConfigError,15 TrajectoryConfigError,
14 build_trajectory_config,16 build_trajectory_config,
15 load_trajectory_config,17 load_trajectory_config,
16 normalize_trajectory_config,18 normalize_trajectory_config,
Importance #4: tests/test_config.py @@ -29,8 +31,27 @@
29 "trajectory_outlier_removal_search_radius": 1.0,31 "trajectory_outlier_removal_search_radius": 1.0,
30}32}
3133
3234
35def _packaged_defaults() -> dict:
36 path = Path(_config.__file__).with_name(_config._DEFAULT_FILENAME)
37 return json.loads(path.read_text(encoding="utf-8"))
38
39
40def test_packaged_json_matches_model_field_defaults() -> None:
41 packaged = _packaged_defaults()
42 model_defaults = {
43 name: field.get_default(call_default_factory=True)
44 for name, field in TrajectoryConfig.model_fields.items()
45 }
46 assert packaged == model_defaults
47
48
49def test_normalize_accepts_a_validated_model() -> None:
50 model = TrajectoryConfig(**_EXPECTED_DEFAULTS)
51 assert normalize_trajectory_config(model) == _EXPECTED_DEFAULTS
52
53
33def test_error_class_is_config_error() -> None:54def test_error_class_is_config_error() -> None:
34 assert issubclass(TrajectoryConfigError, config_loader.ConfigError)55 assert issubclass(TrajectoryConfigError, config_loader.ConfigError)
35 assert issubclass(TrajectoryConfigError, ValueError)56 assert issubclass(TrajectoryConfigError, ValueError)
3657
Importance #5: README.md @@ -16,17 +16,17 @@
1616
17## Requirements17## Requirements
1818
19- Python ≥3.11, <3.1319- Python ≥3.11, <3.13
20- numpy, open3d, laspy, torch20- numpy, open3d, laspy, torch, pydantic ≥2.7, iolabs-common ≥0.9.0, iolabs-logstash
2121
22## Usage22## Usage
2323
24Use this package to detect and extract trajectories (e.g. scanner path or vehicle path) from LIDAR point cloud data.24Use this package to detect and extract trajectories (e.g. scanner path or vehicle path) from LIDAR point cloud data.
2525
26## Config26## Config
2727
28Defaults live in `src/iolabs_point_cloud_trajectory_detect/trajectories.default.json`. The schema is `TrajectoryConfig` in `_config.py` (a `config_loader.ConfigModel`). To add a key, add the field to the model and the matching default in the JSON; nothing else. Unknown keys are rejected.28Defaults live in `src/iolabs_point_cloud_trajectory_detect/trajectories.default.json`. The schema is `TrajectoryConfig` in `_config.py` (a `config_loader.ConfigModel`). To add a key, add the field to the model and the matching default in the JSON; nothing else. Unknown keys are rejected. `load_trajectory_config` / `build_trajectory_config` / `normalize_trajectory_config` return a plain `dict`, not the frozen model.
2929
30## Develop locally (Nexus)30## Develop locally (Nexus)
3131
32Internal `iolabs-*` dependencies resolve through the private Nexus index declared in `pyproject.toml`. Export Nexus credentials before any `uv` command that touches private deps — e.g. by sourcing `../3dai.lanefinder/scripts/nexus_credentials.sh` from your shell rc — then:32Internal `iolabs-*` dependencies resolve through the private Nexus index declared in `pyproject.toml`. Export Nexus credentials before any `uv` command that touches private deps — e.g. by sourcing `../3dai.lanefinder/scripts/nexus_credentials.sh` from your shell rc — then:
Importance #6: src/iolabs_point_cloud_trajectory_detect/_config.py @@ -1,7 +1,8 @@
1from __future__ import annotations1from __future__ import annotations
22
3import logging3import logging
4from collections.abc import Mapping
4from pathlib import Path5from pathlib import Path
5from typing import Any6from typing import Any
67
7from iolabs.common import config_loader8from iolabs.common import config_loader
Importance #7: src/iolabs_point_cloud_trajectory_detect/_config.py @@ -30,20 +31,25 @@
30class TrajectoryConfigError(config_loader.ConfigError):31class TrajectoryConfigError(config_loader.ConfigError):
31 """Raised when trajectory config contains unsupported keys or values."""32 """Raised when trajectory config contains unsupported keys or values."""
3233
3334
34def normalize_trajectory_config(raw_config: dict[str, Any]) -> dict[str, Any]:35def normalize_trajectory_config(
36 raw_config: Mapping[str, Any] | TrajectoryConfig,
37) -> dict[str, Any]:
35 """Validate *raw_config* and fill field defaults.38 """Validate *raw_config* and fill field defaults.
3639
37 Args:40 Args:
38 raw_config: A raw or partial trajectory config mapping.41 raw_config: A raw or partial trajectory config mapping, or an already
42 validated :class:`TrajectoryConfig`.
3943
40 Returns:44 Returns:
41 A plain dict of the validated config.45 A plain dict of the validated config.
4246
43 Raises:47 Raises:
44 TrajectoryConfigError: Unknown keys or values that cannot be coerced.48 TrajectoryConfigError: Unknown keys or values that cannot be coerced.
45 """49 """
50 if isinstance(raw_config, TrajectoryConfig):
51 return raw_config.model_dump()
46 logger.debug("Normalizing trajectory config keys=%s", sorted(raw_config))52 logger.debug("Normalizing trajectory config keys=%s", sorted(raw_config))
47 return config_loader.validate_config(53 return config_loader.validate_config(
48 TrajectoryConfig,54 TrajectoryConfig,
49 raw_config,55 raw_config,
Importance #8: tests/test_config.py @@ -8,9 +8,11 @@
88
9sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))9sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
1010
11from iolabs.common import config_loader # noqa: E40211from iolabs.common import config_loader # noqa: E402
12from iolabs_point_cloud_trajectory_detect import _config # noqa: E402
12from iolabs_point_cloud_trajectory_detect._config import ( # noqa: E40213from iolabs_point_cloud_trajectory_detect._config import ( # noqa: E402
14 TrajectoryConfig,
13 TrajectoryConfigError,15 TrajectoryConfigError,
14 build_trajectory_config,16 build_trajectory_config,
15 load_trajectory_config,17 load_trajectory_config,
16 normalize_trajectory_config,18 normalize_trajectory_config,
Importance #9: tests/test_config.py @@ -29,8 +31,27 @@
29 "trajectory_outlier_removal_search_radius": 1.0,31 "trajectory_outlier_removal_search_radius": 1.0,
30}32}
3133
3234
35def _packaged_defaults() -> dict:
36 path = Path(_config.__file__).with_name(_config._DEFAULT_FILENAME)
37 return json.loads(path.read_text(encoding="utf-8"))
38
39
40def test_packaged_json_matches_model_field_defaults() -> None:
41 packaged = _packaged_defaults()
42 model_defaults = {
43 name: field.get_default(call_default_factory=True)
44 for name, field in TrajectoryConfig.model_fields.items()
45 }
46 assert packaged == model_defaults
47
48
49def test_normalize_accepts_a_validated_model() -> None:
50 model = TrajectoryConfig(**_EXPECTED_DEFAULTS)
51 assert normalize_trajectory_config(model) == _EXPECTED_DEFAULTS
52
53
33def test_error_class_is_config_error() -> None:54def test_error_class_is_config_error() -> None:
34 assert issubclass(TrajectoryConfigError, config_loader.ConfigError)55 assert issubclass(TrajectoryConfigError, config_loader.ConfigError)
35 assert issubclass(TrajectoryConfigError, ValueError)56 assert issubclass(TrajectoryConfigError, ValueError)
3657