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 001/109] 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 002/109] 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 003/109] 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 004/109] 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 005/109] 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 006/109] 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 007/109] 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 008/109] 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 009/109] 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 010/109] 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 011/109] 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 012/109] 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 013/109] 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 014/109] 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 015/109] 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 016/109] 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 017/109] 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 018/109] 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 019/109] 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 020/109] 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 021/109] 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 022/109] 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 023/109] 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 024/109] 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 025/109] 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 026/109] 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 027/109] 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 028/109] 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 029/109] 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 030/109] 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 031/109] 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 032/109] 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 033/109] 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 034/109] 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 035/109] 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 036/109] 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 037/109] 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 038/109] 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 039/109] 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 040/109] 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 041/109] 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 042/109] 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 043/109] 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 044/109] 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 045/109] 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 046/109] 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 047/109] 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 048/109] 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 049/109] 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 050/109] 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 051/109] 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 052/109] 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" From 4f054cabc1a148d75cbc1102456d9df86a4c0c54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Fri, 20 Dec 2024 18:03:03 +0100 Subject: [PATCH 053/109] Change the title of release notes doc. --- docs/source/contributors_guide/release_notes.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/contributors_guide/release_notes.rst b/docs/source/contributors_guide/release_notes.rst index 0b6251f..ec66b1c 100644 --- a/docs/source/contributors_guide/release_notes.rst +++ b/docs/source/contributors_guide/release_notes.rst @@ -1,4 +1,4 @@ -How to create a release candidate -================================= +How to create a release +======================= TODO \ No newline at end of file From 11a4f784a80070a3b1e120c854c2f3be93084acf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Fri, 20 Dec 2024 22:57:15 +0100 Subject: [PATCH 054/109] Initial commit of working docs versioning. --- .github/workflows/docs.yml | 4 +++- docs/requirements.txt | 1 + docs/source/_templates/versioning.html | 8 ++++++++ docs/source/conf.py | 27 ++++++++++++++++++++++---- docs/source/index.rst | 2 +- 5 files changed, 36 insertions(+), 6 deletions(-) create mode 100644 docs/source/_templates/versioning.html diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 1d5909d..efd3954 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -18,6 +18,8 @@ jobs: - name: Build HTML uses: ammaraskar/sphinx-action@7.0.0 + with: + build-command: "sphinx-multiversion source build/html" - name: Upload artifacts uses: actions/upload-artifact@v4 @@ -30,4 +32,4 @@ jobs: if: github.ref == 'refs/heads/master' with: github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: docs/build/html \ No newline at end of file + publish_dir: docs/build/html/master \ No newline at end of file diff --git a/docs/requirements.txt b/docs/requirements.txt index 8e76ca0..ae1043d 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,2 +1,3 @@ sphinxawesome-theme==5.3.2 sphinx-autodoc-typehints==2.3.0 +sphinx-multiversion==0.2.4 \ No newline at end of file diff --git a/docs/source/_templates/versioning.html b/docs/source/_templates/versioning.html new file mode 100644 index 0000000..ef74ed4 --- /dev/null +++ b/docs/source/_templates/versioning.html @@ -0,0 +1,8 @@ +{% if versions %} +

{{ _('Versions') }}

+ +{% endif %} \ No newline at end of file diff --git a/docs/source/conf.py b/docs/source/conf.py index 77420a5..9312dcd 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -13,7 +13,7 @@ project = "Judge0 Python SDK" copyright = "2024, Judge0" author = "Judge0" -release = "0.1" +release = "" # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration @@ -23,19 +23,24 @@ "sphinx.ext.napoleon", "sphinx.ext.autosummary", "sphinx_autodoc_typehints", + "sphinx_multiversion", ] templates_path = ["_templates"] exclude_patterns = [] - -# add_module_names = False - # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output html_theme = "sphinxawesome_theme" html_show_sphinx = False +html_sidebars = { + "**": [ + "sidebar_main_nav_links.html", + "sidebar_toc.html", + "versioning.html", + ], +} sys.path.insert(0, os.path.abspath("../../src/")) # Adjust as needed @@ -50,3 +55,17 @@ autodoc_mock_imports = ["requests", "pydantic"] napoleon_google_docstring = False + +# Whitelist pattern for tags (set to None to ignore all tags) +smv_tag_whitelist = r"^.*$" +# Whitelist pattern for branches (set to None to ignore all branches) +smv_branch_whitelist = r"^master$" +# Whitelist pattern for remotes (set to None to use local branches only) +smv_remote_whitelist = None +# Pattern for released versions +smv_released_pattern = "" # r"^tags/.*$" +# Format for versioned output directories inside the build directory +smv_outputdir_format = "{ref.name}" +# Determines whether remote or local git branches/tags are preferred if their +# output dirs conflict +smv_prefer_remote_refs = False diff --git a/docs/source/index.rst b/docs/source/index.rst index 6c202aa..b8ebcd9 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -15,7 +15,7 @@ You can run minimal Hello World example in three easy steps: 2. Create a minimal script: -.. code-block:: Python +.. code-block:: python import judge0 From 6ae246f7744160e45e9cbd2dd7301d9e64894d18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Fri, 20 Dec 2024 23:30:20 +0100 Subject: [PATCH 055/109] Update docs.yml workflow. --- .github/workflows/docs.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index efd3954..81ac601 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -19,7 +19,7 @@ jobs: - name: Build HTML uses: ammaraskar/sphinx-action@7.0.0 with: - build-command: "sphinx-multiversion source build/html" + build-command: "sphinx-multiversion source build/html -e" - name: Upload artifacts uses: actions/upload-artifact@v4 @@ -32,4 +32,4 @@ jobs: if: github.ref == 'refs/heads/master' with: github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: docs/build/html/master \ No newline at end of file + publish_dir: docs/build/html/master From 013a96ca064adad1e827fd46804f2f8b07599d54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Fri, 20 Dec 2024 23:35:40 +0100 Subject: [PATCH 056/109] Update docs.yml workflow with redirection to sphinx-log.txt --- .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 81ac601..bdea8f0 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -19,7 +19,7 @@ jobs: - name: Build HTML uses: ammaraskar/sphinx-action@7.0.0 with: - build-command: "sphinx-multiversion source build/html -e" + build-command: "sphinx-multiversion source build/html --keep-going --no-color > sphinx-log.txt" - name: Upload artifacts uses: actions/upload-artifact@v4 From 60fbef35ebafc3f8e948dc183da9670db184c361 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Fri, 20 Dec 2024 23:48:48 +0100 Subject: [PATCH 057/109] Update docs.yml with explicit creation of sphinx-log --- .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 bdea8f0..f30f497 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -19,7 +19,7 @@ jobs: - name: Build HTML uses: ammaraskar/sphinx-action@7.0.0 with: - build-command: "sphinx-multiversion source build/html --keep-going --no-color > sphinx-log.txt" + build-command: "touch /tmp/sphinx-log && sphinx-multiversion source build/html" - name: Upload artifacts uses: actions/upload-artifact@v4 From aef4e8ed08e022a7b915f283ec59c4c9e2135fae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Fri, 20 Dec 2024 23:56:55 +0100 Subject: [PATCH 058/109] Update docs.yml --- .github/workflows/docs.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index f30f497..9c3b412 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -16,10 +16,15 @@ jobs: with: persist-credentials: false + # Temporary solution since sphinx-multiversion does + # not have -w option. + - name: Pre-create log file + run: touch /tmp/sphinx-log + - name: Build HTML uses: ammaraskar/sphinx-action@7.0.0 with: - build-command: "touch /tmp/sphinx-log && sphinx-multiversion source build/html" + build-command: "sphinx-multiversion source build/html" - name: Upload artifacts uses: actions/upload-artifact@v4 From de2ee1ef477b4e8a8be8bf8320adbd5d9df1a1f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Sat, 21 Dec 2024 00:07:38 +0100 Subject: [PATCH 059/109] Update to remove ammaraskar/sphinx-action@7.0.0 --- .github/workflows/docs.yml | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 9c3b412..5a05350 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -16,15 +16,18 @@ jobs: with: persist-credentials: false - # Temporary solution since sphinx-multiversion does - # not have -w option. - - name: Pre-create log file - run: touch /tmp/sphinx-log - - - name: Build HTML - uses: ammaraskar/sphinx-action@7.0.0 + - name: Set up Python + uses: actions/setup-python@v4 with: - build-command: "sphinx-multiversion source build/html" + python-version: "3.9" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install sphinx==7.4.7 sphinxawesome-theme==5.3.2 sphinx-autodoc-typehints==2.3.0 sphinx-multiversion==0.2.4 + + - name: Build documentation + run: sphinx-multiversion docs/source docs/build/html --keep-going --no-color" - name: Upload artifacts uses: actions/upload-artifact@v4 From 4cecba931bf0d34f3d73f5b6f7777375df642a23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Sat, 21 Dec 2024 00:09:00 +0100 Subject: [PATCH 060/109] Fix error in docs.yml --- .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 5a05350..3a864c7 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -27,7 +27,7 @@ jobs: pip install sphinx==7.4.7 sphinxawesome-theme==5.3.2 sphinx-autodoc-typehints==2.3.0 sphinx-multiversion==0.2.4 - name: Build documentation - run: sphinx-multiversion docs/source docs/build/html --keep-going --no-color" + run: sphinx-multiversion docs/source docs/build/html --keep-going --no-color - name: Upload artifacts uses: actions/upload-artifact@v4 From bc12fad8fae9c6e4f7dd565c0adc9fa5f642c4fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Sat, 21 Dec 2024 00:11:36 +0100 Subject: [PATCH 061/109] Use docs/requirements in docs.yml workflow. --- .github/workflows/docs.yml | 2 +- docs/requirements.txt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 3a864c7..af15327 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -24,7 +24,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install sphinx==7.4.7 sphinxawesome-theme==5.3.2 sphinx-autodoc-typehints==2.3.0 sphinx-multiversion==0.2.4 + pip install -r docs/requirements.txt - name: Build documentation run: sphinx-multiversion docs/source docs/build/html --keep-going --no-color diff --git a/docs/requirements.txt b/docs/requirements.txt index ae1043d..cd3bc0f 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,3 +1,4 @@ +sphinx==7.4.7 sphinxawesome-theme==5.3.2 sphinx-autodoc-typehints==2.3.0 sphinx-multiversion==0.2.4 \ No newline at end of file From 88d8dc26895de7505f5a7d76a0d30f47da06279a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Sat, 21 Dec 2024 00:14:28 +0100 Subject: [PATCH 062/109] Enable release tags in conf.py 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 9312dcd..3d715eb 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -63,7 +63,7 @@ # Whitelist pattern for remotes (set to None to use local branches only) smv_remote_whitelist = None # Pattern for released versions -smv_released_pattern = "" # r"^tags/.*$" +smv_released_pattern = r"^tags/.*$" # Format for versioned output directories inside the build directory smv_outputdir_format = "{ref.name}" # Determines whether remote or local git branches/tags are preferred if their From c68197e56f24f56afc63d1e49fbd4447a994306b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Sat, 21 Dec 2024 00:21:45 +0100 Subject: [PATCH 063/109] Fetch all tags of repo for building docs. --- .github/workflows/docs.yml | 7 ++++++- docs/source/conf.py | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index af15327..7fe6d47 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -15,7 +15,12 @@ jobs: - uses: actions/checkout@v4 with: persist-credentials: false - + fetch-depth: 0 # Fetch the full history + ref: ${{ github.ref }} # Check out the current branch or tag + + - name: Fetch tags only + run: git fetch --tags --no-recurse-submodules + - name: Set up Python uses: actions/setup-python@v4 with: diff --git a/docs/source/conf.py b/docs/source/conf.py index 3d715eb..9312dcd 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -63,7 +63,7 @@ # Whitelist pattern for remotes (set to None to use local branches only) smv_remote_whitelist = None # Pattern for released versions -smv_released_pattern = r"^tags/.*$" +smv_released_pattern = "" # r"^tags/.*$" # Format for versioned output directories inside the build directory smv_outputdir_format = "{ref.name}" # Determines whether remote or local git branches/tags are preferred if their From ca0d721962c634cf906b47b56c7c6312d022c118 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Sat, 21 Dec 2024 00:24:59 +0100 Subject: [PATCH 064/109] Update publish_dir 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 7fe6d47..42c1b0c 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -45,4 +45,4 @@ jobs: if: github.ref == 'refs/heads/master' with: github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: docs/build/html/master + publish_dir: docs/build/html From 00b1ce6f66d99e1b4cecc5e9f1263ef60f941825 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Sat, 21 Dec 2024 00:37:49 +0100 Subject: [PATCH 065/109] Add redirect file. --- .github/workflows/docs.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 42c1b0c..d5c9a0d 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -34,6 +34,18 @@ jobs: - name: Build documentation run: sphinx-multiversion docs/source docs/build/html --keep-going --no-color + - name: Generate index.html for judge0.github.io/judge0-python. + run: | + echo ' + + + + + +

Redirecting to master/index.html

+ + ' > docs/build/html/index.html + - name: Upload artifacts uses: actions/upload-artifact@v4 with: From eb4fbffbd4ef9a906d14fefd7c8ac3e79afc6a59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Sat, 21 Dec 2024 00:41:19 +0100 Subject: [PATCH 066/109] Remove body from generated index.html --- .github/workflows/docs.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index d5c9a0d..4b2aa46 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -41,9 +41,6 @@ jobs: - -

Redirecting to master/index.html

- ' > docs/build/html/index.html - name: Upload artifacts From 0f63e3ed94c012b7e5c60d88a21a06ca6e97793c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Tue, 24 Dec 2024 09:17:12 +0100 Subject: [PATCH 067/109] Update Client docs. Simplify docs structure. --- docs/source/api/clients.rst | 46 ++++++++- docs/source/api/index.rst | 7 -- docs/source/api/types.rst | 2 +- docs/source/conf.py | 4 +- docs/source/contributors_guide/index.rst | 5 - docs/source/index.rst | 13 ++- src/judge0/clients.py | 115 +++++++++++++++++++++-- 7 files changed, 160 insertions(+), 32 deletions(-) delete mode 100644 docs/source/api/index.rst delete mode 100644 docs/source/contributors_guide/index.rst diff --git a/docs/source/api/clients.rst b/docs/source/api/clients.rst index 52e7e4e..b4d15c9 100644 --- a/docs/source/api/clients.rst +++ b/docs/source/api/clients.rst @@ -1,6 +1,46 @@ Clients Module ============== -.. automodule:: judge0.clients - :members: - :member-order: groupwise + +.. autoclass:: judge0.clients.Client + :exclude-members: API_KEY_ENV + +.. autoclass:: judge0.clients.ATD + :show-inheritance: + +.. autoclass:: judge0.clients.ATDJudge0CE + :show-inheritance: + :exclude-members: DEFAULT_ENDPOINT, DEFAULT_HOST, HOME_URL, DEFAULT_ABOUT_ENDPOINT, + DEFAULT_CONFIG_INFO_ENDPOINT, DEFAULT_LANGUAGE_ENDPOINT, DEFAULT_LANGUAGES_ENDPOINT, + DEFAULT_STATUSES_ENDPOINT, DEFAULT_CREATE_SUBMISSION_ENDPOINT, DEFAULT_GET_SUBMISSION_ENDPOINT, + DEFAULT_CREATE_SUBMISSIONS_ENDPOINT, DEFAULT_GET_SUBMISSIONS_ENDPOINT, get_about, + get_config_info, get_language, get_languages, get_statuses, create_submission, get_submission, + create_submissions, get_submissions + +.. autoclass:: judge0.clients.ATDJudge0ExtraCE + :show-inheritance: + :exclude-members: DEFAULT_ENDPOINT, DEFAULT_HOST, HOME_URL, DEFAULT_ABOUT_ENDPOINT, + DEFAULT_CONFIG_INFO_ENDPOINT, DEFAULT_LANGUAGE_ENDPOINT, DEFAULT_LANGUAGES_ENDPOINT, + DEFAULT_STATUSES_ENDPOINT, DEFAULT_CREATE_SUBMISSION_ENDPOINT, DEFAULT_GET_SUBMISSION_ENDPOINT, + DEFAULT_CREATE_SUBMISSIONS_ENDPOINT, DEFAULT_GET_SUBMISSIONS_ENDPOINT, get_about, + get_config_info, get_language, get_languages, get_statuses, create_submission, get_submission, + create_submissions, get_submissions + + +.. autoclass:: judge0.clients.Rapid + :show-inheritance: + +.. autoclass:: judge0.clients.RapidJudge0CE + :show-inheritance: + +.. autoclass:: judge0.clients.RapidJudge0ExtraCE + :show-inheritance: + +.. autoclass:: judge0.clients.Sulu + :show-inheritance: + +.. autoclass:: judge0.clients.SuluJudge0CE + :show-inheritance: + +.. autoclass:: judge0.clients.SuluJudge0ExtraCE + :show-inheritance: \ No newline at end of file diff --git a/docs/source/api/index.rst b/docs/source/api/index.rst deleted file mode 100644 index eb4ed67..0000000 --- a/docs/source/api/index.rst +++ /dev/null @@ -1,7 +0,0 @@ -.. toctree:: - :maxdepth: 2 - - api - submission - clients - types \ No newline at end of file diff --git a/docs/source/api/types.rst b/docs/source/api/types.rst index 2b415b3..219d7ed 100644 --- a/docs/source/api/types.rst +++ b/docs/source/api/types.rst @@ -3,4 +3,4 @@ Types Module .. automodule:: judge0.base_types :members: - :undoc-members: + diff --git a/docs/source/conf.py b/docs/source/conf.py index 9312dcd..a9ae07c 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -21,7 +21,7 @@ extensions = [ "sphinx.ext.autodoc", "sphinx.ext.napoleon", - "sphinx.ext.autosummary", + # "sphinx.ext.autosummary", "sphinx_autodoc_typehints", "sphinx_multiversion", ] @@ -47,7 +47,7 @@ autodoc_default_options = { "members": True, - "undoc-members": True, + "undoc-members": False, "private-members": False, "special-members": False, "inherited-members": False, diff --git a/docs/source/contributors_guide/index.rst b/docs/source/contributors_guide/index.rst deleted file mode 100644 index 312258b..0000000 --- a/docs/source/contributors_guide/index.rst +++ /dev/null @@ -1,5 +0,0 @@ -.. toctree:: - :maxdepth: 2 - - contributing - release_notes diff --git a/docs/source/index.rst b/docs/source/index.rst index b8ebcd9..3510578 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -25,15 +25,14 @@ You can run minimal Hello World example in three easy steps: 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 @@ -43,7 +42,10 @@ TODO :titlesonly: :hidden: - api/index + api/api + api/submission + api/clients + api/types .. toctree:: :caption: Getting Involved @@ -51,4 +53,5 @@ TODO :titlesonly: :hidden: - contributors_guide/index \ No newline at end of file + contributors_guide/contributing + contributors_guide/release_notes \ No newline at end of file diff --git a/src/judge0/clients.py b/src/judge0/clients.py index ff8e989..311d26b 100644 --- a/src/judge0/clients.py +++ b/src/judge0/clients.py @@ -10,6 +10,24 @@ class Client: + """Base class for all clients. + + Parameters + ---------- + endpoint : str + Client's default endpoint. + auth_headers : dict + Request authentication headers. + + Attributes + ---------- + API_KEY_ENV : str + Environment variable where judge0-python should look for API key for + the client. Set to default values for ATD, RapidAPI, and Sulu clients. + """ + + # Environment variable where judge0-python should look for API key for + # the client. Set to default values for ATD, RapidAPI, and Sulu clients. API_KEY_ENV: ClassVar[str] = None def __init__( @@ -24,7 +42,6 @@ def __init__( self.retry_strategy = retry_strategy self.session = requests.Session() - # TODO: Should be handled differently. try: self.languages = self.get_languages() self.config = self.get_config_info() @@ -39,6 +56,13 @@ def __del__(self): @handle_too_many_requests_error_for_preview_client def get_about(self) -> dict: + """Get general information about judge0. + + Returns + ------- + dict + General information about judge0. + """ response = self.session.get( f"{self.endpoint}/about", headers=self.auth_headers, @@ -48,6 +72,13 @@ def get_about(self) -> dict: @handle_too_many_requests_error_for_preview_client def get_config_info(self) -> Config: + """Get information about client's configuration. + + Returns + ------- + Config + Client's configuration. + """ response = self.session.get( f"{self.endpoint}/config_info", headers=self.auth_headers, @@ -57,6 +88,18 @@ def get_config_info(self) -> Config: @handle_too_many_requests_error_for_preview_client def get_language(self, language_id: int) -> Language: + """Get language corresponding to the id. + + Parameters + ---------- + language_id : int + Language id. + + Returns + ------- + Language + Language corresponding to the passed id. + """ request_url = f"{self.endpoint}/languages/{language_id}" response = self.session.get(request_url, headers=self.auth_headers) response.raise_for_status() @@ -64,6 +107,13 @@ def get_language(self, language_id: int) -> Language: @handle_too_many_requests_error_for_preview_client def get_languages(self) -> list[Language]: + """Get a list of supported languages. + + Returns + ------- + list of language + A list of supported languages. + """ request_url = f"{self.endpoint}/languages" response = self.session.get(request_url, headers=self.auth_headers) response.raise_for_status() @@ -71,6 +121,13 @@ def get_languages(self) -> list[Language]: @handle_too_many_requests_error_for_preview_client def get_statuses(self) -> list[dict]: + """Get a list of possible submission statuses. + + Returns + ------- + list of dict + A list of possible submission statues. + """ response = self.session.get( f"{self.endpoint}/statuses", headers=self.auth_headers, @@ -80,20 +137,43 @@ def get_statuses(self) -> list[dict]: @property def version(self): + """Property corresponding to the current client's version.""" if not hasattr(self, "_version"): _version = self.get_about()["version"] setattr(self, "_version", _version) return self._version def get_language_id(self, language: Union[LanguageAlias, int]) -> int: - """Get language id corresponding to the language alias for the client.""" + """Get language id corresponding to the language alias for the client. + + Parameters + ---------- + language : LanguageAlias or int + Language alias or language id. + + Returns + ------- + Language id corresponding to the language alias. + """ if isinstance(language, LanguageAlias): supported_language_ids = LANGUAGE_TO_LANGUAGE_ID[self.version] language = supported_language_ids.get(language, -1) return language def is_language_supported(self, language: Union[LanguageAlias, int]) -> bool: - """Check if language is supported by the client.""" + """Check if language is supported by the client. + + Parameters + ---------- + language : LanguageAlias or int + Language alias or language id. + + Returns + ------- + bool + Return True if language is supported by the client, otherwise returns + False. + """ language_id = self.get_language_id(language) return any(language_id == lang.id for lang in self.languages) @@ -208,9 +288,6 @@ 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] response = self.session.post( @@ -516,7 +593,15 @@ def __init__(self, api_key, **kwargs): class Sulu(Client): - """Base class for all Sulu clients.""" + """Base class for all Sulu clients. + + Parameters + ---------- + endpoint : str + Default request endpoint. + api_key : str, optional + Sulu API key. + """ API_KEY_ENV: ClassVar[str] = "JUDGE0_SULU_API_KEY" @@ -530,7 +615,13 @@ def __init__(self, endpoint, api_key=None, **kwargs): class SuluJudge0CE(Sulu): - """Sulu client for CE flavor.""" + """Sulu client for CE flavor. + + Parameters + ---------- + api_key : str, optional + Sulu API key. + """ DEFAULT_ENDPOINT: ClassVar[str] = "https://judge0-ce.p.sulu.sh" HOME_URL: ClassVar[str] = "https://sparkhub.sulu.sh/apis/judge0/judge0-ce/readme" @@ -544,7 +635,13 @@ def __init__(self, api_key=None, **kwargs): class SuluJudge0ExtraCE(Sulu): - """Sulu client for Extra CE flavor.""" + """Sulu client for Extra CE flavor. + + Parameters + ---------- + api_key : str + Sulu API key. + """ DEFAULT_ENDPOINT: ClassVar[str] = "https://judge0-extra-ce.p.sulu.sh" HOME_URL: ClassVar[str] = ( From 73fc1f52f8d91d7d8ad97366390477e21f09e3de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Tue, 24 Dec 2024 09:26:59 +0100 Subject: [PATCH 068/109] Update submission and api docs. --- docs/source/api/api.rst | 2 +- docs/source/api/submission.rst | 2 ++ src/judge0/api.py | 5 +++++ src/judge0/submission.py | 4 ++-- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/docs/source/api/api.rst b/docs/source/api/api.rst index 08b5d0e..e5b41b5 100644 --- a/docs/source/api/api.rst +++ b/docs/source/api/api.rst @@ -3,4 +3,4 @@ API Module .. automodule:: judge0.api :members: - :undoc-members: + :exclude-members: sync_run, async_run diff --git a/docs/source/api/submission.rst b/docs/source/api/submission.rst index e42a6aa..4f9977a 100644 --- a/docs/source/api/submission.rst +++ b/docs/source/api/submission.rst @@ -3,4 +3,6 @@ Submission Module .. automodule:: judge0.submission :members: + :exclude-members: process_encoded_fields, process_language, process_post_execution_filesystem, + process_status :member-order: groupwise diff --git a/src/judge0/api.py b/src/judge0/api.py index e254dd9..92b91b1 100644 --- a/src/judge0/api.py +++ b/src/judge0/api.py @@ -188,6 +188,11 @@ def wait( retry_strategy : RetryStrategy, optional A retry strategy. + Returns + ------- + Submission or Submissions + A single submission or a list of submissions. + Raises ------ ClientResolutionError diff --git a/src/judge0/submission.py b/src/judge0/submission.py index b9d474c..10863c8 100644 --- a/src/judge0/submission.py +++ b/src/judge0/submission.py @@ -208,7 +208,7 @@ def process_language( return value def set_attributes(self, attributes: dict[str, Any]) -> None: - """Set Submissions attributes while taking into account different + """Set submissions attributes while taking into account different attribute's types. Parameters @@ -236,7 +236,7 @@ def set_attributes(self, attributes: dict[str, Any]) -> None: def as_body(self, client: "Client") -> dict: """Prepare Submission as a dictionary while taking into account - the `client`'s restrictions. + the client's restrictions. """ body = { "source_code": encode(self.source_code), From f0b5d2b49dcde8371a1271960f77a2ab22c6eded Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Tue, 24 Dec 2024 09:37:28 +0100 Subject: [PATCH 069/109] Update test workflow to be included for PR's as well. --- .github/workflows/test.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 38c14e8..3c33777 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,6 +4,9 @@ on: push: branches: ["master"] paths: ["src/**", "tests/**"] + pull_request: + branches: ["master"] + paths: ["src/**", "tests/**"] permissions: contents: read From 78277eaf9dc90ae41af3ade1b42de8efb104289e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Herman=20Zvonimir=20Do=C5=A1ilovi=C4=87?= Date: Tue, 24 Dec 2024 10:08:51 +0100 Subject: [PATCH 070/109] Add all LanguageAliases. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add all language aliases * Add all language alliases to dunder init. --------- Co-authored-by: Filip Karlo Došilović --- pyproject.toml | 12 ++- src/judge0/__init__.py | 69 +++++++++++++++-- src/judge0/base_types.py | 76 +++++++++++++++++-- src/judge0/data.py | 156 ++++++++++++++++++++++++++++++++++++--- 4 files changed, 288 insertions(+), 25 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3568054..257ab80 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,5 +49,15 @@ docs = ["sphinx==7.4.7"] [tool.flake8] docstring-convention = "numpy" -extend-ignore = ["D205", "D400", "D105", "D100", "D101", "D102", "D103", "F821"] +extend-ignore = [ + 'D100', + 'D101', + 'D102', + 'D103', + 'D104', + 'D105', + 'D205', + 'D400', + 'F821', +] max-line-length = 88 diff --git a/src/judge0/__init__.py b/src/judge0/__init__.py index 5ccf40b..df0f78a 100644 --- a/src/judge0/__init__.py +++ b/src/judge0/__init__.py @@ -113,11 +113,70 @@ 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 +ASSEMBLY = LanguageAlias.ASSEMBLY +BASH = LanguageAlias.BASH +BASIC = LanguageAlias.BASIC +BOSQUE = LanguageAlias.BOSQUE +C = LanguageAlias.C +C3 = LanguageAlias.C3 +CLOJURE = LanguageAlias.CLOJURE +COBOL = LanguageAlias.COBOL +COMMON_LISP = LanguageAlias.COMMON_LISP CPP = LanguageAlias.CPP -JAVA = LanguageAlias.JAVA -CPP_GCC = LanguageAlias.CPP_GCC CPP_CLANG = LanguageAlias.CPP_CLANG +CPP_GCC = LanguageAlias.CPP_GCC +CPP_TEST = LanguageAlias.CPP_TEST +CPP_TEST_CLANG = LanguageAlias.CPP_TEST_CLANG +CPP_TEST_GCC = LanguageAlias.CPP_TEST_GCC +CSHARP = LanguageAlias.CSHARP +CSHARP_DOTNET = LanguageAlias.CSHARP_DOTNET +CSHARP_MONO = LanguageAlias.CSHARP_MONO +CSHARP_TEST = LanguageAlias.CSHARP_TEST +C_CLANG = LanguageAlias.C_CLANG +C_GCC = LanguageAlias.C_GCC +D = LanguageAlias.D +DART = LanguageAlias.DART +ELIXIR = LanguageAlias.ELIXIR +ERLANG = LanguageAlias.ERLANG +EXECUTABLE = LanguageAlias.EXECUTABLE +FORTRAN = LanguageAlias.FORTRAN +FSHARP = LanguageAlias.FSHARP +GO = LanguageAlias.GO +GROOVY = LanguageAlias.GROOVY +HASKELL = LanguageAlias.HASKELL +JAVA = LanguageAlias.JAVA +JAVAFX = LanguageAlias.JAVAFX +JAVASCRIPT = LanguageAlias.JAVASCRIPT +JAVA_JDK = LanguageAlias.JAVA_JDK +JAVA_OPENJDK = LanguageAlias.JAVA_OPENJDK +JAVA_TEST = LanguageAlias.JAVA_TEST +KOTLIN = LanguageAlias.KOTLIN +LUA = LanguageAlias.LUA +MPI_C = LanguageAlias.MPI_C +MPI_CPP = LanguageAlias.MPI_CPP +MPI_PYTHON = LanguageAlias.MPI_PYTHON +MULTI_FILE = LanguageAlias.MULTI_FILE +NIM = LanguageAlias.NIM +OBJECTIVE_C = LanguageAlias.OBJECTIVE_C +OCAML = LanguageAlias.OCAML +OCTAVE = LanguageAlias.OCTAVE +PASCAL = LanguageAlias.PASCAL +PERL = LanguageAlias.PERL +PHP = LanguageAlias.PHP +PLAIN_TEXT = LanguageAlias.PLAIN_TEXT +PROLOG = LanguageAlias.PROLOG +PYTHON = LanguageAlias.PYTHON +PYTHON2 = LanguageAlias.PYTHON2 +PYTHON2_PYPY = LanguageAlias.PYTHON2_PYPY +PYTHON3 = LanguageAlias.PYTHON3 +PYTHON3_PYPY = LanguageAlias.PYTHON3_PYPY PYTHON_FOR_ML = LanguageAlias.PYTHON_FOR_ML +PYTHON_PYPY = LanguageAlias.PYTHON_PYPY +R = LanguageAlias.R +RUBY = LanguageAlias.RUBY +RUST = LanguageAlias.RUST +SCALA = LanguageAlias.SCALA +SQLITE = LanguageAlias.SQLITE +SWIFT = LanguageAlias.SWIFT +TYPESCRIPT = LanguageAlias.TYPESCRIPT +VISUAL_BASIC = LanguageAlias.VISUAL_BASIC diff --git a/src/judge0/base_types.py b/src/judge0/base_types.py index 05a7a64..8b892ba 100644 --- a/src/judge0/base_types.py +++ b/src/judge0/base_types.py @@ -1,7 +1,7 @@ import copy from dataclasses import dataclass -from enum import IntEnum +from enum import IntEnum, auto from typing import Optional, Protocol, runtime_checkable, Sequence, Union from pydantic import BaseModel @@ -59,13 +59,73 @@ class Language(BaseModel): class LanguageAlias(IntEnum): """Language enumeration.""" - - PYTHON = 0 - CPP = 1 - JAVA = 2 - CPP_GCC = 3 - CPP_CLANG = 4 - PYTHON_FOR_ML = 5 + ASSEMBLY = auto() + BASH = auto() + BASIC = auto() + BOSQUE = auto() + C = auto() + C3 = auto() + CLOJURE = auto() + COBOL = auto() + COMMON_LISP = auto() + CPP = auto() + CPP_CLANG = auto() + CPP_GCC = auto() + CPP_TEST = auto() + CPP_TEST_CLANG = auto() + CPP_TEST_GCC = auto() + CSHARP = auto() + CSHARP_DOTNET = auto() + CSHARP_MONO = auto() + CSHARP_TEST = auto() + C_CLANG = auto() + C_GCC = auto() + D = auto() + DART = auto() + ELIXIR = auto() + ERLANG = auto() + EXECUTABLE = auto() + FORTRAN = auto() + FSHARP = auto() + GO = auto() + GROOVY = auto() + HASKELL = auto() + JAVA = auto() + JAVAFX = auto() + JAVASCRIPT = auto() + JAVA_JDK = auto() + JAVA_OPENJDK = auto() + JAVA_TEST = auto() + KOTLIN = auto() + LUA = auto() + MPI_C = auto() + MPI_CPP = auto() + MPI_PYTHON = auto() + MULTI_FILE = auto() + NIM = auto() + OBJECTIVE_C = auto() + OCAML = auto() + OCTAVE = auto() + PASCAL = auto() + PERL = auto() + PHP = auto() + PLAIN_TEXT = auto() + PROLOG = auto() + PYTHON = auto() + PYTHON2 = auto() + PYTHON2_PYPY = auto() + PYTHON3 = auto() + PYTHON3_PYPY = auto() + PYTHON_FOR_ML = auto() + PYTHON_PYPY = auto() + R = auto() + RUBY = auto() + RUST = auto() + SCALA = auto() + SQLITE = auto() + SWIFT = auto() + TYPESCRIPT = auto() + VISUAL_BASIC = auto() class Flavor(IntEnum): diff --git a/src/judge0/data.py b/src/judge0/data.py index 1e759c2..39ad1b3 100644 --- a/src/judge0/data.py +++ b/src/judge0/data.py @@ -2,31 +2,165 @@ LANGUAGE_TO_LANGUAGE_ID = { "1.13.1": { - LanguageAlias.PYTHON: 71, - LanguageAlias.CPP: 54, - LanguageAlias.JAVA: 62, - LanguageAlias.CPP_GCC: 54, + LanguageAlias.ASSEMBLY: 45, + LanguageAlias.BASH: 46, + LanguageAlias.BASIC: 47, + LanguageAlias.C: 50, + LanguageAlias.CLOJURE: 86, + LanguageAlias.COBOL: 77, + LanguageAlias.COMMON_LISP: 55, + LanguageAlias.CPP: 52, LanguageAlias.CPP_CLANG: 76, + LanguageAlias.CPP_GCC: 52, + LanguageAlias.CSHARP: 51, + LanguageAlias.CSHARP_MONO: 51, + LanguageAlias.C_CLANG: 75, + LanguageAlias.C_GCC: 50, + LanguageAlias.D: 56, + LanguageAlias.ELIXIR: 57, + LanguageAlias.ERLANG: 58, + LanguageAlias.EXECUTABLE: 44, + LanguageAlias.FORTRAN: 59, + LanguageAlias.FSHARP: 87, + LanguageAlias.GO: 60, + LanguageAlias.GROOVY: 88, + LanguageAlias.HASKELL: 61, + LanguageAlias.JAVA: 62, + LanguageAlias.JAVASCRIPT: 63, + LanguageAlias.JAVA_OPENJDK: 62, + LanguageAlias.KOTLIN: 78, + LanguageAlias.LUA: 64, + LanguageAlias.MULTI_FILE: 89, + LanguageAlias.OBJECTIVE_C: 79, + LanguageAlias.OCAML: 65, + LanguageAlias.OCTAVE: 66, + LanguageAlias.PASCAL: 67, + LanguageAlias.PERL: 85, + LanguageAlias.PHP: 68, + LanguageAlias.PLAIN_TEXT: 43, + LanguageAlias.PROLOG: 69, + LanguageAlias.PYTHON: 71, + LanguageAlias.PYTHON2: 70, + LanguageAlias.PYTHON3: 71, + LanguageAlias.R: 80, + LanguageAlias.RUBY: 72, + LanguageAlias.RUST: 73, + LanguageAlias.SCALA: 81, + LanguageAlias.SQLITE: 82, + LanguageAlias.SWIFT: 83, + LanguageAlias.TYPESCRIPT: 74, + LanguageAlias.VISUAL_BASIC: 84, }, "1.13.1-extra": { - LanguageAlias.PYTHON: 10, + LanguageAlias.BOSQUE: 11, + LanguageAlias.C: 1, + LanguageAlias.C3: 3, LanguageAlias.CPP: 2, - LanguageAlias.JAVA: 4, LanguageAlias.CPP_CLANG: 2, + LanguageAlias.CPP_TEST: 12, + LanguageAlias.CPP_TEST_CLANG: 15, + LanguageAlias.CPP_TEST_GCC: 12, + LanguageAlias.CSHARP: 22, + LanguageAlias.CSHARP_MONO: 22, + LanguageAlias.CSHARP_DOTNET: 21, + LanguageAlias.CSHARP_TEST: 23, + LanguageAlias.C_CLANG: 1, + LanguageAlias.FSHARP: 24, + LanguageAlias.JAVA: 4, + LanguageAlias.JAVA_OPENJDK: 4, + LanguageAlias.JAVA_TEST: 5, + LanguageAlias.MPI_C: 6, + LanguageAlias.MPI_CPP: 7, + LanguageAlias.MPI_PYTHON: 8, + LanguageAlias.MULTI_FILE: 89, + LanguageAlias.NIM: 9, + LanguageAlias.PYTHON: 10, + LanguageAlias.PYTHON3: 10, LanguageAlias.PYTHON_FOR_ML: 10, + LanguageAlias.VISUAL_BASIC: 20, }, "1.14.0": { - LanguageAlias.PYTHON: 100, + LanguageAlias.ASSEMBLY: 45, + LanguageAlias.BASH: 46, + LanguageAlias.BASIC: 47, + LanguageAlias.C: 103, + LanguageAlias.CLOJURE: 86, + LanguageAlias.COBOL: 77, + LanguageAlias.COMMON_LISP: 55, LanguageAlias.CPP: 105, - LanguageAlias.JAVA: 91, - LanguageAlias.CPP_GCC: 105, LanguageAlias.CPP_CLANG: 76, + LanguageAlias.CPP_GCC: 105, + LanguageAlias.CSHARP: 51, + LanguageAlias.CSHARP_MONO: 51, + LanguageAlias.C_CLANG: 104, + LanguageAlias.C_GCC: 103, + LanguageAlias.D: 56, + LanguageAlias.DART: 90, + LanguageAlias.ELIXIR: 57, + LanguageAlias.ERLANG: 58, + LanguageAlias.EXECUTABLE: 44, + LanguageAlias.FORTRAN: 59, + LanguageAlias.FSHARP: 87, + LanguageAlias.GO: 95, + LanguageAlias.GROOVY: 88, + LanguageAlias.HASKELL: 61, + LanguageAlias.JAVA: 62, + LanguageAlias.JAVAFX: 96, + LanguageAlias.JAVASCRIPT: 102, + LanguageAlias.JAVA_JDK: 91, + LanguageAlias.JAVA_OPENJDK: 62, + LanguageAlias.KOTLIN: 78, + LanguageAlias.LUA: 64, + LanguageAlias.MULTI_FILE: 89, + LanguageAlias.OBJECTIVE_C: 79, + LanguageAlias.OCAML: 65, + LanguageAlias.OCTAVE: 66, + LanguageAlias.PASCAL: 67, + LanguageAlias.PERL: 85, + LanguageAlias.PHP: 98, + LanguageAlias.PLAIN_TEXT: 43, + LanguageAlias.PROLOG: 69, + LanguageAlias.PYTHON: 100, + LanguageAlias.PYTHON2: 70, + LanguageAlias.PYTHON3: 100, + LanguageAlias.R: 99, + LanguageAlias.RUBY: 72, + LanguageAlias.RUST: 73, + LanguageAlias.SCALA: 81, + LanguageAlias.SQLITE: 82, + LanguageAlias.SWIFT: 83, + LanguageAlias.TYPESCRIPT: 101, + LanguageAlias.VISUAL_BASIC: 84, }, "1.14.0-extra": { - LanguageAlias.PYTHON: 25, + LanguageAlias.BOSQUE: 11, + LanguageAlias.C: 1, + LanguageAlias.C3: 3, LanguageAlias.CPP: 2, - LanguageAlias.JAVA: 4, LanguageAlias.CPP_CLANG: 2, + LanguageAlias.CPP_TEST: 12, + LanguageAlias.CPP_TEST_CLANG: 15, + LanguageAlias.CPP_TEST_GCC: 12, + LanguageAlias.CSHARP: 29, + LanguageAlias.CSHARP_MONO: 22, + LanguageAlias.CSHARP_DOTNET: 29, + LanguageAlias.CSHARP_TEST: 23, + LanguageAlias.C_CLANG: 1, + LanguageAlias.FSHARP: 24, + LanguageAlias.JAVA: 4, + LanguageAlias.JAVA_OPENJDK: 4, + LanguageAlias.JAVA_TEST: 5, + LanguageAlias.MPI_C: 6, + LanguageAlias.MPI_CPP: 7, + LanguageAlias.MPI_PYTHON: 8, + LanguageAlias.MULTI_FILE: 89, + LanguageAlias.NIM: 9, + LanguageAlias.PYTHON: 25, + LanguageAlias.PYTHON2: 26, + LanguageAlias.PYTHON2_PYPY: 26, + LanguageAlias.PYTHON3: 25, + LanguageAlias.PYTHON3_PYPY: 28, LanguageAlias.PYTHON_FOR_ML: 25, + LanguageAlias.VISUAL_BASIC: 20, }, } From b28c1d64d983e23f71d9ca3a6dc4e9a23f3daff4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Herman=20Zvonimir=20Do=C5=A1ilovi=C4=87?= Date: Tue, 24 Dec 2024 15:16:25 +0100 Subject: [PATCH 071/109] Add source_code as bytes --- examples/0006_exe.py | 9 +++++++++ src/judge0/submission.py | 2 +- tests/test_submission.py | 18 ++++++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 examples/0006_exe.py diff --git a/examples/0006_exe.py b/examples/0006_exe.py new file mode 100644 index 0000000..81d4ada --- /dev/null +++ b/examples/0006_exe.py @@ -0,0 +1,9 @@ +from base64 import b64decode + +import judge0 + +source_code = b64decode( + "f0VMRgIBAQAAAAAAAAAAAAIAPgABAAAAAABAAAAAAABAAAAAAAAAAEAQAAAAAAAAAAAAAEAAOAABAEAABAADAAEAAAAFAAAAABAAAAAAAAAAAEAAAAAAAAAAQAAAAAAAJQAAAAAAAAAlAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADHAjVANsAG+GABAAInHDwUx/41HPA8FAGhlbGxvLCB3b3JsZAoALnNoc3RydGFiAC50ZXh0AC5yb2RhdGEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAEAAAAGAAAAAAAAAAAAQAAAAAAAABAAAAAAAAAXAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABEAAAABAAAAAgAAAAAAAAAYAEAAAAAAABgQAAAAAAAADQAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAABAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAlEAAAAAAAABkAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAA" # noqa: E501 +) +result = judge0.run(source_code=source_code, language=judge0.EXECUTABLE) +print(result.stdout) diff --git a/src/judge0/submission.py b/src/judge0/submission.py index 10863c8..55b7660 100644 --- a/src/judge0/submission.py +++ b/src/judge0/submission.py @@ -126,7 +126,7 @@ class Submission(BaseModel): URL for a callback to report execution results or status. """ - source_code: Optional[str] = Field(default=None, repr=True) + source_code: Optional[Union[str, bytes]] = Field(default=None, repr=True) language: Union[LanguageAlias, int] = Field( default=LanguageAlias.PYTHON, repr=True, diff --git a/tests/test_submission.py b/tests/test_submission.py index fb1bf73..ddae140 100644 --- a/tests/test_submission.py +++ b/tests/test_submission.py @@ -1,3 +1,5 @@ +from base64 import b64decode + from judge0 import run, Status, Submission, wait from judge0.base_types import LanguageAlias @@ -92,3 +94,19 @@ def test_language_before_and_after_execution(request): assert submission.language == LanguageAlias.JAVA submission = run(client=client, submissions=submission) assert submission.language == LanguageAlias.JAVA + + +def test_language_executable(request): + client = request.getfixturevalue("judge0_ce_client") + code = b64decode( + "f0VMRgIBAQAAAAAAAAAAAAIAPgABAAAAAABAAAAAAABAAAAAAAAAAEAQAAAAAAAAAAAAAEAAOAABAEAABAADAAEAAAAFAAAAABAAAAAAAAAAAEAAAAAAAAAAQAAAAAAAJQAAAAAAAAAlAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADHAjVANsAG+GABAAInHDwUx/41HPA8FAGhlbGxvLCB3b3JsZAoALnNoc3RydGFiAC50ZXh0AC5yb2RhdGEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAEAAAAGAAAAAAAAAAAAQAAAAAAAABAAAAAAAAAXAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABEAAAABAAAAAgAAAAAAAAAYAEAAAAAAABgQAAAAAAAADQAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAABAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAlEAAAAAAAABkAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAA" # noqa: E501 + ) + submission = Submission( + source_code=code, + language=LanguageAlias.EXECUTABLE, + ) + + assert submission.language == LanguageAlias.EXECUTABLE + submission = run(client=client, submissions=submission) + assert submission.language == LanguageAlias.EXECUTABLE + assert submission.stdout == "hello, world\n" From aa1d8123822ecb41c9df864e61a94355325bf949 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Herman=20Zvonimir=20Do=C5=A1ilovi=C4=87?= Date: Tue, 24 Dec 2024 15:21:44 +0100 Subject: [PATCH 072/109] Prepare 0.0.3 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 257ab80..212569c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "judge0" -version = "0.0.2" +version = "0.0.3" description = "The official Python library for Judge0." readme = "README.md" requires-python = ">=3.9" From 1e1af05530ad89f1f9e20b3d499cdc361b684f07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Herman=20Zvonimir=20Do=C5=A1ilovi=C4=87?= Date: Tue, 24 Dec 2024 15:29:25 +0100 Subject: [PATCH 073/109] Update pyproject description --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 212569c..18592ef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "judge0" -version = "0.0.3" -description = "The official Python library for Judge0." +version = "0.0.4-dev" +description = "The official Python SDK for Judge0." readme = "README.md" requires-python = ">=3.9" authors = [{ name = "Judge0", email = "contact@judge0.com" }] From 79aac51bd56aec693206a4c630b1441a214ffb33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Thu, 26 Dec 2024 00:04:34 +0100 Subject: [PATCH 074/109] Add .python-version to gitignore. --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 82f9275..1348066 100644 --- a/.gitignore +++ b/.gitignore @@ -85,7 +85,7 @@ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: -# .python-version +.python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. From 49f61a8d85224f5b7f68dfc4979379844a3ed54d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Sat, 28 Dec 2024 18:47:43 +0100 Subject: [PATCH 075/109] Add pygments style to docs conf. --- docs/source/conf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/conf.py b/docs/source/conf.py index a9ae07c..49c7ab8 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -41,6 +41,7 @@ "versioning.html", ], } +pygments_style = "sphinx" sys.path.insert(0, os.path.abspath("../../src/")) # Adjust as needed From f87358f99b40aa258a0926d7952ff0099cba802d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Sat, 28 Dec 2024 18:50:58 +0100 Subject: [PATCH 076/109] Change permalinks in docs headers. --- docs/source/conf.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/source/conf.py b/docs/source/conf.py index 49c7ab8..83e54e4 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -10,6 +10,8 @@ import os import sys +from sphinxawesome_theme.postprocess import Icons + project = "Judge0 Python SDK" copyright = "2024, Judge0" author = "Judge0" @@ -45,6 +47,8 @@ sys.path.insert(0, os.path.abspath("../../src/")) # Adjust as needed +# -- Awesome theme config -- +html_permalinks_icon = Icons.permalinks_icon autodoc_default_options = { "members": True, From dac233da696304b9125dd1811e308fea618a8d98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Sat, 28 Dec 2024 19:16:04 +0100 Subject: [PATCH 077/109] Add reverse version sorting. --- docs/source/_templates/versioning.html | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/source/_templates/versioning.html b/docs/source/_templates/versioning.html index ef74ed4..1b8de30 100644 --- a/docs/source/_templates/versioning.html +++ b/docs/source/_templates/versioning.html @@ -1,7 +1,10 @@ {% if versions %} +{% set master_version = versions | selectattr('name', 'equalto', 'master') | list %} +{% set other_versions = versions | rejectattr('name', 'equalto', 'master') | sort(attribute='name', reverse=true) %} +{% set sorted_versions = master_version + other_versions %}

{{ _('Versions') }}

    - {%- for item in versions %} + {%- for item in sorted_versions %}
  • {{ item.name }}
  • {%- endfor %}
From 97aa8e30337bcee1ebccde395b5fc447c1a21b90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Sat, 28 Dec 2024 19:41:50 +0100 Subject: [PATCH 078/109] Update workflow to point the index to latest release. --- .github/workflows/docs.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 4b2aa46..6a6a8e9 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -34,12 +34,22 @@ jobs: - name: Build documentation run: sphinx-multiversion docs/source docs/build/html --keep-going --no-color + - name: Get the latest tag + run: | + # Fetch all tags + git fetch --tags + # Get the latest tag + latest_tag=$(git tag --sort=-creatordate | head -n 1) + # Remove the 'v' prefix if it exists + latest_tag_no_v=${latest_tag#v} + echo "LATEST_RELEASE=$latest_tag_no_v" >> $GITHUB_ENV + - name: Generate index.html for judge0.github.io/judge0-python. run: | echo ' - + ' > docs/build/html/index.html From dfaf7d55efe3cf6cc6a66cefdad9bcc703cc9e8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Sat, 28 Dec 2024 19:48:14 +0100 Subject: [PATCH 079/109] Update with explicitly set env variable in the step. --- .github/workflows/docs.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 6a6a8e9..11f7c0c 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -49,9 +49,11 @@ jobs: echo ' - + ' > docs/build/html/index.html + env: + latest_release: ${{ env.LATEST_RELEASE }} - name: Upload artifacts uses: actions/upload-artifact@v4 From 6c3b8b77a15fcefa8cfecf1712e850892a5ed89c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Sat, 28 Dec 2024 19:49:37 +0100 Subject: [PATCH 080/109] Fix unexpected EOF. --- .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 11f7c0c..1c95467 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -49,7 +49,7 @@ jobs: echo ' - + ' > docs/build/html/index.html env: From 335ee1ba91f167eb4b6748dfa65d495d7d0ee2ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Sat, 28 Dec 2024 19:58:03 +0100 Subject: [PATCH 081/109] Don't remove the v from the tag when building index for docs. --- .github/workflows/docs.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 1c95467..4a70709 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -40,9 +40,7 @@ jobs: git fetch --tags # Get the latest tag latest_tag=$(git tag --sort=-creatordate | head -n 1) - # Remove the 'v' prefix if it exists - latest_tag_no_v=${latest_tag#v} - echo "LATEST_RELEASE=$latest_tag_no_v" >> $GITHUB_ENV + echo "LATEST_RELEASE=$latest_tag" >> $GITHUB_ENV - name: Generate index.html for judge0.github.io/judge0-python. run: | From 9a7519817d176dba6f5b49458d1ffa9395153d32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Mon, 30 Dec 2024 12:06:46 +0100 Subject: [PATCH 082/109] Add text to Getting Involved section on index page. --- docs/source/index.rst | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index 3510578..257efda 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -34,7 +34,20 @@ Examples. Getting Involved ================ -TODO +Getting involved in any open-source project is simple and rewarding, with +multiple ways to contribute to its growth and success. You can help by: + +1. `reporting bugs `_ by + creating a detailed issue describing the problem, along with any relevant code or + steps to reproduce it, so it can be addressed effectively, +2. creating a `pull request `_ for + an existing issue; we welcome improvements, fixes, and new features that align + with the project's goals, and +3. you can show support by starring the `repository `_, + letting us know that we’re doing a good job and helping us gain visibility within + the open-source community. + +Every contribution, big or small, is valuable! .. toctree:: :caption: API From 334d6fa2d1e92b2dd7f6309e78ce50a0d86feb85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Mon, 30 Dec 2024 12:44:10 +0100 Subject: [PATCH 083/109] Add retry module to docs. --- docs/source/api/retry.rst | 6 +++++ docs/source/index.rst | 1 + pyproject.toml | 4 +++- src/judge0/retry.py | 50 ++++++++++++++++++++++++++++++++++++--- 4 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 docs/source/api/retry.rst diff --git a/docs/source/api/retry.rst b/docs/source/api/retry.rst new file mode 100644 index 0000000..22977dc --- /dev/null +++ b/docs/source/api/retry.rst @@ -0,0 +1,6 @@ +Retry Module +============ + +.. automodule:: judge0.retry + :members: + :member-order: bysource \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst index 257efda..2c50fdf 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -59,6 +59,7 @@ Every contribution, big or small, is valuable! api/submission api/clients api/types + api/retry .. toctree:: :caption: Getting Involved diff --git a/pyproject.toml b/pyproject.toml index 18592ef..edfcfdd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,6 @@ test = [ docs = ["sphinx==7.4.7"] [tool.flake8] -docstring-convention = "numpy" extend-ignore = [ 'D100', 'D101', @@ -56,8 +55,11 @@ extend-ignore = [ 'D103', 'D104', 'D105', + 'D107', 'D205', + "D209", 'D400', 'F821', ] +docstring-convention = "numpy" max-line-length = 88 diff --git a/src/judge0/retry.py b/src/judge0/retry.py index 20b42ef..f4bff5b 100644 --- a/src/judge0/retry.py +++ b/src/judge0/retry.py @@ -3,62 +3,106 @@ class RetryStrategy(ABC): + """Abstract base class that defines the interface for any retry strategy. + + See :obj:`MaxRetries`, :obj:`MaxWaitTime`, and :obj:`RegularPeriodRetry` for + example implementations. + """ + @abstractmethod def is_done(self) -> bool: + """Check if the retry strategy has exhausted its retries.""" pass @abstractmethod def wait(self) -> None: + """Delay implementation before the next retry attempt.""" pass + @abstractmethod def step(self) -> None: + """Update internal attributes of the retry strategy.""" pass class MaxRetries(RetryStrategy): """Check for submissions status every 100 ms and retry a maximum of - `max_retries` times.""" + `max_retries` times. + + Parameters + ---------- + max_retries : int + Max number of retries. + """ def __init__(self, max_retries: int = 20): + if max_retries < 1: + raise ValueError("max_retries must be at least 1.") self.n_retries = 0 self.max_retries = max_retries def step(self): + """Increment the number of retries by one.""" self.n_retries += 1 def wait(self): + """Wait for 0.1 seconds between retries.""" time.sleep(0.1) def is_done(self) -> bool: + """Check if the number of retries is bigger or equal to specified + maximum number of retries.""" return self.n_retries >= self.max_retries class MaxWaitTime(RetryStrategy): """Check for submissions status every 100 ms and wait for all submissions - a maximum of `max_wait_time` (seconds).""" + a maximum of `max_wait_time` (seconds). + + Parameters + ---------- + max_wait_time_sec : float + Maximum waiting time (in seconds). + """ def __init__(self, max_wait_time_sec: float = 5 * 60): self.max_wait_time_sec = max_wait_time_sec self.total_wait_time = 0 def step(self): + """Add 0.1 seconds to total waiting time.""" self.total_wait_time += 0.1 def wait(self): + """Wait (sleep) for 0.1 seconds.""" time.sleep(0.1) def is_done(self): + """Check if the total waiting time is bigger or equal to the specified + maximum waiting time.""" return self.total_wait_time >= self.max_wait_time_sec class RegularPeriodRetry(RetryStrategy): - """Check for submissions status periodically for indefinite amount of time.""" + """Check for submissions status periodically for indefinite amount of time. + + Parameters + ---------- + wait_time_sec : float + Wait time between retries (in seconds). + """ def __init__(self, wait_time_sec: float = 0.1): self.wait_time_sec = wait_time_sec def wait(self): + """Wait for `wait_time_sec` seconds.""" time.sleep(self.wait_time_sec) def is_done(self) -> bool: + """Return False, as this retry strategy is indefinite.""" return False + + def step(self) -> None: + """Satisfy the interface with a dummy implementation.""" + pass From c498a23f7cf371fbe95e664e60191a3ad3b35687 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Mon, 30 Dec 2024 12:49:20 +0100 Subject: [PATCH 084/109] Add errors module to docs. Add docstrings in common module. --- docs/source/api/errors.rst | 5 +++++ docs/source/index.rst | 1 + src/judge0/common.py | 2 ++ 3 files changed, 8 insertions(+) create mode 100644 docs/source/api/errors.rst diff --git a/docs/source/api/errors.rst b/docs/source/api/errors.rst new file mode 100644 index 0000000..b976cd1 --- /dev/null +++ b/docs/source/api/errors.rst @@ -0,0 +1,5 @@ +Errors Module +============= + +.. automodule:: judge0.errors + :members: diff --git a/docs/source/index.rst b/docs/source/index.rst index 2c50fdf..6cb851f 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -60,6 +60,7 @@ Every contribution, big or small, is valuable! api/clients api/types api/retry + api/errors .. toctree:: :caption: Getting Involved diff --git a/src/judge0/common.py b/src/judge0/common.py index 57ad838..c25ee54 100644 --- a/src/judge0/common.py +++ b/src/judge0/common.py @@ -6,6 +6,7 @@ def encode(content: Union[bytes, str, Encodeable]) -> str: + """Encode content to base64 string.""" if isinstance(content, bytes): return b64encode(content).decode() if isinstance(content, str): @@ -16,6 +17,7 @@ def encode(content: Union[bytes, str, Encodeable]) -> str: def decode(content: Union[bytes, str]) -> str: + """Decode base64 encoded content.""" if isinstance(content, bytes): return b64decode( content.decode(errors="backslashreplace"), validate=True From b97d17141c8fdf7abf595bb6194dd39c67d42a36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Mon, 30 Dec 2024 12:55:19 +0100 Subject: [PATCH 085/109] Add version to dunder init. Update version in pyproject toml. --- pyproject.toml | 2 +- src/judge0/__init__.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index edfcfdd..4974a30 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "judge0" -version = "0.0.4-dev" +version = "0.1.0.dev0" description = "The official Python SDK for Judge0." readme = "README.md" requires-python = ">=3.9" diff --git a/src/judge0/__init__.py b/src/judge0/__init__.py index df0f78a..391e926 100644 --- a/src/judge0/__init__.py +++ b/src/judge0/__init__.py @@ -27,6 +27,8 @@ from .retry import MaxRetries, MaxWaitTime, RegularPeriodRetry from .submission import Submission +__version__ = "0.1.0.dev0" + __all__ = [ "ATD", "ATDJudge0CE", From 4b88795a071cd94c5e9fcb01a5a7304323d137c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Mon, 30 Dec 2024 13:18:34 +0100 Subject: [PATCH 086/109] Update base types docs. Add scroll to top button in docs. --- docs/source/api/types.rst | 39 ++++++++++++++++++++++++++++++++++++++- docs/source/conf.py | 3 +++ src/judge0/base_types.py | 20 +++++++++++++++++--- src/judge0/common.py | 6 +++--- 4 files changed, 61 insertions(+), 7 deletions(-) diff --git a/docs/source/api/types.rst b/docs/source/api/types.rst index 219d7ed..bf0f62a 100644 --- a/docs/source/api/types.rst +++ b/docs/source/api/types.rst @@ -1,6 +1,43 @@ Types Module ============ -.. automodule:: judge0.base_types +Types +----- + +.. autoclass:: judge0.base_types.Config + :members: + :member-order: bysource + +.. autoclass:: judge0.base_types.Encodable + :members: + +.. autoclass:: judge0.base_types.Flavor + :members: + :member-order: bysource + +.. autoclass:: judge0.base_types.Language + :members: + :member-order: bysource + +.. autoclass:: judge0.base_types.LanguageAlias :members: + :member-order: bysource +.. autoclass:: judge0.base_types.Status + :members: + :member-order: bysource + +.. autoclass:: judge0.base_types.TestCase + :members: + :member-order: bysource + +Type aliases +------------ + +.. autoclass:: judge0.base_types.TestCaseType + :members: + :member-order: bysource + +.. autoclass:: judge0.base_types.TestCases + :members: + :member-order: bysource \ No newline at end of file diff --git a/docs/source/conf.py b/docs/source/conf.py index 83e54e4..697f9f5 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -35,6 +35,9 @@ # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output html_theme = "sphinxawesome_theme" +html_theme_options = { + "show_scrolltop": True, +} html_show_sphinx = False html_sidebars = { "**": [ diff --git a/src/judge0/base_types.py b/src/judge0/base_types.py index 8b892ba..5e7a57c 100644 --- a/src/judge0/base_types.py +++ b/src/judge0/base_types.py @@ -1,7 +1,7 @@ import copy from dataclasses import dataclass -from enum import IntEnum, auto +from enum import auto, IntEnum from typing import Optional, Protocol, runtime_checkable, Sequence, Union from pydantic import BaseModel @@ -14,6 +14,8 @@ @dataclass(frozen=True) class TestCase: + """Dataclass for test case.""" + input: Optional[str] = None expected_output: Optional[str] = None @@ -21,7 +23,18 @@ class TestCase: def from_record( cls, test_case: Union[TestCaseType, None] ) -> Union["TestCase", None]: - """Create a TestCase from built-in types.""" + """Create a TestCase from built-in types. + + Parameters + ---------- + test_case: :obj:`TestCaseType` or None + Test case data. + + Returns + ------- + TestCase or None + Created TestCase object or None if test_case is None. + """ if isinstance(test_case, (tuple, list)): test_case = { field: value @@ -42,7 +55,7 @@ def from_record( @runtime_checkable -class Encodeable(Protocol): +class Encodable(Protocol): def encode(self) -> bytes: """Serialize the object to bytes.""" ... @@ -59,6 +72,7 @@ class Language(BaseModel): class LanguageAlias(IntEnum): """Language enumeration.""" + ASSEMBLY = auto() BASH = auto() BASIC = auto() diff --git a/src/judge0/common.py b/src/judge0/common.py index c25ee54..e8ab58e 100644 --- a/src/judge0/common.py +++ b/src/judge0/common.py @@ -2,16 +2,16 @@ from itertools import islice from typing import Union -from judge0.base_types import Encodeable +from judge0.base_types import Encodable -def encode(content: Union[bytes, str, Encodeable]) -> str: +def encode(content: Union[bytes, str, Encodable]) -> str: """Encode content to base64 string.""" if isinstance(content, bytes): return b64encode(content).decode() if isinstance(content, str): return b64encode(content.encode()).decode() - if isinstance(content, Encodeable): + if isinstance(content, Encodable): return b64encode(content.encode()).decode() raise ValueError(f"Unsupported type. Expected bytes or str, got {type(content)}!") From 58194f3011d7ae914745cb4fbd9c8a4c766116fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Mon, 30 Dec 2024 13:27:10 +0100 Subject: [PATCH 087/109] Add filesystem to docs. --- docs/source/api/filesystem.rst | 6 ++++++ docs/source/api/types.rst | 4 ++++ docs/source/index.rst | 7 ++++--- src/judge0/filesystem.py | 20 ++++++++++++++++++++ 4 files changed, 34 insertions(+), 3 deletions(-) create mode 100644 docs/source/api/filesystem.rst diff --git a/docs/source/api/filesystem.rst b/docs/source/api/filesystem.rst new file mode 100644 index 0000000..73eafb6 --- /dev/null +++ b/docs/source/api/filesystem.rst @@ -0,0 +1,6 @@ +Filesystem Module +================= + +.. automodule:: judge0.filesystem + :members: + :member-order: bysource diff --git a/docs/source/api/types.rst b/docs/source/api/types.rst index bf0f62a..8cb94cc 100644 --- a/docs/source/api/types.rst +++ b/docs/source/api/types.rst @@ -34,6 +34,10 @@ Types Type aliases ------------ +.. autoclass:: judge0.base_types.Iterable + :members: + :member-order: bysource + .. autoclass:: judge0.base_types.TestCaseType :members: :member-order: bysource diff --git a/docs/source/index.rst b/docs/source/index.rst index 6cb851f..d31c601 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -56,11 +56,12 @@ Every contribution, big or small, is valuable! :hidden: api/api - api/submission api/clients - api/types - api/retry api/errors + api/filesystem + api/retry + api/submission + api/types .. toctree:: :caption: Getting Involved diff --git a/src/judge0/filesystem.py b/src/judge0/filesystem.py index 6773680..27fae22 100644 --- a/src/judge0/filesystem.py +++ b/src/judge0/filesystem.py @@ -11,6 +11,16 @@ class File(BaseModel): + """File object for storing file content. + + Parameters + ---------- + name : str + File name. + content : str or bytes, optional + File content. If str is provided, it will be encoded to bytes. + """ + name: str content: Optional[Union[str, bytes]] = None @@ -29,6 +39,15 @@ def __str__(self): class Filesystem(BaseModel): + """Filesystem object for storing multiple files. + + Parameters + ---------- + content : str or bytes or File or Iterable[File] or Filesystem, optional + Filesystem content. If str or bytes is provided, it will be decoded to + files. + """ + files: list[File] = [] def __init__(self, **data): @@ -66,6 +85,7 @@ def __repr__(self) -> str: return f"{self.__class__.__name__}(content={content_encoded!r})" def encode(self) -> bytes: + """Encode Filesystem object to bytes.""" zip_buffer = io.BytesIO() with zipfile.ZipFile(zip_buffer, "w") as zip_file: for file in self.files: From 6491bc55721b82557e80f75ff15a7aaf097164f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Mon, 30 Dec 2024 13:37:49 +0100 Subject: [PATCH 088/109] Add link to examples. --- docs/source/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index d31c601..0db5743 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -29,7 +29,7 @@ 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. +`examples `_. Getting Involved ================ From a2c5e45569a148210e15a4951cb8c21521110559 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Tue, 31 Dec 2024 11:07:07 +0100 Subject: [PATCH 089/109] Use None for base client HOME_URL. --- src/judge0/clients.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/judge0/clients.py b/src/judge0/clients.py index 311d26b..ac5af89 100644 --- a/src/judge0/clients.py +++ b/src/judge0/clients.py @@ -46,8 +46,9 @@ def __init__( self.languages = self.get_languages() self.config = self.get_config_info() except Exception as e: + home_url = getattr(self, "HOME_URL", None) raise RuntimeError( - f"Authentication failed. Visit {self.HOME_URL} to get or " + f"Authentication failed. Visit {home_url} to get or " "review your authentication credentials." ) from e From 8338f218fe7fa7b1266a60e3f74f7f93837a615b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Tue, 31 Dec 2024 11:25:58 +0100 Subject: [PATCH 090/109] Update docs in base types module. --- src/judge0/base_types.py | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/src/judge0/base_types.py b/src/judge0/base_types.py index 5e7a57c..dc18bdd 100644 --- a/src/judge0/base_types.py +++ b/src/judge0/base_types.py @@ -14,7 +14,7 @@ @dataclass(frozen=True) class TestCase: - """Dataclass for test case.""" + """Test case data model.""" input: Optional[str] = None expected_output: Optional[str] = None @@ -62,6 +62,11 @@ def encode(self) -> bytes: class Language(BaseModel): + """Language data model. + + Stores information about a language supported by Judge0. + """ + id: int name: str is_archived: Optional[bool] = None @@ -71,7 +76,12 @@ class Language(BaseModel): class LanguageAlias(IntEnum): - """Language enumeration.""" + """Language alias enumeration. + + Enumerates the programming languages supported by Judge0 client. Language + alias is resolved to the latest version of the language supported by the + selected client. + """ ASSEMBLY = auto() BASH = auto() @@ -143,14 +153,20 @@ class LanguageAlias(IntEnum): class Flavor(IntEnum): - """Judge0 flavor enumeration.""" + """Flavor enumeration. + + Enumerates the flavors supported by Judge0 client. + """ CE = 0 EXTRA_CE = 1 class Status(IntEnum): - """Status enumeration.""" + """Status enumeration. + + Enumerates possible status codes of a submission. + """ IN_QUEUE = 1 PROCESSING = 2 @@ -172,7 +188,10 @@ def __str__(self): class Config(BaseModel): - """Client config data.""" + """Client config data model. + + Stores configuration data for the Judge0 client. + """ allow_enable_network: bool allow_enable_per_process_and_thread_memory_limit: bool From 86d935a2788ff4b09d1d3b677261a31c34afea13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Tue, 31 Dec 2024 11:36:45 +0100 Subject: [PATCH 091/109] Minor docs update. --- src/judge0/api.py | 8 +++-- src/judge0/clients.py | 74 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 74 insertions(+), 8 deletions(-) diff --git a/src/judge0/api.py b/src/judge0/api.py index 92b91b1..6c08471 100644 --- a/src/judge0/api.py +++ b/src/judge0/api.py @@ -353,10 +353,12 @@ def async_execute( resolved. submissions : Submission or Submissions, optional Submission or submissions for execution. - source_code: str, optional + source_code : str, optional A source code of a program. - test_cases: TestCaseType or TestCases, optional + test_cases : TestCaseType or TestCases, optional A single test or a list of test cases + **kwargs : dict + Additional keyword arguments to pass to the Submission constructor. Returns ------- @@ -405,6 +407,8 @@ def sync_execute( A source code of a program. test_cases: TestCaseType or TestCases, optional A single test or a list of test cases + **kwargs : dict + Additional keyword arguments to pass to the Submission constructor. Returns ------- diff --git a/src/judge0/clients.py b/src/judge0/clients.py index ac5af89..621b844 100644 --- a/src/judge0/clients.py +++ b/src/judge0/clients.py @@ -356,7 +356,19 @@ def get_submissions( class ATD(Client): - """Base class for all AllThingsDev clients.""" + """Base class for all AllThingsDev clients. + + Parameters + ---------- + endpoint : str + Default request endpoint. + host_header_value : str + Value for the x-apihub-host header. + api_key : str + AllThingsDev API key. + **kwargs : dict + Additional keyword arguments for the base Client. + """ API_KEY_ENV: ClassVar[str] = "JUDGE0_ATD_API_KEY" @@ -376,7 +388,15 @@ def _update_endpoint_header(self, header_value): class ATDJudge0CE(ATD): - """AllThingsDev client for CE flavor.""" + """AllThingsDev client for CE flavor. + + Parameters + ---------- + api_key : str + AllThingsDev API key. + **kwargs : dict + Additional keyword arguments for the base Client. + """ DEFAULT_ENDPOINT: ClassVar[str] = ( "https://judge0-ce.proxy-production.allthingsdev.co" @@ -460,7 +480,15 @@ def get_submissions( class ATDJudge0ExtraCE(ATD): - """AllThingsDev client for Extra CE flavor.""" + """AllThingsDev client for Extra CE flavor. + + Parameters + ---------- + api_key : str + AllThingsDev API key. + **kwargs : dict + Additional keyword arguments for the base Client. + """ DEFAULT_ENDPOINT: ClassVar[str] = ( "https://judge0-extra-ce.proxy-production.allthingsdev.co" @@ -545,7 +573,19 @@ def get_submissions( class Rapid(Client): - """Base class for all RapidAPI clients.""" + """Base class for all RapidAPI clients. + + Parameters + ---------- + endpoint : str + Default request endpoint. + host_header_value : str + Value for the x-rapidapi-host header. + api_key : str + RapidAPI API key. + **kwargs : dict + Additional keyword arguments for the base Client. + """ API_KEY_ENV: ClassVar[str] = "JUDGE0_RAPID_API_KEY" @@ -562,7 +602,15 @@ def __init__(self, endpoint, host_header_value, api_key, **kwargs): class RapidJudge0CE(Rapid): - """RapidAPI client for CE flavor.""" + """RapidAPI client for CE flavor. + + Parameters + ---------- + api_key : str + RapidAPI API key. + **kwargs : dict + Additional keyword arguments for the base Client. + """ DEFAULT_ENDPOINT: ClassVar[str] = "https://judge0-ce.p.rapidapi.com" DEFAULT_HOST: ClassVar[str] = "judge0-ce.p.rapidapi.com" @@ -578,7 +626,15 @@ def __init__(self, api_key, **kwargs): class RapidJudge0ExtraCE(Rapid): - """RapidAPI client for Extra CE flavor.""" + """RapidAPI client for Extra CE flavor. + + Parameters + ---------- + api_key : str + RapidAPI API key. + **kwargs : dict + Additional keyword arguments for the base Client. + """ DEFAULT_ENDPOINT: ClassVar[str] = "https://judge0-extra-ce.p.rapidapi.com" DEFAULT_HOST: ClassVar[str] = "judge0-extra-ce.p.rapidapi.com" @@ -602,6 +658,8 @@ class Sulu(Client): Default request endpoint. api_key : str, optional Sulu API key. + **kwargs : dict + Additional keyword arguments for the base Client. """ API_KEY_ENV: ClassVar[str] = "JUDGE0_SULU_API_KEY" @@ -622,6 +680,8 @@ class SuluJudge0CE(Sulu): ---------- api_key : str, optional Sulu API key. + **kwargs : dict + Additional keyword arguments for the base Client. """ DEFAULT_ENDPOINT: ClassVar[str] = "https://judge0-ce.p.sulu.sh" @@ -642,6 +702,8 @@ class SuluJudge0ExtraCE(Sulu): ---------- api_key : str Sulu API key. + **kwargs : dict + Additional keyword arguments for the base Client. """ DEFAULT_ENDPOINT: ClassVar[str] = "https://judge0-extra-ce.p.sulu.sh" From 3c2fb61ff51057988b98701dea68bf23c440735c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Tue, 31 Dec 2024 12:32:02 +0100 Subject: [PATCH 092/109] Update contributing guide. --- .../contributors_guide/contributing.rst | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/docs/source/contributors_guide/contributing.rst b/docs/source/contributors_guide/contributing.rst index 2a19fb5..5cee988 100644 --- a/docs/source/contributors_guide/contributing.rst +++ b/docs/source/contributors_guide/contributing.rst @@ -25,4 +25,46 @@ Preparing the development setup .. code-block:: console $ pip install -e .[test] + $ pip install -r docs/requirements.txt # needed for building the docs $ pre-commit install + +Building documentation +---------------------- + +Documentation is built using Sphinx. To build the documentation, run the + +.. code-block:: console + + $ cd docs + $ make html + +You should inspect the changes in the documentation by opening the +``docs/build/html/index.html`` file in your browser. + +You'll see a different output since the documentation is build with +`sphinx-multiversion `_ extension. + +Testing +------- + +If you implemented a feature or fixed a bug, please add tests for it. + +Unfortunately, at the moment you cannot run full test suite because it requires +access to API keys for all implemented API hubs (ATD, Sulu, and RapidAPI) and +a private Judge0 instance. To partially address this situation, you can run and +test your implemented feature and tests locally and use the GitHub CI pipeline +to run the full test suite. + +To run the tests locally, you can use the following command: + +.. code-block:: console + + $ pytest -svv tests -k '' + +To make the test compatible with the CI pipeline, you should use one of the +client fixtures: + +.. code-block:: python + + def test_my_test(request): + client = request.getfixturevalue("judge0_ce_client") # or judge0_extra_ce_client From ae10b877dff737f5c6ed63fd3e2b9632a0e1b4b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Herman=20Zvonimir=20Do=C5=A1ilovi=C4=87?= Date: Tue, 31 Dec 2024 13:17:14 +0100 Subject: [PATCH 093/109] Change client order --- src/judge0/clients.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/judge0/clients.py b/src/judge0/clients.py index 621b844..2d8366a 100644 --- a/src/judge0/clients.py +++ b/src/judge0/clients.py @@ -715,5 +715,5 @@ 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 = (SuluJudge0CE, RapidJudge0CE, ATDJudge0CE) +EXTRA_CE = (SuluJudge0ExtraCE, RapidJudge0ExtraCE, ATDJudge0ExtraCE) From 50947518275817cbfba0cbe8ddf16922bdf5b6dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Tue, 31 Dec 2024 14:05:50 +0100 Subject: [PATCH 094/109] Add logo --- docs/assets/logo.png | Bin 0 -> 31233 bytes docs/source/conf.py | 32 ++++++++++++++++++++++++++++++++ docs/source/index.rst | 6 +++--- 3 files changed, 35 insertions(+), 3 deletions(-) create mode 100644 docs/assets/logo.png diff --git a/docs/assets/logo.png b/docs/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..9661bbb3b66cba7024393224b4afefdedb102674 GIT binary patch literal 31233 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

VJca6nosg~zu%#x_>VE4K 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 literal 0 HcmV?d00001 diff --git a/docs/source/conf.py b/docs/source/conf.py index 697f9f5..0807b9f 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -34,9 +34,39 @@ # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output +html_title = project html_theme = "sphinxawesome_theme" html_theme_options = { "show_scrolltop": True, + "extra_header_link_icons": { + "repository on GitHub": { + "link": "https://github.com/judge0/judge0-python", + "icon": ( + '' + '' + ), + }, + }, } html_show_sphinx = False html_sidebars = { @@ -46,6 +76,8 @@ "versioning.html", ], } +html_logo = "../assets/logo.png" +html_favicon = html_logo pygments_style = "sphinx" sys.path.insert(0, os.path.abspath("../../src/")) # Adjust as needed diff --git a/docs/source/index.rst b/docs/source/index.rst index 0db5743..a434535 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,6 +1,6 @@ -=============================== -Judge0 Python SDK documentation -=============================== +================= +Judge0 Python SDK +================= Getting Started =============== From fa0c502daff57d9617d1370d9b83dfe6b4a33bb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Tue, 31 Dec 2024 14:27:21 +0100 Subject: [PATCH 095/109] Update release notes guide. --- .../contributors_guide/release_notes.rst | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/docs/source/contributors_guide/release_notes.rst b/docs/source/contributors_guide/release_notes.rst index ec66b1c..da50b25 100644 --- a/docs/source/contributors_guide/release_notes.rst +++ b/docs/source/contributors_guide/release_notes.rst @@ -1,4 +1,32 @@ How to create a release ======================= -TODO \ No newline at end of file +Creating a release is a simple process that involves a few steps: + +#. **Prepare the release**: + #. Create a separate branch for the release. Name the branch ``release-x.y.z`` + where ``x.y.z`` is the version number. + #. Update the version number in ``judge0/__init__.py``. + #. Update the version number in ``judge0/pyproject.toml``. + #. Sync the branch with any changes from the master branch. + #. Create a pull request for the release branch. Make sure that all tests pass. + #. Merge the pull request. + #. Pull the changes to your local repository and tag the commit (``git tag vX.Y.Z``) with the version number. + #. Push the tags to the remote repository (``git push origin master --tags``). +#. **Create release (notes) on GitHub**. + #. Go to the `releases page `_ on GitHub. + #. Release title should be ``Judge0 Python SDK vX.Y.Z``. + #. Release notes should include a changes from the previous release to the newest release. + #. Use the `template `_ from the repo to organize the changes. + #. Create the release. ("Set as a pre-release" should NOT be checked.) +#. **Release on PyPI**: + #. Use the `GitHub Actions workflow `_ to create a release on PyPI. + #. Select `Run workflow` and as `Target repository` select `pypi`. + #. Click the `Run workflow` button. + +After the release is successfully published on PyPI, create a new pull request +that updates the working version in ``judge0/__init__.py`` and ``judge0/pyproject.toml`` +to the minor version. Merge the pull request and you're done! For example, if the +new release was ``1.2.2``, the working version should be updated to ``1.3.0.dev0``. + +You've successfully created a release! Congratulations! 🎉 \ No newline at end of file From 9fc2353a41ec571128cb6540d633c3ea6f3a79ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Tue, 31 Dec 2024 15:23:29 +0100 Subject: [PATCH 096/109] Update contributing guide. Update tests to use default client fixtures. --- .../contributors_guide/contributing.rst | 47 +++++++++++---- tests/conftest.py | 60 +++++++++++++++---- tests/test_api_test_cases.py | 6 +- tests/test_submission.py | 8 +-- 4 files changed, 92 insertions(+), 29 deletions(-) diff --git a/docs/source/contributors_guide/contributing.rst b/docs/source/contributors_guide/contributing.rst index 5cee988..61cc227 100644 --- a/docs/source/contributors_guide/contributing.rst +++ b/docs/source/contributors_guide/contributing.rst @@ -47,24 +47,47 @@ You'll see a different output since the documentation is build with Testing ------- -If you implemented a feature or fixed a bug, please add tests for it. +.. warning:: + If you are implementing features or fixing bugs, you are expected to have + all of the three API keys (ATD, Sulu, and RapidAPI) setup and set in you + environment variables - ``JUDGE0_SULU_API_KEY``, ``JUDGE0_RAPID_API_KEY``, + and ``JUDGE0_ATD_API_KEY``. -Unfortunately, at the moment you cannot run full test suite because it requires -access to API keys for all implemented API hubs (ATD, Sulu, and RapidAPI) and -a private Judge0 instance. To partially address this situation, you can run and -test your implemented feature and tests locally and use the GitHub CI pipeline -to run the full test suite. +Every bug fix or new feature should have tests for it. The tests are located in +the ``tests`` directory and are written using `pytest `_. -To run the tests locally, you can use the following command: +While working with the tests, you should use the following fixtures: -.. code-block:: console +* ``default_ce_client`` - a client, chosen based on the environment variables set, that uses the CE flavor of the client. +* ``default_extra_ce_client`` - a client, chosen based on the environment variables set, that uses the Extra CE flavor of the client. - $ pytest -svv tests -k '' +The ``default_ce_client`` and ``default_extra_ce_client`` are fixtures that +return a client based on the environment variables set. This enables you to +run the full test suite locally, but also to run the tests on the CI pipeline +without changing the tests. -To make the test compatible with the CI pipeline, you should use one of the -client fixtures: +You can use the fixtures in your tests like this: .. code-block:: python def test_my_test(request): - client = request.getfixturevalue("judge0_ce_client") # or judge0_extra_ce_client + client = request.getfixturevalue("default_ce_client") # or default_extra_ce_client + +To run the tests locally, you can use the following command: + +.. code-block:: console + + $ pytest -svv tests -k '' + +This will enable you to run a single test, without incurring the cost of +running the full test suite. If you want to run the full test suite, you can +use the following command: + +.. code-block:: console + + $ pytest -svv tests + +or you can create a draft PR and let the CI pipeline run the tests for you. +The CI pipeline will run the tests on every PR, using a private instance +of Judge0, so you can be sure that your changes are not breaking the existing +functionality. diff --git a/tests/conftest.py b/tests/conftest.py index b1bd612..0ceddb2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,11 +13,15 @@ def judge0_ce_client(): api_key = os.getenv("JUDGE0_TEST_API_KEY") api_key_header = os.getenv("JUDGE0_TEST_API_KEY_HEADER") endpoint = os.getenv("JUDGE0_TEST_CE_ENDPOINT") - client = clients.Client( - endpoint=endpoint, - auth_headers={api_key_header: api_key}, - ) - return client + + if api_key is None or api_key_header is None or endpoint is None: + return None + else: + client = clients.Client( + endpoint=endpoint, + auth_headers={api_key_header: api_key}, + ) + return client @pytest.fixture(scope="session") @@ -25,11 +29,15 @@ def judge0_extra_ce_client(): api_key = os.getenv("JUDGE0_TEST_API_KEY") api_key_header = os.getenv("JUDGE0_TEST_API_KEY_HEADER") endpoint = os.getenv("JUDGE0_TEST_EXTRA_CE_ENDPOINT") - client = clients.Client( - endpoint=endpoint, - auth_headers={api_key_header: api_key}, - ) - return client + + if api_key is None or api_key_header is None or endpoint is None: + return None + else: + client = clients.Client( + endpoint=endpoint, + auth_headers={api_key_header: api_key}, + ) + return client @pytest.fixture(scope="session") @@ -63,6 +71,12 @@ def rapid_extra_ce_client(): @pytest.fixture(scope="session") def sulu_ce_client(): api_key = os.getenv("JUDGE0_SULU_API_KEY") + if api_key is None: + pytest.fail( + "Sulu API key is not available for testing. Make sure to have " + "JUDGE0_SULU_API_KEY in your environment variables." + ) + client = clients.SuluJudge0CE(api_key) return client @@ -70,5 +84,31 @@ def sulu_ce_client(): @pytest.fixture(scope="session") def sulu_extra_ce_client(): api_key = os.getenv("JUDGE0_SULU_API_KEY") + if api_key is None: + pytest.fail( + "Sulu API key is not available for testing. Make sure to have " + "JUDGE0_SULU_API_KEY in your environment variables." + ) + client = clients.SuluJudge0ExtraCE(api_key) return client + + +@pytest.fixture(scope="session") +def default_ce_client(judge0_ce_client, sulu_ce_client): + if judge0_ce_client is not None: + return judge0_ce_client + if sulu_ce_client is not None: + return sulu_ce_client + + pytest.fail("No default CE client available for testing.") + + +@pytest.fixture(scope="session") +def default_extra_ce_client(judge0_extra_ce_client, sulu_extra_ce_client): + if judge0_extra_ce_client is not None: + return judge0_extra_ce_client + if sulu_extra_ce_client is not None: + return sulu_extra_ce_client + + pytest.fail("No default Extra CE client available for testing.") diff --git a/tests/test_api_test_cases.py b/tests/test_api_test_cases.py index 0d08f5f..c4963e9 100644 --- a/tests/test_api_test_cases.py +++ b/tests/test_api_test_cases.py @@ -207,7 +207,7 @@ def test_single_test_case_in_iterable(self, test_cases, stdin, expected_output): def test_test_cases_from_run( source_code_or_submissions, test_cases, expected_status, request ): - client = request.getfixturevalue("judge0_ce_client") + client = request.getfixturevalue("default_ce_client") if isinstance(source_code_or_submissions, str): submissions = judge0.run( @@ -254,7 +254,7 @@ def test_test_cases_from_run( ], ) def test_no_test_cases(submissions, expected_status, request): - client = request.getfixturevalue("judge0_ce_client") + client = request.getfixturevalue("default_ce_client") submissions = judge0.run( client=client, @@ -269,7 +269,7 @@ def test_no_test_cases(submissions, expected_status, request): @pytest.mark.parametrize("n_submissions", [42, 84]) def test_batched_test_cases(n_submissions, request): - client = request.getfixturevalue("judge0_ce_client") + client = request.getfixturevalue("default_ce_client") submissions = [ Submission(source_code=f"print({i})", expected_output=f"{i}") for i in range(n_submissions) diff --git a/tests/test_submission.py b/tests/test_submission.py index ddae140..ec1885b 100644 --- a/tests/test_submission.py +++ b/tests/test_submission.py @@ -52,7 +52,7 @@ def test_from_json(): def test_status_before_and_after_submission(request): - client = request.getfixturevalue("judge0_ce_client") + client = request.getfixturevalue("default_ce_client") submission = Submission(source_code='print("Hello World!")') assert submission.status is None @@ -65,7 +65,7 @@ def test_status_before_and_after_submission(request): def test_is_done(request): - client = request.getfixturevalue("judge0_ce_client") + client = request.getfixturevalue("default_ce_client") submission = Submission(source_code='print("Hello World!")') assert submission.status is None @@ -77,7 +77,7 @@ def test_is_done(request): def test_language_before_and_after_execution(request): - client = request.getfixturevalue("judge0_ce_client") + client = request.getfixturevalue("default_ce_client") code = """\ public class Main { public static void main(String[] args) { @@ -97,7 +97,7 @@ def test_language_before_and_after_execution(request): def test_language_executable(request): - client = request.getfixturevalue("judge0_ce_client") + client = request.getfixturevalue("default_ce_client") code = b64decode( "f0VMRgIBAQAAAAAAAAAAAAIAPgABAAAAAABAAAAAAABAAAAAAAAAAEAQAAAAAAAAAAAAAEAAOAABAEAABAADAAEAAAAFAAAAABAAAAAAAAAAAEAAAAAAAAAAQAAAAAAAJQAAAAAAAAAlAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADHAjVANsAG+GABAAInHDwUx/41HPA8FAGhlbGxvLCB3b3JsZAoALnNoc3RydGFiAC50ZXh0AC5yb2RhdGEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAEAAAAGAAAAAAAAAAAAQAAAAAAAABAAAAAAAAAXAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABEAAAABAAAAAgAAAAAAAAAYAEAAAAAAABgQAAAAAAAADQAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAABAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAlEAAAAAAAABkAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAA" # noqa: E501 ) From ad9f6e96852287c2814ead9a3992e94c21f6be92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Sun, 5 Jan 2025 19:24:25 +0100 Subject: [PATCH 097/109] Add placeholder for client resolution docs. --- docs/source/contributors_guide/contributing.rst | 4 ++++ docs/source/in_depth/client_resolution.rst | 4 ++++ docs/source/index.rst | 12 +++++++++++- 3 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 docs/source/in_depth/client_resolution.rst diff --git a/docs/source/contributors_guide/contributing.rst b/docs/source/contributors_guide/contributing.rst index 61cc227..546c6cf 100644 --- a/docs/source/contributors_guide/contributing.rst +++ b/docs/source/contributors_guide/contributing.rst @@ -41,6 +41,10 @@ Documentation is built using Sphinx. To build the documentation, run the You should inspect the changes in the documentation by opening the ``docs/build/html/index.html`` file in your browser. +.. note:: + If you are having trouble with the documentation and are seeing unexpected + output, delete the ``docs/build`` directory and rerun the ``make html`` command. + You'll see a different output since the documentation is build with `sphinx-multiversion `_ extension. diff --git a/docs/source/in_depth/client_resolution.rst b/docs/source/in_depth/client_resolution.rst new file mode 100644 index 0000000..dfdd532 --- /dev/null +++ b/docs/source/in_depth/client_resolution.rst @@ -0,0 +1,4 @@ +Client Resolution +================= + +TODO: Describe the approach to client resolution. See `_get_implicit_client`. \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst index a434535..0e3027d 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -63,6 +63,16 @@ Every contribution, big or small, is valuable! api/submission api/types + +.. toctree:: + :caption: In Depth + :glob: + :titlesonly: + :hidden: + + in_depth/client_resolution + + .. toctree:: :caption: Getting Involved :glob: @@ -70,4 +80,4 @@ Every contribution, big or small, is valuable! :hidden: contributors_guide/contributing - contributors_guide/release_notes \ No newline at end of file + contributors_guide/release_notes From df50bfa9c06c9c9fc90624516c2015737d85b06e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Sun, 5 Jan 2025 19:27:31 +0100 Subject: [PATCH 098/109] Add placeholder for API overview. --- docs/source/in_depth/overview.rst | 8 ++++++++ docs/source/index.rst | 1 + 2 files changed, 9 insertions(+) create mode 100644 docs/source/in_depth/overview.rst diff --git a/docs/source/in_depth/overview.rst b/docs/source/in_depth/overview.rst new file mode 100644 index 0000000..d7dd136 --- /dev/null +++ b/docs/source/in_depth/overview.rst @@ -0,0 +1,8 @@ +Overview +======== + +TODO: + +* add a brief overview of the most important classes (Client, Submission, etc.) +* add a brief overview of the most important functions (create_submission, get_submission, etc.) +* write about the difference between high-level api and low-level api \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst index 0e3027d..4f98325 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -70,6 +70,7 @@ Every contribution, big or small, is valuable! :titlesonly: :hidden: + in_depth/overview in_depth/client_resolution From 896ce80b0d44753ff18dd605ef80e9aa583fdaf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Sun, 5 Jan 2025 19:41:54 +0100 Subject: [PATCH 099/109] Add main nav links. Add indication of external links. --- docs/source/conf.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/source/conf.py b/docs/source/conf.py index 0807b9f..ea15353 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -67,6 +67,11 @@ ), }, }, + "awesome_external_links": True, + "main_nav_links": { + "Home": "https://judge0.github.io/judge0-python/", + "Judge0": "https://judge0.com/", + }, } html_show_sphinx = False html_sidebars = { From 711b778e39674936b526a5805de133d08ae4c269 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Sun, 5 Jan 2025 20:40:51 +0100 Subject: [PATCH 100/109] Refactor _get_implict_client. Add ability to load custom clients from get_client. Refactor env variable to read auth headers as dict. --- .github/workflows/test.yml | 8 ++--- src/judge0/__init__.py | 74 +++++++++++++++++++++++++++++--------- tests/conftest.py | 26 +++++--------- 3 files changed, 70 insertions(+), 38 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3c33777..d68acfc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -32,10 +32,10 @@ jobs: JUDGE0_ATD_API_KEY: ${{ secrets.JUDGE0_ATD_API_KEY }} JUDGE0_RAPID_API_KEY: ${{ secrets.JUDGE0_RAPID_API_KEY }} JUDGE0_SULU_API_KEY: ${{ secrets.JUDGE0_SULU_API_KEY }} - JUDGE0_TEST_API_KEY: ${{ secrets.JUDGE0_TEST_API_KEY }} - JUDGE0_TEST_API_KEY_HEADER: ${{ secrets.JUDGE0_TEST_API_KEY_HEADER }} - JUDGE0_TEST_CE_ENDPOINT: ${{ secrets.JUDGE0_TEST_CE_ENDPOINT }} - JUDGE0_TEST_EXTRA_CE_ENDPOINT: ${{ secrets.JUDGE0_TEST_EXTRA_CE_ENDPOINT }} + JUDGE0_CE_AUTH_HEADERS: ${{ secrets.JUDGE0_CE_AUTH_HEADERS }} + JUDGE0_EXTRA_CE_AUTH_HEADERS: ${{ secrets.JUDGE0_EXTRA_CE_AUTH_HEADERS }} + JUDGE0_CE_ENDPOINT: ${{ secrets.JUDGE0_CE_ENDPOINT }} + JUDGE0_EXTRA_CE_ENDPOINT: ${{ secrets.JUDGE0_EXTRA_CE_ENDPOINT }} run: | source venv/bin/activate pytest -vv tests/ diff --git a/src/judge0/__init__.py b/src/judge0/__init__.py index 391e926..0fb78d9 100644 --- a/src/judge0/__init__.py +++ b/src/judge0/__init__.py @@ -1,5 +1,7 @@ import os +from typing import Union + from .api import ( async_execute, async_run, @@ -73,8 +75,6 @@ def _get_implicit_client(flavor: Flavor) -> Client: if flavor == Flavor.EXTRA_CE and JUDGE0_IMPLICIT_EXTRA_CE_CLIENT is not None: return JUDGE0_IMPLICIT_EXTRA_CE_CLIENT - from .clients import CE, EXTRA_CE - try: from dotenv import load_dotenv @@ -82,27 +82,18 @@ def _get_implicit_client(flavor: Flavor) -> Client: except: # noqa: E722 pass - if flavor == Flavor.CE: - client_classes = CE - else: - client_classes = EXTRA_CE + # Let's check if we can find a self-hosted client. + client = _get_custom_client(flavor) # Try to find one of the predefined keys JUDGE0_{SULU,RAPID,ATD}_API_KEY # in environment variables. - client = None - for client_class in client_classes: - api_key = os.getenv(client_class.API_KEY_ENV) - if api_key is not None: - client = client_class(api_key) - break + if client is None: + client = _get_predefined_client(flavor) # If we didn't find any of the possible predefined keys, initialize # the preview Sulu client based on the flavor. if client is None: - if flavor == Flavor.CE: - client = SuluJudge0CE(retry_strategy=RegularPeriodRetry(0.5)) - else: - client = SuluJudge0ExtraCE(retry_strategy=RegularPeriodRetry(0.5)) + client = _get_preview_client(flavor) if flavor == Flavor.CE: JUDGE0_IMPLICIT_CE_CLIENT = client @@ -112,6 +103,57 @@ def _get_implicit_client(flavor: Flavor) -> Client: return client +def _get_preview_client(flavor: Flavor) -> Union[SuluJudge0CE, SuluJudge0ExtraCE]: + if flavor == Flavor.CE: + return SuluJudge0CE(retry_strategy=RegularPeriodRetry(0.5)) + else: + return SuluJudge0ExtraCE(retry_strategy=RegularPeriodRetry(0.5)) + + +def _get_custom_client(flavor: Flavor) -> Union[Client, None]: + ce_endpoint = os.getenv("JUDGE0_CE_ENDPOINT") + ce_auth_header = os.getenv("JUDGE0_CE_AUTH_HEADERS") + extra_ce_endpoint = os.getenv("JUDGE0_EXTRA_CE_ENDPOINT") + extra_ce_auth_header = os.getenv("JUDGE0_EXTRA_CE_AUTH_HEADERS") + + if flavor == Flavor.CE and ce_endpoint is not None and ce_auth_header is not None: + return Client( + endpoint=ce_endpoint, + auth_headers=eval(ce_auth_header), + ) + + if ( + flavor == Flavor.EXTRA_CE + and extra_ce_endpoint is not None + and extra_ce_auth_header is not None + ): + return Client( + endpoint=extra_ce_endpoint, + auth_headers=eval(extra_ce_auth_header), + ) + + return None + + +def _get_predefined_client(flavor: Flavor) -> Union[Client, None]: + from .clients import CE, EXTRA_CE + + if flavor == Flavor.CE: + client_classes = CE + else: + client_classes = EXTRA_CE + + for client_class in client_classes: + api_key = os.getenv(client_class.API_KEY_ENV) + if api_key is not None: + client = client_class(api_key) + break + else: + client = None + + return client + + CE = Flavor.CE EXTRA_CE = Flavor.EXTRA_CE diff --git a/tests/conftest.py b/tests/conftest.py index 0ceddb2..d06dcb2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,34 +10,24 @@ @pytest.fixture(scope="session") def judge0_ce_client(): - api_key = os.getenv("JUDGE0_TEST_API_KEY") - api_key_header = os.getenv("JUDGE0_TEST_API_KEY_HEADER") - endpoint = os.getenv("JUDGE0_TEST_CE_ENDPOINT") + endpoint = os.getenv("JUDGE0_CE_ENDPOINT") + auth_headers = os.getenv("JUDGE0_CE_AUTH_HEADERS") - if api_key is None or api_key_header is None or endpoint is None: + if endpoint is None or auth_headers is None: return None else: - client = clients.Client( - endpoint=endpoint, - auth_headers={api_key_header: api_key}, - ) - return client + return clients.Client(endpoint=endpoint, auth_headers=eval(auth_headers)) @pytest.fixture(scope="session") def judge0_extra_ce_client(): - api_key = os.getenv("JUDGE0_TEST_API_KEY") - api_key_header = os.getenv("JUDGE0_TEST_API_KEY_HEADER") - endpoint = os.getenv("JUDGE0_TEST_EXTRA_CE_ENDPOINT") + endpoint = os.getenv("JUDGE0_EXTRA_CE_ENDPOINT") + auth_headers = os.getenv("JUDGE0_EXTRA_CE_AUTH_HEADERS") - if api_key is None or api_key_header is None or endpoint is None: + if endpoint is None or auth_headers is None: return None else: - client = clients.Client( - endpoint=endpoint, - auth_headers={api_key_header: api_key}, - ) - return client + return clients.Client(endpoint=endpoint, auth_headers=eval(auth_headers)) @pytest.fixture(scope="session") From b893b18cabc65c1b18cf81b15284a6db5bfe362f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Sun, 5 Jan 2025 20:44:37 +0100 Subject: [PATCH 101/109] Add ability to run test CI from console. --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d68acfc..991d2d1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,6 +1,7 @@ name: Test judge0-python on: + workflow_dispatch: push: branches: ["master"] paths: ["src/**", "tests/**"] From 340e0e01c72700dc7303bf34ae0c77ec1d3c76ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Sun, 5 Jan 2025 21:07:12 +0100 Subject: [PATCH 102/109] Use json.loads instead of eval. --- src/judge0/__init__.py | 6 ++++-- tests/conftest.py | 5 +++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/judge0/__init__.py b/src/judge0/__init__.py index 0fb78d9..34de57d 100644 --- a/src/judge0/__init__.py +++ b/src/judge0/__init__.py @@ -111,6 +111,8 @@ def _get_preview_client(flavor: Flavor) -> Union[SuluJudge0CE, SuluJudge0ExtraCE def _get_custom_client(flavor: Flavor) -> Union[Client, None]: + from json import loads + ce_endpoint = os.getenv("JUDGE0_CE_ENDPOINT") ce_auth_header = os.getenv("JUDGE0_CE_AUTH_HEADERS") extra_ce_endpoint = os.getenv("JUDGE0_EXTRA_CE_ENDPOINT") @@ -119,7 +121,7 @@ def _get_custom_client(flavor: Flavor) -> Union[Client, None]: if flavor == Flavor.CE and ce_endpoint is not None and ce_auth_header is not None: return Client( endpoint=ce_endpoint, - auth_headers=eval(ce_auth_header), + auth_headers=loads(ce_auth_header), ) if ( @@ -129,7 +131,7 @@ def _get_custom_client(flavor: Flavor) -> Union[Client, None]: ): return Client( endpoint=extra_ce_endpoint, - auth_headers=eval(extra_ce_auth_header), + auth_headers=loads(extra_ce_auth_header), ) return None diff --git a/tests/conftest.py b/tests/conftest.py index d06dcb2..48aea37 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,4 @@ +import json import os import pytest @@ -16,7 +17,7 @@ def judge0_ce_client(): if endpoint is None or auth_headers is None: return None else: - return clients.Client(endpoint=endpoint, auth_headers=eval(auth_headers)) + return clients.Client(endpoint=endpoint, auth_headers=json.loads(auth_headers)) @pytest.fixture(scope="session") @@ -27,7 +28,7 @@ def judge0_extra_ce_client(): if endpoint is None or auth_headers is None: return None else: - return clients.Client(endpoint=endpoint, auth_headers=eval(auth_headers)) + return clients.Client(endpoint=endpoint, auth_headers=json.loads(auth_headers)) @pytest.fixture(scope="session") From bb29acc2325375d80dc29839a99d59b34ecf3f4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Herman=20Zvonimir=20Do=C5=A1ilovi=C4=87?= Date: Sun, 5 Jan 2025 21:21:54 +0100 Subject: [PATCH 103/109] Change default language to PYTHON_FOR_ML --- src/judge0/submission.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/judge0/submission.py b/src/judge0/submission.py index 55b7660..78d1470 100644 --- a/src/judge0/submission.py +++ b/src/judge0/submission.py @@ -128,7 +128,7 @@ class Submission(BaseModel): source_code: Optional[Union[str, bytes]] = Field(default=None, repr=True) language: Union[LanguageAlias, int] = Field( - default=LanguageAlias.PYTHON, + default=LanguageAlias.PYTHON_FOR_ML, repr=True, ) additional_files: Optional[Union[str, Filesystem]] = Field(default=None, repr=True) From 56f695359025bc01d9a37709c6ee46060235c44f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Sun, 5 Jan 2025 21:45:59 +0100 Subject: [PATCH 104/109] Rename fixtures. Refactor tests to explicitly set Submission language. --- .../contributors_guide/contributing.rst | 8 ++-- src/judge0/__init__.py | 10 ++--- tests/conftest.py | 38 +++++++++++++----- tests/test_api_test_cases.py | 39 ++++++++++++++----- tests/test_submission.py | 16 +++++--- 5 files changed, 77 insertions(+), 34 deletions(-) diff --git a/docs/source/contributors_guide/contributing.rst b/docs/source/contributors_guide/contributing.rst index 546c6cf..552471d 100644 --- a/docs/source/contributors_guide/contributing.rst +++ b/docs/source/contributors_guide/contributing.rst @@ -62,10 +62,10 @@ the ``tests`` directory and are written using `pytest Client: # Let's check if we can find a self-hosted client. client = _get_custom_client(flavor) - # Try to find one of the predefined keys JUDGE0_{SULU,RAPID,ATD}_API_KEY - # in environment variables. + # Try to find one of the API keys JUDGE0_{SULU,RAPID,ATD}_API_KEY + # for hub clients. if client is None: - client = _get_predefined_client(flavor) + client = _get_hub_client(flavor) - # If we didn't find any of the possible predefined keys, initialize + # If we didn't find any of the possible keys, initialize # the preview Sulu client based on the flavor. if client is None: client = _get_preview_client(flavor) @@ -137,7 +137,7 @@ def _get_custom_client(flavor: Flavor) -> Union[Client, None]: return None -def _get_predefined_client(flavor: Flavor) -> Union[Client, None]: +def _get_hub_client(flavor: Flavor) -> Union[Client, None]: from .clients import CE, EXTRA_CE if flavor == Flavor.CE: diff --git a/tests/conftest.py b/tests/conftest.py index 48aea37..c168435 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,7 +10,7 @@ @pytest.fixture(scope="session") -def judge0_ce_client(): +def custom_ce_client(): endpoint = os.getenv("JUDGE0_CE_ENDPOINT") auth_headers = os.getenv("JUDGE0_CE_AUTH_HEADERS") @@ -21,7 +21,7 @@ def judge0_ce_client(): @pytest.fixture(scope="session") -def judge0_extra_ce_client(): +def custom_extra_ce_client(): endpoint = os.getenv("JUDGE0_EXTRA_CE_ENDPOINT") auth_headers = os.getenv("JUDGE0_EXTRA_CE_AUTH_HEADERS") @@ -86,20 +86,38 @@ def sulu_extra_ce_client(): @pytest.fixture(scope="session") -def default_ce_client(judge0_ce_client, sulu_ce_client): - if judge0_ce_client is not None: - return judge0_ce_client +def ce_client( + custom_ce_client, + sulu_ce_client, + rapid_ce_client, + atd_ce_client, +): + if custom_ce_client is not None: + return custom_ce_client if sulu_ce_client is not None: return sulu_ce_client + if rapid_ce_client is not None: + return rapid_ce_client + if atd_ce_client is not None: + return atd_ce_client - pytest.fail("No default CE client available for testing.") + pytest.fail("No CE client available for testing.") @pytest.fixture(scope="session") -def default_extra_ce_client(judge0_extra_ce_client, sulu_extra_ce_client): - if judge0_extra_ce_client is not None: - return judge0_extra_ce_client +def extra_ce_client( + custom_extra_ce_client, + sulu_extra_ce_client, + rapid_extra_ce_client, + atd_extra_ce_client, +): + if custom_extra_ce_client is not None: + return custom_extra_ce_client if sulu_extra_ce_client is not None: return sulu_extra_ce_client + if rapid_extra_ce_client is not None: + return rapid_extra_ce_client + if atd_extra_ce_client is not None: + return atd_extra_ce_client - pytest.fail("No default Extra CE client available for testing.") + pytest.fail("No Extra CE client available for testing.") diff --git a/tests/test_api_test_cases.py b/tests/test_api_test_cases.py index c4963e9..8dea1e6 100644 --- a/tests/test_api_test_cases.py +++ b/tests/test_api_test_cases.py @@ -2,8 +2,9 @@ import judge0 import pytest -from judge0 import Status, Submission, TestCase from judge0.api import create_submissions_from_test_cases +from judge0.base_types import LanguageAlias, Status, TestCase +from judge0.submission import Submission @pytest.mark.parametrize( @@ -172,14 +173,20 @@ def test_single_test_case_in_iterable(self, test_cases, stdin, expected_output): [Status.ACCEPTED, Status.ACCEPTED], ], [ - Submission(source_code="print(f'Hello, {input()}')"), + Submission( + source_code="print(f'Hello, {input()}')", + language=LanguageAlias.PYTHON, + ), [ TestCase("Judge0", "Hello, Judge0"), ], [Status.ACCEPTED], ], [ - Submission(source_code="print(f'Hello, {input()}')"), + Submission( + source_code="print(f'Hello, {input()}')", + language=LanguageAlias.PYTHON, + ), [ TestCase("Judge0", "Hello, Judge0"), TestCase("pytest", "Hi, pytest"), @@ -188,8 +195,14 @@ def test_single_test_case_in_iterable(self, test_cases, stdin, expected_output): ], [ [ - Submission(source_code="print(f'Hello, {input()}')"), - Submission(source_code="print(f'Hello, {input()}')"), + Submission( + source_code="print(f'Hello, {input()}')", + language=LanguageAlias.PYTHON, + ), + Submission( + source_code="print(f'Hello, {input()}')", + language=LanguageAlias.PYTHON, + ), ], [ TestCase("Judge0", "Hello, Judge0"), @@ -207,13 +220,14 @@ def test_single_test_case_in_iterable(self, test_cases, stdin, expected_output): def test_test_cases_from_run( source_code_or_submissions, test_cases, expected_status, request ): - client = request.getfixturevalue("default_ce_client") + client = request.getfixturevalue("ce_client") if isinstance(source_code_or_submissions, str): submissions = judge0.run( client=client, source_code=source_code_or_submissions, test_cases=test_cases, + language=LanguageAlias.PYTHON, ) else: submissions = judge0.run( @@ -231,6 +245,7 @@ def test_test_cases_from_run( [ Submission( source_code="print(f'Hello, {input()}')", + language=LanguageAlias.PYTHON, stdin="Judge0", expected_output="Hello, Judge0", ), @@ -240,11 +255,13 @@ def test_test_cases_from_run( [ Submission( source_code="print(f'Hello, {input()}')", + language=LanguageAlias.PYTHON, stdin="Judge0", expected_output="Hello, Judge0", ), Submission( source_code="print(f'Hello, {input()}')", + language=LanguageAlias.PYTHON, stdin="pytest", expected_output="Hello, pytest", ), @@ -254,7 +271,7 @@ def test_test_cases_from_run( ], ) def test_no_test_cases(submissions, expected_status, request): - client = request.getfixturevalue("default_ce_client") + client = request.getfixturevalue("ce_client") submissions = judge0.run( client=client, @@ -269,9 +286,13 @@ def test_no_test_cases(submissions, expected_status, request): @pytest.mark.parametrize("n_submissions", [42, 84]) def test_batched_test_cases(n_submissions, request): - client = request.getfixturevalue("default_ce_client") + client = request.getfixturevalue("ce_client") submissions = [ - Submission(source_code=f"print({i})", expected_output=f"{i}") + Submission( + source_code=f"print({i})", + language=LanguageAlias.PYTHON, + expected_output=f"{i}", + ) for i in range(n_submissions) ] diff --git a/tests/test_submission.py b/tests/test_submission.py index ec1885b..8675034 100644 --- a/tests/test_submission.py +++ b/tests/test_submission.py @@ -52,8 +52,10 @@ def test_from_json(): def test_status_before_and_after_submission(request): - client = request.getfixturevalue("default_ce_client") - submission = Submission(source_code='print("Hello World!")') + client = request.getfixturevalue("ce_client") + submission = Submission( + source_code='print("Hello World!")', language=LanguageAlias.PYTHON + ) assert submission.status is None @@ -65,8 +67,10 @@ def test_status_before_and_after_submission(request): def test_is_done(request): - client = request.getfixturevalue("default_ce_client") - submission = Submission(source_code='print("Hello World!")') + client = request.getfixturevalue("ce_client") + submission = Submission( + source_code='print("Hello World!")', language=LanguageAlias.PYTHON + ) assert submission.status is None @@ -77,7 +81,7 @@ def test_is_done(request): def test_language_before_and_after_execution(request): - client = request.getfixturevalue("default_ce_client") + client = request.getfixturevalue("ce_client") code = """\ public class Main { public static void main(String[] args) { @@ -97,7 +101,7 @@ def test_language_before_and_after_execution(request): def test_language_executable(request): - client = request.getfixturevalue("default_ce_client") + client = request.getfixturevalue("ce_client") code = b64decode( "f0VMRgIBAQAAAAAAAAAAAAIAPgABAAAAAABAAAAAAABAAAAAAAAAAEAQAAAAAAAAAAAAAEAAOAABAEAABAADAAEAAAAFAAAAABAAAAAAAAAAAEAAAAAAAAAAQAAAAAAAJQAAAAAAAAAlAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADHAjVANsAG+GABAAInHDwUx/41HPA8FAGhlbGxvLCB3b3JsZAoALnNoc3RydGFiAC50ZXh0AC5yb2RhdGEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAEAAAAGAAAAAAAAAAAAQAAAAAAAABAAAAAAAAAXAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABEAAAABAAAAAgAAAAAAAAAYAEAAAAAAABgQAAAAAAAADQAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAABAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAlEAAAAAAAABkAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAA" # noqa: E501 ) From 4a2e4f14341222df9bab335702a3a23452ad8a5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Karlo=20Do=C5=A1ilovi=C4=87?= Date: Sun, 5 Jan 2025 22:14:35 +0100 Subject: [PATCH 105/109] Refactor fixtures. Add preview client as possible fixture for ce and extra ce client. --- tests/conftest.py | 72 +++++++++++++++++++++++++++++++---------------- 1 file changed, 48 insertions(+), 24 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index c168435..4e1547c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,7 +4,7 @@ import pytest from dotenv import load_dotenv -from judge0 import clients +from judge0 import clients, RegularPeriodRetry load_dotenv() @@ -34,55 +34,71 @@ def custom_extra_ce_client(): @pytest.fixture(scope="session") def atd_ce_client(): api_key = os.getenv("JUDGE0_ATD_API_KEY") - client = clients.ATDJudge0CE(api_key) - return client + + if api_key is None: + return None + else: + return clients.ATDJudge0CE(api_key) @pytest.fixture(scope="session") def atd_extra_ce_client(): api_key = os.getenv("JUDGE0_ATD_API_KEY") - client = clients.ATDJudge0ExtraCE(api_key) - return client + + if api_key is None: + return None + else: + return clients.ATDJudge0ExtraCE(api_key) @pytest.fixture(scope="session") def rapid_ce_client(): api_key = os.getenv("JUDGE0_RAPID_API_KEY") - client = clients.RapidJudge0CE(api_key) - return client + + if api_key is None: + return None + else: + return clients.RapidJudge0CE(api_key) @pytest.fixture(scope="session") def rapid_extra_ce_client(): api_key = os.getenv("JUDGE0_RAPID_API_KEY") - client = clients.RapidJudge0ExtraCE(api_key) - return client + + if api_key is None: + return None + else: + return clients.RapidJudge0ExtraCE(api_key) @pytest.fixture(scope="session") def sulu_ce_client(): api_key = os.getenv("JUDGE0_SULU_API_KEY") - if api_key is None: - pytest.fail( - "Sulu API key is not available for testing. Make sure to have " - "JUDGE0_SULU_API_KEY in your environment variables." - ) - client = clients.SuluJudge0CE(api_key) - return client + if api_key is None: + return None + else: + return clients.SuluJudge0CE(api_key) @pytest.fixture(scope="session") def sulu_extra_ce_client(): api_key = os.getenv("JUDGE0_SULU_API_KEY") + if api_key is None: - pytest.fail( - "Sulu API key is not available for testing. Make sure to have " - "JUDGE0_SULU_API_KEY in your environment variables." - ) + return None + else: + return clients.SuluJudge0ExtraCE(api_key) - client = clients.SuluJudge0ExtraCE(api_key) - return client + +@pytest.fixture(scope="session") +def preview_ce_client() -> clients.SuluJudge0CE: + return clients.SuluJudge0CE(retry_strategy=RegularPeriodRetry(0.5)) + + +@pytest.fixture(scope="session") +def preview_extra_ce_client() -> clients.SuluJudge0ExtraCE: + return clients.SuluJudge0ExtraCE(retry_strategy=RegularPeriodRetry(0.5)) @pytest.fixture(scope="session") @@ -91,6 +107,7 @@ def ce_client( sulu_ce_client, rapid_ce_client, atd_ce_client, + preview_ce_client, ): if custom_ce_client is not None: return custom_ce_client @@ -100,8 +117,10 @@ def ce_client( return rapid_ce_client if atd_ce_client is not None: return atd_ce_client + if preview_ce_client is not None: + return preview_ce_client - pytest.fail("No CE client available for testing.") + pytest.fail("No CE client available for testing. This error should not happen!") @pytest.fixture(scope="session") @@ -110,6 +129,7 @@ def extra_ce_client( sulu_extra_ce_client, rapid_extra_ce_client, atd_extra_ce_client, + preview_extra_ce_client, ): if custom_extra_ce_client is not None: return custom_extra_ce_client @@ -119,5 +139,9 @@ def extra_ce_client( return rapid_extra_ce_client if atd_extra_ce_client is not None: return atd_extra_ce_client + if preview_extra_ce_client is not None: + return preview_extra_ce_client - pytest.fail("No Extra CE client available for testing.") + pytest.fail( + "No Extra CE client available for testing. This error should not happen!" + ) From 57050fab0b53ad082eea5eca7d9af9e4b8500b3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Herman=20Zvonimir=20Do=C5=A1ilovi=C4=87?= Date: Sun, 5 Jan 2025 22:31:46 +0100 Subject: [PATCH 106/109] Move docs requirements to pyproject.toml --- .github/workflows/docs.yml | 8 ++++---- docs/requirements.txt | 4 ---- docs/source/contributors_guide/contributing.rst | 5 ++--- pyproject.toml | 11 ++++++++++- 4 files changed, 16 insertions(+), 12 deletions(-) delete mode 100644 docs/requirements.txt diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 4a70709..df34c7f 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -17,10 +17,10 @@ jobs: persist-credentials: false fetch-depth: 0 # Fetch the full history ref: ${{ github.ref }} # Check out the current branch or tag - + - name: Fetch tags only run: git fetch --tags --no-recurse-submodules - + - name: Set up Python uses: actions/setup-python@v4 with: @@ -29,7 +29,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install -r docs/requirements.txt + pip install -e [docs] - name: Build documentation run: sphinx-multiversion docs/source docs/build/html --keep-going --no-color @@ -41,7 +41,7 @@ jobs: # Get the latest tag latest_tag=$(git tag --sort=-creatordate | head -n 1) echo "LATEST_RELEASE=$latest_tag" >> $GITHUB_ENV - + - name: Generate index.html for judge0.github.io/judge0-python. run: | echo ' diff --git a/docs/requirements.txt b/docs/requirements.txt deleted file mode 100644 index cd3bc0f..0000000 --- a/docs/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -sphinx==7.4.7 -sphinxawesome-theme==5.3.2 -sphinx-autodoc-typehints==2.3.0 -sphinx-multiversion==0.2.4 \ No newline at end of file diff --git a/docs/source/contributors_guide/contributing.rst b/docs/source/contributors_guide/contributing.rst index 552471d..194834c 100644 --- a/docs/source/contributors_guide/contributing.rst +++ b/docs/source/contributors_guide/contributing.rst @@ -24,8 +24,7 @@ Preparing the development setup .. code-block:: console - $ pip install -e .[test] - $ pip install -r docs/requirements.txt # needed for building the docs + $ pip install -e [dev] $ pre-commit install Building documentation @@ -73,7 +72,7 @@ without changing the tests. You can use the fixtures in your tests like this: .. code-block:: python - + def test_my_test(request): client = request.getfixturevalue("ce_client") # or extra_ce_client diff --git a/pyproject.toml b/pyproject.toml index 4974a30..1fc29ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,16 @@ test = [ "pytest-cov==6.0.0", "flake8-docstrings==1.7.0", ] -docs = ["sphinx==7.4.7"] +docs = [ + "sphinx==7.4.7", + "sphinxawesome-theme==5.3.2", + "sphinx-autodoc-typehints==2.3.0", + "sphinx-multiversion==0.2.4", +] +dev = [ + "judge0[test]", + "judge0[docs]", +] [tool.flake8] extend-ignore = [ From a06164c7bfd8060670e0182978465f97a24887cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Herman=20Zvonimir=20Do=C5=A1ilovi=C4=87?= Date: Sun, 5 Jan 2025 22:37:07 +0100 Subject: [PATCH 107/109] Fix dependency installation in workflow and in the docs --- .github/workflows/docs.yml | 2 +- docs/source/contributors_guide/contributing.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index df34c7f..11d9d8e 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -29,7 +29,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install -e [docs] + pip install -e .[docs] - name: Build documentation run: sphinx-multiversion docs/source docs/build/html --keep-going --no-color diff --git a/docs/source/contributors_guide/contributing.rst b/docs/source/contributors_guide/contributing.rst index 194834c..094577c 100644 --- a/docs/source/contributors_guide/contributing.rst +++ b/docs/source/contributors_guide/contributing.rst @@ -24,7 +24,7 @@ Preparing the development setup .. code-block:: console - $ pip install -e [dev] + $ pip install -e .[dev] $ pre-commit install Building documentation From 7974f90f5a17f676ee4eaf24457c21bc230ba93b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Herman=20Zvonimir=20Do=C5=A1ilovi=C4=87?= Date: Sun, 5 Jan 2025 22:39:33 +0100 Subject: [PATCH 108/109] Remove newline from workflow file --- .github/workflows/publish.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 35cd8dd..0a4e3e3 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -46,4 +46,3 @@ jobs: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} run: twine upload dist/* - From 9f92d2b2fbda78100bce50bf2263de8495d46b7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Herman=20Zvonimir=20Do=C5=A1ilovi=C4=87?= Date: Sun, 5 Jan 2025 23:05:30 +0100 Subject: [PATCH 109/109] Add pytest ini_options. Fix skipped test. --- .github/workflows/test.yml | 2 +- docs/source/contributors_guide/contributing.rst | 4 ++-- pyproject.toml | 3 +++ src/judge0/api.py | 7 +++++-- src/judge0/base_types.py | 2 ++ tests/test_api.py | 1 - 6 files changed, 13 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 991d2d1..2beadbd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -39,4 +39,4 @@ jobs: JUDGE0_EXTRA_CE_ENDPOINT: ${{ secrets.JUDGE0_EXTRA_CE_ENDPOINT }} run: | source venv/bin/activate - pytest -vv tests/ + pytest tests diff --git a/docs/source/contributors_guide/contributing.rst b/docs/source/contributors_guide/contributing.rst index 094577c..867f3f1 100644 --- a/docs/source/contributors_guide/contributing.rst +++ b/docs/source/contributors_guide/contributing.rst @@ -80,7 +80,7 @@ To run the tests locally, you can use the following command: .. code-block:: console - $ pytest -svv tests -k '' + $ pytest tests -k '' This will enable you to run a single test, without incurring the cost of running the full test suite. If you want to run the full test suite, you can @@ -88,7 +88,7 @@ use the following command: .. code-block:: console - $ pytest -svv tests + $ pytest tests or you can create a draft PR and let the CI pipeline run the tests for you. The CI pipeline will run the tests on every PR, using a private instance diff --git a/pyproject.toml b/pyproject.toml index 1fc29ce..e8a4b3d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -72,3 +72,6 @@ extend-ignore = [ ] docstring-convention = "numpy" max-line-length = 88 + +[tool.pytest.ini_options] +addopts = "-vv" diff --git a/src/judge0/api.py b/src/judge0/api.py index 6c08471..e83ecf7 100644 --- a/src/judge0/api.py +++ b/src/judge0/api.py @@ -67,8 +67,11 @@ def _resolve_client( if isinstance(client, Flavor): return get_client(client) - if client is None and isinstance(submissions, Iterable) and len(submissions) == 0: - raise ValueError("Client cannot be determined from empty submissions.") + if client is None: + if ( + isinstance(submissions, Iterable) and len(submissions) == 0 + ) or submissions is None: + raise ValueError("Client cannot be determined from empty submissions.") # client is None and we have to determine a flavor of the client from the # the submission's languages. diff --git a/src/judge0/base_types.py b/src/judge0/base_types.py index dc18bdd..94cedf8 100644 --- a/src/judge0/base_types.py +++ b/src/judge0/base_types.py @@ -16,6 +16,8 @@ class TestCase: """Test case data model.""" + __test__ = False # Needed to avoid pytest warning + input: Optional[str] = None expected_output: Optional[str] = None diff --git a/tests/test_api.py b/tests/test_api.py index 50a1464..3ba9526 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -48,7 +48,6 @@ def test_resolve_client_with_flavor( None, ], ) -@pytest.mark.skip def test_resolve_client_empty_submissions_argument(submissions): with pytest.raises(ValueError): _resolve_client(submissions=submissions)