"""Blocklist sources repository. CRUD operations for the ``blocklist_sources`` table in the application SQLite database. All methods accept a :class:`aiosqlite.Connection` — no ORM, no HTTP exceptions. """ from __future__ import annotations from typing import TYPE_CHECKING, Any if TYPE_CHECKING: import aiosqlite async def create_source( db: aiosqlite.Connection, name: str, url: str, *, enabled: bool = True, ) -> int: """Insert a new blocklist source and return its generated id. Args: db: Active aiosqlite connection. name: Human-readable display name. url: URL of the blocklist text file. enabled: Whether the source is active. Defaults to ``True``. Returns: The ``ROWID`` / primary key of the new row. """ cursor = await db.execute( """ INSERT INTO blocklist_sources (name, url, enabled) VALUES (?, ?, ?) """, (name, url, int(enabled)), ) await db.commit() return int(cursor.lastrowid) # type: ignore[arg-type] async def get_source( db: aiosqlite.Connection, source_id: int, ) -> dict[str, Any] | None: """Return a single blocklist source row as a plain dict, or ``None``. Args: db: Active aiosqlite connection. source_id: Primary key of the source to retrieve. Returns: A dict with keys matching the ``blocklist_sources`` columns, or ``None`` if no row with that id exists. """ async with db.execute( "SELECT id, name, url, enabled, created_at, updated_at FROM blocklist_sources WHERE id = ?", (source_id,), ) as cursor: row = await cursor.fetchone() if row is None: return None return _row_to_dict(row) async def list_sources(db: aiosqlite.Connection) -> list[dict[str, Any]]: """Return all blocklist sources ordered by id ascending. Args: db: Active aiosqlite connection. Returns: List of dicts, one per row in ``blocklist_sources``. """ async with db.execute( "SELECT id, name, url, enabled, created_at, updated_at FROM blocklist_sources ORDER BY id" ) as cursor: rows = await cursor.fetchall() return [_row_to_dict(r) for r in rows] async def list_enabled_sources(db: aiosqlite.Connection) -> list[dict[str, Any]]: """Return only enabled blocklist sources ordered by id. Args: db: Active aiosqlite connection. Returns: List of dicts for rows where ``enabled = 1``. """ async with db.execute( "SELECT id, name, url, enabled, created_at, updated_at FROM blocklist_sources WHERE enabled = 1 ORDER BY id" ) as cursor: rows = await cursor.fetchall() return [_row_to_dict(r) for r in rows] async def update_source( db: aiosqlite.Connection, source_id: int, *, name: str | None = None, url: str | None = None, enabled: bool | None = None, ) -> bool: """Update one or more fields on a blocklist source. Only the keyword arguments that are not ``None`` are included in the ``UPDATE`` statement. Args: db: Active aiosqlite connection. source_id: Primary key of the source to update. name: New display name, or ``None`` to leave unchanged. url: New URL, or ``None`` to leave unchanged. enabled: New enabled flag, or ``None`` to leave unchanged. Returns: ``True`` if a row was updated, ``False`` if the id does not exist. """ fields: list[str] = [] params: list[Any] = [] if name is not None: fields.append("name = ?") params.append(name) if url is not None: fields.append("url = ?") params.append(url) if enabled is not None: fields.append("enabled = ?") params.append(int(enabled)) if not fields: # Nothing to update — treat as success only if the row exists. return await get_source(db, source_id) is not None fields.append("updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')") params.append(source_id) cursor = await db.execute( f"UPDATE blocklist_sources SET {', '.join(fields)} WHERE id = ?", # noqa: S608 params, ) await db.commit() return cursor.rowcount > 0 async def delete_source(db: aiosqlite.Connection, source_id: int) -> bool: """Delete a blocklist source by id. Args: db: Active aiosqlite connection. source_id: Primary key of the source to remove. Returns: ``True`` if a row was deleted, ``False`` if the id did not exist. """ cursor = await db.execute( "DELETE FROM blocklist_sources WHERE id = ?", (source_id,), ) await db.commit() return cursor.rowcount > 0 # --------------------------------------------------------------------------- # Internal helpers # --------------------------------------------------------------------------- def _row_to_dict(row: Any) -> dict[str, Any]: """Convert an aiosqlite row to a plain Python dict. Args: row: An :class:`aiosqlite.Row` or sequence returned by a cursor. Returns: ``dict`` mapping column names to values with ``enabled`` cast to ``bool``. """ d: dict[str, Any] = dict(row) d["enabled"] = bool(d["enabled"]) return d