Status: DRAFT. Living design document. Sign-off triggers v0.1 scaffolding. Inspiration: LiteLLM (1 API, N providers) adapted for stateful bare-metal compute.
edge-python-sdk is a public PyPI package providing a single Python client for
multiple Latitude-shaped compute providers. It wraps the upstream
latitudesh-python-sdk (a Speakeasy-generated SDK by latitudeshdev), removes
client-side strict-enum validation for catalog fields, and selects a backend
provider at construction time.
- Two providers:
latitude(direct,https://api.latitude.sh) anddigital-frontier(Latitude-shaped adapter at any user-supplied URL). - Permissive enums for: site (3 enum classes), plan (1), operating system (3).
- A small
catalogaccessor that returns the provider's known site/plan/OS values. - Conformance test suite both providers pass.
- Public PyPI release with semver, OIDC trusted-publishing, and a documented
upgrade path from upstream
latitudesh-python-sdk.
- Cross-provider routing (
routing="cheapest", fallback chains). - Persistence of
(provider, server_id)mappings — consumers track this. - Cost tracking / price normalization.
- Direct-to-Akash provider (still goes through the DF adapter for now).
- Provider plugin extension points for third-party providers.
These belong in v0.2+ once v0.1 is stable on PyPI.
from edge_python_sdk import EdgeClient
# Latitude direct (strict enums enforced where upstream enforces them)
client = EdgeClient(provider="latitude", bearer="lt_...")
# Digital Frontier adapter
client = EdgeClient(
provider="digital-frontier",
base_url="https://df.example.com/api/v1",
bearer="df_...",
)Constructor parameters:
| Parameter | Type | Required | Notes |
|---|---|---|---|
provider |
Literal["latitude", "digital-frontier"] |
yes | More providers in v0.2+ |
bearer |
str | Callable[[], str] |
yes | Pass-through to upstream SDK |
base_url |
str | None |
required when provider="digital-frontier"; ignored otherwise |
Adapter's /api/v1 root |
client / async_client |
httpx.Client | AsyncClient |
optional | Pass-through |
retry_config / timeout_ms / debug_logger |
… | optional | Pass-through |
Anything else upstream accepts is also accepted via **kwargs and forwarded.
client.servers, client.projects, client.ssh_keys, client.plans, client.regions,
client.operating_systems etc. behave exactly like upstream latitudesh-python-sdk
on the chosen server_url. We do not re-document operations here — see upstream docs.
The wrapper does not intercept, transform, or proxy these calls beyond server_url
selection and enum permissiveness.
client.catalog.sites() # list[str] — known site codes for this provider
client.catalog.plans() # list[str] — known plan slugs
client.catalog.os() # list[str] — known OS slugs
client.catalog.refresh() # fetch fresh values from the provider's discovery endpoints| Provider | sites() source |
plans() source |
os() source |
|---|---|---|---|
latitude |
Hardcoded list mirroring the upstream enum (18 values) | Calls GET /plans |
Calls GET /operating_systems |
digital-frontier |
Calls GET /regions on the adapter |
Calls GET /plans |
Calls GET /operating_systems |
refresh() re-reads from the network (no-op for Latitude sites() since it's static).
Results are cached for the lifetime of the EdgeClient instance — no TTL in v0.1.
A provider is identified by a string registered in edge_python_sdk.providers.
v0.1 ships two:
provider= |
Default base_url |
Enum behavior | Catalog source for sites |
|---|---|---|---|
latitude |
https://api.latitude.sh |
Upstream enums still validate (we don't loosen Latitude itself) | Static enum mirror |
digital-frontier |
None (required from caller) | All 7 enums permissive (accept any string) | GET /regions |
Why Latitude stays strict: the real Latitude API rejects unknown values server-side anyway. Loosening the SDK there would only delay the error from client to network round-trip with no benefit. We keep upstream behavior to be a faithful passthrough.
Permissive vs strict implementation: the patched SDK lives in
src/edge_python_sdk/_generated/ (gitignored, built from patches/). Permissive
versions replace the seven (str, Enum) classes with type aliases:
# Before (upstream)
class CreateServerServersSite(str, Enum):
ASH = "ASH"
BUE = "BUE"
# ... 16 more
# After (patched)
CreateServerServersSite = str # any string accepted; runtime validation by APIAt runtime, the active provider is enforced via construction-time selection — when
provider="latitude", the wrapper validates inputs against the original enum values
before passing them to the patched SDK; when provider="digital-frontier", no
client-side validation runs.
Pinned upstream: latitudesh-python-sdk==3.0.5.
Three patch files, each targeting one logical concern:
| Patch | Files modified | Enum classes converted |
|---|---|---|
patches/0001-site-enum-permissive.patch |
models/create_serverop.py, models/post_vpn_sessionop.py, models/create_virtual_networkop.py |
CreateServerServersSite, PostVpnSessionVpnSessionsSite, CreateVirtualNetworkPrivateNetworksSite |
patches/0002-plan-enum-permissive.patch |
models/create_serverop.py |
CreateServerServersPlan |
patches/0003-os-enum-permissive.patch |
models/create_serverop.py, models/create_server_reinstallop.py, models/update_server_deploy_configop.py |
CreateServerServersOperatingSystem, CreateServerReinstallServersOperatingSystem, UpdateServerDeployConfigServersOperatingSystem |
Patch format: git format-patch output, applied with git apply against an
unpacked upstream tarball.
Original enum value preservation: the patched SDK exports _ORIGINAL_SITES,
_ORIGINAL_PLANS, _ORIGINAL_OS constants holding the original enum members, so
the Latitude provider can still do strict validation.
upstream tarball (pip download)
│
▼
scripts/apply-patches.sh
│ (extract, apply 3 patches, rewrite import path)
▼
src/edge_python_sdk/_generated/ (gitignored)
│
▼ imported by
src/edge_python_sdk/client.py (EdgeClient wrapper)
Why rewrite the import path: to avoid collision when both latitudesh-python-sdk
and edge-python-sdk are installed in the same environment, the patched module
is namespaced as edge_python_sdk._generated internally, and re-exported only
through EdgeClient and edge_python_sdk.models.
Build script: scripts/apply-patches.sh is idempotent. It pins the upstream
version, downloads via pip download --no-deps --no-binary=:all:, applies the
patches, and rewrites imports. CI runs it on every push.
Decided 2026-05-13 by quorum (codex-1, gemini-1 unanimous; see
.planning/quorum/debates/2026-05-13-edge-python-sdk-version-scheme.md).
- Public API follows independent wrapper semver — fully decoupled from upstream cadence.
- Wrapper version:
0.1.0,0.2.0,1.0.0, … onpyproject.toml. - Upstream version recorded in package metadata, not in the version string:
[project] name = "edge-python-sdk" version = "0.1.0" [tool.edge-python-sdk] upstream-package = "latitudesh-python-sdk" upstream-version = "3.0.5"
- Runtime accessors:
edge_python_sdk.__version__ # "0.1.0" edge_python_sdk.upstream_version # "3.0.5"
- README states the wrapped upstream version prominently.
- Breaking changes to
EdgeClienttrigger wrapper major bumps (1.0.0→2.0.0), independent of upstream major bumps. - v0.1 ships as
0.1.0(wrapper) wrapping upstream3.0.5.
| Alternative | Reason rejected |
|---|---|
3.0.5+ep1 (PEP 440 local version) |
PyPI Warehouse rejects local-version segments on public uploads (PEP 440). Dead on arrival. |
3.0.5.post1 (PEP 440 post-release) |
Misuses .postN — PEP 440 reserves it for trivial post-release corrections, not feature-additive wrapper iterations. Misrepresents the wrapper as a post-release of upstream Latitude. |
edge-python-sdk-3 v 0.1.0 (hybrid name) |
Package-name churn per upstream major hurts pip install edge-python-sdk discoverability. Only pays off if simultaneous parallel installs across upstream majors become a real requirement — not for v0.1. |
This pattern matches mypy-extensions (wraps mypy), pytest-asyncio (wraps pytest), and the broader wrapper-library ecosystem on PyPI.
Three GitHub Actions workflows:
- Triggers: push, pull_request
- Matrix: Python 3.10, 3.11, 3.12, 3.13
- Steps:
apply-patches.sh→uv sync→ruff check→mypy→pytest tests/ - Conformance tests run against:
provider="latitude": mocked HTTP viarespxprovider="digital-frontier": a locallatitude-api-adapterinstance spun up via docker-compose (CockroachDB + Redis + adapter image)
- Trigger: daily cron + manual dispatch
- Steps:
- Read pinned upstream version from
pyproject.toml pip index versions latitudesh-python-sdk→ detect latest- If newer: bump pin, run
apply-patches.sh, run conformance - If patches still apply cleanly and tests pass: open a PR titled
sync: bump upstream to <version> - If patches conflict: open a PR with the failed patch hunks for manual rebase
- Read pinned upstream version from
- Trigger: GitHub release published from a
v*tag - Steps: build wheel + sdist → publish to PyPI via OIDC trusted publishing
- PyPI project:
edge-python-sdk— Trusted Publisher configured to this repo
edge-python-sdk/
├── .github/
│ └── workflows/
│ ├── ci.yml
│ ├── sync-upstream.yml
│ └── release.yml
├── docs/
│ ├── quickstart.md
│ ├── providers.md
│ └── upgrading-from-latitudesh.md
├── patches/
│ ├── 0001-site-enum-permissive.patch
│ ├── 0002-plan-enum-permissive.patch
│ └── 0003-os-enum-permissive.patch
├── scripts/
│ ├── apply-patches.sh
│ └── refresh-upstream.sh
├── src/
│ └── edge_python_sdk/
│ ├── __init__.py
│ ├── client.py
│ ├── providers.py
│ ├── catalog.py
│ ├── _generated/ # gitignored; built from patches
│ └── models/ # re-exports of permissive types
├── tests/
│ ├── conformance/
│ │ ├── test_servers.py
│ │ ├── test_projects.py
│ │ ├── test_ssh_keys.py
│ │ └── test_catalog.py
│ ├── unit/
│ │ ├── test_client_construction.py
│ │ └── test_enum_permissiveness.py
│ └── integration/
│ └── test_with_adapter.py # spins up latitude-api-adapter
├── pyproject.toml
├── README.md
├── SPEC.md # this file
├── CHANGELOG.md
├── LICENSE # Apache-2.0 (matches upstream)
└── .gitignore
- Construction validation (
provideraccepted, requiredbase_url, etc.) - Permissive-enum behavior: arbitrary strings pass through to the SDK
- Catalog cache behavior
- Pass-through of
**kwargsto upstreamLatitudesh
- A single suite parameterized over both providers
- Each test exercises a Latitude SDK operation and asserts the request is well-formed
- Latitude: assertions match upstream wire format exactly
- DF: assertions match adapter's accepted shape (which is also Latitude shape)
- Tests use
respxfor HTTP mocking (Latitude) or a real adapter instance (DF)
- Boot a local
latitude-api-adapterinstance against CockroachDB + Redis - Run end-to-end: create project → create ssh_key → create server → poll status → delete
- Gated by
EDGE_PROVIDER_INTEGRATION=1env var so casual contributors don't need Docker
docs/ ships with:
quickstart.md— install, instantiate both providers, do one create-server flowproviders.md— provider matrix: which catalog entries each supports, validation behavior, link to upstream SDK docsupgrading-from-latitudesh.md— 1:1 migration guide. The constructor is the only API difference; everything else is identical.
README points at all three.
These need answers before v0.1 ships, but not before scaffolding starts:
-
PEP 440 version scheme.✅ RESOLVED 2026-05-13 — wrapper semver0.1.0(independent of upstream), upstream version in[tool.edge-python-sdk]metadata. See §6 and quorum debate.planning/quorum/debates/2026-05-13-edge-python-sdk-version-scheme.md. -
License. Apache-2.0 matches upstream, but does upstream's NOTICE need attribution? Need to audit upstream license headers.
-
Catalog discovery for
latitudeprovider. Static list (hardcoded mirror of upstream enums) vs live fetch fromGET /plans. Live fetch is more correct but adds a network call on firstcatalog.plans(). Likely answer: static for sites (rarely changes), live for plans/OS (changes more often). -
Type stubs for patched SDK. Pydantic models still type as
BaseModel, but patched enum-replacement types lose IDE autocomplete for known values. Do we ship.pyistubs that re-add theLiteral[...]union of known values for IDE hints while keeping runtime permissive? -
Async support. Upstream ships sync + async. Our wrapper must too. Confirmed in §2.1 (
async_clientparam), but conformance tests should cover both. -
Test infrastructure cost. Conformance against a real DF adapter means CI spins up CRDB + Redis + adapter container on every PR. Acceptable for an OSS project but slow. Alternative: a
provider="mock"that records expected wire format and runs in-process. Likely answer: keep the real adapter for integration tests (gated), userespxmocks for conformance. -
Provider plugin extension API. Should v0.1 expose any extension point (entry-points group, registration function)? Risk: locks the API for v0.2 third-party providers before we know what they need. Likely answer: No extension API in v0.1; document explicitly that adding providers requires forking until v0.2.
Not in scope for v0.1, but framing the design:
- Direct-to-Akash provider (
provider="akash"): client-side translation of Latitude shape → Akash console API, no adapter middleman - Routing layer (
EdgeRouterseparate fromEdgeClient): fallback chains, cost-based dispatch, capability matrix - Persistence helper: optional sqlite-backed mapping of
external_id → (provider, native_id) - Cost normalization: per-provider price model, unified
$/hourview - Provider plugin entry points: third parties ship their own providers via
pip installand apyproject.tomlentry point group
When this document is approved, the next steps are (in order):
- Scaffold the repository structure (§8)
- Generate the three patch files against pinned upstream (§4)
- Write
apply-patches.shand verify it produces a working SDK - Implement
EdgeClient,providers.py,catalog.py(§2, §3) - Write conformance + unit tests (§9)
- Write CI workflows (§7)
- Initial commit + push to
main - Tag
v0.1.0-alpha.1, dry-run PyPI publish via TestPyPI - Tag
v0.1.0, publish to PyPI