deerflow-code/offline-backend-20260512/backend/app/gateway/auth/repositories/base.py
2026-09-07 18:24:55 +08:00

146 lines
4.4 KiB
Python

"""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