ODXProxy
SDKs

Python SDK

A typed, sync + async Python client for ODXProxy, built on httpx with HTTP/2 connection reuse.

terrakernel-odxproxyclient is the official Python client for ODXProxy. It exposes sync and async surfaces side by side (same constructor, same method names), is built on httpx with HTTP/2 connection reuse, and is fully PEP 561 typed.

Source: terrakernel/ODXProxyClient-Python · PyPI terrakernel-odxproxyclient · Python 3.12+.

Install

pip install terrakernel-odxproxyclient

Import from terrakernel.odxproxyclient:

from terrakernel.odxproxyclient import ODXProxyClient

Construct a client, bind an instance

Create one ODXProxyClient per process (it owns an httpx.Client — the TCP/TLS pool is the main performance knob), then bind an Odoo instance with for_instance() to get a reusable Session. Per-instance details travel in every request body, so binding once and reusing the session is the canonical pattern.

from terrakernel.odxproxyclient import ODXProxyClient

with ODXProxyClient(
    "https://your-proxy.example.com",  # proxy base URL
    "<proxy x-api-key>",               # the PROXY's key
    default_timeout_secs=15,
) as client:
    erp = client.for_instance(
        url="https://erp.example.com",
        db="prod",
        user_id=2,
        api_key="<odoo user api key>",  # the Odoo USER's key
    )

    partners = erp.search_read(
        "res.partner",
        params=[[["is_company", "=", True]]],
        keyword={"fields": ["id", "name", "email"], "limit": 50, "context": {"tz": "UTC"}},
    )

Two different secrets

The client's api_key (2nd positional arg) is the proxy's x-api-key; for_instance(..., api_key=...) is the Odoo user's API key. They are never the same value — don't conflate them.

Methods return the JSON-RPC result directly (a JsonValue) and raise a typed exception on any error — there's no envelope to unwrap. params (a list) and keyword (a dict) are passed through to Odoo unchanged; you build the raw Odoo shapes yourself.

Async

AsyncODXProxyClient / AsyncSession mirror the sync API exactly — same args, same methods — just await the calls inside async with:

from terrakernel.odxproxyclient import AsyncODXProxyClient

async with AsyncODXProxyClient("https://your-proxy.example.com", "<proxy x-api-key>") as client:
    erp = client.for_instance(url="https://erp.example.com", db="prod", user_id=2, api_key="...")
    count = await erp.search_count("res.partner", params=[[["is_company", "=", True]]])

The 9 actions

Each allowed action is a method on the Session. All take keyword-only params, keyword, request_id, and timeout_secs (per-call override); call_method additionally takes fn_name positionally.

ActionMethod
search_countsession.search_count(model_id, *, params, keyword, ...)
searchsession.search(...)
readsession.read(...)
fields_getsession.fields_get(...)
search_readsession.search_read(...)
createsession.create(...)
writesession.write(...)
unlinksession.unlink(...)
call_methodsession.call_method(model_id, fn_name, *, params, keyword, ...)
new_id = erp.create("res.partner", params=[{"name": "Acme Inc", "is_company": True}])
erp.write("res.partner", params=[[new_id], {"name": "Acme LLC"}])
erp.unlink("res.partner", params=[[new_id]])

# call_method — fn_name is required and positional
erp.call_method("sale.order", "action_confirm", params=[[42]])

Calling call_method with an empty fn_name raises ValueError client-side (the proxy would otherwise return code -32002). Anything outside the 9 actions must go through call_method. Unlike some of the other SDKs, this client does not auto-strip pagination keys — pass only the keyword fields the action expects.

Operational endpoints

These live on the client (not the session) and need no Odoo instance:

client.license()             # -> LicenseInfo(licensee, valid_until, is_valid)
client.about()               # -> BuildInfo(build, version)
client.metrics()             # -> str (Prometheus text)
client.odoo_version("https://erp.example.com")  # -> JsonValue

license() returns a typed LicenseInfo, not a JSON-RPC envelope — the proxy's /_/license endpoint emits a flat object. about, license, and metrics send no API key.

Typed errors

Every failure raises a subclass of ODXProxyError, one per proxy error code, so you can catch exactly what you need. Each carries .code, .message, .data, .http_status, and .request_id. See the full error catalog.

ExceptionProxy codeMeaning
AuthError-32000Missing or wrong x-api-key.
InvalidActionError-32001Action not in the allowlist.
MissingFnNameError-32002call_method without fn_name.
OdooTimeoutError-32003Upstream Odoo timed out.
OdooConnectError-32004Proxy couldn't reach Odoo.
InternalProxyError-32005Internal proxy error.
LicenseError0 (HTTP 403)Proxy integrity/license check failed.
OdooLogicErrorOdoo's code (HTTP 200)Odoo business error (validation, access rights).
TransportErrorLocal transport failure (DNS/TCP/TLS/local timeout) before the proxy replies.
from terrakernel.odxproxyclient import AuthError, OdooLogicError, OdooTimeoutError, ODXProxyError

try:
    partners = erp.search_read("res.partner", params=[[]], keyword={"context": {"tz": "UTC"}})
except AuthError:
    reauth()                      # bad x-api-key
except OdooLogicError as e:
    show(e.message)               # Odoo validation / access error (arrived on HTTP 200)
except OdooTimeoutError:
    retry_with_backoff()
except ODXProxyError as e:
    log(e.code, e.message, e.http_status)

A JSON-RPC error can arrive with HTTP 200 (Odoo logic errors surface as OdooLogicError); the client inspects the envelope's error field for you and raises. Any error code outside the proxy's own catalog is treated as an Odoo pass-through (OdooLogicError).


Looking for another language? See the SDK overview — every client mirrors this same wire protocol.

On this page