使用python3.12+fastapi实现一个最小API框架

项目介绍

使用python3.12+fastapi实现一个api框架

项目目录结构

├── app
│ ├── api
├── core
│ └── exception.py
│ └── middleware.py
│ └── register.py
├── logs
├── utils
│ └── common.py
│ └── response.py
├── .env
├── config.py
├── main.py
└── requirements.txt

编写 main.py
import logging
from fastapi import FastAPI

from core.register import (
    register_exception,
    register_middleware,
    register_router,
)

# 关闭 Uvicorn HTTP 请求日志记录
uvicorn_access_logger = logging.getLogger("uvicorn.access")
uvicorn_access_logger.handlers = []
uvicorn_access_logger.propagate = False

# uvicorn 主日志处理器
uvicorn_logger = logging.getLogger("uvicorn")


def create_app() -> FastAPI:
    # 创建fastapi对象
    _app = FastAPI()

    # 全局异常捕捉处理
    register_exception(_app)

    # 注册中间件
    register_middleware(_app)

    # 引入应用中的路由
    register_router(_app)

    # 控制台日志
    uvicorn_logger.info(f"服务已启动,共加载了 {len(_app.routes)} 条路由!")

    return _app


app = create_app()
app目录
api.py(api示例)
from fastapi import APIRouter
from config import settings
from utils.response import RestfulResponse

router = APIRouter(prefix="/api", tags=["接口服务"])

@router.post("/test", summary="test")
async def test():
    return RestfulResponse.success()
core目录
exception.py
from fastapi.responses import JSONResponse
from starlette.exceptions import HTTPException
from fastapi.exceptions import RequestValidationError
from fastapi import status as fastapi_status
from fastapi import Request, FastAPI
from fastapi.encoders import jsonable_encoder
from typing import Optional, Dict, Any, Union
from loguru import logger

from utils import common


class CustomException(Exception):
    """
    自定义异常类,用于统一处理业务异常
    """

    def __init__(
            self,
            message: str,
            *,
            errcode: int = 400,
            code: int = 400,
            error: int = 400,
            status_code: int = fastapi_status.HTTP_200_OK,
            desc: Optional[str] = None,
    ):
        """
        自定义异常初始化
        :param message: 错误提示信息
        :param errcode: 错误码
        :param code: 兼容旧版错误码
        :param error: 兼容旧版错误码
        :param status_code: HTTP状态码
        :param desc: 详细描述信息(仅日志记录,不返回给前端)
        """
        self.message = message
        self.status_code = status_code
        
        # 如果状态码不是200,则使用状态码作为错误码
        if self.status_code != fastapi_status.HTTP_200_OK:
            self.errcode = self.status_code
            self.code = self.status_code
            self.error = self.status_code
        else:
            self.errcode = errcode
            self.code = code
            self.error = error
        
        self.desc = desc
        super().__init__(self.message)


def create_error_response(status_code: int, message: str, errcode: int) -> JSONResponse:
    """
    创建统一的错误响应
    """
    return JSONResponse(
        status_code=status_code,
        content={
            "message": message,
            "errcode": errcode,
            "code": errcode,
            "error": errcode
        }
    )


def refactoring_exception(app: FastAPI):
    """
    注册全局异常处理器
    """

    @app.exception_handler(CustomException)
    async def custom_exception_handler(request: Request, exc: CustomException):
        """
        处理自定义业务异常
        """
        if exc.desc:
            logger.warning(f"业务异常: {exc.message}, 详情: {exc.desc}")
            
        return JSONResponse(
            status_code=exc.status_code,
            content={"message": exc.message, "errcode": exc.errcode, "code": exc.code, "error": exc.error},
        )

    @app.exception_handler(HTTPException)
    async def http_exception_handler(request: Request, exc: HTTPException):
        """
        处理HTTP异常
        """
        # logger.warning(f"HTTP异常: {exc.detail}, 状态码: {exc.status_code}")
        
        return create_error_response(
            status_code=exc.status_code,
            message=exc.detail,
            errcode=exc.status_code
        )

    @app.exception_handler(RequestValidationError)
    async def validation_exception_handler(request: Request, exc: RequestValidationError):
        """
        处理请求参数验证异常
        """
        # 获取错误信息
        errors = exc.errors()
        if not errors:
            error_msg = "请求参数验证失败"
            missing_field = "未知字段"
        else:
            first_error = errors[0]
            loc = first_error.get("loc", [])
            missing_field = loc[-1] if loc else "未知字段"
            error_msg = first_error.get("msg", "参数验证错误")
        
        # 错误类型映射关系
        error_types = {
            "Field required": f"请求错误,缺少必填参数:{missing_field}",
            "Input should be a valid string": f"参数:{missing_field},输入类型错误,应该为字符串!",
            "Input should be a valid integer": f"参数:{missing_field},输入类型错误,应该为整数!",
            "JSON decode error": "body参数不是一个标准的json格式"
        }
        
        for error_type, message in error_types.items():
            if common.is_string_contained(error_msg, error_type):
                error_msg = message
                break

        return JSONResponse(
            status_code=fastapi_status.HTTP_200_OK,
            content=jsonable_encoder({
                "errcode": 400,
                "code": 400,
                "error": 400,
                "message": error_msg
            }),
        )

    @app.exception_handler(ValueError)
    async def value_exception_handler(request: Request, exc: ValueError):
        """
        处理值异常
        """
        error_message = str(exc)
        logger.error(f"值异常: {error_message}")
        
        return create_error_response(
            status_code=fastapi_status.HTTP_200_OK,
            message=error_message,
            errcode=400
        )

    @app.exception_handler(Exception)
    async def all_exception_handler(request: Request, exc: Exception):
        """
        处理所有未捕获的异常
        """
        error_message = str(exc)
        logger.exception(f"未捕获异常: {error_message}")
        
        return create_error_response(
            status_code=fastapi_status.HTTP_500_INTERNAL_SERVER_ERROR,
            message="服务器错误!",
            errcode=500
        )
middleware.py
import sys
import time
import json
from pathlib import Path
from loguru import logger
from fastapi import FastAPI, Request
from starlette.concurrency import iterate_in_threadpool

from config import settings


# 记录请求日志中间件
def register_request_log_middleware(app: FastAPI):
    # 禁用全局 logger 的默认处理器
    logger.remove()

    # 添加控制台输出
    if settings.system.LOG_CONSOLE_OUT:
        logger.add(sys.stdout, level="DEBUG")

    # 配置日志输出到文件
    log_path = Path(settings.BASE_PATH) / "logs" / "api_info_{time:YYYY_MM_DD}.log"
    logger.add(
        log_path,
        format="{time:YYYY-MM-DD HH:mm:ss.SSS} | {level} | {message}",
        level="INFO",
        enqueue=True,
        rotation="00:00",
        retention="7 days",
        mode="a",
        backtrace=True,
        diagnose=True
    )

    @app.middleware("http")
    async def request_log_middleware(request: Request, call_next):
        start_time = time.time()

        # 获取请求数据,优化判断逻辑
        content_type = request.headers.get("content-type", "")
        
        # 获取URL查询参数
        query_params = dict(request.query_params)
        
        # 获取请求体数据
        if "multipart/form-data" in content_type:
            request_body_data = {}
        else:
            try:
                request_body = await request.body()
                request_body_data = json.loads(request_body.decode("utf-8")) if request_body else None
            except Exception:
                request_body_data = None
        
        # 合并请求数据
        request_data = {
            "query_params": query_params,
            "body": request_body_data
        }

        # 调用下一个中间件或路由
        try:
            response = await call_next(request)
            
            # 计算处理时间
            process_time = time.time() - start_time

            # 获取响应数据
            response_body = b""
            async for chunk in response.body_iterator:
                response_body += chunk

            # 解析返回值为纯json字符串
            try:
                response_text = json.loads(response_body.decode("utf-8"))
            except (json.JSONDecodeError, UnicodeDecodeError):
                response_text = str(response_body)

            # 记录日志
            log_data = {
                "method": request.method,
                "path": request.url.path,
                "status_code": response.status_code,
                "request_data": request_data,
                "response_body": response_text,
                "time": f"{process_time:.2f}s"
            }
            logger.info(json.dumps(log_data))

            # 重新设置响应体,确保其能正确返回
            response.body_iterator = iterate_in_threadpool([response_body])
            
            return response
        except Exception as e:
            process_time = time.time() - start_time
            logger.error(f"请求处理异常: {str(e)}, 路径: {request.url.path}, 完整URL: {request.url}, 耗时: {process_time:.2f}s")
            raise
register.py
import os
import sys
import importlib
from typing import List, Optional
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from loguru import logger

from config import settings
from utils import common
from core.exception import refactoring_exception


def register_middleware(app: FastAPI) -> None:
    """
    注册中间件
    
    Args:
        app: FastAPI应用实例
    """
    # 中间件列表
    middlewares: List[Optional[str]] = [
        "core.middleware.register_request_log_middleware" if settings.system.REQUEST_LOG_RECORD else None,
    ]

    # 过滤掉None值并导入中间件
    valid_middlewares = [m for m in middlewares if m]
    common.import_modules(valid_middlewares, "中间件", app=app)

    # 注册CORS中间件
    if settings.system.CORS_ORIGIN_ENABLE:
        app.add_middleware(
            CORSMiddleware,
            allow_origins=settings.system.ALLOW_ORIGINS,
            allow_credentials=settings.system.ALLOW_CREDENTIALS,
            allow_methods=settings.system.ALLOW_METHODS,
            allow_headers=settings.system.ALLOW_HEADERS,
        )


def register_router(app: FastAPI) -> None:
    """
    动态加载 api 目录下各个文件,注册路由
    
    Args:
        app: FastAPI应用实例
    """
    sys.path.append(settings.router.ROUTERS_PATH)

    # 路由主入口文件名(不含扩展名)
    api_file = settings.router.ROUTERS_FILE.rsplit('.', 1)[0]

    try:
        # 导入 api.py 模块
        module_views = importlib.import_module(api_file)

        # 获取 router 属性
        router = getattr(module_views, "router", None)
        if router:
            app.include_router(router)
        else:
            logger.warning(f"{api_file}.py 中没有 router 属性")
    except ImportError as e:
        logger.error(f"模块 {api_file}.py 导入失败: {str(e)}")
    except Exception as e:
        logger.error(f"路由加载失败: {str(e)}")


def register_exception(app: FastAPI) -> None:
    """
    注册全局异常处理

    Args:
        app: FastAPI应用实例
    """
    refactoring_exception(app)

utils
common.py
import importlib
from loguru import logger

def is_string_contained(full_string: str, substring: str) -> bool:
    """
    判断一个字符串是否是另一个字符串的子串
    :param full_string: 完整的字符串
    :param substring: 需要查找的子串
    :return:
    """
    return substring in full_string


def import_modules(modules: list, desc: str, **kwargs):
    """
    通过反射执行方法
    :param modules:
    :param desc:
    :param kwargs:
    :return:
    """
    for module in modules:
        if not module:
            continue
        try:
            module_pag = importlib.import_module(module[0: module.rindex(".")])
            getattr(module_pag, module[module.rindex(".") + 1:])(**kwargs)
        except ModuleNotFoundError as e:
            logger.error(f"AttributeError:导入{desc}失败,模块:{module},详细报错信息:{e}")
        except AttributeError as e:
            logger.error(f"ModuleNotFoundError:导入{desc}失败,模块方法:{module},详细报错信息:{e}")
response.py
from pydantic import BaseModel, Field
from typing import Generic, TypeVar
from fastapi import status as fastapi_status
from fastapi.responses import ORJSONResponse

DataT = TypeVar("DataT")


class Status:
    HTTP_SUCCESS = 200  # OK 请求成功
    HTTP_ERROR = 400  # BAD_REQUEST 因客户端错误的原因请求失败
    HTTP_401 = 401  # UNAUTHORIZED: 未授权
    HTTP_403 = 403  # FORBIDDEN: 禁止访问
    HTTP_404 = 404  # NOT_FOUND: 未找到
    HTTP_405 = 405  # METHOD_NOT_ALLOWED: 方法不允许
    HTTP_408 = 408  # REQUEST_TIMEOUT: 请求超时
    HTTP_500 = 500  # INTERNAL_SERVER_ERROR: 服务器内部错误
    HTTP_502 = 502  # BAD_GATEWAY: 错误的网关
    HTTP_503 = 503  # SERVICE_UNAVAILABLE: 服务不可用


class ResponseSchema(BaseModel, Generic[DataT]):
    """
    默认响应模型
    """

    errcode: int = Field(Status.HTTP_SUCCESS, description="响应状态码(响应体内)")
    message: str = Field("success", description="响应结果描述")
    data: DataT = Field(None, description="响应结果数据")


class PageResponseSchema(ResponseSchema):
    """
    带有分页的响应模型
    """

    total: int = Field(0, description="总数据量")
    page: int = Field(1, description="当前页数")
    limit: int = Field(10, description="每页多少条数据")


class ErrorResponseSchema(BaseModel, Generic[DataT]):
    """
    默认请求失败响应模型
    """

    errcode: int = Field(Status.HTTP_ERROR, description="响应状态码(响应体内)")
    message: str = Field("请求失败,请联系管理员", description="响应结果描述")
    data: DataT = Field(None, description="响应结果数据")


ResponseSchemaT = TypeVar("ResponseSchemaT", bound=ResponseSchema)


class RestfulResponse:
    """
    响应体
    """

    @staticmethod
    def success(
            message: str = "success",
            *,
            errcode: int = 0,
            data: DataT = None,
            status_code: int = fastapi_status.HTTP_200_OK,
            **kwargs,
    ) -> ORJSONResponse:
        """
        成功响应

        :param message: 响应结果描述
        :param errcode: 业务响应体状态码 0:成功
        :param data: 响应结果数据
        :param status_code: HTTP 响应状态码
        :param kwargs: 额外参数
        :return:
        """
        content = ResponseSchema(errcode=errcode, message=message, data=data)
        content = content.model_dump() | kwargs
        return ORJSONResponse(content=content, status_code=status_code)

    @staticmethod
    def error(
            message: str,
            *,
            errcode: int = Status.HTTP_ERROR,
            data: DataT = None,
            status_code: int = fastapi_status.HTTP_200_OK,
            **kwargs,
    ) -> ORJSONResponse:
        """
        失败响应

        :param message: 响应结果描述
        :param errcode: 业务响应体状态码
        :param data: 响应结果数据
        :param status_code: HTTP 响应状态码
        :return:
        """
        content = ErrorResponseSchema(errcode=errcode, message=message, data=data)
        content = content.model_dump() | kwargs
        return ORJSONResponse(content=content, status_code=status_code)
.env
# -----------------------------------------------
# system 配置项
#   LOG_CONSOLE_OUT: 是否将日志打印在控制台
#   REQUEST_LOG_RECORD: 是否将日志写入文件
# -----------------------------------------------
LOG_CONSOLE_OUT=False
REQUEST_LOG_RECORD=True
config.py
from pathlib import Path
from pydantic_settings import BaseSettings, PydanticBaseSettingsSource, SettingsConfigDict

# 项目根目录
_BASE_PATH: Path = Path(__file__).resolve().parent


class Settings(BaseSettings):
    model_config = SettingsConfigDict(
        env_file=str(_BASE_PATH / ".env"), env_file_encoding="utf-8", extra="ignore"
    )

    @classmethod
    def settings_customise_sources(
            cls,
            settings_cls: type[BaseSettings],
            init_settings: PydanticBaseSettingsSource,
            env_settings: PydanticBaseSettingsSource,
            dotenv_settings: PydanticBaseSettingsSource,
            file_secret_settings: PydanticBaseSettingsSource,
    ) -> tuple[PydanticBaseSettingsSource, ...]:
        return init_settings, env_settings, dotenv_settings


class SystemSettings(Settings):
    # 是否启用跨域
    CORS_ORIGIN_ENABLE: bool = True
    # 只允许访问的域名列表,* 代表所有
    ALLOW_ORIGINS: list[str] = ["*"]
    # 是否支持携带 cookie
    ALLOW_CREDENTIALS: bool = True
    # 设置允许跨域的http方法,比如 get、post、put等。
    ALLOW_METHODS: list[str] = ["*"]
    # 允许携带的headers,可以用来鉴别来源等作用。
    ALLOW_HEADERS: list[str] = ["*"]
    # 是否将日志打印到控制台
    LOG_CONSOLE_OUT: bool = True
    # 是否将日志写入文件
    REQUEST_LOG_RECORD: bool = False

    # 飞书机器人配置
    BOT_WEBHOOK: str
    BOT_SECRET: str
    BOT_TEMPLATE_ID: str


class RouterSettings(Settings):
    """
    路由配置
    主要为系统默认配置,一般情况下不涉及改变
    """

    # 应用路由文件目录
    ROUTERS_PATH: str = str(_BASE_PATH / "app")

    # 路由的主入口文件
    ROUTERS_FILE: str = "api.py"


class GlobalSettings(BaseSettings):
    """
    全局统一配置入口
    """

    BASE_PATH: Path = _BASE_PATH

    # 系统基础配置
    system: SystemSettings = SystemSettings()

    # 系统路由
    router: RouterSettings = RouterSettings()

settings = GlobalSettings()
requirements.txt
fastapi==0.115.6
gunicorn==23.0.0
uvicorn[standard]==0.34.0
pydantic==2.10.5
pydantic_core==2.27.2
pydantic-settings==2.7.1
python-dotenv==1.0.1
python-multipart==0.0.20
loguru==0.7.3
httpx==0.28.1
pycryptodome==3.21.0
numpy==2.3.3
requests==2.32.5
aiohttp==3.13.2
orjson==3.11.4
启动服务(windows)
uvicorn main:app --host 0.0.0.0 --port 10088
是不是我的打包脚本哪里配置不对,需要优化么?不想用户操作太复杂:#!/usr/bin/env python # -*- coding: utf-8 -*- # 版权信息:华为技术有限公司,版本所有(C) 2025-2099 """ 关键参数说明: MANIFEST.in: 文件用于指定需要包含到包中的文件和目录。它告诉 setuptools 在打包时需要包含哪些文件。 package_dir: 配置指定了每个包的根目录路径。它告诉 setuptools 在项目中查找包代码的位置 packages: 列表指定了需要打包的包名称。这些包名称决定了安装包中的顶级目录结构 版本号说明: # 测试版本(Alpha) version = "1.0.0a1" # Beta 版本 version = "1.0.0b1" # 正式版本 version = "1.0.0" """ from setuptools import setup # 所有的目录和子目录,都必须手动添加 # 所有的目录下面,都需要__init__脚本 packages = [ 'isc_agent_kit', 'isc_agent_kit.auth', # 来源:infrastructure 'isc_agent_kit.code_agent', 'isc_agent_kit.config_utils', # 来源:infrastructure 'isc_agent_kit.io_utils', # 来源:infrastructure 'isc_agent_kit.log', 'isc_agent_kit.memory', 'isc_agent_kit.model_client', 'isc_agent_kit.model_client._autogen', 'isc_agent_kit.model_client._common', 'isc_agent_kit.model_client._langgraph', 'isc_agent_kit.postprocess_tools', 'isc_agent_kit.push_welink', 'isc_agent_kit.query_rewrite', 'isc_agent_kit.resource', 'isc_agent_kit.s3', 'isc_agent_kit.self_awareness', 'isc_agent_kit.vo', ] setup( name="isc_agent_kit", # 安装显示的名称 version="0.0.1", author="Agent框架开发团队", description="小智智能体框架,快速构建autogen智能体,搭建langgraph工作流,省事省心", python_requires='>=3.11', long_description_content_type="text/markdown", url="https://codehub-g.huawei.com/SIOC/SIOC-Foundation/iscp-algorithm-foundation-ai/home", include_package_data=True, # 包含所有包数据 packages=packages, package_dir={'isc_agent_kit': 'src/isc_agent_kit'}, package_data={ '': ['*.py', '*.pyd', '*.so', '*.json', '*.yaml', '*.ini'], # 包含所有这些类型的文件 }, install_requires=[ # 依赖项 "pydantic == 2.11.3", # Pydantic 是一个用于数据验证和设置管理的 Python 库 "httpx == 0.28.1", # HTTPX 是一个用于发送 HTTP 请求的 Python 库 "aiohttp == 3.12.15", # AIOHTTP 是一个用于构建异步 HTTP 客户端和服务器的 Python 库 "redis == 5.2.1", # Redis 是一个用于存储键值对的内存数据库 "fastapi == 0.115.7", # FastAPI一个用于构建 APIPython 框架 "requests == 2.32.4", # Requests 是一个用于发送 HTTP 请求的 Python 库 "pandas == 2.2.3", # Pandas 是一个用于数据处理和分析的 Python 库 "uvicorn == 0.24.0", # Uvicorn 是一个用于运行 FastAPI 应用的 ASGI 服务器 "langgraph == 0.6.5", # Langgraph 是一个用于处理语言图的库 "cryptography == 46.0.3", # Cryptography 是一个用于加密操作的 Python 库 "psutil == 7.1.3", # Psutil 是一个用于获取系统进程和系统利用率信息的 Python 库 "langfuse == 2.57.13a0", # Langfuse 是一个用于处理语言融合的库 "langchain == 0.3.27", # Langchain 是一个用于构建语言模型链的库 "langchain_core == 0.3.72", # Langchain Core 是 Langchain 的核心库 "langchain-openai == 0.3.28", # Langchain OpenAI 是 Langchain 的 OpenAI 集成库 "langgraph-checkpoint-postgres == 2.0.25", # Langgraph Checkpoint Postgres 是一个用于 Postgres 检查点的库 "dataclasses_json == 0.6.7", # Dataclasses JSON 是一个用于将数据类转换为 JSON 的库 "autogen-agentchat == 0.7.5", # Autogen Agentchat 是一个用于自动生成智能体对话的库 "autogen-ext[openai] == 0.7.5", # Autogen Ext OpenAI 是一个用于 OpenAI 集成的扩展库 "smolagents == 1.22.0", # 轻量级的多智能体系统框架 ], )
最新发布
11-25
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

吹落的树叶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值