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/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.
|
|
106
|
-
|
|
107
|
-
"""
|
|
108
|
-
# self.__out("Check...")
|
|
109
|
-
verbose_level = verbose_level or 1
|
|
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()
|
|
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.
|
|
123
93
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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)
|
|
94
|
+
Args:
|
|
95
|
+
max_help_position: Maximum help position for formatting.
|
|
96
|
+
width: Width for formatting.
|
|
146
97
|
|
|
147
|
-
|
|
148
|
-
|
|
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
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
parser_checker.add_argument(
|
|
243
|
+
"pg_connection_compared",
|
|
244
|
+
help="PostgreSQL service name or connection string for the database to compare against",
|
|
219
245
|
)
|
|
220
246
|
|
|
221
|
-
|
|
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,7 +311,9 @@ 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
|
|
@@ -285,26 +324,50 @@ def create_parser() -> argparse.ArgumentParser:
|
|
|
285
324
|
action="store_true",
|
|
286
325
|
)
|
|
287
326
|
|
|
288
|
-
# Parser for the "
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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(
|
|
292
334
|
"-p",
|
|
293
335
|
"--parameter",
|
|
294
336
|
nargs=2,
|
|
295
|
-
help="Assign variable for running SQL
|
|
337
|
+
help="Assign variable for running SQL hooks. Format is name value.",
|
|
296
338
|
action="append",
|
|
297
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
|
+
)
|
|
298
346
|
|
|
299
347
|
return parser
|
|
300
348
|
|
|
301
349
|
|
|
302
350
|
def cli() -> int: # noqa: PLR0912
|
|
303
|
-
"""
|
|
351
|
+
"""Run the command line interface.
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
Process exit code.
|
|
355
|
+
|
|
356
|
+
"""
|
|
304
357
|
parser = create_parser()
|
|
305
358
|
args = parser.parse_args()
|
|
306
359
|
|
|
307
|
-
|
|
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)
|
|
308
371
|
logger = logging.getLogger(__name__)
|
|
309
372
|
|
|
310
373
|
# if no command is passed, print the help and exit
|
|
@@ -312,6 +375,55 @@ def cli() -> int: # noqa: PLR0912
|
|
|
312
375
|
parser.print_help()
|
|
313
376
|
parser.exit()
|
|
314
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
|
+
|
|
315
427
|
validate = args.command not in ("info", "baseline")
|
|
316
428
|
if args.config_file:
|
|
317
429
|
config = PumConfig.from_yaml(args.config_file, validate=validate, install_dependencies=True)
|
|
@@ -320,15 +432,15 @@ def cli() -> int: # noqa: PLR0912
|
|
|
320
432
|
Path(args.dir) / ".pum.yaml", validate=validate, install_dependencies=True
|
|
321
433
|
)
|
|
322
434
|
|
|
323
|
-
with psycopg.connect(
|
|
435
|
+
with psycopg.connect(format_connection_string(args.pg_connection)) as conn:
|
|
324
436
|
# Check if the connection is successful
|
|
325
437
|
if not conn:
|
|
326
|
-
logger.error(f"Could not connect to the database
|
|
438
|
+
logger.error(f"Could not connect to the database: {args.pg_connection}")
|
|
327
439
|
sys.exit(1)
|
|
328
440
|
|
|
329
441
|
# Build parameters dict for install and upgrade commands
|
|
330
442
|
parameters = {}
|
|
331
|
-
if args.command in ("install", "upgrade"):
|
|
443
|
+
if args.command in ("install", "upgrade", "uninstall"):
|
|
332
444
|
for p in args.parameter or ():
|
|
333
445
|
param = config.parameter(p[0])
|
|
334
446
|
if not param:
|
|
@@ -346,7 +458,6 @@ def cli() -> int: # noqa: PLR0912
|
|
|
346
458
|
raise ValueError(f"Unsupported parameter type for {p[0]}: {param.type}")
|
|
347
459
|
logger.debug(f"Parameters: {parameters}")
|
|
348
460
|
|
|
349
|
-
pum = Pum(args.pg_service, config)
|
|
350
461
|
exit_code = 0
|
|
351
462
|
|
|
352
463
|
if args.command == "info":
|
|
@@ -357,13 +468,34 @@ def cli() -> int: # noqa: PLR0912
|
|
|
357
468
|
connection=conn,
|
|
358
469
|
parameters=parameters,
|
|
359
470
|
max_version=args.max_version,
|
|
360
|
-
roles=args.
|
|
361
|
-
grant=args.
|
|
471
|
+
roles=not args.skip_roles,
|
|
472
|
+
grant=not args.skip_grant,
|
|
362
473
|
beta_testing=args.beta_testing,
|
|
474
|
+
skip_drop_app=args.skip_drop_app,
|
|
475
|
+
skip_create_app=args.skip_create_app,
|
|
363
476
|
)
|
|
364
477
|
conn.commit()
|
|
365
478
|
if args.demo_data:
|
|
366
|
-
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
|
+
)
|
|
367
499
|
elif args.command == "role":
|
|
368
500
|
if not args.action:
|
|
369
501
|
logger.error(
|
|
@@ -382,22 +514,22 @@ def cli() -> int: # noqa: PLR0912
|
|
|
382
514
|
else:
|
|
383
515
|
logger.error(f"Unknown action: {args.action}")
|
|
384
516
|
exit_code = 1
|
|
385
|
-
elif args.command == "check":
|
|
386
|
-
success = pum.run_check(
|
|
387
|
-
args.pg_service1,
|
|
388
|
-
args.pg_service2,
|
|
389
|
-
ignore_list=args.ignore,
|
|
390
|
-
exclude_schema=args.exclude_schema,
|
|
391
|
-
exclude_field_pattern=args.exclude_field_pattern,
|
|
392
|
-
verbose_level=args.verbose_level,
|
|
393
|
-
output_file=args.output_file,
|
|
394
|
-
)
|
|
395
|
-
if not success:
|
|
396
|
-
exit_code = 1
|
|
397
517
|
elif args.command == "dump":
|
|
398
|
-
|
|
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}")
|
|
399
524
|
elif args.command == "restore":
|
|
400
|
-
|
|
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}")
|
|
401
533
|
elif args.command == "baseline":
|
|
402
534
|
sm = SchemaMigrations(config=config)
|
|
403
535
|
if not sm.exists(connection=conn):
|
|
@@ -412,9 +544,21 @@ def cli() -> int: # noqa: PLR0912
|
|
|
412
544
|
return exit_code
|
|
413
545
|
SchemaMigrations(config=config).set_baseline(connection=conn, version=args.baseline)
|
|
414
546
|
|
|
415
|
-
elif args.command == "
|
|
416
|
-
#
|
|
417
|
-
|
|
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
|
+
|
|
418
562
|
else:
|
|
419
563
|
logger.error(f"Unknown command: {args.command}")
|
|
420
564
|
logger.error("Use -h or --help for help.")
|