# -*- coding: utf-8 -*- """ 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): # detecta set_page_config 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: # sobe na árvore: se estiver dentro de um If com __name__ == "__main__" cur = node while cur in parents: cur = parents[cur] if isinstance(cur, ast.If): # test é __name__ == "__main__" t = cur.test # match Name('__name__') == Constant('__main__') 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 # Se houver set_page_config e nenhuma estiver dentro do guard, marca True (incorreto) 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() # encontra blocos de entries "key": { ... } # Simplificação: captura 'MODULES = { ... }' então entradas por aspas + chave. main_match = re.search(r"MODULES\s*=\s*\{(.+?)\}\s*$", src, flags=re.S) if not main_match: return modmap body = main_match.group(1) # Captura nomes de entradas: "nome": { ... } entry_re = re.compile(r'"\'["\']\s*:\s*\{(.*?)\}', re.S) for em in entry_re.finditer(body): key = em.group(1) obj = em.group(2) # captura "file": "xyz" se houver 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": [], # lista de arquivos "missing_entry_points": [], # (arquivo, has_main, has_render) "modules_map_mismatches": [], # (key, file, suggestion) "db_prod_risk": [], # banco.py hints "missing_in_test": [], # se prod_path fornecido "extra_in_test": [], # se prod_path fornecido }, "summary": {} } env_files = list_py(env_path) report["files_total"] = len(env_files) # modules_map modmap = read_modules_map(env_path) # varredura de .py 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.py heuristic 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() # heurísticas simples (ajuste conforme seu cenário) 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 # Mismatch entre modules_map e arquivos # Constrói um conjunto de nomes de módulo possíveis (arquivo base sem .py) 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: # sugere arquivo com nome mais próximo 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)) # Diferenças entre PRODUÇÃO e TESTE (se prod_path fornecido) 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 # resumo 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) # salva JSON 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}") # imprime resumo markdown print_markdown(report) if __name__ == "__main__": main()