ob-metaflow-extensions 1.1.175rc2__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_config.py +6 -6
- metaflow_extensions/outerbounds/plugins/apps/core/capsule.py +22 -6
- metaflow_extensions/outerbounds/plugins/apps/core/config/__init__.py +1 -0
- metaflow_extensions/outerbounds/plugins/apps/core/config/config_utils.py +110 -50
- 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 +173 -49
- metaflow_extensions/outerbounds/plugins/apps/core/config_schema.yaml +4 -4
- metaflow_extensions/outerbounds/plugins/apps/core/deployer.py +132 -0
- metaflow_extensions/outerbounds/plugins/apps/core/perimeters.py +44 -2
- 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.175rc2.dist-info → ob_metaflow_extensions-1.1.175rc3.dist-info}/METADATA +1 -1
- {ob_metaflow_extensions-1.1.175rc2.dist-info → ob_metaflow_extensions-1.1.175rc3.dist-info}/RECORD +19 -16
- {ob_metaflow_extensions-1.1.175rc2.dist-info → ob_metaflow_extensions-1.1.175rc3.dist-info}/WHEEL +0 -0
- {ob_metaflow_extensions-1.1.175rc2.dist-info → ob_metaflow_extensions-1.1.175rc3.dist-info}/top_level.txt +0 -0
|
@@ -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"))
|
|
@@ -12,7 +12,7 @@ No external dependencies required - uses only Python standard library.
|
|
|
12
12
|
import os
|
|
13
13
|
import json
|
|
14
14
|
from typing import Any, Dict, List, Optional, Union, Type
|
|
15
|
-
|
|
15
|
+
import re
|
|
16
16
|
|
|
17
17
|
from .config_utils import (
|
|
18
18
|
ConfigField,
|
|
@@ -41,9 +41,142 @@ class AuthType:
|
|
|
41
41
|
return [cls.BROWSER, cls.API]
|
|
42
42
|
|
|
43
43
|
|
|
44
|
+
class UnitParser:
|
|
45
|
+
UNIT_FREE_REGEX = r"^\d+$"
|
|
46
|
+
|
|
47
|
+
metrics = {
|
|
48
|
+
"memory": {
|
|
49
|
+
"default_unit": "Mi",
|
|
50
|
+
"requires_unit": True, # if a Unit free value is provided then we will add the default unit to it.
|
|
51
|
+
# Regex to match values with units (e.g., "512Mi", "4Gi", "1024Ki")
|
|
52
|
+
"unit_regex": r"^\d+(\.\d+)?(Ki|Mi|Gi|Ti|Pi|Ei)$",
|
|
53
|
+
},
|
|
54
|
+
"cpu": {
|
|
55
|
+
"default_unit": None,
|
|
56
|
+
"requires_unit": False, # if a Unit free value is provided then we will not add the default unit to it.
|
|
57
|
+
# Accepts values like 400m, 4, 0.4, 1000n, etc.
|
|
58
|
+
# Regex to match values with units (e.g., "400m", "1000n", "2", "0.5")
|
|
59
|
+
"unit_regex": r"^(\d+(\.\d+)?(m|n)?|\d+(\.\d+)?)$",
|
|
60
|
+
},
|
|
61
|
+
"disk": {
|
|
62
|
+
"default_unit": "Mi",
|
|
63
|
+
"requires_unit": True, # if a Unit free value is provided then we will add the default unit to it.
|
|
64
|
+
# Regex to match values with units (e.g., "100Mi", "1Gi", "500Ki")
|
|
65
|
+
"unit_regex": r"^\d+(\.\d+)?(Ki|Mi|Gi|Ti|Pi|Ei)$",
|
|
66
|
+
},
|
|
67
|
+
"gpu": {
|
|
68
|
+
"default_unit": None,
|
|
69
|
+
"requires_unit": False,
|
|
70
|
+
# Regex to match values with units (usually just integer count, e.g., "1", "2")
|
|
71
|
+
"unit_regex": r"^\d+$",
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
def __init__(self, metric_name: str):
|
|
76
|
+
self.metric_name = metric_name
|
|
77
|
+
|
|
78
|
+
def validate(self, value: str):
|
|
79
|
+
if self.metrics[self.metric_name]["requires_unit"]:
|
|
80
|
+
if not re.match(self.metrics[self.metric_name]["unit_regex"], value):
|
|
81
|
+
return False
|
|
82
|
+
return True
|
|
83
|
+
|
|
84
|
+
def process(self, value: str):
|
|
85
|
+
value = str(value)
|
|
86
|
+
if self.metrics[self.metric_name]["requires_unit"]:
|
|
87
|
+
if re.match(self.UNIT_FREE_REGEX, value):
|
|
88
|
+
# This means the value is unit free and we need to add the default unit to it.
|
|
89
|
+
value = "%s%s" % (
|
|
90
|
+
value.strip(),
|
|
91
|
+
self.metrics[self.metric_name]["default_unit"],
|
|
92
|
+
)
|
|
93
|
+
return value
|
|
94
|
+
|
|
95
|
+
if re.match(self.metrics[self.metric_name]["unit_regex"], value):
|
|
96
|
+
return value
|
|
97
|
+
|
|
98
|
+
return value
|
|
99
|
+
|
|
100
|
+
def parse(self, value: str):
|
|
101
|
+
if value is None:
|
|
102
|
+
return None
|
|
103
|
+
return self.process(value)
|
|
104
|
+
|
|
105
|
+
@staticmethod
|
|
106
|
+
def validation_wrapper_fn(
|
|
107
|
+
metric_name: str,
|
|
108
|
+
):
|
|
109
|
+
def validation_fn(value: str):
|
|
110
|
+
if value is None:
|
|
111
|
+
return True
|
|
112
|
+
field_info = ResourceConfig._get_field(ResourceConfig, metric_name) # type: ignore
|
|
113
|
+
parser = UnitParser(metric_name)
|
|
114
|
+
validation = parser.validate(value)
|
|
115
|
+
if not validation:
|
|
116
|
+
raise ConfigValidationFailedException(
|
|
117
|
+
field_name=metric_name,
|
|
118
|
+
field_info=field_info,
|
|
119
|
+
current_value=value,
|
|
120
|
+
message=f"Invalid value for `{metric_name}`. Must be of the format {parser.metrics[metric_name]['unit_regex']}.",
|
|
121
|
+
)
|
|
122
|
+
return validation
|
|
123
|
+
|
|
124
|
+
return validation_fn
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class BasicValidations:
|
|
128
|
+
def __init__(self, config_meta_class, field_name):
|
|
129
|
+
self.config_meta_class = config_meta_class
|
|
130
|
+
self.field_name = field_name
|
|
131
|
+
|
|
132
|
+
def _get_field(self):
|
|
133
|
+
return self.config_meta_class._get_field(self.config_meta_class, self.field_name) # type: ignore
|
|
134
|
+
|
|
135
|
+
def enum_validation(self, enums: List[str], current_value):
|
|
136
|
+
if current_value not in enums:
|
|
137
|
+
raise ConfigValidationFailedException(
|
|
138
|
+
field_name=self.field_name,
|
|
139
|
+
field_info=self._get_field(),
|
|
140
|
+
current_value=current_value,
|
|
141
|
+
message=f"Configuration field {self.field_name} has invalid value {current_value}. Value must be one of: {' '.join(enums)}",
|
|
142
|
+
)
|
|
143
|
+
return True
|
|
144
|
+
|
|
145
|
+
def range_validation(self, min_value, max_value, current_value):
|
|
146
|
+
if current_value < min_value or current_value > max_value:
|
|
147
|
+
raise ConfigValidationFailedException(
|
|
148
|
+
field_name=self.field_name,
|
|
149
|
+
field_info=self._get_field(),
|
|
150
|
+
current_value=current_value,
|
|
151
|
+
message=f"Configuration field {self.field_name} has invalid value {current_value}. Value must be between {min_value} and {max_value}",
|
|
152
|
+
)
|
|
153
|
+
return True
|
|
154
|
+
|
|
155
|
+
def length_validation(self, max_length, current_value):
|
|
156
|
+
if len(current_value) > max_length:
|
|
157
|
+
raise ConfigValidationFailedException(
|
|
158
|
+
field_name=self.field_name,
|
|
159
|
+
field_info=self._get_field(),
|
|
160
|
+
current_value=current_value,
|
|
161
|
+
message=f"Configuration field {self.field_name} has invalid value {current_value}. Value must be less than {max_length}",
|
|
162
|
+
)
|
|
163
|
+
return True
|
|
164
|
+
|
|
165
|
+
def regex_validation(self, regex, current_value):
|
|
166
|
+
if not re.match(regex, current_value):
|
|
167
|
+
raise ConfigValidationFailedException(
|
|
168
|
+
field_name=self.field_name,
|
|
169
|
+
field_info=self._get_field(),
|
|
170
|
+
current_value=current_value,
|
|
171
|
+
message=f"Configuration field {self.field_name} has invalid value {current_value}. Value must match regex {regex}",
|
|
172
|
+
)
|
|
173
|
+
return True
|
|
174
|
+
|
|
175
|
+
|
|
44
176
|
class ResourceConfig(metaclass=ConfigMeta):
|
|
45
177
|
"""Resource configuration for the app."""
|
|
46
178
|
|
|
179
|
+
# TODO: Add Unit Validation/Parsing Support for the Fields.
|
|
47
180
|
cpu = ConfigField(
|
|
48
181
|
default="1",
|
|
49
182
|
cli_meta=CLIOption(
|
|
@@ -53,6 +186,8 @@ class ResourceConfig(metaclass=ConfigMeta):
|
|
|
53
186
|
),
|
|
54
187
|
field_type=str,
|
|
55
188
|
example="500m",
|
|
189
|
+
validation_fn=UnitParser.validation_wrapper_fn("cpu"),
|
|
190
|
+
parsing_fn=UnitParser("cpu").parse,
|
|
56
191
|
)
|
|
57
192
|
memory = ConfigField(
|
|
58
193
|
default="4Gi",
|
|
@@ -63,6 +198,8 @@ class ResourceConfig(metaclass=ConfigMeta):
|
|
|
63
198
|
),
|
|
64
199
|
field_type=str,
|
|
65
200
|
example="512Mi",
|
|
201
|
+
validation_fn=UnitParser.validation_wrapper_fn("memory"),
|
|
202
|
+
parsing_fn=UnitParser("memory").parse,
|
|
66
203
|
)
|
|
67
204
|
gpu = ConfigField(
|
|
68
205
|
cli_meta=CLIOption(
|
|
@@ -72,6 +209,8 @@ class ResourceConfig(metaclass=ConfigMeta):
|
|
|
72
209
|
),
|
|
73
210
|
field_type=str,
|
|
74
211
|
example="1",
|
|
212
|
+
validation_fn=UnitParser.validation_wrapper_fn("gpu"),
|
|
213
|
+
parsing_fn=UnitParser("gpu").parse,
|
|
75
214
|
)
|
|
76
215
|
disk = ConfigField(
|
|
77
216
|
default="20Gi",
|
|
@@ -82,6 +221,8 @@ class ResourceConfig(metaclass=ConfigMeta):
|
|
|
82
221
|
),
|
|
83
222
|
field_type=str,
|
|
84
223
|
example="1Gi",
|
|
224
|
+
validation_fn=UnitParser.validation_wrapper_fn("disk"),
|
|
225
|
+
parsing_fn=UnitParser("disk").parse,
|
|
85
226
|
)
|
|
86
227
|
|
|
87
228
|
|
|
@@ -156,14 +297,11 @@ class AuthConfig(metaclass=ConfigMeta):
|
|
|
156
297
|
|
|
157
298
|
@staticmethod
|
|
158
299
|
def validate(auth_config: "AuthConfig"):
|
|
159
|
-
if auth_config.type is
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
message=f"Invalid auth type: {auth_config.type}. Must be one of {AuthType.choices()}",
|
|
165
|
-
)
|
|
166
|
-
return True
|
|
300
|
+
if auth_config.type is None:
|
|
301
|
+
return True
|
|
302
|
+
return BasicValidations(AuthConfig, "type").enum_validation(
|
|
303
|
+
AuthType.choices(), auth_config.type
|
|
304
|
+
)
|
|
167
305
|
|
|
168
306
|
|
|
169
307
|
class ReplicaConfig(metaclass=ConfigMeta):
|
|
@@ -226,17 +364,17 @@ class ReplicaConfig(metaclass=ConfigMeta):
|
|
|
226
364
|
def _greater_than_equals_zero(x):
|
|
227
365
|
return x is not None and x >= 0
|
|
228
366
|
|
|
229
|
-
if both_min_max_set and replica_config.min > replica_config.max:
|
|
367
|
+
if both_min_max_set and replica_config.min > replica_config.max: # type: ignore
|
|
230
368
|
raise ConfigValidationFailedException(
|
|
231
369
|
field_name="min",
|
|
232
|
-
field_info=replica_config._get_field("min"),
|
|
370
|
+
field_info=replica_config._get_field("min"), # type: ignore
|
|
233
371
|
current_value=replica_config.min,
|
|
234
372
|
message="Min replicas cannot be greater than max replicas",
|
|
235
373
|
)
|
|
236
374
|
if fixed_set and any_min_max_set:
|
|
237
375
|
raise ConfigValidationFailedException(
|
|
238
376
|
field_name="fixed",
|
|
239
|
-
field_info=replica_config._get_field("fixed"),
|
|
377
|
+
field_info=replica_config._get_field("fixed"), # type: ignore
|
|
240
378
|
current_value=replica_config.fixed,
|
|
241
379
|
message="Fixed replicas cannot be set when min or max replicas are set",
|
|
242
380
|
)
|
|
@@ -244,15 +382,15 @@ class ReplicaConfig(metaclass=ConfigMeta):
|
|
|
244
382
|
if max_is_set and not min_is_set:
|
|
245
383
|
raise ConfigValidationFailedException(
|
|
246
384
|
field_name="min",
|
|
247
|
-
field_info=replica_config._get_field("min"),
|
|
385
|
+
field_info=replica_config._get_field("min"), # type: ignore
|
|
248
386
|
current_value=replica_config.min,
|
|
249
387
|
message="If max replicas is set then min replicas must be set too.",
|
|
250
388
|
)
|
|
251
389
|
|
|
252
|
-
if fixed_set and replica_config.fixed < 0:
|
|
390
|
+
if fixed_set and replica_config.fixed < 0: # type: ignore
|
|
253
391
|
raise ConfigValidationFailedException(
|
|
254
392
|
field_name="fixed",
|
|
255
|
-
field_info=replica_config._get_field("fixed"),
|
|
393
|
+
field_info=replica_config._get_field("fixed"), # type: ignore
|
|
256
394
|
current_value=replica_config.fixed,
|
|
257
395
|
message="Fixed replicas cannot be less than 0",
|
|
258
396
|
)
|
|
@@ -260,7 +398,7 @@ class ReplicaConfig(metaclass=ConfigMeta):
|
|
|
260
398
|
if min_is_set and not _greater_than_equals_zero(replica_config.min):
|
|
261
399
|
raise ConfigValidationFailedException(
|
|
262
400
|
field_name="min",
|
|
263
|
-
field_info=replica_config._get_field("min"),
|
|
401
|
+
field_info=replica_config._get_field("min"), # type: ignore
|
|
264
402
|
current_value=replica_config.min,
|
|
265
403
|
message="Min replicas cannot be less than 0",
|
|
266
404
|
)
|
|
@@ -268,7 +406,7 @@ class ReplicaConfig(metaclass=ConfigMeta):
|
|
|
268
406
|
if max_is_set and not _greater_than_equals_zero(replica_config.max):
|
|
269
407
|
raise ConfigValidationFailedException(
|
|
270
408
|
field_name="max",
|
|
271
|
-
field_info=replica_config._get_field("max"),
|
|
409
|
+
field_info=replica_config._get_field("max"), # type: ignore
|
|
272
410
|
current_value=replica_config.max,
|
|
273
411
|
message="Max replicas cannot be less than 0",
|
|
274
412
|
)
|
|
@@ -346,7 +484,7 @@ class DependencyConfig(metaclass=ConfigMeta):
|
|
|
346
484
|
):
|
|
347
485
|
raise ConfigValidationFailedException(
|
|
348
486
|
field_name="from_requirements_file",
|
|
349
|
-
field_info=dependency_config._get_field("from_requirements_file"),
|
|
487
|
+
field_info=dependency_config._get_field("from_requirements_file"), # type: ignore
|
|
350
488
|
current_value=dependency_config.from_requirements_file,
|
|
351
489
|
message="Cannot set from_requirements_file and from_pyproject_toml at the same time",
|
|
352
490
|
)
|
|
@@ -358,7 +496,7 @@ class DependencyConfig(metaclass=ConfigMeta):
|
|
|
358
496
|
):
|
|
359
497
|
raise ConfigValidationFailedException(
|
|
360
498
|
field_name="pypi" if dependency_config.pypi else "conda",
|
|
361
|
-
field_info=dependency_config._get_field(
|
|
499
|
+
field_info=dependency_config._get_field( # type: ignore
|
|
362
500
|
"pypi" if dependency_config.pypi else "conda"
|
|
363
501
|
),
|
|
364
502
|
current_value=dependency_config.pypi or dependency_config.conda,
|
|
@@ -395,27 +533,17 @@ class BasicAppValidations:
|
|
|
395
533
|
def name(name):
|
|
396
534
|
if name is None:
|
|
397
535
|
return True
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
message="Name cannot be longer than 128 characters",
|
|
404
|
-
)
|
|
405
|
-
return True
|
|
536
|
+
regex = r"^[a-z0-9-]+$" # Only allow lowercase letters, numbers, and hyphens
|
|
537
|
+
validator = BasicValidations(CoreConfig, "name")
|
|
538
|
+
return validator.length_validation(20, name) and validator.regex_validation(
|
|
539
|
+
regex, name
|
|
540
|
+
)
|
|
406
541
|
|
|
407
542
|
@staticmethod
|
|
408
543
|
def port(port):
|
|
409
544
|
if port is None:
|
|
410
545
|
return True
|
|
411
|
-
|
|
412
|
-
raise ConfigValidationFailedException(
|
|
413
|
-
field_name="port",
|
|
414
|
-
field_info=CoreConfig._get_field(CoreConfig, "port"),
|
|
415
|
-
current_value=port,
|
|
416
|
-
message="Port must be between 1 and 65535",
|
|
417
|
-
)
|
|
418
|
-
return True
|
|
546
|
+
return BasicValidations(CoreConfig, "port").range_validation(1, 65535, port)
|
|
419
547
|
|
|
420
548
|
@staticmethod
|
|
421
549
|
def tags(tags):
|
|
@@ -424,7 +552,7 @@ class BasicAppValidations:
|
|
|
424
552
|
if not all(isinstance(tag, dict) and len(tag) == 1 for tag in tags):
|
|
425
553
|
raise ConfigValidationFailedException(
|
|
426
554
|
field_name="tags",
|
|
427
|
-
field_info=CoreConfig._get_field(CoreConfig, "tags"),
|
|
555
|
+
field_info=CoreConfig._get_field(CoreConfig, "tags"), # type: ignore
|
|
428
556
|
current_value=tags,
|
|
429
557
|
message="Tags must be a list of dictionaries with one key. Currently they are set to %s "
|
|
430
558
|
% (str(tags)),
|
|
@@ -439,9 +567,10 @@ class BasicAppValidations:
|
|
|
439
567
|
if not isinstance(secrets, list):
|
|
440
568
|
raise ConfigValidationFailedException(
|
|
441
569
|
field_name="secrets",
|
|
442
|
-
field_info=CoreConfig._get_field(CoreConfig, "secrets"),
|
|
570
|
+
field_info=CoreConfig._get_field(CoreConfig, "secrets"), # type: ignore
|
|
443
571
|
current_value=secrets,
|
|
444
|
-
message="Secrets must be a list of strings"
|
|
572
|
+
message="Secrets must be a list of strings. Currently they are set to %s "
|
|
573
|
+
% (str(secrets)),
|
|
445
574
|
)
|
|
446
575
|
from ..validations import secrets_validator
|
|
447
576
|
|
|
@@ -450,7 +579,7 @@ class BasicAppValidations:
|
|
|
450
579
|
except Exception as e:
|
|
451
580
|
raise ConfigValidationFailedException(
|
|
452
581
|
field_name="secrets",
|
|
453
|
-
field_info=CoreConfig._get_field(CoreConfig, "secrets"),
|
|
582
|
+
field_info=CoreConfig._get_field(CoreConfig, "secrets"), # type: ignore
|
|
454
583
|
current_value=secrets,
|
|
455
584
|
message=f"Secrets validation failed, {e}",
|
|
456
585
|
)
|
|
@@ -460,14 +589,9 @@ class BasicAppValidations:
|
|
|
460
589
|
def persistence(persistence):
|
|
461
590
|
if persistence is None:
|
|
462
591
|
return True
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
field_info=CoreConfig._get_field(CoreConfig, "persistence"),
|
|
467
|
-
current_value=persistence,
|
|
468
|
-
message=f"Persistence must be one of: {['none', 'postgres']}",
|
|
469
|
-
)
|
|
470
|
-
return True
|
|
592
|
+
return BasicValidations(CoreConfig, "persistence").enum_validation(
|
|
593
|
+
["none", "postgres"], persistence
|
|
594
|
+
)
|
|
471
595
|
|
|
472
596
|
|
|
473
597
|
class CoreConfig(metaclass=ConfigMeta):
|
|
@@ -787,7 +911,7 @@ How to read this schema:
|
|
|
787
911
|
merged_config = CoreConfig()
|
|
788
912
|
|
|
789
913
|
# Process each field according to its behavior
|
|
790
|
-
for field_name, field_info in CoreConfig._fields.items():
|
|
914
|
+
for field_name, field_info in CoreConfig._fields.items(): # type: ignore
|
|
791
915
|
base_value = getattr(base_config, field_name, None)
|
|
792
916
|
override_value = getattr(override_config, field_name, None)
|
|
793
917
|
|
|
@@ -852,7 +976,7 @@ How to read this schema:
|
|
|
852
976
|
for v in value:
|
|
853
977
|
_env_dict.update(v)
|
|
854
978
|
return _env_dict
|
|
855
|
-
if type(value) == tuple:
|
|
979
|
+
if type(value) == tuple or type(value) == list:
|
|
856
980
|
obj = list(x for x in source_data[key])
|
|
857
981
|
if len(obj) == 0:
|
|
858
982
|
return None # Dont return Empty Lists so that we can set Nones
|