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.
Files changed (106) hide show
  1. {dbqm-2.3.1 → dbqm-2.3.2}/PKG-INFO +2 -1
  2. dbqm-2.3.2/dbqm/_version.py +1 -0
  3. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/cli/commands/query.py +15 -4
  4. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/cli/envelope.py +2 -2
  5. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/cli/params.py +17 -2
  6. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/cli/render.py +2 -1
  7. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/core/audit.py +1 -1
  8. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/core/ddl_mysql.py +3 -1
  9. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/core/html_report.py +2 -2
  10. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/core/paths.py +1 -1
  11. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/core/read_only.py +9 -2
  12. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/core/template_engine.py +1 -1
  13. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/main.py +1 -1
  14. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/models/connection.py +3 -3
  15. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/models/settings.py +3 -2
  16. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/models/template.py +3 -3
  17. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/modals/column_maps.py +1 -1
  18. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/modals/export_picker.py +2 -2
  19. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/modals/param_input.py +1 -1
  20. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/screens/config_port.py +2 -1
  21. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/utils.py +1 -1
  22. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/widgets/sql_viewer.py +3 -1
  23. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm.egg-info/PKG-INFO +2 -1
  24. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm.egg-info/requires.txt +1 -0
  25. {dbqm-2.3.1 → dbqm-2.3.2}/pyproject.toml +62 -1
  26. dbqm-2.3.1/dbqm/_version.py +0 -1
  27. {dbqm-2.3.1 → dbqm-2.3.2}/PYPI.md +0 -0
  28. {dbqm-2.3.1 → dbqm-2.3.2}/README.md +0 -0
  29. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/__init__.py +0 -0
  30. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/__main__.py +0 -0
  31. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/cli/__init__.py +0 -0
  32. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/cli/commands/__init__.py +0 -0
  33. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/cli/commands/config_bundle.py +0 -0
  34. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/cli/commands/connection.py +0 -0
  35. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/cli/commands/inspect.py +0 -0
  36. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/cli/commands/schema.py +0 -0
  37. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/cli/deps.py +0 -0
  38. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/cli/errors.py +0 -0
  39. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/core/__init__.py +0 -0
  40. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/core/config_portability.py +0 -0
  41. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/core/connection_builder.py +0 -0
  42. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/core/crypto.py +0 -0
  43. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/core/db_manager.py +0 -0
  44. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/core/ddl_extractor.py +0 -0
  45. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/core/ddl_pg.py +0 -0
  46. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/core/exporter.py +0 -0
  47. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/core/group_engine.py +0 -0
  48. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/core/history.py +0 -0
  49. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/core/object_browser.py +0 -0
  50. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/core/oracle_client_installer.py +0 -0
  51. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/core/package_editor.py +0 -0
  52. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/core/query_engine.py +0 -0
  53. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/core/table_browser.py +0 -0
  54. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/design/__init__.py +0 -0
  55. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/design/tokens.py +0 -0
  56. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/models/__init__.py +0 -0
  57. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/models/group.py +0 -0
  58. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/models/query.py +0 -0
  59. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/__init__.py +0 -0
  60. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/app.py +0 -0
  61. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/modals/__init__.py +0 -0
  62. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/modals/confirm.py +0 -0
  63. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/modals/error.py +0 -0
  64. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/modals/export_dir_setup.py +0 -0
  65. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/modals/help.py +0 -0
  66. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/modals/oracle_client_dir.py +0 -0
  67. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/modals/text_input.py +0 -0
  68. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/screens/__init__.py +0 -0
  69. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/screens/adhoc.py +0 -0
  70. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/screens/browser.py +0 -0
  71. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/screens/connections.py +0 -0
  72. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/screens/exec_routine.py +0 -0
  73. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/screens/group_exec.py +0 -0
  74. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/screens/group_manage.py +0 -0
  75. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/screens/group_run.py +0 -0
  76. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/screens/history.py +0 -0
  77. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/screens/oracle_clients.py +0 -0
  78. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/screens/package_editor.py +0 -0
  79. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/screens/query_exec.py +0 -0
  80. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/screens/query_manage.py +0 -0
  81. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/screens/settings.py +0 -0
  82. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/screens/template_manage.py +0 -0
  83. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/screens/tools.py +0 -0
  84. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/theme.py +0 -0
  85. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/widgets/__init__.py +0 -0
  86. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/widgets/action_bar.py +0 -0
  87. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/widgets/dialog.py +0 -0
  88. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/widgets/empty_state.py +0 -0
  89. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/widgets/group_result.py +0 -0
  90. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/widgets/hierarchical_list.py +0 -0
  91. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/widgets/panel.py +0 -0
  92. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/widgets/progress.py +0 -0
  93. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/widgets/query_list.py +0 -0
  94. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/widgets/result_table.py +0 -0
  95. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/widgets/skeleton.py +0 -0
  96. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/widgets/status_bar.py +0 -0
  97. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/widgets/templates_sidebar.py +0 -0
  98. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm/ui/widgets/verdict.py +0 -0
  99. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm.egg-info/SOURCES.txt +0 -0
  100. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm.egg-info/dependency_links.txt +0 -0
  101. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm.egg-info/entry_points.txt +0 -0
  102. {dbqm-2.3.1 → dbqm-2.3.2}/dbqm.egg-info/top_level.txt +0 -0
  103. {dbqm-2.3.1 → dbqm-2.3.2}/setup.cfg +0 -0
  104. {dbqm-2.3.1 → dbqm-2.3.2}/tests/test_cli.py +0 -0
  105. {dbqm-2.3.1 → dbqm-2.3.2}/tests/test_cli_markup.py +0 -0
  106. {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.1
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
- result = deps.execute_adhoc(sql, conn, param_values, auto_commit=args.commit)
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 not isinstance(result, tuple) and result.sql_type in ("INSERT", "UPDATE", "DELETE"):
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 not isinstance(result, tuple) and result.sql_type == "DDL":
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 not isinstance(result, tuple) and result.sql_type == "PLSQL":
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
- direct = getattr(args, "password", None)
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
@@ -16,7 +16,7 @@ def log_execution(
16
16
  action: str,
17
17
  name: str,
18
18
  connection: str = "",
19
- params: dict | None = None,
19
+ params: dict[str, str] | None = None,
20
20
  row_count: int = 0,
21
21
  success: bool = True,
22
22
  error: str = "",
@@ -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([s for s in sqlparse.parse(sql) if s.token_first() is not None])
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
 
@@ -2,7 +2,7 @@
2
2
  import sys
3
3
 
4
4
 
5
- def main():
5
+ def main() -> None:
6
6
  # Version flag
7
7
  if len(sys.argv) == 2 and sys.argv[1] in ("--version", "-V"):
8
8
  from dbqm._version import __version__
@@ -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.
@@ -60,7 +60,7 @@ class ParamModal(ModalScreen[dict[str, str] | None]):
60
60
  def __init__(
61
61
  self,
62
62
  query_name: str,
63
- params: list[dict],
63
+ params: list[dict[str, str]],
64
64
  last_values: dict[str, str] | None = None,
65
65
  description: str = "",
66
66
  ) -> None:
@@ -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.1
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
@@ -12,6 +12,7 @@ pymssql>=2.2
12
12
  [dev]
13
13
  pytest>=7.0
14
14
  pytest-asyncio>=0.23
15
+ mypy>=1.11
15
16
 
16
17
  [oracle]
17
18
  oracledb>=2.0
@@ -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
@@ -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