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