host.controller_web_editor.routes.export

Server-side PNG/PDF export wired to host.controller_config.print_render.

The frontend POSTs cannot stream binaries cleanly, so this endpoint is GET-able and returns the rendered file with a download disposition. Same shape as --export on the Tkinter CLI — pipelines can curl this endpoint to bake printable assets.

Only YAML files under host.controller_web_editor.paths are accepted; the caller passes the same path parameter used by /api/config.

 1"""Server-side PNG/PDF export wired to :mod:`host.controller_config.print_render`.
 2
 3The frontend POSTs cannot stream binaries cleanly, so this endpoint is
 4``GET``-able and returns the rendered file with a download disposition.
 5Same shape as ``--export`` on the Tkinter CLI — pipelines can curl this
 6endpoint to bake printable assets.
 7
 8Only YAML files under :mod:`host.controller_web_editor.paths` are
 9accepted; the caller passes the same ``path`` parameter used by
10``/api/config``.
11"""
12
13from __future__ import annotations
14
15from pathlib import Path
16
17from host.controller_config.print_render import render_to_bytes
18from utils.controller.config_io import load_config
19
20
21ALLOWED_ORIENTATIONS = frozenset({"portrait", "landscape"})
22ALLOWED_FORMATS = frozenset({"png", "pdf"})
23
24MIME = {
25    "png": "image/png",
26    "pdf": "application/pdf",
27}
28
29
30class ExportRequestError(ValueError):
31    """Bad client-supplied export parameters (orientation, format)."""
32
33
34def _icon_loader():
35    """Reuse the Tkinter tool's icon loader if available; otherwise None.
36
37    Print output benefits from the small button glyphs but they aren't
38    required — ``print_render`` falls back to text-only labels.
39    """
40    try:
41        from host.controller_config.icon_loader import InputIconLoader
42        from host.controller_config.main import _get_project_root
43    except Exception:
44        return None
45    icons_dir = (_get_project_root()
46                 / "images" / "XboxControlIcons" / "Buttons Full Solid")
47    if not icons_dir.is_dir():
48        return None
49    try:
50        return InputIconLoader(icons_dir)
51    except Exception:
52        return None
53
54
55def export_from_yaml(
56    yaml_path: Path,
57    orientation: str,
58    fmt: str,
59    hide_unassigned: bool = False,
60) -> tuple[bytes, str, str]:
61    """Render an export from a YAML file on disk.
62
63    Returns ``(bytes, content_type, suggested_filename)``.  Raises
64    :class:`ExportRequestError` for bad orientation/format, or whatever
65    ``load_config`` / ``render_to_bytes`` raise for missing or
66    inconsistent configs.
67    """
68    orientation = orientation.lower()
69    fmt = fmt.lower().lstrip(".")
70    if orientation not in ALLOWED_ORIENTATIONS:
71        raise ExportRequestError(
72            f"orientation must be one of {sorted(ALLOWED_ORIENTATIONS)}; "
73            f"got {orientation!r}")
74    if fmt not in ALLOWED_FORMATS:
75        raise ExportRequestError(
76            f"format must be one of {sorted(ALLOWED_FORMATS)}; got {fmt!r}")
77
78    config = load_config(yaml_path)
79    data = render_to_bytes(
80        config, orientation, fmt,
81        label_positions=None,
82        hide_unassigned=hide_unassigned,
83        icon_loader=_icon_loader(),
84    )
85    stem = yaml_path.stem or "controllers"
86    filename = f"{stem}_{orientation}.{fmt}"
87    return data, MIME[fmt], filename
88
89
90__all__ = [
91    "ALLOWED_FORMATS",
92    "ALLOWED_ORIENTATIONS",
93    "ExportRequestError",
94    "MIME",
95    "export_from_yaml",
96]
ALLOWED_FORMATS = frozenset({'png', 'pdf'})
ALLOWED_ORIENTATIONS = frozenset({'landscape', 'portrait'})
class ExportRequestError(builtins.ValueError):
31class ExportRequestError(ValueError):
32    """Bad client-supplied export parameters (orientation, format)."""

Bad client-supplied export parameters (orientation, format).

MIME = {'png': 'image/png', 'pdf': 'application/pdf'}
def export_from_yaml( yaml_path: pathlib.Path, orientation: str, fmt: str, hide_unassigned: bool = False) -> tuple[bytes, str, str]:
56def export_from_yaml(
57    yaml_path: Path,
58    orientation: str,
59    fmt: str,
60    hide_unassigned: bool = False,
61) -> tuple[bytes, str, str]:
62    """Render an export from a YAML file on disk.
63
64    Returns ``(bytes, content_type, suggested_filename)``.  Raises
65    :class:`ExportRequestError` for bad orientation/format, or whatever
66    ``load_config`` / ``render_to_bytes`` raise for missing or
67    inconsistent configs.
68    """
69    orientation = orientation.lower()
70    fmt = fmt.lower().lstrip(".")
71    if orientation not in ALLOWED_ORIENTATIONS:
72        raise ExportRequestError(
73            f"orientation must be one of {sorted(ALLOWED_ORIENTATIONS)}; "
74            f"got {orientation!r}")
75    if fmt not in ALLOWED_FORMATS:
76        raise ExportRequestError(
77            f"format must be one of {sorted(ALLOWED_FORMATS)}; got {fmt!r}")
78
79    config = load_config(yaml_path)
80    data = render_to_bytes(
81        config, orientation, fmt,
82        label_positions=None,
83        hide_unassigned=hide_unassigned,
84        icon_loader=_icon_loader(),
85    )
86    stem = yaml_path.stem or "controllers"
87    filename = f"{stem}_{orientation}.{fmt}"
88    return data, MIME[fmt], filename

Render an export from a YAML file on disk.

Returns (bytes, content_type, suggested_filename). Raises ExportRequestError for bad orientation/format, or whatever load_config / render_to_bytes raise for missing or inconsistent configs.