commit
1f3694908b
@ -0,0 +1,59 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# Virtual environments
|
||||
venv/
|
||||
env/
|
||||
ENV/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Docker
|
||||
Dockerfile
|
||||
docker-compose.yml
|
||||
.dockerignore
|
||||
|
||||
# Git
|
||||
.git/
|
||||
.gitignore
|
||||
|
||||
# Documentation
|
||||
*.md
|
||||
|
||||
# Tests
|
||||
.pytest_cache/
|
||||
.coverage
|
||||
htmlcov/
|
||||
|
||||
# Local config
|
||||
.env
|
||||
config.local.json
|
||||
|
||||
# Data
|
||||
*.db
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
@ -0,0 +1,141 @@
|
||||
# Docker 部署指南
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 方式一:一键启动全部服务(推荐)
|
||||
|
||||
**Windows:**
|
||||
```bash
|
||||
start-docker.bat
|
||||
```
|
||||
|
||||
**Linux/Mac:**
|
||||
```bash
|
||||
chmod +x start-docker.sh
|
||||
./start-docker.sh
|
||||
```
|
||||
|
||||
启动后会自动:
|
||||
1. 构建 Docker 镜像
|
||||
2. 启动 PostgreSQL 数据库
|
||||
3. 启动 Redis
|
||||
4. 启动行情数据服务
|
||||
5. 自动创建数据库表
|
||||
|
||||
### 方式二:只启动数据库(本地开发)
|
||||
|
||||
如果你只想用 Docker 启动数据库,然后在本地运行 Python 服务:
|
||||
|
||||
```bash
|
||||
start-db-only.bat
|
||||
```
|
||||
|
||||
然后在本地启动服务:
|
||||
```bash
|
||||
python -m app.main
|
||||
```
|
||||
|
||||
## 访问服务
|
||||
|
||||
启动成功后,可以通过以下地址访问:
|
||||
|
||||
| 服务 | 地址 |
|
||||
|------|------|
|
||||
| 行情数据服务 | http://localhost:8080 |
|
||||
| 管理后台 | http://localhost:8080/admin |
|
||||
| API 文档 (Swagger) | http://localhost:8080/docs |
|
||||
| API 文档 (ReDoc) | http://localhost:8080/redoc |
|
||||
|
||||
## 数据库连接
|
||||
|
||||
```
|
||||
主机: localhost
|
||||
端口: 5432
|
||||
数据库: marketdata
|
||||
用户名: postgres
|
||||
密码: postgres123
|
||||
```
|
||||
|
||||
连接字符串:
|
||||
```
|
||||
postgresql://postgres:postgres123@localhost:5432/marketdata
|
||||
```
|
||||
|
||||
## 常用命令
|
||||
|
||||
```bash
|
||||
# 查看日志
|
||||
docker compose logs -f
|
||||
|
||||
# 停止服务
|
||||
docker compose down
|
||||
|
||||
# 停止并删除数据卷(清空数据)
|
||||
docker compose down -v
|
||||
|
||||
# 重启服务
|
||||
docker compose restart
|
||||
|
||||
# 进入数据库容器
|
||||
docker exec -it market_data_postgres psql -U postgres -d marketdata
|
||||
```
|
||||
|
||||
## 数据结构
|
||||
|
||||
启动时会自动创建以下表:
|
||||
|
||||
### 股票相关表
|
||||
- `stock_symbols` - 股票标的表
|
||||
- `stock_trading_calendar` - 股票交易日历
|
||||
- `stock_klines_1m` - 股票1分钟K线
|
||||
- `stock_klines_5m` - 股票5分钟K线
|
||||
- `stock_klines_1d` - 股票日线K线
|
||||
|
||||
### 期货相关表
|
||||
- `futures_symbols` - 期货合约表
|
||||
- `futures_trading_calendar` - 期货交易日历
|
||||
- `futures_klines_1m` - 期货1分钟K线
|
||||
- `futures_klines_1d` - 期货日线K线
|
||||
|
||||
### 公共表
|
||||
- `data_source_config` - 数据源配置
|
||||
- `data_quality_checks` - 数据质量检查
|
||||
|
||||
## 故障排查
|
||||
|
||||
### 端口冲突
|
||||
|
||||
如果 5432 或 8080 端口被占用,修改 `docker-compose.yml` 中的端口映射:
|
||||
|
||||
```yaml
|
||||
ports:
|
||||
- "5433:5432" # 使用 5433 代替 5432
|
||||
```
|
||||
|
||||
### 数据库连接失败
|
||||
|
||||
1. 检查容器是否运行:
|
||||
```bash
|
||||
docker ps
|
||||
```
|
||||
|
||||
2. 查看数据库日志:
|
||||
```bash
|
||||
docker logs market_data_postgres
|
||||
```
|
||||
|
||||
3. 检查数据库是否就绪:
|
||||
```bash
|
||||
docker exec -it market_data_postgres pg_isready -U postgres
|
||||
```
|
||||
|
||||
### 数据持久化
|
||||
|
||||
数据默认存储在 Docker 卷中:
|
||||
- `postgres_data` - PostgreSQL 数据
|
||||
- `redis_data` - Redis 数据
|
||||
|
||||
即使删除容器,数据也不会丢失。要清空数据:
|
||||
```bash
|
||||
docker compose down -v
|
||||
```
|
||||
@ -0,0 +1,24 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 安装系统依赖
|
||||
RUN apt-get update && apt-get install -y \
|
||||
gcc \
|
||||
libpq-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 复制依赖文件
|
||||
COPY requirements.txt .
|
||||
|
||||
# 安装 Python 依赖
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# 复制项目代码
|
||||
COPY . .
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 8080
|
||||
|
||||
# 启动命令
|
||||
CMD ["python", "-m", "app.main"]
|
||||
@ -0,0 +1,159 @@
|
||||
# Docker 快速启动指南
|
||||
|
||||
## 前置要求
|
||||
|
||||
- 安装 [Docker Desktop](https://www.docker.com/products/docker-desktop)
|
||||
|
||||
## 启动步骤
|
||||
|
||||
### 1. 启动数据库
|
||||
|
||||
Windows:
|
||||
```bash
|
||||
start-db-only.bat
|
||||
```
|
||||
|
||||
或者手动启动:
|
||||
```bash
|
||||
docker run -d --name market_data_postgres \
|
||||
-e POSTGRES_USER=postgres \
|
||||
-e POSTGRES_PASSWORD=postgres123 \
|
||||
-e POSTGRES_DB=marketdata \
|
||||
-p 5432:5432 \
|
||||
postgres:15-alpine
|
||||
```
|
||||
|
||||
### 2. 初始化数据库表
|
||||
|
||||
```bash
|
||||
python test_db.py
|
||||
```
|
||||
|
||||
输出示例:
|
||||
```
|
||||
==================================================
|
||||
数据库连接测试
|
||||
==================================================
|
||||
✅ 数据库连接成功
|
||||
PostgreSQL 版本: PostgreSQL 15.5 on x86_64-pc-linux-musl...
|
||||
|
||||
正在初始化数据库表...
|
||||
✅ 数据库表创建成功
|
||||
|
||||
已创建的表 (13 个):
|
||||
- data_quality_checks
|
||||
- data_source_config
|
||||
- futures_klines_1d
|
||||
- futures_klines_1m
|
||||
- futures_symbols
|
||||
- futures_trading_calendar
|
||||
- stock_klines_1d
|
||||
- stock_klines_1m
|
||||
- stock_klines_5m
|
||||
- stock_symbols
|
||||
- stock_trading_calendar
|
||||
...
|
||||
|
||||
==================================================
|
||||
数据库初始化完成!
|
||||
==================================================
|
||||
```
|
||||
|
||||
### 3. 启动服务
|
||||
|
||||
```bash
|
||||
python -m app.main
|
||||
```
|
||||
|
||||
## 验证服务
|
||||
|
||||
打开浏览器访问:
|
||||
- http://localhost:8080/admin - 管理后台
|
||||
- http://localhost:8080/docs - API 文档
|
||||
|
||||
## 完整 Docker Compose 启动
|
||||
|
||||
一键启动所有服务(数据库 + Redis + 应用):
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
查看日志:
|
||||
```bash
|
||||
docker compose logs -f
|
||||
```
|
||||
|
||||
停止服务:
|
||||
```bash
|
||||
docker compose down
|
||||
```
|
||||
|
||||
## 故障排查
|
||||
|
||||
### 问题 1: 端口被占用
|
||||
|
||||
错误:
|
||||
```
|
||||
Bind for 0.0.0.0:5432 failed: port is already allocated
|
||||
```
|
||||
|
||||
解决:
|
||||
```bash
|
||||
# 查看占用端口的进程
|
||||
netstat -ano | findstr 5432
|
||||
|
||||
# 停止冲突的容器
|
||||
docker stop <container_id>
|
||||
```
|
||||
|
||||
### 问题 2: 数据库连接失败
|
||||
|
||||
检查步骤:
|
||||
```bash
|
||||
# 1. 检查容器是否运行
|
||||
docker ps | findstr postgres
|
||||
|
||||
# 2. 查看容器日志
|
||||
docker logs market_data_postgres
|
||||
|
||||
# 3. 测试连接
|
||||
docker exec -it market_data_postgres pg_isready -U postgres
|
||||
```
|
||||
|
||||
### 问题 3: 表未创建
|
||||
|
||||
手动初始化:
|
||||
```bash
|
||||
python test_db.py
|
||||
```
|
||||
|
||||
## 数据持久化
|
||||
|
||||
数据存储在 Docker 卷中:
|
||||
|
||||
```bash
|
||||
# 查看卷
|
||||
docker volume ls
|
||||
|
||||
# 备份数据
|
||||
docker exec -it market_data_postgres pg_dump -U postgres marketdata > backup.sql
|
||||
|
||||
# 恢复数据
|
||||
docker exec -i market_data_postgres psql -U postgres marketdata < backup.sql
|
||||
```
|
||||
|
||||
## 完全重置
|
||||
|
||||
删除所有数据并重新启动:
|
||||
|
||||
```bash
|
||||
# 停止并删除容器
|
||||
docker compose down -v
|
||||
|
||||
# 重新启动
|
||||
docker compose up -d
|
||||
|
||||
# 初始化表
|
||||
python test_db.py
|
||||
```
|
||||
@ -0,0 +1,2 @@
|
||||
"""Market Data Service - Python实现"""
|
||||
__version__ = "1.0.0"
|
||||
Binary file not shown.
Binary file not shown.
@ -0,0 +1,13 @@
|
||||
"""数据源适配器模块"""
|
||||
from .base import DataSourceAdapter, TickData, KLineData, SymbolInfo, TradeCalData, TickCallback
|
||||
from .akshare_adapter import AKShareAdapter
|
||||
|
||||
__all__ = [
|
||||
"DataSourceAdapter",
|
||||
"TickData",
|
||||
"KLineData",
|
||||
"SymbolInfo",
|
||||
"TradeCalData",
|
||||
"TickCallback",
|
||||
"AKShareAdapter",
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,102 @@
|
||||
"""数据源适配器基类 - 对应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
|
||||
@ -0,0 +1,5 @@
|
||||
"""API路由模块"""
|
||||
from .routes import router
|
||||
from .admin_routes import admin_router
|
||||
|
||||
__all__ = ["router", "admin_router"]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,232 @@
|
||||
"""管理后台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))
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,135 @@
|
||||
"""配置管理模块"""
|
||||
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:
|
||||
# 优先使用环境变量 DATABASE_URL
|
||||
import os
|
||||
env_url = os.getenv("DATABASE_URL")
|
||||
if env_url:
|
||||
return env_url
|
||||
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 = "akshare"
|
||||
list: Dict[str, SourceInfo] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class SourcesConfig(BaseModel):
|
||||
"""数据源配置"""
|
||||
stock: SourceConfig = Field(default_factory=lambda: SourceConfig(
|
||||
active="akshare",
|
||||
list={"akshare": SourceInfo(type="http", config={"timeout": "30"})}
|
||||
))
|
||||
futures: SourceConfig = Field(default_factory=lambda: SourceConfig(
|
||||
active="akshare",
|
||||
list={"akshare": SourceInfo(type="http", config={"timeout": "30"})}
|
||||
))
|
||||
|
||||
|
||||
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")
|
||||
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()
|
||||
@ -0,0 +1,78 @@
|
||||
"""错误定义模块"""
|
||||
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)
|
||||
@ -0,0 +1,47 @@
|
||||
"""日志工具模块"""
|
||||
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)
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,135 @@
|
||||
"""数据模型模块"""
|
||||
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",
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,250 @@
|
||||
"""管理后台类型定义 - 对应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, Any] = 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测试历史")
|
||||
@ -0,0 +1,4 @@
|
||||
"""数据质量监控模块"""
|
||||
from .monitor import DataQualityMonitor, AlertSender, LogAlertSender
|
||||
|
||||
__all__ = ["DataQualityMonitor", "AlertSender", "LogAlertSender"]
|
||||
@ -0,0 +1,13 @@
|
||||
"""数据访问层模块"""
|
||||
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",
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,268 @@
|
||||
"""期货数据仓库"""
|
||||
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()
|
||||
@ -0,0 +1,202 @@
|
||||
"""数据库模型定义"""
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import (
|
||||
Column, Integer, String, Float, DateTime,
|
||||
Boolean, Numeric, BigInteger, Index, Text
|
||||
)
|
||||
|
||||
from app.repositories.database import Base
|
||||
|
||||
|
||||
# ============================================
|
||||
# 股票相关表
|
||||
# ============================================
|
||||
|
||||
class StockSymbol(Base):
|
||||
"""股票标的表"""
|
||||
__tablename__ = "stock_symbols"
|
||||
|
||||
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__ = "stock_trading_calendar"
|
||||
|
||||
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__ = "stock_klines_1m"
|
||||
__table_args__ = (
|
||||
Index("idx_stock_1m_symbol_ts", "symbol_id", "ts"),
|
||||
)
|
||||
|
||||
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__ = "stock_klines_5m"
|
||||
__table_args__ = (
|
||||
Index("idx_stock_5m_symbol_ts", "symbol_id", "ts"),
|
||||
)
|
||||
|
||||
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__ = "stock_klines_1d"
|
||||
__table_args__ = (
|
||||
Index("idx_stock_1d_symbol_ts", "symbol_id", "ts"),
|
||||
)
|
||||
|
||||
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__ = "futures_symbols"
|
||||
|
||||
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__ = "futures_trading_calendar"
|
||||
|
||||
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__ = "futures_klines_1m"
|
||||
__table_args__ = (
|
||||
Index("idx_futures_1m_symbol_ts", "symbol_id", "ts"),
|
||||
)
|
||||
|
||||
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__ = "futures_klines_1d"
|
||||
__table_args__ = (
|
||||
Index("idx_futures_1d_symbol_ts", "symbol_id", "ts"),
|
||||
)
|
||||
|
||||
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"
|
||||
|
||||
asset_class = Column(String(20), primary_key=True, comment="资产类别")
|
||||
active_source = Column(String(50), nullable=False, comment="当前激活源")
|
||||
standby_sources = Column(Text, nullable=True, comment="待命源列表(JSON)")
|
||||
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="更新时间")
|
||||
|
||||
|
||||
class DataQualityCheck(Base):
|
||||
"""数据质量检查表"""
|
||||
__tablename__ = "data_quality_checks"
|
||||
|
||||
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="创建时间")
|
||||
@ -0,0 +1,222 @@
|
||||
"""股票数据仓库"""
|
||||
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()
|
||||
@ -0,0 +1,16 @@
|
||||
"""业务服务层模块"""
|
||||
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",
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,332 @@
|
||||
"""配置管理服务 - 对应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}")
|
||||
@ -0,0 +1,4 @@
|
||||
"""WebSocket服务模块"""
|
||||
from .server import WebSocketServer, ws_manager
|
||||
|
||||
__all__ = ["WebSocketServer", "ws_manager"]
|
||||
Binary file not shown.
Binary file not shown.
@ -0,0 +1,210 @@
|
||||
"""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
|
||||
@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env python
|
||||
import urllib.request
|
||||
import json
|
||||
|
||||
# 获取 API Key
|
||||
with open('config.json') as f:
|
||||
cfg = json.load(f)
|
||||
api_key = cfg['server']['api_key']
|
||||
|
||||
print("=" * 50)
|
||||
print("Testing API Test List")
|
||||
print("=" * 50)
|
||||
|
||||
# 测试获取 API 测试列表
|
||||
try:
|
||||
req = urllib.request.Request(
|
||||
'http://localhost:8080/v1/admin/tests/api',
|
||||
headers={'X-Admin-Token': api_key}
|
||||
)
|
||||
response = urllib.request.urlopen(req)
|
||||
data = json.loads(response.read().decode())
|
||||
print(f"✓ API Test List: {len(data['data']['categories'])} categories")
|
||||
for cat in data['data']['categories']:
|
||||
print(f" - {cat['name']}: {len(cat['items'])} items")
|
||||
for item in cat['items'][:2]:
|
||||
print(f" - [{item['method']}] {item['name']}")
|
||||
if len(cat['items']) > 2:
|
||||
print(f" ... and {len(cat['items'])-2} more")
|
||||
except Exception as e:
|
||||
print(f"✗ Error: {e}")
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
print("Testing WebSocket Test List")
|
||||
print("=" * 50)
|
||||
|
||||
# 测试获取 WebSocket 测试列表
|
||||
try:
|
||||
req = urllib.request.Request(
|
||||
'http://localhost:8080/v1/admin/tests/ws',
|
||||
headers={'X-Admin-Token': api_key}
|
||||
)
|
||||
response = urllib.request.urlopen(req)
|
||||
data = json.loads(response.read().decode())
|
||||
print(f"✓ WS Test List: {len(data['data']['cases'])} cases")
|
||||
for case in data['data']['cases'][:5]:
|
||||
print(f" - {case['name']}: {case['action']} {case.get('symbols', [])}")
|
||||
except Exception as e:
|
||||
print(f"✗ Error: {e}")
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
print("Testing Run API Test (health check)")
|
||||
print("=" * 50)
|
||||
|
||||
# 测试执行单个 API 测试
|
||||
try:
|
||||
req = urllib.request.Request(
|
||||
'http://localhost:8080/v1/admin/tests/api/run',
|
||||
data=json.dumps({'id': 'admin_health'}).encode('utf-8'),
|
||||
headers={
|
||||
'Content-Type': 'application/json',
|
||||
'X-Admin-Token': api_key
|
||||
},
|
||||
method='POST'
|
||||
)
|
||||
response = urllib.request.urlopen(req)
|
||||
data = json.loads(response.read().decode())
|
||||
if data['code'] == 0:
|
||||
result = data['data']
|
||||
print(f"✓ Test Result: {'PASS' if result['success'] else 'FAIL'}")
|
||||
print(f" - Latency: {result['latency']}ms")
|
||||
print(f" - Status: {result['status_code']}")
|
||||
print(f" - URL: {result['request']['url']}")
|
||||
else:
|
||||
print(f"✗ Error: {data['message']}")
|
||||
except Exception as e:
|
||||
print(f"✗ Error: {e}")
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
print("All tests completed!")
|
||||
print("=" * 50)
|
||||
@ -0,0 +1,44 @@
|
||||
{
|
||||
"server": {
|
||||
"port": 8080,
|
||||
"mode": "debug",
|
||||
"api_key": "demo-api-key-2024"
|
||||
},
|
||||
"database": {
|
||||
"host": "localhost",
|
||||
"port": 3306,
|
||||
"user": "root",
|
||||
"password": "1qazse42W3",
|
||||
"database": "marketdata"
|
||||
},
|
||||
"redis": {
|
||||
"host": "localhost",
|
||||
"port": 6379,
|
||||
"password": "",
|
||||
"db": 0
|
||||
},
|
||||
"sources": {
|
||||
"stock": {
|
||||
"active": "akshare",
|
||||
"list": {
|
||||
"akshare": {
|
||||
"type": "http",
|
||||
"config": {
|
||||
"timeout": "30"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"futures": {
|
||||
"active": "akshare",
|
||||
"list": {
|
||||
"akshare": {
|
||||
"type": "http",
|
||||
"config": {
|
||||
"timeout": "30"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
container_name: market_data_mysql
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: postgres123
|
||||
MYSQL_DATABASE: marketdata
|
||||
MYSQL_USER: postgres
|
||||
MYSQL_PASSWORD: postgres123
|
||||
ports:
|
||||
- "3306:3306"
|
||||
volumes:
|
||||
- mysql_data:/var/lib/mysql
|
||||
command: --default-authentication-plugin=mysql_native_password
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
volumes:
|
||||
mysql_data:
|
||||
@ -0,0 +1,47 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:15-alpine
|
||||
container_name: market_data_postgres
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres123
|
||||
POSTGRES_DB: marketdata
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: market_data_redis
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
|
||||
market-data-service:
|
||||
build: .
|
||||
container_name: market_data_service
|
||||
environment:
|
||||
DATABASE_URL: "postgresql://postgres:postgres123@postgres:5432/marketdata"
|
||||
REDIS_URL: "redis://redis:6379/0"
|
||||
PORT: "8080"
|
||||
ports:
|
||||
- "8080:8080"
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_started
|
||||
command: python -m app.main
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
redis_data:
|
||||
@ -0,0 +1,39 @@
|
||||
"""初始化 MySQL 数据库"""
|
||||
import sys
|
||||
import pymysql
|
||||
|
||||
# 先创建数据库(如果不存在)
|
||||
conn = pymysql.connect(
|
||||
host='localhost',
|
||||
port=3306,
|
||||
user='root',
|
||||
password='1qazse42W3',
|
||||
charset='utf8mb4'
|
||||
)
|
||||
|
||||
try:
|
||||
with conn.cursor() as cursor:
|
||||
cursor.execute("CREATE DATABASE IF NOT EXISTS marketdata CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci")
|
||||
print("[OK] 数据库 'marketdata' 创建成功或已存在")
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
# 使用 SQLAlchemy 创建表
|
||||
import os
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from app.repositories.database import init_db, engine
|
||||
from sqlalchemy import inspect
|
||||
|
||||
print("\n正在创建数据表...")
|
||||
init_db()
|
||||
|
||||
# 验证表是否创建成功
|
||||
inspector = inspect(engine)
|
||||
tables = inspector.get_table_names()
|
||||
print(f"\n[OK] 已创建 {len(tables)} 个表:")
|
||||
for table in sorted(tables):
|
||||
print(f" - {table}")
|
||||
|
||||
print("\n[OK] MySQL 数据库初始化完成!")
|
||||
Binary file not shown.
@ -0,0 +1,44 @@
|
||||
[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*"]
|
||||
@ -0,0 +1,37 @@
|
||||
# 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
|
||||
akshare>=1.12.0
|
||||
|
||||
# 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
|
||||
@ -0,0 +1,78 @@
|
||||
@echo off
|
||||
chcp 65001 >nul
|
||||
echo ==========================================
|
||||
echo 行情数据服务 - 环境初始化
|
||||
echo ==========================================
|
||||
echo.
|
||||
|
||||
REM 检查 Docker
|
||||
where docker >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo [错误] Docker 未安装!请先安装 Docker Desktop
|
||||
echo 下载地址: https://www.docker.com/products/docker-desktop
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo [1/4] 正在启动 PostgreSQL 容器...
|
||||
docker ps | findstr market_data_postgres >nul
|
||||
if %errorlevel% == 0 (
|
||||
echo [提示] PostgreSQL 容器已在运行
|
||||
) else (
|
||||
docker start market_data_postgres >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo 正在创建新容器...
|
||||
docker run -d --name market_data_postgres ^
|
||||
-e POSTGRES_USER=postgres ^
|
||||
-e POSTGRES_PASSWORD=postgres123 ^
|
||||
-e POSTGRES_DB=marketdata ^
|
||||
-p 5432:5432 ^
|
||||
-v postgres_data:/var/lib/postgresql/data ^
|
||||
--restart unless-stopped ^
|
||||
postgres:15-alpine >nul
|
||||
) else (
|
||||
echo [提示] 已启动现有容器
|
||||
)
|
||||
)
|
||||
|
||||
echo.
|
||||
echo [2/4] 等待数据库就绪...
|
||||
set /a count=0
|
||||
:wait_loop
|
||||
timeout /t 1 /nobreak >nul
|
||||
set /a count+=1
|
||||
docker exec market_data_postgres pg_isready -U postgres >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
if %count% lss 30 (
|
||||
echo 等待中... (%count%/30)
|
||||
goto wait_loop
|
||||
) else (
|
||||
echo [错误] 数据库启动超时!
|
||||
exit /b 1
|
||||
)
|
||||
)
|
||||
echo 数据库已就绪!
|
||||
|
||||
echo.
|
||||
echo [3/4] 正在初始化数据库表...
|
||||
python test_db.py
|
||||
if errorlevel 1 (
|
||||
echo [错误] 数据库初始化失败!
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo.
|
||||
echo [4/4] 正在安装依赖...
|
||||
pip install -q -r requirements.txt
|
||||
|
||||
echo.
|
||||
echo ==========================================
|
||||
echo 初始化完成!
|
||||
echo ==========================================
|
||||
echo.
|
||||
echo 现在可以启动服务了:
|
||||
echo python -m app.main
|
||||
echo.
|
||||
echo 访问地址:
|
||||
echo http://localhost:8080
|
||||
echo.
|
||||
pause
|
||||
@ -0,0 +1,54 @@
|
||||
@echo off
|
||||
chcp 65001 >nul
|
||||
echo ==========================================
|
||||
echo 启动 PostgreSQL 数据库 (Docker)
|
||||
echo ==========================================
|
||||
echo.
|
||||
|
||||
REM 检查 Docker 是否安装
|
||||
docker --version >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo [错误] Docker 未安装,请先安装 Docker Desktop
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM 启动 PostgreSQL
|
||||
echo 正在启动 PostgreSQL 容器...
|
||||
docker run -d \
|
||||
--name market_data_postgres \
|
||||
-e POSTGRES_USER=postgres \
|
||||
-e POSTGRES_PASSWORD=postgres123 \
|
||||
-e POSTGRES_DB=marketdata \
|
||||
-p 5432:5432 \
|
||||
-v postgres_data:/var/lib/postgresql/data \
|
||||
--restart unless-stopped \
|
||||
postgres:15-alpine
|
||||
|
||||
if errorlevel 1 (
|
||||
echo [提示] 容器可能已存在,尝试启动现有容器...
|
||||
docker start market_data_postgres
|
||||
)
|
||||
|
||||
echo.
|
||||
echo [提示] 等待数据库初始化...
|
||||
timeout /t 3 /nobreak >nul
|
||||
|
||||
echo.
|
||||
echo ==========================================
|
||||
echo 数据库启动成功!
|
||||
echo ==========================================
|
||||
echo.
|
||||
echo 连接信息:
|
||||
echo - 主机: localhost
|
||||
echo - 端口: 5432
|
||||
echo - 数据库: marketdata
|
||||
echo - 用户名: postgres
|
||||
echo - 密码: postgres123
|
||||
echo.
|
||||
echo 常用命令:
|
||||
echo - 停止: docker stop market_data_postgres
|
||||
echo - 启动: docker start market_data_postgres
|
||||
echo - 删除: docker rm -f market_data_postgres
|
||||
echo - 连接: docker exec -it market_data_postgres psql -U postgres -d marketdata
|
||||
echo.
|
||||
pause
|
||||
@ -0,0 +1,63 @@
|
||||
@echo off
|
||||
chcp 65001 >nul
|
||||
echo ==========================================
|
||||
echo 行情数据服务 - Docker 启动脚本
|
||||
echo ==========================================
|
||||
echo.
|
||||
|
||||
REM 检查 Docker 是否安装
|
||||
docker --version >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo [错误] Docker 未安装,请先安装 Docker Desktop
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM 检查 Docker Compose 是否可用
|
||||
docker compose version >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo [错误] Docker Compose 不可用
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo [1/3] 正在构建镜像...
|
||||
docker compose build
|
||||
|
||||
echo.
|
||||
echo [2/3] 正在启动服务...
|
||||
docker compose up -d
|
||||
|
||||
echo.
|
||||
echo [3/3] 等待数据库初始化...
|
||||
timeout /t 5 /nobreak >nul
|
||||
|
||||
REM 检查服务状态
|
||||
docker ps | findstr market_data >nul
|
||||
if errorlevel 1 (
|
||||
echo [错误] 服务启动失败,请检查日志
|
||||
echo 查看日志: docker compose logs
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo.
|
||||
echo ==========================================
|
||||
echo 服务启动成功!
|
||||
echo ==========================================
|
||||
echo.
|
||||
echo 访问地址:
|
||||
echo - 主服务: http://localhost:8080
|
||||
echo - 管理后台: http://localhost:8080/admin
|
||||
echo - API 文档: http://localhost:8080/docs
|
||||
echo - ReDoc: http://localhost:8080/redoc
|
||||
echo.
|
||||
echo 数据库连接信息:
|
||||
echo - PostgreSQL: localhost:5432
|
||||
echo - 数据库: marketdata
|
||||
echo - 用户名: postgres
|
||||
echo - 密码: postgres123
|
||||
echo.
|
||||
echo 常用命令:
|
||||
echo - 查看日志: docker compose logs -f
|
||||
echo - 停止服务: docker compose down
|
||||
echo - 重启服务: docker compose restart
|
||||
echo.
|
||||
pause
|
||||
@ -0,0 +1,59 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "=========================================="
|
||||
echo " 行情数据服务 - Docker 启动脚本"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
# 检查 Docker 是否安装
|
||||
if ! command -v docker &> /dev/null; then
|
||||
echo "[错误] Docker 未安装,请先安装 Docker"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查 Docker Compose 是否可用
|
||||
if ! docker compose version &> /dev/null; then
|
||||
echo "[错误] Docker Compose 不可用"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[1/3] 正在构建镜像..."
|
||||
docker compose build
|
||||
|
||||
echo ""
|
||||
echo "[2/3] 正在启动服务..."
|
||||
docker compose up -d
|
||||
|
||||
echo ""
|
||||
echo "[3/3] 等待数据库初始化..."
|
||||
sleep 5
|
||||
|
||||
# 检查服务状态
|
||||
if ! docker ps | grep -q "market_data"; then
|
||||
echo "[错误] 服务启动失败,请检查日志"
|
||||
echo "查看日志: docker compose logs"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo " 服务启动成功!"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "访问地址:"
|
||||
echo " - 主服务: http://localhost:8080"
|
||||
echo " - 管理后台: http://localhost:8080/admin"
|
||||
echo " - API 文档: http://localhost:8080/docs"
|
||||
echo " - ReDoc: http://localhost:8080/redoc"
|
||||
echo ""
|
||||
echo "数据库连接信息:"
|
||||
echo " - PostgreSQL: localhost:5432"
|
||||
echo " - 数据库: marketdata"
|
||||
echo " - 用户名: postgres"
|
||||
echo " - 密码: postgres123"
|
||||
echo ""
|
||||
echo "常用命令:"
|
||||
echo " - 查看日志: docker compose logs -f"
|
||||
echo " - 停止服务: docker compose down"
|
||||
echo " - 重启服务: docker compose restart"
|
||||
echo ""
|
||||
@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env python3
|
||||
import urllib.request
|
||||
import json
|
||||
|
||||
req = urllib.request.Request(
|
||||
'http://localhost:8080/v1/admin/adapters',
|
||||
headers={'X-Admin-Token': 'demo-api-key-2024'}
|
||||
)
|
||||
|
||||
try:
|
||||
response = urllib.request.urlopen(req, timeout=10)
|
||||
data = json.loads(response.read().decode())
|
||||
print('✓ Success!')
|
||||
print(f"Code: {data['code']}")
|
||||
print(f"Message: {data['message']}")
|
||||
print(f"Adapters count: {len(data['data']['adapters'])}")
|
||||
for adapter in data['data']['adapters']:
|
||||
print(f" - {adapter['name']}: {adapter['status']} ({adapter['type']})")
|
||||
except Exception as e:
|
||||
print(f'✗ Error: {e}')
|
||||
@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env python3
|
||||
import urllib.request
|
||||
import json
|
||||
|
||||
req = urllib.request.Request(
|
||||
'http://localhost:8080/v1/admin/adapters',
|
||||
headers={'X-Admin-Token': 'demo-api-key-2024'}
|
||||
)
|
||||
|
||||
try:
|
||||
response = urllib.request.urlopen(req, timeout=10)
|
||||
data = json.loads(response.read().decode())
|
||||
print('Success!')
|
||||
print("Code:", data['code'])
|
||||
print("Adapters count:", len(data['data']['adapters']))
|
||||
for adapter in data['data']['adapters']:
|
||||
print(" -", adapter['name'] + ":", adapter['status'])
|
||||
except Exception as e:
|
||||
print('Error:', e)
|
||||
@ -0,0 +1,46 @@
|
||||
"""测试数据库连接"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
# 添加项目根目录到路径
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from app.repositories.database import init_db, engine
|
||||
from sqlalchemy import text
|
||||
|
||||
print("=" * 50)
|
||||
print("数据库连接测试")
|
||||
print("=" * 50)
|
||||
|
||||
try:
|
||||
# 测试连接
|
||||
with engine.connect() as conn:
|
||||
result = conn.execute(text("SELECT version()"))
|
||||
version = result.scalar()
|
||||
print(f"✅ 数据库连接成功")
|
||||
print(f" PostgreSQL 版本: {version}")
|
||||
|
||||
# 初始化数据库(创建表)
|
||||
print("\n正在初始化数据库表...")
|
||||
init_db()
|
||||
print("✅ 数据库表创建成功")
|
||||
|
||||
# 显示所有表
|
||||
from sqlalchemy import inspect
|
||||
inspector = inspect(engine)
|
||||
tables = inspector.get_table_names()
|
||||
print(f"\n已创建的表 ({len(tables)} 个):")
|
||||
for table in sorted(tables):
|
||||
print(f" - {table}")
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
print("数据库初始化完成!")
|
||||
print("=" * 50)
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 错误: {e}")
|
||||
print("\n请检查:")
|
||||
print("1. Docker 是否已启动: docker ps")
|
||||
print("2. 数据库端口是否正确: netstat -ano | findstr 5432")
|
||||
print("3. 数据库密码是否正确")
|
||||
sys.exit(1)
|
||||
@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env python3
|
||||
import urllib.request
|
||||
import json
|
||||
|
||||
# Test source status
|
||||
req = urllib.request.Request(
|
||||
'http://localhost:8080/v1/admin/source/status',
|
||||
headers={'X-API-Key': 'demo-api-key-2024'}
|
||||
)
|
||||
|
||||
try:
|
||||
response = urllib.request.urlopen(req, timeout=10)
|
||||
data = json.loads(response.read().decode())
|
||||
print('Source Status:')
|
||||
print(json.dumps(data, indent=2, ensure_ascii=False))
|
||||
except Exception as e:
|
||||
print('Error:', e)
|
||||
@ -0,0 +1,164 @@
|
||||
/* -*- 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 */
|
||||
Binary file not shown.
@ -0,0 +1 @@
|
||||
pip
|
||||
@ -0,0 +1,19 @@
|
||||
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.
|
||||
@ -0,0 +1,147 @@
|
||||
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
|
||||
@ -0,0 +1,86 @@
|
||||
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
|
||||
@ -0,0 +1,5 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: setuptools (75.6.0)
|
||||
Root-Is-Purelib: true
|
||||
Tag: py3-none-any
|
||||
|
||||
@ -0,0 +1,25 @@
|
||||
[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
|
||||
@ -0,0 +1 @@
|
||||
apscheduler
|
||||
@ -0,0 +1 @@
|
||||
pip
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue