Skip to content

Commit 99e88a0

Browse files
authored
Merge pull request #683 from microsoftgraph/shem/batch_requests
Batch Requests
2 parents b917d8c + 378318f commit 99e88a0

13 files changed

+1414
-0
lines changed

src/msgraph_core/requests/__init__.py

Whitespace-only changes.
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
from typing import TypeVar, Type, Dict, Optional, Union
2+
import logging
3+
4+
from kiota_abstractions.request_adapter import RequestAdapter
5+
from kiota_abstractions.request_information import RequestInformation
6+
from kiota_abstractions.method import Method
7+
from kiota_abstractions.serialization import Parsable
8+
from kiota_abstractions.headers_collection import HeadersCollection
9+
from kiota_abstractions.api_error import APIError
10+
11+
from .batch_request_content import BatchRequestContent
12+
from .batch_request_content_collection import BatchRequestContentCollection
13+
from .batch_response_content import BatchResponseContent
14+
from .batch_response_content_collection import BatchResponseContentCollection
15+
16+
T = TypeVar('T', bound='Parsable')
17+
18+
APPLICATION_JSON = "application/json"
19+
20+
21+
class BatchRequestBuilder:
22+
"""
23+
Provides operations to call the batch method.
24+
"""
25+
26+
def __init__(
27+
self,
28+
request_adapter: RequestAdapter,
29+
error_map: Optional[Dict[str, Type[Parsable]]] = None
30+
):
31+
if request_adapter is None:
32+
raise ValueError("request_adapter cannot be Null.")
33+
self._request_adapter = request_adapter
34+
self.url_template = f"{self._request_adapter.base_url}/$batch"
35+
self.error_map = error_map or {}
36+
37+
async def post(
38+
self,
39+
batch_request_content: Union[BatchRequestContent, BatchRequestContentCollection],
40+
error_map: Optional[Dict[str, Type[Parsable]]] = None,
41+
) -> Union[T, BatchResponseContentCollection]:
42+
"""
43+
Sends a batch request and returns the batch response content.
44+
45+
Args:
46+
batch_request_content (Union[BatchRequestContent,
47+
BatchRequestContentCollection]): The batch request content.
48+
response_type: Optional[Type[T]] : The type to deserialize the response into.
49+
Optional[Dict[str, Type[Parsable]]] = None:
50+
Error mappings for response handling.
51+
52+
Returns:
53+
Union[T, BatchResponseContentCollection]: The batch response content
54+
or the specified response type.
55+
56+
"""
57+
if batch_request_content is None:
58+
raise ValueError("batch_request_content cannot be Null.")
59+
response_type = BatchResponseContent
60+
61+
if isinstance(batch_request_content, BatchRequestContent):
62+
request_info = await self.to_post_request_information(batch_request_content)
63+
bytes_content = request_info.content
64+
json_content = bytes_content.decode("utf-8")
65+
updated_str = '{"requests":' + json_content + '}'
66+
updated_bytes = updated_str.encode("utf-8")
67+
request_info.content = updated_bytes
68+
error_map = error_map or self.error_map
69+
response = None
70+
try:
71+
response = await self._request_adapter.send_async(
72+
request_info, response_type, error_map
73+
)
74+
75+
except APIError as e:
76+
logging.error("API Error: %s", e)
77+
raise e
78+
if response is None:
79+
raise ValueError("Failed to get a valid response from the API.")
80+
return response
81+
if isinstance(batch_request_content, BatchRequestContentCollection):
82+
batch_responses = await self._post_batch_collection(batch_request_content, error_map)
83+
return batch_responses
84+
85+
raise ValueError("Invalid type for batch_request_content.")
86+
87+
async def _post_batch_collection(
88+
self,
89+
batch_request_content_collection: BatchRequestContentCollection,
90+
error_map: Optional[Dict[str, Type[Parsable]]] = None,
91+
) -> BatchResponseContentCollection:
92+
"""
93+
Sends a collection of batch requests and returns a collection of batch response contents.
94+
95+
Args:
96+
batch_request_content_collection (BatchRequestContentCollection): The
97+
collection of batch request contents.
98+
Optional[Dict[str, Type[Parsable]]] = None:
99+
Error mappings for response handling.
100+
101+
Returns:
102+
BatchResponseContentCollection: The collection of batch response contents.
103+
"""
104+
if batch_request_content_collection is None:
105+
raise ValueError("batch_request_content_collection cannot be Null.")
106+
107+
batch_responses = BatchResponseContentCollection()
108+
109+
for batch_request_content in batch_request_content_collection.batches:
110+
request_info = await self.to_post_request_information(batch_request_content)
111+
response = await self._request_adapter.send_async(
112+
request_info, BatchResponseContent, error_map or self.error_map
113+
)
114+
batch_responses.add_response(response)
115+
116+
return batch_responses
117+
118+
async def to_post_request_information(
119+
self, batch_request_content: BatchRequestContent
120+
) -> RequestInformation:
121+
"""
122+
Creates request information for a batch POST request.
123+
124+
Args:
125+
batch_request_content (BatchRequestContent): The batch request content.
126+
127+
Returns:
128+
RequestInformation: The request information.
129+
"""
130+
131+
if batch_request_content is None:
132+
raise ValueError("batch_request_content cannot be Null.")
133+
batch_request_items = list(batch_request_content.requests.values())
134+
135+
request_info = RequestInformation()
136+
request_info.http_method = Method.POST
137+
request_info.url_template = self.url_template
138+
request_info.headers = HeadersCollection()
139+
request_info.headers.try_add("Content-Type", APPLICATION_JSON)
140+
request_info.set_content_from_parsable(
141+
self._request_adapter, APPLICATION_JSON, batch_request_items
142+
)
143+
144+
return request_info
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import uuid
2+
from typing import List, Dict, Union, Optional
3+
4+
from kiota_abstractions.request_information import RequestInformation
5+
from kiota_abstractions.serialization import Parsable, ParseNode
6+
from kiota_abstractions.serialization import SerializationWriter
7+
8+
from .batch_request_item import BatchRequestItem
9+
10+
11+
class BatchRequestContent(Parsable):
12+
"""
13+
Provides operations to call the batch method.
14+
"""
15+
16+
MAX_REQUESTS = 20
17+
18+
def __init__(self, requests: Dict[str, Union['BatchRequestItem', 'RequestInformation']] = {}):
19+
"""
20+
Initializes a new instance of the BatchRequestContent class.
21+
"""
22+
self._requests: Dict[str, Union[BatchRequestItem, 'RequestInformation']] = requests or {}
23+
24+
self.is_finalized = False
25+
for request_id, request in requests.items():
26+
self.add_request(request_id, request)
27+
28+
@property
29+
def requests(self) -> Dict:
30+
"""
31+
Gets the requests.
32+
"""
33+
return self._requests
34+
35+
@requests.setter
36+
def requests(self, requests: List[BatchRequestItem]) -> None:
37+
"""
38+
Sets the requests.
39+
"""
40+
if len(requests) >= BatchRequestContent.MAX_REQUESTS:
41+
raise ValueError(f"Maximum number of requests is {BatchRequestContent.MAX_REQUESTS}")
42+
for request in requests:
43+
self.add_request(request.id, request)
44+
45+
def add_request(self, request_id: Optional[str], request: BatchRequestItem) -> None:
46+
"""
47+
Adds a request to the batch request content.
48+
"""
49+
if len(self.requests) >= BatchRequestContent.MAX_REQUESTS:
50+
raise RuntimeError(f"Maximum number of requests is {BatchRequestContent.MAX_REQUESTS}")
51+
if not request.id:
52+
request.id = str(uuid.uuid4())
53+
if hasattr(request, 'depends_on') and request.depends_on:
54+
for dependent_id in request.depends_on:
55+
if dependent_id not in [req.id for req in self.requests]:
56+
dependent_request = self._request_by_id(dependent_id)
57+
if dependent_request:
58+
self._requests[dependent_id] = dependent_request
59+
self._requests[request.id] = request
60+
61+
def add_request_information(self, request_information: RequestInformation) -> None:
62+
"""
63+
Adds a request to the batch request content.
64+
Args:
65+
request_information (RequestInformation): The request information to add.
66+
"""
67+
request_id = str(uuid.uuid4())
68+
self.add_request(request_id, BatchRequestItem(request_information))
69+
70+
def add_urllib_request(self, request) -> None:
71+
"""
72+
Adds a request to the batch request content.
73+
"""
74+
request_id = str(uuid.uuid4())
75+
self.add_request(request_id, BatchRequestItem.create_with_urllib_request(request))
76+
77+
def remove(self, request_id: str) -> None:
78+
"""
79+
Removes a request from the batch request content.
80+
Also removes the request from the depends_on list of
81+
other requests.
82+
"""
83+
request_to_remove = None
84+
for request in self.requests:
85+
if request.id == request_id:
86+
request_to_remove = request
87+
if hasattr(request, 'depends_on') and request.depends_on:
88+
if request_id in request.depends_on:
89+
request.depends_on.remove(request_id)
90+
if request_to_remove:
91+
del self._requests[request_to_remove.id]
92+
else:
93+
raise ValueError(f"Request ID {request_id} not found in requests.")
94+
95+
def remove_batch_request_item(self, item: BatchRequestItem) -> None:
96+
"""
97+
Removes a request from the batch request content.
98+
"""
99+
self.remove(item.id)
100+
101+
def finalize(self):
102+
"""
103+
Finalizes the batch request content.
104+
"""
105+
self.is_finalized = True
106+
return self._requests
107+
108+
def _request_by_id(self, request_id: str) -> Optional[BatchRequestItem]:
109+
"""
110+
Finds a request by its ID.
111+
112+
Args:
113+
request_id (str): The ID of the request to find.
114+
115+
Returns:
116+
The request with the given ID, or None if not found.
117+
"""
118+
return self._requests.get(request_id)
119+
120+
@staticmethod
121+
def create_from_discriminator_value(
122+
parse_node: Optional[ParseNode] = None
123+
) -> 'BatchRequestContent':
124+
if parse_node is None:
125+
raise ValueError("parse_node cannot be None")
126+
return BatchRequestContent()
127+
128+
def get_field_deserializers(self, ) -> Dict:
129+
"""
130+
The deserialization information for the current model
131+
"""
132+
return {}
133+
134+
def serialize(self, writer: SerializationWriter) -> None:
135+
"""
136+
Serializes information the current object
137+
Args:
138+
writer: Serialization writer to use to serialize this model
139+
"""
140+
writer.write_collection_of_object_values("requests", self.requests)
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
from typing import List, Optional
2+
3+
from kiota_abstractions.request_information import RequestInformation
4+
from kiota_abstractions.serialization import SerializationWriter
5+
6+
from .batch_request_content import BatchRequestContent
7+
from .batch_request_item import BatchRequestItem
8+
9+
10+
class BatchRequestContentCollection:
11+
"""A collection of request content objects."""
12+
13+
def __init__(self) -> None:
14+
"""
15+
Initializes a new instance of the BatchRequestContentCollection class.
16+
17+
18+
"""
19+
self.max_requests_per_batch = BatchRequestContent.MAX_REQUESTS
20+
self.batches: List[BatchRequestContent] = []
21+
self.current_batch: BatchRequestContent = BatchRequestContent()
22+
23+
def add_batch_request_item(self, request: BatchRequestItem) -> None:
24+
"""
25+
Adds a request item to the collection.
26+
Args:
27+
request (BatchRequestItem): The request item to add.
28+
"""
29+
if len(self.current_batch.requests) >= self.max_requests_per_batch:
30+
self.batches.append(self.current_batch.finalize())
31+
self.current_batch = BatchRequestContent()
32+
self.current_batch.add_request(request.id, request)
33+
self.batches.append(self.current_batch)
34+
35+
def remove_batch_request_item(self, request_id: str) -> None:
36+
"""
37+
Removes a request item from the collection.
38+
Args:
39+
request_id (str): The ID of the request item to remove.
40+
"""
41+
for batch in self.batches:
42+
if request_id in batch.requests:
43+
del batch.requests[request_id]
44+
return
45+
if request_id in self.current_batch.requests:
46+
del self.current_batch.requests[request_id]
47+
48+
def new_batch_with_failed_requests(self) -> Optional[BatchRequestContent]:
49+
"""
50+
Creates a new batch with failed requests.
51+
Returns:
52+
Optional[BatchRequestContent]: A new batch with failed requests.
53+
"""
54+
# Use IDs to get response status codes, generate new batch with failed requests
55+
batch_with_failed_responses: Optional[BatchRequestContent] = BatchRequestContent()
56+
for batch in self.batches:
57+
for request in batch.requests:
58+
if request.status_code not in [200, 201, 202, 203, 204, 205, 206, 207, 208, 226]:
59+
if batch_with_failed_responses is not None:
60+
batch_with_failed_responses.add_request(request.id, request)
61+
else:
62+
raise ValueError("batch_with_failed_responses is None")
63+
return batch_with_failed_responses
64+
65+
def get_batch_requests_for_execution(self) -> List[BatchRequestContent]:
66+
"""
67+
Gets the batch requests for execution.
68+
Returns:
69+
List[BatchRequestContent]: The batch requests for execution.
70+
"""
71+
# if not self.current_batch.is_finalized:
72+
# self.current_batch.finalize()
73+
# self.batches.append(self.current_batch)
74+
return self.batches
75+
76+
def serialize(self, writer: SerializationWriter) -> None:
77+
"""
78+
Serializes information the current object
79+
Args:
80+
writer: Serialization writer to use to serialize this model
81+
"""
82+
pass
83+
# print(f"serializing {self.batches}")
84+
# writer.write_collection_of_object_values("requests", self.batches)

0 commit comments

Comments
 (0)