tanbushi commited on
Commit
b7791c2
·
1 Parent(s): df1a318

Sun Jun 8 15:02:12 CST 2025

Browse files
.gitignore CHANGED
@@ -1,3 +1,4 @@
1
  **/__pycache__/
2
  README.md
3
- .env
 
 
1
  **/__pycache__/
2
  README.md
3
+ .env
4
+ api_proxy.db
Dockerfile CHANGED
@@ -13,4 +13,4 @@ COPY --chown=user ./requirements.txt requirements.txt
13
  RUN pip install --no-cache-dir --upgrade -r requirements.txt
14
 
15
  COPY --chown=user . /app
16
- CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
 
13
  RUN pip install --no-cache-dir --upgrade -r requirements.txt
14
 
15
  COPY --chown=user . /app
16
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
app/__init__.py ADDED
File without changes
app/crud.py ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy.orm import Session
2
+ from .database import Base, KeyCategory, APIKey # Import models directly from database module
3
+ # from . import schemas # Schemas will be created later
4
+
5
+ # --- CRUD for KeyCategory ---
6
+
7
+ def create_key_category(db: Session, name: str, type: str, tags: list = None, metadata: dict = None):
8
+ db_category = KeyCategory(name=name, type=type, tags=tags, metadata_=metadata)
9
+ db.add(db_category)
10
+ db.commit()
11
+ db.refresh(db_category)
12
+ return db_category
13
+
14
+ def get_key_category(db: Session, category_id: int):
15
+ return db.query(KeyCategory).filter(KeyCategory.id == category_id).first()
16
+
17
+ def get_key_category_by_name(db: Session, name: str):
18
+ return db.query(KeyCategory).filter(KeyCategory.name == name).first()
19
+
20
+ def get_key_categories(db: Session, skip: int = 0, limit: int = 100):
21
+ return db.query(KeyCategory).offset(skip).limit(limit).all()
22
+
23
+ def update_key_category(db: Session, category_id: int, name: str = None, type: str = None, tags: list = None, metadata: dict = None):
24
+ db_category = get_key_category(db, category_id)
25
+ if db_category:
26
+ if name is not None:
27
+ db_category.name = name
28
+ if type is not None:
29
+ db_category.type = type
30
+ if tags is not None:
31
+ db_category.tags = tags
32
+ if metadata is not None:
33
+ db_category.metadata_ = metadata
34
+ db.commit()
35
+ db.refresh(db_category)
36
+ return db_category
37
+
38
+ def delete_key_category(db: Session, category_id: int):
39
+ db_category = get_key_category(db, category_id)
40
+ if db_category:
41
+ db.delete(db_category)
42
+ db.commit()
43
+ return db_category
44
+
45
+ # --- CRUD for APIKey ---
46
+
47
+ def create_api_key(db: Session, value: str, category_id: int, status: str = "active", metadata: dict = None):
48
+ db_key = APIKey(value=value, category_id=category_id, status=status, metadata_=metadata)
49
+ db.add(db_key)
50
+ db.commit()
51
+ db.refresh(db_key)
52
+ return db_key
53
+
54
+ def get_api_key(db: Session, key_id: int):
55
+ return db.query(APIKey).filter(APIKey.id == key_id).first()
56
+
57
+ def get_api_keys(db: Session, category_id: int = None, status: str = None, skip: int = 0, limit: int = 100):
58
+ query = db.query(APIKey)
59
+ if category_id is not None:
60
+ query = query.filter(APIKey.category_id == category_id)
61
+ if status is not None:
62
+ query = query.filter(APIKey.status == status)
63
+ return query.offset(skip).limit(limit).all()
64
+
65
+ def update_api_key(db: Session, key_id: int, value: str = None, category_id: int = None, status: str = None, metadata: dict = None):
66
+ db_key = get_api_key(db, key_id)
67
+ if db_key:
68
+ if value is not None:
69
+ db_key.value = value
70
+ if category_id is not None:
71
+ db_key.category_id = category_id
72
+ if status is not None:
73
+ db_key.status = status
74
+ if metadata is not None:
75
+ db_key.metadata_ = metadata
76
+ db.commit()
77
+ db.refresh(db_key)
78
+ return db_key
79
+
80
+ def delete_api_key(db: Session, key_id: int):
81
+ db_key = get_api_key(db, key_id)
82
+ if db_key:
83
+ db.delete(db_key)
84
+ db.commit()
85
+ return db_key
86
+
87
+ # --- Key Selection Logic (Basic Placeholder) ---
88
+ # This will be expanded later based on selection strategy (round-robin, etc.)
89
+
90
+ def get_available_keys_for_category(db: Session, category_id: int):
91
+ """Get all active keys for a given category."""
92
+ return db.query(APIKey).filter(
93
+ APIKey.category_id == category_id,
94
+ APIKey.status == "active"
95
+ ).all()
96
+
97
+ # Placeholder for selection strategy - will need to implement round-robin, etc.
98
+ def select_key_from_pool(db: Session, category_id: int):
99
+ """Select one key from the available pool for a category."""
100
+ available_keys = get_available_keys_for_category(db, category_id)
101
+ if not available_keys:
102
+ return None # No keys available
103
+
104
+ # Basic selection: just return the first active key for now
105
+ # TODO: Implement round-robin or other selection strategy
106
+ selected_key = available_keys[0]
107
+
108
+ # Optional: Update usage stats (e.g., increment usage_count, update last_used)
109
+ # selected_key.usage_count += 1
110
+ # selected_key.last_used = func.now()
111
+ # db.commit()
112
+ # db.refresh(selected_key)
113
+
114
+ return selected_key
app/database.py ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from sqlalchemy import create_engine, Column, Integer, String, Text, DateTime, ForeignKey, func
3
+ from sqlalchemy.ext.declarative import declarative_base
4
+ from sqlalchemy.orm import sessionmaker, relationship
5
+ from sqlalchemy.types import TypeDecorator, TEXT
6
+
7
+ # Custom type for JSON storage
8
+ class JSONText(TypeDecorator):
9
+ impl = TEXT
10
+
11
+ def process_bind_param(self, value, dialect):
12
+ if value is not None:
13
+ return json.dumps(value)
14
+ return None
15
+
16
+ def process_result_value(self, value, dialect):
17
+ if value is not None:
18
+ return json.loads(value)
19
+ return None
20
+
21
+ # Database connection URL (using SQLite)
22
+ DATABASE_URL = "sqlite:///./api_proxy.db"
23
+
24
+ # Create SQLAlchemy engine
25
+ engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
26
+
27
+ # Create a configured "Session" class
28
+ SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
29
+
30
+ # Base class for declarative models
31
+ Base = declarative_base()
32
+
33
+ # Define the KeyCategory model
34
+ class KeyCategory(Base):
35
+ __tablename__ = "key_categories"
36
+
37
+ id = Column(Integer, primary_key=True, index=True)
38
+ name = Column(String, unique=True, index=True, nullable=False)
39
+ type = Column(String, nullable=False)
40
+ tags = Column(JSONText) # Stored as JSON string
41
+ metadata_ = Column(JSONText, name="metadata") # Use metadata_ to avoid conflict with SQLAlchemy metadata
42
+
43
+ api_keys = relationship("APIKey", back_populates="category")
44
+
45
+ created_at = Column(DateTime, server_default=func.now())
46
+ updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
47
+
48
+ # Define the APIKey model
49
+ class APIKey(Base):
50
+ __tablename__ = "api_keys"
51
+
52
+ id = Column(Integer, primary_key=True, index=True)
53
+ value = Column(String, nullable=False)
54
+ category_id = Column(Integer, ForeignKey("key_categories.id"), nullable=False)
55
+ status = Column(String, default="active", nullable=False) # 'active', 'inactive'
56
+ usage_count = Column(Integer, default=0, nullable=False)
57
+ last_used = Column(DateTime)
58
+ metadata_ = Column(JSONText, name="metadata") # Use metadata_
59
+
60
+ category = relationship("KeyCategory", back_populates="api_keys")
61
+
62
+ created_at = Column(DateTime, server_default=func.now())
63
+ updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
64
+
65
+ # Function to create database tables
66
+ def create_db_tables():
67
+ Base.metadata.create_all(bind=engine)
68
+
69
+ # Dependency to get DB session
70
+ def get_db():
71
+ db = SessionLocal()
72
+ try:
73
+ yield db
74
+ finally:
75
+ db.close()
key_selector.py CHANGED
@@ -6,7 +6,7 @@ class KeySelector():
6
  def __init__(self):
7
  pass
8
 
9
- def get_api_key(self):
10
  api_key_info = {
11
  "key_at": "at_headers",
12
  "key_name": "X_Goog_Api_Key",
 
6
  def __init__(self):
7
  pass
8
 
9
+ def get_api_key_info(self):
10
  api_key_info = {
11
  "key_at": "at_headers",
12
  "key_name": "X_Goog_Api_Key",
app.py → main.py RENAMED
@@ -36,7 +36,7 @@ async def proxy(request: Request, path: str):
36
  if k.lower() not in ["host", "content-length"]}
37
 
38
  key_selector = KeySelector()
39
- headers["X-Goog-Api-Key"] = key_selector.get_api_key()['key_value'] # 从数据库获取API密钥
40
 
41
  try:
42
  # 关键修复:禁用KeepAlive防止连接冲突
@@ -133,3 +133,9 @@ async def proxy(request: Request, path: str):
133
  except Exception as e:
134
  logger.exception("Unexpected proxy error")
135
  raise HTTPException(500, f"内部服务器错误: {str(e)}")
 
 
 
 
 
 
 
36
  if k.lower() not in ["host", "content-length"]}
37
 
38
  key_selector = KeySelector()
39
+ headers["X-Goog-Api-Key"] = key_selector.get_api_key_info()['key_value'] # 从数据库获取API密钥
40
 
41
  try:
42
  # 关键修复:禁用KeepAlive防止连接冲突
 
133
  except Exception as e:
134
  logger.exception("Unexpected proxy error")
135
  raise HTTPException(500, f"内部服务器错误: {str(e)}")
136
+
137
+ if __name__ == "__main__":
138
+ # In a real application, you would typically run uvicorn here:
139
+ # import uvicorn
140
+ # uvicorn.run(app, host="0.0.0.0", port=8000)
141
+ pass # Placeholder to fix indentation error
memory-bank/activeContext.md CHANGED
@@ -1,25 +1,36 @@
1
  # 当前背景 (Active Context)
2
 
3
  ## 当前工作重点
4
- 初始化项目记忆库。
5
 
6
  ## 最近的变更
7
- - 创建了 `projectbrief.md` 文件,定义了项目的核心目标和范围。
8
- - 创建了 `productContext.md` 文件,描述了项目的目的、解决的问题和用户体验目标。
9
- - 创建了 `systemPatterns.md` 文件,概述了系统架构、设计模式和关键组件。
10
- - 创建了 `techContext.md` 文件,记录了使用的技术、开发环境设置和技术约束。
 
 
 
 
11
 
12
  ## 下一步计划
13
- - 创建 `progress.md` 文件,记录项目的当前状态、已完成和待完成的工作。
14
- - 根据项目需求,开始实现 FastAPI 应用的核心代理转发逻辑。
15
- - 定义和实现代理规则的配置加载机制。
16
- - 集成 HTTP 客户端库 (`httpx`) 进行后端请求转发。
 
 
17
 
18
  ## 重要的模式和偏好
19
  - 优先使用异步编程。
20
  - 保持代码简洁和模块化。
21
  - 遵循 FastAPI 的最佳实践。
 
 
22
 
23
  ## 学习和项目洞察
24
- - 项目处于初期阶段,主要任务是搭建基本框架和配置。
25
- - 需要仔细设计配置文件的结构,以便于灵活地添加和管理代理规则。
 
 
 
 
1
  # 当前背景 (Active Context)
2
 
3
  ## 当前工作重点
4
+ 实现基于数据库的 API Key 管理和代理规则配置,并集成到核心代理转发逻辑中。
5
 
6
  ## 最近的变更
7
+ - 讨论并设计了使用 SQLite 数据库存储 Key 类别、API Key 实例和代理规则的 Schema (三表设计: `key_categories`, `api_keys`, `proxy_rules`)。
8
+ - 安装了 SQLAlchemy 和 aiosqlite 库。
9
+ - 创建了 `app/database.py` 文件,定义了 Key 类别 (`KeyCategory`) 和 API Key 实例 (`APIKey`) 的 SQLAlchemy 模型。
10
+ - 使用 `sqlite3` 命令成功创建了数据库表 (`api_proxy.db`)。
11
+ - 创建了 `app/crud.py` 文件,实现了 Key 类别和 API Key 实例的基本 CRUD (创建、读取、更新、删除) 操作函数。
12
+ - 创建了 `tests/test_crud.py` 文件,为 CRUD 函数编写了单元测试。
13
+ - 成功运行了所有单元测试 (14 个通过)。
14
+ - 解决了 Python 包导入 (`ModuleNotFoundError`, `NameError`) 和文件创建 (`__init__.py` 未成功创建) 等环境和代码问题。
15
 
16
  ## 下一步计划
17
+ - 实现 Key 管理相关的 API 路由 (`/api/keys/...`),使用 `app/crud.py` 中的函数与数据库交互。
18
+ - 完善 Key 选择逻辑 (`app/crud.py` 中的 `select_key_from_pool`),实现 Key 池的轮询或其他策略。
19
+ - 设计并实现代理规则的数据库存储和加载逻辑(`proxy_rules` 表的 CRUD 和查询)。
20
+ - Key 选择和代理规则查找逻辑集成到核心代理转发路由 (`app/routers/proxy.py`) 中。
21
+ - 实现前端/静态文件路由 (`app/routers/frontend.py`) (如果需要)。
22
+ - 编写其他功能的单元测试和集成测试。
23
 
24
  ## 重要的模式和偏好
25
  - 优先使用异步编程。
26
  - 保持代码简洁和模块化。
27
  - 遵循 FastAPI 的最佳实践。
28
+ - 使用数据库进行配置管理以提高灵活性。
29
+ - 通过单元测试验证核心逻辑。
30
 
31
  ## 学习和项目洞察
32
+ - 确认了使用 SQLite 和 SQLAlchemy 进行数据库操作的可行性。
33
+ - 解决了在当前环境中进行 Python 包导入和文件创建时遇到的具体问题。
34
+ - 明确了 Key 的应用方式应与代理规则关联,而非 Key 本身。
35
+ - 数据库三表设计 (`key_categories`, `api_keys`, `proxy_rules`) 能够更好地组织 Key 和规则信息。
36
+ </textarea>
memory-bank/progress.md CHANGED
@@ -1,24 +1,29 @@
1
  # 项目进度 (Progress)
2
 
3
  ## 当前状态
4
- 项目记忆库已初始化。核心记忆文件(projectbrief.md, productContext.md, systemPatterns.md, techContext.md, activeContext.md)已创建。
5
 
6
  ## 已完成的工作
7
- - 创建了 Memory Bank 目录。
8
- - 创建并填充了以下记忆文件:
9
- - `projectbrief.md`
10
- - `productContext.md`
11
- - `systemPatterns.md`
12
- - `techContext.md`
13
- - `activeContext.md`
 
 
 
14
 
15
  ## 待完成的工作
16
- - 实现 FastAPI 应用的核心代理转发逻辑。
17
- - 设计和实现代理规则的配置加载机制。
18
- - 集成 `httpx` 库进行后端请求转发。
 
 
19
  - 添加基本的日志记录功能。
20
  - 根据需求实现请求/响应转换功能。
21
- - 编写单元测试和集成测试。
22
  - 完善 Dockerfile。
23
  - 编写详细的 README.md 文档。
24
 
@@ -28,4 +33,6 @@
28
  ## 项目决策演变
29
  - 决定使用 FastAPI 作为后端框架,因为它提供了高性能和易于使用的异步支持。
30
  - 决定使用 `httpx` 作为 HTTP 客户端,因为它支持异步请求。
31
- - 决定使用配置文件来管理代理规则,以提高灵活性。
 
 
 
1
  # 项目进度 (Progress)
2
 
3
  ## 当前状态
4
+ 数据库已设置,Key 类别和 API Key 实例的基本 CRUD 操作已实现并通过单元测试。项目记忆库已更新。
5
 
6
  ## 已完成的工作
7
+ - 创建了 Memory Bank 目录并初始化了核心记忆文件。
8
+ - 讨论并设计了基于 SQLite 的 Key 管理和代理规则数据库 Schema (三表设计)。
9
+ - 安装了必要的 Python 依赖 (SQLAlchemy, aiosqlite)。
10
+ - 创建了 `app/database.py` 文件,定义了 SQLAlchemy 模型和数据库连接。
11
+ - 使用 `sqlite3` 命令成功创建了数据库表 (`api_proxy.db`)。
12
+ - 创建了 `app/crud.py` 文件,实现了 Key 类别和 API Key 实例的 CRUD 函数。
13
+ - 创建了 `tests/test_crud.py` 文件,为 CRUD 函数编写了单元测试。
14
+ - 成功运行了所有单元测试 (14 个通过)。
15
+ - 解决了环境配置和 Python 导入问题 (`__init__.py` 创建、导入路径修正)。
16
+ - 更新了 Memory Bank 文件 (`systemPatterns.md`, `techContext.md`, `activeContext.md`, `progress.md`)。
17
 
18
  ## 待完成的工作
19
+ - 实现 Key 管理相关的 API 路由 (`/api/keys/...`)。
20
+ - 完善 Key 选择逻辑 (`app/crud.py` 中的 `select_key_from_pool`),实现 Key 池的轮询或其他策略。
21
+ - 设计并实现代理规则的数据库存储和加载逻辑(`proxy_rules` 表的 CRUD 和查询)。
22
+ - 将 Key 选择和代理规则查找逻辑集成到核心代理转发路由 (`app/routers/proxy.py`) 中。
23
+ - 实现前端/静态文件路由 (`app/routers/frontend.py`) (如果需要)。
24
  - 添加基本的日志记录功能。
25
  - 根据需求实现请求/响应转换功能。
26
+ - 编写剩余功能的单元测试和集成测试。
27
  - 完善 Dockerfile。
28
  - 编写详细的 README.md 文档。
29
 
 
33
  ## 项目决策演变
34
  - 决定使用 FastAPI 作为后端框架,因为它提供了高性能和易于使用的异步支持。
35
  - 决定使用 `httpx` 作为 HTTP 客户端,因为它支持异步请求。
36
+ - **重要决策**: 决定使用 SQLite 数据库 (配合 SQLAlchemy 和 aiosqlite) 来管理 API Key 和代理规则,取代了最初使用静态配置文件的想法,以支持更灵活的动态管理和 Key 池化。
37
+ - 数据库 Schema 经过讨论,从单表设计演变为三表设计 (`key_categories`, `api_keys`, `proxy_rules`),以更好地组织数据和处理 Key 应用方式与规则关联的需求。
38
+ </textarea>
memory-bank/systemPatterns.md CHANGED
@@ -1,32 +1,39 @@
1
  # 系统模式 (System Patterns)
2
 
3
  ## 架构概述
4
- 本项目将采用基于 FastAPI 的微服务架构。核心是一个 FastAPI 应用,负责接收所有进来的请求,并根据配置将请求转发到相应的后端服务。
5
 
6
  ```mermaid
7
  graph LR
8
  Client --> Proxy[FastAPI Proxy]
 
9
  Proxy --> BackendA[Backend Service A]
10
  Proxy --> BackendB[Backend Service B]
11
  Proxy --> BackendC[Backend Service C]
 
12
  ```
13
 
14
  ## 设计模式
15
  - **API Gateway Pattern**: FastAPI 应用充当 API 网关,作为客户端访问后端服务的单一入口点。
16
- - **Configuration Pattern**: 代理规则将通过配置文件进行管理,以便于修改和扩展。
 
17
  - **Middleware Pattern**: 利用 FastAPI 的中间件功能处理请求和响应的通用逻辑(如日志、认证等)。
18
 
19
  ## 关键组件
20
- - **FastAPI Application**: 核心应用,处理路由和请求转发。
21
- - **Configuration Loader**: 负责加载和解析代理规则配置文件。
 
 
 
22
  - **HTTP Client**: 用于向后端服务发起请求(例如使用 `httpx` 库)。
23
  - **Request/Response Transformer (Optional)**: 根据配置修改请求和响应。
24
 
25
  ## 数据流
26
  1. 客户端发起请求到 FastAPI Proxy。
27
  2. FastAPI Proxy 接收请求。
28
- 3. Configuration Loader 解析配置,查找匹配的代理规则。
29
- 4. 如果找到匹配规则,HTTP Client 根据规则向后端服务发起请求。
30
- 5. 后端服务返回响应。
31
- 6. Request/Response Transformer (如果存在) 修改响应。
32
- 7. FastAPI Proxy 将响应返回给客户端。
 
 
1
  # 系统模式 (System Patterns)
2
 
3
  ## 架构概述
4
+ 本项目将采用基于 FastAPI 的微服务架构。核心是一个 FastAPI 应用,负责接收所有进来的请求,并根据数据库中的配置将请求转发到相应的后端服务,并管理 API Key。
5
 
6
  ```mermaid
7
  graph LR
8
  Client --> Proxy[FastAPI Proxy]
9
+ Proxy --> Database[SQLite Database]
10
  Proxy --> BackendA[Backend Service A]
11
  Proxy --> BackendB[Backend Service B]
12
  Proxy --> BackendC[Backend Service C]
13
+ Database --> Proxy
14
  ```
15
 
16
  ## 设计模式
17
  - **API Gateway Pattern**: FastAPI 应用充当 API 网关,作为客户端访问后端服务的单一入口点。
18
+ - **Database Pattern**: 使用 SQLite 数据库存储 Key 类别、Key 实例和代理规则配置。
19
+ - **Configuration Pattern**: 代理规则和 Key 信息通过数据库进行管理,而非静态配置文件。
20
  - **Middleware Pattern**: 利用 FastAPI 的中间件功能处理请求和响应的通用逻辑(如日志、认证等)。
21
 
22
  ## 关键组件
23
+ - **FastAPI Application**: 核心应用,处理路由、请求转发和 Key 管理 API。
24
+ - **SQLite Database**: 存储 Key 类别 (`key_categories`)、API Key 实例 (`api_keys`) 和代理规则 (`proxy_rules`)。
25
+ - **SQLAlchemy Models**: 定义与数据库表对应的 Python 模型 (`KeyCategory`, `APIKey`)。
26
+ - **CRUD Operations**: 实现对数据库中 Key 类别和 Key 实例的增删改查逻辑 (`app/crud.py`)。
27
+ - **Key Selection Logic**: 根据代理规则从 Key 池中选择合适 Key 的逻辑。
28
  - **HTTP Client**: 用于向后端服务发起请求(例如使用 `httpx` 库)。
29
  - **Request/Response Transformer (Optional)**: 根据配置修改请求和响应。
30
 
31
  ## 数据流
32
  1. 客户端发起请求到 FastAPI Proxy。
33
  2. FastAPI Proxy 接收请求。
34
+ 3. **Proxy 查找数据库**: 根据请求查找数据库 (`proxy_rules` 表) 获取匹配的代理规则。
35
+ 4. **Key 选择 (如果需要)**: 如果匹配规则需要 Key,Proxy 查询数据库 (`api_keys` 表) 获取 Key 池,并根据策略选择一个 Key。
36
+ 5. **应用 Key 并转发**: Proxy 根据规则中定义的应用方式,将选定的 Key 应用到转发请求中。使用 HTTP Client 向后端服务发起请求。
37
+ 6. 后端服务返回响应。
38
+ 7. Request/Response Transformer (如果存在) 修改响应。
39
+ 8. FastAPI Proxy 将响应返回给客户端。
memory-bank/techContext.md CHANGED
@@ -2,8 +2,11 @@
2
 
3
  ## 使用的技术
4
  - **后端框架**: FastAPI (Python)
 
 
 
5
  - **异步 HTTP 客户端**: httpx
6
- - **配置管理**: 可能使用 `python-dotenv` 或其他配置库来加载配置文件。
7
  - **依赖管理**: pip 和 requirements.txt
8
  - **容器化**: Docker
9
 
@@ -11,16 +14,19 @@
11
  1. 克隆仓库。
12
  2. 创建并激活 Conda 虚拟环境 (`conda create -n api-proxy python=3.9` 或使用现有环境,然后 `conda activate api-proxy`)。
13
  3. 安装依赖 (`pip install -r requirements.txt`)。
14
- 4. 运行应用 (`uvicorn app:app --reload`)。
 
15
 
16
  ## 技术约束
17
  - 需要 Python 3.7+。
18
- - 依赖于 FastAPI httpx 库。
19
 
20
  ## 依赖关系
21
- - `requirements.txt` 文件列出了所有必要的 Python 依赖。
22
 
23
  ## 工具使用模式
24
  - 使用 `uvicorn` 作为 ASGI 服务器运行 FastAPI 应用。
25
  - 使用 `pip` 管理 Python 包。
26
  - 使用 Docker 构建和运行容器。
 
 
 
2
 
3
  ## 使用的技术
4
  - **后端框架**: FastAPI (Python)
5
+ - **数据库**: SQLite
6
+ - **ORM**: SQLAlchemy
7
+ - **异步 SQLite 驱动**: aiosqlite
8
  - **异步 HTTP 客户端**: httpx
9
+ - **配置管理**: 数据库用于存储代理规则和 Key 信息。
10
  - **依赖管理**: pip 和 requirements.txt
11
  - **容器化**: Docker
12
 
 
14
  1. 克隆仓库。
15
  2. 创建并激活 Conda 虚拟环境 (`conda create -n api-proxy python=3.9` 或使用现有环境,然后 `conda activate api-proxy`)。
16
  3. 安装依赖 (`pip install -r requirements.txt`)。
17
+ 4. **创建数据库**: 运行数据库初始化脚本或命令(例如 `sqlite3 ./api_proxy.db "..."`)。
18
+ 5. 运行应用 (`uvicorn app:app --reload`)。
19
 
20
  ## 技术约束
21
  - 需要 Python 3.7+。
22
+ - 依赖于 FastAPI, httpx, SQLAlchemy, aiosqlite 库。
23
 
24
  ## 依赖关系
25
+ - `requirements.txt` 文件列出了所有必要的 Python 依赖,包括 FastAPI, httpx, SQLAlchemy, aiosqlite。
26
 
27
  ## 工具使用模式
28
  - 使用 `uvicorn` 作为 ASGI 服务器运行 FastAPI 应用。
29
  - 使用 `pip` 管理 Python 包。
30
  - 使用 Docker 构建和运行容器。
31
+ - 使用 `sqlite3` 命令或 SQLAlchemy 进行数据库管理。
32
+ - 使用 `pytest` 运行单元测试。
requirements.txt CHANGED
@@ -1,3 +1,5 @@
1
  fastapi
2
  uvicorn[standard]
3
  httpx
 
 
 
1
  fastapi
2
  uvicorn[standard]
3
  httpx
4
+ SQLAlchemy
5
+ aiosqlite
tests/test_crud.py ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pytest
2
+ from sqlalchemy import create_engine
3
+ from sqlalchemy.orm import sessionmaker, Session # Import Session
4
+ from app.database import Base, get_db, KeyCategory, APIKey # Import Base, get_db, and models from database
5
+ from app import crud # Import crud from app.crud
6
+
7
+ # Use an in-memory SQLite database for testing
8
+ SQLALCHEMY_DATABASE_URL = "sqlite:///:memory:"
9
+
10
+ engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
11
+ TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
12
+
13
+ # Override the get_db dependency for testing
14
+ def override_get_db():
15
+ try:
16
+ db = TestingSessionLocal()
17
+ yield db
18
+ finally:
19
+ db.close()
20
+
21
+ # Fixture to create and drop tables for each test
22
+ @pytest.fixture(scope="function")
23
+ def db_session():
24
+ Base.metadata.create_all(bind=engine)
25
+ db = TestingSessionLocal()
26
+ yield db
27
+ db.close()
28
+ Base.metadata.drop_all(bind=engine)
29
+
30
+ # --- Test KeyCategory CRUD ---
31
+
32
+ def test_create_key_category(db_session: Session):
33
+ category = crud.create_key_category(db_session, name="test_llm", type="llm", tags=["llm", "test"])
34
+ assert category.id is not None
35
+ assert category.name == "test_llm"
36
+ assert category.type == "llm"
37
+ assert category.tags == ["llm", "test"]
38
+
39
+ def test_get_key_category(db_session: Session):
40
+ created_category = crud.create_key_category(db_session, name="get_test", type="test")
41
+ retrieved_category = crud.get_key_category(db_session, created_category.id)
42
+ assert retrieved_category is not None
43
+ assert retrieved_category.name == "get_test"
44
+
45
+ def test_get_key_category_by_name(db_session: Session):
46
+ crud.create_key_category(db_session, name="get_by_name_test", type="test")
47
+ retrieved_category = crud.get_key_category_by_name(db_session, "get_by_name_test")
48
+ assert retrieved_category is not None
49
+ assert retrieved_category.name == "get_by_name_test"
50
+
51
+ def test_get_key_categories(db_session: Session):
52
+ crud.create_key_category(db_session, name="list_test_1", type="test")
53
+ crud.create_key_category(db_session, name="list_test_2", type="test")
54
+ categories = crud.get_key_categories(db_session)
55
+ assert len(categories) >= 2 # Account for potential other categories if not in-memory db
56
+
57
+ def test_update_key_category(db_session: Session):
58
+ created_category = crud.create_key_category(db_session, name="update_test", type="old_type")
59
+ updated_category = crud.update_key_category(db_session, created_category.id, name="updated_test", type="new_type", tags=["updated"])
60
+ assert updated_category.name == "updated_test"
61
+ assert updated_category.type == "new_type"
62
+ assert updated_category.tags == ["updated"]
63
+
64
+ def test_delete_key_category(db_session: Session):
65
+ created_category = crud.create_key_category(db_session, name="delete_test", type="test")
66
+ deleted_category = crud.delete_key_category(db_session, created_category.id)
67
+ assert deleted_category is not None
68
+ retrieved_category = crud.get_key_category(db_session, created_category.id)
69
+ assert retrieved_category is None
70
+
71
+ # --- Test APIKey CRUD ---
72
+
73
+ def test_create_api_key(db_session: Session):
74
+ category = crud.create_key_category(db_session, name="key_cat_for_key", type="test")
75
+ api_key = crud.create_api_key(db_session, value="test_key_value", category_id=category.id)
76
+ assert api_key.id is not None
77
+ assert api_key.value == "test_key_value"
78
+ assert api_key.category_id == category.id
79
+ assert api_key.status == "active"
80
+
81
+ def test_get_api_key(db_session: Session):
82
+ category = crud.create_key_category(db_session, name="get_key_cat", type="test")
83
+ created_key = crud.create_api_key(db_session, value="get_test_key", category_id=category.id)
84
+ retrieved_key = crud.get_api_key(db_session, created_key.id)
85
+ assert retrieved_key is not None
86
+ assert retrieved_key.value == "get_test_key"
87
+
88
+ def test_get_api_keys(db_session: Session):
89
+ category1 = crud.create_key_category(db_session, name="list_key_cat_1", type="test")
90
+ category2 = crud.create_key_category(db_session, name="list_key_cat_2", type="test")
91
+ crud.create_api_key(db_session, value="list_key_1", category_id=category1.id)
92
+ crud.create_api_key(db_session, value="list_key_2", category_id=category1.id)
93
+ crud.create_api_key(db_session, value="list_key_3", category_id=category2.id)
94
+
95
+ all_keys = crud.get_api_keys(db_session)
96
+ assert len(all_keys) >= 3
97
+
98
+ category1_keys = crud.get_api_keys(db_session, category_id=category1.id)
99
+ assert len(category1_keys) == 2
100
+
101
+ active_keys = crud.get_api_keys(db_session, status="active")
102
+ assert len(active_keys) >= 3 # All created keys are active by default
103
+
104
+ inactive_keys = crud.get_api_keys(db_session, status="inactive")
105
+ assert len(inactive_keys) == 0
106
+
107
+ def test_update_api_key(db_session: Session):
108
+ category1 = crud.create_key_category(db_session, name="update_key_cat_1", type="test")
109
+ category2 = crud.create_key_category(db_session, name="update_key_cat_2", type="test")
110
+ created_key = crud.create_api_key(db_session, value="old_key_value", category_id=category1.id, status="active")
111
+
112
+ updated_key = crud.update_api_key(db_session, created_key.id, value="new_key_value", category_id=category2.id, status="inactive")
113
+ assert updated_key.value == "new_key_value"
114
+ assert updated_key.category_id == category2.id
115
+ assert updated_key.status == "inactive"
116
+
117
+ def test_delete_api_key(db_session: Session):
118
+ category = crud.create_key_category(db_session, name="delete_key_cat", type="test")
119
+ created_key = crud.create_api_key(db_session, value="delete_test_key", category_id=category.id)
120
+ deleted_key = crud.delete_api_key(db_session, created_key.id)
121
+ assert deleted_key is not None
122
+ retrieved_key = crud.get_api_key(db_session, created_key.id)
123
+ assert retrieved_key is None
124
+
125
+ # --- Test Key Selection Logic Placeholder ---
126
+
127
+ def test_get_available_keys_for_category(db_session: Session):
128
+ category = crud.create_key_category(db_session, name="available_key_cat", type="test")
129
+ crud.create_api_key(db_session, value="key1", category_id=category.id, status="active")
130
+ crud.create_api_key(db_session, value="key2", category_id=category.id, status="inactive")
131
+ crud.create_api_key(db_session, value="key3", category_id=category.id, status="active")
132
+
133
+ available_keys = crud.get_available_keys_for_category(db_session, category.id)
134
+ assert len(available_keys) == 2
135
+ assert all(key.status == "active" for key in available_keys)
136
+ assert {key.value for key in available_keys} == {"key1", "key3"}
137
+
138
+ def test_select_key_from_pool_no_keys(db_session: Session):
139
+ category = crud.create_key_category(db_session, name="empty_pool_cat", type="test")
140
+ selected_key = crud.select_key_from_pool(db_session, category.id)
141
+ assert selected_key is None
142
+
143
+ def test_select_key_from_pool_basic(db_session: Session):
144
+ category = crud.create_key_category(db_session, name="basic_pool_cat", type="test")
145
+ key1 = crud.create_api_key(db_session, value="key1", category_id=category.id, status="active")
146
+ key2 = crud.create_api_key(db_session, value="key2", category_id=category.id, status="active")
147
+
148
+ # Basic selection returns the first one found
149
+ selected_key = crud.select_key_from_pool(db_session, category.id)
150
+ assert selected_key is not None
151
+ # The order might depend on DB implementation, but it should be one of the active keys
152
+ assert selected_key.value in ["key1", "key2"]