Package: zirconium
ZirconiumZirconium is a powerful configuration tool for loading and using configuration in your application.Use CaseZirconium abstracts away the process of loading and type-coercing configuration so that it Just Works for your
application. For exampleKey FeaturesFeaturesSupport for libraries to provide their own default configuration and/or configuration file locationsApplications specify their own configuration with@zirconium.configuredecoratorAutomatic replacement of ${ENVIRONMENT_VARIABLES} in stringsConsistent type coercion for common data types: paths, ints, floats, decimals, bytes, lists, dicts, sets, dates, timedeltas, and datetimesWhere dictionary-style declarations are not supported, instead use the dot syntax (e.g. "foo.bar")Supports multiple file encodingsExtensible to other formats as neededConfiguration is dict-like for ease-of-use in existing locations (e.g. Flask)Multiple files can be specified with different weights to control loading orderSupports default vs. normal configuration file (defaults always loaded first)Supports thread-safe injection of the configuration into your application via autoinjectSupports specifying default configuration for libraries in entry pointszirconium.configand for parsers inzirconium.parsers, as well as using the@zirconium.configuredecorator.Supported configuration methodsDatabase tables (with SQLAlchemy installed)YAML (with pyyaml installed)TOML (with toml installed or Python >= 3.11)JSONSetuptools-like CFG filesINI files (following the defaults of the configparser module)Environment variablesPriority OrderLater items in this list will override previous itemsFiles registered withregister_default_file(), in ascending order byweight(or order called)Files registered withregister_file(), in ascending order byweightFiles from environment variables registered withregister_file_from_environ(), in ascending order byweightValues from environment variables registered withregister_environ_var()Example Usageimportpathlibimportzirconiumfromautoinjectimportinjector@zirconium.configuredefadd_config(config):# Direct load configuration from dict:config.load_from_dict({"version":"0.0.1","database":{# Load these from environment variables"username":"${MYAPP_DATABASE_USERNAME}","password":"${MYAPP_DATABASE_PASSWORD}",},"escaped_environment_example":"$${NOT_AN_ENVIRONMENT VARIABLE","preceding_dollar_sign":"$$${STOCK_PRICE_ENV_VARIABLE}",})# Default configuration, relative to this file, will override the above dictbase_file=pathlib.Path(__file__).parent/".myapp.defaults.toml"config.register_default_file(base_file)# File in user home directory, overrides the defaultsconfig.register_file("~/.myapp.toml")# File in CWD, will override whatever is in homeconfig.register_file("./.myapp.toml")# Load a file path from environment variable, will override ALL registered filesconfig.register_file_from_environ("MYAPP_CONFIG_FILE")# Load values direct from the environment, will override ALL files including those specific in environment variables# sets config["database"]["password"]config.register_environ_var("MYAPP_DATABASE_PASSWORD","database","password")# sets config["database"]["username"]config.register_environ_var("MYAPP_DATABASE_USERNAME","database","username")# Injection exampleclassNeedsConfiguration:config:zirconium.ApplicationConfig=None@injector.constructdef__init__(self):# you have self.config available as of herepass# Method example@injector.injectdefwith_config(config:zirconium.ApplicationConfig=None):print(f"Hello world, my name is{config.as_str('myapp','welcome_name')}")print(f"Database user:{config.as_str('database','username')}")Type Coercion Examplesimportzirconium@zirconium.configuredefadd_config(config):config.load_from_dict({"bytes_example":"5K","timedelta_example":"5m","date_example":"2023-05-05","datetime_example":"2023-05-05T17:05:05","int_example":"5","float_example":"5.55","decimal_example":"5.55","str_example":"5.55","bool_false_example":0,"bool_true_example":1,"path_example":"~/user/file","set_example":["one","one","two"],"list_example":["one","one","two"],"dict_example":{"one":1,"two":2,}})@injector.injectdefshow_examples(config:zirconium.ApplicationConfig=None):config.as_bytes("bytes_example")# 5120 (int)config.as_timedelta("timedelta_example)         # datetime.timedelta(minutes=5)config.as_date("date_example")# datetime.date(2023, 5, 5)config.as_datetime("datetime_example")# datetime.datetime(2023, 5, 5, 17, 5, 5)config.as_int("int_example")# 5 (int)config.as_float("float_example")# 5.55 (float)config.as_decimal("decimal_example")# decimal.Decimal("5.55")config.as_str("str_example")# "5.55"config.as_bool("bool_false_example")# False (bool)config.as_bool("bool_true_example")# True (bool)config.as_path("path_example")# pathlib.Path("~/user/file")config.as_set("set_example")# {"one", "two"}config.as_list("list_example")# ["one", "one", "two"]config.as_dict("dict_example")# {"one": 1, "two": 2}# Raw dicts can still be used as sub-keys, for exampleconfig.as_int(("dict_example","one"))# 1 (int)Config ReferencesIn certain cases, your application might want to let the configuration be reloaded. This is possible via thereload_config()method which will reset your configuration to its base and reload all the values from the original
files. However, where a value has already been used in your program, that value will need to be updated. This leads
us to the ConfigRef() pattern which lets applications obtain a value and keep it current with the latest value loaded.
If you do not plan on reloading your configuration on-the-fly, you can skip this section.When using the methods that end in_ref(), you will obtain an instance of_ConfigRef(). This object has a few
special properties but will mostly behave as the underlying configuration value with a few exceptions:isinstancewill not work with itis Nonewill not return True even if the configuration value is actually None (use.is_none()instead)To get a raw value to work with, useraw_value().The value is cached within the_ConfigRef()object but this cache is invalidated wheneverreload_config()is called.
This should reduce the work you have to do when reloading your configuration (though you may still need to call certain
methods when the configuration is reloaded).To call a method on reload, you can add it viaconfig.on_load(callable). Ifcallableneeds to interact with a
different thread or process than the one wherereload_config()is called, it is your responsibility to manage this
communication (e.g. usethreading.Eventto notify the thread that the configuration needs to be reloaded).Testing classes that use ApplicationConfigUnit test functions decorated withautoinject.injector.test_casecan declare configuration usingzirconium.test_with_config(key, val)to declare configuration for testing. For example, this test case should pass:fromautoinjectimportinjectorimportzirconiumaszrimportunittestclassMyTestCase(unittest.TestCase):# This is essential since we use autoinject's test_case() to handle the ApplicationConfig fixture@injector.test_case# Declare a single value@zr.test_with_config(("foo","bar"),"hello world")# You can repeat the decorator to declare multiple values@zr.test_with_config(("some","value"),"what")# You can also pass a dict instead of a key, value tuple@zr.test_with_config({"foo":{"bar2":"hello world #2"}})deftest_something(self):# As a simple example.@injector.injectdefdo_something(cfg:zr.ApplicationConfig=None):self.assertEqual(cfg.as_str(("foo","bar")),"hello world")self.assertEqual(cfg.as_str(("some","value")),"what")Note that this pattern replaces all configuration values with the ones declared in decorators, so previously loaded
values will not be passed into your test function nor will they be passed between test functions.Change LogVersion 1.2.1Test cases can now use the fixture@zirconium.test_with_config(key: t.Iterable, value: t.Any)to inject test
configuration.Version 1.2.0Addedas_bytes()which will accept values like2Mand return the value converted into bytes (e.g.2097152. If
you really want to use metric prefixes (e.g.2MB=2000000), you must passallow_metric=Trueand then specify your
units as2MB. Prefixes up to exbibyte (EiB) are handled at the moment. You can also specifyBfor bytes orbitfor a number of bits. If no unit is specified, it uses thedefault_unitsparameter, which isBby default. All
units are case-insensitive.Addedas_timedelta()which will accept values like30mand returndatetime.timedelta(minutes=30). Valid units
ares,m,h,d,w,us, andms. If no units are specified, it defaults to thedefault_unitsparameter
which issby default. All units are case-insensitive.Added a new series of methodsas_*_ref()(andget_ref()) which mirror the behaviour of their counterparts not ending in_ref()except these return a_ConfigRef()instance instead of an actual value.Added a methodprint_config()which will print out the configuration to the command line.Version 1.1.0Addedas_list()andas_set()which return as expectedType-hinting added to theas_X()methods to help with usage in your IDEAdded support forregister_files()which takes a set of directories to use and registers a set of files and default files in each.Version 1.0.0Stable release after extensive testing on my ownPython 3.11's tomllib now supported for parsing TOML filesUsingpymitterto manage configuration registration was proving problematic when called from
a different thread than where the application config object was instatiated. Replaced it with a more robust solution.Fixed a bug for registering default filesAddedas_dict()to the configuration object which returns an instance ofMutableDeepDict.