simple-python-audit 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- simple_python_audit/__init__.py +9 -0
- simple_python_audit/engine.py +116 -0
- simple_python_audit/server.py +140 -0
- simple_python_audit-1.0.0.dist-info/METADATA +56 -0
- simple_python_audit-1.0.0.dist-info/RECORD +8 -0
- simple_python_audit-1.0.0.dist-info/WHEEL +5 -0
- simple_python_audit-1.0.0.dist-info/entry_points.txt +2 -0
- simple_python_audit-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import time
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
import logging
|
|
6
|
+
from functools import wraps
|
|
7
|
+
from pyinstrument import Profiler
|
|
8
|
+
from collections import defaultdict
|
|
9
|
+
|
|
10
|
+
_logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
def html_escape(text):
|
|
13
|
+
return str(text).replace("&", "&").replace("<", "<").replace(">", ">").replace('"', """)
|
|
14
|
+
|
|
15
|
+
def profile(html=False, output_path="/tmp/simple_python_audit_perf", deep=False, trace=False):
|
|
16
|
+
def decorator(func):
|
|
17
|
+
@wraps(func)
|
|
18
|
+
def wrapper(*args, **kwargs):
|
|
19
|
+
interval = 0.0001 if deep else 0.001
|
|
20
|
+
profiler = Profiler(interval=interval)
|
|
21
|
+
call_stats = defaultdict(lambda: {'count': 0, 'total_time': 0.0, 'stack': []})
|
|
22
|
+
|
|
23
|
+
def trace_calls(frame, event, arg):
|
|
24
|
+
if event == 'call':
|
|
25
|
+
f_name = frame.f_code.co_name
|
|
26
|
+
call_stats[f_name]['count'] += 1
|
|
27
|
+
call_stats[f_name]['stack'].append(time.time())
|
|
28
|
+
elif event == 'return':
|
|
29
|
+
f_name = frame.f_code.co_name
|
|
30
|
+
if f_name in call_stats and call_stats[f_name]['stack']:
|
|
31
|
+
start_t = call_stats[f_name]['stack'].pop()
|
|
32
|
+
call_stats[f_name]['total_time'] += (time.time() - start_t)
|
|
33
|
+
return trace_calls
|
|
34
|
+
|
|
35
|
+
if trace:
|
|
36
|
+
sys.settrace(trace_calls)
|
|
37
|
+
|
|
38
|
+
profiler.start()
|
|
39
|
+
start_real_time = time.time()
|
|
40
|
+
try:
|
|
41
|
+
return func(*args, **kwargs)
|
|
42
|
+
finally:
|
|
43
|
+
profiler.stop()
|
|
44
|
+
if trace:
|
|
45
|
+
sys.settrace(None)
|
|
46
|
+
|
|
47
|
+
duration = time.time() - start_real_time
|
|
48
|
+
if html:
|
|
49
|
+
if not os.path.exists(output_path):
|
|
50
|
+
os.makedirs(output_path)
|
|
51
|
+
|
|
52
|
+
pyinstrument_html = profiler.output_html()
|
|
53
|
+
vars_snap = {"args": [str(a)[:500] for a in args], "kwargs": {k: str(v)[:500] for k, v in kwargs.items()}}
|
|
54
|
+
|
|
55
|
+
stats_data = []
|
|
56
|
+
if trace:
|
|
57
|
+
stats_data = [
|
|
58
|
+
{"name": k, "count": v['count'], "total": round(v['total_time'], 4), "avg": round(v['total_time']/v['count'], 5)}
|
|
59
|
+
for k, v in sorted(call_stats.items(), key=lambda x: x[1]['total_time'], reverse=True) if v['count'] > 0
|
|
60
|
+
][:50]
|
|
61
|
+
|
|
62
|
+
injection_script = f"""
|
|
63
|
+
<script>
|
|
64
|
+
(function() {{
|
|
65
|
+
const d = {{ func: "{func.__name__}", dur: "{duration:.4f}s", vars: {json.dumps(vars_snap)}, stats: {json.dumps(stats_data)} }};
|
|
66
|
+
function render() {{
|
|
67
|
+
if (document.getElementById('odoo-audit-host')) return;
|
|
68
|
+
const host = document.createElement('div');
|
|
69
|
+
host.id = 'odoo-audit-host';
|
|
70
|
+
host.style = 'position:fixed; bottom:20px; right:20px; z-index:2147483647;';
|
|
71
|
+
document.documentElement.appendChild(host);
|
|
72
|
+
const shadow = host.attachShadow({{mode:'open'}});
|
|
73
|
+
const root = document.createElement('div');
|
|
74
|
+
root.innerHTML = `
|
|
75
|
+
<style>
|
|
76
|
+
:host {{ all: initial; }}
|
|
77
|
+
.fab {{ width:55px; height:55px; background:#714B67; color:white; border-radius:50%; display:flex; align-items:center; justify-content:center; cursor:pointer; box-shadow:0 4px 15px rgba(0,0,0,0.4); font-size:22px; border:2px solid white; position:fixed; bottom:20px; right:20px; transition:0.2s; }}
|
|
78
|
+
.fab:hover {{ transform:scale(1.1); }}
|
|
79
|
+
.modal {{ display:none; position:fixed; bottom:85px; right:20px; width:500px; max-height:75vh; background:white; border-radius:12px; border:1px solid #714B67; box-shadow:0 10px 40px rgba(0,0,0,0.4); flex-direction:column; font-family:sans-serif; overflow:hidden; }}
|
|
80
|
+
.modal.open {{ display:flex; }}
|
|
81
|
+
.header {{ background:#714B67; color:white; padding:12px; display:flex; justify-content:space-between; font-weight:bold; }}
|
|
82
|
+
.body {{ padding:15px; overflow-y:auto; background:white; color:#333; }}
|
|
83
|
+
input {{ width:95%; padding:8px; margin:10px 0; border:1px solid #ddd; border-radius:4px; }}
|
|
84
|
+
table {{ width:100%; border-collapse:collapse; font-size:12px; }}
|
|
85
|
+
th {{ background:#f4f4f4; padding:8px; text-align:left; sticky; top:0; }}
|
|
86
|
+
td {{ padding:8px; border-bottom:1px solid #eee; }}
|
|
87
|
+
pre {{ background:#1e1e1e; color:#9cdcfe; padding:10px; font-size:11px; border-radius:4px; max-height:150px; overflow:auto; }}
|
|
88
|
+
</style>
|
|
89
|
+
<div class="modal" id="m">
|
|
90
|
+
<div class="header"><span>🚀 ${{d.func}}</span><span>${{d.dur}}</span></div>
|
|
91
|
+
<div class="body">
|
|
92
|
+
<details><summary style="cursor:pointer;color:#714B67">📦 Vars</summary><pre>${{JSON.stringify(d.vars,null,2)}}</pre></details>
|
|
93
|
+
<input type="text" id="s" placeholder="Buscar função...">
|
|
94
|
+
<table><thead><tr><th>Função</th><th>Calls</th><th>Total</th><th>Média</th></tr></thead>
|
|
95
|
+
<tbody id="t">${{d.stats.map(s=>`<tr class="r"><td style="font-family:monospace" class="n">${{s.name}}</td><td align="center">${{s.count}}</td><td align="right">${{s.total}}s</td><td align="right">${{s.avg}}s</td></tr>`).join('')}}</tbody>
|
|
96
|
+
</table>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
<div class="fab" id="b">🚀</div>`;
|
|
100
|
+
const m = root.querySelector('#m');
|
|
101
|
+
root.querySelector('#b').onclick = () => m.classList.toggle('open');
|
|
102
|
+
root.querySelector('#s').oninput = (e) => {{
|
|
103
|
+
const v = e.target.value.toLowerCase();
|
|
104
|
+
root.querySelectorAll('.r').forEach(r => r.style.display = r.querySelector('.n').textContent.toLowerCase().includes(v) ? '' : 'none');
|
|
105
|
+
}};
|
|
106
|
+
shadow.appendChild(root);
|
|
107
|
+
}}
|
|
108
|
+
setInterval(render, 1000); render();
|
|
109
|
+
}})();
|
|
110
|
+
</script>
|
|
111
|
+
"""
|
|
112
|
+
final_html = pyinstrument_html.replace('</html>', f'{injection_script}</html>')
|
|
113
|
+
filepath = os.path.join(output_path, f"AUDIT_{func.__name__}_{int(time.time())}.html")
|
|
114
|
+
with open(filepath, "w", encoding="utf-8") as f: f.write(final_html)
|
|
115
|
+
return wrapper
|
|
116
|
+
return decorator
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import http.server
|
|
3
|
+
import os
|
|
4
|
+
import urllib.parse
|
|
5
|
+
import datetime
|
|
6
|
+
import json
|
|
7
|
+
from io import BytesIO
|
|
8
|
+
from datetime import timezone, timedelta
|
|
9
|
+
|
|
10
|
+
class LogManagerHandler(http.server.SimpleHTTPRequestHandler):
|
|
11
|
+
def get_fortaleza_time(self, timestamp):
|
|
12
|
+
tz_fortaleza = timezone(timedelta(hours=-3))
|
|
13
|
+
dt = datetime.datetime.fromtimestamp(timestamp, tz=timezone.utc)
|
|
14
|
+
return dt.astimezone(tz_fortaleza).strftime('%d/%m/%Y %H:%M:%S')
|
|
15
|
+
|
|
16
|
+
def do_POST(self):
|
|
17
|
+
try:
|
|
18
|
+
content_length = int(self.headers.get('Content-Length', 0))
|
|
19
|
+
body = self.rfile.read(content_length).decode('utf-8')
|
|
20
|
+
data = json.loads(body)
|
|
21
|
+
filenames = data.get('files', [])
|
|
22
|
+
|
|
23
|
+
deleted_count = 0
|
|
24
|
+
for name in filenames:
|
|
25
|
+
full_path = os.path.normpath(os.path.join(self.directory, urllib.parse.unquote(name)))
|
|
26
|
+
if full_path.startswith(os.path.abspath(self.directory)) and os.path.isfile(full_path):
|
|
27
|
+
os.remove(full_path)
|
|
28
|
+
deleted_count += 1
|
|
29
|
+
|
|
30
|
+
self.send_response(200)
|
|
31
|
+
self.end_headers()
|
|
32
|
+
self.wfile.write(f"{deleted_count} removidos".encode())
|
|
33
|
+
except Exception as e:
|
|
34
|
+
self.send_error(500, str(e))
|
|
35
|
+
|
|
36
|
+
def list_directory(self, path):
|
|
37
|
+
try:
|
|
38
|
+
list_dir = [n for n in os.listdir(path) if not os.path.isdir(os.path.join(path, n))]
|
|
39
|
+
except OSError:
|
|
40
|
+
self.send_error(404, "Sem permissão")
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
list_dir.sort(key=lambda a: a.lower())
|
|
44
|
+
displaypath = urllib.parse.unquote(self.path)
|
|
45
|
+
|
|
46
|
+
r = []
|
|
47
|
+
r.append('<!DOCTYPE HTML><html><head><meta charset="utf-8">')
|
|
48
|
+
r.append(f'<title>Logs Fortaleza: {displaypath}</title>')
|
|
49
|
+
r.append("""
|
|
50
|
+
<style>
|
|
51
|
+
body { font-family: 'Segoe UI', sans-serif; margin: 30px; background: #f0f4f8; }
|
|
52
|
+
.container { max-width: 1000px; margin: auto; background: white; padding: 25px; border-radius: 12px; box-shadow: 0 5px 15px rgba(0,0,0,0.08); }
|
|
53
|
+
h1 { color: #1a365d; font-size: 1.4rem; border-left: 5px solid #3182ce; padding-left: 15px; }
|
|
54
|
+
.timezone-badge { font-size: 0.8rem; background: #ebf8ff; color: #2b6cb0; padding: 4px 8px; border-radius: 4px; float: right; }
|
|
55
|
+
table { width: 100%; border-collapse: collapse; margin-top: 20px; }
|
|
56
|
+
th, td { padding: 12px; text-align: left; border-bottom: 1px solid #e2e8f0; }
|
|
57
|
+
th { background: #edf2f7; color: #4a5568; font-size: 0.85rem; }
|
|
58
|
+
.btn { padding: 8px 18px; border-radius: 6px; cursor: pointer; border: none; font-weight: bold; }
|
|
59
|
+
.btn-danger { background: #e53e3e; color: white; }
|
|
60
|
+
.btn-danger:disabled { background: #fc8181; opacity: 0.6; }
|
|
61
|
+
input[type="checkbox"] { width: 18px; height: 18px; cursor: pointer; }
|
|
62
|
+
</style>
|
|
63
|
+
<script>
|
|
64
|
+
function toggleAll(master) {
|
|
65
|
+
document.querySelectorAll('.file-check').forEach(cb => cb.checked = master.checked);
|
|
66
|
+
updateButton();
|
|
67
|
+
}
|
|
68
|
+
function updateButton() {
|
|
69
|
+
const count = document.querySelectorAll('.file-check:checked').length;
|
|
70
|
+
document.getElementById('del-btn').innerText = `Excluir Selecionados (${count})`;
|
|
71
|
+
document.getElementById('del-btn').disabled = count === 0;
|
|
72
|
+
}
|
|
73
|
+
async function deleteSelected() {
|
|
74
|
+
const files = Array.from(document.querySelectorAll('.file-check:checked')).map(cb => cb.value);
|
|
75
|
+
if (confirm(`Remover ${files.length} arquivo(s)?`)) {
|
|
76
|
+
const res = await fetch('/', {
|
|
77
|
+
method: 'POST',
|
|
78
|
+
headers: {'Content-Type': 'application/json'},
|
|
79
|
+
body: JSON.stringify({files})
|
|
80
|
+
});
|
|
81
|
+
if (res.ok) location.reload();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
</script>
|
|
85
|
+
</head><body><div class="container">""")
|
|
86
|
+
|
|
87
|
+
r.append(f'<span class="timezone-badge">Timezone: Fortaleza (UTC-3)</span>')
|
|
88
|
+
r.append(f'<h1>Logs: {displaypath}</h1>')
|
|
89
|
+
r.append('<button id="del-btn" class="btn btn-danger" onclick="deleteSelected()" disabled>Excluir Selecionados (0)</button>')
|
|
90
|
+
|
|
91
|
+
r.append('<table><thead><tr>')
|
|
92
|
+
r.append('<th><input type="checkbox" onclick="toggleAll(this)"></th>')
|
|
93
|
+
r.append('<th>Nome do Arquivo</th><th>Tamanho</th><th>Última Modificação</th></tr></thead><tbody>')
|
|
94
|
+
|
|
95
|
+
for name in list_dir:
|
|
96
|
+
fullname = os.path.join(path, name)
|
|
97
|
+
stats = os.stat(fullname)
|
|
98
|
+
|
|
99
|
+
# Formatação de Tamanho
|
|
100
|
+
size = stats.st_size
|
|
101
|
+
readable_size = f"{size} B"
|
|
102
|
+
for unit in ['KB', 'MB', 'GB']:
|
|
103
|
+
if size < 1024: break
|
|
104
|
+
size /= 1024
|
|
105
|
+
readable_size = f"{size:.1f} {unit}"
|
|
106
|
+
|
|
107
|
+
# DATA EM HORÁRIO DE FORTALEZA
|
|
108
|
+
mtime_fortaleza = self.get_fortaleza_time(stats.st_mtime)
|
|
109
|
+
|
|
110
|
+
r.append('<tr>')
|
|
111
|
+
r.append(f'<td><input type="checkbox" class="file-check" value="{urllib.parse.quote(name)}" onclick="updateButton()"></td>')
|
|
112
|
+
r.append(f'<td><a href="{urllib.parse.quote(name)}">{name}</a></td>')
|
|
113
|
+
r.append(f'<td>{readable_size}</td>')
|
|
114
|
+
r.append(f'<td>{mtime_fortaleza}</td>')
|
|
115
|
+
r.append('</tr>')
|
|
116
|
+
|
|
117
|
+
r.append('</tbody></table></div></body></html>')
|
|
118
|
+
|
|
119
|
+
content = "\n".join(r).encode('utf-8')
|
|
120
|
+
f = BytesIO(content)
|
|
121
|
+
self.send_response(200)
|
|
122
|
+
self.send_header("Content-type", "text/html; charset=utf-8")
|
|
123
|
+
self.send_header("Content-Length", str(len(content)))
|
|
124
|
+
self.end_headers()
|
|
125
|
+
return f
|
|
126
|
+
|
|
127
|
+
def start_server(port=8000, directory="/tmp/simple_python_audit_perf"):
|
|
128
|
+
abs_target = os.path.abspath(directory)
|
|
129
|
+
if not os.path.exists(abs_target): os.makedirs(abs_target)
|
|
130
|
+
def handler_factory(*args, **kwargs):
|
|
131
|
+
return LogManagerHandler(*args, directory=abs_target, **kwargs)
|
|
132
|
+
print(f"📍 Simple Python Audit Server [Fortaleza]\n📂 Pasta: {abs_target}\n🔗 http://0.0.0.0:{port}")
|
|
133
|
+
http.server.HTTPServer(('0.0.0.0', port), handler_factory).serve_forever()
|
|
134
|
+
|
|
135
|
+
def cli():
|
|
136
|
+
parser = argparse.ArgumentParser(description="Simple Python Audit Log Server")
|
|
137
|
+
parser.add_argument("--port", type=int, default=8000)
|
|
138
|
+
parser.add_argument("--dir", type=str, default="/tmp/simple_python_audit_perf")
|
|
139
|
+
args = parser.parse_args()
|
|
140
|
+
start_server(port=args.port, directory=args.dir)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: simple-python-audit
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Ferramenta de profiling com widget flutuante e servidor de gerenciamento de logs.
|
|
5
|
+
Author-email: Sadson Diego <sadsondiego@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: odoo,profiler,performance,pyinstrument,audit
|
|
8
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Framework :: Odoo
|
|
14
|
+
Classifier: Topic :: Software Development :: Debuggers
|
|
15
|
+
Requires-Python: >=3.8
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
Requires-Dist: pyinstrument>=4.6.0
|
|
18
|
+
Requires-Dist: typing-extensions>=4.0.0
|
|
19
|
+
|
|
20
|
+
# Simple Python Audit Tool 🚀
|
|
21
|
+
|
|
22
|
+
Ferramenta completa para medição de performance e auditoria de variáveis para python.
|
|
23
|
+
|
|
24
|
+
## 📦 Instalação
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install .
|
|
28
|
+
# Ou via GitHub
|
|
29
|
+
pip install git+[https://github.com/sadson/simple_python_audit.git](https://github.com/sadson/simple_python_audit.git)
|
|
30
|
+
|
|
31
|
+
## 🎯 Como Usar
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from simple_python_audit import profile
|
|
35
|
+
|
|
36
|
+
@profile(html=True, output_path="/tmp/simple_python_audit_perf" ,trace=True, deep=True)
|
|
37
|
+
def meu_metodo_lento(self):
|
|
38
|
+
# Seu código aqui
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Parâmetros:**
|
|
42
|
+
- `html=True`: Gera relatório em HTML
|
|
43
|
+
- `trace=True`: Rastreia chamadas de funções
|
|
44
|
+
- `deep=True`: Rastreia chamadas com tempo de execução a partir de 0.0001 ms
|
|
45
|
+
- `output_path`: Caminho para salvar os relatórios gerados
|
|
46
|
+
|
|
47
|
+
## 🛠️ Executar Servidor de Auditoria
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
simple-python-audit-server --port 8080 --dir /tmp/odoo_perf
|
|
51
|
+
ou
|
|
52
|
+
via python
|
|
53
|
+
|
|
54
|
+
from simple_python_audit import start_server
|
|
55
|
+
start_server(port=8080, directory="/tmp/odoo_perf")
|
|
56
|
+
```
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
simple_python_audit/__init__.py,sha256=_WZYJ-U3BX6iT6K2h6kzWqEX3s2uYMqiwJS3RXIk828,209
|
|
2
|
+
simple_python_audit/engine.py,sha256=QEdt3mF2lmM9Som3W-cTmqv-8bGxG9xPrvutExFKYmc,7343
|
|
3
|
+
simple_python_audit/server.py,sha256=DJ1pYpzHum90MuB7D7BknTtLLBpeUznlyt9lKdsAlGQ,6806
|
|
4
|
+
simple_python_audit-1.0.0.dist-info/METADATA,sha256=2qRoHrbyP2hxo5UmVJ4z_cKh-4dTPjvrSj5icdjiU2o,1743
|
|
5
|
+
simple_python_audit-1.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
6
|
+
simple_python_audit-1.0.0.dist-info/entry_points.txt,sha256=cmagMpsV1dZOmIVQlTSXXWmMFY60cmaMAjdzyvtWu6k,78
|
|
7
|
+
simple_python_audit-1.0.0.dist-info/top_level.txt,sha256=9_l3h-VMe9qwz-XxbAM75awYNycYLhJ97LP7QHYlIgw,20
|
|
8
|
+
simple_python_audit-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
simple_python_audit
|