Decouple all cross-app service calls to HTTP endpoints
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:
@@ -6,7 +6,6 @@ from quart import g, request
|
|||||||
from jinja2 import FileSystemLoader, ChoiceLoader
|
from jinja2 import FileSystemLoader, ChoiceLoader
|
||||||
|
|
||||||
from shared.infrastructure.factory import create_base_app
|
from shared.infrastructure.factory import create_base_app
|
||||||
from shared.services.registry import services
|
|
||||||
|
|
||||||
from bp import register_account_bp, register_auth_bp, register_fragments
|
from bp import register_account_bp, register_auth_bp, register_fragments
|
||||||
|
|
||||||
@@ -17,17 +16,23 @@ async def account_context() -> dict:
|
|||||||
from shared.services.navigation import get_navigation_tree
|
from shared.services.navigation import get_navigation_tree
|
||||||
from shared.infrastructure.cart_identity import current_cart_identity
|
from shared.infrastructure.cart_identity import current_cart_identity
|
||||||
from shared.infrastructure.fragments import fetch_fragments
|
from shared.infrastructure.fragments import fetch_fragments
|
||||||
|
from shared.infrastructure.data_client import fetch_data
|
||||||
|
from shared.contracts.dtos import CartSummaryDTO, dto_from_dict
|
||||||
|
|
||||||
ctx = await base_context()
|
ctx = await base_context()
|
||||||
|
|
||||||
# Fallback for _nav.html when nav-tree fragment fetch fails
|
# Fallback for _nav.html when nav-tree fragment fetch fails
|
||||||
ctx["menu_items"] = await get_navigation_tree(g.s)
|
ctx["menu_items"] = await get_navigation_tree(g.s)
|
||||||
|
|
||||||
# Cart data (consistent with all other apps)
|
# Cart data via internal data endpoint
|
||||||
ident = current_cart_identity()
|
ident = current_cart_identity()
|
||||||
summary = await services.cart.cart_summary(
|
summary_params = {}
|
||||||
g.s, user_id=ident["user_id"], session_id=ident["session_id"],
|
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 + summary.ticket_count
|
ctx["cart_count"] = summary.count + summary.calendar_count + summary.ticket_count
|
||||||
ctx["cart_total"] = float(summary.total + summary.calendar_total + summary.ticket_total)
|
ctx["cart_total"] = float(summary.total + summary.calendar_total + summary.ticket_total)
|
||||||
|
|
||||||
|
|||||||
@@ -5,23 +5,7 @@ from __future__ import annotations
|
|||||||
def register_domain_services() -> None:
|
def register_domain_services() -> None:
|
||||||
"""Register services for the account app.
|
"""Register services for the account app.
|
||||||
|
|
||||||
Account needs all domain services since widgets (tickets, bookings)
|
Account is a consumer-only dashboard app. It has no own domain.
|
||||||
pull data from blog, calendar, market, cart, and federation.
|
All cross-app data comes via fragments and HTTP data endpoints.
|
||||||
"""
|
"""
|
||||||
from shared.services.registry import services
|
pass
|
||||||
from shared.services.federation_impl import SqlFederationService
|
|
||||||
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
|
|
||||||
|
|
||||||
if not services.has("federation"):
|
|
||||||
services.federation = SqlFederationService()
|
|
||||||
if not services.has("blog"):
|
|
||||||
services.blog = SqlBlogService()
|
|
||||||
if not services.has("calendar"):
|
|
||||||
services.calendar = SqlCalendarService()
|
|
||||||
if not services.has("market"):
|
|
||||||
services.market = SqlMarketService()
|
|
||||||
if not services.has("cart"):
|
|
||||||
services.cart = SqlCartService()
|
|
||||||
|
|||||||
17
blog/app.py
17
blog/app.py
@@ -16,6 +16,7 @@ from bp import (
|
|||||||
register_menu_items,
|
register_menu_items,
|
||||||
register_snippets,
|
register_snippets,
|
||||||
register_fragments,
|
register_fragments,
|
||||||
|
register_data,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -28,20 +29,25 @@ async def blog_context() -> dict:
|
|||||||
"""
|
"""
|
||||||
from shared.infrastructure.context import base_context
|
from shared.infrastructure.context import base_context
|
||||||
from shared.services.navigation import get_navigation_tree
|
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.cart_identity import current_cart_identity
|
||||||
from shared.infrastructure.fragments import fetch_fragments
|
from shared.infrastructure.fragments import fetch_fragments
|
||||||
|
from shared.infrastructure.data_client import fetch_data
|
||||||
|
from shared.contracts.dtos import CartSummaryDTO, dto_from_dict
|
||||||
|
|
||||||
ctx = await base_context()
|
ctx = await base_context()
|
||||||
|
|
||||||
# Fallback for _nav.html when nav-tree fragment fetch fails
|
# Fallback for _nav.html when nav-tree fragment fetch fails
|
||||||
ctx["menu_items"] = await get_navigation_tree(g.s)
|
ctx["menu_items"] = await get_navigation_tree(g.s)
|
||||||
|
|
||||||
# Cart data via service (replaces cross-app HTTP API)
|
# Cart data via internal data endpoint
|
||||||
ident = current_cart_identity()
|
ident = current_cart_identity()
|
||||||
summary = await services.cart.cart_summary(
|
summary_params = {}
|
||||||
g.s, user_id=ident["user_id"], session_id=ident["session_id"],
|
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 + summary.ticket_count
|
ctx["cart_count"] = summary.count + summary.calendar_count + summary.ticket_count
|
||||||
ctx["cart_total"] = float(summary.total + summary.calendar_total + summary.ticket_total)
|
ctx["cart_total"] = float(summary.total + summary.calendar_total + summary.ticket_total)
|
||||||
|
|
||||||
@@ -98,6 +104,7 @@ def create_app() -> "Quart":
|
|||||||
app.register_blueprint(register_menu_items())
|
app.register_blueprint(register_menu_items())
|
||||||
app.register_blueprint(register_snippets())
|
app.register_blueprint(register_snippets())
|
||||||
app.register_blueprint(register_fragments())
|
app.register_blueprint(register_fragments())
|
||||||
|
app.register_blueprint(register_data())
|
||||||
|
|
||||||
# --- KV admin endpoints ---
|
# --- KV admin endpoints ---
|
||||||
@app.get("/settings/kv/<key>")
|
@app.get("/settings/kv/<key>")
|
||||||
|
|||||||
@@ -3,3 +3,4 @@ from .admin.routes import register as register_admin
|
|||||||
from .menu_items.routes import register as register_menu_items
|
from .menu_items.routes import register as register_menu_items
|
||||||
from .snippets.routes import register as register_snippets
|
from .snippets.routes import register as register_snippets
|
||||||
from .fragments import register_fragments
|
from .fragments import register_fragments
|
||||||
|
from .data import register_data
|
||||||
|
|||||||
1
blog/bp/data/__init__.py
Normal file
1
blog/bp/data/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .routes import register as register_data
|
||||||
74
blog/bp/data/routes.py
Normal file
74
blog/bp/data/routes.py
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
"""Blog 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)
|
||||||
|
|
||||||
|
# --- post-by-slug ---
|
||||||
|
async def _post_by_slug():
|
||||||
|
slug = request.args.get("slug", "")
|
||||||
|
post = await services.blog.get_post_by_slug(g.s, slug)
|
||||||
|
if not post:
|
||||||
|
return None
|
||||||
|
return dto_to_dict(post)
|
||||||
|
|
||||||
|
_handlers["post-by-slug"] = _post_by_slug
|
||||||
|
|
||||||
|
# --- post-by-id ---
|
||||||
|
async def _post_by_id():
|
||||||
|
post_id = int(request.args.get("id", 0))
|
||||||
|
post = await services.blog.get_post_by_id(g.s, post_id)
|
||||||
|
if not post:
|
||||||
|
return None
|
||||||
|
return dto_to_dict(post)
|
||||||
|
|
||||||
|
_handlers["post-by-id"] = _post_by_id
|
||||||
|
|
||||||
|
# --- posts-by-ids ---
|
||||||
|
async def _posts_by_ids():
|
||||||
|
ids_raw = request.args.get("ids", "")
|
||||||
|
if not ids_raw:
|
||||||
|
return []
|
||||||
|
ids = [int(x.strip()) for x in ids_raw.split(",") if x.strip()]
|
||||||
|
posts = await services.blog.get_posts_by_ids(g.s, ids)
|
||||||
|
return [dto_to_dict(p) for p in posts]
|
||||||
|
|
||||||
|
_handlers["posts-by-ids"] = _posts_by_ids
|
||||||
|
|
||||||
|
# --- search-posts ---
|
||||||
|
async def _search_posts():
|
||||||
|
query = request.args.get("query", "")
|
||||||
|
page = int(request.args.get("page", 1))
|
||||||
|
per_page = int(request.args.get("per_page", 10))
|
||||||
|
posts, total = await services.blog.search_posts(g.s, query, page, per_page)
|
||||||
|
return {"posts": [dto_to_dict(p) for p in posts], "total": total}
|
||||||
|
|
||||||
|
_handlers["search-posts"] = _search_posts
|
||||||
|
|
||||||
|
return bp
|
||||||
@@ -193,7 +193,8 @@ def register():
|
|||||||
"""Show calendar month view for browsing entries"""
|
"""Show calendar month view for browsing entries"""
|
||||||
from shared.models.calendars import Calendar
|
from shared.models.calendars import Calendar
|
||||||
from shared.utils.calendar_helpers import parse_int_arg, add_months, build_calendar_weeks
|
from shared.utils.calendar_helpers import parse_int_arg, add_months, build_calendar_weeks
|
||||||
from shared.services.registry import services
|
from shared.infrastructure.data_client import fetch_data
|
||||||
|
from shared.contracts.dtos import CalendarEntryDTO, dto_from_dict
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
import calendar as pycalendar
|
import calendar as pycalendar
|
||||||
@@ -228,7 +229,7 @@ def register():
|
|||||||
month_name = pycalendar.month_name[month]
|
month_name = pycalendar.month_name[month]
|
||||||
weekday_names = [pycalendar.day_abbr[i] for i in range(7)]
|
weekday_names = [pycalendar.day_abbr[i] for i in range(7)]
|
||||||
|
|
||||||
# Get entries for this month
|
# Get entries for this month via events data endpoint
|
||||||
period_start = datetime(year, month, 1, tzinfo=timezone.utc)
|
period_start = datetime(year, month, 1, tzinfo=timezone.utc)
|
||||||
next_y, next_m = add_months(year, month, +1)
|
next_y, next_m = add_months(year, month, +1)
|
||||||
period_end = datetime(next_y, next_m, 1, tzinfo=timezone.utc)
|
period_end = datetime(next_y, next_m, 1, tzinfo=timezone.utc)
|
||||||
@@ -238,10 +239,15 @@ def register():
|
|||||||
is_admin = bool(user and getattr(user, "is_admin", False))
|
is_admin = bool(user and getattr(user, "is_admin", False))
|
||||||
session_id = qsession.get("calendar_sid")
|
session_id = qsession.get("calendar_sid")
|
||||||
|
|
||||||
month_entries = await services.calendar.visible_entries_for_period(
|
raw_entries = await fetch_data("events", "visible-entries-for-period", params={
|
||||||
g.s, calendar_obj.id, period_start, period_end,
|
"calendar_id": calendar_obj.id,
|
||||||
user_id=user_id, is_admin=is_admin, session_id=session_id,
|
"period_start": period_start.isoformat(),
|
||||||
)
|
"period_end": period_end.isoformat(),
|
||||||
|
"user_id": user_id,
|
||||||
|
"is_admin": str(is_admin).lower(),
|
||||||
|
"session_id": session_id,
|
||||||
|
}, required=False) or []
|
||||||
|
month_entries = [dto_from_dict(CalendarEntryDTO, e) for e in raw_entries]
|
||||||
|
|
||||||
# Get associated entry IDs for this post
|
# Get associated entry IDs for this post
|
||||||
post_id = g.post_data["post"]["id"]
|
post_id = g.post_data["post"]["id"]
|
||||||
@@ -609,18 +615,24 @@ def register():
|
|||||||
return redirect(redirect_url)
|
return redirect(redirect_url)
|
||||||
|
|
||||||
|
|
||||||
|
async def _fetch_page_markets(post_id):
|
||||||
|
"""Fetch marketplaces for a page via market data endpoint."""
|
||||||
|
from shared.infrastructure.data_client import fetch_data
|
||||||
|
from shared.contracts.dtos import MarketPlaceDTO, dto_from_dict
|
||||||
|
raw = await fetch_data("market", "marketplaces-for-container",
|
||||||
|
params={"type": "page", "id": post_id}, required=False) or []
|
||||||
|
return [dto_from_dict(MarketPlaceDTO, m) for m in raw]
|
||||||
|
|
||||||
@bp.get("/markets/")
|
@bp.get("/markets/")
|
||||||
@require_admin
|
@require_admin
|
||||||
async def markets(slug: str):
|
async def markets(slug: str):
|
||||||
"""List markets for this page."""
|
"""List markets for this page."""
|
||||||
from shared.services.registry import services
|
|
||||||
|
|
||||||
post = (g.post_data or {}).get("post", {})
|
post = (g.post_data or {}).get("post", {})
|
||||||
post_id = post.get("id")
|
post_id = post.get("id")
|
||||||
if not post_id:
|
if not post_id:
|
||||||
return await make_response("Post not found", 404)
|
return await make_response("Post not found", 404)
|
||||||
|
|
||||||
page_markets = await services.market.marketplaces_for_container(g.s, "page", post_id)
|
page_markets = await _fetch_page_markets(post_id)
|
||||||
|
|
||||||
html = await render_template(
|
html = await render_template(
|
||||||
"_types/post/admin/_markets_panel.html",
|
"_types/post/admin/_markets_panel.html",
|
||||||
@@ -634,7 +646,6 @@ def register():
|
|||||||
async def create_market(slug: str):
|
async def create_market(slug: str):
|
||||||
"""Create a new market for this page."""
|
"""Create a new market for this page."""
|
||||||
from ..services.markets import create_market as _create_market, MarketError
|
from ..services.markets import create_market as _create_market, MarketError
|
||||||
from shared.services.registry import services
|
|
||||||
from quart import jsonify
|
from quart import jsonify
|
||||||
|
|
||||||
post = (g.post_data or {}).get("post", {})
|
post = (g.post_data or {}).get("post", {})
|
||||||
@@ -651,7 +662,7 @@ def register():
|
|||||||
return jsonify({"error": str(e)}), 400
|
return jsonify({"error": str(e)}), 400
|
||||||
|
|
||||||
# Return updated markets list
|
# Return updated markets list
|
||||||
page_markets = await services.market.marketplaces_for_container(g.s, "page", post_id)
|
page_markets = await _fetch_page_markets(post_id)
|
||||||
|
|
||||||
html = await render_template(
|
html = await render_template(
|
||||||
"_types/post/admin/_markets_panel.html",
|
"_types/post/admin/_markets_panel.html",
|
||||||
@@ -665,7 +676,6 @@ def register():
|
|||||||
async def delete_market(slug: str, market_slug: str):
|
async def delete_market(slug: str, market_slug: str):
|
||||||
"""Soft-delete a market."""
|
"""Soft-delete a market."""
|
||||||
from ..services.markets import soft_delete_market
|
from ..services.markets import soft_delete_market
|
||||||
from shared.services.registry import services
|
|
||||||
from quart import jsonify
|
from quart import jsonify
|
||||||
|
|
||||||
post = (g.post_data or {}).get("post", {})
|
post = (g.post_data or {}).get("post", {})
|
||||||
@@ -676,7 +686,7 @@ def register():
|
|||||||
return jsonify({"error": "Market not found"}), 404
|
return jsonify({"error": "Market not found"}), 404
|
||||||
|
|
||||||
# Return updated markets list
|
# Return updated markets list
|
||||||
page_markets = await services.market.marketplaces_for_container(g.s, "page", post_id)
|
page_markets = await _fetch_page_markets(post_id)
|
||||||
|
|
||||||
html = await render_template(
|
html = await render_template(
|
||||||
"_types/post/admin/_markets_panel.html",
|
"_types/post/admin/_markets_panel.html",
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ from quart import (
|
|||||||
)
|
)
|
||||||
from .services.post_data import post_data
|
from .services.post_data import post_data
|
||||||
from .services.post_operations import toggle_post_like
|
from .services.post_operations import toggle_post_like
|
||||||
from shared.services.registry import services
|
from shared.infrastructure.data_client import fetch_data
|
||||||
|
from shared.contracts.dtos import CartSummaryDTO, dto_from_dict
|
||||||
from shared.infrastructure.fragments import fetch_fragment, fetch_fragments
|
from shared.infrastructure.fragments import fetch_fragment, fetch_fragments
|
||||||
|
|
||||||
from shared.browser.app.redis_cacher import cache_page, clear_cache
|
from shared.browser.app.redis_cacher import cache_page, clear_cache
|
||||||
@@ -92,14 +93,17 @@ def register():
|
|||||||
"container_nav_html": container_nav_html,
|
"container_nav_html": container_nav_html,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Page cart badge via service
|
# Page cart badge via HTTP
|
||||||
post_dict = p_data.get("post") or {}
|
post_dict = p_data.get("post") or {}
|
||||||
if post_dict.get("is_page"):
|
if post_dict.get("is_page"):
|
||||||
ident = current_cart_identity()
|
ident = current_cart_identity()
|
||||||
page_summary = await services.cart.cart_summary(
|
summary_params = {"page_slug": post_dict["slug"]}
|
||||||
g.s, user_id=ident["user_id"], session_id=ident["session_id"],
|
if ident["user_id"] is not None:
|
||||||
page_slug=post_dict["slug"],
|
summary_params["user_id"] = ident["user_id"]
|
||||||
)
|
if ident["session_id"] is not None:
|
||||||
|
summary_params["session_id"] = ident["session_id"]
|
||||||
|
raw_summary = await fetch_data("cart", "cart-summary", params=summary_params, required=False)
|
||||||
|
page_summary = dto_from_dict(CartSummaryDTO, raw_summary) if raw_summary else CartSummaryDTO()
|
||||||
ctx["page_cart_count"] = page_summary.count + page_summary.calendar_count + page_summary.ticket_count
|
ctx["page_cart_count"] = page_summary.count + page_summary.calendar_count + page_summary.ticket_count
|
||||||
ctx["page_cart_total"] = float(page_summary.total + page_summary.calendar_total + page_summary.ticket_total)
|
ctx["page_cart_total"] = float(page_summary.total + page_summary.calendar_total + page_summary.ticket_total)
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from shared.infrastructure.actions import call_action, ActionError
|
||||||
|
from shared.infrastructure.data_client import fetch_data
|
||||||
|
from shared.contracts.dtos import CalendarEntryDTO, dto_from_dict
|
||||||
from shared.services.registry import services
|
from shared.services.registry import services
|
||||||
|
|
||||||
|
|
||||||
@@ -18,10 +21,13 @@ async def toggle_entry_association(
|
|||||||
if not post:
|
if not post:
|
||||||
return False, "Post not found"
|
return False, "Post not found"
|
||||||
|
|
||||||
is_associated = await services.calendar.toggle_entry_post(
|
try:
|
||||||
session, entry_id, "post", post_id,
|
result = await call_action("events", "toggle-entry-post", payload={
|
||||||
)
|
"entry_id": entry_id, "content_type": "post", "content_id": post_id,
|
||||||
return is_associated, None
|
})
|
||||||
|
return result.get("is_associated", False), None
|
||||||
|
except ActionError as e:
|
||||||
|
return False, str(e)
|
||||||
|
|
||||||
|
|
||||||
async def get_post_entry_ids(
|
async def get_post_entry_ids(
|
||||||
@@ -32,7 +38,10 @@ async def get_post_entry_ids(
|
|||||||
Get all entry IDs associated with this post.
|
Get all entry IDs associated with this post.
|
||||||
Returns a set of entry IDs.
|
Returns a set of entry IDs.
|
||||||
"""
|
"""
|
||||||
return await services.calendar.entry_ids_for_content(session, "post", post_id)
|
raw = await fetch_data("events", "entry-ids-for-content",
|
||||||
|
params={"content_type": "post", "content_id": post_id},
|
||||||
|
required=False) or []
|
||||||
|
return set(raw)
|
||||||
|
|
||||||
|
|
||||||
async def get_associated_entries(
|
async def get_associated_entries(
|
||||||
@@ -45,12 +54,14 @@ async def get_associated_entries(
|
|||||||
Get paginated associated entries for this post.
|
Get paginated associated entries for this post.
|
||||||
Returns dict with entries (CalendarEntryDTOs), total_count, and has_more.
|
Returns dict with entries (CalendarEntryDTOs), total_count, and has_more.
|
||||||
"""
|
"""
|
||||||
entries, has_more = await services.calendar.associated_entries(
|
raw = await fetch_data("events", "associated-entries",
|
||||||
session, "post", post_id, page,
|
params={"content_type": "post", "content_id": post_id, "page": page},
|
||||||
)
|
required=False) or {"entries": [], "has_more": False}
|
||||||
|
entries = [dto_from_dict(CalendarEntryDTO, e) for e in raw.get("entries", [])]
|
||||||
|
has_more = raw.get("has_more", False)
|
||||||
total_count = len(entries) + (page - 1) * per_page
|
total_count = len(entries) + (page - 1) * per_page
|
||||||
if has_more:
|
if has_more:
|
||||||
total_count += 1 # at least one more
|
total_count += 1
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"entries": entries,
|
"entries": entries,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||||||
|
|
||||||
from shared.models.page_config import PageConfig
|
from shared.models.page_config import PageConfig
|
||||||
from shared.contracts.dtos import MarketPlaceDTO
|
from shared.contracts.dtos import MarketPlaceDTO
|
||||||
|
from shared.infrastructure.actions import call_action, ActionError
|
||||||
from shared.services.registry import services
|
from shared.services.registry import services
|
||||||
|
|
||||||
|
|
||||||
@@ -48,8 +49,12 @@ async def create_market(sess: AsyncSession, post_id: int, name: str) -> MarketPl
|
|||||||
raise MarketError("Market feature is not enabled for this page. Enable it in page settings first.")
|
raise MarketError("Market feature is not enabled for this page. Enable it in page settings first.")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return await services.market.create_marketplace(sess, "page", post_id, name, slug)
|
result = await call_action("market", "create-marketplace", payload={
|
||||||
except ValueError as e:
|
"container_type": "page", "container_id": post_id,
|
||||||
|
"name": name, "slug": slug,
|
||||||
|
})
|
||||||
|
return MarketPlaceDTO(**result)
|
||||||
|
except ActionError as e:
|
||||||
raise MarketError(str(e)) from e
|
raise MarketError(str(e)) from e
|
||||||
|
|
||||||
|
|
||||||
@@ -58,4 +63,10 @@ async def soft_delete_market(sess: AsyncSession, post_slug: str, market_slug: st
|
|||||||
if not post:
|
if not post:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return await services.market.soft_delete_marketplace(sess, "page", post.id, market_slug)
|
try:
|
||||||
|
result = await call_action("market", "soft-delete-marketplace", payload={
|
||||||
|
"container_type": "page", "container_id": post.id, "slug": market_slug,
|
||||||
|
})
|
||||||
|
return result.get("deleted", False)
|
||||||
|
except ActionError:
|
||||||
|
return False
|
||||||
|
|||||||
@@ -6,23 +6,14 @@ def register_domain_services() -> None:
|
|||||||
"""Register services for the blog app.
|
"""Register services for the blog app.
|
||||||
|
|
||||||
Blog owns: Post, Tag, Author, PostAuthor, PostTag, PostLike.
|
Blog owns: Post, Tag, Author, PostAuthor, PostTag, PostLike.
|
||||||
Standard deployment registers all 4 services as real DB impls
|
Cross-app calls go over HTTP via call_action() / fetch_data().
|
||||||
(shared DB). For composable deployments, swap non-owned services
|
|
||||||
with stubs from shared.services.stubs.
|
|
||||||
"""
|
"""
|
||||||
from shared.services.registry import services
|
from shared.services.registry import services
|
||||||
from shared.services.blog_impl import SqlBlogService
|
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.blog = SqlBlogService()
|
services.blog = SqlBlogService()
|
||||||
if not services.has("calendar"):
|
|
||||||
services.calendar = SqlCalendarService()
|
# Federation needed for AP shared infrastructure (activitypub blueprint)
|
||||||
if not services.has("market"):
|
|
||||||
services.market = SqlMarketService()
|
|
||||||
if not services.has("cart"):
|
|
||||||
services.cart = SqlCartService()
|
|
||||||
if not services.has("federation"):
|
if not services.has("federation"):
|
||||||
from shared.services.federation_impl import SqlFederationService
|
from shared.services.federation_impl import SqlFederationService
|
||||||
services.federation = SqlFederationService()
|
services.federation = SqlFederationService()
|
||||||
|
|||||||
11
cart/app.py
11
cart/app.py
@@ -16,6 +16,8 @@ from bp import (
|
|||||||
register_cart_global,
|
register_cart_global,
|
||||||
register_orders,
|
register_orders,
|
||||||
register_fragments,
|
register_fragments,
|
||||||
|
register_actions,
|
||||||
|
register_data,
|
||||||
)
|
)
|
||||||
from bp.cart.services import (
|
from bp.cart.services import (
|
||||||
get_cart,
|
get_cart,
|
||||||
@@ -135,6 +137,8 @@ def create_app() -> "Quart":
|
|||||||
app.jinja_env.globals["cart_delete_url"] = lambda product_id: f"/delete/{product_id}/"
|
app.jinja_env.globals["cart_delete_url"] = lambda product_id: f"/delete/{product_id}/"
|
||||||
|
|
||||||
app.register_blueprint(register_fragments())
|
app.register_blueprint(register_fragments())
|
||||||
|
app.register_blueprint(register_actions())
|
||||||
|
app.register_blueprint(register_data())
|
||||||
|
|
||||||
# --- Page slug hydration (follows events/market app pattern) ---
|
# --- Page slug hydration (follows events/market app pattern) ---
|
||||||
|
|
||||||
@@ -152,10 +156,15 @@ def create_app() -> "Quart":
|
|||||||
|
|
||||||
@app.before_request
|
@app.before_request
|
||||||
async def hydrate_page():
|
async def hydrate_page():
|
||||||
|
from shared.infrastructure.data_client import fetch_data
|
||||||
|
from shared.contracts.dtos import PostDTO, dto_from_dict
|
||||||
slug = getattr(g, "page_slug", None)
|
slug = getattr(g, "page_slug", None)
|
||||||
if not slug:
|
if not slug:
|
||||||
return
|
return
|
||||||
post = await services.blog.get_post_by_slug(g.s, slug)
|
raw = await fetch_data("blog", "post-by-slug", params={"slug": slug})
|
||||||
|
if not raw:
|
||||||
|
abort(404)
|
||||||
|
post = dto_from_dict(PostDTO, raw)
|
||||||
if not post or not post.is_page:
|
if not post or not post.is_page:
|
||||||
abort(404)
|
abort(404)
|
||||||
g.page_post = post
|
g.page_post = post
|
||||||
|
|||||||
@@ -4,3 +4,5 @@ from .cart.global_routes import register as register_cart_global
|
|||||||
from .order.routes import register as register_order
|
from .order.routes import register as register_order
|
||||||
from .orders.routes import register as register_orders
|
from .orders.routes import register as register_orders
|
||||||
from .fragments import register_fragments
|
from .fragments import register_fragments
|
||||||
|
from .actions import register_actions
|
||||||
|
from .data import register_data
|
||||||
|
|||||||
1
cart/bp/actions/__init__.py
Normal file
1
cart/bp/actions/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .routes import register as register_actions
|
||||||
42
cart/bp/actions/routes.py
Normal file
42
cart/bp/actions/routes.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
"""Cart app action endpoints.
|
||||||
|
|
||||||
|
Exposes write operations at ``/internal/actions/<action_name>`` for
|
||||||
|
cross-app callers (login handler) 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)
|
||||||
|
|
||||||
|
# --- adopt-cart-for-user ---
|
||||||
|
async def _adopt_cart():
|
||||||
|
data = await request.get_json()
|
||||||
|
await services.cart.adopt_cart_for_user(
|
||||||
|
g.s, data["user_id"], data["session_id"],
|
||||||
|
)
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
_handlers["adopt-cart-for-user"] = _adopt_cart
|
||||||
|
|
||||||
|
return bp
|
||||||
@@ -8,7 +8,7 @@ from sqlalchemy import select
|
|||||||
from shared.models.market import CartItem
|
from shared.models.market import CartItem
|
||||||
from shared.models.order import Order
|
from shared.models.order import Order
|
||||||
from shared.models.market_place import MarketPlace
|
from shared.models.market_place import MarketPlace
|
||||||
from shared.services.registry import services
|
from shared.infrastructure.actions import call_action
|
||||||
from .services import (
|
from .services import (
|
||||||
current_cart_identity,
|
current_cart_identity,
|
||||||
get_cart,
|
get_cart,
|
||||||
@@ -91,13 +91,12 @@ def register(url_prefix: str) -> Blueprint:
|
|||||||
tt_raw = (form.get("ticket_type_id") or "").strip()
|
tt_raw = (form.get("ticket_type_id") or "").strip()
|
||||||
ticket_type_id = int(tt_raw) if tt_raw else None
|
ticket_type_id = int(tt_raw) if tt_raw else None
|
||||||
|
|
||||||
await services.calendar.adjust_ticket_quantity(
|
await call_action("events", "adjust-ticket-quantity", payload={
|
||||||
g.s, entry_id, count,
|
"entry_id": entry_id, "count": count,
|
||||||
user_id=ident["user_id"],
|
"user_id": ident["user_id"],
|
||||||
session_id=ident["session_id"],
|
"session_id": ident["session_id"],
|
||||||
ticket_type_id=ticket_type_id,
|
"ticket_type_id": ticket_type_id,
|
||||||
)
|
})
|
||||||
await g.s.flush()
|
|
||||||
|
|
||||||
resp = await make_response("", 200)
|
resp = await make_response("", 200)
|
||||||
resp.headers["HX-Refresh"] = "true"
|
resp.headers["HX-Refresh"] = "true"
|
||||||
@@ -256,13 +255,17 @@ def register(url_prefix: str) -> Blueprint:
|
|||||||
|
|
||||||
# Resolve page/market slugs so product links render correctly
|
# Resolve page/market slugs so product links render correctly
|
||||||
if order.page_config:
|
if order.page_config:
|
||||||
post = await services.blog.get_post_by_id(g.s, order.page_config.container_id)
|
from shared.infrastructure.data_client import fetch_data
|
||||||
|
from shared.contracts.dtos import CalendarEntryDTO, TicketDTO, dto_from_dict
|
||||||
|
post = await fetch_data("blog", "post-by-id",
|
||||||
|
params={"id": order.page_config.container_id},
|
||||||
|
required=False)
|
||||||
if post:
|
if post:
|
||||||
g.page_slug = post.slug
|
g.page_slug = post["slug"]
|
||||||
result = await g.s.execute(
|
result = await g.s.execute(
|
||||||
select(MarketPlace).where(
|
select(MarketPlace).where(
|
||||||
MarketPlace.container_type == "page",
|
MarketPlace.container_type == "page",
|
||||||
MarketPlace.container_id == post.id,
|
MarketPlace.container_id == post["id"],
|
||||||
MarketPlace.deleted_at.is_(None),
|
MarketPlace.deleted_at.is_(None),
|
||||||
).limit(1)
|
).limit(1)
|
||||||
)
|
)
|
||||||
@@ -278,8 +281,14 @@ def register(url_prefix: str) -> Blueprint:
|
|||||||
|
|
||||||
status = (order.status or "pending").lower()
|
status = (order.status or "pending").lower()
|
||||||
|
|
||||||
calendar_entries = await services.calendar.get_entries_for_order(g.s, order.id)
|
from shared.infrastructure.data_client import fetch_data
|
||||||
order_tickets = await services.calendar.get_tickets_for_order(g.s, order.id)
|
from shared.contracts.dtos import CalendarEntryDTO, TicketDTO, dto_from_dict
|
||||||
|
raw_entries = await fetch_data("events", "entries-for-order",
|
||||||
|
params={"order_id": order.id}, required=False) or []
|
||||||
|
calendar_entries = [dto_from_dict(CalendarEntryDTO, e) for e in raw_entries]
|
||||||
|
raw_tickets = await fetch_data("events", "tickets-for-order",
|
||||||
|
params={"order_id": order.id}, required=False) or []
|
||||||
|
order_tickets = [dto_from_dict(TicketDTO, t) for t in raw_tickets]
|
||||||
await g.s.flush()
|
await g.s.flush()
|
||||||
|
|
||||||
html = await render_template(
|
html = await render_template(
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
from shared.services.registry import services
|
from shared.infrastructure.data_client import fetch_data
|
||||||
|
from shared.contracts.dtos import CalendarEntryDTO, TicketDTO, dto_from_dict
|
||||||
from .identity import current_cart_identity
|
from .identity import current_cart_identity
|
||||||
|
|
||||||
|
|
||||||
@@ -12,11 +13,13 @@ async def get_calendar_cart_entries(session):
|
|||||||
current cart identity (user or anonymous session).
|
current cart identity (user or anonymous session).
|
||||||
"""
|
"""
|
||||||
ident = current_cart_identity()
|
ident = current_cart_identity()
|
||||||
return await services.calendar.pending_entries(
|
params = {}
|
||||||
session,
|
if ident["user_id"] is not None:
|
||||||
user_id=ident["user_id"],
|
params["user_id"] = ident["user_id"]
|
||||||
session_id=ident["session_id"],
|
if ident["session_id"] is not None:
|
||||||
)
|
params["session_id"] = ident["session_id"]
|
||||||
|
raw = await fetch_data("events", "pending-entries", params=params, required=False) or []
|
||||||
|
return [dto_from_dict(CalendarEntryDTO, e) for e in raw]
|
||||||
|
|
||||||
|
|
||||||
def calendar_total(entries) -> Decimal:
|
def calendar_total(entries) -> Decimal:
|
||||||
@@ -33,11 +36,13 @@ def calendar_total(entries) -> Decimal:
|
|||||||
async def get_ticket_cart_entries(session):
|
async def get_ticket_cart_entries(session):
|
||||||
"""Return all reserved tickets (as TicketDTOs) for the current identity."""
|
"""Return all reserved tickets (as TicketDTOs) for the current identity."""
|
||||||
ident = current_cart_identity()
|
ident = current_cart_identity()
|
||||||
return await services.calendar.pending_tickets(
|
params = {}
|
||||||
session,
|
if ident["user_id"] is not None:
|
||||||
user_id=ident["user_id"],
|
params["user_id"] = ident["user_id"]
|
||||||
session_id=ident["session_id"],
|
if ident["session_id"] is not None:
|
||||||
)
|
params["session_id"] = ident["session_id"]
|
||||||
|
raw = await fetch_data("events", "pending-tickets", params=params, required=False) or []
|
||||||
|
return [dto_from_dict(TicketDTO, t) for t in raw]
|
||||||
|
|
||||||
|
|
||||||
def ticket_total(tickets) -> Decimal:
|
def ticket_total(tickets) -> Decimal:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from shared.browser.app.payments.sumup import get_checkout as sumup_get_checkout
|
from shared.browser.app.payments.sumup import get_checkout as sumup_get_checkout
|
||||||
from shared.events import emit_activity
|
from shared.events import emit_activity
|
||||||
from shared.services.registry import services
|
from shared.infrastructure.actions import call_action
|
||||||
from .clear_cart_for_order import clear_cart_for_order
|
from .clear_cart_for_order import clear_cart_for_order
|
||||||
|
|
||||||
|
|
||||||
@@ -14,10 +14,13 @@ async def check_sumup_status(session, order):
|
|||||||
if sumup_status == "PAID":
|
if sumup_status == "PAID":
|
||||||
if order.status != "paid":
|
if order.status != "paid":
|
||||||
order.status = "paid"
|
order.status = "paid"
|
||||||
await services.calendar.confirm_entries_for_order(
|
await call_action("events", "confirm-entries-for-order", payload={
|
||||||
session, order.id, order.user_id, order.session_id
|
"order_id": order.id, "user_id": order.user_id,
|
||||||
)
|
"session_id": order.session_id,
|
||||||
await services.calendar.confirm_tickets_for_order(session, order.id)
|
})
|
||||||
|
await call_action("events", "confirm-tickets-for-order", payload={
|
||||||
|
"order_id": order.id,
|
||||||
|
})
|
||||||
|
|
||||||
# Clear cart only after payment is confirmed
|
# Clear cart only after payment is confirmed
|
||||||
page_post_id = page_config.container_id if page_config else None
|
page_post_id = page_config.container_id if page_config else None
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from shared.models.market_place import MarketPlace
|
|||||||
from shared.config import config
|
from shared.config import config
|
||||||
from shared.contracts.dtos import CalendarEntryDTO
|
from shared.contracts.dtos import CalendarEntryDTO
|
||||||
from shared.events import emit_activity
|
from shared.events import emit_activity
|
||||||
from shared.services.registry import services
|
from shared.infrastructure.actions import call_action
|
||||||
|
|
||||||
|
|
||||||
async def find_or_create_cart_item(
|
async def find_or_create_cart_item(
|
||||||
@@ -156,15 +156,17 @@ async def create_order_from_cart(
|
|||||||
)
|
)
|
||||||
session.add(oi)
|
session.add(oi)
|
||||||
|
|
||||||
# Mark pending calendar entries as "ordered" via calendar service
|
# Mark pending calendar entries as "ordered" via events action endpoint
|
||||||
await services.calendar.claim_entries_for_order(
|
await call_action("events", "claim-entries-for-order", payload={
|
||||||
session, order.id, user_id, session_id, page_post_id
|
"order_id": order.id, "user_id": user_id,
|
||||||
)
|
"session_id": session_id, "page_post_id": page_post_id,
|
||||||
|
})
|
||||||
|
|
||||||
# Claim reserved tickets for this order
|
# Claim reserved tickets for this order
|
||||||
await services.calendar.claim_tickets_for_order(
|
await call_action("events", "claim-tickets-for-order", payload={
|
||||||
session, order.id, user_id, session_id, page_post_id
|
"order_id": order.id, "user_id": user_id,
|
||||||
)
|
"session_id": session_id, "page_post_id": page_post_id,
|
||||||
|
})
|
||||||
|
|
||||||
await emit_activity(
|
await emit_activity(
|
||||||
session,
|
session,
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ from sqlalchemy.orm import selectinload
|
|||||||
from shared.models.market import CartItem
|
from shared.models.market import CartItem
|
||||||
from shared.models.market_place import MarketPlace
|
from shared.models.market_place import MarketPlace
|
||||||
from shared.models.page_config import PageConfig
|
from shared.models.page_config import PageConfig
|
||||||
from shared.services.registry import services
|
from shared.infrastructure.data_client import fetch_data
|
||||||
|
from shared.contracts.dtos import CalendarEntryDTO, TicketDTO, PostDTO, dto_from_dict
|
||||||
from .identity import current_cart_identity
|
from .identity import current_cart_identity
|
||||||
|
|
||||||
|
|
||||||
@@ -50,21 +51,25 @@ async def get_cart_for_page(session, post_id: int) -> list[CartItem]:
|
|||||||
async def get_calendar_entries_for_page(session, post_id: int):
|
async def get_calendar_entries_for_page(session, post_id: int):
|
||||||
"""Return pending calendar entries (DTOs) scoped to a specific page."""
|
"""Return pending calendar entries (DTOs) scoped to a specific page."""
|
||||||
ident = current_cart_identity()
|
ident = current_cart_identity()
|
||||||
return await services.calendar.entries_for_page(
|
params = {"page_id": post_id}
|
||||||
session, post_id,
|
if ident["user_id"] is not None:
|
||||||
user_id=ident["user_id"],
|
params["user_id"] = ident["user_id"]
|
||||||
session_id=ident["session_id"],
|
if ident["session_id"] is not None:
|
||||||
)
|
params["session_id"] = ident["session_id"]
|
||||||
|
raw = await fetch_data("events", "entries-for-page", params=params, required=False) or []
|
||||||
|
return [dto_from_dict(CalendarEntryDTO, e) for e in raw]
|
||||||
|
|
||||||
|
|
||||||
async def get_tickets_for_page(session, post_id: int):
|
async def get_tickets_for_page(session, post_id: int):
|
||||||
"""Return reserved tickets (DTOs) scoped to a specific page."""
|
"""Return reserved tickets (DTOs) scoped to a specific page."""
|
||||||
ident = current_cart_identity()
|
ident = current_cart_identity()
|
||||||
return await services.calendar.tickets_for_page(
|
params = {"page_id": post_id}
|
||||||
session, post_id,
|
if ident["user_id"] is not None:
|
||||||
user_id=ident["user_id"],
|
params["user_id"] = ident["user_id"]
|
||||||
session_id=ident["session_id"],
|
if ident["session_id"] is not None:
|
||||||
)
|
params["session_id"] = ident["session_id"]
|
||||||
|
raw = await fetch_data("events", "tickets-for-page", params=params, required=False) or []
|
||||||
|
return [dto_from_dict(TicketDTO, t) for t in raw]
|
||||||
|
|
||||||
|
|
||||||
async def get_cart_grouped_by_page(session) -> list[dict]:
|
async def get_cart_grouped_by_page(session) -> list[dict]:
|
||||||
@@ -167,7 +172,11 @@ async def get_cart_grouped_by_page(session) -> list[dict]:
|
|||||||
configs_by_post: dict[int, PageConfig] = {}
|
configs_by_post: dict[int, PageConfig] = {}
|
||||||
|
|
||||||
if post_ids:
|
if post_ids:
|
||||||
for p in await services.blog.get_posts_by_ids(session, post_ids):
|
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)
|
||||||
posts_by_id[p.id] = p
|
posts_by_id[p.id] = p
|
||||||
|
|
||||||
pc_result = await session.execute(
|
pc_result = await session.execute(
|
||||||
|
|||||||
1
cart/bp/data/__init__.py
Normal file
1
cart/bp/data/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .routes import register as register_data
|
||||||
45
cart/bp/data/routes.py
Normal file
45
cart/bp/data/routes.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
"""Cart 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)
|
||||||
|
|
||||||
|
# --- cart-summary ---
|
||||||
|
async def _cart_summary():
|
||||||
|
user_id = request.args.get("user_id", type=int)
|
||||||
|
session_id = request.args.get("session_id")
|
||||||
|
page_slug = request.args.get("page_slug")
|
||||||
|
summary = await services.cart.cart_summary(
|
||||||
|
g.s, user_id=user_id, session_id=session_id, page_slug=page_slug,
|
||||||
|
)
|
||||||
|
return dto_to_dict(summary)
|
||||||
|
|
||||||
|
_handlers["cart-summary"] = _cart_summary
|
||||||
|
|
||||||
|
return bp
|
||||||
@@ -6,23 +6,9 @@ def register_domain_services() -> None:
|
|||||||
"""Register services for the cart app.
|
"""Register services for the cart app.
|
||||||
|
|
||||||
Cart owns: Order, OrderItem.
|
Cart owns: Order, OrderItem.
|
||||||
Standard deployment registers all 4 services as real DB impls
|
Cross-app calls go over HTTP via call_action() / fetch_data().
|
||||||
(shared DB). For composable deployments, swap non-owned services
|
|
||||||
with stubs from shared.services.stubs.
|
|
||||||
"""
|
"""
|
||||||
from shared.services.registry import services
|
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
|
from shared.services.cart_impl import SqlCartService
|
||||||
|
|
||||||
services.cart = SqlCartService()
|
services.cart = SqlCartService()
|
||||||
if not services.has("blog"):
|
|
||||||
services.blog = SqlBlogService()
|
|
||||||
if not services.has("calendar"):
|
|
||||||
services.calendar = SqlCalendarService()
|
|
||||||
if not services.has("market"):
|
|
||||||
services.market = SqlMarketService()
|
|
||||||
if not services.has("federation"):
|
|
||||||
from shared.services.federation_impl import SqlFederationService
|
|
||||||
services.federation = SqlFederationService()
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from jinja2 import FileSystemLoader, ChoiceLoader
|
|||||||
|
|
||||||
from shared.infrastructure.factory import create_base_app
|
from shared.infrastructure.factory import create_base_app
|
||||||
|
|
||||||
from bp import register_all_events, register_calendars, register_markets, register_payments, register_page, register_fragments
|
from bp import register_all_events, register_calendars, register_markets, register_payments, register_page, register_fragments, register_actions, register_data
|
||||||
|
|
||||||
|
|
||||||
async def events_context() -> dict:
|
async def events_context() -> dict:
|
||||||
@@ -20,20 +20,25 @@ async def events_context() -> dict:
|
|||||||
"""
|
"""
|
||||||
from shared.infrastructure.context import base_context
|
from shared.infrastructure.context import base_context
|
||||||
from shared.services.navigation import get_navigation_tree
|
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.cart_identity import current_cart_identity
|
||||||
from shared.infrastructure.fragments import fetch_fragments
|
from shared.infrastructure.fragments import fetch_fragments
|
||||||
|
from shared.infrastructure.data_client import fetch_data
|
||||||
|
from shared.contracts.dtos import CartSummaryDTO, dto_from_dict
|
||||||
|
|
||||||
ctx = await base_context()
|
ctx = await base_context()
|
||||||
|
|
||||||
# Fallback for _nav.html when nav-tree fragment fetch fails
|
# Fallback for _nav.html when nav-tree fragment fetch fails
|
||||||
ctx["menu_items"] = await get_navigation_tree(g.s)
|
ctx["menu_items"] = await get_navigation_tree(g.s)
|
||||||
|
|
||||||
# Cart data via service (replaces cross-app HTTP API)
|
# Cart data via internal data endpoint
|
||||||
ident = current_cart_identity()
|
ident = current_cart_identity()
|
||||||
summary = await services.cart.cart_summary(
|
summary_params = {}
|
||||||
g.s, user_id=ident["user_id"], session_id=ident["session_id"],
|
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 + summary.ticket_count
|
ctx["cart_count"] = summary.count + summary.calendar_count + summary.ticket_count
|
||||||
ctx["cart_total"] = float(summary.total + summary.calendar_total + summary.ticket_total)
|
ctx["cart_total"] = float(summary.total + summary.calendar_total + summary.ticket_total)
|
||||||
|
|
||||||
@@ -58,7 +63,6 @@ async def events_context() -> dict:
|
|||||||
|
|
||||||
|
|
||||||
def create_app() -> "Quart":
|
def create_app() -> "Quart":
|
||||||
from shared.services.registry import services
|
|
||||||
from services import register_domain_services
|
from services import register_domain_services
|
||||||
|
|
||||||
app = create_base_app(
|
app = create_base_app(
|
||||||
@@ -105,6 +109,8 @@ def create_app() -> "Quart":
|
|||||||
)
|
)
|
||||||
|
|
||||||
app.register_blueprint(register_fragments())
|
app.register_blueprint(register_fragments())
|
||||||
|
app.register_blueprint(register_actions())
|
||||||
|
app.register_blueprint(register_data())
|
||||||
|
|
||||||
# --- Auto-inject slug into url_for() calls ---
|
# --- Auto-inject slug into url_for() calls ---
|
||||||
@app.url_value_preprocessor
|
@app.url_value_preprocessor
|
||||||
@@ -122,31 +128,39 @@ def create_app() -> "Quart":
|
|||||||
# --- Load post data for slug ---
|
# --- Load post data for slug ---
|
||||||
@app.before_request
|
@app.before_request
|
||||||
async def hydrate_post():
|
async def hydrate_post():
|
||||||
|
from shared.infrastructure.data_client import fetch_data
|
||||||
slug = getattr(g, "post_slug", None)
|
slug = getattr(g, "post_slug", None)
|
||||||
if not slug:
|
if not slug:
|
||||||
return
|
return
|
||||||
post = await services.blog.get_post_by_slug(g.s, slug)
|
post = await fetch_data("blog", "post-by-slug", params={"slug": slug})
|
||||||
if not post:
|
if not post:
|
||||||
abort(404)
|
abort(404)
|
||||||
g.post_data = {
|
g.post_data = {
|
||||||
"post": {
|
"post": {
|
||||||
"id": post.id,
|
"id": post["id"],
|
||||||
"title": post.title,
|
"title": post["title"],
|
||||||
"slug": post.slug,
|
"slug": post["slug"],
|
||||||
"feature_image": post.feature_image,
|
"feature_image": post.get("feature_image"),
|
||||||
"status": post.status,
|
"status": post["status"],
|
||||||
"visibility": post.visibility,
|
"visibility": post["visibility"],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@app.context_processor
|
@app.context_processor
|
||||||
async def inject_post():
|
async def inject_post():
|
||||||
|
from shared.infrastructure.data_client import fetch_data
|
||||||
|
from shared.contracts.dtos import CalendarDTO, MarketPlaceDTO, dto_from_dict
|
||||||
|
from shared.services.registry import services
|
||||||
post_data = getattr(g, "post_data", None)
|
post_data = getattr(g, "post_data", None)
|
||||||
if not post_data:
|
if not post_data:
|
||||||
return {}
|
return {}
|
||||||
post_id = post_data["post"]["id"]
|
post_id = post_data["post"]["id"]
|
||||||
|
# Calendar data is local (events owns it)
|
||||||
calendars = await services.calendar.calendars_for_container(g.s, "page", post_id)
|
calendars = await services.calendar.calendars_for_container(g.s, "page", post_id)
|
||||||
markets = await services.market.marketplaces_for_container(g.s, "page", post_id)
|
# Market data is cross-app
|
||||||
|
raw_markets = await fetch_data("market", "marketplaces-for-container",
|
||||||
|
params={"type": "page", "id": post_id}, required=False) or []
|
||||||
|
markets = [dto_from_dict(MarketPlaceDTO, m) for m in raw_markets]
|
||||||
return {
|
return {
|
||||||
**post_data,
|
**post_data,
|
||||||
"calendars": calendars,
|
"calendars": calendars,
|
||||||
@@ -168,6 +182,7 @@ def create_app() -> "Quart":
|
|||||||
from quart import jsonify
|
from quart import jsonify
|
||||||
from shared.infrastructure.urls import events_url
|
from shared.infrastructure.urls import events_url
|
||||||
from shared.infrastructure.oembed import build_oembed_response
|
from shared.infrastructure.oembed import build_oembed_response
|
||||||
|
from shared.infrastructure.data_client import fetch_data
|
||||||
|
|
||||||
url = request.args.get("url", "")
|
url = request.args.get("url", "")
|
||||||
if not url:
|
if not url:
|
||||||
@@ -178,15 +193,15 @@ def create_app() -> "Quart":
|
|||||||
if not slug:
|
if not slug:
|
||||||
return jsonify({"error": "could not extract slug"}), 404
|
return jsonify({"error": "could not extract slug"}), 404
|
||||||
|
|
||||||
post = await services.blog.get_post_by_slug(g.s, slug)
|
post = await fetch_data("blog", "post-by-slug", params={"slug": slug})
|
||||||
if not post:
|
if not post:
|
||||||
return jsonify({"error": "not found"}), 404
|
return jsonify({"error": "not found"}), 404
|
||||||
|
|
||||||
resp = build_oembed_response(
|
resp = build_oembed_response(
|
||||||
title=post.title,
|
title=post["title"],
|
||||||
oembed_type="link",
|
oembed_type="link",
|
||||||
thumbnail_url=post.feature_image,
|
thumbnail_url=post.get("feature_image"),
|
||||||
url=events_url(f"/{post.slug}"),
|
url=events_url(f"/{post['slug']}"),
|
||||||
)
|
)
|
||||||
return jsonify(resp)
|
return jsonify(resp)
|
||||||
|
|
||||||
|
|||||||
@@ -4,3 +4,5 @@ from .markets.routes import register as register_markets
|
|||||||
from .payments.routes import register as register_payments
|
from .payments.routes import register as register_payments
|
||||||
from .page.routes import register as register_page
|
from .page.routes import register as register_page
|
||||||
from .fragments import register_fragments
|
from .fragments import register_fragments
|
||||||
|
from .actions import register_actions
|
||||||
|
from .data import register_data
|
||||||
|
|||||||
1
events/bp/actions/__init__.py
Normal file
1
events/bp/actions/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .routes import register as register_actions
|
||||||
131
events/bp/actions/routes.py
Normal file
131
events/bp/actions/routes.py
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
"""Events app action endpoints.
|
||||||
|
|
||||||
|
Exposes write operations at ``/internal/actions/<action_name>`` for
|
||||||
|
cross-app callers (cart, blog) 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)
|
||||||
|
|
||||||
|
# --- adjust-ticket-quantity ---
|
||||||
|
async def _adjust_ticket_quantity():
|
||||||
|
data = await request.get_json()
|
||||||
|
await services.calendar.adjust_ticket_quantity(
|
||||||
|
g.s,
|
||||||
|
data["entry_id"],
|
||||||
|
data["count"],
|
||||||
|
user_id=data.get("user_id"),
|
||||||
|
session_id=data.get("session_id"),
|
||||||
|
ticket_type_id=data.get("ticket_type_id"),
|
||||||
|
)
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
_handlers["adjust-ticket-quantity"] = _adjust_ticket_quantity
|
||||||
|
|
||||||
|
# --- claim-entries-for-order ---
|
||||||
|
async def _claim_entries():
|
||||||
|
data = await request.get_json()
|
||||||
|
await services.calendar.claim_entries_for_order(
|
||||||
|
g.s,
|
||||||
|
data["order_id"],
|
||||||
|
data.get("user_id"),
|
||||||
|
data.get("session_id"),
|
||||||
|
data.get("page_post_id"),
|
||||||
|
)
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
_handlers["claim-entries-for-order"] = _claim_entries
|
||||||
|
|
||||||
|
# --- claim-tickets-for-order ---
|
||||||
|
async def _claim_tickets():
|
||||||
|
data = await request.get_json()
|
||||||
|
await services.calendar.claim_tickets_for_order(
|
||||||
|
g.s,
|
||||||
|
data["order_id"],
|
||||||
|
data.get("user_id"),
|
||||||
|
data.get("session_id"),
|
||||||
|
data.get("page_post_id"),
|
||||||
|
)
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
_handlers["claim-tickets-for-order"] = _claim_tickets
|
||||||
|
|
||||||
|
# --- confirm-entries-for-order ---
|
||||||
|
async def _confirm_entries():
|
||||||
|
data = await request.get_json()
|
||||||
|
await services.calendar.confirm_entries_for_order(
|
||||||
|
g.s,
|
||||||
|
data["order_id"],
|
||||||
|
data.get("user_id"),
|
||||||
|
data.get("session_id"),
|
||||||
|
)
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
_handlers["confirm-entries-for-order"] = _confirm_entries
|
||||||
|
|
||||||
|
# --- confirm-tickets-for-order ---
|
||||||
|
async def _confirm_tickets():
|
||||||
|
data = await request.get_json()
|
||||||
|
await services.calendar.confirm_tickets_for_order(
|
||||||
|
g.s, data["order_id"],
|
||||||
|
)
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
_handlers["confirm-tickets-for-order"] = _confirm_tickets
|
||||||
|
|
||||||
|
# --- toggle-entry-post ---
|
||||||
|
async def _toggle_entry_post():
|
||||||
|
data = await request.get_json()
|
||||||
|
is_associated = await services.calendar.toggle_entry_post(
|
||||||
|
g.s,
|
||||||
|
data["entry_id"],
|
||||||
|
data["content_type"],
|
||||||
|
data["content_id"],
|
||||||
|
)
|
||||||
|
return {"is_associated": is_associated}
|
||||||
|
|
||||||
|
_handlers["toggle-entry-post"] = _toggle_entry_post
|
||||||
|
|
||||||
|
# --- adopt-entries-for-user ---
|
||||||
|
async def _adopt_entries():
|
||||||
|
data = await request.get_json()
|
||||||
|
await services.calendar.adopt_entries_for_user(
|
||||||
|
g.s, data["user_id"], data["session_id"],
|
||||||
|
)
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
_handlers["adopt-entries-for-user"] = _adopt_entries
|
||||||
|
|
||||||
|
# --- adopt-tickets-for-user ---
|
||||||
|
async def _adopt_tickets():
|
||||||
|
data = await request.get_json()
|
||||||
|
await services.calendar.adopt_tickets_for_user(
|
||||||
|
g.s, data["user_id"], data["session_id"],
|
||||||
|
)
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
_handlers["adopt-tickets-for-user"] = _adopt_tickets
|
||||||
|
|
||||||
|
return bp
|
||||||
@@ -15,6 +15,8 @@ from quart import Blueprint, g, request, render_template, render_template_string
|
|||||||
|
|
||||||
from shared.browser.app.utils.htmx import is_htmx_request
|
from shared.browser.app.utils.htmx import is_htmx_request
|
||||||
from shared.infrastructure.cart_identity import current_cart_identity
|
from shared.infrastructure.cart_identity import current_cart_identity
|
||||||
|
from shared.infrastructure.data_client import fetch_data
|
||||||
|
from shared.contracts.dtos import CartSummaryDTO, PostDTO, dto_from_dict
|
||||||
from shared.services.registry import services
|
from shared.services.registry import services
|
||||||
|
|
||||||
|
|
||||||
@@ -47,8 +49,11 @@ def register() -> Blueprint:
|
|||||||
if e.calendar_container_type == "page" and e.calendar_container_id
|
if e.calendar_container_type == "page" and e.calendar_container_id
|
||||||
})
|
})
|
||||||
if post_ids:
|
if post_ids:
|
||||||
posts = await services.blog.get_posts_by_ids(g.s, post_ids)
|
raw_posts = await fetch_data("blog", "posts-by-ids",
|
||||||
for p in posts:
|
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}
|
page_info[p.id] = {"title": p.title, "slug": p.slug}
|
||||||
|
|
||||||
return entries, has_more, pending_tickets, page_info
|
return entries, has_more, pending_tickets, page_info
|
||||||
@@ -121,9 +126,13 @@ def register() -> Blueprint:
|
|||||||
entry = await services.calendar.entry_by_id(g.s, entry_id)
|
entry = await services.calendar.entry_by_id(g.s, entry_id)
|
||||||
|
|
||||||
# Updated cart count for OOB mini-cart
|
# Updated cart count for OOB mini-cart
|
||||||
summary = await services.cart.cart_summary(
|
summary_params = {}
|
||||||
g.s, user_id=ident["user_id"], session_id=ident["session_id"],
|
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_summary = await fetch_data("cart", "cart-summary", params=summary_params, required=False)
|
||||||
|
summary = dto_from_dict(CartSummaryDTO, raw_summary) if raw_summary else CartSummaryDTO()
|
||||||
cart_count = summary.count + summary.calendar_count + summary.ticket_count
|
cart_count = summary.count + summary.calendar_count + summary.ticket_count
|
||||||
|
|
||||||
# Render widget + OOB cart-mini
|
# Render widget + OOB cart-mini
|
||||||
|
|||||||
@@ -219,13 +219,18 @@ def register():
|
|||||||
select(sa_func.count()).select_from(CalendarEntry).where(*cal_filters)
|
select(sa_func.count()).select_from(CalendarEntry).where(*cal_filters)
|
||||||
) or 0
|
) or 0
|
||||||
|
|
||||||
# Get product cart count via service (same DB, no HTTP needed)
|
# Get product cart count via HTTP
|
||||||
from shared.infrastructure.cart_identity import current_cart_identity
|
from shared.infrastructure.cart_identity import current_cart_identity
|
||||||
from shared.services.registry import services
|
from shared.infrastructure.data_client import fetch_data
|
||||||
|
from shared.contracts.dtos import CartSummaryDTO, dto_from_dict
|
||||||
ident = current_cart_identity()
|
ident = current_cart_identity()
|
||||||
cart_summary = await services.cart.cart_summary(
|
summary_params = {}
|
||||||
g.s, user_id=ident["user_id"], session_id=ident["session_id"],
|
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_summary = await fetch_data("cart", "cart-summary", params=summary_params, required=False)
|
||||||
|
cart_summary = dto_from_dict(CartSummaryDTO, raw_summary) if raw_summary else CartSummaryDTO()
|
||||||
product_count = cart_summary.count
|
product_count = cart_summary.count
|
||||||
total_count = product_count + cal_count
|
total_count = product_count + cal_count
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ from sqlalchemy import select
|
|||||||
from sqlalchemy.sql import func
|
from sqlalchemy.sql import func
|
||||||
|
|
||||||
from models.calendars import CalendarEntry, CalendarEntryPost
|
from models.calendars import CalendarEntry, CalendarEntryPost
|
||||||
from shared.services.registry import services
|
from shared.infrastructure.data_client import fetch_data
|
||||||
|
from shared.contracts.dtos import PostDTO, dto_from_dict
|
||||||
|
|
||||||
|
|
||||||
async def add_post_to_entry(
|
async def add_post_to_entry(
|
||||||
@@ -28,8 +29,8 @@ async def add_post_to_entry(
|
|||||||
return False, "Calendar entry not found"
|
return False, "Calendar entry not found"
|
||||||
|
|
||||||
# Check if post exists
|
# Check if post exists
|
||||||
post = await services.blog.get_post_by_id(session, post_id)
|
raw = await fetch_data("blog", "post-by-id", params={"id": post_id}, required=False)
|
||||||
if not post:
|
if not raw:
|
||||||
return False, "Post not found"
|
return False, "Post not found"
|
||||||
|
|
||||||
# Check if association already exists
|
# Check if association already exists
|
||||||
@@ -103,7 +104,10 @@ async def get_entry_posts(
|
|||||||
post_ids = list(result.scalars().all())
|
post_ids = list(result.scalars().all())
|
||||||
if not post_ids:
|
if not post_ids:
|
||||||
return []
|
return []
|
||||||
posts = await services.blog.get_posts_by_ids(session, post_ids)
|
raw_posts = await fetch_data("blog", "posts-by-ids",
|
||||||
|
params={"ids": ",".join(str(i) for i in post_ids)},
|
||||||
|
required=False) or []
|
||||||
|
posts = [dto_from_dict(PostDTO, p) for p in raw_posts]
|
||||||
return sorted(posts, key=lambda p: (p.title or ""))
|
return sorted(posts, key=lambda p: (p.title or ""))
|
||||||
|
|
||||||
|
|
||||||
@@ -118,4 +122,8 @@ async def search_posts(
|
|||||||
If query is empty, returns all posts in published order.
|
If query is empty, returns all posts in published order.
|
||||||
Returns (post_dtos, total_count).
|
Returns (post_dtos, total_count).
|
||||||
"""
|
"""
|
||||||
return await services.blog.search_posts(session, query, page, per_page)
|
raw = await fetch_data("blog", "search-posts",
|
||||||
|
params={"query": query, "page": page, "per_page": per_page},
|
||||||
|
required=False) or {"posts": [], "total": 0}
|
||||||
|
posts = [dto_from_dict(PostDTO, p) for p in raw.get("posts", [])]
|
||||||
|
return posts, raw.get("total", 0)
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ from sqlalchemy import select
|
|||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from models.calendars import Calendar
|
from models.calendars import Calendar
|
||||||
from shared.services.registry import services
|
from shared.infrastructure.data_client import fetch_data
|
||||||
|
from shared.contracts.dtos import PostDTO, dto_from_dict
|
||||||
from shared.services.relationships import attach_child, detach_child
|
from shared.services.relationships import attach_child, detach_child
|
||||||
import unicodedata
|
import unicodedata
|
||||||
import re
|
import re
|
||||||
@@ -49,7 +50,8 @@ def slugify(value: str, max_len: int = 255) -> str:
|
|||||||
|
|
||||||
|
|
||||||
async def soft_delete(sess: AsyncSession, post_slug: str, calendar_slug: str) -> bool:
|
async def soft_delete(sess: AsyncSession, post_slug: str, calendar_slug: str) -> bool:
|
||||||
post = await services.blog.get_post_by_slug(sess, post_slug)
|
raw = await fetch_data("blog", "post-by-slug", params={"slug": post_slug}, required=False)
|
||||||
|
post = dto_from_dict(PostDTO, raw) if raw else None
|
||||||
if not post:
|
if not post:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -84,7 +86,8 @@ async def create_calendar(sess: AsyncSession, post_id: int, name: str) -> Calend
|
|||||||
slug=slugify(name)
|
slug=slugify(name)
|
||||||
|
|
||||||
# Ensure post exists (avoid silent FK errors in some DBs)
|
# Ensure post exists (avoid silent FK errors in some DBs)
|
||||||
post = await services.blog.get_post_by_id(sess, post_id)
|
raw = await fetch_data("blog", "post-by-id", params={"id": post_id}, required=False)
|
||||||
|
post = dto_from_dict(PostDTO, raw) if raw else None
|
||||||
if not post:
|
if not post:
|
||||||
raise CalendarError(f"Post {post_id} does not exist.")
|
raise CalendarError(f"Post {post_id} does not exist.")
|
||||||
|
|
||||||
|
|||||||
1
events/bp/data/__init__.py
Normal file
1
events/bp/data/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .routes import register as register_data
|
||||||
144
events/bp/data/routes.py
Normal file
144
events/bp/data/routes.py
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
"""Events 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)
|
||||||
|
|
||||||
|
# --- pending-entries ---
|
||||||
|
async def _pending_entries():
|
||||||
|
user_id = request.args.get("user_id", type=int)
|
||||||
|
session_id = request.args.get("session_id")
|
||||||
|
entries = await services.calendar.pending_entries(
|
||||||
|
g.s, user_id=user_id, session_id=session_id,
|
||||||
|
)
|
||||||
|
return [dto_to_dict(e) for e in entries]
|
||||||
|
|
||||||
|
_handlers["pending-entries"] = _pending_entries
|
||||||
|
|
||||||
|
# --- pending-tickets ---
|
||||||
|
async def _pending_tickets():
|
||||||
|
user_id = request.args.get("user_id", type=int)
|
||||||
|
session_id = request.args.get("session_id")
|
||||||
|
tickets = await services.calendar.pending_tickets(
|
||||||
|
g.s, user_id=user_id, session_id=session_id,
|
||||||
|
)
|
||||||
|
return [dto_to_dict(t) for t in tickets]
|
||||||
|
|
||||||
|
_handlers["pending-tickets"] = _pending_tickets
|
||||||
|
|
||||||
|
# --- entries-for-page ---
|
||||||
|
async def _entries_for_page():
|
||||||
|
page_id = request.args.get("page_id", type=int)
|
||||||
|
user_id = request.args.get("user_id", type=int)
|
||||||
|
session_id = request.args.get("session_id")
|
||||||
|
entries = await services.calendar.entries_for_page(
|
||||||
|
g.s, page_id, user_id=user_id, session_id=session_id,
|
||||||
|
)
|
||||||
|
return [dto_to_dict(e) for e in entries]
|
||||||
|
|
||||||
|
_handlers["entries-for-page"] = _entries_for_page
|
||||||
|
|
||||||
|
# --- tickets-for-page ---
|
||||||
|
async def _tickets_for_page():
|
||||||
|
page_id = request.args.get("page_id", type=int)
|
||||||
|
user_id = request.args.get("user_id", type=int)
|
||||||
|
session_id = request.args.get("session_id")
|
||||||
|
tickets = await services.calendar.tickets_for_page(
|
||||||
|
g.s, page_id, user_id=user_id, session_id=session_id,
|
||||||
|
)
|
||||||
|
return [dto_to_dict(t) for t in tickets]
|
||||||
|
|
||||||
|
_handlers["tickets-for-page"] = _tickets_for_page
|
||||||
|
|
||||||
|
# --- entries-for-order ---
|
||||||
|
async def _entries_for_order():
|
||||||
|
order_id = request.args.get("order_id", type=int)
|
||||||
|
entries = await services.calendar.get_entries_for_order(g.s, order_id)
|
||||||
|
return [dto_to_dict(e) for e in entries]
|
||||||
|
|
||||||
|
_handlers["entries-for-order"] = _entries_for_order
|
||||||
|
|
||||||
|
# --- tickets-for-order ---
|
||||||
|
async def _tickets_for_order():
|
||||||
|
order_id = request.args.get("order_id", type=int)
|
||||||
|
tickets = await services.calendar.get_tickets_for_order(g.s, order_id)
|
||||||
|
return [dto_to_dict(t) for t in tickets]
|
||||||
|
|
||||||
|
_handlers["tickets-for-order"] = _tickets_for_order
|
||||||
|
|
||||||
|
# --- entry-ids-for-content ---
|
||||||
|
async def _entry_ids_for_content():
|
||||||
|
content_type = request.args.get("content_type", "")
|
||||||
|
content_id = request.args.get("content_id", type=int)
|
||||||
|
ids = await services.calendar.entry_ids_for_content(g.s, content_type, content_id)
|
||||||
|
return list(ids)
|
||||||
|
|
||||||
|
_handlers["entry-ids-for-content"] = _entry_ids_for_content
|
||||||
|
|
||||||
|
# --- associated-entries ---
|
||||||
|
async def _associated_entries():
|
||||||
|
content_type = request.args.get("content_type", "")
|
||||||
|
content_id = request.args.get("content_id", type=int)
|
||||||
|
page = request.args.get("page", 1, type=int)
|
||||||
|
entries, has_more = await services.calendar.associated_entries(
|
||||||
|
g.s, content_type, content_id, page,
|
||||||
|
)
|
||||||
|
return {"entries": [dto_to_dict(e) for e in entries], "has_more": has_more}
|
||||||
|
|
||||||
|
_handlers["associated-entries"] = _associated_entries
|
||||||
|
|
||||||
|
# --- calendars-for-container ---
|
||||||
|
async def _calendars_for_container():
|
||||||
|
container_type = request.args.get("type", "")
|
||||||
|
container_id = request.args.get("id", type=int)
|
||||||
|
calendars = await services.calendar.calendars_for_container(
|
||||||
|
g.s, container_type, container_id,
|
||||||
|
)
|
||||||
|
return [dto_to_dict(c) for c in calendars]
|
||||||
|
|
||||||
|
_handlers["calendars-for-container"] = _calendars_for_container
|
||||||
|
|
||||||
|
# --- visible-entries-for-period ---
|
||||||
|
async def _visible_entries_for_period():
|
||||||
|
from datetime import datetime
|
||||||
|
calendar_id = request.args.get("calendar_id", type=int)
|
||||||
|
period_start = datetime.fromisoformat(request.args.get("period_start", ""))
|
||||||
|
period_end = datetime.fromisoformat(request.args.get("period_end", ""))
|
||||||
|
user_id = request.args.get("user_id", type=int)
|
||||||
|
is_admin = request.args.get("is_admin", "false").lower() == "true"
|
||||||
|
session_id = request.args.get("session_id")
|
||||||
|
entries = await services.calendar.visible_entries_for_period(
|
||||||
|
g.s, calendar_id, period_start, period_end,
|
||||||
|
user_id=user_id, is_admin=is_admin, session_id=session_id,
|
||||||
|
)
|
||||||
|
return [dto_to_dict(e) for e in entries]
|
||||||
|
|
||||||
|
_handlers["visible-entries-for-period"] = _visible_entries_for_period
|
||||||
|
|
||||||
|
return bp
|
||||||
@@ -9,6 +9,8 @@ from __future__ import annotations
|
|||||||
from quart import Blueprint, Response, g, render_template, request
|
from quart import Blueprint, Response, g, render_template, request
|
||||||
|
|
||||||
from shared.infrastructure.fragments import FRAGMENT_HEADER
|
from shared.infrastructure.fragments import FRAGMENT_HEADER
|
||||||
|
from shared.infrastructure.data_client import fetch_data
|
||||||
|
from shared.contracts.dtos import PostDTO, dto_from_dict
|
||||||
from shared.services.registry import services
|
from shared.services.registry import services
|
||||||
|
|
||||||
|
|
||||||
@@ -139,7 +141,8 @@ def register():
|
|||||||
parts = []
|
parts = []
|
||||||
for s in slugs:
|
for s in slugs:
|
||||||
parts.append(f"<!-- fragment:{s} -->")
|
parts.append(f"<!-- fragment:{s} -->")
|
||||||
post = await services.blog.get_post_by_slug(g.s, s)
|
raw = await fetch_data("blog", "post-by-slug", params={"slug": s}, required=False)
|
||||||
|
post = dto_from_dict(PostDTO, raw) if raw else None
|
||||||
if post:
|
if post:
|
||||||
calendars = await services.calendar.calendars_for_container(
|
calendars = await services.calendar.calendars_for_container(
|
||||||
g.s, "page", post.id,
|
g.s, "page", post.id,
|
||||||
@@ -157,7 +160,8 @@ def register():
|
|||||||
# Single mode
|
# Single mode
|
||||||
if not slug:
|
if not slug:
|
||||||
return ""
|
return ""
|
||||||
post = await services.blog.get_post_by_slug(g.s, slug)
|
raw = await fetch_data("blog", "post-by-slug", params={"slug": slug}, required=False)
|
||||||
|
post = dto_from_dict(PostDTO, raw) if raw else None
|
||||||
if not post:
|
if not post:
|
||||||
return ""
|
return ""
|
||||||
calendars = await services.calendar.calendars_for_container(
|
calendars = await services.calendar.calendars_for_container(
|
||||||
|
|||||||
@@ -5,8 +5,9 @@ import unicodedata
|
|||||||
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from shared.contracts.dtos import MarketPlaceDTO
|
from shared.contracts.dtos import MarketPlaceDTO, PostDTO, dto_from_dict
|
||||||
from shared.services.registry import services
|
from shared.infrastructure.actions import call_action, ActionError
|
||||||
|
from shared.infrastructure.data_client import fetch_data
|
||||||
|
|
||||||
|
|
||||||
class MarketError(ValueError):
|
class MarketError(ValueError):
|
||||||
@@ -37,21 +38,33 @@ async def create_market(sess: AsyncSession, post_id: int, name: str) -> MarketPl
|
|||||||
raise MarketError("Market name must not be empty.")
|
raise MarketError("Market name must not be empty.")
|
||||||
slug = slugify(name)
|
slug = slugify(name)
|
||||||
|
|
||||||
post = await services.blog.get_post_by_id(sess, post_id)
|
raw = await fetch_data("blog", "post-by-id", params={"id": post_id}, required=False)
|
||||||
|
post = dto_from_dict(PostDTO, raw) if raw else None
|
||||||
if not post:
|
if not post:
|
||||||
raise MarketError(f"Post {post_id} does not exist.")
|
raise MarketError(f"Post {post_id} does not exist.")
|
||||||
if not post.is_page:
|
if not post.is_page:
|
||||||
raise MarketError("Markets can only be created on pages, not posts.")
|
raise MarketError("Markets can only be created on pages, not posts.")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return await services.market.create_marketplace(sess, "page", post_id, name, slug)
|
result = await call_action("market", "create-marketplace", payload={
|
||||||
except ValueError as e:
|
"container_type": "page", "container_id": post_id,
|
||||||
|
"name": name, "slug": slug,
|
||||||
|
})
|
||||||
|
return MarketPlaceDTO(**result)
|
||||||
|
except ActionError as e:
|
||||||
raise MarketError(str(e)) from e
|
raise MarketError(str(e)) from e
|
||||||
|
|
||||||
|
|
||||||
async def soft_delete(sess: AsyncSession, post_slug: str, market_slug: str) -> bool:
|
async def soft_delete(sess: AsyncSession, post_slug: str, market_slug: str) -> bool:
|
||||||
post = await services.blog.get_post_by_slug(sess, post_slug)
|
raw = await fetch_data("blog", "post-by-slug", params={"slug": post_slug}, required=False)
|
||||||
|
post = dto_from_dict(PostDTO, raw) if raw else None
|
||||||
if not post:
|
if not post:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return await services.market.soft_delete_marketplace(sess, "page", post.id, market_slug)
|
try:
|
||||||
|
result = await call_action("market", "soft-delete-marketplace", payload={
|
||||||
|
"container_type": "page", "container_id": post.id, "slug": market_slug,
|
||||||
|
})
|
||||||
|
return result.get("deleted", False)
|
||||||
|
except ActionError:
|
||||||
|
return False
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ from quart import Blueprint, g, request, render_template, render_template_string
|
|||||||
|
|
||||||
from shared.browser.app.utils.htmx import is_htmx_request
|
from shared.browser.app.utils.htmx import is_htmx_request
|
||||||
from shared.infrastructure.cart_identity import current_cart_identity
|
from shared.infrastructure.cart_identity import current_cart_identity
|
||||||
|
from shared.infrastructure.data_client import fetch_data
|
||||||
|
from shared.contracts.dtos import CartSummaryDTO, dto_from_dict
|
||||||
from shared.services.registry import services
|
from shared.services.registry import services
|
||||||
|
|
||||||
|
|
||||||
@@ -107,9 +109,13 @@ def register() -> Blueprint:
|
|||||||
entry = await services.calendar.entry_by_id(g.s, entry_id)
|
entry = await services.calendar.entry_by_id(g.s, entry_id)
|
||||||
|
|
||||||
# Updated cart count for OOB mini-cart
|
# Updated cart count for OOB mini-cart
|
||||||
summary = await services.cart.cart_summary(
|
summary_params = {}
|
||||||
g.s, user_id=ident["user_id"], session_id=ident["session_id"],
|
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_summary = await fetch_data("cart", "cart-summary", params=summary_params, required=False)
|
||||||
|
summary = dto_from_dict(CartSummaryDTO, raw_summary) if raw_summary else CartSummaryDTO()
|
||||||
cart_count = summary.count + summary.calendar_count + summary.ticket_count
|
cart_count = summary.count + summary.calendar_count + summary.ticket_count
|
||||||
|
|
||||||
# Render widget + OOB cart-mini
|
# Render widget + OOB cart-mini
|
||||||
|
|||||||
@@ -287,10 +287,15 @@ def register() -> Blueprint:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Compute cart count for OOB mini-cart update
|
# Compute cart count for OOB mini-cart update
|
||||||
from shared.services.registry import services
|
from shared.infrastructure.data_client import fetch_data
|
||||||
summary = await services.cart.cart_summary(
|
from shared.contracts.dtos import CartSummaryDTO, dto_from_dict
|
||||||
g.s, user_id=ident["user_id"], session_id=ident["session_id"],
|
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_summary = await fetch_data("cart", "cart-summary", params=summary_params, required=False)
|
||||||
|
summary = dto_from_dict(CartSummaryDTO, raw_summary) if raw_summary else CartSummaryDTO()
|
||||||
cart_count = summary.count + summary.calendar_count + summary.ticket_count
|
cart_count = summary.count + summary.calendar_count + summary.ticket_count
|
||||||
|
|
||||||
html = await render_template(
|
html = await render_template(
|
||||||
|
|||||||
@@ -7,23 +7,14 @@ def register_domain_services() -> None:
|
|||||||
|
|
||||||
Events owns: Calendar, CalendarEntry, CalendarSlot, TicketType,
|
Events owns: Calendar, CalendarEntry, CalendarSlot, TicketType,
|
||||||
Ticket, CalendarEntryPost.
|
Ticket, CalendarEntryPost.
|
||||||
Standard deployment registers all 4 services as real DB impls
|
Cross-app calls go over HTTP via call_action() / fetch_data().
|
||||||
(shared DB). For composable deployments, swap non-owned services
|
|
||||||
with stubs from shared.services.stubs.
|
|
||||||
"""
|
"""
|
||||||
from shared.services.registry import services
|
from shared.services.registry import services
|
||||||
from shared.services.blog_impl import SqlBlogService
|
|
||||||
from shared.services.calendar_impl import SqlCalendarService
|
from shared.services.calendar_impl import SqlCalendarService
|
||||||
from shared.services.market_impl import SqlMarketService
|
|
||||||
from shared.services.cart_impl import SqlCartService
|
|
||||||
|
|
||||||
services.calendar = SqlCalendarService()
|
services.calendar = SqlCalendarService()
|
||||||
if not services.has("blog"):
|
|
||||||
services.blog = SqlBlogService()
|
# Federation needed for AP shared infrastructure (activitypub blueprint)
|
||||||
if not services.has("market"):
|
|
||||||
services.market = SqlMarketService()
|
|
||||||
if not services.has("cart"):
|
|
||||||
services.cart = SqlCartService()
|
|
||||||
if not services.has("federation"):
|
if not services.has("federation"):
|
||||||
from shared.services.federation_impl import SqlFederationService
|
from shared.services.federation_impl import SqlFederationService
|
||||||
services.federation = SqlFederationService()
|
services.federation = SqlFederationService()
|
||||||
|
|||||||
@@ -21,17 +21,23 @@ async def federation_context() -> dict:
|
|||||||
from shared.services.navigation import get_navigation_tree
|
from shared.services.navigation import get_navigation_tree
|
||||||
from shared.infrastructure.cart_identity import current_cart_identity
|
from shared.infrastructure.cart_identity import current_cart_identity
|
||||||
from shared.infrastructure.fragments import fetch_fragments
|
from shared.infrastructure.fragments import fetch_fragments
|
||||||
|
from shared.infrastructure.data_client import fetch_data
|
||||||
|
from shared.contracts.dtos import CartSummaryDTO, dto_from_dict
|
||||||
|
|
||||||
ctx = await base_context()
|
ctx = await base_context()
|
||||||
|
|
||||||
# Fallback for _nav.html when nav-tree fragment fetch fails
|
# Fallback for _nav.html when nav-tree fragment fetch fails
|
||||||
ctx["menu_items"] = await get_navigation_tree(g.s)
|
ctx["menu_items"] = await get_navigation_tree(g.s)
|
||||||
|
|
||||||
# Cart data (consistent with all other apps)
|
# Cart data via internal data endpoint
|
||||||
ident = current_cart_identity()
|
ident = current_cart_identity()
|
||||||
summary = await services.cart.cart_summary(
|
summary_params = {}
|
||||||
g.s, user_id=ident["user_id"], session_id=ident["session_id"],
|
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 + summary.ticket_count
|
ctx["cart_count"] = summary.count + summary.calendar_count + summary.ticket_count
|
||||||
ctx["cart_total"] = float(summary.total + summary.calendar_total + summary.ticket_total)
|
ctx["cart_total"] = float(summary.total + summary.calendar_total + summary.ticket_total)
|
||||||
|
|
||||||
|
|||||||
@@ -7,21 +7,9 @@ def register_domain_services() -> None:
|
|||||||
|
|
||||||
Federation owns: ActorProfile, APActivity, APFollower, APInboxItem,
|
Federation owns: ActorProfile, APActivity, APFollower, APInboxItem,
|
||||||
APAnchor, IPFSPin.
|
APAnchor, IPFSPin.
|
||||||
Standard deployment registers all services as real DB impls (shared DB).
|
Cross-app calls go over HTTP via call_action() / fetch_data().
|
||||||
"""
|
"""
|
||||||
from shared.services.registry import services
|
from shared.services.registry import services
|
||||||
from shared.services.federation_impl import SqlFederationService
|
from shared.services.federation_impl import SqlFederationService
|
||||||
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.federation = SqlFederationService()
|
services.federation = SqlFederationService()
|
||||||
if not services.has("blog"):
|
|
||||||
services.blog = SqlBlogService()
|
|
||||||
if not services.has("calendar"):
|
|
||||||
services.calendar = SqlCalendarService()
|
|
||||||
if not services.has("market"):
|
|
||||||
services.market = SqlMarketService()
|
|
||||||
if not services.has("cart"):
|
|
||||||
services.cart = SqlCartService()
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from sqlalchemy import select
|
|||||||
from shared.infrastructure.factory import create_base_app
|
from shared.infrastructure.factory import create_base_app
|
||||||
from shared.config import config
|
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:
|
async def market_context() -> dict:
|
||||||
@@ -23,9 +23,10 @@ async def market_context() -> dict:
|
|||||||
"""
|
"""
|
||||||
from shared.infrastructure.context import base_context
|
from shared.infrastructure.context import base_context
|
||||||
from shared.services.navigation import get_navigation_tree
|
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.cart_identity import current_cart_identity
|
||||||
from shared.infrastructure.fragments import fetch_fragments
|
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 shared.models.market import CartItem
|
||||||
from sqlalchemy.orm import selectinload
|
from sqlalchemy.orm import selectinload
|
||||||
|
|
||||||
@@ -36,10 +37,14 @@ async def market_context() -> dict:
|
|||||||
|
|
||||||
ident = current_cart_identity()
|
ident = current_cart_identity()
|
||||||
|
|
||||||
# cart_count/cart_total via service (consistent with blog/events apps)
|
# cart_count/cart_total via internal data endpoint
|
||||||
summary = await services.cart.cart_summary(
|
summary_params = {}
|
||||||
g.s, user_id=ident["user_id"], session_id=ident["session_id"],
|
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_count"] = summary.count + summary.calendar_count
|
||||||
ctx["cart_total"] = float(summary.total + summary.calendar_total)
|
ctx["cart_total"] = float(summary.total + summary.calendar_total)
|
||||||
|
|
||||||
@@ -80,7 +85,6 @@ async def market_context() -> dict:
|
|||||||
|
|
||||||
def create_app() -> "Quart":
|
def create_app() -> "Quart":
|
||||||
from models.market_place import MarketPlace
|
from models.market_place import MarketPlace
|
||||||
from shared.services.registry import services
|
|
||||||
from services import register_domain_services
|
from services import register_domain_services
|
||||||
|
|
||||||
app = create_base_app(
|
app = create_base_app(
|
||||||
@@ -118,6 +122,8 @@ def create_app() -> "Quart":
|
|||||||
)
|
)
|
||||||
|
|
||||||
app.register_blueprint(register_fragments())
|
app.register_blueprint(register_fragments())
|
||||||
|
app.register_blueprint(register_actions())
|
||||||
|
app.register_blueprint(register_data())
|
||||||
|
|
||||||
# --- Auto-inject slugs into url_for() calls ---
|
# --- Auto-inject slugs into url_for() calls ---
|
||||||
@app.url_value_preprocessor
|
@app.url_value_preprocessor
|
||||||
@@ -147,26 +153,27 @@ def create_app() -> "Quart":
|
|||||||
# --- Load post and market data ---
|
# --- Load post and market data ---
|
||||||
@app.before_request
|
@app.before_request
|
||||||
async def hydrate_market():
|
async def hydrate_market():
|
||||||
|
from shared.infrastructure.data_client import fetch_data
|
||||||
post_slug = getattr(g, "post_slug", None)
|
post_slug = getattr(g, "post_slug", None)
|
||||||
market_slug = getattr(g, "market_slug", None)
|
market_slug = getattr(g, "market_slug", None)
|
||||||
if not post_slug:
|
if not post_slug:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Load post by slug via blog service
|
# Load post by slug via blog data endpoint
|
||||||
post = await services.blog.get_post_by_slug(g.s, post_slug)
|
post = await fetch_data("blog", "post-by-slug", params={"slug": post_slug})
|
||||||
if not post:
|
if not post:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
g.post_data = {
|
g.post_data = {
|
||||||
"post": {
|
"post": {
|
||||||
"id": post.id,
|
"id": post["id"],
|
||||||
"title": post.title,
|
"title": post["title"],
|
||||||
"slug": post.slug,
|
"slug": post["slug"],
|
||||||
"feature_image": post.feature_image,
|
"feature_image": post.get("feature_image"),
|
||||||
"html": post.html,
|
"html": post.get("html"),
|
||||||
"status": post.status,
|
"status": post["status"],
|
||||||
"visibility": post.visibility,
|
"visibility": post["visibility"],
|
||||||
"is_page": post.is_page,
|
"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 .all_markets.routes import register as register_all_markets
|
||||||
from .page_markets.routes import register as register_page_markets
|
from .page_markets.routes import register as register_page_markets
|
||||||
from .fragments import register_fragments
|
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 quart import Blueprint, g, request, render_template, make_response
|
||||||
|
|
||||||
from shared.browser.app.utils.htmx import is_htmx_request
|
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
|
from shared.services.registry import services
|
||||||
|
|
||||||
|
|
||||||
@@ -32,8 +34,11 @@ def register() -> Blueprint:
|
|||||||
if m.container_type == "page"
|
if m.container_type == "page"
|
||||||
})
|
})
|
||||||
if post_ids:
|
if post_ids:
|
||||||
posts = await services.blog.get_posts_by_ids(g.s, post_ids)
|
raw_posts = await fetch_data("blog", "posts-by-ids",
|
||||||
for p in posts:
|
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}
|
page_info[p.id] = {"title": p.title, "slug": p.slug}
|
||||||
|
|
||||||
return markets, has_more, page_info
|
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,
|
Market owns: Product, CartItem, MarketPlace, NavTop, NavSub,
|
||||||
Listing, ProductImage.
|
Listing, ProductImage.
|
||||||
Standard deployment registers all 4 services as real DB impls
|
Cross-app calls go over HTTP via call_action() / fetch_data().
|
||||||
(shared DB). For composable deployments, swap non-owned services
|
|
||||||
with stubs from shared.services.stubs.
|
|
||||||
"""
|
"""
|
||||||
from shared.services.registry import services
|
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.market_impl import SqlMarketService
|
||||||
from shared.services.cart_impl import SqlCartService
|
|
||||||
|
|
||||||
services.market = SqlMarketService()
|
services.market = SqlMarketService()
|
||||||
if not services.has("blog"):
|
|
||||||
services.blog = SqlBlogService()
|
# Federation needed for AP shared infrastructure (activitypub blueprint)
|
||||||
if not services.has("calendar"):
|
|
||||||
services.calendar = SqlCalendarService()
|
|
||||||
if not services.has("cart"):
|
|
||||||
services.cart = SqlCartService()
|
|
||||||
if not services.has("federation"):
|
if not services.has("federation"):
|
||||||
from shared.services.federation_impl import SqlFederationService
|
from shared.services.federation_impl import SqlFederationService
|
||||||
services.federation = SqlFederationService()
|
services.federation = SqlFederationService()
|
||||||
|
|||||||
@@ -5,11 +5,74 @@ see ORM model instances from another domain — only these DTOs.
|
|||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import dataclasses
|
||||||
|
import typing
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Serialization helpers for JSON transport over internal data endpoints
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _serialize_value(v):
|
||||||
|
"""Convert a single value to a JSON-safe type."""
|
||||||
|
if isinstance(v, datetime):
|
||||||
|
return v.isoformat()
|
||||||
|
if isinstance(v, Decimal):
|
||||||
|
return str(v)
|
||||||
|
if isinstance(v, set):
|
||||||
|
return list(v)
|
||||||
|
if dataclasses.is_dataclass(v) and not isinstance(v, type):
|
||||||
|
return dto_to_dict(v)
|
||||||
|
if isinstance(v, list):
|
||||||
|
return [_serialize_value(item) for item in v]
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
def dto_to_dict(obj) -> dict:
|
||||||
|
"""Convert a frozen DTO dataclass to a JSON-serialisable dict."""
|
||||||
|
return {k: _serialize_value(v) for k, v in dataclasses.asdict(obj).items()}
|
||||||
|
|
||||||
|
|
||||||
|
def _unwrap_optional(hint):
|
||||||
|
"""Unwrap Optional[X] / X | None to return X."""
|
||||||
|
args = getattr(hint, "__args__", ())
|
||||||
|
if args:
|
||||||
|
real = [a for a in args if a is not type(None)]
|
||||||
|
if real:
|
||||||
|
return real[0]
|
||||||
|
return hint
|
||||||
|
|
||||||
|
|
||||||
|
def dto_from_dict(cls, data: dict):
|
||||||
|
"""Construct a DTO from a dict, coercing dates and Decimals.
|
||||||
|
|
||||||
|
Uses ``typing.get_type_hints()`` to resolve forward-ref annotations
|
||||||
|
(from ``from __future__ import annotations``).
|
||||||
|
"""
|
||||||
|
if not data:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
hints = typing.get_type_hints(cls)
|
||||||
|
except Exception:
|
||||||
|
hints = {}
|
||||||
|
kwargs = {}
|
||||||
|
for f in dataclasses.fields(cls):
|
||||||
|
if f.name not in data:
|
||||||
|
continue
|
||||||
|
val = data[f.name]
|
||||||
|
if val is not None and f.name in hints:
|
||||||
|
hint = _unwrap_optional(hints[f.name])
|
||||||
|
if hint is datetime and isinstance(val, str):
|
||||||
|
val = datetime.fromisoformat(val)
|
||||||
|
elif hint is Decimal:
|
||||||
|
val = Decimal(str(val))
|
||||||
|
kwargs[f.name] = val
|
||||||
|
return cls(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Blog domain
|
# Blog domain
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -1,23 +1,31 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from shared.events import register_activity_handler
|
from shared.events import register_activity_handler
|
||||||
|
from shared.infrastructure.actions import call_action, ActionError
|
||||||
from shared.models.federation import APActivity
|
from shared.models.federation import APActivity
|
||||||
from shared.services.registry import services
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
async def on_user_logged_in(activity: APActivity, session: AsyncSession) -> None:
|
async def on_user_logged_in(activity: APActivity, session: AsyncSession) -> None:
|
||||||
data = activity.object_data
|
data = activity.object_data
|
||||||
user_id = data["user_id"]
|
user_id = data["user_id"]
|
||||||
session_id = data["session_id"]
|
session_id = data["session_id"]
|
||||||
|
payload = {"user_id": user_id, "session_id": session_id}
|
||||||
|
|
||||||
if services.has("cart"):
|
for app, action in [
|
||||||
await services.cart.adopt_cart_for_user(session, user_id, session_id)
|
("cart", "adopt-cart-for-user"),
|
||||||
|
("events", "adopt-entries-for-user"),
|
||||||
if services.has("calendar"):
|
("events", "adopt-tickets-for-user"),
|
||||||
await services.calendar.adopt_entries_for_user(session, user_id, session_id)
|
]:
|
||||||
await services.calendar.adopt_tickets_for_user(session, user_id, session_id)
|
try:
|
||||||
|
await call_action(app, action, payload=payload)
|
||||||
|
except ActionError:
|
||||||
|
log.warning("Failed: %s/%s for user %s", app, action, user_id)
|
||||||
|
|
||||||
|
|
||||||
register_activity_handler("rose:Login", on_user_logged_in)
|
register_activity_handler("rose:Login", on_user_logged_in)
|
||||||
|
|||||||
89
shared/infrastructure/actions.py
Normal file
89
shared/infrastructure/actions.py
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
"""Internal action client for cross-app write operations.
|
||||||
|
|
||||||
|
Each coop app exposes JSON action endpoints at ``/internal/actions/{name}``.
|
||||||
|
This module provides helpers to call those endpoints so that callers don't
|
||||||
|
need direct access to another app's DB session or service layer.
|
||||||
|
|
||||||
|
Failures raise ``ActionError`` so callers can handle or propagate them.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Re-usable async client (created lazily, one per process)
|
||||||
|
_client: httpx.AsyncClient | None = None
|
||||||
|
|
||||||
|
# Default request timeout (seconds) — longer than fragments since these are writes
|
||||||
|
_DEFAULT_TIMEOUT = 5.0
|
||||||
|
|
||||||
|
# Header sent on every action request so providers can gate access.
|
||||||
|
ACTION_HEADER = "X-Internal-Action"
|
||||||
|
|
||||||
|
|
||||||
|
class ActionError(Exception):
|
||||||
|
"""Raised when an internal action call fails."""
|
||||||
|
|
||||||
|
def __init__(self, message: str, status_code: int = 500, detail: dict | None = None):
|
||||||
|
super().__init__(message)
|
||||||
|
self.status_code = status_code
|
||||||
|
self.detail = detail
|
||||||
|
|
||||||
|
|
||||||
|
def _get_client() -> httpx.AsyncClient:
|
||||||
|
global _client
|
||||||
|
if _client is None or _client.is_closed:
|
||||||
|
_client = httpx.AsyncClient(
|
||||||
|
timeout=httpx.Timeout(_DEFAULT_TIMEOUT),
|
||||||
|
follow_redirects=False,
|
||||||
|
)
|
||||||
|
return _client
|
||||||
|
|
||||||
|
|
||||||
|
def _internal_url(app_name: str) -> str:
|
||||||
|
"""Resolve the Docker-internal base URL for *app_name*."""
|
||||||
|
env_key = f"INTERNAL_URL_{app_name.upper()}"
|
||||||
|
return os.getenv(env_key, f"http://{app_name}:8000").rstrip("/")
|
||||||
|
|
||||||
|
|
||||||
|
async def call_action(
|
||||||
|
app_name: str,
|
||||||
|
action_name: str,
|
||||||
|
*,
|
||||||
|
payload: dict | None = None,
|
||||||
|
timeout: float = _DEFAULT_TIMEOUT,
|
||||||
|
) -> dict:
|
||||||
|
"""POST JSON to ``{INTERNAL_URL_APP}/internal/actions/{action_name}``.
|
||||||
|
|
||||||
|
Returns the parsed JSON response on 2xx.
|
||||||
|
Raises ``ActionError`` on network errors or non-2xx responses.
|
||||||
|
"""
|
||||||
|
base = _internal_url(app_name)
|
||||||
|
url = f"{base}/internal/actions/{action_name}"
|
||||||
|
try:
|
||||||
|
resp = await _get_client().post(
|
||||||
|
url,
|
||||||
|
json=payload or {},
|
||||||
|
headers={ACTION_HEADER: "1"},
|
||||||
|
timeout=timeout,
|
||||||
|
)
|
||||||
|
if 200 <= resp.status_code < 300:
|
||||||
|
return resp.json()
|
||||||
|
msg = f"Action {app_name}/{action_name} returned {resp.status_code}"
|
||||||
|
detail = None
|
||||||
|
try:
|
||||||
|
detail = resp.json()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
log.error(msg)
|
||||||
|
raise ActionError(msg, status_code=resp.status_code, detail=detail)
|
||||||
|
except ActionError:
|
||||||
|
raise
|
||||||
|
except Exception as exc:
|
||||||
|
msg = f"Action {app_name}/{action_name} failed: {exc}"
|
||||||
|
log.error(msg)
|
||||||
|
raise ActionError(msg) from exc
|
||||||
91
shared/infrastructure/data_client.py
Normal file
91
shared/infrastructure/data_client.py
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
"""Internal data client for cross-app read operations.
|
||||||
|
|
||||||
|
Each coop app exposes JSON data endpoints at ``/internal/data/{query}``.
|
||||||
|
This module provides helpers to fetch that data so that callers don't
|
||||||
|
need direct access to another app's DB session or service layer.
|
||||||
|
|
||||||
|
Same pattern as the fragment client but returns parsed JSON instead of HTML.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Re-usable async client (created lazily, one per process)
|
||||||
|
_client: httpx.AsyncClient | None = None
|
||||||
|
|
||||||
|
# Default request timeout (seconds)
|
||||||
|
_DEFAULT_TIMEOUT = 3.0
|
||||||
|
|
||||||
|
# Header sent on every data request so providers can gate access.
|
||||||
|
DATA_HEADER = "X-Internal-Data"
|
||||||
|
|
||||||
|
|
||||||
|
class DataError(Exception):
|
||||||
|
"""Raised when an internal data fetch fails."""
|
||||||
|
|
||||||
|
def __init__(self, message: str, status_code: int = 500):
|
||||||
|
super().__init__(message)
|
||||||
|
self.status_code = status_code
|
||||||
|
|
||||||
|
|
||||||
|
def _get_client() -> httpx.AsyncClient:
|
||||||
|
global _client
|
||||||
|
if _client is None or _client.is_closed:
|
||||||
|
_client = httpx.AsyncClient(
|
||||||
|
timeout=httpx.Timeout(_DEFAULT_TIMEOUT),
|
||||||
|
follow_redirects=False,
|
||||||
|
)
|
||||||
|
return _client
|
||||||
|
|
||||||
|
|
||||||
|
def _internal_url(app_name: str) -> str:
|
||||||
|
"""Resolve the Docker-internal base URL for *app_name*."""
|
||||||
|
env_key = f"INTERNAL_URL_{app_name.upper()}"
|
||||||
|
return os.getenv(env_key, f"http://{app_name}:8000").rstrip("/")
|
||||||
|
|
||||||
|
|
||||||
|
async def fetch_data(
|
||||||
|
app_name: str,
|
||||||
|
query_name: str,
|
||||||
|
*,
|
||||||
|
params: dict | None = None,
|
||||||
|
timeout: float = _DEFAULT_TIMEOUT,
|
||||||
|
required: bool = True,
|
||||||
|
) -> dict | list | None:
|
||||||
|
"""GET JSON from ``{INTERNAL_URL_APP}/internal/data/{query_name}``.
|
||||||
|
|
||||||
|
Returns parsed JSON (dict or list) on success.
|
||||||
|
When *required* is True (default), raises ``DataError`` on failure.
|
||||||
|
When *required* is False, returns None on failure.
|
||||||
|
"""
|
||||||
|
base = _internal_url(app_name)
|
||||||
|
url = f"{base}/internal/data/{query_name}"
|
||||||
|
try:
|
||||||
|
resp = await _get_client().get(
|
||||||
|
url,
|
||||||
|
params=params,
|
||||||
|
headers={DATA_HEADER: "1"},
|
||||||
|
timeout=timeout,
|
||||||
|
)
|
||||||
|
if resp.status_code == 200:
|
||||||
|
return resp.json()
|
||||||
|
msg = f"Data {app_name}/{query_name} returned {resp.status_code}"
|
||||||
|
if required:
|
||||||
|
log.error(msg)
|
||||||
|
raise DataError(msg, status_code=resp.status_code)
|
||||||
|
log.warning(msg)
|
||||||
|
return None
|
||||||
|
except DataError:
|
||||||
|
raise
|
||||||
|
except Exception as exc:
|
||||||
|
msg = f"Data {app_name}/{query_name} failed: {exc}"
|
||||||
|
if required:
|
||||||
|
log.error(msg)
|
||||||
|
raise DataError(msg) from exc
|
||||||
|
log.warning(msg)
|
||||||
|
return None
|
||||||
@@ -13,7 +13,6 @@ from sqlalchemy.orm import selectinload
|
|||||||
|
|
||||||
from shared.models.market import CartItem
|
from shared.models.market import CartItem
|
||||||
from shared.models.market_place import MarketPlace
|
from shared.models.market_place import MarketPlace
|
||||||
from shared.models.calendars import CalendarEntry, Calendar
|
|
||||||
from shared.contracts.dtos import CartItemDTO, CartSummaryDTO
|
from shared.contracts.dtos import CartItemDTO, CartSummaryDTO
|
||||||
|
|
||||||
|
|
||||||
@@ -39,13 +38,16 @@ class SqlCartService:
|
|||||||
page_slug: str | None = None,
|
page_slug: str | None = None,
|
||||||
) -> CartSummaryDTO:
|
) -> CartSummaryDTO:
|
||||||
"""Build a lightweight cart summary for the current identity."""
|
"""Build a lightweight cart summary for the current identity."""
|
||||||
# Resolve page filter
|
from shared.infrastructure.data_client import fetch_data
|
||||||
|
from shared.contracts.dtos import CalendarEntryDTO, TicketDTO, dto_from_dict
|
||||||
|
|
||||||
|
# Resolve page filter via blog data endpoint
|
||||||
page_post_id: int | None = None
|
page_post_id: int | None = None
|
||||||
if page_slug:
|
if page_slug:
|
||||||
from shared.services.registry import services
|
post = await fetch_data("blog", "post-by-slug",
|
||||||
post = await services.blog.get_post_by_slug(session, page_slug)
|
params={"slug": page_slug}, required=False)
|
||||||
if post and post.is_page:
|
if post and post.get("is_page"):
|
||||||
page_post_id = post.id
|
page_post_id = post["id"]
|
||||||
|
|
||||||
# --- product cart ---
|
# --- product cart ---
|
||||||
cart_q = select(CartItem).where(CartItem.deleted_at.is_(None))
|
cart_q = select(CartItem).where(CartItem.deleted_at.is_(None))
|
||||||
@@ -75,37 +77,34 @@ class SqlCartService:
|
|||||||
if ci.product and (ci.product.special_price or ci.product.regular_price)
|
if ci.product and (ci.product.special_price or ci.product.regular_price)
|
||||||
)
|
)
|
||||||
|
|
||||||
# --- calendar entries ---
|
# --- calendar entries via events data endpoint ---
|
||||||
from shared.services.registry import services
|
cal_params: dict = {}
|
||||||
|
if user_id is not None:
|
||||||
|
cal_params["user_id"] = user_id
|
||||||
|
if session_id is not None:
|
||||||
|
cal_params["session_id"] = session_id
|
||||||
|
|
||||||
if page_post_id is not None:
|
if page_post_id is not None:
|
||||||
cal_entries = await services.calendar.entries_for_page(
|
cal_params["page_id"] = page_post_id
|
||||||
session, page_post_id,
|
raw_entries = await fetch_data("events", "entries-for-page",
|
||||||
user_id=user_id,
|
params=cal_params, required=False) or []
|
||||||
session_id=session_id,
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
cal_entries = await services.calendar.pending_entries(
|
raw_entries = await fetch_data("events", "pending-entries",
|
||||||
session,
|
params=cal_params, required=False) or []
|
||||||
user_id=user_id,
|
cal_entries = [dto_from_dict(CalendarEntryDTO, e) for e in raw_entries]
|
||||||
session_id=session_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
calendar_count = len(cal_entries)
|
calendar_count = len(cal_entries)
|
||||||
calendar_total = sum(Decimal(str(e.cost or 0)) for e in cal_entries if e.cost is not None)
|
calendar_total = sum(Decimal(str(e.cost or 0)) for e in cal_entries if e.cost is not None)
|
||||||
|
|
||||||
# --- tickets ---
|
# --- tickets via events data endpoint ---
|
||||||
if page_post_id is not None:
|
if page_post_id is not None:
|
||||||
tickets = await services.calendar.tickets_for_page(
|
raw_tickets = await fetch_data("events", "tickets-for-page",
|
||||||
session, page_post_id,
|
params=cal_params, required=False) or []
|
||||||
user_id=user_id,
|
|
||||||
session_id=session_id,
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
tickets = await services.calendar.pending_tickets(
|
tk_params = {k: v for k, v in cal_params.items() if k != "page_id"}
|
||||||
session,
|
raw_tickets = await fetch_data("events", "pending-tickets",
|
||||||
user_id=user_id,
|
params=tk_params, required=False) or []
|
||||||
session_id=session_id,
|
tickets = [dto_from_dict(TicketDTO, t) for t in raw_tickets]
|
||||||
)
|
|
||||||
|
|
||||||
ticket_count = len(tickets)
|
ticket_count = len(tickets)
|
||||||
ticket_total = sum(Decimal(str(t.price or 0)) for t in tickets)
|
ticket_total = sum(Decimal(str(t.price or 0)) for t in tickets)
|
||||||
|
|||||||
@@ -1,18 +1,17 @@
|
|||||||
"""Typed singleton registry for domain services.
|
"""Typed singleton registry for domain services.
|
||||||
|
|
||||||
|
Each app registers ONLY its own domain service. Cross-app calls go
|
||||||
|
over HTTP via ``call_action()`` (writes) and ``fetch_data()`` (reads).
|
||||||
|
|
||||||
Usage::
|
Usage::
|
||||||
|
|
||||||
from shared.services.registry import services
|
from shared.services.registry import services
|
||||||
|
|
||||||
# Register at app startup
|
# Register at app startup (own domain only)
|
||||||
services.blog = SqlBlogService()
|
services.blog = SqlBlogService()
|
||||||
|
|
||||||
# Query anywhere
|
# Use locally within the owning app
|
||||||
if services.has("calendar"):
|
post = await services.blog.get_post_by_slug(session, slug)
|
||||||
entries = await services.calendar.pending_entries(session, ...)
|
|
||||||
|
|
||||||
# Or use stubs for absent domains
|
|
||||||
summary = await services.cart.cart_summary(session, ...)
|
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|||||||
@@ -1,204 +1,15 @@
|
|||||||
"""No-op stub services for absent domains.
|
"""No-op stub services.
|
||||||
|
|
||||||
When an app starts without a particular domain, it registers the stub
|
Cross-app calls now go over HTTP via call_action() / fetch_data().
|
||||||
so that ``services.X.method()`` returns empty/None rather than crashing.
|
Stubs are no longer needed for the 4 main domains (blog, calendar,
|
||||||
|
market, cart). Only StubFederationService remains as a safety net
|
||||||
|
for apps that conditionally load AP infrastructure.
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from decimal import Decimal
|
|
||||||
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
|
||||||
|
|
||||||
from shared.contracts.dtos import (
|
|
||||||
PostDTO,
|
|
||||||
CalendarDTO,
|
|
||||||
CalendarEntryDTO,
|
|
||||||
TicketDTO,
|
|
||||||
MarketPlaceDTO,
|
|
||||||
ProductDTO,
|
|
||||||
CartItemDTO,
|
|
||||||
CartSummaryDTO,
|
|
||||||
ActorProfileDTO,
|
|
||||||
APActivityDTO,
|
|
||||||
APFollowerDTO,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class StubBlogService:
|
|
||||||
async def get_post_by_slug(self, session: AsyncSession, slug: str) -> PostDTO | None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def get_post_by_id(self, session: AsyncSession, id: int) -> PostDTO | None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def get_posts_by_ids(self, session: AsyncSession, ids: list[int]) -> list[PostDTO]:
|
|
||||||
return []
|
|
||||||
|
|
||||||
async def search_posts(self, session, query, page=1, per_page=10):
|
|
||||||
return [], 0
|
|
||||||
|
|
||||||
|
|
||||||
class StubCalendarService:
|
|
||||||
async def calendars_for_container(
|
|
||||||
self, session: AsyncSession, container_type: str, container_id: int,
|
|
||||||
) -> list[CalendarDTO]:
|
|
||||||
return []
|
|
||||||
|
|
||||||
async def pending_entries(
|
|
||||||
self, session: AsyncSession, *, user_id: int | None, session_id: str | None,
|
|
||||||
) -> list[CalendarEntryDTO]:
|
|
||||||
return []
|
|
||||||
|
|
||||||
async def entries_for_page(
|
|
||||||
self, session: AsyncSession, page_id: int, *, user_id: int | None, session_id: str | None,
|
|
||||||
) -> list[CalendarEntryDTO]:
|
|
||||||
return []
|
|
||||||
|
|
||||||
async def entry_by_id(self, session: AsyncSession, entry_id: int) -> CalendarEntryDTO | None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def associated_entries(
|
|
||||||
self, session: AsyncSession, content_type: str, content_id: int, page: int,
|
|
||||||
) -> tuple[list[CalendarEntryDTO], bool]:
|
|
||||||
return [], False
|
|
||||||
|
|
||||||
async def toggle_entry_post(
|
|
||||||
self, session: AsyncSession, entry_id: int, content_type: str, content_id: int,
|
|
||||||
) -> bool:
|
|
||||||
return False
|
|
||||||
|
|
||||||
async def adopt_entries_for_user(
|
|
||||||
self, session: AsyncSession, user_id: int, session_id: str,
|
|
||||||
) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def claim_entries_for_order(
|
|
||||||
self, session: AsyncSession, order_id: int, user_id: int | None,
|
|
||||||
session_id: str | None, page_post_id: int | None,
|
|
||||||
) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def confirm_entries_for_order(
|
|
||||||
self, session: AsyncSession, order_id: int, user_id: int | None,
|
|
||||||
session_id: str | None,
|
|
||||||
) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def get_entries_for_order(
|
|
||||||
self, session: AsyncSession, order_id: int,
|
|
||||||
) -> list[CalendarEntryDTO]:
|
|
||||||
return []
|
|
||||||
|
|
||||||
async def user_tickets(
|
|
||||||
self, session: AsyncSession, *, user_id: int,
|
|
||||||
) -> list[TicketDTO]:
|
|
||||||
return []
|
|
||||||
|
|
||||||
async def user_bookings(
|
|
||||||
self, session: AsyncSession, *, user_id: int,
|
|
||||||
) -> list[CalendarEntryDTO]:
|
|
||||||
return []
|
|
||||||
|
|
||||||
async def confirmed_entries_for_posts(
|
|
||||||
self, session: AsyncSession, post_ids: list[int],
|
|
||||||
) -> dict[int, list[CalendarEntryDTO]]:
|
|
||||||
return {}
|
|
||||||
|
|
||||||
async def pending_tickets(
|
|
||||||
self, session: AsyncSession, *, user_id: int | None, session_id: str | None,
|
|
||||||
) -> list[TicketDTO]:
|
|
||||||
return []
|
|
||||||
|
|
||||||
async def tickets_for_page(
|
|
||||||
self, session: AsyncSession, page_id: int, *, user_id: int | None, session_id: str | None,
|
|
||||||
) -> list[TicketDTO]:
|
|
||||||
return []
|
|
||||||
|
|
||||||
async def claim_tickets_for_order(
|
|
||||||
self, session: AsyncSession, order_id: int, user_id: int | None,
|
|
||||||
session_id: str | None, page_post_id: int | None,
|
|
||||||
) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def confirm_tickets_for_order(
|
|
||||||
self, session: AsyncSession, order_id: int,
|
|
||||||
) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def get_tickets_for_order(
|
|
||||||
self, session: AsyncSession, order_id: int,
|
|
||||||
) -> list[TicketDTO]:
|
|
||||||
return []
|
|
||||||
|
|
||||||
async def adopt_tickets_for_user(
|
|
||||||
self, session: AsyncSession, user_id: int, session_id: str,
|
|
||||||
) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def adjust_ticket_quantity(
|
|
||||||
self, session, entry_id, count, *, user_id, session_id, ticket_type_id=None,
|
|
||||||
) -> int:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
async def upcoming_entries_for_container(self, session, container_type, container_id, *, page=1, per_page=20):
|
|
||||||
return [], False
|
|
||||||
|
|
||||||
async def entry_ids_for_content(self, session, content_type, content_id):
|
|
||||||
return set()
|
|
||||||
|
|
||||||
async def visible_entries_for_period(self, session, calendar_id, period_start, period_end, *, user_id, is_admin, session_id):
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
class StubMarketService:
|
|
||||||
async def marketplaces_for_container(
|
|
||||||
self, session: AsyncSession, container_type: str, container_id: int,
|
|
||||||
) -> list[MarketPlaceDTO]:
|
|
||||||
return []
|
|
||||||
|
|
||||||
async def list_marketplaces(
|
|
||||||
self, session: AsyncSession,
|
|
||||||
container_type: str | None = None, container_id: int | None = None,
|
|
||||||
*, page: int = 1, per_page: int = 20,
|
|
||||||
) -> tuple[list[MarketPlaceDTO], bool]:
|
|
||||||
return [], False
|
|
||||||
|
|
||||||
async def product_by_id(self, session: AsyncSession, product_id: int) -> ProductDTO | None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def create_marketplace(
|
|
||||||
self, session: AsyncSession, container_type: str, container_id: int,
|
|
||||||
name: str, slug: str,
|
|
||||||
) -> MarketPlaceDTO:
|
|
||||||
raise RuntimeError("MarketService not available")
|
|
||||||
|
|
||||||
async def soft_delete_marketplace(
|
|
||||||
self, session: AsyncSession, container_type: str, container_id: int,
|
|
||||||
slug: str,
|
|
||||||
) -> bool:
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class StubCartService:
|
|
||||||
async def cart_summary(
|
|
||||||
self, session: AsyncSession, *, user_id: int | None, session_id: str | None,
|
|
||||||
page_slug: str | None = None,
|
|
||||||
) -> CartSummaryDTO:
|
|
||||||
return CartSummaryDTO()
|
|
||||||
|
|
||||||
async def cart_items(
|
|
||||||
self, session: AsyncSession, *, user_id: int | None, session_id: str | None,
|
|
||||||
) -> list[CartItemDTO]:
|
|
||||||
return []
|
|
||||||
|
|
||||||
async def adopt_cart_for_user(
|
|
||||||
self, session: AsyncSession, user_id: int, session_id: str,
|
|
||||||
) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class StubFederationService:
|
class StubFederationService:
|
||||||
"""No-op federation stub for apps that don't own federation."""
|
"""No-op federation stub for apps that don't load AP."""
|
||||||
|
|
||||||
async def get_actor_by_username(self, session, username):
|
async def get_actor_by_username(self, session, username):
|
||||||
return None
|
return None
|
||||||
@@ -206,109 +17,16 @@ class StubFederationService:
|
|||||||
async def get_actor_by_user_id(self, session, user_id):
|
async def get_actor_by_user_id(self, session, user_id):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def create_actor(self, session, user_id, preferred_username,
|
|
||||||
display_name=None, summary=None):
|
|
||||||
raise RuntimeError("FederationService not available")
|
|
||||||
|
|
||||||
async def username_available(self, session, username):
|
|
||||||
return False
|
|
||||||
|
|
||||||
async def publish_activity(self, session, *, actor_user_id, activity_type,
|
async def publish_activity(self, session, *, actor_user_id, activity_type,
|
||||||
object_type, object_data, source_type=None,
|
object_type, object_data, source_type=None,
|
||||||
source_id=None):
|
source_id=None):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def get_activity(self, session, activity_id):
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def get_outbox(self, session, username, page=1, per_page=20, origin_app=None):
|
|
||||||
return [], 0
|
|
||||||
|
|
||||||
async def get_activity_for_source(self, session, source_type, source_id):
|
async def get_activity_for_source(self, session, source_type, source_id):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def count_activities_for_source(self, session, source_type, source_id, *, activity_type):
|
async def count_activities_for_source(self, session, source_type, source_id, *, activity_type):
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
async def get_followers(self, session, username, app_domain=None):
|
|
||||||
return []
|
|
||||||
|
|
||||||
async def add_follower(self, session, username, follower_acct, follower_inbox,
|
|
||||||
follower_actor_url, follower_public_key=None,
|
|
||||||
app_domain="federation"):
|
|
||||||
raise RuntimeError("FederationService not available")
|
|
||||||
|
|
||||||
async def remove_follower(self, session, username, follower_acct, app_domain="federation"):
|
|
||||||
return False
|
|
||||||
|
|
||||||
async def get_or_fetch_remote_actor(self, session, actor_url):
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def search_remote_actor(self, session, acct):
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def search_actors(self, session, query, page=1, limit=20):
|
|
||||||
return [], 0
|
|
||||||
|
|
||||||
async def send_follow(self, session, local_username, remote_actor_url):
|
|
||||||
raise RuntimeError("FederationService not available")
|
|
||||||
|
|
||||||
async def get_following(self, session, username, page=1, per_page=20):
|
|
||||||
return [], 0
|
|
||||||
|
|
||||||
async def get_followers_paginated(self, session, username, page=1, per_page=20):
|
|
||||||
return [], 0
|
|
||||||
|
|
||||||
async def accept_follow_response(self, session, local_username, remote_actor_url):
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def unfollow(self, session, local_username, remote_actor_url):
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def ingest_remote_post(self, session, remote_actor_id, activity_json, object_json):
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def delete_remote_post(self, session, object_id):
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def get_remote_post(self, session, object_id):
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def get_home_timeline(self, session, actor_profile_id, before=None, limit=20):
|
|
||||||
return []
|
|
||||||
|
|
||||||
async def get_public_timeline(self, session, before=None, limit=20):
|
|
||||||
return []
|
|
||||||
|
|
||||||
async def get_actor_timeline(self, session, remote_actor_id, before=None, limit=20):
|
|
||||||
return []
|
|
||||||
|
|
||||||
async def create_local_post(self, session, actor_profile_id, content, visibility="public", in_reply_to=None):
|
|
||||||
raise RuntimeError("FederationService not available")
|
|
||||||
|
|
||||||
async def delete_local_post(self, session, actor_profile_id, post_id):
|
|
||||||
raise RuntimeError("FederationService not available")
|
|
||||||
|
|
||||||
async def like_post(self, session, actor_profile_id, object_id, author_inbox):
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def unlike_post(self, session, actor_profile_id, object_id, author_inbox):
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def boost_post(self, session, actor_profile_id, object_id, author_inbox):
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def unboost_post(self, session, actor_profile_id, object_id, author_inbox):
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def get_notifications(self, session, actor_profile_id, before=None, limit=20):
|
|
||||||
return []
|
|
||||||
|
|
||||||
async def unread_notification_count(self, session, actor_profile_id):
|
|
||||||
return 0
|
|
||||||
|
|
||||||
async def mark_notifications_read(self, session, actor_profile_id):
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def get_stats(self, session):
|
async def get_stats(self, session):
|
||||||
return {"actors": 0, "activities": 0, "followers": 0}
|
return {"actors": 0, "activities": 0, "followers": 0}
|
||||||
|
|||||||
Reference in New Issue
Block a user