Building Python applications with Postgres and FastAPI

In this guide, we will walk through the process of creating a Python application using FastAPI together with Postgres using Neurelo.

This guide can be easily adapted to other web frameworks, such as Flask.

Our goal is to create a "Fun Facts" application. This app will display a random animal fact to the user upon each site refresh. The facts, stored in a database on a remote server, will be managed through Python, with FastAPI serving as our chosen web framework.

Central to our application is Neurelo which will significantly simplify the database interactions by leveraging Neurelo’s auto-generated API endpoints.

Before we dive into building this application, let us lay the groundwork. This involves creating a new project, setting up a data source, and formulating a database schema suitable for our app. For this, we will use Postgres.

The first step is to create a new project in Neurelo. Following the project creation, add a data source to this project.

Next, a database schema must be defined. For the sake of simplicity, our schema will include the following fields: id, animal_name, fact, and created_at. With this, we can create our schema as:

{
  "objects": {
    "animal_facts": {
      "properties": {
        "animal_name": {
          "type": "string",
          "sourceType": [
            "VarChar",
            255
          ]
        },
        "created_at": {
          "type": "string",
          "format": "date-time",
          "sourceType": [
            "Timestamptz",
            6
          ],
          "default": {
            "function": "now"
          },
          "nullable": true
        },
        "fact": {
          "type": "string"
        },
        "id": {
          "type": "integer",
          "default": {
            "function": "autoincrement"
          },
          "identifier": true
        }
      }
    }
  }
}

For more information on Neurelo’s Schema Language, see the Neurelo Schema Language (NSL) documentation.

Once your schema is ready, commit it and then create an environment attached to this commit.

After the environment is set up, applying a migration to your data source is straightforward. Just follow these steps to migrate your schema.

Once migrations are completed, your database will be updated to match your schema in Neurelo. At this point, you are ready to use Neurelo's auto-generated API endpoints for your project. You can view these in the environment’s APIs tab.

We provide an API Playground to easily run and test these auto-generated APIs!

It's now time to develop the application code in Python. We will begin by creating a class named Neurelo to facilitate interaction with Neurelo’s API endpoints, and consequently, our database. The code for this is straightforward:

# neurelo.py
import os
import argparse
import json

import requests
from dotenv import load_dotenv


class Neurelo:
    def __init__(self):
        load_dotenv()
        self.headers = {
            "X-API-KEY": os.getenv("X_API_KEY"),
            "Content-Type": "application/json",
        }
        # For an object named `animal_facts`, the following GET MANY endpoint is auto-generated.
        # Details on these endpoints can be found in the API reference under the APIs tab.      
        self.api_url = "https://us-west-2.aws.neurelo.com/rest/animal_facts/"

    def store(self):
        """
        Stores the initial animal facts in the database.
        The 'facts.json' file should follow this format:
        [ {"id": 1, "fact": "Fact 1", "animal_name": "Animal 1"}, ..... ]
        """
        with open("./facts.json") as f:
            facts = json.load(f)
        response = requests.post(self.api_url, headers=self.headers, json=facts)
        print(f"Stored: {response}")

    def fetch(self):
        """
        Fetches all the animal facts from the database
        """
        response = requests.get(self.api_url, headers=self.headers)
        return response.json()["data"]

    def delete_all(self):
        """
        Deletes all the animal facts whose id is greater than or equal to 1
        """
        req = self.api_url + "?filter={%22id%22%3A%20{%22gte%22%3A%201}}"
        response = requests.delete(req, headers=self.headers)
        print(f"Deleted: {response}")

if __name__ == "__main__":
    neurelo = Neurelo()

    parser = argparse.ArgumentParser(
        prog="Animal Fun Facts", description="Shows animal-based fun facts"
    )
    parser.add_argument(
        "-c",
        "--cmd",
        default="f",
        help="f/s/d (fetches/stores/deletes) all data using Neurelo's API.",
    )
    args = parser.parse_args()
    actions = {"f": neurelo.fetch, "s": neurelo.store, "d": neurelo.delete_all}
    result = actions.get(args.cmd, lambda: "Wrong cmd. It should be one of (f/s/d)")()
    print(result)

Before running this code, it is essential to generate an API Key in the environments tab. Subsequently, store this key in an .env file as X_API_KEY="<KEY>". Ensure the .env file is located in the same directory as your Python script.

And that's it! We have now established an interface with our database via Neurelo. This setup helps us to integrate this class with our preferred web framework to build our application. As previously mentioned, we will be using FastAPI to illustrate this process,

import random

from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
from neurelo import Neurelo

app = FastAPI()
api = Neurelo()

templates = Jinja2Templates(directory="templates")


@app.get("/random-fact/", response_class=HTMLResponse)
async def read_random_fact(request: Request):
    random_fact = random.choice(api.fetch())
    if random_fact is None:
        raise HTTPException(status_code=404, detail="No facts available")
    return templates.TemplateResponse(
        "fact_page.html",
        {
            "request": request,
            "fact": random_fact["fact"],
            "animal": random_fact["animal_name"],
        },
    )

The server mentioned above includes an endpoint (/random-fact/) that retrieves a random fact from the animal_facts table via the Neurelo class, returning an HTML response rendered with a Jinja2 template. This code is assumed to be in a main.py file. The template, fact_page.html, can be straightforward:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Random Animal Fact</title>
</head>
<body>
    <div>
        <h2>Random Animal Fact</h2>
        <p><strong>Animal:</strong> {{ animal }}</p>
        <p><strong>Fact:</strong> {{ fact }}</p>
    </div>
</body>
</html>

This template uses Jinja2 syntax to display the random fact. That's all there is to it! You can launch your FastAPI application with uvicorn main:app --reload and go to http://127.0.0.1:8000/random-fact/ to see your application powered by Neurelo in action.

As can be imagined, this code can be easily adapted to other web frameworks, such as Flask.

Now, imagine your database holds thousands of random animal facts, and you wish to avoid fetching all to select one on the server side. While you can use filters and other capabilities in Neurelo's auto-generated APIs for such queries, we will use Neurelo’s Custom Queries feature to create custom endpoints as an example here.

To begin, go to the Definitions view and click on the Custom Queries tab.

Next, create a new custom query endpoint as shown below:

You can use Neurelo's AI assistant for swift construction of a custom query. For this scenario, the prompt we will use is "fetch a random animal fact".

And there you have it! The endpoint and its corresponding query are ready in just a few seconds. Our custom query testing playground is also available for convenient query testing and iterations, if needed. The remaining step is to commit this new custom query definition, update our environment to incorporate this change, and then proceed with using the new endpoint.

You can read more on Custom Query Endpoints and AI-Assisted Query Generation in our documentation. Do not forget to update the self.api_url in the Neurelo class with your new custom endpoint, which, in this example, is /custom/random_animal.

Last updated