recon_gen.common.env_keys

Y.2.gate.b.15 — Typed registry for every RECON_GEN_* / RECON_E2E_* env var.

AC.B.3 — env vars renamed from QS_GEN_* / QS_E2E_* to RECON_GEN_* / RECON_E2E_* alongside the package rename. Each spec carries a legacy_name pointing at the old name so operators with export QS_GEN_*=... in their shell rc continue to work for a 1-2 month grace window (mirrors the PyPI shim window). Reads emit a one-shot DeprecationWarning per legacy env-var name. The legacy fallback gets dropped in the same release that drops the quicksight-gen PyPI shim.

Why this exists

Bare os.environ.get("RECON_GEN_RUN_DIR") strings spread across runner + config + conftest + harness + helpers as the test layer chain runner grew. Three independent failure modes:

  1. Typos. os.environ.get("RECON_GEN_DEMO_DATABASE_URLL") silently falls through to “env var unset” — no TypeError, no NameError, no probe failure. The bug surfaces as “DB connection used the cfg default instead of the variant URL” 30 seconds later in a totally different stack frame.

  2. Required-vs-optional context. RECON_GEN_RUN_DIR is optional (sidecar capture); RECON_E2E_USER_ARN is required (browser e2e embed-URL signing); RECON_GEN_DEMO_DATABASE_URL is context- required (set by the runner for non-default variants). Every call site re-implements its own None-handling.

  3. Value validation. When RECON_GEN_CONFIG=/typo/path is set, the failure shows up as FileNotFoundError 5 frames deep inside load_config — the operator has to trace it back to the env var. Better: catch at the boundary with the env var name + description in the error message.

(The historical context above used QS_GEN_* examples — flipped to the new RECON_GEN_* form during AC.B.3 to match the live registry.)

Locked design (Y.2.gate.b.15.spec): typed EnvVar[T] dataclass per env var carrying name + description + coercer + optional + validator. Three operations:

  • .get_or_none() -> T | None — sidecar shape; absent or empty → None. Validator runs on the coerced value when present.

  • .require() -> T — required-context shape; raises EnvVarRequired with the spec’s description as the operator-actionable hint when absent.

  • .serialize(value: T) -> str — set-in-subprocess-env shape; validator runs first (catches set-side bugs like “runner forgot to mkdir before setting RUN_DIR”).

Validation runs in BOTH directions so set-side bugs and get-side bugs both surface at the boundary with EnvVarInvalid carrying name + description.

The b.15.lint follow-up adds an AST lint that catches any code attempting to bypass this registry (bare os.environ.get with a QS_* literal). Until then, the convention is enforced by review.

Functions

dump_env_access()

Return a snapshot of every env-var access made by this process, in order.

matches(pattern)

Return a validator that checks the value matches pattern.

must_be_dir(p)

Path must exist AND be a directory.

must_be_file(p)

Path must exist AND be a regular file.

must_exist(p)

Path must exist (file or directory).

positive_int(n)

Integer must be > 0.

reset_env_access()

Clear the access log.

Classes

EnvVar(name, description, coercer[, ...])

Spec for one env var.

Exceptions

EnvVarError(spec_name, description, detail)

Base for env-var registry errors.

EnvVarInvalid(spec_name, description, detail)

Raised by EnvVar.get_or_none() / .require() / .serialize() when the value fails the spec's validator (e.g., path doesn't exist, int is non-positive).

EnvVarRequired(spec_name, description, detail)

Raised by EnvVar.require() when the env var is unset (or empty).

class recon_gen.common.env_keys.EnvVar(name, description, coercer, optional=True, validator=None, legacy_name=None)[source]

Bases: Generic

Spec for one env var. Frozen so call sites can declare these at module top-level without mutation worry.

Fields:

name: the env-var name (the string passed to os.environ). description: operator-facing description; surfaces in the

error message when require() finds the var unset or get_or_none() finds the value invalid.

coercer: str T. Examples: Path, int,

_bool_coercer, str (identity).

optional: True iff the var is allowed to be absent. Affects

documentation only; get_or_none() always tolerates absence and require() always raises on absence.

validator: optional T None callable. Raises

ValueError (re-wrapped as EnvVarInvalid by the EnvVar machinery) when the coerced value is unacceptable (path doesn’t exist, int non-positive, etc.). Runs in both directions: get-side AND serialize-side.

Parameters:
  • name (str)

  • description (str)

  • coercer (Callable[[str], T])

  • optional (bool)

  • validator (Callable[[T], None] | None)

  • legacy_name (str | None)

coercer: Callable[[str], T]
description: str
get_or_none()[source]

Read the env var. Returns None when unset or empty. Coerces + validates when present; EnvVarInvalid on validator failure.

Return type:

Optional[TypeVar(T)]

legacy_name: str | None = None

AC.B.3 — deprecated legacy env-var name to fall back to when the canonical name is unset. Reads emit a one-shot DeprecationWarning per legacy name (deduped via _legacy_warned). Set during AC.B.3 for the QS_GEN_* / QS_E2E_*RECON_* rename; the fallback is dropped alongside the PyPI shim drop.

name: str
optional: bool = True
require()[source]

Read the env var. Raises EnvVarRequired when unset or empty (with the spec’s description in the error). Coerces + validates when present.

Return type:

TypeVar(T)

serialize(value)[source]

Convert value to a string for placement in a subprocess env dict. Validates before serializing so set-side bugs (“runner forgot to mkdir before setting RUN_DIR”) surface at the same boundary as get-side bugs.

Return type:

str

Parameters:

value (T)

validator: Callable[[T], None] | None = None
exception recon_gen.common.env_keys.EnvVarError(spec_name, description, detail)[source]

Bases: Exception

Base for env-var registry errors. Carries the spec name + description so the operator-facing message is always actionable.

Parameters:
  • spec_name (str)

  • description (str)

  • detail (str)

Return type:

None

exception recon_gen.common.env_keys.EnvVarInvalid(spec_name, description, detail)[source]

Bases: EnvVarError

Raised by EnvVar.get_or_none() / .require() / .serialize() when the value fails the spec’s validator (e.g., path doesn’t exist, int is non-positive).

Parameters:
  • spec_name (str)

  • description (str)

  • detail (str)

Return type:

None

exception recon_gen.common.env_keys.EnvVarRequired(spec_name, description, detail)[source]

Bases: EnvVarError

Raised by EnvVar.require() when the env var is unset (or empty).

Parameters:
  • spec_name (str)

  • description (str)

  • detail (str)

Return type:

None

recon_gen.common.env_keys.dump_env_access()[source]

Return a snapshot of every env-var access made by this process, in order. Each entry is (canonical_name, op) where op is "read_hit" / "read_miss" / "write".

Useful for CB.17.d’s strangler-pattern verification: run both the legacy + thin paths through this hook, compare the access lists, surface any env-var that one path uses but the other doesn’t.

The snapshot is shallow-copied so callers can clear / inspect without racing the next access.

Return type:

list[tuple[str, str]]

recon_gen.common.env_keys.matches(pattern)[source]

Return a validator that checks the value matches pattern.

Return type:

Callable[[str], None]

Parameters:

pattern (Pattern[str])

recon_gen.common.env_keys.must_be_dir(p)[source]

Path must exist AND be a directory.

Return type:

None

Parameters:

p (Path)

recon_gen.common.env_keys.must_be_file(p)[source]

Path must exist AND be a regular file.

Return type:

None

Parameters:

p (Path)

recon_gen.common.env_keys.must_exist(p)[source]

Path must exist (file or directory).

Return type:

None

Parameters:

p (Path)

recon_gen.common.env_keys.positive_int(n)[source]

Integer must be > 0.

Return type:

None

Parameters:

n (int)

recon_gen.common.env_keys.reset_env_access()[source]

Clear the access log. Useful when the same process runs both legacy + thin paths back-to-back and wants per-path diffs.

Return type:

None