Skip to content

Commit

Permalink
Merge pull request #20 from tarsil/feature/run_sync
Browse files Browse the repository at this point in the history
Feature/run sync
  • Loading branch information
tarsil authored Jan 16, 2024
2 parents bcc824f + bde2d6e commit 8084915
Show file tree
Hide file tree
Showing 14 changed files with 102 additions and 137 deletions.
38 changes: 38 additions & 0 deletions docs/queries.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
8 changes: 4 additions & 4 deletions docs/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions mongoz/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__ = [
Expand Down Expand Up @@ -67,4 +68,5 @@
"Time",
"UUID",
"settings",
"run_sync",
]
43 changes: 3 additions & 40 deletions mongoz/conf/__init__.py
Original file line number Diff line number Diff line change
@@ -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 "<MongozLazySettings [Unevaluated]>"
return '<MongozLazySettings "{settings_module}">'.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
20 changes: 0 additions & 20 deletions mongoz/conf/config.py

This file was deleted.

9 changes: 0 additions & 9 deletions mongoz/conf/enums.py

This file was deleted.

15 changes: 8 additions & 7 deletions mongoz/conf/global_settings.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
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

if TYPE_CHECKING:
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",
Expand Down
22 changes: 0 additions & 22 deletions mongoz/conf/module_import.py

This file was deleted.

4 changes: 2 additions & 2 deletions mongoz/core/db/documents/metaclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
22 changes: 11 additions & 11 deletions mongoz/core/db/querysets/core/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
AsyncGenerator,
Dict,
Generator,
Generic,
List,
Sequence,
Set,
Expand All @@ -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
Expand All @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand All @@ -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."""
Expand All @@ -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])
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
17 changes: 17 additions & 0 deletions mongoz/core/db/querysets/core/protocols.py
Original file line number Diff line number Diff line change
@@ -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()
21 changes: 0 additions & 21 deletions mongoz/core/sync.py

This file was deleted.

16 changes: 16 additions & 0 deletions mongoz/core/utils/sync.py
Original file line number Diff line number Diff line change
@@ -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()
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]

Expand Down

0 comments on commit 8084915

Please sign in to comment.