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.
- {baucli-1.0.149 → baucli-1.0.151}/PKG-INFO +2 -1
- {baucli-1.0.149 → baucli-1.0.151}/baucli/__init__.py +1 -1
- {baucli-1.0.149 → baucli-1.0.151}/baucli/cli.py +27 -12
- baucli-1.0.151/baucli/db_check_pdf.py +342 -0
- {baucli-1.0.149 → baucli-1.0.151}/baucli/db_check_report.py +22 -5
- baucli-1.0.151/baucli/templates/logo-bau.jpg +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/baucli.egg-info/PKG-INFO +2 -1
- {baucli-1.0.149 → baucli-1.0.151}/baucli.egg-info/SOURCES.txt +3 -0
- {baucli-1.0.149 → baucli-1.0.151}/baucli.egg-info/requires.txt +1 -0
- {baucli-1.0.149 → baucli-1.0.151}/setup.py +1 -0
- baucli-1.0.151/tests/test_db_check_pdf.py +45 -0
- {baucli-1.0.149 → baucli-1.0.151}/tests/test_db_check_report.py +2 -1
- {baucli-1.0.149 → baucli-1.0.151}/MANIFEST.in +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/README.md +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/baucli/apex_rebuild.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/baucli/apexlang.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/baucli/apexlang_profile.json +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/baucli/apexlint.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/baucli/api.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/baucli/canon.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/baucli/config.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/baucli/db/__init__.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/baucli/db/mssql.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/baucli/db/mysql.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/baucli/db/oracle.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/baucli/db/postgresql.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/baucli/democrm.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/baucli/democrm_sql/mssql/crm_data_mssql.sql +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/baucli/democrm_sql/mssql/crm_demo_mssql.sql +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/baucli/democrm_sql/mysql/crm_data_mysql.sql +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/baucli/democrm_sql/mysql/crm_demo_mysql.sql +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/baucli/democrm_sql/oracle/crm_demo_oracle.sql +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/baucli/democrm_sql/postgresql/crm_data_postgresql.sql +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/baucli/democrm_sql/postgresql/crm_demo_postgresql.sql +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/baucli/diffenvs.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/baucli/docmig.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/baucli/docmig_baseline.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/baucli/docmig_build.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/baucli/docmig_install.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/baucli/docmig_shell.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/baucli/docmig_spec_schema.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/baucli/docmig_spec_tools.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/baucli/docmig_spec_workspace.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/baucli/git.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/baucli/legacy/__init__.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/baucli/legacy/delphi.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/baucli/legacy/delphi_canon.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/baucli/legacy/delphi_evidence.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/baucli/legacy/magic.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/baucli/mcp_server.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/baucli/project.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/baucli/rag_embed.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/baucli/sample_scan.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/baucli/scope_scan.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/baucli/screenshot.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/baucli/seed_check.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/baucli/skills.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/baucli/stress.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/baucli/sync.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/baucli/templates/app_shell/.apex/apexlang.json +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/baucli/templates/app_shell/pages/login.apx.tpl +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/baucli/templates/app_shell/shared-components/lists/navigation-bar.apx +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/baucli/templates/app_shell/shared-components/themes/universal-theme/theme.apx +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/baucli/uikit.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/baucli/unwrap.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/baucli.egg-info/dependency_links.txt +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/baucli.egg-info/entry_points.txt +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/baucli.egg-info/top_level.txt +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/pyproject.toml +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/setup.cfg +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/tests/test_apexlint_binario.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/tests/test_apexlint_dialogo.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/tests/test_apexlint_dominio.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/tests/test_api_request_db.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/tests/test_canon_anchored_markers.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/tests/test_canon_multiengine.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/tests/test_db_tenant_guard.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/tests/test_delphi_canon.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/tests/test_delphi_datasnap.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/tests/test_delphi_evidence.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/tests/test_delphi_fmx.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/tests/test_delta_clock.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/tests/test_deploy_alias.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/tests/test_docmig_baseline.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/tests/test_docmig_build.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/tests/test_docmig_build_extend.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/tests/test_docmig_build_new.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/tests/test_docmig_expressividade.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/tests/test_docmig_install.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/tests/test_docmig_install_extend.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/tests/test_docmig_install_shell.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/tests/test_docmig_project.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/tests/test_docmig_project_new.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/tests/test_docmig_shell.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/tests/test_docmig_spec_analista.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/tests/test_docmig_spec_schema.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/tests/test_docmig_spec_schema_11.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/tests/test_docmig_spec_schema_app.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/tests/test_docmig_spec_use_baseline.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/tests/test_docmig_sqlcl_padrao.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/tests/test_docmig_support_objects.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/tests/test_docrev_dismiss.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/tests/test_doctab_split.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/tests/test_documentation_list_db_label.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/tests/test_mcp_doc_changeset.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/tests/test_mcp_doc_tags.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/tests/test_mcp_doctab_text.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/tests/test_mcp_schema_context.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/tests/test_packaging_deps.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/tests/test_post_apply_uikit.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/tests/test_profiles_add_prompt.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/tests/test_project_guarda_chuva.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/tests/test_scope_scan_call_wrapper.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/tests/test_scope_scan_datasnap_reach.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/tests/test_scope_scan_from_depth.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/tests/test_scope_scan_owner.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/tests/test_scope_scan_pkg_func_from.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/tests/test_skills_apex_domain.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/tests/test_skills_lessons.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/tests/test_skills_size_budget.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/tests/test_spec_sem_beg.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/tests/test_sync_413.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/tests/test_sync_charset.py +0 -0
- {baucli-1.0.149 → baucli-1.0.151}/tests/test_table_status.py +0 -0
- {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.
|
|
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
|
|
@@ -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
|
|
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
|
-
|
|
6692
|
-
|
|
6693
|
-
|
|
6694
|
-
|
|
6695
|
-
|
|
6696
|
-
|
|
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
|
-
|
|
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>•</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
|
|
7
|
-
|
|
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
|
-
|
|
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>"
|
|
Binary file
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: baucli
|
|
3
|
-
Version: 1.0.
|
|
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
|
|
@@ -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 "<ambígua>" 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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|