Last active
October 10, 2024 13:40
-
-
Save petrilli/81511edd88db935d17af0ec271ed950b to your computer and use it in GitHub Desktop.
Example combination of FastAPI and Pydantic with aiosql and aiosqlite
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# -*- coding: utf-8 -*- | |
"""Example combination of FastAPI and Pydantic with aiosql and aiosqlite. | |
This module demonstrates the minimum viable integration approach for putting | |
together a few components: | |
- FastAPI. This provides a very high-performance and type-driving approach to | |
building APIs in Python | |
- Pydantic. A powerful data validation library. | |
- aiosql. Inspired by annosql and originally Clojure's yeSql, a way to programatically, | |
and safely, drive SQL databases with their own DSL: SQL. | |
- aiosqlite. An asyncio compatible version of the sqlite database adapter. | |
What's Shown: | |
1. How to load queries for aiosql to use. | |
2. Creating a dependency in FastAPI that can be used to inject the database connection | |
wherever you need it in an API endpoint. | |
3. Using differeny Pydantic models for the database and the API, and allowing FastAPI | |
to automaticaly convert between them. | |
How to Use: | |
To use this, you will need to create a sqlite database called `testdb.sqlite`. You can | |
do this with the following script run in the same directory as this module: | |
$ sqlite3 testdb.sqlite3 <<EOM | |
CREATE TABLE greetings ( | |
greeting_id INT PRIMARY KEY, | |
greeting TEXT NOT NULL, | |
note TEXT | |
); | |
INSERT INTO greetings (greeting, note) VALUES ("Test greeting", "Sample Note"); | |
EOM | |
Once that's created, you'll also need to install the correct dependencies. I use | |
Pipenv, but you can use anything that manages a virtualenv. Just use a virtualenv. | |
You'll need to install the following packages: | |
fastapi | |
aiosql | |
aiosqlite | |
uvicorn | |
Once those are installed, you can run the script using something like this: | |
$ uvicorn main:app | |
Enjoy. | |
""" | |
from typing import List, Union | |
import aiosql | |
import aiosqlite | |
from fastapi import Depends, FastAPI | |
from pydantic import BaseModel | |
DB_FILE = "testdb.sqlite3" | |
SQL_QUERIES = """ | |
-- name: get-all-greetings | |
-- Get all the greetings from the database | |
SELECT greeting_id, greeting, note FROM greetings; | |
""" | |
app = FastAPI() | |
queries = aiosql.from_str(SQL_QUERIES, "aiosqlite") | |
############################################################################# | |
# Pydantic Models | |
# This is used to provide strong typing on the data going into and out of the | |
# application. Note that normally, these would be stored in their own file, | |
# but to keep this simple, it's being kept in the same file. | |
class Greeting(BaseModel): | |
"""A greeting that can be used to the public. | |
Attributes: | |
greeting_id (int): The primary auto-generated key. | |
greeting (str): The human-readable greeting. | |
""" | |
greeting_id: int | |
greeting: str | |
class GreetingDB(Greeting): | |
"""The database representation of the greeting. | |
Attributes: | |
note (str): An internal-only notation for the greeting. | |
""" | |
note: Union[str, None] | |
############################################################################# | |
# Dependency | |
# Provide a re-usable way to inject a database connection into the individual | |
# endpoints. | |
async def get_db(): | |
"""Return a database connection for use as a dependency. | |
This connection has the Row row factory automatically attached.""" | |
db = await aiosqlite.connect(DB_FILE) | |
# Provide a smarter version of the results. This keeps from having to unpack | |
# tuples manually. | |
db.row_factory = aiosqlite.Row | |
try: | |
yield db | |
finally: | |
await db.close() | |
############################################################################# | |
# Database Queries | |
# Leverage the `aiosql` library to query the database. This is a thinner | |
# approach than an ORM, and also allows you to be very specific about the | |
# query witho fewer surprises. | |
async def get_greetings(conn: aiosqlite.Connection) -> List[GreetingDB]: | |
"""Get all the greetings in the database.""" | |
greetings = await queries.get_all_greetings(conn) | |
# parse_obj allows us to use the Row returned to populate the model by | |
# name, rather than having to unpack the tuples. | |
return [GreetingDB.parse_obj(greeting) for greeting in greetings] | |
############################################################################# | |
# FastAPI Endpoints | |
# Finally, we create a FastAPI endpoint to be called. Note the response model | |
# that is used is different from the one returned by the database call. FastAPI | |
# will automatically convert them. | |
@app.get("/", response_model=List[Greeting]) | |
async def test_get(db_connection: aiosqlite.Connection = Depends(get_db)): | |
greetings = await get_greetings(db_connection) | |
return greetings |
Right now you can just:
async def get_greetings(conn: aiosqlite.Connection) -> List[GreetingDB]:
"""Get all the greetings in the database."""
greetings = await queries.get_all_greetings(conn)
# parse_obj allows us to use the Row returned to populate the model by
# name, rather than having to unpack the tuples.
return [GreetingDB(**dict(greeting)) for greeting in greetings]
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
The call of aiosql ( your custom sql select statement)
function
returns the Record of the type that is provided by your database connector. In case asyncpg it is asyncpg.Record that can be converted to dict by dict(record.items()) method and after that can be converted to pydantic model by the call of the init method: GreetingDB(**dict(record.items())). In case of FastAPI, the result of aiosql."function" can be directly returned by endpoint and further converted and validated by the model specified inresponse_model
param of path function of endpoint.