pum 1.2.2__py3-none-any.whl → 1.3.0__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 +300 -137
- pum/config_model.py +57 -34
- 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 +173 -36
- pum/sql_content.py +83 -21
- pum/upgrader.py +287 -23
- {pum-1.2.2.dist-info → pum-1.3.0.dist-info}/METADATA +6 -2
- pum-1.3.0.dist-info/RECORD +25 -0
- {pum-1.2.2.dist-info → pum-1.3.0.dist-info}/WHEEL +1 -1
- pum-1.2.2.dist-info/RECORD +0 -22
- {pum-1.2.2.dist-info → pum-1.3.0.dist-info}/entry_points.txt +0 -0
- {pum-1.2.2.dist-info → pum-1.3.0.dist-info}/licenses/LICENSE +0 -0
- {pum-1.2.2.dist-info → pum-1.3.0.dist-info}/top_level.txt +0 -0
pum/cli.py
CHANGED
|
@@ -9,23 +9,36 @@ from pathlib import Path
|
|
|
9
9
|
import psycopg
|
|
10
10
|
|
|
11
11
|
from .checker import Checker
|
|
12
|
+
from .report_generator import ReportGenerator
|
|
12
13
|
from .pum_config import PumConfig
|
|
14
|
+
from .connection import format_connection_string
|
|
13
15
|
|
|
14
16
|
from .info import run_info
|
|
15
17
|
from .upgrader import Upgrader
|
|
16
18
|
from .parameter import ParameterType
|
|
17
19
|
from .schema_migrations import SchemaMigrations
|
|
18
|
-
from .dumper import DumpFormat
|
|
20
|
+
from .dumper import DumpFormat, Dumper
|
|
21
|
+
from . import SQL
|
|
19
22
|
|
|
20
23
|
|
|
21
24
|
def setup_logging(verbosity: int = 0):
|
|
22
|
-
"""
|
|
23
|
-
level = logging.WARNING # default
|
|
25
|
+
"""Configure logging for the CLI.
|
|
24
26
|
|
|
25
|
-
|
|
26
|
-
level =
|
|
27
|
+
Args:
|
|
28
|
+
verbosity: Verbosity level (-1=quiet/WARNING, 0=INFO, 1=DEBUG, 2+=SQL).
|
|
29
|
+
|
|
30
|
+
"""
|
|
31
|
+
# Register custom SQL log level
|
|
32
|
+
logging.addLevelName(SQL, "SQL")
|
|
33
|
+
|
|
34
|
+
if verbosity < 0:
|
|
35
|
+
level = logging.WARNING # quiet mode
|
|
27
36
|
elif verbosity >= 2:
|
|
37
|
+
level = SQL # Most verbose - shows all SQL statements
|
|
38
|
+
elif verbosity >= 1:
|
|
28
39
|
level = logging.DEBUG
|
|
40
|
+
else:
|
|
41
|
+
level = logging.INFO # default
|
|
29
42
|
|
|
30
43
|
class ColorFormatter(logging.Formatter):
|
|
31
44
|
COLORS = {
|
|
@@ -33,6 +46,7 @@ def setup_logging(verbosity: int = 0):
|
|
|
33
46
|
logging.WARNING: "\033[33m", # Yellow
|
|
34
47
|
logging.INFO: "\033[36m", # Cyan
|
|
35
48
|
logging.DEBUG: "\033[35m", # Magenta
|
|
49
|
+
SQL: "\033[90m", # Gray for SQL statements
|
|
36
50
|
}
|
|
37
51
|
RESET = "\033[0m"
|
|
38
52
|
|
|
@@ -54,108 +68,56 @@ def setup_logging(verbosity: int = 0):
|
|
|
54
68
|
|
|
55
69
|
|
|
56
70
|
class Pum:
|
|
57
|
-
def __init__(self,
|
|
71
|
+
def __init__(self, pg_connection: str, config: str | PumConfig = None) -> None:
|
|
58
72
|
"""Initialize the PUM class with a database connection and configuration.
|
|
59
73
|
|
|
60
74
|
Args:
|
|
61
|
-
|
|
75
|
+
pg_connection (str): PostgreSQL service name or connection string.
|
|
76
|
+
Can be a service name (e.g., 'mydb') or a full connection string
|
|
77
|
+
(e.g., 'postgresql://user:pass@host/db' or 'host=localhost dbname=mydb').
|
|
62
78
|
config (str | PumConfig): The configuration file path or a PumConfig object.
|
|
63
79
|
|
|
64
80
|
"""
|
|
65
|
-
self.
|
|
81
|
+
self.pg_connection = pg_connection
|
|
66
82
|
|
|
67
83
|
if isinstance(config, str):
|
|
68
84
|
self.config = PumConfig.from_yaml(config)
|
|
69
85
|
else:
|
|
70
86
|
self.config = config
|
|
71
87
|
|
|
72
|
-
def run_check(
|
|
73
|
-
self,
|
|
74
|
-
pg_service1: str,
|
|
75
|
-
pg_service2: str,
|
|
76
|
-
ignore_list: list[str] | None,
|
|
77
|
-
exclude_schema: list[str] | None,
|
|
78
|
-
exclude_field_pattern: list[str] | None,
|
|
79
|
-
verbose_level: int = 1,
|
|
80
|
-
output_file: str | None = None,
|
|
81
|
-
) -> bool:
|
|
82
|
-
"""Run the check command.
|
|
83
88
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
pg_service2:
|
|
89
|
-
The name of the postgres service (defined in pg_service.conf)
|
|
90
|
-
related to the first db to be compared
|
|
91
|
-
ignore_list:
|
|
92
|
-
List of elements to be ignored in check (ex. tables, columns,
|
|
93
|
-
views, ...)
|
|
94
|
-
exclude_schema:
|
|
95
|
-
List of schemas to be ignored in check.
|
|
96
|
-
exclude_field_pattern:
|
|
97
|
-
List of field patterns to be ignored in check.
|
|
98
|
-
verbose_level:
|
|
99
|
-
verbose level, 0 -> nothing, 1 -> print first 80 char of each
|
|
100
|
-
difference, 2 -> print all the difference details
|
|
101
|
-
output_file:
|
|
102
|
-
a file path where write the differences
|
|
103
|
-
|
|
104
|
-
Returns:
|
|
105
|
-
True if no differences are found, False otherwise.
|
|
89
|
+
def create_parser(
|
|
90
|
+
max_help_position: int | None = None, width: int | None = None
|
|
91
|
+
) -> argparse.ArgumentParser:
|
|
92
|
+
"""Create the main argument parser and all subparsers.
|
|
106
93
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
ignore_list = ignore_list or []
|
|
111
|
-
exclude_schema = exclude_schema or []
|
|
112
|
-
exclude_field_pattern = exclude_field_pattern or []
|
|
113
|
-
try:
|
|
114
|
-
checker = Checker(
|
|
115
|
-
pg_service1,
|
|
116
|
-
pg_service2,
|
|
117
|
-
exclude_schema=exclude_schema,
|
|
118
|
-
exclude_field_pattern=exclude_field_pattern,
|
|
119
|
-
ignore_list=ignore_list,
|
|
120
|
-
verbose_level=verbose_level,
|
|
121
|
-
)
|
|
122
|
-
result, differences = checker.run_checks()
|
|
94
|
+
Args:
|
|
95
|
+
max_help_position: Maximum help position for formatting.
|
|
96
|
+
width: Width for formatting.
|
|
123
97
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
else:
|
|
127
|
-
self.__out("DIFFERENCES FOUND")
|
|
128
|
-
|
|
129
|
-
if differences:
|
|
130
|
-
if output_file:
|
|
131
|
-
with open(output_file, "w") as f:
|
|
132
|
-
for k, values in differences.items():
|
|
133
|
-
f.write(k + "\n")
|
|
134
|
-
f.writelines(f"{v}\n" for v in values)
|
|
135
|
-
else:
|
|
136
|
-
for k, values in differences.items():
|
|
137
|
-
print(k)
|
|
138
|
-
for v in values:
|
|
139
|
-
print(v)
|
|
140
|
-
return result
|
|
141
|
-
|
|
142
|
-
except psycopg.Error as e:
|
|
143
|
-
self.__out("ERROR")
|
|
144
|
-
self.__out(e.args[0] if e.args else str(e))
|
|
145
|
-
sys.exit(1)
|
|
146
|
-
|
|
147
|
-
except Exception as e:
|
|
148
|
-
self.__out("ERROR")
|
|
149
|
-
# if e.args is empty then use str(e)
|
|
150
|
-
self.__out(e.args[0] if e.args else str(e))
|
|
151
|
-
sys.exit(1)
|
|
98
|
+
Returns:
|
|
99
|
+
The fully configured argument parser.
|
|
152
100
|
|
|
101
|
+
"""
|
|
102
|
+
if max_help_position is not None or width is not None:
|
|
153
103
|
|
|
154
|
-
def
|
|
155
|
-
|
|
156
|
-
|
|
104
|
+
def formatter_class(prog):
|
|
105
|
+
return argparse.HelpFormatter(
|
|
106
|
+
prog, max_help_position=max_help_position or 40, width=width or 200
|
|
107
|
+
)
|
|
108
|
+
else:
|
|
109
|
+
formatter_class = argparse.HelpFormatter
|
|
110
|
+
parser = argparse.ArgumentParser(
|
|
111
|
+
prog="pum",
|
|
112
|
+
formatter_class=formatter_class,
|
|
113
|
+
)
|
|
157
114
|
parser.add_argument("-c", "--config_file", help="set the config file. Default: .pum.yaml")
|
|
158
|
-
parser.add_argument(
|
|
115
|
+
parser.add_argument(
|
|
116
|
+
"-p",
|
|
117
|
+
"--pg-connection",
|
|
118
|
+
help="PostgreSQL service name or connection string (e.g., 'mydb' or 'postgresql://user:pass@host/db')",
|
|
119
|
+
required=True,
|
|
120
|
+
)
|
|
159
121
|
|
|
160
122
|
parser.add_argument(
|
|
161
123
|
"-d", "--dir", help="Directory or URL of the module. Default: .", default="."
|
|
@@ -166,7 +128,14 @@ def create_parser() -> argparse.ArgumentParser:
|
|
|
166
128
|
"--verbose",
|
|
167
129
|
action="count",
|
|
168
130
|
default=0,
|
|
169
|
-
help="Increase
|
|
131
|
+
help="Increase verbosity (-v for DEBUG, -vv for SQL statements)",
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
parser.add_argument(
|
|
135
|
+
"-q",
|
|
136
|
+
"--quiet",
|
|
137
|
+
action="store_true",
|
|
138
|
+
help="Suppress info messages, only show warnings and errors",
|
|
170
139
|
)
|
|
171
140
|
|
|
172
141
|
version = importlib.metadata.version("pum")
|
|
@@ -182,10 +151,16 @@ def create_parser() -> argparse.ArgumentParser:
|
|
|
182
151
|
)
|
|
183
152
|
|
|
184
153
|
# Parser for the "info" command
|
|
185
|
-
parser_info = subparsers.add_parser(
|
|
154
|
+
parser_info = subparsers.add_parser( # NOQA
|
|
155
|
+
"info",
|
|
156
|
+
help="show info about schema migrations history.",
|
|
157
|
+
formatter_class=formatter_class,
|
|
158
|
+
)
|
|
186
159
|
|
|
187
160
|
# Parser for the "install" command
|
|
188
|
-
parser_install = subparsers.add_parser(
|
|
161
|
+
parser_install = subparsers.add_parser(
|
|
162
|
+
"install", help="Installs the module.", formatter_class=formatter_class
|
|
163
|
+
)
|
|
189
164
|
parser_install.add_argument(
|
|
190
165
|
"-p",
|
|
191
166
|
"--parameter",
|
|
@@ -194,9 +169,9 @@ def create_parser() -> argparse.ArgumentParser:
|
|
|
194
169
|
action="append",
|
|
195
170
|
)
|
|
196
171
|
parser_install.add_argument("--max-version", help="maximum version to install")
|
|
197
|
-
parser_install.add_argument("-
|
|
172
|
+
parser_install.add_argument("--skip-roles", help="Skip creating roles", action="store_true")
|
|
198
173
|
parser_install.add_argument(
|
|
199
|
-
"-
|
|
174
|
+
"--skip-grant", help="Skip granting permissions to roles", action="store_true"
|
|
200
175
|
)
|
|
201
176
|
parser_install.add_argument(
|
|
202
177
|
"-d", "--demo-data", help="Load demo data with the given name", type=str, default=None
|
|
@@ -206,19 +181,70 @@ def create_parser() -> argparse.ArgumentParser:
|
|
|
206
181
|
help="This will install the module in beta testing, meaning that it will not be possible to receive any future updates.",
|
|
207
182
|
action="store_true",
|
|
208
183
|
)
|
|
184
|
+
parser_install.add_argument(
|
|
185
|
+
"--skip-drop-app",
|
|
186
|
+
help="Skip drop app handlers during installation.",
|
|
187
|
+
action="store_true",
|
|
188
|
+
)
|
|
189
|
+
parser_install.add_argument(
|
|
190
|
+
"--skip-create-app",
|
|
191
|
+
help="Skip create app handlers during installation.",
|
|
192
|
+
action="store_true",
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
# Upgrade parser
|
|
196
|
+
parser_upgrade = subparsers.add_parser(
|
|
197
|
+
"upgrade", help="Upgrade the database.", formatter_class=formatter_class
|
|
198
|
+
)
|
|
199
|
+
parser_upgrade.add_argument(
|
|
200
|
+
"-p",
|
|
201
|
+
"--parameter",
|
|
202
|
+
nargs=2,
|
|
203
|
+
help="Assign variable for running SQL deltas. Format is name value.",
|
|
204
|
+
action="append",
|
|
205
|
+
)
|
|
206
|
+
parser_upgrade.add_argument("-u", "--max-version", help="maximum version to upgrade")
|
|
207
|
+
parser_upgrade.add_argument(
|
|
208
|
+
"--skip-grant", help="Skip granting permissions to roles", action="store_true"
|
|
209
|
+
)
|
|
210
|
+
parser_upgrade.add_argument(
|
|
211
|
+
"--beta-testing", help="Install in beta testing mode.", action="store_true"
|
|
212
|
+
)
|
|
213
|
+
parser_upgrade.add_argument(
|
|
214
|
+
"--force",
|
|
215
|
+
help="Allow upgrading a module installed in beta testing mode.",
|
|
216
|
+
action="store_true",
|
|
217
|
+
)
|
|
218
|
+
parser_upgrade.add_argument(
|
|
219
|
+
"--skip-drop-app",
|
|
220
|
+
help="Skip drop app handlers during upgrade.",
|
|
221
|
+
action="store_true",
|
|
222
|
+
)
|
|
223
|
+
parser_upgrade.add_argument(
|
|
224
|
+
"--skip-create-app",
|
|
225
|
+
help="Skip create app handlers during upgrade.",
|
|
226
|
+
action="store_true",
|
|
227
|
+
)
|
|
209
228
|
|
|
210
229
|
# Role management parser
|
|
211
|
-
parser_role = subparsers.add_parser(
|
|
230
|
+
parser_role = subparsers.add_parser(
|
|
231
|
+
"role", help="manage roles in the database", formatter_class=formatter_class
|
|
232
|
+
)
|
|
212
233
|
parser_role.add_argument(
|
|
213
234
|
"action", choices=["create", "grant", "revoke", "drop"], help="Action to perform"
|
|
214
235
|
)
|
|
215
236
|
|
|
216
237
|
# Parser for the "check" command
|
|
217
|
-
|
|
218
|
-
"check", help="check the differences between two databases"
|
|
238
|
+
parser_checker = subparsers.add_parser(
|
|
239
|
+
"check", help="check the differences between two databases", formatter_class=formatter_class
|
|
219
240
|
)
|
|
220
241
|
|
|
221
|
-
|
|
242
|
+
parser_checker.add_argument(
|
|
243
|
+
"pg_connection_compared",
|
|
244
|
+
help="PostgreSQL service name or connection string for the database to compare against",
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
parser_checker.add_argument(
|
|
222
248
|
"-i",
|
|
223
249
|
"--ignore",
|
|
224
250
|
help="Elements to be ignored",
|
|
@@ -235,20 +261,29 @@ def create_parser() -> argparse.ArgumentParser:
|
|
|
235
261
|
"rules",
|
|
236
262
|
],
|
|
237
263
|
)
|
|
238
|
-
|
|
264
|
+
parser_checker.add_argument(
|
|
239
265
|
"-N", "--exclude-schema", help="Schema to be ignored.", action="append"
|
|
240
266
|
)
|
|
241
|
-
|
|
267
|
+
parser_checker.add_argument(
|
|
242
268
|
"-P",
|
|
243
269
|
"--exclude-field-pattern",
|
|
244
270
|
help="Fields to be ignored based on a pattern compatible with SQL LIKE.",
|
|
245
271
|
action="append",
|
|
246
272
|
)
|
|
247
273
|
|
|
248
|
-
|
|
274
|
+
parser_checker.add_argument("-o", "--output_file", help="Output file")
|
|
275
|
+
parser_checker.add_argument(
|
|
276
|
+
"-f",
|
|
277
|
+
"--format",
|
|
278
|
+
choices=["text", "html", "json"],
|
|
279
|
+
default="text",
|
|
280
|
+
help="Output format: text, html, or json. Default: text",
|
|
281
|
+
)
|
|
249
282
|
|
|
250
283
|
# Parser for the "dump" command
|
|
251
|
-
parser_dump = subparsers.add_parser(
|
|
284
|
+
parser_dump = subparsers.add_parser(
|
|
285
|
+
"dump", help="dump a Postgres database", formatter_class=formatter_class
|
|
286
|
+
)
|
|
252
287
|
parser_dump.add_argument(
|
|
253
288
|
"-f",
|
|
254
289
|
"--format",
|
|
@@ -264,7 +299,9 @@ def create_parser() -> argparse.ArgumentParser:
|
|
|
264
299
|
|
|
265
300
|
# Parser for the "restore" command
|
|
266
301
|
parser_restore = subparsers.add_parser(
|
|
267
|
-
"restore",
|
|
302
|
+
"restore",
|
|
303
|
+
help="restore a Postgres database from a dump file",
|
|
304
|
+
formatter_class=formatter_class,
|
|
268
305
|
)
|
|
269
306
|
parser_restore.add_argument("-x", help="ignore pg_restore errors", action="store_true")
|
|
270
307
|
parser_restore.add_argument(
|
|
@@ -274,32 +311,63 @@ def create_parser() -> argparse.ArgumentParser:
|
|
|
274
311
|
|
|
275
312
|
# Parser for the "baseline" command
|
|
276
313
|
parser_baseline = subparsers.add_parser(
|
|
277
|
-
"baseline",
|
|
314
|
+
"baseline",
|
|
315
|
+
help="Create upgrade information table and set baseline",
|
|
316
|
+
formatter_class=formatter_class,
|
|
278
317
|
)
|
|
279
318
|
parser_baseline.add_argument(
|
|
280
319
|
"-b", "--baseline", help="Set baseline in the format x.x.x", required=True
|
|
281
320
|
)
|
|
321
|
+
parser_baseline.add_argument(
|
|
322
|
+
"--create-table",
|
|
323
|
+
help="Create the pum_migrations table if it does not exist",
|
|
324
|
+
action="store_true",
|
|
325
|
+
)
|
|
282
326
|
|
|
283
|
-
# Parser for the "
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
327
|
+
# Parser for the "uninstall" command
|
|
328
|
+
parser_uninstall = subparsers.add_parser(
|
|
329
|
+
"uninstall",
|
|
330
|
+
help="Uninstall the module by executing uninstall hooks",
|
|
331
|
+
formatter_class=formatter_class,
|
|
332
|
+
)
|
|
333
|
+
parser_uninstall.add_argument(
|
|
287
334
|
"-p",
|
|
288
335
|
"--parameter",
|
|
289
336
|
nargs=2,
|
|
290
|
-
help="Assign variable for running SQL
|
|
337
|
+
help="Assign variable for running SQL hooks. Format is name value.",
|
|
291
338
|
action="append",
|
|
292
339
|
)
|
|
340
|
+
parser_uninstall.add_argument(
|
|
341
|
+
"--force",
|
|
342
|
+
help="Skip confirmation prompt and proceed with uninstall",
|
|
343
|
+
action="store_true",
|
|
344
|
+
dest="force",
|
|
345
|
+
)
|
|
293
346
|
|
|
294
347
|
return parser
|
|
295
348
|
|
|
296
349
|
|
|
297
350
|
def cli() -> int: # noqa: PLR0912
|
|
298
|
-
"""
|
|
351
|
+
"""Run the command line interface.
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
Process exit code.
|
|
355
|
+
|
|
356
|
+
"""
|
|
299
357
|
parser = create_parser()
|
|
300
358
|
args = parser.parse_args()
|
|
301
359
|
|
|
302
|
-
|
|
360
|
+
# Validate mutually exclusive flags
|
|
361
|
+
if args.quiet and args.verbose:
|
|
362
|
+
parser.error("--quiet and --verbose are mutually exclusive")
|
|
363
|
+
|
|
364
|
+
# Set verbosity level (-1 for quiet, 0 for normal, 1+ for verbose)
|
|
365
|
+
if args.quiet:
|
|
366
|
+
verbosity = -1
|
|
367
|
+
else:
|
|
368
|
+
verbosity = args.verbose
|
|
369
|
+
|
|
370
|
+
setup_logging(verbosity)
|
|
303
371
|
logger = logging.getLogger(__name__)
|
|
304
372
|
|
|
305
373
|
# if no command is passed, print the help and exit
|
|
@@ -307,20 +375,72 @@ def cli() -> int: # noqa: PLR0912
|
|
|
307
375
|
parser.print_help()
|
|
308
376
|
parser.exit()
|
|
309
377
|
|
|
378
|
+
# Handle check command separately (doesn't need db connection)
|
|
379
|
+
if args.command == "check":
|
|
380
|
+
exit_code = 0
|
|
381
|
+
checker = Checker(
|
|
382
|
+
args.pg_connection,
|
|
383
|
+
args.pg_connection_compared,
|
|
384
|
+
exclude_schema=args.exclude_schema or [],
|
|
385
|
+
exclude_field_pattern=args.exclude_field_pattern or [],
|
|
386
|
+
ignore_list=args.ignore or [],
|
|
387
|
+
)
|
|
388
|
+
report = checker.run_checks()
|
|
389
|
+
checker.conn1.close()
|
|
390
|
+
checker.conn2.close()
|
|
391
|
+
|
|
392
|
+
if report.passed:
|
|
393
|
+
logger.info("OK")
|
|
394
|
+
else:
|
|
395
|
+
logger.info("DIFFERENCES FOUND")
|
|
396
|
+
|
|
397
|
+
if args.format == "html":
|
|
398
|
+
html_report = ReportGenerator.generate_html(report)
|
|
399
|
+
if args.output_file:
|
|
400
|
+
with open(args.output_file, "w", encoding="utf-8") as f:
|
|
401
|
+
f.write(html_report)
|
|
402
|
+
logger.info(f"HTML report written to {args.output_file}")
|
|
403
|
+
else:
|
|
404
|
+
print(html_report)
|
|
405
|
+
elif args.format == "json":
|
|
406
|
+
json_report = ReportGenerator.generate_json(report)
|
|
407
|
+
if args.output_file:
|
|
408
|
+
with open(args.output_file, "w", encoding="utf-8") as f:
|
|
409
|
+
f.write(json_report)
|
|
410
|
+
logger.info(f"JSON report written to {args.output_file}")
|
|
411
|
+
else:
|
|
412
|
+
print(json_report)
|
|
413
|
+
else:
|
|
414
|
+
# Text output (backward compatible)
|
|
415
|
+
text_output = ReportGenerator.generate_text(report)
|
|
416
|
+
if args.output_file:
|
|
417
|
+
with open(args.output_file, "w") as f:
|
|
418
|
+
f.write(text_output)
|
|
419
|
+
else:
|
|
420
|
+
print(text_output)
|
|
421
|
+
|
|
422
|
+
if not report.passed:
|
|
423
|
+
exit_code = 1
|
|
424
|
+
|
|
425
|
+
return exit_code
|
|
426
|
+
|
|
427
|
+
validate = args.command not in ("info", "baseline")
|
|
310
428
|
if args.config_file:
|
|
311
|
-
config = PumConfig.from_yaml(args.config_file, install_dependencies=True)
|
|
429
|
+
config = PumConfig.from_yaml(args.config_file, validate=validate, install_dependencies=True)
|
|
312
430
|
else:
|
|
313
|
-
config = PumConfig.from_yaml(
|
|
431
|
+
config = PumConfig.from_yaml(
|
|
432
|
+
Path(args.dir) / ".pum.yaml", validate=validate, install_dependencies=True
|
|
433
|
+
)
|
|
314
434
|
|
|
315
|
-
with psycopg.connect(
|
|
435
|
+
with psycopg.connect(format_connection_string(args.pg_connection)) as conn:
|
|
316
436
|
# Check if the connection is successful
|
|
317
437
|
if not conn:
|
|
318
|
-
logger.error(f"Could not connect to the database
|
|
438
|
+
logger.error(f"Could not connect to the database: {args.pg_connection}")
|
|
319
439
|
sys.exit(1)
|
|
320
440
|
|
|
321
441
|
# Build parameters dict for install and upgrade commands
|
|
322
442
|
parameters = {}
|
|
323
|
-
if args.command in ("install", "upgrade"):
|
|
443
|
+
if args.command in ("install", "upgrade", "uninstall"):
|
|
324
444
|
for p in args.parameter or ():
|
|
325
445
|
param = config.parameter(p[0])
|
|
326
446
|
if not param:
|
|
@@ -338,7 +458,6 @@ def cli() -> int: # noqa: PLR0912
|
|
|
338
458
|
raise ValueError(f"Unsupported parameter type for {p[0]}: {param.type}")
|
|
339
459
|
logger.debug(f"Parameters: {parameters}")
|
|
340
460
|
|
|
341
|
-
pum = Pum(args.pg_service, config)
|
|
342
461
|
exit_code = 0
|
|
343
462
|
|
|
344
463
|
if args.command == "info":
|
|
@@ -349,13 +468,34 @@ def cli() -> int: # noqa: PLR0912
|
|
|
349
468
|
connection=conn,
|
|
350
469
|
parameters=parameters,
|
|
351
470
|
max_version=args.max_version,
|
|
352
|
-
roles=args.
|
|
353
|
-
grant=args.
|
|
471
|
+
roles=not args.skip_roles,
|
|
472
|
+
grant=not args.skip_grant,
|
|
354
473
|
beta_testing=args.beta_testing,
|
|
474
|
+
skip_drop_app=args.skip_drop_app,
|
|
475
|
+
skip_create_app=args.skip_create_app,
|
|
355
476
|
)
|
|
356
477
|
conn.commit()
|
|
357
478
|
if args.demo_data:
|
|
358
|
-
upg.install_demo_data(
|
|
479
|
+
upg.install_demo_data(
|
|
480
|
+
name=args.demo_data,
|
|
481
|
+
connection=conn,
|
|
482
|
+
parameters=parameters,
|
|
483
|
+
grant=not args.skip_grant,
|
|
484
|
+
skip_create_app=args.skip_create_app,
|
|
485
|
+
skip_drop_app=args.skip_drop_app,
|
|
486
|
+
)
|
|
487
|
+
elif args.command == "upgrade":
|
|
488
|
+
upg = Upgrader(config=config)
|
|
489
|
+
upg.upgrade(
|
|
490
|
+
connection=conn,
|
|
491
|
+
parameters=parameters,
|
|
492
|
+
max_version=args.max_version,
|
|
493
|
+
grant=not args.skip_grant,
|
|
494
|
+
beta_testing=args.beta_testing,
|
|
495
|
+
force=args.force,
|
|
496
|
+
skip_drop_app=args.skip_drop_app,
|
|
497
|
+
skip_create_app=args.skip_create_app,
|
|
498
|
+
)
|
|
359
499
|
elif args.command == "role":
|
|
360
500
|
if not args.action:
|
|
361
501
|
logger.error(
|
|
@@ -374,28 +514,51 @@ def cli() -> int: # noqa: PLR0912
|
|
|
374
514
|
else:
|
|
375
515
|
logger.error(f"Unknown action: {args.action}")
|
|
376
516
|
exit_code = 1
|
|
377
|
-
elif args.command == "check":
|
|
378
|
-
success = pum.run_check(
|
|
379
|
-
args.pg_service1,
|
|
380
|
-
args.pg_service2,
|
|
381
|
-
ignore_list=args.ignore,
|
|
382
|
-
exclude_schema=args.exclude_schema,
|
|
383
|
-
exclude_field_pattern=args.exclude_field_pattern,
|
|
384
|
-
verbose_level=args.verbose_level,
|
|
385
|
-
output_file=args.output_file,
|
|
386
|
-
)
|
|
387
|
-
if not success:
|
|
388
|
-
exit_code = 1
|
|
389
517
|
elif args.command == "dump":
|
|
390
|
-
|
|
518
|
+
dumper = Dumper(args.pg_connection, args.file)
|
|
519
|
+
dumper.pg_dump(
|
|
520
|
+
exclude_schema=args.exclude_schema or [],
|
|
521
|
+
format=args.format,
|
|
522
|
+
)
|
|
523
|
+
logger.info(f"Database dumped to {args.file}")
|
|
391
524
|
elif args.command == "restore":
|
|
392
|
-
|
|
525
|
+
dumper = Dumper(args.pg_connection, args.file)
|
|
526
|
+
try:
|
|
527
|
+
dumper.pg_restore(exclude_schema=args.exclude_schema or [])
|
|
528
|
+
logger.info(f"Database restored from {args.file}")
|
|
529
|
+
except Exception as e:
|
|
530
|
+
if not args.x:
|
|
531
|
+
raise
|
|
532
|
+
logger.warning(f"Restore completed with errors (ignored): {e}")
|
|
393
533
|
elif args.command == "baseline":
|
|
534
|
+
sm = SchemaMigrations(config=config)
|
|
535
|
+
if not sm.exists(connection=conn):
|
|
536
|
+
if args.create_table:
|
|
537
|
+
sm.create(connection=conn)
|
|
538
|
+
logger.info("Created pum_migrations table.")
|
|
539
|
+
else:
|
|
540
|
+
logger.error(
|
|
541
|
+
"pum_migrations table does not exist. Use --create-table to create it."
|
|
542
|
+
)
|
|
543
|
+
exit_code = 1
|
|
544
|
+
return exit_code
|
|
394
545
|
SchemaMigrations(config=config).set_baseline(connection=conn, version=args.baseline)
|
|
395
546
|
|
|
396
|
-
elif args.command == "
|
|
397
|
-
#
|
|
398
|
-
|
|
547
|
+
elif args.command == "uninstall":
|
|
548
|
+
# Confirmation prompt unless --force is used
|
|
549
|
+
if not args.force:
|
|
550
|
+
logger.warning(
|
|
551
|
+
"⚠️ WARNING: This will execute uninstall hooks which may drop schemas and data!"
|
|
552
|
+
)
|
|
553
|
+
response = input("Are you sure you want to proceed? (yes/no): ").strip().lower()
|
|
554
|
+
if response not in ("yes", "y"):
|
|
555
|
+
logger.info("Uninstall cancelled.")
|
|
556
|
+
return 0
|
|
557
|
+
|
|
558
|
+
upg = Upgrader(config=config)
|
|
559
|
+
upg.uninstall(connection=conn, parameters=parameters)
|
|
560
|
+
logger.info("Uninstall completed successfully.")
|
|
561
|
+
|
|
399
562
|
else:
|
|
400
563
|
logger.error(f"Unknown command: {args.command}")
|
|
401
564
|
logger.error("Use -h or --help for help.")
|