"""User repository interface for abstracting database operations.""" from abc import ABC, abstractmethod from app.gateway.auth.models import User class UserNotFoundError(LookupError): """Raised when a user repository operation targets a non-existent row. Subclass of :class:`LookupError` so callers that already catch ``LookupError`` for "missing entity" can keep working unchanged, while specific call sites can pin to this class to distinguish "concurrent delete during update" from other lookups. """ class UserRepository(ABC): """Abstract interface for user data storage. Implement this interface to support different storage backends (SQLite) """ @abstractmethod async def create_user(self, user: User) -> User: """Create a new user. Args: user: User object to create Returns: Created User with ID assigned Raises: ValueError: If email already exists """ raise NotImplementedError @abstractmethod async def get_user_by_id(self, user_id: str) -> User | None: """Get user by ID. Args: user_id: User UUID as string Returns: User if found, None otherwise """ raise NotImplementedError @abstractmethod async def get_user_by_email(self, email: str) -> User | None: """Get user by email. Args: email: User email address Returns: User if found, None otherwise """ raise NotImplementedError @abstractmethod async def find_user_by_email_local_part(self, local_part: str) -> User | None: """Find a user by the local part of their email (the bit before ``@``). Used by the passwordless username-login flow so that a single username keeps mapping to the same account even when the synthetic email domain (e.g. ``cm.com`` vs the legacy ``local.deerflow``) differs between registration and current login. Returns the oldest matching user when multiple rows share the same local part, so collisions resolve deterministically. Args: local_part: Substring to match before the ``@``. Case-insensitive. Returns: User if found, None otherwise. """ raise NotImplementedError async def find_users_by_email_local_part_like(self, substring: str, *, limit: int = 500) -> list["User"]: """Fuzzy-match users whose email local part *contains* ``substring``. Backs the admin LLM-metrics username filter (按用户名模糊搜索). Unlike :meth:`find_user_by_email_local_part` (exact prefix → single account), this returns every matching account. Case-insensitive substring match against the part before ``@``. Non-overriding repositories return an empty list. Args: substring: Case-insensitive substring to match within the local part. limit: Maximum number of users to return. Returns: Matching users (may be empty). """ return [] @abstractmethod async def update_user(self, user: User) -> User: """Update an existing user. Args: user: User object with updated fields Returns: Updated User Raises: UserNotFoundError: If no row exists for ``user.id``. This is a hard failure (not a no-op) so callers cannot mistake a concurrent-delete race for a successful update. """ raise NotImplementedError @abstractmethod async def count_users(self) -> int: """Return total number of registered users.""" raise NotImplementedError @abstractmethod async def count_admin_users(self) -> int: """Return number of users with system_role == 'admin'.""" raise NotImplementedError @abstractmethod async def list_users(self) -> list[User]: """Return all users ordered by creation date.""" raise NotImplementedError @abstractmethod async def get_user_by_oauth(self, provider: str, oauth_id: str) -> User | None: """Get user by OAuth provider and ID. Args: provider: OAuth provider name (e.g. 'github', 'google') oauth_id: User ID from the OAuth provider Returns: User if found, None otherwise """ raise NotImplementedError