require_schema(config, schema) is a generic recursive declarative
checker (nested-dict schema, leaves name required type(s), each
intermediate object node gets its own dict-type check so a wrong-typed
section is named directly instead of surfacing as a confusing "missing
child" error). validate_temp_ctrl() is now just require_schema(config,
TEMPCTRL_SCHEMA) against a declarative schema dict, replacing the
hand-rolled nested loops.
utils/sud_validate.py's static structural check ('steps' must be a
list) now goes through the same require_schema()/SUD_SCHEMA - one
shared mechanism for both server config.json and sud.json documents.
The genuinely dynamic per-step check (ramp.rate required only when a
step sets its own temperature) stays hand-written, since it depends on
the document's content, not just its shape.
Added TestRequireSchema, exercising the generic engine directly
against a schema unrelated to TempCtrl.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F4oEE99rkKVU8L8tkzXWdG
74 lines
3.2 KiB
Python
74 lines
3.2 KiB
Python
class ConfigError(Exception):
|
|
"""Raised for a missing or wrong-typed config key, named by its full
|
|
dotted path (e.g. "TempCtrl.Inner.Hold.kp") so the error points straight
|
|
at the offending line in config.json instead of surfacing as a bare
|
|
KeyError/TypeError from wherever the value first gets used."""
|
|
|
|
|
|
def require(config, path, type=None):
|
|
"""Generic dotted-path lookup against any nested-dict document - not
|
|
tied to server config.json's own shape, so this is exactly as usable
|
|
for a sud.json schedule (see utils/sud_validate.py) as for TempCtrl
|
|
below."""
|
|
node = config
|
|
parts = path.split('.')
|
|
for i, key in enumerate(parts):
|
|
if not isinstance(node, dict) or key not in node:
|
|
raise ConfigError("missing required config key: {}".format('.'.join(parts[:i + 1])))
|
|
node = node[key]
|
|
if type is not None and not isinstance(node, type):
|
|
type_name = ' or '.join(t.__name__ for t in type) if isinstance(type, tuple) else type.__name__
|
|
raise ConfigError("config key {} must be {}, got {!r}".format(path, type_name, node))
|
|
return node
|
|
|
|
|
|
def require_schema(config, schema, _prefix=''):
|
|
"""Generic declarative counterpart to require(): schema mirrors the
|
|
document's own shape, with every leaf naming the required type(s) (see
|
|
TEMPCTRL_SCHEMA below for an example) - one shared mechanism for any
|
|
config-shaped document with a fixed structure, whether that's server
|
|
config.json's TempCtrl section or (for the parts of it that are static -
|
|
see utils/sud_validate.py for the parts that aren't, e.g. per-step
|
|
conditional requirements) a sud.json schedule.
|
|
|
|
A schema value that's itself a dict describes a required sub-object:
|
|
checked (and reported, on failure, as its own dotted path) before
|
|
recursing into it, so a wrong-typed intermediate node (e.g. 'TempCtrl'
|
|
itself not being an object) is named directly rather than surfacing as
|
|
a confusing "missing" error on one of its children instead."""
|
|
for key, spec in schema.items():
|
|
path = '{}.{}'.format(_prefix, key) if _prefix else key
|
|
if isinstance(spec, dict):
|
|
require(config, path, type=dict)
|
|
require_schema(config, spec, path)
|
|
else:
|
|
require(config, path, type=spec)
|
|
|
|
|
|
_GAIN_SCHEMA = {'kp': (int, float), 'ki': (int, float), 'kd': (int, float), 'kt': (int, float)}
|
|
|
|
# Only the gains every pid_type actually reads unconditionally are
|
|
# required; anything with its own default elsewhere (Outer.y_hold_min,
|
|
# Inner.Hold.yi_max, the whole Thresholds section - see
|
|
# temp_controller_base.py/temp_controller_fsm.py's DEFAULT_THRESHOLDS)
|
|
# stays optional here too, on purpose.
|
|
TEMPCTRL_SCHEMA = {
|
|
'TempCtrl': {
|
|
'pid_type': str,
|
|
'Outer': _GAIN_SCHEMA,
|
|
'Inner': {
|
|
'Heat': _GAIN_SCHEMA,
|
|
'Hold': _GAIN_SCHEMA,
|
|
'Cool': _GAIN_SCHEMA,
|
|
},
|
|
},
|
|
}
|
|
|
|
|
|
def validate_temp_ctrl(config):
|
|
"""Checked eagerly at startup so a missing/misnamed PID gain fails with
|
|
a clear message here instead of a bare KeyError mid-__init__ or on the
|
|
first process() tick (this bit us once already: config.json.sim's gain/
|
|
Model nesting mismatch)."""
|
|
require_schema(config, TEMPCTRL_SCHEMA)
|