├── requirements.txt ├── joj_submitter ├── __init__.py └── __main__.py ├── mypy.ini ├── .github └── workflows │ └── pypi.yml ├── .pre-commit-config.yaml ├── LICENSE.txt ├── setup.py ├── README.md └── .gitignore /requirements.txt: -------------------------------------------------------------------------------- 1 | beautifulsoup4>=4.9.3 2 | colorama>=0.4.4 3 | pydantic>=1.8.1 4 | requests>=2.32.2 5 | typer[all]>=0.3.2 6 | urllib3>=2.2.2 # not directly required, pinned by Snyk to avoid a vulnerability 7 | zipp>=3.19.1 # not directly required, pinned by Snyk to avoid a vulnerability 8 | -------------------------------------------------------------------------------- /joj_submitter/__init__.py: -------------------------------------------------------------------------------- 1 | from joj_submitter.__main__ import Detail as Detail 2 | from joj_submitter.__main__ import JOJSubmitter as JOJSubmitter 3 | from joj_submitter.__main__ import Language as Language 4 | from joj_submitter.__main__ import Record as Record 5 | from joj_submitter.__main__ import app 6 | 7 | 8 | def main() -> None: 9 | app() 10 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | plugins = pydantic.mypy 3 | 4 | follow_imports = silent 5 | warn_redundant_casts = True 6 | warn_unused_ignores = True 7 | disallow_any_generics = True 8 | check_untyped_defs = True 9 | no_implicit_reexport = True 10 | 11 | # for strict mypy: (this is the tricky one :-)) 12 | disallow_untyped_defs = True 13 | 14 | [pydantic-mypy] 15 | init_forbid_extra = True 16 | init_typed = True 17 | warn_required_dynamic_aliases = True 18 | warn_untyped_fields = True 19 | -------------------------------------------------------------------------------- /.github/workflows/pypi.yml: -------------------------------------------------------------------------------- 1 | name: Publish Package to Pypi 2 | 3 | on: push 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v3 12 | - name: Set up Python 3.10 13 | uses: actions/setup-python@v3 14 | with: 15 | python-version: "3.10" 16 | - name: Install pypa/build 17 | run: >- 18 | python -m 19 | pip install 20 | build 21 | --user 22 | - name: Build a binary wheel and a source tarball 23 | run: >- 24 | python -m 25 | build 26 | --sdist 27 | --wheel 28 | --outdir dist/ 29 | . 30 | - name: Publish package 31 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') 32 | uses: pypa/gh-action-pypi-publish@release/v1 33 | with: 34 | password: ${{ secrets.PYPI_API_TOKEN }} 35 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.3.0 4 | hooks: 5 | - id: check-yaml 6 | - id: end-of-file-fixer 7 | - id: trailing-whitespace 8 | - id: requirements-txt-fixer 9 | - repo: https://github.com/pre-commit/mirrors-mypy 10 | rev: "v0.982" 11 | hooks: 12 | - id: mypy 13 | additional_dependencies: 14 | - pydantic 15 | - types-requests 16 | - repo: https://github.com/asottile/pyupgrade 17 | rev: v3.1.0 18 | hooks: 19 | - id: pyupgrade 20 | - repo: https://github.com/hadialqattan/pycln 21 | rev: v2.1.1 22 | hooks: 23 | - id: pycln 24 | - repo: https://github.com/PyCQA/isort 25 | rev: 5.10.1 26 | hooks: 27 | - id: isort 28 | args: ["--profile", "black", "--filter-files"] 29 | - repo: https://github.com/psf/black 30 | rev: 22.10.0 31 | hooks: 32 | - id: black 33 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 BoYanZh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | from typing import List 4 | 5 | from setuptools import find_packages, setup 6 | 7 | 8 | def get_version(package: str) -> str: 9 | """ 10 | Return package version as listed in `__version__` in `__main__.py`. 11 | """ 12 | path = os.path.join(package, "__main__.py") 13 | main_py = open(path, "r", encoding="utf8").read() 14 | match = re.search("__version__ = ['\"]([^'\"]+)['\"]", main_py) 15 | if match is None: 16 | return "0.0.0" 17 | return match.group(1) 18 | 19 | 20 | def get_long_description() -> str: 21 | """ 22 | Return the README. 23 | """ 24 | return open("README.md", "r", encoding="utf8").read() 25 | 26 | 27 | def get_packages(package: str) -> List[str]: 28 | """ 29 | Return root package and all sub-packages. 30 | """ 31 | return [ 32 | dirpath 33 | for dirpath, dirnames, filenames in os.walk(package) 34 | if os.path.exists(os.path.join(dirpath, "__init__.py")) 35 | ] 36 | 37 | 38 | def get_install_requires() -> List[str]: 39 | return open("requirements.txt").read().splitlines() 40 | 41 | 42 | setup( 43 | name="joj-submitter", 44 | version=get_version("joj_submitter"), 45 | url="https://github.com/BoYanZh/JOJ-Submitter", 46 | license="MIT", 47 | description="Submit your work to JOJ via cli.", 48 | long_description=get_long_description(), 49 | long_description_content_type="text/markdown", 50 | author="BoYanZh", 51 | author_email="bomingzh@sjtu.edu.cn", 52 | maintainer="BoYanZh", 53 | maintainer_email="bomingzh@sjtu.edu.cn", 54 | packages=find_packages(), 55 | python_requires=">=3.6", 56 | entry_points={"console_scripts": ["joj-submit=joj_submitter:main"]}, 57 | project_urls={ 58 | "Bug Reports": "https://github.com/BoYanZh/JOJ-Submitter/issues", 59 | "Source": "https://github.com/BoYanZh/JOJ-Submitter", 60 | }, 61 | install_requires=[ 62 | "beautifulsoup4>=4.9.3", 63 | "pydantic>=1.8.1", 64 | "requests>=2.25.1", 65 | "typer[all]>=0.3.2", 66 | ], 67 | ) 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JOJ-Submitter 2 | 3 | Submit your work to JOJ via CLI. Greatly improve your efficiency when uploading to JOJ. 4 | 5 | ## Getting Started 6 | 7 | ### Setup venv (Optional) 8 | 9 | ```bash 10 | python3 -m venv env 11 | source env/Scripts/activate 12 | ``` 13 | 14 | ### Install 15 | 16 | ```bash 17 | $ pip install joj-submitter 18 | $ joj-submit --help 19 | Usage: joj-submit [OPTIONS] PROBLEM_URL COMPRESSED_FILE_PATH 20 | LANG:[c|cc|llvm-c|llvm-cc|cmake|make|ocaml|matlab|cs|pas|jav 21 | a|py|py3|octave|php|rs|hs|js|go|rb|other] [SID] 22 | 23 | Arguments: 24 | PROBLEM_URL [required] 25 | COMPRESSED_FILE_PATH [required] 26 | LANG:[c|cc|llvm-c|llvm-cc|cmake|make|ocaml|matlab|cs|pas|java|py|py3|octave|php|rs|hs|js|go|rb|other] 27 | other: other | c: C | cc: C++ | llvm-c: C 28 | (Clang, with memory check) | llvm-cc: C++ 29 | (Clang++, with memory check) | cmake: CMake 30 | | make: GNU Make | ocaml: OCaml | matlab: 31 | MATLAB | cs: C# | pas: Pascal | java: Java | 32 | py: Python | py3: Python 3 | octave: Octave 33 | | php: PHP | rs: Rust | hs: Haskell | js: 34 | JavaScript | go: Go | rb: Ruby [required] 35 | 36 | [SID] [env var: JOJ_SID;default: ] 37 | 38 | Options: 39 | -s, --skip Return immediately once uploaded. [default: False] 40 | -a, --all Show detail of all cases, even accepted. [default: False] 41 | -d, --detail Show stderr, Your answer and JOJ answer section. [default: False] 42 | -j, --json Print the result in json format to stdout. [default: False] 43 | --version Show version. 44 | --help Show this message and exit. 45 | ``` 46 | 47 | ### Example 48 | 49 | First get your JOJ_SID with or via browser on your own. 50 | 51 | Replace `` in the following methods with your actual SID. 52 | 53 | #### Method 1 Call directly via CLI 54 | 55 | 1. Mark `JOJ_SID` shell variable. 56 | 2. Run `joj-submit` with arguments. 57 | 58 | ```bash 59 | $ export JOJ_SID= 60 | $ joj-submit https://joj.sjtu.edu.cn/d/vg101_fall_2020_manuel/homework/5fb1f1379fedcc0006622a06/5fb1ee8b9fedcc00066229d9 ans.zip cc 61 | ans.zip upload succeed, record url https://joj.sjtu.edu.cn/d/vg101_fall_2020_manuel/records/60e42b17597d580006c571d6 62 | status: Accepted, accept number: 49, score: 980, total time: 6167 ms, peak memory: 33.684 MiB 63 | ``` 64 | 65 | #### Method 2 Call from Makefile 66 | 67 | 1. Add `export JOJ_SID=` to your `~/.bashrc` or `~/.zshrc`. Do not forget to restart the shell to load the variable. 68 | 2. Edit and add this Makefile to your project . 69 | 3. Run `make joj`. 70 | 71 | ```bash 72 | $ make joj 73 | tar -cvzf p4-src.tar.gz main.cpp 74 | main.cpp 75 | joj-submit https://joj.sjtu.edu.cn/d/ve281_summer_2021_hongyi/homework/60ed8820597d590006d91e44/60ed869b597d590006d91dad p4-src.tar.gz cc -w 76 | p4-src.tar.gz upload succeed, record url https://joj.sjtu.edu.cn/d/ve281_summer_2021_hongyi/records/60f4451537f07210064b8c20 77 | status: Accepted, accept number: 49, score: 980, total time: 6167 ms, peak memory: 33.684 MiB 78 | ``` 79 | 80 | ## Acknowledgements 81 | 82 | - [VG101-Grade-Helper](https://github.com/BoYanZh/VG101-Grade-Helper) 83 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Python template 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | share/python-wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .nox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | *.py,cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | cover/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | db.sqlite3-journal 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | .pybuilder/ 78 | target/ 79 | 80 | # Jupyter Notebook 81 | .ipynb_checkpoints 82 | 83 | # IPython 84 | profile_default/ 85 | ipython_config.py 86 | 87 | # pyenv 88 | # For a library or package, you might want to ignore these files since the code is 89 | # intended to run in multiple environments; otherwise, check them in: 90 | # .python-version 91 | 92 | # pipenv 93 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 94 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 95 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 96 | # install all needed dependencies. 97 | #Pipfile.lock 98 | 99 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 100 | __pypackages__/ 101 | 102 | # Celery stuff 103 | celerybeat-schedule 104 | celerybeat.pid 105 | 106 | # SageMath parsed files 107 | *.sage.py 108 | 109 | # Environments 110 | .env 111 | .venv 112 | env/ 113 | venv/ 114 | ENV/ 115 | env.bak/ 116 | venv.bak/ 117 | 118 | # Spyder project settings 119 | .spyderproject 120 | .spyproject 121 | 122 | # Rope project settings 123 | .ropeproject 124 | 125 | # mkdocs documentation 126 | /site 127 | 128 | # mypy 129 | .mypy_cache/ 130 | .dmypy.json 131 | dmypy.json 132 | 133 | # Pyre type checker 134 | .pyre/ 135 | 136 | # pytype static type analyzer 137 | .pytype/ 138 | 139 | # Cython debug symbols 140 | cython_debug/ 141 | 142 | 143 | # Created by https://www.toptal.com/developers/gitignore/api/vscode,python 144 | # Edit at https://www.toptal.com/developers/gitignore?templates=vscode,python 145 | 146 | ### Python ### 147 | # Byte-compiled / optimized / DLL files 148 | __pycache__/ 149 | *.py[cod] 150 | *$py.class 151 | 152 | # C extensions 153 | *.so 154 | 155 | # Distribution / packaging 156 | .Python 157 | build/ 158 | develop-eggs/ 159 | dist/ 160 | downloads/ 161 | eggs/ 162 | .eggs/ 163 | lib/ 164 | lib64/ 165 | parts/ 166 | sdist/ 167 | var/ 168 | wheels/ 169 | pip-wheel-metadata/ 170 | share/python-wheels/ 171 | *.egg-info/ 172 | .installed.cfg 173 | *.egg 174 | MANIFEST 175 | 176 | # PyInstaller 177 | # Usually these files are written by a python script from a template 178 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 179 | *.manifest 180 | *.spec 181 | 182 | # Installer logs 183 | pip-log.txt 184 | pip-delete-this-directory.txt 185 | 186 | # Unit test / coverage reports 187 | htmlcov/ 188 | .tox/ 189 | .nox/ 190 | .coverage 191 | .coverage.* 192 | .cache 193 | nosetests.xml 194 | coverage.xml 195 | *.cover 196 | *.py,cover 197 | .hypothesis/ 198 | .pytest_cache/ 199 | pytestdebug.log 200 | 201 | # Translations 202 | *.mo 203 | *.pot 204 | 205 | # Django stuff: 206 | *.log 207 | local_settings.py 208 | db.sqlite3 209 | db.sqlite3-journal 210 | 211 | # Flask stuff: 212 | instance/ 213 | .webassets-cache 214 | 215 | # Scrapy stuff: 216 | .scrapy 217 | 218 | # Sphinx documentation 219 | docs/_build/ 220 | doc/_build/ 221 | 222 | # PyBuilder 223 | target/ 224 | 225 | # Jupyter Notebook 226 | .ipynb_checkpoints 227 | 228 | # IPython 229 | profile_default/ 230 | ipython_config.py 231 | 232 | # pyenv 233 | .python-version 234 | 235 | # pipenv 236 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 237 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 238 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 239 | # install all needed dependencies. 240 | #Pipfile.lock 241 | 242 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 243 | __pypackages__/ 244 | 245 | # Celery stuff 246 | celerybeat-schedule 247 | celerybeat.pid 248 | 249 | # SageMath parsed files 250 | *.sage.py 251 | 252 | # Environments 253 | .env 254 | .venv 255 | env/ 256 | venv/ 257 | ENV/ 258 | env.bak/ 259 | venv.bak/ 260 | pythonenv* 261 | 262 | # Spyder project settings 263 | .spyderproject 264 | .spyproject 265 | 266 | # Rope project settings 267 | .ropeproject 268 | 269 | # mkdocs documentation 270 | /site 271 | 272 | # mypy 273 | .mypy_cache/ 274 | .dmypy.json 275 | dmypy.json 276 | 277 | # Pyre type checker 278 | .pyre/ 279 | 280 | # pytype static type analyzer 281 | .pytype/ 282 | 283 | # profiling data 284 | .prof 285 | 286 | ### vscode ### 287 | .vscode/* 288 | # !.vscode/settings.json 289 | !.vscode/tasks.json 290 | !.vscode/launch.json 291 | !.vscode/extensions.json 292 | *.code-workspace 293 | 294 | # End of https://www.toptal.com/developers/gitignore/api/vscode,python 295 | 296 | pypi.sh 297 | -------------------------------------------------------------------------------- /joj_submitter/__main__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import time 3 | from enum import Enum 4 | from typing import IO, AnyStr, Dict, List, Optional 5 | 6 | import requests 7 | import typer 8 | from bs4 import BeautifulSoup 9 | from colorama import Fore, Style, init 10 | from pydantic import BaseModel, FilePath, HttpUrl, ValidationError 11 | from requests.models import Response 12 | 13 | __version__ = "0.0.10" 14 | app = typer.Typer(add_completion=False) 15 | logging.basicConfig(format="%(message)s", datefmt="%m-%d %H:%M:%S", level=logging.INFO) 16 | 17 | 18 | class Language(str, Enum): 19 | c = "c" 20 | cc = "cc" 21 | llvm_c = "llvm-c" 22 | llvm_cc = "llvm-cc" 23 | cmake = "cmake" 24 | make = "make" 25 | ocaml = "ocaml" 26 | matlab = "matlab" 27 | cs = "cs" 28 | pas = "pas" 29 | java = "java" 30 | py = "py" 31 | py3 = "py3" 32 | octave = "octave" 33 | php = "php" 34 | rs = "rs" 35 | hs = "hs" 36 | js = "js" 37 | go = "go" 38 | rb = "rb" 39 | other = "other" 40 | 41 | 42 | class Detail(BaseModel): 43 | status: str 44 | extra_info: str 45 | time_cost: str 46 | memory_cost: str 47 | stderr: str 48 | out: str 49 | ans: str 50 | 51 | 52 | class Record(BaseModel): 53 | status: str 54 | accepted_count: int 55 | score: str 56 | total_time: str 57 | peak_memory: str 58 | details: List[Detail] = [] 59 | compiler_text: str 60 | 61 | 62 | class JOJSubmitter: 63 | def __init__(self, sid: str, logger: logging.Logger = logging.getLogger()) -> None: 64 | def create_sess(cookies: Dict[str, str]) -> requests.Session: 65 | s = requests.Session() 66 | s.cookies.update(cookies) 67 | return s 68 | 69 | self.sess = create_sess( 70 | cookies={"sid": sid, "JSESSIONID": "dummy", "save": "1"} 71 | ) 72 | self.logger = logger 73 | assert ( 74 | self.sess.get("https://joj.sjtu.edu.cn/login/jaccount").status_code == 200 75 | ), "Unauthorized SID" 76 | 77 | def upload_file(self, problem_url: str, file: IO[AnyStr], lang: str) -> Response: 78 | post_url = problem_url 79 | if not post_url.endswith("/submit"): 80 | post_url += "/submit" 81 | html = self.sess.get(post_url).text 82 | soup = BeautifulSoup(html, features="html.parser") 83 | csrf_token_node = soup.find("input", {"name": "csrf_token"}) 84 | assert csrf_token_node, "Invalid problem" 85 | csrf_token = csrf_token_node.get("value") 86 | response = self.sess.post( 87 | post_url, 88 | files={"code": file}, 89 | data={"csrf_token": csrf_token, "lang": lang}, 90 | ) 91 | return response 92 | 93 | def get_status(self, url: str) -> Record: 94 | while True: 95 | html = self.sess.get(url).text 96 | soup = BeautifulSoup(html, features="html.parser") 97 | status = ( 98 | soup.select_one( 99 | "#status > div.section__header > h1 > span:nth-child(2)" 100 | ) 101 | .get_text() 102 | .strip() 103 | ) 104 | if status not in ["Waiting", "Compiling", "Fetched", "Running"]: 105 | break 106 | else: 107 | time.sleep(1) 108 | summaries = [ 109 | item.get_text() for item in soup.select_one("#summary").find_all("dd") 110 | ] 111 | summaries[1] = summaries[1].replace("ms", " ms") 112 | compiler_text = soup.select_one(".compiler-text").get_text().strip() 113 | details = [] 114 | accepted_count = 0 115 | body = soup.select_one("#status").find("div", class_="section__body no-padding") 116 | table_rows = body.table.tbody.find_all("tr") if body else [] 117 | for detail_tr in table_rows: 118 | status_soup = detail_tr.find("td", class_="col--status typo") 119 | status_soup_span = status_soup.find_all("span") 120 | detail_status = status_soup_span[1].get_text().strip() 121 | accepted_count += "Accepted" == detail_status 122 | time_cost = ( 123 | detail_tr.find("td", class_="col--time") 124 | .get_text() 125 | .strip() 126 | .replace("ms", " ms") 127 | ) 128 | memory_cost = detail_tr.find("td", class_="col--memory").get_text().strip() 129 | stderr, out, ans, extra_info = "", "", "", "" 130 | if status_soup.find("a") is not None: # results are not hidden 131 | detail_url = "https://joj.sjtu.edu.cn" + status_soup.find("a").get( 132 | "href" 133 | ) 134 | detail_html = self.sess.get(detail_url).text 135 | detail_soup = BeautifulSoup(detail_html, features="html.parser") 136 | detail_compiler_text = detail_soup.find_all( 137 | "pre", class_="compiler-text" 138 | ) 139 | stderr = detail_compiler_text[0].get_text().strip() 140 | out = detail_compiler_text[1].get_text().strip() 141 | ans = detail_compiler_text[2].get_text().strip() 142 | extra_info = ( 143 | status_soup_span[2].get_text().strip() 144 | if len(status_soup_span) >= 3 145 | else "" 146 | ) 147 | if extra_info: 148 | extra_info = " " + extra_info 149 | details.append( 150 | Detail( 151 | status=detail_status, 152 | extra_info=extra_info, 153 | time_cost=time_cost, 154 | memory_cost=memory_cost, 155 | stderr=stderr, 156 | out=out, 157 | ans=ans, 158 | ) 159 | ) 160 | return Record( 161 | status=status, 162 | accepted_count=accepted_count, 163 | score=summaries[0], 164 | total_time=summaries[1], 165 | peak_memory=summaries[2], 166 | details=details, 167 | compiler_text=compiler_text, 168 | ) 169 | 170 | def submit( 171 | self, 172 | problem_url: str, 173 | file_path: str, 174 | lang: str, 175 | no_wait: bool, 176 | show_all: bool, 177 | show_compiler_text: bool, 178 | show_detail: bool, 179 | output_json: bool, 180 | ) -> bool: 181 | file = open(file_path, "rb") 182 | response = self.upload_file(problem_url, file, lang) 183 | assert ( 184 | response.status_code == 200 185 | ), f"Upload error with code {response.status_code}" 186 | self.logger.info(f"{file_path} upload succeed, record url {response.url}") 187 | if no_wait: 188 | return True 189 | res = self.get_status(response.url) 190 | if output_json: 191 | print(res.json()) 192 | fore_color = Fore.RED if res.status != "Accepted" else Fore.GREEN 193 | self.logger.info( 194 | f"status: {fore_color}{res.status}{Style.RESET_ALL}, " 195 | + f"accept number: {Fore.BLUE}{res.accepted_count}{Style.RESET_ALL}, " 196 | + f"score: {Fore.BLUE}{res.score}{Style.RESET_ALL}, " 197 | + f"total time: {Fore.BLUE}{res.total_time}{Style.RESET_ALL}, " 198 | + f"peak memory: {Fore.BLUE}{res.peak_memory}{Style.RESET_ALL}" 199 | ) 200 | if show_compiler_text and res.compiler_text: 201 | self.logger.info("compiler text:") 202 | self.logger.info(res.compiler_text) 203 | if (res.status != "Accepted" or show_all) and res.details: 204 | self.logger.info("details:") 205 | for i, detail in enumerate(res.details): 206 | status_string: str = "" 207 | print_status: bool = False 208 | if show_all and detail.status == "Accepted": 209 | status_string = ( 210 | f"#{i + 1}: {Fore.GREEN}{detail.status}{Style.RESET_ALL}, " 211 | ) 212 | print_status = True 213 | elif detail.status != "Accepted": 214 | status_string = f"#{i + 1}: {Fore.RED}{detail.status}{Style.RESET_ALL}{detail.extra_info}, " 215 | print_status = True 216 | if print_status: 217 | self.logger.info( 218 | status_string 219 | + f"time: {Fore.BLUE}{detail.time_cost}{Style.RESET_ALL}, " 220 | + f"memory: {Fore.BLUE}{detail.memory_cost}{Style.RESET_ALL}" 221 | ) 222 | if show_detail: 223 | self.logger.info("Stderr:") 224 | if detail.stderr: 225 | self.logger.info(detail.stderr) 226 | self.logger.info("") 227 | self.logger.info("Your Answer:") 228 | if detail.out: 229 | self.logger.info(detail.out) 230 | self.logger.info("") 231 | self.logger.info("JOJ Answer:") 232 | if detail.ans: 233 | self.logger.info(detail.ans) 234 | self.logger.info("") 235 | return res.status == "Accepted" 236 | 237 | 238 | lang_dict = { 239 | "other": "other", 240 | "c": "C", 241 | "cc": "C++", 242 | "llvm-c": "C (Clang, with memory check)", 243 | "llvm-cc": "C++ (Clang++, with memory check)", 244 | "cmake": "CMake", 245 | "make": "GNU Make", 246 | "ocaml": "OCaml", 247 | "matlab": "MATLAB", 248 | "cs": "C#", 249 | "pas": "Pascal", 250 | "java": "Java", 251 | "py": "Python", 252 | "py3": "Python 3", 253 | "octave": "Octave", 254 | "php": "PHP", 255 | "rs": "Rust", 256 | "hs": "Haskell", 257 | "js": "JavaScript", 258 | "go": "Go", 259 | "rb": "Ruby", 260 | } 261 | 262 | 263 | class arguments(BaseModel): 264 | problem_url: HttpUrl 265 | compressed_file_path: FilePath 266 | lang: Language 267 | sid: str 268 | no_wait: bool 269 | show_all: bool 270 | show_compiler_text: bool 271 | show_detail: bool 272 | output_json: bool 273 | 274 | 275 | def version_callback(value: bool) -> None: 276 | if value: 277 | typer.echo(__version__) 278 | raise typer.Exit() 279 | 280 | 281 | @app.command() 282 | def main( 283 | problem_url: str, 284 | compressed_file_path: str, 285 | lang: Language = typer.Argument( 286 | ..., help=" | ".join([f"{k}: {v}" for k, v in lang_dict.items()]) 287 | ), 288 | sid: str = typer.Argument("", envvar="JOJ_SID"), 289 | no_wait: bool = typer.Option( 290 | False, "-s", "--skip", help="Return immediately once uploaded." 291 | ), 292 | show_all: bool = typer.Option( 293 | False, "-a", "--all", help="Show detail of all cases, even accepted." 294 | ), 295 | show_compiler_text: bool = typer.Option( 296 | False, "-c", "--compiler", help="Show compiler text section." 297 | ), 298 | show_detail: bool = typer.Option( 299 | False, "-d", "--detail", help="Show stderr, Your answer and JOJ answer section." 300 | ), 301 | output_json: bool = typer.Option( 302 | False, "-j", "--json", help="Print the result in json format to stdout." 303 | ), 304 | version: Optional[bool] = typer.Option( 305 | None, "--version", callback=version_callback, help="Show version." 306 | ), 307 | ) -> None: 308 | try: 309 | arguments( 310 | problem_url=problem_url, # type: ignore 311 | compressed_file_path=compressed_file_path, # type: ignore 312 | lang=lang, 313 | sid=sid, 314 | no_wait=no_wait, 315 | show_all=show_all, 316 | show_compiler_text=show_compiler_text, 317 | show_detail=show_detail, 318 | output_json=output_json, 319 | ) 320 | assert sid and sid != "", "Empty SID" 321 | worker = JOJSubmitter(sid) 322 | accepted = worker.submit( 323 | problem_url, 324 | compressed_file_path, 325 | lang.value, 326 | no_wait, 327 | show_all, 328 | show_compiler_text, 329 | show_detail, 330 | output_json, 331 | ) 332 | raise typer.Exit(not accepted) 333 | except ValidationError as e: 334 | logging.error(f"Error: {e}") 335 | raise typer.Exit(128) 336 | except AssertionError as e: 337 | logging.error(f"Error: {e.args[0]}") 338 | raise typer.Exit(128) 339 | 340 | 341 | if __name__ == "__main__": 342 | init() 343 | app() 344 | --------------------------------------------------------------------------------