feat(db): SharedTableProtocol + Store.clone() for dual-backend support
This commit is contained in:
parent
6fbcf90740
commit
78809c761e
3 changed files with 77 additions and 0 deletions
57
app/db/protocol.py
Normal file
57
app/db/protocol.py
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
"""Protocol (duck-type interface) for shared table backends (SQLite and Postgres)."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Optional, Protocol, runtime_checkable
|
||||||
|
|
||||||
|
from app.db.models import MarketComp, Seller
|
||||||
|
|
||||||
|
|
||||||
|
@runtime_checkable
|
||||||
|
class SharedTableProtocol(Protocol):
|
||||||
|
"""Protocol that both Store (SQLite) and SnipeSharedStore (Postgres) must satisfy.
|
||||||
|
|
||||||
|
This enables code that reads/writes shared tables (sellers, market_comps, reported_sellers)
|
||||||
|
to remain agnostic to the underlying backend.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def save_seller(self, seller: Seller) -> None:
|
||||||
|
"""Persist a single seller record."""
|
||||||
|
...
|
||||||
|
|
||||||
|
def save_sellers(self, sellers: list[Seller]) -> None:
|
||||||
|
"""Persist multiple seller records (batch upsert)."""
|
||||||
|
...
|
||||||
|
|
||||||
|
def get_seller(self, platform: str, platform_seller_id: str) -> Optional[Seller]:
|
||||||
|
"""Fetch a single seller by platform and platform_seller_id."""
|
||||||
|
...
|
||||||
|
|
||||||
|
def save_market_comp(self, comp: MarketComp) -> None:
|
||||||
|
"""Persist a market comparison record."""
|
||||||
|
...
|
||||||
|
|
||||||
|
def get_market_comp(self, platform: str, query_hash: str) -> Optional[MarketComp]:
|
||||||
|
"""Fetch a market comparison by platform and query_hash."""
|
||||||
|
...
|
||||||
|
|
||||||
|
def mark_reported(
|
||||||
|
self,
|
||||||
|
platform: str,
|
||||||
|
platform_seller_id: str,
|
||||||
|
username: Optional[str] = None,
|
||||||
|
reported_by: str = "user",
|
||||||
|
) -> None:
|
||||||
|
"""Record that a seller has been reported to the platform."""
|
||||||
|
...
|
||||||
|
|
||||||
|
def list_reported(self, platform: str = "ebay") -> list[str]:
|
||||||
|
"""Return all platform_seller_ids that have been reported."""
|
||||||
|
...
|
||||||
|
|
||||||
|
def delete_seller_data(self, platform: str, platform_seller_id: str) -> None:
|
||||||
|
"""Permanently erase a seller and all related data (GDPR/eBay compliance)."""
|
||||||
|
...
|
||||||
|
|
||||||
|
def clone(self) -> SharedTableProtocol:
|
||||||
|
"""Create a new independent instance pointing to the same backend."""
|
||||||
|
...
|
||||||
|
|
@ -21,6 +21,10 @@ class Store:
|
||||||
# WAL mode: allows concurrent readers + one writer without blocking
|
# WAL mode: allows concurrent readers + one writer without blocking
|
||||||
self._conn.execute("PRAGMA journal_mode=WAL")
|
self._conn.execute("PRAGMA journal_mode=WAL")
|
||||||
|
|
||||||
|
def clone(self) -> Store:
|
||||||
|
"""Create a new independent instance pointing to the same database."""
|
||||||
|
return Store(self._db_path)
|
||||||
|
|
||||||
# --- Seller ---
|
# --- Seller ---
|
||||||
|
|
||||||
def delete_seller_data(self, platform: str, platform_seller_id: str) -> None:
|
def delete_seller_data(self, platform: str, platform_seller_id: str) -> None:
|
||||||
|
|
|
||||||
16
tests/db/test_protocol.py
Normal file
16
tests/db/test_protocol.py
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
"""Verify Store satisfies SharedTableProtocol at import time."""
|
||||||
|
from app.db.protocol import SharedTableProtocol
|
||||||
|
from app.db.store import Store
|
||||||
|
|
||||||
|
|
||||||
|
def test_store_satisfies_protocol():
|
||||||
|
assert issubclass(Store, SharedTableProtocol)
|
||||||
|
|
||||||
|
|
||||||
|
def test_store_clone_returns_new_instance(tmp_path):
|
||||||
|
db = tmp_path / "test.db"
|
||||||
|
s = Store(db)
|
||||||
|
clone = s.clone()
|
||||||
|
assert isinstance(clone, Store)
|
||||||
|
assert clone is not s
|
||||||
|
assert clone._db_path == db
|
||||||
Loading…
Reference in a new issue