""" pypi-server.i80.dk catalog fetcher with in-memory TTL cache. Fetches the package list at startup and refreshes every hour. No persistent storage needed — all data lives in memory. """ import asyncio import logging import time from typing import Any import aiohttp from lsprotocol.types import CompletionItem, CompletionItemKind, MarkupContent, MarkupKind logger = logging.getLogger(__name__) PYPI_SIMPLE_URL = "https://pypi-server.i80.dk/simple/" PYPI_BASE_URL = "https://pypi-server.i80.dk" REFRESH_INTERVAL = 3600 # seconds class PypiCatalog: """Thread-safe singleton catalog for pypi-server.i80.dk packages.""" _packages: list[dict[str, Any]] = [] _last_refresh: float = 0 _lock = asyncio.Lock() _refresh_task: asyncio.Task | None = None @classmethod async def get_packages(cls) -> list[dict[str, Any]]: if not cls._packages or time.time() - cls._last_refresh > REFRESH_INTERVAL: await cls._refresh() return cls._packages @classmethod async def start_background_refresh(cls) -> None: if cls._refresh_task is None or cls._refresh_task.done(): cls._refresh_task = asyncio.create_task(cls._refresh_loop()) @classmethod async def _refresh_loop(cls) -> None: while True: await cls._refresh() await asyncio.sleep(REFRESH_INTERVAL) @classmethod async def _refresh(cls) -> None: async with cls._lock: try: packages = await cls._fetch() cls._packages = packages cls._last_refresh = time.time() logger.info("PyPI catalog refreshed: %d packages", len(packages)) except Exception: logger.exception("Failed to refresh PyPI catalog") @classmethod async def _fetch(cls) -> list[dict[str, Any]]: packages = [] async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=15)) as session: async with session.get(PYPI_SIMPLE_URL) as resp: resp.raise_for_status() html = await resp.text() # Parse simple index HTML — each pkg-name import re for match in re.finditer(r'href="[^"]+/([^/]+)/"[^>]*>([^<]+)<', html): pkg_name = match.group(2).strip() packages.append({ "name": pkg_name, "label": pkg_name, "detail": f"i80 package — pypi-server.i80.dk", "sort_prefix": "0_i80_", # sorts before standard packages }) return packages @classmethod def as_completion_items(cls) -> list[CompletionItem]: return [ CompletionItem( label=pkg["name"], kind=CompletionItemKind.Module, detail=pkg["detail"], sort_text=f"{pkg['sort_prefix']}{pkg['name']}", documentation=MarkupContent( kind=MarkupKind.Markdown, value=f"**{pkg['name']}**\n\nCustom i80/LRU package from `pypi-server.i80.dk`", ), ) for pkg in cls._packages ]