mirror of
https://github.com/kuhyx/steam-backlog-enforcer.git
synced 2026-11-10 19:27:04 +01:00
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (mypy, mypy, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (pylint, pylint, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (rest, , mypy,pylint) (push) Waiting to run
Tests / test (push) Waiting to run
Tests / web-coverage (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
77 lines
2.7 KiB
Python
77 lines
2.7 KiB
Python
"""Typed-phrase friction shared by the CLI, the web job runner and the daemon.
|
|
|
|
One table, so a phrase cannot be strict in one front end and lax in another.
|
|
The phrase is checked wherever the action is actually carried out (the job
|
|
subprocess or the root daemon), never only in the browser: a check that
|
|
``curl`` can skip is decoration, not friction.
|
|
|
|
Templates use ``str.format`` fields; the caller supplies every field the
|
|
template names (``days``, ``minutes``, ``game_name``, ``count``, ``backup_id``).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from typing import Final
|
|
|
|
from steam_backlog_enforcer._store_window import DEFAULT_WINDOW_MINUTES
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Friction:
|
|
"""The friction attached to one command.
|
|
|
|
Attributes:
|
|
phrase_template: Text the user must type, with ``{field}`` slots.
|
|
countdown_seconds: Armed wait with heartbeats before commit (0 = none).
|
|
"""
|
|
|
|
phrase_template: str
|
|
countdown_seconds: int = 0
|
|
|
|
|
|
GAMING_RESET_COUNTDOWN_SECONDS: Final = 300
|
|
|
|
_STORE_WINDOW_PHRASE: Final = "unblock the store for {minutes} minutes"
|
|
|
|
FRICTION: Final[dict[str, Friction]] = {
|
|
"gaming-reset": Friction(
|
|
"reset today's gaming budget", GAMING_RESET_COUNTDOWN_SECONDS
|
|
),
|
|
"block-gaming": Friction("block all gaming for {days} days"),
|
|
"unblock": Friction(_STORE_WINDOW_PHRASE),
|
|
# buy-dlc takes no params: it is always the default-length window, and the
|
|
# daemon checks it as an ``unblock`` of that length. So its phrase is that
|
|
# exact sentence with the number already in it, not a ``{minutes}`` slot
|
|
# that nothing could fill.
|
|
"buy-dlc": Friction(_STORE_WINDOW_PHRASE.format(minutes=DEFAULT_WINDOW_MINUTES)),
|
|
"gaming-unblock": Friction("force release playtime mounts"),
|
|
"abandon-pick": Friction("abandon {game_name}"),
|
|
"pick-manual": Friction("lock in {game_name}"),
|
|
"add-exception": Friction("request exception for {game_name}"),
|
|
"uninstall": Friction("uninstall {count} games"),
|
|
"reset": Friction("wipe all enforcer state"),
|
|
"restore-backup": Friction("restore backup {backup_id}"),
|
|
}
|
|
|
|
|
|
def expected_phrase(command: str, **fields: object) -> str | None:
|
|
"""Return the exact phrase *command* requires, or ``None`` if it has none.
|
|
|
|
Raises:
|
|
KeyError: If the template names a field missing from *fields*.
|
|
"""
|
|
friction = FRICTION.get(command)
|
|
if friction is None:
|
|
return None
|
|
return friction.phrase_template.format(**fields)
|
|
|
|
|
|
def phrase_matches(expected: str, typed: str) -> bool:
|
|
"""Compare a typed phrase to the expected one.
|
|
|
|
Only surrounding whitespace is forgiven. Case and inner spacing must
|
|
match: retyping the sentence exactly is the point.
|
|
"""
|
|
return typed.strip() == expected
|