dayDreams ++

FastAPI + Deta = ⚡️

Posted on Jul 7, 2020 | 10 minutes

FastAPI is a a Python Framework for building RESTful APIs. It has all the simplicity of Python with a added advantages of Async⚡️, automatic Schema Generation and OpenAPI and Python Types✨(with Pydantic). FastAPI is relatively a new Project and is gaining quite a good traction in the Dev world. It has got nearly 16k stars on GitHub and about 160 contributors. The main plus side of FastAPI is the documentation which is Top Notch.

Deta is a company that is building a cloud platform which is developer friendly and to reduce the hassle from idea to app.

“Deta is building a cloud for the developers with less build “bells and whistles”, only what’s needed to get the job done – a Micro Cloud."

Personally what attracted me to deta was the ease to implement. I was having a hard time to connect FastAPI with SQLalchemy since I am a noob. Then Deta entered or at least then I came to know about it.

I was skeptical at first, but the simplicity of working with Deta amazed me and their Slack community is ✨. The creators are almost active everytime and they will help you in anyway possible to overcome an issue.

What we’ll build

We’re going to build a Personal Mailing List API with

  • FastAPI for the API and Logic
  • Deta.sh as our DB
  • Deploy it on Deta Micros

We’re only going to build the API, you can use Mailgun or SendGrid to send the emails. Our API will consist of 4 Operations.

  • Add a new Person to our List
  • Get all the emails from our list
  • Update the email of a specific user in our List
  • Delete a Subscriber/User from our List

Setting up FastAPI and Deta

Hope you have Python 3.6+ interpreter on your machine. If you don’t make sure to download it and install and verify by running python in your terminal

$ python3
Python 3.7.6 (default, Dec 30 2019, 19:38:26)
[Clang 11.0.0 (clang-1100.0.33.16)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>>

The above output is on my machine and it will change from machine to machine.

Next thing is to have a folder and a virtual environment, for that we could use either Pipenv or venv. Pipenv is a package for handling and managing Python envs automatically, you can install pipenv with,

$ pip install --user pipenv

Once we installed Pipenv to set up our environment follow the commands to install

  • FastAPI
  • Uvicorn - The ASGI Server
  • Deta - Python Wrapper for the Deta Bases
$ mkdir mailing-list
$ cd mailing-list
$ pipenv shell #it will make the virtual environment
$ pipenv install fastapi uvicorn deta

If we’re using venv, then for creating a venv and installing the packages, follow

$ mkdir mailing-list
$ cd mailing-list
$ python3 -m venv venv
$ source venv/bin/activate
$ pip install fastapi uvicorn deta

These are for Unix like systems and on Windows to activate a Virtualenv follow,

venv\Scripts\activate

Once we have set up our Dev Environment. Let’s move on to building the Mailing List.

For Deta, create an Account in Deta and create a new Project and get the Project Key. Save the Project key since it will be shown once. You can save it as an environment variable by executing the below code in Unix like(Linux/Mac).

export DETA_PROJECT_KEY = <project key>

and on Windows, use

set DETA_PROJECT_KEY = <project key>

We’ll also save the key in a .env file. This env file s important for the deployment process. For that we need to create a file called .env and save the Project Key like

DETA_PROJECT_KEY = <project key>

That’s it. Lets move on to the code


The Code

Let’s start by making a simple REST API endpoint with FastAPI on a file called main.py

# main.py
from fastapi import FastAPI

app = FastAPI()

@app.get("/")
async def helloworld():
    return "Hello World"

This is just a hello world app with FastAPI and for running this, we’ll need uvicorn. Execute the following in the terminal/cmd,

$ uvicorn main:app --reload
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO:     Started reloader process [13050] using statreload
INFO:     Started server process [13052]
INFO:     Waiting for application startup.
INFO:     Application startup complete.

Our API will be actve the port 8000. FastAPI has inbuilt support for OpenAPI, Swagger and Redoc. To view the docs for our App, just go to localhost:8000/docs. To check if our endpoint works, just execute

$ curl http://127.0.0.1:8000/
"Hello World"

So now, let’s move on to our main parts

New Subscriber

We’ll be using the docs by Deta for all references.

We need to create a new endpoint for a someone to subscribe to our Mailing List. So we’ll just need the Person’s Name and Email to be in our DB.

Take care that in some countries its illegal to send emails to people without consent. We’ll make a delete endpoint for them to delete themselves from the list

Let’s get on to the code for adding a new subscriber to our list. The Python code without the use of a DB or simply the code with FastAPI.

# main.py

from fastapi import FastAPI
from pydantic import BaseModel, NameEmail

app = FastAPI()

class Subscriber(BaseModel):
    full_name: str
    email: NameEmail

@app.post("/subscribe")
async def newUser(sub: Subscriber):
    name = sub.full_name
    email = sub.email

    # you could also return sub.json() or sub.dict()
    return {
        "full_name": name,
        "email": email
    }

We’re using Pydantic BaseModel class to inherit our Subscriber class. Then we make the subscriber class a dict to fetch data from the request’s body. NameEmail will help us get the name part of the email.

The output is a NameEmail object which has two properties: name and email. For [email protected], name property would be “fred.bloggs”.

This snippet will return the things that we have provided to it. I have entered the data by going to localhost:8000/docs if you have your server running. Our Output if we provide athul for name and [email protected] for email will be

{
  "name": "athul",
  "email": "[email protected]",
}

Now let’s connect it with the database, Deta Base in this case. We’ll be using Python’s os package to get the Project Key. We have save the project key to the env vars and restart the server for it to work.

# main.py

import os
from fastapi import FastAPI
from pydantic import BaseModel, NameEmail
from deta import Deta

deta = Deta(os.getenv("DETA_PROJECT_KEY"))
db = deta.Base("mydb")  # this can be anything related to your use case

app = FastAPI()

class Subscriber(BaseModel):
    full_name: str
    email: NameEmail

@app.post("/subscribe")
async def newUser(sub:Subscriber):
    subscriber = db.put({
        "full_name": sub.full_name,
        "email": sub.email.email,
        "key": sub.email.name
    })

    return subscriber

Here we have imported the deta library and initialized it with a db name. We’re specifying a key for the data. This key will be the name part of the email since most times it will be unique. If we don’t specify a key, deta will generate a unique key for the record.

We’re using Deta’s PUT method(/function) to insert our data into our DetaBase. The PUT method will generate a unique key for our data and will be stored if we don’t specify a key. The output if we add ourselves a new subscriber with provide John Doe for name and [email protected] for email will be,

{
  "email": "[email protected]",
  "key": "john",
  "full_name": "John Doe"
}

So we have made an endpoint for people to subscribe to our Mailing List 🎉.
Next we’ll make an endpoint for people to update a theirs info like their Name or Email.

Update a Subscriber

We’ll create a new UpdateSubscriber class for updating a Subscriber’s Name or Email. We’ll use the get and update methods of deta to fetch and update the data. We’ll also raise an error if the record is not found in the base

# main.py

from typing import Optional
from fastapi import NameEmail,HTTPException

class UpdateSub(BaseModel):
    full_name: Optional[str] = None
    email: Optional[NameEmail] = None

@app.patch("/subscriber/{email}")
async def update_sub(upsub: UpdateSub, email: NameEmail):
    sub = db.get(email.name)
    if sub is None:
        raise HTTPException(404,"User Not Subscribed")

    updates = {}
    if upsub.full_name:
        updates["full_name"] = upsub.full_name

    if upsub.email:
        updates["email"] = upsub.email.email

    db.update(updates, key=sub["key"])

    return {"msg": "User Updated"}

This endpoint will update the email or full_name of a subscriber but the caveat here is that the key cannot be changed even if the email is changed. Let’s make a new user with the name John Reese(Person of Interest) with an initial email [email protected]. Now let’s update his email using this endpoint, for that our request URL will be as follows,

http://localhost:8080/subscriber/reese%40example.com You can see that after the /subscriber in the url, we are passing the original email and this is called a Path Parameter. We use the path paramter and get the name part of it and use it as the Key. For updating the email, we take the new email and update it in our Deta base with the update command. The updates should be in the form of a Python dict. If the operation done is correct, then we’ll get a None type in return

Get all Subscribers

This endpoint will send a GET HTTP method for fetching all the data from our Deta-Base. We’ll use the fetch function/method of deta to retreive all the data from our base. The fetch function will return a generator type, so we’ll be using the next() function to get all the data as a List. The endpoint will be like

@app.get("/subscribers")
async def get_all_subs():
    return {"subscribers": next(db.fetch())}

The output of the above request will be

{
  "subscribers": [
    {
      "email": "[email protected]",
      "key": "athul",
      "name": "Athuk"
    },
    {
      "email": "[email protected]",
      "full_name": "John Doe",
      "key": "john"
    },
    {
      "email": "[email protected]",
      "full_name": "John Reese",
      "key": "reese"
    }
  ]
}

Now we have setup a Create, Update and Read endpoints. Next we’ll look into the Delete operation.

Delete Subscribers

To delete a subscriber, we’ll take in the full_name as the input to find the subscriber and if we there is more than one record with the same full_name, we’ll fetch the data with email. Then we’ll use the delete method to delete the record by getting the key from our records.

# main.py

class DelSub(BaseModel):
    email: Optional[NameEmail]
    full_name: str

@app.delete("/subscribers.del")
async def unsubscribe(unsub: DelSub):
    data = delsub.dict()
    name = data.get('full_name')
    email = data.get('email')
    sub = next(db.fetch({'full_name':name}))
    if len(sub) > 1 or len(sub) == 0:
        sub = next(db.fetch({'email':email.email}))
        if len(sub)==0:
            raise HTTPException(400,"email not found")
    db.delete(sub[0]['key'])
    return {"msg":"Record Deleted"}

The response if a record is deleted will be a Python None type so we respond with a message,

{"msg":"Record Deleted"}

So we have made a CRUD API for a personal mailing list. Now let’s deploy it.

Deploy our API

For Deploying our API, we’ll need Deta Micros.

Deta Micros(ervers) are a leightweight but scalable cloud runtime tied to an HTTP endpoint. They are meant to get your apps up and running blazingly fast. Focus on writing your code and Deta will take care of everything else.

You need to have Deta Micros' CLI installed in your system. You can install by following the instructions in this link. Once you’ve installed, we need to login to Deta via the CLI, for that execute the following command

deta login

It’ll open a new browser window and will authenticate the CLI. Next we need to start a new micro. We’ll also need a requirements.txt for managing our dependencies, for that either you can copy the response for the command pip freeze and paste it in a file called requirements.txt or on *nix systems, you can just execute

pip freeze > requirements.txt

This will paste the output of pip freeze to requirements.txt. Once we have done we need to deploy it to deta using the command. Once we’ve done this let’s create a new micro, for that we can use the deta new command

$ deta new
{
        "name": "mailing-list",
        "runtime": "python3.7",
        "endpoint": "<url>",
        "visor": "enabled",
        "http_auth": "enabled"
}

It will automatically detect that we’ll need a Python runtime and will deploy our code and give us a unique link for our Micro.

Yay we have deployed the API, for the details of the micro, just execute the following for details of the micro.

$ deta details
{
        "name": "mailing-list",
        "runtime": "python3.7",
        "endpoint": "<url>",
        "visor": "enabled",
        "http_auth": "enabled"
}

We need to update the environment variable to set our project_key, execute the deta update command like this

$ deta update -e .env
Updating environment variables...
Successfully updated micro's environment variables

You can disable authentication for the API to be publically available, just execute deta auth disable.

$ deta auth disable
Successfully disabled http auth

If we go to our Micro’s url we’ll see a Hello World response which we wrote as the / endpoint. We can see the OpenAPI spec and the Swagger Documentation, if we go to the /docs url, we’ll see a page like this

FastAPI Docs


Yay we have created an API with FastAPI and Deta and Deployed it to the Web.

Yay Gif

That leads to the end of this post, here is the whole source code

import os
from typing import Optional
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, NameEmail
from deta import Deta

deta = Deta(os.getenv("DETA_PROJECT_KEY"))
db = deta.Base('mailing-list')

app = FastAPI()

class Subscriber(BaseModel):
    full_name: str
    email: NameEmail

@app.post("/subscribe")
async def newUser(sub:Subscriber):
    data = sub.dict()
    name = data.get("full_name")
    email = data.get("email")
    subscriber= db.put({
        "full_name":name,
        "email":email.email
    },key=email.name)
    return subscriber

@app.get("/subscribers")
async def getAllSubs():
    return {"subscribers":next(db.fetch())}

class UpdateSub(BaseModel):
    full_name: Optional[str] = None
    email: Optional[NameEmail] = None

@app.patch("/subscriber/{email}")
async def updateSub(subscriber:UpdateSub,email:NameEmail):
    data = subscriber.dict()
    new_name = data.get("full_name")
    new_email = data.get("email")
    sub = db.get(email.name)
    if sub is None:
        raise HTTPException(400,"User Not Subscribed")
    if new_email is None:
        update = {"full_name":new_name}
    if new_name is None:
        update = {"email":new_email.email}
    db.update(updates=update,key=sub['key'])
    return {"msg":"User Updated"}
class DelSub(BaseModel):
    email:Optional[NameEmail]
    full_name:str

@app.delete("/subscribers.del")
async def deleteSub(delsub:DelSub):
    data=delsub.dict()
    name = data.get('full_name')
    email = data.get('email')
    sub = next(db.fetch({'full_name':name}))
    if len(sub) > 1 or len(sub) == 0:
        sub = next(db.fetch({'email':email.email}))
        if len(sub)==0:
            raise HTTPException(400,"email not found")
    db.delete(sub[0]['key'])
    return {"msg":"Record Deleted"} 

@app.get("/")
async def helloworld():
    return "Hello World"

Conclusion

This is just a demo for getting started with FastAPI with Deta.sh. You can change the key for your preference or use the key generated automatically by Deta. You can also use the email address as your key but you may need to switch the @ character with something else like a - or _ for its place. If you have any queries or doubts or feedback, you can use the comments section for the same and do let me if this has helped you. Go forth and build awesome stuff ⚡️. Thank You ❤️