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