ob-metaflow-extensions 1.4.33__py2.py3-none-any.whl → 1.6.2__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.
Files changed (38) hide show
  1. metaflow_extensions/outerbounds/plugins/__init__.py +8 -1
  2. metaflow_extensions/outerbounds/plugins/apps/core/__init__.py +8 -2
  3. metaflow_extensions/outerbounds/plugins/apps/core/_state_machine.py +6 -6
  4. metaflow_extensions/outerbounds/plugins/apps/core/app_config.py +1 -19
  5. metaflow_extensions/outerbounds/plugins/apps/core/app_deploy_decorator.py +333 -0
  6. metaflow_extensions/outerbounds/plugins/apps/core/capsule.py +150 -79
  7. metaflow_extensions/outerbounds/plugins/apps/core/config/__init__.py +4 -1
  8. metaflow_extensions/outerbounds/plugins/apps/core/config/cli_generator.py +4 -0
  9. metaflow_extensions/outerbounds/plugins/apps/core/config/config_utils.py +103 -5
  10. metaflow_extensions/outerbounds/plugins/apps/core/config/schema_export.py +12 -1
  11. metaflow_extensions/outerbounds/plugins/apps/core/config/typed_configs.py +100 -6
  12. metaflow_extensions/outerbounds/plugins/apps/core/config/typed_init_generator.py +141 -2
  13. metaflow_extensions/outerbounds/plugins/apps/core/config/unified_config.py +74 -37
  14. metaflow_extensions/outerbounds/plugins/apps/core/config_schema.yaml +6 -6
  15. metaflow_extensions/outerbounds/plugins/apps/core/dependencies.py +2 -2
  16. metaflow_extensions/outerbounds/plugins/apps/core/deployer.py +1102 -105
  17. metaflow_extensions/outerbounds/plugins/apps/core/exceptions.py +341 -0
  18. metaflow_extensions/outerbounds/plugins/apps/core/perimeters.py +42 -6
  19. metaflow_extensions/outerbounds/plugins/aws/assume_role_decorator.py +43 -3
  20. metaflow_extensions/outerbounds/plugins/fast_bakery/baker.py +10 -1
  21. metaflow_extensions/outerbounds/plugins/optuna/__init__.py +2 -1
  22. metaflow_extensions/outerbounds/plugins/snowflake/snowflake.py +37 -7
  23. metaflow_extensions/outerbounds/plugins/snowpark/snowpark.py +18 -8
  24. metaflow_extensions/outerbounds/plugins/snowpark/snowpark_cli.py +6 -0
  25. metaflow_extensions/outerbounds/plugins/snowpark/snowpark_client.py +39 -15
  26. metaflow_extensions/outerbounds/plugins/snowpark/snowpark_decorator.py +5 -2
  27. metaflow_extensions/outerbounds/plugins/snowpark/snowpark_job.py +2 -2
  28. metaflow_extensions/outerbounds/remote_config.py +20 -7
  29. metaflow_extensions/outerbounds/toplevel/apps/__init__.py +9 -0
  30. metaflow_extensions/outerbounds/toplevel/apps/exceptions.py +11 -0
  31. metaflow_extensions/outerbounds/toplevel/global_aliases_for_metaflow_package.py +1 -1
  32. metaflow_extensions/outerbounds/toplevel/ob_internal.py +1 -1
  33. {ob_metaflow_extensions-1.4.33.dist-info → ob_metaflow_extensions-1.6.2.dist-info}/METADATA +2 -2
  34. {ob_metaflow_extensions-1.4.33.dist-info → ob_metaflow_extensions-1.6.2.dist-info}/RECORD +36 -34
  35. metaflow_extensions/outerbounds/plugins/apps/app_deploy_decorator.py +0 -146
  36. metaflow_extensions/outerbounds/plugins/apps/core/app_cli.py +0 -1200
  37. {ob_metaflow_extensions-1.4.33.dist-info → ob_metaflow_extensions-1.6.2.dist-info}/WHEEL +0 -0
  38. {ob_metaflow_extensions-1.4.33.dist-info → ob_metaflow_extensions-1.6.2.dist-info}/top_level.txt +0 -0
@@ -73,6 +73,100 @@ class PackageConfigDict(TypedDict, total=False):
73
73
 
74
74
 
75
75
  class TypedCoreConfig:
76
+ """
77
+ Parameters
78
+ ----------
79
+ name : str, optional
80
+ The name of the app to deploy.
81
+
82
+ port : int, optional
83
+ Port where the app is hosted. When deployed this will be port on which we will deploy the app.
84
+
85
+ description : str, optional
86
+ The description of the app to deploy.
87
+
88
+ app_type : str, optional
89
+ The User defined type of app to deploy. Its only used for bookkeeping purposes.
90
+
91
+ image : str, optional
92
+ The Docker image to deploy with the App.
93
+
94
+ tags : list, optional
95
+ The tags of the app to deploy.
96
+
97
+ secrets : list, optional
98
+ Outerbounds integrations to attach to the app. You can use the value you set in the `@secrets` decorator in your code.
99
+
100
+ compute_pools : list, optional
101
+ A list of compute pools to deploy the app to.
102
+
103
+ environment : dict, optional
104
+ Environment variables to deploy with the App.
105
+
106
+ commands : list, optional
107
+ A list of commands to run the app with.
108
+
109
+ resources : ResourceConfigDict, optional
110
+ Resource configuration for the app.
111
+ - cpu (str)
112
+ CPU requests
113
+ - memory (str)
114
+ Memory requests
115
+ - gpu (str)
116
+ GPU requests
117
+ - disk (str)
118
+ Storage disk size.
119
+ - shared_memory (str)
120
+ Shared memory
121
+
122
+ auth : AuthConfigDict, optional
123
+ Auth related configurations.
124
+ - type (str)
125
+ The type of authentication to use for the app.
126
+ - public (bool)
127
+ Whether the app is public or not.
128
+
129
+ replicas : ReplicaConfigDict, optional
130
+ The number of replicas to deploy the app with.
131
+ - fixed (int)
132
+ The fixed number of replicas to deploy the app with. If min and max are set, this will raise an error.
133
+ - min (int)
134
+ The minimum number of replicas to deploy the app with.
135
+ - max (int)
136
+ The maximum number of replicas to deploy the app with.
137
+ - scaling_policy (ScalingPolicyConfigDict)
138
+ Scaling policy defines the the metric based on which the replicas will horizontally scale. If min and max replicas are set and are not the same, then a scaling policy will be applied. Default scaling policies can be 60 rpm (ie 1 rps).
139
+ - rpm (int)
140
+ Scale up replicas when the requests per minute crosses this threshold. If nothing is provided and the replicas.max and replicas.min is set then the default rpm would be 60.
141
+
142
+ code_package : tuple, optional
143
+ Pre-packaged code from package_code(). A PackagedCode namedtuple containing url and key.
144
+
145
+ force_upgrade : bool, optional
146
+ Force upgrade the app even if it is currently being upgraded.
147
+
148
+ persistence : str, optional
149
+ The persistence mode to deploy the app with.
150
+ [Experimental] May change in the future.
151
+
152
+ project : str, optional
153
+ The project name to deploy the app to.
154
+ [Experimental] May change in the future.
155
+
156
+ branch : str, optional
157
+ The branch name to deploy the app to.
158
+ [Experimental] May change in the future.
159
+
160
+ models : list, optional
161
+ [Experimental] May change in the future.
162
+
163
+ data : list, optional
164
+ [Experimental] May change in the future.
165
+
166
+ generate_static_url : bool, optional
167
+ Generate a static URL for the app based on its name.
168
+ """
169
+
76
170
  def __init__(
77
171
  self,
78
172
  name: Optional[str] = None,
@@ -88,9 +182,7 @@ class TypedCoreConfig:
88
182
  resources: Optional[ResourceConfigDict] = None,
89
183
  auth: Optional[AuthConfigDict] = None,
90
184
  replicas: Optional[ReplicaConfigDict] = None,
91
- dependencies: Optional[DependencyConfigDict] = None,
92
- package: Optional[PackageConfigDict] = None,
93
- no_deps: Optional[bool] = None,
185
+ code_package: Optional[tuple] = None,
94
186
  force_upgrade: Optional[bool] = None,
95
187
  persistence: Optional[str] = None,
96
188
  project: Optional[str] = None,
@@ -114,9 +206,7 @@ class TypedCoreConfig:
114
206
  "resources": resources,
115
207
  "auth": auth,
116
208
  "replicas": replicas,
117
- "dependencies": dependencies,
118
- "package": package,
119
- "no_deps": no_deps,
209
+ "code_package": code_package,
120
210
  "force_upgrade": force_upgrade,
121
211
  "persistence": persistence,
122
212
  "project": project,
@@ -131,9 +221,13 @@ class TypedCoreConfig:
131
221
  self._kwargs = {k: v for k, v in self._kwargs.items() if v is not None}
132
222
  self._config_class = CoreConfig
133
223
  self._config = self.create_config()
224
+ self._init()
134
225
 
135
226
  def create_config(self) -> CoreConfig:
136
227
  return CoreConfig.from_dict(self._kwargs)
137
228
 
138
229
  def to_dict(self) -> Dict[str, Any]:
139
230
  return self._config.to_dict()
231
+
232
+ def _init(self):
233
+ raise NotImplementedError
@@ -43,6 +43,10 @@ def collect_nested_configs_recursive(
43
43
  """
44
44
  Recursively collect all nested ConfigMeta classes from a config class.
45
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
+
46
50
  Args:
47
51
  config_class: A class that inherits from ConfigMeta
48
52
  visited: Set of already visited class names to avoid infinite recursion
@@ -61,7 +65,7 @@ def collect_nested_configs_recursive(
61
65
 
62
66
  visited.add(config_class.__name__)
63
67
 
64
- # First pass: collect immediate nested configs
68
+ # First pass: collect immediate nested configs (all of them for TypedDict generation)
65
69
  for field_name, field_info in config_class._fields.items():
66
70
  if ConfigMeta.is_instance(field_info.field_type):
67
71
  nested_class = field_info.field_type
@@ -76,6 +80,119 @@ def collect_nested_configs_recursive(
76
80
  return nested_configs
77
81
 
78
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
+
79
196
  def generate_typed_class_code(config_class: Type) -> str:
80
197
  """
81
198
  Generate the actual Python code for a typed class that IDEs can understand.
@@ -98,6 +215,7 @@ def generate_typed_class_code(config_class: Type) -> str:
98
215
  nested_configs = collect_nested_configs_recursive(config_class)
99
216
 
100
217
  # Generate TypedDict classes for all nested configs
218
+ # Note: TypedDicts include ALL fields for type completeness (no context filtering)
101
219
  for nested_name, nested_class in nested_configs.items():
102
220
  dict_name = f"{nested_name}Dict"
103
221
  fields = []
@@ -118,6 +236,10 @@ def generate_typed_class_code(config_class: Type) -> str:
118
236
  all_assignments = []
119
237
 
120
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
+
121
243
  field_type = field_info.field_type
122
244
 
123
245
  # Handle nested ConfigMeta classes
@@ -147,7 +269,16 @@ def generate_typed_class_code(config_class: Type) -> str:
147
269
  else:
148
270
  params_with_kwargs = [" **kwargs"]
149
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
+
150
279
  class_code = f"""class {class_name}:
280
+ {indented_class_docstring}
281
+
151
282
  def __init__(
152
283
  self,
153
284
  {comma_newline.join(params_with_kwargs)}
@@ -161,12 +292,16 @@ def generate_typed_class_code(config_class: Type) -> str:
161
292
  self._kwargs = {{k: v for k, v in self._kwargs.items() if v is not None}}
162
293
  self._config_class = {config_class.__name__}
163
294
  self._config = self.create_config()
295
+ self._init()
164
296
 
165
297
  def create_config(self) -> {config_class.__name__}:
166
298
  return {config_class.__name__}.from_dict(self._kwargs)
167
299
 
168
300
  def to_dict(self) -> Dict[str, Any]:
169
- return self._config.to_dict()"""
301
+ return self._config.to_dict()
302
+
303
+ def _init(self):
304
+ raise NotImplementedError"""
170
305
 
171
306
  # Combine all code
172
307
  full_code = []
@@ -299,6 +434,10 @@ def create_typed_init_class_dynamic(config_class: Type) -> Type:
299
434
  annotations: Dict[str, Any] = {"return": type(None)}
300
435
 
301
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
+
302
441
  field_type = field_info.field_type
303
442
 
304
443
  # Handle nested ConfigMeta classes