ob-metaflow-extensions 1.1.175rc2__py2.py3-none-any.whl → 1.1.175rc4__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 (21) hide show
  1. metaflow_extensions/outerbounds/plugins/__init__.py +1 -2
  2. metaflow_extensions/outerbounds/plugins/apps/app_cli.py +0 -3
  3. metaflow_extensions/outerbounds/plugins/apps/app_deploy_decorator.py +112 -0
  4. metaflow_extensions/outerbounds/plugins/apps/core/__init__.py +9 -0
  5. metaflow_extensions/outerbounds/plugins/apps/core/_state_machine.py +41 -15
  6. metaflow_extensions/outerbounds/plugins/apps/core/app_config.py +6 -6
  7. metaflow_extensions/outerbounds/plugins/apps/core/capsule.py +25 -9
  8. metaflow_extensions/outerbounds/plugins/apps/core/config/__init__.py +1 -0
  9. metaflow_extensions/outerbounds/plugins/apps/core/config/config_utils.py +110 -50
  10. metaflow_extensions/outerbounds/plugins/apps/core/config/typed_configs.py +131 -0
  11. metaflow_extensions/outerbounds/plugins/apps/core/config/typed_init_generator.py +353 -0
  12. metaflow_extensions/outerbounds/plugins/apps/core/config/unified_config.py +176 -50
  13. metaflow_extensions/outerbounds/plugins/apps/core/config_schema.yaml +4 -4
  14. metaflow_extensions/outerbounds/plugins/apps/core/deployer.py +132 -0
  15. metaflow_extensions/outerbounds/plugins/apps/core/perimeters.py +44 -2
  16. metaflow_extensions/outerbounds/toplevel/global_aliases_for_metaflow_package.py +1 -0
  17. metaflow_extensions/outerbounds/toplevel/ob_internal.py +1 -0
  18. {ob_metaflow_extensions-1.1.175rc2.dist-info → ob_metaflow_extensions-1.1.175rc4.dist-info}/METADATA +1 -1
  19. {ob_metaflow_extensions-1.1.175rc2.dist-info → ob_metaflow_extensions-1.1.175rc4.dist-info}/RECORD +21 -18
  20. {ob_metaflow_extensions-1.1.175rc2.dist-info → ob_metaflow_extensions-1.1.175rc4.dist-info}/WHEEL +0 -0
  21. {ob_metaflow_extensions-1.1.175rc2.dist-info → ob_metaflow_extensions-1.1.175rc4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,131 @@
1
+ """
2
+ Auto-generated typed classes for ConfigMeta classes.
3
+
4
+ This module provides IDE-friendly typed interfaces for all configuration classes.
5
+ 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.
6
+ The CoreConfig class is setup in a way that if any additionally params are missed out from being auto-generated then it will not affect the core functionality of the programmatic API.
7
+ The new parameters will just not show up in IDE autocompletions.
8
+ It is fine if this file is not regularly updated by running the script in the .pre-commit-config.app-changes.yaml
9
+ but it is recommended that this file not be deleted or manually edited.
10
+
11
+ """
12
+
13
+ from typing import Optional, List, Dict, Any
14
+ from .unified_config import CoreConfig
15
+
16
+ import sys
17
+ from typing import TYPE_CHECKING
18
+
19
+ # on 3.8+ use the stdlib TypedDict;
20
+ # in TYPE_CHECKING blocks mypy/pyright still pick it up on older Pythons
21
+ if sys.version_info >= (3, 8):
22
+ from typing import TypedDict
23
+ else:
24
+ if TYPE_CHECKING:
25
+ # for the benefit of type-checkers
26
+ from typing import TypedDict # noqa: F401
27
+ # runtime no-op TypedDict shim
28
+ class _TypedDictMeta(type):
29
+ def __new__(cls, name, bases, namespace, total=True):
30
+ # ignore total at runtime
31
+ return super().__new__(cls, name, bases, namespace)
32
+
33
+ class TypedDict(dict, metaclass=_TypedDictMeta):
34
+ # Runtime stand-in for typing.TypedDict on <3.8.
35
+ pass
36
+
37
+
38
+ class ResourceConfigDict(TypedDict, total=False):
39
+ cpu: Optional[str]
40
+ memory: Optional[str]
41
+ gpu: Optional[str]
42
+ disk: Optional[str]
43
+
44
+
45
+ class AuthConfigDict(TypedDict, total=False):
46
+ type: Optional[str]
47
+ public: Optional[bool]
48
+
49
+
50
+ class ReplicaConfigDict(TypedDict, total=False):
51
+ fixed: Optional[int]
52
+ min: Optional[int]
53
+ max: Optional[int]
54
+
55
+
56
+ class DependencyConfigDict(TypedDict, total=False):
57
+ from_requirements_file: Optional[str]
58
+ from_pyproject_toml: Optional[str]
59
+ python: Optional[str]
60
+ pypi: Optional[dict]
61
+ conda: Optional[dict]
62
+
63
+
64
+ class PackageConfigDict(TypedDict, total=False):
65
+ src_path: Optional[str]
66
+ suffixes: Optional[list]
67
+
68
+
69
+ class TypedCoreConfig:
70
+ def __init__(
71
+ self,
72
+ name: Optional[str] = None,
73
+ port: Optional[int] = None,
74
+ description: Optional[str] = None,
75
+ app_type: Optional[str] = None,
76
+ image: Optional[str] = None,
77
+ tags: Optional[list] = None,
78
+ secrets: Optional[list] = None,
79
+ compute_pools: Optional[list] = None,
80
+ environment: Optional[dict] = None,
81
+ commands: Optional[list] = None,
82
+ resources: Optional[ResourceConfigDict] = None,
83
+ auth: Optional[AuthConfigDict] = None,
84
+ replicas: Optional[ReplicaConfigDict] = None,
85
+ dependencies: Optional[DependencyConfigDict] = None,
86
+ package: Optional[PackageConfigDict] = None,
87
+ no_deps: Optional[bool] = None,
88
+ force_upgrade: Optional[bool] = None,
89
+ persistence: Optional[str] = None,
90
+ project: Optional[str] = None,
91
+ branch: Optional[str] = None,
92
+ models: Optional[list] = None,
93
+ data: Optional[list] = None,
94
+ **kwargs
95
+ ) -> None:
96
+ self._kwargs = {
97
+ "name": name,
98
+ "port": port,
99
+ "description": description,
100
+ "app_type": app_type,
101
+ "image": image,
102
+ "tags": tags,
103
+ "secrets": secrets,
104
+ "compute_pools": compute_pools,
105
+ "environment": environment,
106
+ "commands": commands,
107
+ "resources": resources,
108
+ "auth": auth,
109
+ "replicas": replicas,
110
+ "dependencies": dependencies,
111
+ "package": package,
112
+ "no_deps": no_deps,
113
+ "force_upgrade": force_upgrade,
114
+ "persistence": persistence,
115
+ "project": project,
116
+ "branch": branch,
117
+ "models": models,
118
+ "data": data,
119
+ }
120
+ # Add any additional kwargs
121
+ self._kwargs.update(kwargs)
122
+ # Remove None values
123
+ self._kwargs = {k: v for k, v in self._kwargs.items() if v is not None}
124
+ self._config_class = CoreConfig
125
+ self._config = self.create_config()
126
+
127
+ def create_config(self) -> CoreConfig:
128
+ return CoreConfig.from_dict(self._kwargs)
129
+
130
+ def to_dict(self) -> Dict[str, Any]:
131
+ return self._config.to_dict()
@@ -0,0 +1,353 @@
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
+ 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 generate_typed_class_code(config_class: Type) -> str:
41
+ """
42
+ Generate the actual Python code for a typed class that IDEs can understand.
43
+
44
+ Args:
45
+ config_class: A class that inherits from ConfigMeta
46
+
47
+ Returns:
48
+ Python code string for the typed class
49
+ """
50
+ if not hasattr(config_class, "_fields"):
51
+ raise ValueError(f"Class {config_class.__name__} is not a ConfigMeta class")
52
+
53
+ class_name = f"Typed{config_class.__name__}"
54
+
55
+ # Generate TypedDict for nested configs
56
+ nested_typeddict_code = []
57
+
58
+ # First pass: collect all nested configs
59
+ nested_configs = {}
60
+ for field_name, field_info in config_class._fields.items():
61
+ if ConfigMeta.is_instance(field_info.field_type):
62
+ nested_configs[field_info.field_type.__name__] = field_info.field_type
63
+
64
+ # Generate TypedDict classes for nested configs
65
+ for nested_name, nested_class in nested_configs.items():
66
+ dict_name = f"{nested_name}Dict"
67
+ fields = []
68
+
69
+ for field_name, field_info in nested_class._fields.items():
70
+ field_type = _get_type_string(field_info.field_type)
71
+ if not field_info.required:
72
+ field_type = f"Optional[{field_type}]"
73
+ fields.append(f" {field_name}: {field_type}")
74
+
75
+ typeddict_code = f"""class {dict_name}(TypedDict, total=False):
76
+ {chr(10).join(fields)}"""
77
+ nested_typeddict_code.append(typeddict_code)
78
+
79
+ # Generate __init__ method signature
80
+ required_params = []
81
+ optional_params = []
82
+ all_assignments = []
83
+
84
+ for field_name, field_info in config_class._fields.items():
85
+ field_type = field_info.field_type
86
+
87
+ # Handle nested ConfigMeta classes
88
+ if ConfigMeta.is_instance(field_type):
89
+ type_hint = f"Optional[{field_type.__name__}Dict]"
90
+ param_line = f" {field_name}: {type_hint} = None"
91
+ optional_params.append(param_line)
92
+ else:
93
+ # All params will be set as options here even if the are required in the
94
+ # configMeta
95
+ type_hint = _get_type_string(field_type)
96
+ param_line = f" {field_name}: Optional[{type_hint}] = None"
97
+ optional_params.append(param_line)
98
+
99
+ all_assignments.append(f' "{field_name}": {field_name}')
100
+
101
+ # Combine required params first, then optional params
102
+ all_params = required_params + optional_params
103
+
104
+ # Generate the class code
105
+ newline = "\n"
106
+ comma_newline = ",\n"
107
+
108
+ # Add **kwargs to the parameter list
109
+ if all_params:
110
+ params_with_kwargs = all_params + [" **kwargs"]
111
+ else:
112
+ params_with_kwargs = [" **kwargs"]
113
+
114
+ class_code = f"""class {class_name}:
115
+ def __init__(
116
+ self,
117
+ {comma_newline.join(params_with_kwargs)}
118
+ ) -> None:
119
+ self._kwargs = {{
120
+ {comma_newline.join(all_assignments)}
121
+ }}
122
+ # Add any additional kwargs
123
+ self._kwargs.update(kwargs)
124
+ # Remove None values
125
+ self._kwargs = {{k: v for k, v in self._kwargs.items() if v is not None}}
126
+ self._config_class = {config_class.__name__}
127
+ self._config = self.create_config()
128
+
129
+ def create_config(self) -> {config_class.__name__}:
130
+ return {config_class.__name__}.from_dict(self._kwargs)
131
+
132
+ def to_dict(self) -> Dict[str, Any]:
133
+ return self._config.to_dict()"""
134
+
135
+ # Combine all code
136
+ full_code = []
137
+ if nested_typeddict_code:
138
+ full_code.extend(nested_typeddict_code)
139
+ full_code.append("") # Empty line
140
+ full_code.append(class_code)
141
+
142
+ return (newline + newline).join(full_code)
143
+
144
+
145
+ def _get_type_string(field_type: Type) -> str:
146
+ """Convert a type to its string representation for code generation."""
147
+ if field_type == str:
148
+ return "str"
149
+ elif field_type == int:
150
+ return "int"
151
+ elif field_type == float:
152
+ return "float"
153
+ elif field_type == bool:
154
+ return "bool"
155
+ elif hasattr(field_type, "__origin__"):
156
+ # Handle generic types like List[str], Dict[str, str], etc.
157
+ origin = field_type.__origin__
158
+ args = getattr(field_type, "__args__", ())
159
+
160
+ if origin == list:
161
+ if args:
162
+ return f"List[{_get_type_string(args[0])}]"
163
+ return "List[Any]"
164
+ elif origin == dict:
165
+ if len(args) == 2:
166
+ return f"Dict[{_get_type_string(args[0])}, {_get_type_string(args[1])}]"
167
+ return "Dict[str, Any]"
168
+ elif origin == Union:
169
+ # Handle Optional types
170
+ if len(args) == 2 and type(None) in args:
171
+ non_none_type = args[0] if args[1] is type(None) else args[1]
172
+ return f"Optional[{_get_type_string(non_none_type)}]"
173
+ return f"Union[{', '.join(_get_type_string(arg) for arg in args)}]"
174
+
175
+ # Default case - use the type name
176
+ return getattr(field_type, "__name__", str(field_type))
177
+
178
+
179
+ def generate_typed_classes_module(
180
+ config_classes: List[Type], module_name: str = "typed_configs"
181
+ ) -> str:
182
+ """
183
+ Generate a complete Python module with typed classes for multiple ConfigMeta classes.
184
+
185
+ Args:
186
+ config_classes: List of ConfigMeta classes
187
+ module_name: Name for the generated module
188
+
189
+ Returns:
190
+ Complete Python module code
191
+ """
192
+ imports = [
193
+ "from typing import Optional, List, Dict, Any",
194
+ "from .unified_config import "
195
+ + ", ".join(cls.__name__ for cls in config_classes),
196
+ TYPED_DICT_IMPORT,
197
+ ]
198
+
199
+ class_codes = []
200
+ for config_class in config_classes:
201
+ class_codes.append(generate_typed_class_code(config_class))
202
+
203
+ # Use string concatenation instead of f-string with backslashes
204
+ newline = "\n"
205
+ module_code = (
206
+ '"""'
207
+ + newline
208
+ + "Auto-generated typed classes for ConfigMeta classes."
209
+ + newline
210
+ + newline
211
+ + "This module provides IDE-friendly typed interfaces for all configuration classes."
212
+ + newline
213
+ + "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."
214
+ + newline
215
+ + "The CoreConfig class is setup in a way that if any additionally params are missed out from being auto-generated "
216
+ + "then it will not affect the core functionality of the programmatic API."
217
+ + newline
218
+ + "The new parameters will just not show up in IDE autocompletions."
219
+ + newline
220
+ + "It is fine if this file is not regularly updated by running the script in the .pre-commit-config.app-changes.yaml"
221
+ + newline
222
+ + "but it is recommended that this file not be deleted or manually edited."
223
+ + newline
224
+ + newline
225
+ + '"""'
226
+ + newline
227
+ + newline
228
+ + newline.join(imports)
229
+ + newline
230
+ + newline
231
+ + (newline + newline).join(class_codes)
232
+ + newline
233
+ )
234
+
235
+ return module_code
236
+
237
+
238
+ def create_typed_init_class_dynamic(config_class: Type) -> Type:
239
+ """
240
+ Dynamically create a typed init class with proper IDE support.
241
+
242
+ This creates the class at runtime but with proper type annotations
243
+ that IDEs can understand.
244
+ """
245
+ if not hasattr(config_class, "_fields"):
246
+ raise ValueError(f"Class {config_class.__name__} is not a ConfigMeta class")
247
+
248
+ class_name = f"Typed{config_class.__name__}"
249
+
250
+ # Create the init method with proper signature
251
+ def create_init_method():
252
+ # Build the signature dynamically
253
+ sig_params = []
254
+ annotations = {"return": None}
255
+
256
+ for field_name, field_info in config_class._fields.items():
257
+ field_type = field_info.field_type
258
+
259
+ # Handle nested ConfigMeta classes
260
+ if ConfigMeta.is_instance(field_type):
261
+ field_type = Dict[str, Any] # Use Dict for nested configs
262
+
263
+ # Handle Optional fields
264
+ if not field_info.required:
265
+ field_type = Optional[field_type]
266
+
267
+ annotations[field_name] = field_type
268
+
269
+ def __init__(self, **kwargs):
270
+ # Validate kwargs
271
+ required_fields = {
272
+ name for name, info in config_class._fields.items() if info.required
273
+ }
274
+ provided_fields = set(kwargs.keys())
275
+ valid_fields = set(config_class._fields.keys())
276
+
277
+ # Check required fields
278
+ missing_fields = required_fields - provided_fields
279
+ if missing_fields:
280
+ raise ValueError(
281
+ f"Missing required fields: {', '.join(missing_fields)}"
282
+ )
283
+
284
+ # Check for unknown fields - but allow them for flexibility
285
+ unknown_fields = provided_fields - valid_fields
286
+ if unknown_fields:
287
+ print(
288
+ f"Warning: Unknown fields will be passed through: {', '.join(unknown_fields)}"
289
+ )
290
+
291
+ self._kwargs = kwargs
292
+ self._config_class = config_class
293
+
294
+ # Set annotations
295
+ __init__.__annotations__ = annotations
296
+ return __init__
297
+
298
+ def create_config(self) -> config_class:
299
+ """Create and return the ConfigMeta class instance."""
300
+ return config_class.from_dict(self._kwargs)
301
+
302
+ def to_dict(self) -> Dict[str, Any]:
303
+ """Return the raw kwargs as a dictionary."""
304
+ return self._kwargs.copy()
305
+
306
+ def __repr__(self) -> str:
307
+ return f"{class_name}({self._kwargs})"
308
+
309
+ # Create the class
310
+ init_method = create_init_method()
311
+
312
+ TypedClass = type(
313
+ class_name,
314
+ (object,),
315
+ {
316
+ "__init__": init_method,
317
+ "create_config": create_config,
318
+ "to_dict": to_dict,
319
+ "__repr__": __repr__,
320
+ "__module__": __name__,
321
+ "__qualname__": class_name,
322
+ },
323
+ )
324
+
325
+ return TypedClass
326
+
327
+
328
+ # Auto-generate and write typed classes to a file
329
+ def generate_typed_classes_file(output_file: str = None):
330
+ """
331
+ Generate typed classes and write them to a file for IDE support.
332
+
333
+ Args:
334
+ output_file: Path to write the generated classes. If None, prints to stdout.
335
+ """
336
+ from .unified_config import CoreConfig
337
+
338
+ config_classes = [CoreConfig]
339
+
340
+ module_code = generate_typed_classes_module(config_classes)
341
+
342
+ if output_file:
343
+ with open(output_file, "w") as f:
344
+ f.write(module_code)
345
+ print(f"Generated typed classes written to {output_file}")
346
+ else:
347
+ print(module_code)
348
+
349
+
350
+ # Example usage and testing
351
+ if __name__ == "__main__":
352
+ # Generate typed classes file
353
+ generate_typed_classes_file(os.path.join(current_dir, "typed_configs.py"))