outerbounds 0.3.175rc0__py3-none-any.whl → 0.3.176rc1__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 +179 -5
- outerbounds/apps/capsule.py +111 -13
- outerbounds/apps/cli_to_config.py +1 -1
- outerbounds/apps/config_schema.yaml +10 -1
- outerbounds/apps/utils.py +26 -0
- outerbounds/command_groups/apps_cli.py +1 -1
- outerbounds/command_groups/cli.py +2 -0
- outerbounds/command_groups/flowprojects_cli.py +137 -0
- {outerbounds-0.3.175rc0.dist-info → outerbounds-0.3.176rc1.dist-info}/METADATA +3 -3
- {outerbounds-0.3.175rc0.dist-info → outerbounds-0.3.176rc1.dist-info}/RECORD +12 -11
- {outerbounds-0.3.175rc0.dist-info → outerbounds-0.3.176rc1.dist-info}/WHEEL +0 -0
- {outerbounds-0.3.175rc0.dist-info → outerbounds-0.3.176rc1.dist-info}/entry_points.txt +0 -0
outerbounds/apps/app_cli.py
CHANGED
@@ -13,14 +13,11 @@ from .app_config import (
|
|
13
13
|
AuthType,
|
14
14
|
)
|
15
15
|
from .cli_to_config import build_config_from_options
|
16
|
-
from .utils import
|
17
|
-
CommaSeparatedListType,
|
18
|
-
KVPairType,
|
19
|
-
)
|
16
|
+
from .utils import CommaSeparatedListType, KVPairType, KVDictType
|
20
17
|
from . import experimental
|
21
18
|
from .validations import deploy_validations
|
22
19
|
from .code_package import CodePackager
|
23
|
-
from .capsule import Capsule
|
20
|
+
from .capsule import Capsule, list_and_filter_capsules
|
24
21
|
import shlex
|
25
22
|
import time
|
26
23
|
import uuid
|
@@ -30,6 +27,8 @@ LOGGER_TIMESTAMP = "magenta"
|
|
30
27
|
LOGGER_COLOR = "green"
|
31
28
|
LOGGER_BAD_COLOR = "red"
|
32
29
|
|
30
|
+
NativeList = list
|
31
|
+
|
33
32
|
|
34
33
|
def _logger(
|
35
34
|
body="", system_msg=False, head="", bad=False, timestamp=True, nl=True, color=None
|
@@ -75,6 +74,36 @@ def _pre_create_debug(app_config: AppConfig, capsule: Capsule, state_dir: str):
|
|
75
74
|
)
|
76
75
|
|
77
76
|
|
77
|
+
def print_table(data, headers):
|
78
|
+
"""Print data in a formatted table."""
|
79
|
+
if not data:
|
80
|
+
return
|
81
|
+
|
82
|
+
# Calculate column widths
|
83
|
+
col_widths = [len(h) for h in headers]
|
84
|
+
|
85
|
+
# Calculate actual widths based on data
|
86
|
+
for row in data:
|
87
|
+
for i, cell in enumerate(row):
|
88
|
+
col_widths[i] = max(col_widths[i], len(str(cell)))
|
89
|
+
|
90
|
+
# Print header
|
91
|
+
header_row = " | ".join(
|
92
|
+
[headers[i].ljust(col_widths[i]) for i in range(len(headers))]
|
93
|
+
)
|
94
|
+
click.secho("-" * len(header_row), fg="yellow")
|
95
|
+
click.secho(header_row, fg="yellow", bold=True)
|
96
|
+
click.secho("-" * len(header_row), fg="yellow")
|
97
|
+
|
98
|
+
# Print data rows
|
99
|
+
for row in data:
|
100
|
+
formatted_row = " | ".join(
|
101
|
+
[str(row[i]).ljust(col_widths[i]) for i in range(len(row))]
|
102
|
+
)
|
103
|
+
click.secho(formatted_row, fg="green", bold=True)
|
104
|
+
click.secho("-" * len(header_row), fg="yellow")
|
105
|
+
|
106
|
+
|
78
107
|
@click.group()
|
79
108
|
def cli():
|
80
109
|
"""Outerbounds CLI tool."""
|
@@ -231,6 +260,18 @@ def common_deploy_options(func):
|
|
231
260
|
help="Maximum number of replicas to deploy",
|
232
261
|
default=None,
|
233
262
|
)
|
263
|
+
@click.option(
|
264
|
+
"--description",
|
265
|
+
type=str,
|
266
|
+
help="The description of the app to deploy.",
|
267
|
+
default=None,
|
268
|
+
)
|
269
|
+
@click.option(
|
270
|
+
"--app-type",
|
271
|
+
type=str,
|
272
|
+
help="The type of app to deploy.",
|
273
|
+
default=None,
|
274
|
+
)
|
234
275
|
@wraps(func)
|
235
276
|
def wrapper(*args, **kwargs):
|
236
277
|
return func(*args, **kwargs)
|
@@ -478,6 +519,139 @@ def deploy(ctx, command, **options):
|
|
478
519
|
raise e
|
479
520
|
|
480
521
|
|
522
|
+
def _parse_capsule_table(filtered_capsules):
|
523
|
+
headers = ["Name", "ID", "Ready", "App Type", "Port", "Tags", "URL"]
|
524
|
+
table_data = []
|
525
|
+
|
526
|
+
for capsule in filtered_capsules:
|
527
|
+
spec = capsule.get("spec", {})
|
528
|
+
status = capsule.get("status", {})
|
529
|
+
cap_id = capsule.get("id")
|
530
|
+
|
531
|
+
display_name = spec.get("displayName", "")
|
532
|
+
ready = str(status.get("readyToServeTraffic", False))
|
533
|
+
auth_type = spec.get("authConfig", {}).get("authType", "")
|
534
|
+
port = str(spec.get("port", ""))
|
535
|
+
tags_str = ", ".join(
|
536
|
+
[f"{tag['key']}={tag['value']}" for tag in spec.get("tags", [])]
|
537
|
+
)
|
538
|
+
url = status.get("accessInfo", {}).get("outOfClusterURL", "")
|
539
|
+
|
540
|
+
table_data.append(
|
541
|
+
[
|
542
|
+
display_name,
|
543
|
+
cap_id,
|
544
|
+
ready,
|
545
|
+
auth_type,
|
546
|
+
port,
|
547
|
+
tags_str,
|
548
|
+
f"https://{url}",
|
549
|
+
]
|
550
|
+
)
|
551
|
+
return headers, table_data
|
552
|
+
|
553
|
+
|
554
|
+
@app.command(help="List apps in the Outerbounds Platform.")
|
555
|
+
@click.option("--project", type=str, help="Filter apps by project")
|
556
|
+
@click.option("--branch", type=str, help="Filter apps by branch")
|
557
|
+
@click.option("--name", type=str, help="Filter apps by name")
|
558
|
+
@click.option(
|
559
|
+
"--tag",
|
560
|
+
"tags",
|
561
|
+
type=KVDictType,
|
562
|
+
help="Filter apps by tag. Format KEY=VALUE. Example --tag foo=bar --tag x=y. If multiple tags are provided, the app must match all of them.",
|
563
|
+
multiple=True,
|
564
|
+
)
|
565
|
+
@click.option(
|
566
|
+
"--format",
|
567
|
+
type=click.Choice(["json", "text"]),
|
568
|
+
help="Format the output",
|
569
|
+
default="text",
|
570
|
+
)
|
571
|
+
@click.option(
|
572
|
+
"--auth-type", type=click.Choice(AuthType.enums()), help="Filter apps by Auth type"
|
573
|
+
)
|
574
|
+
@click.pass_context
|
575
|
+
def list(ctx, project, branch, name, tags, format, auth_type):
|
576
|
+
"""List apps in the Outerbounds Platform."""
|
577
|
+
|
578
|
+
filtered_capsules = list_and_filter_capsules(
|
579
|
+
ctx.obj.api_url, ctx.obj.perimeter, project, branch, name, tags, auth_type, None
|
580
|
+
)
|
581
|
+
if format == "json":
|
582
|
+
click.echo(json.dumps(filtered_capsules, indent=4))
|
583
|
+
else:
|
584
|
+
headers, table_data = _parse_capsule_table(filtered_capsules)
|
585
|
+
print_table(table_data, headers)
|
586
|
+
|
587
|
+
|
588
|
+
@app.command(help="Delete an app/apps from the Outerbounds Platform.")
|
589
|
+
@click.option("--name", type=str, help="Filter app to delete by name")
|
590
|
+
@click.option("--id", "cap_id", type=str, help="Filter app to delete by id")
|
591
|
+
@click.option("--project", type=str, help="Filter apps to delete by project")
|
592
|
+
@click.option("--branch", type=str, help="Filter apps to delete by branch")
|
593
|
+
@click.option(
|
594
|
+
"--tag",
|
595
|
+
"tags",
|
596
|
+
multiple=True,
|
597
|
+
type=KVPairType,
|
598
|
+
help="Filter apps to delete by tag. Format KEY=VALUE. Example --tag foo=bar --tag x=y. If multiple tags are provided, the app must match all of them.",
|
599
|
+
)
|
600
|
+
@click.pass_context
|
601
|
+
def delete(ctx, name, cap_id, project, branch, tags):
|
602
|
+
|
603
|
+
"""Delete an app/apps from the Outerbounds Platform."""
|
604
|
+
# Atleast one of the args need to be provided
|
605
|
+
if not any(
|
606
|
+
[
|
607
|
+
name is not None,
|
608
|
+
cap_id is not None,
|
609
|
+
project is not None,
|
610
|
+
branch is not None,
|
611
|
+
len(tags) != 0,
|
612
|
+
]
|
613
|
+
):
|
614
|
+
raise AppConfigError(
|
615
|
+
"Atleast one of the options need to be provided. You can use --name, --id, --project, --branch, --tag"
|
616
|
+
)
|
617
|
+
|
618
|
+
filtered_capsules = list_and_filter_capsules(
|
619
|
+
ctx.obj.api_url, ctx.obj.perimeter, project, branch, name, tags, None, cap_id
|
620
|
+
)
|
621
|
+
|
622
|
+
headers, table_data = _parse_capsule_table(filtered_capsules)
|
623
|
+
click.secho("The following apps will be deleted:", fg="red", bold=True)
|
624
|
+
print_table(table_data, headers)
|
625
|
+
|
626
|
+
# Confirm the deletion
|
627
|
+
confirm = click.prompt(
|
628
|
+
click.style(
|
629
|
+
"💊 Are you sure you want to delete these apps?", fg="red", bold=True
|
630
|
+
),
|
631
|
+
default="no",
|
632
|
+
type=click.Choice(["yes", "no"]),
|
633
|
+
)
|
634
|
+
if confirm == "no":
|
635
|
+
exit(1)
|
636
|
+
|
637
|
+
def item_show_func(x):
|
638
|
+
if not x:
|
639
|
+
return None
|
640
|
+
name = x.get("spec", {}).get("displayName", "")
|
641
|
+
id = x.get("id", "")
|
642
|
+
return click.style("💊 deleting %s [%s]" % (name, id), fg="red", bold=True)
|
643
|
+
|
644
|
+
with click.progressbar(
|
645
|
+
filtered_capsules,
|
646
|
+
label=click.style("💊 Deleting apps...", fg="red", bold=True),
|
647
|
+
fill_char=click.style("█", fg="red", bold=True),
|
648
|
+
empty_char=click.style("░", fg="red", bold=True),
|
649
|
+
item_show_func=item_show_func,
|
650
|
+
) as bar:
|
651
|
+
for capsule in bar:
|
652
|
+
Capsule.delete(capsule.get("id"), ctx.obj.api_url, ctx.obj.perimeter)
|
653
|
+
|
654
|
+
|
481
655
|
@app.command(help="Run an app locally (for testing).")
|
482
656
|
@common_run_options
|
483
657
|
@click.pass_context
|
outerbounds/apps/capsule.py
CHANGED
@@ -172,10 +172,16 @@ class CapsuleInput:
|
|
172
172
|
_scheduling_config["computePools"] = [
|
173
173
|
{"name": x} for x in app_config.get_state("compute_pools")
|
174
174
|
]
|
175
|
-
|
175
|
+
_description = app_config.get_state("description")
|
176
|
+
_app_type = app_config.get_state("app_type")
|
177
|
+
_final_info = {}
|
178
|
+
if _description:
|
179
|
+
_final_info["description"] = _description
|
180
|
+
if _app_type:
|
181
|
+
_final_info["endpointType"] = _app_type
|
176
182
|
return {
|
177
183
|
"perimeter": app_config.get_state("perimeter"),
|
178
|
-
|
184
|
+
**_final_info,
|
179
185
|
"codePackagePath": app_config.get_state("code_package_url"),
|
180
186
|
"image": app_config.get_state("image"),
|
181
187
|
"resourceIntegrations": [
|
@@ -264,9 +270,10 @@ def get_capsule(capsule_id: str, api_url: str, request_headers: dict):
|
|
264
270
|
|
265
271
|
|
266
272
|
def delete_capsule(capsule_id: str, api_url: str, request_headers: dict):
|
273
|
+
_url = os.path.join(api_url, capsule_id)
|
267
274
|
response = safe_requests_wrapper(
|
268
275
|
requests.delete,
|
269
|
-
|
276
|
+
_url,
|
270
277
|
headers=request_headers,
|
271
278
|
retryable_status_codes=[409], # todo : verify me
|
272
279
|
)
|
@@ -278,12 +285,112 @@ def delete_capsule(capsule_id: str, api_url: str, request_headers: dict):
|
|
278
285
|
return response.json()
|
279
286
|
|
280
287
|
|
288
|
+
def list_capsules(api_url: str, request_headers: dict):
|
289
|
+
response = safe_requests_wrapper(
|
290
|
+
requests.get,
|
291
|
+
api_url,
|
292
|
+
headers=request_headers,
|
293
|
+
)
|
294
|
+
if response.status_code >= 400:
|
295
|
+
raise TODOException(
|
296
|
+
f"Failed to list capsules: {response.status_code} {response.text}"
|
297
|
+
)
|
298
|
+
return response.json()
|
299
|
+
|
300
|
+
|
301
|
+
def list_and_filter_capsules(
|
302
|
+
api_url, perimeter, project, branch, name, tags, auth_type, capsule_id
|
303
|
+
):
|
304
|
+
capsules = Capsule.list(api_url, perimeter)
|
305
|
+
|
306
|
+
def _tags_match(tags, key, value):
|
307
|
+
for t in tags:
|
308
|
+
if t["key"] == key and t["value"] == value:
|
309
|
+
return True
|
310
|
+
return False
|
311
|
+
|
312
|
+
def _all_tags_match(tags, tags_to_match):
|
313
|
+
for t in tags_to_match:
|
314
|
+
if _tags_match(tags, t["key"], t["value"]):
|
315
|
+
return True
|
316
|
+
return False
|
317
|
+
|
318
|
+
def _filter_capsules(capsules, project, branch, name, tags, auth_type, capsule_id):
|
319
|
+
_filtered_capsules = []
|
320
|
+
for capsule in capsules:
|
321
|
+
set_tags = capsule.get("spec", {}).get("tags", [])
|
322
|
+
display_name = capsule.get("spec", {}).get("displayName", None)
|
323
|
+
set_id = capsule.get("id", None)
|
324
|
+
set_auth_type = (
|
325
|
+
capsule.get("spec", {}).get("authConfig", {}).get("authType", None)
|
326
|
+
)
|
327
|
+
|
328
|
+
if auth_type and set_auth_type != auth_type:
|
329
|
+
continue
|
330
|
+
if project and not _tags_match(set_tags, "project", project):
|
331
|
+
continue
|
332
|
+
if branch and not _tags_match(set_tags, "branch", branch):
|
333
|
+
continue
|
334
|
+
if name and display_name != name:
|
335
|
+
continue
|
336
|
+
if tags and not _all_tags_match(set_tags, tags):
|
337
|
+
continue
|
338
|
+
if capsule_id and set_id != capsule_id:
|
339
|
+
continue
|
340
|
+
|
341
|
+
_filtered_capsules.append(capsule)
|
342
|
+
return _filtered_capsules
|
343
|
+
|
344
|
+
return _filter_capsules(
|
345
|
+
capsules, project, branch, name, tags, auth_type, capsule_id
|
346
|
+
)
|
347
|
+
|
348
|
+
|
281
349
|
class Capsule:
|
282
350
|
|
283
351
|
status: CapsuleStateMachine
|
284
352
|
|
285
353
|
identifier = None
|
286
354
|
|
355
|
+
@classmethod
|
356
|
+
def list(cls, base_url: str, perimeter: str):
|
357
|
+
base_url = cls._create_base_url(base_url, perimeter)
|
358
|
+
from metaflow.metaflow_config import SERVICE_HEADERS
|
359
|
+
|
360
|
+
request_headers = {
|
361
|
+
**{"Content-Type": "application/json", "Connection": "keep-alive"},
|
362
|
+
**(SERVICE_HEADERS or {}),
|
363
|
+
}
|
364
|
+
_capsules = list_capsules(base_url, request_headers)
|
365
|
+
if "capsules" not in _capsules:
|
366
|
+
raise TODOException(f"Failed to list capsules")
|
367
|
+
return _capsules.get("capsules", [])
|
368
|
+
|
369
|
+
@classmethod
|
370
|
+
def delete(cls, identifier: str, base_url: str, perimeter: str):
|
371
|
+
base_url = cls._create_base_url(base_url, perimeter)
|
372
|
+
from metaflow.metaflow_config import SERVICE_HEADERS
|
373
|
+
|
374
|
+
request_headers = {
|
375
|
+
**{"Content-Type": "application/json", "Connection": "keep-alive"},
|
376
|
+
**(SERVICE_HEADERS or {}),
|
377
|
+
}
|
378
|
+
return delete_capsule(identifier, base_url, request_headers)
|
379
|
+
|
380
|
+
@classmethod
|
381
|
+
def _create_base_url(
|
382
|
+
cls,
|
383
|
+
base_url: str,
|
384
|
+
perimeter: str,
|
385
|
+
):
|
386
|
+
return os.path.join(
|
387
|
+
base_url,
|
388
|
+
"v1",
|
389
|
+
"perimeters",
|
390
|
+
perimeter,
|
391
|
+
"capsules",
|
392
|
+
)
|
393
|
+
|
287
394
|
# TODO: Current default timeout is very large of 5 minutes. Ideally we should have finished the deployed in less than 1 minutes.
|
288
395
|
def __init__(
|
289
396
|
self,
|
@@ -293,12 +400,9 @@ class Capsule:
|
|
293
400
|
debug_dir: Optional[str] = None,
|
294
401
|
):
|
295
402
|
self._app_config = app_config
|
296
|
-
self._base_url =
|
403
|
+
self._base_url = self._create_base_url(
|
297
404
|
base_url,
|
298
|
-
"v1",
|
299
|
-
"perimeters",
|
300
405
|
app_config.get_state("perimeter"),
|
301
|
-
"capsules",
|
302
406
|
)
|
303
407
|
self._create_timeout = create_timeout
|
304
408
|
self._debug_dir = debug_dir
|
@@ -365,9 +469,3 @@ class Capsule:
|
|
365
469
|
state_machine.check_for_debug(self._debug_dir)
|
366
470
|
|
367
471
|
return capsule_response
|
368
|
-
|
369
|
-
def list(self):
|
370
|
-
return list_capsules(self._base_url, self._request_headers)
|
371
|
-
|
372
|
-
def delete(self):
|
373
|
-
return delete_capsule(self.identifier, self._base_url, self._request_headers)
|
@@ -6,7 +6,7 @@ def build_config_from_options(options):
|
|
6
6
|
config = {}
|
7
7
|
|
8
8
|
# Set basic fields
|
9
|
-
for key in ["name", "port", "image", "compute_pools"]:
|
9
|
+
for key in ["name", "port", "image", "compute_pools", "description", "app_type"]:
|
10
10
|
if options.get(key):
|
11
11
|
config[key] = options[key]
|
12
12
|
|
@@ -33,11 +33,20 @@ properties:
|
|
33
33
|
example:
|
34
34
|
- foo: bar
|
35
35
|
- x: y
|
36
|
+
description: # Only used in `deploy` command
|
37
|
+
allow_union: true
|
38
|
+
type: string
|
39
|
+
description: The description of the app to deploy.
|
40
|
+
example: "This is a description of my app."
|
41
|
+
app_type: # Only used in `deploy` command
|
42
|
+
allow_union: true
|
43
|
+
type: string
|
44
|
+
description: The User defined type of app to deploy. Its only used for bookkeeping purposes.
|
45
|
+
example: "MyCustomAgent"
|
36
46
|
image: # Only used in `deploy` command
|
37
47
|
allow_union: true # We will overrwite the image if specified on the CLI.
|
38
48
|
type: string
|
39
49
|
description: The Docker image to deploy with the App.
|
40
|
-
example: "python:3.10-slim"
|
41
50
|
secrets: # Used in `run` command
|
42
51
|
allow_union: true
|
43
52
|
type: array
|
outerbounds/apps/utils.py
CHANGED
@@ -18,6 +18,31 @@ class MaximumRetriesExceeded(Exception):
|
|
18
18
|
return f"Maximum retries exceeded for {self.url}[{self.method}] {self.status_code} {self.text}"
|
19
19
|
|
20
20
|
|
21
|
+
class KeyValueDictPair(click.ParamType):
|
22
|
+
name = "KV-DICT-PAIR"
|
23
|
+
|
24
|
+
def convert(self, value, param, ctx):
|
25
|
+
# Parse a string of the form KEY=VALUE into a dict {KEY: VALUE}
|
26
|
+
if len(value.split("=", 1)) != 2:
|
27
|
+
self.fail(
|
28
|
+
f"Invalid format for {value}. Expected format: KEY=VALUE", param, ctx
|
29
|
+
)
|
30
|
+
|
31
|
+
key, _value = value.split("=", 1)
|
32
|
+
try:
|
33
|
+
return {"key": key, "value": json.loads(_value)}
|
34
|
+
except json.JSONDecodeError:
|
35
|
+
return {"key": key, "value": _value}
|
36
|
+
except Exception as e:
|
37
|
+
self.fail(f"Invalid value for {value}. Error: {e}", param, ctx)
|
38
|
+
|
39
|
+
def __str__(self):
|
40
|
+
return repr(self)
|
41
|
+
|
42
|
+
def __repr__(self):
|
43
|
+
return "KV-PAIR"
|
44
|
+
|
45
|
+
|
21
46
|
class KeyValuePair(click.ParamType):
|
22
47
|
name = "KV-PAIR"
|
23
48
|
|
@@ -155,6 +180,7 @@ KVPairType = KeyValuePair()
|
|
155
180
|
MetaflowArtifactType = MountMetaflowArtifact()
|
156
181
|
SecretMountType = MountSecret()
|
157
182
|
CommaSeparatedListType = CommaSeparatedList()
|
183
|
+
KVDictType = KeyValueDictPair()
|
158
184
|
|
159
185
|
|
160
186
|
class TODOException(Exception):
|
@@ -410,7 +410,7 @@ def kill_process(config_dir=None, profile=None, port=-1, name=""):
|
|
410
410
|
default=os.environ.get("METAFLOW_PROFILE", ""),
|
411
411
|
help="The named metaflow profile in which your workstation exists",
|
412
412
|
)
|
413
|
-
def
|
413
|
+
def list_local(config_dir=None, profile=None):
|
414
414
|
if "WORKSTATION_ID" not in os.environ:
|
415
415
|
click.secho(
|
416
416
|
"All outerbounds app commands can only be run from a workstation.",
|
@@ -7,6 +7,7 @@ from . import (
|
|
7
7
|
tutorials_cli,
|
8
8
|
fast_bakery_cli,
|
9
9
|
secrets_cli,
|
10
|
+
flowprojects_cli,
|
10
11
|
)
|
11
12
|
|
12
13
|
|
@@ -20,6 +21,7 @@ from . import (
|
|
20
21
|
tutorials_cli.cli,
|
21
22
|
fast_bakery_cli.cli,
|
22
23
|
secrets_cli.cli,
|
24
|
+
flowprojects_cli.cli,
|
23
25
|
],
|
24
26
|
)
|
25
27
|
def cli(**kwargs):
|
@@ -0,0 +1,137 @@
|
|
1
|
+
import json
|
2
|
+
import os
|
3
|
+
import sys
|
4
|
+
import requests
|
5
|
+
|
6
|
+
from ..utils import metaflowconfig
|
7
|
+
from outerbounds._vendor import click
|
8
|
+
|
9
|
+
|
10
|
+
@click.group()
|
11
|
+
def cli(**kwargs):
|
12
|
+
pass
|
13
|
+
|
14
|
+
|
15
|
+
@cli.group(help="Commands for pushing Deployments metadata.", hidden=True)
|
16
|
+
def flowproject(**kwargs):
|
17
|
+
pass
|
18
|
+
|
19
|
+
|
20
|
+
@flowproject.command()
|
21
|
+
@click.option(
|
22
|
+
"-d",
|
23
|
+
"--config-dir",
|
24
|
+
default=os.path.expanduser(os.environ.get("METAFLOW_HOME", "~/.metaflowconfig")),
|
25
|
+
help="Path to Metaflow configuration directory",
|
26
|
+
show_default=True,
|
27
|
+
)
|
28
|
+
@click.option(
|
29
|
+
"-p",
|
30
|
+
"--profile",
|
31
|
+
default=os.environ.get("METAFLOW_PROFILE", ""),
|
32
|
+
help="The named metaflow profile in which your workstation exists",
|
33
|
+
)
|
34
|
+
@click.option("--id", help="The ID for this deployment")
|
35
|
+
def get_metadata(config_dir, profile, id):
|
36
|
+
api_url = metaflowconfig.get_sanitized_url_from_config(
|
37
|
+
config_dir, profile, "OBP_API_SERVER"
|
38
|
+
)
|
39
|
+
perimeter = _get_perimeter()
|
40
|
+
headers = _get_request_headers()
|
41
|
+
|
42
|
+
project, branch = _parse_id(id)
|
43
|
+
|
44
|
+
# GET the latest flowproject config in order to modify it
|
45
|
+
# /v1/perimeters/:perimeter/:project/:branch/flowprojects/latest
|
46
|
+
response = requests.get(
|
47
|
+
url=f"{api_url}/v1/perimeters/{perimeter}/projects/{project}/branches/{branch}/latestflowproject",
|
48
|
+
headers=headers,
|
49
|
+
)
|
50
|
+
if response.status_code >= 500:
|
51
|
+
raise Exception("API request failed.")
|
52
|
+
|
53
|
+
body = response.json()
|
54
|
+
if response.status_code >= 400:
|
55
|
+
raise Exception("request failed: %s" % body)
|
56
|
+
|
57
|
+
out = json.dumps(body)
|
58
|
+
|
59
|
+
print(out, file=sys.stdout)
|
60
|
+
|
61
|
+
|
62
|
+
@flowproject.command()
|
63
|
+
@click.option(
|
64
|
+
"-d",
|
65
|
+
"--config-dir",
|
66
|
+
default=os.path.expanduser(os.environ.get("METAFLOW_HOME", "~/.metaflowconfig")),
|
67
|
+
help="Path to Metaflow configuration directory",
|
68
|
+
show_default=True,
|
69
|
+
)
|
70
|
+
@click.option(
|
71
|
+
"-p",
|
72
|
+
"--profile",
|
73
|
+
default=os.environ.get("METAFLOW_PROFILE", ""),
|
74
|
+
help="The named metaflow profile in which your workstation exists",
|
75
|
+
)
|
76
|
+
@click.argument("json_str")
|
77
|
+
def set_metadata(config_dir, profile, json_str):
|
78
|
+
api_url = metaflowconfig.get_sanitized_url_from_config(
|
79
|
+
config_dir, profile, "OBP_API_SERVER"
|
80
|
+
)
|
81
|
+
|
82
|
+
perimeter = _get_perimeter()
|
83
|
+
headers = _get_request_headers()
|
84
|
+
payload = json.loads(json_str)
|
85
|
+
|
86
|
+
# POST the updated flowproject config
|
87
|
+
# /v1/perimeters/:perimeter/flowprojects
|
88
|
+
response = requests.post(
|
89
|
+
url=f"{api_url}/v1/perimeters/{perimeter}/flowprojects",
|
90
|
+
json=payload,
|
91
|
+
headers=headers,
|
92
|
+
)
|
93
|
+
if response.status_code >= 500:
|
94
|
+
raise Exception("API request failed. %s" % response.text)
|
95
|
+
|
96
|
+
if response.status_code >= 400:
|
97
|
+
raise Exception("request failed: %s" % response.text)
|
98
|
+
body = response.json()
|
99
|
+
|
100
|
+
print(body, file=sys.stdout)
|
101
|
+
|
102
|
+
|
103
|
+
def _get_request_headers():
|
104
|
+
headers = {"Content-Type": "application/json", "Connection": "keep-alive"}
|
105
|
+
try:
|
106
|
+
from metaflow.metaflow_config import SERVICE_HEADERS
|
107
|
+
|
108
|
+
headers = {**headers, **(SERVICE_HEADERS or {})}
|
109
|
+
except ImportError:
|
110
|
+
headers = headers
|
111
|
+
|
112
|
+
return headers
|
113
|
+
|
114
|
+
|
115
|
+
def _get_perimeter():
|
116
|
+
# Get current perimeter
|
117
|
+
from metaflow_extensions.outerbounds.remote_config import init_config # type: ignore
|
118
|
+
|
119
|
+
conf = init_config()
|
120
|
+
if "OBP_PERIMETER" in conf:
|
121
|
+
perimeter = conf["OBP_PERIMETER"]
|
122
|
+
else:
|
123
|
+
# if the perimeter is not in metaflow config, try to get it from the environment
|
124
|
+
perimeter = os.environ.get("OBP_PERIMETER", None)
|
125
|
+
if perimeter is None:
|
126
|
+
raise Exception("Perimeter not found in config, but is required.")
|
127
|
+
|
128
|
+
return perimeter
|
129
|
+
|
130
|
+
|
131
|
+
def _parse_id(id: str):
|
132
|
+
parts = id.split("/")
|
133
|
+
if len(parts) != 2:
|
134
|
+
raise Exception("ID should consist of two parts: project/branch")
|
135
|
+
|
136
|
+
project, branch = parts
|
137
|
+
return project, branch
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: outerbounds
|
3
|
-
Version: 0.3.
|
3
|
+
Version: 0.3.176rc1
|
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.163rc1)
|
33
|
+
Requires-Dist: ob-metaflow-stubs (==6.0.3.176rc1)
|
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,26 +40,27 @@ 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=
|
43
|
+
outerbounds/apps/app_cli.py,sha256=kAKanVOVdBKI82ihAkRL2XcCLYyA-qkr_4FPS9xY-XI,23377
|
44
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=
|
47
|
-
outerbounds/apps/cli_to_config.py,sha256=
|
46
|
+
outerbounds/apps/capsule.py,sha256=JssY9i43p_7NZlW7VfP4rET3dQt7V-ZYZQmCbcZCZ3U,16650
|
47
|
+
outerbounds/apps/cli_to_config.py,sha256=hV6rfPgCiAX03O363GkvdjSIJBt3-oSbL6F2sTUucFE,3195
|
48
48
|
outerbounds/apps/code_package/__init__.py,sha256=8McF7pgx8ghvjRnazp2Qktlxi9yYwNiwESSQrk-2oW8,68
|
49
49
|
outerbounds/apps/code_package/code_packager.py,sha256=SQDBXKwizzpag5GpwoZpvvkyPOodRSQwk2ecAAfO0HI,23316
|
50
50
|
outerbounds/apps/code_package/examples.py,sha256=aF8qKIJxCVv_ugcShQjqUsXKKKMsm1oMkQIl8w3QKuw,4016
|
51
|
-
outerbounds/apps/config_schema.yaml,sha256=
|
51
|
+
outerbounds/apps/config_schema.yaml,sha256=bN7mXlVddqN8G4jq6qBpVXl_qFJdOYrmj8E4OY23Rr8,8641
|
52
52
|
outerbounds/apps/dependencies.py,sha256=SqvdFQdFZZW0wXX_CHMHCrfE0TwaRkTvGCRbQ2Mx3q0,3935
|
53
53
|
outerbounds/apps/deployer.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
54
54
|
outerbounds/apps/experimental/__init__.py,sha256=12L_FzZyzv162uo4I6cmlrxat7feUtIu_kxbObTJZTA,3059
|
55
55
|
outerbounds/apps/secrets.py,sha256=27qf04lOBqRjvcswj0ldHOmntP2T6SEjtMJtkJQ_GUg,6100
|
56
|
-
outerbounds/apps/utils.py,sha256=
|
56
|
+
outerbounds/apps/utils.py,sha256=xKaSJRmAYToyWvantixLES1vdi5vLLdoQ6yZXAv9IBw,8089
|
57
57
|
outerbounds/apps/validations.py,sha256=AVEw9eCvkzqq1m5ZC8btaWrSR6kWYKzarELfrASuAwQ,1117
|
58
58
|
outerbounds/cli_main.py,sha256=e9UMnPysmc7gbrimq2I4KfltggyU7pw59Cn9aEguVcU,74
|
59
59
|
outerbounds/command_groups/__init__.py,sha256=QPWtj5wDRTINDxVUL7XPqG3HoxHNvYOg08EnuSZB2Hc,21
|
60
|
-
outerbounds/command_groups/apps_cli.py,sha256=
|
61
|
-
outerbounds/command_groups/cli.py,sha256=
|
60
|
+
outerbounds/command_groups/apps_cli.py,sha256=ecXyLhGxjbct62iqviP9qBX8s4d-XG56ICpTM2h2etk,20821
|
61
|
+
outerbounds/command_groups/cli.py,sha256=I8b0zyY_xhZfmaLgJcgR98YXxgirkUK7xsjknEd4nfc,534
|
62
62
|
outerbounds/command_groups/fast_bakery_cli.py,sha256=5kja7v6C651XAY6dsP_IkBPJQgfU4hA4S9yTOiVPhW0,6213
|
63
|
+
outerbounds/command_groups/flowprojects_cli.py,sha256=gFAA_zUIyhD092Hd7IW5InuIxOqdwRJsHgyWQjy8LZw,3792
|
63
64
|
outerbounds/command_groups/local_setup_cli.py,sha256=tuuqJRXQ_guEwOuQSIf9wkUU0yg8yAs31myGViAK15s,36364
|
64
65
|
outerbounds/command_groups/perimeters_cli.py,sha256=iF_Uw7ROiSctf6FgoJEy30iDBLVE1j9FKuR3shgJRmc,19050
|
65
66
|
outerbounds/command_groups/secrets_cli.py,sha256=Vgn_aiTo76a0s5hCJhNWEOrCVhyYeivD08ooQxz0y7c,2952
|
@@ -71,7 +72,7 @@ outerbounds/utils/metaflowconfig.py,sha256=l2vJbgPkLISU-XPGZFaC8ZKmYFyJemlD6bwB-
|
|
71
72
|
outerbounds/utils/schema.py,sha256=lMUr9kNgn9wy-sO_t_Tlxmbt63yLeN4b0xQXbDUDj4A,2331
|
72
73
|
outerbounds/utils/utils.py,sha256=4Z8cszNob_8kDYCLNTrP-wWads_S_MdL3Uj3ju4mEsk,501
|
73
74
|
outerbounds/vendor.py,sha256=gRLRJNXtZBeUpPEog0LOeIsl6GosaFFbCxUvR4bW6IQ,5093
|
74
|
-
outerbounds-0.3.
|
75
|
-
outerbounds-0.3.
|
76
|
-
outerbounds-0.3.
|
77
|
-
outerbounds-0.3.
|
75
|
+
outerbounds-0.3.176rc1.dist-info/METADATA,sha256=fldMIpG3wOYVBJ1_q1trcVRlHP4HwndipT4Yo73ly_4,1846
|
76
|
+
outerbounds-0.3.176rc1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
77
|
+
outerbounds-0.3.176rc1.dist-info/entry_points.txt,sha256=7ye0281PKlvqxu15rjw60zKg2pMsXI49_A8BmGqIqBw,47
|
78
|
+
outerbounds-0.3.176rc1.dist-info/RECORD,,
|
File without changes
|
File without changes
|