reqm 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
reqm/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ # reqm — Ridiculously Easy Quant Manager
2
+
3
+ from reqm.quant import Quant
4
+ from reqm.quant_manager import ConfigValidationError, QuantManager
5
+
6
+ __all__ = ["ConfigValidationError", "Quant", "QuantManager"]
reqm/overrides_ext.py ADDED
@@ -0,0 +1,165 @@
1
+ """
2
+ overrides_ext.py — Extended override utilities for reqm.
3
+
4
+ A thin extension of the `overrides` library that adds `allow_any_override`,
5
+ a marker for abstract methods whose signature subclasses are permitted to narrow.
6
+
7
+ The problem this solves:
8
+ Quant defines ``__call__(self, **kwargs)`` as abstract so subclasses can
9
+ narrow it to any specific signature (e.g. ``__call__(self, text: str)``).
10
+ The vanilla ``@override`` would reject this as a signature mismatch.
11
+ ``@allow_any_override`` on the base method tells our ``@override`` to skip
12
+ signature checking for that particular method — while keeping it enforced
13
+ for all other overrides.
14
+
15
+ Public API:
16
+ allow_any_override — marker decorator for base methods
17
+ override — drop-in for @override, respects @allow_any_override
18
+ final — re-export from overrides library (unchanged)
19
+ EnforceOverrides — re-export from overrides library (unchanged)
20
+
21
+ Usage::
22
+
23
+ from reqm.overrides_ext import allow_any_override, override, EnforceOverrides
24
+
25
+ class Base(EnforceOverrides):
26
+ @abstractmethod
27
+ @allow_any_override
28
+ def __call__(self, **kwargs) -> Any: ... # any signature OK
29
+
30
+ @abstractmethod
31
+ def name(self) -> str: ... # signature enforced
32
+
33
+ class Child(Base):
34
+ @override
35
+ def __call__(self, text: str) -> str: ... # different sig — allowed
36
+
37
+ @override
38
+ def name(self) -> str: ... # same sig — required
39
+ """
40
+
41
+ import sys
42
+ import typing as ty
43
+
44
+ from overrides import EnforceOverrides, final
45
+ from overrides.overrides import _get_base_classes, _overrides
46
+
47
+ F = ty.TypeVar("F", bound=ty.Callable[..., ty.Any])
48
+
49
+
50
+ def _should_enforce_signature(method: ty.Callable) -> bool:
51
+ """Decide whether signature checking applies for this override.
52
+
53
+ Returns False if the method itself carries ``@allow_any_override``, or if
54
+ every parent definition of the same method carries it. Returns True as
55
+ soon as any parent definition is found *without* the marker.
56
+
57
+ Raises:
58
+ TypeError: If no parent class defines a method with the same name.
59
+ """
60
+ if getattr(method, "__allow_any_override__", False):
61
+ return False
62
+
63
+ global_vars = getattr(method, "__globals__", None)
64
+ if global_vars is None:
65
+ global_vars = vars(sys.modules[method.__module__])
66
+
67
+ found = False
68
+ for super_class in _get_base_classes(sys._getframe(3), global_vars):
69
+ parent_method = getattr(super_class, method.__name__, None)
70
+ if parent_method is None:
71
+ continue
72
+ found = True
73
+ if not getattr(parent_method, "__allow_any_override__", False):
74
+ return True
75
+
76
+ if not found:
77
+ raise TypeError(
78
+ f"'{method.__qualname__}' does not override any method in its "
79
+ f"base classes. Remove @override or check the method name."
80
+ )
81
+
82
+ return False
83
+
84
+
85
+ def allow_any_override(method: F) -> F:
86
+ """Mark an abstract method as permitting override with any signature.
87
+
88
+ Place this on base class methods where subclasses are expected to narrow
89
+ the signature (e.g. ``**kwargs`` → specific parameters). Without this
90
+ marker, ``@override`` would reject the signature change as an LSP
91
+ violation.
92
+
93
+ This is a marker only — it sets ``__allow_any_override__ = True`` on the
94
+ method. The check is performed by the ``@override`` decorator in this
95
+ module at class-creation time.
96
+
97
+ Args:
98
+ method: The abstract method to mark.
99
+
100
+ Returns:
101
+ The same method, with ``__allow_any_override__ = True`` set.
102
+
103
+ Examples:
104
+ Mark a base method::
105
+
106
+ class Base(EnforceOverrides):
107
+ @abstractmethod
108
+ @allow_any_override
109
+ def __call__(self, **kwargs) -> Any: ...
110
+
111
+ Subclass freely narrows the signature::
112
+
113
+ class Child(Base):
114
+ @override
115
+ def __call__(self, text: str, max_tokens: int = 512) -> str:
116
+ ...
117
+ """
118
+ method.__allow_any_override__ = True # type: ignore[attr-defined]
119
+ return method
120
+
121
+
122
+ def override(method: F) -> F:
123
+ """Override decorator that respects ``@allow_any_override`` on parent methods.
124
+
125
+ Drop-in replacement for ``@override`` from the ``overrides`` library.
126
+ Behaviour:
127
+ - If the parent method has ``@allow_any_override``: installs the override
128
+ without signature validation.
129
+ - Otherwise: delegates to vanilla ``_overrides`` with full signature
130
+ checking.
131
+
132
+ Resolution happens at class-body execution time via the ``overrides``
133
+ library's ``_get_base_classes`` frame introspection.
134
+
135
+ Args:
136
+ method: The overriding method.
137
+
138
+ Returns:
139
+ The method, marked with ``__override__ = True``.
140
+
141
+ Raises:
142
+ TypeError: If ``method`` does not override any parent method.
143
+
144
+ Examples:
145
+ Narrowing a signature (allowed because parent has @allow_any_override)::
146
+
147
+ class Summarizer(Quant):
148
+ @override
149
+ def __call__(self, text: str, max_tokens: int = 512) -> str:
150
+ ...
151
+
152
+ Normal override with signature enforcement::
153
+
154
+ class Summarizer(Quant):
155
+ @override
156
+ def dummy_inputs(self) -> list[dict[str, Any]]:
157
+ return [{"text": "hello"}]
158
+ """
159
+ method.__override__ = True # type: ignore[attr-defined]
160
+ if _should_enforce_signature(method):
161
+ _overrides(method, check_signature=True, check_at_runtime=False)
162
+ return method
163
+
164
+
165
+ __all__ = ["allow_any_override", "override", "final", "EnforceOverrides"]
reqm/quant.py ADDED
@@ -0,0 +1,120 @@
1
+ """
2
+ quant.py — The Quant abstract base class.
3
+
4
+ A Quant is the unit reqm builds and manages: a callable with constructor args
5
+ defined in config, and a dummy_inputs method that makes it auditable.
6
+ """
7
+
8
+ import abc
9
+
10
+ from reqm.overrides_ext import EnforceOverrides, allow_any_override
11
+
12
+
13
+ class Quant(abc.ABC, EnforceOverrides):
14
+ """Abstract base class for all reqm Quants.
15
+
16
+ A Quant is a callable unit managed by reqm. It is:
17
+
18
+ - **Callable** — invoked directly with its inputs via ``__call__``
19
+ - **Config-driven** — constructor arguments defined in YAML, not hardcoded
20
+ - **Auditable** — ``dummy_inputs()`` provides example inputs that reqm uses
21
+ to verify the Quant actually runs at build time, not silently at inference
22
+
23
+ Subclasses must implement:
24
+
25
+ - ``__call__(**kwargs)`` — narrowed to the specific input signature
26
+ - ``dummy_inputs()`` — returns a list of input dicts, each ``**``-expandable
27
+ into ``__call__``
28
+
29
+ Examples:
30
+ Define a Quant::
31
+
32
+ from reqm import Quant
33
+ from reqm.overrides_ext import override
34
+
35
+ class Summarizer(Quant):
36
+ def __init__(self, model_name: str, max_tokens: int):
37
+ self.model = load_model(model_name)
38
+ self.max_tokens = max_tokens
39
+
40
+ @override
41
+ def __call__(self, text: str) -> str:
42
+ return self.model.summarize(text, max_tokens=self.max_tokens)
43
+
44
+ @override
45
+ def dummy_inputs(self) -> list[dict[str, Any]]:
46
+ return [
47
+ {"text": "The quick brown fox."},
48
+ {"text": "Short."},
49
+ ]
50
+
51
+ reqm calls ``dummy_inputs`` at build time::
52
+
53
+ for inputs in quant.dummy_inputs():
54
+ quant(**inputs) # fails fast here if something is broken
55
+ """
56
+
57
+ @abc.abstractmethod
58
+ @allow_any_override
59
+ def __call__(self, **kwargs) -> object:
60
+ """Call the Quant with the given inputs.
61
+
62
+ Subclasses must override this with their specific input signature.
63
+ The base signature is ``**kwargs`` to allow any narrowing — see
64
+ ``allow_any_override`` for why this is intentional.
65
+
66
+ Args:
67
+ **kwargs: Inputs to the Quant. The actual parameters are defined
68
+ by the subclass.
69
+
70
+ Returns:
71
+ The Quant's output. Type is defined by the subclass.
72
+
73
+ Examples:
74
+ Subclass narrowing the signature::
75
+
76
+ class Greeter(Quant):
77
+ @override
78
+ def __call__(self, name: str) -> str:
79
+ return f"Hello, {name}!"
80
+
81
+ Calling the Quant::
82
+
83
+ greeter = reqm.get("greeter/friendly")
84
+ result = greeter(name="world")
85
+ """
86
+ ...
87
+
88
+ @abc.abstractmethod
89
+ def dummy_inputs(self) -> list[dict[str, object]]:
90
+ """Return a list of example input dicts for build-time sanity checking.
91
+
92
+ Each dict maps argument names to example values. reqm ``**``-expands
93
+ each dict into ``__call__`` when building the Quant. If any call fails,
94
+ the error is raised immediately with context — fail fast, not in prod.
95
+
96
+ Multiple dicts are encouraged: cover the happy path and edge cases.
97
+ These also serve as living documentation of valid call signatures.
98
+
99
+ Returns:
100
+ A non-empty list of dicts, each ``**``-expandable into ``__call__``.
101
+
102
+ Raises:
103
+ NotImplementedError: If not implemented by the subclass.
104
+
105
+ Examples:
106
+ Single input set::
107
+
108
+ def dummy_inputs(self) -> list[dict[str, Any]]:
109
+ return [{"text": "The quick brown fox."}]
110
+
111
+ Multiple input sets (recommended — covers more call patterns)::
112
+
113
+ def dummy_inputs(self) -> list[dict[str, Any]]:
114
+ return [
115
+ {"text": "Short text."},
116
+ {"text": "A longer piece of text to summarize properly."},
117
+ {"text": "Edge case: single word."},
118
+ ]
119
+ """
120
+ ...
reqm/quant_manager.py ADDED
@@ -0,0 +1,264 @@
1
+ """
2
+ quant_manager.py — Directory-based config management for reqm.
3
+
4
+ QuantManager takes an importable Python module (a directory with ``__init__.py``
5
+ and YAML config files) and treats its filesystem root as a Hydra config search
6
+ path. It provides methods to list, validate, load, and instantiate configs.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import types
12
+ from pathlib import Path
13
+
14
+ from hydra import compose, initialize_config_dir
15
+ from hydra.core.global_hydra import GlobalHydra
16
+ from hydra.utils import instantiate
17
+ from omegaconf import DictConfig, OmegaConf
18
+
19
+
20
+ class ConfigValidationError(Exception):
21
+ """Raised when a config file fails validation.
22
+
23
+ Includes the config path and a description of what failed, so the user
24
+ knows exactly which file to fix and what to change.
25
+
26
+ Args:
27
+ message: Human-readable description of the validation failure.
28
+
29
+ Examples:
30
+ Raised when ``# @package _global_`` is missing::
31
+
32
+ raise ConfigValidationError(
33
+ "Config 'model_a' at /path/to/model_a.yaml is missing the "
34
+ "required '# @package _global_' header. Add it as the first "
35
+ "line of the file."
36
+ )
37
+ """
38
+
39
+
40
+ class QuantManager:
41
+ """Directory-based config manager built on Hydra.
42
+
43
+ Takes an importable Python config module and uses its directory as the
44
+ Hydra config root. Provides a uniform API to list, validate, load, and
45
+ build objects from YAML configs.
46
+
47
+ Args:
48
+ config_module: An imported Python module whose directory contains
49
+ YAML config files. Must be a real module with a ``__path__``
50
+ or ``__file__`` attribute.
51
+
52
+ Raises:
53
+ TypeError: If *config_module* is not a Python module.
54
+
55
+ Examples:
56
+ Create a QuantManager from a config module::
57
+
58
+ import my_configs
59
+ from reqm import QuantManager
60
+
61
+ QM = QuantManager(my_configs)
62
+ QM.list_configs() # ["model_a", "sub/model_b"]
63
+ cfg = QM.get_config("model_a")
64
+ obj = QM.build("model_a")
65
+ """
66
+
67
+ def __init__(self, config_module: types.ModuleType) -> None:
68
+ if not isinstance(config_module, types.ModuleType):
69
+ raise TypeError(
70
+ f"Expected an imported Python module, got "
71
+ f"{type(config_module).__name__}. Pass the module object "
72
+ f"itself (e.g. `import my_configs; QuantManager(my_configs)`)."
73
+ )
74
+
75
+ if hasattr(config_module, "__path__"):
76
+ self._config_dir = Path(config_module.__path__[0]).resolve()
77
+ elif hasattr(config_module, "__file__") and config_module.__file__ is not None:
78
+ self._config_dir = Path(config_module.__file__).resolve().parent
79
+ else:
80
+ raise TypeError(
81
+ f"Module {config_module.__name__!r} has no __path__ or __file__ "
82
+ f"attribute. Cannot determine config directory."
83
+ )
84
+
85
+ def _resolve_config_path(self, config_name: str) -> Path:
86
+ """Return the absolute path to a config's YAML file, or raise."""
87
+ yaml_path = self._config_dir / f"{config_name}.yaml"
88
+ if not yaml_path.is_file():
89
+ raise FileNotFoundError(
90
+ f"Config '{config_name}' not found in config module "
91
+ f"at {self._config_dir}. "
92
+ f"Available configs: {self.list_configs()}"
93
+ )
94
+ return yaml_path
95
+
96
+ def list_configs(self) -> list[str]:
97
+ """List all YAML config names in the config module.
98
+
99
+ Recursively walks the config module directory and returns config
100
+ names (relative paths without the ``.yaml`` extension), sorted
101
+ alphabetically.
102
+
103
+ Returns:
104
+ Sorted list of config name strings.
105
+
106
+ Examples:
107
+ >>> QM = QuantManager(my_configs)
108
+ >>> QM.list_configs()
109
+ ['model_a', 'model_b', 'serving/prod', 'serving/staging']
110
+ """
111
+ return sorted(
112
+ str(p.relative_to(self._config_dir).with_suffix("")).replace("\\", "/")
113
+ for p in self._config_dir.rglob("*.yaml")
114
+ )
115
+
116
+ def validate(self, config_name: str | None = None) -> None:
117
+ """Validate that configs have the required ``# @package _global_`` header.
118
+
119
+ If *config_name* is provided, validates only that config. If ``None``,
120
+ validates every YAML file in the config module.
121
+
122
+ Args:
123
+ config_name: Optional config name to validate. If ``None``,
124
+ validates all configs.
125
+
126
+ Raises:
127
+ ConfigValidationError: If a config is missing
128
+ ``# @package _global_``.
129
+ FileNotFoundError: If *config_name* does not correspond to a
130
+ YAML file in the config module.
131
+
132
+ Examples:
133
+ Validate a single config::
134
+
135
+ QM.validate("model_a") # raises if invalid
136
+
137
+ Validate all configs at once::
138
+
139
+ QM.validate() # checks every YAML in the module
140
+ """
141
+ names = [config_name] if config_name is not None else self.list_configs()
142
+ for name in names:
143
+ yaml_path = self._resolve_config_path(name)
144
+ content = yaml_path.read_text(encoding="utf-8")
145
+ if "# @package _global_" not in content:
146
+ raise ConfigValidationError(
147
+ f"Config '{name}' at {yaml_path} is missing the required "
148
+ f"'# @package _global_' header. Add it as the first line "
149
+ f"of the file."
150
+ )
151
+
152
+ def get_config(
153
+ self,
154
+ config_name: str,
155
+ *,
156
+ config_overrides: dict | DictConfig | None = None,
157
+ param_overrides: list[str] | None = None,
158
+ ) -> DictConfig:
159
+ """Load and fully resolve a config by name.
160
+
161
+ Composes the config via Hydra, applies overrides, resolves all
162
+ interpolations, and returns the result as an OmegaConf ``DictConfig``.
163
+
164
+ Args:
165
+ config_name: Config name (relative path, no ``.yaml`` extension).
166
+ config_overrides: Optional dict or ``DictConfig`` merged on top
167
+ of the composed config.
168
+ param_overrides: Optional list of Hydra CLI-style override strings
169
+ (e.g. ``["key=val", "nested.key=123"]``).
170
+
171
+ Returns:
172
+ Fully resolved ``DictConfig``.
173
+
174
+ Raises:
175
+ FileNotFoundError: If *config_name* does not exist.
176
+ omegaconf.errors.MissingMandatoryValue: If an interpolation
177
+ cannot be resolved.
178
+
179
+ Examples:
180
+ >>> cfg = QM.get_config("model_a")
181
+ >>> cfg = QM.get_config("model_a", param_overrides=["lr=0.01"])
182
+ >>> cfg = QM.get_config("model_a", config_overrides={"lr": 0.01})
183
+ """
184
+ self._resolve_config_path(config_name)
185
+
186
+ GlobalHydra.instance().clear()
187
+ with initialize_config_dir(config_dir=str(self._config_dir), version_base=None):
188
+ cfg = compose(config_name=config_name, overrides=param_overrides or [])
189
+ if config_overrides is not None:
190
+ OmegaConf.set_struct(cfg, False)
191
+ cfg = OmegaConf.merge(cfg, config_overrides)
192
+ OmegaConf.resolve(cfg)
193
+ return cfg
194
+
195
+ def get_raw_config(
196
+ self,
197
+ config_name: str,
198
+ *,
199
+ config_overrides: dict | DictConfig | None = None,
200
+ param_overrides: list[str] | None = None,
201
+ ) -> str:
202
+ """Load, resolve, and return a config as a YAML string.
203
+
204
+ Equivalent to calling :meth:`get_config` and serializing the result
205
+ via ``OmegaConf.to_yaml``.
206
+
207
+ Args:
208
+ config_name: Config name (relative path, no ``.yaml`` extension).
209
+ config_overrides: Optional dict or ``DictConfig`` merged on top.
210
+ param_overrides: Optional Hydra CLI-style override strings.
211
+
212
+ Returns:
213
+ The resolved config serialized as a YAML string.
214
+
215
+ Examples:
216
+ >>> yaml_str = QM.get_raw_config("model_a")
217
+ >>> print(yaml_str)
218
+ _target_: my_module.ModelA
219
+ lr: 0.001
220
+ """
221
+ cfg = self.get_config(
222
+ config_name,
223
+ config_overrides=config_overrides,
224
+ param_overrides=param_overrides,
225
+ )
226
+ return OmegaConf.to_yaml(cfg)
227
+
228
+ def build(
229
+ self,
230
+ config_name: str,
231
+ *,
232
+ config_overrides: dict | DictConfig | None = None,
233
+ param_overrides: list[str] | None = None,
234
+ ) -> object:
235
+ """Build an object from a config via ``hydra.utils.instantiate``.
236
+
237
+ Loads the config with :meth:`get_config`, then passes it to Hydra's
238
+ recursive instantiation. Returns whatever ``_target_`` points to.
239
+
240
+ This is a generic instantiator — it does **not** require the result
241
+ to be a :class:`~reqm.quant.Quant` subclass.
242
+
243
+ Args:
244
+ config_name: Config name (relative path, no ``.yaml`` extension).
245
+ config_overrides: Optional dict or ``DictConfig`` merged on top.
246
+ param_overrides: Optional Hydra CLI-style override strings.
247
+
248
+ Returns:
249
+ The instantiated object.
250
+
251
+ Raises:
252
+ FileNotFoundError: If *config_name* does not exist.
253
+ hydra.errors.InstantiationException: If instantiation fails.
254
+
255
+ Examples:
256
+ >>> obj = QM.build("model_a")
257
+ >>> obj = QM.build("model_a", param_overrides=["lr=0.01"])
258
+ """
259
+ cfg = self.get_config(
260
+ config_name,
261
+ config_overrides=config_overrides,
262
+ param_overrides=param_overrides,
263
+ )
264
+ return instantiate(cfg)
@@ -0,0 +1,256 @@
1
+ Metadata-Version: 2.4
2
+ Name: reqm
3
+ Version: 0.1.0
4
+ Summary: Ridiculously Easy Quant Manager — config-based aliased object factory with enforced interfaces
5
+ Project-URL: Homepage, https://github.com/jkvc/reqm
6
+ License: MIT
7
+ License-File: LICENSE
8
+ Requires-Python: >=3.9
9
+ Requires-Dist: hydra-core>=1.3
10
+ Requires-Dist: overrides>=7.7.0
11
+ Description-Content-Type: text/markdown
12
+
13
+ # reqm
14
+
15
+ **R**idiculously **E**asy **Q**uant **M**anager
16
+
17
+ Directory-based config management and object factory built on [Hydra](https://hydra.cc).
18
+
19
+ ---
20
+
21
+ ## The problem
22
+
23
+ Hydra is excellent for config-driven instantiation. But using it as a general object factory requires ceremony:
24
+
25
+ ```python
26
+ # You have to do all of this just to instantiate one object
27
+ with hydra.initialize(config_path="conf"):
28
+ cfg = hydra.compose(config_name="my_model")
29
+ model = hydra.utils.instantiate(cfg.model)
30
+ ```
31
+
32
+ This ceremony means Hydra stays in the lab. It's awkward in a notebook, verbose in a service, and doesn't belong in production call sites.
33
+
34
+ `reqm` gives you Hydra's power — config-driven instantiation, composable overrides, recursive object graphs — with none of the ceremony:
35
+
36
+ ```python
37
+ from reqm import QuantManager
38
+ import my_configs
39
+
40
+ QM = QuantManager(my_configs)
41
+ model = QM.build("summarizer_prod")
42
+ ```
43
+
44
+ Same call in a notebook, a FastAPI endpoint, a test, or a batch job.
45
+
46
+ ---
47
+
48
+ ## Core concept: the Quant
49
+
50
+ A **Quant** is the unit reqm builds and manages. It is:
51
+
52
+ - **Callable** — invoked directly with its inputs
53
+ - **Config-driven** — constructor arguments defined in YAML, no hardcoding
54
+ - **Auditable** — implements `dummy_inputs()`, example inputs the factory uses to verify it actually runs
55
+
56
+ ```python
57
+ from reqm import Quant
58
+ from reqm.overrides_ext import override
59
+
60
+ class Summarizer(Quant):
61
+ def __init__(self, model_name: str, max_tokens: int):
62
+ self.model = load_model(model_name)
63
+ self.max_tokens = max_tokens
64
+
65
+ @override
66
+ def dummy_inputs(self) -> list[dict]:
67
+ return [{"text": "The quick brown fox jumps over the lazy dog."}]
68
+
69
+ @override
70
+ def __call__(self, text: str) -> str:
71
+ return self.model.summarize(text, max_tokens=self.max_tokens)
72
+ ```
73
+
74
+ The `dummy_inputs` contract is what separates a Quant from a plain ABC. reqm can call each Quant with its own dummy inputs at build time — if it fails, it fails early and loudly, not silently in production.
75
+
76
+ ---
77
+
78
+ ## Config modules and QuantManager
79
+
80
+ A **config module** is any importable Python package containing YAML files:
81
+
82
+ ```
83
+ my_configs/
84
+ ├── __init__.py
85
+ ├── summarizer_prod.yaml
86
+ ├── summarizer_fast.yaml
87
+ └── serving/
88
+ └── prod.yaml
89
+ ```
90
+
91
+ Each YAML config declares what to build:
92
+
93
+ ```yaml
94
+ # @package _global_
95
+ _target_: myproject.models.Summarizer
96
+ model_name: gpt-4o
97
+ max_tokens: 512
98
+ ```
99
+
100
+ `QuantManager` takes the config module and gives you a uniform API. Construct it once in the `__init__.py` next to your configs directory, then import `QM` everywhere:
101
+
102
+ ```python
103
+ # myproject/__init__.py (next to configs/)
104
+ import myproject.configs as configs
105
+ from reqm import QuantManager
106
+
107
+ QM = QuantManager(configs)
108
+ ```
109
+
110
+ ```python
111
+ # Any call site — notebook, script, service, test
112
+ from myproject import QM
113
+
114
+ QM.list_configs() # ["serving/prod", "summarizer_fast", "summarizer_prod"]
115
+ QM.validate() # check all configs have # @package _global_
116
+ cfg = QM.get_config("summarizer_prod") # resolved OmegaConf DictConfig
117
+ obj = QM.build("summarizer_prod") # instantiated object
118
+ ```
119
+
120
+ Configs can compose other configs via Hydra defaults lists:
121
+
122
+ ```yaml
123
+ # @package _global_
124
+ defaults:
125
+ - /base_model@child
126
+ - _self_
127
+ _target_: myproject.models.Ensemble
128
+ weight: 0.6
129
+ ```
130
+
131
+ ---
132
+
133
+ ## The uniform call site
134
+
135
+ The core value proposition: ONE script, swap the config name, get different experimental results. No code changes, no if/else chains, no factory functions.
136
+
137
+ Construct `QM` once in the `__init__.py` right next to your configs directory:
138
+
139
+ ```python
140
+ # myproject/__init__.py (lives next to configs/)
141
+ import myproject.configs as configs
142
+ from reqm import QuantManager
143
+
144
+ QM = QuantManager(configs)
145
+ ```
146
+
147
+ Then every script just imports it:
148
+
149
+ ```python
150
+ import sys
151
+ from myproject import QM
152
+
153
+ model = QM.build(sys.argv[1]) # <-- only this string changes
154
+ result = model(text="Hello world")
155
+ ```
156
+
157
+ ```bash
158
+ python evaluate.py summarizer_prod
159
+ python evaluate.py summarizer_fast
160
+ python evaluate.py summarizer_experiment_v3
161
+ ```
162
+
163
+ ---
164
+
165
+ ## Runnable example
166
+
167
+ The repo includes a complete example project at `examples/estimators/` that demonstrates Quant subclasses, non-Quant configurable dependencies (Filters), Hydra config composition, and multiple scripts sharing a single `QM` instance defined in `examples/estimators/__init__.py`:
168
+
169
+ ```bash
170
+ # Evaluate a single estimator config
171
+ uv run python -m examples.estimators.scripts.evaluate mean_simple
172
+
173
+ # Inspect the fully resolved config YAML
174
+ uv run python -m examples.estimators.scripts.inspect_config ensemble/mean_median
175
+
176
+ # Compare multiple configs side by side
177
+ uv run python -m examples.estimators.scripts.compare mean_simple mean_outlier median_simple
178
+
179
+ # Validate all configs
180
+ uv run python -m examples.estimators.scripts.validate_configs
181
+
182
+ # Sweep all configs and rank by performance
183
+ uv run python -m examples.estimators.scripts.sweep
184
+ ```
185
+
186
+ ---
187
+
188
+ ## PyTorch integration
189
+
190
+ reqm does not depend on PyTorch, but its primary use case is config-driven model experimentation with `nn.Module`. The repo includes a `TorchQuant` bridge class that resolves the `__call__` vs `forward()` conflict — subclass it instead of plain `Quant` when your model is an `nn.Module`:
191
+
192
+ Don't subclass TorchQuant directly for every model — create a **domain base class** that locks the `forward` signature for your use case:
193
+
194
+ ```python
195
+ from myproject.torch_quant import TorchQuant # copy from examples/
196
+
197
+ # Domain base — locks forward(self, x: Tensor) -> Tensor for all regressors
198
+ class Regressor(TorchQuant):
199
+ in_features: int
200
+
201
+ @override
202
+ @abc.abstractmethod
203
+ def forward(self, x: torch.Tensor) -> torch.Tensor: ...
204
+
205
+ @override
206
+ def dummy_inputs(self) -> list[dict]:
207
+ return [{"x": torch.randn(4, self.in_features)}]
208
+
209
+
210
+ # Concrete model — must match the Regressor signature
211
+ class LinearRegressor(Regressor):
212
+ def __init__(self, in_features: int):
213
+ super().__init__()
214
+ self.in_features = in_features
215
+ self.linear = nn.Linear(in_features, 1)
216
+
217
+ @override
218
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
219
+ return self.linear(x)
220
+ ```
221
+
222
+ `TorchQuant.forward` uses `@allow_any_override` so domain bases can narrow freely. Once the domain base locks the signature (without `@allow_any_override`), all concrete models must match — enforced at class definition time, not runtime.
223
+
224
+ See `docs/torch_integration.md` for the full explanation, and `examples/torch_models/` for a runnable example:
225
+
226
+ ```bash
227
+ uv run python -m examples.torch_models.scripts.evaluate linear_simple
228
+ uv run python -m examples.torch_models.scripts.evaluate mlp_small
229
+ uv run python -m examples.torch_models.scripts.audit
230
+ ```
231
+
232
+ ---
233
+
234
+ ## Why not just Hydra?
235
+
236
+ Hydra is framework-first. It expects to own your program's entry point. `reqm` is library-first — it has no opinion about your application structure and works wherever Python runs.
237
+
238
+ | | Hydra | reqm |
239
+ |---|---|---|
240
+ | Object instantiation | yes | yes |
241
+ | Config composition | yes | yes (via Hydra) |
242
+ | Auditability (`dummy_inputs`) | no | yes |
243
+ | Works in notebooks | limited | yes |
244
+ | CLI ceremony required | yes | no |
245
+
246
+ ---
247
+
248
+ ## Name
249
+
250
+ `reqm` is also a nod to [Rue Esquermoise](https://en.wikipedia.org/wiki/Rue_Esquermoise), one of the oldest streets in Lille, France, dating to the 13th century. Its etymology traces to the Flemish *eskelm* — "frontier." A fitting name for a library that sits at the frontier between research and production.
251
+
252
+ ---
253
+
254
+ ## Status
255
+
256
+ Core API implemented: `Quant`, `QuantManager`, config composition, and override support all work. See `examples/estimators/` for a complete working project.
@@ -0,0 +1,8 @@
1
+ reqm/__init__.py,sha256=UJmu6qNyiYLzKhaumRQ70NKxVZaHOi3RYDrx6mwy_Kk,202
2
+ reqm/overrides_ext.py,sha256=0_QWmf9r0eonsyjK3c6VaY3m6r_YpuNbbi6851278Zg,5671
3
+ reqm/quant.py,sha256=ZdvfHl-PLdKp2EvageR5GtU39jC_aOfnr2qWmOlktKw,4179
4
+ reqm/quant_manager.py,sha256=ng39ksSXRZPsrwop-DY-HSFqvLSSHmmvWNeDnsxcnIE,9633
5
+ reqm-0.1.0.dist-info/METADATA,sha256=059nNsulphQIQUN_K-XieLgI2IkjleBf7xC-ejU10co,8031
6
+ reqm-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
7
+ reqm-0.1.0.dist-info/licenses/LICENSE,sha256=HXnpKXLs5Z7H4V7nPUlUXb0L4_HQ_Q66xvm5xn45rOY,1061
8
+ reqm-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 jkvc
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.