Initial public release

This commit is contained in:
Harvey
2026-04-03 15:53:00 +01:00
commit a99c738164
679 changed files with 108326 additions and 0 deletions
+90
View File
@@ -0,0 +1,90 @@
import httpx
import logging
from typing import Optional
from core.config import Settings, get_settings
logger = logging.getLogger(__name__)
def _get_user_agent(settings: Optional[Settings] = None) -> str:
if settings:
return settings.get_user_agent()
return get_settings().get_user_agent()
class HttpClientFactory:
_clients: dict[str, httpx.AsyncClient] = {}
@classmethod
def get_client(
cls,
name: str = "default",
timeout: float = 10.0,
connect_timeout: float = 5.0,
max_connections: int = 200,
max_keepalive: int = 200,
settings: Optional[Settings] = None,
http2: bool = True,
**kwargs
) -> httpx.AsyncClient:
if name not in cls._clients:
cls._clients[name] = httpx.AsyncClient(
http2=http2,
timeout=httpx.Timeout(timeout, connect=connect_timeout),
limits=httpx.Limits(
max_connections=max_connections,
max_keepalive_connections=max_keepalive,
keepalive_expiry=60.0,
),
follow_redirects=True,
transport=httpx.AsyncHTTPTransport(http2=http2, retries=0),
headers={"User-Agent": _get_user_agent(settings)},
**kwargs
)
return cls._clients[name]
@classmethod
async def close_all(cls) -> None:
for client in cls._clients.values():
await client.aclose()
cls._clients.clear()
def get_http_client(
settings: Optional[Settings] = None,
timeout: Optional[float] = None,
connect_timeout: Optional[float] = None,
max_connections: Optional[int] = None,
) -> httpx.AsyncClient:
if settings is None:
settings = get_settings()
return HttpClientFactory.get_client(
name="default",
timeout=timeout or settings.http_timeout,
connect_timeout=connect_timeout or settings.http_connect_timeout,
max_connections=max_connections or settings.http_max_connections,
max_keepalive=settings.http_max_keepalive,
settings=settings,
)
async def close_http_clients() -> None:
await HttpClientFactory.close_all()
def get_listenbrainz_http_client(
settings: Optional[Settings] = None,
timeout: Optional[float] = None,
connect_timeout: Optional[float] = None,
) -> httpx.AsyncClient:
if settings is None:
settings = get_settings()
return HttpClientFactory.get_client(
name="listenbrainz",
timeout=timeout or settings.http_timeout,
connect_timeout=connect_timeout or settings.http_connect_timeout,
max_connections=20,
max_keepalive=20,
settings=settings,
http2=False,
)
@@ -0,0 +1,86 @@
import asyncio
import logging
from typing import TypeVar, Awaitable, Callable, Any
from functools import wraps
from core.exceptions import ClientDisconnectedError
logger = logging.getLogger(__name__)
T = TypeVar("T")
class RequestDeduplicator:
"""
Prevents duplicate concurrent requests by coalescing identical requests.
If request A is in-flight and request B arrives with the same key,
request B will wait for A's result instead of making a duplicate call.
"""
def __init__(self):
self._pending: dict[str, asyncio.Future[Any]] = {}
self._lock = asyncio.Lock()
async def dedupe(
self,
key: str,
coro_factory: Callable[[], Awaitable[T]]
) -> T:
while True:
async with self._lock:
if key in self._pending:
logger.debug(f"Deduplicating request: {key}")
future = self._pending[key]
should_execute = False
else:
future = asyncio.get_running_loop().create_future()
self._pending[key] = future
should_execute = True
if should_execute:
try:
result = await coro_factory()
future.set_result(result)
except ClientDisconnectedError:
future.cancel()
raise
except Exception as e: # noqa: BLE001
future.set_exception(e)
finally:
if not future.done():
future.cancel()
async with self._lock:
self._pending.pop(key, None)
try:
return await future
except asyncio.CancelledError:
continue
_global_deduplicator = RequestDeduplicator()
def get_deduplicator() -> RequestDeduplicator:
return _global_deduplicator
def deduplicate(key_func: Callable[..., str]):
"""
Decorator that deduplicates concurrent calls to the same function
with the same key.
Usage:
@deduplicate(lambda self, artist_id: f"artist:{artist_id}")
async def get_artist(self, artist_id: str) -> Artist:
...
"""
def decorator(func: Callable[..., Awaitable[T]]) -> Callable[..., Awaitable[T]]:
@wraps(func)
async def wrapper(*args, **kwargs) -> T:
key = key_func(*args, **kwargs)
dedup = get_deduplicator()
return await dedup.dedupe(key, lambda: func(*args, **kwargs))
return wrapper
return decorator
+18
View File
@@ -0,0 +1,18 @@
from __future__ import annotations
import logging
from collections.abc import Awaitable, Callable
from core.exceptions import ClientDisconnectedError
logger = logging.getLogger(__name__)
DisconnectCallable = Callable[[], Awaitable[bool]]
async def check_disconnected(
is_disconnected: DisconnectCallable | None,
) -> None:
if is_disconnected is not None and await is_disconnected():
logger.debug("Client disconnected — aborting cover fetch")
raise ClientDisconnectedError("Client disconnected")