11from typing import Any , Optional , Union
22
33from model import Detector
4- from pydantic import BaseModel , ConfigDict , Field , model_serializer , model_validator
4+ from pydantic import BaseModel , ConfigDict , Field , field_validator , model_validator
55from typing_extensions import Self
6+ import yaml
67
78
89class GlobalConfig (BaseModel ):
10+ """Global runtime settings for edge-endpoint behavior."""
11+
12+ model_config = ConfigDict (extra = "forbid" )
13+
914 refresh_rate : float = Field (
1015 default = 60.0 ,
1116 description = "The interval (in seconds) at which the inference server checks for a new model binary update." ,
@@ -22,7 +27,7 @@ class InferenceConfig(BaseModel):
2227 """
2328
2429 # Keep shared presets immutable (DEFAULT/NO_CLOUD/etc.) so one mutation cannot globally change behavior.
25- model_config = ConfigDict (frozen = True )
30+ model_config = ConfigDict (extra = "forbid" , frozen = True )
2631
2732 name : str = Field (..., exclude = True , description = "A unique name for this inference config preset." )
2833 enabled : bool = Field (
@@ -71,18 +76,41 @@ class DetectorConfig(BaseModel):
7176 Configuration for a specific detector.
7277 """
7378
79+ model_config = ConfigDict (extra = "forbid" )
80+
7481 detector_id : str = Field (..., description = "Detector ID" )
7582 edge_inference_config : str = Field (..., description = "Config for edge inference." )
7683
7784
78- class DetectorsConfig (BaseModel ):
79- """
80- Detector and inference-config mappings for edge inference.
81- """
85+ class ConfigBase (BaseModel ):
86+ """Shared detector/inference configuration behavior for edge config models."""
87+
88+ model_config = ConfigDict ( extra = "forbid" )
8289
8390 edge_inference_configs : dict [str , InferenceConfig ] = Field (default_factory = dict )
8491 detectors : list [DetectorConfig ] = Field (default_factory = list )
8592
93+ @field_validator ("edge_inference_configs" , mode = "before" )
94+ @classmethod
95+ def hydrate_inference_config_names (
96+ cls , value : dict [str , InferenceConfig | dict [str , Any ]] | None
97+ ) -> dict [str , InferenceConfig | dict [str , Any ]]:
98+ """Hydrate InferenceConfig.name from payload mapping keys."""
99+ if value is None :
100+ return {}
101+ if not isinstance (value , dict ):
102+ return value
103+
104+ hydrated_configs : dict [str , InferenceConfig | dict [str , Any ]] = {}
105+ for name , config in value .items ():
106+ if isinstance (config , InferenceConfig ):
107+ hydrated_configs [name ] = config
108+ continue
109+ if not isinstance (config , dict ):
110+ raise TypeError ("Each edge inference config must be an object." )
111+ hydrated_configs [name ] = {"name" : name , ** config }
112+ return hydrated_configs
113+
86114 @model_validator (mode = "after" )
87115 def validate_inference_configs (self ):
88116 """
@@ -128,7 +156,7 @@ def add_detector(self, detector: Union[str, Detector], edge_inference_config: In
128156 self .detectors .append (DetectorConfig (detector_id = detector_id , edge_inference_config = edge_inference_config .name ))
129157
130158 def to_payload (self ) -> dict [str , Any ]:
131- """Return flattened detector payload used by edge-endpoint config HTTP APIs."""
159+ """Return detector payload used by edge-endpoint config HTTP APIs."""
132160 return {
133161 "edge_inference_configs" : {
134162 name : config .model_dump () for name , config in self .edge_inference_configs .items ()
@@ -137,36 +165,54 @@ def to_payload(self) -> dict[str, Any]:
137165 }
138166
139167
140- class EdgeEndpointConfig (BaseModel ):
168+ class DetectorsConfig (ConfigBase ):
169+ """
170+ Detector and inference-config mappings for edge inference.
171+ """
172+
173+
174+ class EdgeEndpointConfig (ConfigBase ):
141175 """
142176 Top-level edge endpoint configuration.
143177 """
144178
145179 global_config : GlobalConfig = Field (default_factory = GlobalConfig )
146- detectors_config : DetectorsConfig = Field (default_factory = DetectorsConfig )
147-
148- @property
149- def edge_inference_configs (self ) -> dict [str , InferenceConfig ]:
150- """Convenience accessor for detector inference config map."""
151- return self .detectors_config .edge_inference_configs
152180
153- @property
154- def detectors (self ) -> list [DetectorConfig ]:
155- """Convenience accessor for detector assignments."""
156- return self .detectors_config .detectors
181+ @classmethod
182+ def from_yaml (
183+ cls ,
184+ filename : Optional [str ] = None ,
185+ yaml_str : Optional [str ] = None ,
186+ ) -> "EdgeEndpointConfig" :
187+ """Create an EdgeEndpointConfig from a YAML filename or YAML string."""
188+ if filename is None and yaml_str is None :
189+ raise ValueError ("Either filename or yaml_str must be provided." )
190+ if filename is not None and yaml_str is not None :
191+ raise ValueError ("Only one of filename or yaml_str can be provided." )
192+ if filename is not None :
193+ if not filename .strip ():
194+ raise ValueError ("filename must be a non-empty path when provided." )
195+ with open (filename , "r" ) as f :
196+ yaml_str = f .read ()
197+
198+ yaml_text = yaml_str or ""
199+ parsed = yaml .safe_load (yaml_text ) or {}
200+ return cls .model_validate (parsed )
157201
158- @model_serializer (mode = "plain" )
159- def serialize (self ):
160- """Serialize to the flattened shape expected by edge-endpoint configs."""
202+ def to_payload (self ) -> dict [str , Any ]:
203+ """Return the full edge-endpoint payload shape."""
161204 return {
162205 "global_config" : self .global_config .model_dump (),
163- ** self .detectors_config .to_payload (),
206+ "edge_inference_configs" : {
207+ name : config .model_dump () for name , config in self .edge_inference_configs .items ()
208+ },
209+ "detectors" : [detector .model_dump () for detector in self .detectors ],
164210 }
165211
166- def add_detector ( self , detector : Union [ str , Detector ], edge_inference_config : InferenceConfig ) -> None :
167- """Add a detector with the given inference config. Accepts detector ID or Detector object."""
168- self . detectors_config . add_detector ( detector , edge_inference_config )
169-
212+ @ classmethod
213+ def from_payload ( cls , payload : dict [ str , Any ]) -> "EdgeEndpointConfig" :
214+ """Construct an EdgeEndpointConfig from a payload dictionary."""
215+ return cls . model_validate ( payload )
170216
171217# Preset inference configs matching the standard edge-endpoint defaults.
172218DEFAULT = InferenceConfig (name = "default" )
0 commit comments