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>
138 lines
4.4 KiB
Python
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
|