loom.core.config¶
Configuration loading utilities for Loom applications.
Provides an omegaConf-backed loader that supports multiple YAML files and typed section extraction. The framework imposes no shape on the config object — the user owns the structure.
Example:
from loom.core.config import load_config, section
cfg = load_config("config/base.yaml", "config/local.yaml")
db = section(cfg, "database", DatabaseConfig)
- class loom.core.config.ConfigBinding(target, config_path='', overrides=<factory>)[source]¶
Bases:
LoomFrozenStructDeferred binding between a class and a config section.
The binding is a declaration only. It does not read files, access global config state, or instantiate
target. Compiler/bootstrap code resolves it later using an explicit runtime config object.- Parameters:
target (type[object]) – Runtime behaviour class to instantiate later.
config_path (str) – Dot-separated config path. Empty means no YAML section is required and only
overridesare applied.overrides (dict[str, object]) – Explicit keyword overrides. These win over YAML values when the binding is resolved.
- class loom.core.config.ConfigContext(config, *, binder=None)[source]¶
Bases:
objectRuntime config accessor backed by a resolved DictConfig.
Combines typed section extraction with constructor injection so that runner and bootstrap code has a single, consistent entry-point for reading config — no scattered
section()calls or bespoke binding loops across layers.Prefer the factory methods for construction:
from_yaml()— load from one or more YAML files.from_dict()— build from a plain Python mapping (tests, code-defined config).Direct
ConfigContext(DictConfig)— when a DictConfig is already available.
- Parameters:
config (DictConfig) – Resolved OmegaConf DictConfig.
binder (StructBinder | None) – Optional
StructBinder. Defaults to a non-strict binder. PassStructBinder(strict=True)to disallow implicit coercion.
Example:
ctx = ConfigContext.from_yaml("config.yaml") db = ctx.section("database", DatabaseConfig) step = ctx.bind(ReadOrdersStep, path="pipeline.steps.orders") dep = ctx.resolve(MyDep.from_config("services.dep", label="prod"))
- classmethod from_yaml(*paths, resolvers=(), keyed=(), binder=None)[source]¶
Build a
ConfigContextfrom one or more YAML files.Files are merged left-to-right; later files override earlier ones. Accepts local paths and cloud URIs (
s3://,gs://, …).- Parameters:
*paths (str) – One or more local paths or cloud URIs.
resolvers (Sequence[ConfigResolver]) – Optional custom resolvers for
${name:key}placeholders.keyed (Sequence[str]) – Dotted paths of the collections merged by key within an
includescomposition.binder (StructBinder | None) – Optional
StructBinderoverride.
- Returns:
A ready-to-use
ConfigContext.- Raises:
ConfigError – If any file cannot be read or parsed, or a keyed collection holds a duplicate key or mixes list and mapping forms.
- Return type:
- classmethod from_dict(raw, *, binder=None)[source]¶
Build a
ConfigContextfrom a plain Python mapping.Primarily used in tests and inline config construction. The mapping must resolve to a top-level dictionary (not a list or scalar).
- Parameters:
raw (Mapping[str, Any]) – Plain Python mapping of config keys to values.
binder (StructBinder | None) – Optional
StructBinderoverride.
- Returns:
A ready-to-use
ConfigContext.- Raises:
TypeError – If raw does not produce a DictConfig.
- Return type:
- section_or_default(key, target, default)[source]¶
Extract key as target when present, otherwise return default.
- section(key, target)[source]¶
Extract and validate a typed config section by dot-path.
- Parameters:
- Returns:
Validated instance of target.
- Raises:
ConfigError – If the key is absent or the section fails validation.
- Return type:
T
- bind(target, *, path='', **overrides)[source]¶
Instantiate target from a config section merged with overrides.
overrides take precedence over YAML values. Omit path to rely entirely on overrides.
- Parameters:
- Returns:
Fully constructed instance of target.
- Raises:
ConfigError – If a required field is missing or type conversion fails.
- Return type:
T
- resolve(binding)[source]¶
Materialize a
ConfigBindinginto a live object.Delegates to
bind()using the binding’s declared path and overrides. Compilers and bootstrap code that operate on pre-declared bindings use this method rather than callingbind()with unpacked fields.- Parameters:
binding (ConfigBinding) – Deferred binding declaration (from
from_config()orconfigure()).- Returns:
Instantiated
binding.target.- Raises:
ConfigError – If resolution or type conversion fails.
- Return type:
- exception loom.core.config.ConfigError[source]¶
Bases:
ExceptionRaised when configuration loading or validation fails.
- Parameters:
message – Human-readable description of the failure.
Example:
raise ConfigError("Missing required field: db_url")
- class loom.core.config.ConfigKey(value)[source]¶
Bases:
StrEnumTop-level YAML section keys consumed by
section().Using
StrEnumkeeps references typo-proof while remaining compatible with any API that expects plainstrsection identifiers.Example:
cfg = section(raw, ConfigKey.CELERY, CeleryConfig)
- class loom.core.config.ConfigResolver(*args, **kwargs)[source]¶
Bases:
ProtocolProtocol for pluggable config value resolvers.
Implementors provide a name (used as the OmegaConf placeholder prefix) and a resolve callable that fetches the actual value at parse time.
See
SsmResolverfor the bundled AWS SSM implementation. Custom resolvers only need to satisfy this two-member protocol:class VaultResolver: @property def name(self) -> str: return "vault" def resolve(self, key: str) -> object: return vault_client.read_secret(key) cfg = load_config("config/prod.yaml", resolvers=[VaultResolver()])
- property name: str¶
OmegaConf resolver prefix.
Used as the placeholder prefix in YAML:
${<name>:key}. Must be unique across all registered resolvers.- Returns:
Resolver name string (e.g.
"ssm","keyvault").
- class loom.core.config.Configurable[source]¶
Bases:
objectMixin for classes that can be declared from config paths.
The mixin returns
ConfigBindingdeclarations. It does not impose a constructor shape and does not resolve configuration itself.- classmethod from_config(config_path, **overrides)[source]¶
Declare this class as configured from a YAML section.
- Parameters:
- Returns:
Deferred config binding for this class.
- Raises:
ValueError – If
config_pathis empty or blank.- Return type:
- class loom.core.config.OtelConfig(service_name='loom', tracer_name='loom', tracer_version='', protocol='http/protobuf', endpoint='', insecure=False, headers=<factory>, resource_attributes=<factory>, span_attributes=<factory>, exporter_kwargs=<factory>, span_processor_kwargs=<factory>, adopt_host_id_generator=False, sampler='parentbased_always_on', sampler_ratio=1.0, max_span_links=128)[source]¶
Bases:
LoomFrozenStructOpenTelemetry SDK/exporter configuration.
- Parameters:
service_name (str) – Resource attribute
service.name.tracer_name (str) – Tracer instrumentation name.
tracer_version (str) – Optional tracer instrumentation version.
protocol (str) – OTLP protocol (
http/protobuforgrpc).endpoint (str) – OTLP endpoint URI. When empty, uses global OTel runtime defaults.
insecure (bool) – Exporter transport mode when supported by protocol/exporter.
headers (dict[str, str]) – Exporter request headers (vendor auth/tags).
resource_attributes (dict[str, str]) – Additional OTel resource attributes.
span_attributes (dict[str, str]) – Static span attributes added to all spans emitted by this observer.
exporter_kwargs (dict[str, Any]) – Extra keyword args passed through to OTLP exporter.
span_processor_kwargs (dict[str, Any]) – Extra keyword args passed through to BatchSpanProcessor.
adopt_host_id_generator (bool) – Installs Loom’s trace-id generator on the already-installed
TracerProviderso streaming messages get end-to-end traces. Host spans are unaffected — the generator delegates to the default random generator whenever no Loom trace id is active. Only meaningful whenendpointis empty; Loom’s own provider always gets the generator.sampler (str) – Sampler name, one of
SAMPLERS. Applied to Loom’s own provider only: with an emptyendpointthe host sampler decides.sampler_ratio (float) – Sampled fraction for the ratio-based samplers. Because the OTEL trace id is the message trace id, the decision is stable from ingestion to death and across services, so a sampled message yields a complete trace and an unsampled one yields none.
max_span_links (int) – Upper bound on the links a batch span carries. When the bound is hit the span is marked with
loom.links_truncated.
- validate()[source]¶
Validate the transport protocol, the sampler, and the link bound.
- Raises:
ValueError – If protocol is unsupported.
ValueError – If the sampler name is unknown, the sampled ratio is outside
[0.0, 1.0], or the link bound is not positive.
- Return type:
None
- class loom.core.config.SecretsManagerResolver(region=None)[source]¶
Bases:
objectResolves AWS Secrets Manager paths for use with
load_config().Fetches secret values from AWS Secrets Manager. The boto3 client is created lazily on first use and reused across calls.
Env-var tokens in the form
%VAR_NAME%(uppercase letters, digits, and underscores only) are expanded fromos.environbefore the request is made.Keys are trusted deployment configuration: they come from the deployment’s own config files, never from request input. The resolver logs and reports the key as written (with its
%VAR%tokens), never the expanded path. Two channels remain for a caller that lets untrusted input reach a resolver key: a missing variable raisesConfigError("Env var 'X' not found ..."), a deterministic existence oracle for any attacker-chosen variable name, and the chained cause (__cause__) of a fetch error may carry the expanded path echoed by the AWS client. The client is also called with the expanded path, so botocore’s DEBUG request logging reproduces it — keep botocore at INFO or above in production.- Parameters:
region (str | None) – AWS region name. Passed directly to
boto3.client. Defaults toNone, which lets boto3 use its own resolution chain (env vars, instance metadata, etc.).
Example:
resolver = SecretsManagerResolver("eu-west-1") value = resolver.resolve("/myapp/%ENV%/db_password")
- resolve(key)[source]¶
Resolve an AWS Secrets Manager path to its stored value.
Splits the dot-notation tail off key as written, expands
%VAR_NAME%tokens in the remaining base path from the environment, then fetches the secret from AWS Secrets Manager.- Parameters:
key (str) – Secret path, optionally containing
%VAR_NAME%placeholders that are replaced with environment variable values. Supports dot-notation for JSON key navigation:/path/secret.keyfetches/path/secretand returnssecret["key"]. Dots are read from the key as written, so an expanded value containing a dot is part of the path and never a navigation separator.- Returns:
Resolved value. A plain string for secrets without dot-notation; a structured value (string, int, dict, etc.) when dot-notation navigates into a JSON secret.
- Raises:
ConfigError – When key is empty, an env-var placeholder is missing, boto3 is not installed, the secret is binary, or the API call fails.
- Return type:
- class loom.core.config.SsmResolver(region=None, *, with_decryption=True)[source]¶
Bases:
objectResolves SSM Parameter Store paths for use with
load_config().Fetches parameter values from AWS Systems Manager Parameter Store. The boto3 client is created lazily on first use and reused across calls.
Env-var tokens in the form
%VAR_NAME%(uppercase letters, digits, and underscores only) are expanded fromos.environbefore the SSM request is made.Keys are trusted deployment configuration: they come from the deployment’s own config files, never from request input. The resolver logs and reports the key as written (with its
%VAR%tokens), never the expanded path. Two channels remain for a caller that lets untrusted input reach a resolver key: a missing variable raisesConfigError("Env var 'X' not found ..."), a deterministic existence oracle for any attacker-chosen variable name, and the chained cause (__cause__) of a fetch error may carry the expanded path echoed by the AWS client. The client is also called with the expanded path, so botocore’s DEBUG request logging reproduces it — keep botocore at INFO or above in production.- Parameters:
Example:
resolver = SsmResolver("eu-west-1") value = resolver.resolve("/myapp/%ENV%/db_password")
- property name: str¶
Resolver name used as the OmegaConf placeholder prefix.
- Returns:
The string
"ssm".
- resolve(key)[source]¶
Resolve an SSM parameter path to its stored value.
Splits the dot-notation tail off key as written, expands
%VAR_NAME%tokens in the remaining base path from the environment, then fetches the parameter from AWS SSM Parameter Store.- Parameters:
key (str) – SSM parameter path, optionally containing
%VAR_NAME%placeholders that are replaced with environment variable values. Supports dot-notation for JSON key navigation:/path/param.keyfetches/path/paramand returnsparam["key"]. Dots are read from the key as written, so an expanded value containing a dot is part of the path and never a navigation separator.- Returns:
Resolved value. A plain string for parameters without dot-notation; a structured value (string, int, dict, etc.) when dot-notation navigates into a JSON parameter.
- Raises:
ConfigError – When key is empty, an env-var placeholder is missing, boto3 is not installed, or the SSM API call fails.
- Return type:
- class loom.core.config.StructBinder(strict=False)[source]¶
Bases:
objectStrategy: constructor injection from a resolved config mapping.
Converts every annotated
__init__parameter present in raw viamsgspec.convert. Supports primitives (int,str,bool),Literalconstraints, andLoomFrozenStructsubclasses uniformly. All reflection runs once perbindcall at compile time — never on the message-processing hot path.- Parameters:
strict (bool) – When
True,msgspec.convertuses strict mode (no implicit coercion from string to int, etc.).
Example:
binder = StructBinder() step = binder.bind( ReadOrdersStep, {"db": {"host": "localhost", "port": 5432}, "mode": "batch"}, )
- bind(target, raw)[source]¶
Instantiate target injecting and converting values from raw.
For each annotated
__init__parameter that appears in raw, the value is converted to the declared type viamsgspec.convert. Parameters absent from raw use their default; required parameters absent from raw raiseConfigError.- Parameters:
- Returns:
A fully constructed instance of target.
- Raises:
ConfigError – If a required parameter is absent from raw or a value fails type conversion.
- Return type:
T
- loom.core.config.default_resolvers()[source]¶
Return loom’s built-in resolvers:
secretsandssm.Both use the AWS SDK’s default region and credential chain and create their client lazily on the first resolution.
- Returns:
A
SecretsManagerResolverfollowed by aSsmResolver, both freshly built.- Return type:
tuple[ConfigResolver, …]
- loom.core.config.expand_config_glob(pattern)[source]¶
Expand a configuration file pattern on the local or a cloud filesystem.
- Parameters:
pattern (str) – Resolved local path or cloud URI, optionally containing glob characters (
*,?,[).- Returns:
[pattern]for a plain entry; for a glob, the matching regular.yaml/.ymlfiles sorted lexicographically by resolved path.- Raises:
ConfigError – When a glob matches no configuration file or the cloud backend fails to list it.
- Return type:
- loom.core.config.load_config(*config_files, resolvers=(), keyed=())[source]¶
Load and merge one or more YAML config files into a DictConfig.
Accepts local filesystem paths and cloud storage URIs (
s3://,gs://,abfss://,r2://…). Cloud files are fetched viafsspecat call time.Files are merged left-to-right: values in later files override those in earlier ones.
${oc.env:VAR}interpolations are resolved by OmegaConf. Customresolversare registered before parsing so their${name:key}placeholders resolve during the same pass.Any file may declare a top-level
includeslist to pull in additional files before its own values. Entries are relative to the declaring file, may live on any scheme, and may be globs. Circular includes and entries matching no file raiseConfigError.keyednames collections whose mapping form merges by key within anincludescomposition: a key declared by two files of the same composition raisesConfigError, as does mixing a list form with a mapping form. Explicitconfig_fileslayers override keys left-to-right; only the form mismatch is reported across them.The framework does not impose any shape on the resulting config — the user owns the structure entirely. Use
section()to extract typed sub-objects where desired.- Parameters:
*config_files (str) – One or more local paths or cloud URIs.
resolvers (Sequence[ConfigResolver]) – Optional sequence of
ConfigResolverinstances. Each resolver registers a${name:key}placeholder resolved at parse time (e.g. from AWS SSM or Azure Key Vault).keyed (Sequence[str]) – Dotted paths (e.g.
"storage.tables") of the collections merged by key.
- Returns:
Merged
omegaconf.DictConfigwith interpolation support.- Raises:
ConfigError – If no files are provided, a file is not found, parsing fails, an include matches no file, a circular include is detected, a keyed collection holds a duplicate key or mixes list and mapping forms, omegaconf is not installed, or a cloud URI fetch fails.
- Return type:
DictConfig
Example — single local file with inline includes:
cfg = load_config("config.yaml")
Example — explicit multi-file composition:
cfg = load_config("config/base.yaml", "config/production.yaml") db_url = cfg.database.url
Example — cloud URI:
cfg = load_config("s3://my-bucket/config/prod.yaml")
Example — with custom resolver:
cfg = load_config("config/prod.yaml", resolvers=[SsmResolver("eu-west-1")])
Example — keyed collections:
cfg = load_config("config/app.yaml", keyed=("storage.tables", "storage.files"))
- loom.core.config.merge_resolvers(explicit, defaults)[source]¶
Return explicit followed by the defaults whose names are still free.
A default is dropped when an explicit resolver takes its name or when a resolver with that name is already registered in OmegaConf; the latter is logged at DEBUG level.
- Parameters:
explicit (Sequence[ConfigResolver]) – Resolvers that keep their position and always win their name.
defaults (Sequence[ConfigResolver]) – Candidate resolvers appended after
explicitwhen their name is not taken.
- Returns:
explicitin order, followed by the keptdefaultsin order.- Return type:
tuple[ConfigResolver, …]
- loom.core.config.section(cfg, key, target_type)[source]¶
Extract and validate a config section as a typed object.
Navigates
cfgbykey(dot-notation supported, e.g."database.primary"), resolves omegaConf interpolations, and converts the result totarget_typeviamsgspec.convert.Works with any type supported by
msgspec.convert:msgspec.Structsubclasses,dataclasses,TypedDict, plain dicts, etc.- Parameters:
cfg (DictConfig) – Root
omegaconf.DictConfigreturned byload_config().key (str) – Dot-separated path to the desired section (e.g.
"database"or"services.cache").target_type (type[T]) – Type to convert the section into.
- Returns:
Validated instance of
target_type.- Raises:
ConfigError – If the key is absent, the section cannot be resolved, or
msgspecvalidation fails.- Return type:
T
Example:
class DatabaseConfig(msgspec.Struct, kw_only=True): url: str pool_size: int = 5 db = section(cfg, "database", DatabaseConfig)
- loom.core.config.with_default_resolvers(explicit=())[source]¶
Return explicit followed by loom’s built-in resolvers whose names are free.
Equivalent to
merge_resolvers(explicit, default_resolvers()); the factories use it to registersecretsandssmbehind user resolvers.- Parameters:
explicit (Sequence[ConfigResolver]) – User resolvers that keep their position and always win their name.
- Returns:
explicitin order, followed by the built-in resolvers whose name is neither taken byexplicitnor already registered.- Return type:
tuple[ConfigResolver, …]