"""# Summary Generic CRUD service adaptor over a Mongo repository. Bridge pydantic models and mongo-ops documents, exposing create, read, update, patch, delete, and search operations for every domain that needs them. """ from typing import Generic, TypeVar from mongo_ops import BaseDocument, PopulatingRepository from mongo_ops.models import PyObjectId from pydantic import BaseModel from infra.db.repos import mongo Model = TypeVar("Model", bound=BaseModel) Schema = TypeVar("Schema", bound=BaseDocument) class CrudsServiceAdaptor(Generic[Model, Schema]): """Adapt a generic Mongo repository to a model/schema CRUD service. Attributes: repo (mongo.BaseRepository): Repository backing all CRUD operations. model (Type[Model]): Pydantic model type handled by this service. schema (Type[Schema]): Document schema type persisted by the repository. """ repo: mongo.BaseRepository model: type[Model] schema: type[Schema] def __init__(self, *, repo=None): """Initialize the adaptor with a backing repository. Args: repo (mongo.BaseRepository): Repository used for all operations. """ self.repo = repo @staticmethod def model_to_dict(model_obj: Model) -> dict: """Serialize a model into a sparse dictionary. Args: model_obj (Model): Model instance to serialize. Returns: dict: Fields that are set and not ``None``. """ return model_obj.model_dump(exclude_unset=True, exclude_none=True) async def create(self, data: Model | dict) -> Schema: """Create a document from a model or raw mapping. Args: data (Model | dict): Model instance or field mapping to persist. Returns: Schema: Persisted document. """ if isinstance(data, dict): data = self.schema(**data) return await self.repo.create(data) async def read(self, id: PyObjectId) -> Schema | None: """Read a document by primary key. Args: id (PyObjectId): Document identifier. Returns: Schema | None: Matching document, or ``None`` when absent. """ return await self.repo.get_by_id(id) async def update(self, id: PyObjectId, data: Model | dict) -> Schema | None: """Replace a document by primary key. Args: id (PyObjectId): Document identifier. data (Model | dict): Replacement model or field mapping. Returns: Schema | None: Updated document, or ``None`` when absent. Notes: A populating repository expects a document *model* (so FK fields can be depopulated), while a base repository expects a raw mapping. This method normalizes both inputs to whatever the repo requires. """ if isinstance(self.repo, PopulatingRepository): if not isinstance(data, self.schema): fields = data if isinstance(data, dict) else self.model_to_dict(data) data = self.schema(**fields) return await self.repo.update(id, data) return await self.repo.update( id, data if isinstance(data, dict) else self.model_to_dict(data), ) async def patch(self, id: PyObjectId, data: dict) -> Schema | None: """Partially update a document by primary key. Args: id (PyObjectId): Document identifier. data (dict): Fields to patch. Returns: Schema | None: Updated document, or ``None`` when absent. """ return await self.repo.patch(id, data) async def delete(self, id: PyObjectId) -> bool: """Delete a document by primary key. Args: id (PyObjectId): Document identifier. Returns: bool: ``True`` when a document was deleted. """ return await self.repo.delete(id) async def search(self, filter: dict | None = None, *args, **kwargs) -> list[Schema]: """List documents matching an optional filter. Args: filter (dict | None): Query filter passed to the repository. Returns: list[Schema]: Matching documents. Notes: Extra positional and keyword arguments are forwarded to the repository. """ return await self.repo.get_many(filter=filter, *args, **kwargs)