outerbounds 0.3.183rc1__py3-none-any.whl → 0.3.185__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.
- outerbounds/__init__.py +1 -3
- outerbounds/command_groups/apps_cli.py +6 -2
- {outerbounds-0.3.183rc1.dist-info → outerbounds-0.3.185.dist-info}/METADATA +3 -3
- {outerbounds-0.3.183rc1.dist-info → outerbounds-0.3.185.dist-info}/RECORD +6 -29
- outerbounds-0.3.185.dist-info/entry_points.txt +3 -0
- outerbounds/_vendor/spinner/__init__.py +0 -4
- outerbounds/_vendor/spinner/spinners.py +0 -478
- outerbounds/_vendor/spinner.LICENSE +0 -21
- outerbounds/apps/__init__.py +0 -0
- outerbounds/apps/_state_machine.py +0 -472
- outerbounds/apps/app_cli.py +0 -1514
- outerbounds/apps/app_config.py +0 -296
- outerbounds/apps/artifacts.py +0 -0
- outerbounds/apps/capsule.py +0 -839
- outerbounds/apps/cli_to_config.py +0 -99
- outerbounds/apps/click_importer.py +0 -24
- outerbounds/apps/code_package/__init__.py +0 -3
- outerbounds/apps/code_package/code_packager.py +0 -610
- outerbounds/apps/code_package/examples.py +0 -125
- outerbounds/apps/config_schema.yaml +0 -269
- outerbounds/apps/config_schema_autogen.json +0 -336
- outerbounds/apps/dependencies.py +0 -115
- outerbounds/apps/deployer.py +0 -0
- outerbounds/apps/experimental/__init__.py +0 -110
- outerbounds/apps/perimeters.py +0 -45
- outerbounds/apps/secrets.py +0 -164
- outerbounds/apps/utils.py +0 -234
- outerbounds/apps/validations.py +0 -22
- outerbounds-0.3.183rc1.dist-info/entry_points.txt +0 -3
- {outerbounds-0.3.183rc1.dist-info → outerbounds-0.3.185.dist-info}/WHEEL +0 -0
outerbounds/apps/app_config.py
DELETED
@@ -1,296 +0,0 @@
|
|
1
|
-
import json
|
2
|
-
import os
|
3
|
-
|
4
|
-
# TODO: remove vendor'd dependency where.
|
5
|
-
from outerbounds._vendor import yaml
|
6
|
-
from typing import Dict, Any
|
7
|
-
from .cli_to_config import build_config_from_options
|
8
|
-
|
9
|
-
CODE_PACKAGE_PREFIX = "mf.obp-apps"
|
10
|
-
|
11
|
-
CAPSULE_DEBUG = os.environ.get("OUTERBOUNDS_CAPSULE_DEBUG", False)
|
12
|
-
|
13
|
-
|
14
|
-
class classproperty(property):
|
15
|
-
def __get__(self, owner_self, owner_cls):
|
16
|
-
return self.fget(owner_cls)
|
17
|
-
|
18
|
-
|
19
|
-
class AppConfigError(Exception):
|
20
|
-
"""Exception raised when app configuration is invalid."""
|
21
|
-
|
22
|
-
pass
|
23
|
-
|
24
|
-
|
25
|
-
class AuthType:
|
26
|
-
BROWSER = "Browser"
|
27
|
-
API = "API"
|
28
|
-
|
29
|
-
@classmethod
|
30
|
-
def enums(cls):
|
31
|
-
return [cls.BROWSER, cls.API]
|
32
|
-
|
33
|
-
@classproperty
|
34
|
-
def default(cls):
|
35
|
-
return cls.BROWSER
|
36
|
-
|
37
|
-
|
38
|
-
class AppConfig:
|
39
|
-
"""Class representing an Outerbounds App configuration."""
|
40
|
-
|
41
|
-
def __init__(self, config_dict: Dict[str, Any]):
|
42
|
-
"""Initialize configuration from a dictionary."""
|
43
|
-
self.config = config_dict or {}
|
44
|
-
self.schema = self._load_schema()
|
45
|
-
self._final_state: Dict[str, Any] = {}
|
46
|
-
|
47
|
-
def set_state(self, key, value):
|
48
|
-
self._final_state[key] = value
|
49
|
-
return self
|
50
|
-
|
51
|
-
def get_state(self, key, default=None):
|
52
|
-
return self._final_state.get(key, self.config.get(key, default))
|
53
|
-
|
54
|
-
def dump_state(self):
|
55
|
-
x = {k: v for k, v in self.config.items()}
|
56
|
-
for k, v in self._final_state.items():
|
57
|
-
x[k] = v
|
58
|
-
return x
|
59
|
-
|
60
|
-
@staticmethod
|
61
|
-
def _load_schema():
|
62
|
-
"""Load the configuration schema from the YAML file."""
|
63
|
-
# TODO: Make it easier.
|
64
|
-
schema_path = os.path.join(os.path.dirname(__file__), "config_schema.yaml")
|
65
|
-
with open(schema_path, "r") as f:
|
66
|
-
return yaml.safe_load(f)
|
67
|
-
|
68
|
-
def get(self, key: str, default: Any = None) -> Any:
|
69
|
-
"""Get a configuration value by key."""
|
70
|
-
return self.config.get(key, default)
|
71
|
-
|
72
|
-
def validate(self) -> None:
|
73
|
-
"""Validate the configuration against the schema."""
|
74
|
-
self._validate_required_fields()
|
75
|
-
self._validate_field_types()
|
76
|
-
self._validate_field_constraints()
|
77
|
-
|
78
|
-
def set_deploy_defaults(self, packaging_directory: str) -> None:
|
79
|
-
"""Set default values for fields that are not provided."""
|
80
|
-
if not self.config.get("auth"):
|
81
|
-
self.config["auth"] = {}
|
82
|
-
if not self.config["auth"].get("public"):
|
83
|
-
self.config["auth"]["public"] = True
|
84
|
-
if not self.config["auth"].get("type"):
|
85
|
-
self.config["auth"]["type"] = AuthType.BROWSER
|
86
|
-
|
87
|
-
if not self.config.get("health_check"):
|
88
|
-
self.config["health_check"] = {}
|
89
|
-
if not self.config["health_check"].get("enabled"):
|
90
|
-
self.config["health_check"]["enabled"] = False
|
91
|
-
|
92
|
-
if not self.config.get("resources"):
|
93
|
-
self.config["resources"] = {}
|
94
|
-
if not self.config["resources"].get("cpu"):
|
95
|
-
self.config["resources"]["cpu"] = 1
|
96
|
-
if not self.config["resources"].get("memory"):
|
97
|
-
self.config["resources"]["memory"] = "4096Mi"
|
98
|
-
if not self.config["resources"].get("disk"):
|
99
|
-
self.config["resources"]["disk"] = "20Gi"
|
100
|
-
|
101
|
-
if not self.config.get("replicas", None):
|
102
|
-
self.config["replicas"] = {
|
103
|
-
"min": 1,
|
104
|
-
"max": 1,
|
105
|
-
}
|
106
|
-
else:
|
107
|
-
max_is_set = self.config["replicas"].get("max", None) is not None
|
108
|
-
min_is_set = self.config["replicas"].get("min", None) is not None
|
109
|
-
if max_is_set and not min_is_set:
|
110
|
-
# If users want to set 0 replicas for min,
|
111
|
-
# then they need explicitly specify min to 0
|
112
|
-
self.config["replicas"]["min"] = 1 # Atleast set 1 replica
|
113
|
-
if min_is_set and not max_is_set:
|
114
|
-
# In the situations where we dont have min/max replicas, we can
|
115
|
-
# set max to min.
|
116
|
-
self.config["replicas"]["max"] = self.config["replicas"].get("min")
|
117
|
-
|
118
|
-
def _validate_required_fields(self) -> None:
|
119
|
-
"""Validate that all required fields are present."""
|
120
|
-
required_fields = self.schema.get("required", [])
|
121
|
-
for field in required_fields:
|
122
|
-
if field not in self.config:
|
123
|
-
raise AppConfigError(
|
124
|
-
f"Required field '{field}' is missing from the configuration."
|
125
|
-
)
|
126
|
-
|
127
|
-
def _validate_field_types(self) -> None:
|
128
|
-
"""Validate that fields have correct types."""
|
129
|
-
properties = self.schema.get("properties", {})
|
130
|
-
|
131
|
-
for field, value in self.config.items():
|
132
|
-
if field not in properties:
|
133
|
-
raise AppConfigError(f"Unknown field '{field}' in configuration.")
|
134
|
-
|
135
|
-
field_schema = properties[field]
|
136
|
-
field_type = field_schema.get("type")
|
137
|
-
|
138
|
-
if field_type == "string" and not isinstance(value, str):
|
139
|
-
raise AppConfigError(f"Field '{field}' must be a string.")
|
140
|
-
|
141
|
-
elif field_type == "integer" and not isinstance(value, int):
|
142
|
-
raise AppConfigError(f"Field '{field}' must be an integer.")
|
143
|
-
|
144
|
-
elif field_type == "boolean" and not isinstance(value, bool):
|
145
|
-
raise AppConfigError(f"Field '{field}' must be a boolean.")
|
146
|
-
|
147
|
-
elif field_type == "array" and not isinstance(value, list):
|
148
|
-
raise AppConfigError(f"Field '{field}' must be an array.")
|
149
|
-
|
150
|
-
elif field_type == "object" and not isinstance(value, dict):
|
151
|
-
raise AppConfigError(f"Field '{field}' must be an object.")
|
152
|
-
|
153
|
-
def _validate_field_constraints(self) -> None:
|
154
|
-
"""Validate field-specific constraints."""
|
155
|
-
properties = self.schema.get("properties", {})
|
156
|
-
|
157
|
-
# Validate name
|
158
|
-
if "name" in self.config:
|
159
|
-
name = self.config["name"]
|
160
|
-
max_length = properties["name"].get("maxLength", 20)
|
161
|
-
if len(name) > max_length:
|
162
|
-
raise AppConfigError(
|
163
|
-
f"App name '{name}' exceeds maximum length of {max_length} characters."
|
164
|
-
)
|
165
|
-
|
166
|
-
# Validate port
|
167
|
-
if "port" in self.config:
|
168
|
-
port = self.config["port"]
|
169
|
-
min_port = properties["port"].get("minimum", 1)
|
170
|
-
max_port = properties["port"].get("maximum", 65535)
|
171
|
-
if port < min_port or port > max_port:
|
172
|
-
raise AppConfigError(
|
173
|
-
f"Port number {port} is outside valid range ({min_port}-{max_port})."
|
174
|
-
)
|
175
|
-
|
176
|
-
# Validate dependencies (only one type allowed)
|
177
|
-
if "dependencies" in self.config:
|
178
|
-
deps = self.config["dependencies"]
|
179
|
-
if not isinstance(deps, dict):
|
180
|
-
raise AppConfigError("Dependencies must be an object.")
|
181
|
-
|
182
|
-
valid_dep_types = [
|
183
|
-
"from_requirements_file",
|
184
|
-
"from_pyproject_toml",
|
185
|
-
]
|
186
|
-
|
187
|
-
found_types = [dep_type for dep_type in valid_dep_types if dep_type in deps]
|
188
|
-
|
189
|
-
if len(found_types) > 1:
|
190
|
-
raise AppConfigError(
|
191
|
-
f"You can only specify one mode of specifying dependencies. You have specified : {found_types} . Please only set one."
|
192
|
-
)
|
193
|
-
|
194
|
-
# Validate that each tag has exactly one key
|
195
|
-
if "tags" in self.config:
|
196
|
-
tags = self.config["tags"]
|
197
|
-
for tag in tags:
|
198
|
-
if not isinstance(tag, dict):
|
199
|
-
raise AppConfigError(
|
200
|
-
"Each tag must be a dictionary. %s is of type %s"
|
201
|
-
% (str(tag), type(tag))
|
202
|
-
)
|
203
|
-
if len(tag.keys()) != 1:
|
204
|
-
raise AppConfigError(
|
205
|
-
"Each tag must have exactly one key-value pair. Tag %s has %d key-value pairs."
|
206
|
-
% (str(tag), len(tag.keys()))
|
207
|
-
)
|
208
|
-
if "replicas" in self.config:
|
209
|
-
replicas = self.config["replicas"]
|
210
|
-
if not isinstance(replicas, dict):
|
211
|
-
raise AppConfigError("Replicas must be an object.")
|
212
|
-
max_is_set = self.config["replicas"].get("max", None) is not None
|
213
|
-
if max_is_set:
|
214
|
-
if replicas.get("max") == 0:
|
215
|
-
raise AppConfigError("Max replicas must be greater than 0.")
|
216
|
-
|
217
|
-
if replicas.get("min", 1) > replicas.get("max"):
|
218
|
-
raise AppConfigError(
|
219
|
-
"Min replicas must be less than max replicas. %s > %s"
|
220
|
-
% (replicas.get("min", 1), replicas.get("max", 1))
|
221
|
-
)
|
222
|
-
|
223
|
-
def to_dict(self) -> Dict[str, Any]:
|
224
|
-
"""Return the configuration as a dictionary."""
|
225
|
-
return self.config
|
226
|
-
|
227
|
-
def to_yaml(self) -> str:
|
228
|
-
"""Return the configuration as a YAML string."""
|
229
|
-
return yaml.dump(self.config, default_flow_style=False)
|
230
|
-
|
231
|
-
def to_json(self) -> str:
|
232
|
-
"""Return the configuration as a JSON string."""
|
233
|
-
return json.dumps(self.config, indent=2)
|
234
|
-
|
235
|
-
@classmethod
|
236
|
-
def from_file(cls, file_path: str) -> "AppConfig":
|
237
|
-
"""Create a configuration from a file."""
|
238
|
-
if not os.path.exists(file_path):
|
239
|
-
raise AppConfigError(f"Configuration file '{file_path}' does not exist.")
|
240
|
-
|
241
|
-
with open(file_path, "r") as f:
|
242
|
-
try:
|
243
|
-
config_dict = yaml.safe_load(f)
|
244
|
-
except Exception as e:
|
245
|
-
raise AppConfigError(f"Failed to parse configuration file: {e}")
|
246
|
-
|
247
|
-
return cls(config_dict)
|
248
|
-
|
249
|
-
def update_from_cli_options(self, options):
|
250
|
-
"""
|
251
|
-
Update configuration from CLI options using the same logic as build_config_from_options.
|
252
|
-
This ensures consistent handling of CLI options whether they come from a config file
|
253
|
-
or direct CLI input.
|
254
|
-
"""
|
255
|
-
cli_config = build_config_from_options(options)
|
256
|
-
|
257
|
-
# Process each field using allow_union property
|
258
|
-
for key, value in cli_config.items():
|
259
|
-
if key in self.schema.get("properties", {}):
|
260
|
-
self._update_field(key, value)
|
261
|
-
|
262
|
-
return self
|
263
|
-
|
264
|
-
def _update_field(self, field_name, new_value):
|
265
|
-
"""Update a field based on its allow_union property."""
|
266
|
-
properties = self.schema.get("properties", {})
|
267
|
-
|
268
|
-
# Skip if field doesn't exist in schema
|
269
|
-
if field_name not in properties:
|
270
|
-
return
|
271
|
-
|
272
|
-
field_schema = properties[field_name]
|
273
|
-
allow_union = field_schema.get("allow_union", False)
|
274
|
-
|
275
|
-
# If field doesn't exist in config, just set it
|
276
|
-
if field_name not in self.config:
|
277
|
-
self.config[field_name] = new_value
|
278
|
-
return
|
279
|
-
|
280
|
-
# If allow_union is True, merge values based on type
|
281
|
-
if allow_union:
|
282
|
-
current_value = self.config[field_name]
|
283
|
-
|
284
|
-
if isinstance(current_value, list) and isinstance(new_value, list):
|
285
|
-
# For lists, append new items
|
286
|
-
self.config[field_name].extend(new_value)
|
287
|
-
elif isinstance(current_value, dict) and isinstance(new_value, dict):
|
288
|
-
# For dicts, update with new values
|
289
|
-
self.config[field_name].update(new_value)
|
290
|
-
else:
|
291
|
-
# For other types, replace with new value
|
292
|
-
self.config[field_name] = new_value
|
293
|
-
else:
|
294
|
-
raise AppConfigError(
|
295
|
-
f"Field '{field_name}' does not allow union. Current value: {self.config[field_name]}, new value: {new_value}"
|
296
|
-
)
|
outerbounds/apps/artifacts.py
DELETED
File without changes
|