litestar-google-errors 0.1.1__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.
- litestar_google_errors/__init__.py +12 -0
- litestar_google_errors/models.py +40 -0
- litestar_google_errors/openapi.py +114 -0
- litestar_google_errors/plugin.py +41 -0
- litestar_google_errors/py.typed +0 -0
- litestar_google_errors-0.1.1.dist-info/METADATA +168 -0
- litestar_google_errors-0.1.1.dist-info/RECORD +9 -0
- litestar_google_errors-0.1.1.dist-info/WHEEL +4 -0
- litestar_google_errors-0.1.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from litestar_google_errors.models import GoogleError, GoogleErrorItem, GoogleErrorResponse, LocationType
|
|
2
|
+
from litestar_google_errors.openapi import create_error_responses
|
|
3
|
+
from litestar_google_errors.plugin import GoogleErrorResponsesPlugin
|
|
4
|
+
|
|
5
|
+
__all__ = (
|
|
6
|
+
"GoogleError",
|
|
7
|
+
"GoogleErrorItem",
|
|
8
|
+
"GoogleErrorResponse",
|
|
9
|
+
"GoogleErrorResponsesPlugin",
|
|
10
|
+
"LocationType",
|
|
11
|
+
"create_error_responses",
|
|
12
|
+
)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Google JSON style error payload models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from msgspec import Struct
|
|
9
|
+
|
|
10
|
+
__all__ = ("GoogleError", "GoogleErrorItem", "GoogleErrorResponse", "LocationType")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class LocationType(str, Enum):
|
|
14
|
+
FIELD = "field"
|
|
15
|
+
PARAMETER = "parameter"
|
|
16
|
+
PATH = "path"
|
|
17
|
+
HEADER = "header"
|
|
18
|
+
BODY = "body"
|
|
19
|
+
QUERY = "query"
|
|
20
|
+
COOKIE = "cookie"
|
|
21
|
+
ENDPOINT = "endpoint"
|
|
22
|
+
DOMAIN = "domain"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class GoogleErrorItem(Struct):
|
|
26
|
+
domain: str
|
|
27
|
+
reason: str
|
|
28
|
+
message: str
|
|
29
|
+
locationType: Optional[str] = None
|
|
30
|
+
location: Optional[str] = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class GoogleError(Struct):
|
|
34
|
+
errors: list[GoogleErrorItem]
|
|
35
|
+
code: int
|
|
36
|
+
message: str
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class GoogleErrorResponse(Struct):
|
|
40
|
+
error: GoogleError
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""OpenAPI schema generation for Google JSON style error responses."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import contextlib
|
|
6
|
+
from http import HTTPStatus
|
|
7
|
+
from typing import Iterator
|
|
8
|
+
|
|
9
|
+
from litestar import MediaType
|
|
10
|
+
from litestar.exceptions import HTTPException
|
|
11
|
+
from litestar.openapi.spec import OpenAPIMediaType, OpenAPIResponse, OpenAPIType, Schema
|
|
12
|
+
|
|
13
|
+
__all__ = ("create_error_responses",)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _status_phrase(status_code: int) -> str:
|
|
17
|
+
with contextlib.suppress(ValueError):
|
|
18
|
+
return HTTPStatus(status_code).phrase
|
|
19
|
+
return ""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _status_description(status_code: int) -> str:
|
|
23
|
+
with contextlib.suppress(ValueError):
|
|
24
|
+
return HTTPStatus(status_code).description
|
|
25
|
+
return ""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _error_schema(
|
|
29
|
+
exc: type[HTTPException],
|
|
30
|
+
status_code: int,
|
|
31
|
+
detail: str,
|
|
32
|
+
domain: str,
|
|
33
|
+
location_type: str,
|
|
34
|
+
location: str,
|
|
35
|
+
) -> Schema:
|
|
36
|
+
return Schema(
|
|
37
|
+
type=OpenAPIType.OBJECT,
|
|
38
|
+
required=["error"],
|
|
39
|
+
description=exc.__name__,
|
|
40
|
+
properties={
|
|
41
|
+
"error": Schema(
|
|
42
|
+
type=OpenAPIType.OBJECT,
|
|
43
|
+
required=["code", "message", "errors"],
|
|
44
|
+
properties={
|
|
45
|
+
"code": Schema(type=OpenAPIType.INTEGER),
|
|
46
|
+
"message": Schema(type=OpenAPIType.STRING),
|
|
47
|
+
"errors": Schema(
|
|
48
|
+
type=OpenAPIType.ARRAY,
|
|
49
|
+
items=Schema(
|
|
50
|
+
type=OpenAPIType.OBJECT,
|
|
51
|
+
required=["domain", "reason", "message"],
|
|
52
|
+
properties={
|
|
53
|
+
"domain": Schema(type=OpenAPIType.STRING),
|
|
54
|
+
"reason": Schema(type=OpenAPIType.STRING),
|
|
55
|
+
"message": Schema(type=OpenAPIType.STRING),
|
|
56
|
+
"locationType": Schema(type=[OpenAPIType.STRING, OpenAPIType.NULL]),
|
|
57
|
+
"location": Schema(type=[OpenAPIType.STRING, OpenAPIType.NULL]),
|
|
58
|
+
},
|
|
59
|
+
),
|
|
60
|
+
),
|
|
61
|
+
},
|
|
62
|
+
)
|
|
63
|
+
},
|
|
64
|
+
examples=[
|
|
65
|
+
{
|
|
66
|
+
"error": {
|
|
67
|
+
"code": status_code,
|
|
68
|
+
"message": detail,
|
|
69
|
+
"errors": [
|
|
70
|
+
{
|
|
71
|
+
"domain": domain,
|
|
72
|
+
"reason": exc.__name__,
|
|
73
|
+
"message": detail,
|
|
74
|
+
"locationType": location_type,
|
|
75
|
+
"location": location,
|
|
76
|
+
}
|
|
77
|
+
],
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
],
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def create_error_responses(
|
|
85
|
+
exceptions: list[type[HTTPException]],
|
|
86
|
+
*,
|
|
87
|
+
domain: str = "global",
|
|
88
|
+
location_type: str = "endpoint",
|
|
89
|
+
location: str = "/example",
|
|
90
|
+
) -> Iterator[tuple[str, OpenAPIResponse]]:
|
|
91
|
+
"""Create OpenAPI responses for ``exceptions`` in the Google JSON error format.
|
|
92
|
+
|
|
93
|
+
Exceptions sharing a status code are combined into a single response using ``oneOf``.
|
|
94
|
+
"""
|
|
95
|
+
grouped: dict[int, list[type[HTTPException]]] = {}
|
|
96
|
+
for exc in exceptions:
|
|
97
|
+
grouped.setdefault(getattr(exc, "status_code", 500), []).append(exc)
|
|
98
|
+
|
|
99
|
+
for status_code, group in grouped.items():
|
|
100
|
+
description = ""
|
|
101
|
+
schemas: list[Schema] = []
|
|
102
|
+
for exc in group:
|
|
103
|
+
detail = getattr(exc, "detail", None) or _status_phrase(status_code)
|
|
104
|
+
description = description or getattr(exc, "detail", None) or ""
|
|
105
|
+
schemas.append(_error_schema(exc, status_code, detail, domain, location_type, location))
|
|
106
|
+
|
|
107
|
+
schema = schemas[0] if len(schemas) == 1 else Schema(one_of=schemas)
|
|
108
|
+
yield (
|
|
109
|
+
str(status_code),
|
|
110
|
+
OpenAPIResponse(
|
|
111
|
+
description=description or _status_description(status_code),
|
|
112
|
+
content={MediaType.JSON: OpenAPIMediaType(schema=schema)},
|
|
113
|
+
),
|
|
114
|
+
)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Litestar plugin wiring the Google error schema into OpenAPI generation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from functools import partial
|
|
6
|
+
|
|
7
|
+
import litestar._openapi.responses as response_factory
|
|
8
|
+
from litestar.config.app import AppConfig
|
|
9
|
+
from litestar.plugins import InitPlugin
|
|
10
|
+
|
|
11
|
+
from litestar_google_errors.openapi import create_error_responses
|
|
12
|
+
|
|
13
|
+
__all__ = ("GoogleErrorResponsesPlugin",)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class GoogleErrorResponsesPlugin(InitPlugin):
|
|
17
|
+
"""Render ``HTTPException`` responses in the OpenAPI schema using the Google JSON error format.
|
|
18
|
+
|
|
19
|
+
Litestar has no public hook for error response schemas, so the plugin replaces
|
|
20
|
+
``litestar._openapi.responses.create_error_responses``. The replacement is process-wide.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
*,
|
|
26
|
+
domain: str = "global",
|
|
27
|
+
location_type: str = "endpoint",
|
|
28
|
+
example_location: str = "/example",
|
|
29
|
+
) -> None:
|
|
30
|
+
self.domain = domain
|
|
31
|
+
self.location_type = location_type
|
|
32
|
+
self.example_location = example_location
|
|
33
|
+
|
|
34
|
+
def on_app_init(self, app_config: AppConfig) -> AppConfig:
|
|
35
|
+
response_factory.create_error_responses = partial( # type: ignore[assignment]
|
|
36
|
+
create_error_responses,
|
|
37
|
+
domain=self.domain,
|
|
38
|
+
location_type=self.location_type,
|
|
39
|
+
location=self.example_location,
|
|
40
|
+
)
|
|
41
|
+
return app_config
|
|
File without changes
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: litestar-google-errors
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Litestar plugin for Google JSON style error responses in OpenAPI (Swagger) schemas, with typed msgspec error models
|
|
5
|
+
Project-URL: Homepage, https://github.com/alexkorolex/litestar-google-errors
|
|
6
|
+
Project-URL: Repository, https://github.com/alexkorolex/litestar-google-errors
|
|
7
|
+
Project-URL: Issues, https://github.com/alexkorolex/litestar-google-errors/issues
|
|
8
|
+
Author: alexkorolex
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: asgi,error-handling,errors,google-json-style,http-exception,json-api,litestar,litestar-plugin,msgspec,openapi,rest-api,swagger
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Framework :: Litestar
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Requires-Dist: litestar<3,>=2.16
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# litestar-google-errors
|
|
29
|
+
|
|
30
|
+
[](https://pypi.org/project/litestar-google-errors/)
|
|
31
|
+
[](https://pypi.org/project/litestar-google-errors/)
|
|
32
|
+
[](https://github.com/alexkorolex/litestar-google-errors/actions/workflows/ci.yml)
|
|
33
|
+
[](https://github.com/alexkorolex/litestar-google-errors/blob/main/LICENSE)
|
|
34
|
+
|
|
35
|
+
**Google JSON style error responses for [Litestar](https://litestar.dev) OpenAPI.**
|
|
36
|
+
|
|
37
|
+
`litestar-google-errors` is a Litestar plugin that documents `HTTPException` error responses in
|
|
38
|
+
the OpenAPI (Swagger) schema using the
|
|
39
|
+
[Google JSON Style Guide](https://google.github.io/styleguide/jsoncstyleguide.xml#error) error
|
|
40
|
+
format, instead of Litestar's default `{"status_code", "detail", "extra"}` body. It also ships
|
|
41
|
+
typed `msgspec` models to return the same error shape from your API at runtime.
|
|
42
|
+
|
|
43
|
+
## Features
|
|
44
|
+
|
|
45
|
+
- Documents error responses in the OpenAPI schema using the Google JSON error format
|
|
46
|
+
- Covers every exception listed in a route handler's `raises=[...]`
|
|
47
|
+
- Combines exceptions that share an HTTP status code with `oneOf`
|
|
48
|
+
- Lets you configure the example `domain`, `locationType` and `location`
|
|
49
|
+
- Ships typed `msgspec` models (`GoogleErrorResponse`, `GoogleError`, `GoogleErrorItem`) for runtime error bodies
|
|
50
|
+
- Includes `py.typed` for type checkers
|
|
51
|
+
- Supports Python 3.10+ and Litestar 2.16+
|
|
52
|
+
|
|
53
|
+
## Litestar error format: before and after
|
|
54
|
+
|
|
55
|
+
Litestar's default error body:
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"status_code": 404,
|
|
60
|
+
"detail": "Order not found"
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Google JSON style error body, as documented by this plugin:
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"error": {
|
|
69
|
+
"code": 404,
|
|
70
|
+
"message": "Order not found",
|
|
71
|
+
"errors": [
|
|
72
|
+
{
|
|
73
|
+
"domain": "global",
|
|
74
|
+
"reason": "OrderNotFound",
|
|
75
|
+
"message": "Order not found",
|
|
76
|
+
"locationType": "endpoint",
|
|
77
|
+
"location": "/example"
|
|
78
|
+
}
|
|
79
|
+
]
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Installation
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
pip install litestar-google-errors
|
|
88
|
+
# or
|
|
89
|
+
uv add litestar-google-errors
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Quick start
|
|
93
|
+
|
|
94
|
+
Register the plugin on your Litestar app:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from litestar import Litestar
|
|
98
|
+
from litestar_google_errors import GoogleErrorResponsesPlugin
|
|
99
|
+
|
|
100
|
+
app = Litestar(
|
|
101
|
+
route_handlers=[...],
|
|
102
|
+
plugins=[GoogleErrorResponsesPlugin()],
|
|
103
|
+
)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Every exception listed in a handler's `raises=[...]` appears in the OpenAPI schema in the Google
|
|
107
|
+
format. Exceptions that share a status code are combined with `oneOf`.
|
|
108
|
+
|
|
109
|
+
### Configure the OpenAPI examples
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
GoogleErrorResponsesPlugin(domain="orders", location_type="path", example_location="/orders/1")
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Return Google-style errors at runtime
|
|
116
|
+
|
|
117
|
+
The plugin only changes the **OpenAPI documentation**. To return the same JSON error shape from
|
|
118
|
+
your API, use the bundled `msgspec` models in a Litestar exception handler:
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
from litestar import MediaType, Request, Response
|
|
122
|
+
from litestar.exceptions import HTTPException
|
|
123
|
+
from litestar_google_errors import GoogleError, GoogleErrorItem, GoogleErrorResponse, LocationType
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def http_exception_handler(request: Request, exc: HTTPException) -> Response[GoogleErrorResponse]:
|
|
127
|
+
body = GoogleErrorResponse(
|
|
128
|
+
error=GoogleError(
|
|
129
|
+
code=exc.status_code,
|
|
130
|
+
message=exc.detail,
|
|
131
|
+
errors=[
|
|
132
|
+
GoogleErrorItem(
|
|
133
|
+
domain="global",
|
|
134
|
+
reason=type(exc).__name__,
|
|
135
|
+
message=exc.detail,
|
|
136
|
+
locationType=LocationType.ENDPOINT,
|
|
137
|
+
location=request.url.path,
|
|
138
|
+
)
|
|
139
|
+
],
|
|
140
|
+
)
|
|
141
|
+
)
|
|
142
|
+
return Response(body, status_code=exc.status_code, media_type=MediaType.JSON)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
app = Litestar(
|
|
146
|
+
route_handlers=[...],
|
|
147
|
+
plugins=[GoogleErrorResponsesPlugin()],
|
|
148
|
+
exception_handlers={HTTPException: http_exception_handler},
|
|
149
|
+
)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Limitations
|
|
153
|
+
|
|
154
|
+
Litestar has no public hook for error response schemas, so the plugin replaces the private
|
|
155
|
+
`litestar._openapi.responses.create_error_responses` function. The replacement is process-wide,
|
|
156
|
+
and the dependency is pinned to `litestar<3` because that internal may change.
|
|
157
|
+
|
|
158
|
+
## Development
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
uv sync
|
|
162
|
+
uv run pytest
|
|
163
|
+
uv build
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## License
|
|
167
|
+
|
|
168
|
+
[MIT](https://github.com/alexkorolex/litestar-google-errors/blob/main/LICENSE)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
litestar_google_errors/__init__.py,sha256=OS34sKiHpi8URg4WoDzZ7hZy4WZ-w9fDXBdinO4FQ4o,409
|
|
2
|
+
litestar_google_errors/models.py,sha256=0rgugNUDxozbrRZCOdy_DisLZNXU_NU2UYIbqBccxbM,785
|
|
3
|
+
litestar_google_errors/openapi.py,sha256=IYwtacsRJiwsRcW8WdAsraie2xWOwFi6X1PGpY9qL4s,3947
|
|
4
|
+
litestar_google_errors/plugin.py,sha256=Ck_vWWDx28aTDH2Uv3VP1xzTwG-7lc2O3l81-Xs6aHY,1368
|
|
5
|
+
litestar_google_errors/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
litestar_google_errors-0.1.1.dist-info/METADATA,sha256=i_cqEUFi5ZnFQDYA7WKbXwGdvSoZq0551N2xYctdD9M,5693
|
|
7
|
+
litestar_google_errors-0.1.1.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
8
|
+
litestar_google_errors-0.1.1.dist-info/licenses/LICENSE,sha256=BJKFz45zVBAQfQYAtSP__R4P5-xxJJx5QLK8_2IArSE,1068
|
|
9
|
+
litestar_google_errors-0.1.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 alexkorolex
|
|
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.
|