Files
Vishesh 'ironeagle' Bangotra 6c0d859a1e feat(standards): fix F11-F16 and rebuild bundle (43 components) + trial findings
- F11 PopulateRule field_name/collection_name corrections
- F12 service adaptor update handles PopulatingRepository
- F13 nested-references section; F14 authless /users/me guidance
- F15 template ruff-clean; F16 stop vendoring .ruff_cache, ship .gitignore
- document --template-style in build_recipe/spec_first_routing; refresh collected
  openapi-first + standards bundle (12 modules, 43 components)
- add trial.md and trial_findings.md (F1-F16, re-trial A/B evidence)
2026-10-13 19:35:12 +05:30

138 lines
4.4 KiB
Python

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