Skip to main content

Agimus Python SDK

The Python client for the Agimus Platform: the objects of your ontology, read and written through one fluent builder over object sets, in sync and async flavours.

Installation

pip install agimus

Requirements: Python 3.10+. pandas and polars are optional (to_pandas(), to_polars()).

Quick start

from agimus import AgimusClient

client = AgimusClient()  # reads AGIMUS_API_KEY and AGIMUS_BASE_URL

customers = client.objects("Customer")

# Objects in region EU with a rejected approval
rejected = customers.filter(region="EU").has("approvals", state="rejected")
rejected.count()
rejected.sort("-createdAt").all()

# The approvals of those customers, every row, as a DataFrame
df = rejected.pivot("approvals").to_pandas()

# One object, a write
customers.get("C1")
customers.create({"id": "C9", "name": "Acme"})

The ontology

Every element has a stable identifier:

  • Entities (Customer, Order): apiName, PascalCase.
  • Properties (customerId, createdAt): apiName, camelCase.
  • Links: relationships between entities, with a forward and a reverse apiName.
  • Categories: flat tags grouping entities (finance, hr); categoryId, snake_case.

Discover them with client.list_entities(), client.get_entity_schema("Customer"), client.list_links() and client.list_categories().

Authentication and configuration

API keys are created in the Agimus dashboard under Settings > API Access (agm_...) and inherit the permissions of their service user.

from agimus import AgimusClient, RetryPolicy

client = AgimusClient(
    api_key="agm_...",                  # or AGIMUS_API_KEY
    base_url="https://api.agimus.ai",   # or AGIMUS_BASE_URL; this is the default
    timeout=60.0,                       # seconds per request
    retry=RetryPolicy(max_attempts=4, budget=60.0),  # None disables retries
)

Use with AgimusClient() as client: to close the connection pool.

Object sets

client.objects("Customer") is the set of every customer. Each step returns a new set, so a builder can be forked; a terminal sends it to the platform, which computes everything server-side.

Steps

filter(*clauses, **kwargs) keeps the objects matching every clause. Keyword arguments are field=value (equality) or field__op=value:

customers.filter(status="active", amount__gte=100, region__in=["EU", "US"])
customers.filter(name__starts_with="A", email__is_null=True)
Operator Meaning
eq, ne equal, not equal (on an array field: the whole array, element by element)
gt, gte, lt, lte comparisons
between [min, max]
like, ilike SQL pattern, ilike case-insensitive
starts_with, ends_with prefix, suffix
in, nin in a list, not in a list (on an array field: a list of arrays)
is_null, is_not_null no value (email__is_null=True is accepted)
is_empty, is_not_empty arrays and strings; no value
contains an array element, or a JSON subset
overlaps arrays sharing an element

Values follow the property's type: numbers for numeric fields, datetime, date, time, Decimal and bytes are encoded for you. Combine clauses with the F helpers when and is not enough:

from agimus import F

customers.filter(F.or_(F.where(region="EU"), F.where(amount__gte=1000)), status="active")
customers.filter(F.not_(F.where(state="rejected")))

has(link, *clauses, **kwargs) keeps the objects with a related object matching the clauses (any related object when none are given); has_none(link, ...) keeps those with none. These are link predicates, evaluated by the platform as one correlated query, and they nest:

customers.has("approvals", state="rejected")
customers.has_none("orders")
customers.has("orders", F.any("items", sku="X1"))

pivot(link) replaces the set by the related objects; the set's type becomes the link's target:

customers.filter(region="EU").pivot("approvals").filter(state="rejected").all()

with_count(link, ...), with_count_distinct(link, field, ...), with_sum, with_avg, with_min, with_max(link, field, ...) and with_value(link, field) compute one value per object over its related objects, optionally over a filtered subset (sum and avg take numeric fields, min and max orderable scalars: never an array, a boolean, JSON or bytes). The output is named <link>_<op> unless alias= says otherwise, and can be used in later filters, sorts, groups, metrics and projections:

customers.with_count("approvals", alias="rejections", state="rejected") \
         .with_sum("orders", "total", alias="revenue") \
         .filter(rejections__gte=2).sort("-revenue").fields("id", "revenue").all()

top(by, order, n=1) keeps the first n objects of every partition of by under order, so "each customer's latest approval" is a pivot and a top:

customers.pivot("approvals").top(by=["customerId"], order=["-approvedAt"]).filter(state="rejected")

Chains are bounded to three hops (pivots, link predicates, computed values and expands together).

Shape

sort("-createdAt", "name"), fields("id", "name"), expand("owner", "orders.items") (related objects embedded in each page row, up to three hops), page_size(n) (1 to 10,000, default 1,000), limit(n) (stop all() and iter() after n rows).

The plane

Every entity with a backing dataset is served from the warehouse; an entity with the operational tier is also served from Postgres (an entity created without a dataset lives on Postgres only). By default the platform chooses: Postgres for a chain every entity of which is operational and the request is light, the warehouse engine otherwise. plane() makes the choice yours, on every terminal, Arrow included:

customers.plane("operational").filter(id__in=ids).all()   # Postgres, milliseconds, indexes
customers.plane("analytical").has("orders").to_pandas()   # the engine at the applied snapshot, any size

A hint is a contract, never a silent fallback: operational on a chain with an entity that has no operational tier, or analytical on an entity whose warehouse twin is not servable yet, is a ValidationError (object_store.query.cross_plane_unavailable) answered before any query runs, naming the entity and the reason. The data is the same on both planes: edits are visible immediately on either, and the analytical read of an operational entity is pinned to the snapshot Postgres has applied. A page walk keeps the plane it started on.

Terminals

Call Returns
all() every object as a list (Rows, a list with refreshed), page by page
iter(), for row in set the objects one by one; the iterator's refreshed turns True if the walk was re-pinned
page(cursor=None) Page(data, cursor, has_more, refreshed); pass cursor for the next page
first() the first object or None
exists() whether the set has an object (one row, never a count)
count() the exact number of objects
distinct(field, with_counts=False, limit=1000) DistinctResult(values, truncated), most common first; {"value", "count"} rows when counted; None is a value like any other
aggregate(metrics, group_by=(), having=None, sort=(), limit=1000) AggregateResult(data, truncated), one row per group
to_arrow() every object as one pyarrow.Table
to_pandas(), to_polars() the same table as a DataFrame

Cursors are opaque and signed; a page walk is pinned to the snapshot it started on, and refreshed says the platform re-pinned it after that snapshot became unreadable (on page(), on the iterator, on all()'s list).

Bulk: Arrow

to_arrow() delivers the objects of a set as one Arrow table over HTTPS: every row, api names, the platform's types, ordered only under sort(), inline below the platform's Arrow cap (64 MB; a QuotaError above it). It is the way to move data out of the platform; a page walk over millions of rows is not. There is no dataset surface: every read is an entity of the ontology.

table = customers.has("approvals", state="rejected").fields("id", "region").to_arrow()
df = customers.pivot("orders").sort("-createdAt").to_pandas()

expand is not available on Arrow: pivot to the related objects instead.

Aggregation

from agimus import F, G, M

approvals = client.objects("Approval")
result = approvals.aggregate(
    metrics=[M.sum("amount"), M.count(alias="n"), M.max("approvedAt", alias="last"),
             M.percentile("amount", 0.5, alias="p50")],
    group_by=["state", G.grain("approvedAt", "month"), G.ranges("amount", [20, 100])],
    having=F.having(n__gte=3),
    sort=["-sum_amount"],
    limit=100,
)
for row in result:
    print(row["state"], row["approvedAt_month"], row["sum_amount"])

Metrics: count (a plain count, or of a field's non-null values), count_distinct, sum, avg, min, max, first, last, percentile(field, quantile). The default alias is op_field (sum_amount) or op. Groups: a field as is, G.grain(field, granularity) for year, quarter, month, week, day or hour (the key is field_granularity, a UTC timestamp, midnight for a date field), G.ranges(field, edges) for numeric buckets (the key is field_range with labels <20, [20,100), >=100). first, last, sum, avg, min and max take scalar fields, never arrays. having is one clause on a metric alias or a field grouped as is (not a grain or range key), on eq, ne, gt, gte, lt or lte. Aggregations are exact on every engine and return at most 10,000 groups.

One object

customers.get("C1", fields=["id", "name"], expand=["owner"])
customers.get_or_none("C1")            # None only when the object does not exist
customers.batch_get(["C1", "C2"])      # up to 1,000 keys; the objects found, ordered by primary key
customers.related("C1", "approvals")   # a set: the approvals of C1, on the builder's plane and page size
customers.related("C1", "approvals").count()

Keys may be strings or integers; a key containing / is fine.

Writes

Writes need the operational tier; a write on an entity without it is refused typed (object_store.entity.not_operational).

customers.create({"id": "C9", "name": "Acme", "since": datetime.date(2026, 1, 2)})
customers.update("C9", {"name": "Acme Ltd"})
customers.upsert("C9", {"name": "Acme Ltd"})  # creates (every required field) or updates (editable fields only)
customers.delete("C9")
customers.batch([
    {"op": "create", "data": {"id": "C10", "name": "Beta"}},
    {"op": "update", "pk": "C9", "data": {"name": "Acme"}},
    {"op": "delete", "pk": "C8"},
])  # up to 100 operations; each succeeds or fails on its own, the result names every outcome

A write response is the row as the platform now serves it, typed like a read and limited to the properties your key can read.

Types

The SDK reads each entity's schema once (cached for five minutes) and decodes every value to its Python type, on pages, point reads, computed values, aggregates and expanded objects alike. The same types come back from to_arrow().

Ontology type Python Arrow
string str string
integer, long, short, byte int int64 (int16 for short and byte)
float, double float float64
decimal Decimal decimal128(p, s)
boolean bool bool
date datetime.date date32
timestamp datetime.datetime, UTC-aware, or naive when the source column was naive timestamp[us, UTC] or timestamp[us]
time datetime.time time64[us]
bytes bytes binary
struct, geopoint, geoshape dict (or list) one canonical JSON text
attachment, media_reference str string
arrays list of the element type list<...>

Two differences between the JSON page and the Arrow table: JSON-family values are objects on a page and canonical JSON text on Arrow; a decimal stored with more scale than its column declares is quantized on Arrow and served as stored on a page. Computed values follow the platform's rule: counts are int, avg is float, sum is the field's type widened (a decimal sum stays Decimal), min, max and value the field's own type. Point reads also carry the system columns (_created_at and the like) as text.

Errors and retries

Every failure is one AgimusError with the platform's envelope: code, message, category, status, request_id, details, and retry_after (seconds) when the platform said when to try again.

from agimus import (
    AgimusError, AuthenticationError, AccessDeniedError, NotFoundError, ValidationError,
    ConflictError, QuotaError, RateLimitError, ServerError, ConnectionFailed, RequestTimeout,
)

try:
    customers.get("C1")
except NotFoundError as e:
    print(e.code)          # object_store.object.not_found
except ConflictError as e:
    print(e.retry_after)   # the store is resizing or not ready yet; seconds to wait

The client retries on its own inside RetryPolicy(max_attempts=4, budget=60.0): a refusal carrying Retry-After (the rate limit, a busy executor or Arrow slot, a resizing or not-yet-ready store) is resent after exactly that wait when it fits the budget and raised at once otherwise, since such a refusal is answered before anything is applied; a connection failure is retried with backoff for every method (nothing was sent); a timeout only for reads, because a write may have applied. retry=None disables all of it.

Async

from agimus import AsyncAgimusClient

async with AsyncAgimusClient() as client:
    rejected = client.objects("Customer").has("approvals", state="rejected")
    print(await rejected.count())
    async for customer in rejected.sort("id"):
        ...
    table = await rejected.to_arrow()

The same builder, every terminal awaited.

Schema discovery

client.list_entities(categories=["finance"])
client.get_entity_schema("Customer")     # properties (types, declarations, PK), links, categories
client.get_properties("Customer")
client.get_property("Customer", "name")
client.get_primary_key("Customer")
client.get_links("Customer")
client.list_links()
client.get_link_schema("approvals")
client.list_categories()
client.get_category("finance")
client.me()                              # the key's tenant, scope and rate limit

License

MIT

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

agimus-1.0.1.tar.gz (30.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

agimus-1.0.1-py3-none-any.whl (30.1 kB view details)

Uploaded Python 3

File details

Details for the file agimus-1.0.1.tar.gz.

File metadata

  • Download URL: agimus-1.0.1.tar.gz
  • Upload date:
  • Size: 30.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.12.8

File hashes

Hashes for agimus-1.0.1.tar.gz
Algorithm Hash digest
SHA256 332324a830ccf4f75c0ca1f6316d6cabb66d9b57af2900e106aaff1a23290986
MD5 84dca7b87b82c9e2258491ff9835233f
BLAKE2b-256 22b9dca54f7ac734af26109968df60ca0c3d740c44b76793d930b48e507dbd73

See more details on using hashes here.

File details

Details for the file agimus-1.0.1-py3-none-any.whl.

File metadata

  • Download URL: agimus-1.0.1-py3-none-any.whl
  • Upload date:
  • Size: 30.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.12.8

File hashes

Hashes for agimus-1.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 537bf35e6547d58c6db6487ae7fd0ceda3aa44050eb5cb8d692d91c261258611
MD5 78ad6cc28e61322c9a49475749bb3621
BLAKE2b-256 09ba8e1c0648f1947074955adcee259a4535d2cadf9eaf13d3f3457a07704770

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

1.0.1 This release

2 files

1.0.0

2 files

0.5.1

2 files

0.5.0

2 files

0.4.0

2 files

0.3.0

2 files

0.2.0

2 files

0.1.2

2 files

0.1.1

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page