Skip to content

Commit e58b5ab

Browse files
Batch operations (#13713)
* basic starters for batch operations * adding methods to make TableBatchOperations a context manager * more batch changes * adding more steps, stuck on the _base_client _batch_send method around the print statements * removing some print statements * updating a couple things * fridays updates * single insert on a batch returns a 201, but the response is not deserialized * batching works for insert entity * some tests are working, have an issue when there's a single item in the batch and it's not create * first iteration on batching, includes create, update, and delete * linting stuff and excluding batching for versions >3 * added upsert to batching * starting work on async code * batching support for async now * added test for different partition keys * changes to make the request not ask for a response from the service on inserts, no need to return the inserted entity * changed commit_batch to send_batch * Changed naming of PartialBatchErrorException to BatchErrorException * started work on BatchTransactionResponse * aligning more with .NET, working on deserializing the requests to build the entities again * fixed up a test and fixed pylint issues * added all operations into async batch, need to fix up testing, having a preparer error * got async batching working now, need to finish up addressing anna's comments and finish some small implementation details of the batch result object * addresses Annas comments, need to add a bit more to the transaction result and samples * adds a sample for batching, and documentation for the sphinx generation * finished up all comments on the batching for sync/async, will need a final review but is good to go on my end for release * updatest to batching, mostly for documentation * removing an unused import * removed batch.send_batch() method, closed client sessions on async tests * addressing Anna's comments, added another test to verify behavior, added length to the verify transaction result test method * test pipeline failed because cosmos requests were too close to each other, bumping up the delay and re trying Co-authored-by: annatisch <[email protected]>
1 parent 2302d4a commit e58b5ab

File tree

65 files changed

+13859
-496
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

65 files changed

+13859
-496
lines changed

sdk/tables/azure-data-tables/azure/data/tables/__init__.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,13 @@
2323
LocationMode,
2424
ResourceTypes,
2525
AccountSasPermissions,
26+
BatchTransactionResult,
27+
BatchErrorException
2628
)
2729
from ._policies import ExponentialRetry, LinearRetry
2830
from ._version import VERSION
2931
from ._deserialize import TableErrorCode
32+
from ._table_batch import TableBatchOperations
3033

3134
__version__ = VERSION
3235

@@ -53,5 +56,8 @@
5356
'EdmType',
5457
'RetentionPolicy',
5558
'generate_table_sas',
56-
'SASProtocol'
59+
'SASProtocol',
60+
'BatchTransactionResult',
61+
'TableBatchOperations',
62+
'BatchErrorException'
5763
]

sdk/tables/azure-data-tables/azure/data/tables/_base_client.py

+30-24
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
TYPE_CHECKING,
1717
)
1818
import logging
19+
from uuid import uuid4
1920

2021

2122

@@ -29,7 +30,11 @@
2930
from azure.core.configuration import Configuration
3031
from azure.core.exceptions import HttpResponseError
3132
from azure.core.pipeline import Pipeline
32-
from azure.core.pipeline.transport import RequestsTransport, HttpTransport
33+
from azure.core.pipeline.transport import (
34+
RequestsTransport,
35+
HttpTransport,
36+
HttpRequest,
37+
)
3338
from azure.core.pipeline.policies import (
3439
RedirectPolicy,
3540
ContentDecodePolicy,
@@ -42,7 +47,7 @@
4247

4348
from ._shared_access_signature import QueryStringConstants
4449
from ._constants import STORAGE_OAUTH_SCOPE, SERVICE_HOST_BASE, CONNECTION_TIMEOUT, READ_TIMEOUT
45-
from ._models import LocationMode
50+
from ._models import LocationMode, BatchTransactionResult
4651
from ._authentication import SharedKeyCredentialPolicy
4752
from ._policies import (
4853
StorageHeadersPolicy,
@@ -54,7 +59,7 @@
5459
TablesRetryPolicy,
5560
)
5661
from ._error import _process_table_error
57-
from ._models import PartialBatchErrorException
62+
from ._models import BatchErrorException
5863
from ._sdk_moniker import SDK_MONIKER
5964

6065

@@ -251,54 +256,55 @@ def _create_pipeline(self, credential, **kwargs):
251256
return config, Pipeline(config.transport, policies=policies)
252257

253258
def _batch_send(
254-
self, *reqs, # type: HttpRequest
259+
self, entities, # type: List[TableEntity]
260+
*reqs, # type: List[HttpRequest]
255261
**kwargs
256262
):
263+
# (...) -> List[HttpResponse]
257264
"""Given a series of request, do a Storage batch call.
258265
"""
259266
# Pop it here, so requests doesn't feel bad about additional kwarg
260267
raise_on_any_failure = kwargs.pop("raise_on_any_failure", True)
268+
policies = [StorageHeadersPolicy()]
269+
270+
changeset = HttpRequest('POST', None)
271+
changeset.set_multipart_mixed(
272+
*reqs,
273+
policies=policies,
274+
boundary="changeset_{}".format(uuid4())
275+
)
261276
request = self._client._client.post( # pylint: disable=protected-access
262-
url='{}://{}/?comp=batch{}{}'.format(
263-
self.scheme,
264-
self._primary_hostname,
265-
kwargs.pop('sas', None),
266-
kwargs.pop('timeout', None)
267-
),
277+
url='https://{}/$batch'.format(self._primary_hostname),
268278
headers={
269-
'x-ms-version': self.api_version
279+
'x-ms-version': self.api_version,
280+
'DataServiceVersion': '3.0',
281+
'MaxDataServiceVersion': '3.0;NetFx',
270282
}
271283
)
272-
273-
policies = [StorageHeadersPolicy()]
274-
if self._credential_policy:
275-
policies.append(self._credential_policy)
276-
277284
request.set_multipart_mixed(
278-
*reqs,
285+
changeset,
279286
policies=policies,
280-
enforce_https=False
287+
enforce_https=False,
288+
boundary="batch_{}".format(uuid4())
281289
)
282290

283291
pipeline_response = self._pipeline.run(
284292
request, **kwargs
285293
)
286294
response = pipeline_response.http_response
287-
288295
try:
289296
if response.status_code not in [202]:
290297
raise HttpResponseError(response=response)
291298
parts = response.parts()
299+
transaction_result = BatchTransactionResult(reqs, parts, entities)
292300
if raise_on_any_failure:
293-
parts = list(response.parts())
294301
if any(p for p in parts if not 200 <= p.status_code < 300):
295-
error = PartialBatchErrorException(
296-
message="There is a partial failure in the batch operation.",
302+
error = BatchErrorException(
303+
message="There is a failure in the batch operation.",
297304
response=response, parts=parts
298305
)
299306
raise error
300-
return iter(parts)
301-
return parts
307+
return transaction_result
302308
except HttpResponseError as error:
303309
_process_table_error(error)
304310

sdk/tables/azure-data-tables/azure/data/tables/_deserialize.py

-1
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,6 @@ def _convert_to_entity(entry_element):
156156
# Timestamp is a known property
157157
timestamp = properties.pop('Timestamp', None)
158158
if timestamp:
159-
# TODO: verify change here
160159
# entity['Timestamp'] = _from_entity_datetime(timestamp)
161160
entity['Timestamp'] = timestamp
162161

sdk/tables/azure-data-tables/azure/data/tables/_generated/operations/_table_operations.py

+8-8
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ def query(
7070
401: ClientAuthenticationError, 404: ResourceNotFoundError, 409: ResourceExistsError
7171
}
7272
error_map.update(kwargs.pop('error_map', {}))
73-
73+
7474
_format = None
7575
_top = None
7676
_select = None
@@ -165,7 +165,7 @@ def create(
165165
401: ClientAuthenticationError, 404: ResourceNotFoundError, 409: ResourceExistsError
166166
}
167167
error_map.update(kwargs.pop('error_map', {}))
168-
168+
169169
_format = None
170170
if query_options is not None:
171171
_format = query_options.format
@@ -331,7 +331,7 @@ def query_entities(
331331
401: ClientAuthenticationError, 404: ResourceNotFoundError, 409: ResourceExistsError
332332
}
333333
error_map.update(kwargs.pop('error_map', {}))
334-
334+
335335
_format = None
336336
_top = None
337337
_select = None
@@ -437,7 +437,7 @@ def query_entities_with_partition_and_row_key(
437437
401: ClientAuthenticationError, 404: ResourceNotFoundError, 409: ResourceExistsError
438438
}
439439
error_map.update(kwargs.pop('error_map', {}))
440-
440+
441441
_format = None
442442
_select = None
443443
_filter = None
@@ -547,7 +547,7 @@ def update_entity(
547547
401: ClientAuthenticationError, 404: ResourceNotFoundError, 409: ResourceExistsError
548548
}
549549
error_map.update(kwargs.pop('error_map', {}))
550-
550+
551551
_format = None
552552
if query_options is not None:
553553
_format = query_options.format
@@ -655,7 +655,7 @@ def merge_entity(
655655
401: ClientAuthenticationError, 404: ResourceNotFoundError, 409: ResourceExistsError
656656
}
657657
error_map.update(kwargs.pop('error_map', {}))
658-
658+
659659
_format = None
660660
if query_options is not None:
661661
_format = query_options.format
@@ -759,7 +759,7 @@ def delete_entity(
759759
401: ClientAuthenticationError, 404: ResourceNotFoundError, 409: ResourceExistsError
760760
}
761761
error_map.update(kwargs.pop('error_map', {}))
762-
762+
763763
_format = None
764764
if query_options is not None:
765765
_format = query_options.format
@@ -849,7 +849,7 @@ def insert_entity(
849849
401: ClientAuthenticationError, 404: ResourceNotFoundError, 409: ResourceExistsError
850850
}
851851
error_map.update(kwargs.pop('error_map', {}))
852-
852+
853853
_format = None
854854
if query_options is not None:
855855
_format = query_options.format

sdk/tables/azure-data-tables/azure/data/tables/_models.py

+44
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,50 @@ def __init__(self, message, response, parts):
517517
super(PartialBatchErrorException, self).__init__(message=message, response=response)
518518

519519

520+
class BatchErrorException(HttpResponseError):
521+
"""There is a failure in batch operations.
522+
523+
:param str message: The message of the exception.
524+
:param response: Server response to be deserialized.
525+
:param list parts: A list of the parts in multipart response.
526+
"""
527+
528+
def __init__(self, message, response, parts):
529+
self.parts = parts
530+
super(BatchErrorException, self).__init__(message=message, response=response)
531+
532+
533+
class BatchTransactionResult(object):
534+
"""The result of a successful batch operation, can be used by a user to
535+
recreate a request in the case of BatchErrorException
536+
537+
:param List[HttpRequest] requests: The requests of the batch
538+
:param List[HttpResponse] results: The HTTP response of each request
539+
"""
540+
541+
def __init__(self, requests, results, entities):
542+
self.requests = requests
543+
self.results = results
544+
self.entities = entities
545+
546+
def get_entity(self, row_key):
547+
for entity in self.entities:
548+
if entity['RowKey'] == row_key:
549+
return entity
550+
return None
551+
552+
def get_request(self, row_key):
553+
for i, entity in enumerate(self.entities):
554+
if entity['RowKey'] == row_key:
555+
return self.requests[i]
556+
return None
557+
558+
def get_result(self, row_key):
559+
for i, entity in enumerate(self.entities):
560+
if entity['RowKey'] == row_key:
561+
return self.results[i]
562+
return None
563+
520564

521565
class LocationMode(object):
522566
"""

sdk/tables/azure-data-tables/azure/data/tables/_serialize.py

+1-5
Original file line numberDiff line numberDiff line change
@@ -117,11 +117,7 @@ def _to_entity_guid(value):
117117

118118

119119
def _to_entity_int32(value):
120-
# TODO: What the heck? below
121-
if sys.version_info < (3,):
122-
value = int(value)
123-
else:
124-
value = int(value)
120+
value = int(value)
125121
if value >= 2 ** 31 or value < -(2 ** 31):
126122
raise TypeError(_ERROR_VALUE_TOO_LARGE.format(str(value), EdmType.INT32))
127123
return None, value

0 commit comments

Comments
 (0)