"""Geo and IP lookup Pydantic models. Response models for the ``GET /api/geo/lookup/{ip}`` endpoint. """ from __future__ import annotations from collections.abc import Awaitable, Callable from dataclasses import dataclass from typing import TYPE_CHECKING from pydantic import BaseModel, ConfigDict, Field if TYPE_CHECKING: import aiohttp import aiosqlite class GeoDetail(BaseModel): """Enriched geolocation data for an IP address. Populated from the ip-api.com free API. """ model_config = ConfigDict(strict=True) country_code: str | None = Field( default=None, description="ISO 3166-1 alpha-2 country code.", ) country_name: str | None = Field( default=None, description="Human-readable country name.", ) asn: str | None = Field( default=None, description="Autonomous System Number (e.g. ``'AS3320'``).", ) org: str | None = Field( default=None, description="Organisation associated with the ASN.", ) class GeoCacheStatsResponse(BaseModel): """Response for ``GET /api/geo/stats``. Exposes diagnostic counters of the geo cache subsystem so operators can assess resolution health from the UI or CLI. """ model_config = ConfigDict(strict=True) cache_size: int = Field(..., description="Number of positive entries in the in-memory cache.") unresolved: int = Field(..., description="Number of geo_cache rows with country_code IS NULL.") neg_cache_size: int = Field(..., description="Number of entries in the in-memory negative cache.") dirty_size: int = Field(..., description="Number of newly resolved entries not yet flushed to disk.") class IpLookupResponse(BaseModel): """Response for ``GET /api/geo/lookup/{ip}``. Aggregates current ban status and geographical information for an IP. """ model_config = ConfigDict(strict=True) ip: str = Field(..., description="The queried IP address.") currently_banned_in: list[str] = Field( default_factory=list, description="Names of jails where this IP is currently banned.", ) geo: GeoDetail | None = Field( default=None, description="Enriched geographical and network information.", ) # --------------------------------------------------------------------------- # shared service types # --------------------------------------------------------------------------- @dataclass class GeoInfo: """Geo resolution result used throughout backend services.""" country_code: str | None country_name: str | None asn: str | None org: str | None GeoEnricher = Callable[[str], Awaitable[GeoInfo | None]] GeoBatchLookup = Callable[ [list[str], "aiohttp.ClientSession", "aiosqlite.Connection | None"], Awaitable[dict[str, GeoInfo]], ] GeoCacheLookup = Callable[[list[str]], tuple[dict[str, GeoInfo], list[str]]]