Document every API response body in the OpenAPI spec
CI / Build and push docker image (push) Successful in 6m2s
CI / Build and push docker image (push) Successful in 6m2s
kaya-openapi 0.0.8 supports named component schemas, so the routes' description-only responses become $refs: schemas for the core payloads (auth providers, lobby, game view, ongoing games, match history, leaderboard, ratings) live in tavolo.platform.openapi and are registered through the mixin's components. Engine-defined parts (options echo, game views, match results) stay open objects on purpose. Error responses now carry the error envelope schema, and a new suite sweeps the document to keep every /api response typed.
This commit is contained in:
@@ -46,6 +46,7 @@ from tavolo.briscola import (
|
||||
)
|
||||
from tavolo.platform import GameRegistry, Platform, PlatformMixin
|
||||
from tavolo.platform.deadlines import DeadlineScheduler, DeadlineSchedulerMixin
|
||||
from tavolo.platform.openapi import SCHEMAS
|
||||
from tavolo.platform.store import GameStore, InMemoryGameStore, RedisGameStore
|
||||
from tavolo.platform.tortoise_mixin import TortoiseMixin
|
||||
from tavolo.scopa import ScopaEngine, ScopaQuattroEngine
|
||||
@@ -218,6 +219,7 @@ openapi_mixin = OpenAPIMixin(
|
||||
description="Multiplayer card-game platform API",
|
||||
spec_path="/api/openapi.json",
|
||||
docs_path="/api/docs",
|
||||
components={"schemas": SCHEMAS},
|
||||
)
|
||||
tortoise_mixin = TortoiseMixin(
|
||||
database_url=settings.database_url,
|
||||
|
||||
@@ -43,8 +43,8 @@ itself ships no game.
|
||||
history: `python -m tavolo.platform.backfill_elo --database-url postgres://…`.
|
||||
- `auth.py`, `http.py`, `pagination.py`, `openapi.py`,
|
||||
`tortoise_mixin.py` — OIDC helpers, JSON helpers, keyset and
|
||||
page-number pagination, shared OpenAPI fragments, the TortoiseORM
|
||||
lifecycle mixin.
|
||||
page-number pagination, shared OpenAPI fragments and response schemas,
|
||||
the TortoiseORM lifecycle mixin.
|
||||
|
||||
## Adding a game
|
||||
|
||||
|
||||
@@ -1,7 +1,23 @@
|
||||
"""Shared OpenAPI fragments for the ``@operation`` decorators in ``routes/``."""
|
||||
"""Shared OpenAPI fragments for the ``@operation`` decorators in ``routes/``.
|
||||
|
||||
Two kinds of fragment live here:
|
||||
|
||||
* parameter lists (``PAGINATION_PARAMETERS``, ``PAGE_PARAMETERS``), used
|
||||
directly in the route decorators, and
|
||||
* reusable response schemas, registered as ``components.schemas`` by the
|
||||
application (and the test fixtures) through ``SCHEMAS`` and referenced
|
||||
from the routes with :func:`kaya.openapi.schema_ref`.
|
||||
|
||||
Engine-defined payload parts (lobby options echo, game views, match
|
||||
results) are intentionally typed as open objects: the platform is
|
||||
game-agnostic, so their exact shape belongs to each engine, not to this
|
||||
document.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, List
|
||||
from typing import Any, Dict, List, Optional, Sequence
|
||||
|
||||
from kaya.openapi import schema_ref
|
||||
|
||||
PAGINATION_PARAMETERS: List[Dict[str, Any]] = [
|
||||
{
|
||||
@@ -38,3 +54,321 @@ PAGE_PARAMETERS: List[Dict[str, Any]] = [
|
||||
"description": "Maximum number of results per page (clamped to [1, 100]).",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Schema builders
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _object(
|
||||
properties: Dict[str, Any],
|
||||
required: Sequence[str] = (),
|
||||
*,
|
||||
description: Optional[str] = None,
|
||||
additional_properties: bool = False,
|
||||
) -> Dict[str, Any]:
|
||||
"""An object schema with the given properties (JSON Schema 2020-12)."""
|
||||
schema: Dict[str, Any] = {"type": "object", "properties": properties}
|
||||
if required:
|
||||
schema["required"] = list(required)
|
||||
if description is not None:
|
||||
schema["description"] = description
|
||||
if additional_properties:
|
||||
schema["additionalProperties"] = True
|
||||
return schema
|
||||
|
||||
|
||||
def _free_object(description: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""An open object: used for engine-specific payload parts."""
|
||||
schema: Dict[str, Any] = {"type": "object", "additionalProperties": True}
|
||||
if description is not None:
|
||||
schema["description"] = description
|
||||
return schema
|
||||
|
||||
|
||||
def _array(items: Dict[str, Any]) -> Dict[str, Any]:
|
||||
return {"type": "array", "items": items}
|
||||
|
||||
|
||||
def _nullable(schema: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""A copy of ``schema`` that also accepts ``null`` (OpenAPI 3.1 types)."""
|
||||
nullable = dict(schema)
|
||||
kind = nullable.get("type")
|
||||
if isinstance(kind, str):
|
||||
nullable["type"] = [kind, "null"]
|
||||
return nullable
|
||||
|
||||
|
||||
def json_response(description: str, schema: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""A JSON response object carrying ``schema``, for ``@operation``."""
|
||||
return {
|
||||
"description": description,
|
||||
"content": {"application/json": {"schema": schema}},
|
||||
}
|
||||
|
||||
|
||||
def error_response(description: str) -> Dict[str, Any]:
|
||||
"""The standard JSON error envelope (``{"error": <message>}``)."""
|
||||
return json_response(description, schema_ref("Error"))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Component schemas
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
ERROR_SCHEMA = _object({"error": {"type": "string"}}, ("error",))
|
||||
|
||||
HEALTH_SCHEMA = _object(
|
||||
{"status": {"type": "string", "enum": ["ok"]}}, ("status",)
|
||||
)
|
||||
|
||||
ME_SCHEMA = _object(
|
||||
{
|
||||
"sub": {"type": "string"},
|
||||
"name": {"type": "string"},
|
||||
},
|
||||
("sub", "name"),
|
||||
)
|
||||
|
||||
AUTH_PROVIDER_SCHEMA = _object(
|
||||
{
|
||||
"name": {"type": "string"},
|
||||
"login_url": {"type": "string"},
|
||||
},
|
||||
("name", "login_url"),
|
||||
)
|
||||
|
||||
AUTH_PROVIDERS_PAGE_SCHEMA = _object(
|
||||
{"providers": _array(schema_ref("AuthProvider"))}, ("providers",)
|
||||
)
|
||||
|
||||
GAME_TYPE_SCHEMA = _object(
|
||||
{
|
||||
"id": {"type": "string"},
|
||||
"name": {"type": "string"},
|
||||
"description": {"type": "string"},
|
||||
"min_players": {"type": "integer"},
|
||||
"max_players": {"type": "integer"},
|
||||
"options_schema": _free_object(
|
||||
"JSON-schema fragment describing the game-specific creation "
|
||||
"options accepted by POST /api/games."
|
||||
),
|
||||
},
|
||||
("id", "name", "description", "min_players", "max_players", "options_schema"),
|
||||
)
|
||||
|
||||
GAME_TYPES_PAGE_SCHEMA = _object(
|
||||
{"results": _array(schema_ref("GameType"))}, ("results",)
|
||||
)
|
||||
|
||||
LOBBY_PLAYER_SCHEMA = _object(
|
||||
{
|
||||
"sub": {"type": "string"},
|
||||
"name": {"type": "string"},
|
||||
"seat": {"type": "integer"},
|
||||
"team": _nullable({
|
||||
"type": "string",
|
||||
"description": "Game-defined team label; null for games without fixed teams.",
|
||||
}),
|
||||
},
|
||||
("sub", "name", "seat", "team"),
|
||||
)
|
||||
|
||||
_LOBBY_PROPERTIES: Dict[str, Any] = {
|
||||
"id": {"type": "string"},
|
||||
"join_code": {"type": "string"},
|
||||
"game_type": {"type": "string"},
|
||||
"players": _array(schema_ref("LobbyPlayer")),
|
||||
"seats_open": {"type": "integer"},
|
||||
}
|
||||
|
||||
_LOBBY_REQUIRED = ("id", "join_code", "game_type", "players", "seats_open")
|
||||
|
||||
LOBBY_SCHEMA = _object(
|
||||
dict(_LOBBY_PROPERTIES),
|
||||
_LOBBY_REQUIRED,
|
||||
description="The lobby payload: the platform envelope plus the engine's "
|
||||
"lobby_view fields (the creation-options echo and the phase).",
|
||||
additional_properties=True,
|
||||
)
|
||||
|
||||
GAME_PREVIEW_SCHEMA = _object(
|
||||
{
|
||||
**_LOBBY_PROPERTIES,
|
||||
"game_name": {"type": "string"},
|
||||
"game_description": {"type": "string"},
|
||||
},
|
||||
(*_LOBBY_REQUIRED, "game_name", "game_description"),
|
||||
description="The join-code preview: the lobby payload plus the game's "
|
||||
"display name and description.",
|
||||
additional_properties=True,
|
||||
)
|
||||
|
||||
GAME_VIEW_SCHEMA = _object(
|
||||
{
|
||||
"id": {"type": "string"},
|
||||
"game_type": {"type": "string"},
|
||||
"spectator": {"type": "boolean"},
|
||||
"seats_open": {"type": "integer"},
|
||||
"join_code": {
|
||||
"type": "string",
|
||||
"description": "The join code is a credential: present only in a "
|
||||
"seated player's view, never to spectators.",
|
||||
},
|
||||
},
|
||||
("id", "game_type", "spectator", "seats_open"),
|
||||
description="A game snapshot: the platform envelope plus the engine's "
|
||||
"view_for fields. Seated players get their personalized view "
|
||||
"(own hand, join code); everyone else the read-only spectator "
|
||||
"view (no hands, no join code).",
|
||||
additional_properties=True,
|
||||
)
|
||||
|
||||
ONGOING_GAME_SCHEMA = _object(
|
||||
{
|
||||
"id": {"type": "string"},
|
||||
"game_type": {"type": "string"},
|
||||
"players": _array(schema_ref("LobbyPlayer")),
|
||||
"seats_open": {"type": "integer"},
|
||||
"game_name": {"type": "string"},
|
||||
"game_description": {"type": "string"},
|
||||
"your_seat": _nullable({"type": "integer"}),
|
||||
"created_at": _nullable({"type": "string", "format": "date-time"}),
|
||||
},
|
||||
(
|
||||
"id",
|
||||
"game_type",
|
||||
"players",
|
||||
"seats_open",
|
||||
"game_name",
|
||||
"game_description",
|
||||
"your_seat",
|
||||
"created_at",
|
||||
),
|
||||
description="One entry of the caller's ongoing-games list: the lobby "
|
||||
"payload without the join code, plus the game name and the "
|
||||
"caller's seat.",
|
||||
additional_properties=True,
|
||||
)
|
||||
|
||||
ONGOING_GAMES_PAGE_SCHEMA = _object(
|
||||
{"results": _array(schema_ref("OngoingGame"))}, ("results",)
|
||||
)
|
||||
|
||||
MATCH_PLAYER_SCHEMA = _object(
|
||||
{
|
||||
"user_sub": {"type": "string"},
|
||||
"display_name": {"type": "string"},
|
||||
"seat": {"type": "integer"},
|
||||
"team": _nullable({"type": "string"}),
|
||||
"won": {"type": "boolean"},
|
||||
"score": {"type": "number"},
|
||||
"elo_delta": _nullable({"type": "integer"}),
|
||||
"details": _nullable(_free_object("Game-specific extras reported by the engine.")),
|
||||
},
|
||||
(
|
||||
"user_sub",
|
||||
"display_name",
|
||||
"seat",
|
||||
"team",
|
||||
"won",
|
||||
"score",
|
||||
"elo_delta",
|
||||
"details",
|
||||
),
|
||||
)
|
||||
|
||||
MATCH_SCHEMA = _object(
|
||||
{
|
||||
"id": {"type": "string"},
|
||||
"game_type": {"type": "string"},
|
||||
"result": _nullable(_free_object(
|
||||
"The game-specific outcome reported by the engine."
|
||||
)),
|
||||
"started_at": {"type": "string", "format": "date-time"},
|
||||
"finished_at": {"type": "string", "format": "date-time"},
|
||||
"you_won": {"type": "boolean"},
|
||||
"your_elo_delta": _nullable({"type": "integer"}),
|
||||
"players": _array(schema_ref("MatchPlayer")),
|
||||
},
|
||||
(
|
||||
"id",
|
||||
"game_type",
|
||||
"result",
|
||||
"started_at",
|
||||
"finished_at",
|
||||
"you_won",
|
||||
"your_elo_delta",
|
||||
"players",
|
||||
),
|
||||
)
|
||||
|
||||
MATCHES_PAGE_SCHEMA = _object(
|
||||
{
|
||||
"results": _array(schema_ref("Match")),
|
||||
"next_cursor": _nullable({"type": "string"}),
|
||||
},
|
||||
("results", "next_cursor"),
|
||||
)
|
||||
|
||||
LEADERBOARD_ENTRY_SCHEMA = _object(
|
||||
{
|
||||
"user_sub": {"type": "string"},
|
||||
"display_name": {"type": "string"},
|
||||
"elo": {"type": "integer"},
|
||||
"matches": {"type": "integer"},
|
||||
"wins": {"type": "integer"},
|
||||
"points": {"type": "number"},
|
||||
},
|
||||
("user_sub", "display_name", "elo", "matches", "wins", "points"),
|
||||
)
|
||||
|
||||
LEADERBOARD_PAGE_SCHEMA = _object(
|
||||
{
|
||||
"results": _array(schema_ref("LeaderboardEntry")),
|
||||
"page": {"type": "integer"},
|
||||
"pages": {"type": "integer"},
|
||||
"total": {"type": "integer"},
|
||||
"your_rank": _nullable({"type": "integer"}),
|
||||
"your_page": _nullable({"type": "integer"}),
|
||||
},
|
||||
("results", "page", "pages", "total", "your_rank", "your_page"),
|
||||
)
|
||||
|
||||
RATING_SCHEMA = _object(
|
||||
{
|
||||
"game_type": {"type": "string"},
|
||||
"rating": {"type": "integer"},
|
||||
"matches_played": {"type": "integer"},
|
||||
},
|
||||
("game_type", "rating", "matches_played"),
|
||||
)
|
||||
|
||||
RATINGS_PAGE_SCHEMA = _object(
|
||||
{"results": _array(schema_ref("Rating"))}, ("results",)
|
||||
)
|
||||
|
||||
#: Component name -> schema, passed to
|
||||
#: ``OpenAPIMixin(components={"schemas": SCHEMAS})``.
|
||||
SCHEMAS: Dict[str, Dict[str, Any]] = {
|
||||
"Error": ERROR_SCHEMA,
|
||||
"Health": HEALTH_SCHEMA,
|
||||
"Me": ME_SCHEMA,
|
||||
"AuthProvider": AUTH_PROVIDER_SCHEMA,
|
||||
"AuthProvidersPage": AUTH_PROVIDERS_PAGE_SCHEMA,
|
||||
"GameType": GAME_TYPE_SCHEMA,
|
||||
"GameTypesPage": GAME_TYPES_PAGE_SCHEMA,
|
||||
"LobbyPlayer": LOBBY_PLAYER_SCHEMA,
|
||||
"Lobby": LOBBY_SCHEMA,
|
||||
"GamePreview": GAME_PREVIEW_SCHEMA,
|
||||
"GameView": GAME_VIEW_SCHEMA,
|
||||
"OngoingGame": ONGOING_GAME_SCHEMA,
|
||||
"OngoingGamesPage": ONGOING_GAMES_PAGE_SCHEMA,
|
||||
"MatchPlayer": MATCH_PLAYER_SCHEMA,
|
||||
"Match": MATCH_SCHEMA,
|
||||
"MatchesPage": MATCHES_PAGE_SCHEMA,
|
||||
"LeaderboardEntry": LEADERBOARD_ENTRY_SCHEMA,
|
||||
"LeaderboardPage": LEADERBOARD_PAGE_SCHEMA,
|
||||
"Rating": RATING_SCHEMA,
|
||||
"RatingsPage": RATINGS_PAGE_SCHEMA,
|
||||
}
|
||||
|
||||
@@ -16,9 +16,10 @@ from __future__ import annotations
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from kaya.core import HttpContext, KayaApp
|
||||
from kaya.openapi import operation
|
||||
from kaya.openapi import operation, schema_ref
|
||||
|
||||
from ..http import send_json
|
||||
from ..openapi import json_response
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..mixin import Platform
|
||||
@@ -36,7 +37,8 @@ def register(app: KayaApp, platform: "Platform") -> None:
|
||||
description="Names and login URLs of the configured OIDC "
|
||||
"providers, in configuration order.",
|
||||
tags=["auth"],
|
||||
responses={200: {"description": "The configured providers"}})
|
||||
responses={200: json_response("The configured providers",
|
||||
schema_ref("AuthProvidersPage"))})
|
||||
async def auth_providers(ctx: HttpContext) -> None:
|
||||
await send_json(
|
||||
ctx,
|
||||
|
||||
@@ -19,7 +19,7 @@ from logging import getLogger
|
||||
from typing import TYPE_CHECKING, Any, Dict, Optional
|
||||
|
||||
from kaya.core import HttpContext, KayaApp
|
||||
from kaya.openapi import operation
|
||||
from kaya.openapi import operation, schema_ref
|
||||
|
||||
from ..auth import current_user, display_name, require_auth
|
||||
from ..engine import GameSession, Seat
|
||||
@@ -32,6 +32,7 @@ from ..http import (
|
||||
send_error,
|
||||
send_json,
|
||||
)
|
||||
from ..openapi import error_response, json_response
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..mixin import Platform
|
||||
@@ -144,7 +145,8 @@ def register(app: KayaApp, platform: "Platform") -> None:
|
||||
"describes the per-game creation options accepted "
|
||||
"by POST /api/games.",
|
||||
tags=["games"],
|
||||
responses={200: {"description": "The available game types"}})
|
||||
responses={200: json_response("The available game types",
|
||||
schema_ref("GameTypesPage"))})
|
||||
async def list_game_types(ctx: HttpContext) -> None:
|
||||
await send_json(ctx, 200, {
|
||||
"results": [
|
||||
@@ -194,9 +196,9 @@ def register(app: KayaApp, platform: "Platform") -> None:
|
||||
},
|
||||
},
|
||||
responses={
|
||||
201: {"description": "The created lobby"},
|
||||
400: {"description": "Invalid game_type, options or body"},
|
||||
401: {"description": "Authentication required"},
|
||||
201: json_response("The created lobby", schema_ref("Lobby")),
|
||||
400: error_response("Invalid game_type, options or body"),
|
||||
401: error_response("Authentication required"),
|
||||
})
|
||||
@require_auth(platform.oidc)
|
||||
async def create_game(ctx: HttpContext) -> None:
|
||||
@@ -264,11 +266,15 @@ def register(app: KayaApp, platform: "Platform") -> None:
|
||||
},
|
||||
},
|
||||
responses={
|
||||
200: {"description": "Seated; game state (may be playing)"},
|
||||
400: {"description": "Missing code"},
|
||||
401: {"description": "Authentication required"},
|
||||
404: {"description": "Unknown join code"},
|
||||
409: {"description": "Already joined or lobby full"},
|
||||
200: json_response(
|
||||
"Seated: the lobby, or the personalized game state "
|
||||
"when the join started the match",
|
||||
{"anyOf": [schema_ref("Lobby"), schema_ref("GameView")]},
|
||||
),
|
||||
400: error_response("Missing code"),
|
||||
401: error_response("Authentication required"),
|
||||
404: error_response("Unknown join code"),
|
||||
409: error_response("Already joined or lobby full"),
|
||||
})
|
||||
@require_auth(platform.oidc)
|
||||
async def join_game(ctx: HttpContext) -> None:
|
||||
@@ -328,10 +334,11 @@ def register(app: KayaApp, platform: "Platform") -> None:
|
||||
"its engine.",
|
||||
tags=["games"],
|
||||
responses={
|
||||
200: {"description": "The lobby preview"},
|
||||
400: {"description": "Missing code"},
|
||||
401: {"description": "Authentication required"},
|
||||
404: {"description": "Unknown join code"},
|
||||
200: json_response("The lobby preview",
|
||||
schema_ref("GamePreview")),
|
||||
400: error_response("Missing code"),
|
||||
401: error_response("Authentication required"),
|
||||
404: error_response("Unknown join code"),
|
||||
})
|
||||
@require_auth(platform.oidc)
|
||||
async def preview_game(ctx: HttpContext) -> None:
|
||||
@@ -356,9 +363,11 @@ def register(app: KayaApp, platform: "Platform") -> None:
|
||||
"join code, just what is public at the table.",
|
||||
tags=["games"],
|
||||
responses={
|
||||
200: {"description": "The personalized (player) or "
|
||||
"read-only (spectator) game state"},
|
||||
404: {"description": "Game not found"},
|
||||
200: json_response(
|
||||
"The personalized (player) or read-only (spectator) game state",
|
||||
schema_ref("GameView"),
|
||||
),
|
||||
404: error_response("Game not found"),
|
||||
})
|
||||
async def get_game(ctx: HttpContext, game_id: str) -> None:
|
||||
session = await platform.game_store.load(game_id)
|
||||
@@ -378,8 +387,9 @@ def register(app: KayaApp, platform: "Platform") -> None:
|
||||
"list. Finished matches are not included.",
|
||||
tags=["games"],
|
||||
responses={
|
||||
200: {"description": "The caller's ongoing games"},
|
||||
401: {"description": "Authentication required"},
|
||||
200: json_response("The caller's ongoing games",
|
||||
schema_ref("OngoingGamesPage")),
|
||||
401: error_response("Authentication required"),
|
||||
})
|
||||
@require_auth(platform.oidc)
|
||||
async def my_games(ctx: HttpContext) -> None:
|
||||
|
||||
@@ -4,7 +4,9 @@ from __future__ import annotations
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from kaya.core import HttpContext, KayaApp
|
||||
from kaya.openapi import operation
|
||||
from kaya.openapi import operation, schema_ref
|
||||
|
||||
from ..openapi import json_response
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..mixin import Platform
|
||||
@@ -14,7 +16,7 @@ def register(app: KayaApp, platform: "Platform") -> None:
|
||||
@app.GET("/api/health")
|
||||
@operation(summary="Health check",
|
||||
tags=["health"],
|
||||
responses={200: {"description": "The service is up"}})
|
||||
responses={200: json_response("The service is up", schema_ref("Health"))})
|
||||
async def health(ctx: HttpContext) -> None:
|
||||
await ctx.send_bytes(
|
||||
200,
|
||||
|
||||
@@ -4,10 +4,11 @@ from __future__ import annotations
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from kaya.core import HttpContext, KayaApp
|
||||
from kaya.openapi import operation
|
||||
from kaya.openapi import operation, schema_ref
|
||||
|
||||
from ..auth import current_user, display_name, require_auth
|
||||
from ..http import send_json
|
||||
from ..openapi import error_response, json_response
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..mixin import Platform
|
||||
@@ -20,8 +21,8 @@ def register(app: KayaApp, platform: "Platform") -> None:
|
||||
"session; 401 when not logged in.",
|
||||
tags=["auth"],
|
||||
responses={
|
||||
200: {"description": "The current user"},
|
||||
401: {"description": "Not logged in"},
|
||||
200: json_response("The current user", schema_ref("Me")),
|
||||
401: error_response("Not logged in"),
|
||||
})
|
||||
@require_auth(platform.oidc)
|
||||
async def me(ctx: HttpContext) -> None:
|
||||
|
||||
@@ -13,12 +13,17 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple
|
||||
from uuid import UUID
|
||||
|
||||
from kaya.core import HttpContext, KayaApp
|
||||
from kaya.openapi import operation
|
||||
from kaya.openapi import operation, schema_ref
|
||||
|
||||
from ..auth import current_user, require_auth
|
||||
from ..http import extract_query_params, send_error, send_json
|
||||
from ..models import Match, MatchPlayer, PlayerRating
|
||||
from ..openapi import PAGE_PARAMETERS, PAGINATION_PARAMETERS
|
||||
from ..openapi import (
|
||||
PAGE_PARAMETERS,
|
||||
PAGINATION_PARAMETERS,
|
||||
error_response,
|
||||
json_response,
|
||||
)
|
||||
from ..pagination import (
|
||||
CursorDecodeError,
|
||||
PageParamsError,
|
||||
@@ -131,9 +136,10 @@ def register(app: KayaApp, platform: "Platform") -> None:
|
||||
tags=["stats"],
|
||||
parameters=[*PAGINATION_PARAMETERS, GAME_TYPE_PARAMETER],
|
||||
responses={
|
||||
200: {"description": "A page of matches"},
|
||||
400: {"description": "Invalid pagination cursor or game_type"},
|
||||
401: {"description": "Authentication required"},
|
||||
200: json_response("A page of matches",
|
||||
schema_ref("MatchesPage")),
|
||||
400: error_response("Invalid pagination cursor or game_type"),
|
||||
401: error_response("Authentication required"),
|
||||
})
|
||||
@require_auth(platform.oidc)
|
||||
async def my_matches(ctx: HttpContext) -> None:
|
||||
@@ -186,8 +192,9 @@ def register(app: KayaApp, platform: "Platform") -> None:
|
||||
LEADERBOARD_FOCUS_PARAMETER,
|
||||
],
|
||||
responses={
|
||||
200: {"description": "A page of the leaderboard"},
|
||||
400: {"description": "Unknown game_type or invalid parameters"},
|
||||
200: json_response("A page of the leaderboard",
|
||||
schema_ref("LeaderboardPage")),
|
||||
400: error_response("Unknown game_type or invalid parameters"),
|
||||
})
|
||||
async def leaderboard(ctx: HttpContext) -> None:
|
||||
game_type, error = _parse_game_type(platform, ctx.query_string)
|
||||
@@ -261,8 +268,9 @@ def register(app: KayaApp, platform: "Platform") -> None:
|
||||
"type they have played.",
|
||||
tags=["stats"],
|
||||
responses={
|
||||
200: {"description": "The caller's ratings"},
|
||||
401: {"description": "Authentication required"},
|
||||
200: json_response("The caller's ratings",
|
||||
schema_ref("RatingsPage")),
|
||||
401: error_response("Authentication required"),
|
||||
})
|
||||
@require_auth(platform.oidc)
|
||||
async def my_ratings(ctx: HttpContext) -> None:
|
||||
|
||||
@@ -45,6 +45,7 @@ from tavolo.platform import (
|
||||
)
|
||||
from tavolo.platform.auth import get_ws_user # noqa: F401 (re-exported for patching)
|
||||
from tavolo.platform.deadlines import DeadlineScheduler
|
||||
from tavolo.platform.openapi import SCHEMAS
|
||||
from tavolo.platform.registry import GameRegistry
|
||||
from tavolo.platform.store import InMemoryGameStore
|
||||
from tavolo.platform.tortoise_mixin import TortoiseMixin
|
||||
@@ -253,6 +254,7 @@ def make_platform(
|
||||
description="test app",
|
||||
spec_path="/api/openapi.json",
|
||||
docs_path="/api/docs",
|
||||
components={"schemas": SCHEMAS},
|
||||
)
|
||||
scheduler = DeadlineScheduler(
|
||||
game_store,
|
||||
|
||||
Reference in New Issue
Block a user