* Python Web Development with Sanic [[Packt]](https://www.packtpub.com/product/python-web-development-with-sanic/9781801814416?_ga=2.134911217.1837201707.1657723916-1157268863.1584421665&utm_source=github&utm_medium=repository&utm_campaign=9781801814416) [[Amazon]](https://www.amazon.com/dp/1801814414)
* Becoming an Enterprise Django Developer [[Packt]](https://www.packtpub.com/product/becoming-an-enterprise-django-developer/9781801073639?_ga=2.127463693.1837201707.1657723916-1157268863.1584421665&utm_source=github&utm_medium=repository&utm_campaign=9781801073639) [[Amazon]](https://www.amazon.com/dp/1801073635)
## Errata
* **Use port 8000 as per the GitHub repository examples.**
* Page 14 (Code Snippet 1 line 1): **FROM PYTHON:3.8** _should be_ **FROM python:3.8**
* Page 15 (last line): **FROM PYTHON:3.8** _should be_ **(venv)$ uvicorn api:app --port 8080 --reload**
* Page 10,11,12 : Page 10 should display the picture of page 11. Page 11 should display the picture of page 12. Page 12 should display the picture of page 10.
* Page 24, (Code snippet 2 Line 1):
```from Pydantic import BaseModel```
_should be_
```from pydantic import BaseModel```
* Page 24, (Code snippet 2 Line 2):
```class Todo(BaseMode):```
_should be_
```class Todo(BaseModel):```
* Page 24, **Let’s go ahead and use the model in our POST route. In api.py, import the model** _should be_ **Let’s go ahead and use the model in our POST route. In todo.py, import the model**
## Get to Know the Author
**Abdulazeez Abdulazeez Adeshina**
is a skilled Python developer, backend software engineer, and technical writer, with a wide range of technical skill sets in his arsenal. His background has led him to build command-line applications, backend applications in FastAPI, and algorithm-based treasure-hunting tools. He also enjoys teaching Python and solving mathematical-oriented problems through his blog. Abdulazeez is currently in his penultimate year of a water resources and environmental engineering program. His work experience as a guest technical author includes the likes of Auth0, LogRocket, Okteto, and TestDriven.
### Download a free PDF
If you have already purchased a print or Kindle version of this book, you can get a DRM-free PDF version at no cost. Simply click on the link to claim your free PDF.
https://packt.link/free-ebook/9781801076630
================================================
FILE: ch01/Dockerfile
================================================
FROM PYTHON:3.8
# Set working directory to /usr/src/app
WORKDIR /usr/src/app
# Copy the contents of the current local directory into the container’s working directory
ADD . /usr/src/app
# Run a command
CMD [“python”, “hello.py”]
================================================
FILE: ch01/hello.py
================================================
print("Hello!")
================================================
FILE: ch01/todos/api.py
================================================
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def welcome() -> dict:
return {
"message": "Hello World"
}
================================================
FILE: ch01/todos/requirements.txt
================================================
fastapi
uvicorn
================================================
FILE: ch02/todos/api.py
================================================
from fastapi import FastAPI
from todo import todo_router
app = FastAPI()
@app.get("/")
async def welcome() -> dict:
return {
"message": "Hello World"
}
app.include_router(todo_router)
================================================
FILE: ch02/todos/model.py
================================================
from pydantic import BaseModel
class Todo(BaseModel):
id: int
item: str
class Config:
schema_extra = {
"example": {
"id": 1,
"item": "Example Schema!"
}
}
class TodoItem(BaseModel):
item: str
class Config:
schema_extra = {
"example": {
"item": "Read the next chapter of the book"
}
}
================================================
FILE: ch02/todos/requirements.txt
================================================
fastapi==0.70.0
uvicorn==0.15.0
================================================
FILE: ch02/todos/todo.py
================================================
from fastapi import APIRouter, Path
from model import Todo, TodoItem
todo_router = APIRouter()
todo_list = []
@todo_router.post("/todo")
async def add_todo(todo: Todo) -> dict:
todo_list.append(todo)
return {
"message": "Todo added successfully."
}
@todo_router.get("/todo")
async def retrieve_todo() -> dict:
return {
"todos": todo_list
}
@todo_router.get("/todo/{todo_id}")
async def get_single_todo(todo_id: int = Path(..., title="The ID of the todo to retrieve.")) -> dict:
for todo in todo_list:
if todo.id == todo_id:
return {
"todo": todo
}
return {
"message": "Todo with supplied ID doesn't exist."
}
@todo_router.put("/todo/{todo_id}")
async def update_todo(todo_data: TodoItem, todo_id: int = Path(..., title="The ID of the todo to be updated.")) -> dict:
for todo in todo_list:
if todo.id == todo_id:
todo.item = todo_data.item
return {
"message": "Todo updated successfully."
}
return {
"message": "Todo with supplied ID doesn't exist."
}
@todo_router.delete("/todo/{todo_id}")
async def delete_single_todo(todo_id: int) -> dict:
for index in range(len(todo_list)):
todo = todo_list[index]
if todo.id == todo_id:
todo_list.pop(index)
return {
"message": "Todo deleted successfully."
}
return {
"message": "Todo with supplied ID doesn't exist."
}
@todo_router.delete("/todo")
async def delete_all_todo() -> dict:
todo_list.clear()
return {
"message": "Todos deleted successfully."
}
================================================
FILE: ch03/todos/api.py
================================================
from fastapi import FastAPI
from todo import todo_router
app = FastAPI()
@app.get("/")
async def welcome() -> dict:
return {
"message": "Hello World"
}
app.include_router(todo_router)
================================================
FILE: ch03/todos/model.py
================================================
from typing import List
from pydantic import BaseModel
class Todo(BaseModel):
id: int
item: str
class Config:
schema_extra = {
"example": {
"id": 1,
"item": "Example schema!"
}
}
class TodoItem(BaseModel):
item: str
class Config:
schema_extra = {
"example": {
"item": "Read the next chapter of the book"
}
}
class TodoItems(BaseModel):
todos: List[TodoItem]
class Config:
schema_extra = {
"example": {
"todos": [
{
"item": "Example schema 1!"
},
{
"item": "Example schema 2!"
}
]
}
}
================================================
FILE: ch03/todos/requirements.txt
================================================
fastapi==0.70.0
uvicorn==0.15.0
================================================
FILE: ch03/todos/todo.py
================================================
from fastapi import APIRouter, Path, HTTPException, status
from model import Todo, TodoItem, TodoItems
todo_router = APIRouter()
todo_list = []
@todo_router.post("/todo", status_code=201)
async def add_todo(todo: Todo) -> dict:
todo_list.append(todo)
return {
"message": "Todo added successfully."
}
@todo_router.get("/todo", response_model=TodoItems)
async def retrieve_todo() -> dict:
return {
"todos": todo_list
}
@todo_router.get("/todo/{todo_id}")
async def get_single_todo(todo_id: int = Path(..., title="The ID of the todo to retrieve.")) -> dict:
for todo in todo_list:
if todo.id == todo_id:
return {
"todo": todo
}
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Todo with supplied ID doesn't exist",
)
@todo_router.put("/todo/{todo_id}")
async def update_todo(todo_data: TodoItem, todo_id: int = Path(..., title="The ID of the todo to be updated.")) -> dict:
for todo in todo_list:
if todo.id == todo_id:
todo.item = todo_data.item
return {
"message": "Todo updated successfully."
}
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Todo with supplied ID doesn't exist",
)
@todo_router.delete("/todo/{todo_id}")
async def delete_single_todo(todo_id: int) -> dict:
for index in range(len(todo_list)):
todo = todo_list[index]
if todo.id == todo_id:
todo_list.pop(index)
return {
"message": "Todo deleted successfully."
}
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Todo with supplied ID doesn't exist",
)
@todo_router.delete("/todo")
async def delete_all_todo() -> dict:
todo_list.clear()
return {
"message": "Todos deleted successfully."
}
================================================
FILE: ch04/todos/api.py
================================================
from fastapi import FastAPI
from todo import todo_router
app = FastAPI()
@app.get("/")
async def welcome() -> dict:
return {
"message": "Hello World"
}
app.include_router(todo_router)
================================================
FILE: ch04/todos/model.py
================================================
from typing import List, Optional
from fastapi import Form
from pydantic import BaseModel
class Todo(BaseModel):
id: Optional[int]
item: str
@classmethod
def as_form(
cls,
item: str = Form(...)
):
return cls(item=item)
class Config:
schema_extra = {
"example": {
"id": 1,
"item": "Example schema!"
}
}
class TodoItem(BaseModel):
item: str
class Config:
schema_extra = {
"example": {
"item": "Read the next chapter of the book"
}
}
class TodoItems(BaseModel):
todos: List[TodoItem]
class Config:
schema_extra = {
"example": {
"todos": [
{
"item": "Example schema 1!"
},
{
"item": "Example schema 2!"
}
]
}
}
================================================
FILE: ch04/todos/requirements.txt
================================================
fastapi==0.70.0
uvicorn==0.15.0
jinja2 == 3.1.2
python-multipart
================================================
FILE: ch04/todos/templates/home.html
================================================
Packt Todo Application
{% block todo_container %}{% endblock %}
================================================
FILE: ch04/todos/templates/todo.html
================================================
{% extends "home.html" %}
{% block todo_container %}
{% if todo %}
Todo ID: {{ todo.id }}
Item: {{ todo.item }}
{% else %}
{% endblock %}
================================================
FILE: ch04/todos/todo.py
================================================
from fastapi import APIRouter, Path, HTTPException, status, Request, Depends
from fastapi.templating import Jinja2Templates
from model import Todo, TodoItem, TodoItems
todo_router = APIRouter()
todo_list = []
templates = Jinja2Templates(directory="templates/")
@todo_router.post("/todo")
async def add_todo(request: Request, todo: Todo = Depends(Todo.as_form)):
todo.id = len(todo_list) + 1
todo_list.append(todo)
return templates.TemplateResponse("todo.html",
{
"request": request,
"todos": todo_list
})
@todo_router.get("/todo", response_model=TodoItems)
async def retrieve_todo(request: Request):
return templates.TemplateResponse("todo.html", {
"request": request,
"todos": todo_list
})
@todo_router.get("/todo/{todo_id}")
async def get_single_todo(request: Request, todo_id: int = Path(..., title="The ID of the todo to retrieve.")):
for todo in todo_list:
if todo.id == todo_id:
return templates.TemplateResponse("todo.html", {
"request": request,
"todo": todo
})
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Todo with supplied ID doesn't exist",
)
@todo_router.put("/todo/{todo_id}")
async def update_todo(request: Request, todo_data: TodoItem,
todo_id: int = Path(..., title="The ID of the todo to be updated.")) -> dict:
for todo in todo_list:
if todo.id == todo_id:
todo.item = todo_data.item
return {
"message": "Todo updated successfully."
}
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Todo with supplied ID doesn't exist",
)
@todo_router.delete("/todo/{todo_id}")
async def delete_single_todo(request: Request, todo_id: int) -> dict:
for index in range(len(todo_list)):
todo = todo_list[index]
if todo.id == todo_id:
todo_list.pop(index)
return {
"message": "Todo deleted successfully."
}
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Todo with supplied ID doesn't exist",
)
@todo_router.delete("/todo")
async def delete_all_todo() -> dict:
todo_list.clear()
return {
"message": "Todos deleted successfully."
}
================================================
FILE: ch05/planner/database/__init__.py
================================================
================================================
FILE: ch05/planner/main.py
================================================
from fastapi import FastAPI
from fastapi.responses import RedirectResponse
from routes.users import user_router
from routes.events import event_router
import uvicorn
app = FastAPI()
# Register routes
app.include_router(user_router, prefix="/user")
app.include_router(event_router, prefix="/event")
@app.get("/")
async def home():
return RedirectResponse(url="/event/")
if __name__ == '__main__':
uvicorn.run("main:app", host="0.0.0.0", port=8080, reload=True)
================================================
FILE: ch05/planner/models/__init__.py
================================================
================================================
FILE: ch05/planner/models/events.py
================================================
from typing import List
from pydantic import BaseModel
class Event(BaseModel):
id: int
title: str
image: str
description: str
tags: List[str]
location: str
class Config:
schema_extra = {
"example": {
"title": "FastAPI Book Launch",
"image": "https://linktomyimage.com/image.png",
"description": "We will be discussing the contents of the FastAPI book in this event.Ensure to come with your own copy to win gifts!",
"tags": ["python", "fastapi", "book", "launch"],
"location": "Google Meet"
}
}
================================================
FILE: ch05/planner/models/users.py
================================================
from pydantic import BaseModel, EmailStr
class User(BaseModel):
email: EmailStr
password: str
class Config:
schema_extra = {
"example": {
"email": "fastapi@packt.com",
"password": "strong!!!",
}
}
class UserSignIn(BaseModel):
email: EmailStr
password: str
schema_extra = {
"example": {
"email": "fastapi@packt.com",
"password": "strong!!!"
}
}
================================================
FILE: ch05/planner/requirements.txt
================================================
anyio==3.5.0
asgiref==3.5.0
bcrypt==3.2.0
beanie==1.10.4
cffi==1.15.0
click==8.0.4
dnspython==2.2.0
email-validator==1.1.3
fastapi==0.74.1
h11==0.13.0
idna==3.3
Jinja2==3.0.3
MarkupSafe==2.1.0
motor==2.5.1
multidict==6.0.2
passlib==1.7.4
pycparser==2.21
pydantic==1.9.0
PyJWT==2.3.0
pymongo==3.12.3
python-dotenv==0.20.0
python-multipart==0.0.5
six==1.16.0
sniffio==1.2.0
SQLAlchemy==1.4.32
sqlalchemy2-stubs==0.0.2a20
sqlmodel==0.0.6
starlette==0.17.1
toml==0.10.2
typing_extensions==4.1.1
uvicorn==0.17.5
yarl==1.7.2
================================================
FILE: ch05/planner/routes/__init__.py
================================================
================================================
FILE: ch05/planner/routes/events.py
================================================
from typing import List
from fastapi import APIRouter, Body, HTTPException, status
from models.events import Event
event_router = APIRouter(
tags=["Events"]
)
events = []
@event_router.get("/", response_model=List[Event])
async def retrieve_all_events() -> List[Event]:
return events
@event_router.get("/{id}", response_model=Event)
async def retrieve_event(id: int) -> Event:
for event in events:
if event.id == id:
return event
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Event with supplied ID does not exist"
)
@event_router.post("/new")
async def create_event(body: Event = Body(...)) -> dict:
events.append(body)
return {
"message": "Event created successfully"
}
@event_router.delete("/{id}")
async def delete_event(id: int) -> dict:
for event in events:
if event.id == id:
events.remove(event)
return {
"message": "Event deleted successfully"
}
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Event with supplied ID does not exist"
)
================================================
FILE: ch05/planner/routes/users.py
================================================
from fastapi import APIRouter, HTTPException, status
from models.users import User, UserSignIn
user_router = APIRouter(
tags=["User"],
)
users = {}
@user_router.post("/signup")
async def sign_user_up(data: User) -> dict:
if data.email in users:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="User with supplied username exists"
)
users[data.email] = data
return {
"message": "User successfully registered!"
}
@user_router.post("/signin")
async def sign_user_in(user: UserSignIn) -> dict:
if user.email not in users:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User does not exist"
)
if users[user.email].password != user.password:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Wrong credential passed"
)
return {
"message": "User signed in successfully"
}
================================================
FILE: ch06/planner/database/__init__.py
================================================
================================================
FILE: ch06/planner/database/connection.py
================================================
from typing import Any, List, Optional
from beanie import init_beanie, PydanticObjectId
from models.events import Event
from models.users import User
from motor.motor_asyncio import AsyncIOMotorClient
from pydantic import BaseSettings, BaseModel
class Settings(BaseSettings):
DATABASE_URL: Optional[str] = None
async def initialize_database(self):
client = AsyncIOMotorClient(self.DATABASE_URL)
await init_beanie(database=client.get_default_database(),
document_models=[Event, User])
class Config:
env_file = ".env"
class Database:
def __init__(self, model):
self.model = model
async def save(self, document) -> None:
await document.create()
return
async def get(self, id: PydanticObjectId) -> Any:
doc = await self.model.get(id)
if doc:
return doc
return False
async def get_all(self) -> List[Any]:
docs = await self.model.find_all().to_list()
return docs
async def update(self, id: PydanticObjectId, body: BaseModel) -> Any:
doc_id = id
des_body = body.dict()
des_body = {k: v for k, v in des_body.items() if v is not None}
update_query = {"$set": {
field: value for field, value in des_body.items()
}}
doc = await self.get(doc_id)
if not doc:
return False
await doc.update(update_query)
return doc
async def delete(self, id: PydanticObjectId) -> bool:
doc = await self.get(id)
if not doc:
return False
await doc.delete()
return True
================================================
FILE: ch06/planner/main.py
================================================
import uvicorn
from fastapi import FastAPI
from fastapi.responses import RedirectResponse
from database.connection import Settings
from routes.events import event_router
from routes.users import user_router
app = FastAPI()
settings = Settings()
# Register routes
app.include_router(user_router, prefix="/user")
app.include_router(event_router, prefix="/event")
@app.on_event("startup")
async def init_db():
await settings.initialize_database()
@app.get("/")
async def home():
return RedirectResponse(url="/event/")
if __name__ == '__main__':
uvicorn.run("main:app", host="0.0.0.0", port=8080, reload=True)
================================================
FILE: ch06/planner/models/__init__.py
================================================
================================================
FILE: ch06/planner/models/events.py
================================================
from typing import Optional, List
from beanie import Document
from pydantic import BaseModel
class Event(Document):
title: str
image: str
description: str
tags: List[str]
location: str
class Config:
schema_extra = {
"example": {
"title": "FastAPI BookLaunch",
"image": "https://linktomyimage.com/image.png",
"description": "We will be discussing the contents of the FastAPI book in this event.Ensure to come with your own copy to win gifts!",
"tags": ["python", "fastapi", "book", "launch"],
"location": "Google Meet"
}
}
class Settings:
name = "events"
class EventUpdate(BaseModel):
title: Optional[str]
image: Optional[str]
description: Optional[str]
tags: Optional[List[str]]
location: Optional[str]
class Config:
schema_extra = {
"example": {
"title": "FastAPI BookLaunch",
"image": "https://linktomyimage.com/image.png",
"description": "We will be discussing the contents of the FastAPI book in this event.Ensure to come with your own copy to win gifts!",
"tags": ["python", "fastapi", "book", "launch"],
"location": "Google Meet"
}
}
================================================
FILE: ch06/planner/models/users.py
================================================
from beanie import Document
from pydantic import BaseModel, EmailStr
class User(Document):
email: EmailStr
password: str
class Settings:
name = "users"
class Config:
schema_extra = {
"example": {
"email": "fastapi@packt.com",
"password": "strong!!!",
}
}
class UserSignIn(BaseModel):
email: EmailStr
password: str
================================================
FILE: ch06/planner/requirements.txt
================================================
anyio==3.5.0
asgiref==3.5.0
bcrypt==3.2.0
beanie==1.10.4
cffi==1.15.0
click==8.0.4
dnspython==2.2.0
email-validator==1.1.3
fastapi==0.74.1
h11==0.13.0
idna==3.3
Jinja2==3.0.3
MarkupSafe==2.1.0
motor==2.5.1
multidict==6.0.2
passlib==1.7.4
pycparser==2.21
pydantic==1.9.0
PyJWT==2.3.0
pymongo==3.12.3
python-dotenv==0.20.0
python-multipart==0.0.5
six==1.16.0
sniffio==1.2.0
SQLAlchemy==1.4.32
sqlalchemy2-stubs==0.0.2a20
sqlmodel==0.0.6
starlette==0.17.1
toml==0.10.2
typing_extensions==4.1.1
uvicorn==0.17.5
yarl==1.7.2
================================================
FILE: ch06/planner/routes/__init__.py
================================================
================================================
FILE: ch06/planner/routes/events.py
================================================
from typing import List
from beanie import PydanticObjectId
from database.connection import Database
from fastapi import APIRouter, HTTPException, status
from models.events import Event, EventUpdate
event_router = APIRouter(
tags=["Events"]
)
event_database = Database(Event)
@event_router.get("/", response_model=List[Event])
async def retrieve_all_events() -> List[Event]:
events = await event_database.get_all()
return events
@event_router.get("/{id}", response_model=Event)
async def retrieve_event(id: PydanticObjectId) -> Event:
event = await event_database.get(id)
if not event:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Event with supplied ID does not exist"
)
return event
@event_router.post("/new")
async def create_event(body: Event) -> dict:
await event_database.save(body)
return {
"message": "Event created successfully"
}
@event_router.put("/{id}", response_model=Event)
async def update_event(id: PydanticObjectId, body: EventUpdate) -> Event:
updated_event = await event_database.update(id, body)
if not updated_event:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Event with supplied ID does not exist"
)
return updated_event
@event_router.delete("/{id}")
async def delete_event(id: PydanticObjectId) -> dict:
event = await event_database.delete(id)
if not event:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Event with supplied ID does not exist"
)
return {
"message": "Event deleted successfully."
}
================================================
FILE: ch06/planner/routes/users.py
================================================
from database.connection import Database
from fastapi import APIRouter, HTTPException, status
from models.users import User, UserSignIn
user_router = APIRouter(
tags=["User"],
)
user_database = Database(User)
@user_router.post("/signup")
async def sign_user_up(user: User) -> dict:
user_exist = await User.find_one(User.email == user.email)
if user_exist:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="User with email provided exists already."
)
await user_database.save(user)
return {
"message": "User created successfully"
}
@user_router.post("/signin")
async def sign_user_in(user: UserSignIn) -> dict:
user_exist = await User.find_one(User.email == user.email)
if not user_exist:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User with email does not exist."
)
if user_exist.password == user.password:
return {
"message": "User signed in successfully."
}
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid details passed."
)
================================================
FILE: ch07/planner/auth/__init__.py
================================================
================================================
FILE: ch07/planner/auth/authenticate.py
================================================
from auth.jwt_handler import verify_access_token
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/user/signin")
async def authenticate(token: str = Depends(oauth2_scheme)) -> str:
if not token:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Sign in for access"
)
decoded_token = verify_access_token(token)
return decoded_token["user"]
================================================
FILE: ch07/planner/auth/hash_password.py
================================================
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
class HashPassword:
def create_hash(self, password: str):
return pwd_context.hash(password)
def verify_hash(self, plain_password: str, hashed_password: str):
return pwd_context.verify(plain_password, hashed_password)
================================================
FILE: ch07/planner/auth/jwt_handler.py
================================================
import time
from datetime import datetime
from database.connection import Settings
from fastapi import HTTPException, status
from jose import jwt, JWTError
settings = Settings()
def create_access_token(user: str):
payload = {
"user": user,
"expires": time.time() + 3600
}
token = jwt.encode(payload, settings.SECRET_KEY, algorithm="HS256")
return token
def verify_access_token(token: str):
try:
data = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"])
expire = data.get("expires")
if expire is None:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="No access token supplied"
)
if datetime.utcnow() > datetime.utcfromtimestamp(expire):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Token expired!"
)
return data
except JWTError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid token"
)
================================================
FILE: ch07/planner/database/__init__.py
================================================
================================================
FILE: ch07/planner/database/connection.py
================================================
from typing import Optional
from beanie import init_beanie, PydanticObjectId
from models.events import Event
from models.users import User
from motor.motor_asyncio import AsyncIOMotorClient
from pydantic import BaseSettings, BaseModel
class Settings(BaseSettings):
DATABASE_URL: Optional[str] = None
SECRET_KEY: Optional[str] = None
async def initialize_database(self):
client = AsyncIOMotorClient(self.DATABASE_URL)
await init_beanie(database=client.get_default_database(),
document_models=[Event, User])
class Config:
env_file = ".env"
class Database:
def __init__(self, model):
self.model = model
async def save(self, document):
await document.create()
return
async def get(self, id: PydanticObjectId):
doc = await self.model.get(id)
if doc:
return doc
return False
async def get_all(self):
docs = await self.model.find_all().to_list()
return docs
async def update(self, id: PydanticObjectId, body: BaseModel):
doc_id = id
des_body = body.dict()
des_body = {k: v for k, v in des_body.items() if v is not None}
update_query = {"$set": {
field: value for field, value in des_body.items()
}}
doc = await self.get(doc_id)
if not doc:
return False
await doc.update(update_query)
return doc
async def delete(self, id: PydanticObjectId):
doc = await self.get(id)
if not doc:
return False
await doc.delete()
return True
================================================
FILE: ch07/planner/main.py
================================================
import uvicorn
from fastapi import FastAPI
from fastapi.responses import RedirectResponse
from fastapi.middleware.cors import CORSMiddleware
from database.connection import Settings
from routes.events import event_router
from routes.users import user_router
app = FastAPI()
settings = Settings()
# register origins
origins = ["*"]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Register routes
app.include_router(user_router, prefix="/user")
app.include_router(event_router, prefix="/event")
@app.on_event("startup")
async def init_db():
await settings.initialize_database()
@app.get("/")
async def home():
return RedirectResponse(url="/event/")
if __name__ == '__main__':
uvicorn.run("main:app", host="0.0.0.0", port=8080, reload=True)
================================================
FILE: ch07/planner/models/__init__.py
================================================
================================================
FILE: ch07/planner/models/events.py
================================================
from typing import Optional, List
from beanie import Document
from pydantic import BaseModel
class Event(Document):
creator: Optional[str]
title: str
image: str
description: str
tags: List[str]
location: str
class Config:
schema_extra = {
"example": {
"title": "FastAPI BookLaunch",
"image": "https://linktomyimage.com/image.png",
"description": "We will be discussing the contents of the FastAPI book in this event.Ensure to come with your own copy to win gifts!",
"tags": ["python", "fastapi", "book", "launch"],
"location": "Google Meet"
}
}
class Collection:
name = "events"
class EventUpdate(BaseModel):
title: Optional[str]
image: Optional[str]
description: Optional[str]
tags: Optional[List[str]]
location: Optional[str]
class Config:
schema_extra = {
"example": {
"title": "FastAPI BookLaunch",
"image": "https://linktomyimage.com/image.png",
"description": "We will be discussing the contents of the FastAPI book in this event.Ensure to come with your own copy to win gifts!",
"tags": ["python", "fastapi", "book", "launch"],
"location": "Google Meet"
}
}
================================================
FILE: ch07/planner/models/users.py
================================================
from beanie import Document
from pydantic import BaseModel, EmailStr
class User(Document):
email: EmailStr
password: str
class Collection:
name = "users"
class Config:
schema_extra = {
"example": {
"email": "fastapi@packt.com",
"password": "strong!!!"
}
}
class TokenResponse(BaseModel):
access_token: str
token_type: str
================================================
FILE: ch07/planner/requirements.txt
================================================
anyio==3.5.0
asgiref==3.5.0
bcrypt==3.2.0
beanie==1.10.4
cffi==1.15.0
click==8.0.4
cryptography==36.0.2
dnspython==2.2.0
ecdsa==0.17.0
email-validator==1.1.3
fastapi==0.74.1
h11==0.13.0
idna==3.3
Jinja2==3.0.3
MarkupSafe==2.1.0
motor==2.5.1
multidict==6.0.2
passlib==1.7.4
pyasn1==0.4.8
pycparser==2.21
pydantic==1.9.0
pymongo==3.12.3
python-dotenv==0.20.0
python-jose==3.3.0
python-multipart==0.0.5
rsa==4.8
six==1.16.0
sniffio==1.2.0
SQLAlchemy==1.4.32
sqlalchemy2-stubs==0.0.2a20
sqlmodel==0.0.6
starlette==0.17.1
toml==0.10.2
typing_extensions==4.1.1
uvicorn==0.17.5
yarl==1.7.2
================================================
FILE: ch07/planner/routes/__init__.py
================================================
================================================
FILE: ch07/planner/routes/events.py
================================================
from typing import List
from auth.authenticate import authenticate
from beanie import PydanticObjectId
from database.connection import Database
from fastapi import APIRouter, Depends, HTTPException, status
from models.events import Event, EventUpdate
event_router = APIRouter(
tags=["Events"]
)
event_database = Database(Event)
@event_router.get("/", response_model=List[Event])
async def retrieve_all_events() -> List[Event]:
events = await event_database.get_all()
return events
@event_router.get("/{id}", response_model=Event)
async def retrieve_event(id: PydanticObjectId) -> Event:
event = await event_database.get(id)
if not event:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Event with supplied ID does not exist"
)
return event
@event_router.post("/new")
async def create_event(body: Event, user: str = Depends(authenticate)) -> dict:
body.creator = user
await event_database.save(body)
return {
"message": "Event created successfully"
}
@event_router.put("/{id}", response_model=Event)
async def update_event(id: PydanticObjectId, body: EventUpdate, user: str = Depends(authenticate)) -> Event:
event = await event_database.get(id)
if event.creator != user:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Operation not allowed"
)
updated_event = await event_database.update(id, body)
if not updated_event:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Event with supplied ID does not exist"
)
return updated_event
@event_router.delete("/{id}")
async def delete_event(id: PydanticObjectId, user: str = Depends(authenticate)) -> dict:
event = await event_database.get(id)
if not event:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Event not found"
)
if event.creator != user:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Operation not allowed"
)
event = await event_database.delete(id)
return {
"message": "Event deleted successfully."
}
================================================
FILE: ch07/planner/routes/users.py
================================================
from auth.hash_password import HashPassword
from auth.jwt_handler import create_access_token
from database.connection import Database
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from models.users import User, TokenResponse
user_router = APIRouter(
tags=["User"],
)
user_database = Database(User)
hash_password = HashPassword()
@user_router.post("/signup")
async def sign_user_up(user: User) -> dict:
user_exist = await User.find_one(User.email == user.email)
if user_exist:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="User with email provided exists already."
)
hashed_password = hash_password.create_hash(user.password)
user.password = hashed_password
await user_database.save(user)
return {
"message": "User created successfully"
}
@user_router.post("/signin", response_model=TokenResponse)
async def sign_user_in(user: OAuth2PasswordRequestForm = Depends()) -> dict:
user_exist = await User.find_one(User.email == user.username)
if not user_exist:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User with email does not exist."
)
if hash_password.verify_hash(user.password, user_exist.password):
access_token = create_access_token(user_exist.email)
return {
"access_token": access_token,
"token_type": "Bearer"
}
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid details passed."
)
================================================
FILE: ch08/planner/auth/__init__.py
================================================
================================================
FILE: ch08/planner/auth/authenticate.py
================================================
from auth.jwt_handler import verify_access_token
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/user/signin")
async def authenticate(token: str = Depends(oauth2_scheme)) -> str:
if not token:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Sign in for access"
)
decoded_token = await verify_access_token(token)
return decoded_token["user"]
================================================
FILE: ch08/planner/auth/hash_password.py
================================================
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
class HashPassword:
def create_hash(self, password: str) -> str:
return pwd_context.hash(password)
def verify_hash(self, plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)
================================================
FILE: ch08/planner/auth/jwt_handler.py
================================================
import time
from datetime import datetime
from database.connection import Settings
from fastapi import HTTPException, status
from jose import jwt, JWTError
from models.users import User
settings = Settings()
def create_access_token(user: str) -> str:
payload = {
"user": user,
"expires": time.time() + 3600
}
token = jwt.encode(payload, settings.SECRET_KEY, algorithm="HS256")
return token
async def verify_access_token(token: str) -> dict:
try:
data = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"])
expire = data.get("expires")
if expire is None:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="No access token supplied"
)
if datetime.utcnow() > datetime.utcfromtimestamp(expire):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Token expired!"
)
user_exist = await User.find_one(User.email == data["user"])
if not user_exist:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid token"
)
return data
except JWTError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid token"
)
================================================
FILE: ch08/planner/database/__init__.py
================================================
================================================
FILE: ch08/planner/database/connection.py
================================================
from typing import Optional, Any, List
from beanie import init_beanie, PydanticObjectId
from models.events import Event
from models.users import User
from motor.motor_asyncio import AsyncIOMotorClient
from pydantic import BaseSettings, BaseModel
class Settings(BaseSettings):
DATABASE_URL: Optional[str] = None
SECRET_KEY: Optional[str] = "default"
async def initialize_database(self):
client = AsyncIOMotorClient(self.DATABASE_URL)
await init_beanie(database=client.get_default_database(),
document_models=[Event, User])
class Config:
env_file = ".env"
class Database:
def __init__(self, model):
self.model = model
async def save(self, document):
await document.create()
return
async def get(self, id: PydanticObjectId) -> bool:
doc = await self.model.get(id)
if doc:
return doc
return False
async def get_all(self) -> List[Any]:
docs = await self.model.find_all().to_list()
return docs
async def update(self, id: PydanticObjectId, body: BaseModel) -> Any:
doc_id = id
des_body = body.dict()
des_body = {k: v for k, v in des_body.items() if v is not None}
update_query = {"$set": {
field: value for field, value in des_body.items()
}}
doc = await self.get(doc_id)
if not doc:
return False
await doc.update(update_query)
return doc
async def delete(self, id: PydanticObjectId) -> bool:
doc = await self.get(id)
if not doc:
return False
await doc.delete()
return True
================================================
FILE: ch08/planner/main.py
================================================
import uvicorn
from fastapi import FastAPI
from fastapi.responses import RedirectResponse
from fastapi.middleware.cors import CORSMiddleware
from database.connection import Settings
from routes.events import event_router
from routes.users import user_router
app = FastAPI()
settings = Settings()
# register origins
origins = ["*"]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Register routes
app.include_router(user_router, prefix="/user")
app.include_router(event_router, prefix="/event")
@app.on_event("startup")
async def init_db():
await settings.initialize_database()
@app.get("/")
async def home():
return RedirectResponse(url="/event/")
if __name__ == '__main__':
uvicorn.run("main:app", host="0.0.0.0", port=8080, reload=True)
================================================
FILE: ch08/planner/models/__init__.py
================================================
================================================
FILE: ch08/planner/models/events.py
================================================
from typing import Optional, List
from beanie import Document
from pydantic import BaseModel
class Event(Document):
creator: Optional[str]
title: str
image: str
description: str
tags: List[str]
location: str
class Config:
schema_extra = {
"example": {
"title": "FastAPI Book Launch",
"image": "https://linktomyimage.com/image.png",
"description": "We will be discussing the contents of the FastAPI book in this event.Ensure to come with your own copy to win gifts!",
"tags": ["python", "fastapi", "book", "launch"],
"location": "Google Meet"
}
}
class Settings:
name = "events"
class EventUpdate(BaseModel):
title: Optional[str]
image: Optional[str]
description: Optional[str]
tags: Optional[List[str]]
location: Optional[str]
class Config:
schema_extra = {
"example": {
"title": "FastAPI BookLaunch",
"image": "https://linktomyimage.com/image.png",
"description": "We will be discussing the contents of the FastAPI book in this event.Ensure to come with your own copy to win gifts!",
"tags": ["python", "fastapi", "book", "launch"],
"location": "Google Meet"
}
}
================================================
FILE: ch08/planner/models/users.py
================================================
from beanie import Document
from pydantic import BaseModel, EmailStr
class User(Document):
email: EmailStr
password: str
class Settings:
name = "users"
class Config:
schema_extra = {
"example": {
"email": "fastapi@packt.com",
"password": "strong!!!"
}
}
class TokenResponse(BaseModel):
access_token: str
token_type: str
================================================
FILE: ch08/planner/pytest.ini
================================================
[pytest]
asyncio_mode=auto
================================================
FILE: ch08/planner/requirements.txt
================================================
anyio==3.5.0
asgi-lifespan==1.0.1
asgiref==3.5.0
attrs==21.4.0
bcrypt==3.2.2
beanie==1.11.0
certifi==2021.10.8
cffi==1.15.0
charset-normalizer==2.0.12
click==8.0.4
coverage==6.3.3
cryptography==36.0.2
dnspython==2.2.1
ecdsa==0.17.0
email-validator==1.1.3
fastapi==0.77.1
h11==0.12.0
httpcore==0.14.7
httpx==0.22.0
idna==3.3
iniconfig==1.1.1
Jinja2==3.0.3
MarkupSafe==2.1.0
motor==3.0.0
multidict==6.0.2
packaging==21.3
passlib==1.7.4
pluggy==1.0.0
py==1.11.0
pyasn1==0.4.8
pycparser==2.21
pydantic==1.9.0
pymongo==4.1.1
pyparsing==3.0.9
pytest==7.1.2
pytest-asyncio==0.18.3
python-dotenv==0.20.0
python-jose==3.3.0
python-multipart==0.0.5
rfc3986==1.5.0
rsa==4.8
six==1.16.0
sniffio==1.2.0
SQLAlchemy==1.4.32
sqlalchemy2-stubs==0.0.2a20
sqlmodel==0.0.6
starlette==0.19.1
toml==0.10.2
tomli==2.0.1
typing_extensions==4.1.1
uvicorn==0.17.6
yarl==1.7.2
================================================
FILE: ch08/planner/routes/__init__.py
================================================
================================================
FILE: ch08/planner/routes/events.py
================================================
from typing import List
from auth.authenticate import authenticate
from beanie import PydanticObjectId
from database.connection import Database
from fastapi import APIRouter, Depends, HTTPException, status
from models.events import Event, EventUpdate
event_router = APIRouter(
tags=["Events"]
)
event_database = Database(Event)
@event_router.get("/", response_model=List[Event])
async def retrieve_all_events() -> List[Event]:
events = await event_database.get_all()
return events
@event_router.get("/{id}", response_model=Event)
async def retrieve_event(id: PydanticObjectId) -> Event:
event = await event_database.get(id)
if not event:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Event with supplied ID does not exist"
)
return event
@event_router.post("/new")
async def create_event(body: Event, user: str = Depends(authenticate)) -> dict:
body.creator = user
await event_database.save(body)
return {
"message": "Event created successfully"
}
@event_router.put("/{id}", response_model=Event)
async def update_event(id: PydanticObjectId, body: EventUpdate, user: str = Depends(authenticate)) -> Event:
event = await event_database.get(id)
if event.creator != user:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Operation not allowed"
)
updated_event = await event_database.update(id, body)
if not updated_event:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Event with supplied ID does not exist"
)
return updated_event
@event_router.delete("/{id}")
async def delete_event(id: PydanticObjectId, user: str = Depends(authenticate)) -> dict:
event = await event_database.get(id)
if event.creator != user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Event not found"
)
if not event:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Event with supplied ID does not exist"
)
await event_database.delete(id)
return {
"message": "Event deleted successfully."
}
================================================
FILE: ch08/planner/routes/users.py
================================================
from auth.hash_password import HashPassword
from auth.jwt_handler import create_access_token
from database.connection import Database
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from models.users import User, TokenResponse
user_router = APIRouter(
tags=["User"],
)
user_database = Database(User)
hash_password = HashPassword()
@user_router.post("/signup")
async def sign_user_up(user: User) -> dict:
user_exist = await User.find_one(User.email == user.email)
if user_exist:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="User with email provided exists already."
)
hashed_password = hash_password.create_hash(user.password)
user.password = hashed_password
await user_database.save(user)
return {
"message": "User created successfully"
}
@user_router.post("/signin", response_model=TokenResponse)
async def sign_user_in(user: OAuth2PasswordRequestForm = Depends()) -> dict:
user_exist = await User.find_one(User.email == user.username)
if not user_exist:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User with email does not exist."
)
if hash_password.verify_hash(user.password, user_exist.password):
access_token = create_access_token(user_exist.email)
return {
"access_token": access_token,
"token_type": "Bearer"
}
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid details passed."
)
================================================
FILE: ch08/planner/tests/conftest.py
================================================
import asyncio
import httpx
import pytest
from database.connection import Settings
from main import app
from models.events import Event
from models.users import User
@pytest.fixture(scope="session")
def event_loop():
loop = asyncio.get_event_loop()
yield loop
loop.close()
async def init_db():
test_settings = Settings()
test_settings.DATABASE_URL = "mongodb://localhost:27017/testdb"
await test_settings.initialize_database()
@pytest.fixture(scope="session")
async def default_client():
await init_db()
async with httpx.AsyncClient(app=app, base_url="http://app") as client:
yield client
# Clean up resources
await Event.find_all().delete()
await User.find_all().delete()
================================================
FILE: ch08/planner/tests/test_arthmetic_operations.py
================================================
def add(a: int, b: int) -> int:
return a + b
def subtract(a: int, b: int) -> int:
return b - a
def multiply(a: int, b: int) -> int:
return a * b
def divide(a: int, b: int) -> int:
return b // a
def test_add() -> None:
assert add(1, 1) == 2
def test_subtract() -> None:
assert subtract(2, 5) == 3
def test_multiply() -> None:
assert multiply(10, 10) == 100
def test_divide() -> None:
assert divide(25, 100) == 4
================================================
FILE: ch08/planner/tests/test_fixture.py
================================================
import pytest
# Fixture is defined.
from models.events import EventUpdate
@pytest.fixture
def event() -> EventUpdate:
return EventUpdate(
title="FastAPI Book Launch",
image="https://packt.com/fastapi.png",
description="We will be discussing the contents of the FastAPI book in this event.Ensure to come with your own copy to win gifts!",
tags=["python", "fastapi", "book", "launch"],
location="Google Meet"
)
def test_event_name(event: EventUpdate) -> None:
assert event.title == "FastAPI Book Launch"
================================================
FILE: ch08/planner/tests/test_login.py
================================================
import httpx
import pytest
@pytest.mark.asyncio
async def test_sign_new_user(default_client: httpx.AsyncClient) -> None:
payload = {
"email": "testuser@packt.com",
"password": "testpassword",
}
headers = {
"accept": "application/json",
"Content-Type": "application/json"
}
test_response = {
"message": "User created successfully"
}
response = await default_client.post("/user/signup", json=payload, headers=headers)
assert response.status_code == 200
assert response.json() == test_response
@pytest.mark.asyncio
async def test_sign_user_in(default_client: httpx.AsyncClient) -> None:
payload = {
"username": "testuser@packt.com",
"password": "testpassword"
}
headers = {
"accept": "application/json",
"Content-Type": "application/x-www-form-urlencoded"
}
response = await default_client.post("/user/signin", data=payload, headers=headers)
assert response.status_code == 200
assert response.json()["token_type"] == "Bearer"
================================================
FILE: ch08/planner/tests/test_routes.py
================================================
import httpx
import pytest
from auth.jwt_handler import create_access_token
from models.events import Event
@pytest.fixture(scope="module")
async def access_token() -> str:
return create_access_token("testuser@packt.com")
@pytest.fixture(scope="module")
async def mock_event() -> Event:
new_event = Event(
creator="testuser@packt.com",
title="FastAPI Book Launch",
image="https://linktomyimage.com/image.png",
description="We will be discussing the contents of the FastAPI book in this event.Ensure to come with your own copy to win gifts!",
tags=["python", "fastapi", "book", "launch"],
location="Google Meet"
)
await Event.insert_one(new_event)
yield new_event
@pytest.mark.asyncio
async def test_get_events(default_client: httpx.AsyncClient, mock_event: Event) -> None:
response = await default_client.get("/event/")
assert response.status_code == 200
assert response.json()[0]["_id"] == str(mock_event.id)
@pytest.mark.asyncio
async def test_get_event(default_client: httpx.AsyncClient, mock_event: Event) -> None:
url = f"/event/{str(mock_event.id)}"
response = await default_client.get(url)
assert response.status_code == 200
assert response.json()["creator"] == mock_event.creator
assert response.json()["_id"] == str(mock_event.id)
@pytest.mark.asyncio
async def test_post_event(default_client: httpx.AsyncClient, access_token: str) -> None:
payload = {
"title": "FastAPI Book Launch",
"image": "https://linktomyimage.com/image.png",
"description": "We will be discussing the contents of the FastAPI book in this event.Ensure to come with your own copy to win gifts!",
"tags": [
"python",
"fastapi",
"book",
"launch"
],
"location": "Google Meet",
}
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {access_token}"
}
test_response = {
"message": "Event created successfully"
}
response = await default_client.post("/event/new", json=payload, headers=headers)
assert response.status_code == 200
assert response.json() == test_response
@pytest.mark.asyncio
async def test_get_events_count(default_client: httpx.AsyncClient) -> None:
response = await default_client.get("/event/")
events = response.json()
assert response.status_code == 200
assert len(events) == 2
@pytest.mark.asyncio
async def test_update_event(default_client: httpx.AsyncClient, mock_event: Event, access_token: str) -> None:
test_payload = {
"title": "Updated FastAPI event"
}
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {access_token}"
}
url = f"/event/{str(mock_event.id)}"
response = await default_client.put(url, json=test_payload, headers=headers)
assert response.status_code == 200
assert response.json()["title"] == test_payload["title"]
@pytest.mark.asyncio
async def test_delete_event(default_client: httpx.AsyncClient, mock_event: Event, access_token: str) -> None:
test_response = {
"message": "Event deleted successfully."
}
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {access_token}"
}
url = f"/event/{mock_event.id}"
response = await default_client.delete(url, headers=headers)
assert response.status_code == 200
assert response.json() == test_response
@pytest.mark.asyncio
async def test_get_event_again(default_client: httpx.AsyncClient, mock_event: Event) -> None:
url = f"/event/{str(mock_event.id)}"
response = await default_client.get(url)
assert response.status_code == 404
================================================
FILE: ch09/planner/.dockerignore
================================================
venv
.env
.git
================================================
FILE: ch09/planner/Dockerfile
================================================
FROM python:3.10
WORKDIR /app
ADD requirements.txt /app/requirements.txt
RUN pip install --upgrade pip && pip install -r /app/requirements.txt
EXPOSE 8080
COPY ./ /app
CMD ["python", "main.py"]
================================================
FILE: ch09/planner/auth/__init__.py
================================================
================================================
FILE: ch09/planner/auth/authenticate.py
================================================
from auth.jwt_handler import verify_access_token
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/user/signin")
async def authenticate(token: str = Depends(oauth2_scheme)) -> str:
if not token:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Sign in for access"
)
decoded_token = await verify_access_token(token)
return decoded_token["user"]
================================================
FILE: ch09/planner/auth/hash_password.py
================================================
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
class HashPassword:
def create_hash(self, password: str) -> str:
return pwd_context.hash(password)
def verify_hash(self, plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)
================================================
FILE: ch09/planner/auth/jwt_handler.py
================================================
import time
from datetime import datetime
from database.connection import Settings
from fastapi import HTTPException, status
from jose import jwt, JWTError
from models.users import User
settings = Settings()
def create_access_token(user: str) -> str:
payload = {
"user": user,
"expires": time.time() + 3600
}
token = jwt.encode(payload, settings.SECRET_KEY, algorithm="HS256")
return token
async def verify_access_token(token: str) -> dict:
try:
data = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"])
expire = data.get("expires")
if expire is None:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="No access token supplied"
)
if datetime.utcnow() > datetime.utcfromtimestamp(expire):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Token expired!"
)
user_exist = await User.find_one(User.email == data["user"])
if not user_exist:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid token"
)
return data
except JWTError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid token"
)
================================================
FILE: ch09/planner/database/__init__.py
================================================
================================================
FILE: ch09/planner/database/connection.py
================================================
from typing import Optional, Any, List
from beanie import init_beanie, PydanticObjectId
from models.events import Event
from models.users import User
from motor.motor_asyncio import AsyncIOMotorClient
from pydantic import BaseSettings, BaseModel
class Settings(BaseSettings):
DATABASE_URL: Optional[str] = None
SECRET_KEY: Optional[str] = "default"
async def initialize_database(self):
client = AsyncIOMotorClient(self.DATABASE_URL)
await init_beanie(database=client.get_default_database(),
document_models=[Event, User])
class Config:
env_file = ".env"
class Database:
def __init__(self, model):
self.model = model
async def save(self, document):
await document.create()
return
async def get(self, id: PydanticObjectId) -> bool:
doc = await self.model.get(id)
if doc:
return doc
return False
async def get_all(self) -> List[Any]:
docs = await self.model.find_all().to_list()
return docs
async def update(self, id: PydanticObjectId, body: BaseModel) -> Any:
doc_id = id
des_body = body.dict()
des_body = {k: v for k, v in des_body.items() if v is not None}
update_query = {"$set": {
field: value for field, value in des_body.items()
}}
doc = await self.get(doc_id)
if not doc:
return False
await doc.update(update_query)
return doc
async def delete(self, id: PydanticObjectId) -> bool:
doc = await self.get(id)
if not doc:
return False
await doc.delete()
return True
================================================
FILE: ch09/planner/docker-compose.yml
================================================
version: "3"
services:
api:
build: .
image: event-planner-api:latest
ports:
- "8080:8080"
env_file:
- .env.prod
database:
image: mongo
ports:
- "27017"
volumes:
- data:/data/db
volumes:
data:
================================================
FILE: ch09/planner/main.py
================================================
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import RedirectResponse
from database.connection import Settings
from routes.users import user_router
from routes.events import event_router
import uvicorn
app = FastAPI()
settings = Settings()
# register origins
origins = ["*"]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Register routes
app.include_router(user_router, prefix="/user")
app.include_router(event_router, prefix="/event")
@app.on_event("startup")
async def init_db():
await settings.initialize_database()
@app.get("/")
async def home():
return RedirectResponse(url="/event/")
if __name__ == '__main__':
uvicorn.run("main:app", host="0.0.0.0", port=8080, reload=True)
================================================
FILE: ch09/planner/models/__init__.py
================================================
================================================
FILE: ch09/planner/models/events.py
================================================
from typing import Optional, List
from beanie import Document
from pydantic import BaseModel
class Event(Document):
creator: Optional[str]
title: str
image: str
description: str
tags: List[str]
location: str
class Config:
schema_extra = {
"example": {
"title": "FastAPI Book Launch",
"image": "https://linktomyimage.com/image.png",
"description": "We will be discussing the contents of the FastAPI book in this event.Ensure to come with your own copy to win gifts!",
"tags": ["python", "fastapi", "book", "launch"],
"location": "Google Meet"
}
}
class Settings:
name = "events"
class EventUpdate(BaseModel):
title: Optional[str]
image: Optional[str]
description: Optional[str]
tags: Optional[List[str]]
location: Optional[str]
class Config:
schema_extra = {
"example": {
"title": "FastAPI BookLaunch",
"image": "https://linktomyimage.com/image.png",
"description": "We will be discussing the contents of the FastAPI book in this event.Ensure to come with your own copy to win gifts!",
"tags": ["python", "fastapi", "book", "launch"],
"location": "Google Meet"
}
}
================================================
FILE: ch09/planner/models/users.py
================================================
from beanie import Document
from pydantic import BaseModel, EmailStr
class User(Document):
email: EmailStr
password: str
class Settings:
name = "users"
class Config:
schema_extra = {
"example": {
"email": "fastapi@packt.com",
"password": "strong!!!"
}
}
class TokenResponse(BaseModel):
access_token: str
token_type: str
================================================
FILE: ch09/planner/pytest.ini
================================================
[pytest]
asyncio_mode=auto
================================================
FILE: ch09/planner/requirements.txt
================================================
fastapi==0.78.0
bcrypt==3.2.2
beanie==1.11.1
email-validator==1.2.1
httpx==0.22.0
Jinja2==3.0.3
motor==2.5.1
passlib==1.7.4
pytest==7.1.2
python-multipart==.0.0.5
python-dotenv==0.20.0
python-jose==3.3.0
sqlmodel==0.0.6
uvicorn==0.17.6
================================================
FILE: ch09/planner/routes/__init__.py
================================================
================================================
FILE: ch09/planner/routes/events.py
================================================
from typing import List
from auth.authenticate import authenticate
from beanie import PydanticObjectId
from database.connection import Database
from fastapi import APIRouter, Depends, HTTPException, status
from models.events import Event, EventUpdate
event_router = APIRouter(
tags=["Events"]
)
event_database = Database(Event)
@event_router.get("/", response_model=List[Event])
async def retrieve_all_events() -> List[Event]:
events = await event_database.get_all()
return events
@event_router.get("/{id}", response_model=Event)
async def retrieve_event(id: PydanticObjectId) -> Event:
event = await event_database.get(id)
if not event:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Event with supplied ID does not exist"
)
return event
@event_router.post("/new")
async def create_event(body: Event, user: str = Depends(authenticate)) -> dict:
body.creator = user
await event_database.save(body)
return {
"message": "Event created successfully"
}
@event_router.put("/{id}", response_model=Event)
async def update_event(id: PydanticObjectId, body: EventUpdate, user: str = Depends(authenticate)) -> Event:
event = await event_database.get(id)
if event.creator != user:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Operation not allowed"
)
updated_event = await event_database.update(id, body)
if not updated_event:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Event with supplied ID does not exist"
)
return updated_event
@event_router.delete("/{id}")
async def delete_event(id: PydanticObjectId, user: str = Depends(authenticate)) -> dict:
event = await event_database.get(id)
if event.creator != user:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Operation not allowed"
)
if not event:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Event with supplied ID does not exist"
)
await event_database.delete(id)
return {
"message": "Event deleted successfully."
}
================================================
FILE: ch09/planner/routes/users.py
================================================
from auth.hash_password import HashPassword
from auth.jwt_handler import create_access_token
from database.connection import Database
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from models.users import User, TokenResponse
user_router = APIRouter(
tags=["User"],
)
user_database = Database(User)
hash_password = HashPassword()
@user_router.post("/signup")
async def sign_user_up(user: User) -> dict:
user_exist = await User.find_one(User.email == user.email)
if user_exist:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="User with email provided exists already."
)
hashed_password = hash_password.create_hash(user.password)
user.password = hashed_password
await user_database.save(user)
return {
"message": "User created successfully"
}
@user_router.post("/signin", response_model=TokenResponse)
async def sign_user_in(user: OAuth2PasswordRequestForm = Depends()) -> dict:
user_exist = await User.find_one(User.email == user.username)
if not user_exist:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User with email does not exist."
)
if hash_password.verify_hash(user.password, user_exist.password):
access_token = create_access_token(user_exist.email)
return {
"access_token": access_token,
"token_type": "Bearer"
}
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid details passed."
)
================================================
FILE: ch09/planner/tests/conftest.py
================================================
import asyncio
import httpx
import pytest
from database.connection import Settings
from main import app
from models.events import Event
from models.users import User
@pytest.fixture(scope="session")
def event_loop():
loop = asyncio.get_event_loop()
yield loop
loop.close()
async def init_db():
test_settings = Settings()
test_settings.DATABASE_URL = "mongodb://localhost:27017/testdb"
await test_settings.initialize_database()
@pytest.fixture(scope="session")
async def default_client():
await init_db()
async with httpx.AsyncClient(app=app, base_url="http://app") as client:
yield client
# Clean up resources
await Event.find_all().delete()
await User.find_all().delete()
================================================
FILE: ch09/planner/tests/test_arthmetic_operations.py
================================================
def add(a: int, b: int) -> int:
return a + b
def subtract(a: int, b: int) -> int:
return b - a
def multiply(a: int, b: int) -> int:
return a * b
def divide(a: int, b: int) -> int:
return b // a
def test_add() -> None:
assert add(1, 1) == 2
def test_subtract() -> None:
assert subtract(2, 5) == 3
def test_multiply() -> None:
assert multiply(10, 10) == 100
def test_divide() -> None:
assert divide(25, 100) == 4
================================================
FILE: ch09/planner/tests/test_fixture.py
================================================
import pytest
# Fixture is defined.
from models.events import EventUpdate
@pytest.fixture
def event() -> EventUpdate:
return EventUpdate(
title="FastAPI Book Launch",
image="https://packt.com/fastapi.png",
description="We will be discussing the contents of the FastAPI book in this event.Ensure to come with your own copy to win gifts!",
tags=["python", "fastapi", "book", "launch"],
location="Google Meet"
)
def test_event_name(event: EventUpdate) -> None:
assert event.title == "FastAPI Book Launch"
================================================
FILE: ch09/planner/tests/test_login.py
================================================
import httpx
import pytest
@pytest.mark.asyncio
async def test_sign_new_user(default_client: httpx.AsyncClient) -> None:
payload = {
"email": "testuser@packt.com",
"password": "testpassword",
}
headers = {
"accept": "application/json",
"Content-Type": "application/json"
}
test_response = {
"message": "User created successfully"
}
response = await default_client.post("/user/signup", json=payload, headers=headers)
assert response.status_code == 200
assert response.json() == test_response
@pytest.mark.asyncio
async def test_sign_user_in(default_client: httpx.AsyncClient) -> None:
payload = {
"username": "testuser@packt.com",
"password": "testpassword"
}
headers = {
"accept": "application/json",
"Content-Type": "application/x-www-form-urlencoded"
}
response = await default_client.post("/user/signin", data=payload, headers=headers)
assert response.status_code == 200
assert response.json()["token_type"] == "Bearer"
================================================
FILE: ch09/planner/tests/test_routes.py
================================================
import httpx
import pytest
from auth.jwt_handler import create_access_token
from models.events import Event
@pytest.fixture(scope="module")
async def access_token() -> str:
return create_access_token("testuser@packt.com")
@pytest.fixture(scope="module")
async def mock_event() -> Event:
new_event = Event(
creator="testuser@packt.com",
title="FastAPI Book Launch",
image="https://linktomyimage.com/image.png",
description="We will be discussing the contents of the FastAPI book in this event.Ensure to come with your own copy to win gifts!",
tags=["python", "fastapi", "book", "launch"],
location="Google Meet"
)
await Event.insert_one(new_event)
yield new_event
@pytest.mark.asyncio
async def test_get_events(default_client: httpx.AsyncClient, mock_event: Event) -> None:
response = await default_client.get("/event/")
assert response.status_code == 200
assert response.json()[0]["_id"] == str(mock_event.id)
@pytest.mark.asyncio
async def test_get_event(default_client: httpx.AsyncClient, mock_event: Event) -> None:
url = f"/event/{str(mock_event.id)}"
response = await default_client.get(url)
assert response.status_code == 200
assert response.json()["creator"] == mock_event.creator
assert response.json()["_id"] == str(mock_event.id)
@pytest.mark.asyncio
async def test_post_event(default_client: httpx.AsyncClient, access_token: str) -> None:
payload = {
"title": "FastAPI Book Launch",
"image": "https://linktomyimage.com/image.png",
"description": "We will be discussing the contents of the FastAPI book in this event.Ensure to come with your own copy to win gifts!",
"tags": [
"python",
"fastapi",
"book",
"launch"
],
"location": "Google Meet",
}
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {access_token}"
}
test_response = {
"message": "Event created successfully"
}
response = await default_client.post("/event/new", json=payload, headers=headers)
assert response.status_code == 200
assert response.json() == test_response
@pytest.mark.asyncio
async def test_get_events_count(default_client: httpx.AsyncClient) -> None:
response = await default_client.get("/event/")
events = response.json()
assert response.status_code == 200
assert len(events) == 2
@pytest.mark.asyncio
async def test_update_event(default_client: httpx.AsyncClient, mock_event: Event, access_token: str) -> None:
test_payload = {
"title": "Updated FastAPI event"
}
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {access_token}"
}
url = f"/event/{str(mock_event.id)}"
response = await default_client.put(url, json=test_payload, headers=headers)
assert response.status_code == 200
assert response.json()["title"] == test_payload["title"]
@pytest.mark.asyncio
async def test_delete_event(default_client: httpx.AsyncClient, mock_event: Event, access_token: str) -> None:
test_response = {
"message": "Event deleted successfully."
}
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {access_token}"
}
url = f"/event/{mock_event.id}"
response = await default_client.delete(url, headers=headers)
assert response.status_code == 200
assert response.json() == test_response
@pytest.mark.asyncio
async def test_get_event_again(default_client: httpx.AsyncClient, mock_event: Event) -> None:
url = f"/event/{str(mock_event.id)}"
response = await default_client.get(url)
assert response.status_code == 404