onlinecompiler 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- onlinecompiler-0.1.0/.gitignore +3 -0
- onlinecompiler-0.1.0/LICENSE +21 -0
- onlinecompiler-0.1.0/PKG-INFO +285 -0
- onlinecompiler-0.1.0/README.md +262 -0
- onlinecompiler-0.1.0/pyproject.toml +39 -0
- onlinecompiler-0.1.0/src/onlinecompiler/__init__.py +67 -0
- onlinecompiler-0.1.0/src/onlinecompiler/_client.py +622 -0
- onlinecompiler-0.1.0/src/onlinecompiler/_verify.py +206 -0
- onlinecompiler-0.1.0/src/onlinecompiler/py.typed +0 -0
- onlinecompiler-0.1.0/tests/test_client.py +496 -0
- onlinecompiler-0.1.0/tests/test_verify.py +245 -0
- onlinecompiler-0.1.0/uv.lock +234 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 RunCode Inc.
|
|
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.
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: onlinecompiler
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python client for the OnlineCompiler.io code execution API: run and judge code in 46 languages in a sandbox
|
|
5
|
+
Project-URL: Homepage, https://onlinecompiler.io
|
|
6
|
+
Project-URL: Documentation, https://onlinecompiler.io/docs
|
|
7
|
+
Project-URL: Pricing, https://onlinecompiler.io/pricing
|
|
8
|
+
Project-URL: Changelog, https://onlinecompiler.io/changelog
|
|
9
|
+
Author: RunCode Inc.
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: code execution,code judge,compiler api,judge0,online compiler,online judge,onlinecompiler,sandbox,signed receipts,webhooks
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Topic :: Software Development :: Compilers
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Provides-Extra: receipts
|
|
21
|
+
Requires-Dist: cryptography>=42; extra == 'receipts'
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# OnlineCompiler.io Python client
|
|
25
|
+
|
|
26
|
+
Run code in 46 languages from Python, or judge it against test cases, through the
|
|
27
|
+
[OnlineCompiler.io](https://onlinecompiler.io/?utm_source=pypi) code execution API. Every run gets a
|
|
28
|
+
fresh gVisor sandbox with no network, 512 MB of memory and a 30-second limit. Free up to 100,000 runs
|
|
29
|
+
a month, commercial use included.
|
|
30
|
+
|
|
31
|
+
No dependencies beyond the standard library. Python 3.10 or newer. Verifying signed receipts needs
|
|
32
|
+
one optional extra, `pip install "onlinecompiler[receipts]"` (it adds `cryptography`).
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install onlinecompiler
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Get a free API key at https://api.onlinecompiler.io/compilers/api-keys/ and set it as
|
|
39
|
+
`ONLINECOMPILER_API_KEY`, or pass it to the client.
|
|
40
|
+
|
|
41
|
+
## Run a program
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from onlinecompiler import OnlineCompiler
|
|
45
|
+
|
|
46
|
+
client = OnlineCompiler() # reads ONLINECOMPILER_API_KEY
|
|
47
|
+
|
|
48
|
+
result = client.run("python-3.14", "name = input()\nprint(f'Hello, {name}!')", input="Ada")
|
|
49
|
+
print(result["output"]) # Hello, Ada!
|
|
50
|
+
print(result["exit_code"], result["time"], result["memory"])
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The result has `output` (stdout), `error` (stderr or compiler errors), `exit_code`, `signal`, `time`
|
|
54
|
+
and `total` (seconds), `memory` (KB), `truncated`, `time_limit` and `files`. `time`, `total` and
|
|
55
|
+
`memory` are decimal strings, as the API sends them. Compiler ids such as `gcc-15`, `openjdk-25` and
|
|
56
|
+
`nodejs-24` come from `client.compilers()`. Pass `time_limit` (seconds, up to 30) to stop a run sooner.
|
|
57
|
+
|
|
58
|
+
## Judge a solution against test cases
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
result = client.run_tests(
|
|
62
|
+
"python-3.14",
|
|
63
|
+
"a, b = map(int, input().split())\nprint(a + b)",
|
|
64
|
+
tests=[
|
|
65
|
+
{"input": "1 2", "expected_output": "3"},
|
|
66
|
+
{"input": "10 20", "expected_output": "30", "hidden": True},
|
|
67
|
+
],
|
|
68
|
+
)
|
|
69
|
+
print(result["verdict"], result["score"], f"{result['passed']}/{result['total']}")
|
|
70
|
+
for case in result["tests"]:
|
|
71
|
+
print(case["verdict"])
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Each test counts as one run. Hidden tests return only their verdict. `checker` picks how output is
|
|
75
|
+
compared (`trim` by default, `exact`, `tokens`, `float` or `rows`), `scoring` is `all_or_nothing`
|
|
76
|
+
(default), `percent` or `weighted` (with a `weight` per test), and `time_limit` is seconds per test
|
|
77
|
+
(default 2). See the [run-tests docs](https://onlinecompiler.io/docs/run-tests?utm_source=pypi).
|
|
78
|
+
|
|
79
|
+
Every run-tests result also has `summary` (the result in plain words), `submission` (its id) and a
|
|
80
|
+
signed `receipt`.
|
|
81
|
+
|
|
82
|
+
## Judge in the background
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
queued = client.run_tests(
|
|
86
|
+
"python-3.14",
|
|
87
|
+
"a, b = map(int, input().split())\nprint(a + b)",
|
|
88
|
+
tests=[{"input": "1 2", "expected_output": "3"}, {"input": "10 20", "expected_output": "30", "hidden": True}],
|
|
89
|
+
async_=True,
|
|
90
|
+
external_ref="attempt-4821",
|
|
91
|
+
idempotency_key="attempt-4821",
|
|
92
|
+
)
|
|
93
|
+
done = client.wait_for_submission(queued["submission"]) # looks it up every 2 seconds
|
|
94
|
+
result = done["result"] # None when it failed, which means it was not judged
|
|
95
|
+
if result is not None:
|
|
96
|
+
print(result["verdict"], result["score"])
|
|
97
|
+
else:
|
|
98
|
+
print("Not judged:", done["error"]) # send it again
|
|
99
|
+
client.delete_submission(done["submission"]) # optional: drop the stored code and output
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`async_=True` (`async` is a Python keyword) answers at once with the submission id, and the submission
|
|
103
|
+
waits in your account's queue instead of getting 429 in a burst. Add `callback_url` to have the result
|
|
104
|
+
POSTed to your server as a signed webhook (see below); it needs `async_=True`, and the client refuses it
|
|
105
|
+
without. `external_ref` is your own id, signed into the receipt; `metadata` is any JSON object, returned in
|
|
106
|
+
lookups and webhooks. `idempotency_key` (1 to 100 printable ASCII characters, checked before sending) is
|
|
107
|
+
sent as the `Idempotency-Key` header, so a retry after a timeout answers with the original result instead
|
|
108
|
+
of running again. `client.submission(id)` looks a submission up once.
|
|
109
|
+
|
|
110
|
+
## Judge against a stored problem
|
|
111
|
+
|
|
112
|
+
A stored problem keeps its tests, checker and limits on the server, so you send only the candidate's
|
|
113
|
+
code and the hidden tests never leave it.
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
client.create_problem({
|
|
117
|
+
"title": "Sum of two numbers",
|
|
118
|
+
"slug": "sum",
|
|
119
|
+
"languages": ["python-3.14"],
|
|
120
|
+
"tests": [
|
|
121
|
+
{"name": "sample", "input": "1 2\n", "expected_output": "3\n", "hidden": False},
|
|
122
|
+
{"input": "5 7\n", "expected_output": "12\n"}, # tests are hidden unless you say otherwise
|
|
123
|
+
],
|
|
124
|
+
})
|
|
125
|
+
client.publish_problem("sum") # no reference solution, so it is published at once
|
|
126
|
+
|
|
127
|
+
result = client.judge("sum", "python-3.14", "a, b = map(int, input().split())\nprint(a + b)")
|
|
128
|
+
print(result["verdict"], result["score"], result["problem"], result["version"])
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
`judge` takes the problem's id (`prob_...`) or your slug for it. `mode="samples"` judges only the
|
|
132
|
+
visible tests, and `async_`, `callback_url`, `external_ref`, `metadata` and `idempotency_key` work as
|
|
133
|
+
above. Problems are plain dicts, as the
|
|
134
|
+
[stored problems docs](https://onlinecompiler.io/docs/problems?utm_source=pypi) describe them:
|
|
135
|
+
`problems()`, `problem()`, `create_problem()`, `update_problem()`, `replace_draft()`,
|
|
136
|
+
`publish_problem()`, `archive_problem()`, `restore_problem()` and `public_problem()`.
|
|
137
|
+
|
|
138
|
+
## Sessions for the widget
|
|
139
|
+
|
|
140
|
+
A session lets one candidate's [embedded editor](https://onlinecompiler.io/docs/widget?utm_source=pypi)
|
|
141
|
+
Submit to a stored problem without your API key ever reaching the browser. Create it on your server and
|
|
142
|
+
put only its token in the page:
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
problem_id = client.problem("sum")["problem"] # the widget takes the id, never the slug
|
|
146
|
+
session = client.create_session(problem_id, "attempt-4821", max_submits=2, expires_in=3600)
|
|
147
|
+
|
|
148
|
+
html = (f'<div class="runcode" data-key="YOUR_WIDGET_KEY" data-problem="{problem_id}" '
|
|
149
|
+
f'data-session="{session["token"]}"></div>\n'
|
|
150
|
+
'<script src="https://onlinecompiler.io/widget.js"></script>')
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
The token is shown only this once and the client never logs it or puts it in an error, so keep it out
|
|
154
|
+
of your own logs too. `external_ref` should be an opaque id: the candidate's page sees it. Record the
|
|
155
|
+
score from the webhook or `client.submission(id)`, never from the browser.
|
|
156
|
+
`client.session(problem_id, session["session"])` shows the Submits used and the submission ids, and
|
|
157
|
+
`client.revoke_session(problem_id, session["session"])` ends it at once.
|
|
158
|
+
|
|
159
|
+
## Verify webhooks
|
|
160
|
+
|
|
161
|
+
An async submission with a `callback_url` (or a callback URL on the API key) gets one signed
|
|
162
|
+
`submission.finished` event. Check it before you trust it.
|
|
163
|
+
|
|
164
|
+
Verify the raw request body exactly as received: a body that was parsed and serialized again never
|
|
165
|
+
matches. In Flask that is `request.get_data()`, in Django `request.body`, in FastAPI
|
|
166
|
+
`await request.body()`.
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
import os
|
|
170
|
+
|
|
171
|
+
from flask import Flask, abort, request
|
|
172
|
+
from onlinecompiler import verify_webhook
|
|
173
|
+
|
|
174
|
+
app = Flask(__name__)
|
|
175
|
+
SECRET = os.environ["ONLINECOMPILER_WEBHOOK_SECRET"] # whsec_..., shown with the API key in your dashboard
|
|
176
|
+
|
|
177
|
+
@app.post("/hooks/oc")
|
|
178
|
+
def webhook():
|
|
179
|
+
if not verify_webhook(SECRET, request.get_data(), request.headers):
|
|
180
|
+
abort(400)
|
|
181
|
+
event = request.get_json()
|
|
182
|
+
print(event["submission"], event["status"], event["result"] and event["result"]["verdict"])
|
|
183
|
+
return "", 204 # answer quickly; deduplicate on event["delivery"]
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
`verify_webhook(secret, raw_body, headers, tolerance=300)` needs no client and no network. It takes any
|
|
187
|
+
mapping of headers (Django's, Flask's, FastAPI's or a dict), compares in constant time, accepts either
|
|
188
|
+
signature while a secret is being rotated, takes a list of secrets while you rotate yours, and rejects a
|
|
189
|
+
timestamp more than `tolerance` seconds (default 300) from now. Anything missing or malformed returns
|
|
190
|
+
`False`. See the [webhooks docs](https://onlinecompiler.io/docs/webhooks?utm_source=pypi).
|
|
191
|
+
|
|
192
|
+
## Verify receipts
|
|
193
|
+
|
|
194
|
+
Every judged submission comes with a receipt signed with Ed25519: proof of what ran, against which
|
|
195
|
+
tests, and what came out. Anyone can check it with the published keys.
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
pip install "onlinecompiler[receipts]"
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
from onlinecompiler import verify_receipt
|
|
203
|
+
|
|
204
|
+
receipt = client.submission("sub_8f2Kq7LmT3xR9vWb1cYd4hNe")["receipt"]
|
|
205
|
+
print(client.verify_receipt(receipt)) # fetches the published keys once, no API key needed
|
|
206
|
+
|
|
207
|
+
keys = client.receipt_keys() # or save them and verify offline later
|
|
208
|
+
print(verify_receipt(receipt, keys)) # False once any field has been changed
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Both return `False` for `None`, which a submission that was not judged has as its receipt. Without the
|
|
212
|
+
extra, verifying raises an error that says what to install. Store and deduplicate receipts by their
|
|
213
|
+
`submission` id (or the whole receipt), not by the signature string. `canonical_json(value)` returns
|
|
214
|
+
the canonical bytes receipts are signed over, for reproducing their hashes as the
|
|
215
|
+
[receipts docs](https://onlinecompiler.io/docs/receipts?utm_source=pypi) describe.
|
|
216
|
+
|
|
217
|
+
## Files in and out
|
|
218
|
+
|
|
219
|
+
```python
|
|
220
|
+
import base64
|
|
221
|
+
|
|
222
|
+
code = """
|
|
223
|
+
import pandas as pd
|
|
224
|
+
import matplotlib.pyplot as plt
|
|
225
|
+
df = pd.read_csv("sales.csv")
|
|
226
|
+
df.plot(x="month", y="units")
|
|
227
|
+
plt.show() # saved as figure-1.png in $OUTPUT_DIR
|
|
228
|
+
"""
|
|
229
|
+
result = client.run("python-3.14", code, files=[{"name": "sales.csv", "content": open("sales.csv").read()}])
|
|
230
|
+
for f in result["files"]:
|
|
231
|
+
with open(f["name"], "wb") as out:
|
|
232
|
+
out.write(base64.b64decode(f["content_base64"]))
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Input files sit in the program's working directory; send binary ones with `"encoding": "base64"`.
|
|
236
|
+
Files the program writes to `$OUTPUT_DIR` come back in `files`.
|
|
237
|
+
|
|
238
|
+
## Queue runs and get results by callback
|
|
239
|
+
|
|
240
|
+
```python
|
|
241
|
+
queued = client.submit("g++-15", source, input="3\n", extra_params={"submission_id": 981})
|
|
242
|
+
print(queued["id"])
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
`submit` returns at once. When the run finishes, its result is POSTed to the callback URL set on your
|
|
246
|
+
API key in the dashboard, with your `extra_params` echoed back.
|
|
247
|
+
|
|
248
|
+
## Other calls
|
|
249
|
+
|
|
250
|
+
- `client.compilers()`: every compiler id and language name (no key needed).
|
|
251
|
+
- `client.usage()`: this month's runs and what's left, for the account and for the key.
|
|
252
|
+
- `client.limits()`: per-run limits and default account limits (no key needed).
|
|
253
|
+
|
|
254
|
+
## Errors
|
|
255
|
+
|
|
256
|
+
```python
|
|
257
|
+
from onlinecompiler import OnlineCompilerError, RateLimitError
|
|
258
|
+
|
|
259
|
+
try:
|
|
260
|
+
client.run("python-3.14", "print(1)")
|
|
261
|
+
except RateLimitError as e: # HTTP 429
|
|
262
|
+
print(e.message) # which limit was hit, and how to raise it
|
|
263
|
+
except OnlineCompilerError as e: # anything else, including AuthenticationError
|
|
264
|
+
print(e.status, e.message) # status is 0 when the API could not be reached
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
`AuthenticationError` covers a missing, unknown or unauthorized key; `NotFoundError` a submission,
|
|
268
|
+
problem or session that does not exist or is not yours. A conflict, such as an archived problem or an
|
|
269
|
+
`Idempotency-Key` reused with another body, is an `OnlineCompilerError` with status 409 and the API's
|
|
270
|
+
message.
|
|
271
|
+
|
|
272
|
+
The client never retries a request by itself, because every run the API accepts counts toward your
|
|
273
|
+
allowance. `OnlineCompiler(api_key=None, *, base_url="https://api.onlinecompiler.io", timeout=90.0)`
|
|
274
|
+
takes the key, the API address and a timeout in seconds.
|
|
275
|
+
|
|
276
|
+
## Links
|
|
277
|
+
|
|
278
|
+
[Docs](https://onlinecompiler.io/docs?utm_source=pypi) ·
|
|
279
|
+
[Languages](https://onlinecompiler.io/languages?utm_source=pypi) ·
|
|
280
|
+
[Limits](https://onlinecompiler.io/docs/limits?utm_source=pypi) ·
|
|
281
|
+
[Pricing](https://onlinecompiler.io/pricing?utm_source=pypi) ·
|
|
282
|
+
[Security](https://onlinecompiler.io/security?utm_source=pypi) ·
|
|
283
|
+
[Changelog](https://onlinecompiler.io/changelog?utm_source=pypi)
|
|
284
|
+
|
|
285
|
+
MIT licensed. OnlineCompiler.io is a product of RunCode Inc.
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
# OnlineCompiler.io Python client
|
|
2
|
+
|
|
3
|
+
Run code in 46 languages from Python, or judge it against test cases, through the
|
|
4
|
+
[OnlineCompiler.io](https://onlinecompiler.io/?utm_source=pypi) code execution API. Every run gets a
|
|
5
|
+
fresh gVisor sandbox with no network, 512 MB of memory and a 30-second limit. Free up to 100,000 runs
|
|
6
|
+
a month, commercial use included.
|
|
7
|
+
|
|
8
|
+
No dependencies beyond the standard library. Python 3.10 or newer. Verifying signed receipts needs
|
|
9
|
+
one optional extra, `pip install "onlinecompiler[receipts]"` (it adds `cryptography`).
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install onlinecompiler
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Get a free API key at https://api.onlinecompiler.io/compilers/api-keys/ and set it as
|
|
16
|
+
`ONLINECOMPILER_API_KEY`, or pass it to the client.
|
|
17
|
+
|
|
18
|
+
## Run a program
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from onlinecompiler import OnlineCompiler
|
|
22
|
+
|
|
23
|
+
client = OnlineCompiler() # reads ONLINECOMPILER_API_KEY
|
|
24
|
+
|
|
25
|
+
result = client.run("python-3.14", "name = input()\nprint(f'Hello, {name}!')", input="Ada")
|
|
26
|
+
print(result["output"]) # Hello, Ada!
|
|
27
|
+
print(result["exit_code"], result["time"], result["memory"])
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The result has `output` (stdout), `error` (stderr or compiler errors), `exit_code`, `signal`, `time`
|
|
31
|
+
and `total` (seconds), `memory` (KB), `truncated`, `time_limit` and `files`. `time`, `total` and
|
|
32
|
+
`memory` are decimal strings, as the API sends them. Compiler ids such as `gcc-15`, `openjdk-25` and
|
|
33
|
+
`nodejs-24` come from `client.compilers()`. Pass `time_limit` (seconds, up to 30) to stop a run sooner.
|
|
34
|
+
|
|
35
|
+
## Judge a solution against test cases
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
result = client.run_tests(
|
|
39
|
+
"python-3.14",
|
|
40
|
+
"a, b = map(int, input().split())\nprint(a + b)",
|
|
41
|
+
tests=[
|
|
42
|
+
{"input": "1 2", "expected_output": "3"},
|
|
43
|
+
{"input": "10 20", "expected_output": "30", "hidden": True},
|
|
44
|
+
],
|
|
45
|
+
)
|
|
46
|
+
print(result["verdict"], result["score"], f"{result['passed']}/{result['total']}")
|
|
47
|
+
for case in result["tests"]:
|
|
48
|
+
print(case["verdict"])
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Each test counts as one run. Hidden tests return only their verdict. `checker` picks how output is
|
|
52
|
+
compared (`trim` by default, `exact`, `tokens`, `float` or `rows`), `scoring` is `all_or_nothing`
|
|
53
|
+
(default), `percent` or `weighted` (with a `weight` per test), and `time_limit` is seconds per test
|
|
54
|
+
(default 2). See the [run-tests docs](https://onlinecompiler.io/docs/run-tests?utm_source=pypi).
|
|
55
|
+
|
|
56
|
+
Every run-tests result also has `summary` (the result in plain words), `submission` (its id) and a
|
|
57
|
+
signed `receipt`.
|
|
58
|
+
|
|
59
|
+
## Judge in the background
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
queued = client.run_tests(
|
|
63
|
+
"python-3.14",
|
|
64
|
+
"a, b = map(int, input().split())\nprint(a + b)",
|
|
65
|
+
tests=[{"input": "1 2", "expected_output": "3"}, {"input": "10 20", "expected_output": "30", "hidden": True}],
|
|
66
|
+
async_=True,
|
|
67
|
+
external_ref="attempt-4821",
|
|
68
|
+
idempotency_key="attempt-4821",
|
|
69
|
+
)
|
|
70
|
+
done = client.wait_for_submission(queued["submission"]) # looks it up every 2 seconds
|
|
71
|
+
result = done["result"] # None when it failed, which means it was not judged
|
|
72
|
+
if result is not None:
|
|
73
|
+
print(result["verdict"], result["score"])
|
|
74
|
+
else:
|
|
75
|
+
print("Not judged:", done["error"]) # send it again
|
|
76
|
+
client.delete_submission(done["submission"]) # optional: drop the stored code and output
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`async_=True` (`async` is a Python keyword) answers at once with the submission id, and the submission
|
|
80
|
+
waits in your account's queue instead of getting 429 in a burst. Add `callback_url` to have the result
|
|
81
|
+
POSTed to your server as a signed webhook (see below); it needs `async_=True`, and the client refuses it
|
|
82
|
+
without. `external_ref` is your own id, signed into the receipt; `metadata` is any JSON object, returned in
|
|
83
|
+
lookups and webhooks. `idempotency_key` (1 to 100 printable ASCII characters, checked before sending) is
|
|
84
|
+
sent as the `Idempotency-Key` header, so a retry after a timeout answers with the original result instead
|
|
85
|
+
of running again. `client.submission(id)` looks a submission up once.
|
|
86
|
+
|
|
87
|
+
## Judge against a stored problem
|
|
88
|
+
|
|
89
|
+
A stored problem keeps its tests, checker and limits on the server, so you send only the candidate's
|
|
90
|
+
code and the hidden tests never leave it.
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
client.create_problem({
|
|
94
|
+
"title": "Sum of two numbers",
|
|
95
|
+
"slug": "sum",
|
|
96
|
+
"languages": ["python-3.14"],
|
|
97
|
+
"tests": [
|
|
98
|
+
{"name": "sample", "input": "1 2\n", "expected_output": "3\n", "hidden": False},
|
|
99
|
+
{"input": "5 7\n", "expected_output": "12\n"}, # tests are hidden unless you say otherwise
|
|
100
|
+
],
|
|
101
|
+
})
|
|
102
|
+
client.publish_problem("sum") # no reference solution, so it is published at once
|
|
103
|
+
|
|
104
|
+
result = client.judge("sum", "python-3.14", "a, b = map(int, input().split())\nprint(a + b)")
|
|
105
|
+
print(result["verdict"], result["score"], result["problem"], result["version"])
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`judge` takes the problem's id (`prob_...`) or your slug for it. `mode="samples"` judges only the
|
|
109
|
+
visible tests, and `async_`, `callback_url`, `external_ref`, `metadata` and `idempotency_key` work as
|
|
110
|
+
above. Problems are plain dicts, as the
|
|
111
|
+
[stored problems docs](https://onlinecompiler.io/docs/problems?utm_source=pypi) describe them:
|
|
112
|
+
`problems()`, `problem()`, `create_problem()`, `update_problem()`, `replace_draft()`,
|
|
113
|
+
`publish_problem()`, `archive_problem()`, `restore_problem()` and `public_problem()`.
|
|
114
|
+
|
|
115
|
+
## Sessions for the widget
|
|
116
|
+
|
|
117
|
+
A session lets one candidate's [embedded editor](https://onlinecompiler.io/docs/widget?utm_source=pypi)
|
|
118
|
+
Submit to a stored problem without your API key ever reaching the browser. Create it on your server and
|
|
119
|
+
put only its token in the page:
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
problem_id = client.problem("sum")["problem"] # the widget takes the id, never the slug
|
|
123
|
+
session = client.create_session(problem_id, "attempt-4821", max_submits=2, expires_in=3600)
|
|
124
|
+
|
|
125
|
+
html = (f'<div class="runcode" data-key="YOUR_WIDGET_KEY" data-problem="{problem_id}" '
|
|
126
|
+
f'data-session="{session["token"]}"></div>\n'
|
|
127
|
+
'<script src="https://onlinecompiler.io/widget.js"></script>')
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
The token is shown only this once and the client never logs it or puts it in an error, so keep it out
|
|
131
|
+
of your own logs too. `external_ref` should be an opaque id: the candidate's page sees it. Record the
|
|
132
|
+
score from the webhook or `client.submission(id)`, never from the browser.
|
|
133
|
+
`client.session(problem_id, session["session"])` shows the Submits used and the submission ids, and
|
|
134
|
+
`client.revoke_session(problem_id, session["session"])` ends it at once.
|
|
135
|
+
|
|
136
|
+
## Verify webhooks
|
|
137
|
+
|
|
138
|
+
An async submission with a `callback_url` (or a callback URL on the API key) gets one signed
|
|
139
|
+
`submission.finished` event. Check it before you trust it.
|
|
140
|
+
|
|
141
|
+
Verify the raw request body exactly as received: a body that was parsed and serialized again never
|
|
142
|
+
matches. In Flask that is `request.get_data()`, in Django `request.body`, in FastAPI
|
|
143
|
+
`await request.body()`.
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
import os
|
|
147
|
+
|
|
148
|
+
from flask import Flask, abort, request
|
|
149
|
+
from onlinecompiler import verify_webhook
|
|
150
|
+
|
|
151
|
+
app = Flask(__name__)
|
|
152
|
+
SECRET = os.environ["ONLINECOMPILER_WEBHOOK_SECRET"] # whsec_..., shown with the API key in your dashboard
|
|
153
|
+
|
|
154
|
+
@app.post("/hooks/oc")
|
|
155
|
+
def webhook():
|
|
156
|
+
if not verify_webhook(SECRET, request.get_data(), request.headers):
|
|
157
|
+
abort(400)
|
|
158
|
+
event = request.get_json()
|
|
159
|
+
print(event["submission"], event["status"], event["result"] and event["result"]["verdict"])
|
|
160
|
+
return "", 204 # answer quickly; deduplicate on event["delivery"]
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
`verify_webhook(secret, raw_body, headers, tolerance=300)` needs no client and no network. It takes any
|
|
164
|
+
mapping of headers (Django's, Flask's, FastAPI's or a dict), compares in constant time, accepts either
|
|
165
|
+
signature while a secret is being rotated, takes a list of secrets while you rotate yours, and rejects a
|
|
166
|
+
timestamp more than `tolerance` seconds (default 300) from now. Anything missing or malformed returns
|
|
167
|
+
`False`. See the [webhooks docs](https://onlinecompiler.io/docs/webhooks?utm_source=pypi).
|
|
168
|
+
|
|
169
|
+
## Verify receipts
|
|
170
|
+
|
|
171
|
+
Every judged submission comes with a receipt signed with Ed25519: proof of what ran, against which
|
|
172
|
+
tests, and what came out. Anyone can check it with the published keys.
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
pip install "onlinecompiler[receipts]"
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
from onlinecompiler import verify_receipt
|
|
180
|
+
|
|
181
|
+
receipt = client.submission("sub_8f2Kq7LmT3xR9vWb1cYd4hNe")["receipt"]
|
|
182
|
+
print(client.verify_receipt(receipt)) # fetches the published keys once, no API key needed
|
|
183
|
+
|
|
184
|
+
keys = client.receipt_keys() # or save them and verify offline later
|
|
185
|
+
print(verify_receipt(receipt, keys)) # False once any field has been changed
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Both return `False` for `None`, which a submission that was not judged has as its receipt. Without the
|
|
189
|
+
extra, verifying raises an error that says what to install. Store and deduplicate receipts by their
|
|
190
|
+
`submission` id (or the whole receipt), not by the signature string. `canonical_json(value)` returns
|
|
191
|
+
the canonical bytes receipts are signed over, for reproducing their hashes as the
|
|
192
|
+
[receipts docs](https://onlinecompiler.io/docs/receipts?utm_source=pypi) describe.
|
|
193
|
+
|
|
194
|
+
## Files in and out
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
import base64
|
|
198
|
+
|
|
199
|
+
code = """
|
|
200
|
+
import pandas as pd
|
|
201
|
+
import matplotlib.pyplot as plt
|
|
202
|
+
df = pd.read_csv("sales.csv")
|
|
203
|
+
df.plot(x="month", y="units")
|
|
204
|
+
plt.show() # saved as figure-1.png in $OUTPUT_DIR
|
|
205
|
+
"""
|
|
206
|
+
result = client.run("python-3.14", code, files=[{"name": "sales.csv", "content": open("sales.csv").read()}])
|
|
207
|
+
for f in result["files"]:
|
|
208
|
+
with open(f["name"], "wb") as out:
|
|
209
|
+
out.write(base64.b64decode(f["content_base64"]))
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Input files sit in the program's working directory; send binary ones with `"encoding": "base64"`.
|
|
213
|
+
Files the program writes to `$OUTPUT_DIR` come back in `files`.
|
|
214
|
+
|
|
215
|
+
## Queue runs and get results by callback
|
|
216
|
+
|
|
217
|
+
```python
|
|
218
|
+
queued = client.submit("g++-15", source, input="3\n", extra_params={"submission_id": 981})
|
|
219
|
+
print(queued["id"])
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
`submit` returns at once. When the run finishes, its result is POSTed to the callback URL set on your
|
|
223
|
+
API key in the dashboard, with your `extra_params` echoed back.
|
|
224
|
+
|
|
225
|
+
## Other calls
|
|
226
|
+
|
|
227
|
+
- `client.compilers()`: every compiler id and language name (no key needed).
|
|
228
|
+
- `client.usage()`: this month's runs and what's left, for the account and for the key.
|
|
229
|
+
- `client.limits()`: per-run limits and default account limits (no key needed).
|
|
230
|
+
|
|
231
|
+
## Errors
|
|
232
|
+
|
|
233
|
+
```python
|
|
234
|
+
from onlinecompiler import OnlineCompilerError, RateLimitError
|
|
235
|
+
|
|
236
|
+
try:
|
|
237
|
+
client.run("python-3.14", "print(1)")
|
|
238
|
+
except RateLimitError as e: # HTTP 429
|
|
239
|
+
print(e.message) # which limit was hit, and how to raise it
|
|
240
|
+
except OnlineCompilerError as e: # anything else, including AuthenticationError
|
|
241
|
+
print(e.status, e.message) # status is 0 when the API could not be reached
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
`AuthenticationError` covers a missing, unknown or unauthorized key; `NotFoundError` a submission,
|
|
245
|
+
problem or session that does not exist or is not yours. A conflict, such as an archived problem or an
|
|
246
|
+
`Idempotency-Key` reused with another body, is an `OnlineCompilerError` with status 409 and the API's
|
|
247
|
+
message.
|
|
248
|
+
|
|
249
|
+
The client never retries a request by itself, because every run the API accepts counts toward your
|
|
250
|
+
allowance. `OnlineCompiler(api_key=None, *, base_url="https://api.onlinecompiler.io", timeout=90.0)`
|
|
251
|
+
takes the key, the API address and a timeout in seconds.
|
|
252
|
+
|
|
253
|
+
## Links
|
|
254
|
+
|
|
255
|
+
[Docs](https://onlinecompiler.io/docs?utm_source=pypi) ·
|
|
256
|
+
[Languages](https://onlinecompiler.io/languages?utm_source=pypi) ·
|
|
257
|
+
[Limits](https://onlinecompiler.io/docs/limits?utm_source=pypi) ·
|
|
258
|
+
[Pricing](https://onlinecompiler.io/pricing?utm_source=pypi) ·
|
|
259
|
+
[Security](https://onlinecompiler.io/security?utm_source=pypi) ·
|
|
260
|
+
[Changelog](https://onlinecompiler.io/changelog?utm_source=pypi)
|
|
261
|
+
|
|
262
|
+
MIT licensed. OnlineCompiler.io is a product of RunCode Inc.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "onlinecompiler"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Python client for the OnlineCompiler.io code execution API: run and judge code in 46 languages in a sandbox"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "RunCode Inc." }]
|
|
14
|
+
keywords = ["code execution", "online compiler", "compiler api", "judge0", "online judge", "sandbox", "onlinecompiler", "code judge", "webhooks", "signed receipts"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
20
|
+
"Topic :: Software Development :: Compilers",
|
|
21
|
+
"Typing :: Typed",
|
|
22
|
+
]
|
|
23
|
+
dependencies = []
|
|
24
|
+
|
|
25
|
+
[project.optional-dependencies]
|
|
26
|
+
# Ed25519 for verify_receipt; everything else uses the standard library.
|
|
27
|
+
receipts = ["cryptography>=42"]
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://onlinecompiler.io"
|
|
31
|
+
Documentation = "https://onlinecompiler.io/docs"
|
|
32
|
+
Pricing = "https://onlinecompiler.io/pricing"
|
|
33
|
+
Changelog = "https://onlinecompiler.io/changelog"
|
|
34
|
+
|
|
35
|
+
[tool.hatch.version]
|
|
36
|
+
path = "src/onlinecompiler/_client.py"
|
|
37
|
+
|
|
38
|
+
[tool.hatch.build.targets.wheel]
|
|
39
|
+
packages = ["src/onlinecompiler"]
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Python client for the OnlineCompiler.io code execution API: run and judge code in 46 languages."""
|
|
2
|
+
|
|
3
|
+
from ._client import (
|
|
4
|
+
AuthenticationError,
|
|
5
|
+
Case,
|
|
6
|
+
CaseResult,
|
|
7
|
+
Checker,
|
|
8
|
+
Compiler,
|
|
9
|
+
InputFile,
|
|
10
|
+
JudgeResult,
|
|
11
|
+
Mode,
|
|
12
|
+
NotFoundError,
|
|
13
|
+
OnlineCompiler,
|
|
14
|
+
OnlineCompilerError,
|
|
15
|
+
OutputFile,
|
|
16
|
+
ProblemSession,
|
|
17
|
+
ProblemSessionCreated,
|
|
18
|
+
PublishResult,
|
|
19
|
+
Queued,
|
|
20
|
+
RateLimitError,
|
|
21
|
+
Receipt,
|
|
22
|
+
ReceiptKey,
|
|
23
|
+
RunResult,
|
|
24
|
+
RunTestsResult,
|
|
25
|
+
Scoring,
|
|
26
|
+
Submission,
|
|
27
|
+
SubmissionDeleted,
|
|
28
|
+
SubmissionProblem,
|
|
29
|
+
SubmissionQueued,
|
|
30
|
+
SubmissionResult,
|
|
31
|
+
__version__,
|
|
32
|
+
)
|
|
33
|
+
from ._verify import canonical_json, verify_receipt, verify_webhook
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"AuthenticationError",
|
|
37
|
+
"Case",
|
|
38
|
+
"CaseResult",
|
|
39
|
+
"Checker",
|
|
40
|
+
"Compiler",
|
|
41
|
+
"InputFile",
|
|
42
|
+
"JudgeResult",
|
|
43
|
+
"Mode",
|
|
44
|
+
"NotFoundError",
|
|
45
|
+
"OnlineCompiler",
|
|
46
|
+
"OnlineCompilerError",
|
|
47
|
+
"OutputFile",
|
|
48
|
+
"ProblemSession",
|
|
49
|
+
"ProblemSessionCreated",
|
|
50
|
+
"PublishResult",
|
|
51
|
+
"Queued",
|
|
52
|
+
"RateLimitError",
|
|
53
|
+
"Receipt",
|
|
54
|
+
"ReceiptKey",
|
|
55
|
+
"RunResult",
|
|
56
|
+
"RunTestsResult",
|
|
57
|
+
"Scoring",
|
|
58
|
+
"Submission",
|
|
59
|
+
"SubmissionDeleted",
|
|
60
|
+
"SubmissionProblem",
|
|
61
|
+
"SubmissionQueued",
|
|
62
|
+
"SubmissionResult",
|
|
63
|
+
"__version__",
|
|
64
|
+
"canonical_json",
|
|
65
|
+
"verify_receipt",
|
|
66
|
+
"verify_webhook",
|
|
67
|
+
]
|