api-mocker 0.4.0__py3-none-any.whl → 0.5.1__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.
@@ -0,0 +1,602 @@
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):
472
+ # Handle full replacement (preserving type if variable is not string)
473
+ if data.startswith('{{') and data.endswith('}}'):
474
+ var_name = data[2:-2]
475
+ if var_name in variables:
476
+ return variables[var_name]
477
+
478
+ # Handle partial replacement (string interpolation)
479
+ if '{{' in data and '}}' in data:
480
+ for key, value in variables.items():
481
+ data = data.replace(f"{{{{{key}}}}}", str(value))
482
+ return data
483
+ else:
484
+ return data
485
+
486
+ async def handle_subscription(self, query: str, variables: Dict[str, Any] = None,
487
+ operation_name: str = None) -> asyncio.Queue:
488
+ """Handle GraphQL subscription with real-time updates"""
489
+ connection_id = str(uuid.uuid4())
490
+ queue = asyncio.Queue()
491
+ self.subscription_connections[connection_id] = queue
492
+
493
+ # Start subscription task
494
+ asyncio.create_task(self._subscription_worker(connection_id, query, variables, operation_name))
495
+
496
+ return queue
497
+
498
+ async def _subscription_worker(self, connection_id: str, query: str,
499
+ variables: Dict[str, Any], operation_name: str) -> None:
500
+ """Worker for handling subscription updates"""
501
+ try:
502
+ while connection_id in self.subscription_connections:
503
+ # Generate subscription data
504
+ data = await self._generate_subscription_data(query, variables, operation_name)
505
+
506
+ if connection_id in self.subscription_connections:
507
+ await self.subscription_connections[connection_id].put(data)
508
+
509
+ # Wait before next update
510
+ await asyncio.sleep(1.0) # 1 second interval
511
+ except Exception as e:
512
+ print(f"Subscription error: {e}")
513
+ finally:
514
+ if connection_id in self.subscription_connections:
515
+ del self.subscription_connections[connection_id]
516
+
517
+ async def _generate_subscription_data(self, query: str, variables: Dict[str, Any],
518
+ operation_name: str) -> Dict[str, Any]:
519
+ """Generate subscription data"""
520
+ # This would typically generate real-time data
521
+ # For now, return mock data
522
+ return {
523
+ "data": {
524
+ "subscriptionUpdate": {
525
+ "id": str(uuid.uuid4()),
526
+ "timestamp": datetime.now().isoformat(),
527
+ "message": "Real-time update"
528
+ }
529
+ }
530
+ }
531
+
532
+ def get_schema(self) -> Dict[str, Any]:
533
+ """Get the GraphQL schema"""
534
+ return self.schema_manager.get_schema_introspection()
535
+
536
+ def add_custom_resolver(self, field_name: str, type_name: str, resolver_func: Callable) -> None:
537
+ """Add a custom resolver for a field"""
538
+ resolver = GraphQLMockResolver(
539
+ field_name=field_name,
540
+ type_name=type_name,
541
+ resolver_func=resolver_func
542
+ )
543
+ self.schema_manager.add_resolver(resolver)
544
+
545
+
546
+ # Global GraphQL mock server instance
547
+ graphql_mock_server = GraphQLMockServer()
548
+
549
+
550
+ # Convenience functions
551
+ def create_user_query_mock(user_id: str = "123", name: str = "John Doe") -> GraphQLMockResponse:
552
+ """Create a mock response for user query"""
553
+ return GraphQLMockResponse(
554
+ operation_name="GetUser",
555
+ operation_type=GraphQLOperationType.QUERY,
556
+ query="query GetUser($id: ID!) { user(id: $id) { id name email createdAt } }",
557
+ variables={"id": user_id},
558
+ response_data={
559
+ "user": {
560
+ "id": "{{id}}",
561
+ "name": name,
562
+ "email": "john@example.com",
563
+ "createdAt": "2023-01-01T00:00:00Z"
564
+ }
565
+ }
566
+ )
567
+
568
+
569
+ def create_post_mutation_mock(title: str = "Sample Post", content: str = "Post content") -> GraphQLMockResponse:
570
+ """Create a mock response for post creation mutation"""
571
+ return GraphQLMockResponse(
572
+ operation_name="CreatePost",
573
+ operation_type=GraphQLOperationType.MUTATION,
574
+ query="mutation CreatePost($input: CreatePostInput!) { createPost(input: $input) { id title content author { id name } } }",
575
+ response_data={
576
+ "createPost": {
577
+ "id": "{{post_id}}",
578
+ "title": title,
579
+ "content": content,
580
+ "author": {
581
+ "id": "{{author_id}}",
582
+ "name": "{{author_name}}"
583
+ }
584
+ }
585
+ }
586
+ )
587
+
588
+
589
+ def create_subscription_mock(topic: str = "updates") -> GraphQLMockResponse:
590
+ """Create a mock response for subscription"""
591
+ return GraphQLMockResponse(
592
+ operation_name="SubscribeToUpdates",
593
+ operation_type=GraphQLOperationType.SUBSCRIPTION,
594
+ query="subscription SubscribeToUpdates { subscriptionUpdate { id timestamp message } }",
595
+ response_data={
596
+ "subscriptionUpdate": {
597
+ "id": "{{update_id}}",
598
+ "timestamp": "{{timestamp}}",
599
+ "message": f"Update for {topic}"
600
+ }
601
+ }
602
+ )