קלאס לניהול משתמשים באפליקציית FastApi

מחבר:
בתאריך:

אין כמעט אפליקציה שלא דורשת עבודה עם משתמשים, ואתר האינטרנט שנפתח באמצעות FastAPI גם הוא יצטרך לנהל משתמשים. אחרי שבמדריך הקודם פיתחנו קוד CRUD - רישום, קריאה, עדכון ומחיקה שעובד עם מסד הנתונים ומנהל רשומות על מכוניות. במדריך זה נוסיף קוד דומה בשביל המשתמשים. אם זה המדריך הראשון בסדרה שאותו אתם קוראים אז כדאי שקודם תעיינו במדריכים הקודמים בסדרה:

  1. FastAPI - היכרות עם ספרית הקוד הטובה ביותר של פיתון להקמת אפליקציות אינטרנט
  2. אפליקצית אינטרנט עם FastApi - הקמת מסד הנתונים
  3. FastApi - לעבוד עם מסד נתונים - ORM ואלכימיה

להורדת הקוד אותו נפתח במדריך

 

מודל בשביל טבלת users במסד הנתונים

בקובץ models.py כותבים את המודלים של ה- ORM בתוכם נחזיק את המידע על מבנה הטבלאות במסד הנתונים.

מתחת למודל Car נוסיף לקובץ את User:

models.py

from sqlalchemy import Column, Integer, String, DateTime
from sqlalchemy.sql import func
from database import Base

class User(Base):
  __tablename__ = "users"
  id = Column(Integer, primary_key=True, index=True)
  email = Column(String(80), unique=True)
  name = Column(String(80))
  password = Column(String(60))
  picture = Column(String(80))
  created_at = Column(DateTime(timezone=True), server_default=func.now())
  updated_at = Column(DateTime(timezone=True), onupdate=func.now())
  active = Column(Integer, default=0)
  • הטבלה users במסד הנתונים צריכה להכיל את שם המשתמש, סיסמה מוצפנת, אימייל ותמונה נוסף לתאריך היצירה והעדכון של הרשומה ואם היא פעילה.

fastapi - how to add a user class to the application

 

הסכימות של pydantic

הסכימות של pydantic הם קלאסים שמגדירים עם איזה מידע ה-API צריך לעבוד. נוסיף את הקלאס User עם השדות המינימליים ההכרחיים בשביל המשתמש:

schemas.py

from pydantic import BaseModel
 
class User(BaseModel):
   name: str
   picture: str
 
   class Config:
       orm_mode = True
  • מכיוון שהשדות, שם ותמונה, הם מחרוזות נגדיר את סוג הנתונים כ-str.
  • הקלאס config מאפשר למודלים לעבוד עם מסד הנתונים.

התמונה היא לא הכרחית לכן נגדיר אותה כ-Optional:

schemas.py

from pydantic import BaseModel
from typing import Optional
 
class User(BaseModel):
   name: str
   picture: Optional[str]
 
   class Config:
       orm_mode = True

אנחנו לא מוכנים לקבל כל שם, והמגבלות הם מינימום של שני תווים, לכל היותר 80 וללא רווחים לבנים סביב השם:

schemas.py

from pydantic import BaseModel, constr
from typing import Optional
 
class User(BaseModel):
   name: constr(strip_whitespace=True, min_length=2, max_length=80)
   picture: Optional[str]
 
   class Config:
       orm_mode = True
  • הפונקציה constr של Pydantic מאפשרת לנו להוסיף מגבלות גרעיניות על הנתונים איתם אנו יכולים לעבוד.

נשתמש בביטוי רגולרי כדי שהשם יכיל רווחים ואותיות עבריות או אנגליות בלבד:

schemas.py

from pydantic import BaseModel, constr
from typing import Optional
 
class User(BaseModel):
   name: constr(strip_whitespace=True, min_length=2, max_length=80, regex=r'^[a-zA-Z א-ת]+$')
   picture: Optional[str]
 
   class Config:
       orm_mode = True

כאשר משתמש יזין את פרטיו נזדקק נוסף לשדות name ו-picture מהקלאס User גם ל- email, password ו- active. ניצור קלאס UserIn שירש את הקלאס User ויוסיף את השדות המבוקשים:

schemas.py

from pydantic import BaseModel, constr
from typing import Optional
 
class User(BaseModel):
   name: constr(strip_whitespace=True, min_length=2, max_length=80, regex=r'^[a-zA-Z א-ת]+$')
   picture: Optional[str]
 
   class Config:
       orm_mode = True
 
class UserIn(User):
   email: str
   password: constr(strip_whitespace=True, min_length=5, max_length=16, regex=r'^\w+$')
   active: Optional[bool]
 
   class Config:
       orm_mode = True

את האימייל נוודא באמצעות ספרייה אותה נתקין באמצעות pip:

(venv) $ pip3 install email-validator

נשתמש בסוג EmailStr אותו נייבא מ-pydantic כדי לוודא את האימייל של המשתמש:

schemas.py

from pydantic import BaseModel, EmailStr, constr
from typing import Optional
 
class User(BaseModel):
   name: constr(strip_whitespace=True, min_length=2, max_length=80, regex=r'^[a-zA-Z א-ת]+$')
   picture: Optional[str]
 
   class Config:
       orm_mode = True
 
 
class UserIn(User):
   email: EmailStr
   password: constr(strip_whitespace=True, min_length=5, max_length=16, regex=r'^\w+$')
   active: Optional[bool]
 
   class Config:
       orm_mode = True

ניצור קלאס נוסף UserOut בשביל המקרים שבהם צריך להציג את המידע על המשתמשים באופן בטוח, בלי נתונים רגישים, דוגמת אימייל או סיסמה:

schemas.py

class UserOut(User):
   id: int
 
   class Config:
       orm_mode = True
  • מכיוון שהקלאס UserOut מרחיב את הקלאס User הוא ירש ממנו את השדות שם ותמונה. לזה הוספנו את המזהה הייחודי -id - של המשתמש.

 

הצפנת סיסמה

נקפיד להצפין את הסיסמאות של המשתמשים כדי שלא יקלטו על ידי מיני מרעין בישין באינטרנט באמצעות הספרייה ש- FastAPI ממליץ להתקין (Password hashing):

(venv) $ pip3 install passlib

נוסיף לתיקיית השורש של הפרויקט קובץ hash.py שיישם את הספרייה שהתקנו בתוך קלאס Hash שיכלול מתודה get_password_hash:

hash.py

from passlib.context import CryptContext
 
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
 
class Hash():
   def get_password_hash(password:str):
       return pwd_context.hash(password)

 

ניהול פעולות מול מסד הנתונים

התיקייה routers מכילה את המתודות של CRUD - רישום, קריאה, עדכון ומחיקה - עבור כל אחד מהקלאסים. בקובץ routers/users.py נרכז את המתודות לניהול משתמשים.

ראשית נייבא את התלויות:

routers/users.py

from typing import List
from fastapi import APIRouter, Depends, HTTPException, Query
 
from sqlalchemy.orm import Session
import models, schemas, database
from hash import Hash
  • בהמשך נראה איך אנחנו משתמשים בכל התלויות.

נוסיף את הסט שמגדיר את התכונות המשותפות עבור כל המתודות בראוטר:

routers/users.py

router = APIRouter(
  prefix="/users",
  tags=["users"]
)
  • הקידומת users תהיה הראשונה בכל אחד מהנתיבים בקובץ.
  • tags מקבץ את המתודות ביחד בדף התיעוד docs.

 

הוספת משתמש

המתודה create_user מוסיפה משתמש:

routers/users.py

@router.post('/', response_model=schemas.User, status_code=201)
def create_user(request: schemas.UserIn,
               db: Session = Depends(database.get_db)):
   db_item = models.User()
   db_item.name = request.name
   db_item.email = request.email
   if not request.picture:
       request.picture = 'default.png'
   db_item.picture = request.picture
   db_item.password = Hash.get_password_hash(request.password)
   db_item.active = request.active
 
   db.add(db_item)
   db.commit()
   db.refresh(db_item)
 
   # return {'id': db_item.id}
   return db_item
  • response_model - המידע אודות המשתמש אותו מחזירה הפונקציה במקרה של הצלחה ברישום - הוא מסוג User (אותו שהגדרנו בקובץ schemas.py) כי אנחנו לא רוצים שהפונקציה תחזיר את כל פרטי הרשומה. רק שם ותמונה.
  • המידע ששולחים המשתמשים נקלט על ידי הפרמטר request צריך להשתמש בסכמה UserIn הדורשת מידע על האימייל והסיסמה.
  • במקרה שהמשתמש לא מזין תמונה. הקוד מזהה ומחליף בתמונה ברירת מחדל.
  • הסיסמה עוברת הצפנה.

 

שליפת משתמש על סמך מזהה ייחודי

הפונקציה get_user שולפת את פרטי המשתמש על פי ה-id שלו או קוד שגיאה במקרה של כשלון באיתור הרשומה.

routers/users.py

@router.get('/{id}', status_code = 200, response_model=schemas.UserOut)
def get_user(id: int, db: Session = Depends(database.get_db)):
   user = db.query(models.User).filter(models.User.id == id).first()
   if not user:
       raise HTTPException(status_code=404, detail=f"User #{id} not found")
   return user

 

שליפה של רשימת משתמשים

הפונקציה get_users מסננת ומחזירה רשימת משתמשים:

routers/users.py

@router.get('/', response_model=List[schemas.UserOut])
def get_users(limit = 10,
             q: str,
             db: Session = Depends(database.get_db)):
  
   users = db.query(models.User).filter(models.User.name == q)
   # users = db.with_entities(models.User.name, models.User.picture)
   users = users.limit(limit).all()
   return users
  • הפונקציה צפויה להחזיר response_model שהוא רשימה List.
  • הפרמטר q קולט מחרוזת אותה הזין המשתמש ולפיה הפונקציה מסננת את רשימת השמות.

נעשה את זה יותר מעניין. נגביל את הפרמטר q באמצעות ולידציות (ראה בתיעוד של FastAPI):

routers/users.py

@router.get('/', response_model=List[schemas.UserOut])
def get_users(limit = 10,
             q: str = Query(None, min_length=3, max_length=50, regex="^\w[a-zA-Z א-ת]+$")
,
             db: Session = Depends(database.get_db)):
  
   users = db.query(models.User).filter(models.User.name == q)
   # users = db.with_entities(models.User.name, models.User.picture)
   users = users.limit(limit).all()
   return users
  • לכל הפחות 3 תווים, לכל היותר 50 וביטוי רגולרי לפיו מותרים רק אותיות בעברית ובאנגלית ורווחים.

הרבה פעמים במקום שם ספציפי נרצה לאתר רשימה של שמות דומים. בשביל זה נשתמש באופרטור LIKE של SQL:

routers/users.py

@router.get('/', response_model=List[schemas.UserOut])
def get_users(limit = 10,
             q: str = Query(None, min_length=3, max_length=50, regex="^\w[a-zA-Z א-ת]+$"),
             db: Session = Depends(database.get_db)):
  
   users = db.query(models.User)
   # users = db.with_entities(models.User.name, models.User.picture)
   if q:
       search = "%{}%".format(q)
       users = users.filter(models.User.name.like(search))
   users = users.limit(limit).all()
   return users

 

עריכת משתמש

הפונקציה update_user תשמש לעריכת משתמש:

routers/users.py

@router.put("/{id}", status_code=202)
def update_user(id: int, request: schemas.User, db: Session = Depends(database.get_db)):
   user = db.query(models.User).filter(models.User.id == id)
   if not user.first():
       raise HTTPException(status_code=404, detail=f"User #{id} not found")
   user.update({'name': request.name, 'price': request.price})
   db.commit()

 

מחיקת משתמש

הפונקציה delete_user למחיקה:

routers/users.py

@router.delete('/{id}', status_code=204)
def delete_user(id: int, db: Session = Depends(database.get_db)):
   user = db.query(models.User).filter(models.User.id == id)
   if not user.first():
       raise HTTPException(status_code=404, detail=f"User #{id} not found")
   user.delete(synchronize_session=False)
   db.commit()

 

שילוב הקוד באפליקציה

הקובץ main.py הוא הנתב router של כל האפליקציה אליו צריך להתנקז כל הקוד. נייבא לתוכו את קובץ ה-router של ה-users:

main.py

# Core packages
import uvicorn
from fastapi import FastAPI
from database import engine
# our scripts
from routers import cars, users
import models
# create the database
models.Base.metadata.create_all(bind=engine)
# init app
app = FastAPI()
app.include_router(users.router)
app.include_router(cars.router)
if __name__ == '__main__':
  uvicorn.run("main:app", host="127.0.0.1", port=8000, reload=True, access_log=False)

במדריכים הבאים בסדרה ללימוד FastApi

  1. נלמד כיצד ליצור יחסים בין מודלים.
  2. נאפשר ניהול משתמשים.
  3. נוסיף תבניות.
  4. נעבוד עם ה-api דרך אפליקצית צד משתמש הכתובה ב-JavaScript.

לכל המדריכים בסדרה ללימוד פייתון

 

אהבתם? לא אהבתם? דרגו!

0 הצבעות, ממוצע 0 מתוך 5 כוכבים

 

 

הוסף תגובה חדשה

 

= 4 + 2

תמונת המגיב

דוד בתאריך: 18.10.2021

לא מצאתי את הספריה DATABASE (על כל פנים בגירסת פייתון 3.10) הPIP מחזיר שגיאה: ERROR: Could not find a version that satisfies the requirement database (from versions: none) ERROR: No matching distribution found for database