Skip to content

Commit 32691fc

Browse files
author
weidong huang
committed
This contains all the bug fixes after first official publish and also added some tests.
1 parent 7174fc2 commit 32691fc

29 files changed

+2873
-785
lines changed

README.md

Lines changed: 42 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -45,20 +45,21 @@ the local Storage Emulator (with the exception of Service Bus features).
4545
# Usage
4646
## Table Storage
4747

48-
To ensure a table exists, call **create_table**:
48+
To ensure a table exists, call **create\_table**:
4949

5050
```Python
5151
from azure.storage import TableService
5252
ts = TableService(account_name, account_key)
53-
table = ts.create_table('tasktable')
53+
ts.create_table('tasktable')
5454
```
5555

56-
A new entity can be added by calling **insert_entity**:
56+
A new entity can be added by calling **insert\_entity**:
5757

5858
```Python
59+
from datetime import datetime
5960
ts = TableService(account_name, account_key)
60-
table = ts.create_table('tasktable')
61-
table.insert_entity(
61+
ts.create_table('tasktable')
62+
ts.insert_entity(
6263
'tasktable',
6364
{
6465
'PartitionKey' : 'tasksSeattle',
@@ -69,7 +70,7 @@ table.insert_entity(
6970
)
7071
```
7172

72-
The method **get_entity** can then be used to fetch the entity that was just inserted:
73+
The method **get\_entity** can then be used to fetch the entity that was just inserted:
7374

7475
```Python
7576
ts = TableService(account_name, account_key)
@@ -78,27 +79,25 @@ entity = ts.get_entity('tasktable', 'tasksSeattle', '1')
7879

7980
## Blob Storage
8081

81-
The **create_container** method can be used to create a
82+
The **create\_container** method can be used to create a
8283
container in which to store a blob:
8384

8485
```Python
8586
from azure.storage import BlobService
86-
blob_service = BlobService()
87-
container = blob_service.create_container('taskcontainer')
87+
blob_service = BlobService(account_name, account_key)
88+
blob_service.create_container('taskcontainer')
8889
```
8990

90-
To upload a file (assuming it is called task1-upload.txt, it contains the exact text "hello world" (no quotation marks), and it is placed in the same folder as the script below), the method **put_blob** can be used:
91+
To upload a file (assuming it is called task1-upload.txt, it contains the exact text "hello world" (no quotation marks), and it is placed in the same folder as the script below), the method **put\_blob** can be used:
9192

9293
```Python
9394
from azure.storage import BlobService
9495
blob_service = BlobService(account_name, account_key)
95-
blob_service.put_blob('taskcontainer', 'task1',
96-
blobService = azure.createBlobService()
97-
blobService.put_blob('taskcontainer', 'task1', file('task1-upload.txt').read())
96+
blob_service.put_blob('taskcontainer', 'task1', file('task1-upload.txt').read(), 'BlockBlob')
9897

9998
```
10099

101-
To download the blob and write it to the file system, the **get_blob** method can be used:
100+
To download the blob and write it to the file system, the **get\_blob** method can be used:
102101

103102
```Python
104103
from azure.storage import BlobService
@@ -108,85 +107,89 @@ blob = blob_service.get_blob('taskcontainer', 'task1')
108107

109108
## Storage Queues
110109

111-
The **create_queue** method can be used to ensure a queue exists:
110+
The **create\_queue** method can be used to ensure a queue exists:
112111

113112
```Python
114113
from azure.storage import QueueService
115114
queue_service = QueueService(account_name, account_key)
116-
queue = queue_service.create_queue('taskqueue')
115+
queue_service.create_queue('taskqueue')
117116
```
118117

119-
The **put_message** method can then be called to insert the message into the queue:
118+
The **put\_message** method can then be called to insert the message into the queue:
120119

121120
```Python
122121
from azure.storage import QueueService
123122
queue_service = QueueService(account_name, account_key)
124123
queue_service.put_message('taskqueue', 'Hello world!')
125124
```
126125

127-
It is then possible to call the **get___messages** method, process the message and then call **delete_message** on the messages ID. This two-step process ensures messages don't get lost when they are removed from the queue.
126+
It is then possible to call the **get\_messages** method, process the message and then call **delete\_message** with the message id and receipt. This two-step process ensures messages don't get lost when they are removed from the queue.
128127

129128
```Python
130129
from azure.storage import QueueService
131130
queue_service = QueueService(account_name, account_key)
132131
messages = queue_service.get_messages('taskqueue')
133-
queue_service.delete_message('taskqueue', messages[0].message_id)
132+
queue_service.delete_message('taskqueue', messages[0].message_id, messages[0].pop_receipt)
134133
```
135134

136135
## ServiceBus Queues
137136

138137
ServiceBus Queues are an alternative to Storage Queues that might be useful in scenarios where more advanced messaging features are needed (larger message sizes, message ordering, single-operaiton destructive reads, scheduled delivery) using push-style delivery (using long polling).
139138

140-
The **create_queue** method can be used to ensure a queue exists:
139+
The **create\_queue** method can be used to ensure a queue exists:
141140

142141
```Python
143142
from azure.servicebus import ServiceBusService
144-
sbs = ServiceBusService(service_namespace, account_key)
145-
queue = sbs.create_queue('taskqueue');
143+
sbs = ServiceBusService(service_namespace, account_key, 'owner')
144+
sbs.create_queue('taskqueue')
146145
```
147146

148-
The **send__queue__message** method can then be called to insert the message into the queue:
147+
The **send\_queue\_message** method can then be called to insert the message into the queue:
149148

150149
```Python
151-
from azure.servicebus import ServiceBusService
152-
sbs = ServiceBusService(service_namespace, account_key)
153-
sbs.send_queue_message('taskqueue', 'Hello World!')
150+
from azure.servicebus import ServiceBusService, Message
151+
sbs = ServiceBusService(service_namespace, account_key, 'owner')
152+
msg = Message('Hello World!')
153+
sbs.send_queue_message('taskqueue', msg)
154154
```
155155

156-
It is then possible to call the **read__delete___queue__message** method to dequeue the message.
156+
It is then possible to call the **receive\_queue\_message** method to dequeue the message.
157157

158158
```Python
159159
from azure.servicebus import ServiceBusService
160-
sbs = ServiceBusService(service_namespace, account_key)
161-
msg = sbs.read_delete_queue_message('taskqueue')
160+
sbs = ServiceBusService(service_namespace, account_key, 'owner')
161+
msg = sbs.receive_queue_message('taskqueue')
162162
```
163163

164164
## ServiceBus Topics
165165

166166
ServiceBus topics are an abstraction on top of ServiceBus Queues that make pub/sub scenarios easy to implement.
167167

168-
The **create_topic** method can be used to create a server-side topic:
168+
The **create\_topic** method can be used to create a server-side topic:
169169

170170
```Python
171171
from azure.servicebus import ServiceBusService
172-
sbs = ServiceBusService(service_namespace, account_key)
173-
topic = sbs.create_topic('taskdiscussion')
172+
sbs = ServiceBusService(service_namespace, account_key, 'owner')
173+
sbs.create_topic('taskdiscussion')
174174
```
175175

176-
The **send__topic__message** method can be used to send a message to a topic:
176+
The **send\_topic\_message** method can be used to send a message to a topic:
177177

178178
```Python
179-
from azure.servicebus import ServiceBusService
180-
sbs = ServiceBusService(service_namespace, account_key)
181-
sbs.send_topic_message('taskdiscussion', 'Hello world!')
179+
from azure.servicebus import ServiceBusService, Message
180+
sbs = ServiceBusService(service_namespace, account_key, 'owner')
181+
msg = Message('Hello World!')
182+
sbs.send_topic_message('taskdiscussion', msg)
182183
```
183184

184-
A client can then create a subscription and start consuming messages by calling the **create__subscription** method followed by the **receive__subscription__message** method. Please note that any messages sent before the subscription is created will not be received.
185+
A client can then create a subscription and start consuming messages by calling the **create\_subscription** method followed by the **receive\_subscription\_message** method. Please note that any messages sent before the subscription is created will not be received.
185186

186187
```Python
187-
from azure.servicebus import ServiceBusService
188-
sbs = ServiceBusService(service_namespace, account_key)
188+
from azure.servicebus import ServiceBusService, Message
189+
sbs = ServiceBusService(service_namespace, account_key, 'owner')
189190
sbs.create_subscription('taskdiscussion', 'client1')
191+
msg = Message('Hello World!')
192+
sbs.send_topic_message('taskdiscussion', msg)
190193
msg = sbs.receive_subscription_message('taskdiscussion', 'client1')
191194
```
192195

src/azure/__init__.py

Lines changed: 74 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,12 @@
4343
_ERROR_CANNOT_FIND_ROW_KEY = 'Cannot find row key in request.'
4444
_ERROR_INCORRECT_TABLE_IN_BATCH = 'Table should be the same in a batch operations'
4545
_ERROR_INCORRECT_PARTITION_KEY_IN_BATCH = 'Partition Key should be the same in a batch operations'
46-
_ERROR_DUPLICATE_ROW_KEY_IN_BATCH = 'Partition Key should be the same in a batch operations'
46+
_ERROR_DUPLICATE_ROW_KEY_IN_BATCH = 'Row Keys should not be the same in a batch operations'
4747
_ERROR_BATCH_COMMIT_FAIL = 'Batch Commit Fail'
4848
_ERROR_MESSAGE_NOT_PEEK_LOCKED_ON_DELETE = 'Message is not peek locked and cannot be deleted.'
4949
_ERROR_MESSAGE_NOT_PEEK_LOCKED_ON_UNLOCK = 'Message is not peek locked and cannot be unlocked.'
50-
_ERROR_QUEUE_NOT_FOUND = 'Queue is not Found'
51-
_ERROR_TOPIC_NOT_FOUND = 'Topic is not Found'
50+
_ERROR_QUEUE_NOT_FOUND = 'Queue was not found'
51+
_ERROR_TOPIC_NOT_FOUND = 'Topic was not found'
5252
_ERROR_CONFLICT = 'Conflict'
5353
_ERROR_NOT_FOUND = 'Not found'
5454
_ERROR_UNKNOWN = 'Unknown error (%s)'
@@ -58,6 +58,8 @@
5858
_ERROR_VALUE_SHOULD_NOT_BE_NULL = '%s should not be None.'
5959
_ERROR_CANNOT_SERIALIZE_VALUE_TO_ENTITY = 'Cannot serialize the specified value (%s) to an entity. Please use an EntityProperty (which can specify custom types), int, str, bool, or datetime'
6060

61+
METADATA_NS = 'http://schemas.microsoft.com/ado/2007/08/dataservices/metadata'
62+
6163
class WindowsAzureData(object):
6264
''' This is the base of data class. It is only used to check whether it is instance or not. '''
6365
pass
@@ -80,8 +82,11 @@ def __init__(self, message):
8082
self.message = message
8183

8284
class Feed:
83-
def __init__(self, type):
84-
self.type = type
85+
pass
86+
87+
class HeaderDict(dict):
88+
def __getitem__(self, index):
89+
return super(HeaderDict, self).__getitem__(index.lower())
8590

8691
def _get_readable_id(id_name):
8792
"""simplified an id to be more friendly for us people"""
@@ -97,6 +102,9 @@ def _get_entry_properties(xmlstr, include_id):
97102
properties = {}
98103

99104
for entry in _get_child_nodes(xmldoc, 'entry'):
105+
etag = entry.getAttributeNS(METADATA_NS, 'etag')
106+
if etag:
107+
properties['etag'] = etag
100108
for updated in _get_child_nodes(entry, 'updated'):
101109
properties['updated'] = updated.firstChild.nodeValue
102110
for name in _get_children_from_path(entry, 'author', 'name'):
@@ -109,6 +117,14 @@ def _get_entry_properties(xmlstr, include_id):
109117

110118
return properties
111119

120+
def _get_first_child_node_value(parent_node, node_name):
121+
xml_attrs = _get_child_nodes(parent_node, node_name)
122+
if xml_attrs:
123+
xml_attr = xml_attrs[0]
124+
if xml_attr.firstChild:
125+
value = xml_attr.firstChild.nodeValue
126+
return value
127+
112128
def _get_child_nodes(node, tagName):
113129
return [childNode for childNode in node.getElementsByTagName(tagName)
114130
if childNode.parentNode == node]
@@ -142,7 +158,7 @@ def _create_entry(entry_body):
142158
updated_str += '+00:00'
143159

144160
entry_start = '''<?xml version="1.0" encoding="utf-8" standalone="yes"?>
145-
<entry xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices" xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata" xmlns="http://www.w3.org/2005/Atom">
161+
<entry xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices" xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata" xmlns="http://www.w3.org/2005/Atom" >
146162
<title /><updated>{updated}</updated><author><name /></author><id />
147163
<content type="application/xml">
148164
{body}</content></entry>'''
@@ -242,9 +258,23 @@ def _clone_node_with_namespaces(node_to_clone, original_doc):
242258
return clone
243259

244260
def _convert_response_to_feeds(response, convert_func):
245-
feeds = []
261+
if response is None:
262+
return None
263+
264+
feeds = _list_of(Feed)
265+
266+
x_ms_continuation = HeaderDict()
267+
for name, value in response.headers:
268+
if 'x-ms-continuation' in name:
269+
x_ms_continuation[name[len('x-ms-continuation')+1:]] = value
270+
if x_ms_continuation:
271+
setattr(feeds, 'x_ms_continuation', x_ms_continuation)
272+
246273
xmldoc = minidom.parseString(response.body)
247-
for xml_entry in _get_children_from_path(xmldoc, 'feed', 'entry'):
274+
xml_entries = _get_children_from_path(xmldoc, 'feed', 'entry')
275+
if not xml_entries:
276+
xml_entries = _get_children_from_path(xmldoc, 'entry') #in some cases, response contains only entry but no feed
277+
for xml_entry in xml_entries:
248278
new_node = _clone_node_with_namespaces(xml_entry, xmldoc)
249279
feeds.append(convert_func(new_node.toxml()))
250280

@@ -254,16 +284,19 @@ def _validate_not_none(param_name, param):
254284
if param is None:
255285
raise TypeError(_ERROR_VALUE_SHOULD_NOT_BE_NULL % (param_name))
256286

257-
def _html_encode(html):
258-
ch_map = (('&', '&amp;'), ('<', '&lt;'), ('>', '&gt;'), ('"', '&quot'), ('\'', '&apos'))
259-
for name, value in ch_map:
260-
html = html.replace(name, value)
261-
return html
262-
263287
def _fill_list_of(xmldoc, element_type):
264288
xmlelements = _get_child_nodes(xmldoc, element_type.__name__)
265289
return [_parse_response_body(xmlelement.toxml(), element_type) for xmlelement in xmlelements]
266290

291+
def _fill_dict(xmldoc, element_name):
292+
xmlelements = _get_child_nodes(xmldoc, element_name)
293+
if xmlelements:
294+
return_obj = {}
295+
for child in xmlelements[0].childNodes:
296+
if child.firstChild:
297+
return_obj[child.nodeName] = child.firstChild.nodeValue
298+
return return_obj
299+
267300
def _fill_instance_child(xmldoc, element_name, return_type):
268301
'''Converts a child of the current dom element to the specified type. The child name
269302
'''
@@ -272,7 +305,10 @@ def _fill_instance_child(xmldoc, element_name, return_type):
272305
if not xmlelements:
273306
return None
274307

275-
return _fill_instance_element(xmlelements[0], return_type)
308+
return_obj = return_type()
309+
_fill_data_to_return_object(xmlelements[0], return_obj)
310+
311+
return return_obj
276312

277313
def _fill_instance_element(element, return_type):
278314
"""Converts a DOM element into the specified object"""
@@ -367,22 +403,27 @@ def _parse_response(response, return_type):
367403
'''
368404
return _parse_response_body(response.body, return_type)
369405

406+
def _fill_data_to_return_object(node, return_obj):
407+
for name, value in vars(return_obj).iteritems():
408+
if isinstance(value, _list_of):
409+
setattr(return_obj, name, _fill_list_of(node, value.list_type))
410+
elif isinstance(value, WindowsAzureData):
411+
setattr(return_obj, name, _fill_instance_child(node, name, value.__class__))
412+
elif isinstance(value, dict):
413+
setattr(return_obj, name, _fill_dict(node, _get_serialization_name(name)))
414+
else:
415+
value = _fill_data_minidom(node, name, value)
416+
if value is not None:
417+
setattr(return_obj, name, value)
418+
370419
def _parse_response_body(respbody, return_type):
371420
'''
372421
parse the xml and fill all the data into a class of return_type
373422
'''
374423
doc = minidom.parseString(respbody)
375424
return_obj = return_type()
376425
for node in _get_child_nodes(doc, return_type.__name__):
377-
for name, value in vars(return_obj).iteritems():
378-
if isinstance(value, _list_of):
379-
setattr(return_obj, name, _fill_list_of(node, value.list_type))
380-
elif isinstance(value, WindowsAzureData):
381-
setattr(return_obj, name, _fill_instance_child(node, name, value.__class__))
382-
else:
383-
value = _fill_data_minidom(node, name, value)
384-
if value is not None:
385-
setattr(return_obj, name, value)
426+
_fill_data_to_return_object(node, return_obj)
386427

387428
return return_obj
388429

@@ -446,11 +487,12 @@ def _dont_fail_not_exist(error):
446487

447488
def _parse_response_for_dict(response):
448489
''' Extracts name-values from response header. Filter out the standard http headers.'''
449-
490+
491+
if response is None:
492+
return None
450493
http_headers = ['server', 'date', 'location', 'host',
451-
'via', 'proxy-connection', 'x-ms-version', 'connection',
452-
'content-length']
453-
return_dict = {}
494+
'via', 'proxy-connection', 'connection']
495+
return_dict = HeaderDict()
454496
if response.headers:
455497
for name, value in response.headers:
456498
if not name.lower() in http_headers:
@@ -461,6 +503,8 @@ def _parse_response_for_dict(response):
461503
def _parse_response_for_dict_prefix(response, prefix):
462504
''' Extracts name-values for names starting with prefix from response header. Filter out the standard http headers.'''
463505

506+
if response is None:
507+
return None
464508
return_dict = {}
465509
orig_dict = _parse_response_for_dict(response)
466510
if orig_dict:
@@ -475,6 +519,8 @@ def _parse_response_for_dict_prefix(response, prefix):
475519

476520
def _parse_response_for_dict_filter(response, filter):
477521
''' Extracts name-values for names in filter from response header. Filter out the standard http headers.'''
522+
if response is None:
523+
return None
478524
return_dict = {}
479525
orig_dict = _parse_response_for_dict(response)
480526
if orig_dict:

0 commit comments

Comments
 (0)