177 lines
5.3 KiB
Python
177 lines
5.3 KiB
Python
|
|
"""
|
|||
|
|
任务配置数据模型
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
from typing import Dict, List, Any, Optional, Literal
|
|||
|
|
from pydantic import BaseModel, Field, model_validator
|
|||
|
|
|
|||
|
|
|
|||
|
|
class DataSource(BaseModel):
|
|||
|
|
"""数据源配置"""
|
|||
|
|
|
|||
|
|
type: Literal["csv", "xlsx", "sql", "http", "tushare"]
|
|||
|
|
# CSV/Excel 配置
|
|||
|
|
path: Optional[str] = None
|
|||
|
|
encoding: Optional[str] = "utf-8"
|
|||
|
|
sheet_name: Optional[str] = None
|
|||
|
|
# SQL 配置
|
|||
|
|
conn: Optional[str] = None
|
|||
|
|
query: Optional[str] = None
|
|||
|
|
params: Optional[Dict[str, Any]] = None
|
|||
|
|
# HTTP 配置
|
|||
|
|
url: Optional[str] = None
|
|||
|
|
method: Optional[str] = "GET"
|
|||
|
|
headers: Optional[Dict[str, str]] = None
|
|||
|
|
# Tushare 配置
|
|||
|
|
api_name: Optional[str] = None # pro_bar, index_daily, etc.
|
|||
|
|
ts_code: Optional[str] = None # 股票/指数代码
|
|||
|
|
index_code: Optional[str] = None # 指数代码
|
|||
|
|
start_date: Optional[str] = None
|
|||
|
|
end_date: Optional[str] = None
|
|||
|
|
fields: Optional[List[str]] = None
|
|||
|
|
|
|||
|
|
|
|||
|
|
class TransformOp(BaseModel):
|
|||
|
|
"""数据转换操作"""
|
|||
|
|
|
|||
|
|
type: Literal["groupby", "pivot", "merge", "compute", "filter", "sort", "rename"]
|
|||
|
|
# groupby 参数
|
|||
|
|
by: Optional[List[str]] = None
|
|||
|
|
agg: Optional[Dict[str, str]] = None
|
|||
|
|
# pivot 参数
|
|||
|
|
index: Optional[str] = None
|
|||
|
|
columns: Optional[str] = None
|
|||
|
|
values: Optional[str] = None
|
|||
|
|
# merge 参数
|
|||
|
|
on: Optional[str | List[str]] = None
|
|||
|
|
how: Optional[str] = "inner"
|
|||
|
|
# compute 参数
|
|||
|
|
expr: Optional[str] = None
|
|||
|
|
output_col: Optional[str] = None
|
|||
|
|
# filter 参数
|
|||
|
|
condition: Optional[str] = None
|
|||
|
|
# sort 参数
|
|||
|
|
sort_by: Optional[List[str]] = None
|
|||
|
|
ascending: Optional[bool] = True
|
|||
|
|
# rename 参数
|
|||
|
|
map: Optional[Dict[str, str]] = None
|
|||
|
|
|
|||
|
|
|
|||
|
|
class Transform(BaseModel):
|
|||
|
|
"""数据转换配置"""
|
|||
|
|
|
|||
|
|
from_: str | List[str] = Field(..., alias="from")
|
|||
|
|
ops: List[TransformOp]
|
|||
|
|
|
|||
|
|
|
|||
|
|
class ChartSeriesSpec(BaseModel):
|
|||
|
|
"""图表系列配置"""
|
|||
|
|
|
|||
|
|
key: str # DataFrame 列名
|
|||
|
|
name: str # 显示名称
|
|||
|
|
type: Optional[Literal["bar", "line", "area"]] = None # 图表类型(composer 模式用)
|
|||
|
|
axis: Optional[Literal["primary", "secondary"]] = None
|
|||
|
|
slot: Optional[int] = None # 对齐模板系列顺序
|
|||
|
|
|
|||
|
|
|
|||
|
|
class ChartSpec(BaseModel):
|
|||
|
|
"""图表配置"""
|
|||
|
|
|
|||
|
|
mode: Literal["create", "update", "excel_embedded"] = "update"
|
|||
|
|
target: Optional[str] = None # 模板中的图表名称(update 模式)
|
|||
|
|
target_placeholder: Optional[str] = None # 占位符名称(create/excel_embedded 模式)
|
|||
|
|
chart_type: Optional[str] = None # XL_CHART_TYPE(create 模式)或 Excel 图表类型
|
|||
|
|
source: str # 数据源名称
|
|||
|
|
categories: str # 分类列名
|
|||
|
|
series: List[ChartSeriesSpec]
|
|||
|
|
respect_template_types: bool = True
|
|||
|
|
series_order_strict: bool = True
|
|||
|
|
|
|||
|
|
def has_secondary_axis(self) -> bool:
|
|||
|
|
"""检查是否有次坐标轴系列"""
|
|||
|
|
return any(s.axis == "secondary" for s in self.series)
|
|||
|
|
|
|||
|
|
|
|||
|
|
class TableSpec(BaseModel):
|
|||
|
|
"""表格配置"""
|
|||
|
|
|
|||
|
|
target: str # 模板中的表格名称
|
|||
|
|
source: str # 数据源名称
|
|||
|
|
columns: List[str]
|
|||
|
|
header: Optional[List[str]] = None
|
|||
|
|
number_format: Optional[Dict[str, str]] = None
|
|||
|
|
|
|||
|
|
|
|||
|
|
class TextSpec(BaseModel):
|
|||
|
|
"""文本配置"""
|
|||
|
|
|
|||
|
|
target: str # 模板中的文本框名称
|
|||
|
|
value: str # 支持 Jinja2 模板语法
|
|||
|
|
|
|||
|
|
|
|||
|
|
class SlideSpec(BaseModel):
|
|||
|
|
"""幻灯片配置"""
|
|||
|
|
|
|||
|
|
id: str
|
|||
|
|
layout: Optional[str] = None # 版式名称
|
|||
|
|
texts: Optional[List[TextSpec]] = None
|
|||
|
|
tables: Optional[List[TableSpec]] = None
|
|||
|
|
charts: Optional[List[ChartSpec]] = None
|
|||
|
|
|
|||
|
|
|
|||
|
|
class TemplateSpec(BaseModel):
|
|||
|
|
"""模板配置"""
|
|||
|
|
|
|||
|
|
path: str
|
|||
|
|
master: Optional[str] = None
|
|||
|
|
notes: Optional[str] = None
|
|||
|
|
|
|||
|
|
|
|||
|
|
class OutputSpec(BaseModel):
|
|||
|
|
"""输出配置"""
|
|||
|
|
|
|||
|
|
path: str
|
|||
|
|
overwrite: bool = True
|
|||
|
|
add_metadata: bool = True # 是否添加生成信息到元数据
|
|||
|
|
|
|||
|
|
|
|||
|
|
class ComposerPageSpec(BaseModel):
|
|||
|
|
"""Composer 模式的页面规格"""
|
|||
|
|
|
|||
|
|
layout: str # LAYOUT_REGISTRY 中的布局名称
|
|||
|
|
data: Dict[str, Any] # 传给布局函数的 data dict
|
|||
|
|
|
|||
|
|
|
|||
|
|
class Job(BaseModel):
|
|||
|
|
"""完整的任务配置 — 支持 template 和 composer 两种模式"""
|
|||
|
|
|
|||
|
|
mode: Literal["template", "composer"] = "template"
|
|||
|
|
# template 模式字段
|
|||
|
|
template: Optional[TemplateSpec] = None
|
|||
|
|
slides: Optional[List[SlideSpec]] = None
|
|||
|
|
# composer 模式字段
|
|||
|
|
theme: Optional[str] = None # THEMES 中的名称
|
|||
|
|
aspect_ratio: Optional[str] = None # "4:3" 或 "16:9"(默认 16:9)
|
|||
|
|
pages: Optional[List[ComposerPageSpec]] = None
|
|||
|
|
# 共用字段
|
|||
|
|
datasources: Dict[str, DataSource] = Field(default_factory=dict)
|
|||
|
|
transforms: Optional[Dict[str, Transform]] = None
|
|||
|
|
params: Optional[Dict[str, Any]] = None
|
|||
|
|
default_layout_config: Optional[Dict[str, Any]] = None # 图表布局默认配置
|
|||
|
|
output: OutputSpec
|
|||
|
|
|
|||
|
|
class Config:
|
|||
|
|
populate_by_name = True
|
|||
|
|
|
|||
|
|
@model_validator(mode="after")
|
|||
|
|
def check_mode_fields(self):
|
|||
|
|
if self.mode == "template":
|
|||
|
|
if not self.template:
|
|||
|
|
raise ValueError("template 模式必须提供 template 字段")
|
|||
|
|
if not self.slides:
|
|||
|
|
raise ValueError("template 模式必须提供 slides 字段")
|
|||
|
|
elif self.mode == "composer":
|
|||
|
|
if not self.pages:
|
|||
|
|
raise ValueError("composer 模式必须提供 pages 字段")
|
|||
|
|
return self
|