idems-gitstore 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
gitstore/__init__.py ADDED
@@ -0,0 +1,91 @@
1
+ """gitstore: git as the store.
2
+
3
+ Branches are units of work, pull requests are results, and history is the record.
4
+ See docs/design.md.
5
+
6
+ The operations live in their modules (`gitstore.git.clone`, `gitstore.refs.resolve_chain`,
7
+ ...); the names exported here are the types callers construct or catch.
8
+ """
9
+
10
+ from gitstore.git import Backoff, Git, GitError, Identity
11
+ from gitstore.guards import (
12
+ DefaultBranchError,
13
+ DestructiveChangeError,
14
+ Person,
15
+ Problem,
16
+ Problems,
17
+ )
18
+ from gitstore.lease import (
19
+ GitRefLeaseStore,
20
+ Heartbeat,
21
+ Lease,
22
+ LeaseError,
23
+ LeaseHeldError,
24
+ LeaseLostError,
25
+ LeaseStore,
26
+ LeaseUnavailableError,
27
+ MemoryLeaseStore,
28
+ )
29
+ from gitstore.provenance import Entry, Provenance, ProvenanceError
30
+ from gitstore.provider import GitProvider, ProviderError, ProviderResolver
31
+ from gitstore.refs import (
32
+ ChainCycleError,
33
+ ChainError,
34
+ ChainTooDeepError,
35
+ RepoRef,
36
+ ResolvedRef,
37
+ UnpinnedParentError,
38
+ )
39
+ from gitstore.run import Run, RunError
40
+ from gitstore.work import (
41
+ OpenUnit,
42
+ UnitExistsError,
43
+ UnitNotFoundError,
44
+ WorkError,
45
+ Workspace,
46
+ WorkUnit,
47
+ )
48
+
49
+ __version__ = "0.1.0"
50
+
51
+ __all__ = [
52
+ "Backoff",
53
+ "ChainCycleError",
54
+ "ChainError",
55
+ "ChainTooDeepError",
56
+ "DefaultBranchError",
57
+ "DestructiveChangeError",
58
+ "Entry",
59
+ "Git",
60
+ "GitError",
61
+ "GitProvider",
62
+ "GitRefLeaseStore",
63
+ "Heartbeat",
64
+ "Identity",
65
+ "Lease",
66
+ "LeaseError",
67
+ "LeaseHeldError",
68
+ "LeaseLostError",
69
+ "LeaseStore",
70
+ "LeaseUnavailableError",
71
+ "MemoryLeaseStore",
72
+ "OpenUnit",
73
+ "Person",
74
+ "Problem",
75
+ "Problems",
76
+ "Provenance",
77
+ "ProvenanceError",
78
+ "ProviderError",
79
+ "ProviderResolver",
80
+ "RepoRef",
81
+ "ResolvedRef",
82
+ "Run",
83
+ "RunError",
84
+ "UnitExistsError",
85
+ "UnitNotFoundError",
86
+ "UnpinnedParentError",
87
+ "WorkError",
88
+ "WorkUnit",
89
+ "Workspace",
90
+ "__version__",
91
+ ]
gitstore/aio.py ADDED
@@ -0,0 +1,317 @@
1
+ """Async wrappers over gitstore's synchronous core (decision #1).
2
+
3
+ Each wrapper runs the synchronous call in a worker thread with ``asyncio.to_thread`` and
4
+ adds nothing else: same arguments, same results, same exceptions. The logic stays in one
5
+ place, and an async caller holds a thread for the length of one call. Callbacks, such as
6
+ ``resolve_chain``'s ``parent_of``, run in that thread too. ``retry_rejected_push`` is the
7
+ exception: it takes a coroutine function, because the attempt it re-runs is the caller's own.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import asyncio
13
+ import functools
14
+ from collections.abc import Awaitable, Callable, Sequence
15
+ from datetime import timedelta
16
+ from pathlib import Path
17
+ from typing import TYPE_CHECKING, ParamSpec, TypeVar
18
+
19
+ from gitstore import git, guards
20
+ from gitstore import run as run_module
21
+ from gitstore.lease import DEFAULT_TTL, Lease, LeaseStore
22
+ from gitstore.provider.base import (
23
+ Capability,
24
+ GitProvider,
25
+ MergeMethod,
26
+ PullRequest,
27
+ )
28
+
29
+ if TYPE_CHECKING:
30
+ from gitstore.provenance import Actor
31
+ from gitstore.refs import ResolvedRef
32
+ from gitstore.run import Run
33
+ from gitstore.work import OpenUnit, Workspace, WorkUnit
34
+
35
+ P = ParamSpec("P")
36
+ T = TypeVar("T")
37
+
38
+ # ─── Provider ───────────────────────────────────────────────────────────────────────────────
39
+
40
+
41
+ class AsyncProvider:
42
+ """A ``GitProvider`` for async callers. ``sync`` is the provider it wraps.
43
+
44
+ ``kind`` and ``capabilities()`` are plain attributes of a provider already in hand, so
45
+ they stay synchronous. The draft operations exist whatever the wrapped provider
46
+ advertises; check ``capabilities()`` first, exactly as with the synchronous provider.
47
+ """
48
+
49
+ def __init__(self, sync: GitProvider) -> None:
50
+ self.sync = sync
51
+
52
+ @property
53
+ def kind(self) -> str:
54
+ return self.sync.kind
55
+
56
+ def capabilities(self) -> frozenset[Capability]:
57
+ return self.sync.capabilities()
58
+
59
+ async def get_pull_request(self, repo: str, number: int) -> PullRequest:
60
+ return await asyncio.to_thread(self.sync.get_pull_request, repo, number)
61
+
62
+ async def list_open_pull_requests(
63
+ self, repo: str, *, head_prefix: str = "", limit: int | None = None
64
+ ) -> list[PullRequest]:
65
+ return await asyncio.to_thread(
66
+ self.sync.list_open_pull_requests, repo, head_prefix=head_prefix, limit=limit
67
+ )
68
+
69
+ async def open_pull_request(
70
+ self, repo: str, *, head: str, base: str, title: str, body: str
71
+ ) -> PullRequest:
72
+ return await asyncio.to_thread(
73
+ self.sync.open_pull_request, repo, head=head, base=base, title=title, body=body
74
+ )
75
+
76
+ async def update_pull_request(
77
+ self,
78
+ repo: str,
79
+ number: int,
80
+ *,
81
+ title: str | None = None,
82
+ body: str | None = None,
83
+ base: str | None = None,
84
+ ) -> PullRequest:
85
+ return await asyncio.to_thread(
86
+ self.sync.update_pull_request, repo, number, title=title, body=body, base=base
87
+ )
88
+
89
+ async def close_pull_request(
90
+ self, repo: str, number: int, *, comment: str | None = None
91
+ ) -> PullRequest:
92
+ return await asyncio.to_thread(self.sync.close_pull_request, repo, number, comment=comment)
93
+
94
+ async def merge_pull_request(
95
+ self,
96
+ repo: str,
97
+ number: int,
98
+ *,
99
+ method: MergeMethod = "merge",
100
+ message: str | None = None,
101
+ expected_head: str | None = None,
102
+ ) -> PullRequest:
103
+ return await asyncio.to_thread(
104
+ self.sync.merge_pull_request,
105
+ repo,
106
+ number,
107
+ method=method,
108
+ message=message,
109
+ expected_head=expected_head,
110
+ )
111
+
112
+ async def open_draft_pull_request(
113
+ self, repo: str, *, head: str, base: str, title: str, body: str
114
+ ) -> PullRequest:
115
+ return await asyncio.to_thread(
116
+ self.sync.open_draft_pull_request, # type: ignore[attr-defined]
117
+ repo,
118
+ head=head,
119
+ base=base,
120
+ title=title,
121
+ body=body,
122
+ )
123
+
124
+ async def mark_ready(self, repo: str, number: int) -> PullRequest:
125
+ return await asyncio.to_thread(self.sync.mark_ready, repo, number) # type: ignore[attr-defined]
126
+
127
+ async def comment(self, repo: str, number: int, body: str) -> None:
128
+ await asyncio.to_thread(self.sync.comment, repo, number, body)
129
+
130
+ async def set_labels(self, repo: str, number: int, labels: Sequence[str]) -> tuple[str, ...]:
131
+ return await asyncio.to_thread(self.sync.set_labels, repo, number, labels)
132
+
133
+
134
+ # ─── Git ────────────────────────────────────────────────────────────────────────────────────
135
+
136
+
137
+ def _threaded(fn: Callable[P, T]) -> Callable[P, Awaitable[T]]:
138
+ @functools.wraps(fn)
139
+ async def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
140
+ return await asyncio.to_thread(fn, *args, **kwargs)
141
+
142
+ return wrapper
143
+
144
+
145
+ clone = _threaded(git.clone)
146
+ fetch = _threaded(git.fetch)
147
+ rev_parse = _threaded(git.rev_parse)
148
+ head = _threaded(git.head)
149
+ commit_exists = _threaded(git.commit_exists)
150
+ merge_base = _threaded(git.merge_base)
151
+ ls_remote = _threaded(git.ls_remote)
152
+ worktree_list = _threaded(git.worktree_list)
153
+ worktree_add = _threaded(git.worktree_add)
154
+ worktree_remove = _threaded(git.worktree_remove)
155
+ worktree_prune = _threaded(git.worktree_prune)
156
+ commit = _threaded(git.commit)
157
+ push = _threaded(git.push)
158
+ resolve_ref = _threaded(git.resolve_ref)
159
+ read_at = _threaded(git.read_at)
160
+ resolve_chain = _threaded(git.resolve_chain)
161
+
162
+
163
+ async def retry_rejected_push(attempt: Callable[[], Awaitable[T]]) -> T:
164
+ """As ``gitstore.git.retry_rejected_push``, for an attempt that is a coroutine function."""
165
+ try:
166
+ return await attempt()
167
+ except git.GitError as exc:
168
+ if not exc.rejected_push:
169
+ raise
170
+ return await attempt()
171
+
172
+
173
+ # ─── Leases and work units ──────────────────────────────────────────────────────────────────
174
+
175
+
176
+ class AsyncLeaseStore:
177
+ """A ``LeaseStore`` for async callers. ``sync`` is the store it wraps."""
178
+
179
+ def __init__(self, sync: LeaseStore) -> None:
180
+ self.sync = sync
181
+
182
+ async def holder(self, unit: str) -> Lease | None:
183
+ return await asyncio.to_thread(self.sync.holder, unit)
184
+
185
+ async def acquire(self, unit: str, holder: str, *, ttl: timedelta = DEFAULT_TTL) -> Lease:
186
+ return await asyncio.to_thread(self.sync.acquire, unit, holder, ttl=ttl)
187
+
188
+ async def renew(self, lease: Lease, *, ttl: timedelta = DEFAULT_TTL) -> Lease:
189
+ return await asyncio.to_thread(self.sync.renew, lease, ttl=ttl)
190
+
191
+ async def release(self, lease: Lease) -> None:
192
+ await asyncio.to_thread(self.sync.release, lease)
193
+
194
+
195
+ class AsyncWorkspace:
196
+ """A ``Workspace`` for async callers. ``sync`` is the workspace it wraps.
197
+
198
+ ``heartbeat`` stays synchronous: it builds a ``Heartbeat``, which renews on a thread
199
+ of its own and whose ``check()`` does no I/O.
200
+ """
201
+
202
+ def __init__(self, sync: Workspace) -> None:
203
+ self.sync = sync
204
+
205
+ def heartbeat(self, unit: WorkUnit, **kwargs):
206
+ return self.sync.heartbeat(unit, **kwargs)
207
+
208
+ async def create(self, name: str, *, base: str | None = None, lease: bool = True) -> WorkUnit:
209
+ return await asyncio.to_thread(self.sync.create, name, base=base, lease=lease)
210
+
211
+ async def publish(
212
+ self, unit: WorkUnit, *, title: str, body: str, draft: bool = True
213
+ ) -> WorkUnit:
214
+ return await asyncio.to_thread(self.sync.publish, unit, title=title, body=body, draft=draft)
215
+
216
+ async def discover(self, *, limit: int | None = None) -> list[OpenUnit]:
217
+ return await asyncio.to_thread(self.sync.discover, limit=limit)
218
+
219
+ async def resume(self, name: str, *, lease: bool = True) -> WorkUnit:
220
+ return await asyncio.to_thread(self.sync.resume, name, lease=lease)
221
+
222
+ async def release(self, unit: WorkUnit) -> None:
223
+ await asyncio.to_thread(self.sync.release, unit)
224
+
225
+ async def prune(self) -> list[Path]:
226
+ return await asyncio.to_thread(self.sync.prune)
227
+
228
+ async def abandon(self, unit: WorkUnit, *, comment: str | None = None) -> None:
229
+ await asyncio.to_thread(self.sync.abandon, unit, comment=comment)
230
+
231
+
232
+ # ─── Runs and guards ────────────────────────────────────────────────────────────────────────
233
+
234
+ check_deletions = _threaded(guards.check_deletions)
235
+ staged_deletions = _threaded(guards.staged_deletions)
236
+
237
+
238
+ class AsyncRun:
239
+ """A ``Run`` for async callers. ``sync`` is the run it wraps.
240
+
241
+ Its state (``pull_request``, ``problems``, ``status``, the commits) is read from
242
+ ``sync``. As a context manager, a run that raises before it finishes fails itself and
243
+ re-raises, as the synchronous one does.
244
+ """
245
+
246
+ def __init__(self, sync: Run) -> None:
247
+ self.sync = sync
248
+
249
+ async def write(
250
+ self,
251
+ path: str,
252
+ content: str | bytes,
253
+ *,
254
+ actor: Actor,
255
+ producer: str,
256
+ sources: Sequence[ResolvedRef] = (),
257
+ edit: bool = False,
258
+ note: str | None = None,
259
+ ) -> Path:
260
+ return await asyncio.to_thread(
261
+ self.sync.write,
262
+ path,
263
+ content,
264
+ actor=actor,
265
+ producer=producer,
266
+ sources=sources,
267
+ edit=edit,
268
+ note=note,
269
+ )
270
+
271
+ async def record(
272
+ self,
273
+ path: str,
274
+ *,
275
+ actor: Actor,
276
+ producer: str,
277
+ sources: Sequence[ResolvedRef] = (),
278
+ edit: bool = False,
279
+ note: str | None = None,
280
+ ) -> None:
281
+ await asyncio.to_thread(
282
+ self.sync.record,
283
+ path,
284
+ actor=actor,
285
+ producer=producer,
286
+ sources=sources,
287
+ edit=edit,
288
+ note=note,
289
+ )
290
+
291
+ async def remove(self, path: str) -> None:
292
+ await asyncio.to_thread(self.sync.remove, path)
293
+
294
+ async def commit_state(self, subject: str, body: str | None = None, **kwargs) -> str:
295
+ return await asyncio.to_thread(self.sync.commit_state, subject, body, **kwargs)
296
+
297
+ async def succeed(self, subject: str, body: str | None = None, **kwargs) -> str | None:
298
+ return await asyncio.to_thread(self.sync.succeed, subject, body, **kwargs)
299
+
300
+ async def fail(self, reason: str) -> None:
301
+ await asyncio.to_thread(self.sync.fail, reason)
302
+
303
+ async def __aenter__(self):
304
+ return self
305
+
306
+ async def __aexit__(self, exc_type, exc, tb) -> None:
307
+ await asyncio.to_thread(self.sync.__exit__, exc_type, exc, tb)
308
+
309
+
310
+ async def start_run(workspace: Workspace, **kwargs) -> AsyncRun:
311
+ """As ``gitstore.run.start_run``, with the same keyword arguments."""
312
+ return AsyncRun(await asyncio.to_thread(run_module.start_run, workspace, **kwargs))
313
+
314
+
315
+ async def resume_run(workspace: Workspace, run_id: str, **kwargs) -> AsyncRun:
316
+ """As ``gitstore.run.resume_run``, with the same keyword arguments."""
317
+ return AsyncRun(await asyncio.to_thread(run_module.resume_run, workspace, run_id, **kwargs))