|
|
|
|
| """
|
| 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()
|
|
|