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

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