outerbounds 0.3.173rc0__py3-none-any.whl → 0.3.175rc0__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/apps/app_cli.py +20 -16
- outerbounds/apps/app_config.py +64 -79
- outerbounds/apps/capsule.py +23 -32
- outerbounds/apps/cli_to_config.py +91 -0
- outerbounds/apps/config_schema.yaml +60 -4
- outerbounds/apps/experimental/__init__.py +103 -0
- {outerbounds-0.3.173rc0.dist-info → outerbounds-0.3.175rc0.dist-info}/METADATA +3 -3
- {outerbounds-0.3.173rc0.dist-info → outerbounds-0.3.175rc0.dist-info}/RECORD +10 -8
- {outerbounds-0.3.173rc0.dist-info → outerbounds-0.3.175rc0.dist-info}/WHEEL +0 -0
- {outerbounds-0.3.173rc0.dist-info → outerbounds-0.3.175rc0.dist-info}/entry_points.txt +0 -0
outerbounds/apps/app_cli.py
CHANGED
@@ -10,12 +10,14 @@ from .app_config import (
|
|
10
10
|
AppConfigError,
|
11
11
|
CODE_PACKAGE_PREFIX,
|
12
12
|
CAPSULE_DEBUG,
|
13
|
-
|
13
|
+
AuthType,
|
14
14
|
)
|
15
|
+
from .cli_to_config import build_config_from_options
|
15
16
|
from .utils import (
|
16
17
|
CommaSeparatedListType,
|
17
18
|
KVPairType,
|
18
19
|
)
|
20
|
+
from . import experimental
|
19
21
|
from .validations import deploy_validations
|
20
22
|
from .code_package import CodePackager
|
21
23
|
from .capsule import Capsule
|
@@ -134,8 +136,8 @@ def common_deploy_options(func):
|
|
134
136
|
"--tag",
|
135
137
|
"tags",
|
136
138
|
multiple=True,
|
137
|
-
type=
|
138
|
-
help="The tags of the app to deploy.",
|
139
|
+
type=KVPairType,
|
140
|
+
help="The tags of the app to deploy. Format KEY=VALUE. Example --tag foo=bar --tag x=y",
|
139
141
|
default=None,
|
140
142
|
)
|
141
143
|
@click.option(
|
@@ -200,7 +202,7 @@ def common_deploy_options(func):
|
|
200
202
|
)
|
201
203
|
@click.option(
|
202
204
|
"--auth-type",
|
203
|
-
type=click.Choice(
|
205
|
+
type=click.Choice(AuthType.enums()),
|
204
206
|
help="The type of authentication to use for the app.",
|
205
207
|
default=None,
|
206
208
|
)
|
@@ -217,6 +219,18 @@ def common_deploy_options(func):
|
|
217
219
|
help="Do not any dependencies. Directly used the image provided",
|
218
220
|
default=False,
|
219
221
|
)
|
222
|
+
@click.option(
|
223
|
+
"--min-replicas",
|
224
|
+
type=int,
|
225
|
+
help="Minimum number of replicas to deploy",
|
226
|
+
default=None,
|
227
|
+
)
|
228
|
+
@click.option(
|
229
|
+
"--max-replicas",
|
230
|
+
type=int,
|
231
|
+
help="Maximum number of replicas to deploy",
|
232
|
+
default=None,
|
233
|
+
)
|
220
234
|
@wraps(func)
|
221
235
|
def wrapper(*args, **kwargs):
|
222
236
|
return func(*args, **kwargs)
|
@@ -261,18 +275,6 @@ def common_run_options(func):
|
|
261
275
|
help="The suffixes of the source code to deploy with the App.",
|
262
276
|
default=None,
|
263
277
|
)
|
264
|
-
@click.option(
|
265
|
-
"--dep-from-task",
|
266
|
-
type=str,
|
267
|
-
help="The pathspec of the Task from which to resolve dependencies",
|
268
|
-
default=None,
|
269
|
-
)
|
270
|
-
@click.option(
|
271
|
-
"--dep-from-run",
|
272
|
-
type=str,
|
273
|
-
help="The pathspec of the Run from which to resolve dependencies",
|
274
|
-
default=None,
|
275
|
-
)
|
276
278
|
@click.option(
|
277
279
|
"--dep-from-requirements",
|
278
280
|
type=str,
|
@@ -333,6 +335,7 @@ def _package_necessary_things(app_config: AppConfig, logger):
|
|
333
335
|
@app.command(help="Deploy an app to the Outerbounds Platform.")
|
334
336
|
@common_deploy_options
|
335
337
|
@common_run_options
|
338
|
+
@experimental.wrapping_cli_options
|
336
339
|
@click.pass_context
|
337
340
|
@click.argument("command", nargs=-1, type=click.UNPROCESSED, required=False)
|
338
341
|
def deploy(ctx, command, **options):
|
@@ -451,6 +454,7 @@ def deploy(ctx, command, **options):
|
|
451
454
|
|
452
455
|
# 2. Convert to the IR that the backend accepts
|
453
456
|
capsule = Capsule(app_config, ctx.obj.api_url, debug_dir=cache_dir)
|
457
|
+
|
454
458
|
_pre_create_debug(app_config, capsule, cache_dir)
|
455
459
|
# 3. Throw the job into the platform and report deployment status
|
456
460
|
logger(
|
outerbounds/apps/app_config.py
CHANGED
@@ -2,90 +2,16 @@ import json
|
|
2
2
|
import os
|
3
3
|
from outerbounds._vendor import yaml
|
4
4
|
from typing import Dict, Any
|
5
|
+
from .cli_to_config import build_config_from_options
|
5
6
|
|
6
7
|
CODE_PACKAGE_PREFIX = "mf.obp-apps"
|
7
8
|
|
8
9
|
CAPSULE_DEBUG = os.environ.get("OUTERBOUNDS_CAPSULE_DEBUG", False)
|
9
10
|
|
10
11
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
# Set basic fields
|
16
|
-
for key in ["name", "port", "image", "compute_pools"]:
|
17
|
-
if options.get(key):
|
18
|
-
config[key] = options[key]
|
19
|
-
|
20
|
-
# Handle list fields
|
21
|
-
if options.get("tags"):
|
22
|
-
config["tags"] = list(options["tags"])
|
23
|
-
if options.get("secrets"):
|
24
|
-
config["secrets"] = list(options["secrets"])
|
25
|
-
|
26
|
-
# Build env dict from key-value pairs
|
27
|
-
if options.get("envs"):
|
28
|
-
env_dict = {}
|
29
|
-
for env_item in options["envs"]:
|
30
|
-
env_dict.update(env_item)
|
31
|
-
config["environment"] = env_dict
|
32
|
-
|
33
|
-
# Handle dependencies (only one type allowed)
|
34
|
-
deps = {}
|
35
|
-
if options.get("dep_from_task"):
|
36
|
-
deps["from_task"] = options["dep_from_task"]
|
37
|
-
elif options.get("dep_from_run"):
|
38
|
-
deps["from_run"] = options["dep_from_run"]
|
39
|
-
elif options.get("dep_from_requirements"):
|
40
|
-
deps["from_requirements_file"] = options["dep_from_requirements"]
|
41
|
-
elif options.get("dep_from_pyproject"):
|
42
|
-
deps["from_pyproject_toml"] = options["dep_from_pyproject"]
|
43
|
-
|
44
|
-
# TODO: [FIX ME]: Get better CLI abstraction for pypi/conda dependencies
|
45
|
-
|
46
|
-
if deps:
|
47
|
-
config["dependencies"] = deps
|
48
|
-
|
49
|
-
# Handle resources
|
50
|
-
resources = {}
|
51
|
-
for key in ["cpu", "memory", "gpu", "storage"]:
|
52
|
-
if options.get(key):
|
53
|
-
resources[key] = options[key]
|
54
|
-
|
55
|
-
if resources:
|
56
|
-
config["resources"] = resources
|
57
|
-
|
58
|
-
# Handle health check options
|
59
|
-
health_check = {}
|
60
|
-
if options.get("health_check_enabled") is not None:
|
61
|
-
health_check["enabled"] = options["health_check_enabled"]
|
62
|
-
if options.get("health_check_path"):
|
63
|
-
health_check["path"] = options["health_check_path"]
|
64
|
-
if options.get("health_check_initial_delay") is not None:
|
65
|
-
health_check["initial_delay_seconds"] = options["health_check_initial_delay"]
|
66
|
-
if options.get("health_check_period") is not None:
|
67
|
-
health_check["period_seconds"] = options["health_check_period"]
|
68
|
-
|
69
|
-
if health_check:
|
70
|
-
config["health_check"] = health_check
|
71
|
-
|
72
|
-
# Handle package options
|
73
|
-
if options.get("package_src_path") or options.get("package_suffixes"):
|
74
|
-
config["package"] = {}
|
75
|
-
if options.get("package_src_path"):
|
76
|
-
config["package"]["src_path"] = options["package_src_path"]
|
77
|
-
if options.get("package_suffixes"):
|
78
|
-
config["package"]["suffixes"] = options["package_suffixes"]
|
79
|
-
|
80
|
-
# Handle auth options
|
81
|
-
if options.get("auth_type") or options.get("auth_public"):
|
82
|
-
config["auth"] = {}
|
83
|
-
if options.get("auth_type"):
|
84
|
-
config["auth"]["type"] = options["auth_type"]
|
85
|
-
if options.get("auth_public"):
|
86
|
-
config["auth"]["public"] = options["auth_public"]
|
87
|
-
|
88
|
-
return config
|
12
|
+
class classproperty(property):
|
13
|
+
def __get__(self, owner_self, owner_cls):
|
14
|
+
return self.fget(owner_cls)
|
89
15
|
|
90
16
|
|
91
17
|
class AppConfigError(Exception):
|
@@ -94,6 +20,19 @@ class AppConfigError(Exception):
|
|
94
20
|
pass
|
95
21
|
|
96
22
|
|
23
|
+
class AuthType:
|
24
|
+
BROWSER = "Browser"
|
25
|
+
API = "API"
|
26
|
+
|
27
|
+
@classmethod
|
28
|
+
def enums(cls):
|
29
|
+
return [cls.BROWSER, cls.API]
|
30
|
+
|
31
|
+
@classproperty
|
32
|
+
def default(cls):
|
33
|
+
return cls.BROWSER
|
34
|
+
|
35
|
+
|
97
36
|
class AppConfig:
|
98
37
|
"""Class representing an Outerbounds App configuration."""
|
99
38
|
|
@@ -140,7 +79,7 @@ class AppConfig:
|
|
140
79
|
if not self.config["auth"].get("public"):
|
141
80
|
self.config["auth"]["public"] = True
|
142
81
|
if not self.config["auth"].get("type"):
|
143
|
-
self.config["auth"]["type"] =
|
82
|
+
self.config["auth"]["type"] = AuthType.BROWSER
|
144
83
|
|
145
84
|
if not self.config.get("health_check"):
|
146
85
|
self.config["health_check"] = {}
|
@@ -156,6 +95,23 @@ class AppConfig:
|
|
156
95
|
if not self.config["resources"].get("disk"):
|
157
96
|
self.config["resources"]["disk"] = "20Gi"
|
158
97
|
|
98
|
+
if not self.config.get("replicas", None):
|
99
|
+
self.config["replicas"] = {
|
100
|
+
"min": 1,
|
101
|
+
"max": 1,
|
102
|
+
}
|
103
|
+
else:
|
104
|
+
max_is_set = self.config["replicas"].get("max", None) is not None
|
105
|
+
min_is_set = self.config["replicas"].get("min", None) is not None
|
106
|
+
if max_is_set and not min_is_set:
|
107
|
+
# If users want to set 0 replicas for min,
|
108
|
+
# then they need explicitly specify min to 0
|
109
|
+
self.config["replicas"]["min"] = 1 # Atleast set 1 replica
|
110
|
+
if min_is_set and not max_is_set:
|
111
|
+
# In the situations where we dont have min/max replicas, we can
|
112
|
+
# set max to min.
|
113
|
+
self.config["replicas"]["max"] = self.config["replicas"].get("min")
|
114
|
+
|
159
115
|
def _validate_required_fields(self) -> None:
|
160
116
|
"""Validate that all required fields are present."""
|
161
117
|
required_fields = self.schema.get("required", [])
|
@@ -232,6 +188,35 @@ class AppConfig:
|
|
232
188
|
f"You can only specify one mode of specifying dependencies. You have specified : {found_types} . Please only set one."
|
233
189
|
)
|
234
190
|
|
191
|
+
# Validate that each tag has exactly one key
|
192
|
+
if "tags" in self.config:
|
193
|
+
tags = self.config["tags"]
|
194
|
+
for tag in tags:
|
195
|
+
if not isinstance(tag, dict):
|
196
|
+
raise AppConfigError(
|
197
|
+
"Each tag must be a dictionary. %s is of type %s"
|
198
|
+
% (str(tag), type(tag))
|
199
|
+
)
|
200
|
+
if len(tag.keys()) != 1:
|
201
|
+
raise AppConfigError(
|
202
|
+
"Each tag must have exactly one key-value pair. Tag %s has %d key-value pairs."
|
203
|
+
% (str(tag), len(tag.keys()))
|
204
|
+
)
|
205
|
+
if "replicas" in self.config:
|
206
|
+
replicas = self.config["replicas"]
|
207
|
+
if not isinstance(replicas, dict):
|
208
|
+
raise AppConfigError("Replicas must be an object.")
|
209
|
+
max_is_set = self.config["replicas"].get("max", None) is not None
|
210
|
+
if max_is_set:
|
211
|
+
if replicas.get("max") == 0:
|
212
|
+
raise AppConfigError("Max replicas must be greater than 0.")
|
213
|
+
|
214
|
+
if replicas.get("min", 1) > replicas.get("max"):
|
215
|
+
raise AppConfigError(
|
216
|
+
"Min replicas must be less than max replicas. %s > %s"
|
217
|
+
% (replicas.get("min", 1), replicas.get("max", 1))
|
218
|
+
)
|
219
|
+
|
235
220
|
def to_dict(self) -> Dict[str, Any]:
|
236
221
|
"""Return the configuration as a dictionary."""
|
237
222
|
return self.config
|
outerbounds/apps/capsule.py
CHANGED
@@ -6,7 +6,8 @@ import time
|
|
6
6
|
import shlex
|
7
7
|
from typing import Optional
|
8
8
|
from .utils import TODOException, safe_requests_wrapper
|
9
|
-
from .app_config import AppConfig, CAPSULE_DEBUG
|
9
|
+
from .app_config import AppConfig, CAPSULE_DEBUG, AuthType
|
10
|
+
from . import experimental
|
10
11
|
|
11
12
|
|
12
13
|
class CapsuleStateMachine:
|
@@ -186,9 +187,9 @@ class CapsuleInput:
|
|
186
187
|
"ephemeralStorage": str(app_config.get_state("resources").get("disk")),
|
187
188
|
**resources,
|
188
189
|
},
|
189
|
-
"autoscalingConfig": {
|
190
|
-
"minReplicas": 1,
|
191
|
-
"maxReplicas": 1,
|
190
|
+
"autoscalingConfig": {
|
191
|
+
"minReplicas": app_config.get_state("replicas", {}).get("min", 1),
|
192
|
+
"maxReplicas": app_config.get_state("replicas", {}).get("max", 1),
|
192
193
|
},
|
193
194
|
**_scheduling_config,
|
194
195
|
"containerStartupConfig": {
|
@@ -202,14 +203,18 @@ class CapsuleInput:
|
|
202
203
|
"authType": app_config.get_state("auth").get("type"),
|
203
204
|
"publicToDeployment": app_config.get_state("auth").get("public"),
|
204
205
|
},
|
205
|
-
"tags":
|
206
|
+
"tags": [
|
207
|
+
dict(key=k, value=v)
|
208
|
+
for tag in app_config.get_state("tags", [])
|
209
|
+
for k, v in tag.items()
|
210
|
+
],
|
206
211
|
"port": app_config.get_state("port"),
|
207
212
|
"displayName": app_config.get_state("name"),
|
208
213
|
}
|
209
214
|
|
210
215
|
|
211
|
-
def create_capsule(
|
212
|
-
_data = json.dumps(
|
216
|
+
def create_capsule(capsule_input: dict, api_url: str, request_headers: dict):
|
217
|
+
_data = json.dumps(capsule_input)
|
213
218
|
response = safe_requests_wrapper(
|
214
219
|
requests.post,
|
215
220
|
api_url,
|
@@ -273,18 +278,6 @@ def delete_capsule(capsule_id: str, api_url: str, request_headers: dict):
|
|
273
278
|
return response.json()
|
274
279
|
|
275
280
|
|
276
|
-
class StatusTrail:
|
277
|
-
def __init__(self, capsule_id: str):
|
278
|
-
self._capsule_id = capsule_id
|
279
|
-
self._status_trail = []
|
280
|
-
|
281
|
-
def add_status(self, status: dict):
|
282
|
-
self._status_trail.append({"timestamp": time.time(), "status": status})
|
283
|
-
|
284
|
-
def get_status_trail(self):
|
285
|
-
return self._status_trail
|
286
|
-
|
287
|
-
|
288
281
|
class Capsule:
|
289
282
|
|
290
283
|
status: CapsuleStateMachine
|
@@ -318,10 +311,10 @@ class Capsule:
|
|
318
311
|
|
319
312
|
@property
|
320
313
|
def capsule_type(self):
|
321
|
-
auth_type = self._app_config.get_state("auth", {}).get("type",
|
322
|
-
if auth_type ==
|
314
|
+
auth_type = self._app_config.get_state("auth", {}).get("type", AuthType.default)
|
315
|
+
if auth_type == AuthType.BROWSER:
|
323
316
|
return "App"
|
324
|
-
elif auth_type ==
|
317
|
+
elif auth_type == AuthType.API:
|
325
318
|
return "Endpoint"
|
326
319
|
else:
|
327
320
|
raise TODOException(f"Unknown auth type: {auth_type}")
|
@@ -331,11 +324,13 @@ class Capsule:
|
|
331
324
|
return self._app_config.get_state("name")
|
332
325
|
|
333
326
|
def create_input(self):
|
334
|
-
return
|
327
|
+
return experimental.capsule_input_overrides(
|
328
|
+
self._app_config, CapsuleInput.from_app_config(self._app_config)
|
329
|
+
)
|
335
330
|
|
336
331
|
def create(self):
|
337
332
|
capsule_response = create_capsule(
|
338
|
-
self.
|
333
|
+
self.create_input(), self._base_url, self._request_headers
|
339
334
|
)
|
340
335
|
self.identifier = capsule_response.get("id")
|
341
336
|
return self.identifier
|
@@ -346,18 +341,14 @@ class Capsule:
|
|
346
341
|
|
347
342
|
def wait_for_terminal_state(self, logger=print):
|
348
343
|
state_machine = CapsuleStateMachine(self.identifier)
|
344
|
+
logger(
|
345
|
+
"💊 Waiting for %s %s to be ready to serve traffic"
|
346
|
+
% (self.capsule_type.lower(), self.identifier)
|
347
|
+
)
|
349
348
|
for i in range(self._create_timeout):
|
350
349
|
capsule_response = self.get()
|
351
350
|
state_machine.add_status(capsule_response.get("status", {}))
|
352
351
|
time.sleep(1)
|
353
|
-
if state_machine.is_completely_new_capsule() and i == 0:
|
354
|
-
logger(
|
355
|
-
"🔧 🛠️ Creating new %s with id %s"
|
356
|
-
% (self.capsule_type.lower(), self.identifier)
|
357
|
-
)
|
358
|
-
elif not state_machine.is_completely_new_capsule() and i == 0:
|
359
|
-
logger("🔧 🛠️ Updating %s %s" % (self.capsule_type, self.identifier))
|
360
|
-
|
361
352
|
state_machine.report_current_status(logger)
|
362
353
|
if state_machine.ready_to_serve_traffic:
|
363
354
|
logger(
|
@@ -0,0 +1,91 @@
|
|
1
|
+
from . import experimental
|
2
|
+
|
3
|
+
|
4
|
+
def build_config_from_options(options):
|
5
|
+
"""Build an app configuration from CLI options."""
|
6
|
+
config = {}
|
7
|
+
|
8
|
+
# Set basic fields
|
9
|
+
for key in ["name", "port", "image", "compute_pools"]:
|
10
|
+
if options.get(key):
|
11
|
+
config[key] = options[key]
|
12
|
+
|
13
|
+
# Handle list fields
|
14
|
+
if options.get("tags"):
|
15
|
+
config["tags"] = list(options["tags"])
|
16
|
+
if options.get("secrets"):
|
17
|
+
config["secrets"] = list(options["secrets"])
|
18
|
+
|
19
|
+
# Build env dict from key-value pairs
|
20
|
+
if options.get("envs"):
|
21
|
+
env_dict = {}
|
22
|
+
for env_item in options["envs"]:
|
23
|
+
env_dict.update(env_item)
|
24
|
+
config["environment"] = env_dict
|
25
|
+
|
26
|
+
# Handle dependencies (only one type allowed)
|
27
|
+
deps = {}
|
28
|
+
if options.get("dep_from_task"):
|
29
|
+
deps["from_task"] = options["dep_from_task"]
|
30
|
+
elif options.get("dep_from_run"):
|
31
|
+
deps["from_run"] = options["dep_from_run"]
|
32
|
+
elif options.get("dep_from_requirements"):
|
33
|
+
deps["from_requirements_file"] = options["dep_from_requirements"]
|
34
|
+
elif options.get("dep_from_pyproject"):
|
35
|
+
deps["from_pyproject_toml"] = options["dep_from_pyproject"]
|
36
|
+
|
37
|
+
# TODO: [FIX ME]: Get better CLI abstraction for pypi/conda dependencies
|
38
|
+
|
39
|
+
if deps:
|
40
|
+
config["dependencies"] = deps
|
41
|
+
|
42
|
+
# Handle resources
|
43
|
+
resources = {}
|
44
|
+
for key in ["cpu", "memory", "gpu", "storage"]:
|
45
|
+
if options.get(key):
|
46
|
+
resources[key] = options[key]
|
47
|
+
|
48
|
+
if resources:
|
49
|
+
config["resources"] = resources
|
50
|
+
|
51
|
+
# Handle health check options
|
52
|
+
health_check = {}
|
53
|
+
if options.get("health_check_enabled") is not None:
|
54
|
+
health_check["enabled"] = options["health_check_enabled"]
|
55
|
+
if options.get("health_check_path"):
|
56
|
+
health_check["path"] = options["health_check_path"]
|
57
|
+
if options.get("health_check_initial_delay") is not None:
|
58
|
+
health_check["initial_delay_seconds"] = options["health_check_initial_delay"]
|
59
|
+
if options.get("health_check_period") is not None:
|
60
|
+
health_check["period_seconds"] = options["health_check_period"]
|
61
|
+
|
62
|
+
if health_check:
|
63
|
+
config["health_check"] = health_check
|
64
|
+
|
65
|
+
# Handle package options
|
66
|
+
if options.get("package_src_path") or options.get("package_suffixes"):
|
67
|
+
config["package"] = {}
|
68
|
+
if options.get("package_src_path"):
|
69
|
+
config["package"]["src_path"] = options["package_src_path"]
|
70
|
+
if options.get("package_suffixes"):
|
71
|
+
config["package"]["suffixes"] = options["package_suffixes"]
|
72
|
+
|
73
|
+
# Handle auth options
|
74
|
+
if options.get("auth_type") or options.get("auth_public"):
|
75
|
+
config["auth"] = {}
|
76
|
+
if options.get("auth_type"):
|
77
|
+
config["auth"]["type"] = options["auth_type"]
|
78
|
+
if options.get("auth_public"):
|
79
|
+
config["auth"]["public"] = options["auth_public"]
|
80
|
+
|
81
|
+
replicas = {}
|
82
|
+
if options.get("min_replicas"):
|
83
|
+
replicas["min"] = options["min_replicas"]
|
84
|
+
if options.get("max_replicas"):
|
85
|
+
replicas["max"] = options["max_replicas"]
|
86
|
+
if len(replicas) > 0:
|
87
|
+
config["replicas"] = replicas
|
88
|
+
|
89
|
+
config.update(experimental.build_config_from_options(options))
|
90
|
+
|
91
|
+
return config
|
@@ -2,7 +2,9 @@
|
|
2
2
|
title: Outerbounds App Configuration Schema
|
3
3
|
description: |
|
4
4
|
Schema for defining Outerbounds Apps configuration. This schema is what we will end up using on the CLI/programmatic interface.
|
5
|
-
|
5
|
+
How to read this schema:
|
6
|
+
1. If the a property has `allow_union`:true then it will allow overrides from the cli.
|
7
|
+
2. If a property has `experimental` set to true then a lot its validations may-be skipped and parsing handled somewhere else.
|
6
8
|
version: 1.0.0
|
7
9
|
type: object
|
8
10
|
required:
|
@@ -27,8 +29,10 @@ properties:
|
|
27
29
|
type: array
|
28
30
|
description: The tags of the app to deploy.
|
29
31
|
items:
|
30
|
-
type:
|
31
|
-
example:
|
32
|
+
type: object
|
33
|
+
example:
|
34
|
+
- foo: bar
|
35
|
+
- x: y
|
32
36
|
image: # Only used in `deploy` command
|
33
37
|
allow_union: true # We will overrwite the image if specified on the CLI.
|
34
38
|
type: string
|
@@ -141,6 +145,20 @@ properties:
|
|
141
145
|
description: Storage resource request and limit.
|
142
146
|
example: "1Gi"
|
143
147
|
default: "10Gi"
|
148
|
+
replicas:
|
149
|
+
allow_union: true
|
150
|
+
type: object
|
151
|
+
description: |
|
152
|
+
The number of replicas to deploy the app with.
|
153
|
+
properties:
|
154
|
+
min:
|
155
|
+
type: integer
|
156
|
+
description: The minimum number of replicas to deploy the app with.
|
157
|
+
example: 1
|
158
|
+
max:
|
159
|
+
type: integer
|
160
|
+
description: The maximum number of replicas to deploy the app with.
|
161
|
+
example: 10
|
144
162
|
health_check: # Can be used in `run` command
|
145
163
|
type: object
|
146
164
|
# `allow_union` property means that any object in this field will be done a union with the config file if something is provided on commanline.
|
@@ -183,7 +201,7 @@ properties:
|
|
183
201
|
type: string
|
184
202
|
description: |
|
185
203
|
The type of authentication to use for the app.
|
186
|
-
enum: [API,
|
204
|
+
enum: [API, Browser]
|
187
205
|
public:
|
188
206
|
type: boolean
|
189
207
|
description: |
|
@@ -192,3 +210,41 @@ properties:
|
|
192
210
|
# There is an allowed perimeters property
|
193
211
|
# But that needs a little more thought on how
|
194
212
|
# to expose.
|
213
|
+
|
214
|
+
# ------------------------------------ EXPERIMENTAL ------------------------------------
|
215
|
+
project:
|
216
|
+
type: string
|
217
|
+
description: The project name to deploy the app to.
|
218
|
+
experimental: true
|
219
|
+
allow-union: true
|
220
|
+
branch:
|
221
|
+
type: string
|
222
|
+
description: The branch name to deploy the app to.
|
223
|
+
experimental: true
|
224
|
+
allow-union: true
|
225
|
+
|
226
|
+
models: #
|
227
|
+
type: array
|
228
|
+
description: model asset ids to include with the deployment. NO CLI Option for this Now.
|
229
|
+
experimental: true
|
230
|
+
allow-union: true
|
231
|
+
items:
|
232
|
+
type: object
|
233
|
+
properties:
|
234
|
+
asset_id:
|
235
|
+
type: string
|
236
|
+
asset_instance_id:
|
237
|
+
type: string
|
238
|
+
data: #
|
239
|
+
type: array
|
240
|
+
description: data asset ids to include with the deployment.
|
241
|
+
experimental: true
|
242
|
+
allow-union: true
|
243
|
+
items:
|
244
|
+
type: object
|
245
|
+
properties:
|
246
|
+
asset_id:
|
247
|
+
type: string
|
248
|
+
asset_instance_id:
|
249
|
+
type: string
|
250
|
+
# ------------------------------------ EXPERIMENTAL ------------------------------------
|
@@ -0,0 +1,103 @@
|
|
1
|
+
from functools import wraps
|
2
|
+
from outerbounds._vendor import click
|
3
|
+
import os
|
4
|
+
from typing import TYPE_CHECKING
|
5
|
+
|
6
|
+
if TYPE_CHECKING:
|
7
|
+
from ..app_config import AppConfig
|
8
|
+
|
9
|
+
DEFAULT_BRANCH = "test"
|
10
|
+
|
11
|
+
|
12
|
+
def wrapping_cli_options(func):
|
13
|
+
@click.option(
|
14
|
+
"--project",
|
15
|
+
type=str,
|
16
|
+
help="The flow project the app/endpoint belongs to",
|
17
|
+
default=None,
|
18
|
+
)
|
19
|
+
@click.option(
|
20
|
+
"--branch",
|
21
|
+
type=str,
|
22
|
+
help="The branch the app/endpoint belongs to",
|
23
|
+
default=None,
|
24
|
+
)
|
25
|
+
@wraps(func)
|
26
|
+
def wrapper(*args, **kwargs):
|
27
|
+
return func(*args, **kwargs)
|
28
|
+
|
29
|
+
return wrapper
|
30
|
+
|
31
|
+
|
32
|
+
def build_config_from_options(options):
|
33
|
+
"""Build an app configuration from CLI options."""
|
34
|
+
keys = [
|
35
|
+
"project",
|
36
|
+
"branch",
|
37
|
+
]
|
38
|
+
config = {}
|
39
|
+
for key in keys:
|
40
|
+
if options.get(key):
|
41
|
+
config[key] = options.get(key)
|
42
|
+
|
43
|
+
return config
|
44
|
+
|
45
|
+
|
46
|
+
# Account for project / branch and the capsule input.
|
47
|
+
def capsule_input_overrides(app_config: "AppConfig", capsule_input: dict):
|
48
|
+
project = app_config.get_state("project", None)
|
49
|
+
# Update the project/branch related configurations.
|
50
|
+
if project is not None:
|
51
|
+
branch = app_config.get_state("branch", DEFAULT_BRANCH)
|
52
|
+
capsule_input["tags"].extend(
|
53
|
+
[dict(key="project", value=project), dict(key="branch", value=branch)]
|
54
|
+
)
|
55
|
+
|
56
|
+
model_asset_conf = app_config.get_state("models", None)
|
57
|
+
data_asset_conf = app_config.get_state("data", None)
|
58
|
+
code_info = _code_info(app_config)
|
59
|
+
# todo:fix me
|
60
|
+
_objects_key = "associatedObjects"
|
61
|
+
if model_asset_conf or data_asset_conf or code_info:
|
62
|
+
capsule_input[_objects_key] = {}
|
63
|
+
|
64
|
+
if model_asset_conf:
|
65
|
+
capsule_input[_objects_key]["models"] = [
|
66
|
+
{"assetId": x["asset_id"], "assetInstanceId": x["asset_instance_id"]}
|
67
|
+
for x in model_asset_conf
|
68
|
+
]
|
69
|
+
if data_asset_conf:
|
70
|
+
capsule_input[_objects_key]["data"] = [
|
71
|
+
{"assetId": x["asset_id"], "assetInstanceId": x["asset_instance_id"]}
|
72
|
+
for x in data_asset_conf
|
73
|
+
]
|
74
|
+
if code_info:
|
75
|
+
capsule_input[_objects_key]["code"] = code_info
|
76
|
+
|
77
|
+
return capsule_input
|
78
|
+
|
79
|
+
|
80
|
+
def _code_info(app_config: "AppConfig"):
|
81
|
+
from metaflow.metaflow_git import get_repository_info, _call_git
|
82
|
+
|
83
|
+
repo_info = get_repository_info(app_config.get_state("packaging_directory", None))
|
84
|
+
if len(repo_info) == 0:
|
85
|
+
return None
|
86
|
+
|
87
|
+
git_log_info, returncode, failed = _call_git(
|
88
|
+
["log", "-1", "--pretty=%B"],
|
89
|
+
path=app_config.get_state("packaging_directory", None),
|
90
|
+
)
|
91
|
+
_url = (
|
92
|
+
repo_info["repo_url"]
|
93
|
+
if not repo_info["repo_url"].endswith(".git")
|
94
|
+
else repo_info["repo_url"].rstrip(".git")
|
95
|
+
)
|
96
|
+
_code_info = {
|
97
|
+
"commitId": repo_info["commit_sha"],
|
98
|
+
"commitLink": os.path.join(_url, "commit", repo_info["commit_sha"]),
|
99
|
+
}
|
100
|
+
if not failed and returncode == 0:
|
101
|
+
_code_info["commitMessage"] = git_log_info.strip()
|
102
|
+
|
103
|
+
return _code_info
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: outerbounds
|
3
|
-
Version: 0.3.
|
3
|
+
Version: 0.3.175rc0
|
4
4
|
Summary: More Data Science, Less Administration
|
5
5
|
License: Proprietary
|
6
6
|
Keywords: data science,machine learning,MLOps
|
@@ -29,8 +29,8 @@ Requires-Dist: google-cloud-secret-manager (>=2.20.0,<3.0.0) ; extra == "gcp"
|
|
29
29
|
Requires-Dist: google-cloud-storage (>=2.14.0,<3.0.0) ; extra == "gcp"
|
30
30
|
Requires-Dist: metaflow-checkpoint (==0.2.1)
|
31
31
|
Requires-Dist: ob-metaflow (==2.15.14.1)
|
32
|
-
Requires-Dist: ob-metaflow-extensions (==1.1.
|
33
|
-
Requires-Dist: ob-metaflow-stubs (==6.0.3.
|
32
|
+
Requires-Dist: ob-metaflow-extensions (==1.1.162rc0)
|
33
|
+
Requires-Dist: ob-metaflow-stubs (==6.0.3.175rc0)
|
34
34
|
Requires-Dist: opentelemetry-distro (>=0.41b0) ; extra == "otel"
|
35
35
|
Requires-Dist: opentelemetry-exporter-otlp-proto-http (>=1.20.0) ; extra == "otel"
|
36
36
|
Requires-Dist: opentelemetry-instrumentation-requests (>=0.41b0) ; extra == "otel"
|
@@ -40,16 +40,18 @@ outerbounds/_vendor/yaml/scanner.py,sha256=ZcI8IngR56PaQ0m27WU2vxCqmDCuRjz-hr7pi
|
|
40
40
|
outerbounds/_vendor/yaml/serializer.py,sha256=8wFZRy9SsQSktF_f9OOroroqsh4qVUe53ry07P9UgCc,4368
|
41
41
|
outerbounds/_vendor/yaml/tokens.py,sha256=JBSu38wihGr4l73JwbfMA7Ks1-X84g8-NskTz7KwPmA,2578
|
42
42
|
outerbounds/apps/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
43
|
-
outerbounds/apps/app_cli.py,sha256=
|
44
|
-
outerbounds/apps/app_config.py,sha256=
|
43
|
+
outerbounds/apps/app_cli.py,sha256=oUsPGDr3jnW5l7h4Mp1I4WnRZAanXN1se9_aQ1ybhNQ,17559
|
44
|
+
outerbounds/apps/app_config.py,sha256=KBmW9grhiuG9XZG-R0GZkM-024cjj6ztGzOX_2wZW34,11291
|
45
45
|
outerbounds/apps/artifacts.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
46
|
-
outerbounds/apps/capsule.py,sha256=
|
46
|
+
outerbounds/apps/capsule.py,sha256=n23uJi9877IVnArgqdq_ES_c2vAql8rzVm7jlTYUr1M,13351
|
47
|
+
outerbounds/apps/cli_to_config.py,sha256=IjPHeH1lC7l9WPkNLQIWU7obRS3G0G8aTNVZemUvmuw,3168
|
47
48
|
outerbounds/apps/code_package/__init__.py,sha256=8McF7pgx8ghvjRnazp2Qktlxi9yYwNiwESSQrk-2oW8,68
|
48
49
|
outerbounds/apps/code_package/code_packager.py,sha256=SQDBXKwizzpag5GpwoZpvvkyPOodRSQwk2ecAAfO0HI,23316
|
49
50
|
outerbounds/apps/code_package/examples.py,sha256=aF8qKIJxCVv_ugcShQjqUsXKKKMsm1oMkQIl8w3QKuw,4016
|
50
|
-
outerbounds/apps/config_schema.yaml,sha256=
|
51
|
+
outerbounds/apps/config_schema.yaml,sha256=D-qopf3mGZusa4n5GIbrssoJHS3v96_yponFEM127b4,8275
|
51
52
|
outerbounds/apps/dependencies.py,sha256=SqvdFQdFZZW0wXX_CHMHCrfE0TwaRkTvGCRbQ2Mx3q0,3935
|
52
53
|
outerbounds/apps/deployer.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
54
|
+
outerbounds/apps/experimental/__init__.py,sha256=12L_FzZyzv162uo4I6cmlrxat7feUtIu_kxbObTJZTA,3059
|
53
55
|
outerbounds/apps/secrets.py,sha256=27qf04lOBqRjvcswj0ldHOmntP2T6SEjtMJtkJQ_GUg,6100
|
54
56
|
outerbounds/apps/utils.py,sha256=JymjsgpU0osF-eDvstFS9zkM7bqliJdqAEV7kAjqxCM,7298
|
55
57
|
outerbounds/apps/validations.py,sha256=AVEw9eCvkzqq1m5ZC8btaWrSR6kWYKzarELfrASuAwQ,1117
|
@@ -69,7 +71,7 @@ outerbounds/utils/metaflowconfig.py,sha256=l2vJbgPkLISU-XPGZFaC8ZKmYFyJemlD6bwB-
|
|
69
71
|
outerbounds/utils/schema.py,sha256=lMUr9kNgn9wy-sO_t_Tlxmbt63yLeN4b0xQXbDUDj4A,2331
|
70
72
|
outerbounds/utils/utils.py,sha256=4Z8cszNob_8kDYCLNTrP-wWads_S_MdL3Uj3ju4mEsk,501
|
71
73
|
outerbounds/vendor.py,sha256=gRLRJNXtZBeUpPEog0LOeIsl6GosaFFbCxUvR4bW6IQ,5093
|
72
|
-
outerbounds-0.3.
|
73
|
-
outerbounds-0.3.
|
74
|
-
outerbounds-0.3.
|
75
|
-
outerbounds-0.3.
|
74
|
+
outerbounds-0.3.175rc0.dist-info/METADATA,sha256=c69bE_dY1O_vbcTLpQ0FWj2xL0rF1sIvuI-e3A8MTXA,1846
|
75
|
+
outerbounds-0.3.175rc0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
76
|
+
outerbounds-0.3.175rc0.dist-info/entry_points.txt,sha256=7ye0281PKlvqxu15rjw60zKg2pMsXI49_A8BmGqIqBw,47
|
77
|
+
outerbounds-0.3.175rc0.dist-info/RECORD,,
|
File without changes
|
File without changes
|