gitd-client 0.14.0__tar.gz

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.
@@ -0,0 +1,3 @@
1
+ __pycache__/
2
+ dist/
3
+ *.egg-info/
@@ -0,0 +1,92 @@
1
+ Metadata-Version: 2.5
2
+ Name: gitd-client
3
+ Version: 0.14.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 :: OS Independent
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-client
26
+ ```
27
+
28
+ It is imported as `gitd`. Up to 0.13.0 it was published as `gitd`; from 0.14.0 it is
29
+ `gitd-client`, the name every gitd client has.
30
+
31
+ ```python
32
+ from gitd import Gitd, GitdError
33
+
34
+ with Gitd() as gitd: # $GITD_SOCKET, then /run/gitd/gitd.sock
35
+ workspace = gitd.prepare("https://github.com/acme/web", "/var/lib/gitd/workspaces/agent-1")
36
+ print(workspace.path, workspace.commit, workspace.strategy)
37
+
38
+ # … the agent works in workspace.path with plain git, and commits …
39
+
40
+ published = gitd.publish(workspace.path, "agents/fix-login")
41
+ gitd.remove(workspace.path)
42
+ ```
43
+
44
+ No call takes a credential: gitd reads and publishes with the ones it keeps.
45
+
46
+ | Method | What it does |
47
+ |---|---|
48
+ | `prepare(url, path, *, branch, commit, submodules, full_history, strategy, on_progress)` | A workspace of a repository at a path under gitd's roots |
49
+ | `update(path, *, branch, submodules, on_progress)` | To the tip of its branch, or of the branch given |
50
+ | `checkout(path, commit, *, submodules, on_progress)` | To a commit, as a rollback does |
51
+ | `describe(path)` | Its repository, commit, branch, uncommitted changes and submodules |
52
+ | `publish(path, branch, *, requested_by, pull_request, provenance, sandbox, update)` | Its commits pushed to a new branch, or with `update` an earlier publish's branch moved forward; its pull request opened or updated |
53
+ | `remove(path)` | Deleted; its pool stays |
54
+ | `probe(url)` | Whether it can be read, its default branch and branches |
55
+ | `info()` | Which gitd answers, and how it makes workspaces here and why |
56
+ | `audit(*, since, url, limit)` | Every publish, done or refused, newest first |
57
+
58
+ ```python
59
+ from gitd import PullRequestOptions
60
+
61
+ published = gitd.publish(
62
+ workspace.path, "agents/fix-login", update=True,
63
+ pull_request=PullRequestOptions(open=True, submodules=True, title="Fix the login", body="…"),
64
+ )
65
+ print(published.pull_request.url, published.pull_request.updated)
66
+ for submodule in published.submodule_pull_requests: # merged first, with a merge commit
67
+ print(submodule.path, submodule.pull_request or submodule.refused)
68
+ ```
69
+
70
+ A refusal raises `GitdError`, with `reason` (`AUTH_REQUIRED`, `NOT_FOUND`, `PATH_REFUSED`,
71
+ `BRANCH_REFUSED`, `BUSY`, …) and `said`, what git said. Decide from the reason, never from
72
+ the message.
73
+
74
+ [`examples/agent.py`](examples/agent.py) is an agent's whole round trip.
75
+
76
+ ## Layout
77
+
78
+ | Path | What it is |
79
+ |---|---|
80
+ | `src/gitd/client.py` | `Gitd`, the client |
81
+ | `src/gitd/models.py` | What gitd answers, as dataclasses |
82
+ | `src/gitd/errors.py` | `GitdError` |
83
+ | `src/gitd/timestamps.py` | Between the contract's timestamps and `datetime` |
84
+ | `src/gitd/connection.py` | The channel to the socket, and how calls are made |
85
+ | `src/gitd/methods.py` | The gitd methods called, by their gRPC path |
86
+ | `src/gitd/messages.py` | The messages of the contract the client uses |
87
+ | `src/gitd/contract.py` | Reads the contract at run time, with protobuf's public API |
88
+ | `src/gitd/contract.json` | The contract, built from [`proto/gitd/v1/gitd.proto`](../../proto/gitd/v1/gitd.proto) by `make generate` |
89
+
90
+ No code is generated: the client reads gitd's contract when it starts.
91
+
92
+ Python 3.11 or later, on Linux, macOS and Windows. Apache-2.0.
@@ -0,0 +1,75 @@
1
+ # gitd for Python
2
+
3
+ A client for [gitd](https://github.com/dooservice/gitd): instant, secretless git checkouts
4
+ for agent sandboxes. Your agents work in their own copy and publish a branch; they never
5
+ see your token.
6
+
7
+ ```sh
8
+ pip install gitd-client
9
+ ```
10
+
11
+ It is imported as `gitd`. Up to 0.13.0 it was published as `gitd`; from 0.14.0 it is
12
+ `gitd-client`, the name every gitd client has.
13
+
14
+ ```python
15
+ from gitd import Gitd, GitdError
16
+
17
+ with Gitd() as gitd: # $GITD_SOCKET, then /run/gitd/gitd.sock
18
+ workspace = gitd.prepare("https://github.com/acme/web", "/var/lib/gitd/workspaces/agent-1")
19
+ print(workspace.path, workspace.commit, workspace.strategy)
20
+
21
+ # … the agent works in workspace.path with plain git, and commits …
22
+
23
+ published = gitd.publish(workspace.path, "agents/fix-login")
24
+ gitd.remove(workspace.path)
25
+ ```
26
+
27
+ No call takes a credential: gitd reads and publishes with the ones it keeps.
28
+
29
+ | Method | What it does |
30
+ |---|---|
31
+ | `prepare(url, path, *, branch, commit, submodules, full_history, strategy, on_progress)` | A workspace of a repository at a path under gitd's roots |
32
+ | `update(path, *, branch, submodules, on_progress)` | To the tip of its branch, or of the branch given |
33
+ | `checkout(path, commit, *, submodules, on_progress)` | To a commit, as a rollback does |
34
+ | `describe(path)` | Its repository, commit, branch, uncommitted changes and submodules |
35
+ | `publish(path, branch, *, requested_by, pull_request, provenance, sandbox, update)` | Its commits pushed to a new branch, or with `update` an earlier publish's branch moved forward; its pull request opened or updated |
36
+ | `remove(path)` | Deleted; its pool stays |
37
+ | `probe(url)` | Whether it can be read, its default branch and branches |
38
+ | `info()` | Which gitd answers, and how it makes workspaces here and why |
39
+ | `audit(*, since, url, limit)` | Every publish, done or refused, newest first |
40
+
41
+ ```python
42
+ from gitd import PullRequestOptions
43
+
44
+ published = gitd.publish(
45
+ workspace.path, "agents/fix-login", update=True,
46
+ pull_request=PullRequestOptions(open=True, submodules=True, title="Fix the login", body="…"),
47
+ )
48
+ print(published.pull_request.url, published.pull_request.updated)
49
+ for submodule in published.submodule_pull_requests: # merged first, with a merge commit
50
+ print(submodule.path, submodule.pull_request or submodule.refused)
51
+ ```
52
+
53
+ A refusal raises `GitdError`, with `reason` (`AUTH_REQUIRED`, `NOT_FOUND`, `PATH_REFUSED`,
54
+ `BRANCH_REFUSED`, `BUSY`, …) and `said`, what git said. Decide from the reason, never from
55
+ the message.
56
+
57
+ [`examples/agent.py`](examples/agent.py) is an agent's whole round trip.
58
+
59
+ ## Layout
60
+
61
+ | Path | What it is |
62
+ |---|---|
63
+ | `src/gitd/client.py` | `Gitd`, the client |
64
+ | `src/gitd/models.py` | What gitd answers, as dataclasses |
65
+ | `src/gitd/errors.py` | `GitdError` |
66
+ | `src/gitd/timestamps.py` | Between the contract's timestamps and `datetime` |
67
+ | `src/gitd/connection.py` | The channel to the socket, and how calls are made |
68
+ | `src/gitd/methods.py` | The gitd methods called, by their gRPC path |
69
+ | `src/gitd/messages.py` | The messages of the contract the client uses |
70
+ | `src/gitd/contract.py` | Reads the contract at run time, with protobuf's public API |
71
+ | `src/gitd/contract.json` | The contract, built from [`proto/gitd/v1/gitd.proto`](../../proto/gitd/v1/gitd.proto) by `make generate` |
72
+
73
+ No code is generated: the client reads gitd's contract when it starts.
74
+
75
+ Python 3.11 or later, on Linux, macOS and Windows. Apache-2.0.
@@ -0,0 +1,81 @@
1
+ """An agent's round trip with gitd:
2
+
3
+ 1. gitd prepares a workspace of the repository, in milliseconds after the first time;
4
+ 2. the agent works in it with plain git, and commits;
5
+ 3. gitd publishes the commit as a new branch, with the token it keeps.
6
+
7
+ The agent never holds a credential.
8
+
9
+ python examples/agent.py https://github.com/your-org/your-repo /var/lib/gitd/workspaces/agent-1
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import subprocess
15
+ import sys
16
+ import time
17
+ from pathlib import Path
18
+
19
+ from gitd import Gitd, GitdError, Progress, Workspace
20
+
21
+ AUTHOR = ["-c", "user.name=agent", "-c", "user.email=agent@example.com"]
22
+ BRANCH_PREFIX = "agents/example-"
23
+ NOTE_FILE = "AGENT.md"
24
+
25
+
26
+ class Agent:
27
+ """Works on one repository in its own workspace, and publishes what it commits."""
28
+
29
+ def __init__(self, gitd: Gitd, url: str, path: str) -> None:
30
+ self.gitd = gitd
31
+ self.url = url
32
+ self.path = path
33
+
34
+ def run(self) -> None:
35
+ workspace = self.prepare()
36
+ try:
37
+ self.commit_a_change(workspace)
38
+ self.publish(workspace)
39
+ finally:
40
+ self.gitd.remove(workspace.path)
41
+
42
+ def prepare(self) -> Workspace:
43
+ workspace = self.gitd.prepare(self.url, self.path, on_progress=self.show_progress)
44
+ print(file=sys.stderr)
45
+ print(f"Prepared {workspace.path} at {workspace.commit[:12]}, by {workspace.strategy.value}.")
46
+ return workspace
47
+
48
+ def commit_a_change(self, workspace: Workspace) -> None:
49
+ Path(workspace.path, NOTE_FILE).write_text("Written by an agent.\n")
50
+ self.git(workspace, "add", NOTE_FILE)
51
+ self.git(workspace, "commit", "--quiet", "--message", "An agent was here")
52
+ print(f"Committed {NOTE_FILE}.")
53
+
54
+ def publish(self, workspace: Workspace) -> None:
55
+ published = self.gitd.publish(workspace.path, f"{BRANCH_PREFIX}{int(time.time())}")
56
+ repository = published.repositories[0]
57
+ print(f"Published {published.branch} to {repository.url}: {', '.join(repository.subjects)}.")
58
+
59
+ def git(self, workspace: Workspace, *arguments: str) -> None:
60
+ subprocess.run(["git", *AUTHOR, *arguments], cwd=workspace.path, check=True)
61
+
62
+ def show_progress(self, progress: Progress) -> None:
63
+ print(f"\r{progress.phase} {progress.percent}%", end="", file=sys.stderr)
64
+
65
+
66
+ def main(arguments: list[str]) -> int:
67
+ if len(arguments) != 2:
68
+ print("usage: python examples/agent.py <repository url> <workspace path>", file=sys.stderr)
69
+ return 2
70
+ url, path = arguments
71
+ with Gitd() as gitd:
72
+ try:
73
+ Agent(gitd, url, path).run()
74
+ except GitdError as error:
75
+ print(f"gitd refused: {error.reason}: {error.said}", file=sys.stderr)
76
+ return 1
77
+ return 0
78
+
79
+
80
+ if __name__ == "__main__":
81
+ sys.exit(main(sys.argv[1:]))
@@ -0,0 +1,34 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.32.4"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "gitd-client"
7
+ version = "0.14.0"
8
+ description = "Client for gitd: instant, secretless git checkouts for agent sandboxes"
9
+ readme = "README.md"
10
+ license = "Apache-2.0"
11
+ requires-python = ">=3.11"
12
+ keywords = ["git", "ai-agents", "sandbox", "workspace", "grpc"]
13
+ classifiers = [
14
+ "Programming Language :: Python :: 3",
15
+ "Operating System :: OS Independent",
16
+ "Topic :: Software Development :: Version Control :: Git",
17
+ ]
18
+ dependencies = [
19
+ "grpcio>=1.84.0",
20
+ "grpcio-status>=1.84.0",
21
+ "protobuf>=7.36.2",
22
+ ]
23
+
24
+ [project.urls]
25
+ Homepage = "https://github.com/dooservice/gitd"
26
+ Source = "https://github.com/dooservice/gitd/tree/main/sdk/python"
27
+
28
+ [tool.hatch.build.targets.wheel]
29
+ packages = ["src/gitd"]
30
+
31
+ [tool.ruff]
32
+ line-length = 110
33
+ [tool.ruff.lint]
34
+ select = ["E", "F", "I", "B", "UP", "N", "SIM"]
@@ -0,0 +1,88 @@
1
+ """Client for gitd: instant, secretless git checkouts for agent sandboxes."""
2
+
3
+ from .changes import ChangedFile, ChangeKind, Changes, Diff
4
+ from .client import Gitd
5
+ from .comparison import Comparison, Conflict, Event, EventKind, Side
6
+ from .connection import DEFAULT_SOCKET, SOCKET_VARIABLE
7
+ from .endpoint_calls import Endpoint
8
+ from .endpoint_tokens import IssuedToken, TokenInfo
9
+ from .errors import GitdError
10
+ from .kept_credentials import AppToCreate, Connected, CredentialKind, KeptCredential, SignIn
11
+ from .models import (
12
+ Access,
13
+ AuditEntry,
14
+ ConfigurationChange,
15
+ Description,
16
+ Info,
17
+ Layers,
18
+ PendingSubmodule,
19
+ Probe,
20
+ Progress,
21
+ Published,
22
+ PublishedRepository,
23
+ Refusal,
24
+ Strategy,
25
+ Submodule,
26
+ SubmodulePullRequest,
27
+ Workspace,
28
+ )
29
+ from .publishing_settings import (
30
+ OpenedPullRequest,
31
+ Opening,
32
+ Preset,
33
+ PresetOrigin,
34
+ Provenance,
35
+ PublishSettings,
36
+ PullRequestOptions,
37
+ PullRequestSettings,
38
+ )
39
+ from .warm_layers import WarmLayer
40
+
41
+ __all__ = [
42
+ "Comparison",
43
+ "Conflict",
44
+ "Event",
45
+ "EventKind",
46
+ "Side",
47
+ "ChangeKind",
48
+ "ChangedFile",
49
+ "Changes",
50
+ "Diff",
51
+ "AppToCreate",
52
+ "ConfigurationChange",
53
+ "Connected",
54
+ "CredentialKind",
55
+ "KeptCredential",
56
+ "OpenedPullRequest",
57
+ "Opening",
58
+ "Preset",
59
+ "PresetOrigin",
60
+ "Provenance",
61
+ "PublishSettings",
62
+ "PullRequestOptions",
63
+ "PullRequestSettings",
64
+ "SignIn",
65
+ "DEFAULT_SOCKET",
66
+ "SOCKET_VARIABLE",
67
+ "Access",
68
+ "AuditEntry",
69
+ "Description",
70
+ "Gitd",
71
+ "GitdError",
72
+ "Info",
73
+ "Layers",
74
+ "PendingSubmodule",
75
+ "Probe",
76
+ "Progress",
77
+ "Published",
78
+ "PublishedRepository",
79
+ "Refusal",
80
+ "Strategy",
81
+ "Submodule",
82
+ "SubmodulePullRequest",
83
+ "Workspace",
84
+ "WarmLayer",
85
+ "Endpoint",
86
+ "IssuedToken",
87
+ "TokenInfo",
88
+ ]
@@ -0,0 +1,77 @@
1
+ """What was changed in a workspace, and its diff, as plain dataclasses built from the
2
+ messages gitd answers."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from dataclasses import dataclass, field
7
+ from enum import StrEnum
8
+ from typing import Any
9
+
10
+ from . import messages
11
+
12
+ KIND_PREFIX = "KIND_"
13
+
14
+
15
+ class ChangeKind(StrEnum):
16
+ """How a file changed: added, modified or deleted."""
17
+
18
+ ADDED = "added"
19
+ MODIFIED = "modified"
20
+ DELETED = "deleted"
21
+
22
+ @classmethod
23
+ def from_message(cls, number: int) -> ChangeKind:
24
+ name = messages.CHANGE_KIND.name(number).removeprefix(KIND_PREFIX).lower()
25
+ return cls(name) if name in {member.value for member in cls} else cls.MODIFIED
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class ChangedFile:
30
+ """A file changed, relative to the workspace, with its lines; a binary one counts none."""
31
+
32
+ path: str
33
+ kind: ChangeKind
34
+ added_lines: int
35
+ removed_lines: int
36
+ binary: bool
37
+
38
+ @classmethod
39
+ def from_message(cls, file: Any) -> ChangedFile:
40
+ return cls(
41
+ path=file.path,
42
+ kind=ChangeKind.from_message(file.kind),
43
+ added_lines=file.added_lines,
44
+ removed_lines=file.removed_lines,
45
+ binary=file.binary,
46
+ )
47
+
48
+
49
+ @dataclass(frozen=True)
50
+ class Changes:
51
+ """What was changed in a workspace since gitd put it on base, committed or not.
52
+ from_layer says it was read from the agent's own layer, with no git run in the workspace."""
53
+
54
+ base: str
55
+ from_layer: bool
56
+ files: list[ChangedFile] = field(default_factory=list)
57
+
58
+ @classmethod
59
+ def from_message(cls, response: Any) -> Changes:
60
+ return cls(
61
+ base=response.base,
62
+ from_layer=response.from_layer,
63
+ files=[ChangedFile.from_message(file) for file in response.files],
64
+ )
65
+
66
+
67
+ @dataclass(frozen=True)
68
+ class Diff:
69
+ """The unified diff of a workspace's changes; truncated says it was cut short."""
70
+
71
+ base: str
72
+ diff: str
73
+ truncated: bool
74
+
75
+ @classmethod
76
+ def from_message(cls, response: Any) -> Diff:
77
+ return cls(base=response.base, diff=response.diff, truncated=response.truncated)