Skip to main content

rixl

PyPI

The official Python client for the Rixl API.

Rixl handles the media side of your product: uploading and delivering images and videos, organising them into feeds and posts, and reporting on how people engage with them. It also covers the account layer around that: users and organisations, sign-in, subscriptions and invoices. This SDK gives you all of it from Python, as a fluent request builder that mirrors the URL structure of the API, with a dataclass for every request and response.

The client is generated with Kiota and is async throughout. Every call is a coroutine you await. It needs Python 3.10 or later, and pulls in microsoft-kiota-bundle for the HTTP transport, serializers and request adapter.

Installation

pip install rixl

Getting started

Here is the whole thing. Build a client, then list the images in a project:

import asyncio
import os

from kiota_abstractions.authentication.api_key_authentication_provider import (
    ApiKeyAuthenticationProvider,
    KeyLocation,
)
from kiota_http.httpx_request_adapter import HttpxRequestAdapter

from rixl_sdk.rixl_client import RixlClient


async def main():
    auth = ApiKeyAuthenticationProvider(
        key_location=KeyLocation.Header,
        api_key=os.environ["RIXL_API_KEY"],
        parameter_name="X-API-Key",
    )
    adapter = HttpxRequestAdapter(auth, base_url="https://api.rixl.com")
    client = RixlClient(adapter)

    page = await client.media.v1.projects.by_project_id(
        os.environ["RIXL_PROJECT_ID"]
    ).images.get()

    for image in page.images or []:
        print(image.id, image.width, image.height)


asyncio.run(main())

Always pass base_url to the adapter. The client builds every URL from {+baseurl}, so without it requests go nowhere useful.

You navigate to an endpoint by chaining properties that spell out its path, then call the HTTP verb: client.media.v1.projects.by_project_id(pid).images.get() is GET /media/v1/projects/{project_id}/images. Path segments that take an ID are by_* methods. Responses come back as dataclasses; there is no dict to dig through.

Authentication

There are two ways to identify yourself, and they answer different questions.

API keys, for your backend calling as itself

An API key represents your organisation. Use it for work your own systems do: importing a catalogue, running a nightly report, reconciling invoices. Create one in the Rixl dashboard, keep it out of source control, and read it from the environment:

auth = ApiKeyAuthenticationProvider(
    key_location=KeyLocation.Header,
    api_key=os.environ["RIXL_API_KEY"],
    parameter_name="X-API-Key",
)

Note the argument order: key_location comes first, so pass them by keyword.

The key travels as the X-API-Key header. Anyone holding it can do anything your organisation can, so it belongs on a server, never in anything you ship to users.

Client credentials, for acting on behalf of your users

If you are building on top of Rixl and your own users each need their own slice of it, use client credentials. You exchange a client ID and secret for a short-lived token scoped to a single end user, so one customer can never read another's media.

First create the credential. The response carries a secret that is returned once:

from rixl_sdk.models.clientauth.v1.create_client_credential_request import (
    CreateClientCredentialRequest,
)

request = CreateClientCredentialRequest()
request.name = "Production backend"

created = await admin.platform.clientauth.v1.credentials.post(request)
print(created.credential.client_id, created.client_secret)

Then, in the service that handles your users' requests, exchange it for a token. subject is your own identifier for that person, whatever your database calls them:

from rixl_sdk.models.clientauth.v1.mint_client_token_request import (
    MintClientTokenRequest,
)

mint = MintClientTokenRequest()
mint.client_id = os.environ["RIXL_CLIENT_ID"]
mint.client_secret = os.environ["RIXL_CLIENT_SECRET"]
mint.subject = user_id
mint.project_id = os.environ["RIXL_PROJECT_ID"]

token = await client.platform.clientauth.v1.token.post(mint)

Minting needs no credentials of its own, since the ID and secret are in the body, so an anonymous client will do. You get back access_token, token_type and expires_at. Use the token by building a second client whose authentication provider sends it as a bearer token:

from kiota_abstractions.authentication.base_bearer_token_authentication_provider import (
    BaseBearerTokenAuthenticationProvider,
)
from kiota_abstractions.authentication.access_token_provider import AccessTokenProvider
from kiota_abstractions.authentication.allowed_hosts_validator import AllowedHostsValidator


class StaticToken(AccessTokenProvider):
    def __init__(self, token: str):
        self._token = token
        self._validator = AllowedHostsValidator(["api.rixl.com"])

    async def get_authorization_token(self, uri, additional_authentication_context={}):
        return self._token

    def get_allowed_hosts_validator(self):
        return self._validator


user_adapter = HttpxRequestAdapter(
    BaseBearerTokenAuthenticationProvider(StaticToken(token.access_token)),
    base_url="https://api.rixl.com",
)
user_client = RixlClient(user_adapter)

Tokens last at most 15 minutes and there is no refresh token: when one expires, mint another. Nothing here caches or renews for you, so if you are serving many requests, hold each user's token until expires_at and mint again after that. Set mint.ttl_minutes if you want something shorter than the maximum.

To retire a credential, revoke it with client.platform.clientauth.v1.credentials.by_credential_id(id).revoke.post(). New tokens stop immediately, and any already issued expire within 15 minutes.

Public endpoints

Some reads need no credentials at all: fetching a public image or video, reading a public feed, listing supported languages. Build the adapter with the anonymous provider:

from kiota_abstractions.authentication.anonymous_authentication_provider import (
    AnonymousAuthenticationProvider,
)

adapter = HttpxRequestAdapter(
    AnonymousAuthenticationProvider(), base_url="https://api.rixl.com"
)
public = RixlClient(adapter)

image = await public.media.v1.images.by_image_id(image_id).get()
posts = await public.posts.v1.feeds.by_feed_id(feed_id).get()
languages = await public.media.v1.languages.get()

The public set is: the sign-in flows under /auth/v1/, GET /media/v1/images/*, GET /media/v1/videos/*, GET /media/v1/languages, GET /posts/v1/feeds/*, and the token endpoints under /platform/. Everything else needs a key or a token.

What you can do

The client's top-level properties split the API by path prefix. The API itself is organised into six areas:

Media: client.media. Images, videos, audio tracks, chapters, subtitles, supported languages, and the image and video conversion pipelines. Upload and deliver files, attach audio and captions to a video, and convert media into the formats and sizes you serve.

Content: client.posts and client.feeds. Group media into posts and feeds. A project is the container everything else hangs off, which is why so many builder chains start with by_project_id.

Analytics: client.analytics. Dashboards, raw events, post and video and feed metrics, funnels, heatmaps and live activity.

Billing: client.billing. Plans, subscriptions, payments, invoices, metered usage and sales records.

Account management: client.auth, plus client.organizations. Sign-in flows including passkeys and one-time codes, user profiles, organisation membership and roles, access policies, custom domains, transactional email and blog broadcasts.

Platform: client.platform. API keys, platform auth, and the client credentials above.

There is also client.internal, which covers storage-event callbacks the platform makes to itself. You will not need it.

Working with resources

Builders follow the same shape, so once you have used one you have used all of them:

project = client.media.v1.projects.by_project_id(project_id)

page = await project.images.get()
image = await client.media.v1.images.by_image_id(image_id).get()
await project.images.by_image_id(image_id).delete()

Note the asymmetry: reading a single image is a public, project-free route (/media/v1/images/{image_id}), while listing and deleting are scoped to a project. The builder chain always tells you which.

Calls that send data take a request dataclass. Construct it empty and assign fields. Every field is optional and defaults to None, so anything you leave alone is omitted from the JSON:

from rixl_sdk.media.v1.projects.item.images.upload.upload_post_request_body import (
    UploadPostRequestBody,
)

body = UploadPostRequestBody()
body.name = "photo.jpg"

Response fields are optional too, since the API omits what it has nothing to say about, so guard with or [] and is not None rather than assuming a value is there.

Uploading files

Uploads happen in two steps. You ask Rixl for a URL, then send the bytes straight to storage. The bytes never pass through the API, so large files stay fast:

import httpx

body = UploadPostRequestBody()
body.name = "photo.jpg"

upload = await project.images.upload.post(body)

async with httpx.AsyncClient() as http:
    await http.put(
        upload.upload_url,
        content=image_bytes,
        headers={"Content-Type": "image/jpeg"},
    )

Videos work the same way, except you get two URLs back, one for the video and one for its poster image:

from rixl_sdk.media.v1.projects.item.videos.upload.upload_post_request_body import (
    UploadPostRequestBody as VideoUploadBody,
)

body = VideoUploadBody()
body.name = "clip.mp4"

upload = await project.videos.upload.post(body)

async with httpx.AsyncClient() as http:
    await http.put(upload.video_upload_url, content=video_bytes,
                   headers={"Content-Type": "video/mp4"})
    await http.put(upload.poster_upload_url, content=poster_bytes,
                   headers={"Content-Type": "image/jpeg"})

There is no "finish" call to make. Storage tells Rixl when the object lands and the image or video becomes available on its own. The URLs expire, and expires_at tells you when, so upload promptly rather than stashing them.

Pagination

List calls take a limit and an offset through a per-builder query-parameters class, wrapped in a RequestConfiguration:

from kiota_abstractions.base_request_configuration import RequestConfiguration
from rixl_sdk.media.v1.projects.item.images.images_request_builder import (
    ImagesRequestBuilder,
)

limit, offset = 50, 0

while True:
    params = ImagesRequestBuilder.ImagesRequestBuilderGetQueryParameters(
        pagination_limit=limit,
        pagination_offset=offset,
    )
    page = await project.images.get(
        request_configuration=RequestConfiguration(query_parameters=params)
    )

    images = page.images or []
    for image in images:
        print(image.id)

    if len(images) < limit:
        break
    offset += limit

Stop when a page comes back shorter than the limit. The generated list responses carry limit, offset and sort_* but no total, so counting is the only signal you have. The SDK does not paginate for you. There is no iterator and no automatic page fetching. Write the loop.

Handling errors

Anything that is not a 2xx raises APIError, which carries the status code and the response headers:

from kiota_abstractions.api_error import APIError

try:
    image = await client.media.v1.images.by_image_id(image_id).get()
except APIError as err:
    print(f"rixl returned {err.response_status_code}: {err.message}")

What the codes mean:

Status What happened What to do
400 The request was malformed or failed validation Fix the request; retrying will not help
401 The key or token is missing, expired or invalid Check the credential
403 The credential is valid but not allowed to do this Check the policies on it
404 No such resource, or it belongs to another organisation Check the ID and the project
429 You are going too fast Back off and retry
5xx Something broke on our side Retry with backoff

The spec declares no typed error bodies, so APIError is what you get for every failing status, so switch on response_status_code rather than on the exception type. Connection failures and timeouts raise httpx exceptions instead.

Timeouts

The SDK does not impose a timeout and does not retry. It uses the httpx AsyncClient you give it, so the behaviour stays yours to control:

import httpx

adapter = HttpxRequestAdapter(
    auth,
    http_client=httpx.AsyncClient(timeout=30.0),
    base_url="https://api.rixl.com",
)

The same client is where an httpx event hook goes if you want tracing headers on every outbound request.

Versioning

This package follows SemVer. New API operations arrive in minor releases; renamed or removed ones only in major ones. If an upgrade breaks you unexpectedly, please open an issue. We would rather hear about it.

Support

Bugs and feature requests: github.com/rixlhq/rixl-python/issues.

Download files

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

Source Distribution

rixl-1.3.0.tar.gz (86.7 kB view details)

Uploaded Source

Built Distribution

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

rixl-1.3.0-py3-none-any.whl (308.0 kB view details)

Uploaded Python 3

File details

Details for the file rixl-1.3.0.tar.gz.

File metadata

  • Download URL: rixl-1.3.0.tar.gz
  • Upload date:
  • Size: 86.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for rixl-1.3.0.tar.gz
Algorithm Hash digest
SHA256 6df5e021b10140b8f8708dbdf4af973d9fdf70775a7a05ebbcf391d3137a9208
MD5 7a2011e3dd78061c38b492f6d2cef056
BLAKE2b-256 49db0f4ea213a3585b1e2856b48910a96fac0a6ae5263b63ae3dfcad297eaf7a

See more details on using hashes here.

Provenance

The following attestation bundles were made for rixl-1.3.0.tar.gz:

Publisher: release.yml on rixlhq/rixl-python

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file rixl-1.3.0-py3-none-any.whl.

File metadata

  • Download URL: rixl-1.3.0-py3-none-any.whl
  • Upload date:
  • Size: 308.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for rixl-1.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 b4a65d53d2f9e7677396e5550f669a438bee402c2a5c3471665b577a87483be1
MD5 cecf9754f4aa0755eea5e42b368e8227
BLAKE2b-256 4b88faa6c4b22aa84a9a8e7552340d321c30e72f2af757c3d66ccaa63008db72

See more details on using hashes here.

Provenance

The following attestation bundles were made for rixl-1.3.0-py3-none-any.whl:

Publisher: release.yml on rixlhq/rixl-python

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

1.3.0 This release

2 files

1.2.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