post-graph 0.1.0__tar.gz → 0.1.2__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.
- post_graph-0.1.2/.github/workflows/publish.yml +65 -0
- post_graph-0.1.2/LICENSE +21 -0
- post_graph-0.1.2/PKG-INFO +469 -0
- post_graph-0.1.2/README.md +442 -0
- {post_graph-0.1.0 → post_graph-0.1.2}/post_graph/__init__.py +3 -0
- {post_graph-0.1.0 → post_graph-0.1.2}/pyproject.toml +1 -1
- post_graph-0.1.0/PKG-INFO +0 -290
- post_graph-0.1.0/README.md +0 -264
- {post_graph-0.1.0 → post_graph-0.1.2}/.gitignore +0 -0
- {post_graph-0.1.0 → post_graph-0.1.2}/demo.py +0 -0
- {post_graph-0.1.0 → post_graph-0.1.2}/demo_schema_per_realm.py +0 -0
- {post_graph-0.1.0 → post_graph-0.1.2}/demo_transaction.py +0 -0
- {post_graph-0.1.0 → post_graph-0.1.2}/greek_gods.py +0 -0
- {post_graph-0.1.0 → post_graph-0.1.2}/post_graph/client_asyncpg.py +0 -0
- {post_graph-0.1.0 → post_graph-0.1.2}/post_graph/client_sqlalchemy.py +0 -0
- {post_graph-0.1.0 → post_graph-0.1.2}/post_graph/errors.py +0 -0
- {post_graph-0.1.0 → post_graph-0.1.2}/post_graph/models.py +0 -0
- {post_graph-0.1.0 → post_graph-0.1.2}/requirements.txt +0 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v[0-9]+.[0-9]+.[0-9]+*"
|
|
7
|
+
- "v*"
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
publish:
|
|
11
|
+
name: Build & Publish post-graph to PyPI
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
permissions:
|
|
14
|
+
id-token: write # Required for PyPI Trusted Publisher OIDC
|
|
15
|
+
contents: write
|
|
16
|
+
|
|
17
|
+
steps:
|
|
18
|
+
- name: Checkout repository
|
|
19
|
+
uses: actions/checkout@v4
|
|
20
|
+
|
|
21
|
+
- name: Set up Python
|
|
22
|
+
uses: actions/setup-python@v5
|
|
23
|
+
with:
|
|
24
|
+
python-version: "3.11"
|
|
25
|
+
|
|
26
|
+
- name: Update module and package version from git tag
|
|
27
|
+
run: |
|
|
28
|
+
# Extract version (e.g. v1.2.3 -> 1.2.3)
|
|
29
|
+
TAG_NAME="${GITHUB_REF#refs/tags/}"
|
|
30
|
+
export VERSION="${TAG_NAME#v}"
|
|
31
|
+
echo "Release version: $VERSION"
|
|
32
|
+
|
|
33
|
+
# Update version in pyproject.toml and post_graph/__init__.py
|
|
34
|
+
python -c "
|
|
35
|
+
import os, re
|
|
36
|
+
ver = os.environ['VERSION']
|
|
37
|
+
|
|
38
|
+
with open('pyproject.toml', 'r') as f:
|
|
39
|
+
content = f.read()
|
|
40
|
+
updated = re.sub(r'version\s*=\s*\"[^\"]+\"', 'version = \"' + ver + '\"', content, count=1)
|
|
41
|
+
with open('pyproject.toml', 'w') as f:
|
|
42
|
+
f.write(updated)
|
|
43
|
+
|
|
44
|
+
with open('post_graph/__init__.py', 'r') as f:
|
|
45
|
+
content = f.read()
|
|
46
|
+
updated = re.sub(r'__version__\s*=\s*\"[^\"]+\"', '__version__ = \"' + ver + '\"', content, count=1)
|
|
47
|
+
with open('post_graph/__init__.py', 'w') as f:
|
|
48
|
+
f.write(updated)
|
|
49
|
+
"
|
|
50
|
+
|
|
51
|
+
echo "Updated pyproject.toml and post_graph/__init__.py to version ${VERSION}"
|
|
52
|
+
|
|
53
|
+
- name: Install uv
|
|
54
|
+
uses: astral-sh/setup-uv@v3
|
|
55
|
+
with:
|
|
56
|
+
enable-cache: false
|
|
57
|
+
|
|
58
|
+
- name: Build package artifacts
|
|
59
|
+
run: uv build
|
|
60
|
+
|
|
61
|
+
- name: Publish to PyPI
|
|
62
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
63
|
+
with:
|
|
64
|
+
password: ${{ secrets.PYPI_API_TOKEN }}
|
|
65
|
+
skip-existing: true
|
post_graph-0.1.2/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Chandan Rajah
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: post-graph
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: High-performance PostgreSQL-backed graph database library supporting multi-tenant realms, schema-per-realm, shadow auditing, and append-only data history.
|
|
5
|
+
Project-URL: Homepage, https://github.com/crajah/post-graph
|
|
6
|
+
Project-URL: Repository, https://github.com/crajah/post-graph
|
|
7
|
+
Author-email: Chandan Rajah <chandan.rajah@gmail.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: asyncpg,database,graph,postgres,postgresql,sqlalchemy
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Database
|
|
20
|
+
Requires-Python: >=3.9
|
|
21
|
+
Requires-Dist: asyncpg>=0.29.0
|
|
22
|
+
Provides-Extra: all
|
|
23
|
+
Requires-Dist: sqlalchemy>=2.0.0; extra == 'all'
|
|
24
|
+
Provides-Extra: sqlalchemy
|
|
25
|
+
Requires-Dist: sqlalchemy>=2.0.0; extra == 'sqlalchemy'
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# post-graph: PostgreSQL-Backed Graph Database Library
|
|
29
|
+
|
|
30
|
+
[](https://pypi.org/project/post-graph/)
|
|
31
|
+
[](https://pypi.org/project/post-graph/)
|
|
32
|
+
[](https://opensource.org/licenses/MIT)
|
|
33
|
+
|
|
34
|
+
A generic, high-performance library for using PostgreSQL as a graph database in Python. It supports **multi-tenancy (realms)**, automatic **shadow audit logging**, **append-only history tables**, and high-performance **graph traversals (recursive CTEs)**.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Features
|
|
39
|
+
|
|
40
|
+
- **Table-Per-Vertex & Table-Per-Edge Architecture**: Maps graph elements directly to relational tables, taking advantage of PostgreSQL's foreign keys, indexes, and constraints.
|
|
41
|
+
- **Dual Isolation Modes**:
|
|
42
|
+
- **Single-Schema Multi-Tenancy**: Logical isolation using a `realm` column partition.
|
|
43
|
+
- **Schema-Per-Realm**: Physical isolation creating dedicated PostgreSQL schema namespaces per tenant (`CREATE SCHEMA IF NOT EXISTS "realm_name"`).
|
|
44
|
+
- **Autogenerated `BIGSERIAL` Primary Keys & Computed `fqid`**:
|
|
45
|
+
- Vertex FQID: `{realm}/{table_name}/{id}`
|
|
46
|
+
- Edge FQID: `{realm}/{from_table}-{to_table}/{id}` (using hyphen separator)
|
|
47
|
+
- Automatically populated at the PostgreSQL level via `GENERATED ALWAYS AS ... STORED NOT NULL`.
|
|
48
|
+
- **Append-Only History Tables (`{table_name}_data`)**:
|
|
49
|
+
- Automatically created alongside vertex and edge tables.
|
|
50
|
+
- Stores timestamped JSONB payload updates (`data_id`, `realm`, `id`, `payload`, `timestamp`).
|
|
51
|
+
- Cascades deletion when main vertex/edge is deleted (`ON DELETE CASCADE`).
|
|
52
|
+
- **Shadow Audit Tables (`{table_name}_audit`)**:
|
|
53
|
+
- Operates via PostgreSQL triggers to capture all `INSERT`, `UPDATE`, and `DELETE` events.
|
|
54
|
+
- Logs old/new row state and the initiating `user_id` passed via session parameters (`app.current_user_id`).
|
|
55
|
+
- **Advanced Graph Traversals**:
|
|
56
|
+
- Recursive CTE queries for neighbor exploration, path discovery, and cycle-free shortest path calculation.
|
|
57
|
+
- Direct object-oriented traversal APIs (`vertex.to()`, `vertex.from_()`, `step.vertex()`, `step.add_edge_to()`).
|
|
58
|
+
- **Multiple Async Client Drivers**:
|
|
59
|
+
- Raw high-speed `asyncpg` client (`AsyncPostGraph`).
|
|
60
|
+
- `SQLAlchemy` v2.0 async client (`SQLAlchemyPostGraph`).
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Installation
|
|
65
|
+
|
|
66
|
+
Install `post-graph` from PyPI:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# Basic installation (includes asyncpg)
|
|
70
|
+
pip install post-graph
|
|
71
|
+
|
|
72
|
+
# Installation with SQLAlchemy support
|
|
73
|
+
pip install "post-graph[sqlalchemy]"
|
|
74
|
+
|
|
75
|
+
# Installation with all optional dependencies
|
|
76
|
+
pip install "post-graph[all]"
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Using `uv`:
|
|
80
|
+
```bash
|
|
81
|
+
uv add post-graph
|
|
82
|
+
# or with SQLAlchemy support:
|
|
83
|
+
uv add "post-graph[sqlalchemy]"
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Architecture Schema
|
|
89
|
+
|
|
90
|
+
```mermaid
|
|
91
|
+
erDiagram
|
|
92
|
+
users {
|
|
93
|
+
text realm PK
|
|
94
|
+
bigserial id PK
|
|
95
|
+
text fqid
|
|
96
|
+
jsonb payload
|
|
97
|
+
timestamptz created_at
|
|
98
|
+
timestamptz updated_at
|
|
99
|
+
}
|
|
100
|
+
posts {
|
|
101
|
+
text realm PK
|
|
102
|
+
bigserial id PK
|
|
103
|
+
text fqid
|
|
104
|
+
jsonb payload
|
|
105
|
+
timestamptz created_at
|
|
106
|
+
timestamptz updated_at
|
|
107
|
+
}
|
|
108
|
+
likes {
|
|
109
|
+
text realm PK, FK
|
|
110
|
+
bigserial id PK
|
|
111
|
+
text fqid
|
|
112
|
+
bigint from_id FK
|
|
113
|
+
bigint to_id FK
|
|
114
|
+
text relation_type
|
|
115
|
+
jsonb payload
|
|
116
|
+
timestamptz created_at
|
|
117
|
+
timestamptz updated_at
|
|
118
|
+
}
|
|
119
|
+
users_audit {
|
|
120
|
+
bigint audit_id PK
|
|
121
|
+
text realm
|
|
122
|
+
text action
|
|
123
|
+
text changed_by
|
|
124
|
+
timestamptz changed_at
|
|
125
|
+
jsonb old_row
|
|
126
|
+
jsonb new_row
|
|
127
|
+
}
|
|
128
|
+
users_data {
|
|
129
|
+
bigserial data_id PK
|
|
130
|
+
text realm FK
|
|
131
|
+
bigint id FK
|
|
132
|
+
jsonb payload
|
|
133
|
+
timestamptz timestamp
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
users ||--o{ likes : "from_id"
|
|
137
|
+
posts ||--o{ likes : "to_id"
|
|
138
|
+
users ||--o{ users_audit : "logs changes"
|
|
139
|
+
users ||--o{ users_data : "appends records"
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Quick Start Examples
|
|
145
|
+
|
|
146
|
+
### 1. Using `AsyncPostGraph` (`asyncpg`)
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
import asyncio
|
|
150
|
+
from post_graph import AsyncPostGraph
|
|
151
|
+
|
|
152
|
+
async def main():
|
|
153
|
+
client = AsyncPostGraph(dsn="postgresql://postgres:postgres@localhost:5432/postgres")
|
|
154
|
+
await client.connect()
|
|
155
|
+
|
|
156
|
+
# 1. Create schema tables
|
|
157
|
+
await client.create_vertex_table("users")
|
|
158
|
+
await client.create_vertex_table("posts")
|
|
159
|
+
await client.create_edge_table("likes", from_vertex_table="users", to_vertex_table="posts")
|
|
160
|
+
|
|
161
|
+
# 2. Add vertices (id is autogenerated if omitted)
|
|
162
|
+
alice = await client.add_vertex("users", realm="realm_a", payload={"name": "Alice"})
|
|
163
|
+
post = await client.add_vertex("posts", realm="realm_a", payload={"title": "Hello PostGraph!"})
|
|
164
|
+
|
|
165
|
+
# 3. Add edge connecting Alice -> Post
|
|
166
|
+
edge = await client.add_edge(
|
|
167
|
+
table_name="likes",
|
|
168
|
+
realm="realm_a",
|
|
169
|
+
from_id=alice.id,
|
|
170
|
+
to_id=post.id,
|
|
171
|
+
relation_type="like",
|
|
172
|
+
payload={"reaction": "heart"}
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
# 4. Append data history record
|
|
176
|
+
await alice.add_data({"status": "active", "login_count": 1})
|
|
177
|
+
history = await alice.get_data()
|
|
178
|
+
print(f"Alice history count: {len(history)}, last: {history[0].payload}")
|
|
179
|
+
|
|
180
|
+
# 5. Object-oriented traversal
|
|
181
|
+
steps = await alice.to("likes")
|
|
182
|
+
for step in steps:
|
|
183
|
+
target_post = step.vertex()
|
|
184
|
+
print(f"Alice liked post: {target_post.payload['title']}")
|
|
185
|
+
|
|
186
|
+
await client.close()
|
|
187
|
+
|
|
188
|
+
asyncio.run(main())
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### 2. Using `SQLAlchemyPostGraph`
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
import asyncio
|
|
195
|
+
from sqlalchemy.ext.asyncio import create_async_engine
|
|
196
|
+
from post_graph import SQLAlchemyPostGraph
|
|
197
|
+
|
|
198
|
+
async def main():
|
|
199
|
+
engine = create_async_engine("postgresql+asyncpg://postgres:postgres@localhost:5432/postgres")
|
|
200
|
+
client = SQLAlchemyPostGraph(engine)
|
|
201
|
+
|
|
202
|
+
# DDL & CRUD methods share identical signatures
|
|
203
|
+
await client.create_vertex_table("users")
|
|
204
|
+
await client.create_edge_table("follows", from_vertex_table="users", to_vertex_table="users")
|
|
205
|
+
|
|
206
|
+
v1 = await client.add_vertex("users", realm="default", payload={"name": "Bob"})
|
|
207
|
+
v2 = await client.add_vertex("users", realm="default", payload={"name": "Charlie"})
|
|
208
|
+
e = await client.add_edge("follows", realm="default", from_id=v1.id, to_id=v2.id, relation_type="follow")
|
|
209
|
+
|
|
210
|
+
await engine.dispose()
|
|
211
|
+
|
|
212
|
+
asyncio.run(main())
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## Detailed API Reference
|
|
218
|
+
|
|
219
|
+
### Client Initialization & Connection
|
|
220
|
+
|
|
221
|
+
```python
|
|
222
|
+
AsyncPostGraph(
|
|
223
|
+
dsn: Optional[str] = None,
|
|
224
|
+
connection_or_pool: Optional[Union[asyncpg.Pool, asyncpg.Connection]] = None,
|
|
225
|
+
schema_per_realm: bool = False
|
|
226
|
+
)
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
```python
|
|
230
|
+
SQLAlchemyPostGraph(
|
|
231
|
+
engine_or_connection: Union[AsyncEngine, AsyncConnection],
|
|
232
|
+
schema_per_realm: bool = False
|
|
233
|
+
)
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
- **`schema_per_realm`**: If `True`, creates physically isolated schema namespaces per `realm` (e.g. `"realm_a"."users"`). If `False` (default), uses single-schema logical multi-tenancy with a `realm` column partition.
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
### Schema DDL Operations
|
|
241
|
+
|
|
242
|
+
#### `create_vertex_table`
|
|
243
|
+
```python
|
|
244
|
+
await client.create_vertex_table(table_name="users", realm=None)
|
|
245
|
+
```
|
|
246
|
+
Creates:
|
|
247
|
+
1. `{table_name}` main vertex table with `(realm, id)` primary key.
|
|
248
|
+
2. `{table_name}_audit` shadow audit log table.
|
|
249
|
+
3. `{table_name}_data` append-only history table with foreign key `(realm, id) ON DELETE CASCADE`.
|
|
250
|
+
4. Triggers for `updated_at` timestamps and audit logging.
|
|
251
|
+
|
|
252
|
+
#### `create_edge_table`
|
|
253
|
+
```python
|
|
254
|
+
await client.create_edge_table(
|
|
255
|
+
table_name="likes", # Optional; defaults to "{from_table}TO{to_table}" if empty
|
|
256
|
+
from_vertex_table="users",
|
|
257
|
+
to_vertex_table="posts",
|
|
258
|
+
realm=None,
|
|
259
|
+
cascade_delete_from=False, # If True, deleting the edge deletes source vertex
|
|
260
|
+
cascade_delete_to=False # If True, deleting the edge deletes target vertex
|
|
261
|
+
)
|
|
262
|
+
```
|
|
263
|
+
Creates main edge table, shadow audit table `{table_name}_audit`, and append-only history table `{table_name}_data`. Foreign keys reference `(realm, from_id)` and `(realm, to_id)` on vertex tables with `ON DELETE CASCADE`.
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
### Vertex Operations
|
|
268
|
+
|
|
269
|
+
#### `add_vertex`
|
|
270
|
+
```python
|
|
271
|
+
vertex = await client.add_vertex(
|
|
272
|
+
table_name="users",
|
|
273
|
+
realm="tenant_1",
|
|
274
|
+
vertex_id=None, # Optional. If omitted, BIGSERIAL autogenerates integer ID.
|
|
275
|
+
payload={"name": "Alice"}, # Dict payload stored as JSONB.
|
|
276
|
+
user_id="admin_1" # Optional user attribution for audit log.
|
|
277
|
+
)
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
#### `get_vertex`
|
|
281
|
+
```python
|
|
282
|
+
vertex = await client.get_vertex(table_name="users", realm="tenant_1", vertex_id=1)
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
#### `update_vertex`
|
|
286
|
+
```python
|
|
287
|
+
updated = await client.update_vertex(
|
|
288
|
+
table_name="users",
|
|
289
|
+
realm="tenant_1",
|
|
290
|
+
vertex_id=1,
|
|
291
|
+
payload={"name": "Alice Smith"}, # Replaces JSONB payload
|
|
292
|
+
user_id="admin_1"
|
|
293
|
+
)
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
#### `delete_vertex`
|
|
297
|
+
```python
|
|
298
|
+
deleted = await client.delete_vertex(table_name="users", realm="tenant_1", vertex_id=1, user_id="admin_1")
|
|
299
|
+
```
|
|
300
|
+
Automatically cascade deletes all connected edges in the database.
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
### Edge Operations
|
|
305
|
+
|
|
306
|
+
#### `add_edge`
|
|
307
|
+
```python
|
|
308
|
+
edge = await client.add_edge(
|
|
309
|
+
table_name="follows",
|
|
310
|
+
realm="tenant_1",
|
|
311
|
+
edge_id=None, # Optional autogenerated BIGSERIAL ID.
|
|
312
|
+
from_id=1,
|
|
313
|
+
to_id=2,
|
|
314
|
+
relation_type="friend",
|
|
315
|
+
payload={"since": "2026-01-01"},
|
|
316
|
+
user_id="admin_1",
|
|
317
|
+
check_cycle=False # Set True to raise CyclicReferenceError if a cycle is created
|
|
318
|
+
)
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
#### `get_edge`
|
|
322
|
+
```python
|
|
323
|
+
edge = await client.get_edge(table_name="follows", realm="tenant_1", edge_id=100)
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
#### `delete_edge`
|
|
327
|
+
```python
|
|
328
|
+
deleted = await client.delete_edge(table_name="follows", realm="tenant_1", edge_id=100, user_id="admin_1")
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
### Multi-Tenant Realm Operations
|
|
334
|
+
|
|
335
|
+
#### `delete_realm`
|
|
336
|
+
```python
|
|
337
|
+
rows_deleted = await client.delete_realm(realm="tenant_to_remove")
|
|
338
|
+
```
|
|
339
|
+
Completely removes all vertices, edges, audit logs, and data history records belonging to the target realm across all tables.
|
|
340
|
+
|
|
341
|
+
---
|
|
342
|
+
|
|
343
|
+
### Append-Only Data History (`{table_name}_data`)
|
|
344
|
+
|
|
345
|
+
Vertices and edges support appending timestamped historical data snapshots.
|
|
346
|
+
|
|
347
|
+
#### Client Data APIs
|
|
348
|
+
```python
|
|
349
|
+
# Add historical record
|
|
350
|
+
record = await client.add_vertex_data(
|
|
351
|
+
table_name="users",
|
|
352
|
+
realm="realm_a",
|
|
353
|
+
vertex_id=1,
|
|
354
|
+
payload={"status": "active", "login_count": 10},
|
|
355
|
+
timestamp=None # Optional; defaults to CURRENT_TIMESTAMP
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
# Fetch history records (sorted timestamp DESC)
|
|
359
|
+
history = await client.get_vertex_data(table_name="users", realm="realm_a", vertex_id=1, limit=5)
|
|
360
|
+
for rec in history:
|
|
361
|
+
print(rec.data_id, rec.timestamp, rec.payload)
|
|
362
|
+
```
|
|
363
|
+
*(Direct `add_edge_data` and `get_edge_data` methods operate identically for edge tables).*
|
|
364
|
+
|
|
365
|
+
#### Model Convenience Methods
|
|
366
|
+
```python
|
|
367
|
+
# Vertex/Edge instances expose add_data() and get_data() directly
|
|
368
|
+
rec = await vertex.add_data({"status": "active"})
|
|
369
|
+
history = await vertex.get_data(limit=10)
|
|
370
|
+
|
|
371
|
+
rec_edge = await edge.add_data({"event": "click"})
|
|
372
|
+
history_edge = await edge.get_data()
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
---
|
|
376
|
+
|
|
377
|
+
### Object-Oriented Graph Traversal
|
|
378
|
+
|
|
379
|
+
Loaded `Vertex` objects allow seamless graph navigation:
|
|
380
|
+
|
|
381
|
+
```python
|
|
382
|
+
alice = await client.get_vertex("users", "realm_a", 1)
|
|
383
|
+
|
|
384
|
+
# 1. Forward Traversal (.to)
|
|
385
|
+
steps = await alice.to(edge_table="follows")
|
|
386
|
+
for step in steps:
|
|
387
|
+
edge = step.edge
|
|
388
|
+
neighbor = step.vertex()
|
|
389
|
+
print(f"Alice --[{edge.relation_type}]--> {neighbor.payload['name']}")
|
|
390
|
+
|
|
391
|
+
# 2. Reverse Traversal (.from_)
|
|
392
|
+
bob = await client.get_vertex("users", "realm_a", 2)
|
|
393
|
+
inbound_steps = await bob.from_(edge_table="follows")
|
|
394
|
+
for step in inbound_steps:
|
|
395
|
+
origin_user = step.vertex()
|
|
396
|
+
print(f"Followed by: {origin_user.payload['name']}")
|
|
397
|
+
|
|
398
|
+
# 3. Add Edge via Traversal Step
|
|
399
|
+
new_edge = await inbound_steps[0].add_edge_to(to_id=3, edge_table="follows", relation_type="knows")
|
|
400
|
+
|
|
401
|
+
# 4. Delete Vertex directly
|
|
402
|
+
await bob.delete(user_id="admin_1")
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
---
|
|
406
|
+
|
|
407
|
+
### High-Performance Recursive CTE Queries
|
|
408
|
+
|
|
409
|
+
#### `traverse`
|
|
410
|
+
Explores all reachable paths starting from a vertex up to `max_depth`:
|
|
411
|
+
|
|
412
|
+
```python
|
|
413
|
+
paths = await client.traverse(
|
|
414
|
+
realm="realm_a",
|
|
415
|
+
start_table="users",
|
|
416
|
+
start_id=1,
|
|
417
|
+
edge_tables=["follows", "likes"],
|
|
418
|
+
max_depth=3
|
|
419
|
+
)
|
|
420
|
+
for p in paths:
|
|
421
|
+
print(f"Depth: {p['depth']} | Path: {' -> '.join(p['path'])}")
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
#### `shortest_path`
|
|
425
|
+
Calculates the shortest path between two nodes, with built-in loop avoidance:
|
|
426
|
+
|
|
427
|
+
```python
|
|
428
|
+
sp = await client.shortest_path(
|
|
429
|
+
realm="realm_a",
|
|
430
|
+
start_table="users",
|
|
431
|
+
start_id=1,
|
|
432
|
+
target_table="posts",
|
|
433
|
+
target_id=42,
|
|
434
|
+
edge_tables=["follows", "likes"],
|
|
435
|
+
max_depth=5
|
|
436
|
+
)
|
|
437
|
+
if sp:
|
|
438
|
+
print(f"Shortest path found at depth {sp['depth']}: {sp['path']}")
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
---
|
|
442
|
+
|
|
443
|
+
### Transaction Support
|
|
444
|
+
|
|
445
|
+
#### `AsyncPostGraph` (raw asyncpg)
|
|
446
|
+
```python
|
|
447
|
+
async with pool.acquire() as conn:
|
|
448
|
+
async with conn.transaction():
|
|
449
|
+
# Bind transaction connection to client wrapper
|
|
450
|
+
tx_client = AsyncPostGraph(connection_or_pool=conn)
|
|
451
|
+
await tx_client.add_vertex("users", "realm_a", payload={"name": "Tx Node"})
|
|
452
|
+
await tx_client.add_edge("follows", "realm_a", from_id=1, to_id=2, relation_type="knows")
|
|
453
|
+
# Commits automatically on block exit; rolls back on exception.
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
#### `SQLAlchemyPostGraph`
|
|
457
|
+
```python
|
|
458
|
+
async with engine.connect() as conn:
|
|
459
|
+
async with conn.begin():
|
|
460
|
+
tx_client = SQLAlchemyPostGraph(conn)
|
|
461
|
+
await tx_client.add_vertex("sa_users", "default", payload={"name": "Tx Node"})
|
|
462
|
+
await tx_client.add_edge("sa_follows", "default", from_id=1, to_id=2, relation_type="knows")
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
---
|
|
466
|
+
|
|
467
|
+
## License
|
|
468
|
+
|
|
469
|
+
This project is licensed under the [MIT License](LICENSE).
|