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.
- minimi_para-0.1.0/AUTHORS +1 -0
- minimi_para-0.1.0/LICENSE +21 -0
- minimi_para-0.1.0/PKG-INFO +138 -0
- minimi_para-0.1.0/README.md +109 -0
- minimi_para-0.1.0/pyproject.toml +63 -0
- minimi_para-0.1.0/setup.cfg +4 -0
- minimi_para-0.1.0/src/minimi/__init__.py +26 -0
- minimi_para-0.1.0/src/minimi/exceptions.py +33 -0
- minimi_para-0.1.0/src/minimi/migrations.py +136 -0
- minimi_para-0.1.0/src/minimi/minimi.py +115 -0
- minimi_para-0.1.0/src/minimi/types.py +45 -0
- minimi_para-0.1.0/src/minimi_para.egg-info/PKG-INFO +138 -0
- minimi_para-0.1.0/src/minimi_para.egg-info/SOURCES.txt +14 -0
- minimi_para-0.1.0/src/minimi_para.egg-info/dependency_links.txt +1 -0
- minimi_para-0.1.0/src/minimi_para.egg-info/requires.txt +10 -0
- minimi_para-0.1.0/src/minimi_para.egg-info/top_level.txt +1 -0
|
@@ -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,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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
minimi
|