Rename the app from scopa to tavolo
CI / Build and push docker image (push) Successful in 3m12s

The platform now hosts multiple card games, with scopone scientifico as
the first one. Rename the brand wherever it is not a game rule:

- move the Python package to server/src/tavolo and update imports
- rename the Postgres database/user, OIDC issuer path, client id and
  Redis key prefixes to tavolo (clean break: existing pgdata volumes and
  live games are not migrated)
- rename the Cargo package to tavolo-web and set the page title to Tavolo
- update docs and the Docker image path to woggioni/tavolo

The scopa game term (clearing the table) in the engine, state and web UI
is intentionally left untouched.
This commit is contained in:
2026-09-16 21:46:06 +08:00
parent 89cf0a4c96
commit 6932a3272c
46 changed files with 141 additions and 140 deletions
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
id: meta
uses: docker/metadata-action@v5
with:
images: gitea.woggioni.net/woggioni/scopa
images: gitea.woggioni.net/woggioni/tavolo
flavor: latest=false
tags: |
type=match,pattern=release/(.*),group=1
+5 -5
View File
@@ -1,7 +1,7 @@
# scopa
# tavolo
Multiplayer **scopone scientifico** — the four-player, fixed-partnership
Italian card game — as a web application:
A platform for multiplayer card games. The first game is **scopone
scientifico** — the four-player, fixed-partnership Italian card game:
- **`server/`** — backend: Python + [kaya](https://github.com/woggioni/kaya)
framework, OIDC login, live games in Redis, match statistics in Postgres.
@@ -22,7 +22,7 @@ Postgres, Redis, a mock OIDC provider (test users `alice`, `bob`, `carol`,
frontend and API — listens on `http://127.0.0.1:8080`.
Because both the browser and the app talk to the OIDC issuer at
`http://mockoauth:8180/scopa`, add a host entry once:
`http://mockoauth:8180/tavolo`, add a host entry once:
```sh
echo "127.0.0.1 mockoauth" | sudo tee -a /etc/hosts
@@ -71,7 +71,7 @@ run the backend with:
```sh
OIDC_POST_LOGIN_REDIRECT=http://localhost:8000/ \
OIDC_POST_LOGOUT_REDIRECT=http://localhost:8000/ \
.venv/bin/granian --host 127.0.0.1 --port 8080 scopa.app:app
.venv/bin/granian --host 127.0.0.1 --port 8080 tavolo.app:app
```
Card images are committed under `web/assets/cards/`; `web/fetch-cards.sh`
+11 -11
View File
@@ -2,15 +2,15 @@ services:
postgres:
image: postgres:18-alpine
environment:
POSTGRES_DB: scopa
POSTGRES_USER: scopa
POSTGRES_PASSWORD: scopa
POSTGRES_DB: tavolo
POSTGRES_USER: tavolo
POSTGRES_PASSWORD: tavolo
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U scopa"]
test: ["CMD-SHELL", "pg_isready -U tavolo"]
interval: 5s
timeout: 3s
retries: 10
@@ -21,7 +21,7 @@ services:
# The mock does not validate clients, so any client id/secret works.
#
# It listens on 8180 both inside and outside the network so the OIDC
# issuer URL (http://mockoauth:8180/scopa) is identical for
# issuer URL (http://mockoauth:8180/tavolo) is identical for
# container-to-container calls and for browser redirects (via the
# /etc/hosts entry documented in the README).
mockoauth:
@@ -43,7 +43,7 @@ services:
- mockoauth
entrypoint: >
/bin/sh -c "
until curl -sf http://mockoauth:8180/scopa/.well-known/openid-configuration > /dev/null; do
until curl -sf http://mockoauth:8180/tavolo/.well-known/openid-configuration > /dev/null; do
echo 'waiting for mockoauth...'; sleep 2;
done
"
@@ -71,12 +71,12 @@ services:
working_dir: /app
command: ["aerich", "upgrade"]
environment:
DATABASE_URL: postgres://scopa:scopa@postgres:5432/scopa
DATABASE_URL: postgres://tavolo:tavolo@postgres:5432/tavolo
depends_on:
postgres:
condition: service_healthy
scopa:
tavolo:
build:
context: .
dockerfile: server/Dockerfile
@@ -90,13 +90,13 @@ services:
redis:
condition: service_healthy
environment:
DATABASE_URL: postgres://scopa:scopa@postgres:5432/scopa
DATABASE_URL: postgres://tavolo:tavolo@postgres:5432/tavolo
# By default the app and browsers reach the mock IdP under the same
# name (see README /etc/hosts note); override OIDC_ISSUER and
# OIDC_REDIRECT_URI to use a real provider or a different host port.
OIDC_ISSUER: ${OIDC_ISSUER:-http://mockoauth:8180/scopa}
OIDC_ISSUER: ${OIDC_ISSUER:-http://mockoauth:8180/tavolo}
# The mock OIDC server does not validate clients: any id/secret works.
OIDC_CLIENT_ID: scopa
OIDC_CLIENT_ID: tavolo
OIDC_CLIENT_SECRET: ${OIDC_CLIENT_SECRET:-dev-secret}
OIDC_REDIRECT_URI: ${OIDC_REDIRECT_URI:-http://localhost:8080/auth/callback}
REDIS_URL: redis://redis:6379/0
+6 -6
View File
@@ -1,15 +1,15 @@
# Postgres
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_DB=scopa
POSTGRES_USER=scopa
POSTGRES_PASSWORD=scopa
DATABASE_URL=postgres://scopa:scopa@localhost:5432/scopa
POSTGRES_DB=tavolo
POSTGRES_USER=tavolo
POSTGRES_PASSWORD=tavolo
DATABASE_URL=postgres://tavolo:tavolo@localhost:5432/tavolo
# OIDC (mock-oauth2-server in dev; it does not validate clients, so any
# client id/secret works. For a real IdP like Keycloak, use its values here.)
OIDC_ISSUER=http://localhost:8180/scopa
OIDC_CLIENT_ID=scopa
OIDC_ISSUER=http://localhost:8180/tavolo
OIDC_CLIENT_ID=tavolo
OIDC_CLIENT_SECRET=dev-secret
OIDC_REDIRECT_URI=http://localhost:8080/auth/callback
+2 -2
View File
@@ -1,5 +1,5 @@
# syntax=docker/dockerfile:1
# Multi-stage build for the scopa stack (kaya backend + Sycamore/WASM
# Multi-stage build for the tavolo stack (kaya backend + Sycamore/WASM
# frontend). The Docker build context is the REPOSITORY ROOT (see
# docker-compose.yml) so this single image assembles both parts:
#
@@ -86,4 +86,4 @@ EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s \
CMD wget -q -O- http://127.0.0.1:8080/api/health || exit 1
CMD ["granian", "scopa.app:app"]
CMD ["granian", "tavolo.app:app"]
+17 -16
View File
@@ -1,8 +1,9 @@
# scopa
# tavolo
A multiplayer backend for **scopone scientifico** (the four-player,
fixed-partnership variant of the classic Italian card game), built on the
[kaya](../kaya) framework.
The backend for a multiplayer card-game platform, built on the
[kaya](../kaya) framework. The first game implemented is **scopone
scientifico**, the four-player, fixed-partnership variant of the classic
Italian card game.
Players authenticate with the configured **OIDC** provider. Live game state
is kept in **Redis** (with real-time play over WebSocket), and completed
@@ -24,7 +25,7 @@ serves the Sycamore/WASM frontend (built from `../web/` by the Docker
image), so the UI is available at that address.
Because the browser and the app both talk to the OIDC issuer at
`http://mockoauth:8180/scopa`, add a host entry once:
`http://mockoauth:8180/tavolo`, add a host entry once:
```sh
echo "127.0.0.1 mockoauth" | sudo tee -a /etc/hosts
@@ -43,10 +44,10 @@ All configuration comes from environment variables (see `.env.example`):
| Variable | Default | Description |
|---|---|---|
| `DATABASE_URL` | `postgres://scopa:scopa@localhost:5432/scopa` | Postgres DSN for match statistics |
| `DATABASE_URL` | `postgres://tavolo:tavolo@localhost:5432/tavolo` | Postgres DSN for match statistics |
| `REDIS_URL` | unset | Redis DSN for sessions + live games. Unset falls back to in-memory stores |
| `OIDC_ISSUER` | `http://localhost:8180/scopa` | OIDC issuer URL |
| `OIDC_CLIENT_ID` | `scopa` | OIDC client id |
| `OIDC_ISSUER` | `http://localhost:8180/tavolo` | OIDC issuer URL |
| `OIDC_CLIENT_ID` | `tavolo` | OIDC client id |
| `OIDC_CLIENT_SECRET` | unset | OIDC client secret |
| `OIDC_REDIRECT_URI` | `http://localhost:8080/auth/callback` | Login callback URL |
| `GAME_TTL_SECONDS` | `86400` | Sliding TTL of a live game in Redis |
@@ -58,13 +59,13 @@ All configuration comes from environment variables (see `.env.example`):
### Redis (live games)
- `scopa:game:<uuid>` — the whole match as JSON: players (seat 0/2 = team A,
- `tavolo:game:<uuid>` — the whole match as JSON: players (seat 0/2 = team A,
1/3 = team B), hands, table, captured piles, scope, current turn, dealer,
scores, phase (`lobby``playing``finished`). Sliding TTL
(`GAME_TTL_SECONDS`).
- `scopa:code:<JOINCODE>` — the 6-character join code → game id index.
- `scopa:game:<uuid>:lock` — a short-lived lock serializing every mutation.
- `scopa:game:<uuid>:events` — a pub/sub channel carrying "state changed"
- `tavolo:code:<JOINCODE>` — the 6-character join code → game id index.
- `tavolo:game:<uuid>:lock` — a short-lived lock serializing every mutation.
- `tavolo:game:<uuid>:events` — a pub/sub channel carrying "state changed"
signals; every open WebSocket reloads the state and pushes the
personalized view to its player.
@@ -178,11 +179,11 @@ for Redis, a fake OIDC user patched onto the mixins, and `httpx` /
The Postgres schema is owned by aerich migrations in `migrations/`. The
`db-migrate` compose service runs `aerich upgrade` before the app starts.
To add a migration after changing `src/scopa/models.py`:
To add a migration after changing `src/tavolo/models.py`:
```sh
DATABASE_URL=postgres://scopa:scopa@localhost:5432/scopa .venv/bin/aerich migrate
DATABASE_URL=postgres://scopa:scopa@localhost:5432/scopa .venv/bin/aerich upgrade
DATABASE_URL=postgres://tavolo:tavolo@localhost:5432/tavolo .venv/bin/aerich migrate
DATABASE_URL=postgres://tavolo:tavolo@localhost:5432/tavolo .venv/bin/aerich upgrade
```
(`aerich init-db` produces sqlite-flavored DDL when pointed at sqlite;
@@ -194,7 +195,7 @@ baseline.)
Everything lives under `server/`:
```
src/scopa/
src/tavolo/
├── app.py # composition root: session/OIDC/Tortoise/OpenAPI mixins
├── config.py # env -> frozen Settings
├── auth.py # auth helpers (HTTP + WebSocket)
+3 -3
View File
@@ -3,14 +3,14 @@
Tests run against an in-memory sqlite database (overriding ``DATABASE_URL``)
so they need no running Postgres, and with ``REDIS_URL`` unset so sessions
and live games use their in-memory stores. The environment is set before
:mod:`scopa.app` is imported by the test modules.
:mod:`tavolo.app` is imported by the test modules.
"""
from __future__ import annotations
import os
os.environ.setdefault("DATABASE_URL", "sqlite://:memory:")
os.environ.setdefault("OIDC_ISSUER", "http://localhost:8180/scopa")
os.environ.setdefault("OIDC_CLIENT_ID", "scopa")
os.environ.setdefault("OIDC_ISSUER", "http://localhost:8180/tavolo")
os.environ.setdefault("OIDC_CLIENT_ID", "tavolo")
os.environ.setdefault("OIDC_REDIRECT_URI", "http://localhost:8080/auth/callback")
os.environ.pop("REDIS_URL", None)
+1 -1
View File
@@ -1,4 +1,4 @@
# Dev OIDC provider: navikt/mock-oauth2-server with the scopa test
# Dev OIDC provider: navikt/mock-oauth2-server with the tavolo test
# configuration (four ready-made players) baked in. The config is
# COPYed instead of bind-mounted so this also works against containerized
# (e.g. rootless/DinD) docker daemons that cannot see the host workspace.
+1 -1
View File
@@ -2,7 +2,7 @@
"interactiveLogin": true,
"tokenCallbacks": [
{
"issuerId": "scopa",
"issuerId": "tavolo",
"tokenExpiry": 3600,
"requestMappings": [
{
+6 -6
View File
@@ -3,9 +3,9 @@ requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"
[project]
name = "scopa"
name = "tavolo"
version = "0.1.0"
description = "Scopone scientifico multiplayer backend built on the kaya framework"
description = "Multiplayer card-game platform backend built on the kaya framework"
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
@@ -37,7 +37,7 @@ namespaces = false
# Database migrations (aerich). See the Migrations section in README.md.
[tool.aerich]
tortoise_orm = "scopa.aerich_config.TORTOISE_ORM"
tortoise_orm = "tavolo.aerich_config.TORTOISE_ORM"
location = "./migrations"
[tool.mypy]
@@ -49,13 +49,13 @@ plugins = []
# runtime; without the (unavailable here) tortoise mypy plugin the stubs
# only declare the relation field. These are real attributes, not bugs.
[[tool.mypy.overrides]]
module = "scopa.models"
module = "tavolo.models"
disable_error_code = ["attr-defined"]
[[tool.mypy.overrides]]
module = "scopa.routes.*"
module = "tavolo.routes.*"
disable_error_code = ["attr-defined"]
[[tool.mypy.overrides]]
module = "scopa.game.*"
module = "tavolo.game.*"
disable_error_code = ["attr-defined"]
+14 -14
View File
@@ -8,7 +8,7 @@
--extra-index-url https://pypi.org/simple
aerich==0.10.1
# via scopa (pyproject.toml)
# via tavolo (pyproject.toml)
aiosqlite==0.22.1
# via tortoise-orm
anyio==4.15.1
@@ -19,7 +19,7 @@ anyio==4.15.1
asyncclick==8.4.2.1
# via aerich
asyncpg==0.31.0
# via scopa (pyproject.toml)
# via tavolo (pyproject.toml)
certifi==2026.7.22
# via
# httpcore
@@ -35,7 +35,7 @@ dictdiffer==0.10.0
granian==2.8.3
# via
# kaya-rsgi
# scopa (pyproject.toml)
# tavolo (pyproject.toml)
h11==0.16.0
# via httpcore
httpcore==1.0.9
@@ -43,7 +43,7 @@ httpcore==1.0.9
httpx==0.28.1
# via
# kaya-oidc
# scopa (pyproject.toml)
# tavolo (pyproject.toml)
idna==3.19
# via
# anyio
@@ -56,42 +56,42 @@ kaya-core==0.0.3
# kaya-openapi
# kaya-rsgi
# kaya-session
# scopa (pyproject.toml)
# tavolo (pyproject.toml)
kaya-oidc==0.0.3
# via scopa (pyproject.toml)
# via tavolo (pyproject.toml)
kaya-openapi==0.0.3
# via scopa (pyproject.toml)
# via tavolo (pyproject.toml)
kaya-rsgi==0.0.3
# via scopa (pyproject.toml)
# via tavolo (pyproject.toml)
kaya-session==0.0.3
# via
# kaya-oidc
# kaya-session-redis
# scopa (pyproject.toml)
# tavolo (pyproject.toml)
kaya-session-redis==0.0.3
# via scopa (pyproject.toml)
# via tavolo (pyproject.toml)
pwo==0.1.2
# via
# kaya-core
# kaya-rsgi
# kaya-session
# scopa (pyproject.toml)
# tavolo (pyproject.toml)
pycparser==3.0
# via cffi
pyjwt[crypto]==2.14.0
# via
# kaya-oidc
# scopa (pyproject.toml)
# tavolo (pyproject.toml)
pypika-tortoise==0.6.5
# via tortoise-orm
redis==8.1.0
# via
# kaya-session-redis
# scopa (pyproject.toml)
# tavolo (pyproject.toml)
tortoise-orm==1.1.8
# via
# aerich
# scopa (pyproject.toml)
# tavolo (pyproject.toml)
typing-extensions==4.16.0
# via
# anyio
@@ -1,8 +1,8 @@
"""Tortoise ORM configuration consumed by the aerich CLI.
Kept separate from :mod:`scopa.app` so ``aerich`` can import it without
Kept separate from :mod:`tavolo.app` so ``aerich`` can import it without
assembling the whole application (mixins, routes). The database URL comes
from the same :class:`~scopa.config.Settings` the app uses, so the CLI
from the same :class:`~tavolo.config.Settings` the app uses, so the CLI
and the app always point at the same database.
``aerich.models`` is required alongside the app models: it provides the
@@ -16,7 +16,7 @@ TORTOISE_ORM = {
"connections": {"default": settings.database_url},
"apps": {
"models": {
"models": ["scopa.models", "aerich.models"],
"models": ["tavolo.models", "aerich.models"],
"default_connection": "default",
}
},
@@ -6,7 +6,7 @@ Assembles the :class:`~kaya.core.KayaApp` with four mixins:
:class:`~kaya.session.redis.RedisSessionStore` when ``REDIS_URL`` is set,
otherwise an in-memory store e.g. for tests)
- :class:`~kaya.oidc.OIDCMixin` (OIDC login)
- :class:`~scopa.tortoise_mixin.TortoiseMixin` (Postgres match statistics;
- :class:`~tavolo.tortoise_mixin.TortoiseMixin` (Postgres match statistics;
skipped for ``/api/health`` and the OpenAPI documentation endpoints)
- :class:`~kaya.openapi.OpenAPIMixin` (serves the OpenAPI document at
``/api/openapi.json`` and a Swagger UI at ``/api/docs``)
@@ -57,15 +57,15 @@ oidc_mixin = OIDCMixin(
session=session_mixin,
)
openapi_mixin = OpenAPIMixin(
title="scopa",
version=_pkg_version("scopa"),
title="tavolo",
version=_pkg_version("tavolo"),
description="Scopone scientifico multiplayer API",
spec_path="/api/openapi.json",
docs_path="/api/docs",
)
tortoise_mixin = TortoiseMixin(
database_url=settings.database_url,
models_modules=["scopa.models"],
models_modules=["tavolo.models"],
skip_paths=frozenset({"/api/health", "/api/docs", "/api/openapi.json"}),
)
@@ -1,6 +1,6 @@
"""Authentication helpers on top of the kaya-oidc mixin.
Scopa has no application roles: every authenticated user may create and
Tavolo has no application roles: every authenticated user may create and
join games. Authorization beyond login is game membership, checked against
the live game state in Redis.
"""
@@ -1,4 +1,4 @@
"""Environment-driven configuration for the scopa application.
"""Environment-driven configuration for the tavolo application.
Mirrors kaya's own pattern: read ``os.environ`` directly into a plain
dataclass. No pydantic-settings, no settings module.
@@ -50,9 +50,9 @@ class Settings:
@staticmethod
def from_env() -> "Settings":
return Settings(
database_url=_env("DATABASE_URL", "postgres://scopa:scopa@localhost:5432/scopa"),
oidc_issuer=_env("OIDC_ISSUER", "http://localhost:8180/scopa"),
oidc_client_id=_env("OIDC_CLIENT_ID", "scopa"),
database_url=_env("DATABASE_URL", "postgres://tavolo:tavolo@localhost:5432/tavolo"),
oidc_issuer=_env("OIDC_ISSUER", "http://localhost:8180/tavolo"),
oidc_client_id=_env("OIDC_CLIENT_ID", "tavolo"),
oidc_client_secret=os.environ.get("OIDC_CLIENT_SECRET"),
oidc_redirect_uri=_env("OIDC_REDIRECT_URI", "http://localhost:8080/auth/callback"),
oidc_post_login_redirect=_env("OIDC_POST_LOGIN_REDIRECT", "/"),
@@ -1,8 +1,8 @@
"""Pure rules engine for scopone scientifico.
Every function here is deterministic and I/O-free: it mutates (or reads)
:class:`~scopa.game.state.GameState` and raises
:class:`~scopa.game.errors.GameError` subclasses on rule violations. This
:class:`~tavolo.game.state.GameState` and raises
:class:`~tavolo.game.errors.GameError` subclasses on rule violations. This
makes the whole rule set unit-testable without Redis, Postgres or HTTP.
Rules implemented
@@ -210,7 +210,7 @@ def play(
``capture_codes`` selects which table cards to capture; it must be a
legal capture (see :func:`legal_captures`) when one exists and empty
otherwise. Raises a :class:`~scopa.game.errors.GameError` subclass on
otherwise. Raises a :class:`~tavolo.game.errors.GameError` subclass on
any violation.
"""
if state.phase == PHASE_FINISHED:
@@ -450,7 +450,7 @@ def state_for_player(state: GameState, sub: str) -> Dict[str, object]:
"""Serialize ``state`` hiding other players' hands.
Hands are reduced to a count, except for the requesting player's own
hand. Raises :class:`~scopa.game.errors.GameNotFound`-style access via
hand. Raises :class:`~tavolo.game.errors.GameNotFound`-style access via
the caller; this function assumes ``sub`` may or may not be seated and
simply omits the hand for non-seated viewers.
"""
@@ -1,7 +1,7 @@
"""In-memory representation of a scopone scientifico game.
The whole mutable game lives in :class:`GameState`, which is serialized to
and from plain JSON for storage in Redis (see :mod:`scopa.store`). Keeping
and from plain JSON for storage in Redis (see :mod:`tavolo.store`). Keeping
the representation JSON-native means the store needs no custom codecs and
the state is inspectable with ``redis-cli``.
@@ -1,6 +1,6 @@
"""Tortoise ORM models: match statistics persisted in Postgres.
Live game state lives in Redis (see :mod:`scopa.store`); only completed
Live game state lives in Redis (see :mod:`tavolo.store`); only completed
matches are written here. The two tables answer the question "every match
a player took part in, with the final score":
@@ -3,7 +3,7 @@
A game starts as a lobby: the creator is seated first and shares the
six-character ``join_code``. When the fourth player joins, the engine deals
the first hand and the match begins. Live play then happens over the
``/ws/games/{id}`` websocket (see :mod:`scopa.ws`); these endpoints cover
``/ws/games/{id}`` websocket (see :mod:`tavolo.ws`); these endpoints cover
creation, joining and snapshotting state.
"""
from __future__ import annotations
@@ -1,6 +1,6 @@
"""Player statistics endpoints, served from Postgres.
Every finished match is persisted by :func:`scopa.stats.save_match_result`.
Every finished match is persisted by :func:`tavolo.stats.save_match_result`.
These endpoints expose a player's own match history and a global
leaderboard aggregated from the same two tables.
"""
@@ -1,10 +1,10 @@
"""Persistence for live games.
Game state is small, mutable and short-lived, which makes Redis a natural
fit: the whole match is a single JSON value under ``scopa:game:<id>`` with
fit: the whole match is a single JSON value under ``tavolo:game:<id>`` with
a sliding TTL, and a join-code index maps the short code a player shares to
that id. Completed matches are copied to Postgres (see
:mod:`scopa.models`); Redis keeps serving the finished state until it
:mod:`tavolo.models`); Redis keeps serving the finished state until it
expires.
Two implementations satisfy the same interface:
@@ -31,9 +31,9 @@ from redis.asyncio import Redis
from .game.state import GameState
GAME_KEY_PREFIX = "scopa:game:"
CODE_KEY_PREFIX = "scopa:code:"
CHANNEL_PREFIX = "scopa:game:"
GAME_KEY_PREFIX = "tavolo:game:"
CODE_KEY_PREFIX = "tavolo:code:"
CHANNEL_PREFIX = "tavolo:game:"
# Sentinel pushed into in-memory subscriber queues to signal a change.
_BUMP = b"update"
@@ -20,7 +20,7 @@ Client -> server messages are JSON objects::
{"action": "state"}
``capture`` lists the table cards to take and must be a legal capture when
one exists (see :func:`scopa.game.engine.legal_captures`); it is omitted
one exists (see :func:`tavolo.game.engine.legal_captures`); it is omitted
when the played card cannot capture. ``ack`` acknowledges the hand-end
scoring summary; the next hand is dealt when all four players have
acknowledged or the timeout fires.
+3 -3
View File
@@ -1,7 +1,7 @@
"""Test package init.
Sets environment overrides BEFORE any test module imports
:mod:`scopa.app` (which evaluates :data:`scopa.config.settings`
:mod:`tavolo.app` (which evaluates :data:`tavolo.config.settings`
at import time). Works under both ``python -m unittest discover`` and
``pytest``; conftest.py mirrors this for pytest-only collection.
"""
@@ -10,8 +10,8 @@ from __future__ import annotations
import os
os.environ.setdefault("DATABASE_URL", "sqlite://:memory:")
os.environ.setdefault("OIDC_ISSUER", "http://localhost:8180/scopa")
os.environ.setdefault("OIDC_CLIENT_ID", "scopa")
os.environ.setdefault("OIDC_ISSUER", "http://localhost:8180/tavolo")
os.environ.setdefault("OIDC_CLIENT_ID", "tavolo")
os.environ.setdefault("OIDC_REDIRECT_URI", "http://localhost:8080/auth/callback")
# Unset REDIS_URL: sessions and live games use the in-memory fallbacks.
os.environ.pop("REDIS_URL", None)
+4 -4
View File
@@ -1,7 +1,7 @@
"""Helpers for faking the OIDC authenticated user during tests.
HTTP handlers funnel through ``oidc_mixin.get_user``; websocket handlers
through :func:`scopa.auth.get_ws_user`. Patching those two entry points
through :func:`tavolo.auth.get_ws_user`. Patching those two entry points
lets route and websocket tests run entirely in-process with no IdP.
"""
from __future__ import annotations
@@ -13,10 +13,10 @@ from typing import Iterator, Optional, Sequence
from kaya.oidc import OIDCUser
# Import the app first: it pulls in the route modules, which import
# ``scopa.auth`` themselves. Importing ``auth`` before ``app`` would hit a
# ``tavolo.auth`` themselves. Importing ``auth`` before ``app`` would hit a
# partially initialized module (same constraint as reimpasto).
from scopa.app import oidc_mixin
from scopa import auth
from tavolo.app import oidc_mixin
from tavolo import auth
def make_user(sub: str, name: Optional[str] = None) -> OIDCUser:
+3 -3
View File
@@ -4,15 +4,15 @@ from __future__ import annotations
import random
import unittest
from scopa.game import engine
from scopa.game.errors import (
from tavolo.game import engine
from tavolo.game.errors import (
CardNotInHand,
GameFinished,
GameNotStarted,
IllegalMove,
NotYourTurn,
)
from scopa.game.state import (
from tavolo.game.state import (
PHASE_FINISHED,
PHASE_PLAYING,
Card,
+1 -1
View File
@@ -6,7 +6,7 @@ import unittest
from httpx import ASGITransport, AsyncClient
from pwo import async_test
from scopa.app import app
from tavolo.app import app
from tests.helpers import oidc_user
+4 -4
View File
@@ -10,8 +10,8 @@ from unittest import mock
from httpx import ASGITransport, AsyncClient
from pwo import async_test
from scopa.app import app
from scopa.config import settings
from tavolo.app import app
from tavolo.config import settings
from tests.helpers import oidc_user
@@ -45,7 +45,7 @@ class StaticRouteTest(unittest.TestCase):
(cards / "07D.svg").write_text("<svg/>")
patched = dataclasses.replace(settings, static_dir=dist)
with mock.patch("scopa.routes.static.settings", patched):
with mock.patch("tavolo.routes.static.settings", patched):
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://127.0.0.1") as client:
index = await client.get("/")
@@ -74,7 +74,7 @@ class StaticRouteTest(unittest.TestCase):
@async_test
async def test_missing_dist_returns_404(self) -> None:
patched = dataclasses.replace(settings, static_dir="/nonexistent-dist")
with mock.patch("scopa.routes.static.settings", patched):
with mock.patch("tavolo.routes.static.settings", patched):
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://127.0.0.1") as client:
response = await client.get("/")
+6 -6
View File
@@ -8,11 +8,11 @@ from datetime import datetime, timezone
from httpx import ASGITransport, AsyncClient
from pwo import async_test
from scopa.app import app, tortoise_mixin
from scopa.game import engine
from scopa.game.state import GameState
from scopa.models import Match, MatchPlayer
from scopa.stats import save_match_result
from tavolo.app import app, tortoise_mixin
from tavolo.game import engine
from tavolo.game.state import GameState
from tavolo.models import Match, MatchPlayer
from tavolo.stats import save_match_result
from tests.helpers import oidc_user
@@ -37,7 +37,7 @@ def _finished_state() -> GameState:
turn=0,
table=[engine.parse_card("02C")],
)
from scopa.game.state import PlayerState, Card
from tavolo.game.state import PlayerState, Card
state.players = [
PlayerState(sub="alice", name="alice", seat=0, hand=[Card.parse("02D")]),
+2 -2
View File
@@ -6,8 +6,8 @@ import unittest
from pwo import async_test
from scopa.game import engine
from scopa.store import InMemoryGameStore
from tavolo.game import engine
from tavolo.store import InMemoryGameStore
class InMemoryGameStoreTest(unittest.TestCase):
+3 -3
View File
@@ -9,9 +9,9 @@ from httpx_ws import WebSocketDisconnect, aconnect_ws
from httpx_ws.transport import ASGIWebSocketTransport
from pwo import async_test
from scopa.app import app, game_store
from scopa.game import engine
from scopa.game.state import Card, GameState, PlayerState
from tavolo.app import app, game_store
from tavolo.game import engine
from tavolo.game.state import Card, GameState, PlayerState
from tests.helpers import make_user, oidc_user, ws_users
PLAYERS = ("alice", "bob", "carol", "dave")
+18 -18
View File
@@ -369,24 +369,6 @@ version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
[[package]]
name = "scopa-web"
version = "0.1.0"
dependencies = [
"console_error_panic_hook",
"futures",
"gloo-net",
"gloo-timers",
"js-sys",
"serde",
"serde_json",
"sycamore",
"sycamore-router",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
name = "serde"
version = "1.0.229"
@@ -579,6 +561,24 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "tavolo-web"
version = "0.1.0"
dependencies = [
"console_error_panic_hook",
"futures",
"gloo-net",
"gloo-timers",
"js-sys",
"serde",
"serde_json",
"sycamore",
"sycamore-router",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
name = "thiserror"
version = "1.0.69"
+2 -2
View File
@@ -1,8 +1,8 @@
[package]
name = "scopa-web"
name = "tavolo-web"
version = "0.1.0"
edition = "2021"
description = "Sycamore/WASM frontend for the scopone scientifico backend"
description = "Sycamore/WASM frontend for the tavolo card-game platform"
[dependencies]
sycamore = "0.9"
+1 -1
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Scopone scientifico</title>
<title>Tavolo</title>
<link data-trunk rel="rust" href="Cargo.toml">
<link data-trunk rel="css" href="style.css">
<!-- Card images (CC0 woodcut napoletane deck) copied verbatim into dist. -->
+1 -1
View File
@@ -1,4 +1,4 @@
//! REST client for the scopa backend. Same-origin requests carry the
//! REST client for the tavolo backend. Same-origin requests carry the
//! session cookie automatically.
use crate::model::*;
use gloo_net::http::Request;