outerbounds 0.3.89__py3-none-any.whl → 0.3.104__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.
@@ -0,0 +1,586 @@
1
+ import json
2
+ import os
3
+ from os import path
4
+ from outerbounds._vendor import click
5
+ import requests
6
+
7
+ from ..utils import metaflowconfig
8
+ from ..utils.schema import (
9
+ CommandStatus,
10
+ OuterboundsCommandResponse,
11
+ OuterboundsCommandStatus,
12
+ )
13
+
14
+
15
+ @click.group()
16
+ def cli(**kwargs):
17
+ pass
18
+
19
+
20
+ @click.group(help="Manage apps")
21
+ def app(**kwargs):
22
+ pass
23
+
24
+
25
+ @app.command(help="Start an app using a port and a name")
26
+ @click.option(
27
+ "-d",
28
+ "--config-dir",
29
+ default=path.expanduser(os.environ.get("METAFLOW_HOME", "~/.metaflowconfig")),
30
+ help="Path to Metaflow configuration directory",
31
+ show_default=True,
32
+ )
33
+ @click.option(
34
+ "-p",
35
+ "--profile",
36
+ default=os.environ.get("METAFLOW_PROFILE", ""),
37
+ help="The named metaflow profile in which your workstation exists",
38
+ )
39
+ @click.option(
40
+ "--port",
41
+ required=True,
42
+ help="Port number where you want to start your app",
43
+ type=int,
44
+ )
45
+ @click.option(
46
+ "--name",
47
+ required=True,
48
+ help="Name of your app",
49
+ type=str,
50
+ )
51
+ @click.option(
52
+ "-o",
53
+ "--output",
54
+ default="",
55
+ help="Show output in the specified format.",
56
+ type=click.Choice(["json", ""]),
57
+ )
58
+ def start(config_dir=None, profile=None, port=-1, name="", output=""):
59
+ if len(name) == 0 or len(name) >= 20:
60
+ click.secho(
61
+ "App name should not be more than 20 characters long.",
62
+ fg="red",
63
+ err=True,
64
+ )
65
+ return
66
+ elif not name.isalnum() or not name.islower():
67
+ click.secho(
68
+ "App name can only contain lowercase alphanumeric characters.",
69
+ fg="red",
70
+ err=True,
71
+ )
72
+ return
73
+
74
+ start_app_response = OuterboundsCommandResponse()
75
+
76
+ validate_workstation_step = CommandStatus(
77
+ "ValidateRunningOnWorkstation",
78
+ OuterboundsCommandStatus.OK,
79
+ "Command is being run on a workstation.",
80
+ )
81
+
82
+ list_workstations_step = CommandStatus(
83
+ "ListWorkstations",
84
+ OuterboundsCommandStatus.OK,
85
+ "List of workstations fetched.",
86
+ )
87
+
88
+ validate_request = CommandStatus(
89
+ "ValidateRequest",
90
+ OuterboundsCommandStatus.OK,
91
+ "Start app request is valid.",
92
+ )
93
+
94
+ start_app_step = CommandStatus(
95
+ "StartApp",
96
+ OuterboundsCommandStatus.OK,
97
+ f"App {name} started on port {port}!",
98
+ )
99
+
100
+ if "WORKSTATION_ID" not in os.environ:
101
+ validate_workstation_step.update(
102
+ OuterboundsCommandStatus.FAIL,
103
+ "All outerbounds app commands can only be run from a workstation.",
104
+ "",
105
+ )
106
+ start_app_response.add_step(validate_workstation_step)
107
+ click.secho(
108
+ "All outerbounds app commands can only be run from a workstation.",
109
+ fg="red",
110
+ err=True,
111
+ )
112
+
113
+ if output == "json":
114
+ click.echo(json.dumps(start_app_response.as_dict(), indent=4))
115
+ return
116
+
117
+ try:
118
+ try:
119
+ metaflow_token = metaflowconfig.get_metaflow_token_from_config(
120
+ config_dir, profile
121
+ )
122
+ api_url = metaflowconfig.get_sanitized_url_from_config(
123
+ config_dir, profile, "OBP_API_SERVER"
124
+ )
125
+
126
+ workstations_response = requests.get(
127
+ f"{api_url}/v1/workstations", headers={"x-api-key": metaflow_token}
128
+ )
129
+ workstations_response.raise_for_status()
130
+ start_app_response.add_step(list_workstations_step)
131
+ except:
132
+ click.secho("Failed to list workstations!", fg="red", err=True)
133
+ list_workstations_step.update(
134
+ OuterboundsCommandStatus.FAIL, "Failed to list workstations!", ""
135
+ )
136
+ start_app_response.add_step(list_workstations_step)
137
+ if output == "json":
138
+ click.echo(json.dumps(start_app_response.as_dict(), indent=4))
139
+ return
140
+
141
+ workstations_json = workstations_response.json()["workstations"]
142
+ for workstation in workstations_json:
143
+ if workstation["instance_id"] == os.environ["WORKSTATION_ID"]:
144
+ if "named_ports" in workstation["spec"]:
145
+ try:
146
+ ensure_app_start_request_is_valid(
147
+ workstation["spec"]["named_ports"], port, name
148
+ )
149
+ except ValueError as e:
150
+ click.secho(str(e), fg="red", err=True)
151
+ validate_request.update(
152
+ OuterboundsCommandStatus.FAIL,
153
+ str(e),
154
+ "",
155
+ )
156
+ start_app_response.add_step(validate_request)
157
+ if output == "json":
158
+ click.echo(
159
+ json.dumps(start_app_response.as_dict(), indent=4)
160
+ )
161
+ return
162
+
163
+ start_app_response.add_step(validate_request)
164
+
165
+ for named_port in workstation["spec"]["named_ports"]:
166
+ if int(named_port["port"]) == port:
167
+ if named_port["enabled"] and named_port["name"] == name:
168
+ click.secho(
169
+ f"App {name} already running on port {port}!",
170
+ fg="green",
171
+ err=True,
172
+ )
173
+ click.secho(
174
+ f"App URL: {api_url.replace('api', 'ui')}/apps/{os.environ['WORKSTATION_ID']}/{name}/",
175
+ fg="green",
176
+ err=True,
177
+ )
178
+ start_app_response.add_step(start_app_step)
179
+ if output == "json":
180
+ click.echo(
181
+ json.dumps(
182
+ start_app_response.as_dict(), indent=4
183
+ )
184
+ )
185
+ return
186
+ else:
187
+ try:
188
+ response = requests.put(
189
+ f"{api_url}/v1/workstations/update/{os.environ['WORKSTATION_ID']}/namedports",
190
+ headers={"x-api-key": metaflow_token},
191
+ json={
192
+ "port": port,
193
+ "name": name,
194
+ "enabled": True,
195
+ },
196
+ )
197
+
198
+ response.raise_for_status()
199
+ click.secho(
200
+ f"App {name} started on port {port}!",
201
+ fg="green",
202
+ err=True,
203
+ )
204
+ click.secho(
205
+ f"App URL: {api_url.replace('api', 'ui')}/apps/{os.environ['WORKSTATION_ID']}/{name}/",
206
+ fg="green",
207
+ err=True,
208
+ )
209
+ except Exception:
210
+ click.secho(
211
+ f"Failed to start app {name} on port {port}!",
212
+ fg="red",
213
+ err=True,
214
+ )
215
+ start_app_step.update(
216
+ OuterboundsCommandStatus.FAIL,
217
+ f"Failed to start app {name} on port {port}!",
218
+ "",
219
+ )
220
+
221
+ start_app_response.add_step(start_app_step)
222
+ if output == "json":
223
+ click.echo(
224
+ json.dumps(
225
+ start_app_response.as_dict(), indent=4
226
+ )
227
+ )
228
+ return
229
+ except Exception as e:
230
+ click.secho(f"Failed to start app {name} on port {port}!", fg="red", err=True)
231
+ start_app_step.update(
232
+ OuterboundsCommandStatus.FAIL,
233
+ f"Failed to start app {name} on port {port}!",
234
+ "",
235
+ )
236
+ start_app_response.add_step(start_app_step)
237
+ if output == "json":
238
+ click.secho(json.dumps(start_app_response.as_dict(), indent=4))
239
+
240
+
241
+ @app.command(help="Stop an app using its port number")
242
+ @click.option(
243
+ "-d",
244
+ "--config-dir",
245
+ default=path.expanduser(os.environ.get("METAFLOW_HOME", "~/.metaflowconfig")),
246
+ help="Path to Metaflow configuration directory",
247
+ show_default=True,
248
+ )
249
+ @click.option(
250
+ "-p",
251
+ "--profile",
252
+ default=os.environ.get("METAFLOW_PROFILE", ""),
253
+ help="The named metaflow profile in which your workstation exists",
254
+ )
255
+ @click.option(
256
+ "--port",
257
+ required=False,
258
+ default=-1,
259
+ help="Port number where you want to start your app.",
260
+ type=int,
261
+ )
262
+ @click.option(
263
+ "--name",
264
+ required=False,
265
+ help="Name of your app",
266
+ default="",
267
+ type=str,
268
+ )
269
+ @click.option(
270
+ "-o",
271
+ "--output",
272
+ default="",
273
+ help="Show output in the specified format.",
274
+ type=click.Choice(["json", ""]),
275
+ )
276
+ def stop(config_dir=None, profile=None, port=-1, name="", output=""):
277
+ if port == -1 and not name:
278
+ click.secho(
279
+ "Please provide either a port number or a name to stop the app.",
280
+ fg="red",
281
+ err=True,
282
+ )
283
+ return
284
+ elif port > 0 and name:
285
+ click.secho(
286
+ "Please provide either a port number or a name to stop the app, not both.",
287
+ fg="red",
288
+ err=True,
289
+ )
290
+ return
291
+
292
+ stop_app_response = OuterboundsCommandResponse()
293
+
294
+ validate_workstation_step = CommandStatus(
295
+ "ValidateRunningOnWorkstation",
296
+ OuterboundsCommandStatus.OK,
297
+ "Command is being run on a workstation.",
298
+ )
299
+
300
+ list_workstations_step = CommandStatus(
301
+ "ListWorkstations",
302
+ OuterboundsCommandStatus.OK,
303
+ "List of workstations fetched.",
304
+ )
305
+
306
+ validate_port_exists = CommandStatus(
307
+ "ValidatePortExists",
308
+ OuterboundsCommandStatus.OK,
309
+ "Port exists on workstation",
310
+ )
311
+
312
+ stop_app_step = CommandStatus(
313
+ "StopApp",
314
+ OuterboundsCommandStatus.OK,
315
+ f"App stopped on port {port}!",
316
+ )
317
+
318
+ if "WORKSTATION_ID" not in os.environ:
319
+ validate_workstation_step.update(
320
+ OuterboundsCommandStatus.FAIL,
321
+ "All outerbounds app commands can only be run from a workstation.",
322
+ "",
323
+ )
324
+ stop_app_response.add_step(validate_workstation_step)
325
+ click.secho(
326
+ "All outerbounds app commands can only be run from a workstation.",
327
+ fg="red",
328
+ err=True,
329
+ )
330
+
331
+ if output == "json":
332
+ click.echo(json.dumps(stop_app_response.as_dict(), indent=4))
333
+ return
334
+
335
+ try:
336
+ try:
337
+ metaflow_token = metaflowconfig.get_metaflow_token_from_config(
338
+ config_dir, profile
339
+ )
340
+ api_url = metaflowconfig.get_sanitized_url_from_config(
341
+ config_dir, profile, "OBP_API_SERVER"
342
+ )
343
+
344
+ workstations_response = requests.get(
345
+ f"{api_url}/v1/workstations", headers={"x-api-key": metaflow_token}
346
+ )
347
+ workstations_response.raise_for_status()
348
+ stop_app_response.add_step(list_workstations_step)
349
+ except:
350
+ click.secho("Failed to list workstations!", fg="red", err=True)
351
+ list_workstations_step.update(
352
+ OuterboundsCommandStatus.FAIL, "Failed to list workstations!", ""
353
+ )
354
+ stop_app_response.add_step(list_workstations_step)
355
+ if output == "json":
356
+ click.echo(json.dumps(stop_app_response.as_dict(), indent=4))
357
+ return
358
+
359
+ app_found = False
360
+ workstations_json = workstations_response.json()["workstations"]
361
+ for workstation in workstations_json:
362
+ if workstation["instance_id"] == os.environ["WORKSTATION_ID"]:
363
+ if "named_ports" in workstation["spec"]:
364
+ for named_port in workstation["spec"]["named_ports"]:
365
+ if (
366
+ int(named_port["port"]) == port
367
+ or named_port["name"] == name
368
+ ):
369
+ app_found = True
370
+ stop_app_response.add_step(validate_port_exists)
371
+ if named_port["enabled"]:
372
+ try:
373
+ response = requests.put(
374
+ f"{api_url}/v1/workstations/update/{os.environ['WORKSTATION_ID']}/namedports",
375
+ headers={"x-api-key": metaflow_token},
376
+ json={
377
+ "port": named_port["port"],
378
+ "name": named_port["name"],
379
+ "enabled": False,
380
+ },
381
+ )
382
+ response.raise_for_status()
383
+ click.secho(
384
+ f"App {named_port['name']} stopped on port {named_port['port']}!",
385
+ fg="green",
386
+ err=True,
387
+ )
388
+ except Exception as e:
389
+ click.secho(
390
+ f"Failed to stop app {named_port['name']} on port {named_port['port']}!",
391
+ fg="red",
392
+ err=True,
393
+ )
394
+ stop_app_step.update(
395
+ OuterboundsCommandStatus.FAIL,
396
+ f"Failed to stop app {named_port['name']} on port {named_port['port']}!",
397
+ "",
398
+ )
399
+
400
+ stop_app_response.add_step(stop_app_step)
401
+ if output == "json":
402
+ click.echo(
403
+ json.dumps(
404
+ stop_app_response.as_dict(), indent=4
405
+ )
406
+ )
407
+ return
408
+
409
+ if app_found:
410
+ already_stopped_message = (
411
+ f"No deployed app named {name} found."
412
+ if name
413
+ else f"There is no app deployed on port {port}"
414
+ )
415
+ click.secho(
416
+ already_stopped_message,
417
+ fg="green",
418
+ err=True,
419
+ )
420
+ stop_app_response.add_step(stop_app_step)
421
+ if output == "json":
422
+ click.echo(json.dumps(stop_app_response.as_dict(), indent=4))
423
+ return
424
+
425
+ err_message = (
426
+ (f"Port {port} not found on workstation {os.environ['WORKSTATION_ID']}")
427
+ if port != -1
428
+ else f"App {name} not found on workstation {os.environ['WORKSTATION_ID']}"
429
+ )
430
+
431
+ click.secho(
432
+ err_message,
433
+ fg="red",
434
+ err=True,
435
+ )
436
+
437
+ validate_port_exists.update(
438
+ OuterboundsCommandStatus.FAIL,
439
+ err_message,
440
+ "",
441
+ )
442
+ stop_app_response.add_step(validate_port_exists)
443
+ if output == "json":
444
+ click.echo(json.dumps(stop_app_response.as_dict(), indent=4))
445
+ except Exception as e:
446
+ click.secho(f"Failed to stop app on port {port}!", fg="red", err=True)
447
+ stop_app_step.update(
448
+ OuterboundsCommandStatus.FAIL, f"Failed to stop on port {port}!", ""
449
+ )
450
+ stop_app_response.add_step(stop_app_step)
451
+ if output == "json":
452
+ click.echo(json.dumps(stop_app_response.as_dict(), indent=4))
453
+
454
+
455
+ @app.command(help="Stop an app using its port number")
456
+ @click.option(
457
+ "-d",
458
+ "--config-dir",
459
+ default=path.expanduser(os.environ.get("METAFLOW_HOME", "~/.metaflowconfig")),
460
+ help="Path to Metaflow configuration directory",
461
+ show_default=True,
462
+ )
463
+ @click.option(
464
+ "-p",
465
+ "--profile",
466
+ default=os.environ.get("METAFLOW_PROFILE", ""),
467
+ help="The named metaflow profile in which your workstation exists",
468
+ )
469
+ @click.option(
470
+ "-o",
471
+ "--output",
472
+ default="",
473
+ help="Show output in the specified format.",
474
+ type=click.Choice(["json", ""]),
475
+ )
476
+ def list(config_dir=None, profile=None, output=""):
477
+ list_app_response = OuterboundsCommandResponse()
478
+
479
+ validate_workstation_step = CommandStatus(
480
+ "ValidateRunningOnWorkstation",
481
+ OuterboundsCommandStatus.OK,
482
+ "Command is being run on a workstation.",
483
+ )
484
+
485
+ list_workstations_step = CommandStatus(
486
+ "ListWorkstations",
487
+ OuterboundsCommandStatus.OK,
488
+ "List of workstations fetched.",
489
+ )
490
+
491
+ if "WORKSTATION_ID" not in os.environ:
492
+ validate_workstation_step.update(
493
+ OuterboundsCommandStatus.FAIL,
494
+ "All outerbounds app commands can only be run from a workstation.",
495
+ "",
496
+ )
497
+ list_app_response.add_step(validate_workstation_step)
498
+ click.secho(
499
+ "All outerbounds app commands can only be run from a workstation.",
500
+ fg="red",
501
+ err=True,
502
+ )
503
+
504
+ if output == "json":
505
+ click.echo(json.dumps(list_app_response.as_dict(), indent=4))
506
+ return
507
+
508
+ try:
509
+ try:
510
+ metaflow_token = metaflowconfig.get_metaflow_token_from_config(
511
+ config_dir, profile
512
+ )
513
+ api_url = metaflowconfig.get_sanitized_url_from_config(
514
+ config_dir, profile, "OBP_API_SERVER"
515
+ )
516
+
517
+ workstations_response = requests.get(
518
+ f"{api_url}/v1/workstations", headers={"x-api-key": metaflow_token}
519
+ )
520
+ workstations_response.raise_for_status()
521
+ list_app_response.add_step(list_workstations_step)
522
+ except:
523
+ click.secho("Failed to list workstations!", fg="red", err=True)
524
+ list_workstations_step.update(
525
+ OuterboundsCommandStatus.FAIL, "Failed to list workstations!", ""
526
+ )
527
+ list_app_response.add_step(list_workstations_step)
528
+ if output == "json":
529
+ click.echo(json.dumps(list_app_response.as_dict(), indent=4))
530
+ return
531
+
532
+ workstations_json = workstations_response.json()["workstations"]
533
+ for workstation in workstations_json:
534
+ if workstation["instance_id"] == os.environ["WORKSTATION_ID"]:
535
+ if "named_ports" in workstation["spec"]:
536
+ for named_port in workstation["spec"]["named_ports"]:
537
+ if named_port["enabled"]:
538
+ click.secho(
539
+ f"App Name: {named_port['name']}", fg="green", err=True
540
+ )
541
+ click.secho(
542
+ f"App Port on Workstation: {named_port['port']}",
543
+ fg="green",
544
+ err=True,
545
+ )
546
+ click.secho(f"App Status: Deployed", fg="green", err=True)
547
+ click.secho(
548
+ f"App URL: {api_url.replace('api', 'ui')}/apps/{os.environ['WORKSTATION_ID']}/{named_port['name']}/",
549
+ fg="green",
550
+ err=True,
551
+ )
552
+ else:
553
+ click.secho(
554
+ f"App Port on Workstation: {named_port['port']}",
555
+ fg="yellow",
556
+ err=True,
557
+ )
558
+ click.secho(
559
+ f"App Status: Not Deployed", fg="yellow", err=True
560
+ )
561
+
562
+ click.echo("\n", err=True)
563
+ except Exception as e:
564
+ click.secho(f"Failed to list apps!", fg="red", err=True)
565
+ if output == "json":
566
+ click.echo(json.dumps(list_app_response.as_dict(), indent=4))
567
+
568
+
569
+ def ensure_app_start_request_is_valid(existing_named_ports, port: int, name: str):
570
+ existing_apps_by_port = {np["port"]: np for np in existing_named_ports}
571
+
572
+ if port not in existing_apps_by_port:
573
+ raise ValueError(f"Port {port} not found on workstation")
574
+
575
+ for existing_named_port in existing_named_ports:
576
+ if (
577
+ name == existing_named_port["name"]
578
+ and existing_named_port["port"] != port
579
+ and existing_named_port["enabled"]
580
+ ):
581
+ raise ValueError(
582
+ f"App with name '{name}' is already deployed on port {existing_named_port['port']}"
583
+ )
584
+
585
+
586
+ cli.add_command(app, name="app")
@@ -1,12 +1,16 @@
1
1
  from outerbounds._vendor import click
2
- from . import local_setup_cli
3
- from . import workstations_cli
4
- from . import perimeters_cli
2
+ from . import local_setup_cli, workstations_cli, perimeters_cli, apps_cli, tutorials_cli
5
3
 
6
4
 
7
5
  @click.command(
8
6
  cls=click.CommandCollection,
9
- sources=[local_setup_cli.cli, workstations_cli.cli, perimeters_cli.cli],
7
+ sources=[
8
+ local_setup_cli.cli,
9
+ workstations_cli.cli,
10
+ perimeters_cli.cli,
11
+ apps_cli.cli,
12
+ tutorials_cli.cli,
13
+ ],
10
14
  )
11
15
  def cli(**kwargs):
12
16
  pass
@@ -1,10 +1,12 @@
1
1
  import json
2
2
  import os
3
3
  import sys
4
+ from io import StringIO
4
5
  from os import path
5
6
  from typing import Any, Dict
6
7
  from outerbounds._vendor import click
7
8
  import requests
9
+ import configparser
8
10
 
9
11
  from ..utils import metaflowconfig
10
12
  from ..utils.utils import safe_write_to_disk
@@ -440,6 +442,9 @@ def ensure_cloud_credentials_for_shell(config_dir, profile):
440
442
  if "METAFLOW_DEFAULT_GCP_CLIENT_PROVIDER" in mf_config:
441
443
  # This is a GCP deployment.
442
444
  ensure_gcp_cloud_creds(config_dir, profile)
445
+ elif "METAFLOW_DEFAULT_AWS_CLIENT_PROVIDER" in mf_config:
446
+ # This is an AWS deployment.
447
+ ensure_aws_cloud_creds(config_dir, profile)
443
448
 
444
449
 
445
450
  def confirm_user_has_access_to_perimeter_or_fail(
@@ -473,8 +478,13 @@ def ensure_gcp_cloud_creds(config_dir, profile):
473
478
  )
474
479
  metaflow_token = metaflowconfig.get_metaflow_token_from_config(config_dir, profile)
475
480
 
476
- # GOOGLE_APPLICATION_CREDENTIALS is a well known gcloud environment variable
477
- credentials_file_loc = os.environ["GOOGLE_APPLICATION_CREDENTIALS"]
481
+ try:
482
+ # GOOGLE_APPLICATION_CREDENTIALS is a well known gcloud environment variable
483
+ credentials_file_loc = os.environ["GOOGLE_APPLICATION_CREDENTIALS"]
484
+ except KeyError:
485
+ # This is most likely an old workstation when these params weren't set. Do nothing.
486
+ # Alternatively, user might have deliberately unset it to use their own auth.
487
+ return
478
488
 
479
489
  credentials_json = {
480
490
  "type": "external_account",
@@ -492,6 +502,59 @@ def ensure_gcp_cloud_creds(config_dir, profile):
492
502
  safe_write_to_disk(credentials_file_loc, json.dumps(credentials_json))
493
503
 
494
504
 
505
+ def ensure_aws_cloud_creds(config_dir, profile):
506
+ token_info = get_aws_auth_credentials(config_dir, profile)
507
+
508
+ try:
509
+ token_file_loc = os.environ["OBP_AWS_WEB_IDENTITY_TOKEN_FILE"]
510
+
511
+ # AWS_CONFIG_FILE is a well known aws cli environment variable
512
+ config_file_loc = os.environ["AWS_CONFIG_FILE"]
513
+ except KeyError:
514
+ # This is most likely an old workstation when these params weren't set. Do nothing.
515
+ # Alternatively, user might have deliberately unset it to use their own auth.
516
+ return
517
+
518
+ aws_config = configparser.ConfigParser()
519
+ aws_config.read(config_file_loc)
520
+
521
+ aws_config["profile task"] = {
522
+ "role_arn": token_info["role_arn"],
523
+ "web_identity_token_file": token_file_loc,
524
+ }
525
+
526
+ if token_info.get("cspr_role_arn"):
527
+ # If CSPR role is present, then we need to use the task role (in the task profile)
528
+ # to assume the CSPR role.
529
+ aws_config["profile outerbounds"] = {
530
+ "role_arn": token_info["cspr_role_arn"],
531
+ "source_profile": "task",
532
+ }
533
+ else:
534
+ # If no CSPR role is present, just use the task profile as the outerbounds profile.
535
+ aws_config["profile outerbounds"] = aws_config["profile task"]
536
+
537
+ aws_config_string = StringIO()
538
+ aws_config.write(aws_config_string)
539
+
540
+ safe_write_to_disk(token_file_loc, token_info["token"])
541
+ safe_write_to_disk(config_file_loc, aws_config_string.getvalue())
542
+
543
+
544
+ def get_aws_auth_credentials(config_dir, profile): # pragma: no cover
545
+ token = metaflowconfig.get_metaflow_token_from_config(config_dir, profile)
546
+ auth_server_url = metaflowconfig.get_sanitized_url_from_config(
547
+ config_dir, profile, "OBP_AUTH_SERVER"
548
+ )
549
+
550
+ response = requests.get(
551
+ "{}/generate/aws".format(auth_server_url), headers={"x-api-key": token}
552
+ )
553
+ response.raise_for_status()
554
+
555
+ return response.json()
556
+
557
+
495
558
  def get_gcp_auth_credentials(config_dir, profile):
496
559
  token = metaflowconfig.get_metaflow_token_from_config(config_dir, profile)
497
560
  auth_server_url = metaflowconfig.get_sanitized_url_from_config(
@@ -0,0 +1,111 @@
1
+ import os
2
+ from outerbounds._vendor import click
3
+ import requests
4
+
5
+ import tarfile
6
+ import hashlib
7
+ import tempfile
8
+
9
+
10
+ @click.group()
11
+ def cli(**kwargs):
12
+ pass
13
+
14
+
15
+ @click.group(help="Manage tutorials curated by Outerbounds.", hidden=True)
16
+ def tutorials(**kwargs):
17
+ pass
18
+
19
+
20
+ @tutorials.command(help="Pull Outerbounds tutorials.")
21
+ @click.option(
22
+ "--url",
23
+ required=True,
24
+ help="URL to pull the tutorials from.",
25
+ type=str,
26
+ )
27
+ @click.option(
28
+ "--destination-dir",
29
+ help="Show output in the specified format.",
30
+ type=str,
31
+ required=True,
32
+ )
33
+ @click.option(
34
+ "--force-overwrite",
35
+ is_flag=True,
36
+ help="Overwrite all existing files across all tutorials.",
37
+ type=bool,
38
+ required=False,
39
+ default=False,
40
+ )
41
+ def pull(url="", destination_dir="", force_overwrite=False):
42
+ try:
43
+ secure_download_and_extract(
44
+ url, destination_dir, force_overwrite=force_overwrite
45
+ )
46
+ click.secho("Tutorials pulled successfully.", fg="green", err=True)
47
+ except Exception as e:
48
+ print(e)
49
+ click.secho(f"Failed to pull tutorials: {e}", fg="red", err=True)
50
+
51
+
52
+ def secure_download_and_extract(
53
+ url, dest_dir, expected_hash=None, force_overwrite=False
54
+ ):
55
+ """
56
+ Download a tar.gz file from a URL, verify its integrity, and extract its contents.
57
+
58
+ :param url: URL of the tar.gz file to download
59
+ :param dest_dir: Destination directory to extract the contents
60
+ :param expected_hash: Expected SHA256 hash of the file (optional)
61
+ """
62
+
63
+ with tempfile.TemporaryDirectory() as temp_dir:
64
+ temp_file = os.path.join(
65
+ temp_dir, hashlib.md5(url.encode()).hexdigest() + ".tar.gz"
66
+ )
67
+
68
+ # Download the file
69
+ try:
70
+ response = requests.get(url, stream=True, verify=True)
71
+ response.raise_for_status()
72
+
73
+ with open(temp_file, "wb") as f:
74
+ for chunk in response.iter_content(chunk_size=8192):
75
+ f.write(chunk)
76
+ except requests.exceptions.RequestException as e:
77
+ raise Exception(f"Failed to download file: {e}")
78
+
79
+ if expected_hash:
80
+ with open(temp_file, "rb") as f:
81
+ file_hash = hashlib.sha256(f.read()).hexdigest()
82
+ if file_hash != expected_hash:
83
+ raise Exception("File integrity check failed")
84
+
85
+ os.makedirs(dest_dir, exist_ok=True)
86
+
87
+ try:
88
+ with tarfile.open(temp_file, "r:gz") as tar:
89
+ # Keep track of new journeys to extract.
90
+ to_extract = []
91
+ members = tar.getmembers()
92
+ for member in members:
93
+ member_path = os.path.join(dest_dir, member.name)
94
+ # Check for any files trying to write outside the destination
95
+ if not os.path.abspath(member_path).startswith(
96
+ os.path.abspath(dest_dir)
97
+ ):
98
+ raise Exception("Attempted path traversal in tar file")
99
+ if not os.path.exists(member_path):
100
+ # The user might have modified the existing files, leave them untouched.
101
+ to_extract.append(member)
102
+
103
+ if force_overwrite:
104
+ tar.extractall(path=dest_dir)
105
+ else:
106
+ tar.extractall(path=dest_dir, members=to_extract)
107
+ except tarfile.TarError as e:
108
+ raise Exception(f"Failed to extract tar file: {e}")
109
+
110
+
111
+ cli.add_command(tutorials, name="tutorials")
@@ -59,14 +59,14 @@ class OuterboundsCommandResponse:
59
59
  if step.status == OuterboundsCommandStatus.FAIL:
60
60
  self.status = OuterboundsCommandStatus.FAIL
61
61
  self._code = 500
62
- self._message = "We found one or more errors with your installation."
62
+ self._message = "Encountered an error when trying to run command."
63
63
  elif (
64
64
  step.status == OuterboundsCommandStatus.WARN
65
65
  and self.status != OuterboundsCommandStatus.FAIL
66
66
  ):
67
67
  self.status = OuterboundsCommandStatus.WARN
68
68
  self._code = 200
69
- self._message = "We found one or more warnings with your installation."
69
+ self._message = "Encountered one or more warnings when running the command."
70
70
 
71
71
  def as_dict(self):
72
72
  self._data["steps"] = [step.as_dict() for step in self._steps]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: outerbounds
3
- Version: 0.3.89
3
+ Version: 0.3.104
4
4
  Summary: More Data Science, Less Administration
5
5
  License: Proprietary
6
6
  Keywords: data science,machine learning,MLOps
@@ -14,6 +14,7 @@ Classifier: Programming Language :: Python :: 3.8
14
14
  Classifier: Programming Language :: Python :: 3.9
15
15
  Classifier: Programming Language :: Python :: 3.10
16
16
  Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
17
18
  Provides-Extra: azure
18
19
  Provides-Extra: gcp
19
20
  Requires-Dist: azure-identity (>=1.15.0,<2.0.0) ; extra == "azure"
@@ -24,9 +25,9 @@ Requires-Dist: google-api-core (>=2.16.1,<3.0.0) ; extra == "gcp"
24
25
  Requires-Dist: google-auth (>=2.27.0,<3.0.0) ; extra == "gcp"
25
26
  Requires-Dist: google-cloud-secret-manager (>=2.20.0,<3.0.0) ; extra == "gcp"
26
27
  Requires-Dist: google-cloud-storage (>=2.14.0,<3.0.0) ; extra == "gcp"
27
- Requires-Dist: ob-metaflow (==2.12.12.0)
28
- Requires-Dist: ob-metaflow-extensions (==1.1.79)
29
- Requires-Dist: ob-metaflow-stubs (==5.2)
28
+ Requires-Dist: metaflow-checkpoint (==0.0.11)
29
+ Requires-Dist: ob-metaflow (==2.12.22.1)
30
+ Requires-Dist: ob-metaflow-extensions (==1.1.94)
30
31
  Requires-Dist: opentelemetry-distro (==0.41b0)
31
32
  Requires-Dist: opentelemetry-exporter-otlp-proto-http (==1.20.0)
32
33
  Requires-Dist: opentelemetry-instrumentation-requests (==0.41b0)
@@ -41,17 +41,19 @@ outerbounds/_vendor/yaml/serializer.py,sha256=8wFZRy9SsQSktF_f9OOroroqsh4qVUe53r
41
41
  outerbounds/_vendor/yaml/tokens.py,sha256=JBSu38wihGr4l73JwbfMA7Ks1-X84g8-NskTz7KwPmA,2578
42
42
  outerbounds/cli_main.py,sha256=e9UMnPysmc7gbrimq2I4KfltggyU7pw59Cn9aEguVcU,74
43
43
  outerbounds/command_groups/__init__.py,sha256=QPWtj5wDRTINDxVUL7XPqG3HoxHNvYOg08EnuSZB2Hc,21
44
- outerbounds/command_groups/cli.py,sha256=sorDdQvmTPqIwfvgtuNLILelimXu5CknFnWQFsYFGHs,286
44
+ outerbounds/command_groups/apps_cli.py,sha256=iXaLnO-FwU_zK2ZjE-gBu1ZQdOYDLCbT0HJXJJZckeE,21895
45
+ outerbounds/command_groups/cli.py,sha256=q0hdJO4biD3iEOdyJcxnRkeleA8AKAhx842kQ49I6kk,365
45
46
  outerbounds/command_groups/local_setup_cli.py,sha256=tuuqJRXQ_guEwOuQSIf9wkUU0yg8yAs31myGViAK15s,36364
46
- outerbounds/command_groups/perimeters_cli.py,sha256=IFSQC7nwnPksryv4jLMf--orkCWESxTJUx0o_wcperk,16724
47
+ outerbounds/command_groups/perimeters_cli.py,sha256=iF_Uw7ROiSctf6FgoJEy30iDBLVE1j9FKuR3shgJRmc,19050
48
+ outerbounds/command_groups/tutorials_cli.py,sha256=UInFyiMqtscHFfi8YQwiY_6Sdw9quJOtRu5OukEBccw,3522
47
49
  outerbounds/command_groups/workstations_cli.py,sha256=V5Jbj1cVb4IRllI7fOgNgL6OekRpuFDv6CEhDb4xC6w,22016
48
50
  outerbounds/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
49
51
  outerbounds/utils/kubeconfig.py,sha256=yvcyRXGR4AhQuqUDqmbGxEOHw5ixMFV0AZIDg1LI_Qo,7981
50
52
  outerbounds/utils/metaflowconfig.py,sha256=l2vJbgPkLISU-XPGZFaC8ZKmYFyJemlD6bwB-EKUsAw,5770
51
- outerbounds/utils/schema.py,sha256=cNlgjmteLPbDzSEUSQDsq8txdhMGyezSmM83jU3aa0w,2329
53
+ outerbounds/utils/schema.py,sha256=lMUr9kNgn9wy-sO_t_Tlxmbt63yLeN4b0xQXbDUDj4A,2331
52
54
  outerbounds/utils/utils.py,sha256=4Z8cszNob_8kDYCLNTrP-wWads_S_MdL3Uj3ju4mEsk,501
53
55
  outerbounds/vendor.py,sha256=gRLRJNXtZBeUpPEog0LOeIsl6GosaFFbCxUvR4bW6IQ,5093
54
- outerbounds-0.3.89.dist-info/entry_points.txt,sha256=7ye0281PKlvqxu15rjw60zKg2pMsXI49_A8BmGqIqBw,47
55
- outerbounds-0.3.89.dist-info/WHEEL,sha256=vVCvjcmxuUltf8cYhJ0sJMRDLr1XsPuxEId8YDzbyCY,88
56
- outerbounds-0.3.89.dist-info/METADATA,sha256=EbJIp_Ibm7F8sO2SfCcZOnOi61wWO2RUQnnWTwDZV38,1632
57
- outerbounds-0.3.89.dist-info/RECORD,,
56
+ outerbounds-0.3.104.dist-info/METADATA,sha256=LzoQnFF2DDg_kFpGRz-tWE_EtrdHm2im057o1e8ApI4,1689
57
+ outerbounds-0.3.104.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
58
+ outerbounds-0.3.104.dist-info/entry_points.txt,sha256=7ye0281PKlvqxu15rjw60zKg2pMsXI49_A8BmGqIqBw,47
59
+ outerbounds-0.3.104.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.4.0
2
+ Generator: poetry-core 1.9.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any