From 3af8fe22efbc7d5c26e8fc8c1e67af4fffcf7ab1 Mon Sep 17 00:00:00 2001 From: gitea-actions Date: Sat, 22 Aug 2026 18:22:35 +0200 Subject: [PATCH 1/8] =?UTF-8?q?Zamknout=20z=C3=A1vislosti?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bez zamčených verzí si každá stavba stáhne to, co je zrovna nejnovější: binárka z dneška a binárka z příštího jara obsahují něco jiného a není jak zjistit co. Zároveň může vydání kterékoli knihovny rozbít build, aniž by se sáhlo na řádku kódu — a nejspíš zrovna ve chvíli, kdy se tagne release. Volné seznamy jsou nově v `.in`, zamčené v `.txt`. Rozděleno na tři: běh, testy a stavba. Pytest se tak přestane instalovat do prostředí, kde se staví binárka, a PyInstaller má konečně taky zamčenou verzi — ta na podobu výsledku sahá ze všech nejvíc. Zůstává, že se verze píšou i v hlavičce PEP 723, jinak by přestalo fungovat `uv run web.py`. Aby se ta dvě místa nerozešla, hlídá je test. --- .gitea/workflows/binarka.yml | 2 +- .gitea/workflows/testy.yml | 2 +- .github/workflows/binarky.yml | 2 +- README.md | 26 ++++++++++++++- build.sh | 2 +- dice.py | 2 +- requirements-build.in | 4 +++ requirements-build.txt | 37 +++++++++++++++++++++ requirements-dev.in | 4 +++ requirements-dev.txt | 35 ++++++++++++++++++++ requirements.in | 6 ++++ requirements.txt | 29 ++++++++++++++--- tests/test_zavislosti.py | 60 +++++++++++++++++++++++++++++++++++ web.py | 2 +- 14 files changed, 202 insertions(+), 11 deletions(-) create mode 100644 requirements-build.in create mode 100644 requirements-build.txt create mode 100644 requirements-dev.in create mode 100644 requirements-dev.txt create mode 100644 requirements.in create mode 100644 tests/test_zavislosti.py diff --git a/.gitea/workflows/binarka.yml b/.gitea/workflows/binarka.yml index ae0b920..77b53eb 100644 --- a/.gitea/workflows/binarka.yml +++ b/.gitea/workflows/binarka.yml @@ -53,7 +53,7 @@ jobs: uv --version - name: Testy - run: uv run --with-requirements requirements.txt --no-project pytest -q + run: uv run --with-requirements requirements-dev.txt --no-project pytest -q - name: Binárka run: ./build.sh diff --git a/.gitea/workflows/testy.yml b/.gitea/workflows/testy.yml index 10d723c..eb7231e 100644 --- a/.gitea/workflows/testy.yml +++ b/.gitea/workflows/testy.yml @@ -49,7 +49,7 @@ jobs: - name: Testy run: | uv run --managed-python --python "${{ matrix.python }}" \ - --with-requirements requirements.txt --no-project \ + --with-requirements requirements-dev.txt --no-project \ pytest -q - name: Terminálová verze diff --git a/.github/workflows/binarky.yml b/.github/workflows/binarky.yml index f1a9aef..33f4b15 100644 --- a/.github/workflows/binarky.yml +++ b/.github/workflows/binarky.yml @@ -52,7 +52,7 @@ jobs: - uses: astral-sh/setup-uv@v10.0.1 - name: Testy - run: uv run --with-requirements requirements.txt --no-project pytest -q + run: uv run --with-requirements requirements-dev.txt --no-project pytest -q - name: Binárka run: ./build.sh diff --git a/README.md b/README.md index 4ad0c60..72d0dac 100644 --- a/README.md +++ b/README.md @@ -155,10 +155,34 @@ python -m venv .venv Na Fedoře stačí i systémové balíčky, pak se nemusí řešit vůbec nic: ```bash -sudo dnf install python3-flask python3-qrcode python3-tabulate +sudo dnf install python3-flask python3-qrcode python3-tabulate python3-waitress python3 web.py ``` +## Závislosti + +Verze jsou zamčené, aby binárka postavená dnes a za půl roku obsahovala +totéž. Volné seznamy jsou v `.in`, zamčené v `.txt`: + +| Soubor | K čemu | +|---|---| +| `requirements.in` → `.txt` | běh aplikace | +| `requirements-dev.in` → `.txt` | + pytest, pro testy | +| `requirements-build.in` → `.txt` | + PyInstaller, pro stavbu binárky | + +Aktualizace je vědomý krok, ne vedlejší efekt toho, že něco vyšlo na PyPI: + +```bash +for f in requirements requirements-dev requirements-build; do + uv pip compile "$f.in" -o "$f.txt" +done +``` + +Verze se píšou ještě jednou v hlavičce PEP 723 na začátku `web.py` a +`dice.py`, aby fungovalo `uv run web.py`. Že se ty dva zápisy shodují, hlídá +`tests/test_zavislosti.py` — po každém přegenerování je potřeba hlavičky +srovnat. + ## Vlastní binárka ```bash diff --git a/build.sh b/build.sh index ee0cf48..03b120a 100755 --- a/build.sh +++ b/build.sh @@ -26,7 +26,7 @@ if [ -d "$BUILD/Scripts" ]; then pyinstaller="$BUILD/Scripts/pyinstaller.exe" fi -uv pip install --quiet --python "$python" -r requirements.txt pyinstaller +uv pip install --quiet --python "$python" -r requirements-build.txt "$pyinstaller" --clean --noconfirm \ --distpath dist --workpath "$BUILD/work" kostky.spec diff --git a/dice.py b/dice.py index d05bf15..8135999 100644 --- a/dice.py +++ b/dice.py @@ -1,6 +1,6 @@ # /// script # requires-python = ">=3.11" -# dependencies = ["tabulate"] +# dependencies = ["tabulate==0.10.0"] # /// """Počitadlo skóre v terminálu. diff --git a/requirements-build.in b/requirements-build.in new file mode 100644 index 0000000..fffb682 --- /dev/null +++ b/requirements-build.in @@ -0,0 +1,4 @@ +# Co je potřeba ke stavbě binárky. Zamyká se do requirements-build.txt: +# uv pip compile requirements-build.in -o requirements-build.txt +-r requirements.in +pyinstaller diff --git a/requirements-build.txt b/requirements-build.txt new file mode 100644 index 0000000..903e786 --- /dev/null +++ b/requirements-build.txt @@ -0,0 +1,37 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile requirements-build.in -o requirements-build.txt --python-version 3.11 +altgraph==0.17.5 + # via pyinstaller +blinker==1.9.0 + # via flask +click==8.4.2 + # via flask +flask==3.1.3 + # via -r requirements.in +itsdangerous==2.2.0 + # via flask +jinja2==3.1.6 + # via flask +markupsafe==3.0.3 + # via + # flask + # jinja2 + # werkzeug +packaging==26.3 + # via + # pyinstaller + # pyinstaller-hooks-contrib +pyinstaller==6.22.2 + # via -r requirements-build.in +pyinstaller-hooks-contrib==2026.6 + # via pyinstaller +qrcode==8.2 + # via -r requirements.in +setuptools==84.0.0 + # via pyinstaller +tabulate==0.10.0 + # via -r requirements.in +waitress==3.0.2 + # via -r requirements.in +werkzeug==3.1.8 + # via flask diff --git a/requirements-dev.in b/requirements-dev.in new file mode 100644 index 0000000..1e2d1dc --- /dev/null +++ b/requirements-dev.in @@ -0,0 +1,4 @@ +# Co je potřeba k vývoji a testům. Zamyká se do requirements-dev.txt: +# uv pip compile requirements-dev.in -o requirements-dev.txt +-r requirements.in +pytest diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..49187f9 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,35 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile requirements-dev.in -o requirements-dev.txt --python-version 3.11 +blinker==1.9.0 + # via flask +click==8.4.2 + # via flask +flask==3.1.3 + # via -r requirements.in +iniconfig==2.3.0 + # via pytest +itsdangerous==2.2.0 + # via flask +jinja2==3.1.6 + # via flask +markupsafe==3.0.3 + # via + # flask + # jinja2 + # werkzeug +packaging==26.3 + # via pytest +pluggy==1.6.0 + # via pytest +pygments==2.21.0 + # via pytest +pytest==9.1.1 + # via -r requirements-dev.in +qrcode==8.2 + # via -r requirements.in +tabulate==0.10.0 + # via -r requirements.in +waitress==3.0.2 + # via -r requirements.in +werkzeug==3.1.8 + # via flask diff --git a/requirements.in b/requirements.in new file mode 100644 index 0000000..90f9743 --- /dev/null +++ b/requirements.in @@ -0,0 +1,6 @@ +# Běhové závislosti. Verze se zamykají do requirements.txt přes: +# uv pip compile requirements.in -o requirements.txt +flask +qrcode +tabulate +waitress diff --git a/requirements.txt b/requirements.txt index f017b4d..68ed760 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,25 @@ -flask -qrcode -tabulate -pytest +# This file was autogenerated by uv via the following command: +# uv pip compile requirements.in -o requirements.txt --python-version 3.11 +blinker==1.9.0 + # via flask +click==8.4.2 + # via flask +flask==3.1.3 + # via -r requirements.in +itsdangerous==2.2.0 + # via flask +jinja2==3.1.6 + # via flask +markupsafe==3.0.3 + # via + # flask + # jinja2 + # werkzeug +qrcode==8.2 + # via -r requirements.in +tabulate==0.10.0 + # via -r requirements.in +waitress==3.0.2 + # via -r requirements.in +werkzeug==3.1.8 + # via flask diff --git a/tests/test_zavislosti.py b/tests/test_zavislosti.py new file mode 100644 index 0000000..505aa93 --- /dev/null +++ b/tests/test_zavislosti.py @@ -0,0 +1,60 @@ +"""Závislosti jsou zamčené a všechna místa se shodnou. + +Verze se píšou na dvou místech: v `requirements.txt` pro stavbu binárky +a v hlavičce PEP 723 pro `uv run web.py`. Ta dvě místa se dřív nebo později +rozejdou — leda že by to někdo hlídal. Proto tenhle soubor. +""" + +import re +from pathlib import Path + +import pytest + +ROOT = Path(__file__).parent.parent + +# Skript a to, co si podle hlavičky nese s sebou. +SCRIPTS = {"web.py": {"flask", "qrcode", "waitress"}, + "dice.py": {"tabulate"}} + + +def pins(name: str) -> dict[str, str]: + """Zamčené verze z compilovaného requirements souboru.""" + text = (ROOT / name).read_text(encoding="utf-8") + return {m.group(1).lower(): m.group(2) + for m in re.finditer(r"^([A-Za-z0-9_.\-]+)==(\S+)", text, re.M)} + + +def header(script: str) -> dict[str, str]: + """Závislosti z hlavičky PEP 723 na začátku skriptu.""" + text = (ROOT / script).read_text(encoding="utf-8") + line = re.search(r"^# dependencies = \[(.*)\]$", text, re.M) + assert line, f"{script} nemá hlavičku PEP 723" + + return {m.group(1).lower(): m.group(2) + for m in re.finditer(r'"([A-Za-z0-9_.\-]+)==(\S+?)"', line.group(1))} + + +@pytest.mark.parametrize("name", ["requirements.txt", "requirements-dev.txt", + "requirements-build.txt"]) +def test_zavislosti_jsou_zamcene(name): + """Bez zamčených verzí se binárka z března liší od binárky z dubna.""" + lines = [line.strip() for line in (ROOT / name).read_text().splitlines()] + volne = [line for line in lines + if line and not line.startswith("#") and "==" not in line] + + assert not volne, f"{name} má nezamčené závislosti: {volne}" + + +@pytest.mark.parametrize("script", sorted(SCRIPTS)) +def test_hlavicka_sedi_s_requirements(script): + """`uv run web.py` musí dostat totéž, co se zabalí do binárky.""" + zamcene = pins("requirements.txt") + v_hlavicce = header(script) + + assert set(v_hlavicce) == SCRIPTS[script] + + for balicek, verze in v_hlavicce.items(): + assert balicek in zamcene, f"{balicek} chybí v requirements.txt" + assert verze == zamcene[balicek], ( + f"{script}: {balicek}=={verze}, ale zamčeno je " + f"{zamcene[balicek]} — přepiš hlavičku") diff --git a/web.py b/web.py index 57faac7..1dbc687 100644 --- a/web.py +++ b/web.py @@ -1,6 +1,6 @@ # /// script # requires-python = ">=3.11" -# dependencies = ["flask", "qrcode"] +# dependencies = ["flask==3.1.3", "qrcode==8.2", "waitress==3.0.2"] # /// """Webové počitadlo pro domácí síť. From 4ffd7d3c9de5e37029dcd6ee8405c7e496c7da28 Mon Sep 17 00:00:00 2001 From: gitea-actions Date: Sat, 22 Aug 2026 18:23:38 +0200 Subject: [PATCH 2/8] =?UTF-8?q?Verzovat=20sch=C3=A9ma=20datab=C3=A1ze?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dokud to běželo jen doma, šlo dice.db v nejhorším smazat. Teď jsou venku binárky, které si zakládají databázi v %LOCALAPPDATA% a v Library, a ta data patří cizím lidem — první sloupec, který přibude, na nich prostě nebude. Verze se drží v PRAGMA user_version. Existující databáze mají nulu a plné schéma zároveň, což vychází: SCHEMA je samé IF NOT EXISTS, takže se na nich jen orazítkuje a nic se neztratí. Databázi z novější verze aplikace radši odmítneme otevřít. Srozumitelná hláška je lepší než tiše rozbitá data, ke kterým neexistuje záloha. --- README.md | 6 ++++ storage.py | 41 +++++++++++++++++++++++++- tests/test_storage.py | 67 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 113 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 72d0dac..6073eab 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,12 @@ sad vedle sebe. Zálohovat i stěhovat jde prostým zkopírováním souboru; `DICE_DB` si ho najde kdekoli. +Schéma má verzi uloženou v `PRAGMA user_version` a `storage.migrate()` si +databázi při každém připojení dorovná. Starší soubory z verzí, kdy se verze +schématu ještě nepsala, se poznají podle nuly a orazítkují se. Databázi +založenou novější verzí počitadla appka odmítne otevřít, místo aby ji tiše +rozbila. + Počítá se s domácí sítí — appka nemá přihlašování a kdokoli na stejné WiFi může zapisovat. diff --git a/storage.py b/storage.py index 80eca90..db29428 100644 --- a/storage.py +++ b/storage.py @@ -86,6 +86,45 @@ CREATE TABLE IF NOT EXISTS turns ( CREATE INDEX IF NOT EXISTS turns_game ON turns(game_id, id); """ +# Každý krok posouvá schéma o jedno dál; krok na indexu 0 vede z verze 1 na 2. +# Přidávat jen na konec a nikdy nesahat na ten, který už někomu proběhl — +# databáze leží lidem na discích a zpátky se nedají stáhnout. +MIGRATIONS: list[str] = [] + +SCHEMA_VERSION = 1 + len(MIGRATIONS) + + +def migrate(conn: sqlite3.Connection) -> int: + """Dorovná databázi na aktuální schéma a vrátí její verzi. + + Verze se drží v `PRAGMA user_version`, což je celé číslo, které si SQLite + veze v hlavičce souboru a jinak si ho nevšímá. + + Nula znamená dvojí: buď je soubor prázdný, nebo pochází z doby, kdy se + verze ještě nezapisovala. Obojí se vyřeší stejně — `SCHEMA` je samé + `IF NOT EXISTS`, takže na hotové databázi neudělá nic a jen se orazítkuje. + """ + version = conn.execute("PRAGMA user_version").fetchone()[0] + + if version > SCHEMA_VERSION: + raise RuntimeError( + f"Databáze je ve verzi {version}, tahle aplikace umí " + f"{SCHEMA_VERSION}. Nejspíš ji založila novější verze počitadla; " + f"aktualizuj, ať o data nepřijdeš.") + + if version == 0: + conn.executescript(SCHEMA) + version = 1 + + for step in MIGRATIONS[version - 1:]: + conn.executescript(step) + version += 1 + + # PRAGMA neumí zástupné otazníky. Číslo sem teče z kódu, ne od uživatele. + conn.execute(f"PRAGMA user_version = {version}") + conn.commit() + return version + def connect(path: Path | str | None = None) -> sqlite3.Connection: target = Path(path or DB_PATH) @@ -93,7 +132,7 @@ def connect(path: Path | str | None = None) -> sqlite3.Connection: conn = sqlite3.connect(target) conn.row_factory = sqlite3.Row conn.execute("PRAGMA foreign_keys = ON") - conn.executescript(SCHEMA) + migrate(conn) return conn diff --git a/tests/test_storage.py b/tests/test_storage.py index a07e2c6..34af97a 100644 --- a/tests/test_storage.py +++ b/tests/test_storage.py @@ -1,3 +1,4 @@ +import sqlite3 from pathlib import Path import pytest @@ -182,3 +183,69 @@ def test_connect_zalozi_chybejici_adresar(tmp_path): conn.close() assert (tmp_path / "novy" / "adresar" / "dice.db").exists() + + +# --- verze schématu --------------------------------------------------------- + +def verze(conn) -> int: + return conn.execute("PRAGMA user_version").fetchone()[0] + + +def test_nova_databaze_je_orazitkovana(tmp_path): + conn = storage.connect(tmp_path / "nova.db") + try: + assert verze(conn) == storage.SCHEMA_VERSION + finally: + conn.close() + + +def test_databaze_z_doby_pred_verzovanim_prezije(tmp_path): + """Přesně to, co lidem leží na disku z verzí 1.0 a 1.1. + + Tabulky má, razítko ne. Nesmí se ani přijít o data, ani spadnout. + """ + path = tmp_path / "stara.db" + stara = sqlite3.connect(path) + stara.executescript(storage.SCHEMA) # bez PRAGMA user_version + stara.execute("INSERT INTO players (name, key, created_at) " + "VALUES ('Adam', 'adam', '2026-01-01T00:00:00')") + stara.commit() + stara.close() + + conn = storage.connect(path) + try: + assert verze(conn) == storage.SCHEMA_VERSION + assert [p["name"] for p in storage.known_players(conn)] == ["Adam"] + finally: + conn.close() + + +def test_migrace_dojede_jen_ty_chybejici(tmp_path, monkeypatch): + """Podruhé už krok proběhnout nesmí — jinak by ALTER TABLE spadl.""" + monkeypatch.setattr(storage, "MIGRATIONS", + ["ALTER TABLE players ADD COLUMN barva TEXT;"]) + monkeypatch.setattr(storage, "SCHEMA_VERSION", 2) + + path = tmp_path / "migrovana.db" + conn = storage.connect(path) + conn.close() + + conn = storage.connect(path) # druhé spuštění téže verze + try: + assert verze(conn) == 2 + sloupce = [r[1] for r in conn.execute("PRAGMA table_info(players)")] + assert "barva" in sloupce + finally: + conn.close() + + +def test_databaze_z_novejsi_verze_se_odmitne(tmp_path): + """Radši srozumitelná hláška než tiše rozbitá data.""" + path = tmp_path / "budouci.db" + conn = storage.connect(path) + conn.execute(f"PRAGMA user_version = {storage.SCHEMA_VERSION + 1}") + conn.commit() + conn.close() + + with pytest.raises(RuntimeError, match="novější verze"): + storage.connect(path) From e3d56f4c22918f102aadab7e8da8da70a759e1d5 Mon Sep 17 00:00:00 2001 From: gitea-actions Date: Sat, 22 Aug 2026 18:26:32 +0200 Subject: [PATCH 3/8] =?UTF-8?q?Bin=C3=A1rka=20m=C3=A1=20v=C4=9Bd=C4=9Bt,?= =?UTF-8?q?=20kter=C3=A1=20je?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Venku jsou čtyři soubory na release, tři systémy a k tomu zdrojáky. Když někdo napíše, že mu něco nefunguje, není jak zjistit, co vlastně pustil. Verzi zapisuje build.sh do verze.txt, kostky.spec ji přibalí a version.py ji z rozbalené binárky přečte. Ze zdrojáků má přednost git describe, který ví i o commitech nad tagem a o rozdělané práci. V CI se na tagu vnucuje proměnnou, protože runner klonuje jeden commit bez značek. Hlásí se přes --version, v úvodní hlášce serveru a v patičce každé stránky. Zkouška v obou workflow ověří, že se do binárky opravdu dostala. --- .gitea/workflows/binarka.yml | 9 ++++++- .github/workflows/binarky.yml | 12 ++++++++- .gitignore | 1 + README.md | 11 ++++++++ build.sh | 13 +++++++--- dice.py | 7 +++++ kostky.spec | 9 ++++++- static/style.css | 8 ++++++ templates/base.html | 1 + tests/test_version.py | 48 ++++++++++++++++++++++++++++++++++ version.py | 49 +++++++++++++++++++++++++++++++++++ web.py | 21 +++++++++++++-- 12 files changed, 181 insertions(+), 8 deletions(-) create mode 100644 tests/test_version.py create mode 100644 version.py diff --git a/.gitea/workflows/binarka.yml b/.gitea/workflows/binarka.yml index 77b53eb..32fa422 100644 --- a/.gitea/workflows/binarka.yml +++ b/.gitea/workflows/binarka.yml @@ -56,7 +56,13 @@ jobs: run: uv run --with-requirements requirements-dev.txt --no-project pytest -q - name: Binárka - run: ./build.sh + run: | + # Runner klonuje jeden commit bez značek, takže si git describe + # nemá o co opřít — na tagu mu verzi řekneme rovnou. + if [ "$GITHUB_REF_TYPE" = tag ]; then + export VERSION="$GITHUB_REF_NAME" + fi + ./build.sh - name: Zkouška run: | @@ -74,6 +80,7 @@ jobs: curl -sf -o /dev/null http://127.0.0.1:8321/static/style.css curl -sf -o /dev/null http://127.0.0.1:8321/board test -f /tmp/zkouska/kostky/dice.db + ./dist/kostky --version cat /tmp/server.log - name: Na release diff --git a/.github/workflows/binarky.yml b/.github/workflows/binarky.yml index 33f4b15..9d8d0ef 100644 --- a/.github/workflows/binarky.yml +++ b/.github/workflows/binarky.yml @@ -55,7 +55,13 @@ jobs: run: uv run --with-requirements requirements-dev.txt --no-project pytest -q - name: Binárka - run: ./build.sh + run: | + # Na tagu je verze jistá. Jinde je repozitář naklonovaný mělce + # a build.sh si vystačí s tím, co z gitu vyčte. + if [ "$GITHUB_REF_TYPE" = tag ]; then + export VERSION="$GITHUB_REF_NAME" + fi + ./build.sh - name: Zkouška run: | @@ -75,6 +81,10 @@ jobs: curl -sf "http://127.0.0.1:8123/board" | grep -q "127.0.0.1:8123" test -f zkouska/dice.db + # Verze se musí do binárky opravdu dostat, ne se hlásit jako neznámá. + "${{ matrix.built }}" --version + ! "${{ matrix.built }}" --version | grep -q "neznámá" + echo "Binárka jede." - name: Na release diff --git a/.gitignore b/.gitignore index a9551e5..4e2bc71 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ __pycache__/ dist/ build/ .build/ +verze.txt diff --git a/README.md b/README.md index 6073eab..ec6eacd 100644 --- a/README.md +++ b/README.md @@ -59,12 +59,17 @@ Závislosti si skript nese v hlavičce (PEP 723) a `uv` je obstará sám. Server vypíše všechny adresy, na kterých je dostupný: ``` +Kostky v1.1.0 + Počitadlo je dostupné na: → http://10.186.234.182:8000 místní síť http://100.91.0.24:8000 přes Tailscale http://172.17.0.1:8000 virtuální síť ``` +Kterou verzi máš, řekne i `kostky --version`; visí taky v patičce každé +stránky. Když něco nefunguje, je to první věc, na kterou se zeptám. + Na notebooku otevři `/board`, naskenuj QR kód telefonem a hraj. Tabule nabízí i zbylé adresy — když jedna nefunguje, klikni na jinou a QR kód se přepne na ni. Zastavuje se `Ctrl+C`. @@ -138,6 +143,7 @@ při remíze na prvním místě se hraje dál. | `web.py` | Flask aplikace | | `dice.py` | totéž v terminálu, bez ukládání | | `console.py` | aby čeština prošla i windowsovou konzolí | +| `version.py` | která verze to je — z gitu, nebo z binárky | | `build.sh`, `kostky.spec` | stavba binárky | | `.gitea/workflows/` | testy a linuxová binárka doma | | `.github/workflows/` | binárky pro Windows a macOS | @@ -195,6 +201,11 @@ srovnat. ./build.sh ``` +Verzi si `build.sh` vezme z `git describe`, nebo se dá vnutit přes +`VERSION=v1.2.3 ./build.sh`. Zapíše ji do `verze.txt`, `kostky.spec` ji +přibalí a binárka ji pak umí ohlásit i na cizím počítači, kde žádný git není. +Verzi Pythonu, proti kterému se staví, přebíjí `PYTHON_VERSION`. + Staví se proti samostatnému CPythonu od `uv`, ne proti systémovému. Ten je slinkovaný s glibc 2.17; postavené proti Pythonu z Fedory 44 by to chtělo glibc 2.43 a nešlo by spustit skoro nikde — glibc drží zpětnou kompatibilitu, diff --git a/build.sh b/build.sh index 03b120a..1d7606d 100755 --- a/build.sh +++ b/build.sh @@ -12,11 +12,18 @@ # se do ní nedostane, takže kontejner k tomu není potřeba. set -euo pipefail -VERSION=${VERSION:-3.11} +PYTHON_VERSION=${PYTHON_VERSION:-3.11} BUILD=${BUILD:-.build} -uv python install "$VERSION" -uv venv --quiet --managed-python --python "$VERSION" "$BUILD" +# Verze, kterou binárka ohlásí přes --version. V CI ji na tagu vnutíme +# proměnnou, protože tam je repozitář naklonovaný na jeden commit a git +# describe nemá o co se opřít. +VERSION=${VERSION:-$(git describe --tags --always --dirty 2>/dev/null || echo neznámá)} +printf '%s\n' "$VERSION" > verze.txt +echo "Verze: $VERSION" + +uv python install "$PYTHON_VERSION" +uv venv --quiet --managed-python --python "$PYTHON_VERSION" "$BUILD" # Windows dává spustitelné soubory venvu do Scripts/, zbytek světa do bin/. python="$BUILD/bin/python" diff --git a/dice.py b/dice.py index 8135999..433eb89 100644 --- a/dice.py +++ b/dice.py @@ -8,11 +8,13 @@ Pravidla žijí v `core.Game`, tenhle modul je jen vstup a výstup. Hraje se v paměti, nic se neukládá — na historii je webová aplikace (`web.py`). """ +import sys from os import name, system from tabulate import tabulate import console +import version from core import FINAL_SCORE, Game table_header = ["Hráč", "Skóre"] @@ -130,4 +132,9 @@ def main() -> None: if __name__ == "__main__": console.utf8() + + if {"--version", "-V"} & set(sys.argv[1:]): + print(f"kostky {version.current()}") + sys.exit(0) + main() diff --git a/kostky.spec b/kostky.spec index 68164e8..cffe88e 100644 --- a/kostky.spec +++ b/kostky.spec @@ -5,12 +5,19 @@ Staví se přes `./build.sh`, ne ručně — ta se stará o to, aby výsledek je i na starších systémech, než je ten tvůj. """ +import os import sys +# Verzi zapisuje build.sh těsně před stavbou. Když se pyinstaller pustí +# ručně bez ní, binárka se postaví taky — jen se pak hlásí jako neznámá. +datas = [("templates", "templates"), ("static", "static")] +if os.path.exists("verze.txt"): + datas.append(("verze.txt", ".")) + analysis = Analysis( ["web.py"], pathex=["."], - datas=[("templates", "templates"), ("static", "static")], + datas=datas, noarchive=False, ) diff --git a/static/style.css b/static/style.css index 9284f3e..4331551 100644 --- a/static/style.css +++ b/static/style.css @@ -513,3 +513,11 @@ button { font-size: 0.85em; color: var(--ink); } + +/* Verze v patičce. Má být k nalezení, když ji někdo hledá, a jinak neplést. */ +.verze { + margin: 2rem 0 0.75rem; + text-align: center; + font-size: 0.75rem; + opacity: 0.35; +} diff --git a/templates/base.html b/templates/base.html index 5d8cdb6..a2ef8c5 100644 --- a/templates/base.html +++ b/templates/base.html @@ -8,5 +8,6 @@ {% block body %}{% endblock %} +
kostky {{ verze }}
diff --git a/tests/test_version.py b/tests/test_version.py new file mode 100644 index 0000000..06f33a0 --- /dev/null +++ b/tests/test_version.py @@ -0,0 +1,48 @@ +"""Verze se musí poznat i z binárky, kde žádný git není.""" + +import pytest + +import version + + +@pytest.fixture(autouse=True) +def bez_pameti(): + """`current()` si výsledek pamatuje; mezi testy je potřeba čistý stůl.""" + version.current.cache_clear() + yield + version.current.cache_clear() + + +def test_z_binarky_se_cte_pribaleny_soubor(monkeypatch, tmp_path): + """V binárce leží verze.txt v dočasném adresáři vedle šablon.""" + (tmp_path / version.NAME).write_text("v9.9.9\n", encoding="utf-8") + monkeypatch.setattr(version.sys, "frozen", True, raising=False) + monkeypatch.setattr(version.sys, "_MEIPASS", str(tmp_path), raising=False) + + assert version.current() == "v9.9.9" + + +def test_binarka_se_gitu_nepta(monkeypatch, tmp_path): + """Na cizím počítači žádný repozitář není a ptát se nemá koho.""" + monkeypatch.setattr(version.sys, "frozen", True, raising=False) + monkeypatch.setattr(version.sys, "_MEIPASS", str(tmp_path), raising=False) + monkeypatch.setattr(version, "described", + lambda: pytest.fail("binárka se ptala gitu")) + + assert version.current() == version.UNKNOWN + + +def test_ze_zdrojaku_ma_prednost_git(monkeypatch): + monkeypatch.delattr(version.sys, "frozen", raising=False) + monkeypatch.setattr(version, "described", lambda: "v1.2.3-4-gabcdef") + monkeypatch.setattr(version, "bundled", lambda: "v1.0.0") + + assert version.current() == "v1.2.3-4-gabcdef" + + +def test_bez_gitu_a_bez_souboru_zbyde_neznama(monkeypatch): + monkeypatch.delattr(version.sys, "frozen", raising=False) + monkeypatch.setattr(version, "described", lambda: "") + monkeypatch.setattr(version, "bundled", lambda: "") + + assert version.current() == version.UNKNOWN diff --git a/version.py b/version.py new file mode 100644 index 0000000..4e9715f --- /dev/null +++ b/version.py @@ -0,0 +1,49 @@ +"""Která verze počitadla to vlastně je. + +Venku jsou čtyři binárky pro tři systémy a k tomu zdrojáky. Když někdo +napíše, že mu něco nefunguje, je první otázka vždycky stejná — a bez +`--version` na ni nemá jak odpovědět. +""" + +import subprocess +import sys +from functools import lru_cache +from pathlib import Path + +NAME = "verze.txt" +UNKNOWN = "neznámá" + + +def bundled() -> str: + """Verze vypálená do binárky při stavbě. + + `build.sh` ji zapíše do souboru, `kostky.spec` ji přibalí. V binárce + leží vedle šablon v dočasném adresáři, na který ukazuje `sys._MEIPASS`. + """ + root = Path(getattr(sys, "_MEIPASS", Path(__file__).parent)) + try: + return (root / NAME).read_text(encoding="utf-8").strip() + except OSError: + return "" + + +def described() -> str: + """Verze podle gitu. Ze zdrojáků je to přesnější než cokoli zapsaného.""" + try: + done = subprocess.run( + ["git", "describe", "--tags", "--always", "--dirty"], + cwd=Path(__file__).parent, capture_output=True, text=True, + timeout=5, check=True) + except (OSError, subprocess.SubprocessError): + return "" + + return done.stdout.strip() + + +@lru_cache(maxsize=1) +def current() -> str: + """Verze pro výpis. Ptát se gitu má smysl jen mimo binárku.""" + if getattr(sys, "frozen", False): + return bundled() or UNKNOWN + + return described() or bundled() or UNKNOWN diff --git a/web.py b/web.py index 1dbc687..53767e5 100644 --- a/web.py +++ b/web.py @@ -22,6 +22,7 @@ import console import net import stats import storage +import version from core import FINAL_SCORE, Game, GameOver def bundled(folder: str) -> str: @@ -125,6 +126,12 @@ def qr_svg(data: str) -> Markup: ) +@app.context_processor +def verze(): + """Verze do patičky. Když někdo hlásí chybu, tohle je první otázka.""" + return {"verze": version.current()} + + @app.template_filter("datum") def datum(value: str) -> str: """Z ISO textu udělá 12. 8. 2026.""" @@ -320,8 +327,9 @@ def hall(): games=storage.recent_games(conn)) -if __name__ == "__main__": - console.utf8() +def announce() -> None: + """Vypíše, kde všude je počitadlo k mání, a čím je zrovna spuštěné.""" + print(f"Kostky {version.current()}\n") found = net.addresses() @@ -336,4 +344,13 @@ if __name__ == "__main__": print(f"\nDatabáze: {storage.DB_PATH.resolve()}") + +if __name__ == "__main__": + console.utf8() + + if {"--version", "-V"} & set(sys.argv[1:]): + print(f"kostky {version.current()}") + sys.exit(0) + + announce() app.run(host="0.0.0.0", port=PORT, threaded=True) From 33e8582079e03d8b4e8ee2c1cb4b42bcc82d729a Mon Sep 17 00:00:00 2001 From: gitea-actions Date: Sat, 22 Aug 2026 18:29:00 +0200 Subject: [PATCH 4/8] =?UTF-8?q?Obsluhovat=20to=20waitressem,=20ne=20v?= =?UTF-8?q?=C3=BDvojov=C3=BDm=20serverem=20Flasku?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flask pod pěkně naformátovanou úvodní hlášku vysypal červené varování, že tohle není produkční server. Waitress je čistě pythonní, takže ho PyInstaller zabalí bez řečí, a osm vláken pobere telefony u stolu i tabuli, která se doptává každé dvě vteřiny. Cestou se ukázalo, že hláška se nikdy nedostala do přesměrovaného výstupu: mimo konzoli drží Python výstup v bloku a pouští ho, až program skončí — jenže server neskončí. Zkouška binárky u Gitey si logovala do souboru a pak ho vypisovala, takže celou dobu tiskla prázdno. Řeší to řádkové bufferování v console.utf8(), kam to patří vedle té kódové stránky. --- README.md | 4 ++++ console.py | 13 ++++++++++--- tests/test_console.py | 19 ++++++++++++++++++- web.py | 8 +++++++- 4 files changed, 39 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index ec6eacd..05eb5eb 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,10 @@ schématu ještě nepsala, se poznají podle nuly a orazítkují se. Databázi založenou novější verzí počitadla appka odmítne otevřít, místo aby ji tiše rozbila. +Obsluhuje to `waitress` — čistě pythonní WSGI server, který se zabalí do +binárky stejně snadno jako zbytek. Vestavěný server Flasku by pod úvodní +hlášku vysypal varování, že takhle se to nemá, a měl by pravdu. + Počítá se s domácí sítí — appka nemá přihlašování a kdokoli na stejné WiFi může zapisovat. diff --git a/console.py b/console.py index cf6a35d..3ab13ab 100644 --- a/console.py +++ b/console.py @@ -1,10 +1,10 @@ -"""Aby čeština prošla i tam, kde konzole čeká něco jiného.""" +"""Aby výstup prošel i tam, kde ho nikdo nečeká na konzoli.""" import sys def utf8() -> None: - """Přepne standardní výstup na UTF-8. + """Přepne standardní výstup na UTF-8 a na řádkové bufferování. Windows sahá po historické kódové stránce (cp1252, cp852) všude, kde výstup není konzole — přesměrovaný do souboru, puštěný ze skriptu, ve @@ -14,7 +14,14 @@ def utf8() -> None: Na konzoli i na ostatních systémech je UTF-8 dávno výchozí a tohle je tam prázdná operace. `errors="replace"` je pojistka pro zbylé případy: rozsypaný háček je pořád lepší než spadlý server. + + Řádkové bufferování řeší druhou půlku téhož problému. Mimo konzoli + Python drží výstup v bloku a pouští ho, až se naplní nebo až program + skončí — jenže server neskončí, ten běží. Kdo si ho pustí na pozadí + s výpisem do souboru, našel by tam prázdno; přesně to dělá zkouška + binárky v CI. Pár řádek hlášky za to bufferovat nemá cenu. """ for stream in (sys.stdout, sys.stderr): if hasattr(stream, "reconfigure"): - stream.reconfigure(encoding="utf-8", errors="replace") + stream.reconfigure(encoding="utf-8", errors="replace", + line_buffering=True) diff --git a/tests/test_console.py b/tests/test_console.py index f2fed1b..9c7fed2 100644 --- a/tests/test_console.py +++ b/tests/test_console.py @@ -9,10 +9,12 @@ class Proud: def __init__(self): self.encoding = "cp1252" self.errors = "strict" + self.line_buffering = False - def reconfigure(self, encoding, errors): + def reconfigure(self, encoding, errors, line_buffering=False): self.encoding = encoding self.errors = errors + self.line_buffering = line_buffering def test_vystup_se_prepne_na_utf8(monkeypatch): @@ -24,6 +26,7 @@ def test_vystup_se_prepne_na_utf8(monkeypatch): assert (out.encoding, out.errors) == ("utf-8", "replace") assert (err.encoding, err.errors) == ("utf-8", "replace") + assert out.line_buffering and err.line_buffering def test_cestina_projde_i_pres_cp1252(monkeypatch): @@ -46,3 +49,17 @@ def test_proud_bez_reconfigure_se_prezije(monkeypatch): monkeypatch.setattr(console.sys, "stderr", object()) console.utf8() + + +def test_hlaska_je_v_souboru_hned(tmp_path, monkeypatch): + """Server běží dál, takže na vyprázdnění bufferu při konci se čekat nedá.""" + cesta = tmp_path / "server.log" + with cesta.open("w", encoding="utf-8") as soubor: + monkeypatch.setattr(console.sys, "stdout", soubor) + monkeypatch.setattr(console.sys, "stderr", soubor) + + console.utf8() + print("Počitadlo je dostupné na:", file=console.sys.stdout) + + # Bez zavření souboru — přesně jako u běžícího serveru. + assert "Počitadlo je dostupné" in cesta.read_text(encoding="utf-8") diff --git a/web.py b/web.py index 53767e5..1e96c0c 100644 --- a/web.py +++ b/web.py @@ -17,6 +17,7 @@ import qrcode from flask import (Flask, abort, g, redirect, render_template, request, url_for) from markupsafe import Markup, escape +from waitress import serve import console import net @@ -353,4 +354,9 @@ if __name__ == "__main__": sys.exit(0) announce() - app.run(host="0.0.0.0", port=PORT, threaded=True) + + # Ne vestavěný server Flasku: ten pod úvodní hlášku vysype červené + # varování, že takhle se to nemá, a má pravdu. Waitress je čistě + # pythonní, takže se zabalí do binárky bez řečí, a osm vláken pobere + # telefony u stolu i tabuli, která se doptává každé dvě vteřiny. + serve(app, host="0.0.0.0", port=PORT, threads=8) From 1a2053ff661e724d166ba8912d70576d3872b5ae Mon Sep 17 00:00:00 2001 From: gitea-actions Date: Sat, 22 Aug 2026 18:29:29 +0200 Subject: [PATCH 5/8] =?UTF-8?q?P=C5=99idat=20linuxovou=20bin=C3=A1rku=20pr?= =?UTF-8?q?o=20ARM64?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GitHub dává veřejným repozitářům běhouny ubuntu-24.04-arm zadarmo, takže je to jeden řádek do matice. Hodí se pro Raspberry Pi nebo jinou malou pořád zapnutou krabičku, což je rozumnější místo, kde tohle nechat běžet, než notebook, který se každý večer zapíná. x86-64 zůstává doma. Tam jde stavět proti staré glibc a když GitHub vypadne, release má pořád tu platformu, na které to reálně poběží. ARM je výjimka prostě proto, že doma na něm není na čem postavit. --- .github/workflows/binarky.yml | 11 ++++++++--- README.md | 13 ++++++++----- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/.github/workflows/binarky.yml b/.github/workflows/binarky.yml index 9d8d0ef..32ac091 100644 --- a/.github/workflows/binarky.yml +++ b/.github/workflows/binarky.yml @@ -1,4 +1,4 @@ -# Binárky pro Windows a macOS. +# Binárky pro Windows, macOS a Linux na ARM. # # PyInstaller neumí křížovou kompilaci: binárku pro každý systém musí postavit # ten systém. Kupovat kvůli tomu Mac nebo krmit Windows VM by bylo přehnané, @@ -8,8 +8,10 @@ # z Gitey zrcadlí a hotové soubory se posílají zpátky na tamní release přes # Gitea API. Jedna stránka s releasy, ta původní. # -# Linux se tu schválně nestaví — o ten se stará vlastní runner -# (.gitea/workflows/binarka.yml) a dělá to líp, proti glibc 2.17. +# Linux na x86-64 se tu schválně nestaví — o ten se stará vlastní runner +# (.gitea/workflows/binarka.yml). Když GitHub vypadne, release má pořád tu +# platformu, na které to reálně poběží. ARM doma není na čem postavit, takže +# ten jediný linuxový sem patří. name: binárky on: @@ -37,6 +39,9 @@ jobs: - os: macos-latest # Apple Silicon asset: kostky-macos-arm64 built: dist/kostky + - os: ubuntu-24.04-arm # pro Raspberry Pi a spol. + asset: kostky-linux-arm64 + built: dist/kostky runs-on: ${{ matrix.os }} defaults: diff --git a/README.md b/README.md index 05eb5eb..2875a5d 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ Stáhni binárku pro svůj systém z | Systém | Soubor | |---|---| | Linux (x86-64) | `kostky-linux-x86_64` | +| Linux (ARM64) | `kostky-linux-arm64` | | macOS (Apple Silicon) | `kostky-macos-arm64` | | macOS (Intel) | `kostky-macos-x86_64` | | Windows (x86-64) | `kostky-windows-x86_64.exe` | @@ -229,8 +230,8 @@ Otagovaný commit spustí stavbu na obou stranách: | Kde | Co staví | Workflow | |---|---|---| -| vlastní runner u Gitey | Linux | `.gitea/workflows/binarka.yml` | -| GitHub Actions | Windows, macOS ×2 | `.github/workflows/binarky.yml` | +| vlastní runner u Gitey | Linux x86-64 | `.gitea/workflows/binarka.yml` | +| GitHub Actions | Windows, macOS ×2, Linux ARM64 | `.github/workflows/binarky.yml` | ```bash git tag v1.0.0 && git push origin v1.0.0 @@ -239,9 +240,11 @@ git tag v1.0.0 && git push origin v1.0.0 GitHub je tu jen půjčená dílna. Repozitář se tam z Gitey zrcadlí ([mirror](https://github.com/david-spacil/dice-counter)), postavené soubory se posílají zpátky na zdejší release přes Gitea API a projekt má pořád jednu -stránku s releasy — tuhle. Linux se na GitHubu schválně nestaví; doma to jde -proti staré glibc, a když GitHub vypadne, release má aspoň tu platformu, -na které to reálně poběží. +stránku s releasy — tuhle. Linux na x86-64 se na GitHubu schválně nestaví; +doma to jde proti staré glibc, a když GitHub vypadne, release má aspoň tu +platformu, na které to reálně poběží. ARM64 je výjimka — doma na něm není +na čem stavět, a hodí se pro Raspberry Pi nebo jinou malou pořád zapnutou +krabičku. Každá stavba nejdřív projede testy a zkusí hotovou binárku nastartovat, než ji kamkoli pověsí. Vedle každé visí i `.sha256`, takže se stažený soubor dá From a655bfe6f3d906c4b889ae465f8e29e3bc92b941 Mon Sep 17 00:00:00 2001 From: gitea-actions Date: Sat, 22 Aug 2026 18:30:49 +0200 Subject: [PATCH 6/8] =?UTF-8?q?Sta=C5=BEen=C3=AD=20datab=C3=A1ze=20ze=20s?= =?UTF-8?q?=C3=ADn=C4=9B=20sl=C3=A1vy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dokud se hrálo ze zdrojáků, ležel dice.db v pracovním adresáři a záloha byla otázka zkopírování. Z binárky se schovává do systémového datového adresáře, který na Windows běžný člověk nenajde — takže se teď dá stáhnout přes prohlížeč. VACUUM INTO udělá konzistentní kopii i uprostřed zápisu, na rozdíl od prostého přečtení souboru. Kopie se přečte do paměti a hned zahodí, ať nezůstane ležet v dočasném adresáři, když si ji někdo nestáhne celou. --- README.md | 4 +++- templates/stats.html | 3 ++- tests/test_web.py | 33 +++++++++++++++++++++++++++++++++ web.py | 36 +++++++++++++++++++++++++++++++++++- 4 files changed, 73 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 2875a5d..33c194b 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,9 @@ napevno domovský adresář. Ze zdrojáků zůstává relativní, ať se dá mí sad vedle sebe. Zálohovat i stěhovat jde prostým zkopírováním souboru; `DICE_DB` si ho -najde kdekoli. +najde kdekoli. Kdo ho hledat nechce — a z binárky na Windows je zalezlý — +si ho stáhne ze síně slávy odkazem **Stáhnout databázi** (`/export`). Kopie +se dělá přes `VACUUM INTO`, takže je použitelná i uprostřed rozehrané hry. Schéma má verzi uloženou v `PRAGMA user_version` a `storage.migrate()` si databázi při každém připojení dorovná. Starší soubory z verzí, kdy se verze diff --git a/templates/stats.html b/templates/stats.html index 0f748e2..3e12ada 100644 --- a/templates/stats.html +++ b/templates/stats.html @@ -8,7 +8,8 @@

Počitadlo

Síň slávy

-

Nová hra

+

Nová hra + · Stáhnout databázi

{% if records.best_turn %} diff --git a/tests/test_web.py b/tests/test_web.py index e6d28a7..6abb950 100644 --- a/tests/test_web.py +++ b/tests/test_web.py @@ -326,3 +326,36 @@ def test_qr_obdelniky_kryji_stejne_moduly(): assert painted == expected assert svg.count("`), notebook ukazuje zápisník (`/board`). Server-rendered HTML, formuláře přes POST/redirect/GET, žádný build step. """ +import io import os +import shutil import socket import sys +import tempfile +from datetime import date from pathlib import Path import qrcode from flask import (Flask, abort, g, redirect, render_template, request, - url_for) + send_file, url_for) from markupsafe import Markup, escape from waitress import serve @@ -319,6 +323,36 @@ def board_panel(game_id: int): return render_template("_panel.html", **view(game, game_id, row)) +@app.route("/export") +def export(): + """Databáze ke stažení. + + Dokud se hrálo ze zdrojáků, ležel `dice.db` v pracovním adresáři a záloha + byla otázka zkopírování. Z binárky se schovává do systémového datového + adresáře, který na Windows běžný člověk nenajde — tak ať se dá stáhnout + přes prohlížeč. + + `VACUUM INTO` udělá konzistentní kopii i uprostřed zápisu, na rozdíl od + prostého přečtení souboru. Výsledek si přečteme do paměti a kopii hned + zahodíme: databáze počitadla se počítá v kilobajtech a takhle nezůstane + ležet v dočasném adresáři, když si ji někdo nestáhne celou. + """ + conn = db() + conn.commit() # VACUUM nesmí běžet v transakci + + docasny = tempfile.mkdtemp() + try: + kopie = Path(docasny) / "dice.db" + conn.execute("VACUUM INTO ?", (str(kopie),)) + data = kopie.read_bytes() + finally: + shutil.rmtree(docasny, ignore_errors=True) + + return send_file(io.BytesIO(data), mimetype="application/vnd.sqlite3", + as_attachment=True, + download_name=f"kostky-{date.today().isoformat()}.db") + + @app.route("/stats") def hall(): conn = db() From 023b743c765c36d281b5e7f4b1a3137c78a863bd Mon Sep 17 00:00:00 2001 From: gitea-actions Date: Sat, 22 Aug 2026 18:32:16 +0200 Subject: [PATCH 7/8] =?UTF-8?q?Stav=C4=9Bt=20na=C4=8Disto,=20a=C5=A5=20pro?= =?UTF-8?q?jde=20i=20druh=C3=A1=20stavba=20za=20sebou?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit uv nad existujícím prostředím skončí chybou a chce --clear. Domácí runner si pracovní adresář mezi běhy drží a git clean -fd na .build nesáhne, protože je v .gitignore — takže druhá stavba ve stejném adresáři spadla dřív, než se stihlo cokoli postavit. --- build.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/build.sh b/build.sh index 1d7606d..e41ba97 100755 --- a/build.sh +++ b/build.sh @@ -23,7 +23,9 @@ printf '%s\n' "$VERSION" > verze.txt echo "Verze: $VERSION" uv python install "$PYTHON_VERSION" -uv venv --quiet --managed-python --python "$PYTHON_VERSION" "$BUILD" +# --clear, protože runner si pracovní adresář mezi běhy drží a uv nad +# existujícím prostředím jinak skončí chybou. Stavíme načisto. +uv venv --quiet --clear --managed-python --python "$PYTHON_VERSION" "$BUILD" # Windows dává spustitelné soubory venvu do Scripts/, zbytek světa do bin/. python="$BUILD/bin/python" From 3c3805362fc80f2f0232fd23483c19af560c0979 Mon Sep 17 00:00:00 2001 From: gitea-actions Date: Sat, 22 Aug 2026 18:32:27 +0200 Subject: [PATCH 8/8] =?UTF-8?q?Srovnat=20po=C4=8Det=20test=C5=AF=20v=20REA?= =?UTF-8?q?DME?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 33c194b..a9b4069 100644 --- a/README.md +++ b/README.md @@ -267,5 +267,5 @@ uv run --with-requirements requirements.txt --no-project pytest Nebo z připraveného prostředí prostě `pytest`. Nad každým pull requestem a nad `main` běží `.gitea/workflows/testy.yml` — -102 testů na Pythonu 3.11, 3.12, 3.13 i 3.14, a k tomu skriptovaná partie +118 testů na Pythonu 3.11, 3.12, 3.13 i 3.14, a k tomu skriptovaná partie v terminálové verzi, na kterou pytest nesahá.