baucli 1.0.149__tar.gz → 1.0.151__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 (125) hide show
  1. {baucli-1.0.149 → baucli-1.0.151}/PKG-INFO +2 -1
  2. {baucli-1.0.149 → baucli-1.0.151}/baucli/__init__.py +1 -1
  3. {baucli-1.0.149 → baucli-1.0.151}/baucli/cli.py +27 -12
  4. baucli-1.0.151/baucli/db_check_pdf.py +342 -0
  5. {baucli-1.0.149 → baucli-1.0.151}/baucli/db_check_report.py +22 -5
  6. baucli-1.0.151/baucli/templates/logo-bau.jpg +0 -0
  7. {baucli-1.0.149 → baucli-1.0.151}/baucli.egg-info/PKG-INFO +2 -1
  8. {baucli-1.0.149 → baucli-1.0.151}/baucli.egg-info/SOURCES.txt +3 -0
  9. {baucli-1.0.149 → baucli-1.0.151}/baucli.egg-info/requires.txt +1 -0
  10. {baucli-1.0.149 → baucli-1.0.151}/setup.py +1 -0
  11. baucli-1.0.151/tests/test_db_check_pdf.py +45 -0
  12. {baucli-1.0.149 → baucli-1.0.151}/tests/test_db_check_report.py +2 -1
  13. {baucli-1.0.149 → baucli-1.0.151}/MANIFEST.in +0 -0
  14. {baucli-1.0.149 → baucli-1.0.151}/README.md +0 -0
  15. {baucli-1.0.149 → baucli-1.0.151}/baucli/apex_rebuild.py +0 -0
  16. {baucli-1.0.149 → baucli-1.0.151}/baucli/apexlang.py +0 -0
  17. {baucli-1.0.149 → baucli-1.0.151}/baucli/apexlang_profile.json +0 -0
  18. {baucli-1.0.149 → baucli-1.0.151}/baucli/apexlint.py +0 -0
  19. {baucli-1.0.149 → baucli-1.0.151}/baucli/api.py +0 -0
  20. {baucli-1.0.149 → baucli-1.0.151}/baucli/canon.py +0 -0
  21. {baucli-1.0.149 → baucli-1.0.151}/baucli/config.py +0 -0
  22. {baucli-1.0.149 → baucli-1.0.151}/baucli/db/__init__.py +0 -0
  23. {baucli-1.0.149 → baucli-1.0.151}/baucli/db/mssql.py +0 -0
  24. {baucli-1.0.149 → baucli-1.0.151}/baucli/db/mysql.py +0 -0
  25. {baucli-1.0.149 → baucli-1.0.151}/baucli/db/oracle.py +0 -0
  26. {baucli-1.0.149 → baucli-1.0.151}/baucli/db/postgresql.py +0 -0
  27. {baucli-1.0.149 → baucli-1.0.151}/baucli/democrm.py +0 -0
  28. {baucli-1.0.149 → baucli-1.0.151}/baucli/democrm_sql/mssql/crm_data_mssql.sql +0 -0
  29. {baucli-1.0.149 → baucli-1.0.151}/baucli/democrm_sql/mssql/crm_demo_mssql.sql +0 -0
  30. {baucli-1.0.149 → baucli-1.0.151}/baucli/democrm_sql/mysql/crm_data_mysql.sql +0 -0
  31. {baucli-1.0.149 → baucli-1.0.151}/baucli/democrm_sql/mysql/crm_demo_mysql.sql +0 -0
  32. {baucli-1.0.149 → baucli-1.0.151}/baucli/democrm_sql/oracle/crm_demo_oracle.sql +0 -0
  33. {baucli-1.0.149 → baucli-1.0.151}/baucli/democrm_sql/postgresql/crm_data_postgresql.sql +0 -0
  34. {baucli-1.0.149 → baucli-1.0.151}/baucli/democrm_sql/postgresql/crm_demo_postgresql.sql +0 -0
  35. {baucli-1.0.149 → baucli-1.0.151}/baucli/diffenvs.py +0 -0
  36. {baucli-1.0.149 → baucli-1.0.151}/baucli/docmig.py +0 -0
  37. {baucli-1.0.149 → baucli-1.0.151}/baucli/docmig_baseline.py +0 -0
  38. {baucli-1.0.149 → baucli-1.0.151}/baucli/docmig_build.py +0 -0
  39. {baucli-1.0.149 → baucli-1.0.151}/baucli/docmig_install.py +0 -0
  40. {baucli-1.0.149 → baucli-1.0.151}/baucli/docmig_shell.py +0 -0
  41. {baucli-1.0.149 → baucli-1.0.151}/baucli/docmig_spec_schema.py +0 -0
  42. {baucli-1.0.149 → baucli-1.0.151}/baucli/docmig_spec_tools.py +0 -0
  43. {baucli-1.0.149 → baucli-1.0.151}/baucli/docmig_spec_workspace.py +0 -0
  44. {baucli-1.0.149 → baucli-1.0.151}/baucli/git.py +0 -0
  45. {baucli-1.0.149 → baucli-1.0.151}/baucli/legacy/__init__.py +0 -0
  46. {baucli-1.0.149 → baucli-1.0.151}/baucli/legacy/delphi.py +0 -0
  47. {baucli-1.0.149 → baucli-1.0.151}/baucli/legacy/delphi_canon.py +0 -0
  48. {baucli-1.0.149 → baucli-1.0.151}/baucli/legacy/delphi_evidence.py +0 -0
  49. {baucli-1.0.149 → baucli-1.0.151}/baucli/legacy/magic.py +0 -0
  50. {baucli-1.0.149 → baucli-1.0.151}/baucli/mcp_server.py +0 -0
  51. {baucli-1.0.149 → baucli-1.0.151}/baucli/project.py +0 -0
  52. {baucli-1.0.149 → baucli-1.0.151}/baucli/rag_embed.py +0 -0
  53. {baucli-1.0.149 → baucli-1.0.151}/baucli/sample_scan.py +0 -0
  54. {baucli-1.0.149 → baucli-1.0.151}/baucli/scope_scan.py +0 -0
  55. {baucli-1.0.149 → baucli-1.0.151}/baucli/screenshot.py +0 -0
  56. {baucli-1.0.149 → baucli-1.0.151}/baucli/seed_check.py +0 -0
  57. {baucli-1.0.149 → baucli-1.0.151}/baucli/skills.py +0 -0
  58. {baucli-1.0.149 → baucli-1.0.151}/baucli/stress.py +0 -0
  59. {baucli-1.0.149 → baucli-1.0.151}/baucli/sync.py +0 -0
  60. {baucli-1.0.149 → baucli-1.0.151}/baucli/templates/app_shell/.apex/apexlang.json +0 -0
  61. {baucli-1.0.149 → baucli-1.0.151}/baucli/templates/app_shell/pages/login.apx.tpl +0 -0
  62. {baucli-1.0.149 → baucli-1.0.151}/baucli/templates/app_shell/shared-components/lists/navigation-bar.apx +0 -0
  63. {baucli-1.0.149 → baucli-1.0.151}/baucli/templates/app_shell/shared-components/themes/universal-theme/theme.apx +0 -0
  64. {baucli-1.0.149 → baucli-1.0.151}/baucli/uikit.py +0 -0
  65. {baucli-1.0.149 → baucli-1.0.151}/baucli/unwrap.py +0 -0
  66. {baucli-1.0.149 → baucli-1.0.151}/baucli.egg-info/dependency_links.txt +0 -0
  67. {baucli-1.0.149 → baucli-1.0.151}/baucli.egg-info/entry_points.txt +0 -0
  68. {baucli-1.0.149 → baucli-1.0.151}/baucli.egg-info/top_level.txt +0 -0
  69. {baucli-1.0.149 → baucli-1.0.151}/pyproject.toml +0 -0
  70. {baucli-1.0.149 → baucli-1.0.151}/setup.cfg +0 -0
  71. {baucli-1.0.149 → baucli-1.0.151}/tests/test_apexlint_binario.py +0 -0
  72. {baucli-1.0.149 → baucli-1.0.151}/tests/test_apexlint_dialogo.py +0 -0
  73. {baucli-1.0.149 → baucli-1.0.151}/tests/test_apexlint_dominio.py +0 -0
  74. {baucli-1.0.149 → baucli-1.0.151}/tests/test_api_request_db.py +0 -0
  75. {baucli-1.0.149 → baucli-1.0.151}/tests/test_canon_anchored_markers.py +0 -0
  76. {baucli-1.0.149 → baucli-1.0.151}/tests/test_canon_multiengine.py +0 -0
  77. {baucli-1.0.149 → baucli-1.0.151}/tests/test_db_tenant_guard.py +0 -0
  78. {baucli-1.0.149 → baucli-1.0.151}/tests/test_delphi_canon.py +0 -0
  79. {baucli-1.0.149 → baucli-1.0.151}/tests/test_delphi_datasnap.py +0 -0
  80. {baucli-1.0.149 → baucli-1.0.151}/tests/test_delphi_evidence.py +0 -0
  81. {baucli-1.0.149 → baucli-1.0.151}/tests/test_delphi_fmx.py +0 -0
  82. {baucli-1.0.149 → baucli-1.0.151}/tests/test_delta_clock.py +0 -0
  83. {baucli-1.0.149 → baucli-1.0.151}/tests/test_deploy_alias.py +0 -0
  84. {baucli-1.0.149 → baucli-1.0.151}/tests/test_docmig_baseline.py +0 -0
  85. {baucli-1.0.149 → baucli-1.0.151}/tests/test_docmig_build.py +0 -0
  86. {baucli-1.0.149 → baucli-1.0.151}/tests/test_docmig_build_extend.py +0 -0
  87. {baucli-1.0.149 → baucli-1.0.151}/tests/test_docmig_build_new.py +0 -0
  88. {baucli-1.0.149 → baucli-1.0.151}/tests/test_docmig_expressividade.py +0 -0
  89. {baucli-1.0.149 → baucli-1.0.151}/tests/test_docmig_install.py +0 -0
  90. {baucli-1.0.149 → baucli-1.0.151}/tests/test_docmig_install_extend.py +0 -0
  91. {baucli-1.0.149 → baucli-1.0.151}/tests/test_docmig_install_shell.py +0 -0
  92. {baucli-1.0.149 → baucli-1.0.151}/tests/test_docmig_project.py +0 -0
  93. {baucli-1.0.149 → baucli-1.0.151}/tests/test_docmig_project_new.py +0 -0
  94. {baucli-1.0.149 → baucli-1.0.151}/tests/test_docmig_shell.py +0 -0
  95. {baucli-1.0.149 → baucli-1.0.151}/tests/test_docmig_spec_analista.py +0 -0
  96. {baucli-1.0.149 → baucli-1.0.151}/tests/test_docmig_spec_schema.py +0 -0
  97. {baucli-1.0.149 → baucli-1.0.151}/tests/test_docmig_spec_schema_11.py +0 -0
  98. {baucli-1.0.149 → baucli-1.0.151}/tests/test_docmig_spec_schema_app.py +0 -0
  99. {baucli-1.0.149 → baucli-1.0.151}/tests/test_docmig_spec_use_baseline.py +0 -0
  100. {baucli-1.0.149 → baucli-1.0.151}/tests/test_docmig_sqlcl_padrao.py +0 -0
  101. {baucli-1.0.149 → baucli-1.0.151}/tests/test_docmig_support_objects.py +0 -0
  102. {baucli-1.0.149 → baucli-1.0.151}/tests/test_docrev_dismiss.py +0 -0
  103. {baucli-1.0.149 → baucli-1.0.151}/tests/test_doctab_split.py +0 -0
  104. {baucli-1.0.149 → baucli-1.0.151}/tests/test_documentation_list_db_label.py +0 -0
  105. {baucli-1.0.149 → baucli-1.0.151}/tests/test_mcp_doc_changeset.py +0 -0
  106. {baucli-1.0.149 → baucli-1.0.151}/tests/test_mcp_doc_tags.py +0 -0
  107. {baucli-1.0.149 → baucli-1.0.151}/tests/test_mcp_doctab_text.py +0 -0
  108. {baucli-1.0.149 → baucli-1.0.151}/tests/test_mcp_schema_context.py +0 -0
  109. {baucli-1.0.149 → baucli-1.0.151}/tests/test_packaging_deps.py +0 -0
  110. {baucli-1.0.149 → baucli-1.0.151}/tests/test_post_apply_uikit.py +0 -0
  111. {baucli-1.0.149 → baucli-1.0.151}/tests/test_profiles_add_prompt.py +0 -0
  112. {baucli-1.0.149 → baucli-1.0.151}/tests/test_project_guarda_chuva.py +0 -0
  113. {baucli-1.0.149 → baucli-1.0.151}/tests/test_scope_scan_call_wrapper.py +0 -0
  114. {baucli-1.0.149 → baucli-1.0.151}/tests/test_scope_scan_datasnap_reach.py +0 -0
  115. {baucli-1.0.149 → baucli-1.0.151}/tests/test_scope_scan_from_depth.py +0 -0
  116. {baucli-1.0.149 → baucli-1.0.151}/tests/test_scope_scan_owner.py +0 -0
  117. {baucli-1.0.149 → baucli-1.0.151}/tests/test_scope_scan_pkg_func_from.py +0 -0
  118. {baucli-1.0.149 → baucli-1.0.151}/tests/test_skills_apex_domain.py +0 -0
  119. {baucli-1.0.149 → baucli-1.0.151}/tests/test_skills_lessons.py +0 -0
  120. {baucli-1.0.149 → baucli-1.0.151}/tests/test_skills_size_budget.py +0 -0
  121. {baucli-1.0.149 → baucli-1.0.151}/tests/test_spec_sem_beg.py +0 -0
  122. {baucli-1.0.149 → baucli-1.0.151}/tests/test_sync_413.py +0 -0
  123. {baucli-1.0.149 → baucli-1.0.151}/tests/test_sync_charset.py +0 -0
  124. {baucli-1.0.149 → baucli-1.0.151}/tests/test_table_status.py +0 -0
  125. {baucli-1.0.149 → baucli-1.0.151}/tests/test_uikit_scope.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: baucli
3
- Version: 1.0.149
3
+ Version: 1.0.151
4
4
  Summary: Extracts verified business-logic and schema context from Oracle, PostgreSQL, MySQL and SQL Server
5
5
  Home-page: https://www.begcloud.com
6
6
  Author: BEG Support
@@ -27,6 +27,7 @@ Requires-Dist: mysql-connector-python>=8.0
27
27
  Requires-Dist: psycopg>=3.0
28
28
  Requires-Dist: python-tds>=1.12
29
29
  Requires-Dist: jsonschema>=4.18
30
+ Requires-Dist: reportlab>=4.0
30
31
  Requires-Dist: tomli>=1.1; python_version < "3.11"
31
32
  Requires-Dist: mcp<2,>=1.2
32
33
  Provides-Extra: keyring
@@ -3,6 +3,6 @@
3
3
  import warnings
4
4
  warnings.filterwarnings("ignore", message="urllib3 v2 only supports OpenSSL")
5
5
 
6
- __version__ = "1.0.149"
6
+ __version__ = "1.0.151"
7
7
  __product__ = "BAUCLI"
8
8
  __author__ = "BEG Support"
@@ -5788,15 +5788,17 @@ def db_profile(db_name, schema, source, tables_csv):
5788
5788
  @click.option("-v", "--verbose", "verbose", is_flag=True, default=False,
5789
5789
  help="Mostra o SQL de cada etapa junto do tempo — para levar a "
5790
5790
  "query lenta ao EXPLAIN PLAN.")
5791
- @click.option("--format", "fmt", type=click.Choice(["terminal", "json", "html"], case_sensitive=False),
5791
+ @click.option("--format", "fmt", type=click.Choice(["terminal", "json", "html", "pdf"], case_sensitive=False),
5792
5792
  default="terminal", show_default=True,
5793
5793
  help="terminal = tabelas na tela (como sempre); json = contrato versionado "
5794
5794
  "com TODOS os objetos (ignora --top), para CI/scripts; html = um "
5795
- "arquivo só, para enviar ao cliente ou imprimir em PDF pelo navegador.")
5795
+ "arquivo só, para enviar ao cliente; pdf = o relatório pronto para o "
5796
+ "cliente (capa, resumo, seções com os nomes inteiros).")
5796
5797
  @click.option("--output", "out_file", default=None,
5797
- help="Arquivo de saída para --format json|html (default: stdout).")
5798
+ help="Arquivo de saída para --format json|html (default: stdout) e pdf "
5799
+ "(default: prevoo_<SCHEMA>.pdf na pasta atual).")
5798
5800
  @click.option("--client", "client_name", default=None,
5799
- help="Nome do cliente no cabeçalho do HTML (opcional).")
5801
+ help="Nome do cliente no cabeçalho do HTML e na capa do PDF (opcional).")
5800
5802
  def db_check(db_name: str, schema: str, top: int, src_max: int,
5801
5803
  max_cols: int, strict: bool, verbose: bool,
5802
5804
  fmt: str = "terminal", out_file: str = None, client_name: str = None):
@@ -5855,7 +5857,14 @@ def db_check(db_name: str, schema: str, top: int, src_max: int,
5855
5857
  from baucli.db_check_report import empty_report, to_json, to_html
5856
5858
  from baucli import __version__ as _baucli_ver
5857
5859
  fmt = (fmt or "terminal").lower()
5858
- _structured = fmt in ("json", "html")
5860
+ _structured = fmt in ("json", "html", "pdf")
5861
+ if fmt == "pdf":
5862
+ # Falha ANTES de gastar minutos no banco se o reportlab não estiver aí.
5863
+ try:
5864
+ import reportlab # noqa: F401
5865
+ except ImportError:
5866
+ raise click.ClickException("`--format pdf` precisa do reportlab: pip install reportlab "
5867
+ "(instalação por pipx: pipx inject baucli reportlab)")
5859
5868
  report = empty_report()
5860
5869
  report["baucli_version"] = _baucli_ver
5861
5870
  if _structured:
@@ -6688,14 +6697,20 @@ def db_check(db_name: str, schema: str, top: int, src_max: int,
6688
6697
  "aviso": findings["AVISO"]}
6689
6698
  if _structured:
6690
6699
  console.quiet = False
6691
- payload = to_json(report) if fmt == "json" else to_html(report, client=client_name)
6692
- if out_file:
6693
- Path(out_file).write_text(payload, encoding="utf-8")
6694
- console.print(f"[green]{fmt} gravado em {out_file}[/green] "
6695
- f"[dim]({findings['CRÍTICO']} crítico, {findings['ATENÇÃO']} atenção, "
6696
- f"{findings['AVISO']} aviso)[/dim]")
6700
+ tally = (f"[dim]({findings['CRÍTICO']} crítico, {findings['ATENÇÃO']} atenção, "
6701
+ f"{findings['AVISO']} aviso)[/dim]")
6702
+ if fmt == "pdf":
6703
+ from baucli.db_check_pdf import to_pdf
6704
+ out_file = out_file or f"prevoo_{(report.get('schema') or schema or 'schema').upper()}.pdf"
6705
+ Path(out_file).write_bytes(to_pdf(report, client=client_name))
6706
+ console.print(f"[green]pdf gravado em {out_file}[/green] {tally}")
6697
6707
  else:
6698
- click.echo(payload)
6708
+ payload = to_json(report) if fmt == "json" else to_html(report, client=client_name)
6709
+ if out_file:
6710
+ Path(out_file).write_text(payload, encoding="utf-8")
6711
+ console.print(f"[green]{fmt} gravado em {out_file}[/green] {tally}")
6712
+ else:
6713
+ click.echo(payload)
6699
6714
 
6700
6715
  if strict and findings["CRÍTICO"]:
6701
6716
  raise SystemExit(2)
@@ -0,0 +1,342 @@
1
+ """PDF do `baucli db check` (pré-voo de IA) a partir do MESMO dict do JSON/HTML.
2
+
3
+ É o relatório que vai para o cliente antes da primeira carga: capa com a marca
4
+ BAU e os KPIs, resumo executivo, as seções do diagnóstico com os nomes
5
+ inteiros dos objetos e os próximos passos. A fonte é o dict de
6
+ `db_check_report.empty_report` (schema_version 1); não há parse de terminal.
7
+
8
+ Motor: reportlab (A4, Helvetica/Courier embutidas, sem recurso externo). O
9
+ import é tardio para que `db_check_report` continue leve para quem só quer
10
+ JSON/HTML; sem o reportlab instalado, `to_pdf` explica como instalar.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ from datetime import datetime
15
+ from io import BytesIO
16
+ from xml.sax.saxutils import escape as _xml_escape
17
+
18
+ from baucli.db_check_report import _LOGO_FILE, _n
19
+
20
+ _RED, _BLACK, _DARK = "#C8102E", "#0A0A0A", "#1A1A1A"
21
+ _GRAY, _LIGHT, _AMBER = "#6B7280", "#F3F4F6", "#B45309"
22
+
23
+
24
+ def _esc(v) -> str:
25
+ return _xml_escape("" if v is None else str(v))
26
+
27
+
28
+ def _when(iso: str | None) -> str:
29
+ """generated_at ISO (UTC) → 'dd/mm/aaaa hh:mm UTC'; devolve o texto cru se não parsear."""
30
+ if not iso:
31
+ return "—"
32
+ try:
33
+ return datetime.fromisoformat(iso).strftime("%d/%m/%Y %H:%M UTC")
34
+ except ValueError:
35
+ return str(iso)
36
+
37
+
38
+ def to_pdf(report: dict, client: str | None = None) -> bytes:
39
+ """Renderiza o pré-voo em PDF e devolve os bytes do arquivo."""
40
+ try:
41
+ from reportlab.lib.colors import HexColor, white
42
+ from reportlab.lib.enums import TA_CENTER, TA_JUSTIFY
43
+ from reportlab.lib.pagesizes import A4
44
+ from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
45
+ from reportlab.lib.units import cm
46
+ from reportlab.platypus import (Image, PageBreak, Paragraph, SimpleDocTemplate,
47
+ Spacer, Table, TableStyle)
48
+ except ImportError as e: # pragma: no cover - depende do ambiente
49
+ raise ImportError("`--format pdf` precisa do reportlab: pip install reportlab "
50
+ "(instalação por pipx: pipx inject baucli reportlab)") from e
51
+
52
+ W, H = A4
53
+ cw = W - 4 * cm
54
+ C_RED, C_BLACK, C_DARK = HexColor(_RED), HexColor(_BLACK), HexColor(_DARK)
55
+ C_GRAY, C_LIGHT = HexColor(_GRAY), HexColor(_LIGHT)
56
+
57
+ base = getSampleStyleSheet()
58
+ st = {}
59
+
60
+ def S(name, parent, **kw):
61
+ st[name] = ParagraphStyle(name, parent=base[parent], **kw)
62
+
63
+ S("CoverTitle", "Title", fontSize=28, leading=34, textColor=C_BLACK, alignment=TA_CENTER, spaceAfter=6)
64
+ S("CoverSub", "Normal", fontSize=15, leading=19, textColor=C_RED, alignment=TA_CENTER, spaceAfter=6)
65
+ S("CoverInfo", "Normal", fontSize=10.5, leading=14, textColor=C_GRAY, alignment=TA_CENTER)
66
+ S("H1", "Heading1", fontSize=17, leading=21, textColor=C_RED, spaceBefore=16, spaceAfter=6)
67
+ S("H2", "Heading2", fontSize=12.5, leading=16, textColor=C_BLACK, spaceBefore=10, spaceAfter=4)
68
+ S("Body", "Normal", fontSize=10, leading=14, textColor=C_DARK, alignment=TA_JUSTIFY, spaceAfter=6)
69
+ S("Bullet", "Normal", fontSize=10, leading=13, textColor=C_DARK, leftIndent=16, bulletIndent=5, spaceAfter=2)
70
+ S("Small", "Normal", fontSize=8, leading=10.5, textColor=C_GRAY, spaceAfter=4)
71
+ S("Cell", "Normal", fontSize=7.5, leading=9.5, textColor=C_DARK)
72
+ S("CellMono", "Normal", fontSize=7, leading=9, textColor=C_DARK, fontName="Courier")
73
+ S("KpiN", "Normal", fontSize=20, leading=24, textColor=C_RED, alignment=TA_CENTER, fontName="Helvetica-Bold")
74
+ S("KpiL", "Normal", fontSize=8, leading=10, textColor=C_GRAY, alignment=TA_CENTER)
75
+
76
+ def P(text, style="Body"):
77
+ return Paragraph(text, st[style])
78
+
79
+ def B(text):
80
+ return Paragraph(f"<bullet>&bull;</bullet> {text}", st["Bullet"])
81
+
82
+ def mono(v):
83
+ return f'<font face="Courier">{_esc(v)}</font>'
84
+
85
+ def lvl(level):
86
+ color = {"CRÍTICO": _RED, "ATENÇÃO": _AMBER}.get(level or "", _GRAY)
87
+ return f'<font color="{color}"><b>{_esc(level)}</b></font>'
88
+
89
+ def tbl(header, rows, widths, right=()):
90
+ """Tabela com cabeçalho preto repetido a cada página; células já em markup."""
91
+ data = [[Paragraph(f'<b><font color="white">{_esc(h)}</font></b>', st["Cell"]) for h in header]]
92
+ for r in rows:
93
+ data.append([Paragraph(c, st["Cell"]) for c in r])
94
+ t = Table(data, colWidths=widths, repeatRows=1)
95
+ style = [("BACKGROUND", (0, 0), (-1, 0), C_BLACK), ("TEXTCOLOR", (0, 0), (-1, 0), white),
96
+ ("VALIGN", (0, 0), (-1, -1), "TOP"), ("GRID", (0, 0), (-1, -1), 0.3, C_GRAY),
97
+ ("ROWBACKGROUNDS", (0, 1), (-1, -1), [white, C_LIGHT]),
98
+ ("TOPPADDING", (0, 0), (-1, -1), 2), ("BOTTOMPADDING", (0, 0), (-1, -1), 2),
99
+ ("LEFTPADDING", (0, 0), (-1, -1), 3), ("RIGHTPADDING", (0, 0), (-1, -1), 3)]
100
+ for j in right:
101
+ style.append(("ALIGN", (j, 1), (j, -1), "RIGHT"))
102
+ t.setStyle(TableStyle(style))
103
+ return t
104
+
105
+ def compact(items, per_row=3):
106
+ """Lista longa de nomes em N colunas (strings cruas, escapadas aqui)."""
107
+ items = list(items)
108
+ if not items:
109
+ return Spacer(1, 0)
110
+ rows = [items[i:i + per_row] for i in range(0, len(items), per_row)]
111
+ rows[-1] += [""] * (per_row - len(rows[-1]))
112
+ data = [[Paragraph(_esc(c), st["CellMono"]) for c in r] for r in rows]
113
+ t = Table(data, colWidths=[cw / per_row] * per_row)
114
+ t.setStyle(TableStyle([("VALIGN", (0, 0), (-1, -1), "TOP"), ("GRID", (0, 0), (-1, -1), 0.2, C_LIGHT),
115
+ ("ROWBACKGROUNDS", (0, 0), (-1, -1), [white, C_LIGHT]),
116
+ ("TOPPADDING", (0, 0), (-1, -1), 1), ("BOTTOMPADDING", (0, 0), (-1, -1), 1),
117
+ ("LEFTPADDING", (0, 0), (-1, -1), 3)]))
118
+ return t
119
+
120
+ def kpis(items):
121
+ cells = [[Paragraph(str(n), st["KpiN"]) for n, _ in items],
122
+ [Paragraph(label, st["KpiL"]) for _, label in items]]
123
+ t = Table(cells, colWidths=[cw / len(items)] * len(items))
124
+ t.setStyle(TableStyle([("BOX", (0, 0), (-1, -1), 0.5, C_GRAY), ("INNERGRID", (0, 0), (-1, -1), 0.3, C_LIGHT),
125
+ ("TOPPADDING", (0, 0), (-1, -1), 6), ("BOTTOMPADDING", (0, 0), (-1, -1), 6)]))
126
+ return t
127
+
128
+ def page_frame(canvas, doc):
129
+ canvas.saveState()
130
+ canvas.setFont("Helvetica", 7.5)
131
+ canvas.setFillColor(C_GRAY)
132
+ canvas.drawString(2 * cm, 1.2 * cm, "BAU - BEG Application Utilities · Pré-voo de documentação · uso restrito ao cliente")
133
+ canvas.drawRightString(W - 2 * cm, 1.2 * cm, f"página {doc.page}")
134
+ canvas.setStrokeColor(C_RED)
135
+ canvas.setLineWidth(0.8)
136
+ canvas.line(2 * cm, H - 1.5 * cm, W - 2 * cm, H - 1.5 * cm)
137
+ canvas.restoreState()
138
+
139
+ # ── dados ──
140
+ r = report
141
+ db = r.get("database") or {}
142
+ th = r.get("thresholds") or {}
143
+ s = r.get("summary") or {}
144
+ schema = r.get("schema") or "?"
145
+ pl = r.get("plsql") or {"total": 0, "flagged": []}
146
+ w = r.get("wide_tables") or {"items": []}
147
+ npk = r.get("no_pk") or {"items": []}
148
+ bv = r.get("big_views") or {"items": []}
149
+ inv = r.get("invalid") or {"broken": [], "stale": []}
150
+ nb = r.get("packages_no_body") or {"declaring": [], "spec_only": []}
151
+ orr = r.get("object_rules") or {}
152
+ enc = r.get("encoding") or {}
153
+ crit, aten, aviso = s.get("critico", 0), s.get("atencao", 0), s.get("aviso", 0)
154
+ title = f"Pré-voo de documentação - {schema}" + (f" - {client}" if client else "")
155
+
156
+ buf = BytesIO()
157
+ doc = SimpleDocTemplate(buf, pagesize=A4, leftMargin=2 * cm, rightMargin=2 * cm,
158
+ topMargin=2 * cm, bottomMargin=2 * cm, title=title, author="BEG Support",
159
+ subject="baucli db check")
160
+ out = []
161
+
162
+ # ── capa ──
163
+ out.append(Spacer(1, 2.2 * cm))
164
+ if _LOGO_FILE.exists():
165
+ out.append(Image(str(_LOGO_FILE), width=3.5 * cm, height=3.5 * cm, hAlign="CENTER"))
166
+ out.append(Spacer(1, 0.9 * cm))
167
+ out.append(P("Pré-voo de documentação", "CoverTitle"))
168
+ out.append(P(f"Schema {_esc(schema)}" + (f" - {_esc(client)}" if client else ""), "CoverSub"))
169
+ out.append(Spacer(1, 0.5 * cm))
170
+ out.append(P(f"Banco {_esc(db.get('alias'))} ({_esc(db.get('host'))}:{_esc(db.get('port'))}/{_esc(db.get('service'))}, "
171
+ f"usuário {_esc(db.get('user'))})", "CoverInfo"))
172
+ out.append(P(f"Diagnóstico do <b>baucli db check</b> {_esc(r.get('baucli_version') or '')} em {_esc(_when(r.get('generated_at')))}",
173
+ "CoverInfo"))
174
+ out.append(Spacer(1, 1.1 * cm))
175
+ out.append(kpis([(crit, "CRÍTICO"), (aten, "ATENÇÃO"), (aviso, "AVISO"),
176
+ (_n(pl.get("total")), "objetos PL/SQL"), (_n(npk.get("total")), "tabelas sem PK")]))
177
+ out.append(Spacer(1, 1.3 * cm))
178
+ out.append(P("O que este relatório é: a leitura, somente de catálogo e sem alterar nada nem ler dados da base, do que o "
179
+ "schema tem hoje e que influencia a documentação assistida por IA. Cada seção diz o que foi encontrado e "
180
+ "qual o efeito prático."))
181
+ out.append(P("BEG Support · www.begcloud.com", "CoverInfo"))
182
+ out.append(PageBreak())
183
+
184
+ # ── resumo executivo ──
185
+ out.append(P("Resumo executivo", "H1"))
186
+ out.append(P(f"O schema <b>{_esc(schema)}</b> tem <b>{_n(pl.get('total'))}</b> objetos PL/SQL. O pré-voo classificou os "
187
+ "achados em três níveis:"))
188
+ out.append(B(f"<b>{crit} CRÍTICO</b>: rotinas que não cabem numa chamada de IA (limite de {_n(th.get('source_max_chars'))} "
189
+ "caracteres) ou grandes demais para edição assistida com segurança. Precisam de fatiamento por subprograma, "
190
+ "refatoração ou provedor com janela maior."))
191
+ out.append(B(f"<b>{aten} ATENÇÃO</b>: documenta normalmente, mas o resultado deve ser revisado com cuidado (tabelas largas, "
192
+ "tabelas sem chave primária, objetos inválidos com erro de compilação, packages sem corpo)."))
193
+ out.append(B(f"<b>{aviso} AVISO</b>: só registro; sem impedimento."))
194
+ out.append(Spacer(1, 0.2 * cm))
195
+ out.append(P(f"Limiares aplicados: {_n(th.get('source_max_chars'))} caracteres por chamada de IA · tabela até "
196
+ f"{_esc(th.get('max_columns'))} colunas no prompt · corpo de package com {_n(th.get('lines_unsafe'))} linhas ou "
197
+ f"mais é edição insegura · fonte dos limiares: {_esc(th.get('source'))}.", "Small"))
198
+ if not any((crit, aten, aviso)):
199
+ out.append(P("<b>Nada sinalizado</b>: o schema está dentro dos limites de IA."))
200
+
201
+ # ── 1. PL/SQL ──
202
+ out.append(P("1. Rotinas PL/SQL sinalizadas", "H1"))
203
+ out.append(P(f"{_n(pl.get('total'))} objetos no schema, <b>{len(pl['flagged'])}</b> sinalizados."))
204
+ if pl["flagged"]:
205
+ out.append(tbl(["Nível", "Tipo", "Objeto", "Linhas", "KB", "~tokens", "Por quê"],
206
+ [[lvl(f.get("level")), _esc(f.get("type")), mono(f.get("name")), _n(f.get("lines")),
207
+ _n(round((f.get("chars") or 0) / 1024)), _n(f.get("tokens")), _esc(f.get("reason"))]
208
+ for f in pl["flagged"]],
209
+ [1.6 * cm, 2.0 * cm, 5.0 * cm, 1.4 * cm, 1.2 * cm, 1.8 * cm, cw - 13.0 * cm], right=(3, 4, 5)))
210
+
211
+ # ── 2. tabelas largas ──
212
+ out.append(P("2. Tabelas largas", "H1"))
213
+ out.append(P(f"<b>{len(w['items'])}</b> tabelas com mais de {_esc(w.get('max_columns'))} colunas: a IA documenta coluna a "
214
+ "coluna (fan-out), multiplicando chamadas e revisão. Tabelas com centenas de colunas costumam ser planilhas "
215
+ "importadas ou estruturas desnormalizadas; vale confirmar se interessam ao catálogo."))
216
+ if w["items"]:
217
+ out.append(P("Formato: TABELA (colunas; LOBs quando houver), em ordem decrescente de colunas.", "Small"))
218
+ out.append(compact([f"{i['table']} ({i['columns']}{'; ' + str(i['lobs']) + ' LOB' if i.get('lobs') else ''})"
219
+ for i in w["items"]]))
220
+
221
+ # ── 3. sem PK ──
222
+ out.append(P("3. Tabelas sem chave primária", "H1"))
223
+ out.append(P(f"<b>{_n(npk.get('total'))}</b> tabelas sem PK: {_n(npk.get('no_identity'))} sem nenhuma identidade de linha e "
224
+ f"{_n(npk.get('with_unique'))} com chave ou índice único (caso leve). Sem chave, formulários e relatórios "
225
+ "gerados não têm como apontar o registro e a ordenação de colunas perde o primeiro critério."))
226
+ com = [i for i in npk["items"] if i.get("has_identity")]
227
+ sem = [i for i in npk["items"] if not i.get("has_identity")]
228
+
229
+ def _pk(i):
230
+ rows_est = _n(i["rows_est"]) if i.get("rows_est") is not None else "—"
231
+ return f"{i['table']} ({i['columns']} col, {rows_est} lin)"
232
+
233
+ if com:
234
+ out.append(P(f"Com chave ou índice único ({len(com)}), caso leve:", "H2"))
235
+ out.append(compact([_pk(i) for i in com]))
236
+ if sem:
237
+ out.append(P(f"Sem identidade de linha ({len(sem)}). Formato: TABELA (colunas, linhas estimadas):", "H2"))
238
+ out.append(compact([_pk(i) for i in sem]))
239
+
240
+ # ── 4. views ──
241
+ out.append(P("4. Views grandes", "H1"))
242
+ out.append(P(f"{len(bv['items'])} acima de {_n(bv.get('limit_chars'))} caracteres."))
243
+ if bv["items"]:
244
+ out.append(tbl(["View", "Chars", "~tokens"],
245
+ [[mono(i.get("view")), _n(i.get("chars")), _n(i.get("tokens"))] for i in bv["items"]],
246
+ [9 * cm, 3 * cm, 3 * cm], right=(1, 2)))
247
+
248
+ # ── 5. inválidos ──
249
+ out.append(P("5. Objetos inválidos", "H1"))
250
+ out.append(P(f"<b>{_n(inv.get('total'))}</b> objetos INVALID: {len(inv['broken'])} com erro de compilação e "
251
+ f"{len(inv['stale'])} invalidados em cascata. O sync extrai só objetos VALID: tudo o que está aqui fica fora "
252
+ "da documentação até ser resolvido."))
253
+ if inv["broken"]:
254
+ out.append(P("Com erro de compilação", "H2"))
255
+ out.append(tbl(["Tipo", "Objeto", "DDL", "1º erro"],
256
+ [[_esc(i.get("type")), mono(i.get("name")), _esc(i.get("last_ddl")), _esc(i.get("first_error"))]
257
+ for i in inv["broken"]],
258
+ [2.4 * cm, 5.6 * cm, 2.4 * cm, cw - 10.4 * cm]))
259
+ if inv["stale"]:
260
+ out.append(P(f"Só recompilar ({len(inv['stale'])})", "H2"))
261
+ out.append(compact([i.get("name") for i in inv["stale"]]))
262
+ out.append(P(f"Resolve com {mono(f'EXEC DBMS_UTILITY.COMPILE_SCHEMA({chr(39)}{schema}{chr(39)}, FALSE)')}.", "Small"))
263
+
264
+ # ── 6. packages sem corpo ──
265
+ out.append(P("6. Packages sem corpo", "H1"))
266
+ out.append(P(f"{_n(nb.get('total'))} packages só com especificação: {len(nb['declaring'])} declaram subprogramas sem "
267
+ "corpo (falham em runtime; a documentação por subprograma fica sem fonte) e "
268
+ f"{len(nb['spec_only'])} são só constantes e tipos (legítimo)."))
269
+ if nb["declaring"]:
270
+ out.append(tbl(["Package", "Subprogramas", "Spec", "DDL"],
271
+ [[mono(i.get("package")), _n(i.get("subprograms")), _esc(i.get("spec_status")), _esc(i.get("last_ddl"))]
272
+ for i in nb["declaring"]],
273
+ [7 * cm, 2.6 * cm, 2 * cm, cw - 11.6 * cm], right=(1,)))
274
+ if nb["spec_only"]:
275
+ out.append(P("Spec-only: " + ", ".join(_esc(x) for x in nb["spec_only"]), "Small"))
276
+
277
+ # ── 7. regras ──
278
+ out.append(P("7. Objetos fora da cobertura (regras de nome)", "H1"))
279
+ if orr.get("skipped"):
280
+ out.append(P(f"Seção pulada: {_esc(orr['skipped'])}.", "Small"))
281
+ else:
282
+ out.append(P(f"{_esc(orr.get('rules'))} regras de nome classificam objetos como não produtivos. "
283
+ f"<b>{_n(orr.get('excluded'))}</b> de {_n(orr.get('total_objects'))} objetos saem da cobertura. "
284
+ "Confirme se alguma classe está errada para o seu padrão de nomes; a lista é ajustável por cliente."))
285
+ for kind, names in sorted((orr.get("by_kind") or {}).items()):
286
+ out.append(P(f"{_esc(kind)} ({len(names)})", "H2"))
287
+ out.append(compact(names))
288
+
289
+ # ── 8. codificação ──
290
+ out.append(P("8. Codificação e acentuação", "H1"))
291
+ if not enc.get("available", True):
292
+ out.append(P("Checagem indisponível.", "Small"))
293
+ else:
294
+ out.append(P(f"Charset do banco: <b>{_esc(enc.get('charset'))}</b>"
295
+ + (" (UTF-8)." if enc.get("is_utf8") else ". Não é UTF-8: o BAU emite anotações sem acento neste banco; "
296
+ "COMMENT ON não sofre dessa limitação.")))
297
+ src = enc.get("source_objects") or []
298
+ cmt = enc.get("comments") or []
299
+ if src:
300
+ out.append(P(f"{len(src)} objeto(s) PL/SQL com texto UTF-8 gravado como single-byte:"))
301
+ out.append(tbl(["Objeto", "Tipo", "Linhas"],
302
+ [[mono(i.get("name")), _esc(i.get("type")), _n(i.get("lines"))] for i in src],
303
+ [7 * cm, 3 * cm, 2 * cm], right=(2,)))
304
+ if cmt:
305
+ out.append(P(f"{len(cmt)} comentário(s) com sinal de acentuação quebrada:"))
306
+ out.append(tbl(["Onde", "Tabela", "Coluna", "Sinal", "Trecho"],
307
+ [[_esc(i.get("where")), mono(i.get("table")), mono(i.get("column")), _esc(i.get("signals")),
308
+ _esc((i.get("text") or "")[:90])] for i in cmt],
309
+ [1.6 * cm, 4.6 * cm, 4.2 * cm, 2.2 * cm, cw - 12.6 * cm]))
310
+ if src or cmt:
311
+ out.append(P("Estes problemas estão no banco de origem agora, antes de o BAU escrever qualquer coisa; o sync os "
312
+ "registra como pré-existentes. A documentação gerada pela IA, aplicada de volta, os corrige.", "Small"))
313
+ elif enc.get("is_utf8"):
314
+ out.append(P("Nenhum sinal de acentuação quebrada."))
315
+
316
+ # ── 9. próximos passos ──
317
+ out.append(P("9. Próximos passos", "H1"))
318
+ for t in (
319
+ "Corrigir ou descartar os objetos inválidos com erro de compilação (seção 5); recompilar os invalidados em cascata.",
320
+ "Decidir o destino dos packages sem corpo que declaram subprogramas (seção 6): implementar, remover ou "
321
+ "classificar como legado.",
322
+ "Confirmar as classes de objetos fora da cobertura (seção 7) e o padrão de nomes de temporários e backups.",
323
+ "Carga do catálogo pelo BAU (sem IA) e documentação assistida por fases, começando por um módulo piloto, com "
324
+ "revisão humana por responsável de área.",
325
+ "Tratar as tabelas sem chave primária como achados de modelagem, por módulo, na auditoria do BAU.",
326
+ ):
327
+ out.append(B(t))
328
+
329
+ # ── tempos e avisos ──
330
+ timings = r.get("timings") or []
331
+ if timings:
332
+ tot = sum(t.get("seconds", 0) for t in timings)
333
+ out.append(Spacer(1, 0.3 * cm))
334
+ out.append(P(f"Tempo do diagnóstico: {tot:.1f}s em {len(timings)} etapas. Fonte: baucli db check (leitura de "
335
+ "catálogo; nenhum dado da base foi lido).", "Small"))
336
+ if r.get("errors"):
337
+ out.append(P("Avisos", "H2"))
338
+ for x in r["errors"]:
339
+ out.append(B(_esc(x)))
340
+
341
+ doc.build(out, onFirstPage=page_frame, onLaterPages=page_frame)
342
+ return buf.getvalue()
@@ -3,8 +3,9 @@
3
3
  O comando sempre montou o diagnóstico em memória e o despejava em tabelas do
4
4
  rich. Este módulo recebe o MESMO diagnóstico como dict (ver `empty_report`)
5
5
  e o devolve em JSON (para CI, scripts, outro programa) ou HTML (um arquivo
6
- só, sem dependência externa, que o cliente abre no navegador e imprime em
7
- PDF). Não há parse de terminal em lugar nenhum: a fonte é o dict.
6
+ só, sem dependência externa, que o cliente abre no navegador). O PDF pronto
7
+ para o cliente sai do mesmo dict em `db_check_pdf.to_pdf`. Não há parse de
8
+ terminal em lugar nenhum: a fonte é o dict.
8
9
 
9
10
  Contrato do dict (schema_version 1): chaves fixas, listas sempre presentes
10
11
  (vazias quando nada foi encontrado), números como int, nomes de objeto como
@@ -12,12 +13,25 @@ vieram do dicionário Oracle.
12
13
  """
13
14
  from __future__ import annotations
14
15
 
16
+ import base64
15
17
  import html as _html
16
18
  import json
17
19
  from datetime import datetime, timezone
20
+ from pathlib import Path
18
21
 
19
22
  SCHEMA_VERSION = 1
20
23
 
24
+ # Logotipo BAU (quadrado preto) embutido como data: URI: o HTML continua um
25
+ # arquivo só, sem recurso externo, e abre com a marca em qualquer lugar.
26
+ _LOGO_FILE = Path(__file__).parent / "templates" / "logo-bau.jpg"
27
+
28
+
29
+ def _logo_data_uri() -> str | None:
30
+ try:
31
+ return "data:image/jpeg;base64," + base64.b64encode(_LOGO_FILE.read_bytes()).decode("ascii")
32
+ except OSError:
33
+ return None
34
+
21
35
 
22
36
  def empty_report() -> dict:
23
37
  return {
@@ -52,7 +66,8 @@ _CSS = """
52
66
  :root{--red:#C8102E;--ink:#111;--muted:#6B7280;--line:#E5E7EB;--soft:#F7F7F8}
53
67
  *{box-sizing:border-box}body{font-family:-apple-system,Segoe UI,Helvetica,Arial,sans-serif;
54
68
  color:var(--ink);margin:0;background:#fff}main{max-width:1100px;margin:0 auto;padding:32px 24px}
55
- header{border-bottom:3px solid var(--red);padding-bottom:12px;margin-bottom:20px}
69
+ header{border-bottom:3px solid var(--red);padding-bottom:12px;margin-bottom:20px;display:flex;gap:16px;align-items:center}
70
+ header img.logo{width:64px;height:64px;border-radius:8px;flex:none}
56
71
  h1{font-size:26px;margin:0 0 4px}h2{font-size:18px;color:var(--red);margin:32px 0 8px;
57
72
  border-left:4px solid var(--red);padding-left:10px}h3{font-size:14px;margin:18px 0 6px}
58
73
  .sub{color:var(--muted);font-size:14px}.kpis{display:grid;grid-template-columns:repeat(5,1fr);
@@ -113,9 +128,11 @@ def to_html(report: dict, client: str | None = None) -> str:
113
128
  out = [f"<!doctype html><html lang='pt-BR'><head><meta charset='utf-8'>"
114
129
  f"<meta name='viewport' content='width=device-width,initial-scale=1'>"
115
130
  f"<title>{title}</title><style>{_CSS}</style></head><body><main>"]
116
- out.append(f"<header><h1>{title}</h1><div class='sub'>Banco {_e(db.get('alias'))} "
131
+ logo = _logo_data_uri()
132
+ out.append("<header>" + (f"<img class='logo' src='{logo}' alt='BAU'>" if logo else "")
133
+ + f"<div><h1>{title}</h1><div class='sub'>Banco {_e(db.get('alias'))} "
117
134
  f"({_e(db.get('host'))}:{_e(db.get('port'))}/{_e(db.get('service'))}, usuário {_e(db.get('user'))}) · "
118
- f"gerado em {_e(r.get('generated_at'))} · baucli {_e(r.get('baucli_version'))}</div></header>")
135
+ f"gerado em {_e(r.get('generated_at'))} · baucli {_e(r.get('baucli_version'))}</div></div></header>")
119
136
  out.append("<div class='kpis'>"
120
137
  f"<div class='kpi'><b>{s.get('critico', 0)}</b><span>crítico</span></div>"
121
138
  f"<div class='kpi'><b>{s.get('atencao', 0)}</b><span>atenção</span></div>"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: baucli
3
- Version: 1.0.149
3
+ Version: 1.0.151
4
4
  Summary: Extracts verified business-logic and schema context from Oracle, PostgreSQL, MySQL and SQL Server
5
5
  Home-page: https://www.begcloud.com
6
6
  Author: BEG Support
@@ -27,6 +27,7 @@ Requires-Dist: mysql-connector-python>=8.0
27
27
  Requires-Dist: psycopg>=3.0
28
28
  Requires-Dist: python-tds>=1.12
29
29
  Requires-Dist: jsonschema>=4.18
30
+ Requires-Dist: reportlab>=4.0
30
31
  Requires-Dist: tomli>=1.1; python_version < "3.11"
31
32
  Requires-Dist: mcp<2,>=1.2
32
33
  Provides-Extra: keyring
@@ -11,6 +11,7 @@ baucli/api.py
11
11
  baucli/canon.py
12
12
  baucli/cli.py
13
13
  baucli/config.py
14
+ baucli/db_check_pdf.py
14
15
  baucli/db_check_report.py
15
16
  baucli/democrm.py
16
17
  baucli/diffenvs.py
@@ -58,6 +59,7 @@ baucli/legacy/delphi.py
58
59
  baucli/legacy/delphi_canon.py
59
60
  baucli/legacy/delphi_evidence.py
60
61
  baucli/legacy/magic.py
62
+ baucli/templates/logo-bau.jpg
61
63
  baucli/templates/app_shell/.apex/apexlang.json
62
64
  baucli/templates/app_shell/pages/login.apx.tpl
63
65
  baucli/templates/app_shell/shared-components/lists/navigation-bar.apx
@@ -68,6 +70,7 @@ tests/test_apexlint_dominio.py
68
70
  tests/test_api_request_db.py
69
71
  tests/test_canon_anchored_markers.py
70
72
  tests/test_canon_multiengine.py
73
+ tests/test_db_check_pdf.py
71
74
  tests/test_db_check_report.py
72
75
  tests/test_db_tenant_guard.py
73
76
  tests/test_delphi_canon.py
@@ -6,6 +6,7 @@ mysql-connector-python>=8.0
6
6
  psycopg>=3.0
7
7
  python-tds>=1.12
8
8
  jsonschema>=4.18
9
+ reportlab>=4.0
9
10
  mcp<2,>=1.2
10
11
 
11
12
  [:python_version < "3.11"]
@@ -42,6 +42,7 @@ setup(
42
42
  "psycopg>=3.0", # PostgreSQL v3 (pure Python)
43
43
  "python-tds>=1.12", # SQL Server (pure Python, replaces pymssql)
44
44
  "jsonschema>=4.18", # docmig-spec/1.0: forma das secoes da spec validada antes de gravar
45
+ "reportlab>=4.0", # `db check --format pdf`: pré-voo em PDF para o cliente (wheels para todas as plataformas)
45
46
  # bau.toml: tomllib so existe a partir do 3.11; sem tomli o baucli e o MCP
46
47
  # caem com "no TOML parser available" em qualquer projeto com bau.toml.
47
48
  'tomli>=1.1; python_version < "3.11"',
@@ -0,0 +1,45 @@
1
+ """db check --format pdf: o dict do pré-voo vira PDF sem banco."""
2
+ import re
3
+
4
+ import pytest
5
+
6
+ from baucli.db_check_pdf import to_pdf
7
+ from baucli.db_check_report import empty_report
8
+ from test_db_check_report import _sample
9
+
10
+ pytest.importorskip("reportlab")
11
+
12
+
13
+ def _pages(pdf: bytes) -> int:
14
+ # Os dicionários de página não são comprimidos: contar /Type /Page (sem o "s" de /Pages).
15
+ return len(re.findall(rb"/Type\s*/Page[^s]", pdf))
16
+
17
+
18
+ def test_pdf_is_valid_and_has_pages():
19
+ pdf = to_pdf(_sample(), client="Ziz Performance")
20
+ assert pdf.startswith(b"%PDF-")
21
+ assert pdf.rstrip().endswith(b"%%EOF")
22
+ assert _pages(pdf) >= 2 # capa + corpo
23
+ assert len(pdf) > 10_000 # logotipo embutido + texto
24
+
25
+
26
+ def test_pdf_metadata_and_logo():
27
+ pdf = to_pdf(_sample(), client="Ziz Performance")
28
+ assert b"BEG Support" in pdf # /Author
29
+ assert b"/DCTDecode" in pdf # o JPEG do logotipo foi embutido
30
+
31
+
32
+ def test_empty_report_renders():
33
+ pdf = to_pdf(empty_report())
34
+ assert pdf.startswith(b"%PDF-") and _pages(pdf) >= 2
35
+
36
+
37
+ def test_text_content_when_pypdf_available():
38
+ pypdf = pytest.importorskip("pypdf")
39
+ from io import BytesIO
40
+ reader = pypdf.PdfReader(BytesIO(to_pdf(_sample(), client="Ziz Performance")))
41
+ text = "\n".join(p.extract_text() for p in reader.pages)
42
+ for name in ("NAC_PCK_MKT_CAMPANHA", "NAC_RH_CST_GESTAX", "AD_ORG_BKP", "BEG_FNC_RETORNA_IPI",
43
+ "NAC_PRC_B2B_REC_ITENS", "PCK_IMPORTACAO_MULTINAC", "PKG_URLS", "NAC_TMP", "FNC_LIMPA_STR",
44
+ "BEG_CPG_RENEG", "Ziz Performance", "WE8ISO8859P1"):
45
+ assert name in text, name
@@ -68,5 +68,6 @@ def test_html_lists_full_names_and_escapes():
68
68
  assert "WE8ISO8859P1" in html
69
69
  # o "<ambígua>" do erro Oracle vira entidade, não tag
70
70
  assert "&lt;ambígua&gt;" in html and "<ambígua>" not in html
71
- # sem recurso externo: um arquivo só
71
+ # sem recurso externo: um arquivo só — o logotipo vai embutido como data: URI
72
72
  assert "http" not in html.split("<footer>")[0].replace("http-equiv", "")
73
+ assert "<img class='logo' src='data:image/jpeg;base64," in html
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