Files
siftlode/backend/app/progress.py
T
peter 4aaaaa7860 chore(comments): drop epic, sprint and review-round ids from the code
The code cited its plan instead of saying why: `E28 review J4`, `(R6 S3b)`, `S1 justified…`,
`deep-review round 5`. That plan lives in the private ops repo, so for every reader of this one the
citation was noise, and a sentence whose subject was the plan step ("round 2 showed…") hid its
lesson behind an id. Each one is gone and the "why" kept: a bare citation is deleted, a sentence
carrying a lesson is reworded around it ("a later attempt showed…").

Measured (`scripts/audit_metrics.py`, product code): 652 lines in 270 files carried an epic code by
the definition the next commit holds (507 / 241 by the old, two-digit-only one) -> 0. Across all
code, tests, scripts and configs: ~1100 epic-code lines plus ~320 sprint/round ids.

Behaviour unchanged, checked mechanically: every changed .py file has the same AST once docstrings
are blanked; css/sh/toml/yml/json files are identical once comments are stripped; the only
non-comment changes in TS/JS are test titles, one description string in a contract table, and five
ESLint messages, whose text is all the gates snapshot diff holds. Not touched: Markdown (docs and
the changelog keep their history), Alembic migrations, TV episode notation (`S2 · E5`), DESIGN.md
ids (`C1`, `B4`, `S3h`), tracker ids (`SEC-16`, `DX-17`).

One stale claim fixed on the way: test_hls_reaper's docstring said the player checkpoints "every
10 s whether or not it is playing", which plex/stream.py calls wrong; it now says what the code does.
2026-10-05 00:19:39 +02:00

57 lines
2.2 KiB
Python

"""Lightweight, decoupled progress reporting for long-running scheduler jobs.
A job's work function calls `report(current, total, phase)`; the scheduler binds a sink (via
`bind()`) that forwards it into the in-memory activity the admin dashboard polls. A contextvar
holds the sink, so the sync code stays unaware of the scheduler and concurrent jobs (each in
its own thread/context) never cross wires. When no sink is bound (e.g. a manual sync endpoint),
`report` is a cheap no-op.
"""
import contextvars
from typing import Callable, Literal, Optional
Sink = Callable[[int, Optional[int], Optional[str]], None]
_sink: contextvars.ContextVar[Optional[Sink]] = contextvars.ContextVar(
"progress_sink", default=None
)
def report(current: int, total: int | None = None, phase: str | None = None) -> None:
"""Report job progress to the bound sink, if any. `total=None` means indeterminate."""
sink = _sink.get()
if sink is not None:
sink(current, total, phase)
class JobFailed(RuntimeError):
"""A job's own failure, said so the reader's language can name it: the client reads
`scheduler.failure.<reason>` and interpolates `params` (keep them to values the reader may see —
numbers, ids, short codes). The message is the English line the log and `last_error` keep. An
upstream's own error (a timeout, a refused connection) is not one of these: it stays as raised,
being diagnostic."""
def __init__(self, reason: str, message: str, **params: object):
super().__init__(message)
self.reason = reason
self.params = params
class bind:
"""Context manager that binds a progress sink for the duration of a job run."""
def __init__(self, sink: Sink):
self._sink = sink
self._token = None
def __enter__(self) -> "bind":
self._token = _sink.set(self._sink)
return self
# Literal[False], not bool: this never swallows the job's exception, and saying so is what lets
# a type checker know a name assigned inside `with bind(...)` is bound after it.
def __exit__(self, *exc) -> Literal[False]:
if self._token is not None:
_sink.reset(self._token)
return False