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)
# No ssh_public_key given → the box defaults to access_type="none":
# everything below goes through the exec/files/proxy API. Pass
# ssh_public_key=... (+ access_type="ssh-public") if you want a shell too.
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() / .delete() |
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 constructs the client with
Hogland.from_token_file("/var/run/secrets/hogland.posthog.dev/token")(ortoken_provider=): the SDK re-reads the file on every request, so the ~50 min K8s rotation never presents a stale credential. - 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 sdk/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.
Metadata
Release files for posthog-hogland 0.4.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| posthog_hogland-0.4.1.tar.gz | 51.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| posthog_hogland-0.4.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 89.4 kB
Release files / posthog_hogland-0.4.1.tar.gz
| Download URL | posthog_hogland-0.4.1.tar.gz |
|---|---|
| Size | 51.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9f89e6768ba28eb29ffa8868797f8229d196caa9e3e6e56e85df598981325462
|
|
BLAKE2b-256 checksum How to use checksums |
b1cac67965f512f9d4e2afc805bd878650f8cb5a5b91eb530f1ee9004fb5fb4b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.8 {"installer":{"name":"uv","version":"0.12.8","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}
|
Release files / posthog_hogland-0.4.1-py3-none-any.whl
| Download URL | posthog_hogland-0.4.1-py3-none-any.whl |
|---|---|
| Size | 38.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
2bc2fd6bf4a710df8eaf4e5bfa1a4ffd8d29751eb13edc650b89bdcba045eaf2
|
|
BLAKE2b-256 checksum How to use checksums |
352a527ef6d0c3eee98b676bf5c9878c3010e749601210eb1b32dd99548b51b4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.8 {"installer":{"name":"uv","version":"0.12.8","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}
|