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

287 lines
10 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 (
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()