xtr-orm 2.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.
Files changed (69) hide show
  1. {xtr_orm-2.0.0 → xtr_orm-4.0.2}/LICENSE +4 -0
  2. xtr_orm-2.0.0/README.md → xtr_orm-4.0.2/PKG-INFO +135 -7
  3. xtr_orm-2.0.0/PKG-INFO → xtr_orm-4.0.2/README.md +96 -46
  4. {xtr_orm-2.0.0 → xtr_orm-4.0.2}/pyproject.toml +14 -11
  5. {xtr_orm-2.0.0 → xtr_orm-4.0.2}/pyproject.toml.orig +15 -11
  6. xtr_orm-4.0.2/src/xtr_orm/.agents/skills/xtr-orm/SKILL.md +423 -0
  7. xtr_orm-4.0.2/src/xtr_orm/.agents/skills/xtr-orm/references/commands.md +72 -0
  8. xtr_orm-4.0.2/src/xtr_orm/.agents/skills/xtr-orm/references/connections.md +97 -0
  9. xtr_orm-4.0.2/src/xtr_orm/.agents/skills/xtr-orm/references/messenger.md +95 -0
  10. xtr_orm-4.0.2/src/xtr_orm/.agents/skills/xtr-orm/references/migrations.md +71 -0
  11. {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/__init__.py +16 -1
  12. {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/bundle/__init__.py +9 -1
  13. {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/bundle/connection_config.py +32 -2
  14. {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/bundle/orm_bundle.py +50 -33
  15. xtr_orm-4.0.2/src/xtr_orm/bundle/orm_mappings_pass.py +210 -0
  16. {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/command/__init__.py +7 -6
  17. xtr_orm-2.0.0/src/xtr_orm/command/connection_command.py → xtr_orm-4.0.2/src/xtr_orm/command/_reporting.py +16 -58
  18. xtr_orm-4.0.2/src/xtr_orm/command/connection_command.py +66 -0
  19. {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/command/migrations_current_command.py +2 -1
  20. {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/command/migrations_diff_command.py +2 -1
  21. {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/command/migrations_dump_schema_command.py +2 -1
  22. {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/command/migrations_execute_command.py +2 -8
  23. {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/command/migrations_generate_command.py +2 -1
  24. {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/command/migrations_latest_command.py +2 -1
  25. {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/command/migrations_migrate_command.py +2 -2
  26. {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/command/migrations_up_to_date_command.py +2 -1
  27. {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/connection_registry.py +8 -1
  28. {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/database/database_manager.py +18 -3
  29. {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/exception/__init__.py +2 -0
  30. xtr_orm-4.0.2/src/xtr_orm/exception/missing_database_error.py +29 -0
  31. xtr_orm-4.0.2/src/xtr_orm/mapping_driver_interface.py +38 -0
  32. xtr_orm-4.0.2/src/xtr_orm/recipe/__init__.py +15 -0
  33. xtr_orm-4.0.2/src/xtr_orm/recipe/files/config/orm.py.tmpl +24 -0
  34. xtr_orm-4.0.2/src/xtr_orm/recipe/manifest.toml +32 -0
  35. xtr_orm-2.0.0/src/xtr_orm/command/connections.py +0 -37
  36. {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/bundle/orm_config.py +0 -0
  37. {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/command/database_create_command.py +0 -0
  38. {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/command/database_drop_command.py +0 -0
  39. {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/command/migrations_list_command.py +0 -0
  40. {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/command/migrations_rollup_command.py +0 -0
  41. {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/command/migrations_status_command.py +0 -0
  42. {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/command/migrations_version_command.py +0 -0
  43. {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/command/run_sql_command.py +0 -0
  44. {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/database/__init__.py +0 -0
  45. {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/exception/invalid_argument_error.py +0 -0
  46. {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/exception/migration_error.py +0 -0
  47. {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/exception/orm_error.py +0 -0
  48. {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/exception/session_unavailable_error.py +0 -0
  49. {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/exception/unknown_connection_error.py +0 -0
  50. {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/exception/unsupported_database_error.py +0 -0
  51. {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/messenger/__init__.py +0 -0
  52. {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/messenger/close_connection_middleware.py +0 -0
  53. {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/messenger/open_transaction_logger_middleware.py +0 -0
  54. {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/messenger/transaction_middleware.py +0 -0
  55. {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/migrations/__init__.py +0 -0
  56. {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/migrations/_environment.py +0 -0
  57. {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/migrations/_history.py +0 -0
  58. {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/migrations/_revisions.py +0 -0
  59. {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/migrations/_schema.py +0 -0
  60. {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/migrations/available_migration.py +0 -0
  61. {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/migrations/direction.py +0 -0
  62. {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/migrations/executed_migration.py +0 -0
  63. {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/migrations/execution_result.py +0 -0
  64. {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/migrations/migration_plan.py +0 -0
  65. {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/migrations/migration_status.py +0 -0
  66. {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/migrations/migrations_config.py +0 -0
  67. {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/migrations/migrator.py +0 -0
  68. {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/migrations/template/script.py.mako +0 -0
  69. {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/py.typed +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
@@ -163,15 +202,19 @@ removing it undoes.
163
202
  - **Install** — `uv add "xtr-orm[di,console,postgres]"`; `mysql` or `sqlite` instead of
164
203
  `postgres` for those databases; drop `console` to go without the commands; add `messenger`
165
204
  for the [message bus middleware](#message-bus-middleware).
205
+ - **Recipe** — `uv run xtr-recipes recipes:sync` does the *Activate*, *Configure* and
206
+ *Environment* steps below: it lists `OrmBundle`, writes a starting `<app>/config/orm.py` with the
207
+ `default` connection, and `DATABASE_URL` in `.env`. It prints the middleware step, which a recipe
208
+ cannot make for you.
166
209
  - **Activate** — `OrmBundle: {"all": True}` in `BUNDLES` in `<app>/bundles.py`, imported from
167
210
  `xtr_orm.bundle`.
168
211
  - **Brings along** — the logging and console bundles, when those packages are installed.
169
212
  With the messenger bundle active too, the [middleware](#message-bus-middleware) are
170
213
  registered for a bus to list.
171
- - **Configure** — optional: with no configuration there is one connection, `default`, reading
172
- `DATABASE_URL`, with its revisions in `migrations` in the project directory and diffs
173
- against the models advanced-alchemy registers. Connections, replicas and table definitions go
174
- in `<app>/config/orm.py`, a `@configure` function returning `OrmConfig` — see
214
+ - **Configure** — the recipe writes a starting `<app>/config/orm.py` with the `default`
215
+ connection reading `DATABASE_URL`, its revisions in `migrations` in the project directory and
216
+ diffs against the models advanced-alchemy registers. Add connections, replicas and table
217
+ definitions to that `@configure` function returning `OrmConfig` — see
175
218
  [Kernel / bundle](#kernel--bundle).
176
219
  - **Environment** — `DATABASE_URL`, read when a connection is first used — engine, session,
177
220
  migrator or command — not when the application boots. Its query may carry options; see
@@ -295,6 +338,82 @@ whatever the one before wrote. Migrations and the database commands always use t
295
338
  A block can be sent elsewhere with advanced-alchemy's `primary_context()` and
296
339
  `replica_context()`, from `advanced_alchemy.routing`.
297
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
+
298
417
  ## Console commands
299
418
 
300
419
  Every command takes `--connection NAME`; the default connection's is used without it. The
@@ -320,15 +439,18 @@ commands that change the database ask first; `-n` answers yes.
320
439
 
321
440
  `--write-sql` writes to the file named, or to a dated file in the directory named.
322
441
 
323
- Without a container, give the commands their connections once:
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.
324
445
 
325
446
  ```python
326
447
  from xtr_orm import ConnectionRegistry, DatabaseManager
327
- from xtr_orm.command import use_connections
448
+ from xtr_orm.command import MigrationsMigrateCommand
328
449
 
329
450
  registry = ConnectionRegistry()
330
451
  registry.register("default", engine=engine, migrator=migrator, database=DatabaseManager(url))
331
- use_connections(registry)
452
+
453
+ migrate = MigrationsMigrateCommand(registry)
332
454
  ```
333
455
 
334
456
  ## Message bus middleware
@@ -410,6 +532,11 @@ every statement committed as it runs, and with every connection parameter of the
410
532
  database: the credentials, the host and the driver's query options, such as `ssl`. A SQLite
411
533
  database is its file. Any other server is refused with `UnsupportedDatabaseError`.
412
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
+
413
540
  ## Errors
414
541
 
415
542
  Every error derives from `OrmError`.
@@ -418,6 +545,7 @@ Every error derives from `OrmError`.
418
545
  |---|---|
419
546
  | `InvalidArgumentError` | A configuration cannot be used — an empty name, an unknown option, a URL option that does not read (also a `ValueError`) |
420
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`) |
421
549
  | `UnknownConnectionError` | A connection is asked for by a name nothing registered (also a `LookupError`) |
422
550
  | `UnsupportedDatabaseError` | A database is created or dropped on a server whose statements are not known |
423
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: 2.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>=2.0,<3
24
- Requires-Dist: xtr-console>=2.0,<3 ; extra == 'console'
25
- Requires-Dist: xtr-dependency-injection>=2.0,<3 ; extra == 'di'
26
- Requires-Dist: xtr-service-contracts>=2.0,<3 ; extra == 'di'
27
- Requires-Dist: xtr-messenger>=2.0,<3 ; 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
@@ -202,15 +163,19 @@ removing it undoes.
202
163
  - **Install** — `uv add "xtr-orm[di,console,postgres]"`; `mysql` or `sqlite` instead of
203
164
  `postgres` for those databases; drop `console` to go without the commands; add `messenger`
204
165
  for the [message bus middleware](#message-bus-middleware).
166
+ - **Recipe** — `uv run xtr-recipes recipes:sync` does the *Activate*, *Configure* and
167
+ *Environment* steps below: it lists `OrmBundle`, writes a starting `<app>/config/orm.py` with the
168
+ `default` connection, and `DATABASE_URL` in `.env`. It prints the middleware step, which a recipe
169
+ cannot make for you.
205
170
  - **Activate** — `OrmBundle: {"all": True}` in `BUNDLES` in `<app>/bundles.py`, imported from
206
171
  `xtr_orm.bundle`.
207
172
  - **Brings along** — the logging and console bundles, when those packages are installed.
208
173
  With the messenger bundle active too, the [middleware](#message-bus-middleware) are
209
174
  registered for a bus to list.
210
- - **Configure** — optional: with no configuration there is one connection, `default`, reading
211
- `DATABASE_URL`, with its revisions in `migrations` in the project directory and diffs
212
- against the models advanced-alchemy registers. Connections, replicas and table definitions go
213
- in `<app>/config/orm.py`, a `@configure` function returning `OrmConfig` — see
175
+ - **Configure** — the recipe writes a starting `<app>/config/orm.py` with the `default`
176
+ connection reading `DATABASE_URL`, its revisions in `migrations` in the project directory and
177
+ diffs against the models advanced-alchemy registers. Add connections, replicas and table
178
+ definitions to that `@configure` function returning `OrmConfig` — see
214
179
  [Kernel / bundle](#kernel--bundle).
215
180
  - **Environment** — `DATABASE_URL`, read when a connection is first used — engine, session,
216
181
  migrator or command — not when the application boots. Its query may carry options; see
@@ -334,6 +299,82 @@ whatever the one before wrote. Migrations and the database commands always use t
334
299
  A block can be sent elsewhere with advanced-alchemy's `primary_context()` and
335
300
  `replica_context()`, from `advanced_alchemy.routing`.
336
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
+
337
378
  ## Console commands
338
379
 
339
380
  Every command takes `--connection NAME`; the default connection's is used without it. The
@@ -359,15 +400,18 @@ commands that change the database ask first; `-n` answers yes.
359
400
 
360
401
  `--write-sql` writes to the file named, or to a dated file in the directory named.
361
402
 
362
- Without a container, give the commands their connections once:
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.
363
406
 
364
407
  ```python
365
408
  from xtr_orm import ConnectionRegistry, DatabaseManager
366
- from xtr_orm.command import use_connections
409
+ from xtr_orm.command import MigrationsMigrateCommand
367
410
 
368
411
  registry = ConnectionRegistry()
369
412
  registry.register("default", engine=engine, migrator=migrator, database=DatabaseManager(url))
370
- use_connections(registry)
413
+
414
+ migrate = MigrationsMigrateCommand(registry)
371
415
  ```
372
416
 
373
417
  ## Message bus middleware
@@ -449,6 +493,11 @@ every statement committed as it runs, and with every connection parameter of the
449
493
  database: the credentials, the host and the driver's query options, such as `ssl`. A SQLite
450
494
  database is its file. Any other server is refused with `UnsupportedDatabaseError`.
451
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
+
452
501
  ## Errors
453
502
 
454
503
  Every error derives from `OrmError`.
@@ -457,6 +506,7 @@ Every error derives from `OrmError`.
457
506
  |---|---|
458
507
  | `InvalidArgumentError` | A configuration cannot be used — an empty name, an unknown option, a URL option that does not read (also a `ValueError`) |
459
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`) |
460
510
  | `UnknownConnectionError` | A connection is asked for by a name nothing registered (also a `LookupError`) |
461
511
  | `UnsupportedDatabaseError` | A database is created or dropped on a server whose statements are not known |
462
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 = "2.0.0"
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>=2.0,<3",
33
+ "xtr-logging-contracts>=4.0,<5",
34
34
  ]
35
35
 
36
36
  [[project.authors]]
@@ -42,15 +42,18 @@ postgres = ["asyncpg>=0.29"]
42
42
  sqlite = ["aiosqlite>=0.20"]
43
43
  mysql = ["asyncmy>=0.2.9"]
44
44
  di = [
45
- "xtr-dependency-injection>=2.0,<3",
46
- "xtr-service-contracts>=2.0,<3",
45
+ "xtr-dependency-injection>=4.0,<5",
46
+ "xtr-service-contracts>=4.0,<5",
47
47
  ]
48
- console = ["xtr-console>=2.0,<3"]
49
- messenger = ["xtr-messenger>=2.0,<3"]
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"
53
53
 
54
+ [project.entry-points.xtr_recipes]
55
+ orm = "xtr_orm.recipe"
56
+
54
57
  [dependency-groups]
55
58
  dev = [
56
59
  "basedpyright>=1.21",
@@ -60,11 +63,11 @@ dev = [
60
63
  "anyio>=4.0",
61
64
  "ty>=0.0.83",
62
65
  "aiosqlite>=0.20",
63
- "xtr-console>=2.0,<3",
64
- "xtr-dependency-injection>=2.0,<3",
65
- "xtr-logging>=2.0,<3",
66
- "xtr-messenger>=2.0,<3",
67
- "xtr-service-contracts>=2.0,<3",
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",
68
71
  ]
69
72
 
70
73
  [build-system]
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "xtr-orm"
3
- version = "2.0.0"
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>=2.0,<3",
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>=2.0,<3",
44
- "xtr-service-contracts>=2.0,<3",
43
+ "xtr-dependency-injection>=4.0,<5",
44
+ "xtr-service-contracts>=4.0,<5",
45
45
  ]
46
46
  console = [
47
- "xtr-console>=2.0,<3",
47
+ "xtr-console>=4.0,<5",
48
48
  ]
49
49
  messenger = [
50
- "xtr-messenger>=2.0,<3",
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.
@@ -55,6 +55,10 @@ messenger = [
55
55
  [project.entry-points."xtr_dependency_injection.bundles"]
56
56
  orm = "xtr_orm.bundle:OrmBundle"
57
57
 
58
+ # Advertises the recipe — what *Use in an application* takes — for recipes:sync.
59
+ [project.entry-points."xtr_recipes"]
60
+ orm = "xtr_orm.recipe"
61
+
58
62
  [dependency-groups]
59
63
  dev = [
60
64
  "basedpyright>=1.21",
@@ -65,11 +69,11 @@ dev = [
65
69
  "ty>=0.0.83",
66
70
  # Every test runs on SQLite files under the test's own directory: no server.
67
71
  "aiosqlite>=0.20",
68
- "xtr-console>=2.0,<3",
69
- "xtr-dependency-injection>=2.0,<3",
70
- "xtr-logging>=2.0,<3",
71
- "xtr-messenger>=2.0,<3",
72
- "xtr-service-contracts>=2.0,<3",
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",
73
77
  ]
74
78
 
75
79
  [build-system]