abx-pkg Β Β Β Β π¦ aptΒ brewΒ pipΒ npmΒ cargoΒ gemΒ gogetΒ nixΒ dockerΒ bashΒ chromewebstoreΒ puppeteer
Simple Python interfaces for package managers + installed binaries.
It's an ORM for your package managers, providing a nice python types for packages + installers.
This is a Python library for installing & managing packages locally with a variety of package managers.
It's designed for when requirements.txt isn't enough, and you have to detect or install dependencies at runtime. It's great for installing and managing MCP servers and their dependencies at runtime.
pip install abx-pkgfrom abx_pkg import Binary, npm
curl = Binary(name="curl").load()
print(curl.abspath, curl.version, curl.exec(cmd=["--version"]))
npm.install("puppeteer")π¦ Provides consistent interfaces for runtime dependency resolution & installation across multiple package managers & OSs β¨ Built with
pydanticv2 for strong static typing guarantees and easy conversion to/from json π Usable withdjango>= 4.0,django-ninja, and OpenAPI +django-jsonformto build UIs & APIs π¦ Driver layer can bepyinfra/ansible/ or built-inabx-pkgengine
Built by ArchiveBox to install & auto-update our extractor dependencies at runtime (chrome, wget, curl, etc.) on macOS/Linux/Docker.
Source Code: https://github.com/ArchiveBox/abx-pkg/
Documentation: https://github.com/ArchiveBox/abx-pkg/blob/main/README.md
from abx_pkg import Binary, apt, brew, pip, npm, env
# Provider singletons are available as simple imports β no manual instantiation needed
dependencies = [
Binary(name='curl', binproviders=[env, apt, brew]),
Binary(name='wget', binproviders=[env, apt, brew]),
Binary(name='yt-dlp', binproviders=[env, pip, apt, brew]),
Binary(name='playwright', binproviders=[env, pip, npm]),
Binary(name='puppeteer', binproviders=[env, npm]),
]
for binary in dependencies:
binary = binary.load_or_install()
print(binary.abspath, binary.version, binary.binprovider, binary.is_valid, binary.sha256)
# Path(...) SemVer(...) EnvProvider()/AptProvider()/BrewProvider()/PipProvider()/NpmProvider() True '<sha256>'
binary.exec(cmd=['--version']) # curl 7.81.0 (x86_64-apple-darwin23.0) libcurl/7.81.0 ...Binary.min_version is optional. Leave it as None when any discovered version is acceptable, or set it to a SemVer/string to enforce a minimum version after load/install.
from abx_pkg import Binary, apt, brew, env
# Use providers directly for package manager operations
apt.install('wget')
print(apt.PATH, apt.get_abspaths('wget'), apt.get_version('wget'))
# our Binary API provides a nice type-checkable, validated, serializable handle
ffmpeg = Binary(name='ffmpeg', binproviders=[env, apt, brew]).load()
print(ffmpeg) # Binary(name='ffmpeg', abspath=Path(...), version=SemVer(...), sha256='...')
print(ffmpeg.abspaths) # show all matching binaries found via each provider PATH
print(ffmpeg.model_dump(mode='json')) # JSON-ready dict
print(ffmpeg.model_json_schema()) # ... OpenAPI-ready JSON schema showing all available fieldsfrom pydantic import InstanceOf
from abx_pkg import Binary, BinProvider, BrewProvider, EnvProvider
# You can also instantiate provider classes manually for custom configuration,
# or define binaries as classes for type checking
class CurlBinary(Binary):
name: str = 'curl'
binproviders: list[InstanceOf[BinProvider]] = [BrewProvider(), EnvProvider()]
curl = CurlBinary().install()
assert isinstance(curl, CurlBinary) # CurlBinary is a unique type you can use in annotations now
print(curl.abspath, curl.version, curl.binprovider, curl.is_valid) # Path(...) SemVer(...) BrewProvider()/EnvProvider() True
curl.exec(cmd=['--version']) # curl 8.4.0 (x86_64-apple-darwin23.0) libcurl/8.4.0 ...So far it supports installing/finding installed/updating/removing packages or binaries on Linux/macOS with:
apt(Ubuntu/Debian/etc.)brew(macOS/Linux)pip(Linux/macOS)npm(Linux/macOS)cargo(Linux/macOS)gem(Linux/macOS)goget(Linux/macOS, viaGoGetProvider)nix(Linux/macOS)docker(Linux/macOS, using local wrapper scripts that rundocker run)env(looks for existing version of binary in user's$PATHat runtime)bash(Linux/macOS, runs explicit shell-command overrides in a managed install root)chromewebstore(Linux/macOS, downloads and unpacks Chrome Web Store extensions)puppeteer(Linux/macOS, installs browser artifacts via@puppeteer/browsers)pyinfra(Linux/macOS, delegates to host package managers throughpyinfra)ansible(Linux/macOS, delegates to host package managers throughansible-runner)
Planned: apk, pkg, and additional future provider backends.
DockerProvider expects image refs as install args, typically via overrides on a Binary. It writes a local wrapper script for the binary and executes it via docker run ...; the binary version is parsed from the image tag, so semver-like tags work best.
NpmProvider prefers a real npm executable when both npm and pnpm are installed. If npm is unavailable, it can still drive installs and metadata lookups through pnpm using the same provider API.
pip install abx-pkgAll built-in providers are available as lazy singletons β just import them by name:
from abx_pkg import apt, brew, pip, npm, env
apt.install('curl')
env.load('wget')These are instantiated on first access and cached for reuse. If you need custom configuration, you can still instantiate provider classes directly:
from pathlib import Path
from abx_pkg import PipProvider
custom_pip = PipProvider(pip_venv=Path("/tmp/abx-pkg-venv"), min_release_age=0)Binary.min_version is enforced after a provider resolves or installs a binary. Provider discovery can still succeed, but the final Binary will be rejected if the loaded version is below your required floor.
from abx_pkg import Binary, SemVer, env, brew
curl = Binary(
name="curl",
min_version=SemVer("8.0.0"),
binproviders=[env, brew],
).load_or_install()Use min_version=None to explicitly disable version floor checks.
Built-in implementations: EnvProvider, AptProvider, BrewProvider, PipProvider, NpmProvider, CargoProvider, GemProvider, GoGetProvider, NixProvider, DockerProvider, PyinfraProvider, AnsibleProvider, BashProvider, ChromeWebstoreProvider, PuppeteerProvider
This type represents a provider of binaries, e.g. a package manager like apt / pip / npm, or env (which only resolves binaries already present in $PATH).
Every provider exposes the same lifecycle surface:
load()/install()/update()/uninstall()/load_or_install()get_install_args()to resolve package names / formulae / image refs / module specsget_abspath()/get_abspaths()/get_version()/get_sha256()
Shared base defaults come from abx_pkg/binprovider.py and apply unless a concrete provider overrides them:
INSTALLER_BIN = "env"
PATH = str(Path(sys.executable).parent)
postinstall_scripts = None # some providers override this with ABX_PKG_POSTINSTALL_SCRIPTS
min_release_age = None # some providers override this with ABX_PKG_MIN_RELEASE_AGE
install_timeout = 120 # or ABX_PKG_INSTALL_TIMEOUT=120
version_timeout = 10 # or ABX_PKG_VERSION_TIMEOUT=10
dry_run = False # or ABX_PKG_DRY_RUN=1 / DRY_RUN=1dry_run: useprovider.get_provider_with_overrides(dry_run=True), passdry_run=Truedirectly toinstall()/update()/uninstall()/load_or_install(), or setABX_PKG_DRY_RUN=1/DRY_RUN=1. If both env vars are set,ABX_PKG_DRY_RUNwins. Provider subprocesses are logged and skipped,install()/update()return a placeholder loaded binary, anduninstall()returnsTruewithout mutating the host.install_timeout: shared provider-level timeout used byinstall(),update(), anduninstall()handler execution paths. Can also be set withABX_PKG_INSTALL_TIMEOUT.version_timeout: shared provider-level timeout used by version / metadata probes such as--version,npm show,npm list,pip show,go version -m, and brew lookups. Can also be set withABX_PKG_VERSION_TIMEOUT.postinstall_scriptsandmin_release_ageare standard provider/binary/action kwargs, but only supporting providers hydrate default values fromABX_PKG_POSTINSTALL_SCRIPTSandABX_PKG_MIN_RELEASE_AGE.- Providers that do not support one of those controls leave the provider default as
None. If you pass an explicit unsupported value duringinstall()/update(), it is logged as a warning and ignored. - Precedence is: explicit action args >
Binary(...)defaults > provider defaults.
Supported override keys are the same everywhere:
from pathlib import Path
from abx_pkg import PipProvider
provider = PipProvider(pip_venv=Path("/tmp/venv")).get_provider_with_overrides(
overrides={
"black": {
"install_args": ["black==24.4.2"],
"version": "self.default_version_handler",
"abspath": "self.default_abspath_handler",
},
},
dry_run=True,
version_timeout=30,
)install_args/packages: package-manager arguments for that provider.packagesis the legacy alias.abspath,version,install,update,uninstall: literal values, callables, or"self.method_name"references that replace the provider handler for a specific binary.
Providers with isolated install locations also expose a shared constructor surface:
install_root: shared alias for provider-specific roots such aspip_venv,npm_prefix,cargo_root,gem_home,gopath,nix_profile,docker_shim_dir.parent, andbrew_prefix.bin_dir: shared alias for providers that separate package state from executable output, such asgem_bindir,gobin, anddocker_shim_dir.provider.install_root/provider.bin_dir: normalized computed properties you can inspect after construction, regardless of which provider-specific args were used.- Legacy provider-specific args still work. The shared aliases are additive, not replacements.
- Providers that do not have an isolated install location reject
install_root/bin_dirat construction time instead of silently ignoring them. - When an explicit install root or bin dir is configured, that provider-specific bin location wins during binary discovery and subprocess execution instead of being left behind ambient host
PATHentries.
π EnvProvider (env)
Source: abx_pkg/binprovider.py β’ Tests: tests/test_envprovider.py
INSTALLER_BIN = "which"
PATH = DEFAULT_ENV_PATH # current PATH + current Python bin dir- Install root: none.
envis read-only and only searches existing binaries on$PATH. - Auto-switching: none.
- Security:
min_release_ageandpostinstall_scriptsare unsupported here and are ignored with a warning if explicitly passed toinstall()/update(). - Overrides:
abspath/versionare the useful ones here.pythonhas a built-in override to the currentsys.executableand interpreter version. - Notes:
install()/update()return explanatory no-op messages, anduninstall()returnsFalse.
π§ AptProvider (apt)
Source: abx_pkg/binprovider_apt.py β’ Tests: tests/test_aptprovider.py
INSTALLER_BIN = "apt-get"
PATH = "" # populated from `dpkg -L bash` bin dirs
euid = 0 # always runs as root- Install root: no hermetic prefix support. Installs into the host package database.
- Auto-switching: tries
PyinfraProviderfirst, thenAnsibleProvider, then falls back to directapt-get. dry_run: shared behavior.- Security:
min_release_ageandpostinstall_scripts=Falseare unsupported and are ignored with a warning if explicitly requested. - Overrides: in the direct shell fallback,
install_argsbecomesapt-get install -y -qq --no-install-recommends ...;update()usesapt-get install --only-upgrade .... - Notes: direct mode runs
apt-get update -qqat most once per day and requests privilege escalation when needed.
πΊ BrewProvider (brew)
Source: abx_pkg/binprovider_brew.py β’ Tests: tests/test_brewprovider.py
INSTALLER_BIN = "brew"
PATH = "/home/linuxbrew/.linuxbrew/bin:/opt/homebrew/bin:/usr/local/bin"
brew_prefix = guessed host prefix # /opt/homebrew, /usr/local, or linuxbrew- Install root:
brew_prefixis for discovery only.install_root=...aliases tobrew_prefix. This provider does not create an isolated custom Homebrew prefix. - Auto-switching: if
postinstall_scripts=True, it prefersPyinfraProviderand thenAnsibleProvider; otherwise it falls back to directbrew. dry_run: shared behavior.- Security:
min_release_ageis unsupported and is ignored with a warning if explicitly requested.postinstall_scripts=Falseis supported for directbrewinstalls via--skip-post-install, andABX_PKG_POSTINSTALL_SCRIPTShydrates the provider default here. - Overrides: in the direct shell fallback,
install_argsmaps to formula / cask args passed tobrew install,brew upgrade, andbrew uninstall. - Notes: direct mode runs
brew updateat most once per day. Explicit--skip-post-installargs ininstall_argswin over derived defaults.
π PipProvider (pip)
Source: abx_pkg/binprovider_pip.py β’ Tests: tests/test_pipprovider.py, tests/test_security_controls.py
INSTALLER_BIN = "pip"
PATH = "" # auto-built from global/user Python bin dirs
pip_venv = None # set this for hermetic installs
cache_dir = user_cache_path("pip", "abx-pkg") or <system temp>/pip-cache
pip_install_args = ["--no-input", "--disable-pip-version-check", "--quiet"]
pip_bootstrap_packages = ["pip", "setuptools", "uv"]- Install root:
pip_venv=Noneuses the system/user Python environment. Setpip_venv=Path(...)orinstall_root=Path(...)for a hermetic venv rooted at<pip_venv>/bin, and that venv bin dir becomes the provider's active executable search path. - Auto-switching: the provider executable is still
pip, but install / update / show / uninstall calls useuv pip ...whenuvis available andPIP_BINARYis not forcing a specific pip path. dry_run: shared behavior.- Security: supports both
min_release_ageandpostinstall_scripts=False, and hydrates their provider defaults fromABX_PKG_MIN_RELEASE_AGEandABX_PKG_POSTINSTALL_SCRIPTS. - Overrides:
install_argsis passed as pip requirement specs; unpinned specs get a>=min_versionfloor whenmin_versionis supplied. - Notes:
ABX_PKG_POSTINSTALL_SCRIPTSandABX_PKG_MIN_RELEASE_AGEapply here by default.postinstall_scripts=Falseusesuv pip --no-buildor plainpip --only-binary :all:.min_release_ageis enforced withuv --exclude-newer=<cutoff>or plainpip --uploaded-prior-to=<cutoff>when the host pip is new enough. Explicit conflicting flags already present ininstall_argswin over the derived defaults.
π¦ NpmProvider (npm)
Source: abx_pkg/binprovider_npm.py β’ Tests: tests/test_npmprovider.py, tests/test_security_controls.py
INSTALLER_BIN = "npm"
PATH = "" # auto-built from npm/pnpm local + global bin dirs
npm_prefix = None # None = global install, Path(...) = hermetic-ish prefix
cache_dir = user_cache_path("npm", "abx-pkg") or <system temp>/npm-cache
npm_install_args = ["--force", "--no-audit", "--no-fund", "--loglevel=error"]- Install root:
npm_prefix=Noneinstalls globally. Setnpm_prefix=Path(...)orinstall_root=Path(...)to install under<prefix>/node_modules/.bin; that prefix bin dir becomes the provider's active executable search path. - Auto-switching: prefers a real
npmbinary, falls back topnpmifnpmis unavailable, and honorsNPM_BINARY=/abs/path/to/npm-or-pnpm. dry_run: shared behavior.- Security: supports both
min_release_ageandpostinstall_scripts=False, and hydrates their provider defaults fromABX_PKG_MIN_RELEASE_AGEandABX_PKG_POSTINSTALL_SCRIPTS. - Overrides:
install_argsis passed as npm package specs; unpinned specs get rewritten topkg@>=<min_version>whenmin_versionis supplied. - Notes:
ABX_PKG_POSTINSTALL_SCRIPTSandABX_PKG_MIN_RELEASE_AGEapply here by default. Direct npm mode uses--ignore-scriptsand--min-release-age=<days>when the host npm supports it. pnpm mode writespnpm-workspace.yamlwithminimumReleaseAge; that is how release-age enforcement is configured there. Explicit conflicting flags already present ininstall_argswin over the derived defaults.
π§ͺ BashProvider (bash)
Source: abx_pkg/binprovider_bash.py β’ Tests: tests/test_bashprovider.py
INSTALLER_BIN = "sh"
PATH = ""
bash_root = $ABX_PKG_BASH_ROOT or ~/.cache/abx-pkg/bash
bash_bin_dir = <bash_root>/bin- Install root: set
bash_root/install_rootfor the managed state dir, andbash_bin_dir/bin_dirfor the executable output dir. - Auto-switching: none.
dry_run: shared behavior.- Security:
min_release_ageandpostinstall_scripts=Falseare unsupported and are ignored with a warning if explicitly requested. - Overrides: this provider is driven by literal per-binary shell overrides for
install,update, anduninstall. - Notes: the provider exports
INSTALL_ROOT,BIN_DIR,BASH_INSTALL_ROOT, andBASH_BIN_DIRinto the shell environment for those commands.
π¦ CargoProvider (cargo)
Source: abx_pkg/binprovider_cargo.py β’ Tests: tests/test_cargoprovider.py
INSTALLER_BIN = "cargo"
PATH = "" # prepends cargo_root/bin and cargo_home/bin
cargo_root = None # set this for hermetic installs
cargo_home = $CARGO_HOME or ~/.cargo
cargo_install_args = ["--locked"]- Install root: set
cargo_root=Path(...)orinstall_root=Path(...)for isolated installs under<cargo_root>/bin; otherwise installs go throughcargo_home. - Auto-switching: none.
dry_run: shared behavior.- Security:
min_release_ageandpostinstall_scripts=Falseare unsupported and are ignored with a warning if explicitly requested. - Overrides:
install_argsis passed tocargo install;min_versionbecomescargo install --version >=.... - Notes: the provider also sets
CARGO_HOME,CARGO_TARGET_DIR, andCARGO_INSTALL_ROOTwhen applicable.
π GemProvider (gem)
Source: abx_pkg/binprovider_gem.py β’ Tests: tests/test_gemprovider.py
INSTALLER_BIN = "gem"
PATH = DEFAULT_ENV_PATH
gem_home = None # defaults to $GEM_HOME or ~/.local/share/gem
gem_bindir = None # defaults to <gem_home>/bin
gem_install_args = ["--no-document"]- Install root: set
gem_homeorinstall_root, and optionallygem_bindirorbin_dir, for hermetic installs; otherwise it uses$GEM_HOMEor~/.local/share/gem. - Auto-switching: none.
dry_run: shared behavior.- Security:
min_release_ageandpostinstall_scripts=Falseare unsupported and are ignored with a warning if explicitly requested. - Overrides:
install_argsmaps togem install ...,gem update ..., andgem uninstall ...;min_versionbecomes--version >=.... - Notes: generated wrapper scripts are patched so they activate the configured
GEM_HOMEinstead of the host default.
πΉ GoGetProvider (goget)
Source: abx_pkg/binprovider_goget.py β’ Tests: tests/test_gogetprovider.py
INSTALLER_BIN = "go"
PATH = DEFAULT_ENV_PATH
gobin = None # defaults to <gopath>/bin
gopath = $GOPATH or ~/go
go_install_args = []- Install root: set
gopathorinstall_rootfor the Go workspace, andgobinorbin_dirfor the executable dir; otherwise installs land in<gopath>/bin. - Auto-switching: none.
dry_run: shared behavior.- Security:
min_release_ageandpostinstall_scripts=Falseare unsupported and are ignored with a warning if explicitly requested. - Overrides:
install_argsis passed togo install ...; the default is["<bin_name>@latest"]. - Notes:
update()is justinstall()again. Version detection prefersgo version -m <binary>and falls back to the generic version probe. The provider name isgoget, notgo_get.
βοΈ NixProvider (nix)
Source: abx_pkg/binprovider_nix.py β’ Tests: tests/test_nixprovider.py
INSTALLER_BIN = "nix"
PATH = "" # prepends <nix_profile>/bin
nix_profile = $ABX_PKG_NIX_PROFILE or ~/.nix-profile
nix_state_dir = None # optional XDG state/cache isolation
nix_install_args = [
"--extra-experimental-features", "nix-command",
"--extra-experimental-features", "flakes",
]- Install root: set
nix_profile=Path(...)orinstall_root=Path(...)for a custom profile; addnix_state_dir=Path(...)to isolate state/cache paths too. - Auto-switching: none.
dry_run: shared behavior.- Security:
min_release_ageandpostinstall_scripts=Falseare unsupported and are ignored with a warning if explicitly requested. - Overrides:
install_argsis passed tonix profile install ...; default is["nixpkgs#<bin_name>"]. - Notes: update/uninstall operate on the resolved profile element name rather than reusing the full flake ref.
π³ DockerProvider (docker)
Source: abx_pkg/binprovider_docker.py β’ Tests: tests/test_dockerprovider.py
INSTALLER_BIN = "docker"
PATH = "" # prepends docker_shim_dir
docker_shim_dir = ($ABX_PKG_DOCKER_ROOT or ~/.cache/abx-pkg/docker) / "bin"
docker_run_args = ["--rm", "-i"]- Install root: partial only. Images are pulled into Docker's host-managed image store; the provider only controls the local shim dir and metadata dir. Use
install_root=Path(...)for the shim/metadata root orbin_dir=Path(...)for the shim dir directly. - Auto-switching: none.
dry_run: shared behavior.- Security:
min_release_ageandpostinstall_scripts=Falseare unsupported and are ignored with a warning if explicitly requested. - Overrides:
install_argsis a list of Docker image refs. The first item is treated as the main image and becomes the generated shim target. - Notes: default install args are
["<bin_name>:latest"].install()/update()rundocker pull, write metadata JSON, and create an executable wrapper that runsdocker run ....
π§© ChromeWebstoreProvider (chromewebstore)
Source: abx_pkg/binprovider_chromewebstore.py β’ Tests: tests/test_chromewebstoreprovider.py
INSTALLER_BIN = "node"
PATH = ""
extensions_root = $ABX_PKG_CHROMEWEBSTORE_ROOT or ~/.cache/abx-pkg/chromewebstore
extensions_dir = <extensions_root>/extensions- Install root: set
extensions_root/install_rootfor the managed extension cache root, andextensions_dir/bin_dirfor the unpacked extension output dir. - Auto-switching: none.
dry_run: shared behavior.- Security:
min_release_ageis unsupported and is ignored with a warning if explicitly requested.postinstall_scripts=Falseis supported as a standard kwarg andABX_PKG_POSTINSTALL_SCRIPTShydrates the provider default here, but there is no extra install-time toggle beyond the packaged JS runtime path this provider already uses. - Overrides:
install_argsare[webstore_id, "--name=<extension_name>"]. - Notes: the packaged JS runtime under
abx_pkg/js/chrome/is used to download, unpack, and cache the extension, and the resolved binary path is the unpackedmanifest.json.
π PuppeteerProvider (puppeteer)
Source: abx_pkg/binprovider_puppeteer.py β’ Tests: tests/test_puppeteerprovider.py
INSTALLER_BIN = "puppeteer-browsers"
PATH = ""
puppeteer_root = $ABX_PKG_PUPPETEER_ROOT or ~/.cache/abx-pkg/puppeteer
browser_bin_dir = <puppeteer_root>/bin
browser_cache_dir = <puppeteer_root>/cache- Install root: set
puppeteer_root/install_rootfor the managed root,browser_bin_dir/bin_dirfor symlinked executables, andbrowser_cache_dirfor downloaded browser artifacts. - Auto-switching: bootstraps
@puppeteer/browsersthroughNpmProviderand then uses that CLI for browser installs. dry_run: shared behavior.- Security:
min_release_ageis unsupported for browser installs and is ignored with a warning if explicitly requested.postinstall_scripts=Falseis supported for the underlying npm bootstrap path, andABX_PKG_POSTINSTALL_SCRIPTShydrates the provider default here. - Overrides:
install_argsare passed through to@puppeteer/browsers install ..., with the provider appending its managed--path=<cache_dir>. - Notes: installed-browser resolution uses semantic version ordering, not lexicographic string sorting.
π οΈ PyinfraProvider (pyinfra)
Source: abx_pkg/binprovider_pyinfra.py β’ Tests: tests/test_pyinfraprovider.py
INSTALLER_BIN = "pyinfra"
PATH = os.environ.get("PATH", DEFAULT_PATH)
pyinfra_installer_module = "auto"
pyinfra_installer_kwargs = {}- Install root: no hermetic prefix support. It delegates to host package managers through pyinfra operations.
- Auto-switching:
installer_module="auto"resolves tooperations.brew.packageson macOS andoperations.server.packageson Linux. dry_run: shared behavior.- Security:
min_release_ageandpostinstall_scripts=Falseare unsupported and are ignored with a warning if explicitly requested. - Overrides:
install_argsis the package list passed to the selected pyinfra operation. - Notes: privilege requirements depend on the underlying package manager and selected module. When pyinfra tries a privileged sudo path and then falls back, both error outputs are preserved if the final attempt also fails.
π AnsibleProvider (ansible)
Source: abx_pkg/binprovider_ansible.py β’ Tests: tests/test_ansibleprovider.py
INSTALLER_BIN = "ansible"
PATH = os.environ.get("PATH", DEFAULT_PATH)
ansible_installer_module = "auto"
ansible_playbook_template = ANSIBLE_INSTALL_PLAYBOOK_TEMPLATE- Install root: no hermetic prefix support. It delegates to the host via
ansible-runner. - Auto-switching:
installer_module="auto"resolves tocommunity.general.homebrewon macOS andansible.builtin.packageon Linux. dry_run: shared behavior.- Security:
min_release_ageandpostinstall_scripts=Falseare unsupported and are ignored with a warning if explicitly requested. - Overrides:
install_argsbecomes the playbook loop input for the chosen Ansible module. - Notes: when using the Homebrew module, the provider auto-injects the detected brew search path into module kwargs. Privilege requirements still come from the underlying package manager, and failed sudo attempts are included in the final error if the fallback attempt also fails.
This type represents a single binary dependency aka a package (e.g. wget, curl, ffmpeg, etc.).
It can define one or more BinProviders that it supports, along with overrides to customize the behavior for each.
Binarys implement the following interface:
load(),install(),update(),uninstall(),load_or_install()->Binarybinprovidersbinprovider/loaded_binproviderabspath/loaded_abspathabspaths/loaded_abspathsversion/loaded_versionsha256/loaded_sha256
Binary.install() and Binary.update() return a fresh loaded Binary.
Binary.uninstall() returns a Binary with binprovider, abspath, version, and sha256 cleared after removal.
Binary.load(), Binary.install(), Binary.load_or_install(), and Binary.update() all enforce min_version consistently.
from pydantic import InstanceOf
from abx_pkg import BinProvider, Binary, BinProviderName, BinName, HandlerDict, SemVer, BrewProvider
from abx_pkg import env, pip, apt
class CustomBrewProvider(BrewProvider):
name: BinProviderName = 'custom_brew'
def get_macos_packages(self, bin_name: str, **context) -> list[str]:
return ['yt-dlp'] if bin_name == 'ytdlp' else [bin_name]
# Example: Create a reusable class defining a binary and its providers
class YtdlpBinary(Binary):
name: BinName = 'ytdlp'
description: str = 'YT-DLP (Replacement for YouTube-DL) Media Downloader'
# define the providers this binary supports
binproviders: list[InstanceOf[BinProvider]] = [env, pip, apt, CustomBrewProvider()]
# customize installed package names for specific package managers
overrides: dict[BinProviderName, HandlerDict] = {
'pip': {'install_args': ['yt-dlp[default,curl-cffi]']}, # can use literal values (install_args -> list[str], version -> SemVer, abspath -> Path, install -> str log)
'apt': {'install_args': lambda: ['yt-dlp', 'ffmpeg']}, # also accepts any pure Callable that returns a list of packages
'custom_brew': {'install_args': 'self.get_macos_packages'}, # also accepts string reference to function on self (where self is the BinProvider)
}
ytdlp = YtdlpBinary().load_or_install()
print(ytdlp.binprovider) # EnvProvider(...) / PipProvider(...) / AptProvider(...) / CustomBrewProvider(...)
print(ytdlp.abspath) # Path(...)
print(ytdlp.abspaths) # {'env': [Path(...)], 'custom_brew': [Path(...)]}
print(ytdlp.version) # SemVer(...)
print(ytdlp.sha256) # '<sha256>'
print(ytdlp.is_valid) # True
# Lifecycle actions preserve the Binary type and refresh/clear loaded metadata as needed
ytdlp = ytdlp.update()
assert ytdlp.is_valid
ytdlp = ytdlp.uninstall()
assert ytdlp.abspath is None and ytdlp.version is Noneimport os
import platform
from pydantic import InstanceOf
from abx_pkg import BinProvider, Binary, BinProviderName, BinName, HandlerDict, SemVer
from abx_pkg import env, apt
# Example: Create a binary that uses Podman if available, or Docker otherwise
class DockerBinary(Binary):
name: BinName = 'docker'
# define the providers this binary supports
binproviders: list[InstanceOf[BinProvider]] = [env, apt]
overrides: dict[BinProviderName, HandlerDict] = {
'env': {
# example: prefer podman if installed (falling back to docker)
'abspath': lambda: os.which('podman') or os.which('docker') or os.which('docker-ce'),
},
'apt': {
# example: vary installed package name based on your CPU architecture
'install_args': {
'amd64': ['docker'],
'armv7l': ['docker-ce'],
'arm64': ['docker-ce'],
}.get(platform.machine(), 'docker'),
},
}
docker = DockerBinary().load_or_install()
print(docker.binprovider) # EnvProvider(...) / AptProvider(...)
print(docker.abspath) # Path(...)
print(docker.abspaths) # {'env': [Path(...)], ...}
print(docker.version) # SemVer(...)
print(docker.is_valid) # True
# You can also seed loaded field values at construction time,
# e.g. if you want to point at a specific existing binary path:
custom_docker = DockerBinary(abspath='~/custom/bin/podman').load()
print(custom_docker.name) # 'docker'
print(custom_docker.binprovider) # EnvProvider(...) / AptProvider(...)
print(custom_docker.abspath) # Path(...)
print(custom_docker.version) # SemVer(...)
print(custom_docker.is_valid) # Truefrom abx_pkg import SemVer
### Example: Use the SemVer type directly for parsing & verifying version strings
SemVer.parse('Google Chrome 124.0.6367.208+beta_234. 234.234.123') # SemVer(124, 0, 6367)
SemVer.parse('2024.04.05') # SemVer(2024, 4, 5)
SemVer.parse('1.9+beta') # SemVer(1, 9, 0)
str(SemVer(1, 9, 0)) # '1.9.0'These types are all meant to be used library-style to make writing your own apps easier.
e.g. you can use it to build things likeplaywright install --with-deps.
abx-pkg uses uv for local development, dependency sync, linting, and tests.
# create/update the local env with dev deps
uv sync --all-extras --all-groups
# run formatting/lint/type checks
uv run prek run --all-files
# run the full test suite from tests/
uv run pytest -sx tests/
# build distributions
uv build- Tests now live under
tests/. - Use
uv run pytest -sx tests/test_npmprovider.pyor a specific node likeuv run pytest -sx tests/test_npmprovider.py::TestNpmProvider::test_provider_dry_run_does_not_install_zxwhen iterating on one provider.
With a few more packages, you get type-checked Django fields & forms that support BinProvider and Binary.
Tip
For the full Django experience, we recommend installing these 3 excellent packages:
django-admin-data-viewsdjango-pydantic-fielddjango-jsonform
pip install abx-pkg django-admin-data-views django-pydantic-field django-jsonform
pip install django-pydantic-fieldFor more info see the django-pydantic-field docs...
Example Django models.py showing how to store Binary and BinProvider instances in DB fields:
from django.db import models
from abx_pkg import BinProvider, Binary, SemVer
from django_pydantic_field import SchemaField
class Dependency(models.Model):
label = models.CharField(max_length=63)
default_binprovider: BinProvider = SchemaField()
binaries: list[Binary] = SchemaField(default=[])
min_version: SemVer = SchemaField(default=(0, 0, 1))And here's how to save a Binary using the example model:
from abx_pkg import Binary, SemVer, env
# find existing curl Binary in $PATH
curl = Binary(name='curl').load()
# save it to the DB using our new model
obj = Dependency(
label='runtime tools',
default_binprovider=env, # store BinProvider values directly
binaries=[curl], # store Binary/SemVer values directly
min_version=SemVer('6.5.0'),
)
obj.save()When fetching it back from the DB, the Binary field is auto-deserialized / immediately usable:
obj = Dependency.objects.get(label='runtime tools') # everything is transparently serialized to/from the DB,
# and is ready to go immediately after querying:
assert obj.binaries[0].abspath == curl.abspath
print(obj.binaries[0].abspath) # Path('/usr/local/bin/curl')
obj.binaries[0].exec(cmd=['--version']) # curl 7.81.0 (x86_64-apple-darwin23.0) libcurl/7.81.0 ...
For a full example see our provided django_example_project/...
pip install abx-pkg django-admin-data-viewsFor more info see the django-admin-data-views docs...
Then add this to your settings.py:
INSTALLED_APPS = [
# ...
'admin_data_views',
'abx_pkg',
# ...
]
# point these to a function that gets the list of all binaries / a single binary
ABX_PKG_GET_ALL_BINARIES = 'project.views.get_all_binaries'
ABX_PKG_GET_BINARY = 'project.views.get_binary'
ADMIN_DATA_VIEWS = {
"NAME": "Environment",
"URLS": [
{
"route": "binaries/",
"view": "abx_pkg.views.binaries_list_view",
"name": "binaries",
"items": {
"route": "<str:key>/",
"view": "abx_pkg.views.binary_detail_view",
"name": "binary",
},
},
# Coming soon: binprovider_list_view + binprovider_detail_view ...
],
}For a full example see our provided django_example_project/...
Note: If you override the default site admin, you must register the views manually...
admin.py:
class YourSiteAdmin(admin.AdminSite):
"""Your customized version of admin.AdminSite"""
...
custom_admin = YourSiteAdmin()
custom_admin.register(get_user_model())
...
from abx_pkg.admin import register_admin_views
register_admin_views(custom_admin)
Expand to see more...
[!IMPORTANT] This feature is coming soon but is blocked on a few issues being fixed first:
Install django-jsonform to get auto-generated Forms for editing BinProvider, Binary, etc. data
pip install django-pydantic-field django-jsonformFor more info see the django-jsonform docs...
admin.py:
from django.contrib import admin
from django_jsonform.widgets import JSONFormWidget
from django_pydantic_field.v2.fields import PydanticSchemaField
class MyModelAdmin(admin.ModelAdmin):
formfield_overrides = {PydanticSchemaField: {"widget": JSONFormWidget}}
admin.site.register(MyModel, MyModelAdmin)For a full example see our provided django_example_project/...
abx-pkg uses the standard Python logging module. By default it stays quiet unless your application configures logging explicitly.
import logging
from abx_pkg import Binary, env, configure_logging
configure_logging(logging.INFO)
python = Binary(name='python', binproviders=[env]).load()To enable Rich logging:
pip install "abx-pkg[rich]"import logging
from abx_pkg import Binary, EnvProvider, configure_rich_logging
configure_rich_logging(logging.DEBUG)
python = Binary(name='python', binproviders=[EnvProvider()]).load()Debug logging is hardened so logging itself does not become the failure. If a provider/model object has a broken or overly-expensive repr(), abx-pkg falls back to a short ClassName(...) summary instead of raising while formatting log output.
configure_rich_logging(...) uses rich.logging.RichHandler under the hood, so log levels, paths, arguments, and command lines render with terminal colors when supported.
You can also manage it with standard logging primitives:
import logging
logging.basicConfig(level=logging.INFO)
logging.getLogger("abx_pkg").setLevel(logging.DEBUG)from pathlib import Path
from abx_pkg import (
BinProvider,
BinProviderName,
BinName,
HostBinPath,
InstallArgs,
SemVer,
bin_abspath,
bin_version,
)
class CargoProvider(BinProvider):
name: BinProviderName = 'cargo'
INSTALLER_BIN: BinName = 'cargo'
PATH = str(Path.home() / '.cargo/bin')
def default_install_args_handler(self, bin_name: BinName, **context) -> InstallArgs:
return [bin_name]
def default_install_handler(
self,
bin_name: BinName,
install_args: InstallArgs | None = None,
postinstall_scripts: bool | None = None,
min_release_age: float | None = None,
min_version: SemVer | None = None,
timeout: int | None = None,
) -> str:
install_args = install_args or self.get_install_args(bin_name)
installer = self.INSTALLER_BIN_ABSPATH
assert installer
proc = self.exec(installer, cmd=['install', *install_args], timeout=timeout)
assert proc.returncode == 0
return proc.stdout.strip() or proc.stderr.strip()
def default_abspath_handler(self, bin_name: BinName, **context) -> HostBinPath | None:
return bin_abspath(bin_name, PATH=self.PATH)
def default_version_handler(
self,
bin_name: BinName,
abspath: HostBinPath | None = None,
timeout: int | None = None,
**context,
) -> SemVer | None:
return self._version_from_exec(bin_name, abspath=abspath, timeout=timeout)
cargo = CargoProvider()
rg = cargo.install(bin_name='ripgrep')
print(rg.binprovider) # CargoProvider(...)
print(rg.version) # SemVer(...)Note: this package used to be called pydantic-pkgr, it was renamed to abx-pkg on 2024-11-12.
- Implement initial basic support for
apt,brew, andpip - Provide editability and actions via Django Admin UI using
django-pydantic-fieldanddjango-jsonform - Add
preinstallandpostinstallhooks for things like addingaptsources and running cleanup scripts - Implement more package managers (
apk,ppm,pkg, etc.)
- https://github.com/MrThearMan/django-signal-webhooks
- https://github.com/MrThearMan/django-admin-data-views
- https://github.com/lazybird/django-solo
- https://github.com/joshourisman/django-pydantic-settings
- https://github.com/surenkov/django-pydantic-field
- https://github.com/jordaneremieff/djantic

