Initial
This commit is contained in:
commit
c87b0b7d16
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
venv
|
||||||
|
__pycache__
|
||||||
|
.idea
|
||||||
3
README.md
Normal file
3
README.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# kissconfig
|
||||||
|
|
||||||
|
Simple configuration based on dataclasses and typing
|
||||||
21
kissconfig/__init__.py
Normal file
21
kissconfig/__init__.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
from typing import Type
|
||||||
|
|
||||||
|
from kissconfig.config import ConfigClass
|
||||||
|
from kissconfig.loader import Loader, YamlLoader
|
||||||
|
|
||||||
|
|
||||||
|
def load_config(main_config: Type[ConfigClass], loader: Loader = YamlLoader()):
|
||||||
|
if not os.path.exists(config_path):
|
||||||
|
logging.warning(f"Unable to locate app config ({config_path})!")
|
||||||
|
if not os.path.exists(example_config_path):
|
||||||
|
logging.critical(f"Unable to locate example config ({example_config_path})")
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
shutil.copy2(example_config_path, config_path)
|
||||||
|
logging.warning(f"Actual app config (%s) created from example (%s), don't forget to update it!",
|
||||||
|
config_path, example_config_path)
|
||||||
|
return main_config.from_file(config_path, config_namespace)
|
||||||
46
kissconfig/config.py
Normal file
46
kissconfig/config.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
from typing import Any, get_args, get_origin, get_type_hints
|
||||||
|
from dataclasses import is_dataclass, fields, MISSING
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigClass:
|
||||||
|
@staticmethod
|
||||||
|
def parse(data: Any, type_hint):
|
||||||
|
origin = get_origin(type_hint)
|
||||||
|
if origin is list:
|
||||||
|
result = list()
|
||||||
|
for value in data:
|
||||||
|
result.append(ConfigClass.parse(value, get_args(type_hint)[0]))
|
||||||
|
elif origin is dict:
|
||||||
|
result = dict()
|
||||||
|
for key, value in data.items():
|
||||||
|
result[key] = ConfigClass.parse(value, get_args(type_hint)[1])
|
||||||
|
elif origin is None and is_dataclass(type_hint):
|
||||||
|
if issubclass(type_hint, ConfigClass):
|
||||||
|
result = type_hint.from_dict(data)
|
||||||
|
else:
|
||||||
|
raise TypeError(f"Nested dataclass {type_hint}) is not subclass of ConfigClass")
|
||||||
|
else:
|
||||||
|
result = data
|
||||||
|
return result
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, data: dict):
|
||||||
|
kwargs = {}
|
||||||
|
hints = get_type_hints(cls)
|
||||||
|
for f in fields(cls):
|
||||||
|
if f.name not in data:
|
||||||
|
if f.default != MISSING or f.default_factory != MISSING:
|
||||||
|
continue
|
||||||
|
raise ValueError(f"{cls.__name__}.{f.name} is not configured!")
|
||||||
|
kwargs[f.name] = ConfigClass.parse(data[f.name], hints[f.name])
|
||||||
|
return cls(**kwargs)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_file(cls, filepath: str, config_namespace: str = ""):
|
||||||
|
with open(filepath) as f:
|
||||||
|
data = safe_load(f)
|
||||||
|
if data is None:
|
||||||
|
raise ValueError("")
|
||||||
|
if config_namespace:
|
||||||
|
data = data.get(config_namespace, {})
|
||||||
|
return cls.from_dict(data)
|
||||||
34
kissconfig/loader.py
Normal file
34
kissconfig/loader.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
from abc import ABCMeta, abstractmethod
|
||||||
|
from typing import Type
|
||||||
|
|
||||||
|
from yaml import safe_load
|
||||||
|
|
||||||
|
from kissconfig.config import ConfigClass
|
||||||
|
|
||||||
|
|
||||||
|
class EmptyConfig (ValueError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Loader (metaclass=ABCMeta):
|
||||||
|
def __init__(self, path: str = None,
|
||||||
|
example_path: str = None):
|
||||||
|
self.path = path
|
||||||
|
self.example_path = example_path
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def parse_file(self, path: str) -> dict:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def from_file(self, config_class: Type[ConfigClass], path: str) -> ConfigClass:
|
||||||
|
data = self.parse_file(path)
|
||||||
|
if not data:
|
||||||
|
raise EmptyConfig("Configuration file seems to be empty")
|
||||||
|
if self.namespace is not None:
|
||||||
|
data = data.get(self.namespace, {})
|
||||||
|
return config_class.from_dict(data)
|
||||||
|
|
||||||
|
|
||||||
|
class YamlLoader (Loader):
|
||||||
|
def parse_file(self, path: str) -> dict:
|
||||||
|
return safe_load(path)
|
||||||
1
requirements.txt
Normal file
1
requirements.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
pyyaml
|
||||||
Loading…
x
Reference in New Issue
Block a user