Coverage for src/lexigram/graphql/core/batching.py: 94%
52 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-25 04:37 +0800
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-25 04:37 +0800
1"""GraphQL query batching support.
3This module provides batch execution of multiple GraphQL operations
4in a single HTTP request.
5"""
7from __future__ import annotations
9import asyncio
10from dataclasses import dataclass, field
11from typing import Any, cast
13from lexigram.graphql.core.context import (
14 GraphQLContext,
15 GraphQLRequest,
16 GraphQLResponse,
17)
18from lexigram.graphql.exceptions import InputGraphQLError
19from lexigram.logging import get_logger
21logger = get_logger(__name__)
24@dataclass
25class BatchResult:
26 """Result of batch execution.
28 Attributes:
29 responses: List of GraphQL responses.
30 total_count: Total number of operations.
31 success_count: Number of successful operations.
32 error_count: Number of failed operations.
33 """
35 responses: list[GraphQLResponse[Any] | Exception] = field(default_factory=list)
36 total_count: int = 0
37 success_count: int = 0
38 error_count: int = 0
41class BatchExecutor:
42 """Execute multiple GraphQL operations in a single request.
44 Allows clients to send an array of operations in a single HTTP request,
45 improving performance for multiple independent queries.
47 Example:
48 ```python
49 executor = GraphQLExecutorProtocol(schema)
50 batch_executor = BatchExecutor(executor, max_batch_size=10)
52 operations = [
53 GraphQLRequest(query="{ user(id: 1) { name } }"),
54 GraphQLRequest(query="{ user(id: 2) { name } }"),
55 ]
57 results = await batch_executor.execute_batch(operations, context)
58 ```
59 """
61 def __init__(
62 self,
63 executor: Any, # GraphQLExecutorProtocol
64 max_batch_size: int = 10,
65 ):
66 """Initialize the batch executor.
68 Args:
69 executor: The GraphQL executor to use.
70 max_batch_size: Maximum number of operations per batch.
71 """
72 self._executor = executor
73 self._max_batch_size = max_batch_size
75 @property
76 def max_batch_size(self) -> int:
77 """Get maximum batch size."""
78 return self._max_batch_size
80 async def execute_batch(
81 self,
82 operations: list[GraphQLRequest],
83 context: GraphQLContext,
84 ) -> BatchResult:
85 """Execute operations as a batch.
87 Args:
88 operations: List of GraphQL requests to execute.
89 context: GraphQL context.
91 Returns:
92 BatchResult with all responses.
94 Raises:
95 InputGraphQLError: If batch size exceeds limit.
96 """
97 if len(operations) > self._max_batch_size:
98 raise InputGraphQLError(
99 message=f"Batch size {len(operations)} exceeds maximum of {self._max_batch_size}",
100 )
102 if not operations:
103 return BatchResult()
105 # Create tasks for concurrent execution
106 tasks = []
107 for op in operations:
108 # Create a copy of context for each operation
109 op_context = context.copy() if hasattr(context, "copy") else context
110 task = self._execute_single(op, op_context)
111 tasks.append(task)
113 # Execute all operations concurrently
114 responses = await asyncio.gather(*tasks, return_exceptions=True)
116 # Count successes and errors
117 success_count = sum(1 for r in responses if not isinstance(r, Exception))
118 error_count = len(responses) - success_count
120 return BatchResult(
121 responses=cast("list[GraphQLResponse[Any] | Exception]", responses),
122 total_count=len(operations),
123 success_count=success_count,
124 error_count=error_count,
125 )
127 async def _execute_single(
128 self,
129 operation: GraphQLRequest,
130 context: GraphQLContext,
131 ) -> GraphQLResponse[Any] | Exception:
132 """Execute a single operation.
134 Args:
135 operation: GraphQL request.
136 context: GraphQL context.
138 Returns:
139 GraphQL response or exception.
140 """
141 try:
142 # Create a new execution context for this operation
143 from lexigram.graphql.core.execution import ExecutionContextProtocol
145 exec_context = ExecutionContextProtocol(context=context)
146 return cast(
147 "GraphQLResponse[Any]",
148 await self._executor._execute_internal(exec_context),
149 )
150 except Exception as e: # noqa: BLE001 — batch operations must not raise; exception is returned as the batch result
151 logger.exception("Batch operation failed")
152 return e
155def is_batch_request(body: Any) -> bool:
156 """Detect if a request body is a batch operation.
158 Args:
159 body: The request body (parsed JSON).
161 Returns:
162 True if the body is a list of operations.
163 """
164 return isinstance(body, list)
167def parse_batch_request(body: Any) -> list[GraphQLRequest]:
168 """Parse a batch request body into GraphQLRequests.
170 Args:
171 body: The request body (list of operations).
173 Returns:
174 List of GraphQLRequest objects.
175 """
176 requests = []
177 for item in body:
178 if isinstance(item, dict):
179 requests.append(
180 GraphQLRequest(
181 query=item.get("query"),
182 variables=item.get("variables"),
183 operation_name=item.get("operationName"),
184 )
185 )
186 return requests
189__all__ = [
190 "BatchExecutor",
191 "BatchResult",
192 "is_batch_request",
193 "parse_batch_request",
194]