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-odxproxyclientImport from terrakernel.odxproxyclient:
from terrakernel.odxproxyclient import ODXProxyClientConstruct 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.
| Action | Method |
|---|---|
search_count | session.search_count(model_id, *, params, keyword, ...) |
search | session.search(...) |
read | session.read(...) |
fields_get | session.fields_get(...) |
search_read | session.search_read(...) |
create | session.create(...) |
write | session.write(...) |
unlink | session.unlink(...) |
call_method | session.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") # -> JsonValuelicense() 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.
| Exception | Proxy code | Meaning |
|---|---|---|
AuthError | -32000 | Missing or wrong x-api-key. |
InvalidActionError | -32001 | Action not in the allowlist. |
MissingFnNameError | -32002 | call_method without fn_name. |
OdooTimeoutError | -32003 | Upstream Odoo timed out. |
OdooConnectError | -32004 | Proxy couldn't reach Odoo. |
InternalProxyError | -32005 | Internal proxy error. |
LicenseError | 0 (HTTP 403) | Proxy integrity/license check failed. |
OdooLogicError | Odoo's code (HTTP 200) | Odoo business error (validation, access rights). |
TransportError | — | Local 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.