From c055e17b41ebeeb5e3f145eacd7024e1a722b54a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Thu, 28 Nov 2024 22:31:10 +0100 Subject: [PATCH 01/52] Add retry strategy to clients. Make submissions and test cases iterable. Increase retry frequency for default implicit Sulu client. --- src/judge0/__init__.py | 4 +-- src/judge0/api.py | 27 +++++++++------ src/judge0/base_types.py | 18 +++------- src/judge0/clients.py | 72 ++++++++++++++++++++++++---------------- src/judge0/filesystem.py | 7 ++-- src/judge0/retry.py | 12 +++---- src/judge0/submission.py | 8 ++--- 7 files changed, 77 insertions(+), 71 deletions(-) diff --git a/src/judge0/__init__.py b/src/judge0/__init__.py index 8f41ec0..18a7013 100644 --- a/src/judge0/__init__.py +++ b/src/judge0/__init__.py @@ -98,9 +98,9 @@ def _get_implicit_client(flavor: Flavor) -> Client: # the preview Sulu client based on the flavor. if client is None: if flavor == Flavor.CE: - client = SuluJudge0CE() + client = SuluJudge0CE(retry_strategy=RegularPeriodRetry(0.5)) else: - client = SuluJudge0ExtraCE() + client = SuluJudge0ExtraCE(retry_strategy=RegularPeriodRetry(0.5)) if flavor == Flavor.CE: JUDGE0_IMPLICIT_CE_CLIENT = client diff --git a/src/judge0/api.py b/src/judge0/api.py index b5fd64d..43105cb 100644 --- a/src/judge0/api.py +++ b/src/judge0/api.py @@ -1,10 +1,10 @@ -from typing import Iterable, Optional, Union +from typing import Optional, Union -from .base_types import Flavor, TestCase, TestCases +from .base_types import Flavor, Iterable, TestCase, TestCases from .clients import Client from .common import batched -from .retry import RegularPeriodRetry, RetryMechanism +from .retry import RegularPeriodRetry, RetryStrategy from .submission import Submission, Submissions @@ -31,7 +31,7 @@ def _resolve_client( if isinstance(client, Flavor): return get_client(client) - if client is None and isinstance(submissions, list) and len(submissions) == 0: + if client is None and isinstance(submissions, Iterable) and len(submissions) == 0: raise ValueError("Client cannot be determined from empty submissions.") # client is None and we have to determine a flavor of the client from the @@ -57,6 +57,7 @@ def _resolve_client( def create_submissions( + *, client: Optional[Client] = None, submissions: Optional[Union[Submission, Submissions]] = None, ) -> Union[Submission, Submissions]: @@ -81,7 +82,7 @@ def get_submissions( *, client: Optional[Client] = None, submissions: Optional[Union[Submission, Submissions]] = None, - fields: Union[str, Iterable[str], None] = None, + fields: Optional[Union[str, Iterable[str]]] = None, ) -> Union[Submission, Submissions]: client = _resolve_client(client=client, submissions=submissions) @@ -108,12 +109,15 @@ def wait( *, client: Optional[Client] = None, submissions: Optional[Union[Submission, Submissions]] = None, - retry_mechanism: Optional[RetryMechanism] = None, + retry_strategy: Optional[RetryStrategy] = None, ) -> Union[Submission, Submissions]: client = _resolve_client(client, submissions) - if retry_mechanism is None: - retry_mechanism = RegularPeriodRetry() + if retry_strategy is None: + if client.retry_strategy is None: + retry_strategy = RegularPeriodRetry() + else: + retry_strategy = client.retry_strategy if isinstance(submissions, Submission): submissions_to_check = { @@ -124,7 +128,7 @@ def wait( submission.token: submission for submission in submissions } - while len(submissions_to_check) > 0 and not retry_mechanism.is_done(): + while len(submissions_to_check) > 0 and not retry_strategy.is_done(): get_submissions(client=client, submissions=list(submissions_to_check.values())) for token in list(submissions_to_check): submission = submissions_to_check[token] @@ -135,8 +139,8 @@ def wait( if len(submissions_to_check) == 0: break - retry_mechanism.wait() - retry_mechanism.step() + retry_strategy.wait() + retry_strategy.step() return submissions @@ -204,6 +208,7 @@ def _execute( if submissions is None and source_code is None: raise ValueError("Neither source_code nor submissions argument are provided.") + # Internally, let's rely on Submission's dataclass. if source_code is not None: submissions = Submission(source_code=source_code, **kwargs) diff --git a/src/judge0/base_types.py b/src/judge0/base_types.py index b1d4210..e99ce19 100644 --- a/src/judge0/base_types.py +++ b/src/judge0/base_types.py @@ -1,19 +1,11 @@ from abc import ABC, abstractmethod from dataclasses import dataclass from enum import IntEnum -from typing import Optional, Union - - -TestCases = Union[ - list["TestCase"], - tuple["TestCase"], - list[dict], - tuple[dict], - list[list], - list[tuple], - tuple[list], - tuple[tuple], -] +from typing import Optional, Sequence, Union + +Iterable = Sequence + +TestCases = Iterable["TestCase"] @dataclass(frozen=True) diff --git a/src/judge0/clients.py b/src/judge0/clients.py index 0797e9a..ada06cf 100644 --- a/src/judge0/clients.py +++ b/src/judge0/clients.py @@ -1,26 +1,29 @@ -from typing import Iterable, Union +from typing import Optional, Union import requests -from .base_types import Config, Language, LanguageAlias +from .base_types import Config, Iterable, Language, LanguageAlias from .data import LANGUAGE_TO_LANGUAGE_ID +from .retry import RetryStrategy from .submission import Submission, Submissions class Client: - API_KEY_ENV = "JUDGE0_API_KEY" - DEFAULT_MAX_SUBMISSION_BATCH_SIZE = 20 - ENABLED_BATCHED_SUBMISSIONS = True - EFFECTIVE_SUBMISSION_BATCH_SIZE = ( - DEFAULT_MAX_SUBMISSION_BATCH_SIZE if ENABLED_BATCHED_SUBMISSIONS else 1 - ) + API_KEY_ENV = None - def __init__(self, endpoint, auth_headers) -> None: + def __init__( + self, + endpoint, + auth_headers, + *, + retry_strategy: Optional[RetryStrategy] = None, + ) -> None: self.endpoint = endpoint self.auth_headers = auth_headers + self.retry_strategy = retry_strategy try: - self.languages = [Language(**lang) for lang in self.get_languages()] + self.languages = tuple(Language(**lang) for lang in self.get_languages()) self.config = Config(**self.get_config_info()) except Exception as e: raise RuntimeError( @@ -113,7 +116,7 @@ def get_submission( self, submission: Submission, *, - fields: Union[str, Iterable[str], None] = None, + fields: Optional[Union[str, Iterable[str]]] = None, ) -> Submission: """Check the submission status.""" @@ -168,7 +171,7 @@ def get_submissions( self, submissions: Submissions, *, - fields: Union[str, Iterable[str], None] = None, + fields: Optional[Union[str, Iterable[str]]] = None, ) -> Submissions: params = { "base64_encoded": "true", @@ -201,7 +204,7 @@ def get_submissions( class ATD(Client): API_KEY_ENV = "JUDGE0_ATD_API_KEY" - def __init__(self, endpoint, host_header_value, api_key): + def __init__(self, endpoint, host_header_value, api_key, **kwargs): self.api_key = api_key super().__init__( endpoint, @@ -209,6 +212,7 @@ def __init__(self, endpoint, host_header_value, api_key): "x-apihub-host": host_header_value, "x-apihub-key": api_key, }, + **kwargs, ) def _update_endpoint_header(self, header_value): @@ -232,11 +236,12 @@ class ATDJudge0CE(ATD): DEFAULT_CREATE_SUBMISSIONS_ENDPOINT: str = "402b857c-1126-4450-bfd8-22e1f2cbff2f" DEFAULT_GET_SUBMISSIONS_ENDPOINT: str = "e42f2a26-5b02-472a-80c9-61c4bdae32ec" - def __init__(self, api_key): + def __init__(self, api_key, **kwargs): super().__init__( self.DEFAULT_ENDPOINT, self.DEFAULT_HOST, api_key, + **kwargs, ) def get_about(self) -> dict: @@ -267,7 +272,7 @@ def get_submission( self, submission: Submission, *, - fields: Union[str, Iterable[str], None] = None, + fields: Optional[Union[str, Iterable[str]]] = None, ) -> Submission: self._update_endpoint_header(self.DEFAULT_GET_SUBMISSION_ENDPOINT) return super().get_submission(submission, fields=fields) @@ -280,7 +285,7 @@ def get_submissions( self, submissions: Submissions, *, - fields: Union[str, Iterable[str], None] = None, + fields: Optional[Union[str, Iterable[str]]] = None, ) -> Submissions: self._update_endpoint_header(self.DEFAULT_GET_SUBMISSIONS_ENDPOINT) return super().get_submissions(submissions, fields=fields) @@ -303,11 +308,12 @@ class ATDJudge0ExtraCE(ATD): DEFAULT_CREATE_SUBMISSIONS_ENDPOINT: str = "c64df5d3-edfd-4b08-8687-561af2f80d2f" DEFAULT_GET_SUBMISSIONS_ENDPOINT: str = "5d173718-8e6a-4cf5-9d8c-db5e6386d037" - def __init__(self, api_key): + def __init__(self, api_key, **kwargs): super().__init__( self.DEFAULT_ENDPOINT, self.DEFAULT_HOST, api_key, + **kwargs, ) def get_about(self) -> dict: @@ -338,7 +344,7 @@ def get_submission( self, submission: Submission, *, - fields: Union[str, Iterable[str], None] = None, + fields: Optional[Union[str, Iterable[str]]] = None, ) -> Submission: self._update_endpoint_header(self.DEFAULT_GET_SUBMISSION_ENDPOINT) return super().get_submission(submission, fields=fields) @@ -351,7 +357,7 @@ def get_submissions( self, submissions: Submissions, *, - fields: Union[str, Iterable[str], None] = None, + fields: Optional[Union[str, Iterable[str]]] = None, ) -> Submissions: self._update_endpoint_header(self.DEFAULT_GET_SUBMISSIONS_ENDPOINT) return super().get_submissions(submissions, fields=fields) @@ -360,7 +366,7 @@ def get_submissions( class Rapid(Client): API_KEY_ENV = "JUDGE0_RAPID_API_KEY" - def __init__(self, endpoint, host_header_value, api_key): + def __init__(self, endpoint, host_header_value, api_key, **kwargs): self.api_key = api_key super().__init__( endpoint, @@ -368,6 +374,7 @@ def __init__(self, endpoint, host_header_value, api_key): "x-rapidapi-host": host_header_value, "x-rapidapi-key": api_key, }, + **kwargs, ) @@ -376,11 +383,12 @@ class RapidJudge0CE(Rapid): DEFAULT_HOST: str = "judge0-ce.p.rapidapi.com" HOME_URL: str = "https://rapidapi.com/judge0-official/api/judge0-ce" - def __init__(self, api_key): + def __init__(self, api_key, **kwargs): super().__init__( self.DEFAULT_ENDPOINT, self.DEFAULT_HOST, api_key, + **kwargs, ) @@ -389,22 +397,24 @@ class RapidJudge0ExtraCE(Rapid): DEFAULT_HOST: str = "judge0-extra-ce.p.rapidapi.com" HOME_URL: str = "https://rapidapi.com/judge0-official/api/judge0-extra-ce" - def __init__(self, api_key): + def __init__(self, api_key, **kwargs): super().__init__( self.DEFAULT_ENDPOINT, self.DEFAULT_HOST, api_key, + **kwargs, ) class Sulu(Client): API_KEY_ENV = "JUDGE0_SULU_API_KEY" - def __init__(self, endpoint, api_key=None): + def __init__(self, endpoint, api_key=None, **kwargs): self.api_key = api_key super().__init__( endpoint, {"Authorization": f"Bearer {api_key}"} if api_key else None, + **kwargs, ) @@ -412,17 +422,21 @@ class SuluJudge0CE(Sulu): DEFAULT_ENDPOINT: str = "https://judge0-ce.p.sulu.sh" HOME_URL: str = "https://sparkhub.sulu.sh/apis/judge0/judge0-ce/readme" - def __init__(self, api_key=None): - super().__init__(self.DEFAULT_ENDPOINT, api_key) + def __init__(self, api_key=None, **kwargs): + super().__init__( + self.DEFAULT_ENDPOINT, + api_key, + **kwargs, + ) class SuluJudge0ExtraCE(Sulu): DEFAULT_ENDPOINT: str = "https://judge0-extra-ce.p.sulu.sh" HOME_URL: str = "https://sparkhub.sulu.sh/apis/judge0/judge0-extra-ce/readme" - def __init__(self, api_key=None): - super().__init__(self.DEFAULT_ENDPOINT, api_key) + def __init__(self, api_key=None, **kwargs): + super().__init__(self.DEFAULT_ENDPOINT, api_key, **kwargs) -CE = [RapidJudge0CE, SuluJudge0CE, ATDJudge0CE] -EXTRA_CE = [RapidJudge0ExtraCE, SuluJudge0ExtraCE, ATDJudge0ExtraCE] +CE = (RapidJudge0CE, SuluJudge0CE, ATDJudge0CE) +EXTRA_CE = (RapidJudge0ExtraCE, SuluJudge0ExtraCE, ATDJudge0ExtraCE) diff --git a/src/judge0/filesystem.py b/src/judge0/filesystem.py index 590795c..bbdb11b 100644 --- a/src/judge0/filesystem.py +++ b/src/judge0/filesystem.py @@ -3,10 +3,9 @@ import zipfile from base64 import b64decode, b64encode -from collections import abc -from typing import Iterable, Optional, Union +from typing import Optional, Union -from .base_types import Encodeable +from .base_types import Encodeable, Iterable class File: @@ -42,7 +41,7 @@ def __init__( for file_name in zip_file.namelist(): with zip_file.open(file_name) as fp: self.files.append(File(file_name, fp.read())) - elif isinstance(content, abc.Iterable): + elif isinstance(content, Iterable): self.files = list(content) elif isinstance(content, File): self.files = [content] diff --git a/src/judge0/retry.py b/src/judge0/retry.py index 33acc52..20b42ef 100644 --- a/src/judge0/retry.py +++ b/src/judge0/retry.py @@ -2,7 +2,7 @@ from abc import ABC, abstractmethod -class RetryMechanism(ABC): +class RetryStrategy(ABC): @abstractmethod def is_done(self) -> bool: pass @@ -11,12 +11,11 @@ def is_done(self) -> bool: def wait(self) -> None: pass - @abstractmethod def step(self) -> None: pass -class MaxRetries(RetryMechanism): +class MaxRetries(RetryStrategy): """Check for submissions status every 100 ms and retry a maximum of `max_retries` times.""" @@ -34,7 +33,7 @@ def is_done(self) -> bool: return self.n_retries >= self.max_retries -class MaxWaitTime(RetryMechanism): +class MaxWaitTime(RetryStrategy): """Check for submissions status every 100 ms and wait for all submissions a maximum of `max_wait_time` (seconds).""" @@ -52,15 +51,12 @@ def is_done(self): return self.total_wait_time >= self.max_wait_time_sec -class RegularPeriodRetry(RetryMechanism): +class RegularPeriodRetry(RetryStrategy): """Check for submissions status periodically for indefinite amount of time.""" def __init__(self, wait_time_sec: float = 0.1): self.wait_time_sec = wait_time_sec - def step(self): - pass - def wait(self): time.sleep(self.wait_time_sec) diff --git a/src/judge0/submission.py b/src/judge0/submission.py index 2ffa178..7b3d937 100644 --- a/src/judge0/submission.py +++ b/src/judge0/submission.py @@ -1,10 +1,10 @@ import copy from datetime import datetime -from typing import Optional, Union +from typing import Any, Optional, Union from judge0.filesystem import Filesystem -from .base_types import LanguageAlias, Status +from .base_types import Iterable, LanguageAlias, Status from .common import decode, encode ENCODED_REQUEST_FIELDS = { @@ -63,7 +63,7 @@ "wall_time_limit", } -Submissions = Union[list["Submission"], tuple["Submission"]] +Submissions = Iterable["Submission"] class Submission: @@ -138,7 +138,7 @@ def __init__( self.memory = None self.post_execution_filesystem = None - def set_attributes(self, attributes): + def set_attributes(self, attributes: dict[str, Any]) -> None: for attr, value in attributes.items(): if attr in SKIP_FIELDS: continue From 25b07403a335316fcd88849cb7fe543e91dd063e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Fri, 29 Nov 2024 18:01:00 +0100 Subject: [PATCH 02/52] Initial commit of a handling preview client's 429 Too Many Requests error. --- src/judge0/__init__.py | 2 ++ src/judge0/clients.py | 13 +++++++++++- src/judge0/utils.py | 47 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 src/judge0/utils.py diff --git a/src/judge0/__init__.py b/src/judge0/__init__.py index 18a7013..5ccf40b 100644 --- a/src/judge0/__init__.py +++ b/src/judge0/__init__.py @@ -113,6 +113,8 @@ def _get_implicit_client(flavor: Flavor) -> Client: CE = Flavor.CE EXTRA_CE = Flavor.EXTRA_CE +# TODO: Let's use getattr and setattr for this language ALIASES and raise an +# exception if a value already exists. PYTHON = LanguageAlias.PYTHON CPP = LanguageAlias.CPP JAVA = LanguageAlias.JAVA diff --git a/src/judge0/clients.py b/src/judge0/clients.py index ada06cf..a9a63bb 100644 --- a/src/judge0/clients.py +++ b/src/judge0/clients.py @@ -6,6 +6,7 @@ from .data import LANGUAGE_TO_LANGUAGE_ID from .retry import RetryStrategy from .submission import Submission, Submissions +from .utils import handle_too_many_requests_error_for_preview_client class Client: @@ -22,6 +23,7 @@ def __init__( self.auth_headers = auth_headers self.retry_strategy = retry_strategy + # TODO: Should be handled differently. try: self.languages = tuple(Language(**lang) for lang in self.get_languages()) self.config = Config(**self.get_config_info()) @@ -30,6 +32,7 @@ def __init__( f"Authentication failed. Visit {self.HOME_URL} to get or review your authentication credentials." ) from e + @handle_too_many_requests_error_for_preview_client def get_about(self) -> dict: r = requests.get( f"{self.endpoint}/about", @@ -38,6 +41,7 @@ def get_about(self) -> dict: r.raise_for_status() return r.json() + @handle_too_many_requests_error_for_preview_client def get_config_info(self) -> dict: r = requests.get( f"{self.endpoint}/config_info", @@ -46,18 +50,21 @@ def get_config_info(self) -> dict: r.raise_for_status() return r.json() + @handle_too_many_requests_error_for_preview_client def get_language(self, language_id) -> dict: request_url = f"{self.endpoint}/languages/{language_id}" r = requests.get(request_url, headers=self.auth_headers) r.raise_for_status() return r.json() + @handle_too_many_requests_error_for_preview_client def get_languages(self) -> list[dict]: request_url = f"{self.endpoint}/languages" r = requests.get(request_url, headers=self.auth_headers) r.raise_for_status() return r.json() + @handle_too_many_requests_error_for_preview_client def get_statuses(self) -> list[dict]: r = requests.get( f"{self.endpoint}/statuses", @@ -74,7 +81,7 @@ def version(self): return self._version def get_language_id(self, language: Union[LanguageAlias, int]) -> int: - """Get language id for the corresponding language alias for the client.""" + """Get language id corresponding to the language alias for the client.""" if isinstance(language, LanguageAlias): supported_language_ids = LANGUAGE_TO_LANGUAGE_ID[self.version] language = supported_language_ids.get(language, -1) @@ -85,6 +92,7 @@ def is_language_supported(self, language: Union[LanguageAlias, int]) -> bool: language_id = self.get_language_id(language) return any(language_id == lang.id for lang in self.languages) + @handle_too_many_requests_error_for_preview_client def create_submission(self, submission: Submission) -> Submission: # Check if the client supports the language specified in the submission. if not self.is_language_supported(language=submission.language): @@ -112,6 +120,7 @@ def create_submission(self, submission: Submission) -> Submission: return submission + @handle_too_many_requests_error_for_preview_client def get_submission( self, submission: Submission, @@ -143,6 +152,7 @@ def get_submission( return submission + @handle_too_many_requests_error_for_preview_client def create_submissions(self, submissions: Submissions) -> Submissions: # Check if all submissions contain supported language. for submission in submissions: @@ -167,6 +177,7 @@ def create_submissions(self, submissions: Submissions) -> Submissions: return submissions + @handle_too_many_requests_error_for_preview_client def get_submissions( self, submissions: Submissions, diff --git a/src/judge0/utils.py b/src/judge0/utils.py new file mode 100644 index 0000000..184c368 --- /dev/null +++ b/src/judge0/utils.py @@ -0,0 +1,47 @@ +"""Module containing different utility functions for Judge0 Python SDK.""" + +from functools import wraps +from http import HTTPStatus + +from requests import HTTPError + + +def is_http_too_many_requests_error(exception: Exception) -> bool: + return ( + isinstance(exception, HTTPError) + and exception.response is not None + and exception.response.status_code == HTTPStatus.TOO_MANY_REQUESTS + ) + + +def handle_too_many_requests_error_for_preview_client(func): + @wraps(func) + def wrapper(*args, **kwargs): + try: + return func(*args, **kwargs) + except HTTPError as err: + if is_http_too_many_requests_error(exception=err): + # If the raised exception is inside the one of the Sulu clients + # let's check if we are dealing with the implicit client. + if args: + instance = args[0] + class_name = instance.__class__.__name__ + # Check if we are using a preview version of the client. + if ( + class_name in ("SuluJudge0CE", "SuluJudge0ExtraCE") + and instance.api_key is None + ): + raise RuntimeError( + "You are using a preview version of the Sulu " + "clients and you've hit a rate limit on the preview " + f"clients. Visit {instance.HOME_URL} to get or " + "review your authentication credentials." + ) from err + else: + raise err from None + else: + raise err from None + except Exception as err: + raise err from None + + return wrapper From 1343fb1396cdd3477b75ad9666f393d7ac814f72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Fri, 29 Nov 2024 20:54:45 +0100 Subject: [PATCH 03/52] Add pre-commit for checking docstrings. --- .pre-commit-config.yaml | 9 +++++++++ pyproject.toml | 5 +++++ 2 files changed, 14 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e2a0428..fdebef7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,3 +6,12 @@ repos: additional_dependencies: - black == 24.8.0 - usort == 1.0.8.post1 + - repo: https://github.com/pycqa/flake8 + rev: 7.1.1 + hooks: + - id: flake8 + args: + - --docstring-convention=numpydoc + additional_dependencies: + - flake8-docstrings + - pydocstyle diff --git a/pyproject.toml b/pyproject.toml index aeef351..53a9854 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,3 +38,8 @@ Issues = "https://github.com/judge0/judge0-python/issues" [project.optional-dependencies] test = ["pytest", "mkdocs"] + +[tool.flake8] +docstring-convention = "numpydoc" +extend-ignore = ["D205", "D400", "D105"] +max-line-length = 88 From 941b463ed0923dc16c9c81207ed108f4c65f27b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Fri, 29 Nov 2024 20:59:55 +0100 Subject: [PATCH 04/52] Add docstring and typing to Submission. Fix flake8 pre-commit and take into account pyproject.toml. --- .pre-commit-config.yaml | 3 +- Pipfile | 1 + pyproject.toml | 4 +- src/judge0/submission.py | 137 ++++++++++++++++++++++++++++++--------- 4 files changed, 109 insertions(+), 36 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fdebef7..8adce63 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,8 +10,7 @@ repos: rev: 7.1.1 hooks: - id: flake8 - args: - - --docstring-convention=numpydoc additional_dependencies: + - "flake8-pyproject" - flake8-docstrings - pydocstyle diff --git a/Pipfile b/Pipfile index f7f341b..ec5d4e1 100644 --- a/Pipfile +++ b/Pipfile @@ -12,6 +12,7 @@ pre-commit = "==3.8.0" pytest = "==8.3.3" python-dotenv = "==1.0.1" pytest-cov = "6.0.0" +flake8-docstrings = "1.7.0" [requires] python_version = "3.9" diff --git a/pyproject.toml b/pyproject.toml index 53a9854..fb18e0a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,6 @@ Issues = "https://github.com/judge0/judge0-python/issues" test = ["pytest", "mkdocs"] [tool.flake8] -docstring-convention = "numpydoc" -extend-ignore = ["D205", "D400", "D105"] +docstring-convention = "numpy" +extend-ignore = ["D205", "D400", "D105", "D100", "F821"] max-line-length = 88 diff --git a/src/judge0/submission.py b/src/judge0/submission.py index 7b3d937..55a7d3c 100644 --- a/src/judge0/submission.py +++ b/src/judge0/submission.py @@ -69,6 +69,61 @@ class Submission: """ Stores a representation of a Submission to/from Judge0. + + Parameters + ---------- + source_code : str, optional + The source code to be executed. + language : LanguageAlias or int, optional + The programming language of the source code. Defaults to `LanguageAlias.PYTHON`. + additional_files : base64 encoded string, optional + Additional files that should be available alongside the source code. + Value of this string should represent the content of a .zip that + contains additional files. This attribute is required for multi-file + programs. + compiler_options : str, optional + Options for the compiler (i.e. compiler flags). + command_line_arguments : str, optional + Command line arguments for the program. + stdin : str, optional + Input to be fed via standard input during execution. + expected_output : str, optional + The expected output of the program. + cpu_time_limit : float, optional + Maximum CPU time allowed for execution, in seconds. Time in which the + OS assigns the processor to different tasks is not counted. Depends on + configuration. + cpu_extra_time : float, optional + Additional CPU time allowance in case of time extension. Depends on + configuration. + wall_time_limit : float, optional + Maximum wall clock time allowed for execution, in seconds. Depends on + configuration. + memory_limit : float, optional + Maximum memory allocation allowed for the process, in kilobytes. + Depends on configuration. + stack_limit : int, optional + Maximum stack size allowed, in kilobytes. Depends on configuration. + max_processes_and_or_threads : int, optional + Maximum number of processes and/or threads program can create. Depends + on configuration. + enable_per_process_and_thread_time_limit : bool, optional + If True, enforces time limits per process/thread. Depends on + configuration. + enable_per_process_and_thread_memory_limit : bool, optional + If True, enforces memory limits per process/thread. Depends on + configuration. + max_file_size : int, optional + Maximum file size allowed for output files, in kilobytes. Depends on + configuration. + redirect_stderr_to_stdout : bool, optional + If True, redirects standard error output to standard output. + enable_network : bool, optional + If True, enables network access during execution. + number_of_runs : int, optional + Number of times the code should be executed. + callback_url : str, optional + URL for a callback to report execution results or status. """ def __init__( @@ -76,24 +131,24 @@ def __init__( *, source_code: Optional[str] = None, language: Union[LanguageAlias, int] = LanguageAlias.PYTHON, - additional_files=None, - compiler_options=None, - command_line_arguments=None, - stdin=None, - expected_output=None, - cpu_time_limit=None, - cpu_extra_time=None, - wall_time_limit=None, - memory_limit=None, - stack_limit=None, - max_processes_and_or_threads=None, - enable_per_process_and_thread_time_limit=None, - enable_per_process_and_thread_memory_limit=None, - max_file_size=None, - redirect_stderr_to_stdout=None, - enable_network=None, - number_of_runs=None, - callback_url=None, + additional_files: Optional[str] = None, + compiler_options: Optional[str] = None, + command_line_arguments: Optional[str] = None, + stdin: Optional[str] = None, + expected_output: Optional[str] = None, + cpu_time_limit: Optional[float] = None, + cpu_extra_time: Optional[float] = None, + wall_time_limit: Optional[float] = None, + memory_limit: Optional[float] = None, + stack_limit: Optional[int] = None, + max_processes_and_or_threads: Optional[int] = None, + enable_per_process_and_thread_time_limit: Optional[bool] = None, + enable_per_process_and_thread_memory_limit: Optional[bool] = None, + max_file_size: Optional[int] = None, + redirect_stderr_to_stdout: Optional[bool] = None, + enable_network: Optional[bool] = None, + number_of_runs: Optional[int] = None, + callback_url: Optional[str] = None, ): self.source_code = source_code self.language = language @@ -123,22 +178,31 @@ def __init__( self.callback_url = callback_url # Post-execution submission attributes. - self.stdout = None - self.stderr = None - self.compile_output = None - self.message = None - self.exit_code = None - self.exit_signal = None - self.status = None - self.created_at = None - self.finished_at = None - self.token = "" - self.time = None - self.wall_time = None - self.memory = None - self.post_execution_filesystem = None + self.stdout: Optional[str] = None + self.stderr: Optional[str] = None + self.compile_output: Optional[str] = None + self.message: Optional[str] = None + self.exit_code: Optional[int] = None + self.exit_signal: Optional[int] = None + self.status: Optional[Status] = None + self.created_at: Optional[datetime] = None + self.finished_at: Optional[datetime] = None + self.token: str = "" + self.time: Optional[float] = None + self.wall_time: Optional[float] = None + self.memory: Optional[float] = None + self.post_execution_filesystem: Optional[Filesystem] = None def set_attributes(self, attributes: dict[str, Any]) -> None: + """Set Submissions attributes while taking into account different + attribute's types. + + Parameters + ---------- + attributes : dict + Key-value pairs of Submission attributes and the corresponding + value. + """ for attr, value in attributes.items(): if attr in SKIP_FIELDS: continue @@ -157,6 +221,9 @@ def set_attributes(self, attributes: dict[str, Any]) -> None: setattr(self, attr, value) def as_body(self, client: "Client") -> dict: + """Prepare Submission as a dictionary while taking into account + the `client`'s restrictions. + """ body = { "source_code": encode(self.source_code), "language_id": client.get_language_id(self.language), @@ -175,12 +242,18 @@ def as_body(self, client: "Client") -> dict: return body def is_done(self) -> bool: + """Check if submission is finished processing. + + Submission is considered finished if the submission status is not + IN_QUEUE and not PROCESSING. + """ if self.status is None: return False else: return self.status not in (Status.IN_QUEUE, Status.PROCESSING) def pre_execution_copy(self) -> "Submission": + """Create a deep copy of a submission.""" new_submission = Submission() for attr in REQUEST_FIELDS: setattr(new_submission, attr, copy.deepcopy(getattr(self, attr))) From ebaa986bceee6e85dcf6da2601a760f93ebd8e05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Sat, 30 Nov 2024 15:30:26 +0100 Subject: [PATCH 05/52] Add docstring to some Client functions. --- pyproject.toml | 2 +- src/judge0/clients.py | 72 ++++++++++++++++++++++++++++++++++++++++--- src/judge0/errors.py | 9 ++++++ 3 files changed, 78 insertions(+), 5 deletions(-) create mode 100644 src/judge0/errors.py diff --git a/pyproject.toml b/pyproject.toml index fb18e0a..f8126c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,5 +41,5 @@ test = ["pytest", "mkdocs"] [tool.flake8] docstring-convention = "numpy" -extend-ignore = ["D205", "D400", "D105", "D100", "F821"] +extend-ignore = ["D205", "D400", "D105", "D100", "D101", "D102", "F821"] max-line-length = 88 diff --git a/src/judge0/clients.py b/src/judge0/clients.py index a9a63bb..13ccdc6 100644 --- a/src/judge0/clients.py +++ b/src/judge0/clients.py @@ -29,7 +29,8 @@ def __init__( self.config = Config(**self.get_config_info()) except Exception as e: raise RuntimeError( - f"Authentication failed. Visit {self.HOME_URL} to get or review your authentication credentials." + f"Authentication failed. Visit {self.HOME_URL} to get or " + "review your authentication credentials." ) from e @handle_too_many_requests_error_for_preview_client @@ -94,6 +95,20 @@ def is_language_supported(self, language: Union[LanguageAlias, int]) -> bool: @handle_too_many_requests_error_for_preview_client def create_submission(self, submission: Submission) -> Submission: + """Send submission for execution to a client. + + Directly send a submission to create_submission route for execution. + + Parameters + ---------- + submission : Submission + A submission to create. + + Returns + ------- + Submission + A submission with updated token attribute. + """ # Check if the client supports the language specified in the submission. if not self.is_language_supported(language=submission.language): raise RuntimeError( @@ -127,8 +142,21 @@ def get_submission( *, fields: Optional[Union[str, Iterable[str]]] = None, ) -> Submission: - """Check the submission status.""" + """Get submissions status. + + Directly send submission's token to get_submission route for status + check. By default, all submissions attributes (fields) are requested. + + Parameters + ---------- + submission : Submission + Submission to update. + Returns + ------- + Submission + A Submission with updated attributes. + """ params = { "base64_encoded": "true", } @@ -154,7 +182,21 @@ def get_submission( @handle_too_many_requests_error_for_preview_client def create_submissions(self, submissions: Submissions) -> Submissions: - # Check if all submissions contain supported language. + """Send submissions for execution to a client. + + Directly send submissions to create_submissions route for execution. + Cannot handle more submissions than the client supports. + + Parameters + ---------- + submissions : Submissions + A sequence of submissions to create. + + Returns + ------- + Submissions + A sequence of submissions with updated token attribute. + """ for submission in submissions: if not self.is_language_supported(language=submission.language): raise RuntimeError( @@ -162,6 +204,9 @@ def create_submissions(self, submissions: Submissions) -> Submissions: f"{submission.language}!" ) + # TODO: Maybe raise an exception if the number of submissions is bigger + # than the batch size a client supports? + submissions_body = [submission.as_body(self) for submission in submissions] resp = requests.post( @@ -184,6 +229,24 @@ def get_submissions( *, fields: Optional[Union[str, Iterable[str]]] = None, ) -> Submissions: + """Get submissions status. + + Directly send submissions' tokens to get_submissions route for status + check. By default, all submissions attributes (fields) are requested. + Cannot handle more submissions than the client supports. + + Parameters + ---------- + submissions : Submissions + Submissions to update. + + Returns + ------- + Submissions + A sequence of submissions with updated attributes. + """ + # TODO: Maybe raise an exception if the number of submissions is bigger + # than the batch size a client supports? params = { "base64_encoded": "true", } @@ -306,7 +369,8 @@ class ATDJudge0ExtraCE(ATD): DEFAULT_ENDPOINT: str = "https://judge0-extra-ce.proxy-production.allthingsdev.co" DEFAULT_HOST: str = "Judge0-Extra-CE.allthingsdev.co" HOME_URL: str = ( - "https://www.allthingsdev.co/apimarketplace/judge0-extra-ce/66b68838b7b7ad054eb70690" + "https://www.allthingsdev.co/apimarketplace/judge0-extra-ce/" + "66b68838b7b7ad054eb70690" ) DEFAULT_ABOUT_ENDPOINT: str = "1fd631a1-be6a-47d6-bf4c-987e357e3096" diff --git a/src/judge0/errors.py b/src/judge0/errors.py new file mode 100644 index 0000000..a1835a5 --- /dev/null +++ b/src/judge0/errors.py @@ -0,0 +1,9 @@ +"""Library specific errors.""" + + +class PreviewClientLimitError(RuntimeError): + """Limited usage of a preview client exceeded.""" + + +class ClientResolutionError(RuntimeError): + """Failed resolution of an unspecified client.""" From 4d5e24b91aba7c5a079bbea7288e2f2bf4d547dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Sat, 30 Nov 2024 15:42:36 +0100 Subject: [PATCH 06/52] Replace runtime errors with library specific ones. Add docstring to some functions. --- pyproject.toml | 2 +- src/judge0/api.py | 153 ++++++++++++++++++++++++++++++++++----- src/judge0/base_types.py | 10 +-- src/judge0/utils.py | 11 +-- 4 files changed, 143 insertions(+), 33 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f8126c5..e4c72e7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,5 +41,5 @@ test = ["pytest", "mkdocs"] [tool.flake8] docstring-convention = "numpy" -extend-ignore = ["D205", "D400", "D105", "D100", "D101", "D102", "F821"] +extend-ignore = ["D205", "D400", "D105", "D100", "D101", "D102", "D103", "F821"] max-line-length = 88 diff --git a/src/judge0/api.py b/src/judge0/api.py index 43105cb..0d8504f 100644 --- a/src/judge0/api.py +++ b/src/judge0/api.py @@ -1,9 +1,9 @@ from typing import Optional, Union -from .base_types import Flavor, Iterable, TestCase, TestCases +from .base_types import Flavor, Iterable, TestCase, TestCases, TestCaseType from .clients import Client from .common import batched - +from .errors import ClientResolutionError from .retry import RegularPeriodRetry, RetryStrategy from .submission import Submission, Submissions @@ -24,6 +24,13 @@ def _resolve_client( client: Optional[Union[Client, Flavor]] = None, submissions: Optional[Union[Submission, Submissions]] = None, ) -> Client: + """Resolve a client from flavor or submission(s) arguments. + + Raises + ------ + ClientResolutionError + Raised if client resolution fails. + """ # User explicitly passed a client. if isinstance(client, Client): return client @@ -49,7 +56,7 @@ def _resolve_client( ): return client - raise RuntimeError( + raise ClientResolutionError( "Failed to resolve the client from submissions argument. " "None of the implicit clients supports all languages from the submissions. " "Please explicitly provide the client argument." @@ -61,6 +68,21 @@ def create_submissions( client: Optional[Client] = None, submissions: Optional[Union[Submission, Submissions]] = None, ) -> Union[Submission, Submissions]: + """Create submissions to a client. + + Parameters + ---------- + client : Client, optional + A Client where submissions should be created. If None, will try to + be automatically resolved. + submissions: Submission, Submissions + A submission or submissions to create. + + Raises + ------ + ClientResolutionError + Raised if client resolution fails. + """ client = _resolve_client(client=client, submissions=submissions) if isinstance(submissions, Submission): @@ -84,6 +106,21 @@ def get_submissions( submissions: Optional[Union[Submission, Submissions]] = None, fields: Optional[Union[str, Iterable[str]]] = None, ) -> Union[Submission, Submissions]: + """Create submissions to a client. + + Parameters + ---------- + client : Client, optional + A Client where submissions should be created. If None, will try to + be automatically resolved. + submissions: Submission, Submissions + A submission or submissions to create. + + Raises + ------ + ClientResolutionError + Raised if client resolution fails. + """ client = _resolve_client(client=client, submissions=submissions) if isinstance(submissions, Submission): @@ -120,20 +157,23 @@ def wait( retry_strategy = client.retry_strategy if isinstance(submissions, Submission): - submissions_to_check = { - submission.token: submission for submission in [submissions] - } + submissions_list = [submissions] else: - submissions_to_check = { - submission.token: submission for submission in submissions - } + submissions_list = submissions + + submissions_to_check = { + submission.token: submission for submission in submissions_list + } while len(submissions_to_check) > 0 and not retry_strategy.is_done(): get_submissions(client=client, submissions=list(submissions_to_check.values())) - for token in list(submissions_to_check): - submission = submissions_to_check[token] - if submission.is_done(): - submissions_to_check.pop(token) + finished_submissions = [ + token + for token, submission in submissions_to_check.items() + if submission.is_done() + ] + for token in finished_submissions: + submissions_to_check.pop(token) # Don't wait if there is no submissions to check for anymore. if len(submissions_to_check) == 0: @@ -147,12 +187,12 @@ def wait( def create_submissions_from_test_cases( submissions: Union[Submission, Submissions], - test_cases: Optional[Union[TestCase, TestCases]] = None, + test_cases: Optional[Union[TestCaseType, TestCases]] = None, ): - """Utility function for creating submissions from the (submission, test_case) pairs. + """Create submissions from the (submission, test_case) pairs. - The following table contains the return type based on the types of `submissions` - and `test_cases` arguments: + The following table contains the return type based on the types of + `submissions` and `test_cases` arguments: | submissions | test_cases | returns | |:------------|:-----------|:------------| @@ -196,10 +236,11 @@ def _execute( client: Optional[Union[Client, Flavor]] = None, submissions: Optional[Union[Submission, Submissions]] = None, source_code: Optional[str] = None, - test_cases: Optional[Union[TestCase, TestCases]] = None, + test_cases: Optional[Union[TestCaseType, TestCases]] = None, wait_for_result: bool = False, **kwargs, ) -> Union[Submission, Submissions]: + if submissions is not None and source_code is not None: raise ValueError( "Both submissions and source_code arguments are provided. " @@ -227,9 +268,45 @@ def async_execute( client: Optional[Union[Client, Flavor]] = None, submissions: Optional[Union[Submission, Submissions]] = None, source_code: Optional[str] = None, - test_cases: Optional[Union[TestCase, TestCases]] = None, + test_cases: Optional[Union[TestCaseType, TestCases]] = None, **kwargs, ) -> Union[Submission, Submissions]: + """Create submission(s). + + Parameters + ---------- + client : Client or Flavor, optional + A client where submissions should be created. If None, will try to be + resolved. + submissions : Submission or Submissions, optional + Submission or submissions for execution. + source_code: str, optional + A source code of a program. + test_cases: TestCaseType or TestCases, optional + A single test or a list of test cases + + Returns + ------- + Submission or Submissions + A single submission or a list of submissions. + + The following table contains the return type based on the types of + `submissions` (or `source_code`) and `test_cases` arguments: + + | submissions | test_cases | returns | + |:------------|:-----------|:------------| + | Submission | TestCase | Submission | + | Submission | TestCases | Submissions | + | Submissions | TestCase | Submissions | + | Submissions | TestCases | Submissions | + + Raises + ------ + ClientResolutionError + If client cannot be resolved from the submissions or the flavor. + ValueError + If both or neither submissions and source_code arguments are provided. + """ return _execute( client=client, submissions=submissions, @@ -245,9 +322,45 @@ def sync_execute( client: Optional[Union[Client, Flavor]] = None, submissions: Optional[Union[Submission, Submissions]] = None, source_code: Optional[str] = None, - test_cases: Optional[Union[TestCase, TestCases]] = None, + test_cases: Optional[Union[TestCaseType, TestCases]] = None, **kwargs, ) -> Union[Submission, Submissions]: + """Create submission(s) and wait for their finish. + + Parameters + ---------- + client : Client or Flavor, optional + A client where submissions should be created. If None, will try to be + resolved. + submissions : Submission or Submissions, optional + Submission or submissions for execution. + source_code: str, optional + A source code of a program. + test_cases: TestCaseType or TestCases, optional + A single test or a list of test cases + + Returns + ------- + Submission or Submissions + A single submission or a list of submissions. + + The following table contains the return type based on the types of + `submissions` (or `source_code`) and `test_cases` arguments: + + | submissions | test_cases | returns | + |:------------|:-----------|:------------| + | Submission | TestCase | Submission | + | Submission | TestCases | Submissions | + | Submissions | TestCase | Submissions | + | Submissions | TestCases | Submissions | + + Raises + ------ + ClientResolutionError + If client cannot be resolved from the submissions or the flavor. + ValueError + If both or neither submissions and source_code arguments are provided. + """ return _execute( client=client, submissions=submissions, diff --git a/src/judge0/base_types.py b/src/judge0/base_types.py index e99ce19..e89c8d5 100644 --- a/src/judge0/base_types.py +++ b/src/judge0/base_types.py @@ -5,21 +5,17 @@ Iterable = Sequence -TestCases = Iterable["TestCase"] +TestCaseType = Union["TestCase", list, tuple, dict] +TestCases = Iterable[TestCaseType] @dataclass(frozen=True) class TestCase: - # Needed to disable pytest from recognizing it as a class containing different test cases. - __test__ = False - input: Optional[str] = None expected_output: Optional[str] = None @staticmethod - def from_record( - test_case: Optional[Union[tuple, list, dict, "TestCase"]] = None - ) -> "TestCase": + def from_record(test_case: Optional[TestCaseType] = None) -> "TestCase": if isinstance(test_case, (tuple, list)): test_case = { field: value diff --git a/src/judge0/utils.py b/src/judge0/utils.py index 184c368..e38b41f 100644 --- a/src/judge0/utils.py +++ b/src/judge0/utils.py @@ -5,6 +5,8 @@ from requests import HTTPError +from .errors import PreviewClientLimitError + def is_http_too_many_requests_error(exception: Exception) -> bool: return ( @@ -31,11 +33,10 @@ def wrapper(*args, **kwargs): class_name in ("SuluJudge0CE", "SuluJudge0ExtraCE") and instance.api_key is None ): - raise RuntimeError( - "You are using a preview version of the Sulu " - "clients and you've hit a rate limit on the preview " - f"clients. Visit {instance.HOME_URL} to get or " - "review your authentication credentials." + raise PreviewClientLimitError( + "You are using a preview version of a client and " + f"you've hit a rate limit on it. Visit {instance.HOME_URL} " + "to get your authentication credentials." ) from err else: raise err from None From a63bae6e2f97edc6d9ccf13b484ea0e4e0c31c7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Sat, 30 Nov 2024 19:49:55 +0100 Subject: [PATCH 07/52] Use Session object for requests in client methods. --- src/judge0/clients.py | 64 +++++++++++++++++++++++++------------------ 1 file changed, 37 insertions(+), 27 deletions(-) diff --git a/src/judge0/clients.py b/src/judge0/clients.py index 13ccdc6..4f63850 100644 --- a/src/judge0/clients.py +++ b/src/judge0/clients.py @@ -22,6 +22,7 @@ def __init__( self.endpoint = endpoint self.auth_headers = auth_headers self.retry_strategy = retry_strategy + self.session = requests.Session() # TODO: Should be handled differently. try: @@ -33,46 +34,49 @@ def __init__( "review your authentication credentials." ) from e + def __del__(self): + self.session.close() + @handle_too_many_requests_error_for_preview_client def get_about(self) -> dict: - r = requests.get( + response = self.session.get( f"{self.endpoint}/about", headers=self.auth_headers, ) - r.raise_for_status() - return r.json() + response.raise_for_status() + return response.json() @handle_too_many_requests_error_for_preview_client def get_config_info(self) -> dict: - r = requests.get( + response = self.session.get( f"{self.endpoint}/config_info", headers=self.auth_headers, ) - r.raise_for_status() - return r.json() + response.raise_for_status() + return response.json() @handle_too_many_requests_error_for_preview_client def get_language(self, language_id) -> dict: request_url = f"{self.endpoint}/languages/{language_id}" - r = requests.get(request_url, headers=self.auth_headers) - r.raise_for_status() - return r.json() + response = self.session.get(request_url, headers=self.auth_headers) + response.raise_for_status() + return response.json() @handle_too_many_requests_error_for_preview_client def get_languages(self) -> list[dict]: request_url = f"{self.endpoint}/languages" - r = requests.get(request_url, headers=self.auth_headers) - r.raise_for_status() - return r.json() + response = self.session.get(request_url, headers=self.auth_headers) + response.raise_for_status() + return response.json() @handle_too_many_requests_error_for_preview_client def get_statuses(self) -> list[dict]: - r = requests.get( + response = self.session.get( f"{self.endpoint}/statuses", headers=self.auth_headers, ) - r.raise_for_status() - return r.json() + response.raise_for_status() + return response.json() @property def version(self): @@ -123,15 +127,15 @@ def create_submission(self, submission: Submission) -> Submission: body = submission.as_body(self) - resp = requests.post( + response = self.session.post( f"{self.endpoint}/submissions", json=body, params=params, headers=self.auth_headers, ) - resp.raise_for_status() + response.raise_for_status() - submission.set_attributes(resp.json()) + submission.set_attributes(response.json()) return submission @@ -169,14 +173,14 @@ def get_submission( else: params["fields"] = "*" - resp = requests.get( + response = self.session.get( f"{self.endpoint}/submissions/{submission.token}", params=params, headers=self.auth_headers, ) - resp.raise_for_status() + response.raise_for_status() - submission.set_attributes(resp.json()) + submission.set_attributes(response.json()) return submission @@ -209,15 +213,15 @@ def create_submissions(self, submissions: Submissions) -> Submissions: submissions_body = [submission.as_body(self) for submission in submissions] - resp = requests.post( + response = self.session.post( f"{self.endpoint}/submissions/batch", headers=self.auth_headers, params={"base64_encoded": "true"}, json={"submissions": submissions_body}, ) - resp.raise_for_status() + response.raise_for_status() - for submission, attrs in zip(submissions, resp.json()): + for submission, attrs in zip(submissions, response.json()): submission.set_attributes(attrs) return submissions @@ -262,20 +266,22 @@ def get_submissions( tokens = ",".join(submission.token for submission in submissions) params["tokens"] = tokens - resp = requests.get( + response = self.session.get( f"{self.endpoint}/submissions/batch", params=params, headers=self.auth_headers, ) - resp.raise_for_status() + response.raise_for_status() - for submission, attrs in zip(submissions, resp.json()["submissions"]): + for submission, attrs in zip(submissions, response.json()["submissions"]): submission.set_attributes(attrs) return submissions class ATD(Client): + """Base class for all AllThingsDev clients.""" + API_KEY_ENV = "JUDGE0_ATD_API_KEY" def __init__(self, endpoint, host_header_value, api_key, **kwargs): @@ -439,6 +445,8 @@ def get_submissions( class Rapid(Client): + """Base class for all RapidAPI clients.""" + API_KEY_ENV = "JUDGE0_RAPID_API_KEY" def __init__(self, endpoint, host_header_value, api_key, **kwargs): @@ -482,6 +490,8 @@ def __init__(self, api_key, **kwargs): class Sulu(Client): + """Base class for all Sulu clients.""" + API_KEY_ENV = "JUDGE0_SULU_API_KEY" def __init__(self, endpoint, api_key=None, **kwargs): From 41aec341ba26d0dc57661dade491d23e7d9e6ad0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Sun, 1 Dec 2024 19:45:55 +0100 Subject: [PATCH 08/52] Minor docstring update in api functions. --- src/judge0/api.py | 44 ++++++++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/src/judge0/api.py b/src/judge0/api.py index 0d8504f..fabcd4f 100644 --- a/src/judge0/api.py +++ b/src/judge0/api.py @@ -273,6 +273,18 @@ def async_execute( ) -> Union[Submission, Submissions]: """Create submission(s). + Aliases: `async_run`. + + The following table contains the return type based on the types of + `submissions` (or `source_code`) and `test_cases` arguments: + + | submissions | test_cases | returns | + |:------------|:-----------|:------------| + | Submission | TestCase | Submission | + | Submission | TestCases | Submissions | + | Submissions | TestCase | Submissions | + | Submissions | TestCases | Submissions | + Parameters ---------- client : Client or Flavor, optional @@ -290,16 +302,6 @@ def async_execute( Submission or Submissions A single submission or a list of submissions. - The following table contains the return type based on the types of - `submissions` (or `source_code`) and `test_cases` arguments: - - | submissions | test_cases | returns | - |:------------|:-----------|:------------| - | Submission | TestCase | Submission | - | Submission | TestCases | Submissions | - | Submissions | TestCase | Submissions | - | Submissions | TestCases | Submissions | - Raises ------ ClientResolutionError @@ -327,6 +329,18 @@ def sync_execute( ) -> Union[Submission, Submissions]: """Create submission(s) and wait for their finish. + Aliases: `execute`, `run`, `sync_run`. + + The following table contains the return type based on the types of + `submissions` (or `source_code`) and `test_cases` arguments: + + | submissions | test_cases | returns | + |:------------|:-----------|:------------| + | Submission | TestCase | Submission | + | Submission | TestCases | Submissions | + | Submissions | TestCase | Submissions | + | Submissions | TestCases | Submissions | + Parameters ---------- client : Client or Flavor, optional @@ -344,16 +358,6 @@ def sync_execute( Submission or Submissions A single submission or a list of submissions. - The following table contains the return type based on the types of - `submissions` (or `source_code`) and `test_cases` arguments: - - | submissions | test_cases | returns | - |:------------|:-----------|:------------| - | Submission | TestCase | Submission | - | Submission | TestCases | Submissions | - | Submissions | TestCase | Submissions | - | Submissions | TestCases | Submissions | - Raises ------ ClientResolutionError From 755ee9adfa64980659a8b9d0f8564baadf1a71ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Mon, 2 Dec 2024 18:42:15 +0100 Subject: [PATCH 09/52] Remove Pipenv file. Add test/dev dependencies to pyproject.toml. --- .github/workflows/test.yml | 11 ++++++----- Pipfile | 19 ------------------- pyproject.toml | 11 +++++++++-- 3 files changed, 15 insertions(+), 26 deletions(-) delete mode 100644 Pipfile diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 62f57c9..86a0ddd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,7 +9,7 @@ permissions: jobs: test: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 @@ -19,10 +19,10 @@ jobs: python-version: "3.9" - name: Install dependencies run: | + python -m venv venv + source venv/bin/activate python -m pip install --upgrade pip - pip install pipenv - pipenv install --dev - pipenv install -e . + pip install -e .[test] - name: Test with pytest env: # Add necessary api keys as env variables. JUDGE0_ATD_API_KEY: ${{ secrets.JUDGE0_ATD_API_KEY }} @@ -33,4 +33,5 @@ jobs: JUDGE0_TEST_CE_ENDPOINT: ${{ secrets.JUDGE0_TEST_CE_ENDPOINT }} JUDGE0_TEST_EXTRA_CE_ENDPOINT: ${{ secrets.JUDGE0_TEST_EXTRA_CE_ENDPOINT }} run: | - pipenv run pytest -vv tests/ + source venv/bin/activate + pytest -vv tests/ diff --git a/Pipfile b/Pipfile deleted file mode 100644 index ec5d4e1..0000000 --- a/Pipfile +++ /dev/null @@ -1,19 +0,0 @@ -[[source]] -url = "https://pypi.org/simple" -verify_ssl = true -name = "pypi" - -[packages] -requests = "==2.32.3" - -[dev-packages] -ufmt = "==2.7.3" -pre-commit = "==3.8.0" -pytest = "==8.3.3" -python-dotenv = "==1.0.1" -pytest-cov = "6.0.0" -flake8-docstrings = "1.7.0" - -[requires] -python_version = "3.9" -python_full_version = "3.9.20" diff --git a/pyproject.toml b/pyproject.toml index e4c72e7..de23861 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ classifiers = [ "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed", ] -dependencies = ["requests>=2.32.3"] +dependencies = ["requests>=2.28.0,<3.0.0"] [build-system] requires = ["setuptools>=70.0"] @@ -37,7 +37,14 @@ Repository = "https://github.com/judge0/judge0-python.git" Issues = "https://github.com/judge0/judge0-python/issues" [project.optional-dependencies] -test = ["pytest", "mkdocs"] +test = [ + "ufmt==2.7.3", + "pre-commit==3.8.0", + "pytest==8.3.3", + "python-dotenv==1.0.1", + "pytest-cov==6.0.0", + "flake8-docstrings==1.7.0", +] [tool.flake8] docstring-convention = "numpy" From 3c022a0de493d613c67e3e791d094e4ad1beff0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Mon, 2 Dec 2024 18:54:23 +0100 Subject: [PATCH 10/52] Add CONTRIBUTING. --- CONTRIBUTING.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..f5ecd00 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,25 @@ +# How to contribute + +## Preparing the development setup + +1. Install Python 3.9 + +```bash +sudo add-apt-repository ppa:deadsnakes/ppa +sudo apt update +sudo apt install python3.9 python3.9-venv +``` + +2. Clone the repo, create and activate a new virtual environment + +```bash +cd judge0-python +python3.9 -m venv venv +source venv/bin/activate +``` + +3. Install the library and development dependencies + +```bash +pip install -e .[test] +``` From e9fc9da0f9c956a80d82723a1746f784dc19f729 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Mon, 2 Dec 2024 20:19:46 +0100 Subject: [PATCH 11/52] Minor update to CONTRIBUTING. --- CONTRIBUTING.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f5ecd00..e85b338 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -22,4 +22,5 @@ source venv/bin/activate ```bash pip install -e .[test] +pre-commit install ``` From b9c39ce0345052b5ef8133fa15e62950f35c46ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Sat, 7 Dec 2024 19:33:33 +0100 Subject: [PATCH 12/52] Add to_dict and from_dict to Submission and Filesystem objects. --- src/judge0/base_types.py | 16 ++++++++++----- src/judge0/common.py | 4 ++-- src/judge0/filesystem.py | 22 +++++++++++++++++++-- src/judge0/submission.py | 42 +++++++++++++++++++++++++++++++++++++--- 4 files changed, 72 insertions(+), 12 deletions(-) diff --git a/src/judge0/base_types.py b/src/judge0/base_types.py index e89c8d5..f6db9eb 100644 --- a/src/judge0/base_types.py +++ b/src/judge0/base_types.py @@ -1,7 +1,6 @@ -from abc import ABC, abstractmethod from dataclasses import dataclass from enum import IntEnum -from typing import Optional, Sequence, Union +from typing import Optional, Protocol, runtime_checkable, Sequence, Union Iterable = Sequence @@ -33,10 +32,11 @@ def from_record(test_case: Optional[TestCaseType] = None) -> "TestCase": ) -class Encodeable(ABC): - @abstractmethod +@runtime_checkable +class Encodeable(Protocol): def encode(self) -> bytes: - pass + """Serialize the object to bytes.""" + ... @dataclass(frozen=True) @@ -46,6 +46,8 @@ class Language: class LanguageAlias(IntEnum): + """Language enumeration.""" + PYTHON = 0 CPP = 1 JAVA = 2 @@ -55,11 +57,15 @@ class LanguageAlias(IntEnum): class Flavor(IntEnum): + """Judge0 flavor enumeration.""" + CE = 0 EXTRA_CE = 1 class Status(IntEnum): + """Status enumeration.""" + IN_QUEUE = 1 PROCESSING = 2 ACCEPTED = 3 diff --git a/src/judge0/common.py b/src/judge0/common.py index 736895e..1f07b63 100644 --- a/src/judge0/common.py +++ b/src/judge0/common.py @@ -2,7 +2,7 @@ from itertools import islice from typing import Union -from .base_types import Encodeable +from judge0.base_types import Encodeable def encode(content: Union[bytes, str, Encodeable]) -> str: @@ -26,7 +26,7 @@ def decode(content: Union[bytes, str]) -> str: def batched(iterable, n): - """Utility function for batching submissions. + """Iterate over an iterable in batches of a specified size. Adapted from https://docs.python.org/3/library/itertools.html#itertools.batched. """ diff --git a/src/judge0/filesystem.py b/src/judge0/filesystem.py index bbdb11b..1150781 100644 --- a/src/judge0/filesystem.py +++ b/src/judge0/filesystem.py @@ -5,7 +5,7 @@ from base64 import b64decode, b64encode from typing import Optional, Union -from .base_types import Encodeable, Iterable +from .base_types import Iterable class File: @@ -24,7 +24,7 @@ def __str__(self): return self.content.decode(errors="backslashreplace") -class Filesystem(Encodeable): +class Filesystem: def __init__( self, content: Optional[Union[str, bytes, File, Iterable[File], "Filesystem"]] = None, @@ -47,6 +47,14 @@ def __init__( self.files = [content] elif isinstance(content, Filesystem): self.files = copy.deepcopy(content.files) + elif content is None: + pass + else: + raise ValueError( + "Unsupported type for content argument. Expected " + "one of str, bytes, File, Iterable[File], or Filesystem, " + f"got {type(content)}." + ) def __repr__(self) -> str: content_encoded = b64encode(self.encode()).decode() @@ -59,7 +67,17 @@ def encode(self) -> bytes: zip_file.writestr(file.name, file.content) return zip_buffer.getvalue() + def to_dict(self) -> dict: + """Pack the Filesystem object to a dictionary.""" + return {"filesystem": str(self)} + + @staticmethod + def from_dict(filesystem_dict: dict) -> "Filesystem": + """Create a Filesystem object from dictionary.""" + return Filesystem(filesystem_dict.get("filesystem")) + def __str__(self) -> str: + """Create string representation of Filesystem object.""" return b64encode(self.encode()).decode() def __iter__(self): diff --git a/src/judge0/submission.py b/src/judge0/submission.py index 55a7d3c..ddfee95 100644 --- a/src/judge0/submission.py +++ b/src/judge0/submission.py @@ -17,7 +17,7 @@ "stdout", "stderr", "compile_output", - # "post_execution_filesystem", + "post_execution_filesystem", } ENCODED_FIELDS = ENCODED_REQUEST_FIELDS | ENCODED_RESPONSE_FIELDS EXTRA_REQUEST_FIELDS = { @@ -48,7 +48,6 @@ "time", "wall_time", "memory", - "post_execution_filesystem", } REQUEST_FIELDS = ENCODED_REQUEST_FIELDS | EXTRA_REQUEST_FIELDS RESPONSE_FIELDS = ENCODED_RESPONSE_FIELDS | EXTRA_RESPONSE_FIELDS @@ -207,7 +206,7 @@ def set_attributes(self, attributes: dict[str, Any]) -> None: if attr in SKIP_FIELDS: continue - if attr in ENCODED_FIELDS: + if attr in ENCODED_FIELDS and attr not in ("post_execution_filesystem",): value = decode(value) if value else None elif attr == "status": value = Status(value["id"]) @@ -241,6 +240,43 @@ def as_body(self, client: "Client") -> dict: return body + def to_dict(self) -> dict: + encoded_request_fields = { + field_name: encode(getattr(self, field_name)) + for field_name in ENCODED_REQUEST_FIELDS + if getattr(self, field_name) is not None + } + extra_request_fields = { + field_name: getattr(self, field_name) + for field_name in EXTRA_REQUEST_FIELDS + if getattr(self, field_name) is not None + } + encoded_response_fields = { + field_name: encode(getattr(self, field_name)) + for field_name in ENCODED_RESPONSE_FIELDS + if getattr(self, field_name) is not None + } + extra_response_fields = { + field_name: getattr(self, field_name) + for field_name in EXTRA_RESPONSE_FIELDS + if getattr(self, field_name) is not None + } + + submission_dict = ( + encoded_request_fields + | extra_request_fields + | encoded_response_fields + | extra_response_fields + ) + + return submission_dict + + @staticmethod + def from_dict(submission_dict) -> "Submission": + submission = Submission() + submission.set_attributes(submission_dict) + return submission + def is_done(self) -> bool: """Check if submission is finished processing. From 13703bc4594663080311a008136e30069e9832ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Mon, 9 Dec 2024 17:58:47 +0100 Subject: [PATCH 13/52] Add pydantic as dependency. Make Language and Config classes inherit BaseModel. --- pyproject.toml | 4 ++-- src/judge0/base_types.py | 11 +++++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index de23861..01fc8e2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "judge0" -version = "0.0.1" +version = "0.0.2dev" description = "The official Python library for Judge0." readme = "README.md" requires-python = ">=3.9" @@ -25,7 +25,7 @@ classifiers = [ "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed", ] -dependencies = ["requests>=2.28.0,<3.0.0"] +dependencies = ["requests>=2.28.0,<3.0.0", "pydantic>=2.0.0,<3.0.0"] [build-system] requires = ["setuptools>=70.0"] diff --git a/src/judge0/base_types.py b/src/judge0/base_types.py index f6db9eb..0c7f450 100644 --- a/src/judge0/base_types.py +++ b/src/judge0/base_types.py @@ -2,6 +2,8 @@ from enum import IntEnum from typing import Optional, Protocol, runtime_checkable, Sequence, Union +from pydantic import BaseModel + Iterable = Sequence TestCaseType = Union["TestCase", list, tuple, dict] @@ -15,6 +17,7 @@ class TestCase: @staticmethod def from_record(test_case: Optional[TestCaseType] = None) -> "TestCase": + """Create a TestCase from built-in types.""" if isinstance(test_case, (tuple, list)): test_case = { field: value @@ -39,8 +42,7 @@ def encode(self) -> bytes: ... -@dataclass(frozen=True) -class Language: +class Language(BaseModel): id: int name: str @@ -85,8 +87,9 @@ def __str__(self): return self.name.lower().replace("_", " ").title() -@dataclass(frozen=True) -class Config: +class Config(BaseModel): + """Client config data.""" + allow_enable_network: bool allow_enable_per_process_and_thread_memory_limit: bool allow_enable_per_process_and_thread_time_limit: bool From e797c72b141cf1a5ceba133f36062908fabe52aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Mon, 9 Dec 2024 18:18:30 +0100 Subject: [PATCH 14/52] Make Submissions, File, Filesystem work with pydantic's BaseModel. --- src/judge0/filesystem.py | 44 ++++++------ src/judge0/submission.py | 146 +++++++++++---------------------------- 2 files changed, 60 insertions(+), 130 deletions(-) diff --git a/src/judge0/filesystem.py b/src/judge0/filesystem.py index 1150781..6773680 100644 --- a/src/judge0/filesystem.py +++ b/src/judge0/filesystem.py @@ -5,18 +5,22 @@ from base64 import b64decode, b64encode from typing import Optional, Union +from pydantic import BaseModel + from .base_types import Iterable -class File: - def __init__(self, name: str, content: Optional[Union[str, bytes]] = None): - self.name = name +class File(BaseModel): + name: str + content: Optional[Union[str, bytes]] = None + def __init__(self, **data): + super().__init__(**data) # Let's keep content attribute internally encoded as bytes. - if isinstance(content, str): - self.content = content.encode() - elif isinstance(content, bytes): - self.content = content + if isinstance(self.content, str): + self.content = self.content.encode() + elif isinstance(self.content, bytes): + self.content = self.content else: self.content = b"" @@ -24,12 +28,13 @@ def __str__(self): return self.content.decode(errors="backslashreplace") -class Filesystem: - def __init__( - self, - content: Optional[Union[str, bytes, File, Iterable[File], "Filesystem"]] = None, - ): - self.files: list[File] = [] +class Filesystem(BaseModel): + files: list[File] = [] + + def __init__(self, **data): + content = data.pop("content", None) + super().__init__(**data) + self.files = [] if isinstance(content, (bytes, str)): if isinstance(content, bytes): @@ -40,7 +45,7 @@ def __init__( with zipfile.ZipFile(io.BytesIO(zip_bytes), "r") as zip_file: for file_name in zip_file.namelist(): with zip_file.open(file_name) as fp: - self.files.append(File(file_name, fp.read())) + self.files.append(File(name=file_name, content=fp.read())) elif isinstance(content, Iterable): self.files = list(content) elif isinstance(content, File): @@ -48,7 +53,7 @@ def __init__( elif isinstance(content, Filesystem): self.files = copy.deepcopy(content.files) elif content is None: - pass + self.files = [] else: raise ValueError( "Unsupported type for content argument. Expected " @@ -67,15 +72,6 @@ def encode(self) -> bytes: zip_file.writestr(file.name, file.content) return zip_buffer.getvalue() - def to_dict(self) -> dict: - """Pack the Filesystem object to a dictionary.""" - return {"filesystem": str(self)} - - @staticmethod - def from_dict(filesystem_dict: dict) -> "Filesystem": - """Create a Filesystem object from dictionary.""" - return Filesystem(filesystem_dict.get("filesystem")) - def __str__(self) -> str: """Create string representation of Filesystem object.""" return b64encode(self.encode()).decode() diff --git a/src/judge0/submission.py b/src/judge0/submission.py index ddfee95..57a5cc6 100644 --- a/src/judge0/submission.py +++ b/src/judge0/submission.py @@ -2,10 +2,11 @@ from datetime import datetime from typing import Any, Optional, Union -from judge0.filesystem import Filesystem +from pydantic import BaseModel from .base_types import Iterable, LanguageAlias, Status from .common import decode, encode +from .filesystem import Filesystem ENCODED_REQUEST_FIELDS = { "source_code", @@ -65,7 +66,7 @@ Submissions = Iterable["Submission"] -class Submission: +class Submission(BaseModel): """ Stores a representation of a Submission to/from Judge0. @@ -125,72 +126,42 @@ class Submission: URL for a callback to report execution results or status. """ - def __init__( - self, - *, - source_code: Optional[str] = None, - language: Union[LanguageAlias, int] = LanguageAlias.PYTHON, - additional_files: Optional[str] = None, - compiler_options: Optional[str] = None, - command_line_arguments: Optional[str] = None, - stdin: Optional[str] = None, - expected_output: Optional[str] = None, - cpu_time_limit: Optional[float] = None, - cpu_extra_time: Optional[float] = None, - wall_time_limit: Optional[float] = None, - memory_limit: Optional[float] = None, - stack_limit: Optional[int] = None, - max_processes_and_or_threads: Optional[int] = None, - enable_per_process_and_thread_time_limit: Optional[bool] = None, - enable_per_process_and_thread_memory_limit: Optional[bool] = None, - max_file_size: Optional[int] = None, - redirect_stderr_to_stdout: Optional[bool] = None, - enable_network: Optional[bool] = None, - number_of_runs: Optional[int] = None, - callback_url: Optional[str] = None, - ): - self.source_code = source_code - self.language = language - self.additional_files = additional_files - - # Extra pre-execution submission attributes. - self.compiler_options = compiler_options - self.command_line_arguments = command_line_arguments - self.stdin = stdin - self.expected_output = expected_output - self.cpu_time_limit = cpu_time_limit - self.cpu_extra_time = cpu_extra_time - self.wall_time_limit = wall_time_limit - self.memory_limit = memory_limit - self.stack_limit = stack_limit - self.max_processes_and_or_threads = max_processes_and_or_threads - self.enable_per_process_and_thread_time_limit = ( - enable_per_process_and_thread_time_limit - ) - self.enable_per_process_and_thread_memory_limit = ( - enable_per_process_and_thread_memory_limit - ) - self.max_file_size = max_file_size - self.redirect_stderr_to_stdout = redirect_stderr_to_stdout - self.enable_network = enable_network - self.number_of_runs = number_of_runs - self.callback_url = callback_url - - # Post-execution submission attributes. - self.stdout: Optional[str] = None - self.stderr: Optional[str] = None - self.compile_output: Optional[str] = None - self.message: Optional[str] = None - self.exit_code: Optional[int] = None - self.exit_signal: Optional[int] = None - self.status: Optional[Status] = None - self.created_at: Optional[datetime] = None - self.finished_at: Optional[datetime] = None - self.token: str = "" - self.time: Optional[float] = None - self.wall_time: Optional[float] = None - self.memory: Optional[float] = None - self.post_execution_filesystem: Optional[Filesystem] = None + source_code: Optional[str] = None + language: Union[LanguageAlias, int] = LanguageAlias.PYTHON + additional_files: Optional[str] = None + compiler_options: Optional[str] = None + command_line_arguments: Optional[str] = None + stdin: Optional[str] = None + expected_output: Optional[str] = None + cpu_time_limit: Optional[float] = None + cpu_extra_time: Optional[float] = None + wall_time_limit: Optional[float] = None + memory_limit: Optional[float] = None + stack_limit: Optional[int] = None + max_processes_and_or_threads: Optional[int] = None + enable_per_process_and_thread_time_limit: Optional[bool] = None + enable_per_process_and_thread_memory_limit: Optional[bool] = None + max_file_size: Optional[int] = None + redirect_stderr_to_stdout: Optional[bool] = None + enable_network: Optional[bool] = None + number_of_runs: Optional[int] = None + callback_url: Optional[str] = None + + # Post-execution submission attributes. + stdout: Optional[str] = None + stderr: Optional[str] = None + compile_output: Optional[str] = None + message: Optional[str] = None + exit_code: Optional[int] = None + exit_signal: Optional[int] = None + status: Optional[Status] = None + created_at: Optional[datetime] = None + finished_at: Optional[datetime] = None + token: str = "" + time: Optional[float] = None + wall_time: Optional[float] = None + memory: Optional[float] = None + post_execution_filesystem: Optional[Filesystem] = None def set_attributes(self, attributes: dict[str, Any]) -> None: """Set Submissions attributes while taking into account different @@ -215,7 +186,7 @@ def set_attributes(self, attributes: dict[str, Any]) -> None: elif attr in FLOATING_POINT_FIELDS and value is not None: value = float(value) elif attr == "post_execution_filesystem": - value = Filesystem(value) + value = Filesystem(content=value) setattr(self, attr, value) @@ -240,43 +211,6 @@ def as_body(self, client: "Client") -> dict: return body - def to_dict(self) -> dict: - encoded_request_fields = { - field_name: encode(getattr(self, field_name)) - for field_name in ENCODED_REQUEST_FIELDS - if getattr(self, field_name) is not None - } - extra_request_fields = { - field_name: getattr(self, field_name) - for field_name in EXTRA_REQUEST_FIELDS - if getattr(self, field_name) is not None - } - encoded_response_fields = { - field_name: encode(getattr(self, field_name)) - for field_name in ENCODED_RESPONSE_FIELDS - if getattr(self, field_name) is not None - } - extra_response_fields = { - field_name: getattr(self, field_name) - for field_name in EXTRA_RESPONSE_FIELDS - if getattr(self, field_name) is not None - } - - submission_dict = ( - encoded_request_fields - | extra_request_fields - | encoded_response_fields - | extra_response_fields - ) - - return submission_dict - - @staticmethod - def from_dict(submission_dict) -> "Submission": - submission = Submission() - submission.set_attributes(submission_dict) - return submission - def is_done(self) -> bool: """Check if submission is finished processing. From e9fc9334c4a5b0ba68f2c800a0bfac28f7d44014 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Tue, 10 Dec 2024 17:34:36 +0100 Subject: [PATCH 15/52] Make Submission work with pydantic. --- .../1000_http_callback_aka_webhook/main.py | 33 +++-- src/judge0/common.py | 8 +- src/judge0/submission.py | 122 ++++++++++++------ tests/test_submission.py | 47 +++++++ 4 files changed, 149 insertions(+), 61 deletions(-) diff --git a/examples/1000_http_callback_aka_webhook/main.py b/examples/1000_http_callback_aka_webhook/main.py index dab2b77..a65411d 100755 --- a/examples/1000_http_callback_aka_webhook/main.py +++ b/examples/1000_http_callback_aka_webhook/main.py @@ -1,18 +1,10 @@ #!/usr/bin/env python3 -from fastapi import FastAPI, Depends -from pydantic import BaseModel - -import uvicorn import asyncio -import judge0 +import judge0 -class CallbackResponse(BaseModel): - created_at: str - finished_at: str - language: dict - status: dict - stdout: str +import uvicorn +from fastapi import Depends, FastAPI class AppContext: @@ -47,13 +39,14 @@ async def root(app_context=Depends(get_app_context)): @app.put("/callback") -async def callback(response: CallbackResponse): +async def callback(response: judge0.Submission): print(f"Received: {response}") -# We are using free service from https://localhost.run to get a public URL for our local server. -# This approach is not recommended for production use. It is only for demonstration purposes -# since domain names change regularly and there is a speed limit for the free service. +# We are using free service from https://localhost.run to get a public URL for +# our local server. This approach is not recommended for production use. It is +# only for demonstration purposes since domain names change regularly and there +# is a speed limit for the free service. async def run_ssh_tunnel(): app_context = get_app_context() @@ -69,7 +62,9 @@ async def run_ssh_tunnel(): ] process = await asyncio.create_subprocess_exec( - *command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT + *command, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.STDOUT, ) while True: @@ -86,7 +81,11 @@ async def run_ssh_tunnel(): async def run_server(): config = uvicorn.Config( - app, host="127.0.0.1", port=LOCAL_SERVER_PORT, workers=5, loop="asyncio" + app, + host="127.0.0.1", + port=LOCAL_SERVER_PORT, + workers=5, + loop="asyncio", ) server = uvicorn.Server(config) await server.serve() diff --git a/src/judge0/common.py b/src/judge0/common.py index 1f07b63..57ad838 100644 --- a/src/judge0/common.py +++ b/src/judge0/common.py @@ -17,11 +17,13 @@ def encode(content: Union[bytes, str, Encodeable]) -> str: def decode(content: Union[bytes, str]) -> str: if isinstance(content, bytes): - return b64decode(content.decode(errors="backslashreplace")).decode( + return b64decode( + content.decode(errors="backslashreplace"), validate=True + ).decode(errors="backslashreplace") + if isinstance(content, str): + return b64decode(content.encode(), validate=True).decode( errors="backslashreplace" ) - if isinstance(content, str): - return b64decode(content.encode()).decode(errors="backslashreplace") raise ValueError(f"Unsupported type. Expected bytes or str, got {type(content)}!") diff --git a/src/judge0/submission.py b/src/judge0/submission.py index 57a5cc6..069733c 100644 --- a/src/judge0/submission.py +++ b/src/judge0/submission.py @@ -2,7 +2,7 @@ from datetime import datetime from typing import Any, Optional, Union -from pydantic import BaseModel +from pydantic import BaseModel, ConfigDict, Field, field_validator, UUID4 from .base_types import Iterable, LanguageAlias, Status from .common import decode, encode @@ -18,7 +18,7 @@ "stdout", "stderr", "compile_output", - "post_execution_filesystem", + # "post_execution_filesystem", } ENCODED_FIELDS = ENCODED_REQUEST_FIELDS | ENCODED_RESPONSE_FIELDS EXTRA_REQUEST_FIELDS = { @@ -126,42 +126,86 @@ class Submission(BaseModel): URL for a callback to report execution results or status. """ - source_code: Optional[str] = None - language: Union[LanguageAlias, int] = LanguageAlias.PYTHON - additional_files: Optional[str] = None - compiler_options: Optional[str] = None - command_line_arguments: Optional[str] = None - stdin: Optional[str] = None - expected_output: Optional[str] = None - cpu_time_limit: Optional[float] = None - cpu_extra_time: Optional[float] = None - wall_time_limit: Optional[float] = None - memory_limit: Optional[float] = None - stack_limit: Optional[int] = None - max_processes_and_or_threads: Optional[int] = None - enable_per_process_and_thread_time_limit: Optional[bool] = None - enable_per_process_and_thread_memory_limit: Optional[bool] = None - max_file_size: Optional[int] = None - redirect_stderr_to_stdout: Optional[bool] = None - enable_network: Optional[bool] = None - number_of_runs: Optional[int] = None - callback_url: Optional[str] = None + source_code: Optional[str] = Field(default=None, repr=True) + language: Union[LanguageAlias, int] = Field( + default=LanguageAlias.PYTHON, + repr=True, + ) + additional_files: Optional[str] = Field(default=None, repr=True) + compiler_options: Optional[str] = Field(default=None, repr=True) + command_line_arguments: Optional[str] = Field(default=None, repr=True) + stdin: Optional[str] = Field(default=None, repr=True) + expected_output: Optional[str] = Field(default=None, repr=True) + cpu_time_limit: Optional[float] = Field(default=None, repr=True) + cpu_extra_time: Optional[float] = Field(default=None, repr=True) + wall_time_limit: Optional[float] = Field(default=None, repr=True) + memory_limit: Optional[float] = Field(default=None, repr=True) + stack_limit: Optional[int] = Field(default=None, repr=True) + max_processes_and_or_threads: Optional[int] = Field(default=None, repr=True) + enable_per_process_and_thread_time_limit: Optional[bool] = Field( + default=None, repr=True + ) + enable_per_process_and_thread_memory_limit: Optional[bool] = Field( + default=None, repr=True + ) + max_file_size: Optional[int] = Field(default=None, repr=True) + redirect_stderr_to_stdout: Optional[bool] = Field(default=None, repr=True) + enable_network: Optional[bool] = Field(default=None, repr=True) + number_of_runs: Optional[int] = Field(default=None, repr=True) + callback_url: Optional[str] = Field(default=None, repr=True) # Post-execution submission attributes. - stdout: Optional[str] = None - stderr: Optional[str] = None - compile_output: Optional[str] = None - message: Optional[str] = None - exit_code: Optional[int] = None - exit_signal: Optional[int] = None - status: Optional[Status] = None - created_at: Optional[datetime] = None - finished_at: Optional[datetime] = None - token: str = "" - time: Optional[float] = None - wall_time: Optional[float] = None - memory: Optional[float] = None - post_execution_filesystem: Optional[Filesystem] = None + stdout: Optional[str] = Field(default=None, repr=True) + stderr: Optional[str] = Field(default=None, repr=True) + compile_output: Optional[str] = Field(default=None, repr=True) + message: Optional[str] = Field(default=None, repr=True) + exit_code: Optional[int] = Field(default=None, repr=True) + exit_signal: Optional[int] = Field(default=None, repr=True) + status: Optional[Status] = Field(default=None, repr=True) + created_at: Optional[datetime] = Field(default=None, repr=True) + finished_at: Optional[datetime] = Field(default=None, repr=True) + token: Optional[UUID4] = Field(default=None, repr=True) + time: Optional[float] = Field(default=None, repr=True) + wall_time: Optional[float] = Field(default=None, repr=True) + memory: Optional[float] = Field(default=None, repr=True) + post_execution_filesystem: Optional[Filesystem] = Field(default=None, repr=True) + + model_config = ConfigDict(extra="ignore") + + @field_validator(*ENCODED_FIELDS, mode="before") + @classmethod + def process_encoded_fields(cls, value: str) -> Optional[str]: + """Validate all encoded attributes.""" + if value is None: + return None + else: + try: + return decode(value) + except Exception: + return value + + @field_validator("post_execution_filesystem", mode="before") + @classmethod + def process_post_execution_filesystem(cls, content: str) -> Filesystem: + """Validate post_execution_filesystem attribute.""" + return Filesystem(content=content) + + @field_validator("status", mode="before") + @classmethod + def process_status(cls, value: dict) -> Status: + """Validate status attribute.""" + return Status(value["id"]) + + @field_validator("language", mode="before") + @classmethod + def process_language( + cls, value: Union[LanguageAlias, dict] + ) -> Union[LanguageAlias, int]: + """Validate status attribute.""" + if isinstance(value, dict): + return value["id"] + else: + return value def set_attributes(self, attributes: dict[str, Any]) -> None: """Set Submissions attributes while taking into account different @@ -177,7 +221,7 @@ def set_attributes(self, attributes: dict[str, Any]) -> None: if attr in SKIP_FIELDS: continue - if attr in ENCODED_FIELDS and attr not in ("post_execution_filesystem",): + if attr in ENCODED_FIELDS: value = decode(value) if value else None elif attr == "status": value = Status(value["id"]) @@ -229,10 +273,6 @@ def pre_execution_copy(self) -> "Submission": setattr(new_submission, attr, copy.deepcopy(getattr(self, attr))) return new_submission - def __repr__(self) -> str: - arguments = ", ".join(f"{field}={getattr(self, field)!r}" for field in FIELDS) - return f"{self.__class__.__name__}({arguments})" - def __iter__(self): if self.post_execution_filesystem is None: return iter([]) diff --git a/tests/test_submission.py b/tests/test_submission.py index c204bcb..98903ed 100644 --- a/tests/test_submission.py +++ b/tests/test_submission.py @@ -1,6 +1,53 @@ from judge0 import Status, Submission, wait +def test_from_json(): + submission_dict = { + "source_code": "cHJpbnQoJ0hlbGxvLCBXb3JsZCEnKQ==", + "language_id": 100, + "stdin": None, + "expected_output": None, + "stdout": "SGVsbG8sIFdvcmxkIQo=", + "status_id": 3, + "created_at": "2024-12-09T17:22:55.662Z", + "finished_at": "2024-12-09T17:22:56.045Z", + "time": "0.152", + "memory": 13740, + "stderr": None, + "token": "5513d8ca-975b-4499-b54b-342f1952d00e", + "number_of_runs": 1, + "cpu_time_limit": "5.0", + "cpu_extra_time": "1.0", + "wall_time_limit": "10.0", + "memory_limit": 128000, + "stack_limit": 64000, + "max_processes_and_or_threads": 60, + "enable_per_process_and_thread_time_limit": False, + "enable_per_process_and_thread_memory_limit": False, + "max_file_size": 1024, + "compile_output": None, + "exit_code": 0, + "exit_signal": None, + "message": None, + "wall_time": "0.17", + "compiler_options": None, + "command_line_arguments": None, + "redirect_stderr_to_stdout": False, + "callback_url": None, + "additional_files": None, + "enable_network": False, + "post_execution_filesystem": "UEsDBBQACAAIANyKiVkAAAAAAAAAABYAAAAJABwAc" + "2NyaXB0LnB5VVQJAANvJ1dncCdXZ3V4CwABBOgDAAAE6AMAACsoyswr0VD3SM3JyddRCM8v" + "yklRVNcEAFBLBwgynNLKGAAAABYAAABQSwECHgMUAAgACADciolZMpzSyhgAAAAWAAAACQA" + "YAAAAAAABAAAApIEAAAAAc2NyaXB0LnB5VVQFAANvJ1dndXgLAAEE6AMAAAToAwAAUEsFBg" + "AAAAABAAEATwAAAGsAAAAAAA==", + "status": {"id": 3, "description": "Accepted"}, + "language": {"id": 100, "name": "Python (3.12.5)"}, + } + + _ = Submission(**submission_dict) + + def test_status_before_and_after_submission(request): client = request.getfixturevalue("judge0_ce_client") submission = Submission(source_code='print("Hello World!")') From e5124cda243ca6aab04ebdc05328af183127a2db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Thu, 12 Dec 2024 19:18:47 +0100 Subject: [PATCH 16/52] Add additional attributes to Language class. Add minimal docstring to client classes. Redefine output types for get_languages, get_language, and get_config_info methods. --- src/judge0/base_types.py | 4 ++++ src/judge0/clients.py | 44 ++++++++++++++++++++++++---------------- 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/src/judge0/base_types.py b/src/judge0/base_types.py index 0c7f450..48480e8 100644 --- a/src/judge0/base_types.py +++ b/src/judge0/base_types.py @@ -45,6 +45,10 @@ def encode(self) -> bytes: class Language(BaseModel): id: int name: str + is_archived: Optional[bool] = None + source_file: Optional[str] = None + compile_cmd: Optional[str] = None + run_cmd: Optional[str] = None class LanguageAlias(IntEnum): diff --git a/src/judge0/clients.py b/src/judge0/clients.py index 4f63850..29b1ce7 100644 --- a/src/judge0/clients.py +++ b/src/judge0/clients.py @@ -26,8 +26,8 @@ def __init__( # TODO: Should be handled differently. try: - self.languages = tuple(Language(**lang) for lang in self.get_languages()) - self.config = Config(**self.get_config_info()) + self.languages = self.get_languages() + self.config = self.get_config_info() except Exception as e: raise RuntimeError( f"Authentication failed. Visit {self.HOME_URL} to get or " @@ -47,27 +47,27 @@ def get_about(self) -> dict: return response.json() @handle_too_many_requests_error_for_preview_client - def get_config_info(self) -> dict: + def get_config_info(self) -> Config: response = self.session.get( f"{self.endpoint}/config_info", headers=self.auth_headers, ) response.raise_for_status() - return response.json() + return Config(**response.json()) @handle_too_many_requests_error_for_preview_client - def get_language(self, language_id) -> dict: + def get_language(self, language_id: int) -> Language: request_url = f"{self.endpoint}/languages/{language_id}" response = self.session.get(request_url, headers=self.auth_headers) response.raise_for_status() - return response.json() + return Language(**response.json()) @handle_too_many_requests_error_for_preview_client - def get_languages(self) -> list[dict]: + def get_languages(self) -> list[Language]: request_url = f"{self.endpoint}/languages" response = self.session.get(request_url, headers=self.auth_headers) response.raise_for_status() - return response.json() + return [Language(**lang_dict) for lang_dict in response.json()] @handle_too_many_requests_error_for_preview_client def get_statuses(self) -> list[dict]: @@ -249,8 +249,6 @@ def get_submissions( Submissions A sequence of submissions with updated attributes. """ - # TODO: Maybe raise an exception if the number of submissions is bigger - # than the batch size a client supports? params = { "base64_encoded": "true", } @@ -263,7 +261,7 @@ def get_submissions( else: params["fields"] = "*" - tokens = ",".join(submission.token for submission in submissions) + tokens = ",".join([submission.token for submission in submissions]) params["tokens"] = tokens response = self.session.get( @@ -300,6 +298,8 @@ def _update_endpoint_header(self, header_value): class ATDJudge0CE(ATD): + """AllThingsDev client for CE flavor.""" + DEFAULT_ENDPOINT: str = "https://judge0-ce.proxy-production.allthingsdev.co" DEFAULT_HOST: str = "Judge0-CE.allthingsdev.co" HOME_URL: str = ( @@ -328,15 +328,15 @@ def get_about(self) -> dict: self._update_endpoint_header(self.DEFAULT_ABOUT_ENDPOINT) return super().get_about() - def get_config_info(self) -> dict: + def get_config_info(self) -> Config: self._update_endpoint_header(self.DEFAULT_CONFIG_INFO_ENDPOINT) return super().get_config_info() - def get_language(self, language_id) -> dict: + def get_language(self, language_id) -> Language: self._update_endpoint_header(self.DEFAULT_LANGUAGE_ENDPOINT) return super().get_language(language_id) - def get_languages(self) -> list[dict]: + def get_languages(self) -> list[Language]: self._update_endpoint_header(self.DEFAULT_LANGUAGES_ENDPOINT) return super().get_languages() @@ -372,6 +372,8 @@ def get_submissions( class ATDJudge0ExtraCE(ATD): + """AllThingsDev client for Extra CE flavor.""" + DEFAULT_ENDPOINT: str = "https://judge0-extra-ce.proxy-production.allthingsdev.co" DEFAULT_HOST: str = "Judge0-Extra-CE.allthingsdev.co" HOME_URL: str = ( @@ -401,15 +403,15 @@ def get_about(self) -> dict: self._update_endpoint_header(self.DEFAULT_ABOUT_ENDPOINT) return super().get_about() - def get_config_info(self) -> dict: + def get_config_info(self) -> Config: self._update_endpoint_header(self.DEFAULT_CONFIG_INFO_ENDPOINT) return super().get_config_info() - def get_language(self, language_id) -> dict: + def get_language(self, language_id) -> Language: self._update_endpoint_header(self.DEFAULT_LANGUAGE_ENDPOINT) return super().get_language(language_id) - def get_languages(self) -> list[dict]: + def get_languages(self) -> list[Language]: self._update_endpoint_header(self.DEFAULT_LANGUAGES_ENDPOINT) return super().get_languages() @@ -462,6 +464,8 @@ def __init__(self, endpoint, host_header_value, api_key, **kwargs): class RapidJudge0CE(Rapid): + """RapidAPI client for CE flavor.""" + DEFAULT_ENDPOINT: str = "https://judge0-ce.p.rapidapi.com" DEFAULT_HOST: str = "judge0-ce.p.rapidapi.com" HOME_URL: str = "https://rapidapi.com/judge0-official/api/judge0-ce" @@ -476,6 +480,8 @@ def __init__(self, api_key, **kwargs): class RapidJudge0ExtraCE(Rapid): + """RapidAPI client for Extra CE flavor.""" + DEFAULT_ENDPOINT: str = "https://judge0-extra-ce.p.rapidapi.com" DEFAULT_HOST: str = "judge0-extra-ce.p.rapidapi.com" HOME_URL: str = "https://rapidapi.com/judge0-official/api/judge0-extra-ce" @@ -504,6 +510,8 @@ def __init__(self, endpoint, api_key=None, **kwargs): class SuluJudge0CE(Sulu): + """Sulu client for CE flavor.""" + DEFAULT_ENDPOINT: str = "https://judge0-ce.p.sulu.sh" HOME_URL: str = "https://sparkhub.sulu.sh/apis/judge0/judge0-ce/readme" @@ -516,6 +524,8 @@ def __init__(self, api_key=None, **kwargs): class SuluJudge0ExtraCE(Sulu): + """Sulu client for Extra CE flavor.""" + DEFAULT_ENDPOINT: str = "https://judge0-extra-ce.p.sulu.sh" HOME_URL: str = "https://sparkhub.sulu.sh/apis/judge0/judge0-extra-ce/readme" From 16e1dd0bcafa1ec598564f01be5dcf8579e17388 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Thu, 12 Dec 2024 20:03:07 +0100 Subject: [PATCH 17/52] Initial documentation setup. --- docs/Makefile | 20 ++++++++++++++++++++ docs/make.bat | 35 +++++++++++++++++++++++++++++++++++ docs/source/conf.py | 27 +++++++++++++++++++++++++++ docs/source/index.rst | 17 +++++++++++++++++ pyproject.toml | 1 + 5 files changed, 100 insertions(+) create mode 100644 docs/Makefile create mode 100644 docs/make.bat create mode 100644 docs/source/conf.py create mode 100644 docs/source/index.rst diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..d0c3cbf --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..747ffb7 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000..7f7854a --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,27 @@ +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +project = "Judge0 Python SDK" +copyright = "2024, Judge0" +author = "Judge0" +release = "0.1.0" + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions = [] + +templates_path = ["_templates"] +exclude_patterns = [] + + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_theme = "alabaster" +html_static_path = ["_static"] diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 0000000..7b56c95 --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,17 @@ +.. Judge0 Python SDK documentation master file, created by + sphinx-quickstart on Thu Dec 12 19:59:23 2024. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Judge0 Python SDK documentation +=============================== + +Add your content using ``reStructuredText`` syntax. See the +`reStructuredText `_ +documentation for details. + + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + diff --git a/pyproject.toml b/pyproject.toml index 01fc8e2..d2d002d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,6 +45,7 @@ test = [ "pytest-cov==6.0.0", "flake8-docstrings==1.7.0", ] +docs = ["sphinx==7.4.7"] [tool.flake8] docstring-convention = "numpy" From f6295ce4206349583e53d4ee7146705b4341aca3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Thu, 12 Dec 2024 20:16:13 +0100 Subject: [PATCH 18/52] Add workflow file for docs. --- .github/workflows/docs.yml | 28 ++++++++++++++++++++++++++++ docs/requirements.txt | 1 + 2 files changed, 29 insertions(+) create mode 100644 .github/workflows/docs.yml create mode 100644 docs/requirements.txt diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..6564b7c --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,28 @@ +name: "Sphinx: Render docs" + +on: + push: + branches: ["master"] + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Build HTML + uses: ammaraskar/sphinx-action@master + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: html-docs + path: docs/build/html/ + - name: Deploy + uses: peaceiris/actions-gh-pages@v3 + if: github.ref == 'refs/heads/main' + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: docs/build/html \ No newline at end of file diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..3246491 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1 @@ +furo==2021.11.16 \ No newline at end of file From 6676d45531bb64f874130982aa8d1ee5a01a5138 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Thu, 12 Dec 2024 20:24:45 +0100 Subject: [PATCH 19/52] Add workflow dispatch trigger for docs workflow. --- .github/workflows/docs.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 6564b7c..90ce995 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,9 +1,11 @@ name: "Sphinx: Render docs" on: + workflow_dispatch: push: branches: ["master"] + jobs: build: runs-on: ubuntu-latest From c64cfa1a5e46a864b0bac8bf317031addfbe279e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Thu, 12 Dec 2024 20:29:55 +0100 Subject: [PATCH 20/52] Update branch in docs workflow. --- .github/workflows/docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 90ce995..6ba0536 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -24,7 +24,7 @@ jobs: path: docs/build/html/ - name: Deploy uses: peaceiris/actions-gh-pages@v3 - if: github.ref == 'refs/heads/main' + if: github.ref == 'refs/heads/master' with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: docs/build/html \ No newline at end of file From 4d12e302a2e3a6592327e7ff2c85e44e38f7c41a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Thu, 12 Dec 2024 21:08:07 +0100 Subject: [PATCH 21/52] Add base url to sphinx conf. --- docs/source/conf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/conf.py b/docs/source/conf.py index 7f7854a..fea2c58 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -23,5 +23,6 @@ # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output +html_baseurl = "https://docs.judge0.com/python" html_theme = "alabaster" html_static_path = ["_static"] From f1762855d69552fb29932d4b0fff9068835dad6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Thu, 12 Dec 2024 21:12:38 +0100 Subject: [PATCH 22/52] Add extension for githubpages. --- docs/source/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index fea2c58..42c6e3a 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -14,7 +14,7 @@ # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration -extensions = [] +extensions = ["sphinx.ext.githubpages"] templates_path = ["_templates"] exclude_patterns = [] From 43408011b868a0723b01b7d2581916c24ba68f8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Thu, 12 Dec 2024 21:27:19 +0100 Subject: [PATCH 23/52] Update html_baseurl. --- docs/source/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 42c6e3a..7db48bb 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -23,6 +23,6 @@ # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output -html_baseurl = "https://docs.judge0.com/python" +html_baseurl = "https://docs.judge0.com/python/" html_theme = "alabaster" html_static_path = ["_static"] From 90e39c8de9da757384911ace5a7ec373aa715417 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Thu, 12 Dec 2024 21:34:44 +0100 Subject: [PATCH 24/52] Remove base url. Set release to 0.1. --- docs/source/conf.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 7db48bb..5eda353 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -9,12 +9,12 @@ project = "Judge0 Python SDK" copyright = "2024, Judge0" author = "Judge0" -release = "0.1.0" +release = "0.1" # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration -extensions = ["sphinx.ext.githubpages"] +extensions = [] templates_path = ["_templates"] exclude_patterns = [] @@ -23,6 +23,5 @@ # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output -html_baseurl = "https://docs.judge0.com/python/" html_theme = "alabaster" html_static_path = ["_static"] From cb37a811c078bf171e00945e073d3e5d07a6a027 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Herman=20Zvonimir=20Do=C5=A1ilovi=C4=87?= Date: Thu, 12 Dec 2024 21:43:41 +0100 Subject: [PATCH 25/52] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 24baa57..de2703e 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ # Judge0 Python SDK -The official Python library for Judge0. +The official Python SDK for Judge0. From dc1fd09d9030053ffe1fcf56d713ff08d66f8cb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Herman=20Zvonimir=20Do=C5=A1ilovi=C4=87?= Date: Thu, 12 Dec 2024 21:45:41 +0100 Subject: [PATCH 26/52] Create CODE_OF_CONDUCT.md --- CODE_OF_CONDUCT.md | 128 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 CODE_OF_CONDUCT.md diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..0a27e2e --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +contact@judge0.com. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. From a63ed35d2cda3982b5d364160784fa3ba61c190f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Herman=20Zvonimir=20Do=C5=A1ilovi=C4=87?= Date: Thu, 12 Dec 2024 21:46:03 +0100 Subject: [PATCH 27/52] Update pyproject.toml --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d2d002d..b53813e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ version = "0.0.2dev" description = "The official Python library for Judge0." readme = "README.md" requires-python = ">=3.9" -authors = [{ name = "Judge0", email = "support@judge0.com" }] +authors = [{ name = "Judge0", email = "contact@judge0.com" }] classifiers = [ "Intended Audience :: Developers", From 767cd326790da2ddd2b5ae84c293e60f804066b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Herman=20Zvonimir=20Do=C5=A1ilovi=C4=87?= Date: Thu, 12 Dec 2024 21:47:03 +0100 Subject: [PATCH 28/52] Update issue templates --- .github/ISSUE_TEMPLATE/bug_report.md | 38 +++++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 20 ++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..dd84ea7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..bbcbbe7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. From 0dc5e6f25cab055aa20b791140bbf000a32e779f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Herman=20Zvonimir=20Do=C5=A1ilovi=C4=87?= Date: Thu, 12 Dec 2024 21:53:47 +0100 Subject: [PATCH 29/52] Create RELEASE_NOTES_TEMPLATE.md --- RELEASE_NOTES_TEMPLATE.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 RELEASE_NOTES_TEMPLATE.md diff --git a/RELEASE_NOTES_TEMPLATE.md b/RELEASE_NOTES_TEMPLATE.md new file mode 100644 index 0000000..1a46e3d --- /dev/null +++ b/RELEASE_NOTES_TEMPLATE.md @@ -0,0 +1,15 @@ +# vX.Y.Z (YYYY-MM-DD) + +## API Changes + +## New Features + +## Improvements + +## Security Improvements + +## Bug Fixes + +## Security Fixes + +## Other Changes From a906318e7633b88ab9bfa19e410c6f55888f90d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Fri, 13 Dec 2024 16:07:47 +0100 Subject: [PATCH 30/52] Update test workflow to run only when code changes. --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 86a0ddd..38c14e8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,6 +3,7 @@ name: Test judge0-python on: push: branches: ["master"] + paths: ["src/**", "tests/**"] permissions: contents: read From 5b12ca5e215ddf4ca6f6bad151ee6f3bd449523c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Fri, 13 Dec 2024 16:17:10 +0100 Subject: [PATCH 31/52] Use RTD theme. --- docs/source/conf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 5eda353..fe56503 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -14,7 +14,7 @@ # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration -extensions = [] +extensions = ["sphinx_rtd_theme"] templates_path = ["_templates"] exclude_patterns = [] @@ -23,5 +23,5 @@ # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output -html_theme = "alabaster" +html_theme = "sphinx_rtd_theme" html_static_path = ["_static"] From fd9c33777fe1be580413c34475f0e3adce4b5d86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Fri, 13 Dec 2024 16:22:27 +0100 Subject: [PATCH 32/52] Update docs workflow with additional steps to install requirements for sphinx (rtd theme). --- .github/workflows/docs.yml | 15 ++++++++++++++- docs/requirements.txt | 4 +++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 6ba0536..f76b760 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -8,20 +8,33 @@ on: jobs: build: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 permissions: contents: write steps: - uses: actions/checkout@v4 with: persist-credentials: false + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.12' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r docs/requirements.txt + - name: Build HTML uses: ammaraskar/sphinx-action@master + - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: html-docs path: docs/build/html/ + - name: Deploy uses: peaceiris/actions-gh-pages@v3 if: github.ref == 'refs/heads/master' diff --git a/docs/requirements.txt b/docs/requirements.txt index 3246491..1e05642 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1 +1,3 @@ -furo==2021.11.16 \ No newline at end of file +furo==2021.11.16 +Sphinx==7.4.7 +sphinx-rtd-theme==3.0.2 \ No newline at end of file From 49f8520cd6f1b93c391b3b7a6cf7b0eb1fc520f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Fri, 13 Dec 2024 16:24:37 +0100 Subject: [PATCH 33/52] Remove furo from requirements. --- docs/requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 1e05642..97bea5f 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,3 +1,2 @@ -furo==2021.11.16 Sphinx==7.4.7 sphinx-rtd-theme==3.0.2 \ No newline at end of file From fb2ce64cd26ed695dff7873c519d3467e23d2ae6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Fri, 13 Dec 2024 16:30:19 +0100 Subject: [PATCH 34/52] Fix requirements for docs. --- docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 97bea5f..02188c1 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,2 +1,2 @@ -Sphinx==7.4.7 +sphinx==7.4.7 sphinx-rtd-theme==3.0.2 \ No newline at end of file From 5e3da7f2b7ee0da46f9d265cf239166650b4a1af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Fri, 13 Dec 2024 16:38:38 +0100 Subject: [PATCH 35/52] Update docs workflow and requirements. --- .github/workflows/docs.yml | 12 +----------- docs/requirements.txt | 1 - 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index f76b760..1d5909d 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -16,18 +16,8 @@ jobs: with: persist-credentials: false - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: '3.12' - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -r docs/requirements.txt - - name: Build HTML - uses: ammaraskar/sphinx-action@master + uses: ammaraskar/sphinx-action@7.0.0 - name: Upload artifacts uses: actions/upload-artifact@v4 diff --git a/docs/requirements.txt b/docs/requirements.txt index 02188c1..3ffe4e3 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,2 +1 @@ -sphinx==7.4.7 sphinx-rtd-theme==3.0.2 \ No newline at end of file From 4e43bfad5b96065554123356e1adde179bcf0b62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Fri, 13 Dec 2024 21:48:43 +0100 Subject: [PATCH 36/52] Initial commit for api and submissions module docs. --- docs/source/api_index.rst | 20 ++++++++++++++++++++ docs/source/conf.py | 25 ++++++++++++++++++++++++- docs/source/index.rst | 8 ++------ 3 files changed, 46 insertions(+), 7 deletions(-) create mode 100644 docs/source/api_index.rst diff --git a/docs/source/api_index.rst b/docs/source/api_index.rst new file mode 100644 index 0000000..7c3641d --- /dev/null +++ b/docs/source/api_index.rst @@ -0,0 +1,20 @@ +API +=== + +.. toctree:: + :maxdepth: 2 + :caption: API + +API +--- + +.. automodule:: judge0.api + :members: + :undoc-members: + +Submission +---------- + +.. automodule:: judge0.submission + :members: + :undoc-members: \ No newline at end of file diff --git a/docs/source/conf.py b/docs/source/conf.py index fe56503..817dc32 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -6,6 +6,10 @@ # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +import os +import sys + project = "Judge0 Python SDK" copyright = "2024, Judge0" author = "Judge0" @@ -14,7 +18,7 @@ # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration -extensions = ["sphinx_rtd_theme"] +extensions = ["sphinx.ext.autodoc"] templates_path = ["_templates"] exclude_patterns = [] @@ -25,3 +29,22 @@ html_theme = "sphinx_rtd_theme" html_static_path = ["_static"] + +sys.path.insert(0, os.path.abspath("../src/judge0")) # Adjust as needed + +html_theme_options = { + # Toc options + "collapse_navigation": True, + "sticky_navigation": True, + "navigation_depth": 4, + "includehidden": True, + "titles_only": False, +} + +autodoc_default_options = { + "members": True, + "undoc-members": True, + "private-members": False, + "special-members": False, + "inherited-members": False, +} diff --git a/docs/source/index.rst b/docs/source/index.rst index 7b56c95..b61592e 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -6,12 +6,8 @@ Judge0 Python SDK documentation =============================== -Add your content using ``reStructuredText`` syntax. See the -`reStructuredText `_ -documentation for details. - - .. toctree:: :maxdepth: 2 - :caption: Contents: + :caption: Contents + api_index \ No newline at end of file From 86fe15c07df9dee85fd5d676be644307a8b93eb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Fri, 13 Dec 2024 21:54:55 +0100 Subject: [PATCH 37/52] Update lib path in conf for docs. --- docs/source/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 817dc32..b7503af 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -30,7 +30,7 @@ html_theme = "sphinx_rtd_theme" html_static_path = ["_static"] -sys.path.insert(0, os.path.abspath("../src/judge0")) # Adjust as needed +sys.path.insert(0, os.path.abspath("../src/")) # Adjust as needed html_theme_options = { # Toc options From 353916830b5f831185d223c52effa1c5823230bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Fri, 13 Dec 2024 21:57:57 +0100 Subject: [PATCH 38/52] Update path to judge0 lib in conf for docs. --- docs/source/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index b7503af..f667829 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -30,7 +30,7 @@ html_theme = "sphinx_rtd_theme" html_static_path = ["_static"] -sys.path.insert(0, os.path.abspath("../src/")) # Adjust as needed +sys.path.insert(0, os.path.abspath("../../src/")) # Adjust as needed html_theme_options = { # Toc options From 58a2be793e589aed0836032ab0e75436e3455df6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Sat, 14 Dec 2024 00:36:06 +0100 Subject: [PATCH 39/52] Move contributing docs to sphinx. --- CONTRIBUTING.md | 25 +------------------------ docs/source/contributing.rst | 28 ++++++++++++++++++++++++++++ docs/source/index.rst | 8 ++------ 3 files changed, 31 insertions(+), 30 deletions(-) create mode 100644 docs/source/contributing.rst diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e85b338..346d24d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,26 +1,3 @@ # How to contribute -## Preparing the development setup - -1. Install Python 3.9 - -```bash -sudo add-apt-repository ppa:deadsnakes/ppa -sudo apt update -sudo apt install python3.9 python3.9-venv -``` - -2. Clone the repo, create and activate a new virtual environment - -```bash -cd judge0-python -python3.9 -m venv venv -source venv/bin/activate -``` - -3. Install the library and development dependencies - -```bash -pip install -e .[test] -pre-commit install -``` +See [docs](https://judge0.github.io/judge0-python/contributing.html). diff --git a/docs/source/contributing.rst b/docs/source/contributing.rst new file mode 100644 index 0000000..2a19fb5 --- /dev/null +++ b/docs/source/contributing.rst @@ -0,0 +1,28 @@ +Contributing +============ + +Preparing the development setup +------------------------------- + +1. Install Python 3.9 + +.. code-block:: console + + $ sudo add-apt-repository ppa:deadsnakes/ppa + $ sudo apt update + $ sudo apt install python3.9 python3.9-venv + +2. Clone the repo, create and activate a new virtual environment + +.. code-block:: console + + $ cd judge0-python + $ python3.9 -m venv venv + $ . venv/bin/activate + +3. Install the library and development dependencies + +.. code-block:: console + + $ pip install -e .[test] + $ pre-commit install diff --git a/docs/source/index.rst b/docs/source/index.rst index b61592e..3ae70c5 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,8 +1,3 @@ -.. Judge0 Python SDK documentation master file, created by - sphinx-quickstart on Thu Dec 12 19:59:23 2024. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - Judge0 Python SDK documentation =============================== @@ -10,4 +5,5 @@ Judge0 Python SDK documentation :maxdepth: 2 :caption: Contents - api_index \ No newline at end of file + api_index + contributing \ No newline at end of file From 026b9b177dc12d6d0455e14586946928d0126519 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Sat, 14 Dec 2024 01:02:54 +0100 Subject: [PATCH 40/52] Remove tables from docstrings. Render as numpy-based docs using napoleon extension. --- docs/source/api_index.rst | 13 ++++++------- docs/source/conf.py | 8 +++++++- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/docs/source/api_index.rst b/docs/source/api_index.rst index 7c3641d..b846e92 100644 --- a/docs/source/api_index.rst +++ b/docs/source/api_index.rst @@ -1,19 +1,18 @@ API === -.. toctree:: - :maxdepth: 2 - :caption: API +.. autosummary:: + :toctree: generated -API ---- +API Module +---------- .. automodule:: judge0.api :members: :undoc-members: -Submission ----------- +Submission Module +----------------- .. automodule:: judge0.submission :members: diff --git a/docs/source/conf.py b/docs/source/conf.py index f667829..28ddaaf 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -18,7 +18,11 @@ # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration -extensions = ["sphinx.ext.autodoc"] +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.napoleon", + "sphinx.ext.autosummary", +] templates_path = ["_templates"] exclude_patterns = [] @@ -48,3 +52,5 @@ "special-members": False, "inherited-members": False, } + +napoleon_google_docstring = False From dbfd71605a3e5de0dc2c1b3bacaa15ad7873c2cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Sat, 14 Dec 2024 18:14:57 +0100 Subject: [PATCH 41/52] Add mocked imports for requests and pydantic to avoid installation in docs env. Remove tables from docstrings. --- docs/requirements.txt | 3 ++- docs/source/api_index.rst | 9 ++++++- docs/source/conf.py | 2 ++ docs/source/index.rst | 4 +++ src/judge0/api.py | 52 ++++++++++++++++----------------------- 5 files changed, 37 insertions(+), 33 deletions(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 3ffe4e3..db23d3d 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1 +1,2 @@ -sphinx-rtd-theme==3.0.2 \ No newline at end of file +sphinx-rtd-theme==3.0.2 +sphinx-autodoc-typehints==2.3.0 \ No newline at end of file diff --git a/docs/source/api_index.rst b/docs/source/api_index.rst index b846e92..5495d02 100644 --- a/docs/source/api_index.rst +++ b/docs/source/api_index.rst @@ -16,4 +16,11 @@ Submission Module .. automodule:: judge0.submission :members: - :undoc-members: \ No newline at end of file + :member-order: groupwise + +Clients Module +----------------- + +.. automodule:: judge0.clients + :members: + :member-order: groupwise diff --git a/docs/source/conf.py b/docs/source/conf.py index 28ddaaf..e8ac76a 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -22,6 +22,7 @@ "sphinx.ext.autodoc", "sphinx.ext.napoleon", "sphinx.ext.autosummary", + "sphinx_autodoc_typehints", ] templates_path = ["_templates"] @@ -52,5 +53,6 @@ "special-members": False, "inherited-members": False, } +autodoc_mock_imports = ["requests", "pydantic"] napoleon_google_docstring = False diff --git a/docs/source/index.rst b/docs/source/index.rst index 3ae70c5..087fd91 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,6 +1,10 @@ Judge0 Python SDK documentation =============================== +.. note:: + + This project is under active development. + .. toctree:: :maxdepth: 2 :caption: Contents diff --git a/src/judge0/api.py b/src/judge0/api.py index fabcd4f..7b2fb22 100644 --- a/src/judge0/api.py +++ b/src/judge0/api.py @@ -188,19 +188,25 @@ def wait( def create_submissions_from_test_cases( submissions: Union[Submission, Submissions], test_cases: Optional[Union[TestCaseType, TestCases]] = None, -): +) -> Union[Submission, list[Submission]]: """Create submissions from the (submission, test_case) pairs. - The following table contains the return type based on the types of - `submissions` and `test_cases` arguments: + This function always returns a deep copy so make sure you are using the + returned submission(s). - | submissions | test_cases | returns | - |:------------|:-----------|:------------| - | Submission | TestCase | Submission | - | Submission | TestCases | Submissions | - | Submissions | TestCase | Submissions | - | Submissions | TestCases | Submissions | + Parameters + ---------- + submissions : Submission or Submissions + Base submission(s) that need to be expanded with test cases. + test_cases: TestCaseType or TestCases + Test cases. + Returns + ------- + Submissions or Submissions + A single submission if submissions arguments is of type Submission or + source_code argument is provided, and test_cases argument is of type + TestCase. Otherwise returns a list of submissions. """ if isinstance(submissions, Submission): submissions_list = [submissions] @@ -275,16 +281,6 @@ def async_execute( Aliases: `async_run`. - The following table contains the return type based on the types of - `submissions` (or `source_code`) and `test_cases` arguments: - - | submissions | test_cases | returns | - |:------------|:-----------|:------------| - | Submission | TestCase | Submission | - | Submission | TestCases | Submissions | - | Submissions | TestCase | Submissions | - | Submissions | TestCases | Submissions | - Parameters ---------- client : Client or Flavor, optional @@ -300,7 +296,9 @@ def async_execute( Returns ------- Submission or Submissions - A single submission or a list of submissions. + A single submission if submissions arguments is of type Submission or + source_code argument is provided, and test_cases argument is of type + TestCase. Otherwise returns a list of submissions. Raises ------ @@ -331,16 +329,6 @@ def sync_execute( Aliases: `execute`, `run`, `sync_run`. - The following table contains the return type based on the types of - `submissions` (or `source_code`) and `test_cases` arguments: - - | submissions | test_cases | returns | - |:------------|:-----------|:------------| - | Submission | TestCase | Submission | - | Submission | TestCases | Submissions | - | Submissions | TestCase | Submissions | - | Submissions | TestCases | Submissions | - Parameters ---------- client : Client or Flavor, optional @@ -356,7 +344,9 @@ def sync_execute( Returns ------- Submission or Submissions - A single submission or a list of submissions. + A single submission if submissions arguments is of type Submission or + source_code argument is provided, and test_cases argument is of type + TestCase. Otherwise returns a list of submissions. Raises ------ From 0842184f377c54f31cd1e4bcf4ccd75214a46f04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Herman=20Zvonimir=20Do=C5=A1ilovi=C4=87?= Date: Sat, 14 Dec 2024 20:48:05 +0100 Subject: [PATCH 42/52] Add html_show_sphinx --- docs/requirements.txt | 2 +- docs/source/conf.py | 1 + docs/source/index.rst | 6 +----- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index db23d3d..cf4f60f 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,2 +1,2 @@ sphinx-rtd-theme==3.0.2 -sphinx-autodoc-typehints==2.3.0 \ No newline at end of file +sphinx-autodoc-typehints==2.3.0 diff --git a/docs/source/conf.py b/docs/source/conf.py index e8ac76a..97bcdf6 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -34,6 +34,7 @@ html_theme = "sphinx_rtd_theme" html_static_path = ["_static"] +html_show_sphinx = False sys.path.insert(0, os.path.abspath("../../src/")) # Adjust as needed diff --git a/docs/source/index.rst b/docs/source/index.rst index 087fd91..1f189fd 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,13 +1,9 @@ Judge0 Python SDK documentation =============================== -.. note:: - - This project is under active development. - .. toctree:: :maxdepth: 2 :caption: Contents api_index - contributing \ No newline at end of file + contributing From 22d4ae6aaa4022ec722549dda8732fadc2f892c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Sat, 14 Dec 2024 21:16:45 +0100 Subject: [PATCH 43/52] Switch to sphinxawesome-theme. --- docs/requirements.txt | 2 +- docs/source/conf.py | 11 +---------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index cf4f60f..8e76ca0 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,2 +1,2 @@ -sphinx-rtd-theme==3.0.2 +sphinxawesome-theme==5.3.2 sphinx-autodoc-typehints==2.3.0 diff --git a/docs/source/conf.py b/docs/source/conf.py index 97bcdf6..ff7cb8d 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -32,20 +32,11 @@ # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output -html_theme = "sphinx_rtd_theme" -html_static_path = ["_static"] +html_theme = "sphinxawesome_theme" html_show_sphinx = False sys.path.insert(0, os.path.abspath("../../src/")) # Adjust as needed -html_theme_options = { - # Toc options - "collapse_navigation": True, - "sticky_navigation": True, - "navigation_depth": 4, - "includehidden": True, - "titles_only": False, -} autodoc_default_options = { "members": True, From 1b5898f41d57a9884e7feaa3207d0a9bd2b67856 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Sat, 14 Dec 2024 22:38:56 +0100 Subject: [PATCH 44/52] Initial setup of doc structure. --- docs/source/api/api.rst | 6 ++ docs/source/api/clients.rst | 6 ++ docs/source/api/index.rst | 6 ++ docs/source/api/submission.rst | 6 ++ docs/source/api_index.rst | 26 ----- docs/source/conf.py | 2 + .../{ => contributors_guide}/contributing.rst | 0 docs/source/contributors_guide/index.rst | 5 + .../contributors_guide/release_notes.rst | 4 + docs/source/index.rst | 53 +++++++++- src/judge0/clients.py | 100 +++++++++++------- 11 files changed, 145 insertions(+), 69 deletions(-) create mode 100644 docs/source/api/api.rst create mode 100644 docs/source/api/clients.rst create mode 100644 docs/source/api/index.rst create mode 100644 docs/source/api/submission.rst delete mode 100644 docs/source/api_index.rst rename docs/source/{ => contributors_guide}/contributing.rst (100%) create mode 100644 docs/source/contributors_guide/index.rst create mode 100644 docs/source/contributors_guide/release_notes.rst diff --git a/docs/source/api/api.rst b/docs/source/api/api.rst new file mode 100644 index 0000000..08b5d0e --- /dev/null +++ b/docs/source/api/api.rst @@ -0,0 +1,6 @@ +API Module +========== + +.. automodule:: judge0.api + :members: + :undoc-members: diff --git a/docs/source/api/clients.rst b/docs/source/api/clients.rst new file mode 100644 index 0000000..52e7e4e --- /dev/null +++ b/docs/source/api/clients.rst @@ -0,0 +1,6 @@ +Clients Module +============== + +.. automodule:: judge0.clients + :members: + :member-order: groupwise diff --git a/docs/source/api/index.rst b/docs/source/api/index.rst new file mode 100644 index 0000000..ae975c4 --- /dev/null +++ b/docs/source/api/index.rst @@ -0,0 +1,6 @@ +.. toctree:: + :maxdepth: 2 + + api + submission + clients \ No newline at end of file diff --git a/docs/source/api/submission.rst b/docs/source/api/submission.rst new file mode 100644 index 0000000..e42a6aa --- /dev/null +++ b/docs/source/api/submission.rst @@ -0,0 +1,6 @@ +Submission Module +================= + +.. automodule:: judge0.submission + :members: + :member-order: groupwise diff --git a/docs/source/api_index.rst b/docs/source/api_index.rst deleted file mode 100644 index 5495d02..0000000 --- a/docs/source/api_index.rst +++ /dev/null @@ -1,26 +0,0 @@ -API -=== - -.. autosummary:: - :toctree: generated - -API Module ----------- - -.. automodule:: judge0.api - :members: - :undoc-members: - -Submission Module ------------------ - -.. automodule:: judge0.submission - :members: - :member-order: groupwise - -Clients Module ------------------ - -.. automodule:: judge0.clients - :members: - :member-order: groupwise diff --git a/docs/source/conf.py b/docs/source/conf.py index ff7cb8d..77420a5 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -29,6 +29,8 @@ exclude_patterns = [] +# add_module_names = False + # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output diff --git a/docs/source/contributing.rst b/docs/source/contributors_guide/contributing.rst similarity index 100% rename from docs/source/contributing.rst rename to docs/source/contributors_guide/contributing.rst diff --git a/docs/source/contributors_guide/index.rst b/docs/source/contributors_guide/index.rst new file mode 100644 index 0000000..312258b --- /dev/null +++ b/docs/source/contributors_guide/index.rst @@ -0,0 +1,5 @@ +.. toctree:: + :maxdepth: 2 + + contributing + release_notes diff --git a/docs/source/contributors_guide/release_notes.rst b/docs/source/contributors_guide/release_notes.rst new file mode 100644 index 0000000..0b6251f --- /dev/null +++ b/docs/source/contributors_guide/release_notes.rst @@ -0,0 +1,4 @@ +How to create a release candidate +================================= + +TODO \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst index 1f189fd..5df6c21 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,9 +1,54 @@ +=============================== Judge0 Python SDK documentation =============================== +Getting Started +=============== + +You can run minimal Hello World example in three easy steps: + +1. Install Judge0 Python SDK: + +.. code-block:: bash + + pip install judge0 + +2. Create a minimal script: + +.. code-block:: Python + + import judge0 + + submission = judge.run(source_code="print('Hello Judge0!')") + print(submission.stdout) + +3. Run the script. + +Want to learn more +------------------ + + +To learn what is happening behind the scenes and how to best use Judge0 Python +SDK to facilitate the development of your own product see In Depth guide and +Examples. + +Getting Involved +---------------- + +TODO + +.. toctree:: + :caption: Getting Involved + :glob: + :titlesonly: + :hidden: + + contributors_guide/index + .. toctree:: - :maxdepth: 2 - :caption: Contents + :caption: API + :glob: + :titlesonly: + :hidden: - api_index - contributing + api/index \ No newline at end of file diff --git a/src/judge0/clients.py b/src/judge0/clients.py index 29b1ce7..ff8e989 100644 --- a/src/judge0/clients.py +++ b/src/judge0/clients.py @@ -1,4 +1,4 @@ -from typing import Optional, Union +from typing import ClassVar, Optional, Union import requests @@ -10,7 +10,7 @@ class Client: - API_KEY_ENV = None + API_KEY_ENV: ClassVar[str] = None def __init__( self, @@ -280,7 +280,7 @@ def get_submissions( class ATD(Client): """Base class for all AllThingsDev clients.""" - API_KEY_ENV = "JUDGE0_ATD_API_KEY" + API_KEY_ENV: ClassVar[str] = "JUDGE0_ATD_API_KEY" def __init__(self, endpoint, host_header_value, api_key, **kwargs): self.api_key = api_key @@ -300,21 +300,31 @@ def _update_endpoint_header(self, header_value): class ATDJudge0CE(ATD): """AllThingsDev client for CE flavor.""" - DEFAULT_ENDPOINT: str = "https://judge0-ce.proxy-production.allthingsdev.co" - DEFAULT_HOST: str = "Judge0-CE.allthingsdev.co" - HOME_URL: str = ( + DEFAULT_ENDPOINT: ClassVar[str] = ( + "https://judge0-ce.proxy-production.allthingsdev.co" + ) + DEFAULT_HOST: ClassVar[str] = "Judge0-CE.allthingsdev.co" + HOME_URL: ClassVar[str] = ( "https://www.allthingsdev.co/apimarketplace/judge0-ce/66b683c8b7b7ad054eb6ff8f" ) - DEFAULT_ABOUT_ENDPOINT: str = "01fc1c98-ceee-4f49-8614-f2214703e25f" - DEFAULT_CONFIG_INFO_ENDPOINT: str = "b7aab45d-5eb0-4519-b092-89e5af4fc4f3" - DEFAULT_LANGUAGE_ENDPOINT: str = "a50ae6b1-23c1-40eb-b34c-88bc8cf2c764" - DEFAULT_LANGUAGES_ENDPOINT: str = "03824deb-bd18-4456-8849-69d78e1383cc" - DEFAULT_STATUSES_ENDPOINT: str = "c37b603f-6f99-4e31-a361-7154c734f19b" - DEFAULT_CREATE_SUBMISSION_ENDPOINT: str = "6e65686d-40b0-4bf7-a12f-1f6d033c4473" - DEFAULT_GET_SUBMISSION_ENDPOINT: str = "b7032b8b-86da-40b4-b9d3-b1f5e2b4ee1e" - DEFAULT_CREATE_SUBMISSIONS_ENDPOINT: str = "402b857c-1126-4450-bfd8-22e1f2cbff2f" - DEFAULT_GET_SUBMISSIONS_ENDPOINT: str = "e42f2a26-5b02-472a-80c9-61c4bdae32ec" + DEFAULT_ABOUT_ENDPOINT: ClassVar[str] = "01fc1c98-ceee-4f49-8614-f2214703e25f" + DEFAULT_CONFIG_INFO_ENDPOINT: ClassVar[str] = "b7aab45d-5eb0-4519-b092-89e5af4fc4f3" + DEFAULT_LANGUAGE_ENDPOINT: ClassVar[str] = "a50ae6b1-23c1-40eb-b34c-88bc8cf2c764" + DEFAULT_LANGUAGES_ENDPOINT: ClassVar[str] = "03824deb-bd18-4456-8849-69d78e1383cc" + DEFAULT_STATUSES_ENDPOINT: ClassVar[str] = "c37b603f-6f99-4e31-a361-7154c734f19b" + DEFAULT_CREATE_SUBMISSION_ENDPOINT: ClassVar[str] = ( + "6e65686d-40b0-4bf7-a12f-1f6d033c4473" + ) + DEFAULT_GET_SUBMISSION_ENDPOINT: ClassVar[str] = ( + "b7032b8b-86da-40b4-b9d3-b1f5e2b4ee1e" + ) + DEFAULT_CREATE_SUBMISSIONS_ENDPOINT: ClassVar[str] = ( + "402b857c-1126-4450-bfd8-22e1f2cbff2f" + ) + DEFAULT_GET_SUBMISSIONS_ENDPOINT: ClassVar[str] = ( + "e42f2a26-5b02-472a-80c9-61c4bdae32ec" + ) def __init__(self, api_key, **kwargs): super().__init__( @@ -374,22 +384,32 @@ def get_submissions( class ATDJudge0ExtraCE(ATD): """AllThingsDev client for Extra CE flavor.""" - DEFAULT_ENDPOINT: str = "https://judge0-extra-ce.proxy-production.allthingsdev.co" - DEFAULT_HOST: str = "Judge0-Extra-CE.allthingsdev.co" - HOME_URL: str = ( + DEFAULT_ENDPOINT: ClassVar[str] = ( + "https://judge0-extra-ce.proxy-production.allthingsdev.co" + ) + DEFAULT_HOST: ClassVar[str] = "Judge0-Extra-CE.allthingsdev.co" + HOME_URL: ClassVar[str] = ( "https://www.allthingsdev.co/apimarketplace/judge0-extra-ce/" "66b68838b7b7ad054eb70690" ) - DEFAULT_ABOUT_ENDPOINT: str = "1fd631a1-be6a-47d6-bf4c-987e357e3096" - DEFAULT_CONFIG_INFO_ENDPOINT: str = "46e05354-2a43-436a-9458-5d111456f0ff" - DEFAULT_LANGUAGE_ENDPOINT: str = "10465a84-2a2c-4213-845f-45e3c04a5867" - DEFAULT_LANGUAGES_ENDPOINT: str = "774ecece-1200-41f7-a992-38f186c90803" - DEFAULT_STATUSES_ENDPOINT: str = "a2843b3c-673d-4966-9a14-2e7d76dcd0cb" - DEFAULT_CREATE_SUBMISSION_ENDPOINT: str = "be2d195e-dd58-4770-9f3c-d6c0fbc2b6e5" - DEFAULT_GET_SUBMISSION_ENDPOINT: str = "c3a457cd-37a6-4106-97a8-9e60a223abbc" - DEFAULT_CREATE_SUBMISSIONS_ENDPOINT: str = "c64df5d3-edfd-4b08-8687-561af2f80d2f" - DEFAULT_GET_SUBMISSIONS_ENDPOINT: str = "5d173718-8e6a-4cf5-9d8c-db5e6386d037" + DEFAULT_ABOUT_ENDPOINT: ClassVar[str] = "1fd631a1-be6a-47d6-bf4c-987e357e3096" + DEFAULT_CONFIG_INFO_ENDPOINT: ClassVar[str] = "46e05354-2a43-436a-9458-5d111456f0ff" + DEFAULT_LANGUAGE_ENDPOINT: ClassVar[str] = "10465a84-2a2c-4213-845f-45e3c04a5867" + DEFAULT_LANGUAGES_ENDPOINT: ClassVar[str] = "774ecece-1200-41f7-a992-38f186c90803" + DEFAULT_STATUSES_ENDPOINT: ClassVar[str] = "a2843b3c-673d-4966-9a14-2e7d76dcd0cb" + DEFAULT_CREATE_SUBMISSION_ENDPOINT: ClassVar[str] = ( + "be2d195e-dd58-4770-9f3c-d6c0fbc2b6e5" + ) + DEFAULT_GET_SUBMISSION_ENDPOINT: ClassVar[str] = ( + "c3a457cd-37a6-4106-97a8-9e60a223abbc" + ) + DEFAULT_CREATE_SUBMISSIONS_ENDPOINT: ClassVar[str] = ( + "c64df5d3-edfd-4b08-8687-561af2f80d2f" + ) + DEFAULT_GET_SUBMISSIONS_ENDPOINT: ClassVar[str] = ( + "5d173718-8e6a-4cf5-9d8c-db5e6386d037" + ) def __init__(self, api_key, **kwargs): super().__init__( @@ -449,7 +469,7 @@ def get_submissions( class Rapid(Client): """Base class for all RapidAPI clients.""" - API_KEY_ENV = "JUDGE0_RAPID_API_KEY" + API_KEY_ENV: ClassVar[str] = "JUDGE0_RAPID_API_KEY" def __init__(self, endpoint, host_header_value, api_key, **kwargs): self.api_key = api_key @@ -466,9 +486,9 @@ def __init__(self, endpoint, host_header_value, api_key, **kwargs): class RapidJudge0CE(Rapid): """RapidAPI client for CE flavor.""" - DEFAULT_ENDPOINT: str = "https://judge0-ce.p.rapidapi.com" - DEFAULT_HOST: str = "judge0-ce.p.rapidapi.com" - HOME_URL: str = "https://rapidapi.com/judge0-official/api/judge0-ce" + DEFAULT_ENDPOINT: ClassVar[str] = "https://judge0-ce.p.rapidapi.com" + DEFAULT_HOST: ClassVar[str] = "judge0-ce.p.rapidapi.com" + HOME_URL: ClassVar[str] = "https://rapidapi.com/judge0-official/api/judge0-ce" def __init__(self, api_key, **kwargs): super().__init__( @@ -482,9 +502,9 @@ def __init__(self, api_key, **kwargs): class RapidJudge0ExtraCE(Rapid): """RapidAPI client for Extra CE flavor.""" - DEFAULT_ENDPOINT: str = "https://judge0-extra-ce.p.rapidapi.com" - DEFAULT_HOST: str = "judge0-extra-ce.p.rapidapi.com" - HOME_URL: str = "https://rapidapi.com/judge0-official/api/judge0-extra-ce" + DEFAULT_ENDPOINT: ClassVar[str] = "https://judge0-extra-ce.p.rapidapi.com" + DEFAULT_HOST: ClassVar[str] = "judge0-extra-ce.p.rapidapi.com" + HOME_URL: ClassVar[str] = "https://rapidapi.com/judge0-official/api/judge0-extra-ce" def __init__(self, api_key, **kwargs): super().__init__( @@ -498,7 +518,7 @@ def __init__(self, api_key, **kwargs): class Sulu(Client): """Base class for all Sulu clients.""" - API_KEY_ENV = "JUDGE0_SULU_API_KEY" + API_KEY_ENV: ClassVar[str] = "JUDGE0_SULU_API_KEY" def __init__(self, endpoint, api_key=None, **kwargs): self.api_key = api_key @@ -512,8 +532,8 @@ def __init__(self, endpoint, api_key=None, **kwargs): class SuluJudge0CE(Sulu): """Sulu client for CE flavor.""" - DEFAULT_ENDPOINT: str = "https://judge0-ce.p.sulu.sh" - HOME_URL: str = "https://sparkhub.sulu.sh/apis/judge0/judge0-ce/readme" + DEFAULT_ENDPOINT: ClassVar[str] = "https://judge0-ce.p.sulu.sh" + HOME_URL: ClassVar[str] = "https://sparkhub.sulu.sh/apis/judge0/judge0-ce/readme" def __init__(self, api_key=None, **kwargs): super().__init__( @@ -526,8 +546,10 @@ def __init__(self, api_key=None, **kwargs): class SuluJudge0ExtraCE(Sulu): """Sulu client for Extra CE flavor.""" - DEFAULT_ENDPOINT: str = "https://judge0-extra-ce.p.sulu.sh" - HOME_URL: str = "https://sparkhub.sulu.sh/apis/judge0/judge0-extra-ce/readme" + DEFAULT_ENDPOINT: ClassVar[str] = "https://judge0-extra-ce.p.sulu.sh" + HOME_URL: ClassVar[str] = ( + "https://sparkhub.sulu.sh/apis/judge0/judge0-extra-ce/readme" + ) def __init__(self, api_key=None, **kwargs): super().__init__(self.DEFAULT_ENDPOINT, api_key, **kwargs) From e3270b0189e9247c2d7f075faa57503ac34880eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Mon, 16 Dec 2024 18:22:32 +0100 Subject: [PATCH 45/52] Update docs for api module. --- docs/source/api/index.rst | 3 +- docs/source/api/types.rst | 6 +++ docs/source/index.rst | 8 ++-- src/judge0/api.py | 85 ++++++++++++++++++++++++++++++--------- 4 files changed, 77 insertions(+), 25 deletions(-) create mode 100644 docs/source/api/types.rst diff --git a/docs/source/api/index.rst b/docs/source/api/index.rst index ae975c4..eb4ed67 100644 --- a/docs/source/api/index.rst +++ b/docs/source/api/index.rst @@ -3,4 +3,5 @@ api submission - clients \ No newline at end of file + clients + types \ No newline at end of file diff --git a/docs/source/api/types.rst b/docs/source/api/types.rst new file mode 100644 index 0000000..2b415b3 --- /dev/null +++ b/docs/source/api/types.rst @@ -0,0 +1,6 @@ +Types Module +============ + +.. automodule:: judge0.base_types + :members: + :undoc-members: diff --git a/docs/source/index.rst b/docs/source/index.rst index 5df6c21..6c202aa 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -38,17 +38,17 @@ Getting Involved TODO .. toctree:: - :caption: Getting Involved + :caption: API :glob: :titlesonly: :hidden: - contributors_guide/index + api/index .. toctree:: - :caption: API + :caption: Getting Involved :glob: :titlesonly: :hidden: - api/index \ No newline at end of file + contributors_guide/index \ No newline at end of file diff --git a/src/judge0/api.py b/src/judge0/api.py index 7b2fb22..e6d60e2 100644 --- a/src/judge0/api.py +++ b/src/judge0/api.py @@ -9,6 +9,18 @@ def get_client(flavor: Flavor = Flavor.CE) -> Client: + """Resolve client from API keys from environment or default to preview client. + + Parameters + ---------- + flavor : Flavor + Flavor of Judge0 Client. + + Returns + ------- + Client + An object of base type Client and the specified flavor. + """ from . import _get_implicit_client if isinstance(flavor, Flavor): @@ -26,15 +38,32 @@ def _resolve_client( ) -> Client: """Resolve a client from flavor or submission(s) arguments. + Parameters + ---------- + client : Client or Flavor, optional + A Client object or flavor of client. Returns the client if not None. + submissions: Submission or Submissions, optional + Submission(s) used to determine the suitable client. + + Returns + ------- + Client + An object of base type Client. + Raises ------ ClientResolutionError - Raised if client resolution fails. + If there is no implemented client that supports all the languages specified + in the submissions. """ # User explicitly passed a client. if isinstance(client, Client): return client + # NOTE: At the moment, we do not support the option to check if explicit + # flavor of a client supports the submissions, i.e. submissions argument is + # ignored if flavor argument is provided. + if isinstance(client, Flavor): return get_client(client) @@ -42,7 +71,7 @@ def _resolve_client( raise ValueError("Client cannot be determined from empty submissions.") # client is None and we have to determine a flavor of the client from the - # submissions and the languages. + # the submission's languages. if isinstance(submissions, Submission): submissions = [submissions] @@ -65,18 +94,17 @@ def _resolve_client( def create_submissions( *, - client: Optional[Client] = None, + client: Optional[Union[Client, Flavor]] = None, submissions: Optional[Union[Submission, Submissions]] = None, ) -> Union[Submission, Submissions]: - """Create submissions to a client. + """Universal function for creating submissions to the client. Parameters ---------- - client : Client, optional - A Client where submissions should be created. If None, will try to - be automatically resolved. - submissions: Submission, Submissions - A submission or submissions to create. + client : Client or Flavor, optional + A client or client flavor where submissions should be created. + submissions: Submission or Submissions, optional + Submission(s) to create. Raises ------ @@ -102,19 +130,20 @@ def create_submissions( def get_submissions( *, - client: Optional[Client] = None, + client: Optional[Union[Client, Flavor]] = None, submissions: Optional[Union[Submission, Submissions]] = None, fields: Optional[Union[str, Iterable[str]]] = None, ) -> Union[Submission, Submissions]: - """Create submissions to a client. + """Get submission (status) from a client. Parameters ---------- - client : Client, optional - A Client where submissions should be created. If None, will try to - be automatically resolved. - submissions: Submission, Submissions - A submission or submissions to create. + client : Client or Flavor, optional + A client or client flavor where submissions should be checked. + submissions : Submission or Submissions, optional + Submission(s) to update. + fields : str or sequence of str, optional + Submission attributes that need to be updated. Defaults to all attributes. Raises ------ @@ -144,10 +173,26 @@ def get_submissions( def wait( *, - client: Optional[Client] = None, + client: Optional[Union[Client, Flavor]] = None, submissions: Optional[Union[Submission, Submissions]] = None, retry_strategy: Optional[RetryStrategy] = None, ) -> Union[Submission, Submissions]: + """Wait for all the submissions to finish. + + Parameters + ---------- + client : Client or Flavor, optional + A client or client flavor where submissions should be checked. + submissions : Submission or Submissions + Submission(s) to wait for. + retry_strategy : RetryStrategy, optional + A retry strategy. + + Raises + ------ + ClientResolutionError + Raised if client resolution fails. + """ client = _resolve_client(client, submissions) if retry_strategy is None: @@ -189,9 +234,9 @@ def create_submissions_from_test_cases( submissions: Union[Submission, Submissions], test_cases: Optional[Union[TestCaseType, TestCases]] = None, ) -> Union[Submission, list[Submission]]: - """Create submissions from the (submission, test_case) pairs. + """Create submissions from the submission and test case pairs. - This function always returns a deep copy so make sure you are using the + Function always returns a deep copy so make sure you are using the returned submission(s). Parameters @@ -335,7 +380,7 @@ def sync_execute( A client where submissions should be created. If None, will try to be resolved. submissions : Submission or Submissions, optional - Submission or submissions for execution. + Submission(s) for execution. source_code: str, optional A source code of a program. test_cases: TestCaseType or TestCases, optional From dd1ae42d74a1a06611bf6d4b4c34f891d96eff9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Tue, 17 Dec 2024 20:27:43 +0100 Subject: [PATCH 46/52] Fix language change after run. (#11) * Fix language change after run. * Add unit test. --- src/judge0/submission.py | 1 + tests/test_submission.py | 23 ++++++++++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/judge0/submission.py b/src/judge0/submission.py index 069733c..8e5d1cb 100644 --- a/src/judge0/submission.py +++ b/src/judge0/submission.py @@ -271,6 +271,7 @@ def pre_execution_copy(self) -> "Submission": new_submission = Submission() for attr in REQUEST_FIELDS: setattr(new_submission, attr, copy.deepcopy(getattr(self, attr))) + new_submission.language = self.language return new_submission def __iter__(self): diff --git a/tests/test_submission.py b/tests/test_submission.py index 98903ed..fb1bf73 100644 --- a/tests/test_submission.py +++ b/tests/test_submission.py @@ -1,4 +1,5 @@ -from judge0 import Status, Submission, wait +from judge0 import run, Status, Submission, wait +from judge0.base_types import LanguageAlias def test_from_json(): @@ -71,3 +72,23 @@ def test_is_done(request): wait(client=client, submissions=submission) assert submission.is_done() + + +def test_language_before_and_after_execution(request): + client = request.getfixturevalue("judge0_ce_client") + code = """\ + public class Main { + public static void main(String[] args) { + System.out.println("Hello World"); + } + } + """ + + submission = Submission( + source_code=code, + language=LanguageAlias.JAVA, + ) + + assert submission.language == LanguageAlias.JAVA + submission = run(client=client, submissions=submission) + assert submission.language == LanguageAlias.JAVA From 2861d3335fd7d6d9f4632ed1fb9b27e52a46ff37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Tue, 17 Dec 2024 21:23:44 +0100 Subject: [PATCH 47/52] Add tests for TestCase.from_record method. Switch the TestCase.from_record method from static to class method. --- src/judge0/base_types.py | 14 +++++++---- tests/test_api_test_cases.py | 47 +++++++++++++++++++++++++++++++++++- 2 files changed, 55 insertions(+), 6 deletions(-) diff --git a/src/judge0/base_types.py b/src/judge0/base_types.py index 48480e8..125dc54 100644 --- a/src/judge0/base_types.py +++ b/src/judge0/base_types.py @@ -1,3 +1,5 @@ +import copy + from dataclasses import dataclass from enum import IntEnum from typing import Optional, Protocol, runtime_checkable, Sequence, Union @@ -15,8 +17,8 @@ class TestCase: input: Optional[str] = None expected_output: Optional[str] = None - @staticmethod - def from_record(test_case: Optional[TestCaseType] = None) -> "TestCase": + @classmethod + def from_record(cls, test_case: TestCaseType) -> "TestCase": """Create a TestCase from built-in types.""" if isinstance(test_case, (tuple, list)): test_case = { @@ -24,12 +26,14 @@ def from_record(test_case: Optional[TestCaseType] = None) -> "TestCase": for field, value in zip(("input", "expected_output"), test_case) } if isinstance(test_case, dict): - return TestCase( + return cls( input=test_case.get("input", None), expected_output=test_case.get("expected_output", None), ) - if isinstance(test_case, TestCase) or test_case is None: - return test_case + if isinstance(test_case, cls): + return copy.deepcopy(test_case) + if test_case is None: + return cls() raise ValueError( f"Cannot create TestCase object from object of type {type(test_case)}." ) diff --git a/tests/test_api_test_cases.py b/tests/test_api_test_cases.py index f395279..82ec870 100644 --- a/tests/test_api_test_cases.py +++ b/tests/test_api_test_cases.py @@ -6,6 +6,51 @@ from judge0.api import create_submissions_from_test_cases +@pytest.mark.parametrize( + "test_case,expected_output", + [ + [ + TestCase(input="input_1", expected_output="output_1"), + TestCase(input="input_1", expected_output="output_1"), + ], + [ + tuple([]), + TestCase(input=None, expected_output=None), + ], + [ + ("input_tuple",), + TestCase(input="input_tuple", expected_output=None), + ], + [ + ("input_tuple", "output_tuple"), + TestCase(input="input_tuple", expected_output="output_tuple"), + ], + [ + [], + TestCase(input=None, expected_output=None), + ], + [ + ["input_list"], + TestCase(input="input_list", expected_output=None), + ], + [ + ["input_list", "output_list"], + TestCase(input="input_list", expected_output="output_list"), + ], + [ + {"input": "input_dict", "expected_output": "output_dict"}, + TestCase(input="input_dict", expected_output="output_dict"), + ], + [ + None, + TestCase(), + ], + ], +) +def test_test_case_from_record(test_case, expected_output): + assert TestCase.from_record(test_case) == expected_output + + @pytest.mark.parametrize( "submissions,test_cases,expected_type", [ @@ -19,7 +64,7 @@ def test_create_submissions_from_test_cases_return_type( submissions, test_cases, expected_type ): output = create_submissions_from_test_cases(submissions, test_cases) - assert type(output) == expected_type + assert type(output) is expected_type @pytest.mark.parametrize( From e8b05b2a79b2616dc63ba69b22e16940148b80e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Tue, 17 Dec 2024 21:51:42 +0100 Subject: [PATCH 48/52] Make test cases work with all possible variations. --- src/judge0/api.py | 27 ++++++++--- tests/test_api_test_cases.py | 90 +++++++++++++++++++++++++++++++++++- 2 files changed, 110 insertions(+), 7 deletions(-) diff --git a/src/judge0/api.py b/src/judge0/api.py index e6d60e2..e254dd9 100644 --- a/src/judge0/api.py +++ b/src/judge0/api.py @@ -260,10 +260,27 @@ def create_submissions_from_test_cases( if isinstance(test_cases, TestCase) or test_cases is None: test_cases_list = [test_cases] + multiple_test_cases = False else: - test_cases_list = test_cases - - test_cases_list = [TestCase.from_record(tc) for tc in test_cases_list] + try: + # Let's assume that we are dealing with multiple test_cases that + # can be created from test_cases argument. If this fails, i.e. + # raises a ValueError, we know we are dealing with a test_cases=dict, + # or test_cases=["in", "out"], or test_cases=tuple("in", "out"). + test_cases_list = [TestCase.from_record(tc) for tc in test_cases] + + # It is possible to send test_cases={}, or test_cases=[], or + # test_cases=tuple([]). In this case, we are treating that as None. + if len(test_cases) > 0: + multiple_test_cases = True + else: + multiple_test_cases = False + test_cases_list = [None] + except ValueError: + test_cases_list = [test_cases] + multiple_test_cases = False + + test_cases_list = [TestCase.from_record(test_case=tc) for tc in test_cases_list] all_submissions = [] for submission in submissions_list: @@ -274,9 +291,7 @@ def create_submissions_from_test_cases( submission_copy.expected_output = test_case.expected_output all_submissions.append(submission_copy) - if isinstance(submissions, Submission) and ( - isinstance(test_cases, TestCase) or test_cases is None - ): + if isinstance(submissions, Submission) and (not multiple_test_cases): return all_submissions[0] else: return all_submissions diff --git a/tests/test_api_test_cases.py b/tests/test_api_test_cases.py index 82ec870..0dcb129 100644 --- a/tests/test_api_test_cases.py +++ b/tests/test_api_test_cases.py @@ -1,4 +1,4 @@ -"""Separate file containg tests related to test case functionality.""" +"""Separate file containing tests related to test case functionality.""" import judge0 import pytest @@ -67,6 +67,94 @@ def test_create_submissions_from_test_cases_return_type( assert type(output) is expected_type +class TestCreateSubmissionsFromTestCases: + @pytest.mark.parametrize( + "test_case,stdin,expected_output", + [ + [TestCase(), None, None], + [[], None, None], + [{}, None, None], + [tuple([]), None, None], + ], + ) + def test_empty_test_case(self, test_case, stdin, expected_output): + submission = create_submissions_from_test_cases( + Submission(), test_cases=test_case + ) + + assert ( + submission.stdin == stdin and submission.expected_output == expected_output + ) + + @pytest.mark.parametrize( + "test_case,stdin,expected_output", + [ + [TestCase(), None, None], + [TestCase(input="input"), "input", None], + [TestCase(expected_output="output"), None, "output"], + [["input_list"], "input_list", None], + [["input_list", "output_list"], "input_list", "output_list"], + [{"input": "input_dict"}, "input_dict", None], + [ + {"input": "input_dict", "expected_output": "output_dict"}, + "input_dict", + "output_dict", + ], + [("input_tuple",), "input_tuple", None], + [("input_tuple", "output_tuple"), "input_tuple", "output_tuple"], + ], + ) + def test_single_test_case(self, test_case, stdin, expected_output): + submission = create_submissions_from_test_cases( + Submission(), test_cases=test_case + ) + + assert ( + submission.stdin == stdin and submission.expected_output == expected_output + ) + + @pytest.mark.parametrize( + "test_cases,stdin,expected_output", + [ + [[TestCase()], None, None], + [[TestCase(input="input")], "input", None], + [[TestCase(expected_output="output")], None, "output"], + [(["input_list"],), "input_list", None], + [(["input_list", "output_list"],), "input_list", "output_list"], + [({"input": "input_dict"},), "input_dict", None], + [ + ({"input": "input_dict", "expected_output": "output_dict"},), + "input_dict", + "output_dict", + ], + [ + [ + ("input_tuple",), + ], + "input_tuple", + None, + ], + [ + [ + ("input_tuple", "output_tuple"), + ], + "input_tuple", + "output_tuple", + ], + ], + ) + def test_single_test_case_in_iterable(self, test_cases, stdin, expected_output): + submissions = create_submissions_from_test_cases( + Submission(), test_cases=test_cases + ) + + for submission in submissions: + assert ( + submission.stdin == stdin + and submission.expected_output == expected_output + ) + + @pytest.mark.parametrize( "source_code_or_submissions,test_cases,expected_status", [ From 5a2bd1fe38a6fe493c2a140e5ede95bad9a24bac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Tue, 17 Dec 2024 22:13:57 +0100 Subject: [PATCH 49/52] Fix filesystem example. --- examples/0005_filesystem.py | 16 ++++++++-------- src/judge0/submission.py | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/examples/0005_filesystem.py b/examples/0005_filesystem.py index c75a1b4..dc79eb6 100644 --- a/examples/0005_filesystem.py +++ b/examples/0005_filesystem.py @@ -3,7 +3,7 @@ print("Subexample 1") result = judge0.run(source_code="print('hello, world')") -fs = Filesystem(result.post_execution_filesystem) +fs = Filesystem(content=result.post_execution_filesystem) for f in fs: print(f.name) print(f) @@ -11,19 +11,19 @@ print("Subexample 2") -fs = Filesystem(File("my_file.txt", "hello, world")) +fs = Filesystem(content=File(name="my_file.txt", content="hello, world")) result = judge0.run( source_code="print(open('my_file.txt').read())", additional_files=fs ) print(result.stdout) -for f in Filesystem(result.post_execution_filesystem): +for f in Filesystem(content=result.post_execution_filesystem): print(f.name) print(f) print() print("Subexample 3") -fs = Filesystem(File("my_file.txt", "hello, world")) +fs = Filesystem(content=File(name="my_file.txt", content="hello, world")) result = judge0.run( source_code="print(open('my_file.txt').read())", additional_files=fs ) @@ -35,14 +35,14 @@ print("Subexample 4") fs = Filesystem( - [ - File("my_file.txt", "hello, world"), - File("./dir1/dir2/dir3/my_file2.txt", "hello, world2"), + content=[ + File(name="my_file.txt", content="hello, world"), + File(name="./dir1/dir2/dir3/my_file2.txt", content="hello, world2"), ] ) result = judge0.run(source_code="find .", additional_files=fs, language=46) print(result.stdout) -for f in Filesystem(result.post_execution_filesystem): +for f in Filesystem(content=result.post_execution_filesystem): print(f.name) print(f) print() diff --git a/src/judge0/submission.py b/src/judge0/submission.py index 8e5d1cb..b9d474c 100644 --- a/src/judge0/submission.py +++ b/src/judge0/submission.py @@ -131,7 +131,7 @@ class Submission(BaseModel): default=LanguageAlias.PYTHON, repr=True, ) - additional_files: Optional[str] = Field(default=None, repr=True) + additional_files: Optional[Union[str, Filesystem]] = Field(default=None, repr=True) compiler_options: Optional[str] = Field(default=None, repr=True) command_line_arguments: Optional[str] = Field(default=None, repr=True) stdin: Optional[str] = Field(default=None, repr=True) From d4b3a2d05a84d2266e8d3c6014de0a6fbba865b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Tue, 17 Dec 2024 22:19:10 +0100 Subject: [PATCH 50/52] Return None in from_record if None is passed. --- src/judge0/base_types.py | 6 ++++-- tests/test_api_test_cases.py | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/judge0/base_types.py b/src/judge0/base_types.py index 125dc54..05a7a64 100644 --- a/src/judge0/base_types.py +++ b/src/judge0/base_types.py @@ -18,7 +18,9 @@ class TestCase: expected_output: Optional[str] = None @classmethod - def from_record(cls, test_case: TestCaseType) -> "TestCase": + def from_record( + cls, test_case: Union[TestCaseType, None] + ) -> Union["TestCase", None]: """Create a TestCase from built-in types.""" if isinstance(test_case, (tuple, list)): test_case = { @@ -33,7 +35,7 @@ def from_record(cls, test_case: TestCaseType) -> "TestCase": if isinstance(test_case, cls): return copy.deepcopy(test_case) if test_case is None: - return cls() + return None raise ValueError( f"Cannot create TestCase object from object of type {type(test_case)}." ) diff --git a/tests/test_api_test_cases.py b/tests/test_api_test_cases.py index 0dcb129..0d08f5f 100644 --- a/tests/test_api_test_cases.py +++ b/tests/test_api_test_cases.py @@ -43,7 +43,7 @@ ], [ None, - TestCase(), + None, ], ], ) From f63c19a89b8f35d3fb52bbb4200da1b42b15bf6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Tue, 17 Dec 2024 22:20:53 +0100 Subject: [PATCH 51/52] Update pyproject.toml to prepare release candidate 0.0.2. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b53813e..9719326 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "judge0" -version = "0.0.2dev" +version = "0.0.2rc1" description = "The official Python library for Judge0." readme = "README.md" requires-python = ">=3.9" From fd00c399726f11b576fb89323e00a6edefbeee2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Tue, 17 Dec 2024 22:23:11 +0100 Subject: [PATCH 52/52] Set version to 0.0.2 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9719326..3568054 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "judge0" -version = "0.0.2rc1" +version = "0.0.2" description = "The official Python library for Judge0." readme = "README.md" requires-python = ">=3.9"