Ontology SDK
Python
Read objects and apply actions from Python.
The Python client needs Python 3.9 or newer. It uses only the standard library.
Install
Put your token in ~/.netrc once, so it never appears in a URL or in your shell history:
machine cnvrg.si login __token__ password <your token>
The login is always __token__. Then install:
pip install --index-url https://cnvrg.si/api/v2/osdk/pypi/simple/ converge-osdk-<ontology>
Use --index-url, never --extra-index-url, for the Converge index. With --extra-index-url, pip also asks public PyPI for converge-sdk and converge-osdk-<ontology>, and whoever registered those names there would run code on your machine at install time. The Converge index serves everything the package needs.
To install with conda instead, see conda.
Every generated client is the package ontology_sdk. Two ontology clients in one environment overwrite each other, with pip and with conda alike. Use one virtual environment (or conda environment) per ontology.
The package holds your ontology's types and depends on converge-sdk, the open-source runtime (Apache-2.0, source in the TANTIRA/CSDK-Python repository on GitHub). The same Converge index serves converge-sdk too, so one pip install pulls both. The JSON download (GET …/osdk/python) contains the runtime files as well, so a client you copy into a project works with nothing else installed.
Without generated code
converge-sdk on its own reaches datasets, SQL and any ontology by API name:
from converge_sdk import ConvergeClient, prop
client = ConvergeClient() # CONVERGE_HOSTNAME and CONVERGE_TOKEN (or FOUNDRY_TOKEN), or ~/.converge/config
rows = client.sql.execute("SELECT count(*) AS n FROM `/Project/orders`")
o = client.ontology("<ontology>")
orders = o.objects("Order").where(prop("total") > 5).take(10)
o.action("update_tier")(customer="c1", tier="gold")
o.batch_action("update_tier")([{"customer": "c1", "tier": "gold"}, {"customer": "c2", "tier": "gold"}])
value = o.query("customer_lifetime_value")(customer_rid="customer/c1")
The untyped ontology client also has o.attachments and o.media, the same as client.ontology.attachments and client.ontology.media below. Parameters go to the server as you name them.
Create a client
import os
from ontology_sdk import FoundryClient, UserTokenAuth
client = FoundryClient("https://cnvrg.si/api", auth=UserTokenAuth(os.environ["FOUNDRY_TOKEN"]))
FoundryClient waits 60 seconds per request by default. Pass timeout= (in seconds) to change that.
FoundryClient()with no arguments reads the host fromCONVERGE_HOSTNAMEand the token fromCONVERGE_TOKENorFOUNDRY_TOKEN, then falls back tohostnameandtokenin the[default]section of~/.converge/config.- Include
/apiin the host. Withhostname="cnvrg.si"the calls reach the web app instead of the API and fail with confusing errors (HTML instead of JSON, or 405). UserTokenAuthalso takes a zero-argument function that returns the token. It runs on each request, so a refreshed token is picked up.- Without a
config=, proxies come fromHTTPS_PROXY/NO_PROXY(or the OS proxy settings), and certificates from Python's default CA store (SSL_CERT_FILEoverrides it).
The same client also reaches the platform API: client.datasets and client.sql work as on ConvergeClient, with the same host, token and settings:
client.datasets.get("ri.example.dataset.1")
rows = client.sql.execute("SELECT count(*) AS n FROM `/Project/orders`")
HTTP options
As in Palantir's foundry-platform-python, config=Config(...) sets headers, proxies, TLS verification and the timeout:
from converge_sdk import Config
client = FoundryClient("https://cnvrg.si/api", auth=UserTokenAuth(os.environ["FOUNDRY_TOKEN"]),
config=Config(default_headers={"X-Team": "ops"},
proxies={"https": os.environ["CORP_PROXY_URL"]},
verify="/etc/ssl/corp-ca.pem", timeout=30))
default_headersgo on every request, with their names lowercased.Authorizationis refused with aConfigError: the token comes fromauthonly.- So are
Host(which could route the token to another site behind a shared gateway),Proxy-Authorizationand the framing headersContent-Length,Transfer-Encoding,Connection,TEandUpgrade.
proxiesmaps a scheme to a proxy URL, as urllib'sProxyHandlertakes it.{}turns the proxy environment variables off.- An
httporallproxy must be onlocalhost, because it would see plain-http requests with the token in them. - Requests to
https://hosts go through a CONNECT tunnel, so the proxy never sees the token. The CONNECT line and anyuser:pass@in the proxy URL still travel in plaintext, even with anhttps://proxy URL.
- An
verifyisTrue(the system store), the path of a PEM CA bundle file or directory, orFalse.- A file that is not a PEM bundle raises
ConfigError. Falsechecks no certificate, so anyone on the network path could read your token. It raises anInsecureConfigWarningwhen you create theConfigand again on the first request. Use a CA bundle instead.
- A file that is not a PEM bundle raises
timeout, in seconds, overridestimeout=.
Read objects
from ontology_sdk import Customer
customers = client.ontology.objects.Customer
top = (customers
.where((Customer.object_type.revenue > 1000) & Customer.object_type.tier.in_(["gold", "platinum"]))
.order_by(Customer.object_type.revenue.desc())
.take(10))
for c in customers.iterate(page_size=100):
print(c.name)
one = customers.get("c1") # or customers.get("customer/c1")
n = customers.count().compute()
get works for keys made of letters, digits, _ and -. For any other key, filter on the key property instead. In an == filter, a value made of letters, a /, then letters, digits, _ or - (such as a/b or customer/c-1) is read as an object reference, so it matches nothing. Match such a key with a full-value regex instead: customers.where(Customer.object_type.id.regex("a/b")). A / needs no escaping. Other keys with a /, such as a1/b, 1/2 or a/b/c, work with ==.
page(page_size=…) takes 1 to 1000 and defaults to 50. A larger size, such as Palantir's 10,000, answers 422. iterate() and take() page for you.
customers.select(Customer.object_type.name) loads only those properties; the others come back None.
page, iterate and list take apply_modifiers={"<property>": "<modifier>"}, the same modifiers as the TypeScript $applyModifiers: applyReducers returns an array's reduced element, applyMainValue a struct's main value, and applyReducersAndExtractMainValue the main value of the reduced struct of a struct array. Filters and order_by still use the full value, and get has no such option. A key is a property name. A Property is unhashable, so pass it as (property, modifier) pairs: apply_modifiers=[(Site.object_type.readings, "applyReducers")]. A modifier name the SDK does not know raises ValueError. The server answers 400 when the property has no matching configuration and 403 when it is masked.
A geopoint property stored as "lat,lon" is a GeoPoint (from converge_sdk). It is that text, so it compares and serialises as before, with latitude and longitude as floats. A geopoint the server sends as GeoJSON stays as sent.
Properties support ==, <, <=, >, >=, in_, range, regex, starts_with and is_null(), and text search with contains_any_term, contains_all_terms, contains_all_terms_in_order and contains_all_terms_in_order_prefix_last_term: whole words, ignoring case, by the rules in the TypeScript page. Combine filters with & and |.
<,<=,>,>=andrangetake a number or an ISO date or timestamp ("2026-10-31","2026-10-31T09:00:00Z"). Any other string answers 422.starts_withis case-sensitive. It is a full-value regex, unlike the text-search operators, which ignore case.
Follow links
orders = client.ontology.objects.Customer.linked("c1", "orders")
first = orders.page(page_size=50)
for o in orders.iterate():
print(o.rid)
owner = client.ontology.objects.Order.linked("o1", "placedBy").get("c1")
Linked objects come back as their type's class. A type that is not in your client comes back as a plain dict. An unknown link name raises ValueError.
Aggregate
by_tier = customers.aggregate("sum", field=Customer.object_type.revenue,
group_by=[Customer.object_type.tier])
for row in by_tier["data"]:
print(row["group"], row["metrics"])
type is one of count, sum, avg, min, max, exactDistinct or approximateDistinct, and a where on the set applies. Distinct counts work on up to 10,000 visible objects (400 above that). A group_by entry can bucket a number: {"field": Customer.object_type.revenue, "fixedWidth": 500} or {"field": ..., "ranges": [[None, 1000], [1000, None]]}. regex() matches the whole value: ^ and $ are refused.
Several metrics at once, with time buckets and the largest groups only:
from converge_sdk.ontologies import duration, max_group_count, metric
customers.aggregate(
metrics=[metric("count"), metric("sum", Customer.object_type.revenue, "revenue")],
group_by=[duration(Customer.object_type.signed_up, 1, "MONTHS"), max_group_count(Customer.object_type.tier, 5)],
order=("revenue", "desc"),
)
The rules are the same as in TypeScript: UTC buckets, weeks from Monday, APPROXIMATE when max_group_count drops groups, at most 10 metrics and 10,000 groups.
Nearest neighbors
hits = client.ontology.objects.Doc.where(Doc.object_type.lang == "en").nearest_neighbors(embedding, 10, Doc.object_type.embedding)
hits["data"][0]["$score"] # cosine similarity, best first
The answer is the server's dict. skipped counts the readable objects with no comparable vector. The query must have exactly the property's vectorDimension numbers. The rules are the same as in TypeScript.
Apply an action
from ontology_sdk import ActionConfig, ActionMode, ReturnEditsMode
client.ontology.actions.update_tier(
customer="c1", tier="platinum",
action_config=ActionConfig(mode=ActionMode.VALIDATE_ONLY),
)
To get the edits back, pass ActionConfig(return_edits=ReturnEditsMode.ALL).
Leave out action_config to apply the action. A refused call raises PalantirRPCException, which holds the HTTP status and the server's reason.
The answer is a SyncApplyActionResponse, under its Python OSDK 2.x name. It is a dict, so response.validation["result"] and json.dumps(response) work, and its keys also read as snake_case attributes: response.validation.result, response.validation.submission_criteria, response.edits. A key named like a dict method (items, keys, get) reads only by index, as response["items"].
An action that needs approval is not applied yet. The server answers 202 with {"status": "pending_approval", "approval_id": "…"}. Then response.status is "pending_approval", response.approval_id names the request, and response.validation and response.edits are None, so check response.status before you read validation. Both fields are None for an applied action.
client.ontology.actions.update_tier.validate(customer="c1", tier="platinum") runs the checks only, writes nothing and returns a ValidateActionResponse (result, submission_criteria, parameters).
Batches
from ontology_sdk import BatchActionConfig, ReturnEditsMode
client.ontology.batch_actions.update_tier(
[{"customer": "c1", "tier": "gold"}, {"customer": "c2", "tier": "gold"}],
batch_action_config=BatchActionConfig(return_edits=ReturnEditsMode.ALL),
)
It returns a BatchApplyActionResponse (results, edits). A batch takes 1 to 100 parameter sets. For a plain action it is all-or-nothing. Authored actions and actions that need approval run one at a time and stop at the first failure, so the earlier ones stay applied (the error says how many).
Attachments
att = client.ontology.attachments.upload(open("invoice.pdf", "rb").read(), "invoice.pdf", "application/pdf")
client.ontology.actions.attach_invoice(order="o1", invoice=att["rid"])
data = client.ontology.attachments.read(att["rid"]) # bytes
get(rid) returns the metadata. Who can read an attachment works the same as in TypeScript.
Media
A media reference property of a generated object is a Media:
order = client.ontology.objects.Order.get("o1")
data = order.photo.get_media_content() # bytes
meta = order.photo.get_media_metadata() # .path, .size_bytes, .media_type
ref = order.photo.get_media_reference() # the MediaReference the property holds
ref = client.ontology.media.upload("Order", "photo", png_bytes, media_item_path="orders/o9.png")
client.ontology.actions.set_photo(order_id="o9", photo=ref)
client.ontology.media.get("Order", "o1", "photo") reads one without loading the object type. The server checks that you can read the object (its visibility, markings and masking) and the media set. The content is untrusted, because the uploader chose its type. Showing an image, video or audio in an <img>, <video> or <audio> element is fine; never open other content (HTML, SVG) as a blob: or data: URL in a frame, tab or link, where it would run on your app's origin.
Run a query
value = client.ontology.queries.customer_lifetime_value(customer_rid="customer/c1")
Queries run as you and are read-only. Parameters are checked like action parameters (TypeError for an unknown or missing one).
The return value follows the query's output type:
- An object comes back as its class. It is read again as you, so it is
Nonewhen you can't read it. - An object set comes back as an
ObjectSet, read as you when you iterate it. - A list or set of objects comes back as a list of the class.
- A geopoint comes back as a
GeoPoint. - Anything else, a struct (a dict) included, comes back as the server sent it.
Moving existing code
The client uses the same names as the Python OSDK 2.x: FoundryClient, UserTokenAuth, Config, ActionConfig, ActionMode, SyncApplyActionResponse, PalantirRPCException, Type.object_type.<property>, where, order_by, page, iterate, take, count().compute() and get. To move existing code:
- Change the import from your old package name to
ontology_sdk. - Point
FoundryClientathttps://cnvrg.si/api. - Fix the four places where Converge differs from the Python OSDK 2.x:
- Aggregates are one
aggregate(...)call that returns the server's dict (see Aggregate). There is no.group_by(...).count().compute(), and ranges are lists[[lo, hi], ...], not sets of tuples. - Links go through
client.ontology.objects.Customer.linked(pk, "orders"). Objects have nocustomer.orders()link methods. attachments.upload(data, filename, media_type)returns a dict: passatt["rid"]to the action, not anAttachmentobject.get,pageanditeratetake noinclude_ridorpropertieskeyword: either raisesTypeError..ridis always set, and.select(...)replacesproperties=.
- Aggregates are one
Negation works as in the OSDK: ~clause, != and is_null(False). != also matches objects with no value. On one object type, | is a union, & an intersection and a & ~b subtracts b from a.
page() returns a Page with data, next_page_token and total_count.