Dynamic request model in FastAPI - fastapi

I'm writing a class-based generic CRUD router/handler in FastAPI and need something similar to response_model for the request, to generate a Swagger UI to the input model without writing a separate function for the endpoint. Example for reading a user model and filtering it base on it's username:
class ModelReadHandler:
def __init__(self, base_url: str, response_model_cls: type, request_model_cls: type):
self.router = APIRouter()
self.response_model = response_model_cls
self.request_model = request_model_cls
self.add_routes()
def add_routes():
self.router.add_api_route(f'/{self.base_url}/', self.read, methods=['GET'],
response_model=self.response_model)
def read(self, input_model: self.request_model_cls # ** actually can't do this but looking the behaviour of this hypothetical code
session: Depends(get_session),
):
query = input_model.get_read_query()
return session.exec(query).all()
# -- the input_model would extend this abstract class
class InputModel(pydantic.BaseModel):
def get_read_query(self):
raise NotImplementedError()
# class example for InputModel
class UserInputModel(InputModel):
username: int
def get_read_query(self):
return select(UserModel).where(UserModel.username == self.username)
# and we instantiate the user handler like this
user_handler = ModelReadHandler(base_url='user',
request_model_cls=UserInputModel,
response_model_cls=UserModel)
app = FastAPI()
app.include_router(user_handler.router)
Now this doesn't work because there is no way to set the type of the input_model argumnent in ModelReadhandler.read(). Is there a way to achieve this behaviour? - the goal is to add dynamically generated endpoints with swagger uis indicating their request parameters.
Note
An alternative is to extend the class for each model and type hint according to the model. I do not want that.

FastAPI supports defining request models as Pydantic models and uses the defined models to automatically generate Swagger UI documentation for the request parameters. You can use the Body class from fastapi.params to define the request model for the endpoint.
class ModelReadHandler:
def __init__(self, base_url: str, response_model_cls: type, request_model_cls: type):
self.router = APIRouter()
self.response_model = response_model_cls
self.request_model = request_model_cls
self.add_routes()
def add_routes():
self.router.add_api_route(f'/{self.base_url}/', self.read, methods=['GET'],
response_model=self.response_model)
def read(self, input_model: Body(self.request_model)
session: Depends(get_session),
):
query = input_model.get_read_query()
return session.exec(query).all()
# -- the input_model would extend this abstract class
class InputModel(pydantic.BaseModel):
def get_read_query(self):
raise NotImplementedError()
# class example for InputModel
class UserInputModel(InputModel):
username: int
def get_read_query(self):
return select(UserModel).where(UserModel.username == self.username)
# and we instantiate the user handler like this
user_handler = ModelReadHandler(base_url='user',
request_model_cls=UserInputModel,
response_model_cls=UserModel)
app = FastAPI()
app.include_router(user_handler.router)

Related

FastAPI Custom Websocket Object

I want to be able to create a custom WebSocket object rather than using Starlette's so that I can add some more things in the constructor and add some more methods. In FastAPI, you're able to subclass the APIRoute and pass in your own Request object. How would I do the same for the WebSocket router?
As you say, there doesn't seem to be an easy way to set the websocket route class (short of a lot of subclassing and rewriting). I think the simplest way to do this would be to define your own wrapper class around the websocket, taking whatever extra data you want, and then define the methods you need. Then you can inject that as a dependency, either with a separate function, or use the class itself as a dependency, see the documentation for details, which is what I'm doing below.
I've put together a minimal example, where the URL parameter name is passed to the wrapper class:
# main.py
from fastapi import Depends, FastAPI, WebSocket
app = FastAPI()
class WsWrapper:
def __init__(self, websocket: WebSocket, name: str) -> None:
self.name = name
self.websocket = websocket
# You can define all your custom logic here, I'm just adding a print
async def receive_json(self, mode: str = "text"):
print(f"Hello from {self.name}", flush=True)
return await self.websocket.receive_json(mode)
#app.websocket("/{name}")
async def websocket(ws: WsWrapper = Depends()):
await ws.websocket.accept()
while True:
data = await ws.receive_json()
print(data, flush=True)
You can test it by running uvicorn main:app and connecting to ws://localhost:8000/test, and it should print "Hello from test" when receiving JSON.
Ended up just monkeypatching the modules. Track this PR for when monkeypatching isn't necessary: https://github.com/tiangolo/fastapi/pull/4968
from typing import Callable
from fastapi import routing as fastapi_routing
from starlette._utils import is_async_callable
from starlette.concurrency import run_in_threadpool
from starlette.requests import Request as StarletteRequest
from starlette.websockets import WebSocket as StarletteWebSocket
from starlette.types import ASGIApp, Receive, Scope, Send
class Request(StarletteRequest):
pass
class WebSocket(StarletteWebSocket):
pass
def request_response(func: Callable) -> ASGIApp:
"""
Takes a function or coroutine `func(request) -> response`,
and returns an ASGI application.
"""
is_coroutine = is_async_callable(func)
async def app(scope: Scope, receive: Receive, send: Send) -> None:
request = Request(scope, receive=receive, send=send)
# Force all views to be a coroutine
response = await func(request)
if is_coroutine:
response = await func(request)
else:
response = await run_in_threadpool(func, request)
await response(scope, receive, send)
return app
fastapi_routing.request_response = request_response
def websocket_session(func: Callable) -> ASGIApp:
"""
Takes a coroutine `func(session)`, and returns an ASGI application.
"""
# assert asyncio.iscoroutinefunction(func), "WebSocket endpoints must be async"
async def app(scope: Scope, receive: Receive, send: Send) -> None:
session = WebSocket(scope, receive=receive, send=send)
await func(session)
return app
fastapi_routing.websocket_session = websocket_session

Basic token authorization in FastApi

I need help understanding how to process a user-supplied token in my FastApi app.
I have a simple app that takes a user-session key, this may be a jwt or not. I will then call a separate API to validate this token and proceed with the request or not.
Where should this key go in the request:
In the Authorization header as a basic token?
In a custom user-session header key/value?
In the request body with the rest of the required information?
I've been playing around with option 2 and have found several ways of doing it:
Using APIKey as described here:
async def create(api_key: APIKey = Depends(validate)):
Declaring it in the function as described in the docs here
async def create(user_session: str = Header(description="The Users session key")): and having a separate Depends in the router config,
The best approach is to build a custom dependency using any one of the already existing authentication dependencies as a reference.
Example:
class APIKeyHeader(APIKeyBase):
def __init__(
self,
*,
name: str,
scheme_name: Optional[str] = None,
description: Optional[str] = None,
auto_error: bool = True
):
self.model: APIKey = APIKey(
**{"in": APIKeyIn.header}, name=name, description=description
)
self.scheme_name = scheme_name or self.__class__.__name__
self.auto_error = auto_error
async def __call__(self, request: Request) -> Optional[str]:
api_key: str = request.headers.get(self.model.name)
# add your logic here, something like the one below
if not api_key:
if self.auto_error:
raise HTTPException(
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
)
else:
return None
return api_key
After that, just follow this from documentation to use your dependency.

Django JWT authentication - user is anonymous in middleware

I am using Django JWT to power up authentication system in my project.
Also, I have a middleware, and the problem is that inside it, the user is anonymous for some reason, while in the view I am able to access the correct user by request.user. This issue is driving me crazy because some time ago this code worked perfectly ! Is this JWT's bug or I am doing something wrong ?
class TimezoneMiddleware(MiddlewareMixin):
def process_request(self, request):
# request.user is ANONYMOUS HERE !!!!
if not request.user.is_anonymous:
tzname = UserProfile.objects.get(user = request.user).tz_name
if tzname:
timezone.activate(pytz.timezone(tzname))
Relevant settings.py module:
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.BasicAuthentication',
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
),
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
),
'DEFAULT_RENDERER_CLASSES': (
'djangorestframework_camel_case.render.CamelCaseJSONRenderer',
# Any other renders
),
'DEFAULT_PARSER_CLASSES': (
'djangorestframework_camel_case.parser.CamelCaseJSONParser',
# Any other parsers
),
}
JWT_AUTH = {
'JWT_ENCODE_HANDLER':
'rest_framework_jwt.utils.jwt_encode_handler',
'JWT_DECODE_HANDLER':
'rest_framework_jwt.utils.jwt_decode_handler',
'JWT_PAYLOAD_HANDLER':
'rest_framework_jwt.utils.jwt_payload_handler',
'JWT_PAYLOAD_GET_USER_ID_HANDLER':
'rest_framework_jwt.utils.jwt_get_user_id_from_payload_handler',
'JWT_RESPONSE_PAYLOAD_HANDLER': 'rest_framework_jwt.utils.jwt_response_payload_handler',
# 'rest_authentication.views.jwt_response_payload_handler',
'JWT_SECRET_KEY': SECRET_KEY,
'JWT_PUBLIC_KEY': None,
'JWT_PRIVATE_KEY': None,
'JWT_ALGORITHM': 'HS256',
'JWT_VERIFY': True,
'JWT_VERIFY_EXPIRATION': False,
'JWT_LEEWAY': 0,
'JWT_EXPIRATION_DELTA': datetime.timedelta(seconds=300),
'JWT_AUDIENCE': None,
'JWT_ISSUER': None,
'JWT_ALLOW_REFRESH': False,
'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=7),
'JWT_AUTH_HEADER_PREFIX': 'JWT',
}
I have also come across resources which helped me to retrieve the actual user, BUT ! I am still unable to set the timezone (timezone.activate(pytz.timezone(tzname)) seems to be ignored.
Yes, this issue is due to the JWT. You can checkout the discussion for it https://github.com/GetBlimp/django-rest-framework-jwt/issues/45 To fix this you will have to create a custom middleware which will set the request.user. Here is one I am using in my code:
from django.contrib.auth.middleware import get_user
from django.utils.functional import SimpleLazyObject
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
class JWTAuthenticationMiddleware(object):
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
request.user = SimpleLazyObject(lambda:self.__class__.get_jwt_user(request))
return self.get_response(request)
#staticmethod
def get_jwt_user(request):
user = get_user(request)
if user.is_authenticated:
return user
jwt_authentication = JSONWebTokenAuthentication()
if jwt_authentication.get_jwt_value(request):
user, jwt = jwt_authentication.authenticate(request)
return user
Include this in the middlewares. It should come above all the middlewares which are using request.user.
#Atul Mishra: Thank you! Changed your version to the newest drf-jwt package (1.17.2). Seems like the the current github repository moved from this to here
from django.contrib.auth.middleware import get_user
from django.utils.functional import SimpleLazyObject
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
class JWTAuthenticationInMiddleware(object):
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
request.user = SimpleLazyObject(lambda:self.__class__.get_jwt_user(request))
return self.get_response(request)
#staticmethod
def get_jwt_user(request):
# Already authenticated
user = get_user(request)
if user.is_authenticated:
return user
# Do JTW authentication
jwt_authentication = JSONWebTokenAuthentication()
authenticated = jwt_authentication.authenticate(request)
if authenticated:
user, jwt = authenticated
return user

PUT requests with DjangoRestFramework

I am building a rest API and am wondering both about HTTP best practices I guess, and how that would apply to the DRF. When sending a PUT request, in the body, do requests have all of the parameters for objects that they would be manipulating? Even if not all of them are changing? Or do they only send the fields that are being updated? So for example if we had a House object with No. of rooms and floors, and I was changing No. of rooms should I only send that parameter, or both of them?
If requests should only contain the fields that are being updating, then how would that translate to the DjangoRestFramework? Any help would be greatly appreciated!
My Views are:
class HouseDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = House.objects.all()
serializer_class = HouseSerializer
and serializer is:
class HouseSerializer(serializers.ModelSerializer):
quotes = serializers.PrimaryKeyRelatedField(many=True, read_only=True)
class Meta:
model = House
fields = (
'pk',
'address',
'quotes',
)
PUT is for full resource updates, PATCH is for partial resource updates because Fielding says so.
Therefore, in your example, if you wanted to only update a No. of rooms for your HouseDetail model, you would send the following payload in a PATCH request:
{ "no. of rooms": "42" }
It is still possible to partially update things with a PUT request in DRF (explained below) but you should simply use PATCH for that because it was created for this. You get PUT and PATCH functionality for your model because you have subclassed the generics.RetrieveUpdateDestroyAPIView class and have defined a serializer.
If a required field is omitted in your PUT request (see documentation for required), then you will receive a 400 status code with a response like {"detail": "x field is required"}. However, when you request via PATCH, there is a partial=True argument passed to the serializer which allows this partial only update. We can see this for the UpdateModelMixin:
def partial_update(self, request, *args, **kwargs):
kwargs['partial'] = True
return self.update(request, *args, **kwargs)
which calls update, which is:
def update(self, request, *args, **kwargs):
partial = kwargs.pop('partial', False)
instance = self.get_object()
serializer = self.get_serializer(instance, data=request.data, partial=partial)
serializer.is_valid(raise_exception=True)
self.perform_update(serializer)
return Response(serializer.data)

How to wrap asynchronous and gen functions together in Tornado?

How to wrap asynchronous and gen functions together in Tornado?
My code looks like below, the error is 'Future' object has no attribute 'body'.
Did I place the decorators in a wrong way?
import tornado.httpclient
import tornado.web
import tornado.gen
import tornado.httpserver
import tornado.ioloop
class Class1(tornado.web.RequestHandler):
#tornado.web.asynchronous
def post(self, *args, **kwargs):
url = self.get_argument('url', None)
response = self.json_fetch('POST', url, self.request.body)
self.write(response.body)
self.finish()
#tornado.gen.engine
def json_fetch(self, method, url, body=None, *args, **kwargs):
client = tornado.httpclient.AsyncHTTPClient()
headers = tornado.httputil.HTTPHeaders({"content-type": "application/json charset=utf-8"})
request = tornado.httpclient.HTTPRequest(url, method, headers, body)
yield tornado.gen.Task(client.fetch, request)
You don't need "asynchronous" in this code example. "gen.engine" is obsolete, use "coroutine" instead. You don't generally need to use "gen.Task" much these days, either. Make four changes to your code:
Wrap "post" in "coroutine"
"yield" the result of self.json_fetch instead of using the result directly.
No need to call "finish" in a coroutine, Tornado finishes the response when a coroutine completes.
Wrap json_fetch in "coroutine", too.
The result:
class ClubCreateActivity(tornado.web.RequestHandler):
#tornado.gen.coroutine
def post(self, *args, **kwargs):
url = self.get_argument('url', None)
response = yield self.json_fetch('POST', url, self.request.body)
self.write(response.body)
#tornado.gen.coroutine
def json_fetch(self, method, url, body=None, *args, **kwargs):
client = tornado.httpclient.AsyncHTTPClient()
headers = tornado.httputil.HTTPHeaders({"content-type": "application/json charset=utf-8"})
request = tornado.httpclient.HTTPRequest(url, method, headers, body)
response = yield client.fetch(request)
raise gen.Return(response)
Further reading:
The section on coroutines in the Tornado User's Guide
Tornado async request handlers
My article on refactoring coroutines
The recommended method in tornado official documentation is using #tornado.gen.coroutine and yield together.
If you want to use both asynchronous and advantage of yield, you should nesting #tornado.web.asynchronous decorator followed by #tornado.gen.engine
Documentation about "asynchronous call own function" but without additional external callback-function — Asynchronous and non-Blocking I/O
You can make your json_fetch like this:
from tornado.concurrent import Future
def json_fetch(self, method, url, body=None, *args, **kwargs):
http_client = tornado.httpclient.AsyncHTTPClient()
my_future = Future()
fetch_future = http_client.fetch(url)
fetch_future.add_done_callback(
lambda f: my_future.set_result(f.result()))
return my_future
Or like this (from A. Jesse Jiryu Davis's answer):
from tornado import gen
#gen.coroutine
def json_fetch(self, method, url, body=None, *args, **kwargs):
http_client = tornado.httpclient.AsyncHTTPClient()
headers = tornado.httputil.HTTPHeaders({"content-type": "application/json charset=utf-8"})
request = tornado.httpclient.HTTPRequest(url, method, headers, body)
response = yield http_client.fetch(request)
raise gen.Return(response)
* wrap "post" in "gen.coroutine" and "yield" call of json_fetch.
** "raise gen.Return(response)" for Python2 only, in Python3.3 and later you should write "return response".
Thanks to A. Jesse Jiryu Davis for link "Tornado async request handlers", "Asynchronous and non-Blocking I/O" was found there.

Resources