xtr-orm 1.5.0__py3-none-any.whl

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 (57) hide show
  1. xtr_orm/__init__.py +51 -0
  2. xtr_orm/bundle/__init__.py +11 -0
  3. xtr_orm/bundle/connection_config.py +250 -0
  4. xtr_orm/bundle/orm_bundle.py +346 -0
  5. xtr_orm/bundle/orm_config.py +101 -0
  6. xtr_orm/command/__init__.py +56 -0
  7. xtr_orm/command/connection_command.py +134 -0
  8. xtr_orm/command/connections.py +37 -0
  9. xtr_orm/command/database_create_command.py +53 -0
  10. xtr_orm/command/database_drop_command.py +65 -0
  11. xtr_orm/command/migrations_current_command.py +44 -0
  12. xtr_orm/command/migrations_diff_command.py +60 -0
  13. xtr_orm/command/migrations_dump_schema_command.py +56 -0
  14. xtr_orm/command/migrations_execute_command.py +86 -0
  15. xtr_orm/command/migrations_generate_command.py +47 -0
  16. xtr_orm/command/migrations_latest_command.py +44 -0
  17. xtr_orm/command/migrations_list_command.py +71 -0
  18. xtr_orm/command/migrations_migrate_command.py +108 -0
  19. xtr_orm/command/migrations_rollup_command.py +43 -0
  20. xtr_orm/command/migrations_status_command.py +66 -0
  21. xtr_orm/command/migrations_up_to_date_command.py +83 -0
  22. xtr_orm/command/migrations_version_command.py +89 -0
  23. xtr_orm/command/run_sql_command.py +77 -0
  24. xtr_orm/connection_registry.py +192 -0
  25. xtr_orm/database/__init__.py +7 -0
  26. xtr_orm/database/database_manager.py +196 -0
  27. xtr_orm/exception/__init__.py +19 -0
  28. xtr_orm/exception/invalid_argument_error.py +28 -0
  29. xtr_orm/exception/migration_error.py +28 -0
  30. xtr_orm/exception/orm_error.py +14 -0
  31. xtr_orm/exception/session_unavailable_error.py +29 -0
  32. xtr_orm/exception/unknown_connection_error.py +28 -0
  33. xtr_orm/exception/unsupported_database_error.py +29 -0
  34. xtr_orm/messenger/__init__.py +23 -0
  35. xtr_orm/messenger/close_connection_middleware.py +75 -0
  36. xtr_orm/messenger/open_transaction_logger_middleware.py +90 -0
  37. xtr_orm/messenger/transaction_middleware.py +104 -0
  38. xtr_orm/migrations/__init__.py +29 -0
  39. xtr_orm/migrations/_environment.py +188 -0
  40. xtr_orm/migrations/_history.py +178 -0
  41. xtr_orm/migrations/_revisions.py +198 -0
  42. xtr_orm/migrations/_schema.py +66 -0
  43. xtr_orm/migrations/available_migration.py +27 -0
  44. xtr_orm/migrations/direction.py +14 -0
  45. xtr_orm/migrations/executed_migration.py +31 -0
  46. xtr_orm/migrations/execution_result.py +26 -0
  47. xtr_orm/migrations/migration_plan.py +26 -0
  48. xtr_orm/migrations/migration_status.py +48 -0
  49. xtr_orm/migrations/migrations_config.py +162 -0
  50. xtr_orm/migrations/migrator.py +680 -0
  51. xtr_orm/migrations/template/script.py.mako +26 -0
  52. xtr_orm/py.typed +0 -0
  53. xtr_orm-1.5.0.dist-info/METADATA +487 -0
  54. xtr_orm-1.5.0.dist-info/RECORD +57 -0
  55. xtr_orm-1.5.0.dist-info/WHEEL +4 -0
  56. xtr_orm-1.5.0.dist-info/entry_points.txt +3 -0
  57. xtr_orm-1.5.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,77 @@
1
+ """``orm:run-sql``: run a statement and show what it returned."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, cast, final
6
+
7
+ from sqlalchemy import Row, text
8
+ from sqlalchemy.exc import SQLAlchemyError
9
+ from xtr_console import ConsoleStyle, ExitCode, as_command, escape
10
+
11
+ from .connection_command import ConnectionCommand
12
+
13
+ __all__ = ["RunSqlCommand"]
14
+
15
+
16
+ @as_command("orm:run-sql")
17
+ @final
18
+ class RunSqlCommand(ConnectionCommand):
19
+ """Executes arbitrary SQL directly from the command line."""
20
+
21
+ __slots__ = ()
22
+
23
+ async def __call__(
24
+ self,
25
+ io: ConsoleStyle,
26
+ sql: str,
27
+ *,
28
+ force_fetch: bool = False,
29
+ connection: str | None = None,
30
+ ) -> int:
31
+ """Run the statement, committed, and show its rows or how many it changed.
32
+
33
+ Args:
34
+ io: Where the command writes.
35
+ sql: The statement to run.
36
+ force_fetch: Show the result as rows even when the statement
37
+ does not look like it returns any.
38
+ connection: The connection to run it on; the default one when
39
+ left out.
40
+ """
41
+ engine = await self._engine(io, connection)
42
+ if engine is None:
43
+ return ExitCode.FAILURE
44
+ columns: list[str] = []
45
+ rows: list[list[str]] = []
46
+ affected = 0
47
+ try:
48
+ async with engine.begin() as opened:
49
+ result = await opened.execute(text(sql))
50
+ if result.returns_rows:
51
+ columns = [str(column) for column in result.keys()] # noqa: SIM118 — a result's keys are its columns, not a dict's
52
+ rows = [[_cell(value) for value in _values(row)] for row in result.all()]
53
+ else:
54
+ affected = result.rowcount
55
+ except SQLAlchemyError as error:
56
+ io.error(escape(str(error)))
57
+ return ExitCode.FAILURE
58
+ if not columns and not force_fetch:
59
+ # A driver that cannot tell — after a schema change, say — reports -1.
60
+ if affected < 0:
61
+ io.success("The statement was executed.")
62
+ else:
63
+ io.success(f"{affected} row{'' if affected == 1 else 's'} affected.")
64
+ return ExitCode.SUCCESS
65
+ if not rows:
66
+ io.success("The query yielded an empty result set.")
67
+ return ExitCode.SUCCESS
68
+ io.table([escape(column) for column in columns], rows)
69
+ return ExitCode.SUCCESS
70
+
71
+
72
+ def _values(row: Row[Any]) -> tuple[object, ...]: # pyright: ignore[reportExplicitAny] -- a row of a text statement has no known column types
73
+ return tuple(cast("tuple[object, ...]", row))
74
+
75
+
76
+ def _cell(value: object) -> str:
77
+ return "NULL" if value is None else escape(str(value))
@@ -0,0 +1,192 @@
1
+ """Every configured connection by name, each piece built only when first asked for."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import TYPE_CHECKING, Final, TypeVar, cast, final
7
+
8
+ from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession
9
+
10
+ from xtr_orm.database import DatabaseManager
11
+ from xtr_orm.exception import (
12
+ InvalidArgumentError,
13
+ SessionUnavailableError,
14
+ UnknownConnectionError,
15
+ )
16
+ from xtr_orm.migrations import Migrator
17
+
18
+ if TYPE_CHECKING:
19
+ from collections.abc import Awaitable, Callable
20
+
21
+ __all__ = ["DEFAULT_CONNECTION", "ConnectionRegistry"]
22
+
23
+ DEFAULT_CONNECTION: Final = "default"
24
+ """The name of the connection used when none is named."""
25
+
26
+ _T = TypeVar("_T")
27
+
28
+
29
+ @dataclass(frozen=True, slots=True)
30
+ class _Connection:
31
+ engine: Callable[[], Awaitable[AsyncEngine]]
32
+ migrator: Callable[[], Awaitable[Migrator]]
33
+ database: Callable[[], Awaitable[DatabaseManager]]
34
+ session: Callable[[], Awaitable[AsyncSession]] | None
35
+ close: Callable[[], Awaitable[None]] | None
36
+
37
+
38
+ @final
39
+ class ConnectionRegistry:
40
+ """Every connection by name: its engine, its migrator and its database manager.
41
+
42
+ Each is given as the object itself, or as an async function building it —
43
+ called only when that piece is first asked for, so a command touching one
44
+ connection opens nothing of another's.
45
+
46
+ A connection may also say how to reach the session of the current unit
47
+ of work — the one every handler of a message shares — and how to close
48
+ it, releasing every connection it holds; the message bus middleware use
49
+ both.
50
+
51
+ ```python
52
+ registry = ConnectionRegistry()
53
+ registry.register(
54
+ "default",
55
+ engine=engine,
56
+ migrator=Migrator(engine, MigrationsConfig(directory="migrations")),
57
+ database=DatabaseManager(url),
58
+ )
59
+ ```
60
+ """
61
+
62
+ __slots__ = ("_connections", "_default")
63
+
64
+ _connections: dict[str, _Connection]
65
+ _default: str
66
+
67
+ def __init__(self, default: str = DEFAULT_CONNECTION) -> None:
68
+ """Hold connections, answering for ``default`` when none is named."""
69
+ self._connections = {}
70
+ self._default = default
71
+
72
+ @property
73
+ def default(self) -> str:
74
+ """The name of the connection used when none is named."""
75
+ return self._default
76
+
77
+ def register( # noqa: PLR0913 — one keyword per piece of a connection
78
+ self,
79
+ name: str,
80
+ *,
81
+ engine: AsyncEngine | Callable[[], Awaitable[AsyncEngine]],
82
+ migrator: Migrator | Callable[[], Awaitable[Migrator]],
83
+ database: DatabaseManager | Callable[[], Awaitable[DatabaseManager]],
84
+ session: Callable[[], Awaitable[AsyncSession]] | None = None,
85
+ close: Callable[[], Awaitable[None]] | None = None,
86
+ ) -> None:
87
+ """Add the connection ``name``, or replace it.
88
+
89
+ Args:
90
+ name: What the connection is known by.
91
+ engine: Its engine, or what builds it.
92
+ migrator: Its migrator, or what builds it.
93
+ database: Its database manager, or what builds it.
94
+ session: What returns the session of the unit of work under way;
95
+ without it, :meth:`session` is refused.
96
+ close: What closes the connection; its engine is disposed when
97
+ left out.
98
+
99
+ Raises:
100
+ InvalidArgumentError: When ``name`` is empty.
101
+ """
102
+ if not name:
103
+ raise InvalidArgumentError("A connection needs a non-empty name.")
104
+ self._connections[name] = _Connection(
105
+ engine=_provider(engine, AsyncEngine),
106
+ migrator=_provider(migrator, Migrator),
107
+ database=_provider(database, DatabaseManager),
108
+ session=session,
109
+ close=close,
110
+ )
111
+
112
+ def names(self) -> tuple[str, ...]:
113
+ """Every connection's name, in the order registered."""
114
+ return tuple(self._connections)
115
+
116
+ def has(self, name: str | None = None) -> bool:
117
+ """Whether the connection ``name`` — the default one when ``None`` — exists."""
118
+ return (name or self._default) in self._connections
119
+
120
+ async def engine(self, name: str | None = None) -> AsyncEngine:
121
+ """Return the engine of the connection ``name``, the default one when ``None``.
122
+
123
+ Raises:
124
+ UnknownConnectionError: When no connection has that name.
125
+ """
126
+ return await self._get(name).engine()
127
+
128
+ async def migrator(self, name: str | None = None) -> Migrator:
129
+ """Return the migrator of the connection ``name``, the default one when ``None``.
130
+
131
+ Raises:
132
+ UnknownConnectionError: When no connection has that name.
133
+ """
134
+ return await self._get(name).migrator()
135
+
136
+ async def database(self, name: str | None = None) -> DatabaseManager:
137
+ """Return the database manager of the connection ``name``, the default one when ``None``.
138
+
139
+ Raises:
140
+ UnknownConnectionError: When no connection has that name.
141
+ """
142
+ return await self._get(name).database()
143
+
144
+ async def session(self, name: str | None = None) -> AsyncSession:
145
+ """Return the session of the current unit of work on the connection ``name``.
146
+
147
+ Raises:
148
+ UnknownConnectionError: When no connection has that name.
149
+ SessionUnavailableError: When the connection was registered
150
+ without a way to reach one.
151
+ """
152
+ wanted = name or self._default
153
+ provide = self._get(name).session
154
+ if provide is None:
155
+ raise SessionUnavailableError(wanted, "it was registered without a session")
156
+ return await provide()
157
+
158
+ async def close(self, name: str | None = None) -> None:
159
+ """Close the connection ``name``: release every database connection its pools hold.
160
+
161
+ The engine stays usable; its next use connects again.
162
+
163
+ Raises:
164
+ UnknownConnectionError: When no connection has that name.
165
+ """
166
+ connection = self._get(name)
167
+ if connection.close is not None:
168
+ await connection.close()
169
+ return
170
+ await (await connection.engine()).dispose()
171
+
172
+ def _get(self, name: str | None) -> _Connection:
173
+ wanted = name or self._default
174
+ found = self._connections.get(wanted)
175
+ if found is None:
176
+ raise UnknownConnectionError(wanted, self.names())
177
+ return found
178
+
179
+
180
+ def _provider(
181
+ given: _T | Callable[[], Awaitable[_T]], kind: type[_T]
182
+ ) -> Callable[[], Awaitable[_T]]:
183
+ """Return an async function giving ``given``: itself when it is one, else wrapping it."""
184
+ if isinstance(given, kind):
185
+ instance = given
186
+
187
+ async def provide() -> _T:
188
+ return instance
189
+
190
+ return provide
191
+ # Anything not of the built kind is the function building it, as the signature says.
192
+ return cast("Callable[[], Awaitable[_T]]", given)
@@ -0,0 +1,7 @@
1
+ """Creating and dropping the database a connection URL names."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .database_manager import DatabaseManager
6
+
7
+ __all__ = ["DatabaseManager"]
@@ -0,0 +1,196 @@
1
+ """Creating and dropping the database a connection URL names."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from contextlib import asynccontextmanager
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+ from typing import TYPE_CHECKING, Final, final
9
+
10
+ from sqlalchemy import URL, make_url, text
11
+ from sqlalchemy.ext.asyncio import create_async_engine
12
+ from sqlalchemy.pool import NullPool
13
+
14
+ from xtr_orm.exception import InvalidArgumentError, UnsupportedDatabaseError
15
+
16
+ if TYPE_CHECKING:
17
+ from collections.abc import AsyncGenerator, Mapping
18
+
19
+ from sqlalchemy.ext.asyncio import AsyncConnection
20
+
21
+ __all__ = ["DatabaseManager"]
22
+
23
+ _MEMORY: Final = frozenset({"", ":memory:"})
24
+
25
+
26
+ @dataclass(frozen=True, slots=True)
27
+ class _Server:
28
+ """How to reach a kind of server without its database, and ask it about one."""
29
+
30
+ maintenance_database: str | None
31
+ exists: str
32
+
33
+
34
+ _SERVERS: Final[dict[str, _Server]] = {
35
+ "postgresql": _Server("postgres", "SELECT 1 FROM pg_database WHERE datname = :name"),
36
+ "mysql": _Server(None, "SELECT 1 FROM information_schema.schemata WHERE schema_name = :name"),
37
+ "mariadb": _Server(None, "SELECT 1 FROM information_schema.schemata WHERE schema_name = :name"),
38
+ "mssql": _Server("master", "SELECT 1 FROM sys.databases WHERE name = :name"),
39
+ }
40
+
41
+
42
+ @final
43
+ class DatabaseManager:
44
+ """Creates and drops the database a connection URL names, on its server.
45
+
46
+ A database cannot be created from a connection to itself, so the server
47
+ is reached through its maintenance database — ``postgres`` on
48
+ PostgreSQL, ``master`` on SQL Server, none on MySQL and MariaDB — with
49
+ every statement committed as it runs. A SQLite database is its file.
50
+
51
+ ```python
52
+ manager = DatabaseManager("postgresql+asyncpg://app:secret@db/shop")
53
+ await manager.create(if_not_exists=True)
54
+ ```
55
+ """
56
+
57
+ __slots__ = ("_connect_args", "_url")
58
+
59
+ _url: URL
60
+ _connect_args: Mapping[str, object]
61
+
62
+ def __init__(self, url: str | URL, *, connect_args: Mapping[str, object] | None = None) -> None:
63
+ """Manage the database ``url`` names, connecting with ``connect_args``.
64
+
65
+ Raises:
66
+ InvalidArgumentError: When the URL names no database on a server.
67
+ """
68
+ self._url = make_url(url)
69
+ self._connect_args = connect_args or {}
70
+ if self.backend != "sqlite" and not self._url.database:
71
+ raise InvalidArgumentError(
72
+ f"The URL {self.safe_url} names no database to create or drop.",
73
+ )
74
+
75
+ @property
76
+ def backend(self) -> str:
77
+ """The kind of server, such as ``"postgresql"`` or ``"sqlite"``."""
78
+ return self._url.get_backend_name()
79
+
80
+ @property
81
+ def database(self) -> str:
82
+ """The database's name — for SQLite, its file."""
83
+ return self._url.database or ""
84
+
85
+ @property
86
+ def safe_url(self) -> str:
87
+ """The URL with its password hidden, fit for a message."""
88
+ return self._url.render_as_string(hide_password=True)
89
+
90
+ async def exists(self) -> bool:
91
+ """Whether the database exists.
92
+
93
+ Raises:
94
+ UnsupportedDatabaseError: For a server this library cannot ask.
95
+ """
96
+ if self.backend == "sqlite":
97
+ path = self._sqlite_path()
98
+ return path is None or path.exists()
99
+ server = self._server("check")
100
+ async with self._maintenance() as connection:
101
+ found = await connection.execute(text(server.exists), {"name": self.database})
102
+ return found.first() is not None
103
+
104
+ async def create(self, *, if_not_exists: bool = False) -> bool:
105
+ """Create the database; return whether it was created.
106
+
107
+ Args:
108
+ if_not_exists: Leave an existing database alone and return
109
+ ``False``, rather than let the server refuse.
110
+
111
+ Raises:
112
+ UnsupportedDatabaseError: For a server this library cannot ask.
113
+ """
114
+ if if_not_exists and await self.exists():
115
+ return False
116
+ if self.backend == "sqlite":
117
+ self._create_file()
118
+ return True
119
+ _ = self._server("create")
120
+ async with self._maintenance() as connection:
121
+ name = connection.dialect.identifier_preparer.quote(self.database)
122
+ _ = await connection.execute(text(f"CREATE DATABASE {name}"))
123
+ return True
124
+
125
+ async def drop(self, *, if_exists: bool = False) -> bool:
126
+ """Drop the database; return whether it was dropped.
127
+
128
+ Args:
129
+ if_exists: Do nothing for a database that does not exist and
130
+ return ``False``, rather than let the server refuse.
131
+
132
+ Raises:
133
+ UnsupportedDatabaseError: For a server this library cannot ask.
134
+ """
135
+ if if_exists and not await self.exists():
136
+ return False
137
+ if self.backend == "sqlite":
138
+ self._remove_file()
139
+ return True
140
+ _ = self._server("drop")
141
+ async with self._maintenance() as connection:
142
+ name = connection.dialect.identifier_preparer.quote(self.database)
143
+ _ = await connection.execute(text(f"DROP DATABASE {name}"))
144
+ return True
145
+
146
+ def _server(self, operation: str) -> _Server:
147
+ server = _SERVERS.get(self.backend)
148
+ if server is None:
149
+ raise UnsupportedDatabaseError(self.backend, operation)
150
+ return server
151
+
152
+ @asynccontextmanager
153
+ async def _maintenance(self) -> AsyncGenerator[AsyncConnection]:
154
+ """Connect to the server's maintenance database, committing every statement."""
155
+ server = _SERVERS[self.backend]
156
+ url = self._url
157
+ # ``set`` keeps a database it is given None for, so the URL is rebuilt.
158
+ maintenance = URL.create(
159
+ url.drivername,
160
+ username=url.username,
161
+ password=url.password,
162
+ host=url.host,
163
+ port=url.port,
164
+ database=server.maintenance_database,
165
+ query=url.query,
166
+ )
167
+ engine = create_async_engine(
168
+ maintenance,
169
+ poolclass=NullPool,
170
+ isolation_level="AUTOCOMMIT",
171
+ connect_args=dict(self._connect_args),
172
+ )
173
+ try:
174
+ async with engine.connect() as connection:
175
+ yield connection
176
+ finally:
177
+ await engine.dispose()
178
+
179
+ def _sqlite_path(self) -> Path | None:
180
+ """Return the SQLite file, or ``None`` for a database kept in memory."""
181
+ if self.database in _MEMORY or self.database.startswith("file::memory:"):
182
+ return None
183
+ return Path(self.database.removeprefix("file:").split("?", 1)[0])
184
+
185
+ def _create_file(self) -> None:
186
+ """Create the SQLite file, refusing one that exists; nothing for one in memory."""
187
+ path = self._sqlite_path()
188
+ if path is not None:
189
+ path.parent.mkdir(parents=True, exist_ok=True)
190
+ path.touch(exist_ok=False)
191
+
192
+ def _remove_file(self) -> None:
193
+ """Remove the SQLite file; nothing for one in memory."""
194
+ path = self._sqlite_path()
195
+ if path is not None:
196
+ path.unlink()
@@ -0,0 +1,19 @@
1
+ """Every error this library raises, all deriving from :class:`OrmError`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .invalid_argument_error import InvalidArgumentError
6
+ from .migration_error import MigrationError
7
+ from .orm_error import OrmError
8
+ from .session_unavailable_error import SessionUnavailableError
9
+ from .unknown_connection_error import UnknownConnectionError
10
+ from .unsupported_database_error import UnsupportedDatabaseError
11
+
12
+ __all__ = [
13
+ "InvalidArgumentError",
14
+ "MigrationError",
15
+ "OrmError",
16
+ "SessionUnavailableError",
17
+ "UnknownConnectionError",
18
+ "UnsupportedDatabaseError",
19
+ ]
@@ -0,0 +1,28 @@
1
+ """A configuration or a call was given something the library cannot work with."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .orm_error import OrmError
6
+
7
+ __all__ = ["InvalidArgumentError"]
8
+
9
+
10
+ class InvalidArgumentError(OrmError, ValueError):
11
+ """A configuration or a call was given something the library cannot work with.
12
+
13
+ Raised where the value is given — a connection without a name, a version
14
+ table without a name — rather than when a database is first reached.
15
+
16
+ Also a :class:`ValueError`, so code that already guards its configuration
17
+ with ``except ValueError`` keeps working without learning a new exception.
18
+
19
+ Attributes:
20
+ reason: What is wrong with the value.
21
+ """
22
+
23
+ reason: str
24
+
25
+ def __init__(self, reason: str) -> None:
26
+ """Record what is wrong with the value."""
27
+ self.reason = reason
28
+ super().__init__(reason)
@@ -0,0 +1,28 @@
1
+ """A migration operation was refused, or its revisions could not be read."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .orm_error import OrmError
6
+
7
+ __all__ = ["MigrationError"]
8
+
9
+
10
+ class MigrationError(OrmError):
11
+ """A migration operation was refused, or its revisions could not be read.
12
+
13
+ Raised before anything is changed whenever it can be decided up front: a
14
+ version that no revision answers to, a revision run out of order, a diff
15
+ against a database that is behind its revisions. Nothing is written when
16
+ it is raised from a check; a failing revision rolls back what the
17
+ database lets it.
18
+
19
+ Attributes:
20
+ reason: Why the operation was refused.
21
+ """
22
+
23
+ reason: str
24
+
25
+ def __init__(self, reason: str) -> None:
26
+ """Record why the operation was refused."""
27
+ self.reason = reason
28
+ super().__init__(reason)
@@ -0,0 +1,14 @@
1
+ """The root every error in this library derives from."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __all__ = ["OrmError"]
6
+
7
+
8
+ class OrmError(Exception):
9
+ """Base class for every error raised by this library.
10
+
11
+ Catch this to handle anything the library can refuse; catch a subclass to
12
+ handle one cause. Every subclass carries the data a caller needs as typed
13
+ attributes and composes its own message from them.
14
+ """
@@ -0,0 +1,29 @@
1
+ """The session of the current unit of work was asked for where there is none."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .orm_error import OrmError
6
+
7
+ __all__ = ["SessionUnavailableError"]
8
+
9
+
10
+ class SessionUnavailableError(OrmError):
11
+ """The session of the current unit of work was asked for where there is none.
12
+
13
+ A connection reaches the session everything in a unit of work shares —
14
+ a message's handlers, say — only inside one, and only when it was
15
+ registered with a way to.
16
+
17
+ Attributes:
18
+ connection: The connection the session was asked of.
19
+ reason: Why there is none.
20
+ """
21
+
22
+ connection: str
23
+ reason: str
24
+
25
+ def __init__(self, connection: str, reason: str) -> None:
26
+ """Record which connection was asked, and why it has no session to give."""
27
+ self.connection = connection
28
+ self.reason = reason
29
+ super().__init__(f'The "{connection}" connection has no session here: {reason}.')
@@ -0,0 +1,28 @@
1
+ """A connection was asked for by a name nothing registered."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .orm_error import OrmError
6
+
7
+ __all__ = ["UnknownConnectionError"]
8
+
9
+
10
+ class UnknownConnectionError(OrmError, LookupError):
11
+ """A connection was asked for by a name nothing registered.
12
+
13
+ Also a :class:`LookupError`, as a missing key in any other mapping is.
14
+
15
+ Attributes:
16
+ name: The name asked for.
17
+ known: Every name that is registered.
18
+ """
19
+
20
+ name: str
21
+ known: tuple[str, ...]
22
+
23
+ def __init__(self, name: str, known: tuple[str, ...]) -> None:
24
+ """Record the name asked for and the ones that exist."""
25
+ self.name = name
26
+ self.known = known
27
+ names = ", ".join(f'"{each}"' for each in known) or "none"
28
+ super().__init__(f'Unknown connection "{name}"; the connections are: {names}.')
@@ -0,0 +1,29 @@
1
+ """A database server was asked for something this library cannot do on it."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .orm_error import OrmError
6
+
7
+ __all__ = ["UnsupportedDatabaseError"]
8
+
9
+
10
+ class UnsupportedDatabaseError(OrmError):
11
+ """A database server was asked for something this library cannot do on it.
12
+
13
+ Creating or dropping a database takes a statement of the server's own;
14
+ a server whose statement is not known here is refused rather than sent a
15
+ guess.
16
+
17
+ Attributes:
18
+ backend: The kind of server the URL names, such as ``"oracle"``.
19
+ operation: What was asked, such as ``"create"``.
20
+ """
21
+
22
+ backend: str
23
+ operation: str
24
+
25
+ def __init__(self, backend: str, operation: str) -> None:
26
+ """Record which server was asked for what."""
27
+ self.backend = backend
28
+ self.operation = operation
29
+ super().__init__(f'Cannot {operation} a database on "{backend}": it is not supported.')
@@ -0,0 +1,23 @@
1
+ """Message bus middleware for the connections: transactions, closing, and open transactions.
2
+
3
+ Importing this module declares them by name — ``orm_transaction``,
4
+ ``orm_close_connection`` and ``orm_open_transaction_logger`` — for a bus
5
+ configuration to list. With a container, the orm bundle loads it when the
6
+ messenger bundle is active, and the messenger bundle registers each by that
7
+ name. Without one, build them with a :class:`~xtr_orm.ConnectionRegistry`
8
+ and put the instances in the configuration.
9
+
10
+ Needs the ``messenger`` extra: ``xtr-orm[messenger]``.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from .close_connection_middleware import CloseConnectionMiddleware
16
+ from .open_transaction_logger_middleware import OpenTransactionLoggerMiddleware
17
+ from .transaction_middleware import TransactionMiddleware
18
+
19
+ __all__ = [
20
+ "CloseConnectionMiddleware",
21
+ "OpenTransactionLoggerMiddleware",
22
+ "TransactionMiddleware",
23
+ ]