matrix-builder / services /api /app /db /repository.py
ruslanmv
Deploy: metrics + docs (Batch 12)
22b729d
Raw
History Blame Contribute Delete
14.1 kB
"""Workflow persistence repository (Batch C1).
Thin data-access over the ORM for the Continuous Build tables. Every write stamps ``owner_id``
so row-level security isolates rows per user; callers run inside ``session_scope(user_id=...)``
so the same user id is set as the RLS GUC.
"""
from __future__ import annotations
import uuid
from sqlalchemy import func, select
from sqlalchemy.orm import Session
from app.db.orm import (
Artifact,
BuildBatch,
BundleVersion,
MatrixCommit,
Project,
PromptVersion,
RunEvent,
User,
ValidationFinding,
ValidationRun,
)
def _new_id() -> str:
return str(uuid.uuid4())
class WorkflowRepository:
def __init__(self, session: Session) -> None:
self.session = session
# --- users -------------------------------------------------------------
def upsert_user(self, user_id: str, *, email: str | None = None, display_name: str | None = None) -> User:
user = self.session.get(User, user_id)
if user is None:
user = User(id=user_id, email=email, display_name=display_name, plan="free")
self.session.add(user)
else:
if email is not None:
user.email = email
if display_name is not None:
user.display_name = display_name
self.session.flush()
return user
# --- upsert (Track L2 sync: insert-or-update by primary key) -----------
def upsert(self, obj):
merged = self.session.merge(obj)
self.session.flush()
return merged
# --- projects ----------------------------------------------------------
def create_project(self, *, owner_id: str, title: str, slug: str, description: str = "") -> Project:
project = Project(
id=_new_id(), owner_id=owner_id, title=title, slug=slug, description=description
)
self.session.add(project)
self.session.flush()
return project
def get_project(self, project_id: str) -> Project | None:
return self.session.get(Project, project_id)
def list_projects(self, owner_id: str) -> list[Project]:
# RLS already restricts to the current user; the filter is belt-and-suspenders.
return list(
self.session.scalars(select(Project).where(Project.owner_id == owner_id)).all()
)
# --- versions ----------------------------------------------------------
def create_version(
self,
*,
owner_id: str,
project_id: str,
version_label: str,
title: str,
requirements_md: str = "",
parent_version_id: str | None = None,
blueprint_artifact_id: str | None = None,
) -> BundleVersion:
version = BundleVersion(
id=_new_id(),
owner_id=owner_id,
project_id=project_id,
parent_version_id=parent_version_id,
version_label=version_label,
title=title,
requirements_md=requirements_md,
blueprint_artifact_id=blueprint_artifact_id,
created_by=owner_id,
)
self.session.add(version)
self.session.flush()
return version
def get_version(self, version_id: str) -> BundleVersion | None:
return self.session.get(BundleVersion, version_id)
def list_versions(self, project_id: str) -> list[BundleVersion]:
return list(
self.session.scalars(
select(BundleVersion)
.where(BundleVersion.project_id == project_id)
.order_by(BundleVersion.created_at)
).all()
)
def latest_version(self, project_id: str) -> BundleVersion | None:
versions = self.list_versions(project_id)
return versions[-1] if versions else None
# --- batches -----------------------------------------------------------
def create_batch(
self,
*,
owner_id: str,
version_id: str,
ordinal: int,
title: str,
goal_md: str,
change_type: str,
parent_commit_id: str | None = None,
) -> BuildBatch:
batch = BuildBatch(
id=_new_id(),
owner_id=owner_id,
version_id=version_id,
ordinal=ordinal,
title=title,
goal_md=goal_md,
change_type=change_type,
parent_commit_id=parent_commit_id,
requested_by=owner_id,
)
self.session.add(batch)
self.session.flush()
return batch
def get_batch(self, batch_id: str) -> BuildBatch | None:
return self.session.get(BuildBatch, batch_id)
def list_batches(self, version_id: str) -> list[BuildBatch]:
return list(
self.session.scalars(
select(BuildBatch)
.where(BuildBatch.version_id == version_id)
.order_by(BuildBatch.ordinal)
).all()
)
def set_batch_status(self, batch: BuildBatch, status: str) -> BuildBatch:
batch.status = status
self.session.flush()
return batch
def next_batch_ordinal(self, version_id: str) -> int:
current = self.session.scalar(
select(func.max(BuildBatch.ordinal)).where(BuildBatch.version_id == version_id)
)
return (current or 0) + 1
# --- prompt versions ---------------------------------------------------
def create_prompt_version(
self,
*,
owner_id: str,
batch_id: str,
coder: str,
prompt_text: str,
constraints: dict | None = None,
artifact_id: str | None = None,
) -> PromptVersion:
prompt = PromptVersion(
id=_new_id(),
owner_id=owner_id,
batch_id=batch_id,
coder=coder,
prompt_text=prompt_text,
constraints=constraints or {},
artifact_id=artifact_id,
)
self.session.add(prompt)
self.session.flush()
return prompt
def list_prompt_versions(self, batch_id: str) -> list[PromptVersion]:
return list(
self.session.scalars(
select(PromptVersion)
.where(PromptVersion.batch_id == batch_id)
.order_by(PromptVersion.created_at)
).all()
)
# --- commits -----------------------------------------------------------
def create_commit(
self,
*,
owner_id: str,
batch_id: str,
version_id: str,
commit_no: int,
tree_hash: str,
summary: str = "",
validation_status: str = "not-run",
parent_commit_id: str | None = None,
manifest: dict | None = None,
) -> MatrixCommit:
commit = MatrixCommit(
id=_new_id(),
owner_id=owner_id,
batch_id=batch_id,
version_id=version_id,
commit_no=commit_no,
tree_hash=tree_hash,
summary=summary,
validation_status=validation_status,
parent_commit_id=parent_commit_id,
manifest=manifest or {},
)
self.session.add(commit)
self.session.flush()
return commit
def get_commit(self, commit_id: str) -> MatrixCommit | None:
return self.session.get(MatrixCommit, commit_id)
def list_commits(self, version_id: str) -> list[MatrixCommit]:
return list(
self.session.scalars(
select(MatrixCommit)
.where(MatrixCommit.version_id == version_id)
.order_by(MatrixCommit.commit_no)
).all()
)
def latest_commit(self, version_id: str) -> MatrixCommit | None:
commits = self.list_commits(version_id)
return commits[-1] if commits else None
def next_commit_no(self, version_id: str) -> int:
current = self.session.scalar(
select(func.max(MatrixCommit.commit_no)).where(MatrixCommit.version_id == version_id)
)
return (current or 0) + 1
def set_commit_validation_status(self, commit: MatrixCommit, status: str) -> MatrixCommit:
commit.validation_status = status
self.session.flush()
return commit
# --- validation runs + findings ---------------------------------------
def create_validation_run(
self,
*,
owner_id: str,
commit_id: str | None = None,
batch_id: str | None = None,
status: str,
score: int | None = None,
runner: str = "local",
) -> ValidationRun:
run = ValidationRun(
id=_new_id(),
owner_id=owner_id,
commit_id=commit_id,
batch_id=batch_id,
status=status,
score=score,
runner=runner,
)
self.session.add(run)
self.session.flush()
return run
def get_validation_run(self, run_id: str) -> ValidationRun | None:
return self.session.get(ValidationRun, run_id)
def list_runs_for_version(self, version_id: str) -> list[ValidationRun]:
return list(
self.session.scalars(
select(ValidationRun)
.join(BuildBatch, BuildBatch.id == ValidationRun.batch_id)
.where(BuildBatch.version_id == version_id)
.order_by(ValidationRun.started_at)
).all()
)
def finalize_validation_run(
self,
run: ValidationRun,
*,
status: str,
score: int | None = None,
commit_id: str | None = None,
) -> ValidationRun:
from app.utils.time import utc_now
run.status = status
if score is not None:
run.score = score
if commit_id is not None:
run.commit_id = commit_id
run.ended_at = utc_now()
self.session.flush()
return run
# --- run events (append-only) -----------------------------------------
def append_run_event(
self,
*,
owner_id: str,
run_id: str,
event_type: str,
payload: dict | None = None,
) -> RunEvent:
next_seq = (
self.session.scalar(
select(func.max(RunEvent.seq)).where(RunEvent.run_id == run_id)
)
or 0
) + 1
event = RunEvent(
id=_new_id(),
owner_id=owner_id,
run_id=run_id,
seq=next_seq,
event_type=event_type,
payload=payload or {},
)
self.session.add(event)
self.session.flush()
return event
def list_run_events_after(self, run_id: str, after: int = 0) -> list[RunEvent]:
return list(
self.session.scalars(
select(RunEvent)
.where(RunEvent.run_id == run_id, RunEvent.seq > after)
.order_by(RunEvent.seq)
).all()
)
def create_validation_finding(
self,
*,
owner_id: str,
validation_run_id: str,
severity: str,
status: str,
check_name: str,
message: str,
file_path: str | None = None,
remediation: str | None = None,
) -> ValidationFinding:
finding = ValidationFinding(
id=_new_id(),
owner_id=owner_id,
validation_run_id=validation_run_id,
severity=severity,
status=status,
check_name=check_name,
message=message,
file_path=file_path,
remediation=remediation,
)
self.session.add(finding)
self.session.flush()
return finding
def list_findings(self, validation_run_id: str) -> list[ValidationFinding]:
return list(
self.session.scalars(
select(ValidationFinding).where(
ValidationFinding.validation_run_id == validation_run_id
)
).all()
)
# --- artifacts ---------------------------------------------------------
def create_artifact(
self,
*,
owner_id: str,
project_id: str,
artifact_type: str,
storage_key: str,
sha256: str,
size_bytes: int,
version_id: str | None = None,
commit_id: str | None = None,
) -> Artifact:
artifact = Artifact(
id=_new_id(),
owner_id=owner_id,
project_id=project_id,
version_id=version_id,
commit_id=commit_id,
artifact_type=artifact_type,
storage_key=storage_key,
sha256=sha256,
size_bytes=size_bytes,
)
self.session.add(artifact)
self.session.flush()
return artifact
def find_artifact_by_key(self, storage_key: str) -> Artifact | None:
return self.session.scalars(
select(Artifact).where(Artifact.storage_key == storage_key).limit(1)
).first()
def list_artifacts_for_commit(self, commit_id: str) -> list[Artifact]:
return list(
self.session.scalars(
select(Artifact).where(Artifact.commit_id == commit_id)
).all()
)
def persist_saved_bundle(
repo: WorkflowRepository,
*,
owner_id: str,
title: str,
slug: str,
requirements_md: str = "",
version_label: str = "v1.0.0",
) -> tuple[Project, BundleVersion]:
"""Save a guest bundle privately: create the owning project and its initial version.
This is what the "Save privately" action persists, so a saved bundle survives a restart.
"""
repo.upsert_user(owner_id)
project = repo.create_project(owner_id=owner_id, title=title, slug=slug)
version = repo.create_version(
owner_id=owner_id,
project_id=project.id,
version_label=version_label,
title=title,
requirements_md=requirements_md,
)
return project, version
__all__ = ["WorkflowRepository", "persist_saved_bundle"]