Skip to content

Add weftsh: the Python SDK for Weft - #1

Merged
hkd987 merged 3 commits into
mainfrom
claude/weft-typescript-sdk-qp6bkd
Sep 25, 2026
Merged

hkd987 merged 3 commits into
mainfrom
claude/weft-typescript-sdk-qp6bkd

Conversation

@hkd987

@hkd987 hkd987 commented Sep 25, 2026

Copy link
Copy Markdown
Contributor

What

The first release (0.1.0) of weftsh, the Python counterpart of @weftsh/sdk (Weftsh/typescript-sdk#1). It has the same features and the same behaviour, and follows the shape of code.storage's Python SDK (pierre-storage): a client, a repository handle, keyword-only arguments, and httpx underneath.

from weftsh import Weft

weft = Weft(token=os.environ["WEFT_TOKEN"], org="acme")
repo = weft.create_repo()
repo.create_commit(message="step 1").put("src/app.py", "print(1)\n").send()
repo.read_file("src/app.py")
repo.get_remote_url()  # https://x:weft_…@api.weft.sh/acme/….git
  • Sync and async. Weft is the sync client and AsyncWeft is the asyncio one, with identical methods. The sync client is generated from the async one by scripts/unasync.py, and a test fails if the committed copy is stale. pierre-storage is async-only; this SDK ships both because plain scripts want sync.
  • Features: repositories (create, find, iterate, batch create/delete, update, delete, fork, mirrors); commits through a builder; file, tree, history and diff reads with ETag caching; branches and tags; reset/revert; tokens; webhooks; bundle export.
  • get_remote_url() creates a short-lived token that works only on that one repository and puts it in the URL. If the server refuses to create it, the error names the scopes the client token needs, instead of passing on the server's masked 404.
  • expected_parent covers the server's three cases with an UNSET sentinel. Leaving it out omits the field. None sends JSON null, meaning "the branch must not exist yet". A SHA must match the branch tip. update() follows the same rule: None clears a field, and a missing argument leaves it alone.
  • Missing files and repositories: a missing path or revision returns None. A missing repository, which the server answers with a bare not found, raises an error instead.
  • verify_webhook checks the signature with hmac.compare_digest.
  • Packaging: results are frozen dataclasses, it passes mypy --strict, it has one dependency (httpx), and it needs Python 3.10+.
  • Docs: README with a Quickstart, a full guide and an API reference; CHANGELOG; examples/quickstart.py and examples/agent_session.py.
  • CI: ruff, mypy --strict, pytest and a wheel-install check, on Python 3.10–3.13. This PR is the first time it runs.

Quickstart

The README opens with a Quickstart:

  1. Get a token with org:read and repo:write, or org:admin.
  2. Install.
  3. Run examples/quickstart.py, which creates a repository, commits, reads the file back and clones it with the real git CLI.
  4. Compare with the expected output.

The script is embedded in the README verbatim, tests/test_readme.py fails if the two differ, and the e2e suite runs it as a subprocess.

ruff 0.16 formats Python code blocks inside Markdown, and it had mangled the README's first example. Markdown and the quickstart script are now excluded from formatting.

Testing

  • Unit tests (42) pin what goes on the wire through httpx.MockTransport, for both the sync and async clients.
  • End-to-end tests (19) run against a real Weft server built from stratum-core. They are skipped unless WEFT_E2E_URL, WEFT_E2E_TOKEN and WEFT_E2E_ORG are set; the client is built inside a fixture, so a skipped run creates nothing. Among other things they:
    • clone, push and run git fsck --full --strict through get_remote_url();
    • check that a read-only URL can't push, and that a scoped credential can't reach a sibling repository;
    • verify a webhook delivery the server itself signed;
    • run the async client end to end;
    • run the README quickstart as written.
  • Deliberately breaking the expected_parent mapping turned 9 tests red.
  • I checked the quickstart's token claims with real personal tokens: org:read + repo:write runs it all, and repo:write alone fails at the clone step with the new explanatory error.

All 61 tests pass against a local server. With no server set, 42 pass and 19 skip. The unit suite also passes on Python 3.10 and 3.13, the wheel installs and imports on its own, and uv lock --check passes.

Before publishing

  • weftsh is free on PyPI; weft and weft-sdk are taken. The name matches the npm scope.
  • The license is MIT, matching the TypeScript SDK.
  • Small API difference: create_repos takes a list of names (plus public=), while the TypeScript createRepos takes a list of option objects.
  • The weft.sh docs for this SDK are in hkd987/stratum-core#106.

🤖 Generated with Claude Code

https://claude.ai/code/session_01SYuyBuroUvxh32x9SCbx13


Generated by Claude Code

The Python counterpart of @weftsh/sdk, with the same surface and the
same semantics, shaped after code.storage's pierre-storage: a client, a
repository handle, keyword-only arguments, httpx underneath.

- `Weft` (sync) and `AsyncWeft` (asyncio) with identical methods. Every
  method is one request, so the sync client is generated from the async
  one by scripts/unasync.py; a test fails when the committed copy is
  stale, so the two cannot drift.
- Repositories, commits through a builder, reads with ETag caching,
  history and diffs, branches and tags, reset/revert, tokens, webhooks,
  export, mirrors, and `get_remote_url()` minting a repo-bound token.
- `expected_parent` keeps the server's three cases apart with an UNSET
  sentinel: left out omits the key, `None` sends JSON null ("the branch
  must not exist yet"), a SHA must match. `update()` uses the same rule
  so `None` clears a field and a missing argument leaves it alone.
- The 404 rule from the TypeScript SDK: a missing path or revision reads
  as None, a missing repository (a bare `not found`) raises.
- `verify_webhook` with hmac.compare_digest.
- Results are frozen dataclasses; mypy --strict clean; one dependency.

Tested the same two ways as the TypeScript SDK. 36 unit tests pin the
wire through httpx.MockTransport. 18 e2e tests run against a real Weft
server (skipped unless WEFT_E2E_* are set; the client is built inside a
fixture so a skipped run constructs nothing): real `git` clone, push and
`fsck --full --strict` through get_remote_url, a read-only URL that
cannot push, a scoped credential that cannot reach a sibling repo, a
webhook delivery the server signed, and the async client end to end.
Breaking the expected_parent mapping turns 9 tests red.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SYuyBuroUvxh32x9SCbx13
…ample

Same as the TypeScript SDK: a Quickstart section (token with the scopes
it needs, install, run examples/quickstart.py, expected output) whose
script is embedded verbatim, held to examples/quickstart.py by
tests/test_readme.py, and run as a subprocess by the e2e suite.

The first example in the README was mangled: ruff 0.16 formats Python
code blocks inside Markdown, and `ruff format .` had rewritten a
readable builder chain into one split across a call's parentheses.
Markdown is now excluded from formatting, as is the quickstart script
(it keeps the README's hand layout), and the example is rewritten.

get_remote_url() now explains a 400/403/404 from the mint in words
naming the scopes needed (org:read plus repo:write, or org:admin)
instead of passing on the server's masked 404, and the README's note on
the scopes is corrected; both checked with real personal tokens.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SYuyBuroUvxh32x9SCbx13
@hkd987
hkd987 merged commit 84f3336 into main Sep 25, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants