fix: 去除无用代码

master
Lxy 3 months ago
parent 19810e4519
commit 985d70fa2d

@ -9,6 +9,7 @@
### 1. 后端基础架构 ✅
**技术栈:**
- Node.js 20.x LTS
- Express 4.x
- TypeScript 5.x
@ -18,6 +19,7 @@
- Redis 7
**目录结构:**
```
app/backend/
├── src/
@ -44,6 +46,7 @@ app/backend/
### 2. 数据库模型 ✅
**Prisma Schema 包含以下模型:**
- `MarketIndex` - 市场指数
- `Sector` - 版块信息
- `SectorQuote` - 版块行情
@ -59,11 +62,13 @@ app/backend/
### 3. API 接口实现 ✅
**市场数据接口:**
- `GET /api/v1/market/indices` - 获取市场指数
- `GET /api/v1/market/updown-stats` - 获取涨跌家数统计
- `GET /api/v1/market/price-distribution` - 获取涨跌幅分布
**版块数据接口:**
- `GET /api/v1/sectors` - 获取版块列表
- `GET /api/v1/sectors/:sector_code` - 获取版块详情
- `GET /api/v1/sectors/:sector_code/rank-history` - 获取版块历史排名
@ -72,6 +77,7 @@ app/backend/
- `GET /api/v1/sectors/:sector_code/kline` - 获取版块K线
**股票数据接口:**
- `GET /api/v1/stocks/search` - 搜索股票
- `GET /api/v1/stocks/:stock_code` - 获取股票详情
- `GET /api/v1/stocks/:stock_code/kline` - 获取股票K线
@ -80,6 +86,7 @@ app/backend/
- `GET /api/v1/stocks/momentum-recommendation` - 获取动量股推荐
**用户接口:**
- `POST /api/v1/users/register` - 用户注册
- `POST /api/v1/users/login` - 用户登录
- `GET /api/v1/users/profile` - 获取用户信息
@ -90,6 +97,7 @@ app/backend/
### 4. WebSocket 实时数据服务 ✅
**功能:**
- Socket.io 实时连接管理
- 频道订阅/取消订阅机制
- 股票行情实时推送
@ -100,6 +108,7 @@ app/backend/
- 自动重连机制
**协议:**
```javascript
// 订阅
{ action: 'subscribe', channels: ['stock:000001', 'sector:880491'] }
@ -116,6 +125,7 @@ app/backend/
### 5. 数据同步服务 ✅
**定时任务:**
- 每3秒同步实时行情交易时间
- 每分钟同步版块行情
- 每小时同步市场指数
@ -124,12 +134,14 @@ app/backend/
- 每日收盘后全量同步15:10
**数据源:**
- AKShareA股免费数据源
- 支持实时行情、K线数据、版块数据
### 6. 计算服务 ✅
**技术指标计算:**
- 均线计算MA5/MA10/MA20/MA30/MA60
- EMA指数移动平均
- MACD 计算
@ -137,6 +149,7 @@ app/backend/
- RSI 计算6/12/24周期
**评分计算:**
- 动量分数计算
- 版块动量分数
- 综合评分算法
@ -144,11 +157,13 @@ app/backend/
### 7. 中间件 ✅
**认证与授权:**
- JWT Token 认证
- 用户登录/注册
- 可选认证中间件
**限流:**
- 通用限流100次/分钟/IP
- 严格限流(敏感操作)
- 登录限流5次/15分钟
@ -156,12 +171,14 @@ app/backend/
- WebSocket 连接限流
**错误处理:**
- 全局错误处理
- 自定义错误类
- Zod 参数验证
- 异步路由包装器
**日志:**
- Winston 日志系统
- 按天轮转
- 请求日志
@ -170,15 +187,16 @@ app/backend/
### 8. Docker 部署配置 ✅
**Dockerfile**
- 多阶段构建
- 生产环境优化
- 健康检查
**Docker Compose**
- MySQL 8.0 数据库
- Redis 7 缓存
- Node.js 应用服务
- AKShare 数据服务(可选)
- 自动健康检查
### 9. 前端 API 客户端 ✅
@ -186,6 +204,7 @@ app/backend/
**文件:** `app/src/services/api.ts`
**功能:**
- REST API 封装marketApi, sectorApi, stockApi, userApi
- WebSocket 客户端封装
- 自动错误处理
@ -255,39 +274,42 @@ LOG_LEVEL=info
### 近期(高优先级)
1. **前端对接**
- 替换模拟数据为真实 API
- 接入 WebSocket 实时推送
- 实现登录/注册页面
- 实现自选股管理页面
2. **数据完善**
- 导入历史K线数据
- 接入更多数据源Tushare Pro
### 中期(中优先级)
1. **功能增强**
- 预警系统
- 主题切换
- 多语言支持
2. **性能优化**
- 数据库索引优化
- Redis 缓存策略优化
- 前端性能优化
3. **测试**
- 单元测试
- E2E 测试
### 长期(低优先级)
1. **高级功能**
- 策略回测
- 模拟交易
- 资讯系统
2. **运维**
- 监控告警
- 日志收集
- 自动备份
@ -306,3 +328,4 @@ LOG_LEVEL=info
- `app/docs/06-后端实现.md` - 后端实现细节
- `app/docs/07-部署文档.md` - 部署指南
- `app/docs/08-待办事项.md` - 完整任务清单

@ -1,32 +0,0 @@
FROM python:3.11-slim
WORKDIR /app
# 安装系统依赖
RUN apt-get update && apt-get install -y \
gcc \
g++ \
default-libmysqlclient-dev \
pkg-config \
&& rm -rf /var/lib/apt/lists/*
# 复制依赖文件
COPY requirements.txt .
# 安装 Python 依赖
RUN pip install --no-cache-dir -r requirements.txt
# 复制应用代码
COPY database.py .
COPY data_sync.py .
COPY main_api.py .
# 暴露端口
EXPOSE 8000
# 健康检查
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/')" || exit 1
# 启动服务
CMD ["python", "-m", "uvicorn", "main_api:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "1"]

@ -1,421 +0,0 @@
"""
数据同步模块
AKShare 获取数据并同步到 MySQL 数据库
"""
import os
import json
import time
from datetime import datetime, timedelta
from typing import List, Dict, Optional
from concurrent.futures import ThreadPoolExecutor, as_completed
import akshare as ak
import pandas as pd
from sqlalchemy import func
from database import (
get_db, init_db, check_connection,
Stock, StockQuote, StockKLine, Sector, SectorQuote,
MarketIndex, HighLowStock, MomentumStock,
upsert_stock, upsert_stock_quote, upsert_stock_kline,
upsert_sector, upsert_market_index
)
# 配置
BATCH_SIZE = 100
MAX_WORKERS = 5
def dataframe_to_records(df: pd.DataFrame) -> List[Dict]:
"""将 DataFrame 转换为可 JSON 序列化的记录列表"""
if df is None or df.empty:
return []
df = df.replace({pd.NaT: None})
df = df.where(pd.notnull(df), None)
return df.to_dict('records')
def safe_float(value, default=0.0):
"""安全转换为浮点数"""
try:
if pd.isna(value) or value is None:
return default
return float(value)
except:
return default
def safe_int(value, default=0):
"""安全转换为整数"""
try:
if pd.isna(value) or value is None:
return default
return int(value)
except:
return default
# ==================== 股票数据同步 ====================
def sync_all_stocks():
"""同步所有股票基础信息"""
print("开始同步股票列表...")
try:
df = ak.stock_zh_a_spot_em()
records = dataframe_to_records(df)
with get_db() as db:
count = 0
for record in records:
try:
code = record.get("代码")
name = record.get("名称")
industry = record.get("行业", "")
if not code or not name:
continue
upsert_stock(
db,
code=code,
name=name,
pe=safe_float(record.get("市盈率-动态")),
pb=safe_float(record.get("市净率")),
)
count += 1
if count % BATCH_SIZE == 0:
print(f"已处理 {count} 只股票")
except Exception as e:
print(f"处理股票 {record.get('代码')} 失败: {e}")
continue
print(f"股票列表同步完成,共 {count}")
return count
except Exception as e:
print(f"同步股票列表失败: {e}")
return 0
def sync_realtime_quotes():
"""同步实时行情"""
print("开始同步实时行情...")
try:
df = ak.stock_zh_a_spot_em()
records = dataframe_to_records(df)
now = datetime.now()
with get_db() as db:
count = 0
for record in records:
try:
code = record.get("代码")
if not code:
continue
upsert_stock_quote(
db,
stock_code=code,
price=safe_float(record.get("最新价")),
open=safe_float(record.get("开盘价")),
high=safe_float(record.get("最高价")),
low=safe_float(record.get("最低价")),
preClose=safe_float(record.get("昨收")),
volume=safe_int(record.get("成交量")),
turnover=safe_int(record.get("成交额")),
changePercent=safe_float(record.get("涨跌幅")),
turnoverRate=safe_float(record.get("换手率")),
amplitude=safe_float(record.get("振幅")),
quoteTime=now
)
count += 1
except Exception as e:
print(f"处理行情 {record.get('代码')} 失败: {e}")
continue
print(f"实时行情同步完成,共 {count}")
return count
except Exception as e:
print(f"同步实时行情失败: {e}")
return 0
def sync_stock_kline(symbol: str, period: str = "daily", days: int = 365):
"""同步单只股票K线数据"""
try:
end_date = datetime.now()
start_date = end_date - timedelta(days=days)
df = ak.stock_zh_a_hist(
symbol=symbol,
period=period,
start_date=start_date.strftime("%Y%m%d"),
end_date=end_date.strftime("%Y%m%d"),
adjust="qfq"
)
if df is None or df.empty:
return 0
records = dataframe_to_records(df)
period_map = {"daily": "day", "weekly": "week", "monthly": "month"}
db_period = period_map.get(period, "day")
with get_db() as db:
count = 0
for record in records:
try:
date_str = record.get("日期")
if not date_str:
continue
date = datetime.strptime(str(date_str), "%Y-%m-%d")
upsert_stock_kline(
db,
stock_code=symbol,
period=db_period,
date=date,
open=safe_float(record.get("开盘")),
high=safe_float(record.get("最高")),
low=safe_float(record.get("最低")),
close=safe_float(record.get("收盘")),
volume=safe_int(record.get("成交量"))
)
count += 1
except Exception as e:
continue
return count
except Exception as e:
print(f"同步 {symbol} K线失败: {e}")
return 0
def sync_all_klines(period: str = "daily", days: int = 365, max_stocks: Optional[int] = None):
"""批量同步所有股票K线数据"""
print(f"开始同步K线数据 (周期: {period}, 天数: {days})...")
# 获取股票列表
with get_db() as db:
stocks = db.query(Stock).limit(max_stocks).all() if max_stocks else db.query(Stock).all()
total = len(stocks)
print(f"{total} 只股票需要同步")
success_count = 0
fail_count = 0
for i, stock in enumerate(stocks):
try:
count = sync_stock_kline(stock.code, period, days)
if count > 0:
success_count += 1
print(f"[{i+1}/{total}] {stock.code} {stock.name} 同步 {count} 条K线")
else:
fail_count += 1
# 避免请求过快
time.sleep(0.5)
except Exception as e:
print(f"[{i+1}/{total}] {stock.code} 同步失败: {e}")
fail_count += 1
continue
print(f"K线同步完成成功: {success_count}, 失败: {fail_count}")
return success_count
# ==================== 板块数据同步 ====================
def sync_sectors():
"""同步板块信息"""
print("开始同步板块信息...")
try:
df = ak.stock_board_industry_name_em()
records = dataframe_to_records(df)
with get_db() as db:
count = 0
for record in records:
try:
code = record.get("代码")
name = record.get("名称")
if not code or not name:
continue
upsert_sector(db, code=code, name=name)
count += 1
except Exception as e:
print(f"处理板块 {record.get('名称')} 失败: {e}")
continue
print(f"板块信息同步完成,共 {count}")
return count
except Exception as e:
print(f"同步板块信息失败: {e}")
return 0
def sync_sector_quotes():
"""同步板块行情"""
print("开始同步板块行情...")
try:
df = ak.stock_board_industry_name_em()
records = dataframe_to_records(df)
now = datetime.now()
with get_db() as db:
count = 0
for record in records:
try:
code = record.get("代码")
if not code:
continue
quote = SectorQuote(
sectorCode=code,
current=0,
change=0,
changePercent=safe_float(record.get("涨跌幅")),
volume=0,
turnover=0,
quoteTime=now
)
db.add(quote)
count += 1
except Exception as e:
continue
print(f"板块行情同步完成,共 {count}")
return count
except Exception as e:
print(f"同步板块行情失败: {e}")
return 0
# ==================== 指数数据同步 ====================
def sync_market_indices():
"""同步市场指数"""
print("开始同步市场指数...")
try:
df = ak.index_zh_a_spot_em()
records = dataframe_to_records(df)
with get_db() as db:
count = 0
for record in records:
try:
code = record.get("代码")
name = record.get("名称")
if not code or not name:
continue
upsert_market_index(
db,
code=code,
name=name,
current=safe_float(record.get("最新价")),
change=safe_float(record.get("涨跌额")),
changePercent=safe_float(record.get("涨跌幅")),
volume=safe_int(record.get("成交量")),
turnover=safe_int(record.get("成交额"))
)
count += 1
except Exception as e:
print(f"处理指数 {record.get('名称')} 失败: {e}")
continue
print(f"市场指数同步完成,共 {count}")
return count
except Exception as e:
print(f"同步市场指数失败: {e}")
return 0
# ==================== 批量同步工具 ====================
def sync_all(quick: bool = False):
"""执行全部同步
Args:
quick: 如果为True只同步少量数据用于测试
"""
print("=" * 60)
print("开始全量数据同步")
print("=" * 60)
# 1. 同步市场指数
sync_market_indices()
# 2. 同步板块
sync_sectors()
sync_sector_quotes()
# 3. 同步股票列表
sync_all_stocks()
# 4. 同步实时行情
sync_realtime_quotes()
# 5. 同步K线如果quick=True只同步少量股票
if quick:
print("快速模式只同步前10只股票的K线")
sync_all_klines(days=30, max_stocks=10)
else:
sync_all_klines(days=365)
print("=" * 60)
print("全量数据同步完成")
print("=" * 60)
def sync_daily():
"""每日增量同步"""
print("=" * 60)
print(f"开始每日增量同步 - {datetime.now()}")
print("=" * 60)
# 1. 同步实时行情
sync_realtime_quotes()
# 2. 同步板块行情
sync_sector_quotes()
# 3. 同步市场指数
sync_market_indices()
# 4. 同步今日K线
sync_all_klines(days=1)
print("=" * 60)
print("每日增量同步完成")
print("=" * 60)
if __name__ == "__main__":
# 测试数据库连接
if not check_connection():
print("数据库连接失败")
exit(1)
# 初始化数据库表
init_db()
# 执行全量同步(快速模式)
sync_all(quick=True)

@ -1,353 +0,0 @@
"""
数据库连接和模型模块
用于连接 MySQL 数据库并操作数据
"""
import os
from datetime import datetime
from typing import List, Optional, Dict, Any
from contextlib import contextmanager
from sqlalchemy import (
create_engine, Column, Integer, BigInteger, Float, String,
DateTime, Boolean, Text, ForeignKey, Index, UniqueConstraint
)
from sqlalchemy.orm import declarative_base, sessionmaker, Session
from sqlalchemy.pool import QueuePool
# 加载环境变量
from dotenv import load_dotenv
load_dotenv()
# 数据库配置
DB_HOST = os.getenv('DB_HOST', 'mysql')
DB_PORT = os.getenv('DB_PORT', '3306')
DB_NAME = os.getenv('DB_NAME', 'aguzhitou')
DB_USER = os.getenv('DB_USER', 'root')
DB_PASSWORD = os.getenv('DB_PASSWORD', '1qazse42W3')
DATABASE_URL = f"mysql+pymysql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}"
# 创建引擎
engine = create_engine(
DATABASE_URL,
poolclass=QueuePool,
pool_size=10,
max_overflow=20,
pool_pre_ping=True,
pool_recycle=3600,
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
# ==================== 数据模型 ====================
class MarketIndex(Base):
"""市场指数"""
__tablename__ = "market_indices"
__table_args__ = (
Index('idx_market_indices_code', 'code'),
)
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String(100), unique=True, nullable=False)
code = Column(String(50), unique=True, nullable=False)
current = Column(Float, default=0)
change = Column(Float, default=0)
changePercent = Column('change_percent', Float, default=0)
volume = Column(BigInteger, default=0)
turnover = Column(BigInteger, default=0)
updatedAt = Column('updated_at', DateTime, default=datetime.now, onupdate=datetime.now)
createdAt = Column('created_at', DateTime, default=datetime.now)
class Sector(Base):
"""板块信息"""
__tablename__ = "sectors"
__table_args__ = (
Index('idx_sectors_code', 'code'),
)
id = Column(String(36), primary_key=True)
name = Column(String(100), unique=True, nullable=False)
code = Column(String(50), unique=True, nullable=False)
updatedAt = Column('updated_at', DateTime, default=datetime.now, onupdate=datetime.now)
createdAt = Column('created_at', DateTime, default=datetime.now)
class SectorQuote(Base):
"""板块行情"""
__tablename__ = "sector_quotes"
__table_args__ = (
Index('idx_sector_quotes_code', 'sector_code'),
Index('idx_sector_quotes_time', 'quote_time'),
)
id = Column(Integer, primary_key=True, autoincrement=True)
sectorCode = Column('sector_code', String(50), nullable=False)
current = Column(Float, default=0)
change = Column(Float, default=0)
changePercent = Column('change_percent', Float, default=0)
volume = Column(BigInteger, default=0)
turnover = Column(BigInteger, default=0)
momentumScore = Column('momentum_score', Float, default=50)
rank = Column(Integer, default=0)
previousRank = Column('previous_rank', Integer, default=0)
quoteTime = Column('quote_time', DateTime, default=datetime.now)
class Stock(Base):
"""股票信息"""
__tablename__ = "stocks"
__table_args__ = (
Index('idx_stocks_code', 'code'),
Index('idx_stocks_sector', 'sector_code'),
)
id = Column(String(36), primary_key=True)
code = Column(String(50), unique=True, nullable=False)
name = Column(String(100), nullable=False)
sectorCode = Column('sector_code', String(50), nullable=True)
marketCap = Column('market_cap', BigInteger, nullable=True)
pe = Column(Float, nullable=True)
pb = Column(Float, nullable=True)
updatedAt = Column('updated_at', DateTime, default=datetime.now, onupdate=datetime.now)
createdAt = Column('created_at', DateTime, default=datetime.now)
class StockQuote(Base):
"""股票行情"""
__tablename__ = "stock_quotes"
__table_args__ = (
Index('idx_stock_quotes_code', 'stock_code'),
Index('idx_stock_quotes_time', 'quote_time'),
)
id = Column(Integer, primary_key=True, autoincrement=True)
stockCode = Column('stock_code', String(50), nullable=False)
price = Column(Float, default=0)
open = Column(Float, default=0)
high = Column(Float, default=0)
low = Column(Float, default=0)
preClose = Column('pre_close', Float, default=0)
volume = Column(BigInteger, default=0)
turnover = Column(BigInteger, default=0)
changePercent = Column('change_percent', Float, default=0)
turnoverRate = Column('turnover_rate', Float, nullable=True)
amplitude = Column(Float, nullable=True)
quoteTime = Column('quote_time', DateTime, default=datetime.now)
class StockKLine(Base):
"""股票K线数据"""
__tablename__ = "stock_klines"
__table_args__ = (
UniqueConstraint('stock_code', 'period', 'date', name='uk_stock_klines'),
Index('idx_stock_klines_code', 'stock_code'),
Index('idx_stock_klines_date', 'date'),
)
id = Column(Integer, primary_key=True, autoincrement=True)
stockCode = Column('stock_code', String(50), nullable=False)
period = Column(String(20), nullable=False) # day/week/month
date = Column(DateTime, nullable=False)
open = Column(Float, default=0)
high = Column(Float, default=0)
low = Column(Float, default=0)
close = Column(Float, default=0)
volume = Column(BigInteger, default=0)
ma5 = Column(Float, nullable=True)
ma10 = Column(Float, nullable=True)
ma20 = Column(Float, nullable=True)
ma30 = Column(Float, nullable=True)
ma60 = Column(Float, nullable=True)
class SectorKLine(Base):
"""板块K线数据"""
__tablename__ = "sector_klines"
__table_args__ = (
UniqueConstraint('sector_code', 'period', 'date', name='uk_sector_klines'),
Index('idx_sector_klines_code', 'sector_code'),
Index('idx_sector_klines_date', 'date'),
)
id = Column(Integer, primary_key=True, autoincrement=True)
sectorCode = Column('sector_code', String(50), nullable=False)
period = Column(String(20), nullable=False)
date = Column(DateTime, nullable=False)
open = Column(Float, default=0)
high = Column(Float, default=0)
low = Column(Float, default=0)
close = Column(Float, default=0)
volume = Column(BigInteger, default=0)
class HighLowStock(Base):
"""新高新低股票记录"""
__tablename__ = "high_low_stocks"
__table_args__ = (
Index('idx_high_low_code', 'stock_code'),
Index('idx_high_low_type', 'type'),
Index('idx_high_low_date', 'date'),
)
id = Column(Integer, primary_key=True, autoincrement=True)
stockCode = Column('stock_code', String(50), nullable=False)
type = Column(String(10), nullable=False) # high/low
price = Column(Float, default=0)
date = Column(DateTime, nullable=False)
daysToHighLow = Column('days_to_highlow', Integer, default=0)
createdAt = Column('created_at', DateTime, default=datetime.now)
class MomentumStock(Base):
"""动量股票推荐"""
__tablename__ = "momentum_stocks"
__table_args__ = (
Index('idx_momentum_code', 'stock_code'),
Index('idx_momentum_date', 'date'),
)
id = Column(Integer, primary_key=True, autoincrement=True)
stockCode = Column('stock_code', String(50), nullable=False)
momentumScore = Column('momentum_score', Float, default=0)
tags = Column(Text, nullable=True) # JSON string
volumeRatio = Column('volume_ratio', Float, default=0)
breakThrough = Column('break_through', Boolean, default=False)
date = Column(DateTime, nullable=False)
createdAt = Column('created_at', DateTime, default=datetime.now)
# ==================== 数据库操作 ====================
@contextmanager
def get_db():
"""获取数据库会话上下文管理器"""
db = SessionLocal()
try:
yield db
db.commit()
except Exception as e:
db.rollback()
raise e
finally:
db.close()
def init_db():
"""初始化数据库表"""
Base.metadata.create_all(bind=engine)
print("数据库表初始化完成")
def check_connection() -> bool:
"""检查数据库连接"""
try:
with engine.connect() as conn:
conn.execute("SELECT 1")
return True
except Exception as e:
print(f"数据库连接失败: {e}")
return False
# ==================== 数据操作函数 ====================
def upsert_stock(db: Session, code: str, name: str, **kwargs):
"""插入或更新股票信息"""
stock = db.query(Stock).filter(Stock.code == code).first()
if stock:
stock.name = name
for key, value in kwargs.items():
if hasattr(stock, key):
setattr(stock, key, value)
stock.updatedAt = datetime.now()
else:
import uuid
stock = Stock(
id=str(uuid.uuid4()),
code=code,
name=name,
**kwargs
)
db.add(stock)
return stock
def upsert_stock_quote(db: Session, stock_code: str, **data):
"""插入股票行情"""
quote = StockQuote(
stockCode=stock_code,
**data
)
db.add(quote)
return quote
def upsert_stock_kline(db: Session, stock_code: str, period: str, date: datetime, **data):
"""插入或更新K线数据"""
kline = db.query(StockKLine).filter(
StockKLine.stockCode == stock_code,
StockKLine.period == period,
StockKLine.date == date
).first()
if kline:
for key, value in data.items():
if hasattr(kline, key):
setattr(kline, key, value)
else:
kline = StockKLine(
stockCode=stock_code,
period=period,
date=date,
**data
)
db.add(kline)
return kline
def upsert_sector(db: Session, code: str, name: str):
"""插入或更新板块信息"""
sector = db.query(Sector).filter(Sector.code == code).first()
if sector:
sector.name = name
sector.updatedAt = datetime.now()
else:
import uuid
sector = Sector(
id=str(uuid.uuid4()),
code=code,
name=name
)
db.add(sector)
return sector
def upsert_market_index(db: Session, code: str, name: str, **data):
"""插入或更新市场指数"""
index = db.query(MarketIndex).filter(MarketIndex.code == code).first()
if index:
index.name = name
for key, value in data.items():
if hasattr(index, key):
setattr(index, key, value)
else:
index = MarketIndex(
code=code,
name=name,
**data
)
db.add(index)
return index
if __name__ == "__main__":
# 测试数据库连接
if check_connection():
init_db()
print("数据库初始化成功")
else:
print("数据库连接失败,请检查配置")

@ -1,214 +0,0 @@
"""
AKShare HTTP API 服务
提供股票数据接口
"""
from fastapi import FastAPI, HTTPException, Query
from fastapi.middleware.cors import CORSMiddleware
import akshare as ak
import pandas as pd
from typing import Optional, List
import json
app = FastAPI(
title="AKShare HTTP API",
description="AKShare 数据接口 HTTP 服务",
version="1.0.0"
)
# 配置 CORS
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
def dataframe_to_records(df: pd.DataFrame) -> List[dict]:
"""将 DataFrame 转换为可 JSON 序列化的记录列表"""
if df is None or df.empty:
return []
# 处理 NaN 值
df = df.replace({pd.NaT: None})
df = df.where(pd.notnull(df), None)
return df.to_dict('records')
@app.get("/")
async def root():
"""健康检查"""
return {
"status": "healthy",
"service": "AKShare HTTP API",
"version": "1.0.0"
}
@app.get("/stock_zh_a_spot")
async def stock_zh_a_spot():
"""
获取 A 股实时行情数据
"""
try:
df = ak.stock_zh_a_spot_em()
records = dataframe_to_records(df)
# 字段映射,统一返回格式
mapped_records = []
for record in records:
mapped_records.append({
"code": record.get("代码"),
"name": record.get("名称"),
"price": record.get("最新价", 0),
"change": record.get("涨跌额", 0),
"change_percent": record.get("涨跌幅", 0),
"volume": record.get("成交量", 0),
"turnover": record.get("成交额", 0),
"open": record.get("开盘价", 0),
"high": record.get("最高价", 0),
"low": record.get("最低价", 0),
"pre_close": record.get("昨收", 0),
"turnover_rate": record.get("换手率", 0),
"amplitude": record.get("振幅", 0),
"market_cap": record.get("总市值", 0),
"pe": record.get("市盈率-动态", 0),
"pb": record.get("市净率", 0),
"industry": record.get("行业", ""),
})
return mapped_records
except Exception as e:
raise HTTPException(status_code=500, detail=f"获取数据失败: {str(e)}")
@app.get("/stock_zh_a_hist")
async def stock_zh_a_hist(
symbol: str = Query(..., description="股票代码,如 000001"),
period: str = Query("daily", description="周期: daily/weekly/monthly"),
start_date: Optional[str] = Query(None, description="开始日期 YYYYMMDD"),
end_date: Optional[str] = Query(None, description="结束日期 YYYYMMDD"),
adjust: str = Query("qfq", description="复权方式: qfq-前复权, hfq-后复权, 不复权")
):
"""
获取 A 股历史 K 线数据
"""
try:
# 转换周期参数
period_map = {
"daily": "daily",
"weekly": "weekly",
"monthly": "monthly"
}
ak_period = period_map.get(period, "daily")
# 转换复权参数
adjust_map = {
"qfq": "qfq",
"hfq": "hfq",
"": ""
}
ak_adjust = adjust_map.get(adjust, "qfq")
df = ak.stock_zh_a_hist(
symbol=symbol,
period=ak_period,
start_date=start_date or "19700101",
end_date=end_date or "20500101",
adjust=ak_adjust
)
records = dataframe_to_records(df)
# 字段映射
mapped_records = []
for record in records:
mapped_records.append({
"date": record.get("日期"),
"open": record.get("开盘", 0),
"high": record.get("最高", 0),
"low": record.get("最低", 0),
"close": record.get("收盘", 0),
"volume": record.get("成交量", 0),
"turnover": record.get("成交额", 0),
"amplitude": record.get("振幅", 0),
"change": record.get("涨跌幅", 0),
"change_amount": record.get("涨跌额", 0),
"turnover_rate": record.get("换手率", 0),
})
return mapped_records
except Exception as e:
raise HTTPException(status_code=500, detail=f"获取数据失败: {str(e)}")
@app.get("/stock_zh_index_spot")
async def stock_zh_index_spot():
"""
获取股票指数实时行情
"""
try:
df = ak.index_zh_a_spot_em()
records = dataframe_to_records(df)
mapped_records = []
for record in records:
mapped_records.append({
"code": record.get("代码"),
"name": record.get("名称"),
"price": record.get("最新价", 0),
"change": record.get("涨跌额", 0),
"change_percent": record.get("涨跌幅", 0),
"volume": record.get("成交量", 0),
"turnover": record.get("成交额", 0),
"open": record.get("开盘价", 0),
"high": record.get("最高价", 0),
"low": record.get("最低价", 0),
"pre_close": record.get("昨收", 0),
})
return mapped_records
except Exception as e:
raise HTTPException(status_code=500, detail=f"获取数据失败: {str(e)}")
@app.get("/stock_sector_spot")
async def stock_sector_spot():
"""
获取板块行情数据
"""
try:
df = ak.stock_board_industry_name_em()
records = dataframe_to_records(df)
mapped_records = []
for record in records:
mapped_records.append({
"code": record.get("代码"),
"name": record.get("名称"),
"change_percent": record.get("涨跌幅", 0),
})
return mapped_records
except Exception as e:
raise HTTPException(status_code=500, detail=f"获取数据失败: {str(e)}")
@app.get("/stock_sector_cons")
async def stock_sector_cons(
symbol: str = Query(..., description="板块名称")
):
"""
获取板块成分股
"""
try:
df = ak.stock_board_industry_cons_em(symbol=symbol)
records = dataframe_to_records(df)
mapped_records = []
for record in records:
mapped_records.append({
"code": record.get("代码"),
"name": record.get("名称"),
"price": record.get("最新价", 0),
"change_percent": record.get("涨跌幅", 0),
})
return mapped_records
except Exception as e:
raise HTTPException(status_code=500, detail=f"获取数据失败: {str(e)}")
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)

@ -1,349 +0,0 @@
"""
AKShare HTTP API 服务
提供股票数据接口和数据同步功能
"""
import os
from datetime import datetime
from typing import Optional, List
import akshare as ak
import pandas as pd
from fastapi import FastAPI, HTTPException, Query, BackgroundTasks
from fastapi.middleware.cors import CORSMiddleware
from database import check_connection, init_db, get_db, Stock, StockKLine
from data_sync import (
sync_all_stocks, sync_realtime_quotes, sync_stock_kline,
sync_sectors, sync_sector_quotes, sync_market_indices,
sync_all_klines, sync_all, sync_daily
)
app = FastAPI(
title="AKShare HTTP API",
description="AKShare 数据接口 HTTP 服务 - 支持数据同步到 MySQL",
version="2.0.0"
)
# 配置 CORS
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
def dataframe_to_records(df: pd.DataFrame) -> List[dict]:
"""将 DataFrame 转换为可 JSON 序列化的记录列表"""
if df is None or df.empty:
return []
df = df.replace({pd.NaT: None})
df = df.where(pd.notnull(df), None)
return df.to_dict('records')
# ==================== 健康检查 ====================
@app.get("/")
async def root():
"""健康检查"""
db_status = "connected" if check_connection() else "disconnected"
return {
"status": "healthy",
"service": "AKShare HTTP API",
"version": "2.0.0",
"database": db_status,
"timestamp": datetime.now().isoformat()
}
@app.get("/health")
async def health_check():
"""详细健康检查"""
db_ok = check_connection()
return {
"status": "healthy" if db_ok else "unhealthy",
"database": "connected" if db_ok else "disconnected",
"timestamp": datetime.now().isoformat()
}
# ==================== 原始 AKShare 接口 ====================
@app.get("/stock_zh_a_spot")
async def stock_zh_a_spot():
"""
获取 A 股实时行情数据
"""
try:
df = ak.stock_zh_a_spot_em()
records = dataframe_to_records(df)
mapped_records = []
for record in records:
mapped_records.append({
"code": record.get("代码"),
"name": record.get("名称"),
"price": record.get("最新价", 0),
"change": record.get("涨跌额", 0),
"change_percent": record.get("涨跌幅", 0),
"volume": record.get("成交量", 0),
"turnover": record.get("成交额", 0),
"open": record.get("开盘价", 0),
"high": record.get("最高价", 0),
"low": record.get("最低价", 0),
"pre_close": record.get("昨收", 0),
"turnover_rate": record.get("换手率", 0),
"amplitude": record.get("振幅", 0),
"market_cap": record.get("总市值", 0),
"pe": record.get("市盈率-动态", 0),
"pb": record.get("市净率", 0),
"industry": record.get("行业", ""),
})
return mapped_records
except Exception as e:
raise HTTPException(status_code=500, detail=f"获取数据失败: {str(e)}")
@app.get("/stock_zh_a_hist")
async def stock_zh_a_hist(
symbol: str = Query(..., description="股票代码,如 000001"),
period: str = Query("daily", description="周期: daily/weekly/monthly"),
start_date: Optional[str] = Query(None, description="开始日期 YYYYMMDD"),
end_date: Optional[str] = Query(None, description="结束日期 YYYYMMDD"),
adjust: str = Query("qfq", description="复权方式: qfq-前复权, hfq-后复权, 不复权")
):
"""
获取 A 股历史 K 线数据
"""
try:
period_map = {
"daily": "daily",
"weekly": "weekly",
"monthly": "monthly"
}
ak_period = period_map.get(period, "daily")
adjust_map = {
"qfq": "qfq",
"hfq": "hfq",
"": ""
}
ak_adjust = adjust_map.get(adjust, "qfq")
df = ak.stock_zh_a_hist(
symbol=symbol,
period=ak_period,
start_date=start_date or "19700101",
end_date=end_date or "20500101",
adjust=ak_adjust
)
records = dataframe_to_records(df)
mapped_records = []
for record in records:
mapped_records.append({
"date": record.get("日期"),
"open": record.get("开盘", 0),
"high": record.get("最高", 0),
"low": record.get("最低", 0),
"close": record.get("收盘", 0),
"volume": record.get("成交量", 0),
"turnover": record.get("成交额", 0),
"amplitude": record.get("振幅", 0),
"change": record.get("涨跌幅", 0),
"change_amount": record.get("涨跌额", 0),
"turnover_rate": record.get("换手率", 0),
})
return mapped_records
except Exception as e:
raise HTTPException(status_code=500, detail=f"获取数据失败: {str(e)}")
@app.get("/stock_zh_index_spot")
async def stock_zh_index_spot():
"""
获取股票指数实时行情
"""
try:
df = ak.index_zh_a_spot_em()
records = dataframe_to_records(df)
mapped_records = []
for record in records:
mapped_records.append({
"code": record.get("代码"),
"name": record.get("名称"),
"price": record.get("最新价", 0),
"change": record.get("涨跌额", 0),
"change_percent": record.get("涨跌幅", 0),
"volume": record.get("成交量", 0),
"turnover": record.get("成交额", 0),
"open": record.get("开盘价", 0),
"high": record.get("最高价", 0),
"low": record.get("最低价", 0),
"pre_close": record.get("昨收", 0),
})
return mapped_records
except Exception as e:
raise HTTPException(status_code=500, detail=f"获取数据失败: {str(e)}")
@app.get("/stock_sector_spot")
async def stock_sector_spot():
"""
获取板块行情数据
"""
try:
df = ak.stock_board_industry_name_em()
records = dataframe_to_records(df)
mapped_records = []
for record in records:
mapped_records.append({
"code": record.get("代码"),
"name": record.get("名称"),
"change_percent": record.get("涨跌幅", 0),
})
return mapped_records
except Exception as e:
raise HTTPException(status_code=500, detail=f"获取数据失败: {str(e)}")
# ==================== 数据库查询接口 ====================
@app.get("/db/stocks")
async def db_stocks(
limit: int = Query(100, description="限制条数"),
offset: int = Query(0, description="偏移量")
):
"""从数据库获取股票列表"""
try:
with get_db() as db:
stocks = db.query(Stock).offset(offset).limit(limit).all()
return [
{
"code": s.code,
"name": s.name,
"pe": s.pe,
"pb": s.pb,
"sector": s.sectorCode,
}
for s in stocks
]
except Exception as e:
raise HTTPException(status_code=500, detail=f"查询失败: {str(e)}")
@app.get("/db/klines/{symbol}")
async def db_klines(
symbol: str,
period: str = Query("day", description="周期: day/week/month"),
limit: int = Query(60, description="限制条数")
):
"""从数据库获取K线数据"""
try:
with get_db() as db:
klines = db.query(StockKLine).filter(
StockKLine.stockCode == symbol,
StockKLine.period == period
).order_by(StockKLine.date.desc()).limit(limit).all()
return [
{
"date": k.date.strftime("%Y-%m-%d"),
"open": k.open,
"high": k.high,
"low": k.low,
"close": k.close,
"volume": k.volume,
}
for k in reversed(klines)
]
except Exception as e:
raise HTTPException(status_code=500, detail=f"查询失败: {str(e)}")
# ==================== 数据同步接口 ====================
@app.post("/sync/stocks")
async def api_sync_stocks(background_tasks: BackgroundTasks):
"""同步股票列表(后台任务)"""
background_tasks.add_task(sync_all_stocks)
return {"message": "股票列表同步已启动", "status": "running"}
@app.post("/sync/quotes")
async def api_sync_quotes(background_tasks: BackgroundTasks):
"""同步实时行情(后台任务)"""
background_tasks.add_task(sync_realtime_quotes)
return {"message": "实时行情同步已启动", "status": "running"}
@app.post("/sync/klines")
async def api_sync_klines(
background_tasks: BackgroundTasks,
days: int = Query(365, description="同步天数"),
max_stocks: Optional[int] = Query(None, description="最大股票数量")
):
"""同步K线数据后台任务"""
background_tasks.add_task(sync_all_klines, days=days, max_stocks=max_stocks)
return {"message": f"K线同步已启动{days}天)", "status": "running"}
@app.post("/sync/all")
async def api_sync_all(
background_tasks: BackgroundTasks,
quick: bool = Query(False, description="快速模式(只同步少量数据)")
):
"""执行全量同步(后台任务)"""
background_tasks.add_task(sync_all, quick=quick)
return {"message": "全量同步已启动", "status": "running", "quick": quick}
@app.post("/sync/daily")
async def api_sync_daily(background_tasks: BackgroundTasks):
"""执行每日增量同步(后台任务)"""
background_tasks.add_task(sync_daily)
return {"message": "每日增量同步已启动", "status": "running"}
@app.get("/sync/status")
async def sync_status():
"""获取同步状态"""
db_ok = check_connection()
# 获取统计数据
stats = {}
try:
with get_db() as db:
from database import StockQuote, Sector, MarketIndex
stats["stocks"] = db.query(Stock).count()
stats["quotes"] = db.query(StockQuote).count()
stats["klines"] = db.query(StockKLine).count()
stats["sectors"] = db.query(Sector).count()
stats["indices"] = db.query(MarketIndex).count()
except Exception as e:
stats["error"] = str(e)
return {
"database_connected": db_ok,
"timestamp": datetime.now().isoformat(),
"stats": stats
}
# ==================== 初始化 ====================
@app.on_event("startup")
async def startup_event():
"""启动时初始化数据库"""
print("正在初始化数据库...")
if check_connection():
init_db()
print("数据库初始化完成")
else:
print("警告:数据库连接失败")
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)

@ -1,10 +0,0 @@
akshare>=1.14.0
fastapi>=0.100.0
uvicorn[standard]>=0.23.0
pandas>=2.0.0
numpy>=1.24.0
pymysql>=1.1.0
sqlalchemy>=2.0.0
cryptography>=41.0.0
python-dotenv>=1.0.0
schedule>=1.2.0

@ -1,20 +0,0 @@
"""
AKShare HTTP 服务启动脚本
Windows 用户双击运行或 python start.py 启动
"""
import uvicorn
import sys
if __name__ == "__main__":
print("Starting AKShare HTTP API Server...")
print("URL: http://localhost:8000")
print("Press Ctrl+C to stop")
print("-" * 50)
uvicorn.run(
"main:app",
host="0.0.0.0",
port=8000,
reload=False,
log_level="info"
)

@ -1,325 +0,0 @@
# Go 到 Python 迁移指南
本文档详细说明了从Go项目迁移到Python项目的对应关系。
## 技术栈对照表
| Go | Python | 说明 |
|----|--------|------|
| Gin | FastAPI | Web框架 |
| Gorilla WebSocket | FastAPI原生WebSocket | WebSocket支持 |
| database/sql + pq | SQLAlchemy + psycopg2 | 数据库访问 |
| encoding/json | Pydantic + json | JSON序列化 |
| os.Getenv | python-dotenv + pydantic-settings | 环境变量 |
| log | logging | 日志 |
| time | datetime | 时间处理 |
| sync.Mutex | threading.Lock/asyncio.Lock | 并发锁 |
| context.Context | 直接使用async/await | 上下文 |
## 项目结构对照表
```
Go Python
├── adapter/ app/adapters/
│ ├── adapter.go base.py
│ └── tushare/
│ ├── adapter.go tushare_adapter.py
│ └── client.go (集成到adapter)
├── api/
│ ├── types.go app/models/types.py
│ ├── router.go app/api/routes.py
│ ├── admin_types.go app/models/admin_types.py
│ └── admin_router.go app/api/admin_routes.py
├── cmd/
│ ├── server/main.go app/main.py
│ └── sync/main.go scripts/sync_data.py
├── internal/
│ ├── handler/
│ │ ├── handler.go (合并到routes)
│ │ └── admin.go (合并到admin_routes)
│ ├── service/
│ │ ├── service.go (拆分到各service)
│ │ ├── stock.go app/services/stock_service.py
│ │ ├── futures.go app/services/futures_service.py
│ │ ├── admin.go app/services/admin_service.py
│ │ ├── config.go app/services/config_service.py
│ │ ├── adapter.go app/services/adapter_service.py
│ │ └── test.go app/services/test_service.py
│ ├── repository/
│ │ ├── repository.go (合并)
│ │ ├── stock.go app/repositories/stock_repository.py
│ │ └── futures.go app/repositories/futures_repository.py
│ ├── model/model.go app/repositories/models.py
│ ├── websocket/server.go app/websocket/server.py
│ └── monitor/monitor.go app/monitor/monitor.py
├── pkg/
│ ├── config/config.go app/core/config.py
│ ├── logger/logger.go app/core/logger.py
│ └── errors/errors.go app/core/errors.py
└── config.json config.json (相同)
```
## 类型系统对照表
### 基础类型
| Go | Python |
|----|--------|
| `type Frequency string` | `class Frequency(str, Enum)` |
| `type AdjustType string` | `class AdjustType(str, Enum)` |
| `type AssetClass string` | `class AssetClass(str, Enum)` |
| `struct KLineItem` | `class KLineItem(BaseModel)` |
| `struct KLineData` | `class KLineData(BaseModel)` |
| `interface Handler` | 通过FastAPI依赖注入实现 |
### 类型转换示例
**Go:**
```go
type KLineItem struct {
Time time.Time `json:"time"`
Open float64 `json:"open"`
Volume int64 `json:"volume"`
}
```
**Python:**
```python
class KLineItem(BaseModel):
time: datetime = Field(..., description="时间戳")
open: float = Field(..., description="开盘价")
volume: int = Field(..., description="成交量")
class Config:
json_encoders = {
datetime: lambda v: v.isoformat()
}
```
## 接口路由对照表
### 股票接口
| Go路由 | Python路由 | 方法 |
|--------|------------|------|
| `stock.GET("/klines/:symbol", r.queryStockKLines)` | `@router.get("/stock/klines/{symbol}")` | GET |
| `stock.GET("/symbols", r.listStockSymbols)` | `@router.get("/stock/symbols")` | GET |
| `stock.POST("/klines/batch", r.batchQueryStockKLines)` | `@router.post("/stock/klines/batch")` | POST |
| `stock.GET("/trading-dates", r.getStockTradingDates)` | `@router.get("/stock/trading-dates")` | GET |
### 参数绑定对比
**Go (Gin):**
```go
func (r *Router) queryStockKLines(c *gin.Context) {
var req KLineQueryRequest
if err := c.ShouldBindQuery(&req); err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{...})
return
}
req.Symbol = c.Param("symbol")
// ...
}
```
**Python (FastAPI):**
```python
@router.get("/stock/klines/{symbol}")
def query_stock_klines(
symbol: str, # 路径参数
start: str = Query(...), # 查询参数
end: str = Query(...),
db: Session = Depends(get_db) # 依赖注入
):
service = StockService(db)
req = KLineQueryRequest(symbol=symbol, start=start, end=end)
data = service.query_klines(req)
return Response(code=0, message="success", data=data)
```
## 数据库访问对照表
### 查询K线数据
**Go:**
```go
query := fmt.Sprintf(`
SELECT ts, open, high, low, close, volume, amount
FROM %s
WHERE symbol_id = $1 AND ts >= $2 AND ts <= $3
ORDER BY ts ASC
`, tableName)
rows, err := r.db.QueryContext(ctx, query, symbol, start, end)
```
**Python (SQLAlchemy):**
```python
query = self.db.query(kline_model).filter(
kline_model.symbol_id == symbol,
kline_model.ts >= start,
kline_model.ts <= end
).order_by(kline_model.ts.asc())
results = query.all()
```
### 批量插入
**Go:**
```go
query := fmt.Sprintf(`
INSERT INTO %s (symbol_id, ts, open, ...)
VALUES %s
ON CONFLICT (symbol_id, ts) DO UPDATE SET...
`, tableName, strings.Join(valueStrs, ","))
_, err := r.db.ExecContext(ctx, query, args...)
```
**Python (SQLAlchemy):**
```python
for item in items:
existing = self.db.query(kline_model).filter(...).first()
if existing:
# 更新
existing.open = item.open
...
else:
# 插入
new_record = kline_model(...)
self.db.add(new_record)
self.db.commit()
```
## WebSocket对照表
### Go (Gorilla)
```go
type Hub struct {
clients map[*Client]bool
subscriptions map[string]map[*Client]bool
}
func (h *Hub) Run() {
for {
select {
case client := <-h.register:
h.clients[client] = true
// ...
}
}
}
```
### Python (FastAPI)
```python
class WebSocketManager:
def __init__(self):
self.clients: Dict[str, WSClient] = {}
self.subscriptions: Dict[str, Set[str]] = {}
self.lock = asyncio.Lock()
async def connect(self, websocket: WebSocket, client_id: str):
await websocket.accept()
async with self.lock:
self.clients[client_id] = WSClient(id=client_id, websocket=websocket)
```
## 配置管理对照表
### Go
```go
type Config struct {
Server ServerConfig `json:"server"`
Database DatabaseConfig `json:"database"`
}
func Load(path string) (*Config, error) {
data, err := os.ReadFile(path)
// ...
json.Unmarshal(data, &cfg)
return &cfg, nil
}
```
### Python
```python
class Config(BaseModel):
server: ServerConfig = Field(default_factory=ServerConfig)
database: DatabaseConfig = Field(default_factory=DatabaseConfig)
def load_config(config_path: str = "./config.json") -> Config:
with open(config_path, 'r') as f:
data = json.load(f)
return Config.model_validate(data)
```
## 启动方式对照表
### Go
```bash
go run ./cmd/server/main.go
```
### Python
```bash
# 方式1: 直接运行
python -m app.main
# 方式2: 使用uvicorn
uvicorn app.main:app --reload --port 8080
# 方式3: 生产环境
gunicorn app.main:app -w 4 -k uvicorn.workers.UvicornWorker
```
## 数据同步工具对照表
### Go
```bash
go run ./cmd/sync -type stocks
go run ./cmd/sync -type klines -symbol 000001.SZ -start 20240301 -end 20240307
```
### Python
```bash
python scripts/sync_data.py --type stocks
python scripts/sync_data.py --type klines --symbol 000001.SZ --start 20240301 --end 20240307
```
## 依赖管理对照表
### Go (go.mod)
```go
require (
github.com/gin-gonic/gin v1.9.1
github.com/gorilla/websocket v1.5.0
github.com/lib/pq v1.10.9
)
```
### Python (requirements.txt)
```
fastapi==0.115.0
uvicorn[standard]==0.32.0
sqlalchemy==2.0.36
psycopg2-binary==2.9.10
```
## 测试接口对照表
| 接口 | Go调用 | Python调用 |
|------|--------|------------|
| 健康检查 | `curl http://localhost:8080/v1/admin/health` | 相同 |
| 查询股票K线 | `curl "http://localhost:8080/v1/stock/klines/000001.SZ?start=20250301&end=20250307" -H "X-API-Key: key"` | 相同 |
| 批量查询 | `curl -X POST ... -d '{"symbols":["000001.SZ"],...}'` | 相同 |
所有API接口和响应格式与Go版本完全一致客户端无需任何修改即可切换到Python后端。

@ -1,237 +0,0 @@
# 统一行情数据服务 - Python实现
Python版本的统一行情数据服务所有接口和功能与Go版本保持一致。
## 特性
- **多周期K线支持**1m/5m/15m/30m/60m/1d/1w/1month
- **股票复权支持**:前复权(qfq)/后复权(hfq)
- **数据源热切换**支持Wind、Tushare等多个数据源动态切换
- **双轨设计**:股票和期货接口独立,数据存储隔离
- **WebSocket实时订阅**:支持实时行情推送
- **数据质量监控**:自动检测数据缺失并告警
- **交易日历**:支持查询股票和期货的交易日历
- **期货合约查询**:根据品种获取可交易合约列表
## 技术栈
- **语言**: Python 3.10+
- **Web框架**: FastAPI
- **WebSocket**: FastAPI原生WebSocket + python-socketio
- **数据库**: PostgreSQL 15+ (SQLAlchemy ORM)
- **数据源**: Tushare (首期支持)
## 项目结构
```
python_market_data_service/
├── app/
│ ├── __init__.py
│ ├── main.py # 主程序入口
│ ├── api/ # API路由
│ │ ├── __init__.py
│ │ ├── routes.py # 主要API路由
│ │ └── admin_routes.py # 管理后台路由
│ ├── core/ # 核心模块
│ │ ├── __init__.py
│ │ ├── config.py # 配置管理
│ │ ├── errors.py # 错误定义
│ │ └── logger.py # 日志工具
│ ├── models/ # 数据模型
│ │ ├── __init__.py
│ │ ├── types.py # 基础类型
│ │ └── admin_types.py # 管理后台类型
│ ├── repositories/ # 数据访问层
│ │ ├── __init__.py
│ │ ├── database.py # 数据库连接
│ │ ├── models.py # 数据库模型
│ │ ├── stock_repository.py
│ │ └── futures_repository.py
│ ├── services/ # 业务逻辑层
│ │ ├── __init__.py
│ │ ├── stock_service.py
│ │ ├── futures_service.py
│ │ ├── admin_service.py
│ │ ├── config_service.py
│ │ ├── adapter_service.py
│ │ └── test_service.py
│ ├── adapters/ # 数据源适配器
│ │ ├── __init__.py
│ │ ├── base.py # 适配器基类
│ │ └── tushare_adapter.py
│ └── websocket/ # WebSocket服务
│ ├── __init__.py
│ └── server.py
├── scripts/
│ └── sync_data.py # 数据同步工具
├── tests/ # 测试文件
├── requirements.txt # 依赖列表
├── pyproject.toml # 项目配置
└── README.md # 本文件
```
## 快速开始
### 1. 环境准备
- Python 3.10+
- PostgreSQL 15+
- Tushare Token (从 [Tushare官网](https://tushare.pro) 获取)
### 2. 安装依赖
```bash
# 创建虚拟环境
python -m venv venv
# 激活虚拟环境
# Windows:
venv\Scripts\activate
# Linux/Mac:
source venv/bin/activate
# 安装依赖
pip install -r requirements.txt
# 安装Tushare需单独安装
pip install tushare
```
### 3. 配置环境变量
```bash
# Windows PowerShell
$env:TUSHARE_TOKEN="your_tushare_token"
$env:DATABASE_URL="postgresql://user:password@localhost:5432/marketdata"
# Linux/Mac
export TUSHARE_TOKEN="your_tushare_token"
export DATABASE_URL="postgresql://user:password@localhost:5432/marketdata"
```
### 4. 初始化数据库
```bash
# 创建数据库使用psql或pgAdmin
createdb marketdata
# 启动服务时会自动创建表结构
```
### 5. 启动服务
```bash
# 开发模式
python -m app.main
# 或使用uvicorn
uvicorn app.main:app --reload --port 8080
```
服务将启动在 `http://localhost:8080`
- API文档: `http://localhost:8080/docs`
- 管理后台: `http://localhost:8080/admin`
### 6. 同步基础数据
```bash
# 同步股票列表
python scripts/sync_data.py --type stocks
# 同步期货列表
python scripts/sync_data.py --type futures
# 同步交易日历
python scripts/sync_data.py --type calendar --start 20240101 --end 20241231
# 同步K线数据
python scripts/sync_data.py --type klines --symbol 000001.SZ --start 20240301 --end 20240307 --freq 1d
```
## API接口
### 股票接口
| 接口 | 方法 | 说明 |
|------|------|------|
| `/v1/stock/klines/:symbol` | GET | 查询K线数据 |
| `/v1/stock/symbols` | GET | 查询标的列表 |
| `/v1/stock/klines/batch` | POST | 批量查询K线 |
| `/v1/stock/trading-dates` | GET | 获取交易日历 |
### 期货接口
| 接口 | 方法 | 说明 |
|------|------|------|
| `/v1/futures/klines/:symbol` | GET | 查询K线数据 |
| `/v1/futures/symbols` | GET | 查询标的列表 |
| `/v1/futures/klines/batch` | POST | 批量查询K线 |
| `/v1/futures/continuous/:underlying` | GET | 查询主力连续合约(预留) |
| `/v1/futures/trading-dates` | GET | 获取交易日历 |
| `/v1/futures/contracts` | GET | 获取品种合约列表 |
### 管理接口
| 接口 | 方法 | 说明 |
|------|------|------|
| `/v1/admin/source/status` | GET | 获取数据源状态 |
| `/v1/admin/source/switch` | POST | 切换数据源 |
| `/v1/admin/backfill` | POST | 历史数据补录 |
| `/v1/admin/health` | GET | 健康检查 |
### 管理后台
服务启动后,访问 `http://localhost:8080/admin` 进入管理后台。
### WebSocket实时订阅
**连接地址**: `ws://localhost:8080/v1/stream`
**认证**: 连接时在Header中传递 `X-API-Key`
**客户端消息**:
```json
// 订阅
{
"action": "subscribe",
"symbols": ["000001.SZ", "CU2504.SHFE"]
}
// 取消订阅
{
"action": "unsubscribe",
"symbols": ["000001.SZ"]
}
```
**服务器消息**:
```json
// 订阅确认
{
"type": "ack",
"action": "subscribe",
"symbols": ["000001.SZ", "CU2504.SHFE"],
"ts": "2025-03-07T12:30:00Z"
}
// 心跳
{
"type": "heartbeat",
"ts": "2025-03-07T12:30:30Z"
}
```
**限制**: 单连接最大订阅100个标的
## 与Go版本的主要区别
1. **Web框架**: Gin -> FastAPI
2. **ORM**: 原生SQL -> SQLAlchemy
3. **WebSocket**: Gorilla -> FastAPI原生
4. **配置**: 文件+环境变量 -> Pydantic Settings
5. **API文档**: 自动生成Swagger/ReDoc
## License
MIT

@ -1,2 +0,0 @@
"""Market Data Service - Python实现"""
__version__ = "1.0.0"

@ -1,13 +0,0 @@
"""数据源适配器模块"""
from .base import DataSourceAdapter, TickData, KLineData, SymbolInfo, TradeCalData, TickCallback
from .tushare_adapter import TushareAdapter
__all__ = [
"DataSourceAdapter",
"TickData",
"KLineData",
"SymbolInfo",
"TradeCalData",
"TickCallback",
"TushareAdapter",
]

@ -1,102 +0,0 @@
"""数据源适配器基类 - 对应Go的adapter/adapter.go"""
from abc import ABC, abstractmethod
from dataclasses import dataclass
from datetime import datetime
from typing import Callable, List, Optional
@dataclass
class TickData:
"""Tick数据"""
symbol: str
price: float
volume: int
time: int # Unix时间戳
@dataclass
class KLineData:
"""K线数据"""
symbol: str
time: int # Unix时间戳
open: float
high: float
low: float
close: float
volume: int
amount: float
open_interest: int = 0
@dataclass
class SymbolInfo:
"""标的信息"""
symbol_id: str
name: str
exchange: str
underlying: str = "" # 期货品种代码
contract_month: str = ""
list_date: str = ""
delist_date: str = ""
@dataclass
class TradeCalData:
"""交易日历数据"""
date: datetime
is_trading_day: bool
has_night_session: bool = False
# Tick数据回调类型
TickCallback = Callable[[str, TickData], None]
class DataSourceAdapter(ABC):
"""数据源适配器接口"""
@abstractmethod
async def connect(self, config: dict) -> None:
"""建立连接"""
pass
@abstractmethod
async def subscribe_ticks(self, symbols: List[str], callback: TickCallback) -> None:
"""订阅实时Tick"""
pass
@abstractmethod
async def fetch_klines(
self,
symbol: str,
start: str,
end: str,
freq: str
) -> List[KLineData]:
"""拉取历史K线"""
pass
@abstractmethod
async def fetch_symbols(self, asset_type: str) -> List[SymbolInfo]:
"""获取标的列表"""
pass
@abstractmethod
async def fetch_trading_calendar(
self,
exchange: str,
start: str,
end: str
) -> List[TradeCalData]:
"""获取交易日历"""
pass
@abstractmethod
async def health_check(self) -> bool:
"""健康检查"""
pass
@abstractmethod
async def close(self) -> None:
"""关闭连接"""
pass

@ -1,372 +0,0 @@
"""Tushare数据源适配器 - 对应Go的adapter/tushare/adapter.go"""
import asyncio
from datetime import datetime
from typing import List, Optional
import tushare as ts
import pandas as pd
from app.adapters.base import (
DataSourceAdapter, TickData, KLineData, SymbolInfo,
TradeCalData, TickCallback
)
from app.core.logger import info, error
class TushareAdapter(DataSourceAdapter):
"""Tushare数据源适配器"""
def __init__(self):
self.pro = None
self.token = None
self.config = {}
async def connect(self, config: dict) -> None:
"""建立连接"""
self.token = config.get("token")
if not self.token:
raise ValueError("Tushare token is required")
# 设置Tushare token
ts.set_token(self.token)
self.pro = ts.pro_api()
self.config = config
info("Tushare adapter connected")
async def subscribe_ticks(self, symbols: List[str], callback: TickCallback) -> None:
"""订阅实时TickTushare不支持实时推送"""
raise NotImplementedError("Tushare does not support real-time tick subscription")
async def fetch_klines(
self,
symbol: str,
start: str,
end: str,
freq: str
) -> List[KLineData]:
"""拉取历史K线"""
# 判断是股票还是期货
if ".SH" in symbol or ".SZ" in symbol or ".BJ" in symbol:
return await self._fetch_stock_klines(symbol, start, end, freq)
else:
return await self._fetch_futures_klines(symbol, start, end, freq)
async def _fetch_stock_klines(
self,
symbol: str,
start: str,
end: str,
freq: str
) -> List[KLineData]:
"""获取股票K线"""
# 转换日期格式: YYYYMMDD -> YYYYMMDD
start_date = start
end_date = end
if freq in ["1d", "", "day"]:
return await self._fetch_stock_daily(symbol, start_date, end_date)
elif freq in ["1m", "5m", "15m", "30m", "60m"]:
return await self._fetch_stock_minute(symbol, start_date, end_date, freq)
else:
raise ValueError(f"Unsupported frequency: {freq}")
async def _fetch_stock_daily(
self,
ts_code: str,
start_date: str,
end_date: str
) -> List[KLineData]:
"""获取股票日线"""
try:
# 使用线程池执行同步调用
loop = asyncio.get_event_loop()
df = await loop.run_in_executor(
None,
lambda: self.pro.daily(
ts_code=ts_code,
start_date=start_date,
end_date=end_date
)
)
if df is None or df.empty:
return []
results = []
for _, row in df.iterrows():
trade_date = datetime.strptime(str(row['trade_date']), "%Y%m%d")
results.append(KLineData(
symbol=row['ts_code'],
time=int(trade_date.timestamp()),
open=float(row['open']),
high=float(row['high']),
low=float(row['low']),
close=float(row['close']),
volume=int(row['vol'] * 100), # 手 -> 股
amount=float(row['amount'] * 1000) # 千元 -> 元
))
return results
except Exception as e:
error(f"Failed to fetch stock daily: {e}")
raise
async def _fetch_stock_minute(
self,
ts_code: str,
start_date: str,
end_date: str,
freq: str
) -> List[KLineData]:
"""获取股票分钟线"""
try:
freq_map = {
"1m": "1min",
"5m": "5min",
"15m": "15min",
"30m": "30min",
"60m": "60min"
}
ts_freq = freq_map.get(freq, "1min")
loop = asyncio.get_event_loop()
df = await loop.run_in_executor(
None,
lambda: ts.pro_bar(
ts_code=ts_code,
freq=ts_freq,
start_date=start_date,
end_date=end_date,
asset="E" # 股票
)
)
if df is None or df.empty:
return []
results = []
for _, row in df.iterrows():
trade_time = datetime.strptime(str(row['trade_time']), "%Y-%m-%d %H:%M:%S")
results.append(KLineData(
symbol=row['ts_code'],
time=int(trade_time.timestamp()),
open=float(row['open']),
high=float(row['high']),
low=float(row['low']),
close=float(row['close']),
volume=int(row['vol'] * 100),
amount=float(row['amount'] * 1000)
))
return results
except Exception as e:
error(f"Failed to fetch stock minute: {e}")
raise
async def _fetch_futures_klines(
self,
symbol: str,
start: str,
end: str,
freq: str
) -> List[KLineData]:
"""获取期货K线"""
if freq in ["1d", "", "day"]:
return await self._fetch_futures_daily(symbol, start, end)
elif freq in ["1m", "5m", "15m", "30m", "60m"]:
return await self._fetch_futures_minute(symbol, start, end, freq)
else:
raise ValueError(f"Unsupported frequency: {freq}")
async def _fetch_futures_daily(
self,
ts_code: str,
start_date: str,
end_date: str
) -> List[KLineData]:
"""获取期货日线"""
try:
loop = asyncio.get_event_loop()
df = await loop.run_in_executor(
None,
lambda: self.pro.fut_daily(
ts_code=ts_code,
start_date=start_date,
end_date=end_date
)
)
if df is None or df.empty:
return []
results = []
for _, row in df.iterrows():
trade_date = datetime.strptime(str(row['trade_date']), "%Y%m%d")
results.append(KLineData(
symbol=row['ts_code'],
time=int(trade_date.timestamp()),
open=float(row['open']),
high=float(row['high']),
low=float(row['low']),
close=float(row['close']),
volume=int(row['vol']),
amount=float(row['amount'] * 10000), # 万元 -> 元
open_interest=int(row.get('oi', 0))
))
return results
except Exception as e:
error(f"Failed to fetch futures daily: {e}")
raise
async def _fetch_futures_minute(
self,
ts_code: str,
start_date: str,
end_date: str,
freq: str
) -> List[KLineData]:
"""获取期货分钟线"""
# Tushare期货分钟线需要通过stk_mins接口但需要特殊权限
# 这里简化处理,实际使用时可能需要其他数据源
raise NotImplementedError("Futures minute data requires special Tushare permission")
async def fetch_symbols(self, asset_type: str) -> List[SymbolInfo]:
"""获取标的列表"""
if asset_type == "stock":
return await self._fetch_stock_symbols()
elif asset_type == "futures":
return await self._fetch_futures_symbols()
else:
raise ValueError(f"Unsupported asset type: {asset_type}")
async def _fetch_stock_symbols(self) -> List[SymbolInfo]:
"""获取股票列表"""
try:
loop = asyncio.get_event_loop()
df = await loop.run_in_executor(
None,
lambda: self.pro.stock_basic(
exchange="",
list_status="L" # 上市状态
)
)
if df is None or df.empty:
return []
results = []
for _, row in df.iterrows():
results.append(SymbolInfo(
symbol_id=row['ts_code'],
name=row['name'],
exchange=row['exchange'],
list_date=str(row.get('list_date', '')),
delist_date=str(row.get('delist_date', ''))
))
return results
except Exception as e:
error(f"Failed to fetch stock symbols: {e}")
raise
async def _fetch_futures_symbols(self) -> List[SymbolInfo]:
"""获取期货列表"""
try:
loop = asyncio.get_event_loop()
df = await loop.run_in_executor(
None,
lambda: self.pro.fut_basic(exchange="")
)
if df is None or df.empty:
return []
results = []
for _, row in df.iterrows():
results.append(SymbolInfo(
symbol_id=row['ts_code'],
name=row['name'],
exchange=row['exchange'],
underlying=row.get('fut_code', ''),
contract_month=str(row['symbol'])[len(str(row.get('fut_code', ''))):],
list_date=str(row.get('list_date', '')),
delist_date=str(row.get('delist_date', ''))
))
return results
except Exception as e:
error(f"Failed to fetch futures symbols: {e}")
raise
async def fetch_trading_calendar(
self,
exchange: str,
start: str,
end: str
) -> List[TradeCalData]:
"""获取交易日历"""
# Tushare交易所代码映射
exchange_map = {
"SH": "SSE",
"SZ": "SZSE",
"SHFE": "SHFE",
"DCE": "DCE",
"CZCE": "CZCE",
"CFFEX": "CFFEX",
"INE": "INE",
}
ts_exchange = exchange_map.get(exchange, "SSE")
try:
loop = asyncio.get_event_loop()
df = await loop.run_in_executor(
None,
lambda: self.pro.trade_cal(
exchange=ts_exchange,
start_date=start,
end_date=end
)
)
if df is None or df.empty:
return []
results = []
for _, row in df.iterrows():
cal_date = datetime.strptime(str(row['cal_date']), "%Y%m%d")
results.append(TradeCalData(
date=cal_date,
is_trading_day=row['is_open'] == 1
))
return results
except Exception as e:
error(f"Failed to fetch trading calendar: {e}")
raise
async def health_check(self) -> bool:
"""健康检查"""
try:
if self.pro is None:
return False
# 尝试获取交易日历作为健康检查
loop = asyncio.get_event_loop()
df = await loop.run_in_executor(
None,
lambda: self.pro.trade_cal(
exchange="SSE",
start_date=datetime.now().strftime("%Y%m%d"),
end_date=datetime.now().strftime("%Y%m%d")
)
)
return df is not None
except Exception as e:
error(f"Health check failed: {e}")
return False
async def close(self) -> None:
"""关闭连接Tushare是HTTP接口无需关闭"""
info("Tushare adapter closed")

@ -1,5 +0,0 @@
"""API路由模块"""
from .routes import router
from .admin_routes import admin_router
__all__ = ["router", "admin_router"]

@ -1,232 +0,0 @@
"""管理后台API路由 - 对应Go的api/admin_router.go"""
from fastapi import APIRouter, Depends, HTTPException, Header, Query
from typing import Optional
from app.models import (
Response, ConfigListRequest, ConfigUpdateRequest,
ReloadRequest, AdapterToggleRequest, AdapterConfigUpdateRequest,
APITestRequest, WSTestRequest, TestHistoryRequest
)
from app.services import ConfigService, AdapterService, TestService
from app.core.config import get_config
admin_router = APIRouter()
# 服务实例
config_service = ConfigService()
adapter_service = AdapterService()
test_service = TestService()
def verify_admin_token(x_admin_token: Optional[str] = Header(None)):
"""验证Admin Token"""
# TODO: 实现Token验证
return x_admin_token
# ============================================
# 系统管理接口
# ============================================
@admin_router.get("/admin/system/status", response_model=Response)
def get_system_status(
token: str = Depends(verify_admin_token)
):
"""获取系统状态"""
try:
data = config_service.get_system_status()
return Response(code=0, message="success", data=data)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@admin_router.post("/admin/system/reload", response_model=Response)
def reload_config(
req: Optional[ReloadRequest] = None,
token: str = Depends(verify_admin_token)
):
"""热加载配置"""
try:
if req is None:
req = ReloadRequest()
data = config_service.reload_config(req)
return Response(code=0, message="success", data=data)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@admin_router.post("/admin/system/restart", response_model=Response)
def restart_service(
token: str = Depends(verify_admin_token)
):
"""重启服务"""
# TODO: 实现服务重启逻辑
return Response(
code=0,
message="重启命令已发送",
data={"status": "restarting"}
)
# ============================================
# 配置管理接口
# ============================================
@admin_router.get("/admin/config", response_model=Response)
def get_config_list(
type: Optional[str] = Query(None, description="配置类型筛选"),
token: str = Depends(verify_admin_token)
):
"""获取配置列表"""
try:
from app.models import ConfigType
req = ConfigListRequest()
if type:
req.type = ConfigType(type)
data = config_service.get_config_list(req)
return Response(code=0, message="success", data=data)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@admin_router.put("/admin/config", response_model=Response)
def update_config(
req: ConfigUpdateRequest,
token: str = Depends(verify_admin_token)
):
"""更新配置"""
try:
data = config_service.update_config(req)
return Response(code=0, message="success", data=data)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@admin_router.post("/admin/config/reload", response_model=Response)
def reload_config_endpoint(
req: Optional[ReloadRequest] = None,
token: str = Depends(verify_admin_token)
):
"""热加载配置"""
return reload_config(req, token)
# ============================================
# 适配器管理接口
# ============================================
@admin_router.get("/admin/adapters", response_model=Response)
def get_adapter_list(
token: str = Depends(verify_admin_token)
):
"""获取适配器列表"""
try:
data = adapter_service.get_adapter_list()
return Response(code=0, message="success", data=data)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@admin_router.post("/admin/adapters/toggle", response_model=Response)
def toggle_adapter(
req: AdapterToggleRequest,
token: str = Depends(verify_admin_token)
):
"""切换适配器状态"""
try:
adapter_service.toggle_adapter(req)
return Response(code=0, message="success")
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@admin_router.put("/admin/adapters/config", response_model=Response)
def update_adapter_config(
req: AdapterConfigUpdateRequest,
token: str = Depends(verify_admin_token)
):
"""更新适配器配置"""
try:
adapter_service.update_adapter_config(req)
return Response(code=0, message="success")
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
# ============================================
# 测试管理接口
# ============================================
@admin_router.get("/admin/tests/api", response_model=Response)
def get_api_test_list(
token: str = Depends(verify_admin_token)
):
"""获取API测试列表"""
try:
data = test_service.get_api_test_list()
# 设置基础URL
config = get_config()
data.base_url = f"http://localhost:{config.server.port}"
return Response(code=0, message="success", data=data)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@admin_router.post("/admin/tests/api/run", response_model=Response)
async def run_api_test(
req: APITestRequest,
token: str = Depends(verify_admin_token)
):
"""执行API测试"""
try:
config = get_config()
base_url = f"http://localhost:{config.server.port}"
data = await test_service.run_api_test(base_url, req)
return Response(code=0, message="success", data=data)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@admin_router.get("/admin/tests/ws", response_model=Response)
def get_ws_test_list(
token: str = Depends(verify_admin_token)
):
"""获取WebSocket测试列表"""
try:
data = test_service.get_ws_test_list()
config = get_config()
data.ws_url = f"ws://localhost:{config.server.port}/v1/stream"
return Response(code=0, message="success", data=data)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@admin_router.post("/admin/tests/ws/run", response_model=Response)
async def run_ws_test(
req: WSTestRequest,
token: str = Depends(verify_admin_token)
):
"""执行WebSocket测试"""
try:
config = get_config()
ws_url = f"ws://localhost:{config.server.port}/v1/stream"
data = await test_service.run_ws_test(ws_url, req)
return Response(code=0, message="success", data=data)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@admin_router.get("/admin/tests/history", response_model=Response)
def get_test_history(
type: Optional[str] = Query(None, description="测试类型"),
limit: int = Query(default=20, ge=1, le=100, description="数量限制"),
token: str = Depends(verify_admin_token)
):
"""获取测试历史"""
try:
req = TestHistoryRequest(type=type, limit=limit)
data = test_service.get_test_history(req)
return Response(code=0, message="success", data=data)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))

@ -1,304 +0,0 @@
"""API路由 - 对应Go的api/router.go"""
from fastapi import APIRouter, Depends, HTTPException, Header, Query
from typing import Optional
from sqlalchemy.orm import Session
from app.repositories import get_db
from app.services import StockService, FuturesService, AdminService
from app.models import (
Response, ErrorResponse, HealthResponse,
KLineQueryRequest, SymbolListRequest, BatchKLineRequest,
TradingDatesRequest, FuturesContractsRequest,
SourceSwitchRequest, BackfillRequest
)
from app.core.config import get_config
router = APIRouter()
# 获取配置
config = get_config()
# 认证依赖
def verify_api_key(x_api_key: Optional[str] = Header(None)):
"""验证API Key"""
if not x_api_key:
raise HTTPException(status_code=401, detail="Missing API Key")
# TODO: 验证API Key有效性
return x_api_key
# ============================================
# 股票接口
# ============================================
@router.get("/stock/klines/{symbol}", response_model=Response)
def query_stock_klines(
symbol: str,
start: str = Query(..., description="开始日期 YYYYMMDD", min_length=8, max_length=8),
end: str = Query(..., description="结束日期 YYYYMMDD", min_length=8, max_length=8),
freq: str = Query(default="1d", description="周期"),
adjust: str = Query(default="", description="复权类型"),
db: Session = Depends(get_db),
api_key: str = Depends(verify_api_key)
):
"""查询股票K线"""
try:
service = StockService(db)
req = KLineQueryRequest(
symbol=symbol,
start=start,
end=end,
freq=freq,
adjust=adjust
)
data = service.query_klines(req)
return Response(code=0, message="success", data=data)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/stock/symbols", response_model=Response)
def list_stock_symbols(
exchange: Optional[str] = Query(None, description="交易所筛选"),
keyword: Optional[str] = Query(None, description="关键词搜索"),
page: int = Query(default=1, ge=1, description="页码"),
size: int = Query(default=20, ge=1, le=100, description="每页数量"),
db: Session = Depends(get_db),
api_key: str = Depends(verify_api_key)
):
"""查询股票标的列表"""
try:
service = StockService(db)
req = SymbolListRequest(
exchange=exchange,
keyword=keyword,
page=page,
size=size
)
data = service.list_symbols(req)
return Response(code=0, message="success", data=data)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/stock/klines/batch", response_model=Response)
def batch_query_stock_klines(
req: BatchKLineRequest,
db: Session = Depends(get_db),
api_key: str = Depends(verify_api_key)
):
"""批量查询股票K线"""
try:
service = StockService(db)
data = service.batch_query_klines(req)
return Response(code=0, message="success", data=data)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/stock/trading-dates", response_model=Response)
def get_stock_trading_dates(
start: str = Query(..., description="开始日期 YYYYMMDD", min_length=8, max_length=8),
end: str = Query(..., description="结束日期 YYYYMMDD", min_length=8, max_length=8),
db: Session = Depends(get_db),
api_key: str = Depends(verify_api_key)
):
"""获取股票交易日历"""
try:
service = StockService(db)
req = TradingDatesRequest(start=start, end=end)
data = service.get_trading_dates(req)
return Response(code=0, message="success", data=data)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
# ============================================
# 期货接口
# ============================================
@router.get("/futures/klines/{symbol}", response_model=Response)
def query_futures_klines(
symbol: str,
start: str = Query(..., description="开始日期 YYYYMMDD", min_length=8, max_length=8),
end: str = Query(..., description="结束日期 YYYYMMDD", min_length=8, max_length=8),
freq: str = Query(default="1d", description="周期"),
db: Session = Depends(get_db),
api_key: str = Depends(verify_api_key)
):
"""查询期货K线"""
try:
service = FuturesService(db)
req = KLineQueryRequest(
symbol=symbol,
start=start,
end=end,
freq=freq
)
data = service.query_klines(req)
return Response(code=0, message="success", data=data)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/futures/symbols", response_model=Response)
def list_futures_symbols(
exchange: Optional[str] = Query(None, description="交易所筛选"),
underlying: Optional[str] = Query(None, description="品种筛选"),
keyword: Optional[str] = Query(None, description="关键词搜索"),
page: int = Query(default=1, ge=1, description="页码"),
size: int = Query(default=20, ge=1, le=100, description="每页数量"),
db: Session = Depends(get_db),
api_key: str = Depends(verify_api_key)
):
"""查询期货标的列表"""
try:
service = FuturesService(db)
req = SymbolListRequest(
exchange=exchange,
underlying=underlying,
keyword=keyword,
page=page,
size=size
)
data = service.list_symbols(req)
return Response(code=0, message="success", data=data)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/futures/klines/batch", response_model=Response)
def batch_query_futures_klines(
req: BatchKLineRequest,
db: Session = Depends(get_db),
api_key: str = Depends(verify_api_key)
):
"""批量查询期货K线"""
try:
service = FuturesService(db)
data = service.batch_query_klines(req)
return Response(code=0, message="success", data=data)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/futures/continuous/{underlying}", response_model=Response)
def query_continuous_klines(
underlying: str,
start: str = Query(..., description="开始日期 YYYYMMDD", min_length=8, max_length=8),
end: str = Query(..., description="结束日期 YYYYMMDD", min_length=8, max_length=8),
freq: str = Query(default="1d", description="周期"),
db: Session = Depends(get_db),
api_key: str = Depends(verify_api_key)
):
"""查询主力连续合约K线预留"""
# TODO: 实现主力连续合约查询
from app.models import KLineData
data = KLineData(
symbol=f"{underlying}.MAIN",
name=f"{underlying}主力连续",
freq=freq,
count=0,
items=[]
)
return Response(code=0, message="success", data=data)
@router.get("/futures/trading-dates", response_model=Response)
def get_futures_trading_dates(
start: str = Query(..., description="开始日期 YYYYMMDD", min_length=8, max_length=8),
end: str = Query(..., description="结束日期 YYYYMMDD", min_length=8, max_length=8),
db: Session = Depends(get_db),
api_key: str = Depends(verify_api_key)
):
"""获取期货交易日历"""
try:
service = FuturesService(db)
req = TradingDatesRequest(start=start, end=end)
data = service.get_trading_dates(req)
return Response(code=0, message="success", data=data)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/futures/contracts", response_model=Response)
def get_futures_contracts(
underlying: str = Query(..., description="品种代码"),
exchange: Optional[str] = Query(None, description="交易所筛选"),
db: Session = Depends(get_db),
api_key: str = Depends(verify_api_key)
):
"""获取品种合约列表"""
try:
service = FuturesService(db)
req = FuturesContractsRequest(underlying=underlying, exchange=exchange)
data = service.get_contracts_by_underlying(req)
return Response(code=0, message="success", data=data)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
# ============================================
# 管理接口
# ============================================
@router.get("/admin/source/status", response_model=Response)
def get_data_source_status(
db: Session = Depends(get_db),
api_key: str = Depends(verify_api_key)
):
"""获取数据源状态"""
try:
service = AdminService(db)
data = service.get_data_source_status()
return Response(code=0, message="success", data=data)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/admin/source/switch", response_model=Response)
def switch_data_source(
req: SourceSwitchRequest,
db: Session = Depends(get_db),
api_key: str = Depends(verify_api_key)
):
"""切换数据源"""
try:
service = AdminService(db)
service.switch_data_source(req)
return Response(code=0, message="数据源切换成功")
except Exception as e:
raise HTTPException(status_code=422, detail=str(e))
@router.post("/admin/backfill", response_model=Response)
def backfill_data(
req: BackfillRequest,
db: Session = Depends(get_db),
api_key: str = Depends(verify_api_key)
):
"""历史数据补录"""
try:
service = AdminService(db)
task_id = service.backfill_data(req)
return Response(
code=0,
message="补录任务已启动",
data={"task_id": task_id}
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/admin/health", response_model=HealthResponse)
def health_check(
db: Session = Depends(get_db)
):
"""健康检查(无需认证)"""
try:
service = AdminService(db)
return service.health_check()
except Exception as e:
raise HTTPException(status_code=503, detail=str(e))

@ -1,131 +0,0 @@
"""配置管理模块"""
import json
import os
from typing import Dict, Any, Optional
from functools import lru_cache
from pydantic import BaseModel, Field
from pydantic_settings import BaseSettings
class ServerConfig(BaseModel):
"""服务器配置"""
port: int = 8080
mode: str = "debug" # debug/release
api_key: str = "demo-api-key-2024"
class DatabaseConfig(BaseModel):
"""数据库配置"""
host: str = "localhost"
port: int = 5432
user: str = "postgres"
password: str = "postgres"
database: str = "marketdata"
@property
def database_url(self) -> str:
return f"postgresql://{self.user}:{self.password}@{self.host}:{self.port}/{self.database}"
class RedisConfig(BaseModel):
"""Redis配置"""
host: str = "localhost"
port: int = 6379
password: str = ""
db: int = 0
class SourceInfo(BaseModel):
"""数据源信息"""
type: str = "http"
config: Dict[str, str] = Field(default_factory=dict)
class SourceConfig(BaseModel):
"""源配置"""
active: str = "tushare"
list: Dict[str, SourceInfo] = Field(default_factory=dict)
class SourcesConfig(BaseModel):
"""数据源配置"""
stock: SourceConfig = Field(default_factory=lambda: SourceConfig(
active="tushare",
list={"tushare": SourceInfo(type="http", config={"base_url": "https://api.tushare.pro"})}
))
futures: SourceConfig = Field(default_factory=lambda: SourceConfig(
active="tushare",
list={"tushare": SourceInfo(type="http", config={"base_url": "https://api.tushare.pro"})}
))
class Config(BaseModel):
"""主配置类"""
server: ServerConfig = Field(default_factory=ServerConfig)
database: DatabaseConfig = Field(default_factory=DatabaseConfig)
redis: RedisConfig = Field(default_factory=RedisConfig)
sources: SourcesConfig = Field(default_factory=SourcesConfig)
class Config:
populate_by_name = True
class Settings(BaseSettings):
"""环境变量配置"""
port: int = Field(default=8080, alias="PORT")
database_url: Optional[str] = Field(default=None, alias="DATABASE_URL")
tushare_token: Optional[str] = Field(default=None, alias="TUSHARE_TOKEN")
api_key: Optional[str] = Field(default=None, alias="API_KEY")
class Config:
env_file = ".env"
case_sensitive = True
def load_config(config_path: str = "./config.json") -> Config:
"""从文件加载配置"""
if not os.path.exists(config_path):
return Config()
with open(config_path, 'r', encoding='utf-8') as f:
data = json.load(f)
return Config.model_validate(data)
def save_config(config: Config, config_path: str = "./config.json"):
"""保存配置到文件"""
os.makedirs(os.path.dirname(config_path) or '.', exist_ok=True)
with open(config_path, 'w', encoding='utf-8') as f:
json.dump(config.model_dump(), f, indent=2, ensure_ascii=False)
# 全局配置实例
_config: Optional[Config] = None
def get_config() -> Config:
"""获取当前配置"""
global _config
if _config is None:
_config = load_config()
return _config
def set_config(config: Config):
"""设置全局配置"""
global _config
_config = config
def reload_config(config_path: str = "./config.json") -> Config:
"""重新加载配置"""
global _config
_config = load_config(config_path)
return _config
@lru_cache()
def get_settings() -> Settings:
"""获取环境变量设置"""
return Settings()

@ -1,78 +0,0 @@
"""错误定义模块"""
from enum import IntEnum
from typing import Optional, Any, Dict
class ErrorCode(IntEnum):
"""错误码"""
OK = 0
BAD_REQUEST = 400
UNAUTHORIZED = 401
NOT_FOUND = 404
RATE_LIMIT = 429
INTERNAL = 500
class AppException(Exception):
"""应用异常基类"""
def __init__(
self,
message: str,
code: ErrorCode = ErrorCode.INTERNAL,
detail: Optional[str] = None
):
self.message = message
self.code = code
self.detail = detail
super().__init__(message)
def to_dict(self) -> Dict[str, Any]:
return {
"code": int(self.code),
"message": self.message,
"detail": self.detail
}
# 参数错误
class InvalidParamError(AppException):
def __init__(self, message: str = "参数错误", detail: Optional[str] = None):
super().__init__(message, ErrorCode.BAD_REQUEST, detail)
class InvalidSymbolError(AppException):
def __init__(self, message: str = "无效的标的代码"):
super().__init__(message, ErrorCode.BAD_REQUEST)
class InvalidDateError(AppException):
def __init__(self, message: str = "无效的日期格式"):
super().__init__(message, ErrorCode.BAD_REQUEST)
# 数据错误
class SymbolNotFoundError(AppException):
def __init__(self, message: str = "标的不存在"):
super().__init__(message, ErrorCode.NOT_FOUND)
class DataNotFoundError(AppException):
def __init__(self, message: str = "数据不存在"):
super().__init__(message, ErrorCode.NOT_FOUND)
class DataSourceUnavailableError(AppException):
def __init__(self, message: str = "数据源不可用"):
super().__init__(message, ErrorCode.INTERNAL)
# 权限错误
class UnauthorizedError(AppException):
def __init__(self, message: str = "未授权"):
super().__init__(message, ErrorCode.UNAUTHORIZED)
class RateLimitError(AppException):
def __init__(self, message: str = "请求过于频繁"):
super().__init__(message, ErrorCode.RATE_LIMIT)

@ -1,47 +0,0 @@
"""日志工具模块"""
import logging
import sys
from typing import Optional
def setup_logging(
level: int = logging.INFO,
format_string: Optional[str] = None
) -> logging.Logger:
"""设置日志配置"""
if format_string is None:
format_string = "%(asctime)s | %(levelname)-8s | %(message)s"
logging.basicConfig(
level=level,
format=format_string,
handlers=[
logging.StreamHandler(sys.stdout)
]
)
return logging.getLogger("market_data")
# 全局logger实例
logger = setup_logging()
def info(msg: str, *args, **kwargs):
"""信息日志"""
logger.info(msg, *args, **kwargs)
def error(msg: str, *args, **kwargs):
"""错误日志"""
logger.error(msg, *args, **kwargs)
def debug(msg: str, *args, **kwargs):
"""调试日志"""
logger.debug(msg, *args, **kwargs)
def warning(msg: str, *args, **kwargs):
"""警告日志"""
logger.warning(msg, *args, **kwargs)

@ -1,346 +0,0 @@
"""主应用入口 - 对应Go的cmd/server/main.go"""
import os
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from app.api import router, admin_router
from app.websocket import WebSocketServer
from app.core.config import get_config, get_settings
from app.core.logger import info, error, setup_logging
from app.repositories.database import init_db
# 获取配置
config = get_config()
settings = get_settings()
# 设置日志
setup_logging()
@asynccontextmanager
async def lifespan(app: FastAPI):
"""应用生命周期管理"""
# 启动时执行
info("Starting Market Data Service...")
# 初始化数据库
try:
init_db()
info("Database initialized")
except Exception as e:
error(f"Database initialization failed: {e}")
yield
# 关闭时执行
info("Shutting down Market Data Service...")
# 创建FastAPI应用
app = FastAPI(
title="统一行情数据服务",
description="提供股票和期货的标准化行情数据查询服务",
version="1.0.0",
lifespan=lifespan
)
# 添加CORS中间件
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# 注册API路由
app.include_router(router, prefix="/v1")
app.include_router(admin_router, prefix="/v1")
# WebSocket服务器
ws_server = WebSocketServer()
@app.websocket("/v1/stream")
async def websocket_endpoint(websocket):
"""WebSocket端点"""
import uuid
client_id = str(uuid.uuid4())
await ws_server.handle(websocket, client_id)
# 管理后台页面HTML简化版
ADMIN_HTML = """<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>行情数据服务 - 管理后台</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', sans-serif;
background: #f0f2f5;
color: #333;
}
.layout { display: flex; min-height: 100vh; }
.sidebar {
width: 200px;
background: #001529;
color: #fff;
position: fixed;
height: 100vh;
overflow-y: auto;
}
.logo {
padding: 16px;
font-size: 18px;
font-weight: bold;
border-bottom: 1px solid rgba(255,255,255,0.1);
}
.nav-menu { padding: 16px 0; }
.nav-item {
padding: 12px 24px;
cursor: pointer;
transition: all 0.3s;
display: flex;
align-items: center;
gap: 8px;
}
.nav-item:hover { background: rgba(255,255,255,0.05); }
.nav-item.active { background: #1890ff; }
.main-content {
flex: 1;
margin-left: 200px;
padding: 24px;
}
.header {
background: #fff;
padding: 16px 24px;
margin: -24px -24px 24px -24px;
box-shadow: 0 1px 4px rgba(0,0,0,0.1);
display: flex;
justify-content: space-between;
align-items: center;
}
.page-title { font-size: 20px; font-weight: 500; }
.card {
background: #fff;
border-radius: 8px;
padding: 24px;
margin-bottom: 24px;
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
}
.card-title {
font-size: 16px;
font-weight: 500;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid #f0f0f0;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.stat-card {
background: #fff;
border-radius: 8px;
padding: 20px;
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
}
.stat-value {
font-size: 32px;
font-weight: bold;
color: #1890ff;
}
.stat-label { color: #666; margin-top: 4px; }
.btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s;
}
.btn-primary { background: #1890ff; color: #fff; }
.btn-primary:hover { background: #40a9ff; }
.btn-success { background: #52c41a; color: #fff; }
.btn-danger { background: #ff4d4f; color: #fff; }
.hidden { display: none; }
.page { display: none; }
.page.active { display: block; }
</style>
</head>
<body>
<div class="layout">
<aside class="sidebar">
<div class="logo">📊 行情数据服务</div>
<nav class="nav-menu">
<div class="nav-item active" onclick="showPage('dashboard')">
<span>📈</span> 系统概览
</div>
<div class="nav-item" onclick="showPage('config')">
<span></span> 配置管理
</div>
<div class="nav-item" onclick="showPage('adapters')">
<span>🔌</span> 数据源适配
</div>
<div class="nav-item" onclick="showPage('tests')">
<span>🧪</span> 接口测试
</div>
</nav>
</aside>
<main class="main-content">
<div class="header">
<h1 class="page-title">系统概览</h1>
<div>
<button class="btn btn-success" onclick="reloadConfig()">🔄 热加载配置</button>
<button class="btn btn-danger" onclick="restartService()">🔁 重启服务</button>
</div>
</div>
<!-- 系统概览页面 -->
<div id="dashboard" class="page active">
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value" id="stat-status">运行中</div>
<div class="stat-label">运行状态</div>
</div>
<div class="stat-card">
<div class="stat-value" id="stat-uptime">-</div>
<div class="stat-label">运行时长</div>
</div>
<div class="stat-card">
<div class="stat-value">1.0.0</div>
<div class="stat-label">系统版本</div>
</div>
<div class="stat-card">
<div class="stat-value" id="stat-threads">-</div>
<div class="stat-label">线程数量</div>
</div>
</div>
<div class="card">
<div class="card-title">API文档</div>
<p>访问 <a href="/docs">/docs</a> 查看Swagger API文档</p>
<p>访问 <a href="/redoc">/redoc</a> 查看ReDoc API文档</p>
</div>
</div>
<!-- 配置管理页面 -->
<div id="config" class="page">
<div class="card">
<div class="card-title">配置管理</div>
<p>配置管理功能开发中...</p>
</div>
</div>
<!-- 数据源适配页面 -->
<div id="adapters" class="page">
<div class="card">
<div class="card-title">数据源适配</div>
<p>适配器管理功能开发中...</p>
</div>
</div>
<!-- 接口测试页面 -->
<div id="tests" class="page">
<div class="card">
<div class="card-title">接口测试</div>
<p>接口测试功能开发中...</p>
</div>
</div>
</main>
</div>
<script>
function showPage(pageName) {
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
document.getElementById(pageName).classList.add('active');
event.target.classList.add('active');
}
async function reloadConfig() {
try {
const response = await fetch('/v1/admin/system/reload', {
method: 'POST',
headers: {'Content-Type': 'application/json'}
});
const data = await response.json();
alert(data.message || '热加载完成');
} catch (e) {
alert('热加载失败: ' + e.message);
}
}
async function restartService() {
if (confirm('确定要重启服务吗?')) {
try {
const response = await fetch('/v1/admin/system/restart', {
method: 'POST',
headers: {'Content-Type': 'application/json'}
});
const data = await response.json();
alert(data.message || '重启命令已发送');
} catch (e) {
alert('重启失败: ' + e.message);
}
}
}
// 加载系统状态
async function loadSystemStatus() {
try {
const response = await fetch('/v1/admin/system/status');
const data = await response.json();
if (data.data) {
document.getElementById('stat-uptime').textContent = data.data.uptime;
document.getElementById('stat-threads').textContent = data.data.threads;
}
} catch (e) {
console.error('Failed to load system status:', e);
}
}
// 页面加载时获取状态
loadSystemStatus();
setInterval(loadSystemStatus, 30000); // 每30秒刷新
</script>
</body>
</html>
"""
@app.get("/admin", response_class=HTMLResponse)
async def admin_page():
"""管理后台页面"""
return ADMIN_HTML
@app.get("/")
async def root():
"""根路径重定向到管理后台"""
return {"message": "Market Data Service API", "docs": "/docs", "admin": "/admin"}
if __name__ == "__main__":
import uvicorn
# 从环境变量或配置获取端口
port = settings.port or config.server.port
uvicorn.run(
"app.main:app",
host="0.0.0.0",
port=port,
reload=config.server.mode == "debug"
)

@ -1,135 +0,0 @@
"""数据模型模块"""
from app.adapters.base import TradeCalData
from .types import (
Frequency,
AdjustType,
AssetClass,
SymbolType,
Exchange,
DataSourceStatus,
KLineItem,
KLineData,
KLineQueryRequest,
BatchKLineRequest,
BatchKLineResult,
BatchKLineData,
KLineSubData,
Symbol,
SymbolListRequest,
SymbolListData,
DataSourceInfo,
DataSourceStatusData,
SourceSwitchRequest,
BackfillRequest,
TradingDatesRequest,
TradingDatesData,
FuturesContractsRequest,
FuturesContractInfo,
FuturesContractsData,
Response,
ErrorResponse,
SuccessResponse,
HealthResponse,
)
from .admin_types import (
ConfigType,
ConfigItem,
ConfigSection,
ConfigListRequest,
ConfigListData,
ConfigUpdateRequest,
ConfigUpdateData,
AdapterInfo,
AdapterStatus,
AdapterListData,
AdapterToggleRequest,
AdapterConfigUpdateRequest,
SystemStatusData,
MemoryInfo,
RestartRequest,
ReloadRequest,
ReloadData,
APITestCase,
APITestCategory,
APITestListData,
APITestRequest,
APITestResult,
WSTestCase,
WSTestListData,
WSTestRequest,
WSTestResult,
WSMessage,
TestHistoryRequest,
TestHistoryData,
)
__all__ = [
# 数据适配器基础类型
"TradeCalData",
# 基础类型
"Frequency",
"AdjustType",
"AssetClass",
"SymbolType",
"Exchange",
"DataSourceStatus",
# K线数据
"KLineItem",
"KLineData",
"KLineQueryRequest",
"BatchKLineRequest",
"BatchKLineResult",
"BatchKLineData",
"KLineSubData",
# 标的
"Symbol",
"SymbolListRequest",
"SymbolListData",
# 数据源
"DataSourceInfo",
"DataSourceStatusData",
"SourceSwitchRequest",
"BackfillRequest",
# 交易日历
"TradingDatesRequest",
"TradingDatesData",
# 期货
"FuturesContractsRequest",
"FuturesContractInfo",
"FuturesContractsData",
# 响应
"Response",
"ErrorResponse",
"SuccessResponse",
"HealthResponse",
# 管理后台
"ConfigType",
"ConfigItem",
"ConfigSection",
"ConfigListRequest",
"ConfigListData",
"ConfigUpdateRequest",
"ConfigUpdateData",
"AdapterInfo",
"AdapterStatus",
"AdapterListData",
"AdapterToggleRequest",
"AdapterConfigUpdateRequest",
"SystemStatusData",
"MemoryInfo",
"RestartRequest",
"ReloadRequest",
"ReloadData",
"APITestCase",
"APITestCategory",
"APITestListData",
"APITestRequest",
"APITestResult",
"WSTestCase",
"WSTestListData",
"WSTestRequest",
"WSTestResult",
"WSMessage",
"TestHistoryRequest",
"TestHistoryData",
]

@ -1,250 +0,0 @@
"""管理后台类型定义 - 对应Go的api/admin_types.go"""
from datetime import datetime
from typing import List, Optional, Dict, Any, Literal
from enum import Enum
from pydantic import BaseModel, Field
# ============================================
# 配置管理类型
# ============================================
class ConfigType(str, Enum):
"""配置类型"""
SERVER = "server"
DATABASE = "database"
REDIS = "redis"
SOURCE = "source"
MONITOR = "monitor"
LOG = "log"
class ConfigItem(BaseModel):
"""配置项"""
key: str = Field(..., description="配置键")
value: Any = Field(..., description="配置值")
type: str = Field(..., description="值类型: string/int/bool/json")
description: str = Field(..., description="配置说明")
editable: bool = Field(default=True, description="是否可编辑")
required: bool = Field(default=True, description="是否必填")
class ConfigSection(BaseModel):
"""配置分组"""
name: str = Field(..., description="分组名称")
type: ConfigType = Field(..., description="分组类型")
description: str = Field(..., description="分组说明")
items: List[ConfigItem] = Field(default_factory=list, description="配置项列表")
class ConfigListRequest(BaseModel):
"""获取配置列表请求"""
type: Optional[ConfigType] = Field(None, description="配置类型筛选")
class ConfigListData(BaseModel):
"""配置列表响应"""
sections: List[ConfigSection] = Field(default_factory=list, description="配置分组列表")
version: str = Field(default="1.0.0", description="配置版本")
updated: datetime = Field(default_factory=datetime.now, description="最后更新时间")
class ConfigUpdateRequest(BaseModel):
"""更新配置请求"""
type: ConfigType = Field(..., description="配置类型")
items: Dict[str, Any] = Field(..., description="更新的配置项")
class ConfigUpdateData(BaseModel):
"""更新配置响应"""
success: bool = Field(..., description="是否成功")
need_restart: bool = Field(default=False, description="是否需要重启")
message: str = Field(..., description="提示信息")
# ============================================
# 适配器管理类型
# ============================================
class AdapterStatus(str, Enum):
"""适配器状态"""
ACTIVE = "active"
STANDBY = "standby"
DISABLED = "disabled"
ERROR = "error"
class AdapterInfo(BaseModel):
"""适配器信息"""
name: str = Field(..., description="适配器名称")
type: str = Field(..., description="适配器类型")
version: str = Field(..., description="版本")
description: str = Field(..., description="描述")
status: AdapterStatus = Field(..., description="状态")
config: Dict[str, str] = Field(default_factory=dict, description="当前配置")
last_error: Optional[str] = Field(None, description="最后错误")
updated_at: datetime = Field(default_factory=datetime.now, description="更新时间")
class AdapterListData(BaseModel):
"""适配器列表响应"""
adapters: List[AdapterInfo] = Field(default_factory=list, description="适配器列表")
class AdapterToggleRequest(BaseModel):
"""启用/禁用适配器请求"""
name: str = Field(..., description="适配器名称")
enable: bool = Field(..., description="是否启用")
class AdapterConfigUpdateRequest(BaseModel):
"""更新适配器配置请求"""
name: str = Field(..., description="适配器名称")
config: Dict[str, str] = Field(..., description="配置")
# ============================================
# 系统管理类型
# ============================================
class MemoryInfo(BaseModel):
"""内存信息"""
alloc: int = Field(..., description="已分配内存")
total_alloc: int = Field(..., description="累计分配")
sys: int = Field(..., description="系统内存")
num_gc: int = Field(..., description="GC次数")
class SystemStatusData(BaseModel):
"""系统状态数据"""
status: str = Field(..., description="系统状态")
version: str = Field(..., description="系统版本")
start_time: datetime = Field(..., description="启动时间")
uptime: str = Field(..., description="运行时长")
python_version: str = Field(..., description="Python版本")
memory: MemoryInfo = Field(..., description="内存使用")
threads: int = Field(..., description="线程数量")
class RestartRequest(BaseModel):
"""重启服务请求"""
force: bool = Field(default=False, description="是否强制重启")
class ReloadRequest(BaseModel):
"""热加载配置请求"""
config_type: Optional[ConfigType] = Field(None, description="指定配置类型")
class ReloadData(BaseModel):
"""热加载响应"""
success: bool = Field(..., description="是否成功")
message: str = Field(..., description="提示信息")
# ============================================
# 接口测试类型
# ============================================
class APITestCase(BaseModel):
"""接口测试用例"""
id: str = Field(..., description="用例ID")
name: str = Field(..., description="用例名称")
method: str = Field(..., description="HTTP方法")
path: str = Field(..., description="请求路径")
description: str = Field(..., description="描述")
params: Dict[str, str] = Field(default_factory=dict, description="默认参数")
body: Optional[Any] = Field(None, description="请求体")
class APITestCategory(BaseModel):
"""测试分类"""
name: str = Field(..., description="分类名称")
items: List[APITestCase] = Field(default_factory=list, description="测试用例")
class APITestListData(BaseModel):
"""接口测试列表响应"""
categories: List[APITestCategory] = Field(default_factory=list, description="分类列表")
base_url: str = Field(default="", description="基础URL")
class APITestRequest(BaseModel):
"""执行接口测试请求"""
id: str = Field(..., description="用例ID")
params: Dict[str, str] = Field(default_factory=dict, description="自定义参数")
body: Optional[Any] = Field(None, description="自定义请求体")
class APITestResult(BaseModel):
"""接口测试结果"""
id: int = Field(..., description="测试ID")
case_id: str = Field(..., description="用例ID")
name: str = Field(..., description="用例名称")
success: bool = Field(..., description="是否成功")
status_code: int = Field(0, description="HTTP状态码")
latency: int = Field(..., description="延迟(ms)")
request: Any = Field(None, description="请求信息")
response: Any = Field(None, description="响应信息")
error: Optional[str] = Field(None, description="错误信息")
timestamp: datetime = Field(default_factory=datetime.now, description="测试时间")
# ============================================
# WebSocket测试类型
# ============================================
class WSTestCase(BaseModel):
"""WebSocket测试用例"""
id: str = Field(..., description="用例ID")
name: str = Field(..., description="用例名称")
description: str = Field(..., description="描述")
action: str = Field(..., description="动作类型")
symbols: List[str] = Field(default_factory=list, description="订阅标的")
class WSTestListData(BaseModel):
"""WebSocket测试列表响应"""
cases: List[WSTestCase] = Field(default_factory=list, description="测试用例")
ws_url: str = Field(default="", description="WebSocket地址")
class WSTestRequest(BaseModel):
"""WebSocket测试请求"""
id: str = Field(..., description="用例ID")
symbols: List[str] = Field(default_factory=list, description="自定义标的")
class WSMessage(BaseModel):
"""WebSocket消息"""
type: str = Field(..., description="消息类型")
data: Any = Field(None, description="消息内容")
timestamp: datetime = Field(default_factory=datetime.now, description="时间")
class WSTestResult(BaseModel):
"""WebSocket测试结果"""
id: str = Field(..., description="测试ID")
case_id: str = Field(..., description="用例ID")
success: bool = Field(..., description="是否成功")
latency: int = Field(..., description="连接延迟(ms)")
messages: List[WSMessage] = Field(default_factory=list, description="收到的消息")
error: Optional[str] = Field(None, description="错误信息")
timestamp: datetime = Field(default_factory=datetime.now, description="测试时间")
# ============================================
# 测试历史记录类型
# ============================================
class TestHistoryRequest(BaseModel):
"""获取测试历史请求"""
type: Optional[str] = Field(None, description="测试类型: api/ws")
limit: int = Field(default=20, ge=1, le=100, description="数量限制")
class TestHistoryData(BaseModel):
"""测试历史数据"""
api_tests: List[APITestResult] = Field(default_factory=list, description="API测试历史")
ws_tests: List[WSTestResult] = Field(default_factory=list, description="WebSocket测试历史")

@ -1,306 +0,0 @@
"""基础类型定义 - 对应Go的api/types.go"""
from datetime import datetime
from typing import List, Optional, Literal, Any
from enum import Enum
from pydantic import BaseModel, Field
# ============================================
# 基础枚举定义
# ============================================
class Frequency(str, Enum):
"""K线周期"""
FREQ_1M = "1m"
FREQ_5M = "5m"
FREQ_15M = "15m"
FREQ_30M = "30m"
FREQ_60M = "60m"
FREQ_1D = "1d"
FREQ_1W = "1w"
FREQ_1MONTH = "1month"
class AdjustType(str, Enum):
"""复权类型"""
NONE = "" # 不复权
QFQ = "qfq" # 前复权
HFQ = "hfq" # 后复权
class AssetClass(str, Enum):
"""资产类别"""
STOCK = "stock"
FUTURES = "futures"
ALL = "all"
class SymbolType(str, Enum):
"""标的类型"""
STOCK = "stock"
FUTURES = "futures"
INDEX = "index"
ETF = "etf"
CONTINUOUS = "continuous"
class Exchange(str, Enum):
"""交易所"""
# 股票交易所
SZ = "SZ" # 深交所
SH = "SH" # 上交所
BJ = "BJ" # 北交所
# 期货交易所
CFFEX = "CFFEX" # 中金所
SHFE = "SHFE" # 上期所
DCE = "DCE" # 大商所
CZCE = "CZCE" # 郑商所
INE = "INE" # 上期能源
GFEX = "GFEX" # 广期所
class DataSourceStatus(str, Enum):
"""数据源状态"""
HEALTHY = "healthy"
DEGRADED = "degraded"
DOWN = "down"
# ============================================
# K线数据模型
# ============================================
class KLineItem(BaseModel):
"""单条K线数据"""
time: datetime = Field(..., description="时间戳")
open: float = Field(..., description="开盘价")
high: float = Field(..., description="最高价")
low: float = Field(..., description="最低价")
close: float = Field(..., description="收盘价")
volume: int = Field(..., description="成交量")
amount: float = Field(..., description="成交额")
# 期货特有字段
open_interest: Optional[int] = Field(None, description="持仓量")
settlement: Optional[float] = Field(None, description="结算价")
# 股票特有字段
adj_factor: Optional[float] = Field(None, description="复权系数")
class Config:
json_encoders = {
datetime: lambda v: v.isoformat()
}
class KLineData(BaseModel):
"""K线响应数据"""
symbol: str = Field(..., description="标的代码")
name: Optional[str] = Field(None, description="标的名称")
freq: Frequency = Field(..., description="周期")
adjust: AdjustType = Field(default=AdjustType.NONE, description="复权类型")
count: int = Field(..., description="数据条数")
items: List[KLineItem] = Field(default_factory=list, description="K线数据")
class KLineQueryRequest(BaseModel):
"""K线查询请求"""
symbol: str = Field(..., description="标的代码")
start: str = Field(..., description="开始日期 YYYYMMDD", min_length=8, max_length=8)
end: str = Field(..., description="结束日期 YYYYMMDD", min_length=8, max_length=8)
freq: Frequency = Field(default=Frequency.FREQ_1D, description="周期")
adjust: AdjustType = Field(default=AdjustType.NONE, description="复权类型")
class BatchKLineRequest(BaseModel):
"""批量K线查询请求"""
symbols: List[str] = Field(..., description="标的列表最多100只", max_length=100)
start: str = Field(..., description="开始日期 YYYYMMDD", min_length=8, max_length=8)
end: str = Field(..., description="结束日期 YYYYMMDD", min_length=8, max_length=8)
freq: Frequency = Field(default=Frequency.FREQ_1D, description="周期")
adjust: AdjustType = Field(default=AdjustType.NONE, description="复权类型")
class KLineSubData(BaseModel):
"""批量查询中的数据部分"""
count: int = Field(..., description="数据条数")
items: List[KLineItem] = Field(default_factory=list, description="K线数据")
class BatchKLineResult(BaseModel):
"""批量查询单条结果"""
symbol: str = Field(..., description="标的代码")
success: bool = Field(..., description="是否成功")
error: Optional[str] = Field(None, description="错误信息")
data: Optional[KLineSubData] = Field(None, description="数据")
class BatchKLineData(BaseModel):
"""批量K线响应数据"""
results: List[BatchKLineResult] = Field(default_factory=list, description="结果列表")
# ============================================
# 标的模型
# ============================================
class Symbol(BaseModel):
"""标的详细信息"""
symbol_id: str = Field(..., description="统一标的编码")
symbol_type: SymbolType = Field(..., description="标的类型")
exchange: Exchange = Field(..., description="交易所")
name: str = Field(..., description="名称")
name_en: Optional[str] = Field(None, description="英文名称")
list_date: Optional[datetime] = Field(None, description="上市日期")
delist_date: Optional[datetime] = Field(None, description="退市日期")
# 股票特有
industry: Optional[str] = Field(None, description="行业分类")
# 期货特有
underlying: Optional[str] = Field(None, description="标的资产")
contract_month: Optional[str] = Field(None, description="合约月份")
continuous_type: Optional[str] = Field(None, description="连续合约类型")
status: str = Field(..., description="active/suspended/delisted")
class SymbolListRequest(BaseModel):
"""标的列表请求"""
exchange: Optional[Exchange] = Field(None, description="交易所筛选")
keyword: Optional[str] = Field(None, description="关键词搜索")
underlying: Optional[str] = Field(None, description="品种筛选(期货)")
page: int = Field(default=1, ge=1, description="页码")
size: int = Field(default=20, ge=1, le=100, description="每页数量")
class SymbolListData(BaseModel):
"""标的列表响应数据"""
total: int = Field(..., description="总数")
page: int = Field(..., description="当前页")
size: int = Field(..., description="每页数量")
items: List[Symbol] = Field(default_factory=list, description="标的列表")
# ============================================
# 管理接口模型
# ============================================
class DataSourceInfo(BaseModel):
"""数据源信息"""
active_source: str = Field(..., description="当前激活源")
standby_sources: List[str] = Field(default_factory=list, description="待命源列表")
last_tick_ts: Optional[datetime] = Field(None, description="最后Tick时间")
status: DataSourceStatus = Field(..., description="数据源状态")
class DataSourceStatusData(BaseModel):
"""数据源状态响应"""
stock: DataSourceInfo = Field(default_factory=lambda: DataSourceInfo(
active_source="tushare",
status=DataSourceStatus.HEALTHY
))
futures: DataSourceInfo = Field(default_factory=lambda: DataSourceInfo(
active_source="tushare",
status=DataSourceStatus.HEALTHY
))
class SourceSwitchRequest(BaseModel):
"""数据源切换请求"""
asset_class: AssetClass = Field(..., description="资产类别")
source: str = Field(..., description="目标数据源")
sync_backfill: bool = Field(default=False, description="是否同步补录")
start_date: Optional[str] = Field(None, description="补录开始日期 YYYYMMDD")
class BackfillRequest(BaseModel):
"""历史数据补录请求"""
asset_class: AssetClass = Field(..., description="资产类别")
symbols: List[str] = Field(..., description="标的列表,空数组表示全部")
start: str = Field(..., description="开始日期 YYYYMMDD", min_length=8, max_length=8)
end: str = Field(..., description="结束日期 YYYYMMDD", min_length=8, max_length=8)
freqs: List[Frequency] = Field(..., description="需要补录的周期")
source: Optional[str] = Field(None, description="指定数据源")
# ============================================
# 交易日历模型
# ============================================
class TradingDatesRequest(BaseModel):
"""可交易日期查询请求"""
start: str = Field(..., description="开始日期 YYYYMMDD", min_length=8, max_length=8)
end: str = Field(..., description="结束日期 YYYYMMDD", min_length=8, max_length=8)
class TradingDatesData(BaseModel):
"""可交易日期响应数据"""
start: str = Field(..., description="查询开始日期")
end: str = Field(..., description="查询结束日期")
total_days: int = Field(..., description="总天数")
trading_days: int = Field(..., description="交易日数量")
trading_dates: List[str] = Field(default_factory=list, description="交易日列表")
# ============================================
# 期货合约模型
# ============================================
class FuturesContractsRequest(BaseModel):
"""获取期货合约请求"""
underlying: str = Field(..., description="品种代码,如 Cu, RB, M")
exchange: Optional[str] = Field(None, description="交易所筛选")
class FuturesContractInfo(BaseModel):
"""期货合约信息"""
symbol_id: str = Field(..., description="合约代码")
symbol_type: str = Field(default="futures", description="类型")
exchange: Exchange = Field(..., description="交易所")
name: str = Field(..., description="合约名称")
underlying: str = Field(..., description="品种代码")
contract_month: str = Field(..., description="合约月份")
list_date: Optional[datetime] = Field(None, description="上市日期")
delist_date: Optional[datetime] = Field(None, description="退市日期")
status: str = Field(..., description="active/expired")
class FuturesContractsData(BaseModel):
"""期货合约列表响应"""
underlying: str = Field(..., description="品种代码")
count: int = Field(..., description="合约数量")
items: List[FuturesContractInfo] = Field(default_factory=list, description="合约列表")
# ============================================
# 响应模型
# ============================================
class Response(BaseModel):
"""通用响应结构"""
code: int = Field(default=0, description="0表示成功非0表示错误")
message: str = Field(default="success", description="提示信息")
data: Optional[Any] = Field(None, description="响应数据")
class ErrorResponse(BaseModel):
"""错误响应"""
code: int = Field(..., description="错误码")
message: str = Field(..., description="错误信息")
detail: Optional[str] = Field(None, description="详细错误")
class SuccessResponse(BaseModel):
"""成功响应"""
code: int = Field(default=0)
message: str = Field(default="success")
data: Optional[Any] = Field(None)
class HealthResponse(BaseModel):
"""健康检查响应"""
status: str = Field(..., description="状态")
timestamp: datetime = Field(default_factory=datetime.now, description="时间戳")

@ -1,4 +0,0 @@
"""数据质量监控模块"""
from .monitor import DataQualityMonitor, AlertSender, LogAlertSender
__all__ = ["DataQualityMonitor", "AlertSender", "LogAlertSender"]

@ -1,255 +0,0 @@
"""数据质量监控 - 对应Go的internal/monitor/monitor.go"""
import asyncio
from abc import ABC, abstractmethod
from dataclasses import dataclass
from datetime import datetime, timedelta
from typing import List, Optional
from sqlalchemy.orm import Session
from sqlalchemy import text
from app.repositories import StockRepository, FuturesRepository
from app.models import Frequency
from app.core.logger import info, error
@dataclass
class CheckResult:
"""检查结果"""
symbol: str
freq: str
check_date: str
check_type: str
status: str # pass/fail
expect_count: int
actual_count: int
detail: str
@dataclass
class QualityReport:
"""数据质量报告"""
asset_type: str
check_date: str
total_checks: int
pass_count: int
fail_count: int
pass_rate: float
class AlertSender(ABC):
"""告警发送接口"""
@abstractmethod
def send_alert(self, title: str, content: str) -> bool:
"""发送告警"""
pass
class LogAlertSender(AlertSender):
"""日志告警发送器"""
def send_alert(self, title: str, content: str) -> bool:
info(f"[ALERT] {title}: {content}")
return True
class DataQualityMonitor:
"""数据质量监控"""
def __init__(
self,
db: Session,
stock_repo: StockRepository,
futures_repo: FuturesRepository,
sender: Optional[AlertSender] = None
):
self.db = db
self.stock_repo = stock_repo
self.futures_repo = futures_repo
self.sender = sender or LogAlertSender()
async def daily_check(self, check_date: str):
"""每日数据质量检查"""
info(f"Starting daily data quality check for {check_date}")
# 检查股票数据
try:
await self._check_stock_data(check_date)
except Exception as e:
error(f"Stock data check failed: {e}")
# 检查期货数据
try:
await self._check_futures_data(check_date)
except Exception as e:
error(f"Futures data check failed: {e}")
info("Daily data quality check completed")
async def _check_stock_data(self, check_date: str):
"""检查股票数据质量"""
# 获取所有活跃股票
from app.models import SymbolListRequest
symbols, _, _ = self.stock_repo.list_symbols(SymbolListRequest(page=1, size=10000))
# 检查1分钟线完整性股票应有240条
for symbol in symbols[:100]: # 限制检查数量
result = self._check_kline_completeness(
"stock", symbol.symbol_id, "1m", check_date, 240
)
self._save_check_result("stock", result)
async def _check_futures_data(self, check_date: str):
"""检查期货数据质量"""
from app.models import SymbolListRequest
symbols, _, _ = self.futures_repo.list_symbols(SymbolListRequest(page=1, size=10000))
# 检查1分钟线完整性
for symbol in symbols[:100]:
result = self._check_kline_completeness(
"futures", symbol.symbol_id, "1m", check_date, 240
)
self._save_check_result("futures", result)
def _check_kline_completeness(
self,
asset_type: str,
symbol: str,
freq: str,
check_date: str,
expect_count: int
) -> CheckResult:
"""检查K线完整性"""
result = CheckResult(
symbol=symbol,
freq=freq,
check_date=check_date,
check_type="missing",
status="pass",
expect_count=expect_count,
actual_count=0,
detail=""
)
try:
# 解析日期
start = datetime.strptime(check_date, "%Y%m%d")
end = start + timedelta(days=1) - timedelta(seconds=1)
# 查询数据
if asset_type == "stock":
items = self.stock_repo.get_klines(
symbol, Frequency(freq), start, end
)
else:
items = self.futures_repo.get_klines(
symbol, Frequency(freq), start, end
)
actual_count = len(items)
result.actual_count = actual_count
# 判断缺失情况
if actual_count < expect_count:
result.status = "fail"
result.detail = f"Data missing: expected {expect_count}, actual {actual_count}"
# 发送告警
if self.sender:
self.sender.send_alert(
f"[{asset_type}] Data Missing Alert",
f"Symbol: {symbol}, Date: {check_date}, Expected: {expect_count}, Actual: {actual_count}"
)
except Exception as e:
result.status = "fail"
result.detail = f"Error querying data: {e}"
return result
def _save_check_result(self, asset_type: str, result: CheckResult):
"""保存检查结果"""
try:
# 根据资产类型选择schema
schema = "stock" if asset_type == "stock" else "futures"
query = text(f"""
INSERT INTO {schema}.data_quality_checks
(check_date, symbol_id, freq, check_type, status, expect_count, actual_count, detail)
VALUES (:check_date, :symbol_id, :freq, :check_type, :status, :expect_count, :actual_count, :detail)
ON CONFLICT (check_date, symbol_id, freq, check_type) DO UPDATE SET
status = EXCLUDED.status,
expect_count = EXCLUDED.expect_count,
actual_count = EXCLUDED.actual_count,
detail = EXCLUDED.detail,
created_at = NOW()
""")
self.db.execute(query, {
"check_date": result.check_date,
"symbol_id": result.symbol,
"freq": result.freq,
"check_type": result.check_type,
"status": result.status,
"expect_count": result.expect_count,
"actual_count": result.actual_count,
"detail": result.detail
})
self.db.commit()
except Exception as e:
error(f"Failed to save check result: {e}")
def get_quality_report(self, asset_type: str, check_date: str) -> QualityReport:
"""获取数据质量报告"""
schema = "stock" if asset_type == "stock" else "futures"
query = text(f"""
SELECT
COUNT(*) as total_checks,
SUM(CASE WHEN status = 'pass' THEN 1 ELSE 0 END) as pass_count,
SUM(CASE WHEN status = 'fail' THEN 1 ELSE 0 END) as fail_count
FROM {schema}.data_quality_checks
WHERE check_date = :check_date
""")
result = self.db.execute(query, {"check_date": check_date}).fetchone()
total = result.total_checks or 0
pass_count = result.pass_count or 0
fail_count = result.fail_count or 0
pass_rate = (pass_count / total * 100) if total > 0 else 0
return QualityReport(
asset_type=asset_type,
check_date=check_date,
total_checks=total,
pass_count=pass_count,
fail_count=fail_count,
pass_rate=pass_rate
)
async def start_daily_check_cron(self):
"""启动每日检查定时任务"""
while True:
try:
# 计算到下一个盘后的时间假设15:35
now = datetime.now()
next_check = now.replace(hour=15, minute=35, second=0, microsecond=0)
if next_check <= now:
next_check = next_check + timedelta(days=1)
wait_seconds = (next_check - now).total_seconds()
info(f"Next daily check scheduled at {next_check}, waiting {wait_seconds} seconds")
await asyncio.sleep(wait_seconds)
# 执行检查
check_date = now.strftime("%Y%m%d")
await self.daily_check(check_date)
except Exception as e:
error(f"Daily check cron error: {e}")
await asyncio.sleep(3600) # 出错后等待1小时重试

@ -1,13 +0,0 @@
"""数据访问层模块"""
from .database import get_db, SessionLocal, engine, Base
from .stock_repository import StockRepository
from .futures_repository import FuturesRepository
__all__ = [
"get_db",
"SessionLocal",
"engine",
"Base",
"StockRepository",
"FuturesRepository",
]

@ -1,38 +0,0 @@
"""数据库连接管理"""
from typing import Generator
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, Session
from app.core.config import get_config
config = get_config()
# 创建数据库引擎
engine = create_engine(
config.database.database_url,
pool_pre_ping=True, # 自动检测断开的连接
pool_size=10,
max_overflow=20,
)
# 会话工厂
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
# 声明基类
Base = declarative_base()
def get_db() -> Generator[Session, None, None]:
"""获取数据库会话用于FastAPI依赖注入"""
db = SessionLocal()
try:
yield db
finally:
db.close()
def init_db():
"""初始化数据库(创建所有表)"""
Base.metadata.create_all(bind=engine)

@ -1,268 +0,0 @@
"""期货数据仓库"""
from datetime import datetime
from typing import List, Tuple, Optional
from sqlalchemy.orm import Session
from sqlalchemy import func, or_
from app.models import (
KLineItem, Symbol, SymbolListRequest, SymbolListData,
TradingDatesData, TradeCalData, Frequency,
FuturesContractsData, FuturesContractInfo
)
from app.repositories.models import (
FuturesSymbol, FuturesKLine1M, FuturesKLine1D,
FuturesTradingCalendar
)
class FuturesRepository:
"""期货数据仓库"""
def __init__(self, db: Session):
self.db = db
def get_klines(
self,
symbol: str,
freq: Frequency,
start: datetime,
end: datetime
) -> List[KLineItem]:
"""获取K线数据"""
kline_model = self._get_kline_model(freq)
query = self.db.query(kline_model).filter(
kline_model.symbol_id == symbol,
kline_model.ts >= start,
kline_model.ts <= end
).order_by(kline_model.ts.asc())
results = query.all()
items = []
for r in results:
item = KLineItem(
time=r.ts,
open=float(r.open),
high=float(r.high),
low=float(r.low),
close=float(r.close),
volume=r.volume,
amount=float(r.amount),
open_interest=r.open_interest
)
items.append(item)
return items
def _get_kline_model(self, freq: Frequency):
"""根据周期获取K线模型"""
mapping = {
Frequency.FREQ_1M: FuturesKLine1M,
Frequency.FREQ_1D: FuturesKLine1D,
}
return mapping.get(freq, FuturesKLine1D)
def save_klines(
self,
freq: Frequency,
symbol: str,
items: List[KLineItem]
) -> None:
"""保存K线数据"""
if not items:
return
kline_model = self._get_kline_model(freq)
for item in items:
existing = self.db.query(kline_model).filter(
kline_model.symbol_id == symbol,
kline_model.ts == item.time
).first()
if existing:
existing.open = item.open
existing.high = item.high
existing.low = item.low
existing.close = item.close
existing.volume = item.volume
existing.amount = item.amount
existing.open_interest = item.open_interest
else:
new_record = kline_model(
symbol_id=symbol,
ts=item.time,
open=item.open,
high=item.high,
low=item.low,
close=item.close,
volume=item.volume,
amount=item.amount,
open_interest=item.open_interest
)
self.db.add(new_record)
self.db.commit()
def list_symbols(
self,
req: SymbolListRequest
) -> Tuple[List[Symbol], int]:
"""查询标的列表"""
query = self.db.query(FuturesSymbol)
# 筛选条件
if req.exchange:
query = query.filter(FuturesSymbol.exchange == req.exchange.value)
if req.underlying:
query = query.filter(FuturesSymbol.underlying == req.underlying)
if req.keyword:
keyword = f"%{req.keyword}%"
query = query.filter(
or_(
FuturesSymbol.symbol_id.ilike(keyword),
FuturesSymbol.name.ilike(keyword)
)
)
# 查询总数
total = query.count()
# 分页查询
results = query.order_by(FuturesSymbol.symbol_id).offset(
(req.page - 1) * req.size
).limit(req.size).all()
symbols = []
for r in results:
s = Symbol(
symbol_id=r.symbol_id,
symbol_type=r.symbol_type,
exchange=r.exchange,
name=r.name,
underlying=r.underlying,
contract_month=r.contract_month,
list_date=r.list_date,
delist_date=r.delist_date,
status=r.status
)
symbols.append(s)
return symbols, total
def get_trading_dates(self, start: str, end: str) -> TradingDatesData:
"""获取交易日历"""
results = self.db.query(FuturesTradingCalendar).filter(
FuturesTradingCalendar.trade_date >= start,
FuturesTradingCalendar.trade_date <= end,
FuturesTradingCalendar.is_trading_day == True
).order_by(FuturesTradingCalendar.trade_date.asc()).all()
dates = [r.trade_date for r in results]
# 计算总天数
start_date = datetime.strptime(start, "%Y%m%d")
end_date = datetime.strptime(end, "%Y%m%d")
total_days = (end_date - start_date).days + 1
return TradingDatesData(
start=start,
end=end,
total_days=total_days,
trading_days=len(dates),
trading_dates=dates
)
def get_contracts_by_underlying(
self,
underlying: str,
exchange: Optional[str] = None
) -> FuturesContractsData:
"""根据品种获取合约"""
query = self.db.query(FuturesSymbol).filter(
FuturesSymbol.underlying == underlying,
FuturesSymbol.status == "active"
)
if exchange:
query = query.filter(FuturesSymbol.exchange == exchange)
results = query.order_by(FuturesSymbol.contract_month.asc()).all()
contracts = []
for r in results:
c = FuturesContractInfo(
symbol_id=r.symbol_id,
exchange=r.exchange,
name=r.name,
underlying=r.underlying,
contract_month=r.contract_month,
list_date=r.list_date,
delist_date=r.delist_date,
status=r.status
)
contracts.append(c)
return FuturesContractsData(
underlying=underlying,
count=len(contracts),
items=contracts
)
def save_symbols(self, symbols: List[Symbol]) -> None:
"""保存标的列表"""
for s in symbols:
existing = self.db.query(FuturesSymbol).filter(
FuturesSymbol.symbol_id == s.symbol_id
).first()
if existing:
existing.name = s.name
existing.underlying = s.underlying
existing.contract_month = s.contract_month
existing.list_date = s.list_date
existing.delist_date = s.delist_date
existing.status = s.status
else:
new_symbol = FuturesSymbol(
symbol_id=s.symbol_id,
symbol_type=s.symbol_type.value if s.symbol_type else "futures",
exchange=s.exchange.value if s.exchange else "",
name=s.name,
underlying=s.underlying or "",
contract_month=s.contract_month or "",
list_date=s.list_date,
delist_date=s.delist_date,
status=s.status
)
self.db.add(new_symbol)
self.db.commit()
def save_trading_calendar(self, dates: List[TradeCalData]) -> None:
"""保存交易日历"""
for d in dates:
date_str = d.date.strftime("%Y%m%d")
existing = self.db.query(FuturesTradingCalendar).filter(
FuturesTradingCalendar.trade_date == date_str
).first()
if existing:
existing.is_trading_day = d.is_trading_day
existing.has_night_session = d.has_night_session
existing.week_day = d.date.weekday() + 1
else:
new_cal = FuturesTradingCalendar(
trade_date=date_str,
is_trading_day=d.is_trading_day,
has_night_session=d.has_night_session,
week_day=d.date.weekday() + 1
)
self.db.add(new_cal)
self.db.commit()

@ -1,214 +0,0 @@
"""数据库模型定义"""
from datetime import datetime
from typing import Optional
from sqlalchemy import (
Column, Integer, String, Float, DateTime,
Boolean, Numeric, BigInteger, Index
)
from sqlalchemy.dialects.postgresql import ARRAY
from app.repositories.database import Base
# ============================================
# 股票相关表
# ============================================
class StockSymbol(Base):
"""股票标的表"""
__tablename__ = "symbols"
__table_args__ = {"schema": "stock"}
symbol_id = Column(String(20), primary_key=True, index=True, comment="标的代码")
symbol_type = Column(String(20), nullable=False, comment="标的类型")
exchange = Column(String(10), nullable=False, index=True, comment="交易所")
name = Column(String(100), nullable=False, comment="名称")
name_en = Column(String(100), nullable=True, comment="英文名称")
list_date = Column(DateTime, nullable=True, comment="上市日期")
delist_date = Column(DateTime, nullable=True, comment="退市日期")
industry = Column(String(50), nullable=True, comment="行业分类")
status = Column(String(20), nullable=False, default="active", comment="状态")
created_at = Column(DateTime, default=datetime.now, comment="创建时间")
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="更新时间")
class StockTradingCalendar(Base):
"""股票交易日历表"""
__tablename__ = "trading_calendar"
__table_args__ = {"schema": "stock"}
trade_date = Column(String(8), primary_key=True, comment="交易日期")
is_trading_day = Column(Boolean, nullable=False, comment="是否交易日")
week_day = Column(Integer, nullable=True, comment="星期几")
created_at = Column(DateTime, default=datetime.now, comment="创建时间")
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="更新时间")
class StockKLine1M(Base):
"""股票1分钟K线"""
__tablename__ = "klines_1m"
__table_args__ = (
Index("idx_stock_1m_symbol_ts", "symbol_id", "ts"),
{"schema": "stock"}
)
id = Column(BigInteger, primary_key=True, autoincrement=True)
symbol_id = Column(String(20), nullable=False, index=True, comment="标的代码")
ts = Column(DateTime, nullable=False, comment="时间戳")
open = Column(Numeric(18, 4), nullable=False, comment="开盘价")
high = Column(Numeric(18, 4), nullable=False, comment="最高价")
low = Column(Numeric(18, 4), nullable=False, comment="最低价")
close = Column(Numeric(18, 4), nullable=False, comment="收盘价")
volume = Column(BigInteger, nullable=False, comment="成交量")
amount = Column(Numeric(20, 4), nullable=False, comment="成交额")
created_at = Column(DateTime, default=datetime.now, comment="创建时间")
class StockKLine5M(Base):
"""股票5分钟K线"""
__tablename__ = "klines_5m"
__table_args__ = (
Index("idx_stock_5m_symbol_ts", "symbol_id", "ts"),
{"schema": "stock"}
)
id = Column(BigInteger, primary_key=True, autoincrement=True)
symbol_id = Column(String(20), nullable=False, index=True, comment="标的代码")
ts = Column(DateTime, nullable=False, comment="时间戳")
open = Column(Numeric(18, 4), nullable=False, comment="开盘价")
high = Column(Numeric(18, 4), nullable=False, comment="最高价")
low = Column(Numeric(18, 4), nullable=False, comment="最低价")
close = Column(Numeric(18, 4), nullable=False, comment="收盘价")
volume = Column(BigInteger, nullable=False, comment="成交量")
amount = Column(Numeric(20, 4), nullable=False, comment="成交额")
created_at = Column(DateTime, default=datetime.now, comment="创建时间")
class StockKLine1D(Base):
"""股票日线K线"""
__tablename__ = "klines_1d"
__table_args__ = (
Index("idx_stock_1d_symbol_ts", "symbol_id", "ts"),
{"schema": "stock"}
)
id = Column(BigInteger, primary_key=True, autoincrement=True)
symbol_id = Column(String(20), nullable=False, index=True, comment="标的代码")
ts = Column(DateTime, nullable=False, comment="时间戳")
open = Column(Numeric(18, 4), nullable=False, comment="开盘价")
high = Column(Numeric(18, 4), nullable=False, comment="最高价")
low = Column(Numeric(18, 4), nullable=False, comment="最低价")
close = Column(Numeric(18, 4), nullable=False, comment="收盘价")
volume = Column(BigInteger, nullable=False, comment="成交量")
amount = Column(Numeric(20, 4), nullable=False, comment="成交额")
created_at = Column(DateTime, default=datetime.now, comment="创建时间")
# ============================================
# 期货相关表
# ============================================
class FuturesSymbol(Base):
"""期货合约表"""
__tablename__ = "symbols"
__table_args__ = {"schema": "futures"}
symbol_id = Column(String(20), primary_key=True, index=True, comment="合约代码")
symbol_type = Column(String(20), nullable=False, comment="标的类型")
exchange = Column(String(10), nullable=False, index=True, comment="交易所")
name = Column(String(100), nullable=False, comment="名称")
underlying = Column(String(10), nullable=False, index=True, comment="品种代码")
contract_month = Column(String(6), nullable=False, comment="合约月份")
list_date = Column(DateTime, nullable=True, comment="上市日期")
delist_date = Column(DateTime, nullable=True, comment="退市日期")
status = Column(String(20), nullable=False, default="active", comment="状态")
created_at = Column(DateTime, default=datetime.now, comment="创建时间")
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="更新时间")
class FuturesTradingCalendar(Base):
"""期货交易日历表"""
__tablename__ = "trading_calendar"
__table_args__ = {"schema": "futures"}
trade_date = Column(String(8), primary_key=True, comment="交易日期")
is_trading_day = Column(Boolean, nullable=False, comment="是否交易日")
has_night_session = Column(Boolean, default=False, comment="是否有夜盘")
week_day = Column(Integer, nullable=True, comment="星期几")
created_at = Column(DateTime, default=datetime.now, comment="创建时间")
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="更新时间")
class FuturesKLine1M(Base):
"""期货1分钟K线"""
__tablename__ = "klines_1m"
__table_args__ = (
Index("idx_futures_1m_symbol_ts", "symbol_id", "ts"),
{"schema": "futures"}
)
id = Column(BigInteger, primary_key=True, autoincrement=True)
symbol_id = Column(String(20), nullable=False, index=True, comment="合约代码")
ts = Column(DateTime, nullable=False, comment="时间戳")
open = Column(Numeric(18, 4), nullable=False, comment="开盘价")
high = Column(Numeric(18, 4), nullable=False, comment="最高价")
low = Column(Numeric(18, 4), nullable=False, comment="最低价")
close = Column(Numeric(18, 4), nullable=False, comment="收盘价")
volume = Column(BigInteger, nullable=False, comment="成交量")
amount = Column(Numeric(20, 4), nullable=False, comment="成交额")
open_interest = Column(BigInteger, nullable=True, comment="持仓量")
created_at = Column(DateTime, default=datetime.now, comment="创建时间")
class FuturesKLine1D(Base):
"""期货日线K线"""
__tablename__ = "klines_1d"
__table_args__ = (
Index("idx_futures_1d_symbol_ts", "symbol_id", "ts"),
{"schema": "futures"}
)
id = Column(BigInteger, primary_key=True, autoincrement=True)
symbol_id = Column(String(20), nullable=False, index=True, comment="合约代码")
ts = Column(DateTime, nullable=False, comment="时间戳")
open = Column(Numeric(18, 4), nullable=False, comment="开盘价")
high = Column(Numeric(18, 4), nullable=False, comment="最高价")
low = Column(Numeric(18, 4), nullable=False, comment="最低价")
close = Column(Numeric(18, 4), nullable=False, comment="收盘价")
volume = Column(BigInteger, nullable=False, comment="成交量")
amount = Column(Numeric(20, 4), nullable=False, comment="成交额")
open_interest = Column(BigInteger, nullable=True, comment="持仓量")
created_at = Column(DateTime, default=datetime.now, comment="创建时间")
# ============================================
# 公共表
# ============================================
class DataSourceConfig(Base):
"""数据源配置表"""
__tablename__ = "data_source_config"
__table_args__ = {"schema": "public"}
asset_class = Column(String(20), primary_key=True, comment="资产类别")
active_source = Column(String(50), nullable=False, comment="当前激活源")
standby_sources = Column(ARRAY(String), nullable=True, comment="待命源列表")
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="更新时间")
class DataQualityCheck(Base):
"""数据质量检查表"""
__tablename__ = "data_quality_checks"
__table_args__ = {"schema": "stock"} # 也可以是futures
id = Column(BigInteger, primary_key=True, autoincrement=True)
check_date = Column(String(8), nullable=False, index=True, comment="检查日期")
symbol_id = Column(String(20), nullable=False, index=True, comment="标的代码")
freq = Column(String(10), nullable=False, comment="周期")
check_type = Column(String(20), nullable=False, comment="检查类型")
status = Column(String(10), nullable=False, comment="状态 pass/fail")
expect_count = Column(Integer, nullable=True, comment="期望数量")
actual_count = Column(Integer, nullable=True, comment="实际数量")
detail = Column(String(500), nullable=True, comment="详情")
created_at = Column(DateTime, default=datetime.now, comment="创建时间")

@ -1,222 +0,0 @@
"""股票数据仓库"""
from datetime import datetime, time
from typing import List, Tuple, Optional
from sqlalchemy.orm import Session
from sqlalchemy import func, or_
from app.models import (
KLineItem, Symbol, SymbolListRequest, SymbolListData,
TradingDatesData, TradeCalData, AdjustType, Frequency
)
from app.repositories.models import (
StockSymbol, StockKLine1M, StockKLine5M, StockKLine1D,
StockTradingCalendar
)
class StockRepository:
"""股票数据仓库"""
def __init__(self, db: Session):
self.db = db
def get_klines(
self,
symbol: str,
freq: Frequency,
start: datetime,
end: datetime,
adjust: AdjustType = AdjustType.NONE
) -> List[KLineItem]:
"""获取K线数据"""
# 根据周期选择表
kline_model = self._get_kline_model(freq)
query = self.db.query(kline_model).filter(
kline_model.symbol_id == symbol,
kline_model.ts >= start,
kline_model.ts <= end
).order_by(kline_model.ts.asc())
results = query.all()
items = []
for r in results:
item = KLineItem(
time=r.ts,
open=float(r.open),
high=float(r.high),
low=float(r.low),
close=float(r.close),
volume=r.volume,
amount=float(r.amount)
)
items.append(item)
return items
def _get_kline_model(self, freq: Frequency):
"""根据周期获取K线模型"""
mapping = {
Frequency.FREQ_1M: StockKLine1M,
Frequency.FREQ_5M: StockKLine5M,
Frequency.FREQ_1D: StockKLine1D,
}
return mapping.get(freq, StockKLine1D)
def save_klines(self, freq: Frequency, items: List[KLineItem]) -> None:
"""保存K线数据"""
if not items:
return
kline_model = self._get_kline_model(freq)
for item in items:
# 使用upsert逻辑
existing = self.db.query(kline_model).filter(
kline_model.symbol_id == getattr(item, 'symbol', ''),
kline_model.ts == item.time
).first()
if existing:
existing.open = item.open
existing.high = item.high
existing.low = item.low
existing.close = item.close
existing.volume = item.volume
existing.amount = item.amount
else:
new_record = kline_model(
symbol_id=getattr(item, 'symbol', ''),
ts=item.time,
open=item.open,
high=item.high,
low=item.low,
close=item.close,
volume=item.volume,
amount=item.amount
)
self.db.add(new_record)
self.db.commit()
def list_symbols(
self,
req: SymbolListRequest
) -> Tuple[List[Symbol], int]:
"""查询标的列表"""
query = self.db.query(StockSymbol)
# 筛选条件
if req.exchange:
query = query.filter(StockSymbol.exchange == req.exchange.value)
if req.keyword:
keyword = f"%{req.keyword}%"
query = query.filter(
or_(
StockSymbol.symbol_id.ilike(keyword),
StockSymbol.name.ilike(keyword)
)
)
# 查询总数
total = query.count()
# 分页查询
results = query.order_by(StockSymbol.symbol_id).offset(
(req.page - 1) * req.size
).limit(req.size).all()
symbols = []
for r in results:
s = Symbol(
symbol_id=r.symbol_id,
symbol_type=r.symbol_type,
exchange=r.exchange,
name=r.name,
name_en=r.name_en,
list_date=r.list_date,
delist_date=r.delist_date,
industry=r.industry,
status=r.status
)
symbols.append(s)
return symbols, total
def get_trading_dates(self, start: str, end: str) -> TradingDatesData:
"""获取交易日历"""
results = self.db.query(StockTradingCalendar).filter(
StockTradingCalendar.trade_date >= start,
StockTradingCalendar.trade_date <= end,
StockTradingCalendar.is_trading_day == True
).order_by(StockTradingCalendar.trade_date.asc()).all()
dates = [r.trade_date for r in results]
# 计算总天数
start_date = datetime.strptime(start, "%Y%m%d")
end_date = datetime.strptime(end, "%Y%m%d")
total_days = (end_date - start_date).days + 1
return TradingDatesData(
start=start,
end=end,
total_days=total_days,
trading_days=len(dates),
trading_dates=dates
)
def save_symbols(self, symbols: List[Symbol]) -> None:
"""保存标的列表"""
for s in symbols:
existing = self.db.query(StockSymbol).filter(
StockSymbol.symbol_id == s.symbol_id
).first()
if existing:
existing.name = s.name
existing.name_en = s.name_en
existing.list_date = s.list_date
existing.delist_date = s.delist_date
existing.industry = s.industry
existing.status = s.status
else:
new_symbol = StockSymbol(
symbol_id=s.symbol_id,
symbol_type=s.symbol_type.value if s.symbol_type else "stock",
exchange=s.exchange.value if s.exchange else "",
name=s.name,
name_en=s.name_en,
list_date=s.list_date,
delist_date=s.delist_date,
industry=s.industry,
status=s.status
)
self.db.add(new_symbol)
self.db.commit()
def save_trading_calendar(self, dates: List[TradeCalData]) -> None:
"""保存交易日历"""
for d in dates:
date_str = d.date.strftime("%Y%m%d")
existing = self.db.query(StockTradingCalendar).filter(
StockTradingCalendar.trade_date == date_str
).first()
if existing:
existing.is_trading_day = d.is_trading_day
existing.week_day = d.date.weekday() + 1
else:
new_cal = StockTradingCalendar(
trade_date=date_str,
is_trading_day=d.is_trading_day,
week_day=d.date.weekday() + 1
)
self.db.add(new_cal)
self.db.commit()

@ -1,16 +0,0 @@
"""业务服务层模块"""
from .stock_service import StockService
from .futures_service import FuturesService
from .admin_service import AdminService
from .config_service import ConfigService
from .adapter_service import AdapterService
from .test_service import TestService
__all__ = [
"StockService",
"FuturesService",
"AdminService",
"ConfigService",
"AdapterService",
"TestService",
]

@ -1,194 +0,0 @@
"""适配器管理服务 - 对应Go的internal/service/adapter.go"""
import asyncio
from datetime import datetime
from typing import Dict, List, Optional, Callable
from threading import RLock
from app.models import (
AdapterListData, AdapterInfo, AdapterStatus,
AdapterToggleRequest, AdapterConfigUpdateRequest
)
from app.adapters import DataSourceAdapter, TushareAdapter
from app.core.logger import info, error
class AdapterService:
"""适配器管理服务"""
def __init__(self):
self.lock = RLock()
# 已注册的适配器工厂
self.factories: Dict[str, Callable[[], DataSourceAdapter]] = {}
# 适配器配置
self.configs: Dict[str, dict] = {}
# 当前激活的适配器实例
self.active_adapters: Dict[str, DataSourceAdapter] = {}
# 适配器元数据
self.metadata: Dict[str, dict] = {}
# 注册内置适配器
self._register_builtin_adapters()
def _register_builtin_adapters(self):
"""注册内置适配器"""
# 注册Tushare适配器
self.register_adapter("tushare", lambda: TushareAdapter())
# 设置Tushare元数据
self.metadata["tushare"] = {
"name": "tushare",
"type": "http",
"version": "1.0.0",
"description": "Tushare Pro 金融数据接口",
"updated_at": datetime.now()
}
# 默认配置
self.configs["tushare"] = {
"enabled": True,
"config": {
"token": "",
"base_url": "https://api.tushare.pro"
}
}
# 预留Wind适配器
self.metadata["wind"] = {
"name": "wind",
"type": "ws",
"version": "1.0.0",
"description": "Wind 金融终端接口(预留)",
"updated_at": datetime.now()
}
self.configs["wind"] = {
"enabled": False,
"config": {
"host": "localhost",
"port": "8081"
}
}
def get_adapter_list(self) -> AdapterListData:
"""获取适配器列表"""
with self.lock:
adapters = []
for name, meta in self.metadata.items():
cfg = self.configs.get(name, {"enabled": False, "config": {}})
# 确定状态
if not cfg["enabled"]:
status = AdapterStatus.DISABLED
elif name in self.active_adapters:
status = AdapterStatus.ACTIVE
else:
status = AdapterStatus.STANDBY
adapters.append(AdapterInfo(
name=meta["name"],
type=meta["type"],
version=meta["version"],
description=meta["description"],
status=status,
config=cfg["config"],
updated_at=meta["updated_at"]
))
return AdapterListData(adapters=adapters)
def toggle_adapter(self, req: AdapterToggleRequest) -> None:
"""启用/禁用适配器"""
with self.lock:
if req.name not in self.configs:
raise ValueError(f"Adapter not found: {req.name}")
self.configs[req.name]["enabled"] = req.enable
# 如果禁用,关闭适配器连接
if not req.enable and req.name in self.active_adapters:
adapter = self.active_adapters.pop(req.name)
asyncio.create_task(adapter.close())
# 更新元数据
if req.name in self.metadata:
self.metadata[req.name]["updated_at"] = datetime.now()
def update_adapter_config(self, req: AdapterConfigUpdateRequest) -> None:
"""更新适配器配置"""
with self.lock:
if req.name not in self.configs:
raise ValueError(f"Adapter not found: {req.name}")
# 更新配置
self.configs[req.name]["config"].update(req.config)
# 如果适配器已激活,重新连接
if req.name in self.active_adapters:
adapter = self.active_adapters.pop(req.name)
asyncio.create_task(adapter.close())
# 如果启用状态,重新连接
if self.configs[req.name]["enabled"]:
asyncio.create_task(self._connect_adapter(req.name))
# 更新元数据
if req.name in self.metadata:
self.metadata[req.name]["updated_at"] = datetime.now()
def get_active_adapter(self, asset_class: str) -> Optional[DataSourceAdapter]:
"""获取当前激活的适配器"""
with self.lock:
# 根据资产类别获取配置(简化处理)
adapter_name = "tushare"
# 检查是否已有激活的实例
if adapter_name in self.active_adapters:
return self.active_adapters[adapter_name]
return None
def get_available_adapters(self) -> List[str]:
"""获取所有可用的适配器名称"""
with self.lock:
names = []
for name, meta in self.metadata.items():
if name in self.factories:
names.append(f"{name}|{meta['description']}")
return names
def register_adapter(self, name: str, factory: Callable[[], DataSourceAdapter]):
"""注册适配器"""
with self.lock:
self.factories[name] = factory
async def _connect_adapter(self, name: str):
"""连接适配器"""
with self.lock:
if name not in self.factories:
raise ValueError(f"Adapter factory not found: {name}")
if name not in self.configs:
raise ValueError(f"Adapter config not found: {name}")
factory = self.factories[name]
cfg = self.configs[name]
adapter = factory()
await adapter.connect(cfg["config"])
with self.lock:
self.active_adapters[name] = adapter
async def health_check(self, name: str) -> bool:
"""适配器健康检查"""
with self.lock:
if name not in self.active_adapters:
return False
adapter = self.active_adapters[name]
return await adapter.health_check()

@ -1,104 +0,0 @@
"""管理服务 - 对应Go的internal/service/admin.go"""
from datetime import datetime
from typing import Optional
import uuid
from sqlalchemy.orm import Session
from sqlalchemy import text
from app.models import (
DataSourceStatusData, DataSourceInfo, SourceSwitchRequest,
BackfillRequest, HealthResponse, DataSourceStatus
)
from app.core.logger import info
class AdminService:
"""管理服务"""
def __init__(self, db: Session):
self.db = db
def get_data_source_status(self) -> DataSourceStatusData:
"""获取数据源状态"""
try:
# 查询数据库中的数据源配置
result = self.db.execute(text("""
SELECT asset_class, active_source, standby_sources, updated_at
FROM public.data_source_config
"""))
data = DataSourceStatusData()
for row in result:
asset_class, active_source, standby_sources, updated_at = row
info_obj = DataSourceInfo(
active_source=active_source,
standby_sources=standby_sources or [],
status=DataSourceStatus.HEALTHY
)
if asset_class == "stock":
data.stock = info_obj
elif asset_class == "futures":
data.futures = info_obj
return data
except Exception as e:
info(f"Data source config not found, using defaults: {e}")
# 返回默认配置
return DataSourceStatusData()
def switch_data_source(self, req: SourceSwitchRequest) -> None:
"""切换数据源"""
asset_classes = []
if req.asset_class.value == "all":
asset_classes = ["stock", "futures"]
else:
asset_classes = [req.asset_class.value]
for ac in asset_classes:
self.db.execute(
text("""
INSERT INTO public.data_source_config
(asset_class, active_source, updated_at)
VALUES (:asset_class, :source, NOW())
ON CONFLICT (asset_class) DO UPDATE SET
active_source = EXCLUDED.active_source,
updated_at = EXCLUDED.updated_at
"""),
{"asset_class": ac, "source": req.source}
)
self.db.commit()
# 如果需要同步补录,启动后台任务
if req.sync_backfill:
info(f"Starting backfill for {req.asset_class} from {req.start_date}")
# TODO: 启动异步补录任务
def backfill_data(self, req: BackfillRequest) -> str:
"""历史数据补录"""
task_id = str(uuid.uuid4())
info(f"Starting backfill task {task_id} for {req.asset_class}")
# TODO: 将补录任务存入数据库启动后台Worker执行
return task_id
def health_check(self) -> HealthResponse:
"""健康检查"""
try:
# 检查数据库连接
self.db.execute(text("SELECT 1"))
return HealthResponse(
status="healthy",
timestamp=datetime.now()
)
except Exception as e:
return HealthResponse(
status=f"unhealthy: {str(e)}",
timestamp=datetime.now()
)

@ -1,332 +0,0 @@
"""配置管理服务 - 对应Go的internal/service/config.go"""
import platform
import psutil
import threading
from datetime import datetime, timedelta
from typing import Optional, List, Callable, Dict, Any
from app.models import (
ConfigListRequest, ConfigListData, ConfigSection, ConfigItem,
ConfigUpdateRequest, ConfigUpdateData, ConfigType,
ReloadRequest, ReloadData, SystemStatusData, MemoryInfo
)
from app.core.config import get_config, reload_config, save_config, Config
from app.core.logger import info
class ConfigService:
"""配置管理服务"""
def __init__(self):
self.config = get_config()
self.start_time = datetime.now()
self.version = "1.0.0"
self.callbacks: Dict[ConfigType, List[Callable]] = {}
self.lock = threading.RLock()
def get_config_list(self, req: ConfigListRequest) -> ConfigListData:
"""获取配置列表"""
sections = []
# 服务器配置
if not req.type or req.type == ConfigType.SERVER:
sections.append(ConfigSection(
name="服务器配置",
type=ConfigType.SERVER,
description="HTTP服务器相关配置",
items=[
ConfigItem(
key="port",
value=self.config.server.port,
type="int",
description="服务端口",
editable=True,
required=True
),
ConfigItem(
key="mode",
value=self.config.server.mode,
type="string",
description="运行模式: debug/release",
editable=True,
required=True
),
ConfigItem(
key="api_key",
value=self.config.server.api_key,
type="string",
description="API认证密钥",
editable=True,
required=True
),
]
))
# 数据库配置
if not req.type or req.type == ConfigType.DATABASE:
sections.append(ConfigSection(
name="数据库配置",
type=ConfigType.DATABASE,
description="PostgreSQL数据库连接配置",
items=[
ConfigItem(
key="host",
value=self.config.database.host,
type="string",
description="数据库主机地址",
editable=True,
required=True
),
ConfigItem(
key="port",
value=self.config.database.port,
type="int",
description="数据库端口",
editable=True,
required=True
),
ConfigItem(
key="user",
value=self.config.database.user,
type="string",
description="数据库用户名",
editable=True,
required=True
),
ConfigItem(
key="password",
value="********",
type="password",
description="数据库密码",
editable=True,
required=True
),
ConfigItem(
key="database",
value=self.config.database.database,
type="string",
description="数据库名",
editable=True,
required=True
),
]
))
# Redis配置
if not req.type or req.type == ConfigType.REDIS:
sections.append(ConfigSection(
name="Redis配置",
type=ConfigType.REDIS,
description="Redis缓存配置",
items=[
ConfigItem(
key="host",
value=self.config.redis.host,
type="string",
description="Redis主机地址",
editable=True,
required=False
),
ConfigItem(
key="port",
value=self.config.redis.port,
type="int",
description="Redis端口",
editable=True,
required=False
),
ConfigItem(
key="password",
value="********",
type="password",
description="Redis密码",
editable=True,
required=False
),
ConfigItem(
key="db",
value=self.config.redis.db,
type="int",
description="Redis数据库编号",
editable=True,
required=False
),
]
))
# 数据源配置
if not req.type or req.type == ConfigType.SOURCE:
sections.append(ConfigSection(
name="数据源配置",
type=ConfigType.SOURCE,
description="股票和期货数据源配置",
items=[
ConfigItem(
key="stock_active",
value=self.config.sources.stock.active,
type="string",
description="股票数据源适配器",
editable=True,
required=True
),
ConfigItem(
key="futures_active",
value=self.config.sources.futures.active,
type="string",
description="期货数据源适配器",
editable=True,
required=True
),
]
))
return ConfigListData(
sections=sections,
version=self.version,
updated=datetime.now()
)
def update_config(self, req: ConfigUpdateRequest) -> ConfigUpdateData:
"""更新配置"""
need_restart = False
with self.lock:
if req.type == ConfigType.SERVER:
if "port" in req.items:
self.config.server.port = int(req.items["port"])
need_restart = True
if "mode" in req.items:
self.config.server.mode = req.items["mode"]
if "api_key" in req.items:
self.config.server.api_key = req.items["api_key"]
elif req.type == ConfigType.DATABASE:
if "host" in req.items:
self.config.database.host = req.items["host"]
need_restart = True
if "port" in req.items:
self.config.database.port = int(req.items["port"])
need_restart = True
if "user" in req.items:
self.config.database.user = req.items["user"]
need_restart = True
if "password" in req.items:
password = req.items["password"]
if password != "********":
self.config.database.password = password
need_restart = True
if "database" in req.items:
self.config.database.database = req.items["database"]
need_restart = True
elif req.type == ConfigType.SOURCE:
if "stock_active" in req.items:
self.config.sources.stock.active = req.items["stock_active"]
if "futures_active" in req.items:
self.config.sources.futures.active = req.items["futures_active"]
# 保存到文件
try:
save_config(self.config)
self._trigger_callbacks(req.type)
message = "配置更新成功"
if need_restart:
message += ",部分配置需要重启服务后生效"
return ConfigUpdateData(
success=True,
need_restart=need_restart,
message=message
)
except Exception as e:
return ConfigUpdateData(
success=False,
need_restart=False,
message=f"配置保存失败: {e}"
)
def reload_config(self, req: ReloadRequest) -> ReloadData:
"""热加载配置"""
try:
with self.lock:
new_config = reload_config()
# 根据类型选择性更新
if req.config_type is None:
self.config = new_config
else:
if req.config_type == ConfigType.SERVER:
self.config.server = new_config.server
elif req.config_type == ConfigType.DATABASE:
self.config.database = new_config.database
elif req.config_type == ConfigType.REDIS:
self.config.redis = new_config.redis
elif req.config_type == ConfigType.SOURCE:
self.config.sources = new_config.sources
self._trigger_callbacks(req.config_type)
return ReloadData(
success=True,
message="配置热加载成功"
)
except Exception as e:
return ReloadData(
success=False,
message=f"加载配置失败: {e}"
)
def get_system_status(self) -> SystemStatusData:
"""获取系统状态"""
# 获取内存信息
mem = psutil.virtual_memory()
# 计算运行时长
uptime = datetime.now() - self.start_time
uptime_str = self._format_duration(uptime)
return SystemStatusData(
status="running",
version=self.version,
start_time=self.start_time,
uptime=uptime_str,
python_version=platform.python_version(),
memory=MemoryInfo(
alloc=mem.used,
total_alloc=mem.total,
sys=mem.total,
num_gc=0 # Python不需要显式GC计数
),
threads=threading.active_count()
)
def _format_duration(self, d: timedelta) -> str:
"""格式化持续时间"""
days = d.days
hours, remainder = divmod(d.seconds, 3600)
minutes, _ = divmod(remainder, 60)
if days > 0:
return f"{days}{hours}小时{minutes}分钟"
if hours > 0:
return f"{hours}小时{minutes}分钟"
return f"{minutes}分钟"
def register_callback(self, config_type: ConfigType, callback: Callable):
"""注册配置变更回调"""
with self.lock:
if config_type not in self.callbacks:
self.callbacks[config_type] = []
self.callbacks[config_type].append(callback)
def _trigger_callbacks(self, config_type: Optional[ConfigType]):
"""触发回调"""
with self.lock:
# 触发特定类型的回调
if config_type and config_type in self.callbacks:
for cb in self.callbacks[config_type]:
try:
cb()
except Exception as e:
info(f"Callback error: {e}")

@ -1,102 +0,0 @@
"""期货业务服务 - 对应Go的internal/service/futures.go"""
from datetime import datetime, timedelta
from typing import List
from sqlalchemy.orm import Session
from app.models import (
KLineQueryRequest, KLineData, SymbolListRequest, SymbolListData,
BatchKLineRequest, BatchKLineData, BatchKLineResult, KLineSubData,
TradingDatesRequest, TradingDatesData,
FuturesContractsRequest, FuturesContractsData
)
from app.repositories import FuturesRepository
from app.core.logger import error
class FuturesService:
"""期货业务服务"""
def __init__(self, db: Session):
self.repository = FuturesRepository(db)
def query_klines(self, req: KLineQueryRequest) -> KLineData:
"""查询K线数据"""
# 解析日期
try:
start = datetime.strptime(req.start, "%Y%m%d")
end = datetime.strptime(req.end, "%Y%m%d")
end = end + timedelta(days=1) - timedelta(seconds=1)
except ValueError as e:
raise ValueError(f"Invalid date format: {e}")
# 获取K线数据
items = self.repository.get_klines(req.symbol, req.freq, start, end)
return KLineData(
symbol=req.symbol,
freq=req.freq,
count=len(items),
items=items
)
def list_symbols(self, req: SymbolListRequest) -> SymbolListData:
"""查询标的列表"""
if req.page <= 0:
req.page = 1
if req.size <= 0:
req.size = 20
if req.size > 100:
req.size = 100
symbols, total = self.repository.list_symbols(req)
return SymbolListData(
total=total,
page=req.page,
size=req.size,
items=symbols
)
def batch_query_klines(self, req: BatchKLineRequest) -> BatchKLineData:
"""批量查询K线"""
results = []
for symbol in req.symbols:
single_req = KLineQueryRequest(
symbol=symbol,
start=req.start,
end=req.end,
freq=req.freq
)
try:
data = self.query_klines(single_req)
results.append(BatchKLineResult(
symbol=symbol,
success=True,
data=KLineSubData(count=data.count, items=data.items)
))
except Exception as e:
error(f"Batch query failed for {symbol}: {e}")
results.append(BatchKLineResult(
symbol=symbol,
success=False,
error=str(e)
))
return BatchKLineData(results=results)
def get_trading_dates(self, req: TradingDatesRequest) -> TradingDatesData:
"""获取交易日历"""
return self.repository.get_trading_dates(req.start, req.end)
def get_contracts_by_underlying(
self,
req: FuturesContractsRequest
) -> FuturesContractsData:
"""根据品种获取合约"""
return self.repository.get_contracts_by_underlying(
req.underlying,
req.exchange
)

@ -1,115 +0,0 @@
"""股票业务服务 - 对应Go的internal/service/stock.go"""
from datetime import datetime, timedelta
from typing import List
from sqlalchemy.orm import Session
from app.models import (
KLineQueryRequest, KLineData, SymbolListRequest, SymbolListData,
BatchKLineRequest, BatchKLineData, BatchKLineResult, KLineSubData,
TradingDatesRequest, TradingDatesData, AdjustType, Frequency
)
from app.repositories import StockRepository
from app.core.logger import error
class StockService:
"""股票业务服务"""
def __init__(self, db: Session):
self.repository = StockRepository(db)
def query_klines(self, req: KLineQueryRequest) -> KLineData:
"""查询K线数据"""
# 解析日期
try:
start = datetime.strptime(req.start, "%Y%m%d")
end = datetime.strptime(req.end, "%Y%m%d")
end = end + timedelta(days=1) - timedelta(seconds=1) # 包含结束日期全天
except ValueError as e:
raise ValueError(f"Invalid date format: {e}")
# 获取K线数据
items = self.repository.get_klines(
req.symbol,
req.freq,
start,
end,
req.adjust
)
# 处理复权(简化实现,实际需要复权系数表)
if req.adjust != AdjustType.NONE:
items = self._apply_adjust(req.symbol, items, req.adjust)
return KLineData(
symbol=req.symbol,
freq=req.freq,
adjust=req.adjust,
count=len(items),
items=items
)
def _apply_adjust(
self,
symbol: str,
items: List,
adjust_type: AdjustType
) -> List:
"""应用复权计算TODO: 实现复权逻辑)"""
# 复权计算需要从数据库获取复权系数
# 这里简化处理,直接返回原始数据
return items
def list_symbols(self, req: SymbolListRequest) -> SymbolListData:
"""查询标的列表"""
# 设置默认值
if req.page <= 0:
req.page = 1
if req.size <= 0:
req.size = 20
if req.size > 100:
req.size = 100
symbols, total = self.repository.list_symbols(req)
return SymbolListData(
total=total,
page=req.page,
size=req.size,
items=symbols
)
def batch_query_klines(self, req: BatchKLineRequest) -> BatchKLineData:
"""批量查询K线"""
results = []
for symbol in req.symbols:
single_req = KLineQueryRequest(
symbol=symbol,
start=req.start,
end=req.end,
freq=req.freq,
adjust=req.adjust
)
try:
data = self.query_klines(single_req)
results.append(BatchKLineResult(
symbol=symbol,
success=True,
data=KLineSubData(count=data.count, items=data.items)
))
except Exception as e:
error(f"Batch query failed for {symbol}: {e}")
results.append(BatchKLineResult(
symbol=symbol,
success=False,
error=str(e)
))
return BatchKLineData(results=results)
def get_trading_dates(self, req: TradingDatesRequest) -> TradingDatesData:
"""获取交易日历"""
return self.repository.get_trading_dates(req.start, req.end)

@ -1,390 +0,0 @@
"""测试服务 - 对应Go的internal/service/test.go"""
import asyncio
import json
from datetime import datetime, timedelta
from typing import List, Optional
from threading import RLock
import httpx
import websockets
from app.models import (
APITestListData, APITestCategory, APITestCase,
APITestRequest, APITestResult,
WSTestListData, WSTestCase, WSTestRequest, WSTestResult, WSMessage,
TestHistoryRequest, TestHistoryData
)
from app.core.logger import info, error
class TestService:
"""测试服务"""
def __init__(self):
self.lock = RLock()
self.api_history: List[APITestResult] = []
self.ws_history: List[WSTestResult] = []
self.history_size = 100
def get_api_test_list(self) -> APITestListData:
"""获取API测试列表"""
today = datetime.now()
month_ago = today - timedelta(days=30)
week_ago = today - timedelta(days=7)
categories = [
APITestCategory(
name="股票接口",
items=[
APITestCase(
id="stock_klines",
name="查询股票K线",
method="GET",
path="/v1/stock/klines/{symbol}",
description="查询指定股票的K线数据",
params={
"symbol": "000001.SZ",
"start": month_ago.strftime("%Y%m%d"),
"end": today.strftime("%Y%m%d"),
"freq": "1d",
"adjust": "qfq"
}
),
APITestCase(
id="stock_symbols",
name="查询股票列表",
method="GET",
path="/v1/stock/symbols",
description="获取所有可用股票标的",
params={"page": "1", "size": "20"}
),
APITestCase(
id="stock_batch",
name="批量查询股票K线",
method="POST",
path="/v1/stock/klines/batch",
description="批量查询多只股票K线",
body={
"symbols": ["000001.SZ", "000002.SZ"],
"start": week_ago.strftime("%Y%m%d"),
"end": today.strftime("%Y%m%d"),
"freq": "1d"
}
),
APITestCase(
id="stock_calendar",
name="查询交易日历",
method="GET",
path="/v1/stock/trading-dates",
description="查询股票交易日历",
params={
"start": month_ago.strftime("%Y%m%d"),
"end": (today + timedelta(days=30)).strftime("%Y%m%d")
}
),
]
),
APITestCategory(
name="期货接口",
items=[
APITestCase(
id="futures_klines",
name="查询期货K线",
method="GET",
path="/v1/futures/klines/{symbol}",
description="查询指定期货合约的K线数据",
params={
"symbol": "CU2504.SHFE",
"start": month_ago.strftime("%Y%m%d"),
"end": today.strftime("%Y%m%d"),
"freq": "1d"
}
),
APITestCase(
id="futures_symbols",
name="查询期货列表",
method="GET",
path="/v1/futures/symbols",
description="获取所有可用期货标的",
params={"page": "1", "size": "20"}
),
APITestCase(
id="futures_batch",
name="批量查询期货K线",
method="POST",
path="/v1/futures/klines/batch",
description="批量查询多个期货合约K线",
body={
"symbols": ["CU2504.SHFE", "RB2505.SHFE"],
"start": week_ago.strftime("%Y%m%d"),
"end": today.strftime("%Y%m%d"),
"freq": "1d"
}
),
APITestCase(
id="futures_contracts",
name="查询合约列表",
method="GET",
path="/v1/futures/contracts",
description="根据品种查询可交易合约",
params={"underlying": "CU", "exchange": "SHFE"}
),
APITestCase(
id="futures_calendar",
name="查询期货交易日历",
method="GET",
path="/v1/futures/trading-dates",
description="查询期货交易日历",
params={
"start": month_ago.strftime("%Y%m%d"),
"end": (today + timedelta(days=30)).strftime("%Y%m%d")
}
),
]
),
APITestCategory(
name="管理接口",
items=[
APITestCase(
id="admin_health",
name="健康检查",
method="GET",
path="/v1/admin/health",
description="检查服务健康状态",
params={}
),
APITestCase(
id="admin_source_status",
name="数据源状态",
method="GET",
path="/v1/admin/source/status",
description="获取当前数据源状态",
params={}
),
]
),
]
return APITestListData(categories=categories, base_url="")
async def run_api_test(self, base_url: str, req: APITestRequest) -> APITestResult:
"""执行API测试"""
# 获取测试用例
test_list = self.get_api_test_list()
test_case = None
for cat in test_list.categories:
for item in cat.items:
if item.id == req.id:
test_case = item
break
if test_case:
break
if not test_case:
raise ValueError(f"Test case not found: {req.id}")
# 合并参数
params = dict(test_case.params)
if req.params:
params.update(req.params)
# 构建URL
url = base_url + test_case.path
for k, v in params.items():
url = url.replace(f"{{{k}}}", str(v))
# 添加查询参数
if test_case.method == "GET" and params:
query_parts = []
for k, v in params.items():
if f"{{{k}}}" not in test_case.path:
query_parts.append(f"{k}={v}")
if query_parts:
url += "?" + "&".join(query_parts)
# 准备请求体
body = req.body if req.body is not None else test_case.body
# 执行请求
start_time = datetime.now()
async with httpx.AsyncClient() as client:
try:
headers = {"X-API-Key": "test-api-key"}
if test_case.method == "GET":
response = await client.get(url, headers=headers, timeout=30)
elif test_case.method == "POST":
response = await client.post(
url, json=body, headers=headers, timeout=30
)
else:
raise ValueError(f"Unsupported method: {test_case.method}")
latency = int((datetime.now() - start_time).total_seconds() * 1000)
result = APITestResult(
id=int(datetime.now().timestamp()),
case_id=req.id,
name=test_case.name,
success=200 <= response.status_code < 300,
status_code=response.status_code,
latency=latency,
request={
"method": test_case.method,
"url": url,
"body": body
},
response=response.json() if response.headers.get("content-type", "").startswith("application/json") else response.text,
timestamp=datetime.now()
)
self._add_api_history(result)
return result
except Exception as e:
latency = int((datetime.now() - start_time).total_seconds() * 1000)
result = APITestResult(
id=int(datetime.now().timestamp()),
case_id=req.id,
name=test_case.name,
success=False,
latency=latency,
request={
"method": test_case.method,
"url": url,
"body": body
},
error=str(e),
timestamp=datetime.now()
)
self._add_api_history(result)
return result
def get_ws_test_list(self) -> WSTestListData:
"""获取WebSocket测试列表"""
cases = [
WSTestCase(
id="ws_subscribe_stock",
name="订阅股票行情",
description="订阅单只股票实时行情",
action="subscribe",
symbols=["000001.SZ"]
),
WSTestCase(
id="ws_subscribe_futures",
name="订阅期货行情",
description="订阅单个期货合约实时行情",
action="subscribe",
symbols=["CU2504.SHFE"]
),
WSTestCase(
id="ws_subscribe_multi",
name="批量订阅",
description="同时订阅多个标的",
action="subscribe",
symbols=["000001.SZ", "000002.SZ", "CU2504.SHFE"]
),
WSTestCase(
id="ws_unsubscribe",
name="取消订阅",
description="取消订阅标的",
action="unsubscribe",
symbols=["000001.SZ"]
),
]
return WSTestListData(cases=cases, ws_url="")
async def run_ws_test(self, ws_url: str, req: WSTestRequest) -> WSTestResult:
"""执行WebSocket测试"""
# 获取测试用例
test_list = self.get_ws_test_list()
test_case = None
for item in test_list.cases:
if item.id == req.id:
test_case = item
break
if not test_case:
raise ValueError(f"Test case not found: {req.id}")
# 使用自定义标的
symbols = req.symbols if req.symbols else test_case.symbols
result = WSTestResult(
id=f"ws_{int(datetime.now().timestamp())}",
case_id=req.id,
timestamp=datetime.now(),
messages=[]
)
# 连接WebSocket
start_time = datetime.now()
try:
async with websockets.connect(
ws_url,
extra_headers={"X-API-Key": "test-api-key"}
) as ws:
result.latency = int((datetime.now() - start_time).total_seconds() * 1000)
result.success = True
# 发送订阅消息
msg = {
"action": test_case.action,
"symbols": symbols
}
await ws.send(json.dumps(msg))
# 等待响应最多3条消息
for _ in range(3):
try:
msg_data = await asyncio.wait_for(ws.recv(), timeout=5)
result.messages.append(WSMessage(
type="received",
data=json.loads(msg_data),
timestamp=datetime.now()
))
except asyncio.TimeoutError:
break
except Exception as e:
result.latency = int((datetime.now() - start_time).total_seconds() * 1000)
result.success = False
result.error = str(e)
self._add_ws_history(result)
return result
def get_test_history(self, req: TestHistoryRequest) -> TestHistoryData:
"""获取测试历史"""
with self.lock:
limit = req.limit or 20
api_tests = []
ws_tests = []
if not req.type or req.type == "api":
api_tests = self.api_history[-limit:]
if not req.type or req.type == "ws":
ws_tests = self.ws_history[-limit:]
return TestHistoryData(api_tests=api_tests, ws_tests=ws_tests)
def _add_api_history(self, result: APITestResult):
"""添加API测试历史"""
with self.lock:
self.api_history.append(result)
if len(self.api_history) > self.history_size:
self.api_history = self.api_history[-self.history_size:]
def _add_ws_history(self, result: WSTestResult):
"""添加WebSocket测试历史"""
with self.lock:
self.ws_history.append(result)
if len(self.ws_history) > self.history_size:
self.ws_history = self.ws_history[-self.history_size:]

@ -1,4 +0,0 @@
"""WebSocket服务模块"""
from .server import WebSocketServer, ws_manager
__all__ = ["WebSocketServer", "ws_manager"]

@ -1,210 +0,0 @@
"""WebSocket服务 - 对应Go的internal/websocket/server.go"""
import asyncio
import json
from datetime import datetime
from typing import Dict, Set, Optional
from dataclasses import dataclass, field
from fastapi import WebSocket, WebSocketDisconnect
from app.core.logger import info, error
@dataclass
class WSClient:
"""WebSocket客户端"""
id: str
websocket: WebSocket
subscriptions: Set[str] = field(default_factory=set)
async def send(self, message: dict):
"""发送消息"""
try:
await self.websocket.send_json(message)
except Exception as e:
error(f"Failed to send message to client {self.id}: {e}")
class WebSocketManager:
"""WebSocket连接管理器"""
def __init__(self):
self.clients: Dict[str, WSClient] = {}
self.subscriptions: Dict[str, Set[str]] = {} # symbol -> set of client_ids
self.max_symbols_per_client = 100
self.lock = asyncio.Lock()
async def connect(self, websocket: WebSocket, client_id: str) -> WSClient:
"""建立连接"""
await websocket.accept()
client = WSClient(id=client_id, websocket=websocket)
async with self.lock:
self.clients[client_id] = client
info(f"WebSocket client connected: {client_id}, total: {len(self.clients)}")
return client
async def disconnect(self, client_id: str):
"""断开连接"""
async with self.lock:
if client_id in self.clients:
client = self.clients.pop(client_id)
# 清理订阅
for symbol in client.subscriptions:
if symbol in self.subscriptions:
self.subscriptions[symbol].discard(client_id)
if not self.subscriptions[symbol]:
del self.subscriptions[symbol]
info(f"WebSocket client disconnected: {client_id}, total: {len(self.clients)}")
async def subscribe(self, client_id: str, symbols: list) -> bool:
"""订阅标的"""
async with self.lock:
if client_id not in self.clients:
return False
client = self.clients[client_id]
# 检查订阅数量限制
if len(client.subscriptions) + len(symbols) > self.max_symbols_per_client:
return False
for symbol in symbols:
client.subscriptions.add(symbol)
if symbol not in self.subscriptions:
self.subscriptions[symbol] = set()
self.subscriptions[symbol].add(client_id)
return True
async def unsubscribe(self, client_id: str, symbols: list):
"""取消订阅"""
async with self.lock:
if client_id not in self.clients:
return
client = self.clients[client_id]
for symbol in symbols:
client.subscriptions.discard(symbol)
if symbol in self.subscriptions:
self.subscriptions[symbol].discard(client_id)
if not self.subscriptions[symbol]:
del self.subscriptions[symbol]
async def broadcast_to_symbol(self, symbol: str, message: dict):
"""向订阅了某标的的所有客户端广播"""
client_ids = set()
async with self.lock:
if symbol in self.subscriptions:
client_ids = self.subscriptions[symbol].copy()
# 在锁外发送消息
for client_id in client_ids:
if client_id in self.clients:
try:
await self.clients[client_id].send(message)
except Exception as e:
error(f"Failed to broadcast to {client_id}: {e}")
def get_stats(self) -> dict:
"""获取统计信息"""
return {
"total_clients": len(self.clients),
"total_subscriptions": len(self.subscriptions)
}
# 全局WebSocket管理器实例
ws_manager = WebSocketManager()
class WebSocketServer:
"""WebSocket服务器"""
def __init__(self):
self.manager = ws_manager
async def handle(self, websocket: WebSocket, client_id: str):
"""处理WebSocket连接"""
client = await self.manager.connect(websocket, client_id)
try:
while True:
# 接收消息
data = await websocket.receive_text()
try:
msg = json.loads(data)
action = msg.get("action")
symbols = msg.get("symbols", [])
if action == "subscribe":
success = await self.manager.subscribe(client_id, symbols)
if success:
await client.send({
"type": "ack",
"action": "subscribe",
"symbols": symbols,
"ts": datetime.now().isoformat()
})
else:
await client.send({
"type": "error",
"code": 1003,
"message": "Too many subscriptions or subscription failed",
"ts": datetime.now().isoformat()
})
elif action == "unsubscribe":
await self.manager.unsubscribe(client_id, symbols)
await client.send({
"type": "ack",
"action": "unsubscribe",
"symbols": symbols,
"ts": datetime.now().isoformat()
})
else:
await client.send({
"type": "error",
"code": 1001,
"message": "Unknown action",
"ts": datetime.now().isoformat()
})
except json.JSONDecodeError:
await client.send({
"type": "error",
"code": 1000,
"message": "Invalid message format",
"ts": datetime.now().isoformat()
})
except WebSocketDisconnect:
await self.manager.disconnect(client_id)
except Exception as e:
error(f"WebSocket error for client {client_id}: {e}")
await self.manager.disconnect(client_id)
async def send_heartbeat(self):
"""发送心跳(可由定时任务调用)"""
message = {
"type": "heartbeat",
"ts": datetime.now().isoformat()
}
# 向所有客户端发送心跳
clients_copy = list(self.manager.clients.values())
for client in clients_copy:
try:
await client.send(message)
except Exception:
pass

@ -1,46 +0,0 @@
{
"server": {
"port": 8080,
"mode": "debug",
"api_key": "demo-api-key-2024"
},
"database": {
"host": "localhost",
"port": 5432,
"user": "postgres",
"password": "postgres",
"database": "marketdata"
},
"redis": {
"host": "localhost",
"port": 6379,
"password": "",
"db": 0
},
"sources": {
"stock": {
"active": "tushare",
"list": {
"tushare": {
"type": "http",
"config": {
"token": "",
"base_url": "https://api.tushare.pro"
}
}
}
},
"futures": {
"active": "tushare",
"list": {
"tushare": {
"type": "http",
"config": {
"token": "",
"base_url": "https://api.tushare.pro"
}
}
}
}
}
}

@ -1,44 +0,0 @@
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
[project]
name = "market-data-service"
version = "1.0.0"
description = "统一行情数据服务 - Python实现"
readme = "README.md"
requires-python = ">=3.10"
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
]
dependencies = [
"fastapi>=0.115.0",
"uvicorn[standard]>=0.32.0",
"python-socketio>=5.12.1",
"websockets>=14.1",
"sqlalchemy>=2.0.36",
"psycopg2-binary>=2.9.10",
"pandas>=2.2.3",
"numpy>=2.1.3",
"pydantic>=2.10.0",
"pydantic-settings>=2.6.1",
"python-dotenv>=1.0.1",
"PyYAML>=6.0.2",
"httpx>=0.28.0",
"apscheduler>=3.11.0",
]
[project.optional-dependencies]
dev = [
"pytest>=8.3.4",
"pytest-asyncio>=0.24.0",
]
[tool.setuptools.packages.find]
where = ["."]
include = ["app*"]

@ -1,38 +0,0 @@
# Web Framework
fastapi==0.115.0
uvicorn[standard]==0.32.0
python-socketio==5.12.1
websockets==14.1
# Database
sqlalchemy==2.0.36
psycopg2-binary==2.9.10
alembic==1.14.0
# Data Processing
pandas==2.2.3
numpy==2.1.3
# Data Source
# Note: tushare needs to be installed separately with: pip install tushare
tushare==1.4.14
# Configuration
pydantic==2.10.0
pydantic-settings==2.6.1
python-dotenv==1.0.1
PyYAML==6.0.2
# Utilities
python-multipart==0.0.19
httpx==0.28.0
aiohttp==3.11.10
aioredis==2.0.1
# Monitoring
apscheduler==3.11.0
# Testing
pytest==8.3.4
pytest-asyncio==0.24.0
httpx==0.28.0

@ -1,236 +0,0 @@
"""数据同步工具 - 对应Go的cmd/sync/main.go"""
import asyncio
import os
import sys
from datetime import datetime, timedelta
from argparse import ArgumentParser
# 添加项目根目录到路径
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from app.adapters import TushareAdapter
from app.repositories import SessionLocal
from app.repositories.stock_repository import StockRepository
from app.repositories.futures_repository import FuturesRepository
from app.models import Symbol, SymbolType, TradeCalData
from app.core.logger import info, error
def parse_date(date_str: str) -> datetime:
"""解析日期字符串"""
return datetime.strptime(date_str, "%Y%m%d")
def format_date(date: datetime) -> str:
"""格式化日期为字符串"""
return date.strftime("%Y%m%d")
def is_stock(symbol: str) -> bool:
"""判断是否为股票代码"""
return symbol.endswith(".SH") or symbol.endswith(".SZ") or symbol.endswith(".BJ")
async def sync_stocks(adapter: TushareAdapter, db):
"""同步股票基础信息"""
info("Syncing stock basic info...")
try:
symbols_data = await adapter.fetch_symbols("stock")
repo = StockRepository(db)
symbols = []
for d in symbols_data:
list_date = None
if d.list_date:
try:
list_date = datetime.strptime(d.list_date, "%Y%m%d")
except:
pass
symbols.append(Symbol(
symbol_id=d.symbol_id,
symbol_type=SymbolType.STOCK,
exchange=d.exchange,
name=d.name,
list_date=list_date,
status="active"
))
repo.save_symbols(symbols)
info(f"Synced {len(symbols)} stocks")
except Exception as e:
error(f"Failed to sync stocks: {e}")
raise
async def sync_futures(adapter: TushareAdapter, db):
"""同步期货基础信息"""
info("Syncing futures basic info...")
try:
symbols_data = await adapter.fetch_symbols("futures")
repo = FuturesRepository(db)
symbols = []
for d in symbols_data:
list_date = None
delist_date = None
if d.list_date:
try:
list_date = datetime.strptime(d.list_date, "%Y%m%d")
except:
pass
if d.delist_date:
try:
delist_date = datetime.strptime(d.delist_date, "%Y%m%d")
except:
pass
status = "active"
if delist_date and datetime.now() > delist_date:
status = "expired"
symbols.append(Symbol(
symbol_id=d.symbol_id,
symbol_type=SymbolType.FUTURES,
exchange=d.exchange,
name=d.name,
underlying=d.underlying,
contract_month=d.contract_month,
list_date=list_date,
delist_date=delist_date,
status=status
))
repo.save_symbols(symbols)
info(f"Synced {len(symbols)} futures")
except Exception as e:
error(f"Failed to sync futures: {e}")
raise
async def sync_calendar(adapter: TushareAdapter, db, start: str, end: str):
"""同步交易日历"""
info(f"Syncing trading calendar from {start} to {end}...")
try:
# 同步股票交易日历(上交所)
stock_data = await adapter.fetch_trading_calendar("SH", start, end)
stock_repo = StockRepository(db)
stock_dates = [
TradeCalData(date=d.date, is_trading_day=d.is_trading_day)
for d in stock_data
]
stock_repo.save_trading_calendar(stock_dates)
# 同步期货交易日历
futures_repo = FuturesRepository(db)
futures_repo.save_trading_calendar(stock_dates)
info(f"Synced {len(stock_dates)} calendar days")
except Exception as e:
error(f"Failed to sync calendar: {e}")
raise
async def sync_klines(adapter: TushareAdapter, db, symbol: str, start: str, end: str, freq: str):
"""同步K线数据"""
info(f"Syncing {freq} klines for {symbol} from {start} to {end}...")
try:
# 获取K线数据
klines_data = await adapter.fetch_klines(symbol, start, end, freq)
# 转换为KLineItem并保存
from app.models import KLineItem
items = [
KLineItem(
time=datetime.fromtimestamp(d.time),
open=d.open,
high=d.high,
low=d.low,
close=d.close,
volume=d.volume,
amount=d.amount,
open_interest=d.open_interest if d.open_interest > 0 else None
)
for d in klines_data
]
# 判断股票还是期货并保存
from app.models import Frequency
if is_stock(symbol):
repo = StockRepository(db)
# 为每个item设置symbol
for item in items:
item.symbol = symbol
repo.save_klines(Frequency(freq), items)
else:
repo = FuturesRepository(db)
repo.save_klines(Frequency(freq), symbol, items)
info(f"Synced {len(items)} klines")
except Exception as e:
error(f"Failed to sync klines: {e}")
raise
async def main():
"""主函数"""
parser = ArgumentParser(description="Market Data Sync Tool")
parser.add_argument(
"--type", "-t",
required=True,
choices=["stocks", "futures", "calendar", "klines"],
help="同步类型"
)
parser.add_argument("--start", "-s", help="开始日期 YYYYMMDD")
parser.add_argument("--end", "-e", help="结束日期 YYYYMMDD")
parser.add_argument("--symbol", help="标的代码klines类型需要")
parser.add_argument("--freq", "-f", default="1d", help="K线周期")
args = parser.parse_args()
# 配置
tushare_token = os.environ.get("TUSHARE_TOKEN")
if not tushare_token:
error("TUSHARE_TOKEN environment variable is required")
sys.exit(1)
# 初始化适配器
adapter = TushareAdapter()
await adapter.connect({"token": tushare_token})
# 创建数据库会话
db = SessionLocal()
try:
if args.type == "stocks":
await sync_stocks(adapter, db)
elif args.type == "futures":
await sync_futures(adapter, db)
elif args.type == "calendar":
# 设置默认日期范围
start = args.start or (datetime.now() - timedelta(days=30)).strftime("%Y%m%d")
end = args.end or (datetime.now() + timedelta(days=180)).strftime("%Y%m%d")
await sync_calendar(adapter, db, start, end)
elif args.type == "klines":
if not args.symbol:
error("symbol is required for klines sync")
sys.exit(1)
start = args.start or (datetime.now() - timedelta(days=7)).strftime("%Y%m%d")
end = args.end or datetime.now().strftime("%Y%m%d")
await sync_klines(adapter, db, args.symbol, start, end, args.freq)
finally:
db.close()
await adapter.close()
if __name__ == "__main__":
asyncio.run(main())

@ -1,164 +0,0 @@
/* -*- indent-tabs-mode: nil; tab-width: 4; -*- */
/* Greenlet object interface */
#ifndef Py_GREENLETOBJECT_H
#define Py_GREENLETOBJECT_H
#include <Python.h>
#ifdef __cplusplus
extern "C" {
#endif
/* This is deprecated and undocumented. It does not change. */
#define GREENLET_VERSION "1.0.0"
#ifndef GREENLET_MODULE
#define implementation_ptr_t void*
#endif
typedef struct _greenlet {
PyObject_HEAD
PyObject* weakreflist;
PyObject* dict;
implementation_ptr_t pimpl;
} PyGreenlet;
#define PyGreenlet_Check(op) (op && PyObject_TypeCheck(op, &PyGreenlet_Type))
/* C API functions */
/* Total number of symbols that are exported */
#define PyGreenlet_API_pointers 12
#define PyGreenlet_Type_NUM 0
#define PyExc_GreenletError_NUM 1
#define PyExc_GreenletExit_NUM 2
#define PyGreenlet_New_NUM 3
#define PyGreenlet_GetCurrent_NUM 4
#define PyGreenlet_Throw_NUM 5
#define PyGreenlet_Switch_NUM 6
#define PyGreenlet_SetParent_NUM 7
#define PyGreenlet_MAIN_NUM 8
#define PyGreenlet_STARTED_NUM 9
#define PyGreenlet_ACTIVE_NUM 10
#define PyGreenlet_GET_PARENT_NUM 11
#ifndef GREENLET_MODULE
/* This section is used by modules that uses the greenlet C API */
static void** _PyGreenlet_API = NULL;
# define PyGreenlet_Type \
(*(PyTypeObject*)_PyGreenlet_API[PyGreenlet_Type_NUM])
# define PyExc_GreenletError \
((PyObject*)_PyGreenlet_API[PyExc_GreenletError_NUM])
# define PyExc_GreenletExit \
((PyObject*)_PyGreenlet_API[PyExc_GreenletExit_NUM])
/*
* PyGreenlet_New(PyObject *args)
*
* greenlet.greenlet(run, parent=None)
*/
# define PyGreenlet_New \
(*(PyGreenlet * (*)(PyObject * run, PyGreenlet * parent)) \
_PyGreenlet_API[PyGreenlet_New_NUM])
/*
* PyGreenlet_GetCurrent(void)
*
* greenlet.getcurrent()
*/
# define PyGreenlet_GetCurrent \
(*(PyGreenlet * (*)(void)) _PyGreenlet_API[PyGreenlet_GetCurrent_NUM])
/*
* PyGreenlet_Throw(
* PyGreenlet *greenlet,
* PyObject *typ,
* PyObject *val,
* PyObject *tb)
*
* g.throw(...)
*/
# define PyGreenlet_Throw \
(*(PyObject * (*)(PyGreenlet * self, \
PyObject * typ, \
PyObject * val, \
PyObject * tb)) \
_PyGreenlet_API[PyGreenlet_Throw_NUM])
/*
* PyGreenlet_Switch(PyGreenlet *greenlet, PyObject *args)
*
* g.switch(*args, **kwargs)
*/
# define PyGreenlet_Switch \
(*(PyObject * \
(*)(PyGreenlet * greenlet, PyObject * args, PyObject * kwargs)) \
_PyGreenlet_API[PyGreenlet_Switch_NUM])
/*
* PyGreenlet_SetParent(PyObject *greenlet, PyObject *new_parent)
*
* g.parent = new_parent
*/
# define PyGreenlet_SetParent \
(*(int (*)(PyGreenlet * greenlet, PyGreenlet * nparent)) \
_PyGreenlet_API[PyGreenlet_SetParent_NUM])
/*
* PyGreenlet_GetParent(PyObject* greenlet)
*
* return greenlet.parent;
*
* This could return NULL even if there is no exception active.
* If it does not return NULL, you are responsible for decrementing the
* reference count.
*/
# define PyGreenlet_GetParent \
(*(PyGreenlet* (*)(PyGreenlet*)) \
_PyGreenlet_API[PyGreenlet_GET_PARENT_NUM])
/*
* deprecated, undocumented alias.
*/
# define PyGreenlet_GET_PARENT PyGreenlet_GetParent
# define PyGreenlet_MAIN \
(*(int (*)(PyGreenlet*)) \
_PyGreenlet_API[PyGreenlet_MAIN_NUM])
# define PyGreenlet_STARTED \
(*(int (*)(PyGreenlet*)) \
_PyGreenlet_API[PyGreenlet_STARTED_NUM])
# define PyGreenlet_ACTIVE \
(*(int (*)(PyGreenlet*)) \
_PyGreenlet_API[PyGreenlet_ACTIVE_NUM])
/* Macro that imports greenlet and initializes C API */
/* NOTE: This has actually moved to ``greenlet._greenlet._C_API``, but we
keep the older definition to be sure older code that might have a copy of
the header still works. */
# define PyGreenlet_Import() \
{ \
_PyGreenlet_API = (void**)PyCapsule_Import("greenlet._C_API", 0); \
}
#endif /* GREENLET_MODULE */
#ifdef __cplusplus
}
#endif
#endif /* !Py_GREENLETOBJECT_H */

@ -1,19 +0,0 @@
This is the MIT license: http://www.opensource.org/licenses/mit-license.php
Copyright (c) Alex Grönholm
Permission is hereby granted, free of charge, to any person obtaining a copy of this
software and associated documentation files (the "Software"), to deal in the Software
without restriction, including without limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or
substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.

@ -1,147 +0,0 @@
Metadata-Version: 2.1
Name: APScheduler
Version: 3.11.0
Summary: In-process task scheduler with Cron-like capabilities
Author-email: Alex Grönholm <alex.gronholm@nextday.fi>
License: MIT
Project-URL: Documentation, https://apscheduler.readthedocs.io/en/3.x/
Project-URL: Changelog, https://apscheduler.readthedocs.io/en/3.x/versionhistory.html
Project-URL: Source code, https://github.com/agronholm/apscheduler
Project-URL: Issue tracker, https://github.com/agronholm/apscheduler/issues
Keywords: scheduling,cron
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Requires-Python: >=3.8
Description-Content-Type: text/x-rst
License-File: LICENSE.txt
Requires-Dist: tzlocal>=3.0
Requires-Dist: backports.zoneinfo; python_version < "3.9"
Provides-Extra: etcd
Requires-Dist: etcd3; extra == "etcd"
Requires-Dist: protobuf<=3.21.0; extra == "etcd"
Provides-Extra: gevent
Requires-Dist: gevent; extra == "gevent"
Provides-Extra: mongodb
Requires-Dist: pymongo>=3.0; extra == "mongodb"
Provides-Extra: redis
Requires-Dist: redis>=3.0; extra == "redis"
Provides-Extra: rethinkdb
Requires-Dist: rethinkdb>=2.4.0; extra == "rethinkdb"
Provides-Extra: sqlalchemy
Requires-Dist: sqlalchemy>=1.4; extra == "sqlalchemy"
Provides-Extra: tornado
Requires-Dist: tornado>=4.3; extra == "tornado"
Provides-Extra: twisted
Requires-Dist: twisted; extra == "twisted"
Provides-Extra: zookeeper
Requires-Dist: kazoo; extra == "zookeeper"
Provides-Extra: test
Requires-Dist: APScheduler[etcd,mongodb,redis,rethinkdb,sqlalchemy,tornado,zookeeper]; extra == "test"
Requires-Dist: pytest; extra == "test"
Requires-Dist: anyio>=4.5.2; extra == "test"
Requires-Dist: PySide6; (platform_python_implementation == "CPython" and python_version < "3.14") and extra == "test"
Requires-Dist: gevent; python_version < "3.14" and extra == "test"
Requires-Dist: pytz; extra == "test"
Requires-Dist: twisted; python_version < "3.14" and extra == "test"
Provides-Extra: doc
Requires-Dist: packaging; extra == "doc"
Requires-Dist: sphinx; extra == "doc"
Requires-Dist: sphinx-rtd-theme>=1.3.0; extra == "doc"
.. image:: https://github.com/agronholm/apscheduler/workflows/Python%20codeqa/test/badge.svg?branch=3.x
:target: https://github.com/agronholm/apscheduler/actions?query=workflow%3A%22Python+codeqa%2Ftest%22+branch%3A3.x
:alt: Build Status
.. image:: https://coveralls.io/repos/github/agronholm/apscheduler/badge.svg?branch=3.x
:target: https://coveralls.io/github/agronholm/apscheduler?branch=3.x
:alt: Code Coverage
.. image:: https://readthedocs.org/projects/apscheduler/badge/?version=3.x
:target: https://apscheduler.readthedocs.io/en/master/?badge=3.x
:alt: Documentation
Advanced Python Scheduler (APScheduler) is a Python library that lets you schedule your Python code
to be executed later, either just once or periodically. You can add new jobs or remove old ones on
the fly as you please. If you store your jobs in a database, they will also survive scheduler
restarts and maintain their state. When the scheduler is restarted, it will then run all the jobs
it should have run while it was offline [#f1]_.
Among other things, APScheduler can be used as a cross-platform, application specific replacement
to platform specific schedulers, such as the cron daemon or the Windows task scheduler. Please
note, however, that APScheduler is **not** a daemon or service itself, nor does it come with any
command line tools. It is primarily meant to be run inside existing applications. That said,
APScheduler does provide some building blocks for you to build a scheduler service or to run a
dedicated scheduler process.
APScheduler has three built-in scheduling systems you can use:
* Cron-style scheduling (with optional start/end times)
* Interval-based execution (runs jobs on even intervals, with optional start/end times)
* One-off delayed execution (runs jobs once, on a set date/time)
You can mix and match scheduling systems and the backends where the jobs are stored any way you
like. Supported backends for storing jobs include:
* Memory
* `SQLAlchemy <http://www.sqlalchemy.org/>`_ (any RDBMS supported by SQLAlchemy works)
* `MongoDB <http://www.mongodb.org/>`_
* `Redis <http://redis.io/>`_
* `RethinkDB <https://www.rethinkdb.com/>`_
* `ZooKeeper <https://zookeeper.apache.org/>`_
* `Etcd <https://etcd.io/>`_
APScheduler also integrates with several common Python frameworks, like:
* `asyncio <http://docs.python.org/3.4/library/asyncio.html>`_ (:pep:`3156`)
* `gevent <http://www.gevent.org/>`_
* `Tornado <http://www.tornadoweb.org/>`_
* `Twisted <http://twistedmatrix.com/>`_
* `Qt <http://qt-project.org/>`_ (using either
`PyQt <http://www.riverbankcomputing.com/software/pyqt/intro>`_ ,
`PySide6 <https://wiki.qt.io/Qt_for_Python>`_ ,
`PySide2 <https://wiki.qt.io/Qt_for_Python>`_ or
`PySide <http://qt-project.org/wiki/PySide>`_)
There are third party solutions for integrating APScheduler with other frameworks:
* `Django <https://github.com/jarekwg/django-apscheduler>`_
* `Flask <https://github.com/viniciuschiele/flask-apscheduler>`_
.. [#f1] The cutoff period for this is also configurable.
Documentation
-------------
Documentation can be found `here <https://apscheduler.readthedocs.io/>`_.
Source
------
The source can be browsed at `Github <https://github.com/agronholm/apscheduler/tree/3.x>`_.
Reporting bugs
--------------
A `bug tracker <https://github.com/agronholm/apscheduler/issues>`_ is provided by Github.
Getting help
------------
If you have problems or other questions, you can either:
* Ask in the `apscheduler <https://gitter.im/apscheduler/Lobby>`_ room on Gitter
* Ask on the `APScheduler GitHub discussion forum <https://github.com/agronholm/apscheduler/discussions>`_, or
* Ask on `StackOverflow <http://stackoverflow.com/questions/tagged/apscheduler>`_ and tag your
question with the ``apscheduler`` tag

@ -1,86 +0,0 @@
APScheduler-3.11.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
APScheduler-3.11.0.dist-info/LICENSE.txt,sha256=YWP3mH37ONa8MgzitwsvArhivEESZRbVUu8c1DJH51g,1130
APScheduler-3.11.0.dist-info/METADATA,sha256=Mve2P3vZbWWDb5V-XfZO80hkih9E6s00Nn5ptU2__9w,6374
APScheduler-3.11.0.dist-info/RECORD,,
APScheduler-3.11.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
APScheduler-3.11.0.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
APScheduler-3.11.0.dist-info/entry_points.txt,sha256=HSDTxgulLTgymfXK2UNCPP1ib5rlQSFgZJEg72vto3s,1181
APScheduler-3.11.0.dist-info/top_level.txt,sha256=O3oMCWxG-AHkecUoO6Ze7-yYjWrttL95uHO8-RFdYvE,12
apscheduler/__init__.py,sha256=hOpI9oJuk5l5I_VtdsHPous2Qr-ZDX573e7NaYRWFUs,380
apscheduler/__pycache__/__init__.cpython-311.pyc,,
apscheduler/__pycache__/events.cpython-311.pyc,,
apscheduler/__pycache__/job.cpython-311.pyc,,
apscheduler/__pycache__/util.cpython-311.pyc,,
apscheduler/events.py,sha256=W_Wg5aTBXDxXhHtimn93ZEjV3x0ntF-Y0EAVuZPhiXY,3591
apscheduler/executors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
apscheduler/executors/__pycache__/__init__.cpython-311.pyc,,
apscheduler/executors/__pycache__/asyncio.cpython-311.pyc,,
apscheduler/executors/__pycache__/base.cpython-311.pyc,,
apscheduler/executors/__pycache__/debug.cpython-311.pyc,,
apscheduler/executors/__pycache__/gevent.cpython-311.pyc,,
apscheduler/executors/__pycache__/pool.cpython-311.pyc,,
apscheduler/executors/__pycache__/tornado.cpython-311.pyc,,
apscheduler/executors/__pycache__/twisted.cpython-311.pyc,,
apscheduler/executors/asyncio.py,sha256=g0ArcxefoTnEqtyr_IRc-M3dcj0bhuvHcxwRp2s3nDE,1768
apscheduler/executors/base.py,sha256=HErgd8d1g0-BjXnylLcFyoo6GU3wHgW9GJVaFNMV7dI,7116
apscheduler/executors/debug.py,sha256=15_ogSBzl8RRCfBYDnkIV2uMH8cLk1KImYmBa_NVGpc,573
apscheduler/executors/gevent.py,sha256=_ZFpbn7-tH5_lAeL4sxEyPhxyUTtUUSrH8s42EHGQ2w,761
apscheduler/executors/pool.py,sha256=q_shxnvXLjdcwhtKyPvQSYngOjAeKQO8KCvZeb19RSQ,2683
apscheduler/executors/tornado.py,sha256=lb6mshRj7GMLz3d8StwESnlZsAfrNmW78Wokcg__Lk8,1581
apscheduler/executors/twisted.py,sha256=YUEDnaPbP_M0lXCmNAW_yPiLKwbO9vD3KMiBFQ2D4h0,726
apscheduler/job.py,sha256=GzOGMfOM6STwd3HWArVAylO-1Kb0f2qA_PRuXs5LPk4,11153
apscheduler/jobstores/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
apscheduler/jobstores/__pycache__/__init__.cpython-311.pyc,,
apscheduler/jobstores/__pycache__/base.cpython-311.pyc,,
apscheduler/jobstores/__pycache__/etcd.cpython-311.pyc,,
apscheduler/jobstores/__pycache__/memory.cpython-311.pyc,,
apscheduler/jobstores/__pycache__/mongodb.cpython-311.pyc,,
apscheduler/jobstores/__pycache__/redis.cpython-311.pyc,,
apscheduler/jobstores/__pycache__/rethinkdb.cpython-311.pyc,,
apscheduler/jobstores/__pycache__/sqlalchemy.cpython-311.pyc,,
apscheduler/jobstores/__pycache__/zookeeper.cpython-311.pyc,,
apscheduler/jobstores/base.py,sha256=ZDOgMtHLaF3TPUOQwmkBIDcpnHU0aUhtzZOGmMGaJn8,4416
apscheduler/jobstores/etcd.py,sha256=O7C40CGlnn3cPinchJEs2sWcqnzEZQt3c6WnhgPRSdQ,5703
apscheduler/jobstores/memory.py,sha256=HmOs7FbrOoQNywz-yfq2v5esGDHeKE_mvMNFDeGZ31E,3595
apscheduler/jobstores/mongodb.py,sha256=mCIwcKiWcicM2qdAQn51QBEkGlNfbk_73Oi6soShNcM,5319
apscheduler/jobstores/redis.py,sha256=El-H2eUfZjPZca7vwy10B9gZv5RzRucbkDu7Ti07vyM,5482
apscheduler/jobstores/rethinkdb.py,sha256=SdT3jPrhxnmBoL4IClDfHsez4DpREnYEsHndIP8idHA,5922
apscheduler/jobstores/sqlalchemy.py,sha256=2jaq3ZcoXEyIqqvYf3eloaP-_ZAqojt0EuWWvQ2LMRg,6799
apscheduler/jobstores/zookeeper.py,sha256=32bEZNJNniPwmYXBITZ3eSRBq6hipqPKDqh4q4NiZvc,6439
apscheduler/schedulers/__init__.py,sha256=POEy7n3BZgccZ44atMvxj0w5PejN55g-55NduZUZFqQ,406
apscheduler/schedulers/__pycache__/__init__.cpython-311.pyc,,
apscheduler/schedulers/__pycache__/asyncio.cpython-311.pyc,,
apscheduler/schedulers/__pycache__/background.cpython-311.pyc,,
apscheduler/schedulers/__pycache__/base.cpython-311.pyc,,
apscheduler/schedulers/__pycache__/blocking.cpython-311.pyc,,
apscheduler/schedulers/__pycache__/gevent.cpython-311.pyc,,
apscheduler/schedulers/__pycache__/qt.cpython-311.pyc,,
apscheduler/schedulers/__pycache__/tornado.cpython-311.pyc,,
apscheduler/schedulers/__pycache__/twisted.cpython-311.pyc,,
apscheduler/schedulers/asyncio.py,sha256=Jo7tgHP1STnMSxNVAWPSkFpmBLngavivTsG9sF0QoWM,1893
apscheduler/schedulers/background.py,sha256=sRNrikUhpyblvA5RCpKC5Djvf3-b6NHvnXTblxlqIaM,1476
apscheduler/schedulers/base.py,sha256=hvnvcI1DOC9bmvrFk8UiLlGxsXKHtMpEHLDEe63mQ_s,48342
apscheduler/schedulers/blocking.py,sha256=138rf9X1C-ZxWVTVAO_pyfYMBKhkqO2qZqJoyGInv5c,872
apscheduler/schedulers/gevent.py,sha256=zS5nHQUkQMrn0zKOaFnUyiG0fXTE01yE9GXVNCdrd90,987
apscheduler/schedulers/qt.py,sha256=6BHOCi8e6L3wXTWwQDjNl8w_GJF_dY6iiO3gEtCJgmI,1241
apscheduler/schedulers/tornado.py,sha256=dQBQKrTtZLPHuhuzZgrT-laU-estPRWGv9W9kgZETnY,1890
apscheduler/schedulers/twisted.py,sha256=sRkI3hosp-OCLVluR_-wZFCz9auxqqWYauZhtOAoRU4,1778
apscheduler/triggers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
apscheduler/triggers/__pycache__/__init__.cpython-311.pyc,,
apscheduler/triggers/__pycache__/base.cpython-311.pyc,,
apscheduler/triggers/__pycache__/calendarinterval.cpython-311.pyc,,
apscheduler/triggers/__pycache__/combining.cpython-311.pyc,,
apscheduler/triggers/__pycache__/date.cpython-311.pyc,,
apscheduler/triggers/__pycache__/interval.cpython-311.pyc,,
apscheduler/triggers/base.py,sha256=8iKllubaexF456IK9jfi56QTrVIfDDPLavUc8wTlnL0,1333
apscheduler/triggers/calendarinterval.py,sha256=BaH5rbTSVbPk3VhFwA3zORLSuZtYmFudS8GF0YxB51E,7411
apscheduler/triggers/combining.py,sha256=LO0YKgBk8V5YfQ-L3qh8Fb6w0BvNOBghTFeAvZx3_P8,4044
apscheduler/triggers/cron/__init__.py,sha256=ByWq4Q96gUWr4AwKoRRA9BD5ZVBvwQ6BtQMhafdStjw,9753
apscheduler/triggers/cron/__pycache__/__init__.cpython-311.pyc,,
apscheduler/triggers/cron/__pycache__/expressions.cpython-311.pyc,,
apscheduler/triggers/cron/__pycache__/fields.cpython-311.pyc,,
apscheduler/triggers/cron/expressions.py,sha256=89n_HxA0826xBJb8RprVzUDECs0dnZ_rX2wVkVsq6l8,9056
apscheduler/triggers/cron/fields.py,sha256=RVbf6Lcyvg-3CqNzEZsfxzQ_weONCIiq5LGDzA3JUAw,3618
apscheduler/triggers/date.py,sha256=ZS_TMjUCSldqlZsUUjlwvuWeMKeDXqqAMcZVFGYpam4,1698
apscheduler/triggers/interval.py,sha256=u6XLrxlaWA41zvIByQvRLHTAuvkibG2fAZAxrWK3118,4679
apscheduler/util.py,sha256=Lz2ddoeIpufXzW-HWnW5J08ijkXWGElDLVJf0DiPa84,13564

@ -1,5 +0,0 @@
Wheel-Version: 1.0
Generator: setuptools (75.6.0)
Root-Is-Purelib: true
Tag: py3-none-any

@ -1,25 +0,0 @@
[apscheduler.executors]
asyncio = apscheduler.executors.asyncio:AsyncIOExecutor
debug = apscheduler.executors.debug:DebugExecutor
gevent = apscheduler.executors.gevent:GeventExecutor
processpool = apscheduler.executors.pool:ProcessPoolExecutor
threadpool = apscheduler.executors.pool:ThreadPoolExecutor
tornado = apscheduler.executors.tornado:TornadoExecutor
twisted = apscheduler.executors.twisted:TwistedExecutor
[apscheduler.jobstores]
etcd = apscheduler.jobstores.etcd:EtcdJobStore
memory = apscheduler.jobstores.memory:MemoryJobStore
mongodb = apscheduler.jobstores.mongodb:MongoDBJobStore
redis = apscheduler.jobstores.redis:RedisJobStore
rethinkdb = apscheduler.jobstores.rethinkdb:RethinkDBJobStore
sqlalchemy = apscheduler.jobstores.sqlalchemy:SQLAlchemyJobStore
zookeeper = apscheduler.jobstores.zookeeper:ZooKeeperJobStore
[apscheduler.triggers]
and = apscheduler.triggers.combining:AndTrigger
calendarinterval = apscheduler.triggers.calendarinterval:CalendarIntervalTrigger
cron = apscheduler.triggers.cron:CronTrigger
date = apscheduler.triggers.date:DateTrigger
interval = apscheduler.triggers.interval:IntervalTrigger
or = apscheduler.triggers.combining:OrTrigger

@ -1,20 +0,0 @@
Copyright (c) 2017-2021 Ingy döt Net
Copyright (c) 2006-2016 Kirill Simonov
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -1,46 +0,0 @@
Metadata-Version: 2.1
Name: PyYAML
Version: 6.0.2
Summary: YAML parser and emitter for Python
Home-page: https://pyyaml.org/
Download-URL: https://pypi.org/project/PyYAML/
Author: Kirill Simonov
Author-email: xi@resolvent.net
License: MIT
Project-URL: Bug Tracker, https://github.com/yaml/pyyaml/issues
Project-URL: CI, https://github.com/yaml/pyyaml/actions
Project-URL: Documentation, https://pyyaml.org/wiki/PyYAMLDocumentation
Project-URL: Mailing lists, http://lists.sourceforge.net/lists/listinfo/yaml-core
Project-URL: Source Code, https://github.com/yaml/pyyaml
Platform: Any
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Cython
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Text Processing :: Markup
Requires-Python: >=3.8
License-File: LICENSE
YAML is a data serialization format designed for human readability
and interaction with scripting languages. PyYAML is a YAML parser
and emitter for Python.
PyYAML features a complete YAML 1.1 parser, Unicode support, pickle
support, capable extension API, and sensible error messages. PyYAML
supports standard YAML tags and provides Python-specific tags that
allow to represent an arbitrary Python object.
PyYAML is applicable for a broad range of tasks from complex
configuration files to object serialization and persistence.

@ -1,44 +0,0 @@
PyYAML-6.0.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
PyYAML-6.0.2.dist-info/LICENSE,sha256=jTko-dxEkP1jVwfLiOsmvXZBAqcoKVQwfT5RZ6V36KQ,1101
PyYAML-6.0.2.dist-info/METADATA,sha256=9lwXqTOrXPts-jI2Lo5UwuaAYo0hiRA0BZqjch0WjAk,2106
PyYAML-6.0.2.dist-info/RECORD,,
PyYAML-6.0.2.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
PyYAML-6.0.2.dist-info/WHEEL,sha256=yEpuRje-u1Z_HrXQj-UTAfIAegW_HcP2GJ7Ek8BJkUM,102
PyYAML-6.0.2.dist-info/top_level.txt,sha256=rpj0IVMTisAjh_1vG3Ccf9v5jpCQwAz6cD1IVU5ZdhQ,11
_yaml/__init__.py,sha256=04Ae_5osxahpJHa3XBZUAf4wi6XX32gR8D6X6p64GEA,1402
_yaml/__pycache__/__init__.cpython-311.pyc,,
yaml/__init__.py,sha256=N35S01HMesFTe0aRRMWkPj0Pa8IEbHpE9FK7cr5Bdtw,12311
yaml/__pycache__/__init__.cpython-311.pyc,,
yaml/__pycache__/composer.cpython-311.pyc,,
yaml/__pycache__/constructor.cpython-311.pyc,,
yaml/__pycache__/cyaml.cpython-311.pyc,,
yaml/__pycache__/dumper.cpython-311.pyc,,
yaml/__pycache__/emitter.cpython-311.pyc,,
yaml/__pycache__/error.cpython-311.pyc,,
yaml/__pycache__/events.cpython-311.pyc,,
yaml/__pycache__/loader.cpython-311.pyc,,
yaml/__pycache__/nodes.cpython-311.pyc,,
yaml/__pycache__/parser.cpython-311.pyc,,
yaml/__pycache__/reader.cpython-311.pyc,,
yaml/__pycache__/representer.cpython-311.pyc,,
yaml/__pycache__/resolver.cpython-311.pyc,,
yaml/__pycache__/scanner.cpython-311.pyc,,
yaml/__pycache__/serializer.cpython-311.pyc,,
yaml/__pycache__/tokens.cpython-311.pyc,,
yaml/_yaml.cp311-win_amd64.pyd,sha256=6BXrc7YC-BZJ911z64UDwJV3D0ay3GiyETPzbhl0iJc,272384
yaml/composer.py,sha256=_Ko30Wr6eDWUeUpauUGT3Lcg9QPBnOPVlTnIMRGJ9FM,4883
yaml/constructor.py,sha256=kNgkfaeLUkwQYY_Q6Ff1Tz2XVw_pG1xVE9Ak7z-viLA,28639
yaml/cyaml.py,sha256=6ZrAG9fAYvdVe2FK_w0hmXoG7ZYsoYUwapG8CiC72H0,3851
yaml/dumper.py,sha256=PLctZlYwZLp7XmeUdwRuv4nYOZ2UBnDIUy8-lKfLF-o,2837
yaml/emitter.py,sha256=jghtaU7eFwg31bG0B7RZea_29Adi9CKmXq_QjgQpCkQ,43006
yaml/error.py,sha256=Ah9z-toHJUbE9j-M8YpxgSRM5CgLCcwVzJgLLRF2Fxo,2533
yaml/events.py,sha256=50_TksgQiE4up-lKo_V-nBy-tAIxkIPQxY5qDhKCeHw,2445
yaml/loader.py,sha256=UVa-zIqmkFSCIYq_PgSGm4NSJttHY2Rf_zQ4_b1fHN0,2061
yaml/nodes.py,sha256=gPKNj8pKCdh2d4gr3gIYINnPOaOxGhJAUiYhGRnPE84,1440
yaml/parser.py,sha256=ilWp5vvgoHFGzvOZDItFoGjD6D42nhlZrZyjAwa0oJo,25495
yaml/reader.py,sha256=0dmzirOiDG4Xo41RnuQS7K9rkY3xjHiVasfDMNTqCNw,6794
yaml/representer.py,sha256=IuWP-cAW9sHKEnS0gCqSa894k1Bg4cgTxaDwIcbRQ-Y,14190
yaml/resolver.py,sha256=9L-VYfm4mWHxUD1Vg4X7rjDRK_7VZd6b92wzq7Y2IKY,9004
yaml/scanner.py,sha256=YEM3iLZSaQwXcQRg2l2R4MdT0zGP2F9eHkKGKnHyWQY,51279
yaml/serializer.py,sha256=ChuFgmhU01hj4xgI8GaKv6vfM2Bujwa9i7d2FAHj7cA,4165
yaml/tokens.py,sha256=lTQIzSVw8Mg9wv459-TjiOQe6wVziqaRlqX2_89rp54,2573

@ -1,5 +0,0 @@
Wheel-Version: 1.0
Generator: bdist_wheel (0.44.0)
Root-Is-Purelib: false
Tag: cp311-cp311-win_amd64

@ -1 +0,0 @@
This is a dummy package designed to prevent namesquatting on PyPI. You should install `beautifulsoup4 <https://pypi.python.org/pypi/beautifulsoup4>`_ instead.

@ -1,19 +0,0 @@
Copyright 2005-2024 SQLAlchemy authors and contributors <see AUTHORS file>.
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -1,243 +0,0 @@
Metadata-Version: 2.1
Name: SQLAlchemy
Version: 2.0.36
Summary: Database Abstraction Library
Home-page: https://www.sqlalchemy.org
Author: Mike Bayer
Author-email: mike_mp@zzzcomputing.com
License: MIT
Project-URL: Documentation, https://docs.sqlalchemy.org
Project-URL: Issue Tracker, https://github.com/sqlalchemy/sqlalchemy/
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: Topic :: Database :: Front-Ends
Requires-Python: >=3.7
Description-Content-Type: text/x-rst
License-File: LICENSE
Requires-Dist: typing-extensions >=4.6.0
Requires-Dist: greenlet !=0.4.17 ; python_version < "3.13" and (platform_machine == "aarch64" or (platform_machine == "ppc64le" or (platform_machine == "x86_64" or (platform_machine == "amd64" or (platform_machine == "AMD64" or (platform_machine == "win32" or platform_machine == "WIN32"))))))
Requires-Dist: importlib-metadata ; python_version < "3.8"
Provides-Extra: aiomysql
Requires-Dist: greenlet !=0.4.17 ; extra == 'aiomysql'
Requires-Dist: aiomysql >=0.2.0 ; extra == 'aiomysql'
Provides-Extra: aioodbc
Requires-Dist: greenlet !=0.4.17 ; extra == 'aioodbc'
Requires-Dist: aioodbc ; extra == 'aioodbc'
Provides-Extra: aiosqlite
Requires-Dist: greenlet !=0.4.17 ; extra == 'aiosqlite'
Requires-Dist: aiosqlite ; extra == 'aiosqlite'
Requires-Dist: typing-extensions !=3.10.0.1 ; extra == 'aiosqlite'
Provides-Extra: asyncio
Requires-Dist: greenlet !=0.4.17 ; extra == 'asyncio'
Provides-Extra: asyncmy
Requires-Dist: greenlet !=0.4.17 ; extra == 'asyncmy'
Requires-Dist: asyncmy !=0.2.4,!=0.2.6,>=0.2.3 ; extra == 'asyncmy'
Provides-Extra: mariadb_connector
Requires-Dist: mariadb !=1.1.10,!=1.1.2,!=1.1.5,>=1.0.1 ; extra == 'mariadb_connector'
Provides-Extra: mssql
Requires-Dist: pyodbc ; extra == 'mssql'
Provides-Extra: mssql_pymssql
Requires-Dist: pymssql ; extra == 'mssql_pymssql'
Provides-Extra: mssql_pyodbc
Requires-Dist: pyodbc ; extra == 'mssql_pyodbc'
Provides-Extra: mypy
Requires-Dist: mypy >=0.910 ; extra == 'mypy'
Provides-Extra: mysql
Requires-Dist: mysqlclient >=1.4.0 ; extra == 'mysql'
Provides-Extra: mysql_connector
Requires-Dist: mysql-connector-python ; extra == 'mysql_connector'
Provides-Extra: oracle
Requires-Dist: cx-oracle >=8 ; extra == 'oracle'
Provides-Extra: oracle_oracledb
Requires-Dist: oracledb >=1.0.1 ; extra == 'oracle_oracledb'
Provides-Extra: postgresql
Requires-Dist: psycopg2 >=2.7 ; extra == 'postgresql'
Provides-Extra: postgresql_asyncpg
Requires-Dist: greenlet !=0.4.17 ; extra == 'postgresql_asyncpg'
Requires-Dist: asyncpg ; extra == 'postgresql_asyncpg'
Provides-Extra: postgresql_pg8000
Requires-Dist: pg8000 >=1.29.1 ; extra == 'postgresql_pg8000'
Provides-Extra: postgresql_psycopg
Requires-Dist: psycopg >=3.0.7 ; extra == 'postgresql_psycopg'
Provides-Extra: postgresql_psycopg2binary
Requires-Dist: psycopg2-binary ; extra == 'postgresql_psycopg2binary'
Provides-Extra: postgresql_psycopg2cffi
Requires-Dist: psycopg2cffi ; extra == 'postgresql_psycopg2cffi'
Provides-Extra: postgresql_psycopgbinary
Requires-Dist: psycopg[binary] >=3.0.7 ; extra == 'postgresql_psycopgbinary'
Provides-Extra: pymysql
Requires-Dist: pymysql ; extra == 'pymysql'
Provides-Extra: sqlcipher
Requires-Dist: sqlcipher3-binary ; extra == 'sqlcipher'
SQLAlchemy
==========
|PyPI| |Python| |Downloads|
.. |PyPI| image:: https://img.shields.io/pypi/v/sqlalchemy
:target: https://pypi.org/project/sqlalchemy
:alt: PyPI
.. |Python| image:: https://img.shields.io/pypi/pyversions/sqlalchemy
:target: https://pypi.org/project/sqlalchemy
:alt: PyPI - Python Version
.. |Downloads| image:: https://static.pepy.tech/badge/sqlalchemy/month
:target: https://pepy.tech/project/sqlalchemy
:alt: PyPI - Downloads
The Python SQL Toolkit and Object Relational Mapper
Introduction
-------------
SQLAlchemy is the Python SQL toolkit and Object Relational Mapper
that gives application developers the full power and
flexibility of SQL. SQLAlchemy provides a full suite
of well known enterprise-level persistence patterns,
designed for efficient and high-performing database
access, adapted into a simple and Pythonic domain
language.
Major SQLAlchemy features include:
* An industrial strength ORM, built
from the core on the identity map, unit of work,
and data mapper patterns. These patterns
allow transparent persistence of objects
using a declarative configuration system.
Domain models
can be constructed and manipulated naturally,
and changes are synchronized with the
current transaction automatically.
* A relationally-oriented query system, exposing
the full range of SQL's capabilities
explicitly, including joins, subqueries,
correlation, and most everything else,
in terms of the object model.
Writing queries with the ORM uses the same
techniques of relational composition you use
when writing SQL. While you can drop into
literal SQL at any time, it's virtually never
needed.
* A comprehensive and flexible system
of eager loading for related collections and objects.
Collections are cached within a session,
and can be loaded on individual access, all
at once using joins, or by query per collection
across the full result set.
* A Core SQL construction system and DBAPI
interaction layer. The SQLAlchemy Core is
separate from the ORM and is a full database
abstraction layer in its own right, and includes
an extensible Python-based SQL expression
language, schema metadata, connection pooling,
type coercion, and custom types.
* All primary and foreign key constraints are
assumed to be composite and natural. Surrogate
integer primary keys are of course still the
norm, but SQLAlchemy never assumes or hardcodes
to this model.
* Database introspection and generation. Database
schemas can be "reflected" in one step into
Python structures representing database metadata;
those same structures can then generate
CREATE statements right back out - all within
the Core, independent of the ORM.
SQLAlchemy's philosophy:
* SQL databases behave less and less like object
collections the more size and performance start to
matter; object collections behave less and less like
tables and rows the more abstraction starts to matter.
SQLAlchemy aims to accommodate both of these
principles.
* An ORM doesn't need to hide the "R". A relational
database provides rich, set-based functionality
that should be fully exposed. SQLAlchemy's
ORM provides an open-ended set of patterns
that allow a developer to construct a custom
mediation layer between a domain model and
a relational schema, turning the so-called
"object relational impedance" issue into
a distant memory.
* The developer, in all cases, makes all decisions
regarding the design, structure, and naming conventions
of both the object model as well as the relational
schema. SQLAlchemy only provides the means
to automate the execution of these decisions.
* With SQLAlchemy, there's no such thing as
"the ORM generated a bad query" - you
retain full control over the structure of
queries, including how joins are organized,
how subqueries and correlation is used, what
columns are requested. Everything SQLAlchemy
does is ultimately the result of a developer-initiated
decision.
* Don't use an ORM if the problem doesn't need one.
SQLAlchemy consists of a Core and separate ORM
component. The Core offers a full SQL expression
language that allows Pythonic construction
of SQL constructs that render directly to SQL
strings for a target database, returning
result sets that are essentially enhanced DBAPI
cursors.
* Transactions should be the norm. With SQLAlchemy's
ORM, nothing goes to permanent storage until
commit() is called. SQLAlchemy encourages applications
to create a consistent means of delineating
the start and end of a series of operations.
* Never render a literal value in a SQL statement.
Bound parameters are used to the greatest degree
possible, allowing query optimizers to cache
query plans effectively and making SQL injection
attacks a non-issue.
Documentation
-------------
Latest documentation is at:
https://www.sqlalchemy.org/docs/
Installation / Requirements
---------------------------
Full documentation for installation is at
`Installation <https://www.sqlalchemy.org/docs/intro.html#installation>`_.
Getting Help / Development / Bug reporting
------------------------------------------
Please refer to the `SQLAlchemy Community Guide <https://www.sqlalchemy.org/support.html>`_.
Code of Conduct
---------------
Above all, SQLAlchemy places great emphasis on polite, thoughtful, and
constructive communication between users and developers.
Please see our current Code of Conduct at
`Code of Conduct <https://www.sqlalchemy.org/codeofconduct.html>`_.
License
-------
SQLAlchemy is distributed under the `MIT license
<https://www.opensource.org/licenses/mit-license.php>`_.

@ -1,530 +0,0 @@
SQLAlchemy-2.0.36.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
SQLAlchemy-2.0.36.dist-info/LICENSE,sha256=eYQKk6tEYK_iQW6ePf95YIdsg66dK-JwXoOhBNSXQOs,1119
SQLAlchemy-2.0.36.dist-info/METADATA,sha256=grqtAnGnYnQIU7y0X_EVSJjbaTjCEG1mMGnLYhj3Za4,9935
SQLAlchemy-2.0.36.dist-info/RECORD,,
SQLAlchemy-2.0.36.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
SQLAlchemy-2.0.36.dist-info/WHEEL,sha256=qW4RD1rfHm8ZRUjJbXUnZHDNPCXHt6Rq0mgR8lv_JEg,101
SQLAlchemy-2.0.36.dist-info/top_level.txt,sha256=rp-ZgB7D8G11ivXON5VGPjupT1voYmWqkciDt5Uaw_Q,11
sqlalchemy/__init__.py,sha256=PAN5IVcMVwzS9r2V524URkvNSjsiylsHp72xmxuVEvM,13327
sqlalchemy/__pycache__/__init__.cpython-311.pyc,,
sqlalchemy/__pycache__/events.cpython-311.pyc,,
sqlalchemy/__pycache__/exc.cpython-311.pyc,,
sqlalchemy/__pycache__/inspection.cpython-311.pyc,,
sqlalchemy/__pycache__/log.cpython-311.pyc,,
sqlalchemy/__pycache__/schema.cpython-311.pyc,,
sqlalchemy/__pycache__/types.cpython-311.pyc,,
sqlalchemy/connectors/__init__.py,sha256=A2AI8p63aT0jT5CsVX33xlTfiGWliOcGahlK0RyTLXg,494
sqlalchemy/connectors/__pycache__/__init__.cpython-311.pyc,,
sqlalchemy/connectors/__pycache__/aioodbc.cpython-311.pyc,,
sqlalchemy/connectors/__pycache__/asyncio.cpython-311.pyc,,
sqlalchemy/connectors/__pycache__/pyodbc.cpython-311.pyc,,
sqlalchemy/connectors/aioodbc.py,sha256=fg3xfG-5gLsy-DSyVonNNKYhOf0_lzHmixRFa5edtWI,5462
sqlalchemy/connectors/asyncio.py,sha256=EWjnlej7KeY7kDep36_ayJKl06_YnnOPWtUQKBZhgsY,6351
sqlalchemy/connectors/pyodbc.py,sha256=IG5lLCyFbnv1wB85HQuMO3S5piWHaB660OBWvBIQhbg,8750
sqlalchemy/cyextension/__init__.py,sha256=Hlfk91RinbOuNF_fybR5R2UtiIcTeUOXS66QOfSSCV0,250
sqlalchemy/cyextension/__pycache__/__init__.cpython-311.pyc,,
sqlalchemy/cyextension/collections.cp311-win_amd64.pyd,sha256=wQCKVM_Y1pc6qWQMokjHs3B_HD_zMdrSxzpYfcEopks,175616
sqlalchemy/cyextension/collections.pyx,sha256=GXPkr9cHRLW3Vcu-ik3dVBZMR-zf0Q5_K4J-_8yV-gk,12980
sqlalchemy/cyextension/immutabledict.cp311-win_amd64.pyd,sha256=5yHSG99wedmaRjwv-SIPMB8TFsDtkqNqBfS-6RMGM2U,73216
sqlalchemy/cyextension/immutabledict.pxd,sha256=5iGndSbJCgCkNmRbJ_z14RANs2dSSnAzyiRPUTBk58Y,299
sqlalchemy/cyextension/immutabledict.pyx,sha256=IhB2pR49CrORXQ3LXMFpuCIRc6I08QNvIylE1cPQA5o,3668
sqlalchemy/cyextension/processors.cp311-win_amd64.pyd,sha256=aQKjVv4NeM8dpBsOsRg4MdMw4z0Kv0A2LMH7y96a0LI,58880
sqlalchemy/cyextension/processors.pyx,sha256=V9gzqXiNHWsa5DBgYl-3KzclFHY8kXGF_TD1xHFE7eM,1860
sqlalchemy/cyextension/resultproxy.cp311-win_amd64.pyd,sha256=DHaO1-w221oyCGEiV8B7JTz8i-hXJW6CKEVBkkr-THU,60928
sqlalchemy/cyextension/resultproxy.pyx,sha256=h_RrKasbLtKK3LqUh6UiWtkumBlKtcN5eeB_1bZROMA,2827
sqlalchemy/cyextension/util.cp311-win_amd64.pyd,sha256=ms_vCfxgVanYMSxIOLWaof3gdV_IKP8vJ85E3IDQ4zU,73216
sqlalchemy/cyextension/util.pyx,sha256=50QYpSAKgLSUfhFEQgSN2e1qHWCMh_b6ZNlErDUS7ec,2621
sqlalchemy/dialects/__init__.py,sha256=SJfQyxMhOL58EB-S6GQv_0jf2oP7MMfmVdlV2UxGWQo,1831
sqlalchemy/dialects/__pycache__/__init__.cpython-311.pyc,,
sqlalchemy/dialects/__pycache__/_typing.cpython-311.pyc,,
sqlalchemy/dialects/_typing.py,sha256=mN2r8mU8z-mRh4YS3VeK8Nv_IKJmE0Mb1CrJ-ptILas,913
sqlalchemy/dialects/mssql/__init__.py,sha256=r3oTfX2LLbJAGhM57wdPLWxaZBzunkcmyaTbW0FjLuY,1968
sqlalchemy/dialects/mssql/__pycache__/__init__.cpython-311.pyc,,
sqlalchemy/dialects/mssql/__pycache__/aioodbc.cpython-311.pyc,,
sqlalchemy/dialects/mssql/__pycache__/base.cpython-311.pyc,,
sqlalchemy/dialects/mssql/__pycache__/information_schema.cpython-311.pyc,,
sqlalchemy/dialects/mssql/__pycache__/json.cpython-311.pyc,,
sqlalchemy/dialects/mssql/__pycache__/provision.cpython-311.pyc,,
sqlalchemy/dialects/mssql/__pycache__/pymssql.cpython-311.pyc,,
sqlalchemy/dialects/mssql/__pycache__/pyodbc.cpython-311.pyc,,
sqlalchemy/dialects/mssql/aioodbc.py,sha256=b9bhUKcVj4NzoqJIDfECeE_Rmt51sRy8OOUFz_R3vpg,2086
sqlalchemy/dialects/mssql/base.py,sha256=EBnNJv8RFOTci5tpgK2tUU_JUyGdew25j4edOosECj8,136433
sqlalchemy/dialects/mssql/information_schema.py,sha256=A1UJAoFb3UtE8YCY3heBgeTMkzWq3j7C2caZ3gcMGZk,8338
sqlalchemy/dialects/mssql/json.py,sha256=nZVVsgmR4Z4dNn9cv5Gucq596gsQ0MvASPuEEtz-Gek,4949
sqlalchemy/dialects/mssql/provision.py,sha256=jpyErAQ_zgmQKiE0G8dQcNn6O8ssQELrO2BzcukLUa0,5755
sqlalchemy/dialects/mssql/pymssql.py,sha256=RrxWm94UgCipZCM8FbeeENFndJh6JDaSkWPF3feK_VI,4223
sqlalchemy/dialects/mssql/pyodbc.py,sha256=YVI19AnrqxPCBwDqcjrO_rqUUWbV2re7E8iLuV1ilqE,27801
sqlalchemy/dialects/mysql/__init__.py,sha256=PPQDwNqcpxWMt3nFQ66KefX9T9iz7d8lybEwKlfXB1U,2254
sqlalchemy/dialects/mysql/__pycache__/__init__.cpython-311.pyc,,
sqlalchemy/dialects/mysql/__pycache__/aiomysql.cpython-311.pyc,,
sqlalchemy/dialects/mysql/__pycache__/asyncmy.cpython-311.pyc,,
sqlalchemy/dialects/mysql/__pycache__/base.cpython-311.pyc,,
sqlalchemy/dialects/mysql/__pycache__/cymysql.cpython-311.pyc,,
sqlalchemy/dialects/mysql/__pycache__/dml.cpython-311.pyc,,
sqlalchemy/dialects/mysql/__pycache__/enumerated.cpython-311.pyc,,
sqlalchemy/dialects/mysql/__pycache__/expression.cpython-311.pyc,,
sqlalchemy/dialects/mysql/__pycache__/json.cpython-311.pyc,,
sqlalchemy/dialects/mysql/__pycache__/mariadb.cpython-311.pyc,,
sqlalchemy/dialects/mysql/__pycache__/mariadbconnector.cpython-311.pyc,,
sqlalchemy/dialects/mysql/__pycache__/mysqlconnector.cpython-311.pyc,,
sqlalchemy/dialects/mysql/__pycache__/mysqldb.cpython-311.pyc,,
sqlalchemy/dialects/mysql/__pycache__/provision.cpython-311.pyc,,
sqlalchemy/dialects/mysql/__pycache__/pymysql.cpython-311.pyc,,
sqlalchemy/dialects/mysql/__pycache__/pyodbc.cpython-311.pyc,,
sqlalchemy/dialects/mysql/__pycache__/reflection.cpython-311.pyc,,
sqlalchemy/dialects/mysql/__pycache__/reserved_words.cpython-311.pyc,,
sqlalchemy/dialects/mysql/__pycache__/types.cpython-311.pyc,,
sqlalchemy/dialects/mysql/aiomysql.py,sha256=hZg2W3C_O2ExL5bVlcqmDX0vZbVR2dMyuGqTGVlBzig,10332
sqlalchemy/dialects/mysql/asyncmy.py,sha256=xnYyNzk6JJVzxpxu4B7GhUP9CR5rtHVw8mGoWsHAwT8,10404
sqlalchemy/dialects/mysql/base.py,sha256=yNKJCAwsapLTK5GBQZfzPA4DvTqee8_US9VY3brMwqI,126249
sqlalchemy/dialects/mysql/cymysql.py,sha256=0mRP3gFe2t7iJYQqJz1Os_TztFwMAF34w2MmXe-4B_w,2384
sqlalchemy/dialects/mysql/dml.py,sha256=n31-m4vfOIL0MdHpUdIfTLgaMzusfQ-yHYoJWO_ndEc,7864
sqlalchemy/dialects/mysql/enumerated.py,sha256=Nz9Sv3ENX-1T18aEoOY8QfZlAcwRf65lIOse7vwjil8,8692
sqlalchemy/dialects/mysql/expression.py,sha256=uxD1fICubfGh8BhAn6WoeS8AF6hAVEvreDShXqRZTqM,4238
sqlalchemy/dialects/mysql/json.py,sha256=i0Lrd_7VKTd3fNm6kQKzrtPERuW0JeSw7XSUWnl1HQI,2350
sqlalchemy/dialects/mysql/mariadb.py,sha256=WoNxkjiPfIbWAkrVEU9MTM7mePeLHZ2uiJsyfvcpv1s,885
sqlalchemy/dialects/mysql/mariadbconnector.py,sha256=wRuM0PPLnK1avjQnvbgC7pLf54JTondEpi-sffDWPMM,8900
sqlalchemy/dialects/mysql/mysqlconnector.py,sha256=w4a36lSTM8JHgohUoOsjqqaWh3822207gGUFGovkSRo,5909
sqlalchemy/dialects/mysql/mysqldb.py,sha256=2aFRw8C0LdU7hk6zjXDijEY8UeWK360TB8pTMZ5Ddto,9806
sqlalchemy/dialects/mysql/provision.py,sha256=ikb3a6XUnMa51JLmlIbDr9QLvTs3QtdyldHn-lME-qI,3685
sqlalchemy/dialects/mysql/pymysql.py,sha256=Kxi_A34-nbQ5UEFSmy14TXc1v43-1SZ8gE628REGTFo,4220
sqlalchemy/dialects/mysql/pyodbc.py,sha256=CZCEnhyLIgbuiAW32Cw7N1m1aiQv1eBB34pV-txOs70,4435
sqlalchemy/dialects/mysql/reflection.py,sha256=HQOQbNu9o977ki2Xpb7AbZ_mhAR9EK3YOGp3HoezJxg,23511
sqlalchemy/dialects/mysql/reserved_words.py,sha256=qzej7CIVFz2Q2ywue7nKL59cca2kzXhKpDOSQYMlxjU,9829
sqlalchemy/dialects/mysql/types.py,sha256=wqfI5QZ8__Uzn9cYefTMZ387cJJJgbNkCDE9Ax2k1pY,25117
sqlalchemy/dialects/oracle/__init__.py,sha256=_yFT_k0R6yc7MKQG-Al9QZt8wYZsiCtpkhNlba5xqn8,1560
sqlalchemy/dialects/oracle/__pycache__/__init__.cpython-311.pyc,,
sqlalchemy/dialects/oracle/__pycache__/base.cpython-311.pyc,,
sqlalchemy/dialects/oracle/__pycache__/cx_oracle.cpython-311.pyc,,
sqlalchemy/dialects/oracle/__pycache__/dictionary.cpython-311.pyc,,
sqlalchemy/dialects/oracle/__pycache__/oracledb.cpython-311.pyc,,
sqlalchemy/dialects/oracle/__pycache__/provision.cpython-311.pyc,,
sqlalchemy/dialects/oracle/__pycache__/types.cpython-311.pyc,,
sqlalchemy/dialects/oracle/base.py,sha256=bigUmqUbddoa9vCYvrkajlDiYsFhNdAIpkfi7q0JtzY,122947
sqlalchemy/dialects/oracle/cx_oracle.py,sha256=5DyLekZ1IV1-KQdtMHEyt-AtHDq1RLX-XN-N6zPEywA,56718
sqlalchemy/dialects/oracle/dictionary.py,sha256=tmAZLEACqBAPBE0SEV2jr1R4aPcpNOrbomJl-UmgiR4,20026
sqlalchemy/dialects/oracle/oracledb.py,sha256=nWB80zaRoUl9uJ3rjvCnvifeqfNiCzFmDi6uSr6wEkk,14050
sqlalchemy/dialects/oracle/provision.py,sha256=KKlXDQnC8n6BjLJWA7AJg3lwXluH1OyStqfP2Uf9rq0,8524
sqlalchemy/dialects/oracle/types.py,sha256=U9EReFRcr0PiwOxT9vg2cA7WOix8LQ2sVp0gRkMHcPo,8518
sqlalchemy/dialects/postgresql/__init__.py,sha256=C0BhKzUkClwGfAetbBIEd4KQohOtSmfHo5TwhoZLCK0,4059
sqlalchemy/dialects/postgresql/__pycache__/__init__.cpython-311.pyc,,
sqlalchemy/dialects/postgresql/__pycache__/_psycopg_common.cpython-311.pyc,,
sqlalchemy/dialects/postgresql/__pycache__/array.cpython-311.pyc,,
sqlalchemy/dialects/postgresql/__pycache__/asyncpg.cpython-311.pyc,,
sqlalchemy/dialects/postgresql/__pycache__/base.cpython-311.pyc,,
sqlalchemy/dialects/postgresql/__pycache__/dml.cpython-311.pyc,,
sqlalchemy/dialects/postgresql/__pycache__/ext.cpython-311.pyc,,
sqlalchemy/dialects/postgresql/__pycache__/hstore.cpython-311.pyc,,
sqlalchemy/dialects/postgresql/__pycache__/json.cpython-311.pyc,,
sqlalchemy/dialects/postgresql/__pycache__/named_types.cpython-311.pyc,,
sqlalchemy/dialects/postgresql/__pycache__/operators.cpython-311.pyc,,
sqlalchemy/dialects/postgresql/__pycache__/pg8000.cpython-311.pyc,,
sqlalchemy/dialects/postgresql/__pycache__/pg_catalog.cpython-311.pyc,,
sqlalchemy/dialects/postgresql/__pycache__/provision.cpython-311.pyc,,
sqlalchemy/dialects/postgresql/__pycache__/psycopg.cpython-311.pyc,,
sqlalchemy/dialects/postgresql/__pycache__/psycopg2.cpython-311.pyc,,
sqlalchemy/dialects/postgresql/__pycache__/psycopg2cffi.cpython-311.pyc,,
sqlalchemy/dialects/postgresql/__pycache__/ranges.cpython-311.pyc,,
sqlalchemy/dialects/postgresql/__pycache__/types.cpython-311.pyc,,
sqlalchemy/dialects/postgresql/_psycopg_common.py,sha256=fYFqLVxNxAqh3nOvzGOv3Pfpm2BsclHrk71MJZrpJKo,5883
sqlalchemy/dialects/postgresql/array.py,sha256=_vzfyGBY1NsT6blooCgHrLC38VZbM4UWKQHgXLmmyYs,14159
sqlalchemy/dialects/postgresql/asyncpg.py,sha256=mT_-cDcHx_ERtn8LEZEiafOhrBySKEkwnrdq7grSGj0,42348
sqlalchemy/dialects/postgresql/base.py,sha256=Tf0ADIFFeuIuPmFhtcvw7gSj4WOq65efZ_bxd-puXWU,184091
sqlalchemy/dialects/postgresql/dml.py,sha256=uMiqxEkji-UXqk8gO1ramQEvEfCugYmy8Cv1cnG7DQs,11522
sqlalchemy/dialects/postgresql/ext.py,sha256=ct6NQfMAfBnLYhybpF2wPEq-p8-U0tEpy-aq8NwqJLw,16758
sqlalchemy/dialects/postgresql/hstore.py,sha256=4jAZQMPWl3VE4weDRZrgrbVDRZJTM3X0Xj4twr5znYQ,11938
sqlalchemy/dialects/postgresql/json.py,sha256=uYTzL3gECnQ8MGlH76MG7lgOzudWJwEVAukzVBeEaUw,11951
sqlalchemy/dialects/postgresql/named_types.py,sha256=Ykl4GWSf5pQynkAWfZsAjgYU0R_TSvEvaZrb8mI4PuQ,18103
sqlalchemy/dialects/postgresql/operators.py,sha256=iyZuyx_daRyJjiS5rw-XnZlaWj1bmRiHdy5MXzBrFZw,2937
sqlalchemy/dialects/postgresql/pg8000.py,sha256=TPJXX078vW0FSwZ-DlWNkEOXg7Z4xk8IFwi1droMhPw,19302
sqlalchemy/dialects/postgresql/pg_catalog.py,sha256=rG_AGLtjSQ6DAnkqAiurYpnIuLhN9Ib_QydWbmjK--s,9554
sqlalchemy/dialects/postgresql/provision.py,sha256=8ieCPOOsTLLcf2qa4zasAu7UCJ9S7rK5xl2_u_OkB2w,5945
sqlalchemy/dialects/postgresql/psycopg.py,sha256=MtS8vtJB0jStB1nILco6STUfdHv4m8rbWisXBzVXiO4,23940
sqlalchemy/dialects/postgresql/psycopg2.py,sha256=Jczz-5NGNYlkmkDlJYCpXRQSmNszqgZZaX6yqwc0vvg,32884
sqlalchemy/dialects/postgresql/psycopg2cffi.py,sha256=hFg-9GH08ApPy3foVPUdJKwCEzNSv2zD5l4nH97AqgI,1817
sqlalchemy/dialects/postgresql/ranges.py,sha256=oiTmnZ-hd5WqqGNsXbuOJfoNxpbso_M_49gky8dlCrE,33978
sqlalchemy/dialects/postgresql/types.py,sha256=pd1QmuGwJFLqpY2tK-Ql3FNjtT1Ha-lVvfaR9dimvHc,7603
sqlalchemy/dialects/sqlite/__init__.py,sha256=MmQfjHun1U_4q-Dq_yhs9RzAX0VLixSwWeY5xWiDwag,1239
sqlalchemy/dialects/sqlite/__pycache__/__init__.cpython-311.pyc,,
sqlalchemy/dialects/sqlite/__pycache__/aiosqlite.cpython-311.pyc,,
sqlalchemy/dialects/sqlite/__pycache__/base.cpython-311.pyc,,
sqlalchemy/dialects/sqlite/__pycache__/dml.cpython-311.pyc,,
sqlalchemy/dialects/sqlite/__pycache__/json.cpython-311.pyc,,
sqlalchemy/dialects/sqlite/__pycache__/provision.cpython-311.pyc,,
sqlalchemy/dialects/sqlite/__pycache__/pysqlcipher.cpython-311.pyc,,
sqlalchemy/dialects/sqlite/__pycache__/pysqlite.cpython-311.pyc,,
sqlalchemy/dialects/sqlite/aiosqlite.py,sha256=q9NcdCMnlOMiX2ySw-lqzZB0X8_nYoCmHES9SiIy9v0,12741
sqlalchemy/dialects/sqlite/base.py,sha256=SCliffPlfv08INhTwnGhYUYLmxI5aqiMgeoqLnlZB20,100616
sqlalchemy/dialects/sqlite/dml.py,sha256=8JV6Ise7WtmFniy590X5b19AYZcE51M6N5hef7d9JoA,8683
sqlalchemy/dialects/sqlite/json.py,sha256=-9afZnBt07vInCX20CKzjlTG85wHTO5_cxhcYU4phDc,2869
sqlalchemy/dialects/sqlite/provision.py,sha256=nAXZPEjXFrb6a1LxXZMqKmkQoXgl3MPsSHuMyBQ76NU,5830
sqlalchemy/dialects/sqlite/pysqlcipher.py,sha256=p0KfzHBwANDMwKTKEJCjR5RxMYqQwS4E8KXjl3Bx6Fw,5511
sqlalchemy/dialects/sqlite/pysqlite.py,sha256=TcApeaXMjcM-IMRNEp4sQn-lE3Z6khLvoe7L63k7TN4,28824
sqlalchemy/dialects/type_migration_guidelines.txt,sha256=gyh3JCauAIFi_9XEfqm3vYv_jb2Eqcz2HjpmC9ZEPMM,8384
sqlalchemy/engine/__init__.py,sha256=93FWhb62dLCidc6e4FE65wq_P8GeoWQG1OG6RZMBqhM,2880
sqlalchemy/engine/__pycache__/__init__.cpython-311.pyc,,
sqlalchemy/engine/__pycache__/_py_processors.cpython-311.pyc,,
sqlalchemy/engine/__pycache__/_py_row.cpython-311.pyc,,
sqlalchemy/engine/__pycache__/_py_util.cpython-311.pyc,,
sqlalchemy/engine/__pycache__/base.cpython-311.pyc,,
sqlalchemy/engine/__pycache__/characteristics.cpython-311.pyc,,
sqlalchemy/engine/__pycache__/create.cpython-311.pyc,,
sqlalchemy/engine/__pycache__/cursor.cpython-311.pyc,,
sqlalchemy/engine/__pycache__/default.cpython-311.pyc,,
sqlalchemy/engine/__pycache__/events.cpython-311.pyc,,
sqlalchemy/engine/__pycache__/interfaces.cpython-311.pyc,,
sqlalchemy/engine/__pycache__/mock.cpython-311.pyc,,
sqlalchemy/engine/__pycache__/processors.cpython-311.pyc,,
sqlalchemy/engine/__pycache__/reflection.cpython-311.pyc,,
sqlalchemy/engine/__pycache__/result.cpython-311.pyc,,
sqlalchemy/engine/__pycache__/row.cpython-311.pyc,,
sqlalchemy/engine/__pycache__/strategies.cpython-311.pyc,,
sqlalchemy/engine/__pycache__/url.cpython-311.pyc,,
sqlalchemy/engine/__pycache__/util.cpython-311.pyc,,
sqlalchemy/engine/_py_processors.py,sha256=-jlAYPM6etmuKeViiI7BD41kqY0Pr8nzaox22TPqCCQ,3880
sqlalchemy/engine/_py_row.py,sha256=UEGCjAeRsggcUn0QB0PdFC82kuykrOiOZ1KGq_Gf_qQ,3915
sqlalchemy/engine/_py_util.py,sha256=nh1XoVq1b-eGgkdzbqFqzje0RNSmVWotoa6yaB7J5Sw,2558
sqlalchemy/engine/base.py,sha256=ags97hnejh9awIFQ5t1RkHnZOSDJSUiSeaOqrPAU_4k,126333
sqlalchemy/engine/characteristics.py,sha256=hfTuHv_WxSNHOS_LFIypSb27lqoZKRXIQKyT7ay5lTY,4920
sqlalchemy/engine/create.py,sha256=-SCpvMx3DIwt8TD6Cyh_ChieKQ8y3hDD9YLDBIKgY6o,34081
sqlalchemy/engine/cursor.py,sha256=zKN-AiwE-0KkvdWqhg8dOAT4R1U1a4I0tFgAqzihSnE,78573
sqlalchemy/engine/default.py,sha256=vut1eoGPPFksx3iMTr1MNzLvM52GNgqzm9sZuvTVt24,87013
sqlalchemy/engine/events.py,sha256=e0VHj69fH20sB7gocBhr5Rs2FjR8ioY4iE8VQt70oJg,38332
sqlalchemy/engine/interfaces.py,sha256=a7kJmfaTRQgBvoUve_86CFI2tWGF-Htv_OQBJHCCQtU,116339
sqlalchemy/engine/mock.py,sha256=wInBRiHwydTc5ELQLivdezDd1ikbSMVXgLVzZrSC0iQ,4310
sqlalchemy/engine/processors.py,sha256=w4MiVMlU6VvfhIW49nygbHcwX8FteGpz7g3IGEqtZb8,2440
sqlalchemy/engine/reflection.py,sha256=DYyhp221HdpnjtEKvIXoHFj6LvkUntMpK3gc7FwkhZ4,77462
sqlalchemy/engine/result.py,sha256=ItLliL4T31CSD1cTgbVmliEQBtRtneDCqChAM00iiSM,80033
sqlalchemy/engine/row.py,sha256=g7ZqmsqX_BtRUzY-zfXoZZ4-5xZ_KJEVbvqKHUIlqRg,12433
sqlalchemy/engine/strategies.py,sha256=fD4DJn0AD371wlUa7s5Sy4j7QtgGyP7gMy_kUyqCLDQ,461
sqlalchemy/engine/url.py,sha256=tOCRmKkqrpsIfNeSDoy6KKTLtQAMtoIn9xa5kmJQebk,31694
sqlalchemy/engine/util.py,sha256=wIrPulEwr7kAxJ-vNj5QSPGNfWoFx5B4zzJdXu_ZIFg,5849
sqlalchemy/event/__init__.py,sha256=09qZzHwt0PkIDsPwuPUVJvNakjtCBjuUJeY0AEJ9j7k,1022
sqlalchemy/event/__pycache__/__init__.cpython-311.pyc,,
sqlalchemy/event/__pycache__/api.cpython-311.pyc,,
sqlalchemy/event/__pycache__/attr.cpython-311.pyc,,
sqlalchemy/event/__pycache__/base.cpython-311.pyc,,
sqlalchemy/event/__pycache__/legacy.cpython-311.pyc,,
sqlalchemy/event/__pycache__/registry.cpython-311.pyc,,
sqlalchemy/event/api.py,sha256=I7XWFczjgl3RcBH_52TbQ0S3_W4RZz1Be9dXkxtFw5U,8451
sqlalchemy/event/attr.py,sha256=-SHjzXMOs7IICPSgNwpgRS3FIEeLIpB5PyvVlpw8Gp8,21406
sqlalchemy/event/base.py,sha256=5JA45j3ncMWPXDeIPLJ_D5lX06Z3TJf8dla8bCriOkU,15597
sqlalchemy/event/legacy.py,sha256=a8VEvS83PvgbomNnaSa3okZmTkxl_buZ7Lfilechjh8,8473
sqlalchemy/event/registry.py,sha256=f31k0FLqIlWpOK9tksiYXnv-yuZPPz9iLQqvKEYV7ko,11221
sqlalchemy/events.py,sha256=OAy8TK21lWzSe8bDUnAbmsP82bsBYy0LL19hR6y3BrM,542
sqlalchemy/exc.py,sha256=k01TD2xp2BM3DrXdo2U5r8yuRfsoqBND4kwvtD1SVN0,24806
sqlalchemy/ext/__init__.py,sha256=YbMQmRS_9HxRyWM-KA_F76WOss1_Em1ZcrnQDIDXoOc,333
sqlalchemy/ext/__pycache__/__init__.cpython-311.pyc,,
sqlalchemy/ext/__pycache__/associationproxy.cpython-311.pyc,,
sqlalchemy/ext/__pycache__/automap.cpython-311.pyc,,
sqlalchemy/ext/__pycache__/baked.cpython-311.pyc,,
sqlalchemy/ext/__pycache__/compiler.cpython-311.pyc,,
sqlalchemy/ext/__pycache__/horizontal_shard.cpython-311.pyc,,
sqlalchemy/ext/__pycache__/hybrid.cpython-311.pyc,,
sqlalchemy/ext/__pycache__/indexable.cpython-311.pyc,,
sqlalchemy/ext/__pycache__/instrumentation.cpython-311.pyc,,
sqlalchemy/ext/__pycache__/mutable.cpython-311.pyc,,
sqlalchemy/ext/__pycache__/orderinglist.cpython-311.pyc,,
sqlalchemy/ext/__pycache__/serializer.cpython-311.pyc,,
sqlalchemy/ext/associationproxy.py,sha256=4eT25cDGBqCExY3ey1HtQv1n-EtFdTIXpRRN0bt2Ga4,68075
sqlalchemy/ext/asyncio/__init__.py,sha256=tKYIrERYf8hov9m8DuKWRO_53qhrvj2jRmIYjSGQ2Po,1342
sqlalchemy/ext/asyncio/__pycache__/__init__.cpython-311.pyc,,
sqlalchemy/ext/asyncio/__pycache__/base.cpython-311.pyc,,
sqlalchemy/ext/asyncio/__pycache__/engine.cpython-311.pyc,,
sqlalchemy/ext/asyncio/__pycache__/exc.cpython-311.pyc,,
sqlalchemy/ext/asyncio/__pycache__/result.cpython-311.pyc,,
sqlalchemy/ext/asyncio/__pycache__/scoping.cpython-311.pyc,,
sqlalchemy/ext/asyncio/__pycache__/session.cpython-311.pyc,,
sqlalchemy/ext/asyncio/base.py,sha256=slWQTFdgQQlkzrnx3m5a9xT8IRg4iM0gkEbypXr_YXQ,9184
sqlalchemy/ext/asyncio/engine.py,sha256=HJ5IZD0_xfVOMEGYZ1XtDir73SpzBk6ODDUN75ltvzo,49656
sqlalchemy/ext/asyncio/exc.py,sha256=0awLfUB4PhEPVVTKYluyor1tW91GPZZnvdQ-GGSOmJY,660
sqlalchemy/ext/asyncio/result.py,sha256=2hCQKOjbmFCYfEjk33FY1ZUAPQaoDmgoIW8IzqE63Bg,31438
sqlalchemy/ext/asyncio/scoping.py,sha256=XZqvqtIXpWujkjV7DBikIsQcjaVLei1C3LlflZxiZ_o,54222
sqlalchemy/ext/asyncio/session.py,sha256=_9J2zd9-tc4YNKjbe6JQkMNXD_PI-l84CbHRNA0OSGE,65023
sqlalchemy/ext/automap.py,sha256=dIo-IoF9t6xcrBj1EV5caHPmPISXXzH-637utGlqi00,63272
sqlalchemy/ext/baked.py,sha256=jc6vPocoXXsvdZsOsqgT4kG6guWSZD1TdPjoRBmkbRU,18381
sqlalchemy/ext/compiler.py,sha256=iIc87FNm-rgbGMsST5nskNkkQe1WE-cvpgd2g8zobnM,21447
sqlalchemy/ext/declarative/__init__.py,sha256=MHSOffOS4MWcqshAuLNQv0vDXpK_Z3lpGXTm1riyLls,1883
sqlalchemy/ext/declarative/__pycache__/__init__.cpython-311.pyc,,
sqlalchemy/ext/declarative/__pycache__/extensions.cpython-311.pyc,,
sqlalchemy/ext/declarative/extensions.py,sha256=aPpW0PvTKH3CoSMhsOY5GcUMZOVq-OFsV1hflxmb3Lw,20095
sqlalchemy/ext/horizontal_shard.py,sha256=V8vXEt5ZQb_PM39agZD2IyoQNGSqVI1MhY-6mNV5MRY,17231
sqlalchemy/ext/hybrid.py,sha256=UpWd9hOD5I3hCT5FJW9twSDyShDVyygRIFtv-FmMHgM,53972
sqlalchemy/ext/indexable.py,sha256=aDlVpN4rilRrer9qKg3kO7fqnqB5NX4M5qzYuYM8pvw,11373
sqlalchemy/ext/instrumentation.py,sha256=lFsJECWlN1oc1E0r9TaQDZcxAx4VOz6PSHYrl5fLk9Y,16157
sqlalchemy/ext/mutable.py,sha256=nAz3_lF2xkYSARt7GAWQh-OUMcnpe6s1ocjvQGxCPkc,38428
sqlalchemy/ext/mypy/__init__.py,sha256=aqT8_9sNwzC8PIaEZ4zkCYGBvYPaDD3eCgJtJuk3g6A,247
sqlalchemy/ext/mypy/__pycache__/__init__.cpython-311.pyc,,
sqlalchemy/ext/mypy/__pycache__/apply.cpython-311.pyc,,
sqlalchemy/ext/mypy/__pycache__/decl_class.cpython-311.pyc,,
sqlalchemy/ext/mypy/__pycache__/infer.cpython-311.pyc,,
sqlalchemy/ext/mypy/__pycache__/names.cpython-311.pyc,,
sqlalchemy/ext/mypy/__pycache__/plugin.cpython-311.pyc,,
sqlalchemy/ext/mypy/__pycache__/util.cpython-311.pyc,,
sqlalchemy/ext/mypy/apply.py,sha256=1Qb-_FpQ_0LVB2KFA5hVjfPv6DDMIcxXe86Ts1X9GBk,10870
sqlalchemy/ext/mypy/decl_class.py,sha256=f2iWiFVlDFqGb_IoGGotI3IEOUErh25sLT7B_cMfx0g,17899
sqlalchemy/ext/mypy/infer.py,sha256=O-3IjELDSBEAwGGxRM7lr0NWwGD0HMK4vda_iY6iwjs,19959
sqlalchemy/ext/mypy/names.py,sha256=2bHYuQJe71c9JtuJSZP-WWGiOJmY9-FuMQGMDBB6dRs,10814
sqlalchemy/ext/mypy/plugin.py,sha256=TDTziLsYFRqyX8UcQMtBBa6TFR4z9N-XNO8wRkHlEOI,10053
sqlalchemy/ext/mypy/util.py,sha256=s3-QXtuJ5lBZHdhpMdCa36EBV-wic5Dl1hFOB_2bVqw,10317
sqlalchemy/ext/orderinglist.py,sha256=r7La_3nZlGevIgsBL1IB30FvWO_tZHlTKo_FWwid-aY,14800
sqlalchemy/ext/serializer.py,sha256=4DuLXwWsuZU8vyFw2kOmuelSXozpCgr_pH36af1mtVE,6321
sqlalchemy/future/__init__.py,sha256=6-qPdjMHX-V-kAPjTQgNuHztmYiwKlJhKhhljuETvoQ,528
sqlalchemy/future/__pycache__/__init__.cpython-311.pyc,,
sqlalchemy/future/__pycache__/engine.cpython-311.pyc,,
sqlalchemy/future/engine.py,sha256=N_5W2ab5-ueedWzqNdgLPzTW9audT1IbxF6FCDLRZOc,510
sqlalchemy/inspection.py,sha256=GpmMuSAZ53u4W__iGpvzQKCBMFnTxnHt4Lo7Nq1FSKM,5237
sqlalchemy/log.py,sha256=Sg6PGR_wmseiCCpJfRDEkaMs08XTPPsf0X_iYJLvzS0,8895
sqlalchemy/orm/__init__.py,sha256=I-XesvuyjkAAwnsiF5FnXRLNV6W2nW70EnGAIt2GAjU,8633
sqlalchemy/orm/__pycache__/__init__.cpython-311.pyc,,
sqlalchemy/orm/__pycache__/_orm_constructors.cpython-311.pyc,,
sqlalchemy/orm/__pycache__/_typing.cpython-311.pyc,,
sqlalchemy/orm/__pycache__/attributes.cpython-311.pyc,,
sqlalchemy/orm/__pycache__/base.cpython-311.pyc,,
sqlalchemy/orm/__pycache__/bulk_persistence.cpython-311.pyc,,
sqlalchemy/orm/__pycache__/clsregistry.cpython-311.pyc,,
sqlalchemy/orm/__pycache__/collections.cpython-311.pyc,,
sqlalchemy/orm/__pycache__/context.cpython-311.pyc,,
sqlalchemy/orm/__pycache__/decl_api.cpython-311.pyc,,
sqlalchemy/orm/__pycache__/decl_base.cpython-311.pyc,,
sqlalchemy/orm/__pycache__/dependency.cpython-311.pyc,,
sqlalchemy/orm/__pycache__/descriptor_props.cpython-311.pyc,,
sqlalchemy/orm/__pycache__/dynamic.cpython-311.pyc,,
sqlalchemy/orm/__pycache__/evaluator.cpython-311.pyc,,
sqlalchemy/orm/__pycache__/events.cpython-311.pyc,,
sqlalchemy/orm/__pycache__/exc.cpython-311.pyc,,
sqlalchemy/orm/__pycache__/identity.cpython-311.pyc,,
sqlalchemy/orm/__pycache__/instrumentation.cpython-311.pyc,,
sqlalchemy/orm/__pycache__/interfaces.cpython-311.pyc,,
sqlalchemy/orm/__pycache__/loading.cpython-311.pyc,,
sqlalchemy/orm/__pycache__/mapped_collection.cpython-311.pyc,,
sqlalchemy/orm/__pycache__/mapper.cpython-311.pyc,,
sqlalchemy/orm/__pycache__/path_registry.cpython-311.pyc,,
sqlalchemy/orm/__pycache__/persistence.cpython-311.pyc,,
sqlalchemy/orm/__pycache__/properties.cpython-311.pyc,,
sqlalchemy/orm/__pycache__/query.cpython-311.pyc,,
sqlalchemy/orm/__pycache__/relationships.cpython-311.pyc,,
sqlalchemy/orm/__pycache__/scoping.cpython-311.pyc,,
sqlalchemy/orm/__pycache__/session.cpython-311.pyc,,
sqlalchemy/orm/__pycache__/state.cpython-311.pyc,,
sqlalchemy/orm/__pycache__/state_changes.cpython-311.pyc,,
sqlalchemy/orm/__pycache__/strategies.cpython-311.pyc,,
sqlalchemy/orm/__pycache__/strategy_options.cpython-311.pyc,,
sqlalchemy/orm/__pycache__/sync.cpython-311.pyc,,
sqlalchemy/orm/__pycache__/unitofwork.cpython-311.pyc,,
sqlalchemy/orm/__pycache__/util.cpython-311.pyc,,
sqlalchemy/orm/__pycache__/writeonly.cpython-311.pyc,,
sqlalchemy/orm/_orm_constructors.py,sha256=7JCWOOTvMSa0drFcJVb8H2LP1Zme9EmklPtVPn2AKJA,106005
sqlalchemy/orm/_typing.py,sha256=Z9GZT8Vb-wFwvHeOeVE37dvmCWdItLZnqI_pLin4cMc,5152
sqlalchemy/orm/attributes.py,sha256=4PcKfGsqziKh-odMCSoLP37lCzVT27n8qrXuDEDZq74,95369
sqlalchemy/orm/base.py,sha256=SKMbYGIgGcuqwQeteHN-G0sS1d_gx2dSxMjuZmBlYm0,28475
sqlalchemy/orm/bulk_persistence.py,sha256=EZDT2IUFjuVLMra1urGiZSh99aWjUVrua0tBpz9f_lY,74786
sqlalchemy/orm/clsregistry.py,sha256=2pE9BCnPEDvid_2zgFHllr2CTG_jLcx_ThdZo6zJ0ew,18545
sqlalchemy/orm/collections.py,sha256=UOBbkTc4jCMbqM7fb9277VR1fCN5TRaFKV8pAcNyv78,53863
sqlalchemy/orm/context.py,sha256=3egDf1kEB41tiNByqVj5_cUG4AcYDoYTZhxICBSAugA,116223
sqlalchemy/orm/decl_api.py,sha256=i6mXKqNrCxu3bJKhhBt_KtsPZHVVDHemAFEQpUdQvAM,65881
sqlalchemy/orm/decl_base.py,sha256=UpBoWt4f3mY6hsu46CFWXKju6c9qM-M5jHvSEHftgZ4,85533
sqlalchemy/orm/dependency.py,sha256=glstmbB4t-PIRA47u9NgTyyxbENfyQuG9Uzj2iezB_s,48935
sqlalchemy/orm/descriptor_props.py,sha256=uRIgZMIRFACONzgiXz6q6wdvahby2s1ogkjHARhEVFk,38320
sqlalchemy/orm/dynamic.py,sha256=MMPj4Esn_CRweI8YMbYcl9nNwcMWB4O6owOaFgcIvB0,10116
sqlalchemy/orm/evaluator.py,sha256=Uttss9NiHt12sWWjH4XfK8hzQANciaefXwFf7On2cns,12732
sqlalchemy/orm/events.py,sha256=8K_DUstcftizTDyYomGXo_GLbYdUG526JYQa9b5D7DA,131038
sqlalchemy/orm/exc.py,sha256=fd24WdW3CP3oxFcz9CLXPeBIAfqJZbKv7K4G-5X4EOg,7641
sqlalchemy/orm/identity.py,sha256=fOpANTf73r12F_w9DhVoyjkAdh8ldgJcNnwxx0GY8YM,9551
sqlalchemy/orm/instrumentation.py,sha256=a8vi3qEAyO7Z9PYksLkFi_YzxqQhzB-anblegiAtsFw,25075
sqlalchemy/orm/interfaces.py,sha256=c7rAWvsF8xnae19U50OTvuzb_I4n88FH_FgqPtd_sr0,50164
sqlalchemy/orm/loading.py,sha256=RCEUmTS--9QntLQcnwYVxHRsC3ILE58AVNHb8MGcybg,59959
sqlalchemy/orm/mapped_collection.py,sha256=OjiojLC9caz_-0vt2289sS35bGOiM6GbxWywCg1RFZc,20239
sqlalchemy/orm/mapper.py,sha256=5DItf0WognEURCJ3-tInhtYdijDN8jNZEwOns43z-C8,176170
sqlalchemy/orm/path_registry.py,sha256=GQS4KatFTi_6LKdi6I4185igHE6DDwrm8b1AMOHMom4,26731
sqlalchemy/orm/persistence.py,sha256=MKb7TuSLJUQpyqnHxf6uNmGXSznmZgkkFTD04nHbNUQ,63483
sqlalchemy/orm/properties.py,sha256=DicF2OHW-b5z7xJrahELKd5OPm9cVfTIYjQcoEM1naw,30192
sqlalchemy/orm/query.py,sha256=KgFWo2xCpF26VHRD3oRLNvbckekuORcn1LPTjguDCZ0,121104
sqlalchemy/orm/relationships.py,sha256=tj00j1ISHQmJbWeegWn8U29lL6WUnqd8fdY7NRHcLWU,132144
sqlalchemy/orm/scoping.py,sha256=Na86AVpI1bry0ICm67wXT7CmCffVu3Muoorn9c-hLNk,80853
sqlalchemy/orm/session.py,sha256=EDUpkGw5rx66579JfvtME2AeqGsdh1V2nznuc3dgDfc,201280
sqlalchemy/orm/state.py,sha256=TB9954ZttIUj4uMF-4tfocwaDaq9g1uCbgXuuk-VwJA,38813
sqlalchemy/orm/state_changes.py,sha256=4i90vDgBGvVGUzhlonlBkZBAZFOWaAXij2X8OEA3-BA,7013
sqlalchemy/orm/strategies.py,sha256=QcSrpN8wsobzKYThvbNhnbVhz6-AfUvUuPjKpeK1U9w,123339
sqlalchemy/orm/strategy_options.py,sha256=uajUK7bpjJGl8ygbbuzChwXiJEUd6dEHtjXEnqY1dlw,87922
sqlalchemy/orm/sync.py,sha256=aMEMhYTj2rtJZJvjqm-cUx2CoQxYl8P6YddCLpLelhM,5943
sqlalchemy/orm/unitofwork.py,sha256=THggzzAaqmYh5PBDob5dHTP_YyHXYdscs3fIxtRV-gE,27829
sqlalchemy/orm/util.py,sha256=Ju6SEzkmZvDM1N7jmYkB6pImZHRsCqn-k7Nj6Wlhgjg,83445
sqlalchemy/orm/writeonly.py,sha256=j5DcpZKOv1tLGQLhKfk-Uw-B0yEG7LezwJWNTq0FtWQ,22983
sqlalchemy/pool/__init__.py,sha256=ZKUPMKdBU57mhu677UsvRs5Aq9s9BwIbMmSNRoTRPoY,1848
sqlalchemy/pool/__pycache__/__init__.cpython-311.pyc,,
sqlalchemy/pool/__pycache__/base.cpython-311.pyc,,
sqlalchemy/pool/__pycache__/events.cpython-311.pyc,,
sqlalchemy/pool/__pycache__/impl.cpython-311.pyc,,
sqlalchemy/pool/base.py,sha256=D0sKTRla6wpIFbELyGY2JEHUHR324rveIl93qjjmYr8,53751
sqlalchemy/pool/events.py,sha256=ysyFh0mNDpL4N4rQ-o_BC6tpo_zt0_au_QLBgJqaKY8,13517
sqlalchemy/pool/impl.py,sha256=BU5vUQ6NDFIldsG9og6mtO14SsqwpUqvwyGqZsKT6i0,19525
sqlalchemy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
sqlalchemy/schema.py,sha256=UFhZjGmYoqN3zkId7M4CbVCd8KaeZUfKUjdlk0sHQ_E,3264
sqlalchemy/sql/__init__.py,sha256=T16ZB3Za0Tq1LQGXeJuuxDkyu2t-XHR2t-8QH1mE1Uw,5965
sqlalchemy/sql/__pycache__/__init__.cpython-311.pyc,,
sqlalchemy/sql/__pycache__/_dml_constructors.cpython-311.pyc,,
sqlalchemy/sql/__pycache__/_elements_constructors.cpython-311.pyc,,
sqlalchemy/sql/__pycache__/_orm_types.cpython-311.pyc,,
sqlalchemy/sql/__pycache__/_py_util.cpython-311.pyc,,
sqlalchemy/sql/__pycache__/_selectable_constructors.cpython-311.pyc,,
sqlalchemy/sql/__pycache__/_typing.cpython-311.pyc,,
sqlalchemy/sql/__pycache__/annotation.cpython-311.pyc,,
sqlalchemy/sql/__pycache__/base.cpython-311.pyc,,
sqlalchemy/sql/__pycache__/cache_key.cpython-311.pyc,,
sqlalchemy/sql/__pycache__/coercions.cpython-311.pyc,,
sqlalchemy/sql/__pycache__/compiler.cpython-311.pyc,,
sqlalchemy/sql/__pycache__/crud.cpython-311.pyc,,
sqlalchemy/sql/__pycache__/ddl.cpython-311.pyc,,
sqlalchemy/sql/__pycache__/default_comparator.cpython-311.pyc,,
sqlalchemy/sql/__pycache__/dml.cpython-311.pyc,,
sqlalchemy/sql/__pycache__/elements.cpython-311.pyc,,
sqlalchemy/sql/__pycache__/events.cpython-311.pyc,,
sqlalchemy/sql/__pycache__/expression.cpython-311.pyc,,
sqlalchemy/sql/__pycache__/functions.cpython-311.pyc,,
sqlalchemy/sql/__pycache__/lambdas.cpython-311.pyc,,
sqlalchemy/sql/__pycache__/naming.cpython-311.pyc,,
sqlalchemy/sql/__pycache__/operators.cpython-311.pyc,,
sqlalchemy/sql/__pycache__/roles.cpython-311.pyc,,
sqlalchemy/sql/__pycache__/schema.cpython-311.pyc,,
sqlalchemy/sql/__pycache__/selectable.cpython-311.pyc,,
sqlalchemy/sql/__pycache__/sqltypes.cpython-311.pyc,,
sqlalchemy/sql/__pycache__/traversals.cpython-311.pyc,,
sqlalchemy/sql/__pycache__/type_api.cpython-311.pyc,,
sqlalchemy/sql/__pycache__/util.cpython-311.pyc,,
sqlalchemy/sql/__pycache__/visitors.cpython-311.pyc,,
sqlalchemy/sql/_dml_constructors.py,sha256=1xMH5Kd6SLhlFwfIs_lOXGC8GTrqW8mQM7Kc3cKyLuw,4007
sqlalchemy/sql/_elements_constructors.py,sha256=FRFRwYB9X5w01gyf07QjdkNa0rH8GrJOFW9-4sxZn88,65018
sqlalchemy/sql/_orm_types.py,sha256=_bzlAh3-vTIZoLvAM2ry1SF7rsYRM3-jupfhGWZZn5Y,645
sqlalchemy/sql/_py_util.py,sha256=VzThcXk7fKqT9_mZmXrkxePdwyyl_wIciCftzl2Z_-g,2248
sqlalchemy/sql/_selectable_constructors.py,sha256=mRgtlGyctlb1LMBqFxgn0eGzIXMbyZtQafjUuJWhYjs,19415
sqlalchemy/sql/_typing.py,sha256=FzAwrbL9o2CMluxxj1MfYh9ut7NDqkzD6dkWk5tz_dI,13231
sqlalchemy/sql/annotation.py,sha256=PslN1KQV9hN8Ji4k8I3-W-cDuRMCCLwMmJcg-n86Yy4,18830
sqlalchemy/sql/base.py,sha256=LY4zzFtADcqGcvKPUuuC9rK-vucqmHn3zKklVivoZ-M,76110
sqlalchemy/sql/cache_key.py,sha256=nEvUQ4yjtWWblrKjLLDd_b9i5zudgYhkOdJvI1U8Lvo,34725
sqlalchemy/sql/coercions.py,sha256=wFZIVoYcmAoQYSHbv268cIzLfBVX_iI7d1evFSQFuL4,42069
sqlalchemy/sql/compiler.py,sha256=GXd4pruoy7-fMTc8-4GSQyHCTqBMzpykn3WVvqxhkYM,282505
sqlalchemy/sql/crud.py,sha256=YL1HIVrUkcduncUZ6F4gEBf5PlowguGkxMAlgMVlKgI,58183
sqlalchemy/sql/ddl.py,sha256=cebT_-dEY79LvqM63rFN8uaMLZXtTvP7pT4yO_qWlXE,47014
sqlalchemy/sql/default_comparator.py,sha256=lXmd8yAUzfyeP5w4vebrQG99oC0bTrmdGc0crBq1GKw,17259
sqlalchemy/sql/dml.py,sha256=lt5FC6BbJNotE65U-fmvEovBxkADfKBnVcnkVYYQxUM,67431
sqlalchemy/sql/elements.py,sha256=FudYJXww47JMyXnyZnX2foxsXITLLhFTQkFIWvPXf0s,182043
sqlalchemy/sql/events.py,sha256=pG3jqJbPX18N9Amp47aXiQYMYD_HL_lOXHk-0m8m7Hw,18745
sqlalchemy/sql/expression.py,sha256=T-AgCPp30tgKQYLKeSyqQg_VoJFE69m2yDTz6fn-u1E,7748
sqlalchemy/sql/functions.py,sha256=9iazLxbvCMH35CzgtZaNJ0IUn8wz04w9DofY3jNfIHc,65817
sqlalchemy/sql/lambdas.py,sha256=eKlhUhD8urNVvOm_1tUf8ESPIpo2qTAidKHJEarUhj8,50741
sqlalchemy/sql/naming.py,sha256=ERVjqo6fBHBw2BwNgpbb5cvsCkq1jjdztczP9BKzVt8,7070
sqlalchemy/sql/operators.py,sha256=CgijIxSVCh00XnqfhtK0sHD-gR5HlQt8LdJUZlPBu2E,78685
sqlalchemy/sql/roles.py,sha256=8nO4y1hbP1cA8IzeOn6uPgNZNVILb3E-IMeJWOIScu8,7985
sqlalchemy/sql/schema.py,sha256=bCxaa_Y_wZrkDauJX4Cr4Lr1vZaqNCoK3Dzd0ki_fsQ,235875
sqlalchemy/sql/selectable.py,sha256=5jDioKY0V9fT731TtWHtUTKuzO6beUABx9l-fjh-ywo,243464
sqlalchemy/sql/sqltypes.py,sha256=H5YpFPXEPVmDGrRLsLaOL3Whr7WsHstblIe_GRvA1MU,131296
sqlalchemy/sql/traversals.py,sha256=dPHz1Kyyo_a5JcJiajR39iQejisGqefqUjYXqlBT7Vo,34688
sqlalchemy/sql/type_api.py,sha256=0bHHe4o2333vj_hMbLfAMIZ0_Hjee00EdP9ItSRZZI8,86803
sqlalchemy/sql/util.py,sha256=ftTiyNGeJK0MIRMqWMV7Xf8iZuiRGocoJRp3MIO3F3Y,49563
sqlalchemy/sql/visitors.py,sha256=oudlabsf9qleuC78GFe_iflRSAD8H-HjaM7T8Frc538,37482
sqlalchemy/testing/__init__.py,sha256=X7Td6ZZXYkZ_I58YPcHZnZ11RwwAmbnsC1ds3F6lOj8,3256
sqlalchemy/testing/__pycache__/__init__.cpython-311.pyc,,
sqlalchemy/testing/__pycache__/assertions.cpython-311.pyc,,
sqlalchemy/testing/__pycache__/assertsql.cpython-311.pyc,,
sqlalchemy/testing/__pycache__/asyncio.cpython-311.pyc,,
sqlalchemy/testing/__pycache__/config.cpython-311.pyc,,
sqlalchemy/testing/__pycache__/engines.cpython-311.pyc,,
sqlalchemy/testing/__pycache__/entities.cpython-311.pyc,,
sqlalchemy/testing/__pycache__/exclusions.cpython-311.pyc,,
sqlalchemy/testing/__pycache__/pickleable.cpython-311.pyc,,
sqlalchemy/testing/__pycache__/profiling.cpython-311.pyc,,
sqlalchemy/testing/__pycache__/provision.cpython-311.pyc,,
sqlalchemy/testing/__pycache__/requirements.cpython-311.pyc,,
sqlalchemy/testing/__pycache__/schema.cpython-311.pyc,,
sqlalchemy/testing/__pycache__/util.cpython-311.pyc,,
sqlalchemy/testing/__pycache__/warnings.cpython-311.pyc,,
sqlalchemy/testing/assertions.py,sha256=bBn2Ep89FF-WBmzh0VkvnJ9gNMKuqk8OXq7ALpUwar4,32428
sqlalchemy/testing/assertsql.py,sha256=gj4YRBR9cjOtS1WgR3nsyIze1tmqctsNs1uCV8N2Q4w,17333
sqlalchemy/testing/asyncio.py,sha256=xYuWjKFHzolBLgddy1ePI9l8KRRUOWpT-FWjhtV2Ei0,3965
sqlalchemy/testing/config.py,sha256=jfFVUiAOm8im6SlqyAdZVSaA51kmADgfBDqrHnngH7c,12517
sqlalchemy/testing/engines.py,sha256=8R7nbmLNUv2w7tiyVpiVI1s-57wpEs70UAgY-pkPX8k,13953
sqlalchemy/testing/entities.py,sha256=Um-DFSz81p06DhTK899ZRUOZRw3FtUDeNMVHcIg3eLc,3471
sqlalchemy/testing/exclusions.py,sha256=8kjsaFfjCvPlLsQLD_LIDwuqvVlIVbD5qTWBlKdtNkM,12895
sqlalchemy/testing/fixtures/__init__.py,sha256=B1IFCzEVdCqhEvFrLmgxZ_Fr08jDus5FddSA-lnnAAU,1226
sqlalchemy/testing/fixtures/__pycache__/__init__.cpython-311.pyc,,
sqlalchemy/testing/fixtures/__pycache__/base.cpython-311.pyc,,
sqlalchemy/testing/fixtures/__pycache__/mypy.cpython-311.pyc,,
sqlalchemy/testing/fixtures/__pycache__/orm.cpython-311.pyc,,
sqlalchemy/testing/fixtures/__pycache__/sql.cpython-311.pyc,,
sqlalchemy/testing/fixtures/base.py,sha256=S0ODuph0jA2Za4GN3NNhYVIqN9jAa3Q9Vd1N4O4rcTc,12622
sqlalchemy/testing/fixtures/mypy.py,sha256=2H8QxvGvwsb_Z3alRtvCvfXeqGjOb8aemfoYxQiuGMc,12285
sqlalchemy/testing/fixtures/orm.py,sha256=6JvQpIfmgmSTH3Hie4nhmUFfvH0pseujIFA9Lup2Dzw,6322
sqlalchemy/testing/fixtures/sql.py,sha256=w0qyMBLibPqkhE5ZhJtHcJmIDJlwcPpnlq9v0WajrF8,16403
sqlalchemy/testing/pickleable.py,sha256=uYLl557iNep6jSOVl0vK1GwaLHUKidALoPJc-QIrC08,2988
sqlalchemy/testing/plugin/__init__.py,sha256=bbtVIt7LzVnUCcVxHWRH2owOQD067bQwwhyMf_whqHs,253
sqlalchemy/testing/plugin/__pycache__/__init__.cpython-311.pyc,,
sqlalchemy/testing/plugin/__pycache__/bootstrap.cpython-311.pyc,,
sqlalchemy/testing/plugin/__pycache__/plugin_base.cpython-311.pyc,,
sqlalchemy/testing/plugin/__pycache__/pytestplugin.cpython-311.pyc,,
sqlalchemy/testing/plugin/bootstrap.py,sha256=USn6pE-JcE5pSmnEd2wad3goKLx2hdJS3AUUFpXHm-I,1736
sqlalchemy/testing/plugin/plugin_base.py,sha256=CgrNj2wj9KNALu9YfnGSaHX2fXfTtiim_cfx0CPVoy8,22357
sqlalchemy/testing/plugin/pytestplugin.py,sha256=acuAWFec8QGzC_AWOhTsRRgB6dttkbNdoyGVb7WvTng,28524
sqlalchemy/testing/profiling.py,sha256=o8_V3TpF_WytudMQQLm1UxlfNDrLCWxUvkH-Kd0unKU,10472
sqlalchemy/testing/provision.py,sha256=l9KUBqR8_NLACZzE1f_twzjlkaVUzRa5tBwgJBmvqrY,15122
sqlalchemy/testing/requirements.py,sha256=CuMwr9ZvQV-3va7jZ_dx8CW2TsjlhcQJ_DdNHvmFpJQ,54650
sqlalchemy/testing/schema.py,sha256=z2Z5rm3iJ1-vgifUxwzxEjt1qu7QOyr3TeDnQdCHlWE,6737
sqlalchemy/testing/suite/__init__.py,sha256=YvTEqUNHaBlgLgWDAWn79mQrUR4VBGUHtprywJlmDT8,741
sqlalchemy/testing/suite/__pycache__/__init__.cpython-311.pyc,,
sqlalchemy/testing/suite/__pycache__/test_cte.cpython-311.pyc,,
sqlalchemy/testing/suite/__pycache__/test_ddl.cpython-311.pyc,,
sqlalchemy/testing/suite/__pycache__/test_deprecations.cpython-311.pyc,,
sqlalchemy/testing/suite/__pycache__/test_dialect.cpython-311.pyc,,
sqlalchemy/testing/suite/__pycache__/test_insert.cpython-311.pyc,,
sqlalchemy/testing/suite/__pycache__/test_reflection.cpython-311.pyc,,
sqlalchemy/testing/suite/__pycache__/test_results.cpython-311.pyc,,
sqlalchemy/testing/suite/__pycache__/test_rowcount.cpython-311.pyc,,
sqlalchemy/testing/suite/__pycache__/test_select.cpython-311.pyc,,
sqlalchemy/testing/suite/__pycache__/test_sequence.cpython-311.pyc,,
sqlalchemy/testing/suite/__pycache__/test_types.cpython-311.pyc,,
sqlalchemy/testing/suite/__pycache__/test_unicode_ddl.cpython-311.pyc,,
sqlalchemy/testing/suite/__pycache__/test_update_delete.cpython-311.pyc,,
sqlalchemy/testing/suite/test_cte.py,sha256=C_viXJKClFAm91rtPb42tiAA7gYJwKkqGYVJYap0cLM,6662
sqlalchemy/testing/suite/test_ddl.py,sha256=k6D6RreLkDSSpRUM2hQz-_CA48qV2PYx_2LNyUSoZzE,12420
sqlalchemy/testing/suite/test_deprecations.py,sha256=SKRFZDteBO1rw9-BQjDic5nh7fdyw2ypVOewR2pj7-Q,5490
sqlalchemy/testing/suite/test_dialect.py,sha256=ftOWRXWOotB2_jMJJqwoH9f3X2ucc1HwwOiXp573GwM,23663
sqlalchemy/testing/suite/test_insert.py,sha256=v3zrUZaGlke3cI4vabHg7xaI4gNqcHhtMPgYuf0mOxc,19454
sqlalchemy/testing/suite/test_reflection.py,sha256=J--LmZcLez3B21WANzLj2zC_6gcKX-ycBQW2CMQ_azA,112873
sqlalchemy/testing/suite/test_results.py,sha256=i2a_XiUJ5D9womKtJsveVUBUeJRpOOPrr1fJuen_qEY,17416
sqlalchemy/testing/suite/test_rowcount.py,sha256=DCEGxorDcrT5JCLd3_SNQeZmxT6sKIcuKxX1r6vK4Mg,8158
sqlalchemy/testing/suite/test_select.py,sha256=_HTef0mBmcvL2uS9z4G73AXZwXvjerQpl8wF1vqPt4I,63731
sqlalchemy/testing/suite/test_sequence.py,sha256=sIqkfgVqPIgl4lm75EPdag9gK-rTHfUm3pWX-JijPy4,10240
sqlalchemy/testing/suite/test_types.py,sha256=6RiN7L1g3b_3GUw8G_i39WB3M0W4tHNYTiwu57thzR0,69946
sqlalchemy/testing/suite/test_unicode_ddl.py,sha256=juF_KTK1nGrSlsL8z0Ky0rFSNkPGheLB3e0Kq3yRqss,6330
sqlalchemy/testing/suite/test_update_delete.py,sha256=TnJI5U_ZEuu3bni4sH-S6CENxvSZwDgZL-FKSV45bAo,4133
sqlalchemy/testing/util.py,sha256=rQqTjYvb9wQt1UfeZZHNuPG3aNxFyjrvVQUhtfZQYjc,15006
sqlalchemy/testing/warnings.py,sha256=3EhbTlPe4gJnoydj-OKueNOOtGwIRF2kV4XvlFwFYOA,1598
sqlalchemy/types.py,sha256=unCm_O8qKxU3LjLbqeqSNQSsK5k5R5POsyEx2gH6CF4,3244
sqlalchemy/util/__init__.py,sha256=kE29cmjratY0xTq0vIQskMqFbKKmU69fLzer3TOAfYY,8472
sqlalchemy/util/__pycache__/__init__.cpython-311.pyc,,
sqlalchemy/util/__pycache__/_collections.cpython-311.pyc,,
sqlalchemy/util/__pycache__/_concurrency_py3k.cpython-311.pyc,,
sqlalchemy/util/__pycache__/_has_cy.cpython-311.pyc,,
sqlalchemy/util/__pycache__/_py_collections.cpython-311.pyc,,
sqlalchemy/util/__pycache__/compat.cpython-311.pyc,,
sqlalchemy/util/__pycache__/concurrency.cpython-311.pyc,,
sqlalchemy/util/__pycache__/deprecations.cpython-311.pyc,,
sqlalchemy/util/__pycache__/langhelpers.cpython-311.pyc,,
sqlalchemy/util/__pycache__/preloaded.cpython-311.pyc,,
sqlalchemy/util/__pycache__/queue.cpython-311.pyc,,
sqlalchemy/util/__pycache__/tool_support.cpython-311.pyc,,
sqlalchemy/util/__pycache__/topological.cpython-311.pyc,,
sqlalchemy/util/__pycache__/typing.cpython-311.pyc,,
sqlalchemy/util/_collections.py,sha256=602PFadp7l1ZMJdqNTt7gkj-4-Xid0Ioo2xy4o6ICVI,20793
sqlalchemy/util/_concurrency_py3k.py,sha256=8GB5fJVXvKp8BGyDBmFff3fTXQ71QUzkALiscX8qfYE,9458
sqlalchemy/util/_has_cy.py,sha256=IHGc5hUFbXQuv1a1z2P8yVwz0yGbCYXyQM2qsdcBTyg,1287
sqlalchemy/util/_py_collections.py,sha256=2PUqiKIsF8d-gNDAAqYI8WE6XPyRf1flRLkVsJeXuOo,17255
sqlalchemy/util/compat.py,sha256=cJlRkbMD1DjV46dt1S9e4eboqDpKI5FotLYYVUjQ3J4,9061
sqlalchemy/util/concurrency.py,sha256=zlmuK99p5cPpEPxBQYSDfLHP0Pbuw4iDeUzU49Pb1Ow,3412
sqlalchemy/util/deprecations.py,sha256=AnHpDWHi7g2gv_QUTGStQTnr0J94lIF-3aFLOsv9yzg,12372
sqlalchemy/util/langhelpers.py,sha256=6-bwfvTw6x1Ev8rsnHFHXYJOZVJvphrv85fQAGtAyn0,67308
sqlalchemy/util/preloaded.py,sha256=78Sl7VjzTOPajbovvARxNeuZb-iYRpEvL5k8m5Bz4vQ,6054
sqlalchemy/util/queue.py,sha256=4SbSbVamUECjCDpMPR035N1ooVHt9W5GjbqkxfZmH5k,10507
sqlalchemy/util/tool_support.py,sha256=DuurikYgDUIIxk3gubUKl6rs-etXt3eeHaZ4ZkIyJXQ,6336
sqlalchemy/util/topological.py,sha256=_NdtAghZjhZ4e2fwWHmn25erP5cvtGgOUMplsCa_VCE,3578
sqlalchemy/util/typing.py,sha256=BTBK8Lb_4wm_1Jop0U98ULBk3a2PaN2wA1OegAhv1tA,18811

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save