Files
computer-playbook/utils/env/parser.py
T
kevinveenbirkenbachandClaude Opus 5 5009ec81c9 feat(ci): deploy a sweep as serial chunks instead of three mode lines
One discovery query per run replaces one per mode, so the row basis, the
ranking and the budget cut all speak the same role#variant unit. The
result is deployed in serial chunk blocks rather than three parallel
mode matrices.

Chunk size is derived, not pinned: GitHub cancels a job queued for
INFINITO_CI_QUEUE_HOURS, so with INFINITO_CI_CONCURRENCY runners free a
chunk holds concurrency * floor(queue_hours / the deploy job's
timeout-minutes) rows. Assuming every job burns its full timeout is
deliberately pessimistic -- no job can outlast it -- so the drain
estimate can only overshoot.

The run job cap bounds the sweep as a whole; rows beyond it roll into
the next sweep, whose regular line starts at an offset so consecutive
sweeps walk the whole list instead of re-testing the same head. Priority
rows lead every sweep, never move with that offset, and force a chunk
boundary at the seam so a chunk is either all priority or all regular.

sequencing.py goes with the mode lines it ordered: chunking restarts the
queue clock at a controlled size, which is what serialising the modes
was approximating with three buckets of arbitrary size.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-15 06:09:04 +02:00

105 lines
3.8 KiB
Python

"""Parse the project's `default.env`.
The format is the docker-compose `.env`-subset: flat `KEY=value` lines,
`#` for comments, optional surrounding double/single quotes for values
that contain whitespace or special characters. Anything more exotic
(nested mappings, multiline values, variable expansion) is rejected so
drift between the file and its consumers stays loud.
Comments are first-class: every `# ...` line directly above a key is
captured and surfaced via :func:`parse_static_env_with_comments`, so
the generator can preserve per-key documentation in the produced
``.env``.
"""
from __future__ import annotations
import os
import re
from typing import TYPE_CHECKING
from utils.cache.files import PROJECT_ROOT, read_text
if TYPE_CHECKING:
from pathlib import Path
_LINE_RE = re.compile(r"^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$")
def env_setting(key: str) -> str:
"""One env setting: the process environment wins, ``default.env`` is the
SPOT behind it.
The fallback is not a default -- the value still comes from the single
source, just read directly instead of through a generated ``.env``. It is
what lets a CLI run outside ``scripts/meta/env/load.sh`` without either
inventing a value or dying on a key the repository does declare.
Raises:
KeyError: the key is declared nowhere. A budget or filter input that
exists in no source must fail loudly, not resolve to ``""``.
"""
raw = os.environ.get(key)
if raw is not None and raw.strip():
return raw
return parse_static_env(PROJECT_ROOT / "default.env")[key]
def _parse_value(raw_value: str) -> str:
"""One declared value: an unquoted value loses its trailing inline
comment, and a matched pair of outer quotes is stripped with the writer's
``\\\\``/``\\"`` escaping undone (bash double-quote semantics, for the
subset of values this parser is allowed to handle)."""
value = raw_value
if value and value[0] not in ('"', "'"):
value = value.split("#", 1)[0].rstrip()
if len(value) >= 2 and value[0] == value[-1] and value[0] in ('"', "'"):
quote = value[0]
value = value[1:-1]
if quote == '"':
value = value.replace('\\"', '"').replace("\\\\", "\\")
return value
def parse_static_env(path: Path) -> dict[str, str]:
"""Return the `KEY: value` map declared in `path`."""
values, _ = parse_static_env_with_comments(path)
return values
def parse_static_env_with_comments(
path: Path,
) -> tuple[dict[str, str], dict[str, str]]:
"""Return (`values`, `comments`) for the env-file at `path`.
A key's comment is the most recent contiguous block of `# ...`
lines directly above the `KEY=...` line, joined with single spaces.
Section headers (`# --- ... ---`) are treated as separators: they
reset the pending comment so the next key starts fresh, instead
of inheriting the heading line as its own documentation.
"""
values: dict[str, str] = {}
comments: dict[str, str] = {}
pending: list[str] = []
for lineno, raw in enumerate(read_text(str(path)).splitlines(), 1):
stripped = raw.strip()
if not stripped:
pending = []
continue
if stripped.startswith("#"):
body = stripped.lstrip("#").strip()
if body.startswith("---") and body.endswith("---"):
pending = []
continue
pending.append(body)
continue
match = _LINE_RE.match(stripped)
if not match:
raise ValueError(f"{path}:{lineno}: cannot parse line: {raw!r}")
key, raw_value = match.group(1), match.group(2)
values[key] = _parse_value(raw_value)
if pending:
comments[key] = " ".join(pending)
pending = []
return values, comments