crochet-migration 0.1.0__py3-none-any.whl
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.
- crochet/__init__.py +3 -0
- crochet/cli.py +327 -0
- crochet/config.py +116 -0
- crochet/errors.py +75 -0
- crochet/ingest/__init__.py +5 -0
- crochet/ingest/batch.py +61 -0
- crochet/ir/__init__.py +23 -0
- crochet/ir/diff.py +199 -0
- crochet/ir/hash.py +36 -0
- crochet/ir/parser.py +251 -0
- crochet/ir/schema.py +196 -0
- crochet/ledger/__init__.py +5 -0
- crochet/ledger/sqlite.py +282 -0
- crochet/migrations/__init__.py +6 -0
- crochet/migrations/engine.py +279 -0
- crochet/migrations/operations.py +267 -0
- crochet/migrations/template.py +105 -0
- crochet/scaffold/__init__.py +6 -0
- crochet/scaffold/node.py +48 -0
- crochet/scaffold/relationship.py +52 -0
- crochet/verify.py +141 -0
- crochet_migration-0.1.0.dist-info/METADATA +278 -0
- crochet_migration-0.1.0.dist-info/RECORD +26 -0
- crochet_migration-0.1.0.dist-info/WHEEL +4 -0
- crochet_migration-0.1.0.dist-info/entry_points.txt +2 -0
- crochet_migration-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: crochet-migration
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Versioned schema & data migrations for neomodel Neo4j graphs
|
|
5
|
+
Project-URL: Homepage, https://github.com/keshavd/crochet
|
|
6
|
+
Project-URL: Repository, https://github.com/keshavd/crochet
|
|
7
|
+
Project-URL: Issues, https://github.com/keshavd/crochet/issues
|
|
8
|
+
Author-email: Keshav Dial <y.inuyasha@gmail.com>
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: database,graph,migrations,neo4j,neomodel,schema
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Database
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Requires-Dist: click>=8.0
|
|
24
|
+
Requires-Dist: neomodel>=5.0
|
|
25
|
+
Requires-Dist: toml>=0.10
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: mypy; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# Crochet
|
|
34
|
+
|
|
35
|
+
Versioned schema & data migrations for [neomodel](https://github.com/neo4j-contrib/neomodel) Neo4j graphs.
|
|
36
|
+
|
|
37
|
+
Crochet is a Git-backed, migration-driven framework that makes neomodel-defined
|
|
38
|
+
Neo4j graphs evolvable, auditable, and rollback-safe without relying on database
|
|
39
|
+
introspection.
|
|
40
|
+
|
|
41
|
+
## Problem It Solves
|
|
42
|
+
|
|
43
|
+
- neomodel has no native schema diff or migration system
|
|
44
|
+
- Neo4j is schemaless, so schema drift is silent
|
|
45
|
+
- Data loading and schema evolution are often intertwined but unmanaged
|
|
46
|
+
- Rollbacks are usually impossible or unsafe
|
|
47
|
+
- Git history and database state frequently diverge
|
|
48
|
+
|
|
49
|
+
Crochet enforces alignment between neomodel code, data ingests, and the live
|
|
50
|
+
graph.
|
|
51
|
+
|
|
52
|
+
## Installation
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install crochet
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
For development:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
pip install -e ".[dev]"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Quick Start
|
|
65
|
+
|
|
66
|
+
### 1. Initialize a project
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
crochet new-project --name my-graph
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
This creates:
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
my-graph/
|
|
76
|
+
crochet.toml # project config
|
|
77
|
+
models/ # neomodel definitions
|
|
78
|
+
migrations/ # migration files
|
|
79
|
+
.crochet/ledger.db # SQLite ledger
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### 2. Create node and relationship models
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
crochet create-node Person
|
|
86
|
+
crochet create-relationship Friendship --rel-type FRIENDS_WITH
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Each model gets an immutable `__kgid__` identifier. Models can be renamed or
|
|
90
|
+
moved across files without losing identity, because the `__kgid__` is what
|
|
91
|
+
Crochet tracks — not class names or file paths.
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
# models/person.py
|
|
95
|
+
from neomodel import StructuredNode, StringProperty, IntegerProperty
|
|
96
|
+
|
|
97
|
+
class Person(StructuredNode):
|
|
98
|
+
__kgid__ = "person_v1"
|
|
99
|
+
name = StringProperty(required=True, unique_index=True)
|
|
100
|
+
age = IntegerProperty(index=True)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### 3. Create a migration
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
crochet create-migration "add person node"
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Crochet snapshots the current schema IR, diffs it against the previous
|
|
110
|
+
snapshot, and scaffolds a migration file with detected changes as comments:
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
# migrations/0001_add_person_node.py
|
|
114
|
+
|
|
115
|
+
revision_id = "0001_add_person_node"
|
|
116
|
+
parent_id = None
|
|
117
|
+
schema_hash = "a1b2c3..."
|
|
118
|
+
rollback_safe = True
|
|
119
|
+
|
|
120
|
+
def upgrade(ctx):
|
|
121
|
+
ctx.add_unique_constraint("Person", "name")
|
|
122
|
+
ctx.add_index("Person", "age")
|
|
123
|
+
|
|
124
|
+
def downgrade(ctx):
|
|
125
|
+
ctx.drop_index("Person", "age")
|
|
126
|
+
ctx.drop_unique_constraint("Person", "name")
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### 4. Apply migrations
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
crochet upgrade # apply all pending
|
|
133
|
+
crochet upgrade --dry-run # preview without executing
|
|
134
|
+
crochet upgrade --target 0001_add_person_node # apply up to a specific revision
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### 5. Revert migrations
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
crochet downgrade # revert the most recent migration
|
|
141
|
+
crochet downgrade --target 0001_add_person_node # revert down to a target
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Rollback-unsafe migrations will refuse to downgrade and raise an error.
|
|
145
|
+
|
|
146
|
+
### 6. Check status and verify
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
crochet status # show applied/pending migrations, head, batches
|
|
150
|
+
crochet verify # check ledger chain, file presence, schema hash consistency
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Core Concepts
|
|
154
|
+
|
|
155
|
+
### Intermediate Representation (IR)
|
|
156
|
+
|
|
157
|
+
neomodel files are parsed into an intermediate schema representation. IR
|
|
158
|
+
snapshots can be hashed, serialized, and diffed. No Neo4j connection is
|
|
159
|
+
required for schema comparison.
|
|
160
|
+
|
|
161
|
+
### Hash-Chained Migrations
|
|
162
|
+
|
|
163
|
+
Migrations are ordered by a parent chain (Alembic-style). Each migration
|
|
164
|
+
records the schema hash at the time it was created, so drift between code and
|
|
165
|
+
migrations is detectable.
|
|
166
|
+
|
|
167
|
+
### SQLite Ledger
|
|
168
|
+
|
|
169
|
+
A local SQLite database (`.crochet/ledger.db`) is the authoritative record of:
|
|
170
|
+
|
|
171
|
+
- Applied migrations and their order
|
|
172
|
+
- Dataset batches with file checksums and loader versions
|
|
173
|
+
- Schema snapshots for diffing
|
|
174
|
+
|
|
175
|
+
### Deterministic Data Ingest
|
|
176
|
+
|
|
177
|
+
Data loading is a first-class migration operation. The `MigrationContext`
|
|
178
|
+
provides helpers for batch-tracked ingests:
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
def upgrade(ctx):
|
|
182
|
+
batch_id = ctx.begin_batch()
|
|
183
|
+
ctx.create_nodes("Person", [
|
|
184
|
+
{"name": "Alice", "age": 30},
|
|
185
|
+
{"name": "Bob", "age": 25},
|
|
186
|
+
])
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Every node and relationship created through a batch is tagged with
|
|
190
|
+
`_crochet_batch`, enabling delete-by-batch rollback.
|
|
191
|
+
|
|
192
|
+
### Rollback Semantics
|
|
193
|
+
|
|
194
|
+
Rollbacks are explicitly declared, not assumed:
|
|
195
|
+
|
|
196
|
+
- Append-only ingests support `delete_nodes_by_batch` / `delete_relationships_by_batch`
|
|
197
|
+
- Destructive transforms must set `rollback_safe = False`
|
|
198
|
+
- Unsafe downgrades are prevented by policy
|
|
199
|
+
|
|
200
|
+
## Migration Context Operations
|
|
201
|
+
|
|
202
|
+
The `MigrationContext` passed to `upgrade()` and `downgrade()` provides:
|
|
203
|
+
|
|
204
|
+
| Operation | Description |
|
|
205
|
+
|-----------|-------------|
|
|
206
|
+
| `add_unique_constraint(label, prop)` | Create a uniqueness constraint |
|
|
207
|
+
| `drop_unique_constraint(label, prop)` | Drop a uniqueness constraint |
|
|
208
|
+
| `add_node_property_existence_constraint(label, prop)` | Create a NOT NULL constraint |
|
|
209
|
+
| `drop_node_property_existence_constraint(label, prop)` | Drop a NOT NULL constraint |
|
|
210
|
+
| `add_index(label, prop)` | Create an index |
|
|
211
|
+
| `drop_index(label, prop)` | Drop an index |
|
|
212
|
+
| `rename_label(old, new)` | Rename a node label |
|
|
213
|
+
| `rename_relationship_type(old, new)` | Rename a relationship type |
|
|
214
|
+
| `add_node_property(label, prop, default)` | Add a property with optional default |
|
|
215
|
+
| `remove_node_property(label, prop)` | Remove a property |
|
|
216
|
+
| `rename_node_property(label, old, new)` | Rename a property |
|
|
217
|
+
| `run_cypher(cypher, params)` | Execute raw Cypher |
|
|
218
|
+
| `begin_batch(batch_id)` | Start a tracked data batch |
|
|
219
|
+
| `create_nodes(label, data)` | Batch-create nodes |
|
|
220
|
+
| `create_relationships(src, tgt, type, data)` | Batch-create relationships |
|
|
221
|
+
| `delete_nodes_by_batch(label, batch_id)` | Delete nodes by batch |
|
|
222
|
+
| `delete_relationships_by_batch(type, batch_id)` | Delete relationships by batch |
|
|
223
|
+
|
|
224
|
+
## Configuration
|
|
225
|
+
|
|
226
|
+
`crochet.toml`:
|
|
227
|
+
|
|
228
|
+
```toml
|
|
229
|
+
[project]
|
|
230
|
+
name = "my-graph"
|
|
231
|
+
models_path = "models"
|
|
232
|
+
migrations_path = "migrations"
|
|
233
|
+
|
|
234
|
+
[neo4j]
|
|
235
|
+
uri = "bolt://localhost:7687"
|
|
236
|
+
username = "neo4j"
|
|
237
|
+
|
|
238
|
+
[ledger]
|
|
239
|
+
path = ".crochet/ledger.db"
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Neo4j credentials can be overridden with environment variables:
|
|
243
|
+
|
|
244
|
+
- `CROCHET_NEO4J_URI`
|
|
245
|
+
- `CROCHET_NEO4J_USERNAME`
|
|
246
|
+
- `CROCHET_NEO4J_PASSWORD`
|
|
247
|
+
|
|
248
|
+
## CLI Reference
|
|
249
|
+
|
|
250
|
+
| Command | Description |
|
|
251
|
+
|---------|-------------|
|
|
252
|
+
| `crochet new-project` | Initialize a new Crochet project |
|
|
253
|
+
| `crochet create-node NAME` | Scaffold a StructuredNode model |
|
|
254
|
+
| `crochet create-relationship NAME` | Scaffold a StructuredRel model |
|
|
255
|
+
| `crochet create-migration DESC` | Create a new migration file |
|
|
256
|
+
| `crochet upgrade` | Apply pending migrations |
|
|
257
|
+
| `crochet downgrade` | Revert the most recent migration |
|
|
258
|
+
| `crochet status` | Show migration status |
|
|
259
|
+
| `crochet verify` | Run verification checks |
|
|
260
|
+
|
|
261
|
+
## Design Principles
|
|
262
|
+
|
|
263
|
+
- **No hidden magic** — all changes are explicit migration files
|
|
264
|
+
- **Code > database state** — neomodel files are the source of truth
|
|
265
|
+
- **Determinism over convenience** — schema IR is hashed and diffed
|
|
266
|
+
- **Rollback is a contract, not a guess** — explicitly declared per migration
|
|
267
|
+
- **Git history and graph state must agree** — ledger + hash chains enforce this
|
|
268
|
+
|
|
269
|
+
## Development
|
|
270
|
+
|
|
271
|
+
```bash
|
|
272
|
+
pip install -e ".[dev]"
|
|
273
|
+
pytest
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
## License
|
|
277
|
+
|
|
278
|
+
MIT
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
crochet/__init__.py,sha256=1y0jxq3KELfoanb8IVQa5bYbdV-d0NoQVP4Zyjpx5lk,103
|
|
2
|
+
crochet/cli.py,sha256=Ts1tZWpntGaBp_aJ2ZJZZB-ueiaLcA3r6wRgGKgY-Jc,10713
|
|
3
|
+
crochet/config.py,sha256=n6zWkn_p1ilj5s0IRDKFsrPLVdi5wGDWgavPY_rVN04,3611
|
|
4
|
+
crochet/errors.py,sha256=IEzKDJrK2cjAda-SUNLxanPijJ_sdcklAGZ9d71VcgI,2078
|
|
5
|
+
crochet/verify.py,sha256=OmBX0cTS2fQFz5-QVcCyVaPfjC3zBipzYH4gwiLrbRs,4415
|
|
6
|
+
crochet/ingest/__init__.py,sha256=eTwqsyxM21iDDtcrQzsFgzPJftGwKh47ZySKhLX-mgw,163
|
|
7
|
+
crochet/ingest/batch.py,sha256=C0nLIgiYy5zzz36l8Ywsgz8IlK7AmSwaSk3AMv_4Ogw,2014
|
|
8
|
+
crochet/ir/__init__.py,sha256=EIxtO4lQ242aVO7KOWcznnT_oNcQhRK47eBZkhIQACA,525
|
|
9
|
+
crochet/ir/diff.py,sha256=PYPbUX4W86RivpUBh1zda_B29uWsazSu51zjG1QH7Bg,7318
|
|
10
|
+
crochet/ir/hash.py,sha256=adUa5VOd8r3SotKDqXIHqDw88C4tfq54lwxyV1eQ4ZY,1137
|
|
11
|
+
crochet/ir/parser.py,sha256=Khahfvs1i_-kCTxFPrRDMiHuVQjg67IP1x5HRvqA9vg,7880
|
|
12
|
+
crochet/ir/schema.py,sha256=0MTOWGAmtWDVGCSfUEAdHXNM-lriyeT3gCUbsF3OsSc,6160
|
|
13
|
+
crochet/ledger/__init__.py,sha256=xXaClHCWAbo36NfYMo41VftPo3O7kPpIyW-wj4Lwaqw,133
|
|
14
|
+
crochet/ledger/sqlite.py,sha256=yGy4dDh5KPC5-N3HVaK1FpD7GYixoM2M5_IDRaEROvU,9421
|
|
15
|
+
crochet/migrations/__init__.py,sha256=-112mqAx8xTQzu33lA3mmiZAP6v_H0_tPlOpYRazVeE,204
|
|
16
|
+
crochet/migrations/engine.py,sha256=CyqtjWJbFPuIRfgMZ30ng1D7RUJGR0-i8Z1lxq5Z6sc,9563
|
|
17
|
+
crochet/migrations/operations.py,sha256=iv2aFF5w-X5K2y5fqB-w8CuSNN15eEswDHyOJIiamrY,10360
|
|
18
|
+
crochet/migrations/template.py,sha256=1vca5mDkQKt6B4M5mLn3NZErSZAannfkUuf1LPjdU3U,2673
|
|
19
|
+
crochet/scaffold/__init__.py,sha256=ZPyyQ3_s2uwFaw8kdpsYT7q4Br5TdU6KVCJbc0hhslo,236
|
|
20
|
+
crochet/scaffold/node.py,sha256=vXIqyYUj1Ot4qPUIwcwXsFKJsdcIbEh8Y71ASgWH4nY,1266
|
|
21
|
+
crochet/scaffold/relationship.py,sha256=eHKAt0olYcGXAA5TdnIl2DOZpN31GK9y-hJFutF2Tqg,1373
|
|
22
|
+
crochet_migration-0.1.0.dist-info/METADATA,sha256=tGQyC-qLHV0VbIW-u77FU10-EB6_ZXc3hPKa-hW2Tvc,8285
|
|
23
|
+
crochet_migration-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
24
|
+
crochet_migration-0.1.0.dist-info/entry_points.txt,sha256=wKTVp7ky8zuaFIMskw4FEVSppYDoKG-6CV1joyCKSC8,45
|
|
25
|
+
crochet_migration-0.1.0.dist-info/licenses/LICENSE,sha256=plFEmT-Ix7lZ5QZvnBsTTETSVDcBhM9sY8lWCxU6llg,1068
|
|
26
|
+
crochet_migration-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Keshav Dial
|
|
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.
|