Python client for hogland — Firecracker-backed sandboxes (hogboxes)
Project description
hogland (Python)
Python client for hogland's Firecracker-backed sandboxes (hogboxes).
The SDK exposes the user-facing slice of the hogplane API
(/v1/hogboxes/*, /v1/limits, /v1/me, plus snapshots and devboxes)
as a small, typed surface designed to be a viable backend for PostHog's
internal SandboxBase interface — see examples/sandbox_base_adapter.py.
Install
uv pip install posthog-hogland
# or
pip install posthog-hogland
The PyPI distribution is posthog-hogland; the importable package is
hogland (same split as pip install pillow / from PIL import ...).
Quickstart
from hogland import Hogland
client = Hogland() # reads HOG_TOKEN + HOG_HOST from env (same vars as the CLI)
with client.create(cpus=4, memory_mib=8192, disk_gib=50) as box:
box.write_file("/work/run.py", b"print('hi')\n", mkdir=True)
result = box.exec(["python", "/work/run.py"], timeout_seconds=30)
assert result.exit_code == 0
print(result.stdout)
# Streaming exec — yields ExecEvent frames as they arrive.
for event in box.exec_stream(["bash", "-c", "for i in 1 2 3; do echo $i; sleep 1; done"]):
if event.kind == "stdout":
print(event.data, end="")
elif event.kind == "exit":
print(f"exit: {event.exit_code}")
# Snapshot for next session. Returns a record whose .id can be
# passed back as snapshot_id on the next create.
snap = box.snapshot()
Async is symmetric:
import asyncio
from hogland import AsyncHogland
async def main() -> None:
async with AsyncHogland() as client:
async with await client.create(cpus=4, memory_mib=8192) as box:
result = await box.exec(["uname", "-a"])
print(result.stdout)
asyncio.run(main())
API at a glance
| Method | Returns | Notes |
|---|---|---|
Hogland.create(...) / .get(id) / .list() / .iter_boxes() |
Hogbox / HogboxList |
Create, fetch, paginate user-owned boxes |
Hogland.me() / .limits() |
Me / Limits |
Caller identity, server-advertised valid ranges |
Hogbox.exec(argv, timeout_seconds, env, workdir) |
ExecResult |
Batch exec, ≤1 MiB stdout/stderr per stream |
Hogbox.exec_stream(argv, ...) |
Iterator[ExecEvent] |
SSE streaming exec, no output cap |
Hogbox.write_file(path, bytes, mode, mkdir) / .read_file(path) |
FileWriteResponse / bytes |
Atomic file IO; ≤64 MiB |
Hogbox.snapshot() |
SnapshotRecord |
Pause → sync → resume; returns id usable as next snapshot_id |
Hogbox.pause() / .resume() / .destroy() |
Hogbox / None |
Lifecycle |
Hogbox.proxy_url(port, path) |
str |
Authenticated proxy URL into the box at port |
Auth: pass an API credential via HOG_TOKEN env or token=... ctor
arg. The SDK transports it as Authorization: Bearer <credential> and
is agnostic to credential type — hogplane resolves it through one of
several paths (see Auth below). The proxy_url is hit with the
same credential; there's no per-tunnel token system (see
docs/PYTHON_SDK_CODEGEN_RESEARCH.md §1d).
Errors are typed: AuthenticationError, PermissionDeniedError,
NotFoundError, ConflictError, ValidationError, RateLimitError,
ServerError. All derive from APIError (which derives from
HoglandError). The original problem-details body is on
.body; the HTTP status is on .status_code.
Configuration
export HOG_TOKEN=... # API credential (see Auth)
export HOG_HOST=https://hogland.posthog.dev # API base URL
Config is resolved from constructor args (token=, base_url=) first,
then from these env vars. There is no on-disk config-file fallback today;
matching the CLI's ~/.config/hogland/config.json (see
pkg/cliconfig/cliconfig.go) is on the roadmap.
Auth
Hogplane authenticates each request to one of these paths — see
docs/AUTH_PLAN.md for the full model. The SDK transports any of them
identically; the credential string you put in HOG_TOKEN / token= is
what selects the path:
| Path | Credential | Who uses it |
|---|---|---|
| APIToken | Long-lived token minted at signup (tok_...) |
Humans, CLI users, scripts |
| K8s SA (EKS OIDC) | Projected K8s ServiceAccount JWT with hogland audience | In-cluster automation (e.g. PostHog Django → hogland) |
| GitHub OIDC | GitHub Actions workflow JWT | CI workflows in PostHog org |
| Tailscale | Identity injected by Tailscale Operator ingress | Tailnet-resident humans |
For in-cluster automation (the primary SDK consumer), the recommended shape is:
- Pod spec mounts a projected SA token volume with hogland's audience
(default:
hogland) at e.g./var/run/secrets/hogland.posthog.dev/token. Don't reuse/var/run/secrets/kubernetes.io/serviceaccount/token— its audience is the cluster API server, which won't match hogland. - The integration reads that file at request time (not at process boot —
K8s rotates the file every ~50 min) and passes the contents as
token=. - A
k8s_saTrustMapping on hogland side, keyed on{issuer, namespace, sa_name}, resolves the JWT to a Principal.
See examples/sandbox_base_adapter.py for a reference implementation
and INTEGRATION_NOTES.md for the operational checklist.
Multi-deploy / regional setups
The SDK is region-agnostic by construction — one Hogland instance
is one base URL plus one credential. If hogland ever runs as separate
regional deploys (e.g. hogland-eu.posthog.dev and
hogland-us.posthog.dev), the consumer holds one client per region:
clients = {
"eu": Hogland(base_url="https://hogland-eu.posthog.dev", token=eu_token),
"us": Hogland(base_url="https://hogland-us.posthog.dev", token=us_token),
}
box = clients[region].create(cpus=4, memory_mib=8192)
This is the same shape Modal users write today (their _get_modal_region(deployment)
helper picks the right Modal cloud); the only difference is that with hogland
the regional decision is which base_url, not which kwarg. If the deploys
share a Principal (federated TrustMapping matched on the same K8s SA), the
token is the same across regions; if not, the consumer rotates per-region
credentials at construction time.
The vendored examples/sandbox_base_adapter.py factors this through a
small _hogland_client(region=...) factory — extend that one function in
your repo when regional routing becomes a real requirement.
How the SDK is built
The Pydantic v2 models in src/hogland/_generated/models.py are
codegen'd from internal/clitest/testdata/openapi.yaml by
scripts/gen_models.py (which filters to the user-facing slice and
runs datamodel-code-generator underneath). CI runs the regen and
fails the build if the result differs from the committed file. The
hand-written facade (_client.py, _async.py, _box.py, _http.py,
_sse.py) sits on top and is the public surface.
The choice of "models-only codegen + hand-written facade" is the
modern-AI-SDK shape (OpenAI, Anthropic). See
docs/PYTHON_SDK_CODEGEN_RESEARCH.md for the analysis and tradeoffs.
Development
# From the repo root.
cd python
uv venv --python 3.12
uv pip install -e '.[dev]'
# Regenerate models from openapi.yaml.
task py:gen-models
# Run tests, lint, type-check.
uv run pytest -q
uv run ruff check .
uv run ruff format --check .
uv run ty check src tests
Release
See RELEASING.md for the operational flow (bump,
tag, push) and the one-time PyPI Trusted Publishing setup.
Short version: bump __version__ in src/hogland/_version.py — the
single source of truth — then tag vX.Y.Z-py on main. CI verifies
the tag matches _version.py, builds, smoke-tests the wheel, attests,
publishes via OIDC, and creates a GitHub Release. The -py suffix
keeps this release track separate from the CLI's v*-cli tags.
Changes per version are tracked in CHANGELOG.md.
License
MIT — see LICENSE.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
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 posthog_hogland-0.1.0.tar.gz.
File metadata
- Download URL: posthog_hogland-0.1.0.tar.gz
- Upload date:
- Size: 39.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.16 {"installer":{"name":"uv","version":"0.11.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
86ec1dfc99ea7b9652283e14b38552bd2256a764bd21b9fddf854d808463ec0b
|
|
| MD5 |
310f72892152f7dd1c1bea165b09d879
|
|
| BLAKE2b-256 |
d3043cc2cc93105bd737f5a20c026fcb185854a3f3676ca0c1471c9a3bf69b52
|
File details
Details for the file posthog_hogland-0.1.0-py3-none-any.whl.
File metadata
- Download URL: posthog_hogland-0.1.0-py3-none-any.whl
- Upload date:
- Size: 29.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.16 {"installer":{"name":"uv","version":"0.11.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
093bb298dfdf8422cddf9ec77d8257c5728be589cc5caf9e7b1437aade3a8089
|
|
| MD5 |
bad00dfa757fe6bca787ebc7f8d9a40f
|
|
| BLAKE2b-256 |
a70df71912ee0f53c74db9dcb513fa1b02b263a8e8e88076ca590d7d380525d3
|