| |
|
| |
|
| | """
|
| | env_audit.py — Auditoria de ambiente (TESTE/Produção) para projetos Streamlit
|
| | Autor: Rodrigo / ARM | 2026
|
| |
|
| | Verifica:
|
| | • Uso de st.set_page_config fora de if __name__ == "__main__"
|
| | • Presença de funções main()/render() em módulos
|
| | • Diferenças entre arquivos .py e modules_map.py (sugestão de "file")
|
| | • Heurística de banco (banco.py apontando para produção)
|
| | • Gera env_audit_report.json + imprime resumo Markdown
|
| |
|
| | Uso:
|
| | python env_audit.py --env "C:\\...\\ambiente_teste\\LoadApp"
|
| | (ou) comparar pastas:
|
| | python env_audit.py --prod "C:\\...\\producao\\LoadApp" --test "C:\\...\\ambiente_teste\\LoadApp"
|
| | """
|
| |
|
| | import os, sys, json, ast, re
|
| | from typing import Dict, List, Tuple, Optional
|
| |
|
| | def list_py(base: str) -> List[str]:
|
| | files = []
|
| | for root, _, names in os.walk(base):
|
| | for n in names:
|
| | if n.endswith(".py"):
|
| | files.append(os.path.normpath(os.path.join(root, n)))
|
| | return files
|
| |
|
| | def relpath_set(base: str) -> set:
|
| | return set([os.path.relpath(p, base) for p in list_py(base)])
|
| |
|
| | def parse_ast(path: str) -> Optional[ast.AST]:
|
| | try:
|
| | with open(path, "r", encoding="utf-8") as f:
|
| | src = f.read()
|
| | return ast.parse(src, filename=path)
|
| | except Exception:
|
| | return None
|
| |
|
| | def has_set_page_config_outside_main(tree: ast.AST) -> bool:
|
| | """
|
| | Retorna True se encontrar chamada a set_page_config fora do guard if __name__ == "__main__".
|
| | Heurística: procura ast.Call para nome/atributo 'set_page_config' e confere se está dentro de um If guard.
|
| | """
|
| | if tree is None:
|
| | return False
|
| |
|
| | calls = []
|
| | parents = {}
|
| |
|
| | class ParentAnnotator(ast.NodeVisitor):
|
| | def generic_visit(self, node):
|
| | for child in ast.iter_child_nodes(node):
|
| | parents[child] = node
|
| | super().generic_visit(node)
|
| |
|
| | class CallCollector(ast.NodeVisitor):
|
| | def visit_Call(self, node):
|
| |
|
| | name = None
|
| | if isinstance(node.func, ast.Name):
|
| | name = node.func.id
|
| | elif isinstance(node.func, ast.Attribute):
|
| | name = node.func.attr
|
| | if name == "set_page_config":
|
| | calls.append(node)
|
| | self.generic_visit(node)
|
| |
|
| | ParentAnnotator().visit(tree)
|
| | CallCollector().visit(tree)
|
| |
|
| | def inside_main_guard(node: ast.AST) -> bool:
|
| |
|
| | cur = node
|
| | while cur in parents:
|
| | cur = parents[cur]
|
| | if isinstance(cur, ast.If):
|
| |
|
| | t = cur.test
|
| |
|
| | if isinstance(t, ast.Compare):
|
| | left = t.left
|
| | comparators = t.comparators
|
| | if isinstance(left, ast.Name) and left.id == "__name__" and comparators:
|
| | comp = comparators[0]
|
| | if isinstance(comp, ast.Constant) and comp.value == "__main__":
|
| | return True
|
| | return False
|
| |
|
| |
|
| | for c in calls:
|
| | if not inside_main_guard(c):
|
| | return True
|
| | return False
|
| |
|
| | def has_function(tree: ast.AST, fn_name: str) -> bool:
|
| | if tree is None:
|
| | return False
|
| | for node in ast.walk(tree):
|
| | if isinstance(node, ast.FunctionDef) and node.name == fn_name:
|
| | return True
|
| | return False
|
| |
|
| | def guess_module_name(file_path: str, base: str) -> str:
|
| | """Retorna o nome do módulo sem extensão e sem subdiretórios ('modulos/x.py' -> 'x')."""
|
| | rel = os.path.relpath(file_path, base)
|
| | name = os.path.splitext(os.path.basename(rel))[0]
|
| | return name
|
| |
|
| | def read_modules_map(path: str) -> Dict[str, Dict]:
|
| | """
|
| | Lê modules_map.py e tenta extrair o dict MODULES.
|
| | Método simples: regex para entries de primeiro nível.
|
| | """
|
| | modmap = {}
|
| | mm_path = os.path.join(path, "modules_map.py")
|
| | if not os.path.isfile(mm_path):
|
| | return modmap
|
| | try:
|
| | with open(mm_path, "r", encoding="utf-8") as f:
|
| | src = f.read()
|
| |
|
| |
|
| | main_match = re.search(r"MODULES\s*=\s*\{(.+?)\}\s*$", src, flags=re.S)
|
| | if not main_match:
|
| | return modmap
|
| | body = main_match.group(1)
|
| |
|
| | entry_re = re.compile(r'"\'["\']\s*:\s*\{(.*?)\}', re.S)
|
| | for em in entry_re.finditer(body):
|
| | key = em.group(1)
|
| | obj = em.group(2)
|
| |
|
| | file_m = re.search(r'["\']file["\']\s*:\s*"\'["\']', obj)
|
| | label_m = re.search(r'["\']label["\']\s*:\s*"\'["\']', obj)
|
| | perfis_m = re.findall(r'["\']perfis["\']\s*:\s*\[([^\]]+)\]', obj)
|
| | grupo_m = re.search(r'["\']grupo["\']\s*:\s*"\'["\']', obj)
|
| | modmap[key] = {
|
| | "file": file_m.group(1) if file_m else None,
|
| | "label": label_m.group(1) if label_m else key,
|
| | "perfis": perfis_m[0] if perfis_m else None,
|
| | "grupo": grupo_m.group(1) if grupo_m else None,
|
| | }
|
| | except Exception:
|
| | pass
|
| | return modmap
|
| |
|
| | def audit_env(env_path: str, prod_path: Optional[str] = None) -> Dict:
|
| | report = {
|
| | "env_path": env_path,
|
| | "prod_path": prod_path,
|
| | "files_total": 0,
|
| | "issues": {
|
| | "set_page_config_outside_main": [],
|
| | "missing_entry_points": [],
|
| | "modules_map_mismatches": [],
|
| | "db_prod_risk": [],
|
| | "missing_in_test": [],
|
| | "extra_in_test": [],
|
| | },
|
| | "summary": {}
|
| | }
|
| |
|
| | env_files = list_py(env_path)
|
| | report["files_total"] = len(env_files)
|
| |
|
| |
|
| | modmap = read_modules_map(env_path)
|
| |
|
| |
|
| | for f in env_files:
|
| | tree = parse_ast(f)
|
| | if has_set_page_config_outside_main(tree):
|
| | report["issues"]["set_page_config_outside_main"].append(os.path.relpath(f, env_path))
|
| |
|
| | has_main = has_function(tree, "main")
|
| | has_render = has_function(tree, "render")
|
| | if not (has_main or has_render):
|
| | report["issues"]["missing_entry_points"].append((os.path.relpath(f, env_path), has_main, has_render))
|
| |
|
| |
|
| | banco_path = os.path.join(env_path, "banco.py")
|
| | if os.path.isfile(banco_path):
|
| | try:
|
| | with open(banco_path, "r", encoding="utf-8") as bf:
|
| | bsrc = bf.read().lower()
|
| |
|
| | hints = []
|
| | if "prod" in bsrc or "production" in bsrc:
|
| | hints.append("contém 'prod'/'production' no banco.py (pode estar apontando para produção)")
|
| | if "localhost" in bsrc or "127.0.0.1" in bsrc:
|
| | hints.append("contém localhost (ok se seu DB de teste for local)")
|
| | if "ambiente_teste" in bsrc or "test" in bsrc:
|
| | hints.append("contém 'test'/'ambiente_teste' (bom indício de DB de teste)")
|
| | if hints:
|
| | report["issues"]["db_prod_risk"].extend(hints)
|
| | except Exception:
|
| | pass
|
| |
|
| |
|
| |
|
| | module_name_set = set([guess_module_name(f, env_path) for f in env_files])
|
| | for key, info in modmap.items():
|
| | file_hint = info.get("file")
|
| | target = file_hint or key
|
| | if target not in module_name_set:
|
| |
|
| | suggestions = [m for m in module_name_set if m.lower() == key.lower()]
|
| | sug = suggestions[0] if suggestions else None
|
| | report["issues"]["modules_map_mismatches"].append((key, file_hint, sug))
|
| |
|
| |
|
| | if prod_path and os.path.isdir(prod_path):
|
| | prod_set = relpath_set(prod_path)
|
| | test_set = relpath_set(env_path)
|
| | missing = sorted(list(prod_set - test_set))
|
| | extra = sorted(list(test_set - prod_set))
|
| | report["issues"]["missing_in_test"] = missing
|
| | report["issues"]["extra_in_test"] = extra
|
| |
|
| |
|
| | report["summary"] = {
|
| | "files_total": report["files_total"],
|
| | "set_page_config_outside_main_count": len(report["issues"]["set_page_config_outside_main"]),
|
| | "missing_entry_points_count": len(report["issues"]["missing_entry_points"]),
|
| | "modules_map_mismatches_count": len(report["issues"]["modules_map_mismatches"]),
|
| | "db_risk_flags_count": len(report["issues"]["db_prod_risk"]),
|
| | "missing_in_test_count": len(report["issues"]["missing_in_test"]),
|
| | "extra_in_test_count": len(report["issues"]["extra_in_test"]),
|
| | }
|
| |
|
| | return report
|
| |
|
| | def print_markdown(report: Dict):
|
| | env_path = report["env_path"]
|
| | prod_path = report.get("prod_path") or "—"
|
| | print(f"# 🧪 Auditoria de Ambiente\n")
|
| | print(f"- **Pasta auditada (TESTE)**: `{env_path}`")
|
| | print(f"- **Pasta de PRODUÇÃO (comparação)**: `{prod_path}`\n")
|
| |
|
| | s = report["summary"]
|
| | print("## ✅ Resumo")
|
| | for k, v in s.items():
|
| | print(f"- {k.replace('_',' ').title()}: {v}")
|
| | print()
|
| |
|
| | issues = report["issues"]
|
| |
|
| | if issues["set_page_config_outside_main"]:
|
| | print("## ⚠️ `st.set_page_config` fora de `if __name__ == '__main__'`")
|
| | for f in issues["set_page_config_outside_main"]:
|
| | print(f"- {f}")
|
| | print("**Ajuste**: mover `st.set_page_config(...)` para dentro de:\n")
|
| | print("```python\nif __name__ == \"__main__\":\n st.set_page_config(...)\n main()\n```\n")
|
| |
|
| | if issues["missing_entry_points"]:
|
| | print("## ⚠️ Módulos sem `main()` e sem `render()`")
|
| | for f, has_main, has_render in issues["missing_entry_points"]:
|
| | print(f"- {f} — main={has_main}, render={has_render}")
|
| | print("**Ajuste**: garantir pelo menos uma função de entrada (`main` ou `render`).\n")
|
| |
|
| | if issues["modules_map_mismatches"]:
|
| | print("## ⚠️ Diferenças entre `modules_map.py` e arquivos")
|
| | for key, file_hint, sug in issues["modules_map_mismatches"]:
|
| | print(f"- key=`{key}` | file=`{file_hint}` | sugestão de arquivo=`{sug or 'verificar manualmente'}`")
|
| | print("**Ajuste**: em `modules_map.py`, defina `\"file\": \"nome_do_arquivo\"` quando o nome do arquivo não for igual à key.\n")
|
| |
|
| | if issues["db_prod_risk"]:
|
| | print("## ⚠️ Banco (heurística)")
|
| | for m in issues["db_prod_risk"]:
|
| | print(f"- {m}")
|
| | print("**Ajuste**: confirmar que `banco.py` do TESTE aponta para DB de teste (não produção).\n")
|
| |
|
| | if issues["missing_in_test"]:
|
| | print("## 🟠 Arquivos que existem na PRODUÇÃO e faltam no TESTE")
|
| | for f in issues["missing_in_test"]:
|
| | print(f"- {f}")
|
| | print()
|
| |
|
| | if issues["extra_in_test"]:
|
| | print("## 🔵 Arquivos que existem no TESTE e não existem na PRODUÇÃO")
|
| | for f in issues["extra_in_test"]:
|
| | print(f"- {f}")
|
| | print()
|
| |
|
| | def main():
|
| | import argparse
|
| | p = argparse.ArgumentParser()
|
| | p.add_argument("--env", help="Pasta do ambiente a auditar (ex.: ...\\ambiente_teste\\LoadApp)")
|
| | p.add_argument("--prod", help="(Opcional) Pasta de PRODUÇÃO para comparação.")
|
| | p.add_argument("--test", help="(Opcional) Pasta de TESTE para comparação (se usar --prod).")
|
| | args = p.parse_args()
|
| |
|
| | if args.env:
|
| | env_path = os.path.normpath(args.env)
|
| | report = audit_env(env_path)
|
| | elif args.prod and args.test:
|
| | prod = os.path.normpath(args.prod)
|
| | test = os.path.normpath(args.test)
|
| | report = audit_env(test, prod_path=prod)
|
| | else:
|
| | print("Uso: python env_audit.py --env \"C:\\...\\ambiente_teste\\LoadApp\"\n"
|
| | " ou: python env_audit.py --prod \"C:\\...\\producao\\LoadApp\" --test \"C:\\...\\ambiente_teste\\LoadApp\"")
|
| | sys.exit(2)
|
| |
|
| |
|
| | out_json = os.path.join(os.getcwd(), "env_audit_report.json")
|
| | try:
|
| | with open(out_json, "w", encoding="utf-8") as f:
|
| | json.dump(report, f, ensure_ascii=False, indent=2)
|
| | print(f"\n💾 Relatório salvo em: {out_json}\n")
|
| | except Exception as e:
|
| | print(f"Falha ao salvar JSON: {e}")
|
| |
|
| |
|
| | print_markdown(report)
|
| |
|
| | if __name__ == "__main__":
|
| | main()
|
| |
|