Skip to content

Commit c2ffee0

Browse files
authored
User-defined Custom Config (#267)
* User-defined Custom Config * Dict[str, Any] structure added * Fixed environment variable substitution pattern * Unit tests added * User-defined Custom Config * default_factory instead of Optional
1 parent 8b680fb commit c2ffee0

File tree

2 files changed

+117
-6
lines changed

2 files changed

+117
-6
lines changed

src/dipdup/config.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
from dipdup.utils import pascal_to_snake
5555
from dipdup.utils import snake_to_pascal
5656

57-
ENV_VARIABLE_REGEX = r'\${([\w]*):-(.*)}' # ${VARIABLE:-default}
57+
ENV_VARIABLE_REGEX = r'\$\{(?P<var_name>[\w]+)(?:\:\-(?P<default_value>.*))?\}' # ${VARIABLE:-default} | ${VARIABLE}
5858
DEFAULT_RETRY_COUNT = 3
5959
DEFAULT_RETRY_SLEEP = 1
6060
DEFAULT_METADATA_URL = 'https://metadata.dipdup.net'
@@ -983,6 +983,7 @@ class DipDupConfig:
983983
:param jobs: Mapping of job aliases and job configs
984984
:param sentry: Sentry integration config
985985
:param advanced: Advanced config
986+
:param custom: User-defined Custom config
986987
"""
987988

988989
spec_version: str
@@ -998,6 +999,7 @@ class DipDupConfig:
998999
sentry: Optional[SentryConfig] = None
9991000
prometheus: Optional[PrometheusConfig] = None
10001001
advanced: AdvancedConfig = AdvancedConfig()
1002+
custom: Dict[str, Any] = field(default_factory=dict)
10011003

10021004
def __post_init_post_parse__(self):
10031005
self.paths: List[str] = []
@@ -1058,12 +1060,12 @@ def load(
10581060
if environment:
10591061
_logger.debug('Substituting environment variables')
10601062
for match in re.finditer(ENV_VARIABLE_REGEX, raw_config):
1061-
variable, default_value = match.group(1), match.group(2)
1062-
config_environment[variable] = default_value
1063-
value = env.get(variable)
1064-
if not default_value and not value:
1063+
variable, default_value = match.group('var_name'), match.group('default_value')
1064+
value = env.get(variable, default_value)
1065+
if not value:
10651066
raise ConfigurationError(f'Environment variable `{variable}` is not set')
1066-
placeholder = '${' + variable + ':-' + default_value + '}'
1067+
config_environment[variable] = value
1068+
placeholder = match.group(0)
10671069
raw_config = raw_config.replace(placeholder, value or default_value)
10681070

10691071
json_config.update(yaml.load(raw_config))
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import os
2+
from os.path import dirname
3+
from os.path import join
4+
5+
import pytest
6+
from _pytest.tmpdir import TempPathFactory
7+
8+
from dipdup.config import DipDupConfig
9+
from dipdup.exceptions import ConfigurationError
10+
11+
12+
class TestCustomConfig:
13+
@pytest.fixture(scope="session")
14+
def dummy_config_path(self) -> str:
15+
return join(dirname(dirname(__file__)), 'dipdup.yml')
16+
17+
@staticmethod
18+
def appended_config_path(dummy_config_path: str, tmp_path_factory: TempPathFactory, append_raw: str) -> str:
19+
config_file = tmp_path_factory.mktemp('config') / 'dipdup.yml'
20+
with open(dummy_config_path, 'r') as f:
21+
config_raw = f.read()
22+
23+
with open(config_file, 'a') as f:
24+
f.write(config_raw)
25+
f.write(append_raw)
26+
27+
return str(config_file)
28+
29+
@pytest.fixture(
30+
scope="session",
31+
params=(
32+
[
33+
"""
34+
custom:
35+
foo: bar
36+
spam:
37+
- eggs
38+
- rice
39+
40+
"""
41+
]
42+
),
43+
)
44+
def config_with_custom_section_path(self, dummy_config_path: str, tmp_path_factory: TempPathFactory, request) -> str:
45+
return self.appended_config_path(dummy_config_path, tmp_path_factory, request.param)
46+
47+
@staticmethod
48+
def test_empty_custom_section(dummy_config_path: str):
49+
config = DipDupConfig.load([dummy_config_path], False)
50+
config.initialize(True)
51+
assert hasattr(config, 'custom')
52+
assert config.custom == {}
53+
54+
@staticmethod
55+
def test_custom_section_items(config_with_custom_section_path: str):
56+
config = DipDupConfig.load([config_with_custom_section_path], False)
57+
config.initialize(True)
58+
59+
assert hasattr(config, 'custom')
60+
assert isinstance(config.custom, dict)
61+
62+
assert config.custom['foo'] == 'bar'
63+
64+
spam = config.custom['spam']
65+
assert isinstance(spam, list)
66+
assert 'eggs' in spam
67+
assert 'rice' in spam
68+
69+
@pytest.mark.parametrize(
70+
'value, expected',
71+
(
72+
('${USER:-dipdup}', os.environ.get('USER')),
73+
('${USER:-}', os.environ.get('USER')),
74+
('${USER}', os.environ.get('USER')),
75+
('${DEFINITELY_NOT_DEFINED:-default_value}', 'default_value'),
76+
('${DEFINITELY_NOT_DEFINED:- some_spaces_is_ok }', 'some_spaces_is_ok'),
77+
),
78+
)
79+
def test_env_parsing_positive(self, value, expected, dummy_config_path, tmp_path_factory):
80+
append_raw = f"""
81+
custom:
82+
var_from_env: {value}
83+
"""
84+
config_path = self.appended_config_path(dummy_config_path, tmp_path_factory, append_raw)
85+
config = DipDupConfig.load([config_path], True)
86+
config.initialize(True)
87+
88+
assert hasattr(config, 'custom')
89+
assert isinstance(config.custom, dict)
90+
91+
assert config.custom['var_from_env'] == expected
92+
93+
@pytest.mark.parametrize(
94+
'value',
95+
(
96+
'${DEFINITELY_NOT_DEFINED}',
97+
'${DEFINITELY_NOT_DEFINED:-}',
98+
),
99+
)
100+
def test_env_parsing_negative(self, value, dummy_config_path, tmp_path_factory):
101+
append_raw = f"""
102+
custom:
103+
var_from_env: {value}
104+
"""
105+
config_path = self.appended_config_path(dummy_config_path, tmp_path_factory, append_raw)
106+
with pytest.raises(ConfigurationError) as exception_info:
107+
DipDupConfig.load([config_path], True)
108+
109+
assert str(exception_info.value) == 'Environment variable `DEFINITELY_NOT_DEFINED` is not set'

0 commit comments

Comments
 (0)