Merge worktree-endpoints: migrate all ref endpoints to SX with typed handlers
This commit is contained in:
@@ -239,10 +239,19 @@ def register_route_handlers(app_or_bp: Any, service_name: str) -> int:
|
||||
|
||||
async def _route_view(_h=_hdef, **path_kwargs):
|
||||
from shared.sx.helpers import sx_response
|
||||
from shared.sx.primitives_io import reset_response_meta, get_response_meta
|
||||
reset_response_meta()
|
||||
args = dict(request.args)
|
||||
args.update(path_kwargs)
|
||||
result = await execute_handler(_h, service_name, args=args)
|
||||
return sx_response(result)
|
||||
resp = sx_response(result)
|
||||
meta = get_response_meta()
|
||||
if meta:
|
||||
if meta.get("status"):
|
||||
resp.status_code = meta["status"]
|
||||
for k, v in meta.get("headers", {}).items():
|
||||
resp.headers[k] = v
|
||||
return resp
|
||||
|
||||
endpoint = f"sx_route_{name}"
|
||||
view_fn = _route_view
|
||||
|
||||
@@ -46,6 +46,13 @@ _handler_service: contextvars.ContextVar[Any] = contextvars.ContextVar(
|
||||
"_handler_service", default=None
|
||||
)
|
||||
|
||||
_response_meta: contextvars.ContextVar[dict | None] = contextvars.ContextVar(
|
||||
"_response_meta", default=None
|
||||
)
|
||||
|
||||
# Ephemeral per-process state — resets on restart. For demos/testing only.
|
||||
_ephemeral_state: dict[str, Any] = {}
|
||||
|
||||
|
||||
def set_handler_service(service_obj: Any) -> None:
|
||||
"""Bind the local domain service for ``(service ...)`` primitive calls."""
|
||||
@@ -57,6 +64,16 @@ def get_handler_service() -> Any:
|
||||
return _handler_service.get(None)
|
||||
|
||||
|
||||
def reset_response_meta() -> None:
|
||||
"""Reset response meta for a new request."""
|
||||
_response_meta.set(None)
|
||||
|
||||
|
||||
def get_response_meta() -> dict | None:
|
||||
"""Get response meta (headers/status) set by handler IO primitives."""
|
||||
return _response_meta.get(None)
|
||||
|
||||
|
||||
class RequestContext:
|
||||
"""Per-request context provided to I/O primitives."""
|
||||
__slots__ = ("user", "is_htmx", "extras")
|
||||
@@ -372,6 +389,105 @@ async def _io_request_content_type(
|
||||
return request.content_type or NIL
|
||||
|
||||
|
||||
@register_io_handler("request-args-all")
|
||||
async def _io_request_args_all(
|
||||
args: list[Any], kwargs: dict[str, Any], ctx: RequestContext
|
||||
) -> dict:
|
||||
"""``(request-args-all)`` → all query params as dict."""
|
||||
from quart import request
|
||||
return dict(request.args)
|
||||
|
||||
|
||||
@register_io_handler("request-form-all")
|
||||
async def _io_request_form_all(
|
||||
args: list[Any], kwargs: dict[str, Any], ctx: RequestContext
|
||||
) -> dict:
|
||||
"""``(request-form-all)`` → all form fields as dict."""
|
||||
from quart import request
|
||||
form = await request.form
|
||||
return dict(form)
|
||||
|
||||
|
||||
@register_io_handler("request-headers-all")
|
||||
async def _io_request_headers_all(
|
||||
args: list[Any], kwargs: dict[str, Any], ctx: RequestContext
|
||||
) -> dict:
|
||||
"""``(request-headers-all)`` → all headers as dict (lowercase keys)."""
|
||||
from quart import request
|
||||
return {k.lower(): v for k, v in request.headers}
|
||||
|
||||
|
||||
@register_io_handler("request-file-name")
|
||||
async def _io_request_file_name(
|
||||
args: list[Any], kwargs: dict[str, Any], ctx: RequestContext
|
||||
) -> Any:
|
||||
"""``(request-file-name "field")`` → filename or nil."""
|
||||
if not args:
|
||||
raise ValueError("request-file-name requires a field name")
|
||||
from quart import request
|
||||
from .types import NIL
|
||||
files = await request.files
|
||||
f = files.get(str(args[0]))
|
||||
return f.filename if f else NIL
|
||||
|
||||
|
||||
@register_io_handler("set-response-header")
|
||||
async def _io_set_response_header(
|
||||
args: list[Any], kwargs: dict[str, Any], ctx: RequestContext
|
||||
) -> Any:
|
||||
"""``(set-response-header "Name" "value")`` → set on response after handler."""
|
||||
if len(args) < 2:
|
||||
raise ValueError("set-response-header requires name and value")
|
||||
from .types import NIL
|
||||
meta = _response_meta.get(None)
|
||||
if meta is None:
|
||||
meta = {"headers": {}, "status": None}
|
||||
_response_meta.set(meta)
|
||||
meta["headers"][str(args[0])] = str(args[1])
|
||||
return NIL
|
||||
|
||||
|
||||
@register_io_handler("set-response-status")
|
||||
async def _io_set_response_status(
|
||||
args: list[Any], kwargs: dict[str, Any], ctx: RequestContext
|
||||
) -> Any:
|
||||
"""``(set-response-status 503)`` → set status code on response."""
|
||||
if not args:
|
||||
raise ValueError("set-response-status requires a status code")
|
||||
from .types import NIL
|
||||
meta = _response_meta.get(None)
|
||||
if meta is None:
|
||||
meta = {"headers": {}, "status": None}
|
||||
_response_meta.set(meta)
|
||||
meta["status"] = int(args[0])
|
||||
return NIL
|
||||
|
||||
|
||||
@register_io_handler("state-get")
|
||||
async def _io_state_get(
|
||||
args: list[Any], kwargs: dict[str, Any], ctx: RequestContext
|
||||
) -> Any:
|
||||
"""``(state-get "key" default?)`` → read from ephemeral state."""
|
||||
if not args:
|
||||
raise ValueError("state-get requires a key")
|
||||
from .types import NIL
|
||||
key = str(args[0])
|
||||
default = args[1] if len(args) > 1 else NIL
|
||||
return _ephemeral_state.get(key, default)
|
||||
|
||||
|
||||
@register_io_handler("state-set!")
|
||||
async def _io_state_set(
|
||||
args: list[Any], kwargs: dict[str, Any], ctx: RequestContext
|
||||
) -> Any:
|
||||
"""``(state-set! "key" value)`` → write to ephemeral state."""
|
||||
if len(args) < 2:
|
||||
raise ValueError("state-set! requires key and value")
|
||||
from .types import NIL
|
||||
_ephemeral_state[str(args[0])] = args[1]
|
||||
return NIL
|
||||
|
||||
|
||||
@register_io_handler("csrf-token")
|
||||
async def _io_csrf_token(
|
||||
args: list[Any], kwargs: dict[str, Any], ctx: RequestContext
|
||||
|
||||
@@ -188,6 +188,66 @@
|
||||
:doc "Content-Type of the current request."
|
||||
:context :request)
|
||||
|
||||
(define-io-primitive "request-args-all"
|
||||
:params ()
|
||||
:returns "dict"
|
||||
:async true
|
||||
:doc "All query string parameters as a dict."
|
||||
:context :request)
|
||||
|
||||
(define-io-primitive "request-form-all"
|
||||
:params ()
|
||||
:returns "dict"
|
||||
:async true
|
||||
:doc "All form fields as a dict."
|
||||
:context :request)
|
||||
|
||||
(define-io-primitive "request-headers-all"
|
||||
:params ()
|
||||
:returns "dict"
|
||||
:async true
|
||||
:doc "All request headers as a dict (lowercase keys)."
|
||||
:context :request)
|
||||
|
||||
(define-io-primitive "request-file-name"
|
||||
:params (field-name)
|
||||
:returns "string?"
|
||||
:async true
|
||||
:doc "Filename of an uploaded file by field name, or nil."
|
||||
:context :request)
|
||||
|
||||
;; Response manipulation
|
||||
|
||||
(define-io-primitive "set-response-header"
|
||||
:params (name value)
|
||||
:returns "nil"
|
||||
:async true
|
||||
:doc "Set a response header. Applied after handler returns."
|
||||
:context :request)
|
||||
|
||||
(define-io-primitive "set-response-status"
|
||||
:params (status)
|
||||
:returns "nil"
|
||||
:async true
|
||||
:doc "Set the HTTP response status code. Applied after handler returns."
|
||||
:context :request)
|
||||
|
||||
;; Ephemeral state — per-process, resets on restart
|
||||
|
||||
(define-io-primitive "state-get"
|
||||
:params (key &rest default)
|
||||
:returns "any"
|
||||
:async true
|
||||
:doc "Read from ephemeral per-process state dict."
|
||||
:context :request)
|
||||
|
||||
(define-io-primitive "state-set!"
|
||||
:params (key value)
|
||||
:returns "nil"
|
||||
:async true
|
||||
:doc "Write to ephemeral per-process state dict."
|
||||
:context :request)
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Tier 3: Signal primitives — reactive state for islands
|
||||
|
||||
@@ -38,12 +38,13 @@
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; defhandler — (defhandler name [:path "..." :method :get :csrf false] (&key param...) body)
|
||||
;; defhandler — (defhandler name [:path "..." :method :get :csrf false :returns "element"] (&key param...) body)
|
||||
;;
|
||||
;; Keyword options between name and params list:
|
||||
;; :path — public route path (string). Without :path, handler is internal-only.
|
||||
;; :method — HTTP method (keyword: :get :post :put :patch :delete). Default :get.
|
||||
;; :csrf — CSRF protection (boolean). Default true; set false for POST/PUT etc.
|
||||
;; :path — public route path (string). Without :path, handler is internal-only.
|
||||
;; :method — HTTP method (keyword: :get :post :put :patch :delete). Default :get.
|
||||
;; :csrf — CSRF protection (boolean). Default true; set false for POST/PUT etc.
|
||||
;; :returns — return type annotation (types.sx vocabulary). Default "element".
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define parse-handler-args
|
||||
|
||||
@@ -226,10 +226,11 @@ def make_handler_def(name, params, body, env, opts=None):
|
||||
path = opts.get('path') if opts else None
|
||||
method = str(opts.get('method', 'get')) if opts else 'get'
|
||||
csrf = opts.get('csrf', True) if opts else True
|
||||
returns = str(opts.get('returns', 'element')) if opts else 'element'
|
||||
if isinstance(csrf, str):
|
||||
csrf = csrf.lower() not in ('false', 'nil', 'no')
|
||||
return HandlerDef(name=name, params=list(params), body=body, closure=dict(env),
|
||||
path=path, method=method.lower(), csrf=csrf)
|
||||
path=path, method=method.lower(), csrf=csrf, returns=returns)
|
||||
|
||||
|
||||
def make_query_def(name, params, doc, body, env):
|
||||
|
||||
@@ -185,10 +185,11 @@ def make_handler_def(name, params, body, env, opts=None):
|
||||
path = opts.get('path') if opts else None
|
||||
method = str(opts.get('method', 'get')) if opts else 'get'
|
||||
csrf = opts.get('csrf', True) if opts else True
|
||||
returns = str(opts.get('returns', 'element')) if opts else 'element'
|
||||
if isinstance(csrf, str):
|
||||
csrf = csrf.lower() not in ('false', 'nil', 'no')
|
||||
return HandlerDef(name=name, params=list(params), body=body, closure=dict(env),
|
||||
path=path, method=method.lower(), csrf=csrf)
|
||||
path=path, method=method.lower(), csrf=csrf, returns=returns)
|
||||
|
||||
|
||||
def make_query_def(name, params, doc, body, env):
|
||||
|
||||
@@ -235,6 +235,7 @@ class HandlerDef:
|
||||
path: str | None = None # public route path (None = internal fragment only)
|
||||
method: str = "get" # HTTP method (get, post, put, patch, delete)
|
||||
csrf: bool = True # CSRF protection enabled
|
||||
returns: str = "element" # return type (types.sx vocabulary)
|
||||
|
||||
@property
|
||||
def is_route(self) -> bool:
|
||||
|
||||
Reference in New Issue
Block a user