Skip to content

Commit e96ea12

Browse files
committed
adding pydantic validation
1 parent bce92fe commit e96ea12

File tree

2 files changed

+45
-40
lines changed

2 files changed

+45
-40
lines changed

src/groundlight/edge/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ class DetectorConfig(BaseModel): # pylint: disable=too-few-public-methods
7878

7979
model_config = ConfigDict(extra="ignore")
8080

81-
detector_id: str = Field(..., description="Detector ID")
81+
detector_id: str = Field(..., pattern=r"^det_[A-Za-z0-9]{27}$", description="Detector ID")
8282
edge_inference_config: str = Field(..., description="Config for edge inference.")
8383

8484

test/unit/test_edge_config.py

Lines changed: 44 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@
1717
CUSTOM_AUDIT_RATE = 0.0
1818
REFRESH_RATE_SECONDS = 15.0
1919

20+
# Mock detector IDs
21+
DET_1 = "det_000000000000000000000000001"
22+
DET_2 = "det_000000000000000000000000002"
23+
DET_3 = "det_000000000000000000000000003"
24+
2025

2126
def _make_detector(detector_id: str) -> Detector:
2227
return Detector(
@@ -36,15 +41,15 @@ def test_add_detector_allows_equivalent_named_inference_config():
3641
"""Allows reusing the same named inference config with equivalent values."""
3742
detectors_config = DetectorsConfig()
3843
detectors_config.add_detector(
39-
"det_1",
44+
DET_1,
4045
InferenceConfig(
4146
name="custom_config",
4247
always_return_edge_prediction=True,
4348
min_time_between_escalations=0.5,
4449
),
4550
)
4651
detectors_config.add_detector(
47-
"det_2",
52+
DET_2,
4853
InferenceConfig(
4954
name="custom_config",
5055
always_return_edge_prediction=True,
@@ -59,22 +64,22 @@ def test_add_detector_allows_equivalent_named_inference_config():
5964
def test_add_detector_rejects_different_named_inference_config():
6065
"""Rejects conflicting inference config values under the same name."""
6166
detectors_config = DetectorsConfig()
62-
detectors_config.add_detector("det_1", InferenceConfig(name="custom_config"))
67+
detectors_config.add_detector(DET_1, InferenceConfig(name="custom_config"))
6368

6469
with pytest.raises(ValueError, match="different inference config named 'custom_config'"):
6570
detectors_config.add_detector(
66-
"det_2",
71+
DET_2,
6772
InferenceConfig(name="custom_config", always_return_edge_prediction=True),
6873
)
6974

7075

7176
def test_add_detector_rejects_duplicate_detector_id():
7277
"""Rejects adding the same detector ID more than once."""
7378
detectors_config = DetectorsConfig()
74-
detectors_config.add_detector("det_1", DEFAULT)
79+
detectors_config.add_detector(DET_1, DEFAULT)
7580

7681
with pytest.raises(ValueError, match="already exists"):
77-
detectors_config.add_detector("det_1", DEFAULT)
82+
detectors_config.add_detector(DET_1, DEFAULT)
7883

7984

8085
def test_constructor_rejects_duplicate_detector_ids():
@@ -83,8 +88,8 @@ def test_constructor_rejects_duplicate_detector_ids():
8388
DetectorsConfig(
8489
edge_inference_configs={"default": DEFAULT},
8590
detectors=[
86-
{"detector_id": "det_1", "edge_inference_config": "default"},
87-
{"detector_id": "det_1", "edge_inference_config": "default"},
91+
{"detector_id": DET_1, "edge_inference_config": "default"},
92+
{"detector_id": DET_1, "edge_inference_config": "default"},
8893
],
8994
)
9095

@@ -102,18 +107,18 @@ def test_constructor_accepts_matching_inference_config_key_and_name():
102107
"""Accepts constructor input when key/name pairs are consistent."""
103108
config = DetectorsConfig(
104109
edge_inference_configs={"default": InferenceConfig(name="default")},
105-
detectors=[{"detector_id": "det_1", "edge_inference_config": "default"}],
110+
detectors=[{"detector_id": DET_1, "edge_inference_config": "default"}],
106111
)
107112

108113
assert list(config.edge_inference_configs.keys()) == ["default"]
109-
assert [detector.detector_id for detector in config.detectors] == ["det_1"]
114+
assert [detector.detector_id for detector in config.detectors] == [DET_1]
110115

111116

112117
def test_constructor_hydrates_inference_config_name_from_dict_key():
113118
"""Hydrates inference config names from payload dict keys."""
114119
config = DetectorsConfig(
115120
edge_inference_configs={"default": {"enabled": True}},
116-
detectors=[{"detector_id": "det_1", "edge_inference_config": "default"}],
121+
detectors=[{"detector_id": DET_1, "edge_inference_config": "default"}],
117122
)
118123

119124
assert config.edge_inference_configs["default"].name == "default"
@@ -124,7 +129,7 @@ def test_constructor_rejects_detector_map_input():
124129
with pytest.raises(ValueError):
125130
DetectorsConfig(
126131
edge_inference_configs={"default": {"enabled": True}},
127-
detectors={"det_1": {"detector_id": "det_1", "edge_inference_config": "default"}},
132+
detectors={DET_1: {"detector_id": DET_1, "edge_inference_config": "default"}},
128133
)
129134

130135

@@ -133,33 +138,33 @@ def test_constructor_rejects_undefined_inference_config_reference():
133138
with pytest.raises(ValueError, match="not defined"):
134139
DetectorsConfig(
135140
edge_inference_configs={},
136-
detectors=[{"detector_id": "det_1", "edge_inference_config": "does_not_exist"}],
141+
detectors=[{"detector_id": DET_1, "edge_inference_config": "does_not_exist"}],
137142
)
138143

139144

140145
def test_edge_endpoint_config_add_detector_uses_shared_config_logic():
141146
"""Adds detectors via EdgeEndpointConfig and preserves inferred config mapping."""
142147
config = EdgeEndpointConfig()
143-
config.add_detector("det_1", NO_CLOUD)
144-
config.add_detector("det_2", EDGE_ANSWERS_WITH_ESCALATION)
145-
config.add_detector("det_3", DEFAULT)
148+
config.add_detector(DET_1, NO_CLOUD)
149+
config.add_detector(DET_2, EDGE_ANSWERS_WITH_ESCALATION)
150+
config.add_detector(DET_3, DEFAULT)
146151

147-
assert [detector.detector_id for detector in config.detectors] == ["det_1", "det_2", "det_3"]
152+
assert [detector.detector_id for detector in config.detectors] == [DET_1, DET_2, DET_3]
148153
assert set(config.edge_inference_configs.keys()) == {"no_cloud", "edge_answers_with_escalation", "default"}
149154

150155

151156
def test_add_detector_accepts_detector_object():
152157
"""Accepts Detector objects in add_detector."""
153158
config = EdgeEndpointConfig()
154-
config.add_detector(_make_detector("det_1"), DEFAULT)
159+
config.add_detector(_make_detector(DET_1), DEFAULT)
155160

156-
assert [detector.detector_id for detector in config.detectors] == ["det_1"]
161+
assert [detector.detector_id for detector in config.detectors] == [DET_1]
157162

158163

159164
def test_disabled_preset_can_be_used():
160165
"""Allows assigning the DISABLED inference preset to a detector."""
161166
config = EdgeEndpointConfig()
162-
config.add_detector("det_1", DISABLED)
167+
config.add_detector(DET_1, DISABLED)
163168

164169
assert [detector.edge_inference_config for detector in config.detectors] == ["disabled"]
165170
assert config.edge_inference_configs["disabled"] == DISABLED
@@ -168,8 +173,8 @@ def test_disabled_preset_can_be_used():
168173
def test_detectors_config_to_payload_shape():
169174
"""Serializes detector-scoped payload with expected top-level keys."""
170175
detectors_config = DetectorsConfig()
171-
detectors_config.add_detector("det_1", DEFAULT)
172-
detectors_config.add_detector("det_2", NO_CLOUD)
176+
detectors_config.add_detector(DET_1, DEFAULT)
177+
detectors_config.add_detector(DET_2, NO_CLOUD)
173178

174179
payload = detectors_config.to_payload()
175180

@@ -182,11 +187,11 @@ def test_edge_endpoint_config_accepts_top_level_payload_shape():
182187
config = EdgeEndpointConfig.model_validate({
183188
"global_config": {"refresh_rate": CUSTOM_REFRESH_RATE},
184189
"edge_inference_configs": {"default": {"enabled": True}},
185-
"detectors": [{"detector_id": "det_1", "edge_inference_config": "default"}],
190+
"detectors": [{"detector_id": DET_1, "edge_inference_config": "default"}],
186191
})
187192

188193
assert config.global_config.refresh_rate == CUSTOM_REFRESH_RATE
189-
assert [detector.detector_id for detector in config.detectors] == ["det_1"]
194+
assert [detector.detector_id for detector in config.detectors] == [DET_1]
190195

191196

192197
def test_edge_endpoint_config_from_yaml_accepts_yaml_text():
@@ -198,12 +203,12 @@ def test_edge_endpoint_config_from_yaml_accepts_yaml_text():
198203
default:
199204
enabled: true
200205
detectors:
201-
- detector_id: det_1
206+
- detector_id: {DET_1}
202207
edge_inference_config: default
203208
""")
204209

205210
assert config.global_config.refresh_rate == REFRESH_RATE_SECONDS
206-
assert [detector.detector_id for detector in config.detectors] == ["det_1"]
211+
assert [detector.detector_id for detector in config.detectors] == [DET_1]
207212

208213

209214
def test_edge_endpoint_config_from_yaml_accepts_filename(tmp_path):
@@ -215,12 +220,12 @@ def test_edge_endpoint_config_from_yaml_accepts_filename(tmp_path):
215220
" default:\n"
216221
" enabled: true\n"
217222
"detectors:\n"
218-
" - detector_id: det_1\n"
223+
f" - detector_id: {DET_1}\n"
219224
" edge_inference_config: default\n"
220225
)
221226
config = EdgeEndpointConfig.from_yaml(filename=str(config_file))
222227

223-
assert [detector.detector_id for detector in config.detectors] == ["det_1"]
228+
assert [detector.detector_id for detector in config.detectors] == [DET_1]
224229

225230

226231
def test_edge_endpoint_config_from_yaml_requires_exactly_one_input():
@@ -243,23 +248,23 @@ def test_edge_endpoint_config_ignores_extra_fields_at_all_levels():
243248
"default": {"enabled": True, "unknown_inference_field": 42},
244249
},
245250
"detectors": [
246-
{"detector_id": "det_1", "edge_inference_config": "default", "unknown_detector_field": [1, 2]},
251+
{"detector_id": DET_1, "edge_inference_config": "default", "unknown_detector_field": [1, 2]},
247252
],
248253
"unknown_top_level_field": True,
249254
})
250255
assert config.global_config.refresh_rate == REFRESH_RATE_SECONDS
251256
assert config.edge_inference_configs["default"].enabled is True
252-
assert config.detectors[0].detector_id == "det_1"
257+
assert config.detectors[0].detector_id == DET_1
253258

254259

255260
def test_model_dump_shape_for_edge_endpoint_config():
256261
"""Serializes full edge endpoint config in wire payload shape."""
257262
config = EdgeEndpointConfig(
258263
global_config=GlobalConfig(refresh_rate=CUSTOM_REFRESH_RATE, confident_audit_rate=CUSTOM_AUDIT_RATE)
259264
)
260-
config.add_detector("det_1", DEFAULT)
261-
config.add_detector("det_2", EDGE_ANSWERS_WITH_ESCALATION)
262-
config.add_detector("det_3", NO_CLOUD)
265+
config.add_detector(DET_1, DEFAULT)
266+
config.add_detector(DET_2, EDGE_ANSWERS_WITH_ESCALATION)
267+
config.add_detector(DET_3, NO_CLOUD)
263268

264269
payload = config.to_payload()
265270

@@ -272,8 +277,8 @@ def test_model_dump_shape_for_edge_endpoint_config():
272277
def test_edge_endpoint_config_from_payload_round_trip():
273278
"""Round-trips edge endpoint config through payload helpers."""
274279
config = EdgeEndpointConfig()
275-
config.add_detector("det_1", DEFAULT)
276-
config.add_detector("det_2", NO_CLOUD)
280+
config.add_detector(DET_1, DEFAULT)
281+
config.add_detector(DET_2, NO_CLOUD)
277282

278283
payload = config.to_payload()
279284
reconstructed = EdgeEndpointConfig.from_payload(payload)
@@ -286,14 +291,14 @@ def test_edge_endpoint_config_from_payload_accepts_literal_payload():
286291
payload = {
287292
"global_config": {"refresh_rate": REFRESH_RATE_SECONDS},
288293
"edge_inference_configs": {"default": {"enabled": True}},
289-
"detectors": [{"detector_id": "det_1", "edge_inference_config": "default"}],
294+
"detectors": [{"detector_id": DET_1, "edge_inference_config": "default"}],
290295
}
291296

292297
config = EdgeEndpointConfig.from_payload(payload)
293298

294299
assert config.global_config.refresh_rate == REFRESH_RATE_SECONDS
295300
assert config.edge_inference_configs["default"].name == "default"
296-
assert [detector.detector_id for detector in config.detectors] == ["det_1"]
301+
assert [detector.detector_id for detector in config.detectors] == [DET_1]
297302

298303

299304
def test_inference_config_validation_errors():
@@ -318,7 +323,7 @@ def test_edge_get_config_parses_response():
318323
payload = {
319324
"global_config": {"refresh_rate": REFRESH_RATE_SECONDS},
320325
"edge_inference_configs": {"default": {"enabled": True}},
321-
"detectors": [{"detector_id": "det_1", "edge_inference_config": "default"}],
326+
"detectors": [{"detector_id": DET_1, "edge_inference_config": "default"}],
322327
}
323328

324329
mock_response = Mock()
@@ -333,4 +338,4 @@ def test_edge_get_config_parses_response():
333338
assert isinstance(config, EdgeEndpointConfig)
334339
assert config.global_config.refresh_rate == REFRESH_RATE_SECONDS
335340
assert config.edge_inference_configs["default"].name == "default"
336-
assert [d.detector_id for d in config.detectors] == ["det_1"]
341+
assert [d.detector_id for d in config.detectors] == [DET_1]

0 commit comments

Comments
 (0)