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

SwissDataScienceCenter / renku-data-services / 10072614658

23 Jul 2024 11:46AM UTC coverage: 90.452% (+0.03%) from 90.422%
10072614658

push

github

web-flow
fix: use ipv4 for the kind network in devcontainers (#320)

9028 of 9981 relevant lines covered (90.45%)

1.61 hits per line

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

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

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

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

15
from renku_data_services import errors
2✔
16

17

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

21
    code: int
2✔
22
    message: str
2✔
23
    detail: Optional[str]
2✔
24
    quiet: bool
2✔
25

26

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

30
    error: BaseError
2✔
31

32
    def dict(
2✔
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)
2✔
48
BErrorResponse = TypeVar("BErrorResponse", bound=BaseErrorResponse)
2✔
49

50

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

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

57

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

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

65
    def default(self, request: Request, exception: Exception) -> HTTPResponse:
2✔
66
        """Overrides the default error handler."""
67
        formatted_exception = errors.BaseError()
2✔
68
        match exception:
2✔
69
            case errors.BaseError():
2✔
70
                formatted_exception = exception
2✔
71
            case ValidationError():
2✔
72
                extra_exception = None if exception.extra is None else exception.extra["exception"]
2✔
73
                match extra_exception:
2✔
74
                    case TypeError():
2✔
75
                        formatted_exception = errors.ValidationError(
2✔
76
                            message="The validation failed because the provided input has the wrong type"
77
                        )
78
                    case PydanticValidationError():
2✔
79
                        parts = [
2✔
80
                            ".".join(str(i) for i in field["loc"]) + ": " + field["msg"]
81
                            for field in extra_exception.errors()
82
                        ]
83
                        message = f"There are errors in the following fields, {', '.join(parts)}"
2✔
84
                        formatted_exception = errors.ValidationError(message=message)
2✔
85
            case SanicException():
2✔
86
                message = exception.message
2✔
87
                if message == "" or message is None:
2✔
88
                    message = ", ".join([str(i) for i in exception.args])
×
89
                formatted_exception = errors.BaseError(
2✔
90
                    message=message,
91
                    status_code=exception.status_code,
92
                    code=1000 + exception.status_code,
93
                    quiet=exception.quiet or False,
94
                )
95
            case SqliteError():
2✔
96
                formatted_exception = errors.BaseError(
×
97
                    message=f"Database error occurred: {exception.sqlite_errorname}",
98
                    detail=f"Error code: {exception.sqlite_errorcode}",
99
                )
100
            case postgres_exceptions.PostgresError():
2✔
101
                formatted_exception = errors.BaseError(
×
102
                    message=f"Database error occurred: {exception.msg}", detail=f"Error code: {exception.pgcode}"
103
                )
104
            case SQLAlchemyError():
2✔
105
                message = ", ".join([str(i) for i in exception.args])
1✔
106
                if "CharacterNotInRepertoireError" in message:
1✔
107
                    # NOTE: This message is usually triggered if a string field for the database contains
108
                    # NULL - i.e \u0000 or other invalid characters that are not UTF-8 compatible
109
                    formatted_exception = errors.ValidationError(
×
110
                        message="The payload contains characters that are incompatible with the database",
111
                        detail=message,
112
                    )
113
                elif "value out of int32 range" in message:
1✔
114
                    formatted_exception = errors.ValidationError(
×
115
                        message="The payload contains integers with values that are "
116
                        "too large or small for the database",
117
                        detail=message,
118
                    )
119
                else:
120
                    formatted_exception = errors.BaseError(message=f"Database error occurred: {message}")
1✔
121
            case PydanticValidationError():
2✔
122
                parts = [".".join(str(i) for i in field["loc"]) + ": " + field["msg"] for field in exception.errors()]
2✔
123
                message = f"There are errors in the following fields, {', '.join(parts)}"
2✔
124
                formatted_exception = errors.ValidationError(message=message)
2✔
125
            case OverflowError():
1✔
126
                formatted_exception = errors.ValidationError(
×
127
                    message="The provided input is too large to be stored in the database"
128
                )
129
        self.log(request, formatted_exception)
2✔
130
        return json(
2✔
131
            self.api_spec.ErrorResponse(
132
                error=self.api_spec.Error(
133
                    code=formatted_exception.code,
134
                    message=formatted_exception.message,
135
                    detail=formatted_exception.detail,
136
                )
137
            ).model_dump(exclude_none=True),
138
            status=formatted_exception.status_code,
139
        )
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