Skip to main content

AI4Love 通用工具库

面向多项目复用的 Python 通用工具库,当前提供租户隔离模块,后续持续扩展更多模块。

模块概览

  • tenant:租户隔离与上下文管理

租户模块功能特性

  • 租户ID和用户ID提取:从 HTTP 请求头中自动提取租户ID和用户ID
  • 上下文存储:使用 contextvars 实现线程安全的上下文变量存储
  • 装饰器支持:提供 @extract_tenant_id 和 @with_tenant_context 装饰器
  • SQL 自动封装:自动为 SQL 语句附加租户条件
  • SQLAlchemy 集成:在不改动业务代码的前提下,自动为 ORM 查询追加租户过滤

安装

pip install -e .

快速开始

1. 使用装饰器自动提取租户ID(推荐)

from fastapi import FastAPI, Request
from ai4love_tools.tenant.decorators import extract_tenant_id
from ai4love_tools.tenant.context import get_tenant_id, get_user_id

app = FastAPI()

@app.get("/users")
@extract_tenant_id  # 自动从请求头提取租户ID和用户ID
async def get_users(request: Request):
    tenant_id = get_tenant_id()  # 从上下文获取租户ID
    user_id = get_user_id()  # 从上下文获取用户ID
    return {"tenant_id": tenant_id, "user_id": user_id}

2. 手动提取和设置

from ai4love_tools.tenant.extractor import extract_tenant_id_from_request, extract_user_id_from_request
from ai4love_tools.tenant.context import set_tenant_id, set_user_id

def manual_extract(request):
    tenant_id = extract_tenant_id_from_request(request)
    user_id = extract_user_id_from_request(request)
    set_tenant_id(tenant_id)
    set_user_id(user_id)

3. 多线程/后台任务上下文传递

from ai4love_tools.tenant.decorators import with_tenant_context
from ai4love_tools.tenant.context import get_tenant_id, get_user_id
from concurrent.futures import ThreadPoolExecutor

@with_tenant_context
def background_task(data):
    # 在新线程中,租户上下文已自动传递
    tenant_id = get_tenant_id()
    user_id = get_user_id()
    process_data(data)

executor = ThreadPoolExecutor()
executor.submit(background_task, data)

4. SQL 封装使用

from ai4love_tools.tenant.sql import tenant_execute
from ai4love_tools.tenant.context import set_tenant_id

# 设置租户ID到上下文
set_tenant_id("tenant_123")

def list_users(conn):
    # 业务侧只关心业务条件
    base_sql = "SELECT id, name FROM users WHERE status = :status"
    params = {"status": "active"}

    # 底层自动从上下文获取租户ID,并追加 tenant 条件
    def executor(sql: str, params: dict):
        with conn.cursor() as cursor:
            cursor.execute(sql, params)
            return cursor.fetchall()

    rows = tenant_execute(executor, base_sql, params)
    return rows

5. SQLAlchemy ORM 自动租户过滤

同步 SQLAlchemy

from sqlalchemy import create_engine
from sqlalchemy.orm import Session
from ai4love_tools.tenant.sqlalchemy_integration import enable_sqlalchemy_tenant_isolation
from your_models import User

# 在应用启动时启用
engine = create_engine("sqlite:///db.sqlite")
enable_sqlalchemy_tenant_isolation(engine)

# 业务代码无需改动,自动追加租户过滤
with Session(engine) as session:
    # 自动追加 tenant_id 条件
    users = session.query(User).filter(User.status == "active").all()

异步 SQLAlchemy(推荐)

from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.orm import Session  # 必须导入 Session
from sqlalchemy import select
from ai4love_tools.tenant.sqlalchemy_integration import enable_sqlalchemy_tenant_isolation
from ai4love_tools.tenant.decorators import extract_tenant_id
from ai4love_tools.tenant.context import get_tenant_id
from your_models import User

# 1. 创建异步引擎
async_engine = create_async_engine("mysql+aiomysql://user:pass@localhost/db")

# 2. 启用租户隔离(在应用启动时调用一次)
enable_sqlalchemy_tenant_isolation(async_engine, require_tenant=True)

# 3. 创建 async_sessionmaker(重要:必须指定 sync_session_class=Session)
async_session_factory = async_sessionmaker(
    async_engine,
    expire_on_commit=False,
    class_=AsyncSession,
    sync_session_class=Session  # 必须指定这个参数,才能让 do_orm_execute 事件生效
)

# 4. 在 FastAPI 路由中使用
from fastapi import FastAPI, Depends

app = FastAPI()

async def get_db():
    """获取数据库会话的依赖项"""
    async with async_session_factory() as session:
        yield session
        await session.commit()

@app.get("/users")
@extract_tenant_id(required_tenant_id=True)  # 从请求头提取租户ID
async def get_users(db: AsyncSession = Depends(get_db)):
    # 租户ID已自动从请求头提取并设置到上下文
    # 查询时会自动添加 tenant_id 条件
    stmt = select(User).where(User.status == "active")
    result = await db.execute(stmt)
    users = result.scalars().all()
    return users

完整示例:MySQLManager 集成

from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.orm import Session  # 必须导入
from sqlalchemy import event
import time
import logging
from ai4love_tools.tenant.sqlalchemy_integration import enable_sqlalchemy_tenant_isolation

logger = logging.getLogger(__name__)

class MySQLManager:
    def __init__(self, url: str):
        # 1. 创建异步引擎
        self.engine = create_async_engine(
            url,
            echo=False,
            pool_size=25,
            max_overflow=500,
            pool_timeout=15,
            pool_recycle=3600,
            pool_pre_ping=True,
            connect_args={
                "charset": "utf8mb4",
                "ssl": False,
                "autocommit": False,
                "connect_timeout": 10
            },
            future=True
        )

        # 2. 设置 SQL 日志(使用 sync_engine)
        self._setup_sql_logging()

        # 3. 启用租户隔离(在引擎创建后立即调用)
        enable_sqlalchemy_tenant_isolation(
            engine_or_session_factory=self.engine,
            require_tenant=True  # 强制要求租户ID
        )

        # 4. 创建 async_sessionmaker(重要:必须指定 sync_session_class=Session)
        self.async_session_factory = async_sessionmaker(
            self.engine,
            expire_on_commit=False,
            class_=AsyncSession,
            sync_session_class=Session  # 必须指定,才能让 do_orm_execute 事件生效
        )

    def _setup_sql_logging(self):
        """设置SQL执行日志监听器"""
        @event.listens_for(self.engine.sync_engine, "before_cursor_execute")
        def before_cursor_execute(conn, cursor, statement, parameters, context, executemany):
            conn.info.setdefault('query_start_time', []).append(time.time())

        @event.listens_for(self.engine.sync_engine, "after_cursor_execute")
        def after_cursor_execute(conn, cursor, statement, parameters, context, executemany):
            if conn.info.get('query_start_time'):
                total = time.time() - conn.info['query_start_time'].pop(-1)
                sql = ' '.join(str(statement).strip().split())
                rows = cursor.rowcount if cursor.rowcount >= 0 else 0
                elapsed_ms = int(total * 1000)
                logger.info(f"SQL: {sql} (耗时: {elapsed_ms}ms) | rows={rows}")

    def get_session(self) -> AsyncSession:
        """获取数据库会话"""
        return self.async_session_factory()

API 文档

上下文管理

  • get_tenant_id(): 获取当前上下文中的租户ID
  • get_user_id(): 获取当前上下文中的用户ID
  • set_tenant_id(tenant_id): 设置租户ID到上下文
  • set_user_id(user_id): 设置用户ID到上下文

装饰器

  • @extract_tenant_id: 自动从请求头提取租户ID和用户ID并保存到上下文
  • @with_tenant_context: 自动复制租户上下文到新线程/任务

SQL 封装

  • tenant_execute(executor, sql, params, ...): 执行包含租户条件的 SQL 语句
  • build_tenant_sql(sql, ...): 构建包含租户条件的 SQL 语句
  • build_tenant_params(params, tenant_id, ...): 构建包含租户ID的参数

SQLAlchemy 集成

  • enable_sqlalchemy_tenant_isolation(engine_or_session_factory, ...): 启用 SQLAlchemy ORM 自动租户过滤

依赖要求

  • Python >= 3.12
  • fastapi >= 0.115.0
  • sqlalchemy >= 1.4.0

注意事项

  • contextvars 无法跨进程传递,进程池场景需要显式传递租户ID作为参数
  • 装饰器是给其他项目使用的,library 只负责提供装饰器函数,不负责具体的任务调度逻辑
  • library 本身不包含业务逻辑,只提供工具函数和装饰器
  • 尽量保持框架无关性,支持多种 Web 框架

运行测试

# 安装开发依赖
pip install -e ".[dev]"

# 运行所有测试
pytest

# 运行特定测试文件
pytest tests/test_context.py

# 运行测试并显示覆盖率
pytest --cov=src/ai4love_tools --cov-report=html

测试覆盖

测试覆盖以下模块:

  • test_context.py - 上下文存储管理测试
  • test_extractor.py - 租户ID和用户ID提取器测试
  • test_decorators.py - 装饰器功能测试
  • test_sql.py - SQL 封装函数测试
  • test_sqlalchemy_integration.py - SQLAlchemy 集成测试
  • test_integration.py - 集成测试(多模块协同工作)

许可证

MIT

Metadata

Release files for ai4love-tools 0.0.9

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for ai4love-tools 0.0.9
File Size Uploaded Provenance
ai4love_tools-0.0.9.tar.gz 19.0 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for ai4love-tools 0.0.9
File Interpreter ABI Platform
ai4love_tools-0.0.9-py3-none-any.whl Python 3 none any Details

Total release size: 35.2 kB

Release files / ai4love_tools-0.0.9.tar.gz

Download URL ai4love_tools-0.0.9.tar.gz
Size 19.0 kB
Tags Source
SHA-256 checksum
How to use checksums
677778b7ce099067639af517544ea84e91a8a4f06edd9ede082dcabc9fb62a26
BLAKE2b-256 checksum
How to use checksums
b9ee9f9eba82635211b0edfb7bcc8042024efe38702e8f4afa3e1505d4344a4d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.14.0

Release files / ai4love_tools-0.0.9-py3-none-any.whl

Download URL ai4love_tools-0.0.9-py3-none-any.whl
Size 16.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
8069360565b15c35c8fc031af30357b838f27a45516ad985c5303413630e5f0f
BLAKE2b-256 checksum
How to use checksums
298e370fe260a433c0d99b16b83af021a74358b0d41ee926e470fce7afd2ecbe
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.14.0

Security for ai4love-tools 0.0.9

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page