gitd 0.7.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.
- gitd/__init__.py +42 -0
- gitd/client.py +124 -0
- gitd/connection.py +71 -0
- gitd/contract.json +1291 -0
- gitd/contract.py +50 -0
- gitd/errors.py +36 -0
- gitd/messages.py +30 -0
- gitd/methods.py +32 -0
- gitd/models.py +285 -0
- gitd/timestamps.py +22 -0
- gitd-0.7.0.dist-info/METADATA +77 -0
- gitd-0.7.0.dist-info/RECORD +13 -0
- gitd-0.7.0.dist-info/WHEEL +4 -0
gitd/contract.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""gitd's contract, read at run time from contract.json, which `make generate` builds from
|
|
2
|
+
proto/gitd/v1/gitd.proto. Only protobuf's public API is used: no code is generated."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from importlib import resources
|
|
7
|
+
|
|
8
|
+
from google.protobuf import descriptor_pb2, descriptor_pool, json_format, message_factory
|
|
9
|
+
from google.protobuf.descriptor import EnumDescriptor
|
|
10
|
+
from google.protobuf.message import Message
|
|
11
|
+
|
|
12
|
+
PACKAGE = "gitd.v1"
|
|
13
|
+
CONTRACT_FILE = "contract.json"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Enumeration:
|
|
17
|
+
"""An enumeration of the contract, by the names and numbers of its values."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, descriptor: EnumDescriptor) -> None:
|
|
20
|
+
self.descriptor = descriptor
|
|
21
|
+
|
|
22
|
+
def number(self, name: str) -> int:
|
|
23
|
+
return self.descriptor.values_by_name[name].number
|
|
24
|
+
|
|
25
|
+
def name(self, number: int) -> str:
|
|
26
|
+
"""The value's name, or an empty one for a value newer than this client."""
|
|
27
|
+
value = self.descriptor.values_by_number.get(number)
|
|
28
|
+
return value.name if value else ""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Contract:
|
|
32
|
+
"""The messages and enumerations of gitd's API."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, pool: descriptor_pool.DescriptorPool) -> None:
|
|
35
|
+
self.pool = pool
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def load(cls) -> Contract:
|
|
39
|
+
text = resources.files(__package__).joinpath(CONTRACT_FILE).read_text(encoding="utf-8")
|
|
40
|
+
files = json_format.Parse(text, descriptor_pb2.FileDescriptorSet())
|
|
41
|
+
pool = descriptor_pool.DescriptorPool()
|
|
42
|
+
for file in files.file:
|
|
43
|
+
pool.Add(file)
|
|
44
|
+
return cls(pool)
|
|
45
|
+
|
|
46
|
+
def message(self, name: str) -> type[Message]:
|
|
47
|
+
return message_factory.GetMessageClass(self.pool.FindMessageTypeByName(f"{PACKAGE}.{name}"))
|
|
48
|
+
|
|
49
|
+
def enumeration(self, name: str) -> Enumeration:
|
|
50
|
+
return Enumeration(self.pool.FindEnumTypeByName(f"{PACKAGE}.{name}"))
|
gitd/errors.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""A refusal from gitd."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import grpc
|
|
8
|
+
from grpc_status import rpc_status
|
|
9
|
+
|
|
10
|
+
from . import messages
|
|
11
|
+
from .models import Refusal
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class GitdError(Exception):
|
|
15
|
+
"""A refusal from gitd: its reason (AUTH_REQUIRED, NOT_FOUND, PATH_REFUSED, BUSY, …) and
|
|
16
|
+
what git said. Tell users what happened from the reason, never by parsing the message."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, reason: str, said: str, code: grpc.StatusCode | None = None) -> None:
|
|
19
|
+
super().__init__(f"{reason}: {said}" if reason else said)
|
|
20
|
+
self.reason = reason
|
|
21
|
+
self.said = said
|
|
22
|
+
self.code = code
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def from_rpc_error(cls, error: grpc.RpcError) -> GitdError:
|
|
26
|
+
"""Reads the GitFailure gitd attaches to a refused call, if there is one."""
|
|
27
|
+
if not isinstance(error, grpc.Call):
|
|
28
|
+
return cls("", str(error))
|
|
29
|
+
status = rpc_status.from_call(error)
|
|
30
|
+
details = list(status.details) if status else []
|
|
31
|
+
for detail in details:
|
|
32
|
+
if detail.Is(messages.GitFailure.DESCRIPTOR):
|
|
33
|
+
failure: Any = messages.GitFailure()
|
|
34
|
+
detail.Unpack(failure)
|
|
35
|
+
return cls(Refusal.reason_of(failure), failure.git_said, error.code())
|
|
36
|
+
return cls("", error.details() or "", error.code())
|
gitd/messages.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""The messages and enumerations of gitd's contract that this client uses."""
|
|
2
|
+
|
|
3
|
+
from .contract import Contract
|
|
4
|
+
|
|
5
|
+
contract = Contract.load()
|
|
6
|
+
|
|
7
|
+
InfoRequest = contract.message("InfoRequest")
|
|
8
|
+
InfoResponse = contract.message("InfoResponse")
|
|
9
|
+
ProbeRequest = contract.message("ProbeRequest")
|
|
10
|
+
ProbeResponse = contract.message("ProbeResponse")
|
|
11
|
+
Revision = contract.message("Revision")
|
|
12
|
+
PrepareRequest = contract.message("PrepareRequest")
|
|
13
|
+
PrepareResponse = contract.message("PrepareResponse")
|
|
14
|
+
UpdateRequest = contract.message("UpdateRequest")
|
|
15
|
+
UpdateResponse = contract.message("UpdateResponse")
|
|
16
|
+
CheckoutRequest = contract.message("CheckoutRequest")
|
|
17
|
+
CheckoutResponse = contract.message("CheckoutResponse")
|
|
18
|
+
DescribeRequest = contract.message("DescribeRequest")
|
|
19
|
+
DescribeResponse = contract.message("DescribeResponse")
|
|
20
|
+
PublishRequest = contract.message("PublishRequest")
|
|
21
|
+
PublishResponse = contract.message("PublishResponse")
|
|
22
|
+
RemoveRequest = contract.message("RemoveRequest")
|
|
23
|
+
RemoveResponse = contract.message("RemoveResponse")
|
|
24
|
+
AuditRequest = contract.message("AuditRequest")
|
|
25
|
+
AuditResponse = contract.message("AuditResponse")
|
|
26
|
+
GitFailure = contract.message("GitFailure")
|
|
27
|
+
|
|
28
|
+
STRATEGY = contract.enumeration("Strategy")
|
|
29
|
+
ACCESS = contract.enumeration("ProbeResponse.Access")
|
|
30
|
+
REASON = contract.enumeration("GitFailure.Reason")
|
gitd/methods.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""The gitd methods this client calls, by their gRPC path and the message each answers with."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from google.protobuf.message import Message
|
|
8
|
+
|
|
9
|
+
from . import messages
|
|
10
|
+
from .contract import PACKAGE
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class Method:
|
|
15
|
+
service: str
|
|
16
|
+
name: str
|
|
17
|
+
response: type[Message]
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def path(self) -> str:
|
|
21
|
+
return f"/{PACKAGE}.{self.service}/{self.name}"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
INFO = Method("ServerService", "Info", messages.InfoResponse)
|
|
25
|
+
AUDIT = Method("ServerService", "Audit", messages.AuditResponse)
|
|
26
|
+
PROBE = Method("RepositoryService", "Probe", messages.ProbeResponse)
|
|
27
|
+
PREPARE = Method("WorkspaceService", "Prepare", messages.PrepareResponse)
|
|
28
|
+
UPDATE = Method("WorkspaceService", "Update", messages.UpdateResponse)
|
|
29
|
+
CHECKOUT = Method("WorkspaceService", "Checkout", messages.CheckoutResponse)
|
|
30
|
+
DESCRIBE = Method("WorkspaceService", "Describe", messages.DescribeResponse)
|
|
31
|
+
PUBLISH = Method("WorkspaceService", "Publish", messages.PublishResponse)
|
|
32
|
+
REMOVE = Method("WorkspaceService", "Remove", messages.RemoveResponse)
|
gitd/models.py
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
"""What gitd answers, as plain dataclasses, each built from the message gitd sends. The
|
|
2
|
+
messages are made from the contract at run time, so their fields are typed here as Any."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from enum import StrEnum
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from . import messages
|
|
12
|
+
from .timestamps import Timestamp
|
|
13
|
+
|
|
14
|
+
STRATEGY_PREFIX = "STRATEGY_"
|
|
15
|
+
ACCESS_PREFIX = "ACCESS_"
|
|
16
|
+
REASON_PREFIX = "REASON_"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Strategy(StrEnum):
|
|
20
|
+
"""How a workspace is made: a layer on a shared template, a block clone, or every file written."""
|
|
21
|
+
|
|
22
|
+
OVERLAY = "overlay"
|
|
23
|
+
REFLINK = "reflink"
|
|
24
|
+
COPY = "copy"
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def from_message(cls, number: int) -> Strategy:
|
|
28
|
+
name = messages.STRATEGY.name(number).removeprefix(STRATEGY_PREFIX).lower()
|
|
29
|
+
return cls(name) if name in {member.value for member in cls} else cls.COPY
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def preference(cls, strategy: Strategy | None) -> int:
|
|
33
|
+
"""The number a request carries: the strategy asked for, or none, for gitd to choose."""
|
|
34
|
+
name = strategy.value.upper() if strategy else "UNSPECIFIED"
|
|
35
|
+
return messages.STRATEGY.number(STRATEGY_PREFIX + name)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Access(StrEnum):
|
|
39
|
+
"""Whether a repository can be read: by anyone, with a credential gitd keeps, or not at all."""
|
|
40
|
+
|
|
41
|
+
PUBLIC = "public"
|
|
42
|
+
REACHABLE = "reachable"
|
|
43
|
+
DENIED = "denied"
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def from_message(cls, number: int) -> Access:
|
|
47
|
+
name = messages.ACCESS.name(number).removeprefix(ACCESS_PREFIX).lower()
|
|
48
|
+
return cls(name) if name in {member.value for member in cls} else cls.DENIED
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True)
|
|
52
|
+
class Progress:
|
|
53
|
+
"""How far a fetch or checkout has gone; repository is the submodule's URL, empty for the
|
|
54
|
+
workspace's own repository."""
|
|
55
|
+
|
|
56
|
+
phase: str
|
|
57
|
+
percent: int
|
|
58
|
+
repository: str
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
def from_message(cls, progress: Any) -> Progress:
|
|
62
|
+
return cls(phase=progress.phase, percent=progress.percent, repository=progress.repository)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(frozen=True)
|
|
66
|
+
class Layers:
|
|
67
|
+
"""An overlay workspace's template (lower) and its own layer (upper, work)."""
|
|
68
|
+
|
|
69
|
+
lower: str
|
|
70
|
+
upper: str
|
|
71
|
+
work: str
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def from_message(cls, layers: Any) -> Layers:
|
|
75
|
+
return cls(lower=layers.lower, upper=layers.upper, work=layers.work)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass(frozen=True)
|
|
79
|
+
class PendingSubmodule:
|
|
80
|
+
"""A submodule that could not be checked out, and what git said."""
|
|
81
|
+
|
|
82
|
+
path: str
|
|
83
|
+
url: str
|
|
84
|
+
reason: str
|
|
85
|
+
|
|
86
|
+
@classmethod
|
|
87
|
+
def from_message(cls, submodule: Any) -> PendingSubmodule:
|
|
88
|
+
return cls(path=submodule.path, url=submodule.url, reason=submodule.failure.git_said)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass(frozen=True)
|
|
92
|
+
class Workspace:
|
|
93
|
+
"""Where a workspace ended up. branch is empty when it is detached at a commit;
|
|
94
|
+
lfs_patterns are the files the repository keeps in git LFS, which are pointers here, as
|
|
95
|
+
gitd does not download them."""
|
|
96
|
+
|
|
97
|
+
path: str
|
|
98
|
+
commit: str
|
|
99
|
+
branch: str
|
|
100
|
+
strategy: Strategy
|
|
101
|
+
layers: Layers | None = None
|
|
102
|
+
pending_submodules: list[PendingSubmodule] = field(default_factory=list)
|
|
103
|
+
lfs_patterns: list[str] = field(default_factory=list)
|
|
104
|
+
|
|
105
|
+
@classmethod
|
|
106
|
+
def from_message(cls, path: str, result: Any) -> Workspace:
|
|
107
|
+
return cls(
|
|
108
|
+
path=path,
|
|
109
|
+
commit=result.commit,
|
|
110
|
+
branch=result.branch,
|
|
111
|
+
strategy=Strategy.from_message(result.strategy),
|
|
112
|
+
layers=Layers.from_message(result.layers) if result.HasField("layers") else None,
|
|
113
|
+
pending_submodules=[
|
|
114
|
+
PendingSubmodule.from_message(pending) for pending in result.pending_submodules
|
|
115
|
+
],
|
|
116
|
+
lfs_patterns=list(result.lfs_patterns),
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@dataclass(frozen=True)
|
|
121
|
+
class Submodule:
|
|
122
|
+
path: str
|
|
123
|
+
url: str
|
|
124
|
+
commit: str
|
|
125
|
+
checked_out: bool
|
|
126
|
+
|
|
127
|
+
@classmethod
|
|
128
|
+
def from_message(cls, submodule: Any) -> Submodule:
|
|
129
|
+
return cls(
|
|
130
|
+
path=submodule.path,
|
|
131
|
+
url=submodule.url,
|
|
132
|
+
commit=submodule.commit,
|
|
133
|
+
checked_out=submodule.checked_out,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@dataclass(frozen=True)
|
|
138
|
+
class Description:
|
|
139
|
+
"""What a workspace is on disk. modified says whether it has uncommitted changes;
|
|
140
|
+
lfs_patterns are the files kept in git LFS, which are pointers here."""
|
|
141
|
+
|
|
142
|
+
url: str
|
|
143
|
+
commit: str
|
|
144
|
+
branch: str
|
|
145
|
+
modified: bool
|
|
146
|
+
strategy: Strategy
|
|
147
|
+
submodules: list[Submodule] = field(default_factory=list)
|
|
148
|
+
lfs_patterns: list[str] = field(default_factory=list)
|
|
149
|
+
|
|
150
|
+
@classmethod
|
|
151
|
+
def from_message(cls, response: Any) -> Description:
|
|
152
|
+
return cls(
|
|
153
|
+
url=response.url,
|
|
154
|
+
commit=response.commit,
|
|
155
|
+
branch=response.branch,
|
|
156
|
+
modified=response.modified,
|
|
157
|
+
strategy=Strategy.from_message(response.strategy),
|
|
158
|
+
submodules=[Submodule.from_message(submodule) for submodule in response.submodules],
|
|
159
|
+
lfs_patterns=list(response.lfs_patterns),
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@dataclass(frozen=True)
|
|
164
|
+
class PublishedRepository:
|
|
165
|
+
"""A repository a publish pushed. path is the submodule's, empty for the workspace's own
|
|
166
|
+
repository; subjects are the new commits' subjects, newest first."""
|
|
167
|
+
|
|
168
|
+
url: str
|
|
169
|
+
path: str
|
|
170
|
+
commit: str
|
|
171
|
+
subjects: list[str]
|
|
172
|
+
|
|
173
|
+
@classmethod
|
|
174
|
+
def from_message(cls, repository: Any) -> PublishedRepository:
|
|
175
|
+
return cls(
|
|
176
|
+
url=repository.url,
|
|
177
|
+
path=repository.path,
|
|
178
|
+
commit=repository.commit,
|
|
179
|
+
subjects=list(repository.subjects),
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@dataclass(frozen=True)
|
|
184
|
+
class Refusal:
|
|
185
|
+
"""Why gitd refused a call: its reason (BRANCH_REFUSED, AUTH_REQUIRED, …) and what git said."""
|
|
186
|
+
|
|
187
|
+
reason: str
|
|
188
|
+
said: str
|
|
189
|
+
|
|
190
|
+
@classmethod
|
|
191
|
+
def from_message(cls, failure: Any) -> Refusal:
|
|
192
|
+
return cls(reason=cls.reason_of(failure), said=failure.git_said)
|
|
193
|
+
|
|
194
|
+
@staticmethod
|
|
195
|
+
def reason_of(failure: Any) -> str:
|
|
196
|
+
return messages.REASON.name(failure.reason).removeprefix(REASON_PREFIX)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@dataclass(frozen=True)
|
|
200
|
+
class Published:
|
|
201
|
+
"""What a publish pushed: the workspace's repository first, then each submodule."""
|
|
202
|
+
|
|
203
|
+
branch: str
|
|
204
|
+
repositories: list[PublishedRepository]
|
|
205
|
+
|
|
206
|
+
@classmethod
|
|
207
|
+
def from_message(cls, response: Any) -> Published:
|
|
208
|
+
pushed = [response.repository, *response.submodules] if response.HasField("repository") else []
|
|
209
|
+
return cls(
|
|
210
|
+
branch=response.branch,
|
|
211
|
+
repositories=[PublishedRepository.from_message(repository) for repository in pushed],
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@dataclass(frozen=True)
|
|
216
|
+
class Info:
|
|
217
|
+
"""Which gitd answers, and how it makes workspaces on its machine and why."""
|
|
218
|
+
|
|
219
|
+
version: str
|
|
220
|
+
git_version: str
|
|
221
|
+
pools: int
|
|
222
|
+
pools_bytes: int
|
|
223
|
+
templates: int
|
|
224
|
+
templates_bytes: int
|
|
225
|
+
strategy: Strategy
|
|
226
|
+
strategy_reason: str
|
|
227
|
+
|
|
228
|
+
@classmethod
|
|
229
|
+
def from_message(cls, response: Any) -> Info:
|
|
230
|
+
return cls(
|
|
231
|
+
version=response.version,
|
|
232
|
+
git_version=response.git_version,
|
|
233
|
+
pools=response.pools,
|
|
234
|
+
pools_bytes=response.pools_bytes,
|
|
235
|
+
templates=response.templates,
|
|
236
|
+
templates_bytes=response.templates_bytes,
|
|
237
|
+
strategy=Strategy.from_message(response.strategy),
|
|
238
|
+
strategy_reason=response.strategy_reason,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
@dataclass(frozen=True)
|
|
243
|
+
class Probe:
|
|
244
|
+
"""Whether a repository can be read, its default branch, and each branch's commit.
|
|
245
|
+
denied says why it cannot be read, when it cannot."""
|
|
246
|
+
|
|
247
|
+
access: Access
|
|
248
|
+
default_branch: str
|
|
249
|
+
branches: dict[str, str]
|
|
250
|
+
denied: str | None = None
|
|
251
|
+
|
|
252
|
+
@classmethod
|
|
253
|
+
def from_message(cls, response: Any) -> Probe:
|
|
254
|
+
return cls(
|
|
255
|
+
access=Access.from_message(response.access),
|
|
256
|
+
default_branch=response.default_branch,
|
|
257
|
+
branches=dict(response.commits),
|
|
258
|
+
denied=response.failure.git_said if response.HasField("failure") else None,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
@dataclass(frozen=True)
|
|
263
|
+
class AuditEntry:
|
|
264
|
+
"""One publish, done or refused, as the audit log keeps it. url is the workspace's
|
|
265
|
+
repository, empty when the workspace could not be opened; repositories is what was
|
|
266
|
+
pushed, the workspace's repository first, empty when refused."""
|
|
267
|
+
|
|
268
|
+
time: datetime
|
|
269
|
+
workspace: str
|
|
270
|
+
url: str
|
|
271
|
+
branch: str
|
|
272
|
+
repositories: list[PublishedRepository] = field(default_factory=list)
|
|
273
|
+
refused: Refusal | None = None
|
|
274
|
+
|
|
275
|
+
@classmethod
|
|
276
|
+
def from_message(cls, entry: Any) -> AuditEntry:
|
|
277
|
+
pushed = [entry.repository, *entry.submodules] if entry.HasField("repository") else []
|
|
278
|
+
return cls(
|
|
279
|
+
time=Timestamp.to_datetime(entry.time),
|
|
280
|
+
workspace=entry.workspace,
|
|
281
|
+
url=entry.url,
|
|
282
|
+
branch=entry.branch,
|
|
283
|
+
repositories=[PublishedRepository.from_message(repository) for repository in pushed],
|
|
284
|
+
refused=Refusal.from_message(entry.refusal) if entry.HasField("refusal") else None,
|
|
285
|
+
)
|
gitd/timestamps.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Between the contract's google.protobuf.Timestamp and Python's datetime."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import UTC, datetime
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
NANOSECONDS_PER_SECOND = 1_000_000_000
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Timestamp:
|
|
12
|
+
"""A Timestamp message read as a datetime, or written from one, always in UTC."""
|
|
13
|
+
|
|
14
|
+
@staticmethod
|
|
15
|
+
def to_datetime(message: Any) -> datetime:
|
|
16
|
+
return datetime.fromtimestamp(message.seconds + message.nanos / NANOSECONDS_PER_SECOND, tz=UTC)
|
|
17
|
+
|
|
18
|
+
@staticmethod
|
|
19
|
+
def fill(message: Any, moment: datetime) -> None:
|
|
20
|
+
seconds = moment.timestamp()
|
|
21
|
+
message.seconds = int(seconds // 1)
|
|
22
|
+
message.nanos = int(round((seconds % 1) * NANOSECONDS_PER_SECOND))
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: gitd
|
|
3
|
+
Version: 0.7.0
|
|
4
|
+
Summary: Client for gitd: instant, secretless git checkouts for agent sandboxes
|
|
5
|
+
Project-URL: Homepage, https://github.com/dooservice/gitd
|
|
6
|
+
Project-URL: Source, https://github.com/dooservice/gitd/tree/main/sdk/python
|
|
7
|
+
License-Expression: Apache-2.0
|
|
8
|
+
Keywords: ai-agents,git,grpc,sandbox,workspace
|
|
9
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Topic :: Software Development :: Version Control :: Git
|
|
12
|
+
Requires-Python: >=3.11
|
|
13
|
+
Requires-Dist: grpcio-status>=1.84.0
|
|
14
|
+
Requires-Dist: grpcio>=1.84.0
|
|
15
|
+
Requires-Dist: protobuf>=7.36.2
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# gitd for Python
|
|
19
|
+
|
|
20
|
+
A client for [gitd](https://github.com/dooservice/gitd): instant, secretless git checkouts
|
|
21
|
+
for agent sandboxes. Your agents work in their own copy and publish a branch; they never
|
|
22
|
+
see your token.
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
pip install gitd
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from gitd import Gitd, GitdError
|
|
30
|
+
|
|
31
|
+
with Gitd() as gitd: # $GITD_SOCKET, then /run/gitd/gitd.sock
|
|
32
|
+
workspace = gitd.prepare("https://github.com/acme/web", "/var/lib/gitd/workspaces/agent-1")
|
|
33
|
+
print(workspace.path, workspace.commit, workspace.strategy)
|
|
34
|
+
|
|
35
|
+
# … the agent works in workspace.path with plain git, and commits …
|
|
36
|
+
|
|
37
|
+
published = gitd.publish(workspace.path, "agents/fix-login")
|
|
38
|
+
gitd.remove(workspace.path)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
No call takes a credential: gitd reads and publishes with the ones it keeps.
|
|
42
|
+
|
|
43
|
+
| Method | What it does |
|
|
44
|
+
|---|---|
|
|
45
|
+
| `prepare(url, path, *, branch, commit, submodules, full_history, strategy, on_progress)` | A workspace of a repository at a path under gitd's roots |
|
|
46
|
+
| `update(path, *, branch, submodules, on_progress)` | To the tip of its branch, or of the branch given |
|
|
47
|
+
| `checkout(path, commit, *, submodules, on_progress)` | To a commit, as a rollback does |
|
|
48
|
+
| `describe(path)` | Its repository, commit, branch, uncommitted changes and submodules |
|
|
49
|
+
| `publish(path, branch)` | Its commits pushed to a new branch |
|
|
50
|
+
| `remove(path)` | Deleted; its pool stays |
|
|
51
|
+
| `probe(url)` | Whether it can be read, its default branch and branches |
|
|
52
|
+
| `info()` | Which gitd answers, and how it makes workspaces here and why |
|
|
53
|
+
| `audit(*, since, url, limit)` | Every publish, done or refused, newest first |
|
|
54
|
+
|
|
55
|
+
A refusal raises `GitdError`, with `reason` (`AUTH_REQUIRED`, `NOT_FOUND`, `PATH_REFUSED`,
|
|
56
|
+
`BRANCH_REFUSED`, `BUSY`, …) and `said`, what git said. Decide from the reason, never from
|
|
57
|
+
the message.
|
|
58
|
+
|
|
59
|
+
[`examples/agent.py`](examples/agent.py) is an agent's whole round trip.
|
|
60
|
+
|
|
61
|
+
## Layout
|
|
62
|
+
|
|
63
|
+
| Path | What it is |
|
|
64
|
+
|---|---|
|
|
65
|
+
| `src/gitd/client.py` | `Gitd`, the client |
|
|
66
|
+
| `src/gitd/models.py` | What gitd answers, as dataclasses |
|
|
67
|
+
| `src/gitd/errors.py` | `GitdError` |
|
|
68
|
+
| `src/gitd/timestamps.py` | Between the contract's timestamps and `datetime` |
|
|
69
|
+
| `src/gitd/connection.py` | The channel to the socket, and how calls are made |
|
|
70
|
+
| `src/gitd/methods.py` | The gitd methods called, by their gRPC path |
|
|
71
|
+
| `src/gitd/messages.py` | The messages of the contract the client uses |
|
|
72
|
+
| `src/gitd/contract.py` | Reads the contract at run time, with protobuf's public API |
|
|
73
|
+
| `src/gitd/contract.json` | The contract, built from [`proto/gitd/v1/gitd.proto`](../../proto/gitd/v1/gitd.proto) by `make generate` |
|
|
74
|
+
|
|
75
|
+
No code is generated: the client reads gitd's contract when it starts.
|
|
76
|
+
|
|
77
|
+
Python 3.11 or later, on Linux. Apache-2.0.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
gitd/__init__.py,sha256=ryECu7i_jkjA4sn0A3mqEKVNN-iLlSSafMJ_Ei153WE,753
|
|
2
|
+
gitd/client.py,sha256=J0eLNka23N4F1kd_zbV5FmyEZUPr9GXWHOMyp5w3mcY,4831
|
|
3
|
+
gitd/connection.py,sha256=gAQl4Ov3CIIHdmmrZ0vXAOCH6S0VjXOjPFT-TNDgJLY,2548
|
|
4
|
+
gitd/contract.json,sha256=_mw_aEfMoU77syjO0EPDEYpdHF1pxiIu4dpdvIi9IZs,35828
|
|
5
|
+
gitd/contract.py,sha256=_hvrks2Lu4Ugcp53aYSxiO83n8EsaoA2BTqmF7r5lPc,1826
|
|
6
|
+
gitd/errors.py,sha256=QU8NWOyfh5Z6I4irwUds6c6DsouRWDyB_sERdOvGK0M,1326
|
|
7
|
+
gitd/messages.py,sha256=oqI_PyUJ6YqBN3FcbB6u9Rcb5gLrW-wnwodCj0KpUi0,1299
|
|
8
|
+
gitd/methods.py,sha256=_coa3-ZugO2ejmHuEH9VlbMSbhY5ad1b_vln_NySNbA,1105
|
|
9
|
+
gitd/models.py,sha256=Xs-wN0tOf5DSHPdpAjfqH98rLFvNr2Kilra1IvjL5rk,8918
|
|
10
|
+
gitd/timestamps.py,sha256=mroL2g5H6WN5gWxSCrtGG-xo5sznqsFrB5-LUYZ2xIY,717
|
|
11
|
+
gitd-0.7.0.dist-info/METADATA,sha256=UMQ1AhNTl2R18LKFZmVe0lVrwTN-WNEd9NWpaVyQu2E,3384
|
|
12
|
+
gitd-0.7.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
13
|
+
gitd-0.7.0.dist-info/RECORD,,
|