sillo-graphql 1.0.0a1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. sillo_graphql-1.0.0a1/.github/workflows/ci.yml +165 -0
  2. sillo_graphql-1.0.0a1/.github/workflows/release.yml +90 -0
  3. sillo_graphql-1.0.0a1/.gitignore +15 -0
  4. sillo_graphql-1.0.0a1/CHANGELOG.md +124 -0
  5. sillo_graphql-1.0.0a1/LICENSE +27 -0
  6. sillo_graphql-1.0.0a1/PKG-INFO +250 -0
  7. sillo_graphql-1.0.0a1/README.md +215 -0
  8. sillo_graphql-1.0.0a1/pyproject.toml +106 -0
  9. sillo_graphql-1.0.0a1/sillo_graphql/__init__.py +103 -0
  10. sillo_graphql-1.0.0a1/sillo_graphql/context.py +220 -0
  11. sillo_graphql-1.0.0a1/sillo_graphql/errors.py +177 -0
  12. sillo_graphql-1.0.0a1/sillo_graphql/graph.py +723 -0
  13. sillo_graphql-1.0.0a1/sillo_graphql/ide.py +312 -0
  14. sillo_graphql-1.0.0a1/sillo_graphql/limits.py +403 -0
  15. sillo_graphql-1.0.0a1/sillo_graphql/loaders.py +323 -0
  16. sillo_graphql-1.0.0a1/sillo_graphql/persisted.py +242 -0
  17. sillo_graphql-1.0.0a1/sillo_graphql/policy.py +291 -0
  18. sillo_graphql-1.0.0a1/sillo_graphql/py.typed +0 -0
  19. sillo_graphql-1.0.0a1/sillo_graphql/resolvers.py +445 -0
  20. sillo_graphql-1.0.0a1/sillo_graphql/testing.py +341 -0
  21. sillo_graphql-1.0.0a1/sillo_graphql/tracing.py +173 -0
  22. sillo_graphql-1.0.0a1/sillo_graphql/transport/__init__.py +22 -0
  23. sillo_graphql-1.0.0a1/sillo_graphql/transport/http.py +460 -0
  24. sillo_graphql-1.0.0a1/sillo_graphql/transport/sse.py +86 -0
  25. sillo_graphql-1.0.0a1/sillo_graphql/transport/ws.py +276 -0
  26. sillo_graphql-1.0.0a1/tests/conftest.py +169 -0
  27. sillo_graphql-1.0.0a1/tests/test_context.py +144 -0
  28. sillo_graphql-1.0.0a1/tests/test_errors.py +96 -0
  29. sillo_graphql-1.0.0a1/tests/test_graph.py +512 -0
  30. sillo_graphql-1.0.0a1/tests/test_http.py +515 -0
  31. sillo_graphql-1.0.0a1/tests/test_ide.py +79 -0
  32. sillo_graphql-1.0.0a1/tests/test_limits.py +254 -0
  33. sillo_graphql-1.0.0a1/tests/test_loaders.py +292 -0
  34. sillo_graphql-1.0.0a1/tests/test_persisted.py +184 -0
  35. sillo_graphql-1.0.0a1/tests/test_policy.py +170 -0
  36. sillo_graphql-1.0.0a1/tests/test_resolvers.py +454 -0
  37. sillo_graphql-1.0.0a1/tests/test_sse.py +94 -0
  38. sillo_graphql-1.0.0a1/tests/test_testing.py +278 -0
  39. sillo_graphql-1.0.0a1/tests/test_tracing.py +165 -0
  40. sillo_graphql-1.0.0a1/tests/test_ws.py +379 -0
  41. sillo_graphql-1.0.0a1/uv.lock +974 -0
@@ -0,0 +1,165 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ # The framework this package extends, installed from the 1.0 branch rather
9
+ # than from PyPI: this package tracks `main`, not the last tag cut from it.
10
+ # (`sillo-framework>=1.0` does resolve on PyPI now that 1.0.0a1 is published,
11
+ # but only to that pre-release.)
12
+ #
13
+ # Installed first and explicitly, and this package then installed with
14
+ # `--no-deps`, so the declared floor is not re-resolved against PyPI.
15
+ env:
16
+ FRAMEWORK: "git+https://github.com/sillohq/core.git@main"
17
+
18
+ jobs:
19
+ lint:
20
+ runs-on: ubuntu-latest
21
+ steps:
22
+ - uses: actions/checkout@v4
23
+ - uses: actions/setup-python@v5
24
+ with:
25
+ python-version: "3.12"
26
+
27
+ - name: Install
28
+ run: |
29
+ python -m pip install --upgrade pip
30
+ pip install ruff mypy
31
+ pip install "$FRAMEWORK" strawberry-graphql
32
+
33
+ - name: Lint
34
+ run: ruff check sillo_graphql tests
35
+
36
+ - name: Format
37
+ run: ruff format --check sillo_graphql tests
38
+
39
+ - name: Types
40
+ run: mypy sillo_graphql
41
+
42
+ test:
43
+ runs-on: ubuntu-latest
44
+ strategy:
45
+ fail-fast: false
46
+ matrix:
47
+ python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
48
+
49
+ steps:
50
+ - uses: actions/checkout@v4
51
+ - uses: actions/setup-python@v5
52
+ with:
53
+ python-version: ${{ matrix.python-version }}
54
+
55
+ - name: Install
56
+ run: |
57
+ python -m pip install --upgrade pip
58
+ pip install "$FRAMEWORK" strawberry-graphql
59
+ pip install pytest pytest-asyncio pytest-cov httpx
60
+ pip install -e . --no-deps
61
+
62
+ # `fail_under = 100` lives in pyproject, so a line that stops being
63
+ # covered fails here rather than being noticed later.
64
+ - name: Test
65
+ run: pytest --cov --cov-report=term-missing
66
+
67
+ wheel:
68
+ name: sillo_graphql, from a built wheel
69
+ runs-on: ubuntu-latest
70
+ steps:
71
+ - uses: actions/checkout@v4
72
+ # The newest supported interpreter. The matrix already exercises the
73
+ # package on every version through the editable install, so the one place
74
+ # worth a separate job is the wheel path on the interpreter most likely
75
+ # to have changed underneath site-packages.
76
+ - uses: actions/setup-python@v5
77
+ with:
78
+ python-version: "3.14"
79
+
80
+ - name: Build the wheel
81
+ run: |
82
+ python -m pip install --upgrade pip build
83
+ python -m build --wheel -o dist .
84
+
85
+ # Two distributions writing into one package directory goes wrong in both
86
+ # directions, so the wheel must add a top-level `sillo_graphql` and
87
+ # nothing else. Checked on the artefact, not on an install, because an
88
+ # install can hide it behind whatever is already there.
89
+ - name: The wheel ships sillo_graphql only
90
+ run: |
91
+ python - <<'PY'
92
+ import glob
93
+ import zipfile
94
+
95
+ names = zipfile.ZipFile(glob.glob("dist/*.whl")[0]).namelist()
96
+ intruders = [name for name in names if name.startswith("sillo/")]
97
+ assert not intruders, f"wheel writes into sillo/: {intruders}"
98
+ strays = [n for n in names if n.endswith(".pth") or n.startswith("sillo-stubs/")]
99
+ assert not strays, f"alias machinery is back in the wheel: {strays}"
100
+ assert "sillo_graphql/py.typed" in names, "the PEP 561 marker is missing"
101
+ print("\n".join(sorted(names)))
102
+ PY
103
+
104
+ # Against a real install, not the checkout: an editable install can
105
+ # import what the wheel does not actually ship.
106
+ - name: sillo_graphql imports, and sillo.graphql does not
107
+ run: |
108
+ python -m venv /tmp/v
109
+ /tmp/v/bin/pip install -q "$FRAMEWORK" strawberry-graphql mypy
110
+ /tmp/v/bin/pip install -q --no-deps dist/*.whl
111
+ /tmp/v/bin/python - <<'PY'
112
+ import os
113
+
114
+ import sillo
115
+ import sillo_graphql # noqa: F401
116
+
117
+ package = os.path.dirname(sillo.__file__)
118
+ assert "graphql" not in os.listdir(package), "something wrote into sillo/"
119
+
120
+ # The package used to ship a .pth aliasing `sillo.graphql` onto it.
121
+ # That is gone, and the framework has not shipped a module of that
122
+ # name since 1.0, so the import must simply fail.
123
+ try:
124
+ import sillo.graphql # noqa: F401
125
+ except ImportError:
126
+ print("sillo_graphql imports; sillo.graphql does not resolve")
127
+ else:
128
+ raise AssertionError("sillo.graphql still resolves")
129
+ PY
130
+
131
+ # Types come from the package's own annotations plus py.typed now, not
132
+ # from a separate set of stubs that had to be kept in step with them.
133
+ - name: Type checkers resolve the package
134
+ run: |
135
+ cat > /tmp/check.py <<'PY'
136
+ from sillo_graphql import Graph, Limits, not_found
137
+
138
+ limits: Limits = Limits(depth=3)
139
+ error = not_found("gone")
140
+ graph: type[Graph] = Graph
141
+ PY
142
+ /tmp/v/bin/python -m mypy /tmp/check.py
143
+
144
+ # Removing either distribution must leave the other whole. The failure
145
+ # this guards against is an empty `sillo/` left standing with no
146
+ # __init__.py in it, which shadows a later reinstall.
147
+ #
148
+ # Run from /tmp, not the checkout: with the repository on sys.path these
149
+ # would import the source tree and prove nothing about the install.
150
+ - name: Uninstalling either one leaves the other intact
151
+ working-directory: /tmp
152
+ run: |
153
+ /tmp/v/bin/pip uninstall -y -q sillo-graphql
154
+ /tmp/v/bin/python -c "import sillo; print('framework survives')"
155
+ /tmp/v/bin/pip install -q --no-deps $GITHUB_WORKSPACE/dist/*.whl
156
+ /tmp/v/bin/pip uninstall -y -q sillo-framework
157
+ /tmp/v/bin/python - <<'PY'
158
+ import importlib.util
159
+
160
+ # `sillo_graphql` needs the framework to import, so it is not asked
161
+ # to. What must be true is that nothing of `sillo` is left behind:
162
+ # a directory with no __init__.py in it would shadow a reinstall.
163
+ assert importlib.util.find_spec("sillo") is None, "headless sillo/ left behind"
164
+ print("no headless sillo/")
165
+ PY
@@ -0,0 +1,90 @@
1
+ name: release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'sillo-graphql-v*'
7
+
8
+ # The framework this package extends. `sillo-framework>=1.0` now resolves on
9
+ # PyPI -- 1.0.0a1 is published -- but a pre-release only, and this package
10
+ # tracks the 1.0 branch rather than the last tag cut from it. Install the
11
+ # framework from `main`, then this package with `--no-deps`.
12
+ env:
13
+ FRAMEWORK: "git+https://github.com/sillohq/core.git@main"
14
+
15
+ jobs:
16
+ publish:
17
+ runs-on: ubuntu-latest
18
+ permissions:
19
+ # Trusted publishing, used when PYPI_TOKEN is not set.
20
+ id-token: write
21
+
22
+ steps:
23
+ - uses: actions/checkout@v4
24
+
25
+ - name: Install uv
26
+ uses: astral-sh/setup-uv@v5
27
+
28
+ - uses: actions/setup-python@v5
29
+ with:
30
+ python-version: "3.12"
31
+
32
+ - name: Read the tag
33
+ id: version
34
+ run: echo "version=${GITHUB_REF_NAME#sillo-graphql-v}" >> "$GITHUB_OUTPUT"
35
+
36
+ # Three places carry the version; a release that reports one it is not is
37
+ # worse than one that fails to build.
38
+ - name: The tag, pyproject.toml and __version__ must agree
39
+ run: |
40
+ tag='${{ steps.version.outputs.version }}'
41
+ project=$(grep -m1 '^version = ' pyproject.toml | cut -d'"' -f2)
42
+ module=$(grep -m1 '^__version__' sillo_graphql/__init__.py | cut -d'"' -f2)
43
+
44
+ echo "tag=$tag pyproject=$project module=$module"
45
+
46
+ if [ "$tag" != "$project" ] || [ "$tag" != "$module" ]; then
47
+ echo "These disagree. Bump all three and tag again."
48
+ exit 1
49
+ fi
50
+
51
+ - name: Install
52
+ run: |
53
+ uv venv
54
+ uv pip install "$FRAMEWORK" strawberry-graphql
55
+ uv pip install pytest pytest-asyncio pytest-cov httpx
56
+ uv pip install -e . --no-deps
57
+
58
+ - name: Test
59
+ run: uv run pytest -q
60
+
61
+ - name: Build
62
+ run: uv build
63
+
64
+ # The collision this layout exists to avoid: two distributions writing
65
+ # into one package directory. Checked on the artefact, not on an install.
66
+ - name: The wheel must write nothing into sillo/
67
+ run: |
68
+ python - <<'PY'
69
+ import glob
70
+ import zipfile
71
+
72
+ names = zipfile.ZipFile(glob.glob("dist/*.whl")[0]).namelist()
73
+ intruders = [n for n in names if n.startswith("sillo/")]
74
+ assert not intruders, f"wheel writes into sillo/: {intruders}"
75
+ assert "sillo_graphql/__init__.py" in names, "the package is missing"
76
+ assert "sillo_graphql/py.typed" in names, "the PEP 561 marker is missing"
77
+ strays = [n for n in names if n.endswith(".pth") or n.startswith("sillo-stubs/")]
78
+ assert not strays, f"alias machinery is back in the wheel: {strays}"
79
+ print("wheel ships sillo_graphql only, and stays out of sillo/")
80
+ PY
81
+
82
+ - name: Publish
83
+ env:
84
+ UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TOKEN }}
85
+ run: |
86
+ if [ -n "$UV_PUBLISH_TOKEN" ]; then
87
+ uv publish
88
+ else
89
+ uv publish --trusted-publishing always
90
+ fi
@@ -0,0 +1,15 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .coverage
8
+ .coverage.*
9
+ htmlcov/
10
+ .pytest_cache/
11
+ .ruff_cache/
12
+ .mypy_cache/
13
+ .DS_Store
14
+
15
+ .venv*/
@@ -0,0 +1,124 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
5
+ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [1.0.0a1] - 2026-09-13
10
+
11
+ First alpha, released alongside `sillo-framework` 1.0.0a1. Install with
12
+ `pip install --pre sillo-graphql==1.0.0a1`.
13
+
14
+ An alpha: this is what 1.0 is expected to look like, but the API is not frozen
15
+ yet and may still change before `1.0.0`.
16
+
17
+ ### Removed
18
+
19
+ - **The `sillo.graphql` import alias.** `sillo_graphql` is now the only import
20
+ path:
21
+
22
+ ```python
23
+ from sillo_graphql import Graph, field # was: from sillo.graphql import ...
24
+ ```
25
+
26
+ The alias was a meta-path finder in `_sillo_graphql_bootstrap.py`, a
27
+ `sillo_graphql.pth` registering it at interpreter startup, and PEP 561 stubs
28
+ under `sillo-stubs/` to serve type checkers, which never run import hooks.
29
+ That is three mechanisms, a `.pth` executing on every interpreter start in
30
+ every environment the package is installed in, and a second set of type
31
+ declarations to keep in step with the real ones — so that an import could
32
+ read as part of the framework. A plain top-level package needs none of it.
33
+
34
+ `_sillo_graphql_bootstrap.py`, `sillo_graphql.pth` and `sillo-stubs/` are
35
+ gone, along with the `force-include` blocks that shipped them.
36
+
37
+ ### Added
38
+
39
+ - **`sillo_graphql/py.typed`.** The package claimed `Typing :: Typed` but
40
+ shipped no PEP 561 marker of its own — type checkers were served entirely by
41
+ `sillo-stubs/`. Removing the stubs without this would have silently made the
42
+ package untyped for consumers. Its inline annotations are now the single
43
+ source of truth.
44
+
45
+ ### Fixed
46
+
47
+ - **Resolver `Depend(...)` stopped resolving on newer framework builds.** Sillo
48
+ v1's `get_dependant` treats a callable's first parameter as the context slot
49
+ and only scans the parameters after it for `Depend` markers. The stand-in
50
+ signature this package builds for a resolver's dependencies had no such slot,
51
+ so its first dependency was skipped and injected as `None`. It now carries a
52
+ leading context parameter. A resolver dependency that took no arguments must
53
+ now take a leading one (`def get_db(_): ...`), matching the framework's rule
54
+ for every dependency.
55
+
56
+ ### Added
57
+
58
+ - A `release` workflow: pushing a `sillo-graphql-v<version>` tag checks the
59
+ three version strings agree, runs the suite, builds, verifies the wheel
60
+ carries the alias `.pth` and the PEP 561 stubs and writes nothing into
61
+ `sillo/`, and publishes (trusted publishing, or `PYPI_TOKEN`).
62
+
63
+ ## [0.1.0]
64
+
65
+ First release. Extracted from the framework's `sillo.graphql` module, and
66
+ rebuilt around it rather than moved.
67
+
68
+ ### Added
69
+
70
+ - `Graph`, built and then mounted, matching how every other subsystem attaches
71
+ to an application. Mounts on a `SilloApp` or a `Router`.
72
+ - `field`, `mutation` and `subscription` decorators that adapt `sillo`'s
73
+ handler convention onto Strawberry resolvers: `ctx` and `Depend` parameters
74
+ are injected and stripped from the schema; everything else is a GraphQL
75
+ argument. Dependencies are resolved through the framework's own solver, so
76
+ one operation shares one dependency cache.
77
+ - `GraphContext`, a typed context carrying the connection, a response handle
78
+ for setting status, headers and cookies from a resolver, the loader registry
79
+ and the authenticated user. Also a `Mapping`, so `info.context["ctx"]` keeps
80
+ working.
81
+ - Static cost analysis before execution: depth, aliases, breadth, document
82
+ tokens, and a weighted cost with list multipliers derived from `first`,
83
+ `last`, `limit` and friends. `@field(cost=...)` prices a field.
84
+ - Error policy: free builders (`not_found`, `forbidden`, `unauthenticated`,
85
+ `bad_input`, `conflict`, `too_many_requests`, `internal`) with stable
86
+ `extensions.code`, masking of unexpected exceptions, `@graph.on_error`
87
+ mapping, and a correlation id on every error.
88
+ - Request-scoped `DataLoader` batching via `@graph.loader`, with cache
89
+ priming, `load_many`, batch-size chunking and per-key error results.
90
+ - Subscriptions over `graphql-transport-ws`, with an initialisation timeout,
91
+ ping/pong, duplicate-id detection and cancellation of every operation when
92
+ the socket closes. `@graph.on_connect` authenticates from the
93
+ `connection_init` payload.
94
+ - Subscriptions over `text/event-stream` (`sse=True`) for clients that cannot
95
+ hold a socket open.
96
+ - GraphQL-over-HTTP: content negotiation between
97
+ `application/graphql-response+json` and legacy `application/json`, correct
98
+ status codes, `GET` queries with mutations refused, `application/graphql`
99
+ bodies, form bodies, capped sequential batching, and body size limits.
100
+ - File uploads per the GraphQL multipart request spec, with per-file, per-count
101
+ and per-request size limits and a content-type allow-list. Off by default.
102
+ - Persisted operations: APQ with hash verification, and a trusted-document
103
+ manifest that refuses everything not in it.
104
+ - A bundled explorer with no external requests, so it works offline and under
105
+ a strict CSP. GraphiQL from a CDN remains available with `IDE(assets="cdn")`.
106
+ - `Metrics`, `OperationLog` and an OpenTelemetry hook, all through
107
+ `@graph.on_operation`.
108
+ - `GraphClient` and `SubscriptionStream` test helpers.
109
+ - `sillo.graphql` as an import alias for `sillo_graphql`, via a `.pth`-loaded
110
+ meta-path finder and PEP 561 partial stubs. Nothing is written into the
111
+ framework's package directory, and the alias refuses rather than shadows a
112
+ framework that still ships its own `sillo.graphql`.
113
+
114
+ ### Changed from the framework's `sillo.graphql`
115
+
116
+ - The explorer is **off** by default, and introspection with it.
117
+ - Resolver exceptions are **masked** by default.
118
+ - Depth and cost limits are **enforced** by default.
119
+ - A request with no document is a 400, not a 500.
120
+ - The endpoint is excluded from the OpenAPI document, which described it
121
+ incorrectly.
122
+
123
+ [Unreleased]: https://github.com/sillohq/graphql/compare/v0.1.0...HEAD
124
+ [0.1.0]: https://github.com/sillohq/graphql/releases/tag/v0.1.0
@@ -0,0 +1,27 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2024-present, sillo-Labs OSS.
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without modification,
7
+ are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+ 3. Neither the name of the copyright holder nor the names of its
15
+ contributors may be used to endorse or promote products derived from
16
+ this software without specific prior written permission.
17
+
18
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,250 @@
1
+ Metadata-Version: 2.5
2
+ Name: sillo-graphql
3
+ Version: 1.0.0a1
4
+ Summary: Production GraphQL for Sillo — sillo-style resolvers with DI, subscriptions, cost limits, persisted operations and batching.
5
+ Project-URL: Homepage, https://sillo.build
6
+ Project-URL: Documentation, https://docs.sillo.build/packages/graphql/
7
+ Project-URL: Source, https://github.com/sillohq/graphql
8
+ Project-URL: Changelog, https://github.com/sillohq/graphql/blob/main/CHANGELOG.md
9
+ Author-email: Chidebele Dunamis <techwithdunamix@gmail.com>
10
+ License-Expression: BSD-3-Clause
11
+ License-File: LICENSE
12
+ Keywords: api,asgi,dataloader,graphql,persisted-queries,sillo,strawberry,subscriptions
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Framework :: AsyncIO
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: BSD License
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: Topic :: Internet :: WWW/HTTP
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.10
25
+ Requires-Dist: sillo-framework>=1.0
26
+ Requires-Dist: strawberry-graphql>=0.219.0
27
+ Provides-Extra: dev
28
+ Requires-Dist: httpx>=0.27; extra == 'dev'
29
+ Requires-Dist: mypy>=1.11; extra == 'dev'
30
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
31
+ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
32
+ Requires-Dist: pytest>=8.0; extra == 'dev'
33
+ Requires-Dist: ruff>=0.6; extra == 'dev'
34
+ Description-Content-Type: text/markdown
35
+
36
+ # sillo-graphql
37
+
38
+ Production GraphQL for [Sillo](https://sillo.build). Installs as
39
+ `sillo-graphql`, imports as `sillo_graphql`.
40
+
41
+ Strawberry owns the schema. This package owns everything around it — the
42
+ transports, the safety, and the observability.
43
+
44
+ ```bash
45
+ pip install sillo-graphql
46
+ ```
47
+
48
+ ```python
49
+ import strawberry
50
+ from sillo import Depend, HttpContext, SilloApp
51
+ from sillo_graphql import Graph, Limits, field
52
+
53
+ @strawberry.type
54
+ class Query:
55
+ @field
56
+ async def me(ctx: HttpContext, db=Depend(get_db)) -> User:
57
+ return await db.users.get(ctx.user.id)
58
+
59
+ app = SilloApp()
60
+ Graph(strawberry.Schema(query=Query), limits=Limits(depth=8)).mount(app)
61
+ ```
62
+
63
+ ## Resolvers that read like handlers
64
+
65
+ A `sillo` route handler takes the context first and declares what else it
66
+ needs. So does a resolver here:
67
+
68
+ ```python
69
+ @field
70
+ async def posts(ctx: HttpContext, db=Depend(get_db), limit: int = 10) -> list[Post]:
71
+ return await db.posts.recent(limit)
72
+ ```
73
+
74
+ One rule: **`ctx` and anything defaulted to `Depend` are injected and never
75
+ appear in the schema; every other parameter is a GraphQL argument.** So this
76
+ field takes exactly one argument, `limit`.
77
+
78
+ Dependencies are resolved by the framework's own solver, with the framework's
79
+ own pre-flattened execution plan — two resolvers in one operation that both
80
+ ask for `Depend(get_db)` are handed the same session.
81
+
82
+ ## Configuration
83
+
84
+ ```python
85
+ graph = Graph(
86
+ schema,
87
+ path="/graphql",
88
+ ide=False, # explorer, off by default
89
+ introspection=False, # off by default
90
+ subscriptions=True,
91
+ auth=Bearer(), # the route's auth= gate
92
+ limits=Limits(depth=10, cost=1_000, aliases=15),
93
+ errors=ErrorPolicy(mask=True),
94
+ transport=Transport(get_queries=True, batch=10),
95
+ uploads=Uploads(enabled=True, max_size="10MB"),
96
+ persisted=Persisted(apq=True, trusted="operations.json"),
97
+ )
98
+ graph.mount(app)
99
+ ```
100
+
101
+ Common knobs are keyword arguments; the deeper ones are policy objects, the
102
+ same split the framework makes between arguments on `SilloApp` and objects
103
+ like `CSRFConfig`.
104
+
105
+ Every default is chosen for a public endpoint.
106
+
107
+ ## What it does
108
+
109
+ ### Cost limits, enforced before execution
110
+
111
+ Depth, aliases, breadth and document size, plus a weighted cost that
112
+ understands lists — a field returning a list multiplies everything under it, by
113
+ the page size the caller asked for when that is knowable.
114
+
115
+ ```python
116
+ @field(cost=25)
117
+ async def search(ctx: HttpContext, term: str) -> list[Hit]: ...
118
+ ```
119
+
120
+ An operation over budget is refused with `OPERATION_TOO_COMPLEX` and the limit
121
+ it passed, before a single resolver runs. Refusing afterwards would mean having
122
+ already done the work.
123
+
124
+ ### Errors that say what happened, and no more
125
+
126
+ ```python
127
+ from sillo_graphql import forbidden, not_found
128
+
129
+ @field
130
+ async def post(ctx: HttpContext, id: int) -> Post:
131
+ found = await Post.objects.get_or_none(id=id)
132
+ if found is None:
133
+ raise not_found("No such post") # extensions.code == "NOT_FOUND"
134
+ return found
135
+ ```
136
+
137
+ Free builders, like the framework's `json()` and `text()`. Errors raised this
138
+ way are deliberate and reach the client. An exception that escapes a resolver
139
+ is masked, logged with its traceback, and reported as `INTERNAL_SERVER_ERROR` —
140
+ because what it said may name a host, a table or a credential.
141
+
142
+ Map your own:
143
+
144
+ ```python
145
+ @graph.on_error(RecordNotFound)
146
+ def _(exc): return not_found(str(exc))
147
+ ```
148
+
149
+ ### Batching, so a graph query is not a table scan per node
150
+
151
+ ```python
152
+ @graph.loader
153
+ async def load_author(keys: list[int]) -> list[User]:
154
+ rows = await User.objects.filter(id__in=keys).all()
155
+ return align(rows, keys)
156
+
157
+ @field
158
+ async def author(ctx: HttpContext, root: Post) -> User:
159
+ return await load_author(root.author_id)
160
+ ```
161
+
162
+ Keys asked for by sibling fields in the same tick become one call. State is per
163
+ operation, so two concurrent requests never share a cache.
164
+
165
+ ### Subscriptions that exist
166
+
167
+ `graphql-transport-ws` over the framework's own WebSocket layer, with an
168
+ initialisation timeout, ping/pong keepalive, and cancellation in a `finally` so
169
+ an operation cannot outlive its socket. Authentication belongs in
170
+ `connection_init`, because a browser cannot set headers on a WebSocket
171
+ handshake:
172
+
173
+ ```python
174
+ @graph.on_connect
175
+ async def authenticate(socket, params):
176
+ token = params.get("authorization")
177
+ if not token:
178
+ raise unauthenticated("A token is required")
179
+ return {"user": await user_for(token)}
180
+ ```
181
+
182
+ The same subscriptions are available over `text/event-stream` with `sse=True`,
183
+ for clients that cannot hold a socket open.
184
+
185
+ ### The rest of the HTTP surface
186
+
187
+ Batched operations (capped, sequential), `GET` for queries with mutations
188
+ refused, `application/graphql` bodies, file uploads per the multipart request
189
+ spec, and content negotiation between `application/graphql-response+json` — the
190
+ spec's status codes — and legacy `application/json`, which stays always-200 for
191
+ the clients that expect it.
192
+
193
+ ### Persisted operations
194
+
195
+ APQ saves bandwidth. A trusted-document manifest is the one that matters: with
196
+ `Persisted(trusted="operations.json")` the endpoint executes nothing else, so
197
+ the workload becomes finite and known.
198
+
199
+ ### Knowing what it is doing
200
+
201
+ ```python
202
+ graph.on_operation(OperationLog(slower_than=0.5))
203
+
204
+ metrics = Metrics()
205
+ graph.on_operation(metrics)
206
+ ```
207
+
208
+ Per operation, not per path: `p99` on `POST /graphql` averages over work that
209
+ has nothing in common.
210
+
211
+ ## Testing
212
+
213
+ ```python
214
+ from sillo_graphql.testing import GraphClient
215
+
216
+ def test_me():
217
+ with GraphClient(app) as gql:
218
+ result = gql.query("{ me { email } }")
219
+ assert result.ok
220
+ assert result["me"]["email"] == "a@b.c"
221
+
222
+ async def test_prices():
223
+ async with GraphClient(app).subscribe(PRICES, symbol="ACME") as stream:
224
+ assert (await stream.next())["prices"]["last"] == 10
225
+ ```
226
+
227
+ ## Migrating from `sillo.graphql` in the framework
228
+
229
+ | before | now |
230
+ | --- | --- |
231
+ | `GraphQL(app, schema, path=, graphiql=True)` | `Graph(schema, path=, ide=False).mount(app)` |
232
+ | `info.context["ctx"]` | a `ctx: HttpContext` parameter |
233
+ | `self` / `info` resolver convention | `ctx` first, like every handler |
234
+ | — | `Depend(...)`, `@graph.loader`, `@graph.on_error` |
235
+ | errors leaked, IDE on, no limits | masked, IDE off, depth and cost enforced |
236
+
237
+ `info.context["ctx"]` still works — the context is a `Mapping` — so a schema
238
+ can migrate one resolver at a time.
239
+
240
+ ## Requirements
241
+
242
+ Python 3.10+, `sillo-framework` 1.0 or newer, `strawberry-graphql`.
243
+
244
+ 1.0 is the floor because the resolver bridge is built on the context-handler
245
+ API. Versions before 1.0 also shipped a `sillo.graphql` of their own, which is
246
+ unrelated to this package and is what the table above migrates from.
247
+
248
+ ## License
249
+
250
+ BSD-3-Clause.