From 41c0fcabe98d1c793afd73d4582c48f5e700e6d6 Mon Sep 17 00:00:00 2001 From: tarsil Date: Tue, 16 Jan 2024 09:59:51 +0000 Subject: [PATCH 1/3] Add run_sync for mongoz --- mongoz/core/db/documents/metaclasses.py | 4 ++-- mongoz/core/sync.py | 21 --------------------- mongoz/core/utils/sync.py | 16 ++++++++++++++++ 3 files changed, 18 insertions(+), 23 deletions(-) delete mode 100644 mongoz/core/sync.py create mode 100644 mongoz/core/utils/sync.py diff --git a/mongoz/core/db/documents/metaclasses.py b/mongoz/core/db/documents/metaclasses.py index 232a817..d5743e7 100644 --- a/mongoz/core/db/documents/metaclasses.py +++ b/mongoz/core/db/documents/metaclasses.py @@ -24,8 +24,8 @@ from mongoz.core.db.fields.base import BaseField, MongozField from mongoz.core.db.querysets.core.manager import Manager from mongoz.core.signals import Broadcaster, Signal -from mongoz.core.sync import execsync from mongoz.core.utils.functional import extract_field_annotations_and_defaults, mongoz_setattr +from mongoz.core.utils.sync import run_sync from mongoz.exceptions import ImproperlyConfigured, IndexError if TYPE_CHECKING: @@ -385,7 +385,7 @@ def __search_for_fields(base: Type, attrs: Any) -> None: # Build the indexes if not meta.abstract and meta.indexes and meta.autogenerate_index: if not new_class.is_proxy_document: - execsync(new_class.create_indexes)() + run_sync(new_class.create_indexes()) return new_class @property diff --git a/mongoz/core/sync.py b/mongoz/core/sync.py deleted file mode 100644 index e890f0e..0000000 --- a/mongoz/core/sync.py +++ /dev/null @@ -1,21 +0,0 @@ -import functools -from typing import Any - -import anyio -from anyio._core._eventloop import threadlocals - - -def execsync(async_function: Any, raise_error: bool = True) -> Any: - """ - Runs any async function inside a blocking function (sync). - """ - - @functools.wraps(async_function) - def wrapper(*args: Any, **kwargs: Any) -> Any: - current_async_module = getattr(threadlocals, "current_async_module", None) - partial_func = functools.partial(async_function, *args, **kwargs) - if current_async_module is not None and raise_error is True: - return anyio.from_thread.run(partial_func) - return anyio.run(partial_func) - - return wrapper diff --git a/mongoz/core/utils/sync.py b/mongoz/core/utils/sync.py new file mode 100644 index 0000000..1e09bd2 --- /dev/null +++ b/mongoz/core/utils/sync.py @@ -0,0 +1,16 @@ +import asyncio +from concurrent import futures +from concurrent.futures import Future +from typing import Any, Awaitable + + +def run_sync(async_function: Awaitable) -> Any: + """ + Runs the queries in sync mode + """ + try: + return asyncio.run(async_function) + except RuntimeError: + with futures.ThreadPoolExecutor(max_workers=1) as executor: + future: Future = executor.submit(asyncio.run, async_function) + return future.result() From 6875ba28319bd47a430071a2c457e07e69d8f8d2 Mon Sep 17 00:00:00 2001 From: tarsil Date: Tue, 16 Jan 2024 10:06:58 +0000 Subject: [PATCH 2/3] Add dymmond-settings to requirements * Deprecate pydantic-settings --- docs/settings.md | 8 +++---- mongoz/__init__.py | 2 ++ mongoz/conf/__init__.py | 43 +++------------------------------- mongoz/conf/config.py | 20 ---------------- mongoz/conf/enums.py | 9 ------- mongoz/conf/global_settings.py | 15 ++++++------ mongoz/conf/module_import.py | 22 ----------------- pyproject.toml | 2 +- 8 files changed, 18 insertions(+), 103 deletions(-) delete mode 100644 mongoz/conf/config.py delete mode 100644 mongoz/conf/enums.py delete mode 100644 mongoz/conf/module_import.py diff --git a/docs/settings.md b/docs/settings.md index b21d6b9..24a215b 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -10,17 +10,17 @@ This is exactly what happened. The way of using the settings object within a Mongoz use of the ORM is via: -* **MONGOZ_SETTINGS_MODULE** environment variable. +* **SETTINGS_MODULE** environment variable. All the settings are **[Pydantic BaseSettings](https://pypi.org/project/pydantic-settings/)** objects which makes it easier to use and override when needed. -### MONGOZ_SETTINGS_MODULE +### SETTINGS_MODULE -Mongoz by default uses is looking for a `MONGOZ_SETTINGS_MODULE` environment variable to run and +Mongoz by default uses is looking for a `SETTINGS_MODULE` environment variable to run and apply the given settings to your instance. -If no `MONGOZ_SETTINGS_MODULE` is found, Mongoz then uses its own internal settings which are +If no `SETTINGS_MODULE` is found, Mongoz then uses its own internal settings which are widely applied across the system. #### Custom settings diff --git a/mongoz/__init__.py b/mongoz/__init__.py index 3e0ed28..f4206e4 100644 --- a/mongoz/__init__.py +++ b/mongoz/__init__.py @@ -29,6 +29,7 @@ from .core.db.querysets.expressions import Expression, SortExpression from .core.db.querysets.operators import Q from .core.signals import Signal +from .core.utils.sync import run_sync from .exceptions import DocumentNotFound, ImproperlyConfigured, MultipleDocumentsReturned __all__ = [ @@ -67,4 +68,5 @@ "Time", "UUID", "settings", + "run_sync", ] diff --git a/mongoz/conf/__init__.py b/mongoz/conf/__init__.py index cabcbb2..91f6a48 100644 --- a/mongoz/conf/__init__.py +++ b/mongoz/conf/__init__.py @@ -1,43 +1,6 @@ import os -from typing import Any, Optional, Type -from mongoz.conf.functional import LazyObject, empty -from mongoz.conf.module_import import import_string +if not os.environ.get("SETTINGS_MODULE"): + os.environ.setdefault("SETTINGS_MODULE", "mongoz.conf.global_settings.MongozSettings") -ENVIRONMENT_VARIABLE = "MONGOZ_SETTINGS_MODULE" - -DBSettings = Type["MongozLazySettings"] - - -class MongozLazySettings(LazyObject): - def _setup(self, name: Optional[str] = None) -> None: - """ - Load the settings module pointed to by the environment variable. This - is used the first time settings are needed, if the user hasn't - configured settings manually. - """ - settings_module: str = os.environ.get( - ENVIRONMENT_VARIABLE, "mongoz.conf.global_settings.MongozSettings" - ) - settings: Any = import_string(settings_module) - - for setting, _ in settings().model_dump(exclude={"filter_operators"}).items(): - assert setting.islower(), "%s should be in lowercase." % setting - - self._wrapped = settings() - - def __repr__(self: "MongozLazySettings") -> str: - # Hardcode the class name as otherwise it yields 'Settings'. - if self._wrapped is empty: - return "" - return ''.format( - settings_module=self._wrapped.__class__.__name__ - ) - - @property - def configured(self) -> Any: - """Return True if the settings have already been configured.""" - return self._wrapped is not empty - - -settings: DBSettings = MongozLazySettings() # type: ignore +from dymmond_settings import settings as settings diff --git a/mongoz/conf/config.py b/mongoz/conf/config.py deleted file mode 100644 index bb8aa82..0000000 --- a/mongoz/conf/config.py +++ /dev/null @@ -1,20 +0,0 @@ -"""Configuration for Pydantic models.""" -from __future__ import annotations as _annotations - -from typing_extensions import TypedDict - - -class ModelConfig(TypedDict, total=False): - """ - A TypedDict for configuring Document behaviour. - """ - - strict: bool - """ - Weather the model should be strict in the creation or not. - """ - populate_by_alias: bool - """ - Whether an aliased field may be populated by its name as given by the model - attribute, as well as the alias. Defaults to `False`. - """ diff --git a/mongoz/conf/enums.py b/mongoz/conf/enums.py deleted file mode 100644 index 35694e8..0000000 --- a/mongoz/conf/enums.py +++ /dev/null @@ -1,9 +0,0 @@ -from enum import Enum - - -class EnvironmentType(str, Enum): - """An Enum for HTTP methods.""" - - DEVELOPMENT = "development" - TESTING = "testing" - PRODUCTION = "production" diff --git a/mongoz/conf/global_settings.py b/mongoz/conf/global_settings.py index 6738ad4..7cfd527 100644 --- a/mongoz/conf/global_settings.py +++ b/mongoz/conf/global_settings.py @@ -1,7 +1,8 @@ +from dataclasses import dataclass from functools import cached_property -from typing import TYPE_CHECKING, Dict, List, cast +from typing import TYPE_CHECKING, ClassVar, Dict, List, cast -from pydantic_settings import BaseSettings, SettingsConfigDict +from dymmond_settings import Settings from mongoz.exceptions import OperatorInvalid @@ -9,14 +10,14 @@ from mongoz import Expression -class MongozSettings(BaseSettings): - model_config = SettingsConfigDict(extra="allow", ignored_types=(cached_property,)) - ipython_args: List[str] = ["--no-banner"] +@dataclass +class MongozSettings(Settings): + ipython_args: ClassVar[List[str]] = ["--no-banner"] ptpython_config_file: str = "~/.config/ptpython/config.py" - parsed_ids: List[str] = ["id", "pk"] + parsed_ids: ClassVar[List[str]] = ["id", "pk"] - filter_operators: Dict[str, str] = { + filter_operators: ClassVar[Dict[str, str]] = { "exact": "eq", "neq": "neq", "contains": "contains", diff --git a/mongoz/conf/module_import.py b/mongoz/conf/module_import.py deleted file mode 100644 index 59e9105..0000000 --- a/mongoz/conf/module_import.py +++ /dev/null @@ -1,22 +0,0 @@ -from importlib import import_module -from typing import Any - - -def import_string(dotted_path: str) -> Any: - """ - Import a dotted module path and return the attribute/class designated by the - last name in the path. Raise ImportError if the import failed. - """ - try: - module_path, class_name = dotted_path.rsplit(".", 1) - except ValueError as err: - raise ImportError("%s doesn't look like a module path" % dotted_path) from err - - module = import_module(module_path) - - try: - return getattr(module, class_name) - except AttributeError as err: - raise ImportError( - 'Module "{}" does not define a "{}" attribute/class'.format(module_path, class_name) - ) from err diff --git a/pyproject.toml b/pyproject.toml index 10b0489..83f380d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,9 +40,9 @@ classifiers = [ ] dependencies = [ "motor>=3.3.1", + "dymmond-settings>=1.0.1", "orjson>=3.9.5", "pydantic>=2.5.3,<3.0.0", - "pydantic-settings>=2.0.3", ] keywords = ["mongoz"] From bde2d6e15f3eb0c08e179467b38fd3ab703358e7 Mon Sep 17 00:00:00 2001 From: tarsil Date: Tue, 16 Jan 2024 10:29:10 +0000 Subject: [PATCH 3/3] Add run_sync and fixing typing --- docs/queries.md | 38 ++++++++++++++++++++++ mongoz/core/db/querysets/core/manager.py | 22 ++++++------- mongoz/core/db/querysets/core/protocols.py | 17 ++++++++++ 3 files changed, 66 insertions(+), 11 deletions(-) create mode 100644 mongoz/core/db/querysets/core/protocols.py diff --git a/docs/queries.md b/docs/queries.md index d9b3049..c46b14e 100644 --- a/docs/queries.md +++ b/docs/queries.md @@ -1190,4 +1190,42 @@ users = await User.query(Q.lt(User.id, 20)).all() users = await User.query(Q.lte(User.id, 20)).all() ``` +## Blocking Queries + +What happens if you want to use Mongoz with a blocking operation? So by blocking means `sync`. +For instance, Flask does not support natively `async` and Mongoz is an async agnotic ORM and you +probably would like to take advantage of Mongoz but you want without doing a lot of magic behind. + +Well, Mongoz also supports the `run_sync` functionality that allows you to run the queries in +*blocking* mode with ease! + +### How to use + +You simply need to use the `run_sync` functionality from Mongoz and make it happen almost immediatly. + +```python +from mongoz import run_sync +``` + +All the available functionalities of Mongoz run within this wrapper without extra syntax. + +Let us see some examples. + +**Async mode** + +```python +await User.query.all() +await User.query.filter(name__icontains="example") +await User.query.create(name="Mongoz") +``` + +**With run_sync** + +```python +from mongoz import run_sync + +run_sync(User.query.filter(name__icontains="example")) +run_sync(User.query.create(name="Mongoz")) +``` + [document]: ./documents.md diff --git a/mongoz/core/db/querysets/core/manager.py b/mongoz/core/db/querysets/core/manager.py index 66b9bdb..db2e330 100644 --- a/mongoz/core/db/querysets/core/manager.py +++ b/mongoz/core/db/querysets/core/manager.py @@ -4,7 +4,6 @@ AsyncGenerator, Dict, Generator, - Generic, List, Sequence, Set, @@ -27,6 +26,7 @@ ORDER_EQUALITY, VALUE_EQUALITY, ) +from mongoz.core.db.querysets.core.protocols import AwaitableQuery, MongozDocument from mongoz.core.db.querysets.expressions import Expression, SortExpression from mongoz.exceptions import DocumentNotFound, FieldDefinitionError, MultipleDocumentsReturned from mongoz.protocols.queryset import QuerySetProtocol @@ -38,7 +38,7 @@ T = TypeVar("T", bound="Document") -class Manager(QuerySetProtocol, Generic[T]): +class Manager(QuerySetProtocol, AwaitableQuery[MongozDocument]): def __init__( self, model_class: Union[Type["Document"], None] = None, @@ -47,7 +47,7 @@ def __init__( only_fields: Union[str, None] = None, defer_fields: Union[str, None] = None, ) -> None: - self.model_class = model_class + self.model_class = model_class # type: ignore if self.model_class: self._collection = self.model_class.meta.collection._collection # type: ignore @@ -107,8 +107,8 @@ def filter_only_and_defer(self, *fields: Sequence[str], is_only: bool = False) - if any(not isinstance(name, str) for name in document_fields): raise FieldDefinitionError("The fields must be must strings.") - if manager.model_class.meta.id_attribute not in fields and is_only: # type: ignore - document_fields.insert(0, manager.model_class.meta.id_attribute) # type: ignore + if manager.model_class.meta.id_attribute not in fields and is_only: + document_fields.insert(0, manager.model_class.meta.id_attribute) only_or_defer = "_only_fields" if is_only else "_defer_fields" setattr(manager, only_or_defer, document_fields) @@ -329,7 +329,7 @@ async def _all(self) -> List[T]: is_defer_fields = True if manager._defer_fields else False results: List[T] = [ - manager.model_class.from_row( # type: ignore + manager.model_class.from_row( document, is_only_fields=is_only_fields, only_fields=manager._only_fields, @@ -356,7 +356,7 @@ async def create(self, **kwargs: Any) -> "Document": """ manager: "Manager" = self.clone() instance = await manager.model_class(**kwargs).create() - return instance + return cast("Document", instance) async def delete(self) -> int: """Delete documents matching the criteria.""" @@ -382,7 +382,7 @@ async def last(self) -> Union[T, None]: Returns the last document of a matching criteria. """ manager: "Manager" = self.clone() - objects = await manager._all() + objects: Any = await manager._all() if not objects: return None return cast(T, objects[-1]) @@ -488,8 +488,8 @@ async def update_many(self, **kwargs: Any) -> List[T]: if field_definitions: pydantic_model: Type[pydantic.BaseModel] = pydantic.create_model( - __model_name=manager.model_class.__name__, # type: ignore - __config__=manager.model_class.model_config, # type: ignore + __model_name=manager.model_class.__name__, + __config__=manager.model_class.model_config, **field_definitions, ) model = pydantic_model.model_validate(kwargs) @@ -614,5 +614,5 @@ async def exclude(self, **kwargs: Any) -> List["Document"]: async def execute(self) -> Any: manager: "Manager" = self.clone() - records = await manager._all(**manager.extra) + records: Any = await manager._all(**manager.extra) return records diff --git a/mongoz/core/db/querysets/core/protocols.py b/mongoz/core/db/querysets/core/protocols.py new file mode 100644 index 0000000..7da4fa7 --- /dev/null +++ b/mongoz/core/db/querysets/core/protocols.py @@ -0,0 +1,17 @@ +import typing + +if typing.TYPE_CHECKING: + from mongoz.core.db.documents import Document + +# Create a var type for the Edgy Model +MongozDocument = typing.TypeVar("MongozDocument", bound="Document") + + +class AwaitableQuery(typing.Generic[MongozDocument]): + __slots__ = ("model_class",) + + def __init__(self, model_class: typing.Type[MongozDocument]) -> None: + self.model_class: typing.Type[MongozDocument] = model_class + + async def execute(self) -> typing.Any: + raise NotImplementedError()