Coverage for ml_workbench/model.py: 25%
79 statements
« prev ^ index » next coverage.py v7.11.0, created at 2026-01-06 16:09 +0200
« prev ^ index » next coverage.py v7.11.0, created at 2026-01-06 16:09 +0200
1from __future__ import annotations
3from collections.abc import Mapping
4from dataclasses import dataclass
5from importlib import import_module
6from typing import TYPE_CHECKING, Any
8if TYPE_CHECKING: 8 ↛ 9line 8 didn't jump to line 9 because the condition on line 8 was never true
9 from .config import YamlConfig
12@dataclass(frozen=True)
13class ModelSpec:
14 name: str
15 type: str
16 description: str | None
17 params: dict[str, Any]
18 tuning: dict[str, Any]
21class Model:
22 """Encapsulates a model definition from the configuration.
24 Expected YAML structure:
26 models:
27 my_model:
28 description: "..." # optional
29 type: "package.module.Class" # required
30 params: { ... } # optional, free-form dict
31 tuning: { ... } # optional, free-form dict
33 Notes
34 -----
35 - The ``params`` and ``tuning`` sections are model-specific and are stored as given.
36 - Use ``instantiate()`` to create the model class instance with provided params.
37 """
39 def __init__(self, name: str, config: YamlConfig) -> None:
40 self.name = name
41 self.config = config
43 models_section = self._get_models_section(config)
44 if name not in models_section:
45 raise KeyError(f"Model '{name}' not found in configuration") # noqa: TRY003
47 raw = models_section[name]
48 if not isinstance(raw, Mapping):
49 raise TypeError(f"Model '{name}' specification must be a mapping") # noqa: TRY003
51 model_type = raw.get("type")
52 if not isinstance(model_type, str) or not model_type:
53 raise ValueError(f"Model '{name}' is missing required 'type' string field") # noqa: TRY003
55 description = raw.get("description")
56 params = raw.get("params", {})
57 tuning = raw.get("tuning", {})
59 if not isinstance(params, Mapping):
60 raise TypeError(f"Model '{name}'.params must be a mapping if provided") # noqa: TRY003
61 if not isinstance(tuning, Mapping):
62 raise TypeError(f"Model '{name}'.tuning must be a mapping if provided") # noqa: TRY003
64 self.spec = ModelSpec(
65 name=name,
66 type=model_type,
67 description=description if isinstance(description, str) else None,
68 params=dict(params),
69 tuning=dict(tuning),
70 )
72 @staticmethod
73 def _get_models_section(config: YamlConfig) -> Mapping[str, Any]:
74 models = config.get_data().get("models")
75 if not isinstance(models, Mapping):
76 raise KeyError("No 'models' section found in configuration") # noqa: TRY003
77 return models
79 @staticmethod
80 def _import_string(path: str) -> tuple[object, str]:
81 """Import a dotted path like 'package.module.Class'.
83 Returns the imported module object and the class name.
84 """
85 if "." not in path:
86 raise ValueError( # noqa: TRY003
87 f"Invalid model type path '{path}'. Expected 'package.module.Class'"
88 )
89 module_path, class_name = path.rsplit(".", 1)
90 module = import_module(module_path)
91 return module, class_name
93 def instantiate(self, **overrides: Any) -> Any:
94 """Instantiate the model class using configured params.
96 Parameters
97 ----------
98 overrides: Any
99 Optional overrides for parameters when constructing the model.
100 """
101 module, cls_name = self._import_string(self.spec.type)
102 cls = getattr(module, cls_name)
103 init_kwargs = {**self.spec.params, **overrides}
104 return cls(**init_kwargs)
106 def to_dict(self) -> dict[str, Any]:
107 return {
108 "name": self.spec.name,
109 "type": self.spec.type,
110 "description": self.spec.description,
111 "params": dict(self.spec.params),
112 "tuning": dict(self.spec.tuning),
113 }
115 @classmethod
116 def list_model_names(cls, config: YamlConfig) -> list[str]:
117 models = cls._get_models_section(config)
118 return list(models.keys())
120 @classmethod
121 def verify_config(cls, config: YamlConfig) -> None:
122 """Validate that all models are well-formed.
124 Checks
125 ------
126 - models section exists and is a mapping
127 - each model has a non-empty string 'type'
128 - params and tuning, if present, are mappings
129 """
130 models = config.get_data().get("models")
131 if models is None:
132 return
133 if not isinstance(models, Mapping):
134 raise TypeError("'models' section must be a mapping") # noqa: TRY003
136 for model_name, raw in models.items():
137 if not isinstance(raw, Mapping):
138 raise TypeError( # noqa: TRY003
139 f"Model '{model_name}' specification must be a mapping"
140 )
141 model_type = raw.get("type")
142 if not isinstance(model_type, str) or not model_type:
143 raise ValueError( # noqa: TRY003
144 f"Model '{model_name}' is missing required 'type' string field"
145 )
146 params = raw.get("params", {})
147 tuning = raw.get("tuning", {})
148 if not isinstance(params, Mapping):
149 raise TypeError( # noqa: TRY003
150 f"Model '{model_name}'.params must be a mapping if provided"
151 )
152 if not isinstance(tuning, Mapping):
153 raise TypeError( # noqa: TRY003
154 f"Model '{model_name}'.tuning must be a mapping if provided"
155 )
158__all__ = ["Model", "ModelSpec"]