Decouple all cross-app service calls to HTTP endpoints
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 3m0s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 3m0s
Replace every direct cross-app services.* call with HTTP-based communication: call_action() for writes, fetch_data() for reads. Each app now registers only its own domain service. Infrastructure: - shared/infrastructure/actions.py — POST client for /internal/actions/ - shared/infrastructure/data_client.py — GET client for /internal/data/ - shared/contracts/dtos.py — dto_to_dict/dto_from_dict serialization Action endpoints (writes): - events: 8 handlers (ticket adjust, claim/confirm, toggle, adopt) - market: 2 handlers (create/soft-delete marketplace) - cart: 1 handler (adopt cart for user) Data endpoints (reads): - blog: 4 (post-by-slug/id, posts-by-ids, search-posts) - events: 10 (pending entries/tickets, entries/tickets for page/order, entry-ids, associated-entries, calendars, visible-entries-for-period) - market: 1 (marketplaces-for-container) - cart: 1 (cart-summary) Service registration cleanup: - blog→blog+federation, events→calendar+federation, market→market+federation, cart→cart only, federation→federation only, account→nothing - Stubs reduced to minimal StubFederationService Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -10,7 +10,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_fragments
|
||||
from bp import register_market_bp, register_all_markets, register_page_markets, register_fragments, register_actions, register_data
|
||||
|
||||
|
||||
async def market_context() -> dict:
|
||||
@@ -23,9 +23,10 @@ async def market_context() -> dict:
|
||||
"""
|
||||
from shared.infrastructure.context import base_context
|
||||
from shared.services.navigation import get_navigation_tree
|
||||
from shared.services.registry import services
|
||||
from shared.infrastructure.cart_identity import current_cart_identity
|
||||
from shared.infrastructure.fragments import fetch_fragments
|
||||
from shared.infrastructure.data_client import fetch_data
|
||||
from shared.contracts.dtos import CartSummaryDTO, dto_from_dict
|
||||
from shared.models.market import CartItem
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
@@ -36,10 +37,14 @@ async def market_context() -> dict:
|
||||
|
||||
ident = current_cart_identity()
|
||||
|
||||
# cart_count/cart_total via service (consistent with blog/events apps)
|
||||
summary = await services.cart.cart_summary(
|
||||
g.s, user_id=ident["user_id"], session_id=ident["session_id"],
|
||||
)
|
||||
# cart_count/cart_total via internal data endpoint
|
||||
summary_params = {}
|
||||
if ident["user_id"] is not None:
|
||||
summary_params["user_id"] = ident["user_id"]
|
||||
if ident["session_id"] is not None:
|
||||
summary_params["session_id"] = ident["session_id"]
|
||||
raw = await fetch_data("cart", "cart-summary", params=summary_params, required=False)
|
||||
summary = dto_from_dict(CartSummaryDTO, raw) if raw else CartSummaryDTO()
|
||||
ctx["cart_count"] = summary.count + summary.calendar_count
|
||||
ctx["cart_total"] = float(summary.total + summary.calendar_total)
|
||||
|
||||
@@ -80,7 +85,6 @@ async def market_context() -> dict:
|
||||
|
||||
def create_app() -> "Quart":
|
||||
from models.market_place import MarketPlace
|
||||
from shared.services.registry import services
|
||||
from services import register_domain_services
|
||||
|
||||
app = create_base_app(
|
||||
@@ -118,6 +122,8 @@ def create_app() -> "Quart":
|
||||
)
|
||||
|
||||
app.register_blueprint(register_fragments())
|
||||
app.register_blueprint(register_actions())
|
||||
app.register_blueprint(register_data())
|
||||
|
||||
# --- Auto-inject slugs into url_for() calls ---
|
||||
@app.url_value_preprocessor
|
||||
@@ -147,26 +153,27 @@ def create_app() -> "Quart":
|
||||
# --- Load post and market data ---
|
||||
@app.before_request
|
||||
async def hydrate_market():
|
||||
from shared.infrastructure.data_client import fetch_data
|
||||
post_slug = getattr(g, "post_slug", None)
|
||||
market_slug = getattr(g, "market_slug", None)
|
||||
if not post_slug:
|
||||
return
|
||||
|
||||
# Load post by slug via blog service
|
||||
post = await services.blog.get_post_by_slug(g.s, post_slug)
|
||||
# Load post by slug via blog data endpoint
|
||||
post = await fetch_data("blog", "post-by-slug", params={"slug": post_slug})
|
||||
if not post:
|
||||
abort(404)
|
||||
|
||||
g.post_data = {
|
||||
"post": {
|
||||
"id": post.id,
|
||||
"title": post.title,
|
||||
"slug": post.slug,
|
||||
"feature_image": post.feature_image,
|
||||
"html": post.html,
|
||||
"status": post.status,
|
||||
"visibility": post.visibility,
|
||||
"is_page": post.is_page,
|
||||
"id": post["id"],
|
||||
"title": post["title"],
|
||||
"slug": post["slug"],
|
||||
"feature_image": post.get("feature_image"),
|
||||
"html": post.get("html"),
|
||||
"status": post["status"],
|
||||
"visibility": post["visibility"],
|
||||
"is_page": post.get("is_page", False),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -3,3 +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 .fragments import register_fragments
|
||||
from .actions import register_actions
|
||||
from .data import register_data
|
||||
|
||||
1
market/bp/actions/__init__.py
Normal file
1
market/bp/actions/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .routes import register as register_actions
|
||||
66
market/bp/actions/routes.py
Normal file
66
market/bp/actions/routes.py
Normal file
@@ -0,0 +1,66 @@
|
||||
"""Market app action endpoints.
|
||||
|
||||
Exposes write operations at ``/internal/actions/<action_name>`` for
|
||||
cross-app callers (blog, events) via the internal action client.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from quart import Blueprint, g, jsonify, request
|
||||
|
||||
from shared.infrastructure.actions import ACTION_HEADER
|
||||
from shared.services.registry import services
|
||||
|
||||
|
||||
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
|
||||
|
||||
_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
|
||||
result = await handler()
|
||||
return jsonify(result)
|
||||
|
||||
# --- 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
|
||||
|
||||
return bp
|
||||
@@ -12,6 +12,8 @@ from __future__ import annotations
|
||||
from quart import Blueprint, g, request, render_template, make_response
|
||||
|
||||
from shared.browser.app.utils.htmx import is_htmx_request
|
||||
from shared.infrastructure.data_client import fetch_data
|
||||
from shared.contracts.dtos import PostDTO, dto_from_dict
|
||||
from shared.services.registry import services
|
||||
|
||||
|
||||
@@ -32,8 +34,11 @@ def register() -> Blueprint:
|
||||
if m.container_type == "page"
|
||||
})
|
||||
if post_ids:
|
||||
posts = await services.blog.get_posts_by_ids(g.s, post_ids)
|
||||
for p in posts:
|
||||
raw_posts = await fetch_data("blog", "posts-by-ids",
|
||||
params={"ids": ",".join(str(i) for i in post_ids)},
|
||||
required=False) or []
|
||||
for raw_p in raw_posts:
|
||||
p = dto_from_dict(PostDTO, raw_p)
|
||||
page_info[p.id] = {"title": p.title, "slug": p.slug}
|
||||
|
||||
return markets, has_more, page_info
|
||||
|
||||
1
market/bp/data/__init__.py
Normal file
1
market/bp/data/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .routes import register as register_data
|
||||
44
market/bp/data/routes.py
Normal file
44
market/bp/data/routes.py
Normal file
@@ -0,0 +1,44 @@
|
||||
"""Market app data endpoints.
|
||||
|
||||
Exposes read-only JSON queries at ``/internal/data/<query_name>`` for
|
||||
cross-app callers via the internal data client.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from quart import Blueprint, g, jsonify, request
|
||||
|
||||
from shared.infrastructure.data_client import DATA_HEADER
|
||||
from shared.contracts.dtos import dto_to_dict
|
||||
from shared.services.registry import services
|
||||
|
||||
|
||||
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
|
||||
|
||||
_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
|
||||
|
||||
return bp
|
||||
@@ -7,23 +7,14 @@ def register_domain_services() -> None:
|
||||
|
||||
Market owns: Product, CartItem, MarketPlace, NavTop, NavSub,
|
||||
Listing, ProductImage.
|
||||
Standard deployment registers all 4 services as real DB impls
|
||||
(shared DB). For composable deployments, swap non-owned services
|
||||
with stubs from shared.services.stubs.
|
||||
Cross-app calls go over HTTP via call_action() / fetch_data().
|
||||
"""
|
||||
from shared.services.registry import services
|
||||
from shared.services.blog_impl import SqlBlogService
|
||||
from shared.services.calendar_impl import SqlCalendarService
|
||||
from shared.services.market_impl import SqlMarketService
|
||||
from shared.services.cart_impl import SqlCartService
|
||||
|
||||
services.market = SqlMarketService()
|
||||
if not services.has("blog"):
|
||||
services.blog = SqlBlogService()
|
||||
if not services.has("calendar"):
|
||||
services.calendar = SqlCalendarService()
|
||||
if not services.has("cart"):
|
||||
services.cart = SqlCartService()
|
||||
|
||||
# Federation needed for AP shared infrastructure (activitypub blueprint)
|
||||
if not services.has("federation"):
|
||||
from shared.services.federation_impl import SqlFederationService
|
||||
services.federation = SqlFederationService()
|
||||
|
||||
Reference in New Issue
Block a user