Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@

from cq.models import KnowledgeUnit

from .scoring import calculate_relevance
from .tables import ensure_api_keys_table, ensure_review_columns, ensure_users_table
from ..scoring import calculate_relevance
from ..tables import ensure_api_keys_table, ensure_review_columns, ensure_users_table
from ._protocol import Store

__all__ = ["DEFAULT_DB_PATH", "RemoteStore", "Store", "normalize_domains"]

_logger = logging.getLogger(__name__)

Expand Down
157 changes: 157 additions & 0 deletions server/backend/src/cq_server/store/_protocol.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
"""Async `Store` protocol for the cq server.

Pure interface definition — no implementations live here. Concrete
backends (`SqliteStore`, later `PostgresStore`) will conform to this
protocol so callers can depend on the surface without caring about
dialect. Early implementations may shim native sync drivers via a
threadpool; the protocol itself stays async.
"""

from typing import Any, Protocol, runtime_checkable

from cq.models import KnowledgeUnit


@runtime_checkable
class Store(Protocol):
"""Async storage protocol for the cq server.

Implementations are expected to be one-per-dialect (`SqliteStore`,
`PostgresStore`). Method names and argument shapes match the current
`RemoteStore` exactly so callers migrate without rewriting call sites.
"""

async def close(self) -> None:
"""Release underlying connections/resources; idempotent."""
...

async def insert(self, unit: KnowledgeUnit) -> None:
"""Insert a knowledge unit; raises on id conflict or empty domains."""
...

async def get(self, unit_id: str) -> KnowledgeUnit | None:
"""Return an approved KU by id, or None if missing or not approved."""
...

async def get_any(self, unit_id: str) -> KnowledgeUnit | None:
"""Return a KU by id regardless of review status, or None if missing."""
...

async def get_review_status(self, unit_id: str) -> dict[str, str | None] | None:
"""Return review metadata (status, reviewed_by, reviewed_at) or None."""
...

async def set_review_status(self, unit_id: str, status: str, reviewed_by: str) -> None:
"""Update a KU's review status; raises KeyError if the id is unknown."""
...

async def update(self, unit: KnowledgeUnit) -> None:
"""Replace an existing KU; raises KeyError if the id is unknown."""
...

async def query(
self,
domains: list[str],
*,
languages: list[str] | None = None,
frameworks: list[str] | None = None,
pattern: str = "",
limit: int = 5,
) -> list[KnowledgeUnit]:
"""Return approved KUs matching any of the domains, ranked by relevance."""
...

async def count(self) -> int:
"""Return the total number of KUs in the store."""
...

async def domain_counts(self) -> dict[str, int]:
"""Return approved KU counts keyed by domain tag."""
...

async def pending_queue(self, *, limit: int = 20, offset: int = 0) -> list[dict[str, Any]]:
"""Return pending KUs with review metadata, oldest first."""
...

async def pending_count(self) -> int:
"""Return the number of pending KUs."""
...

async def counts_by_status(self) -> dict[str, int]:
"""Return KU counts keyed by review status."""
...

async def counts_by_tier(self) -> dict[str, int]:
"""Return approved KU counts keyed by tier."""
...

async def list_units(
self,
*,
domain: str | None = None,
confidence_min: float | None = None,
confidence_max: float | None = None,
status: str | None = None,
limit: int = 100,
) -> list[dict[str, Any]]:
"""Return KUs with review metadata, filtered by domain/confidence/status."""
...

async def create_user(self, username: str, password_hash: str) -> None:
"""Insert a new user; raises on duplicate username."""
...

async def get_user(self, username: str) -> dict[str, Any] | None:
"""Return user row by username, or None if missing."""
...

async def count_active_api_keys_for_user(self, user_id: int) -> int:
"""Return the number of non-revoked, non-expired API keys for a user."""
...

async def create_api_key(
self,
*,
key_id: str,
user_id: int,
name: str,
labels: list[str],
key_prefix: str,
key_hash: str,
ttl: str,
expires_at: str,
) -> dict[str, Any]:
"""Insert a new API key row and return the inserted row."""
...

async def get_api_key_for_user(self, *, user_id: int, key_id: str) -> dict[str, Any] | None:
"""Return the key row if it exists and belongs to the user, else None."""
...

async def get_active_api_key_by_id(self, key_id: str) -> dict[str, Any] | None:
"""Return the active key row (including the owner's username) by id, or None if missing, revoked, or expired."""
...

async def list_api_keys_for_user(self, user_id: int) -> list[dict[str, Any]]:
"""Return all API keys owned by the user, newest first."""
...

async def revoke_api_key(self, *, user_id: int, key_id: str) -> bool:
"""Mark the key revoked; return True if a row was updated."""
...

async def touch_api_key_last_used(self, key_id: str) -> None:
"""Best-effort update of ``last_used_at``; errors are swallowed."""
...

async def confidence_distribution(self) -> dict[str, int]:
"""Return confidence-bucket counts for approved KUs."""
...

async def recent_activity(self, limit: int = 20) -> list[dict[str, Any]]:
"""Return recent activity events (one per KU), newest first."""
...

async def daily_counts(self, *, days: int = 30) -> list[dict[str, Any]]:
"""Return per-day proposed/approved/rejected counts; raises on days <= 0."""
...
14 changes: 14 additions & 0 deletions server/backend/tests/test_store_protocol.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""Structural test: `RemoteStore` satisfies the async `Store` protocol.
`@runtime_checkable` protocols check attribute names only, not signatures
or sync-vs-async — so today's sync `RemoteStore` qualifies as long as
every method name the protocol declares exists on the class. Once
`SqliteStore` (issue #308) replaces `RemoteStore` with an actually-async
implementation, this same test stays valid.
"""

from cq_server.store import RemoteStore, Store


def test_remote_store_satisfies_store_protocol() -> None:
assert issubclass(RemoteStore, Store)
Loading