outerbounds 0.3.68__py3-none-any.whl → 0.3.89__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. outerbounds/_vendor/PyYAML.LICENSE +20 -0
  2. outerbounds/_vendor/__init__.py +0 -0
  3. outerbounds/_vendor/_yaml/__init__.py +34 -0
  4. outerbounds/_vendor/click/__init__.py +73 -0
  5. outerbounds/_vendor/click/_compat.py +626 -0
  6. outerbounds/_vendor/click/_termui_impl.py +717 -0
  7. outerbounds/_vendor/click/_textwrap.py +49 -0
  8. outerbounds/_vendor/click/_winconsole.py +279 -0
  9. outerbounds/_vendor/click/core.py +2998 -0
  10. outerbounds/_vendor/click/decorators.py +497 -0
  11. outerbounds/_vendor/click/exceptions.py +287 -0
  12. outerbounds/_vendor/click/formatting.py +301 -0
  13. outerbounds/_vendor/click/globals.py +68 -0
  14. outerbounds/_vendor/click/parser.py +529 -0
  15. outerbounds/_vendor/click/py.typed +0 -0
  16. outerbounds/_vendor/click/shell_completion.py +580 -0
  17. outerbounds/_vendor/click/termui.py +787 -0
  18. outerbounds/_vendor/click/testing.py +479 -0
  19. outerbounds/_vendor/click/types.py +1073 -0
  20. outerbounds/_vendor/click/utils.py +580 -0
  21. outerbounds/_vendor/click.LICENSE +28 -0
  22. outerbounds/_vendor/vendor_any.txt +2 -0
  23. outerbounds/_vendor/yaml/__init__.py +471 -0
  24. outerbounds/_vendor/yaml/_yaml.cpython-311-darwin.so +0 -0
  25. outerbounds/_vendor/yaml/composer.py +146 -0
  26. outerbounds/_vendor/yaml/constructor.py +862 -0
  27. outerbounds/_vendor/yaml/cyaml.py +177 -0
  28. outerbounds/_vendor/yaml/dumper.py +138 -0
  29. outerbounds/_vendor/yaml/emitter.py +1239 -0
  30. outerbounds/_vendor/yaml/error.py +94 -0
  31. outerbounds/_vendor/yaml/events.py +104 -0
  32. outerbounds/_vendor/yaml/loader.py +62 -0
  33. outerbounds/_vendor/yaml/nodes.py +51 -0
  34. outerbounds/_vendor/yaml/parser.py +629 -0
  35. outerbounds/_vendor/yaml/reader.py +208 -0
  36. outerbounds/_vendor/yaml/representer.py +378 -0
  37. outerbounds/_vendor/yaml/resolver.py +245 -0
  38. outerbounds/_vendor/yaml/scanner.py +1555 -0
  39. outerbounds/_vendor/yaml/serializer.py +127 -0
  40. outerbounds/_vendor/yaml/tokens.py +129 -0
  41. outerbounds/command_groups/cli.py +1 -1
  42. outerbounds/command_groups/local_setup_cli.py +1 -5
  43. outerbounds/command_groups/perimeters_cli.py +135 -25
  44. outerbounds/command_groups/workstations_cli.py +2 -2
  45. outerbounds/utils/kubeconfig.py +2 -2
  46. outerbounds/utils/metaflowconfig.py +68 -9
  47. outerbounds/utils/utils.py +19 -0
  48. outerbounds/vendor.py +159 -0
  49. {outerbounds-0.3.68.dist-info → outerbounds-0.3.89.dist-info}/METADATA +13 -7
  50. outerbounds-0.3.89.dist-info/RECORD +57 -0
  51. outerbounds-0.3.68.dist-info/RECORD +0 -15
  52. {outerbounds-0.3.68.dist-info → outerbounds-0.3.89.dist-info}/WHEEL +0 -0
  53. {outerbounds-0.3.68.dist-info → outerbounds-0.3.89.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,127 @@
1
+ __all__ = ["Serializer", "SerializerError"]
2
+
3
+ from .error import YAMLError
4
+ from .events import *
5
+ from .nodes import *
6
+
7
+
8
+ class SerializerError(YAMLError):
9
+ pass
10
+
11
+
12
+ class Serializer:
13
+
14
+ ANCHOR_TEMPLATE = "id%03d"
15
+
16
+ def __init__(
17
+ self,
18
+ encoding=None,
19
+ explicit_start=None,
20
+ explicit_end=None,
21
+ version=None,
22
+ tags=None,
23
+ ):
24
+ self.use_encoding = encoding
25
+ self.use_explicit_start = explicit_start
26
+ self.use_explicit_end = explicit_end
27
+ self.use_version = version
28
+ self.use_tags = tags
29
+ self.serialized_nodes = {}
30
+ self.anchors = {}
31
+ self.last_anchor_id = 0
32
+ self.closed = None
33
+
34
+ def open(self):
35
+ if self.closed is None:
36
+ self.emit(StreamStartEvent(encoding=self.use_encoding))
37
+ self.closed = False
38
+ elif self.closed:
39
+ raise SerializerError("serializer is closed")
40
+ else:
41
+ raise SerializerError("serializer is already opened")
42
+
43
+ def close(self):
44
+ if self.closed is None:
45
+ raise SerializerError("serializer is not opened")
46
+ elif not self.closed:
47
+ self.emit(StreamEndEvent())
48
+ self.closed = True
49
+
50
+ # def __del__(self):
51
+ # self.close()
52
+
53
+ def serialize(self, node):
54
+ if self.closed is None:
55
+ raise SerializerError("serializer is not opened")
56
+ elif self.closed:
57
+ raise SerializerError("serializer is closed")
58
+ self.emit(
59
+ DocumentStartEvent(
60
+ explicit=self.use_explicit_start,
61
+ version=self.use_version,
62
+ tags=self.use_tags,
63
+ )
64
+ )
65
+ self.anchor_node(node)
66
+ self.serialize_node(node, None, None)
67
+ self.emit(DocumentEndEvent(explicit=self.use_explicit_end))
68
+ self.serialized_nodes = {}
69
+ self.anchors = {}
70
+ self.last_anchor_id = 0
71
+
72
+ def anchor_node(self, node):
73
+ if node in self.anchors:
74
+ if self.anchors[node] is None:
75
+ self.anchors[node] = self.generate_anchor(node)
76
+ else:
77
+ self.anchors[node] = None
78
+ if isinstance(node, SequenceNode):
79
+ for item in node.value:
80
+ self.anchor_node(item)
81
+ elif isinstance(node, MappingNode):
82
+ for key, value in node.value:
83
+ self.anchor_node(key)
84
+ self.anchor_node(value)
85
+
86
+ def generate_anchor(self, node):
87
+ self.last_anchor_id += 1
88
+ return self.ANCHOR_TEMPLATE % self.last_anchor_id
89
+
90
+ def serialize_node(self, node, parent, index):
91
+ alias = self.anchors[node]
92
+ if node in self.serialized_nodes:
93
+ self.emit(AliasEvent(alias))
94
+ else:
95
+ self.serialized_nodes[node] = True
96
+ self.descend_resolver(parent, index)
97
+ if isinstance(node, ScalarNode):
98
+ detected_tag = self.resolve(ScalarNode, node.value, (True, False))
99
+ default_tag = self.resolve(ScalarNode, node.value, (False, True))
100
+ implicit = (node.tag == detected_tag), (node.tag == default_tag)
101
+ self.emit(
102
+ ScalarEvent(alias, node.tag, implicit, node.value, style=node.style)
103
+ )
104
+ elif isinstance(node, SequenceNode):
105
+ implicit = node.tag == self.resolve(SequenceNode, node.value, True)
106
+ self.emit(
107
+ SequenceStartEvent(
108
+ alias, node.tag, implicit, flow_style=node.flow_style
109
+ )
110
+ )
111
+ index = 0
112
+ for item in node.value:
113
+ self.serialize_node(item, node, index)
114
+ index += 1
115
+ self.emit(SequenceEndEvent())
116
+ elif isinstance(node, MappingNode):
117
+ implicit = node.tag == self.resolve(MappingNode, node.value, True)
118
+ self.emit(
119
+ MappingStartEvent(
120
+ alias, node.tag, implicit, flow_style=node.flow_style
121
+ )
122
+ )
123
+ for key, value in node.value:
124
+ self.serialize_node(key, node, None)
125
+ self.serialize_node(value, node, key)
126
+ self.emit(MappingEndEvent())
127
+ self.ascend_resolver()
@@ -0,0 +1,129 @@
1
+ class Token(object):
2
+ def __init__(self, start_mark, end_mark):
3
+ self.start_mark = start_mark
4
+ self.end_mark = end_mark
5
+
6
+ def __repr__(self):
7
+ attributes = [key for key in self.__dict__ if not key.endswith("_mark")]
8
+ attributes.sort()
9
+ arguments = ", ".join(
10
+ ["%s=%r" % (key, getattr(self, key)) for key in attributes]
11
+ )
12
+ return "%s(%s)" % (self.__class__.__name__, arguments)
13
+
14
+
15
+ # class BOMToken(Token):
16
+ # id = '<byte order mark>'
17
+
18
+
19
+ class DirectiveToken(Token):
20
+ id = "<directive>"
21
+
22
+ def __init__(self, name, value, start_mark, end_mark):
23
+ self.name = name
24
+ self.value = value
25
+ self.start_mark = start_mark
26
+ self.end_mark = end_mark
27
+
28
+
29
+ class DocumentStartToken(Token):
30
+ id = "<document start>"
31
+
32
+
33
+ class DocumentEndToken(Token):
34
+ id = "<document end>"
35
+
36
+
37
+ class StreamStartToken(Token):
38
+ id = "<stream start>"
39
+
40
+ def __init__(self, start_mark=None, end_mark=None, encoding=None):
41
+ self.start_mark = start_mark
42
+ self.end_mark = end_mark
43
+ self.encoding = encoding
44
+
45
+
46
+ class StreamEndToken(Token):
47
+ id = "<stream end>"
48
+
49
+
50
+ class BlockSequenceStartToken(Token):
51
+ id = "<block sequence start>"
52
+
53
+
54
+ class BlockMappingStartToken(Token):
55
+ id = "<block mapping start>"
56
+
57
+
58
+ class BlockEndToken(Token):
59
+ id = "<block end>"
60
+
61
+
62
+ class FlowSequenceStartToken(Token):
63
+ id = "["
64
+
65
+
66
+ class FlowMappingStartToken(Token):
67
+ id = "{"
68
+
69
+
70
+ class FlowSequenceEndToken(Token):
71
+ id = "]"
72
+
73
+
74
+ class FlowMappingEndToken(Token):
75
+ id = "}"
76
+
77
+
78
+ class KeyToken(Token):
79
+ id = "?"
80
+
81
+
82
+ class ValueToken(Token):
83
+ id = ":"
84
+
85
+
86
+ class BlockEntryToken(Token):
87
+ id = "-"
88
+
89
+
90
+ class FlowEntryToken(Token):
91
+ id = ","
92
+
93
+
94
+ class AliasToken(Token):
95
+ id = "<alias>"
96
+
97
+ def __init__(self, value, start_mark, end_mark):
98
+ self.value = value
99
+ self.start_mark = start_mark
100
+ self.end_mark = end_mark
101
+
102
+
103
+ class AnchorToken(Token):
104
+ id = "<anchor>"
105
+
106
+ def __init__(self, value, start_mark, end_mark):
107
+ self.value = value
108
+ self.start_mark = start_mark
109
+ self.end_mark = end_mark
110
+
111
+
112
+ class TagToken(Token):
113
+ id = "<tag>"
114
+
115
+ def __init__(self, value, start_mark, end_mark):
116
+ self.value = value
117
+ self.start_mark = start_mark
118
+ self.end_mark = end_mark
119
+
120
+
121
+ class ScalarToken(Token):
122
+ id = "<scalar>"
123
+
124
+ def __init__(self, value, plain, start_mark, end_mark, style=None):
125
+ self.value = value
126
+ self.plain = plain
127
+ self.start_mark = start_mark
128
+ self.end_mark = end_mark
129
+ self.style = style
@@ -1,4 +1,4 @@
1
- import click
1
+ from outerbounds._vendor import click
2
2
  from . import local_setup_cli
3
3
  from . import workstations_cli
4
4
  from . import perimeters_cli
@@ -11,8 +11,7 @@ from importlib.machinery import PathFinder
11
11
  from os import path
12
12
  from pathlib import Path
13
13
  from typing import Any, Callable, Dict, List
14
-
15
- import click
14
+ from outerbounds._vendor import click
16
15
  import requests
17
16
  from requests.exceptions import HTTPError
18
17
 
@@ -641,7 +640,6 @@ class ConfigurationWriter:
641
640
  if config_type == "inline":
642
641
  if "OBP_PERIMETER" in self.decoded_config:
643
642
  self.selected_perimeter = self.decoded_config["OBP_PERIMETER"]
644
-
645
643
  if "OBP_METAFLOW_CONFIG_URL" in self.decoded_config:
646
644
  self.decoded_config = {
647
645
  "OBP_METAFLOW_CONFIG_URL": self.decoded_config[
@@ -702,8 +700,6 @@ class ConfigurationWriter:
702
700
  with open(config_path, "w") as fd:
703
701
  json.dump(self.existing, fd, indent=4)
704
702
 
705
- # Every time a config is initialized, we should also reset the corresponding ob_config[_profile].json
706
- remote_config = metaflowconfig.init_config(self.out_dir, self.profile)
707
703
  if self.selected_perimeter and "OBP_METAFLOW_CONFIG_URL" in self.decoded_config:
708
704
  with open(self.ob_config_path, "w") as fd:
709
705
  ob_config_dict = {
@@ -1,22 +1,13 @@
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 zlib
9
- from base64 import b64decode, b64encode
10
- from importlib.machinery import PathFinder
11
4
  from os import path
12
- from pathlib import Path
13
- from typing import Any, Callable, Dict, List
14
-
15
- import click
5
+ from typing import Any, Dict
6
+ from outerbounds._vendor import click
16
7
  import requests
17
- from requests.exceptions import HTTPError
18
8
 
19
- from ..utils import kubeconfig, metaflowconfig
9
+ from ..utils import metaflowconfig
10
+ from ..utils.utils import safe_write_to_disk
20
11
  from ..utils.schema import (
21
12
  CommandStatus,
22
13
  OuterboundsCommandResponse,
@@ -81,7 +72,7 @@ def switch(config_dir=None, profile=None, output="", id=None, force=False):
81
72
  id, perimeters, output, switch_perimeter_response, switch_perimeter_step
82
73
  )
83
74
 
84
- path_to_config = get_ob_config_file_path(config_dir, profile)
75
+ path_to_config = metaflowconfig.get_ob_config_file_path(config_dir, profile)
85
76
 
86
77
  import fcntl
87
78
 
@@ -125,9 +116,31 @@ def switch(config_dir=None, profile=None, output="", id=None, force=False):
125
116
  )
126
117
 
127
118
  switch_perimeter_response.add_step(switch_perimeter_step)
119
+
120
+ ensure_cloud_creds_step = CommandStatus(
121
+ "EnsureCloudCredentials",
122
+ OuterboundsCommandStatus.OK,
123
+ "Cloud credentials were successfully updated.",
124
+ )
125
+
126
+ try:
127
+ ensure_cloud_credentials_for_shell(config_dir, profile)
128
+ except:
129
+ click.secho(
130
+ "Failed to update cloud credentials.",
131
+ fg="red",
132
+ err=True,
133
+ )
134
+ ensure_cloud_creds_step.update(
135
+ status=OuterboundsCommandStatus.FAIL,
136
+ reason="Failed to update cloud credentials.",
137
+ mitigation="",
138
+ )
139
+
140
+ switch_perimeter_response.add_step(ensure_cloud_creds_step)
141
+
128
142
  if output == "json":
129
143
  click.echo(json.dumps(switch_perimeter_response.as_dict(), indent=4))
130
- return
131
144
 
132
145
 
133
146
  @perimeter.command(help="Show current perimeter")
@@ -276,6 +289,58 @@ def list(config_dir=None, profile=None, output=""):
276
289
  click.echo(json.dumps(list_perimeters_response.as_dict(), indent=4))
277
290
 
278
291
 
292
+ @perimeter.command(
293
+ help="Ensure credentials for cloud are synced with perimeter", hidden=True
294
+ )
295
+ @click.option(
296
+ "-d",
297
+ "--config-dir",
298
+ default=path.expanduser(os.environ.get("METAFLOW_HOME", "~/.metaflowconfig")),
299
+ help="Path to Metaflow configuration directory",
300
+ show_default=True,
301
+ )
302
+ @click.option(
303
+ "-p",
304
+ "--profile",
305
+ default=os.environ.get("METAFLOW_PROFILE", ""),
306
+ help="The named metaflow profile in which your workstation exists",
307
+ )
308
+ @click.option(
309
+ "-o",
310
+ "--output",
311
+ default="",
312
+ help="Show output in the specified format.",
313
+ type=click.Choice(["json", ""]),
314
+ )
315
+ def ensure_cloud_creds(config_dir=None, profile=None, output=""):
316
+ ensure_cloud_creds_step = CommandStatus(
317
+ "EnsureCloudCredentials",
318
+ OuterboundsCommandStatus.OK,
319
+ "Cloud credentials were successfully updated.",
320
+ )
321
+
322
+ ensure_cloud_creds_response = OuterboundsCommandResponse()
323
+
324
+ try:
325
+ ensure_cloud_credentials_for_shell(config_dir, profile)
326
+ click.secho("Cloud credentials updated successfully.", fg="green", err=True)
327
+ except:
328
+ click.secho(
329
+ "Failed to update cloud credentials.",
330
+ fg="red",
331
+ err=True,
332
+ )
333
+ ensure_cloud_creds_step.update(
334
+ status=OuterboundsCommandStatus.FAIL,
335
+ reason="Failed to update cloud credentials.",
336
+ mitigation="",
337
+ )
338
+
339
+ ensure_cloud_creds_response.add_step(ensure_cloud_creds_step)
340
+ if output == "json":
341
+ click.echo(json.dumps(ensure_cloud_creds_response.as_dict(), indent=4))
342
+
343
+
279
344
  def get_list_perimeters_api_response(config_dir, profile):
280
345
  metaflow_token = metaflowconfig.get_metaflow_token_from_config(config_dir, profile)
281
346
  api_url = metaflowconfig.get_sanitized_url_from_config(
@@ -289,15 +354,6 @@ def get_list_perimeters_api_response(config_dir, profile):
289
354
  return perimeters_response.json()["perimeters"]
290
355
 
291
356
 
292
- def get_ob_config_file_path(config_dir: str, profile: str) -> str:
293
- # If OBP_CONFIG_DIR is set, use that, otherwise use METAFLOW_HOME
294
- # If neither are set, use ~/.metaflowconfig
295
- obp_config_dir = path.expanduser(os.environ.get("OBP_CONFIG_DIR", config_dir))
296
-
297
- ob_config_filename = f"ob_config_{profile}.json" if profile else "ob_config.json"
298
- return os.path.expanduser(os.path.join(obp_config_dir, ob_config_filename))
299
-
300
-
301
357
  def get_perimeters_from_api_or_fail_command(
302
358
  config_dir: str,
303
359
  profile: str,
@@ -332,7 +388,7 @@ def get_ob_config_or_fail_command(
332
388
  command_response: OuterboundsCommandResponse,
333
389
  command_step: CommandStatus,
334
390
  ) -> Dict[str, str]:
335
- path_to_config = get_ob_config_file_path(config_dir, profile)
391
+ path_to_config = metaflowconfig.get_ob_config_file_path(config_dir, profile)
336
392
 
337
393
  if not os.path.exists(path_to_config):
338
394
  click.secho(
@@ -372,6 +428,20 @@ def get_ob_config_or_fail_command(
372
428
  return ob_config_dict
373
429
 
374
430
 
431
+ def ensure_cloud_credentials_for_shell(config_dir, profile):
432
+ if "WORKSTATION_ID" not in os.environ:
433
+ # Naive check to see if we're running in workstation. No need to ensure anything
434
+ # if this is not a workstation.
435
+ return
436
+
437
+ mf_config = metaflowconfig.init_config(config_dir, profile)
438
+
439
+ # Currently we only support GCP. TODO: utkarsh to add support for AWS and Azure
440
+ if "METAFLOW_DEFAULT_GCP_CLIENT_PROVIDER" in mf_config:
441
+ # This is a GCP deployment.
442
+ ensure_gcp_cloud_creds(config_dir, profile)
443
+
444
+
375
445
  def confirm_user_has_access_to_perimeter_or_fail(
376
446
  perimeter_id: str,
377
447
  perimeters: Dict[str, Any],
@@ -396,4 +466,44 @@ def confirm_user_has_access_to_perimeter_or_fail(
396
466
  sys.exit(1)
397
467
 
398
468
 
469
+ def ensure_gcp_cloud_creds(config_dir, profile):
470
+ token_info = get_gcp_auth_credentials(config_dir, profile)
471
+ auth_url = metaflowconfig.get_sanitized_url_from_config(
472
+ config_dir, profile, "OBP_AUTH_SERVER"
473
+ )
474
+ metaflow_token = metaflowconfig.get_metaflow_token_from_config(config_dir, profile)
475
+
476
+ # GOOGLE_APPLICATION_CREDENTIALS is a well known gcloud environment variable
477
+ credentials_file_loc = os.environ["GOOGLE_APPLICATION_CREDENTIALS"]
478
+
479
+ credentials_json = {
480
+ "type": "external_account",
481
+ "audience": f"//iam.googleapis.com/projects/{token_info['gcpProjectNumber']}/locations/global/workloadIdentityPools/{token_info['gcpWorkloadIdentityPool']}/providers/{token_info['gcpWorkloadIdentityPoolProvider']}",
482
+ "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
483
+ "token_url": "https://sts.googleapis.com/v1/token",
484
+ "service_account_impersonation_url": f"https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/{token_info['gcpServiceAccountEmail']}:generateAccessToken",
485
+ "credential_source": {
486
+ "url": f"{auth_url}/generate/gcp",
487
+ "headers": {"x-api-key": metaflow_token},
488
+ "format": {"type": "json", "subject_token_field_name": "token"},
489
+ },
490
+ }
491
+
492
+ safe_write_to_disk(credentials_file_loc, json.dumps(credentials_json))
493
+
494
+
495
+ def get_gcp_auth_credentials(config_dir, profile):
496
+ token = metaflowconfig.get_metaflow_token_from_config(config_dir, profile)
497
+ auth_server_url = metaflowconfig.get_sanitized_url_from_config(
498
+ config_dir, profile, "OBP_AUTH_SERVER"
499
+ )
500
+
501
+ response = requests.get(
502
+ "{}/generate/gcp".format(auth_server_url), headers={"x-api-key": token}
503
+ )
504
+ response.raise_for_status()
505
+
506
+ return response.json()
507
+
508
+
399
509
  cli.add_command(perimeter, name="perimeter")
@@ -1,5 +1,5 @@
1
- import click
2
- import yaml
1
+ from outerbounds._vendor import click
2
+ from outerbounds._vendor import yaml
3
3
  import requests
4
4
  import base64
5
5
  import datetime
@@ -1,6 +1,6 @@
1
1
  import os
2
- import yaml
3
- from yaml.scanner import ScannerError
2
+ from outerbounds._vendor import yaml
3
+ from outerbounds._vendor.yaml.scanner import ScannerError
4
4
  import subprocess
5
5
  from os import path
6
6
  import platform
@@ -1,23 +1,45 @@
1
- import click
1
+ from outerbounds._vendor import click
2
2
  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
- # This is new remote-metaflow config; fetch it from the URL
14
- if "OBP_METAFLOW_CONFIG_URL" in config:
15
- remote_config = init_config_from_url(
16
- config_dir, profile, config["OBP_METAFLOW_CONFIG_URL"]
17
- )
18
- remote_config["OBP_METAFLOW_CONFIG_URL"] = config["OBP_METAFLOW_CONFIG_URL"]
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
- # Legacy config, use from filesystem
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)