ob-metaflow-extensions 1.1.175rc1__py2.py3-none-any.whl → 1.1.175rc2__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/apps/core/app_cli.py +42 -374
- metaflow_extensions/outerbounds/plugins/apps/core/app_config.py +45 -225
- metaflow_extensions/outerbounds/plugins/apps/core/code_package/code_packager.py +16 -15
- metaflow_extensions/outerbounds/plugins/apps/core/config/__init__.py +11 -0
- metaflow_extensions/outerbounds/plugins/apps/core/config/cli_generator.py +161 -0
- metaflow_extensions/outerbounds/plugins/apps/core/config/config_utils.py +768 -0
- metaflow_extensions/outerbounds/plugins/apps/core/config/schema_export.py +285 -0
- metaflow_extensions/outerbounds/plugins/apps/core/config/unified_config.py +870 -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/experimental/__init__.py +4 -36
- metaflow_extensions/outerbounds/plugins/apps/core/validations.py +4 -9
- {ob_metaflow_extensions-1.1.175rc1.dist-info → ob_metaflow_extensions-1.1.175rc2.dist-info}/METADATA +1 -1
- {ob_metaflow_extensions-1.1.175rc1.dist-info → ob_metaflow_extensions-1.1.175rc2.dist-info}/RECORD +16 -13
- 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.175rc2.dist-info}/WHEEL +0 -0
- {ob_metaflow_extensions-1.1.175rc1.dist-info → ob_metaflow_extensions-1.1.175rc2.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
|