Skip to content

Commit 33602c5

Browse files
author
Lev Rubel
committed
changed the way how errors are handled (different response structure: see HISTORY.rst); added back 3.6 support + added newer 3.8
1 parent 5067327 commit 33602c5

File tree

13 files changed

+332
-84
lines changed

13 files changed

+332
-84
lines changed

.github/workflows/pythonpackage.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ jobs:
99
strategy:
1010
max-parallel: 4
1111
matrix:
12-
python-version: [3.7, 3.8]
12+
python-version: [3.6, 3.7, 3.8]
1313

1414
steps:
1515
- uses: actions/checkout@v1

.travis.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ language: python
22
python:
33
- 3.8
44
- 3.7
5+
- 3.6
56
install: pip install -U tox-travis
67
script: tox
78
deploy:
89
provider: pypi
910
distributions: sdist bdist_wheel
11+
skip_existing: true
1012
user: identixone
1113
password:
1214
secure: LW3LxTFsfGqnJKyJmu+fOiDT5bJfBuhkrYjXPWnXY18uCnn/gWnivHtsQNcXK8x6PeRMjHZweFbqJ/xnlPLpqdp4xSWmMhrDX0yP0gsC/6P2dssicvfjuQlN2V1+NEcBYOaAvSoafxQ6b5uPmLuFjNbNQGvLmupsvvXVBgAUnoAS9bC4av7nNmCS8YFDP0kxNd4aP0VNzOxgIxLxdcJtRQgFyGjun7DH2Oj851YxRS5/AOES4zZcjuhNUyJX0zpry5HIPN/uheJKquaBz9tRkSREY88TA+kMAiEflZooY68YsVfNJWl7RRxdQh9jKEz3VqSpObNxT3nl1dKMzrv8Opm0WCst9hHn735ZQcE2Od3uUjH7mTgtJv7x+UHcHkdT9p7hMmSJfxog8kY9Ys0P1gsUyXp5/Ol+xEKuSVSAvyxGRSnryoANsTAlyRKmKlr3CT1xdzPb5f7yrd3+MceuwDCHilSI7TVWVslnYRiDZWWBhefKmxddAMu1xSKe3NDaF/5W0s3vuwy9vEQF2cjLn6oiNhzQFL+BWV4UGgRP0GBHobI/oWCZVUE1aCEfIhGIYoWYggHddYuH4Rh/qQMs22OxND5WnX30mYtHLCB8TP+VkEwY7IpQ++r0P8dFxofiuamFhl503CRXEUs/K1oExOS5PETD7cyjdDbJ+JAZwIk=

AUTHORS.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,5 @@ Contributors
1111
------------
1212

1313
* Yaroslav Ulanovich <yul@datacorp.ee>
14+
* BumagniyPacket <rly@datacorp.ee>
15+
* aCLr <a.ch.clr@gmail.com>

CONTRIBUTING.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ Before you submit a pull request, check that it meets these guidelines:
102102
2. If the pull request adds functionality, the docs should be updated. Put
103103
your new functionality into a function with a docstring, and add the
104104
feature to the list in README.rst.
105-
3. The pull request should work for Python 3.7. Check
105+
3. The pull request should work for Python 3.6, 3.7, 3.8. Check
106106
https://travis-ci.org/identixone/fastapi_contrib/pull_requests
107107
and make sure that the tests pass for all supported Python versions.
108108

HISTORY.rst

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,35 @@
11
=======
22
History
33
=======
4+
5+
0.2.0
6+
--------
7+
8+
General changes:
9+
10+
* Added support for Python 3.8
11+
12+
Breaking changes:
13+
14+
* Changed the way of how the error structure look like
15+
16+
.. code-block:: json
17+
18+
[
19+
{
20+
"error_codes": [400, 401],
21+
"message": "Validation error.",
22+
"fields": [
23+
{
24+
"name": "field1",
25+
"message": "Field is required",
26+
"error_code": 400
27+
},
28+
{
29+
"name": "field2",
30+
"message": "Invalid value",
31+
"error_code": 401
32+
}
33+
]
34+
}
35+
]

README.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ Features
4141
* Opentracing middleware & setup utility with Jaeger tracer + root span available in every Request's state
4242
* StateRequestIDMiddleware: receives configurable header and saves it in request state
4343

44+
History
45+
--------
46+
47+
See `HISTORY.rst <https://github.com/identixone/fastapi_contrib/blob/master/HISTORY.rst>`_.
48+
4449
Roadmap
4550
--------
4651

fastapi_contrib/exception_handlers.py

Lines changed: 33 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,23 @@ def parse_error(
1919
:param raw: Whether this is a raw error or wrapped pydantic error
2020
:return: dict with name of the field (or "__all__") and actual message
2121
"""
22+
print(err.type_, err.loc, raw)
2223
if err.type_ == "value_error.str.regex":
2324
message = "Provided value doesn't match valid format."
2425
elif err.type_ == "type_error.enum":
2526
message = "One or more values provided are not valid."
2627
else:
2728
message = err.msg or ""
29+
30+
if err.type_.startswith("value_error.error_code"):
31+
error_code = int(err.type_.split(".")[-1])
32+
else:
33+
# default error code for non-custom errors is 400
34+
error_code = 400
35+
2836
if not raw:
2937
if len(err.loc) == 2:
30-
if str(err.loc[0]) == "body":
38+
if str(err.loc[0]) in ["body", "query"]:
3139
name = err.loc[1]
3240
else:
3341
name = err.loc[0]
@@ -41,7 +49,7 @@ def parse_error(
4149
else:
4250
if len(err.loc) == 2:
4351
name = str(err.loc[0])
44-
message = f"{str(err.loc[1]).lower()}: {message}"
52+
# message = f"{str(err.loc[1]).lower()}: {message}"
4553
elif len(err.loc) == 1:
4654
name = str(err.loc[0])
4755
else:
@@ -50,7 +58,11 @@ def parse_error(
5058
if name in field_names:
5159
return None
5260

53-
return {"name": name, "message": message.capitalize()}
61+
return {
62+
"name": name,
63+
"message": message.capitalize(),
64+
"error_code": error_code,
65+
}
5466

5567

5668
def raw_errors_to_fields(raw_errors: List) -> List[dict]:
@@ -60,23 +72,22 @@ def raw_errors_to_fields(raw_errors: List) -> List[dict]:
6072
:param raw_errors: List with instances of raw error
6173
:return: List of dicts (1 dict for every raw error)
6274
"""
63-
# import pdb;pdb.set_trace()
6475
fields = []
6576
for top_err in raw_errors:
6677
if hasattr(top_err.exc, "raw_errors"):
6778
for err in top_err.exc.raw_errors:
6879
field_err = parse_error(
6980
err,
7081
field_names=list(map(lambda x: x["name"], fields)),
71-
raw=True
82+
raw=True,
7283
)
7384
if field_err is not None:
7485
fields.append(field_err)
7586
else:
7687
field_err = parse_error(
7788
top_err,
7889
field_names=list(map(lambda x: x["name"], fields)),
79-
raw=False
90+
raw=False,
8091
)
8192
if field_err is not None:
8293
fields.append(field_err)
@@ -98,8 +109,8 @@ async def http_exception_handler(
98109
"""
99110
fields = getattr(exc, "fields", [])
100111
data = {
101-
"code": getattr(exc, "error_code", exc.status_code),
102-
"detail": getattr(exc, "message", exc.detail),
112+
"error_codes": [getattr(exc, "error_code", exc.status_code)],
113+
"message": getattr(exc, "detail", "Validation error."),
103114
"fields": fields,
104115
}
105116
return UJSONResponse(data, status_code=exc.status_code)
@@ -118,35 +129,30 @@ async def validation_exception_handler(
118129
:param exc: StarletteHTTPException instance
119130
:return: UJSONResponse with newly formatted error data
120131
"""
121-
fields = raw_errors_to_fields(exc.raw_errors)
122132
status_code = getattr(exc, "status_code", 400)
133+
fields = raw_errors_to_fields(exc.raw_errors)
134+
135+
if fields:
136+
error_codes = set(list(map(lambda x: x["error_code"], fields)))
137+
else:
138+
error_codes = [getattr(exc, "error_code", status_code)]
139+
123140
data = {
124-
"code": getattr(exc, "error_code", status_code),
125-
"detail": getattr(exc, "message", "Validation error"),
141+
"error_codes": error_codes,
142+
"message": getattr(exc, "message", "Validation error."),
126143
"fields": fields,
127144
}
128145
return UJSONResponse(data, status_code=status_code)
129146

130147

131-
async def internal_server_error_handler(
132-
request: Request, exc: RequestValidationError
133-
) -> UJSONResponse:
134-
code = exc.error_code if hasattr(exc, "error_code") else 500
135-
detail = exc.detail if hasattr(exc, "detail") else "Internal Server Error."
136-
fields = exc.fields if hasattr(exc, "fields") else []
137-
status_code = exc.status_code if hasattr(exc, "status_code") else 500
138-
data = {"code": code, "detail": detail, "fields": fields}
139-
return UJSONResponse(data, status_code=status_code)
140-
141-
142148
async def not_found_error_handler(
143149
request: Request, exc: RequestValidationError
144150
) -> UJSONResponse:
145-
code = exc.error_code if hasattr(exc, "error_code") else 404
146-
detail = exc.detail if hasattr(exc, "detail") else "Not found."
147-
fields = exc.fields if hasattr(exc, "fields") else []
148-
status_code = exc.status_code if hasattr(exc, "status_code") else 404
149-
data = {"code": code, "detail": detail, "fields": fields}
151+
code = getattr(exc, "error_code", 404)
152+
detail = getattr(exc, "detail", "Not found.")
153+
fields = getattr(exc, "fields", [])
154+
status_code = getattr(exc, "status_code", 404)
155+
data = {"error_codes": [code], "message": detail, "fields": fields}
150156
return UJSONResponse(data, status_code=status_code)
151157

152158

@@ -171,4 +177,3 @@ async def startup():
171177
RequestValidationError, validation_exception_handler
172178
)
173179
app.add_exception_handler(404, not_found_error_handler)
174-
app.add_exception_handler(500, internal_server_error_handler)

fastapi_contrib/exceptions.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,3 +103,24 @@ def __init__(
103103
detail=detail,
104104
fields=fields,
105105
)
106+
107+
108+
class InternalServerError(HTTPException):
109+
def __init__(
110+
self,
111+
error_code: int = 500,
112+
detail: Any = "Internal Server Error.",
113+
fields: List[Dict] = None,
114+
):
115+
"""
116+
Generic Internal Server Error with support for custom error code.
117+
118+
:param error_code: Custom error code, unique throughout the app
119+
:param detail: detailed message of the error
120+
"""
121+
super().__init__(
122+
error_code=error_code,
123+
status_code=500,
124+
detail=detail,
125+
fields=fields,
126+
)

setup.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@
1111
with open("HISTORY.rst") as history_file:
1212
history = history_file.read()
1313

14-
requirements = ["fastapi==0.42.0"]
14+
requirements = [
15+
'fastapi==0.42.0',
16+
'contextvars;python_version<"3.7"'
17+
]
1518

1619
setup_requirements = ["pytest-runner"]
1720

@@ -26,7 +29,9 @@
2629
"License :: OSI Approved :: MIT License",
2730
"Natural Language :: English",
2831
"Programming Language :: Python :: 3",
32+
"Programming Language :: Python :: 3.6",
2933
"Programming Language :: Python :: 3.7",
34+
"Programming Language :: Python :: 3.8",
3035
],
3136
description="Opinionated set of utilities on top of FastAPI",
3237
install_requires=requirements,

tests/auth/test_permissions.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def test_doesnt_have_auth_permission():
6868
assert response.status_code == 401
6969
response = response.json()
7070
assert response == {
71-
"code": 401,
72-
"detail": "Not authenticated.",
71+
"error_codes": [401],
72+
"message": "Not authenticated.",
7373
"fields": [],
7474
}

0 commit comments

Comments
 (0)