该动手执行什么
-
FastAPI Docker环境(Nginx容器,应用程序容器,数据库容器)
-
厌氧环境
- 数据库迁移工具
-
用户信息模型
- 在数据库中创建的表的源。用于迁移工具。
-
数据访问类
- 像Dao之类的东西在Java中
-
用于CRUD用户信息的API
- 截至2020年10月27日,不考虑密码哈希
-
请求中间件
- 在请求之前和之后处理的中间件
-
API测试代码
- 创建一个测试数据库,为每个测试用例执行回滚该数据库,并在所有测试完成后删除该测试数据库。
-
避免CORS问题
-
自定义异常(应用程序异常和系统异常)
建筑
1 2 3 4 | python: v3.8.5 postgresql: v12.4 fastapi: v0.60.2 SQLAlchemy: v1.3.18 |
迁移工具
1 | alembic: v1.4.2 |
什么是FastAPI
我将省略详细信息,因为已经有关于Qiita的choco choco文章。
非常高兴有很多文档(未阅读)并且与Swagger兼容。
(看来性能不错,但是我不确定,因为我还没有测量基准,但这可能很快。)
Django是我接触过的唯一框架,因此我很难使用中间件和测试代码。
(Django是一个全栈框架,它自己执行许多操作,因此我并不真正在乎中间件或测试实现。)
为Python 3.8.5创建伪装环境
如果不包含
pyenv和virtualenv,请参阅以下内容。
Mac Mac
Windows
1 | $ pyenv virtualenv 3.8.5 env_fastapi_sample |
应用伪装环境
创建项目根
1 | $ mkdir fastapi_sample |
cd到项目根目录
1 | $ cd fastapi_sample |
应用伪装环境
1 | $ pyenv local env_fastapi_sample |
requirements.txt创建/安装
1 | $ touch requirements.txt |
将fastapi添加到requirements.txt
fastapi_sample / requirements.txt
1 2 | fastapi==0.60.2 uvicorn==0.11.8 |
在伪装环境中安装
1 | $ pip3 install -r requirements.txt |
创建一个入口点(main.py)并显示Swagger-UI
创建入口点(main.py)
1 | $ touch main.py |
编辑main.py
fastapi_sample / main.py
1 2 3 4 5 6 7 8 | from fastapi import FastAPI app = FastAPI() @app.get("/") async def root(): return {"message": "Hello World"} |
Swagger-UI显示
1 | $ uvicorn main:app --reload |
如果从
浏览器访问" http://127.0.0.1:8000",并且显示
码头工人
使应用程序由Nginx的反向代理在https上发布。
基本docker-compose.yml
fastapi_sample / docker-compose.yml
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 | version: '3' services: # Nginxコンテナ nginx: container_name: nginx_fastapi_sample image: nginx:alpine depends_on: - app - db environment: TZ: "Asia/Tokyo" ports: - "80:80" - "443:443" volumes: - ./docker/nginx/conf.d:/etc/nginx/conf.d - ./docker/nginx/ssl:/etc/nginx/ssl # アプリケーションコンテナ app: build: context: . dockerfile: Dockerfile container_name: app_fastapi_sample volumes: - '.:/fastapi_sample/' environment: - LC_ALL=ja_JP.UTF-8 expose: - 8000 depends_on: - db entrypoint: /fastapi_sample/docker/wait-for-it.sh db 5432 postgres postgres db_fastapi_sample command: bash /fastapi_sample/docker/rundevserver.sh restart: always tty: true # DBコンテナ db: image: postgres:12.4-alpine container_name: db_fastapi_sample environment: - POSTGRES_USER=postgres - POSTGRES_PASSWORD=postgres - POSTGRES_DB=db_fastapi_sample - POSTGRES_INITDB_ARGS=--encoding=UTF-8 --locale=C volumes: - db_data:/var/lib/postgresql/data ports: - '5432:5432' volumes: db_data: driver: local |
Nginx容器设置
准备配置文件
1 2 | $ mkdir -p nginx/conf.d $ touch nginx/conf.d/app.conf |
fastapi_sample / docker / nginx / conf.d / app.conf
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 | upstream backend { server app:8000; # appはdocker-compose.ymlの「app」 } # 80番ポートへのアクセスは443番ポートへのアクセスに強制する server { listen 80; return 301 https://$host$request_uri; } server { listen 443 ssl; ssl_certificate /etc/nginx/ssl/server.crt; ssl_certificate_key /etc/nginx/ssl/server.key; ssl_protocols TLSv1.2 TLSv1.3; location / { proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-Host $http_host; proxy_set_header X-Forwarded-Server $http_host; proxy_set_header X-Forwarded-Server $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_redirect http:// https://; proxy_pass http://backend; } # ログを出力したい場合はコメントアウト外してください # access_log /var/log/nginx/access.log; # error_log /var/log/nginx/error.log; } server_tokens off; |
准备Nginx
的SSL转换所需的文件
1 | $ mkdir -p nginx/conf.d nginx/ssl |
使用OpenSSL生成私钥(server.key)
1 2 3 4 5 6 7 8 9 10 | # ディレクトリ移動 $ cd nginx/conf.d # 秘密鍵生成 $ openssl genrsa 2024 > server.key # 確認 $ ls -ll total 8 -rw-r--r-- 1 user staff 1647 10 24 13:23 server.key |
生成证书签名请求(server.csr)
1 | $ openssl req -new -key server.key > server.csr |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | $ openssl req -new -key server.key > server.csr ... .. . Country Name (2 letter code) [AU]:JP # 国を示す2文字のISO略語 State or Province Name (full name) [Some-State]:Tokyo # 会社が置かれている都道府県 Locality Name (eg, city) []:Chiyodaku # 会社が置かれている市区町村 Organization Name (eg, company) [Internet Widgits Pty Ltd]:fabeee # 会社名 Organizational Unit Name (eg, section) []:- # 部署名(ハイフンにしました。) Common Name (e.g. server FQDN or YOUR name) []:localhost(ウェブサーバのFQDN。一応localhostにした) Email Address []: # 未入力でエンター Please enter the following 'extra' attributes to be sent with your certificate request A challenge password []: # 未入力でエンター An optional company name []: # 未入力でエンター ... .. . $ ls -ll total 16 -rw-r--r-- 1 user staff 980 10 24 13:27 server.csr # 証明書署名要求ができた -rw-r--r-- 1 user staff 1647 10 24 13:23 server.key |
生成服务器证书(server.crt)
1 2 3 4 5 6 7 | openssl x509 -req -days 3650 -signkey server.key < server.csr > server.crt % ls -ll total 24 -rw-r--r-- 1 tabata staff 1115 10 24 13:40 server.crt # サーバ証明書ができた -rw-r--r-- 1 tabata staff 948 10 24 13:29 server.csr -rw-r--r-- 1 tabata staff 1647 10 24 13:23 server.key |
信任伺服器凭证
双击
从"使用此证书时"下拉菜单中选择"始终信任"。
如果是这种情况,则Nginx设置已完成
1 2 3 4 5 6 7 8 9 10 11 12 | fastapi_sample ├── docker │ └── nginx │ ├── conf.d │ │ └── app.conf │ └── ssl │ ├── server.crt │ ├── server.csr │ └── server.key ├── docker-compose.yml ├── main.py └── requirements.txt |
应用(FastAPI)容器设置
Dockerfile
图像是Ubuntu 20.04。
容器中的python版本设置为v3.8.5,带有?pyenv
使用apt install安装所需的模块(您可以删除不需要的模块)
在容器内部安装python模块吗?
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 | FROM ubuntu:20.04 ENV DEBIAN_FRONTEND=noninteractive ENV HOME /root ENV PYTHONPATH /fastapi_sample/ ENV PYTHON_VERSION 3.8.5 ENV PYTHON_ROOT $HOME/local/python-$PYTHON_VERSION ENV PATH $PYTHON_ROOT/bin:$PATH ENV PYENV_ROOT $HOME/.pyenv RUN mkdir /fastapi_sample \ && rm -rf /var/lib/apt/lists/* RUN apt update && apt install -y git curl locales python3-pip python3-dev python3-passlib python3-jwt \ libssl-dev libffi-dev zlib1g-dev libpq-dev postgresql RUN echo "ja_JP UTF-8" > /etc/locale.gen \ && locale-gen RUN git clone https://github.com/pyenv/pyenv.git $PYENV_ROOT \ && $PYENV_ROOT/plugins/python-build/install.sh \ && /usr/local/bin/python-build -v $PYTHON_VERSION $PYTHON_ROOT WORKDIR /fastapi_sample ADD . /fastapi_sample/ RUN LC_ALL=ja_JP.UTF-8 \ && pip3 install -r requirements.txt |
等待它.sh
一个shell脚本,它在启动应用程序容器之前等待数据库启动。
容器启动顺序 font>可以由docker-componse中所述的
如果数据库未启动,则应用程序容器中可能会出现错误。
(如果您尝试在应用容器启动时尝试执行操作数据库的命令,则会由于数据库尚未启动而发生错误。)
fastapi_sample /泊坞窗/ wait-for-it.sh
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 | #!/bin/sh set -e # 引数はdocker-compose.ymlで指定している。 # app: # ... # .. # . # entrypoint: /fastapi_sample/docker/wait-for-it.sh db 5432 postgres postgres db_fastapi_sample # ココ # ... host="$1" shift port="$1" shift user="$1" shift password="$1" shift database="$1" shift cmd="$@" echo "Waiting for postgresql" until pg_isready -h"$host" -U"$user" -p"$port" -d"$database" do echo -n "." sleep 1 done >&2 echo "PostgreSQL is up - executing command" exec $cmd # 僕は優しいのでMySQL用も書いてあげるのだ #!/bin/sh # set -e # host="$1" # shift # user="$1" # shift # password="$1" # shift # cmd="$@" # echo "Waiting for mysql" # until mysqladmin ping -h "$host" --silent # do # echo -n "." # sleep 1 # done # >&2 echo "MySQL is up - executing command" # exec $cmd |
rundevserver.sh
用于启动uvicorn和启动应用程序的shell脚本文件。
?每次
用uvicorn启动应用程序
热重载吗?-重载(如果更改python文件,它将立即反映出来)
如果您将?-port更改为8000,则还需要在nginx的app.conf
中更改8000。
1 2 3 4 5 6 7 | pip3 install -r requirements.txt uvicorn main:app\ --reload\ --port 8000\ --host 0.0.0.0\ --log-level debug |
最终目录结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | fastapi_sample ├── Dockerfile ├── docker │ ├── nginx │ │ ├── conf.d │ │ │ └── app.conf │ │ └── ssl │ │ ├── server.crt │ │ ├── server.csr │ │ └── server.key │ ├── rundevserver.sh │ └── wait-for-it.sh ├── docker-compose.yml ├── main.py └── requirements.txt |
容器启动
1 | $ docker-compose up -d |
如果出现此错误,则Docker映像可能已用完磁盘,因此请使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | . .. ... Get:1 http://security.debian.org/debian-security buster/updates InRelease [65.4 kB] Get:2 http://deb.debian.org/debian buster InRelease [122 kB] Get:3 http://deb.debian.org/debian buster-updates InRelease [49.3 kB] Err:1 http://security.debian.org/debian-security buster/updates InRelease At least one invalid signature was encountered. Err:2 http://deb.debian.org/debian buster InRelease At least one invalid signature was encountered. Err:3 http://deb.debian.org/debian buster-updates InRelease At least one invalid signature was encountered. Reading package lists... W: GPG error: http://security.debian.org/debian-security buster/updates InRelease: At least one invalid signature was encountered. E: The repository 'http://security.debian.org/debian-security buster/updates InRelease' is not signed. W: GPG error: http://deb.debian.org/debian buster InRelease: At least one invalid signature was encountered. E: The repository 'http://deb.debian.org/debian buster InRelease' is not signed. W: GPG error: http://deb.debian.org/debian buster-updates InRelease: At least one invalid signature was encountered. E: The repository 'http://deb.debian.org/debian buster-updates InRelease' is not signed. ... .. . |
从浏览器访问
检查容器启动是否完成。
如果STATUS列为" Up ...",则可以。
1 2 3 4 5 | $ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES b77fa2465fb1 nginx:alpine "/docker-entrypoint.…" About a minute ago Up About a minute 0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp nginx_fastapi_sample eb18438efd2c fastapi_sample_app "/fastapi_sample/doc…" About a minute ago Up About a minute 8000/tcp app_fastapi_sample 383b0e46af68 postgres:12.4-alpine "docker-entrypoint.s…" About a minute ago Up About a minute 0.0.0.0:5432->5432/tcp db_fastapi_sample |
访问
https://本地主机,如果显示
使用库" pydantic"处理环境变量(.env)
.env 文件创建
1 | $ touch .env |
fastapi_sample / .env
1 2 | DEBUG=True DATABASE_URL=postgresql://postgres:postgres@db:5432/db_fastapi_sample |
创建一个配置文件以使用" pydatic"
读取环境变量
库``pydantic''使实现从.env读取值的过程变得容易。
这也是超级方便的,因为它可以根据模具进行铸造。
您无需编写令人讨厌的条件表达式,例如
安装pydantic
fastapi_sample / requirements.txt
1 | pydantic[email]==1.6.1 # 追記したらpip3 install |
创建一个文件夹" core",并在其下直接创建" config.py"
1 2 | $ mkdir core $ touch core/config.py |
fastapi_sample /核心/ config.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | import os from functools import lru_cache from pydantic import BaseSettings PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) class Environment(BaseSettings): """ 環境変数を読み込むファイル """ debug: bool # .envから読み込んだ値をbool型にキャッシュ database_url: str class Config: env_file = os.path.join(PROJECT_ROOT, '.env') @lru_cache def get_env(): """ 「@lru_cache」でディスクから読み込んだ.envの結果をキャッシュする """ return Environment() |
准备Alembic(迁移工具)环境和模型
仍然安装
将Alembic和sqlalchemy添加到
requirements.txt并使用
(对于遵循Docker步骤的人员,为
fastapi_samepl / requirements.txt
1 2 3 4 5 6 7 8 9 10 | alembic==1.4.2 # 追記 . .. ... psycopg2==2.8.6 # 追記 . .. ... SQLAlchemy==1.3.18 # 追記 SQLAlchemy-Utils==0.36.8 # 追記 |
直接在项目根目录
下创建一个alembic环境
1 | $ alembic init migrations # Dockerの手順を踏んだ人はdocker-compose exec app alembic init migrations |
将在项目根目录中创建一个Alembic模板。 (Alembic.ini,迁移文件夹)
1 2 3 4 5 6 7 8 9 10 11 | fastapi_sample(プロジェクトルート) ├── ... ├── .. ├── . ├── alembic.ini ├── migrations │ ├── README │ ├── env.py │ ├── script.py.mako │ └── versions └── ... |
基本型号可用
首先,实现基本模型。
1 | touch models.py |
fastapi_sample /迁移/ models.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 | from sqlalchemy import Column from sqlalchemy.dialects.postgresql import INTEGER, TIMESTAMP from sqlalchemy.ext.declarative import declarative_base, declared_attr from sqlalchemy.sql.functions import current_timestamp Base = declarative_base() class BaseModel(Base): """ ベースモデル """ __abstract__ = True id = Column( INTEGER, primary_key=True, autoincrement=True, ) created_at = Column( 'created_at', TIMESTAMP(timezone=True), server_default=current_timestamp(), nullable=False, comment='登録日時', ) updated_at = Column( 'updated_at', TIMESTAMP(timezone=True), onupdate=current_timestamp(), comment='最終更新日時', ) @declared_attr def __mapper_args__(cls): """ デフォルトのオーダリングは主キーの昇順 降順にしたい場合 from sqlalchemy import desc # return {'order_by': desc('id')} """ return {'order_by': 'id'} |
用户模型可用
模型可以是任何模型,但是我将创建一个用户模型,因为在编写身份验证版本时它似乎可以使用。
我不敢划分模块。 (生成迁移文件很麻烦,因此最好不要这样做?)
fastapi_sample /迁移/ models.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 | from sqlalchemy import ( BOOLEAN, # 追加 Column INTEGER, TIMESTAMP, VARCHAR, # 追加 ) from sqlalchemy.ext.declarative import declarative_base, declared_attr from sqlalchemy.sql.functions import current_timestamp Base = declarative_base() class BaseModel(Base): ... .. . class User(BaseModel): __tablename__ = 'users' email = Column(VARCHAR(254), unique=True, nullable=False) password = Column(VARCHAR(128), nullable=False) last_name = Column(VARCHAR(100), nullable=False) first_name = Column(VARCHAR(100), nullable=False) is_admin = Column(BOOLEAN, nullable=False, default=False) is_active = Column(BOOLEAN, nullable=False, default=True) |
修改环境
fastapi_sample /迁移/ env.py
1 2 3 4 5 6 7 8 | from migrations.models import Base # 追加 ... .. . target_metadata = Base.metadata # メタデータをセット ... .. . |
更改数据库连接目标
现在已经实现了
模型,我想立即执行迁移。
但是,由于alembic仍然是项目模板,因此
fastapi_sample / alembic.ini
1 2 3 4 5 6 7 | . .. ... sqlalchemy.url = driver://user:pass@localhost/dbname ... .. . |
由于它是
,因此,自然迁移文件生成命令等将失败。
1 2 3 4 5 6 7 8 9 10 11 | $ docker-compose exec app alembic revision --autogenerate Traceback (most recent call last): File "/root/local/python-3.8.5/bin/alembic", line 8, in <module> sys.exit(main()) ... (長いので省略) . cls = registry.load(name) File "/root/local/python-3.8.5/lib/python3.8/site-packages/sqlalchemy/util/langhelpers.py", line 267, in load raise exc.NoSuchModuleError( sqlalchemy.exc.NoSuchModuleError: Can't load plugin: sqlalchemy.dialects:driver |
最好直接重写alembic.ini,但这并不令人愉快,因为在开发环境,登台环境和生产环境中会创建不同的alembic.ini文件。
因此,在生成迁移文件或执行迁移时,请尝试使用
安装python-dotenv
fastapi_sample / requirements.txt
1 | python-dotenv==0.14.0 # 追記したらインストールする |
固定env.py
fastapi_sample /迁移/ env.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | import os from core.config import PROJECT_ROOT from dotenv import load_dotenv . .. ... # other values from the config, defined by the needs of env.py, # can be acquired: # my_important_option = config.get_main_option("my_important_option") # ... etc. # alembic.iniの'sqlalchemy.url'を.envのDATABASE_URLで書き換える load_dotenv(dotenv_path=os.path.join(PROJECT_ROOT, '.env')) config.set_main_option('sqlalchemy.url', os.getenv('DATABASE_URL')) def run_migrations_offline(): ... .. . |
再次执行迁移文件生成命令
1 2 3 4 5 6 | % docker-compose exec app alembic revision --autogenerate None /fastapi_sample INFO [alembic.runtime.migration] Context impl PostgresqlImpl. INFO [alembic.runtime.migration] Will assume transactional DDL. INFO [alembic.autogenerate.compare] Detected added table 'users' Generating /fastapi_sample/migrations/versions/2bc0a23e563c_.py ... done |
迁移文件已生成。
1 2 3 4 5 6 | migrations ├── ... ├── ... ├── models.py └── versions └── 2bc0a23e563c_.py # マイグレーションファイルができた。 |
但是,如果按原样放置,迁移文件的生成顺序一目了然,并且迁移文件中描述的创建日期(创建日期)仍为UTC,因此我想对其进行更改到日本时间。
修改
alembic.ini。
fastapi_sample / alembic.ini
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | . .. ... [alembic] # path to migration scripts script_location = migrations # ファイルの接頭に「YYYYMMDD_HHMMSS_」がつくようにする # template used to generate migration files file_template = %%(year)d%%(month).2d%%(day).2d_%%(hour).2d%%(minute).2d%%(second).2d_%%(rev)s_%%(slug)s # タイムゾーンを日本時間に # timezone to use when rendering the date # within the migration file as well as the filename. # string value is passed to dateutil.tz.gettz() # leave blank for localtime timezone = Asia/Tokyo ... .. . |
删除先前生成的迁移文件,然后再次生成迁移文件。
1 2 3 4 5 6 | migrations ├── ... ├── ... ├── models.py └── versions └── 20201024_200033_8a19c4c579bf_.py # ファイル内のCreate Dateも日本時間になっている |
迁移运行
1 | $ alembic upgrade head # Dockerの手順を踏んだ人はdocker-compose exec app alembic upgrade head |
用户表已正确创建。
(Alembic_version是alembic用于版本控制的表,并会自动生成)
允许在容器启动时运行迁移
现在,每次启动容器时,都会对其进行迁移。
fastapi_sample / docker / rundevserver.sh
1 2 3 4 5 6 7 8 9 | pip3 install -r requirements.txt alembic upgrade head # 追記 uvicorn main:app\ --reload\ --port 8000\ --host 0.0.0.0\ --log-level debug |
数据访问类的创建
创建基础数据访问类
描述所有案例的简单获取,一个案例的获取,注册,更新,删除等。
base.py中描述的
1 2 | mkdir crud touch crud/base.py |
fastapi_sample /克鲁德/ base.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 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 | from core.config import get_env from migrations.models import Base from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker, scoped_session, query from typing import Generator, List, TypeVarfrom ModelType = TypeVar("ModelType", bound=Base) connection = create_engine( get_env().database_url, echo=get_env().debug, encoding='utf-8', ) def db_session() -> scoped_session: """ 新しいDBコネクションを返す """ return scoped_session(sessionmaker(connection)) def generate_db_session() -> Generator[scoped_session, None, None]: """ DBコネクションのジェネレータ """ yield get_db_session() class BaseCRUD: """ データアクセスクラスのベース """ model: ModelType = None def __init__(self, db_session: scoped_session) -> None: self.db_session = db_session self.model.query = self.db_session.query_property() def get_query(self) -> query.Query: """ ベースのクエリ取得 """ return self.model.query def gets(self) -> List[ModelType]: """ 全件取得 """ return self.get_query().all() def get_by_id(self, id: int) -> ModelType: """ 主キーで取得 """ return self.get_query().filter_by(id=id).first() def create(self, data: dict = {}) -> ModelType: """ 新規登録 """ obj = self.model() for key, value in data.items(): if hasattr(obj, key): setattr(obj, key, value) self.db_session.add(obj) self.db_session.flush() self.db_session.refresh(obj) return obj def update(self, obj: ModelType, data: dict = {}) -> ModelType: """ 更新 """ for key, value in data.items(): if hasattr(obj, key): setattr(obj, key, value) self.db_session.flush() self.db_session.refresh(obj) return obj def delete_by_id(self, id: int) -> None: """ 主キーで削除 """ obj = self.get_by_id(id) if obj: obj.delete() self.db_session.flush() return None |
创建用户数据访问类
1 | touch crud/crud_user.py |
fastapi_sample /克鲁德/ crud_user.py
1 2 3 4 5 6 7 8 | from crud.base import BaseCRUD from migrations.models import User class CRUDUser(BaseCRUD): """ ユーザーデータアクセスクラスのベース """ model = User |
API实施
感谢您的耐心等待,它终于实现了API。
API实施准备
实现依赖关系注入(Depends)功能,该功能将DB会话存储在请求信息中。
这个快速API依赖注入系统非常强大,甚至可以组织参数。
抱歉,我还不完全了解它,但是请参考官方文档了解详细信息。
1 2 | mkdir dependencies touch dependencies/__init__.py |
依赖项/ __ init__.py
1 2 3 4 5 6 7 8 9 10 11 12 | from fastapi import Depends, Request from crud.base import generate_db_session from sqlalchemy.orm import scoped_session async def set_db_session_in_request( request: Request, db_session: scoped_session = Depends(generate_db_session) ): """ リクエストにDBセッションをセットする """ request.state.db_session = db_session |
创建一个实现API
的文件夹
1 | mkdir -p api/v1 |
用户列表获取API实施
1 | touch api/v1/user.py |
fastapi_sample / api / v1 / user.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | from crud.crud_user import CRUDUser from fastapi import Request from fastapi.encoders import jsonable_encoder class UserAPI: """ ユーザーに関するAPI """ @classmethod def gets(cls, request: Request): """ 一覧取得 """ # 依存性注入システムを用いて、リクエスト情報にDBセッションをセットしたので、 # ここで「request.state.db_session」を使用することができる return jsonable_encoder(CRUDUser(request.state.db_session).gets()) |
API路由器实现
路由器
1 2 | mkdir -p api/endpoints/v1 touch api/endpoints/v1/user.py |
fastapi_sample / api /端点/ v1 / user.py
1 2 3 4 5 6 7 8 9 10 11 12 | from api.v1.user import UserAPI from dependencies import set_db_session_in_request from fastapi import APIRouter, Depends, Request router = APIRouter() @router.get( '/', dependencies=[Depends(set_db_session_in_request)]) async def gets(request: Request): return UserAPI.gets(request) |
将路由器注册为入口点
创建API路由器
1 | touch api/endpoints/v1/__init__.py |
fastapi_sample / api /端点/ v1 / __ init__.py
1 2 3 4 5 6 7 8 | from fastapi import APIRouter from api.endpoints.v1 import user api_v1_router = APIRouter() api_v1_router.include_router( user.router, prefix='users', tags=['users']) |
编辑main.py
fastapi_sample / main.py
1 2 3 4 5 6 7 8 9 10 11 | from api.endpoints.v1 import api_v1_router # 追記 from fastapi import FastAPI app = FastAPI() app.include_router(api_v1_router, prefix='/api/v1') # 追記 @app.get("/") async def root(): return {"message": "Hello World"} |
从浏览器访问https:// // localhost / docs,您应该看到已实现的API。
定义架构(表单?)进行注册/更新
1 2 | mkdir api/schemas touch api/schemas/user.py |
fastapi_sample / api /模式/ user.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 | from pydantic import BaseModel, EmailStr from typing import Optional class BaseUser(BaseModel): email: EmailStr last_name: str first_name: str is_admin: bool class CreateUser(BaseUser): password: str class UpdateUser(BaseUser): password: Optional[str] last_name: Optional[str] first_name: Optional[str] is_admin: bool class UserInDB(BaseUser): class Config: orm_mode = True |
添加了用于用户注册/更新/删除的API
fastapi_sample / api / v1 / user.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 | from api.schemas.user import CreateUser, UpdateUser, UserInDB from crud.crud_user import CRUDUser from fastapi import Request # from fastapi.encoders import jsonable_encoder # 削除 from typing import List class UserAPI: """ ユーザーに関するAPI """ @classmethod def gets(cls, request: Request) -> List[UserInDB]: """ 一覧取得 """ return CRUDUser(request.state.db_session).gets() # jsonable_encoderは使わない @classmethod def create( cls, request: Request, schema: CreateUser ) -> UserInDB: """ 新規登録 """ return CRUDUser(request.state.db_session).create(schema.dict()) @classmethod def update( cls, request: Request, id: int, schema: UpdateUser ) -> UserInDB: """ 更新 """ crud = CRUDUser(request.state.db_session) obj = crud.get_by_id(id) return CRUDUser(request.state.db_session).update(obj, schema.dict()) @classmethod def delete(cls, request: Request, id: int) -> None: """ 削除 """ return CRUDUser(request.state.db_session).delete_by_id(id) |
编辑用户的API路由器
fastapi_sample / api /端点/ v1 / user.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 | from api.schemas.user import CreateUser, UpdateUser, UserInDB from api.v1.user import UserAPI from dependencies import set_db_session_in_request from fastapi import APIRouter, Depends, Request from typing import List router = APIRouter() @router.get( '/', response_model=List[UserInDB], # response_modelを追加 dependencies=[Depends(set_db_session_in_request)]) def gets(request: Request) -> List[UserInDB]: """ 一覧取得 """ return UserAPI.gets(request) @router.post( '/', response_model=UserInDB, dependencies=[Depends(set_db_session_in_request)]) def create(request: Request, schema: CreateUser) -> UserInDB: """ 新規登録 """ return UserAPI.create(request, schema) @router.put( '/{id}/', response_model=UserInDB, dependencies=[Depends(set_db_session_in_request)]) def update(request: Request, id: int, schema: UpdateUser) -> UserInDB: """ 更新 """ return UserAPI.update(request, id, schema) @router.delete( '/{id}/', dependencies=[Depends(set_db_session_in_request)]) def delete(request: Request, id: int) -> None: """ 削除 """ return UserAPI.delete(request, id) |
从浏览器访问并检查
您已成功实现CRUD API。
为
路由器指定
它还将反映在swagger-UI上。
*列表获取响应
实施请求中间件
好的,我能够执行API,但是由于我尚未执行数据库会话的提交,因此即使执行注册/更新/删除操作,数据库也不会更改。
为了反映数据库中的更改,请实施并应用以下请求中间件。
?请求完成后,提交数据库会话,然后放弃该数据库会话
?如果发生错误,请回滚,然后放弃数据库会话
1 2 | mkdir middleware touch middleware/__init__.py |
fastapi_sample /中间件/ __ init__.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | from fastapi import Request, Response from starlette.middleware.base import BaseHTTPMiddleware class HttpRequestMiddleware(BaseHTTPMiddleware): async def dispatch(self, request: Request, call_next) -> Response: try: response = await call_next(request) # 予期せぬ例外 except Exception as e: print(e) request.state.db_session.rollback() raise e # 正常終了時 else: request.state.db_session.commit() return response # DBセッションの破棄は必ず行う finally: request.state.db_session.remove() |
将中间件应用到入口点
fastapi_sample / main.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | from api.endpoints.v1 import api_v1_router from fastapi import FastAPI from middleware import HttpRequestMiddleware # 追加 app = FastAPI() app.include_router(api_v1_router, prefix='/api/v1') # ミドルウェアの設定 app.add_middleware(HttpRequestMiddleware) # 追加 @app.get("/") async def root(): return {"message": "Hello World"} |
通过执行CRUD
进行确认
现在它已安全地反映在数据库中。
测试代码实现
现在我们已经实现了
CRUD,是时候实现测试代码了。
但是,如果仅从测试代码执行API,则会在数据库中对其进行注册,更新和删除,因此每次执行测试时结果可能会有所不同。
为防止这种情况,请创建一个测试数据库并在运行测试时使用它。
安装pytest
fastapi_sample / requirements.txt
1 | pytest==6.1.0 # 追記したらrequirements.txtをインストールし直すのを忘れずに |
向环境变量
添加了测试数据库连接信息
fastapi_sample / .env
1 2 3 | DEBUG=True DATABASE_URL=postgresql://postgres:postgres@db:5432/db_fastapi_sample TEST_DATABASE_URL=postgresql://postgres:postgres@db:5432/test_db_fastapi_sample # 追加 |
fastapi_sample /核心/ config.py
1 2 3 4 5 6 | class Environment(BaseSettings): """ 環境変数を読み込むファイル """ debug: bool database_url: str test_database_url: str # 追加 |
创建用于实施测试代码
的文件夹
1 | mkdir tests |
创建conftest.py
一个实现pytest设置代码的文件。
1 | $ touch tests/conftest.py |
fastapi_sample /测试/ conftest.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 | import psycopg2 from core.config import get_env from migrations.models import Base from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT # from sqlalchemy.orm import scoped_session, sessionmaker from sqlalchemy_utils import database_exists, drop_database from tests.base import test_db_connection # conftestで初期データを登録する場合はこのSessionを使用する # Session = scoped_session( # sessionmaker( # bind=test_db_connection # ) # ) def create_test_database(): # テストDBが削除されずに残ってしまっている場合は削除 if database_exists(get_env().test_database_url): drop_database(get_env().test_database_url) # テストDB作成 _con = \ psycopg2.connect('host=db user=postgres password=postgres') _con.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT) _cursor = _con.cursor() _cursor.execute('CREATE DATABASE test_db_fastapi_sample') # テストDBにテーブル追加 Base.metadata.create_all(bind=test_db_connection) def pytest_sessionstart(session): """ pytest実行時に一度だけ呼ばれる処理 """ # テストDB作成 create_test_database() def pytest_sessionfinish(session, exitstatus): """ pytest終了時に一度だけ呼ばれる処理 """ # テストDB削除 if database_exists(get_env().test_database_url): drop_database(get_env().test_database_url) |
创建数据库会话管理类以进行测试
1 | $ touch tests/db_session.py |
fastapi_sample /测试/ db_session.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 53 54 55 56 57 58 59 60 61 | from core.config import get_env from sqlalchemy import create_engine from sqlalchemy.orm import scoped_session, sessionmaker, Session from threading import local as thread_local _thread_local = thread_local() def set_current_test_db_session(db_session: scoped_session): """ スレッドローカルにDBセッションをセット """ setattr(_thread_local, 'db_session', db_session) def get_current_test_db_session() -> scoped_session: """ スレッドローカルからDBセッションを取得 """ return getattr(_thread_local, 'db_session') class TestingDBSession(Session): """ commit()の挙動を変えるため、Sessionクラスをオーバーライド """ def commit(self): # データアクセスクラス(fastapi_sample/crud)やAPIの中でflush()は実行する想定なので、 # ここでflush()はとりあえず不要 # self.flush() self.expire_all() class test_scoped_session(scoped_session): """ リクエストミドルウェアのremove()で何も実行されないように、scoped_sessionクラスをオーバーライド """ def remove(self): pass def test_db_session_remove(self): """ テストDB用のremove() """ if self.registry.has(): self.registry().close() self.registry.clear() test_db_connection = create_engine( get_env().test_database_url, encoding='utf8', pool_pre_ping=True, ) def get_test_db_session(): """ テストDBセッションを返す """ return test_scoped_session( sessionmaker( bind=test_db_connection, class_=TestingDBSession, expire_on_commit=False, ) ) |
创建API基本测试类
1 | touch tests/base.py |
fastapi_sample /测试/ base.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 | from crud.base import generate_db_session from fastapi.testclient import TestClient from main import app from tests.db_session import get_test_db_session, set_current_test_db_session class BaseTestCase: def setup_method(self, method): """ 前処理 """ self.db_session = get_test_db_session() set_current_test_db_session(self.db_session) # スレッドローカルにDBセッションをセット # APIクライアントの設定 self.client = TestClient(app, base_url='https://localhost',) # DBをテスト用のDBでオーバーライド app.dependency_overrides[generate_db_session] = \ self.override_generate_db_session def teardown_method(self, method): """ 後処理 """ self.db_session.test_db_session_remove() # ロールバック # オーバーライドしたDBを元に戻す app.dependency_overrides[self.override_get_db] = \ get_db_session def override_generate_db_session(self): """ DBセッションの依存性オーバーライド関数 """ yield self.db_session |
点是此数据库覆盖部分。
应用程序引用的数据库将切换到测试数据库。
1 2 | # DBをテスト用のDBでオーバーライド app.dependency_overrides[generate_db_session] = self.override_generate_db_session |

*图像的函数名称为" get_db_session"