The ConfZ Class
===============
Raw Class
---------
Per default, the :class:`~confz.ConfZ` class behaves like `BaseModel` of pydantic and allows to specify your config with
typehints, either using standard Python types or more
`advanced ones `_:
>>> from confz import ConfZ
>>> from pydantic import SecretStr, AnyUrl
>>> class DBConfig(ConfZ):
... user: str
... password: SecretStr
>>> class APIConfig(ConfZ):
... host: AnyUrl
... port: int
... db: DBConfig
`Validators `_ are supported too.
This class can now be instantiated with keyword arguments:
>>> api_config = APIConfig(
... host='http://my-host.com',
... port=1234,
... db={'user': 'my-user', 'password': 'my-password'}
... )
>>> api_config
APIConfig(
host=AnyUrl('http://my-host.com', scheme='http', host='my-host.com', tld='com', host_type='domain'),
port=1234,
db=DBConfig(user='my-user', password=SecretStr('**********'))
)
.. note::
Pydantic sees itself as a parsing library, not a validation library. This means, it may cast input data to force it
to conform to model field types, and in some cases this may result in a loss of information. See
`Data conversion `_ for detailed information.
Since ``api_config`` is a standard python object, your IDE will give you full support like code-completion and
type-checks. It also supports all methods available by `BaseModel` of pydantic, for example:
>>> api_config.json()
'{"host": "http://my-host.com", "port": 1234, "db": {"user": "my-user", "password": "**********"}}'
It is `faux-immutable `_ per default:
>>> api_config.port = 1
TypeError: "APIConfig" is immutable and does not support item assignment
Sources as Keyword
------------------
In most cases, we would not want to provide the config as keyword arguments. Instead, we can provide
:class:`~confz.ConfZSources` as argument `config_sources` and :class:`~confz.ConfZ` will load them. For example,
if we have a config file in yaml format like this:
.. code-block:: yaml
host: http://my-host.com
port: 1234
db:
user: my-user
password: my-password
We can load this file as follows:
>>> from pathlib import Path
>>> from confz import ConfZFileSource
>>> APIConfig(config_sources=ConfZFileSource(file=Path('/path/to/config.yaml')))
APIConfig(
host=AnyUrl('http://my-host.com', scheme='http', host='my-host.com', tld='com', host_type='domain'),
port=1234,
db=DBConfig(user='my-user', password=SecretStr('**********'))
)
ConfZ supports a rich set of sources, see :ref:`sources_loaders`. Of course, keyword arguments and config sources can
also be combined.
Sources as Class Variable
-------------------------
Defining config sources as keyword argument still requires you to explicitly instantiate your config class and passing
it to all corresponding software components. :class:`~confz.ConfZ` provides an alternative to this by defining your
source as a class variable `CONFIG_SOURCES`:
>>> class DBConfig(ConfZ):
... user: str
... password: SecretStr
>>> class APIConfig(ConfZ):
... host: AnyUrl
... port: int
... db: DBConfig
...
... CONFIG_SOURCES = ConfZFileSource(file=Path('/path/to/config.yaml'))
From now on, your config values are accessible from anywhere within your code by just importing ``APIConfig`` and
instantiating it:
>>> APIConfig().port
1234
>>> APIConfig().db.user
'my-user'
By defining `CONFIG_SOURCES`, your class will furthermore automatically be a singleton. The first time you access
the constructor, the config sources are loaded. All successive calls will return the same cached instance
(lazy loading):
>>> APIConfig() is APIConfig()
True
As a consequence, an error will be raised if you try to pass keyword arguments to a config class with `CONFIG_SOURCES`
set.
Early Loading
^^^^^^^^^^^^^
:class:`~confz.ConfZ` could also load your config sources directly during class creation. However, this yields unwanted
side effects like reading files and command line arguments during import of your config classes, which should be
avoided. Thus, :class:`~confz.ConfZ` loads your config the first time you instantiate the class.
If at this point the config class cannot populate all mandatory fields, pydantic will raise an error. To make sure
this does not happen in an inconvenient moment, you can also manually load all configs at the beginning of your
program::
from confz import validate_all_configs
if __name__ == '__main__':
validate_all_configs()
# your application code
The function :func:`~confz.validate_all_configs` will instantiate all config classes defined in your code at any
(reachable) location that have `CONFIG_SOURCES` set.