diff --git a/lua/cp/cache.lua b/lua/cp/cache.lua index 9448910..691dc82 100644 --- a/lua/cp/cache.lua +++ b/lua/cp/cache.lua @@ -266,7 +266,6 @@ end ---@param memory_mb number ---@param interactive boolean ---@param multi_test boolean ----@param precision number? function M.set_test_cases( platform, contest_id, @@ -468,7 +467,6 @@ function M.clear_credentials(platform) M.save() end ----@return nil function M.clear_all() cache_data = {} M.save() @@ -490,7 +488,6 @@ function M.get_data_pretty() return vim.inspect(cache_data) end ----@return table function M.get_raw_cache() return cache_data end diff --git a/lua/cp/commands/init.lua b/lua/cp/commands/init.lua index 9ea34a4..d28de00 100644 --- a/lua/cp/commands/init.lua +++ b/lua/cp/commands/init.lua @@ -257,7 +257,10 @@ local function parse_command(args) if vim.tbl_contains(platforms, first) then if #args == 1 then - return { type = 'action', action = 'pick', requires_context = false, platform = first } + return { + type = 'error', + message = 'Too few arguments - specify a contest.', + } elseif #args == 2 then if args[2] == 'login' or args[2] == 'logout' or args[2] == 'signup' then return { type = 'action', action = args[2], requires_context = false, platform = first } @@ -359,7 +362,6 @@ local function check_platform_enabled(platform) end --- Core logic for handling `:CP ...` commands ----@param opts { fargs: string[] } ---@return nil function M.handle_command(opts) local cmd = parse_command(opts.fargs) @@ -398,7 +400,7 @@ function M.handle_command(opts) setup.navigate_problem(-1, cmd.language) elseif cmd.action == 'pick' then local picker = require('cp.commands.picker') - picker.handle_pick_action(cmd.language, cmd.platform) + picker.handle_pick_action(cmd.language) elseif cmd.action == 'edit' then local edit = require('cp.ui.edit') edit.toggle_edit(cmd.test_index) diff --git a/lua/cp/commands/picker.lua b/lua/cp/commands/picker.lua index 04541d2..e01b680 100644 --- a/lua/cp/commands/picker.lua +++ b/lua/cp/commands/picker.lua @@ -5,9 +5,8 @@ local logger = require('cp.log') --- Dispatch `:CP pick` to appropriate picker ---@param language? string ----@param platform? string ---@return nil -function M.handle_pick_action(language, platform) +function M.handle_pick_action(language) local config = config_module.get_config() if not (config.ui and config.ui.picker) then @@ -55,7 +54,7 @@ function M.handle_pick_action(language, platform) picker = fzf_picker end - picker.pick(language, platform) + picker.pick(language) end return M diff --git a/lua/cp/config.lua b/lua/cp/config.lua index 74a40b7..bf2daac 100644 --- a/lua/cp/config.lua +++ b/lua/cp/config.lua @@ -531,12 +531,10 @@ end local current_config = nil ----@param config cp.Config function M.set_current_config(config) current_config = config end ----@return cp.Config function M.get_config() return current_config or M.defaults end diff --git a/lua/cp/constants.lua b/lua/cp/constants.lua index ec3c384..cf62b37 100644 --- a/lua/cp/constants.lua +++ b/lua/cp/constants.lua @@ -208,12 +208,6 @@ M.LANGUAGE_VERSIONS = { python = { python3 = 'PYTH 3', pypy3 = 'PYPY3' }, java = { java = 'JAVA' }, rust = { rust = 'rust' }, - c = { c = 'C' }, - go = { go = 'GO' }, - kotlin = { kotlin = 'KTLN' }, - javascript = { nodejs = 'NODEJS' }, - typescript = { typescript = 'TS' }, - csharp = { csharp = 'C#' }, }, } diff --git a/lua/cp/credentials.lua b/lua/cp/credentials.lua index 5328b8a..031b464 100644 --- a/lua/cp/credentials.lua +++ b/lua/cp/credentials.lua @@ -11,7 +11,6 @@ local STATUS_MESSAGES = { installing_browser = 'Installing browser...', } ----@param platform string? function M.login(platform) platform = platform or state.get_platform() if not platform then @@ -69,7 +68,6 @@ function M.login(platform) end) end ----@param platform string? function M.logout(platform) platform = platform or state.get_platform() if not platform then diff --git a/lua/cp/health.lua b/lua/cp/health.lua index f37a6c5..97595d6 100644 --- a/lua/cp/health.lua +++ b/lua/cp/health.lua @@ -66,7 +66,6 @@ local function check() end end ----@return nil function M.check() local version = require('cp.version') vim.health.start('cp.nvim health check ~') diff --git a/lua/cp/init.lua b/lua/cp/init.lua index aed24b6..cab16ad 100644 --- a/lua/cp/init.lua +++ b/lua/cp/init.lua @@ -38,13 +38,11 @@ function M.handle_command(opts) commands.handle_command(opts) end ----@return boolean function M.is_initialized() return initialized end ---@deprecated Use `vim.g.cp` instead ----@param user_config table? function M.setup(user_config) vim.deprecate('require("cp").setup()', 'vim.g.cp', 'v0.7.7', 'cp.nvim', false) diff --git a/lua/cp/pickers/fzf_lua.lua b/lua/cp/pickers/fzf_lua.lua index 378e9f5..65a600a 100644 --- a/lua/cp/pickers/fzf_lua.lua +++ b/lua/cp/pickers/fzf_lua.lua @@ -58,18 +58,11 @@ local function contest_picker(platform, refresh, language) }) end ----@param language? string ----@param platform? string -function M.pick(language, platform) - if platform then - contest_picker(platform, false, language) - return - end - +function M.pick(language) local fzf = require('fzf-lua') local platforms = picker_utils.get_platforms() - local entries = vim.tbl_map(function(p) - return p.display_name + local entries = vim.tbl_map(function(platform) + return platform.display_name end, platforms) return fzf.fzf_exec(entries, { @@ -81,16 +74,16 @@ function M.pick(language, platform) end local selected_name = selected[1] - local found = nil + local platform = nil for _, p in ipairs(platforms) do if p.display_name == selected_name then - found = p + platform = p break end end - if found then - contest_picker(found.id, false, language) + if platform then + contest_picker(platform.id, false, language) end end, }, diff --git a/lua/cp/pickers/telescope.lua b/lua/cp/pickers/telescope.lua index 9397f8b..890ae6a 100644 --- a/lua/cp/pickers/telescope.lua +++ b/lua/cp/pickers/telescope.lua @@ -64,14 +64,7 @@ local function contest_picker(opts, platform, refresh, language) :find() end ----@param language? string ----@param platform? string -function M.pick(language, platform) - if platform then - contest_picker({}, platform, false, language) - return - end - +function M.pick(language) local opts = {} local platforms = picker_utils.get_platforms() diff --git a/lua/cp/race.lua b/lua/cp/race.lua index 1728506..84333ed 100644 --- a/lua/cp/race.lua +++ b/lua/cp/race.lua @@ -134,9 +134,6 @@ local function race_try_setup(platform, contest_id, language, attempt, token) ) end ----@param platform string ----@param contest_id string ----@param language? string function M.start(platform, contest_id, language) if not platform or not vim.tbl_contains(constants.PLATFORMS, platform) then logger.log('Invalid platform', { level = vim.log.levels.ERROR }) @@ -255,7 +252,6 @@ function M.start(platform, contest_id, language) ) end ----@return nil function M.stop() local timer = race_state.timer if not timer then @@ -280,7 +276,6 @@ function M.stop() ) end ----@return { active: boolean, platform?: string, contest_id?: string, remaining_seconds?: integer } function M.status() if not race_state.timer or not race_state.start_time then return { active = false } diff --git a/lua/cp/runner/execute.lua b/lua/cp/runner/execute.lua index b24174a..41a7b51 100644 --- a/lua/cp/runner/execute.lua +++ b/lua/cp/runner/execute.lua @@ -33,9 +33,6 @@ local function substitute_template(cmd_template, substitutions) return out end ----@param cmd_template string[] ----@param substitutions SubstitutableCommand ----@return string[] function M.build_command(cmd_template, substitutions) return substitute_template(cmd_template, substitutions) end diff --git a/lua/cp/runner/run_render.lua b/lua/cp/runner/run_render.lua index 4922ddb..2dfb45b 100644 --- a/lua/cp/runner/run_render.lua +++ b/lua/cp/runner/run_render.lua @@ -376,7 +376,6 @@ function M.get_highlight_groups() } end ----@return nil function M.setup_highlights() local groups = M.get_highlight_groups() for name, opts in pairs(groups) do diff --git a/lua/cp/scraper.lua b/lua/cp/scraper.lua index 02f20b3..4004ba6 100644 --- a/lua/cp/scraper.lua +++ b/lua/cp/scraper.lua @@ -216,10 +216,6 @@ local function run_scraper(platform, subcommand, args, opts) end end ----@param platform string ----@param contest_id string ----@param callback fun(data: table)? ----@param on_error fun()? function M.scrape_contest_metadata(platform, contest_id, callback, on_error) run_scraper(platform, 'metadata', { contest_id }, { on_exit = function(result) @@ -257,8 +253,6 @@ function M.scrape_contest_metadata(platform, contest_id, callback, on_error) }) end ----@param platform string ----@return { contests: ContestSummary[], supports_countdown: boolean }? function M.scrape_contest_list(platform) local result = run_scraper(platform, 'contests', {}, { sync = true }) if not result or not result.success or not (result.data and result.data.contests) then @@ -336,10 +330,6 @@ function M.scrape_all_tests(platform, contest_id, callback, on_done) }) end ----@param platform string ----@param credentials table ----@param on_status fun(ev: table)? ----@param callback fun(result: table)? function M.login(platform, credentials, on_status, callback) local done = false run_scraper(platform, 'login', {}, { @@ -371,14 +361,6 @@ function M.login(platform, credentials, on_status, callback) }) end ----@param platform string ----@param contest_id string ----@param problem_id string ----@param language string ----@param source_file string ----@param credentials table ----@param on_status fun(ev: table)? ----@param callback fun(result: table)? function M.submit( platform, contest_id, diff --git a/lua/cp/stress.lua b/lua/cp/stress.lua index be3dcf0..caf9699 100644 --- a/lua/cp/stress.lua +++ b/lua/cp/stress.lua @@ -58,8 +58,6 @@ local function build_run_cmd(file) return './' .. file end ----@param generator_cmd? string ----@param brute_cmd? string function M.toggle(generator_cmd, brute_cmd) if state.get_active_panel() == 'stress' then if state.stress_buf and vim.api.nvim_buf_is_valid(state.stress_buf) then @@ -241,7 +239,6 @@ function M.toggle(generator_cmd, brute_cmd) end) end ----@return nil function M.cancel() if state.stress_buf and vim.api.nvim_buf_is_valid(state.stress_buf) then local job = vim.b[state.stress_buf].terminal_job_id diff --git a/lua/cp/submit.lua b/lua/cp/submit.lua index e7146fe..7302d1d 100644 --- a/lua/cp/submit.lua +++ b/lua/cp/submit.lua @@ -38,7 +38,6 @@ local function prompt_credentials(platform, callback) end) end ----@param opts { language?: string }? function M.submit(opts) local platform = state.get_platform() local contest_id = state.get_contest_id() @@ -65,13 +64,13 @@ function M.submit(opts) local eff = plat_effective and plat_effective[language] if eff then if eff.submit_id then - submit_language = eff.submit_id or submit_language + submit_language = eff.submit_id else local ver = eff.version or constants.DEFAULT_VERSIONS[language] if ver then local versions = (constants.LANGUAGE_VERSIONS[platform] or {})[language] if versions and versions[ver] then - submit_language = versions[ver] or submit_language + submit_language = versions[ver] end end end diff --git a/lua/cp/ui/edit.lua b/lua/cp/ui/edit.lua index d380944..93083aa 100644 --- a/lua/cp/ui/edit.lua +++ b/lua/cp/ui/edit.lua @@ -331,7 +331,6 @@ setup_keybindings = function(buf) }) end ----@param test_index? integer function M.toggle_edit(test_index) if edit_state then save_all_tests() diff --git a/lua/cp/ui/layouts.lua b/lua/cp/ui/layouts.lua index 97bc1f8..9b40f49 100644 --- a/lua/cp/ui/layouts.lua +++ b/lua/cp/ui/layouts.lua @@ -1,9 +1,3 @@ ----@class DiffLayout ----@field buffers integer[] ----@field windows integer[] ----@field mode string ----@field cleanup fun() - local M = {} local helpers = require('cp.helpers') @@ -177,11 +171,6 @@ local function create_single_layout(parent_win, content) } end ----@param mode string ----@param parent_win integer ----@param expected_content string ----@param actual_content string ----@return DiffLayout function M.create_diff_layout(mode, parent_win, expected_content, actual_content) if mode == 'single' then return create_single_layout(parent_win, actual_content) @@ -196,13 +185,6 @@ function M.create_diff_layout(mode, parent_win, expected_content, actual_content end end ----@param current_diff_layout DiffLayout? ----@param current_mode string? ----@param main_win integer ----@param run table ----@param config cp.Config ----@param setup_keybindings_for_buffer fun(buf: integer) ----@return DiffLayout?, string? function M.update_diff_panes( current_diff_layout, current_mode, diff --git a/lua/cp/ui/views.lua b/lua/cp/ui/views.lua index 22ff769..599bded 100644 --- a/lua/cp/ui/views.lua +++ b/lua/cp/ui/views.lua @@ -16,7 +16,6 @@ local current_diff_layout = nil local current_mode = nil local _run_gen = 0 ----@return nil function M.disable() local active_panel = state.get_active_panel() if not active_panel then @@ -352,7 +351,6 @@ local function create_window_layout(output_buf, input_buf) vim.api.nvim_set_current_win(solution_win) end ----@return nil function M.ensure_io_view() local platform, contest_id, problem_id = state.get_platform(), state.get_contest_id(), state.get_problem_id() @@ -600,9 +598,6 @@ local function render_io_view_results(io_state, test_indices, mode, combined_res utils.update_buffer_content(io_state.output_buf, output_lines, final_highlights, output_ns) end ----@param test_indices_arg integer[]? ----@param debug boolean? ----@param mode? string function M.run_io_view(test_indices_arg, debug, mode) _run_gen = _run_gen + 1 local gen = _run_gen @@ -759,12 +754,10 @@ function M.run_io_view(test_indices_arg, debug, mode) end) end ----@return nil function M.cancel_io_view() _run_gen = _run_gen + 1 end ----@return nil function M.cancel_interactive() if state.interactive_buf and vim.api.nvim_buf_is_valid(state.interactive_buf) then local job = vim.b[state.interactive_buf].terminal_job_id diff --git a/lua/cp/utils.lua b/lua/cp/utils.lua index 8325369..c27763f 100644 --- a/lua/cp/utils.lua +++ b/lua/cp/utils.lua @@ -314,7 +314,6 @@ end --- Configure the buffer with good defaults ---@param filetype? string ----@return integer function M.create_buffer_with_options(filetype) local buf = vim.api.nvim_create_buf(false, true) vim.api.nvim_set_option_value('bufhidden', 'hide', { buf = buf }) @@ -346,7 +345,6 @@ function M.update_buffer_content(bufnr, lines, highlights, namespace) end end ----@return boolean, string? function M.check_required_runtime() if is_windows() then return false, 'Windows is not supported' @@ -421,19 +419,16 @@ local function find_gnu_timeout() return _timeout_path, _timeout_reason end ----@return string? function M.timeout_path() local path = find_gnu_timeout() return path end ----@return { ok: boolean, path: string|nil, reason: string|nil } function M.timeout_capability() local path, reason = find_gnu_timeout() return { ok = path ~= nil, path = path, reason = reason } end ----@return string[] function M.cwd_executables() local uv = vim.uv local req = uv.fs_scandir('.') @@ -457,7 +452,6 @@ function M.cwd_executables() return out end ----@return nil function M.ensure_dirs() vim.system({ 'mkdir', '-p', 'build', 'io' }):wait() end diff --git a/scrapers/codechef.py b/scrapers/codechef.py index 7767b72..614e8c2 100644 --- a/scrapers/codechef.py +++ b/scrapers/codechef.py @@ -10,7 +10,7 @@ from typing import Any import httpx from .base import BaseScraper -from .timeouts import BROWSER_SESSION_TIMEOUT, HTTP_TIMEOUT +from .timeouts import BROWSER_NAV_TIMEOUT, BROWSER_SESSION_TIMEOUT, HTTP_TIMEOUT from .models import ( ContestListResult, ContestSummary, @@ -23,7 +23,6 @@ from .models import ( BASE_URL = "https://www.codechef.com" API_CONTESTS_ALL = "/api/list/contests/all" -API_CONTESTS_PAST = "/api/list/contests/past" API_CONTEST = "/api/contests/{contest_id}" API_PROBLEM = "/api/contests/{contest_id}/problems/{problem_id}" HEADERS = { @@ -33,19 +32,17 @@ CONNECTIONS = 8 _COOKIE_PATH = Path.home() / ".cache" / "cp-nvim" / "codechef-cookies.json" -_CC_CHECK_LOGIN_JS = "() => !!document.querySelector('a[href*=\"/users/\"]')" - -_CC_LANG_IDS: dict[str, str] = { - "C++": "42", - "PYTH 3": "116", - "JAVA": "10", - "PYPY3": "109", - "GO": "114", - "rust": "93", - "KTLN": "47", - "NODEJS": "56", - "TS": "35", -} +_CC_CHECK_LOGIN_JS = """() => { + const d = document.getElementById('__NEXT_DATA__'); + if (d) { + try { + const p = JSON.parse(d.textContent); + if (p?.props?.pageProps?.currentUser?.username) return true; + } catch(e) {} + } + return !!document.querySelector('a[href="/logout"]') || + !!document.querySelector('[class*="user-name"]'); +}""" async def fetch_json(client: httpx.AsyncClient, path: str) -> dict[str, Any]: @@ -74,19 +71,21 @@ def _login_headless_codechef(credentials: dict[str, str]) -> LoginResult: def check_login(page): nonlocal logged_in - logged_in = "dashboard" in page.url or page.evaluate(_CC_CHECK_LOGIN_JS) + logged_in = page.evaluate(_CC_CHECK_LOGIN_JS) def login_action(page): nonlocal login_error try: - page.locator('input[name="name"]').fill(credentials.get("username", "")) - page.locator('input[name="pass"]').fill(credentials.get("password", "")) - page.locator("input.cc-login-btn").click() - try: - page.wait_for_url(lambda url: "/login" not in url, timeout=3000) - except Exception: - login_error = "Login failed (bad credentials?)" - return + page.locator('input[type="email"], input[name="email"]').first.fill( + credentials.get("username", "") + ) + page.locator('input[type="password"], input[name="password"]').first.fill( + credentials.get("password", "") + ) + page.locator('button[type="submit"]').first.click() + page.wait_for_url( + lambda url: "/login" not in url, timeout=BROWSER_NAV_TIMEOUT + ) except Exception as e: login_error = str(e) @@ -156,19 +155,21 @@ def _submit_headless_codechef( def check_login(page): nonlocal logged_in - logged_in = "dashboard" in page.url or page.evaluate(_CC_CHECK_LOGIN_JS) + logged_in = page.evaluate(_CC_CHECK_LOGIN_JS) def login_action(page): nonlocal login_error try: - page.locator('input[name="name"]').fill(credentials.get("username", "")) - page.locator('input[name="pass"]').fill(credentials.get("password", "")) - page.locator("input.cc-login-btn").click() - try: - page.wait_for_url(lambda url: "/login" not in url, timeout=3000) - except Exception: - login_error = "Login failed (bad credentials?)" - return + page.locator('input[type="email"], input[name="email"]').first.fill( + credentials.get("username", "") + ) + page.locator('input[type="password"], input[name="password"]').first.fill( + credentials.get("password", "") + ) + page.locator('button[type="submit"]').first.click() + page.wait_for_url( + lambda url: "/login" not in url, timeout=BROWSER_NAV_TIMEOUT + ) except Exception as e: login_error = str(e) @@ -178,44 +179,54 @@ def _submit_headless_codechef( needs_relogin = True return try: - page.wait_for_timeout(2000) + selected = False + selects = page.locator("select") + for i in range(selects.count()): + try: + sel = selects.nth(i) + opts = sel.locator("option").all_inner_texts() + match = next( + (o for o in opts if language_id.lower() in o.lower()), None + ) + if match: + sel.select_option(label=match) + selected = True + break + except Exception: + pass - page.locator('[aria-haspopup="listbox"]').click() - page.wait_for_selector('[role="option"]', timeout=5000) - page.locator(f'[role="option"][data-value="{language_id}"]').click() - page.wait_for_timeout(2000) + if not selected: + lang_trigger = page.locator( + '[class*="language"] button, [data-testid*="language"] button' + ).first + lang_trigger.click() + page.wait_for_timeout(500) + page.locator( + f'[role="option"]:has-text("{language_id}"), ' + f'li:has-text("{language_id}")' + ).first.click() - page.locator(".ace_editor").click() - page.keyboard.press("Control+a") - page.wait_for_timeout(200) page.evaluate( """(code) => { - const textarea = document.querySelector('.ace_text-input'); - const dt = new DataTransfer(); - dt.setData('text/plain', code); - textarea.dispatchEvent(new ClipboardEvent('paste', { - clipboardData: dt, bubbles: true, cancelable: true - })); + if (typeof monaco !== 'undefined') { + const models = monaco.editor.getModels(); + if (models.length > 0) { models[0].setValue(code); return; } + } + const cm = document.querySelector('.CodeMirror'); + if (cm && cm.CodeMirror) { cm.CodeMirror.setValue(code); return; } + const ta = document.querySelector('textarea'); + if (ta) { ta.value = code; ta.dispatchEvent(new Event('input', {bubbles: true})); } }""", source_code, ) - page.wait_for_timeout(1000) - page.evaluate( - "() => document.getElementById('submit_btn').scrollIntoView({block:'center'})" + page.locator( + 'button[type="submit"]:has-text("Submit"), button:has-text("Submit Code")' + ).first.click() + page.wait_for_url( + lambda url: "/submit/" not in url or "submission" in url, + timeout=BROWSER_NAV_TIMEOUT * 2, ) - page.wait_for_timeout(200) - page.locator("#submit_btn").dispatch_event("click") - page.wait_for_timeout(3000) - - dialog_text = page.evaluate("""() => { - const d = document.querySelector('[role="dialog"], .swal2-popup'); - return d ? d.textContent.trim() : null; - }""") - if dialog_text and "not available for accepting solutions" in dialog_text: - submit_error = "PRACTICE_FALLBACK" - elif dialog_text: - submit_error = dialog_text except Exception as e: submit_error = str(e) @@ -241,12 +252,10 @@ def _submit_headless_codechef( ) print(json.dumps({"status": "submitting"}), flush=True) - submit_url = ( - f"{BASE_URL}/submit/{problem_id}" - if contest_id == "PRACTICE" - else f"{BASE_URL}/{contest_id}/submit/{problem_id}" + session.fetch( + f"{BASE_URL}/{contest_id}/submit/{problem_id}", + page_action=submit_action, ) - session.fetch(submit_url, page_action=submit_action) try: browser_cookies = session.context.cookies() @@ -266,20 +275,12 @@ def _submit_headless_codechef( _retried=True, ) - if submit_error == "PRACTICE_FALLBACK" and not _retried: - return _submit_headless_codechef( - "PRACTICE", - problem_id, - file_path, - language_id, - credentials, - _retried=True, - ) - if submit_error: return SubmitResult(success=False, error=submit_error) - return SubmitResult(success=True, error="", submission_id="") + return SubmitResult( + success=True, error="", submission_id="", verdict="submitted" + ) except Exception as e: return SubmitResult(success=False, error=str(e)) @@ -295,19 +296,12 @@ class CodeChefScraper(BaseScraper): data = await fetch_json( client, API_CONTEST.format(contest_id=contest_id) ) - problems_raw = data.get("problems") - if not problems_raw and isinstance(data.get("child_contests"), dict): - for div in ("div_4", "div_3", "div_2", "div_1"): - child = data["child_contests"].get(div, {}) - child_code = child.get("contest_code") - if child_code: - return await self.scrape_contest_metadata(child_code) - if not problems_raw: + if not data.get("problems"): return self._metadata_error( f"No problems found for contest {contest_id}" ) problems = [] - for problem_code, problem_data in problems_raw.items(): + for problem_code, problem_data in data["problems"].items(): if problem_data.get("category_name") == "main": problems.append( ProblemSummary( @@ -320,120 +314,42 @@ class CodeChefScraper(BaseScraper): error="", contest_id=contest_id, problems=problems, - url=f"{BASE_URL}/problems/%s", - contest_url=f"{BASE_URL}/{contest_id}", - standings_url=f"{BASE_URL}/{contest_id}/rankings", + url=f"{BASE_URL}/{contest_id}", ) except Exception as e: return self._metadata_error(f"Failed to fetch contest {contest_id}: {e}") async def scrape_contest_list(self) -> ContestListResult: - async with httpx.AsyncClient( - limits=httpx.Limits(max_connections=CONNECTIONS) - ) as client: + async with httpx.AsyncClient() as client: try: data = await fetch_json(client, API_CONTESTS_ALL) except httpx.HTTPStatusError as e: return self._contests_error(f"Failed to fetch contests: {e}") - - present = data.get("present_contests", []) - future = data.get("future_contests", []) - - async def fetch_past_page(offset: int) -> list[dict[str, Any]]: - r = await client.get( - BASE_URL + API_CONTESTS_PAST, - params={ - "sort_by": "START", - "sorting_order": "desc", - "offset": offset, - }, - headers=HEADERS, - timeout=HTTP_TIMEOUT, - ) - r.raise_for_status() - return r.json().get("contests", []) - - past: list[dict[str, Any]] = [] - offset = 0 - while True: - page = await fetch_past_page(offset) - past.extend( - c for c in page if re.match(r"^START\d+", c.get("contest_code", "")) - ) - if len(page) < 20: - break - offset += 20 - - raw: list[dict[str, Any]] = [] - seen_raw: set[str] = set() - for c in present + future + past: - code = c.get("contest_code", "") - if not code or code in seen_raw: - continue - seen_raw.add(code) - raw.append(c) - - sem = asyncio.Semaphore(CONNECTIONS) - - async def expand(c: dict[str, Any]) -> list[ContestSummary]: - code = c["contest_code"] - name = c.get("contest_name", code) - start_time: int | None = None - iso = c.get("contest_start_date_iso") - if iso: - try: - start_time = int(datetime.fromisoformat(iso).timestamp()) - except Exception: - pass - base_name = re.sub(r"\s*\(.*?\)\s*$", "", name).strip() - try: - async with sem: - detail = await fetch_json( - client, API_CONTEST.format(contest_id=code) - ) - children = detail.get("child_contests") - if children and isinstance(children, dict): - divs: list[ContestSummary] = [] - for div_key in ("div_1", "div_2", "div_3", "div_4"): - child = children.get(div_key) - if not child: - continue - child_code = child.get("contest_code") - div_num = child.get("div", {}).get( - "div_number", div_key[-1] - ) - if child_code: - display = f"{base_name} (Div. {div_num})" - divs.append( - ContestSummary( - id=child_code, - name=display, - display_name=display, - start_time=start_time, - ) - ) - if divs: - return divs - except Exception: - pass - return [ - ContestSummary( - id=code, name=name, display_name=name, start_time=start_time - ) - ] - - results = await asyncio.gather(*[expand(c) for c in raw]) - contests: list[ContestSummary] = [] seen: set[str] = set() - for group in results: - for entry in group: - if entry.id not in seen: - seen.add(entry.id) - contests.append(entry) - + for c in data.get("future_contests", []) + data.get("past_contests", []): + code = c.get("contest_code", "") + name = c.get("contest_name", code) + if not re.match(r"^START\d+$", code): + continue + if code in seen: + continue + seen.add(code) + start_time: int | None = None + iso = c.get("contest_start_date_iso") + if iso: + try: + dt = datetime.fromisoformat(iso) + start_time = int(dt.timestamp()) + except Exception: + pass + contests.append( + ContestSummary( + id=code, name=name, display_name=name, start_time=start_time + ) + ) if not contests: - return self._contests_error("No contests found") + return self._contests_error("No Starters contests found") return ContestListResult(success=True, error="", contests=contests) async def stream_tests_for_category_async(self, category_id: str) -> None: @@ -453,15 +369,6 @@ class CodeChefScraper(BaseScraper): ) return all_problems = contest_data.get("problems", {}) - if not all_problems and isinstance( - contest_data.get("child_contests"), dict - ): - for div in ("div_4", "div_3", "div_2", "div_1"): - child = contest_data["child_contests"].get(div, {}) - child_code = child.get("contest_code") - if child_code: - await self.stream_tests_for_category_async(child_code) - return if not all_problems: print( json.dumps( diff --git a/scrapers/cses.py b/scrapers/cses.py index 2d29689..1ddd292 100644 --- a/scrapers/cses.py +++ b/scrapers/cses.py @@ -9,7 +9,7 @@ from typing import Any import httpx from .base import BaseScraper, extract_precision -from .timeouts import HTTP_TIMEOUT +from .timeouts import HTTP_TIMEOUT, SUBMIT_POLL_TIMEOUT from .models import ( ContestListResult, ContestSummary, @@ -465,8 +465,40 @@ class CSESScraper(BaseScraper): err = r.text return self._submit_error(f"Submit request failed: {err}") - submission_id = str(r.json().get("id", "")) - return SubmitResult(success=True, error="", submission_id=submission_id) + info = r.json() + submission_id = str(info.get("id", "")) + + for _ in range(60): + await asyncio.sleep(2) + try: + r = await client.get( + f"{API_URL}/{SUBMIT_SCOPE}/submissions/{submission_id}", + params={"poll": "true"}, + headers={ + "X-Auth-Token": token, + **HEADERS, + }, + timeout=SUBMIT_POLL_TIMEOUT, + ) + if r.status_code == 200: + info = r.json() + if not info.get("pending", True): + verdict = info.get("result", "unknown") + return SubmitResult( + success=True, + error="", + submission_id=submission_id, + verdict=verdict, + ) + except Exception: + pass + + return SubmitResult( + success=True, + error="", + submission_id=submission_id, + verdict="submitted (poll timed out)", + ) if __name__ == "__main__": diff --git a/scrapers/language_ids.py b/scrapers/language_ids.py index f7e4e18..f69c4d9 100644 --- a/scrapers/language_ids.py +++ b/scrapers/language_ids.py @@ -130,14 +130,6 @@ LANGUAGE_IDS = { "python": "PYTH 3", "java": "JAVA", "rust": "rust", - "c": "C", - "go": "GO", - "kotlin": "KTLN", - "javascript": "NODEJS", - "typescript": "TS", - "csharp": "C#", - "php": "PHP", - "r": "R", }, } diff --git a/scrapers/timeouts.py b/scrapers/timeouts.py index e4ff583..715b07f 100644 --- a/scrapers/timeouts.py +++ b/scrapers/timeouts.py @@ -12,3 +12,5 @@ BROWSER_SUBMIT_NAV_TIMEOUT["codeforces"] = BROWSER_NAV_TIMEOUT * 2 BROWSER_TURNSTILE_POLL = 5000 BROWSER_ELEMENT_WAIT = 10000 BROWSER_SETTLE_DELAY = 500 + +SUBMIT_POLL_TIMEOUT = 30.0 diff --git a/tests/conftest.py b/tests/conftest.py index 5d281d3..f1248a5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -221,9 +221,6 @@ def run_scraper_offline(fixture_text): if "/api/list/contests/all" in url: data = json.loads(fixture_text("codechef/contests.json")) return MockResponse(data) - if "/api/list/contests/past" in url: - data = json.loads(fixture_text("codechef/contests_past.json")) - return MockResponse(data) if "/api/contests/START" in url and "/problems/" not in url: contest_id = url.rstrip("/").split("/")[-1] try: diff --git a/tests/fixtures/codechef/contests_past.json b/tests/fixtures/codechef/contests_past.json deleted file mode 100644 index 449b185..0000000 --- a/tests/fixtures/codechef/contests_past.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "status": "success", - "message": "past contests list", - "contests": [ - { - "contest_code": "START209D", - "contest_name": "Starters 209 Div 4", - "contest_start_date_iso": "2025-01-01T10:30:00+05:30" - }, - { - "contest_code": "START208", - "contest_name": "Starters 208", - "contest_start_date_iso": "2024-12-25T10:30:00+05:30" - } - ] -}