Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
b8ff295
WIP: added groups attribute to AssignmentSettings
nadjajovancevic Oct 28, 2025
fd0147d
fix: add deleted lines back
nadjajovancevic Nov 5, 2025
e19ec64
fix: add missing attribute (group) in test_base_handler.py
nadjajovancevic Nov 5, 2025
0da68dd
fix: explicitly check if file is a jupyter notebook instead of using …
nadjajovancevic Nov 17, 2025
a36a6e5
test: rewrote test to check the inclusion of python files
nadjajovancevic Nov 17, 2025
c72bd72
WIP: add cell_timeout to assignment settings
nadjajovancevic Nov 18, 2025
bccf0a9
WIP: create function to validate user custom cell timeout against con…
nadjajovancevic Nov 19, 2025
c0b033e
WIP: initial API endpoint for config
nadjajovancevic Nov 24, 2025
2aa5c95
refactor: assign min/max values to correct variable
nadjajovancevic Nov 24, 2025
892f925
WIP: set default cell timeout in config file
nadjajovancevic Nov 24, 2025
32d34f5
test: write API test for api/config endpoint
nadjajovancevic Nov 24, 2025
425a6b6
fix: set correct seconds for max_cell_timeout
nadjajovancevic Dec 2, 2025
96bcdb9
refactor: remove unnecessary help function
nadjajovancevic Dec 2, 2025
eaa732d
Merge branch 'release-0.9.1' into feat/custom-cell-timeout
nadjajovancevic Dec 2, 2025
c953c80
chore: fix a help comment
nadjajovancevic Dec 2, 2025
be95663
Merge remote-tracking branch 'origin/feat/custom-cell-timeout' into f…
nadjajovancevic Dec 2, 2025
30cecf4
fix: delete function that was merged wrongly after merge conflict
nadjajovancevic Dec 2, 2025
076765a
refactor: define parameter types and change name of a variable
nadjajovancevic Dec 3, 2025
739574a
chore: change a help comment
nadjajovancevic Dec 3, 2025
55460c6
fix: remove wrongly merged code
nadjajovancevic Dec 3, 2025
f8f054f
fix: remove unnecessary argument passed to timeout_func
nadjajovancevic Dec 3, 2025
384b5c5
fix: rewrite tests to adhere to new implementation of timeout_func
nadjajovancevic Dec 3, 2025
ad56cf8
fix: set parameter type to parent class
nadjajovancevic Dec 5, 2025
ea397c7
test: test setting default_cell_timeout to an invalid value
nadjajovancevic Dec 5, 2025
5cac6e4
fix: change timeout_func trait type to Int
nadjajovancevic Dec 5, 2025
92a9929
refactor: refactor _determine_cell_timeout func
nadjajovancevic Dec 5, 2025
d91a05b
fix: validate config values of default, min and max cell timeout
nadjajovancevic Dec 5, 2025
48699ee
config: set default_cell_timeoutin grader_service_config.py for docker
nadjajovancevic Dec 5, 2025
44fb918
fix: pass the result of _determine_cell_timeout correctly
nadjajovancevic Dec 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions api-tests/api/get-config.bru
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
meta {
name: get-config
type: http
seq: 2
}

get {
url: {{grader-base-url}}/api/config
body: none
auth: inherit
}

settings {
encodeUrl: true
timeout: 0
}
3 changes: 3 additions & 0 deletions docs/source/_static/openapi/schemas.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ components:
group:
type: "string"
example: "Chapter 1: Data Types"
cell_timeout:
type: "integer"
example: "300"
example:
late_submission:
- period: "P1W1D"
Expand Down
1 change: 1 addition & 0 deletions examples/dev_environment/grader_service_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
c.GraderService.log_level = "DEBUG"

c.RequestHandlerConfig.autograde_executor_class = LocalAutogradeExecutor
c.LocalAutogradeExecutor.default_cell_timeout = 200

c.CeleryApp.conf = dict(
broker_url="amqp://localhost",
Expand Down
32 changes: 29 additions & 3 deletions grader_service/api/models/assignment_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class AssignmentSettings(Model):
Do not edit the class manually.
"""

def __init__(self, deadline=None, max_submissions=None, allowed_files=[], late_submission=None, autograde_type='auto', group=None): # noqa: E501
def __init__(self, deadline=None, max_submissions=None, allowed_files=[], late_submission=None, autograde_type='auto', group=None, cell_timeout=None): # noqa: E501
"""AssignmentSettings - a model defined in OpenAPI

:param deadline: The deadline of this AssignmentSettings. # noqa: E501
Expand All @@ -29,14 +29,17 @@ def __init__(self, deadline=None, max_submissions=None, allowed_files=[], late_s
:type autograde_type: str
:param group: The group of this AssignmentSettings. # noqa: E501
:type group: str
:param cell_timeout: The cell_timeout of this AssignmentSettings. # noqa: E501
:type cell_timeout: int
"""
self.openapi_types = {
'deadline': datetime,
'max_submissions': int,
'allowed_files': List[str],
'late_submission': List[SubmissionPeriod],
'autograde_type': str,
'group': str
'group': str,
'cell_timeout': int
}

self.attribute_map = {
Expand All @@ -45,7 +48,8 @@ def __init__(self, deadline=None, max_submissions=None, allowed_files=[], late_s
'allowed_files': 'allowed_files',
'late_submission': 'late_submission',
'autograde_type': 'autograde_type',
'group': 'group'
'group': 'group',
'cell_timeout': 'cell_timeout'
}

self._deadline = deadline
Expand All @@ -54,6 +58,7 @@ def __init__(self, deadline=None, max_submissions=None, allowed_files=[], late_s
self._late_submission = late_submission
self._autograde_type = autograde_type
self._group = group
self._cell_timeout = cell_timeout

@classmethod
def from_dict(cls, dikt) -> 'AssignmentSettings':
Expand Down Expand Up @@ -197,3 +202,24 @@ def group(self, group: str):
"""

self._group = group

@property
def cell_timeout(self) -> int:
"""Gets the cell_timeout of this AssignmentSettings.


:return: The cell_timeout of this AssignmentSettings.
:rtype: int
"""
return self._cell_timeout

@cell_timeout.setter
def cell_timeout(self, cell_timeout: int):
"""Sets the cell_timeout of this AssignmentSettings.


:param cell_timeout: The cell_timeout of this AssignmentSettings.
:type cell_timeout: int
"""

self._cell_timeout = cell_timeout
44 changes: 36 additions & 8 deletions grader_service/autograding/local_grader.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,18 @@
from sqlalchemy.orm import Session
from traitlets.config import Config
from traitlets.config.configurable import LoggingConfigurable
from traitlets.traitlets import Callable, TraitError, Type, Unicode, validate
from traitlets.traitlets import Callable, Int, TraitError, Type, Unicode, validate

from grader_service.autograding.git_manager import GitSubmissionManager
from grader_service.autograding.utils import collect_logs, executable_validator, rmtree
from grader_service.convert.converters.autograde import Autograde
from grader_service.convert.gradebook.models import GradeBookModel
from grader_service.orm.assignment import Assignment
from grader_service.orm.lecture import Lecture
from grader_service.orm.submission import AutoStatus, ManualStatus, Submission
from grader_service.orm.submission_logs import SubmissionLogs
from grader_service.orm.submission_properties import SubmissionProperties


def default_timeout_func(lecture: Lecture) -> int:
return 360


class LocalAutogradeExecutor(LoggingConfigurable):
"""
Runs an autograde job on the local machine
Expand All @@ -47,11 +42,20 @@ class LocalAutogradeExecutor(LoggingConfigurable):
git_manager_class = Type(GitSubmissionManager, allow_none=False).tag(config=True)

timeout_func = Callable(
default_timeout_func,
allow_none=False,
help="Function that takes a lecture as an argument and returns the cell timeout in seconds.",
help="Function that returns the cell timeout in seconds, either user-defined, from configuration or default values.",
).tag(config=True)

default_cell_timeout = Int(300, help="Default cell timeout in seconds, defaults to 300").tag(
config=True
)

min_cell_timeout = Int(10, help="Min cell timeout in seconds, defaults to 10.").tag(config=True)

max_cell_timeout = Int(86400, help="Max cell timeout in seconds, defaults to 86400").tag(
config=True
)

def __init__(
self, grader_service_dir: str, submission: Submission, close_session: bool = True, **kwargs
):
Expand Down Expand Up @@ -86,6 +90,8 @@ def __init__(
# Git manager performs the git operations when creating a new repo for the grading results
self.git_manager = self.git_manager_class(grader_service_dir, self.submission)

self.timeout_func = self._determine_cell_timeout

def start(self):
"""
Starts the autograding job.
Expand Down Expand Up @@ -320,6 +326,28 @@ def _cleanup(self) -> None:
if self.close_session:
self.session.close()

def _determine_cell_timeout(self):
cell_timeout = self.default_cell_timeout
if self.assignment.settings.cell_timeout is not None:
custom_cell_timeout = self.assignment.settings.cell_timeout
self.log.info(
f"Found custom cell timeout in assignment settings: {custom_cell_timeout} seconds."
)
if custom_cell_timeout < self.min_cell_timeout:
cell_timeout = self.min_cell_timeout
self.log.info(
f"Custom cell timeout is smaller than the minimum, setting it to the minimum value: {custom_cell_timeout}."
)
elif custom_cell_timeout > self.max_cell_timeout:
cell_timeout = self.max_cell_timeout
self.log.info(
f"Custom cell timeout is bigger than the maximum, setting it to the maximum value: {custom_cell_timeout}."
)
else:
cell_timeout = custom_cell_timeout

return cell_timeout

@validate("relative_input_path", "relative_output_path")
def _validate_service_dir(self, proposal):
path: str = proposal["value"]
Expand Down
2 changes: 2 additions & 0 deletions grader_service/convert/converters/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,8 @@ def matches_allowed_patterns(file_path):
"""
Check if a file matches any of the allowed glob patterns.
"""
if file_path.endswith(".ipynb"):
return False
return any(fnmatch.fnmatch(file_path, pattern) for pattern in files_patterns)

def is_ignored(file_path):
Expand Down
40 changes: 34 additions & 6 deletions grader_service/handlers/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,44 @@
from grader_service.registry import VersionSpecifier, register_handler


def _get_effective_executor_value(app_cfg, executor_class, trait_name):
"""
Return the configured value for trait_name if present in app_cfg,
otherwise return the class default pulled from the trait metadata.
"""
# app_cfg is a traitlets.config.Config object (mapping-like)
# executor_class is the class (LocalAutogradeExecutor or similar)

# 1) look up the per-class node as a mapping (not attribute access)
user_node = app_cfg.get(executor_class.__name__, None)

# user_node may be None, or a Config object / dict-like. Use mapping access.
if user_node is not None and trait_name in user_node:
# Use .get to return the exact user-supplied value (won't be a lazy object)
return user_node.get(trait_name)

# 2) fallback to the trait's default from the class metadata
return executor_class.class_traits()[trait_name].default()


@register_handler(path=r"\/api\/config\/?", version_specifier=VersionSpecifier.ALL)
class ConfigHandler(GraderBaseHandler):
"""
Handler class for requests to /config
"""

@authorize([Scope.student, Scope.tutor, Scope.instructor])
@authorize([Scope.tutor, Scope.instructor])
async def get(self):
"""
Gathers useful config for the grader labextension and returns it.
:return: config in dict
"""
self.write({})
app_cfg = self.application.config
executor_class = app_cfg.RequestHandlerConfig.autograde_executor_class

def resolve(name):
return _get_effective_executor_value(app_cfg, executor_class, name)

self.write_json(
{
"default_cell_timeout": resolve("default_cell_timeout"),
"min_cell_timeout": resolve("min_cell_timeout"),
"max_cell_timeout": resolve("max_cell_timeout"),
}
)
Loading