Back to Articles

oxp-python: A Type-Safe Python Client for the Open eXecution Protocol

[ View on GitHub ]

oxp-python: A Type-Safe Python Client for the Open eXecution Protocol

Hook

Most Python REST clients are hand-written and fall out of sync with their APIs within weeks. oxp-python takes a different approach: it’s completely auto-generated from an API specification, guaranteeing that your client always matches the server contract.

Context

The Open eXecution Protocol (OXP) is a REST-based protocol designed for execution and orchestration services, enabling applications to coordinate distributed workloads and computational tasks. As with any API, consuming it requires writing HTTP client code—authentication headers, retry logic, error handling, request serialization, response parsing, and the endless boilerplate that comes with network programming.

Historically, developers have tackled this in two ways: writing custom wrappers around libraries like requests or httpx, or using code generation tools like OpenAPI Generator. The first approach gives maximum control but requires constant maintenance as APIs evolve. The second promises automation but often produces clunky, unidiomatic code that fights against your IDE and type checker. oxp-python attempts to split the difference, using Stainless—a modern SDK generation platform—to produce a Python client that feels hand-crafted while remaining automatically synchronized with the OXP API specification.

Technical Insight

sync call

async call

TypedDict params

TypedDict params

bearer token + retries

REST API calls

JSON response

raw response

Pydantic validation

typed objects

on error

4xx/5xx errors

Python Application

Oxp Client

AsyncOxp Client

Request Builder

httpx HTTP Client

OXP REST API

Response Handler

Pydantic Models

Exception Classes

System architecture — auto-generated

The architecture of oxp-python centers around three key decisions: auto-generation via Stainless, dual sync/async interfaces, and comprehensive type safety through Pydantic models and TypedDicts.

Stainless generates the client code directly from the OXP API specification, which means every endpoint, parameter, and response shape is mechanically derived from the source of truth. This eliminates an entire class of bugs where documentation says one thing but the client does another. When the OXP API evolves, regenerating the client produces updated code automatically. The generated code wraps httpx, a modern HTTP library with both synchronous and asynchronous support baked in from the ground up.

Using the client is straightforward. Here’s a synchronous example:

from oxp import Oxp

client = Oxp(
    bearer_token="your_token_here",
    base_url="https://api.example.com",  # optional, uses default if omitted
    timeout=30.0,  # seconds
    max_retries=2
)

# Make a request to an OXP endpoint
response = client.executions.create(
    workflow_id="wf_abc123",
    parameters={"input_data": "sample"},
    priority="high"
)

print(response.execution_id)
print(response.status)

The async version is nearly identical:

import asyncio
from oxp import AsyncOxp

async def main():
    client = AsyncOxp(bearer_token="your_token_here")
    
    response = await client.executions.create(
        workflow_id="wf_abc123",
        parameters={"input_data": "sample"},
        priority="high"
    )
    
    print(response.execution_id)

asyncio.run(main())

Notice the only difference is the import and the await keyword. Both clients expose identical methods with identical parameters, so switching between sync and async is trivial. This design decision recognizes that some applications are built on async frameworks like FastAPI or asyncio event loops, while others use traditional synchronous patterns. Rather than forcing a choice or maintaining separate libraries, oxp-python supports both paradigms equally.

The type safety story is where things get interesting. Request parameters use TypedDicts, which provide structure without runtime overhead:

from typing import TypedDict, Literal

class ExecutionCreateParams(TypedDict, total=False):
    workflow_id: str  # required
    parameters: dict[str, object]
    priority: Literal["low", "normal", "high"]
    timeout_seconds: int

Your IDE can autocomplete these parameters, and type checkers like mypy or pyright will catch mistakes before runtime. Response objects use Pydantic models, which validate data structure on deserialization:

from pydantic import BaseModel
from datetime import datetime

class Execution(BaseModel):
    execution_id: str
    workflow_id: str
    status: Literal["pending", "running", "completed", "failed"]
    created_at: datetime
    completed_at: datetime | None
    result: dict[str, object] | None

This means if the server returns unexpected data—say, a string where you expect an integer—Pydantic raises a validation error immediately rather than letting corrupt data propagate through your application. It’s defensive programming with zero boilerplate.

Error handling is granular and specific. Rather than catching generic exceptions, you can handle different HTTP status codes with different exception types:

from oxp import (
    Oxp,
    APIStatusError,
    APITimeoutError,
    RateLimitError,
    InternalServerError
)

client = Oxp(bearer_token="token")

try:
    response = client.executions.create(workflow_id="wf_123")
except RateLimitError as e:
    print(f"Rate limited. Retry after {e.response.headers.get('Retry-After')} seconds")
except APITimeoutError:
    print("Request timed out. Check network or increase timeout.")
except InternalServerError as e:
    print(f"Server error: {e.status_code}. Contact OXP support.")
except APIStatusError as e:
    print(f"API error: {e.status_code} - {e.message}")

The library also supports streaming responses, useful when dealing with large payloads or long-running operations that emit progress updates:

with client.executions.stream(execution_id="exec_456") as stream:
    for chunk in stream:
        process_chunk(chunk)

Automatic retries use exponential backoff by default, configurable per-client or per-request. The retry logic is smart enough to only retry idempotent methods (GET, PUT, DELETE) and to respect server-side retry hints via headers like Retry-After.

Gotcha

The biggest limitation isn’t technical—it’s ecosystem maturity. With only 17 GitHub stars, oxp-python has minimal production usage. That means you’re unlikely to find Stack Overflow answers, community-written guides, or third-party integrations. If you hit an edge case or bug, you’re probably the first person to encounter it, and you’ll be filing an issue rather than finding an existing solution.

The underlying Open eXecution Protocol itself is relatively obscure. While the client library is well-engineered, its usefulness is entirely dependent on OXP services being available and meeting your needs. If you’re evaluating execution orchestration protocols, you’ll need to assess OXP’s feature set, performance characteristics, and ecosystem support independently. A beautifully designed client for a protocol that doesn’t fit your use case is still the wrong tool.

Being auto-generated via Stainless is both a strength and a constraint. The generation ensures consistency and reduces maintenance burden, but it also means customization is limited. If you need to add custom middleware, modify request behavior in ways not exposed through configuration, or extend the client with helper methods, you’re working around the generation tooling rather than with it. You can subclass the client or wrap it, but any modifications live outside the auto-generated code and won’t be preserved when the client is regenerated. For teams that need deep customization or want to contribute improvements back to the client itself, the auto-generation workflow adds friction.

Verdict

Use if: You’re building Python services that interact with OXP-compliant APIs and value type safety, modern async support, and production-ready features like retries and timeouts out of the box. The dual sync/async interfaces make it suitable for both traditional Flask-style applications and modern asyncio-based frameworks like FastAPI. The strong typing via Pydantic and TypedDicts provides excellent developer experience in modern editors. Skip if: You need a battle-tested library with extensive community support and a rich ecosystem of examples and integrations—the low adoption numbers suggest you’ll be an early adopter navigating uncharted territory. Also skip if you’re still evaluating execution orchestration protocols and haven’t committed to OXP specifically, as the client’s usefulness is entirely dependent on OXP meeting your architectural needs. Finally, if you anticipate needing heavy customization beyond standard REST client behavior, the auto-generation approach may feel constraining compared to writing a custom httpx wrapper.

// ADD TO YOUR README
[![Featured on Starlog](https://starlog.is/api/badge/ai-agents/openexecprotocol-oxp-python.svg)](https://starlog.is/api/badge-click/ai-agents/openexecprotocol-oxp-python)