Official Python SDK for the OctaSpace API.
- Resource-oriented API:
client.nodes.list(),client.services.session(uuid).stop() - Sync-first client built on
httpx - Keep-alive mode with configurable pool limits
- URL rotation / failover across multiple base URLs
- Retry with exponential backoff and
Retry-Afterhandling - Typed error hierarchy with request ID and raw response access
on_request/on_responsehooks for logging, tracing, and diagnostics- Shared client helpers for application-style integrations
- Browser playground for manual QA against mock scenarios or the live API
- Typed models for
Account,Balance,Node, andSession
Use this after the first public PyPI release. Until then, use the local development setup below.
pip install octaspaceIf you use uv:
uv add octaspaceRecommended modern setup from the repository root:
uv sync --group devThis installs the SDK in editable mode together with test, lint, typecheck, and playground dependencies.
If you prefer a plain virtualenv + pip flow:
python3 -m venv .venv
.venv/bin/python -m pip install -e . pytest pytest-cov respx mypy ruff hatchling fastapi jinja2 python-multipart uvicornfrom octaspace import OctaSpaceClient
# Public endpoint
with OctaSpaceClient() as client:
print(client.network.info().data)
# Authenticated endpoints
with OctaSpaceClient(api_key="your-api-key") as client:
print(client.accounts.profile().data)
print(client.accounts.balance().data)
print(client.nodes.list().data)from octaspace import OctaSpaceClient
client = OctaSpaceClient(api_key="your-api-key")
# Accounts
client.accounts.profile() # GET /accounts
client.accounts.balance() # GET /accounts/balance
client.accounts.generate_wallet() # POST /accounts
# Nodes
client.nodes.list() # GET /nodes
client.nodes.find(123) # GET /nodes/:id
client.nodes.download_ident(123) # GET /nodes/:id/ident
client.nodes.download_logs(123) # GET /nodes/:id/logs
client.nodes.update_prices(123, gpu_hour=0.5) # PATCH /nodes/:id/prices
client.nodes.reboot(123) # GET /nodes/:id/reboot
# Sessions
client.sessions.list() # GET /sessions
client.sessions.list(recent=True) # GET /sessions?recent=true
# Session proxy
session = client.services.session("uuid-here")
session.info() # GET /services/:uuid/info
session.logs() # GET /services/:uuid/logs
session.stop(score=5) # POST /services/:uuid/stop
# Services
client.services.mr.list() # GET /services/mr
client.services.mr.create(
node_id=1,
disk_size=10,
image="ubuntu:24.04",
app="249b4cb3-3db1-4c06-98a4-772ba88cd81c",
) # POST /services/mr
client.services.vpn.list() # GET /services/vpn
client.services.vpn.create(node_id=1, subkind="wg") # POST /services/vpn
client.services.render.list() # GET /services/render
client.services.render.create(node_id=1, disk_size=100) # POST /services/render
# Apps
client.apps.list() # GET /apps
# Network
client.network.info() # GET /network
# Idle Jobs
client.idle_jobs.find(node_id=69, job_id=42) # GET /idle_jobs/:node_id/:job_id
client.idle_jobs.logs(node_id=69, job_id=42) # GET /idle_jobs/:node_id/:job_id/logs
client.close()Like the Ruby SDK, the Python package exposes a process-level shared client for app-style integrations.
import octaspace
octaspace.configure(
api_key="your-api-key",
keep_alive=True,
pool_size=10,
)octaspace.client() with no arguments returns a lazily initialized shared client based on the current global configuration:
import octaspace
octa_client = octaspace.client()
print(octa_client.network.info().data)Pass arguments to create a one-off client instead:
import octaspace
user_client = octaspace.client(api_key="per-user-token")
print(user_client.accounts.profile().data)
user_client.close()When your process stops, or when a worker is recycling, close the shared client explicitly:
import octaspace
octaspace.shutdown_shared_client()You can also reset the global configuration:
import octaspace
octaspace.reset_configuration()| Option | Default | Description |
|---|---|---|
api_key |
None |
API key sent as the Authorization header |
base_url |
https://api.octa.space |
Primary API endpoint |
base_urls |
None |
Multiple endpoints enabling rotation / failover |
keep_alive |
False |
Reuse pooled connections instead of forcing Connection: close |
pool_size |
5 |
Maximum keep-alive connections |
max_connections |
20 |
Total connection cap |
pool_timeout |
5.0 |
Seconds to wait for an available pooled connection |
idle_timeout |
60.0 |
Keep-alive expiry in seconds |
open_timeout |
10.0 |
TCP connect timeout |
read_timeout |
30.0 |
Response read timeout |
write_timeout |
30.0 |
Request body write timeout |
max_retries |
2 |
Retry attempts on retryable responses |
retry_interval |
0.5 |
Base retry interval in seconds |
backoff_factor |
2.0 |
Exponential backoff multiplier |
ssl_verify |
True |
Toggle TLS certificate verification |
on_request |
None |
Callable receiving RequestContext before each request |
on_response |
None |
Callable receiving APIResponse after each response |
logger |
None |
Standard-library logger used by hook dispatch |
user_agent |
auto | Override the default SDK user agent |
ClientConfig.from_env() reads configuration from environment variables:
from octaspace.config import ClientConfig
config = ClientConfig.from_env()Supported variables:
OCTA_API_KEYOCTA_BASE_URLOCTA_BASE_URLSOCTA_KEEP_ALIVEOCTA_POOL_SIZEOCTA_MAX_CONNECTIONSOCTA_POOL_TIMEOUTOCTA_IDLE_TIMEOUTOCTA_OPEN_TIMEOUTOCTA_READ_TIMEOUTOCTA_WRITE_TIMEOUTOCTA_MAX_RETRIESOCTA_RETRY_INTERVALOCTA_BACKOFF_FACTOROCTA_SSL_VERIFYOCTA_LOGGEROCTA_USER_AGENT
OCTA_BASE_URLS accepts a comma-separated list.
from octaspace import OctaSpaceClient
with OctaSpaceClient(
api_key="your-api-key",
keep_alive=True,
pool_size=10,
idle_timeout=120,
) as client:
print(client.transport_stats())Example transport stats:
{
"mode": "persistent",
"limits": {
"max_keepalive_connections": 10,
"max_connections": 20,
"keepalive_expiry": 120.0,
},
"url": "https://api.octa.space",
}from octaspace import OctaSpaceClient
with OctaSpaceClient(
api_key="your-api-key",
base_urls=[
"https://api.octa.space",
"https://api2.octa.space",
],
) as client:
print(client.transport_stats())Behaviour:
- healthy endpoints are selected round-robin
- connection and timeout failures temporarily mark a URL as failed
- failed URLs automatically re-enter rotation after cooldown
- if only one URL is configured, no rotator is created
import logging
from octaspace import OctaSpaceClient
logger = logging.getLogger("octaspace-demo")
def on_request(ctx) -> None:
logger.info("-> %s %s", ctx.method.upper(), ctx.path)
def on_response(response) -> None:
logger.info("<- %s %s", response.status_code, response.request_id)
with OctaSpaceClient(
api_key="your-api-key",
on_request=on_request,
on_response=on_response,
logger=logger,
) as client:
client.network.info()from octaspace import (
APIConnectionError,
APIPermissionError,
APITimeoutError,
AuthenticationError,
NotFoundError,
OctaSpaceClient,
RateLimitError,
ValidationError,
)
try:
with OctaSpaceClient(api_key="token") as client:
client.nodes.find(999_999)
except NotFoundError as exc:
print("Not found", exc.request_id)
except AuthenticationError:
print("Invalid API key")
except APIPermissionError:
print("Permission denied")
except ValidationError as exc:
print("Validation failed", exc)
except RateLimitError as exc:
print("Retry after", exc.retry_after)
except (APIConnectionError, APITimeoutError) as exc:
print("Network issue", exc)OctaSpaceError
├── ConfigurationError
├── NetworkError
│ ├── APIConnectionError
│ └── APITimeoutError
└── APIError
├── AuthenticationError 401
├── APIPermissionError 403
├── NotFoundError 404
├── ValidationError 422
├── RateLimitError 429
└── ServerError 5xx
├── BadGatewayError 502
├── ServiceUnavailableError 503
└── GatewayTimeoutError 504
All APIError subclasses expose:
status_coderequest_idresponse
RateLimitError also exposes retry_after.
Resources return APIResponse objects by default. If you want structured helper objects, use the typed models explicitly:
from octaspace import OctaSpaceClient
from octaspace.models import Node
with OctaSpaceClient(api_key="your-api-key") as client:
response = client.nodes.find(123)
node = Node.from_dict(response.data)
print(node.id)
print(node.state)
print(node.online)Available models: Node, Account, Balance, Session.
The repository includes a development-only FastAPI playground. It is not included in the published PyPI wheel and exists for manual SDK QA, mirroring the role of the Ruby dummy app.
From the repository root:
uv sync --group playground
uvicorn playground.main:app --reloadOpen:
http://127.0.0.1:8000/playground/dashboard
- the same page set and general layout direction as the Ruby dummy app
- sidebar runtime settings for API key, transport scenario, and base URL
- direct pages for dashboard, network, account, apps, nodes, sessions, services, idle jobs, and diagnostics
- footer request log for every SDK call triggered through the playground
- deterministic offline QA via the
Fixturesscenario - failure simulation via
401,403,404,429,500,slow,timeout, andnetwork-error - live QA against the real API when
Transport = Real API
| Route | Content |
|---|---|
/playground/dashboard |
Network + account summary |
/playground/network |
Public network endpoint and transport summary |
/playground/account |
Profile + balance view |
/playground/apps |
App catalog |
/playground/nodes |
Node list + detail panel |
/playground/nodes/{id} |
Standalone node detail |
/playground/sessions |
Current and recent sessions |
/playground/services |
Machine Rental, Render, and VPN tabs |
/playground/idle-jobs |
Idle job details + logs |
/playground/diagnostics |
Direct SDK call execution + smoke suite |
Notes:
- the sidebar intentionally mirrors the Ruby dummy app navigation, so
Accountexists as a direct route but is not shown in the nav - request log lives in the footer, not on a separate
Requestspage - diagnostics includes mutation methods; only run them against disposable targets when using real credentials
Detailed QA steps live in:
The repository exposes both a module entry point and a script entry point:
.venv/bin/python -m octaspace
.venv/bin/octaspace-smokeIf OCTA_API_KEY is present, smoke output includes accounts.balance() as well as network.info().
.venv/bin/python>>> from octaspace import OctaSpaceClient
>>> client = OctaSpaceClient()
>>> client.network.info().data
>>> client.close()read -s OCTA_API_KEY
export OCTA_API_KEY
.venv/bin/pythonread -s OCTA_API_KEY and export OCTA_API_KEY are shell commands, not Python statements. Run them in zsh / bash, type the token when prompted, press Enter, and only then start Python.
>>> import os
>>> from octaspace import OctaSpaceClient
>>> client = OctaSpaceClient(api_key=os.environ["OCTA_API_KEY"], keep_alive=True, pool_size=10)
>>> client.accounts.profile().data
>>> client.accounts.balance().data
>>> len(client.nodes.list().data)
>>> len(client.sessions.list().data)
>>> client.transport_stats()
>>> client.close()If you are already inside Python REPL, set the variable there instead of using read:
>>> import os, getpass
>>> os.environ["OCTA_API_KEY"] = getpass.getpass("OCTA_API_KEY: ").venv/bin/python examples/console_mock.pyExpected output includes:
transport_statsnetworkprofilebalancenodessession_infosession_stop
.venv/bin/python examples/console_real.pyOptional environment variables:
OCTA_API_KEYOCTA_SESSION_UUIDOCTA_ALLOW_STOP=1
If OCTA_API_KEY is unset, the script still verifies the public network endpoint and skips authenticated checks explicitly.
.venv/bin/ruff check src tests examples playground
.venv/bin/mypy src/octaspace playground
.venv/bin/pytestOr with uv if installed:
uv run ruff check src tests examples playground
uv run mypy src/octaspace playground
uv run pytestBuild the distribution:
.venv/bin/python -m buildOr with uv:
uv buildInspect the wheel contents (should only contain octaspace/, no playground/):
python3 -m zipfile -l dist/octaspace-*.whl | grep -v dist-infoCreate a clean environment and install the built wheel:
python3 -m venv /tmp/octaspace-wheel-test
/tmp/octaspace-wheel-test/bin/python -m pip install dist/octaspace-*.whl
/tmp/octaspace-wheel-test/bin/python -c "from octaspace import OctaSpaceClient; print(OctaSpaceClient().network.info().data)"Release gate:
ruffcleanmypycleanpytestall passing- console verification completed
- browser playground verification completed
- wheel smoke test passing
- follow
docs/runbooks/publish.mdfor TestPyPI → PyPI