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 ADDED
@@ -0,0 +1,42 @@
1
+ """Client for gitd: instant, secretless git checkouts for agent sandboxes."""
2
+
3
+ from .client import Gitd
4
+ from .connection import DEFAULT_SOCKET, SOCKET_VARIABLE
5
+ from .errors import GitdError
6
+ from .models import (
7
+ Access,
8
+ AuditEntry,
9
+ Description,
10
+ Info,
11
+ Layers,
12
+ PendingSubmodule,
13
+ Probe,
14
+ Progress,
15
+ Published,
16
+ PublishedRepository,
17
+ Refusal,
18
+ Strategy,
19
+ Submodule,
20
+ Workspace,
21
+ )
22
+
23
+ __all__ = [
24
+ "DEFAULT_SOCKET",
25
+ "SOCKET_VARIABLE",
26
+ "Access",
27
+ "AuditEntry",
28
+ "Description",
29
+ "Gitd",
30
+ "GitdError",
31
+ "Info",
32
+ "Layers",
33
+ "PendingSubmodule",
34
+ "Probe",
35
+ "Progress",
36
+ "Published",
37
+ "PublishedRepository",
38
+ "Refusal",
39
+ "Strategy",
40
+ "Submodule",
41
+ "Workspace",
42
+ ]
gitd/client.py ADDED
@@ -0,0 +1,124 @@
1
+ """The gitd client."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime
6
+ from typing import Any
7
+
8
+ from . import messages, methods
9
+ from .connection import Connection, OnProgress
10
+ from .models import AuditEntry, Description, Info, Probe, Published, Strategy, Workspace
11
+ from .timestamps import Timestamp
12
+
13
+
14
+ class Gitd:
15
+ """A gitd on its Unix socket. No call takes a credential: gitd reads and publishes with
16
+ the ones it keeps.
17
+
18
+ with Gitd() as gitd:
19
+ workspace = gitd.prepare("https://github.com/acme/web", "/var/lib/gitd/workspaces/agent-1")
20
+ """
21
+
22
+ def __init__(self, socket: str | None = None, *, timeout: float | None = None) -> None:
23
+ self.connection = Connection(socket, timeout)
24
+
25
+ def __enter__(self) -> Gitd:
26
+ return self
27
+
28
+ def __exit__(self, *exception: object) -> None:
29
+ self.close()
30
+
31
+ def close(self) -> None:
32
+ self.connection.close()
33
+
34
+ def info(self) -> Info:
35
+ """Which gitd answers, and how it makes workspaces here and why."""
36
+ return Info.from_message(self.connection.call(methods.INFO, messages.InfoRequest()))
37
+
38
+ def probe(self, url: str) -> Probe:
39
+ """Whether a repository can be read with the credentials gitd keeps, and its branches."""
40
+ return Probe.from_message(self.connection.call(methods.PROBE, messages.ProbeRequest(url=url)))
41
+
42
+ def prepare(
43
+ self,
44
+ url: str,
45
+ path: str,
46
+ *,
47
+ branch: str | None = None,
48
+ commit: str | None = None,
49
+ submodules: bool = False,
50
+ full_history: bool = False,
51
+ strategy: Strategy | None = None,
52
+ on_progress: OnProgress | None = None,
53
+ ) -> Workspace:
54
+ """Makes a workspace of a repository at a path under gitd's roots, at a commit, a
55
+ branch, or the default branch. strategy is a preference: gitd falls back to copy when
56
+ it cannot make one that way here."""
57
+ if commit:
58
+ revision = messages.Revision(commit=commit)
59
+ elif branch:
60
+ revision = messages.Revision(branch=branch)
61
+ else:
62
+ revision = None
63
+ request = messages.PrepareRequest(
64
+ url=url,
65
+ path=path,
66
+ revision=revision,
67
+ with_submodules=submodules,
68
+ full_history=full_history,
69
+ strategy=Strategy.preference(strategy),
70
+ )
71
+ return Workspace.from_message(path, self.connection.follow(methods.PREPARE, request, on_progress))
72
+
73
+ def update(
74
+ self,
75
+ path: str,
76
+ *,
77
+ branch: str | None = None,
78
+ submodules: bool = False,
79
+ on_progress: OnProgress | None = None,
80
+ ) -> Workspace:
81
+ """Brings a workspace to the tip of its branch, or of the branch given."""
82
+ request = messages.UpdateRequest(path=path, branch=branch or "", with_submodules=submodules)
83
+ return Workspace.from_message(path, self.connection.follow(methods.UPDATE, request, on_progress))
84
+
85
+ def checkout(
86
+ self,
87
+ path: str,
88
+ commit: str,
89
+ *,
90
+ submodules: bool = False,
91
+ on_progress: OnProgress | None = None,
92
+ ) -> Workspace:
93
+ """Moves a workspace to a commit, as a rollback does."""
94
+ request = messages.CheckoutRequest(path=path, commit=commit, with_submodules=submodules)
95
+ return Workspace.from_message(path, self.connection.follow(methods.CHECKOUT, request, on_progress))
96
+
97
+ def describe(self, path: str) -> Description:
98
+ """What a workspace is on disk, without the network."""
99
+ response = self.connection.call(methods.DESCRIBE, messages.DescribeRequest(path=path))
100
+ return Description.from_message(response)
101
+
102
+ def publish(self, path: str, branch: str) -> Published:
103
+ """Pushes what was committed in a workspace to a new branch, with the token gitd keeps."""
104
+ response = self.connection.call(methods.PUBLISH, messages.PublishRequest(path=path, branch=branch))
105
+ return Published.from_message(response)
106
+
107
+ def remove(self, path: str) -> None:
108
+ """Deletes a workspace; its pool stays."""
109
+ self.connection.call(methods.REMOVE, messages.RemoveRequest(path=path))
110
+
111
+ def audit(
112
+ self,
113
+ *,
114
+ since: datetime | None = None,
115
+ url: str | None = None,
116
+ limit: int | None = None,
117
+ ) -> list[AuditEntry]:
118
+ """The audit log: every publish, done or refused, newest first; from since on, of
119
+ the repository at url, at most limit of them (100 by default, never more than 1000)."""
120
+ request: Any = messages.AuditRequest(url=url or "", limit=limit or 0)
121
+ if since is not None:
122
+ Timestamp.fill(request.since, since)
123
+ response = self.connection.call(methods.AUDIT, request)
124
+ return [AuditEntry.from_message(entry) for entry in response.entries]
gitd/connection.py ADDED
@@ -0,0 +1,71 @@
1
+ """The channel to gitd's Unix socket, and how a call on it is made and read."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from collections.abc import Callable
7
+ from typing import Any
8
+
9
+ import grpc
10
+ from google.protobuf.message import Message
11
+
12
+ from .errors import GitdError
13
+ from .methods import Method
14
+ from .models import Progress
15
+
16
+ DEFAULT_SOCKET = "/run/gitd/gitd.sock"
17
+ SOCKET_VARIABLE = "GITD_SOCKET"
18
+
19
+ OnProgress = Callable[[Progress], None]
20
+
21
+
22
+ class Connection:
23
+ """A channel to gitd. The socket defaults to $GITD_SOCKET, then /run/gitd/gitd.sock; the
24
+ timeout, in seconds, bounds every call, which gitd otherwise bounds itself."""
25
+
26
+ def __init__(self, socket: str | None, timeout: float | None) -> None:
27
+ self.socket = socket or os.environ.get(SOCKET_VARIABLE) or DEFAULT_SOCKET
28
+ self.timeout = timeout
29
+ self.channel = grpc.insecure_channel(f"unix:{self.socket}")
30
+
31
+ def close(self) -> None:
32
+ self.channel.close()
33
+
34
+ @staticmethod
35
+ def serialize(message: Message) -> bytes:
36
+ return message.SerializeToString()
37
+
38
+ def call(self, method: Method, request: Message) -> Any:
39
+ """Makes a call that answers once."""
40
+ invoke: grpc.UnaryUnaryMultiCallable[Message, Any] = self.channel.unary_unary(
41
+ method.path,
42
+ request_serializer=self.serialize,
43
+ response_deserializer=method.response.FromString,
44
+ )
45
+ try:
46
+ return invoke(request, timeout=self.timeout)
47
+ except grpc.RpcError as error:
48
+ raise GitdError.from_rpc_error(error) from None
49
+
50
+ def follow(
51
+ self,
52
+ method: Method,
53
+ request: Message,
54
+ on_progress: OnProgress | None,
55
+ ) -> Any:
56
+ """Makes a call that streams its progress, and reads it to its result."""
57
+ invoke: grpc.UnaryStreamMultiCallable[Message, Any] = self.channel.unary_stream(
58
+ method.path,
59
+ request_serializer=self.serialize,
60
+ response_deserializer=method.response.FromString,
61
+ )
62
+ try:
63
+ for response in invoke(request, timeout=self.timeout):
64
+ event = response.WhichOneof("event")
65
+ if event == "progress" and on_progress is not None:
66
+ on_progress(Progress.from_message(response.progress))
67
+ elif event == "result":
68
+ return response.result
69
+ except grpc.RpcError as error:
70
+ raise GitdError.from_rpc_error(error) from None
71
+ raise GitdError("", "gitd ended the call without a result")