Skip to content

Commit 1c57676

Browse files
authored
Merge pull request #270 from Azure/dev
Final Release.
2 parents 717f616 + bb17dbc commit 1c57676

File tree

630 files changed

+94654
-72854
lines changed

Some content is hidden

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

630 files changed

+94654
-72854
lines changed

BreakingChanges.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,17 @@
22

33
> See the [Change Log](ChangeLog.md) for a summary of storage library changes.
44
5+
## Version 0.34.0:
6+
7+
### All:
8+
- Several error messages have been clarified or made more specific.
9+
10+
### Blob:
11+
- If-None-Match: * will now fail when reading a blob. Previously this header was ignored for blob reads.
12+
13+
### Queue:
14+
- For put_message a QueueMessage will be returned. This message will have pop receipt, insertion/expiration time, and message ID populated.
15+
516
## Version 0.33.0:
617
- Remove with_filter from service client in favor of the newer callback functions.
718
- Remove max_retries and retry_wait from the blob and file create and get functions in favor of the new client-level retry policies.

ChangeLog.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,33 @@
22

33
> See [BreakingChanges](BreakingChanges.md) for a detailed list of API breaks.
44
5+
## Version 0.34.0:
6+
7+
### All:
8+
- All: Support for 2016-05-31 REST version. Please see our REST API documentation and blogs for information about the related added features. If you are using the Storage Emulator, please update to Emulator version 4.6
9+
- All: Several error messages have been clarified or made more specific.
10+
11+
### Blob:
12+
- Added support for server-side encryption headers.
13+
- Properly return connections to pool when checking for non-existent blobs.
14+
- Fixed a bug with parallel uploads for PageBlobs and BlockBlobs where chunks were being buffered and queued faster than can be processed, potentially causing out-of-memory issues.
15+
- Added large block blob upload support. Blocks can now support sizes up to 100 MB and thus the maximum size of a BlockBlob is now 5,000,000 MB (~4.75 TB).
16+
- Added streaming upload support for the put_block method and a new memory optimized upload algorithm for create_blob_from_stream and create_blob_from_file APIs. (BlockBlobService)
17+
- The new upload strategy will no longer fully buffer seekable streams unless Encryption is enabled. See 'use_byte_buffer' parameter documentation on the 'create_blob_from_stream' method for more details.
18+
- Fixed a deserialization bug with get_block_list() where calling it with anything but the 'all' block_list_type would cause an error.
19+
- Using If-None-Match: * will now fail when reading a blob. Previously this header was ignored for blob reads.
20+
- Populate public access when listing blob containers.
21+
- The public access setting on a blob container is now a container property returned from downloadProperties.
22+
- Populate content MD5 for range gets on blobs.
23+
- Added support for incremental copy on page blobs. The source must be a snapshot of a page blob and include a SAS token.
24+
25+
### File:
26+
- Prefix support for listing files and directories.
27+
- Populate content MD5 for range gets on files.
28+
29+
### Queue:
30+
- put_message now returns a QueueMessage with the PopReceipt, Id, NextVisibleTime, InsertionTime, and ExpirationTime properties populated along with the content.
31+
532
## Version 0.33.0:
633

734
### All:

azure/storage/_common_conversion.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,12 @@
1818
import hmac
1919
import sys
2020
from dateutil.tz import tzutc
21+
from io import (IOBase, SEEK_SET)
2122

23+
from ._error import (
24+
_ERROR_VALUE_SHOULD_BE_BYTES_OR_STREAM,
25+
_ERROR_VALUE_SHOULD_BE_SEEKABLE_STREAM,
26+
)
2227
from .models import (
2328
_unicode_type,
2429
)
@@ -98,8 +103,24 @@ def _sign_string(key, string_to_sign, key_is_base64=True):
98103

99104
def _get_content_md5(data):
100105
md5 = hashlib.md5()
101-
md5.update(data)
102-
return base64.b64encode(md5.digest()).decode('utf-8')
106+
if isinstance(data, bytes):
107+
md5.update(data)
108+
elif hasattr(data, 'read'):
109+
pos = 0
110+
try:
111+
pos = data.tell()
112+
except:
113+
pass
114+
for chunk in iter(lambda: data.read(4096), b""):
115+
md5.update(chunk)
116+
try:
117+
data.seek(pos, SEEK_SET)
118+
except (AttributeError, IOError):
119+
raise ValueError(_ERROR_VALUE_SHOULD_BE_SEEKABLE_STREAM.format('data'))
120+
else:
121+
raise ValueError(_ERROR_VALUE_SHOULD_BE_BYTES_OR_STREAM.format('data'))
122+
123+
return base64.b64encode(md5.digest()).decode('utf-8')
103124

104125
def _lower(text):
105126
return text.lower()

azure/storage/_constants.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@
1515
import platform
1616

1717
__author__ = 'Microsoft Corp. <[email protected]>'
18-
__version__ = '0.33.0'
18+
__version__ = '0.34.0'
1919

2020
# x-ms-version for storage service.
21-
X_MS_VERSION = '2015-07-08'
21+
X_MS_VERSION = '2016-05-31'
2222

2323
# UserAgent string sample: 'Azure-Storage/0.32.0 (Python CPython 3.4.2; Windows 8)'
2424
USER_AGENT_STRING = 'Azure-Storage/{} (Python {} {}; {} {})'.format(__version__, platform.python_implementation(), platform.python_version(), platform.system(), platform.release())

azure/storage/_deserialization.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@
3434
def _int_to_str(value):
3535
return value if value is None else int(value)
3636

37+
def _bool(value):
38+
return value.lower() == 'true'
39+
3740
def _get_download_size(start_range, end_range, resource_size):
3841
if start_range is not None:
3942
end_range = end_range if end_range else (resource_size if resource_size else None)
@@ -53,6 +56,7 @@ def _get_download_size(start_range, end_range, resource_size):
5356
'x-ms-blob-sequence-number': (None, 'page_blob_sequence_number', _int_to_str),
5457
'x-ms-blob-committed-block-count': (None, 'append_blob_committed_block_count', _int_to_str),
5558
'x-ms-share-quota': (None, 'quota', _int_to_str),
59+
'x-ms-server-encrypted': (None, 'server_encrypted', _bool),
5660
'content-type': ('content_settings', 'content_type', _to_str),
5761
'cache-control': ('content_settings', 'cache_control', _to_str),
5862
'content-encoding': ('content_settings', 'content_encoding', _to_str),
@@ -67,6 +71,7 @@ def _get_download_size(start_range, end_range, resource_size):
6771
'x-ms-copy-status': ('copy', 'status', _to_str),
6872
'x-ms-copy-progress': ('copy', 'progress', _to_str),
6973
'x-ms-copy-completion-time': ('copy', 'completion_time', parser.parse),
74+
'x-ms-copy-destination-snapshot': ('copy', 'destination_snapshot_time', _to_str),
7075
'x-ms-copy-status-description': ('copy', 'status_description', _to_str),
7176
}
7277

@@ -324,8 +329,4 @@ def _convert_xml_to_retention_policy(xml, retention_policy):
324329
# Days
325330
days_element = xml.find('Days')
326331
if days_element is not None:
327-
retention_policy.days = int(days_element.text)
328-
329-
330-
def _bool(value):
331-
return value.lower() == 'true'
332+
retention_policy.days = int(days_element.text)

azure/storage/_error.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,20 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414
#--------------------------------------------------------------------------
15-
from ._common_conversion import _to_str
15+
from sys import version_info
16+
from io import IOBase
17+
if version_info < (3,):
18+
def _str(value):
19+
if isinstance(value, unicode):
20+
return value.encode('utf-8')
21+
22+
return str(value)
23+
else:
24+
_str = str
25+
26+
def _to_str(value):
27+
return _str(value) if value is not None else None
28+
1629
from azure.common import (
1730
AzureHttpError,
1831
AzureConflictHttpError,
@@ -34,6 +47,9 @@
3447
'instance'
3548
_ERROR_PARALLEL_NOT_SEEKABLE = 'Parallel operations require a seekable stream.'
3649
_ERROR_VALUE_SHOULD_BE_BYTES = '{0} should be of type bytes.'
50+
_ERROR_VALUE_SHOULD_BE_BYTES_OR_STREAM = '{0} should be of type bytes or a readable file-like/io.IOBase stream object.'
51+
_ERROR_VALUE_SHOULD_BE_SEEKABLE_STREAM = '{0} should be a seekable file-like/io.IOBase type stream object.'
52+
_ERROR_VALUE_SHOULD_BE_STREAM = '{0} should be a file-like/io.IOBase type stream object with a read method.'
3753
_ERROR_VALUE_NONE = '{0} should not be None.'
3854
_ERROR_VALUE_NONE_OR_EMPTY = '{0} should not be None or empty.'
3955
_ERROR_VALUE_NEGATIVE = '{0} should not be negative.'
@@ -102,6 +118,10 @@ def _validate_type_bytes(param_name, param):
102118
if not isinstance(param, bytes):
103119
raise TypeError(_ERROR_VALUE_SHOULD_BE_BYTES.format(param_name))
104120

121+
def _validate_type_bytes_or_stream(param_name, param):
122+
if not (isinstance(param, bytes) or hasattr(param, 'read')):
123+
raise TypeError(_ERROR_VALUE_SHOULD_BE_BYTES_OR_STREAM.format(param_name))
124+
105125
def _validate_not_none(param_name, param):
106126
if param is None:
107127
raise ValueError(_ERROR_VALUE_NONE.format(param_name))

azure/storage/_http/httpclient.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,19 @@
1919
from httplib import (
2020
HTTP_PORT,
2121
HTTPS_PORT,
22-
)
22+
)
2323
from urllib2 import quote as url_quote
2424
else:
2525
from http.client import (
2626
HTTP_PORT,
2727
HTTPS_PORT,
28-
)
28+
)
2929
from urllib.parse import quote as url_quote
3030

3131
from . import HTTPError, HTTPResponse
32+
from .._serialization import _get_data_bytes_or_stream_only
3233

3334
class _HTTPClient(object):
34-
3535
'''
3636
Takes the request and sends it to cloud service and returns the response.
3737
'''
@@ -64,9 +64,9 @@ def set_proxy(self, host, port, user, password):
6464
'''
6565
Sets the proxy server host and port for the HTTP CONNECT Tunnelling.
6666
67-
Note that we set the proxies directly on the request later on rather than
68-
using the session object as requests has a bug where session proxy is ignored
69-
in favor of environment proxy. So, auth will not work unless it is passed
67+
Note that we set the proxies directly on the request later on rather than
68+
using the session object as requests has a bug where session proxy is ignored
69+
in favor of environment proxy. So, auth will not work unless it is passed
7070
directly when making the request as this overrides both.
7171
7272
:param str host:
@@ -95,11 +95,11 @@ def perform_request(self, request):
9595
:param HTTPRequest request:
9696
The request to serialize and send.
9797
:return: An HTTPResponse containing the parsed HTTP response.
98-
:rtype: :class:`~azure.storage._http.HTTPResponse`
98+
:rtype: :class:`~azure.storage._http.HTTPResponse`
9999
'''
100-
# Verify the body is in bytes
100+
# Verify the body is in bytes or either a file-like/stream object
101101
if request.body:
102-
assert isinstance(request.body, bytes)
102+
request.body = _get_data_bytes_or_stream_only('request.body', request.body)
103103

104104
# Construct the URI
105105
uri = self.protocol.lower() + '://' + request.host + request.path
@@ -119,4 +119,7 @@ def perform_request(self, request):
119119
for key, name in response.headers.items():
120120
respheaders[key.lower()] = name
121121

122-
return HTTPResponse(status, response.reason, respheaders, response.content)
122+
wrap = HTTPResponse(status, response.reason, respheaders, response.content)
123+
response.close()
124+
125+
return wrap

azure/storage/_serialization.py

Lines changed: 57 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,13 @@
1818
from dateutil.tz import tzutc
1919
from time import time
2020
from wsgiref.handlers import format_date_time
21+
from os import fstat
22+
from io import (BytesIO, IOBase, SEEK_SET, SEEK_END, UnsupportedOperation)
2123

2224
if sys.version_info >= (3,):
23-
from io import BytesIO
2425
from urllib.parse import quote as url_quote
2526
else:
26-
from cStringIO import StringIO as BytesIO
27-
from urllib2 import quote as url_quote
27+
from urllib2 import quote as url_quote
2828

2929
try:
3030
from xml.etree import cElementTree as ETree
@@ -33,6 +33,8 @@
3333

3434
from ._error import (
3535
_ERROR_VALUE_SHOULD_BE_BYTES,
36+
_ERROR_VALUE_SHOULD_BE_BYTES_OR_STREAM,
37+
_ERROR_VALUE_SHOULD_BE_SEEKABLE_STREAM
3638
)
3739
from ._constants import (
3840
X_MS_VERSION,
@@ -56,11 +58,16 @@ def _to_utc_datetime(value):
5658
def _update_request(request):
5759
# Verify body
5860
if request.body:
59-
assert isinstance(request.body, bytes)
61+
request.body = _get_data_bytes_or_stream_only('request.body', request.body)
62+
length = _len_plus(request.body)
6063

61-
# if it is PUT, POST, MERGE, DELETE, need to add content-length to header.
62-
if request.method in ['PUT', 'POST', 'MERGE', 'DELETE']:
63-
request.headers['Content-Length'] = str(len(request.body))
64+
# only scenario where this case is plausible is if the stream object is not seekable.
65+
if length is None:
66+
raise ValueError(_ERROR_VALUE_SHOULD_BE_SEEKABLE_STREAM)
67+
68+
# if it is PUT, POST, MERGE, DELETE, need to add content-length to header.
69+
if request.method in ['PUT', 'POST', 'MERGE', 'DELETE']:
70+
request.headers['Content-Length'] = str(length)
6471

6572
# append addtional headers based on the service
6673
request.headers['x-ms-version'] = X_MS_VERSION
@@ -99,6 +106,18 @@ def _get_data_bytes_only(param_name, param_value):
99106
raise TypeError(_ERROR_VALUE_SHOULD_BE_BYTES.format(param_name))
100107

101108

109+
def _get_data_bytes_or_stream_only(param_name, param_value):
110+
'''Validates the request body passed in is a stream/file-like or bytes
111+
object.'''
112+
if param_value is None:
113+
return b''
114+
115+
if isinstance(param_value, bytes) or hasattr(param_value, 'read'):
116+
return param_value
117+
118+
raise TypeError(_ERROR_VALUE_SHOULD_BE_BYTES_OR_STREAM.format(param_name))
119+
120+
102121
def _get_request_body(request_body):
103122
'''Converts an object into a request body. If it's None
104123
we'll return an empty string, if it's one of our objects it'll
@@ -107,7 +126,7 @@ def _get_request_body(request_body):
107126
if request_body is None:
108127
return b''
109128

110-
if isinstance(request_body, bytes):
129+
if isinstance(request_body, bytes) or isinstance(request_body, IOBase):
111130
return request_body
112131

113132
if isinstance(request_body, _unicode_type):
@@ -145,7 +164,7 @@ def _convert_signed_identifiers_to_xml(signed_identifiers):
145164
if isinstance(access_policy.expiry, date):
146165
expiry = _to_utc_datetime(expiry)
147166
ETree.SubElement(policy, 'Expiry').text = expiry
148-
167+
149168
if access_policy.permission:
150169
ETree.SubElement(policy, 'Permission').text = _str(access_policy.permission)
151170

@@ -243,7 +262,6 @@ def _convert_service_properties_to_xml(logging, hour_metrics, minute_metrics, co
243262
if target_version:
244263
ETree.SubElement(service_properties_element, 'DefaultServiceVersion').text = target_version
245264

246-
247265
# Add xml declaration and serialize
248266
try:
249267
stream = BytesIO()
@@ -291,3 +309,32 @@ def _convert_retention_policy_to_xml(retention_policy, root):
291309
# Days
292310
if retention_policy.enabled and retention_policy.days:
293311
ETree.SubElement(root, 'Days').text = str(retention_policy.days)
312+
313+
def _len_plus(data):
314+
length = None
315+
# Check if object implements the __len__ method, covers most input cases such as bytearray.
316+
try:
317+
length = len(data)
318+
except:
319+
pass
320+
321+
if not length:
322+
# Check if the stream is a file-like stream object.
323+
# If so, calculate the size using the file descriptor.
324+
try:
325+
fileno = data.fileno()
326+
except (AttributeError, UnsupportedOperation):
327+
pass
328+
else:
329+
return fstat(fileno).st_size
330+
331+
# If the stream is seekable and tell() is implemented, calculate the stream size.
332+
try:
333+
currentPosition = data.tell()
334+
data.seek(0, SEEK_END)
335+
length = data.tell() - currentPosition
336+
data.seek(currentPosition, SEEK_SET)
337+
except (AttributeError, UnsupportedOperation):
338+
pass
339+
340+
return length

0 commit comments

Comments
 (0)