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"