├── .gitignore ├── LICENSE ├── README.md ├── gizmo ├── __init__.py ├── connection.py ├── entity.py ├── event.py ├── exception.py ├── field.py ├── mapper.py ├── statement.py ├── test │ ├── __init__.py │ ├── entity.py │ ├── field.py │ ├── integration │ │ ├── __init__.py │ │ ├── tinkerpop.py │ │ └── titan.py │ └── mapper.py ├── traversal.py ├── util.py └── version.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | 43 | # Translations 44 | *.mo 45 | *.pot 46 | 47 | # Django stuff: 48 | *.log 49 | 50 | # Sphinx documentation 51 | docs/_build/ 52 | 53 | # PyBuilder 54 | target/ 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Mark Henderson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Gizmo 2 | ===== 3 | 4 | > Alpha build of this library. The API may change while working toward a stable release 5 | 6 | Gizmo is a lightweight asynchronous Python 3.5+ Object Graph Mapper (O.G.M.) for the [Tinkerpop Rexster](http://tinkerpop.apache.org) graphs. 7 | 8 | 9 | ## About 10 | 11 | Gizmo starts and ends with Gremlin/Groovy. It is made up of entity, mapper, query, request, response, and other objects whose job is to convert pure Python to a Gremlin/Groovy string to be executed on a server. 12 | 13 | 14 | ### QuickStart Example 15 | 16 | ```python 17 | import asyncio 18 | 19 | from gizmo import Request, Vertex, Edge, Mapper, String 20 | 21 | from gremlinpy import Gremlin 22 | 23 | 24 | # setup the connection 25 | req = Request('localhost', 8182) 26 | gremlin = Gremlin('g') # this should be whatever your graph name is 27 | mapper = Mapper(request=req, gremlin=gremlin) 28 | 29 | 30 | # define a few entity classes 31 | class User(Vertex): 32 | name = String() 33 | 34 | 35 | class Knows(Edge): 36 | pass 37 | 38 | 39 | # create a few entities 40 | mark = User({'name': 'mark'}) 41 | steve = User({'name': 'steve'}) 42 | knows = mapper.connect(mark, steve, Knows) 43 | 44 | 45 | # Save your entities 46 | async def example(): 47 | mapper.save(knows) # saving the edge will save users if they needed 48 | 49 | result = await mapper.send() 50 | 51 | # entities have been updated with response data from the graph 52 | print('mark: {}'.format(mark['id'])) 53 | print('steve: {}'.format(steve['id'])) 54 | print('knows: {}'.format(knows['id'])) 55 | 56 | 57 | asyncio.get_event_loop().run_until_complete(example()) 58 | 59 | 60 | # or write a custom query 61 | 62 | async def custom(): 63 | g = Gremlin() 64 | g.V() 65 | 66 | result = await mapper.query(gremlin=g) 67 | 68 | print(result) # gizmo.mapper.Collection object 69 | print(result[0]) # user object (because of the queries in prev example) 70 | 71 | asyncio.get_event_loop().run_until_complete(custom()) 72 | 73 | 74 | # do whatever Gremlin/Groovy allows you to do 75 | 76 | async def random_java(): 77 | script = 'def x = new Date(); x' 78 | 79 | r = await mapper.query(script=script) 80 | 81 | print(r[0]['response'].value) # whatever x evaluates to 82 | 83 | asyncio.get_event_loop().run_until_complete(random_java()) 84 | ``` 85 | 86 | ## Running tests 87 | 88 | Test can be run via the setup file or directly with `python -m`. 89 | 90 | ``` 91 | python setup.py test 92 | ``` 93 | 94 | or 95 | 96 | ``` 97 | python -m unittest gizmo.test.entity 98 | python -m unittest gizmo.test.mapper 99 | python -m unittest gizmo.test.integration.tinkerpop 100 | ... 101 | ``` 102 | 103 | If you're going to run the integration tests, right now, both the name of the graph and the port are hard-coded into the suite. Make sure your Gremlin and Titan server use these settings: 104 | 105 | __tinkerpop__ 106 | * graph: gizmo_testing 107 | * port: 8182 108 | 109 | __titan__ 110 | * graph: gizmo_testing 111 | * port: 9192 112 | 113 | -------------------------------------------------------------------------------- /gizmo/__init__.py: -------------------------------------------------------------------------------- 1 | from .version import __version__ 2 | from .connection import Request, Response 3 | from .entity import Vertex, GenericVertex, Edge, GenericEdge 4 | from .exception import (AstronomerFieldException, AstronomerEntityException, 5 | AstronomerMapperException, AstronomerQueryException) 6 | from .field import String, Integer, Float, Map, List, Increment, Boolean 7 | from .mapper import Collection, Query, Mapper 8 | -------------------------------------------------------------------------------- /gizmo/connection.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import copy 3 | import json 4 | import logging 5 | import uuid 6 | 7 | import websockets 8 | 9 | from .exception import AstronomerConnectionException 10 | from .util import _query_debug, Timer 11 | 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | 16 | class RequestQueryLogger: 17 | 18 | def __init__(self): 19 | self.queries = [] 20 | 21 | def add(self, script, params, query, execution_time): 22 | self.queries.append({ 23 | 'query': query, 24 | 'script': script, 25 | 'params': params, 26 | 'query': query, 27 | 'execution_time': execution_time, 28 | }) 29 | 30 | def reset(self): 31 | self.queries = [] 32 | 33 | @property 34 | def total_time(self): 35 | return sum(q['execution_time'] for q in self.queries) 36 | 37 | def __len__(self): 38 | return len(self.queries) 39 | 40 | def __add__(self, other): 41 | if isinstance(other, RequestQueryLogger): 42 | self.queries += copy.deepcopy(other.queries) 43 | 44 | 45 | class Request: 46 | 47 | def __init__(self, uri, port=8182, three_two=True, username=None, 48 | password=None, log_requests=None): 49 | gremlin = '/gremlin' if three_two else '' 50 | self.uri = uri 51 | self.port = port 52 | self.three_two = three_two 53 | self._ws_uri = 'ws://{}:{}{}'.format(uri, port, gremlin) 54 | self.username = username 55 | self.password = password 56 | self.connection = None 57 | 58 | if log_requests: 59 | log_requests = RequestQueryLogger() 60 | 61 | self.request_logger = log_requests 62 | 63 | def connect(self): 64 | self.connection = websockets.connect(self._ws_uri) 65 | 66 | def message(self, script, params=None, rebindings=None, op='eval', 67 | processor=None, language='gremlin-groovy', session=None): 68 | message = { 69 | 'requestId': str(uuid.uuid4()), 70 | 'op': op, 71 | 'processor': processor or '', 72 | 'args': { 73 | 'gremlin': script, 74 | 'bindings': params, 75 | 'language': language, 76 | 'rebindings': rebindings or {}, 77 | } 78 | } 79 | 80 | # TODO: add session 81 | 82 | return json.dumps(message) 83 | 84 | async def send(self, script=None, params=None, update_entities=None, 85 | rebindings=None, op='eval', processor=None, 86 | language='gremlin-groovy', session=None): 87 | data = [] 88 | status = ResponseStatus(500, '') 89 | request_id = None 90 | result = None 91 | params = params or {} 92 | update_entities = update_entities or {} 93 | query = _query_debug(script, params) 94 | request_logger = self.request_logger 95 | 96 | logger.debug('RUNNING QUERY WITH PARAMS') 97 | logger.debug(script) 98 | logger.debug(params) 99 | logger.debug(query) 100 | 101 | try: 102 | with Timer() as timer: 103 | self.connect() 104 | 105 | async with self.connection as ws: 106 | message = self.message(script=script, params=params, 107 | rebindings=rebindings, op=op, processor=processor, 108 | language=language, session=session) 109 | 110 | await ws.send(message) 111 | 112 | response = await ws.recv() 113 | data = json.loads(response) 114 | 115 | if data.get('request_id'): 116 | request_id = data['request_id'] 117 | 118 | if data.get('result'): 119 | result = data['result'] 120 | 121 | if data.get('status'): 122 | status = ResponseStatus(**data['status']) 123 | 124 | logger.debug('runtime: {} miliseconds\n'.format(timer.elapsed)) 125 | 126 | if request_logger is not None: 127 | request_logger.add(script, params, query, timer.elapsed) 128 | 129 | return Response(request_id=request_id, result=result, 130 | update_entities=update_entities, script=script, 131 | params=params) 132 | except Exception as e: 133 | raise AstronomerConnectionException(e) 134 | 135 | 136 | class ResponseStatus: 137 | 138 | def __init__(self, code, message, attributes=None): 139 | self.code = code 140 | self.message = message 141 | self.attributes = attributes 142 | 143 | 144 | class Response: 145 | 146 | def __init__(self, request_id=None, result=None, update_entities=None, 147 | script=None, params=None, status=None): 148 | self.request_id = request_id 149 | self.result = result or {} 150 | self.update_entities = update_entities or {} 151 | self.script = script 152 | self.params = params 153 | self.status = status 154 | 155 | self.translate() 156 | 157 | def _fix_titan_data(self, data): 158 | """temp method to address a titan bug where it returns maps in a 159 | different manner than other tinkerpop instances. This will be fixed 160 | in a later version of titan""" 161 | if isinstance(data, (list, tuple,)): 162 | fixed = [] 163 | 164 | for ret in data: 165 | if isinstance(ret, dict): 166 | if 'key' in ret and 'value' in ret: 167 | fixed.append({ret['key']: ret['value']}) 168 | 169 | if len(data) and not len(fixed): 170 | return data 171 | else: 172 | return fixed 173 | else: 174 | return data 175 | 176 | def translate(self): 177 | response = [] 178 | data = self._fix_titan_data(self.result.get('data') or []) 179 | update_keys = list(self.update_entities.keys()) 180 | 181 | def has_update(keys): 182 | for k in keys: 183 | if k in update_keys: 184 | return True 185 | 186 | return False 187 | 188 | def fix_properties(arg): 189 | props = copy.deepcopy(arg) 190 | 191 | if 'properties' in arg: 192 | props.update(arg['properties']) 193 | del(props['properties']) 194 | 195 | props.update({ 196 | 'id': arg.get('id'), 197 | 'type': arg.get('type'), 198 | 'label': arg.get('label'), 199 | }) 200 | 201 | return props 202 | 203 | for arg in data: 204 | if not hasattr(arg, '__iter__'): 205 | response = [{'response': arg}] 206 | elif isinstance(arg, dict): 207 | if has_update(arg.keys()): 208 | for k, v in arg.items(): 209 | if k in self.update_entities: 210 | entity = self.update_entities[k] 211 | props = fix_properties(v) 212 | 213 | response.append(props) 214 | entity.empty().hydrate(props, reset_initial=True) 215 | else: 216 | response.append(fix_properties(arg)) 217 | else: 218 | response.append(arg) 219 | 220 | return response 221 | 222 | @property 223 | def data(self): 224 | return self.translate() 225 | 226 | def __getitem__(self, key): 227 | val = None 228 | 229 | try: 230 | data = self.data[key] 231 | val = copy.deepcopy(data) 232 | # 233 | # if '_properties' in data: 234 | # del val['_properties'] 235 | # val.update(data['_properties']) 236 | except: 237 | pass 238 | 239 | return val 240 | 241 | def update_entities(self, mappings): 242 | fixed = copy.deepcopy(self.data) 243 | 244 | for var, entity in mappings.items(): 245 | if var in self.data: 246 | entity.hydrate(self.data[var]) 247 | 248 | try: 249 | del fixed[var] 250 | except: 251 | pass 252 | 253 | fixed.update(self.data[var]) 254 | 255 | self.data = fixed 256 | 257 | return self 258 | 259 | def __setitem__(self, key, val): 260 | self.data[key] = val 261 | 262 | return self 263 | -------------------------------------------------------------------------------- /gizmo/entity.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import uuid 3 | 4 | from .field import (Field, FieldManager, GremlinID, Integer, String, 5 | GremlinLabel, GremlinType, GIZMOEntity, _ImmutableField, Relationship) 6 | from .util import (camel_to_underscore, entity_name, GIZMO_ID, GIZMO_LABEL, 7 | GIZMO_TYPE, GIZMO_ENTITY) 8 | 9 | 10 | ENTITY_MAP = {} 11 | 12 | 13 | class _EntityType(type): 14 | 15 | def __new__(cls, name, bases, attrs): 16 | 17 | def __init__(self, data=None, data_type='python'): 18 | 19 | if isinstance(self, Edge): 20 | if data and 'outV' in data: 21 | self.outV = data['outV'] 22 | 23 | del data['outV'] 24 | else: 25 | self.outV = None 26 | 27 | if data and 'inV' in data: 28 | self.inV = data['inV'] 29 | 30 | del data['inV'] 31 | else: 32 | self.inV = None 33 | 34 | data = copy.deepcopy(data or {}) 35 | fields = {} 36 | _all_attrs = {} 37 | self._data_type = data_type 38 | self._relationships = {} 39 | 40 | def def_fields(obj_attrs): 41 | keys = obj_attrs.keys() 42 | 43 | for key, val in obj_attrs.items(): 44 | 45 | if isinstance(val, Relationship): 46 | self._relationships[key] = val 47 | 48 | if isinstance(val, Field): 49 | fields[key] = copy.deepcopy(val) 50 | 51 | if not fields[key].name: 52 | fields[key].name = key 53 | 54 | def walk(bases): 55 | for base in bases: 56 | _all_attrs.update(base.__dict__) 57 | walk(base.__bases__) 58 | 59 | walk(bases) 60 | def_fields(_all_attrs) 61 | def_fields(attrs) 62 | _all_attrs.update(attrs) 63 | 64 | ast_entity = entity_name(self) 65 | allow_undefined = _all_attrs.get('allow_undefined', False) 66 | label = attrs.get('label', str(self)) 67 | _id = attrs.get('id', attrs.get('_id', None)) 68 | _type = 'vertex' if isinstance(self, Vertex) else 'edge' 69 | fields[GIZMO_LABEL[0]] = GremlinLabel(GIZMO_LABEL[1], values=label) 70 | fields[GIZMO_ID] = GremlinID(GIZMO_ID, values=_id) 71 | fields[GIZMO_TYPE] = GremlinType(GIZMO_TYPE, values=_type) 72 | fields[GIZMO_ENTITY] = GIZMOEntity(GIZMO_ENTITY, values=ast_entity) 73 | self.fields = FieldManager(fields=fields, 74 | allow_undefined=allow_undefined, data_type=data_type) 75 | 76 | if GIZMO_LABEL[0] in data: 77 | del data[GIZMO_LABEL[0]] 78 | 79 | self.hydrate(data, True) 80 | 81 | attrs['__init__'] = __init__ 82 | cls = type.__new__(cls, name, bases, attrs) 83 | map_name = '{}.{}'.format(cls.__module__, cls.__name__) 84 | ENTITY_MAP[map_name] = cls 85 | 86 | return cls 87 | 88 | def __call__(cls, *args, **kwargs): 89 | entity = super(_EntityType, cls).__call__(*args, **kwargs) 90 | 91 | for k, v in entity.fields.fields.items(): 92 | if not isinstance(v, _ImmutableField): 93 | setattr(entity, k, v) 94 | 95 | return entity 96 | 97 | def __str__(cls): 98 | return camel_to_underscore(cls.__name__) 99 | 100 | 101 | class _Entity(metaclass=_EntityType): 102 | 103 | def hydrate(self, data=None, reset_initial=False): 104 | from pprint import pprint 105 | self.fields.hydrate(data, reset_initial) 106 | 107 | return self 108 | 109 | def __str__(self): 110 | return self.__unicode__() 111 | 112 | def __unicode__(self): 113 | return camel_to_underscore(self.__class__.__name__) 114 | 115 | def __getitem__(self, field): 116 | return self.fields[field] 117 | 118 | def __setitem__(self, field, value): 119 | self.fields[field] = value 120 | 121 | return self.fields[field] 122 | 123 | def __delitem__(self, field): 124 | del self.fields[field] 125 | 126 | def _get_data_type(self): 127 | return self._data_type 128 | 129 | def _set_data_type(self, data_type): 130 | self._data_type = data_type 131 | self.fields.data_type = data_type 132 | 133 | data_type = property(_get_data_type, _set_data_type) 134 | 135 | def __getattr__(self, attr): 136 | return self.__getitem__(attr) 137 | 138 | @property 139 | def data(self): 140 | return self.fields.data 141 | 142 | @property 143 | def changes(self): 144 | return self.fields.changes 145 | 146 | @property 147 | def changed(self): 148 | return self.fields.changed 149 | 150 | @property 151 | def values(self): 152 | return self.fields.values 153 | 154 | @property 155 | def deleted(self): 156 | return self.fields.deleted 157 | 158 | def get_rep(self): 159 | entity = 'E' if self[GIZMO_TYPE] == 'edge' else 'V' 160 | 161 | return entity, self['id'] 162 | 163 | def empty(self): 164 | self.fields.empty() 165 | 166 | return self 167 | 168 | 169 | class Vertex(_Entity): 170 | pass 171 | 172 | 173 | class GenericVertex(Vertex): 174 | allow_undefined = True 175 | 176 | 177 | class Edge(_Entity): 178 | 179 | @property 180 | def out_v(self): 181 | return self.outV 182 | 183 | @property 184 | def in_v(self): 185 | return self.inV 186 | 187 | 188 | class GenericEdge(Edge): 189 | allow_undefined = True 190 | -------------------------------------------------------------------------------- /gizmo/event.py: -------------------------------------------------------------------------------- 1 | from gizmo.entity import Vertex, Edge 2 | from gizmo.mapper import EntityMapper 3 | 4 | 5 | class SourcedEvent(Vertex): 6 | _allowed_undefined = True 7 | 8 | 9 | class SourcedEventMapper(EntityMapper): 10 | entity = SourcedEvent 11 | 12 | 13 | class TriggedSourceEvent(Edge): 14 | pass 15 | 16 | 17 | class SourceEventEntry(Edge): 18 | pass 19 | 20 | 21 | class EventSourceException(Exception): 22 | pass 23 | 24 | 25 | class EventSourceMixin(object): 26 | """ 27 | this class is used to add event sourcing functionality 28 | (http://martinfowler.com/eaaDev/EventSourcing.html) 29 | to Gizmo entities. Simply define a custom mapper and subclass 30 | this class to get the added functionality. 31 | 32 | The source must be defined before saving the entity. 33 | 34 | When using this mixin there are a few things that need to be 35 | considered: 36 | """ 37 | 38 | def save(self, entity, bind_return=True, callback=None, source=None, 39 | *args, **kwargs): 40 | """ 41 | Method used to save the original entity and to add the 42 | source -> event and 43 | entity -> event 44 | relationships 45 | """ 46 | super(EventSourceMixin, self).save(entity=entity, 47 | bind_return=bind_return, 48 | callback=callback, *args, **kwargs) 49 | self.mapper.enqueue_mapper(self) 50 | 51 | if source is not None: 52 | fields_changed = len(entity.changed) > 0 53 | fields_removed = len(entity.deleted) > 0 54 | 55 | # only create the source event if there were actual changes 56 | if fields_changed or fields_removed: 57 | deleted = [] 58 | changed = {} 59 | hydrate = {} 60 | self.event = event = \ 61 | self.mapper.create(entity=SourcedEvent, 62 | data_type=entity.data_type) 63 | 64 | for field, changes in entity.changes.items(): 65 | if not changes['immutable']: 66 | if changes['deleted']: 67 | deleted.append(field) 68 | else: 69 | changed[field] = changes 70 | 71 | if deleted: 72 | hydrate['deleted'] = deleted 73 | 74 | if changes: 75 | hydrate['changes'] = changes 76 | 77 | self.event.hydrate(hydrate) 78 | 79 | source_params = { 80 | 'out_v': source, 81 | 'in_v': event, 82 | 'edge_entity': TriggedSourceEvent, 83 | } 84 | source_edge = self.mapper.connect(**source_params) 85 | event_edge = self.mapper.connect(out_v=entity, in_v=event, 86 | edge_entity=SourceEventEntry) 87 | 88 | self.mapper.save(source_edge, bind_return=True) 89 | self.mapper.save(event_edge, bind_return=True) 90 | 91 | return self 92 | 93 | def get_event_history(self, entity, range_start=None, range_end=None): 94 | # TODO: fill this query out 95 | pass 96 | -------------------------------------------------------------------------------- /gizmo/exception.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class AstronomerException(Exception): 4 | pass 5 | 6 | 7 | class AstronomerConnectionException(AstronomerException): 8 | pass 9 | 10 | 11 | class AstronomerFieldException(AstronomerException): 12 | pass 13 | 14 | 15 | class AstronomerEntityException(AstronomerException): 16 | pass 17 | 18 | 19 | class AstronomerMapperException(AstronomerException): 20 | pass 21 | 22 | 23 | class AstronomerQueryException(AstronomerException): 24 | pass 25 | -------------------------------------------------------------------------------- /gizmo/field.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import json 3 | 4 | from collections import OrderedDict 5 | from datetime import datetime 6 | 7 | from gremlinpy import Param 8 | 9 | from .traversal import Traversal 10 | from .util import is_gremlin_entity 11 | 12 | 13 | class FieldManager: 14 | 15 | def __init__(self, fields=None, data_type='python', allow_undefined=False): 16 | self.fields = fields or {} 17 | self._data_type = data_type 18 | self.allow_undefined = allow_undefined 19 | 20 | for name, field in self.fields.items(): 21 | if not field.name: 22 | field.name = name 23 | 24 | def _set_data_type(self, data_type): 25 | for name, field in self.fields.items(): 26 | field.data_type = data_type 27 | 28 | def _get_data_type(self): 29 | return self._data_type 30 | 31 | data_type = property(_get_data_type, _set_data_type) 32 | 33 | def __getitem__(self, name): 34 | value = None 35 | 36 | if name in self.fields: 37 | value = self.fields[name] 38 | 39 | if not value.name: 40 | value.name = name 41 | 42 | if isinstance(value, _ImmutableField): 43 | return value.data 44 | elif self.allow_undefined: 45 | value = self._add_undefined_field(name, None) 46 | 47 | return value 48 | 49 | def __setitem__(self, name, value): 50 | if name in self.fields: 51 | if isinstance(value, Field): 52 | self.fields[name] = value 53 | else: 54 | field = self.fields[name].empty() 55 | field + value 56 | elif self.allow_undefined: 57 | field = self._add_undefined_field(name, value) 58 | 59 | if not self.fields[name].name: 60 | self.fields[name].name = name 61 | 62 | return self.fields[name] 63 | 64 | def __delitem__(self, name): 65 | self.fields[name].deleted = True 66 | 67 | def _add_undefined_field(self, name, value): 68 | max_values = None 69 | overwrite_last_value = False 70 | gremlin_entity = is_gremlin_entity(value) 71 | original_value = copy.deepcopy(value) 72 | 73 | if gremlin_entity: 74 | value = value[0]['value'] 75 | 76 | if isinstance(value, dict): 77 | field = Map 78 | max_values = 1 79 | overwrite_last_value = True 80 | elif isinstance(value, (list, tuple,)): 81 | field = List 82 | max_values = 1 83 | overwrite_last_value = True 84 | elif isinstance(value, bool): 85 | field = Boolean 86 | max_values = 1 87 | overwrite_last_value = True 88 | elif isinstance(value, int): 89 | field = Integer 90 | elif isinstance(value, float): 91 | field = Float 92 | else: 93 | field = String 94 | 95 | """the field will not be initialized with a value so that it 96 | will register as 'added'. We also need to determine if the value is 97 | a response from the Gremlin server or if it is a plain value 98 | """ 99 | f = field(name=name, data_type=self.data_type, max_values=max_values, 100 | overwrite_last_value=overwrite_last_value) 101 | 102 | # if isinstance(value, (list, tuple)) and len(value) and \ 103 | # isinstance(value[0], dict) and 'value' in value[0]: 104 | if gremlin_entity: 105 | for val in original_value: 106 | val_obj = Value(value=val.get('value', None), 107 | properties=val.get('properties', None), id=val.get('id', 108 | None)) 109 | f + val_obj 110 | else: 111 | f + value 112 | 113 | self.fields[name] = f 114 | 115 | return self.fields[name] 116 | 117 | def hydrate(self, data, reset_initial=False): 118 | for key, val in data.items(): 119 | if key in self.fields: 120 | if is_gremlin_entity(val): 121 | for v in val: 122 | self.fields[key] + Value(value=v.get('value', None), 123 | properties=v.get('properties', None), 124 | id=v.get('id', None)) 125 | else: 126 | self.fields[key] + val 127 | elif self.allow_undefined: 128 | self._add_undefined_field(key, val) 129 | 130 | return self 131 | 132 | def empty(self): 133 | for name, field in self.fields.items(): 134 | field.empty() 135 | 136 | return self 137 | 138 | @property 139 | def data(self): 140 | data = {field.name: field.data for name, field in self.fields.items() 141 | if field.data} 142 | 143 | return OrderedDict(sorted(data.items())) 144 | 145 | @property 146 | def values(self): 147 | values = {field.name: field.values for name, field in 148 | self.fields.items() if field.values} 149 | 150 | return OrderedDict(sorted(values.items())) 151 | 152 | @property 153 | def changes(self): 154 | changes = {field.name: field.changes for name, field in 155 | self.fields.items() if field.changes} 156 | 157 | return OrderedDict(sorted(changes.items())) 158 | 159 | @property 160 | def changed(self): 161 | return [name for name, changes in self.changes.items() if 162 | changes['deleted'] or changes['values']] 163 | 164 | @property 165 | def deleted(self): 166 | return [name for name, changes in self.changes.items() if 167 | changes['deleted']] 168 | 169 | 170 | class Field: 171 | 172 | def __init__(self, name=None, values=None, data_type='python', 173 | max_values=None, overwrite_last_value=False, 174 | default=None): 175 | self.name = name 176 | self.immutable = isinstance(self, _ImmutableField) 177 | self.default = default 178 | self._values = ValueManager(values=values, data_type=data_type, 179 | to_python=self.to_python, 180 | to_graph=self.to_graph, 181 | default_value=self.default_value, 182 | max_values=max_values, 183 | overwrite_last_value=overwrite_last_value, 184 | can_set=self.can_set, 185 | default=self.default) 186 | self._data_type = data_type 187 | self.deleted = False 188 | 189 | def __repr__(self): 190 | return '%s(%r)'.format(self.__class__, self.__dict__) 191 | 192 | def __getitem__(self, value): 193 | return self._values[value] 194 | 195 | def __setitem__(self, key, value): 196 | val = self[key] 197 | val[key] = value 198 | 199 | def __delitem__(self, value): 200 | if self.can_set(value): 201 | del self._values[value] 202 | 203 | def __add__(self, value): 204 | if self.can_set(value): 205 | return self._values + value 206 | 207 | def _set_data_type(self, data_type): 208 | self._values.data_type = data_type 209 | 210 | def _get_data_type(self): 211 | return self._data_type 212 | 213 | data_type = property(_get_data_type, _set_data_type) 214 | 215 | @property 216 | def values(self): 217 | return self._values.values 218 | 219 | @property 220 | def value(self): 221 | values = self.values 222 | 223 | return values[-1] if values else None 224 | 225 | @property 226 | def data(self): 227 | return self._values.data 228 | 229 | @property 230 | def properties(self): 231 | return self._values.properties 232 | 233 | @property 234 | def changes(self): 235 | return { 236 | 'values': self._values.changes, 237 | 'deleted': self.deleted, 238 | 'immutable': self.immutable, 239 | } 240 | 241 | @property 242 | def default_value(self): 243 | return None 244 | 245 | def empty(self): 246 | self._values.empty() 247 | 248 | return self 249 | 250 | def to_python(self, value): 251 | return value._value 252 | 253 | def to_graph(self, value): 254 | return self.to_python(value) 255 | 256 | def can_set(self, value): 257 | return True 258 | 259 | 260 | class ValueManager: 261 | 262 | def __init__(self, values=None, data_type='python', reset_initial=True, 263 | to_python=None, to_graph=None, filter_field=None, 264 | default_value=None, max_values=None, 265 | overwrite_last_value=False, can_set=None, default=None): 266 | self._values = [] 267 | self._deleted = [] 268 | self._data_type = data_type 269 | self.default = None 270 | 271 | if default: 272 | self.default = Value(value=default, properties=None, id=None) 273 | 274 | self.max_values = max_values 275 | self.overwrite_last_value = overwrite_last_value 276 | 277 | if not values and default_value is not None: 278 | values = [default_value, ] 279 | 280 | if not to_python: 281 | to_python = lambda x: x 282 | 283 | if not to_graph: 284 | to_graph = lambda x: x 285 | 286 | if not can_set: 287 | can_set = lambda x: True 288 | 289 | self._to_python = to_python 290 | self._to_graph = to_graph 291 | self._can_set = can_set 292 | self.filter_field = filter_field 293 | 294 | self.hydrate(values=values, reset_initial=reset_initial) 295 | 296 | self.data_type = self._data_type 297 | 298 | def hydrate(self, values=None, reset_initial=False): 299 | if values: 300 | if not isinstance(values, (list, tuple,)): 301 | values = (values,) 302 | 303 | for value in values: 304 | if not isinstance(value, dict): 305 | value = {'value': value} 306 | 307 | properties = value.get('properties', None) 308 | value = value.get('value', None) 309 | 310 | self.add_value(value=value, properties=properties) 311 | 312 | if reset_initial: 313 | self._initial = self._values[:] 314 | 315 | return self 316 | 317 | def __add__(self, value): 318 | if isinstance(value, Value): 319 | if not self._can_set(value.value): 320 | return self 321 | 322 | self.add_value(value.value, value.properties, value.id) 323 | 324 | return self 325 | else: 326 | if not self._can_set(value): 327 | return self 328 | 329 | return self.add_value(value, properties=None) 330 | 331 | def __len__(self): 332 | return len(self.filtered_values) 333 | 334 | def __getitem__(self, value): 335 | if value not in self.values: 336 | self + value 337 | 338 | manager = ValueManager(data_type=self.data_type, 339 | to_python=self._to_python, 340 | to_graph=self._to_graph, 341 | filter_field=value, 342 | max_values=self.max_values, 343 | overwrite_last_value=self.overwrite_last_value) 344 | manager._values = self._values 345 | 346 | return manager 347 | 348 | def __setitem__(self, key, value): 349 | if not self._can_set(value): 350 | return self 351 | 352 | for val in self.filtered_values: 353 | if val.value == key: 354 | val.value = value 355 | 356 | def __delitem__(self, value): 357 | for i, val in enumerate(self._values): 358 | if val.value == value: 359 | deleted = self._values.pop(i) 360 | self._deleted.append(deleted) 361 | 362 | def _set_data_type(self, data_type): 363 | converter = self._to_python if data_type == 'python'\ 364 | else self._to_graph 365 | 366 | for value in self._values: 367 | value.converter = converter 368 | 369 | if self.default: 370 | self.default.converter = converter 371 | 372 | def _get_data_type(self): 373 | return self._data_type 374 | 375 | data_type = property(_get_data_type, _set_data_type) 376 | 377 | def empty(self): 378 | for value in self.values: 379 | del self[value] 380 | 381 | return self 382 | 383 | def add_value(self, value, properties=None, id=None): 384 | if not self._can_set(value): 385 | return self 386 | 387 | val = Value(value=value, properties=properties, id=id) 388 | x = self.max_values 389 | l = len(self._values) 390 | 391 | if self.max_values and len(self._values) >= self.max_values: 392 | if self.overwrite_last_value and\ 393 | len(self._values) == self.max_values: 394 | self._values[-1] = val 395 | elif len(self._values) < self.max_values: 396 | self._values.append(val) 397 | else: 398 | self._values.append(val) 399 | 400 | return self 401 | 402 | @property 403 | def properties(self): 404 | properties = [v.properties for v in self.filtered_values] 405 | 406 | return PropertyManager(properties=properties) 407 | 408 | @property 409 | def filtered_values(self): 410 | if self.filter_field: 411 | values = [v for v in self._values 412 | if self.filter_field == v.value] 413 | else: 414 | values = [v for v in self._values] 415 | 416 | if not len(values) and self.default: 417 | values = [self.default] 418 | 419 | return values 420 | 421 | @property 422 | def values(self): 423 | return [v.value for v in self.filtered_values] 424 | 425 | @property 426 | def data(self): 427 | return [v.data for v in self.filtered_values] 428 | 429 | @property 430 | def changes(self): 431 | changed = {'values': self.values} 432 | changes = [] 433 | added = [] 434 | 435 | for v in self.filtered_values: 436 | if v not in self._initial: 437 | added.append(v.data) 438 | 439 | if added: 440 | changed['added'] = added 441 | 442 | for v in self.filtered_values: 443 | if v not in added: 444 | v_changes = v.changes 445 | 446 | if v_changes: 447 | changes.append(v_changes) 448 | 449 | if changes: 450 | changed['changes'] = changes 451 | 452 | if self._deleted: 453 | changed['deleted'] = [v.data for v in self._deleted] 454 | 455 | return changed 456 | 457 | @property 458 | def default_value(self): 459 | return None 460 | 461 | 462 | class Value: 463 | 464 | def __init__(self, value, properties=None, id=None): 465 | self._value = value 466 | self._callable = callable(value) 467 | self._initial = copy.deepcopy(value) 468 | self.id = id 469 | self.properties = properties or {} 470 | self.converter = lambda value: value._value 471 | 472 | def __setitem__(self, key, value): 473 | self._properties[key] = value 474 | 475 | def __getitem__(self, key): 476 | return self._properties.get(key, None) 477 | 478 | def __getattribute__(self, attr): 479 | val = object.__getattribute__(self, attr) 480 | 481 | if attr == '_value' and self._callable: 482 | return val() 483 | else: 484 | return val 485 | 486 | def get_value(self): 487 | return self.converter(self) 488 | 489 | def set_value(self, value): 490 | self._callable = callable(value) 491 | self._value = value 492 | 493 | value = property(get_value, set_value) 494 | 495 | @property 496 | def data(self): 497 | return { 498 | 'value': self.converter(self), 499 | 'properties': self.properties, 500 | } 501 | 502 | @property 503 | def changes(self): 504 | changes = {} 505 | properties = self.properties 506 | 507 | if self.value != self._initial: 508 | changes['value'] = {'from': self._initial, 'to': self.value} 509 | 510 | if properties: 511 | if 'value' not in changes: 512 | changes['value'] = self._initial 513 | 514 | changes['properties'] = properties 515 | 516 | return changes 517 | 518 | 519 | class PropertyManager: 520 | 521 | def __init__(self, properties): 522 | self.properties = properties 523 | 524 | def __setitem__(self, key, val): 525 | for prop in self.properties: 526 | prop[key] = val 527 | 528 | return val 529 | 530 | def __getitem__(self, key): 531 | return [prop[key] for prop in self.properties if key in prop] 532 | 533 | def __delitem__(self, key): 534 | for prop in self.properties: 535 | if key in prop: 536 | del prop[key] 537 | 538 | @property 539 | def data(self): 540 | return [prop for prop in self.properties if prop] 541 | 542 | 543 | class Property: 544 | 545 | def __init__(self, properties=None): 546 | self._properties = properties or {} 547 | self._initial = copy.deepcopy(self._properties) 548 | 549 | def __getitem__(self, key): 550 | return self._properties[key] 551 | 552 | def __setitem__(self, key, value): 553 | self._properties[key] = value 554 | 555 | def __delitem__(self, key): 556 | del self._properties[key] 557 | 558 | def __contains__(self, key): 559 | return key in self._properties 560 | 561 | @property 562 | def data(self): 563 | return self._properties 564 | 565 | @property 566 | def changes(self): 567 | added = {} 568 | deleted = {} 569 | changed = {} 570 | changes = [] 571 | 572 | for k, v in self._properties.items(): 573 | if k not in self._initial: 574 | added[k] = v 575 | 576 | for k, v in self._properties.items(): 577 | if k not in added and k in self._initial and self._initial[k] != v: 578 | changes.append({ 579 | 'from': self._initial[k], 580 | 'to': v, 581 | 'key': k, 582 | }) 583 | 584 | for k, v in self._initial.items(): 585 | if k not in self._properties: 586 | deleted[k] = v 587 | 588 | if added: 589 | changed['added'] = added 590 | 591 | if changes: 592 | changed['changes'] = changes 593 | 594 | if deleted: 595 | changed['deleted'] = deleted 596 | 597 | return changed 598 | 599 | 600 | class _ImmutableField: 601 | default = '' 602 | 603 | @property 604 | def values(self): 605 | return self.data 606 | 607 | @property 608 | def data(self): 609 | try: 610 | return self.to_python(self._values._values[-1]) 611 | except: 612 | return self.default 613 | 614 | 615 | class String(Field): 616 | 617 | def to_python(self, value): 618 | try: 619 | if not value._value: 620 | return '' 621 | 622 | return str(value._value) 623 | except: 624 | return '' 625 | 626 | 627 | class Integer(Field): 628 | 629 | def to_python(self, value): 630 | try: 631 | return int(float(value._value)) 632 | except: 633 | return 0 634 | 635 | 636 | class Increment(Integer): 637 | 638 | @property 639 | def default_value(self): 640 | return 0 641 | 642 | def to_graph(self, value): 643 | value.value = value._value + 1 644 | 645 | return value._value 646 | 647 | 648 | class Float(Field): 649 | 650 | def to_python(self, value): 651 | try: 652 | return float(value._value) 653 | except: 654 | return 0.0 655 | 656 | 657 | class Boolean(Field): 658 | 659 | @property 660 | def default_value(self): 661 | return False 662 | 663 | def _convert(self, value): 664 | if str(value).lower().strip() not in ['true', 'false']: 665 | value = bool(value) 666 | 667 | value = str(value).lower().strip() 668 | 669 | return bool(json.loads(value)) 670 | 671 | def to_python(self, value): 672 | try: 673 | return self._convert(value._value) 674 | except: 675 | return False 676 | 677 | def to_graph(self, value): 678 | return self.to_python(value=value) 679 | 680 | 681 | class Map(Field): 682 | 683 | def __init__(self, name=None, values=None, data_type='python', *args, 684 | **kwargs): 685 | if isinstance(self, Map) and isinstance(values, dict) \ 686 | and 'value' not in values: 687 | values = {'value': values} 688 | elif isinstance(self, List) and isinstance(values, (list, tuple,))\ 689 | and (len(values) and not isinstance(values[0], dict)): 690 | values = {'value': values} 691 | 692 | super().__init__(name=name, values=values, data_type=data_type, *args, 693 | **kwargs) 694 | 695 | @property 696 | def default_value(self): 697 | return {'value': {}} 698 | 699 | def to_python(self, value): 700 | if (isinstance(value._value, str) and 701 | len(value._value.replace(' ', ''))): 702 | return json.loads(value._value) 703 | else: 704 | return value._value 705 | 706 | 707 | class List(Map): 708 | 709 | @property 710 | def default_value(self): 711 | return {'value': []} 712 | 713 | @property 714 | def default_value(self): 715 | return [] 716 | 717 | 718 | class Option(Field): 719 | 720 | def __init__(self, options, name=None, values=None, data_type='python', 721 | *args, **kwargs): 722 | self.options = options 723 | 724 | super().__init__(name=name, values=values, data_type=data_type, *args, 725 | **kwargs) 726 | 727 | def can_set(self, value): 728 | if isinstance(value, Value): 729 | value = value.value 730 | 731 | return value in self.options 732 | 733 | 734 | class DateTime(Float): 735 | 736 | def to_python(self, value): 737 | val = value._value 738 | 739 | if isinstance(val, datetime): 740 | val = val.timestamp() 741 | 742 | v = Value(value=val, properties=value.properties, id=value.id) 743 | 744 | return super().to_python(value=v) 745 | 746 | 747 | class TimeStamp(DateTime): 748 | 749 | def __init__(self, name=None, values=None, data_type='python', *args, 750 | **kwargs): 751 | 752 | def default(): 753 | return datetime.now() 754 | 755 | kwargs['default'] = default 756 | 757 | super().__init__(name=name, values=values, data_type=data_type, 758 | max_values=1, overwrite_last_value=False, *args, 759 | **kwargs) 760 | 761 | def can_set(self, value): 762 | return False 763 | 764 | 765 | class GremlinID(_ImmutableField, String): 766 | pass 767 | 768 | 769 | class GremlinLabel(GremlinID): 770 | pass 771 | 772 | 773 | class GremlinType(GremlinLabel): 774 | pass 775 | 776 | 777 | class GIZMOEntity(GremlinID): 778 | pass 779 | 780 | 781 | class Relationship(Traversal): 782 | _relationship_edges = { 783 | 'out': 'outE', 784 | 'in': 'inE', 785 | } 786 | _relationship_opposites = { 787 | 'in': 'out', 788 | 'out': 'in', 789 | 'both': 'both', 790 | } 791 | 792 | def __init__(self, edge_entity, mapper=None, entity=None, 793 | relationship_direction='out', allow_multiple=True): 794 | self._edge_entity = edge_entity 795 | self._allow_multiple = allow_multiple 796 | self._relationship_direction = relationship_direction 797 | 798 | super().__init__(entity=entity, mapper=mapper) 799 | 800 | def _build_initial_query(self, edge_only=False): 801 | from .mapper import next_entity_param 802 | 803 | built = super()._build_initial_query() 804 | 805 | if built and self._edge_entity: 806 | label = next_entity_param(self._edge_entity, 'label', 807 | str(self._edge_entity)) 808 | 809 | if edge_only: 810 | func = self._relationship_edges[self._relationship_direction] 811 | self.func(func, label) 812 | else: 813 | self.func(self._relationship_direction, label) 814 | 815 | return self 816 | 817 | def _clear(self): 818 | """method used to clear existing relationships. This will not remove 819 | any vertices. This method does not execute the query, but will 820 | enqueue it with the mapper""" 821 | self.edges 822 | self.drop().iterate() 823 | self._mapper.enqueue_script(gremlin=self) 824 | 825 | return self 826 | 827 | def _add(self, entity, data=None): 828 | """method used to add an entity to an entity as a connection to the 829 | parent entity. This method does not execute the query, but will 830 | enqueue it with the mapper""" 831 | if self._entity: 832 | if not self._allow_multiple: 833 | self._clear() 834 | 835 | kwargs = { 836 | 'data': data, 837 | 'edge_entity': self._edge_entity, 838 | } 839 | 840 | if self._relationship_direction is 'out': 841 | kwargs['out_v'] = self._entity 842 | kwargs['in_v'] = entity 843 | else: 844 | kwargs['out_v'] = entity 845 | kwargs['in_v'] = self._entity 846 | 847 | edge = self._mapper.connect(**kwargs) 848 | self._mapper.save(edge) 849 | 850 | return edge 851 | 852 | return None 853 | 854 | def __add__(self, entity): 855 | return self.add(entity) 856 | 857 | def _remove(self, entity, direction='both'): 858 | """method used to remove a connection between vertices. This method 859 | does not execute the query, but will enqueue it with the mapper""" 860 | from gremlinpy.statement import GetEdge 861 | 862 | try: 863 | edge = GetEdge(entity['id'], self._entity['id'], 864 | label=str(self._edge_entity), direction=direction) 865 | self.mapper.delete(edge) 866 | 867 | return edge 868 | except: 869 | return None 870 | 871 | def __sub__(self, entity): 872 | return self._remove(entity=entity) 873 | 874 | @property 875 | def edges(self): 876 | """utility property that will return all of the existing edges based 877 | on the _edge_entity attribute""" 878 | self.reset() 879 | 880 | return self._build_initial_query(edge_only=True) 881 | -------------------------------------------------------------------------------- /gizmo/mapper.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import inspect 3 | import re 4 | 5 | from collections import OrderedDict 6 | 7 | from gremlinpy.gremlin import Gremlin, Param, AS 8 | 9 | from .entity import (_Entity, Vertex, Edge, GenericVertex, GenericEdge, 10 | ENTITY_MAP) 11 | from .exception import (AstronomerQueryException, AstronomerMapperException) 12 | from .traversal import Traversal 13 | from .util import (camel_to_underscore, GIZMO_ID, GIZMO_LABEL, GIZMO_TYPE, 14 | GIZMO_ENTITY, GIZMO_VARIABLE, entity_name) 15 | 16 | 17 | logger = logging.getLogger(__name__) 18 | ENTITY_MAPPER_MAP = {} 19 | GENERIC_MAPPER = 'generic.mapper' 20 | _count = -1 21 | _query_count = 0 22 | _query_params = {} 23 | 24 | 25 | def next_query_variable(): 26 | global _count 27 | _count += 1 28 | 29 | return '{}_{}'.format(GIZMO_VARIABLE, _count) 30 | 31 | 32 | def get_entity_mapper(entity=None, name=GENERIC_MAPPER): 33 | if isinstance(entity, _Entity): 34 | name = get_qualified_instance_name(entity) 35 | else: 36 | name = get_qualified_name(entity) 37 | 38 | if name not in ENTITY_MAPPER_MAP: 39 | name = GENERIC_MAPPER 40 | 41 | return ENTITY_MAPPER_MAP[name](self) 42 | 43 | 44 | def next_param_name(param): 45 | param = re.sub('\W', '_', param) 46 | 47 | if param not in _query_params: 48 | _query_params[param] = -1 49 | 50 | _query_params[param] += 1 51 | 52 | return '{}_{}'.format(param, _query_params[param]) 53 | 54 | 55 | def next_param(param, value): 56 | if isinstance(value, _Entity): 57 | value = entity_name(value) 58 | 59 | return Param(next_param_name(param), value) 60 | 61 | 62 | def next_entity_param(entity, param, value): 63 | name = entity_name(entity) 64 | field = '{}_{}'.format(name, param) 65 | 66 | return next_param(field, value) 67 | 68 | 69 | class Mapper: 70 | 71 | def __init__(self, request, gremlin=None, auto_commit=True, 72 | graph_instance_name=None): 73 | if not gremlin: 74 | gremlin = Gremlin() 75 | 76 | self.request = request 77 | self.gremlin = gremlin 78 | self.auto_commit = auto_commit 79 | self.graph_instance_name = graph_instance_name 80 | 81 | if not self.auto_commit and not self.graph_instance_name: 82 | error = ('If auto_commit is set, we need to know the' 83 | ' graph instance name') 84 | logger.exception(error) 85 | raise ArgumentError(error) 86 | 87 | self.reset() 88 | 89 | def reset(self): 90 | self.gremlin.reset() 91 | global _query_count 92 | global _count 93 | global _query_params 94 | 95 | _query_count = 0 96 | _count = 0 97 | _query_params = {} 98 | self.queries = [] 99 | self.return_vars = [] 100 | self.entities = OrderedDict() # ensure FIFO for testing 101 | self.del_entities = {} 102 | self.params = {} 103 | self.callbacks = {} 104 | self._magic_method = None 105 | 106 | def get_entity_variable(self, entity): 107 | ret = None 108 | 109 | for key, def_entity in self.entities.items(): 110 | if entity == def_entity: 111 | return key 112 | 113 | return ret 114 | 115 | def get_mapper(self, entity=None, name=GENERIC_MAPPER): 116 | if entity is not None: 117 | name = entity_name(entity) 118 | 119 | if name not in ENTITY_MAPPER_MAP: 120 | name = GENERIC_MAPPER 121 | 122 | return ENTITY_MAPPER_MAP[name](self) 123 | 124 | def enqueue_mapper(self, mapper): 125 | self.queries += mapper.queries 126 | self.return_vars += mapper.return_vars 127 | self.entities.update(mapper.entities) 128 | self.params.update(mapper.params) 129 | 130 | for entity, callbacks in mapper.callbacks.items(): 131 | exisiting = self.callbacks.get(entity, []) 132 | 133 | self.callbacks[entity] = exisiting + callbacks 134 | 135 | mapper.reset() 136 | 137 | return self 138 | 139 | def enqueue_script(self, gremlin=None, script=None, params=None): 140 | if gremlin is not None: 141 | script = [str(gremlin),] 142 | params = gremlin.bound_params 143 | 144 | gremlin.reset() 145 | 146 | if script: 147 | self.queries += script 148 | 149 | if params: 150 | self.params.update(params) 151 | 152 | return self 153 | 154 | def __getattr__(self, magic_method): 155 | """magic method that works in conjunction with __call__ 156 | method these two methods are used to shortcut the retrieval 157 | of an entity's mapper and call a specific method against 158 | 159 | this chain: 160 | user = User() 161 | user_mapper = mapper.get_mapper(user) 162 | emails = user_mapper.get_emails(user) 163 | 164 | can be shortened into: 165 | user = User() 166 | emails = mapper.get_emails(user) 167 | """ 168 | self._magic_method = magic_method 169 | 170 | return self 171 | 172 | def __call__(self, *args, **kwargs): 173 | mapper = self.get_mapper(args[0]) 174 | 175 | return getattr(mapper, self._magic_method)(*args, **kwargs) 176 | 177 | async def data(self, entity, *args): 178 | """utility method used to retrieve an entity's data. It 179 | also allows for method chaining in order to augment the 180 | resulting data. 181 | 182 | class MyMapper(_GenericMapper): 183 | async def add_two(self, entity, data): 184 | data['two'] = 2 185 | return data 186 | 187 | async def add_three(self, entity, data): 188 | data['three'] = 3 189 | return data 190 | 191 | entity = User() 192 | data = await mapper.data(user, 'add_two', 'add_three') 193 | 194 | the resulting data will have the data from the User class, 195 | plus a two and a three member 196 | """ 197 | collection = isinstance(entity, Collection) 198 | 199 | async def get_data(entity, data): 200 | retrieved = data 201 | 202 | for method in args: 203 | mapper = self.get_mapper(entity) 204 | 205 | async def wrapper(entity, data): 206 | res = await getattr(mapper, method)(entity=entity, 207 | data=data) 208 | 209 | return res 210 | 211 | retrieved = await wrapper(entity=entity, 212 | data=retrieved) 213 | 214 | return retrieved 215 | 216 | if collection: 217 | data = [] 218 | 219 | for coll_entity in entity: 220 | mapper = self.get_mapper(coll_entity) 221 | entity_data = await mapper.data(coll_entity) 222 | res = await get_data(coll_entity, entity_data) 223 | 224 | data.append(res) 225 | else: 226 | mapper = self.get_mapper(entity) 227 | entity_data = await mapper.data(entity) 228 | data = await get_data(entity, entity_data) 229 | 230 | return data 231 | 232 | def save(self, entity, bind_return=True, mapper=None, 233 | callback=None, **kwargs): 234 | if mapper is None: 235 | mapper = self.get_mapper(entity) 236 | 237 | logger.debug(('Saving entity: {} with mapper:' 238 | ' {}').format(entity.__repr__, mapper)) 239 | mapper.save(entity, bind_return, callback, **kwargs) 240 | 241 | return self.enqueue_mapper(mapper) 242 | 243 | def delete(self, entity, mapper=None, callback=None): 244 | if mapper is None: 245 | mapper = self.get_mapper(entity) 246 | 247 | 248 | logger.debug(('Deleting entity: {} with mapper:' 249 | ' {}').format(entity.__repr__, mapper)) 250 | mapper.delete(entity, callback=callback) 251 | 252 | # manually add the deleted entity to the self.entities 253 | # collection for callbacks 254 | from random import randrange 255 | key = 'DELETED_%s_entity' % str(randrange(0, 999999999)) 256 | self.del_entities[key] = entity 257 | 258 | return self.enqueue_mapper(mapper) 259 | 260 | def create(self, data=None, entity=None, data_type='python'): 261 | if data is None: 262 | data = {} 263 | 264 | if entity: 265 | mapper = self.get_mapper(entity) 266 | else: 267 | name = data.get(GIZMO_ENTITY, GENERIC_MAPPER) 268 | 269 | if isinstance(name, (list, tuple)): 270 | name = name[0]['value'] 271 | 272 | mapper = self.get_mapper(name=name) 273 | 274 | kwargs = { 275 | 'data': data, 276 | 'entity': entity, 277 | 'data_type': data_type, 278 | } 279 | 280 | return mapper.create(**kwargs) 281 | 282 | def connect(self, out_v, in_v, label=None, data=None, edge_entity=None, 283 | data_type='python'): 284 | """ 285 | method used to connect two vertices and create an Edge object 286 | the resulting edge is not saved to to graph until it is passed to 287 | save allowing further augmentation 288 | """ 289 | if not isinstance(out_v, Vertex): 290 | if not isinstance(out_v, (str, int)): 291 | err = 'The out_v needs to be either a Vertex or an id' 292 | 293 | logger.exception(err) 294 | raise AstronomerMapperException(err) 295 | 296 | if not isinstance(in_v, Vertex): 297 | if not isinstance(in_v, (str, int)): 298 | err = 'The in_v needs to be either a Vertex or an id' 299 | 300 | logger.exception(err) 301 | raise AstronomerMapperException(err) 302 | 303 | if data is None: 304 | data = {} 305 | 306 | data['outV'] = out_v 307 | data['inV'] = in_v 308 | data[GIZMO_TYPE] = 'edge' 309 | data[GIZMO_LABEL[0]] = label 310 | 311 | return self.create(data=data, entity=edge_entity, data_type=data_type) 312 | 313 | def start(self, entity): 314 | mapper = self.get_mapper(entity) 315 | 316 | return mapper.start(entity) 317 | 318 | def _build_queries(self): 319 | if len(self.return_vars) > 0: 320 | returns = [] 321 | 322 | for k in self.return_vars: 323 | returns.append("'{}': {}".format(k, k)) 324 | 325 | ret = '[{}]'.format(', '.join(returns)) 326 | 327 | self.queries.append(ret) 328 | 329 | return self 330 | 331 | def get(self, entity): 332 | mapper = self.get_mapper(entity) 333 | 334 | return mapper.get(entity) 335 | 336 | def apply_statement(self, statement): 337 | self.gremlin.apply_statement(statement) 338 | 339 | return self 340 | 341 | async def send(self): 342 | self._build_queries() 343 | 344 | script = ";\n".join(self.queries) 345 | params = self.params 346 | entities = self.entities 347 | callbacks = self.callbacks 348 | entities.update(self.del_entities) 349 | self.reset() 350 | 351 | res = await self.query(script=script, params=params, 352 | update_entities=entities, callbacks=callbacks) 353 | 354 | return res 355 | 356 | async def query(self, script=None, params=None, gremlin=None, 357 | update_entities=None, callbacks=None, collection=None): 358 | if gremlin is not None: 359 | script = str(gremlin) 360 | params = gremlin.bound_params 361 | 362 | gremlin.reset() 363 | 364 | if script is None: 365 | script = '' 366 | 367 | if params is None: 368 | params = {} 369 | 370 | if update_entities is None: 371 | update_entities = {} 372 | 373 | self.reset() 374 | 375 | response = await self.request.send(script, params, update_entities) 376 | 377 | for k, entity in update_entities.items(): 378 | cbs = callbacks.get(entity, []) 379 | for c in cbs: 380 | c(entity) 381 | 382 | if not collection: 383 | collection = Collection 384 | 385 | return collection(self, response) 386 | 387 | 388 | class _RootMapper(type): 389 | """ 390 | In the case of custom mappers, this metaclass will register the entity name 391 | with the mapper object. This is done so that when entities are loaded by 392 | name the associated mapper is used to CRUD it. 393 | 394 | This only works when the Mapper.create method is used to 395 | create the entity 396 | """ 397 | 398 | def __new__(cls, name, bases, attrs): 399 | cls = super(_RootMapper, cls).__new__(cls, name, bases, attrs) 400 | entity = attrs.pop('entity', None) 401 | 402 | if entity: 403 | map_name = entity_name(entity) 404 | ENTITY_MAPPER_MAP[map_name] = cls 405 | elif name == 'EntityMapper': 406 | ENTITY_MAPPER_MAP[GENERIC_MAPPER] = cls 407 | 408 | return cls 409 | 410 | def __call__(cls, *args, **kwargs): 411 | mapper = super(_RootMapper, cls).__call__(*args, **kwargs) 412 | 413 | for field in dir(mapper): 414 | if field.startswith('_'): 415 | continue 416 | 417 | val = getattr(mapper, field) 418 | 419 | if inspect.isclass(val) and issubclass(val, EntityMapper): 420 | if mapper.mapper: 421 | instance = val(mapper.mapper) 422 | setattr(mapper, field, instance) 423 | 424 | return mapper 425 | 426 | 427 | class EntityMapper(metaclass=_RootMapper): 428 | VARIABLE = GIZMO_VARIABLE 429 | unique = False 430 | unique_fields = None 431 | save_statements = None 432 | 433 | def __init__(self, mapper=None): 434 | self.mapper = mapper 435 | self.gremlin = None 436 | 437 | if self.mapper: 438 | self.gremlin = mapper.gremlin 439 | 440 | self.reset() 441 | 442 | def reset(self): 443 | self.queries = [] 444 | self.return_vars = [] 445 | self.entities = {} 446 | self.params = {} 447 | self.callbacks = {} 448 | 449 | async def data(self, entity): 450 | return entity.data 451 | 452 | def get(self, entity): 453 | trav = self.start(entity) 454 | vertex = issubclass(self.entity, Vertex) 455 | param_value = str(self.entity) 456 | param_name = 'out_{}_{}'.format(entity.__class__.__name__, param_value) 457 | entity_param = next_param(param_name, param_value) 458 | 459 | if vertex: 460 | trav.out().hasLabel(entity_param) 461 | else: 462 | trav.outE(entity_param) 463 | 464 | return trav 465 | 466 | def enqueue(self, query, bind_return=True): 467 | for entry in query.queries: 468 | script = entry['script'] 469 | 470 | if script in self.queries: 471 | continue 472 | 473 | if bind_return: 474 | variable = next_query_variable() 475 | script = '{} = {}'.format(variable, script) 476 | 477 | if 'entity' in entry: 478 | self.entities[variable] = entry['entity'] 479 | self.return_vars.append(variable) 480 | 481 | self.queries.append(script) 482 | self.params.update(entry['params']) 483 | 484 | return self 485 | 486 | def _enqueue_callback(self, entity, callback): 487 | if callback: 488 | listed = self.callbacks.get(entity, []) 489 | 490 | if isinstance(callback, (list, tuple)): 491 | listed += list(callback) 492 | elif callback: 493 | listed.append(callback) 494 | 495 | self.callbacks[entity] = listed 496 | 497 | return self 498 | 499 | def on_create(self, entity): 500 | pass 501 | 502 | def on_update(self, entity): 503 | pass 504 | 505 | def on_delete(self, entity): 506 | pass 507 | 508 | def _build_save_statements(self, entity, query, **kwargs): 509 | statement_query = Query(self.mapper) 510 | query_gremlin = Gremlin(self.gremlin.gv) 511 | 512 | for entry in query.queries: 513 | query_gremlin.bind_params(entry['params']) 514 | 515 | for statement in self.save_statements: 516 | instance = statement(entity, self, query, **kwargs) 517 | 518 | query_gremlin.apply_statement(instance) 519 | 520 | statement_query._add_query(str(query_gremlin), 521 | query_gremlin.bound_params, entity=entity) 522 | 523 | return statement_query 524 | 525 | def start(self, entity=None): 526 | return Traversal(self.mapper, entity or self.entity) 527 | 528 | def save(self, entity, bind_return=True, callback=None, *args, **kwargs): 529 | """callback and be a single callback or a list of them""" 530 | method = '_save_edge' if entity[GIZMO_TYPE] == 'edge' else \ 531 | '_save_vertex' 532 | 533 | if not isinstance(callback, (list, tuple)) and callback: 534 | callback = [callback] 535 | else: 536 | callback = [] 537 | 538 | if entity[GIZMO_ID]: 539 | callback.insert(0, self.on_update) 540 | else: 541 | callback.insert(0, self.on_create) 542 | 543 | self._enqueue_callback(entity, callback) 544 | 545 | return getattr(self, method)(entity=entity, bind_return=bind_return) 546 | 547 | def _save_vertex(self, entity, bind_return=True): 548 | """ 549 | method used to save a entity. IF both the unique_type and unique_fields 550 | params are set, it will run a sub query to check to see if an entity 551 | exists that matches those values 552 | """ 553 | query = Query(self.mapper) 554 | ref = self.mapper.get_entity_variable(entity) 555 | 556 | """ 557 | check to see if the entity has been used already in the current script 558 | execution. 559 | If it has use the reference 560 | if it hasnt, go through the process of saving it 561 | """ 562 | if ref: 563 | query._add_query(ref, params=None, entity=entity) 564 | 565 | return self.enqueue(query, bind_return) 566 | 567 | query.save(entity) 568 | 569 | if not entity[GIZMO_ID] and self.unique_fields: 570 | from .statement import MapperUniqueVertex 571 | 572 | if not self.save_statements: 573 | self.save_statements = [] 574 | 575 | if MapperUniqueVertex not in self.save_statements: 576 | self.save_statements.append(MapperUniqueVertex) 577 | 578 | if self.save_statements and len(self.save_statements): 579 | statement_query = self._build_save_statements(entity, query) 580 | 581 | return self.enqueue(statement_query, bind_return) 582 | else: 583 | return self.enqueue(query, bind_return) 584 | 585 | def _save_edge(self, entity, bind_return=True): 586 | query = Query(self.mapper) 587 | save = True 588 | edge_ref = self.mapper.get_entity_variable(entity) 589 | out_v = entity.out_v 590 | out_v_id = out_v[GIZMO_ID] if isinstance(out_v, Vertex) else None 591 | in_v = entity.in_v 592 | in_v_id = in_v[GIZMO_ID] if isinstance(in_v, Vertex) else None 593 | out_v_ref = self.mapper.get_entity_variable(out_v) 594 | in_v_ref = self.mapper.get_entity_variable(in_v) 595 | 596 | if edge_ref: 597 | query._add_query(edge_ref, params=None, entity=entity) 598 | 599 | return self.enqueue(query, bind_return) 600 | 601 | """ 602 | both out_v and in_v are checked to see if the entities stored in each 603 | respective variable has been used. 604 | If they have not and they are Vertex instances with an empty _id, 605 | send them to be saved. 606 | if they have been used, use the reference variable in the create edge 607 | logic 608 | """ 609 | query.save(entity) 610 | 611 | if not entity[GIZMO_ID] and self.unique and in_v_id and out_v_id: 612 | from .statement import MapperUniqueEdge 613 | 614 | if not self.save_statements: 615 | self.save_statements = [] 616 | 617 | if MapperUniqueEdge not in self.save_statements: 618 | self.save_statements.append(MapperUniqueEdge) 619 | 620 | if self.save_statements and len(self.save_statements): 621 | statement_query = self._build_save_statements(entity, query, 622 | out_v_id=out_v_id, in_v_id=in_v_id, 623 | label=entity[GIZMO_LABEL[0]], direction=self.unique) 624 | 625 | return self.enqueue(statement_query, False) 626 | else: 627 | return self.enqueue(query, bind_return) 628 | 629 | def delete(self, entity, lookup=True, callback=None): 630 | query = Query(self.mapper) 631 | 632 | if not isinstance(callback, (list, tuple)) and callback: 633 | callback = [callback] 634 | else: 635 | callback = [] 636 | 637 | query.delete(entity) 638 | callback.insert(0, self.on_delete) 639 | self._enqueue_callback(entity, callback) 640 | 641 | return self.enqueue(query, False) 642 | 643 | def create(self, data=None, entity=None, data_type='python'): 644 | """ 645 | Method used to create a new entity based on the data that is passed in. 646 | If the kwarg entity is passed in, it will be used to create the 647 | entity else if utils.GIZMO_ENTITY is in data, that will be used 648 | finally, entity.GenericVertex or entity.GenericEdge will be used to 649 | construct the entity 650 | """ 651 | check = True 652 | 653 | if data is None: 654 | data = {} 655 | 656 | if entity is not None: 657 | try: 658 | label = data.get(GIZMO_LABEL[0], None) 659 | entity = entity(data=data, data_type=data_type) 660 | check = False 661 | 662 | for f, r in entity._relationships.items(): 663 | r._mapper = self.mapper 664 | r._entity = entity 665 | except Exception as e: 666 | pass 667 | 668 | if check: 669 | try: 670 | if GIZMO_ENTITY in data: 671 | name = data[GIZMO_ENTITY] 672 | 673 | if isinstance(name, (list, tuple)): 674 | name = name[0]['value'] 675 | 676 | entity = ENTITY_MAP[name](data=data, data_type=data_type) 677 | 678 | for f, r in entity._relationships.items(): 679 | r._mapper = self.mapper 680 | r._entity = entity 681 | else: 682 | raise 683 | except Exception as e: 684 | # all else fails create a GenericVertex unless _type is 'edge' 685 | if data.get(GIZMO_TYPE, None) == 'edge': 686 | entity = GenericEdge(data=data, data_type=data_type) 687 | else: 688 | entity = GenericVertex(data=data, data_type=data_type) 689 | 690 | if GIZMO_ID in data: 691 | entity[GIZMO_ID] = data[GIZMO_ID] 692 | 693 | return entity 694 | 695 | def delete(self, entity, lookup=True, callback=None): 696 | query = Query(self.mapper) 697 | 698 | if not isinstance(callback, (list, tuple)) and callback: 699 | callback = [callback] 700 | else: 701 | callback = [] 702 | 703 | query.delete(entity) 704 | callback.insert(0, self.on_delete) 705 | self._enqueue_callback(entity, callback) 706 | 707 | return self.enqueue(query, False) 708 | 709 | 710 | class Query: 711 | 712 | def __init__(self, mapper): 713 | self.mapper = mapper 714 | self.gremlin = Gremlin(self.mapper.gremlin.gv) 715 | self.queries = [] 716 | self.fields = [] 717 | 718 | self.reset() 719 | 720 | def reset(self): 721 | self.fields = [] 722 | return self 723 | 724 | def _add_query(self, script, params=None, entity=None): 725 | if params is None: 726 | params = {} 727 | 728 | self.queries.append({ 729 | 'script': script, 730 | 'params': params, 731 | 'entity': entity, 732 | }) 733 | 734 | return self 735 | 736 | def _add_gremlin_query(self, entity=None): 737 | script = str(self.gremlin) 738 | params = self.gremlin.bound_params 739 | 740 | self._add_query(script, params, entity) 741 | 742 | return self.reset() 743 | 744 | def _field_changes(self, gremlin, entity, ignore=None): 745 | ignore = ignore or [] 746 | entity_name = str(entity) 747 | entity_alias = '{}_alias'.format(entity_name) 748 | entity_alias = next_param(entity_alias, entity_alias) 749 | 750 | def add_field(field, data): 751 | values = data.get('values', data.get('value', None)) 752 | 753 | if not isinstance(values, (list, tuple,)): 754 | values = [values, ] 755 | 756 | for i, value in enumerate(values): 757 | name = '{}_{}_{}'.format(entity_name, field, i) 758 | prop = "'{}'".format(field) 759 | 760 | gremlin.property(prop, Param(name, value)) 761 | 762 | def add_property(field, value, properties=None, ignore=None): 763 | ignore = ignore or [] 764 | 765 | if field.startswith('T.'): 766 | val_param = next_param('{}_{}'.format(entity_name, 767 | field), value) 768 | gremlin.unbound('property', field, val_param) 769 | return 770 | 771 | field_name = '{}_{}'.format(entity_name, field) 772 | prop = next_param(field_name, field) 773 | value_name = '{}_value'.format(field_name) 774 | value_param = next_param(value_name, value) 775 | params = [prop, value_param] 776 | 777 | if properties: 778 | for key, val in properties.items(): 779 | prop_key = next_param('{}_{}'.format(prop.name, 780 | key), key) 781 | prop_val = next_param('{}_{}_val'.format(prop.name, 782 | key), val) 783 | params += [prop_key, prop_val] 784 | 785 | gremlin.property(*params) 786 | 787 | for field, changes in entity.changes.items(): 788 | if field in ignore: 789 | continue 790 | 791 | if changes['immutable']: 792 | for val in changes['values']['values']: 793 | add_property(field, val) 794 | elif changes['deleted']: 795 | prop = next_param('{}_{}'.format(entity_name, field), field) 796 | remove = Gremlin('').it.get().func('remove') 797 | 798 | gremlin.AS(entity_alias).properties(prop) 799 | gremlin.sideEffect.close(remove) 800 | gremlin.select(entity_alias) 801 | else: 802 | for action, value in changes['values'].items(): 803 | if action == 'added': 804 | for val in value: 805 | add_property(field, val['value'], 806 | val['properties']) 807 | 808 | def _add_vertex(self, entity, set_variable=None): 809 | entity.data_type = 'graph' 810 | gremlin = self.gremlin 811 | label = None 812 | ignore = ['T.label', 'label'] 813 | 814 | if entity['label']: 815 | label = next_entity_param(entity, 'label', entity['label']) 816 | gremlin.unbound('addV', 'T.label', label) 817 | else: 818 | gremlin.addV() 819 | 820 | if set_variable: 821 | gremlin.set_ret_variable(set_variable, ignore=[GIZMO_ID, ]) 822 | 823 | self._field_changes(gremlin, entity, ignore=ignore) 824 | gremlin.func('next') 825 | 826 | entity.data_type = 'python' 827 | 828 | return self._add_gremlin_query(entity) 829 | 830 | def _update_entity(self, entity, set_variable=None): 831 | entity.data_type = 'graph' 832 | gremlin = self.gremlin 833 | entity_type, entity_id = entity.get_rep() 834 | 835 | if not entity_id: 836 | error = (('The entity {} scheduled to be updated does not have' 837 | ' an id').format(str(entity))) 838 | logger.exception(error) 839 | raise Exception() 840 | 841 | _id = next_param('{}_ID'.format(str(entity)), entity_id) 842 | ignore = [GIZMO_ID, GIZMO_LABEL[1]] 843 | alias = '{}_{}_updating'.format(entity_type, entity_id) 844 | alias = next_param(alias, alias) 845 | 846 | getattr(gremlin, entity_type.upper())(_id) 847 | gremlin.AS(alias) 848 | 849 | self._field_changes(gremlin, entity, ignore=ignore) 850 | gremlin.select(alias).next() 851 | 852 | entity.data_type = 'python' 853 | 854 | return self._add_gremlin_query(entity) 855 | 856 | def _add_edge(self, entity, set_variable=None): 857 | if not entity[GIZMO_LABEL[0]]: 858 | msg = 'A label is required in order to create an edge' 859 | logger.exception(msg) 860 | raise AstronomerQueryException(msg) 861 | 862 | def get_or_create_ends(): 863 | """this function will determine if the edge has both ends. If 864 | either end is an _Entity object it will get the reference to 865 | the object or save it and create a reference. Either the entity's 866 | id or reference will be used when saving the edge. 867 | """ 868 | out_v = entity.out_v 869 | out_v_ref = None 870 | in_v = entity.in_v 871 | in_v_ref = None 872 | 873 | if out_v is None or in_v is None: 874 | error = ('Both out and in vertices must be set before' 875 | ' saving the edge') 876 | logger.exception(error) 877 | raise AstronomerQueryException(error) 878 | 879 | if isinstance(out_v, _Entity): 880 | if out_v[GIZMO_ID]: 881 | out_v = out_v[GIZMO_ID] 882 | else: 883 | out_v_ref = self.mapper.get_entity_variable(out_v) 884 | 885 | if not out_v_ref: 886 | self.mapper.save(out_v) 887 | out_v_ref = self.mapper.get_entity_variable(out_v) 888 | 889 | if out_v_ref: 890 | out_v = out_v_ref 891 | 892 | if isinstance(in_v, _Entity): 893 | if in_v[GIZMO_ID]: 894 | in_v = in_v[GIZMO_ID] 895 | else: 896 | in_v_ref = self.mapper.get_entity_variable(in_v) 897 | 898 | if not in_v_ref: 899 | self.mapper.save(in_v) 900 | in_v_ref = self.mapper.get_entity_variable(in_v) 901 | 902 | if in_v_ref: 903 | in_v = in_v_ref 904 | 905 | return { 906 | 'out': { 907 | 'is_ref': out_v_ref, 908 | 'v': out_v, 909 | }, 910 | 'in': { 911 | 'is_ref': in_v_ref, 912 | 'v': in_v, 913 | }, 914 | } 915 | 916 | ends = get_or_create_ends() 917 | name = str(entity) 918 | gremlin = self.gremlin 919 | g = Gremlin(gremlin.gv) 920 | label = next_param('{}_label'.format(name), entity[GIZMO_LABEL[0]]) 921 | 922 | """ 923 | g.V($OUT_ID).next().addEdge($LABEL, g.V($IN_ID).next()).property(....) 924 | """ 925 | in_v = ends['in'] 926 | out_v = ends['out'] 927 | 928 | if in_v['is_ref']: 929 | g.unbound('V', in_v['v']) 930 | else: 931 | in_id = next_param('{}_in'.format(name), in_v['v']) 932 | 933 | g.V(in_id) 934 | 935 | g.func('next') 936 | 937 | if out_v['is_ref']: 938 | gremlin.unbound('V', out_v['v']) 939 | else: 940 | out_id = next_param('{}_out'.format(name), out_v['v']) 941 | 942 | gremlin.V(out_id) 943 | 944 | ignore = [GIZMO_LABEL[0], GIZMO_LABEL[1], GIZMO_TYPE] 945 | edge_args = [label, g] 946 | 947 | # edge properites only get one value and no meta-properties 948 | for field, changes in entity.changes.items(): 949 | if field in ignore: 950 | continue 951 | 952 | try: 953 | if changes['immutable']: 954 | value = changes['values']['values'][-1] 955 | else: 956 | value = changes['values'][-1] 957 | except: 958 | continue 959 | 960 | field_param = next_param('{}_{}'.format(name, field), field) 961 | field_value = next_param('{}_value'.format(field_param.name), 962 | value) 963 | edge_args += [field_param, field_value] 964 | 965 | gremlin.func('next').addEdge(*edge_args) 966 | 967 | return self._add_gremlin_query(entity) 968 | 969 | def save(self, entity, set_variable=None): 970 | if not entity[GIZMO_TYPE]: 971 | msg = 'The entity does not have a type defined' 972 | logger.exception(msg) 973 | raise AstronomerQueryException(msg) 974 | 975 | entity_type = entity[GIZMO_TYPE] 976 | 977 | if not entity[GIZMO_ID]: 978 | if entity_type == 'vertex': 979 | self._add_vertex(entity, set_variable) 980 | else: 981 | self._add_edge(entity, set_variable) 982 | else: 983 | self._update_entity(entity, set_variable) 984 | 985 | def delete(self, entity): 986 | entity_type, _id = entity.get_rep() 987 | 988 | if not _id: 989 | msg = ('The entity does not have an id defined and' 990 | ' connot be deleted') 991 | logger.exception(msg) 992 | raise AstronomerQueryException(msg) 993 | 994 | if not entity[GIZMO_TYPE]: 995 | msg = 'The entity does not have a type defined' 996 | logger.exception(msg) 997 | raise AstronomerQueryException(msg) 998 | 999 | delete = next_param('{}_ID'.format(str(entity)), _id) 1000 | 1001 | getattr(self.gremlin, entity_type)(delete).next().func('remove') 1002 | 1003 | return self._add_gremlin_query(entity) 1004 | 1005 | 1006 | class Collection(object): 1007 | 1008 | def __init__(self, mapper, response=None): 1009 | self.mapper = mapper 1010 | 1011 | if not response: 1012 | response = lambda: None 1013 | response.data = [] 1014 | 1015 | self.response = response 1016 | self._entities = {} 1017 | self._index = 0 1018 | self._data_type = 'python' 1019 | 1020 | def first(self): 1021 | return self[0] 1022 | 1023 | def last(self): 1024 | return self[-1] 1025 | 1026 | def get_data(self): 1027 | return [x for x in self.response.data] 1028 | 1029 | data = property(get_data) 1030 | 1031 | @property 1032 | def entity_data(self): 1033 | """ 1034 | this will get the instance data instead of the 1035 | raw data. This will use the mapper to create each 1036 | entity. Which may have a custom data attribute 1037 | """ 1038 | return [x.data for x in self] 1039 | 1040 | @property 1041 | async def mapper_data(self): 1042 | """this will get the data from the entity's mapper if it has a 1043 | custom mapper 1044 | """ 1045 | data = [] 1046 | 1047 | if len(self): 1048 | mapper = self.mapper.get_mapper(self[0]) 1049 | 1050 | for entity in self: 1051 | data.append(await mapper.data(entity)) 1052 | 1053 | return data 1054 | 1055 | def __len__(self): 1056 | return len(self.response.data) 1057 | 1058 | def __getitem__(self, key): 1059 | entity = self._entities.get(key, None) 1060 | 1061 | if entity is None: 1062 | try: 1063 | data = self.response[key] 1064 | 1065 | if data is not None: 1066 | entity = self.mapper.create(data=data, 1067 | data_type=self._data_type) 1068 | entity.dirty = False 1069 | self._entities[key] = entity 1070 | else: 1071 | raise StopIteration() 1072 | except Exception as e: 1073 | raise StopIteration() 1074 | 1075 | return entity 1076 | 1077 | def __setitem__(self, key, value): 1078 | self._entities[key] = value 1079 | 1080 | def __delitem__(self, key): 1081 | if key in self._entities: 1082 | del self._entities[key] 1083 | 1084 | def __iter__(self): 1085 | return self 1086 | 1087 | def __next__(self): 1088 | entity = self[self._index] 1089 | self._index += 1 1090 | 1091 | return entity 1092 | 1093 | -------------------------------------------------------------------------------- /gizmo/statement.py: -------------------------------------------------------------------------------- 1 | from gremlinpy import Statement, Gremlin, Param 2 | from gremlinpy.statement import GetEdge 3 | 4 | from .mapper import Query, next_param 5 | from .util import GIZMO_LABEL 6 | 7 | 8 | class MapperStatement(Statement): 9 | """Any sub-classed Statement will be applied to an empty Gremlin instance. 10 | The statement is given the current gizmo.mapper.Query instance for the 11 | entity that is currently being evaluated.""" 12 | 13 | def __init__(self, entity, mapper, query, **kwargs): 14 | self.entity = entity 15 | self.mapper = mapper 16 | self.query = query 17 | 18 | 19 | class MapperUniqueVertex(MapperStatement): 20 | """statement used to build a gremlin query that will check for the 21 | existence of a entity before adding the entity 22 | 23 | example query built: 24 | 25 | g.V().has("field", value).tryNext().orElseGet{ 26 | g.addV("field", value).next() 27 | } 28 | 29 | """ 30 | 31 | def build(self): 32 | gremlin = self.gremlin 33 | gremlin.V() 34 | save_query = self.query.queries[0] 35 | name = str(self.entity) 36 | 37 | if not isinstance(self.mapper.unique_fields, (list, set, tuple)): 38 | msg = ('The entity: {} must have a list as the' 39 | ' unique fields'.format(name)) 40 | raise TypeError(msg) 41 | 42 | for field in self.mapper.unique_fields: 43 | holder = '{}_{}'.format(name, field) 44 | g_field = next_param(holder, field) 45 | param = next_param(holder + '_value', 46 | self.entity[field].value) 47 | 48 | gremlin.has(g_field, param) 49 | 50 | gremlin.tryNext().orElseGet.close(save_query['script']) 51 | 52 | 53 | class MapperUniqueEdge(MapperStatement): 54 | """Statement used to build a gremlin query that will check for the 55 | existence of an edge between two entities before creating the edge. 56 | 57 | example query built: 58 | """ 59 | 60 | def __init__(self, entity, mapper, query, out_v_id, in_v_id, label, 61 | direction): 62 | super(MapperUniqueEdge, self).__init__(entity=entity, mapper=mapper, 63 | query=query) 64 | self.out_v_id = out_v_id 65 | self.in_v_id = in_v_id 66 | self.label = label 67 | self.direction = direction 68 | 69 | def build(self): 70 | save_query = self.query.queries[0] 71 | edge = GetEdge(self.out_v_id, self.in_v_id, self.label, 72 | self.mapper.unique) 73 | gremlin = self.gremlin 74 | 75 | gremlin.apply_statement(edge) 76 | gremlin.tryNext().orElseGet.close(save_query['script']) 77 | -------------------------------------------------------------------------------- /gizmo/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emehrkay/gizmo/01db2f51118f7d746061ace0b491237481949bad/gizmo/test/__init__.py -------------------------------------------------------------------------------- /gizmo/test/entity.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import json 3 | from random import randrange, random 4 | from pprint import pprint 5 | 6 | from gizmo.entity import Vertex, Edge, GenericVertex 7 | from gizmo.field import * 8 | from gizmo.util import camel_to_underscore, GIZMO_LABEL 9 | 10 | from gremlinpy.gremlin import Gremlin 11 | 12 | 13 | class TestVertex(Vertex): 14 | some_field = String() 15 | 16 | 17 | class TestEdge(Edge): 18 | some_field = String() 19 | 20 | 21 | class TestUniqueEdge(Edge): 22 | pass 23 | 24 | 25 | class TestUndefinedVertex(GenericVertex): 26 | allow_undefined = True 27 | 28 | 29 | class TestUndefinedEdge(Vertex): 30 | allow_undefined = True 31 | 32 | 33 | class EntityTests(unittest.TestCase): 34 | 35 | def set_up(self): 36 | pass 37 | 38 | def test_can_create_vertex(self): 39 | v = TestVertex() 40 | 41 | self.assertTrue(isinstance(v, Vertex)) 42 | self.assertEqual(v['type'], 'vertex') 43 | 44 | def test_can_access_fields_as_items_or_attributes(self): 45 | v = TestVertex({'id': 7}) 46 | i_id = v.id 47 | a_id = v['id'] 48 | 49 | self.assertEqual(i_id, a_id) 50 | self.assertEqual(v.some_field, v['some_field']) 51 | 52 | def test_can_create_vertex_with_data(self): 53 | d = {'some_field': '1'} 54 | v = TestVertex(d) 55 | data = v.data 56 | 57 | for k, v in d.items(): 58 | self.assertIn(k, data) 59 | self.assertEqual(1, len(data[k])) 60 | self.assertEqual(v, data[k][0]['value']) 61 | 62 | def test_can_create_edge(self): 63 | e = TestEdge() 64 | 65 | self.assertTrue(isinstance(e, Edge)) 66 | self.assertEqual(e['label'], camel_to_underscore(e.__class__.__name__)) 67 | 68 | def test_can_create_edge_with_data(self): 69 | d = {'some_field': '1'} 70 | e = TestEdge(d) 71 | data = e.data 72 | 73 | for k, v in d.items(): 74 | self.assertIn(k, data) 75 | self.assertEqual(1, len(data[k])) 76 | self.assertEqual(v, data[k][0]['value']) 77 | 78 | def test_can_add_undefined_field_to_undefied_vertex(self): 79 | d = {'one': 1, 'two': 2, 'three': 3} 80 | v = TestUndefinedVertex(d) 81 | data = v.data 82 | 83 | for k, v in d.items(): 84 | self.assertIn(k, data) 85 | self.assertEqual(v, data[k][0]['value']) 86 | 87 | def test_can_add_undefined_field_to_undefied_edge(self): 88 | d = {'one': 1, 'two': 2, 'three': 3} 89 | v = TestUndefinedEdge(d) 90 | data = v.data 91 | 92 | for k, v in d.items(): 93 | self.assertIn(k, data) 94 | self.assertEqual(v, data[k][0]['value']) 95 | 96 | def test_can_create_with_and_without_label_override(self): 97 | custom = 'custom_name' + str(random()) 98 | 99 | class CustomV(Vertex): 100 | label = custom 101 | 102 | class V(Vertex): 103 | pass 104 | 105 | custom_v = CustomV() 106 | v = V() 107 | name = camel_to_underscore(v.__class__.__name__) 108 | 109 | self.assertEqual(custom, custom_v[GIZMO_LABEL[0]]) 110 | self.assertEqual(name, v[GIZMO_LABEL[0]]) 111 | 112 | def test_can_get_rep_of_entity(self): 113 | d = {'id': '1'} 114 | v = TestVertex(d) 115 | entity, id = v.get_rep() 116 | 117 | self.assertEqual('V', entity) 118 | self.assertEqual(d['id'], id) 119 | 120 | def test_can_change_datatype_for_entity(self): 121 | v = TestVertex() 122 | t = 'graph' 123 | 124 | v.data_type = t 125 | 126 | self.assertEqual(t, v.data_type) 127 | 128 | def test_can_add_undefined_fields(self): 129 | v = TestUndefinedVertex() 130 | fields = v.fields 131 | dic = {'name': str(random())} 132 | flo = random() 133 | inte = int(random()) 134 | li = ['1', '2', '3'] 135 | s = str(random()) 136 | d = { 137 | 'dic': dic, 138 | 'flo': flo, 139 | 'inte': inte, 140 | 'li': li, 141 | 's': s, 142 | } 143 | v.hydrate(d) 144 | 145 | self.assertIsInstance(fields['dic'], Map) 146 | self.assertIsInstance(fields['flo'], Float) 147 | self.assertIsInstance(fields['inte'], Integer) 148 | self.assertIsInstance(fields['li'], List) 149 | self.assertIsInstance(fields['s'], String) 150 | 151 | def test_can_get_undefiend_field(self): 152 | v = TestUndefinedVertex() 153 | n = 'name' 154 | val = v[n] 155 | 156 | self.assertIsInstance(v.fields[n], String) 157 | 158 | def test_can_create_entities_that_can_subclass_entities(self): 159 | class Base(Vertex): 160 | base_field = String() 161 | name = String() 162 | 163 | class Sub(Base): 164 | sub_field = String() 165 | name = Integer() 166 | 167 | init = { 168 | 'sub_field': str(random()), 169 | 'base_field': str(random()), 170 | 'name': str(random()) 171 | } 172 | ins = Sub(init) 173 | data = ins.data 174 | 175 | self.assertIn('base_field', data) 176 | self.assertIn('sub_field', data) 177 | self.assertIn('name', data) 178 | self.assertIsInstance(ins.fields['name'], Integer) 179 | 180 | class SubTwo(Sub): 181 | sub_two_field = String() 182 | name = Boolean() 183 | 184 | init['sub_two_field'] = str(random()) 185 | ins = SubTwo(init) 186 | data = ins.data 187 | self.assertIn('base_field', data) 188 | self.assertIn('sub_field', data) 189 | self.assertIn('name', data) 190 | self.assertIn('sub_two_field', data) 191 | self.assertIsInstance(ins.fields['name'], Boolean) 192 | 193 | class SubThree(Sub): 194 | sub_three_field = String() 195 | name = Float() 196 | 197 | class Diamond(SubTwo, SubThree): 198 | diamon_field = String() 199 | 200 | init['diamon_field'] = str(random()) 201 | init['sub_three_field'] = str(random()) 202 | ins = Diamond(init) 203 | data = ins.data 204 | 205 | self.assertIn('base_field', data) 206 | self.assertIn('sub_field', data) 207 | self.assertIn('name', data) 208 | self.assertIn('sub_two_field', data) 209 | self.assertIn('sub_three_field', data) 210 | self.assertIn('diamon_field', data) 211 | self.assertIsInstance(ins.fields['name'], String) 212 | 213 | def test_can_create_fields_from_json_gremlin_response(self): 214 | j = '{"requestId":"cce2b0ff-10ff-472f-847e-35c5efdd813a","status":{"message":"","code":200,"attributes":{}},"result":{"data":[{"id":4,"label":"vertex","type":"vertex","properties":{"__GIZMO_ENTITY__":[{"id":31,"value":"gizmo.test.mapper.TestVertex"}],"name":[{"id":34,"value":"mark","properties":{"age":35}}],"id":[{"id":32,"value":"0.28441421794883837"}],"type":[{"id":33,"value":"vertex"}]}}],"meta":{}}}' 215 | j = json.loads(j) 216 | data = j['result']['data'][0]['properties'] 217 | v = TestUndefinedVertex(data=data) 218 | v_data = v.data 219 | 220 | # plus one for the label 221 | self.assertEqual(len(data) + 1, len(v_data)) 222 | 223 | for k, v in data.items(): 224 | self.assertIn(k, v_data) 225 | if k != 'name': 226 | self.assertEqual(v[0]['value'], v_data[k]) 227 | else: 228 | self.assertEqual(v[0]['value'], v_data[k][0]['value']) 229 | self.assertEqual(v[0]['properties'], v_data[k][0]['properties']) 230 | 231 | 232 | if __name__ == '__main__': 233 | unittest.main() 234 | -------------------------------------------------------------------------------- /gizmo/test/field.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import json 3 | import time 4 | 5 | from datetime import datetime 6 | from random import randrange, random, choice, randint 7 | from pprint import pprint 8 | 9 | from gizmo.field import * 10 | 11 | 12 | class FieldTests(unittest.TestCase): 13 | 14 | def test_can_create_field_without_value(self): 15 | f = Field() 16 | 17 | self.assertEqual(type(f), Field) 18 | self.assertEqual(0, len(f.values)) 19 | 20 | def test_can_create_field_without_value_with_default_value(self): 21 | d = str(random()) 22 | f = Field(default=d) 23 | 24 | self.assertEqual(type(f), Field) 25 | self.assertEqual(1, len(f.values)) 26 | 27 | def test_can_create_field_with_one_value(self): 28 | v = str(random()) 29 | f = Field(values=v) 30 | 31 | self.assertEqual(type(f), Field) 32 | self.assertEqual(1, len(f.values)) 33 | self.assertIn(v, f.values) 34 | 35 | def test_can_create_field_with_one_callable_value(self): 36 | v = str(random()) 37 | def value(): 38 | return v 39 | 40 | f = Field(values=value) 41 | 42 | self.assertEqual(type(f), Field) 43 | self.assertEqual(1, len(f.values)) 44 | self.assertIn(v, f.values) 45 | 46 | def test_can_create_field_without_value_with_default_callable_value(self): 47 | d = str(random()) 48 | def value(): 49 | return d 50 | 51 | f = Field(default=d) 52 | 53 | self.assertEqual(type(f), Field) 54 | self.assertEqual(1, len(f.values)) 55 | 56 | def test_can_create_field_with_two_values_in_list(self): 57 | v = [str(random()), str(random())] 58 | f = Field(values=v) 59 | 60 | self.assertEqual(type(f), Field) 61 | self.assertEqual(2, len(f.values)) 62 | self.assertIn(v[0], f.values) 63 | self.assertIn(v[1], f.values) 64 | 65 | def test_can_create_field_with_two_values_in_list_one_callable(self): 66 | val = str(random()) 67 | 68 | def value(): 69 | return val 70 | 71 | v = [str(random()), value] 72 | f = Field(values=v) 73 | 74 | self.assertEqual(type(f), Field) 75 | self.assertEqual(2, len(f.values)) 76 | self.assertIn(v[0], f.values) 77 | self.assertIn(val, f.values) 78 | 79 | def test_can_add_value_to_field(self): 80 | f = Field() 81 | v = str(random()) 82 | f + v 83 | 84 | self.assertEqual(1, len(f.values)) 85 | self.assertIn(v, f.values) 86 | 87 | def test_can_add_callable_value_to_field(self): 88 | f = Field() 89 | v = str(random()) 90 | 91 | def value(): 92 | return v 93 | 94 | f + value 95 | 96 | self.assertEqual(1, len(f.values)) 97 | self.assertIn(v, f.values) 98 | 99 | def test_can_add_multiple_values_to_field(self): 100 | f = Field() 101 | v = str(random()) 102 | d = str(random()) 103 | f + v + d 104 | 105 | self.assertEqual(2, len(f.values)) 106 | self.assertIn(v, f.values) 107 | self.assertIn(d, f.values) 108 | 109 | def test_can_add_multiple_callable_values_to_field(self): 110 | f = Field() 111 | v = str(random()) 112 | d = str(random()) 113 | 114 | def value(): 115 | return v 116 | 117 | def value2(): 118 | return d 119 | 120 | f + value + value2 121 | 122 | self.assertEqual(2, len(f.values)) 123 | self.assertIn(v, f.values) 124 | self.assertIn(d, f.values) 125 | 126 | def test_can_add_multiple_mixed_regular_and_callable_values_to_field(self): 127 | f = Field() 128 | v = str(random()) 129 | d = str(random()) 130 | 131 | def value(): 132 | return v 133 | 134 | f + value + d 135 | 136 | self.assertEqual(2, len(f.values)) 137 | self.assertIn(v, f.values) 138 | self.assertIn(d, f.values) 139 | 140 | def test_can_add_multiple_same_value_to_field(self): 141 | f = Field() 142 | v = str(random()) 143 | f + v + v 144 | 145 | self.assertEqual(2, len(f.values)) 146 | self.assertIn(v, f.values) 147 | 148 | def test_can_add_multiple_same_callable_value_to_field(self): 149 | f = Field() 150 | v = str(random()) 151 | 152 | def value(): 153 | return v 154 | 155 | f + value + value 156 | 157 | self.assertEqual(2, len(f.values)) 158 | self.assertIn(v, f.values) 159 | 160 | def test_can_limit_how_many_values_can_be_added_to_field(self): 161 | mv = 6 162 | r = 15 163 | f = Field(max_values=mv) 164 | 165 | for i in range(r): 166 | f + i 167 | 168 | values = f.values 169 | 170 | self.assertEqual(mv, len(values)) 171 | self.assertTrue(values[-1] < mv) 172 | 173 | def test_can_overwrite_the_last_value_when_limiting_number_of_values(self): 174 | mv = 6 175 | r = 15 176 | f = Field(max_values=mv, overwrite_last_value=True) 177 | 178 | for i in range(r): 179 | f + i 180 | 181 | values = f.values 182 | 183 | self.assertEqual(mv, len(values)) 184 | self.assertTrue(values[-1] == r - 1) 185 | 186 | def test_can_overwrite_the_last_value_when_limiting_number_of_values_to_one(self): 187 | mv = 1 188 | r = 15 189 | f = Field(max_values=mv, overwrite_last_value=True) 190 | 191 | for i in range(r): 192 | f + i 193 | 194 | values = f.values 195 | 196 | self.assertEqual(mv, len(values)) 197 | self.assertTrue(values[-1] == r - 1) 198 | 199 | def test_can_create_field_with_value_and_properties(self): 200 | iv = 'initail_value_' + str(random()) 201 | pk = 'property_key' + str(random()) 202 | pv = 'property_value' + str(random()) 203 | initial = { 204 | 'value': iv, 205 | 'properties': { 206 | pk: pv, 207 | } 208 | } 209 | f = Field(values=initial) 210 | data = f.data 211 | 212 | self.assertEqual(1, len(data)) 213 | 214 | res = data[0] 215 | 216 | self.assertIn('value', res) 217 | self.assertEqual(iv, res['value']) 218 | self.assertIn('properties', res) 219 | self.assertIn(pk, res['properties']) 220 | self.assertEqual(pv, res['properties'][pk]) 221 | 222 | def test_can_create_field_with_multiple_values_and_properties(self): 223 | iv = 'initail_value_' + str(random()) 224 | iv2 = '2initail_value_' + str(random()) 225 | pk = 'property_key' + str(random()) 226 | pv = 'property_value' + str(random()) 227 | pk2 = 'property_key' + str(random()) 228 | pv2 = 'property_value' + str(random()) 229 | initial = [ 230 | { 231 | 'value': iv, 232 | 'properties': { 233 | pk: pv, 234 | } 235 | }, 236 | { 237 | 'value': iv2, 238 | 'properties': { 239 | pk2: pv2, 240 | } 241 | } 242 | ] 243 | f = Field(values=initial) 244 | data = f.data 245 | self.assertEqual(2, len(data)) 246 | 247 | res = data[0] 248 | res2 = data[1] 249 | 250 | self.assertIn('value', res) 251 | self.assertEqual(iv, res['value']) 252 | self.assertIn('properties', res) 253 | self.assertIn(pk, res['properties']) 254 | self.assertEqual(pv, res['properties'][pk]) 255 | 256 | self.assertIn('value', res2) 257 | self.assertEqual(iv2, res2['value']) 258 | self.assertIn('properties', res2) 259 | self.assertIn(pk2, res2['properties']) 260 | self.assertEqual(pv2, res2['properties'][pk2]) 261 | 262 | def test_can_create_multiple_fields_with_value_and_properties(self): 263 | iv = 'initail_value_' + str(random()) 264 | pk = 'property_key' + str(random()) 265 | pv = 'property_value' + str(random()) 266 | iv2 = '2initail_value_' + str(random()) 267 | pk2 = '2property_key' + str(random()) 268 | pv2 = '2property_value' + str(random()) 269 | value1 = { 270 | 'value': iv, 271 | 'properties': { 272 | pk: pv, 273 | } 274 | } 275 | value2 = { 276 | 'value': iv2, 277 | 'properties': { 278 | pk2: pv2, 279 | } 280 | } 281 | initial = [value1, value2,] 282 | f = Field(values=initial) 283 | data = f.data 284 | 285 | self.assertEqual(2, len(data)) 286 | 287 | for res in data: 288 | self.assertIn('value', res) 289 | self.assertIn('properties', res) 290 | self.assertIn(res, initial) 291 | 292 | def test_can_add_property_to_existing_value(self): 293 | f = Field() 294 | v = 'name' 295 | p = str(random()) 296 | prop = 'prop' 297 | f + v 298 | 299 | f[v].properties[prop] = p 300 | 301 | data = f.data 302 | 303 | self.assertEqual(1, len(data)) 304 | self.assertIn('properties', data[0]) 305 | self.assertIn(prop, data[0]['properties']) 306 | self.assertEqual(p, data[0]['properties'][prop]) 307 | 308 | def test_can_add_property_to_nonexisting_value(self): 309 | f = Field() 310 | v = 'name' 311 | p = str(random()) 312 | prop = 'prop' + str(random()) 313 | 314 | f[v].properties[prop] = p 315 | 316 | data = f.data 317 | 318 | self.assertEqual(1, len(data)) 319 | self.assertIn('properties', data[0]) 320 | self.assertIn(prop, data[0]['properties']) 321 | self.assertEqual(p, data[0]['properties'][prop]) 322 | 323 | def test_can_add_multiple_values_with_unique_properties(self): 324 | f = Field() 325 | v = 'v' + str(random()) 326 | v2 = 'v2' + str(random()) 327 | p = str(random()) 328 | f + v + v2 329 | 330 | vprops = f[v].properties 331 | v2props = f[v2].properties 332 | data = f.data 333 | 334 | self.assertEqual(2, len(data)) 335 | self.assertNotEqual(vprops, v2props) 336 | 337 | for d in data: 338 | self.assertIn('properties', d) 339 | 340 | def test_can_add_multiple_values_and_set_properties_on_all(self): 341 | f = Field() 342 | vf = 'vf' + str(random()) 343 | v = 'v' + str(random()) 344 | v2 = 'v2' + str(random()) 345 | p = str(random()) 346 | f + v + v2 347 | 348 | f.properties[vf] = v 349 | data = f.data 350 | properties = f.properties.data 351 | 352 | self.assertEqual(2, len(f.values)) 353 | self.assertEqual(2, len(properties)) 354 | self.assertNotEqual(id(properties[0]), id(properties[1])) 355 | 356 | for prop in properties: 357 | self.assertIn(vf, prop) 358 | self.assertEqual(v, prop[vf]) 359 | 360 | def test_can_add_multiple_values_and_set_properties_on_all_and_some_on_some(self): 361 | f = Field() 362 | vf = 'vf' + str(random()) 363 | v = 'v' + str(random()) 364 | v2 = 'v2' + str(random()) 365 | only = 'only' + str(random()) 366 | onlyprop = 'prop' + str(random()) 367 | p = str(random()) 368 | f + v + v2 369 | 370 | f.properties[vf] = v 371 | f[v2].properties[onlyprop] = only 372 | data = f.data 373 | all_properties = f.properties.data 374 | v_properties = f[v].properties.data 375 | v2_properties = f[v2].properties.data 376 | 377 | self.assertEqual(2, len(f.values)) 378 | self.assertEqual(2, len(all_properties)) 379 | self.assertEqual(1, len(v_properties)) 380 | self.assertEqual(1, len(v2_properties)) 381 | self.assertNotEqual(id(v_properties), id(v2_properties)) 382 | self.assertNotEqual(len(v_properties[0]), len(v2_properties[0])) 383 | self.assertIn(vf, v_properties[0]) 384 | self.assertEqual(v, v_properties[0][vf]) 385 | self.assertIn(vf, v2_properties[0]) 386 | self.assertEqual(v, v2_properties[0][vf]) 387 | self.assertNotIn(onlyprop, v_properties[0]) 388 | self.assertIn(onlyprop, v2_properties[0]) 389 | self.assertEqual(only, v2_properties[0][onlyprop]) 390 | 391 | def test_can_delete_value(self): 392 | f = Field() 393 | v = str(random()) 394 | f + v 395 | data = f.data 396 | 397 | self.assertEqual(1, len(data)) 398 | 399 | del f[v] 400 | 401 | data = f.data 402 | 403 | self.assertEqual(0, len(data)) 404 | 405 | def test_can_delete_values_via_empty(self): 406 | f = Field() 407 | v = str(random()) 408 | f + v 409 | data = f.data 410 | 411 | self.assertEqual(1, len(data)) 412 | f.empty() 413 | 414 | data = f.data 415 | 416 | self.assertEqual(0, len(data)) 417 | 418 | def test_can_get_changes_to_single_value(self): 419 | v = 'initial' 420 | f = Field(v) 421 | changed = 'changed'+ str(random()) 422 | f[v] = changed 423 | changes = f.changes['values'] 424 | 425 | self.assertIn('changes', changes) 426 | self.assertEqual(1, len(changes['changes'])) 427 | self.assertIn('value', changes['changes'][0]) 428 | self.assertIn('from', changes['changes'][0]['value']) 429 | self.assertIn('to', changes['changes'][0]['value']) 430 | self.assertEqual(v, changes['changes'][0]['value']['from']) 431 | self.assertEqual(changed, changes['changes'][0]['value']['to']) 432 | 433 | def test_can_get_changes_to_multiple_values(self): 434 | v = ['initial_one' + str(random()), 'initial_two' + str(random())] 435 | f = Field(values=v) 436 | changed_zero = 'changed_0_'+ str(random()) 437 | changed_one = 'changed_1_'+ str(random()) 438 | values = [(v[0], changed_zero), (v[1], changed_one)] 439 | f[v[0]] = changed_zero 440 | f[v[1]] = changed_one 441 | changes = f.changes 442 | 443 | self.assertIn('changes', changes['values']) 444 | self.assertEqual(2, len(changes['values']['changes'])) 445 | 446 | for change in changes['values']['changes']: 447 | test = (change['value']['from'], change['value']['to']) 448 | self.assertIn(test, values) 449 | 450 | def test_can_get_values_added_to_field_via_changes(self): 451 | f = Field() 452 | v1 = str(random()) 453 | v2 = str(random()) 454 | both = [v1, v2,] 455 | f + v1 + v2 456 | 457 | changes = f.changes['values'] 458 | 459 | self.assertIn('added', changes) 460 | self.assertTrue(2, len(changes['added'])) 461 | 462 | for add in changes['added']: 463 | self.assertIn(add['value'], both) 464 | 465 | def test_can_get_values_added_to_single_value_with_no_value_but_default_value(self): 466 | d = str(random()) 467 | f = Field(default=d) 468 | 469 | changes = f.changes['values'] 470 | 471 | self.assertIn('added', changes) 472 | self.assertTrue(1, len(changes['added'])) 473 | 474 | for add in changes['added']: 475 | self.assertIn(add['value'], d) 476 | 477 | def test_can_get_values_added_to_single_value_with_no_value_but_default_value_is_callable(self): 478 | v = str(random()) 479 | 480 | def call(): 481 | nonlocal v 482 | return v 483 | 484 | f = Field(default=call) 485 | 486 | changes = f.changes['values'] 487 | 488 | self.assertIn('added', changes) 489 | self.assertTrue(1, len(changes['added'])) 490 | 491 | for add in changes['added']: 492 | self.assertIn(add['value'], v) 493 | 494 | def test_can_add_value_to_field_with_default_and_defalut_not_show(self): 495 | d = str(random()) 496 | f = Field(default=d) 497 | v1 = str(random()) 498 | v2 = str(random()) 499 | both = [v1, v2,] 500 | f + v1 + v2 501 | changes = f.changes['values'] 502 | 503 | self.assertIn('added', changes) 504 | self.assertTrue(2, len(changes['added'])) 505 | 506 | for add in changes['added']: 507 | self.assertIn(add['value'], both) 508 | 509 | def test_can_get_changes_after_adding_value_and_changing_value(self): 510 | i = 'initial_' + str(random()) 511 | f = Field(values=i) 512 | v = random() 513 | change = 'changed_' + str(random()) 514 | f + v 515 | f[i] = change 516 | changes = f.changes['values'] 517 | 518 | self.assertTrue(2, len(changes)) 519 | self.assertIn('added', changes) 520 | self.assertIn('changes', changes) 521 | self.assertTrue(1, len(changes['added'])) 522 | self.assertTrue(1, len(changes['changes'])) 523 | self.assertEqual(v, changes['added'][0]['value']) 524 | self.assertEqual(i, changes['changes'][0]['value']['from']) 525 | self.assertEqual(change, changes['changes'][0]['value']['to']) 526 | 527 | def test_can_get_deleted_value_from_changes(self): 528 | i = 'initial_' + str(random()) 529 | f = Field(values=i) 530 | del f[i] 531 | changes = f.changes['values'] 532 | 533 | self.assertEqual(2, len(changes)) 534 | self.assertIn('deleted', changes) 535 | self.assertEqual(1, len(changes['deleted'])) 536 | self.assertEqual(i, changes['deleted'][0]['value']) 537 | 538 | def test_can_get_deleted_value_from_empty_from_changes(self): 539 | i = 'initial_' + str(random()) 540 | f = Field(values=i) 541 | f.empty() 542 | changes = f.changes['values'] 543 | 544 | self.assertEqual(2, len(changes)) 545 | self.assertIn('deleted', changes) 546 | self.assertEqual(1, len(changes['deleted'])) 547 | self.assertEqual(i, changes['deleted'][0]['value']) 548 | 549 | def test_can_add_value_and_delete_it_without_appearing_in_changes(self): 550 | f = Field() 551 | v = str(random()) 552 | f + v 553 | 554 | del f[v] 555 | 556 | changes = f.changes['values'] 557 | 558 | self.assertEqual(2, len(changes)) 559 | self.assertIn('deleted', changes) 560 | self.assertEqual(1, len(changes['deleted'])) 561 | self.assertEqual(v, changes['deleted'][0]['value']) 562 | 563 | def test_can_init_field_with_values_and_delete_one(self): 564 | v = 'strr_' + str(random()) 565 | t = random() 566 | vals = [v, t,] 567 | f = Field(values=vals) 568 | data = f.data 569 | 570 | self.assertTrue(2, len(data)) 571 | 572 | del f[t] 573 | 574 | changes = f.changes['values'] 575 | self.assertEqual(1, len(f.data)) 576 | self.assertEqual(2, len(changes)) 577 | self.assertIn('deleted', changes) 578 | self.assertEqual(1, len(changes['deleted'])) 579 | self.assertEqual(t, changes['deleted'][0]['value']) 580 | 581 | def test_can_init_field_with_values_and_delete_one_and_change_one(self): 582 | v = 'strr_' + str(random()) 583 | t = random() 584 | vals = [v, t,] 585 | f = Field(values=vals) 586 | upped = 'updated!!'+ str(random()) 587 | data = f.data 588 | 589 | self.assertTrue(2, len(data)) 590 | 591 | del f[t] 592 | 593 | f[v] = upped 594 | changes = f.changes['values'] 595 | 596 | self.assertEqual(1, len(f.data)) 597 | self.assertEqual(3, len(changes)) 598 | self.assertIn('changes', changes) 599 | self.assertEqual(1, len(changes['changes'])) 600 | self.assertEqual(v, changes['changes'][0]['value']['from']) 601 | self.assertEqual(upped, changes['changes'][0]['value']['to']) 602 | self.assertIn('deleted', changes) 603 | self.assertEqual(1, len(changes['deleted'])) 604 | self.assertEqual(t, changes['deleted'][0]['value']) 605 | 606 | def test_can_init_field_with_three_values_and_delete_one_and_change_one(self): 607 | v = 'strr_' + str(random()) 608 | t = random() 609 | x = 'third_'+ str(random()) 610 | vals = [v, t, x] 611 | f = Field(values=vals) 612 | upped = 'updated!!'+ str(random()) 613 | data = f.data 614 | 615 | self.assertTrue(2, len(data)) 616 | 617 | del f[t] 618 | 619 | f[v] = upped 620 | changes = f.changes['values'] 621 | 622 | self.assertEqual(2, len(f.data)) 623 | self.assertEqual(3, len(changes)) 624 | self.assertIn('changes', changes) 625 | self.assertEqual(1, len(changes['changes'])) 626 | self.assertEqual(v, changes['changes'][0]['value']['from']) 627 | self.assertEqual(upped, changes['changes'][0]['value']['to']) 628 | self.assertIn('deleted', changes) 629 | self.assertEqual(1, len(changes['deleted'])) 630 | self.assertEqual(t, changes['deleted'][0]['value']) 631 | 632 | def test_can_get_added_property_on_value(self): 633 | i = 'initial_'+ str(random()) 634 | f = Field(values=i) 635 | pk = 'key' + str(random()) 636 | pv = 'val' + str(random()) 637 | f[i].properties[pk] = pv 638 | 639 | changes = f.changes['values'] 640 | 641 | self.assertEqual(2, len(changes)) 642 | self.assertIn('changes', changes) 643 | self.assertEqual(1, len(changes['changes'])) 644 | self.assertIn('properties', changes['changes'][0]) 645 | self.assertIn(pk, changes['changes'][0]['properties']) 646 | self.assertEqual(pv, changes['changes'][0]['properties'][pk]) 647 | 648 | def test_can_get_changed_property_on_value(self): 649 | iv = 'initial_value_' + str(random()) 650 | ipk = 'property_key_' + str(random()) 651 | ipv = 'initial_prop_val_' + str(random()) 652 | up_pv = 'updated_prop_val_' + str(random()) 653 | initial = { 654 | 'value': iv, 655 | 'properties': { 656 | ipk: ipv, 657 | } 658 | } 659 | f = Field(values=initial) 660 | f[iv].properties[ipk] = up_pv 661 | changes = f.changes['values'] 662 | 663 | self.assertEqual(2, len(changes)) 664 | self.assertIn('changes', changes) 665 | 666 | changes = changes['changes'] 667 | 668 | self.assertEqual(1, len(changes)) 669 | 670 | changes = changes[0] 671 | 672 | self.assertIn('properties', changes) 673 | 674 | properties = changes['properties'] 675 | 676 | self.assertEqual(1, len(properties)) 677 | self.assertIn(up_pv, properties.values()) 678 | self.assertIn(ipk, properties.keys()) 679 | 680 | def test_can_add_properties_to_multiple_values(self): 681 | iv = 'initail_value_' + str(random()) 682 | pk = 'property_key' + str(random()) 683 | pv = 'property_value' + str(random()) 684 | iv2 = '2initail_value_' + str(random()) 685 | pk2 = '2property_key' + str(random()) 686 | pv2 = '2property_value' + str(random()) 687 | value1 = { 688 | 'value': iv, 689 | } 690 | value2 = { 691 | 'value': iv2, 692 | } 693 | prop = {pk: pv} 694 | initial = [value1, value2,] 695 | f = Field(values=initial) 696 | f.properties[pk] = pv 697 | data = f.data 698 | 699 | self.assertEqual(2, len(data)) 700 | 701 | for val in data: 702 | self.assertIn('properties', val) 703 | self.assertIn('value', val) 704 | self.assertEqual(val['properties'], prop) 705 | 706 | def test_can_add_properties_to_multiple_values_one_with_properties(self): 707 | iv = 'initail_value_' + str(random()) 708 | pk = 'property_key' + str(random()) 709 | pv = 'property_value' + str(random()) 710 | ipk = 'initial_prop_key_' + str(random()) 711 | ipv = 'initial_prop_val_' + str(random()) 712 | iv2 = '2initail_value_' + str(random()) 713 | pk2 = '2property_key' + str(random()) 714 | pv2 = '2property_value' + str(random()) 715 | value1 = { 716 | 'value': iv, 717 | 'properties': { 718 | ipk: ipv, 719 | } 720 | } 721 | value2 = { 722 | 'value': iv2, 723 | } 724 | initial = [value1, value2,] 725 | f = Field(values=initial) 726 | f.properties[pk] = pv 727 | data = f.data 728 | 729 | self.assertEqual(2, len(data)) 730 | 731 | for val in data: 732 | self.assertIn('properties', val) 733 | self.assertIn('value', val) 734 | 735 | properties = val['properties'] 736 | 737 | if ipk in properties: 738 | self.assertEqual(2, len(properties)) 739 | self.assertEqual(ipv, properties[ipk]) 740 | else: 741 | self.assertEqual(1, len(properties)) 742 | 743 | self.assertIn(pk, properties) 744 | self.assertEqual(pv, properties[pk]) 745 | 746 | 747 | 748 | class StringTests(unittest.TestCase): 749 | 750 | def test_can_create_string_without_value_and_python_type_ret_empty_string(self): 751 | f = String() 752 | 753 | self.assertEqual(0, len(f.values)) 754 | 755 | def test_can_create_string_with_numeric_value_ret_string_for_gremlin(self): 756 | v = random() 757 | f = String(values=v, data_type='graph') 758 | data = f.data 759 | 760 | self.assertIsInstance(f.values[0], str) 761 | 762 | def test_can_create_string_with_numeric_value_ret_str_value(self): 763 | v = random() 764 | f = String(values=v) 765 | 766 | self.assertIsInstance(f.values[0], str) 767 | 768 | def test_will_ensure_that_none_values_return_empty_string_when_converted_to_python(self): 769 | f = String() 770 | f += None 771 | f.data_type = 'python' 772 | 773 | self.assertEqual(f.values[0], '') 774 | 775 | def test_will_ensure_that_none_values_return_empty_string_when_converted_to_graph(self): 776 | f = String() 777 | f += None 778 | f.data_type = 'graph' 779 | 780 | self.assertEqual(f.values[0], '') 781 | 782 | 783 | class IntegerTests(unittest.TestCase): 784 | 785 | def test_can_create_type_with_non_numeric_value_and_get_integer_python(self): 786 | 787 | class X: 788 | pass 789 | 790 | v = ['43.34.', X(), 'iii', '987eee'] 791 | f = Integer(values=choice(v)) 792 | values = f.values 793 | 794 | self.assertIsInstance(values[0], int) 795 | self.assertEqual(values[0], 0) 796 | 797 | def test_can_create_type_with_non_numeric_value_and_get_integer_gremlin(self): 798 | 799 | class X: 800 | pass 801 | 802 | v = ['43.34.', X(), 'iii', '987eee'] 803 | f = Integer(values=choice(v), data_type='graph') 804 | values = f.values 805 | 806 | self.assertIsInstance(values[0], int) 807 | self.assertEqual(values[0], 0) 808 | 809 | def test_will_ensure_that_none_values_return_zero_when_converted_to_python(self): 810 | f = Integer() 811 | f += None 812 | f.data_type = 'python' 813 | 814 | self.assertEqual(f.values[0], 0) 815 | 816 | def test_will_ensure_that_none_values_return_zero_when_converted_to_graph(self): 817 | f = Integer() 818 | f += None 819 | f.data_type = 'graph' 820 | 821 | self.assertEqual(f.values[0], 0) 822 | 823 | 824 | class FloatTests(unittest.TestCase): 825 | 826 | def test_can_get_float_with_non_numeric_value_graph_data_type(self): 827 | v = 'dsafsd$@#4..' 828 | f = Float(values=v) 829 | f.data_type = 'graph' 830 | 831 | self.assertIsInstance(f.values[0], float) 832 | self.assertEqual(f.values[0], 0.0) 833 | 834 | def test_can_convert_integer_to_float(self): 835 | v = 12 836 | f = Float(values=v) 837 | 838 | self.assertIsInstance(f.values[0], float) 839 | self.assertEqual(f.values[0], 12.0) 840 | 841 | def test_can_convert_integer_to_float_graph_data_type(self): 842 | v = 12 843 | f = Float(values=v) 844 | f.data_type = 'graph' 845 | 846 | self.assertIsInstance(f.values[0], float) 847 | self.assertEqual(f.values[0], 12.0) 848 | 849 | def test_will_ensure_that_none_values_return_zero_when_converted_to_python(self): 850 | f = Float() 851 | f += None 852 | f.data_type = 'python' 853 | 854 | self.assertEqual(f.values[0], 0.0) 855 | 856 | def test_will_ensure_that_none_values_return_zero_when_converted_to_graph(self): 857 | f = Float() 858 | f += None 859 | f.data_type = 'graph' 860 | 861 | self.assertEqual(f.values[0], 0.0) 862 | 863 | 864 | class DateTimeTests(unittest.TestCase): 865 | 866 | def test_can_create_empty_datetime_field(self): 867 | f = DateTime() 868 | 869 | self.assertEqual(0, len(f.values)) 870 | 871 | def test_can_create_datetime_with_datetime_instance(self): 872 | d = datetime.today() 873 | f = DateTime(values=d) 874 | 875 | self.assertEqual(1, len(f.values)) 876 | self.assertIsInstance(f.values[0], float) 877 | self.assertEqual(f.values[0], d.timestamp()) 878 | 879 | def test_can_create_datetime_with_number(self): 880 | v = random() 881 | f = DateTime(values=v) 882 | 883 | self.assertEqual(1, len(f.values)) 884 | self.assertIsInstance(f.values[0], float) 885 | self.assertEqual(f.values[0], v) 886 | 887 | def test_will_ensure_that_none_values_return_zero_when_converted_to_python(self): 888 | f = DateTime() 889 | f += None 890 | f.data_type = 'python' 891 | 892 | self.assertEqual(f.values[0], 0.0) 893 | 894 | def test_will_ensure_that_none_values_return_zero_when_converted_to_graph(self): 895 | f = DateTime() 896 | f += None 897 | f.data_type = 'graph' 898 | 899 | self.assertEqual(f.values[0], 0.0) 900 | 901 | 902 | class TimeStampTests(unittest.TestCase): 903 | 904 | def test_can_create_timestamp(self): 905 | d = datetime.now() 906 | f = TimeStamp() 907 | 908 | time.sleep(2.0) 909 | 910 | self.assertEqual(1, len(f.values)) 911 | self.assertIsInstance(f.values[0], float) 912 | self.assertNotEqual(d.timestamp(), f.values[0]) 913 | self.assertTrue(f.values[0] > d.timestamp()) 914 | 915 | 916 | class IncrementTests(unittest.TestCase): 917 | 918 | def test_can_create_increment_without_default_value(self): 919 | f = Increment() 920 | 921 | self.assertEqual(1, len(f.values)) 922 | self.assertEqual(0, f.values[0]) 923 | 924 | def test_can_set_default_value(self): 925 | d = 9 926 | f = Increment(values=d) 927 | v = f.values[0] 928 | 929 | self.assertEqual(v, d) 930 | 931 | def test_will_only_increment_when_data_type_is_graph(self): 932 | f = Increment() 933 | f.data_type = 'graph' 934 | v = f.values[0] 935 | 936 | self.assertEqual(v, 1) 937 | self.assertEqual(1, len(f.values)) 938 | 939 | def test_will_increment_when_data_type_is_graph_with_default_value(self): 940 | d = 9 941 | f = Increment(values=d) 942 | f.data_type = 'graph' 943 | v = f.values[0] 944 | 945 | self.assertEqual(v, d + 1) 946 | 947 | def test_will_increment_multiple_times(self): 948 | d = v = 9 949 | l = choice(range(4, 15)) 950 | f = Increment(values=d) 951 | f.data_type = 'graph' 952 | 953 | for _ in range(l): 954 | v = f.values[0] 955 | 956 | self.assertEqual(v, d + l) 957 | 958 | 959 | class BooleanTests(unittest.TestCase): 960 | 961 | def test_can_create_boolean_without_value_and_get_false(self): 962 | f = Boolean() 963 | 964 | self.assertIsInstance(f.values[0], bool) 965 | self.assertFalse(f.values[0]) 966 | 967 | def test_can_create_boolean_without_value_and_get_false_for_graph(self): 968 | f = Boolean(data_type='graph') 969 | 970 | self.assertIsInstance(f.values[0], bool) 971 | self.assertFalse(f.values[0]) 972 | 973 | def test_can_get_boolean_from_non_bool_val(self): 974 | f = Boolean(values='ooo') 975 | 976 | self.assertIsInstance(f.values[0], bool) 977 | self.assertTrue(f.values[0]) 978 | 979 | def test_can_get_boolean_from_none_graph_data_type(self): 980 | f = Boolean() 981 | f.data_type = 'graph' 982 | 983 | self.assertFalse(f.values[0]) 984 | 985 | def test_can_get_boolean_value_from_boolean_strings(self): 986 | f = Boolean(values='true') 987 | 988 | self.assertIsInstance(f.values[0], bool) 989 | self.assertTrue(f.values[0]) 990 | 991 | f = Boolean(values='false') 992 | 993 | self.assertIsInstance(f.values[0], bool) 994 | self.assertFalse(f.values[0]) 995 | 996 | def test_can_get_graph_boolean_from_non_bool_val(self): 997 | f = Boolean(values='ooo') 998 | f.data_type = 'graph' 999 | 1000 | self.assertTrue(f.values[0]) 1001 | 1002 | def test_will_ensure_that_none_values_return_false_when_converted_to_python(self): 1003 | f = Boolean() 1004 | f += None 1005 | f.data_type = 'python' 1006 | 1007 | self.assertFalse(f.values[0]) 1008 | 1009 | def test_will_ensure_that_none_values_return_false_when_converted_to_graph(self): 1010 | f = Boolean() 1011 | f += None 1012 | f.data_type = 'graph' 1013 | 1014 | self.assertFalse(f.values[0]) 1015 | 1016 | 1017 | class MapTests(unittest.TestCase): 1018 | 1019 | def test_can_create_empty_dict_from_none(self): 1020 | f = Map() 1021 | 1022 | self.assertIsInstance(f.values[0], dict) 1023 | self.assertEqual(len(f.values[0]), 0) 1024 | 1025 | def test_can_create_empty_dict_from_none_for_graph_data_type(self): 1026 | f = Map() 1027 | f.data_type = 'graph' 1028 | 1029 | self.assertIsInstance(f.values[0], dict) 1030 | self.assertEqual(len(f.values[0]), 0) 1031 | 1032 | def test_can_get_dict_from_valid_json_object_literal(self): 1033 | ol = '{"name": "mark", "sex": "male", "loc": {"city": "here"}}' 1034 | j = json.loads(ol) 1035 | f = Map(values=j) 1036 | 1037 | self.assertIsInstance(f.values[0], dict) 1038 | self.assertEqual(len(f.values[0]), len(j)) 1039 | 1040 | def test_can_get_dict_from_valid_empty_json_object_literal(self): 1041 | ol = '{}' 1042 | j = json.loads(ol) 1043 | f = Map(values=j) 1044 | 1045 | self.assertIsInstance(f.values[0], dict) 1046 | self.assertEqual(len(f.values[0]), len(j)) 1047 | 1048 | def test_will_ensure_that_none_values_return_dict_when_converted_to_python(self): 1049 | f = Map() 1050 | f += None 1051 | f.data_type = 'python' 1052 | 1053 | self.assertIsInstance(f.values[0], dict) 1054 | 1055 | def test_will_ensure_that_none_values_return_dict_when_converted_to_graph(self): 1056 | f = Map() 1057 | f += None 1058 | f.data_type = 'graph' 1059 | 1060 | self.assertIsInstance(f.values[0], dict) 1061 | 1062 | 1063 | class ListTests(unittest.TestCase): 1064 | 1065 | def test_can_get_empty_list_from_none(self): 1066 | f = List() 1067 | 1068 | self.assertIsInstance(f.values[0], list) 1069 | self.assertEqual(len(f.values[0]), 0) 1070 | 1071 | def test_can_get_empty_list_from_none_for_graph_data_type(self): 1072 | f = List() 1073 | 1074 | self.assertIsInstance(f.values[0], list) 1075 | self.assertEqual(len(f.values[0]), 0) 1076 | 1077 | def test_can_get_list_from_list(self): 1078 | ol = ["one", 1, 222, "three", "four", random()] 1079 | f = List(values=ol) 1080 | 1081 | self.assertIsInstance(f.values[0], list) 1082 | self.assertEqual(len(f.values[0]), len(ol)) 1083 | self.assertIn(ol, f.values) 1084 | 1085 | def test_can_get_list_from_valid_json_array(self): 1086 | ol = '["one", 1, 222, "three", "four"]' 1087 | j = json.loads(ol) 1088 | 1089 | f = List(values=j) 1090 | 1091 | self.assertIsInstance(f.values[0], list) 1092 | self.assertEqual(len(f.values[0]), len(j)) 1093 | 1094 | def test_can_get_dict_from_empty_json_array(self): 1095 | ol = '[]' 1096 | j = json.loads(ol) 1097 | f = List(values=j) 1098 | 1099 | self.assertIsInstance(f.values[0], list) 1100 | self.assertEqual(len(f.values[0]), len(j)) 1101 | 1102 | def test_can_get_list_from_valid_json_gremlin_response(self): 1103 | ol = """[ 1104 | { 1105 | "value" :["one", 1, 222, "three", "four"] 1106 | } 1107 | ]""" 1108 | j = json.loads(ol) 1109 | f = List(values=j) 1110 | 1111 | self.assertIsInstance(f.values[0], list) 1112 | self.assertEqual(len(f.values[0]), len(j[0]['value'])) 1113 | 1114 | def test_will_ensure_that_none_values_return_list_when_converted_to_python(self): 1115 | f = List() 1116 | f += None 1117 | f.data_type = 'python' 1118 | 1119 | self.assertIsInstance(f.values[0], list) 1120 | 1121 | def test_will_ensure_that_none_values_return_list_when_converted_to_graph(self): 1122 | f = List() 1123 | f += None 1124 | f.data_type = 'graph' 1125 | 1126 | self.assertIsInstance(f.values[0], list) 1127 | 1128 | 1129 | class ImmutableFieldTests(unittest.TestCase): 1130 | 1131 | def test_can_set_a_value_and_return_only_one_value(self): 1132 | f = GremlinID() 1133 | v = str(random()) 1134 | f + v 1135 | data = f.data 1136 | 1137 | self.assertFalse(isinstance(data, (list, tuple))) 1138 | self.assertEqual(v, data) 1139 | 1140 | def test_can_set_value_and_return_changes_dict(self): 1141 | iv = 'initial' + str(random()) 1142 | f = GremlinID(values=iv) 1143 | v = str(random()) 1144 | f + v 1145 | changes = f.changes 1146 | 1147 | self.assertIn('values', changes) 1148 | self.assertIn('deleted', changes) 1149 | 1150 | values = changes['values'] 1151 | 1152 | self.assertIn('added', values) 1153 | 1154 | added = values['added'] 1155 | 1156 | self.assertEqual(1, len(added)) 1157 | self.assertEqual(v, added[0]['value']) 1158 | 1159 | 1160 | class OptionTests(unittest.TestCase): 1161 | 1162 | def test_can_add_allowed_value(self): 1163 | v = str(random()) 1164 | allowed = ['name', v] 1165 | f = Option(options=allowed) 1166 | f + v 1167 | 1168 | self.assertEqual(f.values[0], v) 1169 | self.assertEqual(1, len(f.values)) 1170 | 1171 | def test_can_add_allowed_value_when_attempting_to_add_more(self): 1172 | v = str(random()) 1173 | v2 = str(random()) 1174 | allowed = ['name', v] 1175 | f = Option(options=allowed) 1176 | f + v + v2 1177 | 1178 | self.assertEqual(f.values[0], v) 1179 | self.assertEqual(1, len(f.values)) 1180 | 1181 | def test_can_add_allowed_values(self): 1182 | v = str(random()) 1183 | v2 = str(random()) 1184 | allowed = ['name', v, v2] 1185 | f = Option(options=allowed) 1186 | f + v + v2 1187 | 1188 | self.assertEqual(f.values[0], v) 1189 | self.assertEqual(f.values[1], v2) 1190 | self.assertEqual(2, len(f.values)) 1191 | 1192 | def test_can_add_allowed_values_when_attempting_to_add_more(self): 1193 | v = str(random()) 1194 | v2 = str(random()) 1195 | v3 = str(random()) 1196 | allowed = ['name', v, v2] 1197 | f = Option(options=allowed) 1198 | f + v + v2 + v3 1199 | 1200 | self.assertEqual(f.values[0], v) 1201 | self.assertEqual(f.values[1], v2) 1202 | self.assertEqual(2, len(f.values)) 1203 | 1204 | def test_cannot_add_unallowed_value(self): 1205 | v = str(random()) 1206 | allowed = ['name'] 1207 | f = Option(options=allowed) 1208 | f + v 1209 | 1210 | self.assertNotIn(v, f.values) 1211 | self.assertEqual(0, len(f.values)) 1212 | 1213 | 1214 | class FieldManagerTests(unittest.TestCase): 1215 | 1216 | def test_can_create_a_field_manager_without_fields(self): 1217 | f = FieldManager() 1218 | data = f.data 1219 | 1220 | self.assertIsInstance(f, FieldManager) 1221 | self.assertEqual(0, len(data)) 1222 | 1223 | def test_can_create_a_field_manager_with_one_field(self): 1224 | string = String() 1225 | fields = {'string': string} 1226 | f = FieldManager(fields) 1227 | data = f.data 1228 | 1229 | self.assertIsInstance(f, FieldManager) 1230 | self.assertEqual(1, len(f.fields)) 1231 | self.assertEqual(0, len(data)) 1232 | 1233 | def test_can_create_a_field_manager_with_one_field_and_add_value(self): 1234 | string = String() 1235 | fields = {'string': string} 1236 | f = FieldManager(fields) 1237 | v = str(random()) 1238 | f['string'] = v 1239 | data = f.data 1240 | 1241 | self.assertIsInstance(f, FieldManager) 1242 | self.assertEqual(1, len(f.fields)) 1243 | self.assertEqual(1, len(data)) 1244 | 1245 | def test_can_create_a_field_manager_with_one_field_and_add_two_values(self): 1246 | string = String() 1247 | key = 'string' + str(random()) 1248 | fields = {key: string} 1249 | f = FieldManager(fields) 1250 | v = str(random()) 1251 | v2 = 'v2++++' + str(random()) 1252 | f[key] + v 1253 | f[key] + v2 1254 | data = f.data 1255 | 1256 | self.assertIsInstance(f, FieldManager) 1257 | self.assertEqual(1, len(f.fields)) 1258 | self.assertEqual(1, len(data)) 1259 | self.assertEqual(2, len(data[key])) 1260 | 1261 | def test_can_create_manager_with_no_fields_and_dynamically_add_string(self): 1262 | f = FieldManager(allow_undefined=True) 1263 | key = 'str_key' + str(random()) 1264 | val = 'str_val' + str(random()) 1265 | f[key] = val 1266 | 1267 | self.assertEqual(1, len(f.fields)) 1268 | self.assertIn(key, f.fields) 1269 | self.assertEqual(val, f.fields[key].values[0]) 1270 | self.assertIsInstance(f.fields[key], String) 1271 | 1272 | def test_can_create_manager_with_no_fields_and_dynamically_add_integer(self): 1273 | f = FieldManager(allow_undefined=True) 1274 | key = 'int_key' + str(random()) 1275 | val = randint(1, 1000) 1276 | f[key] = val 1277 | 1278 | self.assertEqual(1, len(f.fields)) 1279 | self.assertIn(key, f.fields) 1280 | self.assertEqual(val, f.fields[key].values[0]) 1281 | self.assertIsInstance(f.fields[key], Integer) 1282 | 1283 | def test_can_create_manager_with_no_fields_and_dynamically_add_float(self): 1284 | f = FieldManager(allow_undefined=True) 1285 | key = 'float_key' + str(random()) 1286 | val = random() 1287 | f[key] = val 1288 | 1289 | self.assertEqual(1, len(f.fields)) 1290 | self.assertIn(key, f.fields) 1291 | self.assertEqual(val, f.fields[key].values[0]) 1292 | self.assertIsInstance(f.fields[key], Float) 1293 | 1294 | def test_can_create_manager_with_no_fields_and_dynamically_add_boolean(self): 1295 | f = FieldManager(allow_undefined=True) 1296 | key = 'bool_key' + str(random()) 1297 | val = choice([True, False]) 1298 | f[key] = val 1299 | 1300 | self.assertEqual(1, len(f.fields)) 1301 | self.assertIn(key, f.fields) 1302 | self.assertEqual(val, f.fields[key].values[0]) 1303 | self.assertIsInstance(f.fields[key], Boolean) 1304 | 1305 | def test_can_create_manager_with_no_fields_and_dynamically_add_list(self): 1306 | f = FieldManager(allow_undefined=True) 1307 | key = 'list_key' + str(random()) 1308 | val = [random(), random(), random()] 1309 | f[key] = val 1310 | 1311 | self.assertEqual(1, len(f.fields)) 1312 | self.assertIn(key, f.fields) 1313 | self.assertEqual(val, f.fields[key].values[0]) 1314 | self.assertIsInstance(f.fields[key], List) 1315 | 1316 | def test_can_create_manager_with_no_fields_and_dynamically_add_map(self): 1317 | f = FieldManager(allow_undefined=True) 1318 | key = 'list_key' + str(random()) 1319 | val = {'1': random(), '2': random(), '3' + str(random()): random()} 1320 | f[key] = val 1321 | 1322 | self.assertEqual(1, len(f.fields)) 1323 | self.assertIn(key, f.fields) 1324 | self.assertEqual(val, f.fields[key].values[0]) 1325 | self.assertIsInstance(f.fields[key], Map) 1326 | 1327 | def test_can_get_all_values_from_manager(self): 1328 | f = FieldManager(allow_undefined=True) 1329 | k = 'key' + str(random()) 1330 | k2 = 'key2' + str(random()) 1331 | v = random() 1332 | v2 = random() 1333 | f[k] + v 1334 | f[k2] + v2 1335 | values = f.values 1336 | 1337 | self.assertEqual(2, len(values)) 1338 | self.assertIn(k, values) 1339 | self.assertIn(v, values[k]) 1340 | self.assertIn(k2, values) 1341 | self.assertIn(v2, values[k2]) 1342 | 1343 | def test_can_get_all_changes_to_initial_values(self): 1344 | ik = 'ik' + str(random()) 1345 | ik2 = 'ik2' + str(random()) 1346 | iv = 'iv' + str(random()) 1347 | iv2 = 'iv2' + str(random()) 1348 | cv = 'changed'+ str(random()) 1349 | cv2 = 'changed_2_'+ str(random()) 1350 | initial = { 1351 | 'value': iv 1352 | } 1353 | initial2 = { 1354 | 'value': iv2 1355 | } 1356 | string = String(values=initial) 1357 | string2 = String(values=initial2) 1358 | fields = {ik: string, ik2: string2} 1359 | f = FieldManager(fields=fields) 1360 | f[ik][iv] = cv 1361 | f[ik2][iv2] = cv2 1362 | changes = f.changes 1363 | 1364 | self.assertEqual(2, len(changes)) 1365 | self.assertIn(ik, changes) 1366 | self.assertEqual(2, len(changes[ik]['values'])) 1367 | self.assertEqual(iv, changes[ik]['values']['changes'][0]['value']['from']) 1368 | self.assertEqual(cv, changes[ik]['values']['changes'][0]['value']['to']) 1369 | self.assertEqual(2, len(changes[ik2]['values'])) 1370 | self.assertEqual(iv2, changes[ik2]['values']['changes'][0]['value']['from']) 1371 | self.assertEqual(cv2, changes[ik2]['values']['changes'][0]['value']['to']) 1372 | 1373 | def test_can_get_the_fields_that_were_removed(self): 1374 | fields = { 1375 | 'name': String(), 1376 | 'age': Integer(), 1377 | } 1378 | v = FieldManager(fields) 1379 | 1380 | del v['age'] 1381 | deleted = v.deleted 1382 | 1383 | self.assertEqual(len(deleted), 1) 1384 | self.assertEqual(len(v.changed), 2) 1385 | 1386 | def test_can_get_data_from_fields_including_immutable(self): 1387 | iid = str(random()) 1388 | iname = 'name_' + str(random()) 1389 | fields = { 1390 | 'id': GremlinID(values=iid), 1391 | 'name': String(values=iname), 1392 | } 1393 | f = FieldManager(fields=fields) 1394 | data = f.data 1395 | 1396 | self.assertEqual(2, len(data)) 1397 | self.assertIn('id', data) 1398 | self.assertIn('name', data) 1399 | 1400 | def test_can_get_data_from_fields_including_immutable_when_values_set_later(self): 1401 | iid = str(random()) 1402 | iname = 'name_' + str(random()) 1403 | fields = { 1404 | 'id': GremlinID(), 1405 | 'name': String(), 1406 | } 1407 | f = FieldManager(fields=fields) 1408 | f['id'] = iid 1409 | f['name'] = iname 1410 | data = f.data 1411 | 1412 | self.assertEqual(2, len(data)) 1413 | self.assertIn('id', data) 1414 | self.assertIn('name', data) 1415 | 1416 | def test_can_overwrite_field_values_when_directly_set(self): 1417 | iid = str(random()) 1418 | iname = 'name_' + str(random()) 1419 | fields = { 1420 | 'id': GremlinID(values=iid), 1421 | 'name': String(values=iname), 1422 | } 1423 | f = FieldManager(fields=fields) 1424 | data = f.data 1425 | 1426 | self.assertEqual(1, len(data['name'])) 1427 | self.assertEqual(iname, data['name'][0]['value']) 1428 | 1429 | updated = 'new_name' + str(random()) 1430 | f['name'] = updated 1431 | data = f.data 1432 | 1433 | self.assertEqual(1, len(data['name'])) 1434 | self.assertEqual(updated, data['name'][0]['value']) 1435 | 1436 | def test_can_add_value_to_field(self): 1437 | iid = str(random()) 1438 | iname = 'name_' + str(random()) 1439 | fields = { 1440 | 'id': GremlinID(values=iid), 1441 | 'name': String(values=iname), 1442 | } 1443 | f = FieldManager(fields=fields) 1444 | data = f.data 1445 | 1446 | self.assertEqual(1, len(data['name'])) 1447 | self.assertEqual(iname, data['name'][0]['value']) 1448 | 1449 | updated = 'new_name' + str(random()) 1450 | f['name'] + updated 1451 | data = f.data 1452 | both = [iname, updated] 1453 | 1454 | self.assertEqual(2, len(data['name'])) 1455 | 1456 | for val in data['name']: 1457 | self.assertIn(val['value'], both) 1458 | 1459 | 1460 | if __name__ == '__main__': 1461 | unittest.main() 1462 | -------------------------------------------------------------------------------- /gizmo/test/integration/__init__.py: -------------------------------------------------------------------------------- 1 | """This file holds all of the test cases for the integration tests 2 | """ 3 | import random 4 | 5 | from gizmo import Mapper, Request, Collection, Vertex, Edge 6 | from gizmo.exception import AstronomerConnectionException 7 | from gizmo.mapper import EntityMapper 8 | from gizmo.util import GIZMO_ENTITY, GIZMO_ID, GIZMO_LABEL 9 | 10 | 11 | class ConnectionTestCases(object): 12 | 13 | def test_cannot_create_connection(self): 14 | request = self.request.__class__(uri=self.request.uri, 15 | port=int(random.random()), three_two=self.request.three_two) 16 | 17 | async def test(): 18 | await request.send(script='some script') 19 | 20 | self.assertRaises(AstronomerConnectionException, 21 | self.ioloop.run_until_complete, test()) 22 | 23 | def test_can_establish_mapper(self): 24 | 25 | async def test(): 26 | await self.purge() 27 | c = '%s.V()' % self.gremlin.gv 28 | r = await self.mapper.query(script=c) 29 | 30 | self.assertEqual(0, len(r)) 31 | 32 | self.ioloop.run_until_complete(test()) 33 | 34 | def test_can_send_request_and_retrieve_collection_objec(self): 35 | 36 | async def test(): 37 | script = 'a = 1' 38 | r = await self.mapper.query(script=script) 39 | 40 | self.assertIsInstance(r, Collection) 41 | self.assertIsInstance(r[0], Vertex) 42 | 43 | self.ioloop.run_until_complete(test()) 44 | 45 | def test_can_get_database_time(self): 46 | 47 | async def test(): 48 | script = 'def x = new Date(); x' 49 | r = await self.mapper.query(script=script) 50 | 51 | self.assertTrue(r[0]['response'] != '') 52 | self.assertIsInstance(r[0]['response'].values[0], int) 53 | 54 | self.ioloop.run_until_complete(test()) 55 | 56 | def test_can_send_math_equation_to_server_and_retrieve_genderic_vertex_with_respnose_to_result(self): 57 | 58 | async def test(): 59 | script = 'b = 1 + 1;' 60 | r = await self.mapper.query(script=script) 61 | r1 = r[0] 62 | 63 | self.assertIsInstance(r1, Vertex) 64 | self.assertIn('response', r1.data) 65 | self.assertEqual(2, r1['response'].values[0]) 66 | 67 | self.ioloop.run_until_complete(test()) 68 | 69 | 70 | class EntityTestCases(object): 71 | 72 | def entity_save_assertions(self, entity): 73 | 74 | empty = ['', 0, None] 75 | non_zero = ['', None] 76 | fields = [GIZMO_ENTITY, GIZMO_ID, GIZMO_LABEL[0]] 77 | 78 | for f in fields: 79 | if f is GIZMO_ID: 80 | check = non_zero 81 | else: 82 | check = empty 83 | 84 | self.assertIsNotNone(entity[f]) 85 | self.assertNotIn(entity[f], check, msg='{} is `{}`'.format(f, entity[f])) 86 | 87 | def test_can_save_generic_vertex_and_update_itsid(self): 88 | 89 | async def test(): 90 | data = {'name': 'mark', 'sex': 'male'} 91 | v = self.mapper.create(data=data) 92 | 93 | self.mapper.save(v) 94 | 95 | res = await self.mapper.send() 96 | self.entity_save_assertions(v) 97 | 98 | self.ioloop.run_until_complete(test()) 99 | 100 | def test_can_save_generic_vertex_and_get_response_entity_withid(self): 101 | 102 | async def test(): 103 | data = {'name': 'mark', 'sex': 'male'} 104 | v = self.mapper.create(data=data) 105 | self.mapper.save(v) 106 | r = await self.mapper.send() 107 | v1 = r.first() 108 | 109 | self.entity_save_assertions(v1) 110 | 111 | self.ioloop.run_until_complete(test()) 112 | 113 | def test_can_save_defined_vertex_and_update_itsid(self): 114 | class TestVertex(Vertex): 115 | allow_undefined = True 116 | 117 | data = {'name': 'mark', 'sex': 'male'} 118 | v = self.mapper.create(data=data, entity=TestVertex) 119 | 120 | async def test(): 121 | await self.mapper.save(v).send() 122 | self.entity_save_assertions(v) 123 | 124 | self.ioloop.run_until_complete(test()) 125 | 126 | def test_can_save_defined_vertex_and_get_response_entity_withid(self): 127 | 128 | class TestVertex(Vertex): 129 | allow_undefined = True 130 | 131 | 132 | async def test(): 133 | data = {'name': 'mark', 'sex': 'male'} 134 | v = self.mapper.create(data=data, entity=TestVertex) 135 | self.mapper.save(v) 136 | r = await self.mapper.send() 137 | v1 = r.first() 138 | 139 | self.entity_save_assertions(v1) 140 | 141 | self.ioloop.run_until_complete(test()) 142 | 143 | def test_can_save_generic_edge_with_two_generic_vertices_all_at_once_and_update_allids(self): 144 | 145 | async def test(): 146 | label = 'some_label' 147 | v1 = self.mapper.create() 148 | v2 = self.mapper.create() 149 | e = self.mapper.connect(v1, v2, label) 150 | 151 | await self.mapper.save(e).send() 152 | self.entity_save_assertions(v1) 153 | self.entity_save_assertions(v2) 154 | self.entity_save_assertions(e) 155 | 156 | self.ioloop.run_until_complete(test()) 157 | 158 | def test_can_save_generic_edge_with_one_generic_vertex_all_at_once_and_update_allids(self): 159 | class TestVertex(Vertex): 160 | allow_undefined = True 161 | 162 | 163 | async def test(): 164 | label = 'some_label' 165 | v1 = self.mapper.create(entity=TestVertex) 166 | v2 = self.mapper.create() 167 | e = self.mapper.connect(v1, v2, label) 168 | 169 | await self.mapper.save(e).send() 170 | self.entity_save_assertions(v1) 171 | self.entity_save_assertions(v2) 172 | self.entity_save_assertions(e) 173 | 174 | self.ioloop.run_until_complete(test()) 175 | 176 | def test_can_save_generic_edge_with_two_defined_vertices_all_at_once_and_update_allids(self): 177 | class TestVertex(Vertex): 178 | allow_undefined = True 179 | 180 | class TestVertex2(Vertex): 181 | allow_undefined = True 182 | 183 | 184 | async def test(): 185 | label = 'some_label' 186 | v1 = self.mapper.create(entity=TestVertex) 187 | v2 = self.mapper.create(entity=TestVertex2) 188 | e = self.mapper.connect(v1, v2, label) 189 | 190 | await self.mapper.save(e).send() 191 | self.entity_save_assertions(v1) 192 | self.entity_save_assertions(v2) 193 | self.entity_save_assertions(e) 194 | 195 | self.ioloop.run_until_complete(test()) 196 | 197 | def test_can_save_defined_edge_with_two_defined_vertices_all_at_once_and_update_allids(self): 198 | class TestVertex(Vertex): 199 | allow_undefined = True 200 | 201 | class TestVertex2(Vertex): 202 | allow_undefined = True 203 | 204 | class TestEdge(Edge): 205 | pass 206 | 207 | async def test(): 208 | label = 'some_label' 209 | v1 = self.mapper.create(entity=TestVertex) 210 | v2 = self.mapper.create(entity=TestVertex2) 211 | e = self.mapper.connect(v1, v2, label, edge_entity=TestEdge) 212 | 213 | await self.mapper.save(e).send() 214 | self.entity_save_assertions(v1) 215 | self.entity_save_assertions(v2) 216 | self.entity_save_assertions(e) 217 | 218 | self.ioloop.run_until_complete(test()) 219 | 220 | def test_can_add_vertex_and_update_it(self): 221 | 222 | async def test(): 223 | await self.purge() 224 | 225 | data = { 226 | 'name': 'before_update', 227 | } 228 | updated = 'updated named {}'.format(str(random.random())) 229 | 230 | class UpdateVertex(Vertex): 231 | allow_undefined = True 232 | 233 | v = self.mapper.create(data=data, entity=UpdateVertex) 234 | x = await self.mapper.save(v).send() 235 | first = x.first() 236 | 237 | self.entity_save_assertions(first) 238 | self.assertEqual(v['id'], first['id']) 239 | 240 | first['name'] = updated 241 | 242 | await self.mapper.save(first).send() 243 | 244 | self.assertEqual(first['name'].values[0], updated) 245 | 246 | self.ioloop.run_until_complete(test()) 247 | 248 | 249 | def test_can_add_vertex_and_remove_it(self): 250 | 251 | async def test(): 252 | await self.purge() 253 | 254 | class RemoveVertex(Vertex): 255 | pass 256 | 257 | v = self.mapper.create(entity=RemoveVertex) 258 | x = await self.mapper.save(v).send() 259 | all_v = self.mapper.gremlin.V() 260 | res = await self.mapper.query(gremlin=self.mapper.gremlin) 261 | 262 | self.assertEqual(1, len(res)) 263 | 264 | await self.mapper.delete(v).send() 265 | 266 | all_v = self.mapper.gremlin.V() 267 | res = await self.mapper.query(gremlin=self.mapper.gremlin) 268 | 269 | self.assertEqual(0, len(res)) 270 | 271 | self.ioloop.run_until_complete(test()) 272 | 273 | def test_can_add_vertices_with_edge_delete_vertices_and_edge_is_automatically_gone(self): 274 | 275 | async def test(): 276 | await self.purge() 277 | 278 | class RemoveVertex2(Vertex): 279 | pass 280 | 281 | class RemoveEdge2(Edge): 282 | pass 283 | 284 | v1 = self.mapper.create(entity=RemoveVertex2) 285 | v2 = self.mapper.create(entity=RemoveVertex2) 286 | e = self.mapper.connect(v1, v2, edge_entity=RemoveEdge2) 287 | await self.mapper.save(e).send() 288 | 289 | all_v = self.mapper.gremlin.V() 290 | all_v_res = await self.mapper.query(gremlin=self.mapper.gremlin) 291 | all_e = self.mapper.gremlin.E() 292 | all_e_res = await self.mapper.query(gremlin=self.mapper.gremlin) 293 | 294 | self.assertEqual(2, len(all_v_res)) 295 | self.assertEqual(1, len(all_e_res)) 296 | 297 | await self.mapper.delete(v1).send() 298 | await self.mapper.delete(v2).send() 299 | 300 | all_v = self.mapper.gremlin.V() 301 | all_v_res = await self.mapper.query(gremlin=self.mapper.gremlin) 302 | all_e = self.mapper.gremlin.E() 303 | all_e_res = await self.mapper.query(gremlin=self.mapper.gremlin) 304 | 305 | self.assertEqual(0, len(all_v_res)) 306 | self.assertEqual(0, len(all_e_res)) 307 | 308 | self.ioloop.run_until_complete(test()) 309 | 310 | def test_can_add_vertices_with_edge_delete_one_vertext_and_edge_is_automatically_gone(self): 311 | 312 | async def test(): 313 | await self.purge() 314 | 315 | class RemoveVertex2(Vertex): 316 | pass 317 | 318 | class RemoveEdge2(Edge): 319 | pass 320 | 321 | v1 = self.mapper.create(entity=RemoveVertex2) 322 | v2 = self.mapper.create(entity=RemoveVertex2) 323 | e = self.mapper.connect(v1, v2, edge_entity=RemoveEdge2) 324 | await self.mapper.save(e).send() 325 | 326 | all_v = self.mapper.gremlin.V() 327 | all_v_res = await self.mapper.query(gremlin=self.mapper.gremlin) 328 | all_e = self.mapper.gremlin.E() 329 | all_e_res = await self.mapper.query(gremlin=self.mapper.gremlin) 330 | 331 | self.assertEqual(2, len(all_v_res)) 332 | self.assertEqual(1, len(all_e_res)) 333 | 334 | await self.mapper.delete(v1).send() 335 | 336 | all_v = self.mapper.gremlin.V() 337 | all_v_res = await self.mapper.query(gremlin=self.mapper.gremlin) 338 | all_e = self.mapper.gremlin.E() 339 | all_e_res = await self.mapper.query(gremlin=self.mapper.gremlin) 340 | 341 | self.assertEqual(1, len(all_v_res)) 342 | self.assertEqual(0, len(all_e_res)) 343 | 344 | self.ioloop.run_until_complete(test()) 345 | 346 | 347 | class MapperTestCases(object): 348 | 349 | def test_can_utilitze_custom_mapper(self): 350 | variable = str(random.random()) 351 | 352 | class MapperTestVertexCutsom(Vertex): 353 | allow_undefined = True 354 | 355 | 356 | class MapperTestMapperCustom(EntityMapper): 357 | entity = MapperTestVertexCutsom 358 | 359 | def create(self, *args, **kwargs): 360 | entity = super(MapperTestMapperCustom, self).create(*args, **kwargs) 361 | entity['variable'] = variable 362 | return entity 363 | 364 | v = self.mapper.create(entity=MapperTestVertexCutsom) 365 | d = v.data 366 | 367 | self.assertIsInstance(v, MapperTestVertexCutsom) 368 | self.assertIn('variable', d) 369 | self.assertEqual(d['variable'][0]['value'], variable) 370 | 371 | def test_can_restrict_entity_creation_based_on_duplicate_field_values(self): 372 | 373 | async def test(): 374 | await self.purge() 375 | 376 | class MapperTestVertexDuplicate(Vertex): 377 | allow_undefined = True 378 | 379 | 380 | class MapperTestMapper(EntityMapper): 381 | entity = MapperTestVertexDuplicate 382 | unique_fields = ['first_name',] 383 | 384 | 385 | d = {'first_name': 'mark' + str(random.random())} 386 | v1 = self.mapper.create(data=d, entity=MapperTestVertexDuplicate) 387 | v2 = self.mapper.create(data=d, entity=MapperTestVertexDuplicate) 388 | 389 | r = await self.mapper.save(v1).send() 390 | r2 = await self.mapper.save(v2).send() 391 | 392 | gremlin = self.mapper.gremlin.V() 393 | res = await self.mapper.query(gremlin=gremlin) 394 | 395 | self.assertEqual(1, len(res)) 396 | 397 | self.ioloop.run_until_complete(test()) 398 | 399 | def test_can_restrict_multiple_entity_connections_both_direction(self): 400 | 401 | async def test(): 402 | await self.purge() 403 | 404 | class MapperTestVertexRestrict(Vertex): 405 | allow_undefined = True 406 | 407 | class MapperTestEdgeRestrict(Edge): 408 | allow_undefined = True 409 | 410 | class MapperTestEdgeMapperRestrict(EntityMapper): 411 | entity = MapperTestEdgeRestrict 412 | unique = 'both' 413 | 414 | d = {'first_name': 'mark' + str(random.random())} 415 | v1 = self.mapper.create(data=d, entity=MapperTestVertexRestrict) 416 | v2 = self.mapper.create(data=d, entity=MapperTestVertexRestrict) 417 | e = self.mapper.connect(v1, v2, edge_entity=MapperTestEdgeRestrict) 418 | e2 = self.mapper.connect(v1, v2, edge_entity=MapperTestEdgeRestrict) 419 | res = await self.mapper.save(e).send() 420 | res2 = await self.mapper.save(e2).send() 421 | gremlin = self.mapper.gremlin.E() 422 | result = await self.mapper.query(gremlin=gremlin) 423 | 424 | self.assertEqual(1, len(result)) 425 | 426 | self.ioloop.run_until_complete(test()) 427 | 428 | def test_can_restrict_multiple_entity_connections_in_direction(self): 429 | 430 | async def test(): 431 | await self.purge() 432 | 433 | class MapperTestVertexRestrictIn(Vertex): 434 | allow_undefined = True 435 | 436 | class MapperTestEdgeRestrictIn(Edge): 437 | allow_undefined = True 438 | 439 | class MapperTestEdgeMapperRestrictIN(EntityMapper): 440 | entity = MapperTestEdgeRestrictIn 441 | unique = 'in' 442 | 443 | d = {'first_name': 'mark' + str(random.random())} 444 | v1 = self.mapper.create(data=d, entity=MapperTestVertexRestrictIn) 445 | v2 = self.mapper.create(data=d, entity=MapperTestVertexRestrictIn) 446 | e = self.mapper.connect(v1, v2, edge_entity=MapperTestEdgeRestrictIn) 447 | e2 = self.mapper.connect(v2, v1, edge_entity=MapperTestEdgeRestrictIn) 448 | res = await self.mapper.save(e).send() 449 | res2 = await self.mapper.save(e2).send() 450 | gremlin = self.mapper.gremlin.E() 451 | result = await self.mapper.query(gremlin=gremlin) 452 | 453 | self.assertEqual(1, len(result)) 454 | 455 | self.ioloop.run_until_complete(test()) 456 | 457 | def test_can_restrict_multiple_entity_connections_out_direction(self): 458 | 459 | async def test(): 460 | await self.purge() 461 | 462 | class MapperTestVertexRestrictOut(Vertex): 463 | allow_undefined = True 464 | 465 | class MapperTestEdgeRestrictOut(Edge): 466 | allow_undefined = True 467 | 468 | class MapperTestEdgeMapperRestrictOut(EntityMapper): 469 | entity = MapperTestEdgeRestrictOut 470 | unique = 'in' 471 | 472 | d = {'first_name': 'mark' + str(random.random())} 473 | v1 = self.mapper.create(data=d, entity=MapperTestVertexRestrictOut) 474 | v2 = self.mapper.create(data=d, entity=MapperTestVertexRestrictOut) 475 | e = self.mapper.connect(v1, v2, edge_entity=MapperTestEdgeRestrictOut) 476 | e2 = self.mapper.connect(v2, v1, edge_entity=MapperTestEdgeRestrictOut) 477 | res = await self.mapper.save(e).send() 478 | res2 = await self.mapper.save(e2).send() 479 | gremlin = self.mapper.gremlin.E() 480 | result = await self.mapper.query(gremlin=gremlin) 481 | 482 | self.assertEqual(1, len(result)) 483 | 484 | self.ioloop.run_until_complete(test()) 485 | 486 | def test_can_save_edge_on_vertices_that_were_used_in_previous_connection_when_unique_is_true(self): 487 | 488 | async def test(): 489 | await self.purge() 490 | 491 | class MapperTestVertexRestrictAgain(Vertex): 492 | allow_undefined = True 493 | 494 | class MapperTestEdgeRestrictAgain(Edge): 495 | allow_undefined = True 496 | 497 | class MapperTestEdgeMapperRestrictAgain(EntityMapper): 498 | entity = MapperTestEdgeRestrictAgain 499 | unique = 'both' 500 | 501 | d = {'first_name': 'mark' + str(random.random())} 502 | v1 = self.mapper.create(data=d, entity=MapperTestVertexRestrictAgain) 503 | v2 = self.mapper.create(data=d, entity=MapperTestVertexRestrictAgain) 504 | v3 = self.mapper.create(data=d, entity=MapperTestVertexRestrictAgain) 505 | e = self.mapper.connect(v1, v2, edge_entity=MapperTestEdgeRestrictAgain) 506 | e2 = self.mapper.connect(v1, v3, edge_entity=MapperTestEdgeRestrictAgain) 507 | e3 = self.mapper.connect(v1, v3, edge_entity=MapperTestEdgeRestrictAgain) 508 | res = await self.mapper.save(e).send() 509 | res2 = await self.mapper.save(e2).send() 510 | res2 = await self.mapper.save(e3).send() 511 | gremlin = self.mapper.gremlin.E() 512 | result = await self.mapper.query(gremlin=gremlin) 513 | 514 | self.assertEqual(2, len(result)) 515 | 516 | self.ioloop.run_until_complete(test()) 517 | 518 | # def test_can_get_or_create(self): 519 | # this is not in the mapper yet 520 | # async def test(): 521 | # await self.purge() 522 | # 523 | # class GoCVertex(Vertex): 524 | # allow_undefined = True 525 | # 526 | # goc_v = await self.mapper.get_or_create(GoCVertex, field_val={'name': 'mark'}) 527 | # goc_v2 = await self.mapper.get_or_create(GoCVertex, field_val={'name': 'mark'}) 528 | # 529 | # ins = GoCVertex() 530 | # g = self.mapper.gremlin 531 | # g.V().has('"_label"', str(ins)) 532 | # res = await self.mapper.query(gremlin=g) 533 | # 534 | # self.assertEqual(goc_v['id'], goc_v2['id']) 535 | # self.assertEqual(1, len(res)) 536 | # 537 | # self.ioloop.run_until_complete(test()) 538 | -------------------------------------------------------------------------------- /gizmo/test/integration/tinkerpop.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import unittest 3 | import random 4 | 5 | from gremlinpy import Gremlin 6 | 7 | from . import ConnectionTestCases, EntityTestCases, MapperTestCases 8 | from gizmo import Mapper, Request, Collection, Vertex, Edge 9 | from gizmo.mapper import EntityMapper 10 | 11 | 12 | class BaseTests(unittest.TestCase): 13 | 14 | def setUp(self): 15 | self.request = Request('localhost', port=8182) 16 | self.gremlin = Gremlin('gizmo_testing') 17 | self.mapper = Mapper(self.request, self.gremlin) 18 | self.ioloop = asyncio.get_event_loop() 19 | super(BaseTests, self).setUp() 20 | 21 | 22 | def tearDown(self): 23 | super(BaseTests, self).tearDown() 24 | 25 | async def purge(self): 26 | script = "%s.V().map{it.get().remove()}" % self.gremlin.gv 27 | res = await self.mapper.query(script=script) 28 | 29 | return res 30 | 31 | 32 | class ConnectionTests(BaseTests, ConnectionTestCases): 33 | pass 34 | 35 | 36 | class EntityTests(EntityTestCases, BaseTests): 37 | pass 38 | 39 | 40 | class MapperTests(MapperTestCases, BaseTests): 41 | pass 42 | 43 | 44 | class CollectionTests(BaseTests): 45 | pass 46 | 47 | class TraversalTests(BaseTests): 48 | pass 49 | 50 | 51 | if __name__ == '__main__': 52 | unittest.main() 53 | -------------------------------------------------------------------------------- /gizmo/test/integration/titan.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import unittest 3 | import random 4 | 5 | from gremlinpy import Gremlin 6 | 7 | from . import ConnectionTestCases, EntityTestCases, MapperTestCases 8 | from gizmo import Mapper, Request, Collection, Vertex, Edge 9 | from gizmo.mapper import EntityMapper 10 | 11 | 12 | class BaseTests(unittest.TestCase): 13 | 14 | def setUp(self): 15 | self.request = Request('localhost', port=9192) 16 | self.gremlin = Gremlin('gizmo_testing') 17 | self.mapper = Mapper(self.request, self.gremlin) 18 | self.ioloop = asyncio.get_event_loop() 19 | super(BaseTests, self).setUp() 20 | 21 | def tearDown(self): 22 | super(BaseTests, self).tearDown() 23 | 24 | async def purge(self): 25 | script = "%s.V().map{it.get().remove()}" % self.gremlin.gv 26 | res = await self.mapper.query(script=script) 27 | 28 | return res 29 | 30 | 31 | class ConnectionTests(BaseTests, ConnectionTestCases): 32 | pass 33 | 34 | 35 | class EntityTests(EntityTestCases, BaseTests): 36 | pass 37 | 38 | 39 | class MapperTests(MapperTestCases, BaseTests): 40 | pass 41 | 42 | 43 | class CollectionTests(BaseTests): 44 | pass 45 | 46 | class TraversalTests(BaseTests): 47 | pass 48 | 49 | 50 | if __name__ == '__main__': 51 | unittest.main() 52 | -------------------------------------------------------------------------------- /gizmo/test/mapper.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | import unittest 4 | import json 5 | import copy 6 | 7 | from random import randrange, random, choice, randint 8 | from pprint import pprint 9 | 10 | from gremlinpy.gremlin import Gremlin 11 | 12 | from gizmo.connection import Response 13 | from gizmo.mapper import * 14 | from gizmo.exception import * 15 | from gizmo.entity import GenericVertex, GenericEdge 16 | from gizmo.util import camel_to_underscore, entity_name, _query_debug 17 | 18 | 19 | DEFAULT_PROPRTIES = sorted([GIZMO_LABEL[0], GIZMO_TYPE, GIZMO_ENTITY]) 20 | 21 | logging.disable(logging.CRITICAL) 22 | 23 | 24 | class TestVertex(GenericVertex): 25 | pass 26 | 27 | 28 | class TestEdge(GenericEdge): 29 | label = 'test_edge_label' 30 | 31 | 32 | class TestRequest: 33 | 34 | async def send(*args, **kwargs): 35 | pass 36 | 37 | 38 | def get_dict_key(params, value, unset=False): 39 | for k, v in params.items(): 40 | if v == value: 41 | if unset: 42 | del(params[k]) 43 | return k, params 44 | 45 | return None, dict 46 | 47 | 48 | def build_prop(key, value, params=None, value_properties=None): 49 | prop = ['property({}'.format(key)] 50 | properties = [] 51 | 52 | if value_properties: 53 | for k, v in value_properties.items(): 54 | kv, _ = get_dict_key(params, k) 55 | vv, _ = get_dict_key(params, v) 56 | properties += ['{}, {}'.format(kv, vv)] 57 | 58 | prop += [', {}'.format(value)] 59 | 60 | if properties: 61 | properties = ', '.join(properties) 62 | prop += [', {}'.format(properties)] 63 | 64 | prop += [')'] 65 | 66 | return ''.join(prop) 67 | 68 | 69 | def build_params(entity, values, mapper, params=None, value_properties=None, 70 | entities=None, deleted=None, ignore=None): 71 | ignore = ignore or [] 72 | params = copy.deepcopy(params or {}) 73 | expected = [] 74 | value_properties = value_properties or {} 75 | deleted = deleted or [] 76 | entity_name = str(entity) 77 | entity_alias = '{}_alias'.format(entity_name) 78 | 79 | def get_key(key): 80 | if key.startswith('T.'): 81 | return key 82 | else: 83 | k, _ = get_dict_key(params, key) 84 | return k 85 | 86 | def delete_key(key): 87 | nonlocal expected 88 | k, _ = get_dict_key(params, key) 89 | a, _ = get_dict_key(params, entity_alias) 90 | expected += ['as({})'.format(a)] 91 | expected += ['properties({})'.format(k)] 92 | expected += ['sideEffect{it.get().remove()}'] 93 | expected += ['select({})'.format(a)] 94 | pass 95 | 96 | for key, val in entity.data.items(): 97 | if key in ignore: 98 | continue 99 | 100 | if key in deleted: 101 | delete_key(key) 102 | continue 103 | 104 | if isinstance(val, (list, tuple)) and len(val)\ 105 | and isinstance(val[0], dict)\ 106 | and 'value' in val[0]: 107 | if isinstance(val[0], (list, tuple)): 108 | for v in val[0]['value']: 109 | if v not in values: 110 | continue 111 | 112 | var, _ = get_dict_key(params, v) 113 | prop = build_prop(get_key(key), var, params, 114 | value_properties.get(v, None)) 115 | 116 | expected.append(prop) 117 | else: 118 | if val[0]['value'] not in values: 119 | continue 120 | 121 | v = val[0]['value'] 122 | var, _ = get_dict_key(params, v) 123 | prop = build_prop(get_key(key), var, params, 124 | value_properties.get(v, None)) 125 | 126 | expected.append(prop) 127 | else: 128 | if val not in values: 129 | continue 130 | 131 | var, _ = get_dict_key(params, val) 132 | prop = build_prop(get_key(key), var, params, 133 | value_properties.get(val, None)) 134 | 135 | expected.append(prop) 136 | 137 | return expected 138 | 139 | 140 | def build_vertex_create_query(entity, values, mapper, params=None, 141 | value_properties=None, entities=None, deleted=None, return_var=None): 142 | expected = [] 143 | label_str = str(entity) 144 | label, _ = get_dict_key(params, label_str) 145 | add = '{}.addV(T.label, {})'.format(mapper.gremlin.gv, label) 146 | ignore = ['T.label', 'label'] 147 | 148 | if return_var: 149 | expected += ['{} = {}'.format(return_var, add)] 150 | else: 151 | expected += [add] 152 | 153 | expected += build_params(entity=entity, values=values, params=params, 154 | value_properties=value_properties, entities=entities, mapper=mapper, 155 | deleted=deleted, ignore=ignore) 156 | 157 | expected.append('next()') 158 | 159 | return '.'.join(expected) 160 | 161 | 162 | def build_update_query(entity, values, mapper, params=None, 163 | value_properties=None, entities=None, deleted=None, return_var=None): 164 | entity_type, _id = entity.get_rep() 165 | alias = '{}_{}_updating'.format(entity_type, _id) 166 | _id, _ = get_dict_key(params, _id) 167 | alias, _ = get_dict_key(params, alias) 168 | expected = [] 169 | update = '{}.{}({})'.format(mapper.gremlin.gv, entity_type.upper(), _id) 170 | 171 | if return_var: 172 | expected += ['{} = {}'.format(return_var, update)] 173 | else: 174 | expected += [update] 175 | 176 | expected += ['as({})'.format(alias)] 177 | 178 | expected += build_params(entity=entity, values=values, params=params, 179 | value_properties=value_properties, entities=entities, mapper=mapper, 180 | deleted=deleted) 181 | 182 | expected += ['select({})'.format(alias), 'next()'] 183 | 184 | return '.'.join(expected) 185 | 186 | 187 | def build_delete_query(entity, mapper, params=None): 188 | params = params or {} 189 | e_type, _id = entity.get_rep() 190 | _id, _ = get_dict_key(params, _id) 191 | 192 | return '{}.{}({}).next().remove()'.format(mapper.gremlin.gv, 193 | e_type.upper(), _id) 194 | 195 | 196 | def build_edge_create_query(entity, out_v, in_v, values, mapper, params=None, 197 | value_properties=None, entities=None, deleted=None): 198 | 199 | def g_v(_id): 200 | if isinstance(_id, str) and _id.startswith('var:'): 201 | _id = _id.split(':')[1] 202 | else: 203 | _id, _ = get_dict_key(params, _id) 204 | 205 | return '{}.V({}).next()'.format(mapper.gremlin.gv, _id) 206 | 207 | v_in = g_v(in_v) 208 | v_out = g_v(out_v) 209 | label, _ = get_dict_key(params, entity[GIZMO_LABEL[0]]) 210 | edge_params = [] 211 | ignore = [GIZMO_LABEL[0], GIZMO_LABEL[1], GIZMO_TYPE] 212 | 213 | for f, changes in entity.changes.items(): 214 | if f in ignore: 215 | continue 216 | 217 | try: 218 | if changes['immutable']: 219 | val = changes['values']['values'][-1] 220 | else: 221 | val = changes['values'][-1] 222 | except: 223 | continue 224 | 225 | f_arg, _ = get_dict_key(params, f) 226 | v_arg, _ = get_dict_key(params, val) 227 | edge_params += [f_arg, v_arg] 228 | 229 | 230 | edge_params = ', '.join(edge_params) 231 | expected = [v_out, ] 232 | 233 | if edge_params: 234 | expected += ['addEdge({}, {}, {})'.format(label, v_in, edge_params)] 235 | else: 236 | expected += ['addEdge({}, {})'.format(label, v_in)] 237 | 238 | return '.'.join(expected) 239 | 240 | 241 | class QueryTests(unittest.TestCase): 242 | 243 | def setUp(self): 244 | super().setUp() 245 | self.request = TestRequest() 246 | self.mapper = Mapper(request=self.request) 247 | self.query = Query(mapper=self.mapper) 248 | 249 | def test_can_save_vertex_with_no_field_values(self): 250 | v = TestVertex() 251 | 252 | self.query.save(v) 253 | 254 | queries = self.query.queries 255 | values = ['vertex', entity_name(v), str(v), GIZMO_ENTITY, GIZMO_TYPE] 256 | 257 | self.assertEqual(1, len(queries)) 258 | 259 | entry = queries[0] 260 | params = entry['params'] 261 | expected = build_vertex_create_query(entity=v, values=values, 262 | params=params, mapper=self.mapper) 263 | 264 | self.assertEqual(expected, entry['script']) 265 | self.assertEqual(len(values), len(params)) 266 | 267 | def test_can_save_vertex_with_one_field_value(self): 268 | v = TestVertex() 269 | ik = '__some_field' + str(random()) 270 | iv = 'some_value' + str(random()) 271 | v[ik] = iv 272 | 273 | self.query.save(v) 274 | 275 | values = [ik, iv, 'vertex', entity_name(v), str(v), GIZMO_ENTITY, 276 | GIZMO_TYPE] 277 | queries = self.query.queries 278 | 279 | self.assertEqual(1, len(queries)) 280 | 281 | entry = queries[0] 282 | params = entry['params'] 283 | expected = build_vertex_create_query(entity=v, values=values, 284 | params=params, mapper=self.mapper) 285 | self.assertEqual(expected, entry['script']) 286 | self.assertEqual(len(values), len(params)) 287 | 288 | def test_can_save_vertex_with_one_field_value_one_property(self): 289 | v = TestVertex() 290 | ik = 'some_field' + str(random()) 291 | iv = 'some_value' + str(random()) 292 | pk = 'prop_key' + str(random()) 293 | pv = 'prop_value' + str(random()) 294 | v[ik] = iv 295 | v[ik][iv].properties[pk] = pv 296 | 297 | self.query.save(v) 298 | 299 | values = [ik, iv, 'vertex', entity_name(v), str(v), GIZMO_ENTITY, 300 | GIZMO_TYPE] 301 | value_properties = { 302 | iv: { 303 | pk: pv, 304 | } 305 | } 306 | queries = self.query.queries 307 | 308 | self.assertEqual(1, len(queries)) 309 | 310 | entry = queries[0] 311 | params = entry['params'] 312 | expected = build_vertex_create_query(entity=v, values=values, params=params, 313 | value_properties=value_properties, mapper=self.mapper) 314 | self.assertEqual(expected, entry['script']) 315 | 316 | # 2 is the number of params added because of the one property defined 317 | self.assertEqual(len(values) + 2, len(params)) 318 | 319 | def test_can_save_vertex_with_one_field_value_two_properties(self): 320 | v = TestVertex() 321 | ik = 'some_field' + str(random()) 322 | iv = 'some_value' + str(random()) 323 | pk = 'prop_key' + str(random()) 324 | pv = 'prop_value' + str(random()) 325 | pk2 = 'prop2_key' + str(random()) 326 | pv2 = 'prop2_value' + str(random()) 327 | v[ik] = iv 328 | v[ik][iv].properties[pk] = pv 329 | v[ik][iv].properties[pk2] = pv2 330 | 331 | self.query.save(v) 332 | 333 | values = [ik, iv, 'vertex', entity_name(v), str(v), GIZMO_ENTITY, GIZMO_TYPE] 334 | value_properties = { 335 | iv: { 336 | pk: pv, 337 | pk2: pv2, 338 | } 339 | } 340 | queries = self.query.queries 341 | 342 | self.assertEqual(1, len(queries)) 343 | 344 | entry = queries[0] 345 | params = entry['params'] 346 | expected = build_vertex_create_query(entity=v, values=values, params=params, 347 | value_properties=value_properties, mapper=self.mapper) 348 | 349 | self.assertEqual(expected, entry['script']) 350 | 351 | # 4 is the number of params added because of the two props defined 352 | self.assertEqual(len(values) + 4, len(params)) 353 | 354 | def test_can_save_vertex_with_two_field_values(self): 355 | v = TestVertex() 356 | ik = '__some_field' + str(random()) 357 | iv = 'some_value' + str(random()) 358 | ik2 = '2__some_field' + str(random()) 359 | iv2 = '2some_value' + str(random()) 360 | v[ik] = iv 361 | v[ik2] = iv2 362 | 363 | self.query.save(v) 364 | 365 | values = [ik, iv, ik2, iv2, 'vertex', entity_name(v), str(v), 366 | GIZMO_ENTITY, GIZMO_TYPE] 367 | queries = self.query.queries 368 | 369 | self.assertEqual(1, len(queries)) 370 | 371 | entry = queries[0] 372 | params = entry['params'] 373 | expected = build_vertex_create_query(entity=v, values=values, 374 | params=params, mapper=self.mapper) 375 | self.assertEqual(expected, entry['script']) 376 | self.assertEqual(len(values), len(params)) 377 | 378 | def test_can_save_vertex_with_two_fields_value_one_property_on_one_field(self): 379 | v = TestVertex() 380 | ik = 'some_field' + str(random()) 381 | iv = 'some_value' + str(random()) 382 | ik2 = '2__some_field' + str(random()) 383 | iv2 = '2some_value' + str(random()) 384 | pk = 'prop_key' + str(random()) 385 | pv = 'prop_value' + str(random()) 386 | v[ik] = iv 387 | v[ik][iv].properties[pk] = pv 388 | v[ik2] = iv2 389 | 390 | self.query.save(v) 391 | 392 | values = [ik, iv, iv2, ik2, 'vertex', entity_name(v), str(v), 393 | GIZMO_ENTITY, GIZMO_TYPE] 394 | value_properties = { 395 | iv: { 396 | pk: pv, 397 | } 398 | } 399 | queries = self.query.queries 400 | 401 | self.assertEqual(1, len(queries)) 402 | 403 | entry = queries[0] 404 | params = entry['params'] 405 | expected = build_vertex_create_query(entity=v, values=values, params=params, 406 | value_properties=value_properties, mapper=self.mapper) 407 | self.assertEqual(expected, entry['script']) 408 | 409 | # 2 is the number of params added because of the one property defined 410 | self.assertEqual(len(values) + 2, len(params)) 411 | 412 | def test_can_save_vertex_with_two_fields_value_one_property_on_each_field(self): 413 | v = TestVertex() 414 | ik = 'some_field' + str(random()) 415 | iv = 'some_value' + str(random()) 416 | ik2 = '2__some_field' + str(random()) 417 | iv2 = '2some_value' + str(random()) 418 | pk = 'prop_key' + str(random()) 419 | pv = 'prop_value' + str(random()) 420 | pk2 = '2prop_key' + str(random()) 421 | pv2 = '2prop_value' + str(random()) 422 | v[ik] = iv 423 | v[ik][iv].properties[pk] = pv 424 | v[ik2] = iv2 425 | v[ik2][iv2].properties[pk2] = pv2 426 | 427 | self.query.save(v) 428 | 429 | values = [ik, iv, iv2, ik2, 'vertex', entity_name(v), str(v), 430 | GIZMO_ENTITY, GIZMO_TYPE, pk2, pv2, pk, pv] 431 | value_properties = { 432 | iv: { 433 | pk: pv, 434 | }, 435 | iv2: { 436 | pk2: pv2, 437 | } 438 | } 439 | queries = self.query.queries 440 | 441 | self.assertEqual(1, len(queries)) 442 | 443 | entry = queries[0] 444 | params = entry['params'] 445 | expected = build_vertex_create_query(entity=v, values=values, params=params, 446 | value_properties=value_properties, mapper=self.mapper) 447 | self.assertEqual(expected, entry['script']) 448 | self.assertEqual(len(values), len(params)) 449 | 450 | def test_can_save_vertex_with_two_fields_value_two_props_on_one_field(self): 451 | v = TestVertex() 452 | ik = 'some_field' + str(random()) 453 | iv = 'some_value' + str(random()) 454 | ik2 = '2__some_field' + str(random()) 455 | iv2 = '2some_value' + str(random()) 456 | pk = 'prop_key' + str(random()) 457 | pv = 'prop_value' + str(random()) 458 | pk2 = '2prop_key' + str(random()) 459 | pv2 = '2prop_value' + str(random()) 460 | v[ik] = iv 461 | v[ik][iv].properties[pk] = pv 462 | v[ik][iv].properties[pk2] = pv2 463 | v[ik2] = iv2 464 | 465 | self.query.save(v) 466 | 467 | values = [ik, iv, iv2, ik2, 'vertex', entity_name(v), str(v), 468 | GIZMO_ENTITY, GIZMO_TYPE, pk2, pv2, pk, pv] 469 | value_properties = { 470 | iv: { 471 | pk: pv, 472 | pk2: pv2, 473 | }, 474 | } 475 | queries = self.query.queries 476 | 477 | self.assertEqual(1, len(queries)) 478 | 479 | entry = queries[0] 480 | params = entry['params'] 481 | expected = build_vertex_create_query(entity=v, values=values, params=params, 482 | value_properties=value_properties, mapper=self.mapper) 483 | self.assertEqual(expected, entry['script']) 484 | self.assertEqual(len(values), len(params)) 485 | 486 | def test_can_save_vertex_with_two_fields_value_two_props_on_one_field_one_on_the_other(self): 487 | v = TestVertex() 488 | ik = 'some_field' + str(random()) 489 | iv = 'some_value' + str(random()) 490 | ik2 = '2__some_field' + str(random()) 491 | iv2 = '2some_value' + str(random()) 492 | pk = 'prop_key' + str(random()) 493 | pv = 'prop_value' + str(random()) 494 | pk2 = '2prop_key' + str(random()) 495 | pv2 = '2prop_value' + str(random()) 496 | pk3 = '3prop_key' + str(random()) 497 | pv3 = '3prop_value' + str(random()) 498 | v[ik] = iv 499 | v[ik][iv].properties[pk] = pv 500 | v[ik][iv].properties[pk2] = pv2 501 | v[ik2] = iv2 502 | v[ik2][iv2].properties[pk3] = pv3 503 | 504 | self.query.save(v) 505 | 506 | values = [ik, iv, iv2, ik2, 'vertex', entity_name(v), str(v), 507 | GIZMO_ENTITY, GIZMO_TYPE, pk2, pv2, pk, pv, pv3, pk3] 508 | value_properties = { 509 | iv: { 510 | pk: pv, 511 | pk2: pv2, 512 | }, 513 | iv2: { 514 | pk3: pv3 515 | } 516 | } 517 | queries = self.query.queries 518 | 519 | self.assertEqual(1, len(queries)) 520 | 521 | entry = queries[0] 522 | params = entry['params'] 523 | expected = build_vertex_create_query(entity=v, values=values, params=params, 524 | value_properties=value_properties, mapper=self.mapper) 525 | self.assertEqual(expected, entry['script']) 526 | self.assertEqual(len(values), len(params)) 527 | 528 | def test_can_update_vertex_with_no_field_values(self): 529 | _id = str(random()) 530 | data = {GIZMO_ID: _id} 531 | v = TestVertex(data) 532 | 533 | self.query.save(v) 534 | 535 | queries = self.query.queries 536 | values = ['vertex', entity_name(v), GIZMO_ENTITY, GIZMO_ID] 537 | 538 | self.assertEqual(1, len(queries)) 539 | 540 | entry = queries[0] 541 | params = entry['params'] 542 | expected = build_update_query(entity=v, values=values, mapper=self.mapper, 543 | params=params) 544 | 545 | self.assertEqual(expected, entry['script']) 546 | 547 | # +1 because we cannot add the _id var to the values list and alias 548 | self.assertEqual(len(values) + 2, len(params)) 549 | 550 | def test_can_update_vertext_with_one_field_and_two_properties(self): 551 | # we only need one test bc properties are tested for adding vertex 552 | _id = str(random()) 553 | data = {GIZMO_ID: _id} 554 | v = TestVertex(data) 555 | ik = 'some_field' + str(random()) 556 | iv = 'some_value' + str(random()) 557 | pk = 'prop_key' + str(random()) 558 | pv = 'prop_value' + str(random()) 559 | pk2 = '2prop_key' + str(random()) 560 | pv2 = '2prop_value' + str(random()) 561 | v[ik] = iv 562 | v[ik][iv].properties[pk] = pv 563 | v[ik][iv].properties[pk2] = pv2 564 | 565 | self.query.save(v) 566 | 567 | values = [ik, iv, 'vertex', entity_name(v), GIZMO_ENTITY, GIZMO_ID, 568 | pk2, pv2, pk, pv] 569 | value_properties = { 570 | iv: { 571 | pk: pv, 572 | pk2: pv2, 573 | }, 574 | } 575 | 576 | queries = self.query.queries 577 | 578 | self.assertEqual(1, len(queries)) 579 | 580 | entry = queries[0] 581 | params = entry['params'] 582 | expected = build_update_query(entity=v, values=values, mapper=self.mapper, 583 | params=params, value_properties=value_properties) 584 | self.assertEqual(expected, entry['script']) 585 | 586 | # +1 because we cannot add the _id var to the values list and alias 587 | self.assertEqual(len(values) + 2, len(params)) 588 | 589 | def test_can_update_vertex_with_two_fields_after_deleting_one(self): 590 | _id = str(random()) 591 | ik = 'key' + str(random()) 592 | iv = 'val' + str(random()) 593 | ik2 = '2key' + str(random()) 594 | iv2 = '2val' + str(random()) 595 | data = {GIZMO_ID: _id, ik: iv, ik2: iv2} 596 | v = TestVertex(data) 597 | 598 | del v[ik2] 599 | 600 | values = [ik, iv, ik2, iv2, GIZMO_ENTITY, entity_name(v), GIZMO_TYPE, 'vertex'] 601 | deleted = [ik2, ] 602 | 603 | self.query.save(v) 604 | queries = self.query.queries 605 | entry = queries[0] 606 | params = entry['params'] 607 | expected = build_update_query(entity=v, values=values, mapper=self.mapper, 608 | params=params, deleted=deleted) 609 | 610 | self.assertEqual(expected, entry['script']) 611 | 612 | def test_cannot_delete_vertex(self): 613 | v = TestVertex() 614 | 615 | self.assertRaises(AstronomerQueryException, self.query.delete, v) 616 | 617 | def test_can_delete_vertex(self): 618 | _id = str(random()) 619 | d = {GIZMO_ID: _id} 620 | v = TestVertex(data=d) 621 | 622 | self.query.delete(v) 623 | 624 | queries = self.query.queries 625 | entry = queries[0] 626 | expected = build_delete_query(entity=v, mapper=self.mapper, 627 | params=entry['params']) 628 | 629 | self.assertEqual(expected, entry['script']) 630 | self.assertEqual(1, len(entry['params'])) 631 | 632 | def test_cannot_save_edge(self): 633 | e = TestEdge() 634 | 635 | self.assertRaises(AstronomerQueryException, self.query.save, e) 636 | 637 | def test_cannot_save_edge_one_end_isnt_defined(self): 638 | d = { 639 | 'outV': 15, 640 | } 641 | e = TestEdge(data=d) 642 | 643 | self.assertRaises(AstronomerQueryException, self.query.save, e) 644 | 645 | def test_can_save_edge_with_ends_being_ids(self): 646 | d = { 647 | 'outV': 10, 648 | 'inV': 99, 649 | } 650 | e = TestEdge(data=d) 651 | 652 | self.query.save(e) 653 | 654 | values = [str(e), e.outV, e.inV, 655 | entity_name(e)] 656 | entry = self.query.queries[0] 657 | expected = build_edge_create_query(entity=e, out_v=e.outV, 658 | in_v=e.inV, values=values, mapper=self.mapper, 659 | params=entry['params']) 660 | 661 | self.assertEqual(expected, entry['script']) 662 | self.assertEqual(len(values) + 1, len(entry['params'])) 663 | 664 | def test_can_save_edge_one_end_being_new_entity_other_being_id(self): 665 | v = TestVertex() 666 | d = { 667 | 'outV': 15, 668 | 'inV': v, 669 | } 670 | e = TestEdge(data=d) 671 | 672 | self.query.save(e) 673 | 674 | in_v = 'var:{}'.format(self.mapper.get_entity_variable(v)) 675 | values = [GIZMO_LABEL[0], str(e), e.outV, 676 | entity_name(e)] 677 | entry = self.query.queries[0] 678 | expected = build_edge_create_query(entity=e, out_v=e.outV, 679 | in_v=in_v, values=values, mapper=self.mapper, 680 | params=entry['params']) 681 | 682 | self.assertEqual(expected, entry['script']) 683 | self.assertEqual(len(values), len(entry['params'])) 684 | 685 | def test_can_save_edge_with_two_new_entities(self): 686 | v = TestVertex() 687 | v2 = TestVertex() 688 | d = { 689 | 'outV': v2, 690 | 'inV': v, 691 | } 692 | e = TestEdge(data=d) 693 | 694 | self.query.save(e) 695 | 696 | in_v = 'var:{}'.format(self.mapper.get_entity_variable(v)) 697 | out_v = 'var:{}'.format(self.mapper.get_entity_variable(v2)) 698 | values = [GIZMO_LABEL[0], str(e), entity_name(e)] 699 | entry = self.query.queries[0] 700 | expected = build_edge_create_query(entity=e, out_v=out_v, 701 | in_v=in_v, values=values, mapper=self.mapper, 702 | params=entry['params']) 703 | 704 | self.assertEqual(expected, entry['script']) 705 | self.assertEqual(len(values), len(entry['params'])) 706 | 707 | 708 | class MapperTests(unittest.TestCase): 709 | 710 | def setUp(self): 711 | super().setUp() 712 | self.request = TestRequest() 713 | self.mapper = Mapper(request=self.request) 714 | self.query = Query(mapper=self.mapper) 715 | self.gremlin = Gremlin() 716 | self.ioloop = asyncio.get_event_loop() 717 | 718 | def test_mapper_instance(self): 719 | m = Mapper(self.request, self.gremlin) 720 | 721 | self.assertTrue(type(m) == Mapper) 722 | 723 | def test_can_create_vertex(self): 724 | v = self.mapper.create(entity=TestVertex) 725 | 726 | self.assertTrue(isinstance(v, Vertex)) 727 | self.assertEqual(v[GIZMO_TYPE], 'vertex') 728 | 729 | def test_can_create_vertex_with_data(self): 730 | d = {'some_field': str(random())} 731 | v = self.mapper.create(d, TestVertex) 732 | vd = v.data 733 | 734 | self.assertTrue(isinstance(v, Vertex)) 735 | self.assertIn('some_field', vd) 736 | self.assertEqual(1, len(vd['some_field'])) 737 | self.assertEqual(d['some_field'], vd['some_field'][0]['value']) 738 | 739 | def test_can_update_existing_vertex(self): 740 | vid = '1111' 741 | d = { 742 | GIZMO_ID: vid, 743 | 'some_field': 'mark', 744 | } 745 | v = self.mapper.create(d, TestVertex) 746 | v['some_field'] = 'xxxx' 747 | 748 | self.mapper.save(v)._build_queries() 749 | 750 | sent_params = copy.deepcopy(self.mapper.params) 751 | values = ['some_field', 'xxxx', 'vertex', entity_name(v), GIZMO_ENTITY, 752 | GIZMO_TYPE] 753 | queries = self.mapper.queries 754 | 755 | self.assertEqual(2, len(queries)) 756 | 757 | params = self.mapper.params 758 | return_var = self.mapper.get_entity_variable(v) 759 | expected = build_update_query(entity=v, values=values, 760 | params=params, mapper=self.mapper, return_var=return_var) 761 | 762 | self.assertEqual(expected, queries[0]) 763 | 764 | # +2 for id and alias 765 | self.assertEqual(len(values) + 2, len(params)) 766 | 767 | def test_can_queue_save_vertex_with_two_params_query(self): 768 | d = { 769 | 'some_field': 'mark', 770 | } 771 | v = self.mapper.create(d, TestVertex) 772 | 773 | self.mapper.save(v)._build_queries() 774 | 775 | params = copy.deepcopy(self.mapper.params) 776 | values = ['some_field', 'mark', 'vertex', entity_name(v), GIZMO_ENTITY, 777 | GIZMO_TYPE, str(v)] 778 | return_var = self.mapper.get_entity_variable(v) 779 | expected = build_vertex_create_query(entity=v, values=values, 780 | params=params, mapper=self.mapper, return_var=return_var) 781 | 782 | self.assertEqual(expected, self.mapper.queries[0]) 783 | self.assertEqual(len(values), len(params)) 784 | 785 | def test_can_delete_existing_vertex(self): 786 | vid = '1111' 787 | d = { 788 | GIZMO_ID: vid, 789 | 'some_field': 'mark', 790 | } 791 | v = self.mapper.create(d, TestVertex) 792 | self.mapper.delete(v)._build_queries() 793 | params = copy.deepcopy(self.mapper.params) 794 | sent_params = copy.deepcopy(self.mapper.params) 795 | eyed = get_dict_key(params, vid) 796 | expected = '{}.V({}).next().remove()'.format(self.mapper.gremlin.gv, 797 | eyed[0]) 798 | 799 | self.assertEqual(expected, self.mapper.queries[0]) 800 | 801 | def test_can_delete_multiple_entities(self): 802 | v1 = {'id': '15'} 803 | v2 = {'id': '10'} 804 | out_v = self.mapper.create(v1, TestVertex) 805 | in_v = self.mapper.create(v2, TestVertex) 806 | ed = {'outV': out_v, 'inV': in_v, 'id': '44'} 807 | edge = self.mapper.create(ed, TestEdge) 808 | 809 | self.mapper.delete(out_v) 810 | self.mapper.delete(in_v) 811 | self.mapper.delete(edge) 812 | self.mapper._build_queries() 813 | 814 | params = self.mapper.params 815 | v1_id = get_dict_key(params, v1['id']) 816 | v2_id = get_dict_key(params, v2['id']) 817 | e_id = get_dict_key(params, ed['id']) 818 | gv = self.mapper.gremlin.gv 819 | expected = [ 820 | '{}.V({}).next().remove()'.format(gv, v1_id[0]), 821 | '{}.V({}).next().remove()'.format(gv, v2_id[0]), 822 | '{}.E({}).next().remove()'.format(gv, e_id[0]), 823 | ] 824 | 825 | self.assertEqual(3, len(self.mapper.queries)) 826 | self.assertEqual(3, len(self.mapper.params)) 827 | 828 | for exp in expected: 829 | self.assertIn(exp, self.mapper.queries) 830 | 831 | def test_can_call_callback_when_save_method_is_called(self): 832 | variable = {'v': ''} 833 | updated = random() 834 | 835 | def save_test_callback(entity): 836 | variable['v'] = updated 837 | 838 | async def test(): 839 | m = self.mapper.create({}, TestVertex) 840 | await self.mapper.save(m, callback=save_test_callback).send() 841 | 842 | self.assertEqual(variable['v'], updated) 843 | 844 | self.ioloop.run_until_complete(test()) 845 | 846 | def test_can_call_callback_when_delete_method_is_called(self): 847 | variable = {'v': ''} 848 | updated = random() 849 | 850 | def delete_test_callback(entity): 851 | variable['v'] = updated 852 | 853 | async def test(): 854 | m = self.mapper.create({'id': '15'}, TestVertex) 855 | await self.mapper.delete(m, callback=delete_test_callback).send() 856 | 857 | self.assertEqual(variable['v'], updated) 858 | 859 | self.ioloop.run_until_complete(test()) 860 | 861 | def test_can_retrieve_data_from_entity_via_mapper(self): 862 | 863 | class TestCaseVertex1(Vertex): 864 | allow_undefined = True 865 | 866 | d = { 867 | 'name': 'name{}'.format(str(random())) 868 | } 869 | 870 | async def test(): 871 | v = self.mapper.create(d, TestCaseVertex1) 872 | data = await self.mapper.data(v) 873 | 874 | self.assertIn('name', data) 875 | self.assertEqual(d['name'], data['name'][0]['value']) 876 | 877 | self.ioloop.run_until_complete(test()) 878 | 879 | def test_can_retrieve_data_from_collection_via_mapper(self): 880 | 881 | class TestCaseVertex1(Vertex): 882 | allow_undefined = True 883 | 884 | class C(object): 885 | data = [] 886 | 887 | coll = [] 888 | items = 15 889 | 890 | for i in range(items): 891 | d = { 892 | 'name': 'name{}'.format(str(random())) 893 | } 894 | v = self.mapper.create(d, TestCaseVertex1) 895 | 896 | coll.append(dict(v.data)) 897 | 898 | resp = Response() 899 | resp.result = {'data': coll} 900 | 901 | collection = Collection(self.mapper, resp) 902 | 903 | async def test(): 904 | data = await self.mapper.data(collection) 905 | self.assertEqual(items, len(data)) 906 | 907 | names = [dd['name'] for dd in data] 908 | 909 | for d in coll: 910 | self.assertIn(d['name'], names) 911 | 912 | self.ioloop.run_until_complete(test()) 913 | 914 | def test_can_retrieve_data_from_two_nested_entities_via_custom_mapper_methods(self): 915 | city = 'city-{}'.format(str(random())) 916 | 917 | class TestCaseVertex2(Vertex): 918 | allow_undefined = True 919 | 920 | class TestCaseVertex2Mapper(EntityMapper): 921 | entity = TestCaseVertex2 922 | 923 | async def get_city(self, entity, data): 924 | data['city'] = city 925 | return data 926 | 927 | d = { 928 | 'name': 'name{}'.format(str(random())) 929 | } 930 | v = self.mapper.create(d, TestCaseVertex2) 931 | 932 | async def test(): 933 | data = await self.mapper.data(v, 'get_city') 934 | 935 | self.assertIn('name', data) 936 | self.assertEqual(d['name'], data['name'][0]['value']) 937 | self.assertIn('city', data) 938 | self.assertEqual(city, data['city']) 939 | 940 | self.ioloop.run_until_complete(test()) 941 | 942 | def test_can_assure_saving_vertex_mulitple_times_only_crud_once(self): 943 | d = {'some_field': str(random())} 944 | v = self.mapper.create(d, TestVertex) 945 | 946 | self.mapper.save(v).save(v)._build_queries() 947 | params = copy.deepcopy(self.mapper.params) 948 | return_var = self.mapper.get_entity_variable(v) 949 | values = ['some_field', d['some_field'], 'vertex', entity_name(v), 950 | GIZMO_ENTITY, GIZMO_TYPE, str(v)] 951 | expected = build_vertex_create_query(entity=v, values=values, 952 | params=params, mapper=self.mapper, return_var=return_var) 953 | self.assertEqual(3, len(self.mapper.queries)) 954 | self.assertIn(expected, self.mapper.queries) 955 | 956 | def test_can_assure_saving_edge_mulitple_times_only_crud_once(self): 957 | d = { 958 | 'outV': 10, 959 | 'inV': 99, 960 | } 961 | e = TestEdge(data=d) 962 | 963 | self.mapper.save(e).save(e)._build_queries() 964 | 965 | self.assertEqual(3, len(self.mapper.queries)) 966 | 967 | def test_can_assure_saving_edge_and_vertex_mulitple_times_only_crud_once(self): 968 | v = TestVertex() 969 | d = { 970 | 'outV': v, 971 | 'inV': 99, 972 | } 973 | e = TestEdge(data=d) 974 | 975 | self.mapper.save(e).save(e)._build_queries() 976 | 977 | self.assertEqual(4, len(self.mapper.queries)) 978 | 979 | 980 | class TestCallbackVertex(Vertex): 981 | allow_undefined = True 982 | 983 | 984 | test_callback_mapper_on_create_variable = None 985 | test_callback_mapper_on_update_variable = None 986 | test_callback_mapper_on_delete_variable = 'DEL' 987 | 988 | 989 | class TestCallbackMapper(EntityMapper): 990 | entity = TestCallbackVertex 991 | on_create_variable = '' 992 | 993 | def on_create(self, entity): 994 | global test_callback_mapper_on_create_variable 995 | test_callback_mapper_on_create_variable = \ 996 | entity['on_create_variable'].data[0]['value'] 997 | 998 | def on_update(self, entity): 999 | global test_callback_mapper_on_update_variable 1000 | test_callback_mapper_on_update_variable = \ 1001 | entity['on_update_variable'].values[0] 1002 | 1003 | def on_delete(self, entity): 1004 | global test_callback_mapper_on_delete_variable 1005 | x = entity['on_delete_variable'].values[0] 1006 | test_callback_mapper_on_delete_variable= \ 1007 | entity['on_delete_variable'].values[0] 1008 | 1009 | 1010 | class CustomMapperTests(unittest.TestCase): 1011 | 1012 | def setUp(self): 1013 | self.gremlin = Gremlin() 1014 | self.request = TestRequest() 1015 | self.mapper = Mapper(self.request, self.gremlin) 1016 | self.ioloop = asyncio.get_event_loop() 1017 | 1018 | def test_can_can_on_create_level_callback(self): 1019 | 1020 | async def test(): 1021 | global test_callback_mapper_on_create_variable 1022 | r = random() 1023 | v = TestCallbackVertex({'on_create_variable': r}) 1024 | await self.mapper.save(v).send() 1025 | self.assertEqual(r, test_callback_mapper_on_create_variable) 1026 | 1027 | self.ioloop.run_until_complete(test()) 1028 | 1029 | def test_can_can_on_update_entity_level_callback(self): 1030 | 1031 | async def test(): 1032 | global test_callback_mapper_on_update_variable 1033 | r = random() 1034 | v = TestCallbackVertex({'id': 10, 'on_update_variable': r}) 1035 | mapper = self.mapper.get_mapper(v) 1036 | await self.mapper.save(v).send() 1037 | self.assertEqual(r, test_callback_mapper_on_update_variable) 1038 | 1039 | self.ioloop.run_until_complete(test()) 1040 | 1041 | def test_can_can_on_delete_entity_level_callback(self): 1042 | 1043 | async def test(): 1044 | global test_callback_mapper_on_delete_variable 1045 | 1046 | r = random() 1047 | v = TestCallbackVertex({'id': 10, 'on_delete_variable': r}) 1048 | mapper = self.mapper.get_mapper(v) 1049 | await self.mapper.delete(v).send() 1050 | self.assertEqual(r, test_callback_mapper_on_delete_variable) 1051 | 1052 | self.ioloop.run_until_complete(test()) 1053 | 1054 | def test_can_can_on_create_level_callback_and_onetime_callback(self): 1055 | 1056 | async def test(): 1057 | global test_callback_mapper_on_create_variable 1058 | 1059 | variable = {'v': ''} 1060 | updated = random() 1061 | 1062 | def create_test_callback(entity): 1063 | variable['v'] = updated 1064 | 1065 | r = random() 1066 | v = TestCallbackVertex({'on_create_variable': r}) 1067 | mapper = self.mapper.get_mapper(v) 1068 | await self.mapper.save(v, callback=create_test_callback).send() 1069 | self.assertEqual(r, test_callback_mapper_on_create_variable) 1070 | self.assertEqual(variable['v'], updated) 1071 | 1072 | self.ioloop.run_until_complete(test()) 1073 | 1074 | def test_can_can_on_update_entity_level_callback_and_onetime_callback(self): 1075 | 1076 | async def test(): 1077 | global test_callback_mapper_on_update_variable 1078 | 1079 | variable = {'v': ''} 1080 | updated = random() 1081 | 1082 | def update_test_callback(entity): 1083 | variable['v'] = updated 1084 | 1085 | r = random() 1086 | v = TestCallbackVertex({'id': 10, 'on_update_variable': r}) 1087 | mapper = self.mapper.get_mapper(v) 1088 | await self.mapper.save(v, callback=update_test_callback).send() 1089 | self.assertEqual(r, test_callback_mapper_on_update_variable) 1090 | self.assertEqual(variable['v'], updated) 1091 | 1092 | self.ioloop.run_until_complete(test()) 1093 | 1094 | def test_can_can_on_delete_entity_level_callback_and_onetime_callback(self): 1095 | 1096 | async def test(): 1097 | global test_callback_mapper_on_delete_variable 1098 | 1099 | variable = {'v': ''} 1100 | updated = random() 1101 | 1102 | def delete_test_callback(entity): 1103 | variable['v'] = updated 1104 | 1105 | r = random() 1106 | v = TestCallbackVertex({'id': 10, 'on_delete_variable': r}) 1107 | mapper = self.mapper.get_mapper(v) 1108 | await self.mapper.delete(v, callback=delete_test_callback).send() 1109 | self.assertEqual(r, test_callback_mapper_on_delete_variable) 1110 | self.assertEqual(variable['v'], updated) 1111 | 1112 | self.ioloop.run_until_complete(test()) 1113 | 1114 | 1115 | if __name__ == '__main__': 1116 | unittest.main() -------------------------------------------------------------------------------- /gizmo/traversal.py: -------------------------------------------------------------------------------- 1 | from gremlinpy import Gremlin 2 | 3 | from .util import camel_to_underscore 4 | 5 | 6 | class Traversal(Gremlin): 7 | """ 8 | class used to start a traversal query based on a given entity 9 | when the class is created, the entity's _id and type are are 10 | set on the Gremlin object 11 | example: 12 | """ 13 | 14 | def __init__(self, mapper, entity): 15 | self._mapper = mapper 16 | self._entity = entity 17 | self._collection = None 18 | 19 | def set_mapper(self, mapper): 20 | if mapper: 21 | graph_variable = mapper.gremlin.gv 22 | 23 | super(Traversal, self).__init__(graph_variable) 24 | 25 | self.__mapper = mapper 26 | 27 | def get_mapper(self): 28 | return self.__mapper 29 | 30 | _mapper = property(get_mapper, set_mapper) 31 | 32 | def get_entity(self): 33 | return self.__entity 34 | 35 | def set_entity(self, entity): 36 | self.__entity = entity 37 | 38 | self._build_initial_query() 39 | 40 | _entity = property(get_entity, set_entity) 41 | 42 | def _build_initial_query(self): 43 | from .entity import _Entity 44 | from .mapper import next_param 45 | 46 | if not self._entity: 47 | return False 48 | 49 | entity = self._entity 50 | _id = None 51 | _base = isinstance(entity, _Entity) 52 | 53 | if _base: 54 | ev, _id = entity.get_rep() 55 | 56 | if _id: 57 | bound_id = next_param('{}_EYE_DEE'.format(str(entity)), _id) 58 | 59 | getattr(self, ev)(bound_id) 60 | else: 61 | if _base: 62 | _type = entity.__class__.__name__ 63 | else: 64 | _type = entity.__name__ 65 | ev, _ = entity().get_rep() 66 | 67 | _type = camel_to_underscore(_type) 68 | bound_type = self.bind_param(_type, 'BOUND_TYPE') 69 | 70 | getattr(self, ev)().hasLabel(bound_type[0]) 71 | 72 | return self 73 | 74 | async def __aiter__(self): 75 | return self 76 | 77 | async def __anext__(self): 78 | await self.to_collection() 79 | 80 | try: 81 | return next(self._collection) 82 | except: 83 | self._collection = None 84 | 85 | raise StopAsyncIteration() 86 | 87 | async def to_collection(self): 88 | # if not self._collection: 89 | self._collection = await self._mapper.query(gremlin=self) 90 | 91 | return self._collection 92 | -------------------------------------------------------------------------------- /gizmo/util.py: -------------------------------------------------------------------------------- 1 | import re 2 | import time 3 | 4 | from timeit import default_timer 5 | 6 | 7 | GIZMO_ID = 'id' 8 | GIZMO_LABEL = ('label', 'T.label') 9 | GIZMO_TYPE = 'type' 10 | GIZMO_ENTITY = '__GIZMO_ENTITY__' 11 | GIZMO_VARIABLE = 'gizmo_var' 12 | 13 | 14 | def camel_to_underscore(name): 15 | s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name) 16 | 17 | return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower() 18 | 19 | 20 | def entity_name(entity): 21 | if isinstance(entity, type): 22 | return '{}.{}'.format(entity.__module__, entity.__name__) 23 | else: 24 | return '{}.{}'.format(entity.__class__.__module__, 25 | entity.__class__.__name__) 26 | 27 | 28 | def is_gremlin_entity(data): 29 | """utility method used to check to see if a value from a gremlin response 30 | is supposed to be an entity or not 31 | """ 32 | try: 33 | if not isinstance(data, (list, tuple, dict)): 34 | return False 35 | 36 | if isinstance(data, dict): 37 | data = [data, ] 38 | 39 | if isinstance(data, (list, tuple)) and len(data) and\ 40 | 'value' in data[0]: 41 | return True 42 | except: 43 | return False 44 | 45 | return False 46 | 47 | 48 | def _query_debug(script, params): # pragma: no cover 49 | if not len(params): 50 | return script 51 | 52 | pattern = re.compile(r'\b(' + '|'.join(params.keys()) + r')\b') 53 | 54 | def su(x): 55 | x = str(params[x.group()]) if params[x.group()] else '' 56 | return "'%s'" % x 57 | 58 | return pattern.sub(su, script) 59 | 60 | 61 | def current_date_time(offset=0): # pragma: no cover 62 | return (int(time.time()) + offset) 63 | 64 | 65 | class Timer(object): 66 | elapsed = 0 67 | 68 | def __str__(self): 69 | return self.elapsed 70 | 71 | def __enter__(self): 72 | self.start = default_timer() 73 | return self 74 | 75 | def __exit__(self, *args): 76 | end = default_timer() 77 | self.elapsed_secs = end - self.start 78 | self.elapsed = self.elapsed_secs * 1000 79 | -------------------------------------------------------------------------------- /gizmo/version.py: -------------------------------------------------------------------------------- 1 | __version_info__ = ('0', '2', '0') 2 | __version__ = '.'.join(__version_info__) 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | Gizmo 3 | ------ 4 | 5 | Gizmo is an object graph mapper for Rexster-based graph servers. 6 | 7 | """ 8 | import sys 9 | from setuptools import setup, find_packages 10 | 11 | 12 | install_requires = [ 13 | 'gremlinpy', 14 | 'websockets' 15 | ] 16 | 17 | # get the version information 18 | exec(open('gizmo/version.py').read()) 19 | 20 | setup( 21 | name = 'gizmo', 22 | packages = find_packages(), 23 | version = __version__, 24 | description = 'Python OGM for Rexster-based graph servers', 25 | url = 'https://github.com/emehrkay/gizmo', 26 | author = 'Mark Henderson', 27 | author_email = 'emehrkay@gmail.com', 28 | long_description = __doc__, 29 | install_requires = install_requires, 30 | classifiers = [ 31 | 'License :: OSI Approved :: MIT License', 32 | 'Development Status :: 3 - Alpha', 33 | 'Programming Language :: Python :: 2.7', 34 | 'Environment :: Web Environment', 35 | 'Topic :: Database', 36 | 'Topic :: Database :: Front-Ends', 37 | 'Topic :: Internet :: WWW/HTTP', 38 | 'Topic :: Software Development', 39 | 'Topic :: Software Development :: Libraries :: Python Modules', 40 | 'Topic :: System :: Distributed Computing', 41 | 'Intended Audience :: Developers', 42 | 'Operating System :: POSIX :: Linux', 43 | 'Operating System :: MacOS', 44 | 'Operating System :: MacOS :: MacOS X', 45 | ], 46 | test_suite = 'gizmo.test', 47 | ) 48 | --------------------------------------------------------------------------------