Add ConfigFile
This commit is contained in:
parent
ec74eb2d6f
commit
fcdd25c974
9 changed files with 153 additions and 0 deletions
|
|
@ -10,3 +10,4 @@ from .nlp import nlp
|
||||||
from .parallel_map import parallel_map
|
from .parallel_map import parallel_map
|
||||||
from .publication_tei import PublicationTEI
|
from .publication_tei import PublicationTEI
|
||||||
from .unique import unique
|
from .unique import unique
|
||||||
|
from .config_file import ConfigFile
|
||||||
|
|
|
||||||
2
great_ai/src/great_ai/utilities/config_file/__init__.py
Normal file
2
great_ai/src/great_ai/utilities/config_file/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
from .config_file import ConfigFile
|
||||||
|
from .parse_error import ParseError
|
||||||
52
great_ai/src/great_ai/utilities/config_file/config_file.py
Normal file
52
great_ai/src/great_ai/utilities/config_file/config_file.py
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
from pathlib import Path
|
||||||
|
import os
|
||||||
|
from typing import Dict, Union
|
||||||
|
|
||||||
|
from ..logger import get_logger
|
||||||
|
from .parse_error import ParseError
|
||||||
|
from .pattern import pattern
|
||||||
|
|
||||||
|
ENVIRONMENT_VARIABLE_KEY_PREFIX = 'ENV'
|
||||||
|
|
||||||
|
logger = get_logger('ConfigFile')
|
||||||
|
|
||||||
|
class ConfigFile:
|
||||||
|
def __init__(self, path: Union[Path, str], ignore_missing_environment_variables:bool=False) -> None:
|
||||||
|
if not isinstance(path, Path):
|
||||||
|
path = Path(path)
|
||||||
|
self._path = path
|
||||||
|
self._ignore_missing_environment_variables = ignore_missing_environment_variables
|
||||||
|
self._key_values: Dict[str, str] = {}
|
||||||
|
|
||||||
|
self._parse()
|
||||||
|
|
||||||
|
def _parse(self):
|
||||||
|
with open(self._path, encoding='utf-8') as f:
|
||||||
|
lines: str = f.read()
|
||||||
|
|
||||||
|
matches = pattern.findall(lines)
|
||||||
|
for key, *values in matches:
|
||||||
|
if key in self._key_values:
|
||||||
|
raise KeyError(f'Key `{key}` has been already defined and its value is `{value}`')
|
||||||
|
|
||||||
|
try:
|
||||||
|
value = next(v for v in values if v)
|
||||||
|
except StopIteration:
|
||||||
|
raise ParseError(f'Cannot parse config file ({self._path.absolute()}), error at key `{key}`')
|
||||||
|
|
||||||
|
if value.startswith(f'{ENVIRONMENT_VARIABLE_KEY_PREFIX}:'):
|
||||||
|
_, value = value.split(':')
|
||||||
|
if value not in os.environ:
|
||||||
|
issue = f'The value of `{key}` contains the "{ENVIRONMENT_VARIABLE_KEY_PREFIX}` prefix but `{value}` is not defined as an environment variable'
|
||||||
|
if self._ignore_missing_environment_variables:
|
||||||
|
logger.warning(f'{issue}, defaulting to `None`')
|
||||||
|
else:
|
||||||
|
raise KeyError(issue)
|
||||||
|
value = os.environ[value]
|
||||||
|
|
||||||
|
self._key_values[key] = value
|
||||||
|
|
||||||
|
def __getattr__(self, key: str) -> str:
|
||||||
|
if key in self._key_values:
|
||||||
|
return self._key_values[key]
|
||||||
|
raise KeyError(f'Key `{key}` is not found in configuration file ({self._path.absolute()})')
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
|
||||||
|
class ParseError(Exception):
|
||||||
|
pass
|
||||||
16
great_ai/src/great_ai/utilities/config_file/pattern.py
Normal file
16
great_ai/src/great_ai/utilities/config_file/pattern.py
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
pattern = re.compile(r'''
|
||||||
|
\s* # leading whitespace is allowed
|
||||||
|
(\w+?) # then comes the key
|
||||||
|
\s*=\s* # the key and value are separated by an equal sign
|
||||||
|
(?: # then comes the value
|
||||||
|
"([^"]*)" # the value can be surrounded by quotes: "value"
|
||||||
|
| '([^']*)' # the value can be surrounded by quotes: 'value'
|
||||||
|
| `([^`]*)` # the value can be surrounded by quotes: `value`
|
||||||
|
| ([^#\n]*?) # or it is bare, in that case, the trailing whitespace is ignored
|
||||||
|
)
|
||||||
|
\s*(?:\#.*)? # comments can be added with the `#` symbol
|
||||||
|
(?:\n|$) # a key-value pairs are separated by new lines
|
||||||
|
''', flags=re.UNICODE | re.VERBOSE)
|
||||||
3
great_ai/tests/utilities/data/bad.conf
Normal file
3
great_ai/tests/utilities/data/bad.conf
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
|
||||||
|
zeroth_key="test"
|
||||||
|
zeroth_key="test"
|
||||||
5
great_ai/tests/utilities/data/env.conf
Normal file
5
great_ai/tests/utilities/data/env.conf
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
|
||||||
|
first_key=test
|
||||||
|
|
||||||
|
second_key="ENV:alma"
|
||||||
|
third_key = ENV:alma
|
||||||
15
great_ai/tests/utilities/data/good.conf
Normal file
15
great_ai/tests/utilities/data/good.conf
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
|
||||||
|
zeroth_key="test"
|
||||||
|
first_key=test
|
||||||
|
second_key = test 2 # comment is ignored
|
||||||
|
third_key= "test= 2=="
|
||||||
|
|
||||||
|
fourth_key = "
|
||||||
|
this#
|
||||||
|
is
|
||||||
|
multiline
|
||||||
|
" # cannot write comments inside multiline
|
||||||
|
|
||||||
|
whitespace = hardly matters
|
||||||
|
|
||||||
|
|
||||||
56
great_ai/tests/utilities/test_config_file.py
Normal file
56
great_ai/tests/utilities/test_config_file.py
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
import pytest
|
||||||
|
from src.great_ai.utilities.config_file import ConfigFile
|
||||||
|
|
||||||
|
DATA_PATH = Path(__file__).parent.resolve() / "data"
|
||||||
|
|
||||||
|
|
||||||
|
class TestConfigFile(unittest.TestCase):
|
||||||
|
def test_simple(self) -> None:
|
||||||
|
c = ConfigFile(DATA_PATH / "good.conf")
|
||||||
|
assert c.zeroth_key == 'test'
|
||||||
|
assert c.first_key == 'test'
|
||||||
|
assert c.second_key == 'test 2'
|
||||||
|
assert c.third_key == 'test= 2=='
|
||||||
|
assert c.fourth_key == '''
|
||||||
|
this#
|
||||||
|
is
|
||||||
|
multiline
|
||||||
|
'''
|
||||||
|
assert c.whitespace == 'hardly matters'
|
||||||
|
|
||||||
|
|
||||||
|
def test_string_path(self) -> None:
|
||||||
|
c = ConfigFile(str(DATA_PATH / "good.conf"))
|
||||||
|
assert c.zeroth_key == 'test'
|
||||||
|
assert c.first_key == 'test'
|
||||||
|
assert c.second_key == 'test 2'
|
||||||
|
assert c.third_key == 'test= 2=='
|
||||||
|
assert c.fourth_key == '''
|
||||||
|
this#
|
||||||
|
is
|
||||||
|
multiline
|
||||||
|
'''
|
||||||
|
assert c.whitespace == 'hardly matters'
|
||||||
|
|
||||||
|
def test_env(self) -> None:
|
||||||
|
import os
|
||||||
|
os.environ['alma'] = '12'
|
||||||
|
c = ConfigFile(DATA_PATH / "env.conf")
|
||||||
|
del os.environ['alma']
|
||||||
|
assert c.first_key == 'test'
|
||||||
|
assert c.second_key == '12'
|
||||||
|
|
||||||
|
def test_env_not_exists(self) -> None:
|
||||||
|
with pytest.raises(KeyError):
|
||||||
|
ConfigFile(DATA_PATH / "env.conf")
|
||||||
|
|
||||||
|
def test_env_not_exists_but_ignored(self) -> None:
|
||||||
|
with pytest.raises(KeyError):
|
||||||
|
ConfigFile(DATA_PATH / "env.conf", ignore_missing_environment_variables=True)
|
||||||
|
|
||||||
|
def test_duplicate_key(self) -> None:
|
||||||
|
with pytest.raises(KeyError):
|
||||||
|
ConfigFile(DATA_PATH / "bad.conf")
|
||||||
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue