Skip to content

Commit bc98539

Browse files
committed
Merge pull request #172 from Azure/dev
Version 0.32.0
2 parents 18ed338 + 7c6a8c0 commit bc98539

File tree

636 files changed

+53130
-174340
lines changed

Some content is hidden

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

636 files changed

+53130
-174340
lines changed

BreakingChanges.md

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

33
> See the [Change Log](ChangeLog.md) for a summary of storage library changes.
44
5+
## Version 0.32.0:
6+
7+
### Blob:
8+
- get_blob_to_* will do an initial get request of size 32 MB. If it then finds the blob is larger than this size, it will parallelize by default.
9+
- Block blob and page blob create_blob_from_* methods will parallelize by default.
10+
11+
### File:
12+
- get_file_to_* will do an initial get request of size 32 MB. If it then finds the file is larger than this size, it will parallelize by default.
13+
- create_file_from_* methods will parallelize by default.
14+
515
## Version 0.30.0
616

717
### All:

ChangeLog.md

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

33
> See [BreakingChanges](BreakingChanges.md) for a detailed list of API breaks.
44
5+
## Version 0.32.0:
6+
7+
### All:
8+
- request_callback and response_callback functions may be set on the service clients. These callbacks will be run before the request is executed and after the response is received, respectively. They maybe used to add custom headers to the request and for logging, among other purposes.
9+
- A client request id is added to requests by default.
10+
11+
### Blob:
12+
- Get requests taking the start_range parameter incorrectly sent an x-ms-range header when start_range was not specified.
13+
- get_blob_to_* will do an initial get request of size 32 MB. If it then finds the blob is larger than this size, it will parallelize by default.
14+
- Block blob and page blob create_blob_from_* methods will parallelize by default.
15+
- The validate_content option on get_blob_to_* and on methods which put blob data will compute and validate an md5 hash of the content if set to True. This is primarily valuable for detecting bitflips on the wire if using http instead of https as https (the default) will already validate.
16+
- Fixed a bug where lease_id was not specified if given by the user for each chunk on parallel get requests.
17+
18+
### File:
19+
- Get requests taking the start_range parameter incorrectly sent an x-ms-range header when start_range was not specified.
20+
- get_file_to_* will do an initial get request of size 32 MB. If it then finds the file is larger than this size, it will parallelize by default.
21+
- create_file_from_* methods will parallelize by default.
22+
- The validate_content option on get_file_to_* and create_file_from_* will compute and validate an md5 hash of the content if set to True. This is primarily valuable for detecting bitflips on the wire if using http instead of https as https (the default) will already validate.
23+
524
## Version 0.31.0:
625

726
### All:

azure-storage-python.pyproj

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,15 @@
2121
<PtvsTargetsFile>$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\Python Tools\Microsoft.PythonTools.targets</PtvsTargetsFile>
2222
</PropertyGroup>
2323
<ItemGroup>
24+
<Compile Include="azure\storage\blob\_upload_chunking.py" />
25+
<Compile Include="azure\storage\file\_upload_chunking.py" />
2426
<Compile Include="azure\storage\_auth.py" />
2527
<Compile Include="azure\storage\blob\appendblobservice.py" />
2628
<Compile Include="azure\storage\blob\blockblobservice.py" />
2729
<Compile Include="azure\storage\blob\models.py" />
2830
<Compile Include="azure\storage\blob\pageblobservice.py" />
2931
<Compile Include="azure\storage\blob\baseblobservice.py" />
30-
<Compile Include="azure\storage\blob\_chunking.py" />
32+
<Compile Include="azure\storage\blob\_download_chunking.py" />
3133
<Compile Include="azure\storage\blob\_deserialization.py" />
3234
<Compile Include="azure\storage\blob\_error.py" />
3335
<Compile Include="azure\storage\blob\_serialization.py" />
@@ -37,7 +39,7 @@
3739
<Compile Include="azure\storage\_constants.py" />
3840
<Compile Include="azure\storage\file\fileservice.py" />
3941
<Compile Include="azure\storage\file\models.py" />
40-
<Compile Include="azure\storage\file\_chunking.py" />
42+
<Compile Include="azure\storage\file\_download_chunking.py" />
4143
<Compile Include="azure\storage\file\_deserialization.py" />
4244
<Compile Include="azure\storage\file\_serialization.py" />
4345
<Compile Include="azure\storage\file\__init__.py" />
@@ -62,13 +64,15 @@
6264
<Compile Include="azure\storage\_error.py" />
6365
<Compile Include="azure\storage\_deserialization.py" />
6466
<Compile Include="azure\storage\_http\httpclient.py" />
65-
<Compile Include="azure\storage\_http\requestsclient.py" />
6667
<Compile Include="azure\storage\_http\__init__.py" />
6768
<Compile Include="azure\storage\_serialization.py" />
6869
<Compile Include="azure\storage\__init__.py" />
6970
<Compile Include="azure\__init__.py" />
7071
<Compile Include="doc\conf.py" />
7172
<Compile Include="doc\__init__.py" />
73+
<Compile Include="samples\advanced\client.py" />
74+
<Compile Include="samples\advanced\authentication.py" />
75+
<Compile Include="samples\advanced\__init__.py" />
7276
<Compile Include="samples\blob\append_blob_usage.py" />
7377
<Compile Include="samples\blob\block_blob_usage.py" />
7478
<Compile Include="samples\blob\page_blob_usage.py" />
@@ -97,6 +101,7 @@
97101
<Compile Include="tests\test_client.py" />
98102
<Compile Include="tests\test_account.py" />
99103
<Compile Include="tests\test_directory.py" />
104+
<Compile Include="tests\test_get_file.py" />
100105
<Compile Include="tests\test_service_stats.py" />
101106
<Compile Include="tests\test_share.py" />
102107
<Compile Include="tests\test_get_blob.py" />
@@ -128,6 +133,7 @@
128133
<Folder Include="samples\" />
129134
<Folder Include="samples\file\" />
130135
<Folder Include="samples\blob\" />
136+
<Folder Include="samples\advanced\" />
131137
<Folder Include="samples\table\" />
132138
<Folder Include="samples\queue\" />
133139
<Folder Include="tests" />

azure/storage/_auth.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ def __init__(self, account_name, account_key):
2323
self.account_key = account_key
2424

2525
def _get_headers(self, request, headers_to_sign):
26-
headers = dict((name.lower(), value) for name, value in request.headers if value)
26+
headers = dict((name.lower(), value) for name, value in request.headers.items() if value)
2727
if 'content-length' in headers and headers['content-length'] == '0':
2828
del headers['content-length']
2929
return '\n'.join(headers.get(x, '') for x in headers_to_sign) + '\n'
@@ -38,7 +38,7 @@ def _get_canonicalized_resource(self, request):
3838
def _get_canonicalized_headers(self, request):
3939
string_to_sign = ''
4040
x_ms_headers = []
41-
for name, value in request.headers:
41+
for name, value in request.headers.items():
4242
if name.startswith('x-ms-'):
4343
x_ms_headers.append((name.lower(), value))
4444
x_ms_headers.sort()
@@ -50,7 +50,7 @@ def _get_canonicalized_headers(self, request):
5050
def _add_authorization_header(self, request, string_to_sign):
5151
signature = _sign_string(self.account_key, string_to_sign)
5252
auth_string = 'SharedKey ' + self.account_name + ':' + signature
53-
request.headers.append(('Authorization', auth_string))
53+
request.headers['Authorization'] = auth_string
5454

5555

5656
class _StorageSharedKeyAuthentication(_StorageSharedKeyAuthentication):
@@ -72,10 +72,11 @@ def sign_request(self, request):
7272
self._add_authorization_header(request, string_to_sign)
7373

7474
def _get_canonicalized_resource_query(self, request):
75-
request.query.sort()
75+
sorted_queries = [(name, value) for name, value in request.query.items()]
76+
sorted_queries.sort()
7677

7778
string_to_sign = ''
78-
for name, value in request.query:
79+
for name, value in sorted_queries:
7980
if value:
8081
string_to_sign += '\n' + name.lower() + ':' + value
8182

@@ -96,7 +97,7 @@ def sign_request(self, request):
9697
self._add_authorization_header(request, string_to_sign)
9798

9899
def _get_canonicalized_resource_query(self, request):
99-
for name, value in request.query:
100+
for name, value in request.query.items():
100101
if name == 'comp':
101102
return '?comp=' + value
102103
return ''

azure/storage/_common_conversion.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,10 @@ def _sign_string(key, string_to_sign, key_is_base64=True):
9898
encoded_digest = _encode_base64(digest)
9999
return encoded_digest
100100

101+
def _get_content_md5(data):
102+
md5 = hashlib.md5()
103+
md5.update(data)
104+
return base64.b64encode(md5.digest()).decode('utf-8')
101105

102106
def _lower(text):
103107
return text.lower()

azure/storage/_constants.py

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

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

2020
# x-ms-version for storage service.
2121
X_MS_VERSION = '2015-07-08'
2222

23-
# UserAgent string sample: 'Azure-Storage/0.31.0 (Python CPython 3.4.2; Windows 8)'
23+
# 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())
2525

2626
# Live ServiceClient URLs

azure/storage/_deserialization.py

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ def _get_download_size(start_range, end_range, resource_size):
4949
'etag': (None, 'etag', _to_str),
5050
'x-ms-blob-type': (None, 'blob_type', _to_str),
5151
'content-length': (None, 'content_length', _int_to_str),
52+
'content-range': (None, 'content_range', _to_str),
5253
'x-ms-blob-sequence-number': (None, 'page_blob_sequence_number', _int_to_str),
5354
'x-ms-blob-committed-block-count': (None, 'append_blob_committed_block_count', _int_to_str),
5455
'x-ms-share-quota': (None, 'quota', _int_to_str),
@@ -78,7 +79,7 @@ def _parse_metadata(response):
7879
return None
7980

8081
metadata = _dict()
81-
for key, value in response.headers:
82+
for key, value in response.headers.items():
8283
if key.startswith('x-ms-meta-'):
8384
metadata[key[10:]] = _to_str(value)
8485

@@ -94,7 +95,7 @@ def _parse_properties(response, result_class):
9495
return None
9596

9697
props = result_class()
97-
for key, value in response.headers:
98+
for key, value in response.headers.items():
9899
info = GET_PROPERTIES_ATTRIBUTE_MAP.get(key)
99100
if info:
100101
if info[0] is None:
@@ -105,21 +106,17 @@ def _parse_properties(response, result_class):
105106

106107
return props
107108

108-
def _parse_response_for_dict(response):
109-
''' Extracts name-values from response header. Filter out the standard
110-
http headers.'''
111-
112-
if response is None:
109+
def _parse_length_from_content_range(content_range):
110+
'''
111+
Parses the blob length from the content range header: bytes 1-3/65537
112+
'''
113+
if content_range is None:
113114
return None
114-
http_headers = ['server', 'date', 'location', 'host',
115-
'via', 'proxy-connection', 'connection']
116-
return_dict = _HeaderDict()
117-
if response.headers:
118-
for name, value in response.headers:
119-
if not name.lower() in http_headers:
120-
return_dict[name] = value
121-
122-
return return_dict
115+
116+
# First, split in space and take the second half: '1-3/65537'
117+
# Next, split on slash and take the second half: '65537'
118+
# Finally, convert to an int: 65537
119+
return int(content_range.split(' ', 1)[1].split('/', 1)[1])
123120

124121
def _convert_xml_to_signed_identifiers(xml):
125122
'''

azure/storage/_error.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414
#--------------------------------------------------------------------------
15-
15+
from ._common_conversion import _to_str
1616
from azure.common import (
1717
AzureHttpError,
1818
AzureConflictHttpError,
1919
AzureMissingResourceHttpError,
20+
AzureException,
2021
)
2122

2223
_ERROR_CONFLICT = 'Conflict ({0})'
@@ -44,6 +45,8 @@
4445
_ERROR_RANGE_TOO_LARGE_FOR_MD5 = \
4546
'Getting content MD5 for a range greater than 4MB ' + \
4647
'is not supported.'
48+
_ERROR_MD5_MISMATCH = \
49+
'MD5 mismatch. Expected value is \'{0}\', computed value is \'{1}\'.'
4750

4851
def _dont_fail_on_exist(error):
4952
''' don't throw exception if the resource exists.
@@ -78,4 +81,8 @@ def _validate_type_bytes(param_name, param):
7881

7982
def _validate_not_none(param_name, param):
8083
if param is None:
81-
raise ValueError(_ERROR_VALUE_NONE.format(param_name))
84+
raise ValueError(_ERROR_VALUE_NONE.format(param_name))
85+
86+
def _validate_content_match(server_md5, computed_md5):
87+
if server_md5 != computed_md5:
88+
raise AzureException(_ERROR_MD5_MISMATCH.format(server_md5, computed_md5))

azure/storage/_http/__init__.py

Lines changed: 33 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,20 @@
1515

1616
class HTTPError(Exception):
1717

18-
''' HTTP Exception when response status code >= 300 '''
18+
'''
19+
Represents an HTTP Exception when response status code >= 300.
20+
21+
:ivar int status:
22+
the status code of the response
23+
:ivar str message:
24+
the message
25+
:ivar list headers:
26+
the returned headers, as a list of (name, value) pairs
27+
:ivar bytes body:
28+
the body of the response
29+
'''
1930

2031
def __init__(self, status, message, respheader, respbody):
21-
'''Creates a new HTTPError with the specified status, message,
22-
response headers and body'''
2332
self.status = status
2433
self.respheader = respheader
2534
self.respbody = respbody
@@ -28,18 +37,18 @@ def __init__(self, status, message, respheader, respbody):
2837

2938
class HTTPResponse(object):
3039

31-
"""Represents a response from an HTTP request. An HTTPResponse has the
32-
following attributes:
40+
'''
41+
Represents a response from an HTTP request.
3342
34-
status:
43+
:ivar int status:
3544
the status code of the response
36-
message:
45+
:ivar str message:
3746
the message
38-
headers:
39-
the returned headers, as a list of (name, value) pairs
40-
body:
47+
:ivar dict headers:
48+
the returned headers
49+
:ivar bytes body:
4150
the body of the response
42-
"""
51+
'''
4352

4453
def __init__(self, status, message, headers, body):
4554
self.status = status
@@ -50,30 +59,27 @@ def __init__(self, status, message, headers, body):
5059

5160
class HTTPRequest(object):
5261

53-
'''Represents an HTTP Request. An HTTP Request consists of the following
54-
attributes:
55-
host:
62+
'''
63+
Represents an HTTP Request.
64+
65+
:ivar str host:
5666
the host name to connect to
57-
method:
67+
:ivar str method:
5868
the method to use to connect (string such as GET, POST, PUT, etc.)
59-
path:
69+
:ivar str path:
6070
the uri fragment
61-
query:
62-
query parameters specified as a list of (name, value) pairs
63-
headers:
64-
header values specified as (name, value) pairs
65-
body:
71+
:ivar dict query:
72+
query parameters
73+
:ivar dict headers:
74+
header values
75+
:ivar bytes body:
6676
the body of the request.
67-
protocol_override:
68-
specify to use this protocol instead of the global one stored in
69-
_HTTPClient.
7077
'''
7178

7279
def __init__(self):
7380
self.host = ''
7481
self.method = ''
7582
self.path = ''
76-
self.query = [] # list of (name, value)
77-
self.headers = [] # list of (header name, header value)
83+
self.query = {} # list of (name, value)
84+
self.headers = {} # list of (header name, header value)
7885
self.body = ''
79-
self.protocol_override = None

0 commit comments

Comments
 (0)