Compare commits

..

No commits in common. "031d5314365a1a7175f51a659102e05549583bbc" and "27d7a4e6b571b83c36a6881b8201b543298a7f18" have entirely different histories.

12 changed files with 152 additions and 289 deletions

View file

@ -10,14 +10,14 @@ https://github.com/user-attachments/assets/e81d8dfb-578f-4a79-9989-210164fc0148
## Features ## Features
- **Multi-platform support**: AtCoder, CodeChef, Codeforces, USACO, CSES, Kattis - **Multi-platform support**: AtCoder, CodeChef, Codeforces, USACO, CSES, Kattis
- **Online Judge Integration**: Submit problems and view contest standings - **Automatic problem setup**: Scrape test cases and metadata in seconds
- **Live Contest Support**: Participate in real-time contests - **Dual view modes**: Lightweight I/O view for quick feedback, full panel for
- **Automatic setup**: Scrape test cases and metadata in seconds detailed analysis
- **Streamlined Editing**: Configure coding view, edit test cases, stress-test - **Test case management**: Quickly view, edit, add, & remove test cases
solutions, run interactive problems, and more - **Rich test output**: 256 color ANSI support for compiler errors and program
- **Rich output**: 256 color ANSI support for compiler errors and program output output
- **Language agnosticism**: Configure with any language - **Language agnostic**: Works with any language
- **Security**: Passwords go untampered - **Diff viewer**: Compare expected vs actual output with 3 diff modes
## Installation ## Installation
@ -37,31 +37,30 @@ luarocks install cp.nvim
## Quick Start ## Quick Start
1. Find a contest: cp.nvim follows a simple principle: **solve locally, submit remotely**.
``` ### Basic Usage
:CP pick
```
2. View the problem: 1. Find a contest or problem
2. Set up contests locally
``` ```
:CP open :CP codeforces 1848
``` ```
3. Code and test 3. Code and test
``` ```
:CP run :CP run
``` ```
4. Navigate between problems 4. Navigate between problems
``` ```
:CP next :CP next
:CP prev :CP prev
:CP e1 :CP e1
``` ```
5. Debug and edit test cases 5. Debug and edit test cases
@ -70,17 +69,7 @@ luarocks install cp.nvim
:CP panel --debug :CP panel --debug
``` ```
6. Submit: 5. Submit on the original website
```
:CP submit
```
7. View contest standings:
```
:CP open standings
```
## Documentation ## Documentation
@ -89,7 +78,7 @@ luarocks install cp.nvim
``` ```
See See
[my config](https://github.com/barrettruth/nix/blob/5d0ede3668eb7f5ad2b4475267fc0458f9fa4527/config/nvim/lua/plugins/dev.lua#L165) [my config](https://github.com/barrettruth/dots/blob/main/.config/nvim/lua/plugins/cp.lua)
for the setup in the video shown above. for the setup in the video shown above.
## Motivation ## Motivation

View file

@ -49,8 +49,7 @@ REQUIREMENTS *cp-requirements*
- Neovim 0.10.0+ - Neovim 0.10.0+
- Unix-like operating system - Unix-like operating system
- (Optional) git 1.7.9+ (credential storage) - uv package manager (https://docs.astral.sh/uv/)
- (Optional) uv package manager (https://docs.astral.sh/uv/)
============================================================================== ==============================================================================
SETUP *cp-setup* SETUP *cp-setup*
@ -999,15 +998,8 @@ CREDENTIALS *cp-credentials*
Manage stored login credentials for platform submission. Manage stored login credentials for platform submission.
Credentials are stored via git-credential(1), using whatever credential Credentials are stored under _credentials in the main cache file
helper is configured in your git config (macOS Keychain, libsecret, (stdpath('data')/cp-nvim.json). Use :CP cache read to inspect them.
credential-store, etc.). Git is required. Cookie files
(~/.cache/cp-nvim/cookies.json) are unaffected.
To inspect stored credentials:
>sh
printf 'protocol=https\nhost=cses.fi\n\n' | git credential fill
<
:CP login [platform] :CP login [platform]
Set or update credentials for a platform. Prompts for username Set or update credentials for a platform. Prompts for username

View file

@ -42,10 +42,13 @@
local M = {} local M = {}
local CACHE_VERSION = 2
local cache_file = vim.fn.stdpath('data') .. '/cp-nvim.json' local cache_file = vim.fn.stdpath('data') .. '/cp-nvim.json'
local cache_data = {} local cache_data = {}
local loaded = false local loaded = false
--- Load the cache from disk if not done already
---@return nil ---@return nil
function M.load() function M.load()
if loaded then if loaded then
@ -53,11 +56,8 @@ function M.load()
end end
if vim.fn.filereadable(cache_file) == 0 then if vim.fn.filereadable(cache_file) == 0 then
vim.fn.mkdir(vim.fn.fnamemodify(cache_file, ':h'), 'p') vim.fn.writefile({}, cache_file)
local tmpfile = vim.fn.tempname() vim.fn.setfperm(cache_file, 'rw-------')
vim.fn.writefile({}, tmpfile)
vim.fn.setfperm(tmpfile, 'rw-------')
vim.uv.fs_rename(tmpfile, cache_file)
loaded = true loaded = true
return return
end end
@ -70,7 +70,26 @@ function M.load()
end end
local ok, decoded = pcall(vim.json.decode, table.concat(content, '\n')) local ok, decoded = pcall(vim.json.decode, table.concat(content, '\n'))
if ok and type(decoded) == 'table' then if not ok then
cache_data = {}
M.save()
loaded = true
return
end
if decoded._version == 1 then
local old_creds = decoded._credentials
decoded._credentials = nil
if old_creds then
for platform, creds in pairs(old_creds) do
decoded[platform] = decoded[platform] or {}
decoded[platform]._credentials = creds
end
end
decoded._version = CACHE_VERSION
cache_data = decoded
M.save()
elseif decoded._version == CACHE_VERSION then
cache_data = decoded cache_data = decoded
else else
cache_data = {} cache_data = {}
@ -79,16 +98,17 @@ function M.load()
loaded = true loaded = true
end end
--- Save the cache to disk, overwriting existing contents
---@return nil ---@return nil
function M.save() function M.save()
vim.schedule(function() vim.schedule(function()
vim.fn.mkdir(vim.fn.fnamemodify(cache_file, ':h'), 'p') vim.fn.mkdir(vim.fn.fnamemodify(cache_file, ':h'), 'p')
cache_data._version = CACHE_VERSION
local encoded = vim.json.encode(cache_data) local encoded = vim.json.encode(cache_data)
local lines = vim.split(encoded, '\n') local lines = vim.split(encoded, '\n')
local tmpfile = vim.fn.tempname() vim.fn.writefile(lines, cache_file)
vim.fn.writefile(lines, tmpfile) vim.fn.setfperm(cache_file, 'rw-------')
vim.fn.setfperm(tmpfile, 'rw-------')
vim.uv.fs_rename(tmpfile, cache_file)
end) end)
end end
@ -425,6 +445,31 @@ function M.get_contest_display_name(platform, contest_id)
return cache_data[platform][contest_id].display_name return cache_data[platform][contest_id].display_name
end end
---@param platform string
---@return table?
function M.get_credentials(platform)
if not cache_data[platform] then
return nil
end
return cache_data[platform]._credentials
end
---@param platform string
---@param creds table
function M.set_credentials(platform, creds)
cache_data[platform] = cache_data[platform] or {}
cache_data[platform]._credentials = creds
M.save()
end
---@param platform string
function M.clear_credentials(platform)
if cache_data[platform] then
cache_data[platform]._credentials = nil
end
M.save()
end
---@return nil ---@return nil
function M.clear_all() function M.clear_all()
cache_data = {} cache_data = {}

View file

@ -1,7 +1,7 @@
local M = {} local M = {}
local cache = require('cp.cache')
local constants = require('cp.constants') local constants = require('cp.constants')
local git_credential = require('cp.git_credential')
local logger = require('cp.log') local logger = require('cp.log')
local state = require('cp.state') local state = require('cp.state')
@ -40,14 +40,14 @@ local function prompt_and_login(platform, display)
end, function(result) end, function(result)
vim.schedule(function() vim.schedule(function()
if result.success then if result.success then
git_credential.store(platform, credentials) cache.set_credentials(platform, credentials)
logger.log( logger.log(
display .. ' login successful', display .. ' login successful',
{ level = vim.log.levels.INFO, override = true } { level = vim.log.levels.INFO, override = true }
) )
else else
local err = result.error or 'unknown error' local err = result.error or 'unknown error'
git_credential.reject(platform, credentials) cache.clear_credentials(platform)
logger.log( logger.log(
display .. ' login failed: ' .. (constants.LOGIN_ERRORS[err] or err), display .. ' login failed: ' .. (constants.LOGIN_ERRORS[err] or err),
{ level = vim.log.levels.ERROR } { level = vim.log.levels.ERROR }
@ -71,17 +71,10 @@ function M.login(platform)
return return
end end
if not git_credential.has_helper() then
logger.log(
'No git credential helper configured. See :help cp-credentials',
{ level = vim.log.levels.ERROR }
)
return
end
local display = constants.PLATFORM_DISPLAY_NAMES[platform] or platform local display = constants.PLATFORM_DISPLAY_NAMES[platform] or platform
local existing = git_credential.get(platform) or {} cache.load()
local existing = cache.get_credentials(platform) or {}
if existing.username and existing.password then if existing.username and existing.password then
local scraper = require('cp.scraper') local scraper = require('cp.scraper')
@ -98,7 +91,7 @@ function M.login(platform)
{ level = vim.log.levels.INFO, override = true } { level = vim.log.levels.INFO, override = true }
) )
else else
git_credential.reject(platform, existing) cache.clear_credentials(platform)
prompt_and_login(platform, display) prompt_and_login(platform, display)
end end
end) end)
@ -119,28 +112,16 @@ function M.logout(platform)
) )
return return
end end
if not git_credential.has_helper() then
logger.log(
'No git credential helper configured. See :help cp-credentials',
{ level = vim.log.levels.ERROR }
)
return
end
local display = constants.PLATFORM_DISPLAY_NAMES[platform] or platform local display = constants.PLATFORM_DISPLAY_NAMES[platform] or platform
local existing = git_credential.get(platform) cache.load()
if existing then cache.clear_credentials(platform)
git_credential.reject(platform, existing)
end
local cookie_file = constants.COOKIE_FILE local cookie_file = constants.COOKIE_FILE
if vim.fn.filereadable(cookie_file) == 1 then if vim.fn.filereadable(cookie_file) == 1 then
local ok, data = pcall(vim.fn.json_decode, vim.fn.readfile(cookie_file, 'b')) local ok, data = pcall(vim.fn.json_decode, vim.fn.readfile(cookie_file, 'b'))
if ok and type(data) == 'table' then if ok and type(data) == 'table' then
data[platform] = nil data[platform] = nil
local tmpfile = vim.fn.tempname() vim.fn.writefile({ vim.fn.json_encode(data) }, cookie_file)
vim.fn.writefile({ vim.fn.json_encode(data) }, tmpfile) vim.fn.setfperm(cookie_file, 'rw-------')
vim.fn.setfperm(tmpfile, 'rw-------')
vim.uv.fs_rename(tmpfile, cookie_file)
end end
end end
logger.log(display .. ' credentials cleared', { level = vim.log.levels.INFO, override = true }) logger.log(display .. ' credentials cleared', { level = vim.log.levels.INFO, override = true })

View file

@ -1,107 +0,0 @@
---@class cp.Credentials
---@field username string
---@field password string
local M = {}
local HOSTS = {
atcoder = 'atcoder.jp',
codechef = 'www.codechef.com',
codeforces = 'codeforces.com',
cses = 'cses.fi',
kattis = 'open.kattis.com',
usaco = 'usaco.org',
}
local _helper_checked = false
local _helper_ok = false
---@return boolean
function M.has_helper()
if not _helper_checked then
local r = vim
.system({ 'git', 'config', 'credential.helper' }, { text = true, timeout = 5000 })
:wait()
_helper_ok = r.code == 0 and r.stdout ~= nil and vim.trim(r.stdout) ~= ''
_helper_checked = true
end
return _helper_ok
end
---@param host string
---@param extra? table<string, string>
---@return string
local function _build_input(host, extra)
local lines = { 'protocol=https', 'host=' .. host }
if extra then
for k, v in pairs(extra) do
table.insert(lines, k .. '=' .. v)
end
end
table.insert(lines, '')
table.insert(lines, '')
return table.concat(lines, '\n')
end
---@param stdout string
---@return table<string, string>
local function _parse_output(stdout)
local result = {}
for line in stdout:gmatch('[^\n]+') do
local k, v = line:match('^(%S+)=(.+)$')
if k and v then
result[k] = v
end
end
return result
end
---@param platform string
---@return cp.Credentials?
function M.get(platform)
local host = HOSTS[platform]
if not host then
return nil
end
local input = _build_input(host)
local obj = vim
.system({ 'git', 'credential', 'fill' }, { stdin = input, text = true, timeout = 5000 })
:wait()
if obj.code ~= 0 then
return nil
end
local parsed = _parse_output(obj.stdout or '')
if not parsed.username or not parsed.password then
return nil
end
return { username = parsed.username, password = parsed.password }
end
---@param platform string
---@param creds cp.Credentials
function M.store(platform, creds)
local host = HOSTS[platform]
if not host then
return
end
local input = _build_input(host, { username = creds.username, password = creds.password })
vim.system({ 'git', 'credential', 'approve' }, { stdin = input, text = true }):wait()
end
---@param platform string
---@param creds cp.Credentials
function M.reject(platform, creds)
local host = HOSTS[platform]
if not host or not creds then
return
end
local input = _build_input(host, { username = creds.username, password = creds.password })
vim.system({ 'git', 'credential', 'reject' }, { stdin = input, text = true }):wait()
end
return M

View file

@ -5,12 +5,12 @@ local utils = require('cp.utils')
local function check() local function check()
vim.health.start('cp.nvim [required] ~') vim.health.start('cp.nvim [required] ~')
local nvim_ver = vim.version() utils.setup_python_env()
local nvim_str = ('%d.%d.%d'):format(nvim_ver.major, nvim_ver.minor, nvim_ver.patch)
if vim.fn.has('nvim-0.10.0') == 1 then if vim.fn.has('nvim-0.10.0') == 1 then
vim.health.ok('Neovim >= 0.10.0: ' .. nvim_str) vim.health.ok('Neovim 0.10.0+ detected')
else else
vim.health.error('Neovim >= 0.10.0 required, found ' .. nvim_str) vim.health.error('cp.nvim requires Neovim 0.10.0+')
end end
local uname = vim.uv.os_uname() local uname = vim.uv.os_uname()
@ -18,24 +18,6 @@ local function check()
vim.health.error('Windows is not supported') vim.health.error('Windows is not supported')
end end
local time_cap = utils.time_capability()
if time_cap.ok then
vim.health.ok('GNU time found: ' .. time_cap.path)
else
vim.health.error('GNU time not found: ' .. (time_cap.reason or ''))
end
local timeout_cap = utils.timeout_capability()
if timeout_cap.ok then
vim.health.ok('GNU timeout found: ' .. timeout_cap.path)
else
vim.health.error('GNU timeout not found: ' .. (timeout_cap.reason or ''))
end
vim.health.start('cp.nvim [optional] ~')
utils.setup_python_env()
if utils.is_nix_build() then if utils.is_nix_build() then
local source = utils.is_nix_discovered() and 'runtime discovery' or 'flake install' local source = utils.is_nix_discovered() and 'runtime discovery' or 'flake install'
vim.health.ok('Nix Python environment detected (' .. source .. ')') vim.health.ok('Nix Python environment detected (' .. source .. ')')
@ -69,30 +51,18 @@ local function check()
end end
end end
if vim.fn.executable('git') == 1 then local time_cap = utils.time_capability()
local r = vim.system({ 'git', '--version' }, { text = true }):wait() if time_cap.ok then
if r.code == 0 then vim.health.ok('GNU time found: ' .. time_cap.path)
local major, minor, patch = r.stdout:match('(%d+)%.(%d+)%.(%d+)')
major, minor, patch = tonumber(major), tonumber(minor), tonumber(patch or 0)
local ver_str = ('%d.%d.%d'):format(major or 0, minor or 0, patch or 0)
if
major
and (major > 1 or (major == 1 and minor > 7) or (major == 1 and minor == 7 and patch >= 9))
then
vim.health.ok('git >= 1.7.9: ' .. ver_str)
else
vim.health.warn('git >= 1.7.9 required for credential storage, found ' .. ver_str)
end
end
local helper = vim.system({ 'git', 'config', 'credential.helper' }, { text = true }):wait()
if helper.code == 0 and helper.stdout and vim.trim(helper.stdout) ~= '' then
vim.health.ok('git credential helper: ' .. vim.trim(helper.stdout))
else
vim.health.warn('no git credential helper configured (required for login/submit)')
end
else else
vim.health.warn('git not found (required for credential storage)') vim.health.error('GNU time not found: ' .. (time_cap.reason or ''))
end
local timeout_cap = utils.timeout_capability()
if timeout_cap.ok then
vim.health.ok('GNU timeout found: ' .. timeout_cap.path)
else
vim.health.error('GNU timeout not found: ' .. (timeout_cap.reason or ''))
end end
end end

View file

@ -347,9 +347,7 @@ function M.login(platform, credentials, on_status, callback)
stdin = vim.json.encode(credentials), stdin = vim.json.encode(credentials),
on_event = function(ev) on_event = function(ev)
if ev.credentials ~= nil and next(ev.credentials) ~= nil then if ev.credentials ~= nil and next(ev.credentials) ~= nil then
vim.schedule(function() require('cp.cache').set_credentials(platform, ev.credentials)
require('cp.git_credential').store(platform, ev.credentials)
end)
end end
if ev.status ~= nil then if ev.status ~= nil then
if type(on_status) == 'function' then if type(on_status) == 'function' then
@ -397,9 +395,7 @@ function M.submit(
stdin = vim.json.encode(credentials), stdin = vim.json.encode(credentials),
on_event = function(ev) on_event = function(ev)
if ev.credentials ~= nil and next(ev.credentials) ~= nil then if ev.credentials ~= nil and next(ev.credentials) ~= nil then
vim.schedule(function() require('cp.cache').set_credentials(platform, ev.credentials)
require('cp.git_credential').store(platform, ev.credentials)
end)
end end
if ev.status ~= nil then if ev.status ~= nil then
if type(on_status) == 'function' then if type(on_status) == 'function' then

View file

@ -1,8 +1,8 @@
local M = {} local M = {}
local cache = require('cp.cache')
local config = require('cp.config') local config = require('cp.config')
local constants = require('cp.constants') local constants = require('cp.constants')
local git_credential = require('cp.git_credential')
local logger = require('cp.log') local logger = require('cp.log')
local state = require('cp.state') local state = require('cp.state')
@ -14,7 +14,7 @@ local STATUS_MSGS = {
} }
local function prompt_credentials(platform, callback) local function prompt_credentials(platform, callback)
local saved = git_credential.get(platform) local saved = cache.get_credentials(platform)
if saved and saved.username and saved.password then if saved and saved.username and saved.password then
callback(saved) callback(saved)
return return
@ -42,14 +42,6 @@ end
---@param opts { language?: string }? ---@param opts { language?: string }?
function M.submit(opts) function M.submit(opts)
if not git_credential.has_helper() then
logger.log(
'No git credential helper configured. See :help cp-credentials',
{ level = vim.log.levels.ERROR }
)
return
end
local platform = state.get_platform() local platform = state.get_platform()
local contest_id = state.get_contest_id() local contest_id = state.get_contest_id()
local problem_id = state.get_problem_id() local problem_id = state.get_problem_id()
@ -117,12 +109,12 @@ function M.submit(opts)
function(result) function(result)
vim.schedule(function() vim.schedule(function()
if result and result.success then if result and result.success then
git_credential.store(platform, creds) cache.set_credentials(platform, creds)
logger.log('Submitted successfully', { level = vim.log.levels.INFO, override = true }) logger.log('Submitted successfully', { level = vim.log.levels.INFO, override = true })
else else
local err = result and result.error or 'unknown error' local err = result and result.error or 'unknown error'
if err == 'bad_credentials' or err:match('^Login failed') then if err == 'bad_credentials' or err:match('^Login failed') then
git_credential.reject(platform, creds) cache.clear_credentials(platform)
logger.log( logger.log(
'Submit failed: ' .. (constants.LOGIN_ERRORS[err] or err), 'Submit failed: ' .. (constants.LOGIN_ERRORS[err] or err),
{ level = vim.log.levels.ERROR } { level = vim.log.levels.ERROR }

View file

@ -366,10 +366,6 @@ function M.check_required_runtime()
return false, timeout.reason return false, timeout.reason
end end
if vim.fn.executable('git') ~= 1 then
return false, 'git is required for credential storage'
end
return true return true
end end

View file

@ -8,7 +8,6 @@ vim.api.nvim_create_user_command('CP', function(opts)
cp.handle_command(opts) cp.handle_command(opts)
end, { end, {
nargs = '*', nargs = '*',
bar = true,
desc = 'Competitive programming helper', desc = 'Competitive programming helper',
complete = function(ArgLead, CmdLine, _) complete = function(ArgLead, CmdLine, _)
local constants = require('cp.constants') local constants = require('cp.constants')

View file

@ -3,7 +3,6 @@ import json
import os import os
import re import re
import sys import sys
import tempfile
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@ -21,18 +20,6 @@ from .models import (
_COOKIE_FILE = Path.home() / ".cache" / "cp-nvim" / "cookies.json" _COOKIE_FILE = Path.home() / ".cache" / "cp-nvim" / "cookies.json"
def _atomic_write(path: Path, content: str) -> None:
fd, tmp = tempfile.mkstemp(dir=path.parent, prefix=".tmp-")
try:
os.fchmod(fd, 0o600)
with os.fdopen(fd, "w") as f:
f.write(content)
os.replace(tmp, path)
except BaseException:
os.unlink(tmp)
raise
def load_platform_cookies(platform: str) -> Any | None: def load_platform_cookies(platform: str) -> Any | None:
try: try:
data = json.loads(_COOKIE_FILE.read_text()) data = json.loads(_COOKIE_FILE.read_text())
@ -42,20 +29,22 @@ def load_platform_cookies(platform: str) -> Any | None:
def save_platform_cookies(platform: str, data: Any) -> None: def save_platform_cookies(platform: str, data: Any) -> None:
_COOKIE_FILE.parent.mkdir(parents=True, exist_ok=True, mode=0o700) _COOKIE_FILE.parent.mkdir(parents=True, exist_ok=True)
try: try:
existing = json.loads(_COOKIE_FILE.read_text()) existing = json.loads(_COOKIE_FILE.read_text())
except Exception: except Exception:
existing = {} existing = {}
existing[platform] = data existing[platform] = data
_atomic_write(_COOKIE_FILE, json.dumps(existing)) _COOKIE_FILE.write_text(json.dumps(existing))
_COOKIE_FILE.chmod(0o600)
def clear_platform_cookies(platform: str) -> None: def clear_platform_cookies(platform: str) -> None:
try: try:
existing = json.loads(_COOKIE_FILE.read_text()) existing = json.loads(_COOKIE_FILE.read_text())
existing.pop(platform, None) existing.pop(platform, None)
_atomic_write(_COOKIE_FILE, json.dumps(existing)) _COOKIE_FILE.write_text(json.dumps(existing))
_COOKIE_FILE.chmod(0o600)
except Exception: except Exception:
pass pass

View file

@ -8,12 +8,7 @@ from typing import Any
import httpx import httpx
from .base import ( from .base import BaseScraper, extract_precision
BaseScraper,
extract_precision,
load_platform_cookies,
save_platform_cookies,
)
from .timeouts import HTTP_TIMEOUT from .timeouts import HTTP_TIMEOUT
from .models import ( from .models import (
ContestListResult, ContestListResult,
@ -253,20 +248,35 @@ class CSESScraper(BaseScraper):
if not username or not password: if not username or not password:
return self._login_error("Missing username or password") return self._login_error("Missing username or password")
token = load_platform_cookies("cses") token = credentials.get("token")
async with httpx.AsyncClient(follow_redirects=True) as client: async with httpx.AsyncClient(follow_redirects=True) as client:
if token: if token:
print(json.dumps({"status": "checking_login"}), flush=True) print(json.dumps({"status": "checking_login"}), flush=True)
if await self._check_token(client, token): if await self._check_token(client, token):
return LoginResult(success=True, error="") return LoginResult(
success=True,
error="",
credentials={
"username": username,
"password": password,
"token": token,
},
)
print(json.dumps({"status": "logging_in"}), flush=True) print(json.dumps({"status": "logging_in"}), flush=True)
token = await self._web_login(client, username, password) token = await self._web_login(client, username, password)
if not token: if not token:
return self._login_error("bad_credentials") return self._login_error("bad_credentials")
save_platform_cookies("cses", token) return LoginResult(
return LoginResult(success=True, error="") success=True,
error="",
credentials={
"username": username,
"password": password,
"token": token,
},
)
async def stream_tests_for_category_async(self, category_id: str) -> None: async def stream_tests_for_category_async(self, category_id: str) -> None:
async with httpx.AsyncClient( async with httpx.AsyncClient(
@ -413,7 +423,7 @@ class CSESScraper(BaseScraper):
return self._submit_error("Missing credentials. Use :CP login cses") return self._submit_error("Missing credentials. Use :CP login cses")
async with httpx.AsyncClient(follow_redirects=True) as client: async with httpx.AsyncClient(follow_redirects=True) as client:
token = load_platform_cookies("cses") token = credentials.get("token")
if token: if token:
print(json.dumps({"status": "checking_login"}), flush=True) print(json.dumps({"status": "checking_login"}), flush=True)
@ -425,7 +435,18 @@ class CSESScraper(BaseScraper):
token = await self._web_login(client, username, password) token = await self._web_login(client, username, password)
if not token: if not token:
return self._submit_error("bad_credentials") return self._submit_error("bad_credentials")
save_platform_cookies("cses", token) print(
json.dumps(
{
"credentials": {
"username": username,
"password": password,
"token": token,
}
}
),
flush=True,
)
print(json.dumps({"status": "submitting"}), flush=True) print(json.dumps({"status": "submitting"}), flush=True)