from __future__ import annotations from dataclasses import dataclass from typing import Any, Protocol from iti.service_client import ServiceClient, service_client from .base import ( ExchangeField, ExchangePlaceholder, ExchangePlan, ExchangeTemplateBinding, ExchangeTemplateKind, ExchangeTemplateSource, ExchangeTemplateSourceKind, ) from .excel import ExcelTemplateCodec class ExchangeSource(Protocol): def resolve_plan( self, *, template_kind: ExchangeTemplateKind | str, template_id: str | None = None, version_id: str | None = None, version: str | None = None, bindings: list[ExchangeTemplateBinding] | None = None, fields: list[ExchangeField] | None = None, placeholders: list[ExchangePlaceholder] | None = None, title: str | None = None, description: str | None = None, sheet_name: str | None = None, meta: dict[str, Any] | None = None, source: ExchangeTemplateSource | None = None, ) -> ExchangePlan: ... def load_template_file(self, plan: ExchangePlan) -> bytes | None: ... @dataclass class MappingExchangeSource: def resolve_plan( self, *, template_kind: ExchangeTemplateKind | str, template_id: str | None = None, version_id: str | None = None, version: str | None = None, bindings: list[ExchangeTemplateBinding] | None = None, fields: list[ExchangeField] | None = None, placeholders: list[ExchangePlaceholder] | None = None, title: str | None = None, description: str | None = None, sheet_name: str | None = None, meta: dict[str, Any] | None = None, source: ExchangeTemplateSource | None = None, ) -> ExchangePlan: if source is not None: return source.to_plan() return ExchangePlan.from_mapping( template_kind=template_kind, template_id=template_id, version_id=version_id, version=version, bindings=bindings, fields=fields, placeholders=placeholders, title=title, description=description, sheet_name=sheet_name, meta=meta, ) def load_template_file(self, plan: ExchangePlan) -> bytes | None: return ExcelTemplateCodec().dump(plan) @dataclass class LocalExchangeSource: app: Any db: Any def resolve_plan( self, *, template_kind: ExchangeTemplateKind | str, template_id: str | None = None, version_id: str | None = None, version: str | None = None, bindings: list[ExchangeTemplateBinding] | None = None, fields: list[ExchangeField] | None = None, placeholders: list[ExchangePlaceholder] | None = None, title: str | None = None, description: str | None = None, sheet_name: str | None = None, meta: dict[str, Any] | None = None, source: ExchangeTemplateSource | None = None, ) -> ExchangePlan: from .service import ExchangeService service = ExchangeService(self.app, self.db) if source is not None: return source.to_plan() if version_id: snapshot = service.get_snapshot_by_version_id(version_id) if snapshot is not None: return snapshot.to_plan() if template_id: snapshot = service.get_current_snapshot(template_id) if snapshot is not None: return snapshot.to_plan() return ExchangePlan.from_mapping( template_kind=template_kind, template_id=template_id, version_id=version_id, version=version, bindings=bindings, fields=fields, placeholders=placeholders, title=title, description=description, sheet_name=sheet_name, meta=meta, ) def load_template_file(self, plan: ExchangePlan) -> bytes | None: if plan.version_id: from .service import ExchangeService return ExchangeService(self.app, self.db).build_template_file(plan.version_id) return ExcelTemplateCodec().dump(plan) @dataclass class RemoteExchangeSource: app: Any service_name: str = "exchange" def resolve_plan( self, *, template_kind: ExchangeTemplateKind | str, template_id: str | None = None, version_id: str | None = None, version: str | None = None, bindings: list[ExchangeTemplateBinding] | None = None, fields: list[ExchangeField] | None = None, placeholders: list[ExchangePlaceholder] | None = None, title: str | None = None, description: str | None = None, sheet_name: str | None = None, meta: dict[str, Any] | None = None, source: ExchangeTemplateSource | None = None, ) -> ExchangePlan: if source is not None: return source.to_plan() client = service_client(self.app, self.service_name) payload = self._fetch_plan(client, template_id=template_id, version=version, version_id=version_id) if payload is not None: meta = payload.get("meta") or {} return ExchangePlan.from_mapping( template_kind=payload.get("template_kind") or template_kind, template_id=payload.get("template_id") or template_id, version_id=payload.get("id") or version_id, version=payload.get("version") or version, bindings=[_binding_from_mapping(item) for item in payload.get("bindings", [])], fields=[_field_from_mapping(item) for item in payload.get("fields", [])], placeholders=[ _placeholder_from_mapping(item) for item in payload.get("placeholders", []) ], title=payload.get("title") or meta.get("title") or title, description=payload.get("description") or meta.get("description") or description, sheet_name=payload.get("sheet_name") or meta.get("sheet_name") or sheet_name, meta=meta or payload.get("meta") or {}, ) return ExchangePlan.from_mapping( template_kind=template_kind, template_id=template_id, version_id=version_id, version=version, bindings=bindings, fields=fields, placeholders=placeholders, title=title, description=description, sheet_name=sheet_name, meta=meta, ) def load_template_file(self, plan: ExchangePlan) -> bytes | None: if not plan.version_id: return ExcelTemplateCodec().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, *, template_id: str | None, version: str | None, version_id: 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/{version}") if template_id: template = client.get(f"/exchange/templates/{template_id}") current_version = (template or {}).get("current_version") if current_version: return client.get(f"/exchange/templates/{template_id}/versions/{current_version}") return None def _binding_from_mapping(item: dict[str, Any]) -> ExchangeTemplateBinding: return ExchangeTemplateBinding( entity=item.get("entity"), template_kind=item.get("template_kind") or item.get("templateKind"), handler=item.get("handler"), description=item.get("description"), default_sheet_name=item.get("default_sheet_name") or item.get("defaultSheetName"), default_file_name=item.get("default_file_name") or item.get("defaultFileName"), title=item.get("title"), meta=item.get("meta") or {}, ) def _field_from_mapping(item: dict[str, Any]) -> ExchangeField: return ExchangeField( key=item.get("key"), label=item.get("label"), placeholder=item.get("placeholder"), required=bool(item.get("required", False)), example=item.get("example"), width=item.get("width"), format=item.get("format"), source=item.get("source"), target=item.get("target"), options=tuple(tuple(option) for option in item.get("options") or []), meta=item.get("meta") or {}, ) def _placeholder_from_mapping(item: dict[str, Any]) -> ExchangePlaceholder: return ExchangePlaceholder( key=item.get("key"), label=item.get("label"), description=item.get("description"), required=bool(item.get("required", False)), example=item.get("example"), ) 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()