Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
270 changes: 260 additions & 10 deletions onlinejudge/service/aoj.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import onlinejudge._implementation.testcase_zipper
import onlinejudge._implementation.utils as utils
import onlinejudge.type
from onlinejudge.type import TestCase
from onlinejudge.type import Language, LanguageId, LoginError, NotLoggedInError, SubmissionError, TestCase

logger = getLogger(__name__)

Expand All @@ -28,16 +28,55 @@ class AOJService(onlinejudge.type.Service):
def get_url(self) -> str:
return 'http://judge.u-aizu.ac.jp/onlinejudge/'

def get_api_base_url(self) -> str:
return 'https://judgeapi.u-aizu.ac.jp'

def get_name(self) -> str:
return 'Aizu Online Judge'

def is_logged_in(self, *, session: Optional[requests.Session] = None) -> bool:
session = session or utils.get_default_session()
url = f'{self.get_api_base_url()}/self'
resp = utils.request('GET', url, session=session,
allow_redirects=False)
return resp.status_code == 200

def login(self, *, get_credentials: onlinejudge.type.CredentialsProvider, session: Optional[requests.Session] = None) -> None:
"""
:raises LoginError:
"""

session = session or utils.get_default_session()
if self.is_logged_in(session=session):
return

# get
url = f'{self.get_api_base_url()}/session'
username, password = get_credentials()
data = {
'id': username,
'password': password,
}
headers = {
'Content-Type': 'application/json;charset=UTF-8',
}
resp = utils.request('POST', url, json=data, session=session,
headers=headers, allow_redirects=False)

# result
if resp.status_code == 200:
logger.info('Welcome,')
else:
logger.error('Username or Password is incorrect.')
raise LoginError

@classmethod
def from_url(cls, url: str) -> Optional['AOJService']:
# example: http://judge.u-aizu.ac.jp/onlinejudge/
# example: https://onlinejudge.u-aizu.ac.jp/home
result = urllib.parse.urlparse(url)
if result.scheme in ('', 'http', 'https') \
and result.netloc in ('judge.u-aizu.ac.jp', 'onlinejudge.u-aizu.ac.jp'):
and result.netloc in ('judge.u-aizu.ac.jp', 'judgeapi.u-aizu.ac.jp', 'onlinejudge.u-aizu.ac.jp'):
return cls()
return None

Expand All @@ -48,7 +87,8 @@ def from_url(cls, url: str) -> Optional['AOJService']:
def is_logged_in(self, *, session: Optional[requests.Session] = None) -> bool:
session = session or utils.get_default_session()
url = 'https://judgeapi.u-aizu.ac.jp/self'
resp = utils.request('GET', url, session=session, raise_for_status=False)
resp = utils.request('GET', url, session=session,
raise_for_status=False)
if resp.status_code != 200:
return False
data = json.loads(resp.content)
Expand All @@ -60,6 +100,7 @@ class AOJProblem(onlinejudge.type.Problem):
"""
:ivar problem_id: :py:class:`str` like `DSL_1_A` or `2256`
"""

def __init__(self, *, problem_id):
self.problem_id = problem_id

Expand All @@ -68,7 +109,8 @@ def download_sample_cases(self, *, session: Optional[requests.Session] = None) -

# get samples via the official API
# reference: http://developers.u-aizu.ac.jp/api?key=judgedat%2Ftestcases%2Fsamples%2F%7BproblemId%7D_GET
url = 'https://judgedat.u-aizu.ac.jp/testcases/samples/{}'.format(self.problem_id)
url = 'https://judgedat.u-aizu.ac.jp/testcases/samples/{}'.format(
self.problem_id)
resp = utils.request('GET', url, session=session)
samples = [] # type: List[TestCase]
for i, sample in enumerate(json.loads(resp.text)):
Expand All @@ -83,11 +125,13 @@ def download_sample_cases(self, *, session: Optional[requests.Session] = None) -
# parse HTML if no samples are registered
# see: https://github.com/kmyk/online-judge-tools/issues/207
if not samples:
logger.warning("sample cases are not registered in the official API")
logger.warning(
"sample cases are not registered in the official API")
logger.info("fallback: parsing HTML")

# reference: http://developers.u-aizu.ac.jp/api?key=judgeapi%2Fresources%2Fdescriptions%2F%7Blang%7D%2F%7Bproblem_id%7D_GET
url = 'https://judgeapi.u-aizu.ac.jp/resources/descriptions/ja/{}'.format(self.problem_id)
url = 'https://judgeapi.u-aizu.ac.jp/resources/descriptions/ja/{}'.format(
self.problem_id)
resp = utils.request('GET', url, session=session)
html = json.loads(resp.text)['html']

Expand All @@ -109,7 +153,8 @@ def download_system_cases(self, *, session: Optional[requests.Session] = None) -

# get header
# reference: http://developers.u-aizu.ac.jp/api?key=judgedat%2Ftestcases%2F%7BproblemId%7D%2Fheader_GET
url = 'https://judgedat.u-aizu.ac.jp/testcases/{}/header'.format(self.problem_id)
url = 'https://judgedat.u-aizu.ac.jp/testcases/{}/header'.format(
self.problem_id)
resp = utils.request('GET', url, session=session)
header = json.loads(resp.text)

Expand All @@ -118,7 +163,8 @@ def download_system_cases(self, *, session: Optional[requests.Session] = None) -
for header in header['headers']:
# NOTE: the endpoints are not same to http://developers.u-aizu.ac.jp/api?key=judgedat%2Ftestcases%2F%7BproblemId%7D%2F%7Bserial%7D_GET since the json API often says "..... (terminated because of the limitation)"
# NOTE: even when using https://judgedat.u-aizu.ac.jp/testcases/PROBLEM_ID/SERIAL, there is the 1G limit (see https://twitter.com/beet_aizu/status/1194947611100188672)
url = 'https://judgedat.u-aizu.ac.jp/testcases/{}/{}'.format(self.problem_id, header['serial'])
url = 'https://judgedat.u-aizu.ac.jp/testcases/{}/{}'.format(
self.problem_id, header['serial'])
resp_in = utils.request('GET', url + '/in', session=session)
resp_out = utils.request('GET', url + '/out', session=session)
testcases += [TestCase(
Expand All @@ -133,6 +179,93 @@ def download_system_cases(self, *, session: Optional[requests.Session] = None) -
def get_url(self) -> str:
return 'http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id={}'.format(self.problem_id)

def get_available_languages(self, *, session: Optional[requests.Session] = None) -> List[Language]:
session = session or utils.get_default_session()

languages = [
Language(id=LanguageId('C'), name='C'),
Language(id=LanguageId('C++'), name='C++'),
Language(id=LanguageId('JAVA'), name='JAVA'),
Language(id=LanguageId('C++11'), name='C++11'),
Language(id=LanguageId('C++14'), name='C++14'),
Language(id=LanguageId('C++17'), name='C++17'),
Language(id=LanguageId('C++20'), name='C++20'),
Language(id=LanguageId('C++23'), name='C++23'),
Language(id=LanguageId('C#'), name='C#'),
Language(id=LanguageId('D'), name='D'),
Language(id=LanguageId('Ruby'), name='Ruby'),
Language(id=LanguageId('Python'), name='Python'),
Language(id=LanguageId('Python3'), name='Python3'),
Language(id=LanguageId('PyPy3'), name='PyPy3'),
Language(id=LanguageId('PHP'), name='PHP'),
Language(id=LanguageId('JavaScript'), name='JavaScript'),
Language(id=LanguageId('Scala'), name='Scala'),
Language(id=LanguageId('Haskell'), name='Haskell'),
Language(id=LanguageId('OCaml'), name='OCaml'),
Language(id=LanguageId('Rust'), name='Rust'),
Language(id=LanguageId('Go'), name='Go'),
Language(id=LanguageId('Kotlin'), name='Kotlin'),
]

return languages

def submit_code(self, code: bytes, language_id: LanguageId, *, filename: Optional[str] = None, session: Optional[requests.Session] = None) -> 'AOJSubmission':
"""
:raises NotLoggedInError:
:raises SubmissionError:
"""
session = session or utils.get_default_session()

# check if logged in
if not self.get_service().is_logged_in(session=session):
raise NotLoggedInError

# get current user ID
user_id = None
try:
self_api_url = 'https://judgeapi.u-aizu.ac.jp/self'
self_resp = utils.request('GET', self_api_url, session=session)
if self_resp.status_code == 200:
user_info = json.loads(self_resp.text)
user_id = user_info.get('id')
except Exception as e:
logger.warning('failed to get user ID: %s', e)
# Continue without user_id - it's optional

# prepare submission data
url = 'https://judgeapi.u-aizu.ac.jp/submissions'
data = {
'problemId': self.problem_id,
'language': str(language_id),
'sourceCode': code.decode('utf-8'),
}
headers = {
'Content-Type': 'application/json;charset=UTF-8',
}

# submit
resp = utils.request('POST', url, json=data,
session=session, headers=headers)

# check response
if resp.status_code != 200:
logger.error('submission failed with status code: %d',
resp.status_code)
raise SubmissionError('submission failed')

# parse response to get submission token
try:
result = json.loads(resp.text)
submission_token = result.get('token')
if submission_token is None:
raise SubmissionError(
'failed to get submission token from response')
logger.info('success: submission token: %s', submission_token)
return AOJSubmission(submission_token=submission_token, problem_id=self.problem_id, user_id=user_id)
except (json.JSONDecodeError, KeyError, ValueError) as e:
logger.error('failed to parse response: %s', e)
raise SubmissionError('failed to parse submission response')

@classmethod
def from_url(cls, url: str) -> Optional['AOJProblem']:
result = urllib.parse.urlparse(url)
Expand All @@ -150,7 +283,8 @@ def from_url(cls, url: str) -> Optional['AOJProblem']:

# example: https://onlinejudge.u-aizu.ac.jp/challenges/sources/JAG/Prelim/2881
# example: https://onlinejudge.u-aizu.ac.jp/courses/library/4/CGL/3/CGL_3_B
m = re.match(r'^/(challenges|courses)/(sources|library/\d+|lesson/\d+)/(\w+)/(\w+)/(\w+)$', utils.normpath(result.path))
m = re.match(r'^/(challenges|courses)/(sources|library/\d+|lesson/\d+)/(\w+)/(\w+)/(\w+)$',
utils.normpath(result.path))
if result.scheme in ('', 'http', 'https') \
and result.netloc == 'onlinejudge.u-aizu.ac.jp' \
and m:
Expand Down Expand Up @@ -179,6 +313,7 @@ class AOJArenaProblem(onlinejudge.type.Problem):

.. versionadded:: 6.1.0
"""

def __init__(self, *, arena_id, alphabet):
assert alphabet in string.ascii_uppercase
self.arena_id = arena_id
Expand All @@ -193,7 +328,8 @@ def get_problem_id(self, *, session: Optional[requests.Session] = None) -> str:

if self._problem_id is None:
session = session or utils.get_default_session()
url = 'https://judgeapi.u-aizu.ac.jp/arenas/{}/problems'.format(self.arena_id)
url = 'https://judgeapi.u-aizu.ac.jp/arenas/{}/problems'.format(
self.arena_id)
resp = utils.request('GET', url, session=session)
problems = json.loads(resp.text)
for problem in problems:
Expand Down Expand Up @@ -234,5 +370,119 @@ def get_service(self) -> AOJService:
return AOJService()


class AOJSubmission(onlinejudge.type.Submission):
"""
:ivar submission_token: :py:class:`str` - UUID format token (e.g., 'afabd5d0-e47c-471f-b988-fde2f62fe6cd')
:ivar problem_id: :py:class:`Optional[str]` - cached problem_id
:ivar user_id: :py:class:`Optional[str]` - cached user_id
"""

def __init__(self, *, submission_token: str, problem_id: Optional[str] = None, user_id: Optional[str] = None):
self.submission_token = submission_token
self._problem_id = problem_id
self._user_id = user_id

def get_url(self) -> str:
"""
Returns the human-readable submission status page URL.
Fetches the current user's last submission from the recent submissions API to construct the URL.

:raises SubmissionError: if failed to get submission data from API or no submissions available
"""
session = utils.get_default_session()

try:
# Get current user ID - use cached value if available
current_user_id = self._user_id
if current_user_id is None:
# Fetch from API if not cached
self_api_url = 'https://judgeapi.u-aizu.ac.jp/self'
self_resp = utils.request('GET', self_api_url, session=session)
if self_resp.status_code != 200:
raise SubmissionError('failed to get current user information from API')

user_info = json.loads(self_resp.text)
current_user_id = user_info.get('id')
if not current_user_id:
raise SubmissionError('user ID not found in user information')

# Get recent submissions
submissions_api_url = 'https://judgeapi.u-aizu.ac.jp/submission_records/recent'
submissions_resp = utils.request('GET', submissions_api_url, session=session)
if submissions_resp.status_code != 200:
raise SubmissionError('failed to get submission data from API')

submissions = json.loads(submissions_resp.text)

# Filter submissions by current user ID
user_submissions = [s for s in submissions if s.get('userId') == current_user_id]

if not user_submissions:
raise SubmissionError('no recent submissions available for current user')

# Pick the last submission from the filtered array
submission_data = user_submissions[0]

# Extract required fields from API response
user_id = submission_data.get('userId')
problem_id = submission_data.get('problemId')
judge_id = submission_data.get('judgeId')
language = submission_data.get('language')

if not all([user_id, problem_id, judge_id, language]):
raise SubmissionError('missing required fields in submission data')

# Construct the submission status URL
return 'https://onlinejudge.u-aizu.ac.jp/status/users/{}/submissions/1/{}/judge/{}/{}'.format(
user_id, problem_id, judge_id, language
)


except (json.JSONDecodeError, KeyError) as e:
logger.error('failed to parse submission data: %s', e)
raise SubmissionError('failed to parse submission data')

def download_problem(self, *, session: Optional[requests.Session] = None) -> AOJProblem:
"""
:raises SubmissionError: if failed to get problem_id from submission data
"""
if self._problem_id is None:
session = session or utils.get_default_session()
# Get submission data from API
url = self.get_url()
resp = utils.request('GET', url, session=session)
if resp.status_code != 200:
raise SubmissionError('failed to get submission data')

try:
data = json.loads(resp.text)
self._problem_id = data.get('problemId')
if self._problem_id is None:
raise SubmissionError(
'problemId not found in submission data')
except (json.JSONDecodeError, KeyError) as e:
logger.error('failed to parse submission data: %s', e)
raise SubmissionError('failed to parse submission data')

return AOJProblem(problem_id=self._problem_id)

@classmethod
def from_url(cls, url: str) -> Optional['AOJSubmission']:
# example: https://judgeapi.u-aizu.ac.jp/submissions/afabd5d0-e47c-471f-b988-fde2f62fe6cd
result = urllib.parse.urlparse(url)
if result.scheme in ('', 'http', 'https') \
and result.netloc == 'judgeapi.u-aizu.ac.jp':
m = re.match(
r'^/submissions/([0-9a-f\-]+)$', utils.normpath(result.path))
if m:
submission_token = m.group(1)
return cls(submission_token=submission_token)
return None

def get_service(self) -> AOJService:
return AOJService()


onlinejudge.dispatch.services += [AOJService]
onlinejudge.dispatch.problems += [AOJProblem, AOJArenaProblem]
onlinejudge.dispatch.submissions += [AOJSubmission]