Skip to content

Commit 3b197de

Browse files
Implement batching to support submissions lists larger than the allowed batch size for create_submissions and get_submissions routes.
Co-authored-by: Filip Karlo Došilović <filipk.dosilovic@gmail.com>
1 parent db22c5b commit 3b197de

File tree

8 files changed

+149
-69
lines changed

8 files changed

+149
-69
lines changed

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
11
# judge0-py
22

33
Python client library for Judge0
4+
5+
| submissions | test_cases | returns |
6+
|:------------|:-----------|:------------|
7+
| Submission | TestCase | Submission |
8+
| Submission | TestCases | Submissions |
9+
| Submissions | TestCase | Submissions |
10+
| Submissions | TestCases | Submissions |

examples/many_submissions.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import judge0
2+
3+
submissions = []
4+
for i in range(42):
5+
submissions.append(judge0.Submission(
6+
source_code=f"print({i})",
7+
))
8+
9+
results = judge0.run(submissions=submissions)
10+
for result in results:
11+
print(result.stdout.strip(), " ", end="")
12+
print()

src/judge0/api.py

Lines changed: 33 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
from typing import Optional, Union
22

3-
from .base_types import Flavor, TestCase
3+
from .base_types import Flavor, TestCase, TestCases
44
from .clients import Client
5+
56
from .retry import RegularPeriodRetry, RetryMechanism
6-
from .submission import Submission
7+
from .submission import Submission, Submissions
78

89

910
def resolve_client(
1011
client: Optional[Union[Client, Flavor]] = None,
11-
submissions: Optional[Union[Submission, list[Submission]]] = None,
12-
) -> Union[Client, None]:
12+
submissions: Optional[Union[Submission, Submissions]] = None,
13+
) -> Client:
1314
# User explicitly passed a client.
1415
if isinstance(client, Client):
1516
return client
@@ -20,6 +21,9 @@ def resolve_client(
2021
if isinstance(client, Flavor):
2122
return _get_implicit_client(flavor=client)
2223

24+
if client is None and isinstance(submissions, list) and len(submissions) == 0:
25+
raise ValueError("Client cannot be determined from empty submissions.")
26+
2327
# client is None and we have to determine a flavor of the client from the
2428
# submissions and the languages.
2529
if isinstance(submissions, Submission):
@@ -44,10 +48,9 @@ def resolve_client(
4448

4549
def wait(
4650
client: Client,
47-
submissions: Union[Submission, list[Submission]],
48-
*,
51+
submissions: Union[Submission, Submissions],
4952
retry_mechanism: Optional[RetryMechanism] = None,
50-
) -> Union[Submission, list[Submission]]:
53+
) -> Union[Submission, Submissions]:
5154
if retry_mechanism is None:
5255
retry_mechanism = RegularPeriodRetry()
5356

@@ -61,15 +64,7 @@ def wait(
6164
}
6265

6366
while len(submissions_to_check) > 0 and not retry_mechanism.is_done():
64-
# We differentiate between getting a single submission and multiple
65-
# submissions to be consistent with the API, even though the API
66-
# allows to get single submission with the same endpoint as for getting
67-
# the multiple submissions.
68-
if len(submissions_to_check) == 1:
69-
client.get_submission(*submissions_to_check.values())
70-
else:
71-
client.get_submissions(submissions_to_check.values())
72-
67+
client.check_submissions(list(submissions_to_check.values()))
7368
for token in list(submissions_to_check):
7469
submission = submissions_to_check[token]
7570
if submission.is_done():
@@ -86,20 +81,20 @@ def wait(
8681

8782

8883
def create_submissions_from_test_cases(
89-
submissions: Union[Submission, list[Submission]],
90-
test_cases: Optional[Union[TestCase, list[TestCase]]] = None,
84+
submissions: Union[Submission, Submissions],
85+
test_cases: Optional[Union[TestCase, TestCases]] = None,
9186
):
9287
"""Utility function for creating submissions from the (submission, test_case) pairs.
9388
9489
The following table contains the return type based on the types of `submissions`
9590
and `test_cases` arguments:
9691
97-
| submissions | test_cases | returns |
98-
|:-----------------|:---------------|:-----------------|
99-
| Submission | TestCase | Submission |
100-
| Submission | list[TestCase] | list[Submission] |
101-
| list[Submission] | TestCase | list[Submission] |
102-
| list[Submission] | list[TestCase] | list[Submission] |
92+
| submissions | test_cases | returns |
93+
|:------------|:-----------|:------------|
94+
| Submission | TestCase | Submission |
95+
| Submission | TestCases | Submissions |
96+
| Submissions | TestCase | Submissions |
97+
| Submissions | TestCases | Submissions |
10398
10499
"""
105100
# Let's deal with the simplest cases where no test cases are provided. We
@@ -127,7 +122,7 @@ def create_submissions_from_test_cases(
127122
submission_copy.expected_output = test_case.expected_output
128123
all_submissions.append(submission_copy)
129124

130-
if isinstance(submissions, Submission) and not isinstance(test_cases, list):
125+
if isinstance(submissions, Submission) and isinstance(test_cases, TestCase):
131126
return all_submissions[0]
132127
else:
133128
return all_submissions
@@ -136,12 +131,12 @@ def create_submissions_from_test_cases(
136131
def _execute(
137132
*,
138133
client: Optional[Union[Client, Flavor]] = None,
139-
submissions: Optional[Union[Submission, list[Submission]]] = None,
134+
submissions: Optional[Union[Submission, Submissions]] = None,
140135
source_code: Optional[str] = None,
136+
test_cases: Optional[Union[TestCase, TestCases]] = None,
141137
wait_for_result: bool = False,
142-
test_cases: Optional[Union[TestCase, list[TestCase]]] = None,
143138
**kwargs,
144-
) -> Union[Submission, list[Submission]]:
139+
) -> Union[Submission, Submissions]:
145140
if submissions is not None and source_code is not None:
146141
raise ValueError(
147142
"Both submissions and source_code arguments are provided. "
@@ -153,62 +148,42 @@ def _execute(
153148
if source_code is not None:
154149
submissions = Submission(source_code=source_code, **kwargs)
155150

156-
# TODO: Since kwargs is ignored if submissions argument is provided, maybe
157-
# use warnings if submission and kwargs are provided?
158-
159-
# There is no need to check for other cases since we are explicitly
160-
# checking for submissions and source_code arguments.
161-
if client is None:
162-
if isinstance(submissions, list) and len(submissions) == 0:
163-
raise ValueError("Client cannot be determined from empty submissions.")
164-
165-
client = resolve_client(client, submissions=submissions)
166-
151+
client = resolve_client(client=client, submissions=submissions)
167152
all_submissions = create_submissions_from_test_cases(submissions, test_cases)
168-
169-
# We differentiate between creating a single submission and multiple
170-
# submissions to be consistent with the API, even though the API
171-
# allows to create single submission with the same endpoint as for
172-
# creating the multiple submissions.
173-
if isinstance(all_submissions, Submission):
174-
all_submissions = client.create_submission(all_submissions)
175-
elif len(all_submissions) == 1:
176-
all_submissions = [client.create_submission(all_submissions[0])]
177-
else:
178-
all_submissions = client.create_submissions(all_submissions)
153+
all_submissions = client.submit(all_submissions)
179154

180155
if wait_for_result:
181-
all_submissions = wait(client, all_submissions)
156+
all_submissions = wait(client=client, submissions=all_submissions)
182157

183158
return all_submissions
184159

185160

186161
def async_execute(
187162
*,
188163
client: Optional[Union[Client, Flavor]] = None,
189-
submissions: Optional[Union[Submission, list[Submission]]] = None,
164+
submissions: Optional[Union[Submission, Submissions]] = None,
190165
source_code: Optional[str] = None,
191-
test_cases: Optional[Union[TestCase, list[TestCase]]] = None,
166+
test_cases: Optional[Union[TestCase, TestCases]] = None,
192167
**kwargs,
193-
) -> Union[Submission, list[Submission]]:
168+
) -> Union[Submission, Submissions]:
194169
return _execute(
195170
client=client,
196171
submissions=submissions,
197172
source_code=source_code,
198-
wait_for_result=False,
199173
test_cases=test_cases,
174+
wait_for_result=False,
200175
**kwargs,
201176
)
202177

203178

204179
def sync_execute(
205180
*,
206181
client: Optional[Union[Client, Flavor]] = None,
207-
submissions: Optional[Union[Submission, list[Submission]]] = None,
182+
submissions: Optional[Union[Submission, Submissions]] = None,
208183
source_code: Optional[str] = None,
209-
test_cases: Optional[Union[TestCase, list[TestCase]]] = None,
184+
test_cases: Optional[Union[TestCase, TestCases]] = None,
210185
**kwargs,
211-
) -> Union[Submission, list[Submission]]:
186+
) -> Union[Submission, Submissions]:
212187
return _execute(
213188
client=client,
214189
submissions=submissions,

src/judge0/base_types.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
from abc import ABC, abstractmethod
22
from dataclasses import dataclass
33
from enum import IntEnum
4-
from typing import Optional
4+
from typing import Optional, Union
5+
6+
7+
TestCases = Union[list["TestCase"], tuple["TestCase"]]
58

69

710
@dataclass(frozen=True)

src/judge0/clients.py

Lines changed: 61 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,18 @@
33
import requests
44

55
from .base_types import Language, LanguageAlias
6+
from .common import batched
67
from .data import LANGUAGE_TO_LANGUAGE_ID
7-
from .submission import Submission
8+
from .submission import Submission, Submissions
89

910

1011
class Client:
1112
API_KEY_ENV = "JUDGE0_API_KEY"
13+
DEFAULT_MAX_SUBMISSION_BATCH_SIZE = 20
14+
ENABLED_BATCHED_SUBMISSIONS = True
15+
EFFECTIVE_SUBMISSION_BATCH_SIZE = (
16+
DEFAULT_MAX_SUBMISSION_BATCH_SIZE if ENABLED_BATCHED_SUBMISSIONS else 1
17+
)
1218

1319
def __init__(self, endpoint, auth_headers) -> None:
1420
self.endpoint = endpoint
@@ -137,7 +143,7 @@ def get_submission(
137143

138144
return submission
139145

140-
def create_submissions(self, submissions: list[Submission]) -> list[Submission]:
146+
def create_submissions(self, submissions: Submissions) -> Submissions:
141147
# Check if all submissions contain supported language.
142148
for submission in submissions:
143149
if not self.is_language_supported(language=submission.language):
@@ -163,10 +169,10 @@ def create_submissions(self, submissions: list[Submission]) -> list[Submission]:
163169

164170
def get_submissions(
165171
self,
166-
submissions: list[Submission],
172+
submissions: Submissions,
167173
*,
168174
fields: Union[str, Iterable[str], None] = None,
169-
) -> list[Submission]:
175+
) -> Submissions:
170176
params = {
171177
"base64_encoded": "true",
172178
}
@@ -194,6 +200,51 @@ def get_submissions(
194200

195201
return submissions
196202

203+
def submit(
204+
self,
205+
submissions: Union[Submission, Submissions],
206+
) -> Union[Submission, Submissions]:
207+
208+
if isinstance(submissions, Submission):
209+
return self.create_submission(submissions)
210+
211+
batch_size = self.EFFECTIVE_SUBMISSION_BATCH_SIZE
212+
result_submissions = []
213+
for submission_batch in batched(submissions, batch_size):
214+
if batch_size > 1:
215+
result_submissions.extend(
216+
self.create_submissions(list(submission_batch))
217+
)
218+
else:
219+
result_submissions.append(
220+
self.create_submission(list(submission_batch)[0])
221+
)
222+
223+
return result_submissions
224+
225+
def check_submissions(
226+
self,
227+
submissions: Union[Submission, Submissions],
228+
*,
229+
fields: Union[str, Iterable[str], None] = None,
230+
) -> Union[Submission, Submissions]:
231+
if isinstance(submissions, Submission):
232+
return self.get_submission(submissions, fields=fields)
233+
234+
batch_size = self.EFFECTIVE_SUBMISSION_BATCH_SIZE
235+
result_submissions = []
236+
for submission_batch in batched(submissions, batch_size):
237+
if batch_size > 1:
238+
result_submissions.extend(
239+
self.get_submissions(list(submission_batch), fields=fields)
240+
)
241+
else:
242+
result_submissions.append(
243+
self.get_submission(list(submission_batch)[0], fields=fields)
244+
)
245+
246+
return result_submissions
247+
197248

198249
class ATD(Client):
199250
API_KEY_ENV = "JUDGE0_ATD_API_KEY"
@@ -269,16 +320,16 @@ def get_submission(
269320
self._update_endpoint_header(self.DEFAULT_GET_SUBMISSION_ENDPOINT)
270321
return super().get_submission(submission, fields=fields)
271322

272-
def create_submissions(self, submissions: list[Submission]) -> list[Submission]:
323+
def create_submissions(self, submissions: Submissions) -> Submissions:
273324
self._update_endpoint_header(self.DEFAULT_CREATE_SUBMISSIONS_ENDPOINT)
274325
return super().create_submissions(submissions)
275326

276327
def get_submissions(
277328
self,
278-
submissions: list[Submission],
329+
submissions: Submissions,
279330
*,
280331
fields: Union[str, Iterable[str], None] = None,
281-
) -> list[Submission]:
332+
) -> Submissions:
282333
self._update_endpoint_header(self.DEFAULT_GET_SUBMISSIONS_ENDPOINT)
283334
return super().get_submissions(submissions, fields=fields)
284335

@@ -340,16 +391,16 @@ def get_submission(
340391
self._update_endpoint_header(self.DEFAULT_GET_SUBMISSION_ENDPOINT)
341392
return super().get_submission(submission, fields=fields)
342393

343-
def create_submissions(self, submissions: list[Submission]) -> list[Submission]:
394+
def create_submissions(self, submissions: Submissions) -> Submissions:
344395
self._update_endpoint_header(self.DEFAULT_CREATE_SUBMISSIONS_ENDPOINT)
345396
return super().create_submissions(submissions)
346397

347398
def get_submissions(
348399
self,
349-
submissions: list[Submission],
400+
submissions: Submissions,
350401
*,
351402
fields: Union[str, Iterable[str], None] = None,
352-
) -> list[Submission]:
403+
) -> Submissions:
353404
self._update_endpoint_header(self.DEFAULT_GET_SUBMISSIONS_ENDPOINT)
354405
return super().get_submissions(submissions, fields=fields)
355406

src/judge0/common.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from base64 import b64decode, b64encode
2+
from itertools import islice
23
from typing import Union
34

45
from .base_types import Encodeable
@@ -22,3 +23,18 @@ def decode(content: Union[bytes, str]) -> str:
2223
if isinstance(content, str):
2324
return b64decode(content.encode()).decode(errors="backslashreplace")
2425
raise ValueError(f"Unsupported type. Expected bytes or str, got {type(content)}!")
26+
27+
28+
def batched(iterable, n):
29+
"""Utility function for batching submissions.
30+
31+
Adapted from https://docs.python.org/3/library/itertools.html#itertools.batched.
32+
"""
33+
if n < 1:
34+
raise ValueError("n must be at least one")
35+
iterator = iter(iterable)
36+
while True:
37+
batch = tuple(islice(iterator, n))
38+
if not batch:
39+
break
40+
yield batch

src/judge0/submission.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@
6363
"wall_time_limit",
6464
}
6565

66+
Submissions = Union[list["Submission"], tuple["Submission"]]
67+
6668

6769
class Submission:
6870
"""

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy