158 lines
5.4 KiB
Python
158 lines
5.4 KiB
Python
import os
|
|
from pathlib import Path
|
|
from typing import Dict, ItemsView, Iterator, KeysView, Mapping, Union, ValuesView
|
|
|
|
from ..logger.get_logger import get_logger
|
|
from .parse_error import ParseError
|
|
from .pattern import pattern
|
|
|
|
logger = get_logger("ConfigFile")
|
|
|
|
|
|
class ConfigFile(Mapping[str, str]):
|
|
"""A small and safe `INI`-style configuration loader with `dict` and `ENV` support.
|
|
|
|
The values can be accessed using both dot- and index-notation. It is compatible
|
|
with the `dict` interface.
|
|
|
|
File format example:
|
|
|
|
```toml
|
|
# comments are allowed everywhere
|
|
|
|
key = value # you can leave or omit whitespace around the equal-sign
|
|
my_hashtag = "#great_ai" # the r-value can be quoted with " or ' or `.
|
|
|
|
my_var = my_default_value # Default values can be given to env-vars,
|
|
# see next line. The default value must come first.
|
|
|
|
my_var = ENV:MY_ENV_VAR # If the value starts with the `ENV:` prefix,
|
|
# it is looked up from the environment variables.
|
|
```
|
|
|
|
Examples:
|
|
>>> ConfigFile('tests/utilities/data/simple.conf')
|
|
ConfigFile(path=tests/utilities/data/simple.conf) {'zeroth_key': 'test', 'first_key': 'András'}
|
|
|
|
>>> ConfigFile('tests/utilities/data/simple.conf').zeroth_key
|
|
'test'
|
|
|
|
>>> ConfigFile('tests/utilities/data/simple.conf').second_key
|
|
Traceback (most recent call last):
|
|
...
|
|
KeyError: 'Key `second_key` is not found in configuration file ...
|
|
|
|
>>> a = ConfigFile('tests/utilities/data/simple.conf')
|
|
>>> {**a}
|
|
{'zeroth_key': 'test', 'first_key': 'András'}
|
|
|
|
"""
|
|
|
|
ENVIRONMENT_VARIABLE_KEY_PREFIX = "ENV"
|
|
|
|
def __init__(self, path: Union[Path, str], *, ignore_missing: bool = False) -> None:
|
|
"""Load and parse a configuration file.
|
|
|
|
Everything is eager-loaded, thus, exceptions may be thrown here.
|
|
|
|
Args:
|
|
path: Local path of the configuration file.
|
|
ignore_missing: Don't raise an exception on missing environment variables.
|
|
|
|
Raises:
|
|
FileNotFoundError: If there is no file at the specified path.
|
|
ParseError: If the provided file does not conform to the expected format.
|
|
KeyError: If there is duplication in the keys.
|
|
ValueError: If an environment variable is referenced but it is not set in
|
|
the system and `ignore_missing=False`.
|
|
"""
|
|
|
|
if not isinstance(path, Path):
|
|
path = Path(path)
|
|
|
|
if not path.exists():
|
|
raise FileNotFoundError(path.absolute())
|
|
|
|
self._ignore_missing = ignore_missing
|
|
|
|
self._path = path
|
|
self._key_values: Dict[str, str] = {}
|
|
|
|
self._parse()
|
|
|
|
@property
|
|
def path(self) -> Path:
|
|
"""Original path from where the configuration was loaded."""
|
|
return self._path
|
|
|
|
def _parse(self) -> None:
|
|
with open(self._path, encoding="utf-8") as f:
|
|
lines: str = f.read()
|
|
|
|
matches = pattern.findall(lines)
|
|
for key, *values in matches:
|
|
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}`"
|
|
)
|
|
|
|
already_exists = key in self._key_values
|
|
if already_exists and not value.startswith(
|
|
f"{self.ENVIRONMENT_VARIABLE_KEY_PREFIX}:"
|
|
):
|
|
raise KeyError(
|
|
f"Key `{key}` has been already defined and its value is `{self._key_values[key]}`"
|
|
)
|
|
|
|
if value.startswith(f"{self.ENVIRONMENT_VARIABLE_KEY_PREFIX}:"):
|
|
_, value = value.split(":")
|
|
if value not in os.environ:
|
|
issue = f"""The value of `{key}` contains the "{
|
|
self.ENVIRONMENT_VARIABLE_KEY_PREFIX
|
|
}` prefix but `{value}` is not defined as an environment variable"""
|
|
if already_exists:
|
|
logger.warning(
|
|
f"""{issue}, using the default value defined above (`{
|
|
self._key_values[key]
|
|
}`)"""
|
|
)
|
|
continue
|
|
elif self._ignore_missing:
|
|
logger.warning(issue)
|
|
else:
|
|
raise ValueError(
|
|
f"{issue} and no default value has been provided"
|
|
)
|
|
else:
|
|
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()})"
|
|
)
|
|
|
|
__getitem__ = __getattr__
|
|
|
|
def __iter__(self) -> Iterator[str]:
|
|
return iter(self._key_values)
|
|
|
|
def __len__(self) -> int:
|
|
return len(self._key_values)
|
|
|
|
def keys(self) -> KeysView[str]:
|
|
return self._key_values.keys()
|
|
|
|
def values(self) -> ValuesView[str]:
|
|
return self._key_values.values()
|
|
|
|
def items(self) -> ItemsView[str, str]:
|
|
return self._key_values.items()
|
|
|
|
def __repr__(self) -> str:
|
|
return f"{type(self).__name__}(path={self._path}) {self._key_values}"
|