outerbounds 0.3.92__py3-none-any.whl → 0.3.93__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.
@@ -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,,