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.
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
new file mode 100644
index 0000000..11d9d8e
--- /dev/null
+++ b/.github/workflows/docs.yml
@@ -0,0 +1,67 @@
+name: "Sphinx: Render docs"
+
+on:
+ workflow_dispatch:
+ push:
+ branches: ["master"]
+
+
+jobs:
+ build:
+ runs-on: ubuntu-24.04
+ permissions:
+ contents: write
+ steps:
+ - 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:
+ python-version: "3.9"
+
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install -e .[docs]
+
+ - 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)
+ echo "LATEST_RELEASE=$latest_tag" >> $GITHUB_ENV
+
+ - name: Generate index.html for judge0.github.io/judge0-python.
+ run: |
+ echo '
+
+
+
+
+ ' > docs/build/html/index.html
+ env:
+ latest_release: ${{ env.LATEST_RELEASE }}
+
+ - 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'
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ publish_dir: docs/build/html
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/*
-
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 62f57c9..2beadbd 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -1,15 +1,20 @@
name: Test judge0-python
on:
+ workflow_dispatch:
push:
branches: ["master"]
+ paths: ["src/**", "tests/**"]
+ pull_request:
+ branches: ["master"]
+ paths: ["src/**", "tests/**"]
permissions:
contents: read
jobs:
test:
- runs-on: ubuntu-latest
+ runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
@@ -19,18 +24,19 @@ 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 }}
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: |
- pipenv run pytest -vv tests/
+ source venv/bin/activate
+ pytest tests
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.
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index e2a0428..8adce63 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -6,3 +6,11 @@ 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
+ additional_dependencies:
+ - "flake8-pyproject"
+ - flake8-docstrings
+ - pydocstyle
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.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..346d24d
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,3 @@
+# How to contribute
+
+See [docs](https://judge0.github.io/judge0-python/contributing.html).
diff --git a/Pipfile b/Pipfile
deleted file mode 100644
index f7f341b..0000000
--- a/Pipfile
+++ /dev/null
@@ -1,18 +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"
-
-[requires]
-python_version = "3.9"
-python_full_version = "3.9.20"
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.
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
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/assets/logo.png b/docs/assets/logo.png
new file mode 100644
index 0000000..9661bbb
Binary files /dev/null and b/docs/assets/logo.png differ
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/_templates/versioning.html b/docs/source/_templates/versioning.html
new file mode 100644
index 0000000..1b8de30
--- /dev/null
+++ b/docs/source/_templates/versioning.html
@@ -0,0 +1,11 @@
+{% 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') }}
+
+{% endif %}
\ No newline at end of file
diff --git a/docs/source/api/api.rst b/docs/source/api/api.rst
new file mode 100644
index 0000000..e5b41b5
--- /dev/null
+++ b/docs/source/api/api.rst
@@ -0,0 +1,6 @@
+API Module
+==========
+
+.. automodule:: judge0.api
+ :members:
+ :exclude-members: sync_run, async_run
diff --git a/docs/source/api/clients.rst b/docs/source/api/clients.rst
new file mode 100644
index 0000000..b4d15c9
--- /dev/null
+++ b/docs/source/api/clients.rst
@@ -0,0 +1,46 @@
+Clients Module
+==============
+
+
+.. 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/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/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/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/api/submission.rst b/docs/source/api/submission.rst
new file mode 100644
index 0000000..4f9977a
--- /dev/null
+++ b/docs/source/api/submission.rst
@@ -0,0 +1,8 @@
+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/docs/source/api/types.rst b/docs/source/api/types.rst
new file mode 100644
index 0000000..8cb94cc
--- /dev/null
+++ b/docs/source/api/types.rst
@@ -0,0 +1,47 @@
+Types Module
+============
+
+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.Iterable
+ :members:
+ :member-order: bysource
+
+.. 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
new file mode 100644
index 0000000..ea15353
--- /dev/null
+++ b/docs/source/conf.py
@@ -0,0 +1,116 @@
+# 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
+
+
+import os
+import sys
+
+from sphinxawesome_theme.postprocess import Icons
+
+project = "Judge0 Python SDK"
+copyright = "2024, Judge0"
+author = "Judge0"
+release = ""
+
+# -- General configuration ---------------------------------------------------
+# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
+
+extensions = [
+ "sphinx.ext.autodoc",
+ "sphinx.ext.napoleon",
+ # "sphinx.ext.autosummary",
+ "sphinx_autodoc_typehints",
+ "sphinx_multiversion",
+]
+
+templates_path = ["_templates"]
+exclude_patterns = []
+
+# -- 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": (
+ ''
+ ' '
+ ),
+ },
+ },
+ "awesome_external_links": True,
+ "main_nav_links": {
+ "Home": "https://judge0.github.io/judge0-python/",
+ "Judge0": "https://judge0.com/",
+ },
+}
+html_show_sphinx = False
+html_sidebars = {
+ "**": [
+ "sidebar_main_nav_links.html",
+ "sidebar_toc.html",
+ "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
+
+# -- Awesome theme config --
+html_permalinks_icon = Icons.permalinks_icon
+
+autodoc_default_options = {
+ "members": True,
+ "undoc-members": False,
+ "private-members": False,
+ "special-members": False,
+ "inherited-members": False,
+}
+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/contributors_guide/contributing.rst b/docs/source/contributors_guide/contributing.rst
new file mode 100644
index 0000000..867f3f1
--- /dev/null
+++ b/docs/source/contributors_guide/contributing.rst
@@ -0,0 +1,96 @@
+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 .[dev]
+ $ 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.
+
+.. 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.
+
+Testing
+-------
+
+.. 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``.
+
+Every bug fix or new feature should have tests for it. The tests are located in
+the ``tests`` directory and are written using `pytest `_.
+
+While working with the tests, you should use the following fixtures:
+
+* ``ce_client`` - a client, chosen based on the environment variables set, that uses the CE flavor of the client.
+* ``extra_ce_client`` - a client, chosen based on the environment variables set, that uses the Extra CE flavor of the client.
+
+The ``ce_client`` and ``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.
+
+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
+
+To run the tests locally, you can use the following command:
+
+.. code-block:: console
+
+ $ 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
+use the following command:
+
+.. code-block:: console
+
+ $ 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
+of Judge0, so you can be sure that your changes are not breaking the existing
+functionality.
diff --git a/docs/source/contributors_guide/release_notes.rst b/docs/source/contributors_guide/release_notes.rst
new file mode 100644
index 0000000..da50b25
--- /dev/null
+++ b/docs/source/contributors_guide/release_notes.rst
@@ -0,0 +1,32 @@
+How to create a release
+=======================
+
+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
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/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
new file mode 100644
index 0000000..4f98325
--- /dev/null
+++ b/docs/source/index.rst
@@ -0,0 +1,84 @@
+=================
+Judge0 Python SDK
+=================
+
+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
+================
+
+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
+ :glob:
+ :titlesonly:
+ :hidden:
+
+ api/api
+ api/clients
+ api/errors
+ api/filesystem
+ api/retry
+ api/submission
+ api/types
+
+
+.. toctree::
+ :caption: In Depth
+ :glob:
+ :titlesonly:
+ :hidden:
+
+ in_depth/overview
+ in_depth/client_resolution
+
+
+.. toctree::
+ :caption: Getting Involved
+ :glob:
+ :titlesonly:
+ :hidden:
+
+ contributors_guide/contributing
+ contributors_guide/release_notes
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/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/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/pyproject.toml b/pyproject.toml
index aeef351..e8a4b3d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,10 +1,10 @@
[project]
name = "judge0"
-version = "0.0.1"
-description = "The official Python library for Judge0."
+version = "0.1.0.dev0"
+description = "The official Python SDK 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",
@@ -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", "pydantic>=2.0.0,<3.0.0"]
[build-system]
requires = ["setuptools>=70.0"]
@@ -37,4 +37,41 @@ 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",
+]
+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 = [
+ 'D100',
+ 'D101',
+ 'D102',
+ 'D103',
+ 'D104',
+ 'D105',
+ 'D107',
+ 'D205',
+ "D209",
+ 'D400',
+ 'F821',
+]
+docstring-convention = "numpy"
+max-line-length = 88
+
+[tool.pytest.ini_options]
+addopts = "-vv"
diff --git a/src/judge0/__init__.py b/src/judge0/__init__.py
index 8f41ec0..f5f33a5 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,
@@ -27,6 +29,8 @@
from .retry import MaxRetries, MaxWaitTime, RegularPeriodRetry
from .submission import Submission
+__version__ = "0.1.0.dev0"
+
__all__ = [
"ATD",
"ATDJudge0CE",
@@ -71,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
@@ -80,32 +82,76 @@ def _get_implicit_client(flavor: Flavor) -> Client:
except: # noqa: E722
pass
+ # Let's check if we can find a self-hosted client.
+ client = _get_custom_client(flavor)
+
+ # Try to find one of the API keys JUDGE0_{SULU,RAPID,ATD}_API_KEY
+ # for hub clients.
+ if client is None:
+ client = _get_hub_client(flavor)
+
+ # 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)
+
+ if flavor == Flavor.CE:
+ JUDGE0_IMPLICIT_CE_CLIENT = client
+ else:
+ JUDGE0_IMPLICIT_EXTRA_CE_CLIENT = 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]:
+ 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")
+ 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=loads(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=loads(extra_ce_auth_header),
+ )
+
+ return None
+
+
+def _get_hub_client(flavor: Flavor) -> Union[Client, None]:
+ from .clients import CE, EXTRA_CE
+
if flavor == Flavor.CE:
client_classes = CE
else:
client_classes = EXTRA_CE
- # 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 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()
- else:
- client = SuluJudge0ExtraCE()
-
- if flavor == Flavor.CE:
- JUDGE0_IMPLICIT_CE_CLIENT = client
else:
- JUDGE0_IMPLICIT_EXTRA_CE_CLIENT = client
+ client = None
return client
@@ -113,9 +159,70 @@ def _get_implicit_client(flavor: Flavor) -> Client:
CE = Flavor.CE
EXTRA_CE = Flavor.EXTRA_CE
-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/api.py b/src/judge0/api.py
index b5fd64d..e83ecf7 100644
--- a/src/judge0/api.py
+++ b/src/judge0/api.py
@@ -1,14 +1,26 @@
-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, TestCaseType
from .clients import Client
from .common import batched
-
-from .retry import RegularPeriodRetry, RetryMechanism
+from .errors import ClientResolutionError
+from .retry import RegularPeriodRetry, RetryStrategy
from .submission import Submission, Submissions
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):
@@ -24,18 +36,45 @@ 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.
+
+ 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
+ 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)
- if client is None and isinstance(submissions, list) 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
- # submissions and the languages.
+ # the submission's languages.
if isinstance(submissions, Submission):
submissions = [submissions]
@@ -49,7 +88,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."
@@ -57,9 +96,24 @@ 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]:
+ """Universal function for creating submissions to the client.
+
+ Parameters
+ ----------
+ 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
+ ------
+ ClientResolutionError
+ Raised if client resolution fails.
+ """
client = _resolve_client(client=client, submissions=submissions)
if isinstance(submissions, Submission):
@@ -79,10 +133,26 @@ def create_submissions(
def get_submissions(
*,
- client: Optional[Client] = None,
+ client: Optional[Union[Client, Flavor]] = None,
submissions: Optional[Union[Submission, Submissions]] = None,
- fields: Union[str, Iterable[str], None] = None,
+ fields: Optional[Union[str, Iterable[str]]] = None,
) -> Union[Submission, Submissions]:
+ """Get submission (status) from a client.
+
+ Parameters
+ ----------
+ 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
+ ------
+ ClientResolutionError
+ Raised if client resolution fails.
+ """
client = _resolve_client(client=client, submissions=submissions)
if isinstance(submissions, Submission):
@@ -106,57 +176,90 @@ def get_submissions(
def wait(
*,
- client: Optional[Client] = None,
+ client: Optional[Union[Client, Flavor]] = None,
submissions: Optional[Union[Submission, Submissions]] = None,
- retry_mechanism: Optional[RetryMechanism] = 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.
+
+ Returns
+ -------
+ Submission or Submissions
+ A single submission or a list of submissions.
+
+ Raises
+ ------
+ ClientResolutionError
+ Raised if client resolution fails.
+ """
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 = {
- submission.token: submission for submission in [submissions]
- }
+ submissions_list = [submissions]
else:
- submissions_to_check = {
- submission.token: submission for submission in submissions
- }
+ submissions_list = submissions
- while len(submissions_to_check) > 0 and not retry_mechanism.is_done():
+ 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:
break
- retry_mechanism.wait()
- retry_mechanism.step()
+ retry_strategy.wait()
+ retry_strategy.step()
return submissions
def create_submissions_from_test_cases(
submissions: Union[Submission, Submissions],
- test_cases: Optional[Union[TestCase, TestCases]] = None,
-):
- """Utility function for creating submissions from the (submission, test_case) pairs.
-
- The following table contains the return type based on the types of `submissions`
- and `test_cases` arguments:
-
- | submissions | test_cases | returns |
- |:------------|:-----------|:------------|
- | Submission | TestCase | Submission |
- | Submission | TestCases | Submissions |
- | Submissions | TestCase | Submissions |
- | Submissions | TestCases | Submissions |
-
+ test_cases: Optional[Union[TestCaseType, TestCases]] = None,
+) -> Union[Submission, list[Submission]]:
+ """Create submissions from the submission and test case pairs.
+
+ Function always returns a deep copy so make sure you are using the
+ returned submission(s).
+
+ 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]
@@ -165,10 +268,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:
@@ -179,9 +299,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
@@ -192,10 +310,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. "
@@ -204,6 +323,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)
@@ -222,9 +342,41 @@ 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).
+
+ Aliases: `async_run`.
+
+ 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
+ **kwargs : dict
+ Additional keyword arguments to pass to the Submission constructor.
+
+ Returns
+ -------
+ Submission 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.
+
+ 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,
@@ -240,9 +392,41 @@ 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.
+
+ Aliases: `execute`, `run`, `sync_run`.
+
+ 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(s) 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
+ **kwargs : dict
+ Additional keyword arguments to pass to the Submission constructor.
+
+ Returns
+ -------
+ Submission 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.
+
+ 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 b1d4210..94cedf8 100644
--- a/src/judge0/base_types.py
+++ b/src/judge0/base_types.py
@@ -1,77 +1,175 @@
-from abc import ABC, abstractmethod
+import copy
+
from dataclasses import dataclass
-from enum import IntEnum
-from typing import Optional, Union
+from enum import auto, IntEnum
+from typing import Optional, Protocol, runtime_checkable, Sequence, Union
+
+from pydantic import BaseModel
+Iterable = Sequence
-TestCases = Union[
- list["TestCase"],
- tuple["TestCase"],
- list[dict],
- tuple[dict],
- list[list],
- list[tuple],
- tuple[list],
- tuple[tuple],
-]
+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
+ """Test case data model."""
+
+ __test__ = False # Needed to avoid pytest warning
input: Optional[str] = None
expected_output: Optional[str] = None
- @staticmethod
+ @classmethod
def from_record(
- test_case: Optional[Union[tuple, list, dict, "TestCase"]] = None
- ) -> "TestCase":
+ cls, test_case: Union[TestCaseType, None]
+ ) -> Union["TestCase", None]:
+ """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
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 None
raise ValueError(
f"Cannot create TestCase object from object of type {type(test_case)}."
)
-class Encodeable(ABC):
- @abstractmethod
+@runtime_checkable
+class Encodable(Protocol):
def encode(self) -> bytes:
- pass
+ """Serialize the object to bytes."""
+ ...
-@dataclass(frozen=True)
-class Language:
+class Language(BaseModel):
+ """Language data model.
+
+ Stores information about a language supported by Judge0.
+ """
+
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):
- PYTHON = 0
- CPP = 1
- JAVA = 2
- CPP_GCC = 3
- CPP_CLANG = 4
- PYTHON_FOR_ML = 5
+ """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()
+ 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):
+ """Flavor enumeration.
+
+ Enumerates the flavors supported by Judge0 client.
+ """
+
CE = 0
EXTRA_CE = 1
class Status(IntEnum):
+ """Status enumeration.
+
+ Enumerates possible status codes of a submission.
+ """
+
IN_QUEUE = 1
PROCESSING = 2
ACCEPTED = 3
@@ -91,8 +189,12 @@ def __str__(self):
return self.name.lower().replace("_", " ").title()
-@dataclass(frozen=True)
-class Config:
+class Config(BaseModel):
+ """Client config data model.
+
+ Stores configuration data for the Judge0 client.
+ """
+
allow_enable_network: bool
allow_enable_per_process_and_thread_memory_limit: bool
allow_enable_per_process_and_thread_time_limit: bool
diff --git a/src/judge0/clients.py b/src/judge0/clients.py
index 0797e9a..2d8366a 100644
--- a/src/judge0/clients.py
+++ b/src/judge0/clients.py
@@ -1,88 +1,199 @@
-from typing import Iterable, Union
+from typing import ClassVar, 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
+from .utils import handle_too_many_requests_error_for_preview_client
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
- )
-
- def __init__(self, endpoint, auth_headers) -> None:
+ """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__(
+ self,
+ endpoint,
+ auth_headers,
+ *,
+ retry_strategy: Optional[RetryStrategy] = None,
+ ) -> None:
self.endpoint = endpoint
self.auth_headers = auth_headers
+ self.retry_strategy = retry_strategy
+ self.session = requests.Session()
try:
- self.languages = [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:
+ home_url = getattr(self, "HOME_URL", None)
raise RuntimeError(
- f"Authentication failed. Visit {self.HOME_URL} to get or review your authentication credentials."
+ f"Authentication failed. Visit {home_url} to get or "
+ "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(
+ """Get general information about judge0.
+
+ Returns
+ -------
+ dict
+ General information about judge0.
+ """
+ response = self.session.get(
f"{self.endpoint}/about",
headers=self.auth_headers,
)
- r.raise_for_status()
- return r.json()
-
- def get_config_info(self) -> dict:
- r = requests.get(
+ response.raise_for_status()
+ return response.json()
+
+ @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,
)
- r.raise_for_status()
- return r.json()
-
- def get_language(self, language_id) -> dict:
+ response.raise_for_status()
+ return Config(**response.json())
+
+ @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}"
- r = requests.get(request_url, headers=self.auth_headers)
- r.raise_for_status()
- return r.json()
-
- def get_languages(self) -> list[dict]:
+ response = self.session.get(request_url, headers=self.auth_headers)
+ response.raise_for_status()
+ return Language(**response.json())
+
+ @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"
- 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 [Language(**lang_dict) for lang_dict in response.json()]
+ @handle_too_many_requests_error_for_preview_client
def get_statuses(self) -> list[dict]:
- r = requests.get(
+ """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,
)
- r.raise_for_status()
- return r.json()
+ response.raise_for_status()
+ return response.json()
@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 for the corresponding 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)
+ @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(
@@ -97,26 +208,40 @@ 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
+ @handle_too_many_requests_error_for_preview_client
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."""
+ """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",
}
@@ -129,19 +254,34 @@ 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
+ @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(
@@ -151,25 +291,42 @@ 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
+ @handle_too_many_requests_error_for_preview_client
def get_submissions(
self,
submissions: Submissions,
*,
- fields: Union[str, Iterable[str], None] = None,
+ 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.
+ """
params = {
"base64_encoded": "true",
}
@@ -182,26 +339,40 @@ 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
- 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):
- API_KEY_ENV = "JUDGE0_ATD_API_KEY"
-
- def __init__(self, endpoint, host_header_value, api_key):
+ """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"
+
+ def __init__(self, endpoint, host_header_value, api_key, **kwargs):
self.api_key = api_key
super().__init__(
endpoint,
@@ -209,6 +380,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):
@@ -216,42 +388,63 @@ def _update_endpoint_header(self, header_value):
class ATDJudge0CE(ATD):
- DEFAULT_ENDPOINT: str = "https://judge0-ce.proxy-production.allthingsdev.co"
- DEFAULT_HOST: str = "Judge0-CE.allthingsdev.co"
- HOME_URL: str = (
+ """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"
+ )
+ 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"
-
- def __init__(self, api_key):
+ 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__(
self.DEFAULT_ENDPOINT,
self.DEFAULT_HOST,
api_key,
+ **kwargs,
)
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()
@@ -267,7 +460,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,49 +473,71 @@ 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)
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"
+ """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"
+ )
+ 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"
-
- def __init__(self, api_key):
+ 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__(
self.DEFAULT_ENDPOINT,
self.DEFAULT_HOST,
api_key,
+ **kwargs,
)
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()
@@ -338,7 +553,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,16 +566,30 @@ 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)
class Rapid(Client):
- API_KEY_ENV = "JUDGE0_RAPID_API_KEY"
-
- def __init__(self, endpoint, host_header_value, api_key):
+ """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"
+
+ def __init__(self, endpoint, host_header_value, api_key, **kwargs):
self.api_key = api_key
super().__init__(
endpoint,
@@ -368,61 +597,123 @@ def __init__(self, endpoint, host_header_value, api_key):
"x-rapidapi-host": host_header_value,
"x-rapidapi-key": api_key,
},
+ **kwargs,
)
class RapidJudge0CE(Rapid):
- 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"
+ """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"
+ HOME_URL: ClassVar[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,
)
class RapidJudge0ExtraCE(Rapid):
- 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"
+ """RapidAPI client for Extra CE flavor.
- def __init__(self, api_key):
+ 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"
+ HOME_URL: ClassVar[str] = "https://rapidapi.com/judge0-official/api/judge0-extra-ce"
+
+ 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"
+ """Base class for all Sulu clients.
+
+ Parameters
+ ----------
+ endpoint : str
+ Default request endpoint.
+ api_key : str, optional
+ Sulu API key.
+ **kwargs : dict
+ Additional keyword arguments for the base Client.
+ """
- def __init__(self, endpoint, api_key=None):
+ API_KEY_ENV: ClassVar[str] = "JUDGE0_SULU_API_KEY"
+
+ 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,
)
class SuluJudge0CE(Sulu):
- DEFAULT_ENDPOINT: str = "https://judge0-ce.p.sulu.sh"
- HOME_URL: str = "https://sparkhub.sulu.sh/apis/judge0/judge0-ce/readme"
+ """Sulu client for CE flavor.
- def __init__(self, api_key=None):
- super().__init__(self.DEFAULT_ENDPOINT, api_key)
+ Parameters
+ ----------
+ 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"
+ HOME_URL: ClassVar[str] = "https://sparkhub.sulu.sh/apis/judge0/judge0-ce/readme"
+
+ 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"
+ """Sulu client for Extra CE flavor.
+
+ Parameters
+ ----------
+ 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"
+ HOME_URL: ClassVar[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 = (SuluJudge0CE, RapidJudge0CE, ATDJudge0CE)
+EXTRA_CE = (SuluJudge0ExtraCE, RapidJudge0ExtraCE, ATDJudge0ExtraCE)
diff --git a/src/judge0/common.py b/src/judge0/common.py
index 736895e..e8ab58e 100644
--- a/src/judge0/common.py
+++ b/src/judge0/common.py
@@ -2,31 +2,35 @@
from itertools import islice
from typing import Union
-from .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)}!")
def decode(content: Union[bytes, str]) -> str:
+ """Decode base64 encoded content."""
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)}!")
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/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,
},
}
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."""
diff --git a/src/judge0/filesystem.py b/src/judge0/filesystem.py
index 590795c..27fae22 100644
--- a/src/judge0/filesystem.py
+++ b/src/judge0/filesystem.py
@@ -3,21 +3,34 @@
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 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):
+ """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
+
+ 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""
@@ -25,12 +38,22 @@ def __str__(self):
return self.content.decode(errors="backslashreplace")
-class Filesystem(Encodeable):
- def __init__(
- self,
- content: Optional[Union[str, bytes, File, Iterable[File], "Filesystem"]] = None,
- ):
- self.files: list[File] = []
+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):
+ content = data.pop("content", None)
+ super().__init__(**data)
+ self.files = []
if isinstance(content, (bytes, str)):
if isinstance(content, bytes):
@@ -41,19 +64,28 @@ 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()))
- elif isinstance(content, abc.Iterable):
+ self.files.append(File(name=file_name, content=fp.read()))
+ elif isinstance(content, Iterable):
self.files = list(content)
elif isinstance(content, File):
self.files = [content]
elif isinstance(content, Filesystem):
self.files = copy.deepcopy(content.files)
+ elif content is None:
+ self.files = []
+ 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()
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:
@@ -61,6 +93,7 @@ def encode(self) -> bytes:
return zip_buffer.getvalue()
def __str__(self) -> str:
+ """Create string representation of Filesystem object."""
return b64encode(self.encode()).decode()
def __iter__(self):
diff --git a/src/judge0/retry.py b/src/judge0/retry.py
index 33acc52..f4bff5b 100644
--- a/src/judge0/retry.py
+++ b/src/judge0/retry.py
@@ -2,67 +2,107 @@
from abc import ABC, abstractmethod
-class RetryMechanism(ABC):
+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(RetryMechanism):
+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(RetryMechanism):
+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(RetryMechanism):
- """Check for submissions status periodically for indefinite amount of time."""
+class RegularPeriodRetry(RetryStrategy):
+ """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 step(self):
- pass
-
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
diff --git a/src/judge0/submission.py b/src/judge0/submission.py
index 2ffa178..78d1470 100644
--- a/src/judge0/submission.py
+++ b/src/judge0/submission.py
@@ -1,11 +1,12 @@
import copy
from datetime import datetime
-from typing import Optional, Union
+from typing import Any, Optional, Union
-from judge0.filesystem import Filesystem
+from pydantic import BaseModel, ConfigDict, Field, field_validator, UUID4
-from .base_types import LanguageAlias, Status
+from .base_types import Iterable, LanguageAlias, Status
from .common import decode, encode
+from .filesystem import Filesystem
ENCODED_REQUEST_FIELDS = {
"source_code",
@@ -48,7 +49,6 @@
"time",
"wall_time",
"memory",
- "post_execution_filesystem",
}
REQUEST_FIELDS = ENCODED_REQUEST_FIELDS | EXTRA_REQUEST_FIELDS
RESPONSE_FIELDS = ENCODED_RESPONSE_FIELDS | EXTRA_RESPONSE_FIELDS
@@ -63,82 +63,160 @@
"wall_time_limit",
}
-Submissions = Union[list["Submission"], tuple["Submission"]]
+Submissions = Iterable["Submission"]
-class Submission:
+class Submission(BaseModel):
"""
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__(
- self,
- *,
- 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,
- ):
- 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 = 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
-
- def set_attributes(self, attributes):
+ source_code: Optional[Union[str, bytes]] = Field(default=None, repr=True)
+ language: Union[LanguageAlias, int] = Field(
+ default=LanguageAlias.PYTHON_FOR_ML,
+ 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)
+ 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] = 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
+ 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
@@ -152,11 +230,14 @@ def set_attributes(self, attributes):
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)
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,21 +256,24 @@ 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)))
+ new_submission.language = self.language
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/src/judge0/utils.py b/src/judge0/utils.py
new file mode 100644
index 0000000..e38b41f
--- /dev/null
+++ b/src/judge0/utils.py
@@ -0,0 +1,48 @@
+"""Module containing different utility functions for Judge0 Python SDK."""
+
+from functools import wraps
+from http import HTTPStatus
+
+from requests import HTTPError
+
+from .errors import PreviewClientLimitError
+
+
+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 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
+ else:
+ raise err from None
+ except Exception as err:
+ raise err from None
+
+ return wrapper
diff --git a/tests/conftest.py b/tests/conftest.py
index b1bd612..4e1547c 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,74 +1,147 @@
+import json
import os
import pytest
from dotenv import load_dotenv
-from judge0 import clients
+from judge0 import clients, RegularPeriodRetry
load_dotenv()
@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")
- client = clients.Client(
- endpoint=endpoint,
- auth_headers={api_key_header: api_key},
- )
- return client
+def custom_ce_client():
+ endpoint = os.getenv("JUDGE0_CE_ENDPOINT")
+ auth_headers = os.getenv("JUDGE0_CE_AUTH_HEADERS")
+
+ if endpoint is None or auth_headers is None:
+ return None
+ else:
+ return clients.Client(endpoint=endpoint, auth_headers=json.loads(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")
- client = clients.Client(
- endpoint=endpoint,
- auth_headers={api_key_header: api_key},
- )
- return client
+def custom_extra_ce_client():
+ endpoint = os.getenv("JUDGE0_EXTRA_CE_ENDPOINT")
+ auth_headers = os.getenv("JUDGE0_EXTRA_CE_AUTH_HEADERS")
+
+ if endpoint is None or auth_headers is None:
+ return None
+ else:
+ return clients.Client(endpoint=endpoint, auth_headers=json.loads(auth_headers))
@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")
- 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")
- client = clients.SuluJudge0ExtraCE(api_key)
- return client
+
+ if api_key is None:
+ return None
+ else:
+ return clients.SuluJudge0ExtraCE(api_key)
+
+
+@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")
+def ce_client(
+ custom_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
+ 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
+ if preview_ce_client is not None:
+ return preview_ce_client
+
+ pytest.fail("No CE client available for testing. This error should not happen!")
+
+
+@pytest.fixture(scope="session")
+def extra_ce_client(
+ custom_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
+ 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
+ if preview_extra_ce_client is not None:
+ return preview_extra_ce_client
+
+ pytest.fail(
+ "No Extra CE client available for testing. This error should not happen!"
+ )
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)
diff --git a/tests/test_api_test_cases.py b/tests/test_api_test_cases.py
index f395279..8dea1e6 100644
--- a/tests/test_api_test_cases.py
+++ b/tests/test_api_test_cases.py
@@ -1,9 +1,55 @@
-"""Separate file containg tests related to test case functionality."""
+"""Separate file containing tests related to test case functionality."""
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(
+ "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,
+ None,
+ ],
+ ],
+)
+def test_test_case_from_record(test_case, expected_output):
+ assert TestCase.from_record(test_case) == expected_output
@pytest.mark.parametrize(
@@ -19,7 +65,95 @@ 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
+
+
+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(
@@ -39,14 +173,20 @@ def test_create_submissions_from_test_cases_return_type(
[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"),
@@ -55,8 +195,14 @@ def test_create_submissions_from_test_cases_return_type(
],
[
[
- 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"),
@@ -74,13 +220,14 @@ def test_create_submissions_from_test_cases_return_type(
def test_test_cases_from_run(
source_code_or_submissions, test_cases, expected_status, request
):
- client = request.getfixturevalue("judge0_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(
@@ -98,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",
),
@@ -107,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",
),
@@ -121,7 +271,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("ce_client")
submissions = judge0.run(
client=client,
@@ -136,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("judge0_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 c204bcb..8675034 100644
--- a/tests/test_submission.py
+++ b/tests/test_submission.py
@@ -1,9 +1,61 @@
-from judge0 import Status, Submission, wait
+from base64 import b64decode
+
+from judge0 import run, Status, Submission, wait
+from judge0.base_types import LanguageAlias
+
+
+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!")')
+ client = request.getfixturevalue("ce_client")
+ submission = Submission(
+ source_code='print("Hello World!")', language=LanguageAlias.PYTHON
+ )
assert submission.status is None
@@ -15,8 +67,10 @@ def test_status_before_and_after_submission(request):
def test_is_done(request):
- client = request.getfixturevalue("judge0_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
@@ -24,3 +78,39 @@ 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("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
+
+
+def test_language_executable(request):
+ client = request.getfixturevalue("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"