Python bindings for grit-lib (a Rust reimplementation of Git)
Project description
pygritlib
Native Python bindings for grit-lib — the
core Rust library of gitbutlerapp/grit, a
from-scratch reimplementation of Git in Rust. pygritlib is built with
PyO3 and packaged as an abi3 wheel with
maturin. pygritlib is a thin Python façade over grit-lib
covering reading — discover/open repositories, read objects (commit/tree/blob/tag),
list and resolve references, walk history, diff commits, and read config — a local
write surface (since 0.2.0): write objects, stage an index, build trees, create
commit/tag objects, and mutate refs — read-path networking (since 0.3.0): clone,
fetch, and list remote refs over git:// and https — and push (since 0.4.0): push
refs to a remote over git:// and https, with force, delete, atomic, dry-run, and
force-with-lease (compare-and-swap) support. No system OpenSSL or libcurl required.
Everything runs in-process, with no external git binary required at runtime and no
system C libraries to build.
Install / build from source
This project uses uv for Python environment and
dependency management (no pip/poetry/requirements.txt), maturin for the build, and
a pinned Rust toolchain (rust-toolchain.toml, channel 1.94.1). There are no
-sys/pkg-config dependencies — grit-lib uses pure-Rust compression
(miniz_oxide) and hashing (sha1/sha2).
# 1. Create the venv and install dev dependencies (maturin, pytest, mypy, ruff).
uv venv
uv sync --group dev
# 2. Build the native extension and install it editable into the venv.
uv run maturin develop --uv
# 3. Run the tests.
uv run pytest tests/ -v
Building a wheel / sdist
uv run maturin build --release --locked # wheel -> target/wheels/
uv run maturin sdist # sdist -> target/wheels/
The wheel is tagged cp311-abi3-<platform> and works on CPython 3.11+.
Quickstart
import pygritlib
# Discover the repository containing the given path (walks upward to find .git).
repo = pygritlib.Repository.discover(".")
# Or open an explicit git dir: pygritlib.Repository.open("/path/to/.git")
# Resolve HEAD to an ObjectId, then read the commit it points at.
head = repo.resolve("HEAD") # ObjectId (also resolves "main", "HEAD~2", hex, ...)
commit = repo.commit(head)
print(commit.id.hex) # 40-char (SHA-1) or 64-char (SHA-256) hex
print(commit.author.name_str, commit.author.email_str) # decoded str accessors
print(commit.message().splitlines()[0]) # decoded subject line
print([p.hex for p in commit.parents])
# List the entries of the commit's tree.
tree = repo.tree(commit.tree)
for entry in tree:
# entry.name is bytes (exact, git-faithful); mode is an int; id is an ObjectId.
print(f"{entry.mode:06o} {entry.id.hex} {entry.name!r}")
# Walk history, newest first (like `git rev-list HEAD`); yields Commit objects.
for c in repo.revwalk(head):
print(c.id.hex[:10], c.message().splitlines()[0])
# Diff a commit against its first parent (pass commit ids; tree extraction is internal).
if commit.parents:
diff = repo.diff(commit.parents[0], head)
s = diff.stats
print(f"{s.files_changed} files changed, +{s.insertions} -{s.deletions}")
for e in diff:
# e.status is one of A/D/M/R/C/T/U; paths are bytes (or None).
print(e.status, (e.new_path or e.old_path))
# Read config (last-wins, includes system config).
cfg = repo.config
print(cfg.get_bool("core.bare")) # None if absent
print(cfg.get_str("user.email"))
# Iterate references; resolve a reference to a final ObjectId.
for ref in repo.references():
print(ref.name, ref.peel().hex)
# Read raw object bytes straight from the object database.
obj = repo.odb.read(head)
print(obj.kind, len(obj.data)) # obj.kind is an ObjectKind; obj.data is bytes
Writing (local write-core)
Since 0.2.0, pygritlib exposes a local write surface — enough to build commits
and move refs entirely in-process. It mirrors git's plumbing (write object → stage index
→ write tree → create commit → update ref), and create_commit/create_tag produce
byte-identical object ids to git.
import pygritlib
from pygritlib import ObjectKind, Signature
repo = pygritlib.Repository.open("/path/to/.git") # or .discover(".")
# 1. Write a blob straight to the object database (== git hash-object -w).
blob = repo.odb.write(ObjectKind.BLOB, b"hello\n")
# repo.odb.hash(kind, data) computes the oid WITHOUT writing it.
# 2. Stage entries into the index, then persist it.
idx = repo.index()
idx.add(b"greeting.txt", blob, 0o100644) # stage a blob already in the odb
# idx.stage(b"path/in/worktree") # OR hash a real working-tree file (needs a work tree)
idx.write() # write .git/index (len(idx) / `for e in idx: ...`)
# 3. Build a tree object from the index (== git write-tree).
tree = idx.write_tree() # -> ObjectId
# 4. Create a commit object (== git commit-tree). Pure: returns the oid, moves no ref.
# Signature is (name, email, (unix_seconds, tz_offset_seconds)).
me = Signature(b"Ada", b"ada@example.com", (1718000000, 0))
commit = repo.create_commit(tree, parents=[], author=me, committer=me, message=b"init\n")
# For byte-exact ids with unusual identities, pass raw header bytes instead of a Signature:
# repo.create_commit(tree, [], author_raw=b"Ada <ada@x> 1718000000 +0000",
# committer_raw=b"...", message=b"...")
# 5. Move a branch at the new commit, and point HEAD at it.
repo.update_ref(b"refs/heads/main", commit, create=True) # create-only: fails if it exists
repo.set_head(b"refs/heads/main")
# Other ref ops: expected_old= for compare-and-swap, message=/signer= to write a reflog,
# delete_ref(...), set_symbolic_ref(...), append_reflog(...).
# Annotated tags create a tag OBJECT; point a ref at it separately.
tag = repo.create_tag(commit, ObjectKind.COMMIT, b"v1", message=b"release\n", tagger=me)
repo.update_ref(b"refs/tags/v1", tag, create=True)
Ref update modes (update_ref): the default overwrites; create=True is create-only
(fails if the ref exists); expected_old=<oid> is a compare-and-swap. Compare-and-swap is
best-effort — grit-lib 0.4.1 has no atomic CAS primitive, so it is a read→compare→write
without a held lock (it catches the common non-concurrent case but is not a hard guarantee
against another writer in the window).
Write-input validation. Constructing a Signature rejects <, >, NUL, or newline in
the name/email and out-of-range / non-minute timezone offsets; index paths must be clean
relative paths (no leading/trailing /, no ./.. components); ref names are validated by
git's ref-format rules; reflog messages and tag names reject NUL/CR/LF. These prevent object/
record injection, path traversal, and a grit-lib stack-overflow on malformed index paths.
Networking (clone / fetch / ls-remote / push)
Since 0.3.0, pygritlib exposes a read-path networking surface — clone from a
remote, fetch into an existing repository, or list a remote's refs without cloning —
over git:// and https (the http-ureq / rustls stack is bundled by default; no
system OpenSSL or libcurl required). Since 0.4.0, repo.push is also available over
the same transports. SSH transport, shallow/depth, bare/mirror clone, and submodules are
not yet supported.
Entry points
# Top-level function — no local repo needed.
pygritlib.ls_remote(
url: str,
*,
username: str | None = None,
password: str | None = None,
use_credential_helpers: bool = True,
heads: bool = False,
tags: bool = False,
) -> list[RemoteRef]
# Class method — init + origin config + fetch + checkout (worktree clone).
# Fetches ALL tags (tags="all"), like `git clone`.
# Sets branch.<name>.remote/merge upstream tracking.
pygritlib.Repository.clone(
url: str,
path: str | bytes | os.PathLike[str],
*,
branch: str | None = None,
username: str | None = None,
password: str | None = None,
use_credential_helpers: bool = True,
) -> Repository
# Instance method — fetch into an existing repo.
# Default refspec: +refs/heads/*:refs/remotes/origin/*
# tags ∈ {"none", "following", "all"} (default "following")
repo.fetch(
url: str,
refspecs: list[str] | None = None,
*,
tags: str = "following",
prune: bool = False,
username: str | None = None,
password: str | None = None,
use_credential_helpers: bool = True,
) -> FetchReport
# Instance method — push refs to a remote (since 0.4.0).
repo.push(
url: str,
refspecs: list[str | PushSpec],
*,
force: bool = False,
atomic: bool = False,
dry_run: bool = False,
push_options: list[str] | None = None,
username: str | None = None,
password: str | None = None,
use_credential_helpers: bool = True,
progress: Callable[[bytes], None] | None = None,
) -> PushReport
Value objects
| Class | Fields |
|---|---|
RemoteRef |
.name: bytes, .oid: ObjectId, .symref_target: bytes | None |
RefUpdate |
.remote_ref: bytes, .local_ref: bytes | None, .old_oid: ObjectId | None, .new_oid: ObjectId | None, .mode: str, .note: str | None |
FetchReport |
.updates: list[RefUpdate], .default_branch: bytes | None |
PushSpec |
dst: bytes (remote ref), src: ObjectId | None (None ⇒ delete), force: bool, delete: bool, expected_old: ObjectId | None, expect_absent: bool |
PushRefResult |
.local_ref: bytes | None, .remote_ref: bytes, .old_oid: ObjectId | None, .new_oid: ObjectId | None, .forced: bool, .deletion: bool, .status: str, .message: str | None |
PushReport |
.results: list[PushRefResult], .ok: bool |
Exceptions
NetworkError and AuthenticationError are both subclasses of GritError (see
Exception hierarchy).
Authentication
Credentials are resolved in this order of precedence:
- Explicit kwargs —
username=/password=passed directly. - URL userinfo —
https://<token>@host/path(token-as-password style). - Git credential helpers — queried when
use_credential_helpers=True(the default), using the standard git credential protocol.
Supported transports
| Transport / feature | Status |
|---|---|
https:// fetch/clone/ls-remote |
Supported (rustls bundled, no system OpenSSL) |
git:// fetch/clone/ls-remote |
Supported |
https:// push |
Supported (since 0.4.0) |
git:// push |
Supported (since 0.4.0) |
ssh:// / git+ssh:// / scp-style user@host:path |
Supported (since 0.5.0; spawns system ssh) |
Signed push (--signed) |
Not yet supported |
| Submodule push | Not yet supported |
| Push protocol v2 | Not yet supported (grit rejects v2 push; falls back to v1) |
Shallow / --depth |
Not yet supported |
| Bare / mirror clone | Not yet supported |
SSH transport
Since 0.5.0, ls_remote, clone, fetch, and push support ssh://,
git+ssh://, and scp-style user@host:path URLs. pygritlib spawns the system
ssh (no embedded SSH library). Authentication (keys, ssh-agent, known_hosts,
~/.ssh/config) is entirely ssh's job; put the user in the URL
(ssh://user@host/...).
The username= / password= kwargs do not apply to ssh URLs and raise ValueError.
The ssh program is configurable per call with ssh_command= — a shell command line run
via sh -c, exactly like Git's GIT_SSH_COMMAND
(e.g. ssh_command="ssh -i ~/.ssh/id_ed25519 -o IdentitiesOnly=yes"). When omitted,
pygritlib follows Git's default precedence: $GIT_SSH_COMMAND, then $GIT_SSH, then
ssh. ls_remote, clone, fetch, and push all accept ssh_command=.
Example
import pygritlib
# Clone a public repo over https (the http stack is bundled).
repo = pygritlib.Repository.clone("https://github.com/octocat/Hello-World.git", "/tmp/hello")
print(repo.head().peel().hex)
# List a remote's branches without cloning.
for ref in pygritlib.ls_remote("https://github.com/octocat/Hello-World.git", heads=True):
print(ref.oid.hex, ref.name.decode())
# Authenticated fetch (token via kwarg, or https://<token>@host/...).
report = repo.fetch("https://github.com/me/private.git", username="x", password="TOKEN")
for u in report.updates:
print(u.mode, u.remote_ref.decode())
Pushing
Since 0.4.0, repo.push sends refs to a remote over git:// or https.
refspecs
refspecs is a list that may contain:
- Strings — git-style refspec shorthand:
"main"→ pushrefs/heads/maintorefs/heads/main(the source's fully-qualified ref is used as the destination); a tag"v1.0"→refs/tags/v1.0likewise."+a:b"— force-pushatob.":refs/heads/old"— deleterefs/heads/oldon the remote.- A bare object id with no explicit destination (e.g.
"abc123") raisesValueError— use aPushSpecinstead. - A source that isn't a local branch/tag/remote ref — including
HEAD— has no inferable destination and raisesValueError; give an explicit one, e.g."HEAD:refs/heads/main".
PushSpecobjects — for full control:PushSpec(dst, *, src=None, force=False, delete=False, expected_old=None, expect_absent=False)dst: bytes— the remote ref to update.src: ObjectId | None— the local object to push;Nonemeans delete.force: bool— force overwrite (no fast-forward check).delete: bool— delete the remote ref (equivalent tosrc=None).expected_old: ObjectId | None+expect_absent: bool— force-with-lease (safe force / create-only): the push is accepted only if the remote ref currently points atexpected_old(or is absent whenexpect_absent=True); a stale ref returnsstatus="reject-stale".
Results — returned, not raised
repo.push returns a PushReport (.results: list[PushRefResult], .ok: bool).
Rejections (non-fast-forward, hook-declined, stale lease, etc.) are returned as data
in each PushRefResult.status, not raised as exceptions. .ok is True only when every
ref in results has status of either "ok" or "up-to-date".
PushRefResult.status values:
| Status | Meaning |
|---|---|
"ok" |
Ref updated successfully |
"up-to-date" |
Remote already at the pushed value; no change |
"reject-non-fast-forward" |
Not a fast-forward; use force=True or PushSpec(force=True) |
"reject-already-exists" |
Remote ref exists and a create-only push was requested |
"reject-fetch-first" |
Remote requires a fetch before pushing |
"reject-needs-force" |
Remote requires an explicit force flag |
"reject-stale" |
Force-with-lease failed: remote ref is not at expected_old |
"remote-rejected" |
Remote hook or policy rejected the ref |
"atomic-push-failed" |
This ref failed because another ref in an atomic push was rejected |
Only transport/auth/protocol failures raise exceptions (NetworkError /
AuthenticationError).
Progress callback
The progress callback (Callable[[bytes], None]) receives the remote's side-band-2
output — the remote: … lines printed by server-side hooks and diagnostics. Unlike
fetch (where the callback never fires), push progress does fire.
Push example
import pygritlib
repo = pygritlib.Repository.open("/path/to/repo/.git", "/path/to/repo")
# Push local 'main' over https (token via kwarg or https://<token>@host/...).
report = repo.push("https://github.com/me/repo.git", ["main"], username="x", password="TOKEN")
for r in report.results:
print(r.status, r.remote_ref.decode(), r.message or "")
if not report.ok:
raise SystemExit("push rejected")
# Force-with-lease (safe force) via a structured PushSpec:
tip = repo.resolve("refs/heads/main")
expected = repo.resolve("refs/remotes/origin/main")
spec = pygritlib.PushSpec(b"refs/heads/main", src=tip, expected_old=expected)
repo.push("https://github.com/me/repo.git", [spec])
# Delete a remote branch:
repo.push("https://github.com/me/repo.git", [":refs/heads/old-feature"])
Supported Python / platforms
- CPython 3.11+ — wheels are
abi3-py311, so a single wheel works on 3.11 and every newer 3.x. (Standard abi3 wheels do not target free-threaded / no-GIL CPython.) - Linux (glibc) —
manylinux_2_17wheels for x86_64 and aarch64. - Linux (musl) —
musllinux_1_2wheels for x86_64 and aarch64 (Alpine and other musl-based distros / containers). - macOS — arm64 (Apple silicon) wheels. Intel (x86_64) Macs install from the sdist, which compiles cleanly (grit-lib is Unix-oriented and its dependencies are pure-Rust); no prebuilt Intel wheel is shipped because GitHub's macOS-13 Intel runners are deprecated and unreliable.
- Windows — deferred until grit-lib gains Windows support (it currently
depends on
libc/nixand is Unix-oriented).
Byte / text policy
Git data is binary: paths, ref names, author/committer fields, and messages are
not guaranteed to be UTF-8. pygritlib therefore returns git data as bytes by
default and offers opt-in decoded accessors so decoding is always your explicit
choice:
TreeEntry.name,Repository.git_dir/work_tree,Reference.name,DiffEntry.old_path/new_path,Commit.message_bytes,Signature.name/emailreturnbytes(exact, byte-faithful).- Decoded counterparts:
Signature.name_str/email_str(UTF-8), andCommit.message(encoding="utf-8", errors="strict"), which decodes the verbatim message bytes with the encoding/errors you supply (pass the commit's own declaredencodingheader if it is not UTF-8).
Exception hierarchy
All pygritlib errors derive from a single base so you can catch broadly or narrowly:
GritError (base — also the catch-all for unmapped grit-lib errors)
├── RepositoryError open/discover/format-validation/ref failures
├── ObjectNotFoundError a requested object is not in the object database
├── InvalidObjectError an object is corrupt or cannot be parsed
├── RefMismatchError a ref's value failed a compare-and-swap / create-only check
├── NetworkError a network-level failure during fetch/clone/ls-remote
└── AuthenticationError authentication failed or credentials were rejected
I/O failures surface as OSError (with errno where available), and the
originating grit-lib message is included in the raised exception's message text.
Known limitations
This is honest about where grit-lib 0.4.1's API constrains byte-fidelity or behavior:
- Diff paths are UTF-8-decoded by grit-lib (
Option<String>), so a non-UTF-8 diff path is not byte-preserved — unlikeTreeEntry.name, which is exact bytes. (Normal ASCII/UTF-8 paths are unaffected.) - Annotated tags: grit-lib 0.4.1's
parse_tagrejects non-UTF-8 tags and exposes the tag name / tagger / message as UTF-8Stringonly — so tag identities and messages are not byte-preserved the way commits are. - Diffstat line counts use grit-lib's
count_changes(thesimilarcrate), which treats a bare\ras a line break, whereasgit --numstatsplits on\nonly. Stats can therefore diverge from git for files containing bare-CR content; ordinary\n-terminated text matches git exactly. resolve()on an unknown revision raises the baseGritError(grit-lib returns a generic "unknown revision or path" message, not a typed not-found error).- Reference names are decoded lossily: grit-lib returns ref names as a UTF-8
String(viato_string_lossy), so non-UTF-8 ref names are not byte-faithful — distinct non-UTF-8 names can collide on the U+FFFD replacement character. This is unlikeTreeEntry.name, which is exact bytes. - Annotated-tag write fidelity: grit-lib's
TagDatastores the tag name, tagger, and message as UTF-8String(no raw-byte fields), so written tags must be UTF-8 and the tag message's trailing newline is normalized — unlike commits, whose ids are byte-exact via theauthor_raw/committer_raw/raw-message escape hatches. - Ref compare-and-swap is best-effort (TOCTOU): grit-lib 0.4.1 exposes no atomic
compare-and-swap primitive, so
expected_old=/create=do a read→compare→write without a held lock — they catch the common non-concurrent case but are not a hard guarantee against a concurrent writer. Atomic ref updates are planned for a later release. - No fetch transfer progress (grit-lib 0.4.1): grit-lib hard-codes
no-progressin its fetch request, so there is no progress callback for fetch/clone and one cannot be added at the binding layer. (Push progress — remote hook/diagnostic output — does fire.) fetch(tags="following")shared-oid quirk (grit-lib 0.4.1): if a tag points at the same commit as a fetched branch tip, grit-lib 0.4.1's tag-following can skip that commit's objects; workaround: usetags="all"ortags="none".clone()always usestags="all"and is unaffected.- Push is v1 only (grit-lib 0.4.1): grit-lib's push implementation uses Git protocol v1; the server's v2 advertisement is ignored, and the client always negotiates v1. This is transparent in practice (all public hosts support v1), but it means v2-specific push features are unavailable.
- Push: no SSH, no signed push, no submodule push:
repo.pushsupportsgit://andhttpsonly;ssh:///git@are not yet supported. Signed push (--signed) and submodule-aware push are also not yet exposed. - Push: string refspecs cannot express force-with-lease. String shorthand (e.g.
"main","+a:b") cannot encode aexpected_oldconstraint. Use aPushSpecobject for force-with-lease. - Still out of scope (planned later phases): working-tree checkout, SSH
transport, shallow/depth clone, bare/mirror clone, submodules, and
insteadOfURL rewriting are not yet exposed.
Security considerations / untrusted repositories
pygritlib is a thin binding over grit-lib 0.4.1 and inherits its behavior. It is intended for trusted, local repositories and is not hardened against adversarial repository content. The caveats below are upstream characteristics of grit-lib 0.4.1 that cannot be fixed in the binding layer; they are candidates for future hardening. Do not point pygritlib at repositories you do not control without the external mitigations noted.
- No resource limits on object reads (DoS). grit-lib decompresses loose objects
with unbounded reads and preallocates packed-object buffers from attacker-controlled
size headers. A maliciously crafted repository (a decompression bomb, or huge
declared object sizes) can exhaust memory and abort the host Python process — and an
out-of-memory abort cannot be caught as a Python exception. Do not read objects
from untrusted repositories in-process without external resource limits
(
ulimit/cgroups) or sandboxing. Repository.discover()may change the process working directory. For repositories with a relativecore.worktree, grit-lib's discovery canchdir()the process (and setGIT_PREFIX) and may not restore the CWD on failure. PreferRepository.open(git_dir, work_tree)for untrusted or concurrency-sensitive use, and treatdiscover()on untrusted repositories with caution.- Reference enumeration follows symlinks without containment. grit-lib's loose-ref
walk follows symlinked directories and lacks depth/containment/visited checks, so a
crafted repository can make
references()traverse outside the repository or loop through large trees. Avoid enumerating refs on untrusted repositories.
The write surface (0.2.0) validates its own inputs at the binding layer — ref names (git
ref-format rules), index paths (no ../absolute components, no leading-slash), Signature
fields, and reflog/tag text — which closes the path-traversal, object/record-injection, and
malformed-index crash (grit-lib stack-overflow) vectors those calls would otherwise inherit.
One upstream write caveat remains: grit-lib writes objects through a deterministic temp file,
so concurrent identical object writes can race (this needs a grit-lib-level fix); distinct
concurrent writes are unaffected.
How it maps to grit-lib
pygritlib is a documented Python façade over grit-lib, not a literal 1:1
re-export. grit-lib 0.4.1 exposes a free-function / data-struct style API (public
fields, free functions taking &Repository/&Odb/git_dir, and parse_*
functions over raw bytes); pygritlib constructs the ergonomic Python classes
(Repository, typed object views, Reference, Signature) on top of those
primitives. The complete, verified mapping — exact module paths, signatures,
return/error types, and the error → exception table — lives in
docs/superpowers/api-matrix.md.
Version compatibility
pygritlib pins grit-lib exactly (= pin) with a committed Cargo.lock and
--locked builds for reproducibility (the published crate fully exposes read-core,
so no git-revision fallback is used).
| pygritlib | grit-lib | pyo3 | Rust toolchain | Python (abi3) | License | Notes |
|---|---|---|---|---|---|---|
| 0.4.0 | =0.4.1 (MIT) |
=0.23.3 |
1.94.1 | ≥ 3.11 | MIT | + push over git:// and https |
| 0.3.0 | =0.4.1 (MIT) |
=0.23.3 |
1.94.1 | ≥ 3.11 | MIT | + read-path networking |
| 0.2.0 | =0.4.1 (MIT) |
=0.23.3 |
1.94.1 | ≥ 3.11 | MIT | + local write-core |
| 0.1.0 | =0.4.1 (MIT) |
=0.23.3 |
1.94.1 | ≥ 3.11 | MIT | read-core release |
Releasing
pygritlib publishes to PyPI via trusted publishing
(OpenID Connect) — no API tokens are stored in the repo. Publishing a GitHub Release
runs .github/workflows/release.yml, which rebuilds
the wheels + sdist with the same build recipe CI uses (released glibc Linux wheels
target the broader manylinux_2_17 tag; musl wheels use musllinux_1_2), re-smoke-tests
every artifact, checks the tag and provenance, and uploads to PyPI over OIDC.
One-time setup (maintainer, manual)
These cannot be automated and must be done once before the first release:
-
Register the PyPI "pending publisher" at https://pypi.org/manage/account/publishing/:
- PyPI Project Name:
pygritlib - Owner:
linsomniac - Repository name:
pygritlib - Workflow name:
release.yml - Environment name:
pypi
For the dry-run path, repeat at https://test.pypi.org/manage/account/publishing/ with Environment name
testpypi. A pending publisher does not reserve the name, so cut the first real release promptly to claimpygritlib. - PyPI Project Name:
-
Create the protected GitHub Environments (Settings → Environments). GitHub silently auto-creates an unprotected environment if a workflow merely references one, so create them explicitly:
pypi— restrict deployments to protectedv*tags (back it with a repository ruleset that protectsv*tags). Required-reviewer protection is impractical for a solo maintainer (self-review is blocked); add a reviewer if the project gains maintainers.testpypi— restrict deployments to themainbranch.
Cutting a release
- Bump the version in both
Cargo.toml([package] version) andCargo.lock: editCargo.toml, then runcargo update -p pygritlib(orcargo buildwithout--locked) so the lockfile matches. The workflow'scargo metadata --lockedversion guard fails ifCargo.lockis stale. - Commit to
mainand push. - Create a GitHub Release with tag
vX.Y.Z(final releases only — the version guard rejects anything that is notvX.Y.Z). Publishing the release builds and smoke-tests the five wheels + sdist, verifiestag == crate versionand that the commit is onmain, and publishes to PyPI automatically.
TestPyPI dry-run (optional)
Trigger the workflow manually (Actions → Release → "Run workflow") to build and publish to TestPyPI instead of PyPI. Because PyPI/TestPyPI filenames are immutable, a repeat dry-run needs a unique version (bump the patch). A green dry-run validates the build/smoke/OIDC mechanics, but TestPyPI uses a separate trusted-publisher registration, so it does not prove the real-PyPI config — the first live release does.
License
MIT — matching grit-lib (also MIT). See LICENSE if present, and the
license metadata in pyproject.toml / Cargo.toml.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pygritlib-0.5.0.tar.gz.
File metadata
- Download URL: pygritlib-0.5.0.tar.gz
- Upload date:
- Size: 330.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f12b7ded8c2aa2ef44dbcde43b2c6c3583665cf4f99dc03400fa9874d0873357
|
|
| MD5 |
36f8d5f333de521b8e50402ef1da6aaa
|
|
| BLAKE2b-256 |
f0c9edd9ea4073104480f48f3c988b1e50ab0152aa487b5df28da411c84b5adb
|
Provenance
The following attestation bundles were made for pygritlib-0.5.0.tar.gz:
Publisher:
release.yml on linsomniac/pygritlib
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pygritlib-0.5.0.tar.gz -
Subject digest:
f12b7ded8c2aa2ef44dbcde43b2c6c3583665cf4f99dc03400fa9874d0873357 - Sigstore transparency entry: 1874830053
- Sigstore integration time:
-
Permalink:
linsomniac/pygritlib@0ae1d9c0d434d0a83f6065e63b8a46b6def89bf0 -
Branch / Tag:
refs/tags/v0.5.0 - Owner: https://github.com/linsomniac
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@0ae1d9c0d434d0a83f6065e63b8a46b6def89bf0 -
Trigger Event:
release
-
Statement type:
File details
Details for the file pygritlib-0.5.0-cp311-abi3-musllinux_1_2_x86_64.whl.
File metadata
- Download URL: pygritlib-0.5.0-cp311-abi3-musllinux_1_2_x86_64.whl
- Upload date:
- Size: 4.2 MB
- Tags: CPython 3.11+, musllinux: musl 1.2+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7090d3a7c63fa3d7a76d29124cac4b63cc0c750106e4917417eb5a768f1da35e
|
|
| MD5 |
019c5d0cfad994b3a4d8709713b956bd
|
|
| BLAKE2b-256 |
6fad94a7cd5cbec0f6160bc3b59e1e168dbf7ac65d5f8db584f1c26fbf7448c3
|
Provenance
The following attestation bundles were made for pygritlib-0.5.0-cp311-abi3-musllinux_1_2_x86_64.whl:
Publisher:
release.yml on linsomniac/pygritlib
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pygritlib-0.5.0-cp311-abi3-musllinux_1_2_x86_64.whl -
Subject digest:
7090d3a7c63fa3d7a76d29124cac4b63cc0c750106e4917417eb5a768f1da35e - Sigstore transparency entry: 1874830237
- Sigstore integration time:
-
Permalink:
linsomniac/pygritlib@0ae1d9c0d434d0a83f6065e63b8a46b6def89bf0 -
Branch / Tag:
refs/tags/v0.5.0 - Owner: https://github.com/linsomniac
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@0ae1d9c0d434d0a83f6065e63b8a46b6def89bf0 -
Trigger Event:
release
-
Statement type:
File details
Details for the file pygritlib-0.5.0-cp311-abi3-musllinux_1_2_aarch64.whl.
File metadata
- Download URL: pygritlib-0.5.0-cp311-abi3-musllinux_1_2_aarch64.whl
- Upload date:
- Size: 4.0 MB
- Tags: CPython 3.11+, musllinux: musl 1.2+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
da3dbe5c4aaebf3545d23f1154354dfb10dd2b62b684d5217ccf0b5c5899b8e8
|
|
| MD5 |
def088e759b46b3e09d884294d5b14e3
|
|
| BLAKE2b-256 |
8a3ae0afe871e6cd8e0dc711ae9763a53575a5a2f4fed3fb0515420680502cd8
|
Provenance
The following attestation bundles were made for pygritlib-0.5.0-cp311-abi3-musllinux_1_2_aarch64.whl:
Publisher:
release.yml on linsomniac/pygritlib
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pygritlib-0.5.0-cp311-abi3-musllinux_1_2_aarch64.whl -
Subject digest:
da3dbe5c4aaebf3545d23f1154354dfb10dd2b62b684d5217ccf0b5c5899b8e8 - Sigstore transparency entry: 1874830435
- Sigstore integration time:
-
Permalink:
linsomniac/pygritlib@0ae1d9c0d434d0a83f6065e63b8a46b6def89bf0 -
Branch / Tag:
refs/tags/v0.5.0 - Owner: https://github.com/linsomniac
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@0ae1d9c0d434d0a83f6065e63b8a46b6def89bf0 -
Trigger Event:
release
-
Statement type:
File details
Details for the file pygritlib-0.5.0-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.
File metadata
- Download URL: pygritlib-0.5.0-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- Upload date:
- Size: 4.0 MB
- Tags: CPython 3.11+, manylinux: glibc 2.17+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5a6bd2ed145cb34050a05c720cc4fb980a336caf7c7205bda76e4a7ea1e81f90
|
|
| MD5 |
4cac955068804a935c161943387c0ee8
|
|
| BLAKE2b-256 |
c8f677693c5fbc07486cdfa1ad4d832fe3e3da97a0e78ebb31e5bb1ecab0a0fc
|
Provenance
The following attestation bundles were made for pygritlib-0.5.0-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:
Publisher:
release.yml on linsomniac/pygritlib
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pygritlib-0.5.0-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl -
Subject digest:
5a6bd2ed145cb34050a05c720cc4fb980a336caf7c7205bda76e4a7ea1e81f90 - Sigstore transparency entry: 1874831000
- Sigstore integration time:
-
Permalink:
linsomniac/pygritlib@0ae1d9c0d434d0a83f6065e63b8a46b6def89bf0 -
Branch / Tag:
refs/tags/v0.5.0 - Owner: https://github.com/linsomniac
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@0ae1d9c0d434d0a83f6065e63b8a46b6def89bf0 -
Trigger Event:
release
-
Statement type:
File details
Details for the file pygritlib-0.5.0-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.
File metadata
- Download URL: pygritlib-0.5.0-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
- Upload date:
- Size: 3.8 MB
- Tags: CPython 3.11+, manylinux: glibc 2.17+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3670b8499bc1dc46053d8549b4dfd8e83a47f98f57d017fc1167407e588fc8af
|
|
| MD5 |
6ed15fc6c003037220cc86eedc75c65a
|
|
| BLAKE2b-256 |
2ccfd12a939090b6095b1d85a39518477658095bbdf039e105db3c7ac2426c7c
|
Provenance
The following attestation bundles were made for pygritlib-0.5.0-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl:
Publisher:
release.yml on linsomniac/pygritlib
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pygritlib-0.5.0-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl -
Subject digest:
3670b8499bc1dc46053d8549b4dfd8e83a47f98f57d017fc1167407e588fc8af - Sigstore transparency entry: 1874830646
- Sigstore integration time:
-
Permalink:
linsomniac/pygritlib@0ae1d9c0d434d0a83f6065e63b8a46b6def89bf0 -
Branch / Tag:
refs/tags/v0.5.0 - Owner: https://github.com/linsomniac
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@0ae1d9c0d434d0a83f6065e63b8a46b6def89bf0 -
Trigger Event:
release
-
Statement type:
File details
Details for the file pygritlib-0.5.0-cp311-abi3-macosx_11_0_arm64.whl.
File metadata
- Download URL: pygritlib-0.5.0-cp311-abi3-macosx_11_0_arm64.whl
- Upload date:
- Size: 3.5 MB
- Tags: CPython 3.11+, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f78ea4e81200f39d2a07e46458436d89e5e52adc7ea630b09fb94116087a6cc3
|
|
| MD5 |
b93f89f7b20189369d48ca8fb273d8ca
|
|
| BLAKE2b-256 |
a17540d20cfbb4bfb59e18b08685c51c75c7291101d457b2058ea0ab3c86237f
|
Provenance
The following attestation bundles were made for pygritlib-0.5.0-cp311-abi3-macosx_11_0_arm64.whl:
Publisher:
release.yml on linsomniac/pygritlib
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pygritlib-0.5.0-cp311-abi3-macosx_11_0_arm64.whl -
Subject digest:
f78ea4e81200f39d2a07e46458436d89e5e52adc7ea630b09fb94116087a6cc3 - Sigstore transparency entry: 1874830825
- Sigstore integration time:
-
Permalink:
linsomniac/pygritlib@0ae1d9c0d434d0a83f6065e63b8a46b6def89bf0 -
Branch / Tag:
refs/tags/v0.5.0 - Owner: https://github.com/linsomniac
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@0ae1d9c0d434d0a83f6065e63b8a46b6def89bf0 -
Trigger Event:
release
-
Statement type: