Skip to content

document 404 response #104

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

Merged
merged 8 commits into from
Sep 19, 2021
Merged

Conversation

sondrelg
Copy link
Contributor

@sondrelg sondrelg commented Sep 18, 2021

Closes #103, #102

PR Includes 404 responses for the retrieve, update, and delete endpoints.

Context

I started working on this thinking that crudrouter manually documented the 200 and 422 response, but I've come to suspect that this is built-into FastAPI - is that right?

Passing responses to fastapi.routing.add_api_route seems to satisfy the wip test I created. I also tested with

Left to do

Before finalizing, I was hoping to run the test suite here @awtkns - I had some trouble setting up some of the test requirements locally 👏 And please let me know if you have any suggestions for improvement.

And if you have any ideas about where to include tests, that would be great! So far I just put a wip test in test_openapi_schema.py which seemed fitting.

@vercel
Copy link

vercel bot commented Sep 18, 2021

This pull request is being automatically deployed with Vercel (learn more).
To see the status of your deployment, click below or on the icon next to each commit.

🔍 Inspect: https://vercel.com/flortz/fastapi-crudrouter/Cm1xoiicEYtMrFQ9H6h6wzQ5XsoN
✅ Preview: https://fastapi-crudrouter-git-fork-sondrelg-sondrelg-doc-96371c-flortz.vercel.app

@awtkns awtkns added the enhancement New feature or request label Sep 18, 2021
@awtkns awtkns linked an issue Sep 18, 2021 that may be closed by this pull request
@awtkns
Copy link
Owner

awtkns commented Sep 18, 2021

Awesome thanks so much for this! I am also tempted to include schemathesis in the test suite to validate the correctness of the generated OpenApi spec. This will be merged and released sometime over the next couple days,

Comment on lines 10 to 11
NOT_FOUND_RESPONSE = {"404": {"detail": "Item not found"}}
RESPONSES = NOT_FOUND_RESPONSE # can be extended to contain multiple responses
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not needed if done as described below 😄

@@ -90,6 +92,7 @@ def __init__(
response_model=self.schema,
summary="Get One",
dependencies=get_one_route,
responses=RESPONSES,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
responses=RESPONSES,
responses=[NOT_FOUND],

Minor: It may be cleaner to add the field responses: [HTTPException] = None to the _add_api_route. In the _add_api_route function. You could then extract the status code / detail of the exception, reducing code duplication.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This won't work unfortunately - the responses must be a mapping, so we would need something like a dictionary merge. But I guess we could also just define a RESPONSES to contain everything needed.

What would you prefer? 🙂

The type needs to be Optional[Dict[Union[int, str], Dict[str, Any]]] and it fails with TypeError: 'list' object is not a mapping if you try to pass a list.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was imagining either

NOT_FOUND_RESPONSE = {"404": {"detail": "Item not found"}}
FORBIDDEN_RESPONSE = {"403": {"detail": "Forbidden"}}
INTERNAL_SERVER_ERROR_RESPONSE = {"500": {"detail": "Server error"}}
RESPONSES = NOT_FOUND_RESPONSE | FORBIDDEN_RESPONSE | INTERNAL_SERVER_ERROR_RESPONSE

or just

RESPONSES = {
    "404": {"detail": "Item not found"}, 
    "403": {"detail": "Forbidden"}, 
    "500": {"detail": "Server error"}
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I implemented the last one in eb489bb, but let me know if you prefer something else 👏

@Stranger6667
Copy link

Hi there :)

I am also tempted to include schemathesis in the test suite to validate the correctness of the generated OpenApi spec.

Awesome! Let me know if you'll need any adjustments on the Schemathesis side for your use case :)

@sondrelg sondrelg changed the title WIP: document 404 response document 404 response Sep 19, 2021
@sondrelg sondrelg requested a review from awtkns September 19, 2021 09:20
@sondrelg
Copy link
Contributor Author

I think this is done btw @awtkns 🙂 Let me know if you want any other changes implemented 👍

@awtkns
Copy link
Owner

awtkns commented Sep 19, 2021

Thanks for this @sondrelg 🎉

Changes will be released to version 0.8.3 on merge.

@awtkns awtkns merged commit 542af61 into awtkns:master Sep 19, 2021
@sondrelg sondrelg deleted the sondrelg/document-404-response branch September 19, 2021 19:29
@sondrelg
Copy link
Contributor Author

sondrelg commented Sep 20, 2021

@awtkns I looked at this a little more, and I think my implementation here might be a little bit off. If you're planning to implement schemathesis I'm sure you'll spot it, but leaving this here in case it's helpful.

Looking through these docs, it looks like this is the way to specify responses:

class NotFoundModel(BaseModel):
    detail: str


@router.get('/{item_id}/', status_code=200, response_model=Model, responses={'404': {'model': NotFoundModel}})
async def retrieve(item_id: int) -> Model:
    try:
        return await Model.objects.get(id=item_id)
    except NoMatch:
        raise HTTPException(status_code=404, detail="Item not found")

In other words, I think this PR fixed one schemathesis issue: the missing response code, but didn't fully resolve it, since the model isn't correctly specified.

@sondrelg
Copy link
Contributor Author

sondrelg commented Sep 20, 2021

Not sure how helpful this is, since it's not 1:1 with the crudrouter implementation, but here's a full example of most of the crudrouter endpoints, using ormar, after adjusting them so they pass schemathesis tests:

import logging
from typing import Optional

from asyncpg import UniqueViolationError, CharacterNotInRepertoireError
from fastapi import APIRouter, HTTPException
from ormar import NoMatch
from pydantic import BaseModel

from payments.models import Author

logger = logging.getLogger(__name__)

author_router = APIRouter(prefix='/authors')


class HTTPExceptionMessage(BaseModel):
    detail: str


responses = {
    'not_found': {'404': {'model': HTTPExceptionMessage}},
    'validation_error': {'422': {'model': HTTPExceptionMessage}}
}


@author_router.get('/', status_code=200, response_model=list[Author])
async def get(skip: Optional[int] = 0, limit: Optional[int] = None) -> list[Author]:
    qs = Author.objects.filter()
    if skip:
        qs.offset(skip)
    if limit:
        qs.limit(limit)
    return await qs.all()


AuthorCreateModel = Author.get_pydantic(exclude={'id', 'books'})


@author_router.post('/', status_code=201, response_model=Author, responses=responses['validation_error'])
async def create(author: AuthorCreateModel) -> Author:
    try:
        return await Author.objects.create(**author.dict())
    except UniqueViolationError:
        raise HTTPException(422, detail="Name is taken")
    except CharacterNotInRepertoireError:
        raise HTTPException(422, detail="Invalid encoding")


@author_router.delete('/', status_code=204)
async def delete_all() -> None:
    authors = await Author.objects.all()
    for author in authors:
        await author.delete()


@author_router.get('/{item_id}/', status_code=200, response_model=Author, responses=responses["not_found"])
async def retrieve(item_id: int) -> Author:
    try:
        return await Author.objects.get(id=item_id)
    except NoMatch:
        raise HTTPException(status_code=404, detail="Item not found")


AuthorUpdateModel = Author.get_pydantic(exclude={'id', 'books'})


@author_router.put('/{item_id}/', status_code=200, response_model=Author, responses=responses['not_found'])
async def update(item_id: int, updated_author: AuthorUpdateModel) -> Author:
    try:
        author = await Author.objects.get(id=item_id)
    except NoMatch:
        raise HTTPException(status_code=404, detail="Item not found")

    await author.update(**updated_author.dict(exclude_unset=True))
    return author


@author_router.delete('/{item_id}/', status_code=204, responses=responses['not_found'])
async def delete(item_id: int) -> None:
    try:
        author = await Author.objects.get(id=item_id)
        await author.delete()
    except NoMatch:
        raise HTTPException(status_code=404, detail="Item not found")

@awtkns
Copy link
Owner

awtkns commented Sep 20, 2021

Thanks again for the detailed write up @sondrelg! I've created an issue to track this in #105.

Have you written any additional code to setup Schemathesis for testing against the file above? Or are you just doing it from the cli?

@sondrelg
Copy link
Contributor Author

Just from the CLI for now 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

404 Status Code not Docummented
3 participants