From 24316a745d3728ed3c7f51c77087f22993a741db Mon Sep 17 00:00:00 2001 From: Roman Right Date: Sat, 5 Nov 2022 16:22:32 +0100 Subject: [PATCH] Remove: sync version (#403) * Remove: sync version --- README.md | 4 +- beanie/__init__.py | 2 +- beanie/sync/__init__.py | 50 - beanie/sync/odm/__init__.py | 45 - beanie/sync/odm/actions.py | 202 ---- beanie/sync/odm/bulk.py | 62 - beanie/sync/odm/cache.py | 45 - beanie/sync/odm/documents.py | 1040 ----------------- beanie/sync/odm/fields.py | 74 -- beanie/sync/odm/interfaces/__init__.py | 0 beanie/sync/odm/interfaces/aggregate.py | 70 -- .../odm/interfaces/aggregation_methods.py | 190 --- beanie/sync/odm/interfaces/detector.py | 13 - beanie/sync/odm/interfaces/find.py | 354 ------ beanie/sync/odm/interfaces/getters.py | 28 - beanie/sync/odm/interfaces/run.py | 6 - beanie/sync/odm/interfaces/session.py | 19 - beanie/sync/odm/interfaces/update.py | 117 -- beanie/sync/odm/queries/__init__.py | 0 beanie/sync/odm/queries/aggregation.py | 110 -- beanie/sync/odm/queries/cursor.py | 86 -- beanie/sync/odm/queries/delete.py | 81 -- beanie/sync/odm/queries/find.py | 879 -------------- beanie/sync/odm/queries/update.py | 238 ---- beanie/sync/odm/settings/__init__.py | 0 beanie/sync/odm/settings/base.py | 24 - beanie/sync/odm/settings/document.py | 144 --- beanie/sync/odm/settings/timeseries.py | 37 - beanie/sync/odm/settings/union_doc.py | 21 - beanie/sync/odm/settings/view.py | 31 - beanie/sync/odm/union_doc.py | 45 - beanie/sync/odm/utils/__init__.py | 0 beanie/sync/odm/utils/dump.py | 17 - beanie/sync/odm/utils/encoder.py | 181 --- beanie/sync/odm/utils/find.py | 65 -- beanie/sync/odm/utils/general.py | 94 -- beanie/sync/odm/utils/parsing.py | 36 - beanie/sync/odm/utils/projection.py | 30 - beanie/sync/odm/utils/relations.py | 82 -- beanie/sync/odm/utils/self_validation.py | 14 - beanie/sync/odm/utils/state.py | 45 - beanie/sync/odm/views.py | 78 -- docs/changelog.md | 18 +- docs/index.md | 4 +- docs/sync_tutorial/actions.md | 99 -- docs/sync_tutorial/aggregate.md | 33 - docs/sync_tutorial/cache.md | 46 - docs/sync_tutorial/collection_setup.md | 135 --- docs/sync_tutorial/defining-a-document.md | 238 ---- docs/sync_tutorial/delete.md | 31 - docs/sync_tutorial/find.md | 182 --- docs/sync_tutorial/init.md | 40 - docs/sync_tutorial/insert.md | 61 - docs/sync_tutorial/multi-model.md | 80 -- docs/sync_tutorial/on_save_validation.md | 31 - docs/sync_tutorial/relations.md | 216 ---- docs/sync_tutorial/revision.md | 38 - docs/sync_tutorial/state_management.md | 91 -- docs/sync_tutorial/sync.md | 108 -- docs/sync_tutorial/update.md | 82 -- docs/sync_tutorial/views.md | 102 -- docs/{async_tutorial => tutorial}/actions.md | 0 .../{async_tutorial => tutorial}/aggregate.md | 0 docs/{async_tutorial => tutorial}/cache.md | 0 .../defining-a-document.md | 17 +- docs/{async_tutorial => tutorial}/delete.md | 0 docs/{async_tutorial => tutorial}/find.md | 0 .../indexes.md} | 59 +- .../inheritance.md | 2 +- docs/{async_tutorial => tutorial}/init.md | 0 docs/{async_tutorial => tutorial}/insert.md | 0 .../migrations.md | 0 .../multi-model.md | 0 .../on_save_validation.md | 0 .../{async_tutorial => tutorial}/relations.md | 0 docs/{async_tutorial => tutorial}/revision.md | 0 .../state_management.md | 0 docs/tutorial/time_series.md | 29 + docs/{async_tutorial => tutorial}/update.md | 0 docs/{async_tutorial => tutorial}/views.md | 0 pydoc-markdown.yml | 106 +- pyproject.toml | 4 +- tests/sync/__init__.py | 0 tests/sync/conftest.py | 253 ---- tests/sync/models.py | 476 -------- tests/sync/odm/__init__.py | 0 tests/sync/odm/documents/__init__.py | 0 tests/sync/odm/documents/test_aggregate.py | 72 -- tests/sync/odm/documents/test_bulk_write.py | 185 --- tests/sync/odm/documents/test_count.py | 15 - tests/sync/odm/documents/test_create.py | 53 - tests/sync/odm/documents/test_delete.py | 53 - tests/sync/odm/documents/test_distinct.py | 28 - tests/sync/odm/documents/test_exists.py | 15 - tests/sync/odm/documents/test_find.py | 185 --- tests/sync/odm/documents/test_init.py | 235 ---- tests/sync/odm/documents/test_inspect.py | 30 - tests/sync/odm/documents/test_multi_model.py | 67 -- .../odm/documents/test_pydantic_config.py | 10 - .../odm/documents/test_pydantic_extras.py | 34 - tests/sync/odm/documents/test_replace.py | 30 - tests/sync/odm/documents/test_revision.py | 58 - tests/sync/odm/documents/test_update.py | 223 ---- .../odm/documents/test_validation_on_save.py | 39 - tests/sync/odm/operators/__init__.py | 0 tests/sync/odm/operators/find/__init__.py | 0 tests/sync/odm/operators/find/test_array.py | 17 - tests/sync/odm/operators/find/test_bitwise.py | 27 - .../odm/operators/find/test_comparison.py | 93 -- tests/sync/odm/operators/find/test_element.py | 18 - .../odm/operators/find/test_evaluation.py | 73 -- .../odm/operators/find/test_geospatial.py | 115 -- tests/sync/odm/operators/find/test_logical.py | 31 - tests/sync/odm/operators/update/__init__.py | 0 tests/sync/odm/operators/update/test_array.py | 33 - .../sync/odm/operators/update/test_bitwise.py | 7 - .../sync/odm/operators/update/test_general.py | 57 - tests/sync/odm/query/__init__.py | 0 tests/sync/odm/query/test_aggregate.py | 98 -- .../sync/odm/query/test_aggregate_methods.py | 95 -- tests/sync/odm/query/test_delete.py | 116 -- tests/sync/odm/query/test_find.py | 358 ------ tests/sync/odm/query/test_update.py | 211 ---- tests/sync/odm/query/test_update_methods.py | 81 -- tests/sync/odm/test_actions.py | 129 -- tests/sync/odm/test_cache.py | 96 -- tests/sync/odm/test_cursor.py | 13 - tests/sync/odm/test_deprecated.py | 0 tests/sync/odm/test_encoder.py | 52 - tests/sync/odm/test_expression_fields.py | 80 -- tests/sync/odm/test_fields.py | 130 --- tests/sync/odm/test_id.py | 20 - tests/sync/odm/test_relations.py | 215 ---- tests/sync/odm/test_run.py | 100 -- tests/sync/odm/test_state_management.py | 234 ---- tests/sync/odm/test_timesries_collection.py | 37 - tests/sync/odm/test_views.py | 19 - tests/sync/odm/views.py | 15 - tests/test_beanie.py | 2 +- 139 files changed, 101 insertions(+), 11043 deletions(-) delete mode 100644 beanie/sync/__init__.py delete mode 100644 beanie/sync/odm/__init__.py delete mode 100644 beanie/sync/odm/actions.py delete mode 100644 beanie/sync/odm/bulk.py delete mode 100644 beanie/sync/odm/cache.py delete mode 100644 beanie/sync/odm/documents.py delete mode 100644 beanie/sync/odm/fields.py delete mode 100644 beanie/sync/odm/interfaces/__init__.py delete mode 100644 beanie/sync/odm/interfaces/aggregate.py delete mode 100644 beanie/sync/odm/interfaces/aggregation_methods.py delete mode 100644 beanie/sync/odm/interfaces/detector.py delete mode 100644 beanie/sync/odm/interfaces/find.py delete mode 100644 beanie/sync/odm/interfaces/getters.py delete mode 100644 beanie/sync/odm/interfaces/run.py delete mode 100644 beanie/sync/odm/interfaces/session.py delete mode 100644 beanie/sync/odm/interfaces/update.py delete mode 100644 beanie/sync/odm/queries/__init__.py delete mode 100644 beanie/sync/odm/queries/aggregation.py delete mode 100644 beanie/sync/odm/queries/cursor.py delete mode 100644 beanie/sync/odm/queries/delete.py delete mode 100644 beanie/sync/odm/queries/find.py delete mode 100644 beanie/sync/odm/queries/update.py delete mode 100644 beanie/sync/odm/settings/__init__.py delete mode 100644 beanie/sync/odm/settings/base.py delete mode 100644 beanie/sync/odm/settings/document.py delete mode 100644 beanie/sync/odm/settings/timeseries.py delete mode 100644 beanie/sync/odm/settings/union_doc.py delete mode 100644 beanie/sync/odm/settings/view.py delete mode 100644 beanie/sync/odm/union_doc.py delete mode 100644 beanie/sync/odm/utils/__init__.py delete mode 100644 beanie/sync/odm/utils/dump.py delete mode 100644 beanie/sync/odm/utils/encoder.py delete mode 100644 beanie/sync/odm/utils/find.py delete mode 100644 beanie/sync/odm/utils/general.py delete mode 100644 beanie/sync/odm/utils/parsing.py delete mode 100644 beanie/sync/odm/utils/projection.py delete mode 100644 beanie/sync/odm/utils/relations.py delete mode 100644 beanie/sync/odm/utils/self_validation.py delete mode 100644 beanie/sync/odm/utils/state.py delete mode 100644 beanie/sync/odm/views.py delete mode 100644 docs/sync_tutorial/actions.md delete mode 100644 docs/sync_tutorial/aggregate.md delete mode 100644 docs/sync_tutorial/cache.md delete mode 100644 docs/sync_tutorial/collection_setup.md delete mode 100644 docs/sync_tutorial/defining-a-document.md delete mode 100644 docs/sync_tutorial/delete.md delete mode 100644 docs/sync_tutorial/find.md delete mode 100644 docs/sync_tutorial/init.md delete mode 100644 docs/sync_tutorial/insert.md delete mode 100644 docs/sync_tutorial/multi-model.md delete mode 100644 docs/sync_tutorial/on_save_validation.md delete mode 100644 docs/sync_tutorial/relations.md delete mode 100644 docs/sync_tutorial/revision.md delete mode 100644 docs/sync_tutorial/state_management.md delete mode 100644 docs/sync_tutorial/sync.md delete mode 100644 docs/sync_tutorial/update.md delete mode 100644 docs/sync_tutorial/views.md rename docs/{async_tutorial => tutorial}/actions.md (100%) rename docs/{async_tutorial => tutorial}/aggregate.md (100%) rename docs/{async_tutorial => tutorial}/cache.md (100%) rename docs/{async_tutorial => tutorial}/defining-a-document.md (98%) rename docs/{async_tutorial => tutorial}/delete.md (100%) rename docs/{async_tutorial => tutorial}/find.md (100%) rename docs/{async_tutorial/collection_setup.md => tutorial/indexes.md} (57%) rename docs/{async_tutorial => tutorial}/inheritance.md (99%) rename docs/{async_tutorial => tutorial}/init.md (100%) rename docs/{async_tutorial => tutorial}/insert.md (100%) rename docs/{async_tutorial => tutorial}/migrations.md (100%) rename docs/{async_tutorial => tutorial}/multi-model.md (100%) rename docs/{async_tutorial => tutorial}/on_save_validation.md (100%) rename docs/{async_tutorial => tutorial}/relations.md (100%) rename docs/{async_tutorial => tutorial}/revision.md (100%) rename docs/{async_tutorial => tutorial}/state_management.md (100%) create mode 100644 docs/tutorial/time_series.md rename docs/{async_tutorial => tutorial}/update.md (100%) rename docs/{async_tutorial => tutorial}/views.md (100%) delete mode 100644 tests/sync/__init__.py delete mode 100644 tests/sync/conftest.py delete mode 100644 tests/sync/models.py delete mode 100644 tests/sync/odm/__init__.py delete mode 100644 tests/sync/odm/documents/__init__.py delete mode 100644 tests/sync/odm/documents/test_aggregate.py delete mode 100644 tests/sync/odm/documents/test_bulk_write.py delete mode 100644 tests/sync/odm/documents/test_count.py delete mode 100644 tests/sync/odm/documents/test_create.py delete mode 100644 tests/sync/odm/documents/test_delete.py delete mode 100644 tests/sync/odm/documents/test_distinct.py delete mode 100644 tests/sync/odm/documents/test_exists.py delete mode 100644 tests/sync/odm/documents/test_find.py delete mode 100644 tests/sync/odm/documents/test_init.py delete mode 100644 tests/sync/odm/documents/test_inspect.py delete mode 100644 tests/sync/odm/documents/test_multi_model.py delete mode 100644 tests/sync/odm/documents/test_pydantic_config.py delete mode 100644 tests/sync/odm/documents/test_pydantic_extras.py delete mode 100644 tests/sync/odm/documents/test_replace.py delete mode 100644 tests/sync/odm/documents/test_revision.py delete mode 100644 tests/sync/odm/documents/test_update.py delete mode 100644 tests/sync/odm/documents/test_validation_on_save.py delete mode 100644 tests/sync/odm/operators/__init__.py delete mode 100644 tests/sync/odm/operators/find/__init__.py delete mode 100644 tests/sync/odm/operators/find/test_array.py delete mode 100644 tests/sync/odm/operators/find/test_bitwise.py delete mode 100644 tests/sync/odm/operators/find/test_comparison.py delete mode 100644 tests/sync/odm/operators/find/test_element.py delete mode 100644 tests/sync/odm/operators/find/test_evaluation.py delete mode 100644 tests/sync/odm/operators/find/test_geospatial.py delete mode 100644 tests/sync/odm/operators/find/test_logical.py delete mode 100644 tests/sync/odm/operators/update/__init__.py delete mode 100644 tests/sync/odm/operators/update/test_array.py delete mode 100644 tests/sync/odm/operators/update/test_bitwise.py delete mode 100644 tests/sync/odm/operators/update/test_general.py delete mode 100644 tests/sync/odm/query/__init__.py delete mode 100644 tests/sync/odm/query/test_aggregate.py delete mode 100644 tests/sync/odm/query/test_aggregate_methods.py delete mode 100644 tests/sync/odm/query/test_delete.py delete mode 100644 tests/sync/odm/query/test_find.py delete mode 100644 tests/sync/odm/query/test_update.py delete mode 100644 tests/sync/odm/query/test_update_methods.py delete mode 100644 tests/sync/odm/test_actions.py delete mode 100644 tests/sync/odm/test_cache.py delete mode 100644 tests/sync/odm/test_cursor.py delete mode 100644 tests/sync/odm/test_deprecated.py delete mode 100644 tests/sync/odm/test_encoder.py delete mode 100644 tests/sync/odm/test_expression_fields.py delete mode 100644 tests/sync/odm/test_fields.py delete mode 100644 tests/sync/odm/test_id.py delete mode 100644 tests/sync/odm/test_relations.py delete mode 100644 tests/sync/odm/test_run.py delete mode 100644 tests/sync/odm/test_state_management.py delete mode 100644 tests/sync/odm/test_timesries_collection.py delete mode 100644 tests/sync/odm/test_views.py delete mode 100644 tests/sync/odm/views.py diff --git a/README.md b/README.md index f6bac665..6c193630 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ ## Overview -[Beanie](https://github.com/roman-right/beanie) - is a Python object-document mapper (ODM) for MongoDB that can be used, and in synchronous, and async contexts. Data models are based on [Pydantic](https://pydantic-docs.helpmanual.io/). +[Beanie](https://github.com/roman-right/beanie) - is an asynchronous Python object-document mapper (ODM) for MongoDB that can be used, and in synchronous, and async contexts. Data models are based on [Pydantic](https://pydantic-docs.helpmanual.io/). When using Beanie each database collection has a corresponding `Document` that is used to interact with that collection. In addition to retrieving data, @@ -17,6 +17,8 @@ the parts of your app that actually matter. Data and schema migrations are supported by Beanie out of the box. +There is a synchronous version of Beanie ODM - [Bunnet](https://github.com/roman-right/bunnet) + ## Installation ### PIP diff --git a/beanie/__init__.py b/beanie/__init__.py index d0cff34c..bbfb17b6 100644 --- a/beanie/__init__.py +++ b/beanie/__init__.py @@ -26,7 +26,7 @@ from beanie.odm.views import View from beanie.odm.union_doc import UnionDoc -__version__ = "1.14.0" +__version__ = "1.15.0" __all__ = [ # ODM "Document", diff --git a/beanie/sync/__init__.py b/beanie/sync/__init__.py deleted file mode 100644 index 57f4a435..00000000 --- a/beanie/sync/__init__.py +++ /dev/null @@ -1,50 +0,0 @@ -from beanie import ( - WriteRules, - DeleteRules, - Insert, - Replace, - SaveChanges, - ValidateOnSave, - Delete, - Before, - After, - Update, -) -from beanie.sync.odm import Link -from beanie.sync.odm.actions import ( - before_event, - after_event, -) -from beanie.sync.odm.bulk import BulkWriter -from beanie.sync.odm.documents import Document -from beanie.sync.odm.settings.timeseries import TimeSeriesConfig, Granularity -from beanie.sync.odm.union_doc import UnionDoc -from beanie.sync.odm.utils.general import init_beanie -from beanie.sync.odm.views import View - -__all__ = [ - # ODM - "Document", - "View", - "UnionDoc", - "init_beanie", - "TimeSeriesConfig", - "Granularity", - # Actions - "before_event", - "after_event", - "Insert", - "Replace", - "SaveChanges", - "ValidateOnSave", - "Delete", - "Before", - "After", - "Update", - # Bulk Write - "BulkWriter", - # Relations - "Link", - "WriteRules", - "DeleteRules", -] diff --git a/beanie/sync/odm/__init__.py b/beanie/sync/odm/__init__.py deleted file mode 100644 index dbe4603d..00000000 --- a/beanie/sync/odm/__init__.py +++ /dev/null @@ -1,45 +0,0 @@ -from beanie.sync.odm.actions import before_event, after_event -from beanie.odm.actions import ( - Insert, - Replace, - SaveChanges, - ValidateOnSave, - Before, - After, - Delete, - Update, -) -from beanie.sync.odm.bulk import BulkWriter -from beanie.sync.odm.fields import ( - Link, -) -from beanie.sync.odm.settings.timeseries import TimeSeriesConfig, Granularity -from beanie.sync.odm.utils.general import init_beanie -from beanie.sync.odm.documents import Document -from beanie.sync.odm.views import View -from beanie.sync.odm.union_doc import UnionDoc - -__all__ = [ - # ODM - "Document", - "View", - "UnionDoc", - "init_beanie", - "TimeSeriesConfig", - "Granularity", - # Actions - "before_event", - "after_event", - "Insert", - "Replace", - "SaveChanges", - "ValidateOnSave", - "Delete", - "Before", - "After", - "Update", - # Bulk Write - "BulkWriter", - # Relations - "Link", -] diff --git a/beanie/sync/odm/actions.py b/beanie/sync/odm/actions.py deleted file mode 100644 index a2d0d235..00000000 --- a/beanie/sync/odm/actions.py +++ /dev/null @@ -1,202 +0,0 @@ -from functools import wraps -from typing import ( - Callable, - List, - Union, - Dict, - TYPE_CHECKING, - Any, - Type, - Optional, - Tuple, -) - -from beanie.odm.actions import EventTypes, ActionDirections - -if TYPE_CHECKING: - from beanie.sync.odm.documents import Document - - -class ActionRegistry: - _actions: Dict[Type, Any] = {} - - # TODO the real type is - # Dict[str, Dict[EventTypes,Dict[ActionDirections: List[Callable]]]] - # But mypy says it has syntax error inside. Fix it. - - @classmethod - def clean_actions(cls, document_class: Type): - if not cls._actions.get(document_class) is None: - del cls._actions[document_class] - - @classmethod - def add_action( - cls, - document_class: Type, - event_types: List[EventTypes], - action_direction: ActionDirections, - funct: Callable, - ): - """ - Add action to the action registry - :param document_class: document class - :param event_types: List[EventTypes] - :param action_direction: ActionDirections - before or after - :param funct: Callable - function - """ - if cls._actions.get(document_class) is None: - cls._actions[document_class] = { - action_type: { - action_direction: [] - for action_direction in ActionDirections - } - for action_type in EventTypes - } - for event_type in event_types: - cls._actions[document_class][event_type][action_direction].append( - funct - ) - - @classmethod - def get_action_list( - cls, - document_class: Type, - event_type: EventTypes, - action_direction: ActionDirections, - ) -> List[Callable]: - """ - Get stored action list - :param document_class: Type - document class - :param event_type: EventTypes - type of needed event - :param action_direction: ActionDirections - before or after - :return: List[Callable] - list of stored methods - """ - if document_class not in cls._actions: - return [] - return cls._actions[document_class][event_type][action_direction] - - @classmethod - def run_actions( - cls, - instance: "Document", - event_type: EventTypes, - action_direction: ActionDirections, - exclude: List[Union[ActionDirections, str]], - ): - """ - Run actions - :param instance: Document - object of the Document subclass - :param event_type: EventTypes - event types - :param action_direction: ActionDirections - before or after - """ - if action_direction in exclude: - return - - document_class = instance.__class__ - actions_list = cls.get_action_list( - document_class, event_type, action_direction - ) - for action in actions_list: - if action.__name__ in exclude: - continue - - action(instance) - - -def register_action( - event_types: Tuple[Union[List[EventTypes], EventTypes]], - action_direction: ActionDirections, -): - """ - Decorator. Base registration method. - Used inside `before_event` and `after_event` - :param event_types: Union[List[EventTypes], EventTypes] - event types - :param action_direction: ActionDirections - before or after - :return: - """ - final_event_types = [] - for event_type in event_types: - if isinstance(event_type, list): - final_event_types.extend(event_type) - else: - final_event_types.append(event_type) - - def decorator(f): - f.has_action = True - f.event_types = final_event_types - f.action_direction = action_direction - return f - - return decorator - - -def before_event(*args: Union[List[EventTypes], EventTypes]): - """ - Decorator. It adds action, which should run before mentioned one - or many events happen - - :param args: Union[List[EventTypes], EventTypes] - event types - :return: None - """ - - return register_action( - action_direction=ActionDirections.BEFORE, - event_types=args, # type: ignore - ) - - -def after_event(*args: Union[List[EventTypes], EventTypes]): - """ - Decorator. It adds action, which should run after mentioned one - or many events happen - - :param args: Union[List[EventTypes], EventTypes] - event types - :return: None - """ - - return register_action( - action_direction=ActionDirections.AFTER, - event_types=args, # type: ignore - ) - - -def wrap_with_actions(event_type: EventTypes): - """ - Helper function to wrap Document methods with - before and after event listeners - :param event_type: EventTypes - event types - :return: None - """ - - def decorator(f: Callable): - @wraps(f) - def wrapper( - self, - *args, - skip_actions: Optional[List[Union[ActionDirections, str]]] = None, - **kwargs, - ): - if skip_actions is None: - skip_actions = [] - - ActionRegistry.run_actions( - self, - event_type=event_type, - action_direction=ActionDirections.BEFORE, - exclude=skip_actions, - ) - - result = f(self, *args, skip_actions=skip_actions, **kwargs) - - ActionRegistry.run_actions( - self, - event_type=event_type, - action_direction=ActionDirections.AFTER, - exclude=skip_actions, - ) - - return result - - return wrapper - - return decorator diff --git a/beanie/sync/odm/bulk.py b/beanie/sync/odm/bulk.py deleted file mode 100644 index 9b67444b..00000000 --- a/beanie/sync/odm/bulk.py +++ /dev/null @@ -1,62 +0,0 @@ -from typing import Dict, Any, List, Optional, Union, Type, Mapping - -from pydantic import BaseModel -from pymongo import ( - InsertOne, - DeleteOne, - DeleteMany, - ReplaceOne, - UpdateOne, - UpdateMany, -) - - -class Operation(BaseModel): - operation: Union[ - Type[InsertOne], - Type[DeleteOne], - Type[DeleteMany], - Type[ReplaceOne], - Type[UpdateOne], - Type[UpdateMany], - ] - first_query: Mapping[str, Any] - second_query: Optional[Dict[str, Any]] = None - object_class: Type - - class Config: - arbitrary_types_allowed = True - - -class BulkWriter: - def __init__(self): - self.operations: List[Operation] = [] - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc, tb): - self.commit() - - def commit(self): - obj_class = None - requests = [] - if self.operations: - for op in self.operations: - if obj_class is None: - obj_class = op.object_class - - if obj_class != op.object_class: - raise ValueError( - "All the operations should be for a single document model" - ) - if op.operation in [InsertOne, DeleteOne]: - query = op.operation(op.first_query) - else: - query = op.operation(op.first_query, op.second_query) - requests.append(query) - - obj_class.get_motor_collection().bulk_write(requests) # type: ignore - - def add_operation(self, operation: Operation): - self.operations.append(operation) diff --git a/beanie/sync/odm/cache.py b/beanie/sync/odm/cache.py deleted file mode 100644 index 57c15a4c..00000000 --- a/beanie/sync/odm/cache.py +++ /dev/null @@ -1,45 +0,0 @@ -import collections -import datetime -from datetime import timedelta -from typing import Any, Optional - -from pydantic import BaseModel, Field - - -class CachedItem(BaseModel): - timestamp: datetime.datetime = Field( - default_factory=datetime.datetime.utcnow - ) - value: Any - - -class LRUCache: - def __init__(self, capacity: int, expiration_time: timedelta): - self.capacity: int = capacity - self.expiration_time: timedelta = expiration_time - self.cache: collections.OrderedDict = collections.OrderedDict() - - def get(self, key) -> Optional[CachedItem]: - try: - item = self.cache.pop(key) - if ( - datetime.datetime.utcnow() - item.timestamp - > self.expiration_time - ): - return None - self.cache[key] = item - return item.value - except KeyError: - return None - - def set(self, key, value) -> None: - try: - self.cache.pop(key) - except KeyError: - if len(self.cache) >= self.capacity: - self.cache.popitem(last=False) - self.cache[key] = CachedItem(value=value) - - @staticmethod - def create_key(*args): - return str(args) # TODO think about this diff --git a/beanie/sync/odm/documents.py b/beanie/sync/odm/documents.py deleted file mode 100644 index 0fd375ba..00000000 --- a/beanie/sync/odm/documents.py +++ /dev/null @@ -1,1040 +0,0 @@ -import inspect -from typing import ClassVar, AbstractSet -from typing import ( - Dict, - Optional, - List, - Type, - Union, - Mapping, - TypeVar, - Any, - Set, -) -from typing import TYPE_CHECKING -from uuid import UUID, uuid4 - -from bson import ObjectId, DBRef -from pydantic import ( - ValidationError, - PrivateAttr, - Field, - parse_obj_as, -) -from pydantic.main import BaseModel -from pymongo import InsertOne -from pymongo.client_session import ClientSession -from pymongo.database import Database -from pymongo.results import ( - DeleteResult, - InsertManyResult, -) - -from beanie.odm.fields import ( - PydanticObjectId, - LinkInfo, - WriteRules, - LinkTypes, - ExpressionField, - DeleteRules, -) -from beanie.exceptions import ( - CollectionWasNotInitialized, - ReplaceError, - DocumentNotFound, - RevisionIdWasChanged, - DocumentWasNotSaved, - NotSupported, -) -from beanie.sync.odm.actions import ( - EventTypes, - wrap_with_actions, - ActionRegistry, - ActionDirections, -) -from beanie.sync.odm.bulk import BulkWriter, Operation -from beanie.sync.odm.cache import LRUCache -from beanie.sync.odm.fields import ( - Link, -) -from beanie.sync.odm.interfaces.aggregate import AggregateInterface -from beanie.sync.odm.interfaces.detector import ModelType -from beanie.sync.odm.interfaces.find import FindInterface -from beanie.sync.odm.interfaces.getters import OtherGettersInterface -from beanie.odm.models import ( - InspectionResult, - InspectionStatuses, - InspectionError, -) -from beanie.odm.operators.find.comparison import In -from beanie.odm.operators.update.general import ( - CurrentDate, - Inc, - Set as SetOperator, -) -from beanie.sync.odm.queries.update import UpdateMany - -from beanie.sync.odm.settings.document import DocumentSettings -from beanie.sync.odm.utils.dump import get_dict -from beanie.sync.odm.utils.relations import detect_link -from beanie.sync.odm.utils.self_validation import validate_self_before -from beanie.sync.odm.utils.state import ( - save_state_after, - swap_revision_after, - saved_state_needed, -) - -if TYPE_CHECKING: - from pydantic.typing import AbstractSetIntStr, MappingIntStrAny, DictStrAny - from beanie.sync.odm.queries.find import FindOne - -DocType = TypeVar("DocType", bound="Document") -DocumentProjectionType = TypeVar("DocumentProjectionType", bound=BaseModel) - - -class Document( - BaseModel, - FindInterface, - AggregateInterface, - OtherGettersInterface, -): - """ - Document Mapping class. - - Fields: - - - `id` - MongoDB document ObjectID "_id" field. - Mapped to the PydanticObjectId class - - Inherited from: - - - Pydantic BaseModel - - [UpdateMethods](https://roman-right.github.io/beanie/api/interfaces/#aggregatemethods) - """ - - id: Optional[PydanticObjectId] = None - - # State - revision_id: Optional[UUID] = Field(default=None, hidden=True) - _previous_revision_id: Optional[UUID] = PrivateAttr(default=None) - _saved_state: Optional[Dict[str, Any]] = PrivateAttr(default=None) - - # Relations - _link_fields: ClassVar[Optional[Dict[str, LinkInfo]]] = None - - # Cache - _cache: ClassVar[Optional[LRUCache]] = None - - # Settings - _document_settings: ClassVar[Optional[DocumentSettings]] = None - - # Other - _hidden_fields: ClassVar[Set[str]] = set() - - def _swap_revision(self): - if self.get_settings().use_revision: - self._previous_revision_id = self.revision_id - self.revision_id = uuid4() - - def __init__(self, *args, **kwargs): - super(Document, self).__init__(*args, **kwargs) - self.get_motor_collection() - self._save_state() - self._swap_revision() - - def _sync(self) -> None: - """ - Update local document from the database - :return: None - """ - if self.id is None: - raise ValueError("Document has no id") - new_instance: Optional[Document] = self.get(self.id).run() - if new_instance is None: - raise DocumentNotFound( - "Can not sync. The document is not in the database anymore." - ) - for key, value in dict(new_instance).items(): - setattr(self, key, value) - if self.use_state_management(): - self._save_state() - - @classmethod - def get( - cls: Type["DocType"], - document_id: PydanticObjectId, - session: Optional[ClientSession] = None, - ignore_cache: bool = False, - fetch_links: bool = False, - **pymongo_kwargs, - ) -> "FindOne[DocType]": - """ - Get document by id, returns None if document does not exist - - :param document_id: PydanticObjectId - document id - :param session: Optional[ClientSession] - pymongo session - :param ignore_cache: bool - ignore cache (if it is turned on) - :param **pymongo_kwargs: pymongo native parameters for find operation - :return: Union["Document", None] - """ - if not isinstance(document_id, cls.__fields__["id"].type_): - document_id = parse_obj_as(cls.__fields__["id"].type_, document_id) - return cls.find_one( - {"_id": document_id}, - session=session, - ignore_cache=ignore_cache, - fetch_links=fetch_links, - **pymongo_kwargs, - ) - - @wrap_with_actions(EventTypes.INSERT) - @save_state_after - @swap_revision_after - @validate_self_before - def insert( - self: DocType, - *, - link_rule: WriteRules = WriteRules.DO_NOTHING, - session: Optional[ClientSession] = None, - skip_actions: Optional[List[Union[ActionDirections, str]]] = None, - ) -> DocType: - """ - Insert the document (self) to the collection - :return: Document - """ - if link_rule == WriteRules.WRITE: - link_fields = self.get_link_fields() - if link_fields is not None: - for field_info in link_fields.values(): - value = getattr(self, field_info.field) - if field_info.link_type in [ - LinkTypes.DIRECT, - LinkTypes.OPTIONAL_DIRECT, - ]: - if isinstance(value, Document): - value.insert(link_rule=WriteRules.WRITE) - if field_info.link_type in [ - LinkTypes.LIST, - LinkTypes.OPTIONAL_LIST, - ]: - if isinstance(value, List): - for obj in value: - if isinstance(obj, Document): - obj.insert(link_rule=WriteRules.WRITE) - result = self.get_motor_collection().insert_one( - get_dict(self, to_db=True), session=session - ) - new_id = result.inserted_id - if not isinstance(new_id, self.__fields__["id"].type_): - new_id = parse_obj_as(self.__fields__["id"].type_, new_id) - self.id = new_id - return self - - def create( - self: DocType, - session: Optional[ClientSession] = None, - ) -> DocType: - """ - The same as self.insert() - :return: Document - """ - return self.insert(session=session) - - @classmethod - def insert_one( - cls: Type[DocType], - document: DocType, - session: Optional[ClientSession] = None, - bulk_writer: "BulkWriter" = None, - link_rule: WriteRules = WriteRules.DO_NOTHING, - ) -> Optional[DocType]: - """ - Insert one document to the collection - :param document: Document - document to insert - :param session: ClientSession - pymongo session - :param bulk_writer: "BulkWriter" - Beanie bulk writer - :param link_rule: InsertRules - hot to manage link fields - :return: DocType - """ - if not isinstance(document, cls): - raise TypeError( - "Inserting document must be of the original document class" - ) - if bulk_writer is None: - return document.insert(link_rule=link_rule, session=session) - else: - if link_rule == WriteRules.WRITE: - raise NotSupported( - "Cascade insert with bulk writing not supported" - ) - bulk_writer.add_operation( - Operation( - operation=InsertOne, - first_query=get_dict(document, to_db=True), - object_class=type(document), - ) - ) - return None - - @classmethod - def insert_many( - cls: Type[DocType], - documents: List[DocType], - session: Optional[ClientSession] = None, - link_rule: WriteRules = WriteRules.DO_NOTHING, - **pymongo_kwargs, - ) -> InsertManyResult: - - """ - Insert many documents to the collection - - :param documents: List["Document"] - documents to insert - :param session: ClientSession - pymongo session - :param link_rule: InsertRules - how to manage link fields - :return: InsertManyResult - """ - if link_rule == WriteRules.WRITE: - raise NotSupported( - "Cascade insert not supported for insert many method" - ) - documents_list = [ - get_dict(document, to_db=True) for document in documents - ] - return cls.get_motor_collection().insert_many( - documents_list, session=session, **pymongo_kwargs - ) - - @wrap_with_actions(EventTypes.REPLACE) - @save_state_after - @swap_revision_after - @validate_self_before - def replace( - self: DocType, - ignore_revision: bool = False, - session: Optional[ClientSession] = None, - bulk_writer: Optional[BulkWriter] = None, - link_rule: WriteRules = WriteRules.DO_NOTHING, - skip_actions: Optional[List[Union[ActionDirections, str]]] = None, - ) -> DocType: - """ - Fully update the document in the database - - :param session: Optional[ClientSession] - pymongo session. - :param ignore_revision: bool - do force replace. - Used when revision based protection is turned on. - :param bulk_writer: "BulkWriter" - Beanie bulk writer - :return: self - """ - if self.id is None: - raise ValueError("Document must have an id") - - if bulk_writer is not None and link_rule != WriteRules.DO_NOTHING: - raise NotSupported - - if link_rule == WriteRules.WRITE: - link_fields = self.get_link_fields() - if link_fields is not None: - for field_info in link_fields.values(): - value = getattr(self, field_info.field) - if field_info.link_type in [ - LinkTypes.DIRECT, - LinkTypes.OPTIONAL_DIRECT, - ]: - if isinstance(value, Document): - value.replace( - link_rule=link_rule, - bulk_writer=bulk_writer, - ignore_revision=ignore_revision, - session=session, - ) - if field_info.link_type in [ - LinkTypes.LIST, - LinkTypes.OPTIONAL_LIST, - ]: - if isinstance(value, List): - for obj in value: - if isinstance(obj, Document): - obj.replace( - link_rule=link_rule, - bulk_writer=bulk_writer, - ignore_revision=ignore_revision, - session=session, - ) - - use_revision_id = self.get_settings().use_revision - find_query: Dict[str, Any] = {"_id": self.id} - - if use_revision_id and not ignore_revision: - find_query["revision_id"] = self._previous_revision_id - try: - self.find_one(find_query).replace_one( - self, - session=session, - bulk_writer=bulk_writer, - ) - except DocumentNotFound: - if use_revision_id and not ignore_revision: - raise RevisionIdWasChanged - else: - raise DocumentNotFound - return self - - def save( - self: DocType, - session: Optional[ClientSession] = None, - link_rule: WriteRules = WriteRules.DO_NOTHING, - **kwargs, - ) -> DocType: - """ - Update an existing model in the database or insert it if it does not yet exist. - - :param session: Optional[ClientSession] - pymongo session. - :return: None - """ - if link_rule == WriteRules.WRITE: - link_fields = self.get_link_fields() - if link_fields is not None: - for field_info in link_fields.values(): - value = getattr(self, field_info.field) - if field_info.link_type in [ - LinkTypes.DIRECT, - LinkTypes.OPTIONAL_DIRECT, - ]: - if isinstance(value, Document): - value.save(link_rule=link_rule, session=session) - if field_info.link_type in [ - LinkTypes.LIST, - LinkTypes.OPTIONAL_LIST, - ]: - if isinstance(value, List): - for obj in value: - if isinstance(obj, Document): - obj.save( - link_rule=link_rule, session=session - ) - - try: - return self.replace(session=session, **kwargs) - except (ValueError, DocumentNotFound): - return self.insert(session=session, **kwargs) - - @saved_state_needed - @wrap_with_actions(EventTypes.SAVE_CHANGES) - @validate_self_before - def save_changes( - self, - ignore_revision: bool = False, - session: Optional[ClientSession] = None, - bulk_writer: Optional[BulkWriter] = None, - skip_actions: Optional[List[Union[ActionDirections, str]]] = None, - ) -> None: - """ - Save changes. - State management usage must be turned on - - :param ignore_revision: bool - ignore revision id, if revision is turned on - :param bulk_writer: "BulkWriter" - Beanie bulk writer - :return: None - """ - if not self.is_changed: - return None - changes = self.get_changes() - return self.set( - changes, # type: ignore #TODO fix typing - ignore_revision=ignore_revision, - session=session, - bulk_writer=bulk_writer, - skip_sync=True, - ) - - @classmethod - def replace_many( - cls: Type[DocType], - documents: List[DocType], - session: Optional[ClientSession] = None, - ) -> None: - """ - Replace list of documents - - :param documents: List["Document"] - :param session: Optional[ClientSession] - pymongo session. - :return: None - """ - ids_list = [document.id for document in documents] - if cls.find(In(cls.id, ids_list)).count() != len(ids_list): - raise ReplaceError( - "Some of the documents are not exist in the collection" - ) - cls.find(In(cls.id, ids_list), session=session).delete().run() - cls.insert_many(documents, session=session) - - @wrap_with_actions(EventTypes.UPDATE) - @save_state_after - def update( - self, - *args, - ignore_revision: bool = False, - session: Optional[ClientSession] = None, - bulk_writer: Optional[BulkWriter] = None, - skip_sync: bool = False, - skip_actions: Optional[List[Union[ActionDirections, str]]] = None, - **pymongo_kwargs, - ) -> None: - """ - Partially update the document in the database - - :param args: *Union[dict, Mapping] - the modifications to apply. - :param session: ClientSession - pymongo session. - :param ignore_revision: bool - force update. Will update even if revision id is not the same, as stored - :param bulk_writer: "BulkWriter" - Beanie bulk writer - :param **pymongo_kwargs: pymongo native parameters for update operation - :return: None - """ - use_revision_id = self.get_settings().use_revision - - find_query: Dict[str, Any] = {"_id": self.id} - - if use_revision_id and not ignore_revision: - find_query["revision_id"] = self._previous_revision_id - - result = ( - self.find_one(find_query) - .update( - *args, - session=session, - bulk_writer=bulk_writer, - **pymongo_kwargs, - ) - .run() - ) - - if ( - use_revision_id - and not ignore_revision - and result.matched_count == 0 - ): - raise RevisionIdWasChanged - if not skip_sync: - self._sync() - - @classmethod - def update_all( - cls, - *args: Union[dict, Mapping], - session: Optional[ClientSession] = None, - bulk_writer: Optional[BulkWriter] = None, - **pymongo_kwargs, - ) -> UpdateMany: - """ - Partially update all the documents - - :param args: *Union[dict, Mapping] - the modifications to apply. - :param session: ClientSession - pymongo session. - :param bulk_writer: "BulkWriter" - Beanie bulk writer - :param **pymongo_kwargs: pymongo native parameters for find operation - :return: UpdateMany query - """ - return cls.find_all().update_many( - *args, session=session, bulk_writer=bulk_writer, **pymongo_kwargs - ) - - def set( - self, - expression: Dict[Union[ExpressionField, str], Any], - session: Optional[ClientSession] = None, - bulk_writer: Optional[BulkWriter] = None, - skip_sync: bool = False, - **kwargs, - ): - """ - Set values - - Example: - - ```python - - class Sample(Document): - one: int - - Document.find(Sample.one == 1).set({Sample.one: 100}) - - ``` - - Uses [Set operator](https://roman-right.github.io/beanie/api/operators/update/#set) - - :param expression: Dict[Union[ExpressionField, str], Any] - keys and - values to set - :param session: Optional[ClientSession] - pymongo session - :param bulk_writer: Optional[BulkWriter] - bulk writer - :param skip_sync: bool - skip doc syncing. Available for the direct instances only - :return: self - """ - return self.update( - SetOperator(expression), - session=session, - bulk_writer=bulk_writer, - skip_sync=skip_sync, - **kwargs, - ) - - def current_date( - self, - expression: Dict[Union[ExpressionField, str], Any], - session: Optional[ClientSession] = None, - bulk_writer: Optional[BulkWriter] = None, - skip_sync: bool = False, - **kwargs, - ): - """ - Set current date - - Uses [CurrentDate operator](https://roman-right.github.io/beanie/api/operators/update/#currentdate) - - :param expression: Dict[Union[ExpressionField, str], Any] - :param session: Optional[ClientSession] - pymongo session - :param bulk_writer: Optional[BulkWriter] - bulk writer - :param skip_sync: bool - skip doc syncing. Available for the direct instances only - :return: self - """ - return self.update( - CurrentDate(expression), - session=session, - bulk_writer=bulk_writer, - skip_sync=skip_sync, - **kwargs, - ) - - def inc( - self, - expression: Dict[Union[ExpressionField, str], Any], - session: Optional[ClientSession] = None, - bulk_writer: Optional[BulkWriter] = None, - skip_sync: bool = False, - **kwargs, - ): - """ - Increment - - Example: - - ```python - - class Sample(Document): - one: int - - Document.find(Sample.one == 1).inc({Sample.one: 100}) - - ``` - - Uses [Inc operator](https://roman-right.github.io/beanie/api/operators/update/#inc) - - :param expression: Dict[Union[ExpressionField, str], Any] - :param session: Optional[ClientSession] - pymongo session - :param bulk_writer: Optional[BulkWriter] - bulk writer - :param skip_sync: bool - skip doc syncing. Available for the direct instances only - :return: self - """ - return self.update( - Inc(expression), - session=session, - bulk_writer=bulk_writer, - skip_sync=skip_sync, - **kwargs, - ) - - @wrap_with_actions(EventTypes.DELETE) - def delete( - self, - session: Optional[ClientSession] = None, - bulk_writer: Optional[BulkWriter] = None, - link_rule: DeleteRules = DeleteRules.DO_NOTHING, - skip_actions: Optional[List[Union[ActionDirections, str]]] = None, - **pymongo_kwargs, - ) -> Optional[DeleteResult]: - """ - Delete the document - - :param session: Optional[ClientSession] - pymongo session. - :param bulk_writer: "BulkWriter" - Beanie bulk writer - :param link_rule: DeleteRules - rules for link fields - :param **pymongo_kwargs: pymongo native parameters for delete operation - :return: Optional[DeleteResult] - pymongo DeleteResult instance. - """ - - if link_rule == DeleteRules.DELETE_LINKS: - link_fields = self.get_link_fields() - if link_fields is not None: - for field_info in link_fields.values(): - value = getattr(self, field_info.field) - if field_info.link_type in [ - LinkTypes.DIRECT, - LinkTypes.OPTIONAL_DIRECT, - ]: - if isinstance(value, Document): - value.delete( - link_rule=DeleteRules.DELETE_LINKS, - **pymongo_kwargs, - ) - if field_info.link_type in [ - LinkTypes.LIST, - LinkTypes.OPTIONAL_LIST, - ]: - if isinstance(value, List): - for obj in value: - if isinstance(obj, Document): - obj.delete( - link_rule=DeleteRules.DELETE_LINKS, - **pymongo_kwargs, - ) - - return ( - self.find_one({"_id": self.id}) - .delete(session=session, bulk_writer=bulk_writer, **pymongo_kwargs) - .run() - ) - - @classmethod - def delete_all( - cls, - session: Optional[ClientSession] = None, - bulk_writer: Optional[BulkWriter] = None, - **pymongo_kwargs, - ) -> Optional[DeleteResult]: - """ - Delete all the documents - - :param session: Optional[ClientSession] - pymongo session. - :param bulk_writer: "BulkWriter" - Beanie bulk writer - :param **pymongo_kwargs: pymongo native parameters for delete operation - :return: Optional[DeleteResult] - pymongo DeleteResult instance. - """ - return ( - cls.find_all() - .delete(session=session, bulk_writer=bulk_writer, **pymongo_kwargs) - .run() - ) - - # State management - - @classmethod - def use_state_management(cls) -> bool: - """ - Is state management turned on - :return: bool - """ - return cls.get_settings().use_state_management - - @classmethod - def state_management_replace_objects(cls) -> bool: - """ - Should objects be replaced when using state management - :return: bool - """ - return cls.get_settings().state_management_replace_objects - - def _save_state(self) -> None: - """ - Save current document state. Internal method - :return: None - """ - if self.use_state_management() and self.id is not None: - self._saved_state = get_dict(self) - - def get_saved_state(self) -> Optional[Dict[str, Any]]: - """ - Saved state getter. It is protected property. - :return: Optional[Dict[str, Any]] - saved state - """ - return self._saved_state - - @property # type: ignore - @saved_state_needed - def is_changed(self) -> bool: - if self._saved_state == get_dict(self, to_db=True): - return False - return True - - def _collect_updates( - self, old_dict: Dict[str, Any], new_dict: Dict[str, Any] - ) -> Dict[str, Any]: - """ - Compares old_dict with new_dict and returns field paths that have been updated - Args: - old_dict: dict1 - new_dict: dict2 - - Returns: dictionary with updates - - """ - updates = {} - - for field_name, field_value in new_dict.items(): - if field_value != old_dict.get(field_name): - if not self.state_management_replace_objects() and ( - isinstance(field_value, dict) - and isinstance(old_dict.get(field_name), dict) - ): - if old_dict.get(field_name) is None: - updates[field_name] = field_value - elif isinstance(field_value, dict) and isinstance( - old_dict.get(field_name), dict - ): - - field_data = self._collect_updates( - old_dict.get(field_name), # type: ignore - field_value, - ) - - for k, v in field_data.items(): - updates[f"{field_name}.{k}"] = v - else: - updates[field_name] = field_value - - return updates - - @saved_state_needed - def get_changes(self) -> Dict[str, Any]: - return self._collect_updates( - self._saved_state, get_dict(self, to_db=True) # type: ignore - ) - - @saved_state_needed - def rollback(self) -> None: - if self.is_changed: - for key, value in self._saved_state.items(): # type: ignore - if key == "_id": - setattr(self, "id", value) - else: - setattr(self, key, value) - - # Initialization - - @classmethod - def init_cache(cls) -> None: - """ - Init model's cache - :return: None - """ - if cls.get_settings().use_cache: - cls._cache = LRUCache( - capacity=cls.get_settings().cache_capacity, - expiration_time=cls.get_settings().cache_expiration_time, - ) - - @classmethod - def init_fields(cls) -> None: - """ - Init class fields - :return: None - """ - if cls._link_fields is None: - cls._link_fields = {} - for k, v in cls.__fields__.items(): - path = v.alias or v.name - setattr(cls, k, ExpressionField(path)) - - link_info = detect_link(v) - if link_info is not None: - cls._link_fields[v.name] = link_info - - cls._hidden_fields = cls.get_hidden_fields() - - @classmethod - def init_settings( - cls, database: Database, allow_index_dropping: bool - ) -> None: - """ - Init document settings (collection and models) - - :param database: Database - pymongo database - :param allow_index_dropping: bool - :return: None - """ - # TODO looks ugly a little. Too many parameters transfers. - cls._document_settings = DocumentSettings.init( - database=database, - document_model=cls, - allow_index_dropping=allow_index_dropping, - ) - - @classmethod - def init_actions(cls): - """ - Init event-based actions - """ - ActionRegistry.clean_actions(cls) - for attr in dir(cls): - f = getattr(cls, attr) - if inspect.isfunction(f): - if hasattr(f, "has_action"): - ActionRegistry.add_action( - document_class=cls, - event_types=f.event_types, # type: ignore - action_direction=f.action_direction, # type: ignore - funct=f, - ) - - @classmethod - def init_model( - cls, database: Database, allow_index_dropping: bool - ) -> None: - """ - Init wrapper - :param database: Database - :param allow_index_dropping: bool - :return: None - """ - cls.init_settings( - database=database, allow_index_dropping=allow_index_dropping - ) - cls.init_fields() - cls.init_cache() - cls.init_actions() - - # Other - - @classmethod - def get_settings(cls) -> DocumentSettings: - """ - Get document settings, which was created on - the initialization step - - :return: DocumentSettings class - """ - if cls._document_settings is None: - raise CollectionWasNotInitialized - return cls._document_settings - - @classmethod - def inspect_collection( - cls, session: Optional[ClientSession] = None - ) -> InspectionResult: - """ - Check, if documents, stored in the MongoDB collection - are compatible with the Document schema - - :return: InspectionResult - """ - inspection_result = InspectionResult() - for json_document in cls.get_motor_collection().find( - {}, session=session - ): - try: - cls.parse_obj(json_document) - except ValidationError as e: - if inspection_result.status == InspectionStatuses.OK: - inspection_result.status = InspectionStatuses.FAIL - inspection_result.errors.append( - InspectionError( - document_id=json_document["_id"], error=str(e) - ) - ) - return inspection_result - - @classmethod - def get_hidden_fields(cls): - return set( - attribute_name - for attribute_name, model_field in cls.__fields__.items() - if model_field.field_info.extra.get("hidden") is True - ) - - def dict( - self, - *, - include: Union["AbstractSetIntStr", "MappingIntStrAny"] = None, - exclude: Union["AbstractSetIntStr", "MappingIntStrAny"] = None, - by_alias: bool = False, - skip_defaults: bool = None, - exclude_hidden: bool = True, - exclude_unset: bool = False, - exclude_defaults: bool = False, - exclude_none: bool = False, - ) -> "DictStrAny": - """ - Overriding of the respective method from Pydantic - Hides fields, marked as "hidden - """ - if exclude_hidden: - if isinstance(exclude, AbstractSet): - exclude = {*self._hidden_fields, *exclude} - elif isinstance(exclude, Mapping): - exclude = dict( - {k: True for k in self._hidden_fields}, **exclude - ) # type: ignore - elif exclude is None: - exclude = self._hidden_fields - return super().dict( - include=include, - exclude=exclude, - by_alias=by_alias, - skip_defaults=skip_defaults, - exclude_unset=exclude_unset, - exclude_defaults=exclude_defaults, - exclude_none=exclude_none, - ) - - @wrap_with_actions(event_type=EventTypes.VALIDATE_ON_SAVE) - def validate_self(self, *args, **kwargs): - # TODO it can be sync, but needs some actions controller improvements - if self.get_settings().validate_on_save: - self.parse_obj(self) - - def to_ref(self): - if self.id is None: - raise DocumentWasNotSaved("Can not create dbref without id") - return DBRef(self.get_motor_collection().name, self.id) - - def fetch_link(self, field: Union[str, Any]): - ref_obj = getattr(self, field, None) - if isinstance(ref_obj, Link): - value = ref_obj.fetch() - setattr(self, field, value) - if isinstance(ref_obj, list) and ref_obj: - values = Link.fetch_list(ref_obj) - setattr(self, field, values) - - def fetch_all_links(self): - link_fields = self.get_link_fields() - if link_fields is not None: - for ref in link_fields.values(): - self.fetch_link(ref.field) - - @classmethod - def get_link_fields(cls) -> Optional[Dict[str, LinkInfo]]: - return cls._link_fields - - @classmethod - def get_model_type(cls) -> ModelType: - return ModelType.Document - - @classmethod - def distinct( - cls, - key: str, - filter: Optional[Mapping[str, Any]] = None, - session: Optional[ClientSession] = None, - **kwargs: Any, - ) -> list: - return cls.get_motor_collection().distinct( - key, filter, session, **kwargs - ) - - @classmethod - def link_from_id(cls, id: Any): - ref = DBRef(id=id, collection=cls.get_collection_name()) - return Link(ref, model_class=cls) - - class Config: - json_encoders = { - ObjectId: lambda v: str(v), - } - allow_population_by_field_name = True - fields = {"id": "_id"} - - @staticmethod - def schema_extra( - schema: Dict[str, Any], model: Type["Document"] - ) -> None: - for field_name in model._hidden_fields: - schema.get("properties", {}).pop(field_name, None) diff --git a/beanie/sync/odm/fields.py b/beanie/sync/odm/fields.py deleted file mode 100644 index 4ab870b0..00000000 --- a/beanie/sync/odm/fields.py +++ /dev/null @@ -1,74 +0,0 @@ -from typing import Generic, TypeVar, Union, Type, List - -from bson import DBRef -from pydantic import BaseModel, parse_obj_as -from pydantic.fields import ModelField -from pydantic.json import ENCODERS_BY_TYPE - -from beanie.odm.operators.find.comparison import ( - In, -) - -T = TypeVar("T") - - -class Link(Generic[T]): - def __init__(self, ref: DBRef, model_class: Type[T]): - self.ref = ref - self.model_class = model_class - - def fetch(self) -> Union[T, "Link"]: - result = self.model_class.get(self.ref.id).run() # type: ignore - return result or self - - @classmethod - def fetch_one(cls, link: "Link"): - return link.fetch() - - @classmethod - def fetch_list(cls, links: List["Link"]): - ids = [] - model_class = None - for link in links: - if model_class is None: - model_class = link.model_class - else: - if model_class != link.model_class: - raise ValueError( - "All the links must have the same model class" - ) - ids.append(link.ref.id) - return model_class.find(In("_id", ids)).to_list() # type: ignore - - @classmethod - def fetch_many(cls, links: List["Link"]): - result = [] - for link in links: - result.append(link.fetch()) - return result - - @classmethod - def __get_validators__(cls): - yield cls.validate - - @classmethod - def validate(cls, v: Union[DBRef, T], field: ModelField): - model_class = field.sub_fields[0].type_ # type: ignore - if isinstance(v, DBRef): - return cls(ref=v, model_class=model_class) - if isinstance(v, Link): - return v - if isinstance(v, dict) or isinstance(v, BaseModel): - return model_class.validate(v) - new_id = parse_obj_as(model_class.__fields__["id"].type_, v) - ref = DBRef(collection=model_class.get_collection_name(), id=new_id) - return cls(ref=ref, model_class=model_class) - - def to_ref(self): - return self.ref - - def to_dict(self): - return {"id": str(self.ref.id), "collection": self.ref.collection} - - -ENCODERS_BY_TYPE[Link] = lambda o: o.to_dict() diff --git a/beanie/sync/odm/interfaces/__init__.py b/beanie/sync/odm/interfaces/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/beanie/sync/odm/interfaces/aggregate.py b/beanie/sync/odm/interfaces/aggregate.py deleted file mode 100644 index 84442562..00000000 --- a/beanie/sync/odm/interfaces/aggregate.py +++ /dev/null @@ -1,70 +0,0 @@ -from typing import TypeVar, Type, Optional, Union, Dict, Any, overload - -from pydantic import BaseModel -from pymongo.client_session import ClientSession - -from beanie.sync.odm.queries.aggregation import AggregationQuery -from beanie.sync.odm.queries.find import FindMany - -DocType = TypeVar("DocType", bound="AggregateInterface") -DocumentProjectionType = TypeVar("DocumentProjectionType", bound=BaseModel) - - -class AggregateInterface: - @classmethod - def find_all(cls) -> FindMany: - pass - - @overload - @classmethod - def aggregate( - cls: Type[DocType], - aggregation_pipeline: list, - projection_model: None = None, - session: Optional[ClientSession] = None, - ignore_cache: bool = False, - **pymongo_kwargs, - ) -> AggregationQuery[Dict[str, Any]]: - ... - - @overload - @classmethod - def aggregate( - cls: Type[DocType], - aggregation_pipeline: list, - projection_model: Type[DocumentProjectionType], - session: Optional[ClientSession] = None, - ignore_cache: bool = False, - **pymongo_kwargs, - ) -> AggregationQuery[DocumentProjectionType]: - ... - - @classmethod - def aggregate( - cls: Type[DocType], - aggregation_pipeline: list, - projection_model: Optional[Type[DocumentProjectionType]] = None, - session: Optional[ClientSession] = None, - ignore_cache: bool = False, - **pymongo_kwargs, - ) -> Union[ - AggregationQuery[Dict[str, Any]], - AggregationQuery[DocumentProjectionType], - ]: - """ - Aggregate over collection. - Returns [AggregationQuery](https://roman-right.github.io/beanie/api/queries/#aggregationquery) query object - :param aggregation_pipeline: list - aggregation pipeline - :param projection_model: Type[BaseModel] - :param session: Optional[ClientSession] - :param ignore_cache: bool - :param **pymongo_kwargs: pymongo native parameters for aggregate operation - :return: [AggregationQuery](https://roman-right.github.io/beanie/api/queries/#aggregationquery) - """ - return cls.find_all().aggregate( - aggregation_pipeline=aggregation_pipeline, - projection_model=projection_model, - session=session, - ignore_cache=ignore_cache, - **pymongo_kwargs, - ) diff --git a/beanie/sync/odm/interfaces/aggregation_methods.py b/beanie/sync/odm/interfaces/aggregation_methods.py deleted file mode 100644 index e3c7ccef..00000000 --- a/beanie/sync/odm/interfaces/aggregation_methods.py +++ /dev/null @@ -1,190 +0,0 @@ -from abc import abstractmethod -from typing import Any, Optional, Union, List, Dict, cast - -from pymongo.client_session import ClientSession - -from beanie.odm.fields import ExpressionField - - -class AggregateMethods: - """ - Aggregate methods - """ - - @abstractmethod - def aggregate( - self, - aggregation_pipeline, - projection_model=None, - session=None, - ignore_cache: bool = False, - ): - ... - - def sum( - self, - field: Union[str, ExpressionField], - session: Optional[ClientSession] = None, - ignore_cache: bool = False, - ) -> Optional[float]: - """ - Sum of values of the given field - - Example: - - ```python - - class Sample(Document): - price: int - count: int - - sum_count = Document.find(Sample.price <= 100).sum(Sample.count) - - ``` - - :param field: Union[str, ExpressionField] - :param session: Optional[ClientSession] - pymongo session - :param ignore_cache: bool - :return: float - sum. None if there are no items. - """ - pipeline = [ - {"$group": {"_id": None, "sum": {"$sum": f"${field}"}}}, - {"$project": {"_id": 0, "sum": 1}}, - ] - - # As we did not supply a projection we can safely cast the type (hinting to mypy that we know the type) - result: List[Dict[str, Any]] = cast( - List[Dict[str, Any]], - self.aggregate( - aggregation_pipeline=pipeline, - session=session, - ignore_cache=ignore_cache, - ).to_list(), # type: ignore # TODO: pyright issue, fix - ) - if not result: - return None - return result[0]["sum"] - - def avg( - self, - field, - session: Optional[ClientSession] = None, - ignore_cache: bool = False, - ) -> Optional[float]: - """ - Average of values of the given field - - Example: - - ```python - - class Sample(Document): - price: int - count: int - - avg_count = Document.find(Sample.price <= 100).avg(Sample.count) - ``` - - :param field: Union[str, ExpressionField] - :param session: Optional[ClientSession] - pymongo session - :param ignore_cache: bool - :return: Optional[float] - avg. None if there are no items. - """ - pipeline = [ - {"$group": {"_id": None, "avg": {"$avg": f"${field}"}}}, - {"$project": {"_id": 0, "avg": 1}}, - ] - - result: List[Dict[str, Any]] = cast( - List[Dict[str, Any]], - self.aggregate( - aggregation_pipeline=pipeline, - session=session, - ignore_cache=ignore_cache, - ).to_list(), # type: ignore # TODO: pyright issue, fix - ) - if not result: - return None - return result[0]["avg"] - - def max( - self, - field: Union[str, ExpressionField], - session: Optional[ClientSession] = None, - ignore_cache: bool = False, - ) -> Optional[float]: - """ - Max of the values of the given field - - Example: - - ```python - - class Sample(Document): - price: int - count: int - - max_count = Document.find(Sample.price <= 100).max(Sample.count) - ``` - - :param field: Union[str, ExpressionField] - :param session: Optional[ClientSession] - pymongo session - :return: float - max. None if there are no items. - """ - pipeline = [ - {"$group": {"_id": None, "max": {"$max": f"${field}"}}}, - {"$project": {"_id": 0, "max": 1}}, - ] - - result: List[Dict[str, Any]] = cast( - List[Dict[str, Any]], - self.aggregate( - aggregation_pipeline=pipeline, - session=session, - ignore_cache=ignore_cache, - ).to_list(), # type: ignore # TODO: pyright issue, fix - ) - if not result: - return None - return result[0]["max"] - - def min( - self, - field: Union[str, ExpressionField], - session: Optional[ClientSession] = None, - ignore_cache: bool = False, - ) -> Optional[float]: - """ - Min of the values of the given field - - Example: - - ```python - - class Sample(Document): - price: int - count: int - - min_count = Document.find(Sample.price <= 100).min(Sample.count) - ``` - - :param field: Union[str, ExpressionField] - :param session: Optional[ClientSession] - pymongo session - :return: float - min. None if there are no items. - """ - pipeline = [ - {"$group": {"_id": None, "min": {"$min": f"${field}"}}}, - {"$project": {"_id": 0, "min": 1}}, - ] - - result: List[Dict[str, Any]] = cast( - List[Dict[str, Any]], - self.aggregate( - aggregation_pipeline=pipeline, - session=session, - ignore_cache=ignore_cache, - ).to_list(), # type: ignore # TODO: pyright issue, fix - ) - if not result: - return None - return result[0]["min"] diff --git a/beanie/sync/odm/interfaces/detector.py b/beanie/sync/odm/interfaces/detector.py deleted file mode 100644 index f04abf6e..00000000 --- a/beanie/sync/odm/interfaces/detector.py +++ /dev/null @@ -1,13 +0,0 @@ -from enum import Enum - - -class ModelType(str, Enum): - Document = "Document" - View = "View" - UnionDoc = "UnionDoc" - - -class DetectionInterface: - @classmethod - def get_model_type(cls) -> ModelType: - return ModelType.Document diff --git a/beanie/sync/odm/interfaces/find.py b/beanie/sync/odm/interfaces/find.py deleted file mode 100644 index 0fce7164..00000000 --- a/beanie/sync/odm/interfaces/find.py +++ /dev/null @@ -1,354 +0,0 @@ -from typing import ( - Optional, - List, - Type, - Union, - Tuple, - Mapping, - Any, - overload, - ClassVar, - TypeVar, -) - -from pydantic import ( - BaseModel, -) -from pymongo.client_session import ClientSession - -from beanie.odm.enums import SortDirection -from beanie.sync.odm.queries.find import FindOne, FindMany -from beanie.sync.odm.settings.base import ItemSettings - -DocType = TypeVar("DocType", bound="FindInterface") -DocumentProjectionType = TypeVar("DocumentProjectionType", bound=BaseModel) - - -class FindInterface: - # Customization - # Query builders could be replaced in the inherited classes - _find_one_query_class: ClassVar[Type] = FindOne - _find_many_query_class: ClassVar[Type] = FindMany - - @classmethod - def get_settings(cls) -> ItemSettings: - pass - - @overload - @classmethod - def find_one( - cls: Type["DocType"], - *args: Union[Mapping[str, Any], bool], - projection_model: None = None, - session: Optional[ClientSession] = None, - ignore_cache: bool = False, - fetch_links: bool = False, - **pymongo_kwargs, - ) -> FindOne["DocType"]: - ... - - @overload - @classmethod - def find_one( - cls: Type["DocType"], - *args: Union[Mapping[str, Any], bool], - projection_model: Type["DocumentProjectionType"], - session: Optional[ClientSession] = None, - ignore_cache: bool = False, - fetch_links: bool = False, - **pymongo_kwargs, - ) -> FindOne["DocumentProjectionType"]: - ... - - @classmethod - def find_one( - cls: Type["DocType"], - *args: Union[Mapping[str, Any], bool], - projection_model: Optional[Type["DocumentProjectionType"]] = None, - session: Optional[ClientSession] = None, - ignore_cache: bool = False, - fetch_links: bool = False, - **pymongo_kwargs, - ) -> Union[FindOne["DocType"], FindOne["DocumentProjectionType"]]: - """ - Find one document by criteria. - Returns [FindOne](https://roman-right.github.io/beanie/api/queries/#findone) query object. - When awaited this will either return a document or None if no document exists for the search criteria. - - :param args: *Mapping[str, Any] - search criteria - :param projection_model: Optional[Type[BaseModel]] - projection model - :param session: Optional[ClientSession] - pymongo session instance - :param ignore_cache: bool - :param **pymongo_kwargs: pymongo native parameters for find operation (if Document class contains links, this parameter must fit the respective parameter of the aggregate MongoDB function) - :return: [FindOne](https://roman-right.github.io/beanie/api/queries/#findone) - find query instance - """ - args = cls._add_class_id_filter(args) - return cls._find_one_query_class(document_model=cls).find_one( - *args, - projection_model=projection_model, - session=session, - ignore_cache=ignore_cache, - fetch_links=fetch_links, - **pymongo_kwargs, - ) - - @overload - @classmethod - def find_many( - cls: Type["DocType"], - *args: Union[Mapping[str, Any], bool], - projection_model: None = None, - skip: Optional[int] = None, - limit: Optional[int] = None, - sort: Union[None, str, List[Tuple[str, SortDirection]]] = None, - session: Optional[ClientSession] = None, - ignore_cache: bool = False, - fetch_links: bool = False, - **pymongo_kwargs, - ) -> FindMany["DocType"]: - ... - - @overload - @classmethod - def find_many( - cls: Type["DocType"], - *args: Union[Mapping[str, Any], bool], - projection_model: Type["DocumentProjectionType"] = None, - skip: Optional[int] = None, - limit: Optional[int] = None, - sort: Union[None, str, List[Tuple[str, SortDirection]]] = None, - session: Optional[ClientSession] = None, - ignore_cache: bool = False, - fetch_links: bool = False, - **pymongo_kwargs, - ) -> FindMany["DocumentProjectionType"]: - ... - - @classmethod - def find_many( - cls: Type["DocType"], - *args: Union[Mapping[str, Any], bool], - projection_model: Optional[Type["DocumentProjectionType"]] = None, - skip: Optional[int] = None, - limit: Optional[int] = None, - sort: Union[None, str, List[Tuple[str, SortDirection]]] = None, - session: Optional[ClientSession] = None, - ignore_cache: bool = False, - fetch_links: bool = False, - **pymongo_kwargs, - ) -> Union[FindMany["DocType"], FindMany["DocumentProjectionType"]]: - """ - Find many documents by criteria. - Returns [FindMany](https://roman-right.github.io/beanie/api/queries/#findmany) query object - - :param args: *Mapping[str, Any] - search criteria - :param skip: Optional[int] - The number of documents to omit. - :param limit: Optional[int] - The maximum number of results to return. - :param sort: Union[None, str, List[Tuple[str, SortDirection]]] - A key or a list of (key, direction) pairs specifying the sort order for this query. - :param projection_model: Optional[Type[BaseModel]] - projection model - :param session: Optional[ClientSession] - pymongo session - :param ignore_cache: bool - :param **pymongo_kwargs: pymongo native parameters for find operation (if Document class contains links, this parameter must fit the respective parameter of the aggregate MongoDB function) - :return: [FindMany](https://roman-right.github.io/beanie/api/queries/#findmany) - query instance - """ - args = cls._add_class_id_filter(args) - return cls._find_many_query_class(document_model=cls).find_many( - *args, - sort=sort, - skip=skip, - limit=limit, - projection_model=projection_model, - session=session, - ignore_cache=ignore_cache, - fetch_links=fetch_links, - **pymongo_kwargs, - ) - - @overload - @classmethod - def find( - cls: Type["DocType"], - *args: Union[Mapping[str, Any], bool], - projection_model: None = None, - skip: Optional[int] = None, - limit: Optional[int] = None, - sort: Union[None, str, List[Tuple[str, SortDirection]]] = None, - session: Optional[ClientSession] = None, - ignore_cache: bool = False, - fetch_links: bool = False, - **pymongo_kwargs, - ) -> FindMany["DocType"]: - ... - - @overload - @classmethod - def find( - cls: Type["DocType"], - *args: Union[Mapping[str, Any], bool], - projection_model: Type["DocumentProjectionType"], - skip: Optional[int] = None, - limit: Optional[int] = None, - sort: Union[None, str, List[Tuple[str, SortDirection]]] = None, - session: Optional[ClientSession] = None, - ignore_cache: bool = False, - fetch_links: bool = False, - **pymongo_kwargs, - ) -> FindMany["DocumentProjectionType"]: - ... - - @classmethod - def find( - cls: Type["DocType"], - *args: Union[Mapping[str, Any], bool], - projection_model: Optional[Type["DocumentProjectionType"]] = None, - skip: Optional[int] = None, - limit: Optional[int] = None, - sort: Union[None, str, List[Tuple[str, SortDirection]]] = None, - session: Optional[ClientSession] = None, - ignore_cache: bool = False, - fetch_links: bool = False, - **pymongo_kwargs, - ) -> Union[FindMany["DocType"], FindMany["DocumentProjectionType"]]: - """ - The same as find_many - """ - return cls.find_many( - *args, - skip=skip, - limit=limit, - sort=sort, - projection_model=projection_model, - session=session, - ignore_cache=ignore_cache, - fetch_links=fetch_links, - **pymongo_kwargs, - ) - - @overload - @classmethod - def find_all( - cls: Type["DocType"], - skip: Optional[int] = None, - limit: Optional[int] = None, - sort: Union[None, str, List[Tuple[str, SortDirection]]] = None, - projection_model: None = None, - session: Optional[ClientSession] = None, - ignore_cache: bool = False, - **pymongo_kwargs, - ) -> FindMany["DocType"]: - ... - - @overload - @classmethod - def find_all( - cls: Type["DocType"], - skip: Optional[int] = None, - limit: Optional[int] = None, - sort: Union[None, str, List[Tuple[str, SortDirection]]] = None, - projection_model: Optional[Type["DocumentProjectionType"]] = None, - session: Optional[ClientSession] = None, - ignore_cache: bool = False, - **pymongo_kwargs, - ) -> FindMany["DocumentProjectionType"]: - ... - - @classmethod - def find_all( - cls: Type["DocType"], - skip: Optional[int] = None, - limit: Optional[int] = None, - sort: Union[None, str, List[Tuple[str, SortDirection]]] = None, - projection_model: Optional[Type["DocumentProjectionType"]] = None, - session: Optional[ClientSession] = None, - ignore_cache: bool = False, - **pymongo_kwargs, - ) -> Union[FindMany["DocType"], FindMany["DocumentProjectionType"]]: - """ - Get all the documents - - :param skip: Optional[int] - The number of documents to omit. - :param limit: Optional[int] - The maximum number of results to return. - :param sort: Union[None, str, List[Tuple[str, SortDirection]]] - A key or a list of (key, direction) pairs specifying the sort order for this query. - :param projection_model: Optional[Type[BaseModel]] - projection model - :param session: Optional[ClientSession] - pymongo session - :param **pymongo_kwargs: pymongo native parameters for find operation (if Document class contains links, this parameter must fit the respective parameter of the aggregate MongoDB function) - :return: [FindMany](https://roman-right.github.io/beanie/api/queries/#findmany) - query instance - """ - return cls.find_many( - {}, - skip=skip, - limit=limit, - sort=sort, - projection_model=projection_model, - session=session, - ignore_cache=ignore_cache, - **pymongo_kwargs, - ) - - @overload - @classmethod - def all( - cls: Type["DocType"], - projection_model: None = None, - skip: Optional[int] = None, - limit: Optional[int] = None, - sort: Union[None, str, List[Tuple[str, SortDirection]]] = None, - session: Optional[ClientSession] = None, - ignore_cache: bool = False, - **pymongo_kwargs, - ) -> FindMany["DocType"]: - ... - - @overload - @classmethod - def all( - cls: Type["DocType"], - projection_model: Type["DocumentProjectionType"], - skip: Optional[int] = None, - limit: Optional[int] = None, - sort: Union[None, str, List[Tuple[str, SortDirection]]] = None, - session: Optional[ClientSession] = None, - ignore_cache: bool = False, - **pymongo_kwargs, - ) -> FindMany["DocumentProjectionType"]: - ... - - @classmethod - def all( - cls: Type["DocType"], - projection_model: Optional[Type["DocumentProjectionType"]] = None, - skip: Optional[int] = None, - limit: Optional[int] = None, - sort: Union[None, str, List[Tuple[str, SortDirection]]] = None, - session: Optional[ClientSession] = None, - ignore_cache: bool = False, - **pymongo_kwargs, - ) -> Union[FindMany["DocType"], FindMany["DocumentProjectionType"]]: - """ - the same as find_all - """ - return cls.find_all( - skip=skip, - limit=limit, - sort=sort, - projection_model=projection_model, - session=session, - ignore_cache=ignore_cache, - **pymongo_kwargs, - ) - - @classmethod - def count(cls) -> int: - """ - Number of documents in the collections - The same as find_all().count() - - :return: int - """ - return cls.find_all().count() - - @classmethod - def _add_class_id_filter(cls, args: Tuple): - if cls.get_settings().union_doc: - args += ({"_class_id": cls.__name__},) - return args diff --git a/beanie/sync/odm/interfaces/getters.py b/beanie/sync/odm/interfaces/getters.py deleted file mode 100644 index bb1c90d8..00000000 --- a/beanie/sync/odm/interfaces/getters.py +++ /dev/null @@ -1,28 +0,0 @@ -from pymongo.collection import Collection - -from beanie.sync.odm.settings.base import ItemSettings - - -class OtherGettersInterface: - @classmethod - def get_settings(cls) -> ItemSettings: - pass - - @classmethod - def get_motor_collection(cls) -> Collection: - return cls.get_settings().motor_collection - - @classmethod - def get_collection_name(cls): - input_class = getattr(cls, "Settings", None) - if input_class is None or not hasattr(input_class, "name"): - return cls.__name__ - return input_class.name - - @classmethod - def get_bson_encoders(cls): - return cls.get_settings().bson_encoders - - @classmethod - def get_link_fields(cls): - return None diff --git a/beanie/sync/odm/interfaces/run.py b/beanie/sync/odm/interfaces/run.py deleted file mode 100644 index 8a288b96..00000000 --- a/beanie/sync/odm/interfaces/run.py +++ /dev/null @@ -1,6 +0,0 @@ -class RunInterface: - def run(self): - raise NotImplementedError - - def __invert__(self): - return self.run() diff --git a/beanie/sync/odm/interfaces/session.py b/beanie/sync/odm/interfaces/session.py deleted file mode 100644 index 4290d243..00000000 --- a/beanie/sync/odm/interfaces/session.py +++ /dev/null @@ -1,19 +0,0 @@ -from typing import Optional - -from pymongo.client_session import ClientSession - - -class SessionMethods: - """ - Session methods - """ - - def set_session(self, session: Optional[ClientSession] = None): - """ - Set pymongo session - :param session: Optional[ClientSession] - pymongo session - :return: - """ - if session is not None: - self.session = session - return self diff --git a/beanie/sync/odm/interfaces/update.py b/beanie/sync/odm/interfaces/update.py deleted file mode 100644 index 46dad13d..00000000 --- a/beanie/sync/odm/interfaces/update.py +++ /dev/null @@ -1,117 +0,0 @@ -from abc import abstractmethod -from typing import Dict, Mapping, Union, Any, Optional - -from pymongo.client_session import ClientSession - -from beanie.sync.odm.bulk import BulkWriter -from beanie.odm.fields import ExpressionField -from beanie.odm.operators.update.general import ( - Set, - CurrentDate, - Inc, -) - - -class UpdateMethods: - """ - Update methods - """ - - @abstractmethod - def update( - self, - *args: Mapping[str, Any], - session: Optional[ClientSession] = None, - bulk_writer: Optional[BulkWriter] = None, - **kwargs - ): - return self - - def set( - self, - expression: Dict[Union[ExpressionField, str], Any], - session: Optional[ClientSession] = None, - bulk_writer: Optional[BulkWriter] = None, - **kwargs - ): - """ - Set values - - Example: - - ```python - - class Sample(Document): - one: int - - Document.find(Sample.one == 1).set({Sample.one: 100}) - - ``` - - Uses [Set operator](https://roman-right.github.io/beanie/api/operators/update/#set) - - :param expression: Dict[Union[ExpressionField, str], Any] - keys and - values to set - :param session: Optional[ClientSession] - pymongo session - :param bulk_writer: Optional[BulkWriter] - bulk writer - :return: self - """ - return self.update( - Set(expression), session=session, bulk_writer=bulk_writer, **kwargs - ) - - def current_date( - self, - expression: Dict[Union[ExpressionField, str], Any], - session: Optional[ClientSession] = None, - bulk_writer: Optional[BulkWriter] = None, - **kwargs - ): - """ - Set current date - - Uses [CurrentDate operator](https://roman-right.github.io/beanie/api/operators/update/#currentdate) - - :param expression: Dict[Union[ExpressionField, str], Any] - :param session: Optional[ClientSession] - pymongo session - :param bulk_writer: Optional[BulkWriter] - bulk writer - :return: self - """ - return self.update( - CurrentDate(expression), - session=session, - bulk_writer=bulk_writer, - **kwargs - ) - - def inc( - self, - expression: Dict[Union[ExpressionField, str], Any], - session: Optional[ClientSession] = None, - bulk_writer: Optional[BulkWriter] = None, - **kwargs - ): - """ - Increment - - Example: - - ```python - - class Sample(Document): - one: int - - Document.find(Sample.one == 1).inc({Sample.one: 100}) - - ``` - - Uses [Inc operator](https://roman-right.github.io/beanie/api/operators/update/#inc) - - :param expression: Dict[Union[ExpressionField, str], Any] - :param session: Optional[ClientSession] - pymongo session - :param bulk_writer: Optional[BulkWriter] - bulk writer - :return: self - """ - return self.update( - Inc(expression), session=session, bulk_writer=bulk_writer, **kwargs - ) diff --git a/beanie/sync/odm/queries/__init__.py b/beanie/sync/odm/queries/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/beanie/sync/odm/queries/aggregation.py b/beanie/sync/odm/queries/aggregation.py deleted file mode 100644 index bb668809..00000000 --- a/beanie/sync/odm/queries/aggregation.py +++ /dev/null @@ -1,110 +0,0 @@ -from typing import ( - Type, - List, - Mapping, - Optional, - TYPE_CHECKING, - Any, - Generic, - TypeVar, -) - -from pydantic import BaseModel - -from motor.core import AgnosticCommandCursor - -from beanie.sync.odm.cache import LRUCache -from beanie.sync.odm.interfaces.session import SessionMethods -from beanie.sync.odm.queries.cursor import BaseCursorQuery -from beanie.sync.odm.utils.projection import get_projection - -if TYPE_CHECKING: - from beanie.sync.odm.documents import DocType - -AggregationProjectionType = TypeVar("AggregationProjectionType") - - -class AggregationQuery( - Generic[AggregationProjectionType], - BaseCursorQuery[AggregationProjectionType], - SessionMethods, -): - """ - Aggregation Query - - Inherited from: - - - [SessionMethods](https://roman-right.github.io/beanie/api/interfaces/#sessionmethods) - session methods - - [BaseCursorQuery](https://roman-right.github.io/beanie/api/queries/#basecursorquery) - generator - """ - - def __init__( - self, - document_model: Type["DocType"], - aggregation_pipeline: List[Mapping[str, Any]], - find_query: Mapping[str, Any], - projection_model: Optional[Type[BaseModel]] = None, - ignore_cache: bool = False, - **pymongo_kwargs - ): - self.aggregation_pipeline: List[ - Mapping[str, Any] - ] = aggregation_pipeline - self.document_model = document_model - self.projection_model = projection_model - self.find_query = find_query - self.session = None - self.ignore_cache = ignore_cache - self.pymongo_kwargs = pymongo_kwargs - - @property - def _cache_key(self) -> str: - return LRUCache.create_key( - { - "type": "Aggregation", - "filter": self.find_query, - "pipeline": self.aggregation_pipeline, - "projection": get_projection(self.projection_model) - if self.projection_model - else None, - } - ) - - def _get_cache(self): - if ( - self.document_model.get_settings().use_cache - and self.ignore_cache is False - ): - return self.document_model._cache.get(self._cache_key) # type: ignore - else: - return None - - def _set_cache(self, data): - if ( - self.document_model.get_settings().use_cache - and self.ignore_cache is False - ): - return self.document_model._cache.set(self._cache_key, data) # type: ignore - - def get_aggregation_pipeline( - self, - ) -> List[Mapping[str, Any]]: - match_pipeline: List[Mapping[str, Any]] = ( - [{"$match": self.find_query}] if self.find_query else [] - ) - projection_pipeline: List[Mapping[str, Any]] = [] - if self.projection_model: - projection = get_projection(self.projection_model) - if projection is not None: - projection_pipeline = [{"$project": projection}] - return match_pipeline + self.aggregation_pipeline + projection_pipeline - - @property - def motor_cursor(self) -> AgnosticCommandCursor: - aggregation_pipeline = self.get_aggregation_pipeline() - return self.document_model.get_motor_collection().aggregate( - aggregation_pipeline, session=self.session, **self.pymongo_kwargs - ) - - def get_projection_model(self) -> Optional[Type[BaseModel]]: - return self.projection_model diff --git a/beanie/sync/odm/queries/cursor.py b/beanie/sync/odm/queries/cursor.py deleted file mode 100644 index 2fc2806e..00000000 --- a/beanie/sync/odm/queries/cursor.py +++ /dev/null @@ -1,86 +0,0 @@ -from abc import abstractmethod -from typing import ( - Optional, - List, - TypeVar, - Type, - Dict, - Any, - Generic, - cast, -) - -from pydantic.main import BaseModel - -from beanie.sync.odm.interfaces.run import RunInterface -from beanie.sync.odm.utils.parsing import parse_obj - -CursorResultType = TypeVar("CursorResultType") - - -class BaseCursorQuery(Generic[CursorResultType], RunInterface): - """ - BaseCursorQuery class. Wrapper over pymongo Cursor, - which parse result with model - """ - - cursor = None - - @abstractmethod - def get_projection_model(self) -> Optional[Type[BaseModel]]: - ... - - @property - @abstractmethod - def motor_cursor(self): - ... - - def _cursor_params(self): - ... - - def __iter__(self): - if self.cursor is None: - self.cursor = self.motor_cursor - return self - - def __next__(self) -> CursorResultType: - if self.cursor is None: - raise RuntimeError("cursor was not set") - next_item = self.cursor.__next__() - projection = self.get_projection_model() - if projection is None: - return next_item - return parse_obj(projection, next_item) # type: ignore - - def _get_cache(self) -> List[Dict[str, Any]]: - ... - - def _set_cache(self, data): - ... - - def to_list( - self, length: Optional[int] = None - ) -> List[CursorResultType]: # noqa - """ - Get list of documents - - :param length: Optional[int] - length of the list - :return: Union[List[BaseModel], List[Dict[str, Any]]] - """ - cursor = self.motor_cursor - if cursor is None: - raise RuntimeError("self.motor_cursor was not set") - motor_list: List[Dict[str, Any]] = self._get_cache() - if motor_list is None: - motor_list = list(cursor)[:length] - self._set_cache(motor_list) - projection = self.get_projection_model() - if projection is not None: - return cast( - List[CursorResultType], - [parse_obj(projection, i) for i in motor_list], - ) - return cast(List[CursorResultType], motor_list) - - def run(self): - return self.to_list() diff --git a/beanie/sync/odm/queries/delete.py b/beanie/sync/odm/queries/delete.py deleted file mode 100644 index 34555209..00000000 --- a/beanie/sync/odm/queries/delete.py +++ /dev/null @@ -1,81 +0,0 @@ -from typing import ( - Type, - TYPE_CHECKING, - Any, - Mapping, - Optional, - Dict, - Union, -) - -from pymongo.results import DeleteResult - -from beanie.sync.odm.bulk import BulkWriter, Operation -from beanie.sync.odm.interfaces.run import RunInterface -from beanie.sync.odm.interfaces.session import SessionMethods -from pymongo import DeleteOne as DeleteOnePyMongo -from pymongo import DeleteMany as DeleteManyPyMongo - -if TYPE_CHECKING: - from beanie.sync.odm.documents import DocType - - -class DeleteQuery(SessionMethods, RunInterface): - """ - Deletion Query - """ - - def __init__( - self, - document_model: Type["DocType"], - find_query: Mapping[str, Any], - bulk_writer: Optional[BulkWriter] = None, - **pymongo_kwargs - ): - self.document_model = document_model - self.find_query = find_query - self.session = None - self.bulk_writer = bulk_writer - self.pymongo_kwargs: Dict[str, Any] = pymongo_kwargs - - -class DeleteMany(DeleteQuery): - def run(self) -> Union[DeleteResult, None, Optional[DeleteResult]]: - """ - Run the query - :return: - """ - if self.bulk_writer is None: - return self.document_model.get_motor_collection().delete_many( - self.find_query, session=self.session, **self.pymongo_kwargs - ) - else: - self.bulk_writer.add_operation( - Operation( - operation=DeleteManyPyMongo, - first_query=self.find_query, - object_class=self.document_model, - ) - ) - return None - - -class DeleteOne(DeleteQuery): - def run(self) -> Union[DeleteResult, None, Optional[DeleteResult]]: - """ - Run the query - :return: - """ - if self.bulk_writer is None: - return self.document_model.get_motor_collection().delete_one( - self.find_query, session=self.session, **self.pymongo_kwargs - ) - else: - self.bulk_writer.add_operation( - Operation( - operation=DeleteOnePyMongo, - first_query=self.find_query, - object_class=self.document_model, - ) - ) - return None diff --git a/beanie/sync/odm/queries/find.py b/beanie/sync/odm/queries/find.py deleted file mode 100644 index 281e644c..00000000 --- a/beanie/sync/odm/queries/find.py +++ /dev/null @@ -1,879 +0,0 @@ -from typing import ( - Callable, - TYPE_CHECKING, - Any, - Dict, - Generic, - List, - Mapping, - Optional, - Tuple, - Type, - TypeVar, - Union, - cast, - overload, -) - -from pydantic import BaseModel -from pymongo.client_session import ClientSession -from pymongo.results import UpdateResult - -from pymongo import ReplaceOne - -from beanie.exceptions import DocumentNotFound -from beanie.sync.odm.cache import LRUCache -from beanie.sync.odm.bulk import BulkWriter, Operation -from beanie.odm.enums import SortDirection -from beanie.sync.odm.interfaces.aggregation_methods import AggregateMethods -from beanie.sync.odm.interfaces.run import RunInterface -from beanie.sync.odm.interfaces.session import SessionMethods -from beanie.sync.odm.interfaces.update import UpdateMethods -from beanie.odm.operators.find.logical import And -from beanie.sync.odm.queries.aggregation import AggregationQuery -from beanie.sync.odm.queries.cursor import BaseCursorQuery -from beanie.sync.odm.queries.delete import ( - DeleteMany, - DeleteOne, -) -from beanie.sync.odm.queries.update import ( - UpdateQuery, - UpdateMany, - UpdateOne, -) -from beanie.sync.odm.utils.encoder import Encoder -from beanie.sync.odm.utils.find import construct_lookup_queries -from beanie.sync.odm.utils.parsing import parse_obj -from beanie.sync.odm.utils.projection import get_projection -from beanie.sync.odm.utils.relations import convert_ids - -if TYPE_CHECKING: - from beanie.sync.odm.documents import DocType - -FindQueryProjectionType = TypeVar("FindQueryProjectionType", bound=BaseModel) -FindQueryResultType = TypeVar("FindQueryResultType", bound=BaseModel) - - -class FindQuery(Generic[FindQueryResultType], UpdateMethods, SessionMethods): - """ - Find Query base class - - Inherited from: - - - [SessionMethods](https://roman-right.github.io/beanie/api/interfaces/#sessionmethods) - - [UpdateMethods](https://roman-right.github.io/beanie/api/interfaces/#aggregatemethods) - """ - - UpdateQueryType: Union[ - Type[UpdateQuery], Type[UpdateMany], Type[UpdateOne] - ] = UpdateQuery - DeleteQueryType: Union[Type[DeleteOne], Type[DeleteMany]] = DeleteMany - AggregationQueryType = AggregationQuery - - def __init__(self, document_model: Type["DocType"]): - self.document_model: Type["DocType"] = document_model - self.find_expressions: List[Mapping[str, Any]] = [] - self.projection_model: Type[FindQueryResultType] = cast( - Type[FindQueryResultType], self.document_model - ) - self.session = None - self.encoders: Dict[Any, Callable[[Any], Any]] = {} - self.ignore_cache: bool = False - self.encoders = self.document_model.get_bson_encoders() - self.fetch_links: bool = False - self.pymongo_kwargs: Dict[str, Any] = {} - - def prepare_find_expressions(self): - if self.document_model.get_link_fields() is not None: - for i, query in enumerate(self.find_expressions): - self.find_expressions[i] = convert_ids( - query, - doc=self.document_model, - fetch_links=self.fetch_links, - ) - - def get_filter_query(self) -> Mapping[str, Any]: - """ - - Returns: MongoDB filter query - - """ - self.prepare_find_expressions() - if self.find_expressions: - return Encoder(custom_encoders=self.encoders).encode( - And(*self.find_expressions).query - ) - else: - return {} - - def update( - self, - *args: Mapping[str, Any], - session: Optional[ClientSession] = None, - bulk_writer: Optional[BulkWriter] = None, - **pymongo_kwargs, - ): - """ - Create Update with modifications query - and provide search criteria there - - :param args: *Mapping[str,Any] - the modifications to apply. - :param session: Optional[ClientSession] - :param bulk_writer: Optional[BulkWriter] - :return: UpdateMany query - """ - self.set_session(session) - return ( - self.UpdateQueryType( - document_model=self.document_model, - find_query=self.get_filter_query(), - ) - .update(*args, bulk_writer=bulk_writer, **pymongo_kwargs) - .set_session(session=self.session) - ) - - def upsert( - self, - *args: Mapping[str, Any], - on_insert: "DocType", - session: Optional[ClientSession] = None, - bulk_writer: Optional[BulkWriter] = None, - **pymongo_kwargs, - ): - """ - Create Update with modifications query - and provide search criteria there - - :param args: *Mapping[str,Any] - the modifications to apply. - :param on_insert: DocType - document to insert if there is no matched - document in the collection - :param session: Optional[ClientSession] - :return: UpdateMany query - """ - self.set_session(session) - return ( - self.UpdateQueryType( - document_model=self.document_model, - find_query=self.get_filter_query(), - ) - .upsert( - *args, - on_insert=on_insert, - bulk_writer=bulk_writer, - **pymongo_kwargs, - ) - .set_session(session=self.session) - ) - - def delete( - self, - session: Optional[ClientSession] = None, - bulk_writer: Optional[BulkWriter] = None, - **pymongo_kwargs, - ) -> Union[DeleteOne, DeleteMany]: - """ - Provide search criteria to the Delete query - - :param session: Optional[ClientSession] - :return: Union[DeleteOne, DeleteMany] - """ - self.set_session(session=session) - return self.DeleteQueryType( - document_model=self.document_model, - find_query=self.get_filter_query(), - bulk_writer=bulk_writer, - **pymongo_kwargs, - ).set_session(session=session) - - def project(self, projection_model): - """ - Apply projection parameter - :param projection_model: Optional[Type[BaseModel]] - projection model - :return: self - """ - if projection_model is not None: - self.projection_model = projection_model - return self - - def get_projection_model(self) -> Type[FindQueryResultType]: - return self.projection_model - - def count(self) -> int: - """ - Number of found documents - :return: int - """ - return self.document_model.get_motor_collection().count_documents( - self.get_filter_query() - ) - - def exists(self) -> bool: - """ - If find query will return anything - - :return: bool - """ - return self.count() > 0 - - -class FindMany( - FindQuery[FindQueryResultType], - BaseCursorQuery[FindQueryResultType], - AggregateMethods, -): - """ - Find Many query class - - Inherited from: - - - [FindQuery](https://roman-right.github.io/beanie/api/queries/#findquery) - - [BaseCursorQuery](https://roman-right.github.io/beanie/api/queries/#basecursorquery) - generator - - [AggregateMethods](https://roman-right.github.io/beanie/api/interfaces/#aggregatemethods) - - """ - - UpdateQueryType = UpdateMany - DeleteQueryType = DeleteMany - - def __init__(self, document_model: Type["DocType"]): - super(FindMany, self).__init__(document_model=document_model) - self.sort_expressions: List[Tuple[str, SortDirection]] = [] - self.skip_number: int = 0 - self.limit_number: int = 0 - - @overload - def find_many( - self: "FindMany[FindQueryResultType]", - *args: Union[Mapping[str, Any], bool], - projection_model: None = None, - skip: Optional[int] = None, - limit: Optional[int] = None, - sort: Union[None, str, List[Tuple[str, SortDirection]]] = None, - session: Optional[ClientSession] = None, - ignore_cache: bool = False, - fetch_links: bool = False, - **pymongo_kwargs, - ) -> "FindMany[FindQueryResultType]": - ... - - @overload - def find_many( - self: "FindMany[FindQueryResultType]", - *args: Union[Mapping[str, Any], bool], - projection_model: Type[FindQueryProjectionType] = None, - skip: Optional[int] = None, - limit: Optional[int] = None, - sort: Union[None, str, List[Tuple[str, SortDirection]]] = None, - session: Optional[ClientSession] = None, - ignore_cache: bool = False, - fetch_links: bool = False, - **pymongo_kwargs, - ) -> "FindMany[FindQueryProjectionType]": - ... - - def find_many( - self: "FindMany[FindQueryResultType]", - *args: Union[Mapping[str, Any], bool], - projection_model: Optional[Type[FindQueryProjectionType]] = None, - skip: Optional[int] = None, - limit: Optional[int] = None, - sort: Union[None, str, List[Tuple[str, SortDirection]]] = None, - session: Optional[ClientSession] = None, - ignore_cache: bool = False, - fetch_links: bool = False, - **pymongo_kwargs, - ) -> Union[ - "FindMany[FindQueryResultType]", "FindMany[FindQueryProjectionType]" - ]: - """ - Find many documents by criteria - - :param args: *Mapping[str, Any] - search criteria - :param skip: Optional[int] - The number of documents to omit. - :param limit: Optional[int] - The maximum number of results to return. - :param sort: Union[None, str, List[Tuple[str, SortDirection]]] - A key - or a list of (key, direction) pairs specifying the sort order - for this query. - :param projection_model: Optional[Type[BaseModel]] - projection model - :param session: Optional[ClientSession] - pymongo session - :param ignore_cache: bool - :param **pymongo_kwargs: pymongo native parameters for find operation (if Document class contains links, this parameter must fit the respective parameter of the aggregate MongoDB function) - :return: FindMany - query instance - """ - self.find_expressions += args # type: ignore # bool workaround - self.skip(skip) - self.limit(limit) - self.sort(sort) - self.project(projection_model) - self.set_session(session=session) - self.ignore_cache = ignore_cache - self.fetch_links = fetch_links - self.pymongo_kwargs.update(pymongo_kwargs) - return self - - # TODO probably merge FindOne and FindMany to one class to avoid this - # code duplication - - @overload - def project( - self: "FindMany", - projection_model: None, - ) -> "FindMany[FindQueryResultType]": - ... - - @overload - def project( - self: "FindMany", - projection_model: Type[FindQueryProjectionType], - ) -> "FindMany[FindQueryProjectionType]": - ... - - def project( - self: "FindMany", - projection_model: Optional[Type[FindQueryProjectionType]], - ) -> Union[ - "FindMany[FindQueryResultType]", "FindMany[FindQueryProjectionType]" - ]: - """ - Apply projection parameter - - :param projection_model: Optional[Type[BaseModel]] - projection model - :return: self - """ - super().project(projection_model) - return self - - @overload - def find( - self: "FindMany[FindQueryResultType]", - *args: Union[Mapping[str, Any], bool], - projection_model: None = None, - skip: Optional[int] = None, - limit: Optional[int] = None, - sort: Union[None, str, List[Tuple[str, SortDirection]]] = None, - session: Optional[ClientSession] = None, - ignore_cache: bool = False, - fetch_links: bool = False, - **pymongo_kwargs, - ) -> "FindMany[FindQueryResultType]": - ... - - @overload - def find( - self: "FindMany[FindQueryResultType]", - *args: Union[Mapping[str, Any], bool], - projection_model: Type[FindQueryProjectionType] = None, - skip: Optional[int] = None, - limit: Optional[int] = None, - sort: Union[None, str, List[Tuple[str, SortDirection]]] = None, - session: Optional[ClientSession] = None, - ignore_cache: bool = False, - fetch_links: bool = False, - **pymongo_kwargs, - ) -> "FindMany[FindQueryProjectionType]": - ... - - def find( - self: "FindMany[FindQueryResultType]", - *args: Union[Mapping[str, Any], bool], - projection_model: Optional[Type[FindQueryProjectionType]] = None, - skip: Optional[int] = None, - limit: Optional[int] = None, - sort: Union[None, str, List[Tuple[str, SortDirection]]] = None, - session: Optional[ClientSession] = None, - ignore_cache: bool = False, - fetch_links: bool = False, - **pymongo_kwargs, - ) -> Union[ - "FindMany[FindQueryResultType]", "FindMany[FindQueryProjectionType]" - ]: - """ - The same as `find_many(...)` - """ - return self.find_many( - *args, - skip=skip, - limit=limit, - sort=sort, - projection_model=projection_model, - session=session, - ignore_cache=ignore_cache, - fetch_links=fetch_links, - **pymongo_kwargs, - ) - - def sort( - self, - *args: Optional[ - Union[ - str, Tuple[str, SortDirection], List[Tuple[str, SortDirection]] - ] - ], - ) -> "FindMany[FindQueryResultType]": - """ - Add sort parameters - :param args: Union[str, Tuple[str, SortDirection], - List[Tuple[str, SortDirection]]] - A key or a tuple (key, direction) - or a list of (key, direction) pairs specifying - the sort order for this query. - :return: self - """ - for arg in args: - if arg is None: - pass - elif isinstance(arg, list): - self.sort(*arg) - elif isinstance(arg, tuple): - self.sort_expressions.append(arg) - elif isinstance(arg, str): - if arg.startswith("+"): - self.sort_expressions.append( - (arg[1:], SortDirection.ASCENDING) - ) - elif arg.startswith("-"): - self.sort_expressions.append( - (arg[1:], SortDirection.DESCENDING) - ) - else: - self.sort_expressions.append( - (arg, SortDirection.ASCENDING) - ) - else: - raise TypeError("Wrong argument type") - return self - - def skip(self, n: Optional[int]) -> "FindMany[FindQueryResultType]": - """ - Set skip parameter - :param n: int - :return: self - """ - if n is not None: - self.skip_number = n - return self - - def limit(self, n: Optional[int]) -> "FindMany[FindQueryResultType]": - """ - Set limit parameter - :param n: int - :return: - """ - if n is not None: - self.limit_number = n - return self - - def update_many( - self, - *args: Mapping[str, Any], - session: Optional[ClientSession] = None, - bulk_writer: Optional[BulkWriter] = None, - **pymongo_kwargs, - ) -> UpdateMany: - """ - Provide search criteria to the - [UpdateMany](https://roman-right.github.io/beanie/api/queries/#updatemany) query - - :param args: *Mappingp[str,Any] - the modifications to apply. - :param session: Optional[ClientSession] - :return: [UpdateMany](https://roman-right.github.io/beanie/api/queries/#updatemany) query - """ - return cast( - UpdateMany, - self.update( - *args, - session=session, - bulk_writer=bulk_writer, - **pymongo_kwargs, - ), - ) - - def delete_many( - self, - session: Optional[ClientSession] = None, - bulk_writer: Optional[BulkWriter] = None, - **pymongo_kwargs, - ) -> DeleteMany: - """ - Provide search criteria to the [DeleteMany](https://roman-right.github.io/beanie/api/queries/#deletemany) query - - :param session: - :return: [DeleteMany](https://roman-right.github.io/beanie/api/queries/#deletemany) query - """ - # We need to cast here to tell mypy that we are sure about the type. - # This is because delete may also return a DeleteOne type in general, and mypy can not be sure in this case - # See https://mypy.readthedocs.io/en/stable/common_issues.html#narrowing-and-inner-functions - return cast( - DeleteMany, - self.delete( - session=session, bulk_writer=bulk_writer, **pymongo_kwargs - ), - ) - - @overload - def aggregate( - self, - aggregation_pipeline: List[Any], - projection_model: None = None, - session: Optional[ClientSession] = None, - ignore_cache: bool = False, - **pymongo_kwargs, - ) -> AggregationQuery[Dict[str, Any]]: - ... - - @overload - def aggregate( - self, - aggregation_pipeline: List[Any], - projection_model: Type[FindQueryProjectionType], - session: Optional[ClientSession] = None, - ignore_cache: bool = False, - **pymongo_kwargs, - ) -> AggregationQuery[FindQueryProjectionType]: - ... - - def aggregate( - self, - aggregation_pipeline: List[Any], - projection_model: Optional[Type[FindQueryProjectionType]] = None, - session: Optional[ClientSession] = None, - ignore_cache: bool = False, - **pymongo_kwargs, - ) -> Union[ - AggregationQuery[Dict[str, Any]], - AggregationQuery[FindQueryProjectionType], - ]: - """ - Provide search criteria to the [AggregationQuery](https://roman-right.github.io/beanie/api/queries/#aggregationquery) - - :param aggregation_pipeline: list - aggregation pipeline. MongoDB doc: - - :param projection_model: Type[BaseModel] - Projection Model - :param session: Optional[ClientSession] - PyMongo session - :param ignore_cache: bool - :return:[AggregationQuery](https://roman-right.github.io/beanie/api/queries/#aggregationquery) - """ - self.set_session(session=session) - return self.AggregationQueryType( - aggregation_pipeline=aggregation_pipeline, - document_model=self.document_model, - projection_model=projection_model, - find_query=self.get_filter_query(), - ignore_cache=ignore_cache, - **pymongo_kwargs, - ).set_session(session=self.session) - - @property - def _cache_key(self) -> str: - return LRUCache.create_key( - { - "type": "FindMany", - "filter": self.get_filter_query(), - "sort": self.sort_expressions, - "projection": get_projection(self.projection_model), - "skip": self.skip_number, - "limit": self.limit_number, - } - ) - - def _get_cache(self): - if ( - self.document_model.get_settings().use_cache - and self.ignore_cache is False - ): - return self.document_model._cache.get( # type: ignore - self._cache_key - ) - else: - return None - - def _set_cache(self, data): - if ( - self.document_model.get_settings().use_cache - and self.ignore_cache is False - ): - return self.document_model._cache.set( # type: ignore - self._cache_key, data - ) - - @property - def motor_cursor(self): - if self.fetch_links: - aggregation_pipeline: List[ - Dict[str, Any] - ] = construct_lookup_queries(self.document_model) - - aggregation_pipeline.append({"$match": self.get_filter_query()}) - - sort_pipeline = { - "$sort": {i[0]: i[1] for i in self.sort_expressions} - } - if sort_pipeline["$sort"]: - aggregation_pipeline.append(sort_pipeline) - if self.skip_number != 0: - aggregation_pipeline.append({"$skip": self.skip_number}) - if self.limit_number != 0: - aggregation_pipeline.append({"$limit": self.limit_number}) - - projection = get_projection(self.projection_model) - - if projection is not None: - aggregation_pipeline.append({"$project": projection}) - - return self.document_model.get_motor_collection().aggregate( - aggregation_pipeline, - session=self.session, - **self.pymongo_kwargs, - ) - - return self.document_model.get_motor_collection().find( - filter=self.get_filter_query(), - sort=self.sort_expressions, - projection=get_projection(self.projection_model), - skip=self.skip_number, - limit=self.limit_number, - session=self.session, - **self.pymongo_kwargs, - ) - - def first_or_none(self) -> Optional[FindQueryResultType]: - """ - Returns the first found element or None if no elements were found - """ - res = self.limit(1).to_list() - if not res: - return None - return res[0] - - -class FindOne(FindQuery[FindQueryResultType], RunInterface): - """ - Find One query class - - Inherited from: - - - [FindQuery](https://roman-right.github.io/beanie/api/queries/#findquery) - """ - - UpdateQueryType = UpdateOne - DeleteQueryType = DeleteOne - - @overload - def project( - self: "FindOne[FindQueryResultType]", - projection_model: None = None, - ) -> "FindOne[FindQueryResultType]": - ... - - @overload - def project( - self: "FindOne[FindQueryResultType]", - projection_model: Type[FindQueryProjectionType], - ) -> "FindOne[FindQueryProjectionType]": - ... - - # TODO probably merge FindOne and FindMany to one class to avoid this - # code duplication - - def project( - self: "FindOne[FindQueryResultType]", - projection_model: Optional[Type[FindQueryProjectionType]] = None, - ) -> Union[ - "FindOne[FindQueryResultType]", "FindOne[FindQueryProjectionType]" - ]: - """ - Apply projection parameter - :param projection_model: Optional[Type[BaseModel]] - projection model - :return: self - """ - super().project(projection_model) - return self - - @overload - def find_one( - self: "FindOne[FindQueryResultType]", - *args: Union[Mapping[str, Any], bool], - projection_model: None = None, - session: Optional[ClientSession] = None, - ignore_cache: bool = False, - fetch_links: bool = False, - **pymongo_kwargs, - ) -> "FindOne[FindQueryResultType]": - ... - - @overload - def find_one( - self: "FindOne[FindQueryResultType]", - *args: Union[Mapping[str, Any], bool], - projection_model: Type[FindQueryProjectionType], - session: Optional[ClientSession] = None, - ignore_cache: bool = False, - fetch_links: bool = False, - **pymongo_kwargs, - ) -> "FindOne[FindQueryProjectionType]": - ... - - def find_one( - self: "FindOne[FindQueryResultType]", - *args: Union[Mapping[str, Any], bool], - projection_model: Optional[Type[FindQueryProjectionType]] = None, - session: Optional[ClientSession] = None, - ignore_cache: bool = False, - fetch_links: bool = False, - **pymongo_kwargs, - ) -> Union[ - "FindOne[FindQueryResultType]", "FindOne[FindQueryProjectionType]" - ]: - """ - Find one document by criteria - - :param args: *Mapping[str, Any] - search criteria - :param projection_model: Optional[Type[BaseModel]] - projection model - :param session: Optional[ClientSession] - pymongo session - :param ignore_cache: bool - :param **pymongo_kwargs: pymongo native parameters for find operation (if Document class contains links, this parameter must fit the respective parameter of the aggregate MongoDB function) - :return: FindOne - query instance - """ - self.find_expressions += args # type: ignore # bool workaround - self.project(projection_model) - self.set_session(session=session) - self.ignore_cache = ignore_cache - self.fetch_links = fetch_links - self.pymongo_kwargs.update(pymongo_kwargs) - return self - - def update_one( - self, - *args: Mapping[str, Any], - session: Optional[ClientSession] = None, - bulk_writer: Optional[BulkWriter] = None, - **pymongo_kwargs, - ) -> UpdateOne: - """ - Create [UpdateOne](https://roman-right.github.io/beanie/api/queries/#updateone) query using modifications and - provide search criteria there - :param args: *Mapping[str,Any] - the modifications to apply - :param session: Optional[ClientSession] - PyMongo sessions - :return: [UpdateOne](https://roman-right.github.io/beanie/api/queries/#updateone) query - """ - return cast( - UpdateOne, - self.update( - *args, - session=session, - bulk_writer=bulk_writer, - **pymongo_kwargs, - ), - ) - - def delete_one( - self, - session: Optional[ClientSession] = None, - bulk_writer: Optional[BulkWriter] = None, - **pymongo_kwargs, - ) -> DeleteOne: - """ - Provide search criteria to the [DeleteOne](https://roman-right.github.io/beanie/api/queries/#deleteone) query - :param session: Optional[ClientSession] - PyMongo sessions - :return: [DeleteOne](https://roman-right.github.io/beanie/api/queries/#deleteone) query - """ - # We need to cast here to tell mypy that we are sure about the type. - # This is because delete may also return a DeleteOne type in general, and mypy can not be sure in this case - # See https://mypy.readthedocs.io/en/stable/common_issues.html#narrowing-and-inner-functions - return cast( - DeleteOne, - self.delete( - session=session, bulk_writer=bulk_writer, **pymongo_kwargs - ), - ) - - def replace_one( - self, - document: "DocType", - session: Optional[ClientSession] = None, - bulk_writer: Optional[BulkWriter] = None, - ) -> Optional[UpdateResult]: - """ - Replace found document by provided - :param document: Document - document, which will replace the found one - :param session: Optional[ClientSession] - PyMongo session - :param bulk_writer: Optional[BulkWriter] - Beanie bulk writer - :return: UpdateResult - """ - self.set_session(session=session) - if bulk_writer is None: - result: UpdateResult = ( - self.document_model.get_motor_collection().replace_one( - self.get_filter_query(), - Encoder(by_alias=True, exclude={"_id"}).encode(document), - session=self.session, - ) - ) - - if not result.raw_result["updatedExisting"]: - raise DocumentNotFound - return result - else: - bulk_writer.add_operation( - Operation( - operation=ReplaceOne, - first_query=self.get_filter_query(), - second_query=Encoder( - by_alias=True, exclude={"_id"} - ).encode(document), - object_class=self.document_model, - ) - ) - return None - - def _find_one(self): - if self.fetch_links: - result = self.document_model.find( - *self.find_expressions, - session=self.session, - fetch_links=self.fetch_links, - **self.pymongo_kwargs, - ).to_list(length=1) - if result: - return result[0] - else: - return None - return self.document_model.get_motor_collection().find_one( - filter=self.get_filter_query(), - projection=get_projection(self.projection_model), - session=self.session, - **self.pymongo_kwargs, - ) - - def run( - self, - ) -> Optional[FindQueryResultType]: - """ - Run the query - :return: BaseModel - """ - if ( - self.document_model.get_settings().use_cache - and self.ignore_cache is False - ): - cache_key = LRUCache.create_key( - "FindOne", - self.get_filter_query(), - self.projection_model, - self.session, - self.fetch_links, - ) - document: Dict[str, Any] = self.document_model._cache.get( # type: ignore - cache_key - ) - if document is None: - document = self._find_one() # type: ignore - self.document_model._cache.set( # type: ignore - cache_key, document - ) - else: - document = self._find_one() # type: ignore - if document is None: - return None - return cast( - FindQueryResultType, parse_obj(self.projection_model, document) - ) diff --git a/beanie/sync/odm/queries/update.py b/beanie/sync/odm/queries/update.py deleted file mode 100644 index 316a14f0..00000000 --- a/beanie/sync/odm/queries/update.py +++ /dev/null @@ -1,238 +0,0 @@ -from abc import abstractmethod - - -from beanie.sync.odm.bulk import BulkWriter, Operation -from beanie.sync.odm.interfaces.run import RunInterface -from beanie.sync.odm.utils.encoder import Encoder -from typing import ( - Callable, - List, - Type, - TYPE_CHECKING, - Optional, - Mapping, - Any, - Dict, - Union, -) - -from pymongo.client_session import ClientSession -from pymongo.results import UpdateResult, InsertOneResult - -from beanie.sync.odm.interfaces.session import SessionMethods -from beanie.sync.odm.interfaces.update import ( - UpdateMethods, -) -from beanie.odm.operators.update import BaseUpdateOperator -from pymongo import UpdateOne as UpdateOnePyMongo -from pymongo import UpdateMany as UpdateManyPyMongo - -if TYPE_CHECKING: - from beanie.sync.odm.documents import DocType - - -class UpdateQuery(UpdateMethods, SessionMethods, RunInterface): - """ - Update Query base class - - Inherited from: - - - [SessionMethods](https://roman-right.github.io/beanie/api/interfaces/#sessionmethods) - - [UpdateMethods](https://roman-right.github.io/beanie/api/interfaces/#aggregatemethods) - """ - - def __init__( - self, - document_model: Type["DocType"], - find_query: Mapping[str, Any], - ): - self.document_model = document_model - self.find_query = find_query - self.update_expressions: List[Mapping[str, Any]] = [] - self.session = None - self.is_upsert = False - self.upsert_insert_doc: Optional["DocType"] = None - self.encoders: Dict[Any, Callable[[Any], Any]] = {} - self.bulk_writer: Optional[BulkWriter] = None - self.encoders = self.document_model.get_settings().bson_encoders - self.pymongo_kwargs: Dict[str, Any] = {} - - @property - def update_query(self) -> Dict[str, Any]: - query: Dict[str, Any] = {} - for expression in self.update_expressions: - if isinstance(expression, BaseUpdateOperator): - query.update(expression.query) - elif isinstance(expression, dict): - query.update(expression) - else: - raise TypeError("Wrong expression type") - return Encoder(custom_encoders=self.encoders).encode(query) - - def update( - self, - *args: Mapping[str, Any], - session: Optional[ClientSession] = None, - bulk_writer: Optional[BulkWriter] = None, - **pymongo_kwargs, - ) -> "UpdateQuery": - """ - Provide modifications to the update query. - - :param args: *Union[dict, Mapping] - the modifications to apply. - :param session: Optional[ClientSession] - :param bulk_writer: Optional[BulkWriter] - :param **pymongo_kwargs: pymongo native parameters for update operation - :return: UpdateMany query - """ - self.set_session(session=session) - self.update_expressions += args - if bulk_writer: - self.bulk_writer = bulk_writer - self.pymongo_kwargs.update(pymongo_kwargs) - return self - - def upsert( - self, - *args: Mapping[str, Any], - on_insert: "DocType", - session: Optional[ClientSession] = None, - bulk_writer: Optional[BulkWriter] = None, - **pymongo_kwargs, - ) -> "UpdateQuery": - """ - Provide modifications to the upsert query. - - :param args: *Union[dict, Mapping] - the modifications to apply. - :param on_insert: DocType - document to insert if there is no matched - document in the collection - :param session: Optional[ClientSession] - :param **pymongo_kwargs: pymongo native parameters for update operation - :return: UpdateMany query - """ - self.upsert_insert_doc = on_insert # type: ignore - self.update( - *args, session=session, bulk_writer=bulk_writer, **pymongo_kwargs - ) - return self - - @abstractmethod - def _update(self) -> UpdateResult: - ... - - def run( - self, - ) -> Union[UpdateResult, InsertOneResult]: - """ - Run the query - :return: - """ - - update_result = self._update() - if self.upsert_insert_doc is None: - return update_result - else: - if update_result is not None and update_result.matched_count == 0: - return self.document_model.insert_one( - document=self.upsert_insert_doc, - session=self.session, - bulk_writer=self.bulk_writer, - ) - else: - return update_result - - -class UpdateMany(UpdateQuery): - """ - Update Many query class - - Inherited from: - - - [UpdateQuery](https://roman-right.github.io/beanie/api/queries/#updatequery) - """ - - def update_many( - self, - *args: Mapping[str, Any], - session: Optional[ClientSession] = None, - bulk_writer: Optional[BulkWriter] = None, - **pymongo_kwargs, - ): - """ - Provide modifications to the update query - - :param args: *Union[dict, Mapping] - the modifications to apply. - :param session: Optional[ClientSession] - :param bulk_writer: "BulkWriter" - Beanie bulk writer - :param **pymongo_kwargs: pymongo native parameters for update operation - :return: UpdateMany query - """ - return self.update( - *args, session=session, bulk_writer=bulk_writer, **pymongo_kwargs - ) - - def _update(self): - if self.bulk_writer is None: - return self.document_model.get_motor_collection().update_many( - self.find_query, - self.update_query, - session=self.session, - **self.pymongo_kwargs, - ) - else: - self.bulk_writer.add_operation( - Operation( - operation=UpdateManyPyMongo, - first_query=self.find_query, - second_query=self.update_query, - object_class=self.document_model, - ) - ) - - -class UpdateOne(UpdateQuery): - """ - Update One query class - - Inherited from: - - - [UpdateQuery](https://roman-right.github.io/beanie/api/queries/#updatequery) - """ - - def update_one( - self, - *args: Mapping[str, Any], - session: Optional[ClientSession] = None, - bulk_writer: Optional[BulkWriter] = None, - **pymongo_kwargs, - ): - """ - Provide modifications to the update query. The same as `update()` - - :param args: *Union[dict, Mapping] - the modifications to apply. - :param session: Optional[ClientSession] - :param bulk_writer: "BulkWriter" - Beanie bulk writer - :param **pymongo_kwargs: pymongo native parameters for update operation - :return: UpdateMany query - """ - return self.update( - *args, session=session, bulk_writer=bulk_writer, **pymongo_kwargs - ) - - def _update(self): - if not self.bulk_writer: - return self.document_model.get_motor_collection().update_one( - self.find_query, - self.update_query, - session=self.session, - **self.pymongo_kwargs, - ) - else: - self.bulk_writer.add_operation( - Operation( - operation=UpdateOnePyMongo, - first_query=self.find_query, - second_query=self.update_query, - object_class=self.document_model, - ) - ) diff --git a/beanie/sync/odm/settings/__init__.py b/beanie/sync/odm/settings/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/beanie/sync/odm/settings/base.py b/beanie/sync/odm/settings/base.py deleted file mode 100644 index 09721452..00000000 --- a/beanie/sync/odm/settings/base.py +++ /dev/null @@ -1,24 +0,0 @@ -from datetime import timedelta -from typing import Optional, Dict, Any, Type - -from pydantic import BaseModel, Field -from pymongo.collection import Collection -from pymongo.database import Database - - -class ItemSettings(BaseModel): - name: Optional[str] - - use_cache: bool = False - cache_capacity: int = 32 - cache_expiration_time: timedelta = timedelta(minutes=10) - bson_encoders: Dict[Any, Any] = Field(default_factory=dict) - projection: Optional[Dict[str, Any]] = None - - motor_db: Optional[Database] - motor_collection: Optional[Collection] = None - - union_doc: Optional[Type] = None - - class Config: - arbitrary_types_allowed = True diff --git a/beanie/sync/odm/settings/document.py b/beanie/sync/odm/settings/document.py deleted file mode 100644 index cb9c6a75..00000000 --- a/beanie/sync/odm/settings/document.py +++ /dev/null @@ -1,144 +0,0 @@ -import warnings -from typing import Optional, Type, List - -from pydantic import Field -from pymongo import IndexModel -from pymongo.database import Database - -from beanie.exceptions import MongoDBVersionError -from beanie.sync.odm.settings.base import ItemSettings -from beanie.sync.odm.settings.timeseries import TimeSeriesConfig - - -class IndexModelField(IndexModel): - @classmethod - def __get_validators__(cls): - yield cls.validate - - @classmethod - def validate(cls, v): - if isinstance(v, IndexModel): - return v - else: - return IndexModel(v) - - -class DocumentSettings(ItemSettings): - use_state_management: bool = False - state_management_replace_objects: bool = False - validate_on_save: bool = False - use_revision: bool = False - - indexes: List[IndexModelField] = Field(default_factory=list) - timeseries: Optional[TimeSeriesConfig] = None - - @classmethod - def init( - cls, - database: Database, - document_model: Type, - allow_index_dropping: bool, - ) -> "DocumentSettings": - - settings_class = getattr(document_model, "Settings", None) - settings_vars = ( - {} if settings_class is None else dict(vars(settings_class)) - ) - - # deprecated Collection class support - - collection_class = getattr(document_model, "Collection", None) - - if collection_class is not None: - warnings.warn( - "Collection inner class is deprecated, use Settings instead", - DeprecationWarning, - ) - - collection_vars = ( - {} if collection_class is None else dict(vars(collection_class)) - ) - - settings_vars.update(collection_vars) - - # ------------------------------------ # - - document_settings = DocumentSettings.parse_obj(settings_vars) - - document_settings.motor_db = database - - # register in the Union Doc - - if document_settings.union_doc is not None: - document_settings.name = document_settings.union_doc.register_doc( - document_model - ) - - # set a name - - if not document_settings.name: - document_settings.name = document_model.__name__ - - # check mongodb version - build_info = database.command({"buildInfo": 1}) - mongo_version = build_info["version"] - major_version = int(mongo_version.split(".")[0]) - - if document_settings.timeseries is not None and major_version < 5: - raise MongoDBVersionError( - "Timeseries are supported by MongoDB version 5 and higher" - ) - - # create motor collection - if ( - document_settings.timeseries is not None - and document_settings.name not in database.list_collection_names() - ): - - collection = database.create_collection( - **document_settings.timeseries.build_query( - document_settings.name - ) - ) - else: - collection = database[document_settings.name] - - document_settings.motor_collection = collection - - # indexes - old_indexes = (collection.index_information()).keys() - new_indexes = ["_id_"] - - # Indexed field wrapped with Indexed() - found_indexes = [ - IndexModel( - [ - ( - fvalue.alias, - fvalue.type_._indexed[0], - ) - ], - **fvalue.type_._indexed[1] - ) - for _, fvalue in document_model.__fields__.items() - if hasattr(fvalue.type_, "_indexed") and fvalue.type_._indexed - ] - - # get indexes from the Collection class - if document_settings.indexes: - found_indexes += document_settings.indexes - - # create indices - if found_indexes: - new_indexes += collection.create_indexes(found_indexes) - - # delete indexes - # Only drop indexes if the user specifically allows for it - if allow_index_dropping: - for index in set(old_indexes) - set(new_indexes): - collection.drop_index(index) - - return document_settings - - class Config: - arbitrary_types_allowed = True diff --git a/beanie/sync/odm/settings/timeseries.py b/beanie/sync/odm/settings/timeseries.py deleted file mode 100644 index f7c72cfb..00000000 --- a/beanie/sync/odm/settings/timeseries.py +++ /dev/null @@ -1,37 +0,0 @@ -from enum import Enum -from typing import Optional, Dict, Any - -from pydantic import BaseModel - - -class Granularity(str, Enum): - """ - Time Series Granuality - """ - - seconds = "seconds" - minutes = "minutes" - hours = "hours" - - -class TimeSeriesConfig(BaseModel): - """ - Time Series Collection config - """ - - time_field: str - meta_field: Optional[str] - granularity: Optional[Granularity] - expire_after_seconds: Optional[float] - - def build_query(self, collection_name: str) -> Dict[str, Any]: - res: Dict[str, Any] = {"name": collection_name} - timeseries = {"timeField": self.time_field} - if self.meta_field is not None: - timeseries["metaField"] = self.meta_field - if self.granularity is not None: - timeseries["granularity"] = self.granularity - res["timeseries"] = timeseries - if self.expire_after_seconds is not None: - res["expireAfterSeconds"] = self.expire_after_seconds - return res diff --git a/beanie/sync/odm/settings/union_doc.py b/beanie/sync/odm/settings/union_doc.py deleted file mode 100644 index 0c1ba9f9..00000000 --- a/beanie/sync/odm/settings/union_doc.py +++ /dev/null @@ -1,21 +0,0 @@ -from typing import Type - -from pymongo.database import Database - -from beanie.sync.odm.settings.base import ItemSettings - - -class UnionDocSettings(ItemSettings): - @classmethod - def init(cls, doc_class: Type, database: Database) -> "UnionDocSettings": - settings_class = getattr(doc_class, "Settings", None) - - multi_doc_settings = cls.parse_obj(vars(settings_class)) - - if multi_doc_settings.name is None: - multi_doc_settings.name = doc_class.__name__ - - multi_doc_settings.motor_db = database - multi_doc_settings.motor_collection = database[multi_doc_settings.name] - - return multi_doc_settings diff --git a/beanie/sync/odm/settings/view.py b/beanie/sync/odm/settings/view.py deleted file mode 100644 index fc61f43d..00000000 --- a/beanie/sync/odm/settings/view.py +++ /dev/null @@ -1,31 +0,0 @@ -from inspect import isclass -from typing import List, Dict, Any, Union, Type - -from pymongo.database import Database - -from beanie.exceptions import ViewHasNoSettings -from beanie.sync.odm.settings.base import ItemSettings - - -class ViewSettings(ItemSettings): - source: Union[str, Type] - pipeline: List[Dict[str, Any]] - - @classmethod - def init(cls, view_class: Type, database: Database) -> "ViewSettings": - settings_class = getattr(view_class, "Settings", None) - if settings_class is None: - raise ViewHasNoSettings("View must have Settings inner class") - - view_settings = cls.parse_obj(vars(settings_class)) - - if view_settings.name is None: - view_settings.name = view_class.__name__ - - if isclass(view_settings.source): - view_settings.source = view_settings.source.get_collection_name() - - view_settings.motor_db = database - view_settings.motor_collection = database[view_settings.name] - - return view_settings diff --git a/beanie/sync/odm/union_doc.py b/beanie/sync/odm/union_doc.py deleted file mode 100644 index a59508a1..00000000 --- a/beanie/sync/odm/union_doc.py +++ /dev/null @@ -1,45 +0,0 @@ -from typing import ClassVar, Type, Dict, Optional - -from pymongo.database import Database - -from beanie.exceptions import UnionDocNotInited -from beanie.sync.odm.interfaces.aggregate import AggregateInterface -from beanie.sync.odm.interfaces.detector import DetectionInterface, ModelType -from beanie.sync.odm.interfaces.find import FindInterface -from beanie.sync.odm.interfaces.getters import OtherGettersInterface -from beanie.sync.odm.settings.union_doc import UnionDocSettings - - -class UnionDoc( - FindInterface, - AggregateInterface, - OtherGettersInterface, - DetectionInterface, -): - _document_models: ClassVar[Optional[Dict[str, Type]]] = None - _is_inited: ClassVar[bool] = False - _settings: ClassVar[UnionDocSettings] - - @classmethod - def get_settings(cls) -> UnionDocSettings: - return cls._settings - - @classmethod - def init(cls, database: Database): - cls._settings = UnionDocSettings.init(database=database, doc_class=cls) - cls._is_inited = True - - @classmethod - def register_doc(cls, doc_model: Type): - if cls._document_models is None: - cls._document_models = {} - - if cls._is_inited is False: - raise UnionDocNotInited - - cls._document_models[doc_model.__name__] = doc_model - return cls.get_settings().name - - @classmethod - def get_model_type(cls) -> ModelType: - return ModelType.UnionDoc diff --git a/beanie/sync/odm/utils/__init__.py b/beanie/sync/odm/utils/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/beanie/sync/odm/utils/dump.py b/beanie/sync/odm/utils/dump.py deleted file mode 100644 index dcb36bcb..00000000 --- a/beanie/sync/odm/utils/dump.py +++ /dev/null @@ -1,17 +0,0 @@ -from typing import TYPE_CHECKING - -from beanie.sync.odm.utils.encoder import Encoder - -if TYPE_CHECKING: - from beanie.sync.odm.documents import Document - - -def get_dict(document: "Document", to_db: bool = False): - exclude = set() - if document.id is None: - exclude.add("_id") - if not document.get_settings().use_revision: - exclude.add("revision_id") - return Encoder(by_alias=True, exclude=exclude, to_db=to_db).encode( - document - ) diff --git a/beanie/sync/odm/utils/encoder.py b/beanie/sync/odm/utils/encoder.py deleted file mode 100644 index 16090dc7..00000000 --- a/beanie/sync/odm/utils/encoder.py +++ /dev/null @@ -1,181 +0,0 @@ -from collections import deque -from datetime import datetime, timedelta -from decimal import Decimal -from enum import Enum -from ipaddress import ( - IPv4Address, - IPv4Interface, - IPv4Network, - IPv6Address, - IPv6Interface, - IPv6Network, -) -from pathlib import PurePath -from types import GeneratorType -from typing import ( - AbstractSet, - List, - Mapping, - Union, -) -from typing import Any, Callable, Dict, Type -from uuid import UUID - -import bson -from bson import ObjectId, DBRef, Binary, Decimal128 -from pydantic import BaseModel -from pydantic import SecretBytes, SecretStr -from pydantic.color import Color - -from beanie.odm.fields import Link, LinkTypes -from beanie.sync.odm import documents - -ENCODERS_BY_TYPE: Dict[Type[Any], Callable[[Any], Any]] = { - Color: str, - timedelta: lambda td: td.total_seconds(), - Decimal: Decimal128, - deque: list, - IPv4Address: str, - IPv4Interface: str, - IPv4Network: str, - IPv6Address: str, - IPv6Interface: str, - IPv6Network: str, - SecretBytes: SecretBytes.get_secret_value, - SecretStr: SecretStr.get_secret_value, - Enum: lambda o: o.value, - PurePath: str, - Link: lambda l: l.ref, - bytes: lambda b: b if isinstance(b, Binary) else Binary(b), - UUID: lambda u: bson.Binary.from_uuid(u), -} - - -class Encoder: - """ - BSON encoding class - """ - - def __init__( - self, - exclude: Union[ - AbstractSet[Union[str, int]], Mapping[Union[str, int], Any], None - ] = None, - custom_encoders: Dict[Type, Callable] = None, - by_alias: bool = True, - to_db: bool = False, - ): - self.exclude = exclude or {} - self.by_alias = by_alias - self.custom_encoders = custom_encoders or {} - self.to_db = to_db - - def encode(self, obj: Any): - """ - Run the encoder - """ - return self._encode(obj=obj) - - def encode_document(self, obj): - """ - Beanie Document class case - """ - encoder = Encoder( - custom_encoders=obj.get_settings().bson_encoders, - by_alias=self.by_alias, - to_db=self.to_db, - ) - - link_fields = obj.get_link_fields() - obj_dict: Dict[str, Any] = {} - if obj.get_settings().union_doc is not None: - obj_dict["_class_id"] = obj.__class__.__name__ - for k, o in obj._iter(to_dict=False, by_alias=self.by_alias): - if k not in self.exclude: - if link_fields and k in link_fields: - if link_fields[k].link_type == LinkTypes.LIST: - obj_dict[k] = [link.to_ref() for link in o] - if link_fields[k].link_type == LinkTypes.DIRECT: - obj_dict[k] = o.to_ref() - if link_fields[k].link_type == LinkTypes.OPTIONAL_DIRECT: - if o is not None: - obj_dict[k] = o.to_ref() - else: - obj_dict[k] = o - if link_fields[k].link_type == LinkTypes.OPTIONAL_LIST: - if o is not None: - obj_dict[k] = [link.to_ref() for link in o] - else: - obj_dict[k] = o - else: - obj_dict[k] = o - obj_dict[k] = encoder.encode(obj_dict[k]) - return obj_dict - - def encode_base_model(self, obj): - """ - BaseModel case - """ - obj_dict = {} - for k, o in obj._iter(to_dict=False, by_alias=self.by_alias): - if k not in self.exclude: - obj_dict[k] = self._encode(o) - - return obj_dict - - def encode_dict(self, obj): - """ - Dictionary case - """ - for key, value in obj.items(): - obj[key] = self._encode(value) - return obj - - def encode_iterable(self, obj): - """ - Iterable case - """ - return [self._encode(item) for item in obj] - - def _encode( - self, - obj, - ) -> Any: - """""" - if self.custom_encoders: - if type(obj) in self.custom_encoders: - return self.custom_encoders[type(obj)](obj) - for encoder_type, encoder in self.custom_encoders.items(): - if isinstance(obj, encoder_type): - return encoder(obj) - if type(obj) in ENCODERS_BY_TYPE: - return ENCODERS_BY_TYPE[type(obj)](obj) - for cls, encoder in ENCODERS_BY_TYPE.items(): - if isinstance(obj, cls): - return encoder(obj) - - if isinstance(obj, documents.Document): - return self.encode_document(obj) - if isinstance(obj, BaseModel): - return self.encode_base_model(obj) - if isinstance(obj, dict): - return self.encode_dict(obj) - if isinstance(obj, (list, set, frozenset, GeneratorType, tuple)): - return self.encode_iterable(obj) - - if isinstance( - obj, (str, int, float, ObjectId, datetime, type(None), DBRef) - ): - return obj - - errors: List[Exception] = [] - try: - data = dict(obj) - except Exception as e: - errors.append(e) - try: - data = vars(obj) - except Exception as e: - errors.append(e) - raise ValueError(errors) - return self._encode(data) diff --git a/beanie/sync/odm/utils/find.py b/beanie/sync/odm/utils/find.py deleted file mode 100644 index dda884ec..00000000 --- a/beanie/sync/odm/utils/find.py +++ /dev/null @@ -1,65 +0,0 @@ -from beanie.exceptions import NotSupported -from beanie.odm.fields import LinkTypes -from typing import TYPE_CHECKING, List, Dict, Any, Type - -from beanie.sync.odm.interfaces.detector import ModelType - -if TYPE_CHECKING: - from beanie import Document - - -def construct_lookup_queries(cls: Type["Document"]) -> List[Dict[str, Any]]: - if cls.get_model_type() == ModelType.UnionDoc: - raise NotSupported("UnionDoc doesn't support link fetching") - queries = [] - link_fields = cls.get_link_fields() - if link_fields is not None: - for link_info in link_fields.values(): - if link_info.link_type in [ - LinkTypes.DIRECT, - LinkTypes.OPTIONAL_DIRECT, - ]: - queries += [ - { - "$lookup": { - "from": link_info.model_class.get_motor_collection().name, # type: ignore - "localField": f"{link_info.field}.$id", - "foreignField": "_id", - "as": f"_link_{link_info.field}", - } - }, - { - "$unwind": { - "path": f"$_link_{link_info.field}", - "preserveNullAndEmptyArrays": True, - } - }, - { - "$set": { - link_info.field: { - "$cond": { - "if": { - "$ifNull": [ - f"$_link_{link_info.field}", - False, - ] - }, - "then": f"$_link_{link_info.field}", - "else": f"${link_info.field}", - } - } - } - }, - ] # type: ignore - else: - queries.append( - { - "$lookup": { - "from": link_info.model_class.get_motor_collection().name, # type: ignore - "localField": f"{link_info.field}.$id", - "foreignField": "_id", - "as": link_info.field, - } - } - ) - return queries diff --git a/beanie/sync/odm/utils/general.py b/beanie/sync/odm/utils/general.py deleted file mode 100644 index 07d70c31..00000000 --- a/beanie/sync/odm/utils/general.py +++ /dev/null @@ -1,94 +0,0 @@ -import importlib -from typing import List, Type, Union, TYPE_CHECKING - -from pymongo import MongoClient -from pymongo.database import Database -from yarl import URL - -from beanie.sync.odm.interfaces.detector import ModelType - -if TYPE_CHECKING: - from beanie.sync.odm.documents import DocType - from beanie.sync.odm.views import View - - -def get_model(dot_path: str) -> Type["DocType"]: - """ - Get the model by the path in format bar.foo.Model - - :param dot_path: str - dot seprated path to the model - :return: Type[DocType] - class of the model - """ - module_name, class_name = None, None - try: - module_name, class_name = dot_path.rsplit(".", 1) - return getattr(importlib.import_module(module_name), class_name) - - except ValueError: - raise ValueError( - f"'{dot_path}' doesn't have '.' path, eg. path.to.your.model.class" - ) - - except AttributeError: - raise AttributeError( - f"module '{module_name}' has no class called '{class_name}'" - ) - - -def init_beanie( - database: Database = None, - connection_string: str = None, - document_models: List[Union[Type["DocType"], Type["View"], str]] = None, - allow_index_dropping: bool = False, - recreate_views: bool = False, -): - """ - Beanie initialization - - :param database: Database - pymongo database instance - :param connection_string: str - MongoDB connection string - :param document_models: List[Union[Type[DocType], str]] - model classes - or strings with dot separated paths - :param allow_index_dropping: bool - if index dropping is allowed. - Default False - :return: None - """ - if (connection_string is None and database is None) or ( - connection_string is not None and database is not None - ): - raise ValueError( - "connection_string parameter or database parameter must be set" - ) - - if document_models is None: - raise ValueError("document_models parameter must be set") - if connection_string is not None: - database = MongoClient(connection_string)[ - URL(connection_string).path[1:] - ] - - sort_order = { - ModelType.UnionDoc: 0, - ModelType.Document: 1, - ModelType.View: 2, - } - - document_models_unwrapped: List[Union[Type[DocType], Type[View]]] = [ - get_model(model) if isinstance(model, str) else model - for model in document_models - ] - - document_models_unwrapped.sort( - key=lambda val: sort_order[val.get_model_type()] - ) - - for model in document_models_unwrapped: - if model.get_model_type() == ModelType.UnionDoc: - model.init(database) - - if model.get_model_type() == ModelType.Document: - model.init_model( - database, allow_index_dropping=allow_index_dropping - ) - if model.get_model_type() == ModelType.View: - model.init_view(database, recreate_view=recreate_views) diff --git a/beanie/sync/odm/utils/parsing.py b/beanie/sync/odm/utils/parsing.py deleted file mode 100644 index 7ba5e6da..00000000 --- a/beanie/sync/odm/utils/parsing.py +++ /dev/null @@ -1,36 +0,0 @@ -from typing import Any, Type, Union, TYPE_CHECKING - -from pydantic import BaseModel - -from beanie.exceptions import ( - UnionHasNoRegisteredDocs, - DocWasNotRegisteredInUnionClass, -) -from beanie.sync.odm.interfaces.detector import ModelType - -if TYPE_CHECKING: - from beanie.sync.odm.documents import Document - - -def parse_obj( - model: Union[Type[BaseModel], Type["Document"]], data: Any -) -> BaseModel: - if ( - hasattr(model, "get_model_type") - and model.get_model_type() == ModelType.UnionDoc - ): - if model._document_models is None: - raise UnionHasNoRegisteredDocs - - if isinstance(data, dict): - class_name = data["_class_id"] - else: - class_name = data._class_id - - if class_name not in model._document_models: - raise DocWasNotRegisteredInUnionClass - return parse_obj(model=model._document_models[class_name], data=data) - - # if hasattr(model, "_parse_obj_saving_state"): - # return model._parse_obj_saving_state(data) # type: ignore - return model.parse_obj(data) diff --git a/beanie/sync/odm/utils/projection.py b/beanie/sync/odm/utils/projection.py deleted file mode 100644 index f4c03fc3..00000000 --- a/beanie/sync/odm/utils/projection.py +++ /dev/null @@ -1,30 +0,0 @@ -from typing import Dict, Type, TypeVar, Optional - -from pydantic import BaseModel - -from beanie.sync.odm.interfaces.detector import ModelType - -ProjectionModelType = TypeVar("ProjectionModelType", bound=BaseModel) - - -def get_projection( - model: Type[ProjectionModelType], -) -> Optional[Dict[str, int]]: - if ( - hasattr(model, "get_model_type") - and model.get_model_type() == ModelType.UnionDoc - ): - return None - if hasattr(model, "Settings"): # MyPy checks - settings = getattr(model, "Settings") - if hasattr(settings, "projection"): - return getattr(settings, "projection") - - if getattr(model.Config, "extra", None) == "allow": - return None - - document_projection: Dict[str, int] = {} - - for name, field in model.__fields__.items(): - document_projection[field.alias] = 1 - return document_projection diff --git a/beanie/sync/odm/utils/relations.py b/beanie/sync/odm/utils/relations.py deleted file mode 100644 index 79e33000..00000000 --- a/beanie/sync/odm/utils/relations.py +++ /dev/null @@ -1,82 +0,0 @@ -import inspect -from typing import Optional, Any, Dict - -from pydantic.fields import ModelField -from pydantic.typing import get_origin - -from beanie.odm.fields import LinkTypes, LinkInfo, ExpressionField - -from typing import TYPE_CHECKING - -from beanie.sync.odm import Link - -if TYPE_CHECKING: - from beanie import Document - - -def detect_link(field: ModelField) -> Optional[LinkInfo]: - """ - It detects link and returns LinkInfo if any found. - - :param field: ModelField - :return: Optional[LinkInfo] - """ - if field.type_ == Link: - if field.allow_none is True: - return LinkInfo( - field=field.name, - model_class=field.sub_fields[0].type_, # type: ignore - link_type=LinkTypes.OPTIONAL_DIRECT, - ) - return LinkInfo( - field=field.name, - model_class=field.sub_fields[0].type_, # type: ignore - link_type=LinkTypes.DIRECT, - ) - if ( - inspect.isclass(get_origin(field.outer_type_)) - and issubclass(get_origin(field.outer_type_), list) # type: ignore - and len(field.sub_fields) == 1 # type: ignore - ): - internal_field = field.sub_fields[0] # type: ignore - if internal_field.type_ == Link: - if field.allow_none is True: - return LinkInfo( - field=field.name, - model_class=internal_field.sub_fields[0].type_, # type: ignore - link_type=LinkTypes.OPTIONAL_LIST, - ) - return LinkInfo( - field=field.name, - model_class=internal_field.sub_fields[0].type_, # type: ignore - link_type=LinkTypes.LIST, - ) - return None - - -def convert_ids( - query: Dict[str, Any], doc: "Document", fetch_links: bool -) -> Dict[str, Any]: - # TODO add all the cases - new_query = {} - for k, v in query.items(): - if ( - isinstance(k, ExpressionField) - and doc.get_link_fields() is not None - and k.split(".")[0] in doc.get_link_fields().keys() # type: ignore - and k.split(".")[1] == "id" - ): - if fetch_links: - new_k = f"{k.split('.')[0]}._id" - else: - new_k = f"{k.split('.')[0]}.$id" - else: - new_k = k - - if isinstance(v, dict): - new_v = convert_ids(v, doc, fetch_links) - else: - new_v = v - - new_query[new_k] = new_v - return new_query diff --git a/beanie/sync/odm/utils/self_validation.py b/beanie/sync/odm/utils/self_validation.py deleted file mode 100644 index d71b95d5..00000000 --- a/beanie/sync/odm/utils/self_validation.py +++ /dev/null @@ -1,14 +0,0 @@ -from functools import wraps -from typing import Callable, TYPE_CHECKING - -if TYPE_CHECKING: - from beanie.sync.odm.documents import DocType - - -def validate_self_before(f: Callable): - @wraps(f) - def wrapper(self: "DocType", *args, **kwargs): - self.validate_self(*args, **kwargs) - return f(self, *args, **kwargs) - - return wrapper diff --git a/beanie/sync/odm/utils/state.py b/beanie/sync/odm/utils/state.py deleted file mode 100644 index 5ac2f7c7..00000000 --- a/beanie/sync/odm/utils/state.py +++ /dev/null @@ -1,45 +0,0 @@ -from functools import wraps -from typing import Callable, TYPE_CHECKING - -from beanie.exceptions import StateManagementIsTurnedOff, StateNotSaved - -if TYPE_CHECKING: - from beanie.sync.odm.documents import DocType - - -def check_if_state_saved(self: "DocType"): - if not self.use_state_management(): - raise StateManagementIsTurnedOff( - "State management is turned off for this document" - ) - if self._saved_state is None: - raise StateNotSaved("No state was saved") - - -def saved_state_needed(f: Callable): - @wraps(f) - def sync_wrapper(self: "DocType", *args, **kwargs): - check_if_state_saved(self) - return f(self, *args, **kwargs) - - return sync_wrapper - - -def save_state_after(f: Callable): - @wraps(f) - def wrapper(self: "DocType", *args, **kwargs): - result = f(self, *args, **kwargs) - self._save_state() - return result - - return wrapper - - -def swap_revision_after(f: Callable): - @wraps(f) - def wrapper(self: "DocType", *args, **kwargs): - result = f(self, *args, **kwargs) - self._swap_revision() - return result - - return wrapper diff --git a/beanie/sync/odm/views.py b/beanie/sync/odm/views.py deleted file mode 100644 index 5e755c98..00000000 --- a/beanie/sync/odm/views.py +++ /dev/null @@ -1,78 +0,0 @@ -from typing import ClassVar - -from pydantic import BaseModel -from pymongo.database import Database - -from beanie.exceptions import ViewWasNotInitialized -from beanie.odm.fields import ExpressionField -from beanie.sync.odm.interfaces.aggregate import AggregateInterface -from beanie.sync.odm.interfaces.detector import DetectionInterface, ModelType -from beanie.sync.odm.interfaces.find import FindInterface -from beanie.sync.odm.interfaces.getters import OtherGettersInterface -from beanie.sync.odm.settings.view import ViewSettings - - -class View( - BaseModel, - FindInterface, - AggregateInterface, - OtherGettersInterface, - DetectionInterface, -): - """ - What is needed: - - Source collection or view - pipeline - - """ - - _settings: ClassVar[ViewSettings] - - @classmethod - def init_view(cls, database, recreate_view: bool): - cls.init_settings(database) - cls.init_fields() - - collection_names = database.list_collection_names() - if recreate_view or cls._settings.name not in collection_names: - if cls._settings.name in collection_names: - cls.get_motor_collection().drop() - - database.command( - { - "create": cls.get_settings().name, - "viewOn": cls.get_settings().source, - "pipeline": cls.get_settings().pipeline, - } - ) - - @classmethod - def init_settings(cls, database: Database) -> None: - cls._settings = ViewSettings.init(database=database, view_class=cls) - - @classmethod - def init_fields(cls) -> None: - """ - Init class fields - :return: None - """ - for k, v in cls.__fields__.items(): - path = v.alias or v.name - setattr(cls, k, ExpressionField(path)) - - @classmethod - def get_settings(cls) -> ViewSettings: - """ - Get view settings, which was created on - the initialization step - - :return: ViewSettings class - """ - if cls._settings is None: - raise ViewWasNotInitialized - return cls._settings - - @classmethod - def get_model_type(cls) -> ModelType: - return ModelType.View diff --git a/docs/changelog.md b/docs/changelog.md index 37c9dba7..dad2014c 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,6 +2,20 @@ Beanie project +## [1.15.0] - 2022-11-05 + +### Feature + +- The sync version was moved to a separate project + +### Breaking change + +- There is no sync version here more. Please use [Bunnet](https://github.com/roman-right/bunnet) instead + +### Implementation + +- PR + ## [1.14.0] - 2022-11-04 ### Feature @@ -1039,4 +1053,6 @@ how specific type should be presented in the database [1.13.1]: https://pypi.org/project/beanie/1.13.1 -[1.14.0]: https://pypi.org/project/beanie/1.14.0 \ No newline at end of file +[1.14.0]: https://pypi.org/project/beanie/1.14.0 + +[1.15.0]: https://pypi.org/project/beanie/1.15.0 \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index f6bac665..6c193630 100644 --- a/docs/index.md +++ b/docs/index.md @@ -5,7 +5,7 @@ ## Overview -[Beanie](https://github.com/roman-right/beanie) - is a Python object-document mapper (ODM) for MongoDB that can be used, and in synchronous, and async contexts. Data models are based on [Pydantic](https://pydantic-docs.helpmanual.io/). +[Beanie](https://github.com/roman-right/beanie) - is an asynchronous Python object-document mapper (ODM) for MongoDB that can be used, and in synchronous, and async contexts. Data models are based on [Pydantic](https://pydantic-docs.helpmanual.io/). When using Beanie each database collection has a corresponding `Document` that is used to interact with that collection. In addition to retrieving data, @@ -17,6 +17,8 @@ the parts of your app that actually matter. Data and schema migrations are supported by Beanie out of the box. +There is a synchronous version of Beanie ODM - [Bunnet](https://github.com/roman-right/bunnet) + ## Installation ### PIP diff --git a/docs/sync_tutorial/actions.md b/docs/sync_tutorial/actions.md deleted file mode 100644 index d100bcb5..00000000 --- a/docs/sync_tutorial/actions.md +++ /dev/null @@ -1,99 +0,0 @@ -# Event-based actions - -You can register methods as pre- or post- actions for document events. - -Currently supported events: - -- Insert -- Replace -- Update -- SaveChanges -- Delete -- ValidateOnSave - -Currently supported directions: - -- `Before` -- `After` - -Current operations creating events: - -- `insert()` for Insert -- `replace()` for Replace -- `save()` triggers Insert if it is creating a new document, triggers Replace if it replaces an existing document -- `save_changes()` for SaveChanges -- `insert()`, `replace()`, `save_changes()`, and `save()` for ValidateOnSave -- `set()`, `update()` for Update -- `delete()` for Delete - -To register an action, you can use `@before_event` and `@after_event` decorators respectively: - -```python -from beanie.sync import Document, before_event, after_event, Insert, Replace - - -class Sample(Document): - num: int - name: str - - @before_event(Insert) - def capitalize_name(self): - self.name = self.name.capitalize() - - @after_event(Replace) - def num_change(self): - self.num -= 1 -``` - -It is possible to register action for several events: - -```python -from beanie.sync import Document, before_event, Insert, Replace - - -class Sample(Document): - num: int - name: str - - @before_event(Insert, Replace) - def capitalize_name(self): - self.name = self.name.capitalize() -``` - -This will capitalize the `name` field value before each document's Insert and Replace. - -Actions can be selectively skipped by passing the `skip_actions` argument when calling -the operations that trigger events. `skip_actions` accepts a list of directions and action names. - -```python -from beanie.sync import Document, before_event, after_event, After, Before, Insert, Replace - - -class Sample(Document): - num: int - name: str - - @before_event(Insert) - def capitalize_name(self): - self.name = self.name.capitalize() - - @before_event(Replace) - def redact_name(self): - self.name = "[REDACTED]" - - @after_event(Replace) - def num_change(self): - self.num -= 1 - - -sample = Sample() - -# capitalize_name will not be executed -sample.insert(skip_actions=['capitalize_name']) - -# num_change will not be executed -sample.replace(skip_actions=[After]) - -# redact_name and num_change will not be executed -sample.replace(skip_actions=[Before, 'num_change']) -``` diff --git a/docs/sync_tutorial/aggregate.md b/docs/sync_tutorial/aggregate.md deleted file mode 100644 index f8a54f29..00000000 --- a/docs/sync_tutorial/aggregate.md +++ /dev/null @@ -1,33 +0,0 @@ -# Aggregations - -You can perform aggregation queries through beanie as well. For example, to calculate the average: - -```python -# With a search: -avg_price = Product.find( - Product.category.name == "Chocolate" -).avg(Product.price) - -# Over the whole collection: -avg_price = Product.avg(Product.price) -``` - -A full list of available methods can be found [here](/api-documentation/interfaces/#aggregatemethods). - -You can also use the native PyMongo syntax by calling the `aggregate` method. -However, as Beanie will not know what output to expect, you will have to supply a projection model yourself. -If you do not supply a projection model, then a dictionary will be returned. - -```python -class OutputItem(BaseModel): - id: str = Field(None, alias="_id") - total: float - - -result = Product.find( - Product.category.name == "Chocolate").aggregate( - [{"$group": {"_id": "$category.name", "total": {"$avg": "$price"}}}], - projection_model=OutputItem -).to_list() - -``` diff --git a/docs/sync_tutorial/cache.md b/docs/sync_tutorial/cache.md deleted file mode 100644 index c6c5aa7b..00000000 --- a/docs/sync_tutorial/cache.md +++ /dev/null @@ -1,46 +0,0 @@ -# Cache -All query results could be locally cached. - -This feature must be explicitly turned on in the `Settings` inner class. - -```python -from beanie.sync import Document - -class Sample(Document): - num: int - name: str - - class Settings: - use_cache = True -``` - -Beanie uses LRU cache with expiration time. -You can set `capacity` (the maximum number of the cached queries) and expiration time in the `Settings` inner class. - -```python -from beanie.sync import Document - -class Sample(Document): - num: int - name: str - - class Settings: - use_cache = True - cache_expiration_time = datetime.timedelta(seconds=10) - cache_capacity = 5 -``` - -Any query will be cached for this document class. - -```python -# on the first call it will go to the database -samples = Sample.find(num>10).to_list() - -# on the second - it will use cache instead -samples = Sample.find(num>10).to_list() - -sleep(15) - -# if the expiration time was reached it will go to the database again -samples = Sample.find(num>10).to_list() -``` \ No newline at end of file diff --git a/docs/sync_tutorial/collection_setup.md b/docs/sync_tutorial/collection_setup.md deleted file mode 100644 index 9e9859e5..00000000 --- a/docs/sync_tutorial/collection_setup.md +++ /dev/null @@ -1,135 +0,0 @@ -# Collection setup (name, indexes, timeseries) - -Although basic pydantic syntax allows you to set all aspects of individual fields, -there is also some need to configure collections as a whole. -In particular, you might want to: - -- Set the MongoDB collection name -- Configure indexes - -This is done by defining a `Settings` class within your `Document` class. - -## Declaring the collection name - -To set MongoDB collection name, you can use the `name` field of the `Settings` inner class. - -```python -from beanie.sync import Document - - -class Sample(Document): - num: int - description: str - - class Settings: - name = "samples" -``` - -## Indexes - -### Indexed function - -To set up an index over a single field, the `Indexed` function can be used to wrap the type -and does not require a `Settings` class: - -```python -from beanie.sync import Document -from beanie import Indexed - - -class Sample(Document): - num: Indexed(int) - description: str -``` - -The `Indexed` function takes an optional `index_type` argument, which may be set to a pymongo index type: - -```python -import pymongo - -from beanie.sync import Document -from beanie import Indexed - - -class Sample(Document): - description: Indexed(str, index_type=pymongo.TEXT) -``` - -The `Indexed` function also supports PyMongo's `IndexModel` kwargs arguments (see the [PyMongo Documentation](https://pymongo.readthedocs.io/en/stable/api/pymongo/operations.html#pymongo.operations.IndexModel) for details). - -For example, to create a `unique` index: - -```python -from beanie.sync import Document -from beanie import Indexed - - -class Sample(Document): - name: Indexed(str, unique=True) -``` - -### Multi-field indexes - -The `indexes` field of the inner `Settings` class is responsible for more complex indexes. -It is a list where items can be: - -- Single key. Name of the document's field (this is equivalent to using the Indexed function described above without any additional arguments) -- List of (key, direction) pairs. Key - string, name of the document's field. Direction - pymongo direction ( - example: `pymongo.ASCENDING`) -- `pymongo.IndexModel` instance - the most flexible - option. [PyMongo Documentation](https://pymongo.readthedocs.io/en/stable/api/pymongo/operations.html#pymongo.operations.IndexModel) - -```python -import pymongo -from pymongo import IndexModel - -from beanie.sync import Document - - -class Sample(Document): - test_int: int - test_str: str - - class Settings: - indexes = [ - "test_int", - [ - ("test_int", pymongo.ASCENDING), - ("test_str", pymongo.DESCENDING), - ], - IndexModel( - [("test_str", pymongo.DESCENDING)], - name="test_string_index_DESCENDING", - ), - ] -``` - -## Time series - -You can set up a timeseries collection using the inner `Settings` class. - -**Be aware, timeseries collections a supported by MongoDB 5.0 and higher only.** - -```python -from datetime import datetime - -from beanie.sync import Document, TimeSeriesConfig, Granularity -from pydantic import Field - - -class Sample(Document): - ts: datetime = Field(default_factory=datetime.now) - meta: str - - class Settings: - timeseries = TimeSeriesConfig( - time_field="ts", # Required - meta_field="meta", # Optional - granularity=Granularity.hours, # Optional - expire_after_seconds=2 # Optional - ) -``` - -TimeSeriesConfig fields reflect the respective parameters of the MongoDB timeseries creation function. - -MongoDB documentation: https://docs.mongodb.com/manual/core/timeseries-collections/ diff --git a/docs/sync_tutorial/defining-a-document.md b/docs/sync_tutorial/defining-a-document.md deleted file mode 100644 index 4a201069..00000000 --- a/docs/sync_tutorial/defining-a-document.md +++ /dev/null @@ -1,238 +0,0 @@ -# Defining a document - -The `Document` class in Beanie is responsible for mapping and handling the data -from the collection. It is inherited from the `BaseModel` Pydantic class, so it -follows the same data typing and parsing behavior. - -```python -from typing import Optional - -import pymongo -from pydantic import BaseModel - -from beanie.sync import Document -from beanie import Indexed - - -class Category(BaseModel): - name: str - description: str - - -class Product(Document): # This is the model - name: str - description: Optional[str] = None - price: Indexed(float, pymongo.DESCENDING) - category: Category - - class Settings: - name = "products" - indexes = [ - [ - ("name", pymongo.TEXT), - ("description", pymongo.TEXT), - ], - ] - -``` - -## Fields - -As it was mentioned before, the `Document` class is inherited from the Pydantic `BaseModel` class. -It uses all the same patterns of `BaseModel`. But also it has special types of fields: - -- id -- Indexed - -### id - -`id` field of the `Document` class reflects the unique `_id` field of the MongoDB document. -Each object of the `Document` type has this field. -The default type of this is [PydanticObjectId](/api-documentation/fields/#pydanticobjectid). - -```python -from beanie.sync import Document - -class Sample(Document): - num: int - description: str - -foo = Sample.find_one(Sample.num > 5).run() - -print(foo.id) # This will print id - -bar = Sample.get(foo.id).run() # get by id -``` - -If you prefer another type, you can set it up too. For example, UUID: - -```python -from uuid import UUID, uuid4 - -from pydantic import Field - -from beanie.sync import Document - - -class Sample(Document): - id: UUID = Field(default_factory=uuid4) - num: int - description: str -``` - -### Indexed - -To set up an index over a single field, the `Indexed` function can be used to wrap the type: - -```python -from beanie import Indexed -from beanie.sync import Document - - -class Sample(Document): - num: Indexed(int) - description: str -``` - -The `Indexed` function takes an optional argument `index_type`, which may be set to a pymongo index type: - -```python -from beanie.sync import Document -from beanie import Indexed - -import pymongo - -class Sample(Document): - description: Indexed(str, index_type=pymongo.TEXT) -``` - -The `Indexed` function also supports pymongo `IndexModel` kwargs arguments ([PyMongo Documentation](https://pymongo.readthedocs.io/en/stable/api/pymongo/operations.html#pymongo.operations.IndexModel)). - -For example, to create a `unique` index: - -```python -from beanie.sync import Document -from beanie import Indexed - -class Sample(Document): - name: Indexed(str, unique=True) -``` - -## Collection - -The inner class `Settings` is used to configure: - -- MongoDB collection name -- Indexes - -### Collection name - -To set MongoDB collection name, you can use the `name` field of the `Settings` inner class. - -```python -from beanie.sync import Document - -class Sample(Document): - num: int - description: str - - class Settings: - name = "samples" -``` - -### Indexes - -The `indexes` field of the inner `Settings` class is responsible for the indexes' setup. -It is a list where items can be: - -- Single key. Name of the document's field (this is equivalent to using the Indexed function described above) -- List of (key, direction) pairs. Key - string, name of the document's field. Direction - pymongo direction ( - example: `pymongo.ASCENDING`) -- `pymongo.IndexModel` instance - the most flexible - option. [PyMongo Documentation](https://pymongo.readthedocs.io/en/stable/api/pymongo/operations.html#pymongo.operations.IndexModel) - -```python -from beanie.sync import Document - -class DocumentTestModelWithIndex(Document): - test_int: int - test_list: List[SubDocument] - test_str: str - - class Settings: - indexes = [ - "test_int", - [ - ("test_int", pymongo.ASCENDING), - ("test_str", pymongo.DESCENDING), - ], - IndexModel( - [("test_str", pymongo.DESCENDING)], - name="test_string_index_DESCENDING", - ), - ] -``` - -## Settings - -The inner class `Settings` is used to configure: - -- Encoders -- Use of `revision_id` -- Use of cache -- Use of state management -- Validation on save - -### Encoders - -The `bson_encoders` field of the inner `Settings` class defines how the Python types are going to be represented -when saved in the database. The default conversions can be overridden with this. - -The `ip` field in the following example is converted to String by default: - -```python -from ipaddress import IPv4Address - -from beanie.sync import Document - - -class Sample(Document): - ip: IPv4Address -``` -> **Note:** Default conversions are defined in `beanie.odm.utils.bson.ENCODERS_BY_TYPE`. - -However, if you want the `ip` field to be represented as Integer in the database, -you need to override the default encoders like this: - -```python -from ipaddress import IPv4Address - -from beanie.sync import Document - -class Sample(Document): - ip: IPv4Address - - class Settings: - bson_encoders = { - IPv4Address: int - } -``` - -You can also define your own function for the encoding: - -```python -from ipaddress import IPv4Address - -from beanie.sync import Document - -def ipv4address_to_int(v: IPv4Address): - return int(v) - -class Sample(Document): - ip: IPv4Address - - class Settings: - bson_encoders = { - IPv4Address: ipv4address_to_int - } -``` diff --git a/docs/sync_tutorial/delete.md b/docs/sync_tutorial/delete.md deleted file mode 100644 index 76a39cdb..00000000 --- a/docs/sync_tutorial/delete.md +++ /dev/null @@ -1,31 +0,0 @@ -# Delete documents - -Beanie supports single and batch deletions: - -## Single - -```python - -Product.find_one(Product.name == "Milka").delete().run() - -# Or -bar = Product.find_one(Product.name == "Milka").run() -bar.delete() -``` - -## Many - -```python - -Product.find(Product.category.name == "Chocolate").delete().run() -``` - -## All - -```python - -Product.delete_all() -# Or -Product.all().delete().run() - -``` \ No newline at end of file diff --git a/docs/sync_tutorial/find.md b/docs/sync_tutorial/find.md deleted file mode 100644 index e570d979..00000000 --- a/docs/sync_tutorial/find.md +++ /dev/null @@ -1,182 +0,0 @@ -To populate the database, please run the examples from the [previous section of the tutorial](inserting-into-the-database.md) -as we will be using the same setup here. - -## Finding documents - -The basic syntax for finding multiple documents in the database is to call the class method `find()` -or it's synonym `find_many()` with some search criteria (see next section): - -```python -findresult = Product.find(search_criteria) -``` - -This returns a `FindMany` object, which can be used to access the results in different ways. -To loop through the results, use a `for` loop: - -```python -for result in Product.find(search_criteria): - print(result) -``` - -If you prefer a list of the results, then you can call `to_list()` method: - -```python -result = Product.find(search_criteria).to_list() -``` - -To get the first document, you can use `.first_or_none()` method. -It returns the first found document or `None`, if no documents were found. - -```python -result = Product.find(search_criteria).first_or_none() -``` - -### Search criteria - -As search criteria, Beanie supports Python-based syntax. -For comparisons Python comparison operators can be used on the class fields (and nested -fields): - -```python -products = Product.find(Product.price < 10).to_list() -``` - -This is supported for the following operators: `==`, `>`, `>=`, `<`, `<=`, `!=`. -Other MongoDB query operators can be used with the included wrappers. -For example, the `$in` operator can be used as follows: - -```python -from beanie.operators import In - -products = Product.find( - In(Product.category.name, ["Chocolate", "Fruits"]) -).to_list() -``` - -The whole list of the find query operators can be found [here](/api-documentation/operators/find). - -For more complex cases native PyMongo syntax is also supported: - -```python -products = Product.find({"price": 1000}).to_list() -``` - -## Finding single documents - -Sometimes you will only need to find a single document. -If you are searching by `id`, then you can use the [get](/api-documentation/document/#documentget) method: - -```python -bar = Product.get("608da169eb9e17281f0ab2ff").run() -``` - -To find a single document via a single search criterion, -you can use the [find_one](/api-documentation/interfaces/#findinterfacefind_one) method: - -```python -bar = ~Product.get("608da169eb9e17281f0ab2ff") -``` - -The `~` prefix can replace `run` methods of any query here and later. - -To find a single document via a searching criteria, you can use the [find_one](/beanie/api-documentation/document/#documentfind_one) method: - -```python -bar = Product.find_one(Product.name == "Peanut Bar").run() -``` - -## More complex queries - -### Multiple search criteria - -If you have multiple criteria to search against, -you can pass them as separate arguments to any of the `find` functions: - -```python -chocolates = Product.find( - Product.category.name == "Chocolate", - Product.price < 5 -).to_list() -``` - - -Alternatively, you can chain `find` methods: - -```python -chocolates = Product - .find(Product.category.name == "Chocolate") - .find(Product.price < 5).to_list() -``` - -### Sorting - -Sorting can be done with the [sort](/api-documentation/query#sort) method. - -You can pass it one or multiple fields to sort by. You may optionally specify a `+` or `-` -(denoting ascending and descending respectively). - -```python -chocolates = Product.find( - Product.category.name == "Chocolate").sort(-Product.price,+Product.name).to_list() -``` - -You can also specify fields as strings or as tuples: - -```python -chocolates = Product.find( - Product.category.name == "Chocolate").sort("-price","+name").to_list() - -chocolates = Product.find( - Product.category.name == "Chocolate").sort( - [ - (Product.price, pymongo.DESCENDING), - (Product.name, pymongo.ASCENDING), - ] -).to_list() -``` - -### Skip and limit - -To skip a certain number of documents, or limit the total number of elements returned, -the `skip` and `limit` methods can be used: -```python -chocolates = Product.find( - Product.category.name == "Chocolate").skip(2).to_list() - -chocolates = Product.find( - Product.category.name == "Chocolate").limit(2).to_list() -``` - -### Projections - -When only a part of a document is required, projections can save a lot of database bandwidth and processing. -For simple projections we can just define a pydantic model with the required fields and pass it to `project()` method: - -```python -class ProductShortView(BaseModel): - name: str - price: float - - -chocolates = Product.find( - Product.category.name == "Chocolate").project(ProductShortView).to_list() -``` - -For more complex projections an inner `Settings` class with a `projection` field can be added: - -```python -class ProductView(BaseModel): - name: str - category: str - - class Settings: - projection = {"name": 1, "category": "$category.name"} - - -chocolates = Product.find( - Product.category.name == "Chocolate").project(ProductView).to_list() -``` - -### Finding all documents - -If you ever want to find all documents, you can use the `find_all()` class method. This is equivalent to `find({})`. diff --git a/docs/sync_tutorial/init.md b/docs/sync_tutorial/init.md deleted file mode 100644 index 76b943c7..00000000 --- a/docs/sync_tutorial/init.md +++ /dev/null @@ -1,40 +0,0 @@ -Beanie uses `PyMongo` as database engine for sync cases. To initialize previously created documents, you should provide a `PyMongo` database instance and a list of your document models to the `init_beanie(...)` function, as it is shown in the example: - -```python -from beanie.sync import init_beanie, Document -from pymongo import MongoClient - -class Sample(Document): - name: str - -def init(): - # Create PyMongo client - client = MongoClient( - "mongodb://user:pass@host:27017" - ) - - # Initialize beanie with the Product document class and a database - init_beanie(database=client.db_name, document_models=[Sample]) -``` - -This creates the collection (if necessary) and sets up any indexes that are defined. - - -`init_beanie` supports not only a list of classes as the document_models argument, -but also strings with dot-separated paths: - -```python -init_beanie( - database=client.db_name, - document_models=[ - "app.models.DemoDocument", - ], -) -``` - -### Warning - -`init_beanie` supports the parameter named `allow_index_dropping` that will drop indexes from your collections. -`allow_index_dropping` is by default set to `False`. If you set this to `True`, -ensure that you are not managing your indexes in another manner. -If you are, these will be deleted when setting `allow_index_dropping=True`. \ No newline at end of file diff --git a/docs/sync_tutorial/insert.md b/docs/sync_tutorial/insert.md deleted file mode 100644 index 4784cd50..00000000 --- a/docs/sync_tutorial/insert.md +++ /dev/null @@ -1,61 +0,0 @@ -# Insert the documents - -Beanie documents behave just like pydantic models (because they subclass `pydantic.BaseModel`). -Hence, a document can be created in a similar fashion to pydantic: - -```python -from typing import Optional - -from pydantic import BaseModel - -from beanie.sync import Document -from beanie import Indexed - - -class Category(BaseModel): - name: str - description: str - - -class Product(Document): # This is the model - name: str - description: Optional[str] = None - price: Indexed(float) - category: Category - - class Settings: - name = "products" - - -chocolate = Category(name="Chocolate", description="A preparation of roasted and ground cacao seeds.") -tonybar = Product(name="Tony's", price=5.95, category=chocolate) -marsbar = Product(name="Mars", price=1, category=chocolate) -``` - -This however does not save the documents to the database yet. - -## Insert a single document - -To insert a document into the database, you can call either `insert()` or `create()` on it (they are synonyms): - -```python -tonybar.insert() -marsbar.create() # does exactly the same as insert() -``` -You can also call `save()`, which behaves in the same manner for new documents, but will also update existing documents. -See the [section on updating](updating-&-deleting.md) of this tutorial for more details. - -If you prefer, you can also call the `insert_one` class method: - -```python -Product.insert_one(tonybar) -``` - -## Inserting many documents - -To reduce the number of database queries, -similarly typed documents should be inserted together by calling the class method `insert_many`: - -```python -Product.insert_many([tonybar,marsbar]) -``` diff --git a/docs/sync_tutorial/multi-model.md b/docs/sync_tutorial/multi-model.md deleted file mode 100644 index 555878e8..00000000 --- a/docs/sync_tutorial/multi-model.md +++ /dev/null @@ -1,80 +0,0 @@ -# Multi-model pattern - -Documents with different schemas could be stored in a single collection and managed correctly. -`UnionDoc` class is used for this. - -It supports `find` and `aggregate` methods. -For `find`, it will fetch all the found documents into the respective `Document` classes. - -Documents that have `union_doc` in their settings can still be used in `find` and other queries. -Queries of one such class will not see the data of others. - -## Example - -Create documents: - -```python -from beanie.sync import Document, UnionDoc - - -class Parent(UnionDoc): # Union - class Settings: - name = "union_doc_collection" # Collection name - - -class One(Document): - int_field: int = 0 - shared: int = 0 - - class Settings: - union_doc = Parent - - -class Two(Document): - str_field: str = "test" - shared: int = 0 - - class Settings: - union_doc = Parent -``` - -The schemas could be incompatible. - -Insert a document - -```python -One().insert() -One().insert() -One().insert() - -Two().insert() -``` - -Find all the documents of the first type: - -```python -docs = One.all().to_list() -print(len(docs)) - ->> 3 # It found only documents of class One -``` - -Of the second type: - -```python -docs = Two.all().to_list() -print(len(docs)) - ->> 1 # It found only documents of class One -``` - -Of both: - -```python -docs = Parent.all().to_list() -print(len(docs)) - ->> 4 # instances of the both classes will be in the output here -``` - -Aggregations will work separately for these two document classes too. diff --git a/docs/sync_tutorial/on_save_validation.md b/docs/sync_tutorial/on_save_validation.md deleted file mode 100644 index 599d1c71..00000000 --- a/docs/sync_tutorial/on_save_validation.md +++ /dev/null @@ -1,31 +0,0 @@ -# On save validation - -Pydantic has a very useful config to validate values on assignment - `validate_assignment = True`. -But, unfortunately, this is an expensive operation and doesn't fit some use cases. -You can validate all the values before saving the document (`insert`, `replace`, `save`, `save_changes`) -with beanie config `validate_on_save` instead. - -This feature must be turned on in the `Settings` inner class explicitly: - -```python -from beanie.sync import Document - - -class Sample(Document): - num: int - name: str - - class Settings: - validate_on_save = True -``` - -If any field has a wrong value, -it will raise an error on write operations (`insert`, `replace`, `save`, `save_changes`). - -```python -sample = Sample.find_one(Sample.name == "Test").run() -sample.num = "wrong value type" - -# Next call will raise an error -sample.replace() -``` diff --git a/docs/sync_tutorial/relations.md b/docs/sync_tutorial/relations.md deleted file mode 100644 index a7c4e6b9..00000000 --- a/docs/sync_tutorial/relations.md +++ /dev/null @@ -1,216 +0,0 @@ -# Relations - -The document can contain links to other documents in their fields. - -*Only top-level fields are fully supported for now.* - -The following field types are supported: - -- `Link[...]` -- `Optional[Link[...]]` -- `List[Link[...]]` -- `Optional[List[Link[...]]]` - -Direct link to the document: - -```python -from beanie.sync import Document, Link - - -class Door(Document): - height: int = 2 - width: int = 1 - - -class House(Document): - name: str - door: Link[Door] -``` - -Optional direct link to the document: - -```python -from typing import Optional - -from beanie import Document, Link - - -class Door(Document): - height: int = 2 - width: int = 1 - - -class House(Document): - name: str - door: Optional[Link[Door]] -``` - -List of the links: - -```python -from typing import List - -from beanie import Document, Link - - -class Window(Document): - x: int = 10 - y: int = 10 - - -class House(Document): - name: str - door: Link[Door] - windows: List[Link[Window]] -``` - -Optional list of the links: - -```python -from typing import List, Optional - -from beanie import Document, Link - -class Window(Document): - x: int = 10 - y: int = 10 - -class Yard(Document): - v: int = 10 - y: int = 10 - -class House(Document): - name: str - door: Link[Door] - windows: List[Link[Window]] - yards: Optional[List[Link[Yard]]] -``` - -Other link patterns are not supported at this moment. If you need something more specific for your use-case, -please open an issue on the GitHub page - - -## Write - -The following write methods support relations: - -- `insert(...)` -- `replace(...)` -- `save(...)` - -To apply a write method to the linked documents, you should pass the respective `link_rule` argument - -```python -house.windows = [Window(x=100, y=100)] -house.name = "NEW NAME" - -# The next call will insert a new window object and replace the house instance with updated data -house.save(link_rule=WriteRules.WRITE) - -# `insert` and `replace` methods will work the same way -``` - -Otherwise, Beanie can ignore internal links with the `link_rule` parameter `WriteRules.DO_NOTHING` - -```python -house.door.height = 3 -house.name = "NEW NAME" - -# The next call will just replace the house instance with new data, but the linked door object will not be synced -house.replace(link_rule=WriteRules.DO_NOTHING) - -# `insert` and `save` methods will work the same way -``` - -## Fetch - -### Prefetch - -You can fetch linked documents on the find query step using the `fetch_links` parameter - -```python -houses = House.find( - House.name == "test", - fetch_links=True -).to_list() -``` -Supported find methods: -- `find` -- `find_one` -- `get` - -Beanie uses the single aggregation query under the hood to fetch all the linked documents. -This operation is very effective. - -If a direct link is referred to a non-existent document, -after fetching it will remain the object of the `Link` class. - -Fetching will ignore non-existent documents for the list of links fields. - -#### Search by linked documents fields - -If the `fetch_links` parameter is set to `True`, search by linked documents fields is available. - -By field of the direct link: - -```python -houses = House.find( - House.door.height == 2, - fetch_links=True -).to_list() -``` - -By list of links: - -```python -houses = House.find( - House.windows.x > 10, - fetch_links=True -).to_list() -``` - -Search by `id` of the linked documents works using the following syntax: - -```python -houses = House.find( - House.door.id == "DOOR_ID_HERE" -).to_list() -``` - -It works the same way with `fetch_links` equal to `True` and `False` and for `find_many` and `find_one` methods. - -### On-demand fetch - -If you don't use prefetching, linked documents will be presented as objects of the `Link` class. -You can fetch them manually afterwards. -To fetch all the linked documents, you can use the `fetch_all_links` method - -```python -house.fetch_all_links() -``` - -It will fetch all the linked documents and replace `Link` objects with them. - -Otherwise, you can fetch a single field: - -```python -house.fetch_link(House.door) -``` - -This will fetch the Door object and put it into the `door` field of the `house` object. - -## Delete - -Delete method works the same way as write operations, but it uses other rules. - -To delete all the links on the document deletion, -you should use the `DeleteRules.DELETE_LINKS` value for the `link_rule` parameter: - -```python -house.delete(link_rule=DeleteRules.DELETE_LINKS).run() -``` - -To keep linked documents, you can use the `DO_NOTHING` rule: - -```python -house.delete(link_rule=DeleteRules.DO_NOTHING).run() -``` diff --git a/docs/sync_tutorial/revision.md b/docs/sync_tutorial/revision.md deleted file mode 100644 index 098af22b..00000000 --- a/docs/sync_tutorial/revision.md +++ /dev/null @@ -1,38 +0,0 @@ -# Revision - -This feature helps with concurrent operations. -It stores `revision_id` together with the document and changes it on each document update. -If the application with an older local copy of the document tries to change it, an exception will be raised. -Only when the local copy is synced with the database, the application will be allowed to change the data. -This helps to avoid data losses. - -This feature must be explicitly turned on in the `Settings` inner class: - -```python -from beanie.sync import Document - - -class Sample(Document): - num: int - name: str - - class Settings: - use_revision = True -``` - -Any changing operation will check if the local copy of the document has the up-to-date `revision_id` value: - -```python -s = Sample.find_one(Sample.name="TestName").run() -s.num = 10 - -# If a concurrent process already changed the doc, the next operation will raise an error -s.replace() -``` - -If you want to ignore revision and apply all the changes even if the local copy is outdated, -you can use the `ignore_revision` parameter: - -```python -s.replace(ignore_revision=True) -``` diff --git a/docs/sync_tutorial/state_management.md b/docs/sync_tutorial/state_management.md deleted file mode 100644 index 9ca3a0d4..00000000 --- a/docs/sync_tutorial/state_management.md +++ /dev/null @@ -1,91 +0,0 @@ -# State Management - -Beanie can keep the document state synced with the database in order to find local changes and save only them. - -This feature must be explicitly turned on in the `Settings` inner class: - -```python -from beanie.sync import Document - -class Sample(Document): - num: int - name: str - - class Settings: - use_state_management = True -``` - -To save only changed values, the `save_changes()` method should be used. - -```python -s = Sample.find_one(Sample.name == "Test").run() -s.num = 100 -s.save_changes() -``` - -The `save_changes()` method can be used only with already existing documents. - -## Options - -By default, state management will merge the changes made to nested objects, -which is fine for most cases as it is non-destructive and does not re-assign the whole object -if only one of its attributes changed: - -```python -from typing import Dict - - -class Item(Document): - name: str - attributes: Dict[str, float] - - class Settings: - use_state_management = True -``` - -```python -i = Item(name="Test", attributes={"attribute_1": 1.0, "attribute_2": 2.0}) -i.insert() -i.attributes = {"attribute_1": 1.0} -i.save_changes() -# Changes will consist of: {"attributes.attribute_1": 1.0} -# Keeping attribute_2 -``` - -However, there are some cases where you would want to replace the whole object when one of its attributes changed. -You can enable the `state_management_replace_objects` attribute in your model's `Settings` inner class: - -```python -from typing import Dict - - -class Item(Document): - name: str - attributes: Dict[str, float] - - class Settings: - use_state_management = True - state_management_replace_objects = True -``` - -With this setting activated, the whole object will be overridden when one attribute of the nested object is changed: - -```python -i = Item(name="Test", attributes={"attribute_1": 1.0, "attribute_2": 2.0}) -i.insert() -i.attributes.attribute_1 = 1.0 -i.save_changes() -# Changes will consist of: {"attributes.attribute_1": 1.0, "attributes.attribute_2": 2.0} -# Keeping attribute_2 -``` - -When the whole object is assigned, the whole nested object will be overridden: - -```python -i = Item(name="Test", attributes={"attribute_1": 1.0, "attribute_2": 2.0}) -i.insert() -i.attributes = {"attribute_1": 1.0} -i.save_changes() -# Changes will consist of: {"attributes": {"attribute_1": 1.0}} -# Removing attribute_2 -``` diff --git a/docs/sync_tutorial/sync.md b/docs/sync_tutorial/sync.md deleted file mode 100644 index 2046ec21..00000000 --- a/docs/sync_tutorial/sync.md +++ /dev/null @@ -1,108 +0,0 @@ -## Migrate from async - -Since version `1.13.0` Beanie supports synchronous interfaces as well as async. Nearly all the syntax is the same. But there are a few significant changes. -## Import - -Most of the entities should be imported from `beanie.sync` instead of just `beanie`. For example, the Document class: - -```python -from beanie.sync import Document - -class Product(Document): - name: str - price: float -``` - -## Init - -As it is a synchronous version, a sync client should be used for the initialization. - -```python -from pymongo import MongoClient - -from beanie.sync import init_beanie - - -cli = MongoClient("mongodb://localhost:27017") -db = cli.products_db - -init_beanie(database=db, document_models=[Product]) -``` - -## Queries - -For query objects `FindOne`, `UpdateQuery`, and `DeleteQuery` you need to call the additional `run()` method at the end of the methods chain to fetch/commit. As a syntax sugar it can be replaced with `~` prefix. - -### Find - -Get -```python -bar = Product.get("608da169eb9e17281f0ab2ff").run() -# or -bar = ~Product.get("608da169eb9e17281f0ab2ff") -``` - -Find one -```python -bar = Product.find_one(Product.name == "Peanut Bar").run() -# or -bar = ~Product.find_one(Product.name == "Peanut Bar") -``` - -For find many you don't need to call `run()` method, as it is iterator: - -```python -for result in Product.find(search_criteria): - print(result) - -# or - -result = Product.find(search_criteria).to_list() -``` - -### Update - -Update one -```python -Product.find_one(Product.name == "Tony's").update({"$set": {Product.price: 3.33}}).run() -# or -~Product.find_one(Product.name == "Tony's").update({"$set": {Product.price: 3.33}}) -``` - -BTW update of the already fetched object works without calling the `run` method as it doesn't return `UpdateQuery` in result -```python -bar = Product.find_one(Product.name == "Milka").run() -bar.update({"$set": {Product.price: 3.33}}) -``` - -Update many -```python -Product.find(Product.price <= 2).update({"$set": {Product.price: 3.33}}).run() -# or -~Product.find(Product.price <= 2).update({"$set": {Product.price: 3.33}}) -``` - -### Delete - -Single -```python -Product.find_one(Product.name == "Milka").delete().run() - -# or - -~Product.find_one(Product.name == "Milka").delete() - -# or - -bar = Product.find_one(Product.name == "Milka").run() -bar.delete() -``` - -Many -```python -Product.find(Product.category.name == "Chocolate").delete().run() - -# or - -~Product.find(Product.category.name == "Chocolate").delete() -``` \ No newline at end of file diff --git a/docs/sync_tutorial/update.md b/docs/sync_tutorial/update.md deleted file mode 100644 index f07c50d5..00000000 --- a/docs/sync_tutorial/update.md +++ /dev/null @@ -1,82 +0,0 @@ -# Updating & Deleting - -Now that we know how to find documents, how do we change them or delete them? - -## Saving changes to existing documents - -The easiest way to change a document in the database is to use either the `replace` or `save` method on an altered document. -These methods both write the document to the database, -but `replace` will raise an exception when the document does not exist yet, while `save` will insert the document. - -Using `save()` method: - -```python -bar = Product.find_one(Product.name == "Mars").run() -bar.price = 10 -bar.save() -``` - -Otherwise, use the `replace()` method, which throws: -- a `ValueError` if the document does not have an `id` yet, or -- a `beanie.exceptions.DocumentNotFound` if it does, but the `id` is not present in the collection - -```python -bar.price = 10 -try: - bar.replace() -except (ValueError, beanie.exceptions.DocumentNotFound): - print("Can't replace a non existing document") -``` - -Note that these methods require multiple queries to the database and replace the entire document with the new version. -A more tailored solution can often be created by applying update queries directly on the database level. - -## Update queries - -Update queries can be performed on the result of a `find` or `find_one` query, -or on a document that was returned from an earlier query. -Simpler updates can be performed using the `set`, `inc`, and `current_date` methods: - -```python -bar = Product.find_one(Product.name == "Mars").run() -bar.set({Product.name:"Gold bar"}) -bar = Product.find_all(Product.price > .5).inc({Product.price: 1}).run() -``` - -More complex update operations can be performed by calling `update()` with an update operator, similar to find queries: - -```python -Product.find_one(Product.name == "Tony's").update(Set({Product.price: 3.33})).run() -``` - -The whole list of the update query operators can be found [here](/api-documentation/operators/update). - -Native MongoDB syntax is also supported: - -```python -Product.find_one(Product.name == "Tony's").update({"$set": {Product.price: 3.33}}).run() -``` - -## Upsert - -To insert a document when no documents are matched against the search criteria, the `upsert` method can be used: - -```python -Product.find_one(Product.name == "Tony's").upsert( - Set({Product.price: 3.33}), - on_insert=Product(name="Tony's", price=3.33, category=chocolate) -) -``` - -## Deleting documents - -Deleting objects works just like updating them, you simply call `delete()` on the found documents: - -```python -bar = Product.find_one(Product.name == "Milka").run() -bar.delete() - -Product.find_one(Product.name == "Milka").delete().run() - -Product.find(Product.category.name == "Chocolate").delete().run() -``` diff --git a/docs/sync_tutorial/views.md b/docs/sync_tutorial/views.md deleted file mode 100644 index 9f4cae9a..00000000 --- a/docs/sync_tutorial/views.md +++ /dev/null @@ -1,102 +0,0 @@ -# Views - -Virtual views are aggregation pipelines stored in MongoDB that act as collections for reading operations. -You can use the `View` class the same way as `Document` for `find` and `aggregate` operations. - -## Here are some examples. - -Create a view: - -```python -from pydantic import Field - -from beanie import Document, View - - -class Bike(Document): - type: str - frame_size: int - is_new: bool - - -class Metrics(View): - type: str = Field(alias="_id") - number: int - new: int - - class Settings: - source = Bike - pipeline = [ - { - "$group": { - "_id": "$type", - "number": {"$sum": 1}, - "new": {"$sum": {"$cond": ["$is_new", 1, 0]}} - } - }, - ] - -``` - -Initialize Beanie: - -```python -from pymongo import MongoClient -from beanie.sync import init_beanie - - -def main(): - uri = "mongodb://beanie:beanie@localhost:27017" - client = MongoClient(uri) - db = client.bikes - - init_beanie( - database=db, - document_models=[Bike, Metrics], - recreate_views=True, - ) -``` - -Create bikes: - -```python -Bike(type="Mountain", frame_size=54, is_new=True).insert() -Bike(type="Mountain", frame_size=60, is_new=False).insert() -Bike(type="Road", frame_size=52, is_new=True).insert() -Bike(type="Road", frame_size=54, is_new=True).insert() -Bike(type="Road", frame_size=58, is_new=False).insert() -``` - -Find metrics for `type == "Road"` - -```python -results = Metrics.find(Metrics.type == "Road").to_list() -print(results) - ->> [Metrics(type='Road', number=3, new=2)] -``` - -Aggregate over metrics to get the count of all the new bikes: - -```python -results = Metrics.aggregate([{ - "$group": { - "_id": None, - "new_total": {"$sum": "$new"} - } -}]).to_list() - -print(results) - ->> [{'_id': None, 'new_total': 3}] -``` - -A better result can be achieved by using find query aggregation syntactic sugar: - -```python -results = Metrics.all().sum(Metrics.new) - -print(results) - ->> 3 -``` diff --git a/docs/async_tutorial/actions.md b/docs/tutorial/actions.md similarity index 100% rename from docs/async_tutorial/actions.md rename to docs/tutorial/actions.md diff --git a/docs/async_tutorial/aggregate.md b/docs/tutorial/aggregate.md similarity index 100% rename from docs/async_tutorial/aggregate.md rename to docs/tutorial/aggregate.md diff --git a/docs/async_tutorial/cache.md b/docs/tutorial/cache.md similarity index 100% rename from docs/async_tutorial/cache.md rename to docs/tutorial/cache.md diff --git a/docs/async_tutorial/defining-a-document.md b/docs/tutorial/defining-a-document.md similarity index 98% rename from docs/async_tutorial/defining-a-document.md rename to docs/tutorial/defining-a-document.md index 9ff71a75..88bd1a0e 100644 --- a/docs/async_tutorial/defining-a-document.md +++ b/docs/tutorial/defining-a-document.md @@ -104,12 +104,17 @@ class Sample(Document): name: Indexed(str, unique=True) ``` -## Collection +## Settings The inner class `Settings` is used to configure: - MongoDB collection name - Indexes +- Encoders +- Use of `revision_id` +- Use of cache +- Use of state management +- Validation on save ### Collection name @@ -155,16 +160,6 @@ class DocumentTestModelWithIndex(Document): ] ``` -## Settings - -The inner class `Settings` is used to configure: - -- Encoders -- Use of `revision_id` -- Use of cache -- Use of state management -- Validation on save - ### Encoders The `bson_encoders` field of the inner `Settings` class defines how the Python types are going to be represented diff --git a/docs/async_tutorial/delete.md b/docs/tutorial/delete.md similarity index 100% rename from docs/async_tutorial/delete.md rename to docs/tutorial/delete.md diff --git a/docs/async_tutorial/find.md b/docs/tutorial/find.md similarity index 100% rename from docs/async_tutorial/find.md rename to docs/tutorial/find.md diff --git a/docs/async_tutorial/collection_setup.md b/docs/tutorial/indexes.md similarity index 57% rename from docs/async_tutorial/collection_setup.md rename to docs/tutorial/indexes.md index 02df59ed..5dd3ffcb 100644 --- a/docs/async_tutorial/collection_setup.md +++ b/docs/tutorial/indexes.md @@ -1,31 +1,6 @@ -# Collection setup (name, indexes, timeseries) +## Indexes setup -Although basic pydantic syntax allows you to set all aspects of individual fields, -there is also some need to configure collections as a whole. -In particular, you might want to: - -- Set the MongoDB collection name -- Configure indexes - -This is done by defining a `Settings` class within your `Document` class. - -## Declaring the collection name - -To set MongoDB collection name, you can use the `name` field of the `Settings` inner class. - -```python -from beanie import Document - - -class Sample(Document): - num: int - description: str - - class Settings: - name = "samples" -``` - -## Indexes +There are more than one way to set up indexes using Beanie ### Indexed function @@ -100,33 +75,3 @@ class Sample(Document): ), ] ``` - -## Time series - -You can set up a timeseries collection using the inner `Settings` class. - -**Be aware, timeseries collections a supported by MongoDB 5.0 and higher only.** - -```python -from datetime import datetime - -from beanie import Document, TimeSeriesConfig, Granularity -from pydantic import Field - - -class Sample(Document): - ts: datetime = Field(default_factory=datetime.now) - meta: str - - class Settings: - timeseries = TimeSeriesConfig( - time_field="ts", # Required - meta_field="meta", # Optional - granularity=Granularity.hours, # Optional - expire_after_seconds=2 # Optional - ) -``` - -TimeSeriesConfig fields reflect the respective parameters of the MongoDB timeseries creation function. - -MongoDB documentation: https://docs.mongodb.com/manual/core/timeseries-collections/ diff --git a/docs/async_tutorial/inheritance.md b/docs/tutorial/inheritance.md similarity index 99% rename from docs/async_tutorial/inheritance.md rename to docs/tutorial/inheritance.md index 6afd8333..d250fd0d 100644 --- a/docs/async_tutorial/inheritance.md +++ b/docs/tutorial/inheritance.md @@ -1,4 +1,4 @@ -## Inheritance +## Inheritance for multi-model use case Beanie `Documents` support inheritance as any other Python classes. But there are additional features available, if you mark the root model with parameter `is_root = True` in the inner Settings class. diff --git a/docs/async_tutorial/init.md b/docs/tutorial/init.md similarity index 100% rename from docs/async_tutorial/init.md rename to docs/tutorial/init.md diff --git a/docs/async_tutorial/insert.md b/docs/tutorial/insert.md similarity index 100% rename from docs/async_tutorial/insert.md rename to docs/tutorial/insert.md diff --git a/docs/async_tutorial/migrations.md b/docs/tutorial/migrations.md similarity index 100% rename from docs/async_tutorial/migrations.md rename to docs/tutorial/migrations.md diff --git a/docs/async_tutorial/multi-model.md b/docs/tutorial/multi-model.md similarity index 100% rename from docs/async_tutorial/multi-model.md rename to docs/tutorial/multi-model.md diff --git a/docs/async_tutorial/on_save_validation.md b/docs/tutorial/on_save_validation.md similarity index 100% rename from docs/async_tutorial/on_save_validation.md rename to docs/tutorial/on_save_validation.md diff --git a/docs/async_tutorial/relations.md b/docs/tutorial/relations.md similarity index 100% rename from docs/async_tutorial/relations.md rename to docs/tutorial/relations.md diff --git a/docs/async_tutorial/revision.md b/docs/tutorial/revision.md similarity index 100% rename from docs/async_tutorial/revision.md rename to docs/tutorial/revision.md diff --git a/docs/async_tutorial/state_management.md b/docs/tutorial/state_management.md similarity index 100% rename from docs/async_tutorial/state_management.md rename to docs/tutorial/state_management.md diff --git a/docs/tutorial/time_series.md b/docs/tutorial/time_series.md new file mode 100644 index 00000000..19596f58 --- /dev/null +++ b/docs/tutorial/time_series.md @@ -0,0 +1,29 @@ +# Time series + +You can set up a timeseries collection using the inner `Settings` class. + +**Be aware, timeseries collections a supported by MongoDB 5.0 and higher only.** + +```python +from datetime import datetime + +from beanie import Document, TimeSeriesConfig, Granularity +from pydantic import Field + + +class Sample(Document): + ts: datetime = Field(default_factory=datetime.now) + meta: str + + class Settings: + timeseries = TimeSeriesConfig( + time_field="ts", # Required + meta_field="meta", # Optional + granularity=Granularity.hours, # Optional + expire_after_seconds=2 # Optional + ) +``` + +TimeSeriesConfig fields reflect the respective parameters of the MongoDB timeseries creation function. + +MongoDB documentation: https://docs.mongodb.com/manual/core/timeseries-collections/ \ No newline at end of file diff --git a/docs/async_tutorial/update.md b/docs/tutorial/update.md similarity index 100% rename from docs/async_tutorial/update.md rename to docs/tutorial/update.md diff --git a/docs/async_tutorial/views.md b/docs/tutorial/views.md similarity index 100% rename from docs/async_tutorial/views.md rename to docs/tutorial/views.md diff --git a/pydoc-markdown.yml b/pydoc-markdown.yml index 3a7fda28..ee85e807 100644 --- a/pydoc-markdown.yml +++ b/pydoc-markdown.yml @@ -61,76 +61,42 @@ renderer: source: docs/getting-started.md - title: Tutorial children: - - title: Async - children: - - title: Defining a document - source: docs/async_tutorial/defining-a-document.md - - title: Initialization - source: docs/async_tutorial/init.md - - title: Inserting into the database - source: docs/async_tutorial/insert.md - - title: Finding documents - source: docs/async_tutorial/find.md - - title: Updating & Deleting - source: docs/async_tutorial/update.md - - title: Multi-model pattern - source: docs/async_tutorial/multi-model.md - - title: Inheritance - source: docs/async_tutorial/inheritance.md - - title: Indexes & collection names - source: docs/async_tutorial/collection_setup.md - - title: Aggregation - source: docs/async_tutorial/aggregate.md - - title: Relations - source: docs/async_tutorial/relations.md - - title: Views - source: docs/async_tutorial/views.md - - title: Event-based actions - source: docs/async_tutorial/actions.md - - title: Cache - source: docs/async_tutorial/cache.md - - title: Revision - source: docs/async_tutorial/revision.md - - title: State Management - source: docs/async_tutorial/state_management.md - - title: On save validation - source: docs/async_tutorial/on_save_validation.md - - title: Migrations - source: docs/async_tutorial/migrations.md - - title: Sync - children: - - title: Migrate from async - source: docs/sync_tutorial/sync.md - - title: Defining a document - source: docs/sync_tutorial/defining-a-document.md - - title: Initialization - source: docs/sync_tutorial/init.md - - title: Inserting into the database - source: docs/sync_tutorial/insert.md - - title: Finding documents - source: docs/sync_tutorial/find.md - - title: Updating & Deleting - source: docs/sync_tutorial/update.md - - title: Multi-model pattern - source: docs/sync_tutorial/multi-model.md - - title: Indexes & collection names - source: docs/sync_tutorial/collection_setup.md - - title: Aggregation - source: docs/sync_tutorial/aggregate.md - - title: Relations - source: docs/sync_tutorial/relations.md - - title: Views - source: docs/sync_tutorial/views.md - - title: Event-based actions - source: docs/sync_tutorial/actions.md - - title: Cache - source: docs/sync_tutorial/cache.md - - title: Revision - source: docs/sync_tutorial/revision.md - - title: State Management - source: docs/sync_tutorial/state_management.md - - title: On save validation - source: docs/sync_tutorial/on_save_validation.md + - title: Defining a document + source: docs/tutorial/defining-a-document.md + - title: Initialization + source: docs/tutorial/init.md + - title: Inserting into the database + source: docs/tutorial/insert.md + - title: Finding documents + source: docs/tutorial/find.md + - title: Updating & Deleting + source: docs/tutorial/update.md + - title: Indexes + source: docs/tutorial/indexes.md + - title: Multi-model pattern + source: docs/tutorial/multi-model.md + - title: Inheritance + source: docs/tutorial/inheritance.md + - title: Aggregation + source: docs/tutorial/aggregate.md + - title: Relations + source: docs/tutorial/relations.md + - title: Views + source: docs/tutorial/views.md + - title: Time Series + source: docs/tutorial/time_series.md + - title: Event-based actions + source: docs/tutorial/actions.md + - title: Cache + source: docs/tutorial/cache.md + - title: Revision + source: docs/tutorial/revision.md + - title: State Management + source: docs/tutorial/state_management.md + - title: On save validation + source: docs/tutorial/on_save_validation.md + - title: Migrations + source: docs/tutorial/migrations.md - title: API Documentation children: - title: Document diff --git a/pyproject.toml b/pyproject.toml index 36155742..7e5bfe58 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [tool.poetry] name = "beanie" -version = "1.14.0" -description = "Python ODM for MongoDB" +version = "1.15.0" +description = "Asynchronous Python ODM for MongoDB" authors = ["Roman "] license = "Apache-2.0" homepage = "https://roman-right.github.io/beanie/" diff --git a/tests/sync/__init__.py b/tests/sync/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/sync/conftest.py b/tests/sync/conftest.py deleted file mode 100644 index c68003d0..00000000 --- a/tests/sync/conftest.py +++ /dev/null @@ -1,253 +0,0 @@ -from datetime import datetime, timedelta -from random import randint -from typing import List - -import pytest -from pydantic import BaseSettings -from pymongo import MongoClient - -from beanie.sync import init_beanie -from tests.sync.models import ( - DocumentTestModel, - DocumentTestModelWithIndexFlagsAliases, - SubDocument, - DocumentTestModelWithCustomCollectionName, - DocumentTestModelWithSimpleIndex, - DocumentTestModelWithIndexFlags, - DocumentTestModelWithComplexIndex, - DocumentTestModelFailInspection, - DocumentWithCustomIdUUID, - DocumentWithCustomIdInt, - DocumentWithCustomFiledsTypes, - DocumentWithBsonEncodersFiledsTypes, - DocumentWithActions, - DocumentWithTurnedOnStateManagement, - DocumentWithTurnedOnReplaceObjects, - DocumentWithTurnedOffStateManagement, - DocumentWithValidationOnSave, - DocumentWithRevisionTurnedOn, - DocumentWithPydanticConfig, - DocumentWithExtras, - House, - Window, - Door, - Roof, - InheritedDocumentWithActions, - DocumentForEncodingTest, - DocumentForEncodingTestDate, - DocumentMultiModelOne, - DocumentMultiModelTwo, - DocumentUnion, - HouseWithRevision, - WindowWithRevision, - DocumentWithActions2, -) -from tests.sync.models import ( - Sample, - Nested, - Option2, - Option1, - GeoObject, -) -from tests.sync.odm.views import TestView - - -class Settings(BaseSettings): - mongodb_dsn: str = "mongodb://localhost:27017/beanie_db" - mongodb_db_name: str = "beanie_db" - - -@pytest.fixture -def settings(): - return Settings() - - -@pytest.fixture() -def cli(settings, loop): - return MongoClient(settings.mongodb_dsn) - - -@pytest.fixture() -def db(cli, settings, loop): - return cli[settings.mongodb_db_name] - - -@pytest.fixture -def point(): - return { - "longitude": 13.404954, - "latitude": 52.520008, - } - - -@pytest.fixture -def preset_documents(point): - docs = [] - for i in range(10): - timestamp = datetime.utcnow() - timedelta(days=i) - integer_1: int = i // 3 - integer_2: int = i // 2 - float_num = integer_1 + 0.3 - string: str = f"test_{integer_1}" - option_1 = Option1(s="TEST") - option_2 = Option2(f=3.14) - union = option_1 if i % 2 else option_2 - optional = option_2 if not i % 3 else None - geo = GeoObject( - coordinates=[ - point["longitude"] + i / 10, - point["latitude"] + i / 10, - ] - ) - nested = Nested( - integer=integer_2, - option_1=option_1, - union=union, - optional=optional, - ) - - sample = Sample( - timestamp=timestamp, - increment=i, - integer=integer_1, - float_num=float_num, - string=string, - nested=nested, - optional=optional, - union=union, - geo=geo, - ) - docs.append(sample) - Sample.insert_many(documents=docs) - - -@pytest.fixture() -def sample_doc_not_saved(point): - nested = Nested( - integer=0, - option_1=Option1(s="TEST"), - union=Option1(s="TEST"), - optional=None, - ) - geo = GeoObject( - coordinates=[ - point["longitude"], - point["latitude"], - ] - ) - return Sample( - timestamp=datetime.utcnow(), - increment=0, - integer=0, - float_num=0, - string="TEST_NOT_SAVED", - nested=nested, - optional=None, - union=Option1(s="TEST"), - geo=geo, - ) - - -@pytest.fixture() -def session(cli, loop): - s = cli.start_session() - yield s - s.end_session() - - -@pytest.fixture(autouse=True) -def init(loop, db): - models = [ - DocumentWithExtras, - DocumentWithPydanticConfig, - DocumentTestModel, - DocumentTestModelWithCustomCollectionName, - DocumentTestModelWithSimpleIndex, - DocumentTestModelWithIndexFlags, - DocumentTestModelWithIndexFlagsAliases, - DocumentTestModelWithComplexIndex, - DocumentTestModelFailInspection, - DocumentWithBsonEncodersFiledsTypes, - DocumentWithCustomFiledsTypes, - DocumentWithCustomIdUUID, - DocumentWithCustomIdInt, - Sample, - DocumentWithActions, - DocumentWithTurnedOnStateManagement, - DocumentWithTurnedOnReplaceObjects, - DocumentWithTurnedOffStateManagement, - DocumentWithValidationOnSave, - DocumentWithRevisionTurnedOn, - House, - Window, - Door, - Roof, - InheritedDocumentWithActions, - DocumentForEncodingTest, - DocumentForEncodingTestDate, - TestView, - DocumentMultiModelOne, - DocumentMultiModelTwo, - DocumentUnion, - HouseWithRevision, - WindowWithRevision, - DocumentWithActions2, - ] - init_beanie( - database=db, - document_models=models, - ) - yield None - - for model in models: - model.get_motor_collection().drop() - model.get_motor_collection().drop_indexes() - - -@pytest.fixture -def document_not_inserted(): - return DocumentTestModel( - test_int=42, - test_list=[SubDocument(test_str="foo"), SubDocument(test_str="bar")], - test_doc=SubDocument(test_str="foobar"), - test_str="kipasa", - ) - - -@pytest.fixture -def documents_not_inserted(): - def generate_documents( - number: int, test_str: str = None, random: bool = False - ) -> List[DocumentTestModel]: - return [ - DocumentTestModel( - test_int=randint(0, 1000000) if random else i, - test_list=[ - SubDocument(test_str="foo"), - SubDocument(test_str="bar"), - ], - test_doc=SubDocument(test_str="foobar"), - test_str="kipasa" if test_str is None else test_str, - ) - for i in range(number) - ] - - return generate_documents - - -@pytest.fixture -def document(document_not_inserted, loop) -> DocumentTestModel: - return document_not_inserted.insert() - - -@pytest.fixture -def documents(documents_not_inserted): - def generate_documents( - number: int, test_str: str = None, random: bool = False - ): - result = DocumentTestModel.insert_many( - documents_not_inserted(number, test_str, random) - ) - return result.inserted_ids - - return generate_documents diff --git a/tests/sync/models.py b/tests/sync/models.py deleted file mode 100644 index 7f76e41d..00000000 --- a/tests/sync/models.py +++ /dev/null @@ -1,476 +0,0 @@ -import datetime -from decimal import Decimal -from ipaddress import ( - IPv4Address, - IPv4Interface, - IPv4Network, - IPv6Address, - IPv6Interface, - IPv6Network, -) -from pathlib import Path -from typing import List, Optional, Set, Tuple, Union -from uuid import UUID, uuid4 - -import pymongo -from pydantic import SecretBytes, SecretStr, Extra, PrivateAttr -from pydantic.color import Color -from pydantic import BaseModel, Field -from pymongo import IndexModel - -from beanie import Indexed -from beanie.sync.odm.documents import Document -from beanie.sync.odm.actions import ( - before_event, - after_event, -) -from beanie.odm.actions import ( - Delete, - Insert, - Replace, - Update, - ValidateOnSave, -) -from beanie.sync.odm.fields import Link -from beanie.sync.odm.settings.timeseries import TimeSeriesConfig -from beanie.sync.odm.union_doc import UnionDoc - - -class Option2(BaseModel): - f: float - - -class Option1(BaseModel): - s: str - - -class Nested(BaseModel): - integer: int - option_1: Option1 - union: Union[Option1, Option2] - optional: Optional[Option2] - - -class GeoObject(BaseModel): - type: str = "Point" - coordinates: Tuple[float, float] - - -class Sample(Document): - timestamp: datetime.datetime - increment: Indexed(int) - integer: Indexed(int) - float_num: float - string: str - nested: Nested - optional: Optional[Option2] - union: Union[Option1, Option2] - geo: GeoObject - - -class SubDocument(BaseModel): - test_str: str - test_int: int = 42 - - -class DocumentTestModel(Document): - test_int: int - test_list: List[SubDocument] = Field(hidden=True) - test_doc: SubDocument - test_str: str - - class Settings: - use_cache = True - cache_expiration_time = datetime.timedelta(seconds=10) - cache_capacity = 5 - use_state_management = True - - -class DocumentTestModelWithCustomCollectionName(Document): - test_int: int - test_list: List[SubDocument] - test_str: str - - class Collection: - name = "custom" - - # class Settings: - # name = "custom" - - -class DocumentTestModelWithSimpleIndex(Document): - test_int: Indexed(int) - test_list: List[SubDocument] - test_str: Indexed(str, index_type=pymongo.TEXT) - - -class DocumentTestModelWithIndexFlags(Document): - test_int: Indexed(int, sparse=True) - test_str: Indexed(str, index_type=pymongo.DESCENDING, unique=True) - - -class DocumentTestModelWithIndexFlagsAliases(Document): - test_int: Indexed(int, sparse=True) = Field(alias="testInt") - test_str: Indexed(str, index_type=pymongo.DESCENDING, unique=True) = Field( - alias="testStr" - ) - - -class DocumentTestModelWithComplexIndex(Document): - test_int: int - test_list: List[SubDocument] - test_str: str - - class Collection: - name = "docs_with_index" - indexes = [ - "test_int", - [ - ("test_int", pymongo.ASCENDING), - ("test_str", pymongo.DESCENDING), - ], - IndexModel( - [("test_str", pymongo.DESCENDING)], - name="test_string_index_DESCENDING", - ), - ] - - # class Settings: - # name = "docs_with_index" - # indexes = [ - # "test_int", - # [ - # ("test_int", pymongo.ASCENDING), - # ("test_str", pymongo.DESCENDING), - # ], - # IndexModel( - # [("test_str", pymongo.DESCENDING)], - # name="test_string_index_DESCENDING", - # ), - # ] - - -class DocumentTestModelWithDroppedIndex(Document): - test_int: int - test_list: List[SubDocument] - test_str: str - - class Collection: - name = "docs_with_index" - indexes = [ - "test_int", - ] - - class Settings: - name = "docs_with_index" - indexes = [ - "test_int", - ] - - -class DocumentTestModelStringImport(Document): - test_int: int - - -class DocumentTestModelFailInspection(Document): - test_int_2: int - - class Collection: - name = "DocumentTestModel" - - class Settings: - name = "DocumentTestModel" - - -class DocumentWithCustomIdUUID(Document): - id: UUID = Field(default_factory=uuid4) - name: str - - -class DocumentWithCustomIdInt(Document): - id: int - name: str - - -class DocumentWithCustomFiledsTypes(Document): - color: Color - decimal: Decimal - secret_bytes: SecretBytes - secret_string: SecretStr - ipv4address: IPv4Address - ipv4interface: IPv4Interface - ipv4network: IPv4Network - ipv6address: IPv6Address - ipv6interface: IPv6Interface - ipv6network: IPv6Network - timedelta: datetime.timedelta - set_type: Set[str] - tuple_type: Tuple[int, str] - path: Path - - -class DocumentWithBsonEncodersFiledsTypes(Document): - color: Color - timestamp: datetime.datetime - - class Settings: - bson_encoders = { - Color: lambda c: c.as_rgb(), - datetime.datetime: lambda o: o.isoformat(timespec="microseconds"), - } - - -class DocumentWithActions(Document): - name: str - num_1: int = 0 - num_2: int = 10 - num_3: int = 100 - - class Inner: - inner_num_1 = 0 - inner_num_2 = 0 - - @before_event(Insert) - def capitalize_name(self): - self.name = self.name.capitalize() - - @before_event([Insert, Replace]) - def add_one(self): - self.num_1 += 1 - - @after_event(Insert) - def num_2_change(self): - self.num_2 -= 1 - - @after_event(Replace) - def num_3_change(self): - self.num_3 -= 1 - - @before_event(Delete) - def inner_num_to_one(self): - self.Inner.inner_num_1 = 1 - - @after_event(Delete) - def inner_num_to_two(self): - self.Inner.inner_num_2 = 2 - - @before_event(Update) - def inner_num_to_one_2(self): - self.num_1 += 1 - - @after_event(Update) - def inner_num_to_two_2(self): - self.num_2 -= 1 - - -class DocumentWithActions2(Document): - name: str - num_1: int = 0 - num_2: int = 10 - num_3: int = 100 - - class Inner: - inner_num_1 = 0 - inner_num_2 = 0 - - @before_event(Insert) - def capitalize_name(self): - self.name = self.name.capitalize() - - @before_event(Insert, Replace) - def add_one(self): - self.num_1 += 1 - - @after_event(Insert) - def num_2_change(self): - self.num_2 -= 1 - - @after_event(Replace) - def num_3_change(self): - self.num_3 -= 1 - - @before_event(Delete) - def inner_num_to_one(self): - self.Inner.inner_num_1 = 1 - - @after_event(Delete) - def inner_num_to_two(self): - self.Inner.inner_num_2 = 2 - - @before_event(Update) - def inner_num_to_one_2(self): - self.num_1 += 1 - - @after_event(Update) - def inner_num_to_two_2(self): - self.num_2 -= 1 - - -class InheritedDocumentWithActions(DocumentWithActions): - ... - - -class InternalDoc(BaseModel): - _private_field: str = PrivateAttr(default="TEST_PRIVATE") - num: int = 100 - string: str = "test" - lst: List[int] = [1, 2, 3, 4, 5] - - def change_private(self): - self._private_field = "PRIVATE_CHANGED" - - def get_private(self): - return self._private_field - - -class DocumentWithTurnedOnStateManagement(Document): - num_1: int - num_2: int - internal: InternalDoc - - class Settings: - use_state_management = True - - -class DocumentWithTurnedOnReplaceObjects(Document): - num_1: int - num_2: int - internal: InternalDoc - - class Settings: - use_state_management = True - state_management_replace_objects = True - - -class DocumentWithTurnedOffStateManagement(Document): - num_1: int - num_2: int - - -class DocumentWithValidationOnSave(Document): - num_1: int - num_2: int - - @after_event(ValidateOnSave) - def num_2_plus_1(self): - self.num_2 += 1 - - class Settings: - validate_on_save = True - use_state_management = True - - -class DocumentWithRevisionTurnedOn(Document): - num_1: int - num_2: int - - class Settings: - use_revision = True - use_state_management = True - - -class DocumentWithPydanticConfig(Document): - num_1: int - - class Config(Document.Config): - validate_assignment = True - - -class DocumentWithExtras(Document): - num_1: int - - class Config(Document.Config): - extra = Extra.allow - - -class DocumentWithExtrasKw(Document, extra=Extra.allow): - num_1: int - - -class Window(Document): - x: int - y: int - - -class Door(Document): - t: int = 10 - - -class Roof(Document): - r: int = 100 - - -class House(Document): - windows: List[Link[Window]] - door: Link[Door] - roof: Optional[Link[Roof]] - name: Indexed(str) = Field(hidden=True) - height: Indexed(int) = 2 - - -class DocumentForEncodingTest(Document): - bytes_field: Optional[bytes] - datetime_field: Optional[datetime.datetime] - - -class DocumentWithTimeseries(Document): - ts: datetime.datetime = Field(default_factory=datetime.datetime.now) - - class Settings: - timeseries = TimeSeriesConfig(time_field="ts", expire_after_seconds=2) - - -class DocumentForEncodingTestDate(Document): - date_field: datetime.date = Field(default_factory=datetime.date.today) - - class Settings: - name = "test_date" - bson_encoders = { - datetime.date: lambda dt: datetime.datetime( - year=dt.year, - month=dt.month, - day=dt.day, - hour=0, - minute=0, - second=0, - ) - } - - -class DocumentUnion(UnionDoc): - class Settings: - name = "multi_model" - - -class DocumentMultiModelOne(Document): - int_filed: int = 0 - shared: int = 0 - - class Settings: - union_doc = DocumentUnion - - -class DocumentMultiModelTwo(Document): - str_filed: str = "test" - shared: int = 0 - linked_doc: Optional[Link[DocumentMultiModelOne]] = None - - class Settings: - union_doc = DocumentUnion - - -class WindowWithRevision(Document): - x: int - y: int - - class Settings: - use_revision = True - use_state_management = True - - -class HouseWithRevision(Document): - windows: List[Link[WindowWithRevision]] - - class Settings: - use_revision = True - use_state_management = True diff --git a/tests/sync/odm/__init__.py b/tests/sync/odm/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/sync/odm/documents/__init__.py b/tests/sync/odm/documents/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/sync/odm/documents/test_aggregate.py b/tests/sync/odm/documents/test_aggregate.py deleted file mode 100644 index ba701034..00000000 --- a/tests/sync/odm/documents/test_aggregate.py +++ /dev/null @@ -1,72 +0,0 @@ -from pydantic import Field -from pydantic.main import BaseModel - -from tests.sync.models import DocumentTestModel - - -def test_aggregate(documents): - documents(4, "uno") - documents(2, "dos") - documents(1, "cuatro") - result = DocumentTestModel.aggregate( - [{"$group": {"_id": "$test_str", "total": {"$sum": "$test_int"}}}] - ).to_list() - assert len(result) == 3 - assert {"_id": "cuatro", "total": 0} in result - assert {"_id": "dos", "total": 1} in result - assert {"_id": "uno", "total": 6} in result - - -def test_aggregate_with_filter(documents): - documents(4, "uno") - documents(2, "dos") - documents(1, "cuatro") - result = ( - DocumentTestModel.find(DocumentTestModel.test_int >= 1) - .aggregate( - [{"$group": {"_id": "$test_str", "total": {"$sum": "$test_int"}}}] - ) - .to_list() - ) - assert len(result) == 2 - assert {"_id": "dos", "total": 1} in result - assert {"_id": "uno", "total": 6} in result - - -def test_aggregate_with_item_model(documents): - class OutputItem(BaseModel): - id: str = Field(None, alias="_id") - total: int - - documents(4, "uno") - documents(2, "dos") - documents(1, "cuatro") - ids = [] - for i in DocumentTestModel.aggregate( - [{"$group": {"_id": "$test_str", "total": {"$sum": "$test_int"}}}], - projection_model=OutputItem, - ): - if i.id == "cuatro": - assert i.total == 0 - elif i.id == "dos": - assert i.total == 1 - elif i.id == "uno": - assert i.total == 6 - else: - raise KeyError - ids.append(i.id) - assert set(ids) == {"cuatro", "dos", "uno"} - - -def test_aggregate_with_session(documents, session): - documents(4, "uno") - documents(2, "dos") - documents(1, "cuatro") - result = DocumentTestModel.aggregate( - [{"$group": {"_id": "$test_str", "total": {"$sum": "$test_int"}}}], - session=session, - ).to_list() - assert len(result) == 3 - assert {"_id": "cuatro", "total": 0} in result - assert {"_id": "dos", "total": 1} in result - assert {"_id": "uno", "total": 6} in result diff --git a/tests/sync/odm/documents/test_bulk_write.py b/tests/sync/odm/documents/test_bulk_write.py deleted file mode 100644 index ae178620..00000000 --- a/tests/sync/odm/documents/test_bulk_write.py +++ /dev/null @@ -1,185 +0,0 @@ -import pytest -from pymongo.errors import BulkWriteError - -from beanie.sync.odm.bulk import BulkWriter -from beanie.odm.operators.update.general import Set -from tests.sync.models import DocumentTestModel - - -def test_insert(documents_not_inserted): - documents = documents_not_inserted(2) - with BulkWriter() as bulk_writer: - DocumentTestModel.insert_one(documents[0], bulk_writer=bulk_writer) - DocumentTestModel.insert_one(documents[1], bulk_writer=bulk_writer) - - new_documents = DocumentTestModel.find_all().to_list() - assert len(new_documents) == 2 - - -def test_update(documents, document_not_inserted): - documents(5) - doc = DocumentTestModel.find_one(DocumentTestModel.test_int == 0).run() - doc.test_int = 100 - with BulkWriter() as bulk_writer: - doc.save_changes(bulk_writer=bulk_writer) - DocumentTestModel.find_one(DocumentTestModel.test_int == 1).update( - Set({DocumentTestModel.test_int: 1000}), - bulk_writer=bulk_writer, - ).run() - DocumentTestModel.find(DocumentTestModel.test_int < 100).update( - Set({DocumentTestModel.test_int: 2000}), - bulk_writer=bulk_writer, - ).run() - - assert len(DocumentTestModel.find_all().to_list()) == 5 - assert ( - len( - DocumentTestModel.find(DocumentTestModel.test_int == 100).to_list() - ) - == 1 - ) - assert ( - len( - DocumentTestModel.find( - DocumentTestModel.test_int == 1000 - ).to_list() - ) - == 1 - ) - assert ( - len( - DocumentTestModel.find( - DocumentTestModel.test_int == 2000 - ).to_list() - ) - == 3 - ) - - -def test_delete(documents, document_not_inserted): - documents(5) - doc = DocumentTestModel.find_one(DocumentTestModel.test_int == 0).run() - with BulkWriter() as bulk_writer: - doc.delete(bulk_writer=bulk_writer) - DocumentTestModel.find_one(DocumentTestModel.test_int == 1).delete( - bulk_writer=bulk_writer - ).run() - DocumentTestModel.find(DocumentTestModel.test_int < 4).delete( - bulk_writer=bulk_writer - ).run() - - assert len(DocumentTestModel.find_all().to_list()) == 1 - - -def test_replace(documents, document_not_inserted): - documents(5) - doc = DocumentTestModel.find_one(DocumentTestModel.test_int == 0).run() - doc.test_int = 100 - with BulkWriter() as bulk_writer: - doc.replace(bulk_writer=bulk_writer) - - document_not_inserted.test_int = 100 - - DocumentTestModel.find_one( - DocumentTestModel.test_int == 1 - ).replace_one(document_not_inserted, bulk_writer=bulk_writer) - - assert len(DocumentTestModel.find_all().to_list()) == 5 - assert ( - len( - DocumentTestModel.find(DocumentTestModel.test_int == 100).to_list() - ) - == 2 - ) - - -def test_upsert_find_many_not_found(documents, document_not_inserted): - documents(5) - document_not_inserted.test_int = -10000 - with BulkWriter() as bulk_writer: - DocumentTestModel.find(DocumentTestModel.test_int < -1000).upsert( - {"$set": {DocumentTestModel.test_int: 0}}, - on_insert=document_not_inserted, - ).run() - - bulk_writer.commit() - - assert len(DocumentTestModel.find_all().to_list()) == 6 - assert ( - len( - DocumentTestModel.find( - DocumentTestModel.test_int == -10000 - ).to_list() - ) - == 1 - ) - - -def test_upsert_find_one_not_found(documents, document_not_inserted): - documents(5) - document_not_inserted.test_int = -10000 - with BulkWriter() as bulk_writer: - DocumentTestModel.find_one(DocumentTestModel.test_int < -1000).upsert( - {"$set": {DocumentTestModel.test_int: 0}}, - on_insert=document_not_inserted, - ).run() - - bulk_writer.commit() - - assert len(DocumentTestModel.find_all().to_list()) == 6 - assert ( - len( - DocumentTestModel.find( - DocumentTestModel.test_int == -10000 - ).to_list() - ) - == 1 - ) - - -def test_upsert_find_many_found(documents, document_not_inserted): - documents(5) - with BulkWriter() as bulk_writer: - DocumentTestModel.find(DocumentTestModel.test_int == 1).upsert( - {"$set": {DocumentTestModel.test_int: -10000}}, - on_insert=document_not_inserted, - ).run() - - bulk_writer.commit() - - assert len(DocumentTestModel.find_all().to_list()) == 5 - assert ( - len( - DocumentTestModel.find( - DocumentTestModel.test_int == -10000 - ).to_list() - ) - == 1 - ) - - -def test_upsert_find_one_found(documents, document_not_inserted): - documents(5) - with BulkWriter() as bulk_writer: - DocumentTestModel.find_one(DocumentTestModel.test_int == 1).upsert( - {"$set": {DocumentTestModel.test_int: -10000}}, - on_insert=document_not_inserted, - ).run() - - bulk_writer.commit() - - assert len(DocumentTestModel.find_all().to_list()) == 5 - assert ( - len( - DocumentTestModel.find( - DocumentTestModel.test_int == -10000 - ).to_list() - ) - == 1 - ) - - -def test_internal_error(document): - with pytest.raises(BulkWriteError): - with BulkWriter() as bulk_writer: - DocumentTestModel.insert_one(document, bulk_writer=bulk_writer) diff --git a/tests/sync/odm/documents/test_count.py b/tests/sync/odm/documents/test_count.py deleted file mode 100644 index 01aef81c..00000000 --- a/tests/sync/odm/documents/test_count.py +++ /dev/null @@ -1,15 +0,0 @@ -from tests.sync.models import DocumentTestModel - - -def test_count(documents): - documents(4, "uno", True) - c = DocumentTestModel.count() - assert c == 4 - - -def test_count_with_filter_query(documents): - documents(4, "uno", True) - documents(2, "dos", True) - documents(1, "cuatro", True) - c = DocumentTestModel.find_many({"test_str": "dos"}).count() - assert c == 2 diff --git a/tests/sync/odm/documents/test_create.py b/tests/sync/odm/documents/test_create.py deleted file mode 100644 index c43b5de3..00000000 --- a/tests/sync/odm/documents/test_create.py +++ /dev/null @@ -1,53 +0,0 @@ -import pytest -from pymongo.errors import DuplicateKeyError - -from beanie.odm.fields import PydanticObjectId -from tests.sync.models import DocumentTestModel - - -def test_insert_one(document_not_inserted): - result = DocumentTestModel.insert_one(document_not_inserted) - document = DocumentTestModel.get(result.id).run() - assert document is not None - assert document.test_int == document_not_inserted.test_int - assert document.test_list == document_not_inserted.test_list - assert document.test_str == document_not_inserted.test_str - - -def test_insert_many(documents_not_inserted): - DocumentTestModel.insert_many(documents_not_inserted(10)) - documents = DocumentTestModel.find_all().to_list() - assert len(documents) == 10 - - -def test_create(document_not_inserted): - document_not_inserted.insert() - assert isinstance(document_not_inserted.id, PydanticObjectId) - - -def test_create_twice(document_not_inserted): - document_not_inserted.insert() - with pytest.raises(DuplicateKeyError): - document_not_inserted.insert() - - -def test_insert_one_with_session(document_not_inserted, session): - result = DocumentTestModel.insert_one( - document_not_inserted, session=session - ) - document = DocumentTestModel.get(result.id, session=session).run() - assert document is not None - assert document.test_int == document_not_inserted.test_int - assert document.test_list == document_not_inserted.test_list - assert document.test_str == document_not_inserted.test_str - - -def test_insert_many_with_session(documents_not_inserted, session): - DocumentTestModel.insert_many(documents_not_inserted(10), session=session) - documents = DocumentTestModel.find_all(session=session).to_list() - assert len(documents) == 10 - - -def test_create_with_session(document_not_inserted, session): - document_not_inserted.insert(session=session) - assert isinstance(document_not_inserted.id, PydanticObjectId) diff --git a/tests/sync/odm/documents/test_delete.py b/tests/sync/odm/documents/test_delete.py deleted file mode 100644 index 496384f0..00000000 --- a/tests/sync/odm/documents/test_delete.py +++ /dev/null @@ -1,53 +0,0 @@ -from tests.sync.models import DocumentTestModel - - -def test_delete_one(documents): - documents(4, "uno") - documents(2, "dos") - documents(1, "cuatro") - DocumentTestModel.find_one({"test_str": "uno"}).delete().run() - documents = DocumentTestModel.find_all().to_list() - assert len(documents) == 6 - - -def test_delete_one_not_found(documents): - documents(4, "uno") - documents(2, "dos") - documents(1, "cuatro") - DocumentTestModel.find_one({"test_str": "wrong"}).delete().run() - documents = DocumentTestModel.find_all().to_list() - assert len(documents) == 7 - - -def test_delete_many(documents): - documents(4, "uno") - documents(2, "dos") - documents(1, "cuatro") - DocumentTestModel.find_many({"test_str": "uno"}).delete().run() - documents = DocumentTestModel.find_all().to_list() - assert len(documents) == 3 - - -def test_delete_many_not_found(documents): - documents(4, "uno") - documents(2, "dos") - documents(1, "cuatro") - DocumentTestModel.find_many({"test_str": "wrong"}).delete().run() - documents = DocumentTestModel.find_all().to_list() - assert len(documents) == 7 - - -def test_delete_all(documents): - documents(4, "uno") - documents(2, "dos") - documents(1, "cuatro") - DocumentTestModel.delete_all() - documents = DocumentTestModel.find_all().to_list() - assert len(documents) == 0 - - -def test_delete(document): - doc_id = document.id - document.delete() - new_document = DocumentTestModel.get(doc_id).run() - assert new_document is None diff --git a/tests/sync/odm/documents/test_distinct.py b/tests/sync/odm/documents/test_distinct.py deleted file mode 100644 index 189f9110..00000000 --- a/tests/sync/odm/documents/test_distinct.py +++ /dev/null @@ -1,28 +0,0 @@ -from tests.sync.models import DocumentTestModel - - -def test_distinct_unique(documents, document_not_inserted): - documents(1, "uno") - documents(2, "dos") - documents(3, "cuatro") - expected_result = ["cuatro", "dos", "uno"] - unique_test_strs = DocumentTestModel.distinct("test_str", {}) - assert unique_test_strs == expected_result - document_not_inserted.test_str = "uno" - document_not_inserted.insert() - another_unique_test_strs = DocumentTestModel.distinct("test_str", {}) - assert another_unique_test_strs == expected_result - - -def test_distinct_different_value(documents, document_not_inserted): - documents(1, "uno") - documents(2, "dos") - documents(3, "cuatro") - expected_result = ["cuatro", "dos", "uno"] - unique_test_strs = DocumentTestModel.distinct("test_str", {}) - assert unique_test_strs == expected_result - document_not_inserted.test_str = "diff_val" - document_not_inserted.insert() - another_unique_test_strs = DocumentTestModel.distinct("test_str", {}) - assert not another_unique_test_strs == expected_result - assert another_unique_test_strs == ["cuatro", "diff_val", "dos", "uno"] diff --git a/tests/sync/odm/documents/test_exists.py b/tests/sync/odm/documents/test_exists.py deleted file mode 100644 index 420b726f..00000000 --- a/tests/sync/odm/documents/test_exists.py +++ /dev/null @@ -1,15 +0,0 @@ -from tests.sync.models import DocumentTestModel - - -def test_count_with_filter_query(documents): - documents(4, "uno", True) - documents(2, "dos", True) - documents(1, "cuatro", True) - e = DocumentTestModel.find_many({"test_str": "dos"}).exists() - assert e is True - - e = DocumentTestModel.find_one({"test_str": "dos"}).exists() - assert e is True - - e = DocumentTestModel.find_many({"test_str": "wrong"}).exists() - assert e is False diff --git a/tests/sync/odm/documents/test_find.py b/tests/sync/odm/documents/test_find.py deleted file mode 100644 index 60fa6e21..00000000 --- a/tests/sync/odm/documents/test_find.py +++ /dev/null @@ -1,185 +0,0 @@ -import pymongo - -from beanie.odm.fields import PydanticObjectId -from tests.sync.models import DocumentTestModel - - -def test_get(document): - new_document = DocumentTestModel.get(document.id).run() - assert new_document == document - - -def test_get_not_found(document): - new_document = DocumentTestModel.get(PydanticObjectId()).run() - assert new_document is None - - -def test_find_one(documents): - inserted_one = documents(1, "kipasa") - documents(10, "smthe else") - expected_doc_id = PydanticObjectId(inserted_one[0]) - new_document = DocumentTestModel.find_one({"test_str": "kipasa"}).run() - assert new_document.id == expected_doc_id - - -def test_find_one_not_found(documents): - documents(10, "smthe else") - - new_document = DocumentTestModel.find_one({"test_str": "wrong"}).run() - assert new_document is None - - -def test_find_one_more_than_one_found(documents): - documents(10, "one") - documents(10, "two") - new_document = DocumentTestModel.find_one({"test_str": "one"}).run() - assert new_document.test_str == "one" - - -def test_find_all(documents): - documents(4, "uno") - documents(2, "dos") - documents(1, "cuatro") - result = DocumentTestModel.find_all().to_list() - assert len(result) == 7 - - -def test_find_all_limit(documents): - documents(4, "uno") - documents(2, "dos") - documents(1, "cuatro") - result = DocumentTestModel.find_all(limit=5).to_list() - assert len(result) == 5 - - -def test_find_all_skip(documents): - documents(4, "uno") - documents(2, "dos") - documents(1, "cuatro") - result = DocumentTestModel.find_all(skip=1).to_list() - assert len(result) == 6 - - -def test_find_all_sort(documents): - documents(4, "uno", True) - documents(2, "dos", True) - documents(1, "cuatro", True) - result = DocumentTestModel.find_all( - sort=[ - ("test_str", pymongo.ASCENDING), - ("test_int", pymongo.DESCENDING), - ] - ).to_list() - assert result[0].test_str == "cuatro" - assert result[1].test_str == result[2].test_str == "dos" - assert ( - result[3].test_str - == result[4].test_str - == result[5].test_str - == result[5].test_str - == "uno" - ) - - assert result[1].test_int >= result[2].test_int - assert ( - result[3].test_int - >= result[4].test_int - >= result[5].test_int - >= result[6].test_int - ) - - -def test_find_many(documents): - documents(4, "uno") - documents(2, "dos") - documents(1, "cuatro") - result = DocumentTestModel.find_many( - DocumentTestModel.test_str == "uno" - ).to_list() - assert len(result) == 4 - - -def test_find_many_limit(documents): - documents(4, "uno") - documents(2, "dos") - documents(1, "cuatro") - result = DocumentTestModel.find_many( - {"test_str": "uno"}, limit=2 - ).to_list() - assert len(result) == 2 - - -def test_find_many_skip(documents): - documents(4, "uno") - documents(2, "dos") - documents(1, "cuatro") - result = DocumentTestModel.find_many({"test_str": "uno"}, skip=1).to_list() - assert len(result) == 3 - - -def test_find_many_sort(documents): - documents(4, "uno", True) - documents(2, "dos", True) - documents(1, "cuatro", True) - result = DocumentTestModel.find_many( - {"test_str": "uno"}, sort="test_int" - ).to_list() - assert ( - result[0].test_int - <= result[1].test_int - <= result[2].test_int - <= result[3].test_int - ) - - result = DocumentTestModel.find_many( - {"test_str": "uno"}, sort=[("test_int", pymongo.DESCENDING)] - ).to_list() - assert ( - result[0].test_int - >= result[1].test_int - >= result[2].test_int - >= result[3].test_int - ) - - -def test_find_many_not_found(documents): - documents(4, "uno") - documents(2, "dos") - documents(1, "cuatro") - result = DocumentTestModel.find_many({"test_str": "wrong"}).to_list() - assert len(result) == 0 - - -def test_get_with_session(document, session): - new_document = DocumentTestModel.get(document.id, session=session).run() - assert new_document == document - - -def test_find_one_with_session(documents, session): - inserted_one = documents(1, "kipasa") - documents(10, "smthe else") - - expected_doc_id = PydanticObjectId(inserted_one[0]) - - new_document = DocumentTestModel.find_one( - {"test_str": "kipasa"}, session=session - ).run() - assert new_document.id == expected_doc_id - - -def test_find_all_with_session(documents, session): - documents(4, "uno") - documents(2, "dos") - documents(1, "cuatro") - result = DocumentTestModel.find_all(session=session).to_list() - assert len(result) == 7 - - -def test_find_many_with_session(documents, session): - documents(4, "uno") - documents(2, "dos") - documents(1, "cuatro") - result = DocumentTestModel.find_many( - {"test_str": "uno"}, session=session - ).to_list() - assert len(result) == 4 diff --git a/tests/sync/odm/documents/test_init.py b/tests/sync/odm/documents/test_init.py deleted file mode 100644 index 099d223a..00000000 --- a/tests/sync/odm/documents/test_init.py +++ /dev/null @@ -1,235 +0,0 @@ -import pytest -from motor.motor_asyncio import AsyncIOMotorCollection -from yarl import URL - -from beanie.sync.odm import Document, init_beanie -from beanie.exceptions import CollectionWasNotInitialized -from beanie.sync.odm.utils.projection import get_projection -from tests.sync.models import ( - DocumentTestModel, - DocumentTestModelWithCustomCollectionName, - DocumentTestModelWithIndexFlagsAliases, - DocumentTestModelWithSimpleIndex, - DocumentTestModelWithIndexFlags, - DocumentTestModelWithComplexIndex, - DocumentTestModelStringImport, - DocumentTestModelWithDroppedIndex, -) - - -def test_init_collection_was_not_initialized(): - class NewDocument(Document): - test_str: str - - with pytest.raises(CollectionWasNotInitialized): - NewDocument(test_str="test") - - -def test_init_connection_string(settings): - class NewDocumentCS(Document): - test_str: str - - init_beanie( - connection_string=settings.mongodb_dsn, document_models=[NewDocumentCS] - ) - assert ( - NewDocumentCS.get_motor_collection().database.name - == URL(settings.mongodb_dsn).path[1:] - ) - - -def test_init_wrong_params(settings, db): - class NewDocumentCS(Document): - test_str: str - - with pytest.raises(ValueError): - init_beanie( - database=db, - connection_string=settings.mongodb_dsn, - document_models=[NewDocumentCS], - ) - - with pytest.raises(ValueError): - init_beanie(document_models=[NewDocumentCS]) - - with pytest.raises(ValueError): - init_beanie(connection_string=settings.mongodb_dsn) - - -def test_collection_with_custom_name(): - collection: AsyncIOMotorCollection = ( - DocumentTestModelWithCustomCollectionName.get_motor_collection() - ) - assert collection.name == "custom" - - -def test_simple_index_creation(): - collection: AsyncIOMotorCollection = ( - DocumentTestModelWithSimpleIndex.get_motor_collection() - ) - index_info = collection.index_information() - assert index_info["test_int_1"] == {"key": [("test_int", 1)], "v": 2} - assert index_info["test_str_text"]["key"] == [ - ("_fts", "text"), - ("_ftsx", 1), - ] - - -def test_flagged_index_creation(): - collection: AsyncIOMotorCollection = ( - DocumentTestModelWithIndexFlags.get_motor_collection() - ) - index_info = collection.index_information() - assert index_info["test_int_1"] == { - "key": [("test_int", 1)], - "sparse": True, - "v": 2, - } - assert index_info["test_str_-1"] == { - "key": [("test_str", -1)], - "unique": True, - "v": 2, - } - - -def test_flagged_index_creation_with_alias(): - collection: AsyncIOMotorCollection = ( - DocumentTestModelWithIndexFlagsAliases.get_motor_collection() - ) - index_info = collection.index_information() - assert index_info["testInt_1"] == { - "key": [("testInt", 1)], - "sparse": True, - "v": 2, - } - assert index_info["testStr_-1"] == { - "key": [("testStr", -1)], - "unique": True, - "v": 2, - } - - -def test_complex_index_creation(): - collection: AsyncIOMotorCollection = ( - DocumentTestModelWithComplexIndex.get_motor_collection() - ) - index_info = collection.index_information() - assert index_info == { - "_id_": {"key": [("_id", 1)], "v": 2}, - "test_int_1": {"key": [("test_int", 1)], "v": 2}, - "test_int_1_test_str_-1": { - "key": [("test_int", 1), ("test_str", -1)], - "v": 2, - }, - "test_string_index_DESCENDING": {"key": [("test_str", -1)], "v": 2}, - } - - -def test_index_dropping_is_allowed(db): - init_beanie( - database=db, document_models=[DocumentTestModelWithComplexIndex] - ) - init_beanie( - database=db, - document_models=[DocumentTestModelWithDroppedIndex], - allow_index_dropping=True, - ) - - collection: AsyncIOMotorCollection = ( - DocumentTestModelWithComplexIndex.get_motor_collection() - ) - index_info = collection.index_information() - assert index_info == { - "_id_": {"key": [("_id", 1)], "v": 2}, - "test_int_1": {"key": [("test_int", 1)], "v": 2}, - } - - -def test_index_dropping_is_not_allowed(db): - init_beanie( - database=db, document_models=[DocumentTestModelWithComplexIndex] - ) - init_beanie( - database=db, - document_models=[DocumentTestModelWithDroppedIndex], - allow_index_dropping=False, - ) - - collection: AsyncIOMotorCollection = ( - DocumentTestModelWithComplexIndex.get_motor_collection() - ) - index_info = collection.index_information() - assert index_info == { - "_id_": {"key": [("_id", 1)], "v": 2}, - "test_int_1": {"key": [("test_int", 1)], "v": 2}, - "test_int_1_test_str_-1": { - "key": [("test_int", 1), ("test_str", -1)], - "v": 2, - }, - "test_string_index_DESCENDING": {"key": [("test_str", -1)], "v": 2}, - } - - -def test_index_dropping_is_not_allowed_as_default(db): - init_beanie( - database=db, document_models=[DocumentTestModelWithComplexIndex] - ) - init_beanie( - database=db, - document_models=[DocumentTestModelWithDroppedIndex], - ) - - collection: AsyncIOMotorCollection = ( - DocumentTestModelWithComplexIndex.get_motor_collection() - ) - index_info = collection.index_information() - assert index_info == { - "_id_": {"key": [("_id", 1)], "v": 2}, - "test_int_1": {"key": [("test_int", 1)], "v": 2}, - "test_int_1_test_str_-1": { - "key": [("test_int", 1), ("test_str", -1)], - "v": 2, - }, - "test_string_index_DESCENDING": {"key": [("test_str", -1)], "v": 2}, - } - - -def test_document_string_import(db): - init_beanie( - database=db, - document_models=[ - "tests.sync.models.DocumentTestModelStringImport", - ], - ) - document = DocumentTestModelStringImport(test_int=1) - assert document.id is None - document.insert() - assert document.id is not None - - with pytest.raises(ValueError): - init_beanie( - database=db, - document_models=[ - "tests", - ], - ) - - with pytest.raises(AttributeError): - init_beanie( - database=db, - document_models=[ - "tests.wrong", - ], - ) - - -def test_projection(): - projection = get_projection(DocumentTestModel) - assert projection == { - "_id": 1, - "test_int": 1, - "test_list": 1, - "test_str": 1, - "test_doc": 1, - "revision_id": 1, - } diff --git a/tests/sync/odm/documents/test_inspect.py b/tests/sync/odm/documents/test_inspect.py deleted file mode 100644 index f64636f6..00000000 --- a/tests/sync/odm/documents/test_inspect.py +++ /dev/null @@ -1,30 +0,0 @@ -from beanie.odm.models import InspectionStatuses -from tests.sync.models import ( - DocumentTestModel, - DocumentTestModelFailInspection, -) - - -def test_inspect_ok(documents): - documents(10, "smth") - result = DocumentTestModel.inspect_collection() - assert result.status == InspectionStatuses.OK - assert result.errors == [] - - -def test_inspect_fail(documents): - documents(10, "smth") - result = DocumentTestModelFailInspection.inspect_collection() - assert result.status == InspectionStatuses.FAIL - assert len(result.errors) == 10 - assert ( - "1 validation error for DocumentTestModelFailInspection" - in result.errors[0].error - ) - - -def test_inspect_ok_with_session(documents, session): - documents(10, "smth") - result = DocumentTestModel.inspect_collection(session=session) - assert result.status == InspectionStatuses.OK - assert result.errors == [] diff --git a/tests/sync/odm/documents/test_multi_model.py b/tests/sync/odm/documents/test_multi_model.py deleted file mode 100644 index 9324400f..00000000 --- a/tests/sync/odm/documents/test_multi_model.py +++ /dev/null @@ -1,67 +0,0 @@ -from tests.sync.models import ( - DocumentMultiModelOne, - DocumentMultiModelTwo, - DocumentUnion, -) - - -class TestMultiModel: - def test_multi_model(self): - doc_1 = DocumentMultiModelOne().insert() - doc_2 = DocumentMultiModelTwo().insert() - - new_doc_1 = DocumentMultiModelOne.get(doc_1.id).run() - new_doc_2 = DocumentMultiModelTwo.get(doc_2.id).run() - - assert new_doc_1 is not None - assert new_doc_2 is not None - - new_doc_1 = DocumentMultiModelTwo.get(doc_1.id).run() - new_doc_2 = DocumentMultiModelOne.get(doc_2.id).run() - - assert new_doc_1 is None - assert new_doc_2 is None - - new_docs_1 = DocumentMultiModelOne.find({}).to_list() - new_docs_2 = DocumentMultiModelTwo.find({}).to_list() - - assert len(new_docs_1) == 1 - assert len(new_docs_2) == 1 - - DocumentMultiModelOne.update_all({"$set": {"shared": 100}}).run() - - new_doc_1 = DocumentMultiModelOne.get(doc_1.id).run() - new_doc_2 = DocumentMultiModelTwo.get(doc_2.id).run() - - assert new_doc_1.shared == 100 - assert new_doc_2.shared == 0 - - def test_union_doc(self): - DocumentMultiModelOne().insert() - DocumentMultiModelTwo().insert() - DocumentMultiModelOne().insert() - DocumentMultiModelTwo().insert() - - docs = DocumentUnion.all().to_list() - assert isinstance(docs[0], DocumentMultiModelOne) - assert isinstance(docs[1], DocumentMultiModelTwo) - assert isinstance(docs[2], DocumentMultiModelOne) - assert isinstance(docs[3], DocumentMultiModelTwo) - - def test_union_doc_aggregation(self): - DocumentMultiModelOne().insert() - DocumentMultiModelTwo().insert() - DocumentMultiModelOne().insert() - DocumentMultiModelTwo().insert() - - docs = DocumentUnion.aggregate( - [{"$match": {"$expr": {"$eq": ["$int_filed", 0]}}}] - ).to_list() - assert len(docs) == 2 - - def test_union_doc_link(self): - doc_1 = DocumentMultiModelOne().insert() - DocumentMultiModelTwo(linked_doc=doc_1).insert() - - docs = DocumentMultiModelTwo.find({}, fetch_links=True).to_list() - assert isinstance(docs[0].linked_doc, DocumentMultiModelOne) diff --git a/tests/sync/odm/documents/test_pydantic_config.py b/tests/sync/odm/documents/test_pydantic_config.py deleted file mode 100644 index c0063157..00000000 --- a/tests/sync/odm/documents/test_pydantic_config.py +++ /dev/null @@ -1,10 +0,0 @@ -import pytest -from pydantic import ValidationError - -from tests.sync.models import DocumentWithPydanticConfig - - -def test_pydantic_config(): - doc = DocumentWithPydanticConfig(num_1=2) - with pytest.raises(ValidationError): - doc.num_1 = "wrong" diff --git a/tests/sync/odm/documents/test_pydantic_extras.py b/tests/sync/odm/documents/test_pydantic_extras.py deleted file mode 100644 index 8222c617..00000000 --- a/tests/sync/odm/documents/test_pydantic_extras.py +++ /dev/null @@ -1,34 +0,0 @@ -import pytest - -from tests.sync.models import ( - DocumentWithExtras, - DocumentWithPydanticConfig, - DocumentWithExtrasKw, -) - - -def test_pydantic_extras(): - doc = DocumentWithExtras(num_1=2) - doc.extra_value = "foo" - doc.save() - - loaded_doc = DocumentWithExtras.get(doc.id).run() - - assert loaded_doc.extra_value == "foo" - - -@pytest.mark.skip(reason="setting extra to allow via class kwargs not working") -def test_pydantic_extras_kw(): - doc = DocumentWithExtrasKw(num_1=2) - doc.extra_value = "foo" - doc.save() - - loaded_doc = DocumentWithExtras.get(doc.id).run() - - assert loaded_doc.extra_value == "foo" - - -def test_fail_with_no_extras(): - doc = DocumentWithPydanticConfig(num_1=2) - with pytest.raises(ValueError): - doc.extra_value = "foo" diff --git a/tests/sync/odm/documents/test_replace.py b/tests/sync/odm/documents/test_replace.py deleted file mode 100644 index 4e904ec7..00000000 --- a/tests/sync/odm/documents/test_replace.py +++ /dev/null @@ -1,30 +0,0 @@ -from tests.sync.models import Sample - - -def test_replace_one(preset_documents): - count_1_before = Sample.find_many(Sample.integer == 1).count() - count_2_before = Sample.find_many(Sample.integer == 2).count() - - a_2 = Sample.find_one(Sample.integer == 2).run() - Sample.find_one(Sample.integer == 1).replace_one(a_2) - - count_1_after = Sample.find_many(Sample.integer == 1).count() - count_2_after = Sample.find_many(Sample.integer == 2).count() - - assert count_1_after == count_1_before - 1 - assert count_2_after == count_2_before + 1 - - -def test_replace_self(preset_documents): - count_1_before = Sample.find_many(Sample.integer == 1).count() - count_2_before = Sample.find_many(Sample.integer == 2).count() - - a_1 = Sample.find_one(Sample.integer == 1).run() - a_1.integer = 2 - a_1.replace() - - count_1_after = Sample.find_many(Sample.integer == 1).count() - count_2_after = Sample.find_many(Sample.integer == 2).count() - - assert count_1_after == count_1_before - 1 - assert count_2_after == count_2_before + 1 diff --git a/tests/sync/odm/documents/test_revision.py b/tests/sync/odm/documents/test_revision.py deleted file mode 100644 index b8a8a2f5..00000000 --- a/tests/sync/odm/documents/test_revision.py +++ /dev/null @@ -1,58 +0,0 @@ -import pytest - -from beanie.exceptions import RevisionIdWasChanged -from tests.sync.models import DocumentWithRevisionTurnedOn - - -def test_replace(): - doc = DocumentWithRevisionTurnedOn(num_1=1, num_2=2) - doc.insert() - - doc.num_1 = 2 - doc.replace() - - doc.num_2 = 3 - doc.replace() - - for i in range(5): - found_doc = DocumentWithRevisionTurnedOn.get(doc.id).run() - found_doc.num_1 += 1 - found_doc.replace() - - doc._previous_revision_id = "wrong" - doc.num_1 = 4 - with pytest.raises(RevisionIdWasChanged): - doc.replace() - - doc.replace(ignore_revision=True) - - -def test_update(): - doc = DocumentWithRevisionTurnedOn(num_1=1, num_2=2) - doc.insert() - - doc.num_1 = 2 - doc.save_changes() - - doc.num_2 = 3 - doc.save_changes() - - for i in range(5): - found_doc = DocumentWithRevisionTurnedOn.get(doc.id).run() - found_doc.num_1 += 1 - found_doc.save_changes() - - doc._previous_revision_id = "wrong" - doc.num_1 = 4 - with pytest.raises(RevisionIdWasChanged): - doc.save_changes() - - doc.save_changes(ignore_revision=True) - - -def test_empty_update(): - doc = DocumentWithRevisionTurnedOn(num_1=1, num_2=2) - doc.insert() - - # This fails with RevisionIdWasChanged - doc.update({"$set": {"num_1": 1}}) diff --git a/tests/sync/odm/documents/test_update.py b/tests/sync/odm/documents/test_update.py deleted file mode 100644 index 89661478..00000000 --- a/tests/sync/odm/documents/test_update.py +++ /dev/null @@ -1,223 +0,0 @@ -import pytest - -from beanie.exceptions import ( - DocumentNotFound, - ReplaceError, -) -from beanie.odm.fields import PydanticObjectId -from tests.sync.models import DocumentTestModel - - -# REPLACE - -# -# def test_replace_one(document): -# new_doc = DocumentTestModel( -# test_int=0, test_str="REPLACED_VALUE", test_list=[] -# ) -# DocumentTestModel.replace_one({"_id": document.id}, new_doc) -# new_document = DocumentTestModel.get(document.id) -# assert new_document.test_str == "REPLACED_VALUE" - - -def test_replace_many(documents): - documents(10, "foo") - created_documents = DocumentTestModel.find_many( - {"test_str": "foo"} - ).to_list() - to_replace = [] - for document in created_documents[:5]: - document.test_str = "REPLACED_VALUE" - to_replace.append(document) - DocumentTestModel.replace_many(to_replace) - - replaced_documetns = DocumentTestModel.find_many( - {"test_str": "REPLACED_VALUE"} - ).to_list() - assert len(replaced_documetns) == 5 - - -def test_replace_many_not_all_the_docs_found(documents): - documents(10, "foo") - created_documents = DocumentTestModel.find_many( - {"test_str": "foo"} - ).to_list() - to_replace = [] - created_documents[0].id = PydanticObjectId() - for document in created_documents[:5]: - document.test_str = "REPLACED_VALUE" - to_replace.append(document) - with pytest.raises(ReplaceError): - DocumentTestModel.replace_many(to_replace) - - -def test_replace(document): - update_data = {"test_str": "REPLACED_VALUE"} - new_doc = document.copy(update=update_data) - # document.test_str = "REPLACED_VALUE" - new_doc.replace() - new_document = DocumentTestModel.get(document.id).run() - assert new_document.test_str == "REPLACED_VALUE" - - -def test_replace_not_saved(document_not_inserted): - with pytest.raises(ValueError): - document_not_inserted.replace() - - -def test_replace_not_found(document_not_inserted): - document_not_inserted.id = PydanticObjectId() - with pytest.raises(DocumentNotFound): - document_not_inserted.replace() - - -# SAVE -def test_save(document): - update_data = {"test_str": "REPLACED_VALUE"} - new_doc = document.copy(update=update_data) - # document.test_str = "REPLACED_VALUE" - new_doc.save() - new_document = DocumentTestModel.get(document.id).run() - assert new_document.test_str == "REPLACED_VALUE" - - -def test_save_not_saved(document_not_inserted): - document_not_inserted.save() - assert ( - hasattr(document_not_inserted, "id") - and document_not_inserted.id is not None - ) - from_db = DocumentTestModel.get(document_not_inserted.id).run() - assert from_db == document_not_inserted - - -def test_save_not_found(document_not_inserted): - document_not_inserted.id = PydanticObjectId() - document_not_inserted.save() - assert ( - hasattr(document_not_inserted, "id") - and document_not_inserted.id is not None - ) - from_db = DocumentTestModel.get(document_not_inserted.id).run() - assert from_db == document_not_inserted - - -# UPDATE - - -def test_update_one(document): - DocumentTestModel.find_one( - {"_id": document.id, "test_list.test_str": "foo"} - ).update({"$set": {"test_list.$.test_str": "foo_foo"}}).run() - new_document = DocumentTestModel.get(document.id).run() - assert new_document.test_list[0].test_str == "foo_foo" - - -def test_update_many(documents): - documents(10, "foo") - documents(7, "bar") - DocumentTestModel.find_many({"test_str": "foo"}).update( - {"$set": {"test_str": "bar"}} - ).run() - bar_documetns = DocumentTestModel.find_many({"test_str": "bar"}).to_list() - assert len(bar_documetns) == 17 - foo_documetns = DocumentTestModel.find_many({"test_str": "foo"}).to_list() - assert len(foo_documetns) == 0 - - -def test_update_all(documents): - documents(10, "foo") - documents(7, "bar") - DocumentTestModel.update_all( - {"$set": {"test_str": "smth_else"}}, - ).run() - bar_documetns = DocumentTestModel.find_many({"test_str": "bar"}).to_list() - assert len(bar_documetns) == 0 - foo_documetns = DocumentTestModel.find_many({"test_str": "foo"}).to_list() - assert len(foo_documetns) == 0 - smth_else_documetns = DocumentTestModel.find_many( - {"test_str": "smth_else"} - ).to_list() - assert len(smth_else_documetns) == 17 - - -# WITH SESSION - - -# def test_update_with_session(document: DocumentTestModel, session): -# buf_len = len(document.test_list) -# to_insert = SubDocument(test_str="test") -# document.update( -# update_query={"$push": {"test_list": to_insert.dict()}}, -# session=session, -# ) -# new_document = DocumentTestModel.get(document.id, session=session) -# assert len(new_document.test_list) == buf_len + 1 -# -# -# def test_replace_one_with_session(document, session): -# new_doc = DocumentTestModel( -# test_int=0, test_str="REPLACED_VALUE", test_list=[] -# ) -# DocumentTestModel.replace_one( -# {"_id": document.id}, new_doc, session=session -# ) -# new_document = DocumentTestModel.get(document.id, session=session) -# assert new_document.test_str == "REPLACED_VALUE" -# -# -# def test_replace_with_session(document, session): -# update_data = {"test_str": "REPLACED_VALUE"} -# new_doc: DocumentTestModel = document.copy(update=update_data) -# # document.test_str = "REPLACED_VALUE" -# new_doc.replace(session=session) -# new_document = DocumentTestModel.get(document.id, session=session) -# assert new_document.test_str == "REPLACED_VALUE" -# -# -# def test_update_one_with_session(document, session): -# DocumentTestModel.update_one( -# update_query={"$set": {"test_list.$.test_str": "foo_foo"}}, -# filter_query={"_id": document.id, "test_list.test_str": "foo"}, -# session=session, -# ) -# new_document = DocumentTestModel.get(document.id, session=session) -# assert new_document.test_list[0].test_str == "foo_foo" -# -# -# def test_update_many_with_session(documents, session): -# documents(10, "foo") -# documents(7, "bar") -# DocumentTestModel.update_many( -# update_query={"$set": {"test_str": "bar"}}, -# filter_query={"test_str": "foo"}, -# session=session, -# ) -# bar_documetns = DocumentTestModel.find_many( -# {"test_str": "bar"}, session=session -# ).to_list() -# assert len(bar_documetns) == 17 -# foo_documetns = DocumentTestModel.find_many( -# {"test_str": "foo"}, session=session -# ).to_list() -# assert len(foo_documetns) == 0 -# -# -# def test_update_all_with_session(documents, session): -# documents(10, "foo") -# documents(7, "bar") -# DocumentTestModel.update_all( -# update_query={"$set": {"test_str": "smth_else"}}, session=session -# ) -# bar_documetns = DocumentTestModel.find_many( -# {"test_str": "bar"}, session=session -# ).to_list() -# assert len(bar_documetns) == 0 -# foo_documetns = DocumentTestModel.find_many( -# {"test_str": "foo"}, session=session -# ).to_list() -# assert len(foo_documetns) == 0 -# smth_else_documetns = DocumentTestModel.find_many( -# {"test_str": "smth_else"}, session=session -# ).to_list() -# assert len(smth_else_documetns) == 17 diff --git a/tests/sync/odm/documents/test_validation_on_save.py b/tests/sync/odm/documents/test_validation_on_save.py deleted file mode 100644 index dd202ad7..00000000 --- a/tests/sync/odm/documents/test_validation_on_save.py +++ /dev/null @@ -1,39 +0,0 @@ -import pytest -from pydantic import ValidationError - -from tests.sync.models import DocumentWithValidationOnSave - - -def test_validate_on_insert(): - doc = DocumentWithValidationOnSave(num_1=1, num_2=2) - doc.num_1 = "wrong_value" - with pytest.raises(ValidationError): - doc.insert() - - -def test_validate_on_replace(): - doc = DocumentWithValidationOnSave(num_1=1, num_2=2) - doc.insert() - doc.num_1 = "wrong_value" - with pytest.raises(ValidationError): - doc.replace() - - -def test_validate_on_save_changes(): - doc = DocumentWithValidationOnSave(num_1=1, num_2=2) - doc.insert() - doc.num_1 = "wrong_value" - with pytest.raises(ValidationError): - doc.save_changes() - - -def test_validate_on_save_action(): - doc = DocumentWithValidationOnSave(num_1=1, num_2=2) - doc.insert() - assert doc.num_2 == 3 - - -def test_validate_on_save_skip_action(): - doc = DocumentWithValidationOnSave(num_1=1, num_2=2) - doc.insert(skip_actions=["num_2_plus_1"]) - assert doc.num_2 == 2 diff --git a/tests/sync/odm/operators/__init__.py b/tests/sync/odm/operators/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/sync/odm/operators/find/__init__.py b/tests/sync/odm/operators/find/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/sync/odm/operators/find/test_array.py b/tests/sync/odm/operators/find/test_array.py deleted file mode 100644 index 106c9dd4..00000000 --- a/tests/sync/odm/operators/find/test_array.py +++ /dev/null @@ -1,17 +0,0 @@ -from beanie.odm.operators.find.array import All, ElemMatch, Size -from tests.sync.models import Sample - - -def test_all(): - q = All(Sample.integer, [1, 2, 3]) - assert q == {"integer": {"$all": [1, 2, 3]}} - - -def test_elem_match(): - q = ElemMatch(Sample.integer, {"a": "b"}) - assert q == {"integer": {"$elemMatch": {"a": "b"}}} - - -def test_size(): - q = Size(Sample.integer, 4) - assert q == {"integer": {"$size": 4}} diff --git a/tests/sync/odm/operators/find/test_bitwise.py b/tests/sync/odm/operators/find/test_bitwise.py deleted file mode 100644 index 64aef180..00000000 --- a/tests/sync/odm/operators/find/test_bitwise.py +++ /dev/null @@ -1,27 +0,0 @@ -from beanie.odm.operators.find.bitwise import ( - BitsAllClear, - BitsAllSet, - BitsAnyClear, - BitsAnySet, -) -from tests.sync.models import Sample - - -def test_bits_all_clear(): - q = BitsAllClear(Sample.integer, "smth") - assert q == {"integer": {"$bitsAllClear": "smth"}} - - -def test_bits_all_set(): - q = BitsAllSet(Sample.integer, "smth") - assert q == {"integer": {"$bitsAllSet": "smth"}} - - -def test_any_clear(): - q = BitsAnyClear(Sample.integer, "smth") - assert q == {"integer": {"$bitsAnyClear": "smth"}} - - -def test_any_set(): - q = BitsAnySet(Sample.integer, "smth") - assert q == {"integer": {"$bitsAnySet": "smth"}} diff --git a/tests/sync/odm/operators/find/test_comparison.py b/tests/sync/odm/operators/find/test_comparison.py deleted file mode 100644 index 261e6a7a..00000000 --- a/tests/sync/odm/operators/find/test_comparison.py +++ /dev/null @@ -1,93 +0,0 @@ -from beanie.odm.operators.find.comparison import ( - Eq, - GT, - GTE, - In, - LT, - LTE, - NE, - NotIn, -) -from tests.sync.models import Sample - - -def test_eq(): - q = Sample.integer == 1 - assert q == {"integer": 1} - - q = Eq(Sample.integer, 1) - assert q == {"integer": 1} - - q = Eq("integer", 1) - assert q == {"integer": 1} - - -def test_gt(): - q = Sample.integer > 1 - assert q == {"integer": {"$gt": 1}} - - q = GT(Sample.integer, 1) - assert q == {"integer": {"$gt": 1}} - - q = GT("integer", 1) - assert q == {"integer": {"$gt": 1}} - - -def test_gte(): - q = Sample.integer >= 1 - assert q == {"integer": {"$gte": 1}} - - q = GTE(Sample.integer, 1) - assert q == {"integer": {"$gte": 1}} - - q = GTE("integer", 1) - assert q == {"integer": {"$gte": 1}} - - -def test_in(): - q = In(Sample.integer, [1]) - assert q == {"integer": {"$in": [1]}} - - q = In(Sample.integer, [1]) - assert q == {"integer": {"$in": [1]}} - - -def test_lt(): - q = Sample.integer < 1 - assert q == {"integer": {"$lt": 1}} - - q = LT(Sample.integer, 1) - assert q == {"integer": {"$lt": 1}} - - q = LT("integer", 1) - assert q == {"integer": {"$lt": 1}} - - -def test_lte(): - q = Sample.integer <= 1 - assert q == {"integer": {"$lte": 1}} - - q = LTE(Sample.integer, 1) - assert q == {"integer": {"$lte": 1}} - - q = LTE("integer", 1) - assert q == {"integer": {"$lte": 1}} - - -def test_ne(): - q = Sample.integer != 1 - assert q == {"integer": {"$ne": 1}} - - q = NE(Sample.integer, 1) - assert q == {"integer": {"$ne": 1}} - - q = NE("integer", 1) - assert q == {"integer": {"$ne": 1}} - - -def test_nin(): - q = NotIn(Sample.integer, [1]) - assert q == {"integer": {"$nin": [1]}} - - q = NotIn(Sample.integer, [1]) - assert q == {"integer": {"$nin": [1]}} diff --git a/tests/sync/odm/operators/find/test_element.py b/tests/sync/odm/operators/find/test_element.py deleted file mode 100644 index 08b27928..00000000 --- a/tests/sync/odm/operators/find/test_element.py +++ /dev/null @@ -1,18 +0,0 @@ -from beanie.odm.operators.find.element import Exists, Type -from tests.sync.models import Sample - - -def test_exists(): - q = Exists(Sample.integer, True) - assert q == {"integer": {"$exists": True}} - - q = Exists(Sample.integer, False) - assert q == {"integer": {"$exists": False}} - - q = Exists(Sample.integer) - assert q == {"integer": {"$exists": True}} - - -def test_type(): - q = Type(Sample.integer, "smth") - assert q == {"integer": {"$type": "smth"}} diff --git a/tests/sync/odm/operators/find/test_evaluation.py b/tests/sync/odm/operators/find/test_evaluation.py deleted file mode 100644 index 62517330..00000000 --- a/tests/sync/odm/operators/find/test_evaluation.py +++ /dev/null @@ -1,73 +0,0 @@ -from beanie.odm.operators.find.evaluation import ( - Expr, - JsonSchema, - Mod, - RegEx, - Text, - Where, -) -from tests.sync.models import Sample - - -def test_expr(): - q = Expr({"a": "B"}) - assert q == {"$expr": {"a": "B"}} - - -def test_json_schema(): - q = JsonSchema({"a": "B"}) - assert q == {"$jsonSchema": {"a": "B"}} - - -def test_mod(): - q = Mod(Sample.integer, 3, 2) - assert q == {"integer": {"$mod": [3, 2]}} - - -def test_regex(): - q = RegEx(Sample.integer, "smth") - assert q == {"integer": {"$regex": "smth"}} - - q = RegEx(Sample.integer, "smth", "options") - assert q == {"integer": {"$regex": "smth", "$options": "options"}} - - -def test_text(): - q = Text("something") - assert q == { - "$text": { - "$search": "something", - "$caseSensitive": False, - "$diacriticSensitive": False, - } - } - q = Text("something", case_sensitive=True) - assert q == { - "$text": { - "$search": "something", - "$caseSensitive": True, - "$diacriticSensitive": False, - } - } - q = Text("something", diacritic_sensitive=True) - assert q == { - "$text": { - "$search": "something", - "$caseSensitive": False, - "$diacriticSensitive": True, - } - } - q = Text("something", language="test") - assert q == { - "$text": { - "$search": "something", - "$caseSensitive": False, - "$diacriticSensitive": False, - "$language": "test", - } - } - - -def test_where(): - q = Where("test") - assert q == {"$where": "test"} diff --git a/tests/sync/odm/operators/find/test_geospatial.py b/tests/sync/odm/operators/find/test_geospatial.py deleted file mode 100644 index b112ee3e..00000000 --- a/tests/sync/odm/operators/find/test_geospatial.py +++ /dev/null @@ -1,115 +0,0 @@ -from beanie.odm.operators.find.geospatial import ( - GeoIntersects, - GeoWithin, - Near, - NearSphere, -) -from tests.sync.models import Sample - - -def test_geo_intersects(): - q = GeoIntersects( - Sample.geo, geo_type="Polygon", coordinates=[[1, 1], [2, 2], [3, 3]] - ) - assert q == { - "geo": { - "$geoIntersects": { - "$geometry": { - "type": "Polygon", - "coordinates": [[1, 1], [2, 2], [3, 3]], - } - } - } - } - - -def test_geo_within(): - q = GeoWithin( - Sample.geo, geo_type="Polygon", coordinates=[[1, 1], [2, 2], [3, 3]] - ) - assert q == { - "geo": { - "$geoWithin": { - "$geometry": { - "type": "Polygon", - "coordinates": [[1, 1], [2, 2], [3, 3]], - } - } - } - } - - -def test_near(): - q = Near(Sample.geo, longitude=1.1, latitude=2.2) - assert q == { - "geo": { - "$near": { - "$geometry": {"type": "Point", "coordinates": [1.1, 2.2]} - } - } - } - - q = Near(Sample.geo, longitude=1.1, latitude=2.2, max_distance=1) - assert q == { - "geo": { - "$near": { - "$geometry": {"type": "Point", "coordinates": [1.1, 2.2]}, - "$maxDistance": 1, - } - } - } - - q = Near( - Sample.geo, - longitude=1.1, - latitude=2.2, - max_distance=1, - min_distance=0.5, - ) - assert q == { - "geo": { - "$near": { - "$geometry": {"type": "Point", "coordinates": [1.1, 2.2]}, - "$maxDistance": 1, - "$minDistance": 0.5, - } - } - } - - -def test_near_sphere(): - q = NearSphere(Sample.geo, longitude=1.1, latitude=2.2) - assert q == { - "geo": { - "$nearSphere": { - "$geometry": {"type": "Point", "coordinates": [1.1, 2.2]} - } - } - } - - q = NearSphere(Sample.geo, longitude=1.1, latitude=2.2, max_distance=1) - assert q == { - "geo": { - "$nearSphere": { - "$geometry": {"type": "Point", "coordinates": [1.1, 2.2]}, - "$maxDistance": 1, - } - } - } - - q = NearSphere( - Sample.geo, - longitude=1.1, - latitude=2.2, - max_distance=1, - min_distance=0.5, - ) - assert q == { - "geo": { - "$nearSphere": { - "$geometry": {"type": "Point", "coordinates": [1.1, 2.2]}, - "$maxDistance": 1, - "$minDistance": 0.5, - } - } - } diff --git a/tests/sync/odm/operators/find/test_logical.py b/tests/sync/odm/operators/find/test_logical.py deleted file mode 100644 index 9b31523b..00000000 --- a/tests/sync/odm/operators/find/test_logical.py +++ /dev/null @@ -1,31 +0,0 @@ -from beanie.odm.operators.find.logical import And, Not, Nor, Or -from tests.sync.models import Sample - - -def test_and(): - q = And(Sample.integer == 1) - assert q == {"integer": 1} - - q = And(Sample.integer == 1, Sample.nested.integer > 3) - assert q == {"$and": [{"integer": 1}, {"nested.integer": {"$gt": 3}}]} - - -def test_not(): - q = Not(Sample.integer == 1) - assert q == {"$not": {"integer": 1}} - - -def test_nor(): - q = Nor(Sample.integer == 1) - assert q == {"$nor": [{"integer": 1}]} - - q = Nor(Sample.integer == 1, Sample.nested.integer > 3) - assert q == {"$nor": [{"integer": 1}, {"nested.integer": {"$gt": 3}}]} - - -def test_or(): - q = Or(Sample.integer == 1) - assert q == {"integer": 1} - - q = Or(Sample.integer == 1, Sample.nested.integer > 3) - assert q == {"$or": [{"integer": 1}, {"nested.integer": {"$gt": 3}}]} diff --git a/tests/sync/odm/operators/update/__init__.py b/tests/sync/odm/operators/update/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/sync/odm/operators/update/test_array.py b/tests/sync/odm/operators/update/test_array.py deleted file mode 100644 index cf5cb22f..00000000 --- a/tests/sync/odm/operators/update/test_array.py +++ /dev/null @@ -1,33 +0,0 @@ -from beanie.odm.operators.update.array import ( - AddToSet, - Pull, - PullAll, - Pop, - Push, -) -from tests.sync.models import Sample - - -def test_add_to_set(): - q = AddToSet({Sample.integer: 2}) - assert q == {"$addToSet": {"integer": 2}} - - -def test_pop(): - q = Pop({Sample.integer: 2}) - assert q == {"$pop": {"integer": 2}} - - -def test_pull(): - q = Pull({Sample.integer: 2}) - assert q == {"$pull": {"integer": 2}} - - -def test_push(): - q = Push({Sample.integer: 2}) - assert q == {"$push": {"integer": 2}} - - -def test_pull_all(): - q = PullAll({Sample.integer: 2}) - assert q == {"$pullAll": {"integer": 2}} diff --git a/tests/sync/odm/operators/update/test_bitwise.py b/tests/sync/odm/operators/update/test_bitwise.py deleted file mode 100644 index a83115ce..00000000 --- a/tests/sync/odm/operators/update/test_bitwise.py +++ /dev/null @@ -1,7 +0,0 @@ -from beanie.odm.operators.update.bitwise import Bit -from tests.sync.models import Sample - - -def test_bit(): - q = Bit({Sample.integer: 2}) - assert q == {"$bit": {"integer": 2}} diff --git a/tests/sync/odm/operators/update/test_general.py b/tests/sync/odm/operators/update/test_general.py deleted file mode 100644 index af8d4a2d..00000000 --- a/tests/sync/odm/operators/update/test_general.py +++ /dev/null @@ -1,57 +0,0 @@ -from beanie.odm.operators.update.general import ( - Set, - CurrentDate, - Inc, - Unset, - SetOnInsert, - Rename, - Mul, - Max, - Min, -) -from tests.sync.models import Sample - - -def test_set(): - q = Set({Sample.integer: 2}) - assert q == {"$set": {"integer": 2}} - - -def test_current_date(): - q = CurrentDate({Sample.integer: 2}) - assert q == {"$currentDate": {"integer": 2}} - - -def test_inc(): - q = Inc({Sample.integer: 2}) - assert q == {"$inc": {"integer": 2}} - - -def test_min(): - q = Min({Sample.integer: 2}) - assert q == {"$min": {"integer": 2}} - - -def test_max(): - q = Max({Sample.integer: 2}) - assert q == {"$max": {"integer": 2}} - - -def test_mul(): - q = Mul({Sample.integer: 2}) - assert q == {"$mul": {"integer": 2}} - - -def test_rename(): - q = Rename({Sample.integer: 2}) - assert q == {"$rename": {"integer": 2}} - - -def test_set_on_insert(): - q = SetOnInsert({Sample.integer: 2}) - assert q == {"$setOnInsert": {"integer": 2}} - - -def test_unset(): - q = Unset({Sample.integer: 2}) - assert q == {"$unset": {"integer": 2}} diff --git a/tests/sync/odm/query/__init__.py b/tests/sync/odm/query/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/sync/odm/query/test_aggregate.py b/tests/sync/odm/query/test_aggregate.py deleted file mode 100644 index bfa24fdf..00000000 --- a/tests/sync/odm/query/test_aggregate.py +++ /dev/null @@ -1,98 +0,0 @@ -import pytest -from pydantic import Field -from pydantic.main import BaseModel -from pymongo.errors import OperationFailure - -from tests.sync.models import Sample - - -def test_aggregate(preset_documents): - q = Sample.aggregate( - [{"$group": {"_id": "$string", "total": {"$sum": "$integer"}}}] - ) - assert q.get_aggregation_pipeline() == [ - {"$group": {"_id": "$string", "total": {"$sum": "$integer"}}} - ] - result = q.to_list() - assert len(result) == 4 - assert {"_id": "test_3", "total": 3} in result - assert {"_id": "test_1", "total": 3} in result - assert {"_id": "test_0", "total": 0} in result - assert {"_id": "test_2", "total": 6} in result - - -def test_aggregate_with_filter(preset_documents): - q = Sample.find(Sample.increment >= 4).aggregate( - [{"$group": {"_id": "$string", "total": {"$sum": "$integer"}}}] - ) - assert q.get_aggregation_pipeline() == [ - {"$match": {"increment": {"$gte": 4}}}, - {"$group": {"_id": "$string", "total": {"$sum": "$integer"}}}, - ] - result = q.to_list() - assert len(result) == 3 - assert {"_id": "test_1", "total": 2} in result - assert {"_id": "test_2", "total": 6} in result - assert {"_id": "test_3", "total": 3} in result - - -def test_aggregate_with_projection_model(preset_documents): - class OutputItem(BaseModel): - id: str = Field(None, alias="_id") - total: int - - ids = [] - q = Sample.find(Sample.increment >= 4).aggregate( - [{"$group": {"_id": "$string", "total": {"$sum": "$integer"}}}], - projection_model=OutputItem, - ) - assert q.get_aggregation_pipeline() == [ - {"$match": {"increment": {"$gte": 4}}}, - {"$group": {"_id": "$string", "total": {"$sum": "$integer"}}}, - {"$project": {"_id": 1, "total": 1}}, - ] - for i in q: - if i.id == "test_1": - assert i.total == 2 - elif i.id == "test_2": - assert i.total == 6 - elif i.id == "test_3": - assert i.total == 3 - else: - raise KeyError - ids.append(i.id) - assert set(ids) == {"test_1", "test_2", "test_3"} - - -def test_aggregate_with_session(preset_documents, session): - q = Sample.find(Sample.increment >= 4).aggregate( - [{"$group": {"_id": "$string", "total": {"$sum": "$integer"}}}], - session=session, - ) - assert q.session == session - - q = Sample.find(Sample.increment >= 4, session=session).aggregate( - [{"$group": {"_id": "$string", "total": {"$sum": "$integer"}}}] - ) - assert q.session == session - - result = q.to_list() - - assert len(result) == 3 - assert {"_id": "test_1", "total": 2} in result - assert {"_id": "test_2", "total": 6} in result - assert {"_id": "test_3", "total": 3} in result - - -def test_aggregate_pymongo_kwargs(preset_documents): - with pytest.raises(OperationFailure): - Sample.find(Sample.increment >= 4).aggregate( - [{"$group": {"_id": "$string", "total": {"$sum": "$integer"}}}], - wrong=True, - ).to_list() - - # with pytest.raises(TypeError): - # Sample.find(Sample.increment >= 4).aggregate( - # [{"$group": {"_id": "$string", "total": {"$sum": "$integer"}}}], - # hint="integer_1", - # ).to_list() diff --git a/tests/sync/odm/query/test_aggregate_methods.py b/tests/sync/odm/query/test_aggregate_methods.py deleted file mode 100644 index e0e33b8a..00000000 --- a/tests/sync/odm/query/test_aggregate_methods.py +++ /dev/null @@ -1,95 +0,0 @@ -from tests.sync.models import Sample - - -def test_sum(preset_documents, session): - n = Sample.find_many(Sample.integer == 1).sum(Sample.increment) - - assert n == 12 - - n = Sample.find_many(Sample.integer == 1).sum( - Sample.increment, session=session - ) - - assert n == 12 - - -def test_sum_without_docs(session): - n = Sample.find_many(Sample.integer == 1).sum(Sample.increment) - - assert n is None - - n = Sample.find_many(Sample.integer == 1).sum( - Sample.increment, session=session - ) - - assert n is None - - -def test_avg(preset_documents, session): - n = Sample.find_many(Sample.integer == 1).avg(Sample.increment) - - assert n == 4 - n = Sample.find_many(Sample.integer == 1).avg( - Sample.increment, session=session - ) - - assert n == 4 - - -def test_avg_without_docs(session): - n = Sample.find_many(Sample.integer == 1).avg(Sample.increment) - - assert n is None - n = Sample.find_many(Sample.integer == 1).avg( - Sample.increment, session=session - ) - - assert n is None - - -def test_max(preset_documents, session): - n = Sample.find_many(Sample.integer == 1).max(Sample.increment) - - assert n == 5 - - n = Sample.find_many(Sample.integer == 1).max( - Sample.increment, session=session - ) - - assert n == 5 - - -def test_max_without_docs(session): - n = Sample.find_many(Sample.integer == 1).max(Sample.increment) - - assert n is None - - n = Sample.find_many(Sample.integer == 1).max( - Sample.increment, session=session - ) - - assert n is None - - -def test_min(preset_documents, session): - n = Sample.find_many(Sample.integer == 1).min(Sample.increment) - - assert n == 3 - - n = Sample.find_many(Sample.integer == 1).min( - Sample.increment, session=session - ) - - assert n == 3 - - -def test_min_without_docs(session): - n = Sample.find_many(Sample.integer == 1).min(Sample.increment) - - assert n is None - - n = Sample.find_many(Sample.integer == 1).min( - Sample.increment, session=session - ) - - assert n is None diff --git a/tests/sync/odm/query/test_delete.py b/tests/sync/odm/query/test_delete.py deleted file mode 100644 index 900c1d9a..00000000 --- a/tests/sync/odm/query/test_delete.py +++ /dev/null @@ -1,116 +0,0 @@ -import pytest -from tests.sync.models import Sample - - -def test_delete_many(preset_documents): - count_before = Sample.count() - count_find = ( - Sample.find_many(Sample.integer > 1) - .find_many(Sample.nested.optional == None) - .count() - ) # noqa - delete_result = ( - Sample.find_many(Sample.integer > 1) - .find_many(Sample.nested.optional == None) - .delete() - .run() - ) # noqa - count_deleted = delete_result.deleted_count - count_after = Sample.count() - assert count_before - count_find == count_after - assert count_after + count_deleted == count_before - # assert isinstance( - # Sample.find_many(Sample.integer > 1) - # .find_many(Sample.nested.optional == None) - # .delete_many(), - # DeleteMany, - # )# noqa - - -def test_delete_all(preset_documents): - count_before = Sample.count() - delete_result = Sample.delete_all() - count_deleted = delete_result.deleted_count - count_after = Sample.count() - assert count_after == 0 - assert count_after + count_deleted == count_before - - -def test_delete_self(preset_documents): - count_before = Sample.count() - result = ( - Sample.find_many(Sample.integer > 1) - .find_many(Sample.nested.optional == None) - .to_list() - ) # noqa - a = result[0] - delete_result = a.delete() - count_deleted = delete_result.deleted_count - count_after = Sample.count() - assert count_before == count_after + 1 - assert count_deleted == 1 - - -def test_delete_one(preset_documents): - count_before = Sample.count() - delete_result = ( - Sample.find_one(Sample.integer > 1) - .find_one(Sample.nested.optional == None) - .delete() - .run() - ) # noqa - count_after = Sample.count() - count_deleted = delete_result.deleted_count - assert count_before == count_after + 1 - assert count_deleted == 1 - - count_before = Sample.count() - delete_result = ( - Sample.find_one(Sample.integer > 1) - .find_one(Sample.nested.optional == None) - .delete_one() - .run() - ) # noqa - count_deleted = delete_result.deleted_count - count_after = Sample.count() - assert count_before == count_after + 1 - assert count_deleted == 1 - - -def test_delete_many_with_session(preset_documents, session): - count_before = Sample.count() - count_find = ( - Sample.find_many(Sample.integer > 1) - .find_many(Sample.nested.optional == None) - .count() - ) # noqa - q = ( - Sample.find_many(Sample.integer > 1) - .find_many(Sample.nested.optional == None) - .set_session(session=session) - .delete() - .run() - ) # noqa - - # assert q.session == session - - delete_result = q - count_deleted = delete_result.deleted_count - count_after = Sample.count() - assert count_before - count_find == count_after - assert count_after + count_deleted == count_before - - -def test_delete_pymongo_kwargs(preset_documents): - with pytest.raises(TypeError): - Sample.find_many(Sample.increment > 4).delete(wrong="integer_1").run() - - delete_result = ( - Sample.find_many(Sample.increment > 4).delete(hint="integer_1").run() - ) - assert delete_result is not None - - delete_result = ( - Sample.find_one(Sample.increment > 4).delete(hint="integer_1").run() - ) - assert delete_result is not None diff --git a/tests/sync/odm/query/test_find.py b/tests/sync/odm/query/test_find.py deleted file mode 100644 index 0425b2d8..00000000 --- a/tests/sync/odm/query/test_find.py +++ /dev/null @@ -1,358 +0,0 @@ -import datetime - -import pytest -from pydantic import BaseModel -from pydantic.color import Color - -from beanie.odm.enums import SortDirection -from tests.sync.models import ( - Sample, - DocumentWithBsonEncodersFiledsTypes, - House, -) - - -def test_find_query(): - q = Sample.find_many(Sample.integer == 1).get_filter_query() - assert q == {"integer": 1} - - q = Sample.find_many( - Sample.integer == 1, Sample.nested.integer >= 2 - ).get_filter_query() - assert q == {"$and": [{"integer": 1}, {"nested.integer": {"$gte": 2}}]} - - q = ( - Sample.find_many(Sample.integer == 1) - .find_many(Sample.nested.integer >= 2) - .get_filter_query() - ) - assert q == {"$and": [{"integer": 1}, {"nested.integer": {"$gte": 2}}]} - - q = Sample.find().get_filter_query() - assert q == {} - - -def test_find_many(preset_documents): - result = ( - Sample.find_many(Sample.integer > 1) - .find_many(Sample.nested.optional == None) - .to_list() - ) # noqa - assert len(result) == 2 - for a in result: - assert a.integer > 1 - assert a.nested.optional is None - - len_result = 0 - for a in Sample.find_many(Sample.integer > 1).find_many( - Sample.nested.optional == None - ): # noqa - assert a in result - len_result += 1 - - assert len_result == len(result) - - -def test_find_many_skip(preset_documents): - q = Sample.find_many(Sample.integer > 1, skip=2) - assert q.skip_number == 2 - - q = Sample.find_many(Sample.integer > 1).skip(2) - assert q.skip_number == 2 - - result = ( - Sample.find_many(Sample.increment > 2) - .find_many(Sample.nested.optional == None) - .skip(1) - .to_list() - ) - assert len(result) == 3 - for sample in result: - assert sample.increment > 2 - assert sample.nested.optional is None - - len_result = 0 - for sample in ( - Sample.find_many(Sample.increment > 2) - .find_many(Sample.nested.optional == None) - .skip(1) - ): # noqa - assert sample in result - len_result += 1 - - assert len_result == len(result) - - -def test_find_many_limit(preset_documents): - q = Sample.find_many(Sample.integer > 1, limit=2) - assert q.limit_number == 2 - - q = Sample.find_many(Sample.integer > 1).limit(2) - assert q.limit_number == 2 - - result = ( - Sample.find_many(Sample.increment > 2) - .find_many(Sample.nested.optional == None) - .sort(Sample.increment) - .limit(2) - .to_list() - ) # noqa - assert len(result) == 2 - for a in result: - assert a.increment > 2 - assert a.nested.optional is None - - len_result = 0 - for a in ( - Sample.find_many(Sample.increment > 2) - .find(Sample.nested.optional == None) - .sort(Sample.increment) - .limit(2) - ): # noqa - assert a in result - len_result += 1 - - assert len_result == len(result) - - -def test_find_all(preset_documents): - result = Sample.find_all().to_list() - assert len(result) == 10 - - len_result = 0 - for a in Sample.find_all(): - assert a in result - len_result += 1 - - assert len_result == len(result) - - -def test_find_one(preset_documents): - a = ( - Sample.find_one(Sample.integer > 1) - .find_one(Sample.nested.optional == None) - .run() - ) # noqa - assert a.integer > 1 - assert a.nested.optional is None - - a = ( - Sample.find_one(Sample.integer > 100) - .find_one(Sample.nested.optional == None) - .run() - ) # noqa - assert a is None - - -def test_get(preset_documents): - a = ( - Sample.find_one(Sample.integer > 1) - .find_one(Sample.nested.optional == None) - .run() - ) # noqa - assert a.integer > 1 - assert a.nested.optional is None - - new_a = Sample.get(a.id).run() - assert new_a == a - - # check for another type - new_a = Sample.get(str(a.id)).run() - assert new_a == a - - -def test_sort(preset_documents): - q = Sample.find_many(Sample.integer > 1, sort="-integer") - assert q.sort_expressions == [("integer", SortDirection.DESCENDING)] - - q = Sample.find_many(Sample.integer > 1, sort="integer") - assert q.sort_expressions == [("integer", SortDirection.ASCENDING)] - - q = Sample.find_many(Sample.integer > 1).sort("-integer") - assert q.sort_expressions == [("integer", SortDirection.DESCENDING)] - - q = ( - Sample.find_many(Sample.integer > 1) - .find_many(Sample.integer < 100) - .sort("-integer") - ) - assert q.sort_expressions == [("integer", SortDirection.DESCENDING)] - - result = Sample.find_many(Sample.integer > 1, sort="-integer").to_list() - i_buf = None - for a in result: - if i_buf is None: - i_buf = a.integer - assert i_buf >= a.integer - i_buf = a.integer - - result = Sample.find_many(Sample.integer > 1, sort="+integer").to_list() - i_buf = None - for a in result: - if i_buf is None: - i_buf = a.integer - assert i_buf <= a.integer - i_buf = a.integer - - result = Sample.find_many(Sample.integer > 1, sort="integer").to_list() - i_buf = None - for a in result: - if i_buf is None: - i_buf = a.integer - assert i_buf <= a.integer - i_buf = a.integer - - result = Sample.find_many( - Sample.integer > 1, sort=-Sample.integer - ).to_list() - i_buf = None - for a in result: - if i_buf is None: - i_buf = a.integer - assert i_buf >= a.integer - i_buf = a.integer - - with pytest.raises(TypeError): - Sample.find_many(Sample.integer > 1, sort=1) - - -def test_find_many_with_projection(preset_documents): - class SampleProjection(BaseModel): - string: str - integer: int - - result = ( - Sample.find_many(Sample.integer > 1) - .find_many(Sample.nested.optional == None) - .project(projection_model=SampleProjection) - .to_list() - ) - assert result == [ - SampleProjection(string="test_2", integer=2), - SampleProjection(string="test_2", integer=2), - ] - - result = ( - Sample.find_many(Sample.integer > 1) - .find_many( - Sample.nested.optional == None, projection_model=SampleProjection - ) - .to_list() - ) - assert result == [ - SampleProjection(string="test_2", integer=2), - SampleProjection(string="test_2", integer=2), - ] - - -def test_find_many_with_custom_projection(preset_documents): - class SampleProjection(BaseModel): - string: str - i: int - - class Settings: - projection = {"string": 1, "i": "$nested.integer"} - - result = ( - Sample.find_many(Sample.integer > 1) - .find_many(Sample.nested.optional == None) - .project(projection_model=SampleProjection) - .sort(Sample.nested.integer) - .to_list() - ) - assert result == [ - SampleProjection(string="test_2", i=3), - SampleProjection(string="test_2", i=4), - ] - - -def test_find_many_with_session(preset_documents, session): - q_1 = ( - Sample.find_many(Sample.integer > 1) - .find_many(Sample.nested.optional == None) - .set_session(session) - ) - assert q_1.session == session - - q_2 = Sample.find_many(Sample.integer > 1).find_many( - Sample.nested.optional == None, session=session - ) - assert q_2.session == session - - result = q_2.to_list() - - assert len(result) == 2 - for a in result: - assert a.integer > 1 - assert a.nested.optional is None - - len_result = 0 - for a in Sample.find_many(Sample.integer > 1).find_many( - Sample.nested.optional == None - ): # noqa - assert a in result - len_result += 1 - - assert len_result == len(result) - - -def test_bson_encoders_filed_types(): - custom = DocumentWithBsonEncodersFiledsTypes( - color="7fffd4", timestamp=datetime.datetime.utcnow() - ) - c = custom.insert() - c_fromdb = DocumentWithBsonEncodersFiledsTypes.find_one( - DocumentWithBsonEncodersFiledsTypes.color == Color("7fffd4") - ).run() - assert c_fromdb.color.as_hex() == c.color.as_hex() - - -def test_find_by_datetime(preset_documents): - datetime_1 = datetime.datetime.utcnow() - datetime.timedelta(days=7) - datetime_2 = datetime.datetime.utcnow() - datetime.timedelta(days=2) - docs = Sample.find( - Sample.timestamp >= datetime_1, - Sample.timestamp <= datetime_2, - ).to_list() - assert len(docs) == 5 - - -def test_find_first_or_none(preset_documents): - doc = ( - Sample.find(Sample.increment > 1) - .sort(-Sample.increment) - .first_or_none() - ) - assert doc.increment == 9 - - doc = ( - Sample.find(Sample.increment > 9) - .sort(-Sample.increment) - .first_or_none() - ) - assert doc is None - - -def test_find_pymongo_kwargs(preset_documents): - with pytest.raises(TypeError): - Sample.find_many(Sample.increment > 1, wrong=100).to_list() - - Sample.find_many( - Sample.increment > 1, Sample.integer > 1, allow_disk_use=True - ).to_list() - - Sample.find_many( - Sample.increment > 1, Sample.integer > 1, hint="integer_1" - ).to_list() - - House.find_many( - House.height > 1, fetch_links=True, hint="height_1" - ).to_list() - - House.find_many( - House.height > 1, fetch_links=True, allowDiskUse=True - ).to_list() - - Sample.find_one(Sample.increment > 1, Sample.integer > 1, hint="integer_1") - - House.find_one(House.height > 1, fetch_links=True, hint="height_1") diff --git a/tests/sync/odm/query/test_update.py b/tests/sync/odm/query/test_update.py deleted file mode 100644 index b2623db0..00000000 --- a/tests/sync/odm/query/test_update.py +++ /dev/null @@ -1,211 +0,0 @@ -import asyncio - -import pytest - -from beanie.odm.operators.update.general import Set, Max -from tests.sync.models import Sample - - -def test_update_query(): - q = ( - Sample.find_many(Sample.integer == 1) - .update(Set({Sample.integer: 10})) - .update_query - ) - assert q == {"$set": {"integer": 10}} - - q = ( - Sample.find_many(Sample.integer == 1) - .update(Max({Sample.integer: 10}), Set({Sample.optional: None})) - .update_query - ) - assert q == {"$max": {"integer": 10}, "$set": {"optional": None}} - - q = ( - Sample.find_many(Sample.integer == 1) - .update(Set({Sample.integer: 10}), Set({Sample.optional: None})) - .update_query - ) - assert q == {"$set": {"optional": None}} - - q = ( - Sample.find_many(Sample.integer == 1) - .update(Max({Sample.integer: 10})) - .update(Set({Sample.optional: None})) - .update_query - ) - assert q == {"$max": {"integer": 10}, "$set": {"optional": None}} - - q = ( - Sample.find_many(Sample.integer == 1) - .update(Set({Sample.integer: 10})) - .update(Set({Sample.optional: None})) - .update_query - ) - assert q == {"$set": {"optional": None}} - - with pytest.raises(TypeError): - Sample.find_many(Sample.integer == 1).update(40).update_query - - -def test_update_many(preset_documents): - Sample.find_many(Sample.increment > 4).find_many( - Sample.nested.optional == None - ).update( - Set({Sample.increment: 100}) - ).run() # noqa - result = Sample.find_many(Sample.increment == 100).to_list() - assert len(result) == 3 - for sample in result: - assert sample.increment == 100 - - -def test_update_many_linked_method(preset_documents): - Sample.find_many(Sample.increment > 4).find_many( - Sample.nested.optional == None - ).update_many( - Set({Sample.increment: 100}) - ).run() # noqa - result = Sample.find_many(Sample.increment == 100).to_list() - assert len(result) == 3 - for sample in result: - assert sample.increment == 100 - - -def test_update_all(preset_documents): - Sample.update_all(Set({Sample.integer: 100})).run() - result = Sample.find_all().to_list() - for sample in result: - assert sample.integer == 100 - - Sample.find_all().update(Set({Sample.integer: 101})).run() - result = Sample.find_all().to_list() - for sample in result: - assert sample.integer == 101 - - -def test_update_one(preset_documents): - Sample.find_one(Sample.integer == 1).update( - Set({Sample.integer: 100}) - ).run() - result = Sample.find_many(Sample.integer == 100).to_list() - assert len(result) == 1 - assert result[0].integer == 100 - - Sample.find_one(Sample.integer == 1).update_one( - Set({Sample.integer: 101}) - ).run() - result = Sample.find_many(Sample.integer == 101).to_list() - assert len(result) == 1 - assert result[0].integer == 101 - - -def test_update_self(preset_documents): - sample = Sample.find_one(Sample.integer == 1).run() - sample.update(Set({Sample.integer: 100})) - assert sample.integer == 100 - - result = Sample.find_many(Sample.integer == 100).to_list() - assert len(result) == 1 - assert result[0].integer == 100 - - -def test_update_many_with_session(preset_documents, session): - q = ( - Sample.find_many(Sample.increment > 4) - .find_many(Sample.nested.optional == None) - .update(Set({Sample.increment: 100})) - .set_session(session=session) - ) - assert q.session == session - - q = ( - Sample.find_many(Sample.increment > 4) - .find_many(Sample.nested.optional == None) - .update(Set({Sample.increment: 100}), session=session) - .run() - ) - # assert q.session == session - - q = ( - Sample.find_many(Sample.increment > 4) - .find_many(Sample.nested.optional == None, session=session) - .update(Set({Sample.increment: 100})) - .run() - ) - # assert q.session == session - - # q # noqa - result = Sample.find_many(Sample.increment == 100).to_list() - assert len(result) == 3 - for sample in result: - assert sample.increment == 100 - - -def test_update_many_upsert_with_insert( - preset_documents, sample_doc_not_saved -): - Sample.find_many(Sample.integer > 100000).upsert( - Set({Sample.integer: 100}), on_insert=sample_doc_not_saved - ).run() - asyncio.sleep(2) - new_docs = Sample.find_many( - Sample.string == sample_doc_not_saved.string - ).to_list() - assert len(new_docs) == 1 - doc = new_docs[0] - assert doc.integer == sample_doc_not_saved.integer - - -def test_update_many_upsert_without_insert( - preset_documents, sample_doc_not_saved -): - Sample.find_many(Sample.integer > 1).upsert( - Set({Sample.integer: 100}), on_insert=sample_doc_not_saved - ).run() - asyncio.sleep(2) - new_docs = Sample.find_many( - Sample.string == sample_doc_not_saved.string - ).to_list() - assert len(new_docs) == 0 - - -def test_update_one_upsert_with_insert(preset_documents, sample_doc_not_saved): - Sample.find_one(Sample.integer > 100000).upsert( - Set({Sample.integer: 100}), on_insert=sample_doc_not_saved - ).run() - asyncio.sleep(2) - new_docs = Sample.find_many( - Sample.string == sample_doc_not_saved.string - ).to_list() - assert len(new_docs) == 1 - doc = new_docs[0] - assert doc.integer == sample_doc_not_saved.integer - - -def test_update_one_upsert_without_insert( - preset_documents, sample_doc_not_saved -): - Sample.find_one(Sample.integer > 1).upsert( - Set({Sample.integer: 100}), on_insert=sample_doc_not_saved - ).run() - asyncio.sleep(2) - new_docs = Sample.find_many( - Sample.string == sample_doc_not_saved.string - ).to_list() - assert len(new_docs) == 0 - - -def test_update_pymongo_kwargs(preset_documents): - with pytest.raises(TypeError): - Sample.find_many(Sample.increment > 4).update( - Set({Sample.increment: 100}), wrong="integer_1" - ).run() - - Sample.find_many(Sample.increment > 4).update( - Set({Sample.increment: 100}), hint="integer_1" - ).run() - - Sample.find_one(Sample.increment > 4).update( - Set({Sample.increment: 100}), hint="integer_1" - ).run() diff --git a/tests/sync/odm/query/test_update_methods.py b/tests/sync/odm/query/test_update_methods.py deleted file mode 100644 index 39b8d849..00000000 --- a/tests/sync/odm/query/test_update_methods.py +++ /dev/null @@ -1,81 +0,0 @@ -from beanie.odm.operators.update.general import Max -from beanie.sync.odm.queries.update import UpdateQuery, UpdateMany -from tests.sync.models import Sample - - -def test_set(session): - q = Sample.find_many(Sample.integer == 1).set( - {Sample.integer: 100}, session=session - ) - - assert isinstance(q, UpdateQuery) - assert isinstance(q, UpdateMany) - assert q.session == session - - assert q.update_query == {"$set": {"integer": 100}} - - q = ( - Sample.find_many(Sample.integer == 1) - .update(Max({Sample.integer: 10})) - .set({Sample.integer: 100}) - ) - - assert isinstance(q, UpdateQuery) - assert isinstance(q, UpdateMany) - - assert q.update_query == { - "$max": {"integer": 10}, - "$set": {"integer": 100}, - } - - -def test_current_date(session): - q = Sample.find_many(Sample.integer == 1).current_date( - {Sample.timestamp: "timestamp"}, session=session - ) - - assert isinstance(q, UpdateQuery) - assert isinstance(q, UpdateMany) - assert q.session == session - - assert q.update_query == {"$currentDate": {"timestamp": "timestamp"}} - - q = ( - Sample.find_many(Sample.integer == 1) - .update(Max({Sample.integer: 10})) - .current_date({Sample.timestamp: "timestamp"}) - ) - - assert isinstance(q, UpdateQuery) - assert isinstance(q, UpdateMany) - - assert q.update_query == { - "$max": {"integer": 10}, - "$currentDate": {"timestamp": "timestamp"}, - } - - -def test_inc(session): - q = Sample.find_many(Sample.integer == 1).inc( - {Sample.integer: 100}, session=session - ) - - assert isinstance(q, UpdateQuery) - assert isinstance(q, UpdateMany) - assert q.session == session - - assert q.update_query == {"$inc": {"integer": 100}} - - q = ( - Sample.find_many(Sample.integer == 1) - .update(Max({Sample.integer: 10})) - .inc({Sample.integer: 100}) - ) - - assert isinstance(q, UpdateQuery) - assert isinstance(q, UpdateMany) - - assert q.update_query == { - "$max": {"integer": 10}, - "$inc": {"integer": 100}, - } diff --git a/tests/sync/odm/test_actions.py b/tests/sync/odm/test_actions.py deleted file mode 100644 index 2a693bca..00000000 --- a/tests/sync/odm/test_actions.py +++ /dev/null @@ -1,129 +0,0 @@ -import pytest - -from beanie import Before, After - -from tests.sync.models import ( - DocumentWithActions, - InheritedDocumentWithActions, - DocumentWithActions2, -) - - -class TestActions: - @pytest.mark.parametrize( - "doc_class", - [ - DocumentWithActions, - DocumentWithActions2, - InheritedDocumentWithActions, - ], - ) - def test_actions_insert(self, doc_class): - test_name = f"test_actions_insert_{doc_class}" - sample = doc_class(name=test_name) - - sample.insert() - assert sample.name != test_name - assert sample.name == test_name.capitalize() - assert sample.num_1 == 1 - assert sample.num_2 == 9 - - @pytest.mark.parametrize( - "doc_class", - [ - DocumentWithActions, - DocumentWithActions2, - InheritedDocumentWithActions, - ], - ) - def test_actions_replace(self, doc_class): - test_name = f"test_actions_replace_{doc_class}" - sample = doc_class(name=test_name) - - sample.insert() - - sample.replace() - assert sample.num_1 == 2 - assert sample.num_3 == 99 - - @pytest.mark.parametrize( - "doc_class", - [ - DocumentWithActions, - DocumentWithActions2, - InheritedDocumentWithActions, - ], - ) - def test_skip_actions_insert(self, doc_class): - test_name = f"test_skip_actions_insert_{doc_class}" - sample = doc_class(name=test_name) - - sample.insert(skip_actions=[After, "capitalize_name"]) - # capitalize_name has been skipped - assert sample.name == test_name - # add_one has not been skipped - assert sample.num_1 == 1 - # num_2_change has been skipped - assert sample.num_2 == 10 - - @pytest.mark.parametrize( - "doc_class", - [ - DocumentWithActions, - DocumentWithActions2, - InheritedDocumentWithActions, - ], - ) - def test_skip_actions_replace(self, doc_class): - test_name = f"test_skip_actions_replace{doc_class}" - sample = doc_class(name=test_name) - - sample.insert() - - sample.replace(skip_actions=[Before, "num_3_change"]) - # add_one has been skipped - assert sample.num_1 == 1 - # num_3_change has been skipped - assert sample.num_3 == 100 - - @pytest.mark.parametrize( - "doc_class", - [ - DocumentWithActions, - DocumentWithActions2, - InheritedDocumentWithActions, - ], - ) - def test_actions_delete(self, doc_class): - test_name = f"test_actions_delete_{doc_class}" - sample = doc_class(name=test_name) - - sample.delete() - assert sample.Inner.inner_num_1 == 1 - assert sample.Inner.inner_num_2 == 2 - - @pytest.mark.parametrize( - "doc_class", - [ - DocumentWithActions, - DocumentWithActions2, - InheritedDocumentWithActions, - ], - ) - def test_actions_update(self, doc_class): - test_name = f"test_actions_update_{doc_class}" - sample = doc_class(name=test_name) - sample.save() - - sample.update({"$set": {"name": "new_name"}}) - assert sample.name == "new_name" - assert sample.num_1 == 2 - assert sample.num_2 == 9 - - sample.set({"name": "awesome_name"}, skip_sync=True) - - assert sample.num_1 == 3 - assert sample.num_2 == 8 - - sample._sync() - assert sample.name == "awesome_name" diff --git a/tests/sync/odm/test_cache.py b/tests/sync/odm/test_cache.py deleted file mode 100644 index dbcc0ff7..00000000 --- a/tests/sync/odm/test_cache.py +++ /dev/null @@ -1,96 +0,0 @@ -from time import sleep - -from tests.sync.models import DocumentTestModel - - -def test_find_one(documents): - documents(5) - doc = DocumentTestModel.find_one(DocumentTestModel.test_int == 1).run() - DocumentTestModel.find_one(DocumentTestModel.test_int == 1).set( - {DocumentTestModel.test_str: "NEW_VALUE"} - ).run() - new_doc = DocumentTestModel.find_one(DocumentTestModel.test_int == 1).run() - assert doc == new_doc - - new_doc = DocumentTestModel.find_one( - DocumentTestModel.test_int == 1, ignore_cache=True - ).run() - assert doc != new_doc - - sleep(10) - - new_doc = DocumentTestModel.find_one(DocumentTestModel.test_int == 1).run() - assert doc != new_doc - - -def test_find_many(documents): - documents(5) - docs = DocumentTestModel.find(DocumentTestModel.test_int > 1).to_list() - - DocumentTestModel.find(DocumentTestModel.test_int > 1).set( - {DocumentTestModel.test_str: "NEW_VALUE"} - ).run() - - new_docs = DocumentTestModel.find(DocumentTestModel.test_int > 1).to_list() - assert docs == new_docs - - new_docs = DocumentTestModel.find( - DocumentTestModel.test_int > 1, ignore_cache=True - ).to_list() - assert docs != new_docs - - sleep(10) - - new_docs = DocumentTestModel.find(DocumentTestModel.test_int > 1).to_list() - assert docs != new_docs - - -def test_aggregation(documents): - documents(5) - docs = DocumentTestModel.aggregate( - [{"$group": {"_id": "$test_str", "total": {"$sum": "$test_int"}}}] - ).to_list() - - DocumentTestModel.find(DocumentTestModel.test_int > 1).set( - {DocumentTestModel.test_str: "NEW_VALUE"} - ).run() - - new_docs = DocumentTestModel.aggregate( - [{"$group": {"_id": "$test_str", "total": {"$sum": "$test_int"}}}] - ).to_list() - assert docs == new_docs - - new_docs = DocumentTestModel.aggregate( - [{"$group": {"_id": "$test_str", "total": {"$sum": "$test_int"}}}], - ignore_cache=True, - ).to_list() - assert docs != new_docs - - sleep(10) - - new_docs = DocumentTestModel.aggregate( - [{"$group": {"_id": "$test_str", "total": {"$sum": "$test_int"}}}] - ).to_list() - assert docs != new_docs - - -def test_capacity(documents): - documents(10) - docs = [] - for i in range(10): - docs.append( - DocumentTestModel.find_one(DocumentTestModel.test_int == i).run() - ) - - DocumentTestModel.find_one(DocumentTestModel.test_int == 1).set( - {DocumentTestModel.test_str: "NEW_VALUE"} - ).run() - DocumentTestModel.find_one(DocumentTestModel.test_int == 9).set( - {DocumentTestModel.test_str: "NEW_VALUE"} - ).run() - - new_doc = DocumentTestModel.find_one(DocumentTestModel.test_int == 1).run() - assert docs[1] != new_doc - - new_doc = DocumentTestModel.find_one(DocumentTestModel.test_int == 9).run() - assert docs[9] == new_doc diff --git a/tests/sync/odm/test_cursor.py b/tests/sync/odm/test_cursor.py deleted file mode 100644 index 4ec737ef..00000000 --- a/tests/sync/odm/test_cursor.py +++ /dev/null @@ -1,13 +0,0 @@ -from tests.sync.models import DocumentTestModel - - -def test_to_list(documents): - documents(10) - result = DocumentTestModel.find_all().to_list() - assert len(result) == 10 - - -def test_async_for(documents): - documents(10) - for document in DocumentTestModel.find_all(): - assert document.test_int in list(range(10)) diff --git a/tests/sync/odm/test_deprecated.py b/tests/sync/odm/test_deprecated.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/sync/odm/test_encoder.py b/tests/sync/odm/test_encoder.py deleted file mode 100644 index 291a6f99..00000000 --- a/tests/sync/odm/test_encoder.py +++ /dev/null @@ -1,52 +0,0 @@ -from datetime import datetime, date - -from bson import Binary - -from beanie.sync.odm.utils.encoder import Encoder -from tests.sync.models import ( - DocumentForEncodingTest, - DocumentForEncodingTestDate, -) - - -def test_encode_datetime(): - assert isinstance(Encoder().encode(datetime.now()), datetime) - - doc = DocumentForEncodingTest(datetime_field=datetime.now()) - doc.insert() - new_doc = DocumentForEncodingTest.get(doc.id).run() - assert isinstance(new_doc.datetime_field, datetime) - - -def test_encode_date(): - assert isinstance(Encoder().encode(datetime.now()), datetime) - - doc = DocumentForEncodingTestDate() - doc.insert() - new_doc = DocumentForEncodingTestDate.get(doc.id).run() - assert new_doc.date_field == doc.date_field - assert isinstance(new_doc.date_field, date) - - -def test_encode_with_custom_encoder(): - assert isinstance( - Encoder(custom_encoders={datetime: str}).encode(datetime.now()), str - ) - - -def test_bytes(): - encoded_b = Encoder().encode(b"test") - assert isinstance(encoded_b, Binary) - assert encoded_b.subtype == 0 - - doc = DocumentForEncodingTest(bytes_field=b"test") - doc.insert() - new_doc = DocumentForEncodingTest.get(doc.id).run() - assert isinstance(new_doc.bytes_field, bytes) - - -def test_bytes_already_binary(): - b = Binary(b"123", 3) - encoded_b = Encoder().encode(b) - assert isinstance(encoded_b, Binary) - assert encoded_b.subtype == 3 diff --git a/tests/sync/odm/test_expression_fields.py b/tests/sync/odm/test_expression_fields.py deleted file mode 100644 index 816df4ba..00000000 --- a/tests/sync/odm/test_expression_fields.py +++ /dev/null @@ -1,80 +0,0 @@ -from beanie.odm.enums import SortDirection -from beanie.odm.operators.find.comparison import In, NotIn -from tests.sync.models import Sample - - -def test_nesting(): - assert Sample.id == "_id" - - q = Sample.find_many(Sample.integer == 1) - assert q.get_filter_query() == {"integer": 1} - assert Sample.integer == "integer" - - q = Sample.find_many(Sample.nested.integer == 1) - assert q.get_filter_query() == {"nested.integer": 1} - assert Sample.nested.integer == "nested.integer" - - q = Sample.find_many(Sample.union.s == "test") - assert q.get_filter_query() == {"union.s": "test"} - assert Sample.union.s == "union.s" - - q = Sample.find_many(Sample.nested.optional == None) # noqa - assert q.get_filter_query() == {"nested.optional": None} - assert Sample.nested.optional == "nested.optional" - - q = Sample.find_many(Sample.nested.integer == 1).find_many( - Sample.nested.union.s == "test" - ) - assert q.get_filter_query() == { - "$and": [{"nested.integer": 1}, {"nested.union.s": "test"}] - } - - -def test_eq(): - q = Sample.find_many(Sample.integer == 1) - assert q.get_filter_query() == {"integer": 1} - - -def test_gt(): - q = Sample.find_many(Sample.integer > 1) - assert q.get_filter_query() == {"integer": {"$gt": 1}} - - -def test_gte(): - q = Sample.find_many(Sample.integer >= 1) - assert q.get_filter_query() == {"integer": {"$gte": 1}} - - -def test_in(): - q = Sample.find_many(In(Sample.integer, [1, 2, 3, 4])) - assert dict(q.get_filter_query()) == {"integer": {"$in": [1, 2, 3, 4]}} - - -def test_lt(): - q = Sample.find_many(Sample.integer < 1) - assert q.get_filter_query() == {"integer": {"$lt": 1}} - - -def test_lte(): - q = Sample.find_many(Sample.integer <= 1) - assert q.get_filter_query() == {"integer": {"$lte": 1}} - - -def test_ne(): - q = Sample.find_many(Sample.integer != 1) - assert q.get_filter_query() == {"integer": {"$ne": 1}} - - -def test_nin(): - q = Sample.find_many(NotIn(Sample.integer, [1, 2, 3, 4])) - assert dict(q.get_filter_query()) == {"integer": {"$nin": [1, 2, 3, 4]}} - - -def test_pos(): - q = +Sample.integer - assert q == ("integer", SortDirection.ASCENDING) - - -def test_neg(): - q = -Sample.integer - assert q == ("integer", SortDirection.DESCENDING) diff --git a/tests/sync/odm/test_fields.py b/tests/sync/odm/test_fields.py deleted file mode 100644 index 86593b21..00000000 --- a/tests/sync/odm/test_fields.py +++ /dev/null @@ -1,130 +0,0 @@ -import datetime -from decimal import Decimal -from pathlib import Path -from typing import Mapping, AbstractSet -import pytest -from pydantic import BaseModel, ValidationError - -from beanie.odm.fields import PydanticObjectId -from beanie.sync.odm.utils.dump import get_dict -from beanie.sync.odm.utils.encoder import Encoder -from tests.sync.models import ( - DocumentWithCustomFiledsTypes, - DocumentWithBsonEncodersFiledsTypes, - DocumentTestModel, - Sample, -) - - -class M(BaseModel): - p: PydanticObjectId - - -def test_pydantic_object_id_wrong_input(): - with pytest.raises(ValidationError): - M(p="test") - - -def test_pydantic_object_id_bytes_input(): - p = PydanticObjectId() - m = M(p=str(p).encode("utf-8")) - assert m.p == p - with pytest.raises(ValidationError): - M(p=b"test") - - -def test_bson_encoders_filed_types(): - custom = DocumentWithBsonEncodersFiledsTypes( - color="7fffd4", timestamp=datetime.datetime.utcnow() - ) - encoded = get_dict(custom) - assert isinstance(encoded["timestamp"], str) - c = custom.insert() - c_fromdb = DocumentWithBsonEncodersFiledsTypes.get(c.id).run() - assert c_fromdb.color.as_hex() == c.color.as_hex() - assert isinstance(c_fromdb.timestamp, datetime.datetime) - assert c_fromdb.timestamp, custom.timestamp - - -def test_custom_filed_types(): - custom1 = DocumentWithCustomFiledsTypes( - color="#753c38", - decimal=500, - secret_bytes=b"secret_bytes", - secret_string="super_secret_password", - ipv4address="127.0.0.1", - ipv4interface="192.0.2.5/24", - ipv4network="192.0.2.0/24", - ipv6address="::abc:7:def", - ipv6interface="2001:db00::2/24", - ipv6network="2001:db00::0/24", - timedelta=4782453, - set_type={"one", "two", "three"}, - tuple_type=tuple([3, "string"]), - path="/etc/hosts", - ) - custom2 = DocumentWithCustomFiledsTypes( - color="magenta", - decimal=Decimal("3.14") + Decimal(10) ** Decimal(-18), - secret_bytes=b"secret_bytes", - secret_string="super_secret_password", - ipv4address="127.0.0.1", - ipv4interface="192.0.2.5/24", - ipv4network="192.0.2.0/24", - ipv6address="::abc:7:def", - ipv6interface="2001:db00::2/24", - ipv6network="2001:db00::0/24", - timedelta=4782453, - set_type=["one", "two", "three"], - tuple_type=[3, "three"], - path=Path("C:\\Windows"), - ) - c1 = custom1.insert() - c2 = custom2.insert() - c1_fromdb = DocumentWithCustomFiledsTypes.get(c1.id).run() - c2_fromdb = DocumentWithCustomFiledsTypes.get(c2.id).run() - assert set(c1_fromdb.set_type) == set(c1.set_type) - assert set(c2_fromdb.set_type) == set(c2.set_type) - c1_fromdb.set_type = c2_fromdb.set_type = c1.set_type = c2.set_type = None - c1_fromdb.revision_id = None - c2_fromdb.revision_id = None - c1_encoded = Encoder().encode(c1) - c1_fromdb_encoded = Encoder().encode(c1_fromdb) - c2_encoded = Encoder().encode(c2) - c2_fromdb_encoded = Encoder().encode(c2_fromdb) - assert c1_fromdb_encoded == c1_encoded - assert c2_fromdb_encoded == c2_encoded - assert Decimal(str(custom1.decimal)) == Decimal( - str(c1_encoded.get("decimal")) - ) - assert Decimal(str(custom2.decimal)) == Decimal( - str(c2_encoded.get("decimal")) - ) - - -def test_hidden(document): - document = DocumentTestModel.find_one().run() - - assert "test_list" not in document.dict() - - -@pytest.mark.parametrize("exclude", [{"test_int"}, {"test_doc": {"test_int"}}]) -def test_param_exclude(document, exclude): - document = DocumentTestModel.find_one().run() - - doc_dict = document.dict(exclude=exclude) - if isinstance(exclude, AbstractSet): - for k in exclude: - assert k not in doc_dict - elif isinstance(exclude, Mapping): - for k, v in exclude.items(): - if isinstance(v, bool) and v: - assert k not in doc_dict - elif isinstance(v, AbstractSet): - for another_k in v: - assert another_k not in doc_dict[k] - - -def test_expression_fields(): - assert Sample.nested.integer == "nested.integer" - assert Sample.nested["integer"] == "nested.integer" diff --git a/tests/sync/odm/test_id.py b/tests/sync/odm/test_id.py deleted file mode 100644 index 22d15361..00000000 --- a/tests/sync/odm/test_id.py +++ /dev/null @@ -1,20 +0,0 @@ -from uuid import UUID - -from tests.sync.models import ( - DocumentWithCustomIdUUID, - DocumentWithCustomIdInt, -) - - -def test_uuid_id(): - doc = DocumentWithCustomIdUUID(name="TEST") - doc.insert() - new_doc = DocumentWithCustomIdUUID.get(doc.id).run() - assert type(new_doc.id) == UUID - - -def test_integer_id(): - doc = DocumentWithCustomIdInt(name="TEST", id=1) - doc.insert() - new_doc = DocumentWithCustomIdInt.get(doc.id).run() - assert type(new_doc.id) == int diff --git a/tests/sync/odm/test_relations.py b/tests/sync/odm/test_relations.py deleted file mode 100644 index ec3d3a68..00000000 --- a/tests/sync/odm/test_relations.py +++ /dev/null @@ -1,215 +0,0 @@ -import pytest - -from beanie.exceptions import DocumentWasNotSaved -from beanie.odm.fields import WriteRules, DeleteRules -from beanie.sync.odm import Link -from tests.sync.models import Window, Door, House, Roof - - -@pytest.fixture -def windows_not_inserted(): - return [Window(x=10, y=10), Window(x=11, y=11)] - - -@pytest.fixture -def door_not_inserted(): - return Door(t=10) - - -@pytest.fixture -def house_not_inserted(windows_not_inserted, door_not_inserted): - return House( - windows=windows_not_inserted, door=door_not_inserted, name="test" - ) - - -@pytest.fixture -def house(house_not_inserted): - return house_not_inserted.insert(link_rule=WriteRules.WRITE) - - -@pytest.fixture -def houses(): - for i in range(10): - roof = Roof() if i % 2 == 0 else None - house = House( - door=Door(t=i), - windows=[Window(x=10, y=10 + i), Window(x=11, y=11 + i)], - roof=roof, - name="test", - height=i, - ).insert(link_rule=WriteRules.WRITE) - if i == 9: - house.windows[0].delete() - house.door.delete() - - -class TestInsert: - def test_rule_do_nothing(self, house_not_inserted): - with pytest.raises(DocumentWasNotSaved): - house_not_inserted.insert() - - def test_rule_write(self, house_not_inserted): - house_not_inserted.insert(link_rule=WriteRules.WRITE) - windows = Window.all().to_list() - assert len(windows) == 2 - doors = Door.all().to_list() - assert len(doors) == 1 - houses = House.all().to_list() - assert len(houses) == 1 - - def test_insert_with_link(self, house_not_inserted, door_not_inserted): - door = door_not_inserted.insert() - link = Door.link_from_id(door.id) - house_not_inserted.door = link - house = House.parse_obj(house_not_inserted) - house.insert(link_rule=WriteRules.WRITE) - house.json() - - -class TestFind: - def test_prefetch_find_many(self, houses): - items = House.find(House.height > 2).sort(House.height).to_list() - assert len(items) == 7 - for window in items[0].windows: - assert isinstance(window, Link) - assert isinstance(items[0].door, Link) - assert items[0].roof is None - assert isinstance(items[1].roof, Link) - - items = ( - House.find(House.height > 2, fetch_links=True) - .sort(House.height) - .to_list() - ) - assert len(items) == 7 - for window in items[0].windows: - assert isinstance(window, Window) - assert isinstance(items[0].door, Door) - assert items[0].roof is None - assert isinstance(items[1].roof, Roof) - - houses = House.find_many(House.height == 9, fetch_links=True).to_list() - assert len(houses[0].windows) == 1 - assert isinstance(houses[0].door, Link) - houses[0].fetch_link(House.door) - assert isinstance(houses[0].door, Link) - - houses = House.find_many(House.door.t > 5, fetch_links=True).to_list() - - assert len(houses) == 3 - - houses = House.find_many( - House.windows.y == 15, fetch_links=True - ).to_list() - - assert len(houses) == 2 - - houses = House.find_many( - House.height > 5, limit=3, fetch_links=True - ).to_list() - - assert len(houses) == 3 - - def test_prefetch_find_one(self, house): - house = House.find_one(House.name == "test").run() - for window in house.windows: - assert isinstance(window, Link) - assert isinstance(house.door, Link) - - house = House.find_one(House.name == "test", fetch_links=True).run() - for window in house.windows: - assert isinstance(window, Window) - assert isinstance(house.door, Door) - - house = House.get(house.id, fetch_links=True).run() - for window in house.windows: - assert isinstance(window, Window) - assert isinstance(house.door, Door) - - def test_fetch(self, house): - house = House.find_one(House.name == "test").run() - for window in house.windows: - assert isinstance(window, Link) - assert isinstance(house.door, Link) - - house.fetch_all_links() - for window in house.windows: - assert isinstance(window, Window) - assert isinstance(house.door, Door) - - house = House.find_one(House.name == "test").run() - assert isinstance(house.door, Link) - house.fetch_link(House.door) - assert isinstance(house.door, Door) - - for window in house.windows: - assert isinstance(window, Link) - house.fetch_link(House.windows) - for window in house.windows: - assert isinstance(window, Window) - - def test_find_by_id_of_the_linked_docs(self, house): - house_lst_1 = House.find(House.door.id == house.door.id).to_list() - house_lst_2 = House.find( - House.door.id == house.door.id, fetch_links=True - ).to_list() - assert len(house_lst_1) == 1 - assert len(house_lst_2) == 1 - - house_1 = House.find_one(House.door.id == house.door.id).run() - house_2 = House.find_one( - House.door.id == house.door.id, fetch_links=True - ).run() - assert house_1 is not None - assert house_2 is not None - - -class TestReplace: - def test_do_nothing(self, house): - house.door.t = 100 - house.replace() - new_house = House.get(house.id, fetch_links=True).run() - assert new_house.door.t == 10 - - def test_write(self, house): - house.door.t = 100 - house.replace(link_rule=WriteRules.WRITE) - new_house = House.get(house.id, fetch_links=True).run() - assert new_house.door.t == 100 - - -class TestSave: - def test_do_nothing(self, house): - house.door.t = 100 - house.save() - new_house = House.get(house.id, fetch_links=True).run() - assert new_house.door.t == 10 - - def test_write(self, house): - house.door.t = 100 - house.windows = [Window(x=100, y=100)] - house.save(link_rule=WriteRules.WRITE) - new_house = House.get(house.id, fetch_links=True).run() - assert new_house.door.t == 100 - for window in new_house.windows: - assert window.x == 100 - assert window.y == 100 - - -class TestDelete: - def test_do_nothing(self, house): - house.delete() - door = Door.get(house.door.id).run() - assert door is not None - - windows = Window.all().to_list() - assert windows is not None - - def test_delete_links(self, house): - house.delete(link_rule=DeleteRules.DELETE_LINKS) - door = Door.get(house.door.id).run() - assert door is None - - windows = Window.all().to_list() - assert windows == [] diff --git a/tests/sync/odm/test_run.py b/tests/sync/odm/test_run.py deleted file mode 100644 index 32b5f335..00000000 --- a/tests/sync/odm/test_run.py +++ /dev/null @@ -1,100 +0,0 @@ -from beanie import PydanticObjectId -from tests.sync.models import DocumentTestModel, Sample - - -class TestRun: - def test_get(self, document): - new_document = DocumentTestModel.get(document.id).run() - assert new_document == document - - new_document = ~DocumentTestModel.get(document.id) - assert new_document == document - - def test_find_many(self, preset_documents): - result = ~Sample.find_many(Sample.integer > 1).find_many( - Sample.nested.optional == None - ) - assert len(result) == 2 - for a in result: - assert a.integer > 1 - assert a.nested.optional is None - - def test_find_many_skip(self, preset_documents): - q = Sample.find_many(Sample.integer > 1, skip=2) - assert q.skip_number == 2 - - q = Sample.find_many(Sample.integer > 1).skip(2) - assert q.skip_number == 2 - - result = ( - ~Sample.find_many(Sample.increment > 2) - .find_many(Sample.nested.optional == None) - .skip(1) - ) - assert len(result) == 3 - for sample in result: - assert sample.increment > 2 - assert sample.nested.optional is None - - def test_find_one(self, documents): - inserted_one = documents(1, "kipasa") - documents(10, "smthe else") - expected_doc_id = PydanticObjectId(inserted_one[0]) - new_document = ~DocumentTestModel.find_one({"test_str": "kipasa"}) - assert new_document.id == expected_doc_id - - def test_find_all(self, documents): - documents(4, "uno") - documents(2, "dos") - documents(1, "cuatro") - result = ~DocumentTestModel.find_all() - assert len(result) == 7 - - def test_update_one(self, document): - ~DocumentTestModel.find_one( - {"_id": document.id, "test_list.test_str": "foo"} - ).update({"$set": {"test_list.$.test_str": "foo_foo"}}) - new_document = ~DocumentTestModel.get(document.id) - assert new_document.test_list[0].test_str == "foo_foo" - - def test_update_many(self, documents): - documents(10, "foo") - documents(7, "bar") - ~DocumentTestModel.find_many({"test_str": "foo"}).update( - {"$set": {"test_str": "bar"}} - ) - bar_documetns = ~DocumentTestModel.find_many({"test_str": "bar"}) - assert len(bar_documetns) == 17 - foo_documetns = ~DocumentTestModel.find_many({"test_str": "foo"}) - assert len(foo_documetns) == 0 - - def test_update_all(self, documents): - documents(10, "foo") - documents(7, "bar") - ~DocumentTestModel.update_all( - {"$set": {"test_str": "smth_else"}}, - ) - bar_documetns = ~DocumentTestModel.find_many({"test_str": "bar"}) - assert len(bar_documetns) == 0 - foo_documetns = ~DocumentTestModel.find_many({"test_str": "foo"}) - assert len(foo_documetns) == 0 - smth_else_documetns = ~DocumentTestModel.find_many( - {"test_str": "smth_else"} - ) - assert len(smth_else_documetns) == 17 - - def test_delete_one(self, documents): - documents(4, "uno") - documents(2, "dos") - documents(1, "cuatro") - ~DocumentTestModel.find_one({"test_str": "uno"}).delete() - documents = DocumentTestModel.find_all().to_list() - assert len(documents) == 6 - - def test_delete_many(self, documents): - documents(4, "uno") - documents(2, "dos") - documents(1, "cuatro") - ~DocumentTestModel.find_many({"test_str": "uno"}).delete() - documents = DocumentTestModel.find_all().to_list() - assert len(documents) == 3 diff --git a/tests/sync/odm/test_state_management.py b/tests/sync/odm/test_state_management.py deleted file mode 100644 index 49ad756f..00000000 --- a/tests/sync/odm/test_state_management.py +++ /dev/null @@ -1,234 +0,0 @@ -import pytest -from bson import ObjectId - -from beanie import WriteRules, PydanticObjectId -from beanie.exceptions import StateManagementIsTurnedOff, StateNotSaved -from tests.sync.models import ( - DocumentWithTurnedOnStateManagement, - DocumentWithTurnedOnReplaceObjects, - DocumentWithTurnedOffStateManagement, - InternalDoc, - HouseWithRevision, - WindowWithRevision, -) - - -@pytest.fixture -def state(): - return { - "num_1": 1, - "num_2": 2, - "_id": ObjectId(), - "internal": InternalDoc(), - } - - -@pytest.fixture -def state_without_id(): - return { - "num_1": 1, - "num_2": 2, - "internal": InternalDoc(), - } - - -@pytest.fixture -def doc_default(state): - return DocumentWithTurnedOnStateManagement.parse_obj(state) - - -@pytest.fixture -def doc_replace(state): - return DocumentWithTurnedOnReplaceObjects.parse_obj(state) - - -@pytest.fixture -def saved_doc_default(doc_default): - doc_default.insert() - return doc_default - - -def test_use_state_management_property(): - assert DocumentWithTurnedOnStateManagement.use_state_management() is True - assert DocumentWithTurnedOffStateManagement.use_state_management() is False - - -def test_save_state(): - doc = DocumentWithTurnedOnStateManagement( - num_1=1, num_2=2, internal=InternalDoc(num=1, string="s") - ) - assert doc.get_saved_state() is None - doc.id = PydanticObjectId() - doc._save_state() - assert doc.get_saved_state() == { - "num_1": 1, - "num_2": 2, - "internal": {"num": 1, "string": "s", "lst": [1, 2, 3, 4, 5]}, - "_id": doc.id, - } - - -def test_parse_object_with_saving_state(): - obj = { - "num_1": 1, - "num_2": 2, - "_id": ObjectId(), - "internal": InternalDoc(), - } - doc = DocumentWithTurnedOnStateManagement.parse_obj(obj) - assert doc.get_saved_state() == obj - - -def test_saved_state_needed(): - doc_1 = DocumentWithTurnedOffStateManagement(num_1=1, num_2=2) - with pytest.raises(StateManagementIsTurnedOff): - doc_1.is_changed - - doc_2 = DocumentWithTurnedOnStateManagement( - num_1=1, num_2=2, internal=InternalDoc() - ) - with pytest.raises(StateNotSaved): - doc_2.is_changed - - -def test_if_changed(doc_default): - assert doc_default.is_changed is False - doc_default.num_1 = 10 - assert doc_default.is_changed is True - - -def test_get_changes_default(doc_default): - doc_default.internal.num = 1000 - doc_default.internal.string = "new_value" - doc_default.internal.lst.append(100) - assert doc_default.get_changes() == { - "internal.num": 1000, - "internal.string": "new_value", - "internal.lst": [1, 2, 3, 4, 5, 100], - } - - -def test_get_changes_default_whole(doc_default): - doc_default.internal = {"num": 1000, "string": "new_value"} - assert doc_default.get_changes() == { - "internal.num": 1000, - "internal.string": "new_value", - } - - -def test_get_changes_replace(doc_replace): - doc_replace.internal.num = 1000 - doc_replace.internal.string = "new_value" - assert doc_replace.get_changes() == { - "internal": { - "num": 1000, - "string": "new_value", - "lst": [1, 2, 3, 4, 5], - } - } - - -def test_get_changes_replace_whole(doc_replace): - doc_replace.internal = {"num": 1000, "string": "new_value"} - assert doc_replace.get_changes() == { - "internal": { - "num": 1000, - "string": "new_value", - } - } - - -def test_save_changes(saved_doc_default): - saved_doc_default.internal.num = 10000 - saved_doc_default.internal.change_private() - assert saved_doc_default.internal.get_private() == "PRIVATE_CHANGED" - - saved_doc_default.save_changes() - assert saved_doc_default.get_saved_state()["internal"]["num"] == 10000 - assert saved_doc_default.internal.get_private() == "PRIVATE_CHANGED" - - new_doc = DocumentWithTurnedOnStateManagement.get( - saved_doc_default.id - ).run() - assert new_doc.internal.num == 10000 - - -def test_find_one(saved_doc_default, state): - new_doc = DocumentWithTurnedOnStateManagement.get( - saved_doc_default.id - ).run() - assert new_doc.get_saved_state() == state - - new_doc = DocumentWithTurnedOnStateManagement.find_one( - DocumentWithTurnedOnStateManagement.id == saved_doc_default.id - ).run() - assert new_doc.get_saved_state() == state - - -def test_find_many(): - docs = [] - for i in range(10): - docs.append( - DocumentWithTurnedOnStateManagement( - num_1=i, num_2=i + 1, internal=InternalDoc() - ) - ) - DocumentWithTurnedOnStateManagement.insert_many(docs) - - found_docs = DocumentWithTurnedOnStateManagement.find( - DocumentWithTurnedOnStateManagement.num_1 > 4 - ).to_list() - - for doc in found_docs: - assert doc.get_saved_state() is not None - - -def test_insert(state_without_id): - doc = DocumentWithTurnedOnStateManagement.parse_obj(state_without_id) - assert doc.get_saved_state() is None - doc.insert() - new_state = doc.get_saved_state() - assert new_state["_id"] is not None - del new_state["_id"] - assert new_state == state_without_id - - -def test_replace(saved_doc_default): - saved_doc_default.num_1 = 100 - saved_doc_default.replace() - assert saved_doc_default.get_saved_state()["num_1"] == 100 - - -def test_save_chages(saved_doc_default): - saved_doc_default.num_1 = 100 - saved_doc_default.save_changes() - assert saved_doc_default.get_saved_state()["num_1"] == 100 - - -def test_rollback(doc_default, state): - doc_default.num_1 = 100 - doc_default.rollback() - assert doc_default.num_1 == state["num_1"] - - -@pytest.fixture -def windows_not_inserted(): - return [WindowWithRevision(x=10, y=10), WindowWithRevision(x=11, y=11)] - - -@pytest.fixture -def house_not_inserted(windows_not_inserted): - return HouseWithRevision(windows=windows_not_inserted) - - -@pytest.fixture -def house(house_not_inserted): - return house_not_inserted.insert(link_rule=WriteRules.WRITE) - - -def test_fetch_save_changes(house): - data = HouseWithRevision.all(fetch_links=True).to_list() - house = data[0] - window_0 = house.windows[0] - window_0.x = 10000 - window_0.save_changes() diff --git a/tests/sync/odm/test_timesries_collection.py b/tests/sync/odm/test_timesries_collection.py deleted file mode 100644 index e7528dfc..00000000 --- a/tests/sync/odm/test_timesries_collection.py +++ /dev/null @@ -1,37 +0,0 @@ -import pytest - -from beanie.sync import init_beanie -from beanie.exceptions import MongoDBVersionError -from tests.sync.models import DocumentWithTimeseries - - -def test_timeseries_collection(db): - build_info = db.command({"buildInfo": 1}) - mongo_version = build_info["version"] - major_version = int(mongo_version.split(".")[0]) - if major_version < 5: - with pytest.raises(MongoDBVersionError): - init_beanie(database=db, document_models=[DocumentWithTimeseries]) - - if major_version >= 5: - init_beanie(database=db, document_models=[DocumentWithTimeseries]) - info = db.command( - { - "listCollections": 1, - "filter": {"name": "DocumentWithTimeseries"}, - } - ) - - assert info["cursor"]["firstBatch"][0] == { - "name": "DocumentWithTimeseries", - "type": "timeseries", - "options": { - "expireAfterSeconds": 2, - "timeseries": { - "timeField": "ts", - "granularity": "seconds", - "bucketMaxSpanSeconds": 3600, - }, - }, - "info": {"readOnly": False}, - } diff --git a/tests/sync/odm/test_views.py b/tests/sync/odm/test_views.py deleted file mode 100644 index bfbb0e4c..00000000 --- a/tests/sync/odm/test_views.py +++ /dev/null @@ -1,19 +0,0 @@ -from tests.sync.odm.views import TestView - - -class TestViews: - def test_simple(self, documents): - documents(number=15) - results = TestView.all().to_list() - assert len(results) == 6 - - def test_aggregate(self, documents): - documents(number=15) - results = TestView.aggregate( - [ - {"$set": {"test_field": 1}}, - {"$match": {"$expr": {"$lt": ["$number", 12]}}}, - ] - ).to_list() - assert len(results) == 3 - assert results[0]["test_field"] == 1 diff --git a/tests/sync/odm/views.py b/tests/sync/odm/views.py deleted file mode 100644 index efefb9a9..00000000 --- a/tests/sync/odm/views.py +++ /dev/null @@ -1,15 +0,0 @@ -from beanie.sync.odm.views import View -from tests.sync.models import DocumentTestModel - - -class TestView(View): - number: int - string: str - - class Settings: - view_name = "test_view" - source = DocumentTestModel - pipeline = [ - {"$match": {"$expr": {"$gt": ["$test_int", 8]}}}, - {"$project": {"number": "$test_int", "string": "$test_str"}}, - ] diff --git a/tests/test_beanie.py b/tests/test_beanie.py index c534bd19..941710d2 100644 --- a/tests/test_beanie.py +++ b/tests/test_beanie.py @@ -2,4 +2,4 @@ def test_version(): - assert __version__ == "1.14.0" + assert __version__ == "1.15.0"