api-mocker 0.3.0__py3-none-any.whl → 0.5.0__py3-none-any.whl
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.
- api_mocker/auth_system.py +610 -0
- api_mocker/cli.py +380 -1
- api_mocker/database_integration.py +566 -0
- api_mocker/graphql_mock.py +593 -0
- api_mocker/ml_integration.py +709 -0
- api_mocker/mock_responses.py +622 -0
- api_mocker/websocket_mock.py +476 -0
- {api_mocker-0.3.0.dist-info → api_mocker-0.5.0.dist-info}/METADATA +15 -2
- {api_mocker-0.3.0.dist-info → api_mocker-0.5.0.dist-info}/RECORD +13 -7
- {api_mocker-0.3.0.dist-info → api_mocker-0.5.0.dist-info}/WHEEL +0 -0
- {api_mocker-0.3.0.dist-info → api_mocker-0.5.0.dist-info}/entry_points.txt +0 -0
- {api_mocker-0.3.0.dist-info → api_mocker-0.5.0.dist-info}/licenses/LICENSE +0 -0
- {api_mocker-0.3.0.dist-info → api_mocker-0.5.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GraphQL Mock Support System
|
|
3
|
+
|
|
4
|
+
This module provides comprehensive GraphQL mocking capabilities including:
|
|
5
|
+
- Schema introspection and validation
|
|
6
|
+
- Query and mutation mocking
|
|
7
|
+
- Subscription support with real-time updates
|
|
8
|
+
- Advanced type system support
|
|
9
|
+
- Custom resolvers and middleware
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import asyncio
|
|
14
|
+
from typing import Any, Dict, List, Optional, Union, Callable
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from enum import Enum
|
|
17
|
+
import re
|
|
18
|
+
from datetime import datetime
|
|
19
|
+
import uuid
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class GraphQLOperationType(Enum):
|
|
23
|
+
"""GraphQL operation types"""
|
|
24
|
+
QUERY = "query"
|
|
25
|
+
MUTATION = "mutation"
|
|
26
|
+
SUBSCRIPTION = "subscription"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class GraphQLScalarType(Enum):
|
|
30
|
+
"""GraphQL scalar types"""
|
|
31
|
+
STRING = "String"
|
|
32
|
+
INT = "Int"
|
|
33
|
+
FLOAT = "Float"
|
|
34
|
+
BOOLEAN = "Boolean"
|
|
35
|
+
ID = "ID"
|
|
36
|
+
DATE = "Date"
|
|
37
|
+
DATETIME = "DateTime"
|
|
38
|
+
JSON = "JSON"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class GraphQLField:
|
|
43
|
+
"""Represents a GraphQL field definition"""
|
|
44
|
+
name: str
|
|
45
|
+
type: str
|
|
46
|
+
description: Optional[str] = None
|
|
47
|
+
args: List[Dict[str, Any]] = field(default_factory=list)
|
|
48
|
+
is_required: bool = False
|
|
49
|
+
is_list: bool = False
|
|
50
|
+
is_nullable: bool = True
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class GraphQLType:
|
|
55
|
+
"""Represents a GraphQL type definition"""
|
|
56
|
+
name: str
|
|
57
|
+
kind: str # OBJECT, SCALAR, ENUM, INTERFACE, UNION, INPUT_OBJECT
|
|
58
|
+
description: Optional[str] = None
|
|
59
|
+
fields: List[GraphQLField] = field(default_factory=list)
|
|
60
|
+
interfaces: List[str] = field(default_factory=list)
|
|
61
|
+
possible_types: List[str] = field(default_factory=list)
|
|
62
|
+
enum_values: List[Dict[str, Any]] = field(default_factory=list)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class GraphQLMockResolver:
|
|
67
|
+
"""Represents a custom resolver for GraphQL operations"""
|
|
68
|
+
field_name: str
|
|
69
|
+
type_name: str
|
|
70
|
+
resolver_func: Callable
|
|
71
|
+
description: Optional[str] = None
|
|
72
|
+
args: List[Dict[str, Any]] = field(default_factory=list)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass
|
|
76
|
+
class GraphQLMockResponse:
|
|
77
|
+
"""Represents a mock response for GraphQL operations"""
|
|
78
|
+
operation_name: str
|
|
79
|
+
operation_type: GraphQLOperationType
|
|
80
|
+
query: str
|
|
81
|
+
variables: Dict[str, Any] = field(default_factory=dict)
|
|
82
|
+
response_data: Dict[str, Any] = field(default_factory=dict)
|
|
83
|
+
errors: List[Dict[str, Any]] = field(default_factory=list)
|
|
84
|
+
extensions: Dict[str, Any] = field(default_factory=dict)
|
|
85
|
+
delay_ms: int = 0
|
|
86
|
+
error_probability: float = 0.0
|
|
87
|
+
conditions: List[Dict[str, Any]] = field(default_factory=list)
|
|
88
|
+
priority: int = 0
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class GraphQLSchemaManager:
|
|
92
|
+
"""Manages GraphQL schema definitions and introspection"""
|
|
93
|
+
|
|
94
|
+
def __init__(self):
|
|
95
|
+
self.types: Dict[str, GraphQLType] = {}
|
|
96
|
+
self.queries: Dict[str, GraphQLField] = {}
|
|
97
|
+
self.mutations: Dict[str, GraphQLField] = {}
|
|
98
|
+
self.subscriptions: Dict[str, GraphQLField] = {}
|
|
99
|
+
self.resolvers: List[GraphQLMockResolver] = []
|
|
100
|
+
|
|
101
|
+
def add_type(self, type_def: GraphQLType) -> None:
|
|
102
|
+
"""Add a type definition to the schema"""
|
|
103
|
+
self.types[type_def.name] = type_def
|
|
104
|
+
|
|
105
|
+
def add_query(self, field: GraphQLField) -> None:
|
|
106
|
+
"""Add a query field to the schema"""
|
|
107
|
+
self.queries[field.name] = field
|
|
108
|
+
|
|
109
|
+
def add_mutation(self, field: GraphQLField) -> None:
|
|
110
|
+
"""Add a mutation field to the schema"""
|
|
111
|
+
self.mutations[field.name] = field
|
|
112
|
+
|
|
113
|
+
def add_subscription(self, field: GraphQLField) -> None:
|
|
114
|
+
"""Add a subscription field to the schema"""
|
|
115
|
+
self.subscriptions[field.name] = field
|
|
116
|
+
|
|
117
|
+
def add_resolver(self, resolver: GraphQLMockResolver) -> None:
|
|
118
|
+
"""Add a custom resolver"""
|
|
119
|
+
self.resolvers.append(resolver)
|
|
120
|
+
|
|
121
|
+
def get_schema_introspection(self) -> Dict[str, Any]:
|
|
122
|
+
"""Generate GraphQL schema introspection data"""
|
|
123
|
+
return {
|
|
124
|
+
"__schema": {
|
|
125
|
+
"queryType": {"name": "Query"},
|
|
126
|
+
"mutationType": {"name": "Mutation"} if self.mutations else None,
|
|
127
|
+
"subscriptionType": {"name": "Subscription"} if self.subscriptions else None,
|
|
128
|
+
"types": [self._type_to_introspection(type_def) for type_def in self.types.values()],
|
|
129
|
+
"directives": []
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
def _type_to_introspection(self, type_def: GraphQLType) -> Dict[str, Any]:
|
|
134
|
+
"""Convert type definition to introspection format"""
|
|
135
|
+
result = {
|
|
136
|
+
"name": type_def.name,
|
|
137
|
+
"kind": type_def.kind.upper(),
|
|
138
|
+
"description": type_def.description
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if type_def.fields:
|
|
142
|
+
result["fields"] = [
|
|
143
|
+
{
|
|
144
|
+
"name": field.name,
|
|
145
|
+
"type": self._field_type_to_introspection(field),
|
|
146
|
+
"description": field.description,
|
|
147
|
+
"args": [
|
|
148
|
+
{
|
|
149
|
+
"name": arg["name"],
|
|
150
|
+
"type": self._field_type_to_introspection(arg),
|
|
151
|
+
"description": arg.get("description")
|
|
152
|
+
}
|
|
153
|
+
for arg in field.args
|
|
154
|
+
]
|
|
155
|
+
}
|
|
156
|
+
for field in type_def.fields
|
|
157
|
+
]
|
|
158
|
+
|
|
159
|
+
if type_def.enum_values:
|
|
160
|
+
result["enumValues"] = [
|
|
161
|
+
{
|
|
162
|
+
"name": enum_val["name"],
|
|
163
|
+
"description": enum_val.get("description"),
|
|
164
|
+
"isDeprecated": enum_val.get("isDeprecated", False)
|
|
165
|
+
}
|
|
166
|
+
for enum_val in type_def.enum_values
|
|
167
|
+
]
|
|
168
|
+
|
|
169
|
+
return result
|
|
170
|
+
|
|
171
|
+
def _field_type_to_introspection(self, field: Union[GraphQLField, Dict[str, Any]]) -> Dict[str, Any]:
|
|
172
|
+
"""Convert field type to introspection format"""
|
|
173
|
+
if isinstance(field, GraphQLField):
|
|
174
|
+
type_name = field.type
|
|
175
|
+
is_required = field.is_required
|
|
176
|
+
is_list = field.is_list
|
|
177
|
+
else:
|
|
178
|
+
type_name = field["type"]
|
|
179
|
+
is_required = field.get("isRequired", False)
|
|
180
|
+
is_list = field.get("isList", False)
|
|
181
|
+
|
|
182
|
+
result = {"name": type_name}
|
|
183
|
+
|
|
184
|
+
if is_list:
|
|
185
|
+
result = {
|
|
186
|
+
"kind": "LIST",
|
|
187
|
+
"ofType": result
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if not is_required:
|
|
191
|
+
result = {
|
|
192
|
+
"kind": "NON_NULL",
|
|
193
|
+
"ofType": result
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return result
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class GraphQLMockServer:
|
|
200
|
+
"""Main GraphQL mock server implementation"""
|
|
201
|
+
|
|
202
|
+
def __init__(self):
|
|
203
|
+
self.schema_manager = GraphQLSchemaManager()
|
|
204
|
+
self.mock_responses: List[GraphQLMockResponse] = []
|
|
205
|
+
self.subscription_connections: Dict[str, asyncio.Queue] = {}
|
|
206
|
+
self._setup_default_schema()
|
|
207
|
+
|
|
208
|
+
def _setup_default_schema(self) -> None:
|
|
209
|
+
"""Setup default GraphQL schema with common types"""
|
|
210
|
+
# User type
|
|
211
|
+
user_type = GraphQLType(
|
|
212
|
+
name="User",
|
|
213
|
+
kind="OBJECT",
|
|
214
|
+
description="A user in the system",
|
|
215
|
+
fields=[
|
|
216
|
+
GraphQLField("id", "ID!", is_required=True),
|
|
217
|
+
GraphQLField("name", "String!", is_required=True),
|
|
218
|
+
GraphQLField("email", "String!", is_required=True),
|
|
219
|
+
GraphQLField("createdAt", "DateTime!"),
|
|
220
|
+
GraphQLField("posts", "[Post!]")
|
|
221
|
+
]
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
# Post type
|
|
225
|
+
post_type = GraphQLType(
|
|
226
|
+
name="Post",
|
|
227
|
+
kind="OBJECT",
|
|
228
|
+
description="A blog post",
|
|
229
|
+
fields=[
|
|
230
|
+
GraphQLField("id", "ID!", is_required=True),
|
|
231
|
+
GraphQLField("title", "String!", is_required=True),
|
|
232
|
+
GraphQLField("content", "String!"),
|
|
233
|
+
GraphQLField("author", "User!"),
|
|
234
|
+
GraphQLField("publishedAt", "DateTime")
|
|
235
|
+
]
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
# Query type
|
|
239
|
+
query_type = GraphQLType(
|
|
240
|
+
name="Query",
|
|
241
|
+
kind="OBJECT",
|
|
242
|
+
fields=[
|
|
243
|
+
GraphQLField("user", "User", args=[
|
|
244
|
+
{"name": "id", "type": "ID!", "isRequired": True}
|
|
245
|
+
]),
|
|
246
|
+
GraphQLField("users", "[User!]"),
|
|
247
|
+
GraphQLField("post", "Post", args=[
|
|
248
|
+
{"name": "id", "type": "ID!", "isRequired": True}
|
|
249
|
+
]),
|
|
250
|
+
GraphQLField("posts", "[Post!]")
|
|
251
|
+
]
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
# Mutation type
|
|
255
|
+
mutation_type = GraphQLType(
|
|
256
|
+
name="Mutation",
|
|
257
|
+
kind="OBJECT",
|
|
258
|
+
fields=[
|
|
259
|
+
GraphQLField("createUser", "User!", args=[
|
|
260
|
+
{"name": "input", "type": "CreateUserInput!", "isRequired": True}
|
|
261
|
+
]),
|
|
262
|
+
GraphQLField("updateUser", "User!", args=[
|
|
263
|
+
{"name": "id", "type": "ID!", "isRequired": True},
|
|
264
|
+
{"name": "input", "type": "UpdateUserInput!", "isRequired": True}
|
|
265
|
+
]),
|
|
266
|
+
GraphQLField("deleteUser", "Boolean!", args=[
|
|
267
|
+
{"name": "id", "type": "ID!", "isRequired": True}
|
|
268
|
+
])
|
|
269
|
+
]
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
# Input types
|
|
273
|
+
create_user_input = GraphQLType(
|
|
274
|
+
name="CreateUserInput",
|
|
275
|
+
kind="INPUT_OBJECT",
|
|
276
|
+
fields=[
|
|
277
|
+
GraphQLField("name", "String!", is_required=True),
|
|
278
|
+
GraphQLField("email", "String!", is_required=True)
|
|
279
|
+
]
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
update_user_input = GraphQLType(
|
|
283
|
+
name="UpdateUserInput",
|
|
284
|
+
kind="INPUT_OBJECT",
|
|
285
|
+
fields=[
|
|
286
|
+
GraphQLField("name", "String"),
|
|
287
|
+
GraphQLField("email", "String")
|
|
288
|
+
]
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
# Add types to schema
|
|
292
|
+
self.schema_manager.add_type(user_type)
|
|
293
|
+
self.schema_manager.add_type(post_type)
|
|
294
|
+
self.schema_manager.add_type(query_type)
|
|
295
|
+
self.schema_manager.add_type(mutation_type)
|
|
296
|
+
self.schema_manager.add_type(create_user_input)
|
|
297
|
+
self.schema_manager.add_type(update_user_input)
|
|
298
|
+
|
|
299
|
+
# Add query fields
|
|
300
|
+
for field in query_type.fields:
|
|
301
|
+
self.schema_manager.add_query(field)
|
|
302
|
+
|
|
303
|
+
# Add mutation fields
|
|
304
|
+
for field in mutation_type.fields:
|
|
305
|
+
self.schema_manager.add_mutation(field)
|
|
306
|
+
|
|
307
|
+
def add_mock_response(self, response: GraphQLMockResponse) -> None:
|
|
308
|
+
"""Add a mock response for GraphQL operations"""
|
|
309
|
+
self.mock_responses.append(response)
|
|
310
|
+
# Sort by priority (higher priority first)
|
|
311
|
+
self.mock_responses.sort(key=lambda x: x.priority, reverse=True)
|
|
312
|
+
|
|
313
|
+
def create_mock_response(self, operation_name: str, operation_type: GraphQLOperationType,
|
|
314
|
+
query: str, response_data: Dict[str, Any],
|
|
315
|
+
variables: Dict[str, Any] = None,
|
|
316
|
+
delay_ms: int = 0, error_probability: float = 0.0,
|
|
317
|
+
conditions: List[Dict[str, Any]] = None,
|
|
318
|
+
priority: int = 0) -> GraphQLMockResponse:
|
|
319
|
+
"""Create a mock response for GraphQL operations"""
|
|
320
|
+
return GraphQLMockResponse(
|
|
321
|
+
operation_name=operation_name,
|
|
322
|
+
operation_type=operation_type,
|
|
323
|
+
query=query,
|
|
324
|
+
variables=variables or {},
|
|
325
|
+
response_data=response_data,
|
|
326
|
+
delay_ms=delay_ms,
|
|
327
|
+
error_probability=error_probability,
|
|
328
|
+
conditions=conditions or [],
|
|
329
|
+
priority=priority
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
def find_matching_response(self, query: str, variables: Dict[str, Any] = None,
|
|
333
|
+
operation_name: str = None) -> Optional[GraphQLMockResponse]:
|
|
334
|
+
"""Find the best matching response for a GraphQL query"""
|
|
335
|
+
variables = variables or {}
|
|
336
|
+
|
|
337
|
+
for response in self.mock_responses:
|
|
338
|
+
if self._matches_query(response, query, variables, operation_name):
|
|
339
|
+
return response
|
|
340
|
+
|
|
341
|
+
return None
|
|
342
|
+
|
|
343
|
+
def _matches_query(self, response: GraphQLMockResponse, query: str,
|
|
344
|
+
variables: Dict[str, Any], operation_name: str = None) -> bool:
|
|
345
|
+
"""Check if a response matches the given query"""
|
|
346
|
+
# Check operation name
|
|
347
|
+
if operation_name and response.operation_name != operation_name:
|
|
348
|
+
return False
|
|
349
|
+
|
|
350
|
+
# Check query matching (exact or pattern)
|
|
351
|
+
if response.query != query and not self._query_pattern_match(response.query, query):
|
|
352
|
+
return False
|
|
353
|
+
|
|
354
|
+
# Check variables
|
|
355
|
+
if response.variables:
|
|
356
|
+
for key, value in response.variables.items():
|
|
357
|
+
if variables.get(key) != value:
|
|
358
|
+
return False
|
|
359
|
+
|
|
360
|
+
# Check conditions
|
|
361
|
+
if response.conditions:
|
|
362
|
+
return self._check_conditions(response.conditions, variables)
|
|
363
|
+
|
|
364
|
+
return True
|
|
365
|
+
|
|
366
|
+
def _query_pattern_match(self, pattern: str, query: str) -> bool:
|
|
367
|
+
"""Check if query matches a pattern"""
|
|
368
|
+
# Simple pattern matching - can be enhanced
|
|
369
|
+
if '*' in pattern:
|
|
370
|
+
regex_pattern = pattern.replace('*', '.*')
|
|
371
|
+
return bool(re.match(regex_pattern, query))
|
|
372
|
+
return False
|
|
373
|
+
|
|
374
|
+
def _check_conditions(self, conditions: List[Dict[str, Any]], variables: Dict[str, Any]) -> bool:
|
|
375
|
+
"""Check if conditions are met"""
|
|
376
|
+
for condition in conditions:
|
|
377
|
+
if not self._evaluate_condition(condition, variables):
|
|
378
|
+
return False
|
|
379
|
+
return True
|
|
380
|
+
|
|
381
|
+
def _evaluate_condition(self, condition: Dict[str, Any], variables: Dict[str, Any]) -> bool:
|
|
382
|
+
"""Evaluate a single condition"""
|
|
383
|
+
condition_type = condition.get('type', 'variable')
|
|
384
|
+
|
|
385
|
+
if condition_type == 'variable':
|
|
386
|
+
field_path = condition.get('field')
|
|
387
|
+
expected_value = condition.get('value')
|
|
388
|
+
actual_value = self._get_nested_value(variables, field_path)
|
|
389
|
+
return actual_value == expected_value
|
|
390
|
+
|
|
391
|
+
elif condition_type == 'custom':
|
|
392
|
+
func = condition.get('function')
|
|
393
|
+
return func(variables) if callable(func) else False
|
|
394
|
+
|
|
395
|
+
return False
|
|
396
|
+
|
|
397
|
+
def _get_nested_value(self, obj: Any, path: str) -> Any:
|
|
398
|
+
"""Get nested value from object using dot notation"""
|
|
399
|
+
if not path:
|
|
400
|
+
return obj
|
|
401
|
+
|
|
402
|
+
keys = path.split('.')
|
|
403
|
+
current = obj
|
|
404
|
+
|
|
405
|
+
for key in keys:
|
|
406
|
+
if isinstance(current, dict):
|
|
407
|
+
current = current.get(key)
|
|
408
|
+
elif isinstance(current, list) and key.isdigit():
|
|
409
|
+
current = current[int(key)]
|
|
410
|
+
else:
|
|
411
|
+
return None
|
|
412
|
+
|
|
413
|
+
return current
|
|
414
|
+
|
|
415
|
+
async def execute_query(self, query: str, variables: Dict[str, Any] = None,
|
|
416
|
+
operation_name: str = None) -> Dict[str, Any]:
|
|
417
|
+
"""Execute a GraphQL query and return mock response"""
|
|
418
|
+
variables = variables or {}
|
|
419
|
+
|
|
420
|
+
# Find matching response
|
|
421
|
+
response = self.find_matching_response(query, variables, operation_name)
|
|
422
|
+
|
|
423
|
+
if not response:
|
|
424
|
+
return {
|
|
425
|
+
"data": None,
|
|
426
|
+
"errors": [{"message": "No mock response found for query"}]
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
# Check for errors
|
|
430
|
+
if response.error_probability > 0 and self._should_return_error(response.error_probability):
|
|
431
|
+
return {
|
|
432
|
+
"data": None,
|
|
433
|
+
"errors": [{"message": "Mock error response", "extensions": {"code": "MOCK_ERROR"}}]
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
# Apply delay
|
|
437
|
+
if response.delay_ms > 0:
|
|
438
|
+
await asyncio.sleep(response.delay_ms / 1000.0)
|
|
439
|
+
|
|
440
|
+
# Generate response data
|
|
441
|
+
data = self._generate_response_data(response, variables)
|
|
442
|
+
|
|
443
|
+
result = {
|
|
444
|
+
"data": data,
|
|
445
|
+
"errors": response.errors,
|
|
446
|
+
"extensions": response.extensions
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return result
|
|
450
|
+
|
|
451
|
+
def _should_return_error(self, error_probability: float) -> bool:
|
|
452
|
+
"""Determine if an error should be returned"""
|
|
453
|
+
import random
|
|
454
|
+
return random.random() < error_probability
|
|
455
|
+
|
|
456
|
+
def _generate_response_data(self, response: GraphQLMockResponse, variables: Dict[str, Any]) -> Dict[str, Any]:
|
|
457
|
+
"""Generate response data with variable substitution"""
|
|
458
|
+
data = response.response_data.copy()
|
|
459
|
+
|
|
460
|
+
# Substitute variables in response data
|
|
461
|
+
data = self._substitute_variables(data, variables)
|
|
462
|
+
|
|
463
|
+
return data
|
|
464
|
+
|
|
465
|
+
def _substitute_variables(self, data: Any, variables: Dict[str, Any]) -> Any:
|
|
466
|
+
"""Recursively substitute variables in data"""
|
|
467
|
+
if isinstance(data, dict):
|
|
468
|
+
return {key: self._substitute_variables(value, variables) for key, value in data.items()}
|
|
469
|
+
elif isinstance(data, list):
|
|
470
|
+
return [self._substitute_variables(item, variables) for item in data]
|
|
471
|
+
elif isinstance(data, str) and data.startswith('{{') and data.endswith('}}'):
|
|
472
|
+
var_name = data[2:-2]
|
|
473
|
+
return variables.get(var_name, data)
|
|
474
|
+
else:
|
|
475
|
+
return data
|
|
476
|
+
|
|
477
|
+
async def handle_subscription(self, query: str, variables: Dict[str, Any] = None,
|
|
478
|
+
operation_name: str = None) -> asyncio.Queue:
|
|
479
|
+
"""Handle GraphQL subscription with real-time updates"""
|
|
480
|
+
connection_id = str(uuid.uuid4())
|
|
481
|
+
queue = asyncio.Queue()
|
|
482
|
+
self.subscription_connections[connection_id] = queue
|
|
483
|
+
|
|
484
|
+
# Start subscription task
|
|
485
|
+
asyncio.create_task(self._subscription_worker(connection_id, query, variables, operation_name))
|
|
486
|
+
|
|
487
|
+
return queue
|
|
488
|
+
|
|
489
|
+
async def _subscription_worker(self, connection_id: str, query: str,
|
|
490
|
+
variables: Dict[str, Any], operation_name: str) -> None:
|
|
491
|
+
"""Worker for handling subscription updates"""
|
|
492
|
+
try:
|
|
493
|
+
while connection_id in self.subscription_connections:
|
|
494
|
+
# Generate subscription data
|
|
495
|
+
data = await self._generate_subscription_data(query, variables, operation_name)
|
|
496
|
+
|
|
497
|
+
if connection_id in self.subscription_connections:
|
|
498
|
+
await self.subscription_connections[connection_id].put(data)
|
|
499
|
+
|
|
500
|
+
# Wait before next update
|
|
501
|
+
await asyncio.sleep(1.0) # 1 second interval
|
|
502
|
+
except Exception as e:
|
|
503
|
+
print(f"Subscription error: {e}")
|
|
504
|
+
finally:
|
|
505
|
+
if connection_id in self.subscription_connections:
|
|
506
|
+
del self.subscription_connections[connection_id]
|
|
507
|
+
|
|
508
|
+
async def _generate_subscription_data(self, query: str, variables: Dict[str, Any],
|
|
509
|
+
operation_name: str) -> Dict[str, Any]:
|
|
510
|
+
"""Generate subscription data"""
|
|
511
|
+
# This would typically generate real-time data
|
|
512
|
+
# For now, return mock data
|
|
513
|
+
return {
|
|
514
|
+
"data": {
|
|
515
|
+
"subscriptionUpdate": {
|
|
516
|
+
"id": str(uuid.uuid4()),
|
|
517
|
+
"timestamp": datetime.now().isoformat(),
|
|
518
|
+
"message": "Real-time update"
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
def get_schema(self) -> Dict[str, Any]:
|
|
524
|
+
"""Get the GraphQL schema"""
|
|
525
|
+
return self.schema_manager.get_schema_introspection()
|
|
526
|
+
|
|
527
|
+
def add_custom_resolver(self, field_name: str, type_name: str, resolver_func: Callable) -> None:
|
|
528
|
+
"""Add a custom resolver for a field"""
|
|
529
|
+
resolver = GraphQLMockResolver(
|
|
530
|
+
field_name=field_name,
|
|
531
|
+
type_name=type_name,
|
|
532
|
+
resolver_func=resolver_func
|
|
533
|
+
)
|
|
534
|
+
self.schema_manager.add_resolver(resolver)
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
# Global GraphQL mock server instance
|
|
538
|
+
graphql_mock_server = GraphQLMockServer()
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
# Convenience functions
|
|
542
|
+
def create_user_query_mock(user_id: str = "123", name: str = "John Doe") -> GraphQLMockResponse:
|
|
543
|
+
"""Create a mock response for user query"""
|
|
544
|
+
return GraphQLMockResponse(
|
|
545
|
+
operation_name="GetUser",
|
|
546
|
+
operation_type=GraphQLOperationType.QUERY,
|
|
547
|
+
query="query GetUser($id: ID!) { user(id: $id) { id name email createdAt } }",
|
|
548
|
+
variables={"id": user_id},
|
|
549
|
+
response_data={
|
|
550
|
+
"user": {
|
|
551
|
+
"id": "{{id}}",
|
|
552
|
+
"name": name,
|
|
553
|
+
"email": "john@example.com",
|
|
554
|
+
"createdAt": "2023-01-01T00:00:00Z"
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def create_post_mutation_mock(title: str = "Sample Post", content: str = "Post content") -> GraphQLMockResponse:
|
|
561
|
+
"""Create a mock response for post creation mutation"""
|
|
562
|
+
return GraphQLMockResponse(
|
|
563
|
+
operation_name="CreatePost",
|
|
564
|
+
operation_type=GraphQLOperationType.MUTATION,
|
|
565
|
+
query="mutation CreatePost($input: CreatePostInput!) { createPost(input: $input) { id title content author { id name } } }",
|
|
566
|
+
response_data={
|
|
567
|
+
"createPost": {
|
|
568
|
+
"id": "{{post_id}}",
|
|
569
|
+
"title": title,
|
|
570
|
+
"content": content,
|
|
571
|
+
"author": {
|
|
572
|
+
"id": "{{author_id}}",
|
|
573
|
+
"name": "{{author_name}}"
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def create_subscription_mock(topic: str = "updates") -> GraphQLMockResponse:
|
|
581
|
+
"""Create a mock response for subscription"""
|
|
582
|
+
return GraphQLMockResponse(
|
|
583
|
+
operation_name="SubscribeToUpdates",
|
|
584
|
+
operation_type=GraphQLOperationType.SUBSCRIPTION,
|
|
585
|
+
query="subscription SubscribeToUpdates { subscriptionUpdate { id timestamp message } }",
|
|
586
|
+
response_data={
|
|
587
|
+
"subscriptionUpdate": {
|
|
588
|
+
"id": "{{update_id}}",
|
|
589
|
+
"timestamp": "{{timestamp}}",
|
|
590
|
+
"message": f"Update for {topic}"
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
)
|