PK!aUFconfect/__init__.py from .conf import Conf from .error import (ConfGroupExistsError, FrozenConfGroupError, FrozenConfPropError, UnknownConfError) __all__ = [ Conf, FrozenConfPropError, FrozenConfGroupError, UnknownConfError, ConfGroupExistsError ] PK!wh00confect/click.pyimport click def get_builtin_click_param_type(type_): type_to_param_type_map = dict([ (str, click.STRING), (int, click.INT), (float, click.FLOAT), (bool, click.BOOL) ]) return type_to_param_type_map.get(type_) def create_click_param_type(name, parser): cap_name = name[0].upper() + name[1:] def convert(self, value, param, ctx): if isinstance(value, str): return parser(value) return value return type(f'{cap_name}ParamType', (click.ParamType, ), dict(name=name, convert=convert))() def get_click_param_type(type_, parser, name): param_type = get_builtin_click_param_type(type_) if param_type is None: param_type = create_click_param_type(name, parser) return param_type PK!q@;;confect/conf.pyimport functools as fnt import importlib import logging import os import weakref from contextlib import contextmanager from copy import deepcopy import confect.prop_type from confect.error import (ConfGroupExistsError, FrozenConfGroupError, FrozenConfPropError, UnknownConfError) logger = logging.getLogger(__name__) class Undefined: '''Undefined value''' __instance = None __slots__ = () def __new__(cls): if cls.__instance is None: cls.__instance = object.__new__(cls) return cls.__instance def __bool__(self): return False def __repr__(self): return f'<{__name__}.{type(self).__qualname__}>' def __deepcopy__(self, memo): return self.__instance Undefined = Undefined() class ConfProperty: __slots__ = ('_value', 'default', 'prop_type') def __init__(self, default=Undefined, parser=None): '''Create configuration property with details >>> import confect >>> import datetime as dt >>> conf = confect.Conf() >>> from enum import Enum >>> class Color(Enum): ... RED = 1 ... GREEN = 2 ... BLUE = 3 >>> with conf.declare_group('dummy') as cg: ... cg.a_number = 3 ... cg.some_string = 'some string' ... cg.color = conf.prop( ... default=Color.RED, ... parser=lambda s: getattr(Color, s.upper())) Paramaters ---------- default : ValueType default value parser : Callable[[str], ValueType] parser for reading environment variable or command line argument into property value ''' self.default = default self._value = Undefined if parser is None: prop_type = confect.prop_type.of_value(default) else: prop_type = confect.prop_type.PropertyType(type(default), parser) self.prop_type = prop_type @property def value(self): if self._value is not Undefined: return self._value return self.default @value.setter def value(self, value): self._value = value def click_callback(self, ctx, param, value): if param.default != value: self._value = value def __repr__(self): return (f'<{__name__}.{type(self).__qualname__} ' f'default={self.default!r} value={self._value!r} ' f'prop_type={self.prop_type}>') class Conf: '''Configuration >>> import confect >>> conf = confect.Conf() Declare new configuration properties with ``Conf.declare_group(group_name)`` >>> with conf.declare_group('dummy') as cg: ... cg.opt1 = 3 ... cg.opt2 = 'some string' >>> conf.dummy.opt1 3 Configurations are immutable >>> conf.dummy.opt2 = 'other string' Traceback (most recent call last): ... confect.error.FrozenConfPropError: Configuration properties are frozen. Configuration properties can only be changed globally by loading configuration file through ``Conf.load_file()`` and ``Conf.load_module()``. And it can be changed locally in the context created by `Conf.mutate_locally()`. ''' # noqa __slots__ = ('_is_setting_imported', '_is_frozen', '_conf_depot', '_conf_groups', '__weakref__', ) def __init__(self): '''Create a new confect.Conf object >>> import confect >>> conf = confect.Conf() Declare new configuration properties with ``Conf.declare_group(group_name)`` >>> with conf.declare_group('dummy') as cg: ... cg.opt1 = 3 ... cg.opt2 = 'some string' >>> conf.dummy.opt1 3 ''' from confect.conf_depot import ConfDepot self._is_setting_imported = False self._is_frozen = True self._conf_depot = ConfDepot() self._conf_groups = {} def declare_group(self, name, **default_properties): '''Add new configuration group and all property names with default values >>> conf = Conf() Add new group and properties through context manager >>> with conf.declare_group('yummy') as yummy: ... yummy.kind='seafood' ... yummy.name='fish' >>> conf.yummy.name 'fish' Add new group and properties through function call >>> conf.declare_group('dummy', ... num_prop=3, ... str_prop='some string') >>> conf.dummy.num_prop 3 ''' if name in self._conf_groups: raise ConfGroupExistsError( f'configuration group {name!r} already exists') with self.mutate_globally(): group = ConfGroup(self, name) self._conf_groups[name] = group default_setter_ctx = group._default_setter() if default_properties: with default_setter_ctx as default_setter: default_setter._update(default_properties) else: return default_setter_ctx def _backup(self): return deepcopy(self._conf_groups) def _restore(self, conf_groups): self._conf_groups = conf_groups @contextmanager def mutate_locally(self): '''Return a context manager that makes this Conf mutable temporarily. All configuration properties will be restored upon completion of the block. >>> conf = Conf() >>> with conf.declare_group('yummy') as yummy: ... yummy.kind='seafood' ... yummy.name='fish' ... >>> with conf.mutate_locally(): ... conf.yummy.name = 'octopus' ... print(conf.yummy.name) ... octopus >>> print(conf.yummy.name) fish ''' # noqa conf_groups_backup = self._backup() with self.mutate_globally(): yield self._restore(conf_groups_backup) @contextmanager def mutate_globally(self): self._is_frozen = False yield self._is_frozen = True @contextmanager def _confect_c_ctx(self): import confect confect.c = self._conf_depot yield del confect.c def __contains__(self, group_name): return group_name in self._conf_groups def __getitem__(self, group_name): if group_name not in self._conf_groups: raise UnknownConfError( f'Unknown configuration group {group_name!r}') conf_group = self._conf_groups[group_name] if group_name in self._conf_depot: conf_depot_group = self._conf_depot[group_name] conf_group._update_from_conf_depot_group(conf_depot_group) del self._conf_depot[group_name] return conf_group def __getattr__(self, group_name): return self[group_name] def __setitem__(self, group_name, group): raise FrozenConfGroupError( 'Configuration groups are frozen. ' 'Call `confect.declare_group()` for ' 'registering new configuration group.' ) def __setattr__(self, name, value): if name in self.__slots__: object.__setattr__(self, name, value) else: self[name] = value def __dir__(self): return object.__dir__(self) + list(self._conf_groups.keys()) def __deepcopy__(self, memo): cls = type(self) new_self = cls.__new__(cls) new_self._is_setting_imported = self._is_setting_imported new_self._is_frozen = self._is_frozen new_self._conf_depot = deepcopy(self._conf_depot) new_self._conf_groups = deepcopy(self._conf_groups) for group in new_self._conf_groups.values(): group._conf = weakref.proxy(new_self) return new_self def load_file(self, path): '''Load python configuration file through file path. All configuration groups and properties should be added through ``Conf.declare_group()`` in your source code. Otherwise, it won't be accessable even if it is in configuration file. >>> conf = Conf() >>> conf.load_file('path/to/conf.py') # doctest: +SKIP Configuration file example .. code: python from confect import c c.yammy.kind = 'seafood' c.yammy.name = 'fish' ''' # noqa from pathlib import Path if not isinstance(path, Path): path = Path(path) with self.mutate_globally(): with self._confect_c_ctx(): exec(path.open('r').read()) def load_module(self, module_name): '''Load python configuration file through import. The module should be importable either through PYTHONPATH or was install as a package. All configuration groups and properties should be added through ``Conf.declare_group()`` in your source code. Otherwise, it won't be accessable even if it is in configuration file. >>> conf = Conf() >>> conf.load_model('some.module.name') # doctest: +SKIP Configuration file example .. code: python from confect import c c.yammy.kind = 'seafood' c.yammy.name = 'fish' ''' # noqa with self.mutate_globally(): with self._confect_c_ctx(): importlib.import_module(module_name) def load_envvars(self, prefix): '''Load python configuration from environment variables This function automatically searches environment variable in ``____`` format. Be aware of that all of these three identifier are case sensitive. If you have a configuration property ``conf.cache.expire_time`` and you call ``Conf.load_envvars('proj_X')``. It will set that ``expire_time`` property to the parsed value of ``proj_X__cache__expire_time`` environment variable. >> conf = confect.Conf() >> conf.load_envvars('proj_X') # doctest: +SKIP Parameters ---------- prefix : str prefix of environment variables ''' prefix = prefix + '__' with self.mutate_globally(): for name, value in os.environ.items(): if name.startswith(prefix): _, group, prop = name.split('__') value = self.parse_prop(group, prop, value) self._conf_depot[group][prop] = value def parse_prop(self, group, prop, string): return self[group].parse_prop(prop, string) @fnt.wraps(ConfProperty.__init__) def prop(self, *args, **kwargs): return ConfProperty(*args, **kwargs) def _iter_props(self): for group_name, group in self._conf_groups.items(): for prop_name, prop in group._properties.items(): yield group_name, prop_name, prop def click_options(self, cmd_func): '''Attaches all configurations to the command in the `---` form.''' import click props = reversed(list(self._iter_props())) for group_name, prop_name, prop in props: cmd_func = click.option( f'--{group_name}-{prop_name}', default=prop.default, callback=prop.click_callback, expose_value=False, type=prop.prop_type.click_param_type, show_default=True)(cmd_func) return cmd_func def __repr__(self): return (f'<{__name__}.{type(self).__qualname__} ' f'groups={list(self._conf_groups.keys())}>') class ConfGroupPropertySetter: __slots__ = ('_conf_group',) def __init__(self, conf_group): self._conf_group = conf_group def __getattr__(self, property_name): return self[property_name] def __setattr__(self, property_name, value): if property_name in self.__slots__: object.__setattr__(self, property_name, value) else: self[property_name] = value def __getitem__(self, property_name): return self._conf_group._properties.setdefault( property_name, ConfProperty()) def __setitem__(self, property_name, default): if isinstance(default, ConfProperty): conf_prop = default else: conf_prop = ConfProperty(default) self._conf_group._properties[property_name] = conf_prop def _update(self, default_properties): for p, v in default_properties.items(): self[p] = v class ConfGroup: __slots__ = ('_conf', '_name', '_properties') def __init__(self, conf: Conf, name: str): self._conf = weakref.proxy(conf) self._name = name self._properties = {} def __getattr__(self, property_name): return self[property_name] def __setattr__(self, property_name, value): if property_name in self.__slots__: object.__setattr__(self, property_name, value) else: self[property_name] = value def __getitem__(self, property_name): if property_name not in self._properties: raise UnknownConfError( f'Unknown {property_name!r} property in ' f'configuration group {self._name!r}') return self._properties[property_name].value def __setitem__(self, property_name, value): if self._conf._is_frozen: raise FrozenConfPropError( 'Configuration properties are frozen.\n' 'Configuration properties can only be changed globally by ' 'loading configuration file through ' '``Conf.load_file()`` and ``Conf.load_module()``.\n' 'And it can be changed locally in the context ' 'created by `Conf.mutate_locally()`.' ) else: self._properties[property_name].value = value def __dir__(self): return self._properties.keys() @contextmanager def _default_setter(self): yield ConfGroupPropertySetter(self) def _update_from_conf_depot_group(self, conf_depot_group): for conf_property, value in conf_depot_group._items(): if conf_property in self._properties: self._properties[conf_property].value = value def __deepcopy__(self, memo): cls = type(self) new_self = cls.__new__(cls) new_self._conf = self._conf # Don't need to copy conf new_self._name = self._name new_self._properties = deepcopy(self._properties) return new_self def parse_prop(self, prop, string): return self._properties[prop].prop_type.parser(string) def as_dict(self): return { name: prob.value for name, prob in self._properties.items() } def __repr__(self): return (f'<{__name__}.{type(self).__qualname__} ' f'{self._name} properties={list(self._properties.keys())}>') PK!confect/conf_depot.py from confect.error import UnknownConfError class ConfDepot: __slots__ = '_depot_groups' def __init__(self): self._depot_groups = {} def __delitem__(self, group_name): del self._depot_groups[group_name] def __getitem__(self, group_name): if group_name not in self._depot_groups: conf_depot_group = ConfDepotGroup() self._depot_groups[group_name] = conf_depot_group return self._depot_groups[group_name] def __getattr__(self, group_name): return self[group_name] def __setattr__(self, name, value): if name in self.__slots__: object.__setattr__(self, name, value) else: raise TypeError( 'Adding property to first level of ConfDepot is forbidding.\n' # noqa 'In configuration file, all configuration properties should be in some configuration group.\n' # noqa 'Configuration group would be created automatically when needed.\n' # noqa 'In the following example, `yummy` is the group name and `kind` is the property name.\n' # noqa '>>> c.yummy.kind = "seafood"' ) def __contains__(self, group_name): return group_name in self._depot_groups def __dir__(self): return self._depot_groups.keys() class ConfDepotGroup: __slots__ = '_depot_properties' def __init__(self): self._depot_properties = {} def _items(self): return self._depot_properties.items() def __getitem__(self, property_name): if property_name not in self._depot_properties: raise UnknownConfError( f'ConfDepotGroup object has no property {property_name!r}' ) return self._depot_properties[property_name] def __setitem__(self, property_name, value): self._depot_properties[property_name] = value def __getattr__(self, property_name): return self[property_name] def __setattr__(self, name, value): if name in self.__slots__: object.__setattr__(self, name, value) else: self[name] = value def __dir__(self): return self._depot_properties.keys() PK!confect/error.py class UnknownConfError(AttributeError, KeyError): pass class FrozenConfPropError(TypeError): pass class FrozenConfGroupError(TypeError): pass class ConfGroupExistsError(ValueError): pass PK!:%1Hxxconfect/prop_type.pyimport ast import datetime as dt import json __all__ = ['of_value', 'PropertyType'] class PropertyType(): def __init__(self, type_, parser, name=None, click_param_type=None): self.type_ = type_ self.parser = parser if name is None: name = type_.__name__ self.name = name if click_param_type is None: try: import confect.click click_param_type = confect.click.get_click_param_type( type_, parser, name) except ModuleNotFoundError as e: pass self.click_param_type = click_param_type prop_types = [ PropertyType(bool, lambda s: bool(ast.literal_eval(s))), PropertyType(str, lambda s: s, str), PropertyType(int, lambda s: int(s)), PropertyType(float, lambda s: float(s)), PropertyType(bytes, lambda s: s.encode()), PropertyType(tuple, lambda s: tuple(json.load(s))), PropertyType(dict, lambda s: json.loads(s)), PropertyType(list, lambda s: json.loads(s)), ] try: import pendulum as pdl prop_types += [ PropertyType(dt.datetime, lambda s: pdl.parse(s)), PropertyType(dt.date, lambda s: pdl.parse(s).date()) ] except ModuleNotFoundError: pass def of_value(value): for prop_type in prop_types: if isinstance(value, prop_type.type_): return prop_type PK!HW"TTconfect-0.3.1.dist-info/WHEEL A н#J."jm)Afb~ ڡ5 G7hiޅF4+-3ڦ/̖?XPK!Hd'67 confect-0.3.1.dist-info/METADATA[rFvSt&EI#8a٣OoЉ *͗ǝܷ謆z,__/ ZXTUʿ\ NN2Cx~4~vgE&I N.1w/ ]w+x_,[o~5eOO?}{|qU^_Z={*MԴ&MJKQίmLMS+~1xcaRNK" jS>*6jSEQZEڈMQ\VdP eׅ*4Qs*stEBiVFaOj%+ڈr>W)cZ) lֈ[8(a!ny,ZmLyw'2OB#Ҝ֫DԈTW{HZ`z-Ԇ>*]J.JwB7.rg$jqK!hTzWFZfMlBk}RyUƝկJ!AJ!]/h&ۦ,7)L9vAJZ%5bK Pi-s@ikMJSP25&h 83d)OYW$a4F:VV/iGv!ΰFr[գ9,UZSbHөL-`[Ǝ`쉤 bw5V[*#몈@;QD1cm iY,&0cIJb8A*  UJs%\9[*4dnZ<2V2Y&),3"Z4,t.SRxʆHhEv`ro^~QΰnT+2sjԙ-JrM$uYh v/||Ow)) KYI}/J #Ft=^{{yvj:}:v;K{V=aAX\GkNRՆF=2`f.Wt |6>*@ ts; zLNu[QZ8?K%;! @Xw3ϖC/j͡ TRj.:dbg*D\Χ1,:)AՠZlڰ*- NO[vMc5m@e$E`} ['=Ǵ3mZT>mqE -tSԍ,5AɳSLC 3cRD嶉"n)pKao`{߳q^ͤ[I=@puc $u#i=iˍHAbFo`F=P̊Ξtc vC"\ O8!ugwD$%-0Q ja.n w2+ %aV#-գU'd"|gn0277 R.)r1Օʷ=t]=${/ѤzEj'w*<|E"b 5`MĢ0o2EQjk tr |r<b_s?Nl)$?Nđg;"ծل"J!D< <[%360]g&ۉLinQhp|!k {ҪIRq`c ؕxƛbJW:v-S8(SᆘnvĆ k2t`k&$C\Yl4 C렕FütB«\IyAJ0UES5&06:( Dqݯ ,Y.OmpiŦNw%∅&O ;P E Ĝ, uGp$K>5$`AON|ڧ<jл.̲T{$f%哧M/X l<B,`;σV/kL>AftEw)%:EpeV(FF:ۄ"yOwđoKmn%9ay@sѻ4rfNvj5'JIF^rk]Qbp6ُ#;Mn 'ˣJtv>V SRp9%Kn Rӏ՛T%+RANoOvZKa;x84Ҟ{k, v+yO8űwP@R)~</Ͽ8}d})U<+Lҳg/^O=ac `\8Gf._hدL,xĎKA2{N0AhaBd߰Z }"_MF6y3Tȝ٭Cб҆9Lti/ QT%'m9.8țL]p;hedtj T'Izb_mֵXd ɕ傞2syюI& >x/4vX0O;qG`1؊r ­gOx'0L}N&TEuUБ=vkt,äJ&\vk-l(/KpHP. lbY PWߜ''XݳNN~2̫_G;tO߳2' }b}ݵbsvǀd:vvONLD k6~672?"kavO4(װ!k;i8xHvC'!vxo¹Mns|:W'j C} i+2aUDqO{(_liGl}gxKFO' T*+P8˶7qQ4>o ts I|;,{|nK]m-G2C㥱[ 7_A 4A•t\'hO* NJ,n%QgձWC>F&qm< f(JhrḎյM'ԤL= g. w!z_3Z( a"Ν2L|9vDd7zysHLIϡ] ?S̨С}ⷧeT#gCۢ{X.o텮tD3WEvҧ篐2܎J|dOXb˺qk?_Ewۏr签 }="Ma˜$P4~aGx>|q>>hd,H;5 L62D+Psoq " #O"{S;$UoN0t. Skbdҕ(Dt[k]q#<}vr2hJ@k&L9 $o/.6q9JMWfJR|*V;D$VSLyV@|iMc N׼|iM0tL{z}=]`u9l6X0]P7Z,1 ٌi- S[qbi3rU#FcZ3sYj޸{U-ED+5!?;)ti*ی  p 0.ـg ^ok3uIX#uF[Z]&=n5EBY|w~:϶t O/tm [n BcARDdy|tɓӁ,VtO9ޡq3fXol9&4,)˄݉G==rvK~W#J+\L,Kt}K_saSTG@עƎɟ|A?qFT f|[$]ֶCMl'7~:rZg4;N=?IaABSl(7"+V<5|Ŵ\~6QhEnadh lLaN < kGOܤLbݔ2NGضk(6W\]kՁB"V (o|5 U &PK!H_(confect-0.3.1.dist-info/RECORDuϻ@EѼm`A PZZ _?ӎtt!M&89.;r똩'h*'8V2`4e{hμVSQc8~)sR9-iucmjQoѥ*2Ҋ:F?o!ݮ`]cu`j<*Qo o\2Up=sn,o0 h@ZD+%B6NxҒm7lYWoq"otR Ηͮ2]жifM!R92 7_ZZBvaj 6cpqN4[-Xf;؂M"GCNAȓ?nHRo1؝MWKmW'"Cdb!ٮ,?˞:ޒa>PK!aUFconfect/__init__.pyPK!wh008confect/click.pyPK!q@;;confect/conf.pyPK!@confect/conf_depot.pyPK!Iconfect/error.pyPK!:%1HxxJconfect/prop_type.pyPK!HW"TT1Pconfect-0.3.1.dist-info/WHEELPK!Hd'67 Pconfect-0.3.1.dist-info/METADATAPK!H_(%dconfect-0.3.1.dist-info/RECORDPK df