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.