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.

Files changed (18) hide show
  1. metaflow_extensions/outerbounds/plugins/apps/core/app_cli.py +42 -374
  2. metaflow_extensions/outerbounds/plugins/apps/core/app_config.py +45 -225
  3. metaflow_extensions/outerbounds/plugins/apps/core/code_package/code_packager.py +16 -15
  4. metaflow_extensions/outerbounds/plugins/apps/core/config/__init__.py +11 -0
  5. metaflow_extensions/outerbounds/plugins/apps/core/config/cli_generator.py +161 -0
  6. metaflow_extensions/outerbounds/plugins/apps/core/config/config_utils.py +768 -0
  7. metaflow_extensions/outerbounds/plugins/apps/core/config/schema_export.py +285 -0
  8. metaflow_extensions/outerbounds/plugins/apps/core/config/unified_config.py +870 -0
  9. metaflow_extensions/outerbounds/plugins/apps/core/config_schema.yaml +217 -211
  10. metaflow_extensions/outerbounds/plugins/apps/core/dependencies.py +3 -3
  11. metaflow_extensions/outerbounds/plugins/apps/core/experimental/__init__.py +4 -36
  12. metaflow_extensions/outerbounds/plugins/apps/core/validations.py +4 -9
  13. {ob_metaflow_extensions-1.1.175rc1.dist-info → ob_metaflow_extensions-1.1.175rc2.dist-info}/METADATA +1 -1
  14. {ob_metaflow_extensions-1.1.175rc1.dist-info → ob_metaflow_extensions-1.1.175rc2.dist-info}/RECORD +16 -13
  15. metaflow_extensions/outerbounds/plugins/apps/core/cli_to_config.py +0 -99
  16. metaflow_extensions/outerbounds/plugins/apps/core/config_schema_autogen.json +0 -336
  17. {ob_metaflow_extensions-1.1.175rc1.dist-info → ob_metaflow_extensions-1.1.175rc2.dist-info}/WHEEL +0 -0
  18. {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