Author SHA1 Message Date
gitea-actionsandClaude Opus 5 85b6618952 binárka ke stažení a jasné místo pro databázi
Stáhni a spusť: jednosouborová binárka pro Linux, uvnitř Python, Flask,
šablony i styly. Staví se ./build.sh v kontejneru na AlmaLinux 8, protože
binárka slinkovaná proti glibc z Fedory 44 by nešla spustit nikde se
starším systémem.

Databáze tím dostává dvě různá místa. Ze zdrojáků zůstává dice.db
v pracovním adresáři, ať se dá mít víc sad vedle sebe. Z binárky jde do
~/.local/share/kostky/ — binárka se rozbaluje do dočasného adresáře
a pouští se odkudkoli, takže relativní cesta by databázi rozsypala po
disku. DICE_DB přebije obojí.

Server teď cestu k databázi vypisuje při startu, ať se na ni nemusí ptát.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 17:12:25 +02:00
gitea-actionsandClaude Opus 5 374e4209c5 spuštění jedním příkazem: uv run web.py
Trojice venv/pip/python byla zbytečná ceremonie pro appku, která se pouští
jednou za herní večer.

- web.py i dice.py si nesou závislosti v hlavičce podle PEP 723, takže
  je uv obstará sám a není co instalovat ani aktivovat
- pytest.ini staví kořen repa na pythonpath, takže testy jdou spustit
  i mimo připravené prostředí
- README nabízí uv jako hlavní cestu, venv a systémové balíčky jako
  záložní

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 21:00:29 +02:00
9 changed files with 215 additions and 10 deletions

No files matched your search

+2
View File
@@ -2,3 +2,5 @@ __pycache__/
.venv/ .venv/
*.db *.db
.pytest_cache/ .pytest_cache/
dist/
build/
+73 -7
View File
@@ -8,12 +8,26 @@ ten samý Adam jako z minulého měsíce. Žádné účty, žádná hesla.
## Spuštění ## Spuštění
Stáhni binárku z [releases](https://gitea.spacilovi.eu/david-spacil/dice-counter/releases)
a spusť ji:
```bash ```bash
python -m venv .venv chmod +x kostky
.venv/bin/pip install -r requirements.txt ./kostky
.venv/bin/python web.py
``` ```
Nic se neinstaluje — Python, Flask i šablony jsou uvnitř. Zatím jen pro Linux
na x86-64; postavené proti glibc 2.28, takže jede na všem od RHEL 8,
Debianu 10 a Ubuntu 20.04 výš.
Ze zdrojáků to je stejně krátké:
```bash
uv run web.py
```
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ý: Server vypíše všechny adresy, na kterých je dostupný:
``` ```
@@ -24,14 +38,33 @@ Počitadlo je dostupné na:
``` ```
Na notebooku otevři `/board`, naskenuj QR kód telefonem a hraj. Tabule Na notebooku otevři `/board`, naskenuj QR kód telefonem a hraj. Tabule
zobrazuje i zbylé adresy — když jedna nefunguje, zkus další. zobrazuje i zbylé adresy — když jedna nefunguje, zkus další. Zastavuje se
`Ctrl+C`.
| Proměnná | K čemu je | Výchozí | | Proměnná | K čemu je | Výchozí |
|---|---|---| |---|---|---|
| `DICE_DB` | soubor s databází | `dice.db` | | `DICE_DB` | soubor s databází | podle způsobu spuštění, viz níž |
| `DICE_PORT` | port | `8000` | | `DICE_PORT` | port | `8000` |
| `DICE_HOST` | pevná adresa; vypne hledání | hledá se za běhu | | `DICE_HOST` | pevná adresa; vypne hledání | hledá se za běhu |
## Kde jsou data
Jeden soubor SQLite. Založí se sám při prvním načtení stránky, cestu k němu
server vypíše při startu.
| Spuštěno | Databáze |
|---|---|
| binárkou | `~/.local/share/kostky/dice.db` |
| ze zdrojáků | `dice.db` v pracovním adresáři |
Binárka se při každém spuštění rozbaluje do dočasného adresáře a pouští se
odkudkoli, takže relativní cesta by databázi rozsypala po disku — proto
napevno domovský adresář. Ze zdrojáků zůstává relativní, ať se dá mít víc
sad vedle sebe.
Zálohovat i stěhovat jde prostým zkopírováním souboru; `DICE_DB` si ho
najde kdekoli.
Počítá se s domácí sítí — appka nemá přihlašování a kdokoli na stejné WiFi Počítá se s domácí sítí — appka nemá přihlašování a kdokoli na stejné WiFi
může zapisovat. může zapisovat.
@@ -68,15 +101,48 @@ při remíze na prvním místě se hraje dál.
| `stats.py` | síň slávy a kariérní statistiky | | `stats.py` | síň slávy a kariérní statistiky |
| `web.py` | Flask aplikace | | `web.py` | Flask aplikace |
| `dice.py` | totéž v terminálu, bez ukládání | | `dice.py` | totéž v terminálu, bez ukládání |
| `build.sh`, `kostky.spec` | stavba binárky |
Terminálová verze zůstává funkční jako záloha: Terminálová verze zůstává funkční jako záloha:
```bash ```bash
.venv/bin/python dice.py uv run dice.py
``` ```
## Bez uv
Když `uv` po ruce není, jde to postaru:
```bash
python -m venv .venv
.venv/bin/pip install -r requirements.txt
.venv/bin/python web.py
```
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
python3 web.py
```
## Vlastní binárka
```bash
./build.sh
```
Staví se v kontejneru (podman nebo docker) na AlmaLinux 8, ne na hostiteli.
Binárka slinkovaná proti glibc z Fedory 44 by nešla spustit nikde se starším
systémem — glibc drží zpětnou kompatibilitu, ne dopřednou. Výsledek je
v `dist/kostky`.
Terminálová verze součástí binárky není; ta se pouští ze zdrojáků.
## Testy ## Testy
```bash ```bash
.venv/bin/python -m pytest tests/ -v uv run --with-requirements requirements.txt --no-project pytest
``` ```
Nebo z připraveného prostředí prostě `pytest`.
Executable
+30
View File
@@ -0,0 +1,30 @@
#!/usr/bin/env bash
# Postaví jednosouborovou binárku pro Linux do dist/kostky.
#
# Staví se v kontejneru s glibc 2.28 (AlmaLinux 8), ne na hostiteli. Binárka
# slinkovaná proti glibc z Fedory 44 by nešla spustit nikde se starším
# systémem — glibc drží zpětnou kompatibilitu, ne dopřednou. Postavené na
# staré glibc poběží i na nové.
set -euo pipefail
IMAGE=${IMAGE:-docker.io/library/almalinux:8}
PYTHON=${PYTHON:-python3.11}
ENGINE=${ENGINE:-$(command -v podman || command -v docker)}
# Rootless podman mapuje roota v kontejneru na tebe, docker ne — tam by
# dist/ zůstalo cizí.
FIXUP=":"
[[ "$ENGINE" == *docker ]] && FIXUP="chown -R $(id -u):$(id -g) dist"
"$ENGINE" run --rm -v "$PWD:/src:z" -w /src "$IMAGE" bash -euc "
dnf install -y -q python3.11 binutils
$PYTHON -m venv /tmp/build
/tmp/build/bin/pip install --quiet --upgrade pip
/tmp/build/bin/pip install --quiet flask qrcode pyinstaller
/tmp/build/bin/pyinstaller --clean --noconfirm \
--distpath dist --workpath /tmp/work kostky.spec
$FIXUP
"
echo
ls -lh dist/kostky
+4
View File
@@ -1,3 +1,7 @@
# /// script
# requires-python = ">=3.11"
# dependencies = ["tabulate"]
# ///
"""Počitadlo skóre v terminálu. """Počitadlo skóre v terminálu.
Pravidla žijí v `core.Game`, tenhle modul je jen vstup a výstup. Hraje se Pravidla žijí v `core.Game`, tenhle modul je jen vstup a výstup. Hraje se
+26
View File
@@ -0,0 +1,26 @@
# -*- mode: python ; coding: utf-8 -*-
"""Jedna binárka se vším: Python, Flask, šablony i styly.
Staví se přes `./build.sh`, ne ručně — ta se stará o to, aby výsledek jel
i na starších systémech, než je ten tvůj.
"""
analysis = Analysis(
["web.py"],
pathex=["."],
datas=[("templates", "templates"), ("static", "static")],
noarchive=False,
)
pyz = PYZ(analysis.pure)
exe = EXE(
pyz,
analysis.scripts,
analysis.binaries,
analysis.datas,
name="kostky",
console=True,
strip=True,
upx=False,
)
+4
View File
@@ -0,0 +1,4 @@
[pytest]
# Testy sahají na moduly v kořeni repa, ne na nainstalovaný balíček.
pythonpath = .
testpaths = tests
+22 -2
View File
@@ -10,12 +10,30 @@ Součty se nikde neukládají; stav hry se rekonstruuje načtením tahů do
import os import os
import re import re
import sqlite3 import sqlite3
import sys
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from core import FINAL_SCORE, Game, Turn from core import FINAL_SCORE, Game, Turn
DB_PATH = Path(os.environ.get("DICE_DB", "dice.db")) def default_db() -> Path:
"""Kam se ukládá databáze.
Ze zdrojáků do pracovního adresáře — dá se tak mít víc rozehraných
sad vedle sebe a je vidět, s čím se pracuje. Z binárky do datového
adresáře podle XDG: binárka se spouští odkudkoli a rozbaluje se do
dočasného adresáře, takže relativní cesta by databázi rozsypala po disku.
"""
chosen = os.environ.get("DICE_DB")
if chosen:
return Path(chosen)
if getattr(sys, "frozen", False):
share = os.environ.get("XDG_DATA_HOME") or Path.home() / ".local" / "share"
return Path(share) / "kostky" / "dice.db"
return Path("dice.db")
DB_PATH = default_db()
STATUS_RUNNING = "probíhá" STATUS_RUNNING = "probíhá"
STATUS_FINISHED = "dohráno" STATUS_FINISHED = "dohráno"
@@ -60,7 +78,9 @@ CREATE INDEX IF NOT EXISTS turns_game ON turns(game_id, id);
def connect(path: Path | str | None = None) -> sqlite3.Connection: def connect(path: Path | str | None = None) -> sqlite3.Connection:
conn = sqlite3.connect(path or DB_PATH) target = Path(path or DB_PATH)
target.parent.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(target)
conn.row_factory = sqlite3.Row conn.row_factory = sqlite3.Row
conn.execute("PRAGMA foreign_keys = ON") conn.execute("PRAGMA foreign_keys = ON")
conn.executescript(SCHEMA) conn.executescript(SCHEMA)
+33
View File
@@ -1,3 +1,5 @@
from pathlib import Path
import pytest import pytest
import storage import storage
@@ -132,3 +134,34 @@ def test_hrac_nesmi_sedet_u_stolu_dvakrat(conn):
def test_prazdne_jmeno(conn): def test_prazdne_jmeno(conn):
with pytest.raises(ValueError): with pytest.raises(ValueError):
storage.get_or_create_player(conn, " ") storage.get_or_create_player(conn, " ")
def test_umisteni_databaze_ze_zdrojaku(monkeypatch):
"""Ze zdrojáků se ukládá do pracovního adresáře."""
monkeypatch.delenv("DICE_DB", raising=False)
monkeypatch.delattr(storage.sys, "frozen", raising=False)
assert storage.default_db() == Path("dice.db")
def test_umisteni_databaze_z_binarky(monkeypatch, tmp_path):
"""Binárka se rozbaluje do dočasného adresáře, databáze tam nesmí."""
monkeypatch.delenv("DICE_DB", raising=False)
monkeypatch.setattr(storage.sys, "frozen", True, raising=False)
monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path))
assert storage.default_db() == tmp_path / "kostky" / "dice.db"
def test_dice_db_prebije_oboje(monkeypatch, tmp_path):
monkeypatch.setattr(storage.sys, "frozen", True, raising=False)
monkeypatch.setenv("DICE_DB", str(tmp_path / "jinde.db"))
assert storage.default_db() == tmp_path / "jinde.db"
def test_connect_zalozi_chybejici_adresar(tmp_path):
conn = storage.connect(tmp_path / "novy" / "adresar" / "dice.db")
conn.close()
assert (tmp_path / "novy" / "adresar" / "dice.db").exists()
+21 -1
View File
@@ -1,3 +1,7 @@
# /// script
# requires-python = ">=3.11"
# dependencies = ["flask", "qrcode"]
# ///
"""Webové počitadlo pro domácí síť. """Webové počitadlo pro domácí síť.
Telefon zapisuje skóre (`/game/<id>`), notebook ukazuje zápisník (`/board`). Telefon zapisuje skóre (`/game/<id>`), notebook ukazuje zápisník (`/board`).
@@ -6,6 +10,8 @@ Server-rendered HTML, formuláře přes POST/redirect/GET, žádný build step.
import os import os
import socket import socket
import sys
from pathlib import Path
import qrcode import qrcode
from flask import (Flask, abort, g, redirect, render_template, request, from flask import (Flask, abort, g, redirect, render_template, request,
@@ -17,7 +23,19 @@ import stats
import storage import storage
from core import FINAL_SCORE, Game, GameOver from core import FINAL_SCORE, Game, GameOver
app = Flask(__name__) def bundled(folder: str) -> str:
"""Cesta k šablonám a stylům.
Ze zdrojáků leží vedle tohoto souboru. V binárce je PyInstaller rozbalí
do dočasného adresáře, na který ukazuje `sys._MEIPASS`.
"""
root = Path(getattr(sys, "_MEIPASS", Path(__file__).parent))
return str(root / folder)
app = Flask(__name__,
template_folder=bundled("templates"),
static_folder=bundled("static"))
PORT = int(os.environ.get("DICE_PORT", 8000)) PORT = int(os.environ.get("DICE_PORT", 8000))
@@ -294,4 +312,6 @@ if __name__ == "__main__":
else: else:
print("Adresu se nepodařilo zjistit. Spusť server s DICE_HOST=<adresa>.") print("Adresu se nepodařilo zjistit. Spusť server s DICE_HOST=<adresa>.")
print(f"\nDatabáze: {storage.DB_PATH.resolve()}")
app.run(host="0.0.0.0", port=PORT, threaded=True) app.run(host="0.0.0.0", port=PORT, threaded=True)