Skip to content

Commit e48c5b4

Browse files
committed
fix(style): code review fixes per CONTRIBUTING.md checklist
- Add missing file header to utils/utils.py - Add type annotations to set_message_callback, set_draft_callback, _pyrogram_task_exception_handler, _install_pyrogram_exception_handler, _restore_pyrogram_exception_handler in clients/pyrogram_client.py - Add return type Callable to _require_auth decorator in dashboard/server.py, remove type: ignore comment - Add return type annotation to _dashboard_print in bot.py - Move builtins.print override from module-level into main() in bot.py - Move Jinja2 tojson expression out of script block in dashboard.html to fix linter false positive
1 parent d11d91a commit e48c5b4

File tree

6 files changed

+53
-29
lines changed

6 files changed

+53
-29
lines changed

bot.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@
5757
_original_print = builtins.print
5858

5959

60-
def _dashboard_print(*args, **kwargs):
60+
def _dashboard_print(*args, **kwargs) -> None:
6161
"""Wrapper around print() that also feeds output to dashboard stats."""
6262
_original_print(*args, **kwargs)
6363
try:
@@ -68,9 +68,6 @@ def _dashboard_print(*args, **kwargs):
6868
pass # Never break the bot due to dashboard
6969

7070

71-
builtins.print = _dashboard_print
72-
73-
7471
# ====== ОБРАБОТЧИК ОШИБОК ======
7572

7673
async def on_error(update: object, context: ContextTypes.DEFAULT_TYPE) -> None:
@@ -83,6 +80,9 @@ async def on_error(update: object, context: ContextTypes.DEFAULT_TYPE) -> None:
8380

8481
def main() -> None:
8582
"""Точка входа — запуск бота в polling-режиме."""
83+
# Активируем перехват print() для дашборда (только при запуске, не при импорте)
84+
builtins.print = _dashboard_print
85+
8686
if not BOT_TOKEN:
8787
print("❌ BOT_TOKEN не задан! Установите его в .env")
8888
return

clients/pyrogram_client.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# clients/pyrogram_client.py — Управление Pyrogram-сессиями пользователей
22

33
import asyncio
4+
from typing import Any, Callable
45
from collections import defaultdict
56
from datetime import datetime, timezone
67

@@ -42,13 +43,13 @@ def _make_processed_message_key(chat_id: int | None, message_id: int | None) ->
4243
return (chat_id, message_id)
4344

4445

45-
def set_message_callback(callback) -> None:
46+
def set_message_callback(callback: Callable | None) -> None:
4647
"""Устанавливает callback для обработки входящих сообщений."""
4748
global _on_new_message_callback
4849
_on_new_message_callback = callback
4950

5051

51-
def set_draft_callback(callback) -> None:
52+
def set_draft_callback(callback: Callable | None) -> None:
5253
"""Устанавливает callback для обработки черновиков."""
5354
global _on_draft_callback
5455
_on_draft_callback = callback
@@ -66,7 +67,7 @@ async def create_client(user_id: int, session_string: str) -> Client:
6667
return client
6768

6869

69-
def _pyrogram_task_exception_handler(loop, context):
70+
def _pyrogram_task_exception_handler(loop: asyncio.AbstractEventLoop, context: dict[str, Any]) -> None:
7071
"""Обработчик исключений в asyncio-задачах Pyrogram.
7172
7273
Pyrogram создаёт внутренние Task-и (handle_updates) которые могут
@@ -87,7 +88,7 @@ def _pyrogram_task_exception_handler(loop, context):
8788
loop.default_exception_handler(context)
8889

8990

90-
def _install_pyrogram_exception_handler(loop) -> None:
91+
def _install_pyrogram_exception_handler(loop: asyncio.AbstractEventLoop) -> None:
9192
"""Устанавливает обёртку над loop exception handler один раз."""
9293
if (
9394
_loop_handler_state["loop"] is loop
@@ -100,7 +101,7 @@ def _install_pyrogram_exception_handler(loop) -> None:
100101
_loop_handler_state["loop"] = loop
101102

102103

103-
def _restore_pyrogram_exception_handler(loop) -> None:
104+
def _restore_pyrogram_exception_handler(loop: asyncio.AbstractEventLoop) -> None:
104105
"""Восстанавливает предыдущий loop exception handler."""
105106
if _loop_handler_state["loop"] is not loop:
106107
return

dashboard/server.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,23 @@
55

66
from __future__ import annotations
77

8+
from typing import Callable
9+
810
import os
911
import pathlib
1012

1113
import jinja2
1214
from aiohttp import web
1315

14-
from config import STYLE_TO_EMOJI
16+
from config import EMOJI_TO_STYLE
1517
from dashboard import stats
1618
from dashboard.auth import DASHBOARD_KEY, check_auth, set_auth_cookie
1719
from database.users import get_dashboard_user_stats
1820

1921
TEMPLATE_DIR = pathlib.Path(__file__).parent / "templates"
2022

2123

22-
def _require_auth(handler): # type: ignore[type-arg]
24+
def _require_auth(handler: Callable) -> Callable:
2325
"""Декоратор: возвращает 401 если запрос не аутентифицирован."""
2426

2527
async def wrapper(request: web.Request) -> web.StreamResponse:
@@ -49,7 +51,8 @@ async def handle_dashboard(request: web.Request) -> web.Response:
4951
autoescape=False,
5052
)
5153
template = env.get_template("dashboard.html")
52-
html = template.render(style_to_emoji=STYLE_TO_EMOJI)
54+
style_emoji_pairs = [[style, emoji] for emoji, style in EMOJI_TO_STYLE.items()]
55+
html = template.render(style_emoji_pairs=style_emoji_pairs)
5356

5457
response = web.Response(text=html, content_type="text/html")
5558
set_auth_cookie(response)

dashboard/stats.py

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,11 @@ def capture_log(message: str) -> None:
9191
})
9292

9393

94+
# Стоимость последнего запроса (вычисляется в update_balance,
95+
# привязывается к модели в record_llm_request).
96+
_last_request_cost: float = 0.0
97+
98+
9499
# ---------------------------------------------------------------------------
95100
# API записи метрик
96101
# ---------------------------------------------------------------------------
@@ -114,14 +119,19 @@ def record_llm_request(
114119
# Per-model детали
115120
md = _stats.model_details.get(model)
116121
if md is None:
117-
md = {"count": 0, "tokens_in": 0, "tokens_out": 0, "reasoning": 0, "latency": 0.0}
122+
md = {"count": 0, "tokens_in": 0, "tokens_out": 0, "reasoning": 0, "latency": 0.0, "cost": 0.0}
118123
_stats.model_details[model] = md
119124
md["count"] += 1
120125
md["tokens_in"] += tokens_in
121126
md["tokens_out"] += tokens_out
122127
md["reasoning"] += reasoning_tokens
123128
md["latency"] += latency_s
124129

130+
# Привязываем стоимость запроса (вычисленную в update_balance)
131+
global _last_request_cost
132+
md["cost"] += _last_request_cost
133+
_last_request_cost = 0.0
134+
125135

126136
def record_llm_error() -> None:
127137
"""Записывает ошибку LLM-запроса."""
@@ -154,13 +164,19 @@ def update_balance(balance: float) -> None:
154164
"""Обновляет кэшированный prepaid-баланс x402gate.
155165
156166
Если баланс вырос — фиксирует пополнение.
167+
Если упал — сохраняет стоимость запроса для привязки к модели.
157168
"""
169+
global _last_request_cost
170+
_last_request_cost = 0.0
158171
if _stats.initial_balance is None:
159172
_stats.initial_balance = balance
160-
elif _stats.last_balance is not None and balance > _stats.last_balance:
161-
topup_amount = balance - _stats.last_balance
162-
_stats.topup_count += 1
163-
_stats.topup_total += topup_amount
173+
elif _stats.last_balance is not None:
174+
if balance > _stats.last_balance:
175+
topup_amount = balance - _stats.last_balance
176+
_stats.topup_count += 1
177+
_stats.topup_total += topup_amount
178+
elif balance < _stats.last_balance:
179+
_last_request_cost = _stats.last_balance - balance
164180
_stats.last_balance = balance
165181

166182

dashboard/templates/dashboard.html

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -347,9 +347,12 @@ <h1>🦉 DraftGuru <span style="color:#8b949e; font-weight:400">Dashboard</span>
347347
</div>
348348
</div>
349349

350-
<!-- Reply Styles -->
350+
<!-- Reply Styles & Activity -->
351351
<div class="summary" id="styles-summary">
352-
<!-- Injected by JS -->
352+
<div class="summary-item">Drafts: <strong id="sum-drafts">0</strong></div>
353+
<div class="summary-item">Auto-replies: <strong id="sum-autoreplies">0</strong></div>
354+
<div class="summary-item">Voice: <strong id="sum-voice">0</strong></div>
355+
<span id="style-items"><!-- Injected by JS --></span>
353356
</div>
354357

355358
<!-- Models -->
@@ -365,9 +368,6 @@ <h3>Models</h3>
365368

366369
<!-- LLM Summary -->
367370
<div class="summary" id="llm-summary">
368-
<div class="summary-item">Drafts: <strong id="sum-drafts">0</strong></div>
369-
<div class="summary-item">Auto-replies: <strong id="sum-autoreplies">0</strong></div>
370-
<div class="summary-item">Voice Transcriptions: <strong id="sum-voice">0</strong></div>
371371
<div class="summary-item">Avg latency: <strong id="sum-latency">0s</strong></div>
372372
<div class="summary-item">Avg cost/req: <strong id="sum-avg-cost"></strong></div>
373373
<div class="summary-item">Tokens in: <strong id="sum-tokens-in">0</strong></div>
@@ -401,6 +401,7 @@ <h3>Live Logs</h3>
401401
<a href="/api/logs">Logs JSON</a>
402402
</div>
403403

404+
<script id="style-emoji-data" type="application/json">{{ style_emoji_pairs | tojson }}</script>
404405
<script>
405406
let _lastLogs = [];
406407
let _logFilter = 'ALL';
@@ -477,6 +478,8 @@ <h3>Live Logs</h3>
477478
const d = details[m];
478479
const short = m.split('/').pop();
479480
const avgLat = d.count > 0 ? (d.latency / d.count).toFixed(2) : '0';
481+
const totalCost = d.cost ? '$' + d.cost.toFixed(4) : '—';
482+
const avgCostModel = d.count > 0 && d.cost ? '$' + (d.cost / d.count).toFixed(4) : '—';
480483
html += `<div class="card">
481484
<div class="card-header">
482485
<span class="card-name">${escHtml(short)}</span>
@@ -487,17 +490,19 @@ <h3>Live Logs</h3>
487490
<div class="metric">Tokens out<strong>${formatNumber(d.tokens_out)}</strong></div>
488491
<div class="metric">Reasoning<strong>${formatNumber(d.reasoning)}</strong></div>
489492
<div class="metric">Avg latency<strong>${avgLat}s</strong></div>
493+
<div class="metric">Total cost<strong>${totalCost}</strong></div>
494+
<div class="metric">Avg cost/req<strong>${avgCostModel}</strong></div>
490495
</div>
491496
</div>`;
492497
}
493498

494499
grid.innerHTML = html;
495500

496501
// Commands bar
502+
const allCommands = ['/start', '/connect', '/disconnect', '/status', '/settings', '/chats', '/poke'];
497503
const cmds = stats.commands || {};
498-
const cmdNames = Object.keys(cmds).sort((a, b) => cmds[b] - cmds[a]);
499504
const cmdBar = document.getElementById('commands-summary');
500-
cmdBar.innerHTML = cmdNames.map(c => `<div class="summary-item">${escHtml(c)}: <strong>${cmds[c]}</strong></div>`).join('');
505+
cmdBar.innerHTML = allCommands.map(c => `<div class="summary-item">${escHtml(c)}: <strong>${cmds[c] || 0}</strong></div>`).join('');
501506
}
502507

503508
function copyLogs() {
@@ -568,13 +573,10 @@ <h3>Live Logs</h3>
568573
}
569574

570575
// Reply styles bar
571-
const styleEmojis = {{ style_to_emoji | tojson }};
576+
const styleEmojiPairs = JSON.parse(document.getElementById('style-emoji-data').textContent);
572577
const draftStyles = stats.draft_styles || {};
573-
const styleBar = document.getElementById('styles-summary');
574-
const styleNames = Object.keys(styleEmojis);
575-
styleBar.innerHTML = styleNames.map(s => {
578+
document.getElementById('style-items').innerHTML = styleEmojiPairs.map(([s, emoji]) => {
576579
const count = draftStyles[s] || 0;
577-
const emoji = styleEmojis[s];
578580
return `<div class="summary-item">${emoji} ${s}: <strong>${count}</strong></div>`;
579581
}).join('');
580582

utils/utils.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
# utils/utils.py — Утилиты общего назначения (форматирование, декораторы, стили)
2+
13
import asyncio
24
from collections import defaultdict
35
from contextlib import asynccontextmanager

0 commit comments

Comments
 (0)