from __future__ import annotations from dataclasses import dataclass from typing import Any, Protocol from iti.service_client import ServiceClient, service_client from .base import ( ExchangeOperation, ExchangeTemplateLayout, ExchangeTemplatePlan, ExchangeTemplateSourceKind, ExchangeVariable, ) class ExchangeSource(Protocol): def resolve_plan( self, *, biz_domain: str, biz_obj: str, operation: ExchangeOperation | str, template_id: str | None = None, version_id: str | None = None, version: str | None = None, code: str | None = None, name: str | None = None, description: str | None = None, layout: ExchangeTemplateLayout | dict[str, Any] | None = None, variables: list[ExchangeVariable] | None = None, ) -> ExchangeTemplatePlan: ... def load_template_file(self, plan: ExchangeTemplatePlan) -> bytes | None: ... @dataclass class MappingExchangeSource: def resolve_plan( self, *, biz_domain: str, biz_obj: str, operation: ExchangeOperation | str, template_id: str | None = None, version_id: str | None = None, version: str | None = None, code: str | None = None, name: str | None = None, description: str | None = None, layout: ExchangeTemplateLayout | dict[str, Any] | None = None, variables: list[ExchangeVariable] | None = None, ) -> ExchangeTemplatePlan: return ExchangeTemplatePlan.from_mapping( biz_domain=biz_domain, biz_obj=biz_obj, operation=operation, template_id=template_id, version_id=version_id, version=version, code=code, name=name, description=description, layout=layout, variables=variables, ) def load_template_file(self, plan: ExchangeTemplatePlan) -> bytes | None: return _excel_template_codec().dump(plan) @dataclass class LocalExchangeSource: app: Any db: Any def resolve_plan( self, *, biz_domain: str, biz_obj: str, operation: ExchangeOperation | str, template_id: str | None = None, version_id: str | None = None, version: str | None = None, code: str | None = None, name: str | None = None, description: str | None = None, layout: ExchangeTemplateLayout | dict[str, Any] | None = None, variables: list[ExchangeVariable] | None = None, ) -> ExchangeTemplatePlan: from .service import ExchangeService return ExchangeService(self.app, self.db).resolve_plan( biz_domain=biz_domain, biz_obj=biz_obj, operation=operation, template_id=template_id, version_id=version_id, version=version, code=code, name=name, description=description, layout=layout, variables=variables, ) def load_template_file(self, plan: ExchangeTemplatePlan) -> bytes | None: if plan.version_id: from .service import ExchangeService return ExchangeService(self.app, self.db).build_template_file(plan.version_id) return _excel_template_codec().dump(plan) @dataclass class RemoteExchangeSource: app: Any service_name: str = "exchange" def resolve_plan( self, *, biz_domain: str, biz_obj: str, operation: ExchangeOperation | str, template_id: str | None = None, version_id: str | None = None, version: str | None = None, code: str | None = None, name: str | None = None, description: str | None = None, layout: ExchangeTemplateLayout | dict[str, Any] | None = None, variables: list[ExchangeVariable] | None = None, ) -> ExchangeTemplatePlan: client = service_client(self.app, self.service_name) payload = self._fetch_plan( client, biz_domain=biz_domain, biz_obj=biz_obj, operation=operation, template_id=template_id, version=version, version_id=version_id, code=code, ) if payload is not None: return _plan_from_mapping(payload) return ExchangeTemplatePlan.from_mapping( biz_domain=biz_domain, biz_obj=biz_obj, operation=operation, template_id=template_id, version_id=version_id, version=version, code=code, name=name, description=description, layout=layout, variables=variables, ) def load_template_file(self, plan: ExchangeTemplatePlan) -> bytes | None: if not plan.version_id: return _excel_template_codec().dump(plan) client = service_client(self.app, self.service_name) response = client.get( f"/exchange/template-versions/{plan.version_id}/download", expect_json=False, ) return response.content def _fetch_plan( self, client: ServiceClient, *, biz_domain: str, biz_obj: str, operation: ExchangeOperation | str, template_id: str | None, version: str | None, version_id: str | None, code: str | None, ) -> dict[str, Any] | None: if version_id: return client.get(f"/exchange/template-versions/{version_id}") if template_id and version: return client.get(f"/exchange/templates/{template_id}/versions/by-version/{version}") if template_id: return client.post( "/exchange/plans/resolve", json={ "bizDomain": biz_domain, "bizObj": biz_obj, "operation": _operation_value(operation), "templateId": template_id, }, ) return client.post( "/exchange/plans/resolve", json={ "bizDomain": biz_domain, "bizObj": biz_obj, "operation": _operation_value(operation), "code": code, }, ) def _plan_from_mapping(item: dict[str, Any]) -> ExchangeTemplatePlan: scope = item.get("scope") or {} return ExchangeTemplatePlan.from_mapping( biz_domain=item.get("biz_domain") or item.get("bizDomain") or scope.get("biz_domain") or scope.get("bizDomain"), biz_obj=item.get("biz_obj") or item.get("bizObj") or scope.get("biz_obj") or scope.get("bizObj"), operation=item.get("operation") or scope.get("operation"), template_id=item.get("template_id") or item.get("templateId"), version_id=item.get("version_id") or item.get("versionId") or item.get("id"), version=item.get("version"), code=item.get("code"), name=item.get("name"), description=item.get("description"), layout=item.get("layout"), variables=[_variable_from_mapping(value) for value in item.get("variables", [])], ) def _variable_from_mapping(item: dict[str, Any]) -> 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 _operation_value(value: ExchangeOperation | str) -> str: return value.value if hasattr(value, "value") else str(value) def _excel_template_codec(): from .excel import ExcelTemplateCodec return ExcelTemplateCodec() def get_exchange_source( app: Any, *, source_kind: ExchangeTemplateSourceKind | str = ExchangeTemplateSourceKind.MAPPING, source_name: str | None = None, db: Any | None = None, service_name: str = "exchange", ) -> ExchangeSource: if source_name: from .registry import get_exchange_registry source = get_exchange_registry(app).get_source(source_name) if source is None: raise ValueError(f"exchange source not registered: {source_name}") return source # type: ignore[return-value] kind = ExchangeTemplateSourceKind(source_kind) if kind == ExchangeTemplateSourceKind.LOCAL: if db is None: raise ValueError("local exchange source requires db") return LocalExchangeSource(app=app, db=db) if kind == ExchangeTemplateSourceKind.REMOTE: return RemoteExchangeSource(app=app, service_name=service_name) if kind == ExchangeTemplateSourceKind.CUSTOM: raise ValueError("custom exchange source requires source_name") return MappingExchangeSource()