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,86 @@
1
+ """``orm:migrations:execute``: run chosen revisions alone, up or down."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import final
6
+
7
+ from xtr_console import ConsoleStyle, ExitCode, as_command, escape
8
+
9
+ from xtr_orm.exception import MigrationError
10
+ from xtr_orm.migrations import Direction
11
+
12
+ from .connection_command import (
13
+ ConnectionCommand,
14
+ confirm_changes,
15
+ report_count,
16
+ report_plan,
17
+ report_results,
18
+ save_sql,
19
+ )
20
+
21
+ __all__ = ["MigrationsExecuteCommand"]
22
+
23
+
24
+ @as_command("orm:migrations:execute")
25
+ @final
26
+ class MigrationsExecuteCommand(ConnectionCommand):
27
+ """Execute one or more migration versions up or down manually."""
28
+
29
+ __slots__ = ()
30
+
31
+ async def __call__( # noqa: PLR0911 — each way out says why it stopped
32
+ self,
33
+ io: ConsoleStyle,
34
+ *versions: str,
35
+ up: bool = False,
36
+ down: bool = False,
37
+ dry_run: bool = False,
38
+ write_sql: str | None = None,
39
+ connection: str | None = None,
40
+ ) -> int:
41
+ """Run exactly the versions, in order, each up (the default) or down, and nothing else.
42
+
43
+ Going up, a revision must not be applied and must follow only applied
44
+ ones; going down, it must be applied and followed by none that is.
45
+ Use orm:migrations:version to record a revision without running it.
46
+
47
+ Args:
48
+ io: Where the command writes.
49
+ versions: The revisions to run, by full or partial id.
50
+ up: Run their upgrades.
51
+ down: Run their downgrades.
52
+ dry_run: List what would run, and run nothing.
53
+ write_sql: Write the SQL that would run to this file — or to a
54
+ dated file in this directory — and run nothing.
55
+ connection: The connection to run them on; the default one when
56
+ left out.
57
+ """
58
+ if up and down:
59
+ io.error("Give --up or --down, not both.")
60
+ return ExitCode.INVALID
61
+ if not versions:
62
+ io.error("Name at least one version to execute.")
63
+ return ExitCode.INVALID
64
+ direction = Direction.DOWN if down else Direction.UP
65
+ migrator = await self._migrator(io, connection)
66
+ if migrator is None:
67
+ return ExitCode.FAILURE
68
+ try:
69
+ if write_sql is not None:
70
+ save_sql(io, write_sql, await migrator.execute_sql(versions, direction))
71
+ return ExitCode.SUCCESS
72
+ planned = await migrator.plan_for_versions(versions, direction)
73
+ if dry_run:
74
+ report_plan(io, planned)
75
+ io.note("Dry run: nothing was executed.")
76
+ return ExitCode.SUCCESS
77
+ if not confirm_changes(io, migrator):
78
+ io.error("Migration cancelled!")
79
+ return ExitCode.FAILURE
80
+ results = await migrator.execute(versions, direction)
81
+ except MigrationError as error:
82
+ io.error(escape(error.reason))
83
+ return ExitCode.FAILURE
84
+ report_results(io, results)
85
+ report_count(io, results)
86
+ return ExitCode.SUCCESS
@@ -0,0 +1,47 @@
1
+ """``orm:migrations:generate``: write an empty revision."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import final
6
+
7
+ from xtr_console import ConsoleStyle, ExitCode, as_command, escape
8
+
9
+ from xtr_orm.exception import MigrationError
10
+
11
+ from .connection_command import ConnectionCommand, report_written
12
+
13
+ __all__ = ["MigrationsGenerateCommand"]
14
+
15
+
16
+ @as_command("orm:migrations:generate")
17
+ @final
18
+ class MigrationsGenerateCommand(ConnectionCommand):
19
+ """Generate a blank migration class."""
20
+
21
+ __slots__ = ()
22
+
23
+ async def __call__(
24
+ self,
25
+ io: ConsoleStyle,
26
+ *,
27
+ message: str | None = None,
28
+ connection: str | None = None,
29
+ ) -> int:
30
+ """Write an empty revision following the latest one, to fill in by hand.
31
+
32
+ Args:
33
+ io: Where the command writes.
34
+ message: What the revision does; it names the file too.
35
+ connection: The connection whose revisions to add to; the default
36
+ one when left out.
37
+ """
38
+ migrator = await self._migrator(io, connection)
39
+ if migrator is None:
40
+ return ExitCode.FAILURE
41
+ try:
42
+ written = migrator.generate(message)
43
+ except MigrationError as error:
44
+ io.error(escape(error.reason))
45
+ return ExitCode.FAILURE
46
+ report_written(io, "Generated new migration class to", written, connection)
47
+ return ExitCode.SUCCESS
@@ -0,0 +1,44 @@
1
+ """``orm:migrations:latest``: the revisions no other follows."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import final
6
+
7
+ from xtr_console import ConsoleStyle, ExitCode, as_command, escape
8
+
9
+ from xtr_orm.exception import MigrationError
10
+
11
+ from .connection_command import ConnectionCommand, format_version
12
+
13
+ __all__ = ["MigrationsLatestCommand"]
14
+
15
+
16
+ @as_command("orm:migrations:latest")
17
+ @final
18
+ class MigrationsLatestCommand(ConnectionCommand):
19
+ """Outputs the latest version."""
20
+
21
+ __slots__ = ()
22
+
23
+ async def __call__(self, io: ConsoleStyle, *, connection: str | None = None) -> int:
24
+ """Print the revisions no other follows, one per line — base when there are none.
25
+
26
+ Args:
27
+ io: Where the command writes.
28
+ connection: The connection whose revisions to read; the default one
29
+ when left out.
30
+ """
31
+ migrator = await self._migrator(io, connection)
32
+ if migrator is None:
33
+ return ExitCode.FAILURE
34
+ try:
35
+ status = await migrator.status()
36
+ except MigrationError as error:
37
+ io.error(escape(error.reason))
38
+ return ExitCode.FAILURE
39
+ heads = [each for each in status.available if each.is_head]
40
+ if not heads:
41
+ io.text("base")
42
+ for head in heads:
43
+ io.text(escape(format_version(head.version, head.description)))
44
+ return ExitCode.SUCCESS
@@ -0,0 +1,71 @@
1
+ """``orm:migrations:list``: every revision, and whether it is applied."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import final
6
+
7
+ from xtr_console import ConsoleStyle, ExitCode, as_command, escape
8
+
9
+ from xtr_orm.exception import MigrationError
10
+ from xtr_orm.migrations import ExecutedMigration
11
+
12
+ from .connection_command import ConnectionCommand
13
+
14
+ __all__ = ["MigrationsListCommand"]
15
+
16
+
17
+ @as_command("orm:migrations:list")
18
+ @final
19
+ class MigrationsListCommand(ConnectionCommand):
20
+ """Display a list of all available migrations and their status."""
21
+
22
+ __slots__ = ()
23
+
24
+ async def __call__(self, io: ConsoleStyle, *, connection: str | None = None) -> int:
25
+ """List every revision, earliest first, then the applied ones whose file is gone.
26
+
27
+ Args:
28
+ io: Where the command writes.
29
+ connection: The connection to read; the default one when left out.
30
+ """
31
+ migrator = await self._migrator(io, connection)
32
+ if migrator is None:
33
+ return ExitCode.FAILURE
34
+ try:
35
+ status = await migrator.status()
36
+ except MigrationError as error:
37
+ io.error(escape(error.reason))
38
+ return ExitCode.FAILURE
39
+ executed = {each.version: each for each in status.executed}
40
+ rows = [
41
+ _row(each.version, executed.get(each.version), each.description, available=True)
42
+ for each in status.available
43
+ ]
44
+ rows += [
45
+ _row(each.version, each, "", available=False) for each in status.executed_unavailable
46
+ ]
47
+ io.table(
48
+ ["Migration Versions", "Status", "Migrated At", "Execution Time", "Description"],
49
+ rows,
50
+ )
51
+ return ExitCode.SUCCESS
52
+
53
+
54
+ def _row(
55
+ version: str, executed: ExecutedMigration | None, description: str, *, available: bool
56
+ ) -> list[str]:
57
+ if executed is None:
58
+ status = "not migrated"
59
+ else:
60
+ status = "migrated" if available else "migrated, not available"
61
+ migrated_at = (
62
+ f"{executed.executed_at:%Y-%m-%d %H:%M:%S}"
63
+ if executed is not None and executed.executed_at is not None
64
+ else ""
65
+ )
66
+ duration = (
67
+ f"{executed.execution_time:.3f}s"
68
+ if executed is not None and executed.execution_time is not None
69
+ else ""
70
+ )
71
+ return [escape(version), status, migrated_at, duration, escape(description)]
@@ -0,0 +1,108 @@
1
+ """``orm:migrations:migrate``: bring the database to a revision, up or down."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Final, final
6
+
7
+ from xtr_console import ConsoleStyle, ExitCode, as_command, escape
8
+
9
+ from xtr_orm.exception import MigrationError
10
+
11
+ from .connection_command import (
12
+ ConnectionCommand,
13
+ confirm_changes,
14
+ report_count,
15
+ report_plan,
16
+ report_results,
17
+ save_sql,
18
+ )
19
+
20
+ __all__ = ["MigrationsMigrateCommand"]
21
+
22
+ _LATEST: Final = frozenset({"latest", "head", "heads"})
23
+
24
+
25
+ @as_command("orm:migrations:migrate")
26
+ @final
27
+ class MigrationsMigrateCommand(ConnectionCommand):
28
+ """Execute a migration to a specified version or the latest available version."""
29
+
30
+ __slots__ = ()
31
+
32
+ async def __call__( # noqa: C901, PLR0911 — each way out says why it stopped
33
+ self,
34
+ io: ConsoleStyle,
35
+ version: str = "latest",
36
+ *,
37
+ dry_run: bool = False,
38
+ write_sql: str | None = None,
39
+ allow_no_migration: bool = False,
40
+ connection: str | None = None,
41
+ ) -> int:
42
+ """Run every revision between where the database is and the version.
43
+
44
+ The version is a revision — a full or partial id — a relative one
45
+ (+1, -2), or one of latest, first (before every
46
+ revision), next, prev and current. A revision already
47
+ applied is migrated down to; one that is not, up to.
48
+
49
+ Args:
50
+ io: Where the command writes.
51
+ version: Where to bring the database.
52
+ dry_run: List what would run, and run nothing.
53
+ write_sql: Write the SQL that would run to this file — or to a
54
+ dated file in this directory — and run nothing.
55
+ allow_no_migration: Succeed when there are no revisions at all.
56
+ connection: The connection to migrate; the default one when left
57
+ out.
58
+ """
59
+ migrator = await self._migrator(io, connection)
60
+ if migrator is None:
61
+ return ExitCode.FAILURE
62
+ try:
63
+ status = await migrator.status()
64
+ if not status.available:
65
+ if allow_no_migration:
66
+ io.warning("No migrations to execute.")
67
+ return ExitCode.SUCCESS
68
+ io.error(
69
+ f'The version "{escape(version)}" couldn\'t be reached, '
70
+ "there are no registered migrations.",
71
+ )
72
+ return ExitCode.FAILURE
73
+ unregistered = len(status.executed_unavailable)
74
+ if unregistered:
75
+ io.warning(
76
+ f"You have {unregistered} previously executed migrations in the database "
77
+ "that are not registered migrations.",
78
+ )
79
+ if not io.confirm("Are you sure you wish to continue?", default=True):
80
+ io.error("Migration cancelled!")
81
+ return ExitCode.FAILURE
82
+ plans = await migrator.plan(version)
83
+ if not plans:
84
+ where = ", ".join(status.current) or "base"
85
+ message = (
86
+ f'Already at the latest version ("{escape(where)}").'
87
+ if version in _LATEST
88
+ else f'Already at "{escape(version)}": no migrations to execute.'
89
+ )
90
+ io.success(message)
91
+ return ExitCode.SUCCESS
92
+ if write_sql is not None:
93
+ save_sql(io, write_sql, await migrator.migrate_sql(version))
94
+ return ExitCode.SUCCESS
95
+ if dry_run:
96
+ report_plan(io, plans)
97
+ io.note("Dry run: nothing was executed.")
98
+ return ExitCode.SUCCESS
99
+ if not confirm_changes(io, migrator):
100
+ io.error("Migration cancelled!")
101
+ return ExitCode.FAILURE
102
+ results = await migrator.migrate(version)
103
+ except MigrationError as error:
104
+ io.error(escape(error.reason))
105
+ return ExitCode.FAILURE
106
+ report_results(io, results)
107
+ report_count(io, results)
108
+ return ExitCode.SUCCESS
@@ -0,0 +1,43 @@
1
+ """``orm:migrations:rollup``: record the single remaining revision as all there is."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import final
6
+
7
+ from xtr_console import ConsoleStyle, ExitCode, as_command, escape
8
+
9
+ from xtr_orm.exception import MigrationError
10
+
11
+ from .connection_command import ConnectionCommand
12
+
13
+ __all__ = ["MigrationsRollupCommand"]
14
+
15
+
16
+ @as_command("orm:migrations:rollup")
17
+ @final
18
+ class MigrationsRollupCommand(ConnectionCommand):
19
+ """Rollup migrations by deleting all tracked versions and insert the one version that exists."""
20
+
21
+ __slots__ = ()
22
+
23
+ async def __call__(self, io: ConsoleStyle, *, connection: str | None = None) -> int:
24
+ """Forget every applied revision, then record the single remaining one as applied.
25
+
26
+ For after squashing every revision into one: the database is left as
27
+ it is, recorded as being at that revision.
28
+
29
+ Args:
30
+ io: Where the command writes.
31
+ connection: The connection to record on; the default one when left
32
+ out.
33
+ """
34
+ migrator = await self._migrator(io, connection)
35
+ if migrator is None:
36
+ return ExitCode.FAILURE
37
+ try:
38
+ rolled = await migrator.rollup()
39
+ except MigrationError as error:
40
+ io.error(escape(error.reason))
41
+ return ExitCode.FAILURE
42
+ io.success(f'Rolled up migrations to version "{escape(rolled.version)}".')
43
+ return ExitCode.SUCCESS
@@ -0,0 +1,66 @@
1
+ """``orm:migrations:status``: where the database stands, in one table."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import final
6
+
7
+ from xtr_console import ConsoleStyle, ExitCode, as_command, escape
8
+
9
+ from xtr_orm.exception import MigrationError
10
+
11
+ from .connection_command import ConnectionCommand
12
+
13
+ __all__ = ["MigrationsStatusCommand"]
14
+
15
+
16
+ @as_command("orm:migrations:status")
17
+ @final
18
+ class MigrationsStatusCommand(ConnectionCommand):
19
+ """View the status of a set of migrations."""
20
+
21
+ __slots__ = ()
22
+
23
+ async def __call__(self, io: ConsoleStyle, *, connection: str | None = None) -> int:
24
+ """Show where the revisions and their tables are, and where the database stands.
25
+
26
+ Args:
27
+ io: Where the command writes.
28
+ connection: The connection to read; the default one when left out.
29
+ """
30
+ migrator = await self._migrator(io, connection)
31
+ if migrator is None:
32
+ return ExitCode.FAILURE
33
+ try:
34
+ status = await migrator.status()
35
+ except MigrationError as error:
36
+ io.error(escape(error.reason))
37
+ return ExitCode.FAILURE
38
+ config = migrator.config
39
+ url = migrator.engine.url
40
+ schema = config.version_table_schema
41
+ rows = [
42
+ ["Storage", "Version table", _qualified(schema, config.version_table)],
43
+ ["", "History table", _qualified(schema, config.history_table)],
44
+ ["Database", "Connection", migrator.name],
45
+ ["", "Driver", f"{url.get_backend_name()}+{url.get_driver_name()}"],
46
+ ["", "Name", url.database or ""],
47
+ ["Versions", "Previous", _versions(status.previous) if status.current else "none"],
48
+ ["", "Current", _versions(status.current)],
49
+ ["", "Next", ", ".join(status.next) or "none"],
50
+ ["", "Latest", _versions(status.latest)],
51
+ ["Migrations", "Executed", str(len(status.executed))],
52
+ ["", "Executed Unavailable", str(len(status.executed_unavailable))],
53
+ ["", "Available", str(len(status.available))],
54
+ ["", "New", str(len(status.new))],
55
+ ["Migrations Directory", "", config.directory],
56
+ ]
57
+ io.table(["Configuration", "", ""], [[escape(cell) for cell in row] for row in rows])
58
+ return ExitCode.SUCCESS
59
+
60
+
61
+ def _versions(versions: tuple[str, ...]) -> str:
62
+ return ", ".join(versions) or "base"
63
+
64
+
65
+ def _qualified(schema: str | None, table: str) -> str:
66
+ return f"{schema}.{table}" if schema else table
@@ -0,0 +1,83 @@
1
+ """``orm:migrations:up-to-date``: whether every revision is applied."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated, Final, final
6
+
7
+ from xtr_console import ConsoleStyle, ExitCode, Option, as_command, escape
8
+
9
+ from xtr_orm.exception import MigrationError
10
+
11
+ from .connection_command import ConnectionCommand
12
+
13
+ __all__ = ["MigrationsUpToDateCommand"]
14
+
15
+ _UNREGISTERED: Final = 2
16
+ """The exit code for applied revisions without a file, with --fail-on-unregistered."""
17
+
18
+
19
+ @as_command("orm:migrations:up-to-date")
20
+ @final
21
+ class MigrationsUpToDateCommand(ConnectionCommand):
22
+ """Tells you if your schema is up-to-date."""
23
+
24
+ __slots__ = ()
25
+
26
+ async def __call__(
27
+ self,
28
+ io: ConsoleStyle,
29
+ *,
30
+ fail_on_unregistered: Annotated[bool, Option(alias="-u")] = False,
31
+ list_migrations: Annotated[bool, Option(alias="-l")] = False,
32
+ connection: str | None = None,
33
+ ) -> int:
34
+ """Tell whether every revision is applied, exiting 1 when some are not.
35
+
36
+ Applied revisions whose file is gone are reported too, and exit 2 with
37
+ --fail-on-unregistered.
38
+
39
+ Args:
40
+ io: Where the command writes.
41
+ fail_on_unregistered: Fail when applied revisions have no file.
42
+ list_migrations: List the revisions not applied, and the applied
43
+ ones without a file.
44
+ connection: The connection to read; the default one when left out.
45
+ """
46
+ migrator = await self._migrator(io, connection)
47
+ if migrator is None:
48
+ return ExitCode.FAILURE
49
+ try:
50
+ status = await migrator.status()
51
+ except MigrationError as error:
52
+ io.error(escape(error.reason))
53
+ return ExitCode.FAILURE
54
+ new = len(status.new)
55
+ unregistered = len(status.executed_unavailable)
56
+ if not new and not unregistered:
57
+ io.success("Up-to-date! No migrations to execute.")
58
+ return ExitCode.SUCCESS
59
+ code: int = ExitCode.SUCCESS
60
+ if new:
61
+ are = " is" if new == 1 else "s are"
62
+ io.error(f"Out-of-date! {new} migration{are} available to execute.")
63
+ code = ExitCode.FAILURE
64
+ if unregistered:
65
+ io.error(
66
+ f"You have {unregistered} previously executed "
67
+ f"migration{'' if unregistered == 1 else 's'} in the database that "
68
+ f"{'is not a' if unregistered == 1 else 'are not'} registered "
69
+ f"migration{'' if unregistered == 1 else 's'}.",
70
+ )
71
+ if fail_on_unregistered:
72
+ code = _UNREGISTERED
73
+ if list_migrations:
74
+ rows = [
75
+ [escape(each.version), "not migrated", escape(each.description)]
76
+ for each in status.new
77
+ ]
78
+ rows += [
79
+ [escape(each.version), "migrated, not available", ""]
80
+ for each in status.executed_unavailable
81
+ ]
82
+ io.table(["Migration Versions", "Status", "Description"], rows)
83
+ return code
@@ -0,0 +1,89 @@
1
+ """``orm:migrations:version``: record revisions as applied or not, without running them."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated, final
6
+
7
+ from xtr_console import ConsoleStyle, ExitCode, Option, as_command, escape
8
+
9
+ from xtr_orm.exception import MigrationError
10
+
11
+ from .connection_command import ConnectionCommand
12
+
13
+ __all__ = ["MigrationsVersionCommand"]
14
+
15
+
16
+ @as_command("orm:migrations:version")
17
+ @final
18
+ class MigrationsVersionCommand(ConnectionCommand):
19
+ """Manually add and delete migration versions from the version table."""
20
+
21
+ __slots__ = ()
22
+
23
+ async def __call__( # noqa: PLR0911 — each way out says why it stopped
24
+ self,
25
+ io: ConsoleStyle,
26
+ version: str | None = None,
27
+ *,
28
+ add: bool = False,
29
+ delete: bool = False,
30
+ all_versions: Annotated[bool, Option(name="--all")] = False,
31
+ connection: str | None = None,
32
+ ) -> int:
33
+ """Record the version as applied (--add) or not (--delete), running nothing.
34
+
35
+ Adding, the revision must follow only applied ones; deleting, no
36
+ applied revision may follow it. An applied revision whose file is gone
37
+ can always be deleted. With --all, every revision is added, or every
38
+ record deleted.
39
+
40
+ Args:
41
+ io: Where the command writes.
42
+ version: The revision to record, by full or partial id.
43
+ add: Record it as applied.
44
+ delete: Record it as not applied.
45
+ all_versions: Every revision, instead of one.
46
+ connection: The connection to record on; the default one when left
47
+ out.
48
+ """
49
+ if add == delete:
50
+ io.error(
51
+ "You must specify whether you want to --add or --delete the specified version."
52
+ )
53
+ return ExitCode.INVALID
54
+ if (version is None) == (not all_versions):
55
+ io.error("You must specify the version or use the --all argument, not both.")
56
+ return ExitCode.INVALID
57
+ migrator = await self._migrator(io, connection)
58
+ if migrator is None:
59
+ return ExitCode.FAILURE
60
+ if not io.confirm(
61
+ "WARNING! You are about to add, delete or synchronize migration versions from the "
62
+ "version table that could result in data lost. Are you sure you wish to continue?",
63
+ default=True,
64
+ ):
65
+ io.error("Migration cancelled!")
66
+ return ExitCode.FAILURE
67
+ try:
68
+ if add:
69
+ done = await (
70
+ migrator.add_versions([version])
71
+ if version is not None
72
+ else migrator.add_all_versions()
73
+ )
74
+ elif version is not None:
75
+ done = await migrator.delete_versions([version])
76
+ else:
77
+ await migrator.delete_all_versions()
78
+ io.success("Every version was deleted from the version table.")
79
+ return ExitCode.SUCCESS
80
+ except MigrationError as error:
81
+ io.error(escape(error.reason))
82
+ return ExitCode.FAILURE
83
+ verb = "Added" if add else "Deleted"
84
+ for each in done:
85
+ io.text(f" {verb} {escape(each)}")
86
+ plural = "" if len(done) == 1 else "s"
87
+ where = "to" if add else "from"
88
+ io.success(f"{verb} {len(done)} version{plural} {where} the version table.")
89
+ return ExitCode.SUCCESS