dbqm 2.3.1__tar.gz → 2.3.2__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.1 → dbqm-2.3.2}/PKG-INFO +2 -1
- dbqm-2.3.2/dbqm/_version.py +1 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/cli/commands/query.py +15 -4
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/cli/envelope.py +2 -2
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/cli/params.py +17 -2
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/cli/render.py +2 -1
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/core/audit.py +1 -1
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/core/ddl_mysql.py +3 -1
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/core/html_report.py +2 -2
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/core/paths.py +1 -1
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/core/read_only.py +9 -2
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/core/template_engine.py +1 -1
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/main.py +1 -1
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/models/connection.py +3 -3
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/models/settings.py +3 -2
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/models/template.py +3 -3
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/modals/column_maps.py +1 -1
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/modals/export_picker.py +2 -2
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/modals/param_input.py +1 -1
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/screens/config_port.py +2 -1
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/utils.py +1 -1
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/widgets/sql_viewer.py +3 -1
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm.egg-info/PKG-INFO +2 -1
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm.egg-info/requires.txt +1 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/pyproject.toml +62 -1
- dbqm-2.3.1/dbqm/_version.py +0 -1
- {dbqm-2.3.1 → dbqm-2.3.2}/PYPI.md +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/README.md +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/__init__.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/__main__.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/cli/__init__.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/cli/commands/__init__.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/cli/commands/config_bundle.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/cli/commands/connection.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/cli/commands/inspect.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/cli/commands/schema.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/cli/deps.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/cli/errors.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/core/__init__.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/core/config_portability.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/core/connection_builder.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/core/crypto.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/core/db_manager.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/core/ddl_extractor.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/core/ddl_pg.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/core/exporter.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/core/group_engine.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/core/history.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/core/object_browser.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/core/oracle_client_installer.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/core/package_editor.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/core/query_engine.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/core/table_browser.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/design/__init__.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/design/tokens.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/models/__init__.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/models/group.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/models/query.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/__init__.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/app.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/modals/__init__.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/modals/confirm.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/modals/error.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/modals/export_dir_setup.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/modals/help.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/modals/oracle_client_dir.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/modals/text_input.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/screens/__init__.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/screens/adhoc.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/screens/browser.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/screens/connections.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/screens/exec_routine.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/screens/group_exec.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/screens/group_manage.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/screens/group_run.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/screens/history.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/screens/oracle_clients.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/screens/package_editor.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/screens/query_exec.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/screens/query_manage.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/screens/settings.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/screens/template_manage.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/screens/tools.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/theme.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/widgets/__init__.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/widgets/action_bar.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/widgets/dialog.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/widgets/empty_state.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/widgets/group_result.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/widgets/hierarchical_list.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/widgets/panel.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/widgets/progress.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/widgets/query_list.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/widgets/result_table.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/widgets/skeleton.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/widgets/status_bar.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/widgets/templates_sidebar.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/widgets/verdict.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm.egg-info/SOURCES.txt +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm.egg-info/dependency_links.txt +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm.egg-info/entry_points.txt +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/dbqm.egg-info/top_level.txt +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/setup.cfg +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/tests/test_cli.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/tests/test_cli_markup.py +0 -0
- {dbqm-2.3.1 → dbqm-2.3.2}/tests/test_cli_tema.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dbqm
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.2
|
|
4
4
|
Summary: Multi-database interactive query tool with fullscreen TUI
|
|
5
5
|
Author: Silvio Chagas
|
|
6
6
|
License-Expression: MIT
|
|
@@ -23,6 +23,7 @@ Requires-Dist: pymssql>=2.2; sys_platform != "win32" or platform_machine != "ARM
|
|
|
23
23
|
Provides-Extra: dev
|
|
24
24
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
25
25
|
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
26
|
+
Requires-Dist: mypy>=1.11; extra == "dev"
|
|
26
27
|
Provides-Extra: oracle
|
|
27
28
|
Requires-Dist: oracledb>=2.0; extra == "oracle"
|
|
28
29
|
Provides-Extra: postgres
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2.3.2"
|
|
@@ -321,12 +321,23 @@ def cmd_sql(args: argparse.Namespace) -> None:
|
|
|
321
321
|
_fail_or_print(args, "sql", "usage", "DML requer --commit para confirmar a operacao.")
|
|
322
322
|
|
|
323
323
|
try:
|
|
324
|
-
|
|
324
|
+
outcome = deps.execute_adhoc(sql, conn, param_values, auto_commit=args.commit)
|
|
325
325
|
except deps.ReadOnlyViolation as e:
|
|
326
326
|
_fail_or_print(args, "sql", "read_only", str(e))
|
|
327
327
|
|
|
328
|
+
# `execute_adhoc` only returns the `(result, connection)` tuple for a DML
|
|
329
|
+
# statement left uncommitted (`auto_commit=False`); the --commit guard
|
|
330
|
+
# above already exits before this call whenever `sql_type` is DML and
|
|
331
|
+
# `--commit` was not given, and no other `sql_type` ever produces that
|
|
332
|
+
# tuple. So this call always yields a plain `AdhocResult`.
|
|
333
|
+
if isinstance(outcome, tuple): # pragma: no cover - unreachable, see above
|
|
334
|
+
raise RuntimeError(
|
|
335
|
+
"execute_adhoc returned a manual-commit tuple despite auto_commit=True"
|
|
336
|
+
)
|
|
337
|
+
result = outcome
|
|
338
|
+
|
|
328
339
|
# For non-SELECT results (always AdhocResult with auto_commit=True at this point)
|
|
329
|
-
if
|
|
340
|
+
if result.sql_type in ("INSERT", "UPDATE", "DELETE"):
|
|
330
341
|
if not result.success:
|
|
331
342
|
_fail_or_print(args, "sql", _sql_error_code(result.error, result.error_kind),
|
|
332
343
|
result.error or "Erro ao executar SQL.")
|
|
@@ -337,7 +348,7 @@ def cmd_sql(args: argparse.Namespace) -> None:
|
|
|
337
348
|
return
|
|
338
349
|
|
|
339
350
|
# DDL results
|
|
340
|
-
if
|
|
351
|
+
if result.sql_type == "DDL":
|
|
341
352
|
if not result.success:
|
|
342
353
|
code = _sql_error_code(result.error, result.error_kind)
|
|
343
354
|
if args.format == "json":
|
|
@@ -352,7 +363,7 @@ def cmd_sql(args: argparse.Namespace) -> None:
|
|
|
352
363
|
return
|
|
353
364
|
|
|
354
365
|
# PL/SQL anonymous block results
|
|
355
|
-
if
|
|
366
|
+
if result.sql_type == "PLSQL":
|
|
356
367
|
if not result.success:
|
|
357
368
|
_fail_or_print(args, "sql", _sql_error_code(result.error, result.error_kind),
|
|
358
369
|
result.error or "Erro ao executar bloco.")
|
|
@@ -17,12 +17,12 @@ from __future__ import annotations
|
|
|
17
17
|
|
|
18
18
|
import json
|
|
19
19
|
import sys
|
|
20
|
-
from typing import Any, NoReturn
|
|
20
|
+
from typing import Any, NoReturn, TextIO
|
|
21
21
|
|
|
22
22
|
from dbqm.cli.errors import exit_for
|
|
23
23
|
|
|
24
24
|
|
|
25
|
-
def _write(stream, payload: dict) -> None:
|
|
25
|
+
def _write(stream: TextIO, payload: dict[str, Any]) -> None:
|
|
26
26
|
json.dump(payload, stream, indent=2, ensure_ascii=False, default=str)
|
|
27
27
|
stream.write("\n")
|
|
28
28
|
|
|
@@ -5,6 +5,7 @@ import argparse
|
|
|
5
5
|
import getpass
|
|
6
6
|
import os
|
|
7
7
|
import sys
|
|
8
|
+
from typing import Literal, overload
|
|
8
9
|
|
|
9
10
|
from rich.markup import escape
|
|
10
11
|
|
|
@@ -14,7 +15,7 @@ from dbqm.cli.render import console
|
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
def _parse_params(param_list: list[str] | None, args: argparse.Namespace | None = None,
|
|
17
|
-
command: str = "params") -> dict:
|
|
18
|
+
command: str = "params") -> dict[str, str]:
|
|
18
19
|
"""Parse key=value parameter pairs from CLI arguments.
|
|
19
20
|
|
|
20
21
|
`args`/`command` let a malformed `-p` speak the envelope under `-f json`
|
|
@@ -77,6 +78,16 @@ def _add_connection_fields(parser: argparse.ArgumentParser) -> None:
|
|
|
77
78
|
help="Formato de saida")
|
|
78
79
|
|
|
79
80
|
|
|
81
|
+
@overload
|
|
82
|
+
def resolve_password(
|
|
83
|
+
args: argparse.Namespace, env_var: str, prompt: str, *, required: Literal[True],
|
|
84
|
+
use_env: bool = True, command: str = "password",
|
|
85
|
+
) -> str: ...
|
|
86
|
+
@overload
|
|
87
|
+
def resolve_password(
|
|
88
|
+
args: argparse.Namespace, env_var: str, prompt: str, *, required: bool,
|
|
89
|
+
use_env: bool = True, command: str = "password",
|
|
90
|
+
) -> str | None: ...
|
|
80
91
|
def resolve_password(
|
|
81
92
|
args: argparse.Namespace, env_var: str, prompt: str, *, required: bool,
|
|
82
93
|
use_env: bool = True, command: str = "password",
|
|
@@ -110,7 +121,11 @@ def resolve_password(
|
|
|
110
121
|
`table`'s unchanged prose-and-`sys.exit` branch.
|
|
111
122
|
"""
|
|
112
123
|
from_stdin = getattr(args, "password_stdin", False)
|
|
113
|
-
|
|
124
|
+
# `args.password`, where the parser defines it, is a plain string
|
|
125
|
+
# argument (argparse's `--password`). `argparse.Namespace` is untyped
|
|
126
|
+
# so `getattr` returns `Any`; the annotation states what the value
|
|
127
|
+
# actually is instead of letting that `Any` leak into the return type.
|
|
128
|
+
direct: str | None = getattr(args, "password", None)
|
|
114
129
|
fmt = getattr(args, "format", "table")
|
|
115
130
|
|
|
116
131
|
if from_stdin and direct:
|
|
@@ -7,6 +7,7 @@ from rich.console import Console
|
|
|
7
7
|
from rich.table import Table
|
|
8
8
|
from rich.theme import Theme as _RichTheme
|
|
9
9
|
|
|
10
|
+
from dbqm.core.group_engine import ComparisonResult
|
|
10
11
|
from dbqm.design.tokens import DARK_TOKENS
|
|
11
12
|
|
|
12
13
|
|
|
@@ -68,7 +69,7 @@ def _print_query_result(result: Any, output_format: str = "table") -> None:
|
|
|
68
69
|
console.print(f"[dim]{result.row_count} registros em {result.elapsed:.2f}s[/dim]")
|
|
69
70
|
|
|
70
71
|
|
|
71
|
-
def _colored_comparison_lines(comparisons: list) -> list[str]:
|
|
72
|
+
def _colored_comparison_lines(comparisons: list[ComparisonResult]) -> list[str]:
|
|
72
73
|
"""Linhas de resumo do grupo, coloridas pelo eixo de veredito.
|
|
73
74
|
|
|
74
75
|
Reconstroi o texto a partir de `ComparisonResult` (contagens), em vez de
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
"""MySQL DDL extraction via SHOW CREATE statements."""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
4
6
|
from dbqm.core.ddl_extractor import ExtractionResult, ExtractedObject
|
|
5
7
|
|
|
6
8
|
|
|
7
|
-
def extract_mysql_ddl(db, object_name: str, result: ExtractionResult, on_progress=None):
|
|
9
|
+
def extract_mysql_ddl(db: Any, object_name: str, result: ExtractionResult, on_progress: Any = None) -> None:
|
|
8
10
|
"""Extract DDL for a MySQL object (table, view, procedure, function)."""
|
|
9
11
|
cursor = db.cursor()
|
|
10
12
|
name = object_name.strip()
|
|
@@ -29,7 +29,7 @@ def _light_theme_block(tokens: dict[str, str]) -> str:
|
|
|
29
29
|
return f"@media (prefers-color-scheme: light) {{\n :root {{\n{linhas}\n }}\n}}"
|
|
30
30
|
|
|
31
31
|
|
|
32
|
-
def export_group_html(group_result: GroupResult, params: dict | None = None) -> str:
|
|
32
|
+
def export_group_html(group_result: GroupResult, params: dict[str, str] | None = None) -> str:
|
|
33
33
|
"""Export group comparison as a standalone HTML file. Returns file path."""
|
|
34
34
|
query_names = list(group_result.query_results.keys())
|
|
35
35
|
filepath = _build_filepath("grupos", group_result.group_name, params=params, ext="html")
|
|
@@ -47,7 +47,7 @@ def _status_label(status: str) -> str:
|
|
|
47
47
|
return {"OK": "OK", "OK*": "OK*", "DIFF": "DIFERE", "ABSENT": "AUSENTE"}.get(status, status)
|
|
48
48
|
|
|
49
49
|
|
|
50
|
-
def _build_html(group_result: GroupResult, query_names: list[str], params: dict | None) -> str:
|
|
50
|
+
def _build_html(group_result: GroupResult, query_names: list[str], params: dict[str, str] | None) -> str:
|
|
51
51
|
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
52
52
|
overall = "CONSISTENTE" if group_result.all_match else "DIVERGENTE"
|
|
53
53
|
overall_class = "ok" if group_result.all_match else "diff"
|
|
@@ -24,7 +24,7 @@ SETTINGS_FILE = CONFIG_DIR / "settings.json"
|
|
|
24
24
|
TEMPLATES_DIR = DBQM_HOME / "templates"
|
|
25
25
|
TEMPLATES_FILE = TEMPLATES_DIR / "templates.json"
|
|
26
26
|
|
|
27
|
-
def ensure_dirs():
|
|
27
|
+
def ensure_dirs() -> None:
|
|
28
28
|
"""Create data directories if they don't exist."""
|
|
29
29
|
for d in [CONFIG_DIR, EXPORTS_DIR, HISTORY_DIR, TEMPLATES_DIR]:
|
|
30
30
|
d.mkdir(parents=True, exist_ok=True)
|
|
@@ -13,9 +13,13 @@ the generated statement, the careless paste.
|
|
|
13
13
|
from __future__ import annotations
|
|
14
14
|
|
|
15
15
|
import re
|
|
16
|
+
from typing import TYPE_CHECKING
|
|
16
17
|
|
|
17
18
|
import sqlparse
|
|
18
19
|
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from dbqm.models.connection import Connection
|
|
22
|
+
|
|
19
23
|
#: What a query-only connection may carry. `EXPLAIN` is here only as a
|
|
20
24
|
#: prefix — see `_explains_a_query`, which is what decides whether a given
|
|
21
25
|
#: EXPLAIN is actually read-only.
|
|
@@ -52,7 +56,10 @@ def statement_count(sql: str) -> int:
|
|
|
52
56
|
and neither is one inside a string literal -- `sqlparse` handles both,
|
|
53
57
|
which is why the count is not a `str.count(";")`.
|
|
54
58
|
"""
|
|
55
|
-
return len([
|
|
59
|
+
return len([
|
|
60
|
+
s for s in sqlparse.parse(sql)
|
|
61
|
+
if s.token_first() is not None # type: ignore[no-untyped-call] # sqlparse ships py.typed, but Statement.token_first() itself is unannotated upstream
|
|
62
|
+
])
|
|
56
63
|
|
|
57
64
|
|
|
58
65
|
def _explains_a_query(sql: str) -> bool:
|
|
@@ -76,7 +83,7 @@ def _explains_a_query(sql: str) -> bool:
|
|
|
76
83
|
return classify_sql(restante) == "SELECT"
|
|
77
84
|
|
|
78
85
|
|
|
79
|
-
def check_read_only(sql: str, conn) -> None:
|
|
86
|
+
def check_read_only(sql: str, conn: "Connection") -> None:
|
|
80
87
|
"""Raise `ReadOnlyViolation` if `conn` is read-only and `sql` could write.
|
|
81
88
|
|
|
82
89
|
Does nothing when the connection is writable, so callers can invoke it
|
|
@@ -127,7 +127,7 @@ def render_template(
|
|
|
127
127
|
values: dict[str, str],
|
|
128
128
|
) -> str:
|
|
129
129
|
"""Render template content by replacing {{field}} placeholders with values."""
|
|
130
|
-
def _replace(match: re.Match) -> str:
|
|
130
|
+
def _replace(match: re.Match[str]) -> str:
|
|
131
131
|
field_name = match.group(1)
|
|
132
132
|
return values.get(field_name, match.group(0))
|
|
133
133
|
|
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import json
|
|
4
4
|
from dataclasses import dataclass, field, asdict
|
|
5
5
|
from datetime import datetime
|
|
6
|
-
from typing import Optional
|
|
6
|
+
from typing import Any, Optional
|
|
7
7
|
|
|
8
8
|
from dbqm.core.paths import CONFIG_DIR, CONNECTIONS_FILE
|
|
9
9
|
|
|
@@ -31,12 +31,12 @@ class Connection:
|
|
|
31
31
|
read_only: bool = False
|
|
32
32
|
created_at: str = field(default_factory=lambda: datetime.now().isoformat(timespec="seconds"))
|
|
33
33
|
|
|
34
|
-
def to_dict(self) -> dict:
|
|
34
|
+
def to_dict(self) -> dict[str, Any]:
|
|
35
35
|
d = asdict(self)
|
|
36
36
|
return {k: v for k, v in d.items() if v is not None}
|
|
37
37
|
|
|
38
38
|
@classmethod
|
|
39
|
-
def from_dict(cls, data: dict) -> Connection:
|
|
39
|
+
def from_dict(cls, data: dict[str, Any]) -> Connection:
|
|
40
40
|
valid_fields = {f.name for f in cls.__dataclass_fields__.values()}
|
|
41
41
|
filtered = {k: v for k, v in data.items() if k in valid_fields}
|
|
42
42
|
return cls(**filtered)
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
|
|
4
4
|
import json
|
|
5
5
|
from dataclasses import dataclass, asdict
|
|
6
|
+
from typing import Any
|
|
6
7
|
|
|
7
8
|
from dbqm.core.paths import CONFIG_DIR, SETTINGS_FILE
|
|
8
9
|
|
|
@@ -20,11 +21,11 @@ class Settings:
|
|
|
20
21
|
# tool (e.g. PL/SQL Developer) cannot hijack the 64-bit client dbqm needs.
|
|
21
22
|
oracle_client_dir: str = ""
|
|
22
23
|
|
|
23
|
-
def to_dict(self) -> dict:
|
|
24
|
+
def to_dict(self) -> dict[str, Any]:
|
|
24
25
|
return asdict(self)
|
|
25
26
|
|
|
26
27
|
@classmethod
|
|
27
|
-
def from_dict(cls, data: dict) -> Settings:
|
|
28
|
+
def from_dict(cls, data: dict[str, Any]) -> Settings:
|
|
28
29
|
return cls(
|
|
29
30
|
audit_log_enabled=data.get("audit_log_enabled", False),
|
|
30
31
|
theme=data.get("theme", "plano-escuro"),
|
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
import json
|
|
5
5
|
from dataclasses import dataclass, field, asdict
|
|
6
6
|
from datetime import datetime
|
|
7
|
-
from typing import Optional
|
|
7
|
+
from typing import Any, Optional
|
|
8
8
|
|
|
9
9
|
from dbqm.core.paths import TEMPLATES_DIR, TEMPLATES_FILE
|
|
10
10
|
|
|
@@ -16,11 +16,11 @@ class Template:
|
|
|
16
16
|
content: str # Template text with {{field}} placeholders
|
|
17
17
|
created_at: str = field(default_factory=lambda: datetime.now().isoformat(timespec="seconds"))
|
|
18
18
|
|
|
19
|
-
def to_dict(self) -> dict:
|
|
19
|
+
def to_dict(self) -> dict[str, str]:
|
|
20
20
|
return asdict(self)
|
|
21
21
|
|
|
22
22
|
@classmethod
|
|
23
|
-
def from_dict(cls, data: dict) -> Template:
|
|
23
|
+
def from_dict(cls, data: dict[str, Any]) -> Template:
|
|
24
24
|
return cls(
|
|
25
25
|
name=data["name"],
|
|
26
26
|
description=data.get("description", ""),
|
|
@@ -10,7 +10,7 @@ from textual.widgets import Button, DataTable, Input, Select
|
|
|
10
10
|
from dbqm.ui.widgets.dialog import Dialog
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
class ColumnMapsModal(ModalScreen[dict | None]):
|
|
13
|
+
class ColumnMapsModal(ModalScreen[dict[str, dict[str, str]] | None]):
|
|
14
14
|
"""Configure column value mappings (DE-PARA).
|
|
15
15
|
|
|
16
16
|
Dismisses with updated maps dict on save, or None on cancel/ESC.
|
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
|
|
4
4
|
from typing import Callable
|
|
5
5
|
|
|
6
|
-
from textual.app import ComposeResult
|
|
6
|
+
from textual.app import App, ComposeResult
|
|
7
7
|
from textual.binding import Binding
|
|
8
8
|
from textual.screen import ModalScreen
|
|
9
9
|
from textual.widgets import Button
|
|
@@ -61,7 +61,7 @@ class ExportPickerModal(ModalScreen[str | None]):
|
|
|
61
61
|
|
|
62
62
|
|
|
63
63
|
def request_export(
|
|
64
|
-
app,
|
|
64
|
+
app: App[object],
|
|
65
65
|
callback: Callable[[str | None], None] | None = None,
|
|
66
66
|
) -> None:
|
|
67
67
|
"""Public entry point for the export flow.
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
4
|
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
5
6
|
|
|
6
7
|
from textual import work
|
|
7
8
|
from textual.app import ComposeResult
|
|
@@ -110,7 +111,7 @@ class ConfigPortScreen(Vertical):
|
|
|
110
111
|
}
|
|
111
112
|
"""
|
|
112
113
|
|
|
113
|
-
def __init__(self, initial_mode: str | None = None, **kwargs) -> None:
|
|
114
|
+
def __init__(self, initial_mode: str | None = None, **kwargs: Any) -> None:
|
|
114
115
|
super().__init__(**kwargs)
|
|
115
116
|
self._initial_mode = initial_mode
|
|
116
117
|
|
|
@@ -7,7 +7,7 @@ from textual.containers import VerticalScroll
|
|
|
7
7
|
from textual.widgets import Select
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
class NavSelect(Select):
|
|
10
|
+
class NavSelect(Select[str | None]):
|
|
11
11
|
"""Select widget that opens only with Enter/Space, not arrows.
|
|
12
12
|
|
|
13
13
|
This allows arrow keys to navigate between widgets in form screens
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"""Read-only SQL display widget with syntax highlighting."""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
4
6
|
from rich.syntax import Syntax
|
|
5
7
|
from textual.widgets import Static
|
|
6
8
|
|
|
@@ -27,7 +29,7 @@ class SqlViewer(Static):
|
|
|
27
29
|
}
|
|
28
30
|
"""
|
|
29
31
|
|
|
30
|
-
def __init__(self, sql: str = "", **kwargs) -> None:
|
|
32
|
+
def __init__(self, sql: str = "", **kwargs: Any) -> None:
|
|
31
33
|
super().__init__(**kwargs)
|
|
32
34
|
self._sql = sql
|
|
33
35
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dbqm
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.2
|
|
4
4
|
Summary: Multi-database interactive query tool with fullscreen TUI
|
|
5
5
|
Author: Silvio Chagas
|
|
6
6
|
License-Expression: MIT
|
|
@@ -23,6 +23,7 @@ Requires-Dist: pymssql>=2.2; sys_platform != "win32" or platform_machine != "ARM
|
|
|
23
23
|
Provides-Extra: dev
|
|
24
24
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
25
25
|
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
26
|
+
Requires-Dist: mypy>=1.11; extra == "dev"
|
|
26
27
|
Provides-Extra: oracle
|
|
27
28
|
Requires-Dist: oracledb>=2.0; extra == "oracle"
|
|
28
29
|
Provides-Extra: postgres
|
|
@@ -36,7 +36,7 @@ dependencies = [
|
|
|
36
36
|
]
|
|
37
37
|
|
|
38
38
|
[project.optional-dependencies]
|
|
39
|
-
dev = ["pytest>=7.0", "pytest-asyncio>=0.23"]
|
|
39
|
+
dev = ["pytest>=7.0", "pytest-asyncio>=0.23", "mypy>=1.11"]
|
|
40
40
|
oracle = ["oracledb>=2.0"]
|
|
41
41
|
postgres = ["psycopg[binary]>=3.1"]
|
|
42
42
|
sqlserver = ["pymssql>=2.2"]
|
|
@@ -87,3 +87,64 @@ select = [
|
|
|
87
87
|
# code to satisfy a number nobody chose.
|
|
88
88
|
# I 197 -- import order. First family to widen into; mostly automatic.
|
|
89
89
|
# F 180 -- 129 unused variables, 45 unused imports. Second to widen.
|
|
90
|
+
|
|
91
|
+
[tool.mypy]
|
|
92
|
+
python_version = "3.10" # `requires-python = ">=3.10"`
|
|
93
|
+
files = ["dbqm"] # so plain `uv run mypy` checks the package
|
|
94
|
+
strict = true
|
|
95
|
+
# A module NOT listed below is strict. That is the ratchet: every new module
|
|
96
|
+
# is strict from its first line, and the list only ever shrinks.
|
|
97
|
+
|
|
98
|
+
[[tool.mypy.overrides]]
|
|
99
|
+
# `pymysql` is the one dependency without `py.typed`. Named alone rather than
|
|
100
|
+
# globally: a blanket `ignore_missing_imports` would silently downgrade a
|
|
101
|
+
# future dependency to `Any`, which is exactly the mistake that made the first
|
|
102
|
+
# measurement of this work report 464 findings instead of 396.
|
|
103
|
+
module = ["pymysql.*"]
|
|
104
|
+
ignore_missing_imports = true
|
|
105
|
+
|
|
106
|
+
[[tool.mypy.overrides]]
|
|
107
|
+
# 36 modules not yet clean under `--strict`, measured 2026-09-13 (361 findings
|
|
108
|
+
# total). Generated from a real run, not transcribed:
|
|
109
|
+
# uv run mypy --strict dbqm/ 2>&1 | sed 's/:.*//' | sort -u
|
|
110
|
+
# Each entry carries its finding count as a debt register, not just a list of
|
|
111
|
+
# names. This list only shrinks -- see tests/design/test_typing_policy.py.
|
|
112
|
+
module = [
|
|
113
|
+
"dbqm.cli.commands.connection", # 3
|
|
114
|
+
"dbqm.cli.commands.inspect", # 17
|
|
115
|
+
"dbqm.cli.commands.schema", # 4
|
|
116
|
+
"dbqm.core.config_portability", # 11
|
|
117
|
+
"dbqm.core.connection_builder", # 5
|
|
118
|
+
"dbqm.core.db_manager", # 2
|
|
119
|
+
"dbqm.core.ddl_extractor", # 42
|
|
120
|
+
"dbqm.core.ddl_pg", # 9
|
|
121
|
+
"dbqm.core.exporter", # 17
|
|
122
|
+
"dbqm.core.group_engine", # 11
|
|
123
|
+
"dbqm.core.history", # 5
|
|
124
|
+
"dbqm.core.object_browser", # 22
|
|
125
|
+
"dbqm.core.package_editor", # 7
|
|
126
|
+
"dbqm.core.query_engine", # 18
|
|
127
|
+
"dbqm.core.table_browser", # 9
|
|
128
|
+
"dbqm.main", # 1
|
|
129
|
+
"dbqm.models.group", # 6
|
|
130
|
+
"dbqm.models.query", # 6
|
|
131
|
+
"dbqm.ui.app", # 20
|
|
132
|
+
"dbqm.ui.screens.adhoc", # 12
|
|
133
|
+
"dbqm.ui.screens.browser", # 20
|
|
134
|
+
"dbqm.ui.screens.connections", # 5
|
|
135
|
+
"dbqm.ui.screens.exec_routine", # 10
|
|
136
|
+
"dbqm.ui.screens.group_exec", # 4
|
|
137
|
+
"dbqm.ui.screens.group_manage", # 11
|
|
138
|
+
"dbqm.ui.screens.group_run", # 18
|
|
139
|
+
"dbqm.ui.screens.history", # 3
|
|
140
|
+
"dbqm.ui.screens.package_editor", # 10
|
|
141
|
+
"dbqm.ui.screens.query_exec", # 14
|
|
142
|
+
"dbqm.ui.screens.query_manage", # 6
|
|
143
|
+
"dbqm.ui.screens.settings", # 5
|
|
144
|
+
"dbqm.ui.screens.template_manage", # 4
|
|
145
|
+
"dbqm.ui.screens.tools", # 3
|
|
146
|
+
"dbqm.ui.widgets.group_result", # 14
|
|
147
|
+
"dbqm.ui.widgets.query_list", # 4
|
|
148
|
+
"dbqm.ui.widgets.result_table", # 3
|
|
149
|
+
]
|
|
150
|
+
ignore_errors = true
|
dbqm-2.3.1/dbqm/_version.py
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "2.3.1"
|
|
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
|