1616from pioreactor import exc
1717from pioreactor import types as pt
1818from pioreactor .logging import create_logger
19+ from pioreactor .utils .files import is_valid_unix_filename
1920
2021
2122T = t .TypeVar ("T" )
@@ -73,6 +74,18 @@ def type(self) -> str:
7374type CalibrationCurveData = PolyFitCoefficients | SplineFitData | AkimaFitData
7475
7576
77+ def artifact_path_component (value : str , field : str ) -> str :
78+ """
79+ Calibration and estimator identities are filesystem path components.
80+
81+ Keep this invariant at the artifact layer so CLI, API, and Huey writers
82+ cannot construct paths from malformed names.
83+ """
84+ if not is_valid_unix_filename (value ):
85+ raise ValueError (f"{ field } must be a valid filename component." )
86+ return value
87+
88+
7689class AutomationSettings (JSONPrintedStruct ):
7790 """
7891 Metadata produced when settings in an automation job change
@@ -226,8 +239,10 @@ def calibration_type(self) -> str:
226239 def path_on_disk_for_device (self , device : str ) -> Path :
227240 from pioreactor .calibrations import CALIBRATION_PATH
228241
229- calibration_dir = CALIBRATION_PATH / device
230- out_file = calibration_dir / f"{ self .calibration_name } .yaml"
242+ calibration_dir = CALIBRATION_PATH / artifact_path_component (device , "device" )
243+ out_file = (
244+ calibration_dir / f"{ artifact_path_component (self .calibration_name , 'calibration_name' )} .yaml"
245+ )
231246 return out_file
232247
233248 def save_to_disk_for_device (self , device : str ) -> str :
@@ -248,29 +263,37 @@ def set_as_active_calibration_for_device(self, device: str) -> None:
248263 from pioreactor .utils import local_persistent_storage
249264
250265 logger = create_logger ("calibrations" , experiment = "$experiment" )
266+ device = artifact_path_component (device , "device" )
267+ calibration_name = artifact_path_component (self .calibration_name , "calibration_name" )
251268
252269 if not self .exists_on_disk_for_device (device ):
253270 self .save_to_disk_for_device (device )
254271
255272 with local_persistent_storage ("active_calibrations" ) as c :
256- c [device ] = self . calibration_name
273+ c [device ] = calibration_name
257274
258275 logger .info (f"Set { self .calibration_name } as active calibration for { device } " )
259276
260277 def remove_as_active_calibration_for_device (self , device : str ) -> None :
261278 from pioreactor .utils import local_persistent_storage
262279
263280 logger = create_logger ("calibrations" , experiment = "$experiment" )
281+ device = artifact_path_component (device , "device" )
282+ calibration_name = artifact_path_component (self .calibration_name , "calibration_name" )
264283
265284 with local_persistent_storage ("active_calibrations" ) as c :
266- if c .get (device ) == self . calibration_name :
285+ if c .get (device ) == calibration_name :
267286 del c [device ]
268287 logger .info (f"Removed { self .calibration_name } as active calibration for { device } " )
269288
270289 def exists_on_disk_for_device (self , device : str ) -> bool :
271290 from pioreactor .calibrations import CALIBRATION_PATH
272291
273- target_file = CALIBRATION_PATH / device / f"{ self .calibration_name } .yaml"
292+ target_file = (
293+ CALIBRATION_PATH
294+ / artifact_path_component (device , "device" )
295+ / f"{ artifact_path_component (self .calibration_name , 'calibration_name' )} .yaml"
296+ )
274297
275298 return target_file .is_file ()
276299
@@ -377,7 +400,9 @@ def is_active(self, device: str) -> bool:
377400 from pioreactor .utils import local_persistent_storage
378401
379402 with local_persistent_storage ("active_calibrations" ) as c :
380- return c .get (device ) == self .calibration_name
403+ return c .get (artifact_path_component (device , "device" )) == artifact_path_component (
404+ self .calibration_name , "calibration_name"
405+ )
381406
382407
383408class EstimatorBase (Struct , tag_field = "estimator_type" , kw_only = True ):
@@ -392,8 +417,8 @@ def estimator_type(self) -> str:
392417 def path_on_disk_for_device (self , device : str ) -> Path :
393418 from pioreactor .estimators import ESTIMATOR_PATH
394419
395- estimator_dir = ESTIMATOR_PATH / device
396- out_file = estimator_dir / f"{ self .estimator_name } .yaml"
420+ estimator_dir = ESTIMATOR_PATH / artifact_path_component ( device , "device" )
421+ out_file = estimator_dir / f"{ artifact_path_component ( self .estimator_name , 'estimator_name' ) } .yaml"
397422 return out_file
398423
399424 def save_to_disk_for_device (self , device : str ) -> str :
@@ -414,37 +439,47 @@ def set_as_active_calibration_for_device(self, device: str) -> None:
414439 from pioreactor .utils import local_persistent_storage
415440
416441 logger = create_logger ("estimators" , experiment = "$experiment" )
442+ device = artifact_path_component (device , "device" )
443+ estimator_name = artifact_path_component (self .estimator_name , "estimator_name" )
417444
418445 if not self .exists_on_disk_for_device (device ):
419446 self .save_to_disk_for_device (device )
420447
421448 with local_persistent_storage ("active_estimators" ) as c :
422- c [device ] = self . estimator_name
449+ c [device ] = estimator_name
423450
424451 logger .info (f"Set { self .estimator_name } as active estimator for { device } " )
425452
426453 def remove_as_active_calibration_for_device (self , device : str ) -> None :
427454 from pioreactor .utils import local_persistent_storage
428455
429456 logger = create_logger ("estimators" , experiment = "$experiment" )
457+ device = artifact_path_component (device , "device" )
458+ estimator_name = artifact_path_component (self .estimator_name , "estimator_name" )
430459
431460 with local_persistent_storage ("active_estimators" ) as c :
432- if c .get (device ) == self . estimator_name :
461+ if c .get (device ) == estimator_name :
433462 del c [device ]
434463 logger .info (f"Removed { self .estimator_name } as active estimator for { device } " )
435464
436465 def exists_on_disk_for_device (self , device : str ) -> bool :
437466 from pioreactor .estimators import ESTIMATOR_PATH
438467
439- target_file = ESTIMATOR_PATH / device / f"{ self .estimator_name } .yaml"
468+ target_file = (
469+ ESTIMATOR_PATH
470+ / artifact_path_component (device , "device" )
471+ / f"{ artifact_path_component (self .estimator_name , 'estimator_name' )} .yaml"
472+ )
440473
441474 return target_file .is_file ()
442475
443476 def is_active (self , device : str ) -> bool :
444477 from pioreactor .utils import local_persistent_storage
445478
446479 with local_persistent_storage ("active_estimators" ) as c :
447- return c .get (device ) == self .estimator_name
480+ return c .get (artifact_path_component (device , "device" )) == artifact_path_component (
481+ self .estimator_name , "estimator_name"
482+ )
448483
449484
450485class ODCalibration (CalibrationBase , kw_only = True , tag = "od" ):
0 commit comments