registers 2.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. registers-2.1.0/LICENSE +6 -0
  2. registers-2.1.0/PKG-INFO +646 -0
  3. registers-2.1.0/README.md +621 -0
  4. registers-2.1.0/pyproject.toml +53 -0
  5. registers-2.1.0/setup.cfg +4 -0
  6. registers-2.1.0/src/registers/__init__.py +2 -0
  7. registers-2.1.0/src/registers/cli/__init__.py +53 -0
  8. registers-2.1.0/src/registers/cli/container.py +66 -0
  9. registers-2.1.0/src/registers/cli/dispatcher.py +106 -0
  10. registers-2.1.0/src/registers/cli/exceptions.py +44 -0
  11. registers-2.1.0/src/registers/cli/middleware.py +83 -0
  12. registers-2.1.0/src/registers/cli/parser.py +176 -0
  13. registers-2.1.0/src/registers/cli/plugins.py +75 -0
  14. registers-2.1.0/src/registers/cli/registry.py +246 -0
  15. registers-2.1.0/src/registers/cli/utils/reflection.py +67 -0
  16. registers-2.1.0/src/registers/cli/utils/typing.py +64 -0
  17. registers-2.1.0/src/registers/db/__init__.py +93 -0
  18. registers-2.1.0/src/registers/db/decorators.py +297 -0
  19. registers-2.1.0/src/registers/db/engine.py +94 -0
  20. registers-2.1.0/src/registers/db/exceptions.py +68 -0
  21. registers-2.1.0/src/registers/db/fields.py +21 -0
  22. registers-2.1.0/src/registers/db/metadata.py +99 -0
  23. registers-2.1.0/src/registers/db/registry.py +626 -0
  24. registers-2.1.0/src/registers/db/relations.py +299 -0
  25. registers-2.1.0/src/registers/db/schema.py +191 -0
  26. registers-2.1.0/src/registers/db/security.py +61 -0
  27. registers-2.1.0/src/registers/db/typing_utils.py +138 -0
  28. registers-2.1.0/src/registers.egg-info/PKG-INFO +646 -0
  29. registers-2.1.0/src/registers.egg-info/SOURCES.txt +35 -0
  30. registers-2.1.0/src/registers.egg-info/dependency_links.txt +1 -0
  31. registers-2.1.0/src/registers.egg-info/requires.txt +7 -0
  32. registers-2.1.0/src/registers.egg-info/top_level.txt +1 -0
  33. registers-2.1.0/tests/test_cli_registry.py +321 -0
  34. registers-2.1.0/tests/test_cli_registry_edge_cases.py +396 -0
  35. registers-2.1.0/tests/test_db_registry.py +963 -0
  36. registers-2.1.0/tests/test_db_registry_edge_cases.py +280 -0
  37. registers-2.1.0/tests/test_db_registry_fastapi_integration.py +223 -0
@@ -0,0 +1,6 @@
1
+ ## LICENSE
2
+ Copyright (c) [2026] [Charles DeFreese]
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
5
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
6
+
@@ -0,0 +1,646 @@
1
+ Metadata-Version: 2.4
2
+ Name: registers
3
+ Version: 2.1.0
4
+ Summary: Decorator-driven persistence registry for Pydantic models and CLI tooling
5
+ Author: Charles DeFreese
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/nexustech101/registers
8
+ Project-URL: Repository, https://github.com/nexustech101/registers
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Requires-Python: >=3.10
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: pydantic>=2.0
19
+ Requires-Dist: sqlalchemy>=2.0
20
+ Provides-Extra: dev
21
+ Requires-Dist: pytest>=7.4; extra == "dev"
22
+ Requires-Dist: pytest-cov>=4.1; extra == "dev"
23
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
24
+ Dynamic: license-file
25
+
26
+
27
+ <div align="center">
28
+
29
+ # Registers Python Framework
30
+
31
+ **Decorator-driven CLI tooling and database persistence for Python.**
32
+
33
+ [![PyPI Version](https://img.shields.io/pypi/v/registers?color=4A90D9&label=pypi)](https://pypi.org/project/registers/)
34
+ [![Python Versions](https://img.shields.io/pypi/pyversions/registers)](https://pypi.org/project/registers/)
35
+ [![CI](https://img.shields.io/github/actions/workflow/status/nexustech101/registers/publish.yml?label=ci)](https://github.com/nexustech101/registers/actions)
36
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green)](https://github.com/nexustech101/registers/blob/main/LICENSE)
37
+
38
+ [CLI Framework](#registerscli) · [Database Registry](#registersdb) · [FastAPI Integration](#fastapi-integration) · [Error Reference](#error-reference)
39
+
40
+ </div>
41
+ <br>
42
+
43
+ A Python framework built with **Developer Experience (DX)** in mind. `registers` uses a clean, ergonomic decorator registry design pattern to eliminate boilerplate when building CLI tools and database-backed applications — from lightweight scripts and data engineering pipelines to full ecommerce systems and enterprise-scale relational models.
44
+
45
+ Designed to integrate seamlessly with **FastAPI** and other ASGI/WSGI frameworks out of the box.
46
+
47
+ ```
48
+ pip install registers
49
+ ```
50
+
51
+ ---
52
+
53
+ ## Contents
54
+
55
+ - [Why registers?](#why-registers)
56
+ - [Packages](#packages)
57
+ - [registers.cli — CLI Framework](#registerscli)
58
+ - [Quick Start](#cli-quick-start)
59
+ - [Argument Types](#argument-types)
60
+ - [Command Aliases](#command-aliases)
61
+ - [Dependency Injection](#dependency-injection)
62
+ - [Middleware](#middleware)
63
+ - [Plugin System](#plugin-system)
64
+ - [Error Handling](#error-handling)
65
+ - [registers.db — Database Registry](#registersdb)
66
+ - [Quick Start](#db-quick-start)
67
+ - [CRUD API](#crud-api)
68
+ - [Querying](#querying)
69
+ - [Schema Management](#schema-management)
70
+ - [Relationships](#relationships)
71
+ - [FastAPI Integration](#fastapi-integration)
72
+ - [Installation](#installation)
73
+ - [Requirements](#requirements)
74
+
75
+ ---
76
+
77
+ ## Why registers?
78
+
79
+ Most Python projects involve some combination of two recurring problems: **wiring up CLI commands** and **persisting data models**. The standard solutions — `argparse`, raw SQLAlchemy, `click` — are powerful but verbose. You spend more time writing plumbing than writing logic.
80
+
81
+ `registers` solves both with a consistent design philosophy: **register once, use everywhere**. A single decorator on a function makes it a CLI command. A single decorator on a Pydantic model gives it a full persistence layer. The framework handles the wiring; you write the behaviour.
82
+
83
+ **It is particularly well-suited for:**
84
+
85
+ - **Data engineering and modeling** — define typed, validated models with automatic table creation and schema evolution
86
+ - **Ecommerce and multi-entity systems** — first-class relationship descriptors for `HasMany`, `BelongsTo`, and `HasManyThrough`
87
+ - **Enterprise relational schemas** — transaction support, upsert semantics, and unique constraint management
88
+ - **FastAPI services** — models attach `create_schema` / `drop_schema` / `schema_exists` class methods that slot directly into FastAPI's `lifespan` startup hooks
89
+ - **Rapid CLI tooling** — go from a plain Python function to a fully-parsed, aliased, DI-wired CLI command in one decorator
90
+
91
+ ---
92
+
93
+ ## Packages
94
+
95
+ | Package | Purpose |
96
+ |---|---|
97
+ | `registers.cli` | Decorator-based CLI framework with argparse, DI, middleware, and plugin loading |
98
+ | `registers.db` | SQLAlchemy-backed persistence manager for Pydantic models |
99
+
100
+ Both packages are independent. Use one, the other, or both together.
101
+
102
+ ---
103
+
104
+ ## registers.cli
105
+
106
+ A lightweight, decorator-driven CLI framework. Register Python functions as subcommands — argument parsing, type coercion, alias resolution, dependency injection, and middleware hooks are all handled by the framework.
107
+
108
+ ### CLI Quick Start
109
+
110
+ ```python
111
+ from registers.cli import CommandRegistry
112
+
113
+ cli = CommandRegistry()
114
+
115
+
116
+ @cli.register(
117
+ ops=["-g", "--greet"],
118
+ name="greet",
119
+ description="Greet someone by name",
120
+ )
121
+ def greet(name: str) -> str:
122
+ return f"Hello, {name}!"
123
+
124
+
125
+ if __name__ == "__main__":
126
+ cli.run()
127
+ ```
128
+
129
+ ```bash
130
+ python app.py greet Alice
131
+ python app.py --greet Alice
132
+ python app.py g Alice
133
+ # → Hello, Alice!
134
+ ```
135
+
136
+ ### Argument Types
137
+
138
+ Argument behaviour is inferred directly from Python type annotations — no schema definitions, no `add_argument` calls.
139
+
140
+ | Annotation | CLI behaviour |
141
+ |---|---|
142
+ | `str` | Required positional argument |
143
+ | `int` | Required positional integer (auto-coerced) |
144
+ | `float` | Required positional float (auto-coerced) |
145
+ | `bool` | Optional `--flag` (store_true) |
146
+ | `Optional[T]` | Optional `--arg value` |
147
+ | Defaulted parameter | Optional `--arg value` |
148
+
149
+ ```python
150
+ @cli.register(name="create-report", description="Generate a report")
151
+ def create_report(
152
+ title: str, # required positional
153
+ pages: int, # required positional, coerced to int
154
+ verbose: bool = False, # optional --verbose flag
155
+ output: Optional[str] = None, # optional --output path
156
+ ) -> str:
157
+ ...
158
+ ```
159
+
160
+ ```bash
161
+ python app.py create-report "Q3 Summary" 12 --verbose --output ./reports
162
+ ```
163
+
164
+ ### Command Aliases
165
+
166
+ The `ops` field registers shorthand and flag-style aliases alongside the canonical command name. All three forms are resolved automatically:
167
+
168
+ ```python
169
+ @cli.register(
170
+ ops=["-s", "--sync"],
171
+ name="sync",
172
+ description="Sync the database",
173
+ )
174
+ def sync(target: str) -> None:
175
+ ...
176
+ ```
177
+
178
+ ```bash
179
+ python app.py sync production
180
+ python app.py --sync production
181
+ python app.py s production
182
+ ```
183
+
184
+ ### Dependency Injection
185
+
186
+ Use `DIContainer` to bind service instances to types. Any command parameter whose type is registered in the container is injected automatically and hidden from the CLI — callers never need to pass it.
187
+
188
+ ```python
189
+ from registers.cli import CommandRegistry, DIContainer, Dispatcher, build_parser
190
+
191
+ registry = CommandRegistry()
192
+ container = DIContainer()
193
+
194
+ container.register(DatabaseService, DatabaseService(url="sqlite:///app.db"))
195
+
196
+
197
+ @registry.register(name="seed", description="Seed the database")
198
+ def seed(count: int, db: DatabaseService) -> str:
199
+ db.insert_fixtures(count)
200
+ return f"Seeded {count} records."
201
+
202
+
203
+ parser = build_parser(registry, container)
204
+ dispatcher = Dispatcher(registry, container)
205
+ args = parser.parse_args()
206
+
207
+ if args.command:
208
+ cli_args = {k: v for k, v in vars(args).items() if k != "command"}
209
+ dispatcher.dispatch(args.command, cli_args)
210
+ ```
211
+
212
+ ```bash
213
+ python app.py seed 100 # `db` is injected; only `count` appears on the CLI
214
+ ```
215
+
216
+ ### Middleware
217
+
218
+ `MiddlewareChain` provides ordered pre- and post-execution hooks. Pre-hooks receive the command name and resolved kwargs; post-hooks receive the command name and return value.
219
+
220
+ ```python
221
+ from registers.cli import CommandRegistry, MiddlewareChain, logging_middleware_pre, logging_middleware_post
222
+
223
+ cli = CommandRegistry()
224
+ chain = MiddlewareChain()
225
+
226
+ chain.add_pre(logging_middleware_pre) # built-in: logs command + args, starts timer
227
+ chain.add_post(logging_middleware_post) # built-in: logs completion + elapsed time
228
+
229
+ # Custom hook
230
+ def audit_hook(command: str, result: Any) -> None:
231
+ audit_log.write(command, result)
232
+
233
+ chain.add_post(audit_hook)
234
+
235
+ cli.run(middleware=chain)
236
+ ```
237
+
238
+ ### Plugin System
239
+
240
+ `load_plugins` dynamically imports every non-private module in a package. Any `@registry.register(...)` calls at module level execute on import — no manual wiring in `main.py` required.
241
+
242
+ ```python
243
+ from registers.cli import CommandRegistry, load_plugins
244
+
245
+ cli = CommandRegistry()
246
+ load_plugins("app.commands", cli) # auto-discovers app/commands/*.py
247
+
248
+ if __name__ == "__main__":
249
+ cli.run()
250
+ ```
251
+
252
+ ```
253
+ app/
254
+ commands/
255
+ users.py # @cli.register(name="create-user", ...)
256
+ reports.py # @cli.register(name="export", ...)
257
+ deploy.py # @cli.register(name="deploy", ...)
258
+ ```
259
+
260
+ ### Error Handling
261
+
262
+ The framework does not impose an error handling policy. A clean pattern is to wrap command handlers with your own decorator:
263
+
264
+ ```python
265
+ import functools, sys
266
+ from typing import Any, Callable
267
+
268
+
269
+ def handle_errors(func: Callable) -> Callable:
270
+ @functools.wraps(func)
271
+ def wrapper(*args, **kwargs) -> Any:
272
+ try:
273
+ return func(*args, **kwargs)
274
+ except KeyboardInterrupt:
275
+ print("\nInterrupted.", file=sys.stderr)
276
+ sys.exit(0)
277
+ except Exception as exc:
278
+ print(f"Error: {exc}", file=sys.stderr)
279
+ sys.exit(1)
280
+ return wrapper
281
+
282
+
283
+ @cli.register(name="deploy", description="Deploy to an environment")
284
+ @handle_errors
285
+ def deploy(env: str) -> str:
286
+ ...
287
+ ```
288
+
289
+ ---
290
+
291
+ ## registers.db
292
+
293
+ A SQLAlchemy-backed persistence manager for Pydantic models. One decorator gives your model a full CRUD interface, automatic table creation, schema evolution helpers, and opt-in relationship descriptors — with no separate repository classes, no manual session management, and no raw SQL.
294
+
295
+ ### DB Quick Start
296
+
297
+ ```python
298
+ from pydantic import BaseModel
299
+ from registers.db import database_registry
300
+
301
+
302
+ @database_registry(
303
+ "app.db",
304
+ table_name="users",
305
+ key_field="id",
306
+ autoincrement=True,
307
+ unique_fields=["email"],
308
+ )
309
+ class User(BaseModel):
310
+ id: int | None = None
311
+ name: str
312
+ email: str
313
+
314
+
315
+ # Create
316
+ user = User.objects.create(name="Alice", email="alice@example.com")
317
+
318
+ # Read
319
+ user = User.objects.get(1)
320
+ user = User.objects.get(email="alice@example.com")
321
+
322
+ # Update
323
+ user.name = "Alicia"
324
+ user.save()
325
+
326
+ # Delete
327
+ user.delete()
328
+ ```
329
+
330
+ Primary-key conventions:
331
+
332
+ - `id: int | None = None` gives the model a database-managed autoincrement primary key.
333
+ - `id: int` is treated as a manual primary key and must be supplied explicitly.
334
+ - `create(id=...)` is rejected for database-managed keys.
335
+ - Persisted primary keys are immutable once the record exists.
336
+
337
+ ### CRUD API
338
+
339
+ All write operations live on the manager (`Model.objects`). Three instance methods — `save()`, `delete()`, and `refresh()` — are injected directly onto model instances for convenience.
340
+
341
+ #### Manager operations
342
+
343
+ ```python
344
+ # Strict insert — raises DuplicateKeyError on collision
345
+ user = User.objects.create(name="Bob", email="bob@example.com")
346
+
347
+ # Alias for callers who want explicit strict-insert wording
348
+ user = User.objects.strict_create(name="Bob", email="bob@example.com")
349
+
350
+ # Atomic upsert — INSERT … ON CONFLICT DO UPDATE, no race conditions
351
+ user = User.objects.upsert(id=1, name="Bob", email="bob@example.com")
352
+
353
+ # If no primary key is supplied, upsert falls back to configured unique fields
354
+ user = User.objects.upsert(name="Bob", email="bob@example.com")
355
+
356
+ # Bulk field update — returns refreshed records
357
+ updated = User.objects.update_where({"role": "trial"}, role="active")
358
+
359
+ # Delete by primary key
360
+ User.objects.delete(user_id)
361
+
362
+ # Delete by criteria — returns row count
363
+ count = User.objects.delete_where(role="inactive")
364
+ ```
365
+
366
+ #### Instance operations
367
+
368
+ ```python
369
+ # Upsert this instance
370
+ user.save()
371
+
372
+ # Persisted primary keys are immutable
373
+ # user.id = 999
374
+ # user.save() # -> ImmutableFieldError
375
+
376
+ # Delete this instance's row
377
+ user.delete()
378
+
379
+ # Re-fetch from the database (raises RecordNotFoundError if gone)
380
+ fresh = user.refresh()
381
+ ```
382
+
383
+ ### Querying
384
+
385
+ ```python
386
+ # All rows
387
+ users = User.objects.all()
388
+
389
+ # Filter with equality criteria
390
+ admins = User.objects.filter(role="admin")
391
+
392
+ # Filter values are validated against the declared field types
393
+ # User.objects.filter(role=123) # -> InvalidQueryError if the type is invalid
394
+
395
+ # Pagination
396
+ page = User.objects.filter(role="active", limit=20, offset=40)
397
+
398
+ # First / last
399
+ newest = User.objects.last()
400
+ first_trial = User.objects.first(role="trial")
401
+
402
+ # Get one or None
403
+ user = User.objects.get(1)
404
+ user = User.objects.get(email="alice@example.com")
405
+
406
+ # Get or raise RecordNotFoundError
407
+ user = User.objects.require(1)
408
+
409
+ # Existence and count
410
+ exists = User.objects.exists(email="alice@example.com")
411
+ total = User.objects.count(role="active")
412
+ ```
413
+
414
+ ### Schema Management
415
+
416
+ Table creation happens automatically on decoration (`auto_create=True` by default). Schema helpers are accessible as both class methods and via the manager:
417
+
418
+ ```python
419
+ # Idempotent table creation
420
+ User.create_schema() # or User.objects.create_schema()
421
+
422
+ # Inspection
423
+ User.schema_exists() # or User.objects.schema_exists()
424
+
425
+ # Destructive operations
426
+ User.truncate() # delete all rows, keep schema
427
+ User.drop_schema() # drop the table entirely
428
+
429
+ # Additive column evolution (no migration framework required)
430
+ User.objects.add_column("verified_at", Optional[datetime])
431
+ User.objects.ensure_column("verified_at", Optional[datetime]) # idempotent
432
+
433
+ # Explicit transaction for batched atomicity
434
+ with User.objects.transaction() as conn:
435
+ User.objects.create(name="Alice", email="alice@example.com")
436
+ Profile.objects.create(user_id=1, bio="...")
437
+ ```
438
+
439
+ ### Relationships
440
+
441
+ Relationships are lazy-loaded, read-only descriptors assigned after class decoration. This pattern avoids conflicts with Pydantic's metaclass and naturally resolves forward-reference ordering.
442
+
443
+ ```python
444
+ from registers.db import database_registry
445
+ from registers.db.relations import HasMany, BelongsTo, HasManyThrough
446
+
447
+
448
+ @database_registry("store.db", table_name="authors", key_field="id", autoincrement=True)
449
+ class Author(BaseModel):
450
+ id: int | None = None
451
+ name: str
452
+
453
+
454
+ @database_registry("store.db", table_name="posts", key_field="id", autoincrement=True)
455
+ class Post(BaseModel):
456
+ id: int | None = None
457
+ author_id: int
458
+ title: str
459
+
460
+
461
+ @database_registry("store.db", table_name="post_tags", key_field="id", autoincrement=True)
462
+ class PostTag(BaseModel):
463
+ id: int | None = None
464
+ post_id: int
465
+ tag_id: int
466
+
467
+
468
+ @database_registry("store.db", table_name="tags", key_field="id", autoincrement=True)
469
+ class Tag(BaseModel):
470
+ id: int | None = None
471
+ name: str
472
+
473
+
474
+ # Optionally declare relationships after all classes are decorated (not required)
475
+ Author.posts = HasMany(Post, foreign_key="author_id")
476
+ Post.author = BelongsTo(Author, local_key="author_id")
477
+ Post.tags = HasManyThrough(Tag, through=PostTag, source_key="post_id", target_key="tag_id")
478
+ ```
479
+
480
+ ```python
481
+ author = Author.objects.require(1)
482
+ author.posts # → list[Post]
483
+
484
+ post = Post.objects.require(1)
485
+ post.author # → Author | None
486
+ post.tags # → list[Tag]
487
+ ```
488
+
489
+ | Descriptor | Relationship | Example |
490
+ |---|---|---|
491
+ | `HasMany` | One-to-many | `Author → Posts` |
492
+ | `BelongsTo` | Many-to-one | `Post → Author` |
493
+ | `HasManyThrough` | Many-to-many via join table | `Post ↔ Tags` |
494
+
495
+ ### FastAPI Integration
496
+
497
+ `registers.db` integrates cleanly with FastAPI's `lifespan` pattern for schema initialization and engine disposal:
498
+
499
+ ```python
500
+ import logging
501
+ from contextlib import asynccontextmanager
502
+ from fastapi import FastAPI
503
+ from models import User, Product, Order
504
+
505
+ def initialize_schemas():
506
+ """Create every table schema exactly once on app startup (idempotent)."""
507
+ logging.info(" Initializing ecommerce database schemas...")
508
+
509
+ models = [
510
+ User,
511
+ Product,
512
+ Order,
513
+ ]
514
+
515
+ for model in models:
516
+ try:
517
+ # The Production Spec guarantees these schema methods exist on the
518
+ # registry/manager attached to the model. We call them directly on
519
+ # the class (the most ergonomic pattern for FastAPI usage).
520
+ if not model.schema_exists():
521
+ model.create_schema()
522
+ logging.info(f"Schema created - {model.__name__}")
523
+ else:
524
+ logging.info(f"Schema already exists - {model.__name__}")
525
+ except AttributeError:
526
+ # Safety net in case the manager is attached under a different name
527
+ # (e.g. model.manager or model.registry). The core CRUD routes will
528
+ # still work.
529
+ logging.warning(
530
+ f"Schema methods not directly on {model.__name__}. "
531
+ "Manual schema creation may be required."
532
+ )
533
+ except Exception as exc: # catches SchemaError, etc.
534
+ logging.error(f"Failed to initialize {model.__name__}: {exc}")
535
+
536
+
537
+ def dispose_engines():
538
+ """Dispose all SQLAlchemy engines on app shutdown to close DB connections."""
539
+ logging.info("Disposing database engines...")
540
+
541
+ models = [
542
+ User,
543
+ Product,
544
+ Order,
545
+ ]
546
+
547
+ for model in models:
548
+ try:
549
+ if model.schema_exists():
550
+ model.drop_schema()
551
+ logging.info(f"Engine dropped → {model.__name__}")
552
+ else:
553
+ logging.info(f"Engine does not exist → {model.__name__}")
554
+ except Exception as exc: # catches SchemaError, etc.
555
+ logging.error(f"Failed to dispose {model.__name__}: {exc}")
556
+
557
+ def dispose_engines():
558
+ for model in [User, Product, Order]:
559
+ model.objects.dispose()
560
+
561
+
562
+ @asynccontextmanager
563
+ async def lifespan(app: FastAPI):
564
+ initialize_schemas()
565
+ yield
566
+ dispose_engines()
567
+
568
+
569
+ app = FastAPI(lifespan=lifespan)
570
+
571
+
572
+ @app.get("/users/{user_id}", response_model=User)
573
+ async def get_user(user_id: int):
574
+ return User.objects.require(user_id)
575
+
576
+
577
+ @app.post("/users/", response_model=User)
578
+ async def create_user(user: User):
579
+ return User.objects.create(**user.model_dump(exclude={"id"}))
580
+ ```
581
+
582
+ ---
583
+
584
+ ## Error Reference
585
+
586
+ ### registers.cli
587
+
588
+ | Exception | Raised when |
589
+ |---|---|
590
+ | `DuplicateCommandError` | A command name is registered more than once |
591
+ | `UnknownCommandError` | A requested command has no registered handler |
592
+ | `DependencyNotFoundError` | The DI container cannot resolve a required type |
593
+ | `PluginLoadError` | A plugin module fails to import |
594
+
595
+ ### registers.db
596
+
597
+ | Exception | Raised when |
598
+ |---|---|
599
+ | `ModelRegistrationError` | The decorated class is not a valid Pydantic `BaseModel` |
600
+ | `ConfigurationError` | Decorator options reference non-existent fields or are invalid |
601
+ | `DuplicateKeyError` | An `INSERT` collides with an existing primary key |
602
+ | `InvalidPrimaryKeyAssignmentError` | A database-managed primary key is assigned explicitly on create |
603
+ | `ImmutableFieldError` | A persisted primary key is mutated and then saved |
604
+ | `UniqueConstraintError` | An `INSERT` or `UPDATE` violates a `UNIQUE` constraint |
605
+ | `RecordNotFoundError` | `require()` finds no matching row |
606
+ | `InvalidQueryError` | Filter criteria reference unknown fields or are malformed |
607
+ | `SchemaError` | A DDL operation (`CREATE` / `DROP` / `ALTER`) fails |
608
+ | `MigrationError` | A schema evolution step cannot be applied |
609
+ | `RelationshipError` | A relationship descriptor is misconfigured or accessed before setup |
610
+
611
+ All exceptions inherit from `FrameworkError` (CLI) or `RegistryError` (DB) for broad catch-all handling.
612
+
613
+ ---
614
+
615
+ ## Installation
616
+
617
+ ```bash
618
+ pip install registers
619
+ ```
620
+
621
+ **Development install (with test dependencies):**
622
+
623
+ ```bash
624
+ pip install "registers[dev]"
625
+ ```
626
+
627
+ **From source:**
628
+
629
+ ```bash
630
+ git clone https://github.com/yourname/registers
631
+ pip install ./registers
632
+ ```
633
+
634
+ ---
635
+
636
+ ## Requirements
637
+
638
+ - Python ≥ 3.10
639
+ - pydantic ≥ 2.0
640
+ - sqlalchemy ≥ 2.0
641
+
642
+ ---
643
+
644
+ ## License
645
+
646
+ MIT