outerbounds 0.3.89__py3-none-any.whl → 0.3.104__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -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