Using FastAPI with Django

栏目: IT技术 · 发布时间: 4年前

内容简介:FastAPI actually plays very well with DjangoYou know me, I’m aMy only issue with Django was that it never really had a good way of making APIs. I hate DRF with somewhat of a passion, I always found its API way too complicated and verbose, and never managed

FastAPI actually plays very well with Django

You know me, I’m a Django fan. It’s my preferred way of developing web apps, mainly because of the absolutely vast ecosystem of apps and libraries it has, and the fact that it is really well-designed. I love how modular it is, and how it lets you use any of the parts you like and forget about the ones you don’t want. This is going to be emphasized rather spectacularly in this article, as I’m going to do things nobody should ever have to do.

My only issue with Django was that it never really had a good way of making APIs. I hate DRF with somewhat of a passion, I always found its API way too complicated and verbose, and never managed to grok it. Even the simplest things felt cumbersome, and the moment your API objects deviated from looking exactly like your DB models, you were in a world of hurt. I generally prefer writing a simple class-based view for my APIs, but then I don’t get automatic docs and other niceties.

It’s no surprise, then, that when I found FastAPI I was really excited, I really liked it’s autogenerated docs, dependency injection system, and lack of magical “request” objects or big JSON blobs. It looked very simple, well-architected and with sane defaults, and I seriously considered developing the API for my company’s next product on it, but was apprehensive about two things: It lacked Django’s ecosystem, and it didn’t have an ORM as good and well-integrated as Django’s. I would also miss Django’s admin interface a lot.

It would have been great if FastAPI was a Django library, but I guess the asynchronicity wouldn’t have been possible. Still, there’s no reason for DRF not to have an API as nice as FastAPI’s, but there’s no helping that. A fantastical notion caught hold of me: What if I could combine FastAPI’s view serving with Django’s ORM and apps? Verily, I say unto thee, it would be rad.

And that’s exactly what I did. Here’s how:

General details

Each part of this unholy union needed a different integration. I will go into details on each part here, and post code in the next section.

High-level overview

The way this profane joining works is by using FastAPI as the view layer, and importing and using parts of Django separately. This means that some things like middleware will obviously not work, since Django is not handling views at all.

I also didn’t try to get asynchronicity working, which might be a dealbreaker for many people. I suspect I’d have issues, as Django doesn’t support it very well yet, and the ORM would probably block all view threads. If you do get it working, or try to, please leave a comment here or contact me.

ORM

The ORM works quite well, basically exactly as if you were using Django. Migrations work fine, and any app that interfaces with or enhances the ORM should also work properly.

Tests

Tests are working great as well, using Django’s ./manage.py test harness/discovery. If you want to use another runner like pytest, that should also work properly, though I haven’t tested it.

To clarify, we’ll be testing the FastAPI views using Django’s test runner and ./manage.py test . The one problem I had was that the usual TestCase class didn’t work (it gave me “table is locked” errors), but TransactionTestCase works splendidly and is probably quite a bit faster, too.

The admin and other views

The admin is a vital part of Django, it makes dealing with your models much, much easier than having to wrangle with the database, so it was important to me to get working. And I did! It’s a pretty hacky way, but it works perfectly, since the trick is to just run Django on another port and have it serve the admin. You can have it serve any other views you want, this way you can use FastAPI to serve your API and use Django for static files and everything else (assuming you have anything else).

All you need is a second stanza in your reverse proxy.

The code

Without further delay, the part you’ve all been waiting for, the code. This section includes both the bare essentials for getting the integration working, and some best practices I’ve found when working with FastAPI, such as URL reversing, easier DB model to API model conversion, etc. I hope you’ll find it useful.

Starting out

To begin, install FastAPI, Uvicorn and Django and create a Django project as you normally would. I recommend my “new project” template , it contains various niceties you may want to use. We’re going to replace some parts and add others. I’ve created a project called goatfish and an app called main to illustrate, you can call yours whatever you want. Make sure you add "main" to Django’s INSTALLED_APPS list in the settings so it discovers the models and migrations.

The models

We should probably start with our data model so everything else makes sense. The API is going to be a straightforward CRUD API, which will serve a model we’ll call Simulation and provide authentication.

Since we have an API and a database, and the models for the two are neither semantically nor functionally identical, we’ll have two sections in our models.py , one for each model type.

main/models.py:

from typing import Any
from typing import Dict
from typing import List

import shortuuid
from django.contrib.auth.models import AbstractUser
from django.db import models
from pydantic import BaseModel


def generate_uuid() -> str:
    """Generate a UUID."""
    return shortuuid.ShortUUID().random(20)


##########
# Models
#


class CharIDModel(models.Model):
    """Base model that gives children random string UUIDs."""

    id = models.CharField(
        max_length=30,
        primary_key=True,
        default=generate_uuid,
        editable=False
    )

    class Meta:
        abstract = True


class User(AbstractUser):
    api_key = models.CharField(
        max_length=100,
        default=generate_uuid,
        db_index=True,
    )

    def __str__(self):
        return self.username


class Simulation(CharIDModel):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    name = models.CharField(max_length=300)
    start_date = models.DateField()
    end_date = models.DateField()

    @classmethod
    def from_api(cls, user: User, model: "DamnFastAPISimulation"):
        """
        Return a Simulation instance from an APISimulation instance.
        """
        return cls(
            user=user,
            name=model.name,
            start_date=model.start_date,
            end_date=model.end_date,
        )

    def update_from_api(self, api_model: "DamnFastAPISimulation"):
        """
        Update the Simulation Django model from an APISimulation instance.
        """
        self.name = api_model.name
        self.start_date = api_model.start_date
        self.end_date = api_model.end_date

    def __str__(self):
        return self.name


##########
# Types
#


# Over here I'm really ticked off I need to make two models just to
# exclude the id field from being required in the bodies of POSTs.
#
# Please comment on this bug if you feel as strongly:
# https://github.com/tiangolo/fastapi/issues/1357
class DamnFastAPISimulation(BaseModel):
    name: str
    start_date: date
    end_date: date

    @classmethod
    def from_model(cls, instance: Simulation):
        """
        Convert a Django Simulation model instance to an APISimulation instance.
        """
        return cls(
            id=instance.id,
            name=instance.name,
            start_date=instance.start_date,
            end_date=instance.end_date,
        )


class APISimulation(DamnFastAPISimulation):
    id: str


class APISimulations(BaseModel):
    items: List[APISimulation]

    @classmethod
    def from_qs(cls, qs):
        """
        Convert a Django Simulation queryset to APISimulation instances.
        """
        return cls(items=[APISimulation.from_model(i) for i in qs])

A few notes: Each class instance has helper methods, like from_api , from_model , from_qs , etc to facilitate converting between API-level and DB-level objects easily.

Also, unfortunately we have to make two separate type classes just to avoid having the id field show up in the POST request, as the user of the API should not be able to send/set the id when creating a new object. It’s annoying, and I have opened an issue on FastAPI’s GitHub .

The entry point

To serve requests, we need a place for our wsgi app to live. The best place is, naturally, wsgi.py :

goatfish/wsgi.py:

import os

from django.core.wsgi import get_wsgi_application
from fastapi import FastAPI

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "goatfish.settings")

application = get_wsgi_application()

from main.urls import router as main_router

app = FastAPI(
    title="Goatfish",
    description="A demo project. Also, an actual fish with a weird name.",
    version="We aren't doing versions yet. Point oh.",
)

app.include_router(main_router, prefix="/api")

This will allow Uvicorn to load and serve our app. Keep in mind that app is FastAPI’s entry point and application is Django’s. You’ll need to remember that if you deploy them to something that needs to load the WSGI entry points.

You can start the server by running uvicorn goatfish.wsgi:app --reload , but it’ll probably crash at this point because there are no URL routes yet. Let’s add them.

The routes

I prefer the Django convention of having each app’s route in a urls.py in the app directory, so let’s put them there:

main/urls.py:

from fastapi import APIRouter

from main import views

# The API model for one object.
from main.models import APISimulation

# The API model for a collection of objects.
from main.models import APISimulations

router = APIRouter()

router.get(
    "/simulation/",
    summary="Retrieve a list of all the simulations.",
    tags=["simulations"],
    response_model=APISimulations,
    name="simulations-get",
)(views.simulations_get)
router.post(
    "/simulation/",
    summary="Create a new simulation.",
    tags=["simulations"],
    response_model=APISimulation,
    name="simulations-post",
)(views.simulation_post)

router.get(
    "/simulation/{simulation_id}/",
    summary="Retrieve a specific simulation.",
    tags=["simulations"],
    response_model=APISimulation,
    name="simulation-get",
)(views.simulation_get)
router.put(
    "/simulation/{simulation_id}/",
    summary="Update a simulation.",
    tags=["simulations"],
    response_model=APISimulation,
    name="simulation-put",
)(views.simulation_put)
router.delete(
    "/simulation/{simulation_id}/",
    summary="Delete a simulation.",
    tags=["simulations"],
    response_model=APISimulation,
    name="simulation-delete",
)(views.simulation_delete)

This should be pretty straightforward. summary and tags are used purely for the documentation site, they have no other functional purpose. name is used for getting a route’s URL by name, and response_model is used for validation of the response and documentation.

Onto the views!

The views

The views are more straightforward than you’d expect. We have the GET/POST on the collection and GET/POST/PUT/DELETE on the object, but the heavy lifting is done by the from_model / from_api methods.

Also, notice how authentication is done with the Depends(get_user) dependency, making it mandatory for each endpoint, and the simulation parameter is an actual Simulation model instance, not an ID. The model instance also gets validated to make sure it actually belongs to the user. We’ll see exactly how in a later section.

main/views.py:

from fastapi import Body
from fastapi import Depends

from .models import APISimulation
from .models import APISimulations
from .models import APIUser
from .models import DamnFastAPISimulation
from .models import Simulation
from .models import User
from .utils import get_simulation
from .utils import get_user


def simulations_get(user: User = Depends(get_user)) -> APISimulations:
    """
    Return a list of available simulations.
    """
    simulations = user.simulation_set.all()
    api_simulations = [APISimulation.from_model(s) for s in simulations]
    return APISimulations(items=api_simulations)


def simulation_post(
    simulation: DamnFastAPISimulation,
    user: User = Depends(get_user)
) -> APISimulation:
    """
    Create a new simulation.
    """
    s = Simulation.from_api(user, simulation)
    s.save()
    return APISimulation.from_model(s)


def simulation_get(
    simulation: Simulation = Depends(get_simulation),
    user: User = Depends(get_user)
) -> APISimulation:
    """
    Return a specific simulation object.
    """
    return APISimulation.from_model(simulation)


def simulation_put(
    simulation: Simulation = Depends(get_simulation),
    sim_body: DamnFastAPISimulation = Body(...),
    user: User = Depends(get_user),
) -> APISimulation:
    """
    Update a simulation.
    """
    simulation.update_from_api(sim_body)
    simulation.save()
    return APISimulation.from_model(simulation)


def simulation_delete(
    simulation: Simulation = Depends(get_simulation),
    user: User = Depends(get_user)
) -> APISimulation:
    """
    Delete a simulation.
    """
    d = APISimulation.from_model(simulation)
    simulation.delete()
    return d

Utils

In this file we define methods to authenticate users and retrieve objects from the database by their ID while still making sure they belong to the authenticating user. get_object is a generic function to avoid repeating ourselves for every one of our models.

main/utils.py:

from typing import Type

from django.db import models
from fastapi import Header
from fastapi import HTTPException
from fastapi import Path

from main.models import Simulation
from main.models import User

# This is to avoid typing it once every object.
API_KEY_HEADER = Header(..., description="The user's API key.")


def get_user(x_api_key: str = API_KEY_HEADER) -> User:
    """
    Retrieve the user by the given API key.
    """
    u = User.objects.filter(api_key=x_api_key).first()
    if not u:
        raise HTTPException(status_code=400, detail="X-API-Key header invalid.")
    return u


def get_object(
    model_class: Type[models.Model],
    id: str,
    x_api_key: str
) -> models.Model:
    """
    Retrieve an object for the given user by id.

    This is a generic helper method that will retrieve any object by ID,
    ensuring that the user owns it.
    """
    user = get_user(x_api_key)
    instance = model_class.objects.filter(user=user, pk=id).first()
    if not instance:
        raise HTTPException(status_code=404, detail="Object not found.")
    return instance


def get_simulation(
    simulation_id: str = Path(..., description="The ID of the simulation."),
    x_api_key: str = Header(...),
):
    """
    Retrieve the user's simulation from the given simulation ID.
    """
    return get_object(Simulation, simulation_id, x_api_key)

Tests

The tests are very close to what you’re already used to from Django. We’ll be using Django’s testing harness/runner, but we can test FastAPI’s views by using FastAPI’s client . We’ll also be using Django’s/unittest’s assert functions, as I find them more convenient, but you can use anything you’d use with Django for those.

As I mentioned earlier, the plain TestCase didn’t work for me, so I had to use the TransactionTestCase .

main/tests.py:

# We use this as TestCase doesn't work.
from django.test import TransactionTestCase
from fastapi.testclient import TestClient

from .models import User
from goatfish.wsgi import app


# A convenient helper for getting URL paths by name.
reverse = app.router.url_path_for


class SmokeTests(TransactionTestCase):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # Warning: Naming this `self.client` leads Django to overwrite it
        # with its own test client.
        self.c = TestClient(app)

    def setUp(self):
        self.user = User.objects.create(username="user", api_key="mykey")
        self.headers = {"X-API-Key": self.user.api_key}

    def test_read_main(self):
        response = self.c.get(reverse("simulations-get"), headers=self.headers)
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.json(), {"username": "user"})

That’s it!

Epilogue

I hope that was clear enough, there is a bit of confusion when trying to figure out which part is served by which library, but I’m confident that you’ll be able to figure it all out without much difficulty. Just keep in mind that FastAPI does everything view-specific and Django does everything else.

I was legitimately surprised at how well the two libraries worked together, and how minimal the amounts of hackery involved were. I don’t really see myself using anything else for APIs in the future, as the convenience of using both libraries is hard to beat. I hope that asynchronicity will work when Django releases async support, which would complete this integration and give us the best of both worlds.

Tweet or toot at me, or email me directly.


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

UML风格

UML风格

布格勒 / 袁峰 / 2008-12 / 35.00元

《UML风格(第2版)(英汉对照)》给出了一系列有效提高团队生产效率的编程风格的原则,描述了创建简洁、易于理解的UML图的标准和指南,涉及类图、定时图、用例图、组合结构图、顺序图、交互概览图、活动图、对象图、状态图、包图、通信图、部署图和组件图等内容。著名UML专家Scott W.Ambler描述了创建UML图的标准和指南,以帮助建模人员创建简明而易于理解的UML 图形。 《UML风格(第2......一起来看看 《UML风格》 这本书的介绍吧!

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

MD5 加密
MD5 加密

MD5 加密工具

SHA 加密
SHA 加密

SHA 加密工具