runic-py 0.2.2__tar.gz → 0.3.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.
Files changed (70) hide show
  1. {runic_py-0.2.2 → runic_py-0.3.0}/.gitignore +1 -0
  2. runic_py-0.2.2/README.md → runic_py-0.3.0/PKG-INFO +111 -27
  3. runic_py-0.2.2/PKG-INFO → runic_py-0.3.0/README.md +77 -48
  4. {runic_py-0.2.2 → runic_py-0.3.0}/pyproject.toml +27 -7
  5. {runic_py-0.2.2 → runic_py-0.3.0}/runic/migrate/__init__.py +10 -0
  6. {runic_py-0.2.2 → runic_py-0.3.0}/runic/migrate/adapters/__init__.py +55 -1
  7. runic_py-0.3.0/runic/migrate/adapters/_base.py +215 -0
  8. runic_py-0.3.0/runic/migrate/adapters/age.py +186 -0
  9. runic_py-0.3.0/runic/migrate/adapters/arcadedb.py +229 -0
  10. {runic_py-0.2.2 → runic_py-0.3.0}/runic/migrate/adapters/falkordb.py +216 -35
  11. runic_py-0.3.0/runic/migrate/adapters/memgraph.py +265 -0
  12. runic_py-0.3.0/runic/migrate/adapters/neo4j.py +283 -0
  13. {runic_py-0.2.2 → runic_py-0.3.0}/runic/migrate/cli.py +9 -4
  14. {runic_py-0.2.2 → runic_py-0.3.0}/runic/migrate/operations.py +38 -67
  15. runic_py-0.3.0/runic/migrate/schema.py +350 -0
  16. {runic_py-0.2.2 → runic_py-0.3.0}/runic/migrate/testing.py +17 -6
  17. {runic_py-0.2.2 → runic_py-0.3.0}/runic/orm/__init__.py +45 -10
  18. {runic_py-0.2.2 → runic_py-0.3.0}/runic/orm/core/descriptors.py +7 -1
  19. {runic_py-0.2.2 → runic_py-0.3.0}/runic/orm/core/types.py +3 -0
  20. runic_py-0.3.0/runic/orm/driver/__init__.py +138 -0
  21. runic_py-0.3.0/runic/orm/driver/age.py +582 -0
  22. runic_py-0.3.0/runic/orm/driver/arcadedb.py +98 -0
  23. runic_py-0.3.0/runic/orm/driver/bolt.py +214 -0
  24. runic_py-0.3.0/runic/orm/driver/factory.py +108 -0
  25. runic_py-0.3.0/runic/orm/driver/falkordb.py +174 -0
  26. runic_py-0.3.0/runic/orm/driver/memgraph.py +133 -0
  27. runic_py-0.3.0/runic/orm/driver/neo4j.py +130 -0
  28. {runic_py-0.2.2 → runic_py-0.3.0}/runic/orm/mapper/mapper.py +105 -66
  29. {runic_py-0.2.2 → runic_py-0.3.0}/runic/orm/mapper/relationship_loader.py +26 -15
  30. {runic_py-0.2.2 → runic_py-0.3.0}/runic/orm/mapper/relationship_writer.py +29 -13
  31. runic_py-0.3.0/runic/orm/operations.py +88 -0
  32. {runic_py-0.2.2 → runic_py-0.3.0}/runic/orm/query/__init__.py +46 -7
  33. {runic_py-0.2.2 → runic_py-0.3.0}/runic/orm/query/builder.py +78 -480
  34. runic_py-0.3.0/runic/orm/query/specialised.py +350 -0
  35. {runic_py-0.2.2 → runic_py-0.3.0}/runic/orm/repository/__init__.py +1 -4
  36. {runic_py-0.2.2 → runic_py-0.3.0}/runic/orm/repository/async_repository.py +26 -28
  37. {runic_py-0.2.2 → runic_py-0.3.0}/runic/orm/repository/cypher.py +7 -22
  38. {runic_py-0.2.2 → runic_py-0.3.0}/runic/orm/repository/protocol.py +12 -8
  39. {runic_py-0.2.2 → runic_py-0.3.0}/runic/orm/repository/repository.py +32 -31
  40. runic_py-0.3.0/runic/orm/schema/__init__.py +8 -0
  41. runic_py-0.3.0/runic/orm/schema/index_manager.py +56 -0
  42. {runic_py-0.2.2 → runic_py-0.3.0}/runic/orm/session/async_session.py +153 -25
  43. {runic_py-0.2.2 → runic_py-0.3.0}/runic/orm/session/connection_pool.py +14 -13
  44. {runic_py-0.2.2 → runic_py-0.3.0}/runic/orm/session/session.py +190 -31
  45. runic_py-0.2.2/runic/orm/repository/pagination.py +0 -105
  46. runic_py-0.2.2/runic/orm/schema/__init__.py +0 -11
  47. runic_py-0.2.2/runic/orm/schema/index_manager.py +0 -195
  48. runic_py-0.2.2/runic/orm/schema/schema_manager.py +0 -173
  49. {runic_py-0.2.2 → runic_py-0.3.0}/LICENSE.md +0 -0
  50. {runic_py-0.2.2 → runic_py-0.3.0}/runic/__init__.py +0 -0
  51. {runic_py-0.2.2 → runic_py-0.3.0}/runic/migrate/autogen.py +0 -0
  52. {runic_py-0.2.2 → runic_py-0.3.0}/runic/migrate/checksum.py +0 -0
  53. {runic_py-0.2.2 → runic_py-0.3.0}/runic/migrate/config.py +0 -0
  54. {runic_py-0.2.2 → runic_py-0.3.0}/runic/migrate/context.py +0 -0
  55. {runic_py-0.2.2 → runic_py-0.3.0}/runic/migrate/exceptions.py +0 -0
  56. {runic_py-0.2.2 → runic_py-0.3.0}/runic/migrate/introspect.py +0 -0
  57. {runic_py-0.2.2 → runic_py-0.3.0}/runic/migrate/manifest.py +0 -0
  58. {runic_py-0.2.2 → runic_py-0.3.0}/runic/migrate/script.py +0 -0
  59. {runic_py-0.2.2 → runic_py-0.3.0}/runic/migrate/service.py +0 -0
  60. {runic_py-0.2.2 → runic_py-0.3.0}/runic/migrate/templates/env.py.mako +0 -0
  61. {runic_py-0.2.2 → runic_py-0.3.0}/runic/migrate/templates/script.py.mako +0 -0
  62. {runic_py-0.2.2 → runic_py-0.3.0}/runic/migrate/version.py +0 -0
  63. {runic_py-0.2.2 → runic_py-0.3.0}/runic/orm/core/__init__.py +0 -0
  64. {runic_py-0.2.2 → runic_py-0.3.0}/runic/orm/core/metadata.py +0 -0
  65. {runic_py-0.2.2 → runic_py-0.3.0}/runic/orm/core/models.py +0 -0
  66. {runic_py-0.2.2 → runic_py-0.3.0}/runic/orm/exceptions.py +0 -0
  67. {runic_py-0.2.2 → runic_py-0.3.0}/runic/orm/mapper/__init__.py +0 -0
  68. {runic_py-0.2.2 → runic_py-0.3.0}/runic/orm/query/expressions.py +0 -0
  69. {runic_py-0.2.2 → runic_py-0.3.0}/runic/orm/query/traversal.py +0 -0
  70. {runic_py-0.2.2 → runic_py-0.3.0}/runic/orm/session/__init__.py +0 -0
@@ -115,3 +115,4 @@ wheels/
115
115
  .task/
116
116
  .runic
117
117
  runic_test/
118
+ .claude/ralph-loop.local.md
@@ -1,11 +1,45 @@
1
+ Metadata-Version: 2.4
2
+ Name: runic-py
3
+ Version: 0.3.0
4
+ Summary: OGM and schema migrations for Graph Databases.
5
+ Project-URL: Homepage, https://runic-py.readthedocs.io/en/latest/
6
+ Project-URL: Repository, https://github.com/jenreh/runic
7
+ Author: Jens Rehpöhler
8
+ License: MIT
9
+ License-File: LICENSE.md
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3 :: Only
14
+ Classifier: Topic :: Home Automation
15
+ Requires-Python: >=3.14
16
+ Requires-Dist: mako<2.0.0,>=1.3.0
17
+ Requires-Dist: rich<16.0.0,>=15.0.0
18
+ Requires-Dist: typer<1.0.0,>=0.26.0
19
+ Provides-Extra: age
20
+ Requires-Dist: psycopg[binary]>=3.3.4; extra == 'age'
21
+ Provides-Extra: all
22
+ Requires-Dist: falkordb<2.0.0,>=1.6.0; extra == 'all'
23
+ Requires-Dist: neo4j>=6.2.0; extra == 'all'
24
+ Requires-Dist: psycopg[binary]>=3.3.4; extra == 'all'
25
+ Provides-Extra: arcadedb
26
+ Requires-Dist: neo4j>=6.2.0; extra == 'arcadedb'
27
+ Provides-Extra: falkordb
28
+ Requires-Dist: falkordb<2.0.0,>=1.6.0; extra == 'falkordb'
29
+ Provides-Extra: memgraph
30
+ Requires-Dist: neo4j>=6.2.0; extra == 'memgraph'
31
+ Provides-Extra: neo4j
32
+ Requires-Dist: neo4j>=6.2.0; extra == 'neo4j'
33
+ Description-Content-Type: text/markdown
34
+
1
35
  <div align="center">
2
36
  <img src="https://raw.githubusercontent.com/jenreh/runic/refs/heads/main/docs/source/_static/runic.svg" width="240" alt="Runic logo">
3
37
 
4
38
  # Runic
5
39
 
6
- **Graph schema migrations and ORM for FalkorDB.**
40
+ **Graph schema migrations and ORM for Cypher-based graph databases.**
7
41
 
8
- ![Version](https://img.shields.io/badge/version-0.2.2-blue)
42
+ ![Version](https://img.shields.io/badge/version-0.3.0-blue)
9
43
  [![Python](https://img.shields.io/badge/python-3.14%2B-orange)](https://www.python.org)
10
44
  [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE.md)
11
45
 
@@ -15,44 +49,64 @@
15
49
 
16
50
  ---
17
51
 
18
- **Runic** is a Python toolkit for [FalkorDB](https://falkordb.com/) that covers two layers:
52
+ **Runic** is a Python toolkit for Cypher-based graph databases that covers two layers:
19
53
 
20
54
  - **`runic.migrate`** — Alembic-style schema migrations with revision tracking, a CLI, and rollback snapshots.
21
- - **`runic.orm`** — A lightweight graph ORM: declare `Node` and `Edge` models, manage sessions, traverse relationships, and sync indexes — all using FalkorDB-native Cypher under the hood.
55
+ - **`runic.orm`** — A lightweight graph ORM: declare `Node` and `Edge` models, manage sessions, traverse relationships, and sync indexes — all via a pluggable driver layer supporting FalkorDB, ArcadeDB, and any Bolt-compatible database.
22
56
 
23
57
  ## Features
24
58
 
25
59
  ### Migration CLI
26
60
 
27
61
  - **Alembic-Style Workflow** — Familiar CLI verbs: `init`, `revision`, `upgrade`, `downgrade`, `current`, `baseline`.
28
- - **Graph-Native** — Migration state stored inside dedicated graph nodes (`:_FalkorMigrateVersion`).
62
+ - **Graph-Native** — Migration state stored inside dedicated graph nodes; no external state table needed.
29
63
  - **Idempotent Cypher** — Explicit, guarded migration steps; safe to replay on an empty graph.
30
64
  - **Offline & Dry Run** — Review generated Cypher scripts before running them in production.
31
- - **Rollback Snapshots** — Uses `GRAPH.COPY` for high-risk, non-reversible migrations.
65
+ - **Rollback Snapshots** — Optional snapshot-based rollback for high-risk, non-reversible migrations.
32
66
 
33
67
  ### Graph ORM
34
68
 
35
69
  - **Declarative Models** — `Node` and `Edge` subclasses with typed `Field` descriptors; no metaclass magic.
36
- - **Session & Repository** — Unit-of-work session with change tracking; typed `Repository` for queries and pagination.
70
+ - **Pluggable Driver Layer** — `GraphDriver` / `GraphDialect` protocols; built-in drivers for FalkorDB, ArcadeDB (via Bolt), and any Bolt-compatible DB. Switch backends without changing model code.
71
+ - **Session & Repository** — Unit-of-work session with change tracking; typed `Repository` for queries and offset reads.
37
72
  - **Relationships** — `Relation` field for INCOMING / OUTGOING edges; lazy and eager loading; edge property models.
38
73
  - **Schema Management** — `IndexManager` and `SchemaManager` to create, validate, and sync RANGE, FULLTEXT, and UNIQUE indexes.
39
- - **Native FalkorDB Types** — First-class `Vector` (vecf32), `GeoLocation` (point), interned strings, and auto-converters for `datetime` and `Enum`.
74
+ - **Native Graph Types** — First-class `Vector` (vecf32), `GeoLocation` (point), interned strings, and auto-converters for `datetime` and `Enum`.
40
75
  - **Async Support** — `AsyncSession`, `AsyncRepository`, and `AsyncConnectionManager` for async-first applications.
41
76
 
42
77
  ## Installation
43
78
 
44
- ```bash
45
- uv pip install runic
46
- ```
79
+ Install the core package, then add the optional extra for your database backend:
47
80
 
48
- Or add it to an existing project:
81
+ | Backend | Extra | Package installed |
82
+ | --- | --- | --- |
83
+ | FalkorDB | `falkordb` | `falkordb` |
84
+ | Neo4j | `neo4j` | `neo4j` |
85
+ | Memgraph | `memgraph` | `neo4j` (Bolt) |
86
+ | ArcadeDB | `arcadedb` | `neo4j` (Bolt) |
87
+ | Apache AGE | `age` | `psycopg[binary]` |
88
+ | All backends | `all` | all of the above |
49
89
 
50
90
  ```bash
51
- uv add runic
91
+ # FalkorDB
92
+ uv add "runic-py[falkordb]"
93
+
94
+ # Neo4j
95
+ uv add "runic-py[neo4j]"
96
+
97
+ # Memgraph or ArcadeDB (both use the Neo4j Bolt driver)
98
+ uv add "runic-py[memgraph]"
99
+ uv add "runic-py[arcadedb]"
100
+
101
+ # Apache AGE (PostgreSQL extension)
102
+ uv add "runic-py[age]"
103
+
104
+ # All backends at once
105
+ uv add "runic-py[all]"
52
106
  ```
53
107
 
54
108
  > [!NOTE]
55
- > Runic requires Python 3.14+ and is optimized for the latest FalkorDB clients.
109
+ > Runic requires Python 3.14+. The core package has no graph-driver dependency — install only the extra for your backend.
56
110
 
57
111
  ---
58
112
 
@@ -90,7 +144,7 @@ runic downgrade 1975e # roll back to a specific revision (prefix is enough)
90
144
 
91
145
  ### Baselining an existing graph
92
146
 
93
- Bring an unmanaged FalkorDB graph under runic control without re-running anything:
147
+ Bring an existing graph under runic control without re-running anything:
94
148
 
95
149
  ```bash
96
150
  runic baseline -m "baseline" # introspect, generate root revision, stamp it
@@ -112,9 +166,12 @@ from runic.migrate.adapters import create_adapter
112
166
 
113
167
  init(Path("runic/"))
114
168
 
169
+ # Any supported backend — swap the adapter name and kwargs
115
170
  adapter = create_adapter(
116
171
  "falkordb", url="falkor://localhost:6379", graph_name="my_graph"
117
172
  )
173
+ # adapter = create_adapter("neo4j", host="localhost", port=7687, database="neo4j", username="neo4j", password="secret")
174
+
118
175
  runic = Runic(adapter, script_location=Path("runic/"))
119
176
  runic.migrate.upgrade("head")
120
177
 
@@ -160,23 +217,28 @@ class Author(Node, labels=["Author"]):
160
217
 
161
218
  ### Session-based CRUD
162
219
 
220
+ `Session` accepts any `GraphDriver`. Use `create_driver()` to build one for your backend:
221
+
163
222
  ```python
164
- from runic.orm import Session, Repository
223
+ from runic.orm import Session, Repository, create_driver
165
224
 
166
- with Session(graph) as session:
225
+ # Pick your backend — the rest of the code is identical
226
+ driver = create_driver("falkordb", host="localhost", port=6379, graph="myapp")
227
+
228
+ with Session(driver) as session:
167
229
  session.add_all([
168
230
  User(id="alice", email="alice@example.com", name="Alice"),
169
231
  User(id="bob", email="bob@example.com", name="Bob"),
170
232
  ])
171
233
  session.commit()
172
234
 
173
- with Session(graph) as session:
235
+ with Session(driver) as session:
174
236
  repo = Repository(session, User)
175
237
  alice = session.get(User, "alice")
176
238
  alice.name = "Alice Smith" # change tracking — no explicit dirty flag
177
239
  session.commit()
178
240
 
179
- with Session(graph) as session:
241
+ with Session(driver) as session:
180
242
  user = session.get(User, "bob")
181
243
  session.delete(user)
182
244
  session.commit()
@@ -186,12 +248,12 @@ with Session(graph) as session:
186
248
 
187
249
  ```python
188
250
  # Lazy load (default) — triggers a query on first access
189
- with Session(graph) as session:
251
+ with Session(driver) as session:
190
252
  author = session.get(Author, "alice")
191
253
  posts = author.posts # query executed here
192
254
 
193
255
  # Eager load — single round-trip
194
- with Session(graph) as session:
256
+ with Session(driver) as session:
195
257
  author = session.get(Author, "alice", fetch=["posts"])
196
258
  posts = author.posts # already loaded, no extra query
197
259
  ```
@@ -199,12 +261,12 @@ with Session(graph) as session:
199
261
  ### Pagination and custom queries
200
262
 
201
263
  ```python
202
- from runic.orm import Pageable, Repository
264
+ from runic.orm import Repository
203
265
 
204
- with Session(graph) as session:
266
+ with Session(driver) as session:
205
267
  repo = Repository(session, User)
206
- page = repo.find_all_paginated(Pageable(page=0, size=20, sort_by="name"))
207
- print(f"{len(list(page))} of {page.total_elements} total")
268
+ first_page = repo.find_all(skip=0, limit=20)
269
+ next_page = repo.find_all(skip=20, limit=20)
208
270
  ```
209
271
 
210
272
  Extend `Repository` to add typed Cypher helpers:
@@ -219,6 +281,28 @@ class UserRepository(Repository[User]):
219
281
  )
220
282
  ```
221
283
 
284
+ ### Composable queries
285
+
286
+ `select()` creates a query statement independently of any session, enabling
287
+ dynamic query composition (e.g. conditional UI filters) before execution:
288
+
289
+ ```python
290
+ from runic.orm import select
291
+
292
+ stmt = select(User).where(User.active == True)
293
+ if min_age > 0:
294
+ stmt = stmt.where(User.age >= min_age)
295
+
296
+ with Session(driver) as session:
297
+ users: list[User] = session.scalars(stmt) # list[User]
298
+ user: User | None = session.scalar(stmt) # User | None
299
+ n: int = session.count(stmt) # int
300
+ ```
301
+
302
+ The same `stmt` is reusable — pass it to multiple sessions or execute it
303
+ multiple times. The older `session.query(User).where(...).all()` pattern is
304
+ still fully supported.
305
+
222
306
  ### Schema management
223
307
 
224
308
  Declare indexes inline on `Field`, then let `SchemaManager` keep the live graph in sync:
@@ -235,14 +319,14 @@ class Place(Node, labels=["Place"]):
235
319
  lon: float = Field(index=True)
236
320
 
237
321
 
238
- schema = SchemaManager(graph)
322
+ schema = SchemaManager(driver)
239
323
  schema.sync_schema([Place], drop_extra=False) # create missing; leave extras alone
240
324
 
241
325
  result = schema.validate_schema([Place])
242
326
  print("valid:", result.is_valid)
243
327
  ```
244
328
 
245
- ### Native FalkorDB types
329
+ ### Native graph types
246
330
 
247
331
  `Vector`, `GeoLocation`, `datetime`, and `Enum` fields get their converters assigned automatically — no `converter=` argument needed:
248
332
 
@@ -1,32 +1,11 @@
1
- Metadata-Version: 2.4
2
- Name: runic-py
3
- Version: 0.2.2
4
- Summary: OGM and schema migrations for FalkorDB.
5
- Project-URL: Homepage, https://runic-py.readthedocs.io/latest/
6
- Project-URL: Repository, https://github.com/jenreh/runic
7
- Author: Jens Rehpöhler
8
- License: MIT
9
- License-File: LICENSE.md
10
- Classifier: Intended Audience :: Developers
11
- Classifier: License :: OSI Approved :: MIT License
12
- Classifier: Programming Language :: Python :: 3
13
- Classifier: Programming Language :: Python :: 3 :: Only
14
- Classifier: Topic :: Home Automation
15
- Requires-Python: >=3.14
16
- Requires-Dist: falkordb<2.0.0,>=1.6.0
17
- Requires-Dist: mako<2.0.0,>=1.3.0
18
- Requires-Dist: rich<16.0.0,>=15.0.0
19
- Requires-Dist: typer<1.0.0,>=0.26.0
20
- Description-Content-Type: text/markdown
21
-
22
1
  <div align="center">
23
2
  <img src="https://raw.githubusercontent.com/jenreh/runic/refs/heads/main/docs/source/_static/runic.svg" width="240" alt="Runic logo">
24
3
 
25
4
  # Runic
26
5
 
27
- **Graph schema migrations and ORM for FalkorDB.**
6
+ **Graph schema migrations and ORM for Cypher-based graph databases.**
28
7
 
29
- ![Version](https://img.shields.io/badge/version-0.2.2-blue)
8
+ ![Version](https://img.shields.io/badge/version-0.3.0-blue)
30
9
  [![Python](https://img.shields.io/badge/python-3.14%2B-orange)](https://www.python.org)
31
10
  [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE.md)
32
11
 
@@ -36,44 +15,64 @@ Description-Content-Type: text/markdown
36
15
 
37
16
  ---
38
17
 
39
- **Runic** is a Python toolkit for [FalkorDB](https://falkordb.com/) that covers two layers:
18
+ **Runic** is a Python toolkit for Cypher-based graph databases that covers two layers:
40
19
 
41
20
  - **`runic.migrate`** — Alembic-style schema migrations with revision tracking, a CLI, and rollback snapshots.
42
- - **`runic.orm`** — A lightweight graph ORM: declare `Node` and `Edge` models, manage sessions, traverse relationships, and sync indexes — all using FalkorDB-native Cypher under the hood.
21
+ - **`runic.orm`** — A lightweight graph ORM: declare `Node` and `Edge` models, manage sessions, traverse relationships, and sync indexes — all via a pluggable driver layer supporting FalkorDB, ArcadeDB, and any Bolt-compatible database.
43
22
 
44
23
  ## Features
45
24
 
46
25
  ### Migration CLI
47
26
 
48
27
  - **Alembic-Style Workflow** — Familiar CLI verbs: `init`, `revision`, `upgrade`, `downgrade`, `current`, `baseline`.
49
- - **Graph-Native** — Migration state stored inside dedicated graph nodes (`:_FalkorMigrateVersion`).
28
+ - **Graph-Native** — Migration state stored inside dedicated graph nodes; no external state table needed.
50
29
  - **Idempotent Cypher** — Explicit, guarded migration steps; safe to replay on an empty graph.
51
30
  - **Offline & Dry Run** — Review generated Cypher scripts before running them in production.
52
- - **Rollback Snapshots** — Uses `GRAPH.COPY` for high-risk, non-reversible migrations.
31
+ - **Rollback Snapshots** — Optional snapshot-based rollback for high-risk, non-reversible migrations.
53
32
 
54
33
  ### Graph ORM
55
34
 
56
35
  - **Declarative Models** — `Node` and `Edge` subclasses with typed `Field` descriptors; no metaclass magic.
57
- - **Session & Repository** — Unit-of-work session with change tracking; typed `Repository` for queries and pagination.
36
+ - **Pluggable Driver Layer** — `GraphDriver` / `GraphDialect` protocols; built-in drivers for FalkorDB, ArcadeDB (via Bolt), and any Bolt-compatible DB. Switch backends without changing model code.
37
+ - **Session & Repository** — Unit-of-work session with change tracking; typed `Repository` for queries and offset reads.
58
38
  - **Relationships** — `Relation` field for INCOMING / OUTGOING edges; lazy and eager loading; edge property models.
59
39
  - **Schema Management** — `IndexManager` and `SchemaManager` to create, validate, and sync RANGE, FULLTEXT, and UNIQUE indexes.
60
- - **Native FalkorDB Types** — First-class `Vector` (vecf32), `GeoLocation` (point), interned strings, and auto-converters for `datetime` and `Enum`.
40
+ - **Native Graph Types** — First-class `Vector` (vecf32), `GeoLocation` (point), interned strings, and auto-converters for `datetime` and `Enum`.
61
41
  - **Async Support** — `AsyncSession`, `AsyncRepository`, and `AsyncConnectionManager` for async-first applications.
62
42
 
63
43
  ## Installation
64
44
 
65
- ```bash
66
- uv pip install runic
67
- ```
45
+ Install the core package, then add the optional extra for your database backend:
68
46
 
69
- Or add it to an existing project:
47
+ | Backend | Extra | Package installed |
48
+ | --- | --- | --- |
49
+ | FalkorDB | `falkordb` | `falkordb` |
50
+ | Neo4j | `neo4j` | `neo4j` |
51
+ | Memgraph | `memgraph` | `neo4j` (Bolt) |
52
+ | ArcadeDB | `arcadedb` | `neo4j` (Bolt) |
53
+ | Apache AGE | `age` | `psycopg[binary]` |
54
+ | All backends | `all` | all of the above |
70
55
 
71
56
  ```bash
72
- uv add runic
57
+ # FalkorDB
58
+ uv add "runic-py[falkordb]"
59
+
60
+ # Neo4j
61
+ uv add "runic-py[neo4j]"
62
+
63
+ # Memgraph or ArcadeDB (both use the Neo4j Bolt driver)
64
+ uv add "runic-py[memgraph]"
65
+ uv add "runic-py[arcadedb]"
66
+
67
+ # Apache AGE (PostgreSQL extension)
68
+ uv add "runic-py[age]"
69
+
70
+ # All backends at once
71
+ uv add "runic-py[all]"
73
72
  ```
74
73
 
75
74
  > [!NOTE]
76
- > Runic requires Python 3.14+ and is optimized for the latest FalkorDB clients.
75
+ > Runic requires Python 3.14+. The core package has no graph-driver dependency — install only the extra for your backend.
77
76
 
78
77
  ---
79
78
 
@@ -111,7 +110,7 @@ runic downgrade 1975e # roll back to a specific revision (prefix is enough)
111
110
 
112
111
  ### Baselining an existing graph
113
112
 
114
- Bring an unmanaged FalkorDB graph under runic control without re-running anything:
113
+ Bring an existing graph under runic control without re-running anything:
115
114
 
116
115
  ```bash
117
116
  runic baseline -m "baseline" # introspect, generate root revision, stamp it
@@ -133,9 +132,12 @@ from runic.migrate.adapters import create_adapter
133
132
 
134
133
  init(Path("runic/"))
135
134
 
135
+ # Any supported backend — swap the adapter name and kwargs
136
136
  adapter = create_adapter(
137
137
  "falkordb", url="falkor://localhost:6379", graph_name="my_graph"
138
138
  )
139
+ # adapter = create_adapter("neo4j", host="localhost", port=7687, database="neo4j", username="neo4j", password="secret")
140
+
139
141
  runic = Runic(adapter, script_location=Path("runic/"))
140
142
  runic.migrate.upgrade("head")
141
143
 
@@ -181,23 +183,28 @@ class Author(Node, labels=["Author"]):
181
183
 
182
184
  ### Session-based CRUD
183
185
 
186
+ `Session` accepts any `GraphDriver`. Use `create_driver()` to build one for your backend:
187
+
184
188
  ```python
185
- from runic.orm import Session, Repository
189
+ from runic.orm import Session, Repository, create_driver
186
190
 
187
- with Session(graph) as session:
191
+ # Pick your backend — the rest of the code is identical
192
+ driver = create_driver("falkordb", host="localhost", port=6379, graph="myapp")
193
+
194
+ with Session(driver) as session:
188
195
  session.add_all([
189
196
  User(id="alice", email="alice@example.com", name="Alice"),
190
197
  User(id="bob", email="bob@example.com", name="Bob"),
191
198
  ])
192
199
  session.commit()
193
200
 
194
- with Session(graph) as session:
201
+ with Session(driver) as session:
195
202
  repo = Repository(session, User)
196
203
  alice = session.get(User, "alice")
197
204
  alice.name = "Alice Smith" # change tracking — no explicit dirty flag
198
205
  session.commit()
199
206
 
200
- with Session(graph) as session:
207
+ with Session(driver) as session:
201
208
  user = session.get(User, "bob")
202
209
  session.delete(user)
203
210
  session.commit()
@@ -207,12 +214,12 @@ with Session(graph) as session:
207
214
 
208
215
  ```python
209
216
  # Lazy load (default) — triggers a query on first access
210
- with Session(graph) as session:
217
+ with Session(driver) as session:
211
218
  author = session.get(Author, "alice")
212
219
  posts = author.posts # query executed here
213
220
 
214
221
  # Eager load — single round-trip
215
- with Session(graph) as session:
222
+ with Session(driver) as session:
216
223
  author = session.get(Author, "alice", fetch=["posts"])
217
224
  posts = author.posts # already loaded, no extra query
218
225
  ```
@@ -220,12 +227,12 @@ with Session(graph) as session:
220
227
  ### Pagination and custom queries
221
228
 
222
229
  ```python
223
- from runic.orm import Pageable, Repository
230
+ from runic.orm import Repository
224
231
 
225
- with Session(graph) as session:
232
+ with Session(driver) as session:
226
233
  repo = Repository(session, User)
227
- page = repo.find_all_paginated(Pageable(page=0, size=20, sort_by="name"))
228
- print(f"{len(list(page))} of {page.total_elements} total")
234
+ first_page = repo.find_all(skip=0, limit=20)
235
+ next_page = repo.find_all(skip=20, limit=20)
229
236
  ```
230
237
 
231
238
  Extend `Repository` to add typed Cypher helpers:
@@ -240,6 +247,28 @@ class UserRepository(Repository[User]):
240
247
  )
241
248
  ```
242
249
 
250
+ ### Composable queries
251
+
252
+ `select()` creates a query statement independently of any session, enabling
253
+ dynamic query composition (e.g. conditional UI filters) before execution:
254
+
255
+ ```python
256
+ from runic.orm import select
257
+
258
+ stmt = select(User).where(User.active == True)
259
+ if min_age > 0:
260
+ stmt = stmt.where(User.age >= min_age)
261
+
262
+ with Session(driver) as session:
263
+ users: list[User] = session.scalars(stmt) # list[User]
264
+ user: User | None = session.scalar(stmt) # User | None
265
+ n: int = session.count(stmt) # int
266
+ ```
267
+
268
+ The same `stmt` is reusable — pass it to multiple sessions or execute it
269
+ multiple times. The older `session.query(User).where(...).all()` pattern is
270
+ still fully supported.
271
+
243
272
  ### Schema management
244
273
 
245
274
  Declare indexes inline on `Field`, then let `SchemaManager` keep the live graph in sync:
@@ -256,14 +285,14 @@ class Place(Node, labels=["Place"]):
256
285
  lon: float = Field(index=True)
257
286
 
258
287
 
259
- schema = SchemaManager(graph)
288
+ schema = SchemaManager(driver)
260
289
  schema.sync_schema([Place], drop_extra=False) # create missing; leave extras alone
261
290
 
262
291
  result = schema.validate_schema([Place])
263
292
  print("valid:", result.is_valid)
264
293
  ```
265
294
 
266
- ### Native FalkorDB types
295
+ ### Native graph types
267
296
 
268
297
  `Vector`, `GeoLocation`, `datetime`, and `Enum` fields get their converters assigned automatically — no `converter=` argument needed:
269
298
 
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "runic-py"
3
- version = "0.2.2"
4
- description = "OGM and schema migrations for FalkorDB."
3
+ version = "0.3.0"
4
+ description = "OGM and schema migrations for Graph Databases."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.14"
7
7
  license = { text = "MIT" }
@@ -18,10 +18,21 @@ classifiers = [
18
18
  dependencies = [
19
19
  "typer>=0.26.0,<1.0.0",
20
20
  "rich>=15.0.0,<16.0.0",
21
- "falkordb>=1.6.0,<2.0.0",
22
21
  "mako>=1.3.0,<2.0.0",
23
22
  ]
24
23
 
24
+ [project.optional-dependencies]
25
+ falkordb = ["falkordb>=1.6.0,<2.0.0"]
26
+ neo4j = ["neo4j>=6.2.0"]
27
+ memgraph = ["neo4j>=6.2.0"]
28
+ arcadedb = ["neo4j>=6.2.0"]
29
+ age = ["psycopg[binary]>=3.3.4"]
30
+ all = [
31
+ "falkordb>=1.6.0,<2.0.0",
32
+ "neo4j>=6.2.0",
33
+ "psycopg[binary]>=3.3.4",
34
+ ]
35
+
25
36
  [dependency-groups]
26
37
  dev = [
27
38
  "bump-my-version==1.3.0",
@@ -38,13 +49,17 @@ dev = [
38
49
  "sphinx-copybutton==0.5.2",
39
50
  "sphinx-design==0.7.0",
40
51
  "ty>=0.0.44",
52
+ "python-dotenv>=1.2.2",
53
+ "falkordb>=1.6.0,<2.0.0",
54
+ "neo4j>=6.2.0",
55
+ "psycopg[binary]>=3.3.4",
41
56
  ]
42
57
 
43
58
  [project.scripts]
44
59
  runic = "runic.migrate.cli:app"
45
60
 
46
61
  [project.urls]
47
- Homepage = "https://runic-py.readthedocs.io/latest/"
62
+ Homepage = "https://runic-py.readthedocs.io/en/latest/"
48
63
  Repository = "https://github.com/jenreh/runic"
49
64
 
50
65
  [tool.hatch.build.targets.wheel]
@@ -76,7 +91,7 @@ exclude = [
76
91
 
77
92
  [tool.bumpversion]
78
93
  commit = false
79
- current_version = "0.2.2"
94
+ current_version = "0.3.0"
80
95
  tag = false
81
96
  allow-dirty = false
82
97
 
@@ -230,8 +245,9 @@ cache_dir = ".cache/pytest"
230
245
  testpaths = ["tests"]
231
246
  asyncio_mode = "auto"
232
247
  markers = [
233
- "sonos_live: requires real Sonos hardware on the network",
234
- "integration: requires falkordblite (embedded FalkorDB, no external server)",
248
+ "integration: requires a live graph backend (embedded FalkorDB via redislite by default; other backends via RUNIC_TEST_BACKENDS env var + docker-compose.test.yml)",
249
+ "requires_multi_label: test requires multi-label node support (not available on Apache AGE)",
250
+ "requires_geo_update: test requires SET n.prop = point() support (not available on ArcadeDB via Bolt)",
235
251
  ]
236
252
 
237
253
  [tool.ty.environment]
@@ -245,12 +261,16 @@ exclude = [
245
261
  ".venv",
246
262
  "build",
247
263
  "dist",
264
+ "examples",
248
265
  ]
249
266
 
250
267
  [tool.ty.rules]
251
268
  unresolved-import = "warn"
252
269
  missing-argument = "error"
253
270
 
271
+ [tool.mypy]
272
+ cache_dir = ".cache/mypy"
273
+
254
274
  [tool.coverage.run]
255
275
  branch = true
256
276
  source = ["app", "runic"]
@@ -8,19 +8,29 @@ from runic.migrate.manifest import (
8
8
  UniqueConstraint,
9
9
  VectorIndex,
10
10
  )
11
+ from runic.migrate.schema import (
12
+ IndexManager,
13
+ SchemaInfo,
14
+ SchemaManager,
15
+ ValidationResult,
16
+ )
11
17
  from runic.migrate.script import AmbiguousRevision, RevisionNotFound
12
18
  from runic.migrate.service import init
13
19
 
14
20
  __all__ = [
15
21
  "AmbiguousRevision",
16
22
  "FulltextIndex",
23
+ "IndexManager",
17
24
  "IrreversibleMigrationError",
18
25
  "MandatoryConstraint",
19
26
  "RangeIndex",
20
27
  "RevisionNotFound",
21
28
  "Runic",
29
+ "SchemaInfo",
30
+ "SchemaManager",
22
31
  "SchemaManifest",
23
32
  "UniqueConstraint",
33
+ "ValidationResult",
24
34
  "VectorIndex",
25
35
  "context",
26
36
  "init",