Skip to main content

Citta Python SDK (citta-sdk)

The Python SDK for the Citta platform. Its first layer, citta.service, is for Custom Services: HTTP backends you host (a FastAPI or Flask application, a LangGraph agent) that the platform routes to under a tenant's own domain and that read the tenant's deployed data model directly. The platform tells your service, on every request, which user and which application instance it is serving and how to reach that instance's data. This SDK turns that contract into a few typed objects and a connection pool.

from fastapi import Depends, FastAPI
from citta.service import ServiceContext
from citta.service.fastapi import citta_context, citta_service, install

app = FastAPI()
install(app)                                   # reads CITTA_SERVICE_KEY and friends from the environment

@app.get("/{session}")
def hello(request, context: ServiceContext = Depends(citta_context)):
    pool = citta_service(request).pool(context)
    with pool.connection() as conn:
        count = conn.execute("SELECT count(*) FROM my_object").fetchone()[0]
    return f"hello {context.user.user_name} from {context.tenant.domain}: {count} rows"

A complete service, ready to deploy to AWS Lambda, comes with the SDK as a starter project: citta new hello-world <directory> writes it (see Install). The guide "Hello world on AWS" (https://docs.citta.ai/developers/custom-services/hello-world-on-aws) takes it from there to a service launched from the platform.

Install

pip install 'citta-sdk[fastapi]'      # the SDK plus FastAPI and uvicorn
pip install 'citta-sdk[flask]'        # the SDK plus Flask
pip install 'citta-sdk[aws]'          # adds the Lambda adapter (Mangum)
pip install citta-sdk                 # the SDK alone (httpx, psycopg)

Python 3.10 or newer.

The package installs a citta command. citta new hello-world <directory> writes the hello world starter project into a directory; citta new langgraph-agent <directory> writes a LangGraph agent whose memory lives in the application instance's extension schema, with the same Lambda image and deploy scripts. Either goes into a directory (an existing one may already hold a virtual environment, but none of the starter's files), with requirements.txt pinning citta-sdk to the installed version:

mkdir my-service && cd my-service
python3 -m venv .venv
.venv/bin/pip install 'citta-sdk[fastapi,aws]'
.venv/bin/citta new hello-world .
.venv/bin/pip install -r requirements.txt

How a request reaches your service

A Custom Service is registered once per platform environment with a code, and each of its versions carries a label of your choosing (dev, uat, prod1.1), a routing target and a service key that is shown exactly once when the version is registered or its key rotated. An application pins one service and one version label; every tenant that deploys that application resolves the same service.

When a user opens the application's service entry, the platform redirects the browser to

https://<tenant host>/<accountCode>_<serviceCode>_<versionLabel>/<appInstanceSession>[/<subPath>]

The platform owns that first path segment, strips it and forwards the rest to your version's target. Your service receives

GET /<appInstanceSession>[/<subPath>]
Cookie: auth=...                   the user's own platform session, forwarded unchanged
X-Forwarded-Host: <tenant host>    where the platform is, for this request
X-Forwarded-Proto: https
X-Forwarded-Prefix: /<accountCode>_<serviceCode>_<versionLabel>
X-Forwarded-For: <client address>

The app-instance session is the first path segment, taken verbatim. The SDK never parses it and neither should your code: it is an opaque string the platform minted for this user and this application instance, and it is passed back to the platform exactly as received.

What the SDK does with it

On every request, CittaService.context_for(path, headers) (or the FastAPI dependency citta_context) calls the platform's context endpoint on the forwarded host with the user's cookie, the session from the path and your service key. When all three agree, the platform releases a ServiceContext:

Block Fields Meaning
user user_id, user_name, email, roles The authenticated end user. Re-validated on every request, never cached, so a revoked session fails on its next click.
tenant tenant_id, tenant_name, domain The tenant whose domain the request came in on.
app_instance application_instance_id, agi_id, application_data_type_id, agi_schema, app_schema, app_ext_schema The application instance and its three schemas: the application group instance's data, the application's own schema, and the extension schema your service owns.
org_context org_hierarchy_id The organisation hierarchy the user entered under, when the application has one.
connection host, read_host, port, database, user, password, search_path, ssl_mode A database login minted for this application instance, valid for your service alone. host is the writer, read_host the reader. The password never appears in repr.
session_settings settings (name → value) The Postgres session settings the platform sets on its own connections for this request: who is acting, which application instance, and the resolved bitemporal properties the object views and merge routines read. Records applies them with SET LOCAL around every read and write; use platform_session for your own SQL.

The user block is handed to your handler and forgotten. The connection block feeds a pool registry: the SDK keeps one psycopg pool per application instance, creates it on first sight, reuses it while the released block names the same login, replaces it when the login changes (a redeploy, say) and closes it after five idle minutes. Ask for it with service.pool(context) (a psycopg_pool.ConnectionPool, reader with read_only=True) or await service.async_pool(context) for handlers on an event loop. Every pooled connection has search_path set to the three schemas, extension schema first, so unqualified table names resolve the way the platform intends.

What the connection can reach

The login is a member of the application's data role and owns the extension schema:

  • Read the objects the application holds through its views in app_schema: the application's own objects and those linked from another application group alike. Records reads there; a bitemporal object answers through its current_version_<object> view, and rows marked deleted are not shown. The tables behind those views, in agi_schema (or the owning application group's schema, for a linked object), are readable too.
  • Write through Records (citta.service.Records(context, connection).write(object, rows)): a standard bitemporal or incremental-snapshot object is merged through its merge_app_bulk_<object> routine in app_schema, which forwards to the owning schema (an existing external_id updates, deleted=True marks rows deleted); a plain object is inserted through its view. Both run inside the platform session settings, so created_by, modified_by and the bitemporal points are what the platform would record for this user. Do not bypass the routines with raw DML on a bitemporal object: its views are not updatable and its history would be wrong. Reads of a bitemporal object go through its current_version_<object> view (Records.count, read_relation).
  • Own app_ext_schema: create your own tables there (state, embeddings, caches). Nothing else touches it, and it travels with the application instance.
  • Nothing under the platform's own metadata schemas is visible, and no other tenant's data is reachable: the login exists per application instance and is released per authenticated request.
from citta.service import Records

with citta_service(request).pool(context).connection() as conn:
    records = Records(context, conn)
    records.objects()                                        # ['events', 'greetings']
    records.count("events")                                  # rows of the current version, at the request's as-of
    records.write("greetings", [{"id": 4, "name": "d", "value": 40}])
    records.write("events", [{"external_id": "e-1", "amount": 10, "label": "first"}])

Configuration

Everything is read from the environment by CittaService.from_env(), or passed explicitly as ServiceSettings.

Variable Meaning
CITTA_SERVICE_KEY The version's service key, shown once at registration. Required unless in local mode.
CITTA_PLATFORM_URL The platform origin to call, overriding the request's X-Forwarded-* headers. Set it when your service is called directly (a notebook, curl) rather than through the platform.
CITTA_LOCAL_CONTEXT Path to a JSON context block. When set, no platform call is made (see local mode below).
CITTA_CA_BUNDLE A CA file to verify the platform's certificate against, for a platform whose certificate a private CA signed.
CITTA_TLS_INSECURE 1 disables certificate verification and logs a warning on every client built. Development only.
CITTA_POOL_IDLE_SECONDS How long an application instance's pool survives without a request. Default 300.
CITTA_POOL_MIN_SIZE, CITTA_POOL_MAX_SIZE Pool bounds. Defaults 1 and 4.
CITTA_PLATFORM_TIMEOUT Seconds to wait for the platform. Default 30.

The service fails at startup, naming the setting, when the key is missing outside local mode or when local mode and a platform URL are both set.

Errors, and what the browser sees

Every failure is a CittaError carrying the platform's message verbatim. The FastAPI integration maps them so that the user and the operator each learn what concerns them:

Raised Meaning HTTP answer
CittaSessionError The cookie is missing or expired, or the user lost access. 401
CittaServiceConfigError (with a kind) The platform refused because of the service's or the application's setup: the key authenticates no active version, the key is for another version or service, the application was deployed before it pinned the service, or the application pins no service. 503
RequestShapeError The request is not shaped as the platform sends it (no session in the path). 404
any other CittaError, or an unhandled exception The platform could not be reached, answered something unexpected, or the handler failed. 522

The body is {"error": "<message>", "status": <code>} plus "kind" on a 503. Register your own exception handler for a class to change its mapping.

Three ways to develop

In a notebook or a workspace inside the platform's network. Run your service there, register a version whose label is yours (dev), and drive it in two ways: through the browser, by opening the application's service entry on that label, or directly with curl, supplying a session string and cookie copied from the browser. For the direct route set CITTA_PLATFORM_URL to the environment's tenant URL, since no forwarded headers arrive.

To copy the cookie: open the application in the browser, open the developer tools, Network tab, select any request to the platform, and copy the auth= value from the request's Cookie header and the app-instance session from a request path (/datatypes/<session>/...). Then

curl -H 'Cookie: auth=<value>' http://localhost:8000/<session>

The cookie is the user's session: treat it as a password and let it expire.

Behind the real redirect. Deploy the version wherever its target points and open the application's service entry. Everything the platform sends is real, including a different tenant domain per tenant.

On a laptop without a platform: local mode. Put a context block in a file (the hello world's context.local.example.json shows its shape), point its connection at a local Postgres holding the application's schema bundle, and set CITTA_LOCAL_CONTEXT to the file. Every request is served as the user in the file; no key, cookie or platform is involved, and the SDK logs one warning at startup so the mode cannot go unnoticed. The same service code runs unchanged in all three.

The schema bundle's SQL files are numbered in load order (the hello world's deploy/load-bundle.sh loads them). When the application links objects from another application group, the bundle also creates that group's schema with what the application reaches, under its real name, so reads and writes of linked objects behave as on the platform. Its tables arrive empty: the owner's rows, its seed rows included, are not in the bundle, so write the rows you need through your service.

Running in AWS Lambda

In AWS the platform maps the reserved path segment onto the tenant domain as an API Gateway base path mapping, and the request reaches your Lambda as a REST proxy event with the segment already stripped. API Gateway does not set X-Forwarded-Host or X-Forwarded-Prefix; citta.service.aws restores them from the event before the SDK reads the request, so the same service code runs locally and in Lambda:

# lambda_handler.py
from citta.service.aws import CittaLambdaHandler
from app import app          # your FastAPI (or any ASGI) application

handler = CittaLambdaHandler(app)

Install citta-sdk[aws,fastapi] for the Lambda's runtime, set CITTA_SERVICE_KEY in the function's environment, and point the REST API's ANY /{proxy+} and ANY / at the function with a Lambda proxy integration. Requests that hit the function on its own execute-api host did not come through the platform; there the SDK asks for CITTA_PLATFORM_URL, as it does for any direct call.

Flask

from flask import Flask
from citta.service.flask import citta_context, install

app = Flask(__name__)
install(app)

@app.get("/<session>")
@citta_context
def hello(session, context):
    return f"hello {context.user.user_name}"

Same error mapping as FastAPI; the view receives the context as the context keyword argument. Install citta-sdk[flask].

Layout

citta/core        transport shared by every layer: CittaClient / AsyncCittaClient, the platform URL shape,
                  the errors-array convention, TLS settings, token-envelope refresh for scripts
citta/service     Custom Services: ServiceContext, the context providers, pool registries, settings,
                  the FastAPI and Flask integrations, the AWS Lambda adapter
citta/templates   the starter projects `citta new` writes: hello_world, langgraph_agent, and _aws_lambda, the
                  Lambda image and deploy scripts both share
examples/         conformance, the platform's acceptance subject, in this repository only
tests/            pytest, with the platform mocked

Release notes

Unreleased

  • New citta command: citta new hello-world <directory> writes the hello world starter project, which until now came only in the SDK zip, and citta new langgraph-agent <directory> the LangGraph agent example as a deployable project. The zip is no longer produced: the SDK comes from PyPI and the starter from citta new.
  • The hello world installs the SDK from PyPI: requirements.txt pins citta-sdk[fastapi,aws]==<version>, and both the local environment and the container image install from it. The project no longer has a wheels/ directory.

7.0.0

First release on PyPI, as 7.0.0.<build> (build 8085). Carries the 1.1.1 changes:

  • Records reads and writes through the application's schema (app_schema) instead of the application group instance's schema, so objects linked from another application group are read and written like the application's own. Merge-routine objects are written through merge_app_bulk_<object>; plain objects are inserted through their view.
  • Records.objects() and tables() list only the objects the application holds, not every object of its application group.
  • Rows marked deleted are no longer read: the application's views show only rows that are not deleted.
  • A write to an object with neither a merge routine nor an insertable view raises NotWritableError, naming the object and the schema.
  • New: Records.schema and Records.merge_routine(table).
  • The hello world's deploy/load-bundle.sh loads whichever numbered SQL files the schema bundle holds, in order.

This package succeeds the earlier asw_sdk distribution, which remains available for existing pipeline steps and is not extended.

Metadata

Release files for citta-sdk 7.0.0.8086

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for citta-sdk 7.0.0.8086
File Size Uploaded
citta_sdk-7.0.0.8086.tar.gz 83.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for citta-sdk 7.0.0.8086
File Interpreter ABI Platform
citta_sdk-7.0.0.8086-py3-none-any.whl Python 3 none any Details

Total release size: 159.4 kB

Release files / citta_sdk-7.0.0.8086.tar.gz

Download URL citta_sdk-7.0.0.8086.tar.gz
Size 83.2 kB
Tags Source
SHA-256 checksum
How to use checksums
09d91114ce4af50a81da44e7b768c052b7197d1f16b4e3d3e7f96b842003eaf8
BLAKE2b-256 checksum
How to use checksums
46f0a5837d50399b9ed0ea9597c559393523dbb6ae84929c6f502b014247ee23
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.15

Release files / citta_sdk-7.0.0.8086-py3-none-any.whl

Download URL citta_sdk-7.0.0.8086-py3-none-any.whl
Size 76.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
e70acd783e5a7ded6852fa52d442ce6fc247bad8f4bba2f5d2f50527247c3636
BLAKE2b-256 checksum
How to use checksums
70977da37b2faead7db2b9a1362fe3112f1db7c81d1425271ef063c786cf36e2
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.15

Release history Release notifications | RSS feed

This release

7.0.0.8086 This release

2 release 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