alembic-utils-extended 0.0.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.
- alembic_utils_extended-0.0.0/LICENSE +21 -0
- alembic_utils_extended-0.0.0/PKG-INFO +165 -0
- alembic_utils_extended-0.0.0/README.md +137 -0
- alembic_utils_extended-0.0.0/pyproject.toml +92 -0
- alembic_utils_extended-0.0.0/src/alembic_utils_extended/__init__.py +0 -0
- alembic_utils_extended-0.0.0/src/alembic_utils_extended/depends.py +145 -0
- alembic_utils_extended-0.0.0/src/alembic_utils_extended/exceptions.py +18 -0
- alembic_utils_extended-0.0.0/src/alembic_utils_extended/experimental/__init__.py +6 -0
- alembic_utils_extended-0.0.0/src/alembic_utils_extended/experimental/_collect_instances.py +90 -0
- alembic_utils_extended-0.0.0/src/alembic_utils_extended/on_entity_mixin.py +51 -0
- alembic_utils_extended-0.0.0/src/alembic_utils_extended/pg_extension.py +83 -0
- alembic_utils_extended-0.0.0/src/alembic_utils_extended/pg_function.py +169 -0
- alembic_utils_extended-0.0.0/src/alembic_utils_extended/pg_grant_table.py +232 -0
- alembic_utils_extended-0.0.0/src/alembic_utils_extended/pg_materialized_view.py +141 -0
- alembic_utils_extended-0.0.0/src/alembic_utils_extended/pg_policy.py +114 -0
- alembic_utils_extended-0.0.0/src/alembic_utils_extended/pg_trigger.py +169 -0
- alembic_utils_extended-0.0.0/src/alembic_utils_extended/pg_view.py +107 -0
- alembic_utils_extended-0.0.0/src/alembic_utils_extended/py.typed +0 -0
- alembic_utils_extended-0.0.0/src/alembic_utils_extended/replaceable_entity.py +438 -0
- alembic_utils_extended-0.0.0/src/alembic_utils_extended/reversible_op.py +172 -0
- alembic_utils_extended-0.0.0/src/alembic_utils_extended/simulate.py +67 -0
- alembic_utils_extended-0.0.0/src/alembic_utils_extended/statement.py +75 -0
- alembic_utils_extended-0.0.0/src/alembic_utils_extended/testbase.py +49 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2020 Candid Health
|
|
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,165 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: alembic-utils-extended
|
|
3
|
+
Version: 0.0.0
|
|
4
|
+
Summary: A sqlalchemy/alembic extension for migrating procedures and views
|
|
5
|
+
License: MIT
|
|
6
|
+
Author: Justin Malin
|
|
7
|
+
Author-email: justin@joincandidhealth.com
|
|
8
|
+
Requires-Python: >=3.9
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: SQL
|
|
19
|
+
Requires-Dist: alembic (>=1.9)
|
|
20
|
+
Requires-Dist: flupy
|
|
21
|
+
Requires-Dist: parse (>=1.8.4)
|
|
22
|
+
Requires-Dist: sqlalchemy (>=1.4)
|
|
23
|
+
Requires-Dist: typing_extensions (>=0.1.0)
|
|
24
|
+
Project-URL: GitHub, https://github.com/candidhealth/alembic-utils-extended
|
|
25
|
+
Project-URL: PyPI, https://pypi.org/project/alembic-utils/
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# Alembic Utils
|
|
29
|
+
|
|
30
|
+
<p>
|
|
31
|
+
<a href="https://github.com/candidhealth/alembic-utils-extended/actions">
|
|
32
|
+
<img src="https://github.com/candidhealth/alembic-utils-extended/workflows/Tests/badge.svg" alt="Test Status" height="18">
|
|
33
|
+
</a>
|
|
34
|
+
<a href="https://github.com/candidhealth/alembic-utils-extended/actions">
|
|
35
|
+
<img src="https://github.com/candidhealth/alembic-utils-extended/workflows/pre-commit%20hooks/badge.svg" alt="Pre-commit Status" height="18">
|
|
36
|
+
</a>
|
|
37
|
+
<a href="https://codecov.io/gh/candidhealth/alembic-utils-extended"><img src="https://codecov.io/gh/candidhealth/alembic-utils-extended/branch/master/graph/badge.svg" height="18"></a>
|
|
38
|
+
</p>
|
|
39
|
+
<p>
|
|
40
|
+
<a href="https://github.com/candidhealth/alembic-utils-extended/blob/master/LICENSE"><img src="https://img.shields.io/pypi/l/markdown-subtemplate.svg" alt="License" height="18"></a>
|
|
41
|
+
<a href="https://badge.fury.io/py/alembic_utils"><img src="https://badge.fury.io/py/alembic_utils.svg" alt="PyPI version" height="18"></a>
|
|
42
|
+
<a href="https://github.com/psf/black">
|
|
43
|
+
<img src="https://img.shields.io/badge/code%20style-black-000000.svg" alt="Codestyle Black" height="18">
|
|
44
|
+
</a>
|
|
45
|
+
<a href="https://pypi.org/project/alembic_utils/"><img src="https://img.shields.io/pypi/dm/alembic_utils.svg" alt="Download count" height="18"></a>
|
|
46
|
+
</p>
|
|
47
|
+
<p>
|
|
48
|
+
<a href="https://www.python.org/downloads/"><img src="https://img.shields.io/badge/python-3.6+-blue.svg" alt="Python version" height="18"></a>
|
|
49
|
+
<a href=""><img src="https://img.shields.io/badge/postgresql-11+-blue.svg" alt="PostgreSQL version" height="18"></a>
|
|
50
|
+
</p>
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
**Documentation**: <a href="https://olirice.github.io/alembic_utils" target="_blank">https://olirice.github.io/alembic_utils</a>
|
|
55
|
+
|
|
56
|
+
**Source Code**: <a href="https://github.com/candidhealth/alembic-utils-extended" target="_blank">https://github.com/candidhealth/alembic-utils-extended</a>
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
**Autogenerate Support for PostgreSQL Functions, Views, Materialized View, Triggers, and Policies**
|
|
61
|
+
|
|
62
|
+
[Alembic](https://alembic.sqlalchemy.org/en/latest/) is the defacto migration tool for use with [SQLAlchemy](https://www.sqlalchemy.org/). Without extensions, alembic can detect local changes to SQLAlchemy models and autogenerate a database migration or "revision" script. That revision can be applied to update the database's schema to match the SQLAlchemy model definitions.
|
|
63
|
+
|
|
64
|
+
Alembic Utils is an extension to alembic that adds support for autogenerating a larger number of [PostgreSQL](https://www.postgresql.org/) entity types, including [functions](https://www.postgresql.org/docs/current/sql-createfunction.html), [views](https://www.postgresql.org/docs/current/sql-createview.html), [materialized views](https://www.postgresql.org/docs/current/sql-creatematerializedview.html), [triggers](https://www.postgresql.org/docs/current/sql-createtrigger.html), and [policies](https://www.postgresql.org/docs/current/sql-createpolicy.html).
|
|
65
|
+
|
|
66
|
+
### TL;DR
|
|
67
|
+
|
|
68
|
+
Update alembic's `env.py` to register a function or view:
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
# migrations/env.py
|
|
72
|
+
from alembic_utils_extended.pg_function import PGFunction
|
|
73
|
+
from alembic_utils_extended.replaceable_entity import register_entities
|
|
74
|
+
|
|
75
|
+
to_upper = PGFunction(
|
|
76
|
+
schema='public',
|
|
77
|
+
signature='to_upper(some_text text)',
|
|
78
|
+
definition="""
|
|
79
|
+
RETURNS text as
|
|
80
|
+
$$
|
|
81
|
+
SELECT upper(some_text)
|
|
82
|
+
$$ language SQL;
|
|
83
|
+
"""
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
register_entities([to_upper])
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
You're done!
|
|
90
|
+
|
|
91
|
+
The next time you autogenerate a revision with
|
|
92
|
+
```shell
|
|
93
|
+
alembic revision --autogenerate -m 'create to_upper'
|
|
94
|
+
```
|
|
95
|
+
Alembic will detect if your entities are new, updated, or removed & populate the revison's `upgrade` and `downgrade` sections automatically.
|
|
96
|
+
|
|
97
|
+
For example:
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
"""create to_upper
|
|
101
|
+
|
|
102
|
+
Revision ID: 8efi0da3a4
|
|
103
|
+
Revises:
|
|
104
|
+
Create Date: 2020-04-22 09:24:25.556995
|
|
105
|
+
"""
|
|
106
|
+
from alembic import op
|
|
107
|
+
import sqlalchemy as sa
|
|
108
|
+
from alembic_utils_extended.pg_function import PGFunction
|
|
109
|
+
|
|
110
|
+
# revision identifiers, used by Alembic.
|
|
111
|
+
revision = '8efi0da3a4'
|
|
112
|
+
down_revision = None
|
|
113
|
+
branch_labels = None
|
|
114
|
+
depends_on = None
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def upgrade():
|
|
118
|
+
public_to_upper_6fa0de = PGFunction(
|
|
119
|
+
schema="public",
|
|
120
|
+
signature="to_upper(some_text text)",
|
|
121
|
+
definition="""
|
|
122
|
+
returns text
|
|
123
|
+
as
|
|
124
|
+
$$ select upper(some_text) $$ language SQL;
|
|
125
|
+
"""
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
op.create_entity(public_to_upper_6fa0de)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def downgrade():
|
|
132
|
+
public_to_upper_6fa0de = PGFunction(
|
|
133
|
+
schema="public",
|
|
134
|
+
signature="to_upper(some_text text)",
|
|
135
|
+
definition="# Not Used"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
op.drop_entity(public_to_upper_6fa0de)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
Visit the [quickstart guide](https://candidhealth.github.io/alembic-utils-extended/quickstart/) for usage instructions.
|
|
143
|
+
|
|
144
|
+
<p align="center">—— ——</p>
|
|
145
|
+
|
|
146
|
+
### Contributing
|
|
147
|
+
|
|
148
|
+
To run the tests
|
|
149
|
+
```
|
|
150
|
+
# install pip dependencies
|
|
151
|
+
pip install wheel && pip install -e ".[dev]"
|
|
152
|
+
|
|
153
|
+
# run the tests
|
|
154
|
+
pytest src/test
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
To invoke the linter automated formatting and generally make use of precommit checks:
|
|
158
|
+
```
|
|
159
|
+
pip install pre-commit
|
|
160
|
+
pre-commit install
|
|
161
|
+
|
|
162
|
+
# manually run
|
|
163
|
+
pre-commit run --all
|
|
164
|
+
```
|
|
165
|
+
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# Alembic Utils
|
|
2
|
+
|
|
3
|
+
<p>
|
|
4
|
+
<a href="https://github.com/candidhealth/alembic-utils-extended/actions">
|
|
5
|
+
<img src="https://github.com/candidhealth/alembic-utils-extended/workflows/Tests/badge.svg" alt="Test Status" height="18">
|
|
6
|
+
</a>
|
|
7
|
+
<a href="https://github.com/candidhealth/alembic-utils-extended/actions">
|
|
8
|
+
<img src="https://github.com/candidhealth/alembic-utils-extended/workflows/pre-commit%20hooks/badge.svg" alt="Pre-commit Status" height="18">
|
|
9
|
+
</a>
|
|
10
|
+
<a href="https://codecov.io/gh/candidhealth/alembic-utils-extended"><img src="https://codecov.io/gh/candidhealth/alembic-utils-extended/branch/master/graph/badge.svg" height="18"></a>
|
|
11
|
+
</p>
|
|
12
|
+
<p>
|
|
13
|
+
<a href="https://github.com/candidhealth/alembic-utils-extended/blob/master/LICENSE"><img src="https://img.shields.io/pypi/l/markdown-subtemplate.svg" alt="License" height="18"></a>
|
|
14
|
+
<a href="https://badge.fury.io/py/alembic_utils"><img src="https://badge.fury.io/py/alembic_utils.svg" alt="PyPI version" height="18"></a>
|
|
15
|
+
<a href="https://github.com/psf/black">
|
|
16
|
+
<img src="https://img.shields.io/badge/code%20style-black-000000.svg" alt="Codestyle Black" height="18">
|
|
17
|
+
</a>
|
|
18
|
+
<a href="https://pypi.org/project/alembic_utils/"><img src="https://img.shields.io/pypi/dm/alembic_utils.svg" alt="Download count" height="18"></a>
|
|
19
|
+
</p>
|
|
20
|
+
<p>
|
|
21
|
+
<a href="https://www.python.org/downloads/"><img src="https://img.shields.io/badge/python-3.6+-blue.svg" alt="Python version" height="18"></a>
|
|
22
|
+
<a href=""><img src="https://img.shields.io/badge/postgresql-11+-blue.svg" alt="PostgreSQL version" height="18"></a>
|
|
23
|
+
</p>
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
**Documentation**: <a href="https://olirice.github.io/alembic_utils" target="_blank">https://olirice.github.io/alembic_utils</a>
|
|
28
|
+
|
|
29
|
+
**Source Code**: <a href="https://github.com/candidhealth/alembic-utils-extended" target="_blank">https://github.com/candidhealth/alembic-utils-extended</a>
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
**Autogenerate Support for PostgreSQL Functions, Views, Materialized View, Triggers, and Policies**
|
|
34
|
+
|
|
35
|
+
[Alembic](https://alembic.sqlalchemy.org/en/latest/) is the defacto migration tool for use with [SQLAlchemy](https://www.sqlalchemy.org/). Without extensions, alembic can detect local changes to SQLAlchemy models and autogenerate a database migration or "revision" script. That revision can be applied to update the database's schema to match the SQLAlchemy model definitions.
|
|
36
|
+
|
|
37
|
+
Alembic Utils is an extension to alembic that adds support for autogenerating a larger number of [PostgreSQL](https://www.postgresql.org/) entity types, including [functions](https://www.postgresql.org/docs/current/sql-createfunction.html), [views](https://www.postgresql.org/docs/current/sql-createview.html), [materialized views](https://www.postgresql.org/docs/current/sql-creatematerializedview.html), [triggers](https://www.postgresql.org/docs/current/sql-createtrigger.html), and [policies](https://www.postgresql.org/docs/current/sql-createpolicy.html).
|
|
38
|
+
|
|
39
|
+
### TL;DR
|
|
40
|
+
|
|
41
|
+
Update alembic's `env.py` to register a function or view:
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
# migrations/env.py
|
|
45
|
+
from alembic_utils_extended.pg_function import PGFunction
|
|
46
|
+
from alembic_utils_extended.replaceable_entity import register_entities
|
|
47
|
+
|
|
48
|
+
to_upper = PGFunction(
|
|
49
|
+
schema='public',
|
|
50
|
+
signature='to_upper(some_text text)',
|
|
51
|
+
definition="""
|
|
52
|
+
RETURNS text as
|
|
53
|
+
$$
|
|
54
|
+
SELECT upper(some_text)
|
|
55
|
+
$$ language SQL;
|
|
56
|
+
"""
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
register_entities([to_upper])
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
You're done!
|
|
63
|
+
|
|
64
|
+
The next time you autogenerate a revision with
|
|
65
|
+
```shell
|
|
66
|
+
alembic revision --autogenerate -m 'create to_upper'
|
|
67
|
+
```
|
|
68
|
+
Alembic will detect if your entities are new, updated, or removed & populate the revison's `upgrade` and `downgrade` sections automatically.
|
|
69
|
+
|
|
70
|
+
For example:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
"""create to_upper
|
|
74
|
+
|
|
75
|
+
Revision ID: 8efi0da3a4
|
|
76
|
+
Revises:
|
|
77
|
+
Create Date: 2020-04-22 09:24:25.556995
|
|
78
|
+
"""
|
|
79
|
+
from alembic import op
|
|
80
|
+
import sqlalchemy as sa
|
|
81
|
+
from alembic_utils_extended.pg_function import PGFunction
|
|
82
|
+
|
|
83
|
+
# revision identifiers, used by Alembic.
|
|
84
|
+
revision = '8efi0da3a4'
|
|
85
|
+
down_revision = None
|
|
86
|
+
branch_labels = None
|
|
87
|
+
depends_on = None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def upgrade():
|
|
91
|
+
public_to_upper_6fa0de = PGFunction(
|
|
92
|
+
schema="public",
|
|
93
|
+
signature="to_upper(some_text text)",
|
|
94
|
+
definition="""
|
|
95
|
+
returns text
|
|
96
|
+
as
|
|
97
|
+
$$ select upper(some_text) $$ language SQL;
|
|
98
|
+
"""
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
op.create_entity(public_to_upper_6fa0de)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def downgrade():
|
|
105
|
+
public_to_upper_6fa0de = PGFunction(
|
|
106
|
+
schema="public",
|
|
107
|
+
signature="to_upper(some_text text)",
|
|
108
|
+
definition="# Not Used"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
op.drop_entity(public_to_upper_6fa0de)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
Visit the [quickstart guide](https://candidhealth.github.io/alembic-utils-extended/quickstart/) for usage instructions.
|
|
116
|
+
|
|
117
|
+
<p align="center">—— ——</p>
|
|
118
|
+
|
|
119
|
+
### Contributing
|
|
120
|
+
|
|
121
|
+
To run the tests
|
|
122
|
+
```
|
|
123
|
+
# install pip dependencies
|
|
124
|
+
pip install wheel && pip install -e ".[dev]"
|
|
125
|
+
|
|
126
|
+
# run the tests
|
|
127
|
+
pytest src/test
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
To invoke the linter automated formatting and generally make use of precommit checks:
|
|
131
|
+
```
|
|
132
|
+
pip install pre-commit
|
|
133
|
+
pre-commit install
|
|
134
|
+
|
|
135
|
+
# manually run
|
|
136
|
+
pre-commit run --all
|
|
137
|
+
```
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "alembic-utils-extended"
|
|
3
|
+
version = "0.0.0"
|
|
4
|
+
description = "A sqlalchemy/alembic extension for migrating procedures and views"
|
|
5
|
+
requires-python = ">=3.9"
|
|
6
|
+
authors = [{ "name" = "Justin Malin", "email" = "justin@joincandidhealth.com" }]
|
|
7
|
+
license = "MIT"
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
packages = [{ include = "alembic-utils-extended", from = "src" }]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 4 - Beta",
|
|
12
|
+
"License :: OSI Approved :: MIT License",
|
|
13
|
+
"Intended Audience :: Developers",
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Programming Language :: Python :: 3.9",
|
|
16
|
+
"Programming Language :: Python :: 3.10",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Programming Language :: Python :: 3.13",
|
|
20
|
+
"Programming Language :: SQL",
|
|
21
|
+
]
|
|
22
|
+
dependencies = [
|
|
23
|
+
"alembic>=1.9",
|
|
24
|
+
"flupy",
|
|
25
|
+
"parse>=1.8.4",
|
|
26
|
+
"sqlalchemy>=1.4",
|
|
27
|
+
"typing_extensions>=0.1.0",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[build-system]
|
|
31
|
+
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
|
32
|
+
build-backend = "poetry.core.masonry.api"
|
|
33
|
+
|
|
34
|
+
[tool.poetry.urls]
|
|
35
|
+
"PyPI" = "https://pypi.org/project/alembic-utils/"
|
|
36
|
+
"GitHub" = "https://github.com/candidhealth/alembic-utils-extended"
|
|
37
|
+
|
|
38
|
+
[tool.poetry.group.dev.dependencies]
|
|
39
|
+
black = "*"
|
|
40
|
+
pylint = "*"
|
|
41
|
+
pre-commit = "*"
|
|
42
|
+
mypy = "*"
|
|
43
|
+
psycopg2-binary = "*"
|
|
44
|
+
pytest = "*"
|
|
45
|
+
pytest-cov = "*"
|
|
46
|
+
mkdocs = "*"
|
|
47
|
+
|
|
48
|
+
[tool.poetry.group.nvim.dependencies]
|
|
49
|
+
neovim = "*"
|
|
50
|
+
python-language-server = "*"
|
|
51
|
+
|
|
52
|
+
[tool.poetry.group.docs.dependencies]
|
|
53
|
+
mkdocs = "*"
|
|
54
|
+
pygments = "*"
|
|
55
|
+
pymdown-extensions = "*"
|
|
56
|
+
mkautodoc = "*"
|
|
57
|
+
|
|
58
|
+
[tool.setuptools.packages.find]
|
|
59
|
+
where = ["src"]
|
|
60
|
+
|
|
61
|
+
[tool.setuptools.package-data]
|
|
62
|
+
"*" = ["py.typed"]
|
|
63
|
+
|
|
64
|
+
[tool.black]
|
|
65
|
+
line-length = 100
|
|
66
|
+
exclude = '''
|
|
67
|
+
/(
|
|
68
|
+
\.git
|
|
69
|
+
| \.hg
|
|
70
|
+
| \.mypy_cache
|
|
71
|
+
| \.tox
|
|
72
|
+
| \.venv
|
|
73
|
+
| _build
|
|
74
|
+
| buck-out
|
|
75
|
+
| build
|
|
76
|
+
| dist
|
|
77
|
+
)/
|
|
78
|
+
'''
|
|
79
|
+
|
|
80
|
+
[tool.isort]
|
|
81
|
+
known_third_party = ["alembic", "flupy", "parse", "pytest", "setuptools", "sqlalchemy"]
|
|
82
|
+
|
|
83
|
+
[tool.mypy]
|
|
84
|
+
follow_imports = "skip"
|
|
85
|
+
strict_optional = true
|
|
86
|
+
warn_redundant_casts = true
|
|
87
|
+
warn_unused_ignores = false
|
|
88
|
+
disallow_any_generics = true
|
|
89
|
+
check_untyped_defs = true
|
|
90
|
+
no_implicit_reexport = true
|
|
91
|
+
ignore_missing_imports = true
|
|
92
|
+
# disallow_untyped_defs = true
|
|
File without changes
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from contextlib import contextmanager
|
|
3
|
+
from typing import Generator, List
|
|
4
|
+
|
|
5
|
+
from sqlalchemy import exc as sqla_exc
|
|
6
|
+
from sqlalchemy.orm import Session
|
|
7
|
+
|
|
8
|
+
from alembic_utils_extended.simulate import simulate_entity
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def solve_resolution_order(sess: Session, entities):
|
|
14
|
+
"""Solve for an entity resolution order that increases the probability that
|
|
15
|
+
a migration will suceed if, for example, two new views are created and one
|
|
16
|
+
refers to the other
|
|
17
|
+
|
|
18
|
+
This strategy will only solve for simple cases
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
resolved = []
|
|
22
|
+
|
|
23
|
+
# Resolve the entities with 0 dependencies first (faster)
|
|
24
|
+
logger.info("Resolving entities with no dependencies")
|
|
25
|
+
for entity in entities:
|
|
26
|
+
try:
|
|
27
|
+
with simulate_entity(sess, entity):
|
|
28
|
+
resolved.append(entity)
|
|
29
|
+
except (sqla_exc.ProgrammingError, sqla_exc.InternalError) as exc:
|
|
30
|
+
continue
|
|
31
|
+
|
|
32
|
+
# Resolve entities with possible dependencies
|
|
33
|
+
logger.info("Resolving entities with dependencies. This may take a minute")
|
|
34
|
+
for _ in range(len(entities)):
|
|
35
|
+
n_resolved = len(resolved)
|
|
36
|
+
|
|
37
|
+
for entity in entities:
|
|
38
|
+
if entity in resolved:
|
|
39
|
+
continue
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
with simulate_entity(sess, entity, dependencies=resolved):
|
|
43
|
+
resolved.append(entity)
|
|
44
|
+
except (sqla_exc.ProgrammingError, sqla_exc.InternalError):
|
|
45
|
+
continue
|
|
46
|
+
|
|
47
|
+
if len(resolved) == n_resolved:
|
|
48
|
+
# No new entities resolved in the last iteration. Exit
|
|
49
|
+
break
|
|
50
|
+
|
|
51
|
+
for entity in entities:
|
|
52
|
+
if entity not in resolved:
|
|
53
|
+
resolved.append(entity)
|
|
54
|
+
|
|
55
|
+
return resolved
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@contextmanager
|
|
59
|
+
def recreate_dropped(connection) -> Generator[Session, None, None]:
|
|
60
|
+
"""Recreate any dropped all ReplaceableEntities that were dropped within block
|
|
61
|
+
|
|
62
|
+
This is useful for making cascading updates. For example, updating a table's column type when it has dependent views.
|
|
63
|
+
|
|
64
|
+
def upgrade() -> None:
|
|
65
|
+
|
|
66
|
+
my_view = PGView(...)
|
|
67
|
+
|
|
68
|
+
with recreate_dropped(op.get_bind()) as conn:
|
|
69
|
+
|
|
70
|
+
op.drop_entity(my_view)
|
|
71
|
+
|
|
72
|
+
# change an integer column to a bigint
|
|
73
|
+
op.alter_column(
|
|
74
|
+
table_name="account",
|
|
75
|
+
column_name="id",
|
|
76
|
+
schema="public"
|
|
77
|
+
type_=sa.BIGINT()
|
|
78
|
+
existing_type=sa.Integer(),
|
|
79
|
+
)
|
|
80
|
+
"""
|
|
81
|
+
from alembic_utils_extended.pg_function import PGFunction
|
|
82
|
+
from alembic_utils_extended.pg_materialized_view import PGMaterializedView
|
|
83
|
+
from alembic_utils_extended.pg_trigger import PGTrigger
|
|
84
|
+
from alembic_utils_extended.pg_view import PGView
|
|
85
|
+
from alembic_utils_extended.replaceable_entity import ReplaceableEntity
|
|
86
|
+
|
|
87
|
+
# Do not include permissions here e.g. PGGrantTable. If columns granted to users are dropped, it will cause an error
|
|
88
|
+
|
|
89
|
+
def collect_all_db_entities(sess: Session) -> List[ReplaceableEntity]:
|
|
90
|
+
"""Collect all entities from the database"""
|
|
91
|
+
|
|
92
|
+
return [
|
|
93
|
+
*PGFunction.from_database(sess, "%"),
|
|
94
|
+
*PGTrigger.from_database(sess, "%"),
|
|
95
|
+
*PGView.from_database(sess, "%"),
|
|
96
|
+
*PGMaterializedView.from_database(sess, "%"),
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
sess = Session(bind=connection)
|
|
100
|
+
|
|
101
|
+
# All existing entities, before the upgrade
|
|
102
|
+
before = collect_all_db_entities(sess)
|
|
103
|
+
|
|
104
|
+
# In the yield, do a
|
|
105
|
+
# op.drop_entity(my_mat_view, cascade=True)
|
|
106
|
+
# op.create_entity(my_mat_view)
|
|
107
|
+
try:
|
|
108
|
+
yield sess
|
|
109
|
+
except:
|
|
110
|
+
sess.rollback()
|
|
111
|
+
raise
|
|
112
|
+
|
|
113
|
+
# All existing entities, after the upgrade
|
|
114
|
+
after = collect_all_db_entities(sess)
|
|
115
|
+
after_identities = {x.identity for x in after}
|
|
116
|
+
|
|
117
|
+
# Entities that were not impacted, or that we have "recovered"
|
|
118
|
+
resolved = []
|
|
119
|
+
unresolved = []
|
|
120
|
+
|
|
121
|
+
# First, ignore the ones that were not impacted by the upgrade
|
|
122
|
+
for ent in before:
|
|
123
|
+
if ent.identity in after_identities:
|
|
124
|
+
resolved.append(ent)
|
|
125
|
+
else:
|
|
126
|
+
unresolved.append(ent)
|
|
127
|
+
|
|
128
|
+
# Attempt to find an acceptable order of creation for the unresolved entities
|
|
129
|
+
ordered_unresolved = solve_resolution_order(sess, unresolved)
|
|
130
|
+
|
|
131
|
+
# Attempt to recreate the missing entities in the specified order
|
|
132
|
+
for ent in ordered_unresolved:
|
|
133
|
+
sess.execute(ent.to_sql_statement_create())
|
|
134
|
+
|
|
135
|
+
# Sanity check that everything is now fine
|
|
136
|
+
sanity_check = collect_all_db_entities(sess)
|
|
137
|
+
# Fail and rollback if the sanity check is wrong
|
|
138
|
+
try:
|
|
139
|
+
assert len(before) == len(sanity_check)
|
|
140
|
+
except:
|
|
141
|
+
sess.rollback()
|
|
142
|
+
raise
|
|
143
|
+
|
|
144
|
+
# Close out the session
|
|
145
|
+
sess.commit()
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
class AlembicUtilsException(Exception):
|
|
2
|
+
"""Base exception for AlembicUtils package"""
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class SQLParseFailure(AlembicUtilsException):
|
|
6
|
+
"""An entity could not be parsed"""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FailedToGenerateComparable(AlembicUtilsException):
|
|
10
|
+
"""Failed to generate a comparable entity"""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class UnreachableException(AlembicUtilsException):
|
|
14
|
+
"""An exception no one should ever see"""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class BadInputException(AlembicUtilsException):
|
|
18
|
+
"""Invalid user input"""
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from types import ModuleType
|
|
5
|
+
from typing import Generator, List, Type, TypeVar
|
|
6
|
+
|
|
7
|
+
from flupy import walk_files
|
|
8
|
+
|
|
9
|
+
T = TypeVar("T")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def walk_modules(module: ModuleType) -> Generator[ModuleType, None, None]:
|
|
13
|
+
"""Recursively yield python import paths to submodules in *module*
|
|
14
|
+
|
|
15
|
+
Example:
|
|
16
|
+
module_iter = walk_modules(alembic_utils_extended)
|
|
17
|
+
|
|
18
|
+
for module_path in module_iter:
|
|
19
|
+
print(module_path)
|
|
20
|
+
|
|
21
|
+
# alembic_utils_extended.exceptions
|
|
22
|
+
# alembic_utils_extended.on_entity_mixin
|
|
23
|
+
# ...
|
|
24
|
+
"""
|
|
25
|
+
top_module = module
|
|
26
|
+
top_path = Path(top_module.__path__[0])
|
|
27
|
+
top_path_absolute = top_path.resolve()
|
|
28
|
+
|
|
29
|
+
directories = (
|
|
30
|
+
walk_files(str(top_path_absolute))
|
|
31
|
+
.filter(lambda x: x.endswith(".py"))
|
|
32
|
+
.map(Path)
|
|
33
|
+
.group_by(lambda x: x.parent)
|
|
34
|
+
.map(lambda x: (x[0], x[1].collect()))
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
for base_path, files in directories:
|
|
38
|
+
if str(base_path / "__init__.py") in [str(x) for x in files]:
|
|
39
|
+
for module_path in files:
|
|
40
|
+
if "__init__.py" not in str(module_path):
|
|
41
|
+
|
|
42
|
+
# Example: elt.settings
|
|
43
|
+
module_import_path = str(module_path)[
|
|
44
|
+
len(str(top_path_absolute)) - len(top_module.__name__) :
|
|
45
|
+
].replace(os.path.sep, ".")[:-3]
|
|
46
|
+
|
|
47
|
+
module = importlib.import_module(module_import_path)
|
|
48
|
+
yield module
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def collect_instances(module: ModuleType, class_: Type[T]) -> List[T]:
|
|
52
|
+
"""Collect all instances of *class_* defined in *module*
|
|
53
|
+
|
|
54
|
+
Note: Will import all submodules in *module*. Beware of import side effects
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
found: List[T] = []
|
|
58
|
+
|
|
59
|
+
for module_ in walk_modules(module):
|
|
60
|
+
|
|
61
|
+
for _, variable in module_.__dict__.items():
|
|
62
|
+
|
|
63
|
+
if isinstance(variable, class_):
|
|
64
|
+
# Ensure variable is not a subclass
|
|
65
|
+
if variable.__class__ == class_:
|
|
66
|
+
found.append(variable)
|
|
67
|
+
return found
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def collect_subclasses(module: ModuleType, class_: Type[T]) -> List[Type[T]]:
|
|
71
|
+
"""Collect all subclasses of *class_* currently imported or defined in *module*
|
|
72
|
+
|
|
73
|
+
Note: Will import all submodules in *module*. Beware of import side effects
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
found: List[Type[T]] = []
|
|
77
|
+
|
|
78
|
+
for module_ in walk_modules(module):
|
|
79
|
+
|
|
80
|
+
for _, variable in module_.__dict__.items():
|
|
81
|
+
try:
|
|
82
|
+
if issubclass(variable, class_) and not class_ == variable:
|
|
83
|
+
found.append(variable)
|
|
84
|
+
except TypeError:
|
|
85
|
+
# argument 2 to issubclass must be a class ....
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
imported: List[Type[T]] = list(class_.__subclasses__()) # type: ignore
|
|
89
|
+
|
|
90
|
+
return list(set(found + imported))
|