Skip to content

Commit

Permalink
Added first simple iteration of the Document
Browse files Browse the repository at this point in the history
  • Loading branch information
tarsil committed Sep 19, 2023
1 parent 9462628 commit 6205af2
Show file tree
Hide file tree
Showing 17 changed files with 251 additions and 127 deletions.
3 changes: 3 additions & 0 deletions .pdbrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import os

alias kkk os.system('kill -9 %d' % os.getpid())
2 changes: 2 additions & 0 deletions mongoz/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
__version__ = "0.1.0"

from .core.connection.database import Database
from .core.connection.registry import Registry
from .core.db.documents import Document, EmbeddedDocument
from .core.db.documents.managers import Manager
Expand Down Expand Up @@ -30,6 +31,7 @@
"Binary",
"Boolean",
"Choice",
"Database",
"Date",
"DateTime",
"Decimal",
Expand Down
5 changes: 1 addition & 4 deletions mongoz/core/connection/collections.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import asyncio
from typing import Any, Callable, Dict, List, Mapping, Sequence, Tuple, Union

from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorCollection, AsyncIOMotorDatabase
from motor.motor_asyncio import AsyncIOMotorCollection


class Collection:
Expand Down
3 changes: 0 additions & 3 deletions mongoz/core/db/constants.py

This file was deleted.

4 changes: 2 additions & 2 deletions mongoz/core/db/datastructures.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import enum
from typing import Any, List, Optional, Sequence, Tuple, Union
from typing import Any, List, Tuple, Union

import pymongo

Expand All @@ -23,7 +23,7 @@ class Index(pymongo.IndexModel):

def __init__(
self,
key: str = None,
key: Union[str, None] = None,
keys: List[Union[Tuple[str, Order], Tuple[str, IndexType]]] = None,
name: str = None,
background: bool = False,
Expand Down
31 changes: 24 additions & 7 deletions mongoz/core/db/documents/base.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
import copy
from functools import cached_property
from typing import TYPE_CHECKING, Any, ClassVar, Optional, Type, Union
from typing import TYPE_CHECKING, Any, ClassVar, Dict, List, Optional, Type, TypeVar, Union

import bson
from pydantic import BaseModel, ConfigDict
from pydantic_core._pydantic_core import SchemaValidator as SchemaValidator
from typing_extensions import Self

from mongoz.core.db.documents._internal import DescriptiveMeta
from mongoz.core.db.documents.managers import Manager
from mongoz.core.db.documents.metaclasses import BaseModelMeta, MetaInfo
from mongoz.core.db.documents.model_proxy import ProxyModel
from mongoz.core.db.fields.base import MongozField
from mongoz.core.db.fields.base import BaseField
from mongoz.core.db.fields.core import ObjectId
from mongoz.core.db.querysets.base import QuerySet
from mongoz.core.db.querysets.expressions import Expression
from mongoz.core.utils.models import DateParser, ModelParser, generify_model_fields

if TYPE_CHECKING:
from mongoz.core.db.documents import Document

EXCLUDED_LOOKUP = ["__model_references__", "_table"]

T = TypeVar("T", bound="MongozBaseModel")


class MongozBaseModel(BaseModel, DateParser, ModelParser, metaclass=BaseModelMeta):
Expand All @@ -33,11 +35,9 @@ class MongozBaseModel(BaseModel, DateParser, ModelParser, metaclass=BaseModelMet
validate_assignment=True,
)

id: Optional[ObjectId] = MongozField(alias="_id")
id: Optional[ObjectId] = BaseField(__type__=Any, alias="_id")
parent: ClassVar[Union[Type[Self], None]]
is_proxy_model: ClassVar[bool] = False

query: ClassVar[Manager] = Manager()
meta: ClassVar[MetaInfo] = MetaInfo(None)
Meta: ClassVar[DescriptiveMeta] = DescriptiveMeta()
__proxy_model__: ClassVar[Union[Type["Document"], None]] = None
Expand Down Expand Up @@ -79,3 +79,20 @@ def get_instance_name(self) -> str:
Returns the name of the class in lowercase.
"""
return self.__class__.__name__.lower()

@classmethod
def query(cls: Type[T], *values: Union[bool, Dict, Expression]) -> QuerySet[T]:
"""Filter query criteria."""
filter_by: List[Expression] = []
if not values:
return QuerySet(model_class=cls)

for arg in values:
assert isinstance(arg, (dict, Expression)), "Invalid argument to Query"
if isinstance(arg, dict):
query_expressions = Expression.unpack(arg)
filter_by.extend(query_expressions)
else:
filter_by.append(arg)

return QuerySet(model_class=cls, filter_by=filter_by)
8 changes: 4 additions & 4 deletions mongoz/core/db/documents/document.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class Document(MongozBaseModel):

__embedded__: ClassVar[bool] = False

async def insert(self: Type["Document"]) -> Type["Document"]:
async def create(self: Type["Document"]) -> Type["Document"]:
"""
Inserts a document.
"""
Expand All @@ -25,7 +25,7 @@ async def insert(self: Type["Document"]) -> Type["Document"]:
return self

@classmethod
async def insert_many(
async def create_many(
cls: Type["Document"], models: Sequence[Type["Document"]]
) -> List[Type["Document"]]:
"""
Expand Down Expand Up @@ -99,10 +99,10 @@ async def save(self: Type["Document"]) -> Type["Document"]:
return self

def __repr__(self) -> str:
return f"<{self.__class__.__name__}: {self}>"
return str(self)

def __str__(self) -> str:
return f"{self.__class__.__name__}({self.pkname}={self.pk})"
return f"{self.__class__.__name__}(id={self.id})"


class EmbeddedDocument(BaseModel, metaclass=EmbeddedModelMetaClass):
Expand Down
118 changes: 88 additions & 30 deletions mongoz/core/db/documents/metaclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@
from pydantic._internal._model_construction import ModelMetaclass

from mongoz.core.connection.collections import Collection
from mongoz.core.connection.database import Database
from mongoz.core.connection.registry import Registry
from mongoz.core.db.datastructures import Index
from mongoz.core.db.documents.managers import Manager
from mongoz.core.db.fields.core import BaseField
from mongoz.core.db.fields.base import BaseField, MongozField
from mongoz.core.utils.functional import extract_field_annotations_and_defaults, mongoz_setattr
from mongoz.exceptions import ImproperlyConfigured

Expand All @@ -41,9 +42,9 @@ class MetaInfo:
"indexes",
"parents",
"manager",
"model",
"managers",
"signals",
"database",
)

def __init__(self, meta: Any = None, **kwargs: Any) -> None:
Expand All @@ -56,10 +57,10 @@ def __init__(self, meta: Any = None, **kwargs: Any) -> None:
self.registry: Optional[Type[Registry]] = getattr(meta, "registry", None)
self.collection: Optional[Collection] = getattr(meta, "collection", None)
self.parents: Any = getattr(meta, "parents", None) or []
self.model: Optional[Type["Document"]] = None
self.manager: "Manager" = getattr(meta, "manager", Manager())
self.indexes: List[Index] = getattr(meta, "indexes", None)
self.managers: List[Manager] = getattr(meta, "managers", [])
self.database: Union["str", Database] = getattr(meta, "database", None)
# self.signals: Optional[Broadcaster] = {} # type: ignore

def model_dump(self) -> Dict[Any, Any]:
Expand All @@ -73,7 +74,7 @@ def load_dict(self, values: Dict[str, Any]) -> None:
mongoz_setattr(self, key, value)


def _check_model_inherited_registry(bases: Tuple[Type, ...]) -> Type[Registry]:
def _check_document_inherited_registry(bases: Tuple[Type, ...]) -> Type[Registry]:
"""
When a registry is missing from the Meta class, it should look up for the bases
and obtain the first found registry.
Expand All @@ -93,11 +94,45 @@ def _check_model_inherited_registry(bases: Tuple[Type, ...]) -> Type[Registry]:

if not found_registry:
raise ImproperlyConfigured(
"Registry for the table not found in the Meta class or any of the superclasses. You must set thr registry in the Meta."
"Registry for the table not found in the Meta class or any of the superclasses. You must set the registry in the Meta."
)
return found_registry


def _check_document_inherited_database(
bases: Tuple[Type, ...], registry: Registry
) -> Type[Registry]:
"""
When a database is missing from the Meta class, it should look up for the bases
and obtain the first found database.
If not found, then a ImproperlyConfigured exception is raised.
"""
found_database: Optional[str] = None

for base in bases:
meta: MetaInfo = getattr(base, "meta", None) # type: ignore
if not meta:
continue

if getattr(meta, "database", None) is not None:
if isinstance(meta.database, str):
found_database = registry.get_database(meta.database)
break
elif isinstance(meta.database, Database):
found_database = meta.database
break
raise ImproperlyConfigured(
"database must be a string name or an instance of mongoz.Database."
)

if not found_database:
raise ImproperlyConfigured(
"Database for the table not found in the Meta class or any of the superclasses. You must set the database in the Meta."
)
return found_database


def _check_manager_for_bases(
base: Tuple[Type, ...],
attrs: Any,
Expand All @@ -121,14 +156,15 @@ class BaseModelMeta(ModelMetaclass):
__mongoz_fields__: ClassVar[Mapping[str, Type["BaseField"]]]

@no_type_check
def __new__(cls, name: str, bases: Tuple[Type, ...], attrs: Any) -> Any:
def __new__(cls, name: str, bases: Tuple[Type, ...], attrs: Any, **kwargs: Any) -> Any:
fields: Dict[str, BaseField] = {}
meta_class: "object" = attrs.get("Meta", type("Meta", (), {}))
pk_attribute: str = "id"
registry: Any = None

# Extract the custom Mongoz Fields in a pydantic format.
attrs, model_fields = extract_field_annotations_and_defaults(attrs)
cls.__mongoz_fields__ = model_fields
super().__new__(cls, name, bases, attrs)

# Searching for fields "Field" in the class hierarchy.
Expand Down Expand Up @@ -197,8 +233,11 @@ def __search_for_fields(base: Type, attrs: Any) -> None:
meta.parents = parents
new_class = cast("Type[Document]", model_class(cls, name, bases, attrs))

# Update the model_fields are updated to the latest
new_class.model_fields.update(model_fields)
if "id" in new_class.model_fields:
new_class.model_fields["id"].default = None

# # Update the model_fields are updated to the latest
# new_class.model_fields = model_fields

# Abstract classes do not allow multiple managers. This make sure it is enforced.
if meta.abstract:
Expand All @@ -220,14 +259,27 @@ def __search_for_fields(base: Type, attrs: Any) -> None:
and hasattr(new_class, "__embedded__")
and new_class.__embedded__
):
meta.registry = _check_model_inherited_registry(bases)
meta.registry = _check_document_inherited_registry(bases)
else:
return new_class

# Making sure the collection is always set if the value is not provided
collection_name: Optional[str] = None
if getattr(meta, "collection", None) is None:
collection = f"{name.lower()}s"
meta.collection = collection
collection_name = f"{name.lower()}s"
else:
collection_name = meta.collection.name

# Assert the databse is also specified
if getattr(meta, "database", None) is None:
meta.database = _check_document_inherited_database(bases, registry=meta.registry)
else:
if isinstance(meta.database, str):
meta.database = meta.registry.get_database(meta.database)
elif not isinstance(meta.database, Database):
raise ImproperlyConfigured(
"database must be a string name or an instance of mongoz.Database."
)

# Handle indexes
if getattr(meta, "indexes", None) is not None:
Expand All @@ -245,31 +297,23 @@ def __search_for_fields(base: Type, attrs: Any) -> None:
field.registry = registry

new_class.__db_model__ = True
cls.__mongoz_fields__ = meta.fields_mapping
meta.model = new_class
meta.collection = meta.database.get_collection(collection_name)
meta.manager.model_class = new_class

# Set the owner of the field
for _, value in new_class.__mongoz_fields__.items():
value.owner = new_class

# Set the manager
for _, value in attrs.items():
if isinstance(value, Manager):
value.model_class = new_class

# Update the model references with the validations of the model
# Being done by the Mongoz fields instead.
# Generates a proxy model for each model created
# Making sure the core model where the fields are inherited
# And mapped contains the main proxy_model
if not new_class.is_proxy_model and not new_class.meta.abstract:
proxy_model = new_class.generate_proxy_model()
new_class.__proxy_model__ = proxy_model
new_class.__proxy_model__.parent = new_class
new_class.__proxy_model__.model_rebuild(force=True)

new_class.model_rebuild(force=True)
mongoz_fields: Dict[str, MongozField] = {}
for field_name, field in new_class.model_fields.items():
if not field.name:
field.name = field_name

new_field = MongozField(pydantic_field=field, model_class=field.annotation)
mongoz_fields[field_name] = new_field

cls.__mongoz_fields__ = mongoz_fields
return new_class

@property
Expand All @@ -279,14 +323,28 @@ def proxy_model(cls) -> Any:
"""
return cls.__proxy_model__

def __getattr__(self, name: str) -> Any:
if name in self.__mongoz_fields__:
return self.__mongoz_fields__[name]
return super().__getattribute__(name)


class EmbeddedModelMetaClass(ModelMetaclass):
__mongoz_fields__: Mapping[str, BaseField]

@no_type_check
def __new__(cls, name: str, bases: Tuple[Type, ...], attrs: Any) -> Any:
# Extract the custom Mongoz Fields in a pydantic format.

attrs, model_fields = extract_field_annotations_and_defaults(attrs)
cls = super().__new__(cls, name, bases, attrs)
cls.__mongoz_fields__ = model_fields
cls = super().__new__(cls, name, bases, attrs)

mongoz_fields: Dict[str, MongozField] = {}
for field_name, field in cls.model_fields.items():
if not field.name:
field.name = field_name
new_field = MongozField(pydantic_field=field, model_class=cls)
mongoz_fields[field_name] = new_field
cls.__mongoz_fields__ = mongoz_fields
return cls
Loading

0 comments on commit 6205af2

Please sign in to comment.