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