From 0bec18f8ab1c45a88ec7826fd749e79859d842fd Mon Sep 17 00:00:00 2001 From: VortexUK Date: Sat, 11 Jul 2026 11:22:00 +0100 Subject: [PATCH] refactor(census): promote BaseCatalogue to backend/db_catalogue.py; convert store.py to CensusStore MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First step of extending the catalogue architecture beyond eq2db: - backend/eq2db/_catalogue.py moves to backend/db_catalogue.py — the base is no longer eq2db-specific; the seven eq2db imports, the base test and CLAUDE.md re-point. - backend/census/store.py: init_db moves onto a CensusStore class (FOREIGN_KEYS=True, CREATE_META=False — census.db predates _meta); the conn-taking get/upsert helpers become staticmethods. Shared `store = CensusStore()` instance; consumers alias it census_store so call sites read the same, and the redundant `census_store.init_db(census_store.DB_PATH)` explicit-default calls collapse to `census_store.init_db()`. - conftest re-points store.path alongside DB_PATH; tests construct CensusStore(tmp) / call the statics via the class, and DB_PATH monkeypatches become instance `path` patches. CensusStore inherits the full base surface: template init_db, dunder repr/bool/eq/fspath/context-manager, and the narrowed unbuilt-schema read handling. Co-Authored-By: Claude Fable 5 --- CLAUDE.md | 2 +- backend/census/store.py | 318 ++++++++++-------- .../{eq2db/_catalogue.py => db_catalogue.py} | 7 +- backend/eq2db/aas.py | 2 +- backend/eq2db/classes.py | 2 +- backend/eq2db/items.py | 2 +- backend/eq2db/raids.py | 2 +- backend/eq2db/recipes.py | 2 +- backend/eq2db/spells.py | 2 +- backend/eq2db/zones.py | 2 +- backend/server/api/aa.py | 8 +- backend/server/api/character/views.py | 8 +- backend/server/api/favorites.py | 4 +- backend/server/api/guild.py | 6 +- backend/server/api/parses/ingest.py | 8 +- backend/server/census_refresh.py | 4 +- backend/server/guild_cache.py | 4 +- tests/census/test_store.py | 52 +-- tests/conftest.py | 1 + tests/eq2db/test_catalogue_base.py | 2 +- tests/server/test_aa_census_store.py | 20 +- tests/server/test_character.py | 18 +- tests/server/test_guild.py | 24 +- tests/server/test_parses_ingest_cache.py | 8 +- 24 files changed, 270 insertions(+), 238 deletions(-) rename backend/{eq2db/_catalogue.py => db_catalogue.py} (97%) diff --git a/CLAUDE.md b/CLAUDE.md index bfcb2c25..613b5641 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,7 +12,7 @@ A web companion site for EverQuest 2 (TLE), with a Discord bot for spot checks ( | `backend/census/models.py` | Dataclasses: `ItemData`, `ItemStat`, `ItemEffect`, `GuildData`, `GuildMember`, `CharacterSpells`, `SpellEntry`, `NodeAA`, `CharacterAAs` | | `backend/census/constants.py` | `STAT_MAP` (stat display names/groups), class frozensets (`FIGHTERS`, `PRIESTS`, `SCOUTS`, `MAGES`, `ARTISANS`), `ARCHETYPES`, `CLASS_GROUPS`, `TYPEINFO_DISPLAY`, `ITEM_DISPLAY` | | `backend/census/item_parser.py` | Item data parsing helpers (parse_item, parse_stats, parse_effects, parse_flags, etc.) extracted from client.py | -| `backend/eq2db/spells.py` | Local SQLite spell catalogue — `SpellCatalogue` class (shared `catalogue` instance): strip_roman, unique_highest_entries, load_blocklist, find_by_ids, find_by_crc, spell_to_row, upsert_spells. Every eq2db module follows this convention: pure helpers are staticmethods with full bodies in the class, DB reads are instance methods on a per-instance `path`, tests construct `XCatalogue(tmp_db)` and patch the instance, `tests/conftest.py` re-points `catalogue.path`. All seven catalogues inherit `BaseCatalogue` (`backend/eq2db/_catalogue.py`) — `init_db` is a template method (connection preamble + commit in the base); subclasses implement `_create_schema` (+ optional `_post_init` backfills), set `FOREIGN_KEYS`/`CREATE_META` class flags, and override `clear_caches` when they hold caches. | +| `backend/eq2db/spells.py` | Local SQLite spell catalogue — `SpellCatalogue` class (shared `catalogue` instance): strip_roman, unique_highest_entries, load_blocklist, find_by_ids, find_by_crc, spell_to_row, upsert_spells. Every eq2db module follows this convention: pure helpers are staticmethods with full bodies in the class, DB reads are instance methods on a per-instance `path`, tests construct `XCatalogue(tmp_db)` and patch the instance, `tests/conftest.py` re-points `catalogue.path`. All seven catalogues inherit `BaseCatalogue` (`backend/db_catalogue.py`) — `init_db` is a template method (connection preamble + commit in the base); subclasses implement `_create_schema` (+ optional `_post_init` backfills), set `FOREIGN_KEYS`/`CREATE_META` class flags, and override `clear_caches` when they hold caches. | | `backend/eq2db/recipes.py` | Local SQLite recipe catalogue (~70k rows) — `RecipeCatalogue`: find_by_id, find_by_name, find_by_spell, find_spells_by_tier, find_by_output_id. Secondary components stored as JSON array. | | `backend/eq2db/aas.py` | Local SQLite AA catalogue (aas.db — committed like classes.db). Tables `aa_trees` (tree_type + max_points precomputed at build) + `aa_nodes` + `aa_limits` (per-xpac caps from aa_limits.json). Accessors: `load_tree_index`, `tree_node_costs`, `tree_max_points`, `total_max_points`, `get_tree`, `xpac_limits` (short-code alias tolerant); `detect_tree_type` is build-time only. Rebuild: `scripts/download_aa_trees.py` (tree JSONs are local intermediates, gitignored) then `scripts/build_aas_db.py`; commit the db. | | `backend/eq2db/zones.py` | Local SQLite zone catalogue (~1124 rows) — `ZoneCatalogue` over frozen dataclass models (`Zone`, `ZoneEncounter`, `ZoneEncounterMob`, `FeaturedRaid*`). Tables: `zones`, `zone_types`, `zone_aliases`, `zone_encounters`, `zone_encounter_mobs` + the featured-raid trio. Lookups: `find_by_name`, `list_by_expansion`, `list_by_event`, `list_by_type`, `list_bosses_for_zone`, `find_zones_by_boss`. Zone metadata from `scripts/dev/eq2_zones.cleaned.json`; rebuild via `scripts/build_zones_db.py`. Boss data is curator-managed in-place. | diff --git a/backend/census/store.py b/backend/census/store.py index 778a1295..f5893a05 100644 --- a/backend/census/store.py +++ b/backend/census/store.py @@ -3,7 +3,11 @@ never blocks on Census; background refreshes merge in fresh data "keep best known" — a sparse Census response never nulls out good data. -Mirrors parses/db.py: DB_CENSUS_PATH env override, WAL, idempotent _MIGRATIONS. +All behaviour lives on :class:`CensusStore` (the catalogue convention — see +backend/db_catalogue.py): the shared module-level ``store`` instance is the +runtime entry point (consumers alias it ``census_store``); the get/upsert +helpers take an open conn (callers batch reads/writes per connection) and are +staticmethods. Tests construct ``CensusStore(tmp_db)``. """ from __future__ import annotations @@ -15,6 +19,7 @@ from pathlib import Path from typing import Any, TypedDict +from backend.db_catalogue import BaseCatalogue from backend.db_helpers import resolve_db_path @@ -74,147 +79,170 @@ class StoreRecord(TypedDict): _MIGRATIONS: list[str] = [] # future schema bumps appended here -def init_db(path: Path = DB_PATH) -> sqlite3.Connection: - """Create tables if missing. Returns an open connection.""" - path.parent.mkdir(parents=True, exist_ok=True) - conn = sqlite3.connect(path) - conn.execute("PRAGMA journal_mode = WAL;") - conn.execute("PRAGMA synchronous = NORMAL;") - conn.execute("PRAGMA foreign_keys = ON;") - conn.execute(_CREATE_CHARACTERS) - conn.execute(_CREATE_GUILDS) - conn.execute(_CREATE_CHARACTER_AAS) - for stmt in _MIGRATIONS: - try: - conn.execute(stmt) - except sqlite3.OperationalError as exc: - _log.info( - "[census-store] migration skipped (likely already applied): %s — %s", - stmt, - exc, - ) - conn.commit() - return conn - - -def upsert_character( - conn: sqlite3.Connection, - name: str, - world: str, - data: dict, - *, - resolved: bool, - now: int | None = None, -) -> None: - """Merge-store a character (keep best-known). - - When ``resolved`` is False the call is a no-op (never overwrite a good row - with a sparse one, never insert a sparse first-sight row). - - When True and a row already exists, the incoming blob is **overlaid** onto - the stored one field-by-field: keys present in ``data`` refresh the stored - values, keys ``data`` omits are preserved. This is what stops a guild-roster - overview (name/level/class/deity only — no equipment/stats) from nulling an - individually-resolved character's gear on the next roster refresh. A partial - write (one that carries no ``equipment`` key, i.e. hasn't re-resolved the - full profile) also leaves ``last_resolved_at`` untouched so the character - view's staleness clock stays honest and still triggers a real refresh.""" - if not resolved: - return - write_ts = int(time.time()) if now is None else now - resolved_ts = write_ts - - existing = get_character(conn, name, world) - if existing is not None: - incoming_is_full = "equipment" in data - data = {**existing["data"], **data} - if not incoming_is_full: - resolved_ts = existing["last_resolved_at"] # sparse overlay — don't advance freshness - - conn.execute( - """ - INSERT INTO characters (name_lower, world, name, level, guild_name, data_json, last_resolved_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) - ON CONFLICT(name_lower, world) DO UPDATE SET - name=excluded.name, level=excluded.level, guild_name=excluded.guild_name, - data_json=excluded.data_json, last_resolved_at=excluded.last_resolved_at, - updated_at=excluded.updated_at - """, - (name.lower(), world, name, data.get("level"), data.get("guild_name"), json.dumps(data), resolved_ts, write_ts), - ) - conn.commit() - - -def get_character(conn: sqlite3.Connection, name: str, world: str) -> StoreRecord | None: - """Return {data, last_resolved_at} or None.""" - row = conn.execute( - "SELECT data_json, last_resolved_at FROM characters WHERE name_lower=? AND world=?", - (name.lower(), world), - ).fetchone() - if row is None: - return None - return {"data": json.loads(row[0]), "last_resolved_at": row[1]} - - -def upsert_guild(conn: sqlite3.Connection, name: str, world: str, data: dict, *, now: int | None = None) -> None: - """Store the guild roster blob (member names+ranks + info). Always replaces — - the roster list is reliable from Census regardless of member login recency.""" - ts = int(time.time()) if now is None else now - conn.execute( - """ - INSERT INTO guilds (name_lower, world, name, data_json, last_resolved_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?) - ON CONFLICT(name_lower, world) DO UPDATE SET - name=excluded.name, data_json=excluded.data_json, - last_resolved_at=excluded.last_resolved_at, updated_at=excluded.updated_at - """, - (name.lower(), world, name, json.dumps(data), ts, ts), - ) - conn.commit() - - -def get_guild(conn: sqlite3.Connection, name: str, world: str) -> StoreRecord | None: - row = conn.execute( - "SELECT data_json, last_resolved_at FROM guilds WHERE name_lower=? AND world=?", - (name.lower(), world), - ).fetchone() - if row is None: - return None - return {"data": json.loads(row[0]), "last_resolved_at": row[1]} - - -def get_character_aas(conn: sqlite3.Connection, name: str, world: str) -> StoreRecord | None: - """Return the persisted CharAAsResponse dict (or None) for (name, world). - - The record carries the model_dump() of the response plus a - last_resolved_at unix timestamp.""" - row = conn.execute( - "SELECT data_json, last_resolved_at FROM character_aas WHERE name_lower = ? AND world = ?", - (name.lower(), world), - ).fetchone() - if row is None: - return None - return { - "data": json.loads(row[0]), - "last_resolved_at": row[1], - } - - -def upsert_character_aas( - conn: sqlite3.Connection, - name: str, - world: str, - data: dict, - *, - now: int | None = None, -) -> None: - """Insert or update the (name, world) AA record. Always overwrites — AAs - have no 'best-known merge' equivalent because the Census response is - authoritative.""" - if now is None: - now = int(time.time()) - conn.execute( - "INSERT OR REPLACE INTO character_aas (name_lower, world, data_json, last_resolved_at) VALUES (?, ?, ?, ?)", - (name.lower(), world, json.dumps(data), now), - ) - conn.commit() +class CensusStore(BaseCatalogue): + """Read/write access to one census.db file (last-known Census lookups).""" + + FOREIGN_KEYS = True + + # census.db predates the shared _meta table and has no build provenance + # to track — rows carry their own timestamps. + CREATE_META = False + + def __init__(self, path: Path = DB_PATH) -> None: + super().__init__(path) + + def _create_schema(self, conn: sqlite3.Connection) -> None: + conn.execute(_CREATE_CHARACTERS) + conn.execute(_CREATE_GUILDS) + conn.execute(_CREATE_CHARACTER_AAS) + for stmt in _MIGRATIONS: + try: + conn.execute(stmt) + except sqlite3.OperationalError as exc: + _log.info( + "[census-store] migration skipped (likely already applied): %s — %s", + stmt, + exc, + ) + + # ── Characters ─────────────────────────────────────────────────────────── + + @staticmethod + def upsert_character( + conn: sqlite3.Connection, + name: str, + world: str, + data: dict, + *, + resolved: bool, + now: int | None = None, + ) -> None: + """Merge-store a character (keep best-known). + + When ``resolved`` is False the call is a no-op (never overwrite a good row + with a sparse one, never insert a sparse first-sight row). + + When True and a row already exists, the incoming blob is **overlaid** onto + the stored one field-by-field: keys present in ``data`` refresh the stored + values, keys ``data`` omits are preserved. This is what stops a guild-roster + overview (name/level/class/deity only — no equipment/stats) from nulling an + individually-resolved character's gear on the next roster refresh. A partial + write (one that carries no ``equipment`` key, i.e. hasn't re-resolved the + full profile) also leaves ``last_resolved_at`` untouched so the character + view's staleness clock stays honest and still triggers a real refresh.""" + if not resolved: + return + write_ts = int(time.time()) if now is None else now + resolved_ts = write_ts + + existing = CensusStore.get_character(conn, name, world) + if existing is not None: + incoming_is_full = "equipment" in data + data = {**existing["data"], **data} + if not incoming_is_full: + resolved_ts = existing["last_resolved_at"] # sparse overlay — don't advance freshness + + conn.execute( + """ + INSERT INTO characters (name_lower, world, name, level, guild_name, data_json, last_resolved_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(name_lower, world) DO UPDATE SET + name=excluded.name, level=excluded.level, guild_name=excluded.guild_name, + data_json=excluded.data_json, last_resolved_at=excluded.last_resolved_at, + updated_at=excluded.updated_at + """, + ( + name.lower(), + world, + name, + data.get("level"), + data.get("guild_name"), + json.dumps(data), + resolved_ts, + write_ts, + ), + ) + conn.commit() + + @staticmethod + def get_character(conn: sqlite3.Connection, name: str, world: str) -> StoreRecord | None: + """Return {data, last_resolved_at} or None.""" + row = conn.execute( + "SELECT data_json, last_resolved_at FROM characters WHERE name_lower=? AND world=?", + (name.lower(), world), + ).fetchone() + if row is None: + return None + return {"data": json.loads(row[0]), "last_resolved_at": row[1]} + + # ── Guilds ─────────────────────────────────────────────────────────────── + + @staticmethod + def upsert_guild(conn: sqlite3.Connection, name: str, world: str, data: dict, *, now: int | None = None) -> None: + """Store the guild roster blob (member names+ranks + info). Always replaces — + the roster list is reliable from Census regardless of member login recency.""" + ts = int(time.time()) if now is None else now + conn.execute( + """ + INSERT INTO guilds (name_lower, world, name, data_json, last_resolved_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?) + ON CONFLICT(name_lower, world) DO UPDATE SET + name=excluded.name, data_json=excluded.data_json, + last_resolved_at=excluded.last_resolved_at, updated_at=excluded.updated_at + """, + (name.lower(), world, name, json.dumps(data), ts, ts), + ) + conn.commit() + + @staticmethod + def get_guild(conn: sqlite3.Connection, name: str, world: str) -> StoreRecord | None: + row = conn.execute( + "SELECT data_json, last_resolved_at FROM guilds WHERE name_lower=? AND world=?", + (name.lower(), world), + ).fetchone() + if row is None: + return None + return {"data": json.loads(row[0]), "last_resolved_at": row[1]} + + # ── Character AAs ──────────────────────────────────────────────────────── + + @staticmethod + def get_character_aas(conn: sqlite3.Connection, name: str, world: str) -> StoreRecord | None: + """Return the persisted CharAAsResponse dict (or None) for (name, world). + + The record carries the model_dump() of the response plus a + last_resolved_at unix timestamp.""" + row = conn.execute( + "SELECT data_json, last_resolved_at FROM character_aas WHERE name_lower = ? AND world = ?", + (name.lower(), world), + ).fetchone() + if row is None: + return None + return { + "data": json.loads(row[0]), + "last_resolved_at": row[1], + } + + @staticmethod + def upsert_character_aas( + conn: sqlite3.Connection, + name: str, + world: str, + data: dict, + *, + now: int | None = None, + ) -> None: + """Insert or update the (name, world) AA record. Always overwrites — AAs + have no 'best-known merge' equivalent because the Census response is + authoritative.""" + if now is None: + now = int(time.time()) + conn.execute( + "INSERT OR REPLACE INTO character_aas (name_lower, world, data_json, last_resolved_at) VALUES (?, ?, ?, ?)", + (name.lower(), world, json.dumps(data), now), + ) + conn.commit() + + +# The shared default instance — every runtime consumer goes through this. +store = CensusStore() diff --git a/backend/eq2db/_catalogue.py b/backend/db_catalogue.py similarity index 97% rename from backend/eq2db/_catalogue.py rename to backend/db_catalogue.py index 5a66db67..50d35762 100644 --- a/backend/eq2db/_catalogue.py +++ b/backend/db_catalogue.py @@ -1,6 +1,9 @@ -"""Shared base class for the eq2db catalogue classes. +"""Shared base class for SQLite catalogue/store classes. -Every data module under ``backend/eq2db/`` exposes one catalogue class +Originally extracted from the eq2db data modules; now the base for every +per-file SQLite data interface in the codebase (eq2db catalogues, the +census store, ...). Every data module under ``backend/eq2db/`` exposes one +catalogue class (AACatalogue, ClassCatalogue, SpellCatalogue, RecipeCatalogue, ZoneCatalogue, ItemCatalogue, RaidCatalogue) following the same convention: diff --git a/backend/eq2db/aas.py b/backend/eq2db/aas.py index 72999958..95ed8853 100644 --- a/backend/eq2db/aas.py +++ b/backend/eq2db/aas.py @@ -25,9 +25,9 @@ from pathlib import Path from typing import Any +from backend.db_catalogue import BaseCatalogue from backend.db_helpers import resolve_db_path from backend.eq2db import _meta as _meta_db -from backend.eq2db._catalogue import BaseCatalogue from backend.sql_loader import load_sql _log = logging.getLogger(__name__) diff --git a/backend/eq2db/classes.py b/backend/eq2db/classes.py index 4798e634..58e94251 100644 --- a/backend/eq2db/classes.py +++ b/backend/eq2db/classes.py @@ -36,8 +36,8 @@ from pathlib import Path from typing import Any, TypeVar +from backend.db_catalogue import BaseCatalogue from backend.db_helpers import resolve_db_path -from backend.eq2db._catalogue import BaseCatalogue from backend.sql_loader import load_sql _T = TypeVar("_T") diff --git a/backend/eq2db/items.py b/backend/eq2db/items.py index 06005b0d..965c5fc5 100644 --- a/backend/eq2db/items.py +++ b/backend/eq2db/items.py @@ -25,8 +25,8 @@ from backend.census._coerce import coerce_int as _coerce_int from backend.census._coerce import coerce_str_or_none as _coerce_str from backend.census.item_level import compute_ilvl +from backend.db_catalogue import BaseCatalogue from backend.db_helpers import like_escape, resolve_db_path -from backend.eq2db._catalogue import BaseCatalogue from backend.eq2db.classes import catalogue as _classes from backend.sql_loader import load_sql diff --git a/backend/eq2db/raids.py b/backend/eq2db/raids.py index f0c3293f..03cf7577 100644 --- a/backend/eq2db/raids.py +++ b/backend/eq2db/raids.py @@ -54,8 +54,8 @@ import time from pathlib import Path +from backend.db_catalogue import BaseCatalogue from backend.db_helpers import resolve_db_path -from backend.eq2db._catalogue import BaseCatalogue from backend.sql_loader import load_sql _SQL = load_sql(__file__) diff --git a/backend/eq2db/recipes.py b/backend/eq2db/recipes.py index 5a9f5624..b066fc67 100644 --- a/backend/eq2db/recipes.py +++ b/backend/eq2db/recipes.py @@ -36,8 +36,8 @@ from typing import TypedDict, cast from backend.census._coerce import coerce_int as _int +from backend.db_catalogue import BaseCatalogue from backend.db_helpers import resolve_db_path -from backend.eq2db._catalogue import BaseCatalogue from backend.sql_loader import load_sql _SQL = load_sql(__file__) diff --git a/backend/eq2db/spells.py b/backend/eq2db/spells.py index 3f70ca9b..4f6b7507 100644 --- a/backend/eq2db/spells.py +++ b/backend/eq2db/spells.py @@ -34,8 +34,8 @@ from backend.census._coerce import coerce_float as _float from backend.census._coerce import coerce_int as _int from backend.census._coerce import coerce_str_or_none as _str +from backend.db_catalogue import BaseCatalogue from backend.db_helpers import resolve_db_path -from backend.eq2db._catalogue import BaseCatalogue from backend.sql_loader import load_sql diff --git a/backend/eq2db/zones.py b/backend/eq2db/zones.py index e22190b7..56ea72ee 100644 --- a/backend/eq2db/zones.py +++ b/backend/eq2db/zones.py @@ -43,8 +43,8 @@ from dataclasses import dataclass from pathlib import Path +from backend.db_catalogue import BaseCatalogue from backend.db_helpers import resolve_db_path -from backend.eq2db._catalogue import BaseCatalogue from backend.sql_loader import load_sql _SQL = load_sql(__file__) diff --git a/backend/server/api/aa.py b/backend/server/api/aa.py index 6a284ca9..27232ca1 100644 --- a/backend/server/api/aa.py +++ b/backend/server/api/aa.py @@ -9,8 +9,8 @@ from fastapi import APIRouter, HTTPException from pydantic import BaseModel -from backend.census import store as census_store from backend.census.store import StoreRecord +from backend.census.store import store as census_store from backend.eq2db.aas import catalogue as aa_db from backend.eq2db.spells import catalogue as spells_db from backend.server.cache import aa_cache @@ -216,7 +216,7 @@ async def _bg_refresh_aas(name: str, cache_key: str) -> None: now = int(time.time()) def _write() -> None: - conn = census_store.init_db(census_store.DB_PATH) + conn = census_store.init_db() try: census_store.upsert_character_aas(conn, name, world, result.model_dump(), now=now) finally: @@ -251,7 +251,7 @@ async def get_character_aas(name: str) -> CharAAsResponse: # 2) Durable store — serve known-good data without a Census round-trip. def _read() -> StoreRecord | None: - conn = census_store.init_db(census_store.DB_PATH) + conn = census_store.init_db() try: return census_store.get_character_aas(conn, name, world) finally: @@ -289,7 +289,7 @@ def _read() -> StoreRecord | None: result = _aas_response_from_census(char_aas) def _write() -> None: - conn = census_store.init_db(census_store.DB_PATH) + conn = census_store.init_db() try: census_store.upsert_character_aas(conn, name, world, result.model_dump(), now=now) finally: diff --git a/backend/server/api/character/views.py b/backend/server/api/character/views.py index 790a33e8..6eabf103 100644 --- a/backend/server/api/character/views.py +++ b/backend/server/api/character/views.py @@ -447,9 +447,9 @@ async def get_character(request: Request, name: str) -> CharacterResponse: return cached # 2) Durable store. - from backend.census import store as census_store + from backend.census.store import store as census_store - conn = census_store.init_db(census_store.DB_PATH) + conn = census_store.init_db() try: rec = census_store.get_character(conn, name, current_world()) finally: @@ -493,7 +493,7 @@ async def get_character(request: Request, name: str) -> CharacterResponse: # Best-effort; the user already has a correct response in hand # so a write failure doesn't degrade the visible behaviour. try: - conn2 = census_store.init_db(census_store.DB_PATH) + conn2 = census_store.init_db() try: census_store.upsert_character( conn2, @@ -532,7 +532,7 @@ async def get_character(request: Request, name: str) -> CharacterResponse: raise HTTPException(status_code=404, detail=f"Character '{name}' not found on {current_world()}") resp = _build_char_response(char) data = resp.model_dump() - conn = census_store.init_db(census_store.DB_PATH) + conn = census_store.init_db() try: census_store.upsert_character(conn, name, current_world(), data, resolved=True, now=now) finally: diff --git a/backend/server/api/favorites.py b/backend/server/api/favorites.py index 3a771cc8..d0c66c2f 100644 --- a/backend/server/api/favorites.py +++ b/backend/server/api/favorites.py @@ -22,7 +22,7 @@ from fastapi import APIRouter, HTTPException, Request from pydantic import BaseModel -from backend.census import store as census_store +from backend.census.store import store as census_store from backend.core.log_safety import scrub as _scrub from backend.server.auth_deps import require_user_session from backend.server.cache import character_cache, favorite_count_cache @@ -106,7 +106,7 @@ def _store_character_data_many(names: list[str], world: str) -> dict[str, dict]: out: dict[str, dict] = {} if not names: return out - conn = census_store.init_db(census_store.DB_PATH) + conn = census_store.init_db() try: for name in names: rec = census_store.get_character(conn, name, world) diff --git a/backend/server/api/guild.py b/backend/server/api/guild.py index ff914bbd..71ba44f7 100644 --- a/backend/server/api/guild.py +++ b/backend/server/api/guild.py @@ -8,7 +8,7 @@ from fastapi import APIRouter, HTTPException, Request from pydantic import BaseModel -from backend.census import store as census_store +from backend.census.store import store as census_store from backend.core.log_safety import scrub as _scrub from backend.server.cache import guild_cache from backend.server.core.cache_keys import guild_adorns_key, guild_info_key, guild_roster_key, guild_spells_key @@ -206,7 +206,7 @@ async def get_guild_info(request: Request, guild_name: str) -> GuildInfoResponse return cached # Fall through to the durable store (the stored blob is the roster shape; # derive a minimal GuildInfoResponse from it — name/world + member count). - conn = census_store.init_db(census_store.DB_PATH) + conn = census_store.init_db() try: rec = census_store.get_guild(conn, guild_name, current_world()) finally: @@ -268,7 +268,7 @@ async def get_guild(request: Request, guild_name: str) -> GuildResponse: cached, is_stale = guild_cache.get_stale(cache_key) if cached is not None and not is_stale: return cached - conn = census_store.init_db(census_store.DB_PATH) + conn = census_store.init_db() try: rec = census_store.get_guild(conn, guild_name, current_world()) finally: diff --git a/backend/server/api/parses/ingest.py b/backend/server/api/parses/ingest.py index c97e5af2..ccc456d0 100644 --- a/backend/server/api/parses/ingest.py +++ b/backend/server/api/parses/ingest.py @@ -16,7 +16,7 @@ from fastapi import BackgroundTasks, HTTPException, Request -from backend.census import store as census_store +from backend.census.store import store as census_store from backend.server.api.parses import router from backend.server.api.parses.list import _classify_zone from backend.server.api.parses.models import ( @@ -128,7 +128,7 @@ async def _resolve_uploader_guild_async( # Durable store — no Census. A known uploader (from any prior path) is # served from here forever; skip the prewarm since their roster is already # persisted and combatant resolution will hit the store. - store_conn = census_store.init_db(census_store.DB_PATH) + store_conn = census_store.init_db() try: rec = census_store.get_character(store_conn, uploader, effective_world) finally: @@ -199,7 +199,7 @@ async def _resolve_combatant_snapshots( effective_world = _sanitize_world(world) or _WORLD world_lower = effective_world.lower() out: dict[str, CombatantSnapshot] = {} - store_conn = census_store.init_db(census_store.DB_PATH) + store_conn = census_store.init_db() try: async with shared_census_client() as client: for name in names: @@ -292,7 +292,7 @@ def _cached_snapshots(names: list[str], world: str | None = None) -> dict[str, C effective_world = _sanitize_world(world) or _WORLD world_lower = effective_world.lower() out: dict[str, CombatantSnapshot] = {} - store_conn = census_store.init_db(census_store.DB_PATH) + store_conn = census_store.init_db() try: for name in names: cached, _ = character_cache.get_stale(f"{name.lower()}:{world_lower}") diff --git a/backend/server/census_refresh.py b/backend/server/census_refresh.py index d680b4c4..71781909 100644 --- a/backend/server/census_refresh.py +++ b/backend/server/census_refresh.py @@ -10,7 +10,7 @@ import logging import time -from backend.census import store as census_store +from backend.census.store import store as census_store from backend.core.log_safety import scrub as _scrub from backend.server import census_events, census_health from backend.server.cache import character_cache @@ -66,7 +66,7 @@ async def _run_character_refresh(name: str, key: str, world: str) -> None: resp = _build_char_response(char) # CharacterResponse (pydantic) data = resp.model_dump() resolved = bool(data.get("cls") or data.get("level")) - conn = census_store.init_db(census_store.DB_PATH) + conn = census_store.init_db() try: census_store.upsert_character(conn, name, world, data, resolved=resolved) finally: diff --git a/backend/server/guild_cache.py b/backend/server/guild_cache.py index 3462ff79..8569ff50 100644 --- a/backend/server/guild_cache.py +++ b/backend/server/guild_cache.py @@ -24,9 +24,9 @@ import time from collections import Counter -from backend.census import store as census_store from backend.census.constants import SPELL_TIER_ORDER as _TIER_ORDER from backend.census.models import CharacterOverview, GuildData, SpellEntry +from backend.census.store import store as census_store from backend.eq2db.spells import ( DB_PATH as _SPELLS_DB, ) @@ -440,7 +440,7 @@ async def _persist_and_publish_guild(guild_name: str, world: str | None = None) _member_fields = set(GuildMemberResponse.model_fields) - conn = census_store.init_db(census_store.DB_PATH) + conn = census_store.init_db() try: stored_by_lower: dict[str, dict] = {} for stub in roster_stubs: diff --git a/tests/census/test_store.py b/tests/census/test_store.py index c0c7f684..b6097ded 100644 --- a/tests/census/test_store.py +++ b/tests/census/test_store.py @@ -4,7 +4,7 @@ def test_init_db_creates_tables(tmp_path): - conn = cs.init_db(tmp_path / "backend.census.db") + conn = cs.CensusStore(tmp_path / "backend.census.db").init_db() try: tables = {r[0] for r in conn.execute("SELECT name FROM sqlite_master WHERE type='table'")} assert {"characters", "guilds"} <= tables @@ -24,9 +24,9 @@ def test_init_db_creates_tables(tmp_path): def test_upsert_character_resolved_then_get(tmp_path): - conn = cs.init_db(tmp_path / "backend.census.db") + conn = cs.CensusStore(tmp_path / "backend.census.db").init_db() try: - cs.upsert_character( + cs.CensusStore.upsert_character( conn, "Menludiir", "Varsoon", @@ -34,7 +34,7 @@ def test_upsert_character_resolved_then_get(tmp_path): resolved=True, now=1000, ) - rec = cs.get_character(conn, "Menludiir", "Varsoon") + rec = cs.CensusStore.get_character(conn, "Menludiir", "Varsoon") assert rec is not None assert rec["data"]["level"] == 90 assert rec["last_resolved_at"] == 1000 @@ -43,15 +43,15 @@ def test_upsert_character_resolved_then_get(tmp_path): def test_sparse_refresh_never_clobbers(tmp_path): - conn = cs.init_db(tmp_path / "backend.census.db") + conn = cs.CensusStore(tmp_path / "backend.census.db").init_db() try: - cs.upsert_character( + cs.CensusStore.upsert_character( conn, "Menludiir", "Varsoon", {"name": "Menludiir", "level": 90, "cls": "Templar"}, resolved=True, now=1000 ) - cs.upsert_character( + cs.CensusStore.upsert_character( conn, "Menludiir", "Varsoon", {"name": "Menludiir", "level": None, "cls": None}, resolved=False, now=2000 ) - rec = cs.get_character(conn, "Menludiir", "Varsoon") + rec = cs.CensusStore.get_character(conn, "Menludiir", "Varsoon") assert rec is not None assert rec["data"]["level"] == 90 # kept assert rec["last_resolved_at"] == 1000 # unchanged @@ -63,10 +63,10 @@ def test_roster_overview_does_not_wipe_resolved_gear(tmp_path): """A guild-roster overview (no equipment key) must NOT null an individually resolved character's gear — it overlays its scalar fields and preserves the rest, without advancing the freshness clock.""" - conn = cs.init_db(tmp_path / "backend.census.db") + conn = cs.CensusStore(tmp_path / "backend.census.db").init_db() try: # Full individual resolve: gear + id present. - cs.upsert_character( + cs.CensusStore.upsert_character( conn, "Menludiir", "Varsoon", @@ -75,7 +75,7 @@ def test_roster_overview_does_not_wipe_resolved_gear(tmp_path): now=1000, ) # Later guild-roster refresh: sparse overview, resolved=True, newer ts. - cs.upsert_character( + cs.CensusStore.upsert_character( conn, "Menludiir", "Varsoon", @@ -83,7 +83,7 @@ def test_roster_overview_does_not_wipe_resolved_gear(tmp_path): resolved=True, now=2000, ) - rec = cs.get_character(conn, "Menludiir", "Varsoon") + rec = cs.CensusStore.get_character(conn, "Menludiir", "Varsoon") assert rec is not None assert rec["data"]["equipment"] == [{"slot": "Head"}] # gear preserved assert rec["data"]["id"] == "123" # id preserved @@ -97,9 +97,9 @@ def test_roster_overview_does_not_wipe_resolved_gear(tmp_path): def test_full_resolve_replaces_and_advances_freshness(tmp_path): """A full resolve (equipment key present) overlays gear and advances the freshness clock — genuine gear changes still take effect.""" - conn = cs.init_db(tmp_path / "backend.census.db") + conn = cs.CensusStore(tmp_path / "backend.census.db").init_db() try: - cs.upsert_character( + cs.CensusStore.upsert_character( conn, "Menludiir", "Varsoon", @@ -107,7 +107,7 @@ def test_full_resolve_replaces_and_advances_freshness(tmp_path): resolved=True, now=1000, ) - cs.upsert_character( + cs.CensusStore.upsert_character( conn, "Menludiir", "Varsoon", @@ -115,7 +115,7 @@ def test_full_resolve_replaces_and_advances_freshness(tmp_path): resolved=True, now=2000, ) - rec = cs.get_character(conn, "Menludiir", "Varsoon") + rec = cs.CensusStore.get_character(conn, "Menludiir", "Varsoon") assert rec is not None assert rec["data"]["equipment"] == [{"slot": "Chest"}] # replaced assert rec["last_resolved_at"] == 2000 # advanced @@ -124,28 +124,28 @@ def test_full_resolve_replaces_and_advances_freshness(tmp_path): def test_unresolved_first_sight_is_not_stored(tmp_path): - conn = cs.init_db(tmp_path / "backend.census.db") + conn = cs.CensusStore(tmp_path / "backend.census.db").init_db() try: - cs.upsert_character(conn, "Ghost", "Varsoon", {"name": "Ghost"}, resolved=False, now=1000) - assert cs.get_character(conn, "Ghost", "Varsoon") is None + cs.CensusStore.upsert_character(conn, "Ghost", "Varsoon", {"name": "Ghost"}, resolved=False, now=1000) + assert cs.CensusStore.get_character(conn, "Ghost", "Varsoon") is None finally: conn.close() def test_get_missing_returns_none(tmp_path): - conn = cs.init_db(tmp_path / "backend.census.db") + conn = cs.CensusStore(tmp_path / "backend.census.db").init_db() try: - assert cs.get_character(conn, "Nobody", "Varsoon") is None + assert cs.CensusStore.get_character(conn, "Nobody", "Varsoon") is None finally: conn.close() def test_upsert_guild_then_get(tmp_path): - conn = cs.init_db(tmp_path / "backend.census.db") + conn = cs.CensusStore(tmp_path / "backend.census.db").init_db() try: blob = {"name": "Exordium", "members": [{"name": "Menludiir", "rank": "Leader"}]} - cs.upsert_guild(conn, "Exordium", "Varsoon", blob, now=1000) - rec = cs.get_guild(conn, "Exordium", "Varsoon") + cs.CensusStore.upsert_guild(conn, "Exordium", "Varsoon", blob, now=1000) + rec = cs.CensusStore.get_guild(conn, "Exordium", "Varsoon") assert rec is not None assert rec["data"]["members"][0]["name"] == "Menludiir" assert rec["last_resolved_at"] == 1000 @@ -154,8 +154,8 @@ def test_upsert_guild_then_get(tmp_path): def test_guild_get_missing_returns_none(tmp_path): - conn = cs.init_db(tmp_path / "backend.census.db") + conn = cs.CensusStore(tmp_path / "backend.census.db").init_db() try: - assert cs.get_guild(conn, "Nope", "Varsoon") is None + assert cs.CensusStore.get_guild(conn, "Nope", "Varsoon") is None finally: conn.close() diff --git a/tests/conftest.py b/tests/conftest.py index f88094d7..810af265 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -114,6 +114,7 @@ def pytest_configure(config: pytest.Config) -> None: # noqa: ARG001 parses_db.DB_PATH = resolve_db_path("DB_PARSES_PATH", "parses", "parses.db") users_db.DB_PATH = resolve_db_path("DB_USERS_PATH", "users.db") census_store.DB_PATH = resolve_db_path("DB_CENSUS_PATH", "census", "census.db") + census_store.store.path = census_store.DB_PATH # eq2db catalogue modules: re-point both the module constant AND the # shared catalogue instance (its path was captured at import time). for mod, env_var, subdir, filename in ( diff --git a/tests/eq2db/test_catalogue_base.py b/tests/eq2db/test_catalogue_base.py index 088d7b02..52d05a3c 100644 --- a/tests/eq2db/test_catalogue_base.py +++ b/tests/eq2db/test_catalogue_base.py @@ -13,7 +13,7 @@ import pytest -from backend.eq2db._catalogue import BaseCatalogue +from backend.db_catalogue import BaseCatalogue from backend.eq2db.raids import RaidCatalogue from backend.eq2db.spells import SpellCatalogue diff --git a/tests/server/test_aa_census_store.py b/tests/server/test_aa_census_store.py index 7a513d08..2ebc01c0 100644 --- a/tests/server/test_aa_census_store.py +++ b/tests/server/test_aa_census_store.py @@ -63,7 +63,7 @@ async def test_cold_cache_calls_census_and_persists(app, tmp_path): fake_aas = _make_census_aas("Coldchar") with ( - patch("backend.server.api.aa.census_store.DB_PATH", db_path), + patch("backend.server.api.aa.census_store.path", db_path), patch("backend.server.core.census_lifecycle._clients", {}), patch("backend.server.core.census_lifecycle.CensusClient") as MockCC, ): @@ -80,9 +80,9 @@ async def test_cold_cache_calls_census_and_persists(app, tmp_path): assert data["total_spent"] == 5 # one node, tier=5 # Verify persisted to the store. - conn = cs.init_db(db_path) + conn = cs.CensusStore(db_path).init_db() try: - rec = cs.get_character_aas(conn, "Coldchar", "Varsoon") + rec = cs.CensusStore.get_character_aas(conn, "Coldchar", "Varsoon") assert rec is not None assert rec["data"]["character_name"] == "Coldchar" finally: @@ -99,18 +99,18 @@ async def test_warm_store_skips_census(app, tmp_path): """When census_store has a fresh record, Census is NOT called.""" db_path = tmp_path / "backend.census.db" # Pre-seed the store with a recent timestamp. - conn = cs.init_db(db_path) + conn = cs.CensusStore(db_path).init_db() stored_data = { "character_name": "Stored", "total_spent": 10, "trees": [], "profiles": [], } - cs.upsert_character_aas(conn, "Stored", "Varsoon", stored_data, now=int(time.time())) + cs.CensusStore.upsert_character_aas(conn, "Stored", "Varsoon", stored_data, now=int(time.time())) conn.close() with ( - patch("backend.server.api.aa.census_store.DB_PATH", db_path), + patch("backend.server.api.aa.census_store.path", db_path), patch("backend.server.api.aa.aa_cache") as mock_cache, patch("backend.server.core.census_lifecycle._clients", {}), patch("backend.server.core.census_lifecycle.CensusClient") as MockCC, @@ -146,14 +146,14 @@ async def test_stale_store_returns_data_and_spawns_refresh(app, tmp_path): db_path = tmp_path / "backend.census.db" old_ts = int(time.time()) - CHARACTER_STALE_S - 60 # definitely stale - conn = cs.init_db(db_path) + conn = cs.CensusStore(db_path).init_db() stored_data = { "character_name": "Stalechar", "total_spent": 7, "trees": [], "profiles": [], } - cs.upsert_character_aas(conn, "Stalechar", "Varsoon", stored_data, now=old_ts) + cs.CensusStore.upsert_character_aas(conn, "Stalechar", "Varsoon", stored_data, now=old_ts) conn.close() tasks_created: list = [] @@ -164,7 +164,7 @@ def _fake_create_task(coro): return MagicMock() with ( - patch("backend.server.api.aa.census_store.DB_PATH", db_path), + patch("backend.server.api.aa.census_store.path", db_path), patch("backend.server.api.aa.aa_cache") as mock_cache, patch("backend.server.api.aa.asyncio.create_task", side_effect=_fake_create_task), patch("backend.server.core.census_lifecycle._clients", {}), @@ -202,7 +202,7 @@ def test_init_db_on_old_schema_adds_character_aas_table(tmp_path): conn.close() # Now run init_db — should add character_aas without raising. - conn = cs.init_db(db_path) + conn = cs.CensusStore(db_path).init_db() try: tables = {r[0] for r in conn.execute("SELECT name FROM sqlite_master WHERE type='table'")} assert "character_aas" in tables diff --git a/tests/server/test_character.py b/tests/server/test_character.py index 0a2675a1..6fd9ca93 100644 --- a/tests/server/test_character.py +++ b/tests/server/test_character.py @@ -282,9 +282,9 @@ async def test_serve_path_self_heals_stored_placeholder(app, tmp_path, monkeypat } ] db_path = tmp_path / "backend.census.db" - conn = census_store.init_db(db_path) + conn = census_store.CensusStore(db_path).init_db() try: - census_store.upsert_character( + census_store.CensusStore.upsert_character( conn, "HealMe", _WORLD, @@ -294,7 +294,7 @@ async def test_serve_path_self_heals_stored_placeholder(app, tmp_path, monkeypat ) finally: conn.close() - monkeypatch.setattr(census_store, "DB_PATH", db_path) + monkeypatch.setattr(census_store.store, "path", db_path) character_cache.delete(f"healme:{_WORLD.lower()}") async def _fake_find(item_id, *args, **kwargs): @@ -326,9 +326,9 @@ async def test_stored_data_served_without_census(app, tmp_path, monkeypatch): # Seed the census_store at an isolated tmp DB. db_path = tmp_path / "backend.census.db" - conn = census_store.init_db(db_path) + conn = census_store.CensusStore(db_path).init_db() try: - census_store.upsert_character( + census_store.CensusStore.upsert_character( conn, "Stored", _WORLD, @@ -340,7 +340,7 @@ async def test_stored_data_served_without_census(app, tmp_path, monkeypatch): conn.close() # Point the module at the tmp DB so the endpoint reads from it. - monkeypatch.setattr(census_store, "DB_PATH", db_path) + monkeypatch.setattr(census_store.store, "path", db_path) # Ensure the in-memory cache is cold for this key. cache_key = f"stored:{_WORLD.lower()}" @@ -375,12 +375,12 @@ async def test_partial_roster_record_served_without_500(app, tmp_path, monkeypat partial = {"name": "Verarec", "level": 90, "cls": "Wizard", "guild_name": "Test"} db_path = tmp_path / "backend.census.db" - conn = census_store.init_db(db_path) + conn = census_store.CensusStore(db_path).init_db() try: - census_store.upsert_character(conn, "Verarec", _WORLD, partial, resolved=True, now=1000) + census_store.CensusStore.upsert_character(conn, "Verarec", _WORLD, partial, resolved=True, now=1000) finally: conn.close() - monkeypatch.setattr(census_store, "DB_PATH", db_path) + monkeypatch.setattr(census_store.store, "path", db_path) character_cache.delete(f"verarec:{_WORLD.lower()}") async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client: diff --git a/tests/server/test_guild.py b/tests/server/test_guild.py index f632348d..700a204c 100644 --- a/tests/server/test_guild.py +++ b/tests/server/test_guild.py @@ -264,7 +264,7 @@ def _get_stale_side_effect(key): def tmp_census_db(tmp_path, monkeypatch): """Seed a temporary census.db with one guild row and redirect DB_PATH to it.""" db_path = tmp_path / "backend.census.db" - monkeypatch.setattr(census_store, "DB_PATH", db_path) + monkeypatch.setattr(census_store.store, "path", db_path) monkeypatch.setenv("CENSUS_DB_PATH", str(db_path)) roster_blob = GuildResponse( name="Exordium", @@ -294,8 +294,8 @@ def tmp_census_db(tmp_path, monkeypatch): type=0, ).model_dump() combined_blob = {"roster": roster_blob, "info": info_blob} - conn = census_store.init_db(db_path) - census_store.upsert_guild(conn, "Exordium", "Varsoon", combined_blob, now=1000) + conn = census_store.CensusStore(db_path).init_db() + census_store.CensusStore.upsert_guild(conn, "Exordium", "Varsoon", combined_blob, now=1000) conn.close() return db_path @@ -307,7 +307,7 @@ async def test_guild_roster_served_from_store_when_census_down(app, tmp_census_d monkeypatch.setattr(_health_mod, "is_down", lambda: True) # Redirect the module-level DB_PATH so the route's local init_db call uses the tmp db. - monkeypatch.setattr(census_store, "DB_PATH", tmp_census_db) + monkeypatch.setattr(census_store.store, "path", tmp_census_db) from backend.server.cache import guild_cache @@ -329,7 +329,7 @@ async def test_guild_info_served_from_store_when_census_down(app, tmp_census_db, import backend.server.census_health as _health_mod monkeypatch.setattr(_health_mod, "is_down", lambda: True) - monkeypatch.setattr(census_store, "DB_PATH", tmp_census_db) + monkeypatch.setattr(census_store.store, "path", tmp_census_db) from backend.server.cache import guild_cache @@ -362,12 +362,12 @@ async def test_persist_merges_offline_member_from_store(app, tmp_path, monkeypat from backend.server.cache import guild_cache db_path = tmp_path / "backend.census.db" - monkeypatch.setattr(census_store, "DB_PATH", db_path) + monkeypatch.setattr(census_store.store, "path", db_path) monkeypatch.setenv("CENSUS_DB_PATH", str(db_path)) # Seed an offline alt that resolved in the PAST (now=1000). - conn = census_store.init_db(db_path) - census_store.upsert_character( + conn = census_store.CensusStore(db_path).init_db() + census_store.CensusStore.upsert_character( conn, "OfflineAlt", _WORLD, @@ -442,9 +442,9 @@ async def test_persist_merges_offline_member_from_store(app, tmp_path, monkeypat ): await _persist_and_publish_guild("TestGuild") - conn = census_store.init_db(db_path) + conn = census_store.CensusStore(db_path).init_db() try: - guild_rec = census_store.get_guild(conn, "TestGuild", _WORLD) + guild_rec = census_store.CensusStore.get_guild(conn, "TestGuild", _WORLD) assert guild_rec is not None members = {m["name"]: m for m in guild_rec["data"]["roster"]["members"]} @@ -463,12 +463,12 @@ async def test_persist_merges_offline_member_from_store(app, tmp_path, monkeypat assert "GhostNoData" not in members # OfflineAlt was carried-forward, NOT freshly resolved → timestamp unchanged. - alt_rec = census_store.get_character(conn, "OfflineAlt", _WORLD) + alt_rec = census_store.CensusStore.get_character(conn, "OfflineAlt", _WORLD) assert alt_rec is not None assert alt_rec["last_resolved_at"] == 1000 # OnlineMain genuinely resolved this fetch → record now exists, freshly stamped. - main_rec = census_store.get_character(conn, "OnlineMain", _WORLD) + main_rec = census_store.CensusStore.get_character(conn, "OnlineMain", _WORLD) assert main_rec is not None assert main_rec["last_resolved_at"] > 1000 # The stored member carries its guild, so a cache-miss lookup (e.g. a diff --git a/tests/server/test_parses_ingest_cache.py b/tests/server/test_parses_ingest_cache.py index 1c07445d..7b67cba3 100644 --- a/tests/server/test_parses_ingest_cache.py +++ b/tests/server/test_parses_ingest_cache.py @@ -30,14 +30,14 @@ def model_dump(self): def store_db(tmp_path, monkeypatch): """Temp census_store wired into the ingest path via DB_PATH.""" db = tmp_path / "census.db" - monkeypatch.setattr(census_store, "DB_PATH", db) - conn = census_store.init_db(db) + monkeypatch.setattr(census_store.store, "path", db) + conn = census_store.CensusStore(db).init_db() yield conn conn.close() def _seed(conn, name, **data): - census_store.upsert_character(conn, name, ingest._WORLD, data, resolved=True) + census_store.CensusStore.upsert_character(conn, name, ingest._WORLD, data, resolved=True) def _empty_cache(): @@ -117,7 +117,7 @@ async def test_ilvl_backfill_writes_through_to_store(store_db): assert out["Raider"].ilvl == 350.0 client.get_character.assert_awaited_once() # Written through to the durable store. - rec = census_store.get_character(store_db, "Raider", ingest._WORLD) + rec = census_store.CensusStore.get_character(store_db, "Raider", ingest._WORLD) assert rec is not None assert rec["data"]["ilvl"] == 350.0