Skip to content

Commit

Permalink
Add support for newer FastAPI and fix quickstart docs
Browse files Browse the repository at this point in the history
  • Loading branch information
zmievsa committed Sep 16, 2024
1 parent 1b54646 commit fd8a09e
Show file tree
Hide file tree
Showing 10 changed files with 879 additions and 470 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ Please follow [the Keep a Changelog standard](https://keepachangelog.com/en/1.0.

## [Unreleased]

## [4.2.3]

### Fixed

* Added support for `embed_body_fields` in `solve_dependencies` and `create_model_field` in FastAPI. FastAPI has made a breaking change for these interfaces which is why we had to fix it
* Fixed invalid imports in quickstart docs
* Fixed default dependencies not including the CLI for FastAPI, thus causing the quickstart docs to be invalid

## [4.2.2]

### Fixed
Expand Down
8 changes: 4 additions & 4 deletions cadwyn/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,11 @@ async def dispatch(
request=request,
dependant=self.version_header_validation_dependant,
async_exit_stack=async_exit_stack,
embed_body_fields=False,
)
values, errors, *_ = solved_result
if errors:
return self.default_response_class(status_code=422, content=_normalize_errors(errors))
api_version = cast(date, values[self.api_version_header_name.replace("-", "_")])
if solved_result.errors:
return self.default_response_class(status_code=422, content=_normalize_errors(solved_result.errors))
api_version = cast(date, solved_result.values[self.api_version_header_name.replace("-", "_")])
self.api_version_var.set(api_version)

response = await call_next(request)
Expand Down
2 changes: 1 addition & 1 deletion cadwyn/schema_generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -441,7 +441,7 @@ def migrate_router_to_version(self, router: fastapi.routing.APIRouter):
def migrate_route_to_version(self, route: fastapi.routing.APIRoute, *, ignore_response_model: bool = False):
if route.response_model is not None and not ignore_response_model:
route.response_model = self.change_version_of_annotation(route.response_model)
route.response_field = fastapi.utils.create_response_field(
route.response_field = fastapi.utils.create_model_field(
name="Response_" + route.unique_id,
type_=route.response_model,
mode="serialization",
Expand Down
25 changes: 18 additions & 7 deletions cadwyn/structure/versions.py
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,9 @@ async def _migrate_request(
request_info: RequestInfo,
current_version: VersionDate,
head_route: APIRoute,
*,
exit_stack: AsyncExitStack,
embed_body_fields: bool = False,
) -> dict[str, Any]:
method = request.method
for v in reversed(self.versions):
Expand All @@ -367,19 +369,20 @@ async def _migrate_request(
request.scope["headers"] = tuple((key.encode(), value.encode()) for key, value in request_info.headers.items())
del request._headers
# Remember this: if len(body_params) == 1, then route.body_schema == route.dependant.body_params[0]
dependencies, errors, _, _, _ = await solve_dependencies(
result = await solve_dependencies(
request=request,
response=response,
dependant=head_dependant,
body=request_info.body,
dependency_overrides_provider=head_route.dependency_overrides_provider,
async_exit_stack=exit_stack,
embed_body_fields=embed_body_fields,
)
if errors:
if result.errors:
raise CadwynHeadRequestValidationError(
_normalize_errors(errors), body=request_info.body, version=current_version
_normalize_errors(result.errors), body=request_info.body, version=current_version
)
return dependencies
return result.values

def _migrate_response(
self,
Expand Down Expand Up @@ -452,7 +455,8 @@ async def decorator(*args: Any, **kwargs: Any) -> _R:
response_param,
route,
head_route,
exit_stack,
exit_stack=exit_stack,
embed_body_fields=route._embed_body_fields,
)

response = await self._convert_endpoint_response_to_version(
Expand Down Expand Up @@ -524,10 +528,14 @@ async def _convert_endpoint_response_to_version( # noqa: C901
if isinstance(response_or_response_body, StreamingResponse | FileResponse):
body = None
elif response_or_response_body.body:
if isinstance(response_or_response_body, JSONResponse) or raised_exception is not None:
if (isinstance(response_or_response_body, JSONResponse) or raised_exception is not None) and isinstance(
response_or_response_body.body, str | bytes
):
body = json.loads(response_or_response_body.body)
else:
elif isinstance(response_or_response_body.body, bytes):
body = response_or_response_body.body.decode(response_or_response_body.charset)
else: # pragma: no cover # I don't see a good use case here yet
body = response_or_response_body.body
else:
body = None
# TODO (https://github.com/zmievsa/cadwyn/issues/51): Only do this if there are migrations
Expand Down Expand Up @@ -614,7 +622,9 @@ async def _convert_endpoint_kwargs_to_version(
response: FastapiResponse,
route: APIRoute,
head_route: APIRoute,
*,
exit_stack: AsyncExitStack,
embed_body_fields: bool,
) -> dict[str, Any]:
request: FastapiRequest = kwargs[request_param_name]
if request_param_name == _CADWYN_REQUEST_PARAM_NAME:
Expand Down Expand Up @@ -655,6 +665,7 @@ async def _convert_endpoint_kwargs_to_version(
api_version,
head_route,
exit_stack=exit_stack,
embed_body_fields=embed_body_fields,
)
# Because we re-added it into our kwargs when we did solve_dependencies
if _CADWYN_REQUEST_PARAM_NAME in new_kwargs:
Expand Down
5 changes: 1 addition & 4 deletions docs/quickstart/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,14 @@ Cadwyn is built around FastAPI and supports all of its functionality out of the
## Installation

```bash
pip install cadwyn
pip install 'cadwyn[cli]'
```

## The basics

First, let's set up the most basic versioned app possible:

```python
# main.py

from datetime import date
from cadwyn import Cadwyn, VersionBundle, HeadVersion, Version

Expand All @@ -36,7 +34,6 @@ fastapi dev main.py

That's it. That's the main difference between setting up FastAPI and Cadwyn: you have to specify your versions. Everything you specify at app level (such as using `include_router` or `app.get(...)`) will end up unversioned and essentially function like a regular FastAPI route.


## Docs

If you visit `/docs`, instead of the regular swagger, you will see a version dashboard:
Expand Down
10 changes: 7 additions & 3 deletions docs/quickstart/tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,16 @@ Here is an initial API setup where the User has a single address. We will be imp

The first API you come up with usually doesn't require more than one address -- why bother?


```python
import uuid
from pydantic import BaseModel
from cadwyn import Cadwyn, VersionBundle, VersionedAPIRouter
from cadwyn.versions import HeadVersion, Version
from cadwyn import (
Cadwyn,
HeadVersion,
Version,
VersionBundle,
VersionedAPIRouter,
)


class BaseUser(BaseModel):
Expand Down
Loading

0 comments on commit fd8a09e

Please sign in to comment.