312 lines
11 KiB
Python
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)
|