outerbounds 0.3.90__py3-none-any.whl → 0.3.92__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.
@@ -0,0 +1,505 @@
1
+ import json
2
+ import os
3
+ from os import path
4
+ from outerbounds._vendor import click
5
+ import requests
6
+
7
+ from ..utils import metaflowconfig
8
+ from ..utils.schema import (
9
+ CommandStatus,
10
+ OuterboundsCommandResponse,
11
+ OuterboundsCommandStatus,
12
+ )
13
+
14
+
15
+ @click.group()
16
+ def cli(**kwargs):
17
+ pass
18
+
19
+
20
+ @click.group(help="Manage perimeters")
21
+ def app(**kwargs):
22
+ pass
23
+
24
+
25
+ @app.command(help="Start an app using a port and a name")
26
+ @click.option(
27
+ "-d",
28
+ "--config-dir",
29
+ default=path.expanduser(os.environ.get("METAFLOW_HOME", "~/.metaflowconfig")),
30
+ help="Path to Metaflow configuration directory",
31
+ show_default=True,
32
+ )
33
+ @click.option(
34
+ "-p",
35
+ "--profile",
36
+ default=os.environ.get("METAFLOW_PROFILE", ""),
37
+ help="The named metaflow profile in which your workstation exists",
38
+ )
39
+ @click.option(
40
+ "--port",
41
+ required=True,
42
+ help="Port number where you want to start your app",
43
+ type=int,
44
+ )
45
+ @click.option(
46
+ "--name",
47
+ required=True,
48
+ help="Name of your app",
49
+ type=str,
50
+ )
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=""):
59
+ start_app_response = OuterboundsCommandResponse()
60
+
61
+ validate_workstation_step = CommandStatus(
62
+ "ValidateRunningOnWorkstation",
63
+ OuterboundsCommandStatus.OK,
64
+ "Command is being run on a workstation.",
65
+ )
66
+
67
+ list_workstations_step = CommandStatus(
68
+ "ListWorkstations",
69
+ OuterboundsCommandStatus.OK,
70
+ "List of workstations fetched.",
71
+ )
72
+
73
+ validate_port_exists = CommandStatus(
74
+ "ValidatePortExists",
75
+ OuterboundsCommandStatus.OK,
76
+ "Port exists on workstation",
77
+ )
78
+
79
+ start_app_step = CommandStatus(
80
+ "StartApp",
81
+ OuterboundsCommandStatus.OK,
82
+ f"App {name} started on port {port}!",
83
+ )
84
+
85
+ if "WORKSTATION_ID" not in os.environ:
86
+ validate_workstation_step.update(
87
+ OuterboundsCommandStatus.FAIL,
88
+ "All outerbounds app commands can only be run from a workstation.",
89
+ "",
90
+ )
91
+ start_app_response.add_step(validate_workstation_step)
92
+ click.secho(
93
+ "All outerbounds app commands can only be run from a workstation.",
94
+ fg="red",
95
+ err=True,
96
+ )
97
+
98
+ if output == "json":
99
+ click.echo(json.dumps(start_app_response.as_dict(), indent=4))
100
+ return
101
+
102
+ try:
103
+ try:
104
+ metaflow_token = metaflowconfig.get_metaflow_token_from_config(
105
+ config_dir, profile
106
+ )
107
+ api_url = metaflowconfig.get_sanitized_url_from_config(
108
+ config_dir, profile, "OBP_API_SERVER"
109
+ )
110
+
111
+ workstations_response = requests.get(
112
+ f"{api_url}/v1/workstations", headers={"x-api-key": metaflow_token}
113
+ )
114
+ workstations_response.raise_for_status()
115
+ start_app_response.add_step(list_workstations_step)
116
+ except:
117
+ click.secho("Failed to list workstations!", fg="red", err=True)
118
+ list_workstations_step.update(
119
+ OuterboundsCommandStatus.FAIL, "Failed to list workstations!", ""
120
+ )
121
+ start_app_response.add_step(list_workstations_step)
122
+ if output == "json":
123
+ click.echo(json.dumps(start_app_response.as_dict(), indent=4))
124
+ return
125
+
126
+ workstations_json = workstations_response.json()["workstations"]
127
+ for workstation in workstations_json:
128
+ if workstation["instance_id"] == os.environ["WORKSTATION_ID"]:
129
+ if "named_ports" in workstation["spec"]:
130
+ for named_port in workstation["spec"]["named_ports"]:
131
+ if int(named_port["port"]) == port:
132
+ start_app_response.add_step(validate_port_exists)
133
+ if named_port["enabled"] and named_port["name"] == name:
134
+ click.secho(
135
+ f"App {name} started on port {port}!",
136
+ fg="green",
137
+ err=True,
138
+ )
139
+ click.secho(
140
+ f"App URL: {api_url.replace('api', 'ui')}/apps/{os.environ['WORKSTATION_ID']}/{name}/",
141
+ fg="green",
142
+ err=True,
143
+ )
144
+ start_app_response.add_step(start_app_step)
145
+ if output == "json":
146
+ click.echo(
147
+ json.dumps(
148
+ start_app_response.as_dict(), indent=4
149
+ )
150
+ )
151
+ return
152
+ else:
153
+ try:
154
+ response = requests.put(
155
+ f"{api_url}/v1/workstations/update/{os.environ['WORKSTATION_ID']}/namedports",
156
+ headers={"x-api-key": metaflow_token},
157
+ json={
158
+ "port": port,
159
+ "name": name,
160
+ "enabled": True,
161
+ },
162
+ )
163
+
164
+ response.raise_for_status()
165
+ click.secho(
166
+ f"App {name} started on port {port}!",
167
+ fg="green",
168
+ err=True,
169
+ )
170
+ except:
171
+ click.secho(
172
+ f"Failed to start app {name} on port {port}!",
173
+ fg="red",
174
+ err=True,
175
+ )
176
+ start_app_step.update(
177
+ OuterboundsCommandStatus.FAIL,
178
+ f"Failed to start app {name} on port {port}!",
179
+ "",
180
+ )
181
+
182
+ start_app_response.add_step(start_app_step)
183
+ if output == "json":
184
+ click.echo(
185
+ json.dumps(
186
+ start_app_response.as_dict(), indent=4
187
+ )
188
+ )
189
+ 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
+ except Exception as e:
205
+ click.secho(f"Failed to start app {name} on port {port}!", fg="red", err=True)
206
+ start_app_step.update(
207
+ OuterboundsCommandStatus.FAIL,
208
+ f"Failed to start app {name} on port {port}!",
209
+ "",
210
+ )
211
+ start_app_response.add_step(start_app_step)
212
+ if output == "json":
213
+ click.secho(json.dumps(start_app_response.as_dict(), indent=4))
214
+
215
+
216
+ @app.command(help="Stop an app using its port number")
217
+ @click.option(
218
+ "-d",
219
+ "--config-dir",
220
+ default=path.expanduser(os.environ.get("METAFLOW_HOME", "~/.metaflowconfig")),
221
+ help="Path to Metaflow configuration directory",
222
+ show_default=True,
223
+ )
224
+ @click.option(
225
+ "-p",
226
+ "--profile",
227
+ default=os.environ.get("METAFLOW_PROFILE", ""),
228
+ help="The named metaflow profile in which your workstation exists",
229
+ )
230
+ @click.option(
231
+ "--port",
232
+ required=True,
233
+ help="Port number where you want to start your app.",
234
+ type=int,
235
+ )
236
+ @click.option(
237
+ "-o",
238
+ "--output",
239
+ default="",
240
+ help="Show output in the specified format.",
241
+ type=click.Choice(["json", ""]),
242
+ )
243
+ def stop(config_dir=None, profile=None, port=-1, output=""):
244
+ stop_app_response = OuterboundsCommandResponse()
245
+
246
+ validate_workstation_step = CommandStatus(
247
+ "ValidateRunningOnWorkstation",
248
+ OuterboundsCommandStatus.OK,
249
+ "Command is being run on a workstation.",
250
+ )
251
+
252
+ list_workstations_step = CommandStatus(
253
+ "ListWorkstations",
254
+ OuterboundsCommandStatus.OK,
255
+ "List of workstations fetched.",
256
+ )
257
+
258
+ validate_port_exists = CommandStatus(
259
+ "ValidatePortExists",
260
+ OuterboundsCommandStatus.OK,
261
+ "Port exists on workstation",
262
+ )
263
+
264
+ stop_app_step = CommandStatus(
265
+ "StopApp",
266
+ OuterboundsCommandStatus.OK,
267
+ f"App stopped on port {port}!",
268
+ )
269
+
270
+ if "WORKSTATION_ID" not in os.environ:
271
+ validate_workstation_step.update(
272
+ OuterboundsCommandStatus.FAIL,
273
+ "All outerbounds app commands can only be run from a workstation.",
274
+ "",
275
+ )
276
+ stop_app_response.add_step(validate_workstation_step)
277
+ click.secho(
278
+ "All outerbounds app commands can only be run from a workstation.",
279
+ fg="red",
280
+ err=True,
281
+ )
282
+
283
+ if output == "json":
284
+ click.echo(json.dumps(stop_app_response.as_dict(), indent=4))
285
+ return
286
+
287
+ try:
288
+ try:
289
+ metaflow_token = metaflowconfig.get_metaflow_token_from_config(
290
+ config_dir, profile
291
+ )
292
+ api_url = metaflowconfig.get_sanitized_url_from_config(
293
+ config_dir, profile, "OBP_API_SERVER"
294
+ )
295
+
296
+ workstations_response = requests.get(
297
+ f"{api_url}/v1/workstations", headers={"x-api-key": metaflow_token}
298
+ )
299
+ workstations_response.raise_for_status()
300
+ stop_app_response.add_step(list_workstations_step)
301
+ except:
302
+ click.secho("Failed to list workstations!", fg="red", err=True)
303
+ list_workstations_step.update(
304
+ OuterboundsCommandStatus.FAIL, "Failed to list workstations!", ""
305
+ )
306
+ stop_app_response.add_step(list_workstations_step)
307
+ if output == "json":
308
+ click.echo(json.dumps(stop_app_response.as_dict(), indent=4))
309
+ return
310
+
311
+ workstations_json = workstations_response.json()["workstations"]
312
+ for workstation in workstations_json:
313
+ if workstation["instance_id"] == os.environ["WORKSTATION_ID"]:
314
+ if "named_ports" in workstation["spec"]:
315
+ for named_port in workstation["spec"]["named_ports"]:
316
+ if int(named_port["port"]) == port:
317
+ stop_app_response.add_step(validate_port_exists)
318
+ if not named_port["enabled"]:
319
+ click.secho(
320
+ f"App stopped on port {port}!", fg="green", err=True
321
+ )
322
+ stop_app_response.add_step(stop_app_step)
323
+ if output == "json":
324
+ click.echo(
325
+ json.dumps(
326
+ stop_app_response.as_dict(), indent=4
327
+ )
328
+ )
329
+ return
330
+ else:
331
+ try:
332
+ response = requests.put(
333
+ f"{api_url}/v1/workstations/update/{os.environ['WORKSTATION_ID']}/namedports",
334
+ headers={"x-api-key": metaflow_token},
335
+ json={
336
+ "port": port,
337
+ "name": named_port["name"],
338
+ "enabled": False,
339
+ },
340
+ )
341
+ response.raise_for_status()
342
+ click.secho(
343
+ f"App stopped on port {port}!",
344
+ fg="green",
345
+ err=True,
346
+ )
347
+ except:
348
+ click.secho(
349
+ f"Failed to stop app on port {port}!",
350
+ fg="red",
351
+ err=True,
352
+ )
353
+ stop_app_step.update(
354
+ OuterboundsCommandStatus.FAIL,
355
+ f"Failed to stop app on port {port}!",
356
+ "",
357
+ )
358
+
359
+ stop_app_response.add_step(stop_app_step)
360
+ if output == "json":
361
+ click.echo(
362
+ json.dumps(
363
+ stop_app_response.as_dict(), indent=4
364
+ )
365
+ )
366
+ return
367
+
368
+ click.secho(
369
+ f"Port {port} not found on workstation {os.environ['WORKSTATION_ID']}",
370
+ fg="red",
371
+ err=True,
372
+ )
373
+ validate_port_exists.update(
374
+ OuterboundsCommandStatus.FAIL,
375
+ f"Port {port} not found on workstation {os.environ['WORKSTATION_ID']}",
376
+ "",
377
+ )
378
+ stop_app_response.add_step(validate_port_exists)
379
+ if output == "json":
380
+ click.echo(json.dumps(stop_app_response.as_dict(), indent=4))
381
+ except Exception as e:
382
+ click.secho(f"Failed to stop app on port {port}!", fg="red", err=True)
383
+ stop_app_step.update(
384
+ OuterboundsCommandStatus.FAIL, f"Failed to stop on port {port}!", ""
385
+ )
386
+ stop_app_response.add_step(stop_app_step)
387
+ if output == "json":
388
+ click.echo(json.dumps(stop_app_response.as_dict(), indent=4))
389
+
390
+
391
+ @app.command(help="Stop an app using its port number")
392
+ @click.option(
393
+ "-d",
394
+ "--config-dir",
395
+ default=path.expanduser(os.environ.get("METAFLOW_HOME", "~/.metaflowconfig")),
396
+ help="Path to Metaflow configuration directory",
397
+ show_default=True,
398
+ )
399
+ @click.option(
400
+ "-p",
401
+ "--profile",
402
+ default=os.environ.get("METAFLOW_PROFILE", ""),
403
+ help="The named metaflow profile in which your workstation exists",
404
+ )
405
+ @click.option(
406
+ "-o",
407
+ "--output",
408
+ default="",
409
+ help="Show output in the specified format.",
410
+ type=click.Choice(["json", ""]),
411
+ )
412
+ def list(config_dir=None, profile=None, output=""):
413
+ list_app_response = OuterboundsCommandResponse()
414
+
415
+ validate_workstation_step = CommandStatus(
416
+ "ValidateRunningOnWorkstation",
417
+ OuterboundsCommandStatus.OK,
418
+ "Command is being run on a workstation.",
419
+ )
420
+
421
+ list_workstations_step = CommandStatus(
422
+ "ListWorkstations",
423
+ OuterboundsCommandStatus.OK,
424
+ "List of workstations fetched.",
425
+ )
426
+
427
+ if "WORKSTATION_ID" not in os.environ:
428
+ validate_workstation_step.update(
429
+ OuterboundsCommandStatus.FAIL,
430
+ "All outerbounds app commands can only be run from a workstation.",
431
+ "",
432
+ )
433
+ list_app_response.add_step(validate_workstation_step)
434
+ click.secho(
435
+ "All outerbounds app commands can only be run from a workstation.",
436
+ fg="red",
437
+ err=True,
438
+ )
439
+
440
+ if output == "json":
441
+ click.echo(json.dumps(list_app_response.as_dict(), indent=4))
442
+ return
443
+
444
+ try:
445
+ try:
446
+ metaflow_token = metaflowconfig.get_metaflow_token_from_config(
447
+ config_dir, profile
448
+ )
449
+ api_url = metaflowconfig.get_sanitized_url_from_config(
450
+ config_dir, profile, "OBP_API_SERVER"
451
+ )
452
+
453
+ workstations_response = requests.get(
454
+ f"{api_url}/v1/workstations", headers={"x-api-key": metaflow_token}
455
+ )
456
+ workstations_response.raise_for_status()
457
+ list_app_response.add_step(list_workstations_step)
458
+ except:
459
+ click.secho("Failed to list workstations!", fg="red", err=True)
460
+ list_workstations_step.update(
461
+ OuterboundsCommandStatus.FAIL, "Failed to list workstations!", ""
462
+ )
463
+ list_app_response.add_step(list_workstations_step)
464
+ if output == "json":
465
+ click.echo(json.dumps(list_app_response.as_dict(), indent=4))
466
+ return
467
+
468
+ workstations_json = workstations_response.json()["workstations"]
469
+ for workstation in workstations_json:
470
+ if workstation["instance_id"] == os.environ["WORKSTATION_ID"]:
471
+ if "named_ports" in workstation["spec"]:
472
+ for named_port in workstation["spec"]["named_ports"]:
473
+ if named_port["enabled"]:
474
+ click.secho(
475
+ f"App Name: {named_port['name']}", fg="green", err=True
476
+ )
477
+ click.secho(
478
+ f"App Port on Workstation: {named_port['port']}",
479
+ fg="green",
480
+ err=True,
481
+ )
482
+ click.secho(f"App Status: Deployed", fg="green", err=True)
483
+ click.secho(
484
+ f"App URL: {api_url.replace('api', 'ui')}/apps/{os.environ['WORKSTATION_ID']}/{named_port['name']}/",
485
+ fg="green",
486
+ err=True,
487
+ )
488
+ else:
489
+ click.secho(
490
+ f"App Port on Workstation: {named_port['port']}",
491
+ fg="yellow",
492
+ err=True,
493
+ )
494
+ click.secho(
495
+ f"App Status: Not Deployed", fg="yellow", err=True
496
+ )
497
+
498
+ click.echo("\n", err=True)
499
+ except Exception as e:
500
+ click.secho(f"Failed to list apps!", fg="red", err=True)
501
+ if output == "json":
502
+ click.echo(json.dumps(list_app_response.as_dict(), indent=4))
503
+
504
+
505
+ cli.add_command(app, name="app")
@@ -2,11 +2,17 @@ from outerbounds._vendor import click
2
2
  from . import local_setup_cli
3
3
  from . import workstations_cli
4
4
  from . import perimeters_cli
5
+ from . import apps_cli
5
6
 
6
7
 
7
8
  @click.command(
8
9
  cls=click.CommandCollection,
9
- sources=[local_setup_cli.cli, workstations_cli.cli, perimeters_cli.cli],
10
+ sources=[
11
+ local_setup_cli.cli,
12
+ workstations_cli.cli,
13
+ perimeters_cli.cli,
14
+ apps_cli.cli,
15
+ ],
10
16
  )
11
17
  def cli(**kwargs):
12
18
  pass
@@ -59,14 +59,14 @@ class OuterboundsCommandResponse:
59
59
  if step.status == OuterboundsCommandStatus.FAIL:
60
60
  self.status = OuterboundsCommandStatus.FAIL
61
61
  self._code = 500
62
- self._message = "We found one or more errors with your installation."
62
+ self._message = "Encountered an error when trying to run command."
63
63
  elif (
64
64
  step.status == OuterboundsCommandStatus.WARN
65
65
  and self.status != OuterboundsCommandStatus.FAIL
66
66
  ):
67
67
  self.status = OuterboundsCommandStatus.WARN
68
68
  self._code = 200
69
- self._message = "We found one or more warnings with your installation."
69
+ self._message = "Encountered one or more warnings when running the command."
70
70
 
71
71
  def as_dict(self):
72
72
  self._data["steps"] = [step.as_dict() for step in self._steps]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: outerbounds
3
- Version: 0.3.90
3
+ Version: 0.3.92
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.12.0)
28
- Requires-Dist: ob-metaflow-extensions (==1.1.79)
29
- Requires-Dist: ob-metaflow-stubs (==5.2)
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)
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,17 +41,18 @@ 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/cli.py,sha256=sorDdQvmTPqIwfvgtuNLILelimXu5CknFnWQFsYFGHs,286
44
+ outerbounds/command_groups/apps_cli.py,sha256=XdGRitYWyKGDZXlGSbcP7U6F3DclqzgBG7ZGuZK8tzY,19361
45
+ outerbounds/command_groups/cli.py,sha256=3NyxnczfANtxKh-KnJHGWAEC5akdXux_hylfGXTs2A0,362
45
46
  outerbounds/command_groups/local_setup_cli.py,sha256=tuuqJRXQ_guEwOuQSIf9wkUU0yg8yAs31myGViAK15s,36364
46
47
  outerbounds/command_groups/perimeters_cli.py,sha256=mrJfFIRYFOjuiz-9h4OKg2JT8Utmbs72z6wvPzDss3s,18685
47
48
  outerbounds/command_groups/workstations_cli.py,sha256=V5Jbj1cVb4IRllI7fOgNgL6OekRpuFDv6CEhDb4xC6w,22016
48
49
  outerbounds/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
49
50
  outerbounds/utils/kubeconfig.py,sha256=yvcyRXGR4AhQuqUDqmbGxEOHw5ixMFV0AZIDg1LI_Qo,7981
50
51
  outerbounds/utils/metaflowconfig.py,sha256=l2vJbgPkLISU-XPGZFaC8ZKmYFyJemlD6bwB-EKUsAw,5770
51
- outerbounds/utils/schema.py,sha256=cNlgjmteLPbDzSEUSQDsq8txdhMGyezSmM83jU3aa0w,2329
52
+ outerbounds/utils/schema.py,sha256=lMUr9kNgn9wy-sO_t_Tlxmbt63yLeN4b0xQXbDUDj4A,2331
52
53
  outerbounds/utils/utils.py,sha256=4Z8cszNob_8kDYCLNTrP-wWads_S_MdL3Uj3ju4mEsk,501
53
54
  outerbounds/vendor.py,sha256=gRLRJNXtZBeUpPEog0LOeIsl6GosaFFbCxUvR4bW6IQ,5093
54
- outerbounds-0.3.90.dist-info/entry_points.txt,sha256=7ye0281PKlvqxu15rjw60zKg2pMsXI49_A8BmGqIqBw,47
55
- outerbounds-0.3.90.dist-info/WHEEL,sha256=vVCvjcmxuUltf8cYhJ0sJMRDLr1XsPuxEId8YDzbyCY,88
56
- outerbounds-0.3.90.dist-info/METADATA,sha256=an69uvqpICHuiBfJVZJJrZU474ehFkjnnilY1jRLYFg,1632
57
- outerbounds-0.3.90.dist-info/RECORD,,
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,,