runic-py 0.2.0__tar.gz → 0.2.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.
Files changed (55) hide show
  1. runic_py-0.2.2/PKG-INFO +294 -0
  2. runic_py-0.2.2/README.md +273 -0
  3. {runic_py-0.2.0 → runic_py-0.2.2}/pyproject.toml +3 -3
  4. {runic_py-0.2.0 → runic_py-0.2.2}/runic/migrate/cli.py +20 -16
  5. {runic_py-0.2.0 → runic_py-0.2.2}/runic/orm/__init__.py +73 -27
  6. {runic_py-0.2.0 → runic_py-0.2.2}/runic/orm/core/descriptors.py +118 -0
  7. runic_py-0.2.2/runic/orm/mapper/__init__.py +5 -0
  8. {runic_py-0.2.0 → runic_py-0.2.2}/runic/orm/mapper/mapper.py +75 -0
  9. runic_py-0.2.2/runic/orm/mapper/relationship_writer.py +145 -0
  10. runic_py-0.2.2/runic/orm/query/__init__.py +63 -0
  11. runic_py-0.2.2/runic/orm/query/builder.py +1516 -0
  12. runic_py-0.2.2/runic/orm/query/expressions.py +349 -0
  13. runic_py-0.2.2/runic/orm/query/traversal.py +144 -0
  14. {runic_py-0.2.0 → runic_py-0.2.2}/runic/orm/repository/async_repository.py +13 -0
  15. {runic_py-0.2.0 → runic_py-0.2.2}/runic/orm/repository/repository.py +19 -0
  16. {runic_py-0.2.0 → runic_py-0.2.2}/runic/orm/session/async_session.py +141 -8
  17. {runic_py-0.2.0 → runic_py-0.2.2}/runic/orm/session/session.py +206 -9
  18. runic_py-0.2.0/PKG-INFO +0 -192
  19. runic_py-0.2.0/README.md +0 -171
  20. runic_py-0.2.0/runic/orm/session/__init__.py +0 -0
  21. {runic_py-0.2.0 → runic_py-0.2.2}/.gitignore +0 -0
  22. {runic_py-0.2.0 → runic_py-0.2.2}/LICENSE.md +0 -0
  23. {runic_py-0.2.0 → runic_py-0.2.2}/runic/__init__.py +0 -0
  24. {runic_py-0.2.0 → runic_py-0.2.2}/runic/migrate/__init__.py +0 -0
  25. {runic_py-0.2.0 → runic_py-0.2.2}/runic/migrate/adapters/__init__.py +0 -0
  26. {runic_py-0.2.0 → runic_py-0.2.2}/runic/migrate/adapters/falkordb.py +0 -0
  27. {runic_py-0.2.0 → runic_py-0.2.2}/runic/migrate/autogen.py +0 -0
  28. {runic_py-0.2.0 → runic_py-0.2.2}/runic/migrate/checksum.py +0 -0
  29. {runic_py-0.2.0 → runic_py-0.2.2}/runic/migrate/config.py +0 -0
  30. {runic_py-0.2.0 → runic_py-0.2.2}/runic/migrate/context.py +0 -0
  31. {runic_py-0.2.0 → runic_py-0.2.2}/runic/migrate/exceptions.py +0 -0
  32. {runic_py-0.2.0 → runic_py-0.2.2}/runic/migrate/introspect.py +0 -0
  33. {runic_py-0.2.0 → runic_py-0.2.2}/runic/migrate/manifest.py +0 -0
  34. {runic_py-0.2.0 → runic_py-0.2.2}/runic/migrate/operations.py +0 -0
  35. {runic_py-0.2.0 → runic_py-0.2.2}/runic/migrate/script.py +0 -0
  36. {runic_py-0.2.0 → runic_py-0.2.2}/runic/migrate/service.py +0 -0
  37. {runic_py-0.2.0 → runic_py-0.2.2}/runic/migrate/templates/env.py.mako +0 -0
  38. {runic_py-0.2.0 → runic_py-0.2.2}/runic/migrate/templates/script.py.mako +0 -0
  39. {runic_py-0.2.0 → runic_py-0.2.2}/runic/migrate/testing.py +0 -0
  40. {runic_py-0.2.0 → runic_py-0.2.2}/runic/migrate/version.py +0 -0
  41. {runic_py-0.2.0 → runic_py-0.2.2}/runic/orm/core/__init__.py +0 -0
  42. {runic_py-0.2.0 → runic_py-0.2.2}/runic/orm/core/metadata.py +0 -0
  43. {runic_py-0.2.0 → runic_py-0.2.2}/runic/orm/core/models.py +0 -0
  44. {runic_py-0.2.0 → runic_py-0.2.2}/runic/orm/core/types.py +0 -0
  45. {runic_py-0.2.0 → runic_py-0.2.2}/runic/orm/exceptions.py +0 -0
  46. {runic_py-0.2.0 → runic_py-0.2.2}/runic/orm/mapper/relationship_loader.py +0 -0
  47. {runic_py-0.2.0 → runic_py-0.2.2}/runic/orm/repository/__init__.py +0 -0
  48. {runic_py-0.2.0 → runic_py-0.2.2}/runic/orm/repository/cypher.py +0 -0
  49. {runic_py-0.2.0 → runic_py-0.2.2}/runic/orm/repository/pagination.py +0 -0
  50. {runic_py-0.2.0 → runic_py-0.2.2}/runic/orm/repository/protocol.py +0 -0
  51. {runic_py-0.2.0 → runic_py-0.2.2}/runic/orm/schema/__init__.py +0 -0
  52. {runic_py-0.2.0 → runic_py-0.2.2}/runic/orm/schema/index_manager.py +0 -0
  53. {runic_py-0.2.0 → runic_py-0.2.2}/runic/orm/schema/schema_manager.py +0 -0
  54. {runic_py-0.2.0/runic/orm/mapper → runic_py-0.2.2/runic/orm/session}/__init__.py +0 -0
  55. {runic_py-0.2.0 → runic_py-0.2.2}/runic/orm/session/connection_pool.py +0 -0
@@ -0,0 +1,294 @@
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
+ <div align="center">
23
+ <img src="https://raw.githubusercontent.com/jenreh/runic/refs/heads/main/docs/source/_static/runic.svg" width="240" alt="Runic logo">
24
+
25
+ # Runic
26
+
27
+ **Graph schema migrations and ORM for FalkorDB.**
28
+
29
+ ![Version](https://img.shields.io/badge/version-0.2.2-blue)
30
+ [![Python](https://img.shields.io/badge/python-3.14%2B-orange)](https://www.python.org)
31
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE.md)
32
+
33
+ [Features](#features) • [Installation](#installation) • [Migrations](#migrations) • [ORM](#runicorm) • [Documentation](#documentation)
34
+
35
+ </div>
36
+
37
+ ---
38
+
39
+ **Runic** is a Python toolkit for [FalkorDB](https://falkordb.com/) that covers two layers:
40
+
41
+ - **`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.
43
+
44
+ ## Features
45
+
46
+ ### Migration CLI
47
+
48
+ - **Alembic-Style Workflow** — Familiar CLI verbs: `init`, `revision`, `upgrade`, `downgrade`, `current`, `baseline`.
49
+ - **Graph-Native** — Migration state stored inside dedicated graph nodes (`:_FalkorMigrateVersion`).
50
+ - **Idempotent Cypher** — Explicit, guarded migration steps; safe to replay on an empty graph.
51
+ - **Offline & Dry Run** — Review generated Cypher scripts before running them in production.
52
+ - **Rollback Snapshots** — Uses `GRAPH.COPY` for high-risk, non-reversible migrations.
53
+
54
+ ### Graph ORM
55
+
56
+ - **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.
58
+ - **Relationships** — `Relation` field for INCOMING / OUTGOING edges; lazy and eager loading; edge property models.
59
+ - **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`.
61
+ - **Async Support** — `AsyncSession`, `AsyncRepository`, and `AsyncConnectionManager` for async-first applications.
62
+
63
+ ## Installation
64
+
65
+ ```bash
66
+ uv pip install runic
67
+ ```
68
+
69
+ Or add it to an existing project:
70
+
71
+ ```bash
72
+ uv add runic
73
+ ```
74
+
75
+ > [!NOTE]
76
+ > Runic requires Python 3.14+ and is optimized for the latest FalkorDB clients.
77
+
78
+ ---
79
+
80
+ ## Migrations
81
+
82
+ Initialize your project and generate a new revision:
83
+
84
+ ```bash
85
+ runic init
86
+ runic revision -m "create user index"
87
+ ```
88
+
89
+ Open the generated file in `runic/versions/` and define your upgrade and downgrade:
90
+
91
+ ```python
92
+ revision = "1975ea83b712"
93
+ down_revision = None
94
+
95
+
96
+ def upgrade(op) -> None:
97
+ op.create_range_index("User", "email")
98
+
99
+
100
+ def downgrade(op) -> None:
101
+ op.drop_range_index("User", "email")
102
+ ```
103
+
104
+ Apply or roll back:
105
+
106
+ ```bash
107
+ runic upgrade # apply all pending revisions
108
+ runic downgrade # roll back one step
109
+ runic downgrade 1975e # roll back to a specific revision (prefix is enough)
110
+ ```
111
+
112
+ ### Baselining an existing graph
113
+
114
+ Bring an unmanaged FalkorDB graph under runic control without re-running anything:
115
+
116
+ ```bash
117
+ runic baseline -m "baseline" # introspect, generate root revision, stamp it
118
+ runic current # verify it is now tracked
119
+ ```
120
+
121
+ The generated revision recreates all indexes from scratch — safe to replay on a fresh graph (CI, cloning, new tenants):
122
+
123
+ ```bash
124
+ runic upgrade head # rebuilds full schema on an empty graph
125
+ ```
126
+
127
+ ### Programmatic SDK
128
+
129
+ ```python
130
+ from pathlib import Path
131
+ from runic import Runic, init
132
+ from runic.migrate.adapters import create_adapter
133
+
134
+ init(Path("runic/"))
135
+
136
+ adapter = create_adapter(
137
+ "falkordb", url="falkor://localhost:6379", graph_name="my_graph"
138
+ )
139
+ runic = Runic(adapter, script_location=Path("runic/"))
140
+ runic.migrate.upgrade("head")
141
+
142
+ print("current:", runic.migrate.current())
143
+ ```
144
+
145
+ ---
146
+
147
+ ## runic.orm
148
+
149
+ ### Defining models
150
+
151
+ ```python
152
+ from runic.orm import Field, Node, Edge, Relation
153
+
154
+
155
+ class User(Node, labels=["User"]):
156
+ id: str
157
+ email: str = Field(unique=True)
158
+ name: str
159
+
160
+
161
+ class Post(Node, labels=["Post"]):
162
+ id: str
163
+ title: str = Field(index_type="FULLTEXT")
164
+ published: bool = False
165
+
166
+
167
+ class AuthoredEdge(Edge, type="AUTHORED"):
168
+ created_at: str # ISO-8601
169
+
170
+
171
+ class Author(Node, labels=["Author"]):
172
+ id: str
173
+ name: str
174
+ posts: list[Post] = Relation(
175
+ relationship="AUTHORED",
176
+ direction="OUTGOING",
177
+ target="Post",
178
+ edge_model=AuthoredEdge,
179
+ )
180
+ ```
181
+
182
+ ### Session-based CRUD
183
+
184
+ ```python
185
+ from runic.orm import Session, Repository
186
+
187
+ with Session(graph) as session:
188
+ session.add_all([
189
+ User(id="alice", email="alice@example.com", name="Alice"),
190
+ User(id="bob", email="bob@example.com", name="Bob"),
191
+ ])
192
+ session.commit()
193
+
194
+ with Session(graph) as session:
195
+ repo = Repository(session, User)
196
+ alice = session.get(User, "alice")
197
+ alice.name = "Alice Smith" # change tracking — no explicit dirty flag
198
+ session.commit()
199
+
200
+ with Session(graph) as session:
201
+ user = session.get(User, "bob")
202
+ session.delete(user)
203
+ session.commit()
204
+ ```
205
+
206
+ ### Relationships
207
+
208
+ ```python
209
+ # Lazy load (default) — triggers a query on first access
210
+ with Session(graph) as session:
211
+ author = session.get(Author, "alice")
212
+ posts = author.posts # query executed here
213
+
214
+ # Eager load — single round-trip
215
+ with Session(graph) as session:
216
+ author = session.get(Author, "alice", fetch=["posts"])
217
+ posts = author.posts # already loaded, no extra query
218
+ ```
219
+
220
+ ### Pagination and custom queries
221
+
222
+ ```python
223
+ from runic.orm import Pageable, Repository
224
+
225
+ with Session(graph) as session:
226
+ 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")
229
+ ```
230
+
231
+ Extend `Repository` to add typed Cypher helpers:
232
+
233
+ ```python
234
+ class UserRepository(Repository[User]):
235
+ def find_by_email(self, email: str) -> User | None:
236
+ return self.cypher_one(
237
+ "MATCH (u:User {email: $email}) RETURN u",
238
+ {"email": email},
239
+ returns=User,
240
+ )
241
+ ```
242
+
243
+ ### Schema management
244
+
245
+ Declare indexes inline on `Field`, then let `SchemaManager` keep the live graph in sync:
246
+
247
+ ```python
248
+ from runic.orm import Field, IndexManager, Node, SchemaManager
249
+
250
+
251
+ class Place(Node, labels=["Place"]):
252
+ id: str
253
+ name: str = Field(index_type="FULLTEXT")
254
+ slug: str = Field(unique=True)
255
+ lat: float = Field(index=True)
256
+ lon: float = Field(index=True)
257
+
258
+
259
+ schema = SchemaManager(graph)
260
+ schema.sync_schema([Place], drop_extra=False) # create missing; leave extras alone
261
+
262
+ result = schema.validate_schema([Place])
263
+ print("valid:", result.is_valid)
264
+ ```
265
+
266
+ ### Native FalkorDB types
267
+
268
+ `Vector`, `GeoLocation`, `datetime`, and `Enum` fields get their converters assigned automatically — no `converter=` argument needed:
269
+
270
+ ```python
271
+ from datetime import UTC, datetime
272
+ from enum import StrEnum
273
+ from runic.orm import Field, GeoLocation, Node, Vector
274
+
275
+
276
+ class Status(StrEnum):
277
+ DRAFT = "draft"
278
+ PUBLISHED = "published"
279
+
280
+
281
+ class Article(Node, labels=["Article"]):
282
+ id: str = Field(primary_key=True)
283
+ category: str = Field(interned=True) # intern() deduplication
284
+ status: Status # EnumConverter auto-assigned
285
+ published_at: datetime | None = None # DatetimeConverter auto-assigned
286
+ embedding: Vector | None = None # VectorConverter → vecf32()
287
+ origin: GeoLocation | None = None # GeoLocationConverter → point()
288
+ ```
289
+
290
+ ---
291
+
292
+ ## Documentation
293
+
294
+ Full conceptual overview, async usage, advanced CLI flags, and API reference at the complete [Runic Documentation](https://runic-py.readthedocs.io/latest/).
@@ -0,0 +1,273 @@
1
+ <div align="center">
2
+ <img src="https://raw.githubusercontent.com/jenreh/runic/refs/heads/main/docs/source/_static/runic.svg" width="240" alt="Runic logo">
3
+
4
+ # Runic
5
+
6
+ **Graph schema migrations and ORM for FalkorDB.**
7
+
8
+ ![Version](https://img.shields.io/badge/version-0.2.2-blue)
9
+ [![Python](https://img.shields.io/badge/python-3.14%2B-orange)](https://www.python.org)
10
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE.md)
11
+
12
+ [Features](#features) • [Installation](#installation) • [Migrations](#migrations) • [ORM](#runicorm) • [Documentation](#documentation)
13
+
14
+ </div>
15
+
16
+ ---
17
+
18
+ **Runic** is a Python toolkit for [FalkorDB](https://falkordb.com/) that covers two layers:
19
+
20
+ - **`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.
22
+
23
+ ## Features
24
+
25
+ ### Migration CLI
26
+
27
+ - **Alembic-Style Workflow** — Familiar CLI verbs: `init`, `revision`, `upgrade`, `downgrade`, `current`, `baseline`.
28
+ - **Graph-Native** — Migration state stored inside dedicated graph nodes (`:_FalkorMigrateVersion`).
29
+ - **Idempotent Cypher** — Explicit, guarded migration steps; safe to replay on an empty graph.
30
+ - **Offline & Dry Run** — Review generated Cypher scripts before running them in production.
31
+ - **Rollback Snapshots** — Uses `GRAPH.COPY` for high-risk, non-reversible migrations.
32
+
33
+ ### Graph ORM
34
+
35
+ - **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.
37
+ - **Relationships** — `Relation` field for INCOMING / OUTGOING edges; lazy and eager loading; edge property models.
38
+ - **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`.
40
+ - **Async Support** — `AsyncSession`, `AsyncRepository`, and `AsyncConnectionManager` for async-first applications.
41
+
42
+ ## Installation
43
+
44
+ ```bash
45
+ uv pip install runic
46
+ ```
47
+
48
+ Or add it to an existing project:
49
+
50
+ ```bash
51
+ uv add runic
52
+ ```
53
+
54
+ > [!NOTE]
55
+ > Runic requires Python 3.14+ and is optimized for the latest FalkorDB clients.
56
+
57
+ ---
58
+
59
+ ## Migrations
60
+
61
+ Initialize your project and generate a new revision:
62
+
63
+ ```bash
64
+ runic init
65
+ runic revision -m "create user index"
66
+ ```
67
+
68
+ Open the generated file in `runic/versions/` and define your upgrade and downgrade:
69
+
70
+ ```python
71
+ revision = "1975ea83b712"
72
+ down_revision = None
73
+
74
+
75
+ def upgrade(op) -> None:
76
+ op.create_range_index("User", "email")
77
+
78
+
79
+ def downgrade(op) -> None:
80
+ op.drop_range_index("User", "email")
81
+ ```
82
+
83
+ Apply or roll back:
84
+
85
+ ```bash
86
+ runic upgrade # apply all pending revisions
87
+ runic downgrade # roll back one step
88
+ runic downgrade 1975e # roll back to a specific revision (prefix is enough)
89
+ ```
90
+
91
+ ### Baselining an existing graph
92
+
93
+ Bring an unmanaged FalkorDB graph under runic control without re-running anything:
94
+
95
+ ```bash
96
+ runic baseline -m "baseline" # introspect, generate root revision, stamp it
97
+ runic current # verify it is now tracked
98
+ ```
99
+
100
+ The generated revision recreates all indexes from scratch — safe to replay on a fresh graph (CI, cloning, new tenants):
101
+
102
+ ```bash
103
+ runic upgrade head # rebuilds full schema on an empty graph
104
+ ```
105
+
106
+ ### Programmatic SDK
107
+
108
+ ```python
109
+ from pathlib import Path
110
+ from runic import Runic, init
111
+ from runic.migrate.adapters import create_adapter
112
+
113
+ init(Path("runic/"))
114
+
115
+ adapter = create_adapter(
116
+ "falkordb", url="falkor://localhost:6379", graph_name="my_graph"
117
+ )
118
+ runic = Runic(adapter, script_location=Path("runic/"))
119
+ runic.migrate.upgrade("head")
120
+
121
+ print("current:", runic.migrate.current())
122
+ ```
123
+
124
+ ---
125
+
126
+ ## runic.orm
127
+
128
+ ### Defining models
129
+
130
+ ```python
131
+ from runic.orm import Field, Node, Edge, Relation
132
+
133
+
134
+ class User(Node, labels=["User"]):
135
+ id: str
136
+ email: str = Field(unique=True)
137
+ name: str
138
+
139
+
140
+ class Post(Node, labels=["Post"]):
141
+ id: str
142
+ title: str = Field(index_type="FULLTEXT")
143
+ published: bool = False
144
+
145
+
146
+ class AuthoredEdge(Edge, type="AUTHORED"):
147
+ created_at: str # ISO-8601
148
+
149
+
150
+ class Author(Node, labels=["Author"]):
151
+ id: str
152
+ name: str
153
+ posts: list[Post] = Relation(
154
+ relationship="AUTHORED",
155
+ direction="OUTGOING",
156
+ target="Post",
157
+ edge_model=AuthoredEdge,
158
+ )
159
+ ```
160
+
161
+ ### Session-based CRUD
162
+
163
+ ```python
164
+ from runic.orm import Session, Repository
165
+
166
+ with Session(graph) as session:
167
+ session.add_all([
168
+ User(id="alice", email="alice@example.com", name="Alice"),
169
+ User(id="bob", email="bob@example.com", name="Bob"),
170
+ ])
171
+ session.commit()
172
+
173
+ with Session(graph) as session:
174
+ repo = Repository(session, User)
175
+ alice = session.get(User, "alice")
176
+ alice.name = "Alice Smith" # change tracking — no explicit dirty flag
177
+ session.commit()
178
+
179
+ with Session(graph) as session:
180
+ user = session.get(User, "bob")
181
+ session.delete(user)
182
+ session.commit()
183
+ ```
184
+
185
+ ### Relationships
186
+
187
+ ```python
188
+ # Lazy load (default) — triggers a query on first access
189
+ with Session(graph) as session:
190
+ author = session.get(Author, "alice")
191
+ posts = author.posts # query executed here
192
+
193
+ # Eager load — single round-trip
194
+ with Session(graph) as session:
195
+ author = session.get(Author, "alice", fetch=["posts"])
196
+ posts = author.posts # already loaded, no extra query
197
+ ```
198
+
199
+ ### Pagination and custom queries
200
+
201
+ ```python
202
+ from runic.orm import Pageable, Repository
203
+
204
+ with Session(graph) as session:
205
+ 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")
208
+ ```
209
+
210
+ Extend `Repository` to add typed Cypher helpers:
211
+
212
+ ```python
213
+ class UserRepository(Repository[User]):
214
+ def find_by_email(self, email: str) -> User | None:
215
+ return self.cypher_one(
216
+ "MATCH (u:User {email: $email}) RETURN u",
217
+ {"email": email},
218
+ returns=User,
219
+ )
220
+ ```
221
+
222
+ ### Schema management
223
+
224
+ Declare indexes inline on `Field`, then let `SchemaManager` keep the live graph in sync:
225
+
226
+ ```python
227
+ from runic.orm import Field, IndexManager, Node, SchemaManager
228
+
229
+
230
+ class Place(Node, labels=["Place"]):
231
+ id: str
232
+ name: str = Field(index_type="FULLTEXT")
233
+ slug: str = Field(unique=True)
234
+ lat: float = Field(index=True)
235
+ lon: float = Field(index=True)
236
+
237
+
238
+ schema = SchemaManager(graph)
239
+ schema.sync_schema([Place], drop_extra=False) # create missing; leave extras alone
240
+
241
+ result = schema.validate_schema([Place])
242
+ print("valid:", result.is_valid)
243
+ ```
244
+
245
+ ### Native FalkorDB types
246
+
247
+ `Vector`, `GeoLocation`, `datetime`, and `Enum` fields get their converters assigned automatically — no `converter=` argument needed:
248
+
249
+ ```python
250
+ from datetime import UTC, datetime
251
+ from enum import StrEnum
252
+ from runic.orm import Field, GeoLocation, Node, Vector
253
+
254
+
255
+ class Status(StrEnum):
256
+ DRAFT = "draft"
257
+ PUBLISHED = "published"
258
+
259
+
260
+ class Article(Node, labels=["Article"]):
261
+ id: str = Field(primary_key=True)
262
+ category: str = Field(interned=True) # intern() deduplication
263
+ status: Status # EnumConverter auto-assigned
264
+ published_at: datetime | None = None # DatetimeConverter auto-assigned
265
+ embedding: Vector | None = None # VectorConverter → vecf32()
266
+ origin: GeoLocation | None = None # GeoLocationConverter → point()
267
+ ```
268
+
269
+ ---
270
+
271
+ ## Documentation
272
+
273
+ Full conceptual overview, async usage, advanced CLI flags, and API reference at the complete [Runic Documentation](https://runic-py.readthedocs.io/latest/).
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "runic-py"
3
- version = "0.2.0"
4
- description = "Graph schema migrations for FalkorDB."
3
+ version = "0.2.2"
4
+ description = "OGM and schema migrations for FalkorDB."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.14"
7
7
  license = { text = "MIT" }
@@ -76,7 +76,7 @@ exclude = [
76
76
 
77
77
  [tool.bumpversion]
78
78
  commit = false
79
- current_version = "0.2.0"
79
+ current_version = "0.2.2"
80
80
  tag = false
81
81
  allow-dirty = false
82
82
 
@@ -41,11 +41,13 @@ def _resolve_config(config: Path) -> Path:
41
41
  return config
42
42
 
43
43
 
44
- _DB_CONNECTION_ERROR_NAMES = frozenset({
45
- "AuthenticationError",
46
- "ConnectionError",
47
- "TimeoutError",
48
- })
44
+ _DB_CONNECTION_ERROR_NAMES = frozenset(
45
+ {
46
+ "AuthenticationError",
47
+ "ConnectionError",
48
+ "TimeoutError",
49
+ }
50
+ )
49
51
 
50
52
 
51
53
  def _exec_env(config: Path, preview: bool = False) -> dict: # noqa: ARG001
@@ -358,17 +360,19 @@ def history(
358
360
  from runic.migrate.script import RevisionInfo
359
361
 
360
362
  items = list(
361
- reversed([
362
- RevisionInfo(
363
- revision=r.revision,
364
- down_revision=r.down_revision,
365
- message=r.message,
366
- create_date=r.create_date,
367
- is_head=False,
368
- is_branch_point=r.revision in bp_set,
369
- )
370
- for r in sd.walk_revisions(start, end, direction="up")
371
- ])
363
+ reversed(
364
+ [
365
+ RevisionInfo(
366
+ revision=r.revision,
367
+ down_revision=r.down_revision,
368
+ message=r.message,
369
+ create_date=r.create_date,
370
+ is_head=False,
371
+ is_branch_point=r.revision in bp_set,
372
+ )
373
+ for r in sd.walk_revisions(start, end, direction="up")
374
+ ]
375
+ )
372
376
  )
373
377
  else:
374
378
  items = list(reversed(sd.revision_history()))