Coverage for mcp_chatkit_widget / rendering.py: 100%
31 statements
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-11 06:37 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-11 06:37 +0000
1"""Schema parsing and template rendering helpers."""
3from __future__ import annotations
4import json
5from functools import cache
6from typing import Any
7from chatkit.widgets import WidgetRoot, WidgetTemplate
8from pydantic import BaseModel
9from mcp_chatkit_widget.naming import _sanitize_tool_name, _to_camel_case
10from mcp_chatkit_widget.pydantic_conversion import json_schema_to_pydantic
11from mcp_chatkit_widget.widget_loader import WidgetDefinition
14def _model_metadata(widget_name: str) -> tuple[str, str]:
15 """Return consistent Pydantic metadata derived from a widget name.
17 Args:
18 widget_name: The widget's identifier, which may include unsafe characters.
20 Returns:
21 A tuple containing the generated model class name and schema title that are
22 safe for Pydantic and tooling (e.g., ``FooBarModel`` and ``FooBarArguments``).
23 """
24 camel_name = _to_camel_case(_sanitize_tool_name(widget_name))
25 return f"{camel_name}Model", f"{camel_name}Arguments"
28@cache
29def _build_pydantic_model(schema_dump: str, widget_name: str) -> type[BaseModel]:
30 """Return a cached Pydantic model class for the given schema snapshot.
32 Args:
33 schema_dump: A JSON string representation of the widget's schema that is
34 already normalized (e.g., via ``json.dumps(..., sort_keys=True)``)
35 so hashing remains stable across calls.
36 widget_name: The name used to generate consistent model metadata for the
37 schema (see :func:`_model_metadata`).
39 Returns:
40 The generated :class:`pydantic.BaseModel` subclass that enforces the schema.
41 """
42 schema = json.loads(schema_dump)
43 model_name, schema_title = _model_metadata(widget_name)
44 return json_schema_to_pydantic(schema, model_name, schema_title)
47def build_widget_model(widget_def: WidgetDefinition) -> type[BaseModel]:
48 """Convert a widget definition's JSON schema into a Pydantic model."""
49 schema_dump = json.dumps(widget_def.json_schema, sort_keys=True)
50 return _build_pydantic_model(schema_dump, widget_def.name)
53@cache
54def _load_widget_template(template_path: str) -> WidgetTemplate:
55 """Load and cache a WidgetTemplate from disk."""
56 return WidgetTemplate.from_file(template_path)
59def render_widget_definition(widget_def: WidgetDefinition, **kwargs: Any) -> WidgetRoot:
60 """Validate inputs, render the widget's template, and return a WidgetRoot."""
61 model = build_widget_model(widget_def)
62 validated = model(**kwargs)
63 template = _load_widget_template(str(widget_def.file_path))
64 render_context = validated.model_dump()
65 render_context["undefined"] = None
66 return template.build(render_context)
69__all__ = [
70 "build_widget_model",
71 "render_widget_definition",
72]