outerbounds 0.3.88__py3-none-any.whl → 0.3.90__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.
- 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
|