diff --git a/mongoz/__init__.py b/mongoz/__init__.py index 2440c93..9eb9f66 100644 --- a/mongoz/__init__.py +++ b/mongoz/__init__.py @@ -25,6 +25,7 @@ Time, ) from .core.db.querysets.base import QuerySet +from .core.db.querysets.expressions import Expression, SortExpression from .core.db.querysets.operators import Q __all__ = [ @@ -42,6 +43,7 @@ "Embed", "EmailField", "EmbeddedDocument", + "Expression", "Index", "IndexType", "Integer", @@ -52,6 +54,7 @@ "Q", "QuerySet", "Registry", + "SortExpression", "String", "Time", "UUID", diff --git a/mongoz/core/db/documents/base.py b/mongoz/core/db/documents/base.py index e24187a..5dd08f5 100644 --- a/mongoz/core/db/documents/base.py +++ b/mongoz/core/db/documents/base.py @@ -1,4 +1,4 @@ -from typing import ClassVar, Dict, List, Type, TypeVar, Union +from typing import ClassVar, Dict, List, Mapping, Type, TypeVar, Union import bson import pydantic @@ -8,6 +8,7 @@ from mongoz.core.db.documents._internal import DescriptiveMeta from mongoz.core.db.documents.metaclasses import BaseModelMeta, MetaInfo +from mongoz.core.db.fields.base import BaseField from mongoz.core.db.fields.core import ObjectId from mongoz.core.db.querysets.base import QuerySet from mongoz.core.db.querysets.expressions import Expression @@ -65,4 +66,5 @@ def __str__(self) -> str: class MongozBaseModel(BaseMongoz): + __mongoz_fields__: ClassVar[Mapping[str, Type["BaseField"]]] id: Union[ObjectId, None] = pydantic.Field(alias="_id") diff --git a/mongoz/core/db/documents/document.py b/mongoz/core/db/documents/document.py index 13eb9bd..ce2d8b7 100644 --- a/mongoz/core/db/documents/document.py +++ b/mongoz/core/db/documents/document.py @@ -1,11 +1,16 @@ -from typing import ClassVar, List, Sequence, Type, Union +from typing import ClassVar, List, Mapping, Sequence, Type, TypeVar, Union +import bson +from bson.errors import InvalidId from pydantic import BaseModel from mongoz.core.db.documents.base import MongozBaseModel from mongoz.core.db.documents.metaclasses import EmbeddedModelMetaClass +from mongoz.core.db.fields.base import BaseField from mongoz.exceptions import InvalidKeyError +T = TypeVar("T", bound="Document") + class Document(MongozBaseModel): """ @@ -33,7 +38,7 @@ async def create_many( if not all(isinstance(model, cls) for model in models): raise TypeError(f"All models must be of type {cls.__name__}") - data = {model.model_dump(exclude={"id"}) for model in models} + data = (model.model_dump(exclude={"id"}) for model in models) results = await cls.meta.collection._collection.insert_many(data) for model, inserted_id in zip(models, results.inserted_ids, strict=True): model.id = inserted_id @@ -97,6 +102,16 @@ async def save(self: Type["Document"]) -> Type["Document"]: setattr(self, k, v) return self + @classmethod + async def get_document_by_id(cls: Type[T], id: Union[str, bson.ObjectId]) -> T: + if isinstance(id, str): + try: + id = bson.ObjectId(id) + except InvalidId as e: + raise InvalidKeyError(f'"{id}" is not a valid ObjectId') from e + + return await cls.query({"_id": id}).get() + def __repr__(self) -> str: return str(self) @@ -109,4 +124,5 @@ class EmbeddedDocument(BaseModel, metaclass=EmbeddedModelMetaClass): Graphical representation of an Embedded document. """ + __mongoz_fields__: ClassVar[Mapping[str, Type["BaseField"]]] __embedded__: ClassVar[bool] = True diff --git a/mongoz/core/db/documents/metaclasses.py b/mongoz/core/db/documents/metaclasses.py index 862f6a2..c0aa8af 100644 --- a/mongoz/core/db/documents/metaclasses.py +++ b/mongoz/core/db/documents/metaclasses.py @@ -236,9 +236,6 @@ def __search_for_fields(base: Type, attrs: Any) -> None: if "id" in new_class.model_fields: new_class.model_fields["id"].default = None - # # Update the model_fields are updated to the latest - # new_class.model_fields = model_fields - # Abstract classes do not allow multiple managers. This make sure it is enforced. if meta.abstract: managers = [k for k, v in attrs.items() if isinstance(v, Manager)] @@ -313,6 +310,7 @@ def __search_for_fields(base: Type, attrs: Any) -> None: new_field = MongozField(pydantic_field=field, model_class=field.annotation) mongoz_fields[field_name] = new_field + new_class.Meta = meta new_class.__mongoz_fields__ = mongoz_fields return new_class @@ -329,14 +327,14 @@ class EmbeddedModelMetaClass(ModelMetaclass): def __new__(cls, name: str, bases: Tuple[Type, ...], attrs: Any) -> Any: attrs, model_fields = extract_field_annotations_and_defaults(attrs) cls.__mongoz_fields__ = model_fields - cls = super().__new__(cls, name, bases, attrs) + new_class = super().__new__(cls, name, bases, attrs) mongoz_fields: Dict[str, MongozField] = {} - for field_name, field in cls.model_fields.items(): + for field_name, field in new_class.model_fields.items(): if not field.alias: field.alias = field_name - new_field = MongozField(pydantic_field=field, model_class=cls) + new_field = MongozField(pydantic_field=field, model_class=new_class) mongoz_fields[field_name] = new_field - cls.__mongoz_fields__ = mongoz_fields - return cls + new_class.__mongoz_fields__ = mongoz_fields + return new_class diff --git a/mongoz/core/db/documents/model_proxy.py b/mongoz/core/db/documents/model_proxy.py deleted file mode 100644 index 8b3ae3a..0000000 --- a/mongoz/core/db/documents/model_proxy.py +++ /dev/null @@ -1,70 +0,0 @@ -from typing import TYPE_CHECKING, Any, Dict, Tuple, Type, Union, cast - -from pydantic import ConfigDict - -if TYPE_CHECKING: - from mongoz.core.db.documents import Document - from mongoz.core.db.documents.metaclasses import MetaInfo - - -class ProxyModel: - """ - When a model needs to be mirrored without affecting the - original, this instance is triggered instead. - """ - - def __init__( - self, - name: str, - module: str, - *, - bases: Union[Tuple[Type["Document"]], None] = None, - definitions: Union[Dict[Any, Any], None] = None, - metadata: Union[Type["MetaInfo"], None] = None, - qualname: Union[str, None] = None, - config: Union[ConfigDict, None] = None, - proxy: bool = True, - pydantic_extra: Union[Any, None] = None, - ) -> None: - self.__name__: str = name - self.__module__: str = module - self.__bases__: Union[Tuple[Type["Document"]], None] = bases - self.__definitions__: Union[Dict[Any, Any], None] = definitions - self.__metadata__: Union[Type["MetaInfo"], None] = metadata - self.__qualname__: Union[str, None] = qualname - self.__config__: Union[ConfigDict, None] = config - self.__proxy__: bool = proxy - self.__pydantic_extra__ = pydantic_extra - self.__model__ = None - - def build(self) -> "ProxyModel": - """ - Generates the model proxy for the __model__ definition. - """ - from mongoz.core.utils.models import create_mongoz_model - - model: Type["Document"] = create_mongoz_model( - __name__=self.__name__, - __module__=self.__module__, - __bases__=self.__bases__, - __definitions__=self.__definitions__, - __metadata__=self.__metadata__, - __qualname__=self.__qualname__, - __config__=self.__config__, - __proxy__=self.__proxy__, - __pydantic_extra__=self.__pydantic_extra__, - ) - self.__model__ = model # type: ignore - return self - - @property - def model(self) -> Type["Document"]: - return cast("Type[Document]", self.__model__) - - @model.setter - def model(self, value: Type["Document"]) -> None: - self.__model__ = value # type: ignore - - def __repr__(self) -> str: - name = f"Proxy{self.__name__}" - return f"<{name}: [{self.__definitions__}]" diff --git a/mongoz/core/db/documents/row.py b/mongoz/core/db/documents/row.py deleted file mode 100644 index 92bc601..0000000 --- a/mongoz/core/db/documents/row.py +++ /dev/null @@ -1,30 +0,0 @@ -from typing import TYPE_CHECKING, Any, Optional, Type - -from mongoz.core.db.documents.base import MongozBaseModel - -if TYPE_CHECKING: # pragma: no cover - from mongoz.core.db.documents import Document - - -class ModelRow(MongozBaseModel): - """ - Builds a row for a specific model - """ - - @classmethod - def from_row(cls, **kwargs: Any) -> Optional[Type["Document"]]: - """ - Class method to convert a SQLAlchemy Row result into a EdgyModel row type. - - Looping through select_related fields if the query comes from a select_related operation. - Validates if exists the select_related and related_field inside the models. - - When select_related and related_field exist for the same field being validated, the related - field is ignored as it won't override the value already collected from the select_related. - - If there is no select_related, then goes through the related field where it **should** - only return the instance of the the ForeignKey with the ID, making it lazy loaded. - - :return: Document class. - """ - ... diff --git a/mongoz/core/db/fields/core.py b/mongoz/core/db/fields/core.py index a9eb9e8..1a066b1 100644 --- a/mongoz/core/db/fields/core.py +++ b/mongoz/core/db/fields/core.py @@ -37,7 +37,7 @@ mongoz_setattr = object.__setattr__ if TYPE_CHECKING: - from mongoz.core.db.documents.document import Document + from mongoz.core.db.documents.document import EmbeddedDocument CLASS_DEFAULTS = ["cls", "__class__", "kwargs"] @@ -401,14 +401,14 @@ class Array(FieldFactory, list): def __new__( # type: ignore cls, - document: type, + type_of: type, **kwargs: Any, ) -> BaseField: kwargs = { **kwargs, **{k: v for k, v in locals().items() if k not in CLASS_DEFAULTS}, } - kwargs["list_type"] = document + kwargs["list_type"] = type_of return super().__new__(cls, **kwargs) @@ -432,7 +432,7 @@ class Embed(FieldFactory): def __new__( # type: ignore cls, - document: Type["Document"], + document: Type["EmbeddedDocument"], **kwargs: Any, ) -> BaseField: kwargs = { @@ -444,10 +444,10 @@ def __new__( # type: ignore @classmethod def validate_field(cls, **kwargs: Any) -> None: - from mongoz.core.db.documents.document import Document, EmbeddedDocument + from mongoz.core.db.documents.document import EmbeddedDocument document = kwargs.get("document") - if not issubclass(document, (Document, EmbeddedDocument)): + if not issubclass(document, EmbeddedDocument): raise FieldDefinitionError( "'document' must be of type mongoz.Document or mongoz.EmbeddedDocument" ) diff --git a/mongoz/core/db/querysets/__init__.py b/mongoz/core/db/querysets/__init__.py index e69de29..ae515bf 100644 --- a/mongoz/core/db/querysets/__init__.py +++ b/mongoz/core/db/querysets/__init__.py @@ -0,0 +1,5 @@ +from .base import QuerySet +from .expressions import Expression, SortExpression +from .operators import Q + +__all__ = ["Expression", "Q", "QuerySet", "SortExpression"] diff --git a/mongoz/core/db/querysets/base.py b/mongoz/core/db/querysets/base.py index 75ba64e..baee3d1 100644 --- a/mongoz/core/db/querysets/base.py +++ b/mongoz/core/db/querysets/base.py @@ -1,9 +1,11 @@ from typing import TYPE_CHECKING, Any, AsyncGenerator, Dict, Generic, List, Type, TypeVar, Union +import pydantic + from mongoz.core.db.datastructures import Order -from mongoz.core.db.fields.base import BaseField +from mongoz.core.db.fields.base import MongozField from mongoz.core.db.querysets.expressions import Expression, SortExpression -from mongoz.exceptions import MultipleObjectsReturned, ObjectNotFound +from mongoz.exceptions import DocumentNotFound, MultipleDumentsReturned from mongoz.protocols.queryset import QuerySetProtocol if TYPE_CHECKING: @@ -80,22 +82,20 @@ async def last(self) -> Union[T, None]: """ Returns the last document of a matching criteria. """ - objects = await self.all() if not objects: return None return objects[-1] async def get(self) -> T: - """Gets the document of a matching criteria.""" objects = await self.limit(2).all() if len(objects) == 0: - raise ObjectNotFound() + raise DocumentNotFound() elif len(objects) == 2: - raise MultipleObjectsReturned() + raise MultipleDumentsReturned() return objects[0] - async def get_or_create(self, defaults: Union[Dict[str, Any], None]) -> T: + async def get_or_create(self, defaults: Union[Dict[str, Any], None] = None) -> T: if not defaults: defaults = {} @@ -135,7 +135,7 @@ def sort(self, key: Any, direction: Union[Order, None] = None) -> "QuerySet[T]": for key_dir in key: sort_expression = SortExpression(*key_dir) self._sort.append(sort_expression) - elif isinstance(key, (str, BaseField)): + elif isinstance(key, (str, MongozField)): sort_expression = SortExpression(key, direction) self._sort.append(sort_expression) else: @@ -143,8 +143,6 @@ def sort(self, key: Any, direction: Union[Order, None] = None) -> "QuerySet[T]": return self def query(self, *args: Union[bool, Dict, Expression]) -> "QuerySet[T]": - """Filter query criteria.""" - for arg in args: assert isinstance(arg, (dict, Expression)), "Invalid argument to Query" if isinstance(arg, dict): @@ -152,5 +150,29 @@ def query(self, *args: Union[bool, Dict, Expression]) -> "QuerySet[T]": self._filter.extend(query_expressions) else: self._filter.append(arg) - return self + + async def update(self, **kwargs: Any) -> List[T]: + field_definitions = { + name: (annotations, ...) + for name, annotations in self.model_class.__annotations__.items() + if name in kwargs + } + + if field_definitions: + pydantic_model: Type[pydantic.BaseModel] = pydantic.create_model( + __model_name=self.model_class.__name__, + __config__=self.model_class.model_config, + **field_definitions, + ) + model = pydantic_model.model_validate(kwargs) + values = model.model_dump() + + filter_query = Expression.compile_many(self._filter) + await self._collection.update_many(filter_query, {"$set": values}) + + _filter = [expression for expression in self._filter if expression.key not in values] + _filter.extend([Expression(key, "$eq", value) for key, value in values.items()]) + + self._filter = _filter + return await self.all() diff --git a/mongoz/core/db/querysets/operators.py b/mongoz/core/db/querysets/operators.py index f6c02ad..d51f1eb 100644 --- a/mongoz/core/db/querysets/operators.py +++ b/mongoz/core/db/querysets/operators.py @@ -37,13 +37,13 @@ def or_(cls, *args: Union[bool, Expression]) -> Expression: @classmethod def contains(cls, key: Any, value: Any) -> Expression: - if key.annotation is str: + if key.pydantic_field.annotation is str: return Expression(key=key, operator="$regex", value=value) return Expression(key=key, operator="$eq", value=value) @classmethod def pattern(cls, key: Any, value: Union[str, re.Pattern]) -> Expression: - if key.annotation is str: + if key.pydantic_field.annotation is str: expression = value.pattern if isinstance(value, re.Pattern) else value return Expression(key=key, operator="$regex", value=expression) name = key if isinstance(key, str) else key._name diff --git a/mongoz/exceptions.py b/mongoz/exceptions.py index b295d92..dc8b39a 100644 --- a/mongoz/exceptions.py +++ b/mongoz/exceptions.py @@ -19,11 +19,11 @@ def __str__(self) -> str: return "".join(self.args).strip() -class ObjectNotFound(MongozException): +class DocumentNotFound(MongozException): ... -class MultipleObjectsReturned(MongozException): +class MultipleDumentsReturned(MongozException): ... diff --git a/tests/models/test_all.py b/tests/models/test_all.py new file mode 100644 index 0000000..806b58e --- /dev/null +++ b/tests/models/test_all.py @@ -0,0 +1,53 @@ +from typing import AsyncGenerator, List, Optional + +import pydantic +import pytest +from tests.conftest import client + +import mongoz +from mongoz import Document, Index, IndexType, ObjectId, Order + +pytestmark = pytest.mark.anyio +pydantic_version = pydantic.__version__[:3] + +indexes = [ + Index("name", unique=True), + Index(keys=[("year", Order.DESCENDING), ("genre", IndexType.HASHED)]), +] + + +class Movie(Document): + name: str = mongoz.String() + year: int = mongoz.Integer() + tags: Optional[List[str]] = mongoz.Array(str, null=True) + uuid: Optional[ObjectId] = mongoz.ObjectId(null=True) + + class Meta: + registry = client + database = "test_db" + indexes = indexes + + +@pytest.fixture(scope="function", autouse=True) +async def prepare_database() -> AsyncGenerator: + await Movie.drop_indexes(force=True) + await Movie.query().delete() + await Movie.create_indexes() + yield + await Movie.drop_indexes(force=True) + await Movie.query().delete() + await Movie.create_indexes() + + +async def test_model_all() -> None: + movies = await Movie.query().all() + assert len(movies) == 0 + + await Movie(name="Forrest Gump", year=2003).create() + + movies = await Movie.query().all() + assert len(movies) == 1 + + cursor = Movie.query() + async for movie in cursor: + assert movie.name == "Forrest Gump" diff --git a/tests/models/test_bulk_update.py b/tests/models/test_bulk_update.py new file mode 100644 index 0000000..2d961f3 --- /dev/null +++ b/tests/models/test_bulk_update.py @@ -0,0 +1,68 @@ +from typing import AsyncGenerator, List, Optional + +import pydantic +import pytest +from tests.conftest import client + +import mongoz +from mongoz import Document, Index, IndexType, ObjectId, Order + +pytestmark = pytest.mark.anyio +pydantic_version = pydantic.__version__[:3] + +indexes = [ + Index("name", unique=True), + Index(keys=[("year", Order.DESCENDING), ("genre", IndexType.HASHED)]), +] + + +class Movie(Document): + name: str = mongoz.String() + year: int = mongoz.Integer() + tags: Optional[List[str]] = mongoz.Array(str, null=True) + uuid: Optional[ObjectId] = mongoz.ObjectId(null=True) + + class Meta: + registry = client + database = "test_db" + indexes = indexes + + +@pytest.fixture(scope="function", autouse=True) +async def prepare_database() -> AsyncGenerator: + await Movie.drop_indexes(force=True) + await Movie.query().delete() + await Movie.create_indexes() + yield + await Movie.drop_indexes(force=True) + await Movie.query().delete() + await Movie.create_indexes() + + +async def test_model_bulk_update() -> None: + await Movie(name="Boyhood", year=2004).create() + await Movie(name="Boyhood-2", year=2011).create() + + movies = await Movie.query({Movie.year: 2004}).update(year=2010) + assert movies[0].year == 2010 + + movies = await Movie.query().all() + assert movies[0].year == 2010 + + movies = await Movie.query({Movie.name: "Boyhood-2"}).update(year=2010) + assert len(movies) == 1 + assert movies[0].year == 2010 + + movies = await Movie.query({Movie.year: 2010}).all() + assert len(movies) == 2 + + movies = await Movie.query({Movie.name: "Boyhood-2"}).update(year=2014, name="Boyhood 2") + assert movies[0].year == 2014 + assert movies[0].name == "Boyhood 2" + + with pytest.raises(pydantic.ValidationError): + movies = await Movie.query({Movie.name: "Boyhood 2"}).update(year="test") + + movies = await Movie.query({Movie.name: "Boyhood 2"}).update(test=2021) + assert movies[0].year == 2014 + assert movies[0].name == "Boyhood 2" diff --git a/tests/models/test_count.py b/tests/models/test_count.py new file mode 100644 index 0000000..e88b53c --- /dev/null +++ b/tests/models/test_count.py @@ -0,0 +1,49 @@ +from typing import AsyncGenerator, List, Optional + +import pydantic +import pytest +from tests.conftest import client + +import mongoz +from mongoz import Document, Index, IndexType, ObjectId, Order + +pytestmark = pytest.mark.anyio +pydantic_version = pydantic.__version__[:3] + +indexes = [ + Index("name", unique=True), + Index(keys=[("year", Order.DESCENDING), ("genre", IndexType.HASHED)]), +] + + +class Movie(Document): + name: str = mongoz.String() + year: int = mongoz.Integer() + tags: Optional[List[str]] = mongoz.Array(str, null=True) + uuid: Optional[ObjectId] = mongoz.ObjectId(null=True) + + class Meta: + registry = client + database = "test_db" + indexes = indexes + + +@pytest.fixture(scope="function", autouse=True) +async def prepare_database() -> AsyncGenerator: + await Movie.drop_indexes(force=True) + await Movie.query().delete() + await Movie.create_indexes() + yield + await Movie.drop_indexes(force=True) + await Movie.query().delete() + await Movie.create_indexes() + + +async def test_model_count() -> None: + count = await Movie.query().count() + assert count == 0 + + await Movie(name="Batman", year=2013).create() + + count = await Movie.query({Movie.year: 2013}).count() + assert count == 1 diff --git a/tests/models/test_create.py b/tests/models/test_create.py new file mode 100644 index 0000000..56f6618 --- /dev/null +++ b/tests/models/test_create.py @@ -0,0 +1,62 @@ +from typing import AsyncGenerator, List, Optional + +import bson +import pydantic +import pytest +from pydantic import ValidationError +from pymongo import errors +from tests.conftest import client + +import mongoz +from mongoz import Document, Index, IndexType, ObjectId, Order + +pytestmark = pytest.mark.anyio +pydantic_version = pydantic.__version__[:3] + +indexes = [ + Index("name", unique=True), + Index(keys=[("year", Order.DESCENDING), ("genre", IndexType.HASHED)]), +] + + +class Movie(Document): + name: str = mongoz.String() + year: int = mongoz.Integer() + tags: Optional[List[str]] = mongoz.Array(str, null=True) + uuid: Optional[ObjectId] = mongoz.ObjectId(null=True) + + class Meta: + registry = client + database = "test_db" + indexes = indexes + + +@pytest.fixture(scope="function", autouse=True) +async def prepare_database() -> AsyncGenerator: + await Movie.drop_indexes(force=True) + await Movie.query().delete() + await Movie.create_indexes() + yield + await Movie.drop_indexes(force=True) + await Movie.query().delete() + await Movie.create_indexes() + + +async def test_model_create() -> None: + movie = await Movie(name="Barbie", year=2023).create() + assert movie.name == "Barbie" + assert movie.year == 2023 + assert isinstance(movie.id, bson.ObjectId) + + with pytest.raises(ValidationError) as exc: + await Movie(name="Justice League", year=2017, uuid="1").create() + + error = exc.value.errors()[0] + + assert error["type"] == "value_error" + assert error["loc"] == ("uuid",) + assert error["msg"] == "Value error, Expected ObjectId, got: " + assert error["input"] == "1" + + with pytest.raises(errors.DuplicateKeyError): + await Movie(name="Barbie", year=2023).create() diff --git a/tests/models/test_create_many.py b/tests/models/test_create_many.py new file mode 100644 index 0000000..d518db0 --- /dev/null +++ b/tests/models/test_create_many.py @@ -0,0 +1,67 @@ +import random +from typing import AsyncGenerator, List, Optional + +import bson +import pydantic +import pytest +from tests.conftest import client + +import mongoz +from mongoz import Document, Index, IndexType, ObjectId, Order + +pytestmark = pytest.mark.anyio +pydantic_version = pydantic.__version__[:3] + +indexes = [ + Index("name", unique=True), + Index(keys=[("year", Order.DESCENDING), ("genre", IndexType.HASHED)]), +] + + +class Movie(Document): + name: str = mongoz.String() + year: int = mongoz.Integer() + tags: Optional[List[str]] = mongoz.Array(str, null=True) + uuid: Optional[ObjectId] = mongoz.ObjectId(null=True) + + class Meta: + registry = client + database = "test_db" + indexes = indexes + + +@pytest.fixture(scope="function", autouse=True) +async def prepare_database() -> AsyncGenerator: + await Movie.drop_indexes(force=True) + await Movie.query().delete() + await Movie.create_indexes() + yield + await Movie.drop_indexes(force=True) + await Movie.query().delete() + await Movie.create_indexes() + + +async def test_model_create_many() -> None: + movies = [] + movie_names = ("The Dark Knight", "The Dark Knight Rises", "The Godfather") + for movie_name in movie_names: + movies.append(Movie(name=movie_name, year=random.randint(1970, 2020))) + + movies_db = await Movie.create_many(movies) + for movie, movie_db in zip(movies, movies_db): + assert movie.name == movie_db.name + assert movie.year == movie_db.year + assert isinstance(movie.id, bson.ObjectId) + + class Book(Document): + name: str = mongoz.String() + year: int = mongoz.Integer() + + class Meta: + indexes = indexes + database = "test_db" + + with pytest.raises(TypeError): + book = Book(name="The Book", year=1972) + movie = Movie(name="Inception", year=2010) + await Movie.create_many([book, movie]) # type: ignore diff --git a/tests/models/test_custom_query_operators.py b/tests/models/test_custom_query_operators.py new file mode 100644 index 0000000..be66e5b --- /dev/null +++ b/tests/models/test_custom_query_operators.py @@ -0,0 +1,105 @@ +import re +from typing import AsyncGenerator, List, Optional + +import pydantic +import pytest +from tests.conftest import client + +import mongoz +from mongoz import Document, Index, IndexType, ObjectId, Order, Q +from mongoz.exceptions import FieldDefinitionError + +pytestmark = pytest.mark.anyio +pydantic_version = pydantic.__version__[:3] + +indexes = [ + Index("name", unique=True), + Index(keys=[("year", Order.DESCENDING), ("genre", IndexType.HASHED)]), +] + + +class Movie(Document): + name: str = mongoz.String() + year: int = mongoz.Integer() + tags: Optional[List[str]] = mongoz.Array(str, null=True) + uuid: Optional[ObjectId] = mongoz.ObjectId(null=True) + + class Meta: + registry = client + database = "test_db" + indexes = indexes + + +@pytest.fixture(scope="function", autouse=True) +async def prepare_database() -> AsyncGenerator: + await Movie.drop_indexes(force=True) + await Movie.query().delete() + await Movie.create_indexes() + yield + await Movie.drop_indexes(force=True) + await Movie.query().delete() + await Movie.create_indexes() + + +async def test_custom_query_operators() -> None: + await Movie(name="The Two Towers", year=2002, tags=["Fantasy", "Adventure"]).create() + await Movie(name="Downfall", year=2004, tags=["Drama"]).create() + await Movie(name="Boyhood", year=2010, tags=["Coming Of Age", "Drama"]).create() + + movies = await Movie.query(Q.in_(Movie.year, [2000, 2001, 2002])).all() + + assert len(movies) == 1 + assert movies[0].name == "The Two Towers" + + movies = ( + await Movie.query(Movie.year > 2000) + .query(Movie.year <= 2010) + .query(Q.not_in(Movie.year, [2001, 2002])) + .all() + ) + + assert len(movies) == 2 + assert movies[0].name == "Boyhood" + assert movies[1].name == "Downfall" + + movies = await Movie.query(Q.or_(Movie.name == "The Two Towers", Movie.year > 2005)).all() + assert movies[0].name == "The Two Towers" + assert movies[1].name == "Boyhood" + + movie = await Movie.query(Q.and_(Movie.name == "The Two Towers", Movie.year > 2000)).get() + assert movie.name == "The Two Towers" + + movie = ( + await Movie.query(Q.and_(Movie.name == "The Two Towers", Movie.year > 2000)) + .query(Movie.name == "The Two Towers") + .get() + ) + assert movie.name == "The Two Towers" + + count = ( + await Movie.query(Q.and_(Movie.name == "The Two Towers", Movie.year > 2000)) + .query(Movie.name == "Boyhood") + .count() + ) + assert count == 0 + + movies = await Movie.query(Q.contains(Movie.tags, "Drama")).all() + assert movies[0].name == "Downfall" + assert movies[1].name == "Boyhood" + + movies = await Movie.query(Q.contains(Movie.name, "Two")).all() + assert movies[0].name == "The Two Towers" + + movies = await Movie.query(Q.pattern(Movie.name, r"\w+ Two \w+")).all() + assert len(movies) == 1 + assert movies[0].name == "The Two Towers" + + movies = await Movie.query(Q.pattern(Movie.name, re.compile(r"\w+ Two \w+"))).all() + assert len(movies) == 1 + assert movies[0].name == "The Two Towers" + + movies = await Movie.query(Q.pattern(Movie.name, r"\w+ The \w+")).all() + assert len(movies) == 0 + + with pytest.raises(FieldDefinitionError): + await Movie.query(Q.pattern(Movie.year, r"\w+ The \w+")).all() diff --git a/tests/models/test_delete.py b/tests/models/test_delete.py new file mode 100644 index 0000000..403af21 --- /dev/null +++ b/tests/models/test_delete.py @@ -0,0 +1,54 @@ +from typing import AsyncGenerator, List, Optional + +import pydantic +import pytest +from tests.conftest import client + +import mongoz +from mongoz import Document, Index, IndexType, ObjectId, Order + +pytestmark = pytest.mark.anyio +pydantic_version = pydantic.__version__[:3] + +indexes = [ + Index("name", unique=True), + Index(keys=[("year", Order.DESCENDING), ("genre", IndexType.HASHED)]), +] + + +class Movie(Document): + name: str = mongoz.String() + year: int = mongoz.Integer() + tags: Optional[List[str]] = mongoz.Array(str, null=True) + uuid: Optional[ObjectId] = mongoz.ObjectId(null=True) + + class Meta: + registry = client + database = "test_db" + indexes = indexes + + +@pytest.fixture(scope="function", autouse=True) +async def prepare_database() -> AsyncGenerator: + await Movie.drop_indexes(force=True) + await Movie.query().delete() + await Movie.create_indexes() + yield + await Movie.drop_indexes(force=True) + await Movie.query().delete() + await Movie.create_indexes() + + +async def test_model_delete() -> None: + await Movie(name="Barbie", year=2023).create() + await Movie(name="Batman", year=2022).create() + + count = await Movie.query({Movie.name: "Batman"}).delete() + assert count == 1 + + count = await Movie.query().delete() + assert count == 1 + + movie = await Movie(name="Batman", year=2013).create() + count = await movie.delete() + assert count == 1 diff --git a/tests/models/test_first.py b/tests/models/test_first.py new file mode 100644 index 0000000..b733c5c --- /dev/null +++ b/tests/models/test_first.py @@ -0,0 +1,50 @@ +from typing import AsyncGenerator, List, Optional + +import pydantic +import pytest +from tests.conftest import client + +import mongoz +from mongoz import Document, Index, IndexType, ObjectId, Order + +pytestmark = pytest.mark.anyio +pydantic_version = pydantic.__version__[:3] + +indexes = [ + Index("name", unique=True), + Index(keys=[("year", Order.DESCENDING), ("genre", IndexType.HASHED)]), +] + + +class Movie(Document): + name: str = mongoz.String() + year: int = mongoz.Integer() + tags: Optional[List[str]] = mongoz.Array(str, null=True) + uuid: Optional[ObjectId] = mongoz.ObjectId(null=True) + + class Meta: + registry = client + database = "test_db" + indexes = indexes + + +@pytest.fixture(scope="function", autouse=True) +async def prepare_database() -> AsyncGenerator: + await Movie.drop_indexes(force=True) + await Movie.query().delete() + await Movie.create_indexes() + yield + await Movie.drop_indexes(force=True) + await Movie.query().delete() + await Movie.create_indexes() + + +async def test_model_first() -> None: + await Movie(name="Batman", year=2022).create() + + movie = await Movie.query({Movie.name: "Justice League"}).first() + assert movie is None + + movie = await Movie.query({Movie.name: "Batman"}).first() + assert movie is not None + assert movie.name == "Batman" diff --git a/tests/models/test_get.py b/tests/models/test_get.py new file mode 100644 index 0000000..e0ff7e3 --- /dev/null +++ b/tests/models/test_get.py @@ -0,0 +1,64 @@ +from typing import AsyncGenerator, List, Optional + +import pydantic +import pytest +from tests.conftest import client + +import mongoz +from mongoz import Document, Index, IndexType, ObjectId, Order +from mongoz.exceptions import DocumentNotFound, MultipleDumentsReturned + +pytestmark = pytest.mark.anyio +pydantic_version = pydantic.__version__[:3] + +indexes = [ + Index("name", unique=True), + Index(keys=[("year", Order.DESCENDING), ("genre", IndexType.HASHED)]), +] + + +class Movie(Document): + name: str = mongoz.String() + year: int = mongoz.Integer() + tags: Optional[List[str]] = mongoz.Array(str, null=True) + uuid: Optional[ObjectId] = mongoz.ObjectId(null=True) + + class Meta: + registry = client + database = "test_db" + indexes = indexes + + +@pytest.fixture(scope="function", autouse=True) +async def prepare_database() -> AsyncGenerator: + await Movie.drop_indexes(force=True) + await Movie.query().delete() + await Movie.create_indexes() + yield + await Movie.drop_indexes(force=True) + await Movie.query().delete() + await Movie.create_indexes() + + +async def test_model_get() -> None: + await Movie(name="Barbie", year=2023).create() + + movie = await Movie.query().get() + assert movie.name == "Barbie" + + await Movie(name="Batman", year=2013).create() + + movie = await Movie.query({Movie.name: "Barbie"}).get() + assert movie.name == "Barbie" + + movie = await Movie.query({"_id": movie.id}).get() + assert movie.name == "Barbie" + + movie = await Movie.query({Movie.id: movie.id}).get() + assert movie.name == "Barbie" + + with pytest.raises(DocumentNotFound): + await Movie.query({Movie.name: "Interstellar"}).get() + + with pytest.raises(MultipleDumentsReturned): + await Movie.query().get() diff --git a/tests/models/test_get_by_id.py b/tests/models/test_get_by_id.py new file mode 100644 index 0000000..0136a0a --- /dev/null +++ b/tests/models/test_get_by_id.py @@ -0,0 +1,57 @@ +import secrets +from typing import AsyncGenerator, List, Optional + +import pydantic +import pytest +from tests.conftest import client + +import mongoz +from mongoz import Document, Index, IndexType, ObjectId, Order +from mongoz.exceptions import DocumentNotFound, InvalidKeyError + +pytestmark = pytest.mark.anyio +pydantic_version = pydantic.__version__[:3] + +indexes = [ + Index("name", unique=True), + Index(keys=[("year", Order.DESCENDING), ("genre", IndexType.HASHED)]), +] + + +class Movie(Document): + name: str = mongoz.String() + year: int = mongoz.Integer() + tags: Optional[List[str]] = mongoz.Array(str, null=True) + uuid: Optional[ObjectId] = mongoz.ObjectId(null=True) + + class Meta: + registry = client + database = "test_db" + indexes = indexes + + +@pytest.fixture(scope="function", autouse=True) +async def prepare_database() -> AsyncGenerator: + await Movie.drop_indexes(force=True) + await Movie.query().delete() + await Movie.create_indexes() + yield + await Movie.drop_indexes(force=True) + await Movie.query().delete() + await Movie.create_indexes() + + +async def test_model_get_document_by_id() -> None: + movie = await Movie(name="Barbie", year=2023).create() + + a_movie = await Movie.get_document_by_id(str(movie.id)) + assert movie.name == a_movie.name + + b_movie = await Movie.get_document_by_id(movie.id) + assert movie.name == b_movie.name + + with pytest.raises(InvalidKeyError): + await Movie.get_document_by_id("invalid_id") + + with pytest.raises(DocumentNotFound): + await Movie.get_document_by_id(secrets.token_hex(12)) diff --git a/tests/models/test_get_or_create.py b/tests/models/test_get_or_create.py new file mode 100644 index 0000000..3b4266e --- /dev/null +++ b/tests/models/test_get_or_create.py @@ -0,0 +1,61 @@ +from typing import AsyncGenerator, List, Optional + +import pydantic +import pytest +from pydantic import ValidationError +from tests.conftest import client + +import mongoz +from mongoz import Document, Index, IndexType, ObjectId, Order + +pytestmark = pytest.mark.anyio +pydantic_version = pydantic.__version__[:3] + +indexes = [ + Index("name", unique=True), + Index(keys=[("year", Order.DESCENDING), ("genre", IndexType.HASHED)]), +] + + +class Movie(Document): + name: str = mongoz.String() + year: int = mongoz.Integer() + tags: Optional[List[str]] = mongoz.Array(str, null=True) + uuid: Optional[ObjectId] = mongoz.ObjectId(null=True) + + class Meta: + registry = client + database = "test_db" + indexes = indexes + + +@pytest.fixture(scope="function", autouse=True) +async def prepare_database() -> AsyncGenerator: + await Movie.drop_indexes(force=True) + await Movie.query().delete() + await Movie.create_indexes() + yield + await Movie.drop_indexes(force=True) + await Movie.query().delete() + await Movie.create_indexes() + + +async def test_model_get_or_create() -> None: + movie = await Movie.query({Movie.name: "Barbie"}).get_or_create({Movie.year: 2023}) + assert movie.name == "Barbie" + assert movie.year == 2023 + + movie = await Movie.query({Movie.name: "Barbie", Movie.year: 2023}).get_or_create() + assert movie.name == "Barbie" + assert movie.year == 2023 + + movie = await Movie.query({Movie.name: "Venom"}).get_or_create({Movie.year: 2021}) + assert movie.name == "Venom" + assert movie.year == 2021 + + movie = await Movie.query({Movie.name: "Eternals", Movie.year: 2021}).get_or_create() + assert movie.name == "Eternals" + assert movie.year == 2021 + + with pytest.raises(ValidationError): + await movie.query({Movie.name: "Venom 2"}).get_or_create({Movie.year: "year 2021"}) diff --git a/tests/models/test_last.py b/tests/models/test_last.py new file mode 100644 index 0000000..d409e34 --- /dev/null +++ b/tests/models/test_last.py @@ -0,0 +1,59 @@ +from typing import AsyncGenerator, List, Optional + +import pydantic +import pytest +from tests.conftest import client + +import mongoz +from mongoz import Document, Index, IndexType, ObjectId, Order + +pytestmark = pytest.mark.anyio +pydantic_version = pydantic.__version__[:3] + +indexes = [ + Index("name", unique=True), + Index(keys=[("year", Order.DESCENDING), ("genre", IndexType.HASHED)]), +] + + +class Movie(Document): + name: str = mongoz.String() + year: int = mongoz.Integer() + tags: Optional[List[str]] = mongoz.Array(str, null=True) + uuid: Optional[ObjectId] = mongoz.ObjectId(null=True) + + class Meta: + registry = client + database = "test_db" + indexes = indexes + + +@pytest.fixture(scope="function", autouse=True) +async def prepare_database() -> AsyncGenerator: + await Movie.drop_indexes(force=True) + await Movie.query().delete() + await Movie.create_indexes() + yield + await Movie.drop_indexes(force=True) + await Movie.query().delete() + await Movie.create_indexes() + + +async def test_model_last() -> None: + await Movie(name="Batman", year=2022).create() + + movie = await Movie.query({Movie.name: "Justice League"}).last() + assert movie is None + + movie = await Movie.query({Movie.name: "Batman"}).last() + assert movie is not None + assert movie.name == "Batman" + + +async def test_model_last_two() -> None: + await Movie(name="Batman", year=2022).create() + await Movie(name="Barbie", year=2023).create() + + movie = await Movie.query().last() + assert movie is not None + assert movie.name == "Barbie" diff --git a/tests/models/test_limit.py b/tests/models/test_limit.py new file mode 100644 index 0000000..0b7970f --- /dev/null +++ b/tests/models/test_limit.py @@ -0,0 +1,52 @@ +from typing import AsyncGenerator, List, Optional + +import pydantic +import pytest +from tests.conftest import client + +import mongoz +from mongoz import Document, Index, IndexType, ObjectId, Order + +pytestmark = pytest.mark.anyio +pydantic_version = pydantic.__version__[:3] + +indexes = [ + Index("name", unique=True), + Index(keys=[("year", Order.DESCENDING), ("genre", IndexType.HASHED)]), +] + + +class Movie(Document): + name: str = mongoz.String() + year: int = mongoz.Integer() + tags: Optional[List[str]] = mongoz.Array(str, null=True) + uuid: Optional[ObjectId] = mongoz.ObjectId(null=True) + + class Meta: + registry = client + database = "test_db" + indexes = indexes + + +@pytest.fixture(scope="function", autouse=True) +async def prepare_database() -> AsyncGenerator: + await Movie.drop_indexes(force=True) + await Movie.query().delete() + await Movie.create_indexes() + yield + await Movie.drop_indexes(force=True) + await Movie.query().delete() + await Movie.create_indexes() + + +async def test_model_limit() -> None: + await Movie(name="Oppenheimer", year=2023).create() + await Movie(name="Batman", year=2022).create() + + movies = await Movie.query().sort(Movie.name, Order.ASCENDING).limit(1).all() + assert len(movies) == 1 + assert movies[0].name == "Batman" + + movies = await Movie.query().sort(Movie.name, Order.ASCENDING).skip(1).limit(1).all() + assert len(movies) == 1 + assert movies[0].name == "Oppenheimer" diff --git a/tests/models/test_models.py b/tests/models/test_models.py new file mode 100644 index 0000000..74ceb1c --- /dev/null +++ b/tests/models/test_models.py @@ -0,0 +1,83 @@ +from typing import AsyncGenerator, List, Optional + +import pytest +from tests.conftest import client + +import mongoz +from mongoz import Document, Index, IndexType, ObjectId, Order + +pytestmark = pytest.mark.anyio + +indexes = [ + Index("name", unique=True), + Index(keys=[("year", Order.DESCENDING), ("genre", IndexType.HASHED)]), +] + + +class Movie(Document): + name: str = mongoz.String() + year: int = mongoz.Integer() + tags: Optional[List[str]] = mongoz.Array(str, null=True) + uuid: Optional[ObjectId] = mongoz.ObjectId(null=True) + + class Meta: + registry = client + database = "test_db" + indexes = indexes + + +@pytest.fixture(scope="function", autouse=True) +async def prepare_database() -> AsyncGenerator: + await Movie.drop_indexes(force=True) + await Movie.query().delete() + await Movie.create_indexes() + yield + await Movie.drop_indexes(force=True) + await Movie.query().delete() + await Movie.create_indexes() + + +def test_model_class() -> None: + class Product(Document): + sku: str = mongoz.String() + + class Meta: + registry = client + database = "test_db" + + with pytest.raises(ValueError): + Product(sku=12345) + + movie = Movie(name="Batman", year=2009) + movie_dump = movie.model_dump() + + assert movie_dump == dict(movie) + assert movie_dump["name"] == "Batman" + assert movie_dump["year"] == 2009 + assert movie_dump["id"] is None + assert movie_dump["tags"] is None + + assert Movie.meta.database.name == "test_db" + assert Movie.meta.collection.name == "movies" + + assert Movie.model_json_schema() == { + "additionalProperties": True, + "properties": { + "_id": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "default": None, + "title": " Id", + }, + "name": {"title": "Name", "type": "string"}, + "year": {"title": "Year", "type": "integer"}, + "tags": { + "anyOf": [{"items": {"type": "string"}, "type": "array"}, {"type": "null"}], + "default": None, + "title": "Tags", + }, + "uuid": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Uuid"}, + }, + "required": ["name", "year"], + "title": "Movie", + "type": "object", + } diff --git a/tests/models/test_query_builder.py b/tests/models/test_query_builder.py new file mode 100644 index 0000000..0ceeb2c --- /dev/null +++ b/tests/models/test_query_builder.py @@ -0,0 +1,83 @@ +from typing import AsyncGenerator, List, Optional + +import pydantic +import pytest +from tests.conftest import client + +import mongoz +from mongoz import Document, Index, IndexType, ObjectId, Order + +pytestmark = pytest.mark.anyio +pydantic_version = pydantic.__version__[:3] + +indexes = [ + Index("name", unique=True), + Index(keys=[("year", Order.DESCENDING), ("genre", IndexType.HASHED)]), +] + + +class Movie(Document): + name: str = mongoz.String() + year: int = mongoz.Integer() + tags: Optional[List[str]] = mongoz.Array(str, null=True) + uuid: Optional[ObjectId] = mongoz.ObjectId(null=True) + + class Meta: + registry = client + database = "test_db" + indexes = indexes + + +@pytest.fixture(scope="function", autouse=True) +async def prepare_database() -> AsyncGenerator: + await Movie.drop_indexes(force=True) + await Movie.query().delete() + await Movie.create_indexes() + yield + await Movie.drop_indexes(force=True) + await Movie.query().delete() + await Movie.create_indexes() + + +async def test_model_query_builder() -> None: + await Movie(name="Downfall", year=2004).create() + await Movie(name="The Two Towers", year=2002).create() + await Movie(name="Casablanca", year=1942).create() + await Movie(name="Gone with the wind", year=1939).create() + + movie = await Movie.query(Movie.year != 1920).first() + assert movie is not None + + movie = await Movie.query(Movie.year == 1939).get() + assert movie.name == "Gone with the wind" + + movie = await Movie.query(Movie.year < 1940).get() + assert movie.name == "Gone with the wind" + assert movie.year == 1939 + + movie = await Movie.query(Movie.year <= 1939).get() + assert movie.name == "Gone with the wind" + assert movie.year == 1939 + + movie = await Movie.query(Movie.year > 2000).first() + assert movie is not None + assert movie.name == "Downfall" + assert movie.year == 2004 + + movie = await Movie.query(Movie.year >= 1940).first() + assert movie is not None + assert movie.name == "Downfall" + assert movie.year == 2004 + + movie = await Movie.query(Movie.name == "Casablanca").query(Movie.year == 1942).get() + assert movie.name == "Casablanca" + assert movie.year == 1942 + + movie = await Movie.query(Movie.year > 2000).query(Movie.year < 2003).get() + assert movie.name == "The Two Towers" + assert movie.year == 2002 + + assert ( + await Movie.query(Movie.name == "Casablanca").query(Movie.year == 1942).get() + == await Movie.query(Movie.name == "Casablanca", Movie.year == 1942).get() + ) diff --git a/tests/models/test_raw_queries.py b/tests/models/test_raw_queries.py new file mode 100644 index 0000000..39bfa76 --- /dev/null +++ b/tests/models/test_raw_queries.py @@ -0,0 +1,89 @@ +from typing import AsyncGenerator, List, Optional + +import pydantic +import pytest +from tests.conftest import client + +import mongoz +from mongoz import Document, Index, IndexType, ObjectId, Order + +pytestmark = pytest.mark.anyio +pydantic_version = pydantic.__version__[:3] + +indexes = [ + Index("name", unique=True), + Index(keys=[("year", Order.DESCENDING), ("genre", IndexType.HASHED)]), +] + + +class Movie(Document): + name: str = mongoz.String() + year: int = mongoz.Integer() + tags: Optional[List[str]] = mongoz.Array(str, null=True) + uuid: Optional[ObjectId] = mongoz.ObjectId(null=True) + + class Meta: + registry = client + database = "test_db" + indexes = indexes + + +@pytest.fixture(scope="function", autouse=True) +async def prepare_database() -> AsyncGenerator: + await Movie.drop_indexes(force=True) + await Movie.query().delete() + await Movie.create_indexes() + yield + await Movie.drop_indexes(force=True) + await Movie.query().delete() + await Movie.create_indexes() + + +async def test_raw_queries() -> None: + await Movie(name="Gone with the wind", year=1939).create() + await Movie(name="Casablanca", year=1942).create() + await Movie(name="The Two Towers", year=2002).create() + await Movie(name="Downfall", year=2004).create() + await Movie(name="Boyhood", year=2010).create() + + movie = await Movie.query({"name": "Casablanca"}).get() + + assert movie.name == "Casablanca" + assert movie.year == 1942 + + movie = await Movie.query({"year": {"$lt": 1940}}).get() + + assert movie.name == "Gone with the wind" + assert movie.year == 1939 + + movie = await Movie.query({"year": {"$lt": 2003, "$gt": 2000}}).get() + + assert movie.name == "The Two Towers" + assert movie.year == 2002 + + movie = await Movie.query({"year": {"$gt": 2000}}).query({"year": {"$lt": 2003}}).get() + + assert movie.name == "The Two Towers" + assert movie.year == 2002 + + movie = await Movie.query({"year": 1942}).query({"name": {"$regex": "Casa"}}).get() + + assert movie.name == "Casablanca" + assert movie.year == 1942 + + movie = await Movie.query({"name": "Casablanca"}).query({"year": {"$lt": 1950}}).get() + + assert movie.name == "Casablanca" + assert movie.year == 1942 + + movie = await Movie.query({"$and": [{"name": "Casablanca", "year": 1942}]}).get() + + assert movie.name == "Casablanca" + assert movie.year == 1942 + + movies = await Movie.query( + {"$or": [{"name": "The Two Towers"}, {"year": {"$gt": 2005}}]} + ).all() + + assert movies[0].name == "The Two Towers" + assert movies[1].name == "Boyhood" diff --git a/tests/models/test_skip.py b/tests/models/test_skip.py new file mode 100644 index 0000000..c491b91 --- /dev/null +++ b/tests/models/test_skip.py @@ -0,0 +1,51 @@ +from typing import AsyncGenerator, List, Optional + +import pydantic +import pytest +from tests.conftest import client + +import mongoz +from mongoz import Document, Index, IndexType, ObjectId, Order + +pytestmark = pytest.mark.anyio +pydantic_version = pydantic.__version__[:3] + +indexes = [ + Index("name", unique=True), + Index(keys=[("year", Order.DESCENDING), ("genre", IndexType.HASHED)]), +] + + +class Movie(Document): + name: str = mongoz.String() + year: int = mongoz.Integer() + tags: Optional[List[str]] = mongoz.Array(str, null=True) + uuid: Optional[ObjectId] = mongoz.ObjectId(null=True) + + class Meta: + registry = client + database = "test_db" + indexes = indexes + + +@pytest.fixture(scope="function", autouse=True) +async def prepare_database() -> AsyncGenerator: + await Movie.drop_indexes(force=True) + await Movie.query().delete() + await Movie.create_indexes() + yield + await Movie.drop_indexes(force=True) + await Movie.query().delete() + await Movie.create_indexes() + + +async def test_model_skip() -> None: + await Movie(name="Oppenheimer", year=2003).create() + await Movie(name="Batman", year=2022).create() + + movies = await Movie.query().sort(Movie.name, Order.ASCENDING).skip(1).all() + assert len(movies) == 1 + assert movies[0].name == "Oppenheimer" + + movie = await Movie.query().sort(Movie.name, Order.ASCENDING).skip(1).get() + assert movie.name == "Oppenheimer" diff --git a/tests/models/test_sort.py b/tests/models/test_sort.py new file mode 100644 index 0000000..9d62abe --- /dev/null +++ b/tests/models/test_sort.py @@ -0,0 +1,86 @@ +from typing import AsyncGenerator, List, Optional + +import pydantic +import pytest +from tests.conftest import client + +import mongoz +from mongoz import Document, Index, IndexType, ObjectId, Order, Q + +pytestmark = pytest.mark.anyio +pydantic_version = pydantic.__version__[:3] + +indexes = [ + Index("name", unique=True), + Index(keys=[("year", Order.DESCENDING), ("genre", IndexType.HASHED)]), +] + + +class Movie(Document): + name: str = mongoz.String() + year: int = mongoz.Integer() + tags: Optional[List[str]] = mongoz.Array(str, null=True) + uuid: Optional[ObjectId] = mongoz.ObjectId(null=True) + + class Meta: + registry = client + database = "test_db" + indexes = indexes + + +@pytest.fixture(scope="function", autouse=True) +async def prepare_database() -> AsyncGenerator: + await Movie.drop_indexes(force=True) + await Movie.query().delete() + await Movie.create_indexes() + yield + await Movie.drop_indexes(force=True) + await Movie.query().delete() + await Movie.create_indexes() + + +async def test_model_sort() -> None: + await Movie(name="Oppenheimer", year=2023).create() + await Movie(name="Batman", year=2022).create() + + movies = await Movie.query().sort("name", Order.ASCENDING).all() + + assert movies[0].name == "Batman" + assert movies[1].name == "Oppenheimer" + + movies = await Movie.query().sort(Movie.name, Order.ASCENDING).all() + + assert movies[0].name == "Batman" + assert movies[1].name == "Oppenheimer" + + movies = await Movie.query().sort([(Movie.name, Order.DESCENDING)]).all() + + assert movies[0].name == "Oppenheimer" + assert movies[1].name == "Batman" + + movies = ( + await Movie.query() + .sort([(Movie.name, Order.DESCENDING), (Movie.year, Order.DESCENDING)]) + .all() + ) + + assert movies[0].name == "Oppenheimer" + assert movies[1].name == "Batman" + + movies = ( + await Movie.query() + .sort(Movie.name, Order.DESCENDING) + .sort(Movie.year, Order.ASCENDING) + .all() + ) + + assert movies[0].name == "Oppenheimer" + assert movies[1].name == "Batman" + + movies = await Movie.query().sort(Q.asc(Movie.name)).all() + assert movies[0].name == "Batman" + assert movies[1].name == "Oppenheimer" + + movies = await Movie.query().sort(Q.desc(Movie.name)).sort(Q.asc(Movie.year)).all() + assert movies[0].name == "Oppenheimer" + assert movies[1].name == "Batman" diff --git a/tests/models/test_update_and_save.py b/tests/models/test_update_and_save.py new file mode 100644 index 0000000..46b6cbb --- /dev/null +++ b/tests/models/test_update_and_save.py @@ -0,0 +1,56 @@ +from typing import AsyncGenerator, List, Optional + +import pydantic +import pytest +from tests.conftest import client + +import mongoz +from mongoz import Document, Index, IndexType, ObjectId, Order + +pytestmark = pytest.mark.anyio +pydantic_version = pydantic.__version__[:3] + +indexes = [ + Index("name", unique=True), + Index(keys=[("year", Order.DESCENDING), ("genre", IndexType.HASHED)]), +] + + +class Movie(Document): + name: str = mongoz.String() + year: int = mongoz.Integer() + tags: Optional[List[str]] = mongoz.Array(str, null=True) + uuid: Optional[ObjectId] = mongoz.ObjectId(null=True) + + class Meta: + registry = client + database = "test_db" + indexes = indexes + + +@pytest.fixture(scope="function", autouse=True) +async def prepare_database() -> AsyncGenerator: + await Movie.drop_indexes(force=True) + await Movie.query().delete() + await Movie.create_indexes() + yield + await Movie.drop_indexes(force=True) + await Movie.query().delete() + await Movie.create_indexes() + + +async def test_model_update_save() -> None: + await Movie(name="Downfall", year=2002).create() + + movie = await Movie.query().get() + movie.year = 2003 + await movie.save() + + movie = await Movie.query().get() + assert movie.year == 2003 + + movie.year += 1 + await movie.save() + + movie = await Movie.query().get() + assert movie.year == 2004