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))
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
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_envattribute) or a Jinja2Environmentdirectly. Sharing the app's environment keeps component templates consistent with the rest of the app.
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.
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/
{"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.
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.BlueprintwithPOST <url_prefix>/<name>wired tocomponent_view().
Usage::
from flask import Flask
from component_framework.adapters.flask import create_component_blueprint
app = Flask(__name__)
app.register_blueprint(create_component_blueprint())
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.