django-ninja-problems 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- django_ninja_problems/__init__.py +26 -0
- django_ninja_problems/exceptions.py +113 -0
- django_ninja_problems/formatters.py +94 -0
- django_ninja_problems/handlers.py +186 -0
- django_ninja_problems/py.typed +0 -0
- django_ninja_problems/registrar.py +83 -0
- django_ninja_problems/schemas.py +19 -0
- django_ninja_problems-0.1.0.dist-info/METADATA +49 -0
- django_ninja_problems-0.1.0.dist-info/RECORD +12 -0
- django_ninja_problems-0.1.0.dist-info/WHEEL +5 -0
- django_ninja_problems-0.1.0.dist-info/licenses/LICENSE +21 -0
- django_ninja_problems-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from .exceptions import (
|
|
2
|
+
BadRequestProblem,
|
|
3
|
+
ConflictProblem,
|
|
4
|
+
ForbiddenProblem,
|
|
5
|
+
NotFoundProblem,
|
|
6
|
+
ProblemException,
|
|
7
|
+
UnauthorizedProblem,
|
|
8
|
+
UnprocessableEntityProblem,
|
|
9
|
+
)
|
|
10
|
+
from .handlers import create_problem_response
|
|
11
|
+
from .registrar import register_problem_handlers
|
|
12
|
+
from .schemas import ErrorItem, ProblemDetails
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"BadRequestProblem",
|
|
16
|
+
"ConflictProblem",
|
|
17
|
+
"ErrorItem",
|
|
18
|
+
"ForbiddenProblem",
|
|
19
|
+
"NotFoundProblem",
|
|
20
|
+
"ProblemDetails",
|
|
21
|
+
"ProblemException",
|
|
22
|
+
"UnauthorizedProblem",
|
|
23
|
+
"UnprocessableEntityProblem",
|
|
24
|
+
"create_problem_response",
|
|
25
|
+
"register_problem_handlers",
|
|
26
|
+
]
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
from .schemas import ErrorItem
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ProblemException(Exception):
|
|
6
|
+
def __init__(
|
|
7
|
+
self,
|
|
8
|
+
status: int = 500,
|
|
9
|
+
title: str = "Internal Server Error",
|
|
10
|
+
detail: str | None = None,
|
|
11
|
+
type: str = "about:blank",
|
|
12
|
+
instance: str | None = None,
|
|
13
|
+
errors: list[ErrorItem] | None = None,
|
|
14
|
+
**extra: Any,
|
|
15
|
+
) -> None:
|
|
16
|
+
self.status = status
|
|
17
|
+
self.title = title
|
|
18
|
+
self.detail = detail
|
|
19
|
+
self.type = type
|
|
20
|
+
self.instance = instance
|
|
21
|
+
self.errors = errors
|
|
22
|
+
self.extra = extra
|
|
23
|
+
super().__init__(detail or title)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class BadRequestProblem(ProblemException):
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
detail: str | None = None,
|
|
30
|
+
errors: list[ErrorItem] | None = None,
|
|
31
|
+
**extra: Any,
|
|
32
|
+
) -> None:
|
|
33
|
+
super().__init__(
|
|
34
|
+
status=400,
|
|
35
|
+
title="Bad Request",
|
|
36
|
+
detail=detail,
|
|
37
|
+
errors=errors,
|
|
38
|
+
**extra,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class UnauthorizedProblem(ProblemException):
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
detail: str | None = "Authentication credentials were not provided or are invalid.",
|
|
46
|
+
**extra: Any,
|
|
47
|
+
) -> None:
|
|
48
|
+
super().__init__(
|
|
49
|
+
status=401,
|
|
50
|
+
title="Unauthorized",
|
|
51
|
+
detail=detail,
|
|
52
|
+
**extra,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class ForbiddenProblem(ProblemException):
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
detail: str | None = "You do not have permission to perform this action.",
|
|
60
|
+
**extra: Any,
|
|
61
|
+
) -> None:
|
|
62
|
+
super().__init__(
|
|
63
|
+
status=403,
|
|
64
|
+
title="Forbidden",
|
|
65
|
+
detail=detail,
|
|
66
|
+
**extra,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class NotFoundProblem(ProblemException):
|
|
71
|
+
def __init__(
|
|
72
|
+
self,
|
|
73
|
+
detail: str | None = "The requested resource was not found.",
|
|
74
|
+
**extra: Any,
|
|
75
|
+
) -> None:
|
|
76
|
+
super().__init__(
|
|
77
|
+
status=404,
|
|
78
|
+
title="Not Found",
|
|
79
|
+
detail=detail,
|
|
80
|
+
**extra,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class ConflictProblem(ProblemException):
|
|
85
|
+
def __init__(
|
|
86
|
+
self,
|
|
87
|
+
detail: str | None = "A conflict occurred with the current state of the resource.",
|
|
88
|
+
errors: list[ErrorItem] | None = None,
|
|
89
|
+
**extra: Any,
|
|
90
|
+
) -> None:
|
|
91
|
+
super().__init__(
|
|
92
|
+
status=409,
|
|
93
|
+
title="Conflict",
|
|
94
|
+
detail=detail,
|
|
95
|
+
errors=errors,
|
|
96
|
+
**extra,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class UnprocessableEntityProblem(ProblemException):
|
|
101
|
+
def __init__(
|
|
102
|
+
self,
|
|
103
|
+
detail: str | None = "Unable to process the contained instructions.",
|
|
104
|
+
errors: list[ErrorItem] | None = None,
|
|
105
|
+
**extra: Any,
|
|
106
|
+
) -> None:
|
|
107
|
+
super().__init__(
|
|
108
|
+
status=422,
|
|
109
|
+
title="Unprocessable Entity",
|
|
110
|
+
detail=detail,
|
|
111
|
+
errors=errors,
|
|
112
|
+
**extra,
|
|
113
|
+
)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Any
|
|
3
|
+
from django.core.exceptions import ValidationError as DjangoValidationError
|
|
4
|
+
from django.db import IntegrityError
|
|
5
|
+
from .schemas import ErrorItem
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def format_pydantic_location(location: tuple[Any, ...]) -> str:
|
|
9
|
+
parts = [str(item) for item in location if str(item) not in {"body", "query", "path", "header", "cookie"}]
|
|
10
|
+
return ".".join(parts) if parts else ".".join(str(item) for item in location)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def format_pydantic_errors(errors: list[dict[str, Any]]) -> list[ErrorItem]:
|
|
14
|
+
formatted: list[ErrorItem] = []
|
|
15
|
+
for error in errors:
|
|
16
|
+
loc = error.get("loc", ())
|
|
17
|
+
field = format_pydantic_location(loc)
|
|
18
|
+
message = str(error.get("msg", "Invalid value"))
|
|
19
|
+
code = str(error.get("type", "value_error"))
|
|
20
|
+
formatted.append(ErrorItem(field=field, message=message, code=code))
|
|
21
|
+
return formatted
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def format_django_validation_error(exc: DjangoValidationError) -> list[ErrorItem]:
|
|
25
|
+
formatted: list[ErrorItem] = []
|
|
26
|
+
if hasattr(exc, "error_dict"):
|
|
27
|
+
for field, error_list in exc.error_dict.items():
|
|
28
|
+
for item in error_list:
|
|
29
|
+
formatted.append(
|
|
30
|
+
ErrorItem(
|
|
31
|
+
field=field,
|
|
32
|
+
message=str(item.message if hasattr(item, "message") else item),
|
|
33
|
+
code=getattr(item, "code", "invalid"),
|
|
34
|
+
)
|
|
35
|
+
)
|
|
36
|
+
return formatted
|
|
37
|
+
|
|
38
|
+
if hasattr(exc, "error_list"):
|
|
39
|
+
for item in exc.error_list:
|
|
40
|
+
formatted.append(
|
|
41
|
+
ErrorItem(
|
|
42
|
+
field=None,
|
|
43
|
+
message=str(item.message if hasattr(item, "message") else item),
|
|
44
|
+
code=getattr(item, "code", "invalid"),
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
return formatted
|
|
48
|
+
|
|
49
|
+
for message in exc.messages:
|
|
50
|
+
formatted.append(ErrorItem(field=None, message=str(message), code="invalid"))
|
|
51
|
+
return formatted
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def is_unique_constraint_violation(exc: Exception) -> bool:
|
|
55
|
+
if isinstance(exc, DjangoValidationError):
|
|
56
|
+
if hasattr(exc, "error_dict"):
|
|
57
|
+
for errors in exc.error_dict.values():
|
|
58
|
+
for error in errors:
|
|
59
|
+
if getattr(error, "code", None) == "unique":
|
|
60
|
+
return True
|
|
61
|
+
return False
|
|
62
|
+
|
|
63
|
+
if isinstance(exc, IntegrityError):
|
|
64
|
+
message = str(exc).lower()
|
|
65
|
+
unique_indicators = [
|
|
66
|
+
"unique constraint",
|
|
67
|
+
"duplicate key",
|
|
68
|
+
"already exists",
|
|
69
|
+
"unique_violation",
|
|
70
|
+
]
|
|
71
|
+
return any(indicator in message for indicator in unique_indicators)
|
|
72
|
+
|
|
73
|
+
return False
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def format_integrity_error_detail(exc: IntegrityError) -> str:
|
|
77
|
+
message = str(exc)
|
|
78
|
+
lower_message = message.lower()
|
|
79
|
+
|
|
80
|
+
if "unique constraint" in lower_message or "duplicate key" in lower_message:
|
|
81
|
+
match = re.search(r"Key \((.*?)\)=\((.*?)\) already exists", message)
|
|
82
|
+
if match:
|
|
83
|
+
field_name, field_value = match.groups()
|
|
84
|
+
return f"A record with {field_name} '{field_value}' already exists."
|
|
85
|
+
match = re.search(r"UNIQUE constraint failed: (.*)", message)
|
|
86
|
+
if match:
|
|
87
|
+
column = match.group(1).split(".")[-1]
|
|
88
|
+
return f"A record with this {column} already exists."
|
|
89
|
+
return "A record with identical unique fields already exists."
|
|
90
|
+
|
|
91
|
+
if "foreign key constraint" in lower_message or "foreign_key_violation" in lower_message:
|
|
92
|
+
return "Referenced resource does not exist or cannot be modified due to dependent records."
|
|
93
|
+
|
|
94
|
+
return "A database integrity constraint was violated."
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
from http import HTTPStatus
|
|
2
|
+
from typing import Any
|
|
3
|
+
from django.core.exceptions import ObjectDoesNotExist, ValidationError as DjangoValidationError
|
|
4
|
+
from django.db import IntegrityError
|
|
5
|
+
from django.http import Http404, HttpRequest, HttpResponse
|
|
6
|
+
from ninja import NinjaAPI
|
|
7
|
+
from ninja.errors import HttpError, ValidationError as NinjaValidationError
|
|
8
|
+
from pydantic import ValidationError as PydanticValidationError
|
|
9
|
+
|
|
10
|
+
from .exceptions import ProblemException
|
|
11
|
+
from .formatters import (
|
|
12
|
+
format_django_validation_error,
|
|
13
|
+
format_integrity_error_detail,
|
|
14
|
+
format_pydantic_errors,
|
|
15
|
+
is_unique_constraint_violation,
|
|
16
|
+
)
|
|
17
|
+
from .schemas import ProblemDetails
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def resolve_http_title(status_code: int) -> str:
|
|
21
|
+
try:
|
|
22
|
+
return HTTPStatus(status_code).phrase
|
|
23
|
+
except ValueError:
|
|
24
|
+
return "HTTP Error"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def create_problem_response(
|
|
28
|
+
api: NinjaAPI,
|
|
29
|
+
request: HttpRequest,
|
|
30
|
+
problem: ProblemDetails,
|
|
31
|
+
) -> HttpResponse:
|
|
32
|
+
payload = problem.model_dump(exclude_none=True)
|
|
33
|
+
response = api.create_response(request, payload, status=problem.status)
|
|
34
|
+
response["Content-Type"] = "application/problem+json"
|
|
35
|
+
return response
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def handle_problem_exception(
|
|
39
|
+
request: HttpRequest,
|
|
40
|
+
exc: ProblemException,
|
|
41
|
+
api: NinjaAPI,
|
|
42
|
+
) -> HttpResponse:
|
|
43
|
+
problem = ProblemDetails(
|
|
44
|
+
type=exc.type,
|
|
45
|
+
title=exc.title,
|
|
46
|
+
status=exc.status,
|
|
47
|
+
detail=exc.detail,
|
|
48
|
+
instance=exc.instance or request.path,
|
|
49
|
+
errors=exc.errors,
|
|
50
|
+
**exc.extra,
|
|
51
|
+
)
|
|
52
|
+
return create_problem_response(api, request, problem)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def handle_http_error(
|
|
56
|
+
request: HttpRequest,
|
|
57
|
+
exc: HttpError,
|
|
58
|
+
api: NinjaAPI,
|
|
59
|
+
) -> HttpResponse:
|
|
60
|
+
status_code = exc.status_code
|
|
61
|
+
detail = None
|
|
62
|
+
title = resolve_http_title(status_code)
|
|
63
|
+
|
|
64
|
+
if isinstance(exc.message, str):
|
|
65
|
+
detail = exc.message
|
|
66
|
+
elif isinstance(exc.message, dict):
|
|
67
|
+
detail = exc.message.get("detail") or exc.message.get("texto") or str(exc.message)
|
|
68
|
+
title = exc.message.get("title") or exc.message.get("titulo") or title
|
|
69
|
+
|
|
70
|
+
problem = ProblemDetails(
|
|
71
|
+
title=title,
|
|
72
|
+
status=status_code,
|
|
73
|
+
detail=detail,
|
|
74
|
+
instance=request.path,
|
|
75
|
+
)
|
|
76
|
+
return create_problem_response(api, request, problem)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def handle_not_found(
|
|
80
|
+
request: HttpRequest,
|
|
81
|
+
exc: Http404 | ObjectDoesNotExist,
|
|
82
|
+
api: NinjaAPI,
|
|
83
|
+
) -> HttpResponse:
|
|
84
|
+
detail_message = str(exc)
|
|
85
|
+
detail = detail_message if detail_message and not detail_message.startswith("<") else "The requested resource was not found."
|
|
86
|
+
problem = ProblemDetails(
|
|
87
|
+
title="Not Found",
|
|
88
|
+
status=404,
|
|
89
|
+
detail=detail,
|
|
90
|
+
instance=request.path,
|
|
91
|
+
)
|
|
92
|
+
return create_problem_response(api, request, problem)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def handle_ninja_validation_error(
|
|
96
|
+
request: HttpRequest,
|
|
97
|
+
exc: NinjaValidationError | PydanticValidationError,
|
|
98
|
+
api: NinjaAPI,
|
|
99
|
+
) -> HttpResponse:
|
|
100
|
+
raw_errors = exc.errors() if callable(getattr(exc, "errors", None)) else getattr(exc, "errors", [])
|
|
101
|
+
errors = format_pydantic_errors(raw_errors if isinstance(raw_errors, list) else [])
|
|
102
|
+
problem = ProblemDetails(
|
|
103
|
+
title="Unprocessable Entity",
|
|
104
|
+
status=422,
|
|
105
|
+
detail="The request body or parameters failed validation.",
|
|
106
|
+
instance=request.path,
|
|
107
|
+
errors=errors,
|
|
108
|
+
)
|
|
109
|
+
return create_problem_response(api, request, problem)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def handle_django_validation_error(
|
|
113
|
+
request: HttpRequest,
|
|
114
|
+
exc: DjangoValidationError,
|
|
115
|
+
api: NinjaAPI,
|
|
116
|
+
) -> HttpResponse:
|
|
117
|
+
errors = format_django_validation_error(exc)
|
|
118
|
+
if is_unique_constraint_violation(exc):
|
|
119
|
+
problem = ProblemDetails(
|
|
120
|
+
title="Conflict",
|
|
121
|
+
status=409,
|
|
122
|
+
detail="A record with conflicting unique values already exists.",
|
|
123
|
+
instance=request.path,
|
|
124
|
+
errors=errors,
|
|
125
|
+
)
|
|
126
|
+
return create_problem_response(api, request, problem)
|
|
127
|
+
|
|
128
|
+
problem = ProblemDetails(
|
|
129
|
+
title="Bad Request",
|
|
130
|
+
status=400,
|
|
131
|
+
detail="The submitted data failed validation.",
|
|
132
|
+
instance=request.path,
|
|
133
|
+
errors=errors,
|
|
134
|
+
)
|
|
135
|
+
return create_problem_response(api, request, problem)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def handle_integrity_error(
|
|
139
|
+
request: HttpRequest,
|
|
140
|
+
exc: IntegrityError,
|
|
141
|
+
api: NinjaAPI,
|
|
142
|
+
) -> HttpResponse:
|
|
143
|
+
detail = format_integrity_error_detail(exc)
|
|
144
|
+
problem = ProblemDetails(
|
|
145
|
+
title="Conflict",
|
|
146
|
+
status=409,
|
|
147
|
+
detail=detail,
|
|
148
|
+
instance=request.path,
|
|
149
|
+
)
|
|
150
|
+
return create_problem_response(api, request, problem)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def handle_authentication_error(
|
|
154
|
+
request: HttpRequest,
|
|
155
|
+
exc: Exception,
|
|
156
|
+
api: NinjaAPI,
|
|
157
|
+
) -> HttpResponse:
|
|
158
|
+
if isinstance(exc, HttpError) and exc.message:
|
|
159
|
+
detail = exc.message if isinstance(exc.message, str) else str(exc.message)
|
|
160
|
+
elif str(exc):
|
|
161
|
+
detail = str(exc)
|
|
162
|
+
else:
|
|
163
|
+
detail = "Authentication credentials were not provided or are invalid."
|
|
164
|
+
|
|
165
|
+
problem = ProblemDetails(
|
|
166
|
+
title="Unauthorized",
|
|
167
|
+
status=401,
|
|
168
|
+
detail=detail,
|
|
169
|
+
instance=request.path,
|
|
170
|
+
)
|
|
171
|
+
return create_problem_response(api, request, problem)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def handle_unexpected_exception(
|
|
175
|
+
request: HttpRequest,
|
|
176
|
+
exc: Exception,
|
|
177
|
+
api: NinjaAPI,
|
|
178
|
+
debug: bool,
|
|
179
|
+
) -> HttpResponse:
|
|
180
|
+
problem = ProblemDetails(
|
|
181
|
+
title="Internal Server Error",
|
|
182
|
+
status=500,
|
|
183
|
+
detail=f"{type(exc).__name__}: {exc}" if debug else "An unexpected error occurred while processing the request.",
|
|
184
|
+
instance=request.path,
|
|
185
|
+
)
|
|
186
|
+
return create_problem_response(api, request, problem)
|
|
File without changes
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from typing import Any, Callable
|
|
2
|
+
from django.conf import settings
|
|
3
|
+
from django.core.exceptions import ObjectDoesNotExist, ValidationError as DjangoValidationError
|
|
4
|
+
from django.db import IntegrityError
|
|
5
|
+
from django.http import Http404, HttpRequest, HttpResponse
|
|
6
|
+
from ninja import NinjaAPI
|
|
7
|
+
from ninja.errors import AuthenticationError, HttpError, ValidationError as NinjaValidationError
|
|
8
|
+
from pydantic import ValidationError as PydanticValidationError
|
|
9
|
+
|
|
10
|
+
from .exceptions import ProblemException
|
|
11
|
+
from .handlers import (
|
|
12
|
+
handle_authentication_error,
|
|
13
|
+
handle_django_validation_error,
|
|
14
|
+
handle_http_error,
|
|
15
|
+
handle_integrity_error,
|
|
16
|
+
handle_ninja_validation_error,
|
|
17
|
+
handle_not_found,
|
|
18
|
+
handle_problem_exception,
|
|
19
|
+
handle_unexpected_exception,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def register_problem_handlers(
|
|
24
|
+
api: NinjaAPI,
|
|
25
|
+
*,
|
|
26
|
+
debug: bool | None = None,
|
|
27
|
+
auth_exceptions: tuple[type[Exception], ...] | None = None,
|
|
28
|
+
custom_handlers: dict[type[Exception], Callable[[HttpRequest, Any], HttpResponse]] | None = None,
|
|
29
|
+
) -> None:
|
|
30
|
+
is_debug = getattr(settings, "DEBUG", False) if debug is None else debug
|
|
31
|
+
|
|
32
|
+
api.add_exception_handler(
|
|
33
|
+
ProblemException,
|
|
34
|
+
lambda request, exc: handle_problem_exception(request, exc, api),
|
|
35
|
+
)
|
|
36
|
+
api.add_exception_handler(
|
|
37
|
+
HttpError,
|
|
38
|
+
lambda request, exc: handle_http_error(request, exc, api),
|
|
39
|
+
)
|
|
40
|
+
api.add_exception_handler(
|
|
41
|
+
Http404,
|
|
42
|
+
lambda request, exc: handle_not_found(request, exc, api),
|
|
43
|
+
)
|
|
44
|
+
api.add_exception_handler(
|
|
45
|
+
ObjectDoesNotExist,
|
|
46
|
+
lambda request, exc: handle_not_found(request, exc, api),
|
|
47
|
+
)
|
|
48
|
+
api.add_exception_handler(
|
|
49
|
+
NinjaValidationError,
|
|
50
|
+
lambda request, exc: handle_ninja_validation_error(request, exc, api),
|
|
51
|
+
)
|
|
52
|
+
api.add_exception_handler(
|
|
53
|
+
PydanticValidationError,
|
|
54
|
+
lambda request, exc: handle_ninja_validation_error(request, exc, api),
|
|
55
|
+
)
|
|
56
|
+
api.add_exception_handler(
|
|
57
|
+
DjangoValidationError,
|
|
58
|
+
lambda request, exc: handle_django_validation_error(request, exc, api),
|
|
59
|
+
)
|
|
60
|
+
api.add_exception_handler(
|
|
61
|
+
IntegrityError,
|
|
62
|
+
lambda request, exc: handle_integrity_error(request, exc, api),
|
|
63
|
+
)
|
|
64
|
+
api.add_exception_handler(
|
|
65
|
+
AuthenticationError,
|
|
66
|
+
lambda request, exc: handle_authentication_error(request, exc, api),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
if auth_exceptions:
|
|
70
|
+
for auth_exc in auth_exceptions:
|
|
71
|
+
api.add_exception_handler(
|
|
72
|
+
auth_exc,
|
|
73
|
+
lambda request, exc: handle_authentication_error(request, exc, api),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
if custom_handlers:
|
|
77
|
+
for exc_class, handler in custom_handlers.items():
|
|
78
|
+
api.add_exception_handler(exc_class, handler)
|
|
79
|
+
|
|
80
|
+
api.add_exception_handler(
|
|
81
|
+
Exception,
|
|
82
|
+
lambda request, exc: handle_unexpected_exception(request, exc, api, is_debug),
|
|
83
|
+
)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ErrorItem(BaseModel):
|
|
6
|
+
field: str | None = None
|
|
7
|
+
message: str
|
|
8
|
+
code: str | None = None
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ProblemDetails(BaseModel):
|
|
12
|
+
model_config = ConfigDict(extra="allow")
|
|
13
|
+
|
|
14
|
+
type: str = "about:blank"
|
|
15
|
+
title: str
|
|
16
|
+
status: int
|
|
17
|
+
detail: str | None = None
|
|
18
|
+
instance: str | None = None
|
|
19
|
+
errors: list[ErrorItem] | None = None
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: django-ninja-problems
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: RFC 7807 and RFC 9457 Problem Details for Django Ninja APIs
|
|
5
|
+
Author: Jansem
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Classifier: Framework :: Django
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: django>=4.2
|
|
14
|
+
Requires-Dist: django-ninja>=1.0.0
|
|
15
|
+
Requires-Dist: pydantic>=2.0.0
|
|
16
|
+
Provides-Extra: dev
|
|
17
|
+
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
18
|
+
Requires-Dist: pytest-django>=4.8.0; extra == "dev"
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# django-ninja-problems
|
|
22
|
+
|
|
23
|
+
RFC 7807 and RFC 9457 Problem Details for Django Ninja APIs.
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install django-ninja-problems
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Quick Start
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from ninja import NinjaAPI
|
|
35
|
+
from django_ninja_problems import register_problem_handlers
|
|
36
|
+
|
|
37
|
+
api = NinjaAPI()
|
|
38
|
+
register_problem_handlers(api)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Supported Exceptions
|
|
42
|
+
|
|
43
|
+
- `ninja.errors.HttpError`
|
|
44
|
+
- `django.http.Http404` and `ObjectDoesNotExist`
|
|
45
|
+
- `ninja.errors.ValidationError` and `pydantic.ValidationError` (HTTP 422)
|
|
46
|
+
- `django.core.exceptions.ValidationError` (HTTP 400 or 409)
|
|
47
|
+
- `django.db.IntegrityError` (HTTP 409)
|
|
48
|
+
- Custom authentication exceptions (HTTP 401)
|
|
49
|
+
- Unexpected `Exception` (HTTP 500 masked in production)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
django_ninja_problems/__init__.py,sha256=8aAVvbieG1HfGTn-3c_5_7l22VvX7E5c3-pONn6b32s,634
|
|
2
|
+
django_ninja_problems/exceptions.py,sha256=UpqnY8Aoj0j_Xwn2bzmAkdrDqhuNFHYd4dYh9q2Kn8E,2840
|
|
3
|
+
django_ninja_problems/formatters.py,sha256=GKWo0bFsIANcOg946woGQjFDITwyTor7on9TYzvp0bY,3595
|
|
4
|
+
django_ninja_problems/handlers.py,sha256=bGq5FDY4Xw4PrFj_LqAiiSvWIg3MZc7wyKGAXfi2dVs,5509
|
|
5
|
+
django_ninja_problems/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
django_ninja_problems/registrar.py,sha256=rJCQJx82s45JMcset0729Z35XEQkc9V-8lRZ8TOp__Q,2845
|
|
7
|
+
django_ninja_problems/schemas.py,sha256=mahDc3JH6GGRi5xK7znhIa-1-pf67cKmpZiNquv81wA,423
|
|
8
|
+
django_ninja_problems-0.1.0.dist-info/licenses/LICENSE,sha256=BmH9uq40Gg695-P1cfM64VP7cbaTEUuXFvyHa6esZpA,1063
|
|
9
|
+
django_ninja_problems-0.1.0.dist-info/METADATA,sha256=q3x_drtKAjgHv4Dxeas3tXNW2OqMupFTGEqm4xz0_bg,1312
|
|
10
|
+
django_ninja_problems-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
11
|
+
django_ninja_problems-0.1.0.dist-info/top_level.txt,sha256=pzrLek-hSNGIEIHUHLVVnGwMMToO-hwcIwYUaCsFx50,22
|
|
12
|
+
django_ninja_problems-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jansem
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
django_ninja_problems
|