You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
iTi-Flask/iti/exchange/routes.py

545 lines
17 KiB
Python

from __future__ import annotations
from fastapi import APIRouter, Depends, File, Query, Request, UploadFile
from fastapi.responses import StreamingResponse
from sqlalchemy.orm import Session
from iti.db import get_db
from iti.exceptions import BizError
from iti.responses import ok, raw_response
from .base import ExchangeTemplateSourceKind, ExchangeVariable
from .registry import get_exchange_registry
from .schemas import (
ExchangePlanResolveRequest,
ExchangePlanTemplateFileRequest,
ExchangeTaskCreateRequest,
ExchangeTaskResponse,
ExchangeTemplateCreateRequest,
ExchangeTemplateResponse,
ExchangeTemplateVersionCreateRequest,
ExchangeTemplateVersionResponse,
ExchangeTemplateUpdateRequest,
)
from .service import ExchangeService
from .sources import get_exchange_source
router = APIRouter(prefix="/exchange", tags=["exchange"])
def _template_payload(item):
return {
"id": item.id,
"code": item.code,
"name": item.name,
"biz_domain": item.biz_domain,
"biz_obj": item.biz_obj,
"operation": item.operation,
"status": item.status,
"description": item.description,
"current_version": item.current_version,
"layout": item.layout,
"meta": item.meta,
"created_at": item.created_at,
"updated_at": item.updated_at,
}
def _version_payload(item):
return {
"id": item.id,
"template_id": item.template_id,
"version": item.version,
"biz_domain": item.biz_domain,
"biz_obj": item.biz_obj,
"operation": item.operation,
"published_at": item.published_at,
"file_key": item.file_key,
"checksum": item.checksum,
"layout": item.layout,
"variables": item.variables,
"meta": item.meta,
"created_at": item.created_at,
"updated_at": item.updated_at,
}
def _task_payload(item):
return {
"id": item.id,
"template_id": item.template_id,
"template_version_id": item.template_version_id,
"biz_domain": item.biz_domain,
"biz_obj": item.biz_obj,
"operation": item.operation,
"status": item.status,
"requested_by": item.requested_by,
"storage_key": item.storage_key,
"success_count": item.success_count,
"failed_count": item.failed_count,
"error_count": item.error_count,
"message": item.message,
"input_payload": item.input_payload,
"result_payload": item.result_payload,
"started_at": item.started_at,
"finished_at": item.finished_at,
"meta": item.meta,
"created_at": item.created_at,
"updated_at": item.updated_at,
}
def _plan_payload(plan):
return plan.as_payload()
def _variable_from_schema(item) -> ExchangeVariable:
return ExchangeVariable(**item.model_dump())
def _layout_payload(layout):
return layout.model_dump() if layout is not None else None
def _resolve_source(payload, request: Request, db: Session):
source_kind = payload.source_kind
source_name = getattr(payload, "source_name", None)
if source_name:
return get_exchange_source(
request.app,
source_name=source_name,
db=db,
service_name=getattr(payload, "source_service", "exchange"),
)
if source_kind == ExchangeTemplateSourceKind.LOCAL:
return get_exchange_source(request.app, source_kind=source_kind, db=db)
if source_kind == ExchangeTemplateSourceKind.REMOTE:
return get_exchange_source(
request.app,
source_kind=source_kind,
service_name=payload.source_service or "exchange",
)
if source_kind is None:
return None
if source_kind == ExchangeTemplateSourceKind.MAPPING:
return get_exchange_source(request.app, source_kind=ExchangeTemplateSourceKind.MAPPING)
return get_exchange_source(request.app, source_kind=source_kind, db=db)
@router.get("/catalog")
def exchange_catalog(request: Request):
return ok(get_exchange_registry(request.app).catalog())
@router.get("/scopes/{biz_domain}/{biz_obj}/{operation}")
def get_scope_spec(
biz_domain: str,
biz_obj: str,
operation: str,
request: Request,
):
spec = get_exchange_registry(request.app).get_spec(
biz_domain=biz_domain,
biz_obj=biz_obj,
operation=operation,
)
if spec is None:
raise BizError("业务导入导出规格不存在", code=404)
return ok(spec.as_payload())
@router.get("/templates/code")
def generate_template_code(
biz_domain: str,
biz_obj: str,
operation: str,
request: Request,
db: Session = Depends(get_db),
):
service = ExchangeService(request.app, db)
return ok(
{
"code": service.generate_template_code(
biz_domain=biz_domain,
biz_obj=biz_obj,
operation=operation,
)
}
)
@router.get("/templates")
def list_templates(
request: Request,
biz_domain: str | None = Query(default=None),
biz_obj: str | None = Query(default=None),
operation: str | None = Query(default=None),
db: Session = Depends(get_db),
):
service = ExchangeService(request.app, db)
return ok(
[
ExchangeTemplateResponse.model_validate(_template_payload(item)).model_dump(mode="json")
for item in service.list_templates(
biz_domain=biz_domain,
biz_obj=biz_obj,
operation=operation,
)
]
)
@router.get("/templates/{template_id}")
def get_template(template_id: str, request: Request, db: Session = Depends(get_db)):
service = ExchangeService(request.app, db)
template = service.get_template_or_404(template_id)
return ok(ExchangeTemplateResponse.model_validate(_template_payload(template)).model_dump(mode="json"))
@router.post("/templates")
def create_template(
payload: ExchangeTemplateCreateRequest,
request: Request,
db: Session = Depends(get_db),
):
service = ExchangeService(request.app, db)
template = service.create_template(
code=payload.code,
name=payload.name,
biz_domain=payload.biz_domain,
biz_obj=payload.biz_obj,
operation=payload.operation,
description=payload.description,
layout=_layout_payload(payload.layout),
meta=payload.meta,
)
return ok(ExchangeTemplateResponse.model_validate(_template_payload(template)).model_dump(mode="json"))
@router.patch("/templates/{template_id}")
def update_template(
template_id: str,
payload: ExchangeTemplateUpdateRequest,
request: Request,
db: Session = Depends(get_db),
):
service = ExchangeService(request.app, db)
template = service.update_template(
template_id,
name=payload.name,
description=payload.description,
status=payload.status,
current_version=payload.current_version,
layout=_layout_payload(payload.layout),
meta=payload.meta,
)
return ok(ExchangeTemplateResponse.model_validate(_template_payload(template)).model_dump(mode="json"))
@router.delete("/templates/{template_id}")
def delete_template(
template_id: str,
request: Request,
db: Session = Depends(get_db),
):
service = ExchangeService(request.app, db)
service.delete_template(template_id)
return ok()
@router.get("/templates/{template_id}/versions")
def list_versions(
template_id: str,
request: Request,
db: Session = Depends(get_db),
):
service = ExchangeService(request.app, db)
return ok(
[
ExchangeTemplateVersionResponse.model_validate(_version_payload(item)).model_dump(mode="json")
for item in service.list_versions(template_id)
]
)
@router.get("/templates/{template_id}/versions/{version_id}")
def get_version(
template_id: str,
version_id: str,
request: Request,
db: Session = Depends(get_db),
):
service = ExchangeService(request.app, db)
version = service.get_version_or_404(version_id)
if version.template_id != template_id:
raise BizError("模板版本不存在", code=404)
return ok(ExchangeTemplateVersionResponse.model_validate(_version_payload(version)).model_dump(mode="json"))
@router.get("/templates/{template_id}/versions/by-version/{version}")
def get_version_by_value(
template_id: str,
version: str,
request: Request,
db: Session = Depends(get_db),
):
service = ExchangeService(request.app, db)
snapshot = service.get_snapshot(template_id=template_id, version=version)
if snapshot is None:
raise BizError("模板版本不存在", code=404)
return ok(snapshot.as_payload())
@router.post("/templates/{template_id}/versions")
def publish_version(
template_id: str,
payload: ExchangeTemplateVersionCreateRequest,
request: Request,
db: Session = Depends(get_db),
):
service = ExchangeService(request.app, db)
version = service.publish_version(
template_id=template_id,
version=payload.version,
variables=[
_variable_from_schema(item) for item in payload.variables
] if payload.variables is not None else None,
layout=_layout_payload(payload.layout),
meta=payload.meta,
make_current=payload.make_current,
)
return ok(ExchangeTemplateVersionResponse.model_validate(_version_payload(version)).model_dump(mode="json"))
@router.delete("/templates/{template_id}/versions/{version_id}")
def delete_version(
template_id: str,
version_id: str,
request: Request,
db: Session = Depends(get_db),
):
service = ExchangeService(request.app, db)
version = service.get_version_or_404(version_id)
if version.template_id != template_id:
raise BizError("模板版本不存在", code=404)
service.delete_version(version_id)
return ok()
@router.get("/template-versions/{version_id}")
def get_template_version_by_id(
version_id: str,
request: Request,
db: Session = Depends(get_db),
):
service = ExchangeService(request.app, db)
snapshot = service.get_snapshot_by_version_id(version_id)
if snapshot is None:
raise BizError("模板版本不存在", code=404)
return ok(snapshot.as_payload())
@router.get("/template-versions/{version_id}/download")
@raw_response
def download_template_version(
version_id: str,
request: Request,
db: Session = Depends(get_db),
):
service = ExchangeService(request.app, db)
content = service.build_template_file(version_id)
return StreamingResponse(
iter([content]),
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={"Content-Disposition": 'attachment; filename="template.xlsx"'},
)
@router.post("/templates/{template_id}/versions/upload")
def upload_template_version(
template_id: str,
version: str,
request: Request,
file: UploadFile = File(...),
db: Session = Depends(get_db),
):
service = ExchangeService(request.app, db)
content = file.file.read()
parsed = _excel_template_codec().load(content)
snapshot = service.publish_version(
template_id=template_id,
version=version,
variables=[_variable_from_payload(item) for item in parsed.get("variables", [])],
layout={
"title": parsed.get("title"),
"sheet_name": parsed.get("sheet_name"),
},
meta={
"source_file": file.filename,
},
file_content=content,
file_name=file.filename,
)
return ok(ExchangeTemplateVersionResponse.model_validate(_version_payload(snapshot)).model_dump(mode="json"))
@router.post("/plans/resolve")
def resolve_plan(
payload: ExchangePlanResolveRequest,
request: Request,
db: Session = Depends(get_db),
):
service = ExchangeService(request.app, db)
source = _resolve_source(payload, request, db)
plan = service.resolve_plan(
biz_domain=payload.biz_domain,
biz_obj=payload.biz_obj,
operation=payload.operation,
template_id=payload.template_id,
version_id=payload.version_id,
version=payload.version,
code=payload.code,
name=payload.name,
description=payload.description,
layout=_layout_payload(payload.layout),
variables=[
_variable_from_schema(item) for item in payload.variables
] if payload.variables is not None else None,
source=source,
)
return ok(_plan_payload(plan))
@router.post("/plans/template-file")
@raw_response
def build_plan_template_file(
payload: ExchangePlanTemplateFileRequest,
request: Request,
db: Session = Depends(get_db),
):
service = ExchangeService(request.app, db)
source = _resolve_source(payload, request, db)
plan = service.resolve_plan(
biz_domain=payload.biz_domain,
biz_obj=payload.biz_obj,
operation=payload.operation,
template_id=payload.template_id,
version_id=payload.version_id,
version=payload.version,
code=payload.code,
name=payload.name,
description=payload.description,
layout=_layout_payload(payload.layout),
variables=[
_variable_from_schema(item) for item in payload.variables
] if payload.variables is not None else None,
source=source,
)
content = source.load_template_file(plan) if source is not None else None
if content is None:
content = service.build_plan_template_file(plan)
return StreamingResponse(
iter([content]),
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={"Content-Disposition": 'attachment; filename="template.xlsx"'},
)
@router.get("/tasks")
def list_tasks(request: Request, db: Session = Depends(get_db)):
service = ExchangeService(request.app, db)
return ok(
[
ExchangeTaskResponse.model_validate(_task_payload(item)).model_dump(mode="json")
for item in service.list_tasks()
]
)
@router.get("/tasks/{task_id}")
def get_task(task_id: str, request: Request, db: Session = Depends(get_db)):
service = ExchangeService(request.app, db)
task = service.get_task_or_404(task_id)
return ok(ExchangeTaskResponse.model_validate(_task_payload(task)).model_dump(mode="json"))
@router.post("/tasks")
def create_task(
payload: ExchangeTaskCreateRequest,
request: Request,
db: Session = Depends(get_db),
):
service = ExchangeService(request.app, db)
source = _resolve_source(payload, request, db)
plan = service.resolve_plan(
biz_domain=payload.biz_domain,
biz_obj=payload.biz_obj,
operation=payload.operation,
template_id=payload.template_id,
version_id=payload.version_id,
version=payload.version,
code=payload.code,
name=payload.name,
description=payload.description,
layout=_layout_payload(payload.layout),
variables=[
_variable_from_schema(item) for item in payload.variables
] if payload.variables is not None else None,
source=source,
)
task = service.create_task(
biz_domain=plan.scope.biz_domain,
biz_obj=plan.scope.biz_obj,
operation=plan.scope.operation,
template_id=plan.template_id,
version_id=plan.version_id,
version=plan.version,
storage_key=payload.storage_key,
input_payload=payload.input_payload,
)
return ok(ExchangeTaskResponse.model_validate(_task_payload(task)).model_dump(mode="json"))
@router.post("/tasks/{task_id}/run")
def run_task(task_id: str, request: Request, db: Session = Depends(get_db)):
service = ExchangeService(request.app, db)
task = service.run_task(task_id)
return ok(ExchangeTaskResponse.model_validate(_task_payload(task)).model_dump(mode="json"))
@router.get("/tasks/{task_id}/rows")
def list_task_rows(task_id: str, request: Request, db: Session = Depends(get_db)):
service = ExchangeService(request.app, db)
return ok(
[
{
"id": item.id,
"task_id": item.task_id,
"row_index": item.row_index,
"status": item.status,
"data": item.data,
"message": item.message,
"result": item.result,
}
for item in service.list_task_rows(task_id)
]
)
def _variable_from_payload(item: dict) -> ExchangeVariable:
return ExchangeVariable(
key=item.get("key"),
label=item.get("label") or item.get("key"),
header=item.get("header"),
description=item.get("description"),
required=bool(item.get("required", False)),
example=item.get("example"),
)
def _excel_template_codec():
from .excel import ExcelTemplateCodec
return ExcelTemplateCodec()