diff --git a/account/app.py b/account/app.py index 639262e..5b92a64 100644 --- a/account/app.py +++ b/account/app.py @@ -8,7 +8,7 @@ from jinja2 import FileSystemLoader, ChoiceLoader from shared.infrastructure.factory import create_base_app -from bp import register_account_bp, register_auth_bp, register_fragments +from bp import register_account_bp, register_auth_bp async def account_context() -> dict: @@ -86,7 +86,8 @@ def create_app() -> "Quart": from shared.sx.pages import auto_mount_pages auto_mount_pages(app, "account") - app.register_blueprint(register_fragments()) + from shared.sx.handlers import auto_mount_fragment_handlers + auto_mount_fragment_handlers(app, "account") from bp.actions.routes import register as register_actions app.register_blueprint(register_actions()) diff --git a/account/bp/__init__.py b/account/bp/__init__.py index fe22f4e..2113b69 100644 --- a/account/bp/__init__.py +++ b/account/bp/__init__.py @@ -1,3 +1,2 @@ from .account.routes import register as register_account_bp from .auth.routes import register as register_auth_bp -from .fragments import register_fragments diff --git a/account/bp/fragments/__init__.py b/account/bp/fragments/__init__.py deleted file mode 100644 index a4af44b..0000000 --- a/account/bp/fragments/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .routes import register as register_fragments diff --git a/account/bp/fragments/routes.py b/account/bp/fragments/routes.py deleted file mode 100644 index 28a4362..0000000 --- a/account/bp/fragments/routes.py +++ /dev/null @@ -1,36 +0,0 @@ -"""Account app fragment endpoints. - -Exposes sx fragments at ``/internal/fragments/`` for consumption -by other coop apps via the fragment client. - -All handlers are defined declaratively in .sx files under -``account/sx/handlers/`` and dispatched via the sx handler registry. -""" - -from __future__ import annotations - -from quart import Blueprint, Response, request - -from shared.infrastructure.fragments import FRAGMENT_HEADER -from shared.sx.handlers import get_handler, execute_handler - - -def register(): - bp = Blueprint("fragments", __name__, url_prefix="/internal/fragments") - - @bp.before_request - async def _require_fragment_header(): - if not request.headers.get(FRAGMENT_HEADER): - return Response("", status=403) - - @bp.get("/") - async def get_fragment(fragment_type: str): - handler_def = get_handler("account", fragment_type) - if handler_def is not None: - result = await execute_handler( - handler_def, "account", args=dict(request.args), - ) - return Response(result, status=200, content_type="text/sx") - return Response("", status=200, content_type="text/sx") - - return bp diff --git a/blog/app.py b/blog/app.py index 9fe803b..664f9c0 100644 --- a/blog/app.py +++ b/blog/app.py @@ -16,7 +16,6 @@ from bp import ( register_admin, register_menu_items, register_snippets, - register_fragments, register_data, register_actions, ) @@ -108,7 +107,9 @@ def create_app() -> "Quart": app.register_blueprint(register_admin("/settings")) app.register_blueprint(register_menu_items()) app.register_blueprint(register_snippets()) - app.register_blueprint(register_fragments()) + from shared.sx.handlers import auto_mount_fragment_handlers + auto_mount_fragment_handlers(app, "blog") + app.register_blueprint(register_data()) app.register_blueprint(register_actions()) diff --git a/blog/bp/__init__.py b/blog/bp/__init__.py index eb7938b..9e21b5a 100644 --- a/blog/bp/__init__.py +++ b/blog/bp/__init__.py @@ -2,6 +2,5 @@ from .blog.routes import register as register_blog_bp from .admin.routes import register as register_admin from .menu_items.routes import register as register_menu_items from .snippets.routes import register as register_snippets -from .fragments import register_fragments from .data import register_data from .actions.routes import register as register_actions diff --git a/blog/bp/fragments/__init__.py b/blog/bp/fragments/__init__.py deleted file mode 100644 index a4af44b..0000000 --- a/blog/bp/fragments/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .routes import register as register_fragments diff --git a/blog/bp/fragments/routes.py b/blog/bp/fragments/routes.py deleted file mode 100644 index 9b0818f..0000000 --- a/blog/bp/fragments/routes.py +++ /dev/null @@ -1,36 +0,0 @@ -"""Blog app fragment endpoints. - -Exposes sx fragments at ``/internal/fragments/`` for consumption -by other coop apps via the fragment client. - -All handlers are defined declaratively in .sx files under -``blog/sx/handlers/`` and dispatched via the sx handler registry. -""" - -from __future__ import annotations - -from quart import Blueprint, Response, request - -from shared.infrastructure.fragments import FRAGMENT_HEADER -from shared.sx.handlers import get_handler, execute_handler - - -def register(): - bp = Blueprint("fragments", __name__, url_prefix="/internal/fragments") - - @bp.before_request - async def _require_fragment_header(): - if not request.headers.get(FRAGMENT_HEADER): - return Response("", status=403) - - @bp.get("/") - async def get_fragment(fragment_type: str): - handler_def = get_handler("blog", fragment_type) - if handler_def is not None: - result = await execute_handler( - handler_def, "blog", args=dict(request.args), - ) - return Response(result, status=200, content_type="text/sx") - return Response("", status=200, content_type="text/sx") - - return bp diff --git a/cart/app.py b/cart/app.py index 5ae5140..b957ec4 100644 --- a/cart/app.py +++ b/cart/app.py @@ -17,7 +17,6 @@ from bp import ( register_page_cart, register_cart_global, register_page_admin, - register_fragments, register_actions, register_data, register_inbox, @@ -141,7 +140,9 @@ def create_app() -> "Quart": app.jinja_env.globals["cart_quantity_url"] = lambda product_id: f"/quantity/{product_id}/" app.jinja_env.globals["cart_delete_url"] = lambda product_id: f"/delete/{product_id}/" - app.register_blueprint(register_fragments()) + from shared.sx.handlers import auto_mount_fragment_handlers + auto_mount_fragment_handlers(app, "cart") + app.register_blueprint(register_actions()) app.register_blueprint(register_data()) app.register_blueprint(register_inbox()) diff --git a/cart/bp/__init__.py b/cart/bp/__init__.py index a48e533..c14d2fa 100644 --- a/cart/bp/__init__.py +++ b/cart/bp/__init__.py @@ -2,7 +2,6 @@ from .cart.overview_routes import register as register_cart_overview from .cart.page_routes import register as register_page_cart from .cart.global_routes import register as register_cart_global from .page_admin.routes import register as register_page_admin -from .fragments import register_fragments from .actions import register_actions from .data import register_data from .inbox import register_inbox diff --git a/cart/bp/fragments/__init__.py b/cart/bp/fragments/__init__.py deleted file mode 100644 index a4af44b..0000000 --- a/cart/bp/fragments/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .routes import register as register_fragments diff --git a/cart/bp/fragments/routes.py b/cart/bp/fragments/routes.py deleted file mode 100644 index 6c84d22..0000000 --- a/cart/bp/fragments/routes.py +++ /dev/null @@ -1,36 +0,0 @@ -"""Cart app fragment endpoints. - -Exposes sx fragments at ``/internal/fragments/`` for consumption -by other coop apps via the fragment client. - -All handlers are defined declaratively in .sx files under -``cart/sx/handlers/`` and dispatched via the sx handler registry. -""" - -from __future__ import annotations - -from quart import Blueprint, Response, request - -from shared.infrastructure.fragments import FRAGMENT_HEADER -from shared.sx.handlers import get_handler, execute_handler - - -def register(): - bp = Blueprint("fragments", __name__, url_prefix="/internal/fragments") - - @bp.before_request - async def _require_fragment_header(): - if not request.headers.get(FRAGMENT_HEADER): - return Response("", status=403) - - @bp.get("/") - async def get_fragment(fragment_type: str): - handler_def = get_handler("cart", fragment_type) - if handler_def is not None: - result = await execute_handler( - handler_def, "cart", args=dict(request.args), - ) - return Response(result, status=200, content_type="text/sx") - return Response("", status=200, content_type="text/sx") - - return bp diff --git a/events/app.py b/events/app.py index 58a5ce0..59dae16 100644 --- a/events/app.py +++ b/events/app.py @@ -9,7 +9,7 @@ from jinja2 import FileSystemLoader, ChoiceLoader from shared.infrastructure.factory import create_base_app -from bp import register_all_events, register_calendar, register_calendars, register_markets, register_page, register_fragments, register_actions, register_data +from bp import register_all_events, register_calendar, register_calendars, register_markets, register_page, register_actions, register_data async def events_context() -> dict: @@ -112,7 +112,12 @@ def create_app() -> "Quart": url_prefix="//markets", ) - app.register_blueprint(register_fragments()) + from shared.sx.handlers import auto_mount_fragment_handlers + from bp.fragments.python_handlers import container_cards_handler, account_page_handler + add_fragment_handler = auto_mount_fragment_handlers(app, "events") + add_fragment_handler("container-cards", container_cards_handler, content_type="text/html") + add_fragment_handler("account-page", account_page_handler) + app.register_blueprint(register_actions()) app.register_blueprint(register_data()) diff --git a/events/bp/__init__.py b/events/bp/__init__.py index cbef22c..ab33a78 100644 --- a/events/bp/__init__.py +++ b/events/bp/__init__.py @@ -3,6 +3,5 @@ from .calendar.routes import register as register_calendar from .calendars.routes import register as register_calendars from .markets.routes import register as register_markets from .page.routes import register as register_page -from .fragments import register_fragments from .actions import register_actions from .data import register_data diff --git a/events/bp/fragments/__init__.py b/events/bp/fragments/__init__.py index a4af44b..e69de29 100644 --- a/events/bp/fragments/__init__.py +++ b/events/bp/fragments/__init__.py @@ -1 +0,0 @@ -from .routes import register as register_fragments diff --git a/events/bp/fragments/python_handlers.py b/events/bp/fragments/python_handlers.py new file mode 100644 index 0000000..0ea1dc4 --- /dev/null +++ b/events/bp/fragments/python_handlers.py @@ -0,0 +1,58 @@ +"""Python fragment handlers for events. + +These handlers call domain services and use sx_call() for rendering, +so they can't be expressed as declarative .sx handlers. +""" + +from __future__ import annotations + +from quart import g, request + +from shared.services.registry import services + + +async def container_cards_handler(): + """Container-cards fragment: entries for blog listing cards. + + Returns text/html with comment markers + so the blog consumer can split per-post fragments. + """ + from sx.sx_components import render_fragment_container_cards + + post_ids_raw = request.args.get("post_ids", "") + post_slugs_raw = request.args.get("post_slugs", "") + post_ids = [int(x) for x in post_ids_raw.split(",") if x.strip()] + post_slugs = [x.strip() for x in post_slugs_raw.split(",") if x.strip()] + if not post_ids: + return "" + + slug_map = {} + for i, pid in enumerate(post_ids): + slug_map[pid] = post_slugs[i] if i < len(post_slugs) else "" + + batch = await services.calendar.confirmed_entries_for_posts(g.s, post_ids) + return render_fragment_container_cards(batch, post_ids, slug_map) + + +async def account_page_handler(): + """Account-page fragment: tickets or bookings panel. + + Returns text/sx — the account app embeds this as sx source. + """ + from sx.sx_components import ( + render_fragment_account_tickets, + render_fragment_account_bookings, + ) + + slug = request.args.get("slug", "") + user_id = request.args.get("user_id", type=int) + if not user_id: + return "" + + if slug == "tickets": + tickets = await services.calendar.user_tickets(g.s, user_id=user_id) + return render_fragment_account_tickets(tickets) + elif slug == "bookings": + bookings = await services.calendar.user_bookings(g.s, user_id=user_id) + return render_fragment_account_bookings(bookings) + return "" diff --git a/events/bp/fragments/routes.py b/events/bp/fragments/routes.py deleted file mode 100644 index 01c9f4b..0000000 --- a/events/bp/fragments/routes.py +++ /dev/null @@ -1,104 +0,0 @@ -"""Events app fragment endpoints. - -Exposes sx fragments at ``/internal/fragments/`` for consumption -by other coop apps via the fragment client. - -All handlers are defined declaratively in .sx files under -``events/sx/handlers/`` and dispatched via the sx handler registry. - -container-cards and account-page remain as Python handlers because they -call domain service methods and return batched/conditional content, but -they use sx_call() for rendering (no Jinja templates). -""" - -from __future__ import annotations - -from quart import Blueprint, Response, g, request - -from shared.infrastructure.fragments import FRAGMENT_HEADER -from shared.services.registry import services -from shared.sx.handlers import get_handler, execute_handler - - -def register(): - bp = Blueprint("fragments", __name__, url_prefix="/internal/fragments") - - _handlers: dict[str, object] = {} - - # Fragment types that return HTML (comment-delimited batch) - _html_types = {"container-cards"} - - @bp.before_request - async def _require_fragment_header(): - if not request.headers.get(FRAGMENT_HEADER): - return Response("", status=403) - - @bp.get("/") - async def get_fragment(fragment_type: str): - # 1. Check Python handlers first - handler = _handlers.get(fragment_type) - if handler is not None: - result = await handler() - ct = "text/html" if fragment_type in _html_types else "text/sx" - return Response(result, status=200, content_type=ct) - - # 2. Check sx handler registry - handler_def = get_handler("events", fragment_type) - if handler_def is not None: - result = await execute_handler( - handler_def, "events", args=dict(request.args), - ) - return Response(result, status=200, content_type="text/sx") - - return Response("", status=200, content_type="text/sx") - - # --- container-cards fragment: entries for blog listing cards ----------- - # Returns text/html with comment markers - # so the blog consumer can split per-post fragments. - - async def _container_cards_handler(): - from sx.sx_components import render_fragment_container_cards - - post_ids_raw = request.args.get("post_ids", "") - post_slugs_raw = request.args.get("post_slugs", "") - post_ids = [int(x) for x in post_ids_raw.split(",") if x.strip()] - post_slugs = [x.strip() for x in post_slugs_raw.split(",") if x.strip()] - if not post_ids: - return "" - - slug_map = {} - for i, pid in enumerate(post_ids): - slug_map[pid] = post_slugs[i] if i < len(post_slugs) else "" - - batch = await services.calendar.confirmed_entries_for_posts(g.s, post_ids) - return render_fragment_container_cards(batch, post_ids, slug_map) - - _handlers["container-cards"] = _container_cards_handler - - # --- account-page fragment: tickets or bookings panel ------------------ - # Returns text/sx — the account app embeds this as sx source. - - async def _account_page_handler(): - from sx.sx_components import ( - render_fragment_account_tickets, - render_fragment_account_bookings, - ) - - slug = request.args.get("slug", "") - user_id = request.args.get("user_id", type=int) - if not user_id: - return "" - - if slug == "tickets": - tickets = await services.calendar.user_tickets(g.s, user_id=user_id) - return render_fragment_account_tickets(tickets) - elif slug == "bookings": - bookings = await services.calendar.user_bookings(g.s, user_id=user_id) - return render_fragment_account_bookings(bookings) - return "" - - _handlers["account-page"] = _account_page_handler - - bp._fragment_handlers = _handlers - - return bp diff --git a/federation/app.py b/federation/app.py index 60cd5a2..5b86d3d 100644 --- a/federation/app.py +++ b/federation/app.py @@ -12,7 +12,6 @@ from shared.services.registry import services from bp import ( register_identity_bp, register_social_bp, - register_fragments, ) @@ -99,7 +98,8 @@ def create_app() -> "Quart": from shared.sx.pages import auto_mount_pages auto_mount_pages(app, "federation") - app.register_blueprint(register_fragments()) + from shared.sx.handlers import auto_mount_fragment_handlers + auto_mount_fragment_handlers(app, "federation") # --- home page --- @app.get("/") diff --git a/federation/bp/__init__.py b/federation/bp/__init__.py index 1be06bb..0965667 100644 --- a/federation/bp/__init__.py +++ b/federation/bp/__init__.py @@ -1,3 +1,2 @@ from .identity.routes import register as register_identity_bp from .social.routes import register as register_social_bp -from .fragments import register_fragments diff --git a/federation/bp/fragments/__init__.py b/federation/bp/fragments/__init__.py deleted file mode 100644 index a4af44b..0000000 --- a/federation/bp/fragments/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .routes import register as register_fragments diff --git a/federation/bp/fragments/routes.py b/federation/bp/fragments/routes.py deleted file mode 100644 index 95da7d9..0000000 --- a/federation/bp/fragments/routes.py +++ /dev/null @@ -1,36 +0,0 @@ -"""Federation app fragment endpoints. - -Exposes sx fragments at ``/internal/fragments/`` for consumption -by other coop apps via the fragment client. - -All handlers are defined declaratively in .sx files under -``federation/sx/handlers/`` and dispatched via the sx handler registry. -""" - -from __future__ import annotations - -from quart import Blueprint, Response, request - -from shared.infrastructure.fragments import FRAGMENT_HEADER -from shared.sx.handlers import get_handler, execute_handler - - -def register(): - bp = Blueprint("fragments", __name__, url_prefix="/internal/fragments") - - @bp.before_request - async def _require_fragment_header(): - if not request.headers.get(FRAGMENT_HEADER): - return Response("", status=403) - - @bp.get("/") - async def get_fragment(fragment_type: str): - handler_def = get_handler("federation", fragment_type) - if handler_def is not None: - result = await execute_handler( - handler_def, "federation", args=dict(request.args), - ) - return Response(result, status=200, content_type="text/sx") - return Response("", status=200, content_type="text/sx") - - return bp diff --git a/market/app.py b/market/app.py index a65ccac..05c6e6b 100644 --- a/market/app.py +++ b/market/app.py @@ -11,7 +11,7 @@ from sqlalchemy import select from shared.infrastructure.factory import create_base_app from shared.config import config -from bp import register_market_bp, register_all_markets, register_page_markets, register_page_admin, register_fragments, register_actions, register_data +from bp import register_market_bp, register_all_markets, register_page_markets, register_page_admin, register_actions, register_data async def market_context() -> dict: @@ -126,7 +126,9 @@ def create_app() -> "Quart": url_prefix="//", ) - app.register_blueprint(register_fragments()) + from shared.sx.handlers import auto_mount_fragment_handlers + auto_mount_fragment_handlers(app, "market") + app.register_blueprint(register_actions()) app.register_blueprint(register_data()) diff --git a/market/bp/__init__.py b/market/bp/__init__.py index 1153c6e..03b3b2e 100644 --- a/market/bp/__init__.py +++ b/market/bp/__init__.py @@ -3,6 +3,5 @@ from .product.routes import register as register_product from .all_markets.routes import register as register_all_markets from .page_markets.routes import register as register_page_markets from .page_admin.routes import register as register_page_admin -from .fragments import register_fragments from .actions import register_actions from .data import register_data diff --git a/market/bp/fragments/__init__.py b/market/bp/fragments/__init__.py deleted file mode 100644 index a4af44b..0000000 --- a/market/bp/fragments/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .routes import register as register_fragments diff --git a/market/bp/fragments/routes.py b/market/bp/fragments/routes.py deleted file mode 100644 index 323c1e5..0000000 --- a/market/bp/fragments/routes.py +++ /dev/null @@ -1,36 +0,0 @@ -"""Market app fragment endpoints. - -Exposes sx fragments at ``/internal/fragments/`` for consumption -by other coop apps via the fragment client. - -All handlers are defined declaratively in .sx files under -``market/sx/handlers/`` and dispatched via the sx handler registry. -""" - -from __future__ import annotations - -from quart import Blueprint, Response, request - -from shared.infrastructure.fragments import FRAGMENT_HEADER -from shared.sx.handlers import get_handler, execute_handler - - -def register(): - bp = Blueprint("fragments", __name__, url_prefix="/internal/fragments") - - @bp.before_request - async def _require_fragment_header(): - if not request.headers.get(FRAGMENT_HEADER): - return Response("", status=403) - - @bp.get("/") - async def get_fragment(fragment_type: str): - handler_def = get_handler("market", fragment_type) - if handler_def is not None: - result = await execute_handler( - handler_def, "market", args=dict(request.args), - ) - return Response(result, status=200, content_type="text/sx") - return Response("", status=200, content_type="text/sx") - - return bp diff --git a/orders/app.py b/orders/app.py index a782056..762b2e1 100644 --- a/orders/app.py +++ b/orders/app.py @@ -14,7 +14,6 @@ from bp import ( register_orders, register_order, register_checkout, - register_fragments, register_actions, register_data, ) @@ -77,7 +76,9 @@ def create_app() -> "Quart": from sxc.pages import setup_orders_pages setup_orders_pages() - app.register_blueprint(register_fragments()) + from shared.sx.handlers import auto_mount_fragment_handlers + auto_mount_fragment_handlers(app, "orders") + app.register_blueprint(register_actions()) app.register_blueprint(register_data()) diff --git a/orders/bp/__init__.py b/orders/bp/__init__.py index 590fe33..61e6380 100644 --- a/orders/bp/__init__.py +++ b/orders/bp/__init__.py @@ -3,4 +3,3 @@ from .orders.routes import register as register_orders from .checkout.routes import register as register_checkout from .data.routes import register as register_data from .actions.routes import register as register_actions -from .fragments.routes import register as register_fragments diff --git a/orders/bp/fragments/__init__.py b/orders/bp/fragments/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/orders/bp/fragments/routes.py b/orders/bp/fragments/routes.py deleted file mode 100644 index e18f5c4..0000000 --- a/orders/bp/fragments/routes.py +++ /dev/null @@ -1,36 +0,0 @@ -"""Orders app fragment endpoints. - -Exposes sx fragments at ``/internal/fragments/`` for consumption -by other coop apps via the fragment client. - -All handlers are defined declaratively in .sx files under -``orders/sx/handlers/`` and dispatched via the sx handler registry. -""" - -from __future__ import annotations - -from quart import Blueprint, Response, request - -from shared.infrastructure.fragments import FRAGMENT_HEADER -from shared.sx.handlers import get_handler, execute_handler - - -def register(): - bp = Blueprint("fragments", __name__, url_prefix="/internal/fragments") - - @bp.before_request - async def _require_fragment_header(): - if not request.headers.get(FRAGMENT_HEADER): - return Response("", status=403) - - @bp.get("/") - async def get_fragment(fragment_type: str): - handler_def = get_handler("orders", fragment_type) - if handler_def is not None: - result = await execute_handler( - handler_def, "orders", args=dict(request.args), - ) - return Response(result, status=200, content_type="text/sx") - return Response("", status=200, content_type="text/sx") - - return bp diff --git a/relations/app.py b/relations/app.py index 3515745..a93419c 100644 --- a/relations/app.py +++ b/relations/app.py @@ -4,7 +4,7 @@ import sx.sx_components as sx_components # noqa: F401 # ensure Hypercorn --rel from shared.infrastructure.factory import create_base_app -from bp import register_actions, register_data, register_fragments +from bp import register_actions, register_data from services import register_domain_services @@ -16,7 +16,9 @@ def create_app() -> "Quart": app.register_blueprint(register_actions()) app.register_blueprint(register_data()) - app.register_blueprint(register_fragments()) + + from shared.sx.handlers import auto_mount_fragment_handlers + auto_mount_fragment_handlers(app, "relations") return app diff --git a/relations/bp/__init__.py b/relations/bp/__init__.py index ee25922..7122ccd 100644 --- a/relations/bp/__init__.py +++ b/relations/bp/__init__.py @@ -1,3 +1,2 @@ from .data.routes import register as register_data from .actions.routes import register as register_actions -from .fragments.routes import register as register_fragments diff --git a/relations/bp/fragments/__init__.py b/relations/bp/fragments/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/relations/bp/fragments/routes.py b/relations/bp/fragments/routes.py deleted file mode 100644 index cc90927..0000000 --- a/relations/bp/fragments/routes.py +++ /dev/null @@ -1,36 +0,0 @@ -"""Relations app fragment endpoints. - -Exposes sx fragments at ``/internal/fragments/`` for consumption -by other coop apps via the fragment client. - -All handlers are defined declaratively in .sx files under -``relations/sx/handlers/`` and dispatched via the sx handler registry. -""" - -from __future__ import annotations - -from quart import Blueprint, Response, request - -from shared.infrastructure.fragments import FRAGMENT_HEADER -from shared.sx.handlers import get_handler, execute_handler - - -def register(): - bp = Blueprint("fragments", __name__, url_prefix="/internal/fragments") - - @bp.before_request - async def _require_fragment_header(): - if not request.headers.get(FRAGMENT_HEADER): - return Response("", status=403) - - @bp.get("/") - async def get_fragment(fragment_type: str): - handler_def = get_handler("relations", fragment_type) - if handler_def is not None: - result = await execute_handler( - handler_def, "relations", args=dict(request.args), - ) - return Response(result, status=200, content_type="text/sx") - return Response("", status=200, content_type="text/sx") - - return bp diff --git a/shared/sx/handlers.py b/shared/sx/handlers.py index 6e653f2..4cf00cc 100644 --- a/shared/sx/handlers.py +++ b/shared/sx/handlers.py @@ -204,3 +204,42 @@ def create_handler_blueprint(service_name: str) -> Any: bp._python_handlers = _python_handlers # type: ignore[attr-defined] return bp + + +# --------------------------------------------------------------------------- +# Direct app mount — replaces per-service fragment blueprint boilerplate +# --------------------------------------------------------------------------- + +def auto_mount_fragment_handlers(app: Any, service_name: str) -> Callable: + """Mount ``/internal/fragments/`` directly on the app. + + Returns an ``add_handler(name, fn, content_type)`` function for + registering Python handler overrides (checked before SX handlers). + """ + from quart import Response, request + from shared.infrastructure.fragments import FRAGMENT_HEADER + + python_handlers: dict[str, Callable[[], Awaitable[str]]] = {} + html_types: set[str] = set() + + @app.get("/internal/fragments/") + async def _fragment_dispatch(fragment_type: str): + if not request.headers.get(FRAGMENT_HEADER): + return Response("", status=403) + py = python_handlers.get(fragment_type) + if py is not None: + result = await py() + ct = "text/html" if fragment_type in html_types else "text/sx" + return Response(result, status=200, content_type=ct) + hdef = get_handler(service_name, fragment_type) + if hdef is not None: + result = await execute_handler(hdef, service_name, args=dict(request.args)) + return Response(result, status=200, content_type="text/sx") + return Response("", status=200, content_type="text/sx") + + def add_handler(name: str, fn: Callable[[], Awaitable[str]], content_type: str = "text/sx") -> None: + python_handlers[name] = fn + if content_type == "text/html": + html_types.add(name) + + return add_handler