vention-storage 0.5.1__tar.gz → 0.6.5__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,494 @@
1
+ Metadata-Version: 2.4
2
+ Name: vention-storage
3
+ Version: 0.6.5
4
+ Summary: A framework for storing and managing component and application data for machine apps.
5
+ License: Proprietary
6
+ Author: VentionCo
7
+ Requires-Python: >=3.10,<3.11
8
+ Classifier: License :: Other/Proprietary License
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Requires-Dist: annotated-doc (==0.0.4) ; python_version == "3.10"
12
+ Requires-Dist: annotated-types (==0.7.0) ; python_version == "3.10"
13
+ Requires-Dist: anyio (==4.12.1) ; python_version == "3.10"
14
+ Requires-Dist: click (==8.3.1) ; python_version == "3.10"
15
+ Requires-Dist: colorama (==0.4.6) ; python_version == "3.10" and platform_system == "Windows"
16
+ Requires-Dist: exceptiongroup (==1.3.1) ; python_version == "3.10"
17
+ Requires-Dist: fastapi (==0.121.1) ; python_version == "3.10"
18
+ Requires-Dist: greenlet (==3.3.1) ; python_version == "3.10" and (platform_machine == "aarch64" or platform_machine == "ppc64le" or platform_machine == "x86_64" or platform_machine == "amd64" or platform_machine == "AMD64" or platform_machine == "win32" or platform_machine == "WIN32")
19
+ Requires-Dist: h11 (==0.16.0) ; python_version == "3.10"
20
+ Requires-Dist: idna (==3.11) ; python_version == "3.10"
21
+ Requires-Dist: pydantic (==2.12.5) ; python_version == "3.10"
22
+ Requires-Dist: pydantic-core (==2.41.5) ; python_version == "3.10"
23
+ Requires-Dist: python-multipart (==0.0.20) ; python_version == "3.10"
24
+ Requires-Dist: sqlalchemy (==2.0.46) ; python_version == "3.10"
25
+ Requires-Dist: sqlmodel (==0.0.27) ; python_version == "3.10"
26
+ Requires-Dist: starlette (==0.49.3) ; python_version == "3.10"
27
+ Requires-Dist: typing-extensions (==4.15.0) ; python_version == "3.10"
28
+ Requires-Dist: typing-inspection (==0.4.2) ; python_version == "3.10"
29
+ Requires-Dist: uvicorn (==0.35.0) ; python_version == "3.10"
30
+ Requires-Dist: vention-communication (==0.3.0) ; python_version == "3.10"
31
+ Description-Content-Type: text/markdown
32
+
33
+ # Vention Storage
34
+
35
+ A framework for storing and managing component and application data with persistence, validation, and audit trails for machine applications.
36
+
37
+ ## Table of Contents
38
+
39
+ - [✨ Features](#-features)
40
+ - [🧠 Concepts & Overview](#-concepts--overview)
41
+ - [⚙️ Installation & Setup](#️-installation--setup)
42
+ - [🚀 Quickstart Tutorial](#-quickstart-tutorial)
43
+ - [🛠 How-to Guides](#-how-to-guides)
44
+ - [📖 API Reference](#-api-reference)
45
+ - [🔍 Troubleshooting & FAQ](#-troubleshooting--faq)
46
+
47
+ ## ✨ Features
48
+
49
+ - Persistent storage with SQLite
50
+ - Automatic audit trails (who, when, what changed)
51
+ - Strong typing & validation via SQLModel
52
+ - Lifecycle hooks before/after insert, update, delete
53
+ - Soft delete with `deleted_at` fields
54
+ - ConnectRPC bundle generation with Create, Read, Update, Delete + audit
55
+ - Health & monitoring actions (audit log, schema diagram)
56
+ - Batch operations for insert/delete
57
+ - Session management with smart reuse & transactions
58
+ - Bootstrap system for one-command setup
59
+ - CSV export/import for backups and migration
60
+ - Database backup/restore with integrity checking
61
+
62
+ ## 🧠 Concepts & Overview
63
+
64
+ Vention Storage is a component-based persistence layer for machine apps:
65
+
66
+ - **Database** → SQLite database with managed sessions and transactions
67
+ - **ModelAccessor** → Strongly-typed Create, Read, Update, Delete interface for your SQLModel classes
68
+ - **Hooks** → Functions that run before/after Create, Read, Update, Delete operations
69
+ - **AuditLog** → Automatically records all data mutations
70
+ - **RpcBundle** → Auto-generated ConnectRPC bundle with Create, Read, Update, Delete + database management actions
71
+
72
+ ## ⚙️ Installation & Setup
73
+
74
+ ```bash
75
+ pip install vention-storage
76
+ ```
77
+
78
+ **Optional dependencies:**
79
+ - sqlalchemy-schemadisplay and Graphviz → enable database schema visualization
80
+
81
+ MacOS:
82
+ ```bash
83
+ brew install graphviz
84
+ pip install sqlalchemy-schemadisplay
85
+ ```
86
+
87
+ Linux (Debian/Ubuntu)
88
+ ```bash
89
+ sudo apt-get install graphviz
90
+ pip install sqlalchemy-schemadisplay
91
+ ```
92
+
93
+ ## 🚀 Quickstart Tutorial
94
+
95
+ Define a model, bootstrap storage, and get full Create, Read, Update, Delete RPC actions in minutes:
96
+
97
+ ```python
98
+ from datetime import datetime
99
+ from typing import Optional
100
+ from sqlmodel import Field, SQLModel
101
+ from communication.app import VentionApp
102
+ from storage.bootstrap import bootstrap
103
+ from storage.accessor import ModelAccessor
104
+ from storage.vention_communication import build_storage_bundle
105
+
106
+ class User(SQLModel, table=True):
107
+ id: Optional[int] = Field(default=None, primary_key=True)
108
+ name: str
109
+ email: str
110
+ deleted_at: Optional[datetime] = Field(default=None, index=True)
111
+
112
+ # Initialize database
113
+ bootstrap(
114
+ database_url="sqlite:///./my_app.db",
115
+ create_tables=True
116
+ )
117
+
118
+ # Create accessor
119
+ user_accessor = ModelAccessor(User, "users")
120
+
121
+ # Build RPC bundle and add to app
122
+ app = VentionApp(name="my-app")
123
+ storage_bundle = build_storage_bundle(
124
+ accessors=[user_accessor],
125
+ max_records_per_model=100,
126
+ enable_db_actions=True
127
+ )
128
+ app.add_bundle(storage_bundle)
129
+ app.finalize()
130
+ ```
131
+
132
+ ➡️ You now have Create, Read, Update, Delete, audit, backup, and CSV actions available via ConnectRPC.
133
+
134
+ ## 🛠 How-to Guides
135
+
136
+ ### Bootstrap Multiple Models
137
+
138
+ ```python
139
+ # user_accessor was created earlier in the Quickstart example
140
+ # Reuse it here to bootstrap multiple models at once
141
+
142
+ product_accessor = ModelAccessor(Product, "products")
143
+
144
+ # Build bundle with multiple accessors
145
+ storage_bundle = build_storage_bundle(
146
+ accessors=[user_accessor, product_accessor],
147
+ max_records_per_model=100,
148
+ enable_db_actions=True
149
+ )
150
+ app.add_bundle(storage_bundle)
151
+ ```
152
+
153
+ ### Export to CSV
154
+
155
+ ```python
156
+ # Using ConnectRPC client
157
+ from communication.client import ConnectClient
158
+
159
+ client = ConnectClient("http://localhost:8000")
160
+ response = await client.call("Database_ExportZip", {})
161
+ with open("backup.zip", "wb") as f:
162
+ f.write(response.data)
163
+ ```
164
+
165
+ ### Backup & Restore
166
+
167
+ ```python
168
+ # Backup
169
+ backup_response = await client.call("Database_BackupSqlite", {})
170
+ with open(backup_response.filename, "wb") as f:
171
+ f.write(backup_response.data)
172
+
173
+ # Restore
174
+ with open("backup.sqlite", "rb") as f:
175
+ restore_response = await client.call(
176
+ "Database_RestoreSqlite",
177
+ {
178
+ "bytes": f.read(),
179
+ "filename": "backup.sqlite",
180
+ "integrity_check": True,
181
+ "dry_run": False
182
+ }
183
+ )
184
+ ```
185
+
186
+ ### Use Lifecycle Hooks
187
+
188
+ ```python
189
+ @user_accessor.before_insert()
190
+ def validate_email(session, instance):
191
+ if "@" not in instance.email:
192
+ raise ValueError("Invalid email")
193
+
194
+ @user_accessor.after_insert()
195
+ def log_creation(session, instance):
196
+ print(f"User created: {instance.name}")
197
+ ```
198
+
199
+ ### Query Audit Logs
200
+
201
+ ```python
202
+ from storage.auditor import AuditLog
203
+ from sqlmodel import select
204
+
205
+ with database.transaction() as session:
206
+ logs = session.exec(select(AuditLog).where(AuditLog.component == "users")).all()
207
+ ```
208
+
209
+ ### Using the model accessors
210
+
211
+ ```python
212
+ # Create
213
+ user = user_accessor.insert(User(name="Alice", email="alice@example.com"), actor="admin")
214
+
215
+ # Read
216
+ user = user_accessor.get(user.id)
217
+
218
+ # Update
219
+ user.name = "Alice Smith"
220
+ user_accessor.save(user, actor="admin")
221
+
222
+ # Delete
223
+ user_accessor.delete(user.id, actor="admin")
224
+
225
+ # Restore (for soft-deleted models)
226
+ user_accessor.restore(user.id, actor="admin")
227
+
228
+ # Find users by exact match
229
+ users = user_accessor.find(user_accessor.where.email == "alice@example.com")
230
+
231
+ # Multiple conditions (AND logic)
232
+ users = user_accessor.find(
233
+ user_accessor.where.name == "Alice",
234
+ user_accessor.where.email == "alice@example.com"
235
+ )
236
+
237
+ # Comparison operators
238
+ adults = user_accessor.find(user_accessor.where.age >= 18)
239
+ recent = user_accessor.find(user_accessor.where.created_at > cutoff_date)
240
+
241
+ # String operations
242
+ smiths = user_accessor.find(user_accessor.where.name.contains("Smith"))
243
+ gmail_users = user_accessor.find(user_accessor.where.email.endswith("@gmail.com"))
244
+ search = user_accessor.find(user_accessor.where.name.ilike("%alice%")) # case-insensitive
245
+
246
+ # Collection check
247
+ admins = user_accessor.find(user_accessor.where.role.in_(["admin", "superadmin"]))
248
+
249
+ # Null checks
250
+ unverified = user_accessor.find(user_accessor.where.verified_at.is_(None))
251
+ verified = user_accessor.find(user_accessor.where.verified_at.isnot(None))
252
+
253
+ # With pagination and sorting
254
+ page = user_accessor.find(
255
+ user_accessor.where.status == "active",
256
+ limit=10,
257
+ offset=20,
258
+ order_by="created_at",
259
+ order_desc=True
260
+ )
261
+
262
+ # Include soft-deleted records
263
+ all_users = user_accessor.find(
264
+ user_accessor.where.role == "admin",
265
+ include_deleted=True
266
+ )
267
+ ```
268
+
269
+ ### Using ConnectRPC Client
270
+
271
+ Once the bundle is added to your `VentionApp`, each `ModelAccessor` automatically exposes full CRUD actions via ConnectRPC.
272
+
273
+ Example: interacting with the `Users` RPC actions.
274
+
275
+ ```typescript
276
+ import { createPromiseClient } from "@connectrpc/connect";
277
+ import { createConnectTransport } from "@connectrpc/connect-web";
278
+
279
+ const transport = createConnectTransport({
280
+ baseUrl: "http://localhost:8000",
281
+ });
282
+
283
+ const client = createPromiseClient(YourServiceClient, transport);
284
+
285
+ // Create
286
+ export async function createUser(name: string, email: string) {
287
+ const res = await client.usersCreateRecord({
288
+ record: { name, email },
289
+ actor: "operator"
290
+ });
291
+ return res.record;
292
+ }
293
+
294
+ // Read
295
+ export async function getUser(id: number) {
296
+ const res = await client.usersGetRecord({
297
+ recordId: id,
298
+ includeDeleted: false
299
+ });
300
+ return res.record;
301
+ }
302
+
303
+ // Update
304
+ export async function updateUser(id: number, name: string) {
305
+ const res = await client.usersUpdateRecord({
306
+ recordId: id,
307
+ record: { name },
308
+ actor: "operator"
309
+ });
310
+ return res.record;
311
+ }
312
+
313
+ // Delete (soft delete if model supports deleted_at)
314
+ export async function deleteUser(id: number) {
315
+ await client.usersDeleteRecord({
316
+ recordId: id,
317
+ actor: "operator"
318
+ });
319
+ }
320
+
321
+ // Restore
322
+ export async function restoreUser(id: number) {
323
+ const res = await client.usersRestoreRecord({
324
+ recordId: id,
325
+ actor: "operator"
326
+ });
327
+ return res.record;
328
+ }
329
+
330
+ // List
331
+ export async function listUsers() {
332
+ const res = await client.usersListRecords({
333
+ includeDeleted: false
334
+ });
335
+ return res.records;
336
+ }
337
+
338
+ // Find by exact match
339
+ export async function findUserByEmail(email: string) {
340
+ const res = await client.usersFindRecords({
341
+ filters: [
342
+ { field: "email", operation: "eq", value: email }
343
+ ]
344
+ });
345
+ return res.records;
346
+ }
347
+
348
+ // Find with multiple conditions (AND logic)
349
+ export async function findActiveAdmins() {
350
+ const res = await client.usersFindRecords({
351
+ filters: [
352
+ { field: "role", operation: "eq", value: "admin" },
353
+ { field: "age", operation: "gte", value: "18" }
354
+ ]
355
+ });
356
+ return res.records;
357
+ }
358
+
359
+ // Find with null checks
360
+ export async function findUnverifiedUsers() {
361
+ const res = await client.usersFindRecords({
362
+ filters: [
363
+ { field: "verified_at", operation: "is_null" }
364
+ ]
365
+ });
366
+ return res.records;
367
+ }
368
+
369
+ // Complex query example
370
+ export async function findRecentPremiumUsers(cutoffDate: string) {
371
+ const res = await client.usersFindRecords({
372
+ filters: [
373
+ { field: "subscription", operation: "in", value: ["premium", "enterprise"] },
374
+ { field: "created_at", operation: "gte", value: cutoffDate },
375
+ { field: "email_verified", operation: "is_not_null" }
376
+ ],
377
+ limit: 50,
378
+ orderBy: "created_at",
379
+ orderDesc: true
380
+ });
381
+ return res.records;
382
+ }
383
+ ```
384
+
385
+ ### Filter Operations Reference
386
+
387
+ | Operation | Description |
388
+ |----------------|--------------------------------------|
389
+ | `eq` | Exact match |
390
+ | `ne` | Not equal to value |
391
+ | `gt` | Greater than value |
392
+ | `gte` | Greater than or equal |
393
+ | `lt` | Less than value |
394
+ | `lte` | Less than or equal |
395
+ | `in` | Value in array |
396
+ | `not_in` | Value not in array |
397
+ | `contains` | Field contains substring |
398
+ | `starts_with` | Field starts with prefix |
399
+ | `ends_with` | Field ends with suffix |
400
+ | `like` | Case-insensitive pattern match |
401
+ | `is_null` | Field is null (no value needed) |
402
+ | `is_not_null` | Field is not null (no value needed) |
403
+
404
+
405
+ ## 📖 API Reference
406
+
407
+ ### bootstrap
408
+
409
+ ```python
410
+ def bootstrap(
411
+ *,
412
+ database_url: Optional[str] = None,
413
+ create_tables: bool = True,
414
+ ) -> None
415
+ ```
416
+
417
+ Initialize the database engine and optionally create tables. This function performs environment setup only.
418
+
419
+ ### build_storage_bundle
420
+
421
+ ```python
422
+ def build_storage_bundle(
423
+ *,
424
+ accessors: Sequence[ModelAccessor[Any]],
425
+ max_records_per_model: Optional[int] = 5,
426
+ enable_db_actions: bool = True,
427
+ ) -> RpcBundle
428
+ ```
429
+
430
+ Build a ConnectRPC RpcBundle exposing CRUD and database utilities. Returns an `RpcBundle` that can be added to a `VentionApp` using `app.add_bundle()`.
431
+
432
+ ### ModelAccessor
433
+
434
+ ```python
435
+ ModelAccessor(
436
+ model: Type[ModelType],
437
+ component_name: str,
438
+ *,
439
+ enable_auditing: bool = True,
440
+ )
441
+ ```
442
+
443
+ **Read**
444
+ - `get(id, include_deleted=False) -> Optional[ModelType]`
445
+ - `all(include_deleted=False) -> List[ModelType]`
446
+
447
+ **Write**
448
+ - `insert(obj, actor="internal") -> ModelType`
449
+ - `save(obj, actor="internal") -> ModelType`
450
+ - `delete(id, actor="internal") -> bool`
451
+ - `restore(id, actor="internal") -> bool`
452
+
453
+ **Batch**
454
+ - `insert_many(objs, actor="internal") -> List[ModelType]`
455
+ - `delete_many(ids, actor="internal") -> int`
456
+
457
+ **Hooks**
458
+ - `@accessor.before_insert()`
459
+ - `@accessor.after_insert()`
460
+ - `@accessor.before_update()`
461
+ - `@accessor.after_update()`
462
+ - `@accessor.before_delete()`
463
+ - `@accessor.after_delete()`
464
+
465
+ **Parameters**
466
+ - `enable_auditing`: If `False`, disables audit logging for this accessor. Useful for models that shouldn't be audited (e.g., audit logs themselves). Defaults to `True`.
467
+
468
+ ### Database Helpers
469
+
470
+ - `database.set_database_url(url: str) -> None`
471
+ - `database.get_engine() -> Engine`
472
+ - `database.transaction() -> Iterator[Session]`
473
+ - `database.use_session(session: Optional[Session] = None) -> Iterator[Session]`
474
+
475
+ ### AuditLog model
476
+
477
+ ```python
478
+ class AuditLog(SQLModel, table=True):
479
+ id: int
480
+ timestamp: datetime
481
+ component: str
482
+ record_id: int
483
+ operation: str
484
+ actor: str
485
+ before: Optional[Dict[str, Any]]
486
+ after: Optional[Dict[str, Any]]
487
+ ```
488
+
489
+ ## 🔍 Troubleshooting & FAQ
490
+
491
+ - **Diagram endpoint fails** → Ensure Graphviz + sqlalchemy-schemadisplay are installed.
492
+ - **No audit actor shown** → Provide X-User header in API requests.
493
+ - **Soft delete not working** → Your model must have a `deleted_at` field.
494
+ - **Restore fails** → Ensure `integrity_check=True` passes when restoring backups.