From fc95d58c5d060e81ccddc5f40e93d2b5d657cdca Mon Sep 17 00:00:00 2001 From: haliphax Date: Tue, 1 Apr 2025 23:34:25 -0500 Subject: [PATCH 01/14] =?UTF-8?q?=F0=9F=9A=A7=20wip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- realm_api/models/character.py | 12 ++++++++++++ realm_api/models/game.py | 14 ++++++++++++++ realm_api/models/guild.py | 8 ++++++++ realm_api/models/system.py | 10 ++++++++++ 4 files changed, 44 insertions(+) create mode 100644 realm_api/models/character.py create mode 100644 realm_api/models/game.py create mode 100644 realm_api/models/guild.py create mode 100644 realm_api/models/system.py diff --git a/realm_api/models/character.py b/realm_api/models/character.py new file mode 100644 index 0000000..8dbf1d9 --- /dev/null +++ b/realm_api/models/character.py @@ -0,0 +1,12 @@ +"""Character model""" + +# 3rd party +from sqlmodel import Field, SQLModel + +# local +from .game import Game + + +class Character(SQLModel, table=True): + id: int = Field(primary_key=True) + game_id: int = Field(foreign_key=Game.id) diff --git a/realm_api/models/game.py b/realm_api/models/game.py new file mode 100644 index 0000000..f5faf13 --- /dev/null +++ b/realm_api/models/game.py @@ -0,0 +1,14 @@ +"""Game model""" + +# 3rd party +from sqlmodel import Field, SQLModel + +# local +from .guild import Guild +from .system import System + + +class Game(SQLModel, table=True): + id: int = Field(primary_key=True) + system_id: int = Field(foreign_key=System.id) + guild_id: str = Field(foreign_key=Guild.id) diff --git a/realm_api/models/guild.py b/realm_api/models/guild.py new file mode 100644 index 0000000..0f165ac --- /dev/null +++ b/realm_api/models/guild.py @@ -0,0 +1,8 @@ +"""Discord Guild model""" + +# 3rd party +from sqlmodel import Field, SQLModel + + +class Guild(SQLModel, table=True): + id: str = Field(primary_key=True, max_length=32) diff --git a/realm_api/models/system.py b/realm_api/models/system.py new file mode 100644 index 0000000..2a9d788 --- /dev/null +++ b/realm_api/models/system.py @@ -0,0 +1,10 @@ +"""Game System model""" + +# 3rd party +from sqlmodel import Field, SQLModel + + +class System(SQLModel, table=True): + id: int = Field(primary_key=True) + name: str = Field(max_length=128) + slug: str = Field(max_length=16, unique=True) From 53f9b9662711c020f66f5b927384891ea7f98ba5 Mon Sep 17 00:00:00 2001 From: haliphax Date: Tue, 12 Aug 2025 22:05:16 -0500 Subject: [PATCH 02/14] =?UTF-8?q?=E2=9C=A8=20moar=20models,=20refactor=20t?= =?UTF-8?q?able=20generator?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- realm_api/db.py | 11 ++++++++++- realm_api/models/character_prop.py | 13 +++++++++++++ realm_api/models/character_stat.py | 13 +++++++++++++ realm_api/models/game.py | 1 + 4 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 realm_api/models/character_prop.py create mode 100644 realm_api/models/character_stat.py diff --git a/realm_api/db.py b/realm_api/db.py index 64419d8..6d6b73e 100644 --- a/realm_api/db.py +++ b/realm_api/db.py @@ -2,6 +2,7 @@ # stdlib import asyncio as aio +from importlib import import_module import os # 3rd party @@ -31,7 +32,15 @@ def __init__(self, app): async def init_db(cls): """Initializes the database.""" - from .models.user_session import UserSession # noqa: F401 + for mod in [ + "models.character_prop.CharacterProp", + "models.character_stat.CharacterStat", + "models.game.Game", + "models.guild.Guild", + "models.system.System", + "models.user_session.UserSession", + ]: + import_module(mod) log.info("Initializing database") diff --git a/realm_api/models/character_prop.py b/realm_api/models/character_prop.py new file mode 100644 index 0000000..eec6082 --- /dev/null +++ b/realm_api/models/character_prop.py @@ -0,0 +1,13 @@ +"""Character model""" + +# 3rd party +from sqlmodel import Field, SQLModel + +# local +from .character import Character + + +class CharacterProp(SQLModel, table=True): + character_id: int = Field(foreign_key=Character.id, primary_key=True) + name: str = Field(primary_key=True, max_length=32) + value: str = Field() diff --git a/realm_api/models/character_stat.py b/realm_api/models/character_stat.py new file mode 100644 index 0000000..c69f0ca --- /dev/null +++ b/realm_api/models/character_stat.py @@ -0,0 +1,13 @@ +"""Character model""" + +# 3rd party +from sqlmodel import Field, SQLModel + +# local +from .character import Character + + +class CharacterStat(SQLModel, table=True): + character_id: int = Field(foreign_key=Character.id, primary_key=True) + name: str = Field(primary_key=True, max_length=32) + value: int = Field() diff --git a/realm_api/models/game.py b/realm_api/models/game.py index f5faf13..09a1cf7 100644 --- a/realm_api/models/game.py +++ b/realm_api/models/game.py @@ -12,3 +12,4 @@ class Game(SQLModel, table=True): id: int = Field(primary_key=True) system_id: int = Field(foreign_key=System.id) guild_id: str = Field(foreign_key=Guild.id) + name: str = Field(max_length=128) From 07af5bfdd995246117fa091ca5d83c3a2fd5db35 Mon Sep 17 00:00:00 2001 From: haliphax Date: Tue, 12 Aug 2025 22:15:14 -0500 Subject: [PATCH 03/14] =?UTF-8?q?=F0=9F=8E=A8=20docstrings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- realm_api/models/character_prop.py | 2 +- realm_api/models/character_stat.py | 2 +- realm_api/models/system.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/realm_api/models/character_prop.py b/realm_api/models/character_prop.py index eec6082..1967374 100644 --- a/realm_api/models/character_prop.py +++ b/realm_api/models/character_prop.py @@ -1,4 +1,4 @@ -"""Character model""" +"""Character property model""" # 3rd party from sqlmodel import Field, SQLModel diff --git a/realm_api/models/character_stat.py b/realm_api/models/character_stat.py index c69f0ca..9cd787d 100644 --- a/realm_api/models/character_stat.py +++ b/realm_api/models/character_stat.py @@ -1,4 +1,4 @@ -"""Character model""" +"""Character stat model""" # 3rd party from sqlmodel import Field, SQLModel diff --git a/realm_api/models/system.py b/realm_api/models/system.py index 2a9d788..06e4305 100644 --- a/realm_api/models/system.py +++ b/realm_api/models/system.py @@ -1,4 +1,4 @@ -"""Game System model""" +"""Game system model""" # 3rd party from sqlmodel import Field, SQLModel From 6295acdb3289127488f4e5de9b5b60fa51df8c90 Mon Sep 17 00:00:00 2001 From: haliphax Date: Tue, 12 Aug 2025 22:16:41 -0500 Subject: [PATCH 04/14] =?UTF-8?q?=F0=9F=A7=91=E2=80=8D=F0=9F=92=BB=20type:?= =?UTF-8?q?=20module?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 4967aa9..066d009 100644 --- a/package.json +++ b/package.json @@ -33,5 +33,6 @@ }, "scripts": { "prepare": "husky" - } + }, + "type": "module" } From df461e37e11a47dba7a708af1930bd29ea93df89 Mon Sep 17 00:00:00 2001 From: haliphax Date: Tue, 12 Aug 2025 22:19:43 -0500 Subject: [PATCH 05/14] =?UTF-8?q?=F0=9F=99=88=20fix=20gitignore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index ed60c1b..6d5e708 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,9 @@ -**/*.egg-info -**/__pycache__/ +*.egg-info .env .ruff_cache/ .vscode/ /build/ /config.toml /data/*.sqlite3 +__pycache__/ node_modules/ From f990316dc3f4464ae3d90929b15a81877900f436 Mon Sep 17 00:00:00 2001 From: haliphax Date: Wed, 13 Aug 2025 12:14:12 -0500 Subject: [PATCH 06/14] =?UTF-8?q?=E2=9C=A8=20moaaaar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- realm_api/db.py | 6 ++++++ realm_api/models/game_player.py | 13 +++++++++++++ realm_api/models/game_player_role.py | 14 ++++++++++++++ realm_api/models/game_role.py | 13 +++++++++++++ realm_api/models/player.py | 9 +++++++++ realm_api/models/role.py | 9 +++++++++ 6 files changed, 64 insertions(+) create mode 100644 realm_api/models/game_player.py create mode 100644 realm_api/models/game_player_role.py create mode 100644 realm_api/models/game_role.py create mode 100644 realm_api/models/player.py create mode 100644 realm_api/models/role.py diff --git a/realm_api/db.py b/realm_api/db.py index b5d92c4..5199c49 100644 --- a/realm_api/db.py +++ b/realm_api/db.py @@ -33,10 +33,16 @@ async def init_db(cls): """Initializes the database.""" for mod in [ + "character.Character", "character_prop.CharacterProp", "character_stat.CharacterStat", "game.Game", + "game_player.GamePlayer", + "game_player_role.GamePlayerRole", + "game_role.GameRole", "guild.Guild", + "player.Player", + "role.Role", "system.System", "user_session.UserSession", ]: diff --git a/realm_api/models/game_player.py b/realm_api/models/game_player.py new file mode 100644 index 0000000..ccf4b4c --- /dev/null +++ b/realm_api/models/game_player.py @@ -0,0 +1,13 @@ +"""Game Player model""" + +# 3rd party +from sqlmodel import Field, SQLModel + +# local +from .game import Game +from .player import Player + + +class GamePlayer(SQLModel, table=True): + game_id: int = Field(foreign_key=Game.id, primary_key=True) + player_id: str = Field(foreign_key=Player.user_id, primary_key=True) diff --git a/realm_api/models/game_player_role.py b/realm_api/models/game_player_role.py new file mode 100644 index 0000000..386a50c --- /dev/null +++ b/realm_api/models/game_player_role.py @@ -0,0 +1,14 @@ +"""GamePlayer -> GameRole model""" + +# 3rd party +from sqlmodel import Field, SQLModel + +# local +from .game_player import GamePlayer +from .game_role import GameRole + + +class GamePlayerRole(SQLModel, table=True): + game_id: int = Field(foreign_key=GameRole.game_id, primary_key=True) + role_id: int = Field(foreign_key=GameRole.role_id, primary_key=True) + player_id: str = Field(foreign_key=GamePlayer.player_id, primary_key=True) diff --git a/realm_api/models/game_role.py b/realm_api/models/game_role.py new file mode 100644 index 0000000..ee00ce9 --- /dev/null +++ b/realm_api/models/game_role.py @@ -0,0 +1,13 @@ +"""Game Role model""" + +# 3rd party +from sqlmodel import Field, SQLModel + +# local +from .game import Game +from .role import Role + + +class GameRole(SQLModel, table=True): + game_id: int = Field(foreign_key=Game.id, primary_key=True) + role_id: int = Field(foreign_key=Role.id, primary_key=True) diff --git a/realm_api/models/player.py b/realm_api/models/player.py new file mode 100644 index 0000000..ec219d1 --- /dev/null +++ b/realm_api/models/player.py @@ -0,0 +1,9 @@ +"""Player model""" + +# 3rd party +from sqlmodel import Field, SQLModel + + +class Player(SQLModel, table=True): + user_id: str = Field(primary_key=True, max_length=32) + name: str = Field(max_length=32) diff --git a/realm_api/models/role.py b/realm_api/models/role.py new file mode 100644 index 0000000..f1502a3 --- /dev/null +++ b/realm_api/models/role.py @@ -0,0 +1,9 @@ +"""Role model""" + +# 3rd party +from sqlmodel import Field, SQLModel + + +class Role(SQLModel, table=True): + id: int = Field(primary_key=True) + name: str = Field(max_length=32) From 73fab85e7b0a8dbcf3fb0b47aba24b8825efbe3e Mon Sep 17 00:00:00 2001 From: haliphax Date: Wed, 13 Aug 2025 12:25:44 -0500 Subject: [PATCH 07/14] =?UTF-8?q?=F0=9F=9A=A7=20wip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- realm_api/db.py | 33 +++++++-------------------------- realm_api/game/__init__.py | 11 +++++++++++ realm_api/game/routes.py | 15 +++++++++++++++ 3 files changed, 33 insertions(+), 26 deletions(-) create mode 100644 realm_api/game/__init__.py create mode 100644 realm_api/game/routes.py diff --git a/realm_api/db.py b/realm_api/db.py index 5199c49..c1a6ae6 100644 --- a/realm_api/db.py +++ b/realm_api/db.py @@ -20,18 +20,15 @@ async_engine = create_async_engine(DB_URL, future=True) -class StartupMiddleware: - """Startup middleware for initializing the database""" - - initialized: aio.Event = aio.Event() +async def get_session(): + """Get an `AsyncSession` object.""" - def __init__(self, app): - self.app = app + async with AsyncSession(async_engine) as session: + yield session - @classmethod - async def init_db(cls): - """Initializes the database.""" +def setup_webapp(app: FastAPI, *_): + async def init_db(): for mod in [ "character.Character", "character_prop.CharacterProp", @@ -53,20 +50,4 @@ async def init_db(cls): async with async_engine.begin() as conn: await conn.run_sync(SQLModel.metadata.create_all) - async def __call__(self, scope, receive, send): - if not StartupMiddleware.initialized.is_set(): - await StartupMiddleware.init_db() - StartupMiddleware.initialized.set() - - await self.app(scope, receive, send) - - -async def get_session(): - """Get an `AsyncSession` object.""" - - async with AsyncSession(async_engine) as session: - yield session - - -def setup_webapp(app: FastAPI, *_): - app.add_middleware(StartupMiddleware) + aio.get_event_loop().run_until_complete(init_db()) diff --git a/realm_api/game/__init__.py b/realm_api/game/__init__.py new file mode 100644 index 0000000..2d98d07 --- /dev/null +++ b/realm_api/game/__init__.py @@ -0,0 +1,11 @@ +"""Game module""" + +# 3rd party +from fastapi import APIRouter, FastAPI + +# local +from .routes import router + + +def setup_webapp(_app: FastAPI, app_router: APIRouter): + app_router.include_router(router) diff --git a/realm_api/game/routes.py b/realm_api/game/routes.py new file mode 100644 index 0000000..4d32aba --- /dev/null +++ b/realm_api/game/routes.py @@ -0,0 +1,15 @@ +"""Game routes""" + +# 3rd party +from fastapi import APIRouter, Depends + +# local +from ..auth.depends import require_login +from ..models.user_session import UserSession + +router = APIRouter(prefix="/game") + + +@router.get("/{guild_id}") +def list_games(guild_id: int, session: UserSession = Depends(require_login)): + return {"games": ["butts"]} From fdf0089b83ebcae58978ce48cd99a5ae0da00385 Mon Sep 17 00:00:00 2001 From: haliphax Date: Wed, 13 Aug 2025 12:38:09 -0500 Subject: [PATCH 08/14] =?UTF-8?q?=F0=9F=90=9B=20fix=20foreign=20keys?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- realm_api/_all.py | 2 +- realm_api/db.py | 26 +++++++++++++------------- realm_api/game/routes.py | 20 +++++++++++++++++--- realm_api/models/character.py | 5 +---- realm_api/models/character_prop.py | 7 +++---- realm_api/models/character_stat.py | 7 +++---- realm_api/models/game.py | 8 ++------ realm_api/models/game_player.py | 10 ++++------ realm_api/models/game_player_role.py | 12 +++++------- realm_api/models/game_role.py | 10 ++++------ realm_api/models/user_session.py | 2 ++ 11 files changed, 55 insertions(+), 54 deletions(-) diff --git a/realm_api/_all.py b/realm_api/_all.py index 58ea27c..3ba7749 100644 --- a/realm_api/_all.py +++ b/realm_api/_all.py @@ -1,4 +1,4 @@ """Realm API metaextension for Aethersprite""" META_EXTENSION = True -_mods = ["auth", "db", "rpc"] +_mods = ["auth", "db", "game", "rpc"] diff --git a/realm_api/db.py b/realm_api/db.py index c1a6ae6..b0b6ee1 100644 --- a/realm_api/db.py +++ b/realm_api/db.py @@ -30,20 +30,20 @@ async def get_session(): def setup_webapp(app: FastAPI, *_): async def init_db(): for mod in [ - "character.Character", - "character_prop.CharacterProp", - "character_stat.CharacterStat", - "game.Game", - "game_player.GamePlayer", - "game_player_role.GamePlayerRole", - "game_role.GameRole", - "guild.Guild", - "player.Player", - "role.Role", - "system.System", - "user_session.UserSession", + "character", + "character_prop", + "character_stat", + "game", + "game_player", + "game_player_role", + "game_role", + "guild", + "player", + "role", + "system", + "user_session", ]: - import_module(f".models.{mod}", __name__) + import_module(f".models.{mod}", __package__) log.info("Initializing database") diff --git a/realm_api/game/routes.py b/realm_api/game/routes.py index 4d32aba..9326467 100644 --- a/realm_api/game/routes.py +++ b/realm_api/game/routes.py @@ -6,10 +6,24 @@ # local from ..auth.depends import require_login from ..models.user_session import UserSession +from .schema import GameResponse router = APIRouter(prefix="/game") -@router.get("/{guild_id}") -def list_games(guild_id: int, session: UserSession = Depends(require_login)): - return {"games": ["butts"]} +@router.get("/list/{guild_id}") +def list_games( + guild_id: int, session: UserSession = Depends(require_login) +) -> list[GameResponse]: + return [ + GameResponse(id=1234, name="Butts"), + GameResponse(id=2345, name="Moar butts"), + ] + + +@router.get("/{game_id}") +def get_game( + game_id: int, + session: UserSession = Depends(require_login), +) -> GameResponse: + return GameResponse(id=1234, name="Butts") diff --git a/realm_api/models/character.py b/realm_api/models/character.py index 8dbf1d9..771b312 100644 --- a/realm_api/models/character.py +++ b/realm_api/models/character.py @@ -3,10 +3,7 @@ # 3rd party from sqlmodel import Field, SQLModel -# local -from .game import Game - class Character(SQLModel, table=True): id: int = Field(primary_key=True) - game_id: int = Field(foreign_key=Game.id) + game_id: int = Field(foreign_key="game.id") diff --git a/realm_api/models/character_prop.py b/realm_api/models/character_prop.py index 1967374..b1f34f5 100644 --- a/realm_api/models/character_prop.py +++ b/realm_api/models/character_prop.py @@ -3,11 +3,10 @@ # 3rd party from sqlmodel import Field, SQLModel -# local -from .character import Character - class CharacterProp(SQLModel, table=True): - character_id: int = Field(foreign_key=Character.id, primary_key=True) + __tablename__ = "character_prop" # type: ignore + + character_id: int = Field(foreign_key="character.id", primary_key=True) name: str = Field(primary_key=True, max_length=32) value: str = Field() diff --git a/realm_api/models/character_stat.py b/realm_api/models/character_stat.py index 9cd787d..5125d9a 100644 --- a/realm_api/models/character_stat.py +++ b/realm_api/models/character_stat.py @@ -3,11 +3,10 @@ # 3rd party from sqlmodel import Field, SQLModel -# local -from .character import Character - class CharacterStat(SQLModel, table=True): - character_id: int = Field(foreign_key=Character.id, primary_key=True) + __tablename__ = "character_stat" # type: ignore + + character_id: int = Field(foreign_key="character.id", primary_key=True) name: str = Field(primary_key=True, max_length=32) value: int = Field() diff --git a/realm_api/models/game.py b/realm_api/models/game.py index 09a1cf7..9ebbea4 100644 --- a/realm_api/models/game.py +++ b/realm_api/models/game.py @@ -3,13 +3,9 @@ # 3rd party from sqlmodel import Field, SQLModel -# local -from .guild import Guild -from .system import System - class Game(SQLModel, table=True): id: int = Field(primary_key=True) - system_id: int = Field(foreign_key=System.id) - guild_id: str = Field(foreign_key=Guild.id) + system_id: int = Field(foreign_key="system.id") + guild_id: str = Field(foreign_key="guild.id") name: str = Field(max_length=128) diff --git a/realm_api/models/game_player.py b/realm_api/models/game_player.py index ccf4b4c..ed9b855 100644 --- a/realm_api/models/game_player.py +++ b/realm_api/models/game_player.py @@ -3,11 +3,9 @@ # 3rd party from sqlmodel import Field, SQLModel -# local -from .game import Game -from .player import Player - class GamePlayer(SQLModel, table=True): - game_id: int = Field(foreign_key=Game.id, primary_key=True) - player_id: str = Field(foreign_key=Player.user_id, primary_key=True) + __tablename__ = "game_player" # type: ignore + + game_id: int = Field(foreign_key="game.id", primary_key=True) + player_id: str = Field(foreign_key="player.user_id", primary_key=True) diff --git a/realm_api/models/game_player_role.py b/realm_api/models/game_player_role.py index 386a50c..e6a588d 100644 --- a/realm_api/models/game_player_role.py +++ b/realm_api/models/game_player_role.py @@ -3,12 +3,10 @@ # 3rd party from sqlmodel import Field, SQLModel -# local -from .game_player import GamePlayer -from .game_role import GameRole - class GamePlayerRole(SQLModel, table=True): - game_id: int = Field(foreign_key=GameRole.game_id, primary_key=True) - role_id: int = Field(foreign_key=GameRole.role_id, primary_key=True) - player_id: str = Field(foreign_key=GamePlayer.player_id, primary_key=True) + __tablename__ = "game_player_role" # type: ignore + + game_id: int = Field(foreign_key="game.id", primary_key=True) + role_id: int = Field(foreign_key="role.id", primary_key=True) + player_id: str = Field(foreign_key="player.user_id", primary_key=True) diff --git a/realm_api/models/game_role.py b/realm_api/models/game_role.py index ee00ce9..b064b57 100644 --- a/realm_api/models/game_role.py +++ b/realm_api/models/game_role.py @@ -3,11 +3,9 @@ # 3rd party from sqlmodel import Field, SQLModel -# local -from .game import Game -from .role import Role - class GameRole(SQLModel, table=True): - game_id: int = Field(foreign_key=Game.id, primary_key=True) - role_id: int = Field(foreign_key=Role.id, primary_key=True) + __tablename__ = "game_role" # type: ignore + + game_id: int = Field(foreign_key="game.id", primary_key=True) + role_id: int = Field(foreign_key="role.id", primary_key=True) diff --git a/realm_api/models/user_session.py b/realm_api/models/user_session.py index cfc294f..daca3c1 100644 --- a/realm_api/models/user_session.py +++ b/realm_api/models/user_session.py @@ -12,6 +12,8 @@ def uuid_factory(): class UserSession(SQLModel, table=True): + __tablename__ = "user_session" # type: ignore + user_id: str = Field(primary_key=True, max_length=32) discord_token: str = Field(max_length=64) realm_token: str = Field( From f136fb819955ee4afe09654623a5a77c10d95691 Mon Sep 17 00:00:00 2001 From: haliphax Date: Wed, 13 Aug 2025 13:06:50 -0500 Subject: [PATCH 09/14] =?UTF-8?q?=F0=9F=A6=BA=20schema?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- realm_api/db.py | 35 ++++++++++++++++++++++++++--------- realm_api/game/routes.py | 37 ++++++++++++++++++++++++++----------- realm_api/game/schema.py | 8 ++++++++ 3 files changed, 60 insertions(+), 20 deletions(-) create mode 100644 realm_api/game/schema.py diff --git a/realm_api/db.py b/realm_api/db.py index b0b6ee1..34cfa9e 100644 --- a/realm_api/db.py +++ b/realm_api/db.py @@ -20,15 +20,18 @@ async_engine = create_async_engine(DB_URL, future=True) -async def get_session(): - """Get an `AsyncSession` object.""" +class StartupMiddleware: + """Startup middleware for initializing the database""" - async with AsyncSession(async_engine) as session: - yield session + initialized: aio.Event = aio.Event() + def __init__(self, app): + self.app = app + + @classmethod + async def init_db(cls): + log.info("Initializing database") -def setup_webapp(app: FastAPI, *_): - async def init_db(): for mod in [ "character", "character_prop", @@ -45,9 +48,23 @@ async def init_db(): ]: import_module(f".models.{mod}", __package__) - log.info("Initializing database") - async with async_engine.begin() as conn: await conn.run_sync(SQLModel.metadata.create_all) - aio.get_event_loop().run_until_complete(init_db()) + async def __call__(self, scope, receive, send): + if not StartupMiddleware.initialized.is_set(): + await StartupMiddleware.init_db() + StartupMiddleware.initialized.set() + + await self.app(scope, receive, send) + + +async def get_session(): + """Get an `AsyncSession` object.""" + + async with AsyncSession(async_engine) as session: + yield session + + +def setup_webapp(app: FastAPI, *_): + app.add_middleware(StartupMiddleware) diff --git a/realm_api/game/routes.py b/realm_api/game/routes.py index 9326467..f4d8346 100644 --- a/realm_api/game/routes.py +++ b/realm_api/game/routes.py @@ -2,28 +2,43 @@ # 3rd party from fastapi import APIRouter, Depends +from sqlmodel import select +from sqlmodel.ext.asyncio.session import AsyncSession # local -from ..auth.depends import require_login -from ..models.user_session import UserSession +from ..db import get_session +from ..models.game import Game +from .depends import user_in_guild from .schema import GameResponse router = APIRouter(prefix="/game") @router.get("/list/{guild_id}") -def list_games( - guild_id: int, session: UserSession = Depends(require_login) +async def list_games( + guild_id: int, + db: AsyncSession = Depends(get_session), + in_guild=Depends(user_in_guild), ) -> list[GameResponse]: - return [ - GameResponse(id=1234, name="Butts"), - GameResponse(id=2345, name="Moar butts"), + games = ( + await db.exec(select(Game).where(Game.guild_id == str(guild_id))) + ).all() + response = [ + GameResponse.model_validate(game, from_attributes=True) + for game in games ] + return response -@router.get("/{game_id}") -def get_game( + +@router.get("/{guild_id}/{game_id}") +async def get_game( + guild_id: int, game_id: int, - session: UserSession = Depends(require_login), + db: AsyncSession = Depends(get_session), + in_guild=Depends(user_in_guild), ) -> GameResponse: - return GameResponse(id=1234, name="Butts") + game = (await db.exec(select(Game).where(Game.id == str(game_id)))).one() + response = GameResponse.model_validate(game, from_attributes=True) + + return response diff --git a/realm_api/game/schema.py b/realm_api/game/schema.py new file mode 100644 index 0000000..0ebe984 --- /dev/null +++ b/realm_api/game/schema.py @@ -0,0 +1,8 @@ +"""HTTP request/response schema for game routes""" + +from pydantic import BaseModel + + +class GameResponse(BaseModel): + id: int + name: str From 1c2cb080f8e0da22d4e50a8c31a73d0f42e16b88 Mon Sep 17 00:00:00 2001 From: haliphax Date: Wed, 13 Aug 2025 13:59:04 -0500 Subject: [PATCH 10/14] =?UTF-8?q?=E2=9C=A8=20some=20routes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- realm_api/game/depends.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 realm_api/game/depends.py diff --git a/realm_api/game/depends.py b/realm_api/game/depends.py new file mode 100644 index 0000000..1661c83 --- /dev/null +++ b/realm_api/game/depends.py @@ -0,0 +1,32 @@ +"""FastAPI checks for use in `Depends()`""" + +# 3rd party +from fastapi import Depends, HTTPException, Path, status +from sqlmodel import select +from sqlmodel.ext.asyncio.session import AsyncSession + +# local +from ..auth.depends import require_login +from ..db import get_session +from ..models.game import Game +from ..models.game_player import GamePlayer +from ..models.user_session import UserSession + + +async def user_in_guild( + guild_id: int = Path(), + session: UserSession = Depends(require_login), + db: AsyncSession = Depends(get_session), +): + if not ( + await db.exec( + select(1) + .select_from(GamePlayer, Game) + .where( + Game.guild_id == str(guild_id), + GamePlayer.game_id == Game.id, + GamePlayer.player_id == session.user_id, + ) + ) + ).one_or_none(): + raise HTTPException(status.HTTP_403_FORBIDDEN) From ec55a318058f91a4987c5c412f71130f1cbcee4f Mon Sep 17 00:00:00 2001 From: haliphax Date: Fri, 15 Aug 2025 14:23:51 -0500 Subject: [PATCH 11/14] =?UTF-8?q?=F0=9F=8E=A8=20moar=20relationships?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- realm_api/db.py | 19 ++------------- realm_api/models/__init__.py | 15 ++++++++++++ realm_api/models/character.py | 21 +++++++++++++++- realm_api/models/character_prop.py | 9 ++++++- realm_api/models/character_stat.py | 9 ++++++- realm_api/models/game.py | 13 +++++++++- realm_api/models/game_player.py | 29 ++++++++++++++++++++++- realm_api/models/game_player_character.py | 12 ++++++++++ realm_api/models/game_role.py | 14 ++++++++++- realm_api/models/guild.py | 6 ++++- realm_api/models/player.py | 12 +++++++++- realm_api/models/role.py | 14 ++++++++++- realm_api/models/system.py | 9 ++++++- 13 files changed, 155 insertions(+), 27 deletions(-) create mode 100644 realm_api/models/game_player_character.py diff --git a/realm_api/db.py b/realm_api/db.py index 34cfa9e..ec4e90d 100644 --- a/realm_api/db.py +++ b/realm_api/db.py @@ -2,7 +2,6 @@ # stdlib import asyncio as aio -from importlib import import_module import os # 3rd party @@ -30,23 +29,9 @@ def __init__(self, app): @classmethod async def init_db(cls): - log.info("Initializing database") + from . import models # noqa: F401 - for mod in [ - "character", - "character_prop", - "character_stat", - "game", - "game_player", - "game_player_role", - "game_role", - "guild", - "player", - "role", - "system", - "user_session", - ]: - import_module(f".models.{mod}", __package__) + log.info("Initializing database") async with async_engine.begin() as conn: await conn.run_sync(SQLModel.metadata.create_all) diff --git a/realm_api/models/__init__.py b/realm_api/models/__init__.py index a8fac9a..94149d5 100644 --- a/realm_api/models/__init__.py +++ b/realm_api/models/__init__.py @@ -1 +1,16 @@ """SQLModel models""" + +# ruff: noqa: F401 +from .character import Character +from .character_prop import CharacterProp +from .character_stat import CharacterStat +from .game import Game +from .game_player import GamePlayer +from .game_player_character import GamePlayerCharacter +from .game_player_role import GamePlayerRole +from .game_role import GameRole +from .guild import Guild +from .player import Player +from .role import Role +from .system import System +from .user_session import UserSession diff --git a/realm_api/models/character.py b/realm_api/models/character.py index 771b312..6de9f8f 100644 --- a/realm_api/models/character.py +++ b/realm_api/models/character.py @@ -1,9 +1,28 @@ """Character model""" +# stdlib +from typing import TYPE_CHECKING + # 3rd party -from sqlmodel import Field, SQLModel +from sqlmodel import Field, Relationship, SQLModel + +# local +from .character_prop import CharacterProp +from .character_stat import CharacterStat + +if TYPE_CHECKING: + from .game_player import GamePlayer class Character(SQLModel, table=True): id: int = Field(primary_key=True) game_id: int = Field(foreign_key="game.id") + managers: list["GamePlayer"] = Relationship( + back_populates="characters", + sa_relationship_kwargs={ + "primaryjoin": "Character.id == GamePlayerCharacter.character_id", + "secondaryjoin": "GamePlayerCharacter.player_id == GamePlayer.player_id", + }, + ) + props: list[CharacterProp] = Relationship(back_populates="character") + stats: list[CharacterStat] = Relationship(back_populates="character") diff --git a/realm_api/models/character_prop.py b/realm_api/models/character_prop.py index b1f34f5..c9f44fc 100644 --- a/realm_api/models/character_prop.py +++ b/realm_api/models/character_prop.py @@ -1,12 +1,19 @@ """Character property model""" +# stdlib +from typing import TYPE_CHECKING + # 3rd party -from sqlmodel import Field, SQLModel +from sqlmodel import Field, Relationship, SQLModel + +if TYPE_CHECKING: + from .character import Character class CharacterProp(SQLModel, table=True): __tablename__ = "character_prop" # type: ignore character_id: int = Field(foreign_key="character.id", primary_key=True) + character: "Character" = Relationship(back_populates="props") name: str = Field(primary_key=True, max_length=32) value: str = Field() diff --git a/realm_api/models/character_stat.py b/realm_api/models/character_stat.py index 5125d9a..dc87473 100644 --- a/realm_api/models/character_stat.py +++ b/realm_api/models/character_stat.py @@ -1,12 +1,19 @@ """Character stat model""" +# stdlib +from typing import TYPE_CHECKING + # 3rd party -from sqlmodel import Field, SQLModel +from sqlmodel import Field, Relationship, SQLModel + +if TYPE_CHECKING: + from .character import Character class CharacterStat(SQLModel, table=True): __tablename__ = "character_stat" # type: ignore character_id: int = Field(foreign_key="character.id", primary_key=True) + character: "Character" = Relationship(back_populates="stats") name: str = Field(primary_key=True, max_length=32) value: int = Field() diff --git a/realm_api/models/game.py b/realm_api/models/game.py index 9ebbea4..a19c1d1 100644 --- a/realm_api/models/game.py +++ b/realm_api/models/game.py @@ -1,11 +1,22 @@ """Game model""" +# stdlib +from typing import TYPE_CHECKING + # 3rd party -from sqlmodel import Field, SQLModel +from sqlmodel import Field, Relationship, SQLModel + +# local +from .system import System + +if TYPE_CHECKING: + from .guild import Guild class Game(SQLModel, table=True): id: int = Field(primary_key=True) system_id: int = Field(foreign_key="system.id") + system: System = Relationship(back_populates="games") guild_id: str = Field(foreign_key="guild.id") + guild: "Guild" = Relationship(back_populates="games") name: str = Field(max_length=128) diff --git a/realm_api/models/game_player.py b/realm_api/models/game_player.py index ed9b855..06175af 100644 --- a/realm_api/models/game_player.py +++ b/realm_api/models/game_player.py @@ -1,11 +1,38 @@ """Game Player model""" +# stdlib +from typing import TYPE_CHECKING + # 3rd party -from sqlmodel import Field, SQLModel +from sqlmodel import Field, Relationship, SQLModel + +# local +from .character import Character +from .game import Game +from .player import Player + +if TYPE_CHECKING: + from .game_role import GameRole class GamePlayer(SQLModel, table=True): __tablename__ = "game_player" # type: ignore game_id: int = Field(foreign_key="game.id", primary_key=True) + game: Game = Relationship(back_populates="players") player_id: str = Field(foreign_key="player.user_id", primary_key=True) + player: Player = Relationship() + characters: list[Character] = Relationship( + back_populates="managers", + sa_relationship_kwargs={ + "primaryjoin": "GamePlayer.player_id == GamePlayerCharacter.player_id", + "secondaryjoin": "GamePlayerCharacter.character_id == Character.id", + }, + ) + roles: list["GameRole"] = Relationship( + back_populates="members", + sa_relationship_kwargs={ + "primaryjoin": "GamePlayer.player_id == GamePlayerRole.player_id", + "secondaryjoin": "GamePlayerRole.role_id == GameRole.role_id", + }, + ) diff --git a/realm_api/models/game_player_character.py b/realm_api/models/game_player_character.py new file mode 100644 index 0000000..2dc8a3c --- /dev/null +++ b/realm_api/models/game_player_character.py @@ -0,0 +1,12 @@ +"""GamePlayer -> Character model""" + +# 3rd party +from sqlmodel import Field, SQLModel + + +class GamePlayerCharacter(SQLModel, table=True): + __tablename__ = "game_player_character" # type: ignore + + game_id: int = Field(foreign_key="game.id", primary_key=True) + character_id: int = Field(foreign_key="character.id", primary_key=True) + player_id: str = Field(foreign_key="player.user_id", primary_key=True) diff --git a/realm_api/models/game_role.py b/realm_api/models/game_role.py index b064b57..cdff3bb 100644 --- a/realm_api/models/game_role.py +++ b/realm_api/models/game_role.py @@ -1,11 +1,23 @@ """Game Role model""" # 3rd party -from sqlmodel import Field, SQLModel +from sqlmodel import Field, Relationship, SQLModel + +# local +from .game import Game +from .game_player import GamePlayer class GameRole(SQLModel, table=True): __tablename__ = "game_role" # type: ignore game_id: int = Field(foreign_key="game.id", primary_key=True) + game: Game = Relationship() role_id: int = Field(foreign_key="role.id", primary_key=True) + members: list[GamePlayer] = Relationship( + back_populates="roles", + sa_relationship_kwargs={ + "primaryjoin": "GameRole.role_id == GamePlayerRole.role_id", + "secondaryjoin": "GamePlayerRole.player_id == GamePlayer.player_id", + }, + ) diff --git a/realm_api/models/guild.py b/realm_api/models/guild.py index 0f165ac..3f4c6d5 100644 --- a/realm_api/models/guild.py +++ b/realm_api/models/guild.py @@ -1,8 +1,12 @@ """Discord Guild model""" # 3rd party -from sqlmodel import Field, SQLModel +from sqlmodel import Field, Relationship, SQLModel + +# local +from .game import Game class Guild(SQLModel, table=True): id: str = Field(primary_key=True, max_length=32) + games: list[Game] = Relationship(back_populates="guild") diff --git a/realm_api/models/player.py b/realm_api/models/player.py index ec219d1..81d5f50 100644 --- a/realm_api/models/player.py +++ b/realm_api/models/player.py @@ -1,9 +1,19 @@ """Player model""" # 3rd party -from sqlmodel import Field, SQLModel +from sqlmodel import Field, Relationship, SQLModel + +# local +from .game import Game class Player(SQLModel, table=True): user_id: str = Field(primary_key=True, max_length=32) name: str = Field(max_length=32) + games: list[Game] = Relationship( + back_populates="players", + sa_relationship_kwargs={ + "primaryjoin": "Player.user_id == GamePlayer.player_id", + "secondaryjoin": "GamePlayer.game_id == Game.id", + }, + ) diff --git a/realm_api/models/role.py b/realm_api/models/role.py index f1502a3..599eae4 100644 --- a/realm_api/models/role.py +++ b/realm_api/models/role.py @@ -1,9 +1,21 @@ """Role model""" +# stdlib +from typing import TYPE_CHECKING + # 3rd party -from sqlmodel import Field, SQLModel +from sqlmodel import Field, Relationship, SQLModel + +if TYPE_CHECKING: + from .player import Player class Role(SQLModel, table=True): id: int = Field(primary_key=True) name: str = Field(max_length=32) + players: list["Player"] = Relationship( + sa_relationship_kwargs={ + "primaryjoin": "Role.id == GamePlayerRole.role_id", + "secondaryjoin": "GamePlayerRole.id == Player.id", + } + ) diff --git a/realm_api/models/system.py b/realm_api/models/system.py index 06e4305..917d9f0 100644 --- a/realm_api/models/system.py +++ b/realm_api/models/system.py @@ -1,10 +1,17 @@ """Game system model""" +# stdlib +from typing import TYPE_CHECKING + # 3rd party -from sqlmodel import Field, SQLModel +from sqlmodel import Field, Relationship, SQLModel + +if TYPE_CHECKING: + from .game import Game class System(SQLModel, table=True): id: int = Field(primary_key=True) name: str = Field(max_length=128) slug: str = Field(max_length=16, unique=True) + games: list["Game"] = Relationship(back_populates="system") From 89b15cdb6aa0e9d0b7623af7c1dbb7f9f6fe6191 Mon Sep 17 00:00:00 2001 From: haliphax Date: Wed, 27 Aug 2025 21:34:38 -0500 Subject: [PATCH 12/14] =?UTF-8?q?=E2=9E=95=20remove=20dependency=20on=20ae?= =?UTF-8?q?thersprite,=20use=20own=20fastapi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 2 +- README.md | 2 +- realm_api/__main__.py | 14 ++ realm_api/_all.py | 4 - realm_api/api/__init__.py | 15 ++ realm_api/api/auth/__init__.py | 1 + realm_api/{ => api}/auth/depends.py | 4 +- .../{auth/routes.py => api/auth/router.py} | 8 +- realm_api/{ => api}/auth/schema.py | 1 + realm_api/api/game/__init__.py | 1 + realm_api/{ => api}/game/depends.py | 10 +- .../{game/routes.py => api/game/router.py} | 4 +- realm_api/{ => api}/game/schema.py | 1 + realm_api/app.py | 33 ++++ realm_api/auth/__init__.py | 11 -- realm_api/cors.py | 15 -- realm_api/db.py | 39 ++--- realm_api/game/__init__.py | 11 -- realm_api/logging.py | 12 ++ realm_api/rpc/__init__.py | 6 +- requirements/dev.txt | 10 +- requirements/docs.txt | 6 +- requirements/requirements.in | 4 +- requirements/requirements.txt | 150 ++++++------------ 24 files changed, 166 insertions(+), 198 deletions(-) create mode 100644 realm_api/__main__.py delete mode 100644 realm_api/_all.py create mode 100644 realm_api/api/__init__.py create mode 100644 realm_api/api/auth/__init__.py rename realm_api/{ => api}/auth/depends.py (89%) rename realm_api/{auth/routes.py => api/auth/router.py} (93%) rename realm_api/{ => api}/auth/schema.py (96%) create mode 100644 realm_api/api/game/__init__.py rename realm_api/{ => api}/game/depends.py (75%) rename realm_api/{game/routes.py => api/game/router.py} (93%) rename realm_api/{ => api}/game/schema.py (92%) create mode 100644 realm_api/app.py delete mode 100644 realm_api/auth/__init__.py delete mode 100644 realm_api/cors.py delete mode 100644 realm_api/game/__init__.py create mode 100644 realm_api/logging.py diff --git a/Dockerfile b/Dockerfile index f8d86c4..5e98856 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,4 +24,4 @@ RUN <<-EOF pip install --no-cache -Ue . EOF -ENTRYPOINT [ "/usr/local/bin/python", "-m", "aethersprite.webapp" ] +ENTRYPOINT [ "/usr/local/bin/python", "-m", "realm_api" ] diff --git a/README.md b/README.md index ddbb547..b1382b0 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ pip install -U 'realm_api@git+https://github.com/realm-ttrpg/api-server.git' In the same directory as your `config.toml` file: ```shell -python -m aethersprite.webapp +python -m realm_api ``` [bot software]: https://github.com/realm-ttrpg/discord-bot diff --git a/realm_api/__main__.py b/realm_api/__main__.py new file mode 100644 index 0000000..b6c53a2 --- /dev/null +++ b/realm_api/__main__.py @@ -0,0 +1,14 @@ +"""Entry point""" + +# 3rd party +from uvicorn import run + +# local +from os import environ + +run( + "realm_api.app:app", + host=environ.get("REALM_HOST", "0.0.0.0"), + port=int(environ.get("REALM_PORT", "5000")), + lifespan="on", +) diff --git a/realm_api/_all.py b/realm_api/_all.py deleted file mode 100644 index 3ba7749..0000000 --- a/realm_api/_all.py +++ /dev/null @@ -1,4 +0,0 @@ -"""Realm API metaextension for Aethersprite""" - -META_EXTENSION = True -_mods = ["auth", "db", "game", "rpc"] diff --git a/realm_api/api/__init__.py b/realm_api/api/__init__.py new file mode 100644 index 0000000..5c9b8c2 --- /dev/null +++ b/realm_api/api/__init__.py @@ -0,0 +1,15 @@ +"""API layer""" + +# 3rd party +from fastapi import APIRouter + +# local +from .auth.router import router as auth_router +from .game.router import router as game_router + + +router = APIRouter() +"""Main router""" + +router.include_router(auth_router) +router.include_router(game_router) diff --git a/realm_api/api/auth/__init__.py b/realm_api/api/auth/__init__.py new file mode 100644 index 0000000..d1928c4 --- /dev/null +++ b/realm_api/api/auth/__init__.py @@ -0,0 +1 @@ +"""Authentication/authorization""" diff --git a/realm_api/auth/depends.py b/realm_api/api/auth/depends.py similarity index 89% rename from realm_api/auth/depends.py rename to realm_api/api/auth/depends.py index 9cc461e..085b952 100644 --- a/realm_api/auth/depends.py +++ b/realm_api/api/auth/depends.py @@ -7,8 +7,8 @@ from sqlmodel import select # local -from ..db import get_session -from ..models.user_session import UserSession +from realm_api.db import get_session +from realm_api.models.user_session import UserSession async def require_login( diff --git a/realm_api/auth/routes.py b/realm_api/api/auth/router.py similarity index 93% rename from realm_api/auth/routes.py rename to realm_api/api/auth/router.py index 608ace5..9e92228 100644 --- a/realm_api/auth/routes.py +++ b/realm_api/api/auth/router.py @@ -9,10 +9,10 @@ from realm_schema import BotGuildsResponse # local -from ..db import get_session -from ..discord import DiscordClient -from ..models.user_session import UserSession -from ..rpc import redis_conn, rpc_bot +from realm_api.db import get_session +from realm_api.discord import DiscordClient +from realm_api.models.user_session import UserSession +from realm_api.rpc import redis_conn, rpc_bot from .depends import require_login from .schema import LoginRequest, LoginResponse, SharedGuildsResponse diff --git a/realm_api/auth/schema.py b/realm_api/api/auth/schema.py similarity index 96% rename from realm_api/auth/schema.py rename to realm_api/api/auth/schema.py index a29c587..0f0da0d 100644 --- a/realm_api/auth/schema.py +++ b/realm_api/api/auth/schema.py @@ -1,5 +1,6 @@ """HTTP request/response schema for authz/authn routes""" +# 3rd party from pydantic import BaseModel diff --git a/realm_api/api/game/__init__.py b/realm_api/api/game/__init__.py new file mode 100644 index 0000000..231e87c --- /dev/null +++ b/realm_api/api/game/__init__.py @@ -0,0 +1 @@ +"""Game module""" diff --git a/realm_api/game/depends.py b/realm_api/api/game/depends.py similarity index 75% rename from realm_api/game/depends.py rename to realm_api/api/game/depends.py index 1661c83..748f75a 100644 --- a/realm_api/game/depends.py +++ b/realm_api/api/game/depends.py @@ -6,11 +6,11 @@ from sqlmodel.ext.asyncio.session import AsyncSession # local -from ..auth.depends import require_login -from ..db import get_session -from ..models.game import Game -from ..models.game_player import GamePlayer -from ..models.user_session import UserSession +from realm_api.api.auth.depends import require_login +from realm_api.db import get_session +from realm_api.models.game import Game +from realm_api.models.game_player import GamePlayer +from realm_api.models.user_session import UserSession async def user_in_guild( diff --git a/realm_api/game/routes.py b/realm_api/api/game/router.py similarity index 93% rename from realm_api/game/routes.py rename to realm_api/api/game/router.py index f4d8346..b659a08 100644 --- a/realm_api/game/routes.py +++ b/realm_api/api/game/router.py @@ -6,8 +6,8 @@ from sqlmodel.ext.asyncio.session import AsyncSession # local -from ..db import get_session -from ..models.game import Game +from realm_api.db import get_session +from realm_api.models.game import Game from .depends import user_in_guild from .schema import GameResponse diff --git a/realm_api/game/schema.py b/realm_api/api/game/schema.py similarity index 92% rename from realm_api/game/schema.py rename to realm_api/api/game/schema.py index 0ebe984..510ac5f 100644 --- a/realm_api/game/schema.py +++ b/realm_api/api/game/schema.py @@ -1,5 +1,6 @@ """HTTP request/response schema for game routes""" +# 3rd party from pydantic import BaseModel diff --git a/realm_api/app.py b/realm_api/app.py new file mode 100644 index 0000000..3504035 --- /dev/null +++ b/realm_api/app.py @@ -0,0 +1,33 @@ +"""FastAPI application""" + +# stdlib +from contextlib import asynccontextmanager + +# 3rd party +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +# local +from .api import router +from .db import init_db + + +@asynccontextmanager +async def lifespan(app: FastAPI): + """Lifespan function: startup -> yield -> shutdown""" + + await init_db() + yield + + +app = FastAPI(lifespan=lifespan) +"""Web application""" + +app.add_middleware( + CORSMiddleware, + allow_credentials=True, + allow_headers=["*"], + allow_methods=["*"], + allow_origins=["*"], +) +app.include_router(router) diff --git a/realm_api/auth/__init__.py b/realm_api/auth/__init__.py deleted file mode 100644 index b48d2f4..0000000 --- a/realm_api/auth/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -"""Authentication/authorization""" - -# 3rd party -from fastapi import APIRouter, FastAPI - -# local -from .routes import router - - -def setup_webapp(_app: FastAPI, app_router: APIRouter): - app_router.include_router(router) diff --git a/realm_api/cors.py b/realm_api/cors.py deleted file mode 100644 index d1be827..0000000 --- a/realm_api/cors.py +++ /dev/null @@ -1,15 +0,0 @@ -"""CORS extension""" - -# 3rd party -from fastapi import APIRouter, FastAPI -from fastapi.middleware.cors import CORSMiddleware - - -def setup_webapp(app: FastAPI, _router: APIRouter): - app.add_middleware( - CORSMiddleware, - allow_credentials=True, - allow_headers=["*"], - allow_methods=["*"], - allow_origins=["*"], - ) diff --git a/realm_api/db.py b/realm_api/db.py index ec4e90d..e8f119d 100644 --- a/realm_api/db.py +++ b/realm_api/db.py @@ -1,17 +1,16 @@ -"""Database functionality and Aethersprite extension""" +"""Database functionality""" # stdlib -import asyncio as aio import os # 3rd party -from fastapi import FastAPI from sqlmodel import SQLModel from sqlalchemy.ext.asyncio import create_async_engine from sqlmodel.ext.asyncio.session import AsyncSession -# api -from aethersprite import log +# local +from realm_api.logging import logger + DB_URL = os.environ.get( "DB_URL", "postgresql+asyncpg://realm:realm@localhost/realm" @@ -19,29 +18,15 @@ async_engine = create_async_engine(DB_URL, future=True) -class StartupMiddleware: - """Startup middleware for initializing the database""" - - initialized: aio.Event = aio.Event() - - def __init__(self, app): - self.app = app - - @classmethod - async def init_db(cls): - from . import models # noqa: F401 +async def init_db(): + """Initialize the database. Used in the FastAPI application lifespan.""" - log.info("Initializing database") + from . import models # noqa: F401 - async with async_engine.begin() as conn: - await conn.run_sync(SQLModel.metadata.create_all) + logger.info("Initializing database") - async def __call__(self, scope, receive, send): - if not StartupMiddleware.initialized.is_set(): - await StartupMiddleware.init_db() - StartupMiddleware.initialized.set() - - await self.app(scope, receive, send) + async with async_engine.begin() as conn: + await conn.run_sync(SQLModel.metadata.create_all) async def get_session(): @@ -49,7 +34,3 @@ async def get_session(): async with AsyncSession(async_engine) as session: yield session - - -def setup_webapp(app: FastAPI, *_): - app.add_middleware(StartupMiddleware) diff --git a/realm_api/game/__init__.py b/realm_api/game/__init__.py deleted file mode 100644 index 2d98d07..0000000 --- a/realm_api/game/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -"""Game module""" - -# 3rd party -from fastapi import APIRouter, FastAPI - -# local -from .routes import router - - -def setup_webapp(_app: FastAPI, app_router: APIRouter): - app_router.include_router(router) diff --git a/realm_api/logging.py b/realm_api/logging.py new file mode 100644 index 0000000..2150c35 --- /dev/null +++ b/realm_api/logging.py @@ -0,0 +1,12 @@ +"""Logging""" + +# stdlib +from logging import getLogger, StreamHandler +from os import environ + + +logger = getLogger() +"""Root logger""" + +logger.addHandler(StreamHandler()) +logger.setLevel(environ.get("LOG_LEVEL", "INFO")) diff --git a/realm_api/rpc/__init__.py b/realm_api/rpc/__init__.py index f132b5b..d39d1c4 100644 --- a/realm_api/rpc/__init__.py +++ b/realm_api/rpc/__init__.py @@ -9,10 +9,8 @@ # 3rd party import redis -# api -from aethersprite import log - # local +from realm_api.logging import logger from .roll import roll_handler redis_conn = redis.StrictRedis(host=os.environ.get("REDIS_HOST", "localhost")) @@ -26,7 +24,7 @@ def handler(message: dict): """Handle an incoming RPC operation and publish the result.""" data: dict = json.loads(message["data"]) - log.info(f"RPC op: {data['op']}") + logger.info(f"RPC op: {data['op']}") result = handlers[data["op"]]( *data.get("args", []), **data.get("kwargs", dict()), diff --git a/requirements/dev.txt b/requirements/dev.txt index 6f03754..c00267f 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -4,21 +4,21 @@ # # pip-compile dev.in # -build==1.2.2.post1 +build==1.3.0 # via pip-tools -click==8.1.7 +click==8.2.1 # via # -c requirements.txt # pip-tools -packaging==24.2 +packaging==25.0 # via build -pip-tools==7.4.1 +pip-tools==7.5.0 # via -r dev.in pyproject-hooks==1.2.0 # via # build # pip-tools -ruff==0.9.10 +ruff==0.12.10 # via -r dev.in wheel==0.45.1 # via pip-tools diff --git a/requirements/docs.txt b/requirements/docs.txt index cf4f228..c22cb9d 100644 --- a/requirements/docs.txt +++ b/requirements/docs.txt @@ -4,11 +4,11 @@ # # pip-compile docs.in # -mako==1.3.9 +mako==1.3.10 # via pdoc3 -markdown==3.7 +markdown==3.8.2 # via pdoc3 markupsafe==3.0.2 # via mako -pdoc3==0.11.5 +pdoc3==0.11.6 # via -r docs.in diff --git a/requirements/requirements.in b/requirements/requirements.in index 49deaef..d84e0c9 100644 --- a/requirements/requirements.in +++ b/requirements/requirements.in @@ -1,7 +1,9 @@ -aethersprite@git+https://github.com/haliphax/aethersprite.git@main +aiohttp asyncpg +fastapi hiredis realm_schema@git+https://github.com/realm-ttrpg/schema.git@main redis sqlalchemy[asyncio] sqlmodel +uvicorn[standard] diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 5f6c6d6..d067be4 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -4,144 +4,94 @@ # # pip-compile requirements.in # -aethersprite @ git+https://github.com/haliphax/aethersprite.git@main - # via -r requirements.in aiohappyeyeballs==2.6.1 - # via - # aethersprite - # aiohttp -aiohttp==3.12.14 - # via - # aethersprite - # discord-py + # via aiohttp +aiohttp==3.12.15 + # via -r requirements.in aiosignal==1.4.0 + # via aiohttp +annotated-types==0.7.0 + # via pydantic +anyio==4.10.0 # via - # aethersprite - # aiohttp -annotated-types==0.6.0 - # via - # aethersprite - # pydantic -anyio==4.3.0 - # via - # aethersprite # starlette # watchfiles asyncpg==0.30.0 # via -r requirements.in -attrs==23.2.0 - # via - # aethersprite - # aiohttp -click==8.1.7 - # via - # aethersprite - # uvicorn -colorlog==6.8.2 - # via aethersprite -discord-pretty-help==2.0.7 - # via aethersprite -discord-py==2.3.2 - # via aethersprite -fastapi==0.110.0 - # via aethersprite -frozenlist==1.4.1 +attrs==25.3.0 + # via aiohttp +click==8.2.1 + # via uvicorn +fastapi==0.116.1 + # via -r requirements.in +frozenlist==1.7.0 # via - # aethersprite # aiohttp # aiosignal -greenlet==3.1.1 +greenlet==3.2.4 # via sqlalchemy h11==0.16.0 - # via - # aethersprite - # uvicorn -hiredis==3.1.0 + # via uvicorn +hiredis==3.2.1 # via -r requirements.in -httptools==0.6.1 +httptools==0.6.4 + # via uvicorn +idna==3.10 # via - # aethersprite - # uvicorn -idna==3.7 - # via - # aethersprite # anyio # yarl -multidict==6.0.5 +multidict==6.6.4 # via - # aethersprite # aiohttp # yarl -propcache==0.2.0 +propcache==0.3.2 # via - # aethersprite # aiohttp # yarl -pydantic==2.6.4 +pydantic==2.11.7 # via - # aethersprite # fastapi # realm-schema # sqlmodel -pydantic-core==2.16.3 - # via - # aethersprite - # pydantic -python-dotenv==1.0.1 - # via - # aethersprite - # uvicorn -pytz==2024.1 - # via aethersprite -pyyaml==6.0.1 - # via - # aethersprite - # uvicorn +pydantic-core==2.33.2 + # via pydantic +python-dotenv==1.1.1 + # via uvicorn +pyyaml==6.0.2 + # via uvicorn realm-schema @ git+https://github.com/realm-ttrpg/schema.git@main # via -r requirements.in -redis==5.2.1 +redis==6.4.0 # via -r requirements.in sniffio==1.3.1 - # via - # aethersprite - # anyio -sqlalchemy[asyncio]==2.0.39 + # via anyio +sqlalchemy[asyncio]==2.0.43 # via # -r requirements.in # sqlmodel -sqlitedict==2.1.0 - # via aethersprite sqlmodel==0.0.24 # via -r requirements.in -starlette==0.36.3 - # via - # aethersprite - # fastapi -toml==0.10.2 - # via aethersprite -typing-extensions==4.10.0 +starlette==0.47.3 + # via fastapi +typing-extensions==4.15.0 # via - # aethersprite # aiosignal + # anyio # fastapi # pydantic # pydantic-core # sqlalchemy -uvicorn[standard]==0.29.0 - # via aethersprite -uvloop==0.19.0 - # via - # aethersprite - # uvicorn -watchfiles==0.21.0 - # via - # aethersprite - # uvicorn -websockets==12.0 - # via - # aethersprite - # uvicorn -yarl==1.17.2 - # via - # aethersprite - # aiohttp + # starlette + # typing-inspection +typing-inspection==0.4.1 + # via pydantic +uvicorn[standard]==0.35.0 + # via -r requirements.in +uvloop==0.21.0 + # via uvicorn +watchfiles==1.1.0 + # via uvicorn +websockets==15.0.1 + # via uvicorn +yarl==1.20.1 + # via aiohttp From 1b444c7c7f02b3b631d3b661fba59771aaa92f41 Mon Sep 17 00:00:00 2001 From: haliphax Date: Wed, 27 Aug 2025 22:37:29 -0500 Subject: [PATCH 13/14] =?UTF-8?q?=F0=9F=90=9B=20fixups?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- realm_api/models/character.py | 2 ++ realm_api/models/game.py | 2 ++ realm_api/models/game_player.py | 8 ++++++-- realm_api/models/game_role.py | 2 ++ realm_api/models/player.py | 4 +++- realm_api/models/role.py | 9 +++++++-- 6 files changed, 22 insertions(+), 5 deletions(-) diff --git a/realm_api/models/character.py b/realm_api/models/character.py index 6de9f8f..0cc6a14 100644 --- a/realm_api/models/character.py +++ b/realm_api/models/character.py @@ -9,6 +9,7 @@ # local from .character_prop import CharacterProp from .character_stat import CharacterStat +from .game_player_character import GamePlayerCharacter if TYPE_CHECKING: from .game_player import GamePlayer @@ -19,6 +20,7 @@ class Character(SQLModel, table=True): game_id: int = Field(foreign_key="game.id") managers: list["GamePlayer"] = Relationship( back_populates="characters", + link_model=GamePlayerCharacter, sa_relationship_kwargs={ "primaryjoin": "Character.id == GamePlayerCharacter.character_id", "secondaryjoin": "GamePlayerCharacter.player_id == GamePlayer.player_id", diff --git a/realm_api/models/game.py b/realm_api/models/game.py index a19c1d1..67dfedc 100644 --- a/realm_api/models/game.py +++ b/realm_api/models/game.py @@ -10,6 +10,7 @@ from .system import System if TYPE_CHECKING: + from .game_player import GamePlayer from .guild import Guild @@ -20,3 +21,4 @@ class Game(SQLModel, table=True): guild_id: str = Field(foreign_key="guild.id") guild: "Guild" = Relationship(back_populates="games") name: str = Field(max_length=128) + players: list["GamePlayer"] = Relationship(back_populates="game") diff --git a/realm_api/models/game_player.py b/realm_api/models/game_player.py index 06175af..27321ec 100644 --- a/realm_api/models/game_player.py +++ b/realm_api/models/game_player.py @@ -9,10 +9,12 @@ # local from .character import Character from .game import Game -from .player import Player +from .game_player_character import GamePlayerCharacter +from .game_player_role import GamePlayerRole if TYPE_CHECKING: from .game_role import GameRole + from .player import Player class GamePlayer(SQLModel, table=True): @@ -21,9 +23,10 @@ class GamePlayer(SQLModel, table=True): game_id: int = Field(foreign_key="game.id", primary_key=True) game: Game = Relationship(back_populates="players") player_id: str = Field(foreign_key="player.user_id", primary_key=True) - player: Player = Relationship() + player: "Player" = Relationship() characters: list[Character] = Relationship( back_populates="managers", + link_model=GamePlayerCharacter, sa_relationship_kwargs={ "primaryjoin": "GamePlayer.player_id == GamePlayerCharacter.player_id", "secondaryjoin": "GamePlayerCharacter.character_id == Character.id", @@ -31,6 +34,7 @@ class GamePlayer(SQLModel, table=True): ) roles: list["GameRole"] = Relationship( back_populates="members", + link_model=GamePlayerRole, sa_relationship_kwargs={ "primaryjoin": "GamePlayer.player_id == GamePlayerRole.player_id", "secondaryjoin": "GamePlayerRole.role_id == GameRole.role_id", diff --git a/realm_api/models/game_role.py b/realm_api/models/game_role.py index cdff3bb..317ea8e 100644 --- a/realm_api/models/game_role.py +++ b/realm_api/models/game_role.py @@ -6,6 +6,7 @@ # local from .game import Game from .game_player import GamePlayer +from .game_player_role import GamePlayerRole class GameRole(SQLModel, table=True): @@ -16,6 +17,7 @@ class GameRole(SQLModel, table=True): role_id: int = Field(foreign_key="role.id", primary_key=True) members: list[GamePlayer] = Relationship( back_populates="roles", + link_model=GamePlayerRole, sa_relationship_kwargs={ "primaryjoin": "GameRole.role_id == GamePlayerRole.role_id", "secondaryjoin": "GamePlayerRole.player_id == GamePlayer.player_id", diff --git a/realm_api/models/player.py b/realm_api/models/player.py index 81d5f50..750de0d 100644 --- a/realm_api/models/player.py +++ b/realm_api/models/player.py @@ -5,15 +5,17 @@ # local from .game import Game +from .game_player import GamePlayer class Player(SQLModel, table=True): user_id: str = Field(primary_key=True, max_length=32) name: str = Field(max_length=32) games: list[Game] = Relationship( - back_populates="players", + link_model=GamePlayer, sa_relationship_kwargs={ "primaryjoin": "Player.user_id == GamePlayer.player_id", "secondaryjoin": "GamePlayer.game_id == Game.id", + "overlaps": "game,player,players", }, ) diff --git a/realm_api/models/role.py b/realm_api/models/role.py index 599eae4..1a8e724 100644 --- a/realm_api/models/role.py +++ b/realm_api/models/role.py @@ -6,6 +6,9 @@ # 3rd party from sqlmodel import Field, Relationship, SQLModel +# local +from .game_player_role import GamePlayerRole + if TYPE_CHECKING: from .player import Player @@ -14,8 +17,10 @@ class Role(SQLModel, table=True): id: int = Field(primary_key=True) name: str = Field(max_length=32) players: list["Player"] = Relationship( + link_model=GamePlayerRole, sa_relationship_kwargs={ "primaryjoin": "Role.id == GamePlayerRole.role_id", - "secondaryjoin": "GamePlayerRole.id == Player.id", - } + "secondaryjoin": "GamePlayerRole.player_id == Player.user_id", + "overlaps": "members,roles", + }, ) From 276a00d24250ea95f3246ee40b15882bfed5f0d1 Mon Sep 17 00:00:00 2001 From: haliphax Date: Wed, 27 Aug 2025 23:17:13 -0500 Subject: [PATCH 14/14] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20async=20redis=20pubs?= =?UTF-8?q?ub?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .commitlintrc.cjs | 87 ++++++++++++++++++++++++++++++++++-- package-lock.json | 52 +++++++++++++++++++++ package.json | 1 + realm_api/api/auth/router.py | 4 +- realm_api/app.py | 3 ++ realm_api/rpc/__init__.py | 52 ++++++++++----------- 6 files changed, 165 insertions(+), 34 deletions(-) diff --git a/.commitlintrc.cjs b/.commitlintrc.cjs index 40efcad..50115a7 100644 --- a/.commitlintrc.cjs +++ b/.commitlintrc.cjs @@ -1,12 +1,91 @@ +// npx gitmoji -l +const gitmojis = [ + "⏪️", + "♻️", + "♿️", + "⚗️", + "⚡️", + "⚰️", + "✅", + "✏️", + "✨", + "➕", + "➖", + "⬆️", + "⬇️", + "🌐", + "🌱", + "🍱", + "🍻", + "🎉", + "🎨", + "🏗️", + "🏷️", + "🐛", + "👔", + "👥", + "👷", + "👽️", + "💄", + "💚", + "💡", + "💥", + "💩", + "💫", + "💬", + "💸", + "📄", + "📈", + "📌", + "📝", + "📦️", + "📱", + "📸", + "🔀", + "🔇", + "🔊", + "🔍️", + "🔐", + "🔒️", + "🔖", + "🔥", + "🔧", + "🔨", + "🗃️", + "🗑️", + "🙈", + "🚀", + "🚑️", + "🚚", + "🚧", + "🚨", + "🚩", + "🚸", + "🛂", + "🤡", + "🥅", + "🥚", + "🦺", + "🧐", + "🧑‍💻", + "🧪", + "🧱", + "🧵", + "🩹", + "🩺", +]; + module.exports = { - extends: ["gitmoji"], + extends: ["@commitlint/config-conventional"], parserPreset: { parserOpts: { - headerPattern: /^[^ ]+ (.*)$/, - headerCorrespondence: ["subject"], + headerPattern: /^([^ ]+) (.*)$/, + headerCorrespondence: ["type", "subject"], }, }, rules: { - "type-empty": [0, "always"], + "type-enum": [2, "always", gitmojis], + "type-case": [0, "always", "lower-case"], + "subject-full-stop": [2, "never", "."], }, }; diff --git a/package-lock.json b/package-lock.json index 784673b..6eefa1c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,6 +5,7 @@ "packages": { "": { "devDependencies": { + "@commitlint/config-conventional": "^19.8.1", "@semantic-release/changelog": "^6.0.3", "@semantic-release/git": "^10.0.1", "@semantic-release/github": "^11.0.1", @@ -123,6 +124,34 @@ "node": ">=v14" } }, + "node_modules/@commitlint/config-conventional": { + "version": "19.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-19.8.1.tgz", + "integrity": "sha512-/AZHJL6F6B/G959CsMAzrPKKZjeEiAVifRyEwXxcT6qtqbPwGw+iQxmNS+Bu+i09OCtdNRW6pNpBvgPrtMr9EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^19.8.1", + "conventional-changelog-conventionalcommits": "^7.0.2" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/config-conventional/node_modules/@commitlint/types": { + "version": "19.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/types/-/types-19.8.1.tgz", + "integrity": "sha512-/yCrWGCoA1SVKOks25EGadP9Pnj0oAIHGpl2wH2M2Y46dPM2ueb8wyCVOD7O3WCTkaJ0IkKvzhl1JY7+uCT2Dw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/conventional-commits-parser": "^5.0.0", + "chalk": "^5.3.0" + }, + "engines": { + "node": ">=v18" + } + }, "node_modules/@commitlint/config-validator": { "version": "17.8.1", "resolved": "https://registry.npmjs.org/@commitlint/config-validator/-/config-validator-17.8.1.tgz", @@ -1647,6 +1676,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/conventional-commits-parser": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@types/conventional-commits-parser/-/conventional-commits-parser-5.0.1.tgz", + "integrity": "sha512-7uz5EHdzz2TqoMfV7ee61Egf5y6NkcO4FB/1iCCQnbeiI1F3xzv3vK5dBCXUCLQgGYS+mUeigK1iKQzvED+QnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/http-cache-semantics": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", @@ -2692,6 +2731,19 @@ "node": ">=18" } }, + "node_modules/conventional-changelog-conventionalcommits": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-7.0.2.tgz", + "integrity": "sha512-NKXYmMR/Hr1DevQegFB4MwfM5Vv0m4UIxKZTTYuD98lpTknaZlSRrDOG4X7wIXpGkfsYxZTghUN+Qq+T0YQI7w==", + "dev": true, + "license": "ISC", + "dependencies": { + "compare-func": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/conventional-changelog-writer": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-8.0.1.tgz", diff --git a/package.json b/package.json index 066d009..e05a9f9 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,6 @@ { "devDependencies": { + "@commitlint/config-conventional": "^19.8.1", "@semantic-release/changelog": "^6.0.3", "@semantic-release/git": "^10.0.1", "@semantic-release/github": "^11.0.1", diff --git a/realm_api/api/auth/router.py b/realm_api/api/auth/router.py index 9e92228..34cb227 100644 --- a/realm_api/api/auth/router.py +++ b/realm_api/api/auth/router.py @@ -78,7 +78,7 @@ async def shared_guilds( discord = DiscordClient(session.user_id, session.discord_token) my_guilds = await discord.get_guilds() - if cached := redis_conn.get("bot.guilds"): + if cached := await redis_conn.get("bot.guilds"): bot_guilds = ( BotGuildsResponse.model_validate_json(cached) # type: ignore ) @@ -86,7 +86,7 @@ async def shared_guilds( bot_guilds = BotGuildsResponse.model_validate_json( await rpc_bot("guilds") ) - redis_conn.setex( + await redis_conn.setex( "bot.guilds", CACHE_EXPIRY, bot_guilds.model_dump_json() ) diff --git a/realm_api/app.py b/realm_api/app.py index 3504035..c78edd6 100644 --- a/realm_api/app.py +++ b/realm_api/app.py @@ -10,6 +10,7 @@ # local from .api import router from .db import init_db +from .rpc import init_pubsub, shutdown_pubsub @asynccontextmanager @@ -17,7 +18,9 @@ async def lifespan(app: FastAPI): """Lifespan function: startup -> yield -> shutdown""" await init_db() + task = await init_pubsub() yield + await shutdown_pubsub(task) app = FastAPI(lifespan=lifespan) diff --git a/realm_api/rpc/__init__.py b/realm_api/rpc/__init__.py index d39d1c4..1f6d046 100644 --- a/realm_api/rpc/__init__.py +++ b/realm_api/rpc/__init__.py @@ -7,19 +7,30 @@ from uuid import uuid4 # 3rd party -import redis +from redis.asyncio import StrictRedis # local from realm_api.logging import logger from .roll import roll_handler -redis_conn = redis.StrictRedis(host=os.environ.get("REDIS_HOST", "localhost")) +redis_conn = StrictRedis(host=os.environ.get("REDIS_HOST", "localhost")) pubsub = redis_conn.pubsub(ignore_subscribe_messages=True) handlers = { "roll": roll_handler, } +async def init_pubsub() -> aio.Task: + await pubsub.subscribe(**{"rpc.api": handler}) + return aio.create_task(pubsub.run()) + + +async def shutdown_pubsub(task: aio.Task): + pubsub.unsubscribe() + await pubsub.close() + task.cancel() + + def handler(message: dict): """Handle an incoming RPC operation and publish the result.""" @@ -37,37 +48,22 @@ async def rpc_bot(op: str, *args, timeout=3, **kwargs): q = aio.Queue() - def handler(message: dict): + async def handler(message: dict): data = message["data"] - aio.new_event_loop().run_until_complete(q.put(data)) + await q.put(data) uuid = str(uuid4()) - pubsub.subscribe(**{uuid: handler}) + await pubsub.subscribe(**{uuid: handler}) + message = { + "uuid": uuid, + "op": op, + "args": args, + "kwargs": kwargs, + } try: - redis_conn.publish( - "rpc.bot", - json.dumps( - { - "uuid": uuid, - "op": op, - "args": args, - "kwargs": kwargs, - } - ), - ) - + await redis_conn.publish("rpc.bot", json.dumps(message)) return await aio.wait_for(q.get(), timeout) finally: - pubsub.unsubscribe(uuid) - - -def setup_webapp(*_): - pubsub.subscribe(**{"rpc.api": handler}) - pubsub.run_in_thread(daemon=True, sleep_time=0.01) - - -def teardown_webapp(*_): - pubsub.unsubscribe() - pubsub.close() + await pubsub.unsubscribe(uuid)