Package: llconfig
llconfigLightweight layered configuration library.All you need is Python >= 3.5 (and a keyboard).Basic conceptThere is aConfigobject holding several layers of configuration keys and their values (= configuration directives).
From top to bottom:Override layer = holds runtime directive overrides, if any.Env layer = directives loaded from environment variables are kept in this layer, if any.File layer = directives loaded from configuration file(s) are kept in this layer, if any.Default layer = holds a default value foreveryinitialized directive.Learn by exampleThe behavior is best shown on following example:fromllconfigimportConfigc=Config('local/override.cnf.py','/etc/my_app/conf.d',env_prefix='MY_',config_files_env_var='CONFIG')c.init('PORT',int,80)c.load()# recommended, but not required (see docstrings)c['PORT']The returned value is80, given that there is noMY_PORTenv
variable, noMY_CONFIGenv variable and noPORT = 1234line in any oflocal/override.cnf.pyor/etc/my_app/conf.d/*.cnf.pyconfiguration files.Search processFirst, the override layer is searched, but there is no runtime override (noc['PORT'] = 1234) in this example.The environment layer is searched next. If there is an env variable calledMY_PORT, its value is taken
andconvertedusingintfunction (this is necessary otherwise it wouldn't be possible to load anything
else thanstrfrom env variables).Then, if the env variable is not present, the file layer is searched. There can be multiple files in this layer
(forming sub-layers) and all of them must be Python-executable. File sub-layers are processed in following order:Files loaded fromMY_CONFIGenv variable. Its value is splitted using:(colon) as a delimiter and
each part is handled the same way as if it would be passed to constructor (see bellow). The handling
preserves order (so the leftmost part is always handled first).Files passed to constructor (local/override.cnf.pyand/etc/my_app/conf.din this example). If there
is a path pointing to directory instead of simple file, thedirectory is expanded(non-recursively). The
expansion lists all files in given directory usingexpansion_glob_patternattributesorted by file name
in reverse order(you can change this behavior by extending this class and overriding_expand_dirmethod). The expanded files are used as separate sub-layers in place of original directory.When all of the file sub-layers are created, each configuration fileis executedand each file's global
namespace is searched for thePORTdirective (still preserving the order). If found, the directive is returned
as is (without conversion).The default layer is searched as a last resort. As it contains values from directives'init, there is always a
default value (unless a search for non-initialized directive is performed). The default value is returned as is.Directive initializationDirective is initialized usinginitmethod. It takes directive name, converter function (see bellow) and
a default value (which isNoneby default). It is recommended to name directives using upper-case only.Any directive you want to use must be initialized,otherwise it is ignored (unknown env variables, unknown
directives in configuration files, etc.).This means that once you initialize a directive you can safely use it withoutKeyErrors or without callingc.get('PORT', 'default'). There will always be at least the default value.ConvertersConverters are arbitrary callables taking singlestrargument and returning anything. The converter is called
only for conversion from env variable. There are some predefined converters available inllconfig.converters,
but it is easy to create own. For example:fromllconfigimportConfigfromllconfig.convertersimportjson,bool_likefrompathlibimportPathc=Config()c.init('PORT',int)# "443" => 443c.init('HOSTNAME',str)# "localhost" => "localhost"c.init('DEBUG',bool_like)# "off" => Falsec.init('DEBUG_2',bool)# BEWARE: "0" => Truec.init('FLEXIBLE',json)# '{"hello": 1}' => {"hello": 1}c.init('PICTURES',lambdaraw:[Path(p)forpinraw.split(':')])# "a.jpg:b.jpg" => [Path("a.jpg"), Path("b.jpg")]Any exception raised during conversion is re-raised as aValueError.Getting the values outTheConfigobject implements a mapping protocol, so you can use it as if it was adict. In addition, there is aget_namespacemethodtaken from Flask frameworkwith exact same behavior (see their docs for more examples).fromllconfigimportConfigc=Config()c.init('DB_HOST',str,'localhost')c.init('DB_PORT',int,3306)c.init('DB_USER',str)c['DB_HOST']# => 'localhost'c['DB_USER']# => Nonec.get_namespace('DB_')# => {'host': 'localhost', 'port': 3306, 'user': None}c['DB_':]# syntactic sugar - does the same as `c.get_namespace('DB_')`dict(c)# => {'DB_HOST': 'localhost', 'DB_PORT': 3306, 'DB_USER': None}SecurityIn short: do not use this library in untrusted environment,unless you completely understand how it works and
what possible attack vectors are. The main concern is that each file forming the file layer is executed. There
is also a possibility to load files usingconfig_files_env_varenvironment variable (APP_CONFIGby default),
unless disabled. On top of that, you can compromise your application using badly written converter.