gfa-sdk 0.1.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.
- gfa_sdk-0.1.0/PKG-INFO +159 -0
- gfa_sdk-0.1.0/README.md +132 -0
- gfa_sdk-0.1.0/gfa/__init__.py +82 -0
- gfa_sdk-0.1.0/gfa/_transport.py +383 -0
- gfa_sdk-0.1.0/gfa/_version.py +8 -0
- gfa_sdk-0.1.0/gfa/async_client.py +31 -0
- gfa_sdk-0.1.0/gfa/cache.py +181 -0
- gfa_sdk-0.1.0/gfa/client.py +535 -0
- gfa_sdk-0.1.0/gfa/defaults.py +97 -0
- gfa_sdk-0.1.0/gfa/errors.py +219 -0
- gfa_sdk-0.1.0/gfa/hints.py +102 -0
- gfa_sdk-0.1.0/gfa/mint_token.py +107 -0
- gfa_sdk-0.1.0/gfa/models.py +262 -0
- gfa_sdk-0.1.0/gfa/partial_clone.py +214 -0
- gfa_sdk-0.1.0/gfa/routing.py +111 -0
- gfa_sdk-0.1.0/gfa/workspace.py +198 -0
- gfa_sdk-0.1.0/gfa_sdk.egg-info/PKG-INFO +159 -0
- gfa_sdk-0.1.0/gfa_sdk.egg-info/SOURCES.txt +33 -0
- gfa_sdk-0.1.0/gfa_sdk.egg-info/dependency_links.txt +1 -0
- gfa_sdk-0.1.0/gfa_sdk.egg-info/requires.txt +8 -0
- gfa_sdk-0.1.0/gfa_sdk.egg-info/top_level.txt +1 -0
- gfa_sdk-0.1.0/pyproject.toml +49 -0
- gfa_sdk-0.1.0/setup.cfg +4 -0
- gfa_sdk-0.1.0/tests/test_cache.py +113 -0
- gfa_sdk-0.1.0/tests/test_client.py +362 -0
- gfa_sdk-0.1.0/tests/test_errors.py +66 -0
- gfa_sdk-0.1.0/tests/test_hints.py +41 -0
- gfa_sdk-0.1.0/tests/test_integration.py +150 -0
- gfa_sdk-0.1.0/tests/test_mint_token.py +91 -0
- gfa_sdk-0.1.0/tests/test_models.py +101 -0
- gfa_sdk-0.1.0/tests/test_openapi_sync.py +73 -0
- gfa_sdk-0.1.0/tests/test_partial_clone.py +262 -0
- gfa_sdk-0.1.0/tests/test_routing.py +91 -0
- gfa_sdk-0.1.0/tests/test_transport.py +111 -0
- gfa_sdk-0.1.0/tests/test_workspace.py +247 -0
gfa_sdk-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gfa-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Opinionated Python client for gfa (Git for Agents) — smart routing, session cache, profile-aware hints.
|
|
5
|
+
Author: gfa contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://gitlab.com/kerusu/gfa
|
|
8
|
+
Project-URL: Repository, https://gitlab.com/kerusu/gfa
|
|
9
|
+
Keywords: gfa,git,agent,storage,sdk
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Software Development :: Version Control :: Git
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
Requires-Dist: httpx[http2]>=0.27
|
|
21
|
+
Requires-Dist: PyJWT[crypto]>=2.8
|
|
22
|
+
Requires-Dist: cryptography>=42
|
|
23
|
+
Provides-Extra: test
|
|
24
|
+
Requires-Dist: pytest>=8; extra == "test"
|
|
25
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "test"
|
|
26
|
+
Requires-Dist: pyyaml>=6; extra == "test"
|
|
27
|
+
|
|
28
|
+
# gfa-sdk — Python SDK for gfa (Git for Agents)
|
|
29
|
+
|
|
30
|
+
Opinionated Python client for [gfa](https://gitlab.com/kerusu/gfa). Picks the
|
|
31
|
+
right endpoint for the access pattern, caches what is safe to cache, and emits
|
|
32
|
+
hints when a strategy change would help. Designed for AI agents reading,
|
|
33
|
+
mutating, and forking gfa-hosted repos.
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install gfa-sdk
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Python >= 3.10 required.
|
|
42
|
+
|
|
43
|
+
## Quickstart
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
import os
|
|
47
|
+
import gfa
|
|
48
|
+
|
|
49
|
+
client = gfa.Client(
|
|
50
|
+
endpoint=os.environ["GFA_ENDPOINT"],
|
|
51
|
+
token=os.environ["GFA_JWT"],
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Profile a repo (cheap, cached for the session)
|
|
55
|
+
profile = client.profile_repo("myrepo")
|
|
56
|
+
print(f"{profile.file_count_at_head} files, {profile.storage_bytes} bytes")
|
|
57
|
+
|
|
58
|
+
# Read one file
|
|
59
|
+
src = client.read_file("myrepo", "src/main.go")
|
|
60
|
+
|
|
61
|
+
# Read many files — SDK picks /file vs /files/batch vs partial-clone
|
|
62
|
+
files = client.read_files("myrepo", ["a.go", "b.go", "c.go"])
|
|
63
|
+
|
|
64
|
+
# Create a commit
|
|
65
|
+
sha = client.create_commit(
|
|
66
|
+
"myrepo",
|
|
67
|
+
branch="main",
|
|
68
|
+
message="agent: refactor",
|
|
69
|
+
files=[gfa.FileChange(path="a.go", content=b"// rewritten\n")],
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Workspace for isolated changes
|
|
73
|
+
with client.create_workspace("myrepo", base_ref="main", ttl_hours=2) as ws:
|
|
74
|
+
ws.create_commit(branch="main", message="WIP", files=[...])
|
|
75
|
+
ws.merge()
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Smart routing
|
|
79
|
+
|
|
80
|
+
`client.read_files(repo, paths)` auto-routes by `len(paths)`:
|
|
81
|
+
|
|
82
|
+
| Count | Surface |
|
|
83
|
+
|---|---|
|
|
84
|
+
| 1 | `GET /file` |
|
|
85
|
+
| 2–200 | `POST /files/batch` |
|
|
86
|
+
| > 200 (default) | `SuggestPartialCloneError` — opt into `auto_clone=True` for automatic partial clone |
|
|
87
|
+
|
|
88
|
+
Threshold tuning:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from gfa import Client, ClientConfig
|
|
92
|
+
client = Client(
|
|
93
|
+
endpoint,
|
|
94
|
+
token,
|
|
95
|
+
config=ClientConfig(batch_read_threshold=500),
|
|
96
|
+
)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Hints
|
|
100
|
+
|
|
101
|
+
The SDK emits structured hints when access patterns suggest a strategy change.
|
|
102
|
+
The default handler writes to stderr; override with a logging hook:
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
def my_handler(hint):
|
|
106
|
+
logger.info("gfa hint", extra={"code": hint.code, "repo": hint.repo})
|
|
107
|
+
|
|
108
|
+
client = gfa.Client(endpoint, token, hint_handler=my_handler)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Silence entirely:
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
client = gfa.Client(endpoint, token, hint_handler=lambda _: None)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Auth
|
|
118
|
+
|
|
119
|
+
Either a pre-minted JWT string, or a `TokenProvider` that mints per-call:
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
provider = gfa.FileKeyTokenProvider("/path/to/preview-jwt-priv.pem", ttl_hours=1)
|
|
123
|
+
client = gfa.Client(endpoint, provider)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`FileKeyTokenProvider` mints ES256 JWTs with `sub=<repo>`, `iat=now`,
|
|
127
|
+
`exp=now + ttl_hours`. Mirrors the Go `tools/mint-token` binary.
|
|
128
|
+
|
|
129
|
+
## Diagnostics
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
client.read_file("myrepo", "x.go")
|
|
133
|
+
client.read_files("myrepo", ["a.go", "b.go"])
|
|
134
|
+
print(client.stats)
|
|
135
|
+
# ClientStats(request_count=2, cache_hit_rate=0.0, ...)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Partial clone
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
with client.partial_clone("myrepo", filter="blob:none", depth=1) as repo:
|
|
142
|
+
content = repo.read("src/foo.go")
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Requires `git` binary on PATH.
|
|
146
|
+
|
|
147
|
+
## Troubleshooting
|
|
148
|
+
|
|
149
|
+
| Symptom | Likely cause |
|
|
150
|
+
|---|---|
|
|
151
|
+
| `UnauthorizedError` | Token missing, malformed, or expired |
|
|
152
|
+
| `ForbiddenError` | JWT `sub` does not match the repo being accessed |
|
|
153
|
+
| `SuggestPartialCloneError` | Asked for too many files; pass `auto_clone=True` or use `partial_clone` |
|
|
154
|
+
| `GitBinaryMissingError` | `partial_clone` needs `git` on PATH |
|
|
155
|
+
| `ConflictError` on merge | Workspace and source diverged — inspect via `conflict_surface()` |
|
|
156
|
+
|
|
157
|
+
## License
|
|
158
|
+
|
|
159
|
+
MIT.
|
gfa_sdk-0.1.0/README.md
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# gfa-sdk — Python SDK for gfa (Git for Agents)
|
|
2
|
+
|
|
3
|
+
Opinionated Python client for [gfa](https://gitlab.com/kerusu/gfa). Picks the
|
|
4
|
+
right endpoint for the access pattern, caches what is safe to cache, and emits
|
|
5
|
+
hints when a strategy change would help. Designed for AI agents reading,
|
|
6
|
+
mutating, and forking gfa-hosted repos.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pip install gfa-sdk
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Python >= 3.10 required.
|
|
15
|
+
|
|
16
|
+
## Quickstart
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
import os
|
|
20
|
+
import gfa
|
|
21
|
+
|
|
22
|
+
client = gfa.Client(
|
|
23
|
+
endpoint=os.environ["GFA_ENDPOINT"],
|
|
24
|
+
token=os.environ["GFA_JWT"],
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# Profile a repo (cheap, cached for the session)
|
|
28
|
+
profile = client.profile_repo("myrepo")
|
|
29
|
+
print(f"{profile.file_count_at_head} files, {profile.storage_bytes} bytes")
|
|
30
|
+
|
|
31
|
+
# Read one file
|
|
32
|
+
src = client.read_file("myrepo", "src/main.go")
|
|
33
|
+
|
|
34
|
+
# Read many files — SDK picks /file vs /files/batch vs partial-clone
|
|
35
|
+
files = client.read_files("myrepo", ["a.go", "b.go", "c.go"])
|
|
36
|
+
|
|
37
|
+
# Create a commit
|
|
38
|
+
sha = client.create_commit(
|
|
39
|
+
"myrepo",
|
|
40
|
+
branch="main",
|
|
41
|
+
message="agent: refactor",
|
|
42
|
+
files=[gfa.FileChange(path="a.go", content=b"// rewritten\n")],
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Workspace for isolated changes
|
|
46
|
+
with client.create_workspace("myrepo", base_ref="main", ttl_hours=2) as ws:
|
|
47
|
+
ws.create_commit(branch="main", message="WIP", files=[...])
|
|
48
|
+
ws.merge()
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Smart routing
|
|
52
|
+
|
|
53
|
+
`client.read_files(repo, paths)` auto-routes by `len(paths)`:
|
|
54
|
+
|
|
55
|
+
| Count | Surface |
|
|
56
|
+
|---|---|
|
|
57
|
+
| 1 | `GET /file` |
|
|
58
|
+
| 2–200 | `POST /files/batch` |
|
|
59
|
+
| > 200 (default) | `SuggestPartialCloneError` — opt into `auto_clone=True` for automatic partial clone |
|
|
60
|
+
|
|
61
|
+
Threshold tuning:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from gfa import Client, ClientConfig
|
|
65
|
+
client = Client(
|
|
66
|
+
endpoint,
|
|
67
|
+
token,
|
|
68
|
+
config=ClientConfig(batch_read_threshold=500),
|
|
69
|
+
)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Hints
|
|
73
|
+
|
|
74
|
+
The SDK emits structured hints when access patterns suggest a strategy change.
|
|
75
|
+
The default handler writes to stderr; override with a logging hook:
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
def my_handler(hint):
|
|
79
|
+
logger.info("gfa hint", extra={"code": hint.code, "repo": hint.repo})
|
|
80
|
+
|
|
81
|
+
client = gfa.Client(endpoint, token, hint_handler=my_handler)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Silence entirely:
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
client = gfa.Client(endpoint, token, hint_handler=lambda _: None)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Auth
|
|
91
|
+
|
|
92
|
+
Either a pre-minted JWT string, or a `TokenProvider` that mints per-call:
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
provider = gfa.FileKeyTokenProvider("/path/to/preview-jwt-priv.pem", ttl_hours=1)
|
|
96
|
+
client = gfa.Client(endpoint, provider)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`FileKeyTokenProvider` mints ES256 JWTs with `sub=<repo>`, `iat=now`,
|
|
100
|
+
`exp=now + ttl_hours`. Mirrors the Go `tools/mint-token` binary.
|
|
101
|
+
|
|
102
|
+
## Diagnostics
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
client.read_file("myrepo", "x.go")
|
|
106
|
+
client.read_files("myrepo", ["a.go", "b.go"])
|
|
107
|
+
print(client.stats)
|
|
108
|
+
# ClientStats(request_count=2, cache_hit_rate=0.0, ...)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Partial clone
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
with client.partial_clone("myrepo", filter="blob:none", depth=1) as repo:
|
|
115
|
+
content = repo.read("src/foo.go")
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Requires `git` binary on PATH.
|
|
119
|
+
|
|
120
|
+
## Troubleshooting
|
|
121
|
+
|
|
122
|
+
| Symptom | Likely cause |
|
|
123
|
+
|---|---|
|
|
124
|
+
| `UnauthorizedError` | Token missing, malformed, or expired |
|
|
125
|
+
| `ForbiddenError` | JWT `sub` does not match the repo being accessed |
|
|
126
|
+
| `SuggestPartialCloneError` | Asked for too many files; pass `auto_clone=True` or use `partial_clone` |
|
|
127
|
+
| `GitBinaryMissingError` | `partial_clone` needs `git` on PATH |
|
|
128
|
+
| `ConflictError` on merge | Workspace and source diverged — inspect via `conflict_surface()` |
|
|
129
|
+
|
|
130
|
+
## License
|
|
131
|
+
|
|
132
|
+
MIT.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""gfa — Opinionated Python SDK for gfa (Git for Agents).
|
|
2
|
+
|
|
3
|
+
Public surface re-exports. Power users can reach into submodules
|
|
4
|
+
(``gfa.routing``, ``gfa.cache``) to inspect internals.
|
|
5
|
+
|
|
6
|
+
See ``design/smart-client-sdk.md`` in the gfa repo for the design spec.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from gfa._version import __version__
|
|
10
|
+
from gfa.async_client import AsyncClient
|
|
11
|
+
from gfa.cache import BlobCache, ProfileCache, RefCache
|
|
12
|
+
from gfa.client import Client
|
|
13
|
+
from gfa.defaults import ClientConfig
|
|
14
|
+
from gfa.errors import (
|
|
15
|
+
BadRequestError,
|
|
16
|
+
ConflictError,
|
|
17
|
+
FileNotFoundError,
|
|
18
|
+
ForbiddenError,
|
|
19
|
+
GfaError,
|
|
20
|
+
GitBinaryMissingError,
|
|
21
|
+
RefNotFoundError,
|
|
22
|
+
RepoNotFoundError,
|
|
23
|
+
ServerError,
|
|
24
|
+
SuggestPartialCloneError,
|
|
25
|
+
TransportError,
|
|
26
|
+
UnauthorizedError,
|
|
27
|
+
UnavailableError,
|
|
28
|
+
UnprocessableError,
|
|
29
|
+
)
|
|
30
|
+
from gfa.hints import Hint, HintHandler, default_hint_handler
|
|
31
|
+
from gfa.mint_token import FileKeyTokenProvider, TokenProvider
|
|
32
|
+
from gfa.models import (
|
|
33
|
+
Author,
|
|
34
|
+
ClientStats,
|
|
35
|
+
CommitInfo,
|
|
36
|
+
ConflictSurface,
|
|
37
|
+
DivergeResult,
|
|
38
|
+
FileChange,
|
|
39
|
+
RepoProfile,
|
|
40
|
+
TreeEntry,
|
|
41
|
+
)
|
|
42
|
+
from gfa.partial_clone import PartialClone
|
|
43
|
+
from gfa.workspace import Workspace
|
|
44
|
+
|
|
45
|
+
__all__ = [
|
|
46
|
+
"__version__",
|
|
47
|
+
"AsyncClient",
|
|
48
|
+
"Author",
|
|
49
|
+
"BadRequestError",
|
|
50
|
+
"BlobCache",
|
|
51
|
+
"Client",
|
|
52
|
+
"ClientConfig",
|
|
53
|
+
"ClientStats",
|
|
54
|
+
"CommitInfo",
|
|
55
|
+
"ConflictError",
|
|
56
|
+
"ConflictSurface",
|
|
57
|
+
"DivergeResult",
|
|
58
|
+
"FileChange",
|
|
59
|
+
"FileKeyTokenProvider",
|
|
60
|
+
"FileNotFoundError",
|
|
61
|
+
"ForbiddenError",
|
|
62
|
+
"GfaError",
|
|
63
|
+
"GitBinaryMissingError",
|
|
64
|
+
"Hint",
|
|
65
|
+
"HintHandler",
|
|
66
|
+
"PartialClone",
|
|
67
|
+
"ProfileCache",
|
|
68
|
+
"RefCache",
|
|
69
|
+
"RefNotFoundError",
|
|
70
|
+
"RepoNotFoundError",
|
|
71
|
+
"RepoProfile",
|
|
72
|
+
"ServerError",
|
|
73
|
+
"SuggestPartialCloneError",
|
|
74
|
+
"TokenProvider",
|
|
75
|
+
"TransportError",
|
|
76
|
+
"TreeEntry",
|
|
77
|
+
"UnauthorizedError",
|
|
78
|
+
"UnavailableError",
|
|
79
|
+
"UnprocessableError",
|
|
80
|
+
"Workspace",
|
|
81
|
+
"default_hint_handler",
|
|
82
|
+
]
|