Source code for httk.serve.web.widgets.optimade_fields

"""A server-side-static table of OPTIMADE property definitions.

Unlike ``optimade_table`` this widget performs no browser fetch and ships no
JavaScript: the served schema's property definitions are known at startup, so
the table is rendered once from the ``properties`` mapping a site supplies.
"""

import re
from collections.abc import Mapping
from html import escape
from importlib.resources import files
from urllib.parse import urlsplit

from .core import WidgetAsset, WidgetContext, WidgetRenderResult
from .optimade_table import (
    MAX_OPTIMADE_LABEL_CHARS,
    MAX_OPTIMADE_TEXT_CHARS,
    MAX_OPTIMADE_URL_CHARS,
    OptimadeTableProtocolError,
    _identifier,
    _internal_root,
    _text,
)

[docs] MAX_OPTIMADE_FIELDS = 512
_BLANK_LINE = re.compile(r"\n[^\S\n]*\n") _OPTIMADE_FIELDS_ASSETS: tuple[WidgetAsset, ...] | None = None
[docs] def render( context: WidgetContext, *, properties: object, caption: str = "Field definitions", ) -> WidgetRenderResult: """Render a static table of OPTIMADE property definitions. Each row links a property's prefixed name to the human-readable definition page at its ``$id`` and shows the first paragraph of its ``description``. Ad-hoc synthesized ids (whose path contains ``/ad-hoc/``) and any non HTTP(S) id render the name as plain text rather than a link. :param context: Immutable widget invocation context. :param properties: Mapping of served property name to its OPTIMADE property-definition mapping (each carrying ``$id`` and ``description``). :param caption: Accessible table caption. :return: Static field-definition table and its stylesheet asset. :raises OptimadeTableProtocolError: If ``properties`` is not a non-empty mapping of at most 512 identifier-named definition mappings. """ normalized_caption = _text(caption, field="caption", maximum=MAX_OPTIMADE_LABEL_CHARS) rows = _rows(properties) internal_root = _internal_root(context) body = "".join( f"<tr>{_name_cell(name, link)}<td>{escape(description, quote=False)}</td></tr>" for name, link, description in rows ) html = ( f'<section class="httk-serve-optimade-fields">' f'<link rel="stylesheet" href="{internal_root}/assets/serve-optimade-fields.css">' f"<table><caption>{escape(normalized_caption, quote=False)}</caption>" '<thead><tr><th scope="col">Field</th><th scope="col">Description</th></tr></thead>' f"<tbody>{body}</tbody></table>" "</section>" ) return WidgetRenderResult(html, assets=_optimade_fields_assets())
def _rows(value: object) -> list[tuple[str, str | None, str]]: if not isinstance(value, Mapping): raise OptimadeTableProtocolError("properties must be a mapping of property name to definition mapping") if not value: raise OptimadeTableProtocolError("properties must not be empty") if len(value) > MAX_OPTIMADE_FIELDS: raise OptimadeTableProtocolError(f"properties may contain at most {MAX_OPTIMADE_FIELDS} entries") rows: list[tuple[str, str | None, str]] = [] for name, definition in value.items(): identifier = _identifier(name, field="property name") if not isinstance(definition, Mapping): raise OptimadeTableProtocolError("each property definition must be a mapping") rows.append((identifier, _link(definition.get("$id")), _description(definition.get("description")))) rows.sort(key=lambda row: row[0]) return rows def _name_cell(name: str, link: str | None) -> str: code = f"<code>{escape(name, quote=False)}</code>" if link is None: return f"<td>{code}</td>" return f'<td><a href="{escape(link, quote=True)}" target="_blank" rel="noopener noreferrer">{code}</a></td>' def _description(value: object) -> str: if not isinstance(value, str): return "" first_paragraph = _BLANK_LINE.split(value, maxsplit=1)[0].strip() return first_paragraph[:MAX_OPTIMADE_TEXT_CHARS] def _link(value: object) -> str | None: if not isinstance(value, str) or not value or len(value) > MAX_OPTIMADE_URL_CHARS: return None try: parsed = urlsplit(value) if parsed.username is not None or parsed.password is not None: return None except ValueError: return None if parsed.scheme not in {"http", "https"} or not parsed.netloc: return None if "/ad-hoc/" in parsed.path: return None return value def _optimade_fields_assets() -> tuple[WidgetAsset, ...]: global _OPTIMADE_FIELDS_ASSETS if _OPTIMADE_FIELDS_ASSETS is None: assets = files("httk.serve.web").joinpath("assets") _OPTIMADE_FIELDS_ASSETS = ( WidgetAsset( "serve-optimade-fields.css", assets.joinpath("serve-optimade-fields.css").read_bytes(), "text/css" ), ) return _OPTIMADE_FIELDS_ASSETS