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.
@@ -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 op using the current session if present, else open a transaction."""
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(self, *, include_deleted: bool = False) -> List[ModelType]:
123
- """Get all models."""
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
- if self._has_soft_delete and not include_deleted:
127
- statement = statement.where(getattr(self.model, "deleted_at").is_(None))
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"):