vention-storage 0.6__tar.gz → 0.6.3__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.
- vention_storage-0.6/README.md → vention_storage-0.6.3/PKG-INFO +138 -2
- vention_storage-0.6/PKG-INFO → vention_storage-0.6.3/README.md +106 -17
- vention_storage-0.6.3/pyproject.toml +117 -0
- {vention_storage-0.6 → vention_storage-0.6.3}/src/storage/accessor.py +126 -17
- vention_storage-0.6.3/src/storage/crud_service.py +362 -0
- {vention_storage-0.6 → vention_storage-0.6.3}/src/storage/database.py +3 -9
- {vention_storage-0.6 → vention_storage-0.6.3}/src/storage/database_service.py +5 -13
- {vention_storage-0.6 → vention_storage-0.6.3}/src/storage/hooks.py +3 -9
- {vention_storage-0.6 → vention_storage-0.6.3}/src/storage/io_helpers.py +7 -21
- {vention_storage-0.6 → vention_storage-0.6.3}/src/storage/utils.py +1 -3
- {vention_storage-0.6 → vention_storage-0.6.3}/src/storage/vention_communication.py +71 -10
- vention_storage-0.6/pyproject.toml +0 -18
- vention_storage-0.6/src/storage/crud_service.py +0 -160
- {vention_storage-0.6 → vention_storage-0.6.3}/src/storage/__init__.py +0 -0
- {vention_storage-0.6 → vention_storage-0.6.3}/src/storage/auditor.py +0 -0
- {vention_storage-0.6 → vention_storage-0.6.3}/src/storage/bootstrap.py +0 -0
|
@@ -1,3 +1,35 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vention-storage
|
|
3
|
+
Version: 0.6.3
|
|
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
|
+
|
|
1
33
|
# Vention Storage
|
|
2
34
|
|
|
3
35
|
A framework for storing and managing component and application data with persistence, validation, and audit trails for machine applications.
|
|
@@ -192,8 +224,47 @@ user_accessor.delete(user.id, actor="admin")
|
|
|
192
224
|
|
|
193
225
|
# Restore (for soft-deleted models)
|
|
194
226
|
user_accessor.restore(user.id, actor="admin")
|
|
195
|
-
```
|
|
196
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
|
+
```
|
|
197
268
|
|
|
198
269
|
### Using ConnectRPC Client
|
|
199
270
|
|
|
@@ -263,8 +334,73 @@ export async function listUsers() {
|
|
|
263
334
|
});
|
|
264
335
|
return res.records;
|
|
265
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
|
+
}
|
|
266
383
|
```
|
|
267
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
|
+
|
|
268
404
|
|
|
269
405
|
## 📖 API Reference
|
|
270
406
|
|
|
@@ -355,4 +491,4 @@ class AuditLog(SQLModel, table=True):
|
|
|
355
491
|
- **Diagram endpoint fails** → Ensure Graphviz + sqlalchemy-schemadisplay are installed.
|
|
356
492
|
- **No audit actor shown** → Provide X-User header in API requests.
|
|
357
493
|
- **Soft delete not working** → Your model must have a `deleted_at` field.
|
|
358
|
-
- **Restore fails** → Ensure `integrity_check=True` passes when restoring backups.
|
|
494
|
+
- **Restore fails** → Ensure `integrity_check=True` passes when restoring backups.
|
|
@@ -1,18 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: vention-storage
|
|
3
|
-
Version: 0.6
|
|
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: python-multipart (>=0.0.20,<0.0.21)
|
|
12
|
-
Requires-Dist: sqlmodel (==0.0.27)
|
|
13
|
-
Requires-Dist: vention-communication (>=0.2.2,<0.3.0)
|
|
14
|
-
Description-Content-Type: text/markdown
|
|
15
|
-
|
|
16
1
|
# Vention Storage
|
|
17
2
|
|
|
18
3
|
A framework for storing and managing component and application data with persistence, validation, and audit trails for machine applications.
|
|
@@ -207,8 +192,47 @@ user_accessor.delete(user.id, actor="admin")
|
|
|
207
192
|
|
|
208
193
|
# Restore (for soft-deleted models)
|
|
209
194
|
user_accessor.restore(user.id, actor="admin")
|
|
210
|
-
```
|
|
211
195
|
|
|
196
|
+
# Find users by exact match
|
|
197
|
+
users = user_accessor.find(user_accessor.where.email == "alice@example.com")
|
|
198
|
+
|
|
199
|
+
# Multiple conditions (AND logic)
|
|
200
|
+
users = user_accessor.find(
|
|
201
|
+
user_accessor.where.name == "Alice",
|
|
202
|
+
user_accessor.where.email == "alice@example.com"
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# Comparison operators
|
|
206
|
+
adults = user_accessor.find(user_accessor.where.age >= 18)
|
|
207
|
+
recent = user_accessor.find(user_accessor.where.created_at > cutoff_date)
|
|
208
|
+
|
|
209
|
+
# String operations
|
|
210
|
+
smiths = user_accessor.find(user_accessor.where.name.contains("Smith"))
|
|
211
|
+
gmail_users = user_accessor.find(user_accessor.where.email.endswith("@gmail.com"))
|
|
212
|
+
search = user_accessor.find(user_accessor.where.name.ilike("%alice%")) # case-insensitive
|
|
213
|
+
|
|
214
|
+
# Collection check
|
|
215
|
+
admins = user_accessor.find(user_accessor.where.role.in_(["admin", "superadmin"]))
|
|
216
|
+
|
|
217
|
+
# Null checks
|
|
218
|
+
unverified = user_accessor.find(user_accessor.where.verified_at.is_(None))
|
|
219
|
+
verified = user_accessor.find(user_accessor.where.verified_at.isnot(None))
|
|
220
|
+
|
|
221
|
+
# With pagination and sorting
|
|
222
|
+
page = user_accessor.find(
|
|
223
|
+
user_accessor.where.status == "active",
|
|
224
|
+
limit=10,
|
|
225
|
+
offset=20,
|
|
226
|
+
order_by="created_at",
|
|
227
|
+
order_desc=True
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
# Include soft-deleted records
|
|
231
|
+
all_users = user_accessor.find(
|
|
232
|
+
user_accessor.where.role == "admin",
|
|
233
|
+
include_deleted=True
|
|
234
|
+
)
|
|
235
|
+
```
|
|
212
236
|
|
|
213
237
|
### Using ConnectRPC Client
|
|
214
238
|
|
|
@@ -278,8 +302,73 @@ export async function listUsers() {
|
|
|
278
302
|
});
|
|
279
303
|
return res.records;
|
|
280
304
|
}
|
|
305
|
+
|
|
306
|
+
// Find by exact match
|
|
307
|
+
export async function findUserByEmail(email: string) {
|
|
308
|
+
const res = await client.usersFindRecords({
|
|
309
|
+
filters: [
|
|
310
|
+
{ field: "email", operation: "eq", value: email }
|
|
311
|
+
]
|
|
312
|
+
});
|
|
313
|
+
return res.records;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Find with multiple conditions (AND logic)
|
|
317
|
+
export async function findActiveAdmins() {
|
|
318
|
+
const res = await client.usersFindRecords({
|
|
319
|
+
filters: [
|
|
320
|
+
{ field: "role", operation: "eq", value: "admin" },
|
|
321
|
+
{ field: "age", operation: "gte", value: "18" }
|
|
322
|
+
]
|
|
323
|
+
});
|
|
324
|
+
return res.records;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Find with null checks
|
|
328
|
+
export async function findUnverifiedUsers() {
|
|
329
|
+
const res = await client.usersFindRecords({
|
|
330
|
+
filters: [
|
|
331
|
+
{ field: "verified_at", operation: "is_null" }
|
|
332
|
+
]
|
|
333
|
+
});
|
|
334
|
+
return res.records;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Complex query example
|
|
338
|
+
export async function findRecentPremiumUsers(cutoffDate: string) {
|
|
339
|
+
const res = await client.usersFindRecords({
|
|
340
|
+
filters: [
|
|
341
|
+
{ field: "subscription", operation: "in", value: ["premium", "enterprise"] },
|
|
342
|
+
{ field: "created_at", operation: "gte", value: cutoffDate },
|
|
343
|
+
{ field: "email_verified", operation: "is_not_null" }
|
|
344
|
+
],
|
|
345
|
+
limit: 50,
|
|
346
|
+
orderBy: "created_at",
|
|
347
|
+
orderDesc: true
|
|
348
|
+
});
|
|
349
|
+
return res.records;
|
|
350
|
+
}
|
|
281
351
|
```
|
|
282
352
|
|
|
353
|
+
### Filter Operations Reference
|
|
354
|
+
|
|
355
|
+
| Operation | Description |
|
|
356
|
+
|----------------|--------------------------------------|
|
|
357
|
+
| `eq` | Exact match |
|
|
358
|
+
| `ne` | Not equal to value |
|
|
359
|
+
| `gt` | Greater than value |
|
|
360
|
+
| `gte` | Greater than or equal |
|
|
361
|
+
| `lt` | Less than value |
|
|
362
|
+
| `lte` | Less than or equal |
|
|
363
|
+
| `in` | Value in array |
|
|
364
|
+
| `not_in` | Value not in array |
|
|
365
|
+
| `contains` | Field contains substring |
|
|
366
|
+
| `starts_with` | Field starts with prefix |
|
|
367
|
+
| `ends_with` | Field ends with suffix |
|
|
368
|
+
| `like` | Case-insensitive pattern match |
|
|
369
|
+
| `is_null` | Field is null (no value needed) |
|
|
370
|
+
| `is_not_null` | Field is not null (no value needed) |
|
|
371
|
+
|
|
283
372
|
|
|
284
373
|
## 📖 API Reference
|
|
285
374
|
|
|
@@ -370,4 +459,4 @@ class AuditLog(SQLModel, table=True):
|
|
|
370
459
|
- **Diagram endpoint fails** → Ensure Graphviz + sqlalchemy-schemadisplay are installed.
|
|
371
460
|
- **No audit actor shown** → Provide X-User header in API requests.
|
|
372
461
|
- **Soft delete not working** → Your model must have a `deleted_at` field.
|
|
373
|
-
- **Restore fails** → Ensure `integrity_check=True` passes when restoring backups.
|
|
462
|
+
- **Restore fails** → Ensure `integrity_check=True` passes when restoring backups.
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "vention-storage"
|
|
3
|
+
version = "0.6.3"
|
|
4
|
+
description = "A framework for storing and managing component and application data for machine apps."
|
|
5
|
+
authors = [ "VentionCo" ]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
license = "Proprietary"
|
|
8
|
+
|
|
9
|
+
[[tool.poetry.packages]]
|
|
10
|
+
include = "storage"
|
|
11
|
+
from = "src"
|
|
12
|
+
|
|
13
|
+
[tool.poetry.dependencies]
|
|
14
|
+
python = ">=3.10,<3.11"
|
|
15
|
+
|
|
16
|
+
[tool.poetry.dependencies.annotated-doc]
|
|
17
|
+
version = "0.0.4"
|
|
18
|
+
markers = 'python_version == "3.10"'
|
|
19
|
+
optional = false
|
|
20
|
+
|
|
21
|
+
[tool.poetry.dependencies.annotated-types]
|
|
22
|
+
version = "0.7.0"
|
|
23
|
+
markers = 'python_version == "3.10"'
|
|
24
|
+
optional = false
|
|
25
|
+
|
|
26
|
+
[tool.poetry.dependencies.anyio]
|
|
27
|
+
version = "4.12.1"
|
|
28
|
+
markers = 'python_version == "3.10"'
|
|
29
|
+
optional = false
|
|
30
|
+
|
|
31
|
+
[tool.poetry.dependencies.click]
|
|
32
|
+
version = "8.3.1"
|
|
33
|
+
markers = 'python_version == "3.10"'
|
|
34
|
+
optional = false
|
|
35
|
+
|
|
36
|
+
[tool.poetry.dependencies.colorama]
|
|
37
|
+
version = "0.4.6"
|
|
38
|
+
markers = 'python_version == "3.10" and platform_system == "Windows"'
|
|
39
|
+
optional = false
|
|
40
|
+
|
|
41
|
+
[tool.poetry.dependencies.exceptiongroup]
|
|
42
|
+
version = "1.3.1"
|
|
43
|
+
markers = 'python_version == "3.10"'
|
|
44
|
+
optional = false
|
|
45
|
+
|
|
46
|
+
[tool.poetry.dependencies.fastapi]
|
|
47
|
+
version = "0.121.1"
|
|
48
|
+
markers = 'python_version == "3.10"'
|
|
49
|
+
optional = false
|
|
50
|
+
|
|
51
|
+
[tool.poetry.dependencies.greenlet]
|
|
52
|
+
version = "3.3.1"
|
|
53
|
+
markers = '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")'
|
|
54
|
+
optional = false
|
|
55
|
+
|
|
56
|
+
[tool.poetry.dependencies.h11]
|
|
57
|
+
version = "0.16.0"
|
|
58
|
+
markers = 'python_version == "3.10"'
|
|
59
|
+
optional = false
|
|
60
|
+
|
|
61
|
+
[tool.poetry.dependencies.idna]
|
|
62
|
+
version = "3.11"
|
|
63
|
+
markers = 'python_version == "3.10"'
|
|
64
|
+
optional = false
|
|
65
|
+
|
|
66
|
+
[tool.poetry.dependencies.pydantic-core]
|
|
67
|
+
version = "2.41.5"
|
|
68
|
+
markers = 'python_version == "3.10"'
|
|
69
|
+
optional = false
|
|
70
|
+
|
|
71
|
+
[tool.poetry.dependencies.pydantic]
|
|
72
|
+
version = "2.12.5"
|
|
73
|
+
markers = 'python_version == "3.10"'
|
|
74
|
+
optional = false
|
|
75
|
+
|
|
76
|
+
[tool.poetry.dependencies.python-multipart]
|
|
77
|
+
version = "0.0.20"
|
|
78
|
+
markers = 'python_version == "3.10"'
|
|
79
|
+
optional = false
|
|
80
|
+
|
|
81
|
+
[tool.poetry.dependencies.sqlalchemy]
|
|
82
|
+
version = "2.0.46"
|
|
83
|
+
markers = 'python_version == "3.10"'
|
|
84
|
+
optional = false
|
|
85
|
+
|
|
86
|
+
[tool.poetry.dependencies.sqlmodel]
|
|
87
|
+
version = "0.0.27"
|
|
88
|
+
markers = 'python_version == "3.10"'
|
|
89
|
+
optional = false
|
|
90
|
+
|
|
91
|
+
[tool.poetry.dependencies.starlette]
|
|
92
|
+
version = "0.49.3"
|
|
93
|
+
markers = 'python_version == "3.10"'
|
|
94
|
+
optional = false
|
|
95
|
+
|
|
96
|
+
[tool.poetry.dependencies.typing-extensions]
|
|
97
|
+
version = "4.15.0"
|
|
98
|
+
markers = 'python_version == "3.10"'
|
|
99
|
+
optional = false
|
|
100
|
+
|
|
101
|
+
[tool.poetry.dependencies.typing-inspection]
|
|
102
|
+
version = "0.4.2"
|
|
103
|
+
markers = 'python_version == "3.10"'
|
|
104
|
+
optional = false
|
|
105
|
+
|
|
106
|
+
[tool.poetry.dependencies.uvicorn]
|
|
107
|
+
version = "0.35.0"
|
|
108
|
+
markers = 'python_version == "3.10"'
|
|
109
|
+
optional = false
|
|
110
|
+
|
|
111
|
+
[tool.poetry.dependencies.vention-communication]
|
|
112
|
+
version = "0.3.0"
|
|
113
|
+
markers = 'python_version == "3.10"'
|
|
114
|
+
optional = false
|
|
115
|
+
|
|
116
|
+
[tool.poetry.group.dev]
|
|
117
|
+
dependencies = { }
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from contextlib import contextmanager
|
|
4
|
+
from functools import reduce
|
|
4
5
|
from typing import (
|
|
5
6
|
Any,
|
|
6
7
|
Callable,
|
|
@@ -15,6 +16,7 @@ from typing import (
|
|
|
15
16
|
)
|
|
16
17
|
|
|
17
18
|
from sqlmodel import SQLModel, Session, select
|
|
19
|
+
from sqlmodel.sql.expression import SelectOfScalar
|
|
18
20
|
|
|
19
21
|
from storage.auditor import audit_operation
|
|
20
22
|
from storage import database
|
|
@@ -25,6 +27,48 @@ from storage.utils import ModelType, utcnow, Operation
|
|
|
25
27
|
WriteResult = TypeVar("WriteResult")
|
|
26
28
|
|
|
27
29
|
|
|
30
|
+
def _apply_conditions(statement: SelectOfScalar[ModelType], conditions: tuple[Any, ...]) -> SelectOfScalar[ModelType]:
|
|
31
|
+
"""Apply filter conditions to a select statement."""
|
|
32
|
+
return reduce(lambda stmt, cond: stmt.where(cond), conditions, statement)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _apply_soft_delete_filter(
|
|
36
|
+
statement: SelectOfScalar[ModelType],
|
|
37
|
+
model: Type[ModelType],
|
|
38
|
+
has_soft_delete: bool,
|
|
39
|
+
include_deleted: bool,
|
|
40
|
+
) -> SelectOfScalar[ModelType]:
|
|
41
|
+
"""Apply soft delete filter if applicable."""
|
|
42
|
+
if not has_soft_delete or include_deleted:
|
|
43
|
+
return statement
|
|
44
|
+
return statement.where(getattr(model, "deleted_at").is_(None))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _apply_ordering(
|
|
48
|
+
statement: SelectOfScalar[ModelType],
|
|
49
|
+
model: Type[ModelType],
|
|
50
|
+
order_by: Optional[str],
|
|
51
|
+
order_desc: bool,
|
|
52
|
+
) -> SelectOfScalar[ModelType]:
|
|
53
|
+
"""Apply ordering to a select statement."""
|
|
54
|
+
if not order_by:
|
|
55
|
+
return statement
|
|
56
|
+
field = getattr(model, order_by, None)
|
|
57
|
+
if field is None:
|
|
58
|
+
raise ValueError(f"Field '{order_by}' not found on {model.__name__}")
|
|
59
|
+
return statement.order_by(field.desc() if order_desc else field)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _apply_pagination(
|
|
63
|
+
statement: SelectOfScalar[ModelType],
|
|
64
|
+
limit: Optional[int],
|
|
65
|
+
offset: int,
|
|
66
|
+
) -> SelectOfScalar[ModelType]:
|
|
67
|
+
"""Apply pagination to a select statement."""
|
|
68
|
+
limited = statement.limit(limit) if limit is not None else statement
|
|
69
|
+
return limited.offset(offset) if offset > 0 else limited
|
|
70
|
+
|
|
71
|
+
|
|
28
72
|
class ModelAccessor(Generic[ModelType]):
|
|
29
73
|
"""
|
|
30
74
|
Accessor for a single SQLModel type with:
|
|
@@ -47,6 +91,8 @@ class ModelAccessor(Generic[ModelType]):
|
|
|
47
91
|
self._hooks: HookRegistry[ModelType] = HookRegistry()
|
|
48
92
|
self._has_soft_delete = hasattr(model, "deleted_at")
|
|
49
93
|
self._enable_auditing = enable_auditing
|
|
94
|
+
# Field proxy for type-safe filtering: accessor.where.field_name
|
|
95
|
+
self.where = model
|
|
50
96
|
|
|
51
97
|
# ---------- Hook decorators ----------
|
|
52
98
|
def before_insert(self) -> Callable[[HookFn[ModelType]], HookFn[ModelType]]:
|
|
@@ -73,9 +119,7 @@ class ModelAccessor(Generic[ModelType]):
|
|
|
73
119
|
with database.use_session(session):
|
|
74
120
|
self._hooks.emit(event, session=session, instance=instance)
|
|
75
121
|
|
|
76
|
-
def _audit_create_operation(
|
|
77
|
-
self, *, session: Session, instance: ModelType, actor: str
|
|
78
|
-
) -> None:
|
|
122
|
+
def _audit_create_operation(self, *, session: Session, instance: ModelType, actor: str) -> None:
|
|
79
123
|
"""Audit a create operation."""
|
|
80
124
|
if not self._enable_auditing:
|
|
81
125
|
return
|
|
@@ -90,7 +134,7 @@ class ModelAccessor(Generic[ModelType]):
|
|
|
90
134
|
)
|
|
91
135
|
|
|
92
136
|
def _run_write(self, fn: Callable[[Session], WriteResult]) -> WriteResult:
|
|
93
|
-
"""Run a write
|
|
137
|
+
"""Run a write operation using the current session if present, else open a transaction."""
|
|
94
138
|
existing = database.CURRENT_SESSION.get()
|
|
95
139
|
if existing is not None:
|
|
96
140
|
return fn(existing)
|
|
@@ -119,12 +163,83 @@ class ModelAccessor(Generic[ModelType]):
|
|
|
119
163
|
return None
|
|
120
164
|
return cast(ModelType, obj)
|
|
121
165
|
|
|
122
|
-
def all(
|
|
123
|
-
|
|
166
|
+
def all(
|
|
167
|
+
self,
|
|
168
|
+
*,
|
|
169
|
+
include_deleted: bool = False,
|
|
170
|
+
limit: Optional[int] = None,
|
|
171
|
+
offset: int = 0,
|
|
172
|
+
order_by: Optional[str] = None,
|
|
173
|
+
order_desc: bool = False,
|
|
174
|
+
) -> List[ModelType]:
|
|
175
|
+
"""Get all models with optional pagination and sorting.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
include_deleted: Whether to include soft-deleted records
|
|
179
|
+
limit: Maximum number of records to return (None = no limit)
|
|
180
|
+
offset: Number of records to skip (for pagination)
|
|
181
|
+
order_by: Field name to sort by (must exist on model)
|
|
182
|
+
order_desc: If True, sort descending; otherwise ascending
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
List of model instances
|
|
186
|
+
|
|
187
|
+
Raises:
|
|
188
|
+
ValueError: If order_by field doesn't exist on the model
|
|
189
|
+
"""
|
|
124
190
|
with self._read_session() as session:
|
|
125
191
|
statement = select(self.model)
|
|
126
|
-
|
|
127
|
-
|
|
192
|
+
statement = _apply_soft_delete_filter(statement, self.model, self._has_soft_delete, include_deleted)
|
|
193
|
+
statement = _apply_ordering(statement, self.model, order_by, order_desc)
|
|
194
|
+
statement = _apply_pagination(statement, limit, offset)
|
|
195
|
+
|
|
196
|
+
return cast(List[ModelType], session.exec(statement).all())
|
|
197
|
+
|
|
198
|
+
def find(
|
|
199
|
+
self,
|
|
200
|
+
*conditions: Any,
|
|
201
|
+
include_deleted: bool = False,
|
|
202
|
+
limit: Optional[int] = None,
|
|
203
|
+
offset: int = 0,
|
|
204
|
+
order_by: Optional[str] = None,
|
|
205
|
+
order_desc: bool = False,
|
|
206
|
+
) -> List[ModelType]:
|
|
207
|
+
"""Find records matching all given filter conditions.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
*conditions: SQLAlchemy filter conditions using accessor.where.field_name
|
|
211
|
+
include_deleted: Whether to include soft-deleted records
|
|
212
|
+
limit: Maximum number of records to return (None = no limit)
|
|
213
|
+
offset: Number of records to skip (for pagination)
|
|
214
|
+
order_by: Field name to sort by (must exist on model)
|
|
215
|
+
order_desc: If True, sort descending; otherwise ascending
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
List of model instances matching all conditions
|
|
219
|
+
|
|
220
|
+
Example:
|
|
221
|
+
# Find active users over 18
|
|
222
|
+
users = user_accessor.find(
|
|
223
|
+
user_accessor.where.age >= 18,
|
|
224
|
+
user_accessor.where.status == "active",
|
|
225
|
+
limit=10
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
# String operations
|
|
229
|
+
users = user_accessor.find(
|
|
230
|
+
user_accessor.where.name.contains("Smith")
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
Raises:
|
|
234
|
+
ValueError: If order_by field doesn't exist on the model
|
|
235
|
+
"""
|
|
236
|
+
with self._read_session() as session:
|
|
237
|
+
statement = select(self.model)
|
|
238
|
+
statement = _apply_conditions(statement, conditions)
|
|
239
|
+
statement = _apply_soft_delete_filter(statement, self.model, self._has_soft_delete, include_deleted)
|
|
240
|
+
statement = _apply_ordering(statement, self.model, order_by, order_desc)
|
|
241
|
+
statement = _apply_pagination(statement, limit, offset)
|
|
242
|
+
|
|
128
243
|
return cast(List[ModelType], session.exec(statement).all())
|
|
129
244
|
|
|
130
245
|
# ---------- Writes ----------
|
|
@@ -227,9 +342,7 @@ class ModelAccessor(Generic[ModelType]):
|
|
|
227
342
|
return self._run_write(write_operation)
|
|
228
343
|
|
|
229
344
|
# ---------- Batch helpers ----------
|
|
230
|
-
def insert_many(
|
|
231
|
-
self, objs: Sequence[ModelType], *, actor: str = "internal"
|
|
232
|
-
) -> List[ModelType]:
|
|
345
|
+
def insert_many(self, objs: Sequence[ModelType], *, actor: str = "internal") -> List[ModelType]:
|
|
233
346
|
"""Insert multiple models."""
|
|
234
347
|
|
|
235
348
|
def write_operation(session: Session) -> List[ModelType]:
|
|
@@ -257,9 +370,7 @@ class ModelAccessor(Generic[ModelType]):
|
|
|
257
370
|
if obj is None:
|
|
258
371
|
continue
|
|
259
372
|
self._emit("before_delete", session=session, instance=obj)
|
|
260
|
-
op_name, before_payload, after_payload = _soft_or_hard_delete(
|
|
261
|
-
session, obj
|
|
262
|
-
)
|
|
373
|
+
op_name, before_payload, after_payload = _soft_or_hard_delete(session, obj)
|
|
263
374
|
if self._enable_auditing:
|
|
264
375
|
audit_operation(
|
|
265
376
|
session=session,
|
|
@@ -277,9 +388,7 @@ class ModelAccessor(Generic[ModelType]):
|
|
|
277
388
|
return self._run_write(write_operation)
|
|
278
389
|
|
|
279
390
|
|
|
280
|
-
def _soft_or_hard_delete(
|
|
281
|
-
session: Session, instance: SQLModel
|
|
282
|
-
) -> tuple[Operation, dict[str, Any], dict[str, Any] | None]:
|
|
391
|
+
def _soft_or_hard_delete(session: Session, instance: SQLModel) -> tuple[Operation, dict[str, Any], dict[str, Any] | None]:
|
|
283
392
|
"""Soft delete if model defines `deleted_at`, else hard delete."""
|
|
284
393
|
before_payload = instance.model_dump()
|
|
285
394
|
if hasattr(instance, "deleted_at"):
|