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 01/37] 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 02/37] 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 03/37] 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 04/37] 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 05/37] 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 06/37] 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 07/37] 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 08/37] 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 09/37] 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 10/37] 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 11/37] 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 12/37] 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 13/37] 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 14/37] 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 15/37] 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 16/37] 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 17/37] 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 18/37] 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 19/37] 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 20/37] 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 21/37] 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 22/37] 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 23/37] 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 24/37] 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 25/37] 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 26/37] 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 27/37] 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 28/37] 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 29/37] 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 30/37] 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 31/37] 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 32/37] 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 33/37] 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 34/37] 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 35/37] 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 36/37] 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 37/37] 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)