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, get_context=True)) -> 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, get_context=True), 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, get_context=True) 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.
Metadata
Release files for sillo-graphql 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 | Provenance | Actions |
|---|---|---|---|---|
| sillo_graphql-1.0.0.tar.gz | 145.3 kB | Without provenance | Details for sillo_graphql-1.0.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| sillo_graphql-1.0.0-py3-none-any.whl | Python 3 | none | any | Details for sillo_graphql-1.0.0-py3-none-any.whl |
Total release size: 207.2 kB
Release files / sillo_graphql-1.0.0.tar.gz
| Download URL | sillo_graphql-1.0.0.tar.gz |
|---|---|
| Size | 145.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
1872af2c47d2d36e67cb019939fbad0bffc0e250642a14541ac45097f7f86684
|
|
BLAKE2b-256 checksum How to use checksums |
77a943804be06939d4c61c5cb0d8f9db4683531a103040ae4733ab051de2f72b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.19 {"installer":{"name":"uv","version":"0.12.19","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 / sillo_graphql-1.0.0-py3-none-any.whl
| Download URL | sillo_graphql-1.0.0-py3-none-any.whl |
|---|---|
| Size | 61.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
83402a805e68c7f14269f7e73d262c0b33735f611dbd5f54b8499ad1689c03d3
|
|
BLAKE2b-256 checksum How to use checksums |
cbd8b00e7895ee871c9e51b96313f73c5f23fbd1e75528eef57f7c3871150687
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.19 {"installer":{"name":"uv","version":"0.12.19","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}
|
Security for sillo-graphql 1.0.0
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.
No provenance found
None of the files in this release were uploaded with provenance metadata.
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.