outerbounds 0.3.109rc1__py3-none-any.whl → 0.3.111__py3-none-any.whl

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