gestaltdb 0.3.0a0__tar.gz

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.
@@ -0,0 +1,136 @@
1
+ Metadata-Version: 2.4
2
+ Name: gestaltdb
3
+ Version: 0.3.0a0
4
+ Summary: A pure Python GraphDB for attributed graphs.
5
+ Requires-Python: <3.14,>=3.9
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: google>=3.0.0
8
+ Requires-Dist: msgpack>=1.1.2
9
+ Requires-Dist: plyvel>=1.5.1
10
+ Requires-Dist: protobuf>=6.33.6
11
+ Provides-Extra: lmdb
12
+ Requires-Dist: lmdb; extra == "lmdb"
13
+ Provides-Extra: leveldb
14
+ Requires-Dist: plyvel; extra == "leveldb"
15
+ Provides-Extra: msgpack
16
+ Requires-Dist: msgpack; extra == "msgpack"
17
+ Provides-Extra: protobuf
18
+ Requires-Dist: protobuf; extra == "protobuf"
19
+ Provides-Extra: bloom
20
+ Requires-Dist: pybloom-live; extra == "bloom"
21
+ Provides-Extra: coverage
22
+ Requires-Dist: coverage; extra == "coverage"
23
+ Requires-Dist: pytest; extra == "coverage"
24
+ Provides-Extra: docs
25
+ Requires-Dist: furo; extra == "docs"
26
+ Requires-Dist: myst-parser; extra == "docs"
27
+ Requires-Dist: sphinx; extra == "docs"
28
+ Provides-Extra: rocksdb
29
+ Requires-Dist: pyrex-rocksdb>=0.3.0a0; extra == "rocksdb"
30
+ Provides-Extra: arrow
31
+ Requires-Dist: pyarrow; extra == "arrow"
32
+ Provides-Extra: polars
33
+ Requires-Dist: polars; extra == "polars"
34
+ Requires-Dist: pyarrow; extra == "polars"
35
+ Provides-Extra: fast-ingest
36
+ Requires-Dist: pyarrow; extra == "fast-ingest"
37
+ Requires-Dist: polars; extra == "fast-ingest"
38
+ Requires-Dist: pyrex-rocksdb>=0.3.0a0; extra == "fast-ingest"
39
+ Provides-Extra: all
40
+ Requires-Dist: lmdb; extra == "all"
41
+ Requires-Dist: msgpack; extra == "all"
42
+ Requires-Dist: plyvel; extra == "all"
43
+ Requires-Dist: protobuf; extra == "all"
44
+ Requires-Dist: pybloom-live; extra == "all"
45
+ Requires-Dist: pyrex-rocksdb>=0.3.0a0; extra == "all"
46
+ Requires-Dist: pyarrow; extra == "all"
47
+ Requires-Dist: polars; extra == "all"
48
+ Provides-Extra: dev
49
+ Requires-Dist: coverage; extra == "dev"
50
+ Requires-Dist: furo; extra == "dev"
51
+ Requires-Dist: lmdb; extra == "dev"
52
+ Requires-Dist: msgpack; extra == "dev"
53
+ Requires-Dist: myst-parser; extra == "dev"
54
+ Requires-Dist: plyvel; extra == "dev"
55
+ Requires-Dist: protobuf; extra == "dev"
56
+ Requires-Dist: pybloom-live; extra == "dev"
57
+ Requires-Dist: pyrex-rocksdb>=0.3.0a0; extra == "dev"
58
+ Requires-Dist: pyarrow; extra == "dev"
59
+ Requires-Dist: polars; extra == "dev"
60
+ Requires-Dist: pytest; extra == "dev"
61
+ Requires-Dist: sphinx; extra == "dev"
62
+
63
+ # GestaltDB
64
+
65
+ ![Coverage](https://raw.githubusercontent.com/mylonasc/gestaltdb/refs/heads/main/assets/coverage_badge.svg)
66
+ [![Documentation](https://img.shields.io/badge/docs-GitHub%20Pages-blue.svg)](https://mylonasc.github.io/gestaltdb/)
67
+
68
+ GestaltDB is a pure Python graph database toolkit for attributed graphs. It stores nodes, edges, labels, typed adjacency records, and property indexes on embedded key-value backends.
69
+
70
+ Documentation: https://mylonasc.github.io/gestaltdb/
71
+
72
+ ## Install
73
+
74
+ From a local checkout:
75
+
76
+ ```sh
77
+ uv sync
78
+ ```
79
+
80
+ Install into another project:
81
+
82
+ ```sh
83
+ uv add /path/to/gestaltdb
84
+ ```
85
+
86
+ With pip:
87
+
88
+ ```sh
89
+ python -m pip install /path/to/gestaltdb
90
+ ```
91
+
92
+ Optional extras include `lmdb`, `leveldb`, `rocksdb`, `arrow`, `polars`, `fast-ingest`, `msgpack`, `protobuf`, `bloom`, `docs`, `dev`, and `all`.
93
+
94
+ ## Quick Example
95
+
96
+ ```python
97
+ from gestaltdb.graphdb import Edge, GraphDB, Node
98
+ from gestaltdb.kvstores import LMDBStore
99
+ from gestaltdb.serializers import PickleSerializer
100
+
101
+ graph = GraphDB(LMDBStore(path="example_lmdb"), PickleSerializer())
102
+
103
+ graph.put_node(Node(node_id="alice", labels=["Person"], properties={"name": "Alice"}))
104
+ graph.put_node(Node(node_id="bob", labels=["Person"], properties={"name": "Bob"}))
105
+ graph.put_edge(Edge(
106
+ edge_id="alice-knows-bob",
107
+ source="alice",
108
+ target="bob",
109
+ properties={"type": "knows", "since": 2024},
110
+ ))
111
+
112
+ result = graph.query('MATCH (a:Person {name: "Alice"}) MATCH (a)-[:knows]->(b) RETURN a.id, b.name')
113
+ print(result.records)
114
+
115
+ graph.close()
116
+ ```
117
+
118
+ ## Features
119
+
120
+ - Attributed `Node` and `Edge` objects with stable IDs.
121
+ - Native node labels and typed edge traversal through `edge.properties["type"]`.
122
+ - LMDB, LevelDB, and RocksDB/PyRex storage backends.
123
+ - Pickle, JSON, MessagePack, and Protobuf serializers.
124
+ - Label, relationship type, property, composite, and range indexes.
125
+ - Read-only Cypher subset for indexed scans, typed traversal, filtering, ordering, limits, and chained `MATCH` clauses.
126
+ - Bulk and columnar ingestion helpers.
127
+ - Typed path and subgraph sampling.
128
+
129
+ See the full documentation for backend selection, indexing, Cypher syntax, ingestion, sampling, and benchmarks.
130
+
131
+ <details>
132
+ <summary>Name origin</summary>
133
+
134
+ The name GestaltDB is inspired by Gestalt psychology and the idea that the whole is something more than its parts.
135
+
136
+ </details>
@@ -0,0 +1,74 @@
1
+ # GestaltDB
2
+
3
+ ![Coverage](https://raw.githubusercontent.com/mylonasc/gestaltdb/refs/heads/main/assets/coverage_badge.svg)
4
+ [![Documentation](https://img.shields.io/badge/docs-GitHub%20Pages-blue.svg)](https://mylonasc.github.io/gestaltdb/)
5
+
6
+ GestaltDB is a pure Python graph database toolkit for attributed graphs. It stores nodes, edges, labels, typed adjacency records, and property indexes on embedded key-value backends.
7
+
8
+ Documentation: https://mylonasc.github.io/gestaltdb/
9
+
10
+ ## Install
11
+
12
+ From a local checkout:
13
+
14
+ ```sh
15
+ uv sync
16
+ ```
17
+
18
+ Install into another project:
19
+
20
+ ```sh
21
+ uv add /path/to/gestaltdb
22
+ ```
23
+
24
+ With pip:
25
+
26
+ ```sh
27
+ python -m pip install /path/to/gestaltdb
28
+ ```
29
+
30
+ Optional extras include `lmdb`, `leveldb`, `rocksdb`, `arrow`, `polars`, `fast-ingest`, `msgpack`, `protobuf`, `bloom`, `docs`, `dev`, and `all`.
31
+
32
+ ## Quick Example
33
+
34
+ ```python
35
+ from gestaltdb.graphdb import Edge, GraphDB, Node
36
+ from gestaltdb.kvstores import LMDBStore
37
+ from gestaltdb.serializers import PickleSerializer
38
+
39
+ graph = GraphDB(LMDBStore(path="example_lmdb"), PickleSerializer())
40
+
41
+ graph.put_node(Node(node_id="alice", labels=["Person"], properties={"name": "Alice"}))
42
+ graph.put_node(Node(node_id="bob", labels=["Person"], properties={"name": "Bob"}))
43
+ graph.put_edge(Edge(
44
+ edge_id="alice-knows-bob",
45
+ source="alice",
46
+ target="bob",
47
+ properties={"type": "knows", "since": 2024},
48
+ ))
49
+
50
+ result = graph.query('MATCH (a:Person {name: "Alice"}) MATCH (a)-[:knows]->(b) RETURN a.id, b.name')
51
+ print(result.records)
52
+
53
+ graph.close()
54
+ ```
55
+
56
+ ## Features
57
+
58
+ - Attributed `Node` and `Edge` objects with stable IDs.
59
+ - Native node labels and typed edge traversal through `edge.properties["type"]`.
60
+ - LMDB, LevelDB, and RocksDB/PyRex storage backends.
61
+ - Pickle, JSON, MessagePack, and Protobuf serializers.
62
+ - Label, relationship type, property, composite, and range indexes.
63
+ - Read-only Cypher subset for indexed scans, typed traversal, filtering, ordering, limits, and chained `MATCH` clauses.
64
+ - Bulk and columnar ingestion helpers.
65
+ - Typed path and subgraph sampling.
66
+
67
+ See the full documentation for backend selection, indexing, Cypher syntax, ingestion, sampling, and benchmarks.
68
+
69
+ <details>
70
+ <summary>Name origin</summary>
71
+
72
+ The name GestaltDB is inspired by Gestalt psychology and the idea that the whole is something more than its parts.
73
+
74
+ </details>
@@ -0,0 +1,44 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "gestaltdb"
7
+ version = "0.3.0a0"
8
+ description = "A pure Python GraphDB for attributed graphs."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9,<3.14"
11
+ dependencies = [
12
+ "google>=3.0.0",
13
+ "msgpack>=1.1.2",
14
+ "plyvel>=1.5.1",
15
+ "protobuf>=6.33.6",
16
+ ]
17
+
18
+ [project.optional-dependencies]
19
+ lmdb = ["lmdb"]
20
+ leveldb = ["plyvel"]
21
+ msgpack = ["msgpack"]
22
+ protobuf = ["protobuf"]
23
+ bloom = ["pybloom-live"]
24
+ coverage = ["coverage", "pytest"]
25
+ docs = ["furo", "myst-parser", "sphinx"]
26
+ rocksdb = ["pyrex-rocksdb>=0.3.0a0"]
27
+ arrow = ["pyarrow"]
28
+ polars = ["polars", "pyarrow"]
29
+ fast-ingest = ["pyarrow", "polars", "pyrex-rocksdb>=0.3.0a0"]
30
+ all = ["lmdb", "msgpack", "plyvel", "protobuf", "pybloom-live", "pyrex-rocksdb>=0.3.0a0", "pyarrow", "polars"]
31
+ dev = ["coverage", "furo", "lmdb", "msgpack", "myst-parser", "plyvel", "protobuf", "pybloom-live", "pyrex-rocksdb>=0.3.0a0", "pyarrow", "polars", "pytest", "sphinx"]
32
+
33
+ [tool.setuptools.packages.find]
34
+ where = ["src"]
35
+
36
+ [tool.pytest.ini_options]
37
+ pythonpath = ["src"]
38
+ testpaths = ["tests"]
39
+
40
+ [dependency-groups]
41
+ dev = [
42
+ "coverage>=7.10.7",
43
+ "pytest>=8.0",
44
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,7 @@
1
+ """GestaltDB package."""
2
+
3
+ from .sampling import SamplingHop, SamplingPattern
4
+ from .ingestion import EdgeList, NodeList
5
+ from .cypher import QueryResult
6
+
7
+ __all__ = ["EdgeList", "NodeList", "QueryResult", "SamplingHop", "SamplingPattern"]
@@ -0,0 +1,104 @@
1
+ """Minimal read-only Cypher support for GestaltDB.
2
+
3
+ The supported subset maps directly to existing typed adjacency and sampling APIs:
4
+
5
+ MATCH (a {id: "node-id"})-[:TYPE1]->(b)<-[:TYPE2]-(c) RETURN a.name, b LIMIT 10
6
+ CALL pg.sample_typed_paths(["node-id"], [{"edge_type": "TYPE", "sample_size": 2}]) YIELD path RETURN path
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass
12
+
13
+ from .cypher_ast import MatchQuery, MultiMatchQuery, NodeScanQuery, RelationshipScanQuery, SampleTypedPathsCall
14
+ from .cypher_plan import LogicalPlan, plan_query
15
+ from .cypher_parser import parse as _parse_query, split_top_level_args as _split_top_level_args
16
+ from .cypher_runtime import QueryContext, execute_match, execute_multi_match, execute_node_scan, execute_relationship_scan
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class QueryResult:
21
+ """Tabular query result returned by ``GraphDB.query``.
22
+
23
+ ``columns`` contains projected column names in return order. ``records`` is
24
+ a list of dictionaries keyed by column name.
25
+
26
+ Examples:
27
+ >>> result = QueryResult(columns=("n",), records=[{"n": "node"}])
28
+ >>> len(result)
29
+ 1
30
+ >>> list(result)[0]["n"]
31
+ 'node'
32
+ """
33
+
34
+ columns: tuple[str, ...]
35
+ records: list[dict[str, object]]
36
+
37
+ def __iter__(self):
38
+ """Iterate over result records."""
39
+ return iter(self.records)
40
+
41
+ def __len__(self):
42
+ """Return the number of result records."""
43
+ return len(self.records)
44
+
45
+
46
+ def parse(query: str) -> MatchQuery | SampleTypedPathsCall | NodeScanQuery | RelationshipScanQuery | MultiMatchQuery:
47
+ """Parse the supported Cypher subset.
48
+
49
+ Args:
50
+ query: Cypher query text.
51
+
52
+ Returns:
53
+ Parsed query object.
54
+
55
+ Raises:
56
+ ValueError: If the query is outside the supported subset.
57
+
58
+ Examples:
59
+ >>> parse('MATCH (n:Drug) RETURN n').label
60
+ 'Drug'
61
+ """
62
+ return _parse_query(query)
63
+
64
+
65
+ def plan(query: str) -> LogicalPlan:
66
+ """Return the logical plan for a supported Cypher query."""
67
+ return plan_query(parse(query))
68
+
69
+
70
+ def execute(graph, query: str, parameters: dict[str, object] | None = None) -> QueryResult:
71
+ """Execute a supported Cypher query against a ``GraphDB`` instance.
72
+
73
+ Args:
74
+ graph: ``GraphDB`` instance used for indexed lookups and traversal.
75
+ query: Cypher query text.
76
+
77
+ Returns:
78
+ ``QueryResult`` with projected records.
79
+
80
+ Examples:
81
+ >>> execute(graph_db, 'MATCH (n:Drug) RETURN n') # doctest: +SKIP
82
+ """
83
+ parsed = parse(query)
84
+ plan_query(parsed)
85
+ parameters = parameters or {}
86
+ if isinstance(parsed, SampleTypedPathsCall):
87
+ paths = graph.sample_typed_paths(parsed.seed_ids, parsed.pattern)
88
+ if parsed.limit is not None:
89
+ paths = paths[:parsed.limit]
90
+ return QueryResult(
91
+ columns=parsed.returns,
92
+ records=[{"path": path} for path in paths],
93
+ )
94
+ if isinstance(parsed, NodeScanQuery):
95
+ records = execute_node_scan(parsed, QueryContext(graph=graph, parameters=parameters))
96
+ return QueryResult(columns=parsed.returns, records=records)
97
+ if isinstance(parsed, RelationshipScanQuery):
98
+ records = execute_relationship_scan(parsed, QueryContext(graph=graph, parameters=parameters))
99
+ return QueryResult(columns=parsed.returns, records=records)
100
+ if isinstance(parsed, MultiMatchQuery):
101
+ records = execute_multi_match(parsed, QueryContext(graph=graph, parameters=parameters))
102
+ return QueryResult(columns=parsed.returns, records=records)
103
+ records = execute_match(parsed, QueryContext(graph=graph, parameters=parameters))
104
+ return QueryResult(columns=parsed.returns, records=records)
@@ -0,0 +1,180 @@
1
+ """AST objects for the GestaltDB Cypher subset."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class Parameter:
10
+ """Cypher query parameter reference, such as ``$name``."""
11
+
12
+ name: str
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class PropertyRef:
17
+ """Reference to a variable property, such as ``n.name``."""
18
+
19
+ variable: str
20
+ property_name: str
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class ComparisonExpression:
25
+ """Binary comparison expression for the current Cypher subset."""
26
+
27
+ left: PropertyRef
28
+ operator: str
29
+ right: object
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class InExpression:
34
+ """Membership predicate, such as ``n.kind IN ["drug"]``."""
35
+
36
+ left: PropertyRef
37
+ values: object
38
+
39
+
40
+ @dataclass(frozen=True)
41
+ class NullPredicate:
42
+ """Null check predicate."""
43
+
44
+ expression: PropertyRef
45
+ negated: bool = False
46
+
47
+
48
+ @dataclass(frozen=True)
49
+ class AndExpression:
50
+ """Conjunction of boolean expressions."""
51
+
52
+ expressions: tuple[object, ...]
53
+
54
+
55
+ @dataclass(frozen=True)
56
+ class OrderItem:
57
+ """One ORDER BY item."""
58
+
59
+ expression: str
60
+ descending: bool = False
61
+
62
+
63
+ @dataclass(frozen=True)
64
+ class TraversalHop:
65
+ """One typed relationship expansion in a parsed ``MATCH`` pattern."""
66
+
67
+ rel_var: str | None
68
+ edge_type: str
69
+ target_var: str
70
+ direction: str = "out"
71
+ edge_types: tuple[str, ...] = ()
72
+
73
+
74
+ @dataclass(frozen=True)
75
+ class MatchQuery:
76
+ """Parsed anchored typed path query."""
77
+
78
+ source_var: str
79
+ source_id: str
80
+ hops: tuple[TraversalHop, ...]
81
+ returns: tuple[str, ...]
82
+ limit: int | None = None
83
+ where: object | None = None
84
+ projections: tuple[str, ...] = ()
85
+ order_by: tuple[OrderItem, ...] = ()
86
+ skip: int | None = None
87
+ distinct: bool = False
88
+
89
+
90
+ @dataclass(frozen=True)
91
+ class NodePatternClause:
92
+ """One node pattern in a multi-clause ``MATCH`` query."""
93
+
94
+ variable: str
95
+ label: str | None = None
96
+ property_name: str | None = None
97
+ property_value: object = None
98
+ labels: tuple[str, ...] = ()
99
+
100
+
101
+ @dataclass(frozen=True)
102
+ class RelationshipPatternClause:
103
+ """One relationship pattern in a multi-clause ``MATCH`` query."""
104
+
105
+ source_var: str
106
+ rel_var: str | None
107
+ edge_type: str
108
+ target_var: str
109
+ direction: str = "out"
110
+ edge_types: tuple[str, ...] = ()
111
+
112
+
113
+ @dataclass(frozen=True)
114
+ class AnchoredPatternClause:
115
+ """One anchored traversal pattern in a multi-clause ``MATCH`` query."""
116
+
117
+ source_var: str
118
+ source_id: str
119
+ hops: tuple[TraversalHop, ...]
120
+
121
+
122
+ @dataclass(frozen=True)
123
+ class MultiMatchQuery:
124
+ """Parsed query containing multiple ``MATCH`` clauses."""
125
+
126
+ clauses: tuple[object, ...]
127
+ returns: tuple[str, ...]
128
+ where: object | None = None
129
+ projections: tuple[str, ...] = ()
130
+ order_by: tuple[OrderItem, ...] = ()
131
+ skip: int | None = None
132
+ limit: int | None = None
133
+ distinct: bool = False
134
+
135
+
136
+ @dataclass(frozen=True)
137
+ class SampleTypedPathsCall:
138
+ """Parsed ``pg.sample_typed_paths`` procedure call."""
139
+
140
+ seed_ids: list[str]
141
+ pattern: list[dict[str, object]]
142
+ returns: tuple[str, ...] = ("path",)
143
+ limit: int | None = None
144
+
145
+
146
+ @dataclass(frozen=True)
147
+ class NodeScanQuery:
148
+ """Parsed indexed node label scan query."""
149
+
150
+ variable: str
151
+ label: str | None
152
+ property_name: str | None
153
+ property_value: object
154
+ returns: tuple[str, ...]
155
+ limit: int | None = None
156
+ where: object | None = None
157
+ labels: tuple[str, ...] = ()
158
+ projections: tuple[str, ...] = ()
159
+ order_by: tuple[OrderItem, ...] = ()
160
+ skip: int | None = None
161
+ distinct: bool = False
162
+
163
+
164
+ @dataclass(frozen=True)
165
+ class RelationshipScanQuery:
166
+ """Parsed unanchored typed relationship scan query."""
167
+
168
+ source_var: str
169
+ rel_var: str | None
170
+ edge_type: str
171
+ target_var: str
172
+ returns: tuple[str, ...]
173
+ direction: str = "out"
174
+ edge_types: tuple[str, ...] = ()
175
+ where: object | None = None
176
+ projections: tuple[str, ...] = ()
177
+ order_by: tuple[OrderItem, ...] = ()
178
+ skip: int | None = None
179
+ limit: int | None = None
180
+ distinct: bool = False