post-graph 0.1.0__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,222 @@
1
+ # Secrets and keys
2
+ **/*secret*
3
+ **/*key*
4
+
5
+ # Byte-compiled / optimized / DLL files
6
+ __pycache__/
7
+ *.py[codz]
8
+ *$py.class
9
+
10
+ # C extensions
11
+ *.so
12
+
13
+ # Distribution / packaging
14
+ .Python
15
+ build/
16
+ develop-eggs/
17
+ dist/
18
+ downloads/
19
+ eggs/
20
+ .eggs/
21
+ lib/
22
+ lib64/
23
+ parts/
24
+ sdist/
25
+ var/
26
+ wheels/
27
+ share/python-wheels/
28
+ *.egg-info/
29
+ .installed.cfg
30
+ *.egg
31
+ MANIFEST
32
+
33
+ # PyInstaller
34
+ # Usually these files are written by a python script from a template
35
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
36
+ *.manifest
37
+ *.spec
38
+
39
+ # Installer logs
40
+ pip-log.txt
41
+ pip-delete-this-directory.txt
42
+
43
+ # Unit test / coverage reports
44
+ htmlcov/
45
+ .tox/
46
+ .nox/
47
+ .coverage
48
+ .coverage.*
49
+ .cache
50
+ nosetests.xml
51
+ coverage.xml
52
+ *.cover
53
+ *.py.cover
54
+ .hypothesis/
55
+ .pytest_cache/
56
+ cover/
57
+
58
+ # Translations
59
+ *.mo
60
+ *.pot
61
+
62
+ # Django stuff:
63
+ *.log
64
+ local_settings.py
65
+ db.sqlite3
66
+ db.sqlite3-journal
67
+
68
+ # Flask stuff:
69
+ instance/
70
+ .webassets-cache
71
+
72
+ # Scrapy stuff:
73
+ .scrapy
74
+
75
+ # Sphinx documentation
76
+ docs/_build/
77
+
78
+ # PyBuilder
79
+ .pybuilder/
80
+ target/
81
+
82
+ # Jupyter Notebook
83
+ .ipynb_checkpoints
84
+
85
+ # IPython
86
+ profile_default/
87
+ ipython_config.py
88
+
89
+ # pyenv
90
+ # For a library or package, you might want to ignore these files since the code is
91
+ # intended to run in multiple environments; otherwise, check them in:
92
+ # .python-version
93
+
94
+ # pipenv
95
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
96
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
97
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
98
+ # install all needed dependencies.
99
+ # Pipfile.lock
100
+
101
+ # UV
102
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
103
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
104
+ # commonly ignored for libraries.
105
+ # uv.lock
106
+
107
+ # poetry
108
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
109
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
110
+ # commonly ignored for libraries.
111
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
112
+ # poetry.lock
113
+ # poetry.toml
114
+
115
+ # pdm
116
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
117
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
118
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
119
+ # pdm.lock
120
+ # pdm.toml
121
+ .pdm-python
122
+ .pdm-build/
123
+
124
+ # pixi
125
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
126
+ # pixi.lock
127
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
128
+ # in the .venv directory. It is recommended not to include this directory in version control.
129
+ .pixi
130
+
131
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
132
+ __pypackages__/
133
+
134
+ # Celery stuff
135
+ celerybeat-schedule
136
+ celerybeat.pid
137
+
138
+ # Redis
139
+ *.rdb
140
+ *.aof
141
+ *.pid
142
+
143
+ # RabbitMQ
144
+ mnesia/
145
+ rabbitmq/
146
+ rabbitmq-data/
147
+
148
+ # ActiveMQ
149
+ activemq-data/
150
+
151
+ # SageMath parsed files
152
+ *.sage.py
153
+
154
+ # Environments
155
+ .env
156
+ .envrc
157
+ .venv
158
+ env/
159
+ venv/
160
+ ENV/
161
+ env.bak/
162
+ venv.bak/
163
+
164
+ # Spyder project settings
165
+ .spyderproject
166
+ .spyproject
167
+
168
+ # Rope project settings
169
+ .ropeproject
170
+
171
+ # mkdocs documentation
172
+ /site
173
+
174
+ # mypy
175
+ .mypy_cache/
176
+ .dmypy.json
177
+ dmypy.json
178
+
179
+ # Pyre type checker
180
+ .pyre/
181
+
182
+ # pytype static type analyzer
183
+ .pytype/
184
+
185
+ # Cython debug symbols
186
+ cython_debug/
187
+
188
+ # PyCharm
189
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
190
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
191
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
192
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
193
+ # .idea/
194
+
195
+ # Abstra
196
+ # Abstra is an AI-powered process automation framework.
197
+ # Ignore directories containing user credentials, local state, and settings.
198
+ # Learn more at https://abstra.io/docs
199
+ .abstra/
200
+
201
+ # Visual Studio Code
202
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
203
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
204
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
205
+ # you could uncomment the following to ignore the entire vscode folder
206
+ # .vscode/
207
+ # Temporary file for partial code execution
208
+ tempCodeRunnerFile.py
209
+
210
+ # Ruff stuff:
211
+ .ruff_cache/
212
+
213
+ # PyPI configuration file
214
+ .pypirc
215
+
216
+ # Marimo
217
+ marimo/_static/
218
+ marimo/_lsp/
219
+ __marimo__/
220
+
221
+ # Streamlit
222
+ .streamlit/secrets.toml
@@ -0,0 +1,290 @@
1
+ Metadata-Version: 2.4
2
+ Name: post-graph
3
+ Version: 0.1.0
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
+ Keywords: asyncpg,database,graph,postgres,postgresql,sqlalchemy
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Database
19
+ Requires-Python: >=3.9
20
+ Requires-Dist: asyncpg>=0.29.0
21
+ Provides-Extra: all
22
+ Requires-Dist: sqlalchemy>=2.0.0; extra == 'all'
23
+ Provides-Extra: sqlalchemy
24
+ Requires-Dist: sqlalchemy>=2.0.0; extra == 'sqlalchemy'
25
+ Description-Content-Type: text/markdown
26
+
27
+ # post-graph: Generic PostgreSQL Graph Database Library
28
+
29
+ A generic, high-performance mechanism to use PostgreSQL as a graph database in Python. It supports **multi-tenancy (realms)**, automatic **shadow audit tables**, and advanced **graph traversals (recursive CTEs)**.
30
+
31
+ Features:
32
+ * **Table-Per-Vertex & Table-Per-Edge Architecture**: Graph elements are mapped to individual tables, utilizing PostgreSQL's native schema constraints and relations.
33
+ * **Multi-Tenancy**: Built-in logical isolation using a `realm` field, allowing multiple tenants to share the same database while enforcing partition boundaries.
34
+ * **Shadow Auditing**: Automatically creates a matching `{table_name}_audit` table for every vertex and edge table. Operates at the database trigger level to log operations (`INSERT`, `UPDATE`, `DELETE`), capturing the complete row history and the initiating `user_id` passed securely via session parameters.
35
+ * **Dynamic Metadata Discovery**: Queries the PostgreSQL catalog tables to dynamically determine the structure and connections of edge tables during traversals.
36
+ * **Advanced Graph CTEs**: High-performance recursive CTE queries for single-hop neighbor queries, multi-hop path traversals, and shortest-path calculation (with early-termination and cycle prevention).
37
+ * **Multiple Backend Drivers**: Includes native clients for raw `asyncpg` and `SQLAlchemy` (v2.0 async connections).
38
+
39
+ ---
40
+
41
+ ## Architecture Schema
42
+
43
+ The schema design isolates graphs by tenant `realm`, ensuring that edges can only link vertices belonging to the exact same realm.
44
+
45
+ ```mermaid
46
+ erDiagram
47
+ users {
48
+ text realm PK
49
+ bigserial id PK
50
+ text fqid
51
+ jsonb payload
52
+ timestamptz created_at
53
+ timestamptz updated_at
54
+ }
55
+ posts {
56
+ text realm PK
57
+ bigserial id PK
58
+ text fqid
59
+ jsonb payload
60
+ timestamptz created_at
61
+ timestamptz updated_at
62
+ }
63
+ likes {
64
+ text realm PK, FK
65
+ bigserial id PK
66
+ text fqid
67
+ bigint from_id FK
68
+ bigint to_id FK
69
+ text relation_type
70
+ jsonb payload
71
+ timestamptz created_at
72
+ timestamptz updated_at
73
+ }
74
+ users_audit {
75
+ bigint audit_id PK
76
+ text realm
77
+ text action
78
+ text changed_by
79
+ timestamptz changed_at
80
+ jsonb old_row
81
+ jsonb new_row
82
+ }
83
+ users_data {
84
+ bigserial data_id PK
85
+ text realm FK
86
+ bigint id FK
87
+ jsonb payload
88
+ timestamptz timestamp
89
+ }
90
+
91
+ users ||--o{ likes : "from_id"
92
+ posts ||--o{ likes : "to_id"
93
+ users ||--o{ users_audit : "logs changes"
94
+ users ||--o{ users_data : "appends records"
95
+ ```
96
+
97
+ ---
98
+
99
+ ## Installation
100
+
101
+ Define dependencies in your environment:
102
+ ```bash
103
+ pip install -r requirements.txt
104
+ ```
105
+
106
+ ---
107
+
108
+ ## Quick Start
109
+
110
+ ### 1. Raw `asyncpg` Client (`AsyncPostGraph`)
111
+
112
+ ```python
113
+ import asyncio
114
+ from post_graph import AsyncPostGraph
115
+
116
+ async def main():
117
+ # Connect to PostgreSQL
118
+ client = AsyncPostGraph(dsn="postgresql://postgres:postgres@localhost:5432/postgres")
119
+ await client.connect()
120
+
121
+ # Create schema (with optional edge-to-vertex cascade deletion parameters)
122
+ await client.create_vertex_table("users")
123
+ await client.create_vertex_table("posts")
124
+ await client.create_edge_table(
125
+ "likes",
126
+ from_vertex_table="users",
127
+ to_vertex_table="posts",
128
+ cascade_delete_from=False, # If True, deleting an edge deletes the source vertex
129
+ cascade_delete_to=False # If True, deleting an edge deletes the target vertex
130
+ )
131
+
132
+ # Add vertices (isolated in realm 'tenant_1' and audited as user 'admin_alice')
133
+ await client.add_vertex(
134
+ table_name="users",
135
+ realm="tenant_1",
136
+ vertex_id="u_1",
137
+ payload={"username": "alice"},
138
+ user_id="admin_alice"
139
+ )
140
+ await client.add_vertex(
141
+ table_name="posts",
142
+ realm="tenant_1",
143
+ vertex_id="p_1",
144
+ payload={"title": "Hello World"},
145
+ user_id="admin_alice"
146
+ )
147
+
148
+ # Link vertices with an edge (with optional cycle prevention checks)
149
+ await client.add_edge(
150
+ table_name="likes",
151
+ realm="tenant_1",
152
+ edge_id="e_1",
153
+ from_id="u_1",
154
+ to_id="p_1",
155
+ relation_type="like",
156
+ payload={"reaction": "heart"},
157
+ user_id="user_bob",
158
+ check_cycle=True # Optional: raise CyclicReferenceError if a loop would be created
159
+ )
160
+
161
+ # Query neighbors
162
+ neighbors = await client.get_neighbors(
163
+ realm="tenant_1",
164
+ vertex_table="users",
165
+ vertex_id="u_1",
166
+ edge_tables=["likes"]
167
+ )
168
+ for vertex, edge in neighbors:
169
+ print(f"Connected to post: {vertex.id} with payload: {vertex.payload}")
170
+
171
+ # Delete an entire realm (tenant) from all tables (including audits)
172
+ deleted_rows = await client.delete_realm("tenant_1")
173
+ print(f"Cleared {deleted_rows} rows belonging to tenant_1")
174
+
175
+ await client.close()
176
+
177
+ asyncio.run(main())
178
+ ```
179
+
180
+ ### 2. SQLAlchemy Client (`SQLAlchemyPostGraph`)
181
+
182
+ Integrates with your existing SQLAlchemy async architecture:
183
+
184
+ ```python
185
+ from sqlalchemy.ext.asyncio import create_async_engine
186
+ from post_graph import SQLAlchemyPostGraph
187
+
188
+ engine = create_async_engine("postgresql+asyncpg://postgres:postgres@localhost:5432/postgres")
189
+ client = SQLAlchemyPostGraph(engine)
190
+
191
+ # All schema creation and graph operation interfaces match the AsyncPostGraph client
192
+ await client.create_vertex_table("users")
193
+ ```
194
+
195
+ ## Advanced Graph Operations
196
+
197
+ ### Direct Object-Level Traversal
198
+ If a `Vertex` is loaded from a client, you can traverse directly from the object using standard OOP calling patterns.
199
+
200
+ *Note: Since `from` is a reserved keyword in Python, the reverse traversal method is named `.from_()` (with an underscore).*
201
+
202
+ ```python
203
+ # Load a vertex
204
+ alice_v = await client.get_vertex(table_name="users", realm="tenant_1", vertex_id="u_1")
205
+
206
+ # 1. Forward traversal
207
+ steps = await alice_v.to("follows")
208
+ if steps:
209
+ neighbor_v = steps[0].vertex()
210
+ print(f"Alice follows: {neighbor_v.id}")
211
+
212
+ # 2. Reverse traversal (incoming links)
213
+ rev_steps = await neighbor_v.from_("follows")
214
+ if rev_steps:
215
+ creator_v = rev_steps[0].vertex() # returns Alice!
216
+ print(f"Alice was found by reverse traversal: {creator_v.id}")
217
+
218
+ # 3. Direct Edge Mutations via Objects/Steps
219
+ if rev_steps:
220
+ new_edge = await rev_steps[0].add_edge_to(to_id="p_5", edge_table="likes")
221
+ print(f"Added edge: {new_edge.from_id} -> {new_edge.to_id}")
222
+
223
+ # 4. Direct Vertex/Edge Deletion
224
+ # Deleting a vertex automatically cascade-deletes all connecting edges in the database
225
+ await neighbor_v.delete(user_id="admin_1")
226
+ ```
227
+
228
+ ### Path Traversal
229
+ Retrieve all reachable paths up to a specified depth (default 3) starting from a vertex:
230
+ ```python
231
+ paths = await client.traverse(
232
+ realm="tenant_1",
233
+ start_table="users",
234
+ start_id="u_1",
235
+ edge_tables=["likes", "follows"],
236
+ max_depth=3
237
+ )
238
+ for p in paths:
239
+ print(f"Path taken: {p['path']} via relations: {p['edge_path']}")
240
+ ```
241
+
242
+ ### Shortest Path
243
+ Find the shortest path between two vertices, preventing cycles:
244
+ ```python
245
+ sp = await client.shortest_path(
246
+ realm="tenant_1",
247
+ start_table="users",
248
+ start_id="u_1",
249
+ target_table="posts",
250
+ target_id="p_5",
251
+ edge_tables=["follows", "likes"]
252
+ )
253
+ if sp:
254
+ print(f"Shortest path depth: {sp['depth']}, steps: {sp['path']}")
255
+ ```
256
+
257
+ ---
258
+
259
+ ## Shadow Auditing & Session User Mapping
260
+
261
+ Auditing works entirely inside PostgreSQL triggers. To attribute a change to a user:
262
+ 1. When calling Python operations (`add_vertex`, `upsert_edge`, `delete_vertex`, etc.), pass the `user_id` keyword argument.
263
+ 2. The library executes `SELECT set_config('app.current_user_id', user_id, true)` in the current transaction block.
264
+ 3. The database trigger automatically maps this value into the `{table_name}_audit` table.
265
+ A query to `{table_name}_audit` returns the modification history:
266
+ ```sql
267
+ SELECT audit_id, realm, action, changed_by, changed_at, old_row, new_row
268
+ FROM users_audit;
269
+ ```
270
+
271
+ ---
272
+
273
+ ## Schema-Per-Realm (Physical Namespace Isolation)
274
+
275
+ By default, `post-graph` uses a single shared set of tables and isolates data logically using a `realm` column. If you require physical data isolation, enable the `schema_per_realm` parameter on client initialization:
276
+
277
+ ```python
278
+ # Initialize Async Client with physical namespace isolation
279
+ client = AsyncPostGraph(dsn=db_url, schema_per_realm=True)
280
+
281
+ # Create vertex table under specific realm schema namespace 'tenant_a'
282
+ await client.create_vertex_table("users", realm="tenant_a")
283
+ ```
284
+
285
+ When enabled:
286
+ 1. Every realm has its own dedicated schema namespace (e.g. `CREATE SCHEMA IF NOT EXISTS "tenant_a"`).
287
+ 2. All vertex, edge, and shadow audit tables for a realm are stored physically inside that schema (e.g., `"tenant_a"."users"` and `"tenant_a"."users_audit"`).
288
+ 3. Database level triggers are completely isolated per schema namespace.
289
+ 4. Schema-specific metadata catalog lookups verify table links locally inside the target schema namespace.
290
+