-
-
Notifications
You must be signed in to change notification settings - Fork 16
[ENH] RFC9457 compliant errors #238
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
PGijsbers
wants to merge
9
commits into
main
Choose a base branch
from
errors
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
855fa05
Allow more lenient api key through configuration
PGijsbers 5c551c7
Added RFC with some failing linting/type checks
PGijsbers 81b4f60
make access safe even if toml doesn't have dev section
PGijsbers 50ed235
Simplify model definition
PGijsbers dcc5fcd
Update name in docstring
PGijsbers b6db690
Rewrite errors to separate classes
PGijsbers 5e12e74
Remove unused dictionary
PGijsbers 4a0d5cb
Remove the ProblemType class as it was confusing and only for tests
PGijsbers 97d5378
Provide default codes for the different errors based on PHP codes
PGijsbers File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,354 @@ | ||
| from enum import IntEnum | ||
| """RFC 9457 Problem Details for HTTP APIs. | ||
|
|
||
| This module provides RFC 9457 compliant error handling for the OpenML REST API. | ||
| See: https://www.rfc-editor.org/rfc/rfc9457.html | ||
| """ | ||
|
|
||
| class DatasetError(IntEnum): | ||
| NOT_FOUND = 111 | ||
| NO_ACCESS = 112 | ||
| NO_DATA_FILE = 113 | ||
| from http import HTTPStatus | ||
|
|
||
| from fastapi import Request | ||
| from fastapi.responses import JSONResponse | ||
|
|
||
| # ============================================================================= | ||
| # Base Exception | ||
| # ============================================================================= | ||
|
|
||
|
|
||
| class ProblemDetailError(Exception): | ||
| """Base exception for RFC 9457 compliant error responses. | ||
|
|
||
| Subclasses should define class attributes: | ||
| - uri: The problem type URI | ||
| - title: Human-readable title | ||
| - _default_status_code: HTTP status code | ||
| - _default_code: Legacy error code (optional) | ||
|
|
||
| The status_code and code can be overridden per-instance. | ||
| """ | ||
|
|
||
| uri: str = "about:blank" | ||
| title: str = "An error occurred" | ||
| _default_status_code: HTTPStatus = HTTPStatus.INTERNAL_SERVER_ERROR | ||
| _default_code: int | None = None | ||
|
|
||
| def __init__( | ||
| self, | ||
| detail: str, | ||
| *, | ||
| code: int | str | None = None, | ||
| instance: str | None = None, | ||
| status_code: HTTPStatus | None = None, | ||
| ) -> None: | ||
| self.detail = detail | ||
| self._code_override = code | ||
| self.instance = instance | ||
| self._status_code_override = status_code | ||
| super().__init__(detail) | ||
|
|
||
| @property | ||
| def status_code(self) -> HTTPStatus: | ||
| """Return the status code, preferring instance override over class default.""" | ||
| if self._status_code_override is not None: | ||
| return self._status_code_override | ||
| return self._default_status_code | ||
|
|
||
| @property | ||
| def code(self) -> int | str | None: | ||
| """Return the code, preferring instance override over class default.""" | ||
| if self._code_override is not None: | ||
| return self._code_override | ||
| return self._default_code | ||
|
|
||
|
|
||
| def problem_detail_exception_handler( | ||
| request: Request, # noqa: ARG001 | ||
| exc: ProblemDetailError, | ||
| ) -> JSONResponse: | ||
| """FastAPI exception handler for ProblemDetailError. | ||
|
|
||
| Returns a response with: | ||
| - Content-Type: application/problem+json | ||
| - RFC 9457 compliant JSON body | ||
| """ | ||
| content: dict[str, str | int] = { | ||
| "type": exc.uri, | ||
| "title": exc.title, | ||
| "status": int(exc.status_code), | ||
| "detail": exc.detail, | ||
| } | ||
| if exc.code is not None: | ||
| content["code"] = str(exc.code) | ||
| if exc.instance is not None: | ||
| content["instance"] = exc.instance | ||
|
|
||
| return JSONResponse( | ||
| status_code=int(exc.status_code), | ||
| content=content, | ||
| media_type="application/problem+json", | ||
| ) | ||
|
|
||
|
|
||
| # ============================================================================= | ||
| # Dataset Errors | ||
| # ============================================================================= | ||
|
|
||
|
|
||
| class DatasetNotFoundError(ProblemDetailError): | ||
| """Raised when a dataset cannot be found.""" | ||
|
|
||
| uri = "https://openml.org/problems/dataset-not-found" | ||
| title = "Dataset Not Found" | ||
| _default_status_code = HTTPStatus.NOT_FOUND | ||
| _default_code = 111 | ||
|
|
||
|
|
||
| class DatasetNoAccessError(ProblemDetailError): | ||
| """Raised when user doesn't have access to a dataset.""" | ||
|
|
||
| uri = "https://openml.org/problems/dataset-no-access" | ||
| title = "Dataset Access Denied" | ||
| _default_status_code = HTTPStatus.FORBIDDEN | ||
| _default_code = 112 | ||
|
|
||
|
|
||
| class DatasetNoDataFileError(ProblemDetailError): | ||
| """Raised when a dataset's data file is missing.""" | ||
|
|
||
| uri = "https://openml.org/problems/dataset-no-data-file" | ||
| title = "Dataset Data File Missing" | ||
| _default_status_code = HTTPStatus.PRECONDITION_FAILED | ||
| _default_code = 113 | ||
|
|
||
|
|
||
| class DatasetNotProcessedError(ProblemDetailError): | ||
| """Raised when a dataset has not been processed yet.""" | ||
|
|
||
| uri = "https://openml.org/problems/dataset-not-processed" | ||
| title = "Dataset Not Processed" | ||
| _default_status_code = HTTPStatus.PRECONDITION_FAILED | ||
| _default_code = 273 | ||
|
|
||
|
|
||
| class DatasetProcessingError(ProblemDetailError): | ||
| """Raised when a dataset had an error during processing.""" | ||
|
|
||
| uri = "https://openml.org/problems/dataset-processing-error" | ||
| title = "Dataset Processing Error" | ||
| _default_status_code = HTTPStatus.PRECONDITION_FAILED | ||
| _default_code = 274 | ||
|
|
||
|
|
||
| class DatasetNoFeaturesError(ProblemDetailError): | ||
| """Raised when a dataset has no features available.""" | ||
|
|
||
| uri = "https://openml.org/problems/dataset-no-features" | ||
| title = "Dataset Features Not Available" | ||
| _default_status_code = HTTPStatus.PRECONDITION_FAILED | ||
| _default_code = 272 | ||
|
|
||
|
|
||
| class DatasetStatusTransitionError(ProblemDetailError): | ||
| """Raised when an invalid dataset status transition is attempted.""" | ||
|
|
||
| uri = "https://openml.org/problems/dataset-status-transition" | ||
| title = "Invalid Status Transition" | ||
| _default_status_code = HTTPStatus.PRECONDITION_FAILED | ||
| _default_code = 694 | ||
|
|
||
|
|
||
| class DatasetNotOwnedError(ProblemDetailError): | ||
| """Raised when user tries to modify a dataset they don't own.""" | ||
|
|
||
| uri = "https://openml.org/problems/dataset-not-owned" | ||
| title = "Dataset Not Owned" | ||
| _default_status_code = HTTPStatus.FORBIDDEN | ||
| _default_code = 693 | ||
|
|
||
|
|
||
| class DatasetAdminOnlyError(ProblemDetailError): | ||
| """Raised when a non-admin tries to perform an admin-only action.""" | ||
|
|
||
| uri = "https://openml.org/problems/dataset-admin-only" | ||
| title = "Administrator Only" | ||
| _default_status_code = HTTPStatus.FORBIDDEN | ||
| _default_code = 696 | ||
|
|
||
|
|
||
| # ============================================================================= | ||
| # Authentication/Authorization Errors | ||
| # ============================================================================= | ||
|
|
||
|
|
||
| class AuthenticationRequiredError(ProblemDetailError): | ||
| """Raised when authentication is required but not provided.""" | ||
|
|
||
| uri = "https://openml.org/problems/authentication-required" | ||
| title = "Authentication Required" | ||
| _default_status_code = HTTPStatus.UNAUTHORIZED | ||
|
|
||
|
|
||
| class AuthenticationFailedError(ProblemDetailError): | ||
| """Raised when authentication credentials are invalid.""" | ||
|
|
||
| uri = "https://openml.org/problems/authentication-failed" | ||
| title = "Authentication Failed" | ||
| _default_status_code = HTTPStatus.UNAUTHORIZED | ||
| _default_code = 103 | ||
|
|
||
|
|
||
| class ForbiddenError(ProblemDetailError): | ||
| """Raised when user is authenticated but not authorized.""" | ||
|
|
||
| uri = "https://openml.org/problems/forbidden" | ||
| title = "Forbidden" | ||
| _default_status_code = HTTPStatus.FORBIDDEN | ||
|
|
||
|
|
||
| # ============================================================================= | ||
| # Tag Errors | ||
| # ============================================================================= | ||
|
|
||
|
|
||
| class TagAlreadyExistsError(ProblemDetailError): | ||
| """Raised when trying to add a tag that already exists.""" | ||
|
|
||
| uri = "https://openml.org/problems/tag-already-exists" | ||
| title = "Tag Already Exists" | ||
| _default_status_code = HTTPStatus.CONFLICT | ||
| _default_code = 473 | ||
|
|
||
|
|
||
| # ============================================================================= | ||
| # Search/List Errors | ||
| # ============================================================================= | ||
|
|
||
|
|
||
| class NoResultsError(ProblemDetailError): | ||
| """Raised when a search returns no results.""" | ||
|
|
||
| uri = "https://openml.org/problems/no-results" | ||
| title = "No Results Found" | ||
| _default_status_code = HTTPStatus.NOT_FOUND | ||
| _default_code = 372 | ||
|
|
||
|
|
||
| # ============================================================================= | ||
| # Study Errors | ||
| # ============================================================================= | ||
|
|
||
|
|
||
| class StudyNotFoundError(ProblemDetailError): | ||
| """Raised when a study cannot be found.""" | ||
|
|
||
| uri = "https://openml.org/problems/study-not-found" | ||
| title = "Study Not Found" | ||
| _default_status_code = HTTPStatus.NOT_FOUND | ||
|
|
||
|
|
||
| class StudyPrivateError(ProblemDetailError): | ||
| """Raised when trying to access a private study without permission.""" | ||
|
|
||
| uri = "https://openml.org/problems/study-private" | ||
| title = "Study Is Private" | ||
| _default_status_code = HTTPStatus.FORBIDDEN | ||
|
|
||
|
|
||
| class StudyLegacyError(ProblemDetailError): | ||
| """Raised when trying to access a legacy study that's no longer supported.""" | ||
|
|
||
| uri = "https://openml.org/problems/study-legacy" | ||
| title = "Legacy Study Not Supported" | ||
| _default_status_code = HTTPStatus.GONE | ||
|
|
||
|
|
||
| class StudyAliasExistsError(ProblemDetailError): | ||
| """Raised when trying to create a study with an alias that already exists.""" | ||
|
|
||
| uri = "https://openml.org/problems/study-alias-exists" | ||
| title = "Study Alias Already Exists" | ||
| _default_status_code = HTTPStatus.CONFLICT | ||
|
|
||
|
|
||
| class StudyInvalidTypeError(ProblemDetailError): | ||
| """Raised when study type configuration is invalid.""" | ||
|
|
||
| uri = "https://openml.org/problems/study-invalid-type" | ||
| title = "Invalid Study Type" | ||
| _default_status_code = HTTPStatus.BAD_REQUEST | ||
|
|
||
|
|
||
| class StudyNotEditableError(ProblemDetailError): | ||
| """Raised when trying to edit a study that cannot be edited.""" | ||
|
|
||
| uri = "https://openml.org/problems/study-not-editable" | ||
| title = "Study Not Editable" | ||
| _default_status_code = HTTPStatus.FORBIDDEN | ||
|
|
||
|
|
||
| class StudyConflictError(ProblemDetailError): | ||
| """Raised when there's a conflict with study data (e.g., duplicate attachment).""" | ||
|
|
||
| uri = "https://openml.org/problems/study-conflict" | ||
| title = "Study Conflict" | ||
| _default_status_code = HTTPStatus.CONFLICT | ||
|
|
||
|
|
||
| # ============================================================================= | ||
| # Task Errors | ||
| # ============================================================================= | ||
|
|
||
|
|
||
| class TaskNotFoundError(ProblemDetailError): | ||
| """Raised when a task cannot be found.""" | ||
|
|
||
| uri = "https://openml.org/problems/task-not-found" | ||
| title = "Task Not Found" | ||
| _default_status_code = HTTPStatus.NOT_FOUND | ||
|
|
||
|
|
||
| class TaskTypeNotFoundError(ProblemDetailError): | ||
| """Raised when a task type cannot be found.""" | ||
|
|
||
| uri = "https://openml.org/problems/task-type-not-found" | ||
| title = "Task Type Not Found" | ||
| _default_status_code = HTTPStatus.NOT_FOUND | ||
| _default_code = 241 | ||
|
|
||
|
|
||
| # ============================================================================= | ||
| # Flow Errors | ||
| # ============================================================================= | ||
|
|
||
|
|
||
| class FlowNotFoundError(ProblemDetailError): | ||
| """Raised when a flow cannot be found.""" | ||
|
|
||
| uri = "https://openml.org/problems/flow-not-found" | ||
| title = "Flow Not Found" | ||
| _default_status_code = HTTPStatus.NOT_FOUND | ||
|
|
||
|
|
||
| # ============================================================================= | ||
| # Service Errors | ||
| # ============================================================================= | ||
|
|
||
|
|
||
| class ServiceNotFoundError(ProblemDetailError): | ||
| """Raised when a service cannot be found.""" | ||
|
|
||
| uri = "https://openml.org/problems/service-not-found" | ||
| title = "Service Not Found" | ||
| _default_status_code = HTTPStatus.NOT_FOUND | ||
|
|
||
|
|
||
| # ============================================================================= | ||
| # Internal Errors | ||
| # ============================================================================= | ||
|
|
||
|
|
||
| class InternalError(ProblemDetailError): | ||
| """Raised for unexpected internal server errors.""" | ||
|
|
||
| uri = "https://openml.org/problems/internal-error" | ||
| title = "Internal Server Error" | ||
| _default_status_code = HTTPStatus.INTERNAL_SERVER_ERROR | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.