pum 1.2.3__py3-none-any.whl → 1.3.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pum/__init__.py +71 -10
- pum/changelog.py +61 -1
- pum/checker.py +444 -214
- pum/cli.py +279 -135
- pum/config_model.py +56 -33
- pum/connection.py +30 -0
- pum/dependency_handler.py +69 -4
- pum/dumper.py +14 -4
- pum/exceptions.py +9 -0
- pum/feedback.py +119 -0
- pum/hook.py +95 -29
- pum/info.py +0 -2
- pum/parameter.py +4 -0
- pum/pum_config.py +103 -20
- pum/report_generator.py +1043 -0
- pum/role_manager.py +151 -23
- pum/schema_migrations.py +163 -30
- pum/sql_content.py +83 -21
- pum/upgrader.py +287 -23
- {pum-1.2.3.dist-info → pum-1.3.1.dist-info}/METADATA +6 -2
- pum-1.3.1.dist-info/RECORD +25 -0
- {pum-1.2.3.dist-info → pum-1.3.1.dist-info}/WHEEL +1 -1
- pum-1.2.3.dist-info/RECORD +0 -22
- {pum-1.2.3.dist-info → pum-1.3.1.dist-info}/entry_points.txt +0 -0
- {pum-1.2.3.dist-info → pum-1.3.1.dist-info}/licenses/LICENSE +0 -0
- {pum-1.2.3.dist-info → pum-1.3.1.dist-info}/top_level.txt +0 -0
pum/sql_content.py
CHANGED
|
@@ -1,14 +1,62 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import re
|
|
3
3
|
from pathlib import Path
|
|
4
|
+
from typing import Any, Optional
|
|
4
5
|
|
|
5
6
|
import psycopg
|
|
6
7
|
|
|
7
8
|
from .exceptions import PumSqlError
|
|
9
|
+
from . import SQL
|
|
8
10
|
|
|
9
11
|
logger = logging.getLogger(__name__)
|
|
10
12
|
|
|
11
13
|
|
|
14
|
+
class CursorResult:
|
|
15
|
+
"""A simple wrapper to hold cursor results after the cursor is closed.
|
|
16
|
+
|
|
17
|
+
This class provides a cursor-compatible interface for accessing query results
|
|
18
|
+
after the actual database cursor has been closed.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self, results: list | None = None, description: Any | None = None, rowcount: int = 0
|
|
23
|
+
):
|
|
24
|
+
self._pum_results = results
|
|
25
|
+
self._pum_description = description
|
|
26
|
+
self._pum_rowcount = rowcount
|
|
27
|
+
self._pum_index = 0
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def description(self):
|
|
31
|
+
"""Return the column description (compatible with cursor.description)."""
|
|
32
|
+
return self._pum_description
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def rowcount(self):
|
|
36
|
+
"""Return the number of rows (compatible with cursor.rowcount)."""
|
|
37
|
+
return self._pum_rowcount
|
|
38
|
+
|
|
39
|
+
def fetchall(self):
|
|
40
|
+
"""Return all results (compatible with cursor.fetchall())."""
|
|
41
|
+
return self._pum_results if self._pum_results is not None else []
|
|
42
|
+
|
|
43
|
+
def fetchone(self):
|
|
44
|
+
"""Return the next result (compatible with cursor.fetchone())."""
|
|
45
|
+
if self._pum_results is None or self._pum_index >= len(self._pum_results):
|
|
46
|
+
return None
|
|
47
|
+
result = self._pum_results[self._pum_index]
|
|
48
|
+
self._pum_index += 1
|
|
49
|
+
return result
|
|
50
|
+
|
|
51
|
+
def fetchmany(self, size: int = 1):
|
|
52
|
+
"""Return the next `size` results (compatible with cursor.fetchmany())."""
|
|
53
|
+
if self._pum_results is None:
|
|
54
|
+
return []
|
|
55
|
+
results = self._pum_results[self._pum_index : self._pum_index + size]
|
|
56
|
+
self._pum_index += len(results)
|
|
57
|
+
return results
|
|
58
|
+
|
|
59
|
+
|
|
12
60
|
def sql_chunks_from_file(file: str | Path) -> list[psycopg.sql.SQL]:
|
|
13
61
|
"""Read SQL from a file, remove comments, and split into chunks.
|
|
14
62
|
|
|
@@ -161,7 +209,7 @@ class SqlContent:
|
|
|
161
209
|
sql: The SQL statement to execute or a path to a SQL file.
|
|
162
210
|
|
|
163
211
|
"""
|
|
164
|
-
if not isinstance(sql,
|
|
212
|
+
if not isinstance(sql, str | psycopg.sql.SQL | Path):
|
|
165
213
|
raise PumSqlError(
|
|
166
214
|
f"SQL must be a string, psycopg.sql.SQL object or a Path object, not {type(sql)}."
|
|
167
215
|
)
|
|
@@ -189,7 +237,7 @@ class SqlContent:
|
|
|
189
237
|
*,
|
|
190
238
|
parameters: dict | None = None,
|
|
191
239
|
commit: bool = False,
|
|
192
|
-
) ->
|
|
240
|
+
) -> CursorResult:
|
|
193
241
|
"""Execute a SQL statement with optional parameters.
|
|
194
242
|
|
|
195
243
|
Args:
|
|
@@ -197,27 +245,41 @@ class SqlContent:
|
|
|
197
245
|
parameters: Parameters to bind to the SQL statement. Defaults to ().
|
|
198
246
|
commit: Whether to commit the transaction. Defaults to False.
|
|
199
247
|
|
|
248
|
+
Returns:
|
|
249
|
+
CursorResult: Object containing results, description and other cursor info.
|
|
250
|
+
|
|
200
251
|
"""
|
|
201
|
-
|
|
252
|
+
with connection.cursor() as cursor:
|
|
253
|
+
for sql_code in self._prepare_sql(parameters):
|
|
254
|
+
try:
|
|
255
|
+
statement = sql_code.as_string(connection)
|
|
256
|
+
except (psycopg.errors.SyntaxError, psycopg.errors.ProgrammingError) as e:
|
|
257
|
+
raise PumSqlError(
|
|
258
|
+
f"SQL preparation failed for the following code: {statement} {e}"
|
|
259
|
+
) from e
|
|
260
|
+
try:
|
|
261
|
+
logger.log(SQL, f"Executing SQL: {statement}")
|
|
262
|
+
cursor.execute(statement)
|
|
263
|
+
except (psycopg.errors.SyntaxError, psycopg.errors.ProgrammingError) as e:
|
|
264
|
+
raise PumSqlError(
|
|
265
|
+
f"SQL execution failed for the following code: {statement} {e}"
|
|
266
|
+
) from e
|
|
267
|
+
if commit:
|
|
268
|
+
connection.commit()
|
|
202
269
|
|
|
203
|
-
|
|
204
|
-
try:
|
|
205
|
-
statement = sql_code.as_string(connection)
|
|
206
|
-
except (psycopg.errors.SyntaxError, psycopg.errors.ProgrammingError) as e:
|
|
207
|
-
raise PumSqlError(
|
|
208
|
-
f"SQL preparation failed for the following code: {statement} {e}"
|
|
209
|
-
) from e
|
|
270
|
+
# Store results before cursor closes
|
|
210
271
|
try:
|
|
211
|
-
|
|
212
|
-
cursor.
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
272
|
+
results = cursor.fetchall()
|
|
273
|
+
description = cursor.description
|
|
274
|
+
rowcount = cursor.rowcount
|
|
275
|
+
except psycopg.ProgrammingError:
|
|
276
|
+
# No results to fetch (e.g., DDL statements)
|
|
277
|
+
results = None
|
|
278
|
+
description = None
|
|
279
|
+
rowcount = 0
|
|
280
|
+
|
|
281
|
+
# Return a CursorResult object with the fetched data
|
|
282
|
+
return CursorResult(results, description, rowcount)
|
|
221
283
|
|
|
222
284
|
def _prepare_sql(self, parameters: dict | None) -> list[psycopg.sql.SQL]:
|
|
223
285
|
"""Prepare SQL for execution.
|
|
@@ -234,7 +296,7 @@ class SqlContent:
|
|
|
234
296
|
|
|
235
297
|
"""
|
|
236
298
|
if isinstance(self.sql, Path):
|
|
237
|
-
logger.
|
|
299
|
+
logger.debug(
|
|
238
300
|
f"Checking SQL from file: {self.sql} with parameters: {parameters}",
|
|
239
301
|
)
|
|
240
302
|
sql_code = sql_chunks_from_file(self.sql)
|
pum/upgrader.py
CHANGED
|
@@ -9,6 +9,7 @@ from .pum_config import PumConfig
|
|
|
9
9
|
from .exceptions import PumException
|
|
10
10
|
from .schema_migrations import SchemaMigrations
|
|
11
11
|
from .sql_content import SqlContent
|
|
12
|
+
from .feedback import Feedback, LogFeedback
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
logger = logging.getLogger(__name__)
|
|
@@ -40,7 +41,7 @@ class Upgrader:
|
|
|
40
41
|
|
|
41
42
|
"""
|
|
42
43
|
self.config = config
|
|
43
|
-
self.max_version = packaging.parse(max_version) if max_version else None
|
|
44
|
+
self.max_version = packaging.version.parse(max_version) if max_version else None
|
|
44
45
|
self.schema_migrations = SchemaMigrations(self.config)
|
|
45
46
|
|
|
46
47
|
def install(
|
|
@@ -52,7 +53,10 @@ class Upgrader:
|
|
|
52
53
|
roles: bool = False,
|
|
53
54
|
grant: bool = False,
|
|
54
55
|
beta_testing: bool = False,
|
|
56
|
+
skip_drop_app: bool = False,
|
|
57
|
+
skip_create_app: bool = False,
|
|
55
58
|
commit: bool = False,
|
|
59
|
+
feedback: Feedback | None = None,
|
|
56
60
|
) -> None:
|
|
57
61
|
"""Installs the given module
|
|
58
62
|
This will create the schema_migrations table if it does not exist.
|
|
@@ -72,11 +76,21 @@ class Upgrader:
|
|
|
72
76
|
If True, permissions will be granted to the roles.
|
|
73
77
|
beta_testing:
|
|
74
78
|
If True, the module is installed in beta testing mode.
|
|
75
|
-
This means that the module will not be
|
|
79
|
+
This means that the module will not be allowed to receive any future updates.
|
|
76
80
|
We strongly discourage using this for production.
|
|
81
|
+
skip_drop_app:
|
|
82
|
+
If True, drop app handlers will be skipped.
|
|
83
|
+
skip_create_app:
|
|
84
|
+
If True, create app handlers will be skipped.
|
|
77
85
|
commit:
|
|
78
86
|
If True, the changes will be committed to the database.
|
|
87
|
+
feedback:
|
|
88
|
+
A Feedback instance to report progress and check for cancellation.
|
|
89
|
+
If None, a LogFeedback instance will be used.
|
|
79
90
|
"""
|
|
91
|
+
if feedback is None:
|
|
92
|
+
feedback = LogFeedback()
|
|
93
|
+
|
|
80
94
|
if self.schema_migrations.exists(connection):
|
|
81
95
|
msg = (
|
|
82
96
|
f"Schema migrations table {self.config.config.pum.migration_table_schema}.pum_migrations already exists. "
|
|
@@ -84,35 +98,70 @@ class Upgrader:
|
|
|
84
98
|
"Use upgrade() to upgrade the db or start with a clean db."
|
|
85
99
|
)
|
|
86
100
|
raise PumException(msg)
|
|
101
|
+
|
|
102
|
+
feedback.report_progress("Creating migrations table...")
|
|
87
103
|
self.schema_migrations.create(connection, commit=False)
|
|
88
104
|
|
|
105
|
+
logger.info("Installing module...")
|
|
106
|
+
feedback.report_progress("Installing module...")
|
|
107
|
+
|
|
108
|
+
# Calculate total steps: drop handlers + all SQL files in changelogs + create handlers + role operations
|
|
109
|
+
drop_handlers = self.config.drop_app_handlers() if not skip_drop_app else []
|
|
110
|
+
changelogs = list(self.config.changelogs(max_version=max_version))
|
|
111
|
+
create_handlers = self.config.create_app_handlers() if not skip_create_app else []
|
|
112
|
+
|
|
113
|
+
total_changelog_files = sum(len(changelog.files()) for changelog in changelogs)
|
|
114
|
+
|
|
115
|
+
# Count role operations
|
|
116
|
+
role_steps = 0
|
|
117
|
+
if roles or grant:
|
|
118
|
+
role_manager = self.config.role_manager()
|
|
119
|
+
role_steps += len(role_manager.roles) # create roles
|
|
120
|
+
if grant:
|
|
121
|
+
role_steps += len(role_manager.roles) # grant permissions
|
|
122
|
+
|
|
123
|
+
total_steps = len(drop_handlers) + total_changelog_files + len(create_handlers) + role_steps
|
|
124
|
+
feedback.set_total_steps(total_steps)
|
|
125
|
+
|
|
89
126
|
if roles or grant:
|
|
127
|
+
feedback.report_progress("Creating roles...")
|
|
90
128
|
self.config.role_manager().create_roles(
|
|
91
|
-
connection=connection, grant=False, commit=False
|
|
129
|
+
connection=connection, grant=False, commit=False, feedback=feedback
|
|
92
130
|
)
|
|
93
131
|
|
|
94
|
-
|
|
95
|
-
|
|
132
|
+
if not skip_drop_app:
|
|
133
|
+
for drop_app_hook in drop_handlers:
|
|
134
|
+
if feedback.is_cancelled():
|
|
135
|
+
raise PumException("Installation cancelled by user")
|
|
136
|
+
feedback.increment_step()
|
|
137
|
+
feedback.report_progress(
|
|
138
|
+
f"Executing drop app handler: {drop_app_hook.file or 'SQL code'}"
|
|
139
|
+
)
|
|
140
|
+
drop_app_hook.execute(connection=connection, commit=False, parameters=parameters)
|
|
96
141
|
|
|
97
|
-
parameters_literals = SqlContent.prepare_parameters(parameters)
|
|
98
142
|
last_changelog = None
|
|
99
|
-
for changelog in
|
|
143
|
+
for changelog in changelogs:
|
|
144
|
+
if feedback.is_cancelled():
|
|
145
|
+
raise PumException("Installation cancelled by user")
|
|
100
146
|
last_changelog = changelog
|
|
101
|
-
|
|
102
|
-
connection,
|
|
103
|
-
)
|
|
104
|
-
changelog_files = [str(f) for f in changelog_files]
|
|
105
|
-
self.schema_migrations.set_baseline(
|
|
106
|
-
connection=connection,
|
|
107
|
-
version=changelog.version,
|
|
108
|
-
beta_testing=beta_testing,
|
|
147
|
+
changelog.apply(
|
|
148
|
+
connection,
|
|
109
149
|
commit=False,
|
|
110
|
-
changelog_files=changelog_files,
|
|
111
150
|
parameters=parameters,
|
|
151
|
+
schema_migrations=self.schema_migrations,
|
|
152
|
+
beta_testing=beta_testing,
|
|
153
|
+
feedback=feedback,
|
|
112
154
|
)
|
|
113
155
|
|
|
114
|
-
|
|
115
|
-
|
|
156
|
+
if not skip_create_app:
|
|
157
|
+
for create_app_hook in create_handlers:
|
|
158
|
+
if feedback.is_cancelled():
|
|
159
|
+
raise PumException("Installation cancelled by user")
|
|
160
|
+
feedback.increment_step()
|
|
161
|
+
feedback.report_progress(
|
|
162
|
+
f"Executing create app handler: {create_app_hook.file or 'SQL code'}"
|
|
163
|
+
)
|
|
164
|
+
create_app_hook.execute(connection=connection, commit=False, parameters=parameters)
|
|
116
165
|
|
|
117
166
|
logger.info(
|
|
118
167
|
"Installed %s.pum_migrations table and applied changelogs up to version %s",
|
|
@@ -121,9 +170,14 @@ class Upgrader:
|
|
|
121
170
|
)
|
|
122
171
|
|
|
123
172
|
if grant:
|
|
124
|
-
|
|
173
|
+
feedback.report_progress("Granting permissions...")
|
|
174
|
+
self.config.role_manager().grant_permissions(
|
|
175
|
+
connection=connection, commit=False, feedback=feedback
|
|
176
|
+
)
|
|
125
177
|
|
|
126
178
|
if commit:
|
|
179
|
+
feedback.lock_cancellation()
|
|
180
|
+
feedback.report_progress("Committing changes...")
|
|
127
181
|
connection.commit()
|
|
128
182
|
logger.info("Changes committed to the database.")
|
|
129
183
|
|
|
@@ -134,6 +188,8 @@ class Upgrader:
|
|
|
134
188
|
*,
|
|
135
189
|
parameters: dict | None = None,
|
|
136
190
|
grant: bool = True,
|
|
191
|
+
skip_drop_app: bool = False,
|
|
192
|
+
skip_create_app: bool = False,
|
|
137
193
|
) -> None:
|
|
138
194
|
"""Install demo data for the module.
|
|
139
195
|
|
|
@@ -142,14 +198,17 @@ class Upgrader:
|
|
|
142
198
|
name: The name of the demo data to install.
|
|
143
199
|
parameters: The parameters to pass to the demo data SQL.
|
|
144
200
|
grant: If True, grant permissions to the roles after installing the demo data. Default is True.
|
|
201
|
+
skip_drop_app: If True, skip drop app handlers during demo data installation. Default is False.
|
|
202
|
+
skip_create_app: If True, skip create app handlers during demo data installation. Default is False.
|
|
145
203
|
"""
|
|
146
204
|
if name not in self.config.demo_data():
|
|
147
205
|
raise PumException(f"Demo data '{name}' not found in the configuration.")
|
|
148
206
|
|
|
149
207
|
logger.info(f"Installing demo data {name}")
|
|
150
208
|
|
|
151
|
-
|
|
152
|
-
|
|
209
|
+
if not skip_drop_app:
|
|
210
|
+
for drop_app_hook in self.config.drop_app_handlers():
|
|
211
|
+
drop_app_hook.execute(connection=connection, commit=False, parameters=parameters)
|
|
153
212
|
|
|
154
213
|
connection.commit()
|
|
155
214
|
|
|
@@ -164,8 +223,9 @@ class Upgrader:
|
|
|
164
223
|
|
|
165
224
|
connection.commit()
|
|
166
225
|
|
|
167
|
-
|
|
168
|
-
|
|
226
|
+
if not skip_create_app:
|
|
227
|
+
for create_app_hook in self.config.create_app_handlers():
|
|
228
|
+
create_app_hook.execute(connection=connection, commit=False, parameters=parameters)
|
|
169
229
|
|
|
170
230
|
connection.commit()
|
|
171
231
|
|
|
@@ -175,3 +235,207 @@ class Upgrader:
|
|
|
175
235
|
connection.commit()
|
|
176
236
|
|
|
177
237
|
logger.info("Demo data '%s' installed successfully.", name)
|
|
238
|
+
|
|
239
|
+
def upgrade(
|
|
240
|
+
self,
|
|
241
|
+
connection: psycopg.Connection,
|
|
242
|
+
*,
|
|
243
|
+
parameters: dict | None = None,
|
|
244
|
+
max_version: str | packaging.version.Version | None = None,
|
|
245
|
+
beta_testing: bool = False,
|
|
246
|
+
force: bool = False,
|
|
247
|
+
skip_drop_app: bool = False,
|
|
248
|
+
skip_create_app: bool = False,
|
|
249
|
+
roles: bool = False,
|
|
250
|
+
grant: bool = False,
|
|
251
|
+
feedback: Feedback | None = None,
|
|
252
|
+
) -> None:
|
|
253
|
+
"""Upgrades the given module
|
|
254
|
+
The changelogs are applied in the order they are found in the directory.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
connection:
|
|
258
|
+
The database connection to use for the upgrade.
|
|
259
|
+
parameters:
|
|
260
|
+
The parameters to pass for the migration.
|
|
261
|
+
max_version:
|
|
262
|
+
The maximum version to apply. If None, all versions are applied.
|
|
263
|
+
beta_testing:
|
|
264
|
+
If True, the module is upgraded in beta testing mode.
|
|
265
|
+
This means that the module will not be allowed to receive any future updates.
|
|
266
|
+
We strongly discourage using this for production.
|
|
267
|
+
force:
|
|
268
|
+
If True, allow upgrading a module that is installed in beta testing mode.
|
|
269
|
+
skip_drop_app:
|
|
270
|
+
If True, drop app handlers will be skipped.
|
|
271
|
+
skip_create_app:
|
|
272
|
+
If True, create app handlers will be skipped.
|
|
273
|
+
roles:
|
|
274
|
+
If True, roles will be created.
|
|
275
|
+
grant:
|
|
276
|
+
If True, permissions will be granted to the roles.
|
|
277
|
+
feedback:
|
|
278
|
+
A Feedback instance to report progress and check for cancellation.
|
|
279
|
+
If None, a LogFeedback instance will be used.
|
|
280
|
+
"""
|
|
281
|
+
if feedback is None:
|
|
282
|
+
feedback = LogFeedback()
|
|
283
|
+
|
|
284
|
+
if not self.schema_migrations.exists(connection):
|
|
285
|
+
msg = (
|
|
286
|
+
f"Schema migrations table {self.config.config.pum.migration_table_schema}.pum_migrations does not exist. "
|
|
287
|
+
"This means that the module is not installed yet. Use install() to install the module."
|
|
288
|
+
)
|
|
289
|
+
raise PumException(msg)
|
|
290
|
+
|
|
291
|
+
migration_details = self.schema_migrations.migration_details(connection)
|
|
292
|
+
installed_beta_testing = bool(migration_details.get("beta_testing", False))
|
|
293
|
+
if installed_beta_testing and not force:
|
|
294
|
+
msg = (
|
|
295
|
+
"This module is installed in beta testing mode, upgrades are disabled. "
|
|
296
|
+
"Re-run with force=True (or --force in the CLI) if you really want to upgrade anyway."
|
|
297
|
+
)
|
|
298
|
+
raise PumException(msg)
|
|
299
|
+
|
|
300
|
+
effective_beta_testing = beta_testing or installed_beta_testing
|
|
301
|
+
|
|
302
|
+
logger.info("Starting upgrade process...")
|
|
303
|
+
feedback.report_progress("Starting upgrade...")
|
|
304
|
+
|
|
305
|
+
# Calculate total steps: drop handlers + applicable changelog files + create handlers
|
|
306
|
+
drop_handlers = self.config.drop_app_handlers() if not skip_drop_app else []
|
|
307
|
+
changelogs = list(self.config.changelogs(max_version=max_version))
|
|
308
|
+
create_handlers = self.config.create_app_handlers() if not skip_create_app else []
|
|
309
|
+
|
|
310
|
+
# First pass: determine applicable changelogs
|
|
311
|
+
applicable_changelogs = []
|
|
312
|
+
for changelog in changelogs:
|
|
313
|
+
if changelog.version <= self.schema_migrations.baseline(connection):
|
|
314
|
+
if not changelog.is_applied(
|
|
315
|
+
connection=connection, schema_migrations=self.schema_migrations
|
|
316
|
+
):
|
|
317
|
+
msg = (
|
|
318
|
+
f"Changelog version {changelog.version} is lower than or equal to the current version "
|
|
319
|
+
f"{self.schema_migrations.current_version(connection)} but not applied. "
|
|
320
|
+
"This indicates a problem with the database state."
|
|
321
|
+
)
|
|
322
|
+
logger.error(msg)
|
|
323
|
+
raise PumException(msg)
|
|
324
|
+
logger.debug("Changelog version %s already applied, skipping.", changelog.version)
|
|
325
|
+
continue
|
|
326
|
+
applicable_changelogs.append(changelog)
|
|
327
|
+
|
|
328
|
+
total_changelog_files = sum(len(changelog.files()) for changelog in applicable_changelogs)
|
|
329
|
+
|
|
330
|
+
# Count role operations
|
|
331
|
+
role_steps = 0
|
|
332
|
+
if roles or grant:
|
|
333
|
+
role_manager = self.config.role_manager()
|
|
334
|
+
role_steps += len(role_manager.roles) # create roles
|
|
335
|
+
if grant:
|
|
336
|
+
role_steps += len(role_manager.roles) # grant permissions
|
|
337
|
+
|
|
338
|
+
total_steps = len(drop_handlers) + total_changelog_files + len(create_handlers) + role_steps
|
|
339
|
+
feedback.set_total_steps(total_steps)
|
|
340
|
+
|
|
341
|
+
if not skip_drop_app:
|
|
342
|
+
for drop_app_hook in drop_handlers:
|
|
343
|
+
if feedback.is_cancelled():
|
|
344
|
+
raise PumException("Upgrade cancelled by user")
|
|
345
|
+
feedback.increment_step()
|
|
346
|
+
feedback.report_progress(
|
|
347
|
+
f"Executing drop app handler: {drop_app_hook.file or 'SQL code'}"
|
|
348
|
+
)
|
|
349
|
+
drop_app_hook.execute(connection=connection, commit=False, parameters=parameters)
|
|
350
|
+
|
|
351
|
+
for changelog in applicable_changelogs:
|
|
352
|
+
if feedback.is_cancelled():
|
|
353
|
+
raise PumException("Upgrade cancelled by user")
|
|
354
|
+
changelog.apply(
|
|
355
|
+
connection,
|
|
356
|
+
commit=False,
|
|
357
|
+
parameters=parameters,
|
|
358
|
+
schema_migrations=self.schema_migrations,
|
|
359
|
+
beta_testing=effective_beta_testing,
|
|
360
|
+
feedback=feedback,
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
if not skip_create_app:
|
|
364
|
+
for create_app_hook in create_handlers:
|
|
365
|
+
if feedback.is_cancelled():
|
|
366
|
+
raise PumException("Upgrade cancelled by user")
|
|
367
|
+
feedback.increment_step()
|
|
368
|
+
feedback.report_progress(
|
|
369
|
+
f"Executing create app handler: {create_app_hook.file or 'SQL code'}"
|
|
370
|
+
)
|
|
371
|
+
create_app_hook.execute(connection=connection, commit=False, parameters=parameters)
|
|
372
|
+
|
|
373
|
+
if roles or grant:
|
|
374
|
+
feedback.report_progress("Creating roles...")
|
|
375
|
+
self.config.role_manager().create_roles(
|
|
376
|
+
connection=connection, grant=False, commit=False, feedback=feedback
|
|
377
|
+
)
|
|
378
|
+
if grant:
|
|
379
|
+
feedback.report_progress("Granting permissions...")
|
|
380
|
+
self.config.role_manager().grant_permissions(
|
|
381
|
+
connection=connection, commit=False, feedback=feedback
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
feedback.lock_cancellation()
|
|
385
|
+
feedback.report_progress("Committing changes...")
|
|
386
|
+
connection.commit()
|
|
387
|
+
logger.info("Upgrade completed and changes committed to the database.")
|
|
388
|
+
|
|
389
|
+
def uninstall(
|
|
390
|
+
self,
|
|
391
|
+
connection: psycopg.Connection,
|
|
392
|
+
*,
|
|
393
|
+
parameters: dict | None = None,
|
|
394
|
+
commit: bool = True,
|
|
395
|
+
feedback: Feedback | None = None,
|
|
396
|
+
) -> None:
|
|
397
|
+
"""Uninstall the module by executing uninstall hooks.
|
|
398
|
+
|
|
399
|
+
Args:
|
|
400
|
+
connection: The database connection to use for the uninstall.
|
|
401
|
+
parameters: The parameters to pass to the uninstall hooks.
|
|
402
|
+
commit: If True, the changes will be committed to the database. Default is True.
|
|
403
|
+
feedback: A Feedback instance to report progress and check for cancellation.
|
|
404
|
+
If None, a LogFeedback instance will be used.
|
|
405
|
+
|
|
406
|
+
Raises:
|
|
407
|
+
PumException: If no uninstall hooks are defined in the configuration.
|
|
408
|
+
"""
|
|
409
|
+
if feedback is None:
|
|
410
|
+
feedback = LogFeedback()
|
|
411
|
+
|
|
412
|
+
uninstall_hooks = self.config.uninstall_handlers()
|
|
413
|
+
|
|
414
|
+
if not uninstall_hooks:
|
|
415
|
+
raise PumException(
|
|
416
|
+
"No uninstall hooks defined in the configuration. "
|
|
417
|
+
"Add 'uninstall' section to your .pum.yaml file to define uninstall hooks."
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
logger.info("Uninstalling module...")
|
|
421
|
+
feedback.report_progress("Starting uninstall...")
|
|
422
|
+
|
|
423
|
+
# Set total steps for progress tracking
|
|
424
|
+
total_steps = len(uninstall_hooks)
|
|
425
|
+
feedback.set_total_steps(total_steps)
|
|
426
|
+
|
|
427
|
+
for uninstall_hook in uninstall_hooks:
|
|
428
|
+
if feedback.is_cancelled():
|
|
429
|
+
raise PumException("Uninstall cancelled by user")
|
|
430
|
+
feedback.increment_step()
|
|
431
|
+
feedback.report_progress(
|
|
432
|
+
f"Executing uninstall handler: {uninstall_hook.file or 'SQL code'}"
|
|
433
|
+
)
|
|
434
|
+
uninstall_hook.execute(connection=connection, commit=False, parameters=parameters)
|
|
435
|
+
|
|
436
|
+
if commit:
|
|
437
|
+
feedback.lock_cancellation()
|
|
438
|
+
feedback.report_progress("Committing changes...")
|
|
439
|
+
connection.commit()
|
|
440
|
+
logger.info("Uninstall completed and changes committed to the database.")
|
|
441
|
+
logger.info("Uninstall completed and changes committed to the database.")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pum
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.1
|
|
4
4
|
Summary: Pum stands for "Postgres Upgrades Manager". It is a Database migration management tool very similar to flyway-db or Liquibase, based on metadata tables.
|
|
5
5
|
Author-email: Denis Rouzaud <denis@opengis.ch>
|
|
6
6
|
License-Expression: GPL-2.0-or-later
|
|
@@ -32,6 +32,8 @@ Requires-Dist: flake8-isort; extra == "dev"
|
|
|
32
32
|
Requires-Dist: flake8-print; extra == "dev"
|
|
33
33
|
Requires-Dist: pre-commit; extra == "dev"
|
|
34
34
|
Requires-Dist: nose2; extra == "dev"
|
|
35
|
+
Provides-Extra: html
|
|
36
|
+
Requires-Dist: Jinja2; extra == "html"
|
|
35
37
|
Dynamic: license-file
|
|
36
38
|
|
|
37
39
|
# PostgreSQL Upgrades Manager (PUM)
|
|
@@ -49,10 +51,12 @@ PUM (PostgreSQL Upgrades Manager) is a robust database migration management tool
|
|
|
49
51
|
|
|
50
52
|
## Key Features
|
|
51
53
|
|
|
54
|
+
- **Flexible Database Connections**: Connect using PostgreSQL service names or direct connection strings (URI or parameters).
|
|
52
55
|
- **Command-line and Python Integration**: Use PUM as a standalone CLI tool or integrate it into your Python project.
|
|
53
56
|
- **Database Versioning**: Automatically manage database versioning with a metadata table.
|
|
54
57
|
- **Changelog Management**: Apply and track SQL delta files for database upgrades.
|
|
55
|
-
- **
|
|
58
|
+
- **Droppable & recreatable app with data isolation**: PUM supports a clean rebuild workflow where an application environment can be dropped and recreated deterministically using hooks (pre and post migration).
|
|
59
|
+
|
|
56
60
|
|
|
57
61
|
## Why PUM?
|
|
58
62
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
pum/__init__.py,sha256=XJWOk-gUJZgh68L1Sttg9AcnnYN0Ig_nbptufVbvtuU,2899
|
|
2
|
+
pum/changelog.py,sha256=UikQ3KtV8e5BZXttjWmvR3nL3-uoLEzZ489k4AtHfR0,6008
|
|
3
|
+
pum/checker.py,sha256=s6mYLXgugooX1ii8O21jp5oAaafY8bStEh4lKvwr1mc,25091
|
|
4
|
+
pum/cli.py,sha256=skNG8xvcAderJmzxnuzAfnJmi7CEYASwvagvltn6j8o,19563
|
|
5
|
+
pum/config_model.py,sha256=2zmT8QP8W1_NOLll7Dp9_nCQa_RHiGGedeWlumVogRI,7944
|
|
6
|
+
pum/connection.py,sha256=lGW8vUyeh_HS6xX0kYeEANbUJ9Aw3cufz46fGjvuNvk,1119
|
|
7
|
+
pum/dependency_handler.py,sha256=1A6720Tv0rBME-gJVLn9PYK46YPXLMD-r9hpNNX8ApY,6965
|
|
8
|
+
pum/dumper.py,sha256=FFRidaOg3ENePNZ61TGbnAdp0TItvWbA4j292Xlf9bA,3878
|
|
9
|
+
pum/exceptions.py,sha256=h1TEPHI_hSbLLQc6zYfTpFexczv1iKIN7OuTsLTN590,1439
|
|
10
|
+
pum/feedback.py,sha256=pbZYdhsocjmJbjcdUpvYhI8XLio15htyVg-Cay6HHdE,3810
|
|
11
|
+
pum/hook.py,sha256=G-qI1J4erWPWw5mRa-BHFVtA9yYTGcTK3aYmqaoKep8,12528
|
|
12
|
+
pum/info.py,sha256=75Fr4Bn-ARe36aK8KV31MSCNWDkAdiMgJ-4IvMF73zU,1346
|
|
13
|
+
pum/parameter.py,sha256=e7Lm5fp2Xg4SEkIDmbxSyNTsYvELCkeyPUeS6CCs6EA,2646
|
|
14
|
+
pum/pum_config.py,sha256=k-YF2nL73HcfybjV09G042Xuiu8nRWSxAI43JyiCTsg,14952
|
|
15
|
+
pum/report_generator.py,sha256=upv6gpVZ_kk7O6IhcO7LWMlPHg_u_V0wx_LxQebMp-c,38908
|
|
16
|
+
pum/role_manager.py,sha256=_LG5LR8osc5bVQVb-AKGU3wvXBrIe9J3IV1ECzhiwnA,15991
|
|
17
|
+
pum/schema_migrations.py,sha256=-yR84KkG1mLY5yeEWzidNPam1hrE_1mMC5KJaHOUxiA,16304
|
|
18
|
+
pum/sql_content.py,sha256=BY5XMS713sIOUT4xLHByOzQhxYItucvkCFEyzmwSTR4,12969
|
|
19
|
+
pum/upgrader.py,sha256=skGfwfuAb0TLMqCPjRbcJNf1VKmekaaQLm_C-abs61E,18353
|
|
20
|
+
pum-1.3.1.dist-info/licenses/LICENSE,sha256=2ylvL381vKOhdO-w6zkrOxe9lLNBhRQpo9_0EbHC_HM,18046
|
|
21
|
+
pum-1.3.1.dist-info/METADATA,sha256=xcbQ-mlS84gzT_6xj2tzxj4JeA2LPf6PowfHY83CfYA,3236
|
|
22
|
+
pum-1.3.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
23
|
+
pum-1.3.1.dist-info/entry_points.txt,sha256=U6dmxSpKs1Pe9vWiR29VPhJMDjrmZeJCSxvfLGR8BD4,36
|
|
24
|
+
pum-1.3.1.dist-info/top_level.txt,sha256=ddiI4HLBhY6ql-NNm0Ez0JhoOHdWDIzrHeCdHmmagcc,4
|
|
25
|
+
pum-1.3.1.dist-info/RECORD,,
|
pum-1.2.3.dist-info/RECORD
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
pum/__init__.py,sha256=P-NHd6_SYpk9aypefLI62QCZ3f5APOMCwSzrFFCKAew,759
|
|
2
|
-
pum/changelog.py,sha256=yDc5swmMd5gb2vCEAlenoq5gs-ZEGc4uXicBtiGxkOk,3692
|
|
3
|
-
pum/checker.py,sha256=GT2v7793HP1g94dv0mL6CHtQfblQwAyeFHEWCy44lkc,14379
|
|
4
|
-
pum/cli.py,sha256=HGt5_aqSk8hKdV3xYKNJFMEcZc_3BULWhvX-6wM_2EM,15046
|
|
5
|
-
pum/config_model.py,sha256=9jGuk8PC3JLS-VT7SNLF-WiVtaLZQi_24rPxt0zekTQ,6933
|
|
6
|
-
pum/dependency_handler.py,sha256=AVeemsh6zUumRJKbRLRwP_FRC0x3K16TJiK2i9ogvn0,3861
|
|
7
|
-
pum/dumper.py,sha256=EJZ8T44JM0GKgdqw1ENOfhZ-RI89OQ4DNdoTZKtLdEw,3404
|
|
8
|
-
pum/exceptions.py,sha256=xyzzY4ht1nKfrVt59Giulflpmu83nJhxoTygrqiqPlw,1137
|
|
9
|
-
pum/hook.py,sha256=5MrVa6Xr0o28RfsXylGDatlM_vOKfKtGJmhYx8crC94,9541
|
|
10
|
-
pum/info.py,sha256=CGj-Lt4Y2l2ymAl3OFqCWfJD5xZF4aaUSztAiSKwgE4,1395
|
|
11
|
-
pum/parameter.py,sha256=qdbWk3WZc419AW-qwGMxlgc-7GEhdwIoPBnDk6UsVZU,2485
|
|
12
|
-
pum/pum_config.py,sha256=NkvmlnqLQVbrfnmGkbPAveWyjlyxyFdRSW2zwpup9fI,11349
|
|
13
|
-
pum/role_manager.py,sha256=iEmznB09YAmmkIFh1Ar8nNdy-ZofO7R3J0VdjtLVyp8,10293
|
|
14
|
-
pum/schema_migrations.py,sha256=rUn14aNkt8nw-69cCgklbtveanq6CrNdKKrc458cGIc,10491
|
|
15
|
-
pum/sql_content.py,sha256=-0h3caJlvkyEjZwjPVrKh5ZYaDctC-5lklBvZ-zgRzA,10620
|
|
16
|
-
pum/upgrader.py,sha256=h6a3Jeem6PQX-abWf7cBqYSaoSUXhuNBj6aYKrBxscM,6665
|
|
17
|
-
pum-1.2.3.dist-info/licenses/LICENSE,sha256=2ylvL381vKOhdO-w6zkrOxe9lLNBhRQpo9_0EbHC_HM,18046
|
|
18
|
-
pum-1.2.3.dist-info/METADATA,sha256=GaGg7gKTmj86gLLDE6JBmYN2_a03kUMbVJWsdpTTP3g,3138
|
|
19
|
-
pum-1.2.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
20
|
-
pum-1.2.3.dist-info/entry_points.txt,sha256=U6dmxSpKs1Pe9vWiR29VPhJMDjrmZeJCSxvfLGR8BD4,36
|
|
21
|
-
pum-1.2.3.dist-info/top_level.txt,sha256=ddiI4HLBhY6ql-NNm0Ez0JhoOHdWDIzrHeCdHmmagcc,4
|
|
22
|
-
pum-1.2.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|