graphitedb 0.1.2__py3-none-any.whl → 0.1.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- graphite/__init__.py +109 -62
- {graphitedb-0.1.2.dist-info → graphitedb-0.1.3.dist-info}/METADATA +2 -4
- graphitedb-0.1.3.dist-info/RECORD +6 -0
- {graphitedb-0.1.2.dist-info → graphitedb-0.1.3.dist-info}/top_level.txt +0 -1
- __init__.py +0 -0
- graphitedb-0.1.2.dist-info/RECORD +0 -7
- {graphitedb-0.1.2.dist-info → graphitedb-0.1.3.dist-info}/WHEEL +0 -0
- {graphitedb-0.1.2.dist-info → graphitedb-0.1.3.dist-info}/licenses/LICENSE +0 -0
graphite/__init__.py
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Graphite: A clean, embedded graph database engine for Python.
|
|
3
|
+
|
|
4
|
+
This is graphite module (installation: ``pip install graphitedb``).
|
|
5
|
+
You can use it with ``import graphite``.
|
|
6
|
+
"""
|
|
1
7
|
from __future__ import annotations
|
|
2
8
|
|
|
3
9
|
import pickle
|
|
@@ -11,6 +17,9 @@ from typing import Any, Callable, Dict, List, Optional, Tuple, Union
|
|
|
11
17
|
# =============== TYPE SYSTEM ===============
|
|
12
18
|
|
|
13
19
|
class DataType(Enum):
|
|
20
|
+
"""
|
|
21
|
+
Valid data types in graphite. Used in nodes and relations properties.
|
|
22
|
+
"""
|
|
14
23
|
STRING = "string"
|
|
15
24
|
INT = "int"
|
|
16
25
|
DATE = "date"
|
|
@@ -19,12 +28,20 @@ class DataType(Enum):
|
|
|
19
28
|
|
|
20
29
|
@dataclass
|
|
21
30
|
class Field:
|
|
31
|
+
"""
|
|
32
|
+
A data field (property) for nodes and relations.
|
|
33
|
+
"""
|
|
22
34
|
name: str
|
|
23
35
|
dtype: DataType
|
|
24
36
|
default: Any = None
|
|
25
37
|
|
|
26
38
|
@dataclass
|
|
27
39
|
class NodeType:
|
|
40
|
+
"""
|
|
41
|
+
A defined node type (with ``node ...`` block in dsl or ``GraphiteEngine.define_node()``).
|
|
42
|
+
Each node type has a name (in snake_case usually), and optional list of fields (properties).
|
|
43
|
+
Supports optional parent node type.
|
|
44
|
+
"""
|
|
28
45
|
name: str
|
|
29
46
|
fields: List[Field] = field(default_factory=list)
|
|
30
47
|
parent: Optional[NodeType] = None
|
|
@@ -41,6 +58,12 @@ class NodeType:
|
|
|
41
58
|
|
|
42
59
|
@dataclass
|
|
43
60
|
class RelationType:
|
|
61
|
+
"""
|
|
62
|
+
A defined relation type (with ``relation ...`` block in dsl or
|
|
63
|
+
``GraphiteEngine.define_relation()``). Each relation type has a name (in UPPER_SNAKE_CASE
|
|
64
|
+
usually), and optional list of fields (properties). A relation type can be from one node
|
|
65
|
+
type to another.
|
|
66
|
+
"""
|
|
44
67
|
name: str
|
|
45
68
|
from_type: str
|
|
46
69
|
to_type: str
|
|
@@ -55,12 +78,17 @@ class RelationType:
|
|
|
55
78
|
|
|
56
79
|
@dataclass
|
|
57
80
|
class Node:
|
|
81
|
+
"""
|
|
82
|
+
A node in database. Has a base type, id, and properties from base type (and it's parent
|
|
83
|
+
type recursively).
|
|
84
|
+
"""
|
|
58
85
|
type_name: str
|
|
59
86
|
id: str
|
|
60
87
|
values: Dict[str, Any]
|
|
61
88
|
_type_ref: Optional[NodeType] = None
|
|
62
89
|
|
|
63
90
|
def get(self, field_name: str) -> Any:
|
|
91
|
+
"""Get a field from this node."""
|
|
64
92
|
return self.values.get(field_name)
|
|
65
93
|
|
|
66
94
|
def __getitem__(self, key):
|
|
@@ -71,6 +99,10 @@ class Node:
|
|
|
71
99
|
|
|
72
100
|
@dataclass
|
|
73
101
|
class Relation:
|
|
102
|
+
"""
|
|
103
|
+
A relation between two nodes in database. Has a base type, source and target node IDs,
|
|
104
|
+
and properties from base type.
|
|
105
|
+
"""
|
|
74
106
|
type_name: str
|
|
75
107
|
from_node: str # node id
|
|
76
108
|
to_node: str # node id
|
|
@@ -78,6 +110,7 @@ class Relation:
|
|
|
78
110
|
_type_ref: Optional[RelationType] = None
|
|
79
111
|
|
|
80
112
|
def get(self, field_name: str) -> Any:
|
|
113
|
+
"""Get a field from this relation."""
|
|
81
114
|
return self.values.get(field_name)
|
|
82
115
|
|
|
83
116
|
def __repr__(self):
|
|
@@ -119,6 +152,7 @@ class GraphiteParser:
|
|
|
119
152
|
|
|
120
153
|
return node_name, fields, parent
|
|
121
154
|
|
|
155
|
+
# pylint: disable=too-many-locals
|
|
122
156
|
@staticmethod
|
|
123
157
|
def parse_relation_definition(line: str) -> Tuple[str, str, str, List[Field], Optional[str], bool]:
|
|
124
158
|
"""Parse relation definition"""
|
|
@@ -241,8 +275,8 @@ class GraphiteParser:
|
|
|
241
275
|
class QueryResult:
|
|
242
276
|
"""Represents a query result that can be chained"""
|
|
243
277
|
|
|
244
|
-
def __init__(self,
|
|
245
|
-
self.engine =
|
|
278
|
+
def __init__(self, graph_engine: GraphiteEngine, nodes: List[Node], edges: List[Relation] = None):
|
|
279
|
+
self.engine = graph_engine
|
|
246
280
|
self.nodes = nodes
|
|
247
281
|
self.edges = edges or []
|
|
248
282
|
self.current_relation: Optional[RelationType] = None
|
|
@@ -254,21 +288,22 @@ class QueryResult:
|
|
|
254
288
|
|
|
255
289
|
if callable(condition):
|
|
256
290
|
# Lambda function
|
|
257
|
-
for
|
|
291
|
+
for processing_node in self.nodes:
|
|
258
292
|
try:
|
|
259
|
-
if condition(
|
|
260
|
-
filtered_nodes.append(
|
|
261
|
-
except e:
|
|
262
|
-
print(f"Graphite Warn: 'where' condition failed for node {
|
|
293
|
+
if condition(processing_node):
|
|
294
|
+
filtered_nodes.append(processing_node)
|
|
295
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
296
|
+
print(f"Graphite Warn: 'where' condition failed for node {processing_node}: {e}")
|
|
263
297
|
else:
|
|
264
298
|
# String condition like "age > 18"
|
|
265
|
-
for
|
|
266
|
-
if self._evaluate_condition(
|
|
267
|
-
filtered_nodes.append(
|
|
299
|
+
for processing_node in self.nodes:
|
|
300
|
+
if self._evaluate_condition(processing_node, condition):
|
|
301
|
+
filtered_nodes.append(processing_node)
|
|
268
302
|
|
|
269
303
|
return QueryResult(self.engine, filtered_nodes, self.edges)
|
|
270
304
|
|
|
271
|
-
|
|
305
|
+
# pylint: disable=too-many-branches
|
|
306
|
+
def _evaluate_condition(self, target_node: Node, condition: str) -> bool:
|
|
272
307
|
"""Evaluate a condition string on a node"""
|
|
273
308
|
# Simple condition parser
|
|
274
309
|
ops = ['>=', '<=', '!=', '==', '>', '<', '=']
|
|
@@ -280,7 +315,7 @@ class QueryResult:
|
|
|
280
315
|
right = right.strip()
|
|
281
316
|
|
|
282
317
|
# Get value from node
|
|
283
|
-
node_value =
|
|
318
|
+
node_value = target_node.get(left)
|
|
284
319
|
if node_value is None:
|
|
285
320
|
return False
|
|
286
321
|
|
|
@@ -295,18 +330,22 @@ class QueryResult:
|
|
|
295
330
|
right_value = right
|
|
296
331
|
|
|
297
332
|
# Apply operation
|
|
333
|
+
result = None
|
|
298
334
|
if op in ('=', '=='):
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
335
|
+
result = node_value == right_value
|
|
336
|
+
if op == '!=':
|
|
337
|
+
result = node_value != right_value
|
|
338
|
+
if op == '>':
|
|
339
|
+
result = node_value > right_value
|
|
340
|
+
if op == '<':
|
|
341
|
+
result = node_value < right_value
|
|
342
|
+
if op == '>=':
|
|
343
|
+
result = node_value >= right_value
|
|
344
|
+
if op == '<=':
|
|
345
|
+
result = node_value <= right_value
|
|
346
|
+
if result is None:
|
|
347
|
+
raise ValueError(f"Invalid condition string: {condition}")
|
|
348
|
+
return result
|
|
310
349
|
|
|
311
350
|
return False
|
|
312
351
|
|
|
@@ -315,14 +354,14 @@ class QueryResult:
|
|
|
315
354
|
result_nodes = []
|
|
316
355
|
result_edges = []
|
|
317
356
|
|
|
318
|
-
for
|
|
357
|
+
for processing_node in self.nodes:
|
|
319
358
|
if direction == 'outgoing':
|
|
320
|
-
edges = self.engine.get_relations_from(
|
|
359
|
+
edges = self.engine.get_relations_from(processing_node.id, relation_type)
|
|
321
360
|
elif direction == 'incoming':
|
|
322
|
-
edges = self.engine.get_relations_to(
|
|
361
|
+
edges = self.engine.get_relations_to(processing_node.id, relation_type)
|
|
323
362
|
else: # both
|
|
324
|
-
edges = (self.engine.get_relations_from(
|
|
325
|
-
self.engine.get_relations_to(
|
|
363
|
+
edges = (self.engine.get_relations_from(processing_node.id, relation_type) +
|
|
364
|
+
self.engine.get_relations_to(processing_node.id, relation_type))
|
|
326
365
|
|
|
327
366
|
for edge in edges:
|
|
328
367
|
result_edges.append(edge)
|
|
@@ -355,17 +394,17 @@ class QueryResult:
|
|
|
355
394
|
"""Get distinct nodes"""
|
|
356
395
|
seen = set()
|
|
357
396
|
distinct_nodes = []
|
|
358
|
-
for
|
|
359
|
-
if
|
|
360
|
-
seen.add(
|
|
361
|
-
distinct_nodes.append(
|
|
397
|
+
for processing_node in self.nodes:
|
|
398
|
+
if processing_node.id not in seen:
|
|
399
|
+
seen.add(processing_node.id)
|
|
400
|
+
distinct_nodes.append(processing_node)
|
|
362
401
|
return QueryResult(self.engine, distinct_nodes, self.edges)
|
|
363
402
|
|
|
364
|
-
def order_by(self,
|
|
403
|
+
def order_by(self, by_field: str, descending: bool = False) -> QueryResult:
|
|
365
404
|
"""Order nodes by field"""
|
|
366
405
|
|
|
367
|
-
def get_key(
|
|
368
|
-
val =
|
|
406
|
+
def get_key(from_node):
|
|
407
|
+
val = from_node.get(by_field)
|
|
369
408
|
return (val is None, val)
|
|
370
409
|
|
|
371
410
|
sorted_nodes = sorted(self.nodes, key=get_key, reverse=descending)
|
|
@@ -387,11 +426,11 @@ class QueryResult:
|
|
|
387
426
|
"""Get node IDs"""
|
|
388
427
|
return [n.id for n in self.nodes]
|
|
389
428
|
|
|
390
|
-
class QueryBuilder:
|
|
429
|
+
class QueryBuilder: # pylint: disable=too-few-public-methods
|
|
391
430
|
"""Builder for creating queries"""
|
|
392
431
|
|
|
393
|
-
def __init__(self,
|
|
394
|
-
self.engine =
|
|
432
|
+
def __init__(self, graphite_engine: GraphiteEngine):
|
|
433
|
+
self.engine = graphite_engine
|
|
395
434
|
|
|
396
435
|
def __getattr__(self, name: str) -> QueryResult:
|
|
397
436
|
"""Allow starting query from node type: engine.User"""
|
|
@@ -402,7 +441,7 @@ class QueryBuilder:
|
|
|
402
441
|
|
|
403
442
|
# =============== MAIN ENGINE ===============
|
|
404
443
|
|
|
405
|
-
class GraphiteEngine:
|
|
444
|
+
class GraphiteEngine: # pylint: disable=too-many-instance-attributes
|
|
406
445
|
"""Main graph database engine"""
|
|
407
446
|
|
|
408
447
|
def __init__(self):
|
|
@@ -472,19 +511,19 @@ class GraphiteEngine:
|
|
|
472
511
|
|
|
473
512
|
# Create values dictionary
|
|
474
513
|
node_values = {}
|
|
475
|
-
for
|
|
514
|
+
for current_field, value in zip(all_fields, values):
|
|
476
515
|
# Convert string dates to date objects
|
|
477
|
-
if
|
|
516
|
+
if current_field.dtype == DataType.DATE and isinstance(value, str):
|
|
478
517
|
try:
|
|
479
518
|
value = datetime.strptime(value, "%Y-%m-%d").date()
|
|
480
|
-
except e:
|
|
481
|
-
raise ValueError(f"
|
|
482
|
-
node_values[
|
|
519
|
+
except Exception as e:
|
|
520
|
+
raise ValueError(f"'{e}' while parsing date string: {value}") from e
|
|
521
|
+
node_values[current_field.name] = value
|
|
483
522
|
|
|
484
|
-
|
|
485
|
-
self.nodes[node_id] =
|
|
486
|
-
self.node_by_type[node_type].append(
|
|
487
|
-
return
|
|
523
|
+
new_node = Node(node_type, node_id, node_values, node_type_obj)
|
|
524
|
+
self.nodes[node_id] = new_node
|
|
525
|
+
self.node_by_type[node_type].append(new_node)
|
|
526
|
+
return new_node
|
|
488
527
|
|
|
489
528
|
def create_relation(self, from_id: str, to_id: str, rel_type: str, *values) -> Relation:
|
|
490
529
|
"""Create a relation instance"""
|
|
@@ -501,21 +540,21 @@ class GraphiteEngine:
|
|
|
501
540
|
|
|
502
541
|
# Create values dictionary
|
|
503
542
|
rel_values = {}
|
|
504
|
-
for i,
|
|
543
|
+
for i, rel_field in enumerate(rel_type_obj.fields):
|
|
505
544
|
if i < len(values):
|
|
506
545
|
value = values[i]
|
|
507
|
-
if
|
|
546
|
+
if rel_field.dtype == DataType.DATE and isinstance(value, str):
|
|
508
547
|
try:
|
|
509
548
|
value = datetime.strptime(value, "%Y-%m-%d").date()
|
|
510
|
-
except e:
|
|
511
|
-
raise ValueError(f"
|
|
512
|
-
rel_values[
|
|
549
|
+
except Exception as e:
|
|
550
|
+
raise ValueError(f"'{e}' while parsing date string: {value}") from e
|
|
551
|
+
rel_values[rel_field.name] = value
|
|
513
552
|
|
|
514
|
-
|
|
515
|
-
self.relations.append(
|
|
516
|
-
self.relations_by_type[rel_type].append(
|
|
517
|
-
self.relations_by_from[from_id].append(
|
|
518
|
-
self.relations_by_to[to_id].append(
|
|
553
|
+
new_relation = Relation(rel_type, from_id, to_id, rel_values, rel_type_obj)
|
|
554
|
+
self.relations.append(new_relation)
|
|
555
|
+
self.relations_by_type[rel_type].append(new_relation)
|
|
556
|
+
self.relations_by_from[from_id].append(new_relation)
|
|
557
|
+
self.relations_by_to[to_id].append(new_relation)
|
|
519
558
|
|
|
520
559
|
# If relation is bidirectional, create reverse automatically
|
|
521
560
|
if rel_type_obj.is_bidirectional:
|
|
@@ -525,7 +564,7 @@ class GraphiteEngine:
|
|
|
525
564
|
self.relations_by_from[to_id].append(reverse_rel)
|
|
526
565
|
self.relations_by_to[from_id].append(reverse_rel)
|
|
527
566
|
|
|
528
|
-
return
|
|
567
|
+
return new_relation
|
|
529
568
|
|
|
530
569
|
# =============== QUERY METHODS ===============
|
|
531
570
|
|
|
@@ -568,7 +607,11 @@ class GraphiteEngine:
|
|
|
568
607
|
# Collect multiline node definition
|
|
569
608
|
node_def = [line]
|
|
570
609
|
i += 1
|
|
571
|
-
while
|
|
610
|
+
while (
|
|
611
|
+
i < len(lines)
|
|
612
|
+
and lines[i].strip()
|
|
613
|
+
and not lines[i].strip().startswith(('node', 'relation'))
|
|
614
|
+
):
|
|
572
615
|
node_def.append(lines[i])
|
|
573
616
|
i += 1
|
|
574
617
|
self.define_node('\n'.join(node_def))
|
|
@@ -577,7 +620,11 @@ class GraphiteEngine:
|
|
|
577
620
|
# Collect multiline relation definition
|
|
578
621
|
rel_def = [line]
|
|
579
622
|
i += 1
|
|
580
|
-
while
|
|
623
|
+
while (
|
|
624
|
+
i < len(lines)
|
|
625
|
+
and lines[i].strip()
|
|
626
|
+
and not lines[i].strip().startswith(('node', 'relation'))
|
|
627
|
+
):
|
|
581
628
|
rel_def.append(lines[i])
|
|
582
629
|
i += 1
|
|
583
630
|
self.define_relation('\n'.join(rel_def))
|
|
@@ -590,7 +637,7 @@ class GraphiteEngine:
|
|
|
590
637
|
|
|
591
638
|
elif '-[' in line and (']->' in line or ']-' in line):
|
|
592
639
|
# Relation instance
|
|
593
|
-
from_id, to_id, rel_type, values,
|
|
640
|
+
from_id, to_id, rel_type, values, _ = self.parser.parse_relation_instance(line)
|
|
594
641
|
self.create_relation(from_id, to_id, rel_type, *values)
|
|
595
642
|
i += 1
|
|
596
643
|
else:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: graphitedb
|
|
3
|
-
Version: 0.1.
|
|
4
|
-
Summary: A clean graph database engine
|
|
3
|
+
Version: 0.1.3
|
|
4
|
+
Summary: A clean, embedded graph database engine for Python.
|
|
5
5
|
Author-email: Mahan Khalili <khalili1388mahan@gmail.com>
|
|
6
6
|
Maintainer-email: Mahan Khalili <khalili1388mahan@gmail.com>
|
|
7
7
|
License-Expression: MIT
|
|
@@ -201,5 +201,3 @@ def example_complete_dsl_loading():
|
|
|
201
201
|
```
|
|
202
202
|
|
|
203
203
|
More examples are available in `example.py` in the GitHub repository.
|
|
204
|
-
::contentReference[oaicite:0]{index=0}
|
|
205
|
-
```
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
graphite/__init__.py,sha256=PDUUAyHqLUImV2yjyblCqjchwBIYHch7MbwCvPrgOGY,22498
|
|
2
|
+
graphitedb-0.1.3.dist-info/licenses/LICENSE,sha256=3n5Zi5QXqsnus7yX3xKY7wKtX7Ios_ofbTxdhSUz0dA,1070
|
|
3
|
+
graphitedb-0.1.3.dist-info/METADATA,sha256=JZBBCG5L6-JwcWNd_HjMTS6872KhITSP23H-CMf6xUQ,4799
|
|
4
|
+
graphitedb-0.1.3.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
5
|
+
graphitedb-0.1.3.dist-info/top_level.txt,sha256=QPrpQuMV9WIM20kS6NN7OWkIzypVFiSTICJLsYYkJ1w,9
|
|
6
|
+
graphitedb-0.1.3.dist-info/RECORD,,
|
__init__.py
DELETED
|
File without changes
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
graphite/__init__.py,sha256=g7eX0jbvKOW0deFbrmj8Y21FQnrRQw5lol0EgeZMbsU,20588
|
|
3
|
-
graphitedb-0.1.2.dist-info/licenses/LICENSE,sha256=3n5Zi5QXqsnus7yX3xKY7wKtX7Ios_ofbTxdhSUz0dA,1070
|
|
4
|
-
graphitedb-0.1.2.dist-info/METADATA,sha256=cTQ7lwk3C9iRFZPZsydjZidCqB2rX6JLMJPFuRBUJ3Q,4820
|
|
5
|
-
graphitedb-0.1.2.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
6
|
-
graphitedb-0.1.2.dist-info/top_level.txt,sha256=3a6k71PT31c_EyE1n54UPWzTLUKEUx8PmMNA4uyTByM,18
|
|
7
|
-
graphitedb-0.1.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|