Commit 08958bbf authored by Yury's avatar Yury

document: add new relations structure

parent 66d31790
Pipeline #320 failed with stages
in 3 minutes and 47 seconds
......@@ -43,6 +43,81 @@ class ClientNotFound(DocumentException):
super().__init__("Client is not provided. Can't connect to database.")
class DocumentField:
def __init__(self, document_cls, name=None, parent=False):
self.document_cls = document_cls = name
self.parent = parent
def __getattr__(self, item):
return DocumentField(self.document_cls, item)
def __getitem__(self, item):
def __repr__(self):
return f"{self.document_cls.__name__}.{}"
def __eq__(self, other: "DocumentField"):
return self.document_cls is other.document_cls and ==
except AttributeError:
raise TypeError(f"Can't compare {other} to DocumentField.")
class DocumentRelations:
def __init__(self):
self.relations = []
def add(self, document_func, first_document_field_name: str,
second_document_field: DocumentField, parent) -> None:
"""Adds new document relation."""
"document_func": document_func,
"first_document_field_name": first_document_field_name,
"second_document_field": second_document_field,
"first_is_parent": parent
def search_by_field(self, field: DocumentField):
"""Search for related fields by DocumentField."""
related_to_field = []
for relation in self.relations:
first_document: type = relation["document_func"](None)
second_document_field: DocumentField = relation["second_document_field"]
if (field.document_cls is first_document
and == relation["first_document_field_name"]):
elif field == second_document_field:
DocumentField(first_document, relation["first_document_field_name"],
not relation["first_is_parent"]))
return related_to_field
def search_by_document(self, document: "Document"):
"""Search for related fields by DocumentField."""
related_field = []
for relation in self.relations:
first_document: type = relation["document_func"](None)
second_document_field: DocumentField = relation["second_document_field"]
if document.__class__ is first_document:
first_document_field = DocumentField(document.__class__,
related_field.append([first_document_field, second_document_field])
elif second_document_field.document_cls is document.__class__:
first_document_field = DocumentField(first_document,
related_field.append([second_document_field, first_document_field])
return related_field
class MetaDocument(type):
database: str
collection: str
......@@ -69,9 +144,13 @@ class MetaDocument(type):
return super().__getattribute__("_collection")
return super().__getattribute__(item)
def Field(cls):
return DocumentField(cls)
class Document(metaclass=MetaDocument):
relations = {}
Relations = DocumentRelations()
def __init__(self, **kwargs):
super().__setattr__("_document_", DocumentDict(kwargs))
......@@ -130,25 +209,13 @@ class Document(metaclass=MetaDocument):
async def _update_related(self):
"""Force updates related fields in other documents."""
self_relations = set({v for v in self.__class__.__dict__.values()
if isinstance(v, property)})
related_fields = self.Relations.search_by_document(self)
relate_properties = set(Document.relations).intersection(self_relations)
if not relate_properties:
property_data = [Document.relations[relate] for relate in relate_properties]
for prop in property_data:
self_field = prop["self_field"]
other_field = prop["other_field"]
document = prop["other_document_func"]()
if self[self_field] != self._shadow_copy_[self_field]:
await document.collection.update_many(
{other_field: self._shadow_copy_[self_field]},
{"$set": {other_field: self[self_field]}}
for self_field, other_field in related_fields:
if self_field.parent:
await other_field.document_cls._collection().update_many({ self.shadow_copy[]
}, {"$set": { self[]}})
async def push_update(self):
......@@ -196,7 +263,6 @@ class Document(metaclass=MetaDocument):
await cls._collection().insert_one(kwargs)
return cls(**kwargs)
async def _delete_by_prop(self, relations):
for prop, delete_info in relations.items():
......@@ -211,14 +277,13 @@ class Document(metaclass=MetaDocument):
async def _delete_related(self):
"""Deletes related documents or pops field values."""
self_properties = set({v for v in self.__class__.__dict__.values()
if isinstance(v, property)})
relations = set(Document.relations).intersection(self_properties)
related_fields = self.Relations.search_by_document(self)
await self._delete_by_prop({prop: delete_info for prop, delete_info
in Document.relations.items() if prop in relations
for self_field, other_field in related_fields:
if self_field.parent:
await other_field.document_cls._collection().delete({ self[]
async def delete(self):
"""Delete current document and related fields or documents base on related."""
......@@ -228,50 +293,38 @@ class Document(metaclass=MetaDocument):
return result
def related(self_path, other_path, multiple=True, parent=True):
def related(other_field: DocumentField, multiple=True, parent=True, self_field_name=None,
"""Decorator for related documents.
:param other_is_parent: defines that other document is a Parent
:param parent: show relations type. When Parent updated or deleted Child is also updated
and deleted. When Child is updated or deleted Parent stays the same.
:param self_path: Document.key to self pk
:param other_path: Document.key to other pk
:param self_field_name: ThisDocument field pk for OtherDocument field
:param other_field: DocumentField path to other document
:param multiple: return multiple documents or only one
def func_wrapper(func):
self_field = ".".join(self_path.split(".")[1:])
other_field = ".".join(other_path.split(".")[1:])
other_field.parent = other_is_parent
def get_other_document(document):
for subclass in document.__subclasses__():
if subclass.__name__ == other_path.split(".")[0]:
return subclass
found = get_other_document(subclass)
if found:
return found
self_field = self_field_name if self_field_name else func.__name__
Document.Relations.add(func, self_field, other_field, parent)
async def fget(self):
other_document = get_other_document(Document)
field = DocumentField(self.__class__, self_field)
if multiple:
return await other_document.many(**{other_field: self[self_field]})
return await other_field.document_cls.many(
**{ self[]})
return await**{other_field: self[self_field]})
return await
**{ self[]})
result = property(fget=fget)
delete_item = {
"self_field": self_field,
"other_field": other_field,
"parent": parent,
"other_document_func": get_other_document
Document.relations[result] = delete_item
return result
return func_wrapper
......@@ -72,6 +72,10 @@ class FakeCollection:
for field in query["$unset"]:
del doc[field]
async def delete(self, query):
async for doc in self.find(query):
class FakeDatabase:
def __init__(self, data):
......@@ -342,9 +346,9 @@ class DocumentTestCase(TestCase):
database = "mdocument"
client = self.client
@Document.related("Video._id", "")
@Document.related(, self_field_name="_id")
def comments(self):
return VideoREL
for prop, params in Document._relations_.items():
self.assertEqual(params["other_field"], "video")
......@@ -365,9 +369,9 @@ class DocumentTestCase(TestCase):
database = "mdocument"
client = self.client
@Document.related("", "VideoREL._id", multiple=False, parent=False)
@Document.related(VideoREL.Field._id, multiple=False, parent=False)
def video(self):
return LikeREL
like = self.loop.run_until_complete(LikeREL.create(video=video._id, count=1))
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment