from __future__ import annotations from dataclasses import dataclass from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload from app.db.models import ProbeResult, Tracker from app.services.scoring import TrackerMetrics, load_metrics @dataclass class SmartListFilters: protocol: str | None = None # udp|http|https min_uptime: float = 0.95 max_latency_ms: float | None = None ip_version: str = "any" # any|ipv4|ipv6 limit: int = 20 min_age_days: float = 0.0 diversity: bool = True require_valid: bool = True min_measurements: int = 10 @dataclass class SmartListEntry: tracker: Tracker metrics: TrackerMetrics async def build_smart_list( session: AsyncSession, filters: SmartListFilters | None = None ) -> list[SmartListEntry]: filters = filters or SmartListFilters() limit = max(1, min(50, filters.limit)) stmt = ( select(Tracker).where(Tracker.current_status == "up").options(selectinload(Tracker.sources)) ) if filters.protocol: stmt = stmt.where(Tracker.scheme == filters.protocol.lower()) result = await session.execute(stmt) trackers = list(result.scalars().unique().all()) scored: list[SmartListEntry] = [] for tracker in trackers: metrics = await load_metrics(session, tracker) if metrics.measurement_count < filters.min_measurements: continue if filters.require_valid and (metrics.valid_rate_7d or 0) <= 0: # require at least some valid responses; last probe should be valid last = await session.execute( select(ProbeResult) .where(ProbeResult.tracker_id == tracker.id) .order_by(ProbeResult.checked_at.desc()) .limit(1) ) last_row = last.scalar_one_or_none() if last_row is None or not last_row.response_valid: continue if metrics.uptime_7d is None or metrics.uptime_7d < filters.min_uptime: continue if filters.max_latency_ms is not None and ( metrics.latency_median_7d is None or metrics.latency_median_7d > filters.max_latency_ms ): continue if metrics.tracking_days < filters.min_age_days: continue if filters.ip_version == "ipv4" and not tracker.supports_ipv4: continue if filters.ip_version == "ipv6" and tracker.supports_ipv6 != "true": continue scored.append(SmartListEntry(tracker=tracker, metrics=metrics)) scored.sort( key=lambda e: ( -(e.metrics.score or 0), e.metrics.latency_median_7d if e.metrics.latency_median_7d is not None else 1e9, ) ) if not filters.diversity: return scored[:limit] selected: list[SmartListEntry] = [] seen_fps: set[str] = set() asn_counts: dict[int, int] = {} for entry in scored: fp = entry.tracker.infrastructure_fingerprint if fp and fp in seen_fps: continue asn = entry.tracker.asn if asn is not None and asn_counts.get(asn, 0) >= 2: continue selected.append(entry) if fp: seen_fps.add(fp) if asn is not None: asn_counts[asn] = asn_counts.get(asn, 0) + 1 if len(selected) >= limit: break return selected def format_smart_list_txt(entries: list[SmartListEntry]) -> str: if not entries: return "" return "\n\n".join(e.tracker.canonical_url for e in entries) + "\n" def format_smart_list_json(entries: list[SmartListEntry]) -> list[dict]: out = [] for e in entries: out.append( { "id": e.tracker.id, "url": e.tracker.canonical_url, "scheme": e.tracker.scheme, "score": e.metrics.score, "uptime_7d": e.metrics.uptime_7d, "latency_median_7d": e.metrics.latency_median_7d, "infrastructure_fingerprint": e.tracker.infrastructure_fingerprint, "asn": e.tracker.asn, } ) return out async def build_live_list(session: AsyncSession) -> list[str]: result = await session.execute( select(Tracker) .where(Tracker.current_status == "up") .order_by(Tracker.last_latency_ms.asc().nullslast()) ) return [t.canonical_url for t in result.scalars().all()]