Metadata-Version: 2.4
Name: gfa-sdk
Version: 0.1.1
Summary: Opinionated Python client for gfa (Git for Agents) — smart routing, session cache, profile-aware hints.
Author: gfa contributors
License: MIT
Project-URL: Homepage, https://gitlab.com/kerusu/gfa
Project-URL: Repository, https://gitlab.com/kerusu/gfa
Keywords: gfa,git,agent,storage,sdk
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Version Control :: Git
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: httpx[http2]>=0.27
Requires-Dist: PyJWT[crypto]>=2.8
Requires-Dist: cryptography>=42
Provides-Extra: test
Requires-Dist: pytest>=8; extra == "test"
Requires-Dist: pytest-asyncio>=0.23; extra == "test"
Requires-Dist: pyyaml>=6; extra == "test"

# gfa-sdk — Python SDK for gfa (Git for Agents)

Opinionated Python client for [gfa](https://gitlab.com/kerusu/gfa). Picks the
right endpoint for the access pattern, caches what is safe to cache, and emits
hints when a strategy change would help. Designed for AI agents reading,
mutating, and forking gfa-hosted repos.

## Install

```bash
pip install gfa-sdk
```

Python >= 3.10 required.

## Quickstart

```python
import os
import gfa

client = gfa.Client(
    endpoint=os.environ["GFA_ENDPOINT"],
    token=os.environ["GFA_JWT"],
)

# Profile a repo (cheap, cached for the session)
profile = client.profile_repo("myrepo")
print(f"{profile.file_count_at_head} files, {profile.storage_bytes} bytes")

# Read one file
src = client.read_file("myrepo", "src/main.go")

# Read many files — SDK picks /file vs /files/batch vs partial-clone
files = client.read_files("myrepo", ["a.go", "b.go", "c.go"])

# Create a commit
sha = client.create_commit(
    "myrepo",
    branch="main",
    message="agent: refactor",
    files=[gfa.FileChange(path="a.go", content=b"// rewritten\n")],
)

# Workspace for isolated changes
with client.create_workspace("myrepo", base_ref="main", ttl_hours=2) as ws:
    ws.create_commit(branch="main", message="WIP", files=[...])
    ws.merge()
```

## Smart routing

`client.read_files(repo, paths)` auto-routes by `len(paths)`:

| Count | Surface |
|---|---|
| 1 | `GET /file` |
| 2–200 | `POST /files/batch` |
| > 200 (default) | `SuggestPartialCloneError` — opt into `auto_clone=True` for automatic partial clone |

Threshold tuning:

```python
from gfa import Client, ClientConfig
client = Client(
    endpoint,
    token,
    config=ClientConfig(batch_read_threshold=500),
)
```

## Hints

The SDK emits structured hints when access patterns suggest a strategy change.
The default handler writes to stderr; override with a logging hook:

```python
def my_handler(hint):
    logger.info("gfa hint", extra={"code": hint.code, "repo": hint.repo})

client = gfa.Client(endpoint, token, hint_handler=my_handler)
```

Silence entirely:

```python
client = gfa.Client(endpoint, token, hint_handler=lambda _: None)
```

## Auth

Either a pre-minted JWT string, or a `TokenProvider` that mints per-call:

```python
provider = gfa.FileKeyTokenProvider("/path/to/preview-jwt-priv.pem", ttl_hours=1)
client = gfa.Client(endpoint, provider)
```

`FileKeyTokenProvider` mints ES256 JWTs with `sub=<repo>`, `iat=now`,
`exp=now + ttl_hours`. Mirrors the Go `tools/mint-token` binary.

## Diagnostics

```python
client.read_file("myrepo", "x.go")
client.read_files("myrepo", ["a.go", "b.go"])
print(client.stats)
# ClientStats(request_count=2, cache_hit_rate=0.0, ...)
```

## Partial clone

```python
with client.partial_clone("myrepo", filter="blob:none", depth=1) as repo:
    content = repo.read("src/foo.go")
```

Requires `git` binary on PATH.

## Troubleshooting

| Symptom | Likely cause |
|---|---|
| `UnauthorizedError` | Token missing, malformed, or expired |
| `ForbiddenError` | JWT `sub` does not match the repo being accessed |
| `SuggestPartialCloneError` | Asked for too many files; pass `auto_clone=True` or use `partial_clone` |
| `GitBinaryMissingError` | `partial_clone` needs `git` on PATH |
| `ConflictError` on merge | Workspace and source diverged — inspect via `conflict_surface()` |

## License

MIT.
