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" pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy