Compare commits
9 commits
c7f2af16d4
...
4622c8c373
| Author | SHA1 | Date | |
|---|---|---|---|
| 4622c8c373 | |||
| 904bd9c18c | |||
| 1d8f01a2a7 | |||
| 4b6b51b8df | |||
| 3b3da9ab9b | |||
| 52fc2f76f4 | |||
| 645417f86b | |||
| 1f0414de8e | |||
| fc2331c242 |
26 changed files with 336 additions and 158 deletions
|
|
@ -266,6 +266,7 @@ end
|
||||||
---@param memory_mb number
|
---@param memory_mb number
|
||||||
---@param interactive boolean
|
---@param interactive boolean
|
||||||
---@param multi_test boolean
|
---@param multi_test boolean
|
||||||
|
---@param precision number?
|
||||||
function M.set_test_cases(
|
function M.set_test_cases(
|
||||||
platform,
|
platform,
|
||||||
contest_id,
|
contest_id,
|
||||||
|
|
@ -467,6 +468,7 @@ function M.clear_credentials(platform)
|
||||||
M.save()
|
M.save()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@return nil
|
||||||
function M.clear_all()
|
function M.clear_all()
|
||||||
cache_data = {}
|
cache_data = {}
|
||||||
M.save()
|
M.save()
|
||||||
|
|
@ -488,6 +490,7 @@ function M.get_data_pretty()
|
||||||
return vim.inspect(cache_data)
|
return vim.inspect(cache_data)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@return table
|
||||||
function M.get_raw_cache()
|
function M.get_raw_cache()
|
||||||
return cache_data
|
return cache_data
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -257,10 +257,7 @@ local function parse_command(args)
|
||||||
|
|
||||||
if vim.tbl_contains(platforms, first) then
|
if vim.tbl_contains(platforms, first) then
|
||||||
if #args == 1 then
|
if #args == 1 then
|
||||||
return {
|
return { type = 'action', action = 'pick', requires_context = false, platform = first }
|
||||||
type = 'error',
|
|
||||||
message = 'Too few arguments - specify a contest.',
|
|
||||||
}
|
|
||||||
elseif #args == 2 then
|
elseif #args == 2 then
|
||||||
if args[2] == 'login' or args[2] == 'logout' or args[2] == 'signup' 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 }
|
return { type = 'action', action = args[2], requires_context = false, platform = first }
|
||||||
|
|
@ -362,6 +359,7 @@ local function check_platform_enabled(platform)
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Core logic for handling `:CP ...` commands
|
--- Core logic for handling `:CP ...` commands
|
||||||
|
---@param opts { fargs: string[] }
|
||||||
---@return nil
|
---@return nil
|
||||||
function M.handle_command(opts)
|
function M.handle_command(opts)
|
||||||
local cmd = parse_command(opts.fargs)
|
local cmd = parse_command(opts.fargs)
|
||||||
|
|
@ -400,7 +398,7 @@ function M.handle_command(opts)
|
||||||
setup.navigate_problem(-1, cmd.language)
|
setup.navigate_problem(-1, cmd.language)
|
||||||
elseif cmd.action == 'pick' then
|
elseif cmd.action == 'pick' then
|
||||||
local picker = require('cp.commands.picker')
|
local picker = require('cp.commands.picker')
|
||||||
picker.handle_pick_action(cmd.language)
|
picker.handle_pick_action(cmd.language, cmd.platform)
|
||||||
elseif cmd.action == 'edit' then
|
elseif cmd.action == 'edit' then
|
||||||
local edit = require('cp.ui.edit')
|
local edit = require('cp.ui.edit')
|
||||||
edit.toggle_edit(cmd.test_index)
|
edit.toggle_edit(cmd.test_index)
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,9 @@ local logger = require('cp.log')
|
||||||
|
|
||||||
--- Dispatch `:CP pick` to appropriate picker
|
--- Dispatch `:CP pick` to appropriate picker
|
||||||
---@param language? string
|
---@param language? string
|
||||||
|
---@param platform? string
|
||||||
---@return nil
|
---@return nil
|
||||||
function M.handle_pick_action(language)
|
function M.handle_pick_action(language, platform)
|
||||||
local config = config_module.get_config()
|
local config = config_module.get_config()
|
||||||
|
|
||||||
if not (config.ui and config.ui.picker) then
|
if not (config.ui and config.ui.picker) then
|
||||||
|
|
@ -54,7 +55,7 @@ function M.handle_pick_action(language)
|
||||||
picker = fzf_picker
|
picker = fzf_picker
|
||||||
end
|
end
|
||||||
|
|
||||||
picker.pick(language)
|
picker.pick(language, platform)
|
||||||
end
|
end
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|
|
||||||
|
|
@ -531,10 +531,12 @@ end
|
||||||
|
|
||||||
local current_config = nil
|
local current_config = nil
|
||||||
|
|
||||||
|
---@param config cp.Config
|
||||||
function M.set_current_config(config)
|
function M.set_current_config(config)
|
||||||
current_config = config
|
current_config = config
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@return cp.Config
|
||||||
function M.get_config()
|
function M.get_config()
|
||||||
return current_config or M.defaults
|
return current_config or M.defaults
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -208,6 +208,12 @@ M.LANGUAGE_VERSIONS = {
|
||||||
python = { python3 = 'PYTH 3', pypy3 = 'PYPY3' },
|
python = { python3 = 'PYTH 3', pypy3 = 'PYPY3' },
|
||||||
java = { java = 'JAVA' },
|
java = { java = 'JAVA' },
|
||||||
rust = { rust = 'rust' },
|
rust = { rust = 'rust' },
|
||||||
|
c = { c = 'C' },
|
||||||
|
go = { go = 'GO' },
|
||||||
|
kotlin = { kotlin = 'KTLN' },
|
||||||
|
javascript = { nodejs = 'NODEJS' },
|
||||||
|
typescript = { typescript = 'TS' },
|
||||||
|
csharp = { csharp = 'C#' },
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ local STATUS_MESSAGES = {
|
||||||
installing_browser = 'Installing browser...',
|
installing_browser = 'Installing browser...',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
---@param platform string?
|
||||||
function M.login(platform)
|
function M.login(platform)
|
||||||
platform = platform or state.get_platform()
|
platform = platform or state.get_platform()
|
||||||
if not platform then
|
if not platform then
|
||||||
|
|
@ -68,6 +69,7 @@ function M.login(platform)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@param platform string?
|
||||||
function M.logout(platform)
|
function M.logout(platform)
|
||||||
platform = platform or state.get_platform()
|
platform = platform or state.get_platform()
|
||||||
if not platform then
|
if not platform then
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,7 @@ local function check()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@return nil
|
||||||
function M.check()
|
function M.check()
|
||||||
local version = require('cp.version')
|
local version = require('cp.version')
|
||||||
vim.health.start('cp.nvim health check ~')
|
vim.health.start('cp.nvim health check ~')
|
||||||
|
|
|
||||||
|
|
@ -38,11 +38,13 @@ function M.handle_command(opts)
|
||||||
commands.handle_command(opts)
|
commands.handle_command(opts)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@return boolean
|
||||||
function M.is_initialized()
|
function M.is_initialized()
|
||||||
return initialized
|
return initialized
|
||||||
end
|
end
|
||||||
|
|
||||||
---@deprecated Use `vim.g.cp` instead
|
---@deprecated Use `vim.g.cp` instead
|
||||||
|
---@param user_config table?
|
||||||
function M.setup(user_config)
|
function M.setup(user_config)
|
||||||
vim.deprecate('require("cp").setup()', 'vim.g.cp', 'v0.7.7', 'cp.nvim', false)
|
vim.deprecate('require("cp").setup()', 'vim.g.cp', 'v0.7.7', 'cp.nvim', false)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -58,11 +58,18 @@ local function contest_picker(platform, refresh, language)
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
function M.pick(language)
|
---@param language? string
|
||||||
|
---@param platform? string
|
||||||
|
function M.pick(language, platform)
|
||||||
|
if platform then
|
||||||
|
contest_picker(platform, false, language)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
local fzf = require('fzf-lua')
|
local fzf = require('fzf-lua')
|
||||||
local platforms = picker_utils.get_platforms()
|
local platforms = picker_utils.get_platforms()
|
||||||
local entries = vim.tbl_map(function(platform)
|
local entries = vim.tbl_map(function(p)
|
||||||
return platform.display_name
|
return p.display_name
|
||||||
end, platforms)
|
end, platforms)
|
||||||
|
|
||||||
return fzf.fzf_exec(entries, {
|
return fzf.fzf_exec(entries, {
|
||||||
|
|
@ -74,16 +81,16 @@ function M.pick(language)
|
||||||
end
|
end
|
||||||
|
|
||||||
local selected_name = selected[1]
|
local selected_name = selected[1]
|
||||||
local platform = nil
|
local found = nil
|
||||||
for _, p in ipairs(platforms) do
|
for _, p in ipairs(platforms) do
|
||||||
if p.display_name == selected_name then
|
if p.display_name == selected_name then
|
||||||
platform = p
|
found = p
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if platform then
|
if found then
|
||||||
contest_picker(platform.id, false, language)
|
contest_picker(found.id, false, language)
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,14 @@ local function contest_picker(opts, platform, refresh, language)
|
||||||
:find()
|
:find()
|
||||||
end
|
end
|
||||||
|
|
||||||
function M.pick(language)
|
---@param language? string
|
||||||
|
---@param platform? string
|
||||||
|
function M.pick(language, platform)
|
||||||
|
if platform then
|
||||||
|
contest_picker({}, platform, false, language)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
local opts = {}
|
local opts = {}
|
||||||
local platforms = picker_utils.get_platforms()
|
local platforms = picker_utils.get_platforms()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -134,6 +134,9 @@ local function race_try_setup(platform, contest_id, language, attempt, token)
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@param platform string
|
||||||
|
---@param contest_id string
|
||||||
|
---@param language? string
|
||||||
function M.start(platform, contest_id, language)
|
function M.start(platform, contest_id, language)
|
||||||
if not platform or not vim.tbl_contains(constants.PLATFORMS, platform) then
|
if not platform or not vim.tbl_contains(constants.PLATFORMS, platform) then
|
||||||
logger.log('Invalid platform', { level = vim.log.levels.ERROR })
|
logger.log('Invalid platform', { level = vim.log.levels.ERROR })
|
||||||
|
|
@ -252,6 +255,7 @@ function M.start(platform, contest_id, language)
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@return nil
|
||||||
function M.stop()
|
function M.stop()
|
||||||
local timer = race_state.timer
|
local timer = race_state.timer
|
||||||
if not timer then
|
if not timer then
|
||||||
|
|
@ -276,6 +280,7 @@ function M.stop()
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@return { active: boolean, platform?: string, contest_id?: string, remaining_seconds?: integer }
|
||||||
function M.status()
|
function M.status()
|
||||||
if not race_state.timer or not race_state.start_time then
|
if not race_state.timer or not race_state.start_time then
|
||||||
return { active = false }
|
return { active = false }
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,9 @@ local function substitute_template(cmd_template, substitutions)
|
||||||
return out
|
return out
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@param cmd_template string[]
|
||||||
|
---@param substitutions SubstitutableCommand
|
||||||
|
---@return string[]
|
||||||
function M.build_command(cmd_template, substitutions)
|
function M.build_command(cmd_template, substitutions)
|
||||||
return substitute_template(cmd_template, substitutions)
|
return substitute_template(cmd_template, substitutions)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -376,6 +376,7 @@ function M.get_highlight_groups()
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@return nil
|
||||||
function M.setup_highlights()
|
function M.setup_highlights()
|
||||||
local groups = M.get_highlight_groups()
|
local groups = M.get_highlight_groups()
|
||||||
for name, opts in pairs(groups) do
|
for name, opts in pairs(groups) do
|
||||||
|
|
|
||||||
|
|
@ -216,6 +216,10 @@ local function run_scraper(platform, subcommand, args, opts)
|
||||||
end
|
end
|
||||||
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)
|
function M.scrape_contest_metadata(platform, contest_id, callback, on_error)
|
||||||
run_scraper(platform, 'metadata', { contest_id }, {
|
run_scraper(platform, 'metadata', { contest_id }, {
|
||||||
on_exit = function(result)
|
on_exit = function(result)
|
||||||
|
|
@ -253,6 +257,8 @@ function M.scrape_contest_metadata(platform, contest_id, callback, on_error)
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@param platform string
|
||||||
|
---@return { contests: ContestSummary[], supports_countdown: boolean }?
|
||||||
function M.scrape_contest_list(platform)
|
function M.scrape_contest_list(platform)
|
||||||
local result = run_scraper(platform, 'contests', {}, { sync = true })
|
local result = run_scraper(platform, 'contests', {}, { sync = true })
|
||||||
if not result or not result.success or not (result.data and result.data.contests) then
|
if not result or not result.success or not (result.data and result.data.contests) then
|
||||||
|
|
@ -330,6 +336,10 @@ function M.scrape_all_tests(platform, contest_id, callback, on_done)
|
||||||
})
|
})
|
||||||
end
|
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)
|
function M.login(platform, credentials, on_status, callback)
|
||||||
local done = false
|
local done = false
|
||||||
run_scraper(platform, 'login', {}, {
|
run_scraper(platform, 'login', {}, {
|
||||||
|
|
@ -361,6 +371,14 @@ function M.login(platform, credentials, on_status, callback)
|
||||||
})
|
})
|
||||||
end
|
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(
|
function M.submit(
|
||||||
platform,
|
platform,
|
||||||
contest_id,
|
contest_id,
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,8 @@ local function build_run_cmd(file)
|
||||||
return './' .. file
|
return './' .. file
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@param generator_cmd? string
|
||||||
|
---@param brute_cmd? string
|
||||||
function M.toggle(generator_cmd, brute_cmd)
|
function M.toggle(generator_cmd, brute_cmd)
|
||||||
if state.get_active_panel() == 'stress' then
|
if state.get_active_panel() == 'stress' then
|
||||||
if state.stress_buf and vim.api.nvim_buf_is_valid(state.stress_buf) then
|
if state.stress_buf and vim.api.nvim_buf_is_valid(state.stress_buf) then
|
||||||
|
|
@ -239,6 +241,7 @@ function M.toggle(generator_cmd, brute_cmd)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@return nil
|
||||||
function M.cancel()
|
function M.cancel()
|
||||||
if state.stress_buf and vim.api.nvim_buf_is_valid(state.stress_buf) then
|
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
|
local job = vim.b[state.stress_buf].terminal_job_id
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ local function prompt_credentials(platform, callback)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@param opts { language?: string }?
|
||||||
function M.submit(opts)
|
function M.submit(opts)
|
||||||
local platform = state.get_platform()
|
local platform = state.get_platform()
|
||||||
local contest_id = state.get_contest_id()
|
local contest_id = state.get_contest_id()
|
||||||
|
|
@ -64,13 +65,13 @@ function M.submit(opts)
|
||||||
local eff = plat_effective and plat_effective[language]
|
local eff = plat_effective and plat_effective[language]
|
||||||
if eff then
|
if eff then
|
||||||
if eff.submit_id then
|
if eff.submit_id then
|
||||||
submit_language = eff.submit_id
|
submit_language = eff.submit_id or submit_language
|
||||||
else
|
else
|
||||||
local ver = eff.version or constants.DEFAULT_VERSIONS[language]
|
local ver = eff.version or constants.DEFAULT_VERSIONS[language]
|
||||||
if ver then
|
if ver then
|
||||||
local versions = (constants.LANGUAGE_VERSIONS[platform] or {})[language]
|
local versions = (constants.LANGUAGE_VERSIONS[platform] or {})[language]
|
||||||
if versions and versions[ver] then
|
if versions and versions[ver] then
|
||||||
submit_language = versions[ver]
|
submit_language = versions[ver] or submit_language
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -331,6 +331,7 @@ setup_keybindings = function(buf)
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@param test_index? integer
|
||||||
function M.toggle_edit(test_index)
|
function M.toggle_edit(test_index)
|
||||||
if edit_state then
|
if edit_state then
|
||||||
save_all_tests()
|
save_all_tests()
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
|
---@class DiffLayout
|
||||||
|
---@field buffers integer[]
|
||||||
|
---@field windows integer[]
|
||||||
|
---@field mode string
|
||||||
|
---@field cleanup fun()
|
||||||
|
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
local helpers = require('cp.helpers')
|
local helpers = require('cp.helpers')
|
||||||
|
|
@ -171,6 +177,11 @@ local function create_single_layout(parent_win, content)
|
||||||
}
|
}
|
||||||
end
|
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)
|
function M.create_diff_layout(mode, parent_win, expected_content, actual_content)
|
||||||
if mode == 'single' then
|
if mode == 'single' then
|
||||||
return create_single_layout(parent_win, actual_content)
|
return create_single_layout(parent_win, actual_content)
|
||||||
|
|
@ -185,6 +196,13 @@ function M.create_diff_layout(mode, parent_win, expected_content, actual_content
|
||||||
end
|
end
|
||||||
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(
|
function M.update_diff_panes(
|
||||||
current_diff_layout,
|
current_diff_layout,
|
||||||
current_mode,
|
current_mode,
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ local current_diff_layout = nil
|
||||||
local current_mode = nil
|
local current_mode = nil
|
||||||
local _run_gen = 0
|
local _run_gen = 0
|
||||||
|
|
||||||
|
---@return nil
|
||||||
function M.disable()
|
function M.disable()
|
||||||
local active_panel = state.get_active_panel()
|
local active_panel = state.get_active_panel()
|
||||||
if not active_panel then
|
if not active_panel then
|
||||||
|
|
@ -351,6 +352,7 @@ local function create_window_layout(output_buf, input_buf)
|
||||||
vim.api.nvim_set_current_win(solution_win)
|
vim.api.nvim_set_current_win(solution_win)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@return nil
|
||||||
function M.ensure_io_view()
|
function M.ensure_io_view()
|
||||||
local platform, contest_id, problem_id =
|
local platform, contest_id, problem_id =
|
||||||
state.get_platform(), state.get_contest_id(), state.get_problem_id()
|
state.get_platform(), state.get_contest_id(), state.get_problem_id()
|
||||||
|
|
@ -598,6 +600,9 @@ 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)
|
utils.update_buffer_content(io_state.output_buf, output_lines, final_highlights, output_ns)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@param test_indices_arg integer[]?
|
||||||
|
---@param debug boolean?
|
||||||
|
---@param mode? string
|
||||||
function M.run_io_view(test_indices_arg, debug, mode)
|
function M.run_io_view(test_indices_arg, debug, mode)
|
||||||
_run_gen = _run_gen + 1
|
_run_gen = _run_gen + 1
|
||||||
local gen = _run_gen
|
local gen = _run_gen
|
||||||
|
|
@ -754,10 +759,12 @@ function M.run_io_view(test_indices_arg, debug, mode)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@return nil
|
||||||
function M.cancel_io_view()
|
function M.cancel_io_view()
|
||||||
_run_gen = _run_gen + 1
|
_run_gen = _run_gen + 1
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@return nil
|
||||||
function M.cancel_interactive()
|
function M.cancel_interactive()
|
||||||
if state.interactive_buf and vim.api.nvim_buf_is_valid(state.interactive_buf) then
|
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
|
local job = vim.b[state.interactive_buf].terminal_job_id
|
||||||
|
|
|
||||||
|
|
@ -314,6 +314,7 @@ end
|
||||||
|
|
||||||
--- Configure the buffer with good defaults
|
--- Configure the buffer with good defaults
|
||||||
---@param filetype? string
|
---@param filetype? string
|
||||||
|
---@return integer
|
||||||
function M.create_buffer_with_options(filetype)
|
function M.create_buffer_with_options(filetype)
|
||||||
local buf = vim.api.nvim_create_buf(false, true)
|
local buf = vim.api.nvim_create_buf(false, true)
|
||||||
vim.api.nvim_set_option_value('bufhidden', 'hide', { buf = buf })
|
vim.api.nvim_set_option_value('bufhidden', 'hide', { buf = buf })
|
||||||
|
|
@ -345,6 +346,7 @@ function M.update_buffer_content(bufnr, lines, highlights, namespace)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@return boolean, string?
|
||||||
function M.check_required_runtime()
|
function M.check_required_runtime()
|
||||||
if is_windows() then
|
if is_windows() then
|
||||||
return false, 'Windows is not supported'
|
return false, 'Windows is not supported'
|
||||||
|
|
@ -419,16 +421,19 @@ local function find_gnu_timeout()
|
||||||
return _timeout_path, _timeout_reason
|
return _timeout_path, _timeout_reason
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@return string?
|
||||||
function M.timeout_path()
|
function M.timeout_path()
|
||||||
local path = find_gnu_timeout()
|
local path = find_gnu_timeout()
|
||||||
return path
|
return path
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@return { ok: boolean, path: string|nil, reason: string|nil }
|
||||||
function M.timeout_capability()
|
function M.timeout_capability()
|
||||||
local path, reason = find_gnu_timeout()
|
local path, reason = find_gnu_timeout()
|
||||||
return { ok = path ~= nil, path = path, reason = reason }
|
return { ok = path ~= nil, path = path, reason = reason }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@return string[]
|
||||||
function M.cwd_executables()
|
function M.cwd_executables()
|
||||||
local uv = vim.uv
|
local uv = vim.uv
|
||||||
local req = uv.fs_scandir('.')
|
local req = uv.fs_scandir('.')
|
||||||
|
|
@ -452,6 +457,7 @@ function M.cwd_executables()
|
||||||
return out
|
return out
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@return nil
|
||||||
function M.ensure_dirs()
|
function M.ensure_dirs()
|
||||||
vim.system({ 'mkdir', '-p', 'build', 'io' }):wait()
|
vim.system({ 'mkdir', '-p', 'build', 'io' }):wait()
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ from typing import Any
|
||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
from .base import BaseScraper
|
from .base import BaseScraper
|
||||||
from .timeouts import BROWSER_NAV_TIMEOUT, BROWSER_SESSION_TIMEOUT, HTTP_TIMEOUT
|
from .timeouts import BROWSER_SESSION_TIMEOUT, HTTP_TIMEOUT
|
||||||
from .models import (
|
from .models import (
|
||||||
ContestListResult,
|
ContestListResult,
|
||||||
ContestSummary,
|
ContestSummary,
|
||||||
|
|
@ -23,6 +23,7 @@ from .models import (
|
||||||
|
|
||||||
BASE_URL = "https://www.codechef.com"
|
BASE_URL = "https://www.codechef.com"
|
||||||
API_CONTESTS_ALL = "/api/list/contests/all"
|
API_CONTESTS_ALL = "/api/list/contests/all"
|
||||||
|
API_CONTESTS_PAST = "/api/list/contests/past"
|
||||||
API_CONTEST = "/api/contests/{contest_id}"
|
API_CONTEST = "/api/contests/{contest_id}"
|
||||||
API_PROBLEM = "/api/contests/{contest_id}/problems/{problem_id}"
|
API_PROBLEM = "/api/contests/{contest_id}/problems/{problem_id}"
|
||||||
HEADERS = {
|
HEADERS = {
|
||||||
|
|
@ -32,17 +33,19 @@ CONNECTIONS = 8
|
||||||
|
|
||||||
_COOKIE_PATH = Path.home() / ".cache" / "cp-nvim" / "codechef-cookies.json"
|
_COOKIE_PATH = Path.home() / ".cache" / "cp-nvim" / "codechef-cookies.json"
|
||||||
|
|
||||||
_CC_CHECK_LOGIN_JS = """() => {
|
_CC_CHECK_LOGIN_JS = "() => !!document.querySelector('a[href*=\"/users/\"]')"
|
||||||
const d = document.getElementById('__NEXT_DATA__');
|
|
||||||
if (d) {
|
_CC_LANG_IDS: dict[str, str] = {
|
||||||
try {
|
"C++": "42",
|
||||||
const p = JSON.parse(d.textContent);
|
"PYTH 3": "116",
|
||||||
if (p?.props?.pageProps?.currentUser?.username) return true;
|
"JAVA": "10",
|
||||||
} catch(e) {}
|
"PYPY3": "109",
|
||||||
|
"GO": "114",
|
||||||
|
"rust": "93",
|
||||||
|
"KTLN": "47",
|
||||||
|
"NODEJS": "56",
|
||||||
|
"TS": "35",
|
||||||
}
|
}
|
||||||
return !!document.querySelector('a[href="/logout"]') ||
|
|
||||||
!!document.querySelector('[class*="user-name"]');
|
|
||||||
}"""
|
|
||||||
|
|
||||||
|
|
||||||
async def fetch_json(client: httpx.AsyncClient, path: str) -> dict[str, Any]:
|
async def fetch_json(client: httpx.AsyncClient, path: str) -> dict[str, Any]:
|
||||||
|
|
@ -71,21 +74,19 @@ def _login_headless_codechef(credentials: dict[str, str]) -> LoginResult:
|
||||||
|
|
||||||
def check_login(page):
|
def check_login(page):
|
||||||
nonlocal logged_in
|
nonlocal logged_in
|
||||||
logged_in = page.evaluate(_CC_CHECK_LOGIN_JS)
|
logged_in = "dashboard" in page.url or page.evaluate(_CC_CHECK_LOGIN_JS)
|
||||||
|
|
||||||
def login_action(page):
|
def login_action(page):
|
||||||
nonlocal login_error
|
nonlocal login_error
|
||||||
try:
|
try:
|
||||||
page.locator('input[type="email"], input[name="email"]').first.fill(
|
page.locator('input[name="name"]').fill(credentials.get("username", ""))
|
||||||
credentials.get("username", "")
|
page.locator('input[name="pass"]').fill(credentials.get("password", ""))
|
||||||
)
|
page.locator("input.cc-login-btn").click()
|
||||||
page.locator('input[type="password"], input[name="password"]').first.fill(
|
try:
|
||||||
credentials.get("password", "")
|
page.wait_for_url(lambda url: "/login" not in url, timeout=3000)
|
||||||
)
|
except Exception:
|
||||||
page.locator('button[type="submit"]').first.click()
|
login_error = "Login failed (bad credentials?)"
|
||||||
page.wait_for_url(
|
return
|
||||||
lambda url: "/login" not in url, timeout=BROWSER_NAV_TIMEOUT
|
|
||||||
)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
login_error = str(e)
|
login_error = str(e)
|
||||||
|
|
||||||
|
|
@ -155,21 +156,19 @@ def _submit_headless_codechef(
|
||||||
|
|
||||||
def check_login(page):
|
def check_login(page):
|
||||||
nonlocal logged_in
|
nonlocal logged_in
|
||||||
logged_in = page.evaluate(_CC_CHECK_LOGIN_JS)
|
logged_in = "dashboard" in page.url or page.evaluate(_CC_CHECK_LOGIN_JS)
|
||||||
|
|
||||||
def login_action(page):
|
def login_action(page):
|
||||||
nonlocal login_error
|
nonlocal login_error
|
||||||
try:
|
try:
|
||||||
page.locator('input[type="email"], input[name="email"]').first.fill(
|
page.locator('input[name="name"]').fill(credentials.get("username", ""))
|
||||||
credentials.get("username", "")
|
page.locator('input[name="pass"]').fill(credentials.get("password", ""))
|
||||||
)
|
page.locator("input.cc-login-btn").click()
|
||||||
page.locator('input[type="password"], input[name="password"]').first.fill(
|
try:
|
||||||
credentials.get("password", "")
|
page.wait_for_url(lambda url: "/login" not in url, timeout=3000)
|
||||||
)
|
except Exception:
|
||||||
page.locator('button[type="submit"]').first.click()
|
login_error = "Login failed (bad credentials?)"
|
||||||
page.wait_for_url(
|
return
|
||||||
lambda url: "/login" not in url, timeout=BROWSER_NAV_TIMEOUT
|
|
||||||
)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
login_error = str(e)
|
login_error = str(e)
|
||||||
|
|
||||||
|
|
@ -179,54 +178,44 @@ def _submit_headless_codechef(
|
||||||
needs_relogin = True
|
needs_relogin = True
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
selected = False
|
page.wait_for_timeout(2000)
|
||||||
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
|
|
||||||
|
|
||||||
if not selected:
|
page.locator('[aria-haspopup="listbox"]').click()
|
||||||
lang_trigger = page.locator(
|
page.wait_for_selector('[role="option"]', timeout=5000)
|
||||||
'[class*="language"] button, [data-testid*="language"] button'
|
page.locator(f'[role="option"][data-value="{language_id}"]').click()
|
||||||
).first
|
page.wait_for_timeout(2000)
|
||||||
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(
|
page.evaluate(
|
||||||
"""(code) => {
|
"""(code) => {
|
||||||
if (typeof monaco !== 'undefined') {
|
const textarea = document.querySelector('.ace_text-input');
|
||||||
const models = monaco.editor.getModels();
|
const dt = new DataTransfer();
|
||||||
if (models.length > 0) { models[0].setValue(code); return; }
|
dt.setData('text/plain', code);
|
||||||
}
|
textarea.dispatchEvent(new ClipboardEvent('paste', {
|
||||||
const cm = document.querySelector('.CodeMirror');
|
clipboardData: dt, bubbles: true, cancelable: true
|
||||||
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,
|
source_code,
|
||||||
)
|
)
|
||||||
|
page.wait_for_timeout(1000)
|
||||||
|
|
||||||
page.locator(
|
page.evaluate(
|
||||||
'button[type="submit"]:has-text("Submit"), button:has-text("Submit Code")'
|
"() => document.getElementById('submit_btn').scrollIntoView({block:'center'})"
|
||||||
).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:
|
except Exception as e:
|
||||||
submit_error = str(e)
|
submit_error = str(e)
|
||||||
|
|
||||||
|
|
@ -252,10 +241,12 @@ def _submit_headless_codechef(
|
||||||
)
|
)
|
||||||
|
|
||||||
print(json.dumps({"status": "submitting"}), flush=True)
|
print(json.dumps({"status": "submitting"}), flush=True)
|
||||||
session.fetch(
|
submit_url = (
|
||||||
f"{BASE_URL}/{contest_id}/submit/{problem_id}",
|
f"{BASE_URL}/submit/{problem_id}"
|
||||||
page_action=submit_action,
|
if contest_id == "PRACTICE"
|
||||||
|
else f"{BASE_URL}/{contest_id}/submit/{problem_id}"
|
||||||
)
|
)
|
||||||
|
session.fetch(submit_url, page_action=submit_action)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
browser_cookies = session.context.cookies()
|
browser_cookies = session.context.cookies()
|
||||||
|
|
@ -275,12 +266,20 @@ def _submit_headless_codechef(
|
||||||
_retried=True,
|
_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:
|
if submit_error:
|
||||||
return SubmitResult(success=False, error=submit_error)
|
return SubmitResult(success=False, error=submit_error)
|
||||||
|
|
||||||
return SubmitResult(
|
return SubmitResult(success=True, error="", submission_id="")
|
||||||
success=True, error="", submission_id="", verdict="submitted"
|
|
||||||
)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return SubmitResult(success=False, error=str(e))
|
return SubmitResult(success=False, error=str(e))
|
||||||
|
|
||||||
|
|
@ -296,12 +295,19 @@ class CodeChefScraper(BaseScraper):
|
||||||
data = await fetch_json(
|
data = await fetch_json(
|
||||||
client, API_CONTEST.format(contest_id=contest_id)
|
client, API_CONTEST.format(contest_id=contest_id)
|
||||||
)
|
)
|
||||||
if not data.get("problems"):
|
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:
|
||||||
return self._metadata_error(
|
return self._metadata_error(
|
||||||
f"No problems found for contest {contest_id}"
|
f"No problems found for contest {contest_id}"
|
||||||
)
|
)
|
||||||
problems = []
|
problems = []
|
||||||
for problem_code, problem_data in data["problems"].items():
|
for problem_code, problem_data in problems_raw.items():
|
||||||
if problem_data.get("category_name") == "main":
|
if problem_data.get("category_name") == "main":
|
||||||
problems.append(
|
problems.append(
|
||||||
ProblemSummary(
|
ProblemSummary(
|
||||||
|
|
@ -314,42 +320,120 @@ class CodeChefScraper(BaseScraper):
|
||||||
error="",
|
error="",
|
||||||
contest_id=contest_id,
|
contest_id=contest_id,
|
||||||
problems=problems,
|
problems=problems,
|
||||||
url=f"{BASE_URL}/{contest_id}",
|
url=f"{BASE_URL}/problems/%s",
|
||||||
|
contest_url=f"{BASE_URL}/{contest_id}",
|
||||||
|
standings_url=f"{BASE_URL}/{contest_id}/rankings",
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return self._metadata_error(f"Failed to fetch contest {contest_id}: {e}")
|
return self._metadata_error(f"Failed to fetch contest {contest_id}: {e}")
|
||||||
|
|
||||||
async def scrape_contest_list(self) -> ContestListResult:
|
async def scrape_contest_list(self) -> ContestListResult:
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient(
|
||||||
|
limits=httpx.Limits(max_connections=CONNECTIONS)
|
||||||
|
) as client:
|
||||||
try:
|
try:
|
||||||
data = await fetch_json(client, API_CONTESTS_ALL)
|
data = await fetch_json(client, API_CONTESTS_ALL)
|
||||||
except httpx.HTTPStatusError as e:
|
except httpx.HTTPStatusError as e:
|
||||||
return self._contests_error(f"Failed to fetch contests: {e}")
|
return self._contests_error(f"Failed to fetch contests: {e}")
|
||||||
contests: list[ContestSummary] = []
|
|
||||||
seen: set[str] = set()
|
present = data.get("present_contests", [])
|
||||||
for c in data.get("future_contests", []) + data.get("past_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", "")
|
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)
|
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
|
start_time: int | None = None
|
||||||
iso = c.get("contest_start_date_iso")
|
iso = c.get("contest_start_date_iso")
|
||||||
if iso:
|
if iso:
|
||||||
try:
|
try:
|
||||||
dt = datetime.fromisoformat(iso)
|
start_time = int(datetime.fromisoformat(iso).timestamp())
|
||||||
start_time = int(dt.timestamp())
|
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
contests.append(
|
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(
|
ContestSummary(
|
||||||
id=code, name=name, display_name=name, start_time=start_time
|
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)
|
||||||
|
|
||||||
if not contests:
|
if not contests:
|
||||||
return self._contests_error("No Starters contests found")
|
return self._contests_error("No contests found")
|
||||||
return ContestListResult(success=True, error="", contests=contests)
|
return ContestListResult(success=True, error="", contests=contests)
|
||||||
|
|
||||||
async def stream_tests_for_category_async(self, category_id: str) -> None:
|
async def stream_tests_for_category_async(self, category_id: str) -> None:
|
||||||
|
|
@ -369,6 +453,15 @@ class CodeChefScraper(BaseScraper):
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
all_problems = contest_data.get("problems", {})
|
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:
|
if not all_problems:
|
||||||
print(
|
print(
|
||||||
json.dumps(
|
json.dumps(
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ from typing import Any
|
||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
from .base import BaseScraper, extract_precision
|
from .base import BaseScraper, extract_precision
|
||||||
from .timeouts import HTTP_TIMEOUT, SUBMIT_POLL_TIMEOUT
|
from .timeouts import HTTP_TIMEOUT
|
||||||
from .models import (
|
from .models import (
|
||||||
ContestListResult,
|
ContestListResult,
|
||||||
ContestSummary,
|
ContestSummary,
|
||||||
|
|
@ -465,40 +465,8 @@ class CSESScraper(BaseScraper):
|
||||||
err = r.text
|
err = r.text
|
||||||
return self._submit_error(f"Submit request failed: {err}")
|
return self._submit_error(f"Submit request failed: {err}")
|
||||||
|
|
||||||
info = r.json()
|
submission_id = str(r.json().get("id", ""))
|
||||||
submission_id = str(info.get("id", ""))
|
return SubmitResult(success=True, error="", submission_id=submission_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__":
|
if __name__ == "__main__":
|
||||||
|
|
|
||||||
|
|
@ -130,6 +130,14 @@ LANGUAGE_IDS = {
|
||||||
"python": "PYTH 3",
|
"python": "PYTH 3",
|
||||||
"java": "JAVA",
|
"java": "JAVA",
|
||||||
"rust": "rust",
|
"rust": "rust",
|
||||||
|
"c": "C",
|
||||||
|
"go": "GO",
|
||||||
|
"kotlin": "KTLN",
|
||||||
|
"javascript": "NODEJS",
|
||||||
|
"typescript": "TS",
|
||||||
|
"csharp": "C#",
|
||||||
|
"php": "PHP",
|
||||||
|
"r": "R",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,5 +12,3 @@ BROWSER_SUBMIT_NAV_TIMEOUT["codeforces"] = BROWSER_NAV_TIMEOUT * 2
|
||||||
BROWSER_TURNSTILE_POLL = 5000
|
BROWSER_TURNSTILE_POLL = 5000
|
||||||
BROWSER_ELEMENT_WAIT = 10000
|
BROWSER_ELEMENT_WAIT = 10000
|
||||||
BROWSER_SETTLE_DELAY = 500
|
BROWSER_SETTLE_DELAY = 500
|
||||||
|
|
||||||
SUBMIT_POLL_TIMEOUT = 30.0
|
|
||||||
|
|
|
||||||
|
|
@ -221,6 +221,9 @@ def run_scraper_offline(fixture_text):
|
||||||
if "/api/list/contests/all" in url:
|
if "/api/list/contests/all" in url:
|
||||||
data = json.loads(fixture_text("codechef/contests.json"))
|
data = json.loads(fixture_text("codechef/contests.json"))
|
||||||
return MockResponse(data)
|
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:
|
if "/api/contests/START" in url and "/problems/" not in url:
|
||||||
contest_id = url.rstrip("/").split("/")[-1]
|
contest_id = url.rstrip("/").split("/")[-1]
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
16
tests/fixtures/codechef/contests_past.json
vendored
Normal file
16
tests/fixtures/codechef/contests_past.json
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue