outerbounds 0.3.52rc6__py3-none-any.whl → 0.3.53rc1__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/cli.py +3 -1
- outerbounds/command_groups/local_setup_cli.py +16 -335
- outerbounds/command_groups/perimeters_cli.py +374 -0
- outerbounds/utils/metaflowconfig.py +40 -60
- {outerbounds-0.3.52rc6.dist-info → outerbounds-0.3.53rc1.dist-info}/METADATA +2 -2
- {outerbounds-0.3.52rc6.dist-info → outerbounds-0.3.53rc1.dist-info}/RECORD +8 -7
- {outerbounds-0.3.52rc6.dist-info → outerbounds-0.3.53rc1.dist-info}/WHEEL +0 -0
- {outerbounds-0.3.52rc6.dist-info → outerbounds-0.3.53rc1.dist-info}/entry_points.txt +0 -0
@@ -1,10 +1,12 @@
|
|
1
1
|
import click
|
2
2
|
from . import local_setup_cli
|
3
3
|
from . import workstations_cli
|
4
|
+
from . import perimeters_cli
|
4
5
|
|
5
6
|
|
6
7
|
@click.command(
|
7
|
-
cls=click.CommandCollection,
|
8
|
+
cls=click.CommandCollection,
|
9
|
+
sources=[local_setup_cli.cli, workstations_cli.cli, perimeters_cli.cli],
|
8
10
|
)
|
9
11
|
def cli(**kwargs):
|
10
12
|
pass
|
@@ -189,7 +189,7 @@ def check_ob_extension(narrator: Narrator) -> CommandStatus:
|
|
189
189
|
if d.is_dir() and not d.name.startswith("__")
|
190
190
|
]
|
191
191
|
subdirs.sort()
|
192
|
-
if subdirs != ["config", "plugins", "toplevel"]:
|
192
|
+
if subdirs != ["config", "plugins", "profilers", "toplevel"]:
|
193
193
|
check_status.update(
|
194
194
|
status=OuterboundsCommandStatus.FAIL,
|
195
195
|
reason=BAD_EXTENSION_MESSAGE,
|
@@ -586,6 +586,12 @@ class ConfigurationWriter:
|
|
586
586
|
self.out_dir = out_dir
|
587
587
|
self.profile = profile
|
588
588
|
|
589
|
+
ob_config_dir = path.expanduser(os.getenv("OBP_CONFIG_DIR", out_dir))
|
590
|
+
self.ob_config_path = path.join(
|
591
|
+
ob_config_dir,
|
592
|
+
"ob_config_{}.json".format(profile) if profile else "ob_config.json",
|
593
|
+
)
|
594
|
+
|
589
595
|
def decode(self):
|
590
596
|
self.decoded_config = deserialize(self.encoded_config)
|
591
597
|
|
@@ -648,6 +654,15 @@ class ConfigurationWriter:
|
|
648
654
|
with open(config_path, "w") as fd:
|
649
655
|
json.dump(self.existing, fd, indent=4)
|
650
656
|
|
657
|
+
# Every time a config is initialized, we should also reset the corresponding ob_config[_profile].json
|
658
|
+
remote_config = metaflowconfig.init_config(self.out_dir, self.profile)
|
659
|
+
with open(self.ob_config_path, "w") as fd:
|
660
|
+
ob_config_dict = {
|
661
|
+
"OB_CURRENT_PERIMETER": remote_config["OBP_PERIMETER"],
|
662
|
+
"OB_CURRENT_PERIMETER_URL": remote_config["OBP_PERIMETER_URL"],
|
663
|
+
}
|
664
|
+
json.dump(ob_config_dict, fd, indent=4)
|
665
|
+
|
651
666
|
def confirm_overwrite_config(self, config_path):
|
652
667
|
if os.path.exists(config_path):
|
653
668
|
if not click.confirm(
|
@@ -778,337 +793,3 @@ def configure(
|
|
778
793
|
except Exception as e:
|
779
794
|
click.secho("Writing the configuration file '{}' failed.".format(writer.path()))
|
780
795
|
click.secho("Error: {}".format(str(e)))
|
781
|
-
|
782
|
-
|
783
|
-
@cli.command(help="Switch current perimeter")
|
784
|
-
@click.option(
|
785
|
-
"-o",
|
786
|
-
"--output",
|
787
|
-
default="",
|
788
|
-
help="Show output in the specified format.",
|
789
|
-
type=click.Choice(["json", ""]),
|
790
|
-
)
|
791
|
-
@click.option("--id", default="", type=str, help="Perimeter name to switch to")
|
792
|
-
@click.option(
|
793
|
-
"-f",
|
794
|
-
"--force",
|
795
|
-
is_flag=True,
|
796
|
-
help="Force change the existing perimeter",
|
797
|
-
default=False,
|
798
|
-
)
|
799
|
-
def switch_perimeter(output="", id=None, force=False):
|
800
|
-
switch_perimeter_response = OuterboundsCommandResponse()
|
801
|
-
|
802
|
-
switch_perimeter_step = CommandStatus(
|
803
|
-
"SwitchPerimeter",
|
804
|
-
OuterboundsCommandStatus.OK,
|
805
|
-
"Perimeter was successfully switched!",
|
806
|
-
)
|
807
|
-
|
808
|
-
metaflow_home_dir = path.expanduser(
|
809
|
-
os.environ.get("METAFLOW_HOME", "~/.metaflowconfig")
|
810
|
-
)
|
811
|
-
|
812
|
-
if not os.path.exists("{}/config_{}.json".format(metaflow_home_dir, id)):
|
813
|
-
click.secho(
|
814
|
-
"Perimeter {} does not exist or you don't have access to it".format(id),
|
815
|
-
fg="red",
|
816
|
-
err=True,
|
817
|
-
)
|
818
|
-
switch_perimeter_step.update(
|
819
|
-
status=OuterboundsCommandStatus.FAIL,
|
820
|
-
reason="Perimeter {} does not exist or you don't have access to it".format(
|
821
|
-
id
|
822
|
-
),
|
823
|
-
mitigation="Please contact your admin or the Outerbounds team for assistance.",
|
824
|
-
)
|
825
|
-
switch_perimeter_response.add_step(switch_perimeter_step)
|
826
|
-
if output == "json":
|
827
|
-
click.echo(json.dumps(switch_perimeter_response.as_dict(), indent=4))
|
828
|
-
return
|
829
|
-
|
830
|
-
# If OBP_CONFIG_DIR is set, use that, otherwise use METAFLOW_HOME
|
831
|
-
# If neither are set, use ~/.metaflowconfig
|
832
|
-
obp_config_dir = path.expanduser(
|
833
|
-
os.environ.get(
|
834
|
-
"OBP_CONFIG_DIR", os.environ.get("METAFLOW_HOME", "~/.metaflowconfig")
|
835
|
-
)
|
836
|
-
)
|
837
|
-
|
838
|
-
path_to_config = os.path.expanduser(os.path.join(obp_config_dir, "ob_config.json"))
|
839
|
-
|
840
|
-
if not os.path.exists(path_to_config):
|
841
|
-
click.secho(
|
842
|
-
"Config file not found at {}".format(path_to_config), fg="red", err=True
|
843
|
-
)
|
844
|
-
switch_perimeter_step.update(
|
845
|
-
status=OuterboundsCommandStatus.FAIL,
|
846
|
-
reason="Config file not found",
|
847
|
-
mitigation="Please make sure the config file exists at {}".format(
|
848
|
-
path_to_config
|
849
|
-
),
|
850
|
-
)
|
851
|
-
switch_perimeter_response.add_step(switch_perimeter_step)
|
852
|
-
if output == "json":
|
853
|
-
click.echo(json.dumps(switch_perimeter_response.as_dict(), indent=4))
|
854
|
-
return
|
855
|
-
|
856
|
-
import fcntl
|
857
|
-
|
858
|
-
fd = os.open(path_to_config, os.O_WRONLY)
|
859
|
-
|
860
|
-
try:
|
861
|
-
if not force:
|
862
|
-
# Try to acquire an exclusive lock
|
863
|
-
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
864
|
-
else:
|
865
|
-
click.secho(
|
866
|
-
"Force flag is set. Perimeter will be switched, but can have unintended consequences on other running processes.",
|
867
|
-
fg="yellow",
|
868
|
-
err=True,
|
869
|
-
)
|
870
|
-
|
871
|
-
ob_config_dict = {"OB_CURRENT_PERIMETER": str(id)}
|
872
|
-
|
873
|
-
# Now that we have the lock, we can safely write to the file
|
874
|
-
with open(path_to_config, "w") as file:
|
875
|
-
json.dump(ob_config_dict, file, indent=4)
|
876
|
-
|
877
|
-
click.secho("Perimeter switched to {}".format(id), fg="green", err=True)
|
878
|
-
except BlockingIOError:
|
879
|
-
# This exception is raised if the file is already locked (non-blocking mode)
|
880
|
-
# Note that its the metaflow package (the extension actually) that acquires a shared read lock
|
881
|
-
# on the file whenever a process imports metaflow.
|
882
|
-
# In the future we might want to get smarter about it and show which process is holding the lock.
|
883
|
-
click.secho(
|
884
|
-
"Can't switch perimeter while Metaflow is in use. Please make sure there are no running python processes or notebooks using metaflow.",
|
885
|
-
fg="red",
|
886
|
-
err=True,
|
887
|
-
)
|
888
|
-
switch_perimeter_step.update(
|
889
|
-
status=OuterboundsCommandStatus.FAIL,
|
890
|
-
reason="Can't switch perimeter while Metaflow is in use.",
|
891
|
-
mitigation="Please make sure there are no running python processes or notebooks using metaflow.",
|
892
|
-
)
|
893
|
-
|
894
|
-
switch_perimeter_response.add_step(switch_perimeter_step)
|
895
|
-
if output == "json":
|
896
|
-
click.echo(json.dumps(switch_perimeter_response.as_dict(), indent=4))
|
897
|
-
return
|
898
|
-
|
899
|
-
|
900
|
-
@cli.command(help="Show current perimeter")
|
901
|
-
@click.option(
|
902
|
-
"-o",
|
903
|
-
"--output",
|
904
|
-
default="",
|
905
|
-
help="Show output in the specified format.",
|
906
|
-
type=click.Choice(["json", ""]),
|
907
|
-
)
|
908
|
-
def show_current_perimeter(output=""):
|
909
|
-
show_current_perimeter_response = OuterboundsCommandResponse()
|
910
|
-
|
911
|
-
show_current_perimeter_step = CommandStatus(
|
912
|
-
"ShowCurrentPerimeter",
|
913
|
-
OuterboundsCommandStatus.OK,
|
914
|
-
"Current Perimeter Fetch Successful.",
|
915
|
-
)
|
916
|
-
|
917
|
-
metaflow_home_dir = path.expanduser(
|
918
|
-
os.environ.get("METAFLOW_HOME", "~/.metaflowconfig")
|
919
|
-
)
|
920
|
-
|
921
|
-
# If OBP_CONFIG_DIR is set, use that, otherwise use METAFLOW_HOME
|
922
|
-
# If neither are set, use ~/.metaflowconfig
|
923
|
-
obp_config_dir = path.expanduser(
|
924
|
-
os.environ.get(
|
925
|
-
"OBP_CONFIG_DIR", os.environ.get("METAFLOW_HOME", "~/.metaflowconfig")
|
926
|
-
)
|
927
|
-
)
|
928
|
-
|
929
|
-
path_to_config = os.path.expanduser(os.path.join(obp_config_dir, "ob_config.json"))
|
930
|
-
|
931
|
-
if not os.path.exists(path_to_config):
|
932
|
-
click.secho(
|
933
|
-
"Config file not found at {}".format(path_to_config), fg="red", err=True
|
934
|
-
)
|
935
|
-
show_current_perimeter_step.update(
|
936
|
-
status=OuterboundsCommandStatus.FAIL,
|
937
|
-
reason="Config file not found",
|
938
|
-
mitigation="Please make sure the config file exists at {}".format(
|
939
|
-
path_to_config
|
940
|
-
),
|
941
|
-
)
|
942
|
-
show_current_perimeter_response.add_step(show_current_perimeter_step)
|
943
|
-
if output == "json":
|
944
|
-
click.echo(json.dumps(show_current_perimeter_response.as_dict(), indent=4))
|
945
|
-
return
|
946
|
-
|
947
|
-
with open(path_to_config, "r") as file:
|
948
|
-
ob_config_dict = json.load(file)
|
949
|
-
|
950
|
-
if "OB_CURRENT_PERIMETER" not in ob_config_dict:
|
951
|
-
click.secho(
|
952
|
-
"OB_CURRENT_PERIMETER not found in Config file: {}".format(path_to_config),
|
953
|
-
fg="red",
|
954
|
-
err=True,
|
955
|
-
)
|
956
|
-
show_current_perimeter_step.update(
|
957
|
-
status=OuterboundsCommandStatus.FAIL,
|
958
|
-
reason="OB_CURRENT_PERIMETER not found in Config file: {}",
|
959
|
-
mitigation="",
|
960
|
-
)
|
961
|
-
show_current_perimeter_response.add_step(show_current_perimeter_step)
|
962
|
-
if output == "json":
|
963
|
-
click.echo(json.dumps(show_current_perimeter_response.as_dict(), indent=4))
|
964
|
-
return
|
965
|
-
|
966
|
-
if os.path.exists(
|
967
|
-
os.path.join(
|
968
|
-
metaflow_home_dir,
|
969
|
-
"config_{}.json".format(ob_config_dict["OB_CURRENT_PERIMETER"]),
|
970
|
-
)
|
971
|
-
):
|
972
|
-
click.secho(
|
973
|
-
"Current Perimeter: {}".format(ob_config_dict["OB_CURRENT_PERIMETER"]),
|
974
|
-
fg="green",
|
975
|
-
err=True,
|
976
|
-
)
|
977
|
-
show_current_perimeter_response.add_or_update_data(
|
978
|
-
"current_perimeter", ob_config_dict["OB_CURRENT_PERIMETER"]
|
979
|
-
)
|
980
|
-
else:
|
981
|
-
click.secho(
|
982
|
-
"Perimeter config file not found at {}".format(
|
983
|
-
os.path.join(
|
984
|
-
metaflow_home_dir,
|
985
|
-
"config_{}.json".format(ob_config_dict["OB_CURRENT_PERIMETER"]),
|
986
|
-
)
|
987
|
-
),
|
988
|
-
fg="red",
|
989
|
-
err=True,
|
990
|
-
)
|
991
|
-
show_current_perimeter_step.update(
|
992
|
-
status=OuterboundsCommandStatus.FAIL,
|
993
|
-
reason="Perimeter config file not found",
|
994
|
-
mitigation="Please make sure the perimeter config file exists at {}/config_{}.json".format(
|
995
|
-
metaflow_home_dir, ob_config_dict["OB_CURRENT_PERIMETER"]
|
996
|
-
),
|
997
|
-
)
|
998
|
-
show_current_perimeter_response.add_step(show_current_perimeter_step)
|
999
|
-
|
1000
|
-
if output == "json":
|
1001
|
-
click.echo(json.dumps(show_current_perimeter_response.as_dict(), indent=4))
|
1002
|
-
|
1003
|
-
|
1004
|
-
@cli.command(help="List all available perimeters")
|
1005
|
-
@click.option(
|
1006
|
-
"-o",
|
1007
|
-
"--output",
|
1008
|
-
default="",
|
1009
|
-
help="Show output in the specified format.",
|
1010
|
-
type=click.Choice(["json", ""]),
|
1011
|
-
)
|
1012
|
-
def list_perimeters(output=""):
|
1013
|
-
list_perimeters_response = OuterboundsCommandResponse()
|
1014
|
-
|
1015
|
-
list_perimeters_step = CommandStatus(
|
1016
|
-
"ListPerimeters", OuterboundsCommandStatus.OK, "Perimeter Fetch Successful."
|
1017
|
-
)
|
1018
|
-
|
1019
|
-
metaflow_home_dir = path.expanduser(
|
1020
|
-
os.environ.get("METAFLOW_HOME", "~/.metaflowconfig")
|
1021
|
-
)
|
1022
|
-
|
1023
|
-
# If OBP_CONFIG_DIR is set, use that, otherwise use METAFLOW_HOME
|
1024
|
-
# If neither are set, use ~/.metaflowconfig
|
1025
|
-
obp_config_dir = path.expanduser(
|
1026
|
-
os.environ.get(
|
1027
|
-
"OBP_CONFIG_DIR", os.environ.get("METAFLOW_HOME", "~/.metaflowconfig")
|
1028
|
-
)
|
1029
|
-
)
|
1030
|
-
|
1031
|
-
path_to_config = os.path.expanduser(os.path.join(obp_config_dir, "ob_config.json"))
|
1032
|
-
|
1033
|
-
if not os.path.exists(path_to_config):
|
1034
|
-
click.secho(
|
1035
|
-
"Config file not found at {}".format(path_to_config), fg="red", err=True
|
1036
|
-
)
|
1037
|
-
list_perimeters_step.update(
|
1038
|
-
status=OuterboundsCommandStatus.FAIL,
|
1039
|
-
reason="Config file not found",
|
1040
|
-
mitigation="Please make sure the config file exists at {}".format(
|
1041
|
-
path_to_config
|
1042
|
-
),
|
1043
|
-
)
|
1044
|
-
list_perimeters_response.add_step(list_perimeters_step)
|
1045
|
-
if output == "json":
|
1046
|
-
click.echo(json.dumps(list_perimeters_response.as_dict(), indent=4))
|
1047
|
-
return
|
1048
|
-
|
1049
|
-
with open(path_to_config, "r") as file:
|
1050
|
-
ob_config_dict = json.load(file)
|
1051
|
-
|
1052
|
-
if "OB_CURRENT_PERIMETER" not in ob_config_dict:
|
1053
|
-
click.secho(
|
1054
|
-
"OB_CURRENT_PERIMETER not found in Config file: {}".format(path_to_config),
|
1055
|
-
fg="red",
|
1056
|
-
err=True,
|
1057
|
-
)
|
1058
|
-
list_perimeters_step.update(
|
1059
|
-
status=OuterboundsCommandStatus.FAIL,
|
1060
|
-
reason="OB_CURRENT_PERIMETER not found in Config file: {}",
|
1061
|
-
mitigation="",
|
1062
|
-
)
|
1063
|
-
list_perimeters_response.add_step(list_perimeters_step)
|
1064
|
-
if output == "json":
|
1065
|
-
click.echo(json.dumps(list_perimeters_response.as_dict(), indent=4))
|
1066
|
-
return
|
1067
|
-
|
1068
|
-
if os.path.exists(
|
1069
|
-
os.path.join(
|
1070
|
-
metaflow_home_dir,
|
1071
|
-
"config_{}.json".format(ob_config_dict["OB_CURRENT_PERIMETER"]),
|
1072
|
-
)
|
1073
|
-
):
|
1074
|
-
active_perimeter = ob_config_dict["OB_CURRENT_PERIMETER"]
|
1075
|
-
else:
|
1076
|
-
click.secho(
|
1077
|
-
"Perimeter config file not found at {}".format(
|
1078
|
-
os.path.join(
|
1079
|
-
metaflow_home_dir,
|
1080
|
-
"config_{}.json".format(ob_config_dict["OB_CURRENT_PERIMETER"]),
|
1081
|
-
)
|
1082
|
-
),
|
1083
|
-
fg="red",
|
1084
|
-
err=True,
|
1085
|
-
)
|
1086
|
-
list_perimeters_step.update(
|
1087
|
-
status=OuterboundsCommandStatus.FAIL,
|
1088
|
-
reason="Perimeter config file not found",
|
1089
|
-
mitigation="Please make sure the perimeter config file exists at {}/config_{}.json".format(
|
1090
|
-
metaflow_home_dir, ob_config_dict["OB_CURRENT_PERIMETER"]
|
1091
|
-
),
|
1092
|
-
)
|
1093
|
-
list_perimeters_response.add_step(list_perimeters_step)
|
1094
|
-
if output == "json":
|
1095
|
-
click.echo(json.dumps(list_perimeters_response.as_dict(), indent=4))
|
1096
|
-
return
|
1097
|
-
|
1098
|
-
click.secho("Perimeter: {} (active)".format(active_perimeter), fg="green", err=True)
|
1099
|
-
|
1100
|
-
perimeter_list = []
|
1101
|
-
for file in os.listdir(metaflow_home_dir):
|
1102
|
-
if file.startswith("config_") and file.endswith(".json"):
|
1103
|
-
perimeter_id = file.split("_")[1].split(".")[0]
|
1104
|
-
perimeter_list.append(
|
1105
|
-
{"id": perimeter_id, "active": perimeter_id == active_perimeter}
|
1106
|
-
)
|
1107
|
-
|
1108
|
-
if perimeter_id != active_perimeter:
|
1109
|
-
click.secho("Perimeter: {}".format(perimeter_id), err=True)
|
1110
|
-
|
1111
|
-
list_perimeters_response.add_or_update_data("perimeters", perimeter_list)
|
1112
|
-
|
1113
|
-
if output == "json":
|
1114
|
-
click.echo(json.dumps(list_perimeters_response.as_dict(), indent=4))
|
@@ -0,0 +1,374 @@
|
|
1
|
+
import base64
|
2
|
+
import hashlib
|
3
|
+
import json
|
4
|
+
import os
|
5
|
+
import re
|
6
|
+
import subprocess
|
7
|
+
import sys
|
8
|
+
import zlib
|
9
|
+
from base64 import b64decode, b64encode
|
10
|
+
from importlib.machinery import PathFinder
|
11
|
+
from os import path
|
12
|
+
from pathlib import Path
|
13
|
+
from typing import Any, Callable, Dict, List
|
14
|
+
|
15
|
+
import boto3
|
16
|
+
import click
|
17
|
+
import requests
|
18
|
+
from requests.exceptions import HTTPError
|
19
|
+
|
20
|
+
from ..utils import kubeconfig, metaflowconfig
|
21
|
+
from ..utils.schema import (
|
22
|
+
CommandStatus,
|
23
|
+
OuterboundsCommandResponse,
|
24
|
+
OuterboundsCommandStatus,
|
25
|
+
)
|
26
|
+
|
27
|
+
|
28
|
+
@click.group()
|
29
|
+
def cli(**kwargs):
|
30
|
+
pass
|
31
|
+
|
32
|
+
|
33
|
+
@cli.command(help="Switch current perimeter", hidden=True)
|
34
|
+
@click.option(
|
35
|
+
"-d",
|
36
|
+
"--config-dir",
|
37
|
+
default=path.expanduser(os.environ.get("METAFLOW_HOME", "~/.metaflowconfig")),
|
38
|
+
help="Path to Metaflow configuration directory",
|
39
|
+
show_default=True,
|
40
|
+
)
|
41
|
+
@click.option(
|
42
|
+
"-p",
|
43
|
+
"--profile",
|
44
|
+
default=os.environ.get("METAFLOW_PROFILE", ""),
|
45
|
+
help="The named metaflow profile in which your workstation exists",
|
46
|
+
)
|
47
|
+
@click.option(
|
48
|
+
"-o",
|
49
|
+
"--output",
|
50
|
+
default="",
|
51
|
+
help="Show output in the specified format.",
|
52
|
+
type=click.Choice(["json", ""]),
|
53
|
+
)
|
54
|
+
@click.option("--id", default="", type=str, help="Perimeter name to switch to")
|
55
|
+
@click.option(
|
56
|
+
"-f",
|
57
|
+
"--force",
|
58
|
+
is_flag=True,
|
59
|
+
help="Force change the existing perimeter",
|
60
|
+
default=False,
|
61
|
+
)
|
62
|
+
def switch_perimeter(config_dir=None, profile=None, output="", id=None, force=False):
|
63
|
+
switch_perimeter_response = OuterboundsCommandResponse()
|
64
|
+
|
65
|
+
switch_perimeter_step = CommandStatus(
|
66
|
+
"SwitchPerimeter",
|
67
|
+
OuterboundsCommandStatus.OK,
|
68
|
+
"Perimeter was successfully switched!",
|
69
|
+
)
|
70
|
+
|
71
|
+
perimeters = get_perimeters_from_api_or_fail_command(
|
72
|
+
config_dir, profile, output, switch_perimeter_response, switch_perimeter_step
|
73
|
+
)
|
74
|
+
confirm_user_has_access_to_perimeter_or_fail(
|
75
|
+
id, perimeters, output, switch_perimeter_response, switch_perimeter_step
|
76
|
+
)
|
77
|
+
|
78
|
+
path_to_config = get_ob_config_file_path(config_dir, profile)
|
79
|
+
|
80
|
+
import fcntl
|
81
|
+
|
82
|
+
try:
|
83
|
+
if os.path.exists(path_to_config):
|
84
|
+
if not force:
|
85
|
+
fd = os.open(path_to_config, os.O_WRONLY)
|
86
|
+
# Try to acquire an exclusive lock
|
87
|
+
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
88
|
+
else:
|
89
|
+
click.secho(
|
90
|
+
"Force flag is set. Perimeter will be switched, but can have unintended consequences on other running processes.",
|
91
|
+
fg="yellow",
|
92
|
+
err=True,
|
93
|
+
)
|
94
|
+
|
95
|
+
ob_config_dict = {
|
96
|
+
"OB_CURRENT_PERIMETER": str(id),
|
97
|
+
"OB_CURRENT_PERIMETER_URL": perimeters[id]["remote_config_url"],
|
98
|
+
}
|
99
|
+
|
100
|
+
# Now that we have the lock, we can safely write to the file
|
101
|
+
with open(path_to_config, "w") as file:
|
102
|
+
json.dump(ob_config_dict, file, indent=4)
|
103
|
+
|
104
|
+
click.secho("Perimeter switched to {}".format(id), fg="green", err=True)
|
105
|
+
except BlockingIOError:
|
106
|
+
# This exception is raised if the file is already locked (non-blocking mode)
|
107
|
+
# Note that its the metaflow package (the extension actually) that acquires a shared read lock
|
108
|
+
# on the file whenever a process imports metaflow.
|
109
|
+
# In the future we might want to get smarter about it and show which process is holding the lock.
|
110
|
+
click.secho(
|
111
|
+
"Can't switch perimeter while Metaflow is in use. Please make sure there are no running python processes or notebooks using metaflow.",
|
112
|
+
fg="red",
|
113
|
+
err=True,
|
114
|
+
)
|
115
|
+
switch_perimeter_step.update(
|
116
|
+
status=OuterboundsCommandStatus.FAIL,
|
117
|
+
reason="Can't switch perimeter while Metaflow is in use.",
|
118
|
+
mitigation="Please make sure there are no running python processes or notebooks using metaflow.",
|
119
|
+
)
|
120
|
+
|
121
|
+
switch_perimeter_response.add_step(switch_perimeter_step)
|
122
|
+
if output == "json":
|
123
|
+
click.echo(json.dumps(switch_perimeter_response.as_dict(), indent=4))
|
124
|
+
return
|
125
|
+
|
126
|
+
|
127
|
+
@cli.command(help="Show current perimeter", hidden=True)
|
128
|
+
@click.option(
|
129
|
+
"-d",
|
130
|
+
"--config-dir",
|
131
|
+
default=path.expanduser(os.environ.get("METAFLOW_HOME", "~/.metaflowconfig")),
|
132
|
+
help="Path to Metaflow configuration directory",
|
133
|
+
show_default=True,
|
134
|
+
)
|
135
|
+
@click.option(
|
136
|
+
"-p",
|
137
|
+
"--profile",
|
138
|
+
default=os.environ.get("METAFLOW_PROFILE", ""),
|
139
|
+
help="Configure a named profile. Activate the profile by setting "
|
140
|
+
"`METAFLOW_PROFILE` environment variable.",
|
141
|
+
)
|
142
|
+
@click.option(
|
143
|
+
"-o",
|
144
|
+
"--output",
|
145
|
+
default="",
|
146
|
+
help="Show output in the specified format.",
|
147
|
+
type=click.Choice(["json", ""]),
|
148
|
+
)
|
149
|
+
def show_current_perimeter(config_dir=None, profile=None, output=""):
|
150
|
+
show_current_perimeter_response = OuterboundsCommandResponse()
|
151
|
+
|
152
|
+
show_current_perimeter_step = CommandStatus(
|
153
|
+
"ShowCurrentPerimeter",
|
154
|
+
OuterboundsCommandStatus.OK,
|
155
|
+
"Current Perimeter Fetch Successful.",
|
156
|
+
)
|
157
|
+
|
158
|
+
ob_config_dict = get_ob_config_or_fail_command(
|
159
|
+
config_dir,
|
160
|
+
profile,
|
161
|
+
output,
|
162
|
+
show_current_perimeter_response,
|
163
|
+
show_current_perimeter_step,
|
164
|
+
)
|
165
|
+
|
166
|
+
perimeters = get_perimeters_from_api_or_fail_command(
|
167
|
+
config_dir,
|
168
|
+
profile,
|
169
|
+
output,
|
170
|
+
show_current_perimeter_response,
|
171
|
+
show_current_perimeter_step,
|
172
|
+
)
|
173
|
+
confirm_user_has_access_to_perimeter_or_fail(
|
174
|
+
ob_config_dict["OB_CURRENT_PERIMETER"],
|
175
|
+
perimeters,
|
176
|
+
output,
|
177
|
+
show_current_perimeter_response,
|
178
|
+
show_current_perimeter_step,
|
179
|
+
)
|
180
|
+
|
181
|
+
click.secho(
|
182
|
+
"Current Perimeter: {}".format(ob_config_dict["OB_CURRENT_PERIMETER"]),
|
183
|
+
fg="green",
|
184
|
+
err=True,
|
185
|
+
)
|
186
|
+
|
187
|
+
show_current_perimeter_response.add_or_update_data(
|
188
|
+
"current_perimeter", ob_config_dict["OB_CURRENT_PERIMETER"]
|
189
|
+
)
|
190
|
+
|
191
|
+
if output == "json":
|
192
|
+
click.echo(json.dumps(show_current_perimeter_response.as_dict(), indent=4))
|
193
|
+
|
194
|
+
|
195
|
+
@cli.command(help="List all available perimeters", hidden=True)
|
196
|
+
@click.option(
|
197
|
+
"-d",
|
198
|
+
"--config-dir",
|
199
|
+
default=path.expanduser(os.environ.get("METAFLOW_HOME", "~/.metaflowconfig")),
|
200
|
+
help="Path to Metaflow configuration directory",
|
201
|
+
show_default=True,
|
202
|
+
)
|
203
|
+
@click.option(
|
204
|
+
"-p",
|
205
|
+
"--profile",
|
206
|
+
default=os.environ.get("METAFLOW_PROFILE", ""),
|
207
|
+
help="The named metaflow profile in which your workstation exists",
|
208
|
+
)
|
209
|
+
@click.option(
|
210
|
+
"-o",
|
211
|
+
"--output",
|
212
|
+
default="",
|
213
|
+
help="Show output in the specified format.",
|
214
|
+
type=click.Choice(["json", ""]),
|
215
|
+
)
|
216
|
+
def list_perimeters(config_dir=None, profile=None, output=""):
|
217
|
+
list_perimeters_response = OuterboundsCommandResponse()
|
218
|
+
|
219
|
+
list_perimeters_step = CommandStatus(
|
220
|
+
"ListPerimeters", OuterboundsCommandStatus.OK, "Perimeter Fetch Successful."
|
221
|
+
)
|
222
|
+
|
223
|
+
ob_config_dict = get_ob_config_or_fail_command(
|
224
|
+
config_dir, profile, output, list_perimeters_response, list_perimeters_step
|
225
|
+
)
|
226
|
+
active_perimeter = ob_config_dict["OB_CURRENT_PERIMETER"]
|
227
|
+
|
228
|
+
perimeters = get_perimeters_from_api_or_fail_command(
|
229
|
+
config_dir, profile, output, list_perimeters_response, list_perimeters_step
|
230
|
+
)
|
231
|
+
|
232
|
+
perimeter_list = []
|
233
|
+
for perimeter in perimeters.values():
|
234
|
+
status = "OK"
|
235
|
+
perimeter_list.append(
|
236
|
+
{
|
237
|
+
"id": perimeter["perimeter"],
|
238
|
+
"active": perimeter["perimeter"] == active_perimeter,
|
239
|
+
"status": status,
|
240
|
+
}
|
241
|
+
)
|
242
|
+
if perimeter["perimeter"] != active_perimeter:
|
243
|
+
click.secho("Perimeter: {}".format(perimeter["perimeter"]), err=True)
|
244
|
+
else:
|
245
|
+
click.secho(
|
246
|
+
"Perimeter: {} (active)".format(perimeter["perimeter"]),
|
247
|
+
fg="green",
|
248
|
+
err=True,
|
249
|
+
)
|
250
|
+
|
251
|
+
list_perimeters_response.add_or_update_data("perimeters", perimeter_list)
|
252
|
+
|
253
|
+
if output == "json":
|
254
|
+
click.echo(json.dumps(list_perimeters_response.as_dict(), indent=4))
|
255
|
+
|
256
|
+
|
257
|
+
def get_list_perimeters_api_response(config_dir, profile):
|
258
|
+
metaflow_token = metaflowconfig.get_metaflow_token_from_config(config_dir, profile)
|
259
|
+
api_url = metaflowconfig.get_sanitized_url_from_config(
|
260
|
+
config_dir, profile, "OBP_API_SERVER"
|
261
|
+
)
|
262
|
+
perimeters_response = requests.get(
|
263
|
+
f"{api_url}/v1/me/perimeters?privilege=Execute",
|
264
|
+
headers={"x-api-key": metaflow_token},
|
265
|
+
)
|
266
|
+
perimeters_response.raise_for_status()
|
267
|
+
return perimeters_response.json()["perimeters"]
|
268
|
+
|
269
|
+
|
270
|
+
def get_ob_config_file_path(config_dir: str, profile: str) -> str:
|
271
|
+
# If OBP_CONFIG_DIR is set, use that, otherwise use METAFLOW_HOME
|
272
|
+
# If neither are set, use ~/.metaflowconfig
|
273
|
+
obp_config_dir = path.expanduser(os.environ.get("OBP_CONFIG_DIR", config_dir))
|
274
|
+
|
275
|
+
ob_config_filename = f"ob_config_{profile}.json" if profile else "ob_config.json"
|
276
|
+
return os.path.expanduser(os.path.join(obp_config_dir, ob_config_filename))
|
277
|
+
|
278
|
+
|
279
|
+
def get_perimeters_from_api_or_fail_command(
|
280
|
+
config_dir: str,
|
281
|
+
profile: str,
|
282
|
+
output: str,
|
283
|
+
command_response: OuterboundsCommandResponse,
|
284
|
+
command_step: CommandStatus,
|
285
|
+
) -> Dict[str, Dict[str, str]]:
|
286
|
+
try:
|
287
|
+
perimeters = get_list_perimeters_api_response(config_dir, profile)
|
288
|
+
except:
|
289
|
+
click.secho(
|
290
|
+
"Failed to fetch perimeters from API.",
|
291
|
+
fg="red",
|
292
|
+
err=True,
|
293
|
+
)
|
294
|
+
command_step.update(
|
295
|
+
status=OuterboundsCommandStatus.FAIL,
|
296
|
+
reason="Failed to fetch perimeters from API",
|
297
|
+
mitigation="",
|
298
|
+
)
|
299
|
+
command_response.add_step(command_step)
|
300
|
+
if output == "json":
|
301
|
+
click.echo(json.dumps(command_response.as_dict(), indent=4))
|
302
|
+
sys.exit(1)
|
303
|
+
return {p["perimeter"]: p for p in perimeters}
|
304
|
+
|
305
|
+
|
306
|
+
def get_ob_config_or_fail_command(
|
307
|
+
config_dir: str,
|
308
|
+
profile: str,
|
309
|
+
output: str,
|
310
|
+
command_response: OuterboundsCommandResponse,
|
311
|
+
command_step: CommandStatus,
|
312
|
+
) -> Dict[str, str]:
|
313
|
+
path_to_config = get_ob_config_file_path(config_dir, profile)
|
314
|
+
|
315
|
+
if not os.path.exists(path_to_config):
|
316
|
+
click.secho(
|
317
|
+
"Config file not found at {}".format(path_to_config), fg="red", err=True
|
318
|
+
)
|
319
|
+
command_step.update(
|
320
|
+
status=OuterboundsCommandStatus.FAIL,
|
321
|
+
reason="Config file not found",
|
322
|
+
mitigation="Please make sure the config file exists at {}".format(
|
323
|
+
path_to_config
|
324
|
+
),
|
325
|
+
)
|
326
|
+
command_response.add_step(command_step)
|
327
|
+
if output == "json":
|
328
|
+
click.echo(json.dumps(command_response.as_dict(), indent=4))
|
329
|
+
sys.exit(1)
|
330
|
+
|
331
|
+
with open(path_to_config, "r") as file:
|
332
|
+
ob_config_dict = json.load(file)
|
333
|
+
|
334
|
+
if "OB_CURRENT_PERIMETER" not in ob_config_dict:
|
335
|
+
click.secho(
|
336
|
+
"OB_CURRENT_PERIMETER not found in Config file: {}".format(path_to_config),
|
337
|
+
fg="red",
|
338
|
+
err=True,
|
339
|
+
)
|
340
|
+
command_step.update(
|
341
|
+
status=OuterboundsCommandStatus.FAIL,
|
342
|
+
reason="OB_CURRENT_PERIMETER not found in Config file: {}",
|
343
|
+
mitigation="",
|
344
|
+
)
|
345
|
+
command_response.add_step(command_step)
|
346
|
+
if output == "json":
|
347
|
+
click.echo(json.dumps(command_response.as_dict(), indent=4))
|
348
|
+
sys.exit(1)
|
349
|
+
|
350
|
+
return ob_config_dict
|
351
|
+
|
352
|
+
|
353
|
+
def confirm_user_has_access_to_perimeter_or_fail(
|
354
|
+
perimeter_id: str,
|
355
|
+
perimeters: Dict[str, Any],
|
356
|
+
output: str,
|
357
|
+
command_response: OuterboundsCommandResponse,
|
358
|
+
command_step: CommandStatus,
|
359
|
+
):
|
360
|
+
if perimeter_id not in perimeters:
|
361
|
+
click.secho(
|
362
|
+
f"You do not have access to perimeter {perimeter_id} or it does not exist.",
|
363
|
+
fg="red",
|
364
|
+
err=True,
|
365
|
+
)
|
366
|
+
command_step.update(
|
367
|
+
status=OuterboundsCommandStatus.FAIL,
|
368
|
+
reason=f"You do not have access to perimeter {perimeter_id} or it does not exist.",
|
369
|
+
mitigation="",
|
370
|
+
)
|
371
|
+
command_response.add_step(command_step)
|
372
|
+
if output == "json":
|
373
|
+
click.echo(json.dumps(command_response.as_dict(), indent=4))
|
374
|
+
sys.exit(1)
|
@@ -1,54 +1,41 @@
|
|
1
1
|
import json
|
2
2
|
import os
|
3
|
+
import requests
|
3
4
|
from os import path
|
5
|
+
import requests
|
4
6
|
|
5
7
|
|
6
|
-
def init_config():
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
path_to_config = os.path.join(home, "config.json")
|
11
|
-
if profile:
|
12
|
-
path_to_config = os.path.join(home, "config_%s.json" % profile)
|
13
|
-
path_to_config = os.path.expanduser(path_to_config)
|
14
|
-
config = {}
|
15
|
-
if os.path.exists(path_to_config):
|
16
|
-
with open(path_to_config, encoding="utf-8") as f:
|
17
|
-
return json.load(f)
|
18
|
-
elif profile:
|
19
|
-
raise Exception(
|
20
|
-
"Unable to locate METAFLOW_PROFILE '%s' in '%s')" % (profile, home)
|
21
|
-
)
|
22
|
-
return config
|
23
|
-
|
24
|
-
|
25
|
-
def get_metaflow_profile():
|
26
|
-
# If OBP_CONFIG_DIR is set, use that, otherwise use METAFLOW_HOME
|
27
|
-
# If neither are set, use ~/.metaflowconfig
|
28
|
-
obp_config_dir = path.expanduser(
|
29
|
-
os.environ.get(
|
30
|
-
"OBP_CONFIG_DIR", os.environ.get("METAFLOW_HOME", "~/.metaflowconfig")
|
31
|
-
)
|
8
|
+
def init_config(config_dir="", profile="") -> dict:
|
9
|
+
profile = profile or os.environ.get("METAFLOW_PROFILE")
|
10
|
+
config_dir = config_dir or os.path.expanduser(
|
11
|
+
os.environ.get("METAFLOW_HOME", "~/.metaflowconfig")
|
32
12
|
)
|
33
13
|
|
34
|
-
|
14
|
+
config_filename = f"config_{profile}.json" if profile else "config.json"
|
15
|
+
path_to_config = os.path.join(config_dir, config_filename)
|
16
|
+
|
17
|
+
if os.path.exists(path_to_config):
|
18
|
+
with open(path_to_config, encoding="utf-8") as json_file:
|
19
|
+
config = json.load(json_file)
|
20
|
+
else:
|
21
|
+
raise Exception("Unable to locate metaflow config at '%s')" % (path_to_config))
|
35
22
|
|
36
|
-
|
37
|
-
|
38
|
-
|
23
|
+
# This is new remote-metaflow config; fetch it from the URL
|
24
|
+
if "OBP_METAFLOW_CONFIG_URL" in config:
|
25
|
+
if config is None or "METAFLOW_SERVICE_AUTH_KEY" not in config:
|
26
|
+
raise Exception("METAFLOW_SERVICE_AUTH_KEY not found in config file")
|
39
27
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
raise Exception(f"OB_CURRENT_PERIMETER not found in {file_path}")
|
44
|
-
elif "OBP_CONFIG_DIR" in os.environ:
|
45
|
-
raise Exception(
|
46
|
-
"OBP_CONFIG_DIR is set, but no ob_config.json found in that directory"
|
28
|
+
config_response = requests.get(
|
29
|
+
config["OBP_METAFLOW_CONFIG_URL"],
|
30
|
+
headers={"x-api-key": f'{config["METAFLOW_SERVICE_AUTH_KEY"]}'},
|
47
31
|
)
|
48
|
-
|
49
|
-
|
32
|
+
config_response.raise_for_status()
|
33
|
+
remote_config = config_response.json()["config"]
|
34
|
+
remote_config["METAFLOW_SERVICE_AUTH_KEY"] = config["METAFLOW_SERVICE_AUTH_KEY"]
|
35
|
+
return remote_config
|
50
36
|
|
51
|
-
|
37
|
+
# Legacy config, use from filesystem
|
38
|
+
return config
|
52
39
|
|
53
40
|
|
54
41
|
def get_metaflow_token_from_config(config_dir: str, profile: str) -> str:
|
@@ -59,13 +46,10 @@ def get_metaflow_token_from_config(config_dir: str, profile: str) -> str:
|
|
59
46
|
config_dir (str): Path to the config directory
|
60
47
|
profile (str): The named metaflow profile
|
61
48
|
"""
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
if config is None or "METAFLOW_SERVICE_AUTH_KEY" not in config:
|
67
|
-
raise Exception("METAFLOW_SERVICE_AUTH_KEY not found in config file")
|
68
|
-
return config["METAFLOW_SERVICE_AUTH_KEY"]
|
49
|
+
config = init_config(config_dir, profile)
|
50
|
+
if config is None or "METAFLOW_SERVICE_AUTH_KEY" not in config:
|
51
|
+
raise Exception("METAFLOW_SERVICE_AUTH_KEY not found in config file")
|
52
|
+
return config["METAFLOW_SERVICE_AUTH_KEY"]
|
69
53
|
|
70
54
|
|
71
55
|
def get_sanitized_url_from_config(config_dir: str, profile: str, key: str) -> str:
|
@@ -77,16 +61,12 @@ def get_sanitized_url_from_config(config_dir: str, profile: str, key: str) -> st
|
|
77
61
|
profile (str): The named metaflow profile
|
78
62
|
key (str): The key to look up in the config file
|
79
63
|
"""
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
url_in_config = f"https://{url_in_config}"
|
90
|
-
|
91
|
-
url_in_config = url_in_config.rstrip("/")
|
92
|
-
return url_in_config
|
64
|
+
config = init_config(config_dir, profile)
|
65
|
+
if key not in config:
|
66
|
+
raise Exception(f"Key {key} not found in config")
|
67
|
+
url_in_config = config[key]
|
68
|
+
if not url_in_config.startswith("https://"):
|
69
|
+
url_in_config = f"https://{url_in_config}"
|
70
|
+
|
71
|
+
url_in_config = url_in_config.rstrip("/")
|
72
|
+
return url_in_config
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: outerbounds
|
3
|
-
Version: 0.3.
|
3
|
+
Version: 0.3.53rc1
|
4
4
|
Summary: More Data Science, Less Administration
|
5
5
|
License: Proprietary
|
6
6
|
Keywords: data science,machine learning,MLOps
|
@@ -24,7 +24,7 @@ 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-storage (>=2.14.0,<3.0.0) ; extra == "gcp"
|
26
26
|
Requires-Dist: ob-metaflow (==2.11.0.4)
|
27
|
-
Requires-Dist: ob-metaflow-extensions (==1.1.
|
27
|
+
Requires-Dist: ob-metaflow-extensions (==1.1.43)
|
28
28
|
Requires-Dist: opentelemetry-distro (==0.41b0)
|
29
29
|
Requires-Dist: opentelemetry-exporter-otlp-proto-http (==1.20.0)
|
30
30
|
Requires-Dist: opentelemetry-instrumentation-requests (==0.41b0)
|
@@ -1,14 +1,15 @@
|
|
1
1
|
outerbounds/__init__.py,sha256=GPdaubvAYF8pOFWJ3b-sPMKCpyfpteWVMZWkmaYhxRw,32
|
2
2
|
outerbounds/cli_main.py,sha256=e9UMnPysmc7gbrimq2I4KfltggyU7pw59Cn9aEguVcU,74
|
3
3
|
outerbounds/command_groups/__init__.py,sha256=QPWtj5wDRTINDxVUL7XPqG3HoxHNvYOg08EnuSZB2Hc,21
|
4
|
-
outerbounds/command_groups/cli.py,sha256=
|
5
|
-
outerbounds/command_groups/local_setup_cli.py,sha256=
|
4
|
+
outerbounds/command_groups/cli.py,sha256=H4LxcYTmsY9DQUrReSRLjvbg9s9Ro7s-eUrcMqEJ_9A,261
|
5
|
+
outerbounds/command_groups/local_setup_cli.py,sha256=DwQYfTnK_kb63oQM0A8cAJeLRCTLOcyEwjkBL42O1fQ,29414
|
6
|
+
outerbounds/command_groups/perimeters_cli.py,sha256=9tOql42d00KfHpZYkLLGEAOiy8iRbIzsknldCyICwU0,12063
|
6
7
|
outerbounds/command_groups/workstations_cli.py,sha256=f3gwHMZPHzeOcGj5VfC5tZZA18JQhFzy2LRGzqAosOk,19286
|
7
8
|
outerbounds/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
8
9
|
outerbounds/utils/kubeconfig.py,sha256=l1mUP1j9VIq3fsffi5bJ1Nk-hYlwd1dIqkpj7DvVS1E,7936
|
9
|
-
outerbounds/utils/metaflowconfig.py,sha256=
|
10
|
+
outerbounds/utils/metaflowconfig.py,sha256=HgaDmK3F97rppfGUdysS1Zppe28ERTLV_HcB5IuPpV4,2631
|
10
11
|
outerbounds/utils/schema.py,sha256=Ht_Yf5uoKO0m36WXHZLSPmWPH6EFWXfZDQsiAUquc5k,2160
|
11
|
-
outerbounds-0.3.
|
12
|
-
outerbounds-0.3.
|
13
|
-
outerbounds-0.3.
|
14
|
-
outerbounds-0.3.
|
12
|
+
outerbounds-0.3.53rc1.dist-info/METADATA,sha256=uUkhPz_gBBdZGk4mA3B5PrwediF87wcbiMRxb9bPe_w,1364
|
13
|
+
outerbounds-0.3.53rc1.dist-info/WHEEL,sha256=vVCvjcmxuUltf8cYhJ0sJMRDLr1XsPuxEId8YDzbyCY,88
|
14
|
+
outerbounds-0.3.53rc1.dist-info/entry_points.txt,sha256=7ye0281PKlvqxu15rjw60zKg2pMsXI49_A8BmGqIqBw,47
|
15
|
+
outerbounds-0.3.53rc1.dist-info/RECORD,,
|
File without changes
|
File without changes
|