Source

graphalchemy / graphalchemy / backends / neo4jem.py

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
# -*- coding: utf-8 -*-
'''neo4j backends'''

from inspect import ismodule

from lucenequerybuilder import Q
from stuf.utils import getdefault
from appspace.six import iteritems

from graphalchemy.backends.utils import Backend, BackendRead

__all__ = (
    'Raw', 'LinkRead', 'LinkWrite', 'NodeRead', 'NodeWrite', 'embedded',
    'close', 'transaction',
)


def close(db):
    '''
    shutdown database

    @param db: database connection
    '''
    db.shutdown()


def embedded(url):
    '''
    neo4j embedded database loader

    @param url: path to database on file system
    '''
    from neo4j import GraphDatabase
    return GraphDatabase(url)


def transaction(db):
    '''
    database transaction

    @param db: database connection
    '''
    return db.transaction


class Raw(BackendRead):

    @staticmethod
    def ands(**kw):
        '''
        "AND" lucene query builder

        see https://github.com/scholrly/lucene-querybuilder
        '''
        def R(k, v):
            return Q(k, v, wildcard=True)
        qs = None
        for k, v in iteritems(kw):
            qs = qs & R(k, v) if qs is not None else R(k, v)
        return qs

    @staticmethod
    def and_nots(**kw):
        '''
        "AND NOT" lucene query builder

        see https://github.com/scholrly/lucene-querybuilder
        '''
        def R(k, v):
            return -Q(k, v, wildcard=True)
        qs = None
        for k, v in iteritems(kw):
            qs = qs & R(k, v) if qs is not None else R(k, v)
        return qs

    @staticmethod
    def ors(**kw):
        '''
        "OR" lucene query builder

        see https://github.com/scholrly/lucene-querybuilder
        '''
        qs = None

        def R(k, v):
            return Q(k, v, wildcard=True)
        for k, v in iteritems(kw):
            qs = qs | R(k, v) if qs is not None else R(k, v)
        return qs

    @staticmethod
    def or_nots(**kw):
        '''
        "OR NOT" lucene query builder

        see https://github.com/scholrly/lucene-querybuilder
        '''
        qs = None

        def R(k, v):
            return -Q(k, v, wildcard=True)
        for k, v in iteritems(kw):
            qs = qs | R(k, v) if qs is not None else R(k, v)
        return qs

    def execute(self, model, expr, **kw):
        '''
        fetch graph items with Cypher query language

        @param model: graph model
        @param express: Cypher query
        @param **kw: variables for Cypher query
        '''
        return self.one_or_all(self._db.query(expr, **kw), model)


class Read(BackendRead):

    '''neo4j reader base'''

    @property
    def root(self):
        '''get root graph element'''
        return self._db.reference_node

    def filter(self, index, queries, model=None):
        '''
        full text search using index

        @param index: name of index
        @param queries: queries create with query builder
        @param model: element model (default: None)
        '''
        return self.one_or_all(self.index(index).query(queries), model)

    def filter_by(self, index, key, value, callback=None):
        '''
        fetch graph elements filtered by keywords

        @param index: index name
        @param key: keyword in index
        @param value: value in index (or second key)
        @param callback: filter callback function (default: None)
        '''
        return self.one_or_all_and_close(
            self.index(index)[key][value], callback,
        )

    def id(self, element):
        '''
        graph element identifier

        @param element: a graph element
        '''
        return getdefault(element, 'id', None)

    def properties(self, element):
        '''
        get graph element properties

        @param element: graph element
        '''
        return dict(element.items()) if element is not None else {}

    def traverser(self, this, tester=None, links=None, unique=None):
        '''
        create a traverse function

        @param this: graph object
        @param tester: filter for traversed elements (default: None)
        @param links: links to traverse (default: None)
        @param unique: how often to traverse the same element (default: None)
        '''
        tv = self._db.traversal()
        if links is not None:
            for link in links:
                if len(link) == 2:
                    tv = tv.relationships(*link)
                else:
                    tv = tv.relationships(link)
        if unique is not None:
            tv = tv.uniqueness(unique)
        if tester is not None:
            tv = tv.evaluator(tester)
        return tv.traverse(this.source)

    def walker(self, this, direction=None, label=None, keys=None):
        '''
        iterate over and yield links

        @param this: graph object
        @param direction: direction of links (default: None)
        @param label: label for links (default: None)
        @param keys: keys to find on elements (default: None)
        '''
        if label is not None:
            links = self.get_links(this.source, label)
        else:
            links = self.get_links(this.source, 'rels')
            if direction is not None:
                links = self.get_links(links, direction)
        for link in links:
            if keys:
                if not set([keys, tuple(link.keys())]).intersection():
                    pass
            yield link


class Write(Backend):

    '''neo4j writer base'''

    def delete_index(self, index):
        '''
        delete graph index

        @param index: graph index
        '''
        self._db.nodes.indexes.get(index).delete()

    def delete_property(self, this, key):
        '''
        delete graph element property

        @param this: graph object
        @param key: graph element property key
        '''
        del this.source[key]

    def index_many(self, index, this, indexed):
        '''
        index properties on a graph element

        @param index: graph index label
        @param this: graph object
        @param indexed: properties to index
        '''
        element = this.source
        for field in indexed:
            index[field][element[field]] = element

    def index_one(self, index, key, value, this):
        '''
        index one property of a graph element

        @param index: graph index label
        @param key: graph element property key
        @param value: graph element property value
        @param this: graph object
        '''
        index[key][value] = this.source

    def update(self, this, data, callback=None):
        '''
        update a graph element's properties

        @param this: graph object
        @param data: cleaned data
        @param callback: callback (default: None)
        '''
        element = this.source
        for k, v in iteritems(data):
            element[k] = v
        if callback is not None:
            callback(element)


class LinkRead(Read):

    '''neo4j link reader'''

    def get(self, element):
        '''
        get link by link id

        @param element: element identifier
        '''
        return self._db.relationships[element]

    def index(self, index):
        '''
        get link index by name

        @param index: index name
        '''
        return self._db.relationships.indexes.get(index)

    def kind(self, element):
        '''
        kind of link

        @param element: a link graph element
        '''
        return element.type.name()

    def traverser(self, this, tester=None, links=None, unique=None):
        '''
        create a traverse function

        @param this: graph object
        @param tester: filter for traversed elements (default: None)
        @param links: links to traverse (default: None)
        @param unique: how often to traverse the same element (default: None)
        '''
        tv = super(LinkRead, self).traverser(this, tester, links, unique)
        return tv.traverse(this.source).relationships


class LinkWrite(Write):

    '''neo4j link writer'''

    def create(self, link, start, end, kw=None, callback=None):
        '''
        create a new graph link

        @param link: link name
        @param start: start node for link
        @param end: end node for link
        @param kw: keywords (default: None)
        @param callback: callback (default: None)
        '''
        kw = {} if kw is None else kw
        this = start.source.rels.create(link, end.source, kw)
        if callback is not None:
            this = callback(this)
        return this

    def create_index(self, index, fts=False):
        '''
        create link index

        @param index: node index name
        @param fts: create a full text index (default: False)
        '''
        kind = 'fulltext' if fts else None
        self._db.relationships.indexes.create(index, type=kind)

    def delete(self, this, indices=None):
        '''
        delete graph element

        @param this: graph object
        @param index: graph index (default: None)
        '''
        db = self._db.relationships.indexes.get
        element = this.source
        if indices is not None:
            for index in indices:
                del db(index)[element]
        for link in element.rels:
            link.delete()
        element.delete()


class NodeRead(Read):

    '''neo4j node reader'''

    def get(self, element):
        '''get node by link id'''
        return self._db.nodes[element]

    def index(self, index):
        '''
        get node lucene index by name

        @param index: node index name
        '''
        return self._db.nodes.indexes.get(index)

    def traverser(self, this, tester=None, links=None, unique=None):
        '''
        create a traverse function

        @param this: graph object
        @param tester: filter for traversed elements (default: None)
        @param links: links to traverse (default: None)
        @param unique: how often to traverse the same element (default: None)
        '''
        tv = super(NodeRead, self).traverser(this, links, unique, tester)
        return tv.traverse(this.source).nodes

    #pylint: disable-msg=w0221
    def walker(self, this, direction=None, label=None, keys=None, end=None):
        '''
        iterate links and yield links

        @param this: graph object
        @param direction: direction of links (default: None)
        @param label: label for links (default: None)
        @param keys: keys to find on elements (default: None)
        @param end: end of link to return (default: None)
        '''
        walker = super(NodeRead, self).walker(this, direction, label, keys)
        for link in walker:
            lnk = link
            yield getattr(lnk, end)
    #pylint: enable-msg=w0221


class NodeWrite(Write):

    '''neo4j node writer'''

    def clone(self, this, model_link, kw=None, callback=None):
        '''
        clone node

        @param this: graph object
        @param model_link: model link to clone
        @param kw: keywords for links (default: None)
        @param callback: callback (default: None)
        '''
        # link list
        links = []
        # node id map
        id_map = {}
        # root node of fork
        fork = None
        db = self._db
        node_func = db.node
        # traverse outgoing nodes
        for source in db.traversal().traverse(this.source).nodes:
            # copy outgoing relationships
            links.extend([link for link in source.rels.outgoing])
            # copy node
            target = node_func(**dict(source.items()))
            # capture root this
            if source.id == this.id:
                fork = target
                reference = this.n.reference()
                target.rels.create(
                    this.C.reference_link, reference.source,
                )
            # add to node map
            id_map[source.id] = target.id
        # copy relationships
        for link in links:
            node_func[id_map[link.start.id]].rels.create(
                link.type,
                node_func[id_map[link.end.id]],
                **dict(link.items())
            )
        kw = {} if kw is None else kw
        # tie back to original
        fork.rels.create(model_link, this.source, **kw)
        if callback is not None:
            fork = callback(fork)
        return fork

    def create(self, data, link=None, other=None, kw=None, callback=None):
        '''
        create a new graph node

        @param data: data
        @param link: link kind (default: None)
        @param other: other node to link to (default: None)
        @param kw: keywords for links (default: None)
        @param callback: callback (default: None)
        '''
        # create node
        node = self._db.node(**data)
        # link back to reference
        if link is not None and other is not None:
            kw = {} if kw is None else kw
            node.rels.create(link, other.source, **kw)
        if callback is not None:
            node = callback(node)
        return node

    def create_index(self, index, fts=False):
        '''
        create node index

        @param index: index name
        @param fts: create a full text index (default: False)
        '''
        kind = 'fulltext' if fts else None
        return self._db.nodes.indexes.create(index, type=kind)

    def delete(self, this, indices=None):
        '''
        delete graph element

        @param this: graph object
        @param index: graph index (default: None)
        '''
        db = self._db.nodes.indexes.get
        element = this.source
        if indices is not None:
            for index in indices:
                del db(index)[element]
        for link in element.rels:
            link.delete()
        element.delete()


__all__ = sorted(name for name, obj in iteritems(locals()) if not any([
    name.startswith('_'), ismodule(obj)]
))