gabriel / musehub public
bridge_service.py python
104 lines 3.3 KB
Raw
sha256:c2e5cf2d754ed2fe9a94e879d41ad12ff221d2ac4ac7b45247fcb981c76858bf docs: Section 8 database/migrations/backups — verified live… Sonnet 5 28 days ago
1 """Bridge mirror service — persistence adapter for musehub_bridge_mirrors.
2
3 This module is the sole point of DB access for the bridge mirror registry.
4 Route handlers delegate here; no business logic lives in routes.
5
6 Boundary rules:
7 - Must NOT import state stores, SSE queues, or LLM clients.
8 - May import ORM models from musehub.db domain-specific modules.
9 - May import Pydantic models from musehub.models.bridge.
10 """
11
12 import logging
13 from datetime import datetime, timezone
14
15 from sqlalchemy import select
16 from sqlalchemy.exc import IntegrityError
17 from sqlalchemy.ext.asyncio import AsyncSession
18
19 from musehub.core.genesis import compute_bridge_mirror_id
20 from musehub.db.musehub_repo_models import MusehubBridgeMirror
21 from musehub.models.bridge import CreateMirrorRequest
22
23 logger = logging.getLogger(__name__)
24
25 def _utc_now() -> datetime:
26 return datetime.now(tz=timezone.utc)
27
28 async def create_mirror(
29 session: AsyncSession,
30 repo_id: str,
31 request: CreateMirrorRequest,
32 created_by: str,
33 ) -> MusehubBridgeMirror:
34 """Create a new bridge mirror registration for a repo.
35
36 Raises ``IntegrityError`` (409 at the route layer) when the same
37 ``git_remote_url`` is already registered for this ``repo_id``.
38 """
39 mirror = MusehubBridgeMirror(
40 id=compute_bridge_mirror_id(repo_id, request.git_remote_url),
41 repo_id=repo_id,
42 git_remote_url=request.git_remote_url,
43 git_branch=request.git_branch,
44 direction=request.direction,
45 auto_export=request.auto_export,
46 created_by=created_by,
47 )
48 session.add(mirror)
49 # Flush here so we propagate IntegrityError before the caller commits.
50 await session.flush()
51 return mirror
52
53 async def list_mirrors(
54 session: AsyncSession,
55 repo_id: str,
56 ) -> list[MusehubBridgeMirror]:
57 """Return all bridge mirrors registered for *repo_id*, ordered by creation time."""
58 result = await session.execute(
59 select(MusehubBridgeMirror)
60 .where(MusehubBridgeMirror.repo_id == repo_id)
61 .order_by(MusehubBridgeMirror.created_at)
62 )
63 return list(result.scalars().all())
64
65 async def delete_mirror(
66 session: AsyncSession,
67 repo_id: str,
68 mirror_id: str,
69 ) -> bool:
70 """Delete the mirror with *mirror_id* that belongs to *repo_id*.
71
72 Returns ``True`` when the row was deleted; ``False`` when it was not found.
73 """
74 result = await session.execute(
75 select(MusehubBridgeMirror).where(
76 MusehubBridgeMirror.id == mirror_id,
77 MusehubBridgeMirror.repo_id == repo_id,
78 )
79 )
80 mirror = result.scalar_one_or_none()
81 if mirror is None:
82 return False
83 await session.delete(mirror)
84 await session.flush()
85 return True
86
87 async def update_mirror_export_state(
88 session: AsyncSession,
89 mirror_id: str,
90 muse_commit_id: str,
91 git_sha: str,
92 ) -> None:
93 """Record a successful export operation on the given mirror."""
94 result = await session.execute(
95 select(MusehubBridgeMirror).where(MusehubBridgeMirror.id == mirror_id)
96 )
97 mirror = result.scalar_one_or_none()
98 if mirror is None:
99 logger.warning("update_mirror_export_state: mirror %s not found", mirror_id)
100 return
101 mirror.last_export_muse_commit_id = muse_commit_id
102 mirror.last_export_git_sha = git_sha
103 mirror.last_export_at = _utc_now()
104 await session.flush()
File History 2 commits
sha256:c2e5cf2d754ed2fe9a94e879d41ad12ff221d2ac4ac7b45247fcb981c76858bf docs: Section 8 database/migrations/backups — verified live… Sonnet 5 28 days ago
sha256:80e1a60a39562f6e616aaafbb27487f03abbec7d8ffc6164302b7a0c0bfc63ee docs: check off Section 0 items verified in inventory doc, … Sonnet 5 28 days ago