Kaiten Python SDK
The official Python client for Kaiten, the control plane of your SaaS:
customers, instances, licenses, entitlements, usage metering, feature flags and releases.
from kaitencloud import KaitenClient
client = KaitenClient(token="ksh_...")
for customer in client.customers.list():
print(customer.name)
client.instances.report_usage("acme-production", "seats", 1)
Why this SDK
- Typed end to end. Every response is a pydantic model generated from Kaiten's OpenAPI
contract; the package ships
py.typedand passesmypy --strict. - Sync and async, identical.
KaitenClientandAsyncKaitenClientexpose the same methods with the same signatures — the sync client is generated from the async one. - Complete lists.
list()walks every page for you, and refuses to hand back a list the API said was incomplete. - Retries that never double-bill. Only requests that are safe to repeat are retried. A usage report is never sent twice.
- Errors you can act on. Every failure is a typed exception carrying the API's RFC 9457
problem:
code,detail,error_id. - Both APIs. The Core API your product talks to, and the Platform API that provisions organizations and mints their tokens.
- Webhooks included. Svix signatures verified, all 55 event types parsed into models.
- Flags through OpenFeature. Kaiten speaks OFREP, so flags are defined here and evaluated with the OpenFeature SDK — a vendor-neutral API, not ours.
- Contract-tested. All 108 operations the SDK wraps are exercised against the contract, and every request body is validated against the API's own schema.
Contents
- Installation
- Quick start
- Configuration
- Guide
- The playbook
- Working with the SDK: pagination · errors · retries and timeouts · models · logging
- API reference
- How the SDK stays true to the API
- Development
Installation
pip install kaitencloud
or, with uv:
uv add kaitencloud
The SDK supports Python 3.10 to 3.14, and depends only on httpx, pydantic v2 and
typing-extensions.
Quick start
Create an organization token (ksh_...) from a service account in the Kaiten console, then
point the client at your deployment:
from kaitencloud import KaitenClient
client = KaitenClient(
base_url="https://kaiten.example.com", # or set KAITEN_BASE_URL
token="ksh_...", # or set KAITEN_AUTH_TOKEN
)
# Every customer, however many pages it takes.
for customer in client.customers.list():
print(customer.slug, customer.name)
# What an instance consumes, next to what its license grants.
for meter in client.instances.list_usage("acme-production"):
granted = meter.limit.value if meter.limit else "not granted"
print(f"{meter.entitlement_slug}: {meter.value.value} of {granted}")
With KAITEN_BASE_URL and KAITEN_AUTH_TOKEN set, a client needs no arguments, and the rest
of this README assumes they are. The async client is the same API, awaited:
import asyncio
from kaitencloud import AsyncKaitenClient
async def main() -> None:
async with AsyncKaitenClient() as client:
customers, instances = await asyncio.gather(
client.customers.list(),
client.instances.list(),
)
print(f"{len(customers)} customers, {len(instances)} instances")
asyncio.run(main())
The model in one picture
Organization you: a SaaS vendor using Kaiten
└── Customer client.customers a client of yours
└── Instance client.instances a deployment of your product for that customer
├── License client.licenses the contract it runs under, granting values
│ client.entitlements ...of the entitlements in your catalog
├── Usage client.instances.report_usage() what it consumed
└── Zone client.deployment_zones where it runs, and the release deployed there
client.releases → client.components
Configuration
| Argument | Environment variable | Default | |
|---|---|---|---|
token |
KAITEN_AUTH_TOKEN |
required | A string, or a callable returning one before each request |
base_url |
KAITEN_BASE_URL |
required | Your deployment's address; /api is appended when missing |
timeout |
30 s (10 s to connect) | Seconds, or an httpx.Timeout |
|
max_retries |
2 |
Retries of a failed request that is safe to repeat | |
default_headers |
Headers sent with every request | ||
http_client |
Your own httpx.Client / httpx.AsyncClient: proxies, TLS, transports |
Both clients are context managers, and closing one closes the connection pool it created — never one you passed in.
Credentials
Kaiten has several kinds of credential, and each API refuses the others with a deliberately
vague error. The SDK recognizes them and raises a clear CredentialError before sending
anything when one ends up in the wrong place.
| Credential | Looks like | Use it with |
|---|---|---|
| Organization token | ksh_... |
KaitenClient |
| Identity-provider JWT | eyJ... |
KaitenClient |
| Platform credential | ksm_... |
KaitenPlatformClient |
| Webhook signing secret | whsec_... |
kaitencloud.webhooks |
A credential that rotates can be supplied as a callable — it is read before every request, so there is no client to rebuild:
client = KaitenClient(token=lambda: vault.read("kaiten/token"))
Guide
Catalog and licenses
An entitlement says what can be granted and how its usage is measured. A license grants
values of entitlements. A numeric grant carries its own enforcement: overage_percent=0 is a
hard limit, 10 tolerates 10% over the granted value before usage reports are refused.
from kaitencloud import UNLIMITED, KaitenClient
client = KaitenClient()
client.entitlements.create(
name="Seats",
slug="seats",
type="NUMBER",
unit_singular="seat",
unit_plural="seats",
user_facing=True,
)
client.entitlements.create(name="Single sign-on", slug="sso", type="BOOLEAN")
client.entitlements.create(name="API calls", slug="api-calls", type="NUMBER", reset_period="MONTH")
client.licenses.create(name="Growth", slug="growth", type="PAID")
client.licenses.associate_entitlement("growth", "seats", 50, overage_percent=10)
client.licenses.associate_entitlement("growth", "sso", True)
client.licenses.associate_entitlement("growth", "api-calls", UNLIMITED)
A granted value is a bool for a BOOLEAN entitlement, a number for a NUMBER one, and a dict
for a CONFIG one.
A license is one version of a product, its license family. Creating a license without
naming a family opens a new one: growth above is version 1 of the family growth. To change
what a plan grants without touching the customers already on it, add a version to the family,
build it as a draft, and put it on sale when it is ready:
client.licenses.create(name="Growth", type="PAID", family_slug="growth", lifecycle_state="DRAFT")
client.licenses.associate_entitlement("growth-v2", "seats", 100, overage_percent=10)
client.licenses.publish("growth-v2")
family = client.license_families.get("growth") # now resolves to growth-v2
A new version's slug is <family>-v<version>, and its number is assigned by the server. A
family resolves to its default version if it has one, otherwise to its highest-numbered
published version — so store the family slug, which survives renames and new versions, rather
than a version's. license_families.get("growth", version=1) pins a version whatever its state,
and include_versions=True adds the whole history. licenses.archive() withdraws a version
from sale: the instances already on it keep it, and no new one can take it until
licenses.unarchive().
Customers and instances
from datetime import datetime, timezone
acme = client.customers.create(name="Acme", slug="acme", domain="acme.com")
growth = client.licenses.get("growth")
client.instances.create(
name="Acme production",
slug="acme-production",
customer_id=acme.id,
license_id=growth.id,
start_license_date=datetime(2026, 9, 1, tzinfo=timezone.utc),
end_license_date=datetime(2027, 9, 1, tzinfo=timezone.utc),
metadata={"region": "eu-west-1"},
)
client.instances.update_status("acme-production", "DEGRADED")
client.instances.update_lifecycle_stage("acme-production", "AT_RISK")
Note —
update()methods are full replacements, as the API'sPUTis: send every field you want to keep. Datetimes must be timezone-aware: the SDK refuses to guess the zone of a license term.
Usage metering
from kaitencloud import ThresholdExceededError
try:
client.instances.report_usage("acme-production", "seats", 1)
except ThresholdExceededError as error:
# Refused at the cap: the granted value plus its overage allowance. Nothing was recorded.
print("Time to upgrade:", error.detail)
behavior="append" (the default) folds the value into the total through the entitlement's
aggregation method; behavior="set" replaces the total, which is also how to correct it.
kaitencloud.usage computes the same boundaries the server enforces, to render a meter or check
a report before sending it:
from kaitencloud import usage
grant = client.licenses.get_entitlement("growth", "seats")
current = client.instances.get_usage("acme-production", "seats")
included, used = grant.value.value, current.value.value
usage.maximum_allowed_usage(included, grant.limit_cap_exceeded_overage_percent) # 55.0
usage.remaining(included, used, grant.limit_cap_exceeded_overage_percent) # left before refusal
usage.percentage_used(included, used) # passes 100 once the allowance is in use
Feature flags
Define flags with the builders in kaitencloud.targeting. Targeting rules are CEL expressions,
evaluated in order; the first match wins. Kaiten fills the reserved __kaiten namespace itself:
evaluate with a customer's slug as the targeting key, and a rule can read the license and the
entitlements that customer holds.
from kaitencloud import targeting
client.feature_flags.create(
name="New checkout",
slug="new-checkout",
type="boolean",
variants=[targeting.variant("on", True), targeting.variant("off", False)],
default_variant="off",
targetings=[
targeting.rule("Growth plan", "__kaiten.license.familySlug == 'growth'", "on"),
targeting.split_rule("Progressive rollout", "true", {"on": 10, "off": 90}),
],
)
This SDK does not evaluate flags, on purpose. Kaiten implements OFREP, the OpenFeature Remote Evaluation Protocol, so evaluation belongs to OpenFeature itself: install the SDK and the generic OFREP provider, point it at the same base URL with the same token, and your application talks to a vendor-neutral API it already knows.
pip install openfeature-sdk openfeature-provider-ofrep
from openfeature import api
from openfeature.contrib.provider.ofrep import OFREPProvider
from openfeature.evaluation_context import EvaluationContext
api.set_provider(
OFREPProvider(
base_url="https://kaiten.example.com/api", # the provider appends /ofrep/v1/...
headers_factory=lambda: {"Authorization": "Bearer ksh_..."},
)
)
flags = api.get_client("checkout")
context = EvaluationContext(targeting_key="acme", attributes={"region": "eu"})
if flags.get_boolean_value("new-checkout", False, context):
...
details = flags.get_boolean_details("new-checkout", False, context)
print(details.value, details.variant, details.reason)
The targeting key is the subject rollouts bucket on — a customer slug, a user id — and it is
what the reserved __kaiten namespace resolves against, so a rule can read the license and the
entitlements that subject holds. Splits are sticky: a subject keeps its variant from one
evaluation to the next. A failed evaluation returns the default you passed rather than raise,
which is OpenFeature's contract, not ours.
client.feature_flags.manifest() returns the OpenFeature manifest — every flag, its type and
its default — for tooling that generates typed accessors from flag definitions.
Releases and deployment zones
gateway = client.components.create(name="api-gateway", version="v1.4.0")
release = client.releases.create(version="v1.4.0", component_ids=[gateway.id])
# Pointing a zone at a release records the deployment.
client.deployment_zones.update(
"eu-west-1", name="AWS eu-west-1", type="production", release_id=release.id
)
Releases are immutable: changing one means deleting it and creating another.
Webhooks
Kaiten delivers webhooks through Svix. verify_webhook() checks the
signature and the timestamp of a delivery, then returns its event as a typed model:
import os
from fastapi import FastAPI, Request, Response
from kaitencloud.webhooks import (
CustomerCreatedEvent,
InstanceEntitlementCapExceededEvent,
WebhookVerificationError,
verify_webhook,
)
app = FastAPI()
@app.post("/webhooks/kaiten")
async def kaiten_webhook(request: Request) -> Response:
try:
event = verify_webhook(
await request.body(), # the raw body: parsing it first would change the signed bytes
request.headers,
secret=os.environ["KAITEN_WEBHOOK_SECRET"],
)
except WebhookVerificationError:
return Response(status_code=400)
match event:
case InstanceEntitlementCapExceededEvent(data=cap):
alert_account_manager(cap.entitlement_slug, cap.overage)
case CustomerCreatedEvent(data=customer):
send_welcome(customer.name)
return Response(status_code=204)
Every event the API publishes has a model named after it, from ComponentCreatedEvent to
ReleaseDeletedEvent. An event newer than the SDK you run parses as UnknownWebhookEvent
instead of failing.
The Platform API
The Platform API administers a whole Kaiten deployment. It listens on its own port, on no
public route, and accepts only a platform credential (ksm_...).
from datetime import timedelta
from kaitencloud import KaitenPlatformClient
platform = KaitenPlatformClient(base_url="http://kaiten-api.kaiten.svc:6001", token="ksm_...")
# Idempotent: the organization's id derives from the identity provider's id.
org = platform.organizations.ensure(external_id="org_2abcDEF", name="Acme")
# A platform credential never acts inside an organization: it mints a token that does.
minted = platform.tokens.mint(
org.id, name="billing-sync-v2", scopes=["write:instances"], ttl=timedelta(days=90)
)
store_secret(minted.token) # the plaintext exists in this response only
Revocation cascades. Revoking a platform credential revokes every token it minted, so rotate by minting the new token, adopting it, then revoking the old one. And
revoke()takes the token's server-generated slug, whichtokens.list()maps back from the name you chose.
old = next(token for token in platform.tokens.list(org.id) if token.name == "billing-sync-v1")
platform.tokens.revoke(org.id, old.slug)
A connector hosted outside Kaiten registers itself on every start — registration is an upsert:
platform.connectors.register(
name="kaiten.integration.crm.acme",
version="1.2.0",
settings_schema={
"type": "object",
"properties": {
"apiKey": {"type": "string", "writeOnly": True}
}, # a secret: never read back
},
entitlement_slug="crm-sync", # the BOOLEAN entitlement a license must grant to activate it
)
The playbook
examples/playbook.py is a guided tour of a deployment of your own:
thirteen steps that build a catalog, a license, a customer and an instance, meter usage until the
license refuses a report, evaluate a feature flag, mint and revoke a token and verify a webhook
signature — then delete everything they made. Each step prints the call before it makes it, so
the script reads as documentation that runs, one step at a time.
uv run python examples/playbook.py --list
examples/README.md is the guide to it: what each step covers, how to
pause and resume a run, what it records about itself, and how cleanup deletes only what that
run created.
Working with the SDK
Pagination
Every list() returns the whole collection: the SDK walks the cursor at the largest page
size the API accepts. If the API ever reports more rows without a usable cursor, list() raises
PaginationError rather than return a list it knows is partial.
The audit trail grows without bound, so it takes a ceiling instead:
entries = client.instances.list_audit_trails(
"acme-production", event_name="INSTANCE_STATUS_CHANGED", limit=100
)
Errors
Every exception derives from KaitenError:
KaitenError
├── CredentialError a credential is missing, or in the wrong place (raised before sending)
├── APIConnectionError the API could not be reached
│ └── APITimeoutError
├── APIStatusError the API answered with a non-2xx status
│ ├── BadRequestError 400
│ ├── AuthenticationError 401 missing, expired or revoked credential
│ ├── PermissionDeniedError 403 a scope is missing
│ ├── NotFoundError 404
│ ├── ConflictError 409
│ │ └── ThresholdExceededError a usage report refused at the cap
│ ├── UnprocessableEntityError 422
│ ├── RateLimitError 429
│ ├── InternalServerError 5xx
│ │ └── ServiceUnavailableError 503
│ └── FlagEvaluationError a flag could not be evaluated
├── APIResponseValidationError a 2xx response that breaks the contract
├── PaginationError a list the API reported as incomplete
├── WebhookVerificationError a webhook that fails verification
└── WebhookPayloadError an authentic webhook that breaks its event contract
An APIStatusError keeps everything the API said. Branch on code rather than on the status:
a 409 for a reached limit and a 409 for a taken slug share a status, not a meaning.
from kaitencloud import APIStatusError
try:
client.customers.create(name="Acme", slug="acme")
except APIStatusError as error:
print(error.status_code, error.code, error.detail)
print("Quote this in a support request:", error.error_id)
Retries and timeouts
A request that fails on a connection error, a 408, a 429 or a 5xx is retried with exponential
backoff and jitter, honouring Retry-After — but only when repeating it is safe:
| Request | Retried |
|---|---|
GET, PUT, DELETE |
✓ |
Flag evaluations, instance status and lifecycle updates, metadata dry runs and reorders, organizations.ensure(), connectors.register() |
✓ — they converge |
Any other POST or PATCH: creations, usage reports, upserts |
only if the connection never opened |
A 401 or 403 |
never |
Tune it per client, or per call with with_options(), which shares the connection pool:
import httpx
client = KaitenClient(timeout=10.0, max_retries=4)
client.with_options(timeout=1.0, max_retries=0).customers.get("acme")
proxied = KaitenClient(http_client=httpx.Client(proxy="http://proxy.internal:3128"))
Models
Responses are pydantic models with snake_case attributes. They are lenient where the API grows:
a field added after your SDK release lands in model_extra, and an unknown enum value parses as a
plain string instead of failing.
instance = client.instances.get("acme-production")
instance.customer_slug # typed attributes
instance.start_license_date # a timezone-aware datetime
instance.to_dict() # the exact wire shape, camelCase keys included
instance.model_extra # fields newer than this SDK
All models, and the typed dictionaries methods accept, are importable from kaitencloud.types.
Logging
The SDK logs to the kaitencloud logger: retries at INFO, flag evaluations that fell back to
their default at WARNING. Credentials are never logged.
import logging
logging.getLogger("kaitencloud").setLevel(logging.INFO)
API reference
The Core API clients cover 98 of its 107 operations. Of the nine left out, three are rule-editor tooling for the Kaiten console, four are a signed-in person's own notification feed and preferences — which an API token has no equivalent of — and two are the OFREP evaluation endpoints, which an OpenFeature provider calls. The Platform API clients cover all 10 of its operations. The async clients have the same methods.
client.components · client.connectors · client.customers
| Method | Operation |
|---|---|
client.components.list() |
GET /components |
client.components.get() |
GET /components/{componentSlug} |
client.components.create() |
POST /components |
client.components.update() |
PUT /components/{componentSlug} |
client.components.delete() |
DELETE /components/{componentSlug} |
client.connectors.list() |
GET /connectors |
client.connectors.get() |
GET /connectors/{connectorName} |
client.connectors.get_state() |
GET /connectors/{connectorName}/state |
client.connectors.activate() |
PUT /connectors/{connectorName}/activation |
client.connectors.deactivate() |
DELETE /connectors/{connectorName}/activation |
client.connectors.get_settings_schema() |
GET /connectors/{connectorName}/settings/schema |
client.connectors.get_settings() |
GET /connectors/{connectorName}/settings |
client.connectors.update_settings() |
PUT /connectors/{connectorName}/settings |
client.connectors.delete_settings() |
DELETE /connectors/{connectorName}/settings |
client.customers.list() |
GET /customers |
client.customers.get() |
GET /customers/{customerSlug} |
client.customers.create() |
POST /customers |
client.customers.update() |
PUT /customers/{customerSlug} |
client.customers.delete() |
DELETE /customers/{customerSlug} |
client.customers.get_integration() |
GET /customers/{customerSlug}/integrations/{integrationName} |
client.customers.create_integration() |
POST /customers/{customerSlug}/integrations/{integrationName} |
client.customers.update_integration() |
PUT /customers/{customerSlug}/integrations/{integrationName} |
client.customers.delete_integration() |
DELETE /customers/{customerSlug}/integrations/{integrationName} |
client.deployment_zones · client.entitlement_groups · client.entitlements
| Method | Operation |
|---|---|
client.deployment_zones.list() |
GET /deployment-zones |
client.deployment_zones.get() |
GET /deployment-zones/{deploymentZoneSlug} |
client.deployment_zones.create() |
POST /deployment-zones |
client.deployment_zones.update() |
PUT /deployment-zones/{deploymentZoneSlug} |
client.deployment_zones.delete() |
DELETE /deployment-zones/{deploymentZoneSlug} |
client.entitlement_groups.list() |
GET /entitlement-groups |
client.entitlement_groups.get() |
GET /entitlement-groups/{entitlementGroupSlug} |
client.entitlement_groups.create() |
POST /entitlement-groups |
client.entitlement_groups.update() |
PUT /entitlement-groups/{entitlementGroupSlug} |
client.entitlement_groups.delete() |
DELETE /entitlement-groups/{entitlementGroupSlug} |
client.entitlement_groups.add_entitlement() |
POST /entitlement-groups/{entitlementGroupSlug}/entitlements |
client.entitlement_groups.remove_entitlement() |
DELETE /entitlement-groups/{entitlementGroupSlug}/entitlements/{entitlementSlug} |
client.entitlement_groups.get_usage() |
GET /entitlement-groups/{entitlementGroupSlug}/usage |
client.entitlements.list() |
GET /entitlements |
client.entitlements.get() |
GET /entitlements/{entitlementSlug} |
client.entitlements.create() |
POST /entitlements |
client.entitlements.update() |
PUT /entitlements/{entitlementSlug} |
client.entitlements.delete() |
DELETE /entitlements/{entitlementSlug} |
client.feature_flags · client.flags
| Method | Operation |
|---|---|
client.feature_flags.list() |
GET /feature-flags |
client.feature_flags.get() |
GET /feature-flags/{featureFlagSlug} |
client.feature_flags.create() |
POST /feature-flags |
client.feature_flags.update() |
PUT /feature-flags/{featureFlagSlug} |
client.feature_flags.delete() |
DELETE /feature-flags/{featureFlagSlug} |
client.feature_flags.manifest() |
GET /openfeature/v0/manifest |
client.instances · client.integrations
| Method | Operation |
|---|---|
client.instances.list() |
GET /instances |
client.instances.get() |
GET /instances/{instanceSlug} |
client.instances.create() |
POST /instances |
client.instances.update() |
PUT /instances/{instanceSlug} |
client.instances.update_status() |
PATCH /instances/{instanceSlug} |
client.instances.update_lifecycle_stage() |
PATCH /instances/{instanceSlug} |
client.instances.delete() |
DELETE /instances/{instanceSlug} |
client.instances.list_audit_trails() |
GET /instances/{instanceSlug}/audit-trails |
client.instances.list_usage() |
GET /instances/{instanceSlug}/entitlements/usage |
client.instances.get_usage() |
GET /instances/{instanceSlug}/entitlements/{entitlementSlug}/usage |
client.instances.report_usage() |
POST /instances/{instanceSlug}/entitlements/{entitlementSlug}/usage |
client.instances.get_integration() |
GET /instances/{instanceSlug}/integrations/{integrationName} |
client.instances.create_integration() |
POST /instances/{instanceSlug}/integrations/{integrationName} |
client.instances.update_integration() |
PUT /instances/{instanceSlug}/integrations/{integrationName} |
client.instances.delete_integration() |
DELETE /instances/{instanceSlug}/integrations/{integrationName} |
client.integrations.get_customer() |
GET /integration/{adapter}/customer/{externalId} |
client.integrations.upsert_customer() |
PATCH /integration/{adapter}/customer/{externalId} |
client.integrations.get_instance() |
GET /integration/{adapter}/instance/{externalId} |
client.integrations.upsert_instance() |
PATCH /integration/{adapter}/instance/{externalId} |
client.licenses · client.metadata_fields
| Method | Operation |
|---|---|
client.license_families.list() |
GET /license-families |
client.license_families.get() |
GET /license-families/{familySlug} |
client.licenses.list() |
GET /licenses |
client.licenses.get() |
GET /licenses/{licenseSlug} |
client.licenses.create() |
POST /licenses |
client.licenses.update() |
PUT /licenses/{licenseSlug} |
client.licenses.delete() |
DELETE /licenses/{licenseSlug} |
client.licenses.publish() |
POST /licenses/{licenseSlug}/publish |
client.licenses.archive() |
POST /licenses/{licenseSlug}/archive |
client.licenses.unarchive() |
POST /licenses/{licenseSlug}/unarchive |
client.licenses.list_entitlements() |
GET /licenses/{licenseSlug}/entitlements |
client.licenses.get_entitlement() |
GET /licenses/{licenseSlug}/entitlements/{entitlementSlug} |
client.licenses.associate_entitlement() |
POST /licenses/{licenseSlug}/entitlements |
client.licenses.update_entitlement() |
PUT /licenses/{licenseSlug}/entitlements/{entitlementSlug} |
client.licenses.delete_entitlement() |
DELETE /licenses/{licenseSlug}/entitlements/{entitlementSlug} |
client.metadata_fields.list() |
GET /metadata-fields |
client.metadata_fields.create() |
POST /metadata-fields |
client.metadata_fields.update() |
PATCH /metadata-fields/{id} |
client.metadata_fields.archive() |
POST /metadata-fields/{id}/archive |
client.metadata_fields.unarchive() |
POST /metadata-fields/{id}/unarchive |
client.metadata_fields.dry_run() |
POST /metadata-fields/{id}/dry-run |
client.metadata_fields.reorder() |
POST /metadata-fields/reorder |
client.releases · client.service_accounts
| Method | Operation |
|---|---|
client.releases.list() |
GET /releases |
client.releases.get() |
GET /releases/{releaseSlug} |
client.releases.create() |
POST /releases |
client.releases.delete() |
DELETE /releases/{releaseSlug} |
client.service_accounts.list() |
GET /service-accounts |
client.service_accounts.get() |
GET /service-accounts/{serviceAccountSlug} |
client.service_accounts.create() |
POST /service-accounts |
client.service_accounts.update() |
PUT /service-accounts/{serviceAccountSlug} |
client.service_accounts.list_tokens() |
GET /service-accounts/{serviceAccountSlug}/tokens |
client.service_accounts.create_token() |
POST /service-accounts/{serviceAccountSlug}/tokens |
client.service_accounts.delete_token() |
DELETE /service-accounts/{serviceAccountSlug}/tokens/{tokenSlug} |
Platform API — KaitenPlatformClient
| Method | Operation |
|---|---|
platform.me() |
GET /platform/me |
platform.connectors.register() |
POST /platform/connectors |
platform.organizations.ensure() |
POST /platform/organizations |
platform.organizations.get() |
GET /platform/organizations/{orgId} |
platform.organizations.delete() |
DELETE /platform/organizations/{orgId} |
platform.organizations.delete_membership() |
DELETE /platform/organizations/{orgId}/memberships/{userId} |
platform.tokens.mint() |
POST /platform/organizations/{orgId}/tokens |
platform.tokens.list() |
GET /platform/organizations/{orgId}/tokens |
platform.tokens.revoke() |
DELETE /platform/organizations/{orgId}/tokens/{tokenSlug} |
platform.users.delete() |
DELETE /platform/users/{id} |
How the SDK stays true to the API
An SDK written by hand drifts from the API it wraps, one renamed field at a time — and a request body with one key too many is refused on every call. This one is built so that it cannot drift silently:
- The contract is vendored.
openapi/holds a snapshot of Kaiten's two OpenAPI documents, copied from kaitencloud/kaiten bytask sync:openapi, with the exact commit recorded inopenapi/source.yaml. - Models are generated from it, by
scripts/generate_models.py. - Every operation is accounted for.
openapi/coverage.yamlmaps each operation to the method that calls it; the tests fail when an operation is left unwrapped or a method maps to nothing. - Every method is tested against the contract. Each one is called against a fake API that answers like the contract says, and every request body it sends is validated against the operation's JSON Schema — once with all arguments, once with only the required ones.
- One implementation, two clients. The sync client is generated from the async source by
scripts/unasync.py, and the tests check both have the same signatures.
CI checks all of it on every change, on Python 3.10 to 3.14 and on the oldest supported versions of the dependencies.
Development
The workflows are Task targets over uv:
task setup # install the development environment
task check # everything CI runs
| Task | What it does |
|---|---|
task test |
Run the test suite with coverage |
task lint / task fmt |
Check or fix linting and formatting (ruff) |
task typecheck |
mypy --strict over the package |
task generate |
Regenerate the models and the sync client |
task sync:openapi |
Copy the contract from a kaiten checkout (KAITEN_PATH, default ../kaiten), then regenerate |
task build |
Build the sdist and the wheel, and check their metadata |
The playbook is part of the test suite: every step runs against the fake API, so a renamed method or an argument the contract no longer accepts fails in CI rather than halfway through somebody's walkthrough.
Never edit src/kaitencloud/_sync/ or the generated src/kaitencloud/types/ modules by hand:
change the async source, the contract or a generator, then run task generate.
Contributions are welcome — CONTRIBUTING.md says how, and every commit carries
a DCO sign-off (git commit -s). Report a vulnerability privately, as
SECURITY.md describes, never in a public issue.
Versioning
The SDK follows Semantic Versioning: only a major release changes the public API — the changelog says how — while a minor release adds to it and a patch release fixes it.
License
Kaiten SDK for Python is open source and licensed under the Apache License, Version 2.0.
By contributing, you agree to certify your contribution under the Developer Certificate of Origin 1.1.
The Kaiten name and logos are not licensed under Apache-2.0. See the Kaiten trademark policy in the main Kaiten repository.
Metadata
Release files for kaitencloud 1.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| kaitencloud-1.0.0.tar.gz | 190.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| kaitencloud-1.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 320.7 kB
Release files / kaitencloud-1.0.0.tar.gz
| Download URL | kaitencloud-1.0.0.tar.gz |
|---|---|
| Size | 190.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
09ecd8670f6a1a83ea9b6397e34d5192e158a3c9b0c260edaee18953d905563c
|
|
BLAKE2b-256 checksum How to use checksums |
10b2583b741a4fccbde6e27e5e04567f48ab2d9be22e3ffa2f2d002d0767e033
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.21 {"installer":{"name":"uv","version":"0.12.21","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / kaitencloud-1.0.0-py3-none-any.whl
| Download URL | kaitencloud-1.0.0-py3-none-any.whl |
|---|---|
| Size | 130.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
bb59df10fd012fbf9c5b74a2add4f235aa36e2e983b45938e8d85511565beaa4
|
|
BLAKE2b-256 checksum How to use checksums |
8caaa4f9b4429a7c27279f990d8ec5723d05fd3f8db5a6accb529496ffe06d2b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.21 {"installer":{"name":"uv","version":"0.12.21","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|