From ffcdfef3da7be73f0cbb38e20ff79831a279819c Mon Sep 17 00:00:00 2001 From: Marcus Messer Date: Tue, 21 Apr 2026 12:05:02 +0100 Subject: [PATCH 1/3] Resolve merge conflict: drop /preview case, fix healthcheck indentation in handle_muEd_command Co-Authored-By: Claude Sonnet 4.6 --- handler.py | 55 ++++++++++++++++++++++++++----------- tests/mued_handling_test.py | 35 +++++++++++++---------- 2 files changed, 59 insertions(+), 31 deletions(-) diff --git a/handler.py b/handler.py index bec16e8..95a383d 100755 --- a/handler.py +++ b/handler.py @@ -122,26 +122,49 @@ def handle_muEd_command(event: JsonType, command: str) -> HandlerResponse: Returns: HandlerResponse: The response object returned by the handler. """ - version_error = check_muEd_version(event) - if version_error: - return wrap_muEd_response(version_error, event, 406) + try: + version_error = check_muEd_version(event) + if version_error: + return wrap_muEd_response(version_error, event, 406) - if command == "eval": - body = parse.body(event) - validate.body(body, MuEdReqBodyValidators.EVALUATION) - response = commands.evaluate_muEd(body) - validate.body(response, MuEdResBodyValidators.EVALUATION) + if command == "eval": + body = parse.body(event) + validate.body(body, MuEdReqBodyValidators.EVALUATION) + response = commands.evaluate_muEd(body) + validate.body(response, MuEdResBodyValidators.EVALUATION) - elif command == "healthcheck": - response = commands.healthcheck_muEd() - validate.body(response, MuEdResBodyValidators.HEALTHCHECK) + elif command == "healthcheck": + response = commands.healthcheck_muEd() + validate.body(response, MuEdResBodyValidators.HEALTHCHECK) - else: - response = Response( - error=ErrorResponse(message=f"Unknown command '{command}'.") - ) + else: + error = { + "title": "Not implemented", + "message": f"Unknown command '{command}'.", + "code": "NOT_IMPLEMENTED", + } + return wrap_muEd_response(error, event, 501) - return wrap_muEd_response(response, event) + return wrap_muEd_response(response, event) + + except (ParseError, ValidationError) as e: + error = { + "title": "Bad request", + "message": e.message, + "code": "VALIDATION_ERROR", + "details": {"error": str(e.error_thrown)} if e.error_thrown else None, + } + return wrap_muEd_response(error, event, 400) + + except EvaluationException as e: + detail = str(e) if str(e) else repr(e) + error = {"title": "Internal server error", "message": detail, "code": "INTERNAL_ERROR"} + return wrap_muEd_response(error, event, 500) + + except Exception as e: + detail = str(e) if str(e) else repr(e) + error = {"title": "Internal server error", "message": detail, "code": "INTERNAL_ERROR"} + return wrap_muEd_response(error, event, 500) def handler(event: JsonType, _=None) -> HandlerResponse: diff --git a/tests/mued_handling_test.py b/tests/mued_handling_test.py index e834b3a..fb14259 100644 --- a/tests/mued_handling_test.py +++ b/tests/mued_handling_test.py @@ -78,8 +78,9 @@ def test_evaluate_missing_submission_returns_error(self): response = handler(event) - self.assertIn("error", response) - self.assertIn("submission", response["error"]["detail"]) # type: ignore + self.assertEqual(response["statusCode"], 400) + body = json.loads(response["body"]) + self.assertEqual(body["code"], "VALIDATION_ERROR") def test_evaluate_invalid_submission_type_returns_error(self): event = { @@ -89,18 +90,19 @@ def test_evaluate_invalid_submission_type_returns_error(self): response = handler(event) - self.assertIn("error", response) + self.assertEqual(response["statusCode"], 400) + body = json.loads(response["body"]) + self.assertEqual(body["code"], "VALIDATION_ERROR") def test_evaluate_bodyless_event_returns_error(self): event = {"path": "/evaluate", "random": "metadata"} response = handler(event) - self.assertIn("error", response) - self.assertEqual( - response["error"]["message"], # type: ignore - "No data supplied in request body.", - ) + self.assertEqual(response["statusCode"], 400) + body = json.loads(response["body"]) + self.assertEqual(body["code"], "VALIDATION_ERROR") + self.assertEqual(body["message"], "No data supplied in request body.") def test_healthcheck(self): event = {"path": "/evaluate/health"} @@ -325,18 +327,19 @@ def test_preview_missing_submission_returns_error(self): response = handler(event) - self.assertIn("error", response) + self.assertEqual(response["statusCode"], 400) + body = json.loads(response["body"]) + self.assertEqual(body["code"], "VALIDATION_ERROR") def test_preview_bodyless_event_returns_error(self): event = {"path": "/evaluate", "random": "metadata"} response = handler(event) - self.assertIn("error", response) - self.assertEqual( - response["error"]["message"], # type: ignore - "No data supplied in request body.", - ) + self.assertEqual(response["statusCode"], 400) + body = json.loads(response["body"]) + self.assertEqual(body["code"], "VALIDATION_ERROR") + self.assertEqual(body["message"], "No data supplied in request body.") def test_preview_invalid_submission_type_returns_error(self): event = { @@ -349,7 +352,9 @@ def test_preview_invalid_submission_type_returns_error(self): response = handler(event) - self.assertIn("error", response) + self.assertEqual(response["statusCode"], 400) + body = json.loads(response["body"]) + self.assertEqual(body["code"], "VALIDATION_ERROR") def test_presubmission_disabled_runs_normal_evaluation(self): event = { From 40ab2584e700db8c1e12845ede2917372c350c20 Mon Sep 17 00:00:00 2001 From: Marcus Messer Date: Mon, 20 Apr 2026 12:10:47 +0100 Subject: [PATCH 2/3] Updated `healthcheck` response handling to include status-based `503` or `200` codes, ensuring proper HTTP response for "UNAVAILABLE" status. --- handler.py | 2 ++ tools/commands.py | 1 + 2 files changed, 3 insertions(+) diff --git a/handler.py b/handler.py index 95a383d..0773461 100755 --- a/handler.py +++ b/handler.py @@ -136,6 +136,8 @@ def handle_muEd_command(event: JsonType, command: str) -> HandlerResponse: elif command == "healthcheck": response = commands.healthcheck_muEd() validate.body(response, MuEdResBodyValidators.HEALTHCHECK) + status_code = 503 if response.get("status") == "UNAVAILABLE" else 200 + return wrap_muEd_response(response, event, status_code) else: error = { diff --git a/tools/commands.py b/tools/commands.py index b44d33f..ee84a54 100644 --- a/tools/commands.py +++ b/tools/commands.py @@ -55,6 +55,7 @@ class CaseResult(NamedTuple): is_correct: bool = False feedback: str = "" + warning: Optional[CaseWarning] = None From 58edd8da409dde627d1008827c984f428afe24e9 Mon Sep 17 00:00:00 2001 From: Marcus Messer Date: Wed, 27 May 2026 20:34:09 +0100 Subject: [PATCH 3/3] Refactored error handling to utilize `ErrorCode` enum for improved consistency and maintainability. --- handler.py | 12 ++++++------ tools/utils.py | 8 ++++++++ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/handler.py b/handler.py index 0773461..e92e287 100755 --- a/handler.py +++ b/handler.py @@ -5,7 +5,7 @@ from .tools.parse import ParseError from typing import Any, Optional -from .tools.utils import DocsResponse, ErrorResponse, HandlerResponse, JsonType, Response +from .tools.utils import DocsResponse, ErrorCode, ErrorResponse, HandlerResponse, JsonType, Response from .tools.validate import ( LegacyReqBodyValidators, LegacyResBodyValidators, @@ -103,7 +103,7 @@ def check_muEd_version(event: JsonType) -> Optional[HandlerResponse]: f"The requested API version '{version}' is not supported. " f"Supported versions are: {commands.SUPPORTED_MUED_VERSIONS}." ), - "code": "VERSION_NOT_SUPPORTED", + "code": ErrorCode.VERSION_NOT_SUPPORTED, "details": { "requestedVersion": version, "supportedVersions": commands.SUPPORTED_MUED_VERSIONS, @@ -143,7 +143,7 @@ def handle_muEd_command(event: JsonType, command: str) -> HandlerResponse: error = { "title": "Not implemented", "message": f"Unknown command '{command}'.", - "code": "NOT_IMPLEMENTED", + "code": ErrorCode.NOT_IMPLEMENTED, } return wrap_muEd_response(error, event, 501) @@ -153,19 +153,19 @@ def handle_muEd_command(event: JsonType, command: str) -> HandlerResponse: error = { "title": "Bad request", "message": e.message, - "code": "VALIDATION_ERROR", + "code": ErrorCode.VALIDATION_ERROR, "details": {"error": str(e.error_thrown)} if e.error_thrown else None, } return wrap_muEd_response(error, event, 400) except EvaluationException as e: detail = str(e) if str(e) else repr(e) - error = {"title": "Internal server error", "message": detail, "code": "INTERNAL_ERROR"} + error = {"title": "Internal server error", "message": detail, "code": ErrorCode.INTERNAL_ERROR} return wrap_muEd_response(error, event, 500) except Exception as e: detail = str(e) if str(e) else repr(e) - error = {"title": "Internal server error", "message": detail, "code": "INTERNAL_ERROR"} + error = {"title": "Internal server error", "message": detail, "code": ErrorCode.INTERNAL_ERROR} return wrap_muEd_response(error, event, 500) diff --git a/tools/utils.py b/tools/utils.py index 7cd00d2..6d5b82c 100644 --- a/tools/utils.py +++ b/tools/utils.py @@ -1,10 +1,18 @@ from __future__ import annotations +import enum from typing import Any, Callable, Dict, Literal, TypedDict, Union from typing_extensions import NotRequired JsonType = Dict[str, Any] + + +class ErrorCode(str, enum.Enum): + VALIDATION_ERROR = "VALIDATION_ERROR" + VERSION_NOT_SUPPORTED = "VERSION_NOT_SUPPORTED" + NOT_IMPLEMENTED = "NOT_IMPLEMENTED" + INTERNAL_ERROR = "INTERNAL_ERROR" SupportedCommands = Literal["eval", "grade", "preview", "healthcheck"] EvaluationFunctionType = Callable[[Any, Any, JsonType], JsonType]