outerbounds 0.3.92__py3-none-any.whl → 0.3.93__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -17,7 +17,7 @@ def cli(**kwargs):
17
17
  pass
18
18
 
19
19
 
20
- @click.group(help="Manage perimeters")
20
+ @click.group(help="Manage apps")
21
21
  def app(**kwargs):
22
22
  pass
23
23
 
@@ -229,10 +229,18 @@ def start(config_dir=None, profile=None, port=-1, name="", output=""):
229
229
  )
230
230
  @click.option(
231
231
  "--port",
232
- required=True,
232
+ required=False,
233
+ default=-1,
233
234
  help="Port number where you want to start your app.",
234
235
  type=int,
235
236
  )
237
+ @click.option(
238
+ "--name",
239
+ required=False,
240
+ help="Name of your app",
241
+ default="",
242
+ type=str,
243
+ )
236
244
  @click.option(
237
245
  "-o",
238
246
  "--output",
@@ -240,7 +248,15 @@ def start(config_dir=None, profile=None, port=-1, name="", output=""):
240
248
  help="Show output in the specified format.",
241
249
  type=click.Choice(["json", ""]),
242
250
  )
243
- def stop(config_dir=None, profile=None, port=-1, output=""):
251
+ def stop(config_dir=None, profile=None, port=-1, name="", output=""):
252
+ if port == -1 and not name:
253
+ click.secho(
254
+ "Please provide either a port number or a name to stop the app.",
255
+ fg="red",
256
+ err=True,
257
+ )
258
+ return
259
+
244
260
  stop_app_response = OuterboundsCommandResponse()
245
261
 
246
262
  validate_workstation_step = CommandStatus(
@@ -313,11 +329,16 @@ def stop(config_dir=None, profile=None, port=-1, output=""):
313
329
  if workstation["instance_id"] == os.environ["WORKSTATION_ID"]:
314
330
  if "named_ports" in workstation["spec"]:
315
331
  for named_port in workstation["spec"]["named_ports"]:
316
- if int(named_port["port"]) == port:
332
+ if (
333
+ int(named_port["port"]) == port
334
+ or named_port["name"] == name
335
+ ):
317
336
  stop_app_response.add_step(validate_port_exists)
318
337
  if not named_port["enabled"]:
319
338
  click.secho(
320
- f"App stopped on port {port}!", fg="green", err=True
339
+ f"App {named_port['name']} stopped on port {port}!",
340
+ fg="green",
341
+ err=True,
321
342
  )
322
343
  stop_app_response.add_step(stop_app_step)
323
344
  if output == "json":
@@ -365,14 +386,23 @@ def stop(config_dir=None, profile=None, port=-1, output=""):
365
386
  )
366
387
  return
367
388
 
389
+ err_message = (
390
+ f"Port {port} not found on workstation {os.environ['WORKSTATION_ID']}"
391
+ )
392
+ if port == -1:
393
+ err_message = (
394
+ f"App {name} not found on workstation {os.environ['WORKSTATION_ID']}"
395
+ )
396
+
368
397
  click.secho(
369
- f"Port {port} not found on workstation {os.environ['WORKSTATION_ID']}",
398
+ err_message,
370
399
  fg="red",
371
400
  err=True,
372
401
  )
402
+
373
403
  validate_port_exists.update(
374
404
  OuterboundsCommandStatus.FAIL,
375
- f"Port {port} not found on workstation {os.environ['WORKSTATION_ID']}",
405
+ err_message,
376
406
  "",
377
407
  )
378
408
  stop_app_response.add_step(validate_port_exists)
@@ -1,8 +1,5 @@
1
1
  from outerbounds._vendor import click
2
- from . import local_setup_cli
3
- from . import workstations_cli
4
- from . import perimeters_cli
5
- from . import apps_cli
2
+ from . import local_setup_cli, workstations_cli, perimeters_cli, apps_cli, tutorials_cli
6
3
 
7
4
 
8
5
  @click.command(
@@ -12,6 +9,7 @@ from . import apps_cli
12
9
  workstations_cli.cli,
13
10
  perimeters_cli.cli,
14
11
  apps_cli.cli,
12
+ tutorials_cli.cli,
15
13
  ],
16
14
  )
17
15
  def cli(**kwargs):
@@ -5,6 +5,7 @@ import os
5
5
  import re
6
6
  import subprocess
7
7
  import sys
8
+ import time
8
9
  import zlib
9
10
  from base64 import b64decode, b64encode
10
11
  from importlib.machinery import PathFinder
@@ -15,6 +16,10 @@ from outerbounds._vendor import click
15
16
  import requests
16
17
  from requests.exceptions import HTTPError
17
18
 
19
+ from google.oauth2 import service_account
20
+ import google.auth
21
+ import google.auth.jwt
22
+
18
23
  from ..utils import kubeconfig, metaflowconfig
19
24
  from ..utils.schema import (
20
25
  CommandStatus,
@@ -762,6 +767,48 @@ def get_gha_jwt(audience: str):
762
767
  sys.exit(1)
763
768
 
764
769
 
770
+ def get_gcp_jwt(audience: str, service_account_token_file_path: str = ""):
771
+ try:
772
+ if service_account_token_file_path != "":
773
+ credentials = service_account.Credentials.from_service_account_file(
774
+ service_account_token_file_path
775
+ )
776
+ else:
777
+ credentials, project = google.auth.default()
778
+ except Exception as e:
779
+ click.secho(
780
+ f"Failed to get Google Cloud service account credentials. Error: {str(e)}",
781
+ fg="red",
782
+ )
783
+ sys.exit(1)
784
+
785
+ # Ensure the credentials are service account credentials to sign the JWT
786
+ if isinstance(credentials, service_account.Credentials):
787
+ try:
788
+ payload = {
789
+ "iat": int(time.time()), # Issued at time
790
+ "exp": int(time.time()) + 172800, # 48 hours expiration time
791
+ "sub": credentials.service_account_email,
792
+ "aud": audience,
793
+ "sa_email": credentials.service_account_email,
794
+ }
795
+
796
+ signed_jwt = google.auth.jwt.encode(credentials.signer, payload)
797
+ return signed_jwt.decode("utf-8")
798
+ except Exception as e:
799
+ click.secho(
800
+ f"Failed to sign JWT token using Google Cloud service account credentials. Error: {str(e)}",
801
+ fg="red",
802
+ )
803
+ sys.exit(1)
804
+
805
+ click.secho(
806
+ "The provided credentials are not service account credentials. Please provide a valid service account credentials file or set valid service account credentials in the environment using GOOGLE_APPLICATION_CREDENTIALS.",
807
+ fg="red",
808
+ )
809
+ sys.exit(1)
810
+
811
+
765
812
  def get_origin_token(
766
813
  service_principal_name: str,
767
814
  deployment: str,
@@ -932,6 +979,17 @@ def configure(
932
979
  is_flag=True,
933
980
  help="Set if the command is being run in a GitHub Actions environment. If both --jwt-token and --github-actions are specified the --github-actions flag will be ignored.",
934
981
  )
982
+ @click.option(
983
+ "--gcp",
984
+ is_flag=True,
985
+ help="Set if the command is being run which a Google Cloud service account credential. If both --jwt-token and --gcp are specified the -gcp flag will be ignored.",
986
+ )
987
+ @click.option(
988
+ "--gcp-service-account-toke-file-path",
989
+ default="",
990
+ help="The full path to the Google Cloud service account token file. If --gcp is set and this value is not specified, the default GOOGLE_APPLICATION_CREDENTIALS environment variable will be used.",
991
+ required=True,
992
+ )
935
993
  @click.option(
936
994
  "-d",
937
995
  "--config-dir",
@@ -963,18 +1021,31 @@ def service_principal_configure(
963
1021
  perimeter: str,
964
1022
  jwt_token="",
965
1023
  github_actions=False,
1024
+ gcp=False,
1025
+ gcp_service_account_toke_file_path="",
966
1026
  config_dir=None,
967
1027
  profile=None,
968
1028
  echo=None,
969
1029
  force=False,
970
1030
  ):
971
1031
  audience = f"https://{deployment_domain}"
972
- if jwt_token == "" and github_actions:
973
- jwt_token = get_gha_jwt(audience)
1032
+
1033
+ # ensure only one of github_actions or gcp is set
1034
+ if github_actions and gcp:
1035
+ click.secho(
1036
+ "Both --github-actions and --gcp flags cannot be set at the same time.",
1037
+ fg="red",
1038
+ )
1039
+ sys.exit(1)
974
1040
 
975
1041
  if jwt_token == "":
1042
+ if github_actions:
1043
+ jwt_token = get_gha_jwt(audience)
1044
+ elif gcp:
1045
+ jwt_token = get_gcp_jwt(audience, gcp_service_account_toke_file_path)
1046
+ else:
976
1047
  click.secho(
977
- "No JWT token provided. Please provider either a valid jwt token or set --github-actions",
1048
+ "No JWT token provided. Please provider either a valid jwt token or set --github-actions or --gcp flag.",
978
1049
  fg="red",
979
1050
  )
980
1051
  sys.exit(1)
@@ -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")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: outerbounds
3
- Version: 0.3.92
3
+ Version: 0.3.93
4
4
  Summary: More Data Science, Less Administration
5
5
  License: Proprietary
6
6
  Keywords: data science,machine learning,MLOps
@@ -41,10 +41,11 @@ 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/apps_cli.py,sha256=XdGRitYWyKGDZXlGSbcP7U6F3DclqzgBG7ZGuZK8tzY,19361
45
- outerbounds/command_groups/cli.py,sha256=3NyxnczfANtxKh-KnJHGWAEC5akdXux_hylfGXTs2A0,362
46
- outerbounds/command_groups/local_setup_cli.py,sha256=tuuqJRXQ_guEwOuQSIf9wkUU0yg8yAs31myGViAK15s,36364
44
+ outerbounds/command_groups/apps_cli.py,sha256=k3zsBh7GupBqM5sbEU8OHSC1TBVdTBJybvPX4DX5Ack,20052
45
+ outerbounds/command_groups/cli.py,sha256=q0hdJO4biD3iEOdyJcxnRkeleA8AKAhx842kQ49I6kk,365
46
+ outerbounds/command_groups/local_setup_cli.py,sha256=AgK4fe-q1uRm20OsMGFGDezheGOPMBoESC98JT9HzqQ,39046
47
47
  outerbounds/command_groups/perimeters_cli.py,sha256=mrJfFIRYFOjuiz-9h4OKg2JT8Utmbs72z6wvPzDss3s,18685
48
+ outerbounds/command_groups/tutorials_cli.py,sha256=UInFyiMqtscHFfi8YQwiY_6Sdw9quJOtRu5OukEBccw,3522
48
49
  outerbounds/command_groups/workstations_cli.py,sha256=V5Jbj1cVb4IRllI7fOgNgL6OekRpuFDv6CEhDb4xC6w,22016
49
50
  outerbounds/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
50
51
  outerbounds/utils/kubeconfig.py,sha256=yvcyRXGR4AhQuqUDqmbGxEOHw5ixMFV0AZIDg1LI_Qo,7981
@@ -52,7 +53,7 @@ outerbounds/utils/metaflowconfig.py,sha256=l2vJbgPkLISU-XPGZFaC8ZKmYFyJemlD6bwB-
52
53
  outerbounds/utils/schema.py,sha256=lMUr9kNgn9wy-sO_t_Tlxmbt63yLeN4b0xQXbDUDj4A,2331
53
54
  outerbounds/utils/utils.py,sha256=4Z8cszNob_8kDYCLNTrP-wWads_S_MdL3Uj3ju4mEsk,501
54
55
  outerbounds/vendor.py,sha256=gRLRJNXtZBeUpPEog0LOeIsl6GosaFFbCxUvR4bW6IQ,5093
55
- outerbounds-0.3.92.dist-info/entry_points.txt,sha256=7ye0281PKlvqxu15rjw60zKg2pMsXI49_A8BmGqIqBw,47
56
- outerbounds-0.3.92.dist-info/WHEEL,sha256=vVCvjcmxuUltf8cYhJ0sJMRDLr1XsPuxEId8YDzbyCY,88
57
- outerbounds-0.3.92.dist-info/METADATA,sha256=enOUPH2DJjaqvzpywbTNnD1aosIzZq8JlsBne9w84o8,1632
58
- outerbounds-0.3.92.dist-info/RECORD,,
56
+ outerbounds-0.3.93.dist-info/entry_points.txt,sha256=7ye0281PKlvqxu15rjw60zKg2pMsXI49_A8BmGqIqBw,47
57
+ outerbounds-0.3.93.dist-info/WHEEL,sha256=vVCvjcmxuUltf8cYhJ0sJMRDLr1XsPuxEId8YDzbyCY,88
58
+ outerbounds-0.3.93.dist-info/METADATA,sha256=DbAq4c18up77upq4UVgOtzl1AL_cjHkdUC1SBHnx0os,1632
59
+ outerbounds-0.3.93.dist-info/RECORD,,