Replace inter-service _handlers dicts with declarative sx defquery/defaction
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m5s

The inter-service data layer (fetch_data/call_action) was the least
structured part of the codebase — Python _handlers dicts with ad-hoc
param extraction scattered across 16 route files. This replaces them
with declarative .sx query/action definitions that make the entire
inter-service protocol self-describing and greppable.

Infrastructure:
- defquery/defaction special forms in the sx evaluator
- Query/action registry with load, lookup, and schema introspection
- Query executor using async_eval with I/O primitives
- Blueprint factories (create_data_blueprint/create_action_blueprint)
  with sx-first dispatch and Python fallback
- /internal/schema endpoint on every service
- parse-datetime and split-ids primitives for type coercion

Service extractions:
- LikesService (toggle, is_liked, liked_slugs, liked_ids)
- PageConfigService (ensure, get_by_container, get_by_id, get_batch, update)
- RelationsService (wraps module-level functions)
- AccountDataService (user_by_email, newsletters)
- CartItemsService, MarketDataService (raw SQLAlchemy lookups)

50 of 54 handlers converted to sx, 4 Python fallbacks remain
(ghost-sync/push-member, clear-cart-for-order, create-order).
Net: -1,383 lines Python, +251 lines modified.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-04 08:13:50 +00:00
parent e53e8cc1f7
commit 1f36987f77
54 changed files with 1599 additions and 1382 deletions

15
market/actions.sx Normal file
View File

@@ -0,0 +1,15 @@
;; Market service — inter-service action endpoints
(defaction create-marketplace (&key container-type container-id name slug)
"Create a new marketplace within a container."
(let ((mp (service "market" "create-marketplace"
:container-type container-type :container-id container-id
:name name :slug slug)))
mp))
(defaction soft-delete-marketplace (&key container-type container-id slug)
"Soft-delete a marketplace by slug within a container."
(let ((deleted (service "market" "soft-delete-marketplace"
:container-type container-type :container-id container-id
:slug slug)))
{"deleted" deleted}))

View File

@@ -1,74 +1,14 @@
"""Market app action endpoints.
Exposes write operations at ``/internal/actions/<action_name>`` for
cross-app callers (blog, events) via the internal action client.
All actions are defined in ``market/actions.sx``.
"""
from __future__ import annotations
from quart import Blueprint, g, jsonify, request
from quart import Blueprint
from shared.infrastructure.actions import ACTION_HEADER
from shared.services.registry import services
from shared.infrastructure.query_blueprint import create_action_blueprint
def register() -> Blueprint:
bp = Blueprint("actions", __name__, url_prefix="/internal/actions")
@bp.before_request
async def _require_action_header():
if not request.headers.get(ACTION_HEADER):
return jsonify({"error": "forbidden"}), 403
from shared.infrastructure.internal_auth import validate_internal_request
if not validate_internal_request():
return jsonify({"error": "forbidden"}), 403
_handlers: dict[str, object] = {}
@bp.post("/<action_name>")
async def handle_action(action_name: str):
handler = _handlers.get(action_name)
if handler is None:
return jsonify({"error": "unknown action"}), 404
try:
result = await handler()
return jsonify(result)
except Exception as exc:
import logging
logging.getLogger(__name__).exception("Action %s failed", action_name)
return jsonify({"error": str(exc)}), 500
# --- create-marketplace ---
async def _create_marketplace():
data = await request.get_json()
mp = await services.market.create_marketplace(
g.s,
data["container_type"],
data["container_id"],
data["name"],
data["slug"],
)
return {
"id": mp.id,
"container_type": mp.container_type,
"container_id": mp.container_id,
"name": mp.name,
"slug": mp.slug,
"description": mp.description,
}
_handlers["create-marketplace"] = _create_marketplace
# --- soft-delete-marketplace ---
async def _soft_delete_marketplace():
data = await request.get_json()
deleted = await services.market.soft_delete_marketplace(
g.s,
data["container_type"],
data["container_id"],
data["slug"],
)
return {"deleted": deleted}
_handlers["soft-delete-marketplace"] = _soft_delete_marketplace
bp, _handlers = create_action_blueprint("market")
return bp

View File

@@ -1,110 +1,14 @@
"""Market app data endpoints.
Exposes read-only JSON queries at ``/internal/data/<query_name>`` for
cross-app callers via the internal data client.
All queries are defined in ``market/queries.sx``.
"""
from __future__ import annotations
from quart import Blueprint, g, jsonify, request
from quart import Blueprint
from shared.infrastructure.data_client import DATA_HEADER
from shared.contracts.dtos import dto_to_dict
from shared.services.registry import services
from shared.infrastructure.query_blueprint import create_data_blueprint
def register() -> Blueprint:
bp = Blueprint("data", __name__, url_prefix="/internal/data")
@bp.before_request
async def _require_data_header():
if not request.headers.get(DATA_HEADER):
return jsonify({"error": "forbidden"}), 403
from shared.infrastructure.internal_auth import validate_internal_request
if not validate_internal_request():
return jsonify({"error": "forbidden"}), 403
_handlers: dict[str, object] = {}
@bp.get("/<query_name>")
async def handle_query(query_name: str):
handler = _handlers.get(query_name)
if handler is None:
return jsonify({"error": "unknown query"}), 404
result = await handler()
return jsonify(result)
# --- marketplaces-for-container ---
async def _marketplaces_for_container():
container_type = request.args.get("type", "")
container_id = request.args.get("id", type=int)
markets = await services.market.marketplaces_for_container(
g.s, container_type, container_id,
)
return [dto_to_dict(m) for m in markets]
_handlers["marketplaces-for-container"] = _marketplaces_for_container
# --- products-by-ids ---
async def _products_by_ids():
"""Return product details for a list of IDs (comma-separated)."""
from sqlalchemy import select
from shared.models.market import Product
ids_raw = request.args.get("ids", "")
try:
ids = [int(x) for x in ids_raw.split(",") if x.strip()]
except ValueError:
return {"error": "ids must be comma-separated integers"}, 400
if not ids:
return []
rows = (await g.s.execute(
select(Product).where(Product.id.in_(ids))
)).scalars().all()
return [
{
"id": p.id,
"title": p.title,
"slug": p.slug,
"image": p.image,
"regular_price": str(p.regular_price) if p.regular_price is not None else None,
"special_price": str(p.special_price) if p.special_price is not None else None,
}
for p in rows
]
_handlers["products-by-ids"] = _products_by_ids
# --- marketplaces-by-ids ---
async def _marketplaces_by_ids():
"""Return marketplace data for a list of IDs (comma-separated)."""
from sqlalchemy import select
from shared.models.market_place import MarketPlace
ids_raw = request.args.get("ids", "")
try:
ids = [int(x) for x in ids_raw.split(",") if x.strip()]
except ValueError:
return {"error": "ids must be comma-separated integers"}, 400
if not ids:
return []
rows = (await g.s.execute(
select(MarketPlace).where(MarketPlace.id.in_(ids))
)).scalars().all()
return [
{
"id": m.id,
"name": m.name,
"slug": m.slug,
"container_type": m.container_type,
"container_id": m.container_id,
}
for m in rows
]
_handlers["marketplaces-by-ids"] = _marketplaces_by_ids
bp, _handlers = create_data_blueprint("market")
return bp

14
market/queries.sx Normal file
View File

@@ -0,0 +1,14 @@
;; Market service — inter-service data queries
(defquery marketplaces-for-container (&key type id)
"Marketplaces attached to a container (page, etc)."
(service "market" "marketplaces-for-container"
:container-type type :container-id id))
(defquery products-by-ids (&key ids)
"Return product details for comma-separated IDs."
(service "market-data" "products-by-ids" :ids (split-ids ids)))
(defquery marketplaces-by-ids (&key ids)
"Return marketplace data for comma-separated IDs."
(service "market-data" "marketplaces-by-ids" :ids (split-ids ids)))

View File

@@ -14,6 +14,9 @@ def register_domain_services() -> None:
services.market = SqlMarketService()
from shared.services.market_data_impl import SqlMarketDataService
services.register("market_data", SqlMarketDataService())
# Federation needed for AP shared infrastructure (activitypub blueprint)
if not services.has("federation"):
from shared.services.federation_impl import SqlFederationService