"""Ports — the contracts the application layer depends on. These are Protocols, not implementations. Concrete adapters live in ``app.infrastructure`` (repositories) and ``app.core.security`` (crypto) and are bound to these ports at the composition root (``app.api.deps``). """ import datetime as dt import uuid from collections.abc import AsyncIterator from contextlib import AbstractAsyncContextManager from pathlib import Path from typing import Protocol from app.domain.entities import Credentials, ObjectStat, User from app.domain.entities.track import Artist, Track from app.domain.tokens import IssuedToken, TokenClaims, TokenType class UserRepository(Protocol): async def get_by_id(self, user_id: uuid.UUID) -> User | None: ... async def get_credentials_by_username(self, username: str) -> Credentials | None: ... async def add(self, *, username: str, password_hash: str, is_superuser: bool) -> User: ... async def list(self, *, limit: int, offset: int) -> list[User]: ... async def set_password_hash(self, user_id: uuid.UUID, password_hash: str) -> None: ... async def set_superuser(self, user_id: uuid.UUID, is_superuser: bool) -> User: ... async def set_active(self, user_id: uuid.UUID, is_active: bool) -> User: ... async def count(self) -> int: ... class RefreshTokenRepository(Protocol): async def add( self, *, jti: uuid.UUID, user_id: uuid.UUID, token_hash: str, expires_at: dt.datetime, ) -> None: ... async def is_valid(self, jti: uuid.UUID) -> bool: """True iff a row exists for ``jti`` that is neither revoked nor expired.""" ... async def revoke(self, jti: uuid.UUID) -> None: ... async def revoke_all_for_user(self, user_id: uuid.UUID) -> None: ... class PasswordHasher(Protocol): def hash(self, password: str) -> str: ... def verify_and_update(self, password: str, password_hash: str) -> tuple[bool, str | None]: """Verify ``password`` against ``password_hash``. Returns ``(is_valid, updated_hash)`` where ``updated_hash`` is a fresh hash to persist when the stored one uses outdated parameters, else ``None``.""" ... class TokenService(Protocol): def issue(self, *, subject: uuid.UUID, token_type: TokenType) -> IssuedToken: ... def decode(self, encoded: str) -> TokenClaims: """Verify signature + expiry and return claims. Raises :class:`~app.domain.errors.AuthenticationError` on any failure.""" ... class FileStorage(Protocol): async def save_file(self, key: str, src_path: Path) -> int: ... async def open_range( self, key: str, start: int, end: int | None ) -> tuple[AsyncIterator[bytes], int]: ... async def stat(self, key: str) -> ObjectStat: ... async def exists(self, key: str) -> bool: ... async def delete(self, key: str) -> None: ... def as_local_path(self, key: str) -> AbstractAsyncContextManager[Path]: ... class ArtistRepository(Protocol): async def get_or_create(self, name: str) -> Artist: ... class TrackRepository(Protocol): async def get_by_id(self, track_id: uuid.UUID) -> Track | None: ... async def get_by_source(self, source: str, source_id: str) -> Track | None: ... async def add( self, *, id: uuid.UUID, title: str, artist_id: uuid.UUID, file_path: str, file_format: str, file_size: int, source: str, source_id: str, metadata_status: str, added_by: uuid.UUID | None, ) -> Track: ... async def delete(self, track_id: uuid.UUID) -> None: ...