Skip to content

Configuration API

The application API of the counterpart package, which is read-only and focused on loading.

advexp.configuration.Configuration

Base class for read-only application configuration.

All operations are class-level: the configuration class itself is the singleton. Instances are supported for reads, but they share the class storage. :meth:to_object copies the values out to a detached instance of the class, and :meth:to_dict copies them to a read-only mapping.

Sources are only ever read. There is no write-back pass; this is the main difference between this package and :mod:advexp.settings. The values themselves are set only by a load: assigning to a declared member, on the class or on an instance, raises TypeError. A :meth:to_object snapshot is completely frozen: none of its attributes can be written or deleted.

A member declared without default= is required: it must get its value from a source. If a load finds nothing usable for a required member, it raises one abort of the package's load pass, :class:~advexp.configuration.AggregateLoadError, with an entry for each failed member (a distinct class for each cause, all subclasses of :class:~advexp.settings_base.member_load_error.MemberLoadError). It raises one error however many members failed, so one run reports them all. Reading a required member that was never loaded raises the same error.

An optional member never aborts a load: it stands at its default, and its load error goes into the report that :meth:load_configuration returns. If a run has both an abort and fallbacks, the fallbacks are kept next to the abort in its fallbacks attribute.

:data:~advexp.settings_base.required.REQUIRED is the explicit spelling of a required member, and the default parameter of the bundled sources defaults to it. :data:~advexp.settings_base.no_default.NO_DEFAULT keeps its family-wide meaning: the member is optional, and None stands in for it.

load_configuration(overrides=None) classmethod

Load all declared members from their sources and report the fallbacks.

Every member is loaded, whatever happened to the previous one. A required member (one declared REQUIRED) whose sources provide nothing usable records an error. The error names the cause (no value, a value that cannot be converted, a constraint violation, or a failed source), the member, its setting, and the class. An optional member records the same error, but it stands at its default instead.

A plain miss is not an error. For an optional member, a miss is the normal way in which the default gets used, so it is not reported at the usual log levels; a debug record names the address that came up empty. When the pass aborts, the optional misses are attached to the abort in the aggregate's misses, so the reader of the failure can see which optional members stood at their defaults.

At the end, one abort reports every required failure at once. It is always :class:~advexp.configuration.AggregateLoadError, however many members failed. Its errors tuple holds each member's own error, and its fallbacks tuple holds the optional fallbacks of the same run. If there is no required failure, there is no abort: the method returns the fallbacks as its report (an empty tuple when every member loaded cleanly), so a caller can learn what fell back without parsing logs. A user-facing source condition (a corrupt or inaccessible source) does not wait for the end: if the failing member has a default that would mask it, the condition propagates and aborts the whole pass immediately.

Each member is resolved through the class's source chain, and the first link that provides a value wins. A member declared through a family's factory reads only the links of that family. A broken link ends the walk down the chain, so the layers below it never hide the problem.

overrides is a layer above the whole chain, not a declared link. Its keys are member names, and its values go through the same conversion and constraint checks as the values of a link. It can reach any member, even one that a path= hides. Keys that name no member (for example, a model_dump() of another model may carry keys that this class never declared) and keys that name constant members (which no override reaches) are dropped, with one debug record for each. A foreign dictionary therefore never makes the load fail.

The pass builds the class state from scratch and applies it in one step. A completed load fully determines the state: every member takes either the value that its sources produced or its declared default. A value from an earlier load therefore never survives when the current sources hold nothing for that member, so one class can be reused across loads, and each pass stands on its own. A pass that ends in the aggregate abort, or in a propagating source condition, leaves the class exactly as the call found it: no values are partly applied, and no defaults are served by the failed run. So if a reload fails, the process keeps the configuration that it is already running with. Nothing is written back: sources are read-only for this class, and the load pass is the only writer of the values themselves.

Library configuration

advexp.configuration.LibraryConfiguration

Library-wide configuration (all state is class-level).

The registries of the family live here (attributes to serializers, plugin classes, and class-metadata validators), next to the parameter that an application touches: the log level of the whole family. The form of storage names is not part of it: a name pattern is the own vocabulary of the naming backend, and every backend family carries its own.

register_settings_plugin(plugin_class) classmethod

Register a plugin class and call its setup() hook, if there is one.

A plugin may define a setup() class method. It runs once, right at registration, and it is the place where a plugin usually registers its attribute-to-serializer pairs through :meth:register_settings_attribute. setup() is optional: a plugin may register its pieces inline at import time instead.

:class:~advexp.settings_base.settings_base_plugin.SettingsBasePlugin ships a setup(). It registers the declared attribute_class/serializer_class pair, so a plugin that derives from it usually declares the pair instead of writing its own setup(). Bundled plugins call this method when their module is imported, which is what makes importing a plugin module enable the plugin.

Registering a plugin that is already registered is a no-op: the setup() hook does not run a second time, so repeated imports or an explicit re-registration leave the state of the plugin untouched.

registered_plugin_classes() classmethod

Return the registered plugin classes, in registration order.

register_settings_attribute(attribute_class, serializer_factory) classmethod

Map an attribute class to the serializer class that serves its settings.

Registering the same pair again is a no-op: several plugins may register one shared attribute-to-serializer pair. Registering the same attribute class with a different serializer class raises :class:ValueError: two backends that claim one attribute are a conflict, not an override.

A dynamic-attribute pin is registered in the same way, as a slot of its own: the serializer that is bound to it serves only the dynamic entries, and it never touches the registration of the attribute class that the pin namespaces.

register_class_metadata_validator(metadata_name, validator) classmethod

Map a class metadata key to the validator of its values.

A plugin family that owns a metadata key (its options vocabulary) registers the validator when its module is imported. This is the same convention that the plugin registry follows, where the import enables the plugin.

The machinery then checks every stored value. A declaration in a class body makes the class statement fail, and a runtime call of :meth:~advexp.settings_base.context.SettingsContext.set_class_metadata raises an error before the value is stored. Keys that have no validator belong to the open channel alone: whatever the application stores there is never questioned.

Registering the same pair again is a no-op: a family module that is imported twice leaves the state untouched. Registering the same key with a different validator raises :class:ValueError: two families that claim one key are a conflict, not an override.

set_log_level(level) classmethod

Set the log level on the root of the family, which governs every channel.

The records of the machinery are carried by the channel of the distribution that drives them (advexp.settings_base, or the own channel of a distribution). All of these channels are under the advexp root, so one level that is set here covers them, whichever distribution is in use. A channel that has its own explicitly set level keeps it, because the logging module resolves the nearest level that is set.

logger() classmethod

Return the logging channel of the family, ready to be configured.

Every channel of the family (the own channel of a distribution, and the one of the foundation) lives under the root of the family. One object therefore receives every record that the machinery writes: you can set a level on it or add a handler with a formatter to it, all with standard logging. The name stays the own spelling of the library: the application configures the channel that it was handed, and never one that it looked up by name:

handler = logging.StreamHandler()
handler.setFormatter(
    logging.Formatter("%(levelname)s:%(name)s: %(message)s")
)
logger = LibraryConfiguration.logger()
logger.addHandler(handler)
logger.propagate = False  # the application's root stays quiet

The propagation parameter matters when the application also configures the root logger. logging.basicConfig() puts a handler there, so a channel that is configured here would duplicate its records. Turning the propagation off prevents this. An application that configures nothing but basicConfig needs none of this: the records flow to its root in the usual way.

The source chain

advexp.configuration.Sources(*bindings)

The source chain of a layered class, in its resolution order.

It is built from the results of the source builders (Sources(env_source(...), json_source(...))), and the first link that is named is the first one that is read. Iteration returns the links in that order. The chain itself is plain data that the load pass walks.

An empty chain cannot be created: a layered class with nothing to read from is a mistake in the declaration, and the class statement is the place to report it.

bindings property

The source builders' results the chain was built from.

The chain's links, in the order a load reads them.

Attribute parameters

The forms that a member's declaration can take. The chain and constant parameters come from the package root; the file families provide theirs in their plugin modules — the same modules that the examples import as js, ts, ys, env and dotenv.

advexp.configuration.chain_setting(*, name=None, default=REQUIRED, metadata=None, aliases=None, gt=None, ge=None, lt=None, le=None, eq=None, ne=None, min_length=None, max_length=None, regex=None, validator=None, when=None)

Declare a member that the whole source chain resolves. This is the type-checker-friendly spelling of :class:ChainSetting.

It is the rich form of the plain declaration: the member reads the class's chain from top to bottom, and every link builds its address by the rules of its own family. If default= is omitted (or is REQUIRED), the member is required: the chain must provide its value, or the load aborts.

name= is the full literal address in every named link: the prefix, the case, and the key pattern never change it. aliases= are full literal addresses in the same way. A load tries the primary address first, then the aliases in their declared order, in every named link of the chain. The mapping link is the exception: its dictionary knows only the member name.

The keyword parameters after aliases are the declared value constraints: gt/ge/lt/le/eq/ne, min_length/max_length, regex, validator, and when. They are applied to whatever the chain provides, and they are the same constraints that every member factory of the family applies.

advexp.configuration.ChainSetting(*, name=None, default=REQUIRED, metadata=None, aliases=None, gt=None, ge=None, lt=None, le=None, eq=None, ne=None, min_length=None, max_length=None, regex=None, validator=None, when=None)

The attribute of a member that the whole source chain resolves.

The attribute names the setting, not the storage. A member declared through it has no source of its own: it reads the class's chain from top to bottom, and the first link that provides a value wins. Both the plain declaration (token: str) and the rich one (:func:chain_setting) produce this attribute. Each named link builds the member's address by the rules of its own family (the environment prefix and case, the JSON key pattern), so the same member is addressed differently in different links. A declared aliases= entry is a full literal address in every named link.

advexp.configuration.constant_setting(*, default=REQUIRED, metadata=None, gt=None, ge=None, lt=None, le=None, eq=None, ne=None, min_length=None, max_length=None, regex=None, validator=None, when=None)

Declare a constant member. This is the type-checker-friendly spelling of :class:ConstantSetting.

The member lives outside the source chain: its value is the declared default=, every load serves exactly that value, and the load overrides (load_configuration(overrides={...})) skip the member and write a debug record. The value goes through the same declared-default machinery as any other member: it is converted to the member's annotation at the class statement, so a value in storage form ("0:30:00" for a timedelta member) works as it does for any other member. default= is required: a constant without a value could never be loaded, so the factory refuses it.

The keyword parameters after metadata are the declared value constraints. gt/ge/lt/le/eq/ne compare the value, min_length and max_length bound len(value), regex is an unanchored pattern that a str value must match, and validator is a callable that receives the value and returns whether it passes. when= names another member: while that member's value is falsy, the whole set of constraints is switched off. The constraints check the declared value in the same way as for any other member: a constant is immune to the sources, but not to its own declaration.

advexp.configuration.ConstantSetting(*, default=REQUIRED, metadata=None, gt=None, ge=None, lt=None, le=None, eq=None, ne=None, min_length=None, max_length=None, regex=None, validator=None, when=None)

The attribute of a member outside the source chain.

The member's value is its declared default, and nothing else: no link of the chain is asked for it, the load overrides skip it, and a reload serves the same declared value again. Examples are a schema version pinned in code, or a feature flag that the sources must not control. This is a member that the application states about itself, rather than reads from its environment. The declaration requires a value: a constant member without one could never be loaded, so the factory refuses it outright.

SOURCELESS = True class-attribute instance-attribute

The load machinery's mark: no source is ever asked for this member, and the load overrides skip it.

advexp.configuration.plugins.json_settings.json_setting(*, name=None, default=REQUIRED, path=None, metadata=None, aliases=None, gt=None, ge=None, lt=None, le=None, eq=None, ne=None, min_length=None, max_length=None, regex=None, validator=None, when=None)

Declare a JSON-file member. This is the type-checker-friendly spelling of JSONSetting.

It builds exactly the attribute that the class builds, with the same keyword parameters. Both spellings exist because of typing. Assigning an attribute instance to an annotated member (port: int = JSONSetting(...)) is valid Python (the settings metaclass replaces the attribute with a value descriptor), but type checkers cannot see the replacement and report a type mismatch. They accept a plain function that returns Any. The Any return also leaves default= unchecked, deliberately: a default may be given in its storage form (default="4" for an int member), and it is converted at class creation, where a default that cannot be converted makes the class statement fail.

default=REQUIRED (which is also the value used when default= is omitted) makes the member required: a configuration class has no placeholder value for it. The load aborts with :class:~advexp.settings_base.missing_value_error.MissingValueError when the document holds nothing usable for the member, and reading a member that was never loaded raises the same error. :data:NO_DEFAULT keeps the meaning that it has in the whole family: the member is optional, and None stands in for it.

aliases= declares additional full storage keys. A load looks up the primary key first, then the aliases in their order, and the first key found wins. Every alias is read under the same path=.

The keyword parameters after aliases are the declared value constraints (gt/ge/lt/le/eq/ne, min_length/max_length, regex, validator, when). They are the same constraints that the base attribute documents, and they are applied wherever a value reaches the member.

advexp.configuration.plugins.json_settings.JSONSetting(*, name=None, default=REQUIRED, path=None, metadata=None, aliases=None, gt=None, ge=None, lt=None, le=None, eq=None, ne=None, min_length=None, max_length=None, regex=None, validator=None, when=None)

Setting attribute: the value is read from the JSON configuration file.

name= is the storage key and may contain any characters. Without it, the {module}.{class}.{member} pattern is used. The optional path= locates the key in the document tree as a JSON Pointer (RFC 6901): a leading / and slash-separated segments, where ~1 escapes / and ~0 escapes ~. For example:

  • path="/net/proxy" reads the setting from {"net": {"proxy": {...}}}.
  • path="/mirrors/1" reads it from the second element of the mirrors array.
  • path="/endpoints/api.internal" reads it from under a key that contains a dot.

A segment that consists only of digits indexes an array if the node it steps into is an array, and names an object key otherwise. path="" and path=None both mean the document root.

advexp.configuration.plugins.toml_settings.toml_setting(*, name=None, default=REQUIRED, path=None, metadata=None, aliases=None, gt=None, ge=None, lt=None, le=None, eq=None, ne=None, min_length=None, max_length=None, regex=None, validator=None, when=None)

Declare a TOML-file member. This is the type-checker-friendly spelling of TOMLSetting.

It builds exactly the attribute that the class builds, with the same keyword parameters. Both spellings exist because of typing. Assigning an attribute instance to an annotated member (port: int = TOMLSetting(...)) is valid Python (the settings metaclass replaces the attribute with a value descriptor), but type checkers cannot see the replacement and report a type mismatch. They accept a plain function that returns Any. The Any return also leaves default= unchecked, deliberately: a default may be given in its storage form (default="4" for an int member), and it is converted at class creation, where a default that cannot be converted makes the class statement fail.

default=REQUIRED (which is also the value used when default= is omitted) makes the member required: a configuration class has no placeholder value for it. The load aborts with :class:~advexp.settings_base.missing_value_error.MissingValueError when the document holds nothing usable for the member, and reading a member that was never loaded raises the same error. :data:NO_DEFAULT keeps the meaning that it has in the whole family: the member is optional, and None stands in for it.

aliases= declares additional full storage addresses. A load looks up the primary address first, then the aliases in their order, and the first address found wins. Every alias is read under the same path= and is nested by its own dots.

The keyword parameters after aliases are the declared value constraints (gt/ge/lt/le/eq/ne, min_length/max_length, regex, validator, when). They are the same constraints that the base attribute documents, and they are applied wherever a value reaches the member.

advexp.configuration.plugins.toml_settings.TOMLSetting(*, name=None, default=REQUIRED, path=None, metadata=None, aliases=None, gt=None, ge=None, lt=None, le=None, eq=None, ne=None, min_length=None, max_length=None, regex=None, validator=None, when=None)

Setting attribute: the value is read from the TOML configuration file.

name= is the address of the member in the document. Every dot in the name (the table separator of TOML) separates a table level, and the last part is the key. For example, name="net.host" reads from the [net] table, and a name without a dot is a plain key. Without a name, the {module}.{class}.{member} pattern is used, and its dots nest in the same way. A literal key that contains a dot is reached through path=, because the segments of a pointer are never split.

The optional path= locates the key in the document tree as a JSON Pointer (RFC 6901): a leading / and slash-separated segments, where ~1 escapes / and ~0 escapes ~. For example:

  • path="/net/proxy" reads the setting from [net.proxy].
  • path="/mirrors/1" reads it from the second element of the mirrors array.
  • path="/endpoints/api.internal" reads it from under a key that contains a dot.

A segment that consists only of digits indexes an array if the node it steps into is an array, and names a table key otherwise. path="" and path=None both mean the document root.

advexp.configuration.plugins.yaml_settings.yaml_setting(*, name=None, default=REQUIRED, path=None, metadata=None, aliases=None, gt=None, ge=None, lt=None, le=None, eq=None, ne=None, min_length=None, max_length=None, regex=None, validator=None, when=None)

Declare a YAML-file member. This is the type-checker-friendly spelling of YAMLSetting.

It builds exactly the attribute that the class builds, with the same keyword parameters. Both spellings exist because of typing. Assigning an attribute instance to an annotated member (port: int = YAMLSetting(...)) is valid Python (the settings metaclass replaces the attribute with a value descriptor), but type checkers cannot see the replacement and report a type mismatch. They accept a plain function that returns Any. The Any return also leaves default= unchecked, deliberately: a default may be given in its storage form (default="4" for an int member), and it is converted at class creation, where a default that cannot be converted makes the class statement fail.

default=REQUIRED (which is also the value used when default= is omitted) makes the member required: a configuration class has no placeholder value for it. The load aborts with :class:~advexp.settings_base.missing_value_error.MissingValueError when the document holds nothing usable for the member, and reading a member that was never loaded raises the same error. :data:NO_DEFAULT keeps the meaning that it has in the whole family: the member is optional, and None stands in for it.

aliases= declares additional full storage keys. A load looks up the primary key first, then the aliases in their order, and the first key found wins. Every alias is read under the same path=.

The keyword parameters after aliases are the declared value constraints (gt/ge/lt/le/eq/ne, min_length/max_length, regex, validator, when). They are the same constraints that the base attribute documents, and they are applied wherever a value reaches the member.

advexp.configuration.plugins.yaml_settings.YAMLSetting(*, name=None, default=REQUIRED, path=None, metadata=None, aliases=None, gt=None, ge=None, lt=None, le=None, eq=None, ne=None, min_length=None, max_length=None, regex=None, validator=None, when=None)

Setting attribute: the value is read from the YAML configuration file.

name= is the storage key and may contain any characters. A dot in it stays inside the key: the name is flat and does not create a nesting level. Without name=, the {module}.{class}.{member} pattern is used. The optional path= locates the key in the document tree as a JSON Pointer (RFC 6901): a leading / and slash-separated segments, where ~1 escapes / and ~0 escapes ~. For example:

  • path="/net/proxy" reads the setting from {net: {proxy: {...}}}.
  • path="/mirrors/1" reads it from the second element of the mirrors sequence.
  • path="/endpoints/api.internal" reads it from under a key that contains a dot.

A segment that consists only of digits indexes a sequence if the node it steps into is a sequence, and names a mapping key otherwise. path="" and path=None both mean the document root.

advexp.configuration.plugins.environment.variables.environment_variable_setting(*, name=None, default=REQUIRED, metadata=None, aliases=None, gt=None, ge=None, lt=None, le=None, eq=None, ne=None, min_length=None, max_length=None, regex=None, validator=None, when=None)

Declare an environment-variable member. This is the type-checker-friendly spelling of EnvironmentVariableSetting.

It is the same attribute with the same keyword parameters. Only the spelling differs, and the reason for it is typing. Assigning an attribute instance to an annotated member is valid Python (the settings metaclass replaces it with a value descriptor), but type checkers cannot see the replacement and report a type mismatch. They accept a plain function that returns Any, and the Any also prevents a default in storage form (default="8080" for an int member) from being flagged.

default=REQUIRED (which is also the value used when default= is omitted) makes the member required: a configuration class has no placeholder value for it. The load aborts with :class:~advexp.settings_base.missing_value_error.MissingValueError when the variable holds nothing usable, and reading a member that was never loaded raises the same error. :data:NO_DEFAULT keeps the meaning that it has in the whole family: the member is optional, and None stands in for it.

aliases= declares additional full variable names. A load looks up the primary name first, then the aliases in their order, and the first name found wins. This is how a rename is migrated: the old variable name keeps working next to the new one.

The keyword parameters after aliases are the declared value constraints (gt/ge/lt/le/eq/ne, min_length/max_length, regex, validator, when). They are the same constraints that the base attribute documents, and they are applied wherever a value reaches the member.

advexp.configuration.plugins.environment.dotenv.dotenv_setting(*, name=None, default=REQUIRED, metadata=None, aliases=None, gt=None, ge=None, lt=None, le=None, eq=None, ne=None, min_length=None, max_length=None, regex=None, validator=None, when=None)

Declare a dotenv-file member. This is the type-checker-friendly spelling of DotenvSetting.

It is the same attribute with the same keyword parameters. Only the spelling differs, and the reason for it is typing. Assigning an attribute instance to an annotated member is valid Python (the settings metaclass replaces it with a value descriptor), but type checkers cannot see the replacement and report a type mismatch. They accept a plain function that returns Any, and the Any also prevents a default in storage form (default="8080" for an int member) from being flagged.

default=REQUIRED (which is also the value used when default= is omitted) makes the member required: a configuration class has no placeholder value for it. The load aborts with :class:~advexp.settings_base.missing_value_error.MissingValueError when the variable holds nothing usable, and reading a member that was never loaded raises the same error. :data:NO_DEFAULT keeps the meaning that it has in the whole family: the member is optional, and None stands in for it.

aliases= declares additional full variable names. A load looks up the primary name first, then the aliases in their order, and the first name found wins.

The keyword parameters after aliases are the declared value constraints (gt/ge/lt/le/eq/ne, min_length/max_length, regex, validator, when). They are the same constraints that the base attribute documents, and they are applied wherever a value reaches the member.

Source builders

The links that a source chain is built from. They are passed as the options= argument of the class statement, with one builder per family, either inside Sources(...) or on its own.

advexp.configuration.plugins.json_settings.json_source(file, path=None, name_pattern=None, dynamic_file=None, dynamic_of=None, parameters=None)

Declare a JSON file link, in the form used by this distribution.

Pass the result of the builder to the options= keyword of the class statement, either alone (for a class that reads nothing else) or inside Sources(...) as one link of the chain:

class AppConfig(Configuration, options=Sources(
    env_source(name_prefix="APP_"),
    json_source(file="app.json"),
)):

file names the file that the link reads. It may contain template slots that are resolved through environment variables: json_source(file="settings-{APP_ENV}.json", parameters=app_env) reads the file of the environment that APP_ENV names. The parameters are declared once next to the chain and are shared by every templated link.

path anchors every key of the link inside a subtree of the document, as a JSON Pointer (RFC 6901). The path= of a member locates its key within the subtree, and the path of the link is added in front of it: json_source(file="app.json", path="/env/prod") reads members with path="/server" from ["env"]["prod"]["server"]. A member that must live outside the subtree needs a link of its own: the same file under another path=.

name_pattern sets the pattern that builds the keys. If it is omitted, the full {module}.{class}.{member} pattern is used. The link belongs to the class that declares it: the own chain of a subclass replaces it for that subclass only, and the base class keeps reading its own file.

The dynamic side reads the group of a writer. dynamic_of names the group key of the writing class, which is the name under which its dynamic entries are stored in the document ("app.LocalSettings" for a writer class with the default name). Without it, the class reads no dynamic entries. By default, the entries are in the file of the link (one document for both sides). dynamic_file gives them a file of their own, which is resolved through the same parameters if it contains template slots. The class reaches the collection through the JSON dynamic plugin of advexp.configuration.plugins.json_dynamic_settings.

advexp.configuration.plugins.toml_settings.toml_source(file, path=None, name_pattern=None, dynamic_file=None, dynamic_of=None, parameters=None)

Declare a TOML file link, in the form used by this distribution.

Pass the result of the builder to the options= keyword of the class statement, either alone (for a class that reads nothing else) or inside Sources(...) as one link of the chain:

class AppConfig(Configuration, options=Sources(
    env_source(name_prefix="APP_"),
    toml_source(file="app.toml"),
)):

file names the file that the link reads. It may contain template slots that are resolved through environment variables: toml_source(file="settings-{APP_ENV}.toml", parameters=app_env) reads the file of the environment that APP_ENV names.

path anchors every key of the link inside a subtree of the document, as a JSON Pointer (RFC 6901). The path= of a member locates its key within the subtree, and the path of the link is added in front of it.

name_pattern sets the pattern that builds the keys. If it is omitted, the full {module}.{class}.{member} pattern is used, and its dots are the table levels through which the address is nested. The link belongs to the class that declares it: the own chain of a subclass replaces it for that subclass only, and the base class keeps reading its own file.

The dynamic side reads the group of a writer. dynamic_of names the group key of the writing class, which is the name under which its dynamic entries are stored in the document ("app.LocalSettings" for a writer class with the default name). Without it, the class reads no dynamic entries. By default, the entries are in the file of the link (one document for both sides). dynamic_file gives them a file of their own, which is resolved through the same parameters if it contains template slots. The class reaches the collection through the TOML dynamic plugin of advexp.configuration.plugins.toml_dynamic_settings.

advexp.configuration.plugins.yaml_settings.yaml_source(file, path=None, name_pattern=None, dynamic_file=None, dynamic_of=None, parameters=None)

Declare a YAML file link, in the form used by this distribution.

Pass the result of the builder to the options= keyword of the class statement, either alone (for a class that reads nothing else) or inside Sources(...) as one link of the chain:

class AppConfig(Configuration, options=Sources(
    env_source(name_prefix="APP_"),
    yaml_source(file="app.yaml"),
)):

file names the file that the link reads. The extension is not checked: any path works. The name may contain template slots that are resolved through environment variables: yaml_source(file="settings-{APP_ENV}.yaml", parameters=app_env) reads the file of the environment that APP_ENV names. The parameters are declared once next to the chain and are shared by every templated link.

path anchors every key of the link inside a subtree of the document, as a JSON Pointer (RFC 6901). The path= of a member locates its key within the subtree, and the path of the link is added in front of it: yaml_source(file="app.yaml", path="/env/prod") reads members with path="/server" from ["env"]["prod"]["server"]. A member that must live outside the subtree needs a link of its own: the same file under another path=.

name_pattern sets the pattern that builds the keys. If it is omitted, the full {module}.{class}.{member} pattern is used. The link belongs to the class that declares it: the own chain of a subclass replaces it for that subclass only, and the base class keeps reading its own file.

The dynamic side reads the group of a writer. dynamic_of names the group key of the writing class, which is the name under which its dynamic entries are stored in the document ("app.LocalSettings" for a writer class with the default name). Without it, the class reads no dynamic entries. By default, the entries are in the file of the link (one document for both sides). dynamic_file gives them a file of their own, which is resolved through the same parameters if it contains template slots. The class reaches the collection through the YAML dynamic plugin of advexp.configuration.plugins.yaml_dynamic_settings.

advexp.configuration.plugins.environment.variables.env_source(name_prefix=None, name_case=None, nested_delimiter=None)

Declare a process-environment link, in the form used by this distribution.

Pass the result of the builder to the options= keyword of the class statement, either alone (for a class that reads nothing else) or inside Sources(...) as one link of the chain:

class AppConfig(Configuration, options=Sources(
    env_source(name_prefix="APP_", name_case=NameCase.UPPER),
    json_source(file="app.json"),
)):

The naming parameters name_prefix, name_case and nested_delimiter declare the naming dictionary of the family for this class. A class has only one seat for these rules, and every environment link of the chain shares it (the process environment and a .env file form one namespace). Declaring the parameters on two environment links in one options= makes the class statement fail, because two records of one dictionary could drift apart through a typo. A link without naming parameters reads the dictionary that the nearest environment link above it declared. If there is none, member names are used as written, with no prefix. The dictionary belongs to the class that declares it: a subclass's own link with naming parameters adds a dictionary of its own, and the members of the base class keep theirs.

advexp.configuration.plugins.environment.variables.NameCase

The letter-case regime of the member-name part of an environment name.

This is the spelling of the name_case option in code: name_case=NameCase.UPPER.

  • UPPER and LOWER change the case of the member name in every built name.
  • ANY leaves the built name as declared, and makes its lookup case-insensitive instead.

It is a str enum, because its members also serve as the values of the stored data form. The class metadata keeps the plain "upper"/"lower"/"any" strings, and the raw metadata channel (a hand-written entry, or a runtime set_class_metadata call) accepts both spellings. A member compares equal to its string.

advexp.configuration.plugins.environment.dotenv.dotenv_source(file, name_prefix=None, name_case=None, nested_delimiter=None, parameters=None)

Declare a .env file link, in the form used by this distribution.

Pass the result of the builder to the options= keyword of the class statement, either alone (for a class that reads nothing else) or inside Sources(...) as one link of the chain:

class AppConfig(Configuration, options=Sources(
    env_source(name_prefix="APP_", name_case=NameCase.UPPER),
    dotenv_source(file="app.env"),
)):

file names the file that the link reads. A missing file behaves as an empty one: required members are reported, and defaults stand in. The name may contain template slots that are filled in from environment variables: dotenv_source(file=".env-{APP_ENV}", parameters=app_env) reads the file of the environment that APP_ENV names. The naming parameters name_prefix, name_case, and nested_delimiter declare the naming dictionary of the family for this class, which has only one seat for these rules; see :func:advexp.configuration.plugins.environment.variables.env_source for the rules of the seat. The link belongs to the class that declares it: a subclass's own chain replaces it for that subclass only, and the base class keeps reading its own file.

advexp.configuration.plugins.mapping_source.mapping_source(mapping=None)

Declare a mapping link: a dictionary that is read as a source.

Pass the result of the builder to the options= keyword of the class statement. You can pass it alone (for a class whose members all come from the dictionary) or inside Sources(...) as the bottom layer of the chain, which is the last place a load looks before the declared defaults:

BASE_DEFAULTS = {"token": "dev-token", "port": 8080}

class AppConfig(Configuration, options=Sources(
    env_source(name_prefix="APP_"),
    json_source(file="app.json"),
    mapping_source(BASE_DEFAULTS),
)):
    token: str

The dictionary that is passed in is copied at the call, so a later change to the caller's object never reaches the chain. A value that a load finds is served as a fresh copy in the same way. The dictionary uses member names only: an explicit name= or aliases= of a member changes nothing here, and a key that names no member is dropped, with a debug record. This form is not meant for a dictionary that the application changes between loads; the load overrides serve that purpose.

If the builder is called without a dictionary, it declares the live form instead. The chain gets an empty mapping layer, and the application fills its dictionary at runtime through mapping_source_for(AppConfig).set({...}). A load that runs before any set passes over the layer as a miss. In both forms the source works in the same way: it uses member names only, serves fresh copies of the values, and is never written back.

advexp.configuration.plugins.mapping_source.mapping_source_for(cls)

The live mapping source of cls: the chain link that set() fills.

Declare the link with mapping_source() (the builder called without a dictionary), and then reach it from the class:

mapping_source_for(AppConfig).set({"token": "dev-token"})
AppConfig.load_configuration()

The resolver reads the nearest chain. A subclass without a chain of its own shares the mapping source of its base class: one dictionary, one set. The resolver raises an error that names the class in these cases: the class was not built by the family, the class has no chain, the chain has no live link (the form that takes a dictionary has no handle for filling it), or the chain has more than one live link.

The link of the live mapping source: a dictionary that is filled at runtime.

The link carries the serializer of the mapping source and its dictionary as the own parameter of the link. This is the same place where the dictionary of the static form sits, and it is served in the same way: by member names only, with every value returned as a fresh copy. The dictionary starts empty, and set(mapping) is the only way to write to it; nothing else changes it.

set(mapping)

Replace the dictionary with mapping. The link stores a copy, not a view.

The replacement is complete: keys that the new dictionary does not contain are removed, whatever the previous dictionary held. The dictionary is deep-copied on the way in, so afterwards the caller's object and the state of the link share nothing. In the same way, a load serves every value that it finds as a fresh copy. A load reads the dictionary once, at its start, so a replacement made while a load is running takes effect from the next load.

Path parameters

advexp.configuration.env_parameter(variable, choices, default=None, slot=None)

Declare one path parameter that is read from an environment variable.

Pass the result to the parameters= of a builder of a source that carries a file, either alone or in a tuple:

app_env = env_parameter("APP_ENV", choices=("dev", "test", "prod"))

class AppConfig(Configuration, options=Sources(
    json_source(file="app.json"),
    json_source(file="settings-{APP_ENV}.json", parameters=app_env),
)):

variable names the environment variable, and the template spells the slot by that name: settings-{APP_ENV}.json.

slot= renames the slot if the template spells another name. The value still comes from variable, and the template spells the renamed slot, so slot="env" reads APP_ENV into settings-{env}.json. This is useful for a template that dictates its own slot names (a file shared with another system, or a path that reads better with a short slot). The name must be a valid identifier: otherwise, the braces would stay as literal text instead of becoming a slot.

choices is the obligatory whitelist of which the value of the variable must be one. It is checked before any file is touched.

default is the value to which an unset variable resolves. It must be one of the choices. Without it, an unset variable breaks the link at load time.

Module file declarations

The reading defaults of the dynamic side can have a file of their own, just as the backends of the settings package can. A module declares its own file with for_this_module, and the process-wide attribute provides the file for all other modules.

advexp.configuration.for_this_module(value)

Declare the value of a parameter for the module in which this call is written.

Write the call in the assignment itself, at the top level of the module:

holder.file_path = for_this_module(Path("app-settings.json"))

The function names the calling module at the call, and the metaclass of the holder consumes the marker at the moment when the assignment executes, during the import of the module. If you wrap the call in a helper of another module, it would name that other module instead: the declaration belongs to the assignment that it addresses.

The call is typed as Any on purpose: the marker can be assigned to every parameter that a holder declares, whatever the own type of the parameter is, and the runtime validates the value at the line of the assignment.

advexp.configuration.declared_module_defaults()

Return a snapshot of every module declaration: (holder, parameter, module) keys over the declared values.

This is the reader for tools that collect the declarations without running the application. Importing the settings modules fills the registry, because a declaration at the module level executes at import, and one call hands everything to the collector. The snapshot is a deep copy: changing it changes nothing in the registry.

advexp.configuration.plugins.json_dynamic_settings.JSONDynamicSettingsConfiguration

Dynamic JSON configuration reading (class-level state).

file_path = None class-attribute instance-attribute

Own file for the dynamic entries this package reads (str or os.PathLike).

When set, every dynamic collection without a binding of its own reads this file, apart from the file of the declared members; the document inside is shaped by the section parameters of the reading plugin, placed exactly as the writing side laid it out.

A module can declare the dynamic entries' own file for itself alone — at the module's top level, with for_this_module from the package root:

JSONDynamicSettingsConfiguration.file_path = for_this_module(
    Path("dynamic-settings.json")
)

The declaration holds for the classes of the module it is written in and wins over this attribute; modules without one keep reading the attribute. When both are unset (the default), the dynamic entries are read from the class's source file — the link's own file=: the one-document default the writing side writes by default too.

advexp.configuration.plugins.toml_dynamic_settings.TOMLDynamicSettingsConfiguration

Dynamic TOML configuration reading (class-level state).

file_path = None class-attribute instance-attribute

Own file for the dynamic entries this package reads (str or os.PathLike).

When set, every dynamic collection without a binding of its own reads this file, apart from the file of the declared members; the document inside is shaped by the section parameters of the reading plugin, placed exactly as the writing side laid it out.

A module can declare the dynamic entries' own file for itself alone — at the module's top level, with for_this_module from the package root:

TOMLDynamicSettingsConfiguration.file_path = for_this_module(
    Path("dynamic-settings.toml")
)

The declaration holds for the classes of the module it is written in and wins over this attribute; modules without one keep reading the attribute. When both are unset (the default), the dynamic entries are read from the class's source file — the link's own file=: the one-document default the writing side writes by default too.

advexp.configuration.plugins.yaml_dynamic_settings.YAMLDynamicSettingsConfiguration

Dynamic YAML configuration reading (class-level state).

file_path = None class-attribute instance-attribute

Own file for the dynamic entries this package reads (str or os.PathLike).

When set, every dynamic collection without a binding of its own reads this file, apart from the file of the declared members; the document inside is shaped by the section parameters of the reading plugin, placed exactly as the writing side laid it out.

A module can declare the dynamic entries' own file for itself alone — at the module's top level, with for_this_module from the package root:

YAMLDynamicSettingsConfiguration.file_path = for_this_module(
    Path("dynamic-settings.yaml")
)

The declaration holds for the classes of the module it is written in and wins over this attribute; modules without one keep reading the attribute. When both are unset (the default), the dynamic entries are read from the class's source file — the link's own file=: the one-document default the writing side writes by default too.

Dynamic readers

The read-only dynamic plugins, which read a collection previously written by Advexp.Settings, are located in their plugin modules. They are described, together with the rest of the dynamic settings, on the Dynamic settings page.

advexp.configuration.plugins.json_dynamic_settings.JSONDynamicSettingsPlugin()

Plugin object for reading dynamic settings from the JSON file.

The namespaced pin names the backend. The collection reads the dynamic entries that a writing class of the settings distribution stored in the JSON document. The group key of the writer, declared next to the source binding of the class (json_source(dynamic_of=...)), names the group; the file levels and the inherited section parameters place it exactly as the writer laid it out. The pin is the own registry slot of the dynamic side: the binding ties it to the JSONSettingsSerializer of this package and never touches the registration of JSONSetting by the static source plugin.

advexp.configuration.plugins.toml_dynamic_settings.TOMLDynamicSettingsPlugin()

Plugin object for reading dynamic settings from the TOML file.

The namespaced pin names the backend. The collection reads the dynamic entries that a writing class of the settings distribution stored in the TOML document. The group key of the writer, declared next to the source binding of the class (toml_source(dynamic_of=...)), names the group; the file levels and the inherited section parameters place it exactly as the writer laid it out. The pin is the own registry slot of the dynamic side: the binding ties it to the TOMLSettingsSerializer of this package and never touches the registration of TOMLSetting by the static source plugin.

advexp.configuration.plugins.yaml_dynamic_settings.YAMLDynamicSettingsPlugin()

Plugin object for reading dynamic settings from the YAML file.

The namespaced pin names the backend. The collection reads the dynamic entries that a writing class of the settings distribution stored in the YAML document. The group key of the writer, declared next to the source binding of the class (yaml_source(dynamic_of=...)), names the group; the file levels and the inherited section parameters place it exactly as the writer laid it out. The pin is the own registry slot of the dynamic side: the binding ties it to the YAMLSettingsSerializer of this package and never touches the registration of YAMLSetting by the static source plugin.

Errors

advexp.configuration.AggregateLoadError(errors, fallbacks=(), misses=(), channel=None)

The error that a failed load raises.

The pass walks every member before aborting, so one run reports every failure at once. The error has three groups of entries:

  • errors holds the individual exceptions that abort the pass, in declaration order: a required member that got no usable value, or a cross-member violation that no reset to the declared defaults could settle.
  • fallbacks holds the errors of the optional members whose sources provided a value that the members could not take (an unconvertible or invalid value). These members stand at their defaults.
  • misses holds the optional members whose sources provided no value at all. These members also stand at their defaults.

These are the structural marks that tell the three groups apart. The message carries each entry as a line. It opens with the channel of the distribution that ran the pass (channel, the name under which its records are logged), spelled as <channel> error: the load failed for ..., so an abort that the application prints names its package even away from the log.

Every load abort is this class, however many members failed: the type of the exception never depends on the number of failures. A class in which all members are optional aborts only on a violation that the resets cannot settle. Otherwise, its load report is the tuple that the loading method returns, and the misses stay out of it (a miss is the normal way in which a default gets used). The misses are attached only to an abort, where the reader of the failure wants to see the whole class at once.

The class is a subclass of :class:~advexp.settings_base.member_load_error.MemberLoadError, so a family-wide except MemberLoadError (or except ValueError) handler keeps catching load aborts.

advexp.configuration.MemberLoadError(message, member_name=None)

A member could not get a valid value from its source.

This is the parent of the load-time reasons: the source provided no value, a value that cannot be converted, a constraint violation, or a failed source. It is also the parent of the aggregate abort of the load pass.

Where the reason is unknown, the error is raised directly. For example, reading a member that was never loaded serves its default, and a member that is declared :data:~advexp.settings_base.required.REQUIRED has none to serve.

The error names a problem, not a policy: the declaration of a member decides the channel. The load error of a required member is an entry of the load abort. The load error of an optional member is an entry of the load report that the loading method returns, and the member itself stands at its default. Subclassing :class:ValueError keeps a plain except ValueError working: the value errors of the family are all subclasses of ValueError.

member_name carries the member that the error is about, when the one who raises the error knows it. The required-read error of the member descriptor always knows it, so a handler can report the finding on that member without parsing the message. Errors that are raised without it leave it None.

advexp.configuration.MissingValueError(message, member_name=None)

The load error of a member for which the source provided no value.

For a required member, this error goes into the errors of the load abort. An optional member treats a miss as the normal way of serving its default. Its miss error is attached only to an abort, in the misses of the aggregate, and it stays out of the report of a successful run.

The error is an entry of the load abort (:class:~advexp.settings_base.aggregate_load_error.AggregateLoadError). The message names the member, its storage name, and the class, so the reader knows exactly what to provide. The storage name is the own spelling of the source when it offers one (for example, the name of an environment variable), and otherwise it is the storage name that the pattern built.

advexp.configuration.ConversionError(message, member_name=None)

The value of a member did not convert to the annotated type.

The source had a value for the member, but the value cannot take the annotated shape of the member. The error names the member, its setting, and the class, and it shows the value and the type to which the value would not convert (the value is masked when the member holds a secret). For a required member, it is an entry of the load abort (:class:~advexp.settings_base.aggregate_load_error.AggregateLoadError). For an optional member, it is an entry of the load report that the loading method returns, and the member stands at its default.

advexp.configuration.ConstraintViolationError(message, member_name=None)

The value of a member violates the constraints that are declared on it.

The value was converted, but the own declared bounds of the member, or a rule across members, reject it. The error names the member, its setting, and the class, and it lists every violated constraint. For a required member, it is an entry of the load abort (:class:~advexp.settings_base.aggregate_load_error.AggregateLoadError). For an optional member, it is an entry of the load report that the loading method returns, and the member stands at its default.

advexp.configuration.SourceLoadError(message, member_name=None)

The source of a member raised an error while loading its value.

The message names the member, its setting, and the class, and it carries the own error of the source (which also stays chained as the cause).

The handling depends on the kind of error and on the member:

  • A source that reports its problems through the user-facing channel (a corrupt or an unreadable file) is handled in the same way on a required member, with its exception carried in the message. On an optional member, it keeps propagating as its own exception, because a served default must not hide it.
  • A plain error of the source (whatever the backend raised without wrapping it) is an entry of the load report that the loading method returns for an optional member, next to the log record that the pass keeps.

advexp.configuration.PluginNotEnabledError

A setting attribute has no registered serializer (plugin not imported).

Sentinels

advexp.configuration.REQUIRED = _Required() module-attribute

The default that makes a member required: its value must come from a source.

Declared as the default (default=REQUIRED). Every boundary that would serve the default — a load that found nothing usable in the source, a read of a never-loaded member — raises :class:~advexp.settings_base.member_load_error.MemberLoadError instead: the load-time reasons are entries of the load pass's aggregate abort. The family-wide spelling of an optional member is :data:~advexp.settings_base.no_default.NO_DEFAULT: None stands in, and its load problems land in the report the loading method returns.

Secret wrappers

advexp.configuration.SecretStr(value)

A string whose repr and str return the mask, not the value.

This is the annotated type of a member that carries a secret. Loading wraps the string of the source into it. A casual print, a log record, or an error message therefore shows '**********'. The only way to get the value is :meth:get_secret_value, which is an explicit step that is easy to grep for.

The constraint checks look at the inner string, while their messages keep the mask. The storage form is the plain string: the save path unwraps the value before the serializers see it. The masking is carried by the wrapper: a plain string that is assigned to the member keeps the semantics of a plain string.

get_secret_value()

Return the wrapped value. This call is the explicit step to take, and it is easy to grep for.

advexp.configuration.SecretBytes(value)

Bytes whose repr and str return the mask, not the value.

This is the annotated type of a member that carries a secret. Loading wraps the payload of the source into it (a string is encoded as UTF-8, as the bytes conversion does). A casual print, a log record, or an error message therefore shows '**********'. The only way to get the value is :meth:get_secret_value, which is an explicit step that is easy to grep for.

The constraint checks look at the inner payload, while their messages keep the mask. The storage form is the plain bytes: the save path unwraps the value before the serializers see it. The masking is carried by the wrapper: plain bytes that are assigned to the member keep the semantics of plain bytes.

get_secret_value()

Return the wrapped value. This call is the explicit step to take, and it is easy to grep for.