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
=== modified file 'LICENSE'
--- LICENSE 2008-07-06 22:51:54 +0000
+++ LICENSE 2009-11-28 01:10:25 +0000
@@ -1,3 +1,8 @@
1Copyright (C) 2008 Tristan Seligmann <mithrandi@mithrandi.net>
2Copyright (C) 2009 Robert Collins <robertc@robertcollins.net>
3Copyright (C) 2009 Canonical Ltd
4Copyright (C) 2009 Duncan McGreggor <oubiwann@adytum.us>
5
1Permission is hereby granted, free of charge, to any person obtaining6Permission is hereby granted, free of charge, to any person obtaining
2a copy of this software and associated documentation files (the7a copy of this software and associated documentation files (the
3"Software"), to deal in the Software without restriction, including8"Software"), to deal in the Software without restriction, including
@@ -11,8 +16,8 @@
1116
12THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,17THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
13EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF18EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
14MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND19MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
15NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE20IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
16LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION21CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
17OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION22TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
18WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.23SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
1924
=== modified file 'README'
--- README 2009-08-19 20:55:49 +0000
+++ README 2009-11-28 01:10:25 +0000
@@ -14,3 +14,9 @@
14* The txaws python package. (No installer at the moment)14* The txaws python package. (No installer at the moment)
1515
16* bin/aws-status, a GUI status program for aws resources.16* bin/aws-status, a GUI status program for aws resources.
17
18License
19-------
20
21txAWS is open source software, MIT License. See the LICENSE file for more
22details.
1723
=== added file 'bin/txaws-create-bucket'
--- bin/txaws-create-bucket 1970-01-01 00:00:00 +0000
+++ bin/txaws-create-bucket 2009-11-28 01:10:25 +0000
@@ -0,0 +1,42 @@
1#!/usr/bin/env python
2"""
3%prog [options]
4"""
5
6import sys
7
8from txaws.credentials import AWSCredentials
9from txaws.script import parse_options
10from txaws.service import AWSServiceRegion
11from txaws.util import reactor
12
13
14def printResults(results):
15 return 0
16
17
18def printError(error):
19 print error.value
20 return 1
21
22
23def finish(return_code):
24 reactor.stop(exitStatus=return_code)
25
26
27options, args = parse_options(__doc__.strip())
28if options.bucket is None:
29 print "Error Message: A bucket name is required."
30 sys.exit(1)
31creds = AWSCredentials(options.access_key, options.secret_key)
32region = AWSServiceRegion(
33 creds=creds, region=options.region, s3_endpoint=options.url)
34client = region.get_s3_client()
35
36d = client.create_bucket(options.bucket)
37d.addCallback(printResults)
38d.addErrback(printError)
39d.addCallback(finish)
40# We use a custom reactor so that we can return the exit status from
41# reactor.run().
42sys.exit(reactor.run())
043
=== added file 'bin/txaws-delete-bucket'
--- bin/txaws-delete-bucket 1970-01-01 00:00:00 +0000
+++ bin/txaws-delete-bucket 2009-11-28 01:10:25 +0000
@@ -0,0 +1,42 @@
1#!/usr/bin/env python
2"""
3%prog [options]
4"""
5
6import sys
7
8from txaws.credentials import AWSCredentials
9from txaws.script import parse_options
10from txaws.service import AWSServiceRegion
11from txaws.util import reactor
12
13
14def printResults(results):
15 return 0
16
17
18def printError(error):
19 print error.value
20 return 1
21
22
23def finish(return_code):
24 reactor.stop(exitStatus=return_code)
25
26
27options, args = parse_options(__doc__.strip())
28if options.bucket is None:
29 print "Error Message: A bucket name is required."
30 sys.exit(1)
31creds = AWSCredentials(options.access_key, options.secret_key)
32region = AWSServiceRegion(
33 creds=creds, region=options.region, s3_endpoint=options.url)
34client = region.get_s3_client()
35
36d = client.delete_bucket(options.bucket)
37d.addCallback(printResults)
38d.addErrback(printError)
39d.addCallback(finish)
40# We use a custom reactor so that we can return the exit status from
41# reactor.run().
42sys.exit(reactor.run())
043
=== added file 'bin/txaws-delete-object'
--- bin/txaws-delete-object 1970-01-01 00:00:00 +0000
+++ bin/txaws-delete-object 2009-11-28 01:10:25 +0000
@@ -0,0 +1,46 @@
1#!/usr/bin/env python
2"""
3%prog [options]
4"""
5
6import sys
7
8from txaws.credentials import AWSCredentials
9from txaws.script import parse_options
10from txaws.service import AWSServiceRegion
11from txaws.util import reactor
12
13
14def printResults(results):
15 print results
16 return 0
17
18
19def printError(error):
20 print error.value
21 return 1
22
23
24def finish(return_code):
25 reactor.stop(exitStatus=return_code)
26
27
28options, args = parse_options(__doc__.strip())
29if options.bucket is None:
30 print "Error Message: A bucket name is required."
31 sys.exit(1)
32if options.object_name is None:
33 print "Error Message: An object name is required."
34 sys.exit(1)
35creds = AWSCredentials(options.access_key, options.secret_key)
36region = AWSServiceRegion(
37 creds=creds, region=options.region, s3_endpoint=options.url)
38client = region.get_s3_client()
39
40d = client.delete_object(options.bucket, options.object_name)
41d.addCallback(printResults)
42d.addErrback(printError)
43d.addCallback(finish)
44# We use a custom reactor so that we can return the exit status from
45# reactor.run().
46sys.exit(reactor.run())
047
=== added file 'bin/txaws-get-object'
--- bin/txaws-get-object 1970-01-01 00:00:00 +0000
+++ bin/txaws-get-object 2009-11-28 01:10:25 +0000
@@ -0,0 +1,46 @@
1#!/usr/bin/env python
2"""
3%prog [options]
4"""
5
6import sys
7
8from txaws.credentials import AWSCredentials
9from txaws.script import parse_options
10from txaws.service import AWSServiceRegion
11from txaws.util import reactor
12
13
14def printResults(results):
15 print results
16 return 0
17
18
19def printError(error):
20 print error.value
21 return 1
22
23
24def finish(return_code):
25 reactor.stop(exitStatus=return_code)
26
27
28options, args = parse_options(__doc__.strip())
29if options.bucket is None:
30 print "Error Message: A bucket name is required."
31 sys.exit(1)
32if options.object_name is None:
33 print "Error Message: An object name is required."
34 sys.exit(1)
35creds = AWSCredentials(options.access_key, options.secret_key)
36region = AWSServiceRegion(
37 creds=creds, region=options.region, s3_endpoint=options.url)
38client = region.get_s3_client()
39
40d = client.get_object(options.bucket, options.object_name)
41d.addCallback(printResults)
42d.addErrback(printError)
43d.addCallback(finish)
44# We use a custom reactor so that we can return the exit status from
45# reactor.run().
46sys.exit(reactor.run())
047
=== added file 'bin/txaws-head-object'
--- bin/txaws-head-object 1970-01-01 00:00:00 +0000
+++ bin/txaws-head-object 2009-11-28 01:10:25 +0000
@@ -0,0 +1,47 @@
1#!/usr/bin/env python
2"""
3%prog [options]
4"""
5
6import sys
7from pprint import pprint
8
9from txaws.credentials import AWSCredentials
10from txaws.script import parse_options
11from txaws.service import AWSServiceRegion
12from txaws.util import reactor
13
14
15def printResults(results):
16 pprint(results)
17 return 0
18
19
20def printError(error):
21 print error.value
22 return 1
23
24
25def finish(return_code):
26 reactor.stop(exitStatus=return_code)
27
28
29options, args = parse_options(__doc__.strip())
30if options.bucket is None:
31 print "Error Message: A bucket name is required."
32 sys.exit(1)
33if options.object_name is None:
34 print "Error Message: An object name is required."
35 sys.exit(1)
36creds = AWSCredentials(options.access_key, options.secret_key)
37region = AWSServiceRegion(
38 creds=creds, region=options.region, s3_endpoint=options.url)
39client = region.get_s3_client()
40
41d = client.head_object(options.bucket, options.object_name)
42d.addCallback(printResults)
43d.addErrback(printError)
44d.addCallback(finish)
45# We use a custom reactor so that we can return the exit status from
46# reactor.run().
47sys.exit(reactor.run())
048
=== added file 'bin/txaws-list-buckets'
--- bin/txaws-list-buckets 1970-01-01 00:00:00 +0000
+++ bin/txaws-list-buckets 2009-11-28 01:10:25 +0000
@@ -0,0 +1,43 @@
1#!/usr/bin/env python
2"""
3%prog [options]
4"""
5
6import sys
7
8from txaws.credentials import AWSCredentials
9from txaws.script import parse_options
10from txaws.service import AWSServiceRegion
11from txaws.util import reactor
12
13
14def printResults(results):
15 print "\nBuckets:"
16 for bucket in results:
17 print "\t%s (created on %s)" % (bucket.name, bucket.creation_date)
18 print "Total buckets: %s\n" % len(list(results))
19 return 0
20
21
22def printError(error):
23 print error.value
24 return 1
25
26
27def finish(return_code):
28 reactor.stop(exitStatus=return_code)
29
30
31options, args = parse_options(__doc__.strip())
32creds = AWSCredentials(options.access_key, options.secret_key)
33region = AWSServiceRegion(
34 creds=creds, region=options.region, s3_endpoint=options.url)
35client = region.get_s3_client()
36
37d = client.list_buckets()
38d.addCallback(printResults)
39d.addErrback(printError)
40d.addCallback(finish)
41# We use a custom reactor so that we can return the exit status from
42# reactor.run().
43sys.exit(reactor.run())
044
=== added file 'bin/txaws-put-object'
--- bin/txaws-put-object 1970-01-01 00:00:00 +0000
+++ bin/txaws-put-object 2009-11-28 01:10:25 +0000
@@ -0,0 +1,56 @@
1#!/usr/bin/env python
2"""
3%prog [options]
4"""
5
6import os
7import sys
8
9from txaws.credentials import AWSCredentials
10from txaws.script import parse_options
11from txaws.service import AWSServiceRegion
12from txaws.util import reactor
13
14
15def printResults(results):
16 return 0
17
18
19def printError(error):
20 print error.value
21 return 1
22
23
24def finish(return_code):
25 reactor.stop(exitStatus=return_code)
26
27
28options, args = parse_options(__doc__.strip())
29if options.bucket is None:
30 print "Error Message: A bucket name is required."
31 sys.exit(1)
32filename = options.object_filename
33if filename:
34 options.object_name = os.path.basename(filename)
35 try:
36 options.object_data = open(filename).read()
37 except Exception, error:
38 print error
39 sys.exit(1)
40elif options.object_name is None:
41 print "Error Message: An object name is required."
42 sys.exit(1)
43creds = AWSCredentials(options.access_key, options.secret_key)
44region = AWSServiceRegion(
45 creds=creds, region=options.region, s3_endpoint=options.url)
46client = region.get_s3_client()
47
48d = client.put_object(
49 options.bucket, options.object_name, options.object_data,
50 options.content_type)
51d.addCallback(printResults)
52d.addErrback(printError)
53d.addCallback(finish)
54# We use a custom reactor so that we can return the exit status from
55# reactor.run().
56sys.exit(reactor.run())
057
=== modified file 'txaws/client/base.py'
--- txaws/client/base.py 2009-11-28 01:10:25 +0000
+++ txaws/client/base.py 2009-11-28 01:10:26 +0000
@@ -1,11 +1,50 @@
1from xml.parsers.expat import ExpatError
2
1from twisted.internet import reactor, ssl3from twisted.internet import reactor, ssl
4from twisted.web import http
2from twisted.web.client import HTTPClientFactory5from twisted.web.client import HTTPClientFactory
6from twisted.web.error import Error as TwistedWebError
37
4from txaws.util import parse8from txaws.util import parse
5from txaws.credentials import AWSCredentials9from txaws.credentials import AWSCredentials
10from txaws.exception import AWSResponseParseError
6from txaws.service import AWSServiceEndpoint11from txaws.service import AWSServiceEndpoint
712
813
14def error_wrapper(error, errorClass):
15 """
16 We want to see all error messages from cloud services. Amazon's EC2 says
17 that their errors are accompanied either by a 400-series or 500-series HTTP
18 response code. As such, the first thing we want to do is check to see if
19 the error is in that range. If it is, we then need to see if the error
20 message is an EC2 one.
21
22 In the event that an error is not a Twisted web error nor an EC2 one, the
23 original exception is raised.
24 """
25 http_status = 0
26 if error.check(TwistedWebError):
27 xml_payload = error.value.response
28 if error.value.status:
29 http_status = int(error.value.status)
30 else:
31 error.raiseException()
32 if http_status >= 400:
33 if not xml_payload:
34 error.raiseException()
35 try:
36 fallback_error = errorClass(
37 xml_payload, error.value.status, error.value.message,
38 error.value.response)
39 except (ExpatError, AWSResponseParseError):
40 error_message = http.RESPONSES.get(http_status)
41 fallback_error = TwistedWebError(
42 http_status, error_message, error.value.response)
43 raise fallback_error
44 else:
45 error.raiseException()
46
47
9class BaseClient(object):48class BaseClient(object):
10 """Create an AWS client.49 """Create an AWS client.
1150
1251
=== modified file 'txaws/ec2/client.py'
--- txaws/ec2/client.py 2009-11-28 01:10:25 +0000
+++ txaws/ec2/client.py 2009-11-28 01:10:26 +0000
@@ -8,16 +8,11 @@
8from datetime import datetime8from datetime import datetime
9from urllib import quote9from urllib import quote
10from base64 import b64encode10from base64 import b64encode
11from xml.parsers.expat import ExpatError
12
13from twisted.web import http
14from twisted.web.error import Error as TwistedWebError
1511
16from txaws import version12from txaws import version
17from txaws.client.base import BaseClient, BaseQuery13from txaws.client.base import BaseClient, BaseQuery, error_wrapper
18from txaws.ec2 import model14from txaws.ec2 import model
19from txaws.ec2.exception import EC2Error15from txaws.ec2.exception import EC2Error
20from txaws.exception import AWSResponseParseError
21from txaws.util import iso8601time, XML16from txaws.util import iso8601time, XML
2217
2318
@@ -25,34 +20,7 @@
2520
2621
27def ec2_error_wrapper(error):22def ec2_error_wrapper(error):
28 """23 error_wrapper(error, EC2Error)
29 We want to see all error messages from cloud services. Amazon's EC2 says
30 that their errors are accompanied either by a 400-series or 500-series HTTP
31 response code. As such, the first thing we want to do is check to see if
32 the error is in that range. If it is, we then need to see if the error
33 message is an EC2 one.
34
35 In the event that an error is not a Twisted web error nor an EC2 one, the
36 original exception is raised.
37 """
38 http_status = 0
39 if error.check(TwistedWebError):
40 xml_payload = error.value.response
41 if error.value.status:
42 http_status = int(error.value.status)
43 else:
44 error.raiseException()
45 if http_status >= 400:
46 try:
47 fallback_error = EC2Error(xml_payload, error.value.status,
48 error.value.message, error.value.response)
49 except (ExpatError, AWSResponseParseError):
50 error_message = http.RESPONSES.get(http_status)
51 fallback_error = TwistedWebError(http_status, error_message,
52 error.value.response)
53 raise fallback_error
54 else:
55 error.raiseException()
5624
5725
58class EC2Client(BaseClient):26class EC2Client(BaseClient):
@@ -848,5 +816,4 @@
848 url = "%s?%s" % (self.endpoint.get_uri(),816 url = "%s?%s" % (self.endpoint.get_uri(),
849 self.get_canonical_query_params())817 self.get_canonical_query_params())
850 d = self.get_page(url, method=self.endpoint.method)818 d = self.get_page(url, method=self.endpoint.method)
851 d.addErrback(ec2_error_wrapper)819 return d.addErrback(ec2_error_wrapper)
852 return d
853820
=== modified file 'txaws/ec2/exception.py'
--- txaws/ec2/exception.py 2009-11-28 01:10:25 +0000
+++ txaws/ec2/exception.py 2009-11-28 01:10:26 +0000
@@ -1,39 +1,14 @@
1# Copyright (c) 2009 Canonical Ltd <duncan.mcgreggor@canonical.com>1# Copyright (c) 2009 Canonical Ltd <duncan.mcgreggor@canonical.com>
2# Licenced under the txaws licence available at /LICENSE in the txaws source.2# Licenced under the txaws licence available at /LICENSE in the txaws source.
33
4from txaws.exception import AWSError, AWSResponseParseError4from txaws.exception import AWSError
5from txaws.util import XML
65
76
8class EC2Error(AWSError):7class EC2Error(AWSError):
9 """8 """
10 A error class providing custom methods on EC2 errors.9 A error class providing custom methods on EC2 errors.
11 """10 """
12 def __init__(self, xml_bytes, status=None, message=None, response=None):11 def _set_400_error(self, tree):
13 super(AWSError, self).__init__(status, message, response)
14 if not xml_bytes:
15 raise ValueError("XML cannot be empty.")
16 self.original = xml_bytes
17 self.errors = []
18 self.request_id = ""
19 self.host_id = ""
20 self.parse()
21
22 def __str__(self):
23 return self._get_error_message_string()
24
25 def __repr__(self):
26 return "<%s object with %s>" % (
27 self.__class__.__name__, self._get_error_code_string())
28
29 def _set_request_id(self, tree):
30 request_id_node = tree.find(".//RequestID")
31 if hasattr(request_id_node, "text"):
32 text = request_id_node.text
33 if text:
34 self.request_id = text
35
36 def _set_400_errors(self, tree):
37 errors_node = tree.find(".//Errors")12 errors_node = tree.find(".//Errors")
38 if errors_node:13 if errors_node:
39 for error in errors_node:14 for error in errors_node:
@@ -41,84 +16,5 @@
41 if data:16 if data:
42 self.errors.append(data)17 self.errors.append(data)
4318
44 def _set_host_id(self, tree):19
45 host_id = tree.find(".//HostID")20
46 if hasattr(host_id, "text"):
47 text = host_id.text
48 if text:
49 self.host_id = text
50
51 def _set_500_error(self, tree):
52 self._set_request_id(tree)
53 self._set_host_id(tree)
54 data = self._node_to_dict(tree)
55 if data:
56 self.errors.append(data)
57
58 def _get_error_code_string(self):
59 count = len(self.errors)
60 error_code = self.get_error_codes()
61 if count > 1:
62 return "Error count: %s" % error_code
63 else:
64 return "Error code: %s" % error_code
65
66 def _get_error_message_string(self):
67 count = len(self.errors)
68 error_message = self.get_error_messages()
69 if count > 1:
70 return "%s." % error_message
71 else:
72 return "Error Message: %s" % error_message
73
74 def _node_to_dict(self, node):
75 data = {}
76 for child in node:
77 if child.tag and child.text:
78 data[child.tag] = child.text
79 return data
80
81 def _check_for_html(self, tree):
82 if tree.tag == "html":
83 message = "Could not parse HTML in the response."
84 raise AWSResponseParseError(message)
85
86 def parse(self, xml_bytes=""):
87 if not xml_bytes:
88 xml_bytes = self.original
89 self.original = xml_bytes
90 tree = XML(xml_bytes.strip())
91 self._check_for_html(tree)
92 self._set_request_id(tree)
93 if self.status:
94 status = int(self.status)
95 else:
96 status = 400
97 if status >= 500:
98 self._set_500_error(tree)
99 else:
100 self._set_400_errors(tree)
101
102 def has_error(self, errorString):
103 for error in self.errors:
104 if errorString in error.values():
105 return True
106 return False
107
108 def get_error_codes(self):
109 count = len(self.errors)
110 if count > 1:
111 return count
112 elif count == 0:
113 return
114 else:
115 return self.errors[0]["Code"]
116
117 def get_error_messages(self):
118 count = len(self.errors)
119 if count > 1:
120 return "Multiple EC2 Errors"
121 elif count == 0:
122 return "Empty error list"
123 else:
124 return self.errors[0]["Message"]
12521
=== modified file 'txaws/ec2/tests/test_exception.py'
--- txaws/ec2/tests/test_exception.py 2009-11-28 01:10:25 +0000
+++ txaws/ec2/tests/test_exception.py 2009-11-28 01:10:26 +0000
@@ -4,7 +4,6 @@
4from twisted.trial.unittest import TestCase4from twisted.trial.unittest import TestCase
55
6from txaws.ec2.exception import EC2Error6from txaws.ec2.exception import EC2Error
7from txaws.exception import AWSResponseParseError
8from txaws.testing import payload7from txaws.testing import payload
9from txaws.util import XML8from txaws.util import XML
109
@@ -14,77 +13,14 @@
1413
15class EC2ErrorTestCase(TestCase):14class EC2ErrorTestCase(TestCase):
1615
17 def test_creation(self):16 def test_set_400_error(self):
18 error = EC2Error("<dummy1 />", 400, "Not Found", "<dummy2 />")
19 self.assertEquals(error.status, 400)
20 self.assertEquals(error.response, "<dummy2 />")
21 self.assertEquals(error.original, "<dummy1 />")
22 self.assertEquals(error.errors, [])
23 self.assertEquals(error.request_id, "")
24
25 def test_node_to_dict(self):
26 xml = "<parent><child1>text1</child1><child2>text2</child2></parent>"
27 error = EC2Error("<dummy />")
28 data = error._node_to_dict(XML(xml))
29 self.assertEquals(data, {"child1": "text1", "child2": "text2"})
30
31 def test_set_request_id(self):
32 xml = "<a><b /><RequestID>%s</RequestID></a>" % REQUEST_ID
33 error = EC2Error("<dummy />")
34 error._set_request_id(XML(xml))
35 self.assertEquals(error.request_id, REQUEST_ID)
36
37 def test_set_400_errors(self):
38 errorsXML = "<Error><Code>1</Code><Message>2</Message></Error>"17 errorsXML = "<Error><Code>1</Code><Message>2</Message></Error>"
39 xml = "<a><Errors>%s</Errors><b /></a>" % errorsXML18 xml = "<a><Errors>%s</Errors><b /></a>" % errorsXML
40 error = EC2Error("<dummy />")19 error = EC2Error("<dummy />")
41 error._set_400_errors(XML(xml))20 error._set_400_error(XML(xml))
42 self.assertEquals(error.errors[0]["Code"], "1")21 self.assertEquals(error.errors[0]["Code"], "1")
43 self.assertEquals(error.errors[0]["Message"], "2")22 self.assertEquals(error.errors[0]["Message"], "2")
4423
45 def test_set_host_id(self):
46 host_id = "ASD@#FDG$E%FG"
47 xml = "<a><b /><HostID>%s</HostID></a>" % host_id
48 error = EC2Error("<dummy />")
49 error._set_host_id(XML(xml))
50 self.assertEquals(error.host_id, host_id)
51
52 def test_set_500_error(self):
53 xml = "<Error><Code>500</Code><Message>Oops</Message></Error>"
54 error = EC2Error("<dummy />")
55 error._set_500_error(XML(xml))
56 self.assertEquals(error.errors[0]["Code"], "500")
57 self.assertEquals(error.errors[0]["Message"], "Oops")
58
59 def test_set_empty_errors(self):
60 xml = "<a><Errors /><b /></a>"
61 error = EC2Error("<dummy />")
62 error._set_400_errors(XML(xml))
63 self.assertEquals(error.errors, [])
64
65 def test_set_empty_error(self):
66 xml = "<a><Errors><Error /><Error /></Errors><b /></a>"
67 error = EC2Error("<dummy />")
68 error._set_400_errors(XML(xml))
69 self.assertEquals(error.errors, [])
70
71 def test_parse_without_xml(self):
72 xml = "<dummy />"
73 error = EC2Error(xml)
74 error.parse()
75 self.assertEquals(error.original, xml)
76
77 def test_parse_with_xml(self):
78 xml1 = "<dummy1 />"
79 xml2 = "<dummy2 />"
80 error = EC2Error(xml1)
81 error.parse(xml2)
82 self.assertEquals(error.original, xml2)
83
84 def test_parse_html(self):
85 xml = "<html><body>a page</body></html>"
86 self.assertRaises(AWSResponseParseError, EC2Error, xml)
87
88 def test_has_error(self):24 def test_has_error(self):
89 errorsXML = "<Error><Code>Code1</Code><Message>2</Message></Error>"25 errorsXML = "<Error><Code>Code1</Code><Message>2</Message></Error>"
90 xml = "<a><Errors>%s</Errors><b /></a>" % errorsXML26 xml = "<a><Errors>%s</Errors><b /></a>" % errorsXML
@@ -99,69 +35,6 @@
99 error = EC2Error(payload.sample_ec2_error_messages)35 error = EC2Error(payload.sample_ec2_error_messages)
100 self.assertEquals(len(error.errors), 2)36 self.assertEquals(len(error.errors), 2)
10137
102 def test_empty_xml(self):
103 self.assertRaises(ValueError, EC2Error, "")
104
105 def test_no_request_id(self):
106 errors = "<Errors><Error><Code /><Message /></Error></Errors>"
107 xml = "<Response>%s<RequestID /></Response>" % errors
108 error = EC2Error(xml)
109 self.assertEquals(error.request_id, "")
110
111 def test_no_request_id_node(self):
112 errors = "<Errors><Error><Code /><Message /></Error></Errors>"
113 xml = "<Response>%s</Response>" % errors
114 error = EC2Error(xml)
115 self.assertEquals(error.request_id, "")
116
117 def test_no_errors_node(self):
118 xml = "<Response><RequestID /></Response>"
119 error = EC2Error(xml)
120 self.assertEquals(error.errors, [])
121
122 def test_no_error_node(self):
123 xml = "<Response><Errors /><RequestID /></Response>"
124 error = EC2Error(xml)
125 self.assertEquals(error.errors, [])
126
127 def test_no_error_code_node(self):
128 errors = "<Errors><Error><Message /></Error></Errors>"
129 xml = "<Response>%s<RequestID /></Response>" % errors
130 error = EC2Error(xml)
131 self.assertEquals(error.errors, [])
132
133 def test_no_error_message_node(self):
134 errors = "<Errors><Error><Code /></Error></Errors>"
135 xml = "<Response>%s<RequestID /></Response>" % errors
136 error = EC2Error(xml)
137 self.assertEquals(error.errors, [])
138
139 def test_single_get_error_codes(self):
140 error = EC2Error(payload.sample_ec2_error_message)
141 self.assertEquals(error.get_error_codes(), "Error.Code")
142
143 def test_multiple_get_error_codes(self):
144 error = EC2Error(payload.sample_ec2_error_messages)
145 self.assertEquals(error.get_error_codes(), 2)
146
147 def test_zero_get_error_codes(self):
148 xml = "<Response><RequestID /></Response>"
149 error = EC2Error(xml)
150 self.assertEquals(error.get_error_codes(), None)
151
152 def test_single_get_error_messages(self):
153 error = EC2Error(payload.sample_ec2_error_message)
154 self.assertEquals(error.get_error_messages(), "Message for Error.Code")
155
156 def test_multiple_get_error_messages(self):
157 error = EC2Error(payload.sample_ec2_error_messages)
158 self.assertEquals(error.get_error_messages(), "Multiple EC2 Errors")
159
160 def test_zero_get_error_messages(self):
161 xml = "<Response><RequestID /></Response>"
162 error = EC2Error(xml)
163 self.assertEquals(error.get_error_messages(), "Empty error list")
164
165 def test_single_error_str(self):38 def test_single_error_str(self):
166 error = EC2Error(payload.sample_ec2_error_message)39 error = EC2Error(payload.sample_ec2_error_message)
167 self.assertEquals(str(error), "Error Message: Message for Error.Code")40 self.assertEquals(str(error), "Error Message: Message for Error.Code")
16841
=== modified file 'txaws/exception.py'
--- txaws/exception.py 2009-10-28 17:43:23 +0000
+++ txaws/exception.py 2009-11-28 01:10:25 +0000
@@ -3,11 +3,124 @@
33
4from twisted.web.error import Error4from twisted.web.error import Error
55
6from txaws.util import XML
7
68
7class AWSError(Error):9class AWSError(Error):
8 """10 """
9 A base class for txAWS errors.11 A base class for txAWS errors.
10 """12 """
13 def __init__(self, xml_bytes, status=None, message=None, response=None):
14 super(AWSError, self).__init__(status, message, response)
15 if not xml_bytes:
16 raise ValueError("XML cannot be empty.")
17 self.original = xml_bytes
18 self.errors = []
19 self.request_id = ""
20 self.host_id = ""
21 self.parse()
22
23 def __str__(self):
24 return self._get_error_message_string()
25
26 def __repr__(self):
27 return "<%s object with %s>" % (
28 self.__class__.__name__, self._get_error_code_string())
29
30 def _set_request_id(self, tree):
31 request_id_node = tree.find(".//RequestID")
32 if hasattr(request_id_node, "text"):
33 text = request_id_node.text
34 if text:
35 self.request_id = text
36
37 def _set_host_id(self, tree):
38 host_id = tree.find(".//HostID")
39 if hasattr(host_id, "text"):
40 text = host_id.text
41 if text:
42 self.host_id = text
43
44 def _get_error_code_string(self):
45 count = len(self.errors)
46 error_code = self.get_error_codes()
47 if count > 1:
48 return "Error count: %s" % error_code
49 else:
50 return "Error code: %s" % error_code
51
52 def _get_error_message_string(self):
53 count = len(self.errors)
54 error_message = self.get_error_messages()
55 if count > 1:
56 return "%s." % error_message
57 else:
58 return "Error Message: %s" % error_message
59
60 def _node_to_dict(self, node):
61 data = {}
62 for child in node:
63 if child.tag and child.text:
64 data[child.tag] = child.text
65 return data
66
67 def _check_for_html(self, tree):
68 if tree.tag == "html":
69 message = "Could not parse HTML in the response."
70 raise AWSResponseParseError(message)
71
72 def _set_400_error(self, tree):
73 """
74 This method needs to be implemented by subclasses.
75 """
76
77 def _set_500_error(self, tree):
78 self._set_request_id(tree)
79 self._set_host_id(tree)
80 data = self._node_to_dict(tree)
81 if data:
82 self.errors.append(data)
83
84 def parse(self, xml_bytes=""):
85 if not xml_bytes:
86 xml_bytes = self.original
87 self.original = xml_bytes
88 tree = XML(xml_bytes.strip())
89 self._check_for_html(tree)
90 self._set_request_id(tree)
91 if self.status:
92 status = int(self.status)
93 else:
94 status = 400
95 if status >= 500:
96 self._set_500_error(tree)
97 else:
98 self._set_400_error(tree)
99
100 def has_error(self, errorString):
101 for error in self.errors:
102 if errorString in error.values():
103 return True
104 return False
105
106 def get_error_codes(self):
107 count = len(self.errors)
108 if count > 1:
109 return count
110 elif count == 0:
111 return
112 else:
113 return self.errors[0]["Code"]
114
115 def get_error_messages(self):
116 count = len(self.errors)
117 if count > 1:
118 return "Multiple EC2 Errors"
119 elif count == 0:
120 return "Empty error list"
121 else:
122 return self.errors[0]["Message"]
123
11124
12125
13class AWSResponseParseError(Exception):126class AWSResponseParseError(Exception):
14127
=== added file 'txaws/meta.py'
--- txaws/meta.py 1970-01-01 00:00:00 +0000
+++ txaws/meta.py 2009-11-28 01:10:25 +0000
@@ -0,0 +1,10 @@
1display_name = "txAWS"
2library_name = "txaws"
3author = "txAWS Deelopers"
4author_email = "txaws-dev@lists.launchpad.net"
5license = "MIT"
6url = "http://launchpad.net/txaws"
7description = """
8Twisted-based Asynchronous Libraries for Amazon Web Services
9"""
10
011
=== modified file 'txaws/s3/client.py'
--- txaws/s3/client.py 2009-11-28 01:10:25 +0000
+++ txaws/s3/client.py 2009-11-28 01:10:26 +0000
@@ -17,12 +17,17 @@
1717
18from epsilon.extime import Time18from epsilon.extime import Time
1919
20from txaws.client.base import BaseClient, BaseQuery20from txaws.client.base import BaseClient, BaseQuery, error_wrapper
21from txaws.s3 import model21from txaws.s3 import model
22from txaws.s3.exception import S3Error
22from txaws.service import AWSServiceEndpoint, S3_ENDPOINT23from txaws.service import AWSServiceEndpoint, S3_ENDPOINT
23from txaws.util import XML, calculate_md524from txaws.util import XML, calculate_md5
2425
2526
27def s3_error_wrapper(error):
28 error_wrapper(error, S3Error)
29
30
26class URLContext(object):31class URLContext(object):
27 """32 """
28 The hosts and the paths that form an S3 endpoint change depending upon the33 The hosts and the paths that form an S3 endpoint change depending upon the
@@ -190,6 +195,10 @@
190 self.endpoint.set_method(self.action)195 self.endpoint.set_method(self.action)
191196
192 def set_content_type(self):197 def set_content_type(self):
198 """
199 Set the content type based on the file extension used in the object
200 name.
201 """
193 if self.object_name and not self.content_type:202 if self.object_name and not self.content_type:
194 # XXX nothing is currently done with the encoding... we may203 # XXX nothing is currently done with the encoding... we may
195 # need to in the future204 # need to in the future
@@ -197,6 +206,9 @@
197 self.object_name, strict=False)206 self.object_name, strict=False)
198207
199 def get_headers(self):208 def get_headers(self):
209 """
210 Build the list of headers needed in order to perform S3 operations.
211 """
200 headers = {"Content-Length": len(self.data),212 headers = {"Content-Length": len(self.data),
201 "Content-MD5": calculate_md5(self.data),213 "Content-MD5": calculate_md5(self.data),
202 "Date": self.date}214 "Date": self.date}
@@ -214,6 +226,9 @@
214 return headers226 return headers
215227
216 def get_canonicalized_amz_headers(self, headers):228 def get_canonicalized_amz_headers(self, headers):
229 """
230 Get the headers defined by Amazon S3.
231 """
217 headers = [232 headers = [
218 (name.lower(), value) for name, value in headers.iteritems()233 (name.lower(), value) for name, value in headers.iteritems()
219 if name.lower().startswith("x-amz-")]234 if name.lower().startswith("x-amz-")]
@@ -224,6 +239,9 @@
224 return "".join("%s:%s\n" % (name, value) for name, value in headers)239 return "".join("%s:%s\n" % (name, value) for name, value in headers)
225240
226 def get_canonicalized_resource(self):241 def get_canonicalized_resource(self):
242 """
243 Get an S3 resource path.
244 """
227 resource = "/"245 resource = "/"
228 if self.bucket:246 if self.bucket:
229 resource += self.bucket247 resource += self.bucket
@@ -232,7 +250,7 @@
232 return resource250 return resource
233251
234 def sign(self, headers):252 def sign(self, headers):
235253 """Sign this query using its built in credentials."""
236 text = (self.action + "\n" +254 text = (self.action + "\n" +
237 headers.get("Content-MD5", "") + "\n" +255 headers.get("Content-MD5", "") + "\n" +
238 headers.get("Content-Type", "") + "\n" +256 headers.get("Content-Type", "") + "\n" +
@@ -242,14 +260,14 @@
242 return self.creds.sign(text, hash_type="sha1")260 return self.creds.sign(text, hash_type="sha1")
243261
244 def submit(self, url_context=None):262 def submit(self, url_context=None):
263 """Submit this query.
264
265 @return: A deferred from get_page
266 """
245 if not url_context:267 if not url_context:
246 url_context = URLContext(268 url_context = URLContext(
247 self.endpoint, self.bucket, self.object_name)269 self.endpoint, self.bucket, self.object_name)
248 d = self.get_page(270 d = self.get_page(
249 url_context.get_url(), method=self.action, postdata=self.data,271 url_context.get_url(), method=self.action, postdata=self.data,
250 headers=self.get_headers())272 headers=self.get_headers())
251 # XXX - we need an error wrapper like we have for ec2... but let's273 return d.addErrback(s3_error_wrapper)
252 # wait until the new error-wrapper branch has landed, and possibly
253 # generalize a base class for all clients.
254 #d.addErrback(s3_error_wrapper)
255 return d
256274
=== added file 'txaws/s3/exception.py'
--- txaws/s3/exception.py 1970-01-01 00:00:00 +0000
+++ txaws/s3/exception.py 2009-11-28 01:10:26 +0000
@@ -0,0 +1,21 @@
1# Copyright (c) 2009 Canonical Ltd <duncan.mcgreggor@canonical.com>
2# Licenced under the txaws licence available at /LICENSE in the txaws source.
3
4from txaws.exception import AWSError
5
6
7class S3Error(AWSError):
8 """
9 A error class providing custom methods on S3 errors.
10 """
11 def _set_400_error(self, tree):
12 if tree.tag.lower() == "error":
13 data = self._node_to_dict(tree)
14 if data:
15 self.errors.append(data)
16
17 def get_error_code(self, *args, **kwargs):
18 return super(S3Error, self).get_error_codes(*args, **kwargs)
19
20 def get_error_message(self, *args, **kwargs):
21 return super(S3Error, self).get_error_messages(*args, **kwargs)
022
=== added file 'txaws/s3/tests/test_exception.py'
--- txaws/s3/tests/test_exception.py 1970-01-01 00:00:00 +0000
+++ txaws/s3/tests/test_exception.py 2009-11-28 01:10:26 +0000
@@ -0,0 +1,62 @@
1# Copyright (c) 2009 Canonical Ltd <duncan.mcgreggor@canonical.com>
2# Licenced under the txaws licence available at /LICENSE in the txaws source.
3
4from twisted.trial.unittest import TestCase
5
6from txaws.s3.exception import S3Error
7from txaws.testing import payload
8from txaws.util import XML
9
10
11REQUEST_ID = "0ef9fc37-6230-4d81-b2e6-1b36277d4247"
12
13
14class S3ErrorTestCase(TestCase):
15
16 def test_set_400_error(self):
17 xml = "<Error><Code>1</Code><Message>2</Message></Error>"
18 error = S3Error("<dummy />")
19 error._set_400_error(XML(xml))
20 self.assertEquals(error.errors[0]["Code"], "1")
21 self.assertEquals(error.errors[0]["Message"], "2")
22
23 def test_get_error_code(self):
24 error = S3Error(payload.sample_s3_invalid_access_key_result)
25 self.assertEquals(error.get_error_code(), "InvalidAccessKeyId")
26
27 def test_get_error_message(self):
28 error = S3Error(payload.sample_s3_invalid_access_key_result)
29 self.assertEquals(
30 error.get_error_message(),
31 ("The AWS Access Key Id you provided does not exist in our "
32 "records."))
33
34 def test_error_count(self):
35 error = S3Error(payload.sample_s3_invalid_access_key_result)
36 self.assertEquals(len(error.errors), 1)
37
38 def test_error_repr(self):
39 error = S3Error(payload.sample_s3_invalid_access_key_result)
40 self.assertEquals(
41 repr(error),
42 "<S3Error object with Error code: InvalidAccessKeyId>")
43
44 def test_signature_mismatch_result(self):
45 error = S3Error(payload.sample_s3_signature_mismatch)
46 self.assertEquals(
47 error.get_error_messages(),
48 ("The request signature we calculated does not match the "
49 "signature you provided. Check your key and signing method."))
50
51 def test_invalid_access_key_result(self):
52 error = S3Error(payload.sample_s3_invalid_access_key_result)
53 self.assertEquals(
54 error.get_error_messages(),
55 ("The AWS Access Key Id you provided does not exist in our "
56 "records."))
57
58 def test_internal_error_result(self):
59 error = S3Error(payload.sample_server_internal_error_result)
60 self.assertEquals(
61 error.get_error_messages(),
62 "We encountered an internal error. Please try again.")
063
=== added file 'txaws/script.py'
--- txaws/script.py 1970-01-01 00:00:00 +0000
+++ txaws/script.py 2009-11-28 01:10:25 +0000
@@ -0,0 +1,42 @@
1from optparse import OptionParser
2
3from txaws import meta
4from txaws import version
5
6
7# XXX Once we start adding script that require conflicting options, we'll need
8# multiple parsers and option dispatching...
9def parse_options(usage):
10 parser = OptionParser(usage, version="%s %s" % (
11 meta.display_name, version.txaws))
12 parser.add_option(
13 "-a", "--access-key", dest="access_key", help="access key ID")
14 parser.add_option(
15 "-s", "--secret-key", dest="secret_key", help="access secret key")
16 parser.add_option(
17 "-r", "--region", dest="region", help="US or EU (valid for AWS only)")
18 parser.add_option(
19 "-U", "--url", dest="url", help="service URL/endpoint")
20 parser.add_option(
21 "-b", "--bucket", dest="bucket", help="name of the bucket")
22 parser.add_option(
23 "-o", "--object-name", dest="object_name", help="name of the object")
24 parser.add_option(
25 "-d", "--object-data", dest="object_data",
26 help="content data of the object")
27 parser.add_option(
28 "--object-file", dest="object_filename",
29 help=("the path to the file that will be saved as an object; if "
30 "provided, the --object-name and --object-data options are "
31 "not necessary"))
32 parser.add_option(
33 "-c", "--content-type", dest="content_type",
34 help="content type of the object")
35 options, args = parser.parse_args()
36 if not (options.access_key and options.secret_key):
37 parser.error(
38 "both the access key ID and the secret key must be supplied")
39 region = options.region
40 if region and region.upper() not in ["US", "EU"]:
41 parser.error("region must be one of 'US' or 'EU'")
42 return (options, args)
043
=== modified file 'txaws/testing/payload.py'
--- txaws/testing/payload.py 2009-11-28 01:10:25 +0000
+++ txaws/testing/payload.py 2009-11-28 01:10:26 +0000
@@ -656,6 +656,31 @@
656"""656"""
657657
658658
659sample_restricted_resource_result = """\
660<?xml version="1.0"?>
661<Response>
662 <Errors>
663 <Error>
664 <Code>AuthFailure</Code>
665 <Message>Unauthorized attempt to access restricted resource</Message>
666 </Error>
667 </Errors>
668 <RequestID>a99e832e-e6e0-416a-9a35-81798ea521b4</RequestID>
669</Response>
670"""
671
672
673sample_server_internal_error_result = """\
674<?xml version="1.0" encoding="UTF-8"?>
675<Error>
676 <Code>InternalError</Code>
677 <Message>We encountered an internal error. Please try again.</Message>
678 <RequestID>A2A7E5395E27DFBB</RequestID>
679 <HostID>f691zulHNsUqonsZkjhILnvWwD3ZnmOM4ObM1wXTc6xuS3GzPmjArp8QC/sGsn6K</HostID>
680</Error>
681"""
682
683
659sample_list_buckets_result = """\684sample_list_buckets_result = """\
660<?xml version="1.0" encoding="UTF-8"?>685<?xml version="1.0" encoding="UTF-8"?>
661<ListAllMyBucketsResult xmlns="http://s3.amazonaws.com/doc/%s/">686<ListAllMyBucketsResult xmlns="http://s3.amazonaws.com/doc/%s/">
@@ -692,26 +717,13 @@
692"""717"""
693718
694719
695sample_server_internal_error_result = """\720sample_s3_invalid_access_key_result = """\
696<?xml version="1.0" encoding="UTF-8"?>721<?xml version="1.0" encoding="UTF-8"?>
697<Error>722<Error>
698 <Code>InternalError</Code>723 <Code>InvalidAccessKeyId</Code>
699 <Message>We encountered an internal error. Please try again.</Message>724 <Message>The AWS Access Key Id you provided does not exist in our records.</Message>
700 <RequestID>A2A7E5395E27DFBB</RequestID>725 <RequestId>0223AD81A94821CE</RequestId>
701 <HostID>f691zulHNsUqonsZkjhILnvWwD3ZnmOM4ObM1wXTc6xuS3GzPmjArp8QC/sGsn6K</HostID>726 <HostId>HAw5g9P1VkN8ztgLKFTK20CY5LmCfTwXcSths1O7UQV6NuJx2P4tmFnpuOsziwOE</HostId>
727 <AWSAccessKeyId>SOMEKEYID</AWSAccessKeyId>
702</Error>728</Error>
703"""729"""
704
705
706sample_restricted_resource_result = """\
707<?xml version="1.0"?>
708<Response>
709 <Errors>
710 <Error>
711 <Code>AuthFailure</Code>
712 <Message>Unauthorized attempt to access restricted resource</Message>
713 </Error>
714 </Errors>
715 <RequestID>a99e832e-e6e0-416a-9a35-81798ea521b4</RequestID>
716</Response>
717"""
718730
=== added file 'txaws/tests/test_exception.py'
--- txaws/tests/test_exception.py 1970-01-01 00:00:00 +0000
+++ txaws/tests/test_exception.py 2009-11-28 01:10:26 +0000
@@ -0,0 +1,114 @@
1# Copyright (c) 2009 Canonical Ltd <duncan.mcgreggor@canonical.com>
2# Licenced under the txaws licence available at /LICENSE in the txaws source.
3
4from twisted.trial.unittest import TestCase
5
6from txaws.exception import AWSError
7from txaws.exception import AWSResponseParseError
8from txaws.util import XML
9
10
11REQUEST_ID = "0ef9fc37-6230-4d81-b2e6-1b36277d4247"
12
13
14class AWSErrorTestCase(TestCase):
15
16 def test_creation(self):
17 error = AWSError("<dummy1 />", 500, "Server Error", "<dummy2 />")
18 self.assertEquals(error.status, 500)
19 self.assertEquals(error.response, "<dummy2 />")
20 self.assertEquals(error.original, "<dummy1 />")
21 self.assertEquals(error.errors, [])
22 self.assertEquals(error.request_id, "")
23
24 def test_node_to_dict(self):
25 xml = "<parent><child1>text1</child1><child2>text2</child2></parent>"
26 error = AWSError("<dummy />")
27 data = error._node_to_dict(XML(xml))
28 self.assertEquals(data, {"child1": "text1", "child2": "text2"})
29
30 def test_set_request_id(self):
31 xml = "<a><b /><RequestID>%s</RequestID></a>" % REQUEST_ID
32 error = AWSError("<dummy />")
33 error._set_request_id(XML(xml))
34 self.assertEquals(error.request_id, REQUEST_ID)
35
36 def test_set_host_id(self):
37 host_id = "ASD@#FDG$E%FG"
38 xml = "<a><b /><HostID>%s</HostID></a>" % host_id
39 error = AWSError("<dummy />")
40 error._set_host_id(XML(xml))
41 self.assertEquals(error.host_id, host_id)
42
43 def test_set_empty_errors(self):
44 xml = "<a><Errors /><b /></a>"
45 error = AWSError("<dummy />")
46 error._set_500_error(XML(xml))
47 self.assertEquals(error.errors, [])
48
49 def test_set_empty_error(self):
50 xml = "<a><Errors><Error /><Error /></Errors><b /></a>"
51 error = AWSError("<dummy />")
52 error._set_500_error(XML(xml))
53 self.assertEquals(error.errors, [])
54
55 def test_parse_without_xml(self):
56 xml = "<dummy />"
57 error = AWSError(xml)
58 error.parse()
59 self.assertEquals(error.original, xml)
60
61 def test_parse_with_xml(self):
62 xml1 = "<dummy1 />"
63 xml2 = "<dummy2 />"
64 error = AWSError(xml1)
65 error.parse(xml2)
66 self.assertEquals(error.original, xml2)
67
68 def test_parse_html(self):
69 xml = "<html><body>a page</body></html>"
70 self.assertRaises(AWSResponseParseError, AWSError, xml)
71
72 def test_empty_xml(self):
73 self.assertRaises(ValueError, AWSError, "")
74
75 def test_no_request_id(self):
76 errors = "<Errors><Error><Code /><Message /></Error></Errors>"
77 xml = "<Response>%s<RequestID /></Response>" % errors
78 error = AWSError(xml)
79 self.assertEquals(error.request_id, "")
80
81 def test_no_request_id_node(self):
82 errors = "<Errors><Error><Code /><Message /></Error></Errors>"
83 xml = "<Response>%s</Response>" % errors
84 error = AWSError(xml)
85 self.assertEquals(error.request_id, "")
86
87 def test_no_errors_node(self):
88 xml = "<Response><RequestID /></Response>"
89 error = AWSError(xml)
90 self.assertEquals(error.errors, [])
91
92 def test_no_error_node(self):
93 xml = "<Response><Errors /><RequestID /></Response>"
94 error = AWSError(xml)
95 self.assertEquals(error.errors, [])
96
97 def test_no_error_code_node(self):
98 errors = "<Errors><Error><Message /></Error></Errors>"
99 xml = "<Response>%s<RequestID /></Response>" % errors
100 error = AWSError(xml)
101 self.assertEquals(error.errors, [])
102
103 def test_no_error_message_node(self):
104 errors = "<Errors><Error><Code /></Error></Errors>"
105 xml = "<Response>%s<RequestID /></Response>" % errors
106 error = AWSError(xml)
107 self.assertEquals(error.errors, [])
108
109 def test_set_500_error(self):
110 xml = "<Error><Code>500</Code><Message>Oops</Message></Error>"
111 error = AWSError("<dummy />")
112 error._set_500_error(XML(xml))
113 self.assertEquals(error.errors[0]["Code"], "500")
114 self.assertEquals(error.errors[0]["Message"], "Oops")
0115
=== modified file 'txaws/util.py'
--- txaws/util.py 2009-11-28 01:10:25 +0000
+++ txaws/util.py 2009-11-28 01:10:26 +0000
@@ -94,3 +94,33 @@
94 if path == "":94 if path == "":
95 path = "/"95 path = "/"
96 return (str(scheme), str(host), port, str(path))96 return (str(scheme), str(host), port, str(path))
97
98
99def get_exitcode_reactor():
100 """
101 This is only neccesary until a fix like the one outlined here is
102 implemented for Twisted:
103 http://twistedmatrix.com/trac/ticket/2182
104 """
105 from twisted.internet.main import installReactor
106 from twisted.internet.selectreactor import SelectReactor
107
108 class ExitCodeReactor(SelectReactor):
109
110 def stop(self, exitStatus=0):
111 super(ExitCodeReactor, self).stop()
112 self.exitStatus = exitStatus
113
114 def run(self, *args, **kwargs):
115 super(ExitCodeReactor, self).run(*args, **kwargs)
116 return self.exitStatus
117
118 reactor = ExitCodeReactor()
119 installReactor(reactor)
120 return reactor
121
122
123try:
124 reactor = get_exitcode_reactor()
125except:
126 from twisted.internet import reactor

Subscribers

People subscribed via source and target branches

to all changes: