Fastapi - oceanbei333/leetcode GitHub Wiki

Fastapi 简介

openapi

2.0与3.0

Python类型标注

类型标注,主要是给函数、类、变量等添加类型注解(约束必须是某种类型),或者其他约束(是否可选,是否有某种属性或者方法)

常用的注解类型

python内置的类型和标准库的类型,一般都有相对应的注解类型

并且这些类型在3.9以后将被弃用

建议现在大多数的场景下的python版本都不会高于3.9,建议还是typing的类型,而不是python自带的类型

  • typing.Tuple -> tuple

二元组 Tuple[str, int]

  • typing.List -> list

  • typing.Dict -> dict

  • typing.Set -> set

  • typing.FrozenSet -> frozenset

  • typing.Iterable

  • typing. Callable

可调用类型; Callable[[int], str] 是一个函数,接受一个 int 参数,返回一个 str 。

  • typing.Type -> type

    注解类,而不是类的实例

    Type(str), 必须是类型str或者str的子类,也就是必须是类

class User: ...
class BasicUser(User): ...
class ProUser(User): ...
class TeamUser(User): ...

# Accepts User, BasicUser, ProUser, TeamUser, ...
def make_new_user(user_instance: Type[User]) -> User:
    # ...
    return user_class()

Type 合法的参数仅有类、 Any 、类型变量 以及上述类型的联合类型。

  • typing.Literal

    Enum

    功能上其实类似于使用枚举类型进行标注,而在pydantic也是使用枚举的

  • typing.ClassVar

    标记类变量

enterprise_d = Starship(3000)
enterprise_d.stats = {} # Error, setting class variable on instance
Starship.stats = {}     # This is OK

创建新的注解类型

  • typing.NamedTuple

  • typing.NewType(name, tp)

    和原来的类型一样,只是标识,他们是不同的类而已

UserId = NewType('UserId', int)
first_user = UserId(1)
  • typing.TypedDict
class Point2D(TypedDict):
    x: int
    y: int
    label: str

a: Point2D = {'x': 1, 'y': 2, 'label': 'good'}  # OK
b: Point2D = {'z': 3, 'label': 'bad'}  

其他

  • typing.Union

Union[str, int],标注可以是str或者int类型

  • typing.Optional

Optional[str] 等价于 Union[str, None]

这与可选参数并非相同的概念。可选参数是一个具有默认值的参数

def foo(arg: int = 0) -> None:

此时arg参数不可以使用Optional[int], 因为Optional[int]的意思是Union[int, None], 也就是要么是整型类型,要么是None

除非可选参数的arg的默认值是None

def foo(arg: Optional[int] = None) -> None:
  • typing.Final

不能被再次赋值或者在子类中被重载

MAX_SIZE: Final = 9000
MAX_SIZE += 1  # Error reported by type checker

class Connection:
    TIMEOUT: Final[int] = 10

class FastConnector(Connection):
    TIMEOUT = 1  # Error reported by type checker
  • typing.overload

类似于Java的重载

@overload
def process(response: None) -> None:
    ...
@overload
def process(response: int) -> tuple[int, str]:
    ...
@overload
def process(response: bytes) -> str:
    ...
def process(response):
    <actual implementation>
  • typing.final

    不能被子类重写

  • typing.AnyStr

AnyStr = TypeVar('AnyStr', str, bytes)

  • typing.Protocol

    类似于abc.ABC, 只不过ABC是通过在父类中定义虚拟方法,约束子类必须实现某种方法

    而Protocol,通过注解,约束某个对象必须有某种方法,才可以被使用

    比如定义function的时候,对传入的参数进行约束

    如果需要使用ABC实现,可以约束对象必须某种虚拟类的子类

class Proto(Protocol):
    def meth(self) -> int:
    
class C:
    def meth(self) -> int:
        return 0

def func1(x: Proto) -> int:
    return x.meth()

class D(ABC):
	@abstractmethod
	def meth(self) -> int:
		return 0
 
def func2(x: Proto) -> int:
   if isinstance(x, D):
   		return x.meth()
    	
def func3(x: Proto) -> int:
		if hasattr(x, 'meth'):
			return x.meth()


func(C())  # Passes static type chec

  • typing.runtime_checkable

泛型

They are building blocks for creating generic types.

  • typing.Generic

    使用:实例化Generic,并把注解类型作为变量传入,需要注解的类,需要继承这个实例

    适用于,暂时不能确定注解类型,延迟到使用时或者子类中确定

    比如对于一个类似dict的类

class Mapping(Generic[KT, VT]):
    def __getitem__(self, key: KT) -> VT:
        ...
        # Etc.
def lookup_name(mapping: Mapping[str, str], key: str, default: str) -> str:
    try:
        return mapping[key]
    except KeyError:
        return default
        
class IntMapping(Maping[int: int]):

  • typing.TypeVar

    变量,其值是注解类型

    T = TypeVar('T')  # Can be anything
    A = TypeVar('A', str, bytes)  # Must be str or bytes
    

主要是和Generic配合使用,因为Generic实例的时候,需要将注解类型作为变量传入,所以这时候就需要Typevar

pydantic

Data validation and settings management

pydantic enforces type hints at runtime, and provides user friendly errors when data is invalid.

让类型注解不仅仅是静态检查的提示而已

相比于typing,添加了一些额外的类型,比如ip、email、uuid、date、datetime等

也可以添加自对应的约束

也可以字段mapping,值的mapping,数据结构转换

from datetime import datetime
from typing import List, Optional
from pydantic import BaseModel
from enum import Enum

class User(BaseModel):
    id: int
    name = 'John Doe'
    signup_ts: Optional[datetime] = Field(None)
    friends: List[int] = []


external_data = {
    'id': '123',
    'signup_ts': '2019-06-01 12:22',
    'friends': [1, 2, '3'],
}
user = User(**external_data)
print(user.id)
#> 123
print(repr(user.signup_ts))
#> datetime.datetime(2019, 6, 1, 12, 22)
print(user.friends)
#> [1, 2, 3]
print(user.dict())
"""
{
    'id': 123,
    'signup_ts': datetime.datetime(2019, 6, 1, 12, 22),
    'friends': [1, 2, 3],
    'name': 'John Doe',
}
"""

Settings management

from typing import Set

from pydantic import (
    BaseModel,
    BaseSettings,
    PyObject,
    RedisDsn,
    PostgresDsn,
    Field,
)


class SubModel(BaseModel):
    foo = 'bar'
    apple = 1


class Settings(BaseSettings):
    auth_key: str
    api_key: str = Field(..., env='my_api_key')

    redis_dsn: RedisDsn = 'redis://user:pass@localhost:6379/1'
    pg_dsn: PostgresDsn = 'postgres://user:pass@localhost:5432/foobar'

    special_function: PyObject = 'math.cos'

    # to override domains:
    # export my_prefix_domains='["foo.com", "bar.com"]'
    domains: Set[str] = set()

    # to override more_settings:
    # export my_prefix_more_settings='{"foo": "x", "apple": 1}'
    more_settings: SubModel = SubModel()

    class Config:
        env_prefix = 'my_prefix_'  # defaults to no prefix, i.e. ""
        fields = {
            'auth_key': {
                'env': 'my_auth_key',
            },
            'redis_dsn': {
                'env': ['service_redis_dsn', 'redis_url']
            }
        }


print(Settings().dict())
"""
{
    'auth_key': 'xxx',
    'api_key': 'xxx',
    'redis_dsn': RedisDsn('redis://user:pass@localhost:6379/1',
scheme='redis', user='user', password='pass', host='localhost',
host_type='int_domain', port='6379', path='/1'),
    'pg_dsn': PostgresDsn('postgres://user:pass@localhost:5432/foobar',
scheme='postgres', user='user', password='pass', host='localhost',
host_type='int_domain', port='5432', path='/foobar'),
    'special_function': <built-in function cos>,
    'domains': set(),
    'more_settings': {'foo': 'bar', 'apple': 1},
}
"""

项目启动

  1. 下载项目代码

项目地址

  1. 环境变量

    默认已经有一个.env文件

  2. 项目启动

docker-compose up -d
  1. 容器
    1. 数据库容器
    2. 后端容器
    3. celery容器

Makefile

make shell 后需要手动建立数据连接

后端服务

预入口文件

python /app/app/backend_pre_start.py

# make migrations
alembic revision --autogenerate
# Run migrations
alembic upgrade head

# Create initial data in DB
python /app/app/initial_data.py

目录结构

项目根路径

backend/

对于单模块的项目,默认app模块

对比django的目录结构

api/

对应于django的views.py和urls.py文件

api/api.py

可以使用api.py文件来组织路由

api/接口文件.py

views.p

专注于处理请求,业务逻辑,

具体业务逻辑的实现需要委托给crud的类的实例

不直接面对数据库,数据库操作由crud实现

models/

对应于django的 modes.py

专注于数据库建表,没有任何业务逻辑,目前使用sqlalchemy,可以换成其他ORM框架,或者直接使用sql

不同领域的表在不同文件中组织,然后统一在__init__.py暴露

db/

封装了数据库连接

通过依赖注入,在接口中使用数据库连接

schemas/

对应于django的serializers.py

主要是构造接口返回的数据与对数据校验

但是不同于django的同一张表一般只会对应于一个serializers类,fastapi对于一张表可能会对应多个BaseModel,是因为不同的场景需要使用不同的BaseModel,比如post接口的request body、更新的request body、以及序列化与反序列化

所有类依然通过__init__.py暴露,但是因为这里的类比较多,建议直接 import *

crud/

这是一个django中缺失的目录

这里主要是处理数据的增改删除

这里的类都是一个具有能力的类,也是实践面向对象思想最佳场所

基类CRUDBase中封装了对数据库的常规操作

子类可以专注于业务逻辑的实现

CRUDBase详解

泛型(Generic)

类中的某些类型注解推迟确定,类似于函数的传参,方便对父类进行类型注解

ModelType = TypeVar("ModelType", bound=Base)