component_framework.adapters.flask

Flask adapter for component endpoints.

Provides a Jinja2-backed FlaskRenderer and a synchronous HTTP endpoint (exposed as a Flask blueprint) that dispatches events to the component registry, mirroring the protocol used by the FastAPI, Litestar, and Django adapters.

Install with the flask extra::

pip install "component-framework[flask]"
  1"""Flask adapter for component endpoints.
  2
  3Provides a Jinja2-backed :class:`FlaskRenderer` and a synchronous HTTP endpoint
  4(exposed as a Flask blueprint) that dispatches events to the component registry,
  5mirroring the protocol used by the FastAPI, Litestar, and Django adapters.
  6
  7Install with the ``flask`` extra::
  8
  9    pip install "component-framework[flask]"
 10"""
 11
 12import json
 13import logging
 14
 15try:
 16    from flask import Blueprint, jsonify, request
 17except ImportError as e:
 18    from . import _require_extra
 19
 20    raise _require_extra("flask", "flask") from e
 21
 22from ..core import Renderer, StateSerializer, registry
 23
 24logger = logging.getLogger(__name__)
 25
 26
 27class FlaskRenderer(Renderer):
 28    """Renderer backed by a Jinja2 environment.
 29
 30    Pass the Flask application (or its ``jinja_env``) so component templates
 31    inherit the app's configured filters, globals, and extensions rather than a
 32    fresh, empty environment::
 33
 34        from flask import Flask
 35        from component_framework.adapters.flask import FlaskRenderer
 36        from component_framework.core.component import Component
 37
 38        app = Flask(__name__)
 39        Component.renderer = FlaskRenderer(app)  # shares app.jinja_env
 40    """
 41
 42    def __init__(self, app_or_env):
 43        """
 44        Initialize the renderer.
 45
 46        Args:
 47            app_or_env: A Flask application (anything exposing a ``jinja_env``
 48                attribute) or a Jinja2 ``Environment`` directly. Sharing the
 49                app's environment keeps component templates consistent with the
 50                rest of the app.
 51        """
 52        self.env = getattr(app_or_env, "jinja_env", app_or_env)
 53
 54    def render(self, template_name: str, context: dict) -> str:
 55        """Render ``template_name`` with ``context`` via the Jinja2 environment."""
 56        return self.env.get_template(template_name).render(**context)
 57
 58
 59def _parse_json_str(value, default: dict | None = None) -> dict | None:
 60    """Parse a value that may be a JSON string, a dict, or None."""
 61    if value is None:
 62        return default
 63    if isinstance(value, dict):
 64        return value
 65    try:
 66        return json.loads(value)
 67    except (json.JSONDecodeError, ValueError):
 68        return default
 69
 70
 71def _parse_request_data() -> dict:
 72    """Parse the current Flask request body from JSON or form-encoded data.
 73
 74    HTMX sends ``application/x-www-form-urlencoded`` by default; the bundled
 75    ``component-client.js`` sends ``application/json``. Both normalise into a
 76    dict carrying ``event``, ``payload``, ``state``, and ``params`` keys.
 77
 78    Raises:
 79        ValueError: If a JSON content type is declared but the body is invalid.
 80    """
 81    if request.is_json:
 82        data = request.get_json(silent=True)
 83        if data is None:
 84            raise ValueError("Invalid JSON body")
 85        return data
 86    return request.form.to_dict()
 87
 88
 89def _extract_params(data: dict) -> tuple[dict, str | None, dict, dict | None]:
 90    """Extract and normalise event, payload, state, and params from parsed data.
 91
 92    Returns:
 93        A ``(params, event, payload, state)`` tuple ready for component dispatch.
 94
 95    Raises:
 96        ValueError: If the supplied state cannot be deserialized.
 97    """
 98    params = _parse_json_str(data.get("params"), default={}) or {}
 99    event = data.get("event")
100    payload = _parse_json_str(data.get("payload"), default={}) or {}
101    state_raw = data.get("state")
102
103    state = None
104    if state_raw:
105        try:
106            state = (
107                StateSerializer.deserialize(state_raw) if isinstance(state_raw, str) else state_raw
108            )
109        except Exception as e:
110            raise ValueError(f"Invalid state: {e}")
111
112    return params, event, payload, state
113
114
115def component_view(name: str):
116    """
117    Generic component endpoint for Flask.
118
119    POST /components/<name>
120    Body (JSON or form-encoded)::
121
122        {"event": "event_name", "payload": {...}, "state": "serialized_state"}
123
124    Returns JSON::
125
126        {"html": "...", "state": "...", "component_id": "...", "slots": {...}}
127
128    Args:
129        name: Registered component name from the URL.
130
131    Returns:
132        A Flask JSON response with the rendered component, or a JSON error with
133        status 404 (unknown component), 400 (bad request), or 500.
134    """
135    component_cls = registry.get(name)
136    if not component_cls:
137        return jsonify({"error": f"Component '{name}' not found"}), 404
138
139    try:
140        data = _parse_request_data()
141        params, event, payload, state = _extract_params(data)
142    except ValueError as e:
143        return jsonify({"error": str(e)}), 400
144
145    try:
146        component = component_cls(**params)
147        result = component.dispatch(event=event, payload=payload, state=state)
148        result["state"] = StateSerializer.serialize(result["state"])
149        return jsonify(result), 200
150    except Exception:
151        logger.exception(f"Error processing component '{name}'")
152        return jsonify({"error": "Internal server error"}), 500
153
154
155def create_component_blueprint(url_prefix: str = "/components") -> "Blueprint":
156    """
157    Build a Flask blueprint exposing the component endpoint.
158
159    Args:
160        url_prefix: URL prefix the blueprint is mounted under.
161
162    Returns:
163        A :class:`flask.Blueprint` with ``POST <url_prefix>/<name>`` wired to
164        :func:`component_view`.
165
166    Usage::
167
168        from flask import Flask
169        from component_framework.adapters.flask import create_component_blueprint
170
171        app = Flask(__name__)
172        app.register_blueprint(create_component_blueprint())
173    """
174    bp = Blueprint("components", __name__, url_prefix=url_prefix)
175    # strict_slashes=False so both "/components/<name>" and "/components/<name>/"
176    # match — the bundled component-client.js posts to the trailing-slash form.
177    bp.add_url_rule(
178        "/<name>",
179        endpoint="component",
180        view_func=component_view,
181        methods=["POST"],
182        strict_slashes=False,
183    )
184    return bp
185
186
187def register_component_routes(app, url_prefix: str = "/components") -> None:
188    """
189    Register the component blueprint on a Flask application.
190
191    Args:
192        app: The Flask application.
193        url_prefix: URL prefix to mount the component endpoint under.
194    """
195    app.register_blueprint(create_component_blueprint(url_prefix=url_prefix))
logger = <Logger component_framework.adapters.flask (WARNING)>
class FlaskRenderer(component_framework.core.renderer.Renderer):
28class FlaskRenderer(Renderer):
29    """Renderer backed by a Jinja2 environment.
30
31    Pass the Flask application (or its ``jinja_env``) so component templates
32    inherit the app's configured filters, globals, and extensions rather than a
33    fresh, empty environment::
34
35        from flask import Flask
36        from component_framework.adapters.flask import FlaskRenderer
37        from component_framework.core.component import Component
38
39        app = Flask(__name__)
40        Component.renderer = FlaskRenderer(app)  # shares app.jinja_env
41    """
42
43    def __init__(self, app_or_env):
44        """
45        Initialize the renderer.
46
47        Args:
48            app_or_env: A Flask application (anything exposing a ``jinja_env``
49                attribute) or a Jinja2 ``Environment`` directly. Sharing the
50                app's environment keeps component templates consistent with the
51                rest of the app.
52        """
53        self.env = getattr(app_or_env, "jinja_env", app_or_env)
54
55    def render(self, template_name: str, context: dict) -> str:
56        """Render ``template_name`` with ``context`` via the Jinja2 environment."""
57        return self.env.get_template(template_name).render(**context)

Renderer backed by a Jinja2 environment.

Pass the Flask application (or its jinja_env) so component templates inherit the app's configured filters, globals, and extensions rather than a fresh, empty environment::

from flask import Flask
from component_framework.adapters.flask import FlaskRenderer
from component_framework.core.component import Component

app = Flask(__name__)
Component.renderer = FlaskRenderer(app)  # shares app.jinja_env
FlaskRenderer(app_or_env)
43    def __init__(self, app_or_env):
44        """
45        Initialize the renderer.
46
47        Args:
48            app_or_env: A Flask application (anything exposing a ``jinja_env``
49                attribute) or a Jinja2 ``Environment`` directly. Sharing the
50                app's environment keeps component templates consistent with the
51                rest of the app.
52        """
53        self.env = getattr(app_or_env, "jinja_env", app_or_env)

Initialize the renderer.

Arguments:
  • app_or_env: A Flask application (anything exposing a jinja_env attribute) or a Jinja2 Environment directly. Sharing the app's environment keeps component templates consistent with the rest of the app.
env
def render(self, template_name: str, context: dict) -> str:
55    def render(self, template_name: str, context: dict) -> str:
56        """Render ``template_name`` with ``context`` via the Jinja2 environment."""
57        return self.env.get_template(template_name).render(**context)

Render template_name with context via the Jinja2 environment.

def component_view(name: str):
116def component_view(name: str):
117    """
118    Generic component endpoint for Flask.
119
120    POST /components/<name>
121    Body (JSON or form-encoded)::
122
123        {"event": "event_name", "payload": {...}, "state": "serialized_state"}
124
125    Returns JSON::
126
127        {"html": "...", "state": "...", "component_id": "...", "slots": {...}}
128
129    Args:
130        name: Registered component name from the URL.
131
132    Returns:
133        A Flask JSON response with the rendered component, or a JSON error with
134        status 404 (unknown component), 400 (bad request), or 500.
135    """
136    component_cls = registry.get(name)
137    if not component_cls:
138        return jsonify({"error": f"Component '{name}' not found"}), 404
139
140    try:
141        data = _parse_request_data()
142        params, event, payload, state = _extract_params(data)
143    except ValueError as e:
144        return jsonify({"error": str(e)}), 400
145
146    try:
147        component = component_cls(**params)
148        result = component.dispatch(event=event, payload=payload, state=state)
149        result["state"] = StateSerializer.serialize(result["state"])
150        return jsonify(result), 200
151    except Exception:
152        logger.exception(f"Error processing component '{name}'")
153        return jsonify({"error": "Internal server error"}), 500

Generic component endpoint for Flask.

POST /components/ Body (JSON or form-encoded)::

{"event": "event_name", "payload": {...}, "state": "serialized_state"}

Returns JSON::

{"html": "...", "state": "...", "component_id": "...", "slots": {...}}
Arguments:
  • name: Registered component name from the URL.
Returns:

A Flask JSON response with the rendered component, or a JSON error with status 404 (unknown component), 400 (bad request), or 500.

def create_component_blueprint(url_prefix: str = '/components') -> flask.blueprints.Blueprint:
156def create_component_blueprint(url_prefix: str = "/components") -> "Blueprint":
157    """
158    Build a Flask blueprint exposing the component endpoint.
159
160    Args:
161        url_prefix: URL prefix the blueprint is mounted under.
162
163    Returns:
164        A :class:`flask.Blueprint` with ``POST <url_prefix>/<name>`` wired to
165        :func:`component_view`.
166
167    Usage::
168
169        from flask import Flask
170        from component_framework.adapters.flask import create_component_blueprint
171
172        app = Flask(__name__)
173        app.register_blueprint(create_component_blueprint())
174    """
175    bp = Blueprint("components", __name__, url_prefix=url_prefix)
176    # strict_slashes=False so both "/components/<name>" and "/components/<name>/"
177    # match — the bundled component-client.js posts to the trailing-slash form.
178    bp.add_url_rule(
179        "/<name>",
180        endpoint="component",
181        view_func=component_view,
182        methods=["POST"],
183        strict_slashes=False,
184    )
185    return bp

Build a Flask blueprint exposing the component endpoint.

Arguments:
  • url_prefix: URL prefix the blueprint is mounted under.
Returns:

A flask.Blueprint with POST <url_prefix>/<name> wired to component_view().

Usage::

from flask import Flask
from component_framework.adapters.flask import create_component_blueprint

app = Flask(__name__)
app.register_blueprint(create_component_blueprint())
def register_component_routes(app, url_prefix: str = '/components') -> None:
188def register_component_routes(app, url_prefix: str = "/components") -> None:
189    """
190    Register the component blueprint on a Flask application.
191
192    Args:
193        app: The Flask application.
194        url_prefix: URL prefix to mount the component endpoint under.
195    """
196    app.register_blueprint(create_component_blueprint(url_prefix=url_prefix))

Register the component blueprint on a Flask application.

Arguments:
  • app: The Flask application.
  • url_prefix: URL prefix to mount the component endpoint under.