mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-10 22:07:15 +01:00
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
earned_time pre-commit / pre-commit (push) Waiting to run
earned_time tests / test (push) Waiting to run
file length / file-length (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
palette drift / palette (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
shared gate tests / gate unit tests (push) Waiting to run
Rows carry no earner name, so only the ledger path moves; the tutor copies its data dir (old rows included). Pre-cutover AUTOMATION (anki quota), LADDER and the waiver are unchanged. Same minutes, cap, ladder, TUTOR_FROM and confirmed_on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YADHKXzZhhePzQ2rNoXFcK
125 lines
4.5 KiB
Python
125 lines
4.5 KiB
Python
# Copyright (c) 2026 Krzysztof Rudnicki
|
|
"""Demo: what the tutor cutover pays, read from a signed temp ledger.
|
|
|
|
Writes 0.7.0 tutor block rows (no ``detail.minutes``: 15 minutes each since
|
|
0.8.0, which pays per active minute) in the Automation tutor's contract,
|
|
signs them with a throwaway key, counts them with the shared reader
|
|
(``credit_units``) and resolves the day. Prints gaming minutes and shutdown
|
|
for the day before ``TUTOR_FROM`` and for ``TUTOR_FROM`` itself with 0-5
|
|
blocks and a tampered row. Touches no real ledger, key or schedule.
|
|
|
|
Run: ``PYTHONPATH=earned_time python3 earned_time/scripts/demo_tutor_cutover.py``
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import date, datetime, time, timedelta
|
|
import json
|
|
from pathlib import Path
|
|
import sys
|
|
import tempfile
|
|
|
|
import earned_time
|
|
from earned_time import TUTOR, TUTOR_FROM, credit_units, entry_signature
|
|
|
|
_KEY = b"demo-key-not-the-real-one"
|
|
_OTHERS = {"workout": 1, "leetcode": 1, "reading": 1}
|
|
_NONE = {"workout": 0, "leetcode": 0, "reading": 0}
|
|
|
|
|
|
def _say(line: str) -> None:
|
|
sys.stdout.write(line + "\n")
|
|
|
|
|
|
def _hhmm(minutes: int) -> str:
|
|
return f"{minutes // 60:02d}:{minutes % 60:02d}"
|
|
|
|
|
|
def _block(session: str, number: int, day: date) -> dict[str, object]:
|
|
start = datetime.combine(day, time(18)).astimezone()
|
|
ended = (start + timedelta(minutes=15 * number)).timestamp()
|
|
row: dict[str, object] = {
|
|
"kind": "credit",
|
|
"entry_id": f"{session}-b{number}",
|
|
"day": day.isoformat(),
|
|
"created_at": ended + 1,
|
|
"detail": {
|
|
"session_id": session,
|
|
"block": number,
|
|
"active_seconds": 900,
|
|
"ended_at": ended,
|
|
"checks_passed": 3,
|
|
"checks_total": 3,
|
|
},
|
|
}
|
|
return {**row, "hmac": entry_signature(row, _KEY)}
|
|
|
|
|
|
def _units(folder: Path, rows: list[dict[str, object]], day: date) -> int:
|
|
ledger = folder / "ledger.json"
|
|
ledger.write_text(json.dumps({"entries": rows}), encoding="utf-8")
|
|
key = folder / "hmac.key"
|
|
key.write_bytes(_KEY)
|
|
units = credit_units(TUTOR, ledger, key, day)
|
|
if units is None:
|
|
msg = "demo ledger unreadable"
|
|
raise RuntimeError(msg)
|
|
return units
|
|
|
|
|
|
def _line(label: str, answers: dict[str, int], day: date) -> str:
|
|
res = earned_time.resolve(answers, day)
|
|
names = {t.earner.name for t in res.terms}
|
|
if "tutor" in names:
|
|
term = res.term("tutor")
|
|
paid = f"tutor +{term.gaming_minutes}g/+{term.shutdown_minutes}s"
|
|
else:
|
|
paid = "automation waived" # ANKI_WAIVED_FROM..TUTOR_FROM
|
|
return (
|
|
f"{label:<44} {res.gaming_minutes:>4} min ({res.gaming_minutes / 60:.2f} h)"
|
|
f" {_hhmm(res.shutdown_minutes)}"
|
|
f" [{paid},"
|
|
f" base {res.base.gaming_minutes}g/{_hhmm(res.base.shutdown_minutes)}]"
|
|
)
|
|
|
|
|
|
def main() -> None:
|
|
"""Print the table."""
|
|
eve = TUTOR_FROM - timedelta(days=1)
|
|
_say(f"TUTOR_FROM = {TUTOR_FROM} (no first_credits: an unwired consumer)")
|
|
_say(f"{'case':<44} {'gaming':>15} shutdown")
|
|
for done in (0, 1):
|
|
answers = {**_OTHERS, "anki": done, "automation": done}
|
|
_say(_line(f"{eve} eve: others + anki/automation={done}", answers, eve))
|
|
nothing = {**_NONE, "anki": 0, "automation": 0}
|
|
_say(_line(f"{eve} eve: nothing done", nothing, eve))
|
|
with tempfile.TemporaryDirectory() as raw:
|
|
folder = Path(raw)
|
|
for blocks in (0, 1, 2, 4, 5):
|
|
rows = [_block("s1", n, TUTOR_FROM) for n in range(1, blocks + 1)]
|
|
units = _units(folder, rows, TUTOR_FROM)
|
|
for others, tag in ((_OTHERS, "+ all others"), (_NONE, "tutor only")):
|
|
answers = {**others, "tutor": units}
|
|
label = f"{TUTOR_FROM}: {blocks} rows -> {units} units, {tag}"
|
|
_say(_line(label, answers, TUTOR_FROM))
|
|
rows = [_block("s2", n, TUTOR_FROM) for n in range(1, 5)]
|
|
detail = rows[2]["detail"]
|
|
if not isinstance(detail, dict):
|
|
msg = "demo row lost its detail"
|
|
raise TypeError(msg)
|
|
rows[2]["detail"] = {**detail, "active_seconds": 9000} # edited after signing
|
|
units = _units(folder, rows, TUTOR_FROM)
|
|
label = f"{TUTOR_FROM}: 4 rows, 1 tampered -> {units} units"
|
|
_say(_line(label, {**_OTHERS, "tutor": units}, TUTOR_FROM))
|
|
anki = {**_OTHERS, "anki": 1, "tutor": 60}
|
|
_say(
|
|
_line(
|
|
f"{TUTOR_FROM}: all + a stale anki answer (ignored)", anki, TUTOR_FROM
|
|
)
|
|
)
|
|
_say(f"gaming ceiling {earned_time.GAMING_CEILING_MINUTES} min (8 h)")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|