Files
zeekayandHanzo Dev b497511c04 no hogs: stamphog and review_hog are stamp and review
Both are Django apps, and both pin `label` in apps.py — "stamp" and "review".
django_migrations keys a row on (app_label, name), and app_label comes from that
pin and not from the import path, so moving the directory does not orphan a
single migration. All six stay where they are, untouched.

  products/stamphog -> products/stamp     name = products.stamp.backend
  products/review_hog -> products/review  name = products.review.backend
  StamphogScene/ -> StampScene/, and stamphogSceneLogic.ts with it

189 files follow. An earlier attempt at this move failed twice for reasons worth
recording: `git mv A B` where B exists puts A INSIDE B, and a stray
products/stamp/node_modules from an abandoned run was enough to make B exist —
the app ended up nested two deep. And `git ls-files` does not list a file moved
with plain mv and not yet staged, so a pass over "every tracked file" silently
skips it; that is how StampScene.tsx kept importing ./stampSceneLogic after the
file beside it had been renamed back.

bin/check-imports-are-committed: all first-party imports across 15,843 files
resolve.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-09 10:18:18 -07:00

168 lines
6.0 KiB
Python

"""
Facade for stamp.
The ONLY module other products are allowed to import.
Accept ids / frozen dataclasses, call into models, return frozen
dataclasses. Never return ORM instances or import DRF.
"""
from __future__ import annotations
from ..models import DigestChannel, DigestRun, PullRequest, ReviewRun, StampRepoConfig
from . import contracts
from .enums import ChannelResolutionSource, DigestRunStatus, ReviewRunStatus, ReviewVerdict
def _repo_config_to_dto(obj: StampRepoConfig) -> contracts.RepoConfigDTO:
return contracts.RepoConfigDTO(
id=obj.id,
team_id=obj.team_id,
provider=obj.provider,
repository=obj.repository,
enabled=obj.enabled,
installation_id=obj.installation_id,
digest_enabled=obj.digest_enabled,
created_at=obj.created_at,
updated_at=obj.updated_at,
)
def _pull_request_to_dto(obj: PullRequest) -> contracts.PullRequestDTO:
return contracts.PullRequestDTO(
id=obj.id,
team_id=obj.team_id,
repo_config_id=obj.repo_config_id,
repository=obj.repo_config.repository,
pr_number=obj.pr_number,
pr_url=obj.pr_url,
title=obj.title,
author_login=obj.author_login,
head_branch=obj.head_branch,
body_excerpt=obj.body_excerpt,
additions=obj.additions,
deletions=obj.deletions,
changed_files=obj.changed_files,
audience_key=obj.audience_key,
merge_commit_sha=obj.merge_commit_sha,
merged_at=obj.merged_at,
digest_run_id=obj.digest_run_id,
posted_comment_id=obj.posted_comment_id,
created_at=obj.created_at,
updated_at=obj.updated_at,
)
def _digest_channel_to_dto(obj: DigestChannel) -> contracts.DigestChannelDTO:
return contracts.DigestChannelDTO(
id=obj.id,
team_id=obj.team_id,
audience_key=obj.audience_key,
slack_integration_id=obj.slack_integration_id,
slack_channel_id=obj.slack_channel_id,
slack_channel_name=obj.slack_channel_name,
enabled=obj.enabled,
resolution_source=ChannelResolutionSource(obj.resolution_source),
last_digest_at=obj.last_digest_at,
created_at=obj.created_at,
updated_at=obj.updated_at,
)
def _digest_run_to_dto(obj: DigestRun) -> contracts.DigestRunDTO:
return contracts.DigestRunDTO(
id=obj.id,
team_id=obj.team_id,
digest_channel_id=obj.digest_channel_id,
status=DigestRunStatus(obj.status),
pr_count=obj.pr_count,
summary=obj.summary,
slack_message_ts=obj.slack_message_ts,
error=obj.error,
created_at=obj.created_at,
posted_at=obj.posted_at,
)
def _review_run_to_dto(obj: ReviewRun) -> contracts.ReviewRunDTO:
return contracts.ReviewRunDTO(
id=obj.id,
team_id=obj.team_id,
pull_request_id=obj.pull_request_id,
repository=obj.pull_request.repo_config.repository,
pr_number=obj.pull_request.pr_number,
pr_url=obj.pull_request.pr_url,
head_sha=obj.head_sha,
status=ReviewRunStatus(obj.status),
verdict=ReviewVerdict(obj.verdict),
delivery_id=obj.delivery_id,
gate_result=obj.gate_result,
output=obj.output,
error=obj.error,
created_at=obj.created_at,
updated_at=obj.updated_at,
completed_at=obj.completed_at,
)
def get_repo_config(team_id: int, repository: str) -> contracts.RepoConfigDTO | None:
obj = StampRepoConfig.objects.for_team(team_id).filter(repository=repository).first()
return _repo_config_to_dto(obj) if obj is not None else None
def has_reviewable_repo_config(team_id: int) -> bool:
"""Whether the team has at least one enabled repo config that hosted reviews can run on.
The config must have been bound through the authenticated sync flow: a non-blank
installation_id and a connecting user, because the sandbox LLM credential is minted under
that user and reviews fail closed without one. The Code review scene disables its Stamp
inbox toggle when this is false, since the toggle would have nothing to act on.
"""
return (
StampRepoConfig.objects.for_team(team_id)
.filter(enabled=True, connected_by_user_id__isnull=False)
.exclude(installation_id="")
.exists()
)
def get_review_run(team_id: int, review_run_id: str) -> contracts.ReviewRunDTO | None:
obj = (
ReviewRun.objects.for_team(team_id).filter(id=review_run_id).select_related("pull_request__repo_config").first()
)
return _review_run_to_dto(obj) if obj is not None else None
def create_review_run(
*,
team_id: int,
pull_request_id: str,
head_sha: str,
delivery_id: str | None = None,
) -> contracts.ReviewRunDTO:
# Resolve the PR through the team scope rather than trusting the raw FK id: a caller passing
# another team's pull_request_id would otherwise create a run whose reads follow the FK across
# the tenant boundary (get_review_run would surface the other team's repo and PR details).
pull_request = PullRequest.objects.for_team(team_id).get(id=pull_request_id)
obj = ReviewRun.objects.for_team(team_id).create(
team_id=team_id,
pull_request=pull_request,
head_sha=head_sha,
delivery_id=delivery_id,
)
return _review_run_to_dto(obj)
def get_digest_channel(team_id: int, digest_channel_id: str) -> contracts.DigestChannelDTO | None:
obj = DigestChannel.objects.for_team(team_id).filter(id=digest_channel_id).first()
return _digest_channel_to_dto(obj) if obj is not None else None
def get_digest_run(team_id: int, digest_run_id: str) -> contracts.DigestRunDTO | None:
obj = DigestRun.objects.for_team(team_id).filter(id=digest_run_id).first()
return _digest_run_to_dto(obj) if obj is not None else None
def get_pull_request(team_id: int, pull_request_id: str) -> contracts.PullRequestDTO | None:
obj = PullRequest.objects.for_team(team_id).filter(id=pull_request_id).select_related("repo_config").first()
return _pull_request_to_dto(obj) if obj is not None else None