Add render export adapter contract

This commit is contained in:
2026-05-15 13:40:25 +02:00
parent 6cc44da628
commit 2887d57fa9
13 changed files with 1092 additions and 10 deletions

View File

@@ -259,6 +259,25 @@ from markitect_tool.runtime import (
load_runtime_context_file_result,
run_contract_assessments,
)
from markitect_tool.render import (
RENDER_EXPORT_ADAPTER_ENTRY_POINT_GROUP,
RENDER_EXPORT_ADAPTER_KIND,
RENDER_EXPORT_SCHEMA_VERSION,
FakeRenderExportAdapter,
RenderArtifact,
RenderExportAdapter,
RenderExportAdapterDescriptor,
RenderExportAdapterError,
RenderExportAdapterRegistry,
RenderExportRequest,
RenderExportResult,
RenderProvenance,
default_render_export_adapter_registry,
discover_render_export_adapters,
render_capability_diagnostics,
render_export_registry_descriptor,
render_with_adapter,
)
from markitect_tool.schema import (
MarkdownSchema,
SchemaValidationResult,
@@ -570,6 +589,23 @@ __all__ = [
"load_runtime_context_file",
"load_runtime_context_file_result",
"run_contract_assessments",
"RENDER_EXPORT_ADAPTER_ENTRY_POINT_GROUP",
"RENDER_EXPORT_ADAPTER_KIND",
"RENDER_EXPORT_SCHEMA_VERSION",
"FakeRenderExportAdapter",
"RenderArtifact",
"RenderExportAdapter",
"RenderExportAdapterDescriptor",
"RenderExportAdapterError",
"RenderExportAdapterRegistry",
"RenderExportRequest",
"RenderExportResult",
"RenderProvenance",
"default_render_export_adapter_registry",
"discover_render_export_adapters",
"render_capability_diagnostics",
"render_export_registry_descriptor",
"render_with_adapter",
"MissingTemplateVariable",
"TemplateAnalysis",
"TemplateError",

View File

@@ -5,6 +5,10 @@ from __future__ import annotations
from markitect_tool.extension.registry import ExtensionDescriptor, ExtensionRegistry
from markitect_tool.extension.processing import ProcessingCapability
from markitect_tool.query import default_query_engine_registry
from markitect_tool.render import (
default_render_export_adapter_registry,
render_export_registry_descriptor,
)
from markitect_tool.source import (
default_source_adapter_registry,
source_adapter_registry_descriptor,
@@ -28,9 +32,12 @@ def builtin_extension_registry() -> ExtensionRegistry:
_memory_graph_contract_descriptor(),
_memory_runtime_adapter_descriptor(),
_agent_memory_descriptor(),
render_export_registry_descriptor(),
source_adapter_registry_descriptor(),
]:
registry.register(descriptor)
for descriptor in default_render_export_adapter_registry().extension_descriptors():
registry.register(descriptor)
for descriptor in default_source_adapter_registry().extension_descriptors():
registry.register(descriptor)
return registry

View File

@@ -0,0 +1,41 @@
"""Render/export adapter contracts."""
from markitect_tool.render.engine import (
RENDER_EXPORT_ADAPTER_ENTRY_POINT_GROUP,
RENDER_EXPORT_ADAPTER_KIND,
RENDER_EXPORT_SCHEMA_VERSION,
FakeRenderExportAdapter,
RenderArtifact,
RenderExportAdapter,
RenderExportAdapterDescriptor,
RenderExportAdapterError,
RenderExportAdapterRegistry,
RenderExportRequest,
RenderExportResult,
RenderProvenance,
default_render_export_adapter_registry,
discover_render_export_adapters,
render_capability_diagnostics,
render_export_registry_descriptor,
render_with_adapter,
)
__all__ = [
"RENDER_EXPORT_ADAPTER_ENTRY_POINT_GROUP",
"RENDER_EXPORT_ADAPTER_KIND",
"RENDER_EXPORT_SCHEMA_VERSION",
"FakeRenderExportAdapter",
"RenderArtifact",
"RenderExportAdapter",
"RenderExportAdapterDescriptor",
"RenderExportAdapterError",
"RenderExportAdapterRegistry",
"RenderExportRequest",
"RenderExportResult",
"RenderProvenance",
"default_render_export_adapter_registry",
"discover_render_export_adapters",
"render_capability_diagnostics",
"render_export_registry_descriptor",
"render_with_adapter",
]

View File

@@ -0,0 +1,725 @@
"""Contract-only render/export adapter framework."""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
import hashlib
import importlib.metadata
import json
from pathlib import Path
from typing import Any, Iterable, Protocol, runtime_checkable
from markitect_tool.diagnostics import Diagnostic, SourceLocation, has_error
from markitect_tool.extension import (
ExtensionDependencyCheck,
ExtensionDescriptor,
OptionalDependency,
ProcessingCapability,
)
RENDER_EXPORT_SCHEMA_VERSION = "markitect.render.export.v1"
RENDER_EXPORT_ADAPTER_KIND = "render-export"
RENDER_EXPORT_ADAPTER_ENTRY_POINT_GROUP = "markitect_tool.render_export_adapters"
RENDER_OPERATIONS = {
"inspect-profile",
"export-source",
"render-artifact",
}
RENDER_PROFILES = {
"plain",
"docs",
"slides",
"paged",
"static-site",
"pdf",
}
_SAFETY_POLICY_FLAGS = {
"filesystem_read": "filesystem_read",
"filesystem_write": "filesystem_write",
"external_process": "external_process",
"network": "network",
"native_renderer_dependency": "native_renderer_dependency",
"assisted_generation": "assisted_generation",
"publication_side_effect": "publication_side_effect",
}
class RenderExportAdapterError(ValueError):
"""Raised when render/export adapter contracts are invalid."""
@dataclass(frozen=True)
class RenderArtifact:
"""Metadata for a rendered or exported artifact."""
artifact_id: str
role: str
media_type: str
content: str | None = None
path: str | None = None
uri: str | None = None
digest: str | None = None
metadata: dict[str, Any] = field(default_factory=dict)
@classmethod
def from_content(
cls,
content: str,
*,
role: str,
media_type: str,
artifact_id: str | None = None,
metadata: dict[str, Any] | None = None,
) -> "RenderArtifact":
digest = _digest_text(content)
return cls(
artifact_id=artifact_id or f"artifact:{digest.removeprefix('sha256:')[:16]}",
role=role,
media_type=media_type,
content=content,
digest=digest,
metadata=metadata or {},
)
def to_dict(self) -> dict[str, Any]:
return _drop_empty(asdict(self))
@dataclass(frozen=True)
class RenderProvenance:
"""Source-to-render provenance envelope."""
operation: str
adapter_id: str
profile: str
source_path: str | None = None
source_digest: str | None = None
artifact_id: str | None = None
metadata: dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> dict[str, Any]:
return _drop_empty(asdict(self))
@dataclass(frozen=True)
class RenderExportRequest:
"""Service-free render/export request."""
source: str
operation: str = "render-artifact"
profile: str = "plain"
source_path: str | None = None
options: dict[str, Any] = field(default_factory=dict)
policy: dict[str, Any] = field(default_factory=dict)
schema_version: str = RENDER_EXPORT_SCHEMA_VERSION
metadata: dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> dict[str, Any]:
return _drop_empty(
{
"schema_version": self.schema_version,
"operation": self.operation,
"profile": self.profile,
"source": self.source,
"source_path": self.source_path,
"options": self.options,
"policy": self.policy,
"metadata": self.metadata,
}
)
@dataclass(frozen=True)
class RenderExportResult:
"""Result of a render/export adapter operation."""
adapter: dict[str, Any]
operation: str
profile: str
artifacts: list[RenderArtifact] = field(default_factory=list)
exported_source: str | None = None
diagnostics: list[Diagnostic] = field(default_factory=list)
provenance: list[RenderProvenance] = field(default_factory=list)
schema_version: str = RENDER_EXPORT_SCHEMA_VERSION
metadata: dict[str, Any] = field(default_factory=dict)
@property
def valid(self) -> bool:
return not has_error(self.diagnostics)
def to_dict(self) -> dict[str, Any]:
return _drop_empty(
{
"schema_version": self.schema_version,
"valid": self.valid,
"adapter": self.adapter,
"operation": self.operation,
"profile": self.profile,
"artifacts": [artifact.to_dict() for artifact in self.artifacts],
"exported_source": self.exported_source,
"diagnostics": [diagnostic.to_dict() for diagnostic in self.diagnostics],
"provenance": [event.to_dict() for event in self.provenance],
"metadata": self.metadata,
}
)
@runtime_checkable
class RenderExportAdapter(Protocol):
"""Render/export adapter protocol."""
descriptor: "RenderExportAdapterDescriptor"
def render(self, request: RenderExportRequest) -> RenderExportResult:
"""Run one render/export operation."""
RenderExportAdapterFactory = Any
@dataclass(frozen=True)
class RenderExportAdapterDescriptor:
"""Inspectable descriptor for one render/export adapter."""
id: str
version: str
name: str
operations: list[str]
input_contracts: list[str]
output_profiles: list[str]
artifact_media_types: list[str]
factory: RenderExportAdapterFactory = field(compare=False, repr=False)
summary: str | None = None
option_schema: dict[str, Any] = field(default_factory=dict)
optional_dependencies: list[OptionalDependency] = field(default_factory=list)
safety: dict[str, Any] = field(default_factory=dict)
quality_profile: dict[str, Any] = field(default_factory=dict)
metadata: dict[str, Any] = field(default_factory=dict)
def __post_init__(self) -> None:
if not self.id.strip():
raise RenderExportAdapterError("Render/export adapter id cannot be empty")
if not self.name.strip():
raise RenderExportAdapterError("Render/export adapter name cannot be empty")
unsupported_operations = sorted(set(self.operations) - RENDER_OPERATIONS)
if unsupported_operations:
raise RenderExportAdapterError(
"Unsupported render/export operation(s): " + ", ".join(unsupported_operations)
)
if not self.operations:
raise RenderExportAdapterError("Render/export adapter must declare at least one operation")
unsupported_profiles = sorted(set(self.output_profiles) - RENDER_PROFILES)
if unsupported_profiles:
raise RenderExportAdapterError(
"Unsupported render/export profile(s): " + ", ".join(unsupported_profiles)
)
object.__setattr__(self, "artifact_media_types", [value.lower() for value in self.artifact_media_types])
def instantiate(self) -> RenderExportAdapter:
adapter = self.factory()
if not isinstance(adapter, RenderExportAdapter):
missing = ["render"] if not callable(getattr(adapter, "render", None)) else []
if missing:
raise RenderExportAdapterError(
f"Render/export adapter `{self.id}` is missing method(s): {', '.join(missing)}"
)
return adapter
def to_dict(self) -> dict[str, Any]:
return _drop_empty(
{
"id": self.id,
"kind": RENDER_EXPORT_ADAPTER_KIND,
"version": self.version,
"name": self.name,
"summary": self.summary,
"operations": self.operations,
"input_contracts": self.input_contracts,
"output_profiles": self.output_profiles,
"artifact_media_types": self.artifact_media_types,
"option_schema": self.option_schema,
"optional_dependencies": [
dependency.to_dict() for dependency in self.optional_dependencies
],
"safety": self.safety,
"quality_profile": self.quality_profile,
"metadata": self.metadata,
"capabilities": [
capability.to_dict() for capability in _render_export_capabilities()
],
"docs": ["docs/render-export-adapters.md"],
}
)
def to_extension_descriptor(self) -> ExtensionDescriptor:
return ExtensionDescriptor(
id=self.id,
kind=RENDER_EXPORT_ADAPTER_KIND,
version=self.version,
summary=self.summary or self.name,
capabilities=_render_export_capabilities(),
optional_dependencies=self.optional_dependencies,
safety=self.safety,
input_contract="RenderExportRequest",
output_contract="RenderExportResult",
diagnostics_namespace="render",
provenance_prefix=self.id,
docs=["docs/render-export-adapters.md"],
examples=["examples/render/fake-render-request.yaml"],
metadata={
"render_export_adapter": {
"name": self.name,
"operations": self.operations,
"input_contracts": self.input_contracts,
"output_profiles": self.output_profiles,
"artifact_media_types": self.artifact_media_types,
"option_schema": self.option_schema,
"quality_profile": self.quality_profile,
"metadata": self.metadata,
}
},
)
class RenderExportAdapterRegistry:
"""Registry of render/export adapter descriptors."""
def __init__(self, descriptors: Iterable[RenderExportAdapterDescriptor] | None = None) -> None:
self._descriptors: dict[str, RenderExportAdapterDescriptor] = {}
for descriptor in descriptors or []:
self.register(descriptor)
def register(self, descriptor: RenderExportAdapterDescriptor) -> None:
if descriptor.id in self._descriptors:
raise RenderExportAdapterError(f"Duplicate render/export adapter id `{descriptor.id}`")
self._descriptors[descriptor.id] = descriptor
def get(self, adapter_id: str) -> RenderExportAdapterDescriptor:
try:
return self._descriptors[adapter_id]
except KeyError as exc:
raise RenderExportAdapterError(f"Unknown render/export adapter `{adapter_id}`") from exc
def list(self) -> list[RenderExportAdapterDescriptor]:
return [self._descriptors[key] for key in sorted(self._descriptors)]
def check_dependencies(
self,
adapter_id: str,
*,
available_modules: set[str] | None = None,
) -> ExtensionDependencyCheck:
descriptor = self.get(adapter_id)
available = (
available_modules
if available_modules is not None
else _available_modules(dependency.name for dependency in descriptor.optional_dependencies)
)
missing: list[str] = []
optional_missing: list[str] = []
for dependency in descriptor.optional_dependencies:
if dependency.name in available:
continue
if dependency.required:
missing.append(dependency.name)
else:
optional_missing.append(dependency.name)
return ExtensionDependencyCheck(
extension_id=adapter_id,
missing=missing,
optional_missing=optional_missing,
)
def to_dict(self) -> dict[str, Any]:
adapters = []
for descriptor in self.list():
data = descriptor.to_dict()
data["dependency_check"] = self.check_dependencies(descriptor.id).to_dict()
adapters.append(data)
return {"count": len(adapters), "adapters": adapters}
def extension_descriptors(self) -> list[ExtensionDescriptor]:
return [descriptor.to_extension_descriptor() for descriptor in self.list()]
class FakeRenderExportAdapter:
"""Deterministic no-op renderer used for contract tests."""
def __init__(self) -> None:
self.descriptor = fake_render_export_adapter_descriptor()
def render(self, request: RenderExportRequest) -> RenderExportResult:
diagnostics = render_capability_diagnostics(self.descriptor, request)
diagnostics.extend(_validate_request(self.descriptor, request))
adapter = {
"id": self.descriptor.id,
"version": self.descriptor.version,
"name": self.descriptor.name,
}
if diagnostics:
return RenderExportResult(
adapter=adapter,
operation=request.operation,
profile=request.profile,
diagnostics=diagnostics,
)
exported = _fake_exported_source(request)
provenance = [
RenderProvenance(
operation=request.operation,
adapter_id=self.descriptor.id,
profile=request.profile,
source_path=request.source_path,
source_digest=_digest_text(request.source),
metadata={"deterministic_fake": True},
)
]
if request.operation == "inspect-profile":
return RenderExportResult(
adapter=adapter,
operation=request.operation,
profile=request.profile,
provenance=provenance,
metadata={
"profile": request.profile,
"supported_operations": self.descriptor.operations,
"external_renderer_invoked": False,
},
)
if request.operation == "export-source":
artifact = RenderArtifact.from_content(
exported,
role="renderer-source",
media_type="text/markdown",
metadata={"profile": request.profile},
)
provenance = [
_event_with_artifact(event, artifact.artifact_id)
for event in provenance
]
return RenderExportResult(
adapter=adapter,
operation=request.operation,
profile=request.profile,
artifacts=[artifact],
exported_source=exported,
provenance=provenance,
metadata={"external_renderer_invoked": False},
)
rendered = f"FAKE RENDER ARTIFACT\nprofile: {request.profile}\n\n{exported}"
artifact = RenderArtifact.from_content(
rendered,
role="rendered-artifact",
media_type=_fake_media_type(request.profile),
metadata={"profile": request.profile, "fake_renderer": True},
)
provenance = [_event_with_artifact(event, artifact.artifact_id) for event in provenance]
return RenderExportResult(
adapter=adapter,
operation=request.operation,
profile=request.profile,
artifacts=[artifact],
exported_source=exported,
provenance=provenance,
metadata={"external_renderer_invoked": False},
)
def render_with_adapter(
request: RenderExportRequest,
*,
registry: RenderExportAdapterRegistry | None = None,
adapter_id: str = "render.fake",
) -> RenderExportResult:
"""Render/export through a registered adapter."""
registry = registry or default_render_export_adapter_registry()
descriptor = registry.get(adapter_id)
dependency_check = registry.check_dependencies(adapter_id)
if not dependency_check.compatible:
return RenderExportResult(
adapter={"id": descriptor.id, "version": descriptor.version, "name": descriptor.name},
operation=request.operation,
profile=request.profile,
diagnostics=[
_render_error(
"render.missing_dependency",
f"Render/export adapter `{descriptor.id}` is missing required dependencies.",
details=dependency_check.to_dict(),
)
],
)
return descriptor.instantiate().render(request)
def render_capability_diagnostics(
descriptor: RenderExportAdapterDescriptor,
request: RenderExportRequest,
) -> list[Diagnostic]:
"""Return diagnostics for capabilities blocked by request policy."""
policy = request.policy or {}
blocked = set(policy.get("blocked_capabilities") or [])
denied = []
for capability in _render_export_capabilities():
if capability.id in blocked:
denied.append(capability.id)
for safety_key, policy_key in _SAFETY_POLICY_FLAGS.items():
if descriptor.safety.get(safety_key) and policy.get(policy_key) is False:
denied.append(policy_key)
if not denied:
return []
return [
_render_error(
"render.capability_blocked",
f"Render/export adapter `{descriptor.id}` requires blocked capabilities.",
details={"adapter_id": descriptor.id, "capabilities": sorted(set(denied))},
)
]
def default_render_export_adapter_registry() -> RenderExportAdapterRegistry:
"""Return the built-in render/export adapter registry."""
return RenderExportAdapterRegistry([*discover_render_export_adapters(), fake_render_export_adapter_descriptor()])
def discover_render_export_adapters() -> list[RenderExportAdapterDescriptor]:
"""Discover package-provided render/export adapter descriptors."""
descriptors: list[RenderExportAdapterDescriptor] = []
for entry_point in _entry_points():
try:
descriptors.extend(_normalize_entry_point_result(entry_point.load()))
except Exception as exc: # pragma: no cover - defensive boundary
descriptors.append(_failed_discovery_descriptor(entry_point.name, exc))
return descriptors
def render_export_registry_descriptor() -> ExtensionDescriptor:
"""Descriptor for the render/export adapter registry itself."""
return ExtensionDescriptor(
id="render.export-registry",
kind="render-export-registry",
summary="Registry and descriptor contract for optional render/export adapters.",
capabilities=[
ProcessingCapability(id="render_export_adapters", kind="discover"),
ProcessingCapability(id="diagnostics", kind="emit"),
ProcessingCapability(id="provenance", kind="emit"),
],
safety={"network": False, "external_process": False, "filesystem_write": False},
input_contract="RenderExportAdapterDescriptor entry points",
output_contract="RenderExportAdapterRegistry",
diagnostics_namespace="render",
provenance_prefix="render.export_registry",
docs=["docs/render-export-adapters.md"],
examples=["examples/render/fake-render-request.yaml"],
metadata={
"entry_point_group": RENDER_EXPORT_ADAPTER_ENTRY_POINT_GROUP,
"schema_version": RENDER_EXPORT_SCHEMA_VERSION,
"concrete_renderer_execution_required": False,
},
)
def fake_render_export_adapter_descriptor() -> RenderExportAdapterDescriptor:
"""Descriptor for the built-in fake render/export adapter."""
return RenderExportAdapterDescriptor(
id="render.fake",
version="1",
name="Deterministic Fake Renderer",
summary="Fake renderer for render/export contract tests; never invokes external processes.",
operations=["inspect-profile", "export-source", "render-artifact"],
input_contracts=["Markdown", "DocumentFunctionEvaluationResult"],
output_profiles=["plain", "docs", "slides", "paged", "static-site", "pdf"],
artifact_media_types=["text/plain", "text/markdown"],
factory=FakeRenderExportAdapter,
safety={
"filesystem_read": False,
"filesystem_write": False,
"external_process": False,
"network": False,
"native_renderer_dependency": False,
"assisted_generation": False,
"publication_side_effect": False,
},
quality_profile={"deterministic_fake": True, "layout_fidelity": "none"},
metadata={
"external_renderer_invoked": False,
"quarkdown_dependency": False,
"contract_fixture": True,
},
)
def _validate_request(
descriptor: RenderExportAdapterDescriptor,
request: RenderExportRequest,
) -> list[Diagnostic]:
diagnostics: list[Diagnostic] = []
if request.schema_version != RENDER_EXPORT_SCHEMA_VERSION:
diagnostics.append(
_render_error(
"render.schema_version",
f"Expected schema_version `{RENDER_EXPORT_SCHEMA_VERSION}`.",
severity="warning",
details={"actual": request.schema_version},
)
)
if request.operation not in descriptor.operations:
diagnostics.append(
_render_error(
"render.operation_unsupported",
f"Adapter `{descriptor.id}` does not support operation `{request.operation}`.",
details={"adapter_id": descriptor.id, "operation": request.operation},
)
)
if request.profile not in descriptor.output_profiles:
diagnostics.append(
_render_error(
"render.profile_unsupported",
f"Adapter `{descriptor.id}` does not support profile `{request.profile}`.",
details={"adapter_id": descriptor.id, "profile": request.profile},
)
)
if not request.source and request.operation != "inspect-profile":
diagnostics.append(_render_error("render.source_missing", "Render/export request source cannot be empty."))
return diagnostics
def _render_export_capabilities() -> list[ProcessingCapability]:
return [
ProcessingCapability(id="render_export", kind="execute"),
ProcessingCapability(id="renderer_source", kind="emit"),
ProcessingCapability(id="render_artifact", kind="emit"),
ProcessingCapability(id="diagnostics", kind="emit"),
ProcessingCapability(id="provenance", kind="emit"),
]
def _event_with_artifact(event: RenderProvenance, artifact_id: str) -> RenderProvenance:
return RenderProvenance(
operation=event.operation,
adapter_id=event.adapter_id,
profile=event.profile,
source_path=event.source_path,
source_digest=event.source_digest,
artifact_id=artifact_id,
metadata=event.metadata,
)
def _fake_exported_source(request: RenderExportRequest) -> str:
return (
f"<!-- render.fake profile={request.profile} operation={request.operation} -->\n\n"
+ request.source
)
def _fake_media_type(profile: str) -> str:
if profile == "pdf":
return "text/plain"
if profile in {"docs", "static-site"}:
return "text/html"
return "text/plain"
def _digest_text(value: str) -> str:
return "sha256:" + hashlib.sha256(value.encode("utf-8")).hexdigest()
def _available_modules(module_names: Iterable[str]) -> set[str]:
import importlib.util
return {
module_name
for module_name in module_names
if importlib.util.find_spec(module_name) is not None
}
def _entry_points() -> list[Any]:
entry_points = importlib.metadata.entry_points()
if hasattr(entry_points, "select"):
return list(entry_points.select(group=RENDER_EXPORT_ADAPTER_ENTRY_POINT_GROUP))
return list(entry_points.get(RENDER_EXPORT_ADAPTER_ENTRY_POINT_GROUP, []))
def _normalize_entry_point_result(loaded: Any) -> list[RenderExportAdapterDescriptor]:
value = loaded() if callable(loaded) and not isinstance(loaded, RenderExportAdapterDescriptor) else loaded
if isinstance(value, RenderExportAdapterDescriptor):
return [value]
if isinstance(value, Iterable) and not isinstance(value, (str, bytes, dict)):
descriptors = list(value)
if all(isinstance(descriptor, RenderExportAdapterDescriptor) for descriptor in descriptors):
return descriptors
raise RenderExportAdapterError("Render/export adapter entry point must return descriptor objects")
def _failed_discovery_descriptor(name: str, exc: Exception) -> RenderExportAdapterDescriptor:
def factory() -> RenderExportAdapter:
return _FailedRenderExportAdapter(name, exc)
return RenderExportAdapterDescriptor(
id=f"render.discovery-failed.{name}",
version="0",
name=f"Failed render/export adapter: {name}",
operations=["inspect-profile"],
input_contracts=["RenderExportRequest"],
output_profiles=["plain"],
artifact_media_types=[],
factory=factory,
summary="Failed render/export adapter discovery placeholder.",
safety={"filesystem_read": False, "filesystem_write": False, "network": False},
metadata={"error": str(exc)},
)
class _FailedRenderExportAdapter:
def __init__(self, name: str, exc: Exception) -> None:
self.descriptor = _failed_discovery_descriptor(name, exc)
self._error = exc
def render(self, request: RenderExportRequest) -> RenderExportResult:
return RenderExportResult(
adapter={"id": self.descriptor.id, "version": self.descriptor.version, "name": self.descriptor.name},
operation=request.operation,
profile=request.profile,
diagnostics=[
_render_error(
"render.discovery_failed",
f"Render/export adapter `{self.descriptor.id}` failed during discovery.",
details={"error": str(self._error)},
)
],
)
def _render_error(
code: str,
message: str,
*,
severity: str = "error",
details: dict[str, Any] | None = None,
) -> Diagnostic:
return Diagnostic(
severity=severity,
code=code,
message=message,
source=SourceLocation(path="<render>"),
details=details or {},
)
def _drop_empty(data: dict[str, Any]) -> dict[str, Any]:
return {
key: value
for key, value in data.items()
if value not in (None, [], {}, "")
}