outerbounds 0.3.88__py3-none-any.whl → 0.3.90__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- outerbounds/command_groups/local_setup_cli.py +0 -3
- outerbounds/command_groups/perimeters_cli.py +188 -23
- outerbounds/utils/metaflowconfig.py +67 -8
- outerbounds/utils/utils.py +19 -0
- {outerbounds-0.3.88.dist-info → outerbounds-0.3.90.dist-info}/METADATA +1 -1
- {outerbounds-0.3.88.dist-info → outerbounds-0.3.90.dist-info}/RECORD +8 -7
- {outerbounds-0.3.88.dist-info → outerbounds-0.3.90.dist-info}/WHEEL +0 -0
- {outerbounds-0.3.88.dist-info → outerbounds-0.3.90.dist-info}/entry_points.txt +0 -0
@@ -640,7 +640,6 @@ class ConfigurationWriter:
|
|
640
640
|
if config_type == "inline":
|
641
641
|
if "OBP_PERIMETER" in self.decoded_config:
|
642
642
|
self.selected_perimeter = self.decoded_config["OBP_PERIMETER"]
|
643
|
-
|
644
643
|
if "OBP_METAFLOW_CONFIG_URL" in self.decoded_config:
|
645
644
|
self.decoded_config = {
|
646
645
|
"OBP_METAFLOW_CONFIG_URL": self.decoded_config[
|
@@ -701,8 +700,6 @@ class ConfigurationWriter:
|
|
701
700
|
with open(config_path, "w") as fd:
|
702
701
|
json.dump(self.existing, fd, indent=4)
|
703
702
|
|
704
|
-
# Every time a config is initialized, we should also reset the corresponding ob_config[_profile].json
|
705
|
-
remote_config = metaflowconfig.init_config(self.out_dir, self.profile)
|
706
703
|
if self.selected_perimeter and "OBP_METAFLOW_CONFIG_URL" in self.decoded_config:
|
707
704
|
with open(self.ob_config_path, "w") as fd:
|
708
705
|
ob_config_dict = {
|
@@ -1,21 +1,15 @@
|
|
1
|
-
import base64
|
2
|
-
import hashlib
|
3
1
|
import json
|
4
2
|
import os
|
5
|
-
import re
|
6
|
-
import subprocess
|
7
3
|
import sys
|
8
|
-
import
|
9
|
-
from base64 import b64decode, b64encode
|
10
|
-
from importlib.machinery import PathFinder
|
4
|
+
from io import StringIO
|
11
5
|
from os import path
|
12
|
-
from
|
13
|
-
from typing import Any, Callable, Dict, List
|
6
|
+
from typing import Any, Dict
|
14
7
|
from outerbounds._vendor import click
|
15
8
|
import requests
|
16
|
-
|
9
|
+
import configparser
|
17
10
|
|
18
|
-
from ..utils import
|
11
|
+
from ..utils import metaflowconfig
|
12
|
+
from ..utils.utils import safe_write_to_disk
|
19
13
|
from ..utils.schema import (
|
20
14
|
CommandStatus,
|
21
15
|
OuterboundsCommandResponse,
|
@@ -80,7 +74,7 @@ def switch(config_dir=None, profile=None, output="", id=None, force=False):
|
|
80
74
|
id, perimeters, output, switch_perimeter_response, switch_perimeter_step
|
81
75
|
)
|
82
76
|
|
83
|
-
path_to_config = get_ob_config_file_path(config_dir, profile)
|
77
|
+
path_to_config = metaflowconfig.get_ob_config_file_path(config_dir, profile)
|
84
78
|
|
85
79
|
import fcntl
|
86
80
|
|
@@ -124,9 +118,31 @@ def switch(config_dir=None, profile=None, output="", id=None, force=False):
|
|
124
118
|
)
|
125
119
|
|
126
120
|
switch_perimeter_response.add_step(switch_perimeter_step)
|
121
|
+
|
122
|
+
ensure_cloud_creds_step = CommandStatus(
|
123
|
+
"EnsureCloudCredentials",
|
124
|
+
OuterboundsCommandStatus.OK,
|
125
|
+
"Cloud credentials were successfully updated.",
|
126
|
+
)
|
127
|
+
|
128
|
+
try:
|
129
|
+
ensure_cloud_credentials_for_shell(config_dir, profile)
|
130
|
+
except:
|
131
|
+
click.secho(
|
132
|
+
"Failed to update cloud credentials.",
|
133
|
+
fg="red",
|
134
|
+
err=True,
|
135
|
+
)
|
136
|
+
ensure_cloud_creds_step.update(
|
137
|
+
status=OuterboundsCommandStatus.FAIL,
|
138
|
+
reason="Failed to update cloud credentials.",
|
139
|
+
mitigation="",
|
140
|
+
)
|
141
|
+
|
142
|
+
switch_perimeter_response.add_step(ensure_cloud_creds_step)
|
143
|
+
|
127
144
|
if output == "json":
|
128
145
|
click.echo(json.dumps(switch_perimeter_response.as_dict(), indent=4))
|
129
|
-
return
|
130
146
|
|
131
147
|
|
132
148
|
@perimeter.command(help="Show current perimeter")
|
@@ -275,6 +291,58 @@ def list(config_dir=None, profile=None, output=""):
|
|
275
291
|
click.echo(json.dumps(list_perimeters_response.as_dict(), indent=4))
|
276
292
|
|
277
293
|
|
294
|
+
@perimeter.command(
|
295
|
+
help="Ensure credentials for cloud are synced with perimeter", hidden=True
|
296
|
+
)
|
297
|
+
@click.option(
|
298
|
+
"-d",
|
299
|
+
"--config-dir",
|
300
|
+
default=path.expanduser(os.environ.get("METAFLOW_HOME", "~/.metaflowconfig")),
|
301
|
+
help="Path to Metaflow configuration directory",
|
302
|
+
show_default=True,
|
303
|
+
)
|
304
|
+
@click.option(
|
305
|
+
"-p",
|
306
|
+
"--profile",
|
307
|
+
default=os.environ.get("METAFLOW_PROFILE", ""),
|
308
|
+
help="The named metaflow profile in which your workstation exists",
|
309
|
+
)
|
310
|
+
@click.option(
|
311
|
+
"-o",
|
312
|
+
"--output",
|
313
|
+
default="",
|
314
|
+
help="Show output in the specified format.",
|
315
|
+
type=click.Choice(["json", ""]),
|
316
|
+
)
|
317
|
+
def ensure_cloud_creds(config_dir=None, profile=None, output=""):
|
318
|
+
ensure_cloud_creds_step = CommandStatus(
|
319
|
+
"EnsureCloudCredentials",
|
320
|
+
OuterboundsCommandStatus.OK,
|
321
|
+
"Cloud credentials were successfully updated.",
|
322
|
+
)
|
323
|
+
|
324
|
+
ensure_cloud_creds_response = OuterboundsCommandResponse()
|
325
|
+
|
326
|
+
try:
|
327
|
+
ensure_cloud_credentials_for_shell(config_dir, profile)
|
328
|
+
click.secho("Cloud credentials updated successfully.", fg="green", err=True)
|
329
|
+
except:
|
330
|
+
click.secho(
|
331
|
+
"Failed to update cloud credentials.",
|
332
|
+
fg="red",
|
333
|
+
err=True,
|
334
|
+
)
|
335
|
+
ensure_cloud_creds_step.update(
|
336
|
+
status=OuterboundsCommandStatus.FAIL,
|
337
|
+
reason="Failed to update cloud credentials.",
|
338
|
+
mitigation="",
|
339
|
+
)
|
340
|
+
|
341
|
+
ensure_cloud_creds_response.add_step(ensure_cloud_creds_step)
|
342
|
+
if output == "json":
|
343
|
+
click.echo(json.dumps(ensure_cloud_creds_response.as_dict(), indent=4))
|
344
|
+
|
345
|
+
|
278
346
|
def get_list_perimeters_api_response(config_dir, profile):
|
279
347
|
metaflow_token = metaflowconfig.get_metaflow_token_from_config(config_dir, profile)
|
280
348
|
api_url = metaflowconfig.get_sanitized_url_from_config(
|
@@ -288,15 +356,6 @@ def get_list_perimeters_api_response(config_dir, profile):
|
|
288
356
|
return perimeters_response.json()["perimeters"]
|
289
357
|
|
290
358
|
|
291
|
-
def get_ob_config_file_path(config_dir: str, profile: str) -> str:
|
292
|
-
# If OBP_CONFIG_DIR is set, use that, otherwise use METAFLOW_HOME
|
293
|
-
# If neither are set, use ~/.metaflowconfig
|
294
|
-
obp_config_dir = path.expanduser(os.environ.get("OBP_CONFIG_DIR", config_dir))
|
295
|
-
|
296
|
-
ob_config_filename = f"ob_config_{profile}.json" if profile else "ob_config.json"
|
297
|
-
return os.path.expanduser(os.path.join(obp_config_dir, ob_config_filename))
|
298
|
-
|
299
|
-
|
300
359
|
def get_perimeters_from_api_or_fail_command(
|
301
360
|
config_dir: str,
|
302
361
|
profile: str,
|
@@ -331,7 +390,7 @@ def get_ob_config_or_fail_command(
|
|
331
390
|
command_response: OuterboundsCommandResponse,
|
332
391
|
command_step: CommandStatus,
|
333
392
|
) -> Dict[str, str]:
|
334
|
-
path_to_config = get_ob_config_file_path(config_dir, profile)
|
393
|
+
path_to_config = metaflowconfig.get_ob_config_file_path(config_dir, profile)
|
335
394
|
|
336
395
|
if not os.path.exists(path_to_config):
|
337
396
|
click.secho(
|
@@ -371,6 +430,23 @@ def get_ob_config_or_fail_command(
|
|
371
430
|
return ob_config_dict
|
372
431
|
|
373
432
|
|
433
|
+
def ensure_cloud_credentials_for_shell(config_dir, profile):
|
434
|
+
if "WORKSTATION_ID" not in os.environ:
|
435
|
+
# Naive check to see if we're running in workstation. No need to ensure anything
|
436
|
+
# if this is not a workstation.
|
437
|
+
return
|
438
|
+
|
439
|
+
mf_config = metaflowconfig.init_config(config_dir, profile)
|
440
|
+
|
441
|
+
# Currently we only support GCP. TODO: utkarsh to add support for AWS and Azure
|
442
|
+
if "METAFLOW_DEFAULT_GCP_CLIENT_PROVIDER" in mf_config:
|
443
|
+
# This is a GCP deployment.
|
444
|
+
ensure_gcp_cloud_creds(config_dir, profile)
|
445
|
+
elif "METAFLOW_DEFAULT_AWS_CLIENT_PROVIDER" in mf_config:
|
446
|
+
# This is an AWS deployment.
|
447
|
+
ensure_aws_cloud_creds(config_dir, profile)
|
448
|
+
|
449
|
+
|
374
450
|
def confirm_user_has_access_to_perimeter_or_fail(
|
375
451
|
perimeter_id: str,
|
376
452
|
perimeters: Dict[str, Any],
|
@@ -395,4 +471,93 @@ def confirm_user_has_access_to_perimeter_or_fail(
|
|
395
471
|
sys.exit(1)
|
396
472
|
|
397
473
|
|
474
|
+
def ensure_gcp_cloud_creds(config_dir, profile):
|
475
|
+
token_info = get_gcp_auth_credentials(config_dir, profile)
|
476
|
+
auth_url = metaflowconfig.get_sanitized_url_from_config(
|
477
|
+
config_dir, profile, "OBP_AUTH_SERVER"
|
478
|
+
)
|
479
|
+
metaflow_token = metaflowconfig.get_metaflow_token_from_config(config_dir, profile)
|
480
|
+
|
481
|
+
try:
|
482
|
+
# GOOGLE_APPLICATION_CREDENTIALS is a well known gcloud environment variable
|
483
|
+
credentials_file_loc = os.environ["GOOGLE_APPLICATION_CREDENTIALS"]
|
484
|
+
except KeyError:
|
485
|
+
# This is most likely an old workstation when these params weren't set. Do nothing.
|
486
|
+
# Alternatively, user might have deliberately unset it to use their own auth.
|
487
|
+
return
|
488
|
+
|
489
|
+
credentials_json = {
|
490
|
+
"type": "external_account",
|
491
|
+
"audience": f"//iam.googleapis.com/projects/{token_info['gcpProjectNumber']}/locations/global/workloadIdentityPools/{token_info['gcpWorkloadIdentityPool']}/providers/{token_info['gcpWorkloadIdentityPoolProvider']}",
|
492
|
+
"subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
|
493
|
+
"token_url": "https://sts.googleapis.com/v1/token",
|
494
|
+
"service_account_impersonation_url": f"https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/{token_info['gcpServiceAccountEmail']}:generateAccessToken",
|
495
|
+
"credential_source": {
|
496
|
+
"url": f"{auth_url}/generate/gcp",
|
497
|
+
"headers": {"x-api-key": metaflow_token},
|
498
|
+
"format": {"type": "json", "subject_token_field_name": "token"},
|
499
|
+
},
|
500
|
+
}
|
501
|
+
|
502
|
+
safe_write_to_disk(credentials_file_loc, json.dumps(credentials_json))
|
503
|
+
|
504
|
+
|
505
|
+
def ensure_aws_cloud_creds(config_dir, profile):
|
506
|
+
token_info = get_aws_auth_credentials(config_dir, profile)
|
507
|
+
|
508
|
+
try:
|
509
|
+
token_file_loc = os.environ["OBP_AWS_WEB_IDENTITY_TOKEN_FILE"]
|
510
|
+
|
511
|
+
# AWS_CONFIG_FILE is a well known aws cli environment variable
|
512
|
+
config_file_loc = os.environ["AWS_CONFIG_FILE"]
|
513
|
+
except KeyError:
|
514
|
+
# This is most likely an old workstation when these params weren't set. Do nothing.
|
515
|
+
# Alternatively, user might have deliberately unset it to use their own auth.
|
516
|
+
return
|
517
|
+
|
518
|
+
aws_config = configparser.ConfigParser()
|
519
|
+
aws_config.read(config_file_loc)
|
520
|
+
|
521
|
+
profile_name = "profile outerbounds"
|
522
|
+
if profile_name not in aws_config:
|
523
|
+
aws_config[profile_name] = {}
|
524
|
+
|
525
|
+
aws_config[profile_name]["role_arn"] = token_info["role_arn"]
|
526
|
+
aws_config[profile_name]["web_identity_token_file"] = token_file_loc
|
527
|
+
|
528
|
+
aws_config_string = StringIO()
|
529
|
+
aws_config.write(aws_config_string)
|
530
|
+
|
531
|
+
safe_write_to_disk(token_file_loc, token_info["token"])
|
532
|
+
safe_write_to_disk(config_file_loc, aws_config_string.getvalue())
|
533
|
+
|
534
|
+
|
535
|
+
def get_aws_auth_credentials(config_dir, profile): # pragma: no cover
|
536
|
+
token = metaflowconfig.get_metaflow_token_from_config(config_dir, profile)
|
537
|
+
auth_server_url = metaflowconfig.get_sanitized_url_from_config(
|
538
|
+
config_dir, profile, "OBP_AUTH_SERVER"
|
539
|
+
)
|
540
|
+
|
541
|
+
response = requests.get(
|
542
|
+
"{}/generate/aws".format(auth_server_url), headers={"x-api-key": token}
|
543
|
+
)
|
544
|
+
response.raise_for_status()
|
545
|
+
|
546
|
+
return response.json()
|
547
|
+
|
548
|
+
|
549
|
+
def get_gcp_auth_credentials(config_dir, profile):
|
550
|
+
token = metaflowconfig.get_metaflow_token_from_config(config_dir, profile)
|
551
|
+
auth_server_url = metaflowconfig.get_sanitized_url_from_config(
|
552
|
+
config_dir, profile, "OBP_AUTH_SERVER"
|
553
|
+
)
|
554
|
+
|
555
|
+
response = requests.get(
|
556
|
+
"{}/generate/gcp".format(auth_server_url), headers={"x-api-key": token}
|
557
|
+
)
|
558
|
+
response.raise_for_status()
|
559
|
+
|
560
|
+
return response.json()
|
561
|
+
|
562
|
+
|
398
563
|
cli.add_command(perimeter, name="perimeter")
|
@@ -3,21 +3,43 @@ import json
|
|
3
3
|
import os
|
4
4
|
import requests
|
5
5
|
from os import path
|
6
|
-
from typing import Dict
|
6
|
+
from typing import Dict, Union
|
7
7
|
import sys
|
8
8
|
|
9
|
+
"""
|
10
|
+
key: perimeter specific URL to fetch the remote metaflow config from
|
11
|
+
value: the remote metaflow config
|
12
|
+
"""
|
13
|
+
CACHED_REMOTE_METAFLOW_CONFIG: Dict[str, Dict[str, str]] = {}
|
14
|
+
|
15
|
+
|
16
|
+
CURRENT_PERIMETER_KEY = "OB_CURRENT_PERIMETER"
|
17
|
+
CURRENT_PERIMETER_URL = "OB_CURRENT_PERIMETER_MF_CONFIG_URL"
|
18
|
+
CURRENT_PERIMETER_URL_LEGACY_KEY = (
|
19
|
+
"OB_CURRENT_PERIMETER_URL" # For backwards compatibility with workstations.
|
20
|
+
)
|
21
|
+
|
9
22
|
|
10
23
|
def init_config(config_dir, profile) -> Dict[str, str]:
|
24
|
+
global CACHED_REMOTE_METAFLOW_CONFIG
|
11
25
|
config = read_metaflow_config_from_filesystem(config_dir, profile)
|
12
26
|
|
13
|
-
#
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
27
|
+
# Either user has an ob_config.json file with the perimeter URL
|
28
|
+
# or the default config on the filesystem has the config URL in it.
|
29
|
+
perimeter_specifc_url = get_perimeter_config_url_if_set_in_ob_config(
|
30
|
+
config_dir, profile
|
31
|
+
) or config.get("OBP_METAFLOW_CONFIG_URL", "")
|
32
|
+
|
33
|
+
if perimeter_specifc_url != "":
|
34
|
+
if perimeter_specifc_url in CACHED_REMOTE_METAFLOW_CONFIG:
|
35
|
+
return CACHED_REMOTE_METAFLOW_CONFIG[perimeter_specifc_url]
|
36
|
+
|
37
|
+
remote_config = init_config_from_url(config_dir, profile, perimeter_specifc_url)
|
38
|
+
remote_config["OBP_METAFLOW_CONFIG_URL"] = perimeter_specifc_url
|
39
|
+
|
40
|
+
CACHED_REMOTE_METAFLOW_CONFIG[perimeter_specifc_url] = remote_config
|
19
41
|
return remote_config
|
20
|
-
|
42
|
+
|
21
43
|
return config
|
22
44
|
|
23
45
|
|
@@ -101,3 +123,40 @@ def get_remote_metaflow_config_for_perimeter(
|
|
101
123
|
fg="red",
|
102
124
|
)
|
103
125
|
sys.exit(1)
|
126
|
+
|
127
|
+
|
128
|
+
def get_ob_config_file_path(config_dir: str, profile: str) -> str:
|
129
|
+
# If OBP_CONFIG_DIR is set, use that, otherwise use METAFLOW_HOME
|
130
|
+
# If neither are set, use ~/.metaflowconfig
|
131
|
+
obp_config_dir = path.expanduser(os.environ.get("OBP_CONFIG_DIR", config_dir))
|
132
|
+
|
133
|
+
ob_config_filename = f"ob_config_{profile}.json" if profile else "ob_config.json"
|
134
|
+
return os.path.expanduser(os.path.join(obp_config_dir, ob_config_filename))
|
135
|
+
|
136
|
+
|
137
|
+
def get_perimeter_config_url_if_set_in_ob_config(
|
138
|
+
config_dir: str, profile: str
|
139
|
+
) -> Union[str, None]:
|
140
|
+
file_path = get_ob_config_file_path(config_dir, profile)
|
141
|
+
|
142
|
+
if os.path.exists(file_path):
|
143
|
+
with open(file_path, "r") as f:
|
144
|
+
ob_config = json.loads(f.read())
|
145
|
+
|
146
|
+
if CURRENT_PERIMETER_URL in ob_config:
|
147
|
+
return ob_config[CURRENT_PERIMETER_URL]
|
148
|
+
elif CURRENT_PERIMETER_URL_LEGACY_KEY in ob_config:
|
149
|
+
return ob_config[CURRENT_PERIMETER_URL_LEGACY_KEY]
|
150
|
+
else:
|
151
|
+
raise ValueError(
|
152
|
+
"{} does not contain the key {}".format(
|
153
|
+
file_path, CURRENT_PERIMETER_KEY
|
154
|
+
)
|
155
|
+
)
|
156
|
+
elif "OBP_CONFIG_DIR" in os.environ:
|
157
|
+
raise FileNotFoundError(
|
158
|
+
"Environment variable OBP_CONFIG_DIR is set to {} but this directory does not contain an ob_config.json file.".format(
|
159
|
+
os.environ["OBP_CONFIG_DIR"]
|
160
|
+
)
|
161
|
+
)
|
162
|
+
return None
|
@@ -0,0 +1,19 @@
|
|
1
|
+
import tempfile
|
2
|
+
import os
|
3
|
+
|
4
|
+
"""
|
5
|
+
Writes the given data to file_loc. Ensures that the directory exists
|
6
|
+
and uses a temporary file to ensure that the file is written atomically.
|
7
|
+
"""
|
8
|
+
|
9
|
+
|
10
|
+
def safe_write_to_disk(file_loc, data):
|
11
|
+
# Ensure the directory exists
|
12
|
+
os.makedirs(os.path.dirname(file_loc), exist_ok=True)
|
13
|
+
|
14
|
+
with tempfile.NamedTemporaryFile(
|
15
|
+
"w", dir=os.path.dirname(file_loc), delete=False
|
16
|
+
) as f:
|
17
|
+
f.write(data)
|
18
|
+
tmp_file = f.name
|
19
|
+
os.rename(tmp_file, file_loc)
|
@@ -42,15 +42,16 @@ outerbounds/_vendor/yaml/tokens.py,sha256=JBSu38wihGr4l73JwbfMA7Ks1-X84g8-NskTz7
|
|
42
42
|
outerbounds/cli_main.py,sha256=e9UMnPysmc7gbrimq2I4KfltggyU7pw59Cn9aEguVcU,74
|
43
43
|
outerbounds/command_groups/__init__.py,sha256=QPWtj5wDRTINDxVUL7XPqG3HoxHNvYOg08EnuSZB2Hc,21
|
44
44
|
outerbounds/command_groups/cli.py,sha256=sorDdQvmTPqIwfvgtuNLILelimXu5CknFnWQFsYFGHs,286
|
45
|
-
outerbounds/command_groups/local_setup_cli.py,sha256=
|
46
|
-
outerbounds/command_groups/perimeters_cli.py,sha256=
|
45
|
+
outerbounds/command_groups/local_setup_cli.py,sha256=tuuqJRXQ_guEwOuQSIf9wkUU0yg8yAs31myGViAK15s,36364
|
46
|
+
outerbounds/command_groups/perimeters_cli.py,sha256=mrJfFIRYFOjuiz-9h4OKg2JT8Utmbs72z6wvPzDss3s,18685
|
47
47
|
outerbounds/command_groups/workstations_cli.py,sha256=V5Jbj1cVb4IRllI7fOgNgL6OekRpuFDv6CEhDb4xC6w,22016
|
48
48
|
outerbounds/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
49
49
|
outerbounds/utils/kubeconfig.py,sha256=yvcyRXGR4AhQuqUDqmbGxEOHw5ixMFV0AZIDg1LI_Qo,7981
|
50
|
-
outerbounds/utils/metaflowconfig.py,sha256=
|
50
|
+
outerbounds/utils/metaflowconfig.py,sha256=l2vJbgPkLISU-XPGZFaC8ZKmYFyJemlD6bwB-EKUsAw,5770
|
51
51
|
outerbounds/utils/schema.py,sha256=cNlgjmteLPbDzSEUSQDsq8txdhMGyezSmM83jU3aa0w,2329
|
52
|
+
outerbounds/utils/utils.py,sha256=4Z8cszNob_8kDYCLNTrP-wWads_S_MdL3Uj3ju4mEsk,501
|
52
53
|
outerbounds/vendor.py,sha256=gRLRJNXtZBeUpPEog0LOeIsl6GosaFFbCxUvR4bW6IQ,5093
|
53
|
-
outerbounds-0.3.
|
54
|
-
outerbounds-0.3.
|
55
|
-
outerbounds-0.3.
|
56
|
-
outerbounds-0.3.
|
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,,
|
File without changes
|
File without changes
|