2.4. FastAPI Schema¶
Schema - also known as Model
Represents data in your system
Pydantic class
schema_extra
is used by Swagger to show examplesEllipsis (
...
) in Pydantic indicates that a Field is required
2.4.1. Example¶
>>> from pydantic import BaseModel
>>>
>>>
>>> class Astronaut(BaseModel):
... firstname: str
... lastname: str
... active: bool | None = True
2.4.2. Validation¶
Validators are "class methods", so the first argument value they receive is the Astronaut class, not an instance of Astronaut.
The second argument is always the field value to validate; it can be named as you please
Validators should either return the parsed value or raise a ValueError, TypeError, or AssertionError (assert statements may be used).
Validation is done in the order fields are defined
>>> from string import ascii_uppercase
>>> from pydantic import BaseModel, root_validator, validator
>>>
>>>
>>> class Astronaut(BaseModel):
... firstname: str
... lastname: str
... age: float | None = None
...
... @validator('firstname', 'lastname', allow_reuse=True)
... def is_capitalize(cls, value: str):
... uppercase = tuple(ascii_uppercase)
... if not value.startswith(uppercase):
... raise ValueError('Must starts with uppercase letter')
... return value
...
... @validator('age', allow_reuse=True)
... def age_in_range(cls, value: float):
... if 0 < value < 130:
... return value
... else:
... raise ValueError('Age must be in range from 0 to 130')
...
... @validator('*', allow_reuse=True)
... def not_empty(cls, value: str | float | None):
... if type(value) is str and value == '':
... raise ValueError('Invalid field value')
... return value
...
... @root_validator(pre=True, allow_reuse=True)
... def check_names_differs(cls, values: dict):
... firstname = values.get('firstname')
... lastname = values.get('lastname')
...
... if firstname is None or lastname is None:
... raise ValueError('fields firstname and lastname are not optional')
... if firstname == lastname:
... raise ValueError('firstname and lastname cannot be the same')
... else:
... return values
>>>
>>>
>>> Astronaut(firstname='Mark', lastname='Watney')
Astronaut(firstname='Mark', lastname='Watney', age=None)
>>>
>>> Astronaut(firstname='Mark', lastname='Watney', age=10)
Astronaut(firstname='Mark', lastname='Watney', age=10.0)
>>>
>>> Astronaut(firstname='Mark', lastname='Watney', age=-1)
Traceback (most recent call last):
pydantic.error_wrappers.ValidationError: 1 validation error for Astronaut
age
Age must be in range from 0 to 130 (type=value_error)
>>>
>>> Astronaut(firstname='Mark', lastname='Mark', age=1)
Traceback (most recent call last):
pydantic.error_wrappers.ValidationError: 1 validation error for Astronaut
__root__
firstname and lastname cannot be the same (type=value_error)
>>>
>>> Astronaut(firstname='mark', lastname='watney', age=-1)
Traceback (most recent call last):
pydantic.error_wrappers.ValidationError: 3 validation errors for Astronaut
firstname
Must starts with uppercase letter (type=value_error)
lastname
Must starts with uppercase letter (type=value_error)
age
Age must be in range from 0 to 130 (type=value_error)
>>> from datetime import datetime
>>> from pydantic import BaseModel, validator
>>>
>>>
>>> class PastDate(BaseModel):
... dt: datetime = None
...
... @validator('dt', pre=True, always=True, allow_reuse=True)
... def past_timestamp(cls, value):
... if datetime.fromisoformat(value) >= datetime.now():
... raise ValueError('Timestamp is not in the past')
... return value
>>>
>>> print(PastDate(dt='1969-07-21T02:56:15'))
dt=datetime.datetime(1969, 7, 21, 2, 56, 15)
>>>
>>> print(PastDate(dt='2969-07-21T02:56:15'))
Traceback (most recent call last):
pydantic.error_wrappers.ValidationError: 1 validation error for PastDate
dt
Timestamp is not in the past (type=value_error)
2.4.3. Use Case - 0x01¶
schema_extra
is used by Swagger to show examplesEllipsis (
...
) in Pydantic indicates that a Field is required
$ pip install 'pydantic[email]'
>>> from pydantic import BaseModel, EmailStr, Field
>>>
>>>
>>> class StudentSchema(BaseModel):
... fullname: str = Field(...)
... email: EmailStr = Field(...)
... course_of_study: str = Field(...)
... year: int = Field(..., gt=0, lt=9)
... gpa: float = Field(..., le=4.0)
...
... class Config:
... schema_extra = {
... "example": {
... "fullname": "Mark Watney",
... "email": "mark.watney@nasa.gov",
... "course_of_study": "Botanics",
... "year": 2,
... "gpa": "3.0",
... }
... }
>>>
>>>
>>> class UpdateStudentModel(BaseModel):
... fullname: str | None
... email: EmailStr | None
... course_of_study: str | None
... year: int | None
... gpa: float | None
...
... class Config:
... schema_extra = {
... "example": {
... "fullname": "Mark Watney",
... "email": "mark.watney@nasa.gov",
... "course_of_study": "Botanics",
... "year": 4,
... "gpa": "4.0",
... }
... }
2.4.4. Use Case - 0x01¶
>>> from fastapi import FastAPI
>>> from fastapi.responses import JSONResponse
>>> from pydantic import BaseModel as Schema
>>> app = FastAPI()
>>>
>>>
>>> SQL_SELECT_LOGIN = """
... SELECT id, firstname, lastname
... FROM users
... WHERE username = :username
... AND password = :password; """
>>>
>>>
>>> class LoginIn(Schema):
... username: str
... password: str
...
... class Config:
... schema_extra = {
... 'example': {
... 'username': 'mwatney',
... 'password': 'nasa'}}
>>>
>>>
>>> class LoginOut(Schema):
... id: int
... firstname: str
... lastname: str
...
... class Config:
... schema_extra = {
... 'example': {
... 'uid': 1,
... 'firstname': 'Mark',
... 'lastname': 'Watney'}}
>>>
>>>
>>> class NotFound(Schema):
... status: int = 404
... reason: str = 'Not Found'
... details: str | None = None
>>>
>>>
>>> @app.post(
... path='/login',
... response_model=LoginOut,
... responses={
... 200: {'model': LoginOut},
... 404: {'model': NotFound}},
... tags=['auth'])
... def login(user: LoginIn):
... from database import DATABASE, SQL_SELECT_LOGIN
... with sqlite3.connect(DATABASE) as db:
... db.row_factory = sqlite3.Row
... if result := db.execute(SQL_SELECT_LOGIN, user.dict()).fetchone():
... return dict(result)
... else:
... return JSONResponse(status_code=404, content={'details': 'invalid login/password'})
2.4.5. Assignments¶
"""
* Assignment: FastAPI Schema Model
* Complexity: easy
* Lines of code: 15 lines
* Time: 13 min
English:
1. TODO: Translation
Polish:
1. Stworzyć dwa endpointy: `GET /astronaut` i `POST /astronaut`
2. Endpoint POST, przyjmuje dane w formacie JSON:
a) firstname - wymagane
b) lastname - wymagane
c) age - opcjonalne
d) height - opcjonalne
e) weight - opcjonalne
f) missions - opcjonalna list[str]
3. Używając Pydantic Schema (Base Model) zamodeluj dane wejściowe
4. Zapisz dane do pliku `FILE`
5. Endpoint GET, odczytuje z pliku i wysyła je użytkownikowi
Hint:
* Documentation: http://localhost:8000/docs
Tests:
>>> import sys; sys.tracebacklimit = 0
"""
import json
import uvicorn
from fastapi import FastAPI
from pydantic import BaseModel as Schema
app = FastAPI()
FILE = '_temporary.txt'
if __name__ == '__main__':
uvicorn.run('main:app', host='127.0.0.1', port=8000)