graphql-http 2.3.3__tar.gz → 2.4.0__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.
- graphql_http-2.4.0/PKG-INFO +256 -0
- graphql_http-2.4.0/README.md +228 -0
- graphql_http-2.4.0/VERSION +1 -0
- {graphql_http-2.3.3 → graphql_http-2.4.0}/graphql_http/introspection.py +6 -2
- {graphql_http-2.3.3 → graphql_http-2.4.0}/graphql_http/server.py +418 -3
- graphql_http-2.4.0/graphql_http.egg-info/PKG-INFO +256 -0
- {graphql_http-2.3.3 → graphql_http-2.4.0}/graphql_http.egg-info/SOURCES.txt +2 -1
- {graphql_http-2.3.3 → graphql_http-2.4.0}/graphql_http.egg-info/requires.txt +3 -3
- {graphql_http-2.3.3 → graphql_http-2.4.0}/pyproject.toml +3 -3
- {graphql_http-2.3.3 → graphql_http-2.4.0}/tests/test_federation.py +1 -1
- graphql_http-2.4.0/tests/test_sse.py +889 -0
- {graphql_http-2.3.3 → graphql_http-2.4.0}/uv.lock +139 -131
- graphql_http-2.3.3/PKG-INFO +0 -134
- graphql_http-2.3.3/README.md +0 -106
- graphql_http-2.3.3/VERSION +0 -1
- graphql_http-2.3.3/graphql_http.egg-info/PKG-INFO +0 -134
- {graphql_http-2.3.3 → graphql_http-2.4.0}/.coverage +0 -0
- {graphql_http-2.3.3 → graphql_http-2.4.0}/.github/workflows/pages.yml +0 -0
- {graphql_http-2.3.3 → graphql_http-2.4.0}/.github/workflows/python-package.yml +0 -0
- {graphql_http-2.3.3 → graphql_http-2.4.0}/.gitignore +0 -0
- {graphql_http-2.3.3 → graphql_http-2.4.0}/.gitmodules +0 -0
- {graphql_http-2.3.3 → graphql_http-2.4.0}/AGENTS.md +0 -0
- {graphql_http-2.3.3 → graphql_http-2.4.0}/CLAUDE.md +0 -0
- {graphql_http-2.3.3 → graphql_http-2.4.0}/LICENSE +0 -0
- {graphql_http-2.3.3 → graphql_http-2.4.0}/docs/.gitmodules +0 -0
- {graphql_http-2.3.3 → graphql_http-2.4.0}/docs/content/_index.md +0 -0
- {graphql_http-2.3.3 → graphql_http-2.4.0}/docs/content/docs/_index.md +0 -0
- {graphql_http-2.3.3 → graphql_http-2.4.0}/docs/content/docs/api-reference.md +0 -0
- {graphql_http-2.3.3 → graphql_http-2.4.0}/docs/content/docs/authentication.md +0 -0
- {graphql_http-2.3.3 → graphql_http-2.4.0}/docs/content/docs/configuration.md +0 -0
- {graphql_http-2.3.3 → graphql_http-2.4.0}/docs/content/docs/examples.md +0 -0
- {graphql_http-2.3.3 → graphql_http-2.4.0}/docs/content/docs/getting-started.md +0 -0
- {graphql_http-2.3.3 → graphql_http-2.4.0}/docs/content/docs/testing.md +0 -0
- {graphql_http-2.3.3 → graphql_http-2.4.0}/docs/hugo.toml +0 -0
- {graphql_http-2.3.3 → graphql_http-2.4.0}/docs/index.md +0 -0
- {graphql_http-2.3.3 → graphql_http-2.4.0}/example_advanced_server.py +0 -0
- {graphql_http-2.3.3 → graphql_http-2.4.0}/examples/README.md +0 -0
- {graphql_http-2.3.3 → graphql_http-2.4.0}/examples/__init__.py +0 -0
- {graphql_http-2.3.3 → graphql_http-2.4.0}/examples/advanced_server.py +0 -0
- {graphql_http-2.3.3 → graphql_http-2.4.0}/examples/auth_server.py +0 -0
- {graphql_http-2.3.3 → graphql_http-2.4.0}/examples/basic_server.py +0 -0
- {graphql_http-2.3.3 → graphql_http-2.4.0}/examples/graphql_api_server.py +0 -0
- {graphql_http-2.3.3 → graphql_http-2.4.0}/graphql_http/__init__.py +0 -0
- {graphql_http-2.3.3 → graphql_http-2.4.0}/graphql_http/error.py +0 -0
- {graphql_http-2.3.3 → graphql_http-2.4.0}/graphql_http/graphiql/index.html +0 -0
- {graphql_http-2.3.3 → graphql_http-2.4.0}/graphql_http/helpers.py +0 -0
- {graphql_http-2.3.3 → graphql_http-2.4.0}/graphql_http.egg-info/dependency_links.txt +0 -0
- {graphql_http-2.3.3 → graphql_http-2.4.0}/graphql_http.egg-info/top_level.txt +0 -0
- {graphql_http-2.3.3 → graphql_http-2.4.0}/setup.cfg +0 -0
- {graphql_http-2.3.3 → graphql_http-2.4.0}/test_server.py +0 -0
- {graphql_http-2.3.3 → graphql_http-2.4.0}/tests/__init__.py +0 -0
- {graphql_http-2.3.3 → graphql_http-2.4.0}/tests/app.py +0 -0
- {graphql_http-2.3.3 → graphql_http-2.4.0}/tests/app_federation.py +0 -0
- {graphql_http-2.3.3 → graphql_http-2.4.0}/tests/conftest.py +0 -0
- {graphql_http-2.3.3 → graphql_http-2.4.0}/tests/test_auth.py +0 -0
- {graphql_http-2.3.3 → graphql_http-2.4.0}/tests/test_cors.py +0 -0
- {graphql_http-2.3.3 → graphql_http-2.4.0}/tests/test_examples.py +0 -0
- {graphql_http-2.3.3 → graphql_http-2.4.0}/tests/test_graphql_api.py +0 -0
- {graphql_http-2.3.3 → graphql_http-2.4.0}/tests/test_introspection.py +0 -0
- {graphql_http-2.3.3 → graphql_http-2.4.0}/tests/test_linting.py +0 -0
- {graphql_http-2.3.3 → graphql_http-2.4.0}/tests/test_server.py +0 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: graphql-http
|
|
3
|
+
Version: 2.4.0
|
|
4
|
+
Summary: HTTP Server for GraphQL.
|
|
5
|
+
Author-email: Robert Parker <rob@parob.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://gitlab.com/parob/graphql-http
|
|
8
|
+
Project-URL: Repository, https://gitlab.com/parob/graphql-http
|
|
9
|
+
Keywords: GraphQL,HTTP,Server,starlette
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Requires-Python: <3.13,>=3.11
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: graphql-core
|
|
19
|
+
Requires-Dist: graphql-api>=1.6.9
|
|
20
|
+
Requires-Dist: packaging
|
|
21
|
+
Requires-Dist: graphql-schema-diff
|
|
22
|
+
Requires-Dist: pyjwt[crypto]
|
|
23
|
+
Requires-Dist: starlette>=1.0.1
|
|
24
|
+
Requires-Dist: uvicorn
|
|
25
|
+
Requires-Dist: python-multipart>=0.0.27
|
|
26
|
+
Requires-Dist: httpx
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# GraphQL HTTP
|
|
30
|
+
|
|
31
|
+
[](https://badge.fury.io/py/graphql-http)
|
|
32
|
+
[](https://pypi.org/project/graphql-http/)
|
|
33
|
+
[](https://opensource.org/licenses/MIT)
|
|
34
|
+
|
|
35
|
+
**[📚 Documentation](https://graphql-http.parob.com/)** | **[📦 PyPI](https://pypi.org/project/graphql-http/)** | **[🔧 GitHub](https://github.com/parob/graphql-http)**
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
A lightweight, production-ready HTTP server for GraphQL APIs built on top of Starlette/FastAPI. This server provides a simple yet powerful way to serve GraphQL schemas over HTTP with built-in support for authentication, CORS, GraphiQL integration, and more.
|
|
40
|
+
|
|
41
|
+
## Features
|
|
42
|
+
|
|
43
|
+
- 🚀 **High Performance**: Built on Starlette/ASGI for excellent async performance
|
|
44
|
+
- 🔐 **JWT Authentication**: Built-in JWT authentication with JWKS support
|
|
45
|
+
- 🌐 **CORS Support**: Configurable CORS middleware for cross-origin requests
|
|
46
|
+
- 🎨 **GraphiQL Integration**: Interactive GraphQL IDE for development
|
|
47
|
+
- 📊 **Health Checks**: Built-in health check endpoints
|
|
48
|
+
- 🔄 **Batch Queries**: Support for batched GraphQL operations
|
|
49
|
+
- 📡 **Subscriptions**: GraphQL subscriptions streamed over Server-Sent Events ([graphql-sse](https://github.com/enisdenjo/graphql-sse/blob/master/PROTOCOL.md) compatible)
|
|
50
|
+
|
|
51
|
+
## Installation
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
uv add graphql_http
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Or with pip:
|
|
58
|
+
```bash
|
|
59
|
+
pip install graphql_http
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Quick Start
|
|
63
|
+
|
|
64
|
+
### Basic Usage
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from graphql import GraphQLSchema, GraphQLObjectType, GraphQLField, GraphQLString
|
|
68
|
+
from graphql_http import GraphQLHTTP
|
|
69
|
+
|
|
70
|
+
# Define your GraphQL schema
|
|
71
|
+
schema = GraphQLSchema(
|
|
72
|
+
query=GraphQLObjectType(
|
|
73
|
+
name="Query",
|
|
74
|
+
fields={
|
|
75
|
+
"hello": GraphQLField(
|
|
76
|
+
GraphQLString,
|
|
77
|
+
resolve=lambda obj, info: "Hello, World!"
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# Create the HTTP server
|
|
84
|
+
app = GraphQLHTTP(schema=schema)
|
|
85
|
+
|
|
86
|
+
# Run the server
|
|
87
|
+
if __name__ == "__main__":
|
|
88
|
+
app.run(host="0.0.0.0", port=8000)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Using with graphql-api
|
|
92
|
+
|
|
93
|
+
For building GraphQL schemas, use [graphql-api](https://graphql-api.parob.com/):
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from graphql_api import GraphQLAPI
|
|
97
|
+
from graphql_http import GraphQLHTTP
|
|
98
|
+
|
|
99
|
+
api = GraphQLAPI()
|
|
100
|
+
|
|
101
|
+
@api.type(is_root_type=True)
|
|
102
|
+
class Query:
|
|
103
|
+
@api.field
|
|
104
|
+
def hello(self, name: str = "World") -> str:
|
|
105
|
+
return f"Hello, {name}!"
|
|
106
|
+
|
|
107
|
+
server = GraphQLHTTP.from_api(api)
|
|
108
|
+
server.run()
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Subscriptions (Server-Sent Events)
|
|
112
|
+
|
|
113
|
+
GraphQL subscriptions are served over Server-Sent Events, following the
|
|
114
|
+
[GraphQL over SSE protocol](https://github.com/enisdenjo/graphql-sse/blob/master/PROTOCOL.md)
|
|
115
|
+
in **distinct connections mode**: each operation gets its own SSE connection, results
|
|
116
|
+
stream as `next` events, and a final `complete` event signals the end of the stream.
|
|
117
|
+
|
|
118
|
+
Any request whose `Accept` header includes `text/event-stream` (with a non-zero
|
|
119
|
+
q-value) is answered over SSE — subscriptions stream one `next` event per result,
|
|
120
|
+
while queries and mutations respond with a single `next` followed by `complete`.
|
|
121
|
+
A valid subscription sent *without* `Accept: text/event-stream` is rejected with
|
|
122
|
+
`406 Not Acceptable`.
|
|
123
|
+
|
|
124
|
+
### Defining a subscription with graphql-api
|
|
125
|
+
|
|
126
|
+
Any field returning an `AsyncGenerator` becomes a subscription:
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
import asyncio
|
|
130
|
+
from typing import AsyncGenerator
|
|
131
|
+
|
|
132
|
+
from graphql_api import GraphQLAPI
|
|
133
|
+
from graphql_http import GraphQLHTTP
|
|
134
|
+
|
|
135
|
+
api = GraphQLAPI()
|
|
136
|
+
|
|
137
|
+
@api.type(is_root_type=True)
|
|
138
|
+
class Root:
|
|
139
|
+
@api.field
|
|
140
|
+
def hello(self) -> str:
|
|
141
|
+
return "world"
|
|
142
|
+
|
|
143
|
+
@api.field
|
|
144
|
+
async def countdown(self, start: int = 3) -> AsyncGenerator[int, None]:
|
|
145
|
+
while start >= 0:
|
|
146
|
+
yield start
|
|
147
|
+
start -= 1
|
|
148
|
+
await asyncio.sleep(1)
|
|
149
|
+
|
|
150
|
+
server = GraphQLHTTP.from_api(api)
|
|
151
|
+
server.run()
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Consuming with curl
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
curl -N \
|
|
158
|
+
-H "Accept: text/event-stream" \
|
|
159
|
+
-H "Content-Type: application/json" \
|
|
160
|
+
-d '{"query": "subscription { countdown(start: 3) }"}' \
|
|
161
|
+
http://localhost:5000/graphql
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
```
|
|
165
|
+
event: next
|
|
166
|
+
data: {"data":{"countdown":3}}
|
|
167
|
+
|
|
168
|
+
event: next
|
|
169
|
+
data: {"data":{"countdown":2}}
|
|
170
|
+
|
|
171
|
+
event: next
|
|
172
|
+
data: {"data":{"countdown":1}}
|
|
173
|
+
|
|
174
|
+
event: next
|
|
175
|
+
data: {"data":{"countdown":0}}
|
|
176
|
+
|
|
177
|
+
event: complete
|
|
178
|
+
data:
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Consuming with the graphql-sse JavaScript client
|
|
182
|
+
|
|
183
|
+
The [`graphql-sse`](https://github.com/enisdenjo/graphql-sse) client uses distinct
|
|
184
|
+
connections mode by default (`singleConnection: false`):
|
|
185
|
+
|
|
186
|
+
```javascript
|
|
187
|
+
import { createClient } from 'graphql-sse';
|
|
188
|
+
|
|
189
|
+
const client = createClient({
|
|
190
|
+
url: 'http://localhost:5000/graphql',
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const unsubscribe = client.subscribe(
|
|
194
|
+
{ query: 'subscription { countdown(start: 3) }' },
|
|
195
|
+
{
|
|
196
|
+
next: (result) => console.log(result), // { data: { countdown: 3 } } ...
|
|
197
|
+
error: (error) => console.error(error),
|
|
198
|
+
complete: () => console.log('done'),
|
|
199
|
+
},
|
|
200
|
+
);
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Behavior notes
|
|
204
|
+
|
|
205
|
+
- **Auth**: SSE requests go through the same JWT/auth enforcement as regular
|
|
206
|
+
requests. Authentication failures are returned as HTTP-level JSON errors
|
|
207
|
+
(401/403) before any stream is opened. The introspection auth-bypass never
|
|
208
|
+
applies to subscription operations.
|
|
209
|
+
- **Errors**: per the graphql-sse protocol, errors raised before execution
|
|
210
|
+
starts — missing query, parse errors, validation errors, and `subscribe()`
|
|
211
|
+
failures — are delivered over the accepted `200 text/event-stream` response
|
|
212
|
+
as a `next` event carrying the errors, followed by `complete` (a `400` would
|
|
213
|
+
leave e.g. a browser `EventSource` with no error detail). Resolver errors
|
|
214
|
+
during the stream ride inside `next` payloads as standard GraphQL errors; a
|
|
215
|
+
fatal source error emits a final `next` carrying the error, then `complete`.
|
|
216
|
+
- **Middleware & execution context**: subscription events resolve through the
|
|
217
|
+
same `middleware` chain and `execution_context_class` as queries and
|
|
218
|
+
mutations, so field-authorization or error-masking middleware applies to
|
|
219
|
+
streamed results too.
|
|
220
|
+
- **Keep-alive**: while a stream is idle the server emits `: ping` SSE comments
|
|
221
|
+
every 15 seconds so intermediary proxies don't drop the connection. Configure
|
|
222
|
+
with `GraphQLHTTP(..., sse_keepalive_interval=30.0)` (must be positive;
|
|
223
|
+
`None` disables pings).
|
|
224
|
+
- **Concurrency limit**: at most `sse_max_streams` subscription streams
|
|
225
|
+
(default 100) may be open concurrently per server; further subscription
|
|
226
|
+
requests are rejected with `429 Too Many Requests` until a slot frees up.
|
|
227
|
+
Pass `sse_max_streams=None` to remove the limit.
|
|
228
|
+
- **Disconnects**: when the client closes the connection the underlying
|
|
229
|
+
subscription generator is closed promptly — including `await`-based cleanup
|
|
230
|
+
in its `finally` block — with no leaked tasks.
|
|
231
|
+
|
|
232
|
+
## Related Projects
|
|
233
|
+
|
|
234
|
+
- **[graphql-api](https://graphql-api.parob.com/)** - Build GraphQL schemas with decorators
|
|
235
|
+
- **[graphql-db](https://graphql-db.parob.com/)** - SQLAlchemy integration for database-backed APIs
|
|
236
|
+
- **[graphql-mcp](https://graphql-mcp.parob.com/)** - Expose GraphQL as MCP tools
|
|
237
|
+
|
|
238
|
+
See the [documentation](https://graphql-http.parob.com/) for configuration, authentication, and advanced features.
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
## Documentation
|
|
242
|
+
|
|
243
|
+
**Visit the [official documentation](https://graphql-http.parob.com/)** for comprehensive guides, examples, and API reference.
|
|
244
|
+
|
|
245
|
+
### Key Topics
|
|
246
|
+
|
|
247
|
+
- **[Getting Started](https://graphql-http.parob.com/docs/getting-started/)** - Quick introduction and basic usage
|
|
248
|
+
- **[Configuration](https://graphql-http.parob.com/docs/configuration/)** - Configure your HTTP server
|
|
249
|
+
- **[Authentication](https://graphql-http.parob.com/docs/authentication/)** - JWT and auth setup
|
|
250
|
+
- **[Testing](https://graphql-http.parob.com/docs/testing/)** - Test your GraphQL endpoints
|
|
251
|
+
- **[Examples](https://graphql-http.parob.com/docs/examples/)** - Real-world usage examples
|
|
252
|
+
- **[API Reference](https://graphql-http.parob.com/docs/api-reference/)** - Complete API documentation
|
|
253
|
+
|
|
254
|
+
## License
|
|
255
|
+
|
|
256
|
+
MIT License - see LICENSE file for details.
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# GraphQL HTTP
|
|
2
|
+
|
|
3
|
+
[](https://badge.fury.io/py/graphql-http)
|
|
4
|
+
[](https://pypi.org/project/graphql-http/)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
|
|
7
|
+
**[📚 Documentation](https://graphql-http.parob.com/)** | **[📦 PyPI](https://pypi.org/project/graphql-http/)** | **[🔧 GitHub](https://github.com/parob/graphql-http)**
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
A lightweight, production-ready HTTP server for GraphQL APIs built on top of Starlette/FastAPI. This server provides a simple yet powerful way to serve GraphQL schemas over HTTP with built-in support for authentication, CORS, GraphiQL integration, and more.
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
- 🚀 **High Performance**: Built on Starlette/ASGI for excellent async performance
|
|
16
|
+
- 🔐 **JWT Authentication**: Built-in JWT authentication with JWKS support
|
|
17
|
+
- 🌐 **CORS Support**: Configurable CORS middleware for cross-origin requests
|
|
18
|
+
- 🎨 **GraphiQL Integration**: Interactive GraphQL IDE for development
|
|
19
|
+
- 📊 **Health Checks**: Built-in health check endpoints
|
|
20
|
+
- 🔄 **Batch Queries**: Support for batched GraphQL operations
|
|
21
|
+
- 📡 **Subscriptions**: GraphQL subscriptions streamed over Server-Sent Events ([graphql-sse](https://github.com/enisdenjo/graphql-sse/blob/master/PROTOCOL.md) compatible)
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
uv add graphql_http
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Or with pip:
|
|
30
|
+
```bash
|
|
31
|
+
pip install graphql_http
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Quick Start
|
|
35
|
+
|
|
36
|
+
### Basic Usage
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from graphql import GraphQLSchema, GraphQLObjectType, GraphQLField, GraphQLString
|
|
40
|
+
from graphql_http import GraphQLHTTP
|
|
41
|
+
|
|
42
|
+
# Define your GraphQL schema
|
|
43
|
+
schema = GraphQLSchema(
|
|
44
|
+
query=GraphQLObjectType(
|
|
45
|
+
name="Query",
|
|
46
|
+
fields={
|
|
47
|
+
"hello": GraphQLField(
|
|
48
|
+
GraphQLString,
|
|
49
|
+
resolve=lambda obj, info: "Hello, World!"
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Create the HTTP server
|
|
56
|
+
app = GraphQLHTTP(schema=schema)
|
|
57
|
+
|
|
58
|
+
# Run the server
|
|
59
|
+
if __name__ == "__main__":
|
|
60
|
+
app.run(host="0.0.0.0", port=8000)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Using with graphql-api
|
|
64
|
+
|
|
65
|
+
For building GraphQL schemas, use [graphql-api](https://graphql-api.parob.com/):
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from graphql_api import GraphQLAPI
|
|
69
|
+
from graphql_http import GraphQLHTTP
|
|
70
|
+
|
|
71
|
+
api = GraphQLAPI()
|
|
72
|
+
|
|
73
|
+
@api.type(is_root_type=True)
|
|
74
|
+
class Query:
|
|
75
|
+
@api.field
|
|
76
|
+
def hello(self, name: str = "World") -> str:
|
|
77
|
+
return f"Hello, {name}!"
|
|
78
|
+
|
|
79
|
+
server = GraphQLHTTP.from_api(api)
|
|
80
|
+
server.run()
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Subscriptions (Server-Sent Events)
|
|
84
|
+
|
|
85
|
+
GraphQL subscriptions are served over Server-Sent Events, following the
|
|
86
|
+
[GraphQL over SSE protocol](https://github.com/enisdenjo/graphql-sse/blob/master/PROTOCOL.md)
|
|
87
|
+
in **distinct connections mode**: each operation gets its own SSE connection, results
|
|
88
|
+
stream as `next` events, and a final `complete` event signals the end of the stream.
|
|
89
|
+
|
|
90
|
+
Any request whose `Accept` header includes `text/event-stream` (with a non-zero
|
|
91
|
+
q-value) is answered over SSE — subscriptions stream one `next` event per result,
|
|
92
|
+
while queries and mutations respond with a single `next` followed by `complete`.
|
|
93
|
+
A valid subscription sent *without* `Accept: text/event-stream` is rejected with
|
|
94
|
+
`406 Not Acceptable`.
|
|
95
|
+
|
|
96
|
+
### Defining a subscription with graphql-api
|
|
97
|
+
|
|
98
|
+
Any field returning an `AsyncGenerator` becomes a subscription:
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
import asyncio
|
|
102
|
+
from typing import AsyncGenerator
|
|
103
|
+
|
|
104
|
+
from graphql_api import GraphQLAPI
|
|
105
|
+
from graphql_http import GraphQLHTTP
|
|
106
|
+
|
|
107
|
+
api = GraphQLAPI()
|
|
108
|
+
|
|
109
|
+
@api.type(is_root_type=True)
|
|
110
|
+
class Root:
|
|
111
|
+
@api.field
|
|
112
|
+
def hello(self) -> str:
|
|
113
|
+
return "world"
|
|
114
|
+
|
|
115
|
+
@api.field
|
|
116
|
+
async def countdown(self, start: int = 3) -> AsyncGenerator[int, None]:
|
|
117
|
+
while start >= 0:
|
|
118
|
+
yield start
|
|
119
|
+
start -= 1
|
|
120
|
+
await asyncio.sleep(1)
|
|
121
|
+
|
|
122
|
+
server = GraphQLHTTP.from_api(api)
|
|
123
|
+
server.run()
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Consuming with curl
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
curl -N \
|
|
130
|
+
-H "Accept: text/event-stream" \
|
|
131
|
+
-H "Content-Type: application/json" \
|
|
132
|
+
-d '{"query": "subscription { countdown(start: 3) }"}' \
|
|
133
|
+
http://localhost:5000/graphql
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
event: next
|
|
138
|
+
data: {"data":{"countdown":3}}
|
|
139
|
+
|
|
140
|
+
event: next
|
|
141
|
+
data: {"data":{"countdown":2}}
|
|
142
|
+
|
|
143
|
+
event: next
|
|
144
|
+
data: {"data":{"countdown":1}}
|
|
145
|
+
|
|
146
|
+
event: next
|
|
147
|
+
data: {"data":{"countdown":0}}
|
|
148
|
+
|
|
149
|
+
event: complete
|
|
150
|
+
data:
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Consuming with the graphql-sse JavaScript client
|
|
154
|
+
|
|
155
|
+
The [`graphql-sse`](https://github.com/enisdenjo/graphql-sse) client uses distinct
|
|
156
|
+
connections mode by default (`singleConnection: false`):
|
|
157
|
+
|
|
158
|
+
```javascript
|
|
159
|
+
import { createClient } from 'graphql-sse';
|
|
160
|
+
|
|
161
|
+
const client = createClient({
|
|
162
|
+
url: 'http://localhost:5000/graphql',
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const unsubscribe = client.subscribe(
|
|
166
|
+
{ query: 'subscription { countdown(start: 3) }' },
|
|
167
|
+
{
|
|
168
|
+
next: (result) => console.log(result), // { data: { countdown: 3 } } ...
|
|
169
|
+
error: (error) => console.error(error),
|
|
170
|
+
complete: () => console.log('done'),
|
|
171
|
+
},
|
|
172
|
+
);
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Behavior notes
|
|
176
|
+
|
|
177
|
+
- **Auth**: SSE requests go through the same JWT/auth enforcement as regular
|
|
178
|
+
requests. Authentication failures are returned as HTTP-level JSON errors
|
|
179
|
+
(401/403) before any stream is opened. The introspection auth-bypass never
|
|
180
|
+
applies to subscription operations.
|
|
181
|
+
- **Errors**: per the graphql-sse protocol, errors raised before execution
|
|
182
|
+
starts — missing query, parse errors, validation errors, and `subscribe()`
|
|
183
|
+
failures — are delivered over the accepted `200 text/event-stream` response
|
|
184
|
+
as a `next` event carrying the errors, followed by `complete` (a `400` would
|
|
185
|
+
leave e.g. a browser `EventSource` with no error detail). Resolver errors
|
|
186
|
+
during the stream ride inside `next` payloads as standard GraphQL errors; a
|
|
187
|
+
fatal source error emits a final `next` carrying the error, then `complete`.
|
|
188
|
+
- **Middleware & execution context**: subscription events resolve through the
|
|
189
|
+
same `middleware` chain and `execution_context_class` as queries and
|
|
190
|
+
mutations, so field-authorization or error-masking middleware applies to
|
|
191
|
+
streamed results too.
|
|
192
|
+
- **Keep-alive**: while a stream is idle the server emits `: ping` SSE comments
|
|
193
|
+
every 15 seconds so intermediary proxies don't drop the connection. Configure
|
|
194
|
+
with `GraphQLHTTP(..., sse_keepalive_interval=30.0)` (must be positive;
|
|
195
|
+
`None` disables pings).
|
|
196
|
+
- **Concurrency limit**: at most `sse_max_streams` subscription streams
|
|
197
|
+
(default 100) may be open concurrently per server; further subscription
|
|
198
|
+
requests are rejected with `429 Too Many Requests` until a slot frees up.
|
|
199
|
+
Pass `sse_max_streams=None` to remove the limit.
|
|
200
|
+
- **Disconnects**: when the client closes the connection the underlying
|
|
201
|
+
subscription generator is closed promptly — including `await`-based cleanup
|
|
202
|
+
in its `finally` block — with no leaked tasks.
|
|
203
|
+
|
|
204
|
+
## Related Projects
|
|
205
|
+
|
|
206
|
+
- **[graphql-api](https://graphql-api.parob.com/)** - Build GraphQL schemas with decorators
|
|
207
|
+
- **[graphql-db](https://graphql-db.parob.com/)** - SQLAlchemy integration for database-backed APIs
|
|
208
|
+
- **[graphql-mcp](https://graphql-mcp.parob.com/)** - Expose GraphQL as MCP tools
|
|
209
|
+
|
|
210
|
+
See the [documentation](https://graphql-http.parob.com/) for configuration, authentication, and advanced features.
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
## Documentation
|
|
214
|
+
|
|
215
|
+
**Visit the [official documentation](https://graphql-http.parob.com/)** for comprehensive guides, examples, and API reference.
|
|
216
|
+
|
|
217
|
+
### Key Topics
|
|
218
|
+
|
|
219
|
+
- **[Getting Started](https://graphql-http.parob.com/docs/getting-started/)** - Quick introduction and basic usage
|
|
220
|
+
- **[Configuration](https://graphql-http.parob.com/docs/configuration/)** - Configure your HTTP server
|
|
221
|
+
- **[Authentication](https://graphql-http.parob.com/docs/authentication/)** - JWT and auth setup
|
|
222
|
+
- **[Testing](https://graphql-http.parob.com/docs/testing/)** - Test your GraphQL endpoints
|
|
223
|
+
- **[Examples](https://graphql-http.parob.com/docs/examples/)** - Real-world usage examples
|
|
224
|
+
- **[API Reference](https://graphql-http.parob.com/docs/api-reference/)** - Complete API documentation
|
|
225
|
+
|
|
226
|
+
## License
|
|
227
|
+
|
|
228
|
+
MIT License - see LICENSE file for details.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
2.4.0
|
|
@@ -196,8 +196,12 @@ def _check_introspection_string(query_str: str) -> bool:
|
|
|
196
196
|
clean_query = re.sub(r"'[^']*'", '', clean_query) # Remove string literals
|
|
197
197
|
clean_query = clean_query.lower()
|
|
198
198
|
|
|
199
|
-
# Check for non-query operations first (immediate block)
|
|
200
|
-
|
|
199
|
+
# Check for non-query operations first (immediate block). This must
|
|
200
|
+
# catch named operations too ("subscription Foo { ... }") so that a
|
|
201
|
+
# subscription or mutation document can never bypass authentication.
|
|
202
|
+
# Introspection results like "subscriptionType" are not matched because
|
|
203
|
+
# of the trailing word boundary.
|
|
204
|
+
if re.search(r'\b(mutation|subscription)\b', clean_query):
|
|
201
205
|
logger.debug("Found mutation or subscription - blocking immediately")
|
|
202
206
|
return False
|
|
203
207
|
|