Skip to content

Commit

Permalink
Merge pull request #31 from lsst-sqre/tickets/DM-31361
Browse files Browse the repository at this point in the history
[DM-31361] Safir improvements from mobu
  • Loading branch information
rra authored Aug 10, 2021
2 parents 87ab2fb + ccd5bc9 commit dae60f0
Show file tree
Hide file tree
Showing 6 changed files with 90 additions and 2 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ Change log
.. Headline template:
X.Y.Z (YYYY-MM-DD)
2.1.0 (2021-08-09)
==================

- Add ``safir.models.ErrorModel``, which is a model of the error message format preferred by FastAPI.
Using the model is not necessary but it's helpful to reference it in API documentation to generate more complete information about the error messages.
- Mark all FastAPI dependencies as async so that FastAPI doesn't run them in an external thread pool.

2.0.1 (2021-06-24)
==================

Expand Down
3 changes: 3 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,8 @@ API reference
.. automodapi:: safir.metadata
:include-all-objects:

.. automodapi:: safir.models
:include-all-objects:

.. automodapi:: safir.middleware.x_forwarded
:include-all-objects:
2 changes: 1 addition & 1 deletion src/safir/dependencies/http_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ async def shutdown_event() -> None:
def __init__(self) -> None:
self.http_client: Optional[httpx.AsyncClient] = None

def __call__(self) -> httpx.AsyncClient:
async def __call__(self) -> httpx.AsyncClient:
"""Return the cached ``httpx.AsyncClient``."""
if not self.http_client:
self.http_client = httpx.AsyncClient(timeout=DEFAULT_HTTP_TIMEOUT)
Expand Down
2 changes: 1 addition & 1 deletion src/safir/dependencies/logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class LoggerDependency:
def __init__(self) -> None:
self.logger: Optional[BoundLogger] = None

def __call__(self, request: Request) -> BoundLogger:
async def __call__(self, request: Request) -> BoundLogger:
"""Return a logger bound with request information.
Returns
Expand Down
56 changes: 56 additions & 0 deletions src/safir/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"""Standard models for FastAPI applications.
Examples
--------
To reference the `ErrorModel` model when returning an error message, use code
similar to this:
.. code-block:: python
@router.get(
"/route/{foo}",
...,
responses={404: {"description": "Not found", "model": ErrorModel}},
)
async def route(foo: str) -> None:
...
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=[
{"loc": ["path", "foo"], "msg": msg, "type": "invalid_foo"},
],
)
Notes
-----
FastAPI does not appear to export its error response model in a usable form,
so define a copy of it so that we can reference it in API definitions to
generate good documentation.
"""

# Examples and notes kept in the module docstring because they're not
# appropriate for the API documentation generated for a service.

from typing import List, Optional

from pydantic import BaseModel, Field

__all__ = ["ErrorDetail", "ErrorModel"]


class ErrorDetail(BaseModel):
"""The detail of the error message."""

loc: Optional[List[str]] = Field(
None, title="Location", example=["area", "field"]
)

msg: str = Field(..., title="Message", example="Some error messge")

type: str = Field(..., title="Error type", example="some_code")


class ErrorModel(BaseModel):
"""A structured API error message."""

detail: List[ErrorDetail] = Field(..., title="Detail")
22 changes: 22 additions & 0 deletions tests/models_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""Tests for safir.models."""

from __future__ import annotations

import json

from safir.models import ErrorModel


def test_error_model() -> None:
"""Nothing much to test, but make sure the code can be imported."""
error = {
"detail": [
{
"loc": ["path", "foo"],
"msg": "Invalid foo",
"type": "invalid_foo",
}
]
}
model = ErrorModel.parse_raw(json.dumps(error))
assert model.dict() == error

0 comments on commit dae60f0

Please sign in to comment.