Commit 88bf47c0 authored by Yury's avatar Yury

document: add new relations structure

parent 66d31790
Pipeline #323 passed with stages
in 5 minutes and 28 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
self.name = name
self.parent = parent
def __getattr__(self, item):
return DocumentField(self.document_cls, item)
def __getitem__(self, item):
pass
def __repr__(self):
return f"{self.document_cls.__name__}.{self.name}"
def __eq__(self, other: "DocumentField"):
try:
return self.document_cls is other.document_cls and self.name == other.name
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."""
self.relations.append({
"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 field.name == relation["first_document_field_name"]):
related_to_field.append(second_document_field)
elif field == second_document_field:
related_to_field.append(
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__,
relation["first_document_field_name"],
relation["first_is_parent"])
related_field.append([first_document_field, second_document_field])
elif second_document_field.document_cls is document.__class__:
first_document_field = DocumentField(first_document,
relation["first_document_field_name"],
relation["first_is_parent"])
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)
@property
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:
return
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({
other_field.name: self.shadow_copy[self_field.name]
}, {"$set": {other_field.name: self[self_field.name]}})
@check_setup
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_many({
other_field.name: self[self_field.name]
})
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
@staticmethod
def related(self_path, other_path, multiple=True, parent=True):
def related(other_field: DocumentField, multiple=True, parent=True, self_field_name=None,
other_is_parent=False):
"""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
else:
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)
@wraps(func)
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(
**{other_field.name: self[field.name]})
else:
return await other_document.one(**{other_field: self[self_field]})
return await other_field.document_cls.one(
**{other_field.name: self[field.name]})
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
......@@ -9,14 +9,15 @@ from mdocument import ClientNotFound, Document, DocumentDoesntExist, DocumentExc
class DocumentTestCase(TestCase):
def setUp(self) -> None:
Document._relations_ = {}
self.loop = asyncio.get_event_loop()
self.client = motor.motor_asyncio.AsyncIOMotorClient()
self.loop.run_until_complete(self.client.drop_database("mdocument"))
def loop_run_lambda(self, func, *args, **kwargs):
return lambda: self.loop.run_until_complete(func(*args, **kwargs))
def tearDown(self) -> None:
self.loop.run_until_complete(self.client.drop_database("mdocument"))
def test___init__(self):
"""Tests initiation of document."""
......@@ -129,7 +130,7 @@ class DocumentTestCase(TestCase):
video = self.loop.run_until_complete(Video.one(admin="Me"))
self.assertEqual(dict(video1), dict(video))
self.assertEqual(video1, video)
def test_client_not_set(self):
......@@ -246,14 +247,9 @@ class DocumentTestCase(TestCase):
database = "mdocument"
client = self.client
@Document.related("Video._id", "CommentsREL.video")
@Document.related(CommentsREL.Field.video, self_field_name="_id")
def comments(self):
pass
for prop, params in Document._relations_.items():
self.assertEqual(params["other_field"], "video")
self.assertEqual(params["self_field"], "_id")
self.assertEqual(params["parent"], True)
return VideoREL
video = self.loop.run_until_complete(VideoREL.create(author="me"))
comment1 = self.loop.run_until_complete(CommentsREL.create(video=video._id, text="test1"))
......@@ -261,7 +257,7 @@ class DocumentTestCase(TestCase):
for comment in self.loop.run_until_complete(video.comments):
self.assertTrue(
dict(comment) == dict(comment1) or dict(comment) == dict(comment2)
comment == comment1 or comment == comment2
)
class LikeREL(Document):
......@@ -269,14 +265,14 @@ class DocumentTestCase(TestCase):
database = "mdocument"
client = self.client
@Document.related("LikeREL.video", "VideoREL._id", multiple=False, parent=False)
@Document.related(VideoREL.Field._id, multiple=False, parent=False)
def video(self):
pass
return LikeREL
like = self.loop.run_until_complete(LikeREL.create(video=video._id, count=1))
self.assertEqual(
dict(self.loop.run_until_complete(like.video)), dict(video))
self.loop.run_until_complete(like.video), video)
self.loop.run_until_complete(video.delete())
self.assertEqual({
......@@ -293,3 +289,13 @@ class DocumentTestCase(TestCase):
"likes": self.loop.run_until_complete(
self.client["mdocument"]["likes"].find().to_list(9999)),
}})
def test_quals(self):
"""Tests valid comparison."""
d1 = Document(a=1)
d2 = Document(a=1)
d3 = Document(a=2)
self.assertTrue(d1 == d2)
self.assertFalse(d2 == d3)
......@@ -72,6 +72,10 @@ class FakeCollection:
for field in query["$unset"]:
del doc[field]
async def delete_many(self, query):
async for doc in self.find(query):
self.data.remove(doc)
class FakeDatabase:
def __init__(self, data):
......@@ -101,18 +105,26 @@ class FakeClient:
self.data[item] = {}
return FakeDatabase(self.data[item])
async def drop_database(self, name):
try:
self.data.pop(name)
except KeyError:
pass
class DocumentTestCase(TestCase):
@mock.patch("motor.motor_asyncio.AsyncIOMotorClient", new=FakeClient)
def setUp(self) -> None:
Document._relations_ = {}
self.loop = asyncio.get_event_loop()
self.client = motor.motor_asyncio.AsyncIOMotorClient()
def loop_run_lambda(self, func, *args, **kwargs):
return lambda: self.loop.run_until_complete(func(*args, **kwargs))
def tearDown(self) -> None:
self.loop.run_until_complete(self.client.drop_database("mdocument"))
def test___init__(self):
"""Tests initiation of document."""
......@@ -342,14 +354,9 @@ class DocumentTestCase(TestCase):
database = "mdocument"
client = self.client
@Document.related("Video._id", "CommentsREL.video")
@Document.related(CommentsREL.Field.video, self_field_name="_id")
def comments(self):
pass
for prop, params in Document._relations_.items():
self.assertEqual(params["other_field"], "video")
self.assertEqual(params["self_field"], "_id")
self.assertEqual(params["parent"], True)
return VideoREL
video = self.loop.run_until_complete(VideoREL.create(author="me"))
comment1 = self.loop.run_until_complete(CommentsREL.create(video=video._id, text="test1"))
......@@ -365,9 +372,9 @@ class DocumentTestCase(TestCase):
database = "mdocument"
client = self.client
@Document.related("LikeREL.video", "VideoREL._id", multiple=False, parent=False)
@Document.related(VideoREL.Field._id, multiple=False, parent=False)
def video(self):
pass
return LikeREL
like = self.loop.run_until_complete(LikeREL.create(video=video._id, count=1))
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment