Elmo Python SDK
Read and manage the brands, prompts, competitors, and AI-visibility analytics of this deployment.
This package calls the Elmo API from Python, with a synchronous and an asynchronous client built on httpx2. It includes Pydantic models for every request and reply.
Installation
The SDK is published on PyPI. Install it with the package manager you use.
uv add elmo-sdk
Other package managers
pip install elmo-sdk
poetry add elmo-sdk
pdm add elmo-sdk
Usage
Set ELMO_API_KEY in your environment, then call the API:
from elmo_sdk import Elmo
elmo = Elmo()
data = elmo.brands.list()
print(data)
The same call with AsyncElmo
import asyncio
from elmo_sdk import AsyncElmo
async_elmo = AsyncElmo()
async def main() -> None:
data = await async_elmo.brands.list()
print(data)
asyncio.run(main())
Requirements
- Python 3.10 or later
Pagination
Iterate a paginated call to read every item. Each page is fetched when the loop reaches it.
from elmo_sdk import Elmo
elmo = Elmo()
for item in elmo.brands.list():
print(item)
Errors
When the API answers with an error status, the call raises APIError, or a subclass named for the status. data holds the body as the model its status declares, where there is one. request_id holds the id to quote when you report a failure. Every exception a call raises subclasses ElmoError.
from elmo_sdk import APIError, Elmo
elmo = Elmo()
try:
elmo.brands.list()
except APIError as error:
print(error.status, error.request_id, error.data)
An argument the call refuses raises ElmoError itself, before anything is sent, with pydantic's error as its __cause__. A model you build yourself raises pydantic.ValidationError, as any pydantic model does.
Catch one status by its class:
| Status | Error |
|---|---|
| 400 | BadRequestError |
| 401 | AuthenticationError |
| 402 | PaymentRequiredError |
| 403 | PermissionDeniedError |
| 404 | NotFoundError |
| 409 | ConflictError |
| 422 | UnprocessableEntityError |
| 429 | RateLimitError |
| 500 and above | InternalServerError |
| Any other error status | APIError |
A call that got no reply has no status. It raises TransportError, or TransportTimeoutError where an attempt ran out of time. Neither is an APIError.
A call made with no credential raises MissingCredentialError before anything is sent. A reply the SDK cannot read raises DecodeError.
By the time a call raises RateLimitError, the client has sent it again up to 2 times. retry_after_seconds on RateLimitError holds how long the API asked to wait, in seconds, where it said.
To report a failure, open an issue and quote request_id.
Configuration
Authentication
The client reads ELMO_API_KEY from the environment. To pass the value yourself, set api_key:
from elmo_sdk import Elmo
elmo = Elmo(api_key="…")
api_key also takes a function that returns the value, such as one that reads it from a secret store. The client calls it for each request.
Base URL
Calls go to /api/v1. To send them elsewhere, such as a proxy or a gateway, set base_url, or the ELMO_BASE_URL environment variable:
from elmo_sdk import Elmo
elmo = Elmo(base_url="https://gateway.example.com")
Closing the client
The client opens a connection pool on its first call, and a with block closes it on the way out. A client that lives as long as the process closes with close() when you shut down. AsyncElmo closes with async with and aclose(). A client you passed as http_client is yours to close.
from elmo_sdk import Elmo
with Elmo() as elmo:
elmo.brands.list()
The same with AsyncElmo
import asyncio
from elmo_sdk import AsyncElmo
async def main() -> None:
async with AsyncElmo() as async_elmo:
await async_elmo.brands.list()
asyncio.run(main())
Per-call options
One call can run under settings of its own. with_options returns a client that differs only in what you pass and sends over the same connection pool, and takes every option the client does. Headers merge per name, and every other option replaces the client's:
from elmo_sdk import Elmo
elmo = Elmo()
elmo.with_options(timeout=5, max_retries=0).brands.list()
A call also takes the same settings as keyword arguments:
from elmo_sdk import Elmo
elmo = Elmo()
elmo.brands.list(timeout=5, max_retries=0)
Retries
A failed call is sent again up to 2 times, after a lost connection, a 408, 425 or 429 status, or most 5xx statuses. A POST or PATCH call may already have changed something, so it is sent again only after a request that never reached the API or a 408, 425 or 429 status.
Each wait is longer than the last on average, or as long as Retry-After asks.
To change that count, set max_retries on the client or on one call. For every other rule, such as which failures are retried and how long each wait is, set retry, which takes a RetryOptions:
from elmo_sdk import Elmo
elmo = Elmo(max_retries=5)
elmo.with_options(max_retries=0).brands.list()
To wait longer between attempts, set the first wait and the longest one:
from elmo_sdk import Elmo
elmo = Elmo(retry={"delay": 1, "max_delay": 10})
Timeouts
Each attempt may run for NaN seconds. One that runs longer raises TransportTimeoutError. To change the limit, set timeout in seconds on the client or on one call. False removes it:
from elmo_sdk import Elmo
elmo = Elmo(timeout=20)
elmo.with_options(timeout=5).brands.list()
Logging
Logging is off by default. To see what the SDK sends and receives, set log_level or the ELMO_LOG environment variable. "info" logs each call and its reply, and "debug" adds headers, with credentials redacted. Bodies are never logged. Records go to a logger under elmo_sdk, which writes to standard error until the application configures logging. Pass logger to use your own.
from elmo_sdk import Elmo
elmo = Elmo(log_level="info")
Custom HTTP client
To send requests over HTTP/2, through a proxy, or with other TLS settings or connection limits, pass an httpx2.Client you built as http_client. HTTP/2 needs the httpx2[http2] extra. A plain httpx.Client (or httpx.AsyncClient) works here too, since the two share their shape. AsyncElmo takes an httpx2.AsyncClient.
import httpx2
from elmo_sdk import Elmo
elmo = Elmo(http_client=httpx2.Client(http2=True))
Default headers
To send a header with every call, set default_headers on the client. A call's own extra_headers are merged with them by name, and None drops one:
from elmo_sdk import Elmo
elmo = Elmo(default_headers={"x-team": "billing"})
Interceptors
To run code of your own around every call, set interceptors on the client. A request hook runs on each request last before it is sent, with the credential already on it. A response hook returns the result the caller reads, and an error hook returns the error the caller gets, so a hook that only observes returns what it was given. Hooks run in the order you list them. AsyncElmo also waits for a hook that returns an awaitable.
from elmo_sdk import Elmo, PreparedRequest
def tag(request: PreparedRequest) -> None:
request.meta["x-request-source"] = "worker"
elmo = Elmo(interceptors={"request": [tag]})
Raw response
To read the HTTP response as well, make the call through with_response. What comes back holds:
data: what the call returns on its ownstatus: the status it arrived withrequest_id: the id to quote when you report a problemresponse: thehttpx2.Responseitself, for a header or a field the SDK does not model
from elmo_sdk import Elmo
elmo = Elmo()
reply = elmo.with_response.me.get()
print(reply.status, reply.response.headers)
Environment variables
The client reads each variable when its option is not set:
| Variable | Option | Default |
|---|---|---|
ELMO_API_KEY |
api_key |
|
ELMO_BASE_URL |
base_url |
"/api/v1" |
ELMO_LOG |
log_level |
"off" |
Forward compatibility
The API can add a field or a value after this version of the SDK is released. This version keeps working when that happens, and you can use the new part of the API before a release declares it.
- A field of a reply that this version does not declare is kept. Read it from
model_extraon the model that holds it. - An enum value this version does not list is a member of its own, whose
valueis what the API sent. - To send a field this version does not declare, pass it in
extra_bodyto a call. A model or a dictionary you pass keeps any other key you set on it, and sends it.
License
MIT. See LICENSE.md.
Metadata
Release files for elmo-sdk 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| elmo_sdk-0.1.0.tar.gz | 85.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| elmo_sdk-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 196.8 kB
Release files / elmo_sdk-0.1.0.tar.gz
| Download URL | elmo_sdk-0.1.0.tar.gz |
|---|---|
| Size | 85.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
5c17edfa4e3da7dcb2771b1dfa2b8eb917a641baa11608045703661cc337384d
|
|
BLAKE2b-256 checksum How to use checksums |
0aacaa5ab3782a2f2743d43cf7cbbf4d450fdce68934a5c5864ee24707ef281b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.24 {"installer":{"name":"uv","version":"0.12.24","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / elmo_sdk-0.1.0-py3-none-any.whl
| Download URL | elmo_sdk-0.1.0-py3-none-any.whl |
|---|---|
| Size | 111.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
cef96c4531d17d611cec2554fff9fcfab995aed85d23507af4b50350b7946c61
|
|
BLAKE2b-256 checksum How to use checksums |
a1aeb1a70e4be7b6b43e76c136ed793932e6777751a48965198acbc60096242f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.24 {"installer":{"name":"uv","version":"0.12.24","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|