• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

SwissDataScienceCenter / renku-data-services / 8648070466

11 Apr 2024 02:04PM UTC coverage: 90.244% (-0.04%) from 90.28%
8648070466

push

gihub-action

web-flow
feat: find a project by namespace/slug (#174)

17 of 18 new or added lines in 2 files covered. (94.44%)

5 existing lines in 4 files now uncovered.

5763 of 6386 relevant lines covered (90.24%)

0.9 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.25
/components/renku_data_services/base_api/error_handler.py
1
"""The error handler for the application."""
1✔
2

3
from collections.abc import Mapping, Set
1✔
4
from sqlite3 import Error as SqliteError
1✔
5
from typing import Any, Optional, Protocol, TypeVar, Union
1✔
6

7
from asyncpg import exceptions as postgres_exceptions
1✔
8
from pydantic import ValidationError as PydanticValidationError
1✔
9
from sanic import HTTPResponse, Request, SanicException, json
1✔
10
from sanic.errorpages import BaseRenderer, TextRenderer
1✔
11
from sanic.handlers import ErrorHandler
1✔
12
from sanic.log import logger
1✔
13
from sanic_ext.exceptions import ValidationError
1✔
14
from sqlalchemy.exc import SQLAlchemyError
1✔
15

16
from renku_data_services import errors
1✔
17

18

19
class BaseError(Protocol):
1✔
20
    """Protocol for the error type of an apispec module."""
1✔
21

22
    code: int
1✔
23
    message: str
1✔
24
    detail: Optional[str]
1✔
25

26

27
class BaseErrorResponse(Protocol):
1✔
28
    """Porotocol for the error response class of an apispec module."""
1✔
29

30
    error: BaseError
1✔
31

32
    def dict(
1✔
33
        self,
34
        *,
35
        include: Optional[Union[Set[Union[int, str]], Mapping[Union[int, str], Any]]] = None,
36
        exclude: Optional[Union[Set[Union[int, str]], Mapping[Union[int, str], Any]]] = None,
37
        by_alias: bool = False,
38
        skip_defaults: Optional[bool] = None,
39
        exclude_unset: bool = False,
40
        exclude_defaults: bool = False,
41
        exclude_none: bool = False,
42
    ) -> dict[str, Any]:
43
        """Turn the response to dict."""
44
        ...
×
45

46

47
BError = TypeVar("BError", bound=BaseError)
1✔
48
BErrorResponse = TypeVar("BErrorResponse", bound=BaseErrorResponse)
1✔
49

50

51
class ApiSpec(Protocol[BErrorResponse, BError]):
1✔
52
    """Protocol for an apispec with error data."""
1✔
53

54
    ErrorResponse: BErrorResponse
1✔
55
    Error: BError
1✔
56

57

58
class CustomErrorHandler(ErrorHandler):
1✔
59
    """Central error handling."""
1✔
60

61
    def __init__(self, api_spec: ApiSpec, base: type[BaseRenderer] = TextRenderer):
1✔
62
        self.api_spec = api_spec
1✔
63
        super().__init__(base)
1✔
64

65
    def _log_unhandled_exception(self, exception: Exception):
1✔
66
        if self.debug:
1✔
67
            logger.exception("An unknown or unhandled exception occurred", exc_info=exception)
×
68
        logger.error("An unknown or unhandled exception of type %s occurred", type(exception).__name__)
1✔
69

70
    def default(self, request: Request, exception: Exception) -> HTTPResponse:
1✔
71
        """Overrides the default error handler."""
72
        formatted_exception = errors.BaseError()
1✔
73
        logger.exception("An unknown or unhandled exception occurred", exc_info=exception)
1✔
74
        match exception:
1✔
75
            case errors.BaseError():
1✔
76
                formatted_exception = exception
1✔
77
            case ValidationError():
1✔
78
                extra_exception = None if exception.extra is None else exception.extra["exception"]
1✔
79
                match extra_exception:
1✔
80
                    case TypeError():
1✔
81
                        formatted_exception = errors.ValidationError(
1✔
82
                            message="The validation failed because the provided input has the wrong type"
83
                        )
84
                    case PydanticValidationError():
1✔
85
                        parts = [
1✔
86
                            ".".join(str(i) for i in field["loc"]) + ": " + field["msg"]
87
                            for field in extra_exception.errors()
88
                        ]
89
                        message = f"There are errors in the following fields, {', '.join(parts)}"
1✔
90
                        formatted_exception = errors.ValidationError(message=message)
1✔
91
                    case _:
1✔
92
                        self._log_unhandled_exception(exception)
1✔
93
            case SanicException():
1✔
94
                message = exception.message
1✔
95
                if message == "" or message is None:
1✔
96
                    message = ", ".join([str(i) for i in exception.args])
1✔
97
                formatted_exception = errors.BaseError(
1✔
98
                    message=message, status_code=exception.status_code, code=1000 + exception.status_code
99
                )
100
            case SqliteError():
1✔
101
                formatted_exception = errors.BaseError(
×
102
                    message=f"Database error occurred: {exception.sqlite_errorname}",
103
                    detail=f"Error code: {exception.sqlite_errorcode}",
104
                )
105
            case postgres_exceptions.PostgresError():
1✔
106
                formatted_exception = errors.BaseError(
×
107
                    message=f"Database error occurred: {exception.msg}", detail=f"Error code: {exception.pgcode}"
108
                )
109
            case SQLAlchemyError():
1✔
110
                message = ", ".join([str(i) for i in exception.args])
1✔
111
                if "CharacterNotInRepertoireError" in message:
1✔
112
                    # NOTE: This message is usually triggered if a string field for the database contains
113
                    # NULL - i.e \u0000 or other invalid characters that are not UTF-8 compatible
UNCOV
114
                    formatted_exception = errors.ValidationError(
×
115
                        message="The payload contains characters that are incompatible with the database",
116
                        detail=message,
117
                    )
118
                elif "value out of int32 range" in message:
1✔
119
                    formatted_exception = errors.ValidationError(
×
120
                        message="The payload contains integers with values that are "
121
                        "too large or small for the database",
122
                        detail=message,
123
                    )
124
                else:
125
                    formatted_exception = errors.BaseError(message=f"Database error occurred: {message}")
1✔
126
            case PydanticValidationError():
1✔
127
                parts = [".".join(str(i) for i in field["loc"]) + ": " + field["msg"] for field in exception.errors()]
1✔
128
                message = f"There are errors in the following fields, {', '.join(parts)}"
1✔
129
                formatted_exception = errors.ValidationError(message=message)
1✔
130
            case OverflowError():
1✔
131
                formatted_exception = errors.ValidationError(
×
132
                    message="The provided input is too large to be stored in the database"
133
                )
134
            case _:
1✔
135
                self._log_unhandled_exception(exception)
1✔
136
        return json(
1✔
137
            self.api_spec.ErrorResponse(
138
                error=self.api_spec.Error(
139
                    code=formatted_exception.code,
140
                    message=formatted_exception.message,
141
                    detail=formatted_exception.detail,
142
                )
143
            ).model_dump(exclude_none=True),
144
            status=formatted_exception.status_code,
145
        )
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc