diff --git a/handler.py b/handler.py index bec16e8..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, @@ -122,26 +122,51 @@ 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) + + 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) - if command == "eval": - body = parse.body(event) - validate.body(body, MuEdReqBodyValidators.EVALUATION) - response = commands.evaluate_muEd(body) - validate.body(response, MuEdResBodyValidators.EVALUATION) + else: + error = { + "title": "Not implemented", + "message": f"Unknown command '{command}'.", + "code": ErrorCode.NOT_IMPLEMENTED, + } + return wrap_muEd_response(error, event, 501) - elif command == "healthcheck": - response = commands.healthcheck_muEd() - validate.body(response, MuEdResBodyValidators.HEALTHCHECK) + return wrap_muEd_response(response, event) - else: - response = Response( - error=ErrorResponse(message=f"Unknown command '{command}'.") - ) + except (ParseError, ValidationError) as e: + error = { + "title": "Bad request", + "message": e.message, + "code": ErrorCode.VALIDATION_ERROR, + "details": {"error": str(e.error_thrown)} if e.error_thrown else None, + } + return wrap_muEd_response(error, event, 400) - return wrap_muEd_response(response, event) + except EvaluationException as e: + detail = str(e) if str(e) else repr(e) + 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": ErrorCode.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 a5a0629..d077414 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"} @@ -389,18 +391,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 = { @@ -413,7 +416,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 = { diff --git a/tools/commands.py b/tools/commands.py index d73bcbb..8d8b21b 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 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]