Spaces:
Running
Running
| # -*- 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() | |