ob-metaflow-extensions 1.1.151__py2.py3-none-any.whl → 1.4.33__py2.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.
- metaflow_extensions/outerbounds/__init__.py +1 -1
- metaflow_extensions/outerbounds/plugins/__init__.py +17 -3
- metaflow_extensions/outerbounds/plugins/apps/app_cli.py +0 -0
- metaflow_extensions/outerbounds/plugins/apps/app_deploy_decorator.py +146 -0
- metaflow_extensions/outerbounds/plugins/apps/core/__init__.py +10 -0
- metaflow_extensions/outerbounds/plugins/apps/core/_state_machine.py +506 -0
- metaflow_extensions/outerbounds/plugins/apps/core/_vendor/__init__.py +0 -0
- metaflow_extensions/outerbounds/plugins/apps/core/_vendor/spinner/__init__.py +4 -0
- metaflow_extensions/outerbounds/plugins/apps/core/_vendor/spinner/spinners.py +478 -0
- metaflow_extensions/outerbounds/plugins/apps/core/app_cli.py +1200 -0
- metaflow_extensions/outerbounds/plugins/apps/core/app_config.py +146 -0
- metaflow_extensions/outerbounds/plugins/apps/core/artifacts.py +0 -0
- metaflow_extensions/outerbounds/plugins/apps/core/capsule.py +958 -0
- metaflow_extensions/outerbounds/plugins/apps/core/click_importer.py +24 -0
- metaflow_extensions/outerbounds/plugins/apps/core/code_package/__init__.py +3 -0
- metaflow_extensions/outerbounds/plugins/apps/core/code_package/code_packager.py +618 -0
- metaflow_extensions/outerbounds/plugins/apps/core/code_package/examples.py +125 -0
- metaflow_extensions/outerbounds/plugins/apps/core/config/__init__.py +12 -0
- metaflow_extensions/outerbounds/plugins/apps/core/config/cli_generator.py +161 -0
- metaflow_extensions/outerbounds/plugins/apps/core/config/config_utils.py +868 -0
- metaflow_extensions/outerbounds/plugins/apps/core/config/schema_export.py +288 -0
- metaflow_extensions/outerbounds/plugins/apps/core/config/typed_configs.py +139 -0
- metaflow_extensions/outerbounds/plugins/apps/core/config/typed_init_generator.py +398 -0
- metaflow_extensions/outerbounds/plugins/apps/core/config/unified_config.py +1088 -0
- metaflow_extensions/outerbounds/plugins/apps/core/config_schema.yaml +337 -0
- metaflow_extensions/outerbounds/plugins/apps/core/dependencies.py +115 -0
- metaflow_extensions/outerbounds/plugins/apps/core/deployer.py +303 -0
- metaflow_extensions/outerbounds/plugins/apps/core/experimental/__init__.py +89 -0
- metaflow_extensions/outerbounds/plugins/apps/core/perimeters.py +87 -0
- metaflow_extensions/outerbounds/plugins/apps/core/secrets.py +164 -0
- metaflow_extensions/outerbounds/plugins/apps/core/utils.py +233 -0
- metaflow_extensions/outerbounds/plugins/apps/core/validations.py +17 -0
- metaflow_extensions/outerbounds/plugins/aws/__init__.py +4 -0
- metaflow_extensions/outerbounds/plugins/aws/assume_role.py +3 -0
- metaflow_extensions/outerbounds/plugins/aws/assume_role_decorator.py +78 -0
- metaflow_extensions/outerbounds/plugins/checkpoint_datastores/coreweave.py +9 -77
- metaflow_extensions/outerbounds/plugins/checkpoint_datastores/external_chckpt.py +85 -0
- metaflow_extensions/outerbounds/plugins/checkpoint_datastores/nebius.py +7 -78
- metaflow_extensions/outerbounds/plugins/fast_bakery/baker.py +110 -0
- metaflow_extensions/outerbounds/plugins/fast_bakery/docker_environment.py +17 -3
- metaflow_extensions/outerbounds/plugins/fast_bakery/fast_bakery.py +1 -0
- metaflow_extensions/outerbounds/plugins/kubernetes/kubernetes_client.py +18 -44
- metaflow_extensions/outerbounds/plugins/kubernetes/pod_killer.py +374 -0
- metaflow_extensions/outerbounds/plugins/nim/card.py +1 -6
- metaflow_extensions/outerbounds/plugins/nim/{__init__.py → nim_decorator.py} +13 -49
- metaflow_extensions/outerbounds/plugins/nim/nim_manager.py +294 -233
- metaflow_extensions/outerbounds/plugins/nim/utils.py +36 -0
- metaflow_extensions/outerbounds/plugins/nvcf/constants.py +2 -2
- metaflow_extensions/outerbounds/plugins/nvct/nvct_decorator.py +32 -8
- metaflow_extensions/outerbounds/plugins/nvct/nvct_runner.py +1 -1
- metaflow_extensions/outerbounds/plugins/ollama/__init__.py +171 -16
- metaflow_extensions/outerbounds/plugins/ollama/constants.py +1 -0
- metaflow_extensions/outerbounds/plugins/ollama/exceptions.py +22 -0
- metaflow_extensions/outerbounds/plugins/ollama/ollama.py +1710 -114
- metaflow_extensions/outerbounds/plugins/ollama/status_card.py +292 -0
- metaflow_extensions/outerbounds/plugins/optuna/__init__.py +48 -0
- metaflow_extensions/outerbounds/plugins/profilers/simple_card_decorator.py +96 -0
- metaflow_extensions/outerbounds/plugins/s3_proxy/__init__.py +7 -0
- metaflow_extensions/outerbounds/plugins/s3_proxy/binary_caller.py +132 -0
- metaflow_extensions/outerbounds/plugins/s3_proxy/constants.py +11 -0
- metaflow_extensions/outerbounds/plugins/s3_proxy/exceptions.py +13 -0
- metaflow_extensions/outerbounds/plugins/s3_proxy/proxy_bootstrap.py +59 -0
- metaflow_extensions/outerbounds/plugins/s3_proxy/s3_proxy_api.py +93 -0
- metaflow_extensions/outerbounds/plugins/s3_proxy/s3_proxy_decorator.py +250 -0
- metaflow_extensions/outerbounds/plugins/s3_proxy/s3_proxy_manager.py +225 -0
- metaflow_extensions/outerbounds/plugins/snowpark/snowpark_client.py +6 -3
- metaflow_extensions/outerbounds/plugins/snowpark/snowpark_decorator.py +13 -7
- metaflow_extensions/outerbounds/plugins/snowpark/snowpark_job.py +8 -2
- metaflow_extensions/outerbounds/plugins/torchtune/__init__.py +163 -0
- metaflow_extensions/outerbounds/plugins/vllm/__init__.py +255 -0
- metaflow_extensions/outerbounds/plugins/vllm/constants.py +1 -0
- metaflow_extensions/outerbounds/plugins/vllm/exceptions.py +1 -0
- metaflow_extensions/outerbounds/plugins/vllm/status_card.py +352 -0
- metaflow_extensions/outerbounds/plugins/vllm/vllm_manager.py +621 -0
- metaflow_extensions/outerbounds/remote_config.py +27 -3
- metaflow_extensions/outerbounds/toplevel/global_aliases_for_metaflow_package.py +86 -2
- metaflow_extensions/outerbounds/toplevel/ob_internal.py +4 -0
- metaflow_extensions/outerbounds/toplevel/plugins/optuna/__init__.py +1 -0
- metaflow_extensions/outerbounds/toplevel/plugins/torchtune/__init__.py +1 -0
- metaflow_extensions/outerbounds/toplevel/plugins/vllm/__init__.py +1 -0
- metaflow_extensions/outerbounds/toplevel/s3_proxy.py +88 -0
- {ob_metaflow_extensions-1.1.151.dist-info → ob_metaflow_extensions-1.4.33.dist-info}/METADATA +2 -2
- ob_metaflow_extensions-1.4.33.dist-info/RECORD +134 -0
- metaflow_extensions/outerbounds/plugins/nim/utilities.py +0 -5
- ob_metaflow_extensions-1.1.151.dist-info/RECORD +0 -74
- {ob_metaflow_extensions-1.1.151.dist-info → ob_metaflow_extensions-1.4.33.dist-info}/WHEEL +0 -0
- {ob_metaflow_extensions-1.1.151.dist-info → ob_metaflow_extensions-1.4.33.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,868 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
from typing import Any, Dict, List, Optional, Union, Type, Callable
|
|
4
|
+
from ..click_importer import click
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class FieldBehavior:
|
|
8
|
+
"""
|
|
9
|
+
Defines how configuration fields behave when merging values from multiple sources.
|
|
10
|
+
|
|
11
|
+
FieldBehavior controls the merging logic when the same field receives values from
|
|
12
|
+
different configuration sources (CLI options, config files, environment variables).
|
|
13
|
+
This is crucial for maintaining consistent and predictable configuration behavior
|
|
14
|
+
across different deployment scenarios.
|
|
15
|
+
|
|
16
|
+
The behavior system allows fine-grained control over how different types of fields
|
|
17
|
+
should handle conflicting values, ensuring that sensitive configuration (like
|
|
18
|
+
dependency specifications) cannot be accidentally overridden while still allowing
|
|
19
|
+
flexible configuration for runtime parameters.
|
|
20
|
+
|
|
21
|
+
Behavior Types:
|
|
22
|
+
|
|
23
|
+
UNION (Default):
|
|
24
|
+
- **For Primitive Types**: Override value takes precedence
|
|
25
|
+
- **For Lists**: Values are merged by extending the base list with override values
|
|
26
|
+
- **For Dictionaries**: Values are merged by updating base dict with override values
|
|
27
|
+
- **For Nested Objects**: Recursively merge nested configuration objects
|
|
28
|
+
|
|
29
|
+
Example:
|
|
30
|
+
```python
|
|
31
|
+
# Base config: {"tags": ["prod", "web"]}
|
|
32
|
+
# CLI config: {"tags": ["urgent"]}
|
|
33
|
+
# Result: {"tags": ["prod", "web", "urgent"]}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
NOT_ALLOWED:
|
|
37
|
+
- CLI values cannot override config file values
|
|
38
|
+
- CLI values are only used if config file value is None
|
|
39
|
+
- Ensures critical configuration is only set in one place to avoid ambiguity.
|
|
40
|
+
|
|
41
|
+
Example:
|
|
42
|
+
```python
|
|
43
|
+
# Base config: {"dependencies": {"numpy": "1.21.0"}}
|
|
44
|
+
# CLI config: {"dependencies": {"numpy": "1.22.0"}}
|
|
45
|
+
# Result: Exception is raised
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
# Base config: {"dependencies": {"pypi": null, "conda": null}}
|
|
50
|
+
# CLI config: {"dependencies": {"pypi": {"numpy": "1.22.0"}}}
|
|
51
|
+
# Result: {"dependencies": {"pypi": {"numpy": "1.22.0"}}} # since there is nothing in base config, the CLI config is used.
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Integration with Merging:
|
|
55
|
+
The behavior is enforced by the `merge_field_values` function during configuration
|
|
56
|
+
merging. Each field's behavior is checked and the appropriate merging logic is applied.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
UNION = "union" # CLI values are merged with config file values
|
|
60
|
+
# CLI values are not allowed to ovveride the config values
|
|
61
|
+
# unless config values are not specified
|
|
62
|
+
NOT_ALLOWED = "not_allowed"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class CLIOption:
|
|
66
|
+
"""Metadata container for automatic CLI option generation from configuration fields.
|
|
67
|
+
|
|
68
|
+
CLIOption defines how a ConfigField should be exposed as a command-line option in the
|
|
69
|
+
generated CLI interface. It provides a declarative way to specify CLI parameter names,
|
|
70
|
+
help text, validation rules, and Click-specific behaviors without tightly coupling
|
|
71
|
+
configuration definitions to CLI implementation details.
|
|
72
|
+
|
|
73
|
+
This class bridges the gap between configuration field definitions and Click option
|
|
74
|
+
generation, allowing the same field definition to work seamlessly across different
|
|
75
|
+
interfaces (CLI, config files, programmatic usage).
|
|
76
|
+
|
|
77
|
+
Click Integration:
|
|
78
|
+
The CLIOption metadata is used by CLIGenerator to create Click options:
|
|
79
|
+
```python
|
|
80
|
+
@click.option("--port", "port", type=int, help="Application port")
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
This is automatically generated from:
|
|
84
|
+
```python
|
|
85
|
+
port = ConfigField(
|
|
86
|
+
cli_meta=CLIOption(
|
|
87
|
+
name="port",
|
|
88
|
+
cli_option_str="--port",
|
|
89
|
+
help="Application port"
|
|
90
|
+
),
|
|
91
|
+
field_type=int
|
|
92
|
+
)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
Parameters
|
|
97
|
+
----------
|
|
98
|
+
name : str
|
|
99
|
+
Parameter name used in Click option and function signature (e.g., "my_foo").
|
|
100
|
+
cli_option_str : str
|
|
101
|
+
Command-line option string (e.g., "--foo", "--enable/--disable").
|
|
102
|
+
help : Optional[str], optional
|
|
103
|
+
Help text displayed in CLI help output.
|
|
104
|
+
short : Optional[str], optional
|
|
105
|
+
Short option character (e.g., "-f" for "--foo").
|
|
106
|
+
multiple : bool, optional
|
|
107
|
+
Whether the option accepts multiple values.
|
|
108
|
+
is_flag : bool, optional
|
|
109
|
+
Whether this is a boolean flag option.
|
|
110
|
+
choices : Optional[List[str]], optional
|
|
111
|
+
List of valid choices for the option.
|
|
112
|
+
default : Any, optional
|
|
113
|
+
Default value for the CLI option (separate from ConfigField default).
|
|
114
|
+
hidden : bool, optional
|
|
115
|
+
Whether to hide this option from CLI (config file only).
|
|
116
|
+
click_type : Optional[Any], optional
|
|
117
|
+
Custom Click type for specialized parsing (e.g., KeyValuePair).
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
def __init__(
|
|
121
|
+
self,
|
|
122
|
+
name: str, # This name corresponds to the `"my_foo"` in `@click.option("--foo","my_foo")`
|
|
123
|
+
cli_option_str: str, # This corresponds to the `--foo` in the `@click.option("--foo","my_foo")`
|
|
124
|
+
help: Optional[str] = None,
|
|
125
|
+
short: Optional[str] = None,
|
|
126
|
+
multiple: bool = False,
|
|
127
|
+
is_flag: bool = False,
|
|
128
|
+
choices: Optional[List[str]] = None,
|
|
129
|
+
default: Any = None,
|
|
130
|
+
hidden: bool = False,
|
|
131
|
+
click_type: Optional[Any] = None,
|
|
132
|
+
):
|
|
133
|
+
self.name = name
|
|
134
|
+
self.cli_option_str = cli_option_str
|
|
135
|
+
self.help = help
|
|
136
|
+
self.short = short
|
|
137
|
+
self.multiple = multiple
|
|
138
|
+
self.is_flag = is_flag
|
|
139
|
+
self.choices = choices
|
|
140
|
+
self.default = default
|
|
141
|
+
self.hidden = hidden
|
|
142
|
+
self.click_type = click_type
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class ConfigField:
|
|
146
|
+
"""Descriptor for configuration fields with comprehensive metadata and behavior control.
|
|
147
|
+
|
|
148
|
+
ConfigField is a Python descriptor that provides a declarative way to define configuration
|
|
149
|
+
fields with rich metadata, validation, CLI integration, and merging behavior. It acts as
|
|
150
|
+
both a data descriptor (controlling get/set access) and a metadata container.
|
|
151
|
+
|
|
152
|
+
Key Functionality:
|
|
153
|
+
- **Descriptor Protocol**: Implements __get__, __set__, and __set_name__ to control
|
|
154
|
+
field access and automatically capture the field name during class creation.
|
|
155
|
+
- **Type Safety**: Optional strict type checking during value assignment.
|
|
156
|
+
- **CLI Integration**: Automatic CLI option generation via CLIOption metadata.
|
|
157
|
+
- **Validation**: Built-in validation functions and required field checks.
|
|
158
|
+
- **Merging Behavior**: Controls how values are merged from different sources (CLI, config files).
|
|
159
|
+
- **Default Values**: Supports both static defaults and callable defaults for dynamic initialization.
|
|
160
|
+
|
|
161
|
+
Merging Behaviors:
|
|
162
|
+
- **UNION**: Values from different sources are merged (lists extended, dicts updated).
|
|
163
|
+
- **NOT_ALLOWED**: Override values are ignored if base value exists.
|
|
164
|
+
|
|
165
|
+
Field Lifecycle:
|
|
166
|
+
1. **Definition**: Field is defined in a ConfigMeta-based class
|
|
167
|
+
2. **Registration**: __set_name__ is called to register the field name
|
|
168
|
+
3. **Initialization**: Field is initialized with None or nested config objects
|
|
169
|
+
4. **Population**: Values are set from CLI options, config files, or direct assignment
|
|
170
|
+
5. **Validation**: Validation functions are called during commit phase
|
|
171
|
+
6. **Default Application**: Default values are applied to None fields
|
|
172
|
+
|
|
173
|
+
Examples:
|
|
174
|
+
Basic field definition:
|
|
175
|
+
```python
|
|
176
|
+
name = ConfigField(
|
|
177
|
+
field_type=str,
|
|
178
|
+
required=True,
|
|
179
|
+
help="Application name",
|
|
180
|
+
example="myapp"
|
|
181
|
+
)
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Field with CLI integration:
|
|
185
|
+
```python
|
|
186
|
+
port = ConfigField(
|
|
187
|
+
cli_meta=CLIOption(
|
|
188
|
+
name="port",
|
|
189
|
+
cli_option_str="--port",
|
|
190
|
+
help="Application port"
|
|
191
|
+
),
|
|
192
|
+
field_type=int,
|
|
193
|
+
required=True,
|
|
194
|
+
validation_fn=lambda x: 1 <= x <= 65535
|
|
195
|
+
)
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Nested configuration field:
|
|
199
|
+
```python
|
|
200
|
+
resources = ConfigField(
|
|
201
|
+
field_type=ResourceConfig,
|
|
202
|
+
help="Resource configuration"
|
|
203
|
+
)
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Parameters
|
|
207
|
+
----------
|
|
208
|
+
default : Any or Callable[["ConfigField"], Any], optional
|
|
209
|
+
Default value for the field. Can be a static value or a callable for dynamic defaults.
|
|
210
|
+
cli_meta : CLIOption, optional
|
|
211
|
+
CLIOption instance defining CLI option generation parameters.
|
|
212
|
+
field_type : type, optional
|
|
213
|
+
Expected type of the field value (used for validation and nesting).
|
|
214
|
+
required : bool, optional
|
|
215
|
+
Whether the field must have a non-None value after configuration.
|
|
216
|
+
help : str, optional
|
|
217
|
+
Help text describing the field's purpose.
|
|
218
|
+
behavior : str, optional
|
|
219
|
+
FieldBehavior controlling how values are merged from different sources.
|
|
220
|
+
example : Any, optional
|
|
221
|
+
Example value for documentation and schema generation.
|
|
222
|
+
strict_types : bool, optional
|
|
223
|
+
Whether to enforce type checking during value assignment.
|
|
224
|
+
validation_fn : callable, optional
|
|
225
|
+
Optional function to validate field values.
|
|
226
|
+
is_experimental : bool, optional
|
|
227
|
+
Whether this field is experimental (for documentation).
|
|
228
|
+
"""
|
|
229
|
+
|
|
230
|
+
def __init__(
|
|
231
|
+
self,
|
|
232
|
+
default: Union[Any, Callable[["ConfigField"], Any]] = None,
|
|
233
|
+
cli_meta=None,
|
|
234
|
+
field_type=None,
|
|
235
|
+
required=False,
|
|
236
|
+
help=None,
|
|
237
|
+
behavior: str = FieldBehavior.UNION,
|
|
238
|
+
example=None,
|
|
239
|
+
strict_types=True,
|
|
240
|
+
validation_fn: Optional[Callable] = None,
|
|
241
|
+
is_experimental=False, # This property is for bookkeeping purposes and for export in schema.
|
|
242
|
+
parsing_fn: Optional[Callable] = None,
|
|
243
|
+
):
|
|
244
|
+
if behavior == FieldBehavior.NOT_ALLOWED and ConfigMeta.is_instance(field_type):
|
|
245
|
+
raise ValueError(
|
|
246
|
+
"NOT_ALLOWED behavior cannot be set for ConfigMeta-based objects."
|
|
247
|
+
)
|
|
248
|
+
if callable(default) and not ConfigMeta.is_instance(field_type):
|
|
249
|
+
raise ValueError(
|
|
250
|
+
f"Default value for {field_type} is a callable but it is not a ConfigMeta-based object."
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
self.default = default
|
|
254
|
+
self.cli_meta = cli_meta
|
|
255
|
+
self.field_type = field_type
|
|
256
|
+
self.strict_types = strict_types
|
|
257
|
+
# Strict types means that the __set__ will raise an error if the type of the valu
|
|
258
|
+
# doesn't match the type of the field.
|
|
259
|
+
self.required = required
|
|
260
|
+
self.help = help
|
|
261
|
+
self.behavior = behavior
|
|
262
|
+
self.example = example
|
|
263
|
+
self.name = None
|
|
264
|
+
self.validation_fn = validation_fn
|
|
265
|
+
self.is_experimental = is_experimental
|
|
266
|
+
self.parsing_fn = parsing_fn
|
|
267
|
+
self._qual_name_stack = []
|
|
268
|
+
|
|
269
|
+
# This function allows config fields to be made aware of the
|
|
270
|
+
# owner instance's names. It's called from the `commit_owner_names_across_tree`
|
|
271
|
+
# Decorator. Its called once the config instance is completely ready and
|
|
272
|
+
# it wil not have any further runtime instance modifications done to it.
|
|
273
|
+
# The core intent is to ensure that the full config lineage tree is captured and
|
|
274
|
+
# we have a full trace of where the config is coming from so that we can showcase it
|
|
275
|
+
# to users when they make configurational errors. It also allows us to reference those
|
|
276
|
+
# config values in the error messages across different types of errors.
|
|
277
|
+
def _set_owner_name(self, owner_name: str):
|
|
278
|
+
self._qual_name_stack.append(owner_name)
|
|
279
|
+
|
|
280
|
+
def fully_qualified_name(self):
|
|
281
|
+
return ".".join(self._qual_name_stack + [self.name])
|
|
282
|
+
|
|
283
|
+
def __set_name__(self, owner, name):
|
|
284
|
+
self.name = name
|
|
285
|
+
|
|
286
|
+
def __get__(self, instance, owner):
|
|
287
|
+
if instance is None:
|
|
288
|
+
return self
|
|
289
|
+
# DEFAULTS need to be explicilty set to ensure that
|
|
290
|
+
# only explicitly set values are return on get
|
|
291
|
+
return instance.__dict__.get(self.name)
|
|
292
|
+
|
|
293
|
+
def __set__(self, instance, value):
|
|
294
|
+
if self.parsing_fn:
|
|
295
|
+
value = self.parsing_fn(value)
|
|
296
|
+
|
|
297
|
+
# TODO: handle this exception at top level if necessary.
|
|
298
|
+
if value is not None and self.strict_types and self.field_type is not None:
|
|
299
|
+
if not isinstance(value, self.field_type):
|
|
300
|
+
raise ValueError(
|
|
301
|
+
f"Value {value} is not of type {self.field_type} for the field {self.name}"
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
instance.__dict__[self.name] = value
|
|
305
|
+
|
|
306
|
+
def __str__(self) -> str:
|
|
307
|
+
type_name = (
|
|
308
|
+
getattr(self.field_type, "__name__", "typing.Any")
|
|
309
|
+
if self.field_type
|
|
310
|
+
else "typing.Any"
|
|
311
|
+
)
|
|
312
|
+
return f"<ConfigField name='{self.name}' type={type_name} default={self.default!r}>"
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
# Add this decorator function before the ConfigMeta class
|
|
316
|
+
# One of the core utilities of the ConfigMeta class
|
|
317
|
+
# is that we can track the tree of elements in the Config
|
|
318
|
+
# class that allow us to make those visible at runtime when
|
|
319
|
+
# the user has some configurational error. it also allows us to
|
|
320
|
+
# Figure out what the user is trying to extactly configure and where
|
|
321
|
+
# that configuration is coming from.
|
|
322
|
+
# Hence this decorator is set on what ever configMeta class based function
|
|
323
|
+
# so that when it gets called, the full call tree is properly set.
|
|
324
|
+
def commit_owner_names_across_tree(func):
|
|
325
|
+
"""
|
|
326
|
+
Decorator that commits owner names across the configuration tree before executing the decorated function.
|
|
327
|
+
|
|
328
|
+
This decorator ensures that all ConfigField instances in the configuration tree are aware of their
|
|
329
|
+
fully qualified names by traversing the tree and calling _set_owner_name on each field.
|
|
330
|
+
"""
|
|
331
|
+
|
|
332
|
+
def wrapper(self, *args, **kwargs):
|
|
333
|
+
def _commit_owner_names_recursive(instance, field_name_stack=None):
|
|
334
|
+
if field_name_stack is None:
|
|
335
|
+
field_name_stack = []
|
|
336
|
+
|
|
337
|
+
if not ConfigMeta.is_instance(instance):
|
|
338
|
+
return
|
|
339
|
+
|
|
340
|
+
fields = instance._fields # type: ignore
|
|
341
|
+
# fields is a dictionary of field_name: ConfigField
|
|
342
|
+
for field_name, field_info in fields.items():
|
|
343
|
+
if ConfigMeta.is_instance(field_info.field_type):
|
|
344
|
+
# extract the actual instance of the ConfigMeta class
|
|
345
|
+
_instance = instance.__dict__[field_name]
|
|
346
|
+
# The instance should hold the _commit_owner_names_across_tree
|
|
347
|
+
_commit_owner_names_recursive(
|
|
348
|
+
_instance, field_name_stack + [field_name]
|
|
349
|
+
)
|
|
350
|
+
else:
|
|
351
|
+
if len(field_name_stack) > 0:
|
|
352
|
+
for x in field_name_stack:
|
|
353
|
+
field_info._set_owner_name(x) # type: ignore
|
|
354
|
+
|
|
355
|
+
# Commit owner names before executing the original function
|
|
356
|
+
_commit_owner_names_recursive(self)
|
|
357
|
+
|
|
358
|
+
# Execute the original function
|
|
359
|
+
return func(self, *args, **kwargs)
|
|
360
|
+
|
|
361
|
+
return wrapper
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
class ConfigMeta(type):
|
|
365
|
+
"""Metaclass implementing the configuration system's class transformation layer.
|
|
366
|
+
|
|
367
|
+
This metaclass exists to solve the fundamental problem of creating a declarative configuration
|
|
368
|
+
system that can automatically generate runtime behavior from field definitions. Without a
|
|
369
|
+
metaclass, each configuration class would need to manually implement field discovery,
|
|
370
|
+
validation, CLI integration, and nested object handling, leading to boilerplate code and
|
|
371
|
+
inconsistent behavior across the system.
|
|
372
|
+
|
|
373
|
+
Technical Implementation:
|
|
374
|
+
|
|
375
|
+
During class creation (__new__), this metaclass intercepts the class namespace and performs
|
|
376
|
+
several critical transformations:
|
|
377
|
+
|
|
378
|
+
1. Field Discovery: Scans the class namespace for ConfigField instances and extracts their
|
|
379
|
+
metadata into a `_fields` registry. This registry becomes the source of truth for all
|
|
380
|
+
runtime operations including validation, CLI generation, and serialization.
|
|
381
|
+
|
|
382
|
+
2. Method Injection: Adds the `_get_field` method to enable programmatic access to field
|
|
383
|
+
metadata. This method is used throughout the system by validation functions, CLI
|
|
384
|
+
generators, and configuration mergers.
|
|
385
|
+
|
|
386
|
+
3. __init__ Override: Replaces the class's __init__ method with a standardized version that
|
|
387
|
+
handles three critical initialization phases:
|
|
388
|
+
- Field initialization to None (explicit defaulting happens later via apply_defaults)
|
|
389
|
+
- Nested config object instantiation for ConfigMeta-based field types
|
|
390
|
+
- Keyword argument processing for programmatic configuration
|
|
391
|
+
|
|
392
|
+
System Integration and Lifecycle:
|
|
393
|
+
|
|
394
|
+
The metaclass integrates with the broader configuration system through several key interfaces:
|
|
395
|
+
|
|
396
|
+
- populate_config_recursive: Uses the _fields registry to map external data sources
|
|
397
|
+
(CLI options, config files) to object attributes
|
|
398
|
+
- apply_defaults: Traverses the _fields registry to apply default values after population
|
|
399
|
+
- validate_config_meta: Uses field metadata to execute validation functions
|
|
400
|
+
- merge_field_values: Consults field behavior settings to determine merge strategies
|
|
401
|
+
- config_meta_to_dict: Converts instances back to dictionaries for serialization
|
|
402
|
+
|
|
403
|
+
Lifecycle Phases:
|
|
404
|
+
|
|
405
|
+
1. Class Definition: Metaclass transforms the class, creating _fields registry
|
|
406
|
+
2. Instance Creation: Auto-generated __init__ initializes fields and nested objects
|
|
407
|
+
3. Population: External systems use _fields to populate from CLI/config files
|
|
408
|
+
4. Validation: Field metadata drives validation and required field checking
|
|
409
|
+
5. Default Application: Fields with None values receive their defaults
|
|
410
|
+
6. Runtime Usage: Descriptor protocol provides controlled field access
|
|
411
|
+
|
|
412
|
+
Why a Metaclass:
|
|
413
|
+
|
|
414
|
+
The alternatives to a metaclass would be:
|
|
415
|
+
- Manual field registration in each class (error-prone, inconsistent)
|
|
416
|
+
- Inheritance-based approach (doesn't solve the field discovery problem)
|
|
417
|
+
- Decorator-based approach (requires manual application, less automatic)
|
|
418
|
+
- Runtime introspection (performance overhead, less reliable)
|
|
419
|
+
|
|
420
|
+
The metaclass provides automatic, consistent behavior while maintaining the declarative
|
|
421
|
+
syntax that makes configuration classes readable and maintainable.
|
|
422
|
+
|
|
423
|
+
Usage Pattern:
|
|
424
|
+
```python
|
|
425
|
+
class MyConfig(metaclass=ConfigMeta):
|
|
426
|
+
name = ConfigField(field_type=str, required=True)
|
|
427
|
+
port = ConfigField(field_type=int, default=8080)
|
|
428
|
+
resources = ConfigField(field_type=ResourceConfig)
|
|
429
|
+
```
|
|
430
|
+
"""
|
|
431
|
+
|
|
432
|
+
@staticmethod
|
|
433
|
+
def is_instance(value) -> bool:
|
|
434
|
+
return hasattr(value, "_fields")
|
|
435
|
+
|
|
436
|
+
def __new__(mcs, name, bases, namespace):
|
|
437
|
+
# Collect field metadata
|
|
438
|
+
fields = {}
|
|
439
|
+
for key, value in namespace.items():
|
|
440
|
+
if isinstance(value, ConfigField):
|
|
441
|
+
fields[key] = value
|
|
442
|
+
|
|
443
|
+
# Store fields metadata on the class
|
|
444
|
+
namespace["_fields"] = fields
|
|
445
|
+
|
|
446
|
+
# Inject a function to get configField from a field name
|
|
447
|
+
def get_field(cls, field_name: str) -> ConfigField:
|
|
448
|
+
return fields[field_name]
|
|
449
|
+
|
|
450
|
+
namespace["_get_field"] = get_field
|
|
451
|
+
|
|
452
|
+
# Auto-generate __init__ method;
|
|
453
|
+
# Override it for all classes.
|
|
454
|
+
def __init__(self, **kwargs):
|
|
455
|
+
# Initialize all fields with Nones or other values
|
|
456
|
+
# We initiaze with None because defaulting should be a
|
|
457
|
+
# constant behavior
|
|
458
|
+
for field_name, field_info in fields.items():
|
|
459
|
+
default_value = None
|
|
460
|
+
|
|
461
|
+
# Handle nested config objects
|
|
462
|
+
if ConfigMeta.is_instance(field_info.field_type):
|
|
463
|
+
# Create nested config instance with defaults
|
|
464
|
+
default_value = field_info.field_type()
|
|
465
|
+
|
|
466
|
+
# Set from kwargs or use default
|
|
467
|
+
if field_name in kwargs:
|
|
468
|
+
setattr(self, field_name, kwargs[field_name])
|
|
469
|
+
else:
|
|
470
|
+
setattr(self, field_name, default_value)
|
|
471
|
+
|
|
472
|
+
return super().__new__(mcs, name, bases, namespace)
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def apply_defaults(config) -> None:
|
|
476
|
+
"""
|
|
477
|
+
Apply default values to any fields that are still None.
|
|
478
|
+
|
|
479
|
+
Args:
|
|
480
|
+
config: instance of a ConfigMeta object
|
|
481
|
+
"""
|
|
482
|
+
for field_name, field_info in config._fields.items():
|
|
483
|
+
current_value = getattr(config, field_name, None)
|
|
484
|
+
|
|
485
|
+
if current_value is None:
|
|
486
|
+
# The nested configs will never be set to None
|
|
487
|
+
# Since we always override the init function.
|
|
488
|
+
# The init function will always instantiate the
|
|
489
|
+
# sub-objects under the class
|
|
490
|
+
# Set default value for regular fields
|
|
491
|
+
setattr(config, field_name, field_info.default)
|
|
492
|
+
elif ConfigMeta.is_instance(field_info.field_type) and ConfigMeta.is_instance(
|
|
493
|
+
current_value
|
|
494
|
+
):
|
|
495
|
+
|
|
496
|
+
# Apply defaults to nested config (to any sub values that might need it)
|
|
497
|
+
apply_defaults(current_value)
|
|
498
|
+
# also apply defaults to the current value if a defaults callable is provided.
|
|
499
|
+
# Certain top level config fields might require default setting based on the
|
|
500
|
+
# what the current value in the fields is set.
|
|
501
|
+
if field_info.default:
|
|
502
|
+
field_info.default(current_value)
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
class ConfigValidationFailedException(Exception):
|
|
506
|
+
def __init__(
|
|
507
|
+
self,
|
|
508
|
+
field_name: str,
|
|
509
|
+
field_info: ConfigField,
|
|
510
|
+
current_value,
|
|
511
|
+
message: Optional[str] = None,
|
|
512
|
+
):
|
|
513
|
+
self.field_name = field_name
|
|
514
|
+
self.field_info = field_info
|
|
515
|
+
self.current_value = current_value
|
|
516
|
+
self.message = (
|
|
517
|
+
f"Validation failed for field {field_name} with value {current_value}"
|
|
518
|
+
)
|
|
519
|
+
if message is not None:
|
|
520
|
+
self.message = message
|
|
521
|
+
|
|
522
|
+
suffix = "\n\tThis configuration is set via the the following interfaces:\n\n"
|
|
523
|
+
suffix += "\t\t1. Config file: `%s`\n" % field_info.fully_qualified_name()
|
|
524
|
+
suffix += (
|
|
525
|
+
"\t\t2. Programatic API (Python): `%s`\n"
|
|
526
|
+
% field_info.fully_qualified_name()
|
|
527
|
+
)
|
|
528
|
+
if field_info.cli_meta:
|
|
529
|
+
suffix += "\t\t3. CLI: `%s`\n" % field_info.cli_meta.cli_option_str
|
|
530
|
+
|
|
531
|
+
self.message += suffix
|
|
532
|
+
|
|
533
|
+
super().__init__(self.message)
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
class RequiredFieldMissingException(ConfigValidationFailedException):
|
|
537
|
+
pass
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
class MergingNotAllowedFieldsException(ConfigValidationFailedException):
|
|
541
|
+
def __init__(
|
|
542
|
+
self,
|
|
543
|
+
field_name: str,
|
|
544
|
+
field_info: ConfigField,
|
|
545
|
+
current_value: Any,
|
|
546
|
+
override_value: Any,
|
|
547
|
+
):
|
|
548
|
+
super().__init__(
|
|
549
|
+
field_name=field_name,
|
|
550
|
+
field_info=field_info,
|
|
551
|
+
current_value=current_value,
|
|
552
|
+
message=f"Merging not allowed for field {field_name} with value {current_value} and override value {override_value}",
|
|
553
|
+
)
|
|
554
|
+
self.override_value = override_value
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
def validate_required_fields(config_instance):
|
|
558
|
+
for field_name, field_info in config_instance._fields.items():
|
|
559
|
+
if field_info.required:
|
|
560
|
+
current_value = getattr(config_instance, field_name, None)
|
|
561
|
+
if current_value is None:
|
|
562
|
+
raise RequiredFieldMissingException(
|
|
563
|
+
field_name, field_info, current_value
|
|
564
|
+
)
|
|
565
|
+
if ConfigMeta.is_instance(field_info.field_type) and ConfigMeta.is_instance(
|
|
566
|
+
current_value
|
|
567
|
+
):
|
|
568
|
+
validate_required_fields(current_value)
|
|
569
|
+
# TODO: Fix the exception handling over here.
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def validate_config_meta(config_instance):
|
|
573
|
+
for field_name, field_info in config_instance._fields.items():
|
|
574
|
+
current_value = getattr(config_instance, field_name, None)
|
|
575
|
+
|
|
576
|
+
if ConfigMeta.is_instance(field_info.field_type) and ConfigMeta.is_instance(
|
|
577
|
+
current_value
|
|
578
|
+
):
|
|
579
|
+
validate_config_meta(current_value)
|
|
580
|
+
|
|
581
|
+
if field_info.validation_fn:
|
|
582
|
+
if not field_info.validation_fn(current_value):
|
|
583
|
+
raise ConfigValidationFailedException(
|
|
584
|
+
field_name, field_info, current_value
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def config_meta_to_dict(config_instance) -> Optional[Dict[str, Any]]:
|
|
589
|
+
"""Convert a configuration instance to a nested dictionary.
|
|
590
|
+
|
|
591
|
+
Recursively converts ConfigMeta-based configuration instances to dictionaries,
|
|
592
|
+
handling nested config objects and preserving the structure.
|
|
593
|
+
|
|
594
|
+
Args:
|
|
595
|
+
config_instance: Instance of a ConfigMeta-based configuration class
|
|
596
|
+
|
|
597
|
+
Returns:
|
|
598
|
+
Nested dictionary representation of the configuration
|
|
599
|
+
|
|
600
|
+
Examples:
|
|
601
|
+
# Convert a config instance to dict
|
|
602
|
+
|
|
603
|
+
config_dict = to_dict(config)
|
|
604
|
+
|
|
605
|
+
# Result will be:
|
|
606
|
+
# {
|
|
607
|
+
# "name": "myapp",
|
|
608
|
+
# "port": 8000,
|
|
609
|
+
# "resources": {
|
|
610
|
+
# "cpu": "500m",
|
|
611
|
+
# "memory": "1Gi",
|
|
612
|
+
# "gpu": None,
|
|
613
|
+
# "disk": "20Gi"
|
|
614
|
+
# },
|
|
615
|
+
# "auth": None,
|
|
616
|
+
# ...
|
|
617
|
+
# }
|
|
618
|
+
"""
|
|
619
|
+
if config_instance is None:
|
|
620
|
+
return None
|
|
621
|
+
|
|
622
|
+
# Check if this is a ConfigMeta-based class
|
|
623
|
+
if not ConfigMeta.is_instance(config_instance):
|
|
624
|
+
# If it's not a config object, return as-is
|
|
625
|
+
return config_instance
|
|
626
|
+
|
|
627
|
+
result = {}
|
|
628
|
+
|
|
629
|
+
# Iterate through all fields defined in the class
|
|
630
|
+
for field_name, field_info in config_instance._fields.items():
|
|
631
|
+
# Get the current value
|
|
632
|
+
value = getattr(config_instance, field_name, None)
|
|
633
|
+
|
|
634
|
+
# Handle nested config objects recursively
|
|
635
|
+
if value is not None and ConfigMeta.is_instance(value):
|
|
636
|
+
# It's a nested config object
|
|
637
|
+
result[field_name] = config_meta_to_dict(value)
|
|
638
|
+
elif isinstance(value, list) and value:
|
|
639
|
+
# Handle lists that might contain config objects
|
|
640
|
+
result[field_name] = [
|
|
641
|
+
config_meta_to_dict(item) if ConfigMeta.is_instance(item) else item
|
|
642
|
+
for item in value
|
|
643
|
+
]
|
|
644
|
+
elif isinstance(value, dict) and value:
|
|
645
|
+
# Handle dictionaries that might contain config objects
|
|
646
|
+
result[field_name] = {
|
|
647
|
+
k: config_meta_to_dict(v) if ConfigMeta.is_instance(v) else v
|
|
648
|
+
for k, v in value.items()
|
|
649
|
+
}
|
|
650
|
+
else:
|
|
651
|
+
# Primitive type or None
|
|
652
|
+
result[field_name] = value
|
|
653
|
+
|
|
654
|
+
return result
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
def merge_field_values(
|
|
658
|
+
base_value: Any, override_value: Any, field_info, behavior: str
|
|
659
|
+
) -> Any:
|
|
660
|
+
"""
|
|
661
|
+
Merge individual field values based on behavior.
|
|
662
|
+
|
|
663
|
+
Args:
|
|
664
|
+
base_value: Value from base config
|
|
665
|
+
override_value: Value from override config
|
|
666
|
+
field_info: Field metadata
|
|
667
|
+
behavior: FieldBehavior for this field
|
|
668
|
+
|
|
669
|
+
Returns:
|
|
670
|
+
Merged value
|
|
671
|
+
"""
|
|
672
|
+
# Handle NOT_ALLOWED behavior
|
|
673
|
+
if behavior == FieldBehavior.NOT_ALLOWED:
|
|
674
|
+
if base_value is not None and override_value is not None:
|
|
675
|
+
raise MergingNotAllowedFieldsException(
|
|
676
|
+
field_name=field_info.name,
|
|
677
|
+
field_info=field_info,
|
|
678
|
+
current_value=base_value,
|
|
679
|
+
override_value=override_value,
|
|
680
|
+
)
|
|
681
|
+
if base_value is None:
|
|
682
|
+
return override_value
|
|
683
|
+
return base_value
|
|
684
|
+
|
|
685
|
+
# Handle UNION behavior (default)
|
|
686
|
+
if behavior == FieldBehavior.UNION:
|
|
687
|
+
# If override is None, use base value
|
|
688
|
+
if override_value is None:
|
|
689
|
+
return (
|
|
690
|
+
base_value if base_value is not None else None
|
|
691
|
+
) # We will not set defaults!
|
|
692
|
+
|
|
693
|
+
# If base is None, use override value
|
|
694
|
+
if base_value is None:
|
|
695
|
+
return override_value
|
|
696
|
+
|
|
697
|
+
# Handle nested config objects
|
|
698
|
+
if ConfigMeta.is_instance(field_info.field_type):
|
|
699
|
+
if isinstance(base_value, field_info.field_type) and isinstance(
|
|
700
|
+
override_value, field_info.field_type
|
|
701
|
+
):
|
|
702
|
+
# Merge nested configs recursively
|
|
703
|
+
merged_nested = field_info.field_type()
|
|
704
|
+
for (
|
|
705
|
+
nested_field_name,
|
|
706
|
+
nested_field_info,
|
|
707
|
+
) in field_info.field_type._fields.items():
|
|
708
|
+
nested_base = getattr(base_value, nested_field_name, None)
|
|
709
|
+
nested_override = getattr(override_value, nested_field_name, None)
|
|
710
|
+
nested_behavior = getattr(
|
|
711
|
+
nested_field_info, "behavior", FieldBehavior.UNION
|
|
712
|
+
)
|
|
713
|
+
|
|
714
|
+
merged_nested_value = merge_field_values(
|
|
715
|
+
nested_base, nested_override, nested_field_info, nested_behavior
|
|
716
|
+
)
|
|
717
|
+
setattr(merged_nested, nested_field_name, merged_nested_value)
|
|
718
|
+
|
|
719
|
+
return merged_nested
|
|
720
|
+
else:
|
|
721
|
+
# One is not a config object, use override
|
|
722
|
+
return override_value
|
|
723
|
+
|
|
724
|
+
# Handle lists (union behavior merges lists)
|
|
725
|
+
if isinstance(base_value, list) and isinstance(override_value, list):
|
|
726
|
+
# Merge lists by extending
|
|
727
|
+
merged_list = base_value.copy()
|
|
728
|
+
merged_list.extend(override_value)
|
|
729
|
+
return merged_list
|
|
730
|
+
|
|
731
|
+
# Handle dicts (union behavior merges dicts)
|
|
732
|
+
if isinstance(base_value, dict) and isinstance(override_value, dict):
|
|
733
|
+
merged_dict = base_value.copy()
|
|
734
|
+
merged_dict.update(override_value)
|
|
735
|
+
return merged_dict
|
|
736
|
+
|
|
737
|
+
# For other types, override takes precedence
|
|
738
|
+
return override_value
|
|
739
|
+
|
|
740
|
+
# Default: override takes precedence
|
|
741
|
+
return (
|
|
742
|
+
override_value
|
|
743
|
+
if override_value is not None
|
|
744
|
+
else (base_value if base_value is not None else None)
|
|
745
|
+
)
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
class JsonFriendlyKeyValuePair(click.ParamType): # type: ignore
|
|
749
|
+
name = "KV-PAIR" # type: ignore
|
|
750
|
+
|
|
751
|
+
def convert(self, value, param, ctx):
|
|
752
|
+
# Parse a string of the form KEY=VALUE into a dict {KEY: VALUE}
|
|
753
|
+
if len(value.split("=", 1)) != 2:
|
|
754
|
+
self.fail(
|
|
755
|
+
f"Invalid format for {value}. Expected format: KEY=VALUE", param, ctx
|
|
756
|
+
)
|
|
757
|
+
|
|
758
|
+
key, _value = value.split("=", 1)
|
|
759
|
+
try:
|
|
760
|
+
return {key: json.loads(_value)}
|
|
761
|
+
except json.JSONDecodeError:
|
|
762
|
+
return {key: _value}
|
|
763
|
+
except Exception as e:
|
|
764
|
+
self.fail(f"Invalid value for {value}. Error: {e}", param, ctx)
|
|
765
|
+
|
|
766
|
+
def __str__(self):
|
|
767
|
+
return repr(self)
|
|
768
|
+
|
|
769
|
+
def __repr__(self):
|
|
770
|
+
return "KV-PAIR"
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
class CommaSeparatedList(click.ParamType): # type: ignore
|
|
774
|
+
name = "COMMA-SEPARATED-LIST" # type: ignore
|
|
775
|
+
|
|
776
|
+
def convert(self, value, param, ctx):
|
|
777
|
+
if isinstance(value, list):
|
|
778
|
+
return value
|
|
779
|
+
if isinstance(value, str):
|
|
780
|
+
return [item.strip() for item in value.split(",") if item.strip()]
|
|
781
|
+
return value
|
|
782
|
+
|
|
783
|
+
def __str__(self):
|
|
784
|
+
return repr(self)
|
|
785
|
+
|
|
786
|
+
def __repr__(self):
|
|
787
|
+
return "COMMA-SEPARATED-LIST"
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
class PureStringKVPair(click.ParamType): # type: ignore
|
|
791
|
+
"""Click type for key-value pairs (KEY=VALUE)."""
|
|
792
|
+
|
|
793
|
+
name = "key=value"
|
|
794
|
+
|
|
795
|
+
def convert(self, value, param, ctx):
|
|
796
|
+
if isinstance(value, dict):
|
|
797
|
+
return value
|
|
798
|
+
try:
|
|
799
|
+
key, val = value.split("=", 1)
|
|
800
|
+
return {key: val}
|
|
801
|
+
except ValueError:
|
|
802
|
+
self.fail(f"'{value}' is not a valid key=value pair", param, ctx)
|
|
803
|
+
|
|
804
|
+
|
|
805
|
+
PureStringKVPairType = PureStringKVPair()
|
|
806
|
+
CommaSeparatedListType = CommaSeparatedList()
|
|
807
|
+
JsonFriendlyKeyValuePairType = JsonFriendlyKeyValuePair()
|
|
808
|
+
|
|
809
|
+
|
|
810
|
+
def populate_config_recursive(
|
|
811
|
+
config_instance,
|
|
812
|
+
config_class,
|
|
813
|
+
source_data,
|
|
814
|
+
get_source_key_fn,
|
|
815
|
+
get_source_value_fn,
|
|
816
|
+
):
|
|
817
|
+
"""
|
|
818
|
+
Recursively populate a config instance from source data.
|
|
819
|
+
|
|
820
|
+
Args:
|
|
821
|
+
config_instance: Config object to populate
|
|
822
|
+
config_class: Class of the config object
|
|
823
|
+
source_data: Source data (dict, CLI options, etc.)
|
|
824
|
+
get_source_key_fn: Function to get the source key for a field
|
|
825
|
+
get_source_value_fn: Function to get the value from source for a key
|
|
826
|
+
"""
|
|
827
|
+
|
|
828
|
+
for field_name, field_info in config_class._fields.items():
|
|
829
|
+
# When we populate the ConfigMeta based objects, we want to do the following:
|
|
830
|
+
# If we find some key associated to the object inside the source data, then we populate the object
|
|
831
|
+
# with it. If that key corresponds to a nested config meta object then we just recusively pass down the
|
|
832
|
+
# value of the key from source data and populate the nested object other wise just end up setting the object
|
|
833
|
+
source_key = get_source_key_fn(field_name, field_info)
|
|
834
|
+
if source_key and source_key in source_data:
|
|
835
|
+
value = get_source_value_fn(source_data, source_key)
|
|
836
|
+
if value is not None:
|
|
837
|
+
# Handle nested config objects (for dict sources with nested data)
|
|
838
|
+
if ConfigMeta.is_instance(field_info.field_type) and isinstance(
|
|
839
|
+
value, dict
|
|
840
|
+
):
|
|
841
|
+
nested_config = field_info.field_type()
|
|
842
|
+
populate_config_recursive(
|
|
843
|
+
nested_config,
|
|
844
|
+
field_info.field_type,
|
|
845
|
+
value, # For dict, use the nested dict as source
|
|
846
|
+
get_source_key_fn,
|
|
847
|
+
get_source_value_fn,
|
|
848
|
+
)
|
|
849
|
+
setattr(config_instance, field_name, nested_config)
|
|
850
|
+
else:
|
|
851
|
+
# Direct value assignment for regular fields
|
|
852
|
+
setattr(config_instance, field_name, value)
|
|
853
|
+
elif ConfigMeta.is_instance(field_info.field_type):
|
|
854
|
+
# It might be possible that the source key that respresents a subfield is
|
|
855
|
+
# is present at the root level but there is no field pertainig to the ConfigMeta
|
|
856
|
+
# itself. So always recurisvely check the subtree and no matter what keep instantiating
|
|
857
|
+
# any nested config objects.
|
|
858
|
+
_nested_config = field_info.field_type()
|
|
859
|
+
populate_config_recursive(
|
|
860
|
+
_nested_config,
|
|
861
|
+
field_info.field_type,
|
|
862
|
+
source_data,
|
|
863
|
+
get_source_key_fn,
|
|
864
|
+
get_source_value_fn,
|
|
865
|
+
)
|
|
866
|
+
setattr(config_instance, field_name, _nested_config)
|
|
867
|
+
else:
|
|
868
|
+
pass
|