PK!tEgconfigs/__init__.py# -*- coding: utf-8 -*- """ gConfigs - Config and Secret parser ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Nothing shows better than some snippets: from gconfigs import envs, configs, secrets HOME = envs('HOME') DEBUG = configs.as_bool('DEBUG', default=False) DATABASE_USER = configs('DATABASE_USER') DATABASE_PASS = secrets('DATABASE_PASS') >>> # envs, configs and secrets are iterables >>> from gconfigs import envs >>> for env in envs: ... print(env) ... print(env.key) ... print(env.value) ... EnvironmentVariable(key='ENV_TEST', value='env-test-1') ENV_TEST env-test-1 ... >>> 'ENV_TEST' in envs True >>> envs.json() '{"ENV_TEST": "env-test-1", "HOME": "/root", ...}' """ __author__ = """Douglas Miranda""" __email__ = "douglascoding@gmail.com" __version__ = "0.1.5" from .gconfigs import GConfigs from .backends import LocalEnv, DotEnv, LocalMountFile envs = GConfigs(backend=LocalEnv, object_type_name="EnvironmentVariable") dotenvs = GConfigs(backend=DotEnv, object_type_name="DotEnvConfig") configs = GConfigs( backend=LocalMountFile(root_dir="/run/configs"), object_type_name="Config" ) secrets = GConfigs( backend=LocalMountFile(root_dir="/run/secrets"), object_type_name="Secret" ) PK!o gconfigs/backends.py""" Backends for gConfigs ~~~~~~~~~~~~~~~~~~~~~ Backends are just simple classes with at least two methods implemented: `get` and `keys`. Example: class RedisBackend: def keys(self): # return a iterable of all keys available return available_keys def get(self, key: str): # this method receive a key (identifier of a config) # and return its respective value return value Notes: - If it's not possible to provide a `.keys` method, just declare with: raise NotImplementedError. Of course it will limit the goodies of gConfigs. - For errors on `.get` method just throw exceptions. (Config doesn't exists, you don't have permission, stuff like that) See `GConfigs.get` and you'll see that it has a `default` parameter, and of course if you provide a default value it will not throw a exception. """ from pathlib import Path import os class LocalEnv: def keys(self): return os.environ.keys() def get(self, key, **kwargs): value = os.environ.get(key) if value is None: raise KeyError( f"Environment variable '{key}' not set. Check for any " "misconfiguration or misspelling of the variable name." ) return value class LocalMountFile: def __init__(self, root_dir="/", pattern="*"): self.pattern = pattern self.root_dir = root_dir @property def root_dir(self): if not self._root_dir.exists(): raise RootDirectoryNotFound( f"The root directory {self._root_dir} doesn't exist." ) return self._root_dir @root_dir.setter def root_dir(self, root_dir): self._root_dir = Path(root_dir) def keys(self): for item in self.root_dir.glob(self.pattern): if item.is_file(): yield item.name def get(self, key, **kwargs): file = self.root_dir / key if file.exists(): return file.read_text() raise FileNotFoundError( f"Check if your files are mounted on {self.root_dir}. " "And remember to check if your system is case sensitive." ) class DotEnv: def keys(self): return self._data.keys() def get(self, key, **kwargs): if not hasattr(self, "_dotenv_file"): raise Exception("It seems like you didn't loaded the your dotenv file yet.") value = self._data.get(key) if value is None: raise KeyError( f"The config '{key}' is not set on {self._dotenv_file}. Check " "for any misconfiguration or misspelling of the variable name." ) return value def load_file(self, filepath): self._dotenv_file = filepath self._data = {} with open(self._dotenv_file) as file: for line in file.readlines(): # ignore comments, section title or invalid lines if line.startswith(("#", ";", "[")) or "=" not in line: continue # split on the first =, allows for subsequent `=` in strings key, value = line.split("=", 1) key = key.strip() if not (key and value): continue self._data[key] = value.rstrip("\r\n") class RootDirectoryNotFound(FileNotFoundError): pass PK!=2ɩTTgconfigs/gconfigs.py# -*- coding: utf-8 -*- from collections import namedtuple from functools import lru_cache import json class NoValue: def __repr__(self): # pragma: no cover return f"<{self.__class__.__name__}>" NOTSET = NoValue() class GConfigs: def __init__(self, backend, strip=True, object_type_name="KeyValue"): """ :param backend: Backend / parser of configs. A simple class implementing `get` and `keys` methods. `gconfigs.backends` for more information. :param strip: Control the stripping of return value of `self.get` method. :param object_type_name: Simply a nice name for our key value named tuple. """ if hasattr(backend, "get") or hasattr(backend, "keys"): self._backend = backend() if callable(backend) else backend else: raise AttributeError( "'backend' class must have at least the methods 'get' and 'keys'." ) self.strip = strip self.object_type_name = object_type_name if hasattr(self._backend, "load_file") and callable(self._backend.load_file): self.load_file = self._load_file self._iter_configs = self.iterator() def get(self, key, default=NOTSET, use_instead=NOTSET, strip=None, **kwargs): """Return value for given key. :param var: Key (Name) of config. :param default: If backend doesn't return valid config, return this instead. :param use_instead: If `key` doesn't exist use the alternative key `use_instead`. :param strip: Control the stripping of return value. Override the default `self.strip` with `True` or `False`. Will strip if is a string value. :returns: Parsed value or default. Or raises exceptions you implement in your backend. """ try: value = self._backend.get(key) # This may seem a generic try/except but I'm actually catching the # specific Exception that you will implement in your backend. except Exception as e: if use_instead is not NOTSET: return self.get(use_instead, default, NOTSET, strip, **kwargs) if default is NOTSET: raise e value = default strip_ = self.strip if strip is None else strip if strip_ and isinstance(value, str): return value.strip() return value def as_bool(self, key, **kwargs): value = self.get(key, **kwargs) if isinstance(value, bool): return value if isinstance(value, str) and value.lower() in ("true", "false"): return self._cast(value.lower()) raise ValueError(f"Could not cast the value '{value}' to boolean.") def as_list(self, key, **kwargs): value = self.get(key, **kwargs) if isinstance(value, list) or isinstance(value, tuple): return list(value) if isinstance(value, str) and value.startswith("[") and value.endswith("]"): return self._cast(value) raise ValueError(f"Could not cast the value '{value}' to list.") def as_dict(self, key, **kwargs): value = self.get(key, **kwargs) if isinstance(value, dict): return value if isinstance(value, str) and value.startswith("{") and value.endswith("}"): return self._cast(value) raise ValueError(f"Could not cast the value '{value}' to dict.") def _cast(self, value): try: return json.loads(value) except json.decoder.JSONDecodeError as e: raise ValueError( f"Could not cast the value {value}. Tried to cast with json module, so it must be a valid json value." ) def json(self): """Returns json parsed data of all available data. """ return json.dumps({item.key: item.value for item in self.iterator()}) def _load_file(self, filepath): self._backend.load_file(filepath) @property @lru_cache() def _cached_namedtuple(self): return namedtuple(self.object_type_name, ["key", "value"]) def iterator(self): for key in self._backend.keys(): yield self._cached_namedtuple(key, self.get(key)) def __next__(self): return next(self._iter_configs) def __iter__(self): return self def __call__(self, key, **kwargs): return self.get(key, **kwargs) def __contains__(self, key): return key in self._backend.keys() def __len__(self): return len(self._backend.keys()) def __repr__(self): # pragma: no cover return f"" PK!Aq22 gconfigs-0.1.5.dist-info/LICENSEMIT License Copyright (c) 2018, Douglas Miranda Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PK!HڽTUgconfigs-0.1.5.dist-info/WHEEL A н#Z;/"d&F[xzw@Zpy3Fv]\fi4WZ^EgM_-]#0(q7PK!H7!gconfigs-0.1.5.dist-info/METADATAON0 )4 )4LD9[8 Rߞ$߯]R XGs9:R`t5Qq39N! ?-;#*.Wl: C XzzK`.sG4xPK!tEgconfigs/__init__.pyPK!o Egconfigs/backends.pyPK!=2ɩTTgconfigs/gconfigs.pyPK!Aq22 %gconfigs-0.1.5.dist-info/LICENSEPK!HڽTU)gconfigs-0.1.5.dist-info/WHEELPK!H7!*gconfigs-0.1.5.dist-info/METADATAPK!HY+gconfigs-0.1.5.dist-info/RECORDPKG-