Files
content-factory/apps/backend/src/application/site_config.py
T

312 lines
11 KiB
Python

from __future__ import annotations
from datetime import UTC, datetime
from hashlib import sha256
from uuid import NAMESPACE_URL, UUID, uuid5
from src.domain.contracts import (
CurrentUser,
ScriptConfigVersionAuditEventListResponse,
ScriptConfigVersionAuditEventSummary,
ScriptConfigVersionCreateRequest,
ScriptConfigVersionResponse,
ScriptConfigVersionStatus,
ScriptConfigVersionSummary,
TargetSiteConfigCreateRequest,
TargetSiteConfigResponse,
TargetSiteConfigUpdateRequest,
)
_VERSION_CREATED_EVENT_TYPE = "SCRIPT_CONFIG_VERSION_CREATED"
_VERSION_ACTIVATED_EVENT_TYPE = "SCRIPT_CONFIG_VERSION_ACTIVATED"
_VERSION_ROLLBACK_EVENT_TYPE = "SCRIPT_CONFIG_VERSION_ROLLBACK"
def create_target_site(
repository: object,
request: TargetSiteConfigCreateRequest,
) -> TargetSiteConfigResponse:
now = _now()
site = repository.target_sites.upsert(
site_id=_stable_uuid(f"target-site:{request.slug}"),
name=request.name,
slug=request.slug,
publishing_type=request.publishing_type,
default_language=request.default_language,
brand_voice=request.brand_voice,
audience=request.audience,
seo_rules=request.seo_rules,
visual_rules=request.visual_rules,
source_rules=request.source_rules,
publishing_rules=request.publishing_rules,
active_script_config_version_id=request.active_script_config_version_id,
created_at=now,
updated_at=now,
)
return TargetSiteConfigResponse(site=site)
def get_target_site(
repository: object,
site_id: UUID,
) -> TargetSiteConfigResponse:
site = repository.target_sites.get_by_id(site_id)
return TargetSiteConfigResponse(site=site)
def update_target_site(
repository: object,
site_id: UUID,
request: TargetSiteConfigUpdateRequest,
) -> TargetSiteConfigResponse:
existing = repository.target_sites.get_by_id(site_id)
changes = request.model_dump(exclude_unset=True)
publishing_rules = (
request.publishing_rules
if "publishing_rules" in changes
else existing.publishing_rules
)
now = _now()
site = repository.target_sites.update(
site_id=existing.id,
name=changes.get("name", existing.name),
slug=changes.get("slug", existing.slug),
publishing_type=changes.get("publishing_type", existing.publishing_type),
default_language=changes.get("default_language", existing.default_language),
brand_voice=changes.get("brand_voice", existing.brand_voice),
audience=changes.get("audience", existing.audience),
seo_rules=changes.get("seo_rules", existing.seo_rules),
visual_rules=changes.get("visual_rules", existing.visual_rules),
source_rules=changes.get("source_rules", existing.source_rules),
publishing_rules=publishing_rules,
active_script_config_version_id=changes.get(
"active_script_config_version_id",
existing.active_script_config_version_id,
),
updated_at=now,
)
return TargetSiteConfigResponse(site=site)
def create_script_config_version(
repository: object,
*,
site_id: UUID,
current_user: CurrentUser,
request: ScriptConfigVersionCreateRequest,
) -> ScriptConfigVersionResponse:
repository.target_sites.get_by_id(site_id)
if request.rollback_target_version_id is not None:
rollback_target = repository.script_config_versions.get_by_id(
request.rollback_target_version_id
)
if str(rollback_target["target_site_id"]) != str(site_id):
raise LookupError(
f"Rollback target version is not for site: {request.rollback_target_version_id}"
)
existing_versions = repository.script_config_versions.list_for_site(site_id)
version = request.version or _next_version(existing_versions)
now = _now()
version_id = _stable_uuid(f"script-config-version:{site_id}:{version}")
status = (
ScriptConfigVersionStatus.ACTIVE
if request.activate
else ScriptConfigVersionStatus.DRAFT
)
row = repository.script_config_versions.upsert(
version_id=version_id,
target_site_id=site_id,
version=version,
status=status,
created_by=current_user.id,
created_at=now,
updated_at=now,
diff=request.diff,
rollback_target_version_id=request.rollback_target_version_id,
activated_at=now if request.activate else None,
publishing_yaml=request.publishing_yaml,
publishing_yaml_hash=_sha256(request.publishing_yaml),
transform_script=request.transform_script,
transform_script_hash=_sha256(request.transform_script),
)
if request.activate:
row = repository.script_config_versions.activate(
target_site_id=site_id,
version_id=version_id,
activated_at=now,
rollback_target_version_id=request.rollback_target_version_id,
)
repository.script_config_version_events.create(
target_site_id=site_id,
version_id=version_id,
event_type=_VERSION_ACTIVATED_EVENT_TYPE,
actor_user_id=current_user.id,
payload={"action": "activate", "version": version},
created_at=now,
)
else:
repository.script_config_version_events.create(
target_site_id=site_id,
version_id=version_id,
event_type=_VERSION_CREATED_EVENT_TYPE,
actor_user_id=current_user.id,
payload={"action": "create", "version": version},
created_at=now,
)
return ScriptConfigVersionResponse(version=_script_version_summary(row))
def activate_script_config_version(
repository: object,
*,
site_id: UUID,
version_id: UUID,
current_user: CurrentUser,
) -> ScriptConfigVersionResponse:
repository.target_sites.get_by_id(site_id)
row = repository.script_config_versions.activate(
target_site_id=site_id,
version_id=version_id,
activated_at=_now(),
)
repository.script_config_version_events.create(
target_site_id=site_id,
version_id=version_id,
event_type=_VERSION_ACTIVATED_EVENT_TYPE,
actor_user_id=current_user.id,
payload={"action": "activate", "version_id": str(version_id)},
created_at=_now(),
)
return ScriptConfigVersionResponse(version=_script_version_summary(row))
def rollback_script_config_version(
repository: object,
*,
site_id: UUID,
version_id: UUID,
current_user: CurrentUser,
) -> ScriptConfigVersionResponse:
target_site = repository.target_sites.get_by_id(site_id)
version_row = repository.script_config_versions.get_by_id(version_id)
if str(version_row["target_site_id"]) != str(site_id):
raise LookupError(
f"Script config version not found for site: {site_id} {version_id}"
)
rollback_target_id = target_site.active_script_config_version_id
if rollback_target_id == version_id:
rollback_target_id = None
row = repository.script_config_versions.activate(
target_site_id=site_id,
version_id=version_id,
activated_at=_now(),
rollback_target_version_id=rollback_target_id,
)
repository.script_config_version_events.create(
target_site_id=site_id,
version_id=version_id,
event_type=_VERSION_ROLLBACK_EVENT_TYPE,
actor_user_id=current_user.id,
payload={
"action": "rollback",
"rollback_target_version_id": (
str(rollback_target_id) if rollback_target_id is not None else None
),
},
created_at=_now(),
)
return ScriptConfigVersionResponse(version=_script_version_summary(row))
def list_script_config_versions(
repository: object,
site_id: UUID,
) -> list[ScriptConfigVersionSummary]:
repository.target_sites.get_by_id(site_id)
rows = repository.script_config_versions.list_for_site(site_id)
return [_script_version_summary(row) for row in rows]
def list_script_config_audit_events(
repository: object,
site_id: UUID,
) -> ScriptConfigVersionAuditEventListResponse:
repository.target_sites.get_by_id(site_id)
version_rows = repository.script_config_versions.list_for_site(site_id)
version_by_id = {str(row["id"]): row for row in version_rows}
events = repository.script_config_version_events.list_for_site(site_id)
summaries: list[ScriptConfigVersionAuditEventSummary] = []
for event in events:
version_row = version_by_id.get(str(event["version_id"]))
payload = event["payload"] if isinstance(event["payload"], dict) else {}
summaries.append(
ScriptConfigVersionAuditEventSummary(
id=event["id"],
target_site_id=event["target_site_id"],
version_id=event["version_id"],
event_type=event["event_type"],
actor_user_id=event["actor_user_id"],
payload=payload,
created_at=event["created_at"],
version=(
int(version_row["version"])
if version_row is not None and version_row.get("version") is not None
else None
),
diff=(
version_row["diff"]
if version_row is not None and isinstance(version_row.get("diff"), dict)
else {}
),
rollback_target_version_id=(
version_row["rollback_target_version_id"]
if version_row is not None
else None
),
)
)
return ScriptConfigVersionAuditEventListResponse(events=summaries)
def _script_version_summary(row: dict[str, object]) -> ScriptConfigVersionSummary:
return ScriptConfigVersionSummary(
id=row["id"],
target_site_id=row["target_site_id"],
version=row["version"],
status=row["status"],
diff=row["diff"],
rollback_target_version_id=row["rollback_target_version_id"],
publishing_yaml_hash=row["publishing_yaml_hash"],
transform_script_hash=row["transform_script_hash"],
created_by=row["created_by"],
created_at=row["created_at"],
activated_at=row["activated_at"],
)
def _next_version(rows: list[dict[str, object]]) -> int:
versions = [int(row["version"]) for row in rows]
if not versions:
return 1
return max(versions) + 1
def _stable_uuid(value: str) -> UUID:
return uuid5(NAMESPACE_URL, f"ai-content-pipeline:{value}")
def _sha256(value: str) -> str:
return sha256(value.encode("utf-8")).hexdigest()
def _now() -> datetime:
return datetime.now(UTC)