minimi-para 0.1.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.
@@ -0,0 +1 @@
1
+ The Minimi was created by Petr Jindra
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Authors and contributors listed in the AUTHORS file
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,138 @@
1
+ Metadata-Version: 2.4
2
+ Name: minimi-para
3
+ Version: 0.1.0
4
+ Summary: A mini migration tool
5
+ Author-email: Petr Jindra <el.mordo@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/elmordo/minimi
8
+ Project-URL: Repository, https://github.com/elmordo/minimi
9
+ Keywords: migrations,sqlalchemy,database
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Requires-Python: >=3.12
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ License-File: AUTHORS
20
+ Requires-Dist: sqlalchemy<3.0,>=2.0.52
21
+ Requires-Dist: sa-values<0.2,>=0.1.0
22
+ Provides-Extra: test
23
+ Requires-Dist: pytest; extra == "test"
24
+ Provides-Extra: dev
25
+ Requires-Dist: sa-values[test]; extra == "dev"
26
+ Requires-Dist: build; extra == "dev"
27
+ Requires-Dist: twine; extra == "dev"
28
+ Dynamic: license-file
29
+
30
+ **MINI**mal **MI**gration library tool for managing database migrations in small projects where
31
+ a full-featured migration tool would be overkill.
32
+
33
+ Why the "_para_" suffix? The original name of the library `minimi` was rejected by the pypi.org because
34
+ it is too similar to another library. Also, the Minimi Para is compatct version of the original Minimi :-)
35
+
36
+ ```
37
+
38
+ ====
39
+ __@----------------\ \
40
+ ||=========||==== ===\_____\___________ // //
41
+ || ||==== |======|=| ALTER TABLE users DROP COLUMN email
42
+ ||=========||---_____======------------------------|=| \\ \\
43
+ / / \\ / \____________| ||
44
+ / /------ ||
45
+ /___/ ||
46
+ ||
47
+ ====
48
+ ```
49
+
50
+ # Quick start
51
+
52
+ Create a migration module in your source directory
53
+
54
+ ```
55
+ src
56
+ +- mylib
57
+ +- migrations
58
+ | +- __init__.py <- list of migration modules
59
+ | +- m01_db_init.py <- first revision
60
+ | +- m02_new_table.py <- second revision
61
+ +- other module
62
+ +- main.py
63
+ ```
64
+
65
+ ```python
66
+ # __init__.py
67
+
68
+ from . import m01_db_init, m02_new_table
69
+
70
+ MIGRATIONS = [
71
+ m01_db_init,
72
+ m02_new_table,
73
+ ]
74
+ ```
75
+
76
+ ```python
77
+ # m01_db_init.py
78
+
79
+ # the UP migration only
80
+ MIGRATIONS = """
81
+ CREATE TABLE IF NOT EXISTS users (
82
+ id INTEGER PRIMARY KEY,
83
+ name TEXT NOT NULL,
84
+ email TEXT NOT NULL UNIQUE
85
+ );
86
+ """
87
+ ```
88
+
89
+ ```python
90
+ # m02_new_table.py
91
+
92
+ # the UP and DOWN migrations as tuple of two strings
93
+ MIGRATIONS = """
94
+ CREATE TABLE IF NOT EXISTS user_comments (
95
+ id INTEGER PRIMARY KEY,
96
+ user_id INTEGER NOT NULL
97
+ REFERENCES users(id),
98
+ subjects TEXT NOT NULL,
99
+ message TEXT NOT NULL
100
+ );
101
+ """, """
102
+ DROP TABLE IF EXISTS user_comments;
103
+ """
104
+ ```
105
+
106
+ ```python
107
+ # main.py
108
+
109
+ from minimi import Minimi
110
+ from sqlalchemy import create_engine
111
+ from sa_values import setup_sa_values
112
+
113
+ import mylib.migrations as migrations
114
+
115
+
116
+ def main():
117
+ # initialize the sa_values first
118
+ conn = create_engine("sqlite:///:memory:").connect()
119
+ setup_sa_values(conn)
120
+ # run the migrations
121
+ Minimi(conn, migrations).apply()
122
+
123
+
124
+ if __name__ == "__main__":
125
+ main()
126
+ ```
127
+
128
+ # Usage
129
+
130
+ The `__init__.py` file contains a list of migration modules in the `MIGRATIONS` global variable with list of migration
131
+ modules.
132
+
133
+ Each migration module must contain the `MIGRATION` global variable with the migration.
134
+
135
+ # Buy me a ~~coffee~~ beer
136
+
137
+ If you like this library, or you want to support its development, support me by one
138
+ cold [beer](https://www.buymeacoffee.com/elmordo). The beer is tasty and full of vitamins :-)
@@ -0,0 +1,109 @@
1
+ **MINI**mal **MI**gration library tool for managing database migrations in small projects where
2
+ a full-featured migration tool would be overkill.
3
+
4
+ Why the "_para_" suffix? The original name of the library `minimi` was rejected by the pypi.org because
5
+ it is too similar to another library. Also, the Minimi Para is compatct version of the original Minimi :-)
6
+
7
+ ```
8
+
9
+ ====
10
+ __@----------------\ \
11
+ ||=========||==== ===\_____\___________ // //
12
+ || ||==== |======|=| ALTER TABLE users DROP COLUMN email
13
+ ||=========||---_____======------------------------|=| \\ \\
14
+ / / \\ / \____________| ||
15
+ / /------ ||
16
+ /___/ ||
17
+ ||
18
+ ====
19
+ ```
20
+
21
+ # Quick start
22
+
23
+ Create a migration module in your source directory
24
+
25
+ ```
26
+ src
27
+ +- mylib
28
+ +- migrations
29
+ | +- __init__.py <- list of migration modules
30
+ | +- m01_db_init.py <- first revision
31
+ | +- m02_new_table.py <- second revision
32
+ +- other module
33
+ +- main.py
34
+ ```
35
+
36
+ ```python
37
+ # __init__.py
38
+
39
+ from . import m01_db_init, m02_new_table
40
+
41
+ MIGRATIONS = [
42
+ m01_db_init,
43
+ m02_new_table,
44
+ ]
45
+ ```
46
+
47
+ ```python
48
+ # m01_db_init.py
49
+
50
+ # the UP migration only
51
+ MIGRATIONS = """
52
+ CREATE TABLE IF NOT EXISTS users (
53
+ id INTEGER PRIMARY KEY,
54
+ name TEXT NOT NULL,
55
+ email TEXT NOT NULL UNIQUE
56
+ );
57
+ """
58
+ ```
59
+
60
+ ```python
61
+ # m02_new_table.py
62
+
63
+ # the UP and DOWN migrations as tuple of two strings
64
+ MIGRATIONS = """
65
+ CREATE TABLE IF NOT EXISTS user_comments (
66
+ id INTEGER PRIMARY KEY,
67
+ user_id INTEGER NOT NULL
68
+ REFERENCES users(id),
69
+ subjects TEXT NOT NULL,
70
+ message TEXT NOT NULL
71
+ );
72
+ """, """
73
+ DROP TABLE IF EXISTS user_comments;
74
+ """
75
+ ```
76
+
77
+ ```python
78
+ # main.py
79
+
80
+ from minimi import Minimi
81
+ from sqlalchemy import create_engine
82
+ from sa_values import setup_sa_values
83
+
84
+ import mylib.migrations as migrations
85
+
86
+
87
+ def main():
88
+ # initialize the sa_values first
89
+ conn = create_engine("sqlite:///:memory:").connect()
90
+ setup_sa_values(conn)
91
+ # run the migrations
92
+ Minimi(conn, migrations).apply()
93
+
94
+
95
+ if __name__ == "__main__":
96
+ main()
97
+ ```
98
+
99
+ # Usage
100
+
101
+ The `__init__.py` file contains a list of migration modules in the `MIGRATIONS` global variable with list of migration
102
+ modules.
103
+
104
+ Each migration module must contain the `MIGRATION` global variable with the migration.
105
+
106
+ # Buy me a ~~coffee~~ beer
107
+
108
+ If you like this library, or you want to support its development, support me by one
109
+ cold [beer](https://www.buymeacoffee.com/elmordo). The beer is tasty and full of vitamins :-)
@@ -0,0 +1,63 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "minimi-para"
7
+ version = "0.1.0"
8
+ description = "A mini migration tool"
9
+ readme = "README.md"
10
+ requires-python = ">=3.12"
11
+ license = "MIT"
12
+ authors = [
13
+ { name = "Petr Jindra", email = "el.mordo@gmail.com" }
14
+ ]
15
+ keywords = ["migrations", "sqlalchemy", "database"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Intended Audience :: Developers",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Programming Language :: Python :: 3.13",
22
+ "Programming Language :: Python :: 3.14",
23
+ ]
24
+ dependencies = [
25
+ "sqlalchemy>=2.0.52,<3.0",
26
+ "sa-values>=0.1.0,<0.2"
27
+ ]
28
+
29
+ [project.optional-dependencies]
30
+ test = [
31
+ "pytest",
32
+ ]
33
+ dev = [
34
+ "sa-values[test]",
35
+ "build",
36
+ "twine",
37
+ ]
38
+
39
+ [project.urls]
40
+ Homepage = "https://github.com/elmordo/minimi"
41
+ Repository = "https://github.com/elmordo/minimi"
42
+
43
+ [tool.setuptools]
44
+ package-dir = { "" = "src" }
45
+
46
+ [tool.setuptools.packages.find]
47
+ where = ["src"]
48
+
49
+ [tool.black]
50
+ line-length = 100
51
+
52
+ [tool.ruff]
53
+ line-length = 100
54
+
55
+ [tool.ruff.lint.isort]
56
+ force-sort-within-sections = true
57
+ order-by-type = false
58
+ lines-after-imports = 2
59
+
60
+ [tool.ruff.format]
61
+ quote-style = "double"
62
+ indent-style = "space"
63
+ docstring-code-format = true
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,26 @@
1
+ # MIT License
2
+ #
3
+ # Copyright (c) 2026 Authors and contributors listed in the AUTHORS file
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in all
13
+ # copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
22
+
23
+ from .minimi import Minimi
24
+
25
+
26
+ __all__ = ["Minimi"]
@@ -0,0 +1,33 @@
1
+ # MIT License
2
+ #
3
+ # Copyright (c) 2026 Authors and contributors listed in the AUTHORS file
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in all
13
+ # copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
22
+
23
+
24
+ class MinimiException(Exception):
25
+ pass
26
+
27
+
28
+ class MigrationFailedError(MinimiException):
29
+ pass
30
+
31
+
32
+ class InvalidModuleStructureError(MinimiException):
33
+ pass
@@ -0,0 +1,136 @@
1
+ # MIT License
2
+ #
3
+ # Copyright (c) 2026 Authors and contributors listed in the AUTHORS file
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in all
13
+ # copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
22
+ from dataclasses import dataclass
23
+
24
+ from sqlalchemy import Connection, text
25
+
26
+ from .exceptions import InvalidModuleStructureError
27
+ from .types import MigrationCallback, MigrationModule, MigrationStatement, MigrationStep
28
+
29
+
30
+ @dataclass
31
+ class MigrationCallbackPair:
32
+ """Container for normalized migration step logic.
33
+
34
+ The `up` contains a callback called when migration is applied.
35
+ The `down` contains a callback called when migration is rolled back.
36
+
37
+ Both callbacks must be defined, even if the migration logic for the step part is empty.
38
+ """
39
+
40
+ up: MigrationCallback
41
+ down: MigrationCallback
42
+
43
+
44
+ def normalize_migration_steps(
45
+ steps: MigrationStep | list[MigrationStep],
46
+ ) -> list[MigrationCallbackPair]:
47
+ """Get a list of migration steps in generic format (see the `MigrationStep` type) and convert
48
+ it into the list of `MigrationCallbackPair` instances.
49
+
50
+ Each step can be one of the following variants:
51
+
52
+ * `None` or `tuple[None, None]` - the empty step is converted to the "noop-noop" pair
53
+ * `str | MigrationCallback` or `tuple[str | MigrationCallback, None]` - up-only step is converted to "up-noop" pair
54
+ * `tuple[str | MigrationCallback, str | MigrationCallback]` - the full step is converted to the "up-down" pair
55
+ * `tuple[None, str | MigrationCallback]` - down-only step is converted to "noop-down" pair
56
+ """
57
+ steps = _generic_steps_to_list(steps)
58
+ return [_step_to_callback_pair(s) for s in steps]
59
+
60
+
61
+ def get_migration_name(mod: MigrationModule) -> str:
62
+ """Extract migration name from the module
63
+
64
+ Raises:
65
+ InvalidModuleStructureError: If module does not have the `__name__` attribute
66
+ """
67
+ try:
68
+ return mod.__name__
69
+ except AttributeError:
70
+ raise InvalidModuleStructureError(
71
+ f"Module {mod} does not have __name__ attribute. "
72
+ "Make sure that module is a valid Python module.",
73
+ )
74
+
75
+
76
+ def noop_migration(_conn: Connection):
77
+ """Follows the migration callback interface and do nothing"""
78
+
79
+
80
+ def _generic_steps_to_list(steps: MigrationStep | list[MigrationStep]) -> list[MigrationStep]:
81
+ """Convert migration steps into a list of migration steps.
82
+
83
+ The `steps` could be:
84
+
85
+ * a single migration step - the step is wrapped into the list with the single item
86
+ * a list of migration steps - a copy of the list is returned
87
+ """
88
+ if isinstance(steps, list):
89
+ # nothing to do - list of steps
90
+ return list(steps)
91
+ else:
92
+ # single step
93
+ return [steps]
94
+
95
+
96
+ def _step_to_callback_pair(step: MigrationStep | None) -> MigrationCallbackPair:
97
+ """Convert a generic step into a `MigrationCallbackPair` instance.
98
+
99
+ See the `normalize_migration_steps` function for more details.
100
+ """
101
+ step_tuple = _step_to_tuple(step)
102
+ up = _statement_to_callback(step_tuple[0])
103
+ down = _statement_to_callback(step_tuple[1])
104
+ return MigrationCallbackPair(up, down)
105
+
106
+
107
+ def _step_to_tuple(
108
+ step: MigrationStep | None,
109
+ ) -> tuple[MigrationStatement | None, MigrationStatement | None]:
110
+ """Convert step to tuple. Each element of the tuple is a migration statement or `None`."""
111
+ if step is None:
112
+ return None, None
113
+ elif isinstance(step, tuple):
114
+ return step
115
+ else:
116
+ return step, None
117
+
118
+
119
+ def _statement_to_callback(stmt: MigrationStatement | None) -> MigrationCallback:
120
+ """Check the type of the stmt. If it is the `str`, convert it into the callback"""
121
+
122
+ if stmt is None:
123
+ return noop_migration
124
+ if callable(stmt):
125
+ return stmt
126
+ else:
127
+ return _make_callback_from_string(stmt)
128
+
129
+
130
+ def _make_callback_from_string(stmt: str) -> MigrationCallback:
131
+ """Convert the string statement into the `MigrationCallback`."""
132
+
133
+ def _callback(conn: Connection):
134
+ conn.execute(text(stmt))
135
+
136
+ return _callback
@@ -0,0 +1,115 @@
1
+ # MIT License
2
+ #
3
+ # Copyright (c) 2026 Authors and contributors listed in the AUTHORS file
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in all
13
+ # copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
22
+
23
+ from sa_values import SaValues
24
+ from sqlalchemy import Connection
25
+ from sqlalchemy.exc import DBAPIError
26
+
27
+ from .exceptions import InvalidModuleStructureError, MigrationFailedError
28
+ from .migrations import get_migration_name, MigrationCallbackPair, normalize_migration_steps
29
+ from .types import MigrationCallback, MigrationModule
30
+
31
+
32
+ class Minimi:
33
+ """Apply or roll back migrations"""
34
+
35
+ SA_VALUE_MIGRATION_KEY = "minimi.migration"
36
+
37
+ def __init__(self, connection: Connection, migrations: list[MigrationModule]):
38
+ self.connection = connection
39
+ self.migrations = migrations
40
+ self._applied_migrations = SaValues(self.connection).multi_value_key(
41
+ self.SA_VALUE_MIGRATION_KEY,
42
+ )
43
+
44
+ def apply(self):
45
+ """Apply all unapplied migrations"""
46
+ applied_migrations = set(self._applied_migrations.get_all())
47
+ for mod in self.migrations:
48
+ migration_name = get_migration_name(mod)
49
+ if migration_name in applied_migrations:
50
+ continue
51
+ self._apply_migration(mod)
52
+
53
+ try:
54
+ self._applied_migrations.add(migration_name)
55
+ except Exception:
56
+ try:
57
+ self._rollback_migration(mod)
58
+ except Exception: # noqa
59
+ # TODO: log error and better exception catching
60
+ pass
61
+
62
+ raise
63
+
64
+ def rollback(self):
65
+ """Roll back all migrations"""
66
+ applied_migrations = set(self._applied_migrations.get_all())
67
+
68
+ for m in reversed(self.migrations):
69
+ migration_name = get_migration_name(m)
70
+ if migration_name not in applied_migrations:
71
+ continue
72
+ self._rollback_migration(m)
73
+ self._applied_migrations.delete(migration_name)
74
+
75
+ def _apply_migration(self, mod: MigrationModule) -> None:
76
+ """Apply a single migration module"""
77
+ steps = self._get_normalized_steps(mod)
78
+ applied_steps = []
79
+ for step in steps:
80
+ try:
81
+ self._call_callback(step.up)
82
+ except MigrationFailedError:
83
+ for revert_step in reversed(applied_steps):
84
+ try:
85
+ self._call_callback(revert_step.down)
86
+ except MigrationFailedError:
87
+ # stop on rollback failure
88
+ break
89
+ raise
90
+ applied_steps.append(step)
91
+
92
+ def _rollback_migration(self, mod: MigrationModule) -> None:
93
+ """Roll back a single migration module"""
94
+ steps = self._get_normalized_steps(mod)
95
+ for step in reversed(steps):
96
+ self._call_callback(step.down)
97
+
98
+ def _call_callback(self, cbk: MigrationCallback | None):
99
+ """Call the migration callback and handle DB related errors."""
100
+ if cbk is None:
101
+ return
102
+
103
+ try:
104
+ cbk(self.connection)
105
+ except DBAPIError as err:
106
+ raise MigrationFailedError from err
107
+
108
+ @staticmethod
109
+ def _get_normalized_steps(mod) -> list[MigrationCallbackPair]:
110
+ try:
111
+ return normalize_migration_steps(mod.MIGRATIONS)
112
+ except AttributeError:
113
+ raise InvalidModuleStructureError(
114
+ f"Module {mod.__name__} does not have a MIGRATIONS attribute",
115
+ )
@@ -0,0 +1,45 @@
1
+ # MIT License
2
+ #
3
+ # Copyright (c) 2026 Authors and contributors listed in the AUTHORS file
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in all
13
+ # copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
22
+
23
+ from collections.abc import Callable
24
+ from typing import Protocol
25
+
26
+ from sqlalchemy import Connection
27
+
28
+
29
+ MigrationCallback = Callable[[Connection], None]
30
+
31
+ MigrationStatement = str | MigrationCallback
32
+ """Single migration statement or callable with execution logic"""
33
+
34
+ MigrationStep = MigrationStatement | tuple[MigrationStatement | None, MigrationStatement | None]
35
+ """One step of migration. One migration can contain multiple steps"""
36
+
37
+
38
+ class MigrationModule(Protocol):
39
+ """Each migration module must contain list of migrations in the `MIGRATIONS` global variable."""
40
+
41
+ __name__: str
42
+ """Name of the migration module"""
43
+
44
+ MIGRATIONS: MigrationStep | list[MigrationStep]
45
+ """Migration step or list of migration steps. The container MUST be the `list`"""
@@ -0,0 +1,138 @@
1
+ Metadata-Version: 2.4
2
+ Name: minimi-para
3
+ Version: 0.1.0
4
+ Summary: A mini migration tool
5
+ Author-email: Petr Jindra <el.mordo@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/elmordo/minimi
8
+ Project-URL: Repository, https://github.com/elmordo/minimi
9
+ Keywords: migrations,sqlalchemy,database
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Requires-Python: >=3.12
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ License-File: AUTHORS
20
+ Requires-Dist: sqlalchemy<3.0,>=2.0.52
21
+ Requires-Dist: sa-values<0.2,>=0.1.0
22
+ Provides-Extra: test
23
+ Requires-Dist: pytest; extra == "test"
24
+ Provides-Extra: dev
25
+ Requires-Dist: sa-values[test]; extra == "dev"
26
+ Requires-Dist: build; extra == "dev"
27
+ Requires-Dist: twine; extra == "dev"
28
+ Dynamic: license-file
29
+
30
+ **MINI**mal **MI**gration library tool for managing database migrations in small projects where
31
+ a full-featured migration tool would be overkill.
32
+
33
+ Why the "_para_" suffix? The original name of the library `minimi` was rejected by the pypi.org because
34
+ it is too similar to another library. Also, the Minimi Para is compatct version of the original Minimi :-)
35
+
36
+ ```
37
+
38
+ ====
39
+ __@----------------\ \
40
+ ||=========||==== ===\_____\___________ // //
41
+ || ||==== |======|=| ALTER TABLE users DROP COLUMN email
42
+ ||=========||---_____======------------------------|=| \\ \\
43
+ / / \\ / \____________| ||
44
+ / /------ ||
45
+ /___/ ||
46
+ ||
47
+ ====
48
+ ```
49
+
50
+ # Quick start
51
+
52
+ Create a migration module in your source directory
53
+
54
+ ```
55
+ src
56
+ +- mylib
57
+ +- migrations
58
+ | +- __init__.py <- list of migration modules
59
+ | +- m01_db_init.py <- first revision
60
+ | +- m02_new_table.py <- second revision
61
+ +- other module
62
+ +- main.py
63
+ ```
64
+
65
+ ```python
66
+ # __init__.py
67
+
68
+ from . import m01_db_init, m02_new_table
69
+
70
+ MIGRATIONS = [
71
+ m01_db_init,
72
+ m02_new_table,
73
+ ]
74
+ ```
75
+
76
+ ```python
77
+ # m01_db_init.py
78
+
79
+ # the UP migration only
80
+ MIGRATIONS = """
81
+ CREATE TABLE IF NOT EXISTS users (
82
+ id INTEGER PRIMARY KEY,
83
+ name TEXT NOT NULL,
84
+ email TEXT NOT NULL UNIQUE
85
+ );
86
+ """
87
+ ```
88
+
89
+ ```python
90
+ # m02_new_table.py
91
+
92
+ # the UP and DOWN migrations as tuple of two strings
93
+ MIGRATIONS = """
94
+ CREATE TABLE IF NOT EXISTS user_comments (
95
+ id INTEGER PRIMARY KEY,
96
+ user_id INTEGER NOT NULL
97
+ REFERENCES users(id),
98
+ subjects TEXT NOT NULL,
99
+ message TEXT NOT NULL
100
+ );
101
+ """, """
102
+ DROP TABLE IF EXISTS user_comments;
103
+ """
104
+ ```
105
+
106
+ ```python
107
+ # main.py
108
+
109
+ from minimi import Minimi
110
+ from sqlalchemy import create_engine
111
+ from sa_values import setup_sa_values
112
+
113
+ import mylib.migrations as migrations
114
+
115
+
116
+ def main():
117
+ # initialize the sa_values first
118
+ conn = create_engine("sqlite:///:memory:").connect()
119
+ setup_sa_values(conn)
120
+ # run the migrations
121
+ Minimi(conn, migrations).apply()
122
+
123
+
124
+ if __name__ == "__main__":
125
+ main()
126
+ ```
127
+
128
+ # Usage
129
+
130
+ The `__init__.py` file contains a list of migration modules in the `MIGRATIONS` global variable with list of migration
131
+ modules.
132
+
133
+ Each migration module must contain the `MIGRATION` global variable with the migration.
134
+
135
+ # Buy me a ~~coffee~~ beer
136
+
137
+ If you like this library, or you want to support its development, support me by one
138
+ cold [beer](https://www.buymeacoffee.com/elmordo). The beer is tasty and full of vitamins :-)
@@ -0,0 +1,14 @@
1
+ AUTHORS
2
+ LICENSE
3
+ README.md
4
+ pyproject.toml
5
+ src/minimi/__init__.py
6
+ src/minimi/exceptions.py
7
+ src/minimi/migrations.py
8
+ src/minimi/minimi.py
9
+ src/minimi/types.py
10
+ src/minimi_para.egg-info/PKG-INFO
11
+ src/minimi_para.egg-info/SOURCES.txt
12
+ src/minimi_para.egg-info/dependency_links.txt
13
+ src/minimi_para.egg-info/requires.txt
14
+ src/minimi_para.egg-info/top_level.txt
@@ -0,0 +1,10 @@
1
+ sqlalchemy<3.0,>=2.0.52
2
+ sa-values<0.2,>=0.1.0
3
+
4
+ [dev]
5
+ sa-values[test]
6
+ build
7
+ twine
8
+
9
+ [test]
10
+ pytest