all repos — rastro @ main

BitTorrent tracker!

app/db/queries.py (view raw)

 1
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
 100
 101
 102
 103
 104
 105
 106
 107
 108
 109
 110
 111
 112
 113
 114
 115
 116
 117
 118
 119
 120
 121
 122
 123
 124
 125
 126
 127
 128
 129
 130
 131
 132
 133
 134
 135
 136
 137
 138
 139
 140
 141
 142
 143
 144
 145
 146
 147
 148
 149
 150
 151
from __future__ import annotations

from datetime import UTC, datetime, timedelta
from typing import Any

from sqlalchemy import Select, func, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload

from app.db.models import (
    AppState,
    ProbeResult,
    SourceImportRun,
    Tracker,
    TrackerSource,
)


async def get_app_state(session: AsyncSession, key: str) -> str | None:
    row = await session.get(AppState, key)
    return row.value if row else None


async def set_app_state(session: AsyncSession, key: str, value: str) -> None:
    row = await session.get(AppState, key)
    if row is None:
        session.add(AppState(key=key, value=value, updated_at=datetime.now(UTC)))
    else:
        row.value = value
        row.updated_at = datetime.now(UTC)
    await session.commit()


async def count_trackers(session: AsyncSession) -> int:
    result = await session.scalar(select(func.count()).select_from(Tracker))
    return int(result or 0)


async def latest_successful_import_at(session: AsyncSession) -> datetime | None:
    """Finished-at of the most recent import that actually applied data.

    `not_modified` counts as success: the upstream feed was reachable and simply
    unchanged, so the inventory is not stale.
    """
    return await session.scalar(
        select(func.max(SourceImportRun.finished_at)).where(
            SourceImportRun.status.in_(("ok", "not_modified"))
        )
    )


async def overview_stats(session: AsyncSession) -> dict[str, Any]:
    total = await count_trackers(session)
    by_status: dict[str, int] = {}
    rows = await session.execute(
        select(Tracker.current_status, func.count()).group_by(Tracker.current_status)
    )
    for status, count in rows.all():
        by_status[status] = count

    last_import = await session.scalar(
        select(SourceImportRun)
        .where(SourceImportRun.status.in_(("ok", "not_modified", "failed")))
        .order_by(SourceImportRun.finished_at.desc())
        .limit(1)
    )
    last_probe = await session.scalar(
        select(ProbeResult).order_by(ProbeResult.checked_at.desc()).limit(1)
    )

    return {
        "total": total,
        "up": by_status.get("up", 0),
        "degraded": by_status.get("degraded", 0),
        "down": by_status.get("down", 0),
        "unknown": by_status.get("unknown", 0),
        "last_import_at": last_import.finished_at if last_import else None,
        "last_probe_at": last_probe.checked_at if last_probe else None,
    }


def trackers_query(
    *,
    q: str | None = None,
    scheme: str | None = None,
    status: str | None = None,
    source: str | None = None,
    sort: str = "score",
    order: str = "desc",
) -> Select[tuple[Tracker]]:
    stmt = select(Tracker).options(selectinload(Tracker.sources))
    if q:
        like = f"%{q.strip()}%"
        stmt = stmt.where(Tracker.canonical_url.ilike(like) | Tracker.hostname.ilike(like))
    if scheme:
        stmt = stmt.where(Tracker.scheme == scheme.lower())
    if status:
        stmt = stmt.where(Tracker.current_status == status.lower())
    if source:
        stmt = stmt.join(TrackerSource).where(TrackerSource.source_name == source)

    sort_map = {
        "url": Tracker.canonical_url,
        "status": Tracker.current_status,
        "latency": Tracker.last_latency_ms,
        "checked": Tracker.last_checked_at,
        "hostname": Tracker.hostname,
        "protocol": Tracker.scheme,
    }
    col = sort_map.get(sort, Tracker.last_checked_at)
    if order.lower() == "asc":
        stmt = stmt.order_by(col.asc().nullslast(), Tracker.id.asc())
    else:
        stmt = stmt.order_by(col.desc().nullslast(), Tracker.id.desc())
    return stmt


async def get_tracker(session: AsyncSession, tracker_id: int) -> Tracker | None:
    result = await session.execute(
        select(Tracker)
        .options(
            selectinload(Tracker.sources),
            selectinload(Tracker.dns_records),
        )
        .where(Tracker.id == tracker_id)
    )
    return result.scalar_one_or_none()


async def due_trackers(session: AsyncSession, limit: int = 50) -> list[Tracker]:
    now = datetime.now(UTC)
    result = await session.execute(
        select(Tracker)
        .where(
            Tracker.current_status != "unsupported",
            (Tracker.next_check_at.is_(None)) | (Tracker.next_check_at <= now),
        )
        .order_by(Tracker.next_check_at.asc().nullsfirst())
        .limit(limit)
    )
    return list(result.scalars().all())


async def prune_probe_results(session: AsyncSession, retention_days: int) -> int:
    cutoff = datetime.now(UTC) - timedelta(days=retention_days)
    rows = await session.execute(select(ProbeResult).where(ProbeResult.checked_at < cutoff))
    old = list(rows.scalars().all())
    for row in old:
        await session.delete(row)
    await session.commit()
    return len(old)