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.
- {xtr_orm-2.0.0 → xtr_orm-4.0.2}/LICENSE +4 -0
- xtr_orm-2.0.0/README.md → xtr_orm-4.0.2/PKG-INFO +135 -7
- xtr_orm-2.0.0/PKG-INFO → xtr_orm-4.0.2/README.md +96 -46
- {xtr_orm-2.0.0 → xtr_orm-4.0.2}/pyproject.toml +14 -11
- {xtr_orm-2.0.0 → xtr_orm-4.0.2}/pyproject.toml.orig +15 -11
- xtr_orm-4.0.2/src/xtr_orm/.agents/skills/xtr-orm/SKILL.md +423 -0
- xtr_orm-4.0.2/src/xtr_orm/.agents/skills/xtr-orm/references/commands.md +72 -0
- xtr_orm-4.0.2/src/xtr_orm/.agents/skills/xtr-orm/references/connections.md +97 -0
- xtr_orm-4.0.2/src/xtr_orm/.agents/skills/xtr-orm/references/messenger.md +95 -0
- xtr_orm-4.0.2/src/xtr_orm/.agents/skills/xtr-orm/references/migrations.md +71 -0
- {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/__init__.py +16 -1
- {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/bundle/__init__.py +9 -1
- {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/bundle/connection_config.py +32 -2
- {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/bundle/orm_bundle.py +50 -33
- xtr_orm-4.0.2/src/xtr_orm/bundle/orm_mappings_pass.py +210 -0
- {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/command/__init__.py +7 -6
- 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
- xtr_orm-4.0.2/src/xtr_orm/command/connection_command.py +66 -0
- {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/command/migrations_current_command.py +2 -1
- {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/command/migrations_diff_command.py +2 -1
- {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/command/migrations_dump_schema_command.py +2 -1
- {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/command/migrations_execute_command.py +2 -8
- {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/command/migrations_generate_command.py +2 -1
- {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/command/migrations_latest_command.py +2 -1
- {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/command/migrations_migrate_command.py +2 -2
- {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/command/migrations_up_to_date_command.py +2 -1
- {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/connection_registry.py +8 -1
- {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/database/database_manager.py +18 -3
- {xtr_orm-2.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-4.0.2/src/xtr_orm/recipe/__init__.py +15 -0
- xtr_orm-4.0.2/src/xtr_orm/recipe/files/config/orm.py.tmpl +24 -0
- xtr_orm-4.0.2/src/xtr_orm/recipe/manifest.toml +32 -0
- xtr_orm-2.0.0/src/xtr_orm/command/connections.py +0 -37
- {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/bundle/orm_config.py +0 -0
- {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/command/database_create_command.py +0 -0
- {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/command/database_drop_command.py +0 -0
- {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/command/migrations_list_command.py +0 -0
- {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/command/migrations_rollup_command.py +0 -0
- {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/command/migrations_status_command.py +0 -0
- {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/command/migrations_version_command.py +0 -0
- {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/command/run_sql_command.py +0 -0
- {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/database/__init__.py +0 -0
- {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/exception/invalid_argument_error.py +0 -0
- {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/exception/migration_error.py +0 -0
- {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/exception/orm_error.py +0 -0
- {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/exception/session_unavailable_error.py +0 -0
- {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/exception/unknown_connection_error.py +0 -0
- {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/exception/unsupported_database_error.py +0 -0
- {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/messenger/__init__.py +0 -0
- {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/messenger/close_connection_middleware.py +0 -0
- {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/messenger/open_transaction_logger_middleware.py +0 -0
- {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/messenger/transaction_middleware.py +0 -0
- {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/migrations/__init__.py +0 -0
- {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/migrations/_environment.py +0 -0
- {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/migrations/_history.py +0 -0
- {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/migrations/_revisions.py +0 -0
- {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/migrations/_schema.py +0 -0
- {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/migrations/available_migration.py +0 -0
- {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/migrations/direction.py +0 -0
- {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/migrations/executed_migration.py +0 -0
- {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/migrations/execution_result.py +0 -0
- {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/migrations/migration_plan.py +0 -0
- {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/migrations/migration_status.py +0 -0
- {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/migrations/migrations_config.py +0 -0
- {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/migrations/migrator.py +0 -0
- {xtr_orm-2.0.0 → xtr_orm-4.0.2}/src/xtr_orm/migrations/template/script.py.mako +0 -0
- {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** —
|
|
172
|
-
`DATABASE_URL`,
|
|
173
|
-
against the models advanced-alchemy registers.
|
|
174
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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** —
|
|
211
|
-
`DATABASE_URL`,
|
|
212
|
-
against the models advanced-alchemy registers.
|
|
213
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 = "
|
|
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,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>=
|
|
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"
|
|
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>=
|
|
64
|
-
"xtr-dependency-injection>=
|
|
65
|
-
"xtr-logging>=
|
|
66
|
-
"xtr-messenger>=
|
|
67
|
-
"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",
|
|
68
71
|
]
|
|
69
72
|
|
|
70
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.
|
|
@@ -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>=
|
|
69
|
-
"xtr-dependency-injection>=
|
|
70
|
-
"xtr-logging>=
|
|
71
|
-
"xtr-messenger>=
|
|
72
|
-
"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",
|
|
73
77
|
]
|
|
74
78
|
|
|
75
79
|
[build-system]
|