From 8044c03c793c9002d314a47487c5559c117c57eb Mon Sep 17 00:00:00 2001 From: Seungpyo1007 Date: Thu, 17 Sep 2026 21:34:50 +0900 Subject: [PATCH] chore: drop the game category (moved to GetTechAPI/game-catalog) Games were 962,384 of TechAPI's 1.11M records and referenced nothing, so they carried 86% of the dump cost for a leaf category: over five hours to generate and pull requests GitHub could neither render nor merge. The records now live in GetTechAPI/game-catalog with their import history intact, validated there (962,384 records, 0 errors) and published at https://gettechapi.github.io/game-catalog/. Removes the model, schema, router, serializer, seed and validation paths, plus the Pages workaround that had to skip games at dump time. Refs #1 --- .github/workflows/deploy-pages.yml | 7 +--- app/dump.py | 3 +- app/main.py | 2 - app/models/__init__.py | 2 - app/models/game.py | 53 ------------------------- app/routers/games.py | 63 ------------------------------ app/schemas/__init__.py | 2 - app/schemas/game.py | 33 ---------------- app/schemas/serializers.py | 29 -------------- app/seed.py | 11 ------ app/validate.py | 20 ---------- tests/integration/game_fixtures.py | 40 ------------------- tests/integration/test_dump.py | 6 +-- tests/integration/test_games.py | 38 ------------------ tests/unit/test_stable_ids.py | 2 +- 15 files changed, 6 insertions(+), 305 deletions(-) delete mode 100644 app/models/game.py delete mode 100644 app/routers/games.py delete mode 100644 app/schemas/game.py delete mode 100644 tests/integration/game_fixtures.py delete mode 100644 tests/integration/test_games.py diff --git a/.github/workflows/deploy-pages.yml b/.github/workflows/deploy-pages.yml index 2c1a95a..481e339 100644 --- a/.github/workflows/deploy-pages.yml +++ b/.github/workflows/deploy-pages.yml @@ -44,12 +44,7 @@ jobs: - name: Generate static JSON dump + openapi.json env: TECHAPI_DATA_DIR: ${{ github.workspace }}/TechAPI/data - # Games are not published here: ~962k per-record files exceed the fixed - # GitHub Pages deployment window (the deploy step times out at - # "syncing_files"). Skipping them at generation time also keeps this - # step from writing files that would only be deleted before upload. - # They remain available as versioned data in the TechAPI repo. - run: python -m app.dump --output dump --exclude games + run: python -m app.dump --output dump - uses: actions/setup-node@v4 with: diff --git a/app/dump.py b/app/dump.py index c20761c..9cc5bbb 100644 --- a/app/dump.py +++ b/app/dump.py @@ -31,7 +31,6 @@ "cpus", "laptops", "monitors", - "games", "software", "websites", ] @@ -151,7 +150,7 @@ def run(output_dir: Path = OUTPUT_DIR, exclude: list[str] | None = None) -> None default=[], metavar="COLLECTION", help=( - "collection to skip, repeatable (e.g. --exclude games). Useful when a " + "collection to skip, repeatable (e.g. --exclude software). Useful when a " "consumer does not publish a large collection: skipping it avoids " "writing hundreds of thousands of files that are discarded anyway." ), diff --git a/app/main.py b/app/main.py index ae7f479..d806451 100644 --- a/app/main.py +++ b/app/main.py @@ -17,7 +17,6 @@ from app.routers import ( brands, cpus, - games, gpus, laptops, meta, @@ -89,7 +88,6 @@ async def add_request_id( app.include_router(cpus.router, prefix=PREFIX) app.include_router(laptops.router, prefix=PREFIX) app.include_router(monitors.router, prefix=PREFIX) -app.include_router(games.router, prefix=PREFIX) app.include_router(software.router, prefix=PREFIX) app.include_router(websites.router, prefix=PREFIX) diff --git a/app/models/__init__.py b/app/models/__init__.py index b974ca6..414d9cd 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -5,7 +5,6 @@ from app.models.brand import Brand from app.models.cpu import CPU -from app.models.game import Game from app.models.gpu import DiscreteGPU from app.models.laptop import Laptop from app.models.mobile_device import PDA, Tablet, Watch @@ -25,6 +24,5 @@ "CPU", "Laptop", "Monitor", - "Game", "Software", ] diff --git a/app/models/game.py b/app/models/game.py deleted file mode 100644 index c01d586..0000000 --- a/app/models/game.py +++ /dev/null @@ -1,53 +0,0 @@ -"""Game model (§6.10). - -A video game. Unlike the hardware categories, a game references no Brand — its -makers are recorded as free-text ``developers`` / ``publishers`` lists (a game's -"brand" is its studio/publisher, which does not map onto the hardware Brand -catalogue). Games are unscored. -""" - -from __future__ import annotations - -from datetime import UTC, date, datetime - -from sqlalchemy import JSON, Column -from sqlmodel import Field, SQLModel - - -def _utcnow() -> datetime: - return datetime.now(UTC) - - -class Game(SQLModel, table=True): - """A video game title (e.g. The Witcher 3: Wild Hunt).""" - - __tablename__ = "games" - - id: int | None = Field(default=None, primary_key=True) - slug: str = Field(index=True, unique=True) - name: str - - release_date: date | None = None # None for TBA/unreleased titles - - # Ratings / reception - rating: float | None = None # 0-5 aggregate user rating - rating_count: int | None = None - metacritic: int | None = None # 0-100 - playtime_hours: int | None = None - - # Classification — stored as JSON string lists - platforms: list[str] = Field(default_factory=list, sa_column=Column(JSON)) - genres: list[str] = Field(default_factory=list, sa_column=Column(JSON)) - stores: list[str] = Field(default_factory=list, sa_column=Column(JSON)) - developers: list[str] = Field(default_factory=list, sa_column=Column(JSON)) - publishers: list[str] = Field(default_factory=list, sa_column=Column(JSON)) - tags: list[str] = Field(default_factory=list, sa_column=Column(JSON)) - esrb_rating: str | None = None - - background_image: str | None = None - - # Meta - verified: bool = False - source_urls: list[str] = Field(default_factory=list, sa_column=Column(JSON)) - created_at: datetime = Field(default_factory=_utcnow) - updated_at: datetime = Field(default_factory=_utcnow) diff --git a/app/routers/games.py b/app/routers/games.py deleted file mode 100644 index cb69752..0000000 --- a/app/routers/games.py +++ /dev/null @@ -1,63 +0,0 @@ -"""Game endpoints (§6.10). List + detail; games are unscored.""" - -from __future__ import annotations - -from typing import Annotated, Any - -from fastapi import APIRouter, Query -from sqlalchemy import func -from sqlmodel import select -from sqlmodel.sql.expression import SelectOfScalar - -from app.dependencies import PaginationDep, SessionDep -from app.errors import APIError, not_found -from app.models.game import Game -from app.routers.utils import build_ref_page -from app.schemas.common import Page, ResourceRef -from app.schemas.game import GameRead -from app.schemas.serializers import game_read, resource_ref - -router = APIRouter(prefix="/games", tags=["games"]) - -_SORT_FIELDS: dict[str, Any] = { - "name": Game.name, - "release_date": Game.release_date, - "rating": Game.rating, - "metacritic": Game.metacritic, -} - - -def _apply_sort(stmt: SelectOfScalar[Any], sort: str | None) -> SelectOfScalar[Any]: - if not sort: - return stmt.order_by(Game.name) - descending = sort.startswith("-") - field = sort[1:] if descending else sort - column = _SORT_FIELDS.get(field) - if column is None: - raise APIError(400, "INVALID_REQUEST", f"Cannot sort by '{field}'") - return stmt.order_by(column.desc() if descending else column.asc()) - - -@router.get("", summary="List games") -def list_games( - session: SessionDep, - pagination: PaginationDep, - sort: Annotated[str | None, Query()] = None, -) -> Page[ResourceRef]: - count = session.exec(select(func.count()).select_from(Game)).one() - list_stmt = _apply_sort(select(Game), sort).offset(pagination.offset).limit(pagination.limit) - rows = session.exec(list_stmt).all() - - refs = [resource_ref("games", row.slug, row.name) for row in rows] - applied = {k: v for k, v in (("sort", sort),) if v} - return build_ref_page( - refs, count=count, path="/v1/games", pagination=pagination, filters=applied - ) - - -@router.get("/{slug}", summary="Get a game") -def get_game(slug: str, session: SessionDep) -> GameRead: - game = session.exec(select(Game).where(Game.slug == slug)).first() - if game is None: - raise not_found("Game", slug) - return game_read(game) diff --git a/app/schemas/__init__.py b/app/schemas/__init__.py index d0fccf4..cd5da9e 100644 --- a/app/schemas/__init__.py +++ b/app/schemas/__init__.py @@ -2,7 +2,6 @@ from app.schemas.brand import BrandRead, BrandSummary from app.schemas.common import ErrorBody, ErrorResponse, Page, ResourceRef -from app.schemas.game import GameRead from app.schemas.laptop import LaptopRead from app.schemas.monitor import MonitorRead from app.schemas.smartphone import ScoreRead, SmartphoneRead @@ -23,6 +22,5 @@ "ScoreRead", "LaptopRead", "MonitorRead", - "GameRead", "SoftwareRead", ] diff --git a/app/schemas/game.py b/app/schemas/game.py deleted file mode 100644 index 3ecf035..0000000 --- a/app/schemas/game.py +++ /dev/null @@ -1,33 +0,0 @@ -"""Game response schema (§6.10). Games are unscored (no ``score`` field).""" - -from __future__ import annotations - -from datetime import date, datetime - -from pydantic import BaseModel - - -class GameRead(BaseModel): - """Full game detail response.""" - - id: int - slug: str - name: str - release_date: date | None = None - rating: float | None = None - rating_count: int | None = None - metacritic: int | None = None - playtime_hours: int | None = None - platforms: list[str] - genres: list[str] - stores: list[str] - developers: list[str] - publishers: list[str] - tags: list[str] - esrb_rating: str | None = None - background_image: str | None = None - verified: bool - source_urls: list[str] - created_at: datetime - updated_at: datetime - url: str diff --git a/app/schemas/serializers.py b/app/schemas/serializers.py index 445ef06..eda192a 100644 --- a/app/schemas/serializers.py +++ b/app/schemas/serializers.py @@ -5,7 +5,6 @@ from app.config import settings from app.models.brand import Brand from app.models.cpu import CPU -from app.models.game import Game from app.models.gpu import DiscreteGPU from app.models.laptop import Laptop from app.models.mobile_device import MobileDeviceFields @@ -17,7 +16,6 @@ from app.schemas.brand import BrandRead, BrandSummary from app.schemas.common import HybridRead, ManufacturerRef, ResourceRef from app.schemas.cpu import CPURead, CPUScoreRead -from app.schemas.game import GameRead from app.schemas.gpu import GPURead, GPUScoreRead from app.schemas.laptop import LaptopRead from app.schemas.mobile_device import MobileDeviceRead @@ -384,33 +382,6 @@ def monitor_read(monitor: Monitor, brand: Brand) -> MonitorRead: ) -def game_read(game: Game) -> GameRead: - assert game.id is not None - return GameRead( - id=game.id, - slug=game.slug, - name=game.name, - release_date=game.release_date, - rating=game.rating, - rating_count=game.rating_count, - metacritic=game.metacritic, - playtime_hours=game.playtime_hours, - platforms=game.platforms, - genres=game.genres, - stores=game.stores, - developers=game.developers, - publishers=game.publishers, - tags=game.tags, - esrb_rating=game.esrb_rating, - background_image=game.background_image, - verified=game.verified, - source_urls=game.source_urls, - created_at=game.created_at, - updated_at=game.updated_at, - url=url_for("games", game.slug), - ) - - def software_read(software: Software) -> SoftwareRead: assert software.id is not None return SoftwareRead( diff --git a/app/seed.py b/app/seed.py index 4febd15..0d7e6ca 100644 --- a/app/seed.py +++ b/app/seed.py @@ -27,7 +27,6 @@ from app.database import create_db_and_tables, engine from app.models.brand import Brand from app.models.cpu import CPU -from app.models.game import Game from app.models.gpu import DiscreteGPU from app.models.laptop import Laptop from app.models.mobile_device import PDA, Tablet, Watch @@ -103,7 +102,6 @@ def seed(session: Session, data_dir: Path = DATA_DIR) -> dict[str, int]: "cpus": 0, "laptops": 0, "monitors": 0, - "games": 0, "software": 0, "websites": 0, } @@ -264,15 +262,6 @@ def seed_mobile_devices(subdir: str, model: type[SQLModel], count_key: str) -> N counts["monitors"] += 1 session.commit() - # --- Games (standalone; no brand FK) --- - game_slugs = _existing_slugs(session, Game) - for record in _load_dir(data_dir / "game"): - if record["slug"] in game_slugs: - continue - session.add(_with_id(Game(**record), taken)) - counts["games"] += 1 - session.commit() - # --- Software (standalone; no brand FK) --- software_slugs = _existing_slugs(session, Software) for record in _load_dir(data_dir / "software"): diff --git a/app/validate.py b/app/validate.py index 680d70f..09afa31 100644 --- a/app/validate.py +++ b/app/validate.py @@ -110,13 +110,6 @@ "verified", } -GAME_REQUIRED = { - "slug", - "name", - "source_urls", - "verified", -} - SOFTWARE_REQUIRED = { "slug", "name", @@ -243,7 +236,6 @@ def validate() -> list[str]: cpus = _load("cpu") laptops = _load("laptop") monitors = _load("monitor") - games = _load("game") software = _load("software") websites = _load("website") @@ -263,7 +255,6 @@ def validate() -> list[str]: ("cpu", cpus), ("laptop", laptops), ("monitor", monitors), - ("game", games), ("software", software), ("website", websites), ): @@ -416,17 +407,6 @@ def validate() -> list[str]: errors.append(f"{fname}: brand '{rec.get('brand')}' not a known brand") _check_variant_path(fname, rec, "monitor", errors, allow_flat=True) - for fname, rec in games: - _check_required(fname, rec, GAME_REQUIRED, errors) - _check_source_urls(fname, rec, errors) - _check_slug(fname, rec.get("slug"), errors) - if rec.get("release_date") is not None: - _check_date(fname, rec["release_date"], errors) - if rec.get("rating") is not None: - _check_range(fname, "rating", rec.get("rating"), 0, 5, errors) - if rec.get("metacritic") is not None: - _check_range(fname, "metacritic", rec.get("metacritic"), 0, 100, errors) - for fname, rec in software: _check_required(fname, rec, SOFTWARE_REQUIRED, errors) _check_source_urls(fname, rec, errors) diff --git a/tests/integration/game_fixtures.py b/tests/integration/game_fixtures.py deleted file mode 100644 index fdbbb96..0000000 --- a/tests/integration/game_fixtures.py +++ /dev/null @@ -1,40 +0,0 @@ -"""Small database fixtures for game endpoint tests.""" - -from __future__ import annotations - -from datetime import date - -from sqlmodel import Session, select - -from app.database import engine -from app.models.game import Game - - -def ensure_game_fixtures() -> None: - """Insert a compact game when the data checkout lacks it.""" - - with Session(engine) as session: - game = session.exec( - select(Game).where(Game.slug == "the-witcher-3-test") - ).first() - if game is None: - session.add( - Game( - slug="the-witcher-3-test", - name="The Witcher 3: Wild Hunt (test)", - release_date=date(2015, 5, 19), - rating=4.7, - rating_count=6000, - metacritic=92, - playtime_hours=46, - platforms=["PC", "PlayStation 5"], - genres=["zzz-test-genre", "RPG"], - stores=["Steam", "GOG"], - developers=["CD Projekt Red"], - publishers=["CD Projekt"], - tags=["singleplayer", "open-world"], - esrb_rating="Mature", - source_urls=["https://example.com"], - ) - ) - session.commit() diff --git a/tests/integration/test_dump.py b/tests/integration/test_dump.py index 5f0e9c2..ed66bcd 100644 --- a/tests/integration/test_dump.py +++ b/tests/integration/test_dump.py @@ -56,9 +56,9 @@ def test_resolve_collections_defaults_to_everything() -> None: def test_resolve_collections_drops_excluded_and_keeps_order() -> None: - resolved = resolve_collections(["games"]) - assert "games" not in resolved - assert resolved == [c for c in COLLECTIONS if c != "games"] + resolved = resolve_collections(["software"]) + assert "software" not in resolved + assert resolved == [c for c in COLLECTIONS if c != "software"] def test_resolve_collections_rejects_unknown_names() -> None: diff --git a/tests/integration/test_games.py b/tests/integration/test_games.py deleted file mode 100644 index ed0736e..0000000 --- a/tests/integration/test_games.py +++ /dev/null @@ -1,38 +0,0 @@ -"""Integration tests for game endpoints (unscored category).""" - -from __future__ import annotations - -from fastapi.testclient import TestClient - -from tests.integration.game_fixtures import ensure_game_fixtures - - -def test_list_games(client: TestClient) -> None: - ensure_game_fixtures() - body = client.get("/v1/games").json() - # The fixture may be buried under real game data in pagination, so only the - # count is asserted here; the detail test verifies the fixture itself. - assert body["count"] >= 1 - assert "results" in body - - -def test_list_games_sorted(client: TestClient) -> None: - ensure_game_fixtures() - body = client.get("/v1/games?sort=-metacritic").json() - assert body["count"] >= 1 - - -def test_game_detail(client: TestClient) -> None: - ensure_game_fixtures() - body = client.get("/v1/games/the-witcher-3-test").json() - assert body["slug"] == "the-witcher-3-test" - assert body["metacritic"] == 92 - assert "RPG" in body["genres"] - assert "CD Projekt Red" in body["developers"] - # Games are unscored — no score field. - assert "score" not in body - - -def test_game_not_found(client: TestClient) -> None: - ensure_game_fixtures() - assert client.get("/v1/games/nonexistent-game").status_code == 404 diff --git a/tests/unit/test_stable_ids.py b/tests/unit/test_stable_ids.py index a2bef13..c1a8cf6 100644 --- a/tests/unit/test_stable_ids.py +++ b/tests/unit/test_stable_ids.py @@ -27,4 +27,4 @@ def test_collision_falls_back_to_a_rehash_not_a_duplicate(): def test_id_fits_in_a_json_safe_integer(): - assert 0 < _stable_id("games", "doom", set()) < 2**53 + assert 0 < _stable_id("software", "vim", set()) < 2**53