Difference between revisions of "FastAPI"
Adelo Vieira (talk | contribs) (→Project 2 - Data validations and HTTP Exceptions) |
Adelo Vieira (talk | contribs) |
||
(2 intermediate revisions by the same user not shown) | |||
Line 1: | Line 1: | ||
FastAPI official documentation: https://fastapi.tiangolo.com | FastAPI official documentation: https://fastapi.tiangolo.com | ||
+ | |||
+ | Entrevista al creador de FastAPI Sebastién Ramirez: https://www.youtube.com/watch?v=QzuGFU6n_Gs | ||
+ | |||
+ | |||
+ | To run the fastapi app: | ||
+ | <syntaxhighlight lang="python3"> | ||
+ | fastapi run main.py # With the new[standard] version - fastapi[standard]==0.112.0 | ||
+ | uvicorn main:app --reload # It used to be done this way | ||
+ | </syntaxhighlight> | ||
Line 201: | Line 210: | ||
<code>project_2/books2.py</code> | <code>project_2/books2.py</code> | ||
<syntaxhighlight lang="python3"> | <syntaxhighlight lang="python3"> | ||
+ | from typing import Optional | ||
+ | |||
+ | from fastapi import FastAPI, Path, Query, HTTPException | ||
+ | from pydantic import BaseModel, Field | ||
+ | from starlette import status | ||
+ | |||
+ | app = FastAPI() | ||
+ | |||
+ | |||
+ | class Book: | ||
+ | id: int | ||
+ | title: str | ||
+ | author: str | ||
+ | description: str | ||
+ | rating: int | ||
+ | published_date: int | ||
+ | |||
+ | def __init__(self, id, title, author, description, rating, published_date): | ||
+ | self.id = id | ||
+ | self.title = title | ||
+ | self.author = author | ||
+ | self.description = description | ||
+ | self.rating = rating | ||
+ | self.published_date = published_date | ||
+ | |||
+ | |||
+ | class BookRequest(BaseModel): # In project 1 we used Body (from fastapi import Body) as the type of the object that passed in the body of the request. However, using «Body()» we are not able tu add data validations, which can be done with BookRequest(BaseModel) | ||
+ | id: Optional[int] = Field(title='id is not needed') | ||
+ | title: str = Field(min_length=3) | ||
+ | author: str = Field(min_length=1) | ||
+ | description: str = Field(min_length=1, max_length=100) | ||
+ | rating: int = Field(gt=0, lt=6) | ||
+ | published_date: int = Field(gt=1999, lt=2031) | ||
+ | |||
+ | class Config: | ||
+ | schema_extra = { # This adds an example value that will be displayed in our Swagger. Note that there is no id cause we want the id to be autogenerated | ||
+ | 'example': { | ||
+ | 'title': 'A new book', | ||
+ | 'author': 'codingwithroby', | ||
+ | 'description': 'A new description of a book', | ||
+ | 'rating': 5, | ||
+ | 'published_date': 2029 | ||
+ | } | ||
+ | } | ||
+ | |||
+ | |||
+ | BOOKS = [ | ||
+ | Book(1, 'Computer Science Pro', 'codingwithroby', 'A very nice book!', 5, 2030), | ||
+ | Book(2, 'Be Fast with FastAPI', 'codingwithroby', 'A great book!', 5, 2030), | ||
+ | Book(3, 'Master Endpoints', 'codingwithroby', 'A awesome book!', 5, 2029), | ||
+ | Book(4, 'HP1', 'Author 1', 'Book Description', 2, 2028), | ||
+ | Book(5, 'HP2', 'Author 2', 'Book Description', 3, 2027), | ||
+ | Book(6, 'HP3', 'Author 3', 'Book Description', 1, 2026) | ||
+ | ] | ||
+ | |||
+ | BOOKS | ||
+ | |||
+ | |||
+ | @app.get("/books", status_code=status.HTTP_200_OK) # By using status.HTTP_200_OK, we dictate exactly what status response is returned after each successful request | ||
+ | async def read_all_books(): | ||
+ | return BOOKS | ||
+ | |||
+ | @app.get("/books/{book_id}", status_code=status.HTTP_200_OK) | ||
+ | async def read_book(book_id: int = Path(gt=0)): # With Pydantic we've added data validation for the post or put request body; but with haven't added any validation for our path parameters | ||
+ | for book in BOOKS: # Now, to add validations for path parameters, we can use the Path() class | ||
+ | if book.id == book_id: | ||
+ | return book | ||
+ | raise HTTPException(status_code=404, detail='Item not found') # By adding HTTP Exceptions a status code and message is sent back to the user so the user is able to know what happened with the request | ||
+ | |||
+ | |||
+ | @app.get("/books/", status_code=status.HTTP_200_OK) | ||
+ | async def read_book_by_rating(book_rating: int = Query(gt=0, lt=6)): # Query parameters validation using Query() | ||
+ | books_to_return = [] | ||
+ | for book in BOOKS: | ||
+ | if book.rating == book_rating: | ||
+ | books_to_return.append(book) | ||
+ | return books_to_return | ||
+ | |||
+ | |||
+ | @app.get("/books/publish/", status_code=status.HTTP_200_OK) | ||
+ | async def read_books_by_publish_date(published_date: int = Query(gt=1999, lt=2031)): | ||
+ | books_to_return = [] | ||
+ | for book in BOOKS: | ||
+ | if book.published_date == published_date: | ||
+ | books_to_return.append(book) | ||
+ | return books_to_return | ||
+ | |||
+ | |||
+ | @app.post("/create-book", status_code=status.HTTP_201_CREATED) | ||
+ | async def create_book(book_request: BookRequest): | ||
+ | new_book = Book(**book_request.dict()) # Here we are converting the book_request object, which is type BookRequest to a type Book | ||
+ | BOOKS.append(find_book_id(new_book)) | ||
+ | |||
+ | |||
+ | def find_book_id(book: Book): | ||
+ | book.id = 1 if len(BOOKS) == 0 else BOOKS[-1].id + 1 | ||
+ | return book | ||
+ | |||
+ | |||
+ | @app.put("/books/update_book", status_code=status.HTTP_204_NO_CONTENT) # This is the most common status code for a PUT request. It means, the request was successful but no content is returned to the client. | ||
+ | async def update_book(book: BookRequest): | ||
+ | book_changed = False | ||
+ | for i in range(len(BOOKS)): | ||
+ | if BOOKS[i].id == book.id: | ||
+ | BOOKS[i] = book | ||
+ | book_changed = True | ||
+ | if not book_changed: | ||
+ | raise HTTPException(status_code=404, detail='Item not found') | ||
+ | |||
+ | @app.delete("/books/{book_id}", status_code=status.HTTP_204_NO_CONTENT) | ||
+ | async def delete_book(book_id: int = Path(gt=0)): | ||
+ | book_changed = False | ||
+ | for i in range(len(BOOKS)): | ||
+ | if BOOKS[i].id == book_id: | ||
+ | BOOKS.pop(i) | ||
+ | book_changed = True | ||
+ | break | ||
+ | if not book_changed: | ||
+ | raise HTTPException(status_code=404, detail='Item not found') | ||
</syntaxhighlight> | </syntaxhighlight> | ||
Latest revision as of 17:52, 24 November 2024
FastAPI official documentation: https://fastapi.tiangolo.com
Entrevista al creador de FastAPI Sebastién Ramirez: https://www.youtube.com/watch?v=QzuGFU6n_Gs
To run the fastapi app:
fastapi run main.py # With the new[standard] version - fastapi[standard]==0.112.0
uvicorn main:app --reload # It used to be done this way
Contents
Course 1
FastAPI - The Complete Course 2023 (Beginner + Advanced): https://www.udemy.com/course/fastapi-the-complete-course/
Source code: https://github.com/codingwithroby/FastAPI-The-Complete-Course
Theory
HTTP request methods
- CRUD:
- GET : Read method: retrieves data
- POST : Create method, to submit data. POST can have a body that has additional information that GET does not have.
- PUT : Update the entire resource. PUT can also have a body.
- PATCH : Update part of the resource
- DELETE : Delete the resource
- TRACE : Performs a message loop-back to the target
- OPTIONS : Describes communication options to the target
- CONNECT : Creates a tunnel to the server, based on the target resource
Response status code
https://www.udemy.com/course/fastapi-the-complete-course/learn/lecture/36994202#overview
- 1xx : Informational Response: Requestprocessing
- 2xx : Success: Request successfully complete
- 200: OK : Standard Response for a Successful Request. Commonly used for successful Get requests when data is being returned.
- 201 : The request has been successful, creating a new resource. Used when a POST creates an entity.
- 204: No : The request has been successful, did not create an entity nor return anything. Commonly used with PUT requests.
- 3xx : Redirection: Further action must be complete
- 4xx : Client Errors: An error was caused by the request from the client
- 400: Bad : Cannot process request due to client error. Commonly used for invalid request methods.
- 401: Unauthorized : Client does not have valid authentication for target resource
- 404: Not : The clients requested resource can not be found
- 422: UnprocessableEntity : Semantic Errors in Client Request
- 5xx : Server Errors: An error has occurred on the server
- 500: Internal Server Error : Generic Error Message, when an unexpected issue on the server happened.
Project 1 - A first very basic example
Creating a FastAPI application:
project_1/books.py
from fastapi import FastAPI, Body
app = FastAPI()
BOOKS = [
{'title': 'Title One', 'author': 'Author One', 'category': 'science'},
{'title': 'Title Two', 'author': 'Author Two', 'category': 'science'},
{'title': 'Title Three', 'author': 'Author Three', 'category': 'history'},
{'title': 'Title Four', 'author': 'Author Four', 'category': 'math'},
{'title': 'Title Five', 'author': 'Author Five', 'category': 'math'},
{'title': 'Title Six', 'author': 'Author Two', 'category': 'math'}
]
@app.get("/books")
async def read_all_books():
return BOOKS
@app.get("/books/{book_title}") # path parameter
async def read_book(book_title: str):
for book in BOOKS:
if book.get('title').casefold() == book_title.casefold():
return book
@app.get("/books/") # query parameter: http://127.0.0.1:8000/books/?category=science
async def read_category_by_query(category: str):
books_to_return = []
for book in BOOKS:
if book.get('category').casefold() == category.casefold():
books_to_return.append(book)
return books_to_return
@app.get("/books/byauthor/") # Get all books from a specific author using path or query parameters
async def read_books_by_author_path(author: str):
books_to_return = []
for book in BOOKS:
if book.get('author').casefold() == author.casefold():
books_to_return.append(book)
return books_to_return
@app.get("/books/{book_author}/") # Using path parameters AND query parameters
async def read_author_category_by_query(book_author: str, category: str):
books_to_return = []
for book in BOOKS:
if book.get('author').casefold() == book_author.casefold() and \
book.get('category').casefold() == category.casefold():
books_to_return.append(book)
return books_to_return
@app.post("/books/create_book") # This end-point doesn't have any parameter but it has a required «body» where we can pass a new book entry, such as {"title": "Title Seven", "author": "Author One", "category": "history"}
async def create_book(new_book=Body()):
BOOKS.append(new_book)
@app.put("/books/update_book") # As in the case of the POST method, PUT requires a «body» where we can also pass a book. In this case, if we pass a new book whose title matches some of the existing books, it will be updated. For example: {"title": "Title Six", "author": "Author One", "category": "history"}
async def update_book(updated_book=Body()):
for i in range(len(BOOKS)):
if BOOKS[i].get('title').casefold() == updated_book.get('title').casefold():
BOOKS[i] = updated_book
@app.delete("/books/delete_book/{book_title}")
async def delete_book(book_title: str):
for i in range(len(BOOKS)):
if BOOKS[i].get('title').casefold() == book_title.casefold():
BOOKS.pop(i)
break
We can run our first app:
uvicorn books:app --reload
FastAPI has an integrated Swagger: http://127.0.0.1:8000/docs
Take a look at this lesson https://www.udemy.com/course/fastapi-the-complete-course/learn/lecture/29025524#overview to see how to try a post request using Swagger.
Project 2 - Data validations and HTTP Exceptions
Data validations:
POST/PUT request body data validation: https://www.udemy.com/course/fastapi-the-complete-course/learn/lecture/36994096#overview
- Pydantics is used for data modeling, data parsing and has efficient error handling. Pydantics is commonly used as a resource for data validation and how to handle data comming to our FastAPI application.
- With Pydantics:
- We're gonna create a different request model for data validation.
- We're gonna add Pydantics Field data validation on each variable/element of the request body.
Path parameters validation: https://www.udemy.com/course/fastapi-the-complete-course/learn/lecture/29025686#overview
- With Pydantic can add data validation for the post or put request body; but not for our path parameters.
- In order to add validationsfor path parameters, we can use the
Path()
class fromfastapi
Query parameters validation: https://www.udemy.com/course/fastapi-the-complete-course/learn/lecture/36994198#overview
- This can be done with the
Query()
class fromfastapi
HTTP Exceptions: https://www.udemy.com/course/fastapi-the-complete-course/learn/lecture/36994210#overview
HTTP exception is something that we have to raise within our method, which will cancel the functionality of our method and return a status code and a message back to the user so the user is able to know what happened with the request.
We're gonna handle HTTP Exceptions with the
HTTPException
class from fromfastapi
Explicit status code responses: https://www.udemy.com/course/fastapi-the-complete-course/learn/lecture/36994220#overview
We can also add status code responses for a successful API endpoint request. So, so far it will be returned a 200 if the request is a success. Well, a 200 does mean success, but we can go a little bit more detail than just a normal 200 and dictate exactly what status response is returned after each successful.To do so we're gonna be using
from starlette import status
.fastAPI
is built usingStarlet
, soStarlet
will be installed automatically when you installfastAPI
.
project_2/books2.py
from typing import Optional
from fastapi import FastAPI, Path, Query, HTTPException
from pydantic import BaseModel, Field
from starlette import status
app = FastAPI()
class Book:
id: int
title: str
author: str
description: str
rating: int
published_date: int
def __init__(self, id, title, author, description, rating, published_date):
self.id = id
self.title = title
self.author = author
self.description = description
self.rating = rating
self.published_date = published_date
class BookRequest(BaseModel): # In project 1 we used Body (from fastapi import Body) as the type of the object that passed in the body of the request. However, using «Body()» we are not able tu add data validations, which can be done with BookRequest(BaseModel)
id: Optional[int] = Field(title='id is not needed')
title: str = Field(min_length=3)
author: str = Field(min_length=1)
description: str = Field(min_length=1, max_length=100)
rating: int = Field(gt=0, lt=6)
published_date: int = Field(gt=1999, lt=2031)
class Config:
schema_extra = { # This adds an example value that will be displayed in our Swagger. Note that there is no id cause we want the id to be autogenerated
'example': {
'title': 'A new book',
'author': 'codingwithroby',
'description': 'A new description of a book',
'rating': 5,
'published_date': 2029
}
}
BOOKS = [
Book(1, 'Computer Science Pro', 'codingwithroby', 'A very nice book!', 5, 2030),
Book(2, 'Be Fast with FastAPI', 'codingwithroby', 'A great book!', 5, 2030),
Book(3, 'Master Endpoints', 'codingwithroby', 'A awesome book!', 5, 2029),
Book(4, 'HP1', 'Author 1', 'Book Description', 2, 2028),
Book(5, 'HP2', 'Author 2', 'Book Description', 3, 2027),
Book(6, 'HP3', 'Author 3', 'Book Description', 1, 2026)
]
BOOKS
@app.get("/books", status_code=status.HTTP_200_OK) # By using status.HTTP_200_OK, we dictate exactly what status response is returned after each successful request
async def read_all_books():
return BOOKS
@app.get("/books/{book_id}", status_code=status.HTTP_200_OK)
async def read_book(book_id: int = Path(gt=0)): # With Pydantic we've added data validation for the post or put request body; but with haven't added any validation for our path parameters
for book in BOOKS: # Now, to add validations for path parameters, we can use the Path() class
if book.id == book_id:
return book
raise HTTPException(status_code=404, detail='Item not found') # By adding HTTP Exceptions a status code and message is sent back to the user so the user is able to know what happened with the request
@app.get("/books/", status_code=status.HTTP_200_OK)
async def read_book_by_rating(book_rating: int = Query(gt=0, lt=6)): # Query parameters validation using Query()
books_to_return = []
for book in BOOKS:
if book.rating == book_rating:
books_to_return.append(book)
return books_to_return
@app.get("/books/publish/", status_code=status.HTTP_200_OK)
async def read_books_by_publish_date(published_date: int = Query(gt=1999, lt=2031)):
books_to_return = []
for book in BOOKS:
if book.published_date == published_date:
books_to_return.append(book)
return books_to_return
@app.post("/create-book", status_code=status.HTTP_201_CREATED)
async def create_book(book_request: BookRequest):
new_book = Book(**book_request.dict()) # Here we are converting the book_request object, which is type BookRequest to a type Book
BOOKS.append(find_book_id(new_book))
def find_book_id(book: Book):
book.id = 1 if len(BOOKS) == 0 else BOOKS[-1].id + 1
return book
@app.put("/books/update_book", status_code=status.HTTP_204_NO_CONTENT) # This is the most common status code for a PUT request. It means, the request was successful but no content is returned to the client.
async def update_book(book: BookRequest):
book_changed = False
for i in range(len(BOOKS)):
if BOOKS[i].id == book.id:
BOOKS[i] = book
book_changed = True
if not book_changed:
raise HTTPException(status_code=404, detail='Item not found')
@app.delete("/books/{book_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_book(book_id: int = Path(gt=0)):
book_changed = False
for i in range(len(BOOKS)):
if BOOKS[i].id == book_id:
BOOKS.pop(i)
book_changed = True
break
if not book_changed:
raise HTTPException(status_code=404, detail='Item not found')
Course 2
Build a Modern API with FastAPI and Python: https://www.udemy.com/course/build-a-movie-tracking-api-with-fastapi-and-python/
During this course we will build a Movie Tracking API with Fast API and Python for tracking movies.
- Section 2: Environment Setup
- We will install Pycharm, Docker and Docker-compose and Insomnia. If you already have an environment you can skip this section.
- Note: If you're an university student you can apply for a free licence of Pycharm Professional here: https://www.jetbrains.com/community/education/#students
- Section 3: Docker Basic
- We will learn some basic docker commands that will help us in improving our workflow.
- Section 4: MongoDB Basics
- We will learn very basic MongoDB commands and we'll execute them inside the docker container and in Pycharm Professional.
- Section 5: Web API Project Structure
- In this section we'll learn how to structure the project and we will write some basic endpoints with FastAPI just to make you more familiar with writing endpoints.
- Section 6: Storage Layer
- We talk about CRUD and we'll apply the repository pattern to develop and In-Memory repository and a MongoDB repository in order to use them within our application. We will also test the implementations.
- Section 7: Movie Tracker API
- We will write the actual API for tracking movies using the previous developed components. We'll implement the application's settings module and we'll add pagination to some of the routes. In the end we will write unit tests.
- Section 8: Middleware
- We'll talk about Fast API middleware and how to write your own custom middleware.
- Section 9: Authentication
- We'll talk about implementing Basic Authentication and validating JWT tokens.
- Section 10: Deployment
- We'll containerize the application and we will deploy it on a local microk8s kubernetes cluster. In the end we'll visualise some metrics with Grafana. Having metrics is a good way to observe your applications performance and behaviour and troubleshoot it.
- Resources for this lecture at https://www.udemy.com/course/build-a-movie-tracking-api-with-fastapi-and-python/learn/lecture/35148314?start=1#overview
Environment setup
- python3, python3-pip, docker, docker Compose.
- IDE: The instructor is using Pycharm Professional, which is paid but we can install Pycharm community. On Ubuntu:
sudo snap install pycharm-community --classic
- Database: MongoDB in a Docker container
- DB IDE:
- I thinks the instructor is also using PyCharm.
- I'll be using the MongoDB for VS Code extension:
- For testing the API we are going to use Insomnia, which is a powerful REST client that allows developers to interact with and test RESTful APIs. It provides a user-friendly interface for making HTTP requests, inspecting responses, and debugging API interactions. The tool is often used for tasks such as sending requests, setting headers, managing authentication, and viewing API documentation.
- To install it on Ubuntu:
sudo snap install insomnia
Creating our Mongo Docker comtainer using docker-compose
docker-compose.yaml
version: '3.1'
services:
mongo:
image: mongo:5.0.14
restart: always
ports:
- "27017:27017"
docker-compose up -d
MongoDB basics
We can start our MongoDB shell by:
docker exec -it fastapi-rest-api-movie-tracker-mongo-1 mongosh
Or we can use our favory DB IDE to manage our MongoDB. I'm using MongoDB for VS Code extension.
Let's see some MongoDB basics:
playground-1.mongodb.js
show('databases') // show databases can be used from mongosh
use('movies') // use movies can be used from mongosh. If the movies collection doesn't exits it will create it
db.movies.insertOne({'title':'My Movie', 'year':2022, watched:false})
db.movies.insertMany([
{
'title': 'The Shawshank Redemption',
'year': 1994,
'watched': false
},
{
'title': 'The Dark Knkght',
'year': 2008,
'watched': false
},
{
'title': 'Puld Fiction',
'year': 1994,
'watched': false
},
{
'title': 'Fight Club',
'year': 1999,
'watched': false
},
{
'title': 'The Lord of the Rings: The Two Towers',
'year': 2002,
'watched': false
},
])
db.movies.findOne()
// Find all the movies
db.movies.find()
// Filtering by title
db.movies.find({'title': 'Fight Club'})
// Find a movie that has been produced before 2000
db.movies.find({'year': {'$lt': 2000}})
// Filter by id
db.movies.find({'_id':ObjectId('647b582d74a86f2cb913d881')})
// Find all movies but skip the first one and limit the result to only 2 movies
db.movies.find().skip(1).limit(2)
// Sorting
db.movies.find().sort({'year':-1}) // 1: ascending; -1: descending
// Select only some attributes
db.movies.find({}, {'title':1, 'year':1})
db.movies.find({}, {'title':0})
// Delete a document
db.movies.deleteOne({'_id': ObjectId('647b55cdc98722c8abe8ee94')})
db.movies.find()
// Updating
db.movies.updateOne({'_id': ObjectId('647b582d74a86f2cb913d87e')}, {$set: {'watched': true}})
db.movies.updateOne({'_id': ObjectId('647b582d74a86f2cb913d87e')}, {$inc: {'year': -3}})
db.movies.find()