From e5805c62e9190c13292b0004cc1269e6c1da3a01 Mon Sep 17 00:00:00 2001 From: marcosf2 Date: Wed, 15 Apr 2026 18:00:50 -0300 Subject: [PATCH 1/3] Add game available to configs and endpoint for frontend to check --- pyproject.toml | 1 + src/pqnstack/app/api/main.py | 2 ++ src/pqnstack/app/api/routes/games.py | 10 ++++++++ src/pqnstack/app/core/config.py | 7 ++++++ src/pqnstack/cli.py | 36 ++++++++++++++++++++++++++++ uv.lock | 11 +++++++++ 6 files changed, 67 insertions(+) create mode 100644 src/pqnstack/app/api/routes/games.py diff --git a/pyproject.toml b/pyproject.toml index 2614a8a9..52597617 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,7 @@ webapp = [ "fastapi[standard]>=0.115.14", "httpx>=0.28.1", "pydantic-settings>=2.10.1", + "tomli-w>=1.0.0", ] [dependency-groups] diff --git a/src/pqnstack/app/api/main.py b/src/pqnstack/app/api/main.py index 175e0899..34ee765e 100644 --- a/src/pqnstack/app/api/main.py +++ b/src/pqnstack/app/api/main.py @@ -3,6 +3,7 @@ from pqnstack.app.api.routes import chsh from pqnstack.app.api.routes import coordination from pqnstack.app.api.routes import debug +from pqnstack.app.api.routes import games from pqnstack.app.api.routes import qkd from pqnstack.app.api.routes import rng from pqnstack.app.api.routes import serial @@ -16,3 +17,4 @@ api_router.include_router(serial.router) api_router.include_router(coordination.router) api_router.include_router(debug.router) +api_router.include_router(games.router) diff --git a/src/pqnstack/app/api/routes/games.py b/src/pqnstack/app/api/routes/games.py new file mode 100644 index 00000000..2e23a879 --- /dev/null +++ b/src/pqnstack/app/api/routes/games.py @@ -0,0 +1,10 @@ +from fastapi import APIRouter + +from pqnstack.app.core.config import GamesAvailability, get_settings + +router = APIRouter(prefix="/games", tags=["games"]) + + +@router.get("/availability") +def get_availability() -> GamesAvailability: + return get_settings().games_availability diff --git a/src/pqnstack/app/core/config.py b/src/pqnstack/app/core/config.py index 0505b17c..412bc191 100644 --- a/src/pqnstack/app/core/config.py +++ b/src/pqnstack/app/core/config.py @@ -43,6 +43,12 @@ class QKDSettings(BaseModel): measurement_config: MeasurementConfig = Field(default_factory=lambda: MeasurementConfig(integration_time_s=5)) +class GamesAvailability(BaseModel): + chsh: bool = True # "Verify Quantum Link" + qf: bool = True # "Quantum Fortune" + ssm: bool = True # "Share a Secret Message" + + class Settings(BaseSettings): node_name: str = "node1" router_name: str = "router1" @@ -54,6 +60,7 @@ class Settings(BaseSettings): timetagger: tuple[str, str] | None = None # Name of the timetagger to use for the CHSH experiment. rotary_encoder_address: str = "/dev/ttyACM0" virtual_rotator: bool = False # If True, use terminal input instead of hardware rotary encoder + games_availability: GamesAvailability = Field(default_factory=GamesAvailability) model_config = SettingsConfigDict( toml_file="./config.toml", diff --git a/src/pqnstack/cli.py b/src/pqnstack/cli.py index d2d72228..600a8fc9 100644 --- a/src/pqnstack/cli.py +++ b/src/pqnstack/cli.py @@ -184,5 +184,41 @@ def start_router( router.start() +@app.command() +def toggle_game( + games: Annotated[list[str], typer.Argument(help="Games to toggle: chsh, qf, ssm")], + enable: Annotated[bool, typer.Option("--enable/--disable", help="Enable or disable the games")] = True, + config: Annotated[str, typer.Option(help="Path to config.toml")] = "./config.toml", +) -> None: + """ + Enable or disable one or more games in config.toml. + + Changes take effect on the next server restart. Games: chsh (Verify Quantum Link), qf (Quantum Fortune), ssm (Share a Secret Message). + """ + import tomllib + + import tomli_w + + valid_games = {"chsh", "qf", "ssm"} + invalid = [g for g in games if g not in valid_games] + if invalid: + msg = f"Game(s) must be one of: chsh, qf, ssm. Invalid: {invalid}" + raise InvalidNetworkConfigurationError(msg) + + path = Path(config) + with path.open("rb") as f: + cfg = tomllib.load(f) + + cfg.setdefault("games_availability", {}) + for game in games: + cfg["games_availability"][game] = enable + + with path.open("wb") as f: + tomli_w.dump(cfg, f) + + status = "enabled" if enable else "disabled" + logger.info("Games %s %s in %s. Restart the server for changes to take effect.", games, status, path) + + if __name__ == "__main__": app() diff --git a/uv.lock b/uv.lock index 403bd664..7ff1e9a4 100644 --- a/uv.lock +++ b/uv.lock @@ -741,6 +741,7 @@ webapp = [ { name = "fastapi", extra = ["standard"] }, { name = "httpx" }, { name = "pydantic-settings" }, + { name = "tomli-w" }, ] [package.dev-dependencies] @@ -761,6 +762,7 @@ requires-dist = [ { name = "pyfirmata2", specifier = ">=2.5.0" }, { name = "pyzmq", specifier = ">=26.2.0" }, { name = "thorlabs-apt-device", specifier = ">=0.3.8" }, + { name = "tomli-w", marker = "extra == 'webapp'", specifier = ">=1.0.0" }, { name = "typer", specifier = ">=0.15.1" }, ] provides-extras = ["webapp"] @@ -1230,6 +1232,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cc/38/60afff0deb2379c5feae251a5da9e9773c6ac8e2f09fbdac8d2980e28aa2/thorlabs_apt_device-0.3.8-py3-none-any.whl", hash = "sha256:84897581dc99e6f1ab774f07f9466d9137c94ac3dfa0fa59cdd305a4c9682872", size = 64631, upload-time = "2024-03-09T09:39:16.829Z" }, ] +[[package]] +name = "tomli-w" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/75/241269d1da26b624c0d5e110e8149093c759b7a286138f4efd61a60e75fe/tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021", size = 7184, upload-time = "2025-01-15T12:07:24.262Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675, upload-time = "2025-01-15T12:07:22.074Z" }, +] + [[package]] name = "typer" version = "0.20.0" From b01d2d12a5817399b131f2ef45b2e84fdfa2c787 Mon Sep 17 00:00:00 2001 From: marcosf2 Date: Wed, 15 Apr 2026 18:15:50 -0300 Subject: [PATCH 2/3] ruff mypy checks --- src/pqnstack/app/api/routes/games.py | 3 ++- src/pqnstack/cli.py | 7 ++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/pqnstack/app/api/routes/games.py b/src/pqnstack/app/api/routes/games.py index 2e23a879..99f03224 100644 --- a/src/pqnstack/app/api/routes/games.py +++ b/src/pqnstack/app/api/routes/games.py @@ -1,6 +1,7 @@ from fastapi import APIRouter -from pqnstack.app.core.config import GamesAvailability, get_settings +from pqnstack.app.core.config import GamesAvailability +from pqnstack.app.core.config import get_settings router = APIRouter(prefix="/games", tags=["games"]) diff --git a/src/pqnstack/cli.py b/src/pqnstack/cli.py index 600a8fc9..988a162e 100644 --- a/src/pqnstack/cli.py +++ b/src/pqnstack/cli.py @@ -4,6 +4,7 @@ from pathlib import Path from typing import Annotated +import tomli_w import typer from pqnstack.base.errors import InvalidNetworkConfigurationError @@ -187,7 +188,7 @@ def start_router( @app.command() def toggle_game( games: Annotated[list[str], typer.Argument(help="Games to toggle: chsh, qf, ssm")], - enable: Annotated[bool, typer.Option("--enable/--disable", help="Enable or disable the games")] = True, + enable: Annotated[bool, typer.Option("--enable/--disable", help="Enable or disable the games")] = True, # noqa: FBT002 config: Annotated[str, typer.Option(help="Path to config.toml")] = "./config.toml", ) -> None: """ @@ -195,10 +196,6 @@ def toggle_game( Changes take effect on the next server restart. Games: chsh (Verify Quantum Link), qf (Quantum Fortune), ssm (Share a Secret Message). """ - import tomllib - - import tomli_w - valid_games = {"chsh", "qf", "ssm"} invalid = [g for g in games if g not in valid_games] if invalid: From 1471da2db5ed2146c3cffb385dc8f7b3798fc60f Mon Sep 17 00:00:00 2001 From: marcosf2 Date: Thu, 16 Apr 2026 17:11:06 -0300 Subject: [PATCH 3/3] Removes extra import in cli.py --- pyproject.toml | 2 +- uv.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 52597617..4900f0e2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,7 @@ dependencies = [ "pyfirmata2>=2.5.0", "pyzmq>=26.2.0", "thorlabs-apt-device>=0.3.8", + "tomli-w>=1.0.0", "typer>=0.15.1", ] @@ -31,7 +32,6 @@ webapp = [ "fastapi[standard]>=0.115.14", "httpx>=0.28.1", "pydantic-settings>=2.10.1", - "tomli-w>=1.0.0", ] [dependency-groups] diff --git a/uv.lock b/uv.lock index 7ff1e9a4..8f7b208a 100644 --- a/uv.lock +++ b/uv.lock @@ -733,6 +733,7 @@ dependencies = [ { name = "pyfirmata2" }, { name = "pyzmq" }, { name = "thorlabs-apt-device" }, + { name = "tomli-w" }, { name = "typer" }, ] @@ -741,7 +742,6 @@ webapp = [ { name = "fastapi", extra = ["standard"] }, { name = "httpx" }, { name = "pydantic-settings" }, - { name = "tomli-w" }, ] [package.dev-dependencies] @@ -762,7 +762,7 @@ requires-dist = [ { name = "pyfirmata2", specifier = ">=2.5.0" }, { name = "pyzmq", specifier = ">=26.2.0" }, { name = "thorlabs-apt-device", specifier = ">=0.3.8" }, - { name = "tomli-w", marker = "extra == 'webapp'", specifier = ">=1.0.0" }, + { name = "tomli-w", specifier = ">=1.0.0" }, { name = "typer", specifier = ">=0.15.1" }, ] provides-extras = ["webapp"]