Skip to main content

Kante

Kante is a simple lightweight strawberry utily library, that merges the efforts aims to provide common utilities for strawberry and strawberry-django projects.

Installation

pip install kante

Usage

Here is a simple example of how to use kante with strawberry and strawberry-django. It can be used with any ASGI application, but this example uses Django.

import asyncio
from typing import AsyncGenerator
from kante.types import Info, require_ws
import strawberry
from strawberry import ID, scalars
from typing import cast
from kante.channel import build_channel
import kante
from pydantic import BaseModel
from strawberry.experimental import pydantic
import strawberry_django

class StrChannelModel(BaseModel):
    id: str
    name: str


@pydantic.type(StrChannelModel)
class StrChannel:
    id: str
    name: str


str_channel = build_channel(StrChannelModel, "test_channel")
ROOM = "str_channel_room"


@strawberry.type
class Me:
    id: str



@strawberry.type
class Query:
    
    
    @strawberry.field
    def me(self, info: Info, id: ID) -> Me:
        return Me(id=id)


@strawberry.type
class Mutation:
    
    
    @strawberry.field
    def me(self, info: Info, id: ID) -> Me:
        return Me(id=id)
    
    
    @strawberry_django.field
    def send(self, info: Info, id: ID) -> str:
        
        str_channel.broadcast(StrChannelModel(id=str(id), name="test"), groups=[ROOM])
        return str(id)
    
    
    
@strawberry.type
class Subscription:
    
    
    @strawberry.subscription
    async def time(self, info: Info) -> AsyncGenerator[scalars.JSON, None]:
        context = require_ws(info)
        for i in range(2):
            yield context.connection_params
            await asyncio.sleep(1)
            
            
    @strawberry.subscription
    async def listen_str_channel(self, info: Info) -> AsyncGenerator[StrChannel, None]:
        """ Listen to the str_channel and yield messages."""
        async for i in str_channel.listen(info, [ROOM]):
            yield cast(StrChannel, i)

schema = kante.Schema(
    query=Query,
    mutation=Mutation,
    subscription=Subscription,
)

To wrap the schema with the ASGI application, you can use the router function from kante.router.

import os
from django.core.asgi import get_asgi_application
from kante.router import router

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_project.settings")
# Initialize Django ASGI application early to ensure the AppRegistry
# is populated before importing code that may import ORM models.
django_asgi_app = get_asgi_application()


from .schema import schema  # noqa



application = router(
    schema=schema,
    django_asgi_app=django_asgi_app,
    schema_path="schema",  # optional: serves the SDL as text/plain at /schema
)

What kante actually adds

Most of what kante exports is a plain alias of a strawberry symbol. The parts that do something are:

Symbol What it does
kante.Schema federation schema with DjangoOptimizerExtension installed by default
kante.django_type Django type + federation @key(fields: "id") + a batching resolve_reference
kante.context the request seam: user, client, organization, membership, provenance
kante.channel typed pub/sub over the channels layer
kante.scoping organization (tenant) scoping for querysets
kante.errors GraphQL errors carrying a machine-readable code, and pydantic failures as prose
kante.unions discriminated input unions: one merged wire type, derived from its members
kante.router ASGI wiring for HTTP + websocket GraphQL
kante.testing ASGI test clients and request-context factories

Prefer strawberry.type over kante.type. The plain re-exports (type, input, interface, field, mutation, scalar, ...) are module-level aliases of overloaded decorators, and mypy does not carry an overload set across an alias -- @kante.type makes mypy resolve the class to builtins.type, so every constructor keyword is reported as unexpected. They remain for backwards compatibility. django_type and django_interface are real functions and are unaffected.

Tenant scoping

Every read must be limited to the organization the request authenticated for. Single-object access goes through kante.scoping:

from kante.scoping import for_org, get_for_org, aget_for_org

# instead of models.File.objects.get(id=id)
file = get_for_org(models.File, info, id=id)

The path from a model to its organization is discovered by walking non-nullable foreign keys (a nullable path would hide rows rather than scope them). A model with no such path raises UnscopedModelError instead of quietly returning everything; declare the deliberate exceptions on your own scoper:

from kante.scoping import OrganizationScoper

scoper = OrganizationScoper(unscoped_models={"SystemSetting"}, max_path_depth=5)

List fields are scoped on the queryset instead:

@kante.django_type(models.File)
class File:
    id: strawberry.ID

    @classmethod
    def get_queryset(cls, queryset, info):
        return scoper.prescope(info, queryset)

Subscriptions

Relay the id of what changed and let the subscriber re-fetch it scoped -- that way the room name is not the only thing standing between two tenants:

from kante.channel import CRUDSignal, build_channel
from kante.scoping import aget_for_org

file_channel = build_channel(CRUDSignal, "files")

# signals.py -- broadcast after the transaction commits
@receiver(post_save, sender=models.File)
def on_file_saved(sender, instance, created, **kwargs):
    file_channel.broadcast_on_commit(
        CRUDSignal(create=instance.id) if created else CRUDSignal(update=instance.id),
        groups=[file_channel.org_group(instance.organization)],
    )

# subscriptions.py -- same helper builds the same room name
async def files(self, info: Info) -> AsyncGenerator[FileEvent, None]:
    room = file_channel.org_group(info.context.request.organization)
    async for message in file_channel.listen(info, [room]):
        if message.create:
            yield FileEvent(create=await aget_for_org(models.File, info, id=message.create))

listen() takes info directly and narrows to the websocket context itself, so no assert isinstance(info.context, WsContext) is needed. Use kante.require_ws(info) when you need the context for something else.

Always pass groups. Omitting it falls back to a single process-wide "default" room shared by every channel in the deployment; that fallback now warns and will be removed in kante 3. A channel that genuinely wants an implicit room should declare its own: build_channel(Model, "name", default_groups=[...]).

Errors

from kante.errors import NotFound, PermissionDenied

raise NotFound("No dataset with that id")
# {"errors": [{"message": "No dataset with that id",
#              "extensions": {"code": "NOT_FOUND"}}]}

Nothing is masked -- the message is the message you wrote. The addition is a code the client can branch on, so a deliberate error is distinguishable from an internal one.

Discriminated input unions

GraphQL has no input unions, so a union arriving through an argument is wired as one merged input: a discriminator plus every member's fields, all optional. Write the members; the merged type is derived from them.

from kante.unions import merged_input, union_member, union_member_types, unionElementOf

class ScaleTransformModel(BaseModel):
    kind: Literal["SCALE"] = "SCALE"
    scale: list[float] = Field(description="The per-axis factors")
    model_config = ConfigDict(extra="forbid")   # required: this is the strictness

@union_member("TransformInput", key="SCALE")
@kante.pydantic_input(ScaleTransformModel, description="The fields a SCALE member reads")
class ScaleTransformInput:
    kind: TransformKind
    scale: list[float]

@merged_input(members=[ScaleTransformInput, FieldTransformInput], noun="transformation")
class TransformInput:
    """One authored edge, discriminated by `kind`."""

schema = kante.Schema(
    query=Query,
    types=union_member_types(TransformInput),   # nothing references these; omit them and
    schema_directives=[unionElementOf],         # they vanish from the SDL silently
)

The merged type carries every member's fields, optional, typed as the member types them, and described as "(SCALE, BY_DIMENSION) ..." -- the prefix computed from the members that read the field, so it cannot go stale. Its generated to_pydantic() returns the member model the discriminator selects, and a field that contradicts the kind is an error naming both rather than a silent drop:

A SCALE transformation does not read `field`: it reads `scale`.
Drop it, or pick the kind that reads it.

Each member is published in the SDL under @unionElementOf, which is how a generated client rebuilds the tagged union -- turms emits Annotated[A | B, Field(discriminator="kind")] from it. That only works on an SDL-sourced schema: an introspected one carries no directive applications, and codegen degrades to a plain input with no warning.

Descriptions live on the pydantic model. Strawberry's pydantic integration takes a field's description from Field(description=...) and ignores any strawberry.field(description=...) in the member's class body -- silently.

Testing

from kante.testing import build_http_context

@pytest.fixture
def authenticated_context(db) -> HttpContext:
    user = User.objects.create(sub="1", username="tester")
    org = Organization.objects.create(slug="test_org")
    return build_http_context(user=user, organization=org, token="test")

Principals left unset raise on access, exactly as an unauthenticated request does. For subscriptions there is build_ws_context(consumer, ...).

Building on kante

Kante is the bottom layer: it must not import anything above it. Auth and audit live in authentikate and koherent, which build on it.

Download files

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

Source Distribution

kante-2.2.0.tar.gz (37.2 kB view details)

Uploaded Source

Built Distribution

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

kante-2.2.0-py3-none-any.whl (44.1 kB view details)

Uploaded Python 3

File details

Details for the file kante-2.2.0.tar.gz.

File metadata

  • Download URL: kante-2.2.0.tar.gz
  • Upload date:
  • Size: 37.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","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}

File hashes

Hashes for kante-2.2.0.tar.gz
Algorithm Hash digest
SHA256 d405ef92140a09fbf171217231aefb7b917990099cda827497efc42cd50fc37a
MD5 1b7322ef1b1316880de84ac4e563100e
BLAKE2b-256 79f1f58a2d51f8f9df8458b42531ca983721897492cab7ca34bbe08c0317bfe7

See more details on using hashes here.

File details

Details for the file kante-2.2.0-py3-none-any.whl.

File metadata

  • Download URL: kante-2.2.0-py3-none-any.whl
  • Upload date:
  • Size: 44.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","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}

File hashes

Hashes for kante-2.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 aa83bc40a9af29064bf43591d9db2add6ea4503c872d8fbc8ac98dfda2f2feb4
MD5 a6f0a68388cbac1e142c25258b417c7c
BLAKE2b-256 0af9c38ca2d85eeaba8a6ac7db218971144dcc846988430f1df882361507ca79

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

2.2.0 This release

2 files

2.1.1

2 files

2.1.0

2 files

2.0.1

2 files

2.0.0

2 files

1.3.0

2 files

1.2.0

2 files

1.1.5

2 files

1.1.4

2 files

1.1.3

2 files

1.1.2

2 files

1.1.1

2 files

1.1.0

2 files

1.0.0

2 files

0.16.1

2 files

0.16.0

2 files

0.15.0

2 files

0.14.0

2 files

0.13.0

2 files

0.12.1

2 files

0.12.0

2 files

0.11.0

2 files

0.10.1

2 files

0.10.0

2 files

0.9.0

2 files

0.8.0

2 files

0.7.0

2 files

0.6.0

2 files

0.5.0

2 files

0.4.0

2 files

0.3.0

2 files

0.2.1

2 files

0.2.0

2 files

0.1.7

2 files

0.1.6

2 files

0.1.5

2 files

0.1.4

2 files

0.1.3

2 files

0.1.2

2 files

0.1.1

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