ob-metaflow-extensions 1.1.130__py2.py3-none-any.whl → 1.5.1__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.
Potentially problematic release.
This version of ob-metaflow-extensions might be problematic. Click here for more details.
- metaflow_extensions/outerbounds/__init__.py +1 -1
- metaflow_extensions/outerbounds/plugins/__init__.py +34 -4
- metaflow_extensions/outerbounds/plugins/apps/__init__.py +0 -0
- metaflow_extensions/outerbounds/plugins/apps/app_cli.py +0 -0
- metaflow_extensions/outerbounds/plugins/apps/app_utils.py +187 -0
- metaflow_extensions/outerbounds/plugins/apps/consts.py +3 -0
- metaflow_extensions/outerbounds/plugins/apps/core/__init__.py +15 -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_config.py +128 -0
- metaflow_extensions/outerbounds/plugins/apps/core/app_deploy_decorator.py +330 -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 +15 -0
- metaflow_extensions/outerbounds/plugins/apps/core/config/cli_generator.py +165 -0
- metaflow_extensions/outerbounds/plugins/apps/core/config/config_utils.py +966 -0
- metaflow_extensions/outerbounds/plugins/apps/core/config/schema_export.py +299 -0
- metaflow_extensions/outerbounds/plugins/apps/core/config/typed_configs.py +233 -0
- metaflow_extensions/outerbounds/plugins/apps/core/config/typed_init_generator.py +537 -0
- metaflow_extensions/outerbounds/plugins/apps/core/config/unified_config.py +1125 -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 +959 -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/apps/deploy_decorator.py +201 -0
- metaflow_extensions/outerbounds/plugins/apps/supervisord_utils.py +243 -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 +118 -0
- metaflow_extensions/outerbounds/plugins/card_utilities/injector.py +1 -1
- metaflow_extensions/outerbounds/plugins/checkpoint_datastores/__init__.py +2 -0
- metaflow_extensions/outerbounds/plugins/checkpoint_datastores/coreweave.py +71 -0
- metaflow_extensions/outerbounds/plugins/checkpoint_datastores/external_chckpt.py +85 -0
- metaflow_extensions/outerbounds/plugins/checkpoint_datastores/nebius.py +73 -0
- metaflow_extensions/outerbounds/plugins/fast_bakery/baker.py +110 -0
- metaflow_extensions/outerbounds/plugins/fast_bakery/docker_environment.py +43 -9
- metaflow_extensions/outerbounds/plugins/fast_bakery/fast_bakery.py +12 -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 +2 -16
- 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/nvcf/nvcf.py +100 -19
- metaflow_extensions/outerbounds/plugins/nvcf/nvcf_decorator.py +6 -1
- metaflow_extensions/outerbounds/plugins/nvct/__init__.py +0 -0
- metaflow_extensions/outerbounds/plugins/nvct/exceptions.py +71 -0
- metaflow_extensions/outerbounds/plugins/nvct/nvct.py +131 -0
- metaflow_extensions/outerbounds/plugins/nvct/nvct_cli.py +289 -0
- metaflow_extensions/outerbounds/plugins/nvct/nvct_decorator.py +286 -0
- metaflow_extensions/outerbounds/plugins/nvct/nvct_runner.py +218 -0
- metaflow_extensions/outerbounds/plugins/nvct/utils.py +29 -0
- metaflow_extensions/outerbounds/plugins/ollama/__init__.py +225 -0
- 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 +1924 -0
- 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/secrets/secrets.py +38 -2
- metaflow_extensions/outerbounds/plugins/snowflake/snowflake.py +81 -11
- metaflow_extensions/outerbounds/plugins/snowpark/snowpark.py +18 -8
- metaflow_extensions/outerbounds/plugins/snowpark/snowpark_cli.py +6 -0
- metaflow_extensions/outerbounds/plugins/snowpark/snowpark_client.py +45 -18
- metaflow_extensions/outerbounds/plugins/snowpark/snowpark_decorator.py +18 -9
- metaflow_extensions/outerbounds/plugins/snowpark/snowpark_job.py +10 -4
- 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 +46 -9
- metaflow_extensions/outerbounds/toplevel/global_aliases_for_metaflow_package.py +94 -2
- metaflow_extensions/outerbounds/toplevel/ob_internal.py +4 -0
- metaflow_extensions/outerbounds/toplevel/plugins/ollama/__init__.py +1 -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.130.dist-info → ob_metaflow_extensions-1.5.1.dist-info}/METADATA +2 -2
- ob_metaflow_extensions-1.5.1.dist-info/RECORD +133 -0
- metaflow_extensions/outerbounds/plugins/nim/utilities.py +0 -5
- ob_metaflow_extensions-1.1.130.dist-info/RECORD +0 -56
- {ob_metaflow_extensions-1.1.130.dist-info → ob_metaflow_extensions-1.5.1.dist-info}/WHEEL +0 -0
- {ob_metaflow_extensions-1.1.130.dist-info → ob_metaflow_extensions-1.5.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Typed Init Generator for ConfigMeta Classes
|
|
3
|
+
|
|
4
|
+
This module provides a mechanism to dynamically generate explicit typed classes
|
|
5
|
+
from ConfigMeta classes that IDEs can understand and provide autocomplete for.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any, Dict, List, Optional, Union, Type, Set
|
|
9
|
+
|
|
10
|
+
from .config_utils import ConfigMeta
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
|
|
14
|
+
current_dir = os.path.dirname(__file__)
|
|
15
|
+
|
|
16
|
+
TYPED_DICT_IMPORT = """
|
|
17
|
+
import sys
|
|
18
|
+
from typing import TYPE_CHECKING
|
|
19
|
+
|
|
20
|
+
# on 3.8+ use the stdlib TypedDict;
|
|
21
|
+
# in TYPE_CHECKING blocks mypy/pyright still pick it up on older Pythons
|
|
22
|
+
if sys.version_info >= (3, 8):
|
|
23
|
+
from typing import TypedDict
|
|
24
|
+
else:
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
# for the benefit of type-checkers
|
|
27
|
+
from typing import TypedDict # noqa: F401
|
|
28
|
+
# runtime no-op TypedDict shim
|
|
29
|
+
class _TypedDictMeta(type):
|
|
30
|
+
def __new__(cls, name, bases, namespace, total=True):
|
|
31
|
+
# ignore total at runtime
|
|
32
|
+
return super().__new__(cls, name, bases, namespace)
|
|
33
|
+
|
|
34
|
+
class TypedDict(dict, metaclass=_TypedDictMeta):
|
|
35
|
+
# Runtime stand-in for typing.TypedDict on <3.8.
|
|
36
|
+
pass
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def collect_nested_configs_recursive(
|
|
41
|
+
config_class: Type, visited: Optional[Set[str]] = None
|
|
42
|
+
) -> Dict[str, Type]:
|
|
43
|
+
"""
|
|
44
|
+
Recursively collect all nested ConfigMeta classes from a config class.
|
|
45
|
+
|
|
46
|
+
Note: This collects ALL nested configs regardless of ConfigContext.
|
|
47
|
+
TypedDict definitions are always generated for type completeness.
|
|
48
|
+
The filtering by context only happens for TypedCoreConfig.__init__ parameters.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
config_class: A class that inherits from ConfigMeta
|
|
52
|
+
visited: Set of already visited class names to avoid infinite recursion
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Dictionary mapping class names to ConfigMeta classes
|
|
56
|
+
"""
|
|
57
|
+
if visited is None:
|
|
58
|
+
visited = set()
|
|
59
|
+
|
|
60
|
+
nested_configs = {}
|
|
61
|
+
|
|
62
|
+
# Avoid infinite recursion by tracking visited classes
|
|
63
|
+
if config_class.__name__ in visited:
|
|
64
|
+
return nested_configs
|
|
65
|
+
|
|
66
|
+
visited.add(config_class.__name__)
|
|
67
|
+
|
|
68
|
+
# First pass: collect immediate nested configs (all of them for TypedDict generation)
|
|
69
|
+
for field_name, field_info in config_class._fields.items():
|
|
70
|
+
if ConfigMeta.is_instance(field_info.field_type):
|
|
71
|
+
nested_class = field_info.field_type
|
|
72
|
+
nested_configs[nested_class.__name__] = nested_class
|
|
73
|
+
|
|
74
|
+
# Recursively collect nested configs from this nested class
|
|
75
|
+
deeper_nested = collect_nested_configs_recursive(
|
|
76
|
+
nested_class, visited.copy()
|
|
77
|
+
)
|
|
78
|
+
nested_configs.update(deeper_nested)
|
|
79
|
+
|
|
80
|
+
return nested_configs
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _get_field_help(field_info) -> str:
|
|
84
|
+
"""
|
|
85
|
+
Get help text from a ConfigField, checking both direct help and cli_meta.help.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
field_info: A ConfigField instance
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Help text string or empty string if none available
|
|
92
|
+
"""
|
|
93
|
+
# First check direct help attribute
|
|
94
|
+
if field_info.help:
|
|
95
|
+
return field_info.help
|
|
96
|
+
# Fall back to cli_meta.help if available
|
|
97
|
+
if (
|
|
98
|
+
hasattr(field_info, "cli_meta")
|
|
99
|
+
and field_info.cli_meta
|
|
100
|
+
and hasattr(field_info.cli_meta, "help")
|
|
101
|
+
):
|
|
102
|
+
return field_info.cli_meta.help or ""
|
|
103
|
+
return ""
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _generate_nested_type_docs(nested_class: Type, indent: str = " ") -> List[str]:
|
|
107
|
+
"""
|
|
108
|
+
Generate documentation for nested ConfigMeta class fields.
|
|
109
|
+
|
|
110
|
+
Note: Documents ALL fields in nested classes for completeness.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
nested_class: A nested ConfigMeta class
|
|
114
|
+
indent: The indentation string to use
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
List of documentation lines for the nested fields
|
|
118
|
+
"""
|
|
119
|
+
lines = []
|
|
120
|
+
for sub_field_name, sub_field_info in nested_class._fields.items():
|
|
121
|
+
sub_help = _get_field_help(sub_field_info)
|
|
122
|
+
sub_type = sub_field_info.field_type
|
|
123
|
+
|
|
124
|
+
if ConfigMeta.is_instance(sub_type):
|
|
125
|
+
sub_type_str = f"{sub_type.__name__}Dict"
|
|
126
|
+
else:
|
|
127
|
+
sub_type_str = _get_type_string(sub_type) if sub_type else "Any"
|
|
128
|
+
|
|
129
|
+
# Field name and type on one line
|
|
130
|
+
lines.append(f"{indent}- {sub_field_name} ({sub_type_str})")
|
|
131
|
+
# Help text on next line with extra indentation
|
|
132
|
+
if sub_help:
|
|
133
|
+
lines.append(f"{indent} {sub_help}")
|
|
134
|
+
|
|
135
|
+
# Recursively document deeply nested types
|
|
136
|
+
if ConfigMeta.is_instance(sub_type):
|
|
137
|
+
deeper_lines = _generate_nested_type_docs(sub_type, indent + " ")
|
|
138
|
+
lines.extend(deeper_lines)
|
|
139
|
+
|
|
140
|
+
return lines
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _generate_class_docstring(config_class: Type) -> str:
|
|
144
|
+
"""
|
|
145
|
+
Generate a class-level docstring with parameter descriptions in NumPy/Sphinx style.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
config_class: A class that inherits from ConfigMeta
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
Formatted docstring string for class level
|
|
152
|
+
"""
|
|
153
|
+
lines = ['"""', "Parameters", "----------"]
|
|
154
|
+
|
|
155
|
+
first_param = True
|
|
156
|
+
for field_name, field_info in config_class._fields.items():
|
|
157
|
+
# Skip fields not available in programmatic context
|
|
158
|
+
if not field_info.is_available_in_programmatic():
|
|
159
|
+
continue
|
|
160
|
+
|
|
161
|
+
help_text = _get_field_help(field_info)
|
|
162
|
+
field_type = field_info.field_type
|
|
163
|
+
is_experimental = getattr(field_info, "is_experimental", False)
|
|
164
|
+
|
|
165
|
+
# Add blank line between parameters (except before the first one)
|
|
166
|
+
if not first_param:
|
|
167
|
+
lines.append("")
|
|
168
|
+
first_param = False
|
|
169
|
+
|
|
170
|
+
# Get type string for documentation
|
|
171
|
+
if ConfigMeta.is_instance(field_type):
|
|
172
|
+
type_str = f"{field_type.__name__}Dict"
|
|
173
|
+
else:
|
|
174
|
+
type_str = _get_type_string(field_type) if field_type else "Any"
|
|
175
|
+
|
|
176
|
+
# Build parameter doc line in NumPy style
|
|
177
|
+
lines.append(f"{field_name} : {type_str}, optional")
|
|
178
|
+
|
|
179
|
+
if help_text:
|
|
180
|
+
lines.append(f" {help_text}")
|
|
181
|
+
|
|
182
|
+
# Add experimental notice as suffix on next line if applicable
|
|
183
|
+
if is_experimental:
|
|
184
|
+
lines.append(" [Experimental] May change in the future.")
|
|
185
|
+
|
|
186
|
+
# For nested ConfigMeta types, expand their fields
|
|
187
|
+
if ConfigMeta.is_instance(field_type):
|
|
188
|
+
nested_docs = _generate_nested_type_docs(field_type, indent=" ")
|
|
189
|
+
if nested_docs:
|
|
190
|
+
lines.extend(nested_docs)
|
|
191
|
+
|
|
192
|
+
lines.append('"""')
|
|
193
|
+
return "\n".join(lines)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def generate_typed_class_code(config_class: Type) -> str:
|
|
197
|
+
"""
|
|
198
|
+
Generate the actual Python code for a typed class that IDEs can understand.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
config_class: A class that inherits from ConfigMeta
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
Python code string for the typed class
|
|
205
|
+
"""
|
|
206
|
+
if not hasattr(config_class, "_fields"):
|
|
207
|
+
raise ValueError(f"Class {config_class.__name__} is not a ConfigMeta class")
|
|
208
|
+
|
|
209
|
+
class_name = f"Typed{config_class.__name__}"
|
|
210
|
+
|
|
211
|
+
# Generate TypedDict for nested configs - now recursive
|
|
212
|
+
nested_typeddict_code = []
|
|
213
|
+
|
|
214
|
+
# Recursively collect all nested configs
|
|
215
|
+
nested_configs = collect_nested_configs_recursive(config_class)
|
|
216
|
+
|
|
217
|
+
# Generate TypedDict classes for all nested configs
|
|
218
|
+
# Note: TypedDicts include ALL fields for type completeness (no context filtering)
|
|
219
|
+
for nested_name, nested_class in nested_configs.items():
|
|
220
|
+
dict_name = f"{nested_name}Dict"
|
|
221
|
+
fields = []
|
|
222
|
+
|
|
223
|
+
for field_name, field_info in nested_class._fields.items():
|
|
224
|
+
field_type = _get_type_string(field_info.field_type, quote_config_meta=True)
|
|
225
|
+
if not field_info.required:
|
|
226
|
+
field_type = f"Optional[{field_type}]"
|
|
227
|
+
fields.append(f" {field_name}: {field_type}")
|
|
228
|
+
|
|
229
|
+
typeddict_code = f"""class {dict_name}(TypedDict, total=False):
|
|
230
|
+
{chr(10).join(fields)}"""
|
|
231
|
+
nested_typeddict_code.append(typeddict_code)
|
|
232
|
+
|
|
233
|
+
# Generate __init__ method signature
|
|
234
|
+
required_params = []
|
|
235
|
+
optional_params = []
|
|
236
|
+
all_assignments = []
|
|
237
|
+
|
|
238
|
+
for field_name, field_info in config_class._fields.items():
|
|
239
|
+
# Skip fields not available in programmatic context
|
|
240
|
+
if not field_info.is_available_in_programmatic():
|
|
241
|
+
continue
|
|
242
|
+
|
|
243
|
+
field_type = field_info.field_type
|
|
244
|
+
|
|
245
|
+
# Handle nested ConfigMeta classes
|
|
246
|
+
if ConfigMeta.is_instance(field_type):
|
|
247
|
+
type_hint = f"Optional[{field_type.__name__}Dict]"
|
|
248
|
+
param_line = f" {field_name}: {type_hint} = None"
|
|
249
|
+
optional_params.append(param_line)
|
|
250
|
+
else:
|
|
251
|
+
# All params will be set as options here even if the are required in the
|
|
252
|
+
# configMeta
|
|
253
|
+
type_hint = _get_type_string(field_type)
|
|
254
|
+
param_line = f" {field_name}: Optional[{type_hint}] = None"
|
|
255
|
+
optional_params.append(param_line)
|
|
256
|
+
|
|
257
|
+
all_assignments.append(f' "{field_name}": {field_name}')
|
|
258
|
+
|
|
259
|
+
# Combine required params first, then optional params
|
|
260
|
+
all_params = required_params + optional_params
|
|
261
|
+
|
|
262
|
+
# Generate the class code
|
|
263
|
+
newline = "\n"
|
|
264
|
+
comma_newline = ",\n"
|
|
265
|
+
|
|
266
|
+
# Add **kwargs to the parameter list
|
|
267
|
+
if all_params:
|
|
268
|
+
params_with_kwargs = all_params + [" **kwargs"]
|
|
269
|
+
else:
|
|
270
|
+
params_with_kwargs = [" **kwargs"]
|
|
271
|
+
|
|
272
|
+
# Generate class-level docstring with parameter help
|
|
273
|
+
class_docstring = _generate_class_docstring(config_class)
|
|
274
|
+
# Indent the docstring for class level (4 spaces)
|
|
275
|
+
indented_class_docstring = "\n".join(
|
|
276
|
+
" " + line if line else "" for line in class_docstring.split("\n")
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
class_code = f"""class {class_name}:
|
|
280
|
+
{indented_class_docstring}
|
|
281
|
+
|
|
282
|
+
def __init__(
|
|
283
|
+
self,
|
|
284
|
+
{comma_newline.join(params_with_kwargs)}
|
|
285
|
+
) -> None:
|
|
286
|
+
self._kwargs = {{
|
|
287
|
+
{comma_newline.join(all_assignments)}
|
|
288
|
+
}}
|
|
289
|
+
# Add any additional kwargs
|
|
290
|
+
self._kwargs.update(kwargs)
|
|
291
|
+
# Remove None values
|
|
292
|
+
self._kwargs = {{k: v for k, v in self._kwargs.items() if v is not None}}
|
|
293
|
+
self._config_class = {config_class.__name__}
|
|
294
|
+
self._config = self.create_config()
|
|
295
|
+
self._init()
|
|
296
|
+
|
|
297
|
+
def create_config(self) -> {config_class.__name__}:
|
|
298
|
+
return {config_class.__name__}.from_dict(self._kwargs)
|
|
299
|
+
|
|
300
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
301
|
+
return self._config.to_dict()
|
|
302
|
+
|
|
303
|
+
def _init(self):
|
|
304
|
+
raise NotImplementedError"""
|
|
305
|
+
|
|
306
|
+
# Combine all code
|
|
307
|
+
full_code = []
|
|
308
|
+
if nested_typeddict_code:
|
|
309
|
+
full_code.extend(nested_typeddict_code)
|
|
310
|
+
full_code.append("") # Empty line
|
|
311
|
+
full_code.append(class_code)
|
|
312
|
+
|
|
313
|
+
return (newline + newline).join(full_code)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _get_type_string(field_type: Type, quote_config_meta: bool = False) -> str:
|
|
317
|
+
"""Convert a type to its string representation for code generation.
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
field_type: The type to convert
|
|
321
|
+
quote_config_meta: Whether to quote ConfigMeta type references for forward declarations
|
|
322
|
+
"""
|
|
323
|
+
if field_type == str:
|
|
324
|
+
return "str"
|
|
325
|
+
elif field_type == int:
|
|
326
|
+
return "int"
|
|
327
|
+
elif field_type == float:
|
|
328
|
+
return "float"
|
|
329
|
+
elif field_type == bool:
|
|
330
|
+
return "bool"
|
|
331
|
+
elif ConfigMeta.is_instance(field_type):
|
|
332
|
+
# Handle ConfigMeta classes by referencing their Dict type
|
|
333
|
+
dict_type = f"{field_type.__name__}Dict"
|
|
334
|
+
return f'"{dict_type}"' if quote_config_meta else dict_type
|
|
335
|
+
elif hasattr(field_type, "__origin__"):
|
|
336
|
+
# Handle generic types like List[str], Dict[str, str], etc.
|
|
337
|
+
origin = field_type.__origin__
|
|
338
|
+
args = getattr(field_type, "__args__", ())
|
|
339
|
+
|
|
340
|
+
if origin == list:
|
|
341
|
+
if args:
|
|
342
|
+
return f"List[{_get_type_string(args[0], quote_config_meta)}]"
|
|
343
|
+
return "List[Any]"
|
|
344
|
+
elif origin == dict:
|
|
345
|
+
if len(args) == 2:
|
|
346
|
+
return f"Dict[{_get_type_string(args[0], quote_config_meta)}, {_get_type_string(args[1], quote_config_meta)}]"
|
|
347
|
+
return "Dict[str, Any]"
|
|
348
|
+
elif origin == Union:
|
|
349
|
+
# Handle Optional types
|
|
350
|
+
if len(args) == 2 and type(None) in args:
|
|
351
|
+
non_none_type = args[0] if args[1] is type(None) else args[1]
|
|
352
|
+
return f"Optional[{_get_type_string(non_none_type, quote_config_meta)}]"
|
|
353
|
+
return f"Union[{', '.join(_get_type_string(arg, quote_config_meta) for arg in args)}]"
|
|
354
|
+
|
|
355
|
+
# Default case - use the type name
|
|
356
|
+
return getattr(field_type, "__name__", str(field_type))
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def generate_typed_classes_module(
|
|
360
|
+
config_classes: List[Type], module_name: str = "typed_configs"
|
|
361
|
+
) -> str:
|
|
362
|
+
"""
|
|
363
|
+
Generate a complete Python module with typed classes for multiple ConfigMeta classes.
|
|
364
|
+
|
|
365
|
+
Args:
|
|
366
|
+
config_classes: List of ConfigMeta classes
|
|
367
|
+
module_name: Name for the generated module
|
|
368
|
+
|
|
369
|
+
Returns:
|
|
370
|
+
Complete Python module code
|
|
371
|
+
"""
|
|
372
|
+
imports = [
|
|
373
|
+
"from typing import Optional, List, Dict, Any",
|
|
374
|
+
"from .unified_config import "
|
|
375
|
+
+ ", ".join(cls.__name__ for cls in config_classes),
|
|
376
|
+
TYPED_DICT_IMPORT,
|
|
377
|
+
]
|
|
378
|
+
|
|
379
|
+
class_codes = []
|
|
380
|
+
for config_class in config_classes:
|
|
381
|
+
class_codes.append(generate_typed_class_code(config_class))
|
|
382
|
+
|
|
383
|
+
# Use string concatenation instead of f-string with backslashes
|
|
384
|
+
newline = "\n"
|
|
385
|
+
module_code = (
|
|
386
|
+
'"""'
|
|
387
|
+
+ newline
|
|
388
|
+
+ "Auto-generated typed classes for ConfigMeta classes."
|
|
389
|
+
+ newline
|
|
390
|
+
+ newline
|
|
391
|
+
+ "This module provides IDE-friendly typed interfaces for all configuration classes."
|
|
392
|
+
+ newline
|
|
393
|
+
+ "The reason we auto-generate this file is because we want to provide a bridge between what is the ConfigMeta classes and the typed programmatic interface."
|
|
394
|
+
+ newline
|
|
395
|
+
+ "The CoreConfig class is setup in a way that if any additionally params are missed out from being auto-generated "
|
|
396
|
+
+ "then it will not affect the core functionality of the programmatic API."
|
|
397
|
+
+ newline
|
|
398
|
+
+ "The new parameters will just not show up in IDE autocompletions."
|
|
399
|
+
+ newline
|
|
400
|
+
+ "It is fine if this file is not regularly updated by running the script in the .pre-commit-config.app-changes.yaml"
|
|
401
|
+
+ newline
|
|
402
|
+
+ "but it is recommended that this file not be deleted or manually edited."
|
|
403
|
+
+ newline
|
|
404
|
+
+ newline
|
|
405
|
+
+ '"""'
|
|
406
|
+
+ newline
|
|
407
|
+
+ newline
|
|
408
|
+
+ newline.join(imports)
|
|
409
|
+
+ newline
|
|
410
|
+
+ newline
|
|
411
|
+
+ (newline + newline).join(class_codes)
|
|
412
|
+
+ newline
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
return module_code
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def create_typed_init_class_dynamic(config_class: Type) -> Type:
|
|
419
|
+
"""
|
|
420
|
+
Dynamically create a typed init class with proper IDE support.
|
|
421
|
+
|
|
422
|
+
This creates the class at runtime but with proper type annotations
|
|
423
|
+
that IDEs can understand.
|
|
424
|
+
"""
|
|
425
|
+
if not hasattr(config_class, "_fields"):
|
|
426
|
+
raise ValueError(f"Class {config_class.__name__} is not a ConfigMeta class")
|
|
427
|
+
|
|
428
|
+
class_name = f"Typed{config_class.__name__}"
|
|
429
|
+
|
|
430
|
+
# Create the init method with proper signature
|
|
431
|
+
def create_init_method():
|
|
432
|
+
# Build the signature dynamically
|
|
433
|
+
sig_params = []
|
|
434
|
+
annotations: Dict[str, Any] = {"return": type(None)}
|
|
435
|
+
|
|
436
|
+
for field_name, field_info in config_class._fields.items():
|
|
437
|
+
# Skip fields not available in programmatic context
|
|
438
|
+
if not field_info.is_available_in_programmatic():
|
|
439
|
+
continue
|
|
440
|
+
|
|
441
|
+
field_type = field_info.field_type
|
|
442
|
+
|
|
443
|
+
# Handle nested ConfigMeta classes
|
|
444
|
+
if ConfigMeta.is_instance(field_type):
|
|
445
|
+
field_type = Dict[str, Any] # Use Dict for nested configs
|
|
446
|
+
|
|
447
|
+
# Handle Optional fields
|
|
448
|
+
if not field_info.required:
|
|
449
|
+
field_type = Optional[field_type]
|
|
450
|
+
|
|
451
|
+
annotations[field_name] = field_type
|
|
452
|
+
|
|
453
|
+
def __init__(self, **kwargs):
|
|
454
|
+
# Validate kwargs
|
|
455
|
+
required_fields = {
|
|
456
|
+
name for name, info in config_class._fields.items() if info.required
|
|
457
|
+
}
|
|
458
|
+
provided_fields = set(kwargs.keys())
|
|
459
|
+
valid_fields = set(config_class._fields.keys())
|
|
460
|
+
|
|
461
|
+
# Check required fields
|
|
462
|
+
missing_fields = required_fields - provided_fields
|
|
463
|
+
if missing_fields:
|
|
464
|
+
raise ValueError(
|
|
465
|
+
f"Missing required fields: {', '.join(missing_fields)}"
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
# Check for unknown fields - but allow them for flexibility
|
|
469
|
+
unknown_fields = provided_fields - valid_fields
|
|
470
|
+
if unknown_fields:
|
|
471
|
+
print(
|
|
472
|
+
f"Warning: Unknown fields will be passed through: {', '.join(unknown_fields)}"
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
self._kwargs = kwargs
|
|
476
|
+
self._config_class = config_class
|
|
477
|
+
|
|
478
|
+
# Set annotations
|
|
479
|
+
__init__.__annotations__ = annotations
|
|
480
|
+
return __init__
|
|
481
|
+
|
|
482
|
+
def create_config(self):
|
|
483
|
+
"""Create and return the ConfigMeta class instance."""
|
|
484
|
+
return config_class.from_dict(self._kwargs)
|
|
485
|
+
|
|
486
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
487
|
+
"""Return the raw kwargs as a dictionary."""
|
|
488
|
+
return self._kwargs.copy()
|
|
489
|
+
|
|
490
|
+
def __repr__(self) -> str:
|
|
491
|
+
return f"{class_name}({self._kwargs})"
|
|
492
|
+
|
|
493
|
+
# Create the class
|
|
494
|
+
init_method = create_init_method()
|
|
495
|
+
|
|
496
|
+
TypedClass = type(
|
|
497
|
+
class_name,
|
|
498
|
+
(object,),
|
|
499
|
+
{
|
|
500
|
+
"__init__": init_method,
|
|
501
|
+
"create_config": create_config,
|
|
502
|
+
"to_dict": to_dict,
|
|
503
|
+
"__repr__": __repr__,
|
|
504
|
+
"__module__": __name__,
|
|
505
|
+
"__qualname__": class_name,
|
|
506
|
+
},
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
return TypedClass
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
# Auto-generate and write typed classes to a file
|
|
513
|
+
def generate_typed_classes_file(output_file: Optional[str] = None):
|
|
514
|
+
"""
|
|
515
|
+
Generate typed classes and write them to a file for IDE support.
|
|
516
|
+
|
|
517
|
+
Args:
|
|
518
|
+
output_file: Path to write the generated classes. If None, prints to stdout.
|
|
519
|
+
"""
|
|
520
|
+
from .unified_config import CoreConfig
|
|
521
|
+
|
|
522
|
+
config_classes = [CoreConfig]
|
|
523
|
+
|
|
524
|
+
module_code = generate_typed_classes_module(config_classes)
|
|
525
|
+
|
|
526
|
+
if output_file:
|
|
527
|
+
with open(output_file, "w") as f:
|
|
528
|
+
f.write(module_code)
|
|
529
|
+
print(f"Generated typed classes written to {output_file}")
|
|
530
|
+
else:
|
|
531
|
+
print(module_code)
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
# Example usage and testing
|
|
535
|
+
if __name__ == "__main__":
|
|
536
|
+
# Generate typed classes file
|
|
537
|
+
generate_typed_classes_file(os.path.join(current_dir, "typed_configs.py"))
|