|
@@ -0,0 +1,478 @@
|
|
|
|
|
+"""SQLAlchemy final-publication unit of work.
|
|
|
|
|
+
|
|
|
|
|
+All branch-zero, readback, pointer, artifact, publication and build mutations use
|
|
|
|
|
+one AsyncSession and one commit.
|
|
|
|
|
+"""
|
|
|
|
|
+
|
|
|
|
|
+from __future__ import annotations
|
|
|
|
|
+
|
|
|
|
|
+from datetime import UTC, datetime
|
|
|
|
|
+from typing import Any, cast
|
|
|
|
|
+
|
|
|
|
|
+from sqlalchemy import CursorResult, delete, insert, select, update
|
|
|
|
|
+from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
|
|
|
|
+
|
|
|
|
|
+from script_build_host.application.legacy_projection import LegacyDetailProjectionService
|
|
|
|
|
+from script_build_host.domain.artifacts import ArtifactState, ArtifactVersion
|
|
|
|
|
+from script_build_host.domain.errors import (
|
|
|
|
|
+ BuildNotFound,
|
|
|
|
|
+ MissionFencingTokenStale,
|
|
|
|
|
+ ProtocolViolation,
|
|
|
|
|
+ PublicationReadbackMismatch,
|
|
|
|
|
+)
|
|
|
|
|
+from script_build_host.domain.phase_three_artifacts import RootDeliveryManifestV1
|
|
|
|
|
+from script_build_host.domain.phase_two_artifacts import StructuredScriptArtifactV1
|
|
|
|
|
+from script_build_host.domain.records import (
|
|
|
|
|
+ MissionOwnerToken,
|
|
|
|
|
+ Publication,
|
|
|
|
|
+ PublicationResult,
|
|
|
|
|
+ PublicationState,
|
|
|
|
|
+ PublicationType,
|
|
|
|
|
+)
|
|
|
|
|
+from script_build_host.infrastructure.legacy_tables import (
|
|
|
|
|
+ script_build_element,
|
|
|
|
|
+ script_build_paragraph,
|
|
|
|
|
+ script_build_paragraph_element,
|
|
|
|
|
+ script_build_record,
|
|
|
|
|
+)
|
|
|
|
|
+from script_build_host.infrastructure.ownership import FencedCommandGate
|
|
|
|
|
+from script_build_host.infrastructure.tables import (
|
|
|
|
|
+ artifact_version_table,
|
|
|
|
|
+ mission_binding_table,
|
|
|
|
|
+ publication_table,
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+class SqlAlchemyFinalPublicationUnitOfWork:
|
|
|
|
|
+ def __init__(
|
|
|
|
|
+ self,
|
|
|
|
|
+ sessions: async_sessionmaker[AsyncSession],
|
|
|
|
|
+ projection: LegacyDetailProjectionService,
|
|
|
|
|
+ fencing: FencedCommandGate,
|
|
|
|
|
+ ) -> None:
|
|
|
|
|
+ self._sessions = sessions
|
|
|
|
|
+ self._projection = projection
|
|
|
|
|
+ self._fencing = fencing
|
|
|
|
|
+
|
|
|
|
|
+ async def reserve(
|
|
|
|
|
+ self,
|
|
|
|
|
+ *,
|
|
|
|
|
+ script_build_id: int,
|
|
|
|
|
+ accept_decision_id: str,
|
|
|
|
|
+ artifact_version_id: int,
|
|
|
|
|
+ expected_sha256: str,
|
|
|
|
|
+ owner_token: MissionOwnerToken,
|
|
|
|
|
+ ) -> Publication:
|
|
|
|
|
+ now = datetime.now(UTC)
|
|
|
|
|
+ expected = expected_sha256.removeprefix("sha256:")
|
|
|
|
|
+ async with self._sessions() as session, session.begin():
|
|
|
|
|
+ await self._fencing.verify_in_session(session, owner_token)
|
|
|
|
|
+ row = (
|
|
|
|
|
+ (
|
|
|
|
|
+ await session.execute(
|
|
|
|
|
+ select(publication_table)
|
|
|
|
|
+ .where(
|
|
|
|
|
+ publication_table.c.script_build_id == script_build_id,
|
|
|
|
|
+ publication_table.c.publication_type == PublicationType.FINAL.value,
|
|
|
|
|
+ )
|
|
|
|
|
+ .with_for_update()
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ .mappings()
|
|
|
|
|
+ .one_or_none()
|
|
|
|
|
+ )
|
|
|
|
|
+ if row is None:
|
|
|
|
|
+ result = await session.execute(
|
|
|
|
|
+ insert(publication_table).values(
|
|
|
|
|
+ script_build_id=script_build_id,
|
|
|
|
|
+ publication_type=PublicationType.FINAL.value,
|
|
|
|
|
+ accept_decision_id=accept_decision_id,
|
|
|
|
|
+ artifact_version_id=artifact_version_id,
|
|
|
|
|
+ expected_sha256=expected,
|
|
|
|
|
+ state=PublicationState.PENDING.value,
|
|
|
|
|
+ attempt_count=0,
|
|
|
|
|
+ publication_revision=0,
|
|
|
|
|
+ created_at=now,
|
|
|
|
|
+ updated_at=now,
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ publication_id = int(cast(Any, result).inserted_primary_key[0])
|
|
|
|
|
+ row = (
|
|
|
|
|
+ (
|
|
|
|
|
+ await session.execute(
|
|
|
|
|
+ select(publication_table).where(
|
|
|
|
|
+ publication_table.c.id == publication_id
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ .mappings()
|
|
|
|
|
+ .one()
|
|
|
|
|
+ )
|
|
|
|
|
+ if (
|
|
|
|
|
+ int(row["script_build_id"]) != script_build_id
|
|
|
|
|
+ or row["accept_decision_id"] != accept_decision_id
|
|
|
|
|
+ or int(row["artifact_version_id"]) != artifact_version_id
|
|
|
|
|
+ or row["expected_sha256"] != expected
|
|
|
|
|
+ ):
|
|
|
|
|
+ raise ProtocolViolation("final publication identity already differs")
|
|
|
|
|
+ return _publication(row)
|
|
|
|
|
+
|
|
|
|
|
+ async def mark_failed(
|
|
|
|
|
+ self,
|
|
|
|
|
+ publication_id: int,
|
|
|
|
|
+ *,
|
|
|
|
|
+ error_code: str,
|
|
|
|
|
+ error_summary: str,
|
|
|
|
|
+ owner_token: MissionOwnerToken,
|
|
|
|
|
+ ) -> None:
|
|
|
|
|
+ async with self._sessions() as session, session.begin():
|
|
|
|
|
+ await self._fencing.verify_in_session(session, owner_token)
|
|
|
|
|
+ result = await session.execute(
|
|
|
|
|
+ update(publication_table)
|
|
|
|
|
+ .where(
|
|
|
|
|
+ publication_table.c.id == publication_id,
|
|
|
|
|
+ publication_table.c.script_build_id == owner_token.script_build_id,
|
|
|
|
|
+ publication_table.c.state != PublicationState.PUBLISHED.value,
|
|
|
|
|
+ )
|
|
|
|
|
+ .values(
|
|
|
|
|
+ state=PublicationState.FAILED.value,
|
|
|
|
|
+ last_error_code=error_code[:64],
|
|
|
|
|
+ last_error_summary=error_summary[:1000],
|
|
|
|
|
+ updated_at=datetime.now(UTC),
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ if result.rowcount != 1:
|
|
|
|
|
+ raise ProtocolViolation("final publication failure CAS did not match")
|
|
|
|
|
+
|
|
|
|
|
+ async def publish(
|
|
|
|
|
+ self,
|
|
|
|
|
+ *,
|
|
|
|
|
+ script_build_id: int,
|
|
|
|
|
+ publication_id: int,
|
|
|
|
|
+ manifest_version: ArtifactVersion,
|
|
|
|
|
+ structured_version: ArtifactVersion,
|
|
|
|
|
+ manifest: RootDeliveryManifestV1,
|
|
|
|
|
+ structured_script: StructuredScriptArtifactV1,
|
|
|
|
|
+ direction_markdown: str,
|
|
|
|
|
+ owner_token: MissionOwnerToken,
|
|
|
|
|
+ ) -> PublicationResult:
|
|
|
|
|
+ now = datetime.now(UTC)
|
|
|
|
|
+ async with self._sessions() as session:
|
|
|
|
|
+ async with session.begin():
|
|
|
|
|
+ # A published identity may be read back after a lost commit ACK,
|
|
|
|
|
+ # even though the build is already success. No writes occur on
|
|
|
|
|
+ # that path.
|
|
|
|
|
+ await self._fencing.verify_in_session(session, owner_token, allow_success=True)
|
|
|
|
|
+ publication = (
|
|
|
|
|
+ (
|
|
|
|
|
+ await session.execute(
|
|
|
|
|
+ select(publication_table)
|
|
|
|
|
+ .where(publication_table.c.id == publication_id)
|
|
|
|
|
+ .with_for_update()
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ .mappings()
|
|
|
|
|
+ .one_or_none()
|
|
|
|
|
+ )
|
|
|
|
|
+ if publication is None:
|
|
|
|
|
+ raise BuildNotFound()
|
|
|
|
|
+ if (
|
|
|
|
|
+ int(publication["script_build_id"]) != script_build_id
|
|
|
|
|
+ or publication["publication_type"] != PublicationType.FINAL.value
|
|
|
|
|
+ or int(publication["artifact_version_id"])
|
|
|
|
|
+ != manifest_version.artifact_version_id
|
|
|
|
|
+ or publication["expected_sha256"]
|
|
|
|
|
+ != manifest_version.canonical_sha256.removeprefix("sha256:")
|
|
|
|
|
+ ):
|
|
|
|
|
+ raise ProtocolViolation("final publication identity differs from its manifest")
|
|
|
|
|
+ if publication["state"] == PublicationState.PUBLISHED.value:
|
|
|
|
|
+ detail = await self._projection.project_in_session(
|
|
|
|
|
+ script_build_id, session=session
|
|
|
|
|
+ )
|
|
|
|
|
+ readback = self._projection.to_canonical(detail)
|
|
|
|
|
+ if readback.canonical_sha256 != manifest.legacy_projection_digest:
|
|
|
|
|
+ raise PublicationReadbackMismatch()
|
|
|
|
|
+ return PublicationResult(
|
|
|
|
|
+ script_build_id=script_build_id,
|
|
|
|
|
+ publication_id=publication_id,
|
|
|
|
|
+ publication_type=PublicationType.FINAL,
|
|
|
|
|
+ state=PublicationState.PUBLISHED,
|
|
|
|
|
+ legacy_projection_digest=readback.canonical_sha256,
|
|
|
|
|
+ committed=True,
|
|
|
|
|
+ )
|
|
|
|
|
+ await self._fencing.verify_in_session(session, owner_token)
|
|
|
|
|
+ await session.execute(
|
|
|
|
|
+ update(publication_table)
|
|
|
|
|
+ .where(publication_table.c.id == publication_id)
|
|
|
|
|
+ .values(
|
|
|
|
|
+ state=PublicationState.PUBLISHING.value,
|
|
|
|
|
+ attempt_count=publication_table.c.attempt_count + 1,
|
|
|
|
|
+ publication_revision=publication_table.c.publication_revision + 1,
|
|
|
|
|
+ updated_at=now,
|
|
|
|
|
+ last_error_code=None,
|
|
|
|
|
+ last_error_summary=None,
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ await self._lock_branch_zero(session, script_build_id)
|
|
|
|
|
+ await session.execute(
|
|
|
|
|
+ delete(script_build_paragraph_element).where(
|
|
|
|
|
+ script_build_paragraph_element.c.script_build_id == script_build_id,
|
|
|
|
|
+ script_build_paragraph_element.c.branch_id == 0,
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ await session.execute(
|
|
|
|
|
+ delete(script_build_paragraph).where(
|
|
|
|
|
+ script_build_paragraph.c.script_build_id == script_build_id,
|
|
|
|
|
+ script_build_paragraph.c.branch_id == 0,
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ await session.execute(
|
|
|
|
|
+ delete(script_build_element).where(
|
|
|
|
|
+ script_build_element.c.script_build_id == script_build_id,
|
|
|
|
|
+ script_build_element.c.branch_id == 0,
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ paragraph_ids = await self._insert_paragraphs(
|
|
|
|
|
+ session, script_build_id, structured_script, now
|
|
|
|
|
+ )
|
|
|
|
|
+ element_ids = await self._insert_elements(
|
|
|
|
|
+ session, script_build_id, structured_script, now
|
|
|
|
|
+ )
|
|
|
|
|
+ await self._insert_links(
|
|
|
|
|
+ session,
|
|
|
|
|
+ script_build_id,
|
|
|
|
|
+ structured_script,
|
|
|
|
|
+ paragraph_ids,
|
|
|
|
|
+ element_ids,
|
|
|
|
|
+ )
|
|
|
|
|
+ await session.execute(
|
|
|
|
|
+ update(script_build_record)
|
|
|
|
|
+ .where(script_build_record.c.id == script_build_id)
|
|
|
|
|
+ .values(
|
|
|
|
|
+ script_direction=direction_markdown,
|
|
|
|
|
+ summary=manifest.build_summary,
|
|
|
|
|
+ paragraph_count=len(
|
|
|
|
|
+ [p for p in structured_script.paragraphs if p.is_active]
|
|
|
|
|
+ ),
|
|
|
|
|
+ element_count=len([e for e in structured_script.elements if e.is_active]),
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ detail = await self._projection.project_in_session(script_build_id, session=session)
|
|
|
|
|
+ readback = self._projection.to_canonical(detail)
|
|
|
|
|
+ if readback.canonical_sha256 != manifest.legacy_projection_digest:
|
|
|
|
|
+ raise PublicationReadbackMismatch()
|
|
|
|
|
+ await self._fencing.verify_in_session(session, owner_token)
|
|
|
|
|
+ artifact_ids = sorted(
|
|
|
|
|
+ {manifest_version.artifact_version_id, structured_version.artifact_version_id}
|
|
|
|
|
+ )
|
|
|
|
|
+ locked_artifacts = list(
|
|
|
|
|
+ (
|
|
|
|
|
+ await session.execute(
|
|
|
|
|
+ select(artifact_version_table.c.id)
|
|
|
|
|
+ .where(artifact_version_table.c.id.in_(artifact_ids))
|
|
|
|
|
+ .order_by(artifact_version_table.c.id)
|
|
|
|
|
+ .with_for_update()
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ .scalars()
|
|
|
|
|
+ .all()
|
|
|
|
|
+ )
|
|
|
|
|
+ if locked_artifacts != artifact_ids:
|
|
|
|
|
+ raise ProtocolViolation("final publication artifacts are missing")
|
|
|
|
|
+ pointer_result = await session.execute(
|
|
|
|
|
+ update(mission_binding_table)
|
|
|
|
|
+ .where(
|
|
|
|
|
+ mission_binding_table.c.script_build_id == script_build_id,
|
|
|
|
|
+ mission_binding_table.c.accepted_root_artifact_version_id.is_(None),
|
|
|
|
|
+ )
|
|
|
|
|
+ .values(
|
|
|
|
|
+ accepted_root_artifact_version_id=manifest_version.artifact_version_id,
|
|
|
|
|
+ updated_at=now,
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ if pointer_result.rowcount != 1:
|
|
|
|
|
+ pointer = await session.scalar(
|
|
|
|
|
+ select(mission_binding_table.c.accepted_root_artifact_version_id).where(
|
|
|
|
|
+ mission_binding_table.c.script_build_id == script_build_id
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ if pointer != manifest_version.artifact_version_id:
|
|
|
|
|
+ raise MissionFencingTokenStale()
|
|
|
|
|
+ artifact_result = await session.execute(
|
|
|
|
|
+ update(artifact_version_table)
|
|
|
|
|
+ .where(
|
|
|
|
|
+ artifact_version_table.c.id.in_(artifact_ids),
|
|
|
|
|
+ artifact_version_table.c.state.in_(
|
|
|
|
|
+ [ArtifactState.FROZEN.value, ArtifactState.PUBLISHED.value]
|
|
|
|
|
+ ),
|
|
|
|
|
+ )
|
|
|
|
|
+ .values(state=ArtifactState.PUBLISHED.value, published_at=now)
|
|
|
|
|
+ )
|
|
|
|
|
+ if artifact_result.rowcount != len(artifact_ids):
|
|
|
|
|
+ raise ProtocolViolation("final publication artifacts are not publishable")
|
|
|
|
|
+ await session.execute(
|
|
|
|
|
+ update(publication_table)
|
|
|
|
|
+ .where(publication_table.c.id == publication_id)
|
|
|
|
|
+ .values(
|
|
|
|
|
+ state=PublicationState.PUBLISHED.value,
|
|
|
|
|
+ updated_at=now,
|
|
|
|
|
+ published_at=now,
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ build_result = await session.execute(
|
|
|
|
|
+ update(script_build_record)
|
|
|
|
|
+ .where(script_build_record.c.id == script_build_id)
|
|
|
|
|
+ .values(
|
|
|
|
|
+ status="success",
|
|
|
|
|
+ error_message=None,
|
|
|
|
|
+ end_time=now,
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ if build_result.rowcount != 1:
|
|
|
|
|
+ raise BuildNotFound()
|
|
|
|
|
+ # The context above performs the only commit in the final UoW.
|
|
|
|
|
+ return PublicationResult(
|
|
|
|
|
+ script_build_id=script_build_id,
|
|
|
|
|
+ publication_id=publication_id,
|
|
|
|
|
+ publication_type=PublicationType.FINAL,
|
|
|
|
|
+ state=PublicationState.PUBLISHED,
|
|
|
|
|
+ legacy_projection_digest=manifest.legacy_projection_digest,
|
|
|
|
|
+ committed=True,
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ async def _lock_branch_zero(self, session: AsyncSession, script_build_id: int) -> None:
|
|
|
|
|
+ for table in (
|
|
|
|
|
+ script_build_paragraph_element,
|
|
|
|
|
+ script_build_paragraph,
|
|
|
|
|
+ script_build_element,
|
|
|
|
|
+ ):
|
|
|
|
|
+ await session.execute(
|
|
|
|
|
+ select(table.c.id)
|
|
|
|
|
+ .where(table.c.script_build_id == script_build_id, table.c.branch_id == 0)
|
|
|
|
|
+ .order_by(table.c.id)
|
|
|
|
|
+ .with_for_update()
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ async def _insert_paragraphs(
|
|
|
|
|
+ self,
|
|
|
|
|
+ session: AsyncSession,
|
|
|
|
|
+ script_build_id: int,
|
|
|
|
|
+ script: StructuredScriptArtifactV1,
|
|
|
|
|
+ now: datetime,
|
|
|
|
|
+ ) -> dict[int, int]:
|
|
|
|
|
+ active = sorted(
|
|
|
|
|
+ (item for item in script.paragraphs if item.is_active),
|
|
|
|
|
+ key=lambda item: (item.level, item.paragraph_index, item.paragraph_id),
|
|
|
|
|
+ )
|
|
|
|
|
+ identifiers: dict[int, int] = {}
|
|
|
|
|
+ for item in active:
|
|
|
|
|
+ parent_id = identifiers.get(item.parent_id) if item.parent_id is not None else None
|
|
|
|
|
+ if item.parent_id is not None and parent_id is None:
|
|
|
|
|
+ raise ProtocolViolation("final paragraph parent is outside the accepted script")
|
|
|
|
|
+ result = await session.execute(
|
|
|
|
|
+ insert(script_build_paragraph).values(
|
|
|
|
|
+ script_build_id=script_build_id,
|
|
|
|
|
+ branch_id=0,
|
|
|
|
|
+ base_ref_id=None,
|
|
|
|
|
+ paragraph_index=item.paragraph_index,
|
|
|
|
|
+ level=item.level,
|
|
|
|
|
+ parent_id=parent_id,
|
|
|
|
|
+ name=item.name,
|
|
|
|
|
+ content_range=item.content_range,
|
|
|
|
|
+ theme=item.theme,
|
|
|
|
|
+ form=item.form,
|
|
|
|
|
+ function=item.function,
|
|
|
|
|
+ feeling=item.feeling,
|
|
|
|
|
+ theme_elements=list(item.theme_elements),
|
|
|
|
|
+ form_elements=list(item.form_elements),
|
|
|
|
|
+ function_elements=list(item.function_elements),
|
|
|
|
|
+ feeling_elements=list(item.feeling_elements),
|
|
|
|
|
+ description=item.description,
|
|
|
|
|
+ full_description=item.full_description,
|
|
|
|
|
+ is_active=True,
|
|
|
|
|
+ created_at=now,
|
|
|
|
|
+ updated_at=now,
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ identifiers[item.paragraph_id] = _inserted_id(result)
|
|
|
|
|
+ return identifiers
|
|
|
|
|
+
|
|
|
|
|
+ async def _insert_elements(
|
|
|
|
|
+ self,
|
|
|
|
|
+ session: AsyncSession,
|
|
|
|
|
+ script_build_id: int,
|
|
|
|
|
+ script: StructuredScriptArtifactV1,
|
|
|
|
|
+ now: datetime,
|
|
|
|
|
+ ) -> dict[int, int]:
|
|
|
|
|
+ identifiers: dict[int, int] = {}
|
|
|
|
|
+ for item in (value for value in script.elements if value.is_active):
|
|
|
|
|
+ result = await session.execute(
|
|
|
|
|
+ insert(script_build_element).values(
|
|
|
|
|
+ script_build_id=script_build_id,
|
|
|
|
|
+ branch_id=0,
|
|
|
|
|
+ base_ref_id=None,
|
|
|
|
|
+ name=item.name,
|
|
|
|
|
+ dimension_primary=item.dimension_primary,
|
|
|
|
|
+ dimension_secondary=item.dimension_secondary,
|
|
|
|
|
+ commonality_analysis=item.commonality_analysis,
|
|
|
|
|
+ topic_support=item.topic_support,
|
|
|
|
|
+ weight_score=item.weight_score,
|
|
|
|
|
+ support_elements=list(item.support_elements),
|
|
|
|
|
+ is_active=True,
|
|
|
|
|
+ created_at=now,
|
|
|
|
|
+ updated_at=now,
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ identifiers[item.element_id] = _inserted_id(result)
|
|
|
|
|
+ return identifiers
|
|
|
|
|
+
|
|
|
|
|
+ async def _insert_links(
|
|
|
|
|
+ self,
|
|
|
|
|
+ session: AsyncSession,
|
|
|
|
|
+ script_build_id: int,
|
|
|
|
|
+ script: StructuredScriptArtifactV1,
|
|
|
|
|
+ paragraph_ids: dict[int, int],
|
|
|
|
|
+ element_ids: dict[int, int],
|
|
|
|
|
+ ) -> None:
|
|
|
|
|
+ for item in script.paragraph_element_links:
|
|
|
|
|
+ paragraph_id = paragraph_ids.get(item.paragraph_id)
|
|
|
|
|
+ element_id = element_ids.get(item.element_id)
|
|
|
|
|
+ if paragraph_id is None or element_id is None:
|
|
|
|
|
+ raise ProtocolViolation("final paragraph-element link is dangling")
|
|
|
|
|
+ await session.execute(
|
|
|
|
|
+ insert(script_build_paragraph_element).values(
|
|
|
|
|
+ script_build_id=script_build_id,
|
|
|
|
|
+ branch_id=0,
|
|
|
|
|
+ paragraph_id=paragraph_id,
|
|
|
|
|
+ element_id=element_id,
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _publication(row: Any) -> Publication:
|
|
|
|
|
+ return Publication(
|
|
|
|
|
+ publication_id=int(row["id"]),
|
|
|
|
|
+ script_build_id=int(row["script_build_id"]),
|
|
|
|
|
+ publication_type=PublicationType(str(row["publication_type"])),
|
|
|
|
|
+ accept_decision_id=str(row["accept_decision_id"]),
|
|
|
|
|
+ artifact_version_id=int(row["artifact_version_id"]),
|
|
|
|
|
+ expected_sha256="sha256:" + str(row["expected_sha256"]),
|
|
|
|
|
+ state=PublicationState(str(row["state"])),
|
|
|
|
|
+ attempt_count=int(row["attempt_count"]),
|
|
|
|
|
+ publication_revision=int(row["publication_revision"]),
|
|
|
|
|
+ last_error_code=row["last_error_code"],
|
|
|
|
|
+ last_error_summary=row["last_error_summary"],
|
|
|
|
|
+ created_at=cast(datetime, row["created_at"]),
|
|
|
|
|
+ updated_at=cast(datetime, row["updated_at"]),
|
|
|
|
|
+ published_at=row["published_at"],
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _inserted_id(result: Any) -> int:
|
|
|
|
|
+ primary_key = cast(CursorResult[Any], result).inserted_primary_key
|
|
|
|
|
+ if not primary_key or primary_key[0] is None:
|
|
|
|
|
+ raise ProtocolViolation("database did not return a final projection identity")
|
|
|
|
|
+ return int(primary_key[0])
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+__all__ = ["SqlAlchemyFinalPublicationUnitOfWork"]
|