0%

fastapi 筆記

 

helloworld

建立環境

1
python -m venv fastapienv

啟動

1
.\fastapienv\Scripts\Activate.ps1

退出

1
deactivate

pip 安裝

1
2
3
pip install fastapi
pip install "uvicorn[standard]"
pip list

conda 安裝

1
2
conda install fastapi
conda install uvicorn-standard

啟動 uvicorn web server

1
uvicorn books:app --reload

另一種啟動方法

1
fastapi run books.py

預設會在 http://127.0.0.1:8000/ 這個路徑進去的話會給這樣

1
{"detail":"Not Found"}

想要進去 swagger 路徑要這樣寫 http://127.0.0.1:8000/docs
另外還有一套 redoc http://127.0.0.1:8000/redoc

\ 表示換行

path parameter = .net 的 route parameter
query parameter = query parameter

post 要先 import 才可以使用

1
from fastapi import FastAPI, Path, Query, HTTPException, Body

Body() = FromBody

@app.post(“qq”)
async def QQ(xxx=Body()) = FromBody

post json 必須要使用雙引號

uvicorn 不正常關閉

這問題滿鳥的, 直接把 python 砍了就好, 不然找 process 起來也是砍不到

1
2
3
4
5
6
7
netstat -ano | findstr :8000

TCP 127.0.0.1:8000 0.0.0.0:0 LISTENING 5944


🌹 taskkill /PID 5944 /F
錯誤: 找不到處理程序 "5944"。

Pydantic

Pydantic => 驗證資料用的咚咚

可以在 Field 加上想要驗證的設定即可
需要引用以下的咚咚

1
from pydantic import BaseModel, Field

model_config 則是會出現在 swagger 上面的說明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
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):
id: Optional[int] = Field(description='ID is not needed on create', default=None)
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)

model_config = {
"json_schema_extra": {
"example": {
"title": "A new book",
"author": "codingwithroby",
"description": "A new description of a book",
"rating": 5,
'published_date': 2029
}
}
}


@app.post("/create-book", status_code=status.HTTP_201_CREATED)
async def create_book(book_request: BookRequest):
new_book = Book(**book_request.model_dump())
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

sqlalchemy 煉金術

先到 https://www.sqlite.org/download.html 官網下載管理工具 sqlite-tools-win-x64-3490100.zip 然後 rename 為 sqlite3 放到 c 底下
接著用 win + env 可以快速叫出環境變數編輯的介面, 這裡要選系統的, 不要選成自己帳號, 將 C:\sqlite3 加入 path
用以下方是開啟 db

1
sqlite3 todos.db

.schema 命令則可以看到目前資料表結構
.mode column 則可以變換輸出模式, 他還支援 markdown 等一狗票輸出滿酷

1
2
3
4
5
6
7
8
9
10
sqlite> .schema
CREATE TABLE todos (
id INTEGER NOT NULL,
title VARCHAR,
description VARCHAR,
priority INTEGER,
complete BOOLEAN,
PRIMARY KEY (id)
);
CREATE INDEX ix_todos_id ON todos (id);

接著隨便新增 5 筆資料

1
2
3
4
5
6
7
8
9
10
11
12
13
INSERT INTO todos (id, title, description, priority, complete) VALUES (1, 'Buy groceries', 'Milk, Bread, Eggs', 2, 0);
INSERT INTO todos (id, title, description, priority, complete) VALUES (2, 'Workout', 'Gym session at 6 PM', 3, 0);
INSERT INTO todos (id, title, description, priority, complete) VALUES (3, 'Read book', 'Finish reading Chapter 4', 1, 1);
INSERT INTO todos (id, title, description, priority, complete) VALUES (4, 'Call Alice', 'Discuss weekend plans', 2, 0);
INSERT INTO todos (id, title, description, priority, complete) VALUES (5, 'Pay bills', 'Electricity and Internet', 3, 1);

| id | title | description | priority | complete |
|----|---------------|--------------------------|----------|----------|
| 1 | Buy groceries | Milk, Bread, Eggs | 2 | 0 |
| 2 | Workout | Gym session at 6 PM | 3 | 0 |
| 3 | Read book | Finish reading Chapter 4 | 1 | 1 |
| 4 | Call Alice | Discuss weekend plans | 2 | 0 |
| 5 | Pay bills | Electricity and Internet | 3 | 1 |

安裝

1
conda install sqlalchemy

新增 TodoApp 資料夾, 加入以下檔案

database.py

1
2
3
4
5
6
7
8
9
10
11
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base

SQLALCHEMY_DATABASE_URL = 'sqlite:///./todosapp.db'

engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={'check_same_thread': False})

SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

Base = declarative_base()

models.py
這邊定義欄位型別, 水應該也很深

1
2
3
4
5
6
7
8
9
10
11
12
from database import Base
from sqlalchemy import Column, Integer , String , Boolean


class Todos(Base):
__tablename__ = 'todos'

id = Column(Integer, primary_key=True, index=True)
title = Column(String)
description = Column(String)
priority = Column(Integer)
complete = Column(Boolean, default=False)

main.py
這裡執行這句 models.Base.metadata.create_all(bind=engine) 以後, 然後啟動 uvicorn main:app --reload 就會出現 todo.db
依照以前用 ef 的經驗這種咚咚就是難用 LOL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from typing import Annotated
from sqlalchemy.orm import Session
from fastapi import FastAPI , Depends

import models
from models import Todos
from database import engine, SessionLocal

app = FastAPI()

models.Base.metadata.create_all(bind=engine)

def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()

@app.get("/")
async def read_all(db:Annotated[Session, Depends(get_db)]):
return db.query(Todos).all()

接著可以建立以 id 查詢的函數

1
2
3
4
5
6
@app.get("/todo/{todo_id}", status_code=status.HTTP_200_OK)
async def read_todo(db: db_dependency, todo_id: int = Path(gt=0)):
todo_model = db.query(Todos).filter(Todos.id == todo_id).first()
if todo_model is not None:
return todo_model
raise HTTPException(status_code=404, detail='Todo not found')

post

1
2
3
4
5
6
7
8
9
10
11
12
class TodoRequest(BaseModel):
title: str = Field(min_length=3)
description: str = Field(min_length=3, max_length=100)
priority: int = Field(gt=0, lt=6)
complete: bool


@app.post("/todo", status_code=status.HTTP_201_CREATED)
async def create_todo(db: db_dependency, todo_request: TodoRequest):
todo_model = Todos(**todo_request.dict())
db.add(todo_model)
db.commit()

put

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@app.put("/todo",status_code=status.HTTP_204_NO_CONTENT)
async def update_todo(db:db_dependency, todo_id:int, todo_request:TodoRequest):
todo_model = db.query(Todos).filter(Todos.id == todo_id).first()

if todo_model is None:
raise HTTPException(status_code=404, detail='Todo not found')

todo_model.title = todo_request.title
todo_model.description = todo_request.description
todo_model.priority = todo_request.priority
todo_model.complete = todo_request.complete

db.add(todo_model)
db.commit()

delete

1
2
3
4
5
6
7
@app.delete("/todo", status_code=status.HTTP_204_NO_CONTENT)
async def delete_todo(db: db_dependency, todo_id: int = Path(gt=0)):
todo_model = db.query(Todos).filter(Todos.id == todo_id).first()
if todo_model is None:
raise HTTPException(status_code=404, detail='Todo not found')
db.query(Todos).filter(Todos.id == todo_id).delete()
db.commit()

Authentication & Authorization

先建立 package routers 接著新增 auth.py 在該目錄底下

1
2
3
4
5
6
7
from fastapi import FastAPI , APIRouter

router = APIRouter()

@router.get("/auth/")
async def auth():
return {"user": "authenticated"}

回到 main.py 修改並且引用剛剛建立的 routers

1
2
3
4
5
6
7
from routers import auth

app = FastAPI()

models.Base.metadata.create_all(bind=engine)

app.include_router(auth.router)

最後啟動即可

1
uvicorn main:app --reload

接著來重構內容, 加入 todos.pymain.py 內容複製並修改如下
主要異動就是把 app 改成 APIRouter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
from typing import Annotated, cast

from pydantic import BaseModel, Field
from sqlalchemy.orm import Session
from fastapi import APIRouter, Depends, HTTPException, status, Path

from models import Todos
from database import SessionLocal

router = APIRouter()

def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()


db_dependency = Annotated[Session, Depends(get_db)]


class TodoRequest(BaseModel):
title: str = Field(min_length=3)
description: str = Field(min_length=3, max_length=100)
priority: int = Field(gt=0, lt=6)
complete: bool


@router.get("/")
async def read_all(db: db_dependency):
return db.query(Todos).all()


@router.get("/todo/{todo_id}", status_code=status.HTTP_200_OK)
async def read_todo(db: db_dependency, todo_id: int = Path(gt=0)):
todo_model = db.query(Todos).filter(cast("ColumnElement[bool]", Todos.id == todo_id)).first()
if todo_model is not None:
return todo_model
raise HTTPException(status_code=404, detail='Todo not found')


@router.post("/todo", status_code=status.HTTP_201_CREATED)
async def create_todo(db: db_dependency, todo_request: TodoRequest):
todo_model = Todos(**todo_request.dict())
db.add(todo_model)
db.commit()


@router.put("/todo", status_code=status.HTTP_204_NO_CONTENT)
async def update_todo(db: db_dependency, todo_id: int, todo_request: TodoRequest):
todo_model = db.query(Todos).filter(cast("ColumnElement[bool]", Todos.id == todo_id)).first()

if todo_model is None:
raise HTTPException(status_code=404, detail='Todo not found')

todo_model.title = todo_request.title
todo_model.description = todo_request.description
todo_model.priority = todo_request.priority
todo_model.complete = todo_request.complete

db.add(todo_model)
db.commit()


@router.delete("/todo/{todo_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_todo(db: db_dependency, todo_id: int = Path(gt=0)):
todo_model = db.query(Todos).filter(Todos.id == todo_id).first()
if todo_model is None:
raise HTTPException(status_code=404, detail='Todo not found')
db.query(Todos).filter(Todos.id == todo_id).delete()
db.commit()

並且修正 main.py 加入剛剛的 todos router 並且移除不需要的部分即可完成

1
2
3
4
5
6
7
8
9
10
11
12
from fastapi import FastAPI

import models
from database import engine
from routers import auth , todos

app = FastAPI()

models.Base.metadata.create_all(bind=engine)

app.include_router(auth.router)
app.include_router(todos.router)

接續修改 database.py 把本來的 todos.db 改為 todosapp.db
這裡要注意下, 萬一 uvicorn main:app --reload 還在運作的話會馬上生效

1
SQLALCHEMY_DATABASE_URL = 'sqlite:///./todosapp.db'

接著在 models.py 底下調整程式碼, 主要加入了 Users 這張表, 並且調整 Todos 關聯

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from database import Base
from sqlalchemy import Column, Integer, String, Boolean, ForeignKey


class Users(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True, index=True)
email = Column(String, unique=True)
username = Column(String, unique=True)
first_name = Column(String)
last_name = Column(String)
hashed_password = Column(String)
is_active = Column(Boolean, default=True)
role = Column(String)


class Todos(Base):
__tablename__ = 'todos'

id = Column(Integer, primary_key=True, index=True)
title = Column(String)
description = Column(String)
priority = Column(Integer)
complete = Column(Boolean, default=False)
owner_id = Column(Integer, ForeignKey("user.id"))

回到 auth.py 修改程式碼, 搞定後就可以測試看看是否正常 post

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from Demos.win32ts_logoff_disconnected import username
from fastapi import FastAPI, APIRouter
from pydantic import BaseModel
from models import Users

router = APIRouter()


class CreateUserRequest(BaseModel):
username: str
email: str
first_name: str
last_name: str
password: str
role: str


@router.post("/auth")
async def create_user(create_user_request: CreateUserRequest):
create_user_model = Users(
email=create_user_request.email,
username=create_user_request.username,
first_name=create_user_request.first_name,
last_name=create_user_request.last_name,
role=create_user_request.role,
hashed_password=create_user_request.password,
is_active=True
)
return create_user_model

接著安裝套件 passlib

1
2
3
conda install passlib
#這個在 conda 找不到所以用 pip 安裝
pip install bcrypt==4.0.1

修改 auth.py 把密碼欄位加密, 並且確實的寫入到 db 內

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
from typing import Annotated

from fastapi import APIRouter, Depends, HTTPException, status, Path
from pydantic import BaseModel

from sqlalchemy.orm import Session

from database import SessionLocal
from models import Users
from passlib.context import CryptContext

router = APIRouter()

bcrypt_context = CryptContext(schemes=['bcrypt'], deprecated='auto')


def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()


db_dependency = Annotated[Session, Depends(get_db)]


class CreateUserRequest(BaseModel):
username: str
email: str
first_name: str
last_name: str
password: str
role: str


@router.post("/auth", status_code=status.HTTP_201_CREATED)
async def create_user(
db: db_dependency,
create_user_request: CreateUserRequest):
create_user_model = Users(
email=create_user_request.email,
username=create_user_request.username,
first_name=create_user_request.first_name,
last_name=create_user_request.last_name,
role=create_user_request.role,
hashed_password=bcrypt_context.hash(create_user_request.password),
is_active=True
)
db.add(create_user_model)
db.commit()

post 以下參數

1
2
3
4
5
6
7
8
{
"username": "qq",
"email": "qq@gmail.com",
"first_name": "qq",
"last_name": "qq",
"password": "qq",
"role": "admin"
}

然後用 sqlite 看看結果

1
2
3
4
sqlite> select * from users;
| id | email | username | first_name | last_name | hashed_password | is_active | role |
|----|--------------|----------|------------|-----------|--------------------------------------------------------------|-----------|-------|
| 1 | qq@gmail.com | qq | qq | qq | $2b$12$/NLBaJB01yCy3sxOg2JwpOGJkZS5gXNgxy1j6sZ190c580Ot0u.j6 | 1 | admin |

接著安裝以下套件

1
conda install python-multipart

引用此命名空間 from fastapi.security import OAuth2PasswordRequestForm 並且調整程式碼

他會出現讓人熟悉的 OAuth 欄位 XD 但只有 username password 必填

grant_type
username
password
scope
client_id
client_secret

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
from typing import Annotated

from fastapi import APIRouter, Depends, HTTPException, status, Path
from pydantic import BaseModel

from sqlalchemy.orm import Session

from database import SessionLocal
from models import Users
from passlib.context import CryptContext
from fastapi.security import OAuth2PasswordRequestForm

router = APIRouter()

bcrypt_context = CryptContext(schemes=['bcrypt'], deprecated='auto')


def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()


db_dependency = Annotated[Session, Depends(get_db)]


class CreateUserRequest(BaseModel):
username: str
email: str
first_name: str
last_name: str
password: str
role: str


@router.post("/auth", status_code=status.HTTP_201_CREATED)
async def create_user(
db: db_dependency,
create_user_request: CreateUserRequest):
create_user_model = Users(
email=create_user_request.email,
username=create_user_request.username,
first_name=create_user_request.first_name,
last_name=create_user_request.last_name,
role=create_user_request.role,
hashed_password=bcrypt_context.hash(create_user_request.password),
is_active=True
)
db.add(create_user_model)
db.commit()


def authenticate_user(username: str, password: str, db):
user = db.query(Users).filter(Users.username == username).first()
if not user:
return False
if not bcrypt_context.verify(password, user.hashed_password):
return False
return True


@router.post("/token")
async def login_for_access_token(
form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
db: db_dependency):
user = authenticate_user(form_data.username, form_data.password, db)
if not user:
return 'Failed Authentication'
return 'Successful Authentication'

接著加入 JWT 功能, 先安裝套件

1
pip install python-jose[cryptography]

加入以下程式碼

1
2
3
4
5
6
7
SECRET_KEY = '0123456789'
ALGORITHM = 'HS256'
def create_access_token(username: str, user_id: int, expires_delta: timedelta):
encode = {'sub': username, 'id': user_id}
expires = datetime.now(timezone.utc) + expires_delta
encode.update({'exp': expires})
return jwt.encode(encode, SECRET_KEY, ALGORITHM)

然後調整 login_for_access_token 相關程式碼, 這裡的 authenticate_user 改為 return user
他這邊如果打錯 username or password 會噴 internal server error 是正常的, 因為他把 response_model=Token 設定這樣, 導致本來隨便回傳有了強制限定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Token(BaseModel):
access_token: str
token_type: str

def authenticate_user(username: str, password: str, db):
user = db.query(Users).filter(Users.username == username).first()
if not user:
return False
if not bcrypt_context.verify(password, user.hashed_password):
return False
return user


@router.post("/token", response_model=Token)
async def login_for_access_token(
form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
db: db_dependency):
user = authenticate_user(form_data.username, form_data.password, db)
if not user:
return 'Failed Authentication'
token = create_access_token(user.username, user.id, timedelta(minutes=20))
return {'access_token': token, 'token_type': 'bearer'}

fullcode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
from datetime import timedelta, datetime, timezone
from typing import Annotated

from fastapi import APIRouter, Depends, HTTPException, status, Path
from pydantic import BaseModel

from sqlalchemy.orm import Session

from database import SessionLocal
from models import Users
from passlib.context import CryptContext
from fastapi.security import OAuth2PasswordRequestForm
from jose import jwt

router = APIRouter()

SECRET_KEY = '0123456789'
ALGORITHM = 'HS256'

bcrypt_context = CryptContext(schemes=['bcrypt'], deprecated='auto')


def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()


db_dependency = Annotated[Session, Depends(get_db)]


class CreateUserRequest(BaseModel):
username: str
email: str
first_name: str
last_name: str
password: str
role: str


class Token(BaseModel):
access_token: str
token_type: str


def create_access_token(username: str, user_id: int, expires_delta: timedelta):
encode = {'sub': username, 'id': user_id}
expires = datetime.now(timezone.utc) + expires_delta
encode.update({'exp': expires})
return jwt.encode(encode, SECRET_KEY, ALGORITHM)


@router.post("/auth", status_code=status.HTTP_201_CREATED)
async def create_user(
db: db_dependency,
create_user_request: CreateUserRequest):
create_user_model = Users(
email=create_user_request.email,
username=create_user_request.username,
first_name=create_user_request.first_name,
last_name=create_user_request.last_name,
role=create_user_request.role,
hashed_password=bcrypt_context.hash(create_user_request.password),
is_active=True
)
db.add(create_user_model)
db.commit()


def authenticate_user(username: str, password: str, db):
user = db.query(Users).filter(Users.username == username).first()
if not user:
return False
if not bcrypt_context.verify(password, user.hashed_password):
return False
return user


@router.post("/token", response_model=Token)
async def login_for_access_token(
form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
db: db_dependency):
user = authenticate_user(form_data.username, form_data.password, db)
if not user:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED,
detail='Could not validate user.')

token = create_access_token(user.username, user.id, timedelta(minutes=20))
return {'access_token': token, 'token_type': 'bearer'}

最後加入以下 decode 程式碼

1
2
3
4
5
6
7
8
9
10
11
12
13
async def get_current_user(token: Annotated[str, Depends(oauth2_bearer)]):
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get('sub')
user_id: int = payload.get('id')
user_role: str = payload.get('role')
if username is None or user_id is None:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED,
detail='Could not validate user.')
return {'username': username, 'id': user_id, 'user_role': user_role}
except JWTError:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED,
detail='Could not validate user.')

重構 router 路徑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
from datetime import timedelta, datetime, timezone
from typing import Annotated

from fastapi import APIRouter, Depends, HTTPException, status, Path
from pydantic import BaseModel
from pygments.lexer import default

from sqlalchemy.orm import Session

from database import SessionLocal
from models import Users
from passlib.context import CryptContext
from fastapi.security import OAuth2PasswordRequestForm, OAuth2PasswordBearer
from jose import jwt, JWTError

router = APIRouter(
prefix="/auth",
tags=['auth']
)

SECRET_KEY = '0123456789'
ALGORITHM = 'HS256'

bcrypt_context = CryptContext(schemes=['bcrypt'], deprecated='auto')
oauth2_bearer = OAuth2PasswordBearer(tokenUrl='auth/token')


def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()


db_dependency = Annotated[Session, Depends(get_db)]


class CreateUserRequest(BaseModel):
username: str
email: str
first_name: str
last_name: str
password: str
role: str


class Token(BaseModel):
access_token: str
token_type: str


def create_access_token(username: str, user_id: int, expires_delta: timedelta):
encode = {'sub': username, 'id': user_id}
expires = datetime.now(timezone.utc) + expires_delta
encode.update({'exp': expires})
return jwt.encode(encode, SECRET_KEY, ALGORITHM)


async def get_current_user(token: Annotated[str, Depends(oauth2_bearer)]):
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get('sub')
user_id: int = payload.get('id')
user_role: str = payload.get('role')
if username is None or user_id is None:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED,
detail='Could not validate user.')
return {'username': username, 'id': user_id, 'user_role': user_role}
except JWTError:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED,
detail='Could not validate user.')



@router.post("/", status_code=status.HTTP_201_CREATED)
async def create_user(
db: db_dependency,
create_user_request: CreateUserRequest):
create_user_model = Users(
email=create_user_request.email,
username=create_user_request.username,
first_name=create_user_request.first_name,
last_name=create_user_request.last_name,
role=create_user_request.role,
hashed_password=bcrypt_context.hash(create_user_request.password),
is_active=True
)
db.add(create_user_model)
db.commit()


def authenticate_user(username: str, password: str, db):
user = db.query(Users).filter(Users.username == username).first()
if not user:
return False
if not bcrypt_context.verify(password, user.hashed_password):
return False
return user


@router.post("/token", response_model=Token)
async def login_for_access_token(
form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
db: db_dependency):
user = authenticate_user(form_data.username, form_data.password, db)
print("user:", user)
if not user:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED,
detail='Could not validate user.')
token = create_access_token(user.username, user.id, timedelta(minutes=20))
print(token)
return {'access_token': token, 'token_type': 'bearer'}

接著調整 todos.py 匯入 get_current_user 方法, 注意這邊要用 .auth 表示目前目錄底下

1
from .auth import get_current_user

接著補上注入, 並調整 create_todo 開 swagger 測試登入後能否新增

1
2
3
4
5
6
7
8
9
10
11
12
user_dependency = Annotated[dict, Depends(get_current_user)]

@router.post("/todo", status_code=status.HTTP_201_CREATED)
async def create_todo(
user: user_dependency,
db: db_dependency,
todo_request: TodoRequest):
if user is None:
raise HTTPException(status_code=401, detail="Authentication Failed")
todo_model = Todos(**todo_request.dict(), owner_id=user.get('id'))
db.add(todo_model)
db.commit()

接著調整 read_all 讓只有該 user 能看到自己的資料

1
2
3
4
5
6
7
@router.get("/")
async def read_all(
user: user_dependency,
db: db_dependency):
if user is None:
raise HTTPException(status_code=401, detail="Authentication Failed")
return db.query(Todos).filter(Todos.owner_id == user.get('id')).all()

修改 read_todo 一樣限定只有自己才能看見, 這裡看是要用 \ 換行, 或是用小括號包住都可以

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@router.get("/todo/{todo_id}", status_code=status.HTTP_200_OK)
async def read_todo(
user: user_dependency,
db: db_dependency,
todo_id: int = Path(gt=0)):
if user is None:
raise HTTPException(status_code=401, detail="Authentication Failed")

todo_model = (db.query(Todos)
.filter(cast("ColumnElement[bool]", Todos.id == todo_id))
.filter(Todos.owner_id == user.get('id'))
.first())
if todo_model is not None:
return todo_model
raise HTTPException(status_code=404, detail='Todo not found')

修改 put 跟 delete 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@router.put("/todo", status_code=status.HTTP_204_NO_CONTENT)
async def update_todo(
user: user_dependency,
db: db_dependency,
todo_id: int, todo_request: TodoRequest):
if user is None:
raise HTTPException(status_code=401, detail="Authentication Failed")

todo_model = (db.query(Todos).filter(Todos.id == todo_id)
.filter(Todos.owner_id == user.get('id'))
.first())

if todo_model is None:
raise HTTPException(status_code=404, detail='Todo not found')

todo_model.title = todo_request.title
todo_model.description = todo_request.description
todo_model.priority = todo_request.priority
todo_model.complete = todo_request.complete

db.add(todo_model)
db.commit()


@router.delete("/todo/{todo_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_todo(
user: user_dependency,
db: db_dependency,
todo_id: int = Path(gt=0)):
if user is None:
raise HTTPException(status_code=401, detail="Authentication Failed")

todo_model = (db.query(Todos).filter(Todos.id == todo_id)
.filter(Todos.owner_id == user.get('id'))
.first())
if todo_model is None:
raise HTTPException(status_code=404, detail='Todo not found')
db.query(Todos).filter(Todos.id == todo_id).delete()
db.commit()

接著建立管理員相關功能, 先調整 create_access_token get_current_user login_for_access_token 加入角色

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
def create_access_token(username: str, user_id: int, role: str, expires_delta: timedelta):
encode = {'sub': username, 'id': user_id, 'role': role}
expires = datetime.now(timezone.utc) + expires_delta
encode.update({'exp': expires})
return jwt.encode(encode, SECRET_KEY, ALGORITHM)


async def get_current_user(token: Annotated[str, Depends(oauth2_bearer)]):
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get('sub')
user_id: int = payload.get('id')
user_role: str = payload.get('role')
if username is None or user_id is None:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED,
detail='Could not validate user.')
return {'username': username, 'id': user_id, 'user_role': user_role}
except JWTError:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED,
detail='Could not validate user.')

@router.post("/token", response_model=Token)
async def login_for_access_token(
form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
db: db_dependency):
user = authenticate_user(form_data.username, form_data.password, db)
print("user:", user)
if not user:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED,
detail='Could not validate user.')
token = create_access_token(user.username, user.id, user.role, timedelta(minutes=20))
print(token)
return {'access_token': token, 'token_type': 'bearer'}

routers 資料夾底下加入 admin.py 並加入相關權限的 api

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
from typing import Annotated, cast

from pydantic import BaseModel, Field
from sqlalchemy.orm import Session
from fastapi import APIRouter, Depends, HTTPException, status, Path

from models import Todos
from database import SessionLocal
from .auth import get_current_user

router = APIRouter(
prefix="/admin",
tags=['admin']
)


def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()


db_dependency = Annotated[Session, Depends(get_db)]
user_dependency = Annotated[dict, Depends(get_current_user)]


@router.get("todo", status_code=status.HTTP_200_OK)
async def read_all(user: user_dependency, db: db_dependency):
if user is None or user.get('user_role') != 'admin':
raise HTTPException(status_code=401, detail='Authentication Failed')
return db.query(Todos).all()


@router.delete("/todo/{todo_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_todo(user: user_dependency, db: db_dependency, todo_id: int = Path(gt=0)):
if user is None or user.get('user_role') != 'admin':
raise HTTPException(status_code=401, detail='Authentication Failed')
todo_model = db.query(Todos).filter(Todos.id == todo_id).first()
if todo_model is None:
raise HTTPException(status_code=401, detail='Todo not found.')
db.query(Todos).filter(Todos.id == todo_id).delete()
db.commit()

別忘了要在 main.py 底下加入 app.include_router(admin.router)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from fastapi import FastAPI

import models
from database import engine
from routers import auth , todos, admin
#from routers import auth , todos, admin , users

app = FastAPI()

models.Base.metadata.create_all(bind=engine)

app.include_router(auth.router)
app.include_router(todos.router)
app.include_router(admin.router)
# app.include_router(users.router)

最後建立可以修改密碼的 users.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
from typing import Annotated, cast

from pydantic import BaseModel, Field
from sqlalchemy.orm import Session
from fastapi import APIRouter, Depends, HTTPException, status, Path

from models import Todos, Users
from database import SessionLocal
from .auth import get_current_user
from passlib.context import CryptContext

router = APIRouter(
prefix="/user",
tags=['user']
)


def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()


db_dependency = Annotated[Session, Depends(get_db)]
user_dependency = Annotated[dict, Depends(get_current_user)]
bcrypt_context = CryptContext(schemes=['bcrypt'], deprecated='auto')


class UserVerification(BaseModel):
password: str
new_password: str = Field(min_length=6)


@router.get("/", status_code=status.HTTP_200_OK)
async def get_user(user: user_dependency, db: db_dependency):
if user is None:
raise HTTPException(status_code=401, detail='Authentication Failed')
return db.query(Users).filter(Users.id == user.get('id')).first()


@router.put("/password", status_code=status.HTTP_204_NO_CONTENT)
async def change_password(user: user_dependency, db: db_dependency, user_verification: UserVerification):
if user is None:
raise HTTPException(status_code=401, detail='Authentication Failed')
user_model = db.query(Users).filter(Users.id == user.get('id')).first()
if not bcrypt_context.verify(user_verification.password, user_model.hashed_password):
raise HTTPException(status_code=401, detail='Error on password change')
user_model.hashed_password = bcrypt_context.hash(user_verification.new_password)
db.add(user_model)
db.commit()

畫一隻土撥鼠

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import io
from typing import Optional
from fastapi import FastAPI, Path, Query, HTTPException, Body
from pydantic import BaseModel, Field
from starlette import status
import numpy as np
import cv2
import matplotlib.pyplot as plt
from fastapi.responses import StreamingResponse

app = FastAPI()


@app.get("/marmot")
async def marmot():
# 讀圖 + 轉成 RGB
marmot = cv2.imread('marmot.jpg')
marmot = cv2.cvtColor(marmot, cv2.COLOR_BGR2RGB)

# 畫圖但不顯示
fig, ax = plt.subplots()
ax.imshow(marmot)
ax.axis('off') # 不顯示座標軸

# 儲存到記憶體
buf = io.BytesIO()
plt.savefig(buf, format='png', bbox_inches='tight')
plt.close(fig)
buf.seek(0)

# 回傳成 image/png
return StreamingResponse(buf, media_type="image/png")

pycharm debug

點選上方工具列的 Run → Edit Configurations…

點左上角的 ➕ 新增一個配置

選擇:Python

設定以下欄位:

欄位 設定內容
Name FastAPI(你可以自訂)
Script path ✅ 勾選 “Module name”
Module name uvicorn
Parameters main:app –reload(視你的檔案名而定)
Python interpreter 選你要的虛擬環境
Working directory 專案根目錄即可

設定好後點 debug 然後打中斷點 f5 下去就搞定

關閉