IOI-RUN / env_audit.py
Roudrigus's picture
Upload 82 files
0f0ef8d verified
# -*- 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()