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

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

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

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

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 08:13:50 +00:00

138 lines
4.4 KiB
Python

"""SQL implementation of PageConfig service methods.
Extracted from blog/bp/data/routes.py and blog/bp/actions/routes.py
to enable sx defquery/defaction conversion.
"""
from __future__ import annotations
from typing import Any
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm.attributes import flag_modified
from shared.models.page_config import PageConfig
def _to_dict(pc: PageConfig) -> dict[str, Any]:
return {
"id": pc.id,
"container_type": pc.container_type,
"container_id": pc.container_id,
"features": pc.features or {},
"sumup_merchant_code": pc.sumup_merchant_code,
"sumup_api_key": pc.sumup_api_key,
"sumup_checkout_prefix": pc.sumup_checkout_prefix,
}
class SqlPageConfigService:
async def ensure(
self, session: AsyncSession, *,
container_type: str = "page", container_id: int,
) -> dict[str, Any]:
"""Get or create a PageConfig. Returns minimal dict with id."""
row = (await session.execute(
select(PageConfig).where(
PageConfig.container_type == container_type,
PageConfig.container_id == container_id,
)
)).scalar_one_or_none()
if row is None:
row = PageConfig(
container_type=container_type,
container_id=container_id,
features={},
)
session.add(row)
await session.flush()
return {
"id": row.id,
"container_type": row.container_type,
"container_id": row.container_id,
}
async def get_by_container(
self, session: AsyncSession, *,
container_type: str = "page", container_id: int,
) -> dict[str, Any] | None:
pc = (await session.execute(
select(PageConfig).where(
PageConfig.container_type == container_type,
PageConfig.container_id == container_id,
)
)).scalar_one_or_none()
return _to_dict(pc) if pc else None
async def get_by_id(
self, session: AsyncSession, *, id: int,
) -> dict[str, Any] | None:
pc = await session.get(PageConfig, id)
return _to_dict(pc) if pc else None
async def get_batch(
self, session: AsyncSession, *,
container_type: str = "page", ids: list[int],
) -> list[dict[str, Any]]:
if not ids:
return []
result = await session.execute(
select(PageConfig).where(
PageConfig.container_type == container_type,
PageConfig.container_id.in_(ids),
)
)
return [_to_dict(pc) for pc in result.scalars().all()]
async def update(
self, session: AsyncSession, *,
container_type: str = "page", container_id: int,
features: dict | None = None,
sumup_merchant_code: str | None = None,
sumup_checkout_prefix: str | None = None,
sumup_api_key: str | None = None,
) -> dict[str, Any]:
pc = (await session.execute(
select(PageConfig).where(
PageConfig.container_type == container_type,
PageConfig.container_id == container_id,
)
)).scalar_one_or_none()
if pc is None:
pc = PageConfig(
container_type=container_type,
container_id=container_id,
features=features or {},
)
session.add(pc)
await session.flush()
if features is not None:
merged = dict(pc.features or {})
for key, val in features.items():
if isinstance(val, bool):
merged[key] = val
elif val in ("true", "1", "on"):
merged[key] = True
elif val in ("false", "0", "off", None):
merged[key] = False
pc.features = merged
flag_modified(pc, "features")
if sumup_merchant_code is not None:
pc.sumup_merchant_code = sumup_merchant_code or None
if sumup_checkout_prefix is not None:
pc.sumup_checkout_prefix = sumup_checkout_prefix or None
if sumup_api_key is not None:
pc.sumup_api_key = sumup_api_key or None
await session.flush()
result = _to_dict(pc)
result["sumup_configured"] = bool(pc.sumup_api_key)
return result