File size: 13,311 Bytes
0f0ef8d |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 |
# -*- 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()
|