diff --git a/README.md b/README.md index 26910dd..f99ad6c 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ The goals of this project include providing an object-oriented description of th This project is based on the official package nebula-python https://github.com/vesoft-inc/nebula-python. -At present, the project has just started and is not stable yet, the code changes rapidly and the method is unstable. +At present, the project is not stable yet, the code changes rapidly and the methods are unstable. If you have any ideas for improving this project, you are very welcome to contact me via issue or email. ## Requirements @@ -150,7 +150,8 @@ class LimitedCharacter(models.VertexModel): * An EdgeModel is used to define a nebula edge. But note that there will be no subclasses for edge model since we don't need it. ### Migrations -use `make_migrations` and `migrate` to synchronize the schema to current space. +Use `make_migrations` and `migrate` to synchronize the schema to current space. +Please note that only final consistency on DB schema is currently supported. ```python from nebula_carina.models.migrations import make_migrations, migrate @@ -162,6 +163,12 @@ make_migrations() migrate(make_migrations()) ``` +#### Django +If you are using Django, you can do the migration by the following command: +``` +python manage.py nebulamigrate +``` + ### Data Model Method ```python from example.models import VirtualCharacter, Figure, Source, LimitedCharacter, Love, Support @@ -281,16 +288,41 @@ async def what_a_complex_human_relation(character_id: str): #### Django Basically things are the same as in fastapi except that you have to use `.dict()` method to serialize a model before using it in response. -Match method would be harder to use. A wrapper that support `.dict()` method will be implemented in 0.3.0. +In django, you can use `ModelBuilder.serialized_match` method to directly get the serialized result of match method. ```python from example.models import VirtualCharacter from django.http import JsonResponse +from nebula_carina.models.models import EdgeModel +from nebula_carina.ngql.query.conditions import Q +from nebula_carina.models.model_builder import ModelBuilder def some_view(request, character_id: str): vr = VirtualCharacter.objects.get(character_id) # make sure that use .dict() function to serialize the result return JsonResponse(vr.dict()) + +def what_a_complex_human_relation(request, character_id: str): + return JsonResponse(ModelBuilder.serialized_match( + '(v)-[e:love]->(v2)-[e2:love]->(v3)', { + 'v': VirtualCharacter, 'e': EdgeModel, 'v2': VirtualCharacter, + 'e2': EdgeModel, 'v3': VirtualCharacter + }, + condition=Q(v__id=character_id), + ), safe=False) # returns a list of dict with keys {v, e, v2, e2, v3} + + +# same method as the previous one if you would like to access the models +def what_a_complex_human_relation_another(request, character_id: str): + return JsonResponse([ + single_match_result.dict() for single_match_result in ModelBuilder.match( + '(v)-[e:love]->(v2)-[e2:love]->(v3)', { + 'v': VirtualCharacter, 'e': EdgeModel, 'v2': VirtualCharacter, + 'e2': EdgeModel, 'v3': VirtualCharacter + }, + condition=Q(v__id=character_id), + ) + ], safe=False) # returns a list of dict with keys {v, e, v2, e2, v3} ``` #### Flask @@ -306,5 +338,5 @@ Flask usage is quite similar to the Django usage. Basically use `.dict()` functi - [ ] Default values for schema models - [ ] Generic Vertex Model - [x] Basic Django Support - - [ ] Django management.py - - [ ] Match Result Wrapper + - [x] Django management.py + - [x] Match Result Wrapper diff --git a/example/models.py b/example/models.py index 3f09580..66e8487 100644 --- a/example/models.py +++ b/example/models.py @@ -1,5 +1,4 @@ from datetime import datetime -from typing import Optional from nebula_carina.models import models from nebula_carina.models.fields import create_nebula_field as _ diff --git a/main.py b/main.py index c9276a1..11eeafa 100644 --- a/main.py +++ b/main.py @@ -16,13 +16,6 @@ @app.get("/") async def root(): - return ModelBuilder.match( - '(v)-[e:love]->(v2)-[e2:love]->(v3)', { - 'v': VirtualCharacter, 'e': EdgeModel, 'v2': VirtualCharacter, - 'e2': EdgeModel, 'v3': VirtualCharacter - }, - condition=Q(v__id="char_test1"), - ) # run_ngql('SHOW SPACES;') # print(show_spaces()) # print(use_space('main')) @@ -112,20 +105,27 @@ async def root(): # ).save() # EdgeModel(src_vid='char_test1', dst_vid='char_test2', ranking=0, edge_type=Love(way='gun', times=40)).save() # return EdgeModel.objects.find_between('char_test1', 'char_test2') - # character1 = VirtualCharacter.objects.get('char_test1') + character1 = VirtualCharacter.objects.get('char_test1') # LocalSession().session.release() - # character2 = VirtualCharacter.objects.get('char_test2') - # character1.get_out_edges(Love) - # character2.get_reverse_edges(Love) - # character1.get_out_edge_and_destinations(Love, VirtualCharacter) - # character2.get_reverse_edge_and_sources(Love, VirtualCharacter) - # return VirtualCharacter.objects.find_destinations('char_test1', Love) - # return VirtualCharacter.objects.find_sources('char_test2', Love, distinct=False, limit=Limit(1)) - # return character2.get_sources(Love, VirtualCharacter) - # return character1.get_destinations(Love, VirtualCharacter) + character2 = VirtualCharacter.objects.get('char_test2') + character1.get_out_edges(Love) + character2.get_reverse_edges(Love) + character1.get_out_edge_and_destinations(Love, VirtualCharacter) + character2.get_reverse_edge_and_sources(Love, VirtualCharacter) + VirtualCharacter.objects.find_destinations('char_test1', Love) + VirtualCharacter.objects.find_sources('char_test2', Love, distinct=False, limit=Limit(1)) + character2.get_sources(Love, VirtualCharacter) + character1.get_destinations(Love, VirtualCharacter) # return character1 # return rst # return ModelBuilder.match( # '(v)-[e:love]->(v2)', {'v': VirtualCharacter, 'e': EdgeModel, 'v2': VirtualCharacter}, # condition=Q(v__id__in=[112, 113]), # ) + # EdgeModel(src_vid='char_test1', dst_vid='char_test2', ranking=0, edge_type=Love(way='gun', times=40)).save() + return ModelBuilder.serialized_match( + '(v)-[e:love]->(v2)', { + 'v': VirtualCharacter, 'e': EdgeModel, 'v2': VirtualCharacter, + }, + condition=Q(v__id="char_test1"), + ) diff --git a/nebula_carina/management/__init__.py b/nebula_carina/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/nebula_carina/management/commands/__init__.py b/nebula_carina/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/nebula_carina/management/commands/nebulamigrate.py b/nebula_carina/management/commands/nebulamigrate.py new file mode 100644 index 0000000..f5011e0 --- /dev/null +++ b/nebula_carina/management/commands/nebulamigrate.py @@ -0,0 +1,22 @@ +try: + from django.core.management.base import BaseCommand, CommandError + from nebula_carina.models.migrations import make_migrations, migrate + + + class Command(BaseCommand): + help = 'Run Nebula Graph migrations' + + def handle(self, *args, **options): + migrations = make_migrations() + if not migrations: + self.stdout.write('No Nebula Graph schema change found') + return + self.stdout.write('The following migration NGQLs will be executed: \n\n%s' % ('\n'.join(migrations))) + if input("Type 'yes' to continue, or 'no' to cancel\n") == 'yes': + migrate(make_migrations()) + self.stdout.write(self.style.SUCCESS('Nebula migration succeeded.')) + else: + self.stdout.write(self.style.NOTICE('Nebula migration cancelled.')) + +except ModuleNotFoundError: + pass diff --git a/nebula_carina/models/abstract.py b/nebula_carina/models/abstract.py index 0cf0142..e947771 100644 --- a/nebula_carina/models/abstract.py +++ b/nebula_carina/models/abstract.py @@ -3,7 +3,11 @@ from nebula3.common.ttypes import Vertex, Edge -class NebulaAdaptor(ABC): +class NebulaConvertableProtocol(ABC): @classmethod def from_nebula_db_cls(cls, raw_db_item: Vertex | Edge): pass + + def dict(self, *args, **kwargs): + # this method will be overridden by pydantic + raise NotImplementedError diff --git a/nebula_carina/models/managers.py b/nebula_carina/models/managers.py index 9e6f156..19b3c6e 100644 --- a/nebula_carina/models/managers.py +++ b/nebula_carina/models/managers.py @@ -123,7 +123,7 @@ def get(self, src_vid: str | int, dst_vid: str | int, edge_type): try: return self.find_between(src_vid, dst_vid, edge_type)[0] except IndexError: - raise EdgeDoesNotExistError + raise EdgeDoesNotExistError(src_vid, dst_vid) def delete(self, edge_definitions: list[EdgeDefinition]): return run_ngql(delete_edge_ngql(self.model.get_edge_type_and_model()[1].db_name(), edge_definitions)) diff --git a/nebula_carina/models/model_builder.py b/nebula_carina/models/model_builder.py index fa62499..0269c3a 100644 --- a/nebula_carina/models/model_builder.py +++ b/nebula_carina/models/model_builder.py @@ -1,24 +1,44 @@ -from typing import Iterable, Type -from nebula_carina.models.abstract import NebulaAdaptor +from typing import Type, Iterable +from nebula_carina.models.abstract import NebulaConvertableProtocol from nebula_carina.ngql.query.conditions import Condition from nebula_carina.ngql.query.match import match, OrderBy, Limit +class SingleMatchResult(object): + + def __init__(self, result: dict[str, NebulaConvertableProtocol]): + self.__data = result + + def dict(self): + return {k: v.dict() for k, v in self.__data.items()} + + def __getitem__(self, item): + return self.__data[item] + + def __iter__(self): + for key, value in self.__data.items(): + yield key, value + + class ModelBuilder(object): @staticmethod def match( - pattern: str, to_model_dict: dict[str, Type[NebulaAdaptor]], # should be model + pattern: str, to_model_dict: dict[str, Type[NebulaConvertableProtocol]], *, distinct_field: str = None, condition: Condition = None, order_by: OrderBy = None, limit: Limit = None - ) -> Iterable[dict[str, NebulaAdaptor]]: # should be model + ) -> Iterable[SingleMatchResult]: # should be model output = ', '.join( ("DISTINCT " if key == distinct_field else "") + key for key in to_model_dict.keys() ) results = match(pattern, output, condition, order_by, limit) return ( - { + SingleMatchResult({ key: to_model_dict[key].from_nebula_db_cls(value.value) for key, value in zip(results.keys(), row.values) if key in to_model_dict - } for row in results.rows() + }) for row in results.rows() ) + + @staticmethod + def serialized_match(*args, **kwargs): + return [res.dict() for res in ModelBuilder.match(*args, **kwargs)] diff --git a/nebula_carina/models/models.py b/nebula_carina/models/models.py index a12cdcd..cbb647d 100644 --- a/nebula_carina/models/models.py +++ b/nebula_carina/models/models.py @@ -9,7 +9,7 @@ from pydantic.fields import ModelField from pydantic.main import ModelMetaclass -from nebula_carina.models.abstract import NebulaAdaptor +from nebula_carina.models.abstract import NebulaConvertableProtocol from nebula_carina.models.errors import VertexDoesNotExistError, EdgeDoesNotExistError, DuplicateEdgeTypeNameError from nebula_carina.models.fields import NebulaFieldInfo from nebula_carina.models.managers import Manager, BaseVertexManager, BaseEdgeManager @@ -176,7 +176,7 @@ def __init__(cls, *args, **kwargs): val.register(cls) -class NebulaRecordModel(BaseModel, NebulaAdaptor, metaclass=NebulaRecordModelMetaClass): +class NebulaRecordModel(BaseModel, NebulaConvertableProtocol, metaclass=NebulaRecordModelMetaClass): objects = BaseVertexManager() @@ -272,7 +272,7 @@ def get_out_edges(self, edge_type: EdgeTypeModel = None, *, limit: Limit = None) return EdgeModel.objects.find_by_source(self.vid, edge_type, limit=limit) def get_out_edge_and_destinations(self, edge_type, dst_vertex_model, *, limit: Limit = None) \ - -> Iterable[dict[str, NebulaAdaptor]]: + -> Iterable[dict[str, NebulaConvertableProtocol]]: if edge_type is None: edge_type = EdgeTypeModel return ( @@ -290,7 +290,7 @@ def get_reverse_edges(self, edge_type: EdgeTypeModel = None, *, limit: Limit = N return EdgeModel.objects.find_by_destination(self.vid, edge_type, limit=limit) def get_reverse_edge_and_sources(self, edge_type, src_vertex_model, *, limit: Limit = None) \ - -> Iterable[dict[str, NebulaAdaptor]]: + -> Iterable[dict[str, NebulaConvertableProtocol]]: if edge_type is None: edge_type = EdgeTypeModel return ( diff --git a/setup.py b/setup.py index 933597b..dc3832e 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setup( name='nebula-carina', - version='0.2.1', + version='0.3.0', author='Sword Elucidator', author_email='nagisa940216@gmail.com', url='https://github.com/SwordElucidator/nebula-carina',