ohbin 0.2.2__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.
ohbin-0.2.2/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 prostomarkeloff
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
ohbin-0.2.2/PKG-INFO ADDED
@@ -0,0 +1,161 @@
1
+ Metadata-Version: 2.4
2
+ Name: ohbin
3
+ Version: 0.2.2
4
+ Summary: Declarative GitHub-release binaries for uv projects — declare a tool in pyproject, `ohbin run <tool>` downloads, SHA256-verifies, caches, and execs it. POSIX only (uses flock).
5
+ Author: prostomarkeloff
6
+ Author-email: prostomarkeloff <prostomarkeloff@ohreally.me>
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Requires-Dist: tomlkit>=0.13
10
+ Requires-Python: >=3.11
11
+ Description-Content-Type: text/markdown
12
+
13
+ # ohbin
14
+
15
+ [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
16
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
17
+
18
+ ohbin runs the binaries your project needs but can't `pip install`. You know the ones:
19
+ `ripgrep` ([or… can you?](https://pypi.org/project/ripgrep/)), [`find-dup-defs`](https://github.com/prostomarkeloff/find-dup-defs), [`oasdiff`](https://github.com/oasdiff/oasdiff), some linter
20
+ written in Rust that only ships as a GitHub release. uv
21
+ installs Python packages, and those aren't Python packages, so normally you're stuck either
22
+ telling everyone to install them by hand and watching the versions drift, or writing a little
23
+ download-and-verify wrapper package and copying it into every repo.
24
+
25
+ With ohbin you just write the tool down in your `pyproject.toml`. The first time you run it,
26
+ ohbin downloads it, checks it against a SHA256 you pinned, caches it, and runs it. One
27
+ dev-dependency, as many tools as you want.
28
+
29
+ It's a small thing on purpose, built for people who already live in uv. Your binaries get
30
+ pinned right next to your Python deps, in the same file, and run through the same flow.
31
+
32
+ What it gives you:
33
+
34
+ * binaries pinned to a version, pulled from GitHub releases
35
+ * a SHA256 per platform, checked before anything gets unpacked
36
+ * one dev-dependency, however many tools you declare
37
+ * a per-host cache that's safe to hit from parallel CI
38
+ * mostly stdlib (it shells out to `gh` and `openssl` only for the private-gist part)
39
+
40
+ ## Installation
41
+
42
+ It's a dev dependency, so with uv:
43
+
44
+ ```sh
45
+ uv add --dev git+https://github.com/prostomarkeloff/ohbin.git
46
+ ```
47
+
48
+ ## How to?
49
+
50
+ Say you want ripgrep. Point ohbin at the repo:
51
+
52
+ ```sh
53
+ uv run ohbin add BurntSushi/ripgrep --version 14.1.1 --name rg --binary rg
54
+ ```
55
+
56
+ This goes and looks at the release, finds the right asset for each platform, pins the SHA256s,
57
+ and writes a little table into your `pyproject.toml`. Your comments and formatting stay where
58
+ they are:
59
+
60
+ ```toml
61
+ [tool.ohbin.tools.rg]
62
+ repo = "BurntSushi/ripgrep"
63
+ version = "14.1.1"
64
+ binary = "rg"
65
+ # add writes one [..assets.<os>-<arch>] table per platform under here, checksums and all
66
+ ```
67
+
68
+ `--name` is what you'll type when it's different from the repo name (ripgrep becomes rg), and
69
+ `--binary` is the actual executable inside the archive. If add guesses an asset wrong, don't
70
+ fight it, the table is the source of truth, just fix the line.
71
+
72
+ Then run it:
73
+
74
+ ```sh
75
+ uv run ohbin run rg -- --files # first time: downloads, checks, caches, runs
76
+ uv run ohbin run rg -- TODO src/ # after that it just runs
77
+ ```
78
+
79
+ ohbin hands the process straight over with execv, so the tool itself gets stdin, stdout,
80
+ signals and the exit code, exactly like you'd run it yourself. In a Makefile I usually hide
81
+ the prefix behind a variable:
82
+
83
+ ```make
84
+ RG := uv run ohbin run rg --
85
+ search:; $(RG) TODO src/
86
+ ```
87
+
88
+ `ohbin which fd` prints the cached path (and downloads it first if it has to), and `ohbin list`
89
+ shows what you've declared.
90
+
91
+ ### Private binaries
92
+
93
+ Sometimes the binary isn't on a public release page. Maybe you built it yourself and you don't
94
+ want it in a repo at all. ohbin can ship it through a secret gist instead, encrypted with a
95
+ password:
96
+
97
+ ```sh
98
+ uv run ohbin publish-gist ./dist/mytool --password "$PW"
99
+ ```
100
+
101
+ That gzips it, encrypts it, and drops one gist file per platform plus a small index. Run it
102
+ from each platform's own machine, passing `--gist <id>` to add to the same gist. After that
103
+ it's just another tool:
104
+
105
+ ```sh
106
+ uv run ohbin add-gist https://gist.github.com/you/ab12… --name mytool
107
+ uv run ohbin run --password "$PW" mytool -- --help
108
+ ```
109
+
110
+ Why a gist and not a private repo? Because a gist isn't tied to a repo, and that's the whole
111
+ point. You don't commit the binary anywhere, you don't hand out repo access and tokens to
112
+ everyone who needs it, you just give them a link and a password. The link is unlisted and the
113
+ bytes are AES-256-CBC, so a leaked link on its own is nothing without the password. The
114
+ password goes to openssl over a file descriptor, never on the command line. To take access
115
+ away, delete the gist or change the password.
116
+
117
+ ### From Python
118
+
119
+ If you want the path instead of running the thing:
120
+
121
+ ```python
122
+ from ohbin import ensure
123
+
124
+ path = ensure("rg") # a Path, downloaded and checked the first time
125
+ ```
126
+
127
+ It finds your `pyproject.toml` by walking up from wherever you are. Set `OHBIN_PYPROJECT` if
128
+ you need to point it at a specific file, like in CI.
129
+
130
+ ## How it works
131
+
132
+ Nothing clever. On `ohbin run rg`, it reads the rg table, works out your os and arch, and looks
133
+ for `~/.cache/ohbin/rg/14.1.1/rg`. If it's there, it runs it. If not, it downloads under a lock
134
+ (so two parallel runs don't race), checks the SHA256 before unpacking anything, extracts, and
135
+ runs. The version is in the cache path, so bumping it is just a fresh download that doesn't step
136
+ on the old one. Downloads retry with backoff, and a real 404 doesn't get mistaken for a flaky
137
+ network.
138
+
139
+ ## Limitations
140
+
141
+ POSIX only for now, the locking uses `fcntl` so it won't even import on Windows. add
142
+ auto-resolves four platforms (linux and macOS, x86_64 and arm64); anything else (windows, musl,
143
+ riscv) you add to the table by hand and the engine runs it fine. Asset matching is just looking
144
+ at the os/arch words in the filename and preferring `.tar.gz`, so a weird naming scheme might
145
+ cost you a one-line fix.
146
+
147
+ ## Development
148
+
149
+ ```sh
150
+ git clone https://github.com/prostomarkeloff/ohbin
151
+ cd ohbin && uv sync
152
+
153
+ make lint-heavy # ruff format + check + pyright
154
+ make test-full # the network-free test suite
155
+ ```
156
+
157
+ ## License
158
+
159
+ MIT, see [LICENSE](LICENSE).
160
+
161
+ Made with 📦 by [prostomarkeloff](https://github.com/prostomarkeloff) and contributors.
ohbin-0.2.2/README.md ADDED
@@ -0,0 +1,149 @@
1
+ # ohbin
2
+
3
+ [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ ohbin runs the binaries your project needs but can't `pip install`. You know the ones:
7
+ `ripgrep` ([or… can you?](https://pypi.org/project/ripgrep/)), [`find-dup-defs`](https://github.com/prostomarkeloff/find-dup-defs), [`oasdiff`](https://github.com/oasdiff/oasdiff), some linter
8
+ written in Rust that only ships as a GitHub release. uv
9
+ installs Python packages, and those aren't Python packages, so normally you're stuck either
10
+ telling everyone to install them by hand and watching the versions drift, or writing a little
11
+ download-and-verify wrapper package and copying it into every repo.
12
+
13
+ With ohbin you just write the tool down in your `pyproject.toml`. The first time you run it,
14
+ ohbin downloads it, checks it against a SHA256 you pinned, caches it, and runs it. One
15
+ dev-dependency, as many tools as you want.
16
+
17
+ It's a small thing on purpose, built for people who already live in uv. Your binaries get
18
+ pinned right next to your Python deps, in the same file, and run through the same flow.
19
+
20
+ What it gives you:
21
+
22
+ * binaries pinned to a version, pulled from GitHub releases
23
+ * a SHA256 per platform, checked before anything gets unpacked
24
+ * one dev-dependency, however many tools you declare
25
+ * a per-host cache that's safe to hit from parallel CI
26
+ * mostly stdlib (it shells out to `gh` and `openssl` only for the private-gist part)
27
+
28
+ ## Installation
29
+
30
+ It's a dev dependency, so with uv:
31
+
32
+ ```sh
33
+ uv add --dev git+https://github.com/prostomarkeloff/ohbin.git
34
+ ```
35
+
36
+ ## How to?
37
+
38
+ Say you want ripgrep. Point ohbin at the repo:
39
+
40
+ ```sh
41
+ uv run ohbin add BurntSushi/ripgrep --version 14.1.1 --name rg --binary rg
42
+ ```
43
+
44
+ This goes and looks at the release, finds the right asset for each platform, pins the SHA256s,
45
+ and writes a little table into your `pyproject.toml`. Your comments and formatting stay where
46
+ they are:
47
+
48
+ ```toml
49
+ [tool.ohbin.tools.rg]
50
+ repo = "BurntSushi/ripgrep"
51
+ version = "14.1.1"
52
+ binary = "rg"
53
+ # add writes one [..assets.<os>-<arch>] table per platform under here, checksums and all
54
+ ```
55
+
56
+ `--name` is what you'll type when it's different from the repo name (ripgrep becomes rg), and
57
+ `--binary` is the actual executable inside the archive. If add guesses an asset wrong, don't
58
+ fight it, the table is the source of truth, just fix the line.
59
+
60
+ Then run it:
61
+
62
+ ```sh
63
+ uv run ohbin run rg -- --files # first time: downloads, checks, caches, runs
64
+ uv run ohbin run rg -- TODO src/ # after that it just runs
65
+ ```
66
+
67
+ ohbin hands the process straight over with execv, so the tool itself gets stdin, stdout,
68
+ signals and the exit code, exactly like you'd run it yourself. In a Makefile I usually hide
69
+ the prefix behind a variable:
70
+
71
+ ```make
72
+ RG := uv run ohbin run rg --
73
+ search:; $(RG) TODO src/
74
+ ```
75
+
76
+ `ohbin which fd` prints the cached path (and downloads it first if it has to), and `ohbin list`
77
+ shows what you've declared.
78
+
79
+ ### Private binaries
80
+
81
+ Sometimes the binary isn't on a public release page. Maybe you built it yourself and you don't
82
+ want it in a repo at all. ohbin can ship it through a secret gist instead, encrypted with a
83
+ password:
84
+
85
+ ```sh
86
+ uv run ohbin publish-gist ./dist/mytool --password "$PW"
87
+ ```
88
+
89
+ That gzips it, encrypts it, and drops one gist file per platform plus a small index. Run it
90
+ from each platform's own machine, passing `--gist <id>` to add to the same gist. After that
91
+ it's just another tool:
92
+
93
+ ```sh
94
+ uv run ohbin add-gist https://gist.github.com/you/ab12… --name mytool
95
+ uv run ohbin run --password "$PW" mytool -- --help
96
+ ```
97
+
98
+ Why a gist and not a private repo? Because a gist isn't tied to a repo, and that's the whole
99
+ point. You don't commit the binary anywhere, you don't hand out repo access and tokens to
100
+ everyone who needs it, you just give them a link and a password. The link is unlisted and the
101
+ bytes are AES-256-CBC, so a leaked link on its own is nothing without the password. The
102
+ password goes to openssl over a file descriptor, never on the command line. To take access
103
+ away, delete the gist or change the password.
104
+
105
+ ### From Python
106
+
107
+ If you want the path instead of running the thing:
108
+
109
+ ```python
110
+ from ohbin import ensure
111
+
112
+ path = ensure("rg") # a Path, downloaded and checked the first time
113
+ ```
114
+
115
+ It finds your `pyproject.toml` by walking up from wherever you are. Set `OHBIN_PYPROJECT` if
116
+ you need to point it at a specific file, like in CI.
117
+
118
+ ## How it works
119
+
120
+ Nothing clever. On `ohbin run rg`, it reads the rg table, works out your os and arch, and looks
121
+ for `~/.cache/ohbin/rg/14.1.1/rg`. If it's there, it runs it. If not, it downloads under a lock
122
+ (so two parallel runs don't race), checks the SHA256 before unpacking anything, extracts, and
123
+ runs. The version is in the cache path, so bumping it is just a fresh download that doesn't step
124
+ on the old one. Downloads retry with backoff, and a real 404 doesn't get mistaken for a flaky
125
+ network.
126
+
127
+ ## Limitations
128
+
129
+ POSIX only for now, the locking uses `fcntl` so it won't even import on Windows. add
130
+ auto-resolves four platforms (linux and macOS, x86_64 and arm64); anything else (windows, musl,
131
+ riscv) you add to the table by hand and the engine runs it fine. Asset matching is just looking
132
+ at the os/arch words in the filename and preferring `.tar.gz`, so a weird naming scheme might
133
+ cost you a one-line fix.
134
+
135
+ ## Development
136
+
137
+ ```sh
138
+ git clone https://github.com/prostomarkeloff/ohbin
139
+ cd ohbin && uv sync
140
+
141
+ make lint-heavy # ruff format + check + pyright
142
+ make test-full # the network-free test suite
143
+ ```
144
+
145
+ ## License
146
+
147
+ MIT, see [LICENSE](LICENSE).
148
+
149
+ Made with 📦 by [prostomarkeloff](https://github.com/prostomarkeloff) and contributors.
@@ -0,0 +1,51 @@
1
+ [project]
2
+ name = "ohbin"
3
+ version = "0.2.2"
4
+ description = "Declarative GitHub-release binaries for uv projects — declare a tool in pyproject, `ohbin run <tool>` downloads, SHA256-verifies, caches, and execs it. POSIX only (uses flock)."
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "prostomarkeloff", email = "prostomarkeloff@ohreally.me" }
8
+ ]
9
+ license = "MIT"
10
+ license-files = ["LICENSE"]
11
+ requires-python = ">=3.11"
12
+ # Hot path (`run` / `ensure`) is pure stdlib. tomlkit is only imported by `add`,
13
+ # which edits pyproject.toml and must preserve its comments/formatting.
14
+ dependencies = [
15
+ "tomlkit>=0.13",
16
+ ]
17
+
18
+ [project.scripts]
19
+ ohbin = "ohbin.cli:main"
20
+
21
+ [dependency-groups]
22
+ dev = [
23
+ "pyright>=1.1.409",
24
+ "pytest>=8.0",
25
+ "ruff>=0.15.14",
26
+ ]
27
+
28
+ [build-system]
29
+ requires = ["uv_build>=0.8.1,<0.9.0"]
30
+ build-backend = "uv_build"
31
+
32
+ [tool.ruff]
33
+ target-version = "py311"
34
+ line-length = 120
35
+ src = ["src", "tests"]
36
+
37
+ [tool.ruff.lint]
38
+ # Conservative ruleset: errors/warnings, pyflakes, isort, bugbear, pyupgrade,
39
+ # naming, comprehensions, simplify, ruff-specific.
40
+ select = ["E", "F", "W", "I", "B", "UP", "N", "C4", "SIM", "RUF"]
41
+
42
+ [tool.ruff.lint.per-file-ignores]
43
+ # Tests intentionally reach into `_*`-prefixed internals.
44
+ "tests/*" = ["SLF001"]
45
+
46
+ [tool.pyright]
47
+ include = ["src", "tests"]
48
+ pythonVersion = "3.11"
49
+ typeCheckingMode = "basic"
50
+ reportMissingImports = true
51
+ reportUnusedImport = true
@@ -0,0 +1,95 @@
1
+ """ohbin — declarative GitHub-release binaries for uv projects.
2
+
3
+ Declare tools in your project's pyproject:
4
+
5
+ [tool.ohbin.tools.rg]
6
+ repo = "BurntSushi/ripgrep"
7
+ version = "14.1.1"
8
+ binary = "rg"
9
+
10
+ [tool.ohbin.tools.rg.assets.darwin-arm64]
11
+ url = "https://github.com/BurntSushi/ripgrep/releases/download/14.1.1/ripgrep-14.1.1-aarch64-apple-darwin.tar.gz"
12
+ sha256 = "..."
13
+
14
+ then `uv run ohbin run rg -- <args>`, or in-process `ohbin.ensure("rg")`.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import sys
20
+ from pathlib import Path
21
+
22
+ from ohbin._engine import (
23
+ BinaryNotFoundError,
24
+ ChecksumMismatchError,
25
+ MissingPasswordError,
26
+ cache_root,
27
+ ensure_from,
28
+ )
29
+ from ohbin._errors import OhbinError
30
+ from ohbin._manifest import ManifestError, find_pyproject, load_tool, load_tools
31
+ from ohbin._platform import Platform, UnsupportedPlatformError, current_platform
32
+ from ohbin._types import AssetEntry, ToolConfig
33
+
34
+ __all__ = [
35
+ "AssetEntry",
36
+ "BinaryNotFoundError",
37
+ "ChecksumMismatchError",
38
+ "ManifestError",
39
+ "MissingPasswordError",
40
+ "OhbinError",
41
+ "Platform",
42
+ "ToolConfig",
43
+ "UnsupportedPlatformError",
44
+ "cache_root",
45
+ "current_platform",
46
+ "ensure",
47
+ "ensure_from",
48
+ "find_pyproject",
49
+ "load_tool",
50
+ "load_tools",
51
+ ]
52
+
53
+
54
+ def _resolve_password(tool: str, cfg: ToolConfig, explicit: str | None) -> str | None:
55
+ """`--password` wins; otherwise fall back to the manifest, warning unless ack'd."""
56
+ if explicit is not None:
57
+ return explicit
58
+ pw = cfg.get("password")
59
+ if pw is not None and not cfg.get("password_committed_ok"):
60
+ print(
61
+ f"ohbin: warning: reading the password for {tool!r} from pyproject — keep it out "
62
+ "of public repos; set 'password_committed_ok = true' to silence this.",
63
+ file=sys.stderr,
64
+ )
65
+ return pw
66
+
67
+
68
+ def ensure(tool: str, *, pyproject: Path | None = None, password: str | None = None) -> Path:
69
+ """Return the cached binary path for a declared tool, downloading on first use.
70
+
71
+ Reads `[tool.ohbin.tools.<tool>]` from the project's pyproject (discovered
72
+ from CWD, or `OHBIN_PYPROJECT`). This is the in-process entry point — call it
73
+ from build tooling that needs the binary's path rather than exec-ing it.
74
+
75
+ For encrypted tools, `password` (from `--password`) takes precedence over the
76
+ manifest's `password` field.
77
+ """
78
+ cfg = load_tool(tool, pyproject=pyproject)
79
+ plat = current_platform()
80
+ entry = cfg["assets"].get(plat.key)
81
+ if entry is None:
82
+ declared = ", ".join(sorted(cfg["assets"])) or "none"
83
+ msg = f"tool {tool!r} has no asset for {plat.key} (declared: {declared})"
84
+ raise UnsupportedPlatformError(msg)
85
+ encrypted = bool(cfg.get("encrypted"))
86
+ return ensure_from(
87
+ tool=tool,
88
+ version=cfg["version"],
89
+ binary=cfg["binary"],
90
+ url=entry["url"],
91
+ sha256=entry["sha256"],
92
+ encrypted=encrypted,
93
+ password=_resolve_password(tool, cfg, password) if encrypted else None,
94
+ binary_sha256=entry.get("binary_sha256"),
95
+ )
@@ -0,0 +1,10 @@
1
+ """Allow `python -m ohbin`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+
7
+ from ohbin.cli import main
8
+
9
+ if __name__ == "__main__":
10
+ sys.exit(main())