- blocklist_repo.py: CRUD for blocklist_sources table - import_log_repo.py: add/list/get-last log entries - blocklist_service.py: source CRUD, preview, import (download/validate/ban), import_all, schedule get/set/info - blocklist_import.py: APScheduler task (hourly/daily/weekly schedule triggers) - blocklist.py router: 9 endpoints (list/create/update/delete/preview/import/ schedule-get+put/log) - blocklist.py models: ScheduleFrequency (StrEnum), ScheduleConfig, ScheduleInfo, ImportSourceResult, ImportRunResult, PreviewResponse - 59 new tests (18 repo + 19 service + 22 router); 374 total pass - ruff clean, mypy clean for Stage 10 files - types/blocklist.ts, api/blocklist.ts, hooks/useBlocklist.ts - BlocklistsPage.tsx: source management, schedule picker, import log table - Frontend tsc + ESLint clean
188 lines
5.2 KiB
Python
188 lines
5.2 KiB
Python
"""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
|