outerbounds 0.3.109rc1__py3-none-any.whl → 0.3.111__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.
@@ -1,15 +1,16 @@
1
- import json
2
1
  import os
3
2
  from os import path
4
3
  from outerbounds._vendor import click
5
4
  import requests
5
+ import time
6
+ import random
6
7
 
7
8
  from ..utils import metaflowconfig
8
- from ..utils.schema import (
9
- CommandStatus,
10
- OuterboundsCommandResponse,
11
- OuterboundsCommandStatus,
12
- )
9
+
10
+ APP_READY_POLL_TIMEOUT_SECONDS = 300
11
+ # Even after our backend validates that the app routes are ready, it takes a few seconds for
12
+ # the app to be accessible via the browser. Till we hunt down this delay, add an extra buffer.
13
+ APP_READY_EXTRA_BUFFER_SECONDS = 30
13
14
 
14
15
 
15
16
  @click.group()
@@ -48,14 +49,7 @@ def app(**kwargs):
48
49
  help="Name of your app",
49
50
  type=str,
50
51
  )
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=""):
52
+ def start(config_dir=None, profile=None, port=-1, name=""):
59
53
  if len(name) == 0 or len(name) >= 20:
60
54
  click.secho(
61
55
  "App name should not be more than 20 characters long.",
@@ -71,49 +65,16 @@ def start(config_dir=None, profile=None, port=-1, name="", output=""):
71
65
  )
72
66
  return
73
67
 
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
68
  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
69
  click.secho(
108
70
  "All outerbounds app commands can only be run from a workstation.",
109
71
  fg="red",
110
72
  err=True,
111
73
  )
112
-
113
- if output == "json":
114
- click.echo(json.dumps(start_app_response.as_dict(), indent=4))
115
74
  return
116
75
 
76
+ workstation_id = os.environ["WORKSTATION_ID"]
77
+
117
78
  try:
118
79
  try:
119
80
  metaflow_token = metaflowconfig.get_metaflow_token_from_config(
@@ -127,15 +88,8 @@ def start(config_dir=None, profile=None, port=-1, name="", output=""):
127
88
  f"{api_url}/v1/workstations", headers={"x-api-key": metaflow_token}
128
89
  )
129
90
  workstations_response.raise_for_status()
130
- start_app_response.add_step(list_workstations_step)
131
91
  except:
132
92
  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
93
  return
140
94
 
141
95
  workstations_json = workstations_response.json()["workstations"]
@@ -148,20 +102,8 @@ def start(config_dir=None, profile=None, port=-1, name="", output=""):
148
102
  )
149
103
  except ValueError as e:
150
104
  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
105
  return
162
106
 
163
- start_app_response.add_step(validate_request)
164
-
165
107
  for named_port in workstation["spec"]["named_ports"]:
166
108
  if int(named_port["port"]) == port:
167
109
  if named_port["enabled"] and named_port["name"] == name:
@@ -171,22 +113,15 @@ def start(config_dir=None, profile=None, port=-1, name="", output=""):
171
113
  err=True,
172
114
  )
173
115
  click.secho(
174
- f"App URL: {api_url.replace('api', 'ui')}/apps/{os.environ['WORKSTATION_ID']}/{name}/",
116
+ f"App URL: {api_url.replace('api', 'ui')}/apps/{workstation_id}/{name}/",
175
117
  fg="green",
176
118
  err=True,
177
119
  )
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
120
  return
186
121
  else:
187
122
  try:
188
123
  response = requests.put(
189
- f"{api_url}/v1/workstations/update/{os.environ['WORKSTATION_ID']}/namedports",
124
+ f"{api_url}/v1/workstations/update/{workstation_id}/namedports",
190
125
  headers={"x-api-key": metaflow_token},
191
126
  json={
192
127
  "port": port,
@@ -196,46 +131,40 @@ def start(config_dir=None, profile=None, port=-1, name="", output=""):
196
131
  )
197
132
 
198
133
  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,
134
+ poll_success = wait_for_app_port_to_be_accessible(
135
+ api_url,
136
+ metaflow_token,
137
+ workstation_id,
138
+ name,
139
+ APP_READY_POLL_TIMEOUT_SECONDS,
208
140
  )
141
+ if poll_success:
142
+ click.secho(
143
+ f"App {name} started on port {port}!",
144
+ fg="green",
145
+ err=True,
146
+ )
147
+ click.secho(
148
+ f"App URL: {api_url.replace('api', 'ui')}/apps/{os.environ['WORKSTATION_ID']}/{name}/",
149
+ fg="green",
150
+ err=True,
151
+ )
152
+ else:
153
+ click.secho(
154
+ f"The app could not be deployed in {APP_READY_POLL_TIMEOUT_SECONDS / 60} minutes. Please try again later.",
155
+ fg="red",
156
+ err=True,
157
+ )
158
+ return
209
159
  except Exception:
210
160
  click.secho(
211
161
  f"Failed to start app {name} on port {port}!",
212
162
  fg="red",
213
163
  err=True,
214
164
  )
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
165
  return
229
166
  except Exception as e:
230
167
  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
168
 
240
169
 
241
170
  @app.command(help="Stop an app using its port number")
@@ -266,14 +195,7 @@ def start(config_dir=None, profile=None, port=-1, name="", output=""):
266
195
  default="",
267
196
  type=str,
268
197
  )
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=""):
198
+ def stop(config_dir=None, profile=None, port=-1, name=""):
277
199
  if port == -1 and not name:
278
200
  click.secho(
279
201
  "Please provide either a port number or a name to stop the app.",
@@ -289,47 +211,13 @@ def stop(config_dir=None, profile=None, port=-1, name="", output=""):
289
211
  )
290
212
  return
291
213
 
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
214
  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
215
  click.secho(
326
216
  "All outerbounds app commands can only be run from a workstation.",
327
217
  fg="red",
328
218
  err=True,
329
219
  )
330
220
 
331
- if output == "json":
332
- click.echo(json.dumps(stop_app_response.as_dict(), indent=4))
333
221
  return
334
222
 
335
223
  try:
@@ -345,15 +233,8 @@ def stop(config_dir=None, profile=None, port=-1, name="", output=""):
345
233
  f"{api_url}/v1/workstations", headers={"x-api-key": metaflow_token}
346
234
  )
347
235
  workstations_response.raise_for_status()
348
- stop_app_response.add_step(list_workstations_step)
349
236
  except:
350
237
  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
238
  return
358
239
 
359
240
  app_found = False
@@ -367,7 +248,6 @@ def stop(config_dir=None, profile=None, port=-1, name="", output=""):
367
248
  or named_port["name"] == name
368
249
  ):
369
250
  app_found = True
370
- stop_app_response.add_step(validate_port_exists)
371
251
  if named_port["enabled"]:
372
252
  try:
373
253
  response = requests.put(
@@ -391,19 +271,6 @@ def stop(config_dir=None, profile=None, port=-1, name="", output=""):
391
271
  fg="red",
392
272
  err=True,
393
273
  )
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
274
  return
408
275
 
409
276
  if app_found:
@@ -417,9 +284,6 @@ def stop(config_dir=None, profile=None, port=-1, name="", output=""):
417
284
  fg="green",
418
285
  err=True,
419
286
  )
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
287
  return
424
288
 
425
289
  err_message = (
@@ -433,26 +297,11 @@ def stop(config_dir=None, profile=None, port=-1, name="", output=""):
433
297
  fg="red",
434
298
  err=True,
435
299
  )
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
300
  except Exception as e:
446
301
  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
302
 
454
303
 
455
- @app.command(help="Stop an app using its port number")
304
+ @app.command(help="List all apps on the workstation")
456
305
  @click.option(
457
306
  "-d",
458
307
  "--config-dir",
@@ -466,43 +315,14 @@ def stop(config_dir=None, profile=None, port=-1, name="", output=""):
466
315
  default=os.environ.get("METAFLOW_PROFILE", ""),
467
316
  help="The named metaflow profile in which your workstation exists",
468
317
  )
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
-
318
+ def list(config_dir=None, profile=None):
491
319
  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
320
  click.secho(
499
321
  "All outerbounds app commands can only be run from a workstation.",
500
322
  fg="red",
501
323
  err=True,
502
324
  )
503
325
 
504
- if output == "json":
505
- click.echo(json.dumps(list_app_response.as_dict(), indent=4))
506
326
  return
507
327
 
508
328
  try:
@@ -518,15 +338,8 @@ def list(config_dir=None, profile=None, output=""):
518
338
  f"{api_url}/v1/workstations", headers={"x-api-key": metaflow_token}
519
339
  )
520
340
  workstations_response.raise_for_status()
521
- list_app_response.add_step(list_workstations_step)
522
341
  except:
523
342
  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
343
  return
531
344
 
532
345
  workstations_json = workstations_response.json()["workstations"]
@@ -562,8 +375,6 @@ def list(config_dir=None, profile=None, output=""):
562
375
  click.echo("\n", err=True)
563
376
  except Exception as e:
564
377
  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
378
 
568
379
 
569
380
  def ensure_app_start_request_is_valid(existing_named_ports, port: int, name: str):
@@ -583,4 +394,57 @@ def ensure_app_start_request_is_valid(existing_named_ports, port: int, name: str
583
394
  )
584
395
 
585
396
 
397
+ def wait_for_app_port_to_be_accessible(
398
+ api_url, metaflow_token, workstation_id, app_name, poll_timeout_seconds
399
+ ) -> bool:
400
+ num_retries_per_request = 3
401
+ start_time = time.time()
402
+ retry_delay = 1.0
403
+ poll_interval = 10
404
+ wait_message = f"App {app_name} is currently being deployed..."
405
+ while time.time() - start_time < poll_timeout_seconds:
406
+ for _ in range(num_retries_per_request):
407
+ try:
408
+ workstations_response = requests.get(
409
+ f"{api_url}/v1/workstations", headers={"x-api-key": metaflow_token}
410
+ )
411
+ workstations_response.raise_for_status()
412
+ if is_app_ready(workstations_response.json(), workstation_id, app_name):
413
+ click.secho(
414
+ wait_message,
415
+ fg="yellow",
416
+ err=True,
417
+ )
418
+ time.sleep(APP_READY_EXTRA_BUFFER_SECONDS)
419
+ return True
420
+ else:
421
+ click.secho(
422
+ wait_message,
423
+ fg="yellow",
424
+ err=True,
425
+ )
426
+ time.sleep(poll_interval)
427
+ except (
428
+ requests.exceptions.ConnectionError,
429
+ requests.exceptions.ReadTimeout,
430
+ ):
431
+ time.sleep(retry_delay)
432
+ retry_delay *= 2 # Double the delay for the next attempt
433
+ retry_delay += random.uniform(0, 1) # Add jitter
434
+ retry_delay = min(retry_delay, 10)
435
+ return False
436
+
437
+
438
+ def is_app_ready(response_json: dict, workstation_id: str, app_name: str) -> bool:
439
+ """Checks if the app is ready in the given workstation's response."""
440
+ workstations = response_json.get("workstations", [])
441
+ for workstation in workstations:
442
+ if workstation.get("instance_id") == workstation_id:
443
+ hosted_apps = workstation.get("status", {}).get("hosted_apps", [])
444
+ for hosted_app in hosted_apps:
445
+ if hosted_app.get("name") == app_name:
446
+ return bool(hosted_app.get("ready"))
447
+ return False
448
+
449
+
586
450
  cli.add_command(app, name="app")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: outerbounds
3
- Version: 0.3.109rc1
3
+ Version: 0.3.111
4
4
  Summary: More Data Science, Less Administration
5
5
  License: Proprietary
6
6
  Keywords: data science,machine learning,MLOps
@@ -25,10 +25,10 @@ Requires-Dist: google-api-core (>=2.16.1,<3.0.0) ; extra == "gcp"
25
25
  Requires-Dist: google-auth (>=2.27.0,<3.0.0) ; extra == "gcp"
26
26
  Requires-Dist: google-cloud-secret-manager (>=2.20.0,<3.0.0) ; extra == "gcp"
27
27
  Requires-Dist: google-cloud-storage (>=2.14.0,<3.0.0) ; extra == "gcp"
28
- Requires-Dist: metaflow-checkpoint (==0.0.13)
29
- Requires-Dist: ob-metaflow (==2.12.25.1)
30
- Requires-Dist: ob-metaflow-extensions (==1.1.98)
31
- Requires-Dist: ob-metaflow-stubs (==6.0.3.109rc1)
28
+ Requires-Dist: metaflow-checkpoint (==0.1.0)
29
+ Requires-Dist: ob-metaflow (==2.12.25.2)
30
+ Requires-Dist: ob-metaflow-extensions (==1.1.99)
31
+ Requires-Dist: ob-metaflow-stubs (==6.0.3.111)
32
32
  Requires-Dist: opentelemetry-distro (==0.41b0)
33
33
  Requires-Dist: opentelemetry-exporter-otlp-proto-http (==1.20.0)
34
34
  Requires-Dist: opentelemetry-instrumentation-requests (==0.41b0)
@@ -41,7 +41,7 @@ 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=iXaLnO-FwU_zK2ZjE-gBu1ZQdOYDLCbT0HJXJJZckeE,21895
44
+ outerbounds/command_groups/apps_cli.py,sha256=8jmQufa0bK2sfRfs7DiWjoJ1oWiqZAixsL4Dte_KY4Y,17201
45
45
  outerbounds/command_groups/cli.py,sha256=q0hdJO4biD3iEOdyJcxnRkeleA8AKAhx842kQ49I6kk,365
46
46
  outerbounds/command_groups/local_setup_cli.py,sha256=tuuqJRXQ_guEwOuQSIf9wkUU0yg8yAs31myGViAK15s,36364
47
47
  outerbounds/command_groups/perimeters_cli.py,sha256=iF_Uw7ROiSctf6FgoJEy30iDBLVE1j9FKuR3shgJRmc,19050
@@ -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.109rc1.dist-info/METADATA,sha256=y_w5rsXhyL29ELQT7xiktXegZB5GrFO69ZvxwEDunoA,1742
57
- outerbounds-0.3.109rc1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
58
- outerbounds-0.3.109rc1.dist-info/entry_points.txt,sha256=7ye0281PKlvqxu15rjw60zKg2pMsXI49_A8BmGqIqBw,47
59
- outerbounds-0.3.109rc1.dist-info/RECORD,,
56
+ outerbounds-0.3.111.dist-info/METADATA,sha256=v6zcvET5H7gPPNFcR-Gqy_QDh6EB_XYq0N6htQdfVBA,1735
57
+ outerbounds-0.3.111.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
58
+ outerbounds-0.3.111.dist-info/entry_points.txt,sha256=7ye0281PKlvqxu15rjw60zKg2pMsXI49_A8BmGqIqBw,47
59
+ outerbounds-0.3.111.dist-info/RECORD,,