graphql-http 2.0.2__tar.gz → 2.0.4__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 (36) hide show
  1. {graphql_http-2.0.2 → graphql_http-2.0.4}/PKG-INFO +1 -1
  2. graphql_http-2.0.4/VERSION +1 -0
  3. {graphql_http-2.0.2 → graphql_http-2.0.4}/examples/advanced_server.py +31 -33
  4. {graphql_http-2.0.2 → graphql_http-2.0.4}/examples/auth_server.py +13 -13
  5. {graphql_http-2.0.2 → graphql_http-2.0.4}/examples/graphql_api_server.py +55 -56
  6. {graphql_http-2.0.2 → graphql_http-2.0.4}/graphql_http/server.py +11 -10
  7. {graphql_http-2.0.2 → graphql_http-2.0.4}/graphql_http.egg-info/PKG-INFO +1 -1
  8. {graphql_http-2.0.2 → graphql_http-2.0.4}/tests/test_auth.py +13 -15
  9. graphql_http-2.0.2/VERSION +0 -1
  10. {graphql_http-2.0.2 → graphql_http-2.0.4}/.coverage +0 -0
  11. {graphql_http-2.0.2 → graphql_http-2.0.4}/.github/workflows/python-package.yml +0 -0
  12. {graphql_http-2.0.2 → graphql_http-2.0.4}/.gitignore +0 -0
  13. {graphql_http-2.0.2 → graphql_http-2.0.4}/LICENSE +0 -0
  14. {graphql_http-2.0.2 → graphql_http-2.0.4}/README.md +0 -0
  15. {graphql_http-2.0.2 → graphql_http-2.0.4}/examples/README.md +0 -0
  16. {graphql_http-2.0.2 → graphql_http-2.0.4}/examples/basic_server.py +0 -0
  17. {graphql_http-2.0.2 → graphql_http-2.0.4}/graphql_http/__init__.py +0 -0
  18. {graphql_http-2.0.2 → graphql_http-2.0.4}/graphql_http/error.py +0 -0
  19. {graphql_http-2.0.2 → graphql_http-2.0.4}/graphql_http/graphiql/index.html +0 -0
  20. {graphql_http-2.0.2 → graphql_http-2.0.4}/graphql_http/helpers.py +0 -0
  21. {graphql_http-2.0.2 → graphql_http-2.0.4}/graphql_http.egg-info/SOURCES.txt +0 -0
  22. {graphql_http-2.0.2 → graphql_http-2.0.4}/graphql_http.egg-info/dependency_links.txt +0 -0
  23. {graphql_http-2.0.2 → graphql_http-2.0.4}/graphql_http.egg-info/requires.txt +0 -0
  24. {graphql_http-2.0.2 → graphql_http-2.0.4}/graphql_http.egg-info/top_level.txt +0 -0
  25. {graphql_http-2.0.2 → graphql_http-2.0.4}/pyproject.toml +0 -0
  26. {graphql_http-2.0.2 → graphql_http-2.0.4}/setup.cfg +0 -0
  27. {graphql_http-2.0.2 → graphql_http-2.0.4}/test_server.py +0 -0
  28. {graphql_http-2.0.2 → graphql_http-2.0.4}/tests/__init__.py +0 -0
  29. {graphql_http-2.0.2 → graphql_http-2.0.4}/tests/app.py +0 -0
  30. {graphql_http-2.0.2 → graphql_http-2.0.4}/tests/app_federation.py +0 -0
  31. {graphql_http-2.0.2 → graphql_http-2.0.4}/tests/conftest.py +0 -0
  32. {graphql_http-2.0.2 → graphql_http-2.0.4}/tests/test_cors.py +0 -0
  33. {graphql_http-2.0.2 → graphql_http-2.0.4}/tests/test_federation.py +0 -0
  34. {graphql_http-2.0.2 → graphql_http-2.0.4}/tests/test_graphql_api.py +0 -0
  35. {graphql_http-2.0.2 → graphql_http-2.0.4}/tests/test_server.py +0 -0
  36. {graphql_http-2.0.2 → graphql_http-2.0.4}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: graphql_http
3
- Version: 2.0.2
3
+ Version: 2.0.4
4
4
  Summary: HTTP Server for GraphQL.
5
5
  Author-email: Robert Parker <rob@parob.com>
6
6
  License: MIT
@@ -0,0 +1 @@
1
+ 2.0.4
@@ -12,8 +12,6 @@ This example demonstrates advanced features including:
12
12
  """
13
13
 
14
14
  import time
15
- import asyncio
16
- from typing import Any, Dict, List, Optional
17
15
  from graphql import (
18
16
  GraphQLSchema,
19
17
  GraphQLObjectType,
@@ -33,15 +31,15 @@ from graphql_http import GraphQLHTTP
33
31
  # Custom execution context for performance monitoring
34
32
  class PerformanceExecutionContext(ExecutionContext):
35
33
  """Custom execution context that tracks performance metrics."""
36
-
34
+
37
35
  def __init__(self, *args, **kwargs):
38
36
  super().__init__(*args, **kwargs)
39
37
  self.start_time = time.time()
40
38
  self.field_count = 0
41
-
39
+
42
40
  def __enter__(self):
43
41
  return super().__enter__()
44
-
42
+
45
43
  def __exit__(self, exc_type, exc_val, exc_tb):
46
44
  execution_time = time.time() - self.start_time
47
45
  print(f"Query executed in {execution_time:.3f}s with {self.field_count} fields")
@@ -51,20 +49,20 @@ class PerformanceExecutionContext(ExecutionContext):
51
49
  # Custom context class
52
50
  class RequestContext:
53
51
  """Custom context that provides request-specific information."""
54
-
52
+
55
53
  def __init__(self, request=None):
56
54
  self.request = request
57
55
  self.start_time = time.time()
58
56
  self.user_id = None
59
57
  self.cache = {}
60
-
58
+
61
59
  def get_user_id(self):
62
60
  """Extract user ID from request headers (simplified)."""
63
61
  if self.request and not self.user_id:
64
62
  # In real apps, this would decode JWT or session
65
63
  self.user_id = self.request.headers.get("X-User-ID", "anonymous")
66
64
  return self.user_id
67
-
65
+
68
66
  def get_elapsed_time(self):
69
67
  """Get elapsed time since request started."""
70
68
  return time.time() - self.start_time
@@ -88,19 +86,19 @@ posts_data = [
88
86
  def performance_middleware(next_fn, root, info, **args):
89
87
  """Middleware that measures field resolution time."""
90
88
  field_start = time.time()
91
-
89
+
92
90
  # Track field count in execution context
93
91
  if hasattr(info.context, 'field_count'):
94
92
  info.context.field_count += 1
95
-
93
+
96
94
  try:
97
95
  result = next_fn(root, info, **args)
98
96
  field_time = time.time() - field_start
99
-
97
+
100
98
  # Log slow fields
101
99
  if field_time > 0.1: # 100ms threshold
102
100
  print(f"Slow field detected: {info.field_name} took {field_time:.3f}s")
103
-
101
+
104
102
  return result
105
103
  except Exception as e:
106
104
  field_time = time.time() - field_start
@@ -112,40 +110,40 @@ def caching_middleware(next_fn, root, info, **args):
112
110
  """Simple in-memory caching middleware."""
113
111
  # Create cache key
114
112
  cache_key = f"{info.field_name}:{hash(str(args))}"
115
-
113
+
116
114
  # Check if we have cached result
117
115
  if hasattr(info.context, 'cache') and cache_key in info.context.cache:
118
116
  print(f"Cache hit for {info.field_name}")
119
117
  return info.context.cache[cache_key]
120
-
118
+
121
119
  # Execute and cache result
122
120
  result = next_fn(root, info, **args)
123
-
121
+
124
122
  if hasattr(info.context, 'cache'):
125
123
  info.context.cache[cache_key] = result
126
124
  print(f"Cached result for {info.field_name}")
127
-
125
+
128
126
  return result
129
127
 
130
128
 
131
129
  def auth_middleware(next_fn, root, info, **args):
132
130
  """Authentication middleware for protected fields."""
133
131
  field_name = info.field_name
134
-
132
+
135
133
  # Check if field requires authentication
136
134
  protected_fields = ["adminData", "deletePost", "promoteUser"]
137
135
  if field_name in protected_fields:
138
136
  user_id = info.context.get_user_id() if hasattr(info.context, 'get_user_id') else None
139
137
  if not user_id or user_id == "anonymous":
140
138
  raise GraphQLError(f"Authentication required for field: {field_name}")
141
-
139
+
142
140
  # Check admin role for admin fields
143
141
  admin_fields = ["adminData", "promoteUser"]
144
142
  if field_name in admin_fields:
145
143
  # In real apps, check user role from database/JWT
146
144
  if not user_id.startswith("admin"):
147
145
  raise GraphQLError(f"Admin access required for field: {field_name}")
148
-
146
+
149
147
  return next_fn(root, info, **args)
150
148
 
151
149
 
@@ -162,11 +160,11 @@ def get_user_by_id(obj, info, user_id):
162
160
  """Get user by ID with validation."""
163
161
  if user_id <= 0:
164
162
  raise GraphQLError("User ID must be positive")
165
-
163
+
166
164
  user = next((u for u in users_data if u["id"] == user_id), None)
167
165
  if not user:
168
166
  raise GraphQLError(f"User with ID {user_id} not found")
169
-
167
+
170
168
  return user
171
169
 
172
170
 
@@ -325,27 +323,27 @@ def create_custom_context(request):
325
323
  def main():
326
324
  """Run the advanced GraphQL server."""
327
325
  print("Starting advanced GraphQL server...")
328
-
326
+
329
327
  # Create server with advanced configuration
330
328
  server = GraphQLHTTP(
331
329
  schema=schema,
332
330
  serve_graphiql=True,
333
331
  allow_cors=True,
334
332
  health_path="/health",
335
-
333
+
336
334
  # Add middleware stack
337
335
  middleware=[
338
336
  auth_middleware, # Authentication first
339
337
  performance_middleware, # Then performance monitoring
340
338
  caching_middleware, # Finally caching
341
339
  ],
342
-
340
+
343
341
  # Custom execution context
344
342
  execution_context_class=PerformanceExecutionContext,
345
-
343
+
346
344
  # Custom context factory
347
345
  context_value=lambda: create_custom_context(None),
348
-
346
+
349
347
  graphiql_default_query="""
350
348
  # Try these queries to see advanced features:
351
349
 
@@ -360,17 +358,17 @@ def main():
360
358
  likes
361
359
  }
362
360
  }
363
-
361
+
364
362
  # Context information
365
363
  contextInfo {
366
364
  elapsedTime
367
365
  userId
368
366
  cacheSize
369
367
  }
370
-
368
+
371
369
  # This will be slow and trigger performance warning
372
370
  # slowField
373
-
371
+
374
372
  # This will demonstrate error handling
375
373
  # errorField
376
374
  }
@@ -384,7 +382,7 @@ def main():
384
382
  # }
385
383
  """.strip(),
386
384
  )
387
-
385
+
388
386
  print("Advanced server features enabled:")
389
387
  print(" ✓ Performance monitoring middleware")
390
388
  print(" ✓ Simple in-memory caching")
@@ -392,17 +390,17 @@ def main():
392
390
  print(" ✓ Custom execution context")
393
391
  print(" ✓ Custom request context")
394
392
  print(" ✓ Error handling and validation")
395
-
393
+
396
394
  print(f"\nTesting tips:")
397
395
  print(f" • Set 'X-User-ID: admin123' header for admin access")
398
396
  print(f" • Watch console for performance metrics")
399
397
  print(f" • Try the slowField and errorField for testing")
400
398
  print(f" • Run same query twice to see caching in action")
401
-
399
+
402
400
  print(f"\nEndpoints:")
403
401
  print(f" GraphiQL: http://localhost:8000/graphql")
404
402
  print(f" Health: http://localhost:8000/health")
405
-
403
+
406
404
  # Run the server
407
405
  server.run(host="0.0.0.0", port=8000)
408
406
 
@@ -92,7 +92,7 @@ schema = GraphQLSchema(
92
92
  resolve=get_public_info,
93
93
  description="Public information available without authentication",
94
94
  ),
95
-
95
+
96
96
  # Protected fields - require authentication
97
97
  "me": GraphQLField(
98
98
  UserType,
@@ -120,20 +120,20 @@ def create_server_with_auth():
120
120
  jwks_uri = os.getenv("JWKS_URI", "https://your-auth0-domain.auth0.com/.well-known/jwks.json")
121
121
  issuer = os.getenv("JWT_ISSUER", "https://your-auth0-domain.auth0.com/")
122
122
  audience = os.getenv("JWT_AUDIENCE", "your-api-identifier")
123
-
123
+
124
124
  return GraphQLHTTP(
125
125
  schema=schema,
126
126
  serve_graphiql=True,
127
127
  allow_cors=True, # Enable CORS for web applications
128
128
  health_path="/health", # Health check endpoint
129
-
129
+
130
130
  # JWT Authentication configuration
131
131
  auth_enabled=True,
132
132
  auth_jwks_uri=jwks_uri,
133
133
  auth_issuer=issuer,
134
134
  auth_audience=audience,
135
- auth_enabled_for_introspection=False, # Allow introspection without auth
136
-
135
+ auth_bypass_during_introspection=False,
136
+
137
137
  graphiql_default_query="""
138
138
  # Try these queries:
139
139
 
@@ -160,7 +160,7 @@ def create_server_with_auth():
160
160
  # role
161
161
  # }
162
162
  # }
163
- #
163
+ #
164
164
  # {
165
165
  # users {
166
166
  # id
@@ -204,7 +204,7 @@ def main():
204
204
  """Run the GraphQL server."""
205
205
  # Check if authentication should be enabled
206
206
  enable_auth = os.getenv("ENABLE_AUTH", "false").lower() == "true"
207
-
207
+
208
208
  if enable_auth:
209
209
  print("Starting GraphQL server with JWT authentication...")
210
210
  server = create_server_with_auth()
@@ -220,12 +220,12 @@ def main():
220
220
  server = create_server_without_auth()
221
221
  print("\nAuthentication disabled for development!")
222
222
  print("Set ENABLE_AUTH=true to enable authentication")
223
-
224
- print(f"\nEndpoints:")
225
- print(f" GraphiQL: http://localhost:8000/graphql")
226
- print(f" Health: http://localhost:8000/health")
227
- print(f" API: http://localhost:8000/graphql")
228
-
223
+
224
+ print("\nEndpoints:")
225
+ print(" GraphiQL: http://localhost:8000/graphql")
226
+ print(" Health: http://localhost:8000/health")
227
+ print(" API: http://localhost:8000/graphql")
228
+
229
229
  # Run the server
230
230
  server.run(host="0.0.0.0", port=8000)
231
231
 
@@ -11,9 +11,8 @@ Install with: pip install graphql-api
11
11
  """
12
12
 
13
13
  try:
14
- from graphql_api import GraphQLAPI, field, type
14
+ from graphql_api import GraphQLAPI, field
15
15
  from graphql_api.context import GraphQLContext
16
- from graphql_api.mapper import GraphQLMetaKey
17
16
  except ImportError:
18
17
  print("This example requires graphql-api to be installed.")
19
18
  print("Install with: pip install graphql-api")
@@ -75,52 +74,52 @@ api = GraphQLAPI()
75
74
  @api.type
76
75
  class AuthorType:
77
76
  """Author type with automatic field resolution."""
78
-
77
+
79
78
  @field
80
79
  def id(self) -> int:
81
- return self.id
82
-
80
+ return self._id
81
+
83
82
  @field
84
83
  def name(self) -> str:
85
- return self.name
86
-
84
+ return self._name
85
+
87
86
  @field
88
87
  def email(self) -> str:
89
- return self.email
90
-
88
+ return self._email
89
+
91
90
  @field
92
- def posts(self) -> List["PostType"]:
91
+ def posts(self) -> List[Post]:
93
92
  """Get all posts by this author."""
94
- return [post for post in posts if post.author_id == self.id]
93
+ return [post for post in posts if post.author_id == self._id]
95
94
 
96
95
 
97
96
  @api.type
98
97
  class PostType:
99
98
  """Post type with relationships."""
100
-
99
+
101
100
  @field
102
101
  def id(self) -> int:
103
- return self.id
104
-
102
+ return self._id
103
+
105
104
  @field
106
105
  def title(self) -> str:
107
- return self.title
108
-
106
+ return self._title
107
+
109
108
  @field
110
109
  def content(self) -> str:
111
- return self.content
112
-
110
+ return self._content
111
+
113
112
  @field
114
113
  def published(self) -> bool:
115
- return self.published
116
-
114
+ return self._published
115
+
117
116
  @field
118
117
  def author(self) -> Optional[AuthorType]:
119
118
  """Get the author of this post."""
120
- return next((author for author in authors if author.id == self.author_id), None)
121
-
119
+ return next((author for author in authors if author.id == self._author_id), None)
120
+
122
121
  @field
123
- def comments(self) -> List["CommentType"]:
122
+ def comments(self) -> List[Comment]:
124
123
  """Get all comments for this post."""
125
124
  return [comment for comment in comments if comment.post_id == self.id]
126
125
 
@@ -128,56 +127,56 @@ class PostType:
128
127
  @api.type
129
128
  class CommentType:
130
129
  """Comment type."""
131
-
130
+
132
131
  @field
133
132
  def id(self) -> int:
134
- return self.id
135
-
133
+ return self._id
134
+
136
135
  @field
137
136
  def author_name(self) -> str:
138
- return self.author_name
139
-
137
+ return self._author_name
138
+
140
139
  @field
141
140
  def content(self) -> str:
142
- return self.content
143
-
141
+ return self._content
142
+
144
143
  @field
145
144
  def post(self) -> Optional[PostType]:
146
145
  """Get the post this comment belongs to."""
147
- return next((post for post in posts if post.id == self.post_id), None)
146
+ return next((post for post in posts if post.id == self._post_id), None)
148
147
 
149
148
 
150
149
  @api.type(is_root_type=True)
151
150
  class Query:
152
151
  """Root Query type."""
153
-
152
+
154
153
  @field
155
154
  def hello(self, name: str = "World") -> str:
156
155
  """Simple hello field for testing."""
157
156
  return f"Hello, {name}!"
158
-
157
+
159
158
  @field
160
159
  def authors(self) -> List[AuthorType]:
161
160
  """Get all authors."""
162
161
  return authors
163
-
162
+
164
163
  @field
165
164
  def author(self, author_id: int) -> Optional[AuthorType]:
166
165
  """Get an author by ID."""
167
166
  return next((author for author in authors if author.id == author_id), None)
168
-
167
+
169
168
  @field
170
169
  def posts(self, published_only: bool = False) -> List[PostType]:
171
170
  """Get all posts, optionally filter by published status."""
172
171
  if published_only:
173
172
  return [post for post in posts if post.published]
174
173
  return posts
175
-
174
+
176
175
  @field
177
176
  def post(self, post_id: int) -> Optional[PostType]:
178
177
  """Get a post by ID."""
179
178
  return next((post for post in posts if post.id == post_id), None)
180
-
179
+
181
180
  @field
182
181
  def search_posts(self, query: str) -> List[PostType]:
183
182
  """Search posts by title or content."""
@@ -191,7 +190,7 @@ class Query:
191
190
  @api.type
192
191
  class Mutation:
193
192
  """Root Mutation type."""
194
-
193
+
195
194
  @field
196
195
  def create_post(self, title: str, content: str, author_id: int, published: bool = False) -> PostType:
197
196
  """Create a new post."""
@@ -204,30 +203,30 @@ class Mutation:
204
203
  )
205
204
  posts.append(new_post)
206
205
  return new_post
207
-
206
+
208
207
  @field
209
208
  def update_post(self, post_id: int, title: Optional[str] = None, content: Optional[str] = None, published: Optional[bool] = None) -> Optional[PostType]:
210
209
  """Update an existing post."""
211
210
  post = next((p for p in posts if p.id == post_id), None)
212
211
  if not post:
213
212
  return None
214
-
213
+
215
214
  if title is not None:
216
215
  post.title = title
217
216
  if content is not None:
218
217
  post.content = content
219
218
  if published is not None:
220
219
  post.published = published
221
-
220
+
222
221
  return post
223
-
222
+
224
223
  @field
225
- def add_comment(self, post_id: int, author_name: str, content: str) -> Optional["CommentType"]:
224
+ def add_comment(self, post_id: int, author_name: str, content: str) -> Optional[Comment]:
226
225
  """Add a comment to a post."""
227
226
  post = next((p for p in posts if p.id == post_id), None)
228
227
  if not post:
229
228
  return None
230
-
229
+
231
230
  new_comment = Comment(
232
231
  id=max(comment.id for comment in comments) + 1,
233
232
  post_id=post_id,
@@ -247,12 +246,12 @@ def custom_middleware(next_fn, root, info, **args):
247
246
  # Log the field being accessed
248
247
  field_name = info.field_name
249
248
  print(f"Accessing field: {field_name}")
250
-
249
+
251
250
  # You could add authentication logic here
252
251
  # if field_name in ["sensitive_field"]:
253
252
  # # Check authentication
254
253
  # pass
255
-
254
+
256
255
  # Call the next resolver
257
256
  return next_fn(root, info, **args)
258
257
 
@@ -279,7 +278,7 @@ class ContextQuery:
279
278
  def main():
280
279
  """Run the GraphQL server with GraphQL-API integration."""
281
280
  print("Starting GraphQL server with GraphQL-API integration...")
282
-
281
+
283
282
  # Create server from the GraphQL API
284
283
  server = GraphQLHTTP.from_api(
285
284
  api=api,
@@ -294,7 +293,7 @@ def main():
294
293
  {
295
294
  # Simple greeting
296
295
  hello(name: "GraphQL API User")
297
-
296
+
298
297
  # Get all authors with their posts
299
298
  authors {
300
299
  id
@@ -311,7 +310,7 @@ def main():
311
310
  }
312
311
  }
313
312
  }
314
-
313
+
315
314
  # Get only published posts
316
315
  posts(publishedOnly: true) {
317
316
  id
@@ -321,7 +320,7 @@ def main():
321
320
  email
322
321
  }
323
322
  }
324
-
323
+
325
324
  # Search posts
326
325
  searchPosts(query: "GraphQL") {
327
326
  id
@@ -347,18 +346,18 @@ def main():
347
346
  # }
348
347
  """.strip(),
349
348
  )
350
-
349
+
351
350
  print("GraphQL-API server features:")
352
351
  print(" ✓ Automatic type inference from Python types")
353
352
  print(" ✓ Dataclass integration")
354
353
  print(" ✓ Relationship resolution")
355
354
  print(" ✓ Context injection")
356
355
  print(" ✓ Custom middleware support")
357
-
358
- print(f"\nEndpoints:")
359
- print(f" GraphiQL: http://localhost:8000/graphql")
360
- print(f" Health: http://localhost:8000/health")
361
-
356
+
357
+ print("\nEndpoints:")
358
+ print(" GraphiQL: http://localhost:8000/graphql")
359
+ print(" Health: http://localhost:8000/health")
360
+
362
361
  # Run the server
363
362
  server.run(host="0.0.0.0", port=8000)
364
363
 
@@ -3,9 +3,11 @@ import json
3
3
  import os
4
4
  from json import JSONDecodeError
5
5
  from typing import Any, Awaitable, Callable, Dict, List, Optional, Type, Union
6
+ from logging import getLogger
6
7
 
7
8
  import jwt
8
9
  import uvicorn
10
+
9
11
  from graphql import GraphQLError, ExecutionResult
10
12
  from graphql.execution.execute import ExecutionContext
11
13
  from graphql.type.schema import GraphQLSchema
@@ -33,6 +35,7 @@ try:
33
35
  except ImportError:
34
36
  GraphQLContext = None # type: ignore
35
37
 
38
+ logger = getLogger(__name__)
36
39
  # Constants
37
40
  GRAPHIQL_DIR = os.path.join(os.path.dirname(__file__), "graphiql")
38
41
  DEFAULT_HOST = "127.0.0.1"
@@ -94,7 +97,7 @@ class GraphQLHTTP:
94
97
  auth_issuer: Optional[str] = None,
95
98
  auth_audience: Optional[str] = None,
96
99
  auth_enabled: bool = False,
97
- auth_enabled_for_introspection: bool = False,
100
+ auth_bypass_during_introspection: bool = True,
98
101
  ) -> None:
99
102
  """Initialize GraphQL HTTP server.
100
103
 
@@ -112,7 +115,7 @@ class GraphQLHTTP:
112
115
  auth_issuer: Expected JWT issuer
113
116
  auth_audience: Expected JWT audience
114
117
  auth_enabled: Whether to enable JWT authentication
115
- auth_enabled_for_introspection: Whether auth is required for introspection
118
+ auth_bypass_during_introspection: Whether auth is required for introspection only queries
116
119
 
117
120
  Raises:
118
121
  ValueError: If invalid configuration is provided
@@ -142,7 +145,7 @@ class GraphQLHTTP:
142
145
  self.auth_issuer = auth_issuer
143
146
  self.auth_audience = auth_audience
144
147
  self.auth_enabled = auth_enabled
145
- self.auth_enabled_for_introspection = auth_enabled_for_introspection
148
+ self.auth_bypass_during_introspection = auth_bypass_during_introspection
146
149
 
147
150
  if auth_jwks_uri:
148
151
  self.jwks_client = PyJWKClient(auth_jwks_uri)
@@ -325,8 +328,6 @@ class GraphQLHTTP:
325
328
  Returns:
326
329
  True if request is introspection-only
327
330
  """
328
- if not self.auth_enabled or self.auth_enabled_for_introspection:
329
- return False
330
331
 
331
332
  # Simple heuristic: if it contains introspection fields AND no regular fields
332
333
  query_data_lower = str(data).lower()
@@ -444,11 +445,11 @@ class GraphQLHTTP:
444
445
  # Handle authentication
445
446
  allow_only_introspection = False
446
447
  if self.auth_enabled:
447
- allow_only_introspection = self._check_introspection_only(data)
448
-
449
- if not allow_only_introspection:
450
- auth_error = self._authenticate_request(request)
451
- if auth_error:
448
+ auth_error = self._authenticate_request(request)
449
+ if auth_error:
450
+ if self.auth_bypass_during_introspection and self._check_introspection_only(data):
451
+ logger.info("Authentication bypassed as introspection only query.")
452
+ else:
452
453
  return auth_error
453
454
 
454
455
  # Prepare context for GraphQL execution
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: graphql_http
3
- Version: 2.0.2
3
+ Version: 2.0.4
4
4
  Summary: HTTP Server for GraphQL.
5
5
  Author-email: Robert Parker <rob@parob.com>
6
6
  License: MIT
@@ -163,11 +163,11 @@ class TestGraphQLHTTPAuthentication:
163
163
  assert "Invalid token" in result["errors"][0]["message"]
164
164
 
165
165
  def test_introspection_without_auth_when_disabled(self, schema):
166
- """Test introspection queries work without auth when auth_enabled_for_introspection=False."""
166
+ """Test introspection queries work without auth when auth_introspection_bypass=False."""
167
167
  server = GraphQLHTTP(
168
168
  schema=schema,
169
169
  auth_enabled=True,
170
- auth_enabled_for_introspection=False,
170
+ auth_bypass_during_introspection=False,
171
171
  auth_jwks_uri="https://example.com/.well-known/jwks.json",
172
172
  auth_issuer="https://example.com/",
173
173
  auth_audience="test-audience"
@@ -179,18 +179,18 @@ class TestGraphQLHTTPAuthentication:
179
179
  "/graphql",
180
180
  json={"query": "{ __schema { queryType { name } } }"}
181
181
  )
182
- assert response.status_code == 200
183
- # Should get a valid introspection response, not an auth error
182
+ assert response.status_code == 401
183
+ result = response.json()
184
+ assert "errors" in result
185
+ assert "Authorization header is missing" in result["errors"][0]["message"]
184
186
 
185
- @patch('graphql_http.server.PyJWKClient')
186
- def test_introspection_with_auth_when_enabled(self, mock_jwks_client_class, schema):
187
- """Test introspection queries require auth when auth_enabled_for_introspection=True."""
188
- mock_jwks_client_class.return_value = MagicMock()
187
+ def test_introspection_with_auth_when_enabled(self, schema):
188
+ """Test introspection queries require auth when auth_introspection_bypass=True."""
189
189
 
190
190
  server = GraphQLHTTP(
191
191
  schema=schema,
192
192
  auth_enabled=True,
193
- auth_enabled_for_introspection=True,
193
+ auth_bypass_during_introspection=True,
194
194
  auth_jwks_uri="https://example.com/.well-known/jwks.json",
195
195
  auth_issuer="https://example.com/",
196
196
  auth_audience="test-audience"
@@ -202,17 +202,15 @@ class TestGraphQLHTTPAuthentication:
202
202
  "/graphql",
203
203
  json={"query": "{ __schema { queryType { name } } }"}
204
204
  )
205
- assert response.status_code == 401
206
- result = response.json()
207
- assert "errors" in result
208
- assert "Authorization header is missing" in result["errors"][0]["message"]
205
+ assert response.status_code == 200
206
+ # Should get a valid introspection response, not an auth error
209
207
 
210
208
  def test_regular_query_requires_auth(self, schema):
211
209
  """Test that regular queries always require auth when auth is enabled."""
212
210
  server = GraphQLHTTP(
213
211
  schema=schema,
214
212
  auth_enabled=True,
215
- auth_enabled_for_introspection=False, # Only introspection is exempt
213
+ auth_bypass_during_introspection=False, # Only introspection is exempt
216
214
  auth_jwks_uri="https://example.com/.well-known/jwks.json",
217
215
  auth_issuer="https://example.com/",
218
216
  auth_audience="test-audience"
@@ -274,7 +272,7 @@ class TestGraphQLHTTPAuthentication:
274
272
  server = GraphQLHTTP(
275
273
  schema=schema,
276
274
  auth_enabled=True,
277
- auth_enabled_for_introspection=False,
275
+ auth_bypass_during_introspection=False,
278
276
  auth_jwks_uri="https://example.com/.well-known/jwks.json",
279
277
  auth_issuer="https://example.com/",
280
278
  auth_audience="test-audience"
@@ -1 +0,0 @@
1
- 2.0.2
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes