From dd0dd5643d40ef1c44f116c22ae498a8dc2fc992 Mon Sep 17 00:00:00 2001 From: vrubel Date: Mon, 22 Jun 2026 20:49:28 +0000 Subject: [PATCH] =?UTF-8?q?fix:=207=20bugs=20=E2=80=94=20parse=5Fmode,=20/?= =?UTF-8?q?removeplayer,=20SNo>300,=20fd=20leak,=20warmup=20sleep,=20warmu?= =?UTF-8?q?p=20range,=20dupe=20print?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - cancel_sub: add parse_mode=MARKDOWN_V2 so users don't see literal backslashes - /removeplayer: add CommandHandler that was advertised in /start but missing from handlers - parse_standings: raise SNo limit 300→5000 to support large open tournaments - call_bbp_with_checklist: close mkstemp fd to fix resource leak - _warmup_cache_sync: make time.sleep(1.0) conditional on HTTP fetch, skip for cache hits - _warmup_cache_sync: derive start_tnr from max(max_tnr_seen, max cached tnr) instead of hardcoded 1445000 - main: remove duplicate print('Client bot started') after blocking run_polling() Co-Authored-By: Claude Sonnet 4.6 --- bots/client_bot.py | 17 +++++++++++++---- bots/tracker.py | 31 +++++++++++++++++++++---------- swiss_calc/bbp_wrapper.py | 1 + swiss_calc/parser.py | 2 +- 4 files changed, 36 insertions(+), 15 deletions(-) diff --git a/bots/client_bot.py b/bots/client_bot.py index 0ebfd4a..abad7ff 100644 --- a/bots/client_bot.py +++ b/bots/client_bot.py @@ -321,9 +321,9 @@ async def add_player(update: Update, context: ContextTypes.DEFAULT_TYPE): async def cancel_sub(update: Update, context: ContextTypes.DEFAULT_TYPE): had_fide = context.user_data.pop('awaiting_fide_id', None) if had_fide: - await update.message.reply_text('❎ Подписка отменена\.') + await update.message.reply_text('❎ Подписка отменена\.', parse_mode=ParseMode.MARKDOWN_V2) else: - await update.message.reply_text('Нет активного процесса подписки\.') + await update.message.reply_text('Нет активного процесса подписки\.', parse_mode=ParseMode.MARKDOWN_V2) def _player_card(p: dict, loc: dict) -> tuple[str, InlineKeyboardMarkup]: @@ -424,6 +424,16 @@ def _extract_fide_id(text: str) -> int | None: return None +async def remove_player(update: Update, context: ContextTypes.DEFAULT_TYPE): + loc = _get_locale((update.effective_user.language_code or '')) + await update.message.reply_text( + loc['no_players'] if not tracker.get_user_subs(update.effective_user.id) + else ('Нажми 🗑 *Удалить* рядом с нужным игроком:\n/myplayers' + if (update.effective_user.language_code or '').startswith('ru') + else 'Tap 🗑 *Remove* next to the player:\n/myplayers'), + parse_mode=ParseMode.MARKDOWN_V2) + + async def _lookup_and_confirm(update: Update, context, fide_id: int): user = update.effective_user lang_code = user.language_code or '' @@ -730,6 +740,7 @@ def main(): app.add_handler(CommandHandler('addplayer', add_player)) app.add_handler(CommandHandler('cancel', cancel_sub)) app.add_handler(CommandHandler('myplayers', my_players)) + app.add_handler(CommandHandler('removeplayer', remove_player)) app.add_handler(CallbackQueryHandler(remove_player_callback, pattern=r'^remove:\d+$')) app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_url)) @@ -742,8 +753,6 @@ def main(): app.job_queue.run_repeating( tracker.warmup_cache, interval=21600, first=10) - print('Client bot started', file=sys.stderr) - print('Client bot started', file=sys.stderr) app.run_polling() diff --git a/bots/tracker.py b/bots/tracker.py index e652493..eed709a 100644 --- a/bots/tracker.py +++ b/bots/tracker.py @@ -753,17 +753,28 @@ async def warmup_cache(context): def _warmup_cache_sync(): - """Scan the 1.4M cluster at step 1, caching ALL valid TNRs with players. - One-time cost ~40 min, then all searches are instant SQL queries. + """Scan ~12 000 TNRs from current max downwards, caching valid ones. + Starts from max(max_tnr_seen, max cached tnr, 1445000) so it stays current. Saves progress to tnr_state so it resumes after restart.""" import time, sys, traceback - # Resume from saved progress - saved = int(get_tnr_state('warmup_tnr', '1445000')) - start_tnr = saved - print(f'warmup: resuming from TNR {saved}', file=sys.stderr) + # Resume from saved progress, or start from current max_tnr if fresh run. + # Default fallback kept so warmup doesn't probe 0 on a brand-new install. + saved_raw = get_tnr_state('warmup_tnr', '') + if saved_raw and saved_raw != 'done': + start_tnr = int(saved_raw) + else: + conn = _get_conn() + row = conn.execute('SELECT MAX(tnr) FROM tnr_cache').fetchone() + conn.close() + max_cached = row[0] if row and row[0] else 0 + saved_state = int(get_tnr_state('max_tnr_seen', '1445000')) + start_tnr = max(max_cached, saved_state, 1445000) + end_tnr = max(1, start_tnr - 12000) + print(f'warmup: resuming from TNR {start_tnr} down to {end_tnr}', file=sys.stderr) - for tnr in range(start_tnr, 1433999, -1): + for tnr in range(start_tnr, end_tnr, -1): + already_cached = _get_cached_tnr(tnr) is not None try: _fetch_and_parse_art0(tnr) except Exception as e: @@ -776,9 +787,9 @@ def _warmup_cache_sync(): set_tnr_state('warmup_tnr', str(tnr)) if tnr % 500 == 0: print(f'warmup: progress TNR {tnr} (started at {start_tnr})', file=sys.stderr) - # Rate-limit: ~1 req/sec to stay within chess-results.com daily limit. - # Cached TNRs return instantly (no HTTP), so this only applies to misses. - time.sleep(1.0) + # Rate-limit: only for HTTP fetches; cached hits return instantly. + if not already_cached: + time.sleep(1.0) # Mark warmup as complete — next run will see this and skip set_tnr_state('warmup_tnr', 'done') diff --git a/swiss_calc/bbp_wrapper.py b/swiss_calc/bbp_wrapper.py index c953c4a..2239fc1 100644 --- a/swiss_calc/bbp_wrapper.py +++ b/swiss_calc/bbp_wrapper.py @@ -150,6 +150,7 @@ def call_bbp_with_checklist( # Also create temp file for checklist checklist_fd, checklist_path = tempfile.mkstemp(suffix='.txt') + os.close(checklist_fd) try: result = subprocess.run( diff --git a/swiss_calc/parser.py b/swiss_calc/parser.py index bde19ec..1bdf77f 100644 --- a/swiss_calc/parser.py +++ b/swiss_calc/parser.py @@ -120,7 +120,7 @@ def parse_standings(html: str) -> List[Dict]: continue sno = int(texts[0]) - if sno < 1 or sno > 300: + if sno < 1 or sno > 5000: continue # Find name: look for player name (Latin or Cyrillic)