ob-metaflow-extensions 1.1.175rc1__py2.py3-none-any.whl → 1.1.175rc3__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/plugins/__init__.py +1 -1
- metaflow_extensions/outerbounds/plugins/apps/app_deploy_decorator.py +112 -0
- metaflow_extensions/outerbounds/plugins/apps/core/__init__.py +9 -0
- metaflow_extensions/outerbounds/plugins/apps/core/app_cli.py +42 -374
- metaflow_extensions/outerbounds/plugins/apps/core/app_config.py +45 -225
- metaflow_extensions/outerbounds/plugins/apps/core/capsule.py +22 -6
- metaflow_extensions/outerbounds/plugins/apps/core/code_package/code_packager.py +16 -15
- 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 +828 -0
- metaflow_extensions/outerbounds/plugins/apps/core/config/schema_export.py +285 -0
- metaflow_extensions/outerbounds/plugins/apps/core/config/typed_configs.py +104 -0
- metaflow_extensions/outerbounds/plugins/apps/core/config/typed_init_generator.py +317 -0
- metaflow_extensions/outerbounds/plugins/apps/core/config/unified_config.py +994 -0
- metaflow_extensions/outerbounds/plugins/apps/core/config_schema.yaml +217 -211
- metaflow_extensions/outerbounds/plugins/apps/core/dependencies.py +3 -3
- metaflow_extensions/outerbounds/plugins/apps/core/deployer.py +132 -0
- metaflow_extensions/outerbounds/plugins/apps/core/experimental/__init__.py +4 -36
- metaflow_extensions/outerbounds/plugins/apps/core/perimeters.py +44 -2
- metaflow_extensions/outerbounds/plugins/apps/core/validations.py +4 -9
- metaflow_extensions/outerbounds/toplevel/global_aliases_for_metaflow_package.py +1 -0
- metaflow_extensions/outerbounds/toplevel/ob_internal.py +1 -0
- {ob_metaflow_extensions-1.1.175rc1.dist-info → ob_metaflow_extensions-1.1.175rc3.dist-info}/METADATA +1 -1
- {ob_metaflow_extensions-1.1.175rc1.dist-info → ob_metaflow_extensions-1.1.175rc3.dist-info}/RECORD +26 -20
- metaflow_extensions/outerbounds/plugins/apps/core/cli_to_config.py +0 -99
- metaflow_extensions/outerbounds/plugins/apps/core/config_schema_autogen.json +0 -336
- {ob_metaflow_extensions-1.1.175rc1.dist-info → ob_metaflow_extensions-1.1.175rc3.dist-info}/WHEEL +0 -0
- {ob_metaflow_extensions-1.1.175rc1.dist-info → ob_metaflow_extensions-1.1.175rc3.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Schema Export Module for Unified Configuration System
|
|
3
|
+
|
|
4
|
+
This module provides standalone functions to export configuration schemas in various formats:
|
|
5
|
+
- OpenAPI schemas in YAML or JSON format
|
|
6
|
+
- JSON schemas in YAML or JSON format
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
from schema_export import export_schema, to_openapi_schema, to_json_schema, to_dict
|
|
10
|
+
|
|
11
|
+
# Export schema to file
|
|
12
|
+
export_schema(CoreConfig, "schema.yaml")
|
|
13
|
+
export_schema(CoreConfig, "schema.json", schema_type="json", format="json")
|
|
14
|
+
|
|
15
|
+
# Generate schema in memory
|
|
16
|
+
openapi_schema = to_openapi_schema(CoreConfig)
|
|
17
|
+
json_schema = to_json_schema(CoreConfig)
|
|
18
|
+
|
|
19
|
+
# Export config instance to dict
|
|
20
|
+
config_instance = CoreConfig(name="myapp", port=8000)
|
|
21
|
+
config_dict = to_dict(config_instance)
|
|
22
|
+
|
|
23
|
+
No external dependencies required for basic functionality.
|
|
24
|
+
PyYAML is optional for YAML export support.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import json
|
|
28
|
+
import textwrap
|
|
29
|
+
from collections import OrderedDict
|
|
30
|
+
from typing import Any, Dict
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
import yaml
|
|
34
|
+
|
|
35
|
+
HAS_YAML = True
|
|
36
|
+
except ImportError:
|
|
37
|
+
HAS_YAML = False
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def to_openapi_schema(config_class) -> Dict[str, Any]:
|
|
41
|
+
"""Generate OpenAPI schema for a configuration class.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
config_class: The configuration class to generate schema for
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
OpenAPI schema dictionary
|
|
48
|
+
"""
|
|
49
|
+
return _generate_openapi_schema(config_class)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def to_json_schema(config_class) -> Dict[str, Any]:
|
|
53
|
+
"""Generate JSON schema for a configuration class.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
config_class: The configuration class to generate schema for
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
JSON schema dictionary
|
|
60
|
+
"""
|
|
61
|
+
return _generate_json_schema(config_class)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def export_schema(
|
|
65
|
+
config_class, filepath: str, schema_type: str = "openapi", format: str = "yaml"
|
|
66
|
+
) -> None:
|
|
67
|
+
"""Export configuration schema to file.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
config_class: The configuration class to export schema for
|
|
71
|
+
filepath: Path to save the schema file
|
|
72
|
+
schema_type: Type of schema to generate ('openapi' or 'json')
|
|
73
|
+
format: Output format ('yaml' or 'json')
|
|
74
|
+
|
|
75
|
+
Examples:
|
|
76
|
+
# Export OpenAPI schema as YAML (default)
|
|
77
|
+
export_schema(CoreConfig, "schema.yaml")
|
|
78
|
+
|
|
79
|
+
# Export JSON schema as YAML
|
|
80
|
+
export_schema(CoreConfig, "schema.yaml", schema_type="json")
|
|
81
|
+
|
|
82
|
+
# Export OpenAPI schema as JSON
|
|
83
|
+
export_schema(CoreConfig, "schema.json", schema_type="openapi", format="json")
|
|
84
|
+
|
|
85
|
+
# Export JSON schema as JSON
|
|
86
|
+
export_schema(CoreConfig, "schema.json", schema_type="json", format="json")
|
|
87
|
+
"""
|
|
88
|
+
# Validate inputs
|
|
89
|
+
if schema_type.lower() not in ["openapi", "json"]:
|
|
90
|
+
raise ValueError(
|
|
91
|
+
f"Unsupported schema type: {schema_type}. Use 'openapi' or 'json'."
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
if format.lower() not in ["yaml", "json"]:
|
|
95
|
+
raise ValueError(f"Unsupported format: {format}. Use 'yaml' or 'json'.")
|
|
96
|
+
|
|
97
|
+
# Generate the appropriate schema
|
|
98
|
+
if schema_type.lower() == "openapi":
|
|
99
|
+
base_schema = _generate_openapi_schema(config_class)
|
|
100
|
+
# Wrap in OpenAPI document structure with proper ordering
|
|
101
|
+
schema_data = OrderedDict(
|
|
102
|
+
[
|
|
103
|
+
("openapi", "3.0.0"),
|
|
104
|
+
(
|
|
105
|
+
"info",
|
|
106
|
+
OrderedDict(
|
|
107
|
+
[
|
|
108
|
+
("title", f"{config_class.__name__} Configuration Schema"),
|
|
109
|
+
("version", "1.0.0"),
|
|
110
|
+
]
|
|
111
|
+
),
|
|
112
|
+
),
|
|
113
|
+
(
|
|
114
|
+
"components",
|
|
115
|
+
OrderedDict(
|
|
116
|
+
[
|
|
117
|
+
(
|
|
118
|
+
"schemas",
|
|
119
|
+
OrderedDict([(config_class.__name__, base_schema)]),
|
|
120
|
+
)
|
|
121
|
+
]
|
|
122
|
+
),
|
|
123
|
+
),
|
|
124
|
+
]
|
|
125
|
+
)
|
|
126
|
+
else: # json schema
|
|
127
|
+
schema_data = _generate_json_schema(config_class)
|
|
128
|
+
|
|
129
|
+
# Export in the requested format
|
|
130
|
+
if format.lower() == "yaml":
|
|
131
|
+
if not HAS_YAML:
|
|
132
|
+
raise ImportError(
|
|
133
|
+
"PyYAML is required for YAML export. Install with: pip install pyyaml"
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# Custom YAML representer for multiline strings
|
|
137
|
+
def multiline_representer(dumper, data):
|
|
138
|
+
if "\n" in data or len(data) > 80:
|
|
139
|
+
style = "|" # use literal block
|
|
140
|
+
else:
|
|
141
|
+
style = None # normal flow
|
|
142
|
+
return dumper.represent_scalar("tag:yaml.org,2002:str", data, style=style)
|
|
143
|
+
|
|
144
|
+
# Custom YAML representer for OrderedDict to preserve order
|
|
145
|
+
def ordered_dict_representer(dumper, data):
|
|
146
|
+
return dumper.represent_dict(data.items())
|
|
147
|
+
|
|
148
|
+
yaml.add_representer(str, multiline_representer)
|
|
149
|
+
yaml.add_representer(OrderedDict, ordered_dict_representer)
|
|
150
|
+
|
|
151
|
+
with open(filepath, "w") as f:
|
|
152
|
+
yaml.dump(
|
|
153
|
+
schema_data,
|
|
154
|
+
f,
|
|
155
|
+
default_flow_style=False,
|
|
156
|
+
sort_keys=False,
|
|
157
|
+
allow_unicode=True,
|
|
158
|
+
width=120,
|
|
159
|
+
indent=2,
|
|
160
|
+
)
|
|
161
|
+
else: # json format
|
|
162
|
+
with open(filepath, "w") as f:
|
|
163
|
+
json.dump(schema_data, f, indent=2)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
# Private helper functions
|
|
167
|
+
def _generate_openapi_schema(cls) -> Dict[str, Any]:
|
|
168
|
+
"""Generate OpenAPI schema for a configuration class."""
|
|
169
|
+
# Clean up class docstring for better YAML formatting
|
|
170
|
+
description = f"{cls.__name__} configuration"
|
|
171
|
+
get_description = getattr(cls, "SCHEMA_DOC", None)
|
|
172
|
+
if get_description:
|
|
173
|
+
description = get_description
|
|
174
|
+
elif cls.__doc__:
|
|
175
|
+
# Remove common indentation and clean up whitespace
|
|
176
|
+
cleaned_doc = textwrap.dedent(cls.__doc__).strip()
|
|
177
|
+
# Replace multiple spaces with single spaces but preserve line breaks
|
|
178
|
+
lines = [line.strip() for line in cleaned_doc.split("\n") if line.strip()]
|
|
179
|
+
description = "\n".join(lines)
|
|
180
|
+
|
|
181
|
+
# Create ordered schema with specific order: title, description, type, required, then properties
|
|
182
|
+
schema = OrderedDict(
|
|
183
|
+
[
|
|
184
|
+
("title", cls.__name__),
|
|
185
|
+
("description", description),
|
|
186
|
+
("type", "object"),
|
|
187
|
+
("required", []),
|
|
188
|
+
("properties", OrderedDict()),
|
|
189
|
+
]
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
for field_name, field_info in cls._fields.items():
|
|
193
|
+
if field_name.startswith("_"):
|
|
194
|
+
continue
|
|
195
|
+
|
|
196
|
+
field_schema = _get_field_schema(field_info)
|
|
197
|
+
schema["properties"][field_name] = field_schema
|
|
198
|
+
|
|
199
|
+
# Add to required if field is required
|
|
200
|
+
if field_info.required:
|
|
201
|
+
schema["required"].append(field_name)
|
|
202
|
+
|
|
203
|
+
return schema
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _generate_json_schema(cls) -> Dict[str, Any]:
|
|
207
|
+
"""Generate JSON schema for a configuration class."""
|
|
208
|
+
openapi_schema = _generate_openapi_schema(cls)
|
|
209
|
+
|
|
210
|
+
# Create ordered JSON schema with $schema first
|
|
211
|
+
schema = OrderedDict(
|
|
212
|
+
[
|
|
213
|
+
("$schema", "https://json-schema.org/draft/2020-12/schema"),
|
|
214
|
+
("title", openapi_schema["title"]),
|
|
215
|
+
("description", openapi_schema["description"]),
|
|
216
|
+
("type", openapi_schema["type"]),
|
|
217
|
+
("required", openapi_schema["required"]),
|
|
218
|
+
("properties", openapi_schema["properties"]),
|
|
219
|
+
]
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
return schema
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _get_field_schema(field_info) -> Dict[str, Any]:
|
|
226
|
+
"""Generate schema for a single field."""
|
|
227
|
+
field_schema = OrderedDict()
|
|
228
|
+
|
|
229
|
+
# Get description from ConfigField.help first, then CLI metadata
|
|
230
|
+
description = field_info.help or (
|
|
231
|
+
field_info.cli_meta.help if field_info.cli_meta else None
|
|
232
|
+
)
|
|
233
|
+
if description:
|
|
234
|
+
field_schema["description"] = description
|
|
235
|
+
|
|
236
|
+
# Handle field type
|
|
237
|
+
if field_info.field_type == str:
|
|
238
|
+
field_schema["type"] = "string"
|
|
239
|
+
elif field_info.field_type == int:
|
|
240
|
+
field_schema["type"] = "integer"
|
|
241
|
+
elif field_info.field_type == float:
|
|
242
|
+
field_schema["type"] = "number"
|
|
243
|
+
elif field_info.field_type == bool:
|
|
244
|
+
field_schema["type"] = "boolean"
|
|
245
|
+
elif field_info.field_type == list:
|
|
246
|
+
field_schema["type"] = "array"
|
|
247
|
+
field_schema["items"] = {"type": "string"} # Default to string items
|
|
248
|
+
elif field_info.field_type == dict:
|
|
249
|
+
field_schema["type"] = "object"
|
|
250
|
+
field_schema["additionalProperties"] = True
|
|
251
|
+
elif hasattr(field_info.field_type, "_fields"):
|
|
252
|
+
# Nested configuration object
|
|
253
|
+
field_schema = _generate_openapi_schema(field_info.field_type)
|
|
254
|
+
else:
|
|
255
|
+
# Fallback to string for unknown types
|
|
256
|
+
field_schema["type"] = "string"
|
|
257
|
+
|
|
258
|
+
# Add default value
|
|
259
|
+
if field_info.default is not None and not callable(field_info.default):
|
|
260
|
+
field_schema["default"] = field_info.default
|
|
261
|
+
|
|
262
|
+
# Handle choices from CLI metadata
|
|
263
|
+
if field_info.cli_meta and field_info.cli_meta.choices:
|
|
264
|
+
if "type" in field_schema:
|
|
265
|
+
field_schema["enum"] = field_info.cli_meta.choices
|
|
266
|
+
|
|
267
|
+
# Add examples from ConfigField
|
|
268
|
+
if field_info.example is not None:
|
|
269
|
+
field_schema["example"] = field_info.example
|
|
270
|
+
|
|
271
|
+
# Add experimental flag
|
|
272
|
+
if field_info.is_experimental:
|
|
273
|
+
field_schema["experimental"] = True
|
|
274
|
+
|
|
275
|
+
# Add Field Behavior
|
|
276
|
+
if field_info.behavior:
|
|
277
|
+
field_schema["mutation_behavior"] = field_info.behavior
|
|
278
|
+
|
|
279
|
+
# Handle validation from CLI metadata
|
|
280
|
+
if field_info.cli_meta and field_info.validation_fn:
|
|
281
|
+
# Add validation hints in description
|
|
282
|
+
if "description" in field_schema:
|
|
283
|
+
field_schema["description"] += " (validation applied)"
|
|
284
|
+
|
|
285
|
+
return field_schema
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Auto-generated typed classes for ConfigMeta classes.
|
|
3
|
+
|
|
4
|
+
This module provides IDE-friendly typed interfaces for all configuration classes.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Optional, List, Dict, Any, TypedDict
|
|
8
|
+
from .unified_config import CoreConfig
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ResourceConfigDict(TypedDict, total=False):
|
|
12
|
+
cpu: Optional[str]
|
|
13
|
+
memory: Optional[str]
|
|
14
|
+
gpu: Optional[str]
|
|
15
|
+
disk: Optional[str]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AuthConfigDict(TypedDict, total=False):
|
|
19
|
+
type: Optional[str]
|
|
20
|
+
public: Optional[bool]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ReplicaConfigDict(TypedDict, total=False):
|
|
24
|
+
fixed: Optional[int]
|
|
25
|
+
min: Optional[int]
|
|
26
|
+
max: Optional[int]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class DependencyConfigDict(TypedDict, total=False):
|
|
30
|
+
from_requirements_file: Optional[str]
|
|
31
|
+
from_pyproject_toml: Optional[str]
|
|
32
|
+
python: Optional[str]
|
|
33
|
+
pypi: Optional[dict]
|
|
34
|
+
conda: Optional[dict]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class PackageConfigDict(TypedDict, total=False):
|
|
38
|
+
src_path: Optional[str]
|
|
39
|
+
suffixes: Optional[list]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class TypedCoreConfig:
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
name: Optional[str] = None,
|
|
46
|
+
port: Optional[int] = None,
|
|
47
|
+
description: Optional[str] = None,
|
|
48
|
+
app_type: Optional[str] = None,
|
|
49
|
+
image: Optional[str] = None,
|
|
50
|
+
tags: Optional[list] = None,
|
|
51
|
+
secrets: Optional[list] = None,
|
|
52
|
+
compute_pools: Optional[list] = None,
|
|
53
|
+
environment: Optional[dict] = None,
|
|
54
|
+
commands: Optional[list] = None,
|
|
55
|
+
resources: Optional[ResourceConfigDict] = None,
|
|
56
|
+
auth: Optional[AuthConfigDict] = None,
|
|
57
|
+
replicas: Optional[ReplicaConfigDict] = None,
|
|
58
|
+
dependencies: Optional[DependencyConfigDict] = None,
|
|
59
|
+
package: Optional[PackageConfigDict] = None,
|
|
60
|
+
no_deps: Optional[bool] = None,
|
|
61
|
+
force_upgrade: Optional[bool] = None,
|
|
62
|
+
persistence: Optional[str] = None,
|
|
63
|
+
project: Optional[str] = None,
|
|
64
|
+
branch: Optional[str] = None,
|
|
65
|
+
models: Optional[list] = None,
|
|
66
|
+
data: Optional[list] = None,
|
|
67
|
+
**kwargs
|
|
68
|
+
) -> None:
|
|
69
|
+
self._kwargs = {
|
|
70
|
+
"name": name,
|
|
71
|
+
"port": port,
|
|
72
|
+
"description": description,
|
|
73
|
+
"app_type": app_type,
|
|
74
|
+
"image": image,
|
|
75
|
+
"tags": tags,
|
|
76
|
+
"secrets": secrets,
|
|
77
|
+
"compute_pools": compute_pools,
|
|
78
|
+
"environment": environment,
|
|
79
|
+
"commands": commands,
|
|
80
|
+
"resources": resources,
|
|
81
|
+
"auth": auth,
|
|
82
|
+
"replicas": replicas,
|
|
83
|
+
"dependencies": dependencies,
|
|
84
|
+
"package": package,
|
|
85
|
+
"no_deps": no_deps,
|
|
86
|
+
"force_upgrade": force_upgrade,
|
|
87
|
+
"persistence": persistence,
|
|
88
|
+
"project": project,
|
|
89
|
+
"branch": branch,
|
|
90
|
+
"models": models,
|
|
91
|
+
"data": data,
|
|
92
|
+
}
|
|
93
|
+
# Add any additional kwargs
|
|
94
|
+
self._kwargs.update(kwargs)
|
|
95
|
+
# Remove None values
|
|
96
|
+
self._kwargs = {k: v for k, v in self._kwargs.items() if v is not None}
|
|
97
|
+
self._config_class = CoreConfig
|
|
98
|
+
self._config = self.create_config()
|
|
99
|
+
|
|
100
|
+
def create_config(self) -> CoreConfig:
|
|
101
|
+
return CoreConfig.from_dict(self._kwargs)
|
|
102
|
+
|
|
103
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
104
|
+
return self._config.to_dict()
|
|
@@ -0,0 +1,317 @@
|
|
|
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
|
|
9
|
+
|
|
10
|
+
from .config_utils import ConfigMeta
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
|
|
14
|
+
current_dir = os.path.dirname(__file__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def generate_typed_class_code(config_class: Type) -> str:
|
|
18
|
+
"""
|
|
19
|
+
Generate the actual Python code for a typed class that IDEs can understand.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
config_class: A class that inherits from ConfigMeta
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Python code string for the typed class
|
|
26
|
+
"""
|
|
27
|
+
if not hasattr(config_class, "_fields"):
|
|
28
|
+
raise ValueError(f"Class {config_class.__name__} is not a ConfigMeta class")
|
|
29
|
+
|
|
30
|
+
class_name = f"Typed{config_class.__name__}"
|
|
31
|
+
|
|
32
|
+
# Generate TypedDict for nested configs
|
|
33
|
+
nested_typeddict_code = []
|
|
34
|
+
|
|
35
|
+
# First pass: collect all nested configs
|
|
36
|
+
nested_configs = {}
|
|
37
|
+
for field_name, field_info in config_class._fields.items():
|
|
38
|
+
if ConfigMeta.is_instance(field_info.field_type):
|
|
39
|
+
nested_configs[field_info.field_type.__name__] = field_info.field_type
|
|
40
|
+
|
|
41
|
+
# Generate TypedDict classes for nested configs
|
|
42
|
+
for nested_name, nested_class in nested_configs.items():
|
|
43
|
+
dict_name = f"{nested_name}Dict"
|
|
44
|
+
fields = []
|
|
45
|
+
|
|
46
|
+
for field_name, field_info in nested_class._fields.items():
|
|
47
|
+
field_type = _get_type_string(field_info.field_type)
|
|
48
|
+
if not field_info.required:
|
|
49
|
+
field_type = f"Optional[{field_type}]"
|
|
50
|
+
fields.append(f" {field_name}: {field_type}")
|
|
51
|
+
|
|
52
|
+
typeddict_code = f"""class {dict_name}(TypedDict, total=False):
|
|
53
|
+
{chr(10).join(fields)}"""
|
|
54
|
+
nested_typeddict_code.append(typeddict_code)
|
|
55
|
+
|
|
56
|
+
# Generate __init__ method signature
|
|
57
|
+
required_params = []
|
|
58
|
+
optional_params = []
|
|
59
|
+
all_assignments = []
|
|
60
|
+
|
|
61
|
+
for field_name, field_info in config_class._fields.items():
|
|
62
|
+
field_type = field_info.field_type
|
|
63
|
+
|
|
64
|
+
# Handle nested ConfigMeta classes
|
|
65
|
+
if ConfigMeta.is_instance(field_type):
|
|
66
|
+
type_hint = f"Optional[{field_type.__name__}Dict]"
|
|
67
|
+
param_line = f" {field_name}: {type_hint} = None"
|
|
68
|
+
optional_params.append(param_line)
|
|
69
|
+
else:
|
|
70
|
+
# All params will be set as options here even if the are required in the
|
|
71
|
+
# configMeta
|
|
72
|
+
type_hint = _get_type_string(field_type)
|
|
73
|
+
param_line = f" {field_name}: Optional[{type_hint}] = None"
|
|
74
|
+
optional_params.append(param_line)
|
|
75
|
+
|
|
76
|
+
all_assignments.append(f' "{field_name}": {field_name}')
|
|
77
|
+
|
|
78
|
+
# Combine required params first, then optional params
|
|
79
|
+
all_params = required_params + optional_params
|
|
80
|
+
|
|
81
|
+
# Generate the class code
|
|
82
|
+
newline = "\n"
|
|
83
|
+
comma_newline = ",\n"
|
|
84
|
+
|
|
85
|
+
# Add **kwargs to the parameter list
|
|
86
|
+
if all_params:
|
|
87
|
+
params_with_kwargs = all_params + [" **kwargs"]
|
|
88
|
+
else:
|
|
89
|
+
params_with_kwargs = [" **kwargs"]
|
|
90
|
+
|
|
91
|
+
class_code = f"""class {class_name}:
|
|
92
|
+
def __init__(
|
|
93
|
+
self,
|
|
94
|
+
{comma_newline.join(params_with_kwargs)}
|
|
95
|
+
) -> None:
|
|
96
|
+
self._kwargs = {{
|
|
97
|
+
{comma_newline.join(all_assignments)}
|
|
98
|
+
}}
|
|
99
|
+
# Add any additional kwargs
|
|
100
|
+
self._kwargs.update(kwargs)
|
|
101
|
+
# Remove None values
|
|
102
|
+
self._kwargs = {{k: v for k, v in self._kwargs.items() if v is not None}}
|
|
103
|
+
self._config_class = {config_class.__name__}
|
|
104
|
+
self._config = self.create_config()
|
|
105
|
+
|
|
106
|
+
def create_config(self) -> {config_class.__name__}:
|
|
107
|
+
return {config_class.__name__}.from_dict(self._kwargs)
|
|
108
|
+
|
|
109
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
110
|
+
return self._config.to_dict()"""
|
|
111
|
+
|
|
112
|
+
# Combine all code
|
|
113
|
+
full_code = []
|
|
114
|
+
if nested_typeddict_code:
|
|
115
|
+
full_code.extend(nested_typeddict_code)
|
|
116
|
+
full_code.append("") # Empty line
|
|
117
|
+
full_code.append(class_code)
|
|
118
|
+
|
|
119
|
+
return (newline + newline).join(full_code)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _get_type_string(field_type: Type) -> str:
|
|
123
|
+
"""Convert a type to its string representation for code generation."""
|
|
124
|
+
if field_type == str:
|
|
125
|
+
return "str"
|
|
126
|
+
elif field_type == int:
|
|
127
|
+
return "int"
|
|
128
|
+
elif field_type == float:
|
|
129
|
+
return "float"
|
|
130
|
+
elif field_type == bool:
|
|
131
|
+
return "bool"
|
|
132
|
+
elif hasattr(field_type, "__origin__"):
|
|
133
|
+
# Handle generic types like List[str], Dict[str, str], etc.
|
|
134
|
+
origin = field_type.__origin__
|
|
135
|
+
args = getattr(field_type, "__args__", ())
|
|
136
|
+
|
|
137
|
+
if origin == list:
|
|
138
|
+
if args:
|
|
139
|
+
return f"List[{_get_type_string(args[0])}]"
|
|
140
|
+
return "List[Any]"
|
|
141
|
+
elif origin == dict:
|
|
142
|
+
if len(args) == 2:
|
|
143
|
+
return f"Dict[{_get_type_string(args[0])}, {_get_type_string(args[1])}]"
|
|
144
|
+
return "Dict[str, Any]"
|
|
145
|
+
elif origin == Union:
|
|
146
|
+
# Handle Optional types
|
|
147
|
+
if len(args) == 2 and type(None) in args:
|
|
148
|
+
non_none_type = args[0] if args[1] is type(None) else args[1]
|
|
149
|
+
return f"Optional[{_get_type_string(non_none_type)}]"
|
|
150
|
+
return f"Union[{', '.join(_get_type_string(arg) for arg in args)}]"
|
|
151
|
+
|
|
152
|
+
# Default case - use the type name
|
|
153
|
+
return getattr(field_type, "__name__", str(field_type))
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def generate_typed_classes_module(
|
|
157
|
+
config_classes: List[Type], module_name: str = "typed_configs"
|
|
158
|
+
) -> str:
|
|
159
|
+
"""
|
|
160
|
+
Generate a complete Python module with typed classes for multiple ConfigMeta classes.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
config_classes: List of ConfigMeta classes
|
|
164
|
+
module_name: Name for the generated module
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
Complete Python module code
|
|
168
|
+
"""
|
|
169
|
+
imports = [
|
|
170
|
+
"from typing import Optional, List, Dict, Any, TypedDict",
|
|
171
|
+
"from .unified_config import "
|
|
172
|
+
+ ", ".join(cls.__name__ for cls in config_classes),
|
|
173
|
+
]
|
|
174
|
+
|
|
175
|
+
class_codes = []
|
|
176
|
+
for config_class in config_classes:
|
|
177
|
+
class_codes.append(generate_typed_class_code(config_class))
|
|
178
|
+
|
|
179
|
+
# Use string concatenation instead of f-string with backslashes
|
|
180
|
+
newline = "\n"
|
|
181
|
+
module_code = (
|
|
182
|
+
'"""'
|
|
183
|
+
+ newline
|
|
184
|
+
+ "Auto-generated typed classes for ConfigMeta classes."
|
|
185
|
+
+ newline
|
|
186
|
+
+ newline
|
|
187
|
+
+ "This module provides IDE-friendly typed interfaces for all configuration classes."
|
|
188
|
+
+ newline
|
|
189
|
+
+ '"""'
|
|
190
|
+
+ newline
|
|
191
|
+
+ newline
|
|
192
|
+
+ newline.join(imports)
|
|
193
|
+
+ newline
|
|
194
|
+
+ newline
|
|
195
|
+
+ (newline + newline).join(class_codes)
|
|
196
|
+
+ newline
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
return module_code
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def create_typed_init_class_dynamic(config_class: Type) -> Type:
|
|
203
|
+
"""
|
|
204
|
+
Dynamically create a typed init class with proper IDE support.
|
|
205
|
+
|
|
206
|
+
This creates the class at runtime but with proper type annotations
|
|
207
|
+
that IDEs can understand.
|
|
208
|
+
"""
|
|
209
|
+
if not hasattr(config_class, "_fields"):
|
|
210
|
+
raise ValueError(f"Class {config_class.__name__} is not a ConfigMeta class")
|
|
211
|
+
|
|
212
|
+
class_name = f"Typed{config_class.__name__}"
|
|
213
|
+
|
|
214
|
+
# Create the init method with proper signature
|
|
215
|
+
def create_init_method():
|
|
216
|
+
# Build the signature dynamically
|
|
217
|
+
sig_params = []
|
|
218
|
+
annotations = {"return": None}
|
|
219
|
+
|
|
220
|
+
for field_name, field_info in config_class._fields.items():
|
|
221
|
+
field_type = field_info.field_type
|
|
222
|
+
|
|
223
|
+
# Handle nested ConfigMeta classes
|
|
224
|
+
if ConfigMeta.is_instance(field_type):
|
|
225
|
+
field_type = Dict[str, Any] # Use Dict for nested configs
|
|
226
|
+
|
|
227
|
+
# Handle Optional fields
|
|
228
|
+
if not field_info.required:
|
|
229
|
+
field_type = Optional[field_type]
|
|
230
|
+
|
|
231
|
+
annotations[field_name] = field_type
|
|
232
|
+
|
|
233
|
+
def __init__(self, **kwargs):
|
|
234
|
+
# Validate kwargs
|
|
235
|
+
required_fields = {
|
|
236
|
+
name for name, info in config_class._fields.items() if info.required
|
|
237
|
+
}
|
|
238
|
+
provided_fields = set(kwargs.keys())
|
|
239
|
+
valid_fields = set(config_class._fields.keys())
|
|
240
|
+
|
|
241
|
+
# Check required fields
|
|
242
|
+
missing_fields = required_fields - provided_fields
|
|
243
|
+
if missing_fields:
|
|
244
|
+
raise ValueError(
|
|
245
|
+
f"Missing required fields: {', '.join(missing_fields)}"
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
# Check for unknown fields - but allow them for flexibility
|
|
249
|
+
unknown_fields = provided_fields - valid_fields
|
|
250
|
+
if unknown_fields:
|
|
251
|
+
print(
|
|
252
|
+
f"Warning: Unknown fields will be passed through: {', '.join(unknown_fields)}"
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
self._kwargs = kwargs
|
|
256
|
+
self._config_class = config_class
|
|
257
|
+
|
|
258
|
+
# Set annotations
|
|
259
|
+
__init__.__annotations__ = annotations
|
|
260
|
+
return __init__
|
|
261
|
+
|
|
262
|
+
def create_config(self) -> config_class:
|
|
263
|
+
"""Create and return the ConfigMeta class instance."""
|
|
264
|
+
return config_class.from_dict(self._kwargs)
|
|
265
|
+
|
|
266
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
267
|
+
"""Return the raw kwargs as a dictionary."""
|
|
268
|
+
return self._kwargs.copy()
|
|
269
|
+
|
|
270
|
+
def __repr__(self) -> str:
|
|
271
|
+
return f"{class_name}({self._kwargs})"
|
|
272
|
+
|
|
273
|
+
# Create the class
|
|
274
|
+
init_method = create_init_method()
|
|
275
|
+
|
|
276
|
+
TypedClass = type(
|
|
277
|
+
class_name,
|
|
278
|
+
(object,),
|
|
279
|
+
{
|
|
280
|
+
"__init__": init_method,
|
|
281
|
+
"create_config": create_config,
|
|
282
|
+
"to_dict": to_dict,
|
|
283
|
+
"__repr__": __repr__,
|
|
284
|
+
"__module__": __name__,
|
|
285
|
+
"__qualname__": class_name,
|
|
286
|
+
},
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
return TypedClass
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
# Auto-generate and write typed classes to a file
|
|
293
|
+
def generate_typed_classes_file(output_file: str = None):
|
|
294
|
+
"""
|
|
295
|
+
Generate typed classes and write them to a file for IDE support.
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
output_file: Path to write the generated classes. If None, prints to stdout.
|
|
299
|
+
"""
|
|
300
|
+
from .unified_config import CoreConfig
|
|
301
|
+
|
|
302
|
+
config_classes = [CoreConfig]
|
|
303
|
+
|
|
304
|
+
module_code = generate_typed_classes_module(config_classes)
|
|
305
|
+
|
|
306
|
+
if output_file:
|
|
307
|
+
with open(output_file, "w") as f:
|
|
308
|
+
f.write(module_code)
|
|
309
|
+
print(f"Generated typed classes written to {output_file}")
|
|
310
|
+
else:
|
|
311
|
+
print(module_code)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
# Example usage and testing
|
|
315
|
+
if __name__ == "__main__":
|
|
316
|
+
# Generate typed classes file
|
|
317
|
+
generate_typed_classes_file(os.path.join(current_dir, "typed_configs.py"))
|