aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--README.md39
-rw-r--r--configlib/__init__.py6
-rw-r--r--configlib/model.py41
-rw-r--r--configlib/model_impl.py59
-rw-r--r--configlib/util.py18
-rw-r--r--configlib/version.py16
-rw-r--r--requirements.txt0
-rw-r--r--setup.py33
-rw-r--r--tests/__init__.py7
-rw-r--r--tests/test_something.py39
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)