graphql-http 2.3.4__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.
Files changed (59) hide show
  1. {graphql_http-2.3.4 → graphql_http-2.4.0}/PKG-INFO +123 -1
  2. graphql_http-2.4.0/README.md +228 -0
  3. graphql_http-2.4.0/VERSION +1 -0
  4. {graphql_http-2.3.4 → graphql_http-2.4.0}/graphql_http/introspection.py +6 -2
  5. {graphql_http-2.3.4 → graphql_http-2.4.0}/graphql_http/server.py +418 -3
  6. {graphql_http-2.3.4 → graphql_http-2.4.0}/graphql_http.egg-info/PKG-INFO +123 -1
  7. {graphql_http-2.3.4 → graphql_http-2.4.0}/graphql_http.egg-info/SOURCES.txt +2 -1
  8. graphql_http-2.4.0/tests/test_sse.py +889 -0
  9. graphql_http-2.3.4/README.md +0 -106
  10. graphql_http-2.3.4/VERSION +0 -1
  11. {graphql_http-2.3.4 → graphql_http-2.4.0}/.coverage +0 -0
  12. {graphql_http-2.3.4 → graphql_http-2.4.0}/.github/workflows/pages.yml +0 -0
  13. {graphql_http-2.3.4 → graphql_http-2.4.0}/.github/workflows/python-package.yml +0 -0
  14. {graphql_http-2.3.4 → graphql_http-2.4.0}/.gitignore +0 -0
  15. {graphql_http-2.3.4 → graphql_http-2.4.0}/.gitmodules +0 -0
  16. {graphql_http-2.3.4 → graphql_http-2.4.0}/AGENTS.md +0 -0
  17. {graphql_http-2.3.4 → graphql_http-2.4.0}/CLAUDE.md +0 -0
  18. {graphql_http-2.3.4 → graphql_http-2.4.0}/LICENSE +0 -0
  19. {graphql_http-2.3.4 → graphql_http-2.4.0}/docs/.gitmodules +0 -0
  20. {graphql_http-2.3.4 → graphql_http-2.4.0}/docs/content/_index.md +0 -0
  21. {graphql_http-2.3.4 → graphql_http-2.4.0}/docs/content/docs/_index.md +0 -0
  22. {graphql_http-2.3.4 → graphql_http-2.4.0}/docs/content/docs/api-reference.md +0 -0
  23. {graphql_http-2.3.4 → graphql_http-2.4.0}/docs/content/docs/authentication.md +0 -0
  24. {graphql_http-2.3.4 → graphql_http-2.4.0}/docs/content/docs/configuration.md +0 -0
  25. {graphql_http-2.3.4 → graphql_http-2.4.0}/docs/content/docs/examples.md +0 -0
  26. {graphql_http-2.3.4 → graphql_http-2.4.0}/docs/content/docs/getting-started.md +0 -0
  27. {graphql_http-2.3.4 → graphql_http-2.4.0}/docs/content/docs/testing.md +0 -0
  28. {graphql_http-2.3.4 → graphql_http-2.4.0}/docs/hugo.toml +0 -0
  29. {graphql_http-2.3.4 → graphql_http-2.4.0}/docs/index.md +0 -0
  30. {graphql_http-2.3.4 → graphql_http-2.4.0}/example_advanced_server.py +0 -0
  31. {graphql_http-2.3.4 → graphql_http-2.4.0}/examples/README.md +0 -0
  32. {graphql_http-2.3.4 → graphql_http-2.4.0}/examples/__init__.py +0 -0
  33. {graphql_http-2.3.4 → graphql_http-2.4.0}/examples/advanced_server.py +0 -0
  34. {graphql_http-2.3.4 → graphql_http-2.4.0}/examples/auth_server.py +0 -0
  35. {graphql_http-2.3.4 → graphql_http-2.4.0}/examples/basic_server.py +0 -0
  36. {graphql_http-2.3.4 → graphql_http-2.4.0}/examples/graphql_api_server.py +0 -0
  37. {graphql_http-2.3.4 → graphql_http-2.4.0}/graphql_http/__init__.py +0 -0
  38. {graphql_http-2.3.4 → graphql_http-2.4.0}/graphql_http/error.py +0 -0
  39. {graphql_http-2.3.4 → graphql_http-2.4.0}/graphql_http/graphiql/index.html +0 -0
  40. {graphql_http-2.3.4 → graphql_http-2.4.0}/graphql_http/helpers.py +0 -0
  41. {graphql_http-2.3.4 → graphql_http-2.4.0}/graphql_http.egg-info/dependency_links.txt +0 -0
  42. {graphql_http-2.3.4 → graphql_http-2.4.0}/graphql_http.egg-info/requires.txt +0 -0
  43. {graphql_http-2.3.4 → graphql_http-2.4.0}/graphql_http.egg-info/top_level.txt +0 -0
  44. {graphql_http-2.3.4 → graphql_http-2.4.0}/pyproject.toml +0 -0
  45. {graphql_http-2.3.4 → graphql_http-2.4.0}/setup.cfg +0 -0
  46. {graphql_http-2.3.4 → graphql_http-2.4.0}/test_server.py +0 -0
  47. {graphql_http-2.3.4 → graphql_http-2.4.0}/tests/__init__.py +0 -0
  48. {graphql_http-2.3.4 → graphql_http-2.4.0}/tests/app.py +0 -0
  49. {graphql_http-2.3.4 → graphql_http-2.4.0}/tests/app_federation.py +0 -0
  50. {graphql_http-2.3.4 → graphql_http-2.4.0}/tests/conftest.py +0 -0
  51. {graphql_http-2.3.4 → graphql_http-2.4.0}/tests/test_auth.py +0 -0
  52. {graphql_http-2.3.4 → graphql_http-2.4.0}/tests/test_cors.py +0 -0
  53. {graphql_http-2.3.4 → graphql_http-2.4.0}/tests/test_examples.py +0 -0
  54. {graphql_http-2.3.4 → graphql_http-2.4.0}/tests/test_federation.py +0 -0
  55. {graphql_http-2.3.4 → graphql_http-2.4.0}/tests/test_graphql_api.py +0 -0
  56. {graphql_http-2.3.4 → graphql_http-2.4.0}/tests/test_introspection.py +0 -0
  57. {graphql_http-2.3.4 → graphql_http-2.4.0}/tests/test_linting.py +0 -0
  58. {graphql_http-2.3.4 → graphql_http-2.4.0}/tests/test_server.py +0 -0
  59. {graphql_http-2.3.4 → graphql_http-2.4.0}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: graphql-http
3
- Version: 2.3.4
3
+ Version: 2.4.0
4
4
  Summary: HTTP Server for GraphQL.
5
5
  Author-email: Robert Parker <rob@parob.com>
6
6
  License: MIT
@@ -46,6 +46,7 @@ A lightweight, production-ready HTTP server for GraphQL APIs built on top of Sta
46
46
  - 🎨 **GraphiQL Integration**: Interactive GraphQL IDE for development
47
47
  - 📊 **Health Checks**: Built-in health check endpoints
48
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)
49
50
 
50
51
  ## Installation
51
52
 
@@ -107,6 +108,127 @@ server = GraphQLHTTP.from_api(api)
107
108
  server.run()
108
109
  ```
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
+
110
232
  ## Related Projects
111
233
 
112
234
  - **[graphql-api](https://graphql-api.parob.com/)** - Build GraphQL schemas with decorators
@@ -0,0 +1,228 @@
1
+ # GraphQL HTTP
2
+
3
+ [![PyPI version](https://badge.fury.io/py/graphql-http.svg)](https://badge.fury.io/py/graphql-http)
4
+ [![Python versions](https://img.shields.io/pypi/pyversions/graphql-http.svg)](https://pypi.org/project/graphql-http/)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
- if re.search(r'\b(mutation|subscription)\s*\{', clean_query):
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