Migrate 6 reactive demo handlers from Python f-strings to SX defhandlers
Moved flash-sale, settle-data, search-products/events/posts, and catalog endpoints from bp/pages/routes.py into sx/sx/handlers/reactive-api.sx. routes.py now contains only the SSE endpoint (async generators need Python). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -154,7 +154,9 @@ async def execute_handler(
|
|||||||
if use_ocaml:
|
if use_ocaml:
|
||||||
from .ocaml_bridge import get_bridge
|
from .ocaml_bridge import get_bridge
|
||||||
|
|
||||||
# Serialize handler body with bound params as a let expression
|
# Serialize handler body with bound params as a let expression.
|
||||||
|
# Define constants and defcomps from the handler file are available
|
||||||
|
# in the kernel's global env (loaded by _ensure_components).
|
||||||
param_bindings = []
|
param_bindings = []
|
||||||
for param in handler_def.params:
|
for param in handler_def.params:
|
||||||
val = args.get(param, args.get(param.replace("-", "_"), NIL))
|
val = args.get(param, args.get(param.replace("-", "_"), NIL))
|
||||||
|
|||||||
@@ -1,26 +1,22 @@
|
|||||||
"""SX docs page routes.
|
"""SX docs page routes.
|
||||||
|
|
||||||
Page GET routes are defined declaratively in sxc/pages/docs.sx via defpage.
|
Page GET routes are defined declaratively in sxc/pages/docs.sx via defpage.
|
||||||
Example API endpoints are now defined in sx/handlers/examples.sx via defhandler.
|
API endpoints are defined in sx/handlers/*.sx via defhandler.
|
||||||
This file contains only SSE and marsh demo endpoints that need Python.
|
This file contains only SSE endpoints that need Python (async generators).
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import random
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from quart import Blueprint, Response, request
|
from quart import Blueprint, Response
|
||||||
|
|
||||||
|
|
||||||
def register(url_prefix: str = "/") -> Blueprint:
|
def register(url_prefix: str = "/") -> Blueprint:
|
||||||
bp = Blueprint("pages", __name__, url_prefix=url_prefix)
|
bp = Blueprint("pages", __name__, url_prefix=url_prefix)
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Reference API endpoints — remaining Python-only
|
# SSE — async generator, fundamentally not expressible in SX
|
||||||
#
|
|
||||||
# Most reference endpoints migrated to sx/sx/handlers/ref-api.sx.
|
|
||||||
# SSE stays in Python — fundamentally different paradigm (async generator).
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
@bp.get("/sx/(geography.(hypermedia.(reference.(api.sse-time))))")
|
@bp.get("/sx/(geography.(hypermedia.(reference.(api.sse-time))))")
|
||||||
@@ -34,132 +30,4 @@ def register(url_prefix: str = "/") -> Blueprint:
|
|||||||
return Response(generate(), content_type="text/event-stream",
|
return Response(generate(), content_type="text/event-stream",
|
||||||
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"})
|
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"})
|
||||||
|
|
||||||
# --- Marsh demos ---
|
|
||||||
|
|
||||||
_marsh_sale_idx = {"n": 0}
|
|
||||||
|
|
||||||
@bp.get("/sx/(geography.(reactive.(api.flash-sale)))")
|
|
||||||
async def api_marsh_flash_sale():
|
|
||||||
from shared.sx.helpers import sx_response
|
|
||||||
prices = [14.99, 9.99, 24.99, 12.49, 7.99, 29.99, 4.99, 16.50]
|
|
||||||
_marsh_sale_idx["n"] = (_marsh_sale_idx["n"] + 1) % len(prices)
|
|
||||||
new_price = prices[_marsh_sale_idx["n"]]
|
|
||||||
now = datetime.now().strftime("%H:%M:%S")
|
|
||||||
sx_src = (
|
|
||||||
f'(<>'
|
|
||||||
f' (div :class "space-y-2"'
|
|
||||||
f' (p :class "text-sm text-emerald-600 font-medium"'
|
|
||||||
f' "\u26A1 Flash sale! Price: ${new_price:.2f}")'
|
|
||||||
f' (p :class "text-xs text-stone-400" "at {now}"))'
|
|
||||||
f' (script :type "text/sx" :data-init ""'
|
|
||||||
f' "(reset! (use-store \\"demo-price\\") {new_price})"))'
|
|
||||||
)
|
|
||||||
return sx_response(sx_src)
|
|
||||||
|
|
||||||
# --- Demo 3: sx-on-settle endpoint ---
|
|
||||||
|
|
||||||
_settle_counter = {"n": 0}
|
|
||||||
|
|
||||||
@bp.get("/sx/(geography.(reactive.(api.settle-data)))")
|
|
||||||
async def api_settle_data():
|
|
||||||
from shared.sx.helpers import sx_response
|
|
||||||
_settle_counter["n"] += 1
|
|
||||||
items = ["Widget", "Gadget", "Sprocket", "Gizmo", "Doohickey"]
|
|
||||||
item = items[_settle_counter["n"] % len(items)]
|
|
||||||
now = datetime.now().strftime("%H:%M:%S")
|
|
||||||
sx_src = (
|
|
||||||
f'(div :class "space-y-1"'
|
|
||||||
f' (p :class "text-sm font-medium text-stone-700" "Fetched: {item}")'
|
|
||||||
f' (p :class "text-xs text-stone-400" "at {now}"))'
|
|
||||||
)
|
|
||||||
return sx_response(sx_src)
|
|
||||||
|
|
||||||
# --- Demo 4: signal-bound URL endpoints ---
|
|
||||||
|
|
||||||
@bp.get("/sx/(geography.(reactive.(api.search-products)))")
|
|
||||||
async def api_search_products():
|
|
||||||
from shared.sx.helpers import sx_response
|
|
||||||
q = request.args.get("q", "")
|
|
||||||
items = ["Artisan Widget", "Premium Gadget", "Handcrafted Sprocket",
|
|
||||||
"Bespoke Gizmo", "Organic Doohickey"]
|
|
||||||
matches = [i for i in items if q.lower() in i.lower()] if q else items
|
|
||||||
rows = " ".join(
|
|
||||||
f'(li :class "text-sm text-stone-600" "{m}")'
|
|
||||||
for m in matches[:3]
|
|
||||||
)
|
|
||||||
sx_src = (
|
|
||||||
f'(div :class "space-y-1"'
|
|
||||||
f' (p :class "text-xs font-semibold text-violet-600 uppercase" "Products")'
|
|
||||||
f' (ul :class "list-disc pl-4" {rows})'
|
|
||||||
f' (p :class "text-xs text-stone-400" "{len(matches)} result(s)"))'
|
|
||||||
)
|
|
||||||
return sx_response(sx_src)
|
|
||||||
|
|
||||||
@bp.get("/sx/(geography.(reactive.(api.search-events)))")
|
|
||||||
async def api_search_events():
|
|
||||||
from shared.sx.helpers import sx_response
|
|
||||||
q = request.args.get("q", "")
|
|
||||||
items = ["Summer Workshop", "Craft Fair", "Open Studio",
|
|
||||||
"Artist Talk", "Gallery Opening"]
|
|
||||||
matches = [i for i in items if q.lower() in i.lower()] if q else items
|
|
||||||
rows = " ".join(
|
|
||||||
f'(li :class "text-sm text-stone-600" "{m}")'
|
|
||||||
for m in matches[:3]
|
|
||||||
)
|
|
||||||
sx_src = (
|
|
||||||
f'(div :class "space-y-1"'
|
|
||||||
f' (p :class "text-xs font-semibold text-emerald-600 uppercase" "Events")'
|
|
||||||
f' (ul :class "list-disc pl-4" {rows})'
|
|
||||||
f' (p :class "text-xs text-stone-400" "{len(matches)} result(s)"))'
|
|
||||||
)
|
|
||||||
return sx_response(sx_src)
|
|
||||||
|
|
||||||
@bp.get("/sx/(geography.(reactive.(api.search-posts)))")
|
|
||||||
async def api_search_posts():
|
|
||||||
from shared.sx.helpers import sx_response
|
|
||||||
q = request.args.get("q", "")
|
|
||||||
items = ["On Craft and Code", "The SX Manifesto", "Islands and Lakes",
|
|
||||||
"Reactive Marshes", "Self-Hosting Spec"]
|
|
||||||
matches = [i for i in items if q.lower() in i.lower()] if q else items
|
|
||||||
rows = " ".join(
|
|
||||||
f'(li :class "text-sm text-stone-600" "{m}")'
|
|
||||||
for m in matches[:3]
|
|
||||||
)
|
|
||||||
sx_src = (
|
|
||||||
f'(div :class "space-y-1"'
|
|
||||||
f' (p :class "text-xs font-semibold text-amber-600 uppercase" "Posts")'
|
|
||||||
f' (ul :class "list-disc pl-4" {rows})'
|
|
||||||
f' (p :class "text-xs text-stone-400" "{len(matches)} result(s)"))'
|
|
||||||
)
|
|
||||||
return sx_response(sx_src)
|
|
||||||
|
|
||||||
# --- Demo 5: marsh transform endpoint ---
|
|
||||||
|
|
||||||
@bp.get("/sx/(geography.(reactive.(api.catalog)))")
|
|
||||||
async def api_catalog():
|
|
||||||
from shared.sx.helpers import sx_response
|
|
||||||
items = [
|
|
||||||
("Artisan Widget", "19.99", "Hand-crafted with care"),
|
|
||||||
("Premium Gadget", "34.50", "Top-of-the-line quality"),
|
|
||||||
("Vintage Sprocket", "12.99", "Classic design"),
|
|
||||||
("Custom Gizmo", "27.00", "Made to order"),
|
|
||||||
]
|
|
||||||
random.shuffle(items)
|
|
||||||
now = datetime.now().strftime("%H:%M:%S")
|
|
||||||
# Build an SX list literal for the data-init script.
|
|
||||||
# Inner quotes must be escaped since the whole expression lives
|
|
||||||
# inside an SX string literal (the script tag's text content).
|
|
||||||
items_sx = "(list " + " ".join(
|
|
||||||
f'(dict \\"name\\" \\"{n}\\" \\"price\\" \\"{p}\\" \\"desc\\" \\"{d}\\")'
|
|
||||||
for n, p, d in items
|
|
||||||
) + ")"
|
|
||||||
sx_src = (
|
|
||||||
f'(<>'
|
|
||||||
f' (p :class "text-sm text-emerald-600 font-medium"'
|
|
||||||
f' "Catalog loaded: {len(items)} items (shuffled at {now})")'
|
|
||||||
f' (script :type "text/sx" :data-init ""'
|
|
||||||
f' "(reset! (use-store \\"catalog-items\\") {items_sx})"))'
|
|
||||||
)
|
|
||||||
return sx_response(sx_src)
|
|
||||||
|
|
||||||
return bp
|
return bp
|
||||||
|
|||||||
166
sx/sx/handlers/reactive-api.sx
Normal file
166
sx/sx/handlers/reactive-api.sx
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
;; ==========================================================================
|
||||||
|
;; Reactive API endpoints — live demos for marsh/reactive-islands pages
|
||||||
|
;;
|
||||||
|
;; Migrated from bp/pages/routes.py (Python f-string handlers).
|
||||||
|
;; ==========================================================================
|
||||||
|
|
||||||
|
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
;; Data constants
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
(define flash-sale-prices
|
||||||
|
(list 14.99 9.99 24.99 12.49 7.99 29.99 4.99 16.50))
|
||||||
|
|
||||||
|
(define settle-items
|
||||||
|
(list "Widget" "Gadget" "Sprocket" "Gizmo" "Doohickey"))
|
||||||
|
|
||||||
|
(define search-product-items
|
||||||
|
(list "Artisan Widget" "Premium Gadget" "Handcrafted Sprocket"
|
||||||
|
"Bespoke Gizmo" "Organic Doohickey"))
|
||||||
|
|
||||||
|
(define search-event-items
|
||||||
|
(list "Summer Workshop" "Craft Fair" "Open Studio"
|
||||||
|
"Artist Talk" "Gallery Opening"))
|
||||||
|
|
||||||
|
(define search-post-items
|
||||||
|
(list "On Craft and Code" "The SX Manifesto" "Islands and Lakes"
|
||||||
|
"Reactive Marshes" "Self-Hosting Spec"))
|
||||||
|
|
||||||
|
(define catalog-items
|
||||||
|
(list
|
||||||
|
(dict "name" "Artisan Widget" "price" "19.99" "desc" "Hand-crafted with care")
|
||||||
|
(dict "name" "Premium Gadget" "price" "34.50" "desc" "Top-of-the-line quality")
|
||||||
|
(dict "name" "Vintage Sprocket" "price" "12.99" "desc" "Classic design")
|
||||||
|
(dict "name" "Custom Gizmo" "price" "27.00" "desc" "Made to order")))
|
||||||
|
|
||||||
|
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
;; Helper: search results panel
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
(defcomp ~reactive-api/search-results (&key label color items query)
|
||||||
|
(let ((matches (if (= query "")
|
||||||
|
items
|
||||||
|
(filter (fn (item) (string-contains? (lower item) (lower query)))
|
||||||
|
items))))
|
||||||
|
(let ((shown (slice matches 0 3)))
|
||||||
|
(div :class "space-y-1"
|
||||||
|
(p :class (str "text-xs font-semibold uppercase " color) label)
|
||||||
|
(ul :class "list-disc pl-4"
|
||||||
|
(map (fn (m) (li :class "text-sm text-stone-600" m)) shown))
|
||||||
|
(p :class "text-xs text-stone-400"
|
||||||
|
(str (length matches) " result(s)"))))))
|
||||||
|
|
||||||
|
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
;; Flash Sale
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
(defhandler reactive-flash-sale
|
||||||
|
:path "/sx/(geography.(reactive.(api.flash-sale)))"
|
||||||
|
:method :get
|
||||||
|
:returns "element"
|
||||||
|
(&key)
|
||||||
|
(let ((idx (random-int 0 (- (length flash-sale-prices) 1)))
|
||||||
|
(now (helper "now" "%H:%M:%S")))
|
||||||
|
(let ((price (nth flash-sale-prices idx)))
|
||||||
|
(<>
|
||||||
|
(div :class "space-y-2"
|
||||||
|
(p :class "text-sm text-emerald-600 font-medium"
|
||||||
|
(str "⚡ Flash sale! Price: $" price))
|
||||||
|
(p :class "text-xs text-stone-400" (str "at " now)))
|
||||||
|
(script :type "text/sx" :data-init ""
|
||||||
|
(str "(reset! (use-store \"demo-price\") " price ")"))))))
|
||||||
|
|
||||||
|
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
;; Settle Data
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
(defhandler reactive-settle-data
|
||||||
|
:path "/sx/(geography.(reactive.(api.settle-data)))"
|
||||||
|
:method :get
|
||||||
|
:returns "element"
|
||||||
|
(&key)
|
||||||
|
(let ((idx (random-int 0 (- (length settle-items) 1)))
|
||||||
|
(now (helper "now" "%H:%M:%S")))
|
||||||
|
(let ((item (nth settle-items idx)))
|
||||||
|
(div :class "space-y-1"
|
||||||
|
(p :class "text-sm font-medium text-stone-700" (str "Fetched: " item))
|
||||||
|
(p :class "text-xs text-stone-400" (str "at " now))))))
|
||||||
|
|
||||||
|
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
;; Search Products
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
(defhandler reactive-search-products
|
||||||
|
:path "/sx/(geography.(reactive.(api.search-products)))"
|
||||||
|
:method :get
|
||||||
|
:returns "element"
|
||||||
|
(&key)
|
||||||
|
(let ((q (helper "request-arg" "q" "")))
|
||||||
|
(~reactive-api/search-results
|
||||||
|
:label "Products" :color "text-violet-600"
|
||||||
|
:items search-product-items :query q)))
|
||||||
|
|
||||||
|
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
;; Search Events
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
(defhandler reactive-search-events
|
||||||
|
:path "/sx/(geography.(reactive.(api.search-events)))"
|
||||||
|
:method :get
|
||||||
|
:returns "element"
|
||||||
|
(&key)
|
||||||
|
(let ((q (helper "request-arg" "q" "")))
|
||||||
|
(~reactive-api/search-results
|
||||||
|
:label "Events" :color "text-emerald-600"
|
||||||
|
:items search-event-items :query q)))
|
||||||
|
|
||||||
|
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
;; Search Posts
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
(defhandler reactive-search-posts
|
||||||
|
:path "/sx/(geography.(reactive.(api.search-posts)))"
|
||||||
|
:method :get
|
||||||
|
:returns "element"
|
||||||
|
(&key)
|
||||||
|
(let ((q (helper "request-arg" "q" "")))
|
||||||
|
(~reactive-api/search-results
|
||||||
|
:label "Posts" :color "text-amber-600"
|
||||||
|
:items search-post-items :query q)))
|
||||||
|
|
||||||
|
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
;; Catalog
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
(defhandler reactive-catalog
|
||||||
|
:path "/sx/(geography.(reactive.(api.catalog)))"
|
||||||
|
:method :get
|
||||||
|
:returns "element"
|
||||||
|
(&key)
|
||||||
|
(let ((now (helper "now" "%H:%M:%S"))
|
||||||
|
(idx (random-int 0 3)))
|
||||||
|
;; Rotate items by a random offset to simulate shuffle
|
||||||
|
(let ((rotated (append (slice catalog-items idx (length catalog-items))
|
||||||
|
(slice catalog-items 0 idx))))
|
||||||
|
(let ((items-sx
|
||||||
|
(str "(list "
|
||||||
|
(reduce (fn (acc item)
|
||||||
|
(str acc
|
||||||
|
"(dict \"name\" \"" (get item "name")
|
||||||
|
"\" \"price\" \"" (get item "price")
|
||||||
|
"\" \"desc\" \"" (get item "desc") "\") "))
|
||||||
|
"" rotated)
|
||||||
|
")")))
|
||||||
|
(<>
|
||||||
|
(p :class "text-sm text-emerald-600 font-medium"
|
||||||
|
(str "Catalog loaded: " (length rotated) " items (shuffled at " now ")"))
|
||||||
|
(script :type "text/sx" :data-init ""
|
||||||
|
(str "(reset! (use-store \"catalog-items\") " items-sx ")")))))))
|
||||||
Reference in New Issue
Block a user