API Reference¶
This section is the map to Arvel’s public surface area. The pages below are auto-generated from the source code using mkdocstrings (Google-style docstrings, members in source order). When you upgrade Arvel or pin a new minor release, regenerate the docs so signatures and narrative descriptions stay aligned with what actually ships.
Use these modules as entry points: they group the framework by concern—data layer, HTTP stack, bootstrap and providers, auth, caching, events, queues, validation, and the testing helpers. Subpackages expand into more specific types as you drill in.
Core framework¶
arvel.data — ORM models, repositories, query builder, relationships, transactions, observers, and migrations-facing utilities. This is where typed Mapped columns and the repository pattern live.
arvel.http — Routing resources, controllers, requests, responses, and HTTP-facing glue that sits alongside FastAPI.
arvel.foundation — Application bootstrap, service container, providers, and lifecycle hooks that wire the framework together.
Cross-cutting services¶
arvel.auth — Guards, user resolution, and authentication plumbing you integrate with your identity strategy.
arvel.cache — Cache contracts and drivers; swap implementations per environment without changing call sites.
arvel.events — Domain events, dispatch, and listener registration for decoupled reactions inside the app.
arvel.queue — Job types, dispatch, and queue contracts for async and background work.
arvel.validation — Validation rules and helpers that align with Arvel’s request and DTO patterns.
Testing¶
arvel.testing — TestClient, TestResponse, DatabaseTestCase, ModelFactory, FactoryBuilder, and fakes for cache, mail, queue, storage, locks, media, notifications, events, and broadcasting.
arvel.data
¶
Arvel data layer.
ORM, repositories, transactions, observers, migrations, seeders, views, relationships, result types, collections, scopes, casts, soft deletes, and polymorphic relationships.
Imports are lazy — submodules are loaded on first attribute access
to keep import arvel.data fast.
Caster
¶
Bases: Protocol
Protocol for custom cast classes.
Source code in src/arvel/data/casts.py
36 37 38 39 40 41 42 43 44 45 46 | |
get(value, attr_name, model)
¶
Transform the database value into a Python value.
Source code in src/arvel/data/casts.py
40 41 42 | |
set(value, attr_name, model)
¶
Transform the Python value into a database value.
Source code in src/arvel/data/casts.py
44 45 46 | |
EncryptedCaster
¶
Cast via the framework's EncrypterContract.
The encrypter is resolved lazily at first use to avoid circular imports.
Source code in src/arvel/data/casts.py
145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 | |
EnumCaster
¶
Cast between enum members and their values.
Source code in src/arvel/data/casts.py
124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 | |
JsonCaster
¶
Cast between Python dicts/lists and JSON strings.
Source code in src/arvel/data/casts.py
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | |
ArvelCollection
¶
Bases: list[T]
List subclass with chainable data-manipulation helpers.
Behaves exactly like a built-in list for iteration, indexing,
len(), truthiness, and serialization — but adds fluent methods
inspired by Laravel's Collection.
Source code in src/arvel/data/collection.py
28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 | |
first(default=None)
¶
Return the first item, or default if the collection is empty.
Source code in src/arvel/data/collection.py
40 41 42 | |
last(default=None)
¶
Return the last item, or default if the collection is empty.
Source code in src/arvel/data/collection.py
44 45 46 | |
map(fn)
¶
Apply fn to each item and return a new collection.
Source code in src/arvel/data/collection.py
52 53 54 | |
flat_map(fn)
¶
Map then flatten one level.
Source code in src/arvel/data/collection.py
56 57 58 59 60 61 | |
filter(fn=None)
¶
Keep items where fn returns truthy, or all truthy items if fn is None.
Source code in src/arvel/data/collection.py
63 64 65 66 67 | |
reject(fn)
¶
Remove items where fn returns truthy.
Source code in src/arvel/data/collection.py
69 70 71 | |
each(fn)
¶
Call fn on each item for side effects; return self.
Source code in src/arvel/data/collection.py
73 74 75 76 77 | |
pluck(key)
¶
Extract a single attribute or dict key from each item.
Source code in src/arvel/data/collection.py
83 84 85 | |
values()
¶
Return a re-indexed copy (removes gaps after filtering).
Source code in src/arvel/data/collection.py
87 88 89 | |
where(key, value)
¶
Keep items where item.key == value (or item[key] == value).
Source code in src/arvel/data/collection.py
95 96 97 | |
where_in(key, values)
¶
Keep items where item.key is in values.
Source code in src/arvel/data/collection.py
99 100 101 102 | |
first_where(key, value)
¶
Return the first item matching key == value, or None.
Source code in src/arvel/data/collection.py
104 105 106 107 108 109 | |
contains(fn_or_value)
¶
Check if any item satisfies fn or equals value.
Source code in src/arvel/data/collection.py
111 112 113 114 115 | |
group_by(key)
¶
Group items by a key attribute name or callable.
Source code in src/arvel/data/collection.py
121 122 123 124 125 126 127 128 129 130 | |
chunk(size)
¶
Split into sub-collections of at most size items.
Source code in src/arvel/data/collection.py
132 133 134 135 136 137 138 139 140 141 142 143 144 | |
sort_by(key, *, descending=False)
¶
Return a new collection sorted by key.
Source code in src/arvel/data/collection.py
150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 | |
unique(key=None)
¶
Remove duplicates, preserving order.
If key is given, uniqueness is determined by the key value.
Source code in src/arvel/data/collection.py
168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 | |
reverse()
¶
Return a new reversed collection (non-mutating).
Intentionally overrides list.reverse() to return a new
collection instead of mutating in place, matching the immutable
API style of all other ArvelCollection methods.
Source code in src/arvel/data/collection.py
195 196 197 198 199 200 201 202 | |
count()
¶
Return the number of items (alias for len()).
Intentionally overrides list.count(value) to return the
collection length without requiring a value argument, matching
Laravel's Collection.count() API.
Source code in src/arvel/data/collection.py
208 209 210 211 212 213 214 215 | |
sum(key=None)
¶
Sum item values, optionally by attribute or callable.
Source code in src/arvel/data/collection.py
217 218 219 220 221 222 223 | |
avg(key=None)
¶
Return the average value.
Source code in src/arvel/data/collection.py
225 226 227 228 229 230 | |
min(key=None)
¶
Return the minimum value.
Source code in src/arvel/data/collection.py
232 233 234 235 236 237 238 239 240 | |
max(key=None)
¶
Return the maximum value.
Source code in src/arvel/data/collection.py
242 243 244 245 246 247 248 249 250 | |
take(n)
¶
Take the first n items (negative n takes from the end).
Source code in src/arvel/data/collection.py
256 257 258 259 260 | |
skip(n)
¶
Skip the first n items.
Source code in src/arvel/data/collection.py
262 263 264 | |
slice(offset, length=None)
¶
Extract a portion starting at offset.
Source code in src/arvel/data/collection.py
266 267 268 269 270 | |
nth(step, offset=0)
¶
Return every step-th item, starting at offset.
Source code in src/arvel/data/collection.py
272 273 274 | |
for_page(page, per_page)
¶
Return a page of results (1-indexed).
Source code in src/arvel/data/collection.py
276 277 278 279 | |
take_while(fn)
¶
Take items while fn returns truthy.
Source code in src/arvel/data/collection.py
281 282 283 284 285 286 287 288 | |
take_until(fn)
¶
Take items until fn returns truthy.
Source code in src/arvel/data/collection.py
290 291 292 293 294 295 296 297 | |
skip_while(fn)
¶
Skip items while fn returns truthy.
Source code in src/arvel/data/collection.py
299 300 301 302 303 304 305 306 307 308 | |
skip_until(fn)
¶
Skip items until fn returns truthy.
Source code in src/arvel/data/collection.py
310 311 312 313 314 315 316 317 318 319 320 321 | |
merge(other)
¶
Combine with another iterable.
Source code in src/arvel/data/collection.py
327 328 329 | |
concat(other)
¶
Alias for merge.
Source code in src/arvel/data/collection.py
331 332 333 | |
diff(other)
¶
Items in this collection but not in other.
Source code in src/arvel/data/collection.py
335 336 337 338 | |
intersect(other)
¶
Items present in both this collection and other.
Source code in src/arvel/data/collection.py
340 341 342 343 | |
zip(other)
¶
Pair items with another iterable.
Source code in src/arvel/data/collection.py
345 346 347 | |
partition(fn)
¶
Split into two collections: items passing fn and items failing.
Source code in src/arvel/data/collection.py
353 354 355 356 357 358 359 | |
split(n)
¶
Split into n groups as evenly as possible.
Source code in src/arvel/data/collection.py
361 362 363 364 365 366 367 368 369 370 371 372 373 | |
sliding(size, step=1)
¶
Return a sliding window of size items, advancing by step.
Source code in src/arvel/data/collection.py
375 376 377 378 379 380 381 382 383 | |
flatten(depth=-1)
¶
Flatten nested iterables (excluding strings/dicts).
depth=-1 flattens fully; depth=1 flattens one level.
Source code in src/arvel/data/collection.py
389 390 391 392 393 394 | |
collapse()
¶
Flatten one level of nesting.
Source code in src/arvel/data/collection.py
396 397 398 | |
pipe(fn)
¶
Pass the entire collection through fn and return the result.
Source code in src/arvel/data/collection.py
404 405 406 | |
tap(fn)
¶
Call fn with the collection for side effects, return self.
Source code in src/arvel/data/collection.py
408 409 410 411 | |
when(condition, fn, default=None)
¶
Apply fn if condition is truthy, otherwise apply default.
Source code in src/arvel/data/collection.py
413 414 415 416 417 418 419 420 421 422 423 424 | |
unless(condition, fn, default=None)
¶
Apply fn if condition is falsy, otherwise apply default.
Source code in src/arvel/data/collection.py
426 427 428 429 430 431 432 433 434 435 436 437 | |
reduce(fn, initial)
¶
Fold left over the collection.
Source code in src/arvel/data/collection.py
443 444 445 | |
every(fn)
¶
Return True if all items satisfy fn.
Source code in src/arvel/data/collection.py
447 448 449 | |
some(fn)
¶
Return True if any item satisfies fn. Alias for callable contains.
Source code in src/arvel/data/collection.py
451 452 453 | |
search(value)
¶
search(value: Callable[[T], bool]) -> int | None
search(value: T) -> int | None
Return the index of the first matching item, or None.
Accepts a value or a callable predicate.
Source code in src/arvel/data/collection.py
463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 | |
sole(fn=None)
¶
Return the only item matching fn, or the only item if fn is None.
Raises ValueError if zero or more than one match.
Source code in src/arvel/data/collection.py
479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 | |
implode(glue, key=None)
¶
Join items into a string, optionally plucking key first.
Source code in src/arvel/data/collection.py
499 500 501 502 503 | |
random(count=1)
¶
Return count random items. Returns a single item when count=1.
Source code in src/arvel/data/collection.py
509 510 511 512 513 514 515 516 517 518 | |
shuffle()
¶
Return a new collection with items in random order.
Source code in src/arvel/data/collection.py
520 521 522 523 524 | |
median(key=None)
¶
Return the statistical median.
Source code in src/arvel/data/collection.py
530 531 532 533 534 535 | |
mode(key=None)
¶
Return the most common value(s).
Source code in src/arvel/data/collection.py
537 538 539 540 541 542 543 544 | |
count_by(fn=None)
¶
Count occurrences grouped by the return value of fn or attribute key.
Source code in src/arvel/data/collection.py
546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 | |
duplicates(key=None)
¶
Return items that appear more than once.
Source code in src/arvel/data/collection.py
562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 | |
prepend(item)
¶
Return a new collection with item at the front.
Source code in src/arvel/data/collection.py
583 584 585 | |
push(*items)
¶
Return a new collection with items appended.
Source code in src/arvel/data/collection.py
587 588 589 | |
pop_item()
¶
Return the last item and a new collection without it.
Named pop_item to avoid shadowing list.pop.
Source code in src/arvel/data/collection.py
591 592 593 594 595 596 597 598 599 | |
shift()
¶
Return the first item and a new collection without it.
Source code in src/arvel/data/collection.py
601 602 603 604 605 606 | |
ensure(*types)
¶
Verify every item is an instance of one of types.
Returns self if all items match, raises TypeError otherwise.
Source code in src/arvel/data/collection.py
612 613 614 615 616 617 618 619 620 621 622 | |
percentage(fn, precision=2)
¶
Return the percentage of items satisfying fn.
Source code in src/arvel/data/collection.py
624 625 626 627 628 629 | |
multiply(n)
¶
Repeat the collection n times.
Source code in src/arvel/data/collection.py
631 632 633 | |
after(fn_or_value)
¶
Return the item after the first match, or None.
Source code in src/arvel/data/collection.py
635 636 637 638 639 640 641 642 643 644 645 646 | |
before(fn_or_value)
¶
Return the item before the first match, or None.
Source code in src/arvel/data/collection.py
648 649 650 651 652 653 654 655 656 657 658 659 660 661 | |
select(*keys)
¶
Pick only the given keys from each item (dict or object).
Source code in src/arvel/data/collection.py
663 664 665 666 667 668 669 670 671 | |
to_list()
¶
Return a plain Python list.
Source code in src/arvel/data/collection.py
702 703 704 | |
to_dict(key)
¶
Key items by an attribute, returning a dict.
Source code in src/arvel/data/collection.py
706 707 708 709 710 711 | |
to_json(*, indent=2)
¶
Serialize the collection to a JSON string.
Calls model_dump() on Pydantic/ArvelModel items so the
output is always plain dicts that json.dumps can handle.
Source code in src/arvel/data/collection.py
730 731 732 733 734 735 736 737 | |
DatabaseSettings
¶
Bases: ModuleSettings
Database connection and pool configuration.
All fields are prefixed with DB_ in environment variables.
Pool defaults follow SA recommended practices:
- pool_pre_ping=True detects stale connections from DB restarts
- pool_recycle=3600 prevents long-lived connections from going stale
- expire_on_commit=False prevents lazy-load errors after commit in async
Source code in src/arvel/data/config.py
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | |
url
property
¶
Resolve DB URL from DB_URL or structured DB_* values.
ConfigurationError
¶
Bases: DataError
Raised when a model or repository is misconfigured.
Examples: setting both __fillable__ and __guarded__,
missing required config, or conflicting options.
Source code in src/arvel/data/exceptions.py
96 97 98 99 100 101 | |
CreationAbortedError
¶
Bases: DataError
Raised when an observer vetoes record creation.
Attributes:
| Name | Type | Description |
|---|---|---|
model_name |
The model class name. |
|
observer_name |
The observer that vetoed. |
Source code in src/arvel/data/exceptions.py
36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | |
DataError
¶
Bases: ArvelError
Base for all data layer exceptions.
Source code in src/arvel/data/exceptions.py
12 13 | |
DeletionAbortedError
¶
Bases: DataError
Raised when an observer vetoes a record deletion.
Attributes:
| Name | Type | Description |
|---|---|---|
model_name |
The model class name. |
|
observer_name |
The observer that vetoed. |
Source code in src/arvel/data/exceptions.py
76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 | |
IntegrityError
¶
Bases: DataError
Raised when a database constraint is violated.
Wraps SA's IntegrityError with a human-readable message.
Source code in src/arvel/data/exceptions.py
124 125 126 127 128 | |
MassAssignmentError
¶
Bases: DataError
Raised in strict mode when a guarded field is assigned.
Attributes:
| Name | Type | Description |
|---|---|---|
model_name |
The model class name. |
|
field_name |
The field that was blocked. |
Source code in src/arvel/data/exceptions.py
104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 | |
NotFoundError
¶
Bases: DataError
Raised when a record is not found by ID.
Attributes:
| Name | Type | Description |
|---|---|---|
model_name |
The model class name. |
|
record_id |
The ID that was searched for. |
Source code in src/arvel/data/exceptions.py
16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | |
TransactionError
¶
Bases: DataError
Raised when a database transaction fails.
Source code in src/arvel/data/exceptions.py
131 132 | |
UpdateAbortedError
¶
Bases: DataError
Raised when an observer vetoes a record update.
Attributes:
| Name | Type | Description |
|---|---|---|
model_name |
The model class name. |
|
observer_name |
The observer that vetoed. |
Source code in src/arvel/data/exceptions.py
56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | |
MaterializedView
¶
Bases: ABC
Abstract base for materialized views.
Subclass, set __viewname__, and implement query_definition()::
class ActiveUserStats(MaterializedView):
__viewname__ = "active_user_stats"
@classmethod
def query_definition(cls) -> Select:
return select(User.id, func.count(Post.id)).join(...).group_by(User.id)
Source code in src/arvel/data/materialized_view.py
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | |
query_definition()
abstractmethod
classmethod
¶
Return a SQLAlchemy select() defining the view.
Source code in src/arvel/data/materialized_view.py
29 30 31 32 33 | |
ViewRegistry
¶
Registry for materialized view classes.
Source code in src/arvel/data/materialized_view.py
36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 | |
refresh(name, *, db_url)
async
¶
Refresh a single materialized view by name.
On SQLite this is a no-op that returns a status dict. On PostgreSQL this executes REFRESH MATERIALIZED VIEW.
Source code in src/arvel/data/materialized_view.py
55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 | |
refresh_all(*, db_url)
async
¶
Refresh all registered views.
Source code in src/arvel/data/materialized_view.py
82 83 84 85 86 87 88 | |
MigrationRunner
¶
Programmatic Alembic migration runner.
Wraps alembic.command operations behind an async-friendly interface. Uses ArvelModel.metadata for auto-generation and reads DB URL from the caller (typically DatabaseSettings).
The original DB URL (async or sync dialect) is passed through to
run_alembic_env() which dispatches the correct engine strategy.
Source code in src/arvel/data/migrations.py
261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 | |
generate(message)
¶
Generate a new migration file from a Jinja template.
Source code in src/arvel/data/migrations.py
317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 | |
upgrade(revision='head', *, environment='development', force=False)
async
¶
Run pending migrations up to revision.
Source code in src/arvel/data/migrations.py
352 353 354 355 356 357 358 359 360 361 362 363 364 365 | |
downgrade(steps=1, *, environment='development', force=False)
async
¶
Rollback N migration steps. Silently succeeds if there are no migrations to rollback.
Source code in src/arvel/data/migrations.py
367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 | |
fresh(*, environment='development', force=False)
async
¶
Drop all tables then re-run all migrations from scratch.
Unlike downgrade (which replays downgrade scripts), this method
uses metadata.drop_all() to unconditionally drop every known
table — including the Alembic version table — so the database is
truly empty before upgrade runs. This matches Laravel's
migrate:fresh semantics.
Source code in src/arvel/data/migrations.py
386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 | |
status()
async
¶
Return a list of migration entries with applied/pending status.
Source code in src/arvel/data/migrations.py
441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 | |
get_env_template()
staticmethod
¶
Return the async env.py template string.
Source code in src/arvel/data/migrations.py
458 459 460 461 | |
MigrationStatusEntry
¶
Bases: TypedDict
Structured migration status entry.
Source code in src/arvel/data/migrations.py
33 34 35 36 37 38 | |
ArvelModel
¶
Bases: HasRelationships, DeclarativeBase
Base model class bridging SQLAlchemy and Pydantic.
Subclasses get automatic: - SA table mapping (via DeclarativeBase) - Pydantic schema generation (pydantic_model) - Timestamp fields (created_at, updated_at) when declared - model_dump() / model_validate() compatibility - Declarative relationship helpers via HasRelationships
Set __singular__ on models with irregular plural table names so
convention-based FK inference works correctly::
class Person(ArvelModel):
__tablename__ = "people"
__singular__ = "person"
Source code in src/arvel/data/model.py
120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 | |
model_validate(data)
classmethod
¶
Validate data through the auto-generated Pydantic schema, then create an instance.
Source code in src/arvel/data/model.py
250 251 252 253 254 255 | |
__getattribute__(name)
¶
Transparent cast on read + accessor resolution.
For cast columns, the raw SA value is transformed through the
caster's get() on every attribute access — no manual
get_cast_value() needed.
Models without __casts__ skip the cast-check branch entirely.
Source code in src/arvel/data/model.py
257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 | |
__setattr__(name, value)
¶
Transparent cast on write + mutator integration.
For cast columns, the value is transformed through the caster's
set() before storage. Mutators run before casts.
Source code in src/arvel/data/model.py
287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 | |
get_cast_value(attr_name)
¶
Explicitly get a column value with its cast applied.
With transparent casting, this is equivalent to getattr(self, attr_name)
for cast columns. Kept for backward compatibility.
Source code in src/arvel/data/model.py
309 310 311 312 313 314 315 | |
make_hidden(*fields)
¶
Hide additional fields on this instance (does not affect the class).
Source code in src/arvel/data/model.py
319 320 321 322 323 324 | |
make_visible(*fields)
¶
Un-hide fields on this instance (does not affect the class).
Source code in src/arvel/data/model.py
326 327 328 329 330 331 | |
model_dump(*, include=None, exclude=None, include_relations=False)
¶
Serialize the model instance to a dict.
Respects __hidden__, __visible__, __appends__,
and instance-level make_hidden() / make_visible() overrides.
__visible__is a whitelist — if set, ONLY those fields appear__hidden__is a blacklist — those fields are excluded__appends__adds accessor-computed values automatically- Cast values use their Python-side representations (via getattribute)
Source code in src/arvel/data/model.py
346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 | |
model_json(*, include=None, exclude=None, include_relations=False)
¶
Serialize the model instance to a JSON string.
Convenience wrapper around model_dump() with JSON encoding
of non-serializable types (datetime, date, Decimal, etc.).
Source code in src/arvel/data/model.py
382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 | |
set_session_resolver(resolver)
classmethod
¶
Set the default session resolver for all models.
Called by DatabaseServiceProvider during boot so that
Model.query() works without an explicit session.
Source code in src/arvel/data/model.py
485 486 487 488 489 490 491 492 | |
set_session_factory(factory)
classmethod
¶
Set a factory that creates auto-managed sessions for CRUD ops.
Unlike set_session_resolver, sessions from this factory are
committed on success and closed on exit by _session_scope.
Source code in src/arvel/data/model.py
494 495 496 497 498 499 500 501 | |
clear_session_resolver()
classmethod
¶
Remove the default session resolver and factory.
Source code in src/arvel/data/model.py
503 504 505 506 507 | |
set_observer_registry(resolver)
classmethod
¶
Set the default observer registry resolver for all models.
Source code in src/arvel/data/model.py
559 560 561 562 | |
clear_observer_registry()
classmethod
¶
Remove the default observer registry resolver.
Source code in src/arvel/data/model.py
564 565 566 567 | |
query(session=None)
classmethod
¶
Create a fluent query builder.
When session is omitted, uses the resolver set by
DatabaseServiceProvider. Pass an explicit session to
override (useful in tests or transactions).
Source code in src/arvel/data/model.py
580 581 582 583 584 585 586 587 588 589 590 591 592 | |
where(*criteria, session=None)
classmethod
¶
Shortcut for Model.query().where(...).
Source code in src/arvel/data/model.py
618 619 620 621 622 623 | |
first(*, session=None)
async
classmethod
¶
Return the first record or None.
Source code in src/arvel/data/model.py
625 626 627 628 629 | |
last(*, session=None)
async
classmethod
¶
Return the last record (by PK descending) or None.
Source code in src/arvel/data/model.py
631 632 633 634 635 | |
count(*, session=None)
async
classmethod
¶
Return the total record count.
Source code in src/arvel/data/model.py
637 638 639 640 | |
find(record_id, *, session=None)
async
classmethod
¶
Find a record by primary key or raise NotFoundError.
Source code in src/arvel/data/model.py
644 645 646 647 648 649 650 651 652 653 654 655 | |
find_or_none(record_id, *, session=None)
async
classmethod
¶
Find a record by primary key, returning None if not found.
Source code in src/arvel/data/model.py
657 658 659 660 661 662 663 | |
find_many(record_ids, *, session=None)
async
classmethod
¶
Find multiple records by primary keys.
Missing IDs are silently skipped — no error is raised.
Source code in src/arvel/data/model.py
665 666 667 668 669 670 671 672 673 674 675 676 677 678 | |
all(*, session=None)
async
classmethod
¶
Retrieve all records.
Source code in src/arvel/data/model.py
680 681 682 683 | |
create(data, *, session=None)
async
classmethod
¶
Create a new record with mass-assignment protection and observer dispatch.
Event order: saving → creating → INSERT → created → saved
Source code in src/arvel/data/model.py
687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 | |
update(data, *, session=None)
async
¶
Update this instance with observer dispatch.
Event order: saving → updating → UPDATE → updated → saved
Source code in src/arvel/data/model.py
718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 | |
save(*, session=None)
async
¶
Persist the current state of this instance.
Detects new vs existing: fires creating/created for inserts, updating/updated for updates. Always fires saving/saved.
Source code in src/arvel/data/model.py
751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 | |
delete(*, session=None)
async
¶
Delete this instance with observer dispatch.
If the model uses SoftDeletes, sets deleted_at instead
of removing the row.
Source code in src/arvel/data/model.py
799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 | |
refresh(*, session=None)
async
¶
Reload this instance from the database.
Source code in src/arvel/data/model.py
828 829 830 831 832 | |
fill(data)
¶
Mass-assign attributes without saving.
Respects __fillable__ / __guarded__ protection.
Source code in src/arvel/data/model.py
836 837 838 839 840 841 842 843 844 | |
first_or_create(search, values=None, *, session=None)
async
classmethod
¶
Find a matching record or create one.
search fields are used to find; values are merged when creating. Observer events fire on creation.
Source code in src/arvel/data/model.py
848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 | |
first_or_new(search, values=None)
classmethod
¶
Build a new (unsaved) instance from search + values.
Does NOT persist. Does NOT query the database.
Use first_or_create if you need to check the DB first.
Source code in src/arvel/data/model.py
874 875 876 877 878 879 880 881 882 883 884 885 886 887 | |
update_or_create(search, values=None, *, session=None)
async
classmethod
¶
Find a matching record and update it, or create a new one.
Observer events fire for both update and create paths.
Source code in src/arvel/data/model.py
889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 | |
destroy(record_ids, *, session=None)
async
classmethod
¶
Delete one or more records by primary key.
Returns the number of records deleted. Fires observer events for each record.
Source code in src/arvel/data/model.py
916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 | |
ModelObserver
¶
Base class for model lifecycle observers.
Parameterize with the model type for typed hooks::
class UserObserver(ModelObserver[User]):
async def creating(self, instance: User) -> bool: ...
Using ModelObserver without a type parameter falls back to
Any (backward compatible).
Override any hook method. creating, updating, and deleting
can return False to abort the operation.
Source code in src/arvel/data/observer.py
45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 | |
ObserverRegistry
¶
Stores and dispatches model lifecycle observers.
Observers can be registered by model class or by model name string (for cross-module decoupling). Multiple observers per model execute in priority order (lower = earlier).
Source code in src/arvel/data/observer.py
97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 | |
dispatch(event, model_cls, instance)
async
¶
Dispatch a lifecycle event to all registered observers.
For pre-events (creating, updating, deleting, etc.): returns False if any observer returns False.
Source code in src/arvel/data/observer.py
131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 | |
CursorMeta
¶
Bases: TypedDict
Typed metadata for cursor-based pagination.
Source code in src/arvel/data/pagination.py
36 37 38 39 40 | |
CursorResponse
¶
Bases: TypedDict
Typed response for cursor-based pagination, suitable for API serialization.
Source code in src/arvel/data/pagination.py
43 44 45 46 47 | |
CursorResult
dataclass
¶
Cursor-based pagination result.
Source code in src/arvel/data/pagination.py
87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 | |
to_response()
¶
Return a typed response dict for API serialization.
Source code in src/arvel/data/pagination.py
95 96 97 98 99 100 101 102 103 | |
PaginatedResponse
¶
Bases: TypedDict
Typed response for offset-based pagination, suitable for API serialization.
Source code in src/arvel/data/pagination.py
29 30 31 32 33 | |
PaginatedResult
dataclass
¶
Offset-based pagination result.
Source code in src/arvel/data/pagination.py
50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 | |
to_response()
¶
Return a typed response dict for API serialization.
Source code in src/arvel/data/pagination.py
73 74 75 76 77 78 79 80 81 82 83 84 | |
PaginationMeta
¶
Bases: TypedDict
Typed metadata for offset-based pagination.
Source code in src/arvel/data/pagination.py
19 20 21 22 23 24 25 26 | |
DatabaseServiceProvider
¶
Bases: ServiceProvider
Async engine + session factory. Priority 5 (before infra at 10).
Source code in src/arvel/data/provider.py
34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 | |
QueryBuilder
¶
Fluent query builder producing parameterized SQL.
Usage::
users = await User.query(session).where(User.active == True).order_by(User.name).all()
Source code in src/arvel/data/query.py
37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 | |
__getattr__(name)
¶
Resolve local query scopes defined on the model.
Source code in src/arvel/data/query.py
94 95 96 97 98 99 100 101 102 103 104 105 106 | |
without_global_scope(scope_name)
¶
Exclude a named global scope from this query.
Source code in src/arvel/data/query.py
108 109 110 111 | |
without_global_scopes()
¶
Exclude all global scopes from this query.
Source code in src/arvel/data/query.py
113 114 115 116 | |
with_trashed()
¶
Include soft-deleted rows (removes the SoftDeleteScope).
Source code in src/arvel/data/query.py
118 119 120 | |
only_trashed()
¶
Return only soft-deleted rows.
Source code in src/arvel/data/query.py
122 123 124 125 126 127 128 129 130 | |
with_(*relationships)
¶
Eager-load relationships by name using selectinload.
Supports nested dot notation: with_("posts", "posts.comments")
produces selectinload(Model.posts).selectinload(Post.comments).
Raises ValueError if a relationship name does not exist on the model.
Source code in src/arvel/data/query.py
168 169 170 171 172 173 174 175 176 177 178 179 180 | |
has(relationship_name, operator='>', count=0)
¶
Filter to models that have related records matching a count condition.
Raises ValueError if the relationship name doesn't exist.
Examples::
User.query(s).has("posts").all() # users with >=1 post
User.query(s).has("posts", ">", 5).all() # users with >5 posts
Source code in src/arvel/data/query.py
209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 | |
doesnt_have(relationship_name)
¶
Filter to models that have zero related records.
Source code in src/arvel/data/query.py
230 231 232 | |
where_has(relationship_name, callback)
¶
Filter to models whose related records match extra conditions.
Raises ValueError if the relationship name doesn't exist.
The callback receives the related model class and should return a SQLAlchemy criterion::
User.query(s).where_has("posts", lambda Post: Post.is_published == True).all()
Source code in src/arvel/data/query.py
234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 | |
with_count(relationship_name)
¶
Add a {relationship}_count column to the result.
The count is computed as a correlated subquery. Supports both direct FK relationships and M2M (pivot/secondary) relationships.
Raises ValueError if the relationship name doesn't exist.
Source code in src/arvel/data/query.py
275 276 277 278 279 280 281 282 283 284 285 286 287 288 | |
recursive(anchor, step, *, max_depth=None, cycle_detection=False)
¶
Build a WITH RECURSIVE CTE from anchor and step conditions.
Returns a RecursiveQueryBuilder whose all() / first()
produce TreeNode[T] results instead of plain T.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
anchor
|
Any
|
WHERE clause for the anchor (base case) rows. |
required |
step
|
Callable[..., Any]
|
Callable receiving the CTE alias and returning the join condition for the recursive term. |
required |
max_depth
|
int | None
|
Maximum recursion depth (default DEFAULT_MAX_DEPTH). |
None
|
cycle_detection
|
bool
|
If True, add path-tracking to detect cycles. |
False
|
Source code in src/arvel/data/query.py
375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 | |
ancestors(node_id, *, max_depth=None)
¶
Return all ancestors of node_id up to the root.
Auto-detects the parent_id column from self-referencing FK.
Source code in src/arvel/data/query.py
430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 | |
descendants(node_id, *, max_depth=None)
¶
Return all descendants of node_id down to the leaves.
Auto-detects the parent_id column from self-referencing FK.
Source code in src/arvel/data/query.py
448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 | |
build_statement()
¶
Return the underlying SA Select for inspection/testing.
Source code in src/arvel/data/query.py
488 489 490 | |
all()
async
¶
Execute the query and return all results.
Source code in src/arvel/data/query.py
492 493 494 495 496 497 498 499 500 501 | |
first()
async
¶
Execute the query and return the first result, or None.
Source code in src/arvel/data/query.py
503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 | |
all_with_counts()
async
¶
Execute a with_count() query and return typed results.
Each result wraps the model instance with relationship counts::
results = await User.query(s).with_count("posts").all_with_counts()
for r in results:
print(r.instance.name, r.counts["posts"])
Source code in src/arvel/data/query.py
521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 | |
first_with_count()
async
¶
Execute a with_count() query and return the first typed result.
Source code in src/arvel/data/query.py
537 538 539 540 541 542 543 544 545 546 547 | |
to_sql()
¶
Return the compiled SQL string for debugging.
Disabled in production (ARVEL_DEBUG must be true).
Raises RuntimeError if called with debug mode off.
Source code in src/arvel/data/query.py
565 566 567 568 569 570 571 572 573 574 575 | |
max(column)
async
¶
Return the maximum value for column.
Source code in src/arvel/data/query.py
598 599 600 601 602 603 604 605 606 607 | |
min(column)
async
¶
Return the minimum value for column.
Source code in src/arvel/data/query.py
609 610 611 612 613 614 615 616 617 618 | |
sum(column)
async
¶
Return the sum for column.
Source code in src/arvel/data/query.py
620 621 622 623 624 625 626 627 628 629 | |
avg(column)
async
¶
Return the average for column.
Source code in src/arvel/data/query.py
631 632 633 634 635 636 637 638 639 640 | |
RecursiveQueryBuilder
¶
Bases: QueryBuilder[T]
Query builder for recursive CTE queries.
Returned by QueryBuilder.recursive(), ancestors(), and
descendants(). Terminal methods (all, first) produce
TreeNode[T] results; all_as_tree / first_as_tree build
a nested hierarchy.
Source code in src/arvel/data/query.py
643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 | |
all()
async
¶
Execute the recursive CTE and return flat TreeNode results.
Source code in src/arvel/data/query.py
668 669 670 671 672 673 674 675 676 677 678 | |
first()
async
¶
Execute the recursive CTE and return the first flat TreeNode.
Source code in src/arvel/data/query.py
680 681 682 683 684 685 686 687 688 689 690 691 | |
all_as_tree()
async
¶
Execute the recursive CTE and return a nested tree.
Flat CTE rows are assembled into a hierarchy using the model's
self-referencing FK. Returns only root nodes; children are
accessible via node.children::
roots = await Category.query(s).descendants(1).all_as_tree()
for root in roots:
for child in root.children:
print(child.data["name"], child.depth)
Source code in src/arvel/data/query.py
693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 | |
first_as_tree()
async
¶
Execute the recursive CTE and return the first nested root node.
The full subtree is nested under the returned node.
Source code in src/arvel/data/query.py
718 719 720 721 722 723 724 | |
HasRelationships
¶
Mixin that enables declarative relationship helpers on ArvelModel subclasses.
Replaces RelationshipDescriptor instances in the class namespace with proper SA relationship() objects before DeclarativeBase processes the class.
Models with irregular plural table names should set __singular__ to the
singular form (e.g. __singular__ = "person" for __tablename__ = "people").
Source code in src/arvel/data/relationships/mixin.py
152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 | |
get_relationships()
classmethod
¶
Return a dict of {name: RelationshipDescriptor} for all declared relationships.
Source code in src/arvel/data/relationships/mixin.py
187 188 189 190 191 192 193 | |
LazyLoadError
¶
Bases: Exception
Raised when a relationship is accessed without prior eager loading in strict mode.
Source code in src/arvel/data/relationships/mixin.py
30 31 | |
PivotManager
¶
Manages rows in a many-to-many pivot table for one side of the relationship.
All mutations happen through the session and are flushed immediately so callers can read back consistent state within the same transaction.
Source code in src/arvel/data/relationships/pivot.py
18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 | |
attach(related_id, **extra)
async
¶
Insert a pivot row linking owner to related_id.
Extra kwargs are written to additional pivot columns if they exist.
Source code in src/arvel/data/relationships/pivot.py
40 41 42 43 44 45 46 47 48 49 50 51 52 | |
detach(related_id)
async
¶
Remove the pivot row linking owner to related_id.
Source code in src/arvel/data/relationships/pivot.py
54 55 56 57 58 59 60 61 62 | |
sync(related_ids)
async
¶
Replace all pivot rows for owner with exactly the given related_ids.
Operates atomically: removes extras, batch-inserts missing.
Source code in src/arvel/data/relationships/pivot.py
64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 | |
ids()
async
¶
Return all related IDs currently linked via the pivot table.
Source code in src/arvel/data/relationships/pivot.py
94 95 96 97 98 | |
RelationshipDescriptor
dataclass
¶
Immutable metadata about a declared relationship.
Stored in the model's relationship_registry for introspection.
At runtime, HasRelationships.__init_subclass__ replaces these with
SA relationship() objects, so __get__ is never actually called
on a configured model. The __get__ exists purely for static type
checkers.
Source code in src/arvel/data/relationships/descriptors.py
28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | |
Repository
¶
Base repository providing typed CRUD operations.
The session is private — custom query methods use self.query()
which returns a QueryBuilder[T], not the raw session.
When DatabaseServiceProvider is registered, both session and
observer_registry are optional — the repository resolves them
from the model's session resolver and creates an empty registry::
repo = UserRepository() # uses default session
repo = UserRepository(session=my_session) # explicit override
Source code in src/arvel/data/repository.py
53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 | |
restore(record_id)
async
¶
Restore a soft-deleted record by setting deleted_at to NULL.
Raises NotFoundError if the record doesn't exist.
Only meaningful for models using the SoftDeletes mixin.
Source code in src/arvel/data/repository.py
189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 | |
force_delete(record_id)
async
¶
Permanently remove a record, bypassing soft deletes.
Dispatches force_deleting/force_deleted observer events.
Source code in src/arvel/data/repository.py
222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 | |
TreeNode
¶
Bases: BaseModel
Nested tree wrapper for recursive CTE query results.
Rows returned by a recursive CTE are flat; build_tree assembles
them into a proper nested structure using the parent FK column::
roots = await Category.query(s).descendants(1).all()
for node in roots:
print(node.model_dump_json(indent=2))
Source code in src/arvel/data/results.py
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 | |
from_row(row, column_names)
classmethod
¶
Build a flat TreeNode from a raw CTE result row.
The last element is always the depth column added by the
recursive builder.
Source code in src/arvel/data/results.py
63 64 65 66 67 68 69 70 71 72 | |
build_tree(flat_nodes, *, id_key='id', parent_key='parent_id')
classmethod
¶
Assemble flat nodes into a nested tree.
Returns the root nodes with children populated recursively.
Nodes whose parent is missing from the set are treated as roots.
Source code in src/arvel/data/results.py
74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 | |
model_dump(**kwargs)
¶
Flatten data into the top level and nest children.
Source code in src/arvel/data/results.py
102 103 104 105 106 107 | |
WithCount
dataclass
¶
Typed wrapper for query results that include relationship counts.
Access the original model instance via .instance and counts
via .counts::
results: list[WithCount[User]] = await User.query(s).with_count("posts").all()
for r in results:
user = r.instance
n_posts = r.counts["posts"]
# or use the convenience accessor:
n_posts = r.posts_count # equivalent to r.counts["posts"]
Source code in src/arvel/data/results.py
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | |
GlobalScope
¶
Base class for global scopes applied to every query on a model.
Subclass and implement apply to define the constraint::
class ActiveScope(GlobalScope):
def apply(self, query):
return query.where(User.is_active == True)
class User(ArvelModel):
__global_scopes__ = [ActiveScope()]
Source code in src/arvel/data/scopes.py
60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 | |
apply(query)
¶
Apply the scope to the given query builder. Must return the query.
Source code in src/arvel/data/scopes.py
75 76 77 | |
Seeder
¶
Bases: ABC
Base class for database seeders.
Subclass and implement run() to populate data::
class UserSeeder(Seeder):
async def run(self, tx: Transaction) -> None:
await tx.users.create({"name": "Admin", "email": "admin@app.com"})
Source code in src/arvel/data/seeder.py
26 27 28 29 30 31 32 33 34 35 36 37 | |
SeedRunner
¶
Discovers and executes seeders with production safety.
Source code in src/arvel/data/seeder.py
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 | |
run(*, environment='development', force=False, seeder_class=None)
async
¶
Run seeders. Refuses in production without force.
Source code in src/arvel/data/seeder.py
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 | |
Blueprint
¶
Table definition builder — collects columns for a single table.
Source code in src/arvel/data/schema.py
136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 | |
id(name='id', key_type=KeyType.BIG_INT)
¶
Add an auto-incrementing or UUID primary key.
For integer types, uses BigInteger with a Integer variant for
SQLite — SQLite only auto-increments INTEGER PRIMARY KEY columns.
Source code in src/arvel/data/schema.py
152 153 154 155 156 157 158 159 160 161 162 | |
timestamps()
¶
Add created_at and updated_at columns.
Source code in src/arvel/data/schema.py
191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 | |
soft_deletes()
¶
Add a deleted_at column for soft deletes.
Source code in src/arvel/data/schema.py
211 212 213 | |
foreign_id(name)
¶
Add a BigInteger FK column (convention:
Source code in src/arvel/data/schema.py
215 216 217 | |
ColumnBuilder
¶
Fluent builder for column constraints.
Source code in src/arvel/data/schema.py
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 | |
on_delete(action)
¶
Set ON DELETE behavior for the last attached foreign key.
Source code in src/arvel/data/schema.py
100 101 102 103 104 | |
on_update(action)
¶
Set ON UPDATE behavior for the last attached foreign key.
Source code in src/arvel/data/schema.py
106 107 108 109 110 | |
references(table, column='id', *, on_delete=None, on_update=None)
¶
Attach an explicit foreign-key reference to this column.
Generates a conventional constraint name fk_<col>_<table> so
the FK works in both Schema.create() and Schema.table()
(Alembic batch mode requires named constraints).
Source code in src/arvel/data/schema.py
112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 | |
ForeignKeyAction
¶
Bases: StrEnum
Allowed ON DELETE / ON UPDATE actions.
Source code in src/arvel/data/schema.py
55 56 57 58 59 60 61 62 | |
KeyType
¶
Bases: Enum
Primary key column type.
Source code in src/arvel/data/schema.py
47 48 49 50 51 52 | |
Schema
¶
Static facade for table DDL operations.
Source code in src/arvel/data/schema.py
220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 | |
create(table_name, callback)
staticmethod
¶
Create a new table from a Blueprint callback.
Source code in src/arvel/data/schema.py
223 224 225 226 227 228 | |
table(table_name, callback)
staticmethod
¶
Alter an existing table via a Blueprint callback.
Indexes are created outside the batch context to avoid SQLite batch-mode issues where the copy-table process tries to recreate indexes on columns that may not exist in the target table.
Source code in src/arvel/data/schema.py
230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 | |
drop_columns(table_name, *columns)
staticmethod
¶
Remove columns (with their indexes and FK constraints) from a table.
For each column this method:
- Drops the standalone index
ix_{table}_{col}(created by :meth:table) before entering the batch — SQLite's copy-table process would otherwise try to recreate it on a column that no longer exists. - Inside the batch, drops the FK constraint
fk_{col}_*(following the convention set by :meth:ColumnBuilder.references) and the column itself.
Source code in src/arvel/data/schema.py
251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 | |
drop(table_name)
staticmethod
¶
Drop a table.
Source code in src/arvel/data/schema.py
286 287 288 289 | |
rename(old_name, new_name)
staticmethod
¶
Rename a table.
Source code in src/arvel/data/schema.py
291 292 293 294 | |
SoftDeletes
¶
Mixin that adds soft-delete behavior to an ArvelModel subclass.
Declares deleted_at: Mapped[datetime | None] and registers a
global scope that filters out soft-deleted rows.
The Repository checks for this mixin at runtime and redirects
delete() to set deleted_at instead of issuing a hard DELETE.
Source code in src/arvel/data/soft_deletes.py
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | |
trashed
property
¶
Whether this instance is soft-deleted.
Transaction
¶
Transaction context manager for atomic repository operations.
Preferred pattern — typed @property methods::
class AppTransaction(Transaction):
@property
def users(self) -> UserRepository:
return self._get_repo(UserRepository)
Legacy pattern — annotation-based (returns Any to checkers)::
class AppTransaction(Transaction):
users: UserRepository
When DatabaseServiceProvider is registered, both session and
observer_registry are optional::
async with AppTransaction() as tx:
user = await tx.users.create(data)
Source code in src/arvel/data/transaction.py
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 | |
nested()
async
¶
Create a nested savepoint within the current transaction.
Source code in src/arvel/data/transaction.py
151 152 153 154 155 156 157 158 159 160 161 162 163 164 | |
accessor(attr_name)
¶
Decorator to register a method as an accessor for attr_name.
Source code in src/arvel/data/accessors.py
36 37 38 39 40 41 42 43 44 45 46 | |
mutator(attr_name)
¶
Decorator to register a method as a mutator for attr_name.
Source code in src/arvel/data/accessors.py
49 50 51 52 53 54 55 56 57 58 59 | |
collect(items=None)
¶
Create an ArvelCollection from any iterable.
::
from arvel.data.collection import collect
numbers = collect([1, 2, 3, 4, 5])
names = collect(user.name for user in users)
Source code in src/arvel/data/collection.py
780 781 782 783 784 785 786 787 788 789 790 791 792 | |
detect_pg_ivm(db_url)
async
¶
Check if the pg_ivm extension is available.
Returns False for non-PostgreSQL databases.
Source code in src/arvel/data/materialized_view.py
91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 | |
register_framework_migration(filename, content)
¶
Register a migration that ships with the framework.
Called by framework providers (e.g. MediaProvider) at import time.
The migration is published into the user's database/migrations/
directory when MigrationRunner.upgrade() or fresh() runs,
but only if a file with the same base name doesn't already exist.
Source code in src/arvel/data/migrations.py
46 47 48 49 50 51 52 53 54 | |
run_alembic_env()
¶
Run Alembic env logic using framework-managed defaults.
Starter/local migration env.py files call this so they don't need to import Alembic or SQLAlchemy directly.
Source code in src/arvel/data/migrations.py
123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 | |
belongs_to(related, *, foreign_key=None, local_key=None, back_populates=None)
¶
belongs_to(
related: type[T],
*,
foreign_key: str | None = ...,
local_key: str | None = ...,
back_populates: str | None = ...,
) -> T
belongs_to(
related: str,
*,
foreign_key: str | None = ...,
local_key: str | None = ...,
back_populates: str | None = ...,
) -> Any
Declare the inverse of has_one/has_many — the FK lives on this model.
Convention: FK column on this table is {related_tablename}_id.
Source code in src/arvel/data/relationships/descriptors.py
174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 | |
belongs_to_many(related, *, pivot_table=None, foreign_key=None, related_key=None, pivot_fields=None, back_populates=None)
¶
belongs_to_many(
related: type[T],
*,
pivot_table: str | None = ...,
foreign_key: str | None = ...,
related_key: str | None = ...,
pivot_fields: list[str] | None = ...,
back_populates: str | None = ...,
) -> list[T]
belongs_to_many(
related: str,
*,
pivot_table: str | None = ...,
foreign_key: str | None = ...,
related_key: str | None = ...,
pivot_fields: list[str] | None = ...,
back_populates: str | None = ...,
) -> Any
Declare a many-to-many relationship through a pivot (association) table.
Conventions:
- Pivot table: alphabetical join of both table names (e.g. role_user).
- FK columns: {table}_id for each side.
Source code in src/arvel/data/relationships/descriptors.py
218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 | |
has_many(related, *, foreign_key=None, local_key=None, back_populates=None)
¶
has_many(
related: type[T],
*,
foreign_key: str | None = ...,
local_key: str | None = ...,
back_populates: str | None = ...,
) -> list[T]
has_many(
related: str,
*,
foreign_key: str | None = ...,
local_key: str | None = ...,
back_populates: str | None = ...,
) -> Any
Declare a one-to-many relationship where the FK lives on the related model.
Convention: FK on the related table is {owner_tablename}_id.
Source code in src/arvel/data/relationships/descriptors.py
134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 | |
has_one(related, *, foreign_key=None, local_key=None, back_populates=None)
¶
has_one(
related: type[T],
*,
foreign_key: str | None = ...,
local_key: str | None = ...,
back_populates: str | None = ...,
) -> T
has_one(
related: str,
*,
foreign_key: str | None = ...,
local_key: str | None = ...,
back_populates: str | None = ...,
) -> Any
Declare a one-to-one relationship where the FK lives on the related model.
Convention: FK on the related table is {owner_tablename}_id.
Source code in src/arvel/data/relationships/descriptors.py
94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 | |
load_morph_parent(instance, name, session)
async
¶
Eagerly load the polymorphic parent for a morph_to relationship.
Reads {name}_type and {name}_id from the instance, resolves
the parent model class via the morph type map, and queries for it.
Source code in src/arvel/data/relationships/morphs.py
118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 | |
morph_many(related, name)
¶
Declare the parent side of a polymorphic one-to-many relationship.
Example::
class Post(ArvelModel):
comments = morph_many(Comment, "commentable")
Source code in src/arvel/data/relationships/morphs.py
86 87 88 89 90 91 92 93 94 | |
morph_to(name)
¶
Declare the child side of a polymorphic relationship.
The model must have {name}_type and {name}_id columns.
Example::
class Comment(ArvelModel):
commentable_type: Mapped[str] = mapped_column(String(100))
commentable_id: Mapped[int] = mapped_column()
commentable = morph_to("commentable")
Source code in src/arvel/data/relationships/morphs.py
71 72 73 74 75 76 77 78 79 80 81 82 83 | |
morph_to_many(related, name, *, morph_map=None)
¶
Declare a polymorphic many-to-many relationship via a pivot table.
Example::
class Post(ArvelModel):
tags = morph_to_many(Tag, "taggable")
Source code in src/arvel/data/relationships/morphs.py
97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 | |
query_morph_children(parent, related_cls, name, session)
async
¶
Query all children of a polymorphic one-to-many relationship.
Filters {name}_type to the parent's morph alias and
{name}_id to the parent's PK.
Source code in src/arvel/data/relationships/morphs.py
142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 | |
register_morph_type(alias, model_cls)
¶
Register a short alias for a model class in the morph type map.
Source code in src/arvel/data/relationships/morphs.py
27 28 29 | |
scope(fn)
¶
Mark a static/classmethod as a local query scope.
The decorator sets a marker attribute; __init_subclass__ picks it
up and registers the scope name (stripped of the scope_ prefix if
present).
Works with both bare functions and @staticmethod/@classmethod
wrappers regardless of decorator order.
Source code in src/arvel/data/scopes.py
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | |
discover_seeders(seeders_dir)
¶
Scan a directory for Seeder subclasses, sorted alphabetically by filename.
The project root (seeders_dir.parent.parent, i.e. two levels up from
database/seeders/) is temporarily added to sys.path so that seeder
modules can use application-level imports such as
from app.models.user import User.
Source code in src/arvel/data/seeder.py
66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 | |
arvel.http
¶
HTTP and Routing layer — route registration, middleware, controller DI, URL generation.
arvel.foundation
¶
Foundation layer — kernel, container, providers, config, pipeline.
PipeSpec = type | Pipe | Callable[..., Any]
module-attribute
¶
A pipe specification: a class (resolved via DI), a Pipe callable, or any callable.
Application
¶
Arvel application kernel.
User code should call Application.configure(base_path) which returns an
ASGI-compatible object suitable for uvicorn. The async bootstrap runs
automatically on the first ASGI event.
Source code in src/arvel/foundation/application.py
118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 | |
configure(base_path='.', *, testing=False)
classmethod
¶
Configure an Arvel application (sync, safe for uvicorn factory).
Returns an Application that implements the ASGI protocol. The
heavy async bootstrap (config loading, provider lifecycle) runs
automatically on the first ASGI event — not at import time.
Source code in src/arvel/foundation/application.py
164 165 166 167 168 169 170 171 172 173 174 175 176 177 | |
create(base_path='.', *, testing=False)
async
classmethod
¶
Create and bootstrap an Arvel application eagerly (async).
Useful for tests or scripts where you need a fully-booted app
immediately. For arvel serve, prefer configure().
Source code in src/arvel/foundation/application.py
179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 | |
settings(settings_type)
¶
Return a typed settings slice loaded on this application.
Source code in src/arvel/foundation/application.py
201 202 203 204 205 206 207 208 | |
__call__(scope, receive, send)
async
¶
ASGI interface — lazy-boots on first call, then delegates to FastAPI.
Exceptions that reach FastAPI's exception handlers are logged once
there and converted into an HTTP response. Starlette's
ServerErrorMiddleware re-raises after sending that response,
which causes Uvicorn to log the same traceback a second time.
We absorb the re-raise here so every error produces exactly one
structured log entry.
Source code in src/arvel/foundation/application.py
210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 | |
AppSettings
¶
Bases: BaseSettings
Root application settings loaded from APP_* environment variables.
FastAPI metadata fields (description, version, summary, etc.) are forwarded
to the FastAPI() constructor at boot time. Set them via environment
variables (APP_DESCRIPTION, APP_VERSION, …) or in config/app.py.
Source code in src/arvel/app/config.py
28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 | |
ModuleSettings
¶
Bases: BaseSettings
Base for module-level config slices.
Subclasses set model_config with their own env_prefix so that DB_HOST maps to DatabaseSettings and CACHE_HOST maps to CacheSettings.
Source code in src/arvel/foundation/config.py
21 22 23 24 25 26 27 28 | |
Container
¶
Resolved DI container — provides typed dependency resolution.
Source code in src/arvel/foundation/container.py
111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 | |
has(interface)
¶
Check whether a binding exists without triggering resolution.
Source code in src/arvel/foundation/container.py
129 130 131 132 133 134 135 | |
instance(interface, value)
¶
Register a pre-built instance at runtime (post-boot).
Use sparingly — prefer ContainerBuilder.provide_factory during
register(). This exists for services assembled during boot()
that depend on other resolved services.
Source code in src/arvel/foundation/container.py
254 255 256 257 258 259 260 261 | |
enter_scope(scope)
¶
Create a child container with the given scope (O(1) — no dict copy).
Source code in src/arvel/foundation/container.py
263 264 265 266 | |
ContainerBuilder
¶
Collects bindings during the register phase.
Source code in src/arvel/foundation/container.py
68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 | |
Scope
¶
Bases: Enum
DI lifetime scopes.
APP — singleton for the application lifetime. REQUEST — fresh instance per HTTP request. SESSION — shared within a user session across requests.
Source code in src/arvel/foundation/container.py
28 29 30 31 32 33 34 35 36 37 38 | |
Pipeline
¶
Sends a passable through an ordered list of pipes.
Pipes can be: - Async callables matching the Pipe signature - Sync callables (adapted transparently) - Class references resolved through the DI container
The passable type is erased at the pipeline level (see ADR-001). Individual pipes cast the passable to their expected type at runtime.
Source code in src/arvel/foundation/pipeline.py
22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 | |
ServiceProvider
¶
Base for all module service providers.
Lower priority boots first. Framework: 0-20, user: 50 (default).
Source code in src/arvel/foundation/provider.py
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | |
configure(config)
¶
Capture loaded config so factories use .env values, not bare defaults.
Source code in src/arvel/foundation/provider.py
21 22 | |
register(container)
async
¶
Declare DI bindings. Don't resolve here — container isn't built yet.
Source code in src/arvel/foundation/provider.py
24 25 | |
boot(app)
async
¶
Late-stage wiring: routes, listeners, middleware, resolved deps.
Source code in src/arvel/foundation/provider.py
27 28 | |
shutdown(app)
async
¶
Release long-lived resources. Called in reverse provider order.
Source code in src/arvel/foundation/provider.py
30 31 | |
arvel.auth
¶
Auth module — login, JWT tokens, guards, password reset, OAuth2/OIDC, claims, and audit.
AuthSettings
¶
Bases: ModuleSettings
Auth module settings.
Source code in src/arvel/auth/config.py
32 33 34 35 36 37 38 39 40 41 42 | |
arvel.cache
¶
Cache contract and drivers — swappable caching with TTL support.
arvel.events
¶
Domain event bus — dispatch events, register sync/queued listeners, auto-discovery.
EventDispatcher
¶
In-process event dispatcher with sync and queued listener support.
Source code in src/arvel/events/dispatcher.py
24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | |
register(event_type, listener_class, *, priority=50)
¶
Register a listener class for an event type with optional priority.
Source code in src/arvel/events/dispatcher.py
30 31 32 33 34 35 36 37 38 39 40 | |
listeners_for(event_type)
¶
Return listener classes registered for an event type, sorted by priority.
Source code in src/arvel/events/dispatcher.py
42 43 44 | |
dispatch(event)
async
¶
Dispatch an event to all registered listeners.
Sync listeners execute inline. Queued listeners are skipped here (a real integration would dispatch them via QueueContract).
Source code in src/arvel/events/dispatcher.py
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | |
Event
¶
Bases: BaseModel
Base class for domain events.
Events are immutable Pydantic models. Subclass and add fields for
your domain-specific payload. Sensitive fields should use
exclude=True in their Field definition to prevent queue serialization.
Source code in src/arvel/events/event.py
10 11 12 13 14 15 16 17 18 19 20 | |
Listener
¶
Base class for event listeners.
Subclass and implement handle(self, event: YourEvent). The type
hint on event determines which event type this listener handles.
The dispatcher inspects the subclass's type hint at runtime to route
events, so Any on the base is intentional — subclasses narrow it.
Source code in src/arvel/events/listener.py
8 9 10 11 12 13 14 15 16 17 18 19 20 | |
queued(cls)
¶
Mark a listener for queue dispatch instead of sync execution.
Source code in src/arvel/events/listener.py
23 24 25 26 | |
arvel.queue
¶
Queue system — job dispatch, retries, middleware, chaining, batching.
Batch
¶
Dispatches jobs concurrently and fires a callback when all complete.
Unlike Chain, Batch doesn't stop on failure. It collects results and invokes the callback (if set) after all jobs finish.
Source code in src/arvel/queue/batch.py
26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | |
then(callback)
¶
Set a job to run after all batch jobs complete.
Source code in src/arvel/queue/batch.py
37 38 39 40 | |
dispatch(queue)
async
¶
Execute all jobs via the given queue driver and fire callback.
Source code in src/arvel/queue/batch.py
42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | |
Chain
¶
Dispatches jobs in sequence, halting on permanent failure.
Each job runs after the previous one succeeds. If any job fails (after exhausting retries), the chain stops and the error propagates.
Source code in src/arvel/queue/chain.py
12 13 14 15 16 17 18 19 20 21 22 23 24 25 | |
dispatch(queue)
async
¶
Execute all jobs in order via the given queue driver.
Source code in src/arvel/queue/chain.py
22 23 24 25 | |
QueueSettings
¶
Bases: ModuleSettings
Configuration for the queue subsystem.
Env vars are prefixed with QUEUE_:
- QUEUE_DRIVER — which driver to use
- QUEUE_DEFAULT — default queue name
- QUEUE_REDIS_URL — Redis connection (Taskiq-Redis)
- QUEUE_TASKIQ_BROKER — Taskiq broker backend
- QUEUE_TASKIQ_URL — Taskiq broker URL (falls back to redis_url for redis)
Source code in src/arvel/queue/config.py
10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | |
QueueContract
¶
Bases: ABC
Abstract base class for queue drivers.
Implementations: SyncQueue (testing), NullQueue (dry-run), TaskiqQueue (multi-broker via Taskiq).
Source code in src/arvel/queue/contracts.py
15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | |
dispatch(job)
abstractmethod
async
¶
Enqueue a job for immediate processing.
Source code in src/arvel/queue/contracts.py
22 23 24 | |
later(delay, job)
abstractmethod
async
¶
Enqueue a job for processing after a delay.
Source code in src/arvel/queue/contracts.py
26 27 28 | |
bulk(jobs)
abstractmethod
async
¶
Enqueue multiple jobs at once.
Source code in src/arvel/queue/contracts.py
30 31 32 | |
size(queue_name='default')
abstractmethod
async
¶
Return the number of pending jobs in the given queue.
Source code in src/arvel/queue/contracts.py
34 35 36 | |
Job
¶
Bases: BaseModel
Base class for background jobs.
Subclass and implement handle(). Configure retries, backoff,
timeout, and middleware by overriding the class attributes or
the middleware() method.
Backoff accepts three forms:
- int: fixed delay in seconds between every retry
- list[int]: per-attempt delays (last value repeats for overflow)
- "exponential": uses backoff_base ** attempt seconds
Source code in src/arvel/queue/job.py
10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | |
handle()
async
¶
Execute the job's work. Override in subclasses.
Source code in src/arvel/queue/job.py
37 38 39 | |
on_failure(error)
async
¶
Called when the job fails permanently (retries exhausted).
Source code in src/arvel/queue/job.py
41 42 | |
middleware()
¶
Return middleware instances to wrap this job's execution.
Source code in src/arvel/queue/job.py
44 45 46 | |
get_unique_id()
¶
Return the uniqueness key for this job. Override for custom keys.
Source code in src/arvel/queue/job.py
48 49 50 | |
QueueManager
¶
Resolves the configured :class:QueueContract implementation.
Uses QueueSettings.driver to pick from built-in drivers
(sync, null, taskiq) or custom-registered ones.
Source code in src/arvel/queue/manager.py
29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | |
register_driver(name, factory)
¶
Register a custom driver factory by name.
Source code in src/arvel/queue/manager.py
39 40 41 | |
create_driver(settings=None)
¶
Build and return the queue driver specified by settings.
Source code in src/arvel/queue/manager.py
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | |
JobMiddleware
¶
Base class for job middleware.
Subclass and implement handle(). Call await next_call(job)
to continue the chain, or return without calling to skip execution.
Source code in src/arvel/queue/middleware.py
20 21 22 23 24 25 26 27 28 | |
RateLimited
¶
Bases: JobMiddleware
Limits job execution to max_attempts within decay_seconds.
Uses a LockContract-based counter. When the limit is exceeded, the job is silently skipped (released back for later processing).
Source code in src/arvel/queue/middleware.py
31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | |
WithoutOverlapping
¶
Bases: JobMiddleware
Prevents concurrent execution of jobs with the same key.
Uses a distributed lock. If the lock is already held, the job is silently skipped (released for later processing).
Source code in src/arvel/queue/middleware.py
71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 | |
UniqueJobGuard
¶
Dispatch-time guard that prevents duplicate job execution.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
lock
|
LockContract
|
A LockContract implementation for distributed uniqueness. |
required |
Source code in src/arvel/queue/unique_job.py
22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | |
acquire(job)
async
¶
Try to acquire the uniqueness lock.
Returns True if the job should proceed (lock acquired or no uniqueness configured). Returns False if the job is a duplicate.
Source code in src/arvel/queue/unique_job.py
38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | |
release_for_processing(job)
async
¶
Release the uniqueness lock when unique_until_processing=True.
Called at the start of job processing so a new dispatch of the same job is allowed while this one runs.
Source code in src/arvel/queue/unique_job.py
58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | |
JobRunner
¶
Executes a job with retry logic, timeout, middleware pipeline, and context propagation.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
middleware_overrides
|
list[JobMiddleware] | None
|
If provided, these middleware run instead of |
None
|
Source code in src/arvel/queue/worker.py
25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 | |
execute(job, *, context=None)
async
¶
Run job through the middleware pipeline, then execute with retry/timeout.
When context is provided, it's hydrated before handle() and flushed after.
Source code in src/arvel/queue/worker.py
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | |
arvel.validation
¶
Arvel validation layer — form requests, rules, and validators.
AuthorizationFailedError
¶
Bases: Exception
Raised when form request authorization fails.
Source code in src/arvel/validation/exceptions.py
56 57 58 59 60 61 | |
FieldError
¶
Single field validation error.
Source code in src/arvel/validation/exceptions.py
23 24 25 26 27 28 29 30 31 32 33 34 | |
FieldErrorDict
¶
Bases: TypedDict
Serialized representation of a single field validation error.
Source code in src/arvel/validation/exceptions.py
8 9 10 11 12 13 | |
ValidationError
¶
Bases: Exception
Raised when validation fails.
Source code in src/arvel/validation/exceptions.py
37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | |
ValidationErrorDict
¶
Bases: TypedDict
Serialized representation of a validation error response.
Source code in src/arvel/validation/exceptions.py
16 17 18 19 20 | |
FormRequest
¶
Base class for form request objects.
Subclasses override authorize(), rules(), and optionally
messages() and after_validation().
Source code in src/arvel/validation/form_request.py
16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | |
authorize(request)
¶
Return True if the request is authorized, False otherwise.
Source code in src/arvel/validation/form_request.py
23 24 25 | |
rules()
¶
Return validation rules keyed by field name.
Source code in src/arvel/validation/form_request.py
27 28 29 | |
messages()
¶
Return custom error messages keyed by 'field.RuleName'.
Source code in src/arvel/validation/form_request.py
31 32 33 | |
after_validation(data)
¶
Post-validation hook. Transform or enrich the validated data.
Source code in src/arvel/validation/form_request.py
35 36 37 | |
validate_request(*, request, data)
async
¶
Run authorization, validation, and after-hook in order.
Source code in src/arvel/validation/form_request.py
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | |
AsyncRule
¶
Bases: Protocol
Async validation rule (for DB queries, external calls).
Source code in src/arvel/validation/rule.py
16 17 18 19 20 21 | |
Rule
¶
Bases: Protocol
Synchronous validation rule.
Source code in src/arvel/validation/rule.py
8 9 10 11 12 13 | |
ConditionalRule
¶
Base marker for conditional rules.
Conditional rules expose condition_met(data) to signal whether
the field's validation should proceed. When the condition is not met,
the Validator skips all remaining rules on the field (short-circuit).
Source code in src/arvel/validation/rules/conditional.py
12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | |
ProhibitedIf
¶
Bases: ConditionalRule
Field must not be present when another field equals a specific value.
Source code in src/arvel/validation/rules/conditional.py
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 | |
RequiredIf
¶
Bases: ConditionalRule
Field is required when another field equals a specific value.
Source code in src/arvel/validation/rules/conditional.py
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | |
RequiredUnless
¶
Bases: ConditionalRule
Field is required unless another field equals a specific value.
Source code in src/arvel/validation/rules/conditional.py
47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | |
RequiredWith
¶
Bases: ConditionalRule
Field is required when another field is present.
Source code in src/arvel/validation/rules/conditional.py
64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | |
Exists
¶
Check that a value exists in a database table.
Source code in src/arvel/validation/rules/database.py
68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 | |
Unique
¶
Check that a value doesn't already exist in a database table.
Optionally ignores a specific row (for update scenarios).
Source code in src/arvel/validation/rules/database.py
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | |
Validator
¶
Run validation rules against data, collecting all errors.
Source code in src/arvel/validation/validator.py
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 | |
arvel.testing
¶
Testing support — TestClient, DatabaseTestCase, ModelFactory, fakes.
All contract test doubles (fakes) are re-exported here so users can write::
from arvel.testing import CacheFake, MailFake
TestResponse
¶
Wraps httpx.Response with fluent assertion methods.
Every assert_* method returns self so assertions can be chained::
response.assert_status(200).assert_json_path("data.name", "Alice")
Source code in src/arvel/testing/assertions.py
35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 | |
TestClient
¶
Source code in src/arvel/testing/client.py
26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 | |
__test__ = False
class-attribute
instance-attribute
¶
Test client for Arvel apps.
Wraps httpx.AsyncClient with ASGITransport so requests go through
the full middleware pipeline. Supports acting_as() for injecting auth
context into subsequent requests.
Usage::
async with TestClient(app) as client:
client.acting_as(user_id=42, headers={"Authorization": "Bearer tok"})
resp = await client.get("/me")
acting_as(*, user_id=None, headers=None)
¶
Inject auth headers for subsequent requests.
Any headers passed here persist for the lifetime of this client session.
Source code in src/arvel/testing/client.py
60 61 62 63 64 65 66 67 68 69 70 71 72 73 | |
DatabaseTestCase
¶
Test helper providing a database session with rollback isolation.
Wraps an existing AsyncSession (typically from a fixture) and provides
convenience methods for seeding data and asserting database state.
Usage::
@pytest.fixture
async def db(db_session):
return DatabaseTestCase(db_session)
async def test_something(db):
await db.seed([User(name="Alice")])
await db.assert_database_has("users", {"name": "Alice"})
Source code in src/arvel/testing/database.py
17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | |
seed(models)
async
¶
Add multiple model instances to the session and flush.
Source code in src/arvel/testing/database.py
41 42 43 44 45 | |
refresh(instance)
async
¶
Refresh a model instance from the database.
Source code in src/arvel/testing/database.py
47 48 49 | |
assert_database_has(table, conditions)
async
¶
Assert at least one row matching conditions exists in table.
Source code in src/arvel/testing/database.py
51 52 53 54 55 56 57 58 59 | |
assert_database_missing(table, conditions)
async
¶
Assert no row matching conditions exists in table.
Source code in src/arvel/testing/database.py
61 62 63 64 65 66 67 68 69 | |
assert_database_count(table, expected)
async
¶
Assert the total number of rows in table equals expected.
Source code in src/arvel/testing/database.py
71 72 73 74 75 76 77 78 | |
assert_soft_deleted(instance)
¶
Assert instance has a non-null deleted_at timestamp.
Source code in src/arvel/testing/database.py
80 81 82 83 84 85 86 | |
FactoryBuilder
¶
Immutable builder returned by ModelFactory.state() for chained creation.
Source code in src/arvel/testing/factory.py
34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | |
ModelFactory
¶
Base factory for generating ArvelModel instances.
make() returns in-memory instances, create() persists to
the database, batch() / make_batch() produce multiple.
Sequence-based uniqueness: call _next_seq() inside defaults()
to get an auto-incrementing integer unique per factory class.
Source code in src/arvel/testing/factory.py
58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 | |
defaults()
classmethod
¶
Override in subclasses to provide default field values.
Use cls._next_seq() for fields that require uniqueness::
@classmethod
def defaults(cls) -> dict[str, Any]:
seq = cls._next_seq()
return {"name": f"User {seq}", "email": f"user{seq}@test.com"}
Source code in src/arvel/testing/factory.py
82 83 84 85 86 87 88 89 90 91 92 93 | |
state(name)
classmethod
¶
Return a builder pre-loaded with the named state's overrides.
States are defined as state_<name>() class methods that return dicts::
@classmethod
def state_admin(cls) -> dict[str, Any]:
return {"role": "admin"}
Source code in src/arvel/testing/factory.py
95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 | |
make(**overrides)
classmethod
¶
Create an in-memory model instance (not persisted).
Source code in src/arvel/testing/factory.py
112 113 114 115 116 | |
create(*, session, **overrides)
async
classmethod
¶
Create and persist a model instance to the database.
Source code in src/arvel/testing/factory.py
118 119 120 121 122 123 124 125 | |
make_batch(count, **overrides)
classmethod
¶
Create multiple in-memory instances.
Source code in src/arvel/testing/factory.py
127 128 129 130 | |
batch(count, *, session, **overrides)
async
classmethod
¶
Create and persist multiple instances.
Source code in src/arvel/testing/factory.py
132 133 134 135 136 137 138 139 | |
create_many(count, *, session, **overrides)
async
classmethod
¶
Alias for batch() — creates and persists count instances.
Source code in src/arvel/testing/factory.py
141 142 143 144 | |
BroadcastFake
¶
Bases: BroadcastContract
Captures all broadcasts for test assertions.
Use in tests to replace the real broadcaster and verify that events were broadcast to the correct channels.
Source code in src/arvel/broadcasting/fake.py
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | |
assert_broadcast(event_name, *, channel=None)
¶
Assert that an event with event_name was broadcast.
Optionally filter by channel name.
Source code in src/arvel/broadcasting/fake.py
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | |
assert_broadcast_on(channel)
¶
Assert that any event was broadcast to channel.
Source code in src/arvel/broadcasting/fake.py
59 60 61 62 63 64 65 | |
assert_nothing_broadcast()
¶
Assert that no events were broadcast.
Source code in src/arvel/broadcasting/fake.py
67 68 69 70 71 72 | |
assert_broadcast_count(event_name, expected)
¶
Assert that event_name was broadcast exactly expected times.
Source code in src/arvel/broadcasting/fake.py
74 75 76 77 78 79 | |
CacheFake
¶
Bases: MemoryCache
Extends MemoryCache with assertion helpers for tests.
Source code in src/arvel/cache/fakes.py
10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | |
EventFake
¶
Captures dispatched events for test assertions.
Use in tests to replace the real EventDispatcher and verify that events were dispatched without executing listeners.
Source code in src/arvel/events/fake.py
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | |
LockFake
¶
Bases: MemoryLock
Extends MemoryLock with assertion helpers for tests.
Source code in src/arvel/lock/fakes.py
8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | |
MailFake
¶
Bases: MailContract
Captures all sent mailables for test assertions.
Source code in src/arvel/mail/fakes.py
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | |
MediaFake
¶
Bases: MediaContract
In-memory media library for tests with validation, events, and assertion helpers.
Source code in src/arvel/media/fakes.py
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 | |
NotificationFake
¶
Bases: NotificationContract
Captures all sent notifications for test assertions.
Source code in src/arvel/notifications/fakes.py
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | |
QueueFake
¶
Bases: QueueContract
Captures dispatched jobs for test assertions.
Use in tests to replace the real queue driver and verify that jobs were dispatched with expected payloads.
Source code in src/arvel/queue/fake.py
16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | |
StorageFake
¶
Bases: StorageContract
In-memory storage for tests with assertion helpers.
Source code in src/arvel/storage/fakes.py
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | |