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>
This commit is contained in:
1 parent
f33104b5c2
commit
fcfa0e489b
7 files changed
+180
-7
No files matched your search
@@ -2,3 +2,5 @@ __pycache__/
|
|||||||
.venv/
|
.venv/
|
||||||
*.db
|
*.db
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
@@ -8,12 +8,25 @@ 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
|
||||||
|
chmod +x kostky
|
||||||
|
./kostky
|
||||||
|
```
|
||||||
|
|
||||||
|
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
|
```bash
|
||||||
uv run web.py
|
uv run web.py
|
||||||
```
|
```
|
||||||
|
|
||||||
To je celé. Závislosti si skript nese v hlavičce (PEP 723) a `uv` je obstará
|
Závislosti si skript nese v hlavičce (PEP 723) a `uv` je obstará sám.
|
||||||
sám — není co instalovat ani aktivovat.
|
|
||||||
|
|
||||||
Server vypíše všechny adresy, na kterých je dostupný:
|
Server vypíše všechny adresy, na kterých je dostupný:
|
||||||
|
|
||||||
@@ -25,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.
|
||||||
|
|
||||||
@@ -69,6 +101,7 @@ 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:
|
||||||
|
|
||||||
@@ -93,6 +126,19 @@ sudo dnf install python3-flask python3-qrcode python3-tabulate
|
|||||||
python3 web.py
|
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
|
||||||
|
|||||||
@@ -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
|
||||||
+26
@@ -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,
|
||||||
|
)
|
||||||
+22
-2
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -10,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,
|
||||||
@@ -21,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))
|
||||||
|
|
||||||
@@ -298,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)
|
||||||
Reference in new issue
Block a user