tlcconfig.store

Per-source ledger of raw configuration values.

The store records each option’s value alongside the source that wrote it (file / environment / CLI / programmatic API), so callers can answer “which tier won?”. It performs no validation, transformation, or business logic — those live one layer up.

.. rubric:: Example

from tlcconfig.store import ConfigStore

store = ConfigStore()
port = store.get("service.port")

raw = store.get_raw("service.port")
print(f"{raw.value} from {raw.source.name}")

Module Contents

Classes

Class

Description

ConfigStore

Lightweight configuration store.

ProvenancedItem

A single sub-element of a compound option, with its own provenance.

RawValue

A raw loaded value with source tracking.

Data

Data

Description

logger

API

class ConfigStore(
config_file: str | None = None,
load_defaults: bool = True,
load_env: bool = True,
)

Lightweight configuration store.

This store is intentionally “dumb” — it reads values and tracks sources, but performs no validation or transformation. This keeps it fast to import and free of side effects.

The store follows a precedence order (highest to lowest):

  1. API (programmatic setting via set())

  2. Command line arguments

  3. Environment variables

  4. Config file

  5. Secondary config files (project-level configs)

  6. Defaults (from registered options)

Variables:

config_files – List of config files that were loaded.

Initialize the configuration store.

Parameters:
  • config_file – Path to config file. If None, auto-discovers. If empty string “”, skips config file loading.

  • load_defaults – Whether to load default values from registered options.

  • load_env – Whether to load values from environment variables.

add_item(
key: str,
sub_key: Any,
value: Any,
source: ConfigSource,
source_info: str | None = None,
) None

Add a single sub-element to a compound option without replacing the rest of the slice.

Unlike :meth:set_value (which replaces the entire (source, source_info) slice with the new value), add_item appends one ProvenancedItem leaving any other items already present at the same source intact. Used by the facade verbs append / update and by callers that want surgical, single-element writes.

Parameters:
  • key – Option key.

  • sub_key – Dict sub-key to set, or None for list-compound append.

  • value – Value to set / append.

  • source – ConfigSource for this write.

  • source_info – Additional source context.

Raises:
  • ValueError – If the option is not registered or is not a compound.

  • ValueError – For dict-compound options when sub_key is None.

bootstrap_from_root_config() None

Load <ROOT_URL>/config.3lc.yaml if it exists.

Resolves ROOT_URL from this store, joins it with the default config-file name, and loads the file at PROJECT_ROOT_CONFIG_FILE precedence if present. No-op when ROOT_URL is unset or no file exists at the resolved path.

Lives on the store (not on Configuration) so callers that only need tlcconfig — notably tlccli.main’s root callback — can invoke it without triggering import tlc / init_global_objects.

claim_pending(
key: str,
) None

Move any pending entries for key into the live store.

Idempotent. Called by OptionRegistry.register when an option is registered after a config source has already been loaded. Routes each pending entry through _set_raw so compound options get their per-item splitting applied retroactively (the option wasn’t registered when the value was first stashed, so the splitter couldn’t see it then — now it can).

claim_pending_env(
option: Option[Any],
) None

Drain stashed TLC_* env vars matching option’s envvar bindings.

Mirrors the eager-pass logic in _load_environment: canonical wins over deprecated aliases, regex envvars match every stashed name once. Deprecation warnings fire here (claim time) rather than at load time because the option wasn’t registered when the env was scanned.

Idempotent. Called by OptionRegistry.register.

property config_files: Mapping[str, ConfigSource]

Read-only mapping of loaded config file paths to the tier they were loaded at, in load order.

in membership, iteration (yields paths), and .get(path) all work as expected. For the first user-tier file (the writable target for 3lc config project-root) use :attr:user_config_path. For tier-filtered lookups use :meth:files_at.

files_at(
source: ConfigSource,
) list[str]

Return the loaded config files at the given tier, in load order.

get(
key: str,
) Any | None

Get the highest-precedence raw value for a key.

Parameters:

key – Option key (e.g., “service.port”).

Returns:

The raw value, or None if not set.

get_all_raw(
key: str,
) list[RawValue]

Get all raw values for a key (useful for compound types).

For non-compound keys: returns the list of raw entries verbatim. For compound keys: synthesizes one RawValue per (source, source_info) group by reassembling the per-item ProvenancedItem entries — back-compat shim for callers that used to receive whole-tier writes here. Use

Meth:

iter_provenanced if you actually want per-item provenance.

Parameters:

key – Option key.

Returns:

List of all RawValues for the key.

get_merged(
key: str,
) RawValue | None

Get the merged value for compound types, or highest-precedence for simple types.

For compound types (dict/list), merges all per-item entries in precedence order (lowest first so higher precedence overwrites on dict-key collisions; latest-wins on list-item structural equality). The wrapper’s source is set to

Class:

ConfigSource.MIXED when items span tiers, otherwise the single tier shared by all items. For simple types, returns the highest-precedence value (same as get_raw).

Parameters:

key – Option key.

Returns:

RawValue with merged value and provenance summary, or None if not set.

get_provenance(
key: str,
) ProvenancedItem | list[ProvenancedItem] | dict[Any, ProvenancedItem] | None

Return per-element provenance, polymorphic by the option’s compound type.

  • Scalar option (or unregistered key): single

    class:

    ProvenancedItem with the highest-precedence value, or None if unset.

  • List-compound option: list[ProvenancedItem] in merged order, deduped by structural identity. The first tier (lowest precedence) that contributed each item is the surviving provenance — matches the get_merged dedup rule. Empty list if unset.

  • Dict-compound option: dict[K, ProvenancedItem] keyed by sub-key. Per-key, the highest-tier writer wins. Empty dict if unset.

Pure query — does not mutate the store, never warns. Use the returned container with native Python idioms; there are no index= or sub_key= kwargs (you index/iterate the container yourself).

Parameters:

key – Option key.

Returns:

Polymorphic provenance view; see above.

get_raw(
key: str,
) RawValue | None

Get raw value with source info.

For compound options, delegates to :meth:get_merged so callers that haven’t been ported to per-item provenance still see a single RawValue wrapper. The wrapper’s source will be

Class:

ConfigSource.MIXED if items span tiers.

Parameters:

key – Option key.

Returns:

RawValue with value and source info, or None if not set.

get_source(
key: str,
) ConfigSource | None

Get the source of a value.

Parameters:

key – Option key.

Returns:

The ConfigSource, or None if not set.

classmethod instance() ConfigStore

Get the global ConfigStore instance.

Creates a new instance if none exists.

Returns:

The global ConfigStore instance.

load_cli(
args: dict[str, Any],
) None

Load values from parsed CLI arguments.

Parameters:

args – Dictionary of CLI argument values (from argparse or click).

load_config(
path: str,
source: ConfigSource = ConfigSource.USER_CONFIG_FILE,
) None

Load a YAML config file at the given source tier.

Source-explicit public loader. Reloading the same (path, source) replaces any previously loaded entries from that path at that tier so callers can re-invoke this method when the file changes.

Parameters:
  • path – Path to the YAML file.

  • source – The :class:ConfigSource tier to load at. Defaults to USER_CONFIG_FILE.

load_data_config(
path: str,
) None

Load a data-bundled (secondary) config file. Wrapper for

Meth:

load_config at DATA_CONFIG_FILE tier.

Lower precedence than project-root, user-global, environment, and command line. Used for config files discovered alongside project data — e.g. shipped on a read-only mount or scanned via the indexing table.

load_project_root_config(
path: str,
) None

Load a project-root config.3lc.yaml. Wrapper for

Meth:

load_config at PROJECT_ROOT_CONFIG_FILE tier.

Beats data-bundled, loses to user-global so the user’s writable global config still wins.

pending_count() int

Return the number of unclaimed pending entries.

Sum of YAML keys parked in _pending_unknown and TLC_* env vars parked in _pending_env — i.e. values the user supplied that no Option has claimed yet. Pure query; does not warn. Callers can build optional strict modes, status panes, or post-bootstrap diagnostics on top of this.

pending_env_vars() list[str]

Return the list of TLC_* env vars stashed for an unregistered option.

pending_unknown_keys() list[str]

Return the list of keys still waiting for an Option to claim them.

remove_item(
key: str,
sub_key_or_value: Any,
source: ConfigSource,
source_info: str | None = None,
) bool

Remove a single sub-element from a compound option at the given tier.

For dict-compound options, sub_key_or_value is the sub-key to remove. For list-compound options, it’s the value to match by structural identity (the same json.dumps(sort_keys=True) rule get_merged uses for dedup). Only items at the specified (source, source_info) tier are affected — lower-tier items resurface in the merged view.

Parameters:
  • key – Option key.

  • sub_key_or_value – For dict, the sub-key to remove; for list, the value to match.

  • source – ConfigSource of the entry to remove.

  • source_info – Source-info of the entry to remove.

Returns:

True if at least one item was removed, False otherwise.

Raises:

ValueError – If the option is not registered or is not a compound.

classmethod reset_instance() None

Reset the global instance. Primarily for testing.

set(
key: str,
value: Any,
) None

Programmatically set a value (highest precedence).

Parameters:
  • key – Option key.

  • value – Value to set.

classmethod set_instance(
store: ConfigStore,
) None

Set the global ConfigStore instance.

Used by CLI to set up the store before subcommands run.

Parameters:

store – The store instance to use globally.

set_value(
key: str,
value: Any,
source: ConfigSource,
source_info: str | None = None,
) None

Set a value with an explicit source.

Any existing raw entry with the same (source, source_info) tuple is replaced so repeated calls stay idempotent and the internal raw value list does not grow unboundedly. For compound options the sweep operates on the per-item store (_compound).

Parameters:
  • key – Option key.

  • value – Value to set.

  • source – The ConfigSource indicating where this value came from.

  • source_info – Additional context about the source (e.g., file path).

stash_unknown(
key: str,
value: Any,
source: ConfigSource,
source_info: str | None = None,
) None

Record a value for a key that no Option has claimed yet.

Used by loaders (and by callers contributing unknown values from higher-precedence tiers) so that a later OptionRegistry.register can replay the value at its original source. See claim_pending.

to_default_yaml(
include_docs: bool = True,
) str

Generate YAML with only default values.

Parameters:

include_docs – Whether to include documentation comments.

Returns:

YAML-formatted configuration with defaults.

to_yaml(
include_docs: bool = True,
detail: bool = False,
) str

Serialize current configuration to YAML format.

Parameters:
  • include_docs – Whether to include documentation comments.

  • detail – Whether to include source info for non-default values.

Returns:

YAML-formatted configuration string.

property user_config_path: str | None

First file loaded at USER_CONFIG_FILE tier, or None.

Authoritative answer to “where do I write user-level config?” — used by tlccli config project-root. Returning the first file in

Attr:

config_files is wrong: it can be a DATA-tier file the indexer picked up before the user-tier load.

validate() None

Validate that required options are set.

Raises:

ValueError – If any required options are missing or empty.

warn_unclaimed_pending() None

Emit a WARNING for each newly-unclaimed pending key/env var.

Intended to be called at the end of bootstrap (e.g. in init_global_objects) so plugin keys that never got claimed — typos, removed-but-still-configured options, plugins not yet imported — surface to the user instead of silently rotting.

The pending stash is not cleared: a plugin imported later in the same process can still claim its values via the late-bind path. Already-warned entries are tracked so repeated checkpoint calls (e.g. periodic re-init in long-running services) stay quiet unless new unclaimed entries appear. Use pending_count to observe the queue size programmatically.

write_to_yaml_file(
path: str,
force: bool = False,
include_docs: bool = True,
detail: bool = False,
) bool

Write configuration to a YAML file.

Parameters:
  • path – Path to write to.

  • force – Whether to overwrite existing file.

  • include_docs – Whether to include documentation comments.

  • detail – Whether to include source info.

Returns:

True if file was written, False if file exists and force=False.

class ProvenancedItem

A single sub-element of a compound option, with its own provenance.

Compound options (lists, dicts) can mix items from different sources — e.g. some scan-urls from YAML, others from env vars. Each item is stored independently so callers can ask “where did this URL come from?” via :meth:ConfigStore.get_provenance (which returns list[ProvenancedItem] for list-compounds, dict[K, ProvenancedItem] for dict-compounds, or a single instance for scalars). Frozen so instances are hashable and safe to dedupe.

Variables:
  • value – The single sub-element (a list element, or a one-key dict slice {k: v} for dict-compounds).

  • source – Where this specific element came from.

  • source_info – Additional context (filename, env var name).

source: ConfigSource = None
source_info: str | None = None
value: Any = None
class RawValue

A raw loaded value with source tracking.

Variables:
  • value – The raw value as loaded (no transformation applied).

  • source – Where the value came from (DEFAULT, USER_CONFIG_FILE, ENVIRONMENT, etc.).

  • source_info – Additional context (filename, env var name, CLI flag).

source: ConfigSource = None
source_info: str | None = None
value: Any = None
logger = getLogger(...)