xtr-orm 3.0.0__tar.gz → 4.0.2__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.
- {xtr_orm-3.0.0 → xtr_orm-4.0.2}/LICENSE +4 -0
- xtr_orm-3.0.0/README.md → xtr_orm-4.0.2/PKG-INFO +127 -3
- xtr_orm-3.0.0/PKG-INFO → xtr_orm-4.0.2/README.md +88 -42
- {xtr_orm-3.0.0 → xtr_orm-4.0.2}/pyproject.toml +11 -11
- {xtr_orm-3.0.0 → xtr_orm-4.0.2}/pyproject.toml.orig +11 -11
- {xtr_orm-3.0.0 → xtr_orm-4.0.2}/src/xtr_orm/.agents/skills/xtr-orm/SKILL.md +146 -5
- {xtr_orm-3.0.0 → xtr_orm-4.0.2}/src/xtr_orm/.agents/skills/xtr-orm/references/commands.md +8 -3
- {xtr_orm-3.0.0 → xtr_orm-4.0.2}/src/xtr_orm/__init__.py +16 -1
- {xtr_orm-3.0.0 → xtr_orm-4.0.2}/src/xtr_orm/bundle/__init__.py +9 -1
- {xtr_orm-3.0.0 → xtr_orm-4.0.2}/src/xtr_orm/bundle/connection_config.py +32 -2
- {xtr_orm-3.0.0 → xtr_orm-4.0.2}/src/xtr_orm/bundle/orm_bundle.py +16 -8
- xtr_orm-4.0.2/src/xtr_orm/bundle/orm_mappings_pass.py +210 -0
- {xtr_orm-3.0.0 → xtr_orm-4.0.2}/src/xtr_orm/command/__init__.py +7 -6
- xtr_orm-3.0.0/src/xtr_orm/command/connection_command.py → xtr_orm-4.0.2/src/xtr_orm/command/_reporting.py +16 -58
- xtr_orm-4.0.2/src/xtr_orm/command/connection_command.py +66 -0
- {xtr_orm-3.0.0 → xtr_orm-4.0.2}/src/xtr_orm/command/migrations_current_command.py +2 -1
- {xtr_orm-3.0.0 → xtr_orm-4.0.2}/src/xtr_orm/command/migrations_diff_command.py +2 -1
- {xtr_orm-3.0.0 → xtr_orm-4.0.2}/src/xtr_orm/command/migrations_dump_schema_command.py +2 -1
- {xtr_orm-3.0.0 → xtr_orm-4.0.2}/src/xtr_orm/command/migrations_execute_command.py +2 -8
- {xtr_orm-3.0.0 → xtr_orm-4.0.2}/src/xtr_orm/command/migrations_generate_command.py +2 -1
- {xtr_orm-3.0.0 → xtr_orm-4.0.2}/src/xtr_orm/command/migrations_latest_command.py +2 -1
- {xtr_orm-3.0.0 → xtr_orm-4.0.2}/src/xtr_orm/command/migrations_migrate_command.py +2 -2
- {xtr_orm-3.0.0 → xtr_orm-4.0.2}/src/xtr_orm/command/migrations_up_to_date_command.py +2 -1
- {xtr_orm-3.0.0 → xtr_orm-4.0.2}/src/xtr_orm/connection_registry.py +8 -1
- {xtr_orm-3.0.0 → xtr_orm-4.0.2}/src/xtr_orm/database/database_manager.py +18 -3
- {xtr_orm-3.0.0 → xtr_orm-4.0.2}/src/xtr_orm/exception/__init__.py +2 -0
- xtr_orm-4.0.2/src/xtr_orm/exception/missing_database_error.py +29 -0
- xtr_orm-4.0.2/src/xtr_orm/mapping_driver_interface.py +38 -0
- xtr_orm-3.0.0/src/xtr_orm/command/connections.py +0 -37
- {xtr_orm-3.0.0 → xtr_orm-4.0.2}/src/xtr_orm/.agents/skills/xtr-orm/references/connections.md +0 -0
- {xtr_orm-3.0.0 → xtr_orm-4.0.2}/src/xtr_orm/.agents/skills/xtr-orm/references/messenger.md +0 -0
- {xtr_orm-3.0.0 → xtr_orm-4.0.2}/src/xtr_orm/.agents/skills/xtr-orm/references/migrations.md +0 -0
- {xtr_orm-3.0.0 → xtr_orm-4.0.2}/src/xtr_orm/bundle/orm_config.py +0 -0
- {xtr_orm-3.0.0 → xtr_orm-4.0.2}/src/xtr_orm/command/database_create_command.py +0 -0
- {xtr_orm-3.0.0 → xtr_orm-4.0.2}/src/xtr_orm/command/database_drop_command.py +0 -0
- {xtr_orm-3.0.0 → xtr_orm-4.0.2}/src/xtr_orm/command/migrations_list_command.py +0 -0
- {xtr_orm-3.0.0 → xtr_orm-4.0.2}/src/xtr_orm/command/migrations_rollup_command.py +0 -0
- {xtr_orm-3.0.0 → xtr_orm-4.0.2}/src/xtr_orm/command/migrations_status_command.py +0 -0
- {xtr_orm-3.0.0 → xtr_orm-4.0.2}/src/xtr_orm/command/migrations_version_command.py +0 -0
- {xtr_orm-3.0.0 → xtr_orm-4.0.2}/src/xtr_orm/command/run_sql_command.py +0 -0
- {xtr_orm-3.0.0 → xtr_orm-4.0.2}/src/xtr_orm/database/__init__.py +0 -0
- {xtr_orm-3.0.0 → xtr_orm-4.0.2}/src/xtr_orm/exception/invalid_argument_error.py +0 -0
- {xtr_orm-3.0.0 → xtr_orm-4.0.2}/src/xtr_orm/exception/migration_error.py +0 -0
- {xtr_orm-3.0.0 → xtr_orm-4.0.2}/src/xtr_orm/exception/orm_error.py +0 -0
- {xtr_orm-3.0.0 → xtr_orm-4.0.2}/src/xtr_orm/exception/session_unavailable_error.py +0 -0
- {xtr_orm-3.0.0 → xtr_orm-4.0.2}/src/xtr_orm/exception/unknown_connection_error.py +0 -0
- {xtr_orm-3.0.0 → xtr_orm-4.0.2}/src/xtr_orm/exception/unsupported_database_error.py +0 -0
- {xtr_orm-3.0.0 → xtr_orm-4.0.2}/src/xtr_orm/messenger/__init__.py +0 -0
- {xtr_orm-3.0.0 → xtr_orm-4.0.2}/src/xtr_orm/messenger/close_connection_middleware.py +0 -0
- {xtr_orm-3.0.0 → xtr_orm-4.0.2}/src/xtr_orm/messenger/open_transaction_logger_middleware.py +0 -0
- {xtr_orm-3.0.0 → xtr_orm-4.0.2}/src/xtr_orm/messenger/transaction_middleware.py +0 -0
- {xtr_orm-3.0.0 → xtr_orm-4.0.2}/src/xtr_orm/migrations/__init__.py +0 -0
- {xtr_orm-3.0.0 → xtr_orm-4.0.2}/src/xtr_orm/migrations/_environment.py +0 -0
- {xtr_orm-3.0.0 → xtr_orm-4.0.2}/src/xtr_orm/migrations/_history.py +0 -0
- {xtr_orm-3.0.0 → xtr_orm-4.0.2}/src/xtr_orm/migrations/_revisions.py +0 -0
- {xtr_orm-3.0.0 → xtr_orm-4.0.2}/src/xtr_orm/migrations/_schema.py +0 -0
- {xtr_orm-3.0.0 → xtr_orm-4.0.2}/src/xtr_orm/migrations/available_migration.py +0 -0
- {xtr_orm-3.0.0 → xtr_orm-4.0.2}/src/xtr_orm/migrations/direction.py +0 -0
- {xtr_orm-3.0.0 → xtr_orm-4.0.2}/src/xtr_orm/migrations/executed_migration.py +0 -0
- {xtr_orm-3.0.0 → xtr_orm-4.0.2}/src/xtr_orm/migrations/execution_result.py +0 -0
- {xtr_orm-3.0.0 → xtr_orm-4.0.2}/src/xtr_orm/migrations/migration_plan.py +0 -0
- {xtr_orm-3.0.0 → xtr_orm-4.0.2}/src/xtr_orm/migrations/migration_status.py +0 -0
- {xtr_orm-3.0.0 → xtr_orm-4.0.2}/src/xtr_orm/migrations/migrations_config.py +0 -0
- {xtr_orm-3.0.0 → xtr_orm-4.0.2}/src/xtr_orm/migrations/migrator.py +0 -0
- {xtr_orm-3.0.0 → xtr_orm-4.0.2}/src/xtr_orm/migrations/template/script.py.mako +0 -0
- {xtr_orm-3.0.0 → xtr_orm-4.0.2}/src/xtr_orm/py.typed +0 -0
- {xtr_orm-3.0.0 → xtr_orm-4.0.2}/src/xtr_orm/recipe/__init__.py +0 -0
- {xtr_orm-3.0.0 → xtr_orm-4.0.2}/src/xtr_orm/recipe/files/config/orm.py.tmpl +0 -0
- {xtr_orm-3.0.0 → xtr_orm-4.0.2}/src/xtr_orm/recipe/manifest.toml +0 -0
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
3
|
Copyright (c) 2026 xterr
|
|
4
|
+
Copyright (c) 2006-2018 Doctrine Project
|
|
5
|
+
Copyright (c) 2011 Fabien Potencier, Doctrine Project
|
|
6
|
+
Copyright (c) 2006-2013 Doctrine Project
|
|
7
|
+
Copyright (c) 2004-present Fabien Potencier
|
|
4
8
|
|
|
5
9
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
10
|
of this software and associated documentation files (the "Software"), to deal
|
|
@@ -1,3 +1,42 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: xtr-orm
|
|
3
|
+
Version: 4.0.2
|
|
4
|
+
Summary: Databases for async applications: engines, sessions and versioned schema migrations, configured once per connection.
|
|
5
|
+
Keywords: orm,database,migrations,sqlalchemy,alembic,asyncio
|
|
6
|
+
Author: Xterr
|
|
7
|
+
Author-email: Xterr <me@xterr.dev>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Framework :: AsyncIO
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Topic :: Database
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Dist: advanced-alchemy>=1.11
|
|
20
|
+
Requires-Dist: alembic>=1.20
|
|
21
|
+
Requires-Dist: sqlalchemy[asyncio]>=2.0.41
|
|
22
|
+
Requires-Dist: typing-extensions>=4.4
|
|
23
|
+
Requires-Dist: xtr-logging-contracts>=4.0,<5
|
|
24
|
+
Requires-Dist: xtr-console>=4.0,<5 ; extra == 'console'
|
|
25
|
+
Requires-Dist: xtr-dependency-injection>=4.0,<5 ; extra == 'di'
|
|
26
|
+
Requires-Dist: xtr-service-contracts>=4.0,<5 ; extra == 'di'
|
|
27
|
+
Requires-Dist: xtr-messenger>=4.0,<5 ; extra == 'messenger'
|
|
28
|
+
Requires-Dist: asyncmy>=0.2.9 ; extra == 'mysql'
|
|
29
|
+
Requires-Dist: asyncpg>=0.29 ; extra == 'postgres'
|
|
30
|
+
Requires-Dist: aiosqlite>=0.20 ; extra == 'sqlite'
|
|
31
|
+
Requires-Python: >=3.11
|
|
32
|
+
Provides-Extra: console
|
|
33
|
+
Provides-Extra: di
|
|
34
|
+
Provides-Extra: messenger
|
|
35
|
+
Provides-Extra: mysql
|
|
36
|
+
Provides-Extra: postgres
|
|
37
|
+
Provides-Extra: sqlite
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
|
|
1
40
|
<div align="center">
|
|
2
41
|
|
|
3
42
|
# xtr-orm
|
|
@@ -299,6 +338,82 @@ whatever the one before wrote. Migrations and the database commands always use t
|
|
|
299
338
|
A block can be sent elsewhere with advanced-alchemy's `primary_context()` and
|
|
300
339
|
`replica_context()`, from `advanced_alchemy.routing`.
|
|
301
340
|
|
|
341
|
+
## Mapping tables from a bundle
|
|
342
|
+
|
|
343
|
+
A library can bring its own tables to an application's database — tables whose prefix, names
|
|
344
|
+
or column types the application configures, and which therefore exist only once that
|
|
345
|
+
configuration is read. `OrmMappingsPass` puts them into the table definitions of the
|
|
346
|
+
connection the application picked, while the container is compiled: early enough for
|
|
347
|
+
`orm:migrations:diff` to write them like any of the application's own models.
|
|
348
|
+
|
|
349
|
+
The library's bundle keeps the pass, registers it in `build`, and hands over the driver it
|
|
350
|
+
builds from its configuration in `load_extension`:
|
|
351
|
+
|
|
352
|
+
```python
|
|
353
|
+
from xtr_orm.bundle import OrmBundle, OrmMappingsPass
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
@final
|
|
357
|
+
@required_bundle(OrmBundle)
|
|
358
|
+
@as_bundle("invoicing", config=InvoicingConfig)
|
|
359
|
+
class InvoicingBundle(Bundle[InvoicingConfig]):
|
|
360
|
+
def __init__(self) -> None:
|
|
361
|
+
self._mappings = OrmMappingsPass("invoicing.connection", "invoicing.persist")
|
|
362
|
+
|
|
363
|
+
@override
|
|
364
|
+
def build(self, builder: ContainerBuilder) -> None:
|
|
365
|
+
builder.add_compiler_pass(self._mappings)
|
|
366
|
+
|
|
367
|
+
@override
|
|
368
|
+
def load_extension(
|
|
369
|
+
self, config: InvoicingConfig, services: ServiceConfigurator, builder: ContainerBuilder
|
|
370
|
+
) -> None:
|
|
371
|
+
builder.set_parameter("invoicing.connection", config.connection)
|
|
372
|
+
builder.set_parameter("invoicing.persist", config.persist)
|
|
373
|
+
if config.persist:
|
|
374
|
+
self._mappings.set_driver(InvoicingDriver(config.table_prefix))
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
A driver is anything satisfying `MappingDriverInterface`: one call, adding its tables and
|
|
378
|
+
naming them. Subclass it to have the type checker hold you to that call.
|
|
379
|
+
|
|
380
|
+
```python
|
|
381
|
+
from xtr_orm import MappingDriverInterface
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
@final
|
|
385
|
+
class InvoicingDriver(MappingDriverInterface):
|
|
386
|
+
def __init__(self, prefix: str) -> None:
|
|
387
|
+
self._prefix = prefix
|
|
388
|
+
|
|
389
|
+
def load_metadata(self, metadata: MetaData) -> tuple[str, ...]:
|
|
390
|
+
name = f"{self._prefix}invoice"
|
|
391
|
+
if name in metadata.tables:
|
|
392
|
+
return ()
|
|
393
|
+
_ = Table(name, metadata, Column("id", Integer, primary_key=True))
|
|
394
|
+
return (name,)
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
- **Which definitions** — the ones the connection's migrations compare the database with: its
|
|
398
|
+
`metadata` when it has one, else the ones advanced-alchemy registers for its `bind_key`. A
|
|
399
|
+
connection keeping several sets is refused, naming it; give that one a single `MetaData`.
|
|
400
|
+
- **Idempotent, always** — `load_metadata` skips the tables already there and leaves them out
|
|
401
|
+
of what it returns. The registered definitions are process-wide — advanced-alchemy keeps one
|
|
402
|
+
`MetaData` per bind key for the whole process — so two kernels in one process load the same
|
|
403
|
+
driver into the same one. That outlives the flag: a kernel built with the library's
|
|
404
|
+
persistence off maps nothing itself, yet still *sees* every table a persisting kernel of the
|
|
405
|
+
same process put in that bind key, or in a module-level `MetaData` both reach, and diffs
|
|
406
|
+
them as its own. Give such a kernel a `bind_key`, or a freshly built `MetaData`, that no
|
|
407
|
+
persisting kernel used.
|
|
408
|
+
- **On or off** — the flag parameter is read for truth, and an unset one means off, so a
|
|
409
|
+
library whose persistence an application turned off maps nothing. On with no driver handed
|
|
410
|
+
over is refused, as is a connection name nothing configured — both naming what is missing.
|
|
411
|
+
- **Read while compiling** — both parameters are read before `%name%` references are resolved
|
|
412
|
+
and long before an `env(...)` one is, so a bundle sets them from its own resolved
|
|
413
|
+
configuration: a plain name and a plain flag. One still holding a `%name%` reference is
|
|
414
|
+
refused, naming the parameter, as is a connection parameter holding something that is not a
|
|
415
|
+
name at all — a name is never coerced out of it, since a connection may be called `7`.
|
|
416
|
+
|
|
302
417
|
## Console commands
|
|
303
418
|
|
|
304
419
|
Every command takes `--connection NAME`; the default connection's is used without it. The
|
|
@@ -324,15 +439,18 @@ commands that change the database ask first; `-n` answers yes.
|
|
|
324
439
|
|
|
325
440
|
`--write-sql` writes to the file named, or to a dated file in the directory named.
|
|
326
441
|
|
|
327
|
-
|
|
442
|
+
Every command takes the `ConnectionRegistry` it acts on as its one constructor argument — the
|
|
443
|
+
bundle's with a container, and your own without one. Nothing stands in for it: a console with no
|
|
444
|
+
container to supply it reports the parameter it cannot fill.
|
|
328
445
|
|
|
329
446
|
```python
|
|
330
447
|
from xtr_orm import ConnectionRegistry, DatabaseManager
|
|
331
|
-
from xtr_orm.command import
|
|
448
|
+
from xtr_orm.command import MigrationsMigrateCommand
|
|
332
449
|
|
|
333
450
|
registry = ConnectionRegistry()
|
|
334
451
|
registry.register("default", engine=engine, migrator=migrator, database=DatabaseManager(url))
|
|
335
|
-
|
|
452
|
+
|
|
453
|
+
migrate = MigrationsMigrateCommand(registry)
|
|
336
454
|
```
|
|
337
455
|
|
|
338
456
|
## Message bus middleware
|
|
@@ -414,6 +532,11 @@ every statement committed as it runs, and with every connection parameter of the
|
|
|
414
532
|
database: the credentials, the host and the driver's query options, such as `ssl`. A SQLite
|
|
415
533
|
database is its file. Any other server is refused with `UnsupportedDatabaseError`.
|
|
416
534
|
|
|
535
|
+
`create` and `drop` let the server refuse a database that is already there or is not, which is
|
|
536
|
+
why both take a flag — `create(if_not_exists=True)`, `drop(if_exists=True)` — to be told so
|
|
537
|
+
instead. On SQLite the file is the database: dropping one that is not there raises
|
|
538
|
+
`MissingDatabaseError`.
|
|
539
|
+
|
|
417
540
|
## Errors
|
|
418
541
|
|
|
419
542
|
Every error derives from `OrmError`.
|
|
@@ -422,6 +545,7 @@ Every error derives from `OrmError`.
|
|
|
422
545
|
|---|---|
|
|
423
546
|
| `InvalidArgumentError` | A configuration cannot be used — an empty name, an unknown option, a URL option that does not read (also a `ValueError`) |
|
|
424
547
|
| `MigrationError` | A migration is refused or its revisions cannot be read — an unknown or ambiguous version, a revision out of order, a diff of a database behind its revisions |
|
|
548
|
+
| `MissingDatabaseError` | A database is dropped that is not there — a SQLite file that does not exist — without `if_exists` (also a `FileNotFoundError`) |
|
|
425
549
|
| `UnknownConnectionError` | A connection is asked for by a name nothing registered (also a `LookupError`) |
|
|
426
550
|
| `UnsupportedDatabaseError` | A database is created or dropped on a server whose statements are not known |
|
|
427
551
|
| `SessionUnavailableError` | A connection's session is asked for outside a unit of work, or of a connection registered without one |
|
|
@@ -1,42 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: xtr-orm
|
|
3
|
-
Version: 3.0.0
|
|
4
|
-
Summary: Databases for async applications: engines, sessions and versioned schema migrations, configured once per connection.
|
|
5
|
-
Keywords: orm,database,migrations,sqlalchemy,alembic,asyncio
|
|
6
|
-
Author: Xterr
|
|
7
|
-
Author-email: Xterr <me@xterr.dev>
|
|
8
|
-
License-Expression: MIT
|
|
9
|
-
License-File: LICENSE
|
|
10
|
-
Classifier: Development Status :: 3 - Alpha
|
|
11
|
-
Classifier: Framework :: AsyncIO
|
|
12
|
-
Classifier: Intended Audience :: Developers
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
-
Classifier: Topic :: Database
|
|
18
|
-
Classifier: Typing :: Typed
|
|
19
|
-
Requires-Dist: advanced-alchemy>=1.11
|
|
20
|
-
Requires-Dist: alembic>=1.20
|
|
21
|
-
Requires-Dist: sqlalchemy[asyncio]>=2.0.41
|
|
22
|
-
Requires-Dist: typing-extensions>=4.4
|
|
23
|
-
Requires-Dist: xtr-logging-contracts>=3.0,<4
|
|
24
|
-
Requires-Dist: xtr-console>=3.0,<4 ; extra == 'console'
|
|
25
|
-
Requires-Dist: xtr-dependency-injection>=3.0,<4 ; extra == 'di'
|
|
26
|
-
Requires-Dist: xtr-service-contracts>=3.0,<4 ; extra == 'di'
|
|
27
|
-
Requires-Dist: xtr-messenger>=3.0,<4 ; extra == 'messenger'
|
|
28
|
-
Requires-Dist: asyncmy>=0.2.9 ; extra == 'mysql'
|
|
29
|
-
Requires-Dist: asyncpg>=0.29 ; extra == 'postgres'
|
|
30
|
-
Requires-Dist: aiosqlite>=0.20 ; extra == 'sqlite'
|
|
31
|
-
Requires-Python: >=3.11
|
|
32
|
-
Provides-Extra: console
|
|
33
|
-
Provides-Extra: di
|
|
34
|
-
Provides-Extra: messenger
|
|
35
|
-
Provides-Extra: mysql
|
|
36
|
-
Provides-Extra: postgres
|
|
37
|
-
Provides-Extra: sqlite
|
|
38
|
-
Description-Content-Type: text/markdown
|
|
39
|
-
|
|
40
1
|
<div align="center">
|
|
41
2
|
|
|
42
3
|
# xtr-orm
|
|
@@ -338,6 +299,82 @@ whatever the one before wrote. Migrations and the database commands always use t
|
|
|
338
299
|
A block can be sent elsewhere with advanced-alchemy's `primary_context()` and
|
|
339
300
|
`replica_context()`, from `advanced_alchemy.routing`.
|
|
340
301
|
|
|
302
|
+
## Mapping tables from a bundle
|
|
303
|
+
|
|
304
|
+
A library can bring its own tables to an application's database — tables whose prefix, names
|
|
305
|
+
or column types the application configures, and which therefore exist only once that
|
|
306
|
+
configuration is read. `OrmMappingsPass` puts them into the table definitions of the
|
|
307
|
+
connection the application picked, while the container is compiled: early enough for
|
|
308
|
+
`orm:migrations:diff` to write them like any of the application's own models.
|
|
309
|
+
|
|
310
|
+
The library's bundle keeps the pass, registers it in `build`, and hands over the driver it
|
|
311
|
+
builds from its configuration in `load_extension`:
|
|
312
|
+
|
|
313
|
+
```python
|
|
314
|
+
from xtr_orm.bundle import OrmBundle, OrmMappingsPass
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
@final
|
|
318
|
+
@required_bundle(OrmBundle)
|
|
319
|
+
@as_bundle("invoicing", config=InvoicingConfig)
|
|
320
|
+
class InvoicingBundle(Bundle[InvoicingConfig]):
|
|
321
|
+
def __init__(self) -> None:
|
|
322
|
+
self._mappings = OrmMappingsPass("invoicing.connection", "invoicing.persist")
|
|
323
|
+
|
|
324
|
+
@override
|
|
325
|
+
def build(self, builder: ContainerBuilder) -> None:
|
|
326
|
+
builder.add_compiler_pass(self._mappings)
|
|
327
|
+
|
|
328
|
+
@override
|
|
329
|
+
def load_extension(
|
|
330
|
+
self, config: InvoicingConfig, services: ServiceConfigurator, builder: ContainerBuilder
|
|
331
|
+
) -> None:
|
|
332
|
+
builder.set_parameter("invoicing.connection", config.connection)
|
|
333
|
+
builder.set_parameter("invoicing.persist", config.persist)
|
|
334
|
+
if config.persist:
|
|
335
|
+
self._mappings.set_driver(InvoicingDriver(config.table_prefix))
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
A driver is anything satisfying `MappingDriverInterface`: one call, adding its tables and
|
|
339
|
+
naming them. Subclass it to have the type checker hold you to that call.
|
|
340
|
+
|
|
341
|
+
```python
|
|
342
|
+
from xtr_orm import MappingDriverInterface
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
@final
|
|
346
|
+
class InvoicingDriver(MappingDriverInterface):
|
|
347
|
+
def __init__(self, prefix: str) -> None:
|
|
348
|
+
self._prefix = prefix
|
|
349
|
+
|
|
350
|
+
def load_metadata(self, metadata: MetaData) -> tuple[str, ...]:
|
|
351
|
+
name = f"{self._prefix}invoice"
|
|
352
|
+
if name in metadata.tables:
|
|
353
|
+
return ()
|
|
354
|
+
_ = Table(name, metadata, Column("id", Integer, primary_key=True))
|
|
355
|
+
return (name,)
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
- **Which definitions** — the ones the connection's migrations compare the database with: its
|
|
359
|
+
`metadata` when it has one, else the ones advanced-alchemy registers for its `bind_key`. A
|
|
360
|
+
connection keeping several sets is refused, naming it; give that one a single `MetaData`.
|
|
361
|
+
- **Idempotent, always** — `load_metadata` skips the tables already there and leaves them out
|
|
362
|
+
of what it returns. The registered definitions are process-wide — advanced-alchemy keeps one
|
|
363
|
+
`MetaData` per bind key for the whole process — so two kernels in one process load the same
|
|
364
|
+
driver into the same one. That outlives the flag: a kernel built with the library's
|
|
365
|
+
persistence off maps nothing itself, yet still *sees* every table a persisting kernel of the
|
|
366
|
+
same process put in that bind key, or in a module-level `MetaData` both reach, and diffs
|
|
367
|
+
them as its own. Give such a kernel a `bind_key`, or a freshly built `MetaData`, that no
|
|
368
|
+
persisting kernel used.
|
|
369
|
+
- **On or off** — the flag parameter is read for truth, and an unset one means off, so a
|
|
370
|
+
library whose persistence an application turned off maps nothing. On with no driver handed
|
|
371
|
+
over is refused, as is a connection name nothing configured — both naming what is missing.
|
|
372
|
+
- **Read while compiling** — both parameters are read before `%name%` references are resolved
|
|
373
|
+
and long before an `env(...)` one is, so a bundle sets them from its own resolved
|
|
374
|
+
configuration: a plain name and a plain flag. One still holding a `%name%` reference is
|
|
375
|
+
refused, naming the parameter, as is a connection parameter holding something that is not a
|
|
376
|
+
name at all — a name is never coerced out of it, since a connection may be called `7`.
|
|
377
|
+
|
|
341
378
|
## Console commands
|
|
342
379
|
|
|
343
380
|
Every command takes `--connection NAME`; the default connection's is used without it. The
|
|
@@ -363,15 +400,18 @@ commands that change the database ask first; `-n` answers yes.
|
|
|
363
400
|
|
|
364
401
|
`--write-sql` writes to the file named, or to a dated file in the directory named.
|
|
365
402
|
|
|
366
|
-
|
|
403
|
+
Every command takes the `ConnectionRegistry` it acts on as its one constructor argument — the
|
|
404
|
+
bundle's with a container, and your own without one. Nothing stands in for it: a console with no
|
|
405
|
+
container to supply it reports the parameter it cannot fill.
|
|
367
406
|
|
|
368
407
|
```python
|
|
369
408
|
from xtr_orm import ConnectionRegistry, DatabaseManager
|
|
370
|
-
from xtr_orm.command import
|
|
409
|
+
from xtr_orm.command import MigrationsMigrateCommand
|
|
371
410
|
|
|
372
411
|
registry = ConnectionRegistry()
|
|
373
412
|
registry.register("default", engine=engine, migrator=migrator, database=DatabaseManager(url))
|
|
374
|
-
|
|
413
|
+
|
|
414
|
+
migrate = MigrationsMigrateCommand(registry)
|
|
375
415
|
```
|
|
376
416
|
|
|
377
417
|
## Message bus middleware
|
|
@@ -453,6 +493,11 @@ every statement committed as it runs, and with every connection parameter of the
|
|
|
453
493
|
database: the credentials, the host and the driver's query options, such as `ssl`. A SQLite
|
|
454
494
|
database is its file. Any other server is refused with `UnsupportedDatabaseError`.
|
|
455
495
|
|
|
496
|
+
`create` and `drop` let the server refuse a database that is already there or is not, which is
|
|
497
|
+
why both take a flag — `create(if_not_exists=True)`, `drop(if_exists=True)` — to be told so
|
|
498
|
+
instead. On SQLite the file is the database: dropping one that is not there raises
|
|
499
|
+
`MissingDatabaseError`.
|
|
500
|
+
|
|
456
501
|
## Errors
|
|
457
502
|
|
|
458
503
|
Every error derives from `OrmError`.
|
|
@@ -461,6 +506,7 @@ Every error derives from `OrmError`.
|
|
|
461
506
|
|---|---|
|
|
462
507
|
| `InvalidArgumentError` | A configuration cannot be used — an empty name, an unknown option, a URL option that does not read (also a `ValueError`) |
|
|
463
508
|
| `MigrationError` | A migration is refused or its revisions cannot be read — an unknown or ambiguous version, a revision out of order, a diff of a database behind its revisions |
|
|
509
|
+
| `MissingDatabaseError` | A database is dropped that is not there — a SQLite file that does not exist — without `if_exists` (also a `FileNotFoundError`) |
|
|
464
510
|
| `UnknownConnectionError` | A connection is asked for by a name nothing registered (also a `LookupError`) |
|
|
465
511
|
| `UnsupportedDatabaseError` | A database is created or dropped on a server whose statements are not known |
|
|
466
512
|
| `SessionUnavailableError` | A connection's session is asked for outside a unit of work, or of a connection registered without one |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "xtr-orm"
|
|
3
|
-
version = "
|
|
3
|
+
version = "4.0.2"
|
|
4
4
|
description = "Databases for async applications: engines, sessions and versioned schema migrations, configured once per connection."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -30,7 +30,7 @@ dependencies = [
|
|
|
30
30
|
"alembic>=1.20",
|
|
31
31
|
"sqlalchemy[asyncio]>=2.0.41",
|
|
32
32
|
"typing-extensions>=4.4",
|
|
33
|
-
"xtr-logging-contracts>=
|
|
33
|
+
"xtr-logging-contracts>=4.0,<5",
|
|
34
34
|
]
|
|
35
35
|
|
|
36
36
|
[[project.authors]]
|
|
@@ -42,11 +42,11 @@ postgres = ["asyncpg>=0.29"]
|
|
|
42
42
|
sqlite = ["aiosqlite>=0.20"]
|
|
43
43
|
mysql = ["asyncmy>=0.2.9"]
|
|
44
44
|
di = [
|
|
45
|
-
"xtr-dependency-injection>=
|
|
46
|
-
"xtr-service-contracts>=
|
|
45
|
+
"xtr-dependency-injection>=4.0,<5",
|
|
46
|
+
"xtr-service-contracts>=4.0,<5",
|
|
47
47
|
]
|
|
48
|
-
console = ["xtr-console>=
|
|
49
|
-
messenger = ["xtr-messenger>=
|
|
48
|
+
console = ["xtr-console>=4.0,<5"]
|
|
49
|
+
messenger = ["xtr-messenger>=4.0,<5"]
|
|
50
50
|
|
|
51
51
|
[project.entry-points."xtr_dependency_injection.bundles"]
|
|
52
52
|
orm = "xtr_orm.bundle:OrmBundle"
|
|
@@ -63,11 +63,11 @@ dev = [
|
|
|
63
63
|
"anyio>=4.0",
|
|
64
64
|
"ty>=0.0.83",
|
|
65
65
|
"aiosqlite>=0.20",
|
|
66
|
-
"xtr-console>=
|
|
67
|
-
"xtr-dependency-injection>=
|
|
68
|
-
"xtr-logging>=
|
|
69
|
-
"xtr-messenger>=
|
|
70
|
-
"xtr-service-contracts>=
|
|
66
|
+
"xtr-console>=4.0,<5",
|
|
67
|
+
"xtr-dependency-injection>=4.0,<5",
|
|
68
|
+
"xtr-logging>=4.0,<5",
|
|
69
|
+
"xtr-messenger>=4.0,<5",
|
|
70
|
+
"xtr-service-contracts>=4.0,<5",
|
|
71
71
|
]
|
|
72
72
|
|
|
73
73
|
[build-system]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "xtr-orm"
|
|
3
|
-
version = "
|
|
3
|
+
version = "4.0.2"
|
|
4
4
|
description = "Databases for async applications: engines, sessions and versioned schema migrations, configured once per connection."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -26,7 +26,7 @@ dependencies = [
|
|
|
26
26
|
"alembic>=1.20",
|
|
27
27
|
"sqlalchemy[asyncio]>=2.0.41",
|
|
28
28
|
"typing-extensions>=4.4",
|
|
29
|
-
"xtr-logging-contracts>=
|
|
29
|
+
"xtr-logging-contracts>=4.0,<5",
|
|
30
30
|
]
|
|
31
31
|
|
|
32
32
|
[project.optional-dependencies]
|
|
@@ -40,14 +40,14 @@ mysql = [
|
|
|
40
40
|
"asyncmy>=0.2.9",
|
|
41
41
|
]
|
|
42
42
|
di = [
|
|
43
|
-
"xtr-dependency-injection>=
|
|
44
|
-
"xtr-service-contracts>=
|
|
43
|
+
"xtr-dependency-injection>=4.0,<5",
|
|
44
|
+
"xtr-service-contracts>=4.0,<5",
|
|
45
45
|
]
|
|
46
46
|
console = [
|
|
47
|
-
"xtr-console>=
|
|
47
|
+
"xtr-console>=4.0,<5",
|
|
48
48
|
]
|
|
49
49
|
messenger = [
|
|
50
|
-
"xtr-messenger>=
|
|
50
|
+
"xtr-messenger>=4.0,<5",
|
|
51
51
|
]
|
|
52
52
|
|
|
53
53
|
# Advertises the bundle so debug:bundles can name it when installed but not listed.
|
|
@@ -69,11 +69,11 @@ dev = [
|
|
|
69
69
|
"ty>=0.0.83",
|
|
70
70
|
# Every test runs on SQLite files under the test's own directory: no server.
|
|
71
71
|
"aiosqlite>=0.20",
|
|
72
|
-
"xtr-console>=
|
|
73
|
-
"xtr-dependency-injection>=
|
|
74
|
-
"xtr-logging>=
|
|
75
|
-
"xtr-messenger>=
|
|
76
|
-
"xtr-service-contracts>=
|
|
72
|
+
"xtr-console>=4.0,<5",
|
|
73
|
+
"xtr-dependency-injection>=4.0,<5",
|
|
74
|
+
"xtr-logging>=4.0,<5",
|
|
75
|
+
"xtr-messenger>=4.0,<5",
|
|
76
|
+
"xtr-service-contracts>=4.0,<5",
|
|
77
77
|
]
|
|
78
78
|
|
|
79
79
|
[build-system]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: xtr-orm
|
|
3
|
-
description: How to configure database connections, inject async sessions and run versioned schema migrations with xtr-orm. Use when application code needs an AsyncEngine, an AsyncSession, a session per request or per message, read replicas, a DATABASE_URL, a new migration revision, a schema diff against models, the orm:migrations:* or orm:database:* console commands, one transaction per message on a bus, or when adding OrmBundle to an application on xtr-dependency-injection.
|
|
3
|
+
description: How to configure database connections, inject async sessions and run versioned schema migrations with xtr-orm. Use when application code needs an AsyncEngine, an AsyncSession, a session per request or per message, read replicas, a DATABASE_URL, a new migration revision, a schema diff against models, the orm:migrations:* or orm:database:* console commands, one transaction per message on a bus, when a library bundle must add tables it builds in code to a connection's definitions, or when adding OrmBundle to an application on xtr-dependency-injection.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# xtr-orm
|
|
@@ -20,6 +20,8 @@ connection's name. Everything is async; there is no sync counterpart.
|
|
|
20
20
|
- Write a revision from your models: `orm:migrations:diff --message "add invoices"`. Apply:
|
|
21
21
|
`orm:migrations:migrate`. Read it: `orm:migrations:status`.
|
|
22
22
|
- Without a container: `Migrator(engine, MigrationsConfig(directory="migrations"), Base.metadata)`.
|
|
23
|
+
- A library bringing its own tables registers an `OrmMappingsPass` from its bundle's `build`; its
|
|
24
|
+
driver's `load_metadata` must be idempotent.
|
|
23
25
|
- One transaction per message: list `orm_transaction` in the bus's `middleware`; handlers never
|
|
24
26
|
commit.
|
|
25
27
|
- On SQLite, set `MigrationsConfig(render_as_batch=True)` or table alterations cannot be written.
|
|
@@ -114,6 +116,75 @@ under a kernel start it with `%kernel.project_dir%`), `render_as_batch` (SQLite)
|
|
|
114
116
|
`template_directory`, `user_module_prefix`. See [references/migrations.md](references/migrations.md)
|
|
115
117
|
for every field and for writing your own `script.py.mako`.
|
|
116
118
|
|
|
119
|
+
## Map tables from a bundle
|
|
120
|
+
|
|
121
|
+
For a library whose own tables an application configures — their prefix, which of them it keeps,
|
|
122
|
+
which column type an id gets — so they exist only once that configuration is read. Its bundle
|
|
123
|
+
keeps an `OrmMappingsPass`, registers it in `build`, and hands it the driver it builds in
|
|
124
|
+
`load_extension`; the pass loads the tables into the definitions the named connection's
|
|
125
|
+
migrations use, so `orm:migrations:diff` writes them like the application's own models.
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
from typing import final
|
|
129
|
+
|
|
130
|
+
from sqlalchemy import Column, Integer, MetaData, Table
|
|
131
|
+
from typing_extensions import override
|
|
132
|
+
from xtr_dependency_injection import (
|
|
133
|
+
Bundle,
|
|
134
|
+
ContainerBuilder,
|
|
135
|
+
ServiceConfigurator,
|
|
136
|
+
as_bundle,
|
|
137
|
+
required_bundle,
|
|
138
|
+
)
|
|
139
|
+
from xtr_orm import MappingDriverInterface
|
|
140
|
+
from xtr_orm.bundle import OrmBundle, OrmMappingsPass
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@final
|
|
144
|
+
@required_bundle(OrmBundle)
|
|
145
|
+
@as_bundle("invoicing", config=InvoicingConfig)
|
|
146
|
+
class InvoicingBundle(Bundle[InvoicingConfig]):
|
|
147
|
+
def __init__(self) -> None:
|
|
148
|
+
self._mappings = OrmMappingsPass("invoicing.connection", "invoicing.persist")
|
|
149
|
+
|
|
150
|
+
@override
|
|
151
|
+
def build(self, builder: ContainerBuilder) -> None:
|
|
152
|
+
builder.add_compiler_pass(self._mappings)
|
|
153
|
+
|
|
154
|
+
@override
|
|
155
|
+
def load_extension(
|
|
156
|
+
self, config: InvoicingConfig, services: ServiceConfigurator, builder: ContainerBuilder
|
|
157
|
+
) -> None:
|
|
158
|
+
builder.set_parameter("invoicing.connection", config.connection)
|
|
159
|
+
builder.set_parameter("invoicing.persist", config.persist)
|
|
160
|
+
if config.persist:
|
|
161
|
+
self._mappings.set_driver(InvoicingDriver(config.table_prefix))
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@final
|
|
165
|
+
class InvoicingDriver(MappingDriverInterface):
|
|
166
|
+
def __init__(self, prefix: str) -> None:
|
|
167
|
+
self._prefix = prefix
|
|
168
|
+
|
|
169
|
+
def load_metadata(self, metadata: MetaData) -> tuple[str, ...]:
|
|
170
|
+
name = f"{self._prefix}invoice"
|
|
171
|
+
if name in metadata.tables:
|
|
172
|
+
return ()
|
|
173
|
+
_ = Table(name, metadata, Column("id", Integer, primary_key=True))
|
|
174
|
+
return (name,)
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
A driver is anything satisfying `MappingDriverInterface`: `load_metadata(metadata)` adds its
|
|
178
|
+
tables and returns the names it added. Subclassing it has the type checker hold you to that call.
|
|
179
|
+
|
|
180
|
+
| Rule | What it means |
|
|
181
|
+
|---|---|
|
|
182
|
+
| Idempotent | Skip every name already in `metadata.tables` and leave it out of the return. The registered definitions are process-wide — one `MetaData` per bind key for the whole process — so a second kernel loads the same driver into the same one. It outlives the flag: a kernel built with persistence off maps nothing itself, yet still sees and diffs every table a persisting kernel of the same process put in that bind key, or in a module-level `MetaData` both reach. Give such a kernel a `bind_key`, or a freshly built `MetaData`, no persisting kernel used |
|
|
183
|
+
| Which definitions | The connection's `metadata` when it has one, else advanced-alchemy's for its `bind_key`. A connection keeping several sets is refused, naming it |
|
|
184
|
+
| On or off | The flag parameter is read for truth; unset means off. On with no driver, or a connection name nothing configured, fails the build with `InvalidArgumentError` |
|
|
185
|
+
| Plain values | Both parameters are read while compiling, before `%name%` is resolved and long before `env(...)` is — set them from the bundle's own resolved config. One still holding a `%name%` reference fails the build naming the parameter, as does a connection parameter holding something that is not a name at all: a name is never coerced out of it, a connection being allowed to be called `7` |
|
|
186
|
+
| Required peer | The bundle must declare `@required_bundle(OrmBundle)`; without it the pass cannot read the orm's configuration and the build fails with `UnknownConfigTypeError` |
|
|
187
|
+
|
|
117
188
|
## Console commands
|
|
118
189
|
|
|
119
190
|
Needs the `console` extra. Every command takes `--connection NAME`, defaulting to
|
|
@@ -133,15 +204,18 @@ Nine more, and every flag: [references/commands.md](references/commands.md).
|
|
|
133
204
|
Deploy order: `orm:migrations:migrate` from **one** place, before the application starts —
|
|
134
205
|
migrations take no lock.
|
|
135
206
|
|
|
136
|
-
|
|
207
|
+
Every command takes the `ConnectionRegistry` it acts on as its one constructor argument. Nothing
|
|
208
|
+
stands in for it: a console with no container to supply it raises `MissingContainerError` naming
|
|
209
|
+
`connections`.
|
|
137
210
|
|
|
138
211
|
```python
|
|
139
212
|
from xtr_orm import ConnectionRegistry, DatabaseManager
|
|
140
|
-
from xtr_orm.command import
|
|
213
|
+
from xtr_orm.command import MigrationsMigrateCommand
|
|
141
214
|
|
|
142
215
|
registry = ConnectionRegistry()
|
|
143
216
|
registry.register("default", engine=engine, migrator=migrator, database=DatabaseManager(url))
|
|
144
|
-
|
|
217
|
+
|
|
218
|
+
migrate = MigrationsMigrateCommand(registry)
|
|
145
219
|
```
|
|
146
220
|
|
|
147
221
|
## Use in an application
|
|
@@ -251,7 +325,68 @@ database.backend, database.database, database.safe_url # properties
|
|
|
251
325
|
|
|
252
326
|
The server is reached through its maintenance database — `postgres`, `master`, none on MySQL and
|
|
253
327
|
MariaDB — carrying every connection parameter of the URL but the database name. A SQLite database
|
|
254
|
-
is its file.
|
|
328
|
+
is its file. Without `if_exists`, dropping a SQLite file that is not there raises
|
|
329
|
+
`MissingDatabaseError`. Any other server raises `UnsupportedDatabaseError`.
|
|
330
|
+
|
|
331
|
+
## Testing
|
|
332
|
+
|
|
333
|
+
Test against a SQLite file in the test's own directory: it is a real database, needs no server,
|
|
334
|
+
and goes away with `tmp_path`. Give a `Migrator` the engine, a `MigrationsConfig` pointing at a
|
|
335
|
+
directory of its own, and your table definitions.
|
|
336
|
+
|
|
337
|
+
```python
|
|
338
|
+
# tests/test_invoices.py
|
|
339
|
+
from pathlib import Path
|
|
340
|
+
|
|
341
|
+
import pytest
|
|
342
|
+
from sqlalchemy import text
|
|
343
|
+
from sqlalchemy.ext.asyncio import create_async_engine
|
|
344
|
+
from xtr_orm import DatabaseManager, MigrationsConfig, Migrator
|
|
345
|
+
|
|
346
|
+
from app.models import Base
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
@pytest.fixture
|
|
350
|
+
def anyio_backend() -> str:
|
|
351
|
+
return "asyncio"
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
@pytest.mark.anyio
|
|
355
|
+
async def test_the_schema_the_models_describe_is_migrated(tmp_path: Path) -> None:
|
|
356
|
+
url = f"sqlite+aiosqlite:///{tmp_path / 'app.sqlite'}"
|
|
357
|
+
assert await DatabaseManager(url).create()
|
|
358
|
+
engine = create_async_engine(url)
|
|
359
|
+
migrator = Migrator(
|
|
360
|
+
engine,
|
|
361
|
+
# render_as_batch: SQLite cannot alter a table any other way.
|
|
362
|
+
MigrationsConfig(directory=str(tmp_path / "migrations"), render_as_batch=True),
|
|
363
|
+
Base.metadata,
|
|
364
|
+
)
|
|
365
|
+
try:
|
|
366
|
+
assert await migrator.diff("the catalogue") is not None
|
|
367
|
+
_ = await migrator.migrate()
|
|
368
|
+
|
|
369
|
+
assert (await migrator.status()).is_up_to_date
|
|
370
|
+
async with engine.connect() as connection:
|
|
371
|
+
_ = await connection.execute(text("select count(*) from invoice"))
|
|
372
|
+
finally:
|
|
373
|
+
await engine.dispose()
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
- **A command** takes a `ConnectionRegistry`, so build one for the test and hand it over:
|
|
377
|
+
`MigrationsStatusCommand(registry)`. Register the connection with the engine, migrator and
|
|
378
|
+
`DatabaseManager` the test made.
|
|
379
|
+
- **A unit of work** — the scope an `AsyncSession` lives in — is opened by
|
|
380
|
+
`bind_callable(container, work, per_call_scope=True)` under a kernel; outside one, ask the
|
|
381
|
+
registry for a session only where you registered one, or `SessionUnavailableError` says so.
|
|
382
|
+
- **Two kernels in one process** share advanced-alchemy's model registry, so give each
|
|
383
|
+
connection its `metadata` rather than let a diff pick up another test's tables.
|
|
384
|
+
- **A mapping driver** needs no container: `driver.load_metadata(MetaData())` returns the names
|
|
385
|
+
it added, and a second call on the same `MetaData` must return `()`. A kernel test asserting
|
|
386
|
+
a library mapped nothing needs a bind key of its own, or an earlier kernel's tables are still
|
|
387
|
+
in the one it diffs.
|
|
388
|
+
- **Dispose every engine you build**, in a fixture's teardown: an engine left open holds the
|
|
389
|
+
SQLite file and warns when the loop closes.
|
|
255
390
|
|
|
256
391
|
## Errors
|
|
257
392
|
|
|
@@ -261,6 +396,7 @@ Every error derives from `OrmError`.
|
|
|
261
396
|
|---|---|
|
|
262
397
|
| `InvalidArgumentError` | A configuration cannot be used: an empty name, an unknown engine/session option, a URL option that does not read. Also a `ValueError` |
|
|
263
398
|
| `MigrationError` | A migration is refused or revisions cannot be read: an unknown or ambiguous version, a revision out of order, a diff of a database behind its revisions |
|
|
399
|
+
| `MissingDatabaseError` | A database dropped without `if_exists` is not there — a SQLite file that does not exist. Also a `FileNotFoundError` |
|
|
264
400
|
| `UnknownConnectionError` | A connection asked for by a name nothing registered. Also a `LookupError` |
|
|
265
401
|
| `UnsupportedDatabaseError` | A database created or dropped on a server whose statements are not known |
|
|
266
402
|
| `SessionUnavailableError` | A session asked for outside a unit of work, or of a connection registered without one |
|
|
@@ -279,4 +415,9 @@ Every error derives from `OrmError`.
|
|
|
279
415
|
- Do not run `orm:migrations:migrate` from several processes at once; it takes no lock.
|
|
280
416
|
- Do not rely on advanced-alchemy's process-wide model registry when two kernels share one
|
|
281
417
|
process: give each connection its `metadata`.
|
|
418
|
+
- Do not write a mapping driver that defines a table unconditionally: a `load_metadata` that
|
|
419
|
+
does not skip what is already there fails the second kernel of the process.
|
|
420
|
+
- Do not read turning a library's persistence off as a clean slate: that kernel maps nothing of
|
|
421
|
+
its own, but still sees and diffs the tables a persisting kernel of the same process left in
|
|
422
|
+
the bind key — give it a `bind_key`, or a freshly built `MetaData`, no persisting kernel used.
|
|
282
423
|
- Do not expect a timezone on `executed_at` outside PostgreSQL; it is naive elsewhere.
|