Skip to content

Commit d1c94e9

Browse files
authored
Merge pull request #51 from bcdev/forman-36-env_vars_in_config
Allow for env vars in config files
2 parents ce38ee7 + 880aae4 commit d1c94e9

File tree

3 files changed

+52
-7
lines changed

3 files changed

+52
-7
lines changed

CHANGES.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
### Enhancements
44

5+
* It is now possible to reference environment variables
6+
in configuration files using the syntax `${ENV_VAR}`. [#36]
7+
58
* Added a demo Notebook `exmaples/zappend-demo.ipynb` and linked
69
it by a binder badge in README.md. [#47]
710

tests/test_config.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
# https://opensource.org/licenses/MIT.
44

55
import json
6+
import os
67
import unittest
78

89
import fsspec
@@ -83,6 +84,40 @@ def test_normalize_yaml_uri(self):
8384
f.write(yaml.dump(config))
8485
self.assertEqual(config, normalize_config(uri))
8586

87+
def test_interpolate_env_vars_json(self):
88+
uri = "memory://config.json"
89+
config = {
90+
"slice_storage_options": {
91+
"key": "${_TEST_S3_KEY}",
92+
"secret": "$_TEST_S3_SECRET",
93+
}
94+
}
95+
with fsspec.open(uri, "wt") as f:
96+
f.write(json.dumps(config))
97+
os.environ["_TEST_S3_KEY"] = "abc"
98+
os.environ["_TEST_S3_SECRET"] = "123"
99+
self.assertEqual(
100+
{"slice_storage_options": {"key": "abc", "secret": "123"}},
101+
normalize_config(uri),
102+
)
103+
104+
def test_interpolate_env_vars_yaml(self):
105+
uri = "memory://config.yaml"
106+
config = {
107+
"slice_storage_options": {
108+
"key": "${_TEST_S3_KEY}",
109+
"secret": "$_TEST_S3_SECRET",
110+
}
111+
}
112+
with fsspec.open(uri, "w") as f:
113+
f.write(yaml.dump(config))
114+
os.environ["_TEST_S3_KEY"] = "abc"
115+
os.environ["_TEST_S3_SECRET"] = "123"
116+
self.assertEqual(
117+
{"slice_storage_options": {"key": "abc", "secret": 123}},
118+
normalize_config(uri),
119+
)
120+
86121
def test_normalize_file_obj(self):
87122
file_obj = FileObj("memory://config.yaml")
88123
config = {"version": 1, "zarr_version": 2}
@@ -233,6 +268,8 @@ def test_exclude_from_config(self):
233268
with exclude_from_config({"a": 1, "b": 2}, "b", "a") as config:
234269
self.assertEqual({}, config)
235270

271+
272+
class ConfigSchemaTest(unittest.TestCase):
236273
def test_get_config_schema(self):
237274
schema = get_config_schema()
238275
self.assertIn("properties", schema)

zappend/config/config.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22
# Permissions are hereby granted under the terms of the MIT License:
33
# https://opensource.org/licenses/MIT.
44

5-
from contextlib import contextmanager
5+
from typing import Any
6+
import contextlib
7+
import io
68
import json
79
import os.path
8-
from typing import Any
10+
import string
911

1012
import jsonschema
1113
import jsonschema.exceptions
@@ -91,10 +93,13 @@ def load_config(config_file: FileObj) -> dict[str, Any]:
9193
logger.info(f"Reading configuration {config_file.uri}")
9294
_, ext = os.path.splitext(config_file.path)
9395
with config_file.fs.open(config_file.path, "rt") as f:
94-
if ext in yaml_extensions:
95-
config = yaml.safe_load(f)
96-
else:
97-
config = json.load(f)
96+
source = f.read()
97+
source = string.Template(source).safe_substitute(os.environ)
98+
stream = io.StringIO(source)
99+
if ext in yaml_extensions:
100+
config = yaml.safe_load(stream)
101+
else:
102+
config = json.load(stream)
98103
if not isinstance(config, dict):
99104
raise TypeError(
100105
f"Invalid configuration:" f" {config_file.uri}: object expected"
@@ -138,6 +143,6 @@ def _merge_values(value_1: Any, value_2: Any) -> Any:
138143
return value_2
139144

140145

141-
@contextmanager
146+
@contextlib.contextmanager
142147
def exclude_from_config(config: dict[str, Any], *keys: str) -> dict[str, Any]:
143148
yield {k: v for k, v in config.items() if k not in keys}

0 commit comments

Comments
 (0)