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.
Files changed (23) hide show
  1. alembic_utils_extended-0.0.0/LICENSE +21 -0
  2. alembic_utils_extended-0.0.0/PKG-INFO +165 -0
  3. alembic_utils_extended-0.0.0/README.md +137 -0
  4. alembic_utils_extended-0.0.0/pyproject.toml +92 -0
  5. alembic_utils_extended-0.0.0/src/alembic_utils_extended/__init__.py +0 -0
  6. alembic_utils_extended-0.0.0/src/alembic_utils_extended/depends.py +145 -0
  7. alembic_utils_extended-0.0.0/src/alembic_utils_extended/exceptions.py +18 -0
  8. alembic_utils_extended-0.0.0/src/alembic_utils_extended/experimental/__init__.py +6 -0
  9. alembic_utils_extended-0.0.0/src/alembic_utils_extended/experimental/_collect_instances.py +90 -0
  10. alembic_utils_extended-0.0.0/src/alembic_utils_extended/on_entity_mixin.py +51 -0
  11. alembic_utils_extended-0.0.0/src/alembic_utils_extended/pg_extension.py +83 -0
  12. alembic_utils_extended-0.0.0/src/alembic_utils_extended/pg_function.py +169 -0
  13. alembic_utils_extended-0.0.0/src/alembic_utils_extended/pg_grant_table.py +232 -0
  14. alembic_utils_extended-0.0.0/src/alembic_utils_extended/pg_materialized_view.py +141 -0
  15. alembic_utils_extended-0.0.0/src/alembic_utils_extended/pg_policy.py +114 -0
  16. alembic_utils_extended-0.0.0/src/alembic_utils_extended/pg_trigger.py +169 -0
  17. alembic_utils_extended-0.0.0/src/alembic_utils_extended/pg_view.py +107 -0
  18. alembic_utils_extended-0.0.0/src/alembic_utils_extended/py.typed +0 -0
  19. alembic_utils_extended-0.0.0/src/alembic_utils_extended/replaceable_entity.py +438 -0
  20. alembic_utils_extended-0.0.0/src/alembic_utils_extended/reversible_op.py +172 -0
  21. alembic_utils_extended-0.0.0/src/alembic_utils_extended/simulate.py +67 -0
  22. alembic_utils_extended-0.0.0/src/alembic_utils_extended/statement.py +75 -0
  23. 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">&mdash;&mdash; &mdash;&mdash;</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">&mdash;&mdash; &mdash;&mdash;</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
@@ -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,6 @@
1
+ from alembic_utils_extended.experimental._collect_instances import (
2
+ collect_instances,
3
+ collect_subclasses,
4
+ )
5
+
6
+ __all__ = ["collect_instances", "collect_subclasses"]
@@ -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))