graphql-http 2.0.3__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.3 → graphql_http-2.0.4}/PKG-INFO +1 -1
  2. graphql_http-2.0.4/VERSION +1 -0
  3. {graphql_http-2.0.3 → graphql_http-2.0.4}/examples/advanced_server.py +31 -33
  4. {graphql_http-2.0.3 → graphql_http-2.0.4}/examples/auth_server.py +4 -4
  5. {graphql_http-2.0.3 → graphql_http-2.0.4}/examples/graphql_api_server.py +55 -56
  6. {graphql_http-2.0.3 → graphql_http-2.0.4}/graphql_http/server.py +8 -3
  7. {graphql_http-2.0.3 → graphql_http-2.0.4}/graphql_http.egg-info/PKG-INFO +1 -1
  8. graphql_http-2.0.3/VERSION +0 -1
  9. {graphql_http-2.0.3 → graphql_http-2.0.4}/.coverage +0 -0
  10. {graphql_http-2.0.3 → graphql_http-2.0.4}/.github/workflows/python-package.yml +0 -0
  11. {graphql_http-2.0.3 → graphql_http-2.0.4}/.gitignore +0 -0
  12. {graphql_http-2.0.3 → graphql_http-2.0.4}/LICENSE +0 -0
  13. {graphql_http-2.0.3 → graphql_http-2.0.4}/README.md +0 -0
  14. {graphql_http-2.0.3 → graphql_http-2.0.4}/examples/README.md +0 -0
  15. {graphql_http-2.0.3 → graphql_http-2.0.4}/examples/basic_server.py +0 -0
  16. {graphql_http-2.0.3 → graphql_http-2.0.4}/graphql_http/__init__.py +0 -0
  17. {graphql_http-2.0.3 → graphql_http-2.0.4}/graphql_http/error.py +0 -0
  18. {graphql_http-2.0.3 → graphql_http-2.0.4}/graphql_http/graphiql/index.html +0 -0
  19. {graphql_http-2.0.3 → graphql_http-2.0.4}/graphql_http/helpers.py +0 -0
  20. {graphql_http-2.0.3 → graphql_http-2.0.4}/graphql_http.egg-info/SOURCES.txt +0 -0
  21. {graphql_http-2.0.3 → graphql_http-2.0.4}/graphql_http.egg-info/dependency_links.txt +0 -0
  22. {graphql_http-2.0.3 → graphql_http-2.0.4}/graphql_http.egg-info/requires.txt +0 -0
  23. {graphql_http-2.0.3 → graphql_http-2.0.4}/graphql_http.egg-info/top_level.txt +0 -0
  24. {graphql_http-2.0.3 → graphql_http-2.0.4}/pyproject.toml +0 -0
  25. {graphql_http-2.0.3 → graphql_http-2.0.4}/setup.cfg +0 -0
  26. {graphql_http-2.0.3 → graphql_http-2.0.4}/test_server.py +0 -0
  27. {graphql_http-2.0.3 → graphql_http-2.0.4}/tests/__init__.py +0 -0
  28. {graphql_http-2.0.3 → graphql_http-2.0.4}/tests/app.py +0 -0
  29. {graphql_http-2.0.3 → graphql_http-2.0.4}/tests/app_federation.py +0 -0
  30. {graphql_http-2.0.3 → graphql_http-2.0.4}/tests/conftest.py +0 -0
  31. {graphql_http-2.0.3 → graphql_http-2.0.4}/tests/test_auth.py +0 -0
  32. {graphql_http-2.0.3 → graphql_http-2.0.4}/tests/test_cors.py +0 -0
  33. {graphql_http-2.0.3 → graphql_http-2.0.4}/tests/test_federation.py +0 -0
  34. {graphql_http-2.0.3 → graphql_http-2.0.4}/tests/test_graphql_api.py +0 -0
  35. {graphql_http-2.0.3 → graphql_http-2.0.4}/tests/test_server.py +0 -0
  36. {graphql_http-2.0.3 → 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.3
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
 
@@ -221,10 +221,10 @@ def main():
221
221
  print("\nAuthentication disabled for development!")
222
222
  print("Set ENABLE_AUTH=true to enable authentication")
223
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")
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
228
 
229
229
  # Run the server
230
230
  server.run(host="0.0.0.0", port=8000)
@@ -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"
@@ -442,9 +445,11 @@ class GraphQLHTTP:
442
445
  # Handle authentication
443
446
  allow_only_introspection = False
444
447
  if self.auth_enabled:
445
- if not self.auth_bypass_during_introspection or not self._check_introspection_only(data):
446
- auth_error = self._authenticate_request(request)
447
- 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:
448
453
  return auth_error
449
454
 
450
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.3
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
@@ -1 +0,0 @@
1
- 2.0.3
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes