diff options
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | README.md | 39 | ||||
-rw-r--r-- | configlib/__init__.py | 6 | ||||
-rw-r--r-- | configlib/model.py | 41 | ||||
-rw-r--r-- | configlib/model_impl.py | 59 | ||||
-rw-r--r-- | configlib/util.py | 18 | ||||
-rw-r--r-- | configlib/version.py | 16 | ||||
-rw-r--r-- | requirements.txt | 0 | ||||
-rw-r--r-- | setup.py | 33 | ||||
-rw-r--r-- | tests/__init__.py | 7 | ||||
-rw-r--r-- | tests/test_something.py | 39 |
11 files changed, 260 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4d93ca8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +configlib.egg-info +venv/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..62d8bd8 --- /dev/null +++ b/README.md @@ -0,0 +1,39 @@ +Configlib +========= + +An easy python config library. Manage multiple configuration environments and complex nested configurations with ease. + +Examples +-------- +```json +{ + "config_var": "jo", + "other_var": 3, + "SubConfig": { + "subconfivar": 10 + } +} +``` + + +```py +from configlib import BaseConfig + + +class Config(BaseConfig): + config_var: str + other_var: int + class SubConfig: + subconfigvar: int + +# Usage + +config = Config.get_instance() +config.config_var # "jo" +config.SubConfig.subconfigvar # 10 +``` + +Other Features +-------------- + +Having multiple config environments inheriting properties from each other, loading data from environment variables, etc.
\ No newline at end of file diff --git a/configlib/__init__.py b/configlib/__init__.py new file mode 100644 index 0000000..c0f25aa --- /dev/null +++ b/configlib/__init__.py @@ -0,0 +1,6 @@ +from .model import Config, ConfigValueMissingException, InvalidConfigTypingException +from .model_impl import BaseConfig +from .version import VersionInfo, version + +__all__ = ['ConfigValueMissingException', 'Config', 'InvalidConfigTypingException', + 'BaseConfig', 'VersionInfo', 'version'] diff --git a/configlib/model.py b/configlib/model.py new file mode 100644 index 0000000..6d79464 --- /dev/null +++ b/configlib/model.py @@ -0,0 +1,41 @@ +import json +from abc import abstractmethod, ABC +from typing import TypeVar, Type, Union, AnyStr, TextIO + +_T = TypeVar('_T', bound='Config') + + +class InvalidConfigTypingException(Exception): + pass + + +class ConfigValueMissingException(Exception): + pass + + +class Config(ABC): + + @classmethod + @abstractmethod + def get_name(cls) -> str: + pass + + @classmethod + @abstractmethod + def parse_dict(cls: Type[_T], data: dict) -> _T: + pass + + @classmethod + def load(cls: Type[_T], file: Union[AnyStr, TextIO]) -> _T: + if hasattr(file, 'read'): + return cls.loads(file.read()) + with open(file) as fp: + return cls.load(fp) + + @classmethod + def loads(cls: Type[_T], text: str) -> _T: + return cls.parse_dict(json.loads(text)) + + @classmethod + def get_instance(cls: Type[_T]) -> _T: + return cls diff --git a/configlib/model_impl.py b/configlib/model_impl.py new file mode 100644 index 0000000..4b7a866 --- /dev/null +++ b/configlib/model_impl.py @@ -0,0 +1,59 @@ +from typing import TypeVar, Type, Dict, List + +from .model import Config, ConfigValueMissingException, InvalidConfigTypingException +from .util import snake_case + +_T = TypeVar('_T', bound=Config) + +_T0 = TypeVar('_T0') + + +def parse_list_impl(cls: Type[_T0], data, path=''): + lis = [] + for i, item in enumerate(data): + list.append(parse_single_item(cls, item, path + '[' + str(i) + ']')) + return lis + + +def parse_obj_impl(cls: Type[_T0], data, path='Config') -> _T0: + obj = cls() + annotations: Dict[str, Type] = obj.__annotations__ + for key, val_type in annotations.items(): + if key not in data.keys(): + raise ConfigValueMissingException(path + key) + val = data[key] + setattr(obj, key, parse_single_item(val_type, val, path + '.' + key)) + return obj + + +def parse_dict_impl(val_type: Type[_T0], val, path) -> _T0: + dic = {} + for key, value in val.items(): + dic[key] = parse_single_item(val_type, value, path + '[' + repr(key) + ']') + return dic + + +def parse_single_item(val_type: Type[_T0], val, path) -> _T0: + if issubclass(val_type, (str, int, float)): + return val + if isinstance(val_type, List): + if len(val_type.__args__) != 1: + raise InvalidConfigTypingException(path + ': List must be supplied exactly one type') + return parse_list_impl(val_type.__args__[0], val, path) + if isinstance(val_type, Dict): + if len(val_type.__args__) != 2: + raise InvalidConfigTypingException(path + ': Dict must be supplied exactly two types') + if val_type.__args__[0] != str: + raise InvalidConfigTypingException(path + ': Dict must have `str` as indexing') + return parse_dict_impl(val_type.__args__[1], val, path) + return parse_obj_impl(val_type, val, path) + + +class BaseConfig(Config): + @classmethod + def get_name(cls) -> str: + return snake_case(cls.__name__).upper() + + @classmethod + def parse_dict(cls: Type[_T], data: dict) -> _T: + return parse_obj_impl(cls, data) diff --git a/configlib/util.py b/configlib/util.py new file mode 100644 index 0000000..3c7618d --- /dev/null +++ b/configlib/util.py @@ -0,0 +1,18 @@ +import re +from typing import List + + +def parse_case(any_case: str) -> List[str]: + if '_' in any_case: + return any_case.lower().split('_') + if '-' in any_case: + return any_case.lower().split('-') + return [word.lower() for word in re.split('(?<=[a-z0-9])(?=[A-Z])', any_case)] + + +def snake_case(any_case: str) -> str: + return '_'.join(parse_case(any_case)) + + +def pascal_case(any_case: str) -> str: + return ''.join(word.capitalize() for word in parse_case(any_case)) diff --git a/configlib/version.py b/configlib/version.py new file mode 100644 index 0000000..475dc6e --- /dev/null +++ b/configlib/version.py @@ -0,0 +1,16 @@ +from dataclasses import dataclass + + +@dataclass +class VersionInfo: + major: int + minor: int + build: int + level: str + serial: int + + def __str__(self): + return '{major}.{minor}.{build}{level}{serial}'.format(**self.__dict__) + + +version = VersionInfo(1, 0, 0, 'a', 0) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/requirements.txt diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..c7445fd --- /dev/null +++ b/setup.py @@ -0,0 +1,33 @@ +from setuptools import setup, find_packages + +find_packages + +with open('configlib/version.py') as f: + _loc, _glob = {}, {} + exec(f.read(), _loc, _glob) + version = {**_loc, **_glob}['version'] + +with open('requirements.txt') as f: + requirements = f.read().splitlines() + +with open('README.md') as f: + readme = f.read() + +if not version: + raise RuntimeError('version is not set in configlib/version.py') + +setup( + name="configlib", + author="romangraef", + url="https://github.com/romangraef/configlib", + version=str(version), + install_requires=requirements, + long_description=readme, + test_suite='tests.all_tests', + license="MIT", + packages=['configlib'], + description="An easy python config manager", + classifiers=[ + 'Topic :: Utilities', + ] +) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e8b0db2 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,7 @@ +import unittest + + +def all_tests(): + test_loader = unittest.TestLoader() + test_suite = test_loader.discover('tests', pattern='test_*.py') + return test_suite diff --git a/tests/test_something.py b/tests/test_something.py new file mode 100644 index 0000000..8091f46 --- /dev/null +++ b/tests/test_something.py @@ -0,0 +1,39 @@ +import json +from unittest import TestCase + +from configlib.model_impl import BaseConfig + + +class Yeeah(object): + a: int + + +class SomeConfig(BaseConfig): + something: str + ye: Yeeah + + +test_dict = { + 'something': 'hmm', + 'ye': { + 'a': 1 + } +} + + +def verify_test_dict(conf): + assert conf.something == 'hmm' + assert isinstance(conf, SomeConfig) + assert conf.ye.a == 1 + assert isinstance(conf.ye, Yeeah) + + +class TestSomething(TestCase): + + def test_yeah(self): + conf: SomeConfig = SomeConfig.parse_dict(test_dict) + verify_test_dict(conf) + + def test_text(self): + conf: SomeConfig = SomeConfig.loads(json.dumps(test_dict)) + verify_test_dict(conf) |