outerbounds 0.3.93__py3-none-any.whl → 0.3.95__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.
@@ -56,6 +56,21 @@ def app(**kwargs):
56
56
  type=click.Choice(["json", ""]),
57
57
  )
58
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
+
59
74
  start_app_response = OuterboundsCommandResponse()
60
75
 
61
76
  validate_workstation_step = CommandStatus(
@@ -70,10 +85,10 @@ def start(config_dir=None, profile=None, port=-1, name="", output=""):
70
85
  "List of workstations fetched.",
71
86
  )
72
87
 
73
- validate_port_exists = CommandStatus(
74
- "ValidatePortExists",
88
+ validate_request = CommandStatus(
89
+ "ValidateRequest",
75
90
  OuterboundsCommandStatus.OK,
76
- "Port exists on workstation",
91
+ "Start app request is valid.",
77
92
  )
78
93
 
79
94
  start_app_step = CommandStatus(
@@ -127,12 +142,31 @@ def start(config_dir=None, profile=None, port=-1, name="", output=""):
127
142
  for workstation in workstations_json:
128
143
  if workstation["instance_id"] == os.environ["WORKSTATION_ID"]:
129
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
+
130
165
  for named_port in workstation["spec"]["named_ports"]:
131
166
  if int(named_port["port"]) == port:
132
- start_app_response.add_step(validate_port_exists)
133
167
  if named_port["enabled"] and named_port["name"] == name:
134
168
  click.secho(
135
- f"App {name} started on port {port}!",
169
+ f"App {name} already running on port {port}!",
136
170
  fg="green",
137
171
  err=True,
138
172
  )
@@ -167,7 +201,12 @@ def start(config_dir=None, profile=None, port=-1, name="", output=""):
167
201
  fg="green",
168
202
  err=True,
169
203
  )
170
- except:
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:
171
210
  click.secho(
172
211
  f"Failed to start app {name} on port {port}!",
173
212
  fg="red",
@@ -187,20 +226,6 @@ def start(config_dir=None, profile=None, port=-1, name="", output=""):
187
226
  )
188
227
  )
189
228
  return
190
-
191
- click.secho(
192
- f"Port {port} not found on workstation {os.environ['WORKSTATION_ID']}",
193
- fg="red",
194
- err=True,
195
- )
196
- validate_port_exists.update(
197
- OuterboundsCommandStatus.FAIL,
198
- f"Port {port} not found on workstation {os.environ['WORKSTATION_ID']}",
199
- "",
200
- )
201
- start_app_response.add_step(validate_port_exists)
202
- if output == "json":
203
- click.echo(json.dumps(start_app_response.as_dict(), indent=4))
204
229
  except Exception as e:
205
230
  click.secho(f"Failed to start app {name} on port {port}!", fg="red", err=True)
206
231
  start_app_step.update(
@@ -256,6 +281,13 @@ def stop(config_dir=None, profile=None, port=-1, name="", output=""):
256
281
  err=True,
257
282
  )
258
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
259
291
 
260
292
  stop_app_response = OuterboundsCommandResponse()
261
293
 
@@ -324,6 +356,7 @@ def stop(config_dir=None, profile=None, port=-1, name="", output=""):
324
356
  click.echo(json.dumps(stop_app_response.as_dict(), indent=4))
325
357
  return
326
358
 
359
+ app_found = False
327
360
  workstations_json = workstations_response.json()["workstations"]
328
361
  for workstation in workstations_json:
329
362
  if workstation["instance_id"] == os.environ["WORKSTATION_ID"]:
@@ -333,47 +366,34 @@ def stop(config_dir=None, profile=None, port=-1, name="", output=""):
333
366
  int(named_port["port"]) == port
334
367
  or named_port["name"] == name
335
368
  ):
369
+ app_found = True
336
370
  stop_app_response.add_step(validate_port_exists)
337
- if not named_port["enabled"]:
338
- click.secho(
339
- f"App {named_port['name']} stopped on port {port}!",
340
- fg="green",
341
- err=True,
342
- )
343
- stop_app_response.add_step(stop_app_step)
344
- if output == "json":
345
- click.echo(
346
- json.dumps(
347
- stop_app_response.as_dict(), indent=4
348
- )
349
- )
350
- return
351
- else:
371
+ if named_port["enabled"]:
352
372
  try:
353
373
  response = requests.put(
354
374
  f"{api_url}/v1/workstations/update/{os.environ['WORKSTATION_ID']}/namedports",
355
375
  headers={"x-api-key": metaflow_token},
356
376
  json={
357
- "port": port,
377
+ "port": named_port["port"],
358
378
  "name": named_port["name"],
359
379
  "enabled": False,
360
380
  },
361
381
  )
362
382
  response.raise_for_status()
363
383
  click.secho(
364
- f"App stopped on port {port}!",
384
+ f"App {named_port['name']} stopped on port {named_port['port']}!",
365
385
  fg="green",
366
386
  err=True,
367
387
  )
368
- except:
388
+ except Exception as e:
369
389
  click.secho(
370
- f"Failed to stop app on port {port}!",
390
+ f"Failed to stop app {named_port['name']} on port {named_port['port']}!",
371
391
  fg="red",
372
392
  err=True,
373
393
  )
374
394
  stop_app_step.update(
375
395
  OuterboundsCommandStatus.FAIL,
376
- f"Failed to stop app on port {port}!",
396
+ f"Failed to stop app {named_port['name']} on port {named_port['port']}!",
377
397
  "",
378
398
  )
379
399
 
@@ -386,13 +406,27 @@ def stop(config_dir=None, profile=None, port=-1, name="", output=""):
386
406
  )
387
407
  return
388
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
+
389
425
  err_message = (
390
- f"Port {port} not found on workstation {os.environ['WORKSTATION_ID']}"
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']}"
391
429
  )
392
- if port == -1:
393
- err_message = (
394
- f"App {name} not found on workstation {os.environ['WORKSTATION_ID']}"
395
- )
396
430
 
397
431
  click.secho(
398
432
  err_message,
@@ -532,4 +566,21 @@ def list(config_dir=None, profile=None, output=""):
532
566
  click.echo(json.dumps(list_app_response.as_dict(), indent=4))
533
567
 
534
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
+
535
586
  cli.add_command(app, name="app")
@@ -5,7 +5,6 @@ import os
5
5
  import re
6
6
  import subprocess
7
7
  import sys
8
- import time
9
8
  import zlib
10
9
  from base64 import b64decode, b64encode
11
10
  from importlib.machinery import PathFinder
@@ -16,10 +15,6 @@ from outerbounds._vendor import click
16
15
  import requests
17
16
  from requests.exceptions import HTTPError
18
17
 
19
- from google.oauth2 import service_account
20
- import google.auth
21
- import google.auth.jwt
22
-
23
18
  from ..utils import kubeconfig, metaflowconfig
24
19
  from ..utils.schema import (
25
20
  CommandStatus,
@@ -767,48 +762,6 @@ def get_gha_jwt(audience: str):
767
762
  sys.exit(1)
768
763
 
769
764
 
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
-
812
765
  def get_origin_token(
813
766
  service_principal_name: str,
814
767
  deployment: str,
@@ -979,17 +932,6 @@ def configure(
979
932
  is_flag=True,
980
933
  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.",
981
934
  )
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
- )
993
935
  @click.option(
994
936
  "-d",
995
937
  "--config-dir",
@@ -1021,31 +963,18 @@ def service_principal_configure(
1021
963
  perimeter: str,
1022
964
  jwt_token="",
1023
965
  github_actions=False,
1024
- gcp=False,
1025
- gcp_service_account_toke_file_path="",
1026
966
  config_dir=None,
1027
967
  profile=None,
1028
968
  echo=None,
1029
969
  force=False,
1030
970
  ):
1031
971
  audience = f"https://{deployment_domain}"
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)
972
+ if jwt_token == "" and github_actions:
973
+ jwt_token = get_gha_jwt(audience)
1040
974
 
1041
975
  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:
1047
976
  click.secho(
1048
- "No JWT token provided. Please provider either a valid jwt token or set --github-actions or --gcp flag.",
977
+ "No JWT token provided. Please provider either a valid jwt token or set --github-actions",
1049
978
  fg="red",
1050
979
  )
1051
980
  sys.exit(1)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: outerbounds
3
- Version: 0.3.93
3
+ Version: 0.3.95
4
4
  Summary: More Data Science, Less Administration
5
5
  License: Proprietary
6
6
  Keywords: data science,machine learning,MLOps
@@ -24,9 +24,9 @@ Requires-Dist: google-api-core (>=2.16.1,<3.0.0) ; extra == "gcp"
24
24
  Requires-Dist: google-auth (>=2.27.0,<3.0.0) ; extra == "gcp"
25
25
  Requires-Dist: google-cloud-secret-manager (>=2.20.0,<3.0.0) ; extra == "gcp"
26
26
  Requires-Dist: google-cloud-storage (>=2.14.0,<3.0.0) ; extra == "gcp"
27
- Requires-Dist: ob-metaflow (==2.12.17.1)
28
- Requires-Dist: ob-metaflow-extensions (==1.1.82)
29
- Requires-Dist: ob-metaflow-stubs (==5.5)
27
+ Requires-Dist: ob-metaflow (==2.12.18.1)
28
+ Requires-Dist: ob-metaflow-extensions (==1.1.83)
29
+ Requires-Dist: ob-metaflow-stubs (==5.6)
30
30
  Requires-Dist: opentelemetry-distro (==0.41b0)
31
31
  Requires-Dist: opentelemetry-exporter-otlp-proto-http (==1.20.0)
32
32
  Requires-Dist: opentelemetry-instrumentation-requests (==0.41b0)
@@ -41,9 +41,9 @@ 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=k3zsBh7GupBqM5sbEU8OHSC1TBVdTBJybvPX4DX5Ack,20052
44
+ outerbounds/command_groups/apps_cli.py,sha256=iXaLnO-FwU_zK2ZjE-gBu1ZQdOYDLCbT0HJXJJZckeE,21895
45
45
  outerbounds/command_groups/cli.py,sha256=q0hdJO4biD3iEOdyJcxnRkeleA8AKAhx842kQ49I6kk,365
46
- outerbounds/command_groups/local_setup_cli.py,sha256=AgK4fe-q1uRm20OsMGFGDezheGOPMBoESC98JT9HzqQ,39046
46
+ outerbounds/command_groups/local_setup_cli.py,sha256=tuuqJRXQ_guEwOuQSIf9wkUU0yg8yAs31myGViAK15s,36364
47
47
  outerbounds/command_groups/perimeters_cli.py,sha256=mrJfFIRYFOjuiz-9h4OKg2JT8Utmbs72z6wvPzDss3s,18685
48
48
  outerbounds/command_groups/tutorials_cli.py,sha256=UInFyiMqtscHFfi8YQwiY_6Sdw9quJOtRu5OukEBccw,3522
49
49
  outerbounds/command_groups/workstations_cli.py,sha256=V5Jbj1cVb4IRllI7fOgNgL6OekRpuFDv6CEhDb4xC6w,22016
@@ -53,7 +53,7 @@ outerbounds/utils/metaflowconfig.py,sha256=l2vJbgPkLISU-XPGZFaC8ZKmYFyJemlD6bwB-
53
53
  outerbounds/utils/schema.py,sha256=lMUr9kNgn9wy-sO_t_Tlxmbt63yLeN4b0xQXbDUDj4A,2331
54
54
  outerbounds/utils/utils.py,sha256=4Z8cszNob_8kDYCLNTrP-wWads_S_MdL3Uj3ju4mEsk,501
55
55
  outerbounds/vendor.py,sha256=gRLRJNXtZBeUpPEog0LOeIsl6GosaFFbCxUvR4bW6IQ,5093
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,,
56
+ outerbounds-0.3.95.dist-info/entry_points.txt,sha256=7ye0281PKlvqxu15rjw60zKg2pMsXI49_A8BmGqIqBw,47
57
+ outerbounds-0.3.95.dist-info/WHEEL,sha256=vVCvjcmxuUltf8cYhJ0sJMRDLr1XsPuxEId8YDzbyCY,88
58
+ outerbounds-0.3.95.dist-info/METADATA,sha256=3LLiN9Qy5q9P2ps9CMZO6C_5zi8eSTkxniYoRILxbxA,1632
59
+ outerbounds-0.3.95.dist-info/RECORD,,