graphitedb 0.1.3__py3-none-any.whl → 0.2__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/engine.py ADDED
@@ -0,0 +1,564 @@
1
+ """
2
+ Main graph database engine of Graphite
3
+ """
4
+ import json
5
+ import warnings
6
+ import os
7
+ from collections import defaultdict
8
+ from typing import Dict, List, Optional, Any, Union
9
+
10
+ from .exceptions import (
11
+ FileSizeError, InvalidJSONError, InvalidPropertiesError, NotFoundError,
12
+ SafeLoadExtensionError, TooNestedJSONError, ValidationError,
13
+ )
14
+ from .types import Field, NodeType, RelationType
15
+ from .instances import Node, Relation
16
+ from .parser import GraphiteParser
17
+ from .query import QueryBuilder
18
+ from .serialization import GraphiteJSONEncoder, graphite_object_hook
19
+
20
+ class GraphiteEngine: # pylint: disable=too-many-instance-attributes
21
+ """Main graph database engine"""
22
+
23
+ def __init__(self):
24
+ self.node_types: Dict[str, NodeType] = {}
25
+ self.relation_types: Dict[str, RelationType] = {}
26
+ self.nodes: Dict[str, Node] = {}
27
+ self.relations: List[Relation] = []
28
+ self.node_by_type: Dict[str, List[Node]] = defaultdict(list)
29
+ self.relations_by_type: Dict[str, List[Relation]] = defaultdict(list)
30
+ self.relations_by_from: Dict[str, List[Relation]] = defaultdict(list)
31
+ self.relations_by_to: Dict[str, List[Relation]] = defaultdict(list)
32
+ self.parser = GraphiteParser()
33
+ self.query = QueryBuilder(self)
34
+
35
+ # =============== SCHEMA DEFINITION ===============
36
+
37
+ def define_node(self, definition: str):
38
+ """Define a node type from DSL"""
39
+ node_name, fields, parent_name = self.parser.parse_node_definition(definition)
40
+
41
+ parent = None
42
+ if parent_name:
43
+ if parent_name not in self.node_types:
44
+ raise NotFoundError(
45
+ "Parent node type",
46
+ parent_name,
47
+ )
48
+ parent = self.node_types[parent_name]
49
+
50
+ node_type = NodeType(node_name, fields, parent)
51
+ self.node_types[node_name] = node_type
52
+
53
+ def define_relation(self, definition: str):
54
+ """Define a relation type from DSL"""
55
+ (rel_name, from_type, to_type, fields,
56
+ reverse_name, is_bidirectional) = self.parser.parse_relation_definition(definition)
57
+
58
+ # Validate node types exist
59
+ if from_type not in self.node_types:
60
+ raise NotFoundError(
61
+ "Node type",
62
+ from_type,
63
+ )
64
+ if to_type not in self.node_types:
65
+ raise NotFoundError(
66
+ "Node type",
67
+ to_type,
68
+ )
69
+
70
+ rel_type = RelationType(
71
+ rel_name, from_type, to_type,
72
+ fields, reverse_name, is_bidirectional
73
+ )
74
+ self.relation_types[rel_name] = rel_type
75
+
76
+ # Register reverse relation if specified
77
+ if reverse_name:
78
+ reverse_rel = RelationType(
79
+ reverse_name, to_type, from_type,
80
+ fields, rel_name, is_bidirectional
81
+ )
82
+ self.relation_types[reverse_name] = reverse_rel
83
+
84
+ # =============== DATA MANIPULATION ===============
85
+
86
+ def create_node(self, node_type: str, node_id: str, *values) -> Node:
87
+ """Create a node instance"""
88
+ if node_type not in self.node_types:
89
+ raise NotFoundError(
90
+ "Node type",
91
+ node_type
92
+ )
93
+
94
+ node_type_obj = self.node_types[node_type]
95
+ all_fields = node_type_obj.get_all_fields()
96
+
97
+ if len(values) != len(all_fields):
98
+ raise InvalidPropertiesError(
99
+ all_fields,
100
+ len(values)
101
+ )
102
+
103
+ # Create values dictionary
104
+ node_values = {}
105
+ for current_field, value in zip(all_fields, values):
106
+ node_values[current_field.name] = self.parser.parse_field_value(value, current_field)
107
+
108
+ new_node = Node(node_type, node_id, node_values, node_type_obj)
109
+ self.nodes[node_id] = new_node
110
+ self.node_by_type[node_type].append(new_node)
111
+ return new_node
112
+
113
+ def create_relation(self, from_id: str, to_id: str, rel_type: str, *values) -> Relation:
114
+ """Create a relation instance"""
115
+ if rel_type not in self.relation_types:
116
+ raise NotFoundError(
117
+ "Relation type",
118
+ rel_type,
119
+ )
120
+
121
+ rel_type_obj = self.relation_types[rel_type]
122
+
123
+ # Check if nodes exist
124
+ if from_id not in self.nodes:
125
+ raise NotFoundError(
126
+ "Node",
127
+ from_id,
128
+ )
129
+ if to_id not in self.nodes:
130
+ raise NotFoundError(
131
+ "Node",
132
+ to_id
133
+ )
134
+
135
+ if len(values) != len(rel_type_obj.fields):
136
+ raise InvalidPropertiesError(
137
+ rel_type_obj.fields,
138
+ len(values)
139
+ )
140
+
141
+ # Create values dictionary
142
+ rel_values = {}
143
+ for i, rel_field in enumerate(rel_type_obj.fields):
144
+ rel_values[rel_field.name] = self.parser.parse_value(values[i])
145
+
146
+ new_relation = Relation(rel_type, from_id, to_id, rel_values, rel_type_obj)
147
+ self.relations.append(new_relation)
148
+ self.relations_by_type[rel_type].append(new_relation)
149
+ self.relations_by_from[from_id].append(new_relation)
150
+ self.relations_by_to[to_id].append(new_relation)
151
+
152
+ # If relation is bidirectional, create reverse automatically
153
+ if rel_type_obj.is_bidirectional:
154
+ reverse_rel = Relation(rel_type, to_id, from_id, rel_values, rel_type_obj)
155
+ self.relations.append(reverse_rel)
156
+ self.relations_by_type[rel_type].append(reverse_rel)
157
+ self.relations_by_from[to_id].append(reverse_rel)
158
+ self.relations_by_to[from_id].append(reverse_rel)
159
+
160
+ return new_relation
161
+
162
+ # =============== QUERY METHODS ===============
163
+
164
+ def get_node(self, node_id: str) -> Optional[Node]:
165
+ """Get node by ID"""
166
+ return self.nodes.get(node_id)
167
+
168
+ def get_nodes_of_type(self, node_type: str, with_subtypes: bool = True) -> List[Node]:
169
+ """Get all nodes of a specific type"""
170
+ nodes: List[Node] = self.node_by_type.get(node_type, [])
171
+ if with_subtypes:
172
+ for ntype in self.node_types.values():
173
+ if ntype.parent and ntype.parent.name == node_type:
174
+ for new_node in self.get_nodes_of_type(ntype.name):
175
+ if new_node not in nodes:
176
+ nodes.append(new_node)
177
+ return nodes
178
+
179
+ def get_relations_from(self, node_id: str, rel_type: str = None) -> List[Relation]:
180
+ """Get relations from a node"""
181
+ all_rels = self.relations_by_from.get(node_id, [])
182
+ if rel_type:
183
+ return [r for r in all_rels if r.type_name == rel_type]
184
+ return all_rels
185
+
186
+ def get_relations_to(self, node_id: str, rel_type: str = None) -> List[Relation]:
187
+ """Get relations to a node"""
188
+ all_rels = self.relations_by_to.get(node_id, [])
189
+ if rel_type:
190
+ return [r for r in all_rels if r.type_name == rel_type]
191
+ return all_rels
192
+
193
+ # =============== BULK LOADING ===============
194
+
195
+ def load_dsl(self, dsl: str):
196
+ """Load Graphite DSL"""
197
+ lines = dsl.strip().split('\n')
198
+ i = 0
199
+
200
+ while i < len(lines):
201
+ line = lines[i].strip()
202
+ if not line or line.startswith('#'):
203
+ i += 1
204
+ continue
205
+
206
+ if line.startswith('node'):
207
+ # Collect multiline node definition
208
+ node_def = [line]
209
+ i += 1
210
+ while (
211
+ i < len(lines)
212
+ and lines[i].strip()
213
+ and not lines[i].strip().startswith(('node', 'relation'))
214
+ ):
215
+ node_def.append(lines[i])
216
+ i += 1
217
+ self.define_node('\n'.join(node_def))
218
+
219
+ elif line.startswith('relation'):
220
+ # Collect multiline relation definition
221
+ rel_def = [line]
222
+ i += 1
223
+ while (
224
+ i < len(lines)
225
+ and lines[i].strip()
226
+ and not lines[i].strip().startswith(('node', 'relation'))
227
+ ):
228
+ rel_def.append(lines[i])
229
+ i += 1
230
+ self.define_relation('\n'.join(rel_def))
231
+
232
+ elif '[' not in line:
233
+ # Node instance
234
+ node_type, node_id, values = self.parser.parse_node_instance(line)
235
+ self.create_node(node_type, node_id, *values)
236
+ i += 1
237
+
238
+ elif '-[' in line and (']->' in line or ']-' in line):
239
+ # Relation instance
240
+ from_id, to_id, rel_type, values, _ = self.parser.parse_relation_instance(line)
241
+ self.create_relation(from_id, to_id, rel_type, *values)
242
+ i += 1
243
+ else:
244
+ i += 1
245
+
246
+ # =============== PERSISTENCE ===============
247
+
248
+ @staticmethod
249
+ def _graphite_object_hook(dct: Dict[str, Any]) -> Any:
250
+ """Object hook for decoding Graphite objects from JSON."""
251
+ return graphite_object_hook(dct)
252
+
253
+ def save(self, filename: str):
254
+ """Save database to file using JSON"""
255
+ data = self._build_save_payload()
256
+ with open(filename, 'w', encoding='utf-8') as f:
257
+ # noinspection PyTypeChecker
258
+ json.dump(data, f, cls=GraphiteJSONEncoder, indent=2, ensure_ascii=False)
259
+
260
+ def load_safe(
261
+ self, filename: str, max_size_mb: Union[int, float] = 100, validate_schema: bool = True
262
+ ) -> None:
263
+ """
264
+ Safely load database with security checks
265
+
266
+ Args:
267
+ filename: File to load
268
+ max_size_mb: Maximum allowed file size in MB
269
+ validate_schema: Whether to validate schema consistency
270
+
271
+ Returns:
272
+ True if loaded successfully, False otherwise
273
+ """
274
+ # Check file size
275
+ file_size = os.path.getsize(filename)
276
+ if file_size > max_size_mb * 1024 * 1024:
277
+ raise FileSizeError(
278
+ file_size / 1024 / 1024,
279
+ max_size_mb
280
+ )
281
+
282
+ # Check file extension
283
+ if not filename.lower().endswith('.json'):
284
+ raise SafeLoadExtensionError()
285
+
286
+ try:
287
+ with open(filename, 'r', encoding='utf-8') as f:
288
+ data = json.load(f, object_hook=self._graphite_object_hook)
289
+ except json.JSONDecodeError as e:
290
+ raise InvalidJSONError() from e
291
+ except RecursionError as exc:
292
+ raise TooNestedJSONError() from exc
293
+
294
+ # Validate structure
295
+ if validate_schema:
296
+ self._validate_loaded_data(data)
297
+
298
+ # Load normally
299
+ self._load_from_dict(data)
300
+
301
+ @staticmethod
302
+ # pylint: disable=too-many-branches
303
+ def _validate_loaded_data(data: Dict[str, Any]):
304
+ """Validate loaded data for consistency"""
305
+ if not isinstance(data, dict):
306
+ raise ValidationError(
307
+ "Loaded data must be a dictionary",
308
+ "data",
309
+ str(type(data))
310
+ )
311
+
312
+ required_keys = ['version', 'node_types', 'relation_types', 'nodes']
313
+ for key in required_keys:
314
+ if key not in data:
315
+ raise ValidationError(
316
+ f"Missing required key {key}",
317
+ key,
318
+ "'Missing'"
319
+ )
320
+
321
+ if not isinstance(data.get('node_types'), list):
322
+ raise ValidationError(
323
+ "node_types must be a list",
324
+ "node_types",
325
+ str(type(data.get('node_types')))
326
+ )
327
+ if not isinstance(data.get('relation_types'), list):
328
+ raise ValidationError(
329
+ "relation_types must be a list",
330
+ "relation_types",
331
+ str(type(data.get('relation_types')))
332
+ )
333
+ if not isinstance(data.get('nodes'), list):
334
+ raise ValidationError(
335
+ "nodes must be a list",
336
+ "nodes",
337
+ str(type(data.get('nodes')))
338
+ )
339
+ if 'relations' in data and not isinstance(data.get('relations'), list):
340
+ raise ValidationError(
341
+ "relations must be a list",
342
+ "relations",
343
+ str(type(data.get('relations')))
344
+ )
345
+
346
+ # Check for unexpected keys
347
+ allowed_keys = {
348
+ 'version', 'node_types', 'relation_types', 'nodes', 'relations',
349
+ 'node_by_type', 'relations_by_type', 'relations_by_from', 'relations_by_to'
350
+ }
351
+ for key in data.keys():
352
+ if key not in allowed_keys:
353
+ warnings.warn(f"Unexpected key in data: {key}", UserWarning)
354
+
355
+ # Validate nodes reference existing types
356
+ node_type_names = set()
357
+ for node_type in data.get('node_types', []):
358
+ if isinstance(node_type, NodeType):
359
+ node_type_names.add(node_type.name)
360
+ elif isinstance(node_type, dict) and 'name' in node_type:
361
+ node_type_names.add(node_type['name'])
362
+
363
+ for check_node in data.get('nodes', []):
364
+ type_name = check_node.type_name if isinstance(check_node, Node) else check_node.get('type_name')
365
+ if type_name not in node_type_names:
366
+ raise NotFoundError(
367
+ "Node type",
368
+ type_name,
369
+ )
370
+
371
+ # pylint: disable=too-many-branches, too-many-locals
372
+ def _load_from_dict(self, data: Dict[str, Any]):
373
+ """Internal method to load from dictionary (used by both load and load_safe)"""
374
+ # Clear existing data
375
+ self.clear()
376
+
377
+ node_types_data = data.get('node_types', [])
378
+ relation_types_data = data.get('relation_types', [])
379
+ nodes_data = data.get('nodes', [])
380
+ relations_data = data.get('relations', [])
381
+
382
+ # Restore node types
383
+ for nt_dict in node_types_data:
384
+ if isinstance(nt_dict, NodeType):
385
+ nt = nt_dict
386
+ else:
387
+ # Convert from dict if needed
388
+ fields: List[Field] = list(map(
389
+ lambda fld: Field(fld["name"], fld["dtype"], fld["default"]),
390
+ nt_dict.get("fields", [])
391
+ ))
392
+ nt = NodeType(
393
+ name=nt_dict['name'],
394
+ fields=fields,
395
+ parent=None # Will be restored later
396
+ )
397
+ self.node_types[nt.name] = nt
398
+
399
+ # Restore parent references for node types
400
+ for nt in node_types_data:
401
+ if isinstance(nt, dict):
402
+ parent_name = nt.get('parent')
403
+ name = nt.get('name')
404
+ else:
405
+ parent_name = nt.parent.name if nt.parent else None
406
+ name = nt.name
407
+ if isinstance(parent_name, dict):
408
+ parent_name = parent_name["name"]
409
+ if parent_name and parent_name in self.node_types and name in self.node_types:
410
+ self.node_types[name].parent = self.node_types[parent_name]
411
+
412
+ # Restore relation types
413
+ for rt_dict in relation_types_data:
414
+ if isinstance(rt_dict, RelationType):
415
+ rt = rt_dict
416
+ else:
417
+ rt = RelationType(
418
+ name=rt_dict['name'],
419
+ from_type=rt_dict['from_type'],
420
+ to_type=rt_dict['to_type'],
421
+ fields=rt_dict.get('fields', []),
422
+ reverse_name=rt_dict.get('reverse_name'),
423
+ is_bidirectional=rt_dict.get('is_bidirectional', False)
424
+ )
425
+ self.relation_types[rt.name] = rt
426
+
427
+ # Restore nodes
428
+ for node_data in nodes_data:
429
+ if isinstance(node_data, Node):
430
+ loading_node = node_data
431
+ else:
432
+ loading_node = Node(
433
+ type_name=node_data['type_name'],
434
+ id=node_data['id'],
435
+ values=node_data['values'],
436
+ type_ref=None
437
+ )
438
+
439
+ # Restore type reference
440
+ if loading_node.type_name in self.node_types:
441
+ loading_node.type_ref = self.node_types[loading_node.type_name]
442
+
443
+ self.nodes[loading_node.id] = loading_node
444
+
445
+ # Restore relations
446
+ for rel_data in relations_data:
447
+ if isinstance(rel_data, Relation):
448
+ rel = rel_data
449
+ else:
450
+ rel = Relation(
451
+ type_name=rel_data['type_name'],
452
+ from_node=rel_data['from_node'],
453
+ to_node=rel_data['to_node'],
454
+ values=rel_data['values'],
455
+ type_ref=None
456
+ )
457
+
458
+ # Restore type reference
459
+ if rel.type_name in self.relation_types:
460
+ rel.type_ref = self.relation_types[rel.type_name]
461
+
462
+ self.relations.append(rel)
463
+
464
+ # Rebuild all indexes
465
+ self._rebuild_all_indexes()
466
+
467
+ def _build_save_payload(self) -> Dict[str, Any]:
468
+ """Build a JSON-serializable payload for persistence."""
469
+ return {
470
+ "version" : "1.0",
471
+ "node_types" : list(self.node_types.values()),
472
+ "relation_types" : list(self.relation_types.values()),
473
+ "nodes" : list(self.nodes.values()),
474
+ "relations" : list(self.relations),
475
+ "node_by_type" : dict(self.node_by_type.items()),
476
+ "relations_by_type": dict(self.relations_by_type.items()),
477
+ "relations_by_from": dict(self.relations_by_from.items()),
478
+ "relations_by_to" : dict(self.relations_by_to.items()),
479
+ }
480
+
481
+ def _rebuild_all_indexes(self):
482
+ self._rebuild_node_by_type()
483
+ self._rebuild_relations_indexes()
484
+
485
+ def load(self, filename: str, safe_mode: bool = True) -> None:
486
+ """
487
+ Load database from file
488
+
489
+ Args:
490
+ filename: File to load
491
+ safe_mode: If True, use safe loading with validation (default: True)
492
+ """
493
+ if safe_mode:
494
+ self.load_safe(filename)
495
+ return
496
+
497
+ # Legacy unsafe loading (for backward compatibility)
498
+ warnings.warn(
499
+ "Unsafe loading mode will be deprecated in next versions. Use safe_mode=True for security. "
500
+ "You can use 'graphite.Migration.convert_pickle_to_json()' to update your database.",
501
+ PendingDeprecationWarning
502
+ )
503
+ self._load_unsafe(filename)
504
+
505
+ def _load_unsafe(self, filename: str):
506
+ """Legacy unsafe loading (kept for compatibility)"""
507
+ with open(filename, 'r', encoding='utf-8') as f:
508
+ data = json.load(f, object_hook=self._graphite_object_hook)
509
+ self._load_from_dict(data)
510
+
511
+ def _rebuild_node_by_type(self):
512
+ """Rebuild node_by_type index"""
513
+ self.node_by_type = defaultdict(list)
514
+ for node_instance in self.nodes.values():
515
+ self.node_by_type[node_instance.type_name].append(node_instance)
516
+
517
+ def _rebuild_relations_indexes(self):
518
+ """Rebuild all relation indexes"""
519
+ self.relations_by_type = defaultdict(list)
520
+ self.relations_by_from = defaultdict(list)
521
+ self.relations_by_to = defaultdict(list)
522
+
523
+ for rel in self.relations:
524
+ self.relations_by_type[rel.type_name].append(rel)
525
+ self.relations_by_from[rel.from_node].append(rel)
526
+ self.relations_by_to[rel.to_node].append(rel)
527
+
528
+ def _rebuild_remaining_indexes(self):
529
+ """Rebuild indexes that might not be in the saved data"""
530
+ # Ensure relations_by_from and relations_by_to are built
531
+ if not self.relations_by_from or not self.relations_by_to:
532
+ self.relations_by_from = defaultdict(list)
533
+ self.relations_by_to = defaultdict(list)
534
+ for rel in self.relations:
535
+ self.relations_by_from[rel.from_node].append(rel)
536
+ self.relations_by_to[rel.to_node].append(rel)
537
+
538
+ # =============== UTILITY METHODS ===============
539
+
540
+ def clear(self):
541
+ """Clear all data"""
542
+ self.node_types.clear()
543
+ self.relation_types.clear()
544
+ self.nodes.clear()
545
+ self.relations.clear()
546
+ self.node_by_type.clear()
547
+ self.relations_by_type.clear()
548
+ self.relations_by_from.clear()
549
+ self.relations_by_to.clear()
550
+
551
+ def stats(self) -> Dict[str, Any]:
552
+ """Get database statistics"""
553
+ return {
554
+ 'node_types' : len(self.node_types),
555
+ 'relation_types': len(self.relation_types),
556
+ 'nodes' : len(self.nodes),
557
+ 'relations' : len(self.relations),
558
+ }
559
+
560
+ # =============== SYNTAX SUGAR ===============
561
+
562
+ def parse(self, data: str):
563
+ """Parse data into nodes and relations (structure or data)"""
564
+ self.load_dsl(data)
graphite/exceptions.py ADDED
@@ -0,0 +1,89 @@
1
+ """
2
+ Possible exceptions in Graphite
3
+ """
4
+ from typing import Any
5
+
6
+ from src.graphite.types import Field
7
+
8
+ class GraphiteError(Exception):
9
+ """Base exception for all Graphite errors"""
10
+
11
+ class SchemaError(GraphiteError):
12
+ """Schema-related errors"""
13
+ def __init__(self, message: str, line: int = None, column: int = None):
14
+ self.line = line
15
+ self.column = column
16
+ super().__init__(message)
17
+
18
+ class ValidationError(GraphiteError):
19
+ """Data validation errors"""
20
+ def __init__(self, message: str, field: str = None, value: Any = None):
21
+ self.field = field
22
+ self.value = value
23
+ super().__init__(message)
24
+
25
+ class QueryError(GraphiteError):
26
+ """Query parsing/execution errors"""
27
+
28
+ class NotFoundError(GraphiteError):
29
+ """Resource not found errors"""
30
+ def __init__(self, resource_type: str, resource_id: str):
31
+ self.resource_type = resource_type
32
+ self.resource_id = resource_id
33
+ super().__init__(f"{resource_type} '{resource_id}' not found")
34
+
35
+ class InvalidPropertiesError(GraphiteError):
36
+ """Invalid properties error"""
37
+ def __init__(self, valid_properties: list[Field], got_count: int):
38
+ self.valid_properties = valid_properties
39
+ self.got_count = got_count
40
+ # pylint: disable=consider-using-f-string
41
+ super().__init__(
42
+ "Expected {} properties ({}), got {}".format(len(valid_properties), valid_properties, got_count)
43
+ )
44
+
45
+ class FieldError(GraphiteError):
46
+ """Field error"""
47
+ def __init__(self, field: Field, value: Any):
48
+ self.field = field
49
+ self.value = value
50
+ super().__init__(
51
+ f"Field {field.name} must be '{str(field.dtype)}', got {type(value)} ({value})"
52
+ )
53
+
54
+ class DateParseError(GraphiteError):
55
+ """Date parsing error"""
56
+ def __init__(self, date_str: str, expected_format: str = "%Y-%m-%d"):
57
+ self.date_str = date_str
58
+ self.expected_format = expected_format
59
+ super().__init__(
60
+ f"Failed to parse date '{date_str}'. Expected format: {expected_format}"
61
+ )
62
+
63
+ class FileSizeError(GraphiteError):
64
+ """File size error"""
65
+ def __init__(self, file_size: float, max_size: int):
66
+ self.file_size = file_size
67
+ self.max_size = max_size
68
+ super().__init__(f"File is too large: {file_size:.1f}MB > {max_size}MB limit")
69
+
70
+ class SafeLoadExtensionError(GraphiteError):
71
+ """Safe-load extension error"""
72
+ def __init__(self):
73
+ super().__init__("Only '.json' files are allowed for safe loading")
74
+
75
+ class InvalidJSONError(GraphiteError):
76
+ """Invalid JSON error"""
77
+ def __init__(self):
78
+ super().__init__("Invalid JSON")
79
+
80
+ class TooNestedJSONError(GraphiteError):
81
+ """Too Nested JSON error"""
82
+ def __init__(self):
83
+ super().__init__("JSON structure is too nested")
84
+
85
+ class ConditionError(QueryError):
86
+ """Condition error"""
87
+ def __init__(self, condition: str):
88
+ self.condition = condition
89
+ super().__init__(f"Invalid condition string: {condition}")
graphite/instances.py ADDED
@@ -0,0 +1,50 @@
1
+ """
2
+ Node and relation instance objects
3
+ """
4
+ from __future__ import annotations
5
+ from dataclasses import dataclass
6
+ from typing import Dict, Any, Optional
7
+ from .types import NodeType, RelationType
8
+
9
+ @dataclass
10
+ class Node:
11
+ """
12
+ A node in database. Has a base type, id, and properties from base type (and it's parent
13
+ type recursively).
14
+ """
15
+ type_name: str
16
+ id: str
17
+ values: Dict[str, Any]
18
+ type_ref: Optional[NodeType] = None
19
+
20
+ def get(self, field_name: str) -> Any:
21
+ """Get a field from this node."""
22
+ return self.values.get(field_name)
23
+
24
+ def __getitem__(self, key):
25
+ return self.get(key)
26
+
27
+ def __repr__(self):
28
+ return f"Node({self.type_name}:{self.id})"
29
+
30
+ @dataclass
31
+ class Relation:
32
+ """
33
+ A relation between two nodes in database. Has a base type, source and target node IDs,
34
+ and properties from base type.
35
+ """
36
+ type_name: str
37
+ from_node: str # node id
38
+ to_node: str # node id
39
+ values: Dict[str, Any]
40
+ type_ref: Optional[RelationType] = None
41
+
42
+ def get(self, field_name: str) -> Any:
43
+ """Get a field from this relation."""
44
+ return self.values.get(field_name)
45
+
46
+ def __getitem__(self, key):
47
+ return self.get(key)
48
+
49
+ def __repr__(self):
50
+ return f"Relation({self.type_name}:{self.from_node}->{self.to_node})"