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
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
is_null, is_not_null pass True
is_empty, is_not_empty arrays and strings; pass True
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. 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, page by page
iter(), for row in set the objects one by one
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.

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). having is one clause on a metric alias or a group column, 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
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"})
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; the result names each outcome

A write response is the row as the platform now serves it, typed like a 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.0.tar.gz (29.2 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.0-py3-none-any.whl (28.8 kB view details)

Uploaded Python 3

File details

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

File metadata

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

File hashes

Hashes for agimus-1.0.0.tar.gz
Algorithm Hash digest
SHA256 485bbfbb40377f7a31a1bc5ae4c3aba9df1f320542515fae1572458932c2d2c0
MD5 d0ff9d6da6450166b9fc70d142c40338
BLAKE2b-256 64f7ac5559ce752b938278c794cc304275e671224163c4cd834aa5b55c870dfc

See more details on using hashes here.

File details

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

File metadata

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

File hashes

Hashes for agimus-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 9a8852145849a5fa76ef6a0477233ac91c6698e8c1166c7b86ac7095778d8896
MD5 0af8b15e128d617a5b91bca27e2f415b
BLAKE2b-256 a71a353363b07d3df03752f0f7d72d864af0b7d7880c47637290ec98e31bf695

See more details on using hashes here.

Release history Release notifications | RSS feed

1.0.1

2 files

This release

1.0.0 This release

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