xtr-orm 5.2.1__tar.gz → 6.0.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 (69) hide show
  1. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/PKG-INFO +37 -5
  2. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/README.md +35 -3
  3. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/pyproject.toml +13 -7
  4. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/pyproject.toml.orig +10 -7
  5. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/src/xtr_orm/.agents/skills/xtr-orm/SKILL.md +17 -0
  6. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/src/xtr_orm/bundle/orm_bundle.py +101 -6
  7. xtr_orm-6.0.0/src/xtr_orm/messenger_transport.py +24 -0
  8. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/LICENSE +0 -0
  9. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/src/xtr_orm/.agents/skills/xtr-orm/references/commands.md +0 -0
  10. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/src/xtr_orm/.agents/skills/xtr-orm/references/connections.md +0 -0
  11. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/src/xtr_orm/.agents/skills/xtr-orm/references/messenger.md +0 -0
  12. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/src/xtr_orm/.agents/skills/xtr-orm/references/migrations.md +0 -0
  13. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/src/xtr_orm/__init__.py +0 -0
  14. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/src/xtr_orm/bundle/__init__.py +0 -0
  15. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/src/xtr_orm/bundle/connection_config.py +0 -0
  16. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/src/xtr_orm/bundle/orm_config.py +0 -0
  17. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/src/xtr_orm/bundle/orm_mappings_pass.py +0 -0
  18. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/src/xtr_orm/command/__init__.py +0 -0
  19. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/src/xtr_orm/command/_reporting.py +0 -0
  20. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/src/xtr_orm/command/connection_command.py +0 -0
  21. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/src/xtr_orm/command/database_create_command.py +0 -0
  22. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/src/xtr_orm/command/database_drop_command.py +0 -0
  23. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/src/xtr_orm/command/migrations_current_command.py +0 -0
  24. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/src/xtr_orm/command/migrations_diff_command.py +0 -0
  25. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/src/xtr_orm/command/migrations_dump_schema_command.py +0 -0
  26. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/src/xtr_orm/command/migrations_execute_command.py +0 -0
  27. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/src/xtr_orm/command/migrations_generate_command.py +0 -0
  28. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/src/xtr_orm/command/migrations_latest_command.py +0 -0
  29. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/src/xtr_orm/command/migrations_list_command.py +0 -0
  30. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/src/xtr_orm/command/migrations_migrate_command.py +0 -0
  31. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/src/xtr_orm/command/migrations_rollup_command.py +0 -0
  32. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/src/xtr_orm/command/migrations_status_command.py +0 -0
  33. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/src/xtr_orm/command/migrations_up_to_date_command.py +0 -0
  34. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/src/xtr_orm/command/migrations_version_command.py +0 -0
  35. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/src/xtr_orm/command/run_sql_command.py +0 -0
  36. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/src/xtr_orm/connection_registry.py +0 -0
  37. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/src/xtr_orm/database/__init__.py +0 -0
  38. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/src/xtr_orm/database/database_manager.py +0 -0
  39. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/src/xtr_orm/exception/__init__.py +0 -0
  40. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/src/xtr_orm/exception/invalid_argument_error.py +0 -0
  41. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/src/xtr_orm/exception/migration_error.py +0 -0
  42. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/src/xtr_orm/exception/missing_database_error.py +0 -0
  43. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/src/xtr_orm/exception/orm_error.py +0 -0
  44. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/src/xtr_orm/exception/session_unavailable_error.py +0 -0
  45. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/src/xtr_orm/exception/unknown_connection_error.py +0 -0
  46. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/src/xtr_orm/exception/unsupported_database_error.py +0 -0
  47. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/src/xtr_orm/mapping_driver_interface.py +0 -0
  48. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/src/xtr_orm/messenger/__init__.py +0 -0
  49. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/src/xtr_orm/messenger/close_connection_middleware.py +0 -0
  50. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/src/xtr_orm/messenger/open_transaction_logger_middleware.py +0 -0
  51. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/src/xtr_orm/messenger/transaction_middleware.py +0 -0
  52. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/src/xtr_orm/migrations/__init__.py +0 -0
  53. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/src/xtr_orm/migrations/_environment.py +0 -0
  54. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/src/xtr_orm/migrations/_history.py +0 -0
  55. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/src/xtr_orm/migrations/_revisions.py +0 -0
  56. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/src/xtr_orm/migrations/_schema.py +0 -0
  57. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/src/xtr_orm/migrations/available_migration.py +0 -0
  58. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/src/xtr_orm/migrations/direction.py +0 -0
  59. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/src/xtr_orm/migrations/executed_migration.py +0 -0
  60. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/src/xtr_orm/migrations/execution_result.py +0 -0
  61. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/src/xtr_orm/migrations/migration_plan.py +0 -0
  62. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/src/xtr_orm/migrations/migration_status.py +0 -0
  63. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/src/xtr_orm/migrations/migrations_config.py +0 -0
  64. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/src/xtr_orm/migrations/migrator.py +0 -0
  65. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/src/xtr_orm/migrations/template/script.py.mako +0 -0
  66. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/src/xtr_orm/py.typed +0 -0
  67. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/src/xtr_orm/recipe/__init__.py +0 -0
  68. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/src/xtr_orm/recipe/files/config/orm.py.tmpl +0 -0
  69. {xtr_orm-5.2.1 → xtr_orm-6.0.0}/src/xtr_orm/recipe/manifest.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: xtr-orm
3
- Version: 5.2.1
3
+ Version: 6.0.0
4
4
  Summary: Databases for async applications: engines, sessions and versioned schema migrations, configured once per connection.
5
5
  Keywords: orm,database,migrations,sqlalchemy,alembic,asyncio
6
6
  Author: Xterr
@@ -20,7 +20,7 @@ Requires-Dist: advanced-alchemy>=1.11
20
20
  Requires-Dist: alembic>=1.20
21
21
  Requires-Dist: sqlalchemy[asyncio]>=2.0.41
22
22
  Requires-Dist: typing-extensions>=4.4
23
- Requires-Dist: xtr-logging-contracts>=5.0,<6
23
+ Requires-Dist: xtr-logging-contracts>=6.0,<7
24
24
  Requires-Dist: asyncmy>=0.2.9 ; extra == 'mysql'
25
25
  Requires-Dist: asyncpg>=0.29 ; extra == 'postgres'
26
26
  Requires-Dist: aiosqlite>=0.20 ; extra == 'sqlite'
@@ -75,6 +75,7 @@ uv add "xtr-orm[postgres]" # asyncpg
75
75
  uv add "xtr-orm[postgres]" xtr-dependency-injection # + the container, for the OrmBundle
76
76
  uv add xtr-console # beside it: the orm:* commands
77
77
  uv add xtr-messenger # beside it: the message bus middleware
78
+ uv add "xtr-messenger[orm]" # beside it: the orm:// transport too
78
79
  ```
79
80
 
80
81
  | Extra | Brings | For |
@@ -86,7 +87,8 @@ sessions, migrators from one configuration — is used when
86
87
  [xtr-dependency-injection](../xtr-dependency-injection) is installed beside the package, and a
87
88
  `Migrator` works without it, as the [Quick start](#quick-start) shows. Installed beside it too,
88
89
  [xtr-console](../xtr-console) brings the `orm:*` commands and [xtr-messenger](../xtr-messenger)
89
- the [message bus middleware](#message-bus-middleware).
90
+ the [message bus middleware](#message-bus-middleware); with
91
+ [xtr-orm-messenger](../xtr-orm-messenger) as well, the [`orm://` transport](#message-bus-transport).
90
92
 
91
93
  ## Quick start
92
94
 
@@ -201,7 +203,8 @@ removing it undoes.
201
203
  - **Install** — `uv add "xtr-orm[postgres]" xtr-dependency-injection`: the bundle needs the
202
204
  container installed beside it, never as an extra; `mysql` or `sqlite` instead of `postgres` for
203
205
  those databases. Install xtr-console beside it for the `orm:*` commands, and xtr-messenger for
204
- the [message bus middleware](#message-bus-middleware).
206
+ the [message bus middleware](#message-bus-middleware) — and xtr-orm-messenger, or
207
+ `xtr-messenger[orm]`, for the [`orm://` transport](#message-bus-transport).
205
208
  - **Recipe** — `uv run xtr-recipes recipes:sync` does the *Activate*, *Configure* and
206
209
  *Environment* steps below: it lists `OrmBundle`, writes a starting `<app>/config/packages/orm.py` with the
207
210
  `default` connection, and `DATABASE_URL` in `.env`, and adds a PostgreSQL `database` service to
@@ -211,7 +214,8 @@ removing it undoes.
211
214
  `xtr_orm.bundle`.
212
215
  - **Brings along** — the logging and console bundles, when those packages are installed.
213
216
  With the messenger bundle active too, the [middleware](#message-bus-middleware) are
214
- registered for a bus to list.
217
+ registered for a bus to list, and with xtr-orm-messenger installed the
218
+ [`orm://` transport](#message-bus-transport) too.
215
219
  - **Configure** — the recipe writes a starting `<app>/config/packages/orm.py` with the `default`
216
220
  connection reading `DATABASE_URL`, its revisions in `migrations` in the project directory and
217
221
  diffs against the models advanced-alchemy registers. Add connections, replicas and table
@@ -532,6 +536,34 @@ Without a container, build them with a `ConnectionRegistry` whose connections ar
532
536
  with a `session` — what returns the session the handlers use — and put the instances in the
533
537
  configuration.
534
538
 
539
+ ## Message bus transport
540
+
541
+ With the messenger bundle active and [xtr-orm-messenger](../xtr-orm-messenger) installed —
542
+ `uv add "xtr-messenger[orm]"` — a bus can keep its messages in a table on one of these
543
+ connections. There is no bundle to list: this one registers the transport factory with the
544
+ messenger, built on the connections, once it finds the package installed.
545
+
546
+ ```python
547
+ # app/config/packages/messenger.py
548
+ MessageBusConfig(
549
+ transports={"jobs": TransportConfig("orm://default", queue="jobs")},
550
+ routing={IngestDocument: "jobs"},
551
+ )
552
+ ```
553
+
554
+ - **The DSN names a connection.** `orm://default` is the `default` connection, `orm://` the
555
+ default one, whatever it is called; its URL stays in `OrmConfig`.
556
+ - **Migrations write the table.** Each connection's migrator adds the messages table of every
557
+ `orm://` transport on it to its table definitions, so `orm:migrations:diff` writes it like
558
+ one of the application's own tables. The bus configuration is read for that when a migrator
559
+ is first built, so the environment variables its DSNs name must be set then. A connection
560
+ keeping several sets of table definitions is refused, naming it.
561
+ - **Until the revision is applied**, the transport creates its table the first time it runs;
562
+ `auto_setup=false` on the DSN leaves it to the migrations alone.
563
+
564
+ Options, workers and notifications are the transport's: see
565
+ [xtr-orm-messenger](../xtr-orm-messenger).
566
+
535
567
  ## Creating and dropping databases
536
568
 
537
569
  `DatabaseManager(url)` creates and drops the database a URL names. A database cannot be
@@ -43,6 +43,7 @@ uv add "xtr-orm[postgres]" # asyncpg
43
43
  uv add "xtr-orm[postgres]" xtr-dependency-injection # + the container, for the OrmBundle
44
44
  uv add xtr-console # beside it: the orm:* commands
45
45
  uv add xtr-messenger # beside it: the message bus middleware
46
+ uv add "xtr-messenger[orm]" # beside it: the orm:// transport too
46
47
  ```
47
48
 
48
49
  | Extra | Brings | For |
@@ -54,7 +55,8 @@ sessions, migrators from one configuration — is used when
54
55
  [xtr-dependency-injection](../xtr-dependency-injection) is installed beside the package, and a
55
56
  `Migrator` works without it, as the [Quick start](#quick-start) shows. Installed beside it too,
56
57
  [xtr-console](../xtr-console) brings the `orm:*` commands and [xtr-messenger](../xtr-messenger)
57
- the [message bus middleware](#message-bus-middleware).
58
+ the [message bus middleware](#message-bus-middleware); with
59
+ [xtr-orm-messenger](../xtr-orm-messenger) as well, the [`orm://` transport](#message-bus-transport).
58
60
 
59
61
  ## Quick start
60
62
 
@@ -169,7 +171,8 @@ removing it undoes.
169
171
  - **Install** — `uv add "xtr-orm[postgres]" xtr-dependency-injection`: the bundle needs the
170
172
  container installed beside it, never as an extra; `mysql` or `sqlite` instead of `postgres` for
171
173
  those databases. Install xtr-console beside it for the `orm:*` commands, and xtr-messenger for
172
- the [message bus middleware](#message-bus-middleware).
174
+ the [message bus middleware](#message-bus-middleware) — and xtr-orm-messenger, or
175
+ `xtr-messenger[orm]`, for the [`orm://` transport](#message-bus-transport).
173
176
  - **Recipe** — `uv run xtr-recipes recipes:sync` does the *Activate*, *Configure* and
174
177
  *Environment* steps below: it lists `OrmBundle`, writes a starting `<app>/config/packages/orm.py` with the
175
178
  `default` connection, and `DATABASE_URL` in `.env`, and adds a PostgreSQL `database` service to
@@ -179,7 +182,8 @@ removing it undoes.
179
182
  `xtr_orm.bundle`.
180
183
  - **Brings along** — the logging and console bundles, when those packages are installed.
181
184
  With the messenger bundle active too, the [middleware](#message-bus-middleware) are
182
- registered for a bus to list.
185
+ registered for a bus to list, and with xtr-orm-messenger installed the
186
+ [`orm://` transport](#message-bus-transport) too.
183
187
  - **Configure** — the recipe writes a starting `<app>/config/packages/orm.py` with the `default`
184
188
  connection reading `DATABASE_URL`, its revisions in `migrations` in the project directory and
185
189
  diffs against the models advanced-alchemy registers. Add connections, replicas and table
@@ -500,6 +504,34 @@ Without a container, build them with a `ConnectionRegistry` whose connections ar
500
504
  with a `session` — what returns the session the handlers use — and put the instances in the
501
505
  configuration.
502
506
 
507
+ ## Message bus transport
508
+
509
+ With the messenger bundle active and [xtr-orm-messenger](../xtr-orm-messenger) installed —
510
+ `uv add "xtr-messenger[orm]"` — a bus can keep its messages in a table on one of these
511
+ connections. There is no bundle to list: this one registers the transport factory with the
512
+ messenger, built on the connections, once it finds the package installed.
513
+
514
+ ```python
515
+ # app/config/packages/messenger.py
516
+ MessageBusConfig(
517
+ transports={"jobs": TransportConfig("orm://default", queue="jobs")},
518
+ routing={IngestDocument: "jobs"},
519
+ )
520
+ ```
521
+
522
+ - **The DSN names a connection.** `orm://default` is the `default` connection, `orm://` the
523
+ default one, whatever it is called; its URL stays in `OrmConfig`.
524
+ - **Migrations write the table.** Each connection's migrator adds the messages table of every
525
+ `orm://` transport on it to its table definitions, so `orm:migrations:diff` writes it like
526
+ one of the application's own tables. The bus configuration is read for that when a migrator
527
+ is first built, so the environment variables its DSNs name must be set then. A connection
528
+ keeping several sets of table definitions is refused, naming it.
529
+ - **Until the revision is applied**, the transport creates its table the first time it runs;
530
+ `auto_setup=false` on the DSN leaves it to the migrations alone.
531
+
532
+ Options, workers and notifications are the transport's: see
533
+ [xtr-orm-messenger](../xtr-orm-messenger).
534
+
503
535
  ## Creating and dropping databases
504
536
 
505
537
  `DatabaseManager(url)` creates and drops the database a URL names. A database cannot be
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "xtr-orm"
3
- version = "5.2.1"
3
+ version = "6.0.0"
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>=5.0,<6",
33
+ "xtr-logging-contracts>=6.0,<7",
34
34
  ]
35
35
 
36
36
  [[project.authors]]
@@ -57,11 +57,12 @@ dev = [
57
57
  "anyio>=4.0",
58
58
  "ty>=0.0.83",
59
59
  "aiosqlite>=0.20",
60
- "xtr-console>=5.0,<6",
61
- "xtr-dependency-injection>=5.0,<6",
62
- "xtr-logging>=5.0,<6",
63
- "xtr-messenger>=5.0,<6",
64
- "xtr-service-contracts>=5.0,<6",
60
+ "xtr-console>=6.0,<7",
61
+ "xtr-dependency-injection>=6.0,<7",
62
+ "xtr-logging>=6.0,<7",
63
+ "xtr-messenger>=6.0,<7",
64
+ "xtr-orm-messenger>=6.0,<7",
65
+ "xtr-service-contracts>=6.0,<7",
65
66
  ]
66
67
 
67
68
  [build-system]
@@ -143,6 +144,11 @@ unfixable = []
143
144
  "TC002",
144
145
  "TC003",
145
146
  ]
147
+ "src/xtr_orm/messenger_transport.py" = [
148
+ "TC001",
149
+ "TC002",
150
+ "TC003",
151
+ ]
146
152
  "tests/**/*.py" = [
147
153
  "S101",
148
154
  "ARG",
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "xtr-orm"
3
- version = "5.2.1"
3
+ version = "6.0.0"
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>=5.0,<6",
29
+ "xtr-logging-contracts>=6.0,<7",
30
30
  ]
31
31
 
32
32
  [project.optional-dependencies]
@@ -59,11 +59,12 @@ dev = [
59
59
  "ty>=0.0.83",
60
60
  # Every test runs on SQLite files under the test's own directory: no server.
61
61
  "aiosqlite>=0.20",
62
- "xtr-console>=5.0,<6",
63
- "xtr-dependency-injection>=5.0,<6",
64
- "xtr-logging>=5.0,<6",
65
- "xtr-messenger>=5.0,<6",
66
- "xtr-service-contracts>=5.0,<6",
62
+ "xtr-console>=6.0,<7",
63
+ "xtr-dependency-injection>=6.0,<7",
64
+ "xtr-logging>=6.0,<7",
65
+ "xtr-messenger>=6.0,<7",
66
+ "xtr-orm-messenger>=6.0,<7",
67
+ "xtr-service-contracts>=6.0,<7",
67
68
  ]
68
69
 
69
70
  [build-system]
@@ -147,6 +148,8 @@ unfixable = []
147
148
  # runtime, and the container resolves its config's fields; deferring these
148
149
  # would register the factories under nothing.
149
150
  "src/xtr_orm/bundle/*.py" = ["TC001", "TC002", "TC003"]
151
+ # The same for the orm:// transport factory the bundle registers.
152
+ "src/xtr_orm/messenger_transport.py" = ["TC001", "TC002", "TC003"]
150
153
  "tests/**/*.py" = [
151
154
  "S101", # assert is the point of pytest
152
155
  "ARG", # fixtures look unused
@@ -317,6 +317,23 @@ A handler asks for `Injected[AsyncSession]` like any service and adds rows; it n
317
317
  More, including building the middleware without a container:
318
318
  [references/messenger.md](references/messenger.md).
319
319
 
320
+ ## A queue in a table
321
+
322
+ With the messenger bundle active and xtr-orm-messenger installed (`uv add "xtr-messenger[orm]"`),
323
+ this bundle registers the `orm://` transport on its connections — no other bundle to list.
324
+
325
+ ```python
326
+ # <app>/config/packages/messenger.py
327
+ MessageBusConfig(
328
+ transports={"jobs": TransportConfig("orm://default", queue="jobs")},
329
+ routing={IngestDocument: "jobs"},
330
+ )
331
+ ```
332
+
333
+ Each connection's migrator adds the messages table of every `orm://` transport on it, so
334
+ `orm:migrations:diff` writes it. The transport itself — options, workers, notifications — is
335
+ the `xtr-orm-messenger` skill's.
336
+
320
337
  ## Creating and dropping a database
321
338
 
322
339
  ```python
@@ -21,14 +21,20 @@ disposed when the container closes. When the console bundle is active, the
21
21
  ``orm_open_transaction_logger`` middleware are, for a bus to list; when the
22
22
  logging bundle is, an ``orm`` channel is added and every migrator logs there.
23
23
 
24
+ With the messenger bundle active and xtr-orm-messenger installed, the
25
+ ``orm://`` transport is registered with the messenger too, built on these
26
+ connections, and each connection's migrator writes the messages tables of the
27
+ ``orm://`` transports on it.
28
+
24
29
  Nothing connects until a service is first used, and a URL given as
25
30
  ``env(...)`` is read only then.
26
31
  """
27
32
 
28
33
  from __future__ import annotations
29
34
 
30
- from collections.abc import AsyncIterator, Awaitable, Callable
35
+ from collections.abc import AsyncIterator, Awaitable, Callable, Sequence
31
36
  from functools import partial
37
+ from importlib.util import find_spec
32
38
  from operator import contains
33
39
  from typing import Final, TypeVar, cast, final
34
40
 
@@ -37,6 +43,7 @@ from advanced_alchemy.config import AsyncSessionConfig, EngineConfig, SQLAlchemy
37
43
  from advanced_alchemy.config.routing import EngineConfig as ReplicaEngine
38
44
  from advanced_alchemy.config.routing import RoutingConfig, RoutingStrategy
39
45
  from advanced_alchemy.routing import RoutingAsyncSessionMaker, stick_to_primary_var
46
+ from sqlalchemy import MetaData
40
47
  from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker
41
48
  from typing_extensions import override
42
49
  from xtr_dependency_injection import (
@@ -48,6 +55,7 @@ from xtr_dependency_injection import (
48
55
  current_unit_of_work,
49
56
  named_factory,
50
57
  optional_service,
58
+ qualified_name,
51
59
  required_bundle,
52
60
  )
53
61
  from xtr_logging_contracts import LoggerInterface
@@ -55,7 +63,7 @@ from xtr_service_contracts import ContainerInterface
55
63
 
56
64
  from xtr_orm.connection_registry import ConnectionRegistry
57
65
  from xtr_orm.database import DatabaseManager
58
- from xtr_orm.exception import SessionUnavailableError
66
+ from xtr_orm.exception import InvalidArgumentError, SessionUnavailableError
59
67
  from xtr_orm.migrations import MigrationsConfig, Migrator
60
68
 
61
69
  from .connection_config import ConnectionConfig
@@ -71,6 +79,9 @@ _SessionMaker = type[async_sessionmaker[AsyncSession]] | type[RoutingAsyncSessio
71
79
  ORM_CHANNEL: Final = "orm"
72
80
  """The logging channel migrators write to."""
73
81
 
82
+ _MESSENGER_TRANSPORT: Final = "xtr_orm_messenger"
83
+ """The package of the ``orm://`` transport, registered with the messenger when installed."""
84
+
74
85
  _PER_CONNECTION: Final[tuple[type, ...]] = (
75
86
  SQLAlchemyAsyncConfig,
76
87
  AsyncEngine,
@@ -108,6 +119,8 @@ class OrmBundle(Bundle[OrmConfig]):
108
119
  builder: ContainerBuilder,
109
120
  ) -> None:
110
121
  """Register every connection's services under its name, the registry, the commands."""
122
+ messenger = bundle_active(builder, "messenger")
123
+ transport = messenger and find_spec(_MESSENGER_TRANSPORT) is not None
111
124
  for name, connection in config.connections.items():
112
125
  maker_type: _SessionMaker = (
113
126
  RoutingAsyncSessionMaker
@@ -123,7 +136,15 @@ class OrmBundle(Bundle[OrmConfig]):
123
136
  else _session_maker_factory(name),
124
137
  "session_maker",
125
138
  ),
126
- (_migrator_factory(name, connection), "migrator"),
139
+ (
140
+ _migrator_factory(
141
+ name,
142
+ connection,
143
+ default=config.default_connection,
144
+ messages_tables=transport,
145
+ ),
146
+ "migrator",
147
+ ),
127
148
  (_database_factory(connection), "database"),
128
149
  )
129
150
  for factory, kind in factories:
@@ -148,8 +169,37 @@ class OrmBundle(Bundle[OrmConfig]):
148
169
 
149
170
  if bundle_active(builder, "console"):
150
171
  services.load("xtr_orm.command")
151
- if bundle_active(builder, "messenger"):
172
+ if messenger:
152
173
  services.load("xtr_orm.messenger")
174
+ if transport:
175
+ _register_orm_transport(services)
176
+
177
+
178
+ def _register_orm_transport(services: ServiceConfigurator) -> None:
179
+ """Register the ``orm://`` transport factory with the messenger, built on these connections.
180
+
181
+ Registered under the messenger's transport factory interface and tagged
182
+ with its transport factory tag, which is how the messenger consults a
183
+ factory built from services ahead of the ones it discovers by DSN scheme.
184
+ """
185
+ # Optional peers: importable only once the messenger bundle is active and
186
+ # the transport installed, which the caller checked.
187
+ from xtr_messenger import TransportFactoryInterface # noqa: PLC0415 — optional peer
188
+ from xtr_messenger.bundle import TRANSPORT_FACTORY_TAG # noqa: PLC0415 — optional peer
189
+
190
+ from xtr_orm.messenger_transport import ( # noqa: PLC0415 — needs the optional peers
191
+ OrmTransportFactory,
192
+ orm_transport_factory,
193
+ )
194
+
195
+ qualifier = qualified_name(OrmTransportFactory)
196
+ _ = services.set(orm_transport_factory, qualifier=qualifier).add_tag(TRANSPORT_FACTORY_TAG)
197
+ services.alias(
198
+ TransportFactoryInterface,
199
+ OrmTransportFactory,
200
+ alias_qualifier=qualifier,
201
+ target_qualifier=qualifier,
202
+ )
153
203
 
154
204
 
155
205
  @final
@@ -291,9 +341,14 @@ def _session_factory(
291
341
 
292
342
 
293
343
  def _migrator_factory(
294
- name: str, connection: ConnectionConfig
344
+ name: str, connection: ConnectionConfig, *, default: str, messages_tables: bool
295
345
  ) -> Callable[[ContainerInterface], Awaitable[Migrator]]:
296
- """Build the factory of the connection ``name``'s migrator, logging to ``orm`` when it can."""
346
+ """Build the factory of the connection ``name``'s migrator, logging to ``orm`` when it can.
347
+
348
+ With ``messages_tables``, the messages tables of the ``orm://`` transports
349
+ on this connection join its table definitions first, so a diff writes them
350
+ like any of the application's own tables.
351
+ """
297
352
 
298
353
  async def migrator(container: ContainerInterface) -> Migrator:
299
354
  resolved = connection.with_url_options()
@@ -302,6 +357,8 @@ def _migrator_factory(
302
357
  if resolved.metadata is not None
303
358
  else metadata_registry.get(resolved.bind_key)
304
359
  )
360
+ if messages_tables:
361
+ await _map_messages_tables(container, name, default, metadata)
305
362
  built = Migrator(
306
363
  await container.get(AsyncEngine, name),
307
364
  resolved.migrations or MigrationsConfig(),
@@ -316,6 +373,44 @@ def _migrator_factory(
316
373
  return migrator
317
374
 
318
375
 
376
+ async def _map_messages_tables(
377
+ container: ContainerInterface,
378
+ name: str,
379
+ default: str,
380
+ metadata: MetaData | Sequence[MetaData],
381
+ ) -> None:
382
+ """Add the messages table of every ``orm://`` transport on the connection ``name``.
383
+
384
+ The transports are read from the bus configuration as the application
385
+ resolved it, so a DSN given as ``env(...)`` is read here — when a migrator
386
+ is first built.
387
+
388
+ Raises:
389
+ InvalidArgumentError: When a transport keeps a table on a connection
390
+ that keeps several sets of table definitions.
391
+ """
392
+ # Optional peers: importable only once the messenger bundle is active and
393
+ # the transport installed, which the bundle checked.
394
+ from xtr_messenger import MessageBusConfig # noqa: PLC0415 — optional peer
395
+ from xtr_orm_messenger import ( # noqa: PLC0415 — optional peer
396
+ MessagesTableDriver,
397
+ messages_tables,
398
+ )
399
+
400
+ bus = await optional_service(container, MessageBusConfig)
401
+ tables = messages_tables(bus, name, default) if bus is not None else ()
402
+ if not tables:
403
+ return
404
+ if not isinstance(metadata, MetaData):
405
+ raise InvalidArgumentError(
406
+ f'The "{name}" connection keeps several sets of table definitions; the messages '
407
+ f"tables of its orm:// transports need the one they belong to. Give that connection "
408
+ f"a single MetaData.",
409
+ )
410
+ for table in tables:
411
+ _ = MessagesTableDriver(table).load_metadata(metadata)
412
+
413
+
319
414
  def _database_factory(connection: ConnectionConfig) -> Callable[[], DatabaseManager]:
320
415
  """Build the factory of ``connection``'s database manager."""
321
416
 
@@ -0,0 +1,24 @@
1
+ """Building the ``orm://`` transport factory from the container's services.
2
+
3
+ The orm bundle registers it when the messenger bundle is active and
4
+ xtr-orm-messenger is installed: the factory needs this package's connections,
5
+ which only this package's bundle holds. Importing this module therefore needs
6
+ both xtr-messenger and xtr-orm-messenger; the bundle imports it only once it
7
+ has found them, and nothing scans it.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from xtr_messenger import SerializerInterface
13
+ from xtr_orm_messenger import OrmTransportFactory
14
+
15
+ from xtr_orm.connection_registry import ConnectionRegistry
16
+
17
+ __all__ = ["OrmTransportFactory", "orm_transport_factory"]
18
+
19
+
20
+ def orm_transport_factory(
21
+ registry: ConnectionRegistry, serializer: SerializerInterface
22
+ ) -> OrmTransportFactory:
23
+ """Build the factory on the application's connections, with the messenger's serializer."""
24
+ return OrmTransportFactory(registry, serializer)
File without changes
File without changes
File without changes