Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

sillo-graphql

Production GraphQL for Sillo. Installs as sillo-graphql, imports as sillo_graphql.

Strawberry owns the schema. This package owns everything around it — the transports, the safety, and the observability.

pip install sillo-graphql
import strawberry
from sillo import Depend, HttpContext, SilloApp
from sillo_graphql import Graph, Limits, field

@strawberry.type
class Query:
    @field
    async def me(ctx: HttpContext, db=Depend(get_db)) -> User:
        return await db.users.get(ctx.user.id)

app = SilloApp()
Graph(strawberry.Schema(query=Query), limits=Limits(depth=8)).mount(app)

Resolvers that read like handlers

A sillo route handler takes the context first and declares what else it needs. So does a resolver here:

@field
async def posts(ctx: HttpContext, db=Depend(get_db), limit: int = 10) -> list[Post]:
    return await db.posts.recent(limit)

One rule: ctx and anything defaulted to Depend are injected and never appear in the schema; every other parameter is a GraphQL argument. So this field takes exactly one argument, limit.

Dependencies are resolved by the framework's own solver, with the framework's own pre-flattened execution plan — two resolvers in one operation that both ask for Depend(get_db) are handed the same session.

Configuration

graph = Graph(
    schema,
    path="/graphql",
    ide=False,                                       # explorer, off by default
    introspection=False,                             # off by default
    subscriptions=True,
    auth=Bearer(),                                   # the route's auth= gate
    limits=Limits(depth=10, cost=1_000, aliases=15),
    errors=ErrorPolicy(mask=True),
    transport=Transport(get_queries=True, batch=10),
    uploads=Uploads(enabled=True, max_size="10MB"),
    persisted=Persisted(apq=True, trusted="operations.json"),
)
graph.mount(app)

Common knobs are keyword arguments; the deeper ones are policy objects, the same split the framework makes between arguments on SilloApp and objects like CSRFConfig.

Every default is chosen for a public endpoint.

What it does

Cost limits, enforced before execution

Depth, aliases, breadth and document size, plus a weighted cost that understands lists — a field returning a list multiplies everything under it, by the page size the caller asked for when that is knowable.

@field(cost=25)
async def search(ctx: HttpContext, term: str) -> list[Hit]: ...

An operation over budget is refused with OPERATION_TOO_COMPLEX and the limit it passed, before a single resolver runs. Refusing afterwards would mean having already done the work.

Errors that say what happened, and no more

from sillo_graphql import forbidden, not_found

@field
async def post(ctx: HttpContext, id: int) -> Post:
    found = await Post.objects.get_or_none(id=id)
    if found is None:
        raise not_found("No such post")     # extensions.code == "NOT_FOUND"
    return found

Free builders, like the framework's json() and text(). Errors raised this way are deliberate and reach the client. An exception that escapes a resolver is masked, logged with its traceback, and reported as INTERNAL_SERVER_ERROR — because what it said may name a host, a table or a credential.

Map your own:

@graph.on_error(RecordNotFound)
def _(exc): return not_found(str(exc))

Batching, so a graph query is not a table scan per node

@graph.loader
async def load_author(keys: list[int]) -> list[User]:
    rows = await User.objects.filter(id__in=keys).all()
    return align(rows, keys)

@field
async def author(ctx: HttpContext, root: Post) -> User:
    return await load_author(root.author_id)

Keys asked for by sibling fields in the same tick become one call. State is per operation, so two concurrent requests never share a cache.

Subscriptions that exist

graphql-transport-ws over the framework's own WebSocket layer, with an initialisation timeout, ping/pong keepalive, and cancellation in a finally so an operation cannot outlive its socket. Authentication belongs in connection_init, because a browser cannot set headers on a WebSocket handshake:

@graph.on_connect
async def authenticate(socket, params):
    token = params.get("authorization")
    if not token:
        raise unauthenticated("A token is required")
    return {"user": await user_for(token)}

The same subscriptions are available over text/event-stream with sse=True, for clients that cannot hold a socket open.

The rest of the HTTP surface

Batched operations (capped, sequential), GET for queries with mutations refused, application/graphql bodies, file uploads per the multipart request spec, and content negotiation between application/graphql-response+json — the spec's status codes — and legacy application/json, which stays always-200 for the clients that expect it.

Persisted operations

APQ saves bandwidth. A trusted-document manifest is the one that matters: with Persisted(trusted="operations.json") the endpoint executes nothing else, so the workload becomes finite and known.

Knowing what it is doing

graph.on_operation(OperationLog(slower_than=0.5))

metrics = Metrics()
graph.on_operation(metrics)

Per operation, not per path: p99 on POST /graphql averages over work that has nothing in common.

Testing

from sillo_graphql.testing import GraphClient

def test_me():
    with GraphClient(app) as gql:
        result = gql.query("{ me { email } }")
        assert result.ok
        assert result["me"]["email"] == "a@b.c"

async def test_prices():
    async with GraphClient(app).subscribe(PRICES, symbol="ACME") as stream:
        assert (await stream.next())["prices"]["last"] == 10

Migrating from sillo.graphql in the framework

before now
GraphQL(app, schema, path=, graphiql=True) Graph(schema, path=, ide=False).mount(app)
info.context["ctx"] a ctx: HttpContext parameter
self / info resolver convention ctx first, like every handler
Depend(...), @graph.loader, @graph.on_error
errors leaked, IDE on, no limits masked, IDE off, depth and cost enforced

info.context["ctx"] still works — the context is a Mapping — so a schema can migrate one resolver at a time.

Requirements

Python 3.10+, sillo-framework 1.0 or newer, strawberry-graphql.

1.0 is the floor because the resolver bridge is built on the context-handler API. Versions before 1.0 also shipped a sillo.graphql of their own, which is unrelated to this package and is what the table above migrates from.

License

BSD-3-Clause.

Download files

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

Source Distribution

sillo_graphql-1.0.0a1.tar.gz (145.2 kB view details)

Uploaded Source

Built Distribution

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

sillo_graphql-1.0.0a1-py3-none-any.whl (61.8 kB view details)

Uploaded Python 3

File details

Details for the file sillo_graphql-1.0.0a1.tar.gz.

File metadata

  • Download URL: sillo_graphql-1.0.0a1.tar.gz
  • Upload date:
  • Size: 145.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.12.13 {"installer":{"name":"uv","version":"0.12.13","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 sillo_graphql-1.0.0a1.tar.gz
Algorithm Hash digest
SHA256 96747313f68525722f2424d5b4a95f32d5f4219f7c73e40f0abe6dbf4e694526
MD5 f5580c9617f0bfba52c5461df1a7da8b
BLAKE2b-256 7c33e320ec909cbfd90d96915fe7f0b33e24df912d14d28c9a0379be254760a4

See more details on using hashes here.

File details

Details for the file sillo_graphql-1.0.0a1-py3-none-any.whl.

File metadata

  • Download URL: sillo_graphql-1.0.0a1-py3-none-any.whl
  • Upload date:
  • Size: 61.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.12.13 {"installer":{"name":"uv","version":"0.12.13","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 sillo_graphql-1.0.0a1-py3-none-any.whl
Algorithm Hash digest
SHA256 96e677365a657b120c2e57fd519ec2368fbf026c59b6073c30092961fb12ed69
MD5 bcca3e980003e946611953890dc63c6e
BLAKE2b-256 93a4747c043137a055a99e3ab76412031f2b4cdf1f355a4ead4434bd36324454

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

1.0.0a1 This release

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