Merge lp:~oubiwann/txaws/484858-s3-scripts into lp:txaws

Proposed by Duncan McGreggor
Status: Merged
Merged at revision: not available
Proposed branch: lp:~oubiwann/txaws/484858-s3-scripts
Merge into: lp:txaws
Prerequisite: lp:~oubiwann/txaws/474353-storage-ec2-symmetry
Diff against target: 1415 lines (+834/-304)
22 files modified
LICENSE (+10/-5)
README (+6/-0)
bin/txaws-create-bucket (+42/-0)
bin/txaws-delete-bucket (+42/-0)
bin/txaws-delete-object (+46/-0)
bin/txaws-get-object (+46/-0)
bin/txaws-head-object (+47/-0)
bin/txaws-list-buckets (+43/-0)
bin/txaws-put-object (+56/-0)
txaws/client/base.py (+39/-0)
txaws/ec2/client.py (+3/-36)
txaws/ec2/exception.py (+4/-108)
txaws/ec2/tests/test_exception.py (+2/-129)
txaws/exception.py (+113/-0)
txaws/meta.py (+10/-0)
txaws/s3/client.py (+25/-7)
txaws/s3/exception.py (+21/-0)
txaws/s3/tests/test_exception.py (+62/-0)
txaws/script.py (+42/-0)
txaws/testing/payload.py (+31/-19)
txaws/tests/test_exception.py (+114/-0)
txaws/util.py (+30/-0)
To merge this branch: bzr merge lp:~oubiwann/txaws/484858-s3-scripts
Reviewer Review Type Date Requested Status
Duncan McGreggor Approve
Review via email: mp+15340@code.launchpad.net

This proposal supersedes a proposal from 2009-11-22.

To post a comment you must log in.
Revision history for this message
Duncan McGreggor (oubiwann) wrote : Posted in a previous version of this proposal

This branch depends upon two other branches:
 * 474353-storage-ec2-symmetry
 * 475571-s3-error-wrapper

These scripts serve dual purpose:
 1. convenience for users wanting to perform actions against S3-compliant storage servers, and
 2. examples for how to use the API.

For the latter purpose, code in the scripts has not been abstracted out for maximal reuse. Instead, each script presents a complete picture to an API user on how to use the given method to accomplish a common task. This is done at the cost of code redundancy.

Note that the DELETE operations currently return an exit code of 1 when it should be 0. This will be addressed by bug #486363 (Successful delete operations return a 204 No Content "error").

Revision history for this message
Duncan McGreggor (oubiwann) wrote : Posted in a previous version of this proposal

Update:

bug #486363 (Successful delete operations return a 204 No Content "error") is currently in review.

Revision history for this message
Duncan McGreggor (oubiwann) wrote : Posted in a previous version of this proposal

Landscape trunk has been tested against this branch.

Revision history for this message
Duncan McGreggor (oubiwann) wrote :

Robert made this comment in the merge proposal of a child branch:

> There is a bunch of duplication in the example scripts. I'd like to see that removed. Perhaps:
> - give them a if __name__ == guard
> - move the error/return etc callback support into script.py

There is a bunch of duplication... for now, that is intentional, as it offers an example of API usage: all a potential user/developer has to do is read the bin scripts to see how to use the API.

I did think about moving redundant code into a txaws.script module, but I chose in favor of clarity, sacrificing with redundancy.

If you don't this is a valid concern, I'm willing to be convinced :-)

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'LICENSE'
2--- LICENSE 2008-07-06 22:51:54 +0000
3+++ LICENSE 2009-11-28 01:10:25 +0000
4@@ -1,3 +1,8 @@
5+Copyright (C) 2008 Tristan Seligmann <mithrandi@mithrandi.net>
6+Copyright (C) 2009 Robert Collins <robertc@robertcollins.net>
7+Copyright (C) 2009 Canonical Ltd
8+Copyright (C) 2009 Duncan McGreggor <oubiwann@adytum.us>
9+
10 Permission is hereby granted, free of charge, to any person obtaining
11 a copy of this software and associated documentation files (the
12 "Software"), to deal in the Software without restriction, including
13@@ -11,8 +16,8 @@
14
15 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17-MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18-NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19-LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20-OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21-WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
23+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
24+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
25+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
26+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
27
28=== modified file 'README'
29--- README 2009-08-19 20:55:49 +0000
30+++ README 2009-11-28 01:10:25 +0000
31@@ -14,3 +14,9 @@
32 * The txaws python package. (No installer at the moment)
33
34 * bin/aws-status, a GUI status program for aws resources.
35+
36+License
37+-------
38+
39+txAWS is open source software, MIT License. See the LICENSE file for more
40+details.
41
42=== added file 'bin/txaws-create-bucket'
43--- bin/txaws-create-bucket 1970-01-01 00:00:00 +0000
44+++ bin/txaws-create-bucket 2009-11-28 01:10:25 +0000
45@@ -0,0 +1,42 @@
46+#!/usr/bin/env python
47+"""
48+%prog [options]
49+"""
50+
51+import sys
52+
53+from txaws.credentials import AWSCredentials
54+from txaws.script import parse_options
55+from txaws.service import AWSServiceRegion
56+from txaws.util import reactor
57+
58+
59+def printResults(results):
60+ return 0
61+
62+
63+def printError(error):
64+ print error.value
65+ return 1
66+
67+
68+def finish(return_code):
69+ reactor.stop(exitStatus=return_code)
70+
71+
72+options, args = parse_options(__doc__.strip())
73+if options.bucket is None:
74+ print "Error Message: A bucket name is required."
75+ sys.exit(1)
76+creds = AWSCredentials(options.access_key, options.secret_key)
77+region = AWSServiceRegion(
78+ creds=creds, region=options.region, s3_endpoint=options.url)
79+client = region.get_s3_client()
80+
81+d = client.create_bucket(options.bucket)
82+d.addCallback(printResults)
83+d.addErrback(printError)
84+d.addCallback(finish)
85+# We use a custom reactor so that we can return the exit status from
86+# reactor.run().
87+sys.exit(reactor.run())
88
89=== added file 'bin/txaws-delete-bucket'
90--- bin/txaws-delete-bucket 1970-01-01 00:00:00 +0000
91+++ bin/txaws-delete-bucket 2009-11-28 01:10:25 +0000
92@@ -0,0 +1,42 @@
93+#!/usr/bin/env python
94+"""
95+%prog [options]
96+"""
97+
98+import sys
99+
100+from txaws.credentials import AWSCredentials
101+from txaws.script import parse_options
102+from txaws.service import AWSServiceRegion
103+from txaws.util import reactor
104+
105+
106+def printResults(results):
107+ return 0
108+
109+
110+def printError(error):
111+ print error.value
112+ return 1
113+
114+
115+def finish(return_code):
116+ reactor.stop(exitStatus=return_code)
117+
118+
119+options, args = parse_options(__doc__.strip())
120+if options.bucket is None:
121+ print "Error Message: A bucket name is required."
122+ sys.exit(1)
123+creds = AWSCredentials(options.access_key, options.secret_key)
124+region = AWSServiceRegion(
125+ creds=creds, region=options.region, s3_endpoint=options.url)
126+client = region.get_s3_client()
127+
128+d = client.delete_bucket(options.bucket)
129+d.addCallback(printResults)
130+d.addErrback(printError)
131+d.addCallback(finish)
132+# We use a custom reactor so that we can return the exit status from
133+# reactor.run().
134+sys.exit(reactor.run())
135
136=== added file 'bin/txaws-delete-object'
137--- bin/txaws-delete-object 1970-01-01 00:00:00 +0000
138+++ bin/txaws-delete-object 2009-11-28 01:10:25 +0000
139@@ -0,0 +1,46 @@
140+#!/usr/bin/env python
141+"""
142+%prog [options]
143+"""
144+
145+import sys
146+
147+from txaws.credentials import AWSCredentials
148+from txaws.script import parse_options
149+from txaws.service import AWSServiceRegion
150+from txaws.util import reactor
151+
152+
153+def printResults(results):
154+ print results
155+ return 0
156+
157+
158+def printError(error):
159+ print error.value
160+ return 1
161+
162+
163+def finish(return_code):
164+ reactor.stop(exitStatus=return_code)
165+
166+
167+options, args = parse_options(__doc__.strip())
168+if options.bucket is None:
169+ print "Error Message: A bucket name is required."
170+ sys.exit(1)
171+if options.object_name is None:
172+ print "Error Message: An object name is required."
173+ sys.exit(1)
174+creds = AWSCredentials(options.access_key, options.secret_key)
175+region = AWSServiceRegion(
176+ creds=creds, region=options.region, s3_endpoint=options.url)
177+client = region.get_s3_client()
178+
179+d = client.delete_object(options.bucket, options.object_name)
180+d.addCallback(printResults)
181+d.addErrback(printError)
182+d.addCallback(finish)
183+# We use a custom reactor so that we can return the exit status from
184+# reactor.run().
185+sys.exit(reactor.run())
186
187=== added file 'bin/txaws-get-object'
188--- bin/txaws-get-object 1970-01-01 00:00:00 +0000
189+++ bin/txaws-get-object 2009-11-28 01:10:25 +0000
190@@ -0,0 +1,46 @@
191+#!/usr/bin/env python
192+"""
193+%prog [options]
194+"""
195+
196+import sys
197+
198+from txaws.credentials import AWSCredentials
199+from txaws.script import parse_options
200+from txaws.service import AWSServiceRegion
201+from txaws.util import reactor
202+
203+
204+def printResults(results):
205+ print results
206+ return 0
207+
208+
209+def printError(error):
210+ print error.value
211+ return 1
212+
213+
214+def finish(return_code):
215+ reactor.stop(exitStatus=return_code)
216+
217+
218+options, args = parse_options(__doc__.strip())
219+if options.bucket is None:
220+ print "Error Message: A bucket name is required."
221+ sys.exit(1)
222+if options.object_name is None:
223+ print "Error Message: An object name is required."
224+ sys.exit(1)
225+creds = AWSCredentials(options.access_key, options.secret_key)
226+region = AWSServiceRegion(
227+ creds=creds, region=options.region, s3_endpoint=options.url)
228+client = region.get_s3_client()
229+
230+d = client.get_object(options.bucket, options.object_name)
231+d.addCallback(printResults)
232+d.addErrback(printError)
233+d.addCallback(finish)
234+# We use a custom reactor so that we can return the exit status from
235+# reactor.run().
236+sys.exit(reactor.run())
237
238=== added file 'bin/txaws-head-object'
239--- bin/txaws-head-object 1970-01-01 00:00:00 +0000
240+++ bin/txaws-head-object 2009-11-28 01:10:25 +0000
241@@ -0,0 +1,47 @@
242+#!/usr/bin/env python
243+"""
244+%prog [options]
245+"""
246+
247+import sys
248+from pprint import pprint
249+
250+from txaws.credentials import AWSCredentials
251+from txaws.script import parse_options
252+from txaws.service import AWSServiceRegion
253+from txaws.util import reactor
254+
255+
256+def printResults(results):
257+ pprint(results)
258+ return 0
259+
260+
261+def printError(error):
262+ print error.value
263+ return 1
264+
265+
266+def finish(return_code):
267+ reactor.stop(exitStatus=return_code)
268+
269+
270+options, args = parse_options(__doc__.strip())
271+if options.bucket is None:
272+ print "Error Message: A bucket name is required."
273+ sys.exit(1)
274+if options.object_name is None:
275+ print "Error Message: An object name is required."
276+ sys.exit(1)
277+creds = AWSCredentials(options.access_key, options.secret_key)
278+region = AWSServiceRegion(
279+ creds=creds, region=options.region, s3_endpoint=options.url)
280+client = region.get_s3_client()
281+
282+d = client.head_object(options.bucket, options.object_name)
283+d.addCallback(printResults)
284+d.addErrback(printError)
285+d.addCallback(finish)
286+# We use a custom reactor so that we can return the exit status from
287+# reactor.run().
288+sys.exit(reactor.run())
289
290=== added file 'bin/txaws-list-buckets'
291--- bin/txaws-list-buckets 1970-01-01 00:00:00 +0000
292+++ bin/txaws-list-buckets 2009-11-28 01:10:25 +0000
293@@ -0,0 +1,43 @@
294+#!/usr/bin/env python
295+"""
296+%prog [options]
297+"""
298+
299+import sys
300+
301+from txaws.credentials import AWSCredentials
302+from txaws.script import parse_options
303+from txaws.service import AWSServiceRegion
304+from txaws.util import reactor
305+
306+
307+def printResults(results):
308+ print "\nBuckets:"
309+ for bucket in results:
310+ print "\t%s (created on %s)" % (bucket.name, bucket.creation_date)
311+ print "Total buckets: %s\n" % len(list(results))
312+ return 0
313+
314+
315+def printError(error):
316+ print error.value
317+ return 1
318+
319+
320+def finish(return_code):
321+ reactor.stop(exitStatus=return_code)
322+
323+
324+options, args = parse_options(__doc__.strip())
325+creds = AWSCredentials(options.access_key, options.secret_key)
326+region = AWSServiceRegion(
327+ creds=creds, region=options.region, s3_endpoint=options.url)
328+client = region.get_s3_client()
329+
330+d = client.list_buckets()
331+d.addCallback(printResults)
332+d.addErrback(printError)
333+d.addCallback(finish)
334+# We use a custom reactor so that we can return the exit status from
335+# reactor.run().
336+sys.exit(reactor.run())
337
338=== added file 'bin/txaws-put-object'
339--- bin/txaws-put-object 1970-01-01 00:00:00 +0000
340+++ bin/txaws-put-object 2009-11-28 01:10:25 +0000
341@@ -0,0 +1,56 @@
342+#!/usr/bin/env python
343+"""
344+%prog [options]
345+"""
346+
347+import os
348+import sys
349+
350+from txaws.credentials import AWSCredentials
351+from txaws.script import parse_options
352+from txaws.service import AWSServiceRegion
353+from txaws.util import reactor
354+
355+
356+def printResults(results):
357+ return 0
358+
359+
360+def printError(error):
361+ print error.value
362+ return 1
363+
364+
365+def finish(return_code):
366+ reactor.stop(exitStatus=return_code)
367+
368+
369+options, args = parse_options(__doc__.strip())
370+if options.bucket is None:
371+ print "Error Message: A bucket name is required."
372+ sys.exit(1)
373+filename = options.object_filename
374+if filename:
375+ options.object_name = os.path.basename(filename)
376+ try:
377+ options.object_data = open(filename).read()
378+ except Exception, error:
379+ print error
380+ sys.exit(1)
381+elif options.object_name is None:
382+ print "Error Message: An object name is required."
383+ sys.exit(1)
384+creds = AWSCredentials(options.access_key, options.secret_key)
385+region = AWSServiceRegion(
386+ creds=creds, region=options.region, s3_endpoint=options.url)
387+client = region.get_s3_client()
388+
389+d = client.put_object(
390+ options.bucket, options.object_name, options.object_data,
391+ options.content_type)
392+d.addCallback(printResults)
393+d.addErrback(printError)
394+d.addCallback(finish)
395+# We use a custom reactor so that we can return the exit status from
396+# reactor.run().
397+sys.exit(reactor.run())
398
399=== modified file 'txaws/client/base.py'
400--- txaws/client/base.py 2009-11-28 01:10:25 +0000
401+++ txaws/client/base.py 2009-11-28 01:10:26 +0000
402@@ -1,11 +1,50 @@
403+from xml.parsers.expat import ExpatError
404+
405 from twisted.internet import reactor, ssl
406+from twisted.web import http
407 from twisted.web.client import HTTPClientFactory
408+from twisted.web.error import Error as TwistedWebError
409
410 from txaws.util import parse
411 from txaws.credentials import AWSCredentials
412+from txaws.exception import AWSResponseParseError
413 from txaws.service import AWSServiceEndpoint
414
415
416+def error_wrapper(error, errorClass):
417+ """
418+ We want to see all error messages from cloud services. Amazon's EC2 says
419+ that their errors are accompanied either by a 400-series or 500-series HTTP
420+ response code. As such, the first thing we want to do is check to see if
421+ the error is in that range. If it is, we then need to see if the error
422+ message is an EC2 one.
423+
424+ In the event that an error is not a Twisted web error nor an EC2 one, the
425+ original exception is raised.
426+ """
427+ http_status = 0
428+ if error.check(TwistedWebError):
429+ xml_payload = error.value.response
430+ if error.value.status:
431+ http_status = int(error.value.status)
432+ else:
433+ error.raiseException()
434+ if http_status >= 400:
435+ if not xml_payload:
436+ error.raiseException()
437+ try:
438+ fallback_error = errorClass(
439+ xml_payload, error.value.status, error.value.message,
440+ error.value.response)
441+ except (ExpatError, AWSResponseParseError):
442+ error_message = http.RESPONSES.get(http_status)
443+ fallback_error = TwistedWebError(
444+ http_status, error_message, error.value.response)
445+ raise fallback_error
446+ else:
447+ error.raiseException()
448+
449+
450 class BaseClient(object):
451 """Create an AWS client.
452
453
454=== modified file 'txaws/ec2/client.py'
455--- txaws/ec2/client.py 2009-11-28 01:10:25 +0000
456+++ txaws/ec2/client.py 2009-11-28 01:10:26 +0000
457@@ -8,16 +8,11 @@
458 from datetime import datetime
459 from urllib import quote
460 from base64 import b64encode
461-from xml.parsers.expat import ExpatError
462-
463-from twisted.web import http
464-from twisted.web.error import Error as TwistedWebError
465
466 from txaws import version
467-from txaws.client.base import BaseClient, BaseQuery
468+from txaws.client.base import BaseClient, BaseQuery, error_wrapper
469 from txaws.ec2 import model
470 from txaws.ec2.exception import EC2Error
471-from txaws.exception import AWSResponseParseError
472 from txaws.util import iso8601time, XML
473
474
475@@ -25,34 +20,7 @@
476
477
478 def ec2_error_wrapper(error):
479- """
480- We want to see all error messages from cloud services. Amazon's EC2 says
481- that their errors are accompanied either by a 400-series or 500-series HTTP
482- response code. As such, the first thing we want to do is check to see if
483- the error is in that range. If it is, we then need to see if the error
484- message is an EC2 one.
485-
486- In the event that an error is not a Twisted web error nor an EC2 one, the
487- original exception is raised.
488- """
489- http_status = 0
490- if error.check(TwistedWebError):
491- xml_payload = error.value.response
492- if error.value.status:
493- http_status = int(error.value.status)
494- else:
495- error.raiseException()
496- if http_status >= 400:
497- try:
498- fallback_error = EC2Error(xml_payload, error.value.status,
499- error.value.message, error.value.response)
500- except (ExpatError, AWSResponseParseError):
501- error_message = http.RESPONSES.get(http_status)
502- fallback_error = TwistedWebError(http_status, error_message,
503- error.value.response)
504- raise fallback_error
505- else:
506- error.raiseException()
507+ error_wrapper(error, EC2Error)
508
509
510 class EC2Client(BaseClient):
511@@ -848,5 +816,4 @@
512 url = "%s?%s" % (self.endpoint.get_uri(),
513 self.get_canonical_query_params())
514 d = self.get_page(url, method=self.endpoint.method)
515- d.addErrback(ec2_error_wrapper)
516- return d
517+ return d.addErrback(ec2_error_wrapper)
518
519=== modified file 'txaws/ec2/exception.py'
520--- txaws/ec2/exception.py 2009-11-28 01:10:25 +0000
521+++ txaws/ec2/exception.py 2009-11-28 01:10:26 +0000
522@@ -1,39 +1,14 @@
523 # Copyright (c) 2009 Canonical Ltd <duncan.mcgreggor@canonical.com>
524 # Licenced under the txaws licence available at /LICENSE in the txaws source.
525
526-from txaws.exception import AWSError, AWSResponseParseError
527-from txaws.util import XML
528+from txaws.exception import AWSError
529
530
531 class EC2Error(AWSError):
532 """
533 A error class providing custom methods on EC2 errors.
534 """
535- def __init__(self, xml_bytes, status=None, message=None, response=None):
536- super(AWSError, self).__init__(status, message, response)
537- if not xml_bytes:
538- raise ValueError("XML cannot be empty.")
539- self.original = xml_bytes
540- self.errors = []
541- self.request_id = ""
542- self.host_id = ""
543- self.parse()
544-
545- def __str__(self):
546- return self._get_error_message_string()
547-
548- def __repr__(self):
549- return "<%s object with %s>" % (
550- self.__class__.__name__, self._get_error_code_string())
551-
552- def _set_request_id(self, tree):
553- request_id_node = tree.find(".//RequestID")
554- if hasattr(request_id_node, "text"):
555- text = request_id_node.text
556- if text:
557- self.request_id = text
558-
559- def _set_400_errors(self, tree):
560+ def _set_400_error(self, tree):
561 errors_node = tree.find(".//Errors")
562 if errors_node:
563 for error in errors_node:
564@@ -41,84 +16,5 @@
565 if data:
566 self.errors.append(data)
567
568- def _set_host_id(self, tree):
569- host_id = tree.find(".//HostID")
570- if hasattr(host_id, "text"):
571- text = host_id.text
572- if text:
573- self.host_id = text
574-
575- def _set_500_error(self, tree):
576- self._set_request_id(tree)
577- self._set_host_id(tree)
578- data = self._node_to_dict(tree)
579- if data:
580- self.errors.append(data)
581-
582- def _get_error_code_string(self):
583- count = len(self.errors)
584- error_code = self.get_error_codes()
585- if count > 1:
586- return "Error count: %s" % error_code
587- else:
588- return "Error code: %s" % error_code
589-
590- def _get_error_message_string(self):
591- count = len(self.errors)
592- error_message = self.get_error_messages()
593- if count > 1:
594- return "%s." % error_message
595- else:
596- return "Error Message: %s" % error_message
597-
598- def _node_to_dict(self, node):
599- data = {}
600- for child in node:
601- if child.tag and child.text:
602- data[child.tag] = child.text
603- return data
604-
605- def _check_for_html(self, tree):
606- if tree.tag == "html":
607- message = "Could not parse HTML in the response."
608- raise AWSResponseParseError(message)
609-
610- def parse(self, xml_bytes=""):
611- if not xml_bytes:
612- xml_bytes = self.original
613- self.original = xml_bytes
614- tree = XML(xml_bytes.strip())
615- self._check_for_html(tree)
616- self._set_request_id(tree)
617- if self.status:
618- status = int(self.status)
619- else:
620- status = 400
621- if status >= 500:
622- self._set_500_error(tree)
623- else:
624- self._set_400_errors(tree)
625-
626- def has_error(self, errorString):
627- for error in self.errors:
628- if errorString in error.values():
629- return True
630- return False
631-
632- def get_error_codes(self):
633- count = len(self.errors)
634- if count > 1:
635- return count
636- elif count == 0:
637- return
638- else:
639- return self.errors[0]["Code"]
640-
641- def get_error_messages(self):
642- count = len(self.errors)
643- if count > 1:
644- return "Multiple EC2 Errors"
645- elif count == 0:
646- return "Empty error list"
647- else:
648- return self.errors[0]["Message"]
649+
650+
651
652=== modified file 'txaws/ec2/tests/test_exception.py'
653--- txaws/ec2/tests/test_exception.py 2009-11-28 01:10:25 +0000
654+++ txaws/ec2/tests/test_exception.py 2009-11-28 01:10:26 +0000
655@@ -4,7 +4,6 @@
656 from twisted.trial.unittest import TestCase
657
658 from txaws.ec2.exception import EC2Error
659-from txaws.exception import AWSResponseParseError
660 from txaws.testing import payload
661 from txaws.util import XML
662
663@@ -14,77 +13,14 @@
664
665 class EC2ErrorTestCase(TestCase):
666
667- def test_creation(self):
668- error = EC2Error("<dummy1 />", 400, "Not Found", "<dummy2 />")
669- self.assertEquals(error.status, 400)
670- self.assertEquals(error.response, "<dummy2 />")
671- self.assertEquals(error.original, "<dummy1 />")
672- self.assertEquals(error.errors, [])
673- self.assertEquals(error.request_id, "")
674-
675- def test_node_to_dict(self):
676- xml = "<parent><child1>text1</child1><child2>text2</child2></parent>"
677- error = EC2Error("<dummy />")
678- data = error._node_to_dict(XML(xml))
679- self.assertEquals(data, {"child1": "text1", "child2": "text2"})
680-
681- def test_set_request_id(self):
682- xml = "<a><b /><RequestID>%s</RequestID></a>" % REQUEST_ID
683- error = EC2Error("<dummy />")
684- error._set_request_id(XML(xml))
685- self.assertEquals(error.request_id, REQUEST_ID)
686-
687- def test_set_400_errors(self):
688+ def test_set_400_error(self):
689 errorsXML = "<Error><Code>1</Code><Message>2</Message></Error>"
690 xml = "<a><Errors>%s</Errors><b /></a>" % errorsXML
691 error = EC2Error("<dummy />")
692- error._set_400_errors(XML(xml))
693+ error._set_400_error(XML(xml))
694 self.assertEquals(error.errors[0]["Code"], "1")
695 self.assertEquals(error.errors[0]["Message"], "2")
696
697- def test_set_host_id(self):
698- host_id = "ASD@#FDG$E%FG"
699- xml = "<a><b /><HostID>%s</HostID></a>" % host_id
700- error = EC2Error("<dummy />")
701- error._set_host_id(XML(xml))
702- self.assertEquals(error.host_id, host_id)
703-
704- def test_set_500_error(self):
705- xml = "<Error><Code>500</Code><Message>Oops</Message></Error>"
706- error = EC2Error("<dummy />")
707- error._set_500_error(XML(xml))
708- self.assertEquals(error.errors[0]["Code"], "500")
709- self.assertEquals(error.errors[0]["Message"], "Oops")
710-
711- def test_set_empty_errors(self):
712- xml = "<a><Errors /><b /></a>"
713- error = EC2Error("<dummy />")
714- error._set_400_errors(XML(xml))
715- self.assertEquals(error.errors, [])
716-
717- def test_set_empty_error(self):
718- xml = "<a><Errors><Error /><Error /></Errors><b /></a>"
719- error = EC2Error("<dummy />")
720- error._set_400_errors(XML(xml))
721- self.assertEquals(error.errors, [])
722-
723- def test_parse_without_xml(self):
724- xml = "<dummy />"
725- error = EC2Error(xml)
726- error.parse()
727- self.assertEquals(error.original, xml)
728-
729- def test_parse_with_xml(self):
730- xml1 = "<dummy1 />"
731- xml2 = "<dummy2 />"
732- error = EC2Error(xml1)
733- error.parse(xml2)
734- self.assertEquals(error.original, xml2)
735-
736- def test_parse_html(self):
737- xml = "<html><body>a page</body></html>"
738- self.assertRaises(AWSResponseParseError, EC2Error, xml)
739-
740 def test_has_error(self):
741 errorsXML = "<Error><Code>Code1</Code><Message>2</Message></Error>"
742 xml = "<a><Errors>%s</Errors><b /></a>" % errorsXML
743@@ -99,69 +35,6 @@
744 error = EC2Error(payload.sample_ec2_error_messages)
745 self.assertEquals(len(error.errors), 2)
746
747- def test_empty_xml(self):
748- self.assertRaises(ValueError, EC2Error, "")
749-
750- def test_no_request_id(self):
751- errors = "<Errors><Error><Code /><Message /></Error></Errors>"
752- xml = "<Response>%s<RequestID /></Response>" % errors
753- error = EC2Error(xml)
754- self.assertEquals(error.request_id, "")
755-
756- def test_no_request_id_node(self):
757- errors = "<Errors><Error><Code /><Message /></Error></Errors>"
758- xml = "<Response>%s</Response>" % errors
759- error = EC2Error(xml)
760- self.assertEquals(error.request_id, "")
761-
762- def test_no_errors_node(self):
763- xml = "<Response><RequestID /></Response>"
764- error = EC2Error(xml)
765- self.assertEquals(error.errors, [])
766-
767- def test_no_error_node(self):
768- xml = "<Response><Errors /><RequestID /></Response>"
769- error = EC2Error(xml)
770- self.assertEquals(error.errors, [])
771-
772- def test_no_error_code_node(self):
773- errors = "<Errors><Error><Message /></Error></Errors>"
774- xml = "<Response>%s<RequestID /></Response>" % errors
775- error = EC2Error(xml)
776- self.assertEquals(error.errors, [])
777-
778- def test_no_error_message_node(self):
779- errors = "<Errors><Error><Code /></Error></Errors>"
780- xml = "<Response>%s<RequestID /></Response>" % errors
781- error = EC2Error(xml)
782- self.assertEquals(error.errors, [])
783-
784- def test_single_get_error_codes(self):
785- error = EC2Error(payload.sample_ec2_error_message)
786- self.assertEquals(error.get_error_codes(), "Error.Code")
787-
788- def test_multiple_get_error_codes(self):
789- error = EC2Error(payload.sample_ec2_error_messages)
790- self.assertEquals(error.get_error_codes(), 2)
791-
792- def test_zero_get_error_codes(self):
793- xml = "<Response><RequestID /></Response>"
794- error = EC2Error(xml)
795- self.assertEquals(error.get_error_codes(), None)
796-
797- def test_single_get_error_messages(self):
798- error = EC2Error(payload.sample_ec2_error_message)
799- self.assertEquals(error.get_error_messages(), "Message for Error.Code")
800-
801- def test_multiple_get_error_messages(self):
802- error = EC2Error(payload.sample_ec2_error_messages)
803- self.assertEquals(error.get_error_messages(), "Multiple EC2 Errors")
804-
805- def test_zero_get_error_messages(self):
806- xml = "<Response><RequestID /></Response>"
807- error = EC2Error(xml)
808- self.assertEquals(error.get_error_messages(), "Empty error list")
809-
810 def test_single_error_str(self):
811 error = EC2Error(payload.sample_ec2_error_message)
812 self.assertEquals(str(error), "Error Message: Message for Error.Code")
813
814=== modified file 'txaws/exception.py'
815--- txaws/exception.py 2009-10-28 17:43:23 +0000
816+++ txaws/exception.py 2009-11-28 01:10:25 +0000
817@@ -3,11 +3,124 @@
818
819 from twisted.web.error import Error
820
821+from txaws.util import XML
822+
823
824 class AWSError(Error):
825 """
826 A base class for txAWS errors.
827 """
828+ def __init__(self, xml_bytes, status=None, message=None, response=None):
829+ super(AWSError, self).__init__(status, message, response)
830+ if not xml_bytes:
831+ raise ValueError("XML cannot be empty.")
832+ self.original = xml_bytes
833+ self.errors = []
834+ self.request_id = ""
835+ self.host_id = ""
836+ self.parse()
837+
838+ def __str__(self):
839+ return self._get_error_message_string()
840+
841+ def __repr__(self):
842+ return "<%s object with %s>" % (
843+ self.__class__.__name__, self._get_error_code_string())
844+
845+ def _set_request_id(self, tree):
846+ request_id_node = tree.find(".//RequestID")
847+ if hasattr(request_id_node, "text"):
848+ text = request_id_node.text
849+ if text:
850+ self.request_id = text
851+
852+ def _set_host_id(self, tree):
853+ host_id = tree.find(".//HostID")
854+ if hasattr(host_id, "text"):
855+ text = host_id.text
856+ if text:
857+ self.host_id = text
858+
859+ def _get_error_code_string(self):
860+ count = len(self.errors)
861+ error_code = self.get_error_codes()
862+ if count > 1:
863+ return "Error count: %s" % error_code
864+ else:
865+ return "Error code: %s" % error_code
866+
867+ def _get_error_message_string(self):
868+ count = len(self.errors)
869+ error_message = self.get_error_messages()
870+ if count > 1:
871+ return "%s." % error_message
872+ else:
873+ return "Error Message: %s" % error_message
874+
875+ def _node_to_dict(self, node):
876+ data = {}
877+ for child in node:
878+ if child.tag and child.text:
879+ data[child.tag] = child.text
880+ return data
881+
882+ def _check_for_html(self, tree):
883+ if tree.tag == "html":
884+ message = "Could not parse HTML in the response."
885+ raise AWSResponseParseError(message)
886+
887+ def _set_400_error(self, tree):
888+ """
889+ This method needs to be implemented by subclasses.
890+ """
891+
892+ def _set_500_error(self, tree):
893+ self._set_request_id(tree)
894+ self._set_host_id(tree)
895+ data = self._node_to_dict(tree)
896+ if data:
897+ self.errors.append(data)
898+
899+ def parse(self, xml_bytes=""):
900+ if not xml_bytes:
901+ xml_bytes = self.original
902+ self.original = xml_bytes
903+ tree = XML(xml_bytes.strip())
904+ self._check_for_html(tree)
905+ self._set_request_id(tree)
906+ if self.status:
907+ status = int(self.status)
908+ else:
909+ status = 400
910+ if status >= 500:
911+ self._set_500_error(tree)
912+ else:
913+ self._set_400_error(tree)
914+
915+ def has_error(self, errorString):
916+ for error in self.errors:
917+ if errorString in error.values():
918+ return True
919+ return False
920+
921+ def get_error_codes(self):
922+ count = len(self.errors)
923+ if count > 1:
924+ return count
925+ elif count == 0:
926+ return
927+ else:
928+ return self.errors[0]["Code"]
929+
930+ def get_error_messages(self):
931+ count = len(self.errors)
932+ if count > 1:
933+ return "Multiple EC2 Errors"
934+ elif count == 0:
935+ return "Empty error list"
936+ else:
937+ return self.errors[0]["Message"]
938+
939
940
941 class AWSResponseParseError(Exception):
942
943=== added file 'txaws/meta.py'
944--- txaws/meta.py 1970-01-01 00:00:00 +0000
945+++ txaws/meta.py 2009-11-28 01:10:25 +0000
946@@ -0,0 +1,10 @@
947+display_name = "txAWS"
948+library_name = "txaws"
949+author = "txAWS Deelopers"
950+author_email = "txaws-dev@lists.launchpad.net"
951+license = "MIT"
952+url = "http://launchpad.net/txaws"
953+description = """
954+Twisted-based Asynchronous Libraries for Amazon Web Services
955+"""
956+
957
958=== modified file 'txaws/s3/client.py'
959--- txaws/s3/client.py 2009-11-28 01:10:25 +0000
960+++ txaws/s3/client.py 2009-11-28 01:10:26 +0000
961@@ -17,12 +17,17 @@
962
963 from epsilon.extime import Time
964
965-from txaws.client.base import BaseClient, BaseQuery
966+from txaws.client.base import BaseClient, BaseQuery, error_wrapper
967 from txaws.s3 import model
968+from txaws.s3.exception import S3Error
969 from txaws.service import AWSServiceEndpoint, S3_ENDPOINT
970 from txaws.util import XML, calculate_md5
971
972
973+def s3_error_wrapper(error):
974+ error_wrapper(error, S3Error)
975+
976+
977 class URLContext(object):
978 """
979 The hosts and the paths that form an S3 endpoint change depending upon the
980@@ -190,6 +195,10 @@
981 self.endpoint.set_method(self.action)
982
983 def set_content_type(self):
984+ """
985+ Set the content type based on the file extension used in the object
986+ name.
987+ """
988 if self.object_name and not self.content_type:
989 # XXX nothing is currently done with the encoding... we may
990 # need to in the future
991@@ -197,6 +206,9 @@
992 self.object_name, strict=False)
993
994 def get_headers(self):
995+ """
996+ Build the list of headers needed in order to perform S3 operations.
997+ """
998 headers = {"Content-Length": len(self.data),
999 "Content-MD5": calculate_md5(self.data),
1000 "Date": self.date}
1001@@ -214,6 +226,9 @@
1002 return headers
1003
1004 def get_canonicalized_amz_headers(self, headers):
1005+ """
1006+ Get the headers defined by Amazon S3.
1007+ """
1008 headers = [
1009 (name.lower(), value) for name, value in headers.iteritems()
1010 if name.lower().startswith("x-amz-")]
1011@@ -224,6 +239,9 @@
1012 return "".join("%s:%s\n" % (name, value) for name, value in headers)
1013
1014 def get_canonicalized_resource(self):
1015+ """
1016+ Get an S3 resource path.
1017+ """
1018 resource = "/"
1019 if self.bucket:
1020 resource += self.bucket
1021@@ -232,7 +250,7 @@
1022 return resource
1023
1024 def sign(self, headers):
1025-
1026+ """Sign this query using its built in credentials."""
1027 text = (self.action + "\n" +
1028 headers.get("Content-MD5", "") + "\n" +
1029 headers.get("Content-Type", "") + "\n" +
1030@@ -242,14 +260,14 @@
1031 return self.creds.sign(text, hash_type="sha1")
1032
1033 def submit(self, url_context=None):
1034+ """Submit this query.
1035+
1036+ @return: A deferred from get_page
1037+ """
1038 if not url_context:
1039 url_context = URLContext(
1040 self.endpoint, self.bucket, self.object_name)
1041 d = self.get_page(
1042 url_context.get_url(), method=self.action, postdata=self.data,
1043 headers=self.get_headers())
1044- # XXX - we need an error wrapper like we have for ec2... but let's
1045- # wait until the new error-wrapper branch has landed, and possibly
1046- # generalize a base class for all clients.
1047- #d.addErrback(s3_error_wrapper)
1048- return d
1049+ return d.addErrback(s3_error_wrapper)
1050
1051=== added file 'txaws/s3/exception.py'
1052--- txaws/s3/exception.py 1970-01-01 00:00:00 +0000
1053+++ txaws/s3/exception.py 2009-11-28 01:10:26 +0000
1054@@ -0,0 +1,21 @@
1055+# Copyright (c) 2009 Canonical Ltd <duncan.mcgreggor@canonical.com>
1056+# Licenced under the txaws licence available at /LICENSE in the txaws source.
1057+
1058+from txaws.exception import AWSError
1059+
1060+
1061+class S3Error(AWSError):
1062+ """
1063+ A error class providing custom methods on S3 errors.
1064+ """
1065+ def _set_400_error(self, tree):
1066+ if tree.tag.lower() == "error":
1067+ data = self._node_to_dict(tree)
1068+ if data:
1069+ self.errors.append(data)
1070+
1071+ def get_error_code(self, *args, **kwargs):
1072+ return super(S3Error, self).get_error_codes(*args, **kwargs)
1073+
1074+ def get_error_message(self, *args, **kwargs):
1075+ return super(S3Error, self).get_error_messages(*args, **kwargs)
1076
1077=== added file 'txaws/s3/tests/test_exception.py'
1078--- txaws/s3/tests/test_exception.py 1970-01-01 00:00:00 +0000
1079+++ txaws/s3/tests/test_exception.py 2009-11-28 01:10:26 +0000
1080@@ -0,0 +1,62 @@
1081+# Copyright (c) 2009 Canonical Ltd <duncan.mcgreggor@canonical.com>
1082+# Licenced under the txaws licence available at /LICENSE in the txaws source.
1083+
1084+from twisted.trial.unittest import TestCase
1085+
1086+from txaws.s3.exception import S3Error
1087+from txaws.testing import payload
1088+from txaws.util import XML
1089+
1090+
1091+REQUEST_ID = "0ef9fc37-6230-4d81-b2e6-1b36277d4247"
1092+
1093+
1094+class S3ErrorTestCase(TestCase):
1095+
1096+ def test_set_400_error(self):
1097+ xml = "<Error><Code>1</Code><Message>2</Message></Error>"
1098+ error = S3Error("<dummy />")
1099+ error._set_400_error(XML(xml))
1100+ self.assertEquals(error.errors[0]["Code"], "1")
1101+ self.assertEquals(error.errors[0]["Message"], "2")
1102+
1103+ def test_get_error_code(self):
1104+ error = S3Error(payload.sample_s3_invalid_access_key_result)
1105+ self.assertEquals(error.get_error_code(), "InvalidAccessKeyId")
1106+
1107+ def test_get_error_message(self):
1108+ error = S3Error(payload.sample_s3_invalid_access_key_result)
1109+ self.assertEquals(
1110+ error.get_error_message(),
1111+ ("The AWS Access Key Id you provided does not exist in our "
1112+ "records."))
1113+
1114+ def test_error_count(self):
1115+ error = S3Error(payload.sample_s3_invalid_access_key_result)
1116+ self.assertEquals(len(error.errors), 1)
1117+
1118+ def test_error_repr(self):
1119+ error = S3Error(payload.sample_s3_invalid_access_key_result)
1120+ self.assertEquals(
1121+ repr(error),
1122+ "<S3Error object with Error code: InvalidAccessKeyId>")
1123+
1124+ def test_signature_mismatch_result(self):
1125+ error = S3Error(payload.sample_s3_signature_mismatch)
1126+ self.assertEquals(
1127+ error.get_error_messages(),
1128+ ("The request signature we calculated does not match the "
1129+ "signature you provided. Check your key and signing method."))
1130+
1131+ def test_invalid_access_key_result(self):
1132+ error = S3Error(payload.sample_s3_invalid_access_key_result)
1133+ self.assertEquals(
1134+ error.get_error_messages(),
1135+ ("The AWS Access Key Id you provided does not exist in our "
1136+ "records."))
1137+
1138+ def test_internal_error_result(self):
1139+ error = S3Error(payload.sample_server_internal_error_result)
1140+ self.assertEquals(
1141+ error.get_error_messages(),
1142+ "We encountered an internal error. Please try again.")
1143
1144=== added file 'txaws/script.py'
1145--- txaws/script.py 1970-01-01 00:00:00 +0000
1146+++ txaws/script.py 2009-11-28 01:10:25 +0000
1147@@ -0,0 +1,42 @@
1148+from optparse import OptionParser
1149+
1150+from txaws import meta
1151+from txaws import version
1152+
1153+
1154+# XXX Once we start adding script that require conflicting options, we'll need
1155+# multiple parsers and option dispatching...
1156+def parse_options(usage):
1157+ parser = OptionParser(usage, version="%s %s" % (
1158+ meta.display_name, version.txaws))
1159+ parser.add_option(
1160+ "-a", "--access-key", dest="access_key", help="access key ID")
1161+ parser.add_option(
1162+ "-s", "--secret-key", dest="secret_key", help="access secret key")
1163+ parser.add_option(
1164+ "-r", "--region", dest="region", help="US or EU (valid for AWS only)")
1165+ parser.add_option(
1166+ "-U", "--url", dest="url", help="service URL/endpoint")
1167+ parser.add_option(
1168+ "-b", "--bucket", dest="bucket", help="name of the bucket")
1169+ parser.add_option(
1170+ "-o", "--object-name", dest="object_name", help="name of the object")
1171+ parser.add_option(
1172+ "-d", "--object-data", dest="object_data",
1173+ help="content data of the object")
1174+ parser.add_option(
1175+ "--object-file", dest="object_filename",
1176+ help=("the path to the file that will be saved as an object; if "
1177+ "provided, the --object-name and --object-data options are "
1178+ "not necessary"))
1179+ parser.add_option(
1180+ "-c", "--content-type", dest="content_type",
1181+ help="content type of the object")
1182+ options, args = parser.parse_args()
1183+ if not (options.access_key and options.secret_key):
1184+ parser.error(
1185+ "both the access key ID and the secret key must be supplied")
1186+ region = options.region
1187+ if region and region.upper() not in ["US", "EU"]:
1188+ parser.error("region must be one of 'US' or 'EU'")
1189+ return (options, args)
1190
1191=== modified file 'txaws/testing/payload.py'
1192--- txaws/testing/payload.py 2009-11-28 01:10:25 +0000
1193+++ txaws/testing/payload.py 2009-11-28 01:10:26 +0000
1194@@ -656,6 +656,31 @@
1195 """
1196
1197
1198+sample_restricted_resource_result = """\
1199+<?xml version="1.0"?>
1200+<Response>
1201+ <Errors>
1202+ <Error>
1203+ <Code>AuthFailure</Code>
1204+ <Message>Unauthorized attempt to access restricted resource</Message>
1205+ </Error>
1206+ </Errors>
1207+ <RequestID>a99e832e-e6e0-416a-9a35-81798ea521b4</RequestID>
1208+</Response>
1209+"""
1210+
1211+
1212+sample_server_internal_error_result = """\
1213+<?xml version="1.0" encoding="UTF-8"?>
1214+<Error>
1215+ <Code>InternalError</Code>
1216+ <Message>We encountered an internal error. Please try again.</Message>
1217+ <RequestID>A2A7E5395E27DFBB</RequestID>
1218+ <HostID>f691zulHNsUqonsZkjhILnvWwD3ZnmOM4ObM1wXTc6xuS3GzPmjArp8QC/sGsn6K</HostID>
1219+</Error>
1220+"""
1221+
1222+
1223 sample_list_buckets_result = """\
1224 <?xml version="1.0" encoding="UTF-8"?>
1225 <ListAllMyBucketsResult xmlns="http://s3.amazonaws.com/doc/%s/">
1226@@ -692,26 +717,13 @@
1227 """
1228
1229
1230-sample_server_internal_error_result = """\
1231+sample_s3_invalid_access_key_result = """\
1232 <?xml version="1.0" encoding="UTF-8"?>
1233 <Error>
1234- <Code>InternalError</Code>
1235- <Message>We encountered an internal error. Please try again.</Message>
1236- <RequestID>A2A7E5395E27DFBB</RequestID>
1237- <HostID>f691zulHNsUqonsZkjhILnvWwD3ZnmOM4ObM1wXTc6xuS3GzPmjArp8QC/sGsn6K</HostID>
1238+ <Code>InvalidAccessKeyId</Code>
1239+ <Message>The AWS Access Key Id you provided does not exist in our records.</Message>
1240+ <RequestId>0223AD81A94821CE</RequestId>
1241+ <HostId>HAw5g9P1VkN8ztgLKFTK20CY5LmCfTwXcSths1O7UQV6NuJx2P4tmFnpuOsziwOE</HostId>
1242+ <AWSAccessKeyId>SOMEKEYID</AWSAccessKeyId>
1243 </Error>
1244 """
1245-
1246-
1247-sample_restricted_resource_result = """\
1248-<?xml version="1.0"?>
1249-<Response>
1250- <Errors>
1251- <Error>
1252- <Code>AuthFailure</Code>
1253- <Message>Unauthorized attempt to access restricted resource</Message>
1254- </Error>
1255- </Errors>
1256- <RequestID>a99e832e-e6e0-416a-9a35-81798ea521b4</RequestID>
1257-</Response>
1258-"""
1259
1260=== added file 'txaws/tests/test_exception.py'
1261--- txaws/tests/test_exception.py 1970-01-01 00:00:00 +0000
1262+++ txaws/tests/test_exception.py 2009-11-28 01:10:26 +0000
1263@@ -0,0 +1,114 @@
1264+# Copyright (c) 2009 Canonical Ltd <duncan.mcgreggor@canonical.com>
1265+# Licenced under the txaws licence available at /LICENSE in the txaws source.
1266+
1267+from twisted.trial.unittest import TestCase
1268+
1269+from txaws.exception import AWSError
1270+from txaws.exception import AWSResponseParseError
1271+from txaws.util import XML
1272+
1273+
1274+REQUEST_ID = "0ef9fc37-6230-4d81-b2e6-1b36277d4247"
1275+
1276+
1277+class AWSErrorTestCase(TestCase):
1278+
1279+ def test_creation(self):
1280+ error = AWSError("<dummy1 />", 500, "Server Error", "<dummy2 />")
1281+ self.assertEquals(error.status, 500)
1282+ self.assertEquals(error.response, "<dummy2 />")
1283+ self.assertEquals(error.original, "<dummy1 />")
1284+ self.assertEquals(error.errors, [])
1285+ self.assertEquals(error.request_id, "")
1286+
1287+ def test_node_to_dict(self):
1288+ xml = "<parent><child1>text1</child1><child2>text2</child2></parent>"
1289+ error = AWSError("<dummy />")
1290+ data = error._node_to_dict(XML(xml))
1291+ self.assertEquals(data, {"child1": "text1", "child2": "text2"})
1292+
1293+ def test_set_request_id(self):
1294+ xml = "<a><b /><RequestID>%s</RequestID></a>" % REQUEST_ID
1295+ error = AWSError("<dummy />")
1296+ error._set_request_id(XML(xml))
1297+ self.assertEquals(error.request_id, REQUEST_ID)
1298+
1299+ def test_set_host_id(self):
1300+ host_id = "ASD@#FDG$E%FG"
1301+ xml = "<a><b /><HostID>%s</HostID></a>" % host_id
1302+ error = AWSError("<dummy />")
1303+ error._set_host_id(XML(xml))
1304+ self.assertEquals(error.host_id, host_id)
1305+
1306+ def test_set_empty_errors(self):
1307+ xml = "<a><Errors /><b /></a>"
1308+ error = AWSError("<dummy />")
1309+ error._set_500_error(XML(xml))
1310+ self.assertEquals(error.errors, [])
1311+
1312+ def test_set_empty_error(self):
1313+ xml = "<a><Errors><Error /><Error /></Errors><b /></a>"
1314+ error = AWSError("<dummy />")
1315+ error._set_500_error(XML(xml))
1316+ self.assertEquals(error.errors, [])
1317+
1318+ def test_parse_without_xml(self):
1319+ xml = "<dummy />"
1320+ error = AWSError(xml)
1321+ error.parse()
1322+ self.assertEquals(error.original, xml)
1323+
1324+ def test_parse_with_xml(self):
1325+ xml1 = "<dummy1 />"
1326+ xml2 = "<dummy2 />"
1327+ error = AWSError(xml1)
1328+ error.parse(xml2)
1329+ self.assertEquals(error.original, xml2)
1330+
1331+ def test_parse_html(self):
1332+ xml = "<html><body>a page</body></html>"
1333+ self.assertRaises(AWSResponseParseError, AWSError, xml)
1334+
1335+ def test_empty_xml(self):
1336+ self.assertRaises(ValueError, AWSError, "")
1337+
1338+ def test_no_request_id(self):
1339+ errors = "<Errors><Error><Code /><Message /></Error></Errors>"
1340+ xml = "<Response>%s<RequestID /></Response>" % errors
1341+ error = AWSError(xml)
1342+ self.assertEquals(error.request_id, "")
1343+
1344+ def test_no_request_id_node(self):
1345+ errors = "<Errors><Error><Code /><Message /></Error></Errors>"
1346+ xml = "<Response>%s</Response>" % errors
1347+ error = AWSError(xml)
1348+ self.assertEquals(error.request_id, "")
1349+
1350+ def test_no_errors_node(self):
1351+ xml = "<Response><RequestID /></Response>"
1352+ error = AWSError(xml)
1353+ self.assertEquals(error.errors, [])
1354+
1355+ def test_no_error_node(self):
1356+ xml = "<Response><Errors /><RequestID /></Response>"
1357+ error = AWSError(xml)
1358+ self.assertEquals(error.errors, [])
1359+
1360+ def test_no_error_code_node(self):
1361+ errors = "<Errors><Error><Message /></Error></Errors>"
1362+ xml = "<Response>%s<RequestID /></Response>" % errors
1363+ error = AWSError(xml)
1364+ self.assertEquals(error.errors, [])
1365+
1366+ def test_no_error_message_node(self):
1367+ errors = "<Errors><Error><Code /></Error></Errors>"
1368+ xml = "<Response>%s<RequestID /></Response>" % errors
1369+ error = AWSError(xml)
1370+ self.assertEquals(error.errors, [])
1371+
1372+ def test_set_500_error(self):
1373+ xml = "<Error><Code>500</Code><Message>Oops</Message></Error>"
1374+ error = AWSError("<dummy />")
1375+ error._set_500_error(XML(xml))
1376+ self.assertEquals(error.errors[0]["Code"], "500")
1377+ self.assertEquals(error.errors[0]["Message"], "Oops")
1378
1379=== modified file 'txaws/util.py'
1380--- txaws/util.py 2009-11-28 01:10:25 +0000
1381+++ txaws/util.py 2009-11-28 01:10:26 +0000
1382@@ -94,3 +94,33 @@
1383 if path == "":
1384 path = "/"
1385 return (str(scheme), str(host), port, str(path))
1386+
1387+
1388+def get_exitcode_reactor():
1389+ """
1390+ This is only neccesary until a fix like the one outlined here is
1391+ implemented for Twisted:
1392+ http://twistedmatrix.com/trac/ticket/2182
1393+ """
1394+ from twisted.internet.main import installReactor
1395+ from twisted.internet.selectreactor import SelectReactor
1396+
1397+ class ExitCodeReactor(SelectReactor):
1398+
1399+ def stop(self, exitStatus=0):
1400+ super(ExitCodeReactor, self).stop()
1401+ self.exitStatus = exitStatus
1402+
1403+ def run(self, *args, **kwargs):
1404+ super(ExitCodeReactor, self).run(*args, **kwargs)
1405+ return self.exitStatus
1406+
1407+ reactor = ExitCodeReactor()
1408+ installReactor(reactor)
1409+ return reactor
1410+
1411+
1412+try:
1413+ reactor = get_exitcode_reactor()
1414+except:
1415+ from twisted.internet import reactor

Subscribers

People subscribed via source and target branches

to all changes: