使用 Fast API 和 PostgreSQL 进行 DevOps:如何使用 Docker 容器化 Fast API 应用程序

FastAPI是一个开源现代框架,用于在 Python 中构建 API。

PostgreSQL是一个开源的对象关系数据库管理系统。

在本教程中,我们将使用 Fast API 构建示例 RESTful API,并利用 PostgreSQL 持久数据的强大功能。然后,我们将使用Dockerfile和Docker Compose文件对 API 和数据库进行容器化。Dockerfile 是一个文本文件,其中包含将在 Docker Compose 文件中执行以构建容器的一系列指令。Docker compose是一个定义和共享多容器Docker容器的工具。我们的应用程序将包含两个容器。Fast API 容器和 PostgreSQL 容器。

前提要求与工具

  1. Docker - 您需要对 Docker 的工作原理有基本的了解。要了解 docker 的工作原理,您可以前往我之前的docker 入门帖子。您将学习如何安装 docker、docker 的工作原理以及 docker 命令。

  2. Python - 您需要在计算机上安装 Python。最好是Python 3.10。

  3. VSCode

  4. 相关网址

    FastAPI - https://fastapi.tiangolo.com/

    PostgreSQLhttps://www.postgresql.org

    Dockerfile https://docs.docker.com/engine/reference/builder/

    Docker Compose - https://docs.docker.com/get-started/08_using_compose/

    docker 入门 - https://dev.to/mbuthi/docker-2oge

    Python官网 - https://www.python.org/

    VsCode官网 - https://code.visualstudio.com/download

FastAPI 入门

我们将使用 Python 构建一个产品列表示例应用程序,用户将能够通过 API 执行 CRUD 操作。然后我们将使用 PostgreSQL 来保存产品数据。但是,我们需要了解项目目录结构是什么样的。下面是 FastAPI 中项目目录结构的快照:

.
└── FastAPI_APP/
    ├── app/
    │   ├── api/
    │   │   ├── v1/
    │   │   │   ├── endpoints/
    │   │   │   │   ├── __init__.py
    │   │   │   │   └── products.py
    │   │   │   ├── __init__.py
    │   │   │   └── api.py
    │   │   ├── __init__.py
    │   │   └── deps.py
    │   ├── core/
    │   │   ├── __init__.py
    │   │   └── settings.py
    │   ├── crud/
    │   │   ├── __init__.py
    │   │   ├── base.py
    │   │   └── product.py
    │   ├── db/
    │   │   ├── __init__.py
    │   │   └── session.py
    │   ├── models/
    │   │   ├── __init__.py
    │   │   ├── basemodel.py
    │   │   └── products.py
    │   ├── schemas/
    │   │   ├── __init__.py
    │   │   └── product.py
    │   └── utils/
    │       ├── __init__.py
    │       └── idgen.py
    └── main.py

粗略地概括一下:

  • FastAPI_APP - 这是我们应用程序的根目录。

  • app - 保存我们 API 的服务。

  • main.py - 这是 API 入口点。

  • api - 包含 API 端点。

  • core - 包含核心功能,例如设置和日志记录。

  • crud - 包含 CRUD(创建、读取、更新、删除)操作。

  • db - 包含与数据库相关的代码。

  • models - 包含数据库模型。

  • utils - 包含实用函数和类。

要开始构建 API,您需要安装 Vscode 或您喜欢的 IDE。然后使用上面所示的目录结构创建一个新项目。

设置我们的docker环境

我们将首先为我们的应用程序创建一个 docker compose 文件和一个 docker 文件。
因此,前往 vscode code 打开名为Dockerfile的文件并粘贴以下说明。

# 使用来自 docker hub 的 python 官方镜像
FROM python:3.12-rc-slim-bullseye


# 防止 pyc 文件被复制到容器中
ENV PYTHONDONTWRITEBYTECODE 1

# 确保 python 输出记录在容器的终端中
ENV PYTHONUNBUFFERED 1




RUN apt-get update \
  # 构建 Python 包的依赖项
  && apt-get install -y build-essential \
  # psycopg2 依赖项
  && apt-get install -y libpq-dev \
  # Translations 依赖项
  && apt-get install -y gettext \
  # 清理未使用的文件
  && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \
  && rm -rf /var/lib/apt/lists/*

# 将文件“requirements.txt”从本地构建上下文复制到容器的文件系统。
COPY ./requirements.txt /requirements.txt

# 安装python依赖项
RUN pip install -r /requirements.txt


# 设置工作目录
WORKDIR /app

# 运行 Uvicorn 来启动您的 Python Web 应用程序
CMD ["uvicorn", "main:app", "--host", "0.0.0.0" "--port" "8000"]

接下来,我们将创建 docker compose 文件。在 vscode 中打开docker-compose.yml文件并粘贴以下说明。

# 指定 compose 版本version: '3.8'# 为我们的 docker compose setupservices 指定服务:
  api:
    build:
      context: .
      # 指定我们的 dockerfile 路径。 我们的 Dockerfile 位于根文件夹中
      dockerfile: .
      # 指定图像名称
      image: products_api
      # 该卷用于将主机上的文件和文件夹映射到容器
      # 因此,如果我们更改主机上的代码,docker容器中的代码也会更改
      volumes:
        - .:/app
      # 将主机上的8000端口映射到容器上的8000端口
      ports:
        - 8000:8000
      # 指定.env文件路径
      env_file:
        - ./env
      # 定义对“products_db”服务的依赖,因此它首先启动
      depends_on:
        - products_db
  products_db:
    # 指定我们数据库的图像名称
    # 如果在我们的本地存储库中找不到该图像
    # 将从 Docker Hub 的 docker 注册表中拉取
    image: 16rc1-alpine3.18
    # 安装卷以保存 postgreSQL 数据
    volumes:
      - postgres_data:/var/lib/postgresql/data/
    environment:  # Use environment variables for db configuration
      - POSTGRES_USER=${POSTGRES_USER}
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
      - POSTGRES_DATABASE=${POSTGRES_DATABASE}# Define a volume for persisting postgreSQL datavolumes:
  postgres_data:

FastAPI 的环境变量。

接下来,我们将创建一个.env文件并实例化我们的环境变量。在你的 vscode 中打开 .env 文件并包含以下变量

# PostgreSQL 数据库主机
POSTGRES_HOST=products_db

# PostgreSQL 数据库用户
POSTGRES_USER=username

# PostgreSQL 数据库密码
POSTGRES_PASSWORD=password

# PostgreSQL 数据库名称
POSTGRES_DATABASE=database

# PostgreSQL 数据库端口
POSTGRES_PORT=5432

# 用于连接 PostgreSQL 的异步数据库 URI
ASYNC_DATABASE_URI=postgresql+asyncpg://username:password@products_db:5432/database

# 项目或应用程序的名称
PROJECT_NAME=Product Listings

.env 文件包含敏感变量。将这些
敏感变量包含在 .env 文件中始终是一个好习惯。

生成唯一 ID。

在我们的 FastAPI 应用程序中,我们将定义一个强大的实用函数来生成唯一的 ID。该函数将使用 UUID 模块。转到 utils module/ 文件夹并打开 idgen.py 文件并粘贴下面的代码片段。

import uuid

def idgen() -> str:
    # 生成随机 uuid 字符串
    return str(uuid.uuid4().hex)

FastAPI 中的设置配置

接下来,我们将创建一个配置类。该类将从 pydantic 基设置类继承。该类将负责将环境变量加载到应用程序上下文并定义其他应用程序设置。在 vscode 中打开名为 settings.py 的文件并插入以下代码片段。

# 导入包
from pydantic_settings import BaseSettings
import os
from dotenv import load_dotenv
import secrets

load_dotenv()
class Settings(BaseSettings):
    """
    应用程序设置和配置参数

    此类使用 pydantic 数据验证库定义应用程序设置
    """
    PROJECT_NAME: str = os.getenv("PROJECT_NAME")
    API_V1_STR: str = "/api/v1"
    ASYNC_DATABASE_URI: str = os.getenv("ASYNC_DATABASE_URI")
    SECRET_KEY: str = secrets.token_urlsafe(32)

settings = Settings()

在 Fast API 中创建我们的模型

我们将从创建一个基类开始。基类将包含所有模型中的公共属性。这将有助于保持我们的代码干燥。打开位于 models 文件夹中名为 base.py 的文件。在文件内粘贴以下代码片段

from sqlalchemy import DateTime, func
from sqlalchemy.orm import Mapped, declared_attr, DeclarativeBase, mapped_column
from app.utils.idgen import idgen
from datetime import datetime


class Base_(DeclarativeBase):
    """
    SQLAlchemy 模型的基类,具有保持 DRY(不要重复)的公共属性。

    此类旨在充当 SQLAlchemy 模型的基类。
    它定义了常见的属性,例如表名、创建时间戳、
    并更新可以被其他模型继承的时间戳,帮助您
    坚持 DRY(不要重复自己)原则。

    属性:
        __tablename__ (str): 表名,源自小写的类名。
        id (str): 每条记录的唯一ID。
        created_on (datetime): 创建记录的时间戳。
        updated_on (datetime, optional): 上次更新记录的时间戳。
            默认为“无”,直到发生更新。

    示例:
        要使用此基类创建 SQLAlchemy 模型:


        class YourModel(Base_):
            # 在此处为您的模型定义其他属性。

    """
    @declared_attr
    def __tablename__(cls):
        # 表名源自小写的类名
        return cls.__name__.lower()

    # 每条记录的唯一 UUID ID
    id: Mapped[str] = mapped_column(primary_key=True, default=idgen,index=True)

    # 记录创建的时间戳
    created_on: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())

    # 记录更新的时间戳,最初为 None,直到发生更新
    updated_on: Mapped[datetime] = mapped_column(DateTime(timezone=True), onupdate=func.now(), nullable=True)

基类包含 id 属性,它是每个记录的唯一 UUID,还包含created_on 属性,它是记录创建的时间戳,还包含updated_on,它是记录更新的时间戳。

接下来,我们将定义我们产品的模型。该模型将从基类继承Base_。打开名为 products.py 的文件,该文件包含在名为 models 的文件夹中。在文件内,粘贴以下代码片段。

from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy import String
from .base import Base_

class Product(Base_):
    """
    这是用于定义产品模型的 SQLAlchemy 类。
    它继承了Base_类的所有属性和方法。
    该类定义了常用属性,例如名称、价格图像、和重量。
    属性:
        name (str): 产品名称
        price (str): 产品价格
        image (str): 产品图片 url
        weight (str): 产品价格
    'nullable=False' 表示这些列在数据库中不能有 NULL 值。
    """
    name: Mapped[str] = mapped_column(String(30), index=True, nullable=False)
    price: Mapped[str] = mapped_column(String(30), nullable=False)
    image: Mapped[str] = mapped_column(String, nullable=False)
    weight: Mapped[str] = mapped_column(String, nullable=False)

这Mapped[str]只是一个 Python 类型的提示。它强调该属性将保存字符串类型的值。取代mapped_column了之前的 sqlalchemy Column。

在 Fast API 中创建模式。

我们现在将定义我们的pydantic模式。这些模式充当数据类,定义某个 API 端点期望接收哪些数据,以便将请求视为有效请求。它们还可以在 Fast API 中使用来定义响应模型,即端点返回的响应。打开名为product.py 的文件(包含在名为 的文件夹中)schemas并粘贴以下代码片段。

from typing import Optional
from pydantic import BaseModel

class ProductBase(BaseModel):
    name: str      # 产品名称(必填)
    price: str     # 产品价格(必填)
    image: str     # 产品图片的 URL 或路径(必填)
    weight: str    # 产品的重量(必填)

class ProductCreate(ProductBase):
    ...

class ProductUpdate(ProductBase):
    ...

class ProductPatch(ProductBase):
    name: Optional[str]   # 修补时名称是可选的
    price: Optional[str]  # 修补时价格是可选的
    image: Optional[str]  # 修补时图像是可选的
    weight: Optional[str] # 修补时权重是可选的


class Product(ProductBase):
    id: str

    class Config:
        orm_mode = True

Optional从 Python 类型模块导入,该模块定义该字段不是必需的,因此可以为 None。

在 Fast API 中创建 CRUD 操作

我们现在将定义创建、读取、更新和删除方法。首先,我们将为操作创建一个基类。基类类将有助于维护 Python 中的 DRY 代码设计。各种SQLAlchemy模型也会继承该类来执行数据库操作。因此,打开名为 crud 的文件夹中名为 base.py 的文件。粘贴下面的代码片段。

from typing import Any, Dict, Generic, Optional, Type, TypeVar
from pydantic import BaseModel
from sqlalchemy.ext.declarative import DeclarativeMeta
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.future import select
from sqlalchemy import func, update
from fastapi.encoders import jsonable_encoder

ModelType = TypeVar("ModelType", bound=DeclarativeMeta)
CreateSchemaType = TypeVar("CreateSchemaType", bound=BaseModel)
UpdateSchemaType = TypeVar("UpdateSchemaType", bound=BaseModel)

class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
    """
      SQLAlchemy 模型的通用 CRUD(创建、读取、更新、删除)操作。

    此类提供了一组可与 SQLAlchemy 模型一起使用的通用 CRUD 操作。
    它包括创建、检索、更新和删除数据库中记录的方法。

    参数:
        model (Type[ModelType]): 要执行 CRUD 操作的 SQLAlchemy 模型类。

    例子:
        为特定模型(例如用户模型)创建 CRUD 实例:

        ```

python
        crud_user = CRUDBase[Prodcut, ProductCreateSchema, ProductUpdateSchema]


        ```
    """
    def __init__(self, model: Type[ModelType]):
        self.model = model
    # 获取单个实例
    async def get(self, db: AsyncSession, obj_id: str) -> Optional[ModelType]:
        query = await db.execute(select(self.model).where(self.model.id == obj_id))
        return query.scalar_one_or_none()

    # 获取所有多个实体
    async def get_multi(self, db: AsyncSession, *, skip: int = 0, limit: int = 100) -> ModelType:
        query = await db.execute(select(self.model))
        return query.scalars().all()

    # 搜索特定实体
    async def get_by_params(self, db: AsyncSession, **params: Any) -> Optional[ModelType]:
        query = select(self.model)
        for key, value in params.items():
            if isinstance(value, str):
                query = query.where(func.lower(getattr(self.model, key)) == func.lower(value))
            else:
                query = query.where(getattr(self.model, key) == value)
        result = await db.execute(query)
        return result.scalar_one_or_none()

    # 添加实体
    async def get_or_create(self, db: AsyncSession,
                            defaults: Optional[Dict[str, Any]], **kwargs: Any) -> ModelType:
        instance = await self.get_by_params(db, **kwargs)
        if instance:
            return instance, False
        params = defaults or {}
        params.update(kwargs)
        instance = self.model(**params)
        db.add(instance)
        await db.commit()
        await db.refresh(instance)
        return instance, True

    #  部分更新实体
    async def patch(self, db: AsyncSession,
                    *, obj_id: str,
                    obj_in: UpdateSchemaType | Dict[str, Any]
                    ) -> Optional[ModelType]:
        db_obj = await self.get(db=db, obj_id=obj_id)
        if not db_obj:
            return None
        update_data = obj_in if isinstance(obj_in, dict) else obj_in.model_dump(exclude_unset=True)
        query = (
            update(self.model)
            .where(self.model.id == obj_id)
            .values(**update_data)
        )
        await db.execute(query)
        return await self.get(db, obj_id)

    # 完全更新实体
    async def update(
        self,
        db: AsyncSession,
        *,
        obj_current: ModelType,
        obj_new: UpdateSchemaType | Dict[str, Any] | ModelType
    ):
        obj_data = jsonable_encoder(obj_current)

        if isinstance(obj_new, dict):
            update_data = obj_new
        else:
            update_data = obj_new.model_dump(exclude_unset=True)
        for field in obj_data:
            if field in update_data:
                setattr(obj_current, field, update_data[field])
        db.add(obj_current)
        await db.commit()
        await db.refresh(obj_current)
        return obj_current


    # 从数据库中完全删除实体
    async def remove(self, db: AsyncSession, *, obj_id: str) -> Optional[ModelType]:
        db_obj = await self.get(db, obj_id)
        if not db_obj:
            return None

        await db.delete(db_obj)
        await db.commit()

        return db_obj

我们定义了各种方法。get 方法从数据库中获取与对象 ID 匹配的单个记录。get_multi 方法从数据库获取分页文档。get_by_params 方法根据匹配的参数搜索匹配的记录。get_or_create方法首先检查实体是否存在,如果不存在,则在数据库中创建实体。patch 方法更新记录字段。update方法完全更新记录字段。remove 方法从数据库中删除一条记录。

定义了 CRUD 操作的基类后,我们现在将定义产品 CRUD 操作。Product CRUD 操作将从基类继承CRUDBase。打开 crud 文件夹中名为product.py 的文件。粘贴下面的代码片段。

from typing import Any, Coroutine, Dict, Optional
from fastapi_pagination import Page
from sqlalchemy.ext.asyncio import AsyncSession
from .base import CRUDBase
from app.schemas.product import ProductUpdate, ProductCreate
from app.models.product import Product

class CRUDProduct(CRUDBase[Product, ProductCreate, ProductUpdate]):

    async def get(self, db: AsyncSession, obj_id: str) -> Product:
        return await super().get(db, obj_id)

    async def get_or_create(self, db: AsyncSession, defaults: Dict[str, Any] | None, **kwargs: Any) ->  Product:
        return await super().get_or_create(db, defaults, **kwargs)

    async def get_multi(self, db: AsyncSession, *, skip: int = 0, limit: int = 20) -> Page[Product]:
        return await super().get_multi(db, skip=skip, limit=limit)

    async def update(self, db: AsyncSession, *, obj_current: Product, obj_new: ProductUpdate | Dict[str, Any] | Product):
        return await super().update(db, obj_current=obj_current, obj_new=obj_new)

    async def remove(self, db: AsyncSession, *, obj_id: str) -> Product | None:
        return await super().remove(db, obj_id=obj_id)

product = CRUDProduct(Product)

创建数据库会话

这里我们将定义一个异步数据库引擎来对数据库执行异步操作。然后我们将引擎绑定到 sessionmaker,它将与数据库异步交互。打开名为 db 的文件夹中包含的名为 session.py 的文件。粘贴下面的代码片段。

from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
from app.core.settings import settings

# 使用应用程序设置中的 ASYNC_DATABASE_URI 创建异步 SQLAlchemy 引擎。
engine = create_async_engine(
    settings.ASYNC_DATABASE_URI,
)

# 使用 sessionmaker 创建一个 AsyncSession 类,绑定到 SQLAlchemy 引擎。
# 该会话类将用于与数据库异步交互。
SessionLocal = sessionmaker(
    engine, expire_on_commit=False, class_=AsyncSession
)

create_async_engine- 这将使用应用程序设置中的 ASYNC_DATABASE_URI 创建异步 SQLAlchemy 引擎。
sessionmaker- 这将使用 sessionmaker 创建一个 AsyncSession 类,绑定到 SQLAlchemy 引擎。

创建快速 API 依赖项

在这里,我们将定义将在我们的应用程序中使用的所有依赖项。这可能包括数据库会话。打开 API 文件夹中包含的名为 deps.py 的文件并粘贴下面的代码片段。

from typing import AsyncGenerator
from sqlalchemy.ext.asyncio import AsyncSession
from app.db.session import SessionLocal


async def get_db() -> AsyncGenerator[AsyncSession, None]:
    async with SessionLocal() as db:
        yield db

该get_db函数是一个异步生成函数,用于生成数据库会话。

创建产品列表端点

这里我们将定义 POST、GET、PUT、PATCH 和 DELETE 方法。

  • POST 将创建一个新产品。

  • GET 将检索一个或多个产品。

  • PUT 将完全更新产品。

  • PATCH 将更新为产品指定的字段。

  • DELETE 将从数据库中删除产品。

转到代码编辑器并打开名为 products.py 的文件,该文件包含在名为 endpoints 的文件夹中。在文件内粘贴下面的代码片段。

# 导入必要的模块和组件
from typing import Annotated
from fastapi import APIRouter, status, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from fastapi_pagination import Page, paginate
from app.schemas.product import Product, ProductCreate, ProductPatch, ProductUpdate
from app.api.deps import get_db
from app import crud

# 创建 APIRouter 实例
router = APIRouter()

# 定义创建新产品的路线
@router.post("/", response_model=Product, status_code=status.HTTP_201_CREATED)
async def create_product(
    db: Annotated[AsyncSession, Depends(get_db)],
    product_in: ProductCreate
):
    # 使用“crud”模块中的 CRUD(创建、读取、更新、删除)操作
    # 创建新产品或返回现有产品(如果已存在)
    product, created = await crud.product.get_or_create(
        db=db, defaults=product_in.dict()
    )

    # 如果产品已存在,则引发带有 400 状态代码的 HTTPException
    if not created:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Product exists"
        )

    # 返回创建的或现有的产品
    return product

# 定义通过 ID 检索产品的路由
@router.get("/{productId}", response_model=Product, status_code=status.HTTP_200_OK)
async def get_product(
    db: Annotated[AsyncSession, Depends(get_db)],
    productId: str
):
    # 使用 CRUD 操作通过 ID 检索产品
    product = await crud.product.get(db=db, obj_id=productId)

    # 如果产品不存在,则引发带有 404 状态代码的 HTTPException
    if not product:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Product not found"
        )

    # 返回检索到的产品
    return product

# 定义一个路由来检索分页的产品列表
@router.get("/", response_model=Page[Product], status_code=status.HTTP_200_OK)
async def get_products(
    db: Annotated[AsyncSession, Depends(get_db)],
    skip: int = 0,
    limit: int = 20
):
    # 使用CRUD操作检索多个带分页的产品
    products = await crud.product.get_multi(db=db, skip=skip, limit=limit)

    # 如果未找到产品,则引发带有 404 状态代码的 HTTPException
    if not products:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Products not found"
        )

    # 返回分页的产品列表
    return paginate(products)

# 定义部分更新产品的路线
@router.patch("/{productId}", status_code=status.HTTP_200_OK)
async def patch_product(
    db: Annotated[AsyncSession, Depends(get_db)],
    product_Id: str,
    product_in: ProductPatch
):
    # 使用 CRUD 操作通过 ID 检索产品
    product = await crud.product.get(db=db, obj_id=product_Id)

    # 如果产品不存在,则引发带有 404 状态代码的 HTTPException
    if not product:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Product not found"
        )

    # 使用CRUD操作来修补(部分更新)产品
    product_patched = await crud.product.patch(db=db, obj_id=product_Id, obj_in=product_in.dict())

    # 返回已修补的产品
    return product_patched

# 定义完全更新产品的路线
@router.put("/{productId}", response_model=Product, status_code=status.HTTP_200_OK)
async def update_product(
    db: Annotated[AsyncSession, Depends(get_db)],
    productId: str,
    product_in: ProductUpdate
):
    # 使用 CRUD 操作通过 ID 检索产品
    product = await crud.product.get(db=db, obj_id=productId)

    # 如果产品不存在,则引发带有 404 状态代码的 HTTPException
    if not product:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Product not found"
        )

    # 使用CRUD操作来全面更新产品
    product_updated = await crud.product.update(
        db=db, obj_current=product, obj_new=product_in
    )

    # 返回更新后的产品
    return product_updated

# 定义删除产品的路线
@router.delete("/{productId}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_product(
    db: Annotated[AsyncSession, Depends(get_db)],
    productId: str
):
    # 使用 CRUD 操作通过 ID 检索产品
    product = await crud.product.get(db=db, obj_id=productId)

    # 如果产品不存在,则引发带有 404 状态代码的 HTTPException
    if not product:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Product not found"
        )

    # 使用CRUD操作移除(删除)产品
    await crud.product.remove(db=db, obj_id=productId)

    # 返回 204 No Content 响应表示删除成功
    return

端点由注释组成,解释每个端点中发生的情况。

现在我们需要将端点公开给 API 入口点,以便我们编辑两个文件。对于第一个文件,我们将打开名为 v1 的文件夹内的 api.py 文件。然后粘贴下面的代码片段。

# 从 FastAPI 导入 APIRouter 类
from fastapi import APIRouter

# 从“app.api.v1.endpoints”模块导入“products”路由器
from app.api.v1.endpoints import products

# 创建 APIRouter 的实例
router = APIRouter()

# 将“products”路由器作为子路由器包含在“/products”前缀下
# 并分配标签“Products”以对相关 API 端点进行分组
router.include_router(products.router, prefix="/products", tags=["Products"])

然后打开 main.py 文件并粘贴下面的代码片段。

# 从FastAPI框架导入FastAPI类
from fastapi import FastAPI

# 导入add_pagination
from fastapi_pagination import add_pagination

# 从“app.api.v1.api”模块导入“路由器”
from app.api.v1.api import router

#从“app.core.settings”模块导入“settings”对象
from app.core.settings import settings

# 创建 FastAPI 应用程序的实例
# - “title”设置为“settings”中的项目名称
# - 'openapi_url' 指定 OpenAPI 文档的 URL
app = FastAPI(
    title=settings.PROJECT_NAME,
    openapi_url=f"{settings.API_V1_STR}/openapi.json"
)

# 为所有使用 paginate 的路由添加必要的分页参数
add_pagination(app)

# 在 FastAPI 应用程序中包含“路由器”(其中包含您的 API 路由)
app.include_router(router)

到目前为止,我们可以尝试启动我们的服务器。为此,我们必须使用 docker 容器和 docker 镜像构建我们的应用程序。

运行产品列表 API

在这里我们将尝试运行我们的 API。

假设您本地计算机上安装了docker,请打开 vscode 终端。

要打开终端:

  • Windows 使用快捷键ctrl + `。

  • Mac OS 使用快捷键 ⌘ +`。

  • Linux 使用快捷键Ctrl+Shift+`。

在终端中写入以下命令:

docker-compose -f docker-compose.yml up -d
  • docker-compose- 此命令用于使用 Docker compose 管理 Docker 容器。

  • -f - 用于指定撰写文件的路径。

  • docker-compose.yml - 这是定义容器的 compose 文件的路径。在我们的例子中是 docker-compose.yml。

  • up - 用于初始化和启动 compose 文件中指定的服务。在我们的例子中,它启动products_db和api服务。

  • -d - 这指定容器应以分离模式启动,即容器作为后台服务启动。

成功执行命令后,您可以通过在 vscode 终端中执行以下命令来验证容器确实已启动并正在运行:

docker ps

您应该能够看到以下输出:

过在 vscode 终端中执行以下命令来验证容器确实已启动并正在运行

要通过 Swagger 查看 API 文档,您可以打开您的首选浏览器并粘贴以下 URL:

http://localhost:8000/docs

默认情况下,我们将通过端口 8000 访问 API,因为这是我们之前在 docker compose 文件中指定的映射到主机的端口。

在您的浏览器中,您将能够看到类似以下内容:

默认情况下,我们将通过端口 8000 访问 API,因为这是我们之前在 docker compose 文件中指定的映射到主机的端口。

我们现在已经成功设置了用于产品列表的 API。但是,如果我们尝试在 Swagger 中执行 POST 请求,我们将收到 500 内部服务器错误。

要查看导致错误的原因,我们将查看api容器日志。要查看日志,我们可以使用docker 桌面或使用我们的终端来查看日志。为此,我们将在 vscode 终端中执行以下命令:

docker logs <CONTAINER ID>

是当前正在运行的容器CONTAINER ID的 ID.api

为了获得CONTAINER ID我们将运行:

docker ps

成功运行docker logs命令后,我们会得到如下错误,如下图所示:

成功运行docker logs命令后,我们会得到如下错误

在最后一行,我们可以清楚地看到日志表明"database" does not exist. 之前,我们已经在**.env**我们的 POSTGRES_DATABASE=database 中定义了。而这个名为database的数据库不存在。这意味着我们实际上必须首先创建数据库本身。

为了创建数据库,我们将使用products_db容器。

在你的 vscode 终端中:

  • 运行下面的命令

docker exec -it <CONTAINER ID> /bin/bash

上面的命令在容器内启动 Bash 终端。

运行docker ps以获取products_dbID 并将其替换CONTAINER ID为您的映像实例的 ID products_db。

  • 我们需要创建数据库。为此,我们将在容器 Bash 终端中运行以下一系列命令:

psql -U username

上述命令启动 PostgreSQL 的基于终端的前端。它允许我们交互式地输入查询。

CREATE DATABASE database;

上面的命令在 PostgreSQL 中创建一个名为database的数据库。

ALTER ROLE username WITH PASSWORD 'password';

上述命令更改角色用户名并为其分配密码password。

GRANT ALL PRIVILEGES ON DATABASE database TO username;

上述命令将所有数据库权限授予名为 username 的用户。

完成此操作后,我们现在需要执行数据库迁移。

快速 API 数据库迁移

数据库迁移或模式迁移是为修改关系数据库中对象的结构而开发的受控更改集。

为了在我们的 API 中执行迁移,我们将在项目根目录中创建一个 alembic.ini 文件和一个 alembic 文件夹。在 alembic 文件夹内创建另一个名为 versions 的文件夹以及两个名为 env.py 和 script.py.mako 的文件。

现在项目目录结构如下所示:

.
└── FastAPI_APP/
    ├── app/
    │   ├── alembic.ini
    │   ├── alembic/
    │   │   ├── versions
    │   │   ├── env.py
    │   │   └── script.py.mako
    │   ├── api/
    │   │   ├── v1/
    │   │   │   ├── endpoints/
    │   │   │   │   ├── __init__.py
    │   │   │   │   └── products.py
    │   │   │   ├── __init__.py
    │   │   │   └── api.py
    │   │   ├── __init__.py
    │   │   └── deps.py
    │   ├── core/
    │   │   ├── __init__.py
    │   │   └── settings.py
    │   ├── crud/
    │   │   ├── __init__.py
    │   │   ├── base.py
    │   │   └── product.py
    │   ├── db/
    │   │   ├── __init__.py
    │   │   └── session.py
    │   ├── models/
    │   │   ├── __init__.py
    │   │   ├── basemodel.py
    │   │   └── products.py
    │   ├── schemas/
    │   │   ├── __init__.py
    │   │   └── product.py
    │   └── utils/
    │       ├── __init__.py
    │       └── idgen.py
    └── main.py

我们现在将编辑已添加的文件。

打开 alembic.ini 文件并粘贴以下脚本:

# 通用的单一数据库配置。

[alembic]
# 迁移脚本的路径
script_location = alembic

# 用于生成迁移文件名的模板; 默认值为 %%(rev)s_%%(slug)s
file_template = %%(year)d-%%(month).2d-%%(day).2d-%%(hour).2d-%%(minute).2d_%%(rev)s

# sys.path path, will be prepended to sys.path if present.
# defaults to the current working directory.
prepend_sys_path = .

version_path_separator = os  # Use os.pathsep. Default configuration used for new projects.


sqlalchemy.url =


[post_write_hooks]

# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic

[handlers]
keys = console

[formatters]
keys = generic

[logger_root]
level = WARN
handlers = console
qualname =

[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine

[logger_alembic]
level = INFO
handlers =
qualname = alembic

[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic

[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

alembci.ini 文件是与 Alembic 一起使用的配置文件,它提供用于管理数据库架构随时间变化的设置和选项。

打开 alembic 文件夹或模块中包含的 env.py 文件并粘贴以下代码片段:

# Import necessary modules
import asyncio
import sys
import pathlib
from alembic import context
from sqlalchemy.ext.asyncio import create_async_engine

# Import the necessary database models and settings
from app.models.product import Product
from app.core.settings import settings
from app.models.base import Base_
from sqlalchemy.orm import declarative_base

# Define the target metadata for migrations
target_metadata = Base_.metadata

# Append the parent directory of the current file to the sys.path
# This allows importing modules from the parent directory
sys.path.append(str(pathlib.Path(__file__).resolve().parents[1]))

# Define a function to run migrations
def do_run_migrations(connection):
    context.configure(
        compare_type=True,
        dialect_opts={"paramstyle": "named"},
        connection=connection,
        target_metadata=target_metadata,
        include_schemas=True,
        version_table_schema=target_metadata.schema,
    )

    with context.begin_transaction():
        context.run_migrations()

# Define an asynchronous function to run migrations online
async def run_migrations_online():
    """Run migrations in 'online' mode.

    In this scenario, we create an Engine
    and associate a connection with the context.

    """
    # Create an asynchronous database engine using the URI from settings
    connectable = create_async_engine(settings.ASYNC_DATABASE_URI, future=True)

    # Connect to the database and run migrations within a transaction
    async with connectable.connect() as connection:
        await connection.run_sync(do_run_migrations)

# Run the migrations online using asyncio
asyncio.run(run_migrations_online())

上面的脚本使用 Alembic 在 Fast API 中作为异步数据库引擎运行数据库迁移。

打开 alembic 模块中包含的名为 script.py.mako 的文件。粘贴下面的脚本:

"""
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""

# Import necessary modules from Alembic and SQLAlchemy
from alembic import op
import sqlalchemy as sa

# Import any additional necessary modules (if specified)
${imports if imports else ""}

# Define revision identifiers used by Alembic
revision = ${repr(up_revision)}        # The unique identifier for this revision
down_revision = ${repr(down_revision)}  # The revision to which this one applies (if any)
branch_labels = ${repr(branch_labels)}  # Labels associated with this revision (if any)
depends_on = ${repr(depends_on)}        # Dependencies for this revision (if any)

def upgrade():
    ${upgrades if upgrades else "pass"}
    """
    This function is called when upgrading the database schema.
    You can specify SQL operations to apply schema changes.
    If no operations are specified, 'pass' can be used.
    """

def downgrade():
    ${downgrades if downgrades else "pass"}
    """
    This function is called when downgrading the database schema.
    You can specify SQL operations to reverse schema changes.
    If no operations are specified, 'pass' can be used.
    """

定义了用于处理迁移的脚本后,我们现在可以在 api 容器中执行它们。为此,运行以下命令:

docker exec -it <CONTAINER ID> /bin/bash

上面的命令在容器内启动 Bash 终端。

将 替换为 api 容器的实际 ID。要获取 api 容器,请运行docker ps命令。

alembic revision --autogenerate -m "Migrate products table"

上述命令生成一个新的迁移脚本。新的迁移脚本包含当前数据库架构与代码中的模型定义之间的差异。

alembic upgrade head

上述命令适用于所有待处理的迁移。

测试我们的 API

由于我们已经执行了数据库模式的迁移,因此我们现在可以通过 swagger 文档自信地测试我们的 API。

要访问 Swagger 文档,请在浏览器中输入以下 URL:

http://localhost:8000/docs

我们可以通过执行 POST 请求开始。

POST 请求快速 API

在 Swagger 中,展开可折叠的 POST 请求,然后单击Try it out按钮。在“响应正文”部分中,将 JSON 架构的键值更改为您的首选项,如下所示。

POST 请求快速 API

对于图像键,您可以输入图像 URL。然后单击执行按钮。成功 POST 后,您将看到 201 创建的状态代码以及响应正文,如下所示:

对于图像键,您可以输入图像 URL。然后单击执行按钮。成功 POST 后,您将看到 201 创建的状态代码以及响应正文

根据您分配给 JSON 架构的值,响应正文可能与上面显示的有所不同。

GET 请求(分页数据)

在 GET 请求中,我们想要获取多个项目。为此,我们可以指定跳过和限制。
Skip 与 OFFSET 类似,是在检索任何行之前要跳过的结果表的行数。
Limit 是指定获取结果表的前 N 行的语法。

单击获取请求,对于跳过参数,我们可以使用默认值 0,对于限制,我们也可以使用默认值 20。

单击执行按钮,您将看到包含分页产品数据的响应正文。

GET 请求(分页数据)

奖励积分

作为额外的好处,您可以选择探索其余端点并在评论部分分享您对“响应正文”的想法。

您可以通过以下 URL 访问相关的 GitHub 存储库中的项目:

https://github.com/mbuthi/product_listing_API

将项目克隆到本地存储库,然后继续运行它。

结论

总之,本文引导您完成了使用 docker 对 Fast API 应用程序和 PostgreSQL 数据库进行容器化的过程。通过将 API 和数据库捆绑到单独的容器中,我们实现了可移植性和易于部署。

我们首先为 docker 环境设置 dockerfile 和 docker compose 文件,设置模型、模式、CRUD 操作和端点。

在整篇文章中,我们定义了如何使用 docker 中的卷来持久化数据,以及 docker 最佳实践,并强调了 DRY 编程设计。

我希望本文能让您深入了解如何使用 docker 将 Fast API 应用程序和 PostgreSQL 数据库容器化,从而使您的 Web 应用程序更上一层楼。当您继续接触容器化之旅时,请探索 Fast API 和 docker 中的更多高级主题。


文章来源地址https://www.toymoban.com/diary/apps/345.html

到此这篇关于使用 Fast API 和 PostgreSQL 进行 DevOps:如何使用 Docker 容器化 Fast API 应用程序的文章就介绍到这了,更多相关内容可以在右上角搜索或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

原文地址:https://www.toymoban.com/diary/apps/345.html

如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请联系站长进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用
上一篇 2023年10月04日 13:16
下一篇 2023年10月05日 18:04

相关文章

  • 【DevOps-08-3】Jenkins容器内部使用Docker

    构建镜像和发布镜像到harbor都需要使用到docker命令。而在Jenkins容器内部安装Docker官方推荐直接采用宿主机带的Docker即可。 设置Jenkins容器使用宿主机Docker。

    2024年01月16日
    浏览(48)
  • Docker部署FAST OS DOCKER容器管理工具

    FAST OS DOCKER是Docker的图形化管理工具,为用户提供了docker总览、本地容器管理、远程镜像拉取、服务器磁盘映射、服务器网络管理等功能,基本能满足中小型单位对容器管理的全部需求。 轻松管理Docker,可视化操作。 界面直观、简洁,上手简单易操作。 可以采用Dcoker安装方

    2024年02月03日
    浏览(51)
  • 如何对Docker容器进行健康检查

    熟悉使用过kubernetes的人应该知道,kubernetes支持对pod进行健康检查的功能,这对生产业务来说其实是非常有用处的,能快速发现服务不可用,并进行快速重启恢复。其实不使用kubernetes这种容器管理工具,docker自身也能实现对容器的健康检查。 从docker 1.12 版本之后,Docker 实现

    2024年02月09日
    浏览(45)
  • 当docker中容器运行时,如何将目录和宿主机进行挂载

    容器已运行,但还想挂载文件 容器已经运行起来了,突然想给容器的目录进行挂载,通常是在运行容器时加上 -v  命令 进行挂载。运行起来的容器想挂载文件夹可以通过修改容器在宿主机的配置文件进行解决。 在配置文件中加入新的挂载 1.查看容器存放目录 2.进入该目录

    2024年01月25日
    浏览(52)
  • 云原生架构的核心技术(微服务、DevOps、容器云、Service Mesh、Serverless、声明式API)

    天上飞的理念☁️☁️☁️☁️☁️,必然有落地的实现 文章介绍 读完本文,你将对云原生下的核心概念微服务、DevOps、容器云、Service Mesh、Serverless、Immutable Infrastructure、Declarative-API等有一个详细的了解,帮助你快速掌握云原生的核心和要点。 IaaS(Infrastructure-as-a-Service基

    2024年02月03日
    浏览(47)
  • docker中运行PostgreSQL容器

    我们如何在docker中运行postgresql容器,要进过如下几个步骤就可以了。 拉取postgresql容器 使用上述命令将从 Docker Hub 存储库中提取最新可用版本的 PostgreSQL。 从 PostgreSQL 服务器 Docker 镜像运行容器 在部署之前,您需要设置一个 Docker 卷或绑定安装来持久化您的数据库。否则,当

    2024年02月13日
    浏览(44)
  • 使用VSCode的 Dev Containers 插件搭配Docker 容器进行开发环境的搭建

    需要安装插件 https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers 安装Docker 这样做的好处 每一个项目可以运行一个容器,在容器内开发,相关之间node环境隔离,彻底解决本地包版本依赖关错乱问题 共用宿主机的git 配置, 如果用的是 alpine 版本的镜像,则没有

    2024年01月21日
    浏览(71)
  • 如何在PostgreSQL中使用pg_stat_statements插件进行SQL性能统计和分析?

    PostgreSQL中的 pg_stat_statements 是一个强大的插件,用于追踪执行时间最长的SQL语句。通过它,我们可以获取有关SQL语句执行频率、总执行时间、平均执行时间等信息,从而进行性能调优和问题分析。 首先,我们需要确保 pg_stat_statements 插件已经安装。在大多数PostgreSQL发行版中,

    2024年04月25日
    浏览(80)
  • 【Python编程工具】【ssh连接Docker容器】如何使用Docker容器里的python环境,如何调试在容器中的代码

    本篇博客将介绍如何在Docker容器中打开SSH连接服务,以及如何使用JetBrains Gateway软件进行代码调试。 JetBrains Gateway是一款紧凑型桌面应用,可让您通过 JetBrains IDE 远程工作,甚至无需下载这些IDE。通过在桌面端安装这款软件,您可以直接调试服务器上的代码。 使用以下命令启

    2024年01月25日
    浏览(67)
  • 如何使用docker容器中的redis

    1.检查docker容器中是否启动了redis;命令: docker ps 2.如果没启动,则先启动服务;命令: docker run -p 6379:6379 --name redis -d redis (这里name后面的redis就是你在docker里面的redis服务映射的名称,可以叫任何名称,端口号也是) 3.根据查询出来的名称,比如你的redis服务就叫redis;命令

    2024年02月12日
    浏览(43)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包