dbqm 2.3.2__tar.gz → 2.4.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.
- {dbqm-2.3.2 → dbqm-2.4.0}/PKG-INFO +1 -1
- {dbqm-2.3.2 → dbqm-2.4.0}/README.md +3 -1
- dbqm-2.4.0/dbqm/_version.py +1 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/cli/__init__.py +4 -4
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/cli/commands/query.py +23 -3
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/cli/deps.py +3 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/core/html_report.py +83 -11
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm.egg-info/PKG-INFO +1 -1
- {dbqm-2.3.2 → dbqm-2.4.0}/tests/test_cli.py +109 -0
- dbqm-2.3.2/dbqm/_version.py +0 -1
- {dbqm-2.3.2 → dbqm-2.4.0}/PYPI.md +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/__init__.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/__main__.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/cli/commands/__init__.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/cli/commands/config_bundle.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/cli/commands/connection.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/cli/commands/inspect.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/cli/commands/schema.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/cli/envelope.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/cli/errors.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/cli/params.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/cli/render.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/core/__init__.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/core/audit.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/core/config_portability.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/core/connection_builder.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/core/crypto.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/core/db_manager.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/core/ddl_extractor.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/core/ddl_mysql.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/core/ddl_pg.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/core/exporter.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/core/group_engine.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/core/history.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/core/object_browser.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/core/oracle_client_installer.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/core/package_editor.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/core/paths.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/core/query_engine.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/core/read_only.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/core/table_browser.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/core/template_engine.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/design/__init__.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/design/tokens.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/main.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/models/__init__.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/models/connection.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/models/group.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/models/query.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/models/settings.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/models/template.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/ui/__init__.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/ui/app.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/ui/modals/__init__.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/ui/modals/column_maps.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/ui/modals/confirm.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/ui/modals/error.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/ui/modals/export_dir_setup.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/ui/modals/export_picker.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/ui/modals/help.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/ui/modals/oracle_client_dir.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/ui/modals/param_input.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/ui/modals/text_input.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/ui/screens/__init__.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/ui/screens/adhoc.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/ui/screens/browser.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/ui/screens/config_port.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/ui/screens/connections.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/ui/screens/exec_routine.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/ui/screens/group_exec.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/ui/screens/group_manage.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/ui/screens/group_run.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/ui/screens/history.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/ui/screens/oracle_clients.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/ui/screens/package_editor.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/ui/screens/query_exec.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/ui/screens/query_manage.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/ui/screens/settings.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/ui/screens/template_manage.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/ui/screens/tools.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/ui/theme.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/ui/utils.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/ui/widgets/__init__.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/ui/widgets/action_bar.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/ui/widgets/dialog.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/ui/widgets/empty_state.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/ui/widgets/group_result.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/ui/widgets/hierarchical_list.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/ui/widgets/panel.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/ui/widgets/progress.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/ui/widgets/query_list.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/ui/widgets/result_table.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/ui/widgets/skeleton.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/ui/widgets/sql_viewer.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/ui/widgets/status_bar.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/ui/widgets/templates_sidebar.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm/ui/widgets/verdict.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm.egg-info/SOURCES.txt +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm.egg-info/dependency_links.txt +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm.egg-info/entry_points.txt +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm.egg-info/requires.txt +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/dbqm.egg-info/top_level.txt +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/pyproject.toml +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/setup.cfg +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/tests/test_cli_markup.py +0 -0
- {dbqm-2.3.2 → dbqm-2.4.0}/tests/test_cli_tema.py +0 -0
|
@@ -225,7 +225,9 @@ offer `table|json|csv|raw`; every other command — `test`, `list`, `ddl`,
|
|
|
225
225
|
`connection` group — offers `table|json`. `raw` prints plain values with no
|
|
226
226
|
headers/decoration, handy for piping the body of a view, package, or
|
|
227
227
|
procedure to another tool.
|
|
228
|
-
`--export csv|json|txt` writes the result to a file regardless of `-f`.
|
|
228
|
+
`--export csv|json|txt|html` writes the result to a file regardless of `-f`.
|
|
229
|
+
`html` writes a standalone report meant to be read in a browser, and is not
|
|
230
|
+
available together with `--flat`.
|
|
229
231
|
|
|
230
232
|
`-f json` wraps every command in one envelope: a success prints
|
|
231
233
|
`{"ok": true, "command": "...", "data": {...}}` to stdout; a failure prints
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2.4.0"
|
|
@@ -33,7 +33,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
33
33
|
help="Parametro (pode repetir)")
|
|
34
34
|
p_run.add_argument("-f", "--format", choices=["table", "json", "csv", "raw"], default="table",
|
|
35
35
|
help="Formato de saida (padrao: table). 'raw' imprime valores sem decoracao.")
|
|
36
|
-
p_run.add_argument("-e", "--export", choices=["csv", "json", "txt"],
|
|
36
|
+
p_run.add_argument("-e", "--export", choices=["csv", "json", "txt", "html"],
|
|
37
37
|
help="Exportar resultado para arquivo")
|
|
38
38
|
|
|
39
39
|
# --- run-group ---
|
|
@@ -43,8 +43,8 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
43
43
|
help="Parametro compartilhado (pode repetir)")
|
|
44
44
|
p_grp.add_argument("-f", "--format", choices=["table", "json"], default="table",
|
|
45
45
|
help="Formato de saida")
|
|
46
|
-
p_grp.add_argument("-e", "--export", choices=["csv", "json", "txt"],
|
|
47
|
-
help="Exportar resultado para arquivo")
|
|
46
|
+
p_grp.add_argument("-e", "--export", choices=["csv", "json", "txt", "html"],
|
|
47
|
+
help="Exportar resultado para arquivo. 'html' nao pode ser usado com --flat.")
|
|
48
48
|
p_grp.add_argument("--flat", action="store_true",
|
|
49
49
|
help="Usar formato flat (um bloco por coluna)")
|
|
50
50
|
|
|
@@ -69,7 +69,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
69
69
|
help="Parametro (pode repetir)")
|
|
70
70
|
p_sql.add_argument("-f", "--format", choices=["table", "json", "csv", "raw"], default="table",
|
|
71
71
|
help="Formato de saida. 'raw' imprime valores sem decoracao.")
|
|
72
|
-
p_sql.add_argument("-e", "--export", choices=["csv", "json", "txt"],
|
|
72
|
+
p_sql.add_argument("-e", "--export", choices=["csv", "json", "txt", "html"],
|
|
73
73
|
help="Exportar resultado para arquivo")
|
|
74
74
|
p_sql.add_argument("--commit", action="store_true",
|
|
75
75
|
help="Auto-commit para DML (INSERT/UPDATE/DELETE)")
|
|
@@ -133,6 +133,8 @@ def cmd_run(args: argparse.Namespace) -> None:
|
|
|
133
133
|
path = deps.export_query_json(result, table_name, param_values)
|
|
134
134
|
elif fmt == "txt":
|
|
135
135
|
path = deps.export_query_txt(result, table_name, param_values)
|
|
136
|
+
elif fmt == "html":
|
|
137
|
+
path = deps.export_query_html(result, table_name, param_values)
|
|
136
138
|
else:
|
|
137
139
|
_fail_or_print(args, "run", "usage", f"Formato de export invalido: {fmt}")
|
|
138
140
|
if args.format == "json":
|
|
@@ -162,6 +164,14 @@ def cmd_run_group(args: argparse.Namespace) -> None:
|
|
|
162
164
|
on stdout. The exit code alone is what lets a shell branch on the verdict
|
|
163
165
|
without parsing anything.
|
|
164
166
|
"""
|
|
167
|
+
# An argument combination that can never be valid is refused before any
|
|
168
|
+
# work happens -- before the group is even resolved, let alone any query
|
|
169
|
+
# run or any history record written.
|
|
170
|
+
if args.export == "html" and args.flat:
|
|
171
|
+
_fail_or_print(args, "run-group", "usage",
|
|
172
|
+
"--flat nao tem versao HTML. Use --export html sem --flat, "
|
|
173
|
+
"ou --flat com csv, json ou txt.")
|
|
174
|
+
|
|
165
175
|
group = deps.find_group(args.group)
|
|
166
176
|
if not group:
|
|
167
177
|
_fail_or_print(args, "run-group", "not_found", f"Grupo '{args.group}' nao encontrado.")
|
|
@@ -219,15 +229,21 @@ def cmd_run_group(args: argparse.Namespace) -> None:
|
|
|
219
229
|
path = deps.export_group_flat_csv(group_result, param_values)
|
|
220
230
|
elif fmt == "json":
|
|
221
231
|
path = deps.export_group_flat_json(group_result, param_values)
|
|
222
|
-
|
|
232
|
+
elif fmt == "txt":
|
|
223
233
|
path = deps.export_group_flat_txt(group_result, param_values)
|
|
234
|
+
else:
|
|
235
|
+
_fail_or_print(args, "run-group", "usage", f"Formato de export invalido: {fmt}")
|
|
224
236
|
else:
|
|
225
237
|
if fmt == "csv":
|
|
226
238
|
path = deps.export_group_csv(group_result, param_values)
|
|
227
239
|
elif fmt == "json":
|
|
228
240
|
path = deps.export_group_json(group_result, param_values)
|
|
229
|
-
|
|
241
|
+
elif fmt == "txt":
|
|
230
242
|
path = deps.export_group_txt(group_result, param_values)
|
|
243
|
+
elif fmt == "html":
|
|
244
|
+
path = deps.export_group_html(group_result, param_values)
|
|
245
|
+
else:
|
|
246
|
+
_fail_or_print(args, "run-group", "usage", f"Formato de export invalido: {fmt}")
|
|
231
247
|
if args.format == "json":
|
|
232
248
|
ok("run-group", {"exported": str(path), "format": fmt})
|
|
233
249
|
else:
|
|
@@ -412,8 +428,12 @@ def cmd_sql(args: argparse.Namespace) -> None:
|
|
|
412
428
|
path = deps.export_query_csv(qr, "adhoc", param_values)
|
|
413
429
|
elif fmt == "json":
|
|
414
430
|
path = deps.export_query_json(qr, "adhoc", param_values)
|
|
415
|
-
|
|
431
|
+
elif fmt == "txt":
|
|
416
432
|
path = deps.export_query_txt(qr, "adhoc", param_values)
|
|
433
|
+
elif fmt == "html":
|
|
434
|
+
path = deps.export_query_html(qr, "adhoc", param_values)
|
|
435
|
+
else:
|
|
436
|
+
_fail_or_print(args, "sql", "usage", f"Formato de export invalido: {fmt}")
|
|
417
437
|
if args.format == "json":
|
|
418
438
|
ok("sql", {"exported": str(path), "format": fmt})
|
|
419
439
|
return
|
|
@@ -39,6 +39,7 @@ from dbqm.core.exporter import (
|
|
|
39
39
|
export_query_txt,
|
|
40
40
|
)
|
|
41
41
|
from dbqm.core.group_engine import build_group_result
|
|
42
|
+
from dbqm.core.html_report import export_group_html, export_query_html
|
|
42
43
|
from dbqm.core.history import (
|
|
43
44
|
clear_history,
|
|
44
45
|
load_history,
|
|
@@ -83,9 +84,11 @@ __all__ = [
|
|
|
83
84
|
"export_group_flat_csv",
|
|
84
85
|
"export_group_flat_json",
|
|
85
86
|
"export_group_flat_txt",
|
|
87
|
+
"export_group_html",
|
|
86
88
|
"export_group_json",
|
|
87
89
|
"export_group_txt",
|
|
88
90
|
"export_query_csv",
|
|
91
|
+
"export_query_html",
|
|
89
92
|
"export_query_json",
|
|
90
93
|
"export_query_txt",
|
|
91
94
|
"extract_ddl",
|
|
@@ -6,6 +6,7 @@ from html import escape as h
|
|
|
6
6
|
from typing import Any
|
|
7
7
|
|
|
8
8
|
from dbqm.core.group_engine import GroupResult
|
|
9
|
+
from dbqm.core.query_engine import QueryResult
|
|
9
10
|
from dbqm.core.exporter import _build_filepath
|
|
10
11
|
from dbqm.design.tokens import LIGHT_TOKENS, DARK_TOKENS
|
|
11
12
|
|
|
@@ -29,6 +30,25 @@ def _light_theme_block(tokens: dict[str, str]) -> str:
|
|
|
29
30
|
return f"@media (prefers-color-scheme: light) {{\n :root {{\n{linhas}\n }}\n}}"
|
|
30
31
|
|
|
31
32
|
|
|
33
|
+
#: Layout rules shared by every report shell -- reset, body, header, the
|
|
34
|
+
#: params table and the data table. Renderer-specific rules (status
|
|
35
|
+
#: colouring, filter bar, search box, …) stay in their own `_build_*`
|
|
36
|
+
#: function; only what both renderers actually share lives here, so a change
|
|
37
|
+
#: to shared padding cannot drift between the two documents.
|
|
38
|
+
_BASE_STYLE_RULES = """\
|
|
39
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
40
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: var(--ds-background); color: var(--ds-text); padding: 24px; }
|
|
41
|
+
.header { background: var(--ds-panel); border-radius: 8px; padding: 20px; margin-bottom: 20px; }
|
|
42
|
+
.header h1 { color: var(--ds-identity); font-size: 1.4em; }
|
|
43
|
+
.header .meta { color: var(--ds-text-muted); font-size: 0.85em; margin-top: 8px; }
|
|
44
|
+
.params { margin: 12px 0; border-collapse: collapse; }
|
|
45
|
+
.params td { padding: 4px 16px 4px 0; color: var(--ds-text-muted); font-size: 0.9em; }
|
|
46
|
+
table.data { width: 100%; border-collapse: collapse; margin-bottom: 8px; font-size: 0.9em; }
|
|
47
|
+
table.data th { background: var(--ds-panel); color: var(--ds-identity); padding: 8px 12px; text-align: left; border-bottom: 2px solid var(--ds-border); }
|
|
48
|
+
table.data td { padding: 6px 12px; border-bottom: 1px solid var(--ds-border); }
|
|
49
|
+
table.data tr:hover { background: var(--ds-surface-raised); }"""
|
|
50
|
+
|
|
51
|
+
|
|
32
52
|
def export_group_html(group_result: GroupResult, params: dict[str, str] | None = None) -> str:
|
|
33
53
|
"""Export group comparison as a standalone HTML file. Returns file path."""
|
|
34
54
|
query_names = list(group_result.query_results.keys())
|
|
@@ -39,6 +59,18 @@ def export_group_html(group_result: GroupResult, params: dict[str, str] | None =
|
|
|
39
59
|
return str(filepath)
|
|
40
60
|
|
|
41
61
|
|
|
62
|
+
def export_query_html(
|
|
63
|
+
result: QueryResult,
|
|
64
|
+
table: str = "",
|
|
65
|
+
params: dict[str, str] | None = None,
|
|
66
|
+
) -> str:
|
|
67
|
+
"""Export one result set as a standalone HTML file. Returns file path."""
|
|
68
|
+
label = table or result.query_name
|
|
69
|
+
filepath = _build_filepath("consultas", label, result.connection_name, params, "html")
|
|
70
|
+
filepath.write_text(_build_query_html(result, label, params), encoding="utf-8")
|
|
71
|
+
return str(filepath)
|
|
72
|
+
|
|
73
|
+
|
|
42
74
|
def _status_class(status: str) -> str:
|
|
43
75
|
return {"OK": "ok", "OK*": "ok", "DIFF": "diff", "ABSENT": "absent"}.get(status, "")
|
|
44
76
|
|
|
@@ -106,26 +138,16 @@ def _build_html(group_result: GroupResult, query_names: list[str], params: dict[
|
|
|
106
138
|
despercebido. Por isso: nenhuma cor composta em CSS pode carregar
|
|
107
139
|
texto; texto so usa var(--token) puro, e estado em superficie e
|
|
108
140
|
borda/marcador, nunca preenchimento. */
|
|
109
|
-
|
|
110
|
-
body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: var(--ds-background); color: var(--ds-text); padding: 24px; }}
|
|
111
|
-
.header {{ background: var(--ds-panel); border-radius: 8px; padding: 20px; margin-bottom: 20px; }}
|
|
112
|
-
.header h1 {{ color: var(--ds-identity); font-size: 1.4em; }}
|
|
113
|
-
.header .meta {{ color: var(--ds-text-muted); font-size: 0.85em; margin-top: 8px; }}
|
|
141
|
+
{_BASE_STYLE_RULES}
|
|
114
142
|
.badge {{ display: inline-block; padding: 4px 12px; border-radius: 4px; font-weight: bold; font-size: 0.85em; }}
|
|
115
143
|
/* Estado em superficie e borda/marcador, nunca preenchimento: um preenchimento
|
|
116
144
|
misturado a partir do proprio texto sempre compromete um dos dois. */
|
|
117
145
|
.badge.ok {{ background: var(--ds-panel); color: var(--ds-verdict-match); border: 1px solid var(--ds-verdict-match); }}
|
|
118
146
|
.badge.diff {{ background: var(--ds-panel); color: var(--ds-verdict-diff); border: 1px solid var(--ds-verdict-diff); }}
|
|
119
|
-
.params {{ margin: 12px 0; border-collapse: collapse; }}
|
|
120
|
-
.params td {{ padding: 4px 16px 4px 0; color: var(--ds-text-muted); font-size: 0.9em; }}
|
|
121
147
|
h3 {{ color: var(--ds-identity); margin: 24px 0 8px; }}
|
|
122
148
|
.filter-bar {{ margin-bottom: 8px; }}
|
|
123
149
|
.filter-btn {{ background: var(--ds-panel); color: var(--ds-text-muted); border: 1px solid var(--ds-border); padding: 4px 12px; border-radius: 4px; cursor: pointer; margin-right: 4px; font-size: 0.8em; }}
|
|
124
150
|
.filter-btn.active {{ background: var(--ds-surface-raised); color: var(--ds-identity); border-color: var(--ds-identity); }}
|
|
125
|
-
table.data {{ width: 100%; border-collapse: collapse; margin-bottom: 8px; font-size: 0.9em; }}
|
|
126
|
-
table.data th {{ background: var(--ds-panel); color: var(--ds-identity); padding: 8px 12px; text-align: left; border-bottom: 2px solid var(--ds-border); }}
|
|
127
|
-
table.data td {{ padding: 6px 12px; border-bottom: 1px solid var(--ds-border); }}
|
|
128
|
-
table.data tr:hover {{ background: var(--ds-surface-raised); }}
|
|
129
151
|
.key {{ color: var(--ds-text-strong); font-weight: 600; }}
|
|
130
152
|
.ok {{ color: var(--ds-verdict-match); font-weight: 600; }}
|
|
131
153
|
.diff {{ color: var(--ds-verdict-diff); font-weight: 600; }}
|
|
@@ -182,3 +204,53 @@ function filterSearch(term) {{
|
|
|
182
204
|
</script>
|
|
183
205
|
</body>
|
|
184
206
|
</html>"""
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _build_query_html(result: QueryResult, label: str, params: dict[str, str] | None) -> str:
|
|
210
|
+
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
211
|
+
|
|
212
|
+
params_html = ""
|
|
213
|
+
if params:
|
|
214
|
+
rows = "".join(
|
|
215
|
+
f"<tr><td><strong>{h(str(k))}</strong></td><td>{h(str(v))}</td></tr>"
|
|
216
|
+
for k, v in params.items()
|
|
217
|
+
)
|
|
218
|
+
params_html = f'<div class="meta">Parametros:</div><table class="params">{rows}</table>'
|
|
219
|
+
|
|
220
|
+
header_cols = "".join(f"<th>{h(str(col))}</th>" for col in result.columns)
|
|
221
|
+
rows_html = ""
|
|
222
|
+
for row in result.rows:
|
|
223
|
+
cells = "".join(f"<td>{'' if v is None else h(str(v))}</td>" for v in row)
|
|
224
|
+
rows_html += f"<tr>{cells}</tr>"
|
|
225
|
+
|
|
226
|
+
return f"""<!DOCTYPE html>
|
|
227
|
+
<html lang="pt-BR">
|
|
228
|
+
<head>
|
|
229
|
+
<meta charset="UTF-8">
|
|
230
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
231
|
+
<title>Relatorio - {h(label)}</title>
|
|
232
|
+
<style>
|
|
233
|
+
{css_variables(DARK_TOKENS)}
|
|
234
|
+
{_light_theme_block(LIGHT_TOKENS)}
|
|
235
|
+
{_BASE_STYLE_RULES}
|
|
236
|
+
</style>
|
|
237
|
+
</head>
|
|
238
|
+
<body>
|
|
239
|
+
<div class="header">
|
|
240
|
+
<h1>DB Query Manager - Relatorio de Consulta</h1>
|
|
241
|
+
<div class="meta">
|
|
242
|
+
Consulta: <strong>{h(label)}</strong> |
|
|
243
|
+
Conexao: <strong>{h(result.connection_name)}</strong> |
|
|
244
|
+
Data: {now} |
|
|
245
|
+
Linhas: {result.row_count} |
|
|
246
|
+
Tempo: {result.elapsed:.3f}s
|
|
247
|
+
</div>
|
|
248
|
+
{params_html}
|
|
249
|
+
</div>
|
|
250
|
+
|
|
251
|
+
<table class="data">
|
|
252
|
+
<thead><tr>{header_cols}</tr></thead>
|
|
253
|
+
<tbody>{rows_html}</tbody>
|
|
254
|
+
</table>
|
|
255
|
+
</body>
|
|
256
|
+
</html>"""
|
|
@@ -261,6 +261,20 @@ class TestCmdRun:
|
|
|
261
261
|
run_cli(["run", "test_query", "-e", "csv"])
|
|
262
262
|
mock_exp.assert_called_once()
|
|
263
263
|
|
|
264
|
+
def test_run_export_html(self, tmp_config_dir):
|
|
265
|
+
query = _make_query()
|
|
266
|
+
conn = _make_connection()
|
|
267
|
+
result = _make_query_result()
|
|
268
|
+
|
|
269
|
+
with patch("dbqm.cli.deps.find_query", return_value=query), \
|
|
270
|
+
patch("dbqm.cli.deps.find_connection", return_value=conn), \
|
|
271
|
+
patch("dbqm.cli.deps.execute_query", return_value=result), \
|
|
272
|
+
patch("dbqm.cli.deps.record_query_execution"), \
|
|
273
|
+
patch("dbqm.cli.deps.log_execution"), \
|
|
274
|
+
patch("dbqm.cli.deps.export_query_html", return_value="/tmp/out.html") as mock_exp:
|
|
275
|
+
run_cli(["run", "test_query", "-e", "html"])
|
|
276
|
+
mock_exp.assert_called_once()
|
|
277
|
+
|
|
264
278
|
def test_run_format_json(self, capsys):
|
|
265
279
|
"""`data` is `QueryResult.to_dict()` verbatim: `rows` are parallel
|
|
266
280
|
arrays (a list per row, indexed by `columns`), not a dict per row —
|
|
@@ -428,6 +442,83 @@ class TestCmdRunGroup:
|
|
|
428
442
|
run_cli(["run-group", "test_group", "-e", "csv"])
|
|
429
443
|
mock_exp.assert_called_once()
|
|
430
444
|
|
|
445
|
+
def test_group_export_html(self):
|
|
446
|
+
group = _make_group()
|
|
447
|
+
q1 = _make_query("q1", "c1")
|
|
448
|
+
q2 = _make_query("q2", "c2")
|
|
449
|
+
c1 = _make_connection("c1")
|
|
450
|
+
c2 = _make_connection("c2")
|
|
451
|
+
gr = _make_group_result()
|
|
452
|
+
|
|
453
|
+
def find_query_side(name):
|
|
454
|
+
return {"q1": q1, "q2": q2}.get(name)
|
|
455
|
+
|
|
456
|
+
def find_conn_side(name):
|
|
457
|
+
return {"c1": c1, "c2": c2}.get(name)
|
|
458
|
+
|
|
459
|
+
with patch("dbqm.cli.deps.find_group", return_value=group), \
|
|
460
|
+
patch("dbqm.cli.deps.find_query", side_effect=find_query_side), \
|
|
461
|
+
patch("dbqm.cli.deps.find_connection", side_effect=find_conn_side), \
|
|
462
|
+
patch("dbqm.cli.deps.execute_query", return_value=_make_query_result()), \
|
|
463
|
+
patch("dbqm.cli.deps.build_group_result", return_value=gr), \
|
|
464
|
+
patch("dbqm.cli.deps.record_group_execution"), \
|
|
465
|
+
patch("dbqm.cli.deps.export_group_html", return_value="/tmp/g.html") as mock_exp:
|
|
466
|
+
run_cli(["run-group", "test_group", "-e", "html"])
|
|
467
|
+
mock_exp.assert_called_once()
|
|
468
|
+
|
|
469
|
+
def test_group_flat_html_is_refused_table_format(self, capsys):
|
|
470
|
+
"""The trap: `--flat` is a CSV-shaped denormalisation with no HTML
|
|
471
|
+
equivalent. Silently ignoring the flag would hand back a different
|
|
472
|
+
shape (the non-flat report) with no signal that `--flat` was
|
|
473
|
+
dropped. Checked under `table` too, not just `json`: a refusal that
|
|
474
|
+
only fires under one renderer is the bug class 2.3.0 existed to
|
|
475
|
+
fix.
|
|
476
|
+
|
|
477
|
+
The refusal fires before the group is even resolved (a bad argument
|
|
478
|
+
combination costs no work), so `find_group` is mocked but must never
|
|
479
|
+
be called -- that is also what proves this test is pinned to *this*
|
|
480
|
+
refusal and not to the flat arm's generic bare-`else` fallback,
|
|
481
|
+
which would also exit 2 with no export call but only after
|
|
482
|
+
resolving the group and running every query in it.
|
|
483
|
+
"""
|
|
484
|
+
with patch("dbqm.cli.deps.find_group") as mock_find_group, \
|
|
485
|
+
patch("dbqm.cli.deps.export_group_flat_csv") as mock_flat_csv, \
|
|
486
|
+
patch("dbqm.cli.deps.export_group_flat_json") as mock_flat_json, \
|
|
487
|
+
patch("dbqm.cli.deps.export_group_flat_txt") as mock_flat_txt:
|
|
488
|
+
with pytest.raises(SystemExit) as excinfo:
|
|
489
|
+
run_cli(["run-group", "test_group", "--flat", "-e", "html"])
|
|
490
|
+
assert excinfo.value.code == 2
|
|
491
|
+
saida = capsys.readouterr().out
|
|
492
|
+
assert "--flat" in saida, "must name the refused flag, not just exit 2"
|
|
493
|
+
assert "Exportado:" not in saida, "no export must have happened"
|
|
494
|
+
mock_find_group.assert_not_called()
|
|
495
|
+
mock_flat_csv.assert_not_called()
|
|
496
|
+
mock_flat_json.assert_not_called()
|
|
497
|
+
mock_flat_txt.assert_not_called()
|
|
498
|
+
|
|
499
|
+
def test_group_flat_html_is_refused_json_format(self, capsys):
|
|
500
|
+
"""Same refusal, `-f json` side: the contract is that stdout stays
|
|
501
|
+
empty and the failure carries the usage token on stderr. Also fires
|
|
502
|
+
before the group is resolved -- see the table-format sibling above
|
|
503
|
+
for why `find_group` must stay uncalled."""
|
|
504
|
+
with patch("dbqm.cli.deps.find_group") as mock_find_group, \
|
|
505
|
+
patch("dbqm.cli.deps.export_group_flat_csv") as mock_flat_csv, \
|
|
506
|
+
patch("dbqm.cli.deps.export_group_flat_json") as mock_flat_json, \
|
|
507
|
+
patch("dbqm.cli.deps.export_group_flat_txt") as mock_flat_txt:
|
|
508
|
+
with pytest.raises(SystemExit) as excinfo:
|
|
509
|
+
run_cli(["run-group", "test_group", "--flat", "-e", "html", "-f", "json"])
|
|
510
|
+
assert excinfo.value.code == 2
|
|
511
|
+
saida = capsys.readouterr()
|
|
512
|
+
assert saida.out == ""
|
|
513
|
+
corpo = json.loads(saida.err)
|
|
514
|
+
assert corpo["error"]["code"] == "usage"
|
|
515
|
+
assert "--flat" in corpo["error"]["message"]
|
|
516
|
+
assert "html" in corpo["error"]["message"]
|
|
517
|
+
mock_find_group.assert_not_called()
|
|
518
|
+
mock_flat_csv.assert_not_called()
|
|
519
|
+
mock_flat_json.assert_not_called()
|
|
520
|
+
mock_flat_txt.assert_not_called()
|
|
521
|
+
|
|
431
522
|
def test_group_export_json_format_emits_envelope(self, capsys):
|
|
432
523
|
"""CRITICAL fix: `-e` used to print bare prose and return under
|
|
433
524
|
`-f json`."""
|
|
@@ -683,6 +774,24 @@ class TestCmdSql:
|
|
|
683
774
|
run_cli(["sql", "SELECT 1", "test_conn", "-e", "csv"])
|
|
684
775
|
mock_exp.assert_called_once()
|
|
685
776
|
|
|
777
|
+
def test_sql_export_html_does_not_fall_through_to_txt(self):
|
|
778
|
+
"""The trap: `sql`'s export branch used to end in a bare `else` that
|
|
779
|
+
wrote TXT, so a new format would have written the wrong file and
|
|
780
|
+
reported success."""
|
|
781
|
+
conn = _make_connection()
|
|
782
|
+
from dbqm.core.query_engine import AdhocResult
|
|
783
|
+
adhoc = AdhocResult(
|
|
784
|
+
sql_type="SELECT", connection_name="test_conn",
|
|
785
|
+
columns=["x"], rows=[[1]], row_count=1, elapsed=0.01,
|
|
786
|
+
)
|
|
787
|
+
with patch("dbqm.cli.deps.find_connection", return_value=conn), \
|
|
788
|
+
patch("dbqm.cli.deps.execute_adhoc", return_value=adhoc), \
|
|
789
|
+
patch("dbqm.cli.deps.export_query_html", return_value="/tmp/o.html") as mock_html, \
|
|
790
|
+
patch("dbqm.cli.deps.export_query_txt") as mock_txt:
|
|
791
|
+
run_cli(["sql", "SELECT 1", "test_conn", "-e", "html"])
|
|
792
|
+
mock_html.assert_called_once()
|
|
793
|
+
mock_txt.assert_not_called()
|
|
794
|
+
|
|
686
795
|
def test_sql_export_json_format_emits_envelope(self, capsys):
|
|
687
796
|
"""CRITICAL fix: `-e` used to print bare prose and return under
|
|
688
797
|
`-f json`."""
|
dbqm-2.3.2/dbqm/_version.py
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "2.3.2"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|