utils.cache package¶
Subpackages¶
Submodules¶
utils.cache.applications module¶
Application-domain cache: variants, defaults, merged lookup.
Owns _APPLICATIONS_DEFAULTS_CACHE, _VARIANTS_CACHE, and _MERGED_APPLICATIONS_CACHE. Public API: get_application_defaults, get_variants, get_merged_applications. Strictly ansible-free at import time so the GitHub Actions runner-host CLI path (cli.administration.deploy.development.init -> plan_dev_inventory_matrix -> get_variants) keeps working without ansible installed.
- utils.cache.applications.get_application_defaults(*, roles_dir: str | os.PathLike[str] | None = None) dict[str, Any]¶
- utils.cache.applications.get_canonical_volumes(application_id: str) dict[str, Any]¶
Return the canonical dict-of-dicts meta/volumes.yml for application_id.
Lives outside the applications payload so its embedded Jinja source: strings stay raw — see the _CANONICAL_VOLUMES_BY_ROLE doc-comment.
An empty result is cached under its id too, or a role without the file would re-stat it on every call.
- utils.cache.applications.get_merged_applications(*, variables: dict[str, Any] | None = None, roles_dir: str | os.PathLike[str] | None = None, templar: Any = None) dict[str, Any]¶
- utils.cache.applications.get_variant_overrides_only(*, roles_dir: str | os.PathLike[str] | None = None) dict[str, list[dict[str, Any]]]¶
Return
{application_id: [override_0, ...]}, the raw meta/variants.yml entries per role WITHOUT deep-merging meta/services.yml.Used by matrix-deploy’s –vars bake so the host_vars payload stays sparse: only the variant-specific overrides land there, leaving meta/services.yml fields (notably image/version) blank in host_vars so apply_mirror_overrides can populate them from mirrors.yml.
- utils.cache.applications.get_variants(*, roles_dir: str | os.PathLike[str] | None = None) dict[str, list[Any]]¶
Return
{application_id: [variant_0, ...]}cached perroles_dir. Each variant is the role’s effective configuration after the corresponding meta/variants.yml override has been deep-merged on top of meta/services.yml.
utils.cache.base module¶
Cross-cutting helpers for the utils.cache.* package.
Holds primitives that the domain modules (users, applications, domains) all need but no domain owns: filesystem constants, YAML loaders, deep-merge, cache-key + content-fingerprint signatures, templar-render machinery, and the cross-domain re-entry guard.
Stays import-cheap so that utils.cache.applications (consumed on GitHub Actions runner hosts that ship without ansible — see CI runs 24934007615 / 24935979190) can pull this module without dragging ansible in. The single ansible-coupled symbol exposed from here is _render_with_templar, which lazy-imports utils.templating.ansible only when actually invoked.
utils.cache.carrier module¶
Play-scoped carrier of the rendered applications payload.
Ansible forks a fresh worker process per task, so the in-process _MERGED_APPLICATIONS_CACHE starts empty in every worker. The constructor stage renders once and parks the payload plus its cache key in the host fact named by APPLICATIONS_RENDERED_FACT; every later worker inherits that fact at fork time and get_merged_applications serves it instead of rendering. Ansible-free at import time like the rest of the package.
- utils.cache.carrier.merged_applications_cache_key(variables: Mapping[str, Any] | None, *, roles_dir: str | os.PathLike[str] | None = None) tuple[str, tuple]¶
Key under which one rendered applications payload stays valid.
- Parameters:
variables – The play’s variable scope. Only the subset that _stable_variables_signature reads, and the tokens file it resolves to, influence the key.
roles_dir – Roles tree the payload is built from; defaults to the repository’s roles/.
- Returns:
(resolved roles dir, stable variables signature extended by the tokens file's path and content digest).
utils.cache.domains module¶
Domain-name cache: canonical-domains map derived from merged apps.
Owns _MERGED_DOMAINS_CACHE. Public API: get_merged_domains. The domain map is intentionally derived from the applications view rather than living in a parallel top-level overrides path. Per-app domain declarations live in applications.<app>.domains and flow through the regular applications-merge pipeline.
- utils.cache.domains.get_merged_domains(*, variables: dict[str, Any] | None = None, roles_dir: str | os.PathLike[str] | None = None, templar: Any = None) dict[str, Any]¶
Build the canonical-domain map lazily from the merged applications view.
The result is canonical_domains_map(applications, DOMAIN_PRIMARY). Per-app domain declarations live in applications.<app>.domains (canonical/aliases) and flow through the regular applications-merge pipeline.
Cached keyed on (roles_dir, variables_signature, render_state). A read issued while the applications render is in progress sees the unrendered tree, whose service flags are still Jinja strings; its map is cached under its own key so the render’s own call sites reuse it without it ever being served as the finished map. The tor flags are rendered before they are read (
_resolved_flag), so that mid-render map carries the same onion domains the finished one does.
utils.cache.files module¶
Process-level cached filesystem helpers for project-tree scans.
Many integration/lint tests and CLI tools walk the whole repo and read every matching file. Doing that N times per process is wasteful — the tree doesn’t change during a run. These helpers walk the tree once per process and memoise both the path list and file contents, so every caller after the first one pays near-zero cost.
Usage:
from utils.cache.files import iter_project_files, read_text
- for path in iter_project_files(extensions=(“.yml”, “.yml.j2”), exclude_tests=True):
content = read_text(path) …
PROJECT_ROOT is the repo root derived from this file’s location.
Gitignore-filtered iteration¶
The matcher itself lives in utils.cache.gitignore (single SPOT for
.gitignore parsing and matching). iter_non_ignored_files()
here composes the cached project walk with that matcher and is the
recommended helper for lint tests that want the “files git would track”
view without subprocessing.
- utils.cache.files.iter_non_ignored_files(*, extensions: Iterable[str] | None = None, exclude_tests: bool = False, exclude_dirs: Iterable[str] | None = None, root: str | None = None) Iterator[str]¶
Yield project files filtered through
.gitignore.Combines
iter_project_files()withis_path_gitignored()so callers get the “files git would track” view without spawning a git subprocess.rootdefaults toPROJECT_ROOT.A path is yielded iff it survives both the in-process skip-dir pruning of
iter_project_files()and the.gitignorematch.
- utils.cache.files.iter_project_files(*, extensions: Iterable[str] | None = None, exclude_tests: bool = False, exclude_dirs: Iterable[str] | None = None) Iterator[str]¶
Yield absolute paths of project files matching the filter.
- Parameters:
extensions – Suffix strings (e.g.
(".yml", ".yaml.j2")). Matching is case-insensitive via plain string suffix.Noneyields all files.exclude_tests – Skip everything under
<root>/tests/.exclude_dirs – Additional top-level-or-nested directory names to skip. Matched by path-component containment, not by prefix. E.g.
exclude_dirs=("docs",)skips any file whose path contains adocssegment.
- utils.cache.files.iter_project_files_with_content(*, extensions: Iterable[str] | None = None, exclude_tests: bool = False, exclude_dirs: Iterable[str] | None = None) Iterator[tuple[str, str]]¶
Yield
(path, content)for matching files; content is cached.Files that fail to read (permission errors, non-UTF-8) are silently skipped — that matches the prior caller behaviour of wrapping
open().read()in a baretry/except Exception: continue.
- utils.cache.files.read_text(path: str) str¶
Return the UTF-8 text content of a file; cached process-wide.
Raises OSError / UnicodeDecodeError unchanged — callers that scan arbitrary files can wrap this in try/except if non-UTF-8 blobs may appear.
utils.cache.gitignore module¶
Gitignore-aware path matching for project-tree scans.
SPOT for the fnmatch-based .gitignore-style filter used by lint and
integration tests that need to walk the repository without spawning
git. Covers the shapes the repo’s .gitignore actually uses
(basename patterns, path-qualified patterns, trailing-slash-as-directory)
and intentionally omits negation (!) and ** globstar — adding
either would require a real pathspec implementation.
Public API:
load_gitignore_patterns()— read non-empty, non-comment, non-negated lines from<root>/.gitignore. Cached process-wide.is_path_gitignored()— pure matcher: does a repo-relative POSIX-style path match any of the supplied patterns?
The combined “yield project files filtered through .gitignore”
helper lives in utils.cache.files (iter_non_ignored_files)
because it composes the cached project walk with this matcher.
- utils.cache.gitignore.is_path_gitignored(rel_path: str, patterns: Iterable[str]) bool¶
Return True iff
rel_path(POSIX-style, repo-root-relative) is matched by any ofpatterns.Supports the shapes the repo’s
.gitignoreactually uses:Bare-basename patterns (no slash): match any path component via
fnmatch.Path-qualified patterns (with slash): match from the repo root via
fnmatch, plusprefix/*shorthand and bare-prefix directory matching.Trailing
/(directory-only): treated as the stripped pattern because we only test against file paths.
Behaviour intentionally diverges from real git
pathspec:*here matches across/(fnmatchsemantics). If you need full fidelity, use a dedicated pathspec library.
- utils.cache.gitignore.load_gitignore_patterns(root: str | None = None) tuple[str, ...]¶
Load non-comment, non-blank, non-negated patterns from
<root>/.gitignore.Returns a tuple (immutable, hashable for
lru_cache). Passrootas a string so the cache key is plainly hashable; defaults toPROJECT_ROOTwhen omitted.Negation (
!) and**globstar are intentionally not supported — the repo’s.gitignoredoes not use them, and supporting either would require a real pathspec implementation. If a future.gitignoreadds them, extend this helper rather than duplicating the logic in callers.
utils.cache.schema_credentials module¶
Extract literal default: credential values from a meta/secrets.yml
credentials: tree.
Kept in its own module so utils.cache.applications stays under the
repo’s per-file line cap. Strictly ansible-free (stdlib only) so the
runner-host CLI path keeps importing it without ansible installed.
- utils.cache.schema_credentials.extract_default_credentials(creds_node: Any) dict[str, Any]¶
Return the subset of a
credentials:tree’s leaves that carry a literaldefault:Jinja string.The shape mirrors the schema tree: nested keys stay nested. The literal string is preserved verbatim, with no rendering and no validation. Leaves WITHOUT
default:are intentionally absent so the inventory’s apply_schema-generated values win the merge.
utils.cache.yaml module¶
In-process YAML loader with per-path memoisation.
Many CLI tools walk the same roles/<role>/<config|vars|meta>/…yml files multiple times during a single invocation (dependency resolution, service resolution, role introspection, …). Each direct yaml.safe_load(path.read_text()) re-parses YAML and re-touches disk.
This module exposes a single cached load_yaml(path) so each absolute path is parsed at most once per Python process. dump_yaml writes through and evicts the cached entry. invalidate(path) is the explicit escape hatch when a caller knows a file was changed externally.
CACHE SEMANTICS - Cache key: (str(Path(path).resolve()), st_mtime_ns, st_size).
Symlinks resolve once at the cache boundary so the same target shares an entry regardless of how callers spell the path. The mtime+size signature invalidates entries whenever the file is rewritten by an external process (e.g. Ansible’s copy: module persisting tokens while the same Python process keeps running across many calls).
Cached value: the list of documents
yaml.safe_load_allreturns for that file. Single-document YAML files cache as a 1-element list, so a callsite that asks for the single doc (load_yaml/load_yaml_any) and a callsite that asks for the multi-doc list (load_yaml_all) share the same parse. The same list / dict instances are returned to every caller, so callers that mutate the result MUST copy.deepcopy() it first. Treat the value as read-only.Lifetime: process-wide. CLI tools that never rewrite their inputs see the same hit rate as before; long-lived Ansible processes pick up out-of-band writes on the very next read.
- utils.cache.yaml.dump_yaml(path, data: Any) None¶
Write data to path as YAML and evict the cached entry.
Accepts any safe-dumpable Python structure (dict, list of dicts, scalar). The next load_yaml* call for path will parse the just-written file and refill the cache, which is what callers expect when a CLI tool or test fixture edits a file mid-run.
- utils.cache.yaml.dump_yaml_str(data: Any, *, sort_keys: bool = False, default_flow_style: bool = False, width: int | None = None) str¶
Serialise data to a YAML string.
Use this for stdout output, log messages, or any place a YAML representation is needed without an on-disk file. There is no cache involved (the destination is a string, not a path), but the helper exists so callers don’t have to
import yamljust to callyaml.safe_dump— keeping every YAML touchpoint inutils.cache.yaml.- Parameters:
data – the structure to serialise.
sort_keys – emit mapping keys in sorted order.
default_flow_style – emit collections inline instead of block style.
width – column to wrap long scalars at, None for the PyYAML default.
- utils.cache.yaml.invalidate(path) None¶
Drop one path from the cache.
Use when the file was changed by code that does not go through dump_yaml (e.g. an external subprocess, a manual file rewrite, a fixture in tests). With the mtime+size cache key, normal out-of-band writes invalidate automatically; this helper is the explicit escape hatch when the timestamps must not be trusted.
- utils.cache.yaml.load_yaml(path, *, default_if_missing: Any = <object object>) dict[str, Any]¶
Load a YAML file as a dict, memoised by absolute path.
default_if_missing controls the missing-file behaviour: - Default (_MISSING): raise FileNotFoundError for callers that
want to fail loud (resolution code).
Pass a value (typically {}): return that value when the file does not exist, matching the behaviour expected by cli.administration.inventory.provision.yaml_io.load_yaml.
Raises TypeError when the YAML root is not a mapping; use load_yaml_any for files whose root is a list or scalar.
- utils.cache.yaml.load_yaml_all(path, *, default_if_missing: Any = <object object>) list¶
Memoised multi-document YAML load.
Returns the list of documents from a multi-doc YAML file (
--- ... --- ...). Shares the unified per-path cache withload_yaml_any()/load_yaml(), so loading a file as multi-doc and then as single-doc (or vice versa) parses it exactly once.
- utils.cache.yaml.load_yaml_all_str(text: str) list¶
Parse a multi-document YAML string into a list of documents.
No caching (the source is a string, not a path) — provided for symmetry with
load_yaml_str()so callers never needimport yamlfor the parse.
- utils.cache.yaml.load_yaml_any(path, *, default_if_missing: Any = <object object>) Any¶
Memoised YAML load that accepts any root shape (dict, list, scalar). Use this for files where the root is not a mapping (e.g. Ansible task lists, the root list in meta/variants.yml).
Empty files surface as {} (matching yaml.safe_load(…) or {} semantics that most call sites already rely on).
- utils.cache.yaml.load_yaml_str(text: str) Any¶
Parse a YAML string.
Use this when the YAML payload comes from somewhere other than a file (HTTP response body, stdin, an intermediate buffer). The path-keyed cache cannot help here — every call parses afresh — but the helper exists for symmetry with
dump_yaml_str()so callers never have toimport yamldirectly.
Module contents¶
In-process caches for runtime data (applications, users, domains).
Domain modules:
utils.cache.base— cross-cutting helpers (constants, deep-merge, signatures, render machinery).utils.cache.applications— variants +get_merged_applications(ansible-free at import time, runner-host friendly).utils.cache.users—get_user_defaults,get_merged_users.utils.cache.domains—get_merged_domains.utils.cache.yaml— process-wideload_yaml/load_yaml_anyparser cache shared by everything that reads YAML off-disk.utils.cache.files— process-wide project-tree walk + file-content cache (read_text,iter_project_files,iter_non_ignored_files).utils.cache.gitignore— cached.gitignorepattern loader + path matcher used by lint/integration tests that need a “files git would track” view without spawning git.
Use the domain module directly. The package-level
_reset_cache_for_tests clears every domain’s caches plus the shared
fingerprint memo in one call — preferred over hand-rolling N
_reset() calls per test fixture.
PROJECT_ROOT is the single source of truth for the repository root.
Every callsite that needs it — CLI tools, plugins, tests — MUST import
this value rather than re-derive it via Path(__file__).parents[N] or
os.pardir chains; tests/lint/repository/test_project_root_import.py
enforces that.