Posted on 23rd October, 2022
FASTApi is a light weight and fast back-end framework written in Python used to create professional APIs quickly. It has support for automatic API documentation and follows Python type hinting.
In this post we would be covering slightly complex file upload feature implementation in FAST Api with database connection and models. We would be saving the file in the same project folder and would store the file path in the database. We would start the proceedings by creating a virtual environment. We would then add FastAPI and relevant packages like uvicorn (to use as a server) and create a hello world api route. All these things would be done in Windows operating system.
- python -m venv venv
- cd venv/Scripts
- activate.bat
Create a main.py file and write the following code.
from fastapi import FastAPI, Request
import uvicorn
app = FastAPI(title="Test Fast API",
docs_url="/docs",
version="0.0.1")
@app.get("/")
async def main(request: Request):
return {
'message': 'Hello World API'
}
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)
You can now try hitting port 8000 on your system after running main.py file. It should print the 'Hello World' message in JSON format. You can also try visiting /docs end-point which would show you the automated API docs based on Swagger specifications. It is one of the coolest FastAPI features. Let us install some more packages and work on database settings.
pip install psycopg2
pip install alembic
pip install SQLAlchemy
We would be using Postgres and PgAdmin 14 on Windows platform for demonstration. Make sure you have Postgres installed along with GUI supporting tool - PgAdmin. SQLAlchemy library is used to perform database queries, migrations that is changes to the table are taken care of by 'Alembic' package. Next step would be to link our app with Alembic and create a migrations folder.
alembic init alembic
This would create a migrations folder named 'alembic' and a config file 'alembic.ini' inside your project directory. Purpose of this config file is to let the app know the location of the migrations. We would be avoid making any advanced changes in this file and leave it as it is. Moving forward, let us now create a separate config file which would hold database settings for our app.
DATABASE_USERNAME = 'postgres'
DATABASE_PASSWORD = 'pass12345'
DATABASE_HOST = '127.0.0.1'
DATABASE_NAME = 'fast-book'
We would need to open PgAdmin and create database named 'fast-book'. Of course it could be any name you want to give, just check database name, user and password settings match with the one you created using PgAdmin. We would create a 'db.py' file and use these settings to establish database connection in our app.
from sqlalchemy import create_engine, MetaData
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
import config
DATABASE_USERNAME = config.DATABASE_USERNAME
DATABASE_PASSWORD = config.DATABASE_PASSWORD
DATABASE_HOST = config.DATABASE_HOST
DATABASE_NAME = config.DATABASE_NAME
SQLALCHEMY_DATABASE_URL = f"postgresql://{DATABASE_USERNAME}:{DATABASE_PASSWORD}@{DATABASE_HOST}/{DATABASE_NAME}"
engine = create_engine(SQLALCHEMY_DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
metadata = MetaData()
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
We have created a database engine using the imported config settings. Then, we created a session and meta data object. Meta data object would help in keeping track of changes in the database through migrations. We would import these settings in other files, create a session, perform database operations and then close the session. Next, we would work on database models and fields. Conventionally, we put our model files inside a folder named 'models'. So let's create the folder and put 'photo.py' file inside it. We are naming our model as 'Photo' here which would have three fields, automatically generated ID, location and name.
from sqlalchemy import Column, String, Integer
from db import Base
class Photo(Base):
__tablename__ = "photos"
id = Column(Integer, primary_key=True, autoincrement=True)
location = Column(String(50))
name = Column(String(50))
Next step is to edit the env file inside the 'alembic' folder. This is done to keep track of automatic changes in the database and generate tables.
from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool
import config as dbConfig
from alembic import context
from db import Base
from models.photo import Photo
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
target_metadata = Base.metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def get_url():
db_user = dbConfig.DATABASE_USERNAME
db_password = dbConfig.DATABASE_PASSWORD
db_host = dbConfig.DATABASE_HOST
db_name = dbConfig.DATABASE_NAME
return f"postgresql://{db_user}:{db_password}@{db_host}/{db_name}"
def run_migrations_offline():
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = get_url()
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
configuration = config.get_section(config.config_ini_section)
configuration["sqlalchemy.url"] = get_url()
connectable = engine_from_config(
configuration, prefix="sqlalchemy.", poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(
connection=connection, target_metadata=target_metadata
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
Most of the settings in this file would be been generated automatically when we linked our application with Alembic. However, we have tweaked certain settings. We are importing database settings from the config file which we created earlier. We have used target_metadata to include support for automatic database table creation based on the structure of the model files. We have also included photo model in this file, this would help detecting any changes you do in future once initial database tables have been setup. We would autogenerate migrations now by typing the following command in the terminal.
alembic revision --autogenerate -m "Added photo table"
This would generate migrations and record them in the database. But, note that actual 'photos' table is not yet created as we haven't applied the created migrations yet. You would be able to see the migrations table in the database. This also creates a migrations file inside the versions directory in alembic folder. For each change you make in a database, an additional migration file is created to keep track of the changes done in database. This post is assuming you have some knowledge about database changes and migration files.
alembic upgrade head
The above command would finally create the 'photos' table whose structure would be based on the 'photo.py' file we defined earlier. You can verify this by opening PgAdmin in your browser and checking the tables for 'fast-book' (or whatever name you gave to the database) database. We are now ready to implement file upload feature in our application. We would create a separate folder named 'services'. This would have a 'photo.py' file which would contain functions to handle creation of database entries.
from fastapi import HTTPException, status
from typing import List
from models.photo import Photo
async def create_new_photo(name, location, database) -> Photo:
new_photo = Photo(location=location, name=name)
database.add(new_photo)
database.commit()
database.refresh(new_photo)
return new_photo
I've written a function to handle POST requests to the end-point which we would create later and use for file-upload. As you can see above FASTApi uses return types hint supported in Python versions 3.5 and above. A post request would return one single object which was created with the request. We would now create a folder named 'media' which would house all our media files. We would then also connect this media to our site url so that we can see navigate to the contents of this folder through browser. We would import these functions inside 'services.py' file into 'main.py' file.
from fastapi import FastAPI, Request, Form, UploadFile, File, Depends
from services.photo import create_new_photo, get_all_photos
import uvicorn
from sqlalchemy.orm import Session
from starlette.staticfiles import StaticFiles
from models.photo import Photo
import db
app = FastAPI(title="Test Fast API",
docs_url="/docs",
version="0.0.1")
@app.get("/")
async def main(request: Request):
return {
'message': 'Hello World API'
}
@app.post("/photo")
async def create_upload_file(name: str = Form(...), file: UploadFile = File(...), database: Session = Depends(db.get_db)):
if not file:
return {"message": "No upload file sent"}
else:
file_location = f"media/{file.filename}"
with open(file_location, "wb+") as file_object:
file_object.write(file.file.read())
new_photo = await create_new_photo(name, file_location, database)
return new_photo
@app.get('/photo')
async def get_all_photos(database: Session = Depends(db.get_db)):
photos = database.query(Photo).all()
return photos
app.mount("/media", StaticFiles(directory="media"), name="media")
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)
I have modified our 'main.py' file. This is the finalized version which we would make useful in a moment by decoding the changes done line-by-line. First, we need to install a package called 'python-multipart'. This supports file upload in FastAPI as the classes defined for file upload in FastAPI depend on this package. So, let's install this package.
pip install python-multipart
We have created a new end-point called '/photo' in our app. This would accept GET and POST requests for files through asynchronous functions. Hitting the API with a post request on this url would upload the given file inside the 'media' folder. After this, it would pass the filename in the 'create_new_photo' function which we defined inside the 'services' folder.
from starlette.staticfiles import StaticFiles
app.mount("/media", StaticFiles(directory="media"), name="media")
I'd take a moment to explain the purpose of the above two lines we added. This would make sure the files under the 'media' folder are accessible through the url. If we skip this, even though file upload would work but we would not be able to access the uploaded files through the browser. You can test this end-point using a tool called 'Postman'. Try to hit '/photo' url with form data request with 'file' and 'name' fields. You can get all the uploaded files with their names by making a GET request on the same url. With this, I'd conclude this blog. Hope you found this useful, thanks for the read.
Have something to share ? Please post it in the comments section.
You must be logged in through your Google account to post comments
No comments available for this post