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/sources.py

275 lines
8.8 KiB
Python

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()