Package: configly
TL;DR# config.ymlfoo:bar:<% ENV[REQUIRED] %>baz:<% ENV[OPTIONAL, true] %>list_of_stuff:-fun<% ENV[NICE, dament] %>al-fun<% ENV[AGH, er] %>al-more/<% ENV[THAN, er] %>/one/<% ENV[interpolation, er] %>!# app.pyconfig=Config.from_yaml('config.yml')print(config.foo.bar)print(config.foo['baz'])foriteminconfig.list_of_stuff:print(item)pipinstallconfigly[yaml]IntroductionLoading configuration is done in every (application) project, and yet it is often
overlooked and condidered too easy or straightforward to bother using a library
to manage doing it.Therefore, we often see code like this:# config.pyimportos# Maybe it's following 12factor and loading all the config from the environment.config={'log_level':os.getenv('LOG_LEVEL'),'database':{# At least here, I can nest values if I want to organize things.'password':os.environ['DATABASE_PASSWORD'],'port':int(os.environ['DATABASE_PORT']),}}or this# config.pyimportosclassConfig:log_level=os.getenv('LOG_LEVEL')# Here it's not so easy to namespacedatabase_password=os.environ['DATABASE_PASSWORD']database_port=int(os.environ['DATABASE_PORT'])# Oh goodness!classDevConfig(Config):environment='dev'or thisimportconfigparser# ...🤢... Okay I dont even want to get into this one.And this is all assuming that everyone is loading configuration at the outermost entrypoint!
The two worst possible outcomes in configuration are:You are loading configuration lazily and/or deeply within your application, such that it
hits a critical failure after having seemingly successfully started up.There is not a singular location at which you can go to see all configuration your app might
possibly be reading from.The pitchConfiglyasserts configuration should:Be centralizedOne should be able to look at one file to see all (env vars, files, etc) which must exist for the
application to function.Be comprehensiveOne should not find configuration being loaded secretly elsewhereBe declarative/staticcode-execution (e.g. the class above) in the definition of the config inevitably makes it
hard to interpret, as the config becomes more complex.Be namespacableOne should not have to prependfoo_namespaces to allfoorelated config namesBe loaded, once, at app startup(At least thedefinitionof the configuration you're loading)(Ideally) have structured outputIf something is anint, ideally it would be read as an int.To that end, theconfigly.Configclass exposes a series of classmethods from which your config
can be loaded. It's largely unimportant what the input format is, but we started with formats
that deserialize into at leaststr,float,int,boolandNonetypes.# Currently supported input formats.config=Config.from_yaml('config.yml')config=Config.from_json('config.json')config=Config.from_toml('config.toml')Given an inputconfig.ymlfile:# config.ymlfoo:bar:<% ENV[REQUIRED] %>baz:<% ENV[OPTIONAL, true] %>list_of_stuff:-fun<% ENV[NICE, dament] %>al-fun<% ENV[AGH, er] %>al-more/<% ENV[THAN, er] %>/one/<% ENV[interpolation, er] %>!A number of things are exemplified in the example above:Each<% ... %>section indicates an interpolated value, the interpolation can
be a fragment of the overall value, and multiple values can be interpolated
within a single value.ENVis an "interpolator" which knows how to obtain environment variables[VAR]Will raise an error if that piece of config is not found, whereas[VAR, true]will defaultVARto the value after the commaWhatever the final value is, it's interpreted as a literal value in the
format of the file which loads it. I.E.true-> pythonTrue,1->
python1, andnull-> pythonNone.Now that you've loaded the above configuration:# app.pyconfig=Config.from_yaml('config.yml')# You can access namespaced config using dot accessprint(config.foo.bar)# You have use index syntax for dynamic, or non-attribute-safe key values.print(config.foo['baz'])# You can iterate over listsforiteminconfig.list_of_stuff:print(item)# You can *generally* treat key-value maps as dictsforkey,valueinconfig.foo.items():print(key,value)# You can *actually* turn key-value maps into dictsdict(config.foo)==config.foo.to_dict()Installing# Basic installationpipinstallconfigly# To use the yaml config loaderpipinstallconfigly[yaml]# To use the toml config loaderpipinstallconfigly[toml]# To use the vault config loaderpipinstallconfigly[vault]