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/migration.py ADDED
@@ -0,0 +1,104 @@
1
+ """
2
+ Helper module to update Graphite databases and handle other migrations
3
+ """
4
+ import warnings
5
+ import pickle
6
+ import os
7
+ import glob
8
+ from .engine import GraphiteEngine
9
+ from .utils import SecurityWarning
10
+
11
+ class Migration:
12
+ """Utility for migrating from older versions"""
13
+
14
+ @staticmethod
15
+ def convert_pickle_to_json(
16
+ pickle_file: str, json_file: str, delete_original: bool = False
17
+ ) -> bool:
18
+ """
19
+ Convert a pickle file to JSON format
20
+
21
+ Args:
22
+ pickle_file: Path to existing pickle file
23
+ json_file: Path for new JSON file
24
+ delete_original: Whether to delete pickle file after conversion
25
+
26
+ Returns:
27
+ True if successful, False otherwise
28
+ """
29
+ try:
30
+ warnings.warn(
31
+ "'convert_pickle_to_json' will be deprecated because of security reasons. "
32
+ "Please convert your pickle files to JSON and don't use old files anymore.",
33
+ PendingDeprecationWarning
34
+ )
35
+
36
+ # Load from pickle (with safety warnings)
37
+ warnings.warn(
38
+ f"Loading from pickle file: {pickle_file}. "
39
+ "Pickle files can contain malicious code. "
40
+ "Only load files from trusted sources.",
41
+ SecurityWarning
42
+ )
43
+
44
+ with open(pickle_file, 'rb') as f:
45
+ data = pickle.load(f)
46
+
47
+ # Create a new engine with the loaded data
48
+ converter_engine = GraphiteEngine()
49
+
50
+ # Restore data structures
51
+ converter_engine.node_types = data['node_types']
52
+ converter_engine.relation_types = data['relation_types']
53
+ converter_engine.nodes = data['nodes']
54
+ converter_engine.relations = data['relations']
55
+ converter_engine.node_by_type = data['node_by_type']
56
+ converter_engine.relations_by_type = data['relations_by_type']
57
+ converter_engine.relations_by_from = data['relations_by_from']
58
+ converter_engine.relations_by_to = data['relations_by_to']
59
+
60
+ # Save to JSON
61
+ converter_engine.save(json_file)
62
+
63
+ if delete_original:
64
+ os.unlink(pickle_file)
65
+ print(f"Converted {pickle_file} to {json_file} and deleted original")
66
+ else:
67
+ print(f"Converted {pickle_file} to {json_file}")
68
+
69
+ return True
70
+
71
+ except Exception as e: # pylint: disable=broad-exception-caught
72
+ print(f"Conversion failed: {e}")
73
+ return False
74
+
75
+ @staticmethod
76
+ def detect_pickle_and_convert_to_json(
77
+ directory: str, pattern: str = "*.db", delete_originals: bool = False
78
+ ):
79
+ """
80
+ Find and convert all pickle files in a directory
81
+
82
+ Args:
83
+ directory: Directory to scan
84
+ pattern: File pattern to match (default: *.db)
85
+ delete_originals: Whether to delete pickle files after conversion
86
+ """
87
+ for pickle_file in glob.glob(os.path.join(directory, pattern)):
88
+ if pickle_file.endswith('.json'):
89
+ continue
90
+
91
+ json_file = pickle_file.rsplit('.', 1)[0] + '.json'
92
+
93
+ try:
94
+ # Quick check if it's a pickle file
95
+ with open(pickle_file, 'rb') as f:
96
+ # Try to read pickle header
97
+ header = f.read(4)
98
+ if header == b'\x80\x04' or header.startswith(b'\x80'): # Pickle protocol 4
99
+ Migration.convert_pickle_to_json(
100
+ pickle_file, json_file, delete_originals
101
+ )
102
+ except Exception as e: # pylint: disable=broad-exception-caught
103
+ # Not a pickle file or can't read
104
+ print(f"File '{pickle_file}' skipped: {e}")
graphite/parser.py ADDED
@@ -0,0 +1,228 @@
1
+ """
2
+ Parser for Graphite DSL
3
+ """
4
+ import re
5
+ from datetime import date, datetime
6
+ from typing import Any, List, Optional, Tuple, Union
7
+
8
+ from .exceptions import DateParseError, FieldError, NotFoundError, SchemaError
9
+ from .types import DataType, Field
10
+
11
+ class GraphiteParser:
12
+ """Parser for Graphite DSL"""
13
+
14
+ def parse_field_value(self, value: Any, field: Field) -> Any:
15
+ """
16
+ Parse a raw value for a field (node or relation) and return it.
17
+
18
+ **Note:** Value will be validated with field information, use ``parse_value()``
19
+ to ignore validation.
20
+ """
21
+ value = self.parse_value(value)
22
+ return self.validate_field_value(value, field)
23
+
24
+ @staticmethod
25
+ # pylint: disable=broad-exception-caught, too-many-branches
26
+ def validate_field_value(value: Any, field: Field) -> Any:
27
+ """
28
+ Converts given value to field's data type. Raises ``FieldError`` at fail.
29
+ """
30
+ if value is None:
31
+ return None
32
+ exc = None
33
+ if field.dtype == DataType.STRING and not isinstance(value, str):
34
+ try:
35
+ value = str(exc)
36
+ except Exception as e:
37
+ exc = e
38
+ elif field.dtype == DataType.INT and not isinstance(value, int):
39
+ try:
40
+ value = int(value)
41
+ except Exception as e:
42
+ exc = e
43
+ elif field.dtype == DataType.DATE and not isinstance(value, (datetime, date)):
44
+ try:
45
+ value = datetime.strptime(value, "%Y-%m-%d").date()
46
+ except Exception as e:
47
+ exc = e
48
+ elif field.dtype == DataType.FLOAT and not isinstance(value, float):
49
+ try:
50
+ value = float(value)
51
+ except Exception as e:
52
+ exc = e
53
+ elif field.dtype == DataType.BOOL and not isinstance(value, bool):
54
+ if isinstance(value, str):
55
+ value = value.lower() == "true"
56
+ else:
57
+ try:
58
+ value = bool(value)
59
+ except Exception as e:
60
+ exc = e
61
+ elif field.dtype not in DataType:
62
+ raise NotFoundError(
63
+ "Data type",
64
+ str(field.dtype)
65
+ )
66
+ if exc is not None:
67
+ raise FieldError(
68
+ field,
69
+ value
70
+ ) from exc
71
+ return value
72
+
73
+ @staticmethod
74
+ # pylint: disable=too-many-return-statements
75
+ def parse_value(value: Any) -> Any:
76
+ """Parses a raw value (usually ``str``) into correct type."""
77
+ if not isinstance(value, str):
78
+ return value
79
+ value = value.strip()
80
+ if value.startswith('"') and value.endswith('"'):
81
+ return value[1:-1]
82
+ if value.replace('-', '').isdigit() and value.count("-") == 2: # Date-like
83
+ try:
84
+ return datetime.strptime(value, '%Y-%m-%d').date()
85
+ except ValueError as e:
86
+ raise DateParseError(value) from e
87
+ if value.isdigit() or (value.startswith('-') and value[1:].isdigit()):
88
+ return int(value)
89
+ if value.replace('.', '').isdigit() and value.count('.') == 1:
90
+ return float(value)
91
+ if value.lower() in ('true', 'false'):
92
+ return value.lower() == 'true'
93
+ return value
94
+
95
+ @staticmethod
96
+ def parse_node_definition(line: str) -> Tuple[str, List[Field], str]:
97
+ """Parse node type definition: 'node Person\nname: string\nage: int'"""
98
+ lines = line.strip().split('\n')
99
+ first_line = lines[0].strip()
100
+
101
+ # Parse inheritance
102
+ if ' from ' in first_line:
103
+ parts = first_line.split(' from ')
104
+ node_name = parts[0].replace('node', '').strip()
105
+ parent = parts[1].strip()
106
+ fields_start = 1
107
+ else:
108
+ node_name = first_line.replace('node', '').strip()
109
+ parent = None
110
+ fields_start = 1
111
+
112
+ fields = []
113
+ for field_line in lines[fields_start:]:
114
+ field_line = field_line.strip()
115
+ if not field_line:
116
+ continue
117
+ name_type = field_line.split(':')
118
+ if len(name_type) == 2:
119
+ name = name_type[0].strip()
120
+ dtype_str = name_type[1].strip()
121
+ dtype = DataType(dtype_str)
122
+ fields.append(Field(name, dtype))
123
+
124
+ return node_name, fields, parent
125
+
126
+ # pylint: disable=too-many-locals
127
+ @staticmethod
128
+ def parse_relation_definition(line: str) -> Tuple[str, str, str, List[Field], Optional[str], bool]:
129
+ """Parse relation definition"""
130
+ lines = line.strip().split('\n')
131
+ first_line = lines[0].strip()
132
+
133
+ # Check for 'both' keyword
134
+ is_bidirectional = ' both' in first_line
135
+ if is_bidirectional:
136
+ first_line = first_line.replace(' both', '')
137
+
138
+ # Parse reverse
139
+ reverse_name = None
140
+ if ' reverse ' in first_line:
141
+ parts = first_line.split(' reverse ')
142
+ relation_name = parts[0].replace('relation', '').strip()
143
+ reverse_name = parts[1].strip()
144
+ else:
145
+ relation_name = first_line.replace('relation', '').strip()
146
+
147
+ # Parse participants
148
+ participants_line = lines[1].strip()
149
+ if '->' in participants_line:
150
+ from_to = participants_line.split('->')
151
+ from_type = from_to[0].strip()
152
+ to_type = from_to[1].strip()
153
+ elif '-' in participants_line:
154
+ parts = participants_line.split('-')
155
+ from_type = parts[0].strip()
156
+ to_type = parts[2].strip() if len(parts) > 2 else parts[1].strip()
157
+ else:
158
+ raise SchemaError(f"Invalid relation type format: {participants_line}")
159
+
160
+ # Parse fields
161
+ fields = []
162
+ for field_line in lines[2:]:
163
+ field_line = field_line.strip()
164
+ if not field_line:
165
+ continue
166
+ name_type = field_line.split(':')
167
+ if len(name_type) == 2:
168
+ name = name_type[0].strip()
169
+ dtype_str = name_type[1].strip()
170
+ dtype = DataType(dtype_str)
171
+ fields.append(Field(name, dtype))
172
+
173
+ return relation_name, from_type, to_type, fields, reverse_name, is_bidirectional
174
+
175
+ @staticmethod
176
+ def parse_node_instance(line: str) -> Tuple[str, str, List[Any]]:
177
+ """Parse node instance: 'User, user_1, "Joe Doe", 32, "joe4030"'"""
178
+ # Handle quoted strings
179
+ parts = []
180
+ current = ''
181
+ in_quotes = False
182
+ for char in line:
183
+ if char == '"':
184
+ in_quotes = not in_quotes
185
+ current += char
186
+ elif char == ',' and not in_quotes:
187
+ parts.append(current.strip())
188
+ current = ''
189
+ else:
190
+ current += char
191
+ if current:
192
+ parts.append(current.strip())
193
+
194
+ node_type = parts[0].strip()
195
+ node_id = parts[1].strip()
196
+ values = list(map(GraphiteParser.parse_value, parts[2:]))
197
+
198
+ return node_type, node_id, values
199
+
200
+ @staticmethod
201
+ def parse_relation_instance(
202
+ line: str
203
+ ) -> tuple[Union[str, Any], Union[str, Any], Any, list[Any], str]:
204
+ """Parse relation instance: 'user_1 -[OWNER, 2000-10-04]-> notebook'"""
205
+ # Extract relation type and attributes
206
+ pattern = r'(\w+)\s*(-\[([^\]]+)\]\s*[->-]\s*|\s*[->-]\s*\[([^\]]+)\]\s*->\s*)(\w+)'
207
+ match = re.search(pattern, line)
208
+ if not match:
209
+ raise SchemaError(f"Invalid relation format: {line}")
210
+
211
+ from_node = match.group(1)
212
+ to_node = match.group(5)
213
+
214
+ # Get relation type and attributes
215
+ rel_part = match.group(3) or match.group(4)
216
+ rel_parts = [p.strip() for p in rel_part.split(',')]
217
+ rel_type = rel_parts[0]
218
+ attributes = list(map(GraphiteParser.parse_value, rel_parts[1:]) if len(rel_parts) > 1 else [])
219
+
220
+ # Parse direction
221
+ if '->' in line:
222
+ direction = 'forward'
223
+ elif '-[' in line and ']-' in line:
224
+ direction = 'bidirectional'
225
+ else:
226
+ direction = 'forward'
227
+
228
+ return from_node, to_node, rel_type, attributes, direction
graphite/query.py ADDED
@@ -0,0 +1,199 @@
1
+ """
2
+ Query engine and object for Graphite
3
+ """
4
+ from datetime import date, datetime
5
+ from typing import TYPE_CHECKING, List, Callable, Optional, Union
6
+
7
+ from .instances import Node, Relation
8
+ from .types import RelationType
9
+ from .exceptions import ConditionError, DateParseError, NotFoundError
10
+
11
+ if TYPE_CHECKING:
12
+ from .engine import GraphiteEngine
13
+
14
+ class QueryResult:
15
+ """Represents a query result that can be chained"""
16
+
17
+ def __init__(
18
+ self, graph_engine: 'GraphiteEngine', nodes: List[Node], edges: List[Relation] = None
19
+ ):
20
+ self.engine = graph_engine
21
+ self.nodes = nodes
22
+ self.edges = edges or []
23
+ self.current_relation: Optional[RelationType] = None
24
+ self.direction: str = 'outgoing'
25
+
26
+ def where(self, condition: Union[str, Callable]):
27
+ """Filter nodes based on condition"""
28
+ filtered_nodes = []
29
+
30
+ if callable(condition):
31
+ # Lambda function
32
+ for processing_node in self.nodes:
33
+ try:
34
+ if condition(processing_node):
35
+ filtered_nodes.append(processing_node)
36
+ except Exception as e:
37
+ raise ConditionError(str(condition)) from e
38
+ else:
39
+ # String condition like "age > 18"
40
+ for processing_node in self.nodes:
41
+ if self._evaluate_condition(processing_node, condition):
42
+ filtered_nodes.append(processing_node)
43
+
44
+ return QueryResult(self.engine, filtered_nodes, self.edges)
45
+
46
+ # pylint: disable=too-many-branches
47
+ @staticmethod
48
+ def _evaluate_condition(target_node: Node, condition: str) -> bool:
49
+ """Evaluate a condition string on a node"""
50
+ # Simple condition parser
51
+ ops = ['>=', '<=', '!=', '==', '>', '<', '=']
52
+
53
+ for op in ops:
54
+ if op in condition:
55
+ left, right = condition.split(op)
56
+ left = left.strip()
57
+ right = right.strip()
58
+
59
+ # Get value from node
60
+ node_value = target_node.get(left)
61
+ if node_value is None:
62
+ return False
63
+
64
+ # Parse right side
65
+ if right[0] in ('"', "'") and right[-1] in ('"', "'"):
66
+ right_value = right[1:-1]
67
+ elif right.isdigit():
68
+ right_value = int(right)
69
+ elif right.replace('.', '').isdigit() and right.count('.') == 1:
70
+ right_value = float(right)
71
+ else:
72
+ right_value = right
73
+
74
+ if right_value == "true":
75
+ right_value = True
76
+ elif right_value == "false":
77
+ right_value = False
78
+
79
+ if isinstance(node_value, date):
80
+ try:
81
+ right_value = datetime.strptime(right_value, "%Y-%m-%d").date()
82
+ except Exception as e:
83
+ raise DateParseError(right_value) from e
84
+
85
+ # Apply operation
86
+ result = None
87
+ try:
88
+ if op in ('=', '=='):
89
+ result = node_value == right_value
90
+ if op == '!=':
91
+ result = node_value != right_value
92
+ if op == '>':
93
+ result = node_value > right_value
94
+ if op == '<':
95
+ result = node_value < right_value
96
+ if op == '>=':
97
+ result = node_value >= right_value
98
+ if op == '<=':
99
+ result = node_value <= right_value
100
+ except TypeError as e:
101
+ raise e
102
+ if result is None:
103
+ raise ConditionError(condition)
104
+ return result
105
+
106
+ raise ConditionError(condition)
107
+
108
+ def traverse(self, relation_type: str, direction: str = 'outgoing'):
109
+ """Traverse relations from current nodes"""
110
+ result_nodes = []
111
+ result_edges = []
112
+
113
+ for processing_node in self.nodes:
114
+ if direction == 'outgoing':
115
+ edges = self.engine.get_relations_from(processing_node.id, relation_type)
116
+ elif direction == 'incoming':
117
+ edges = self.engine.get_relations_to(processing_node.id, relation_type)
118
+ else: # both
119
+ edges = (self.engine.get_relations_from(processing_node.id, relation_type) +
120
+ self.engine.get_relations_to(processing_node.id, relation_type))
121
+
122
+ for edge in edges:
123
+ result_edges.append(edge)
124
+ target_id = edge.to_node if direction == 'outgoing' else edge.from_node
125
+ target_node = self.engine.get_node(target_id)
126
+ if target_node:
127
+ result_nodes.append(target_node)
128
+
129
+ # Remove duplicates
130
+ result_nodes = list(dict((n.id, n) for n in result_nodes).values())
131
+ return QueryResult(self.engine, result_nodes, result_edges)
132
+
133
+ def outgoing(self, relation_type: str):
134
+ """Traverse outgoing relations"""
135
+ return self.traverse(relation_type, 'outgoing')
136
+
137
+ def incoming(self, relation_type: str):
138
+ """Traverse incoming relations"""
139
+ return self.traverse(relation_type, 'incoming')
140
+
141
+ def both(self, relation_type: str):
142
+ """Traverse both directions"""
143
+ return self.traverse(relation_type, 'both')
144
+
145
+ def limit(self, n: int):
146
+ """Limit number of results"""
147
+ return QueryResult(self.engine, self.nodes[:n], self.edges[:n])
148
+
149
+ def distinct(self):
150
+ """Get distinct nodes"""
151
+ seen = set()
152
+ distinct_nodes = []
153
+ for processing_node in self.nodes:
154
+ if processing_node.id not in seen:
155
+ seen.add(processing_node.id)
156
+ distinct_nodes.append(processing_node)
157
+ return QueryResult(self.engine, distinct_nodes, self.edges)
158
+
159
+ def order_by(self, by_field: str, descending: bool = False):
160
+ """Order nodes by field"""
161
+
162
+ def get_key(from_node):
163
+ val = from_node.get(by_field)
164
+ return val is None, val
165
+
166
+ sorted_nodes = sorted(self.nodes, key=get_key, reverse=descending)
167
+ return QueryResult(self.engine, sorted_nodes, self.edges)
168
+
169
+ def count(self) -> int:
170
+ """Count nodes"""
171
+ return len(self.nodes)
172
+
173
+ def get(self) -> List[Node]:
174
+ """Get all nodes"""
175
+ return self.nodes
176
+
177
+ def first(self) -> Optional[Node]:
178
+ """Get first node"""
179
+ return self.nodes[0] if self.nodes else None
180
+
181
+ def ids(self) -> List[str]:
182
+ """Get node IDs"""
183
+ return [n.id for n in self.nodes]
184
+
185
+ class QueryBuilder: # pylint: disable=too-few-public-methods
186
+ """Builder for creating queries"""
187
+
188
+ def __init__(self, graphite_engine: 'GraphiteEngine'):
189
+ self.engine = graphite_engine
190
+
191
+ def __getattr__(self, name: str) -> QueryResult:
192
+ """Allow starting query from node type: engine.User"""
193
+ if name in self.engine.node_types:
194
+ nodes = self.engine.get_nodes_of_type(name)
195
+ return QueryResult(self.engine, nodes)
196
+ raise NotFoundError(
197
+ "Node type",
198
+ name
199
+ )
@@ -0,0 +1,174 @@
1
+ """
2
+ Serialization utils for Graphite databases
3
+ """
4
+ import json
5
+ from collections import defaultdict
6
+ from dataclasses import asdict, is_dataclass
7
+ from datetime import date, datetime
8
+ from enum import Enum
9
+ from typing import Any, Callable, Union
10
+
11
+ from .instances import Node, Relation
12
+ from .types import DataType, Field, NodeType, RelationType
13
+
14
+ GRAPHITE_TYPE_FIELD = "__graphite_type__"
15
+ DEFAULT_FACTORY_FIELD = "__default_factory"
16
+
17
+ def _serialize_instance(instance: Union[Node, Relation]) -> dict[str, Any]:
18
+ return {
19
+ GRAPHITE_TYPE_FIELD: type(instance).__name__,
20
+ "type_name" : instance.type_name,
21
+ "id" : instance.id if hasattr(instance, "id") else None,
22
+ "values" : instance.values,
23
+ "from_node" : instance.from_node if hasattr(instance, "from_node") else None,
24
+ "to_node" : instance.to_node if hasattr(instance, "to_node") else None,
25
+ "type_ref" : instance.type_ref.name if instance.type_ref else None
26
+ }
27
+
28
+ class GraphiteJSONEncoder(json.JSONEncoder):
29
+ """Custom JSON encoder for Graphite data structures"""
30
+
31
+ def default(self, o: Any) -> Any: # pylint: disable=too-many-return-statements
32
+ # Handle date/datetime objects
33
+ if isinstance(o, (date, datetime)):
34
+ return {
35
+ GRAPHITE_TYPE_FIELD: "datetime",
36
+ "value" : o.isoformat(),
37
+ "is_date" : isinstance(o, date)
38
+ }
39
+
40
+ # Handle DataType enum specifically (must come before Enum)
41
+ if isinstance(o, DataType):
42
+ return {
43
+ GRAPHITE_TYPE_FIELD: "datatype",
44
+ "value" : o.value
45
+ }
46
+
47
+ # Handle Enum objects
48
+ if isinstance(o, Enum):
49
+ return {
50
+ GRAPHITE_TYPE_FIELD: "enum",
51
+ "enum_class" : type(o).__name__,
52
+ "value" : o.value
53
+ }
54
+
55
+ # Handle defaultdict
56
+ if isinstance(o, defaultdict):
57
+ result = dict(o)
58
+ result[GRAPHITE_TYPE_FIELD] = "defaultdict"
59
+ result[DEFAULT_FACTORY_FIELD] = o.default_factory.__name__ if o.default_factory else None
60
+ return result
61
+
62
+ # Handle Node and Relation instances (already dataclasses but need special handling)
63
+ if isinstance(o, (Node, Relation)):
64
+ # Convert to dict with minimal information
65
+ return _serialize_instance(o)
66
+
67
+ # Handle NodeType and RelationType (dataclasses with parent references)
68
+ if isinstance(o, (NodeType, RelationType)):
69
+ result = asdict(o)
70
+ result[GRAPHITE_TYPE_FIELD] = type(o).__name__
71
+ # Convert parent to name reference to avoid circular references
72
+ if isinstance(o, NodeType) and o.parent:
73
+ result["parent"] = o.parent.name
74
+ # Remove type_ref from serialization
75
+ result.pop("type_ref", None)
76
+ return result
77
+
78
+ # Handle Field
79
+ if isinstance(o, Field):
80
+ result = asdict(o)
81
+ result[GRAPHITE_TYPE_FIELD] = "Field"
82
+ # Convert dtype to value
83
+ result["dtype"] = o.dtype.value
84
+ return result
85
+
86
+ # Handle dataclasses
87
+ if is_dataclass(o) and not isinstance(o, type):
88
+ # Convert to dict and add type info
89
+ result = asdict(o)
90
+ result[GRAPHITE_TYPE_FIELD] = type(o).__name__
91
+ return result
92
+
93
+ if isinstance(o, (dict, list)):
94
+ return o
95
+
96
+ return super().default(o)
97
+
98
+ # pylint: disable=too-many-branches
99
+ def graphite_object_hook(dct: dict[str, Any]) -> Any: # pylint: disable=too-many-return-statements
100
+ """Decode Graphite-specific objects from JSON."""
101
+ if GRAPHITE_TYPE_FIELD not in dct:
102
+ return dct
103
+
104
+ graphite_type = dct.pop(GRAPHITE_TYPE_FIELD)
105
+
106
+ if graphite_type == "datetime":
107
+ value = dct["value"]
108
+ if dct.get("is_date"):
109
+ return date.fromisoformat(value)
110
+ return datetime.fromisoformat(value)
111
+
112
+ if graphite_type == "enum":
113
+ enum_class = dct["enum_class"]
114
+ value = dct["value"]
115
+ if enum_class == "DataType":
116
+ return DataType(value)
117
+ return dct
118
+
119
+ if graphite_type == "datatype":
120
+ return DataType(dct["value"])
121
+
122
+ if graphite_type == "defaultdict":
123
+ factory_name = dct.pop(DEFAULT_FACTORY_FIELD, None)
124
+ factory: Union[Callable[[], Any], None] = None
125
+ if factory_name == "list":
126
+ factory = list
127
+ elif factory_name == "dict":
128
+ factory = dict
129
+ result = defaultdict(factory)
130
+ result.update(dct)
131
+ return result
132
+
133
+ if graphite_type == "Node":
134
+ return Node(
135
+ type_name=dct["type_name"],
136
+ id=dct["id"],
137
+ values=dct["values"],
138
+ type_ref=None
139
+ )
140
+
141
+ if graphite_type == "Relation":
142
+ return Relation(
143
+ type_name=dct["type_name"],
144
+ from_node=dct["from_node"],
145
+ to_node=dct["to_node"],
146
+ values=dct["values"],
147
+ type_ref=None
148
+ )
149
+
150
+ if graphite_type == "NodeType":
151
+ return {
152
+ "name": dct["name"],
153
+ "fields": dct.get("fields", []),
154
+ "parent": dct.get("parent")
155
+ }
156
+
157
+ if graphite_type == "RelationType":
158
+ return RelationType(
159
+ name=dct["name"],
160
+ from_type=dct["from_type"],
161
+ to_type=dct["to_type"],
162
+ fields=dct.get("fields", []),
163
+ reverse_name=dct.get("reverse_name"),
164
+ is_bidirectional=dct.get("is_bidirectional", False)
165
+ )
166
+
167
+ if graphite_type == "Field":
168
+ return Field(
169
+ name=dct["name"],
170
+ dtype=DataType(dct["dtype"]),
171
+ default=dct.get("default")
172
+ )
173
+
174
+ return dct