Skip to main content

horizonmedia-blu-sdk (Python)

Make your Python API a Blu module. It's the Python counterpart of @blu/sdk/server:

  • Verify Blu's callers: users signed in through Blu, other backends with Blu service tokens, users of your own identity provider, and API keys you issue.
  • Route policies.
  • Tools for the Blu Concierge (MCP).

There are adapters for FastAPI, Flask and Django. The core is framework-neutral.

Blu's front-end pieces (the React hooks, isolated frames, the Next.js adapter) stay in the JS packages. A Python team usually pairs this with a small JS front end, or exposes only an API and Concierge tools.

Install

The SDK is public on PyPI as horizonmedia-blu-sdk. You still import it as blu_sdk. There's no token or private index to configure.

pip install "horizonmedia-blu-sdk[fastapi]~=1.2.3"

pyproject.toml (pip / uv):

[project]
dependencies = ["horizonmedia-blu-sdk[fastapi]~=1.2.3"]

requirements.txt: horizonmedia-blu-sdk[fastapi]~=1.2.3

Poetry: poetry add "horizonmedia-blu-sdk[fastapi]@^1.2.3"

Extras: [fastapi], [flask] or [django]. Without an extra, you get the framework-neutral core and the ASGI middleware. ~=1.2.3 takes fixes and new 1.x features (any 1.x from 1.2.3 up) and never a 2.0. Commit your lock file so builds are repeatable. The GitHub release for each sdk-py-v* tag says what changed.

Before 1.2.3 the SDK was installed from Git tags (blu-sdk @ git+https://github.com/HorizonMedia/blu-composable-experience@sdk-py-v…). To move, replace that line with the one above and drop any token you set up to clone the repository. blu check warns about the old form.

Quick start (FastAPI)

from fastapi import Depends, FastAPI
from blu_sdk import BluPlatformAuth, ModuleAuth, Principal, PrincipalDirectory
from blu_sdk.asgi import CorsAllowlist
from blu_sdk.fastapi import BluFastAPI

BLU_ISSUER = os.environ["BLU_ISSUER"]  # this environment's Blu (its host-api URL)
SELF = os.environ["PUBLIC_URL"]  # this API's own URL

blu = BluFastAPI(
    ModuleAuth(
        [
            BluPlatformAuth(
                issuer=BLU_ISSUER,
                module_id="forecast",  # = blu.module.json "id"
                role_map={
                    "viewer": ["forecast:read"],  # YOUR decision: Blu role → your permissions
                    "analyst": ["forecast:read"],
                    "owner": ["forecast:read", "forecast:write"],
                },
            )
        ],
        api_url=SELF,
    ),
    directory=PrincipalDirectory(),  # Blu users recorded on first sight (no account needed)
)

app = FastAPI()
blu.install(app)  # refusals answer {"error", "detail"} with their status
app.add_middleware(CorsAllowlist, origins=[os.environ["BLU_SHELL_ORIGIN"]])


@app.get("/api/forecast")
async def forecast(p: Principal = Depends(blu.require({"blu": ["forecast:read"]}))):
    return rows_for(p.client_id)  # scope EVERY query by the verified client

Flask (blu_sdk.flask.BluFlask, decorator @blu.require(...), current_principal()) and Django (blu_sdk.django.configure(...), decorator @require(...), request.blu_principal) work the same way. Each module's docstring has a complete example.

The four ways in

Entry point Strategy Who Client comes from
blu BluPlatformAuth(issuer, module_id, role_map) a user signed in through Blu: in a Blu shell, through the Concierge, or your own app's "Sign in with Blu" the token (cid)
service BluServiceAuth(issuer, module_id) another backend, with a Blu client-credentials token; scopes granted in the Blu Admin X-Client-Id, from the clients the token lists
team TeamIdpAuth(issuer, audience, jwks) users of your own identity provider (Cognito, Auth0, Entra, …); map your claims with to_principal your token
api-key ApiKeyAuth(keys) outside integrations, server-to-server only (requests with an Origin are refused); keys stored as hash_api_key(...) only X-Client-Id, from the key's own grant

Turn entry points on per deployment: enabled_modes(os.environ.get("AUTH_MODES")).

What Blu guarantees about a Blu token:

  • It's for your module only (audience blu-module:<module_id>). A token for another module, or the shell's own session token, is refused here.
  • It's only issued while your module is enabled for that client.
  • It lives 60 seconds, so a role or access change in the Blu Admin reaches you within a minute.

Passwordless test sign-ins (local personas, sign-in simulators) carry amr: ["mock"]. Only an API whose own api_url is on the developer's machine accepts them; leave api_url out and they're always refused.

The principal

Every entry point yields the same Principal:

Field Meaning
auth_mode which entry point the caller used
kind user or service
subject, display_name, email who it is
client_id the active client. Scope every query by it; never trust a client id from the request body
roles, permissions Blu roles, and your permissions after the role map (p.can("forecast:read"))
key a stable identity (<mode>|<issuer>|<subject>). Key your own records on it
via, actor for Blu callers: which app asked (blu-host, blu-concierge, your own app), and software acting for the user (blu-concierge)

Route policies

require({...}) (or ModuleAuth.authorize(principal, policy, asked_client_id)) names which entry points may call a route, and the permissions each needs, all of which are required. An entry point that isn't named is refused even if it authenticated, so you can keep a route team-only:

blu.require({"blu": ["forecast:read"], "team": ["forecast:read"], "service": ["forecast:read"]})
blu.require({"team": ["forecast:admin"]})  # never Blu

A user whose request names a different client than their credential gets 403 client_mismatch, never an override.

Concierge tools (MCP)

Wrap your read endpoints as tools. Each runs with the caller's own permissions and client, and a caller only sees the tools its role allows.

from pydantic import BaseModel, Field
from blu_sdk import McpTool, ToolResult, link


class Args(BaseModel):
    months: int = Field(12, ge=1, le=12)


async def get_forecast(p, args: Args) -> ToolResult:
    rows = rows_for(p.client_id)[: args.months]
    return ToolResult(
        summary=f"{len(rows)} months for {p.client_id} …", data=rows, links=[link("forecast", {"year": "2027"}, "2027 forecast")]
    )  # routes from your manifest only


TOOLS = [
    McpTool(
        name="get_forecast",
        title="Spend forecast",
        description="Forecast team: monthly spend forecast for the current client. Use for …",
        input=Args,
        policy={"blu": ["forecast:read"]},
        read_only=True,
        run=get_forecast,
    )
]

blu.mount_mcp(app, name="forecast", version="1.0.0", tools=TOOLS, manifest=MANIFEST)  # POST /mcp
  • The manifest is required. The endpoint refuses to start unless the tools match mcp.tools in your blu.module.json exactly: the names, the readOnly flags, and the requires (which must equal each tool's policy).
  • Links name routes from your manifest, never URLs. Links to undeclared routes are dropped.
  • The Concierge only reads. It never uses a write tool (read_only=False): the SDK doesn't list or run one for a caller acting for a user (the Concierge's tokens carry act), and Blu never offers one. Write tools still serve your own callers (users, services).
  • Flask and Django serve blu_sdk.mcp.McpEndpoint from their ASGI side (see each adapter's docstring).

CORS

blu_sdk.asgi.CorsAllowlist(app, origins=[...]) echoes only the origins you list and answers preflights for Authorization. Others get no CORS headers, so the browser won't hand them your responses.

  • An inline module is called from the Blu shell's origin.
  • An isolated module's frame page calls from its own origin.
  • Your own standalone app needs its own origin listed too.

Blu sends credentials as headers (Bearer), never cookies.

Configuration per environment

Env var (suggested) Value
BLU_ISSUER this environment's Blu issuer, blu_issuer(stage) (the JWKS is <issuer>/.well-known/jwks.json)
BLU_STAGE dev, staging or prod; blu_shell_origins(stage) gives the shell's origin for CORS (https://dev.blu-ce.horizonmedia.com; prod: https://blu-ce.horizonmedia.com)
PUBLIC_URL this API's own URL (api_url; also the manifest's apiBase)
AUTH_MODES e.g. blu,team

Never hard-code a Blu environment.

Register a release

Once per release, after uploading its files to their versioned folder:

BLU_CI_TOKEN=… python -m blu_sdk register      # in the folder with blu.module.json

It sends the manifest to the Blu registry (blu_sdk.REGISTRY_URL, https://registry.blu-ce.horizonmedia.com; BLU_REGISTRY_URL or --registry overrides it). Releases are immutable: bump version for every release. Blu copies and reviews the release (read the report with your CI token at GET <registry>/v1/modules/<id>/versions/<version>/report), then Blu admins approve it environment by environment. Keep your API backward-compatible for about 30 days: an environment can be rolled back to an earlier front end.

Check your integration

Run blu check from the JS SDK (npx blu check, or ask your Blu contact) in the folder with your blu.module.json. It reads Python too. It checks:

  • your imports;
  • that blu-sdk is declared, pinned to a release tag;
  • that module_id matches the manifest;
  • that the MCP endpoint is given the manifest and defines every declared tool;
  • that links go only to declared routes.

Tests

Strategies take jwks= as the key set itself instead of a URL, so tests can sign their own tokens:

BluPlatformAuth(issuer="https://blu.test", module_id="forecast", role_map=ROLE_MAP, jwks={"keys": [public_jwk]})

Same behaviour as the JS SDK

This SDK and @blu/sdk/server run the same authentication cases: real signed tokens, API keys and policies, in packages/sdk/test/auth-vectors.json. They must give the same principal, or the same status and error code, for every case. The manifest checks share packages/contracts/test/vectors.json in the same way. A rule change lands in both SDKs, or the tests fail.

Releasing (Blu platform team)

  1. Bump version in pyproject.toml and __version__ in src/blu_sdk/__init__.py.

  2. Run pnpm test:backend at the repository root. It runs this package's tests and both shared case files in both languages.

  3. Tag the commit sdk-py-v<version> and push the tag: git tag sdk-py-v1.0.1 && git push origin sdk-py-v1.0.1. The release-sdk-python workflow then:

    • checks that the tag matches the version;
    • runs the tests;
    • publishes the wheel and source distribution to PyPI (trusted publishing from the pypi environment, no token);
    • creates a GitHub release with them attached.

    Tags are never moved or reused.

Metadata

Release files for horizonmedia-blu-sdk 1.2.3

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

Source distribution (sdist)

Source distribution for horizonmedia-blu-sdk 1.2.3
File Size Uploaded Provenance Actions
horizonmedia_blu_sdk-1.2.3.tar.gz 36.6 kB With provenance Details for horizonmedia_blu_sdk-1.2.3.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for horizonmedia-blu-sdk 1.2.3
File Interpreter ABI Platform Actions
horizonmedia_blu_sdk-1.2.3-py3-none-any.whl Python 3 none any Details for horizonmedia_blu_sdk-1.2.3-py3-none-any.whl

Total release size: 69.9 kB

Release files / horizonmedia_blu_sdk-1.2.3.tar.gz

Download URL horizonmedia_blu_sdk-1.2.3.tar.gz
Size 36.6 kB
Tags Source
SHA-256 checksum
How to use checksums
5e09e7ce09700b08996808ed93ac893809a61da1cd7de8840db0574e0e7e5eca
BLAKE2b-256 checksum
How to use checksums
578e0d5498687f827a2e68762c264f67671d89b6ba844ddc072a4b0250f5f4f4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 5, 2026.

Transparency log

Release files / horizonmedia_blu_sdk-1.2.3-py3-none-any.whl

Download URL horizonmedia_blu_sdk-1.2.3-py3-none-any.whl
Size 33.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
07657e724aa8d6bdda5e2e3a0f84ce8278961b88595b06b30c4628dad0441144
BLAKE2b-256 checksum
How to use checksums
912a816166b834ccc0be5c7de921c957cc7664ad68bb2bfabe3f95ba78ca2109
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 5, 2026.

Transparency log

Security for horizonmedia-blu-sdk 1.2.3

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for horizonmedia_blu_sdk-1.2.3.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 5, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

1.2.3 This release

2 release files Full provenance

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