Merge lp:~oubiwann/txaws/486363-no-content-fix into lp:txaws

Proposed by Duncan McGreggor
Status: Merged
Merge reported by: Duncan McGreggor
Merged at revision: not available
Proposed branch: lp:~oubiwann/txaws/486363-no-content-fix
Merge into: lp:txaws
Prerequisite: lp:~oubiwann/txaws/484858-s3-scripts
Diff against target: 2499 lines (+1095/-453)
26 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 (+41/-0)
txaws/client/tests/test_client.py (+41/-2)
txaws/ec2/client.py (+70/-85)
txaws/ec2/exception.py (+4/-108)
txaws/ec2/tests/test_client.py (+132/-87)
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/service.py (+12/-9)
txaws/testing/payload.py (+31/-19)
txaws/tests/test_exception.py (+114/-0)
txaws/tests/test_service.py (+7/-2)
txaws/util.py (+30/-0)
To merge this branch: bzr merge lp:~oubiwann/txaws/486363-no-content-fix
Reviewer Review Type Date Requested Status
Duncan McGreggor Approve
Review via email: mp+15339@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 adds a check in the error wrapper for HTTP status codes from 200 (inclusive) up to (but not including) 300. Both the bucket and object delete scripts now work as expected, and have proper exit status upon successful deletion.

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

> This branch adds a check in the error wrapper for HTTP status codes from 200
> (inclusive) up to (but not including) 300. Both the bucket and object delete
> scripts now work as expected, and have proper exit status upon successful
> deletion.

I forgot to mention that this branch is based on lp:~oubiwann/txaws/484858-s3-scripts (in case a reviewer is not used to looking at the prerequisite branch field).

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 approved this for merge via a merge proposal for a child branch.

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:23 +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:23 +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:23 +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:23 +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:23 +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:23 +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:23 +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:23 +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:23 +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:23 +0000
401+++ txaws/client/base.py 2009-11-28 01:10:23 +0000
402@@ -1,11 +1,52 @@
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+ elif 200 <= http_status < 300:
447+ return str(error.value)
448+ else:
449+ error.raiseException()
450+
451+
452 class BaseClient(object):
453 """Create an AWS client.
454
455
456=== modified file 'txaws/client/tests/test_client.py'
457--- txaws/client/tests/test_client.py 2009-11-28 01:10:23 +0000
458+++ txaws/client/tests/test_client.py 2009-11-28 01:10:23 +0000
459@@ -1,16 +1,55 @@
460 import os
461
462 from twisted.internet import reactor
463+from twisted.internet.error import ConnectionRefusedError
464 from twisted.protocols.policies import WrappingFactory
465 from twisted.python import log
466 from twisted.python.filepath import FilePath
467+from twisted.python.failure import Failure
468+from twisted.web import server, static
469 from twisted.web.client import HTTPClientFactory
470-from twisted.web import server, static
471+from twisted.web.error import Error as TwistedWebError
472
473-from txaws.client.base import BaseClient, BaseQuery
474+from txaws.client.base import BaseClient, BaseQuery, error_wrapper
475 from txaws.testing.base import TXAWSTestCase
476
477
478+class ErrorWrapperTestCase(TXAWSTestCase):
479+
480+ def test_204_no_content(self):
481+ failure = Failure(TwistedWebError(204, "No content"))
482+ wrapped = error_wrapper(failure, None)
483+ self.assertEquals(wrapped, "204 No content")
484+
485+ def test_302_found(self):
486+ # XXX I'm not sure we want to raise for 300s...
487+ failure = Failure(TwistedWebError(302, "found"))
488+ error = self.assertRaises(
489+ Exception, error_wrapper, failure, None)
490+ self.assertEquals(failure.type, type(error))
491+ self.assertTrue(isinstance(error, TwistedWebError))
492+ self.assertEquals(str(error), "302 found")
493+
494+ def test_500(self):
495+ failure = Failure(TwistedWebError(500, "internal error"))
496+ error = self.assertRaises(
497+ Exception, error_wrapper, failure, None)
498+ self.assertTrue(isinstance(error, TwistedWebError))
499+ self.assertEquals(str(error), "500 internal error")
500+
501+ def test_timeout_error(self):
502+ failure = Failure(Exception("timeout"))
503+ error = self.assertRaises(Exception, error_wrapper, failure, None)
504+ self.assertTrue(isinstance(error, Exception))
505+ self.assertEquals(error.message, "timeout")
506+
507+ def test_connection_error(self):
508+ failure = Failure(ConnectionRefusedError("timeout"))
509+ error = self.assertRaises(
510+ Exception, error_wrapper, failure, ConnectionRefusedError)
511+ self.assertTrue(isinstance(error, ConnectionRefusedError))
512+
513+
514 class BaseClientTestCase(TXAWSTestCase):
515
516 def test_creation(self):
517
518=== modified file 'txaws/ec2/client.py'
519--- txaws/ec2/client.py 2009-11-28 01:10:23 +0000
520+++ txaws/ec2/client.py 2009-11-28 01:10:23 +0000
521@@ -8,16 +8,11 @@
522 from datetime import datetime
523 from urllib import quote
524 from base64 import b64encode
525-from xml.parsers.expat import ExpatError
526-
527-from twisted.web import http
528-from twisted.web.error import Error as TwistedWebError
529
530 from txaws import version
531-from txaws.client.base import BaseClient, BaseQuery
532+from txaws.client.base import BaseClient, BaseQuery, error_wrapper
533 from txaws.ec2 import model
534 from txaws.ec2.exception import EC2Error
535-from txaws.exception import AWSResponseParseError
536 from txaws.util import iso8601time, XML
537
538
539@@ -25,34 +20,7 @@
540
541
542 def ec2_error_wrapper(error):
543- """
544- We want to see all error messages from cloud services. Amazon's EC2 says
545- that their errors are accompanied either by a 400-series or 500-series HTTP
546- response code. As such, the first thing we want to do is check to see if
547- the error is in that range. If it is, we then need to see if the error
548- message is an EC2 one.
549-
550- In the event that an error is not a Twisted web error nor an EC2 one, the
551- original exception is raised.
552- """
553- http_status = 0
554- if error.check(TwistedWebError):
555- xml_payload = error.value.response
556- if error.value.status:
557- http_status = int(error.value.status)
558- else:
559- error.raiseException()
560- if http_status >= 400:
561- try:
562- fallback_error = EC2Error(xml_payload, error.value.status,
563- error.value.message, error.value.response)
564- except (ExpatError, AWSResponseParseError):
565- error_message = http.RESPONSES.get(http_status)
566- fallback_error = TwistedWebError(http_status, error_message,
567- error.value.response)
568- raise fallback_error
569- else:
570- error.raiseException()
571+ error_wrapper(error, EC2Error)
572
573
574 class EC2Client(BaseClient):
575@@ -60,16 +28,17 @@
576
577 def __init__(self, creds=None, endpoint=None, query_factory=None):
578 if query_factory is None:
579- self.query_factory = Query
580+ query_factory = Query
581 super(EC2Client, self).__init__(creds, endpoint, query_factory)
582
583 def describe_instances(self, *instance_ids):
584 """Describe current instances."""
585- instanceset = {}
586+ instances= {}
587 for pos, instance_id in enumerate(instance_ids):
588- instanceset["InstanceId.%d" % (pos + 1)] = instance_id
589- query = self.query_factory("DescribeInstances", self.creds,
590- self.endpoint, instanceset)
591+ instances["InstanceId.%d" % (pos + 1)] = instance_id
592+ query = self.query_factory(
593+ action="DescribeInstances", creds=self.creds,
594+ endpoint=self.endpoint, other_params=instances)
595 d = query.submit()
596 return d.addCallback(self._parse_describe_instances)
597
598@@ -164,7 +133,8 @@
599 if ramdisk_id is not None:
600 params["RamdiskId"] = ramdisk_id
601 query = self.query_factory(
602- "RunInstances", self.creds, self.endpoint, params)
603+ action="RunInstances", creds=self.creds, endpoint=self.endpoint,
604+ other_params=params)
605 d = query.submit()
606 return d.addCallback(self._parse_run_instances)
607
608@@ -195,11 +165,12 @@
609 @return: A deferred which on success gives an iterable of
610 (id, old-state, new-state) tuples.
611 """
612- instanceset = {}
613+ instances = {}
614 for pos, instance_id in enumerate(instance_ids):
615- instanceset["InstanceId.%d" % (pos+1)] = instance_id
616+ instances["InstanceId.%d" % (pos+1)] = instance_id
617 query = self.query_factory(
618- "TerminateInstances", self.creds, self.endpoint, instanceset)
619+ action="TerminateInstances", creds=self.creds,
620+ endpoint=self.endpoint, other_params=instances)
621 d = query.submit()
622 return d.addCallback(self._parse_terminate_instances)
623
624@@ -224,12 +195,13 @@
625 @return: A C{Deferred} that will fire with a list of L{SecurityGroup}s
626 retrieved from the cloud.
627 """
628- group_names = None
629+ group_names = {}
630 if names:
631 group_names = dict([("GroupName.%d" % (i+1), name)
632 for i, name in enumerate(names)])
633- query = self.query_factory("DescribeSecurityGroups", self.creds,
634- self.endpoint, group_names)
635+ query = self.query_factory(
636+ action="DescribeSecurityGroups", creds=self.creds,
637+ endpoint=self.endpoint, other_params=group_names)
638 d = query.submit()
639 return d.addCallback(self._parse_describe_security_groups)
640
641@@ -282,8 +254,9 @@
642 success of the operation.
643 """
644 parameters = {"GroupName": name, "GroupDescription": description}
645- query = self.query_factory("CreateSecurityGroup", self.creds,
646- self.endpoint, parameters)
647+ query = self.query_factory(
648+ action="CreateSecurityGroup", creds=self.creds,
649+ endpoint=self.endpoint, other_params=parameters)
650 d = query.submit()
651 return d.addCallback(self._parse_truth_return)
652
653@@ -298,8 +271,9 @@
654 success of the operation.
655 """
656 parameter = {"GroupName": name}
657- query = self.query_factory("DeleteSecurityGroup", self.creds,
658- self.endpoint, parameter)
659+ query = self.query_factory(
660+ action="DeleteSecurityGroup", creds=self.creds,
661+ endpoint=self.endpoint, other_params=parameter)
662 d = query.submit()
663 return d.addCallback(self._parse_truth_return)
664
665@@ -354,8 +328,9 @@
666 "all the ip parameters.")
667 raise ValueError(msg)
668 parameters["GroupName"] = group_name
669- query = self.query_factory("AuthorizeSecurityGroupIngress", self.creds,
670- self.endpoint, parameters)
671+ query = self.query_factory(
672+ action="AuthorizeSecurityGroupIngress", creds=self.creds,
673+ endpoint=self.endpoint, other_params=parameters)
674 d = query.submit()
675 return d.addCallback(self._parse_truth_return)
676
677@@ -438,8 +413,9 @@
678 "all the ip parameters.")
679 raise ValueError(msg)
680 parameters["GroupName"] = group_name
681- query = self.query_factory("RevokeSecurityGroupIngress", self.creds,
682- self.endpoint, parameters)
683+ query = self.query_factory(
684+ action="RevokeSecurityGroupIngress", creds=self.creds,
685+ endpoint=self.endpoint, other_params=parameters)
686 d = query.submit()
687 return d.addCallback(self._parse_truth_return)
688
689@@ -477,7 +453,8 @@
690 for pos, volume_id in enumerate(volume_ids):
691 volumeset["VolumeId.%d" % (pos + 1)] = volume_id
692 query = self.query_factory(
693- "DescribeVolumes", self.creds, self.endpoint, volumeset)
694+ action="DescribeVolumes", creds=self.creds, endpoint=self.endpoint,
695+ other_params=volumeset)
696 d = query.submit()
697 return d.addCallback(self._parse_describe_volumes)
698
699@@ -520,7 +497,8 @@
700 if snapshot_id is not None:
701 params["SnapshotId"] = snapshot_id
702 query = self.query_factory(
703- "CreateVolume", self.creds, self.endpoint, params)
704+ action="CreateVolume", creds=self.creds, endpoint=self.endpoint,
705+ other_params=params)
706 d = query.submit()
707 return d.addCallback(self._parse_create_volume)
708
709@@ -541,7 +519,8 @@
710
711 def delete_volume(self, volume_id):
712 query = self.query_factory(
713- "DeleteVolume", self.creds, self.endpoint, {"VolumeId": volume_id})
714+ action="DeleteVolume", creds=self.creds, endpoint=self.endpoint,
715+ other_params={"VolumeId": volume_id})
716 d = query.submit()
717 return d.addCallback(self._parse_truth_return)
718
719@@ -551,7 +530,8 @@
720 for pos, snapshot_id in enumerate(snapshot_ids):
721 snapshot_set["SnapshotId.%d" % (pos + 1)] = snapshot_id
722 query = self.query_factory(
723- "DescribeSnapshots", self.creds, self.endpoint, snapshot_set)
724+ action="DescribeSnapshots", creds=self.creds,
725+ endpoint=self.endpoint, other_params=snapshot_set)
726 d = query.submit()
727 return d.addCallback(self._parse_snapshots)
728
729@@ -575,8 +555,8 @@
730 def create_snapshot(self, volume_id):
731 """Create a new snapshot of an existing volume."""
732 query = self.query_factory(
733- "CreateSnapshot", self.creds, self.endpoint,
734- {"VolumeId": volume_id})
735+ action="CreateSnapshot", creds=self.creds, endpoint=self.endpoint,
736+ other_params={"VolumeId": volume_id})
737 d = query.submit()
738 return d.addCallback(self._parse_create_snapshot)
739
740@@ -596,17 +576,17 @@
741 def delete_snapshot(self, snapshot_id):
742 """Remove a previously created snapshot."""
743 query = self.query_factory(
744- "DeleteSnapshot", self.creds, self.endpoint,
745- {"SnapshotId": snapshot_id})
746+ action="DeleteSnapshot", creds=self.creds, endpoint=self.endpoint,
747+ other_params={"SnapshotId": snapshot_id})
748 d = query.submit()
749 return d.addCallback(self._parse_truth_return)
750
751 def attach_volume(self, volume_id, instance_id, device):
752 """Attach the given volume to the specified instance at C{device}."""
753 query = self.query_factory(
754- "AttachVolume", self.creds, self.endpoint,
755- {"VolumeId": volume_id, "InstanceId": instance_id,
756- "Device": device})
757+ action="AttachVolume", creds=self.creds, endpoint=self.endpoint,
758+ other_params={"VolumeId": volume_id, "InstanceId": instance_id,
759+ "Device": device})
760 d = query.submit()
761 return d.addCallback(self._parse_attach_volume)
762
763@@ -620,11 +600,12 @@
764
765 def describe_keypairs(self, *keypair_names):
766 """Returns information about key pairs available."""
767- keypair_set = {}
768- for pos, keypair_name in enumerate(keypair_names):
769- keypair_set["KeyPair.%d" % (pos + 1)] = keypair_name
770+ keypairs = {}
771+ for index, keypair_name in enumerate(keypair_names):
772+ keypairs["KeyPair.%d" % (index + 1)] = keypair_name
773 query = self.query_factory(
774- "DescribeKeyPairs", self.creds, self.endpoint, keypair_set)
775+ action="DescribeKeyPairs", creds=self.creds,
776+ endpoint=self.endpoint, other_params=keypairs)
777 d = query.submit()
778 return d.addCallback(self._parse_describe_keypairs)
779
780@@ -646,8 +627,8 @@
781 used to reference the created key pair when launching new instances.
782 """
783 query = self.query_factory(
784- "CreateKeyPair", self.creds, self.endpoint,
785- {"KeyName": keypair_name})
786+ action="CreateKeyPair", creds=self.creds, endpoint=self.endpoint,
787+ other_params={"KeyName": keypair_name})
788 d = query.submit()
789 return d.addCallback(self._parse_create_keypair)
790
791@@ -661,8 +642,8 @@
792 def delete_keypair(self, keypair_name):
793 """Delete a given keypair."""
794 query = self.query_factory(
795- "DeleteKeyPair", self.creds, self.endpoint,
796- {"KeyName": keypair_name})
797+ action="DeleteKeyPair", creds=self.creds, endpoint=self.endpoint,
798+ other_params={"KeyName": keypair_name})
799 d = query.submit()
800 return d.addCallback(self._parse_truth_return)
801
802@@ -673,8 +654,10 @@
803
804 @return: the IP address allocated.
805 """
806+ # XXX remove empty other_params
807 query = self.query_factory(
808- "AllocateAddress", self.creds, self.endpoint, {})
809+ action="AllocateAddress", creds=self.creds, endpoint=self.endpoint,
810+ other_params={})
811 d = query.submit()
812 return d.addCallback(self._parse_allocate_address)
813
814@@ -689,8 +672,8 @@
815 @return: C{True} if the operation succeeded.
816 """
817 query = self.query_factory(
818- "ReleaseAddress", self.creds, self.endpoint,
819- {"PublicIp": address})
820+ action="ReleaseAddress", creds=self.creds, endpoint=self.endpoint,
821+ other_params={"PublicIp": address})
822 d = query.submit()
823 return d.addCallback(self._parse_truth_return)
824
825@@ -702,8 +685,9 @@
826 @return: C{True} if the operation succeeded.
827 """
828 query = self.query_factory(
829- "AssociateAddress", self.creds, self.endpoint,
830- {"InstanceId": instance_id, "PublicIp": address})
831+ action="AssociateAddress", creds=self.creds,
832+ endpoint=self.endpoint,
833+ other_params={"InstanceId": instance_id, "PublicIp": address})
834 d = query.submit()
835 return d.addCallback(self._parse_truth_return)
836
837@@ -714,8 +698,8 @@
838 called several times without error.
839 """
840 query = self.query_factory(
841- "DisassociateAddress", self.creds, self.endpoint,
842- {"PublicIp": address})
843+ action="DisassociateAddress", creds=self.creds,
844+ endpoint=self.endpoint, other_params={"PublicIp": address})
845 d = query.submit()
846 return d.addCallback(self._parse_truth_return)
847
848@@ -732,7 +716,8 @@
849 for pos, address in enumerate(addresses):
850 address_set["PublicIp.%d" % (pos + 1)] = address
851 query = self.query_factory(
852- "DescribeAddresses", self.creds, self.endpoint, address_set)
853+ action="DescribeAddresses", creds=self.creds,
854+ endpoint=self.endpoint, other_params=address_set)
855 d = query.submit()
856 return d.addCallback(self._parse_describe_addresses)
857
858@@ -750,8 +735,9 @@
859 if names:
860 zone_names = dict([("ZoneName.%d" % (i+1), name)
861 for i, name in enumerate(names)])
862- query = self.query_factory("DescribeAvailabilityZones", self.creds,
863- self.endpoint, zone_names)
864+ query = self.query_factory(
865+ action="DescribeAvailabilityZones", creds=self.creds,
866+ endpoint=self.endpoint, other_params=zone_names)
867 d = query.submit()
868 return d.addCallback(self._parse_describe_availability_zones)
869
870@@ -830,5 +816,4 @@
871 url = "%s?%s" % (self.endpoint.get_uri(),
872 self.get_canonical_query_params())
873 d = self.get_page(url, method=self.endpoint.method)
874- d.addErrback(ec2_error_wrapper)
875- return d
876+ return d.addErrback(ec2_error_wrapper)
877
878=== modified file 'txaws/ec2/exception.py'
879--- txaws/ec2/exception.py 2009-11-28 01:10:23 +0000
880+++ txaws/ec2/exception.py 2009-11-28 01:10:23 +0000
881@@ -1,39 +1,14 @@
882 # Copyright (c) 2009 Canonical Ltd <duncan.mcgreggor@canonical.com>
883 # Licenced under the txaws licence available at /LICENSE in the txaws source.
884
885-from txaws.exception import AWSError, AWSResponseParseError
886-from txaws.util import XML
887+from txaws.exception import AWSError
888
889
890 class EC2Error(AWSError):
891 """
892 A error class providing custom methods on EC2 errors.
893 """
894- def __init__(self, xml_bytes, status=None, message=None, response=None):
895- super(AWSError, self).__init__(status, message, response)
896- if not xml_bytes:
897- raise ValueError("XML cannot be empty.")
898- self.original = xml_bytes
899- self.errors = []
900- self.request_id = ""
901- self.host_id = ""
902- self.parse()
903-
904- def __str__(self):
905- return self._get_error_message_string()
906-
907- def __repr__(self):
908- return "<%s object with %s>" % (
909- self.__class__.__name__, self._get_error_code_string())
910-
911- def _set_request_id(self, tree):
912- request_id_node = tree.find(".//RequestID")
913- if hasattr(request_id_node, "text"):
914- text = request_id_node.text
915- if text:
916- self.request_id = text
917-
918- def _set_400_errors(self, tree):
919+ def _set_400_error(self, tree):
920 errors_node = tree.find(".//Errors")
921 if errors_node:
922 for error in errors_node:
923@@ -41,84 +16,5 @@
924 if data:
925 self.errors.append(data)
926
927- def _set_host_id(self, tree):
928- host_id = tree.find(".//HostID")
929- if hasattr(host_id, "text"):
930- text = host_id.text
931- if text:
932- self.host_id = text
933-
934- def _set_500_error(self, tree):
935- self._set_request_id(tree)
936- self._set_host_id(tree)
937- data = self._node_to_dict(tree)
938- if data:
939- self.errors.append(data)
940-
941- def _get_error_code_string(self):
942- count = len(self.errors)
943- error_code = self.get_error_codes()
944- if count > 1:
945- return "Error count: %s" % error_code
946- else:
947- return "Error code: %s" % error_code
948-
949- def _get_error_message_string(self):
950- count = len(self.errors)
951- error_message = self.get_error_messages()
952- if count > 1:
953- return "%s." % error_message
954- else:
955- return "Error Message: %s" % error_message
956-
957- def _node_to_dict(self, node):
958- data = {}
959- for child in node:
960- if child.tag and child.text:
961- data[child.tag] = child.text
962- return data
963-
964- def _check_for_html(self, tree):
965- if tree.tag == "html":
966- message = "Could not parse HTML in the response."
967- raise AWSResponseParseError(message)
968-
969- def parse(self, xml_bytes=""):
970- if not xml_bytes:
971- xml_bytes = self.original
972- self.original = xml_bytes
973- tree = XML(xml_bytes.strip())
974- self._check_for_html(tree)
975- self._set_request_id(tree)
976- if self.status:
977- status = int(self.status)
978- else:
979- status = 400
980- if status >= 500:
981- self._set_500_error(tree)
982- else:
983- self._set_400_errors(tree)
984-
985- def has_error(self, errorString):
986- for error in self.errors:
987- if errorString in error.values():
988- return True
989- return False
990-
991- def get_error_codes(self):
992- count = len(self.errors)
993- if count > 1:
994- return count
995- elif count == 0:
996- return
997- else:
998- return self.errors[0]["Code"]
999-
1000- def get_error_messages(self):
1001- count = len(self.errors)
1002- if count > 1:
1003- return "Multiple EC2 Errors"
1004- elif count == 0:
1005- return "Empty error list"
1006- else:
1007- return self.errors[0]["Message"]
1008+
1009+
1010
1011=== modified file 'txaws/ec2/tests/test_client.py'
1012--- txaws/ec2/tests/test_client.py 2009-11-28 01:10:23 +0000
1013+++ txaws/ec2/tests/test_client.py 2009-11-28 01:10:23 +0000
1014@@ -76,13 +76,14 @@
1015
1016 class StubQuery(object):
1017
1018- def __init__(stub, action, creds, endpoint, other_params):
1019+ def __init__(stub, action="", creds=None, endpoint=None,
1020+ other_params={}):
1021 self.assertEqual(action, "DescribeAvailabilityZones")
1022 self.assertEqual(creds.access_key, "foo")
1023 self.assertEqual(creds.secret_key, "bar")
1024 self.assertEqual(
1025- {"ZoneName.1": "us-east-1a"},
1026- other_params)
1027+ other_params,
1028+ {"ZoneName.1": "us-east-1a"})
1029
1030 def submit(self):
1031 return succeed(
1032@@ -105,7 +106,8 @@
1033
1034 class StubQuery(object):
1035
1036- def __init__(stub, action, creds, endpoint, other_params):
1037+ def __init__(stub, action="", creds=None, endpoint=None,
1038+ other_params={}):
1039 self.assertEqual(action, "DescribeAvailabilityZones")
1040 self.assertEqual(creds.access_key, "foo")
1041 self.assertEqual(creds.secret_key, "bar")
1042@@ -199,11 +201,12 @@
1043
1044 class StubQuery(object):
1045
1046- def __init__(stub, action, creds, endpoint, params):
1047+ def __init__(stub, action="", creds=None, endpoint=None,
1048+ other_params={}):
1049 self.assertEqual(action, "DescribeInstances")
1050 self.assertEqual(creds.access_key, "foo")
1051 self.assertEqual(creds.secret_key, "bar")
1052- self.assertEquals(params, {})
1053+ self.assertEquals(other_params, {})
1054
1055 def submit(self):
1056 return succeed(payload.sample_describe_instances_result)
1057@@ -218,11 +221,12 @@
1058
1059 class StubQuery(object):
1060
1061- def __init__(stub, action, creds, endpoint, params):
1062+ def __init__(stub, action="", creds=None, endpoint=None,
1063+ other_params={}):
1064 self.assertEqual(action, "DescribeInstances")
1065 self.assertEqual(creds.access_key, "foo")
1066 self.assertEqual(creds.secret_key, "bar")
1067- self.assertEquals(params, {})
1068+ self.assertEquals(other_params, {})
1069
1070 def submit(self):
1071 return succeed(
1072@@ -238,12 +242,13 @@
1073
1074 class StubQuery(object):
1075
1076- def __init__(stub, action, creds, endpoint, params):
1077+ def __init__(stub, action="", creds=None, endpoint=None,
1078+ other_params={}):
1079 self.assertEqual(action, "DescribeInstances")
1080 self.assertEqual(creds.access_key, "foo")
1081 self.assertEqual(creds.secret_key, "bar")
1082 self.assertEquals(
1083- params,
1084+ other_params,
1085 {"InstanceId.1": "i-16546401",
1086 "InstanceId.2": "i-49873415"})
1087
1088@@ -261,13 +266,14 @@
1089
1090 class StubQuery(object):
1091
1092- def __init__(stub, action, creds, endpoint, other_params):
1093+ def __init__(stub, action="", creds=None, endpoint=None,
1094+ other_params={}):
1095 self.assertEqual(action, "TerminateInstances")
1096 self.assertEqual(creds.access_key, "foo")
1097 self.assertEqual(creds.secret_key, "bar")
1098 self.assertEqual(
1099- {"InstanceId.1": "i-1234", "InstanceId.2": "i-5678"},
1100- other_params)
1101+ other_params,
1102+ {"InstanceId.1": "i-1234", "InstanceId.2": "i-5678"})
1103
1104 def submit(self):
1105 return succeed(payload.sample_terminate_instances_result)
1106@@ -313,12 +319,13 @@
1107
1108 class StubQuery(object):
1109
1110- def __init__(stub, action, creds, endpoint, params):
1111+ def __init__(stub, action="", creds=None, endpoint=None,
1112+ other_params={}):
1113 self.assertEqual(action, "RunInstances")
1114 self.assertEqual(creds.access_key, "foo")
1115 self.assertEqual(creds.secret_key, "bar")
1116 self.assertEquals(
1117- params,
1118+ other_params,
1119 {"ImageId": "ami-1234", "MaxCount": "2", "MinCount": "1",
1120 "SecurityGroup.1": u"group1", "KeyName": u"default",
1121 "UserData": "Zm9v", "InstanceType": u"m1.small",
1122@@ -348,11 +355,12 @@
1123 """
1124 class StubQuery(object):
1125
1126- def __init__(stub, action, creds, endpoint, other_params=None):
1127+ def __init__(stub, action="", creds=None, endpoint=None,
1128+ other_params={}):
1129 self.assertEqual(action, "DescribeSecurityGroups")
1130 self.assertEqual(creds.access_key, "foo")
1131 self.assertEqual(creds.secret_key, "bar")
1132- self.assertEqual(other_params, None)
1133+ self.assertEqual(other_params, {})
1134
1135 def submit(self):
1136 return succeed(payload.sample_describe_security_groups_result)
1137@@ -382,11 +390,12 @@
1138 """
1139 class StubQuery(object):
1140
1141- def __init__(stub, action, creds, endpoint, other_params=None):
1142+ def __init__(stub, action="", creds=None, endpoint=None,
1143+ other_params={}):
1144 self.assertEqual(action, "DescribeSecurityGroups")
1145 self.assertEqual(creds.access_key, "foo")
1146 self.assertEqual(creds.secret_key, "bar")
1147- self.assertEqual(other_params, None)
1148+ self.assertEqual(other_params, {})
1149
1150 def submit(self):
1151 return succeed(
1152@@ -431,7 +440,8 @@
1153 """
1154 class StubQuery(object):
1155
1156- def __init__(stub, action, creds, endpoint, other_params=None):
1157+ def __init__(stub, action="", creds=None, endpoint=None,
1158+ other_params={}):
1159 self.assertEqual(action, "DescribeSecurityGroups")
1160 self.assertEqual(creds.access_key, "foo")
1161 self.assertEqual(creds.secret_key, "bar")
1162@@ -457,7 +467,8 @@
1163 """
1164 class StubQuery(object):
1165
1166- def __init__(stub, action, creds, endpoint, other_params=None):
1167+ def __init__(stub, action="", creds=None, endpoint=None,
1168+ other_params={}):
1169 self.assertEqual(action, "CreateSecurityGroup")
1170 self.assertEqual(creds.access_key, "foo")
1171 self.assertEqual(creds.secret_key, "bar")
1172@@ -484,7 +495,8 @@
1173 """
1174 class StubQuery(object):
1175
1176- def __init__(stub, action, creds, endpoint, other_params=None):
1177+ def __init__(stub, action="", creds=None, endpoint=None,
1178+ other_params={}):
1179 self.assertEqual(action, "DeleteSecurityGroup")
1180 self.assertEqual(creds.access_key, "foo")
1181 self.assertEqual(creds.secret_key, "bar")
1182@@ -508,7 +520,8 @@
1183 """
1184 class StubQuery(object):
1185
1186- def __init__(stub, action, creds, endpoint, other_params=None):
1187+ def __init__(stub, action="", creds=None, endpoint=None,
1188+ other_params={}):
1189 self.assertEqual(action, "DeleteSecurityGroup")
1190 self.assertEqual(creds.access_key, "foo")
1191 self.assertEqual(creds.secret_key, "bar")
1192@@ -542,7 +555,8 @@
1193 """
1194 class StubQuery(object):
1195
1196- def __init__(stub, action, creds, endpoint, other_params=None):
1197+ def __init__(stub, action="", creds=None, endpoint=None,
1198+ other_params={}):
1199 self.assertEqual(action, "AuthorizeSecurityGroupIngress")
1200 self.assertEqual(creds.access_key, "foo")
1201 self.assertEqual(creds.secret_key, "bar")
1202@@ -572,7 +586,8 @@
1203 """
1204 class StubQuery(object):
1205
1206- def __init__(stub, action, creds, endpoint, other_params=None):
1207+ def __init__(stub, action="", creds=None, endpoint=None,
1208+ other_params={}):
1209 self.assertEqual(action, "AuthorizeSecurityGroupIngress")
1210 self.assertEqual(creds.access_key, "foo")
1211 self.assertEqual(creds.secret_key, "bar")
1212@@ -622,7 +637,8 @@
1213 """
1214 class StubQuery(object):
1215
1216- def __init__(stub, action, creds, endpoint, other_params=None):
1217+ def __init__(stub, action="", creds=None, endpoint=None,
1218+ other_params={}):
1219 self.assertEqual(action, "AuthorizeSecurityGroupIngress")
1220 self.assertEqual(creds.access_key, "foo")
1221 self.assertEqual(creds.secret_key, "bar")
1222@@ -650,7 +666,8 @@
1223 """
1224 class StubQuery(object):
1225
1226- def __init__(stub, action, creds, endpoint, other_params=None):
1227+ def __init__(stub, action="", creds=None, endpoint=None,
1228+ other_params={}):
1229 self.assertEqual(action, "AuthorizeSecurityGroupIngress")
1230 self.assertEqual(creds.access_key, "foo")
1231 self.assertEqual(creds.secret_key, "bar")
1232@@ -680,7 +697,8 @@
1233 """
1234 class StubQuery(object):
1235
1236- def __init__(stub, action, creds, endpoint, other_params=None):
1237+ def __init__(stub, action="", creds=None, endpoint=None,
1238+ other_params={}):
1239 self.assertEqual(action, "RevokeSecurityGroupIngress")
1240 self.assertEqual(creds.access_key, "foo")
1241 self.assertEqual(creds.secret_key, "bar")
1242@@ -710,7 +728,8 @@
1243 """
1244 class StubQuery(object):
1245
1246- def __init__(stub, action, creds, endpoint, other_params=None):
1247+ def __init__(stub, action="", creds=None, endpoint=None,
1248+ other_params={}):
1249 self.assertEqual(action, "RevokeSecurityGroupIngress")
1250 self.assertEqual(creds.access_key, "foo")
1251 self.assertEqual(creds.secret_key, "bar")
1252@@ -760,7 +779,8 @@
1253 """
1254 class StubQuery(object):
1255
1256- def __init__(stub, action, creds, endpoint, other_params=None):
1257+ def __init__(stub, action="", creds=None, endpoint=None,
1258+ other_params={}):
1259 self.assertEqual(action, "RevokeSecurityGroupIngress")
1260 self.assertEqual(creds.access_key, "foo")
1261 self.assertEqual(creds.secret_key, "bar")
1262@@ -788,7 +808,8 @@
1263 """
1264 class StubQuery(object):
1265
1266- def __init__(stub, action, creds, endpoint, other_params=None):
1267+ def __init__(stub, action="", creds=None, endpoint=None,
1268+ other_params={}):
1269 self.assertEqual(action, "RevokeSecurityGroupIngress")
1270 self.assertEqual(creds.access_key, "foo")
1271 self.assertEqual(creds.secret_key, "bar")
1272@@ -838,11 +859,12 @@
1273
1274 class StubQuery(object):
1275
1276- def __init__(stub, action, creds, endpoint, params):
1277+ def __init__(stub, action="", creds=None, endpoint=None,
1278+ other_params={}):
1279 self.assertEqual(action, "DescribeVolumes")
1280 self.assertEqual(self.creds, creds)
1281 self.assertEqual(self.endpoint, endpoint)
1282- self.assertEquals(params, {})
1283+ self.assertEquals(other_params, {})
1284
1285 def submit(self):
1286 return succeed(payload.sample_describe_volumes_result)
1287@@ -857,12 +879,13 @@
1288
1289 class StubQuery(object):
1290
1291- def __init__(stub, action, creds, endpoint, params):
1292+ def __init__(stub, action="", creds=None, endpoint=None,
1293+ other_params={}):
1294 self.assertEqual(action, "DescribeVolumes")
1295 self.assertEqual(self.creds, creds)
1296 self.assertEqual(self.endpoint, endpoint)
1297 self.assertEquals(
1298- params,
1299+ other_params,
1300 {"VolumeId.1": "vol-4282672b"})
1301
1302 def submit(self):
1303@@ -888,11 +911,12 @@
1304
1305 class StubQuery(object):
1306
1307- def __init__(stub, action, creds, endpoint, params):
1308+ def __init__(stub, action="", creds=None, endpoint=None,
1309+ other_params={}):
1310 self.assertEqual(action, "DescribeSnapshots")
1311 self.assertEqual(self.creds, creds)
1312 self.assertEqual(self.endpoint, endpoint)
1313- self.assertEquals(params, {})
1314+ self.assertEquals(other_params, {})
1315
1316 def submit(self):
1317 return succeed(payload.sample_describe_snapshots_result)
1318@@ -907,12 +931,13 @@
1319
1320 class StubQuery(object):
1321
1322- def __init__(stub, action, creds, endpoint, params):
1323+ def __init__(stub, action="", creds=None, endpoint=None,
1324+ other_params={}):
1325 self.assertEqual(action, "DescribeSnapshots")
1326 self.assertEqual(self.creds, creds)
1327 self.assertEqual(self.endpoint, endpoint)
1328 self.assertEquals(
1329- params,
1330+ other_params,
1331 {"SnapshotId.1": "snap-78a54011"})
1332
1333 def submit(self):
1334@@ -928,13 +953,14 @@
1335
1336 class StubQuery(object):
1337
1338- def __init__(stub, action, creds, endpoint, params):
1339+ def __init__(stub, action="", creds=None, endpoint=None,
1340+ other_params={}):
1341 self.assertEqual(action, "CreateVolume")
1342 self.assertEqual(self.creds, creds)
1343 self.assertEqual(self.endpoint, endpoint)
1344 self.assertEqual(
1345- {"AvailabilityZone": "us-east-1", "Size": "800"},
1346- params)
1347+ other_params,
1348+ {"AvailabilityZone": "us-east-1", "Size": "800"})
1349
1350 def submit(self):
1351 return succeed(payload.sample_create_volume_result)
1352@@ -956,14 +982,15 @@
1353
1354 class StubQuery(object):
1355
1356- def __init__(stub, action, creds, endpoint, params):
1357+ def __init__(stub, action="", creds=None, endpoint=None,
1358+ other_params={}):
1359 self.assertEqual(action, "CreateVolume")
1360 self.assertEqual(self.creds, creds)
1361 self.assertEqual(self.endpoint, endpoint)
1362 self.assertEqual(
1363+ other_params,
1364 {"AvailabilityZone": "us-east-1",
1365- "SnapshotId": "snap-12345678"},
1366- params)
1367+ "SnapshotId": "snap-12345678"})
1368
1369 def submit(self):
1370 return succeed(payload.sample_create_volume_result)
1371@@ -999,13 +1026,14 @@
1372
1373 class StubQuery(object):
1374
1375- def __init__(stub, action, creds, endpoint, params):
1376+ def __init__(stub, action="", creds=None, endpoint=None,
1377+ other_params={}):
1378 self.assertEqual(action, "DeleteVolume")
1379 self.assertEqual(self.creds, creds)
1380 self.assertEqual(self.endpoint, endpoint)
1381 self.assertEqual(
1382- {"VolumeId": "vol-4282672b"},
1383- params)
1384+ other_params,
1385+ {"VolumeId": "vol-4282672b"})
1386
1387 def submit(self):
1388 return succeed(payload.sample_delete_volume_result)
1389@@ -1020,13 +1048,14 @@
1390
1391 class StubQuery(object):
1392
1393- def __init__(stub, action, creds, endpoint, params):
1394+ def __init__(stub, action="", creds=None, endpoint=None,
1395+ other_params={}):
1396 self.assertEqual(action, "CreateSnapshot")
1397 self.assertEqual(self.creds, creds)
1398 self.assertEqual(self.endpoint, endpoint)
1399 self.assertEqual(
1400- {"VolumeId": "vol-4d826724"},
1401- params)
1402+ other_params,
1403+ {"VolumeId": "vol-4d826724"})
1404
1405 def submit(self):
1406 return succeed(payload.sample_create_snapshot_result)
1407@@ -1049,13 +1078,14 @@
1408
1409 class StubQuery(object):
1410
1411- def __init__(stub, action, creds, endpoint, params):
1412+ def __init__(stub, action="", creds=None, endpoint=None,
1413+ other_params={}):
1414 self.assertEqual(action, "DeleteSnapshot")
1415 self.assertEqual(self.creds, creds)
1416 self.assertEqual(self.endpoint, endpoint)
1417 self.assertEqual(
1418- {"SnapshotId": "snap-78a54011"},
1419- params)
1420+ other_params,
1421+ {"SnapshotId": "snap-78a54011"})
1422
1423 def submit(self):
1424 return succeed(payload.sample_delete_snapshot_result)
1425@@ -1070,14 +1100,15 @@
1426
1427 class StubQuery(object):
1428
1429- def __init__(stub, action, creds, endpoint, params):
1430+ def __init__(stub, action="", creds=None, endpoint=None,
1431+ other_params={}):
1432 self.assertEqual(action, "AttachVolume")
1433 self.assertEqual(self.creds, creds)
1434 self.assertEqual(self.endpoint, endpoint)
1435 self.assertEqual(
1436+ other_params,
1437 {"VolumeId": "vol-4d826724", "InstanceId": "i-6058a509",
1438- "Device": "/dev/sdh"},
1439- params)
1440+ "Device": "/dev/sdh"})
1441
1442 def submit(self):
1443 return succeed(payload.sample_attach_volume_result)
1444@@ -1106,10 +1137,11 @@
1445
1446 class StubQuery(object):
1447
1448- def __init__(stub, action, creds, endpoint, params):
1449+ def __init__(stub, action="", creds=None, endpoint=None,
1450+ other_params={}):
1451 self.assertEqual(action, "DescribeKeyPairs")
1452 self.assertEqual("foo", creds)
1453- self.assertEquals(params, {})
1454+ self.assertEquals(other_params, {})
1455
1456 def submit(self):
1457 return succeed(payload.sample_single_describe_keypairs_result)
1458@@ -1134,10 +1166,12 @@
1459 "1f:51:ae:28:bf:89:e9:d8:1f:25:5d:37:2d:7d:b8:ca:9f:f5:f1:70")
1460
1461 class StubQuery(object):
1462- def __init__(stub, action, creds, endpoint, params):
1463+
1464+ def __init__(stub, action="", creds=None, endpoint=None,
1465+ other_params={}):
1466 self.assertEqual(action, "DescribeKeyPairs")
1467 self.assertEqual("foo", creds)
1468- self.assertEquals(params, {})
1469+ self.assertEquals(other_params, {})
1470
1471 def submit(self):
1472 return succeed(
1473@@ -1152,11 +1186,12 @@
1474
1475 class StubQuery(object):
1476
1477- def __init__(stub, action, creds, endpoint, params):
1478+ def __init__(stub, action="", creds=None, endpoint=None,
1479+ other_params={}):
1480 self.assertEqual(action, "DescribeKeyPairs")
1481 self.assertEqual("foo", creds)
1482 self.assertEquals(
1483- params,
1484+ other_params,
1485 {"KeyPair.1": "gsg-keypair"})
1486
1487 def submit(self):
1488@@ -1182,11 +1217,12 @@
1489
1490 class StubQuery(object):
1491
1492- def __init__(stub, action, creds, endpoint, params):
1493+ def __init__(stub, action="", creds=None, endpoint=None,
1494+ other_params={}):
1495 self.assertEqual(action, "CreateKeyPair")
1496 self.assertEqual("foo", creds)
1497 self.assertEquals(
1498- params,
1499+ other_params,
1500 {"KeyName": "example-key-name"})
1501
1502 def submit(self):
1503@@ -1201,12 +1237,13 @@
1504
1505 class StubQuery(object):
1506
1507- def __init__(stub, action, creds, endpoint, params):
1508+ def __init__(stub, action="", creds=None, endpoint=None,
1509+ other_params={}):
1510 self.assertEqual(action, "DeleteKeyPair")
1511 self.assertEqual("foo", creds)
1512 self.assertEqual("http:///", endpoint.get_uri())
1513 self.assertEquals(
1514- params,
1515+ other_params,
1516 {"KeyName": "example-key-name"})
1517
1518 def submit(self):
1519@@ -1221,12 +1258,13 @@
1520
1521 class StubQuery(object):
1522
1523- def __init__(stub, action, creds, endpoint, params):
1524+ def __init__(stub, action="", creds=None, endpoint=None,
1525+ other_params={}):
1526 self.assertEqual(action, "DeleteKeyPair")
1527 self.assertEqual("foo", creds)
1528 self.assertEqual("http:///", endpoint.get_uri())
1529 self.assertEquals(
1530- params,
1531+ other_params,
1532 {"KeyName": "example-key-name"})
1533
1534 def submit(self):
1535@@ -1241,12 +1279,13 @@
1536
1537 class StubQuery(object):
1538
1539- def __init__(stub, action, creds, endpoint, params):
1540+ def __init__(stub, action="", creds=None, endpoint=None,
1541+ other_params={}):
1542 self.assertEqual(action, "DeleteKeyPair")
1543 self.assertEqual("foo", creds)
1544 self.assertEqual("http:///", endpoint.get_uri())
1545 self.assertEquals(
1546- params,
1547+ other_params,
1548 {"KeyName": "example-key-name"})
1549
1550 def submit(self):
1551@@ -1373,12 +1412,12 @@
1552 self.assertTrue("Timestamp" in query.params)
1553 del query.params["Timestamp"]
1554 self.assertEqual(
1555+ query.params,
1556 {"AWSAccessKeyId": "foo",
1557 "Action": "DescribeInstances",
1558 "SignatureMethod": "HmacSHA256",
1559 "SignatureVersion": "2",
1560- "Version": "2008-12-01"},
1561- query.params)
1562+ "Version": "2008-12-01"})
1563
1564 def test_init_other_args_are_params(self):
1565 query = client.Query(
1566@@ -1386,14 +1425,14 @@
1567 endpoint=self.endpoint, other_params={"InstanceId.0": "12345"},
1568 time_tuple=(2007,11,12,13,14,15,0,0,0))
1569 self.assertEqual(
1570+ query.params,
1571 {"AWSAccessKeyId": "foo",
1572 "Action": "DescribeInstances",
1573 "InstanceId.0": "12345",
1574 "SignatureMethod": "HmacSHA256",
1575 "SignatureVersion": "2",
1576 "Timestamp": "2007-11-12T13:14:15Z",
1577- "Version": "2008-12-01"},
1578- query.params)
1579+ "Version": "2008-12-01"})
1580
1581 def test_sorted_params(self):
1582 query = client.Query(
1583@@ -1604,11 +1643,12 @@
1584
1585 class StubQuery(object):
1586
1587- def __init__(stub, action, creds, endpoint, params):
1588+ def __init__(stub, action="", creds=None, endpoint=None,
1589+ other_params={}):
1590 self.assertEqual(action, "DescribeAddresses")
1591 self.assertEqual(self.creds, creds)
1592 self.assertEqual(self.endpoint, endpoint)
1593- self.assertEquals(params, {})
1594+ self.assertEquals(other_params, {})
1595
1596 def submit(self):
1597 return succeed(payload.sample_describe_addresses_result)
1598@@ -1625,12 +1665,13 @@
1599
1600 class StubQuery(object):
1601
1602- def __init__(stub, action, creds, endpoint, params):
1603+ def __init__(stub, action="", creds=None, endpoint=None,
1604+ other_params={}):
1605 self.assertEqual(action, "DescribeAddresses")
1606 self.assertEqual(self.creds, creds)
1607 self.assertEqual(self.endpoint, endpoint)
1608 self.assertEquals(
1609- params,
1610+ other_params,
1611 {"PublicIp.1": "67.202.55.255"})
1612
1613 def submit(self):
1614@@ -1648,12 +1689,13 @@
1615
1616 class StubQuery(object):
1617
1618- def __init__(stub, action, creds, endpoint, params):
1619+ def __init__(stub, action="", creds=None, endpoint=None,
1620+ other_params={}):
1621 self.assertEqual(action, "AssociateAddress")
1622 self.assertEqual(self.creds, creds)
1623 self.assertEqual(self.endpoint, endpoint)
1624 self.assertEquals(
1625- params,
1626+ other_params,
1627 {"InstanceId": "i-28a64341", "PublicIp": "67.202.55.255"})
1628
1629 def submit(self):
1630@@ -1669,11 +1711,12 @@
1631
1632 class StubQuery(object):
1633
1634- def __init__(stub, action, creds, endpoint, params):
1635+ def __init__(stub, action="", creds=None, endpoint=None,
1636+ other_params={}):
1637 self.assertEqual(action, "AllocateAddress")
1638 self.assertEqual(self.creds, creds)
1639 self.assertEqual(self.endpoint, endpoint)
1640- self.assertEquals(params, {})
1641+ self.assertEquals(other_params, {})
1642
1643 def submit(self):
1644 return succeed(payload.sample_allocate_address_result)
1645@@ -1688,11 +1731,12 @@
1646
1647 class StubQuery(object):
1648
1649- def __init__(stub, action, creds, endpoint, params):
1650+ def __init__(stub, action="", creds=None, endpoint=None,
1651+ other_params={}):
1652 self.assertEqual(action, "ReleaseAddress")
1653 self.assertEqual(self.creds, creds)
1654 self.assertEqual(self.endpoint, endpoint)
1655- self.assertEquals(params, {"PublicIp": "67.202.55.255"})
1656+ self.assertEquals(other_params, {"PublicIp": "67.202.55.255"})
1657
1658 def submit(self):
1659 return succeed(payload.sample_release_address_result)
1660@@ -1707,11 +1751,12 @@
1661
1662 class StubQuery(object):
1663
1664- def __init__(stub, action, creds, endpoint, params):
1665+ def __init__(stub, action="", creds=None, endpoint=None,
1666+ other_params={}):
1667 self.assertEqual(action, "DisassociateAddress")
1668 self.assertEqual(self.creds, creds)
1669 self.assertEqual(self.endpoint, endpoint)
1670- self.assertEquals(params, {"PublicIp": "67.202.55.255"})
1671+ self.assertEquals(other_params, {"PublicIp": "67.202.55.255"})
1672
1673 def submit(self):
1674 return succeed(payload.sample_disassociate_address_result)
1675
1676=== modified file 'txaws/ec2/tests/test_exception.py'
1677--- txaws/ec2/tests/test_exception.py 2009-11-28 01:10:23 +0000
1678+++ txaws/ec2/tests/test_exception.py 2009-11-28 01:10:23 +0000
1679@@ -4,7 +4,6 @@
1680 from twisted.trial.unittest import TestCase
1681
1682 from txaws.ec2.exception import EC2Error
1683-from txaws.exception import AWSResponseParseError
1684 from txaws.testing import payload
1685 from txaws.util import XML
1686
1687@@ -14,77 +13,14 @@
1688
1689 class EC2ErrorTestCase(TestCase):
1690
1691- def test_creation(self):
1692- error = EC2Error("<dummy1 />", 400, "Not Found", "<dummy2 />")
1693- self.assertEquals(error.status, 400)
1694- self.assertEquals(error.response, "<dummy2 />")
1695- self.assertEquals(error.original, "<dummy1 />")
1696- self.assertEquals(error.errors, [])
1697- self.assertEquals(error.request_id, "")
1698-
1699- def test_node_to_dict(self):
1700- xml = "<parent><child1>text1</child1><child2>text2</child2></parent>"
1701- error = EC2Error("<dummy />")
1702- data = error._node_to_dict(XML(xml))
1703- self.assertEquals(data, {"child1": "text1", "child2": "text2"})
1704-
1705- def test_set_request_id(self):
1706- xml = "<a><b /><RequestID>%s</RequestID></a>" % REQUEST_ID
1707- error = EC2Error("<dummy />")
1708- error._set_request_id(XML(xml))
1709- self.assertEquals(error.request_id, REQUEST_ID)
1710-
1711- def test_set_400_errors(self):
1712+ def test_set_400_error(self):
1713 errorsXML = "<Error><Code>1</Code><Message>2</Message></Error>"
1714 xml = "<a><Errors>%s</Errors><b /></a>" % errorsXML
1715 error = EC2Error("<dummy />")
1716- error._set_400_errors(XML(xml))
1717+ error._set_400_error(XML(xml))
1718 self.assertEquals(error.errors[0]["Code"], "1")
1719 self.assertEquals(error.errors[0]["Message"], "2")
1720
1721- def test_set_host_id(self):
1722- host_id = "ASD@#FDG$E%FG"
1723- xml = "<a><b /><HostID>%s</HostID></a>" % host_id
1724- error = EC2Error("<dummy />")
1725- error._set_host_id(XML(xml))
1726- self.assertEquals(error.host_id, host_id)
1727-
1728- def test_set_500_error(self):
1729- xml = "<Error><Code>500</Code><Message>Oops</Message></Error>"
1730- error = EC2Error("<dummy />")
1731- error._set_500_error(XML(xml))
1732- self.assertEquals(error.errors[0]["Code"], "500")
1733- self.assertEquals(error.errors[0]["Message"], "Oops")
1734-
1735- def test_set_empty_errors(self):
1736- xml = "<a><Errors /><b /></a>"
1737- error = EC2Error("<dummy />")
1738- error._set_400_errors(XML(xml))
1739- self.assertEquals(error.errors, [])
1740-
1741- def test_set_empty_error(self):
1742- xml = "<a><Errors><Error /><Error /></Errors><b /></a>"
1743- error = EC2Error("<dummy />")
1744- error._set_400_errors(XML(xml))
1745- self.assertEquals(error.errors, [])
1746-
1747- def test_parse_without_xml(self):
1748- xml = "<dummy />"
1749- error = EC2Error(xml)
1750- error.parse()
1751- self.assertEquals(error.original, xml)
1752-
1753- def test_parse_with_xml(self):
1754- xml1 = "<dummy1 />"
1755- xml2 = "<dummy2 />"
1756- error = EC2Error(xml1)
1757- error.parse(xml2)
1758- self.assertEquals(error.original, xml2)
1759-
1760- def test_parse_html(self):
1761- xml = "<html><body>a page</body></html>"
1762- self.assertRaises(AWSResponseParseError, EC2Error, xml)
1763-
1764 def test_has_error(self):
1765 errorsXML = "<Error><Code>Code1</Code><Message>2</Message></Error>"
1766 xml = "<a><Errors>%s</Errors><b /></a>" % errorsXML
1767@@ -99,69 +35,6 @@
1768 error = EC2Error(payload.sample_ec2_error_messages)
1769 self.assertEquals(len(error.errors), 2)
1770
1771- def test_empty_xml(self):
1772- self.assertRaises(ValueError, EC2Error, "")
1773-
1774- def test_no_request_id(self):
1775- errors = "<Errors><Error><Code /><Message /></Error></Errors>"
1776- xml = "<Response>%s<RequestID /></Response>" % errors
1777- error = EC2Error(xml)
1778- self.assertEquals(error.request_id, "")
1779-
1780- def test_no_request_id_node(self):
1781- errors = "<Errors><Error><Code /><Message /></Error></Errors>"
1782- xml = "<Response>%s</Response>" % errors
1783- error = EC2Error(xml)
1784- self.assertEquals(error.request_id, "")
1785-
1786- def test_no_errors_node(self):
1787- xml = "<Response><RequestID /></Response>"
1788- error = EC2Error(xml)
1789- self.assertEquals(error.errors, [])
1790-
1791- def test_no_error_node(self):
1792- xml = "<Response><Errors /><RequestID /></Response>"
1793- error = EC2Error(xml)
1794- self.assertEquals(error.errors, [])
1795-
1796- def test_no_error_code_node(self):
1797- errors = "<Errors><Error><Message /></Error></Errors>"
1798- xml = "<Response>%s<RequestID /></Response>" % errors
1799- error = EC2Error(xml)
1800- self.assertEquals(error.errors, [])
1801-
1802- def test_no_error_message_node(self):
1803- errors = "<Errors><Error><Code /></Error></Errors>"
1804- xml = "<Response>%s<RequestID /></Response>" % errors
1805- error = EC2Error(xml)
1806- self.assertEquals(error.errors, [])
1807-
1808- def test_single_get_error_codes(self):
1809- error = EC2Error(payload.sample_ec2_error_message)
1810- self.assertEquals(error.get_error_codes(), "Error.Code")
1811-
1812- def test_multiple_get_error_codes(self):
1813- error = EC2Error(payload.sample_ec2_error_messages)
1814- self.assertEquals(error.get_error_codes(), 2)
1815-
1816- def test_zero_get_error_codes(self):
1817- xml = "<Response><RequestID /></Response>"
1818- error = EC2Error(xml)
1819- self.assertEquals(error.get_error_codes(), None)
1820-
1821- def test_single_get_error_messages(self):
1822- error = EC2Error(payload.sample_ec2_error_message)
1823- self.assertEquals(error.get_error_messages(), "Message for Error.Code")
1824-
1825- def test_multiple_get_error_messages(self):
1826- error = EC2Error(payload.sample_ec2_error_messages)
1827- self.assertEquals(error.get_error_messages(), "Multiple EC2 Errors")
1828-
1829- def test_zero_get_error_messages(self):
1830- xml = "<Response><RequestID /></Response>"
1831- error = EC2Error(xml)
1832- self.assertEquals(error.get_error_messages(), "Empty error list")
1833-
1834 def test_single_error_str(self):
1835 error = EC2Error(payload.sample_ec2_error_message)
1836 self.assertEquals(str(error), "Error Message: Message for Error.Code")
1837
1838=== modified file 'txaws/exception.py'
1839--- txaws/exception.py 2009-10-28 17:43:23 +0000
1840+++ txaws/exception.py 2009-11-28 01:10:23 +0000
1841@@ -3,11 +3,124 @@
1842
1843 from twisted.web.error import Error
1844
1845+from txaws.util import XML
1846+
1847
1848 class AWSError(Error):
1849 """
1850 A base class for txAWS errors.
1851 """
1852+ def __init__(self, xml_bytes, status=None, message=None, response=None):
1853+ super(AWSError, self).__init__(status, message, response)
1854+ if not xml_bytes:
1855+ raise ValueError("XML cannot be empty.")
1856+ self.original = xml_bytes
1857+ self.errors = []
1858+ self.request_id = ""
1859+ self.host_id = ""
1860+ self.parse()
1861+
1862+ def __str__(self):
1863+ return self._get_error_message_string()
1864+
1865+ def __repr__(self):
1866+ return "<%s object with %s>" % (
1867+ self.__class__.__name__, self._get_error_code_string())
1868+
1869+ def _set_request_id(self, tree):
1870+ request_id_node = tree.find(".//RequestID")
1871+ if hasattr(request_id_node, "text"):
1872+ text = request_id_node.text
1873+ if text:
1874+ self.request_id = text
1875+
1876+ def _set_host_id(self, tree):
1877+ host_id = tree.find(".//HostID")
1878+ if hasattr(host_id, "text"):
1879+ text = host_id.text
1880+ if text:
1881+ self.host_id = text
1882+
1883+ def _get_error_code_string(self):
1884+ count = len(self.errors)
1885+ error_code = self.get_error_codes()
1886+ if count > 1:
1887+ return "Error count: %s" % error_code
1888+ else:
1889+ return "Error code: %s" % error_code
1890+
1891+ def _get_error_message_string(self):
1892+ count = len(self.errors)
1893+ error_message = self.get_error_messages()
1894+ if count > 1:
1895+ return "%s." % error_message
1896+ else:
1897+ return "Error Message: %s" % error_message
1898+
1899+ def _node_to_dict(self, node):
1900+ data = {}
1901+ for child in node:
1902+ if child.tag and child.text:
1903+ data[child.tag] = child.text
1904+ return data
1905+
1906+ def _check_for_html(self, tree):
1907+ if tree.tag == "html":
1908+ message = "Could not parse HTML in the response."
1909+ raise AWSResponseParseError(message)
1910+
1911+ def _set_400_error(self, tree):
1912+ """
1913+ This method needs to be implemented by subclasses.
1914+ """
1915+
1916+ def _set_500_error(self, tree):
1917+ self._set_request_id(tree)
1918+ self._set_host_id(tree)
1919+ data = self._node_to_dict(tree)
1920+ if data:
1921+ self.errors.append(data)
1922+
1923+ def parse(self, xml_bytes=""):
1924+ if not xml_bytes:
1925+ xml_bytes = self.original
1926+ self.original = xml_bytes
1927+ tree = XML(xml_bytes.strip())
1928+ self._check_for_html(tree)
1929+ self._set_request_id(tree)
1930+ if self.status:
1931+ status = int(self.status)
1932+ else:
1933+ status = 400
1934+ if status >= 500:
1935+ self._set_500_error(tree)
1936+ else:
1937+ self._set_400_error(tree)
1938+
1939+ def has_error(self, errorString):
1940+ for error in self.errors:
1941+ if errorString in error.values():
1942+ return True
1943+ return False
1944+
1945+ def get_error_codes(self):
1946+ count = len(self.errors)
1947+ if count > 1:
1948+ return count
1949+ elif count == 0:
1950+ return
1951+ else:
1952+ return self.errors[0]["Code"]
1953+
1954+ def get_error_messages(self):
1955+ count = len(self.errors)
1956+ if count > 1:
1957+ return "Multiple EC2 Errors"
1958+ elif count == 0:
1959+ return "Empty error list"
1960+ else:
1961+ return self.errors[0]["Message"]
1962+
1963
1964
1965 class AWSResponseParseError(Exception):
1966
1967=== added file 'txaws/meta.py'
1968--- txaws/meta.py 1970-01-01 00:00:00 +0000
1969+++ txaws/meta.py 2009-11-28 01:10:23 +0000
1970@@ -0,0 +1,10 @@
1971+display_name = "txAWS"
1972+library_name = "txaws"
1973+author = "txAWS Deelopers"
1974+author_email = "txaws-dev@lists.launchpad.net"
1975+license = "MIT"
1976+url = "http://launchpad.net/txaws"
1977+description = """
1978+Twisted-based Asynchronous Libraries for Amazon Web Services
1979+"""
1980+
1981
1982=== modified file 'txaws/s3/client.py'
1983--- txaws/s3/client.py 2009-11-28 01:10:23 +0000
1984+++ txaws/s3/client.py 2009-11-28 01:10:23 +0000
1985@@ -17,12 +17,17 @@
1986
1987 from epsilon.extime import Time
1988
1989-from txaws.client.base import BaseClient, BaseQuery
1990+from txaws.client.base import BaseClient, BaseQuery, error_wrapper
1991 from txaws.s3 import model
1992+from txaws.s3.exception import S3Error
1993 from txaws.service import AWSServiceEndpoint, S3_ENDPOINT
1994 from txaws.util import XML, calculate_md5
1995
1996
1997+def s3_error_wrapper(error):
1998+ error_wrapper(error, S3Error)
1999+
2000+
2001 class URLContext(object):
2002 """
2003 The hosts and the paths that form an S3 endpoint change depending upon the
2004@@ -190,6 +195,10 @@
2005 self.endpoint.set_method(self.action)
2006
2007 def set_content_type(self):
2008+ """
2009+ Set the content type based on the file extension used in the object
2010+ name.
2011+ """
2012 if self.object_name and not self.content_type:
2013 # XXX nothing is currently done with the encoding... we may
2014 # need to in the future
2015@@ -197,6 +206,9 @@
2016 self.object_name, strict=False)
2017
2018 def get_headers(self):
2019+ """
2020+ Build the list of headers needed in order to perform S3 operations.
2021+ """
2022 headers = {"Content-Length": len(self.data),
2023 "Content-MD5": calculate_md5(self.data),
2024 "Date": self.date}
2025@@ -214,6 +226,9 @@
2026 return headers
2027
2028 def get_canonicalized_amz_headers(self, headers):
2029+ """
2030+ Get the headers defined by Amazon S3.
2031+ """
2032 headers = [
2033 (name.lower(), value) for name, value in headers.iteritems()
2034 if name.lower().startswith("x-amz-")]
2035@@ -224,6 +239,9 @@
2036 return "".join("%s:%s\n" % (name, value) for name, value in headers)
2037
2038 def get_canonicalized_resource(self):
2039+ """
2040+ Get an S3 resource path.
2041+ """
2042 resource = "/"
2043 if self.bucket:
2044 resource += self.bucket
2045@@ -232,7 +250,7 @@
2046 return resource
2047
2048 def sign(self, headers):
2049-
2050+ """Sign this query using its built in credentials."""
2051 text = (self.action + "\n" +
2052 headers.get("Content-MD5", "") + "\n" +
2053 headers.get("Content-Type", "") + "\n" +
2054@@ -242,14 +260,14 @@
2055 return self.creds.sign(text, hash_type="sha1")
2056
2057 def submit(self, url_context=None):
2058+ """Submit this query.
2059+
2060+ @return: A deferred from get_page
2061+ """
2062 if not url_context:
2063 url_context = URLContext(
2064 self.endpoint, self.bucket, self.object_name)
2065 d = self.get_page(
2066 url_context.get_url(), method=self.action, postdata=self.data,
2067 headers=self.get_headers())
2068- # XXX - we need an error wrapper like we have for ec2... but let's
2069- # wait until the new error-wrapper branch has landed, and possibly
2070- # generalize a base class for all clients.
2071- #d.addErrback(s3_error_wrapper)
2072- return d
2073+ return d.addErrback(s3_error_wrapper)
2074
2075=== added file 'txaws/s3/exception.py'
2076--- txaws/s3/exception.py 1970-01-01 00:00:00 +0000
2077+++ txaws/s3/exception.py 2009-11-28 01:10:23 +0000
2078@@ -0,0 +1,21 @@
2079+# Copyright (c) 2009 Canonical Ltd <duncan.mcgreggor@canonical.com>
2080+# Licenced under the txaws licence available at /LICENSE in the txaws source.
2081+
2082+from txaws.exception import AWSError
2083+
2084+
2085+class S3Error(AWSError):
2086+ """
2087+ A error class providing custom methods on S3 errors.
2088+ """
2089+ def _set_400_error(self, tree):
2090+ if tree.tag.lower() == "error":
2091+ data = self._node_to_dict(tree)
2092+ if data:
2093+ self.errors.append(data)
2094+
2095+ def get_error_code(self, *args, **kwargs):
2096+ return super(S3Error, self).get_error_codes(*args, **kwargs)
2097+
2098+ def get_error_message(self, *args, **kwargs):
2099+ return super(S3Error, self).get_error_messages(*args, **kwargs)
2100
2101=== added file 'txaws/s3/tests/test_exception.py'
2102--- txaws/s3/tests/test_exception.py 1970-01-01 00:00:00 +0000
2103+++ txaws/s3/tests/test_exception.py 2009-11-28 01:10:23 +0000
2104@@ -0,0 +1,62 @@
2105+# Copyright (c) 2009 Canonical Ltd <duncan.mcgreggor@canonical.com>
2106+# Licenced under the txaws licence available at /LICENSE in the txaws source.
2107+
2108+from twisted.trial.unittest import TestCase
2109+
2110+from txaws.s3.exception import S3Error
2111+from txaws.testing import payload
2112+from txaws.util import XML
2113+
2114+
2115+REQUEST_ID = "0ef9fc37-6230-4d81-b2e6-1b36277d4247"
2116+
2117+
2118+class S3ErrorTestCase(TestCase):
2119+
2120+ def test_set_400_error(self):
2121+ xml = "<Error><Code>1</Code><Message>2</Message></Error>"
2122+ error = S3Error("<dummy />")
2123+ error._set_400_error(XML(xml))
2124+ self.assertEquals(error.errors[0]["Code"], "1")
2125+ self.assertEquals(error.errors[0]["Message"], "2")
2126+
2127+ def test_get_error_code(self):
2128+ error = S3Error(payload.sample_s3_invalid_access_key_result)
2129+ self.assertEquals(error.get_error_code(), "InvalidAccessKeyId")
2130+
2131+ def test_get_error_message(self):
2132+ error = S3Error(payload.sample_s3_invalid_access_key_result)
2133+ self.assertEquals(
2134+ error.get_error_message(),
2135+ ("The AWS Access Key Id you provided does not exist in our "
2136+ "records."))
2137+
2138+ def test_error_count(self):
2139+ error = S3Error(payload.sample_s3_invalid_access_key_result)
2140+ self.assertEquals(len(error.errors), 1)
2141+
2142+ def test_error_repr(self):
2143+ error = S3Error(payload.sample_s3_invalid_access_key_result)
2144+ self.assertEquals(
2145+ repr(error),
2146+ "<S3Error object with Error code: InvalidAccessKeyId>")
2147+
2148+ def test_signature_mismatch_result(self):
2149+ error = S3Error(payload.sample_s3_signature_mismatch)
2150+ self.assertEquals(
2151+ error.get_error_messages(),
2152+ ("The request signature we calculated does not match the "
2153+ "signature you provided. Check your key and signing method."))
2154+
2155+ def test_invalid_access_key_result(self):
2156+ error = S3Error(payload.sample_s3_invalid_access_key_result)
2157+ self.assertEquals(
2158+ error.get_error_messages(),
2159+ ("The AWS Access Key Id you provided does not exist in our "
2160+ "records."))
2161+
2162+ def test_internal_error_result(self):
2163+ error = S3Error(payload.sample_server_internal_error_result)
2164+ self.assertEquals(
2165+ error.get_error_messages(),
2166+ "We encountered an internal error. Please try again.")
2167
2168=== added file 'txaws/script.py'
2169--- txaws/script.py 1970-01-01 00:00:00 +0000
2170+++ txaws/script.py 2009-11-28 01:10:23 +0000
2171@@ -0,0 +1,42 @@
2172+from optparse import OptionParser
2173+
2174+from txaws import meta
2175+from txaws import version
2176+
2177+
2178+# XXX Once we start adding script that require conflicting options, we'll need
2179+# multiple parsers and option dispatching...
2180+def parse_options(usage):
2181+ parser = OptionParser(usage, version="%s %s" % (
2182+ meta.display_name, version.txaws))
2183+ parser.add_option(
2184+ "-a", "--access-key", dest="access_key", help="access key ID")
2185+ parser.add_option(
2186+ "-s", "--secret-key", dest="secret_key", help="access secret key")
2187+ parser.add_option(
2188+ "-r", "--region", dest="region", help="US or EU (valid for AWS only)")
2189+ parser.add_option(
2190+ "-U", "--url", dest="url", help="service URL/endpoint")
2191+ parser.add_option(
2192+ "-b", "--bucket", dest="bucket", help="name of the bucket")
2193+ parser.add_option(
2194+ "-o", "--object-name", dest="object_name", help="name of the object")
2195+ parser.add_option(
2196+ "-d", "--object-data", dest="object_data",
2197+ help="content data of the object")
2198+ parser.add_option(
2199+ "--object-file", dest="object_filename",
2200+ help=("the path to the file that will be saved as an object; if "
2201+ "provided, the --object-name and --object-data options are "
2202+ "not necessary"))
2203+ parser.add_option(
2204+ "-c", "--content-type", dest="content_type",
2205+ help="content type of the object")
2206+ options, args = parser.parse_args()
2207+ if not (options.access_key and options.secret_key):
2208+ parser.error(
2209+ "both the access key ID and the secret key must be supplied")
2210+ region = options.region
2211+ if region and region.upper() not in ["US", "EU"]:
2212+ parser.error("region must be one of 'US' or 'EU'")
2213+ return (options, args)
2214
2215=== modified file 'txaws/service.py'
2216--- txaws/service.py 2009-11-28 01:10:23 +0000
2217+++ txaws/service.py 2009-11-28 01:10:23 +0000
2218@@ -77,19 +77,22 @@
2219 """
2220 # XXX update unit test to check for both ec2 and s3 endpoints
2221 def __init__(self, creds=None, access_key="", secret_key="",
2222- region=REGION_US, ec2_endpoint="", s3_endpoint=""):
2223+ region=REGION_US, uri="", ec2_uri="", s3_uri=""):
2224 if not creds:
2225 creds = AWSCredentials(access_key, secret_key)
2226 self.creds = creds
2227- if not ec2_endpoint and region == REGION_US:
2228- ec2_endpoint = EC2_ENDPOINT_US
2229- elif not ec2_endpoint and region == REGION_EU:
2230- ec2_endpoint = EC2_ENDPOINT_EU
2231- if not s3_endpoint:
2232- s3_endpoint = S3_ENDPOINT
2233+ # Provide backwards compatibility for the "uri" parameter.
2234+ if uri and not ec2_uri:
2235+ ec2_uri = uri
2236+ if not ec2_uri and region == REGION_US:
2237+ ec2_uri = EC2_ENDPOINT_US
2238+ elif not ec2_uri and region == REGION_EU:
2239+ ec2_uri = EC2_ENDPOINT_EU
2240+ if not s3_uri:
2241+ s3_uri = S3_ENDPOINT
2242 self._clients = {}
2243- self.ec2_endpoint = AWSServiceEndpoint(uri=ec2_endpoint)
2244- self.s3_endpoint = AWSServiceEndpoint(uri=s3_endpoint)
2245+ self.ec2_endpoint = AWSServiceEndpoint(uri=ec2_uri)
2246+ self.s3_endpoint = AWSServiceEndpoint(uri=s3_uri)
2247
2248 def get_client(self, cls, purge_cache=False, *args, **kwds):
2249 """
2250
2251=== modified file 'txaws/testing/payload.py'
2252--- txaws/testing/payload.py 2009-11-28 01:10:23 +0000
2253+++ txaws/testing/payload.py 2009-11-28 01:10:23 +0000
2254@@ -656,6 +656,31 @@
2255 """
2256
2257
2258+sample_restricted_resource_result = """\
2259+<?xml version="1.0"?>
2260+<Response>
2261+ <Errors>
2262+ <Error>
2263+ <Code>AuthFailure</Code>
2264+ <Message>Unauthorized attempt to access restricted resource</Message>
2265+ </Error>
2266+ </Errors>
2267+ <RequestID>a99e832e-e6e0-416a-9a35-81798ea521b4</RequestID>
2268+</Response>
2269+"""
2270+
2271+
2272+sample_server_internal_error_result = """\
2273+<?xml version="1.0" encoding="UTF-8"?>
2274+<Error>
2275+ <Code>InternalError</Code>
2276+ <Message>We encountered an internal error. Please try again.</Message>
2277+ <RequestID>A2A7E5395E27DFBB</RequestID>
2278+ <HostID>f691zulHNsUqonsZkjhILnvWwD3ZnmOM4ObM1wXTc6xuS3GzPmjArp8QC/sGsn6K</HostID>
2279+</Error>
2280+"""
2281+
2282+
2283 sample_list_buckets_result = """\
2284 <?xml version="1.0" encoding="UTF-8"?>
2285 <ListAllMyBucketsResult xmlns="http://s3.amazonaws.com/doc/%s/">
2286@@ -692,26 +717,13 @@
2287 """
2288
2289
2290-sample_server_internal_error_result = """\
2291+sample_s3_invalid_access_key_result = """\
2292 <?xml version="1.0" encoding="UTF-8"?>
2293 <Error>
2294- <Code>InternalError</Code>
2295- <Message>We encountered an internal error. Please try again.</Message>
2296- <RequestID>A2A7E5395E27DFBB</RequestID>
2297- <HostID>f691zulHNsUqonsZkjhILnvWwD3ZnmOM4ObM1wXTc6xuS3GzPmjArp8QC/sGsn6K</HostID>
2298+ <Code>InvalidAccessKeyId</Code>
2299+ <Message>The AWS Access Key Id you provided does not exist in our records.</Message>
2300+ <RequestId>0223AD81A94821CE</RequestId>
2301+ <HostId>HAw5g9P1VkN8ztgLKFTK20CY5LmCfTwXcSths1O7UQV6NuJx2P4tmFnpuOsziwOE</HostId>
2302+ <AWSAccessKeyId>SOMEKEYID</AWSAccessKeyId>
2303 </Error>
2304 """
2305-
2306-
2307-sample_restricted_resource_result = """\
2308-<?xml version="1.0"?>
2309-<Response>
2310- <Errors>
2311- <Error>
2312- <Code>AuthFailure</Code>
2313- <Message>Unauthorized attempt to access restricted resource</Message>
2314- </Error>
2315- </Errors>
2316- <RequestID>a99e832e-e6e0-416a-9a35-81798ea521b4</RequestID>
2317-</Response>
2318-"""
2319
2320=== added file 'txaws/tests/test_exception.py'
2321--- txaws/tests/test_exception.py 1970-01-01 00:00:00 +0000
2322+++ txaws/tests/test_exception.py 2009-11-28 01:10:23 +0000
2323@@ -0,0 +1,114 @@
2324+# Copyright (c) 2009 Canonical Ltd <duncan.mcgreggor@canonical.com>
2325+# Licenced under the txaws licence available at /LICENSE in the txaws source.
2326+
2327+from twisted.trial.unittest import TestCase
2328+
2329+from txaws.exception import AWSError
2330+from txaws.exception import AWSResponseParseError
2331+from txaws.util import XML
2332+
2333+
2334+REQUEST_ID = "0ef9fc37-6230-4d81-b2e6-1b36277d4247"
2335+
2336+
2337+class AWSErrorTestCase(TestCase):
2338+
2339+ def test_creation(self):
2340+ error = AWSError("<dummy1 />", 500, "Server Error", "<dummy2 />")
2341+ self.assertEquals(error.status, 500)
2342+ self.assertEquals(error.response, "<dummy2 />")
2343+ self.assertEquals(error.original, "<dummy1 />")
2344+ self.assertEquals(error.errors, [])
2345+ self.assertEquals(error.request_id, "")
2346+
2347+ def test_node_to_dict(self):
2348+ xml = "<parent><child1>text1</child1><child2>text2</child2></parent>"
2349+ error = AWSError("<dummy />")
2350+ data = error._node_to_dict(XML(xml))
2351+ self.assertEquals(data, {"child1": "text1", "child2": "text2"})
2352+
2353+ def test_set_request_id(self):
2354+ xml = "<a><b /><RequestID>%s</RequestID></a>" % REQUEST_ID
2355+ error = AWSError("<dummy />")
2356+ error._set_request_id(XML(xml))
2357+ self.assertEquals(error.request_id, REQUEST_ID)
2358+
2359+ def test_set_host_id(self):
2360+ host_id = "ASD@#FDG$E%FG"
2361+ xml = "<a><b /><HostID>%s</HostID></a>" % host_id
2362+ error = AWSError("<dummy />")
2363+ error._set_host_id(XML(xml))
2364+ self.assertEquals(error.host_id, host_id)
2365+
2366+ def test_set_empty_errors(self):
2367+ xml = "<a><Errors /><b /></a>"
2368+ error = AWSError("<dummy />")
2369+ error._set_500_error(XML(xml))
2370+ self.assertEquals(error.errors, [])
2371+
2372+ def test_set_empty_error(self):
2373+ xml = "<a><Errors><Error /><Error /></Errors><b /></a>"
2374+ error = AWSError("<dummy />")
2375+ error._set_500_error(XML(xml))
2376+ self.assertEquals(error.errors, [])
2377+
2378+ def test_parse_without_xml(self):
2379+ xml = "<dummy />"
2380+ error = AWSError(xml)
2381+ error.parse()
2382+ self.assertEquals(error.original, xml)
2383+
2384+ def test_parse_with_xml(self):
2385+ xml1 = "<dummy1 />"
2386+ xml2 = "<dummy2 />"
2387+ error = AWSError(xml1)
2388+ error.parse(xml2)
2389+ self.assertEquals(error.original, xml2)
2390+
2391+ def test_parse_html(self):
2392+ xml = "<html><body>a page</body></html>"
2393+ self.assertRaises(AWSResponseParseError, AWSError, xml)
2394+
2395+ def test_empty_xml(self):
2396+ self.assertRaises(ValueError, AWSError, "")
2397+
2398+ def test_no_request_id(self):
2399+ errors = "<Errors><Error><Code /><Message /></Error></Errors>"
2400+ xml = "<Response>%s<RequestID /></Response>" % errors
2401+ error = AWSError(xml)
2402+ self.assertEquals(error.request_id, "")
2403+
2404+ def test_no_request_id_node(self):
2405+ errors = "<Errors><Error><Code /><Message /></Error></Errors>"
2406+ xml = "<Response>%s</Response>" % errors
2407+ error = AWSError(xml)
2408+ self.assertEquals(error.request_id, "")
2409+
2410+ def test_no_errors_node(self):
2411+ xml = "<Response><RequestID /></Response>"
2412+ error = AWSError(xml)
2413+ self.assertEquals(error.errors, [])
2414+
2415+ def test_no_error_node(self):
2416+ xml = "<Response><Errors /><RequestID /></Response>"
2417+ error = AWSError(xml)
2418+ self.assertEquals(error.errors, [])
2419+
2420+ def test_no_error_code_node(self):
2421+ errors = "<Errors><Error><Message /></Error></Errors>"
2422+ xml = "<Response>%s<RequestID /></Response>" % errors
2423+ error = AWSError(xml)
2424+ self.assertEquals(error.errors, [])
2425+
2426+ def test_no_error_message_node(self):
2427+ errors = "<Errors><Error><Code /></Error></Errors>"
2428+ xml = "<Response>%s<RequestID /></Response>" % errors
2429+ error = AWSError(xml)
2430+ self.assertEquals(error.errors, [])
2431+
2432+ def test_set_500_error(self):
2433+ xml = "<Error><Code>500</Code><Message>Oops</Message></Error>"
2434+ error = AWSError("<dummy />")
2435+ error._set_500_error(XML(xml))
2436+ self.assertEquals(error.errors[0]["Code"], "500")
2437+ self.assertEquals(error.errors[0]["Message"], "Oops")
2438
2439=== modified file 'txaws/tests/test_service.py'
2440--- txaws/tests/test_service.py 2009-11-28 01:10:23 +0000
2441+++ txaws/tests/test_service.py 2009-11-28 01:10:23 +0000
2442@@ -97,12 +97,17 @@
2443
2444 def test_creation_with_uri(self):
2445 region = AWSServiceRegion(
2446- creds=self.creds, ec2_endpoint="http://foo/bar")
2447+ creds=self.creds, ec2_uri="http://foo/bar")
2448+ self.assertEquals(region.ec2_endpoint.get_uri(), "http://foo/bar")
2449+
2450+ def test_creation_with_uri_backwards_compatible(self):
2451+ region = AWSServiceRegion(
2452+ creds=self.creds, uri="http://foo/bar")
2453 self.assertEquals(region.ec2_endpoint.get_uri(), "http://foo/bar")
2454
2455 def test_creation_with_uri_and_region(self):
2456 region = AWSServiceRegion(
2457- creds=self.creds, region=REGION_EU, ec2_endpoint="http://foo/bar")
2458+ creds=self.creds, region=REGION_EU, ec2_uri="http://foo/bar")
2459 self.assertEquals(region.ec2_endpoint.get_uri(), "http://foo/bar")
2460
2461 def test_creation_with_region_override(self):
2462
2463=== modified file 'txaws/util.py'
2464--- txaws/util.py 2009-11-28 01:10:23 +0000
2465+++ txaws/util.py 2009-11-28 01:10:23 +0000
2466@@ -94,3 +94,33 @@
2467 if path == "":
2468 path = "/"
2469 return (str(scheme), str(host), port, str(path))
2470+
2471+
2472+def get_exitcode_reactor():
2473+ """
2474+ This is only neccesary until a fix like the one outlined here is
2475+ implemented for Twisted:
2476+ http://twistedmatrix.com/trac/ticket/2182
2477+ """
2478+ from twisted.internet.main import installReactor
2479+ from twisted.internet.selectreactor import SelectReactor
2480+
2481+ class ExitCodeReactor(SelectReactor):
2482+
2483+ def stop(self, exitStatus=0):
2484+ super(ExitCodeReactor, self).stop()
2485+ self.exitStatus = exitStatus
2486+
2487+ def run(self, *args, **kwargs):
2488+ super(ExitCodeReactor, self).run(*args, **kwargs)
2489+ return self.exitStatus
2490+
2491+ reactor = ExitCodeReactor()
2492+ installReactor(reactor)
2493+ return reactor
2494+
2495+
2496+try:
2497+ reactor = get_exitcode_reactor()
2498+except:
2499+ from twisted.internet import reactor

Subscribers

People subscribed via source and target branches

to all changes: