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.

Files changed (19) hide show
  1. metaflow_extensions/outerbounds/plugins/__init__.py +1 -1
  2. metaflow_extensions/outerbounds/plugins/apps/app_deploy_decorator.py +112 -0
  3. metaflow_extensions/outerbounds/plugins/apps/core/__init__.py +9 -0
  4. metaflow_extensions/outerbounds/plugins/apps/core/app_config.py +6 -6
  5. metaflow_extensions/outerbounds/plugins/apps/core/capsule.py +22 -6
  6. metaflow_extensions/outerbounds/plugins/apps/core/config/__init__.py +1 -0
  7. metaflow_extensions/outerbounds/plugins/apps/core/config/config_utils.py +110 -50
  8. metaflow_extensions/outerbounds/plugins/apps/core/config/typed_configs.py +104 -0
  9. metaflow_extensions/outerbounds/plugins/apps/core/config/typed_init_generator.py +317 -0
  10. metaflow_extensions/outerbounds/plugins/apps/core/config/unified_config.py +173 -49
  11. metaflow_extensions/outerbounds/plugins/apps/core/config_schema.yaml +4 -4
  12. metaflow_extensions/outerbounds/plugins/apps/core/deployer.py +132 -0
  13. metaflow_extensions/outerbounds/plugins/apps/core/perimeters.py +44 -2
  14. metaflow_extensions/outerbounds/toplevel/global_aliases_for_metaflow_package.py +1 -0
  15. metaflow_extensions/outerbounds/toplevel/ob_internal.py +1 -0
  16. {ob_metaflow_extensions-1.1.175rc2.dist-info → ob_metaflow_extensions-1.1.175rc3.dist-info}/METADATA +1 -1
  17. {ob_metaflow_extensions-1.1.175rc2.dist-info → ob_metaflow_extensions-1.1.175rc3.dist-info}/RECORD +19 -16
  18. {ob_metaflow_extensions-1.1.175rc2.dist-info → ob_metaflow_extensions-1.1.175rc3.dist-info}/WHEEL +0 -0
  19. {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 not None and auth_config.type not in AuthType.choices():
160
- raise ConfigValidationFailedException(
161
- field_name="type",
162
- field_info=auth_config._get_field("type"),
163
- current_value=auth_config.type,
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
- if len(name) > 128:
399
- raise ConfigValidationFailedException(
400
- field_name="name",
401
- field_info=CoreConfig._get_field(CoreConfig, "name"),
402
- current_value=name,
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
- if port < 1 or port > 65535:
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
- if persistence not in ["none", "postgres"]:
464
- raise ConfigValidationFailedException(
465
- field_name="persistence",
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