# Zirconium
Zirconium is a powerful configuration tool for loading and using configuration in your application.
## Use Case
Zirconium abstracts away the process of loading and type-coercing configuration so that it Just Works for your
application. For example
## Key Features
### Features
* Support for libraries to provide their own default configuration and/or configuration file locations
* Applications specify their own configuration with `@zirconium.configure` decorator
* Automatic replacement of ${ENVIRONMENT_VARIABLES} in strings
* Consistent type coercion for common data types: paths, ints, floats, decimals, bytes, lists, dicts, sets, dates, timedeltas, and datetimes
* Where dictionary-style declarations are not supported, instead use the dot syntax (e.g. "foo.bar")
* Supports multiple file encodings
* Extensible to other formats as needed
* Configuration is dict-like for ease-of-use in existing locations (e.g. Flask)
* Multiple files can be specified with different weights to control loading order
* Supports default vs. normal configuration file (defaults always loaded first)
* Supports thread-safe injection of the configuration into your application via autoinject
* Supports specifying default configuration for libraries in entry points `zirconium.config` and for parsers in
`zirconium.parsers`, as well as using the `@zirconium.configure` decorator.
### Supported configuration methods
* Database tables (with SQLAlchemy installed)
* YAML (with pyyaml installed)
* TOML (with toml installed or Python >= 3.11)
* JSON
* Setuptools-like CFG files
* INI files (following the defaults of the configparser module)
* Environment variables
### Priority Order
Later items in this list will override previous items
1. Files registered with `register_default_file()`, in ascending order by `weight` (or order called)
2. Files registered with `register_file()`, in ascending order by `weight`
3. Files from environment variables registered with `register_file_from_environ()`, in ascending order by `weight`
5. Values from environment variables registered with `register_environ_var()`
## Example Usage
```python
import pathlib
import zirconium
from autoinject import injector
@zirconium.configure
def add_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 dict
base_file = pathlib.Path(__file__).parent / ".myapp.defaults.toml"
config.register_default_file(base_file)
# File in user home directory, overrides the defaults
config.register_file("~/.myapp.toml")
# File in CWD, will override whatever is in home
config.register_file("./.myapp.toml")
# Load a file path from environment variable, will override ALL registered files
config.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 example
class NeedsConfiguration:
config: zirconium.ApplicationConfig = None
@injector.construct
def __init__(self):
# you have self.config available as of here
pass
# Method example
@injector.inject
def with_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 Examples
```python
import zirconium
@zirconium.configure
def add_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.inject
def show_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 example
config.as_int(("dict_example", "one")) # 1 (int)
```
## Config References
In certain cases, your application might want to let the configuration be reloaded. This is possible via the
`reload_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:
- `isinstance` will not work with it
- `is None` will not return True even if the configuration value is actually None (use `.is_none()` instead)
To get a raw value to work with, use `raw_value()`.
The value is cached within the `_ConfigRef()` object but this cache is invalidated whenever `reload_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 via `config.on_load(callable)`. If `callable` needs to interact with a
different thread or process than the one where `reload_config()` is called, it is your responsibility to manage this
communication (e.g. use `threading.Event` to notify the thread that the configuration needs to be reloaded).
## Testing classes that use ApplicationConfig
Unit test functions decorated with `autoinject.injector.test_case` can declare configuration using `zirconium.test_with_config(key, val)`
to declare configuration for testing. For example, this test case should pass:
```python
from autoinject import injector
import zirconium as zr
import unittest
class MyTestCase(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"
}
})
def test_something(self):
# As a simple example.
@injector.inject
def do_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 Log
### Version 1.2.1
- Test cases can now use the fixture `@zirconium.test_with_config(key: t.Iterable, value: t.Any)` to inject test
configuration.
### Version 1.2.0
- Added `as_bytes()` which will accept values like `2M` and return the value converted into bytes (e.g. `2097152`. If
you really want to use metric prefixes (e.g. `2MB=2000000`), you must pass `allow_metric=True` and then specify your
units as `2MB`. Prefixes up to exbibyte (`EiB`) are handled at the moment. You can also specify `B` for bytes or `bit`
for a number of bits. If no unit is specified, it uses the `default_units` parameter, which is `B` by default. All
units are case-insensitive.
- Added `as_timedelta()` which will accept values like `30m` and return `datetime.timedelta(minutes=30)`. Valid units
are `s`, `m`, `h`, `d`, `w`, `us`, and `ms`. If no units are specified, it defaults to the `default_units` parameter
which is `s` by default. All units are case-insensitive.
- Added a new series of methods `as_*_ref()` (and `get_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 method `print_config()` which will print out the configuration to the command line.
### Version 1.1.0
- Added `as_list()` and `as_set()` which return as expected
- Type-hinting added to the `as_X()` methods to help with usage in your IDE
- Added support for `register_files()` which takes a set of directories to use and registers a set of files and default files in each.
### Version 1.0.0
- Stable release after extensive testing on my own
- Python 3.11's tomllib now supported for parsing TOML files
- Using `pymitter` to 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 files
- Added `as_dict()` to the configuration object which returns an instance of `MutableDeepDict`.
Raw data
{
"_id": null,
"home_page": "https://github.com/turnbullerin/zirconium",
"name": "zirconium",
"maintainer": "",
"docs_url": null,
"requires_python": ">=3.7",
"maintainer_email": "",
"keywords": "",
"author": "Erin Turnbull",
"author_email": "erin.a.turnbull@gmail.com",
"download_url": "https://files.pythonhosted.org/packages/57/c2/6df97c7234fe7dbb4b2fd9dcd8813fd04cdb8a901acc8d8cdc1a733ce042/zirconium-1.2.4.tar.gz",
"platform": null,
"description": "# Zirconium\r\n\r\nZirconium is a powerful configuration tool for loading and using configuration in your application.\r\n\r\n## Use Case\r\n\r\nZirconium abstracts away the process of loading and type-coercing configuration so that it Just Works for your \r\napplication. For example\r\n\r\n## Key Features\r\n\r\n### Features\r\n\r\n* Support for libraries to provide their own default configuration and/or configuration file locations\r\n* Applications specify their own configuration with `@zirconium.configure` decorator\r\n* Automatic replacement of ${ENVIRONMENT_VARIABLES} in strings\r\n* Consistent type coercion for common data types: paths, ints, floats, decimals, bytes, lists, dicts, sets, dates, timedeltas, and datetimes\r\n* Where dictionary-style declarations are not supported, instead use the dot syntax (e.g. \"foo.bar\") \r\n* Supports multiple file encodings \r\n* Extensible to other formats as needed\r\n* Configuration is dict-like for ease-of-use in existing locations (e.g. Flask)\r\n* Multiple files can be specified with different weights to control loading order\r\n* Supports default vs. normal configuration file (defaults always loaded first)\r\n* Supports thread-safe injection of the configuration into your application via autoinject\r\n* Supports specifying default configuration for libraries in entry points `zirconium.config` and for parsers in\r\n `zirconium.parsers`, as well as using the `@zirconium.configure` decorator.\r\n\r\n### Supported configuration methods\r\n\r\n* Database tables (with SQLAlchemy installed)\r\n* YAML (with pyyaml installed)\r\n* TOML (with toml installed or Python >= 3.11)\r\n* JSON\r\n* Setuptools-like CFG files\r\n* INI files (following the defaults of the configparser module)\r\n* Environment variables\r\n\r\n### Priority Order\r\n\r\nLater items in this list will override previous items\r\n\r\n1. Files registered with `register_default_file()`, in ascending order by `weight` (or order called)\r\n2. Files registered with `register_file()`, in ascending order by `weight`\r\n3. Files from environment variables registered with `register_file_from_environ()`, in ascending order by `weight`\r\n5. Values from environment variables registered with `register_environ_var()`\r\n\r\n\r\n## Example Usage\r\n\r\n```python\r\nimport pathlib\r\nimport zirconium\r\nfrom autoinject import injector\r\n\r\n\r\n@zirconium.configure\r\ndef add_config(config):\r\n \r\n # Direct load configuration from dict:\r\n config.load_from_dict({\r\n \"version\": \"0.0.1\",\r\n \"database\": {\r\n # Load these from environment variables\r\n \"username\": \"${MYAPP_DATABASE_USERNAME}\",\r\n \"password\": \"${MYAPP_DATABASE_PASSWORD}\",\r\n },\r\n \"escaped_environment_example\": \"$${NOT_AN_ENVIRONMENT VARIABLE\",\r\n \"preceding_dollar_sign\": \"$$${STOCK_PRICE_ENV_VARIABLE}\",\r\n })\r\n \r\n # Default configuration, relative to this file, will override the above dict\r\n base_file = pathlib.Path(__file__).parent / \".myapp.defaults.toml\"\r\n config.register_default_file(base_file) \r\n \r\n # File in user home directory, overrides the defaults\r\n config.register_file(\"~/.myapp.toml\")\r\n \r\n # File in CWD, will override whatever is in home\r\n config.register_file(\"./.myapp.toml\")\r\n \r\n # Load a file path from environment variable, will override ALL registered files\r\n config.register_file_from_environ(\"MYAPP_CONFIG_FILE\")\r\n \r\n # Load values direct from the environment, will override ALL files including those specific in environment variables\r\n # sets config[\"database\"][\"password\"]\r\n config.register_environ_var(\"MYAPP_DATABASE_PASSWORD\", \"database\", \"password\")\r\n # sets config[\"database\"][\"username\"]\r\n config.register_environ_var(\"MYAPP_DATABASE_USERNAME\", \"database\", \"username\")\r\n \r\n \r\n# Injection example\r\nclass NeedsConfiguration:\r\n\r\n config: zirconium.ApplicationConfig = None\r\n\r\n @injector.construct\r\n def __init__(self):\r\n # you have self.config available as of here\r\n pass\r\n \r\n \r\n# Method example\r\n\r\n@injector.inject \r\ndef with_config(config: zirconium.ApplicationConfig = None):\r\n print(f\"Hello world, my name is {config.as_str('myapp', 'welcome_name')}\")\r\n print(f\"Database user: {config.as_str('database', 'username')}\")\r\n```\r\n\r\n## Type Coercion Examples\r\n\r\n```python \r\nimport zirconium\r\n\r\n@zirconium.configure \r\ndef add_config(config):\r\n config.load_from_dict({\r\n \"bytes_example\": \"5K\",\r\n \"timedelta_example\": \"5m\",\r\n \"date_example\": \"2023-05-05\",\r\n \"datetime_example\": \"2023-05-05T17:05:05\",\r\n \"int_example\": \"5\",\r\n \"float_example\": \"5.55\",\r\n \"decimal_example\": \"5.55\",\r\n \"str_example\": \"5.55\",\r\n \"bool_false_example\": 0,\r\n \"bool_true_example\": 1,\r\n \"path_example\": \"~/user/file\",\r\n \"set_example\": [\"one\", \"one\", \"two\"],\r\n \"list_example\": [\"one\", \"one\", \"two\"],\r\n \"dict_example\": {\r\n \"one\": 1,\r\n \"two\": 2,\r\n }\r\n })\r\n \r\n\r\n@injector.inject \r\ndef show_examples(config: zirconium.ApplicationConfig = None):\r\n config.as_bytes(\"bytes_example\") # 5120 (int)\r\n config.as_timedelta(\"timedelta_example) # datetime.timedelta(minutes=5)\r\n config.as_date(\"date_example\") # datetime.date(2023, 5, 5)\r\n config.as_datetime(\"datetime_example\") # datetime.datetime(2023, 5, 5, 17, 5, 5)\r\n config.as_int(\"int_example\") # 5 (int)\r\n config.as_float(\"float_example\") # 5.55 (float)\r\n config.as_decimal(\"decimal_example\") # decimal.Decimal(\"5.55\")\r\n config.as_str(\"str_example\") # \"5.55\"\r\n config.as_bool(\"bool_false_example\") # False (bool)\r\n config.as_bool(\"bool_true_example\") # True (bool)\r\n config.as_path(\"path_example\") # pathlib.Path(\"~/user/file\")\r\n config.as_set(\"set_example\") # {\"one\", \"two\"}\r\n config.as_list(\"list_example\") # [\"one\", \"one\", \"two\"]\r\n config.as_dict(\"dict_example\") # {\"one\": 1, \"two\": 2}\r\n \r\n # Raw dicts can still be used as sub-keys, for example\r\n config.as_int((\"dict_example\", \"one\")) # 1 (int) \r\n \r\n```\r\n\r\n## Config References\r\n\r\nIn certain cases, your application might want to let the configuration be reloaded. This is possible via the \r\n`reload_config()` method which will reset your configuration to its base and reload all the values from the original\r\nfiles. However, where a value has already been used in your program, that value will need to be updated. This leads\r\nus to the ConfigRef() pattern which lets applications obtain a value and keep it current with the latest value loaded.\r\nIf you do not plan on reloading your configuration on-the-fly, you can skip this section.\r\n\r\nWhen using the methods that end in `_ref()`, you will obtain an instance of `_ConfigRef()`. This object has a few\r\nspecial properties but will mostly behave as the underlying configuration value with a few exceptions:\r\n\r\n- `isinstance` will not work with it\r\n- `is None` will not return True even if the configuration value is actually None (use `.is_none()` instead)\r\n\r\nTo get a raw value to work with, use `raw_value()`.\r\n\r\nThe value is cached within the `_ConfigRef()` object but this cache is invalidated whenever `reload_config()` is called.\r\nThis should reduce the work you have to do when reloading your configuration (though you may still need to call certain\r\nmethods when the configuration is reloaded).\r\n\r\nTo call a method on reload, you can add it via `config.on_load(callable)`. If `callable` needs to interact with a \r\ndifferent thread or process than the one where `reload_config()` is called, it is your responsibility to manage this\r\ncommunication (e.g. use `threading.Event` to notify the thread that the configuration needs to be reloaded).\r\n\r\n## Testing classes that use ApplicationConfig\r\n\r\nUnit test functions decorated with `autoinject.injector.test_case` can declare configuration using `zirconium.test_with_config(key, val)`\r\nto declare configuration for testing. For example, this test case should pass:\r\n\r\n```python\r\nfrom autoinject import injector\r\nimport zirconium as zr \r\nimport unittest \r\n\r\nclass MyTestCase(unittest.TestCase):\r\n\r\n # This is essential since we use autoinject's test_case() to handle the ApplicationConfig fixture\r\n @injector.test_case \r\n # Declare a single value\r\n @zr.test_with_config((\"foo\", \"bar\"), \"hello world\")\r\n # You can repeat the decorator to declare multiple values\r\n @zr.test_with_config((\"some\", \"value\"), \"what\")\r\n # You can also pass a dict instead of a key, value tuple\r\n @zr.test_with_config({\r\n \"foo\": {\r\n \"bar2\": \"hello world #2\"\r\n }\r\n })\r\n def test_something(self):\r\n \r\n # As a simple example.\r\n @injector.inject \r\n def do_something(cfg: zr.ApplicationConfig = None):\r\n self.assertEqual(cfg.as_str((\"foo\", \"bar\")), \"hello world\")\r\n self.assertEqual(cfg.as_str((\"some\", \"value\")), \"what\")\r\n```\r\n\r\nNote that this pattern replaces all configuration values with the ones declared in decorators, so previously loaded\r\nvalues will not be passed into your test function nor will they be passed between test functions.\r\n\r\n## Change Log\r\n\r\n### Version 1.2.1\r\n- Test cases can now use the fixture `@zirconium.test_with_config(key: t.Iterable, value: t.Any)` to inject test \r\n configuration.\r\n\r\n### Version 1.2.0\r\n- Added `as_bytes()` which will accept values like `2M` and return the value converted into bytes (e.g. `2097152`. If \r\n you really want to use metric prefixes (e.g. `2MB=2000000`), you must pass `allow_metric=True` and then specify your \r\n units as `2MB`. Prefixes up to exbibyte (`EiB`) are handled at the moment. You can also specify `B` for bytes or `bit` \r\n for a number of bits. If no unit is specified, it uses the `default_units` parameter, which is `B` by default. All \r\n units are case-insensitive.\r\n- Added `as_timedelta()` which will accept values like `30m` and return `datetime.timedelta(minutes=30)`. Valid units \r\n are `s`, `m`, `h`, `d`, `w`, `us`, and `ms`. If no units are specified, it defaults to the `default_units` parameter\r\n which is `s` by default. All units are case-insensitive.\r\n- Added a new series of methods `as_*_ref()` (and `get_ref()`) which mirror the behaviour of their counterparts not ending in `_ref()`\r\n except these return a `_ConfigRef()` instance instead of an actual value.\r\n- Added a method `print_config()` which will print out the configuration to the command line.\r\n\r\n### Version 1.1.0\r\n- Added `as_list()` and `as_set()` which return as expected\r\n- Type-hinting added to the `as_X()` methods to help with usage in your IDE\r\n- Added support for `register_files()` which takes a set of directories to use and registers a set of files and default files in each.\r\n\r\n### Version 1.0.0\r\n\r\n- Stable release after extensive testing on my own\r\n- Python 3.11's tomllib now supported for parsing TOML files\r\n- Using `pymitter` to manage configuration registration was proving problematic when called from\r\n a different thread than where the application config object was instatiated. Replaced it with a more robust solution.\r\n- Fixed a bug for registering default files\r\n- Added `as_dict()` to the configuration object which returns an instance of `MutableDeepDict`.\r\n",
"bugtrack_url": null,
"license": "",
"summary": "Excellent configuration management for Python",
"version": "1.2.4",
"project_urls": {
"Bug Tracker": "https://github.com/turnbullerin/zirconium/issues",
"Homepage": "https://github.com/turnbullerin/zirconium"
},
"split_keywords": [],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "5356dca42fa310f08eea5ca6d6aec9cf1209524ce57c8e8c47085628d09cd1ea",
"md5": "46141ce37b008657660eeb82145e94bd",
"sha256": "0e8eb980050f3243f0f3442f57cf7d566e8d4d42aa5b8de2738e2f5e80a676e8"
},
"downloads": -1,
"filename": "zirconium-1.2.4-py3-none-any.whl",
"has_sig": false,
"md5_digest": "46141ce37b008657660eeb82145e94bd",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.7",
"size": 16188,
"upload_time": "2023-05-18T19:33:42",
"upload_time_iso_8601": "2023-05-18T19:33:42.748205Z",
"url": "https://files.pythonhosted.org/packages/53/56/dca42fa310f08eea5ca6d6aec9cf1209524ce57c8e8c47085628d09cd1ea/zirconium-1.2.4-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "57c26df97c7234fe7dbb4b2fd9dcd8813fd04cdb8a901acc8d8cdc1a733ce042",
"md5": "9c7218c8dcfb9a8178c88889f1e60f66",
"sha256": "6dab2ec78ef11d5b41130191a58b7ea7cac3a629d536cff76e7928386535da54"
},
"downloads": -1,
"filename": "zirconium-1.2.4.tar.gz",
"has_sig": false,
"md5_digest": "9c7218c8dcfb9a8178c88889f1e60f66",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.7",
"size": 23049,
"upload_time": "2023-05-18T19:33:44",
"upload_time_iso_8601": "2023-05-18T19:33:44.544001Z",
"url": "https://files.pythonhosted.org/packages/57/c2/6df97c7234fe7dbb4b2fd9dcd8813fd04cdb8a901acc8d8cdc1a733ce042/zirconium-1.2.4.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2023-05-18 19:33:44",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "turnbullerin",
"github_project": "zirconium",
"travis_ci": false,
"coveralls": true,
"github_actions": false,
"requirements": [
{
"name": "pyyaml",
"specs": []
},
{
"name": "sqlalchemy",
"specs": []
},
{
"name": "toml",
"specs": []
},
{
"name": "autoinject",
"specs": [
[
">=",
"1.3.2"
]
]
}
],
"lcname": "zirconium"
}