@@ -0,0 +1,395 @@
"""
Project API Handler
实现的接口:
GET:
- /project/list - 获取项目列表
- /project - 获取项目详情
- /project/all_info - 获取项目完整信息
PUT (6个更新接口):
- /project - 更新项目基本信息
- /project/key_constraints - 关键节点
- /project/complete_investment - 完成投资
- /project/investment_decision - 投资决策
- /project/dynamic_investment - 动态投资
- /project/settlement_investment - 结算投资
不支持: CREATE, DELETE
"""
from typing import List , Dict , Any
from . . . datasource . api . handler import BaseApiHandler
from . . . datasource . task_result import TaskResult
from . . . common . sync_node import SyncNode
from . sync_node import ProjectSyncNode
from schemas . project . project_base import (
ProjectResponseBase ,
ProjectBaseInfoUpdate ,
ProjectKeyConstraints ,
ProjectCompleteInvestment ,
ProjectInvestmentDecision ,
ProjectDynamicInvestment ,
ProjectSettlementInvestment
)
from schemas . project . project import ProjectInfoResponse
class ProjectApiHandler ( BaseApiHandler ) :
""" 项目 API Handler """
# 类级别缓存:存储 all_info 数据
_all_info_cache : Dict [ str , Dict [ str , Any ] ] = { }
def __init__ ( self , filter_project_id : str | List [ str ] | None = None ) :
"""
初始化 Project Handler
Args:
filter_project_id: 指定只加载的项目ID列表(可选,用于多项目测试)
"""
super ( ) . __init__ ( )
self . _node_type = " project "
self . _node_class = ProjectSyncNode
self . _schema = ProjectResponseBase
if filter_project_id is None :
self . _filter_project_id = None
elif isinstance ( filter_project_id , str ) :
self . _filter_project_id = [ filter_project_id ]
else :
self . _filter_project_id = filter_project_id # 保存过滤条件
def set_target_project_ids ( self , target_project_ids : List [ str ] ) - > None :
self . _filter_project_id = list ( target_project_ids ) if target_project_ids else None
@classmethod
async def get_all_info ( cls , client , project_id : str ) - > Dict [ str , Any ] :
""" 获取项目完整信息(带缓存)
Args:
client: API client
project_id: 项目ID
Returns:
项目完整信息字典
"""
if project_id in cls . _all_info_cache :
return cls . _all_info_cache [ project_id ]
all_info = await api_get_project_all_info ( client , project_id )
cls . _all_info_cache [ project_id ] = all_info
return all_info
@classmethod
def clear_cache ( cls ) :
""" 清空缓存(每次同步开始前调用) """
cls . _all_info_cache . clear ( )
async def load ( self ) - > List [ SyncNode ] :
""" 加载项目列表并预加载 all_info(支持分页)
Args:
project_id: 可选,指定项目ID列表则只加载这些项目(优先级高于构造函数的filter_project_id)
"""
try :
# 清空旧缓存
self . clear_cache ( )
nodes = [ ]
# 确定要加载的项目ID列表:参数 > 构造函数配置
target_project_ids = self . _filter_project_id or [ ]
if isinstance ( target_project_ids , str ) :
target_project_ids = [ target_project_ids ]
if target_project_ids :
# 只加载指定项目列表
projects : List [ ProjectResponseBase ] = [ ]
for project_id in target_project_ids :
project_response = await api_get_project ( self . api_client , project_id )
projects . append ( project_response )
else :
# 加载所有项目(分页)
projects : List [ ProjectResponseBase ] = [ ]
page = 1
page_size = 100
while True :
# 获取一页数据
page_projects , total = await api_get_project_list (
self . api_client ,
page = page ,
page_size = page_size
)
# 没有数据了,退出循环
if not page_projects :
break
projects . extend ( page_projects )
# 检查是否还有下一页
if page * page_size > = total :
break
page + = 1
# 处理所有项目
for project_data in projects :
node = self . _create_node ( project_data . model_dump ( ) )
# 设置 context
node . context [ " project_id " ] = project_data . id
# 预加载并缓存 all_info,同时存储到 node.context
all_info = await self . get_all_info ( self . api_client , project_data . id )
node . context [ " all_info " ] = all_info
nodes . append ( node )
return nodes
except Exception as e :
# 打印详细错误信息并重新抛出,让上层处理
print ( f " \n { ' = ' * 70 } " )
print ( " ❌ 项目加载失败 " )
print ( f " { ' = ' * 70 } " )
print ( f " 错误类型: { type ( e ) . __name__ } " )
print ( f " 错误信息: { str ( e ) } " )
print ( f " { ' = ' * 70 } " )
import traceback
traceback . print_exc ( )
print ( f " { ' = ' * 70 } \n " )
# 重新抛出异常,让DataSource能正确处理
raise
async def create_all ( self , nodes : List [ SyncNode ] ) - > List [ TaskResult ] :
""" 不支持创建 """
return [
TaskResult . failed ( node_id = node . node_id , error = " Project creation not supported " )
for node in nodes
]
async def update_all ( self , nodes : List [ SyncNode ] ) - > List [ TaskResult ] :
"""
批量更新项目(支持多种更新类型)
支持 6 种更新 schema:
1. ProjectBaseInfoUpdate - 基础信息
2. ProjectKeyConstraints - 关键约束
3. ProjectCompleteInvestment - 竣工投资
4. ProjectInvestmentDecision - 投资决策
5. ProjectDynamicInvestment - 动态投资
6. ProjectSettlementInvestment - 结算投资
"""
results = [ ]
# 定义所有更新 schema 和对应的 API 函数
update_configs = [
( ProjectBaseInfoUpdate , api_update_project_base_info ) ,
( ProjectKeyConstraints , api_update_project_key_constraints ) ,
( ProjectCompleteInvestment , api_update_project_complete_investment ) ,
( ProjectInvestmentDecision , api_update_project_investment_decision ) ,
( ProjectDynamicInvestment , api_update_project_dynamic_investment ) ,
( ProjectSettlementInvestment , api_update_project_settlement_investment ) ,
]
for node in nodes :
data = node . get_data ( )
if not data :
error_msg = node . error or " Node data is missing or invalid "
results . append ( TaskResult . failed ( node_id = node . node_id , error = f " Cannot update project: { error_msg } " ) )
continue
original_data = node . get_origin_data ( ) or { }
biz_id = node . data_id
if not biz_id :
results . append ( TaskResult . failed ( node_id = node . node_id , error = " Missing biz_id for update " ) )
continue
node_updated = False
error = None
# 遍历所有更新 schema,检查差异并更新
for schema , api_func in update_configs :
update_data = self . _get_schema_diff ( schema , data , original_data , node . node_id )
if not update_data :
continue
# 执行更新(差异已由 _get_schema_diff 打印)
try :
push_id = self . _generate_push_id ( )
# project 需要使用 _filter_update_data 返回的完整 schema 数据
full_update_data = self . _filter_update_data ( data , original_data , schema )
await api_func ( self . api_client , full_update_data , push_id , biz_id )
node_updated = True
except Exception as e :
error = str ( e )
break
# 汇总结果
if error :
results . append ( TaskResult . failed ( node_id = node . node_id , error = error ) )
elif node_updated :
results . append ( TaskResult . success ( node_id = node . node_id ) )
else :
# 没有任何字段需要更新
reason = f " No physical diff found across all 6 sub-schemas for project { biz_id } despite strategy update request (Normalization Discrepancy). "
print ( f " [WARNING] { reason } " )
results . append ( TaskResult . skipped ( node_id = node . node_id , reason = reason ) )
return results
async def delete_all ( self , nodes : List [ SyncNode ] ) - > List [ TaskResult ] :
""" 不支持删除 """
return [
TaskResult . failed ( node_id = node . node_id , error = " Project deletion not supported " )
for node in nodes
]
async def poll_tasks ( self , task_ids : List [ str ] ) - > Dict [ str , TaskResult ] :
""" 轮询异步任务状态 """
return await super ( ) . poll_tasks ( task_ids )
def _create_node ( self , data : Dict [ str , Any ] ) - > SyncNode :
node = self . _create_basic_node ( data , depend_ids = [ ] )
# 设置 context
node . context [ " project_id " ] = node . data_id
return node
# ========== 静态 API 函数(可抽成 SDK) ==========
async def api_get_project_list ( client , page : int = 1 , page_size : int = 100 ) - > tuple [ List [ ProjectResponseBase ] , int ] :
""" GET /project/list - 获取项目列表
Args:
client: API客户端
page: 页码,从1开始
page_size: 每页大小
Returns:
tuple[List[ProjectResponseBase], int]: (验证过的项目列表, 总数)
"""
response = await client . request (
method = " GET " ,
endpoint = " /project/list " ,
params = { " page " : page , " page_size " : page_size }
)
if response . get ( " code " ) != 200 :
raise Exception ( f " API error: { response . get ( ' message ' , ' Unknown error ' ) } " )
data = response . get ( " data " , { } )
list_data = data . get ( " list " , [ ] )
total = data . get ( " total " , 0 )
# 验证每个返回项
validated_items = [ ]
for item in list_data :
validated_item = ProjectResponseBase . model_validate ( item )
validated_items . append ( validated_item )
return validated_items , total
async def api_get_project ( client , project_id : str ) - > ProjectResponseBase :
""" GET /project - 获取项目详情 """
response = await client . request (
method = " GET " ,
endpoint = " /project " ,
params = { " project_id " : project_id }
)
if response . get ( " code " ) != 200 :
raise Exception ( f " API error: { response . get ( ' message ' , ' Unknown error ' ) } " )
return ProjectResponseBase . model_validate ( response . get ( " data " ) )
async def api_get_project_all_info ( client , project_id : str ) - > Dict [ str , Any ] :
""" GET /project/all_info - 获取项目完整信息(校验为 ProjectInfoResponse) """
response = await client . request (
method = " GET " ,
endpoint = " /project/all_info " ,
params = { " project_id " : project_id }
)
# 检查响应状态
code = response . get ( ' code ' )
if code != 200 :
error_msg = response . get ( ' message ' , ' Unknown error ' )
# 422 验证错误时打印详细信息
if code == 422 :
print ( f " \n { ' = ' * 70 } " )
print ( " ❌ API 422 验证错误 " )
print ( f " { ' = ' * 70 } " )
print ( " 接口: GET /project/all_info " )
print ( f " 项目ID: { project_id } " )
print ( f " 错误信息: { error_msg } " )
print ( " 完整响应: " )
import json
print ( json . dumps ( response , indent = 2 , ensure_ascii = False ) )
print ( f " { ' = ' * 70 } \n " )
raise Exception ( f " API error { code } : { error_msg } " )
data = response . get ( ' data ' , { } )
validated = ProjectInfoResponse . model_validate ( data )
return validated . model_dump ( )
async def api_update_project_base_info ( client , data : Dict [ str , Any ] , push_id : str , biz_id : str ) - > None :
""" PUT /project - 更新项目基本信息 """
ProjectBaseInfoUpdate . model_validate ( data )
request_data = { " push_id " : push_id , " biz_id " : biz_id , " data " : data }
response = await client . request ( method = " PUT " , endpoint = " /project " , data = request_data )
if response . get ( " code " ) != 200 :
raise Exception ( f " API error: { response . get ( ' message ' , ' Unknown error ' ) } " )
async def api_update_project_key_constraints ( client , data : Dict [ str , Any ] , push_id : str , biz_id : str ) - > None :
""" PUT /project/key_constraints - 更新关键节点 """
ProjectKeyConstraints . model_validate ( data )
request_data = { " push_id " : push_id , " biz_id " : biz_id , " data " : data }
response = await client . request ( method = " PUT " , endpoint = " /project/key_constraints " , data = request_data )
if response . get ( " code " ) != 200 :
raise Exception ( f " API error: { response . get ( ' message ' , ' Unknown error ' ) } " )
async def api_update_project_complete_investment ( client , data : Dict [ str , Any ] , push_id : str , biz_id : str ) - > None :
""" PUT /project/complete_investment - 更新完成投资 """
ProjectCompleteInvestment . model_validate ( data )
request_data = { " push_id " : push_id , " biz_id " : biz_id , " data " : data }
response = await client . request ( method = " PUT " , endpoint = " /project/complete_investment " , data = request_data )
if response . get ( " code " ) != 200 :
raise Exception ( f " API error: { response . get ( ' message ' , ' Unknown error ' ) } " )
async def api_update_project_investment_decision ( client , data : Dict [ str , Any ] , push_id : str , biz_id : str ) - > None :
""" PUT /project/investment_decision - 更新投资决策 """
ProjectInvestmentDecision . model_validate ( data )
request_data = { " push_id " : push_id , " biz_id " : biz_id , " data " : data }
response = await client . request ( method = " PUT " , endpoint = " /project/investment_decision " , data = request_data )
if response . get ( " code " ) != 200 :
raise Exception ( f " API error: { response . get ( ' message ' , ' Unknown error ' ) } " )
async def api_update_project_dynamic_investment ( client , data : Dict [ str , Any ] , push_id : str , biz_id : str ) - > None :
""" PUT /project/dynamic_investment - 更新动态投资 """
ProjectDynamicInvestment . model_validate ( data )
request_data = { " push_id " : push_id , " biz_id " : biz_id , " data " : data }
response = await client . request ( method = " PUT " , endpoint = " /project/dynamic_investment " , data = request_data )
if response . get ( " code " ) != 200 :
raise Exception ( f " API error: { response . get ( ' message ' , ' Unknown error ' ) } " )
async def api_update_project_settlement_investment ( client , data : Dict [ str , Any ] , push_id : str , biz_id : str ) - > None :
""" PUT /project/settlement_investment - 更新结算投资 """
ProjectSettlementInvestment . model_validate ( data )
request_data = { " push_id " : push_id , " biz_id " : biz_id , " data " : data }
response = await client . request ( method = " PUT " , endpoint = " /project/settlement_investment " , data = request_data )
if response . get ( " code " ) != 200 :
raise Exception ( f " API error: { response . get ( ' message ' , ' Unknown error ' ) } " )