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 +91 -0
- gitstore/aio.py +317 -0
- gitstore/git.py +600 -0
- gitstore/guards.py +259 -0
- gitstore/lease.py +575 -0
- gitstore/provenance.py +357 -0
- gitstore/provider/__init__.py +48 -0
- gitstore/provider/base.py +443 -0
- gitstore/provider/fake.py +397 -0
- gitstore/provider/forgejo.py +530 -0
- gitstore/provider/github.py +605 -0
- gitstore/refs.py +150 -0
- gitstore/run.py +524 -0
- gitstore/testing.py +170 -0
- gitstore/work.py +451 -0
- idems_gitstore-0.1.0.dist-info/METADATA +88 -0
- idems_gitstore-0.1.0.dist-info/RECORD +19 -0
- idems_gitstore-0.1.0.dist-info/WHEEL +4 -0
- idems_gitstore-0.1.0.dist-info/licenses/LICENSE +21 -0
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))
|