Skip to content
Closed
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
855fa05
Allow more lenient api key through configuration
PGijsbers Feb 12, 2026
5c551c7
Added RFC with some failing linting/type checks
PGijsbers Feb 11, 2026
81b4f60
make access safe even if toml doesn't have dev section
PGijsbers Feb 11, 2026
50ed235
Simplify model definition
PGijsbers Feb 12, 2026
dcc5fcd
Update name in docstring
PGijsbers Feb 12, 2026
b6db690
Rewrite errors to separate classes
PGijsbers Feb 13, 2026
5e12e74
Remove unused dictionary
PGijsbers Feb 13, 2026
4a0d5cb
Remove the ProblemType class as it was confusing and only for tests
PGijsbers Feb 13, 2026
97d5378
Provide default codes for the different errors based on PHP codes
PGijsbers Feb 13, 2026
9832365
chore: enable selective docstring linting (remove global D ignore)
Feb 27, 2026
9d865d8
Merge upstream/main with conflict resolution in datasets_test.py
Toton642 Feb 28, 2026
e7e7d63
fix: address code review comments - config handling and test assertions
Toton642 Feb 28, 2026
0fdfd4a
fix: remove duplicate line-length setting from [tool.ruff.lint]
Toton642 Feb 28, 2026
d893f56
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 28, 2026
9c283dd
fix: use AuthenticationRequiredError for missing user in tag_dataset
Toton642 Feb 28, 2026
8e5dbe5
Merge remote-tracking branch 'origin/feat/enable-docstring-linting' i…
Toton642 Feb 28, 2026
4dc3a5b
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 28, 2026
8c8bba3
fix: handle race condition in study creation with alias
Toton642 Feb 28, 2026
83eb22f
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 28, 2026
f55d3de
fix: handle non-JSON responses in datasets migration test
Toton642 Feb 28, 2026
1695f6f
fix: preserve original type of error code in problem+json response
Toton642 Feb 28, 2026
48ddac4
docs: add docstrings to core modules to enable docstring linting
Toton642 Feb 28, 2026
2518d9b
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 28, 2026
6fcf09d
docs: add missing docstrings to database modules
Toton642 Feb 28, 2026
731c180
Merge remote-tracking branch 'origin/feat/enable-docstring-linting'
Toton642 Feb 28, 2026
880fc25
fix: resolve ruff docstring failures
Toton642 Feb 28, 2026
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
11 changes: 7 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,18 @@ show_missing=true
line-length = 100

[tool.ruff.lint]
# The D (doc) and DTZ (datetime zone) lint classes current heavily violated - fix later
line-length = 100
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
# Gradually enable docstring (D) checks: remove global ignore for "D"
# and keep specific D-codes ignored that are still noisy.
select = ["ALL"]
ignore = [
"CPY", # we do not require copyright in every file
"D", # todo: docstring linting
"CPY", # we do not require copyright in every file
# D (docstrings) — removed to enable most D-rules. We continue to ignore
# some specific D-codes that are currently noisy:
"D203",
"D204",
"D213",
"DTZ", # To add
"DTZ", # to add
# Linter does not detect when types are used for Pydantic
"TC001",
"TC003",
Expand Down
357 changes: 352 additions & 5 deletions src/core/errors.py
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)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
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
Loading