pip install -U causilo-client scikit-learn requests
python usage_limits_and_cancel.py
import os
import time
import numpy as np
import pandas as pd
import requests
from sklearn.datasets import load_diabetes
from causilo_client import (
Causilo,
CausiloError,
CausiloQuotaExceeded,
CausiloRateLimited,
CausiloTimeout,
)
ENDPOINT = os.environ.get("CAUSILO_ENDPOINT", "https://api.nums.world")
cx = Causilo(ENDPOINT, token=os.environ["CAUSILO_KEY"])
Limits, before you send anything
GET /v1/limits needs no key. The numbers it returns are part of the API contract, so a client can check a table locally before uploading it.
limits = requests.get(f"{ENDPOINT}/v1/limits", timeout=30).json()
print("limits:", limits)
def fits(context: pd.DataFrame, query: pd.DataFrame) -> str:
cells = (len(context) + len(query)) * (context.shape[1] - 1)
if len(context) + len(query) > limits["max_rows"]:
return "too many rows"
if context.shape[1] > limits["max_cols"]:
return "too many columns"
if cells > limits["max_cells"]:
return "too many cells"
return f"fits; {cells:,} cells would be charged"
big = pd.DataFrame(np.zeros((3_000_000, 5)), columns=list("abcde"))
print("3,000,000 x 5 table:", fits(big, big.iloc[:10]))
What a call costs
A call uses one cell for each value in the tables you send, counting context and query rows together, so a 1,000 by 21 context with a 200 by 20 query uses 24,000 cells. Your account has a monthly and a daily allowance, and usage() reads both without using anything.
u = cx.usage()
print(pd.DataFrame(u["scopes"])[["scope", "used", "resets_at"]].to_string(index=False))
When an allowance is used up the server answers 429 and the client raises CausiloQuotaExceeded. The exception says which allowance it was, how much was used and when it resets, so the calling code can wait for the reset instead of retrying immediately. Too many requests in a minute or an hour produce the same status code with a different exception, CausiloRateLimited, which the client raises only after it has waited out Retry-After three times.
def predict_or_wait(context, query, **kw):
try:
return cx.predict(context, query, **kw)
except CausiloQuotaExceeded as exc:
print(f"{exc.scope} cap of {exc.limit:,} cells spent ({exc.used:,} used); resets at {exc.resets_at}")
raise
except CausiloRateLimited as exc:
print(f"rate limited on the {exc.scope} window; resets at {exc.resets_at}")
raise
Input the server refuses
Tables travel as Parquet. Datetime, timedelta, Period and decimal columns are refused with a message that names the column, because converting them silently would change what the model sees. Split dates into the parts that carry meaning for your problem.
d = load_diabetes(as_frame=True)
context = d.data.assign(progression=d.target)
query = d.data.iloc[:20]
visit = pd.date_range("2024-01-01", periods=len(context), freq="D")
try:
cx.predict(context.assign(visit=visit), query.assign(visit=pd.Timestamp("2024-01-01")),
target="progression", model="causilo-reg")
except CausiloError as exc:
print("refused:", type(exc).__name__, "-", exc)
context2 = context.assign(visit_month=visit.month, visit_dow=visit.dayofweek)
query2 = query.assign(visit_month=1, visit_dow=0)
pred = cx.predict(context2, query2, target="progression", model="causilo-reg")
print("with the date split into parts:", len(pred), "predictions")
Giving up on a call
The client waits 900 seconds by default. If you set a shorter timeout and it expires, the client raises CausiloTimeout and does not retry, because the server is still computing and a retry would be counted a second time.
Giving up does not return the cells. The prediction runs behind an asynchronous endpoint that never learns that the caller disconnected, so it runs to the end and its cells are counted. The client asks the server to cancel, the server answers 501, and CausiloTimeout.refunded comes back None to show that the usage stands. Choose a timeout long enough that you will read the answer.
The table below is small to send and slow to compute: 60,000 context rows take the server well over five seconds, and this client gives up after five.
rng = np.random.default_rng(0)
context = pd.DataFrame(rng.normal(size=(60_000, 3)), columns=["f0", "f1", "f2"])
context["y"] = (context["f0"] + context["f1"] > 0).astype(int)
query = context.drop(columns="y").iloc[:100]
before = cx.usage()["used"]
impatient = Causilo(ENDPOINT, token=os.environ["CAUSILO_KEY"], timeout=5)
try:
impatient.predict(context, query, target="y", model="causilo-clf")
except CausiloTimeout as exc:
print("timed out; cells refunded:", exc.refunded)
The script waits a minute before reading /v1/usage, so that the prediction it abandoned has certainly finished.
time.sleep(60)
print("cells charged for the answer nobody read:", cx.usage()["used"] - before)
A request that fails is a different case and is not counted. A 413, a 422, a response too large to return and an inference that errors all release the cells before the reply goes out. A call you abandon is counted in full because the server never hears that you left, and the reference lists the server-side failures that are counted as well.
The exceptions the client raises:
| Exception | Status | What to do |
|---|---|---|
CausiloValidationError |
400, 422 | Fix the request. The message names the column or the parameter. |
CausiloTooLarge |
413 | The message names a row count that fits. Nothing was counted. |
CausiloQuotaExceeded |
429 | Wait for resets_at, or ask for a higher cap. |
CausiloRateLimited |
429 | The client already waited. Slow the loop down. |
CausiloOverloaded |
429 | Retry once the queue drains. Not the quota 429: this one carries no code. |
CausiloUnavailable |
503 | The client already retried three times. |
CausiloAuthError |
401 | Check the key. |
CausiloTimeout |
504 or none | Not retried. The work continues and is counted; send a smaller table. |
Every one carries .status, .error_code and .request_id, the identifier
to quote when asking us about a call.
Output
stdout
limits: {'models': ['causilo-clf', 'causilo-reg'], 'output_types': {'classification': ['probas', 'preds'], 'regression': ['mean', 'median', 'quantiles']}, 'max_request_bytes': 1073741824, 'max_response_bytes': 536870912, 'max_rows': 300000, 'max_cols': 4096, 'max_features': 4095, 'max_cells': 60000000, 'max_classes': 64, 'max_inference_s': 300.0, 'context_max_age_s': 21600.0, 'key_cache_ttl_s': 60.0}
3,000,000 x 5 table: too many rows
scope used resets_at
caller 48840 2026-10-01T00:00:00+00:00
daily 48840 2026-09-30T00:00:00+00:00
refused: CausiloValidationError - Datetime-like columns are not accepted: visit. Preprocessing drops them without warning. Convert to a number (epoch seconds) or a string. (request 20b4cd83f1924694)
with the date split into parts: 20 predictions
timed out; cells refunded: None
cells charged for the answer nobody read: 180300