Skip to content

Commit e9fc933

Browse files
committed
Make Submission work with pydantic.
1 parent e797c72 commit e9fc933

File tree

4 files changed

+149
-61
lines changed

4 files changed

+149
-61
lines changed

examples/1000_http_callback_aka_webhook/main.py

+16-17
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,10 @@
11
#!/usr/bin/env python3
2-
from fastapi import FastAPI, Depends
3-
from pydantic import BaseModel
4-
5-
import uvicorn
62
import asyncio
7-
import judge0
83

4+
import judge0
95

10-
class CallbackResponse(BaseModel):
11-
created_at: str
12-
finished_at: str
13-
language: dict
14-
status: dict
15-
stdout: str
6+
import uvicorn
7+
from fastapi import Depends, FastAPI
168

179

1810
class AppContext:
@@ -47,13 +39,14 @@ async def root(app_context=Depends(get_app_context)):
4739

4840

4941
@app.put("/callback")
50-
async def callback(response: CallbackResponse):
42+
async def callback(response: judge0.Submission):
5143
print(f"Received: {response}")
5244

5345

54-
# We are using free service from https://localhost.run to get a public URL for our local server.
55-
# This approach is not recommended for production use. It is only for demonstration purposes
56-
# since domain names change regularly and there is a speed limit for the free service.
46+
# We are using free service from https://localhost.run to get a public URL for
47+
# our local server. This approach is not recommended for production use. It is
48+
# only for demonstration purposes since domain names change regularly and there
49+
# is a speed limit for the free service.
5750
async def run_ssh_tunnel():
5851
app_context = get_app_context()
5952

@@ -69,7 +62,9 @@ async def run_ssh_tunnel():
6962
]
7063

7164
process = await asyncio.create_subprocess_exec(
72-
*command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT
65+
*command,
66+
stdout=asyncio.subprocess.PIPE,
67+
stderr=asyncio.subprocess.STDOUT,
7368
)
7469

7570
while True:
@@ -86,7 +81,11 @@ async def run_ssh_tunnel():
8681

8782
async def run_server():
8883
config = uvicorn.Config(
89-
app, host="127.0.0.1", port=LOCAL_SERVER_PORT, workers=5, loop="asyncio"
84+
app,
85+
host="127.0.0.1",
86+
port=LOCAL_SERVER_PORT,
87+
workers=5,
88+
loop="asyncio",
9089
)
9190
server = uvicorn.Server(config)
9291
await server.serve()

src/judge0/common.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,13 @@ def encode(content: Union[bytes, str, Encodeable]) -> str:
1717

1818
def decode(content: Union[bytes, str]) -> str:
1919
if isinstance(content, bytes):
20-
return b64decode(content.decode(errors="backslashreplace")).decode(
20+
return b64decode(
21+
content.decode(errors="backslashreplace"), validate=True
22+
).decode(errors="backslashreplace")
23+
if isinstance(content, str):
24+
return b64decode(content.encode(), validate=True).decode(
2125
errors="backslashreplace"
2226
)
23-
if isinstance(content, str):
24-
return b64decode(content.encode()).decode(errors="backslashreplace")
2527
raise ValueError(f"Unsupported type. Expected bytes or str, got {type(content)}!")
2628

2729

src/judge0/submission.py

+81-41
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from datetime import datetime
33
from typing import Any, Optional, Union
44

5-
from pydantic import BaseModel
5+
from pydantic import BaseModel, ConfigDict, Field, field_validator, UUID4
66

77
from .base_types import Iterable, LanguageAlias, Status
88
from .common import decode, encode
@@ -18,7 +18,7 @@
1818
"stdout",
1919
"stderr",
2020
"compile_output",
21-
"post_execution_filesystem",
21+
# "post_execution_filesystem",
2222
}
2323
ENCODED_FIELDS = ENCODED_REQUEST_FIELDS | ENCODED_RESPONSE_FIELDS
2424
EXTRA_REQUEST_FIELDS = {
@@ -126,42 +126,86 @@ class Submission(BaseModel):
126126
URL for a callback to report execution results or status.
127127
"""
128128

129-
source_code: Optional[str] = None
130-
language: Union[LanguageAlias, int] = LanguageAlias.PYTHON
131-
additional_files: Optional[str] = None
132-
compiler_options: Optional[str] = None
133-
command_line_arguments: Optional[str] = None
134-
stdin: Optional[str] = None
135-
expected_output: Optional[str] = None
136-
cpu_time_limit: Optional[float] = None
137-
cpu_extra_time: Optional[float] = None
138-
wall_time_limit: Optional[float] = None
139-
memory_limit: Optional[float] = None
140-
stack_limit: Optional[int] = None
141-
max_processes_and_or_threads: Optional[int] = None
142-
enable_per_process_and_thread_time_limit: Optional[bool] = None
143-
enable_per_process_and_thread_memory_limit: Optional[bool] = None
144-
max_file_size: Optional[int] = None
145-
redirect_stderr_to_stdout: Optional[bool] = None
146-
enable_network: Optional[bool] = None
147-
number_of_runs: Optional[int] = None
148-
callback_url: Optional[str] = None
129+
source_code: Optional[str] = Field(default=None, repr=True)
130+
language: Union[LanguageAlias, int] = Field(
131+
default=LanguageAlias.PYTHON,
132+
repr=True,
133+
)
134+
additional_files: Optional[str] = Field(default=None, repr=True)
135+
compiler_options: Optional[str] = Field(default=None, repr=True)
136+
command_line_arguments: Optional[str] = Field(default=None, repr=True)
137+
stdin: Optional[str] = Field(default=None, repr=True)
138+
expected_output: Optional[str] = Field(default=None, repr=True)
139+
cpu_time_limit: Optional[float] = Field(default=None, repr=True)
140+
cpu_extra_time: Optional[float] = Field(default=None, repr=True)
141+
wall_time_limit: Optional[float] = Field(default=None, repr=True)
142+
memory_limit: Optional[float] = Field(default=None, repr=True)
143+
stack_limit: Optional[int] = Field(default=None, repr=True)
144+
max_processes_and_or_threads: Optional[int] = Field(default=None, repr=True)
145+
enable_per_process_and_thread_time_limit: Optional[bool] = Field(
146+
default=None, repr=True
147+
)
148+
enable_per_process_and_thread_memory_limit: Optional[bool] = Field(
149+
default=None, repr=True
150+
)
151+
max_file_size: Optional[int] = Field(default=None, repr=True)
152+
redirect_stderr_to_stdout: Optional[bool] = Field(default=None, repr=True)
153+
enable_network: Optional[bool] = Field(default=None, repr=True)
154+
number_of_runs: Optional[int] = Field(default=None, repr=True)
155+
callback_url: Optional[str] = Field(default=None, repr=True)
149156

150157
# Post-execution submission attributes.
151-
stdout: Optional[str] = None
152-
stderr: Optional[str] = None
153-
compile_output: Optional[str] = None
154-
message: Optional[str] = None
155-
exit_code: Optional[int] = None
156-
exit_signal: Optional[int] = None
157-
status: Optional[Status] = None
158-
created_at: Optional[datetime] = None
159-
finished_at: Optional[datetime] = None
160-
token: str = ""
161-
time: Optional[float] = None
162-
wall_time: Optional[float] = None
163-
memory: Optional[float] = None
164-
post_execution_filesystem: Optional[Filesystem] = None
158+
stdout: Optional[str] = Field(default=None, repr=True)
159+
stderr: Optional[str] = Field(default=None, repr=True)
160+
compile_output: Optional[str] = Field(default=None, repr=True)
161+
message: Optional[str] = Field(default=None, repr=True)
162+
exit_code: Optional[int] = Field(default=None, repr=True)
163+
exit_signal: Optional[int] = Field(default=None, repr=True)
164+
status: Optional[Status] = Field(default=None, repr=True)
165+
created_at: Optional[datetime] = Field(default=None, repr=True)
166+
finished_at: Optional[datetime] = Field(default=None, repr=True)
167+
token: Optional[UUID4] = Field(default=None, repr=True)
168+
time: Optional[float] = Field(default=None, repr=True)
169+
wall_time: Optional[float] = Field(default=None, repr=True)
170+
memory: Optional[float] = Field(default=None, repr=True)
171+
post_execution_filesystem: Optional[Filesystem] = Field(default=None, repr=True)
172+
173+
model_config = ConfigDict(extra="ignore")
174+
175+
@field_validator(*ENCODED_FIELDS, mode="before")
176+
@classmethod
177+
def process_encoded_fields(cls, value: str) -> Optional[str]:
178+
"""Validate all encoded attributes."""
179+
if value is None:
180+
return None
181+
else:
182+
try:
183+
return decode(value)
184+
except Exception:
185+
return value
186+
187+
@field_validator("post_execution_filesystem", mode="before")
188+
@classmethod
189+
def process_post_execution_filesystem(cls, content: str) -> Filesystem:
190+
"""Validate post_execution_filesystem attribute."""
191+
return Filesystem(content=content)
192+
193+
@field_validator("status", mode="before")
194+
@classmethod
195+
def process_status(cls, value: dict) -> Status:
196+
"""Validate status attribute."""
197+
return Status(value["id"])
198+
199+
@field_validator("language", mode="before")
200+
@classmethod
201+
def process_language(
202+
cls, value: Union[LanguageAlias, dict]
203+
) -> Union[LanguageAlias, int]:
204+
"""Validate status attribute."""
205+
if isinstance(value, dict):
206+
return value["id"]
207+
else:
208+
return value
165209

166210
def set_attributes(self, attributes: dict[str, Any]) -> None:
167211
"""Set Submissions attributes while taking into account different
@@ -177,7 +221,7 @@ def set_attributes(self, attributes: dict[str, Any]) -> None:
177221
if attr in SKIP_FIELDS:
178222
continue
179223

180-
if attr in ENCODED_FIELDS and attr not in ("post_execution_filesystem",):
224+
if attr in ENCODED_FIELDS:
181225
value = decode(value) if value else None
182226
elif attr == "status":
183227
value = Status(value["id"])
@@ -229,10 +273,6 @@ def pre_execution_copy(self) -> "Submission":
229273
setattr(new_submission, attr, copy.deepcopy(getattr(self, attr)))
230274
return new_submission
231275

232-
def __repr__(self) -> str:
233-
arguments = ", ".join(f"{field}={getattr(self, field)!r}" for field in FIELDS)
234-
return f"{self.__class__.__name__}({arguments})"
235-
236276
def __iter__(self):
237277
if self.post_execution_filesystem is None:
238278
return iter([])

tests/test_submission.py

+47
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,53 @@
11
from judge0 import Status, Submission, wait
22

33

4+
def test_from_json():
5+
submission_dict = {
6+
"source_code": "cHJpbnQoJ0hlbGxvLCBXb3JsZCEnKQ==",
7+
"language_id": 100,
8+
"stdin": None,
9+
"expected_output": None,
10+
"stdout": "SGVsbG8sIFdvcmxkIQo=",
11+
"status_id": 3,
12+
"created_at": "2024-12-09T17:22:55.662Z",
13+
"finished_at": "2024-12-09T17:22:56.045Z",
14+
"time": "0.152",
15+
"memory": 13740,
16+
"stderr": None,
17+
"token": "5513d8ca-975b-4499-b54b-342f1952d00e",
18+
"number_of_runs": 1,
19+
"cpu_time_limit": "5.0",
20+
"cpu_extra_time": "1.0",
21+
"wall_time_limit": "10.0",
22+
"memory_limit": 128000,
23+
"stack_limit": 64000,
24+
"max_processes_and_or_threads": 60,
25+
"enable_per_process_and_thread_time_limit": False,
26+
"enable_per_process_and_thread_memory_limit": False,
27+
"max_file_size": 1024,
28+
"compile_output": None,
29+
"exit_code": 0,
30+
"exit_signal": None,
31+
"message": None,
32+
"wall_time": "0.17",
33+
"compiler_options": None,
34+
"command_line_arguments": None,
35+
"redirect_stderr_to_stdout": False,
36+
"callback_url": None,
37+
"additional_files": None,
38+
"enable_network": False,
39+
"post_execution_filesystem": "UEsDBBQACAAIANyKiVkAAAAAAAAAABYAAAAJABwAc"
40+
"2NyaXB0LnB5VVQJAANvJ1dncCdXZ3V4CwABBOgDAAAE6AMAACsoyswr0VD3SM3JyddRCM8v"
41+
"yklRVNcEAFBLBwgynNLKGAAAABYAAABQSwECHgMUAAgACADciolZMpzSyhgAAAAWAAAACQA"
42+
"YAAAAAAABAAAApIEAAAAAc2NyaXB0LnB5VVQFAANvJ1dndXgLAAEE6AMAAAToAwAAUEsFBg"
43+
"AAAAABAAEATwAAAGsAAAAAAA==",
44+
"status": {"id": 3, "description": "Accepted"},
45+
"language": {"id": 100, "name": "Python (3.12.5)"},
46+
}
47+
48+
_ = Submission(**submission_dict)
49+
50+
451
def test_status_before_and_after_submission(request):
552
client = request.getfixturevalue("judge0_ce_client")
653
submission = Submission(source_code='print("Hello World!")')

0 commit comments

Comments
 (0)