-
Notifications
You must be signed in to change notification settings - Fork 179
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(robot-server): add formatted json responses and error handling #5208
Conversation
if isinstance(exception, StarletteHTTPException): | ||
request_error = { | ||
'status': status_code, | ||
'detail': exception.detail, | ||
'title': 'Bad Request' | ||
} | ||
error_response = ErrorResponse(errors=[request_error]) | ||
return filter_none(error_response.dict()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is annoying b/c the error comes back differently if it's a HTTPException
, I can probably clean this up a bit (feels a bit redundant)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My preference would be a different function for each Exception type.
# NOTE(isk: 3/10/20): mock DB / robot response | ||
item = Item(name="apple", quantity=10, price=1.20) | ||
data = ItemResponse.resource_object(id=item_id, attributes=item) | ||
return ItemResponse(data=data, links={"self": f'/items/{item_id}'}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I should add a helper method in ItemResponse
to format links similar to resource_object class method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Couple small typos but I like the model factories. Is the idea to use this on everything?
title: Optional[str] | ||
detail: Optional[str] | ||
source: Optional[ErrorSource] | ||
meta: Optional[dict] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
here and elsewhere, we should use typing.Dict
in type definitions - it's the typing generic version of dictionaries
def dict( | ||
self, | ||
*, | ||
serlialize_none: bool = False, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo
|
||
def transform_to_json_api_errors(status_code, exception) -> dict: | ||
if isinstance(exception, StarletteHTTPException): | ||
request_error = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can also construct this using an Error
object rather than a dict.
if isinstance(exception, StarletteHTTPException): | ||
request_error = { | ||
'status': status_code, | ||
'detail': exception.detail, | ||
'title': 'Bad Request' | ||
} | ||
error_response = ErrorResponse(errors=[request_error]) | ||
return filter_none(error_response.dict()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My preference would be a different function for each Exception type.
from .request import JsonApiRequest, RequestModel | ||
from .response import JsonApiResponse, ResponseModel | ||
|
||
def JsonApiModel( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
functions should use snake case.
'title': 'Bad Request' | ||
} | ||
error_response = ErrorResponse(errors=[request_error]) | ||
return filter_none(error_response.dict()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As you saw, you can direct fastapi to omit default or unset values in handlers. Looks like you can also tell pydantic to do that when converting to dict. So you might not need the filter functions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice! this was what i found yesterday (https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter) so it looks like we can get rid of this filter_none
stuff entirely and TBH it's a bit more explicit in the handlers
@sfoster1 yeah, the idea would be to use this to format our api responses going forward (new endpoints). |
Codecov Report
@@ Coverage Diff @@
## edge #5208 +/- ##
==========================================
+ Coverage 61.17% 61.34% +0.16%
==========================================
Files 1032 1039 +7
Lines 29416 29559 +143
==========================================
+ Hits 17995 18132 +137
- Misses 11421 11427 +6
Continue to review full report at Codecov.
|
|
||
|
||
class ErrorSource(BaseModel): | ||
pointer: Optional[str] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not that it's essential for this PR, but adding documentation for these attributes will be great for the docs UI:
my_field: str = Field(..., description="This is my field")
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah, yeah - thanks i'll add now
errors: List[Error] | ||
|
||
|
||
def transform_http_exception_to_json_api_errors(exception) -> Dict: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: I would return an ErrorResponse
object from these two functions. Only converting it to Dict when it needs to go over the wire. It's a minor point, but it's about habit. The habit of using clearly defined types rather than dicts and tuples and so on.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
makes a lot of sense
from typing import Generic, TypeVar, Optional, Any, Type | ||
from typing_extensions import Literal | ||
from pydantic.generics import GenericModel | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know I'm old fashioned, but I like comments. This is somewhat complicated and comments explain what these types are would be great.
from robot_server.service.main import app | ||
from tests.service.helpers import ItemData | ||
|
||
client = TestClient(app) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's nothing necessarily wrong with this, but our python code base tends to use pytest and their fixtures.
In fact, if you rebase of of edge, you can just use the fixture that's already set up called api_client
. That way your tests could look like:
def test_get_item(api_client):
response = api_client.get(....)
from robot_server.service.models.json_api.response import JsonApiResponse | ||
from tests.service.helpers import ItemModel, ItemData | ||
|
||
class TestJsonApiResponse: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see a need for there to be a class here. Can't the tests just all be global functions? That would more jive with the rest of our code base.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
they sure can. yup, makes sense to be consistent was just trying to keep nicely organized, I didn't realize that we don't do this elsewhere. my bad
This is a great piece of work! A lot of style comments. Pythonisms etc. And just asking for more comments. |
a8794cf
to
bf814b3
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Woot!
@@ -28,6 +36,8 @@ | |||
tags=["settings"]) | |||
app.include_router(router=deck_calibration.router, | |||
tags=["deckCalibration"]) | |||
app.include_router(router=item.router, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could use a TODO comment noting that this should be removed
" meta-information about the error.") | ||
|
||
|
||
class ErrorResponse(BaseModel): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't match what's in our documentation at https://coda.io/d/HTTP-API-v2_dS1dJrSYETc/Error-Responses_suowf#_luSMh. Can you make sure the docs are up to date?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah, for sure. I will update this accordingly. thanks for pointing out
exception: RequestValidationError | ||
) -> JSONResponse: | ||
errors = transform_validation_error_to_json_api_errors( | ||
HTTP_422_UNPROCESSABLE_ENTITY, exception |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm curious about the rational behind using what appears to be a WebDAV extension code (422) instead of an old-fashioned 400
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fastapi and json:api spec both return validation errors as 422
. The main difference between 400
is that the syntax is correct in the request. See below (https://tools.ietf.org/html/rfc4918#section-11.2):
The 422 (Unprocessable Entity) status code means the server understands the content type of the request entity (hence a 415(Unsupported Media Type) status code is inappropriate), and the syntax of the request entity is correct (thus a 400 (Bad Request) status code is inappropriate) but was unable to process the contained instructions.
|
||
|
||
@router.get("/items/{item_id}", | ||
description="Get an individual item by it's ID", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/it's/its
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One minor nitpick but if it passes tests I'm ok with it. A lot of fun type manipulation, I like it. You might want to get some solid documentation going somewhere to onboard people who haven't really used runtime type handling.
|
||
# Note(isk: 3/13/20): returns response based on whether | ||
# the data object is a list or not | ||
def JsonApiResponse( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's keep to snake case or (if you must) camelCase for functions please
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah, sorry - i meant to change that. thanks for catching
Add formatted json responses and error handling in FastAPI framework closes #4636
821b170
to
0b1fb67
Compare
Adds formatted json responses and error handling in FastAPI framework
closes #4636
overview
changelog
review requests
In the
robot-server
project:OT_API_FF_useFastApi=TRUE make dev
You should be able to visit
/docs
route on localhost. This will allow you to review the swagger interactive docs. The two endpoints these changes affect are the placeholder endpoints/items
and/items/{item_id}
Please add comments around any additions you would like to see, this is a first pass at solidifying some structure based on the JSON:API spec but we can change and add as we see fit.
Still todo: