Merge lp:~oubiwann/txaws/416109-arbitrary-endpoints into lp:~txawsteam/txaws/trunk

Proposed by Duncan McGreggor
Status: Superseded
Proposed branch: lp:~oubiwann/txaws/416109-arbitrary-endpoints
Merge into: lp:~txawsteam/txaws/trunk
Diff against target: None lines
To merge this branch: bzr merge lp:~oubiwann/txaws/416109-arbitrary-endpoints
Reviewer Review Type Date Requested Status
Original txAWS Team Pending
Review via email: mp+10477@code.launchpad.net

This proposal has been superseded by a proposal from 2009-08-23.

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

This branch adds support for a service object that manages host endpoints as well as authorization keys (thus obviating the need for the AWSCredential object).

Revision history for this message
Robert Collins (lifeless) wrote :

On Thu, 2009-08-20 at 19:25 +0000, Duncan McGreggor wrote:
> Duncan McGreggor has proposed merging
> lp:~oubiwann/txaws/416109-arbitrary-endpoints into lp:txaws.
>
> Requested reviews:
> txAWS Team (txawsteam)
>
> This branch adds support for a service object that manages host
> endpoints as well as authorization keys (thus obviating the need for
> the AWSCredential object).

Lets be careful to keep space under storage, ec2 etc for server
components. storage.service isn't really a storage service :) Lets call
the description of an end point AWSServiceEndpoint, or something like
that.

local and credentials appear orthogonal to me - for instance,
EC2 EU and EC2 US are different endpoints/services with common
credentials. I think conflating them is unnecessary and undesirable.
Further to that, the AWSCredentials are usable on related services in a
single region - EC2, S3 and so on, so when we're passing around a
description, we probably want to have a region that describes the
endpoints for a collection of services. The goal being able to have a
static object
AWS_US1 = #...
AWS_US2 = #...
and for people to make their own;
my_eucalyptus_region = #...

At runtime then, one would ask a region for a client of a particular
service, using some credentials.

AWS_US1.make_ec2_client(my_creds)
AWS_US1.make_sqs_client(my_creds)

etc.

We could do this without changing the existing clients at all, by just
storing scheme,host tuples in a AWSRegion - but I think it is cleaner to
do the sort of refactoring you have done. I think it would be best by
having an AWSServiceEndpoint which has the scheme and url, and keeping
the creds separate. For instance,
class AWSServiceRegion:
    def make_ec2_client(self, creds=None):
        return EC2Client(creds=creds, service_endpoint=self.ec2_endpoint)

Also a bit of detail review - 'default_schema = https' - in URL terms
(see http://www.ietf.org/rfc/rfc3986.txt) that is a _scheme_, not a
_schema_.

review needsfixing

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

> On Thu, 2009-08-20 at 19:25 +0000, Duncan McGreggor wrote:
> > Duncan McGreggor has proposed merging
> > lp:~oubiwann/txaws/416109-arbitrary-endpoints into lp:txaws.
> >
> > Requested reviews:
> > txAWS Team (txawsteam)
> >
> > This branch adds support for a service object that manages host
> > endpoints as well as authorization keys (thus obviating the need for
> > the AWSCredential object).
>
>
> Lets be careful to keep space under storage, ec2 etc for server
> components. storage.service isn't really a storage service :) Lets call
> the description of an end point AWSServiceEndpoint, or something like
> that.
>
> local and credentials appear orthogonal to me - for instance,
> EC2 EU and EC2 US are different endpoints/services with common
> credentials. I think conflating them is unnecessary and undesirable.
> Further to that, the AWSCredentials are usable on related services in a
> single region - EC2, S3 and so on, so when we're passing around a
> description, we probably want to have a region that describes the
> endpoints for a collection of services. The goal being able to have a
> static object
> AWS_US1 = #...
> AWS_US2 = #...
> and for people to make their own;
> my_eucalyptus_region = #...
>
> At runtime then, one would ask a region for a client of a particular
> service, using some credentials.
>
> AWS_US1.make_ec2_client(my_creds)
> AWS_US1.make_sqs_client(my_creds)
>
> etc.
>
> We could do this without changing the existing clients at all, by just
> storing scheme,host tuples in a AWSRegion - but I think it is cleaner to
> do the sort of refactoring you have done. I think it would be best by
> having an AWSServiceEndpoint which has the scheme and url, and keeping
> the creds separate. For instance,
> class AWSServiceRegion:
> def make_ec2_client(self, creds=None):
> return EC2Client(creds=creds, service_endpoint=self.ec2_endpoint)
>
> Also a bit of detail review - 'default_schema = https' - in URL terms
> (see http://www.ietf.org/rfc/rfc3986.txt) that is a _scheme_, not a
> _schema_.
>
> review needsfixing

+1 on these suggestions. I'll give it another go with this in mind.

18. By Duncan McGreggor

A couple tiny tweaks.

19. By Duncan McGreggor

Added missing service file.

20. By Duncan McGreggor

Added credentials back.

21. By Duncan McGreggor

Merged from trunk and resolved conflicts.

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

> On Thu, 2009-08-20 at 19:25 +0000, Duncan McGreggor wrote:
> > Duncan McGreggor has proposed merging
> > lp:~oubiwann/txaws/416109-arbitrary-endpoints into lp:txaws.
> >
> > Requested reviews:
> > txAWS Team (txawsteam)
> >
> > This branch adds support for a service object that manages host
> > endpoints as well as authorization keys (thus obviating the need for
> > the AWSCredential object).
>
>
> Lets be careful to keep space under storage, ec2 etc for server
> components. storage.service isn't really a storage service :) Lets call
> the description of an end point AWSServiceEndpoint, or something like
> that.

[1] Renamed.

> local and credentials appear orthogonal to me - for instance,
> EC2 EU and EC2 US are different endpoints/services with common
> credentials. I think conflating them is unnecessary and undesirable.
> Further to that, the AWSCredentials are usable on related services in a
> single region - EC2, S3 and so on, so when we're passing around a
> description, we probably want to have a region that describes the
> endpoints for a collection of services.

[2]

Brought the credentials back into the source. Pulled credential code out of service endpoint code.

> The goal being able to have a
> static object
> AWS_US1 = #...
> AWS_US2 = #...
> and for people to make their own;
> my_eucalyptus_region = #...
>
> At runtime then, one would ask a region for a client of a particular
> service, using some credentials.
>
> AWS_US1.make_ec2_client(my_creds)
> AWS_US1.make_sqs_client(my_creds)
>
> etc.
>
> We could do this without changing the existing clients at all, by just
> storing scheme,host tuples in a AWSRegion - but I think it is cleaner to
> do the sort of refactoring you have done. I think it would be best by
> having an AWSServiceEndpoint which has the scheme and url, and keeping
> the creds separate. For instance,
> class AWSServiceRegion:
> def make_ec2_client(self, creds=None):
> return EC2Client(creds=creds, service_endpoint=self.ec2_endpoint)

[3]

I'm got an implementation of this in place right now. It ended up pretty similar to what you suggested. There are some missing unit tests right now -- I'll be hitting those this afternoon.

> Also a bit of detail review - 'default_schema = https' - in URL terms
> (see http://www.ietf.org/rfc/rfc3986.txt) that is a _scheme_, not a
> _schema_.

[4]

Ugh, thanks. The first place I wrote it was good, then I copied a typo everywhere else. Fixed.

22. By Duncan McGreggor

- Updated the AWSCredentials with the refactored code that had been written in
  the AWService class.
- Renamed the AWSService class to AWSServiceEndpoint (lifeless 1)
- Created and AWSServiceRegion object that acts as a client factory (lifeless 3)
- Added a placeholder for the service unit tests.
- Removed storage service.

23. By Duncan McGreggor

Added a TODO comment for tests.

24. By Duncan McGreggor

Added missing test for set_path.

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

Okay! Just pushed up the latest code for the missing unit tests. It's ready for another review :-)

25. By Duncan McGreggor

Added missing tests for the AWS service region object.

26. By Duncan McGreggor

Added another check for client caching.

27. By Duncan McGreggor

Removed new cred files.

28. By Duncan McGreggor

Reverted to original cred files (-r13..12) in an effort to fix some weirdness
in this branch with those files.

29. By Duncan McGreggor

Reapplied the recent changes to the cred files.

30. By Duncan McGreggor

- Changed the gtk client to use creds and service region instead of the
  no-longer-supported service object.
- Added a cache-purging keyword parameter to the AWS service region's
  get_client method.
- Added a docstring.
- Added region string objects to service.__all__.
- Tweaked the gtk code to check for an already-installed gtk Twisted reactor.
- Cleaned up some remaining references to the service object in the client and
  replaced those with endpoint references.
- Fixed a stub query signature in a unit test to include a parameter for an
  endpoint object.

31. By Duncan McGreggor

- Removed old service unit test file.
- Added unit test for purge client option.
- Fixed typo in client check unit tests.

32. By Duncan McGreggor

- Added access and secret key parameters to the AWSServiceRegion constructor.
- Updated AWSServiceRegion to create creds based on access and secret key if no
  creds are supplied.
- Updated docstrings.

33. By Duncan McGreggor

Added a uri parameter for service region creation to ease the creation of
service region objects with non-Amazon endpoints (e.g., in Landscape).

34. By Duncan McGreggor

Swapped the ordering of an import to be in alphabetical order.

35. By Duncan McGreggor

- Fixed the creds parameter in the get_ec2_client method.
- Removed redundant code in check_parsed_instances.
- Created a testing subpackage for generally useful testing classes.
- Added fake ec2 client and region classes.
- Moved base test case into new testing module.

36. By Duncan McGreggor

- Removed unimplemented methods (jkakar 1).
- Made environment mutation methods private (jkakar 3).
- Tweaked the default values for the FakeEC2Client (jkakar 4).
- Removed unnecessary test case methods (jkakar 5).

37. By Duncan McGreggor

Tweaked the storage request object's enpoint/uri stuff and added some unit
tests (jkakar 2).

38. By Duncan McGreggor

Removed unnecessary region instantiation (therve 3).
Added parse utility function (therve 4).

39. By Duncan McGreggor

Fixed pyflakes (therve 1).

40. By Duncan McGreggor

Removed unneeded try/except block in gtk client (therve 2).

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'txaws/client/gui/gtk.py'
2--- txaws/client/gui/gtk.py 2009-08-18 22:53:53 +0000
3+++ txaws/client/gui/gtk.py 2009-08-20 19:14:32 +0000
4@@ -8,7 +8,7 @@
5 import gobject
6 import gtk
7
8-from txaws.credentials import AWSCredentials
9+from txaws.ec2.service import EC2Service
10
11
12 __all__ = ['main']
13@@ -27,10 +27,10 @@
14 # Nested import because otherwise we get 'reactor already installed'.
15 self.password_dialog = None
16 try:
17- creds = AWSCredentials()
18+ service = AWSService()
19 except ValueError:
20- creds = self.from_gnomekeyring()
21- self.create_client(creds)
22+ service = self.from_gnomekeyring()
23+ self.create_client(service)
24 menu = '''
25 <ui>
26 <menubar name="Menubar">
27@@ -54,10 +54,10 @@
28 '/Menubar/Menu/Stop instances').props.parent
29 self.connect('popup-menu', self.on_popup_menu)
30
31- def create_client(self, creds):
32+ def create_client(self, service):
33 from txaws.ec2.client import EC2Client
34- if creds is not None:
35- self.client = EC2Client(creds=creds)
36+ if service is not None:
37+ self.client = EC2Client(service=service)
38 self.on_activate(None)
39 else:
40 # waiting on user entered credentials.
41@@ -65,7 +65,7 @@
42
43 def from_gnomekeyring(self):
44 # Try for gtk gui specific credentials.
45- creds = None
46+ service = None
47 try:
48 items = gnomekeyring.find_items_sync(
49 gnomekeyring.ITEM_GENERIC_SECRET,
50@@ -78,7 +78,7 @@
51 return None
52 else:
53 key_id, secret_key = items[0].secret.split(':')
54- return AWSCredentials(access_key=key_id, secret_key=secret_key)
55+ return EC2Service(access_key=key_id, secret_key=secret_key)
56
57 def show_a_password_dialog(self):
58 self.password_dialog = gtk.Dialog(
59@@ -133,8 +133,8 @@
60 content = self.password_dialog.get_content_area()
61 key_id = content.get_children()[0].get_children()[1].get_text()
62 secret_key = content.get_children()[1].get_children()[1].get_text()
63- creds = AWSCredentials(access_key=key_id, secret_key=secret_key)
64- self.create_client(creds)
65+ service = EC2Service(access_key=key_id, secret_key=secret_key)
66+ self.create_client(service)
67 gnomekeyring.item_create_sync(
68 None,
69 gnomekeyring.ITEM_GENERIC_SECRET,
70
71=== removed file 'txaws/credentials.py'
72--- txaws/credentials.py 2009-08-17 11:18:56 +0000
73+++ txaws/credentials.py 1970-01-01 00:00:00 +0000
74@@ -1,37 +0,0 @@
75-# Copyright (C) 2009 Robert Collins <robertc@robertcollins.net>
76-# Licenced under the txaws licence available at /LICENSE in the txaws source.
77-
78-"""Credentials for accessing AWS services."""
79-
80-import os
81-
82-from txaws.util import *
83-
84-
85-__all__ = ['AWSCredentials']
86-
87-
88-class AWSCredentials(object):
89-
90- def __init__(self, access_key=None, secret_key=None):
91- """Create an AWSCredentials object.
92-
93- :param access_key: The access key to use. If None the environment
94- variable AWS_ACCESS_KEY_ID is consulted.
95- :param secret_key: The secret key to use. If None the environment
96- variable AWS_SECRET_ACCESS_KEY is consulted.
97- """
98- self.secret_key = secret_key
99- if self.secret_key is None:
100- self.secret_key = os.environ.get('AWS_SECRET_ACCESS_KEY')
101- if self.secret_key is None:
102- raise ValueError('Could not find AWS_SECRET_ACCESS_KEY')
103- self.access_key = access_key
104- if self.access_key is None:
105- self.access_key = os.environ.get('AWS_ACCESS_KEY_ID')
106- if self.access_key is None:
107- raise ValueError('Could not find AWS_ACCESS_KEY_ID')
108-
109- def sign(self, bytes):
110- """Sign some bytes."""
111- return hmac_sha1(self.secret_key, bytes)
112
113=== modified file 'txaws/ec2/client.py'
114--- txaws/ec2/client.py 2009-08-18 21:56:36 +0000
115+++ txaws/ec2/client.py 2009-08-20 16:47:54 +0000
116@@ -8,7 +8,7 @@
117
118 from twisted.web.client import getPage
119
120-from txaws import credentials
121+from txaws.ec2.service import EC2Service
122 from txaws.util import iso8601time, XML
123
124
125@@ -46,16 +46,15 @@
126
127 name_space = '{http://ec2.amazonaws.com/doc/2008-12-01/}'
128
129- def __init__(self, creds=None, query_factory=None):
130+ def __init__(self, service=None, query_factory=None):
131 """Create an EC2Client.
132
133- @param creds: Explicit credentials to use. If None, credentials are
134- inferred as per txaws.credentials.AWSCredentials.
135+ @param service: Explicit service to use.
136 """
137- if creds is None:
138- self.creds = credentials.AWSCredentials()
139+ if service is None:
140+ self.service = EC2Service()
141 else:
142- self.creds = creds
143+ self.service = service
144 if query_factory is None:
145 self.query_factory = Query
146 else:
147@@ -63,7 +62,7 @@
148
149 def describe_instances(self):
150 """Describe current instances."""
151- q = self.query_factory('DescribeInstances', self.creds)
152+ q = self.query_factory('DescribeInstances', self.service)
153 d = q.submit()
154 return d.addCallback(self._parse_instances)
155
156@@ -119,7 +118,7 @@
157 instanceset = {}
158 for pos, instance_id in enumerate(instance_ids):
159 instanceset["InstanceId.%d" % (pos+1)] = instance_id
160- q = self.query_factory('TerminateInstances', self.creds, instanceset)
161+ q = self.query_factory('TerminateInstances', self.service, instanceset)
162 d = q.submit()
163 return d.addCallback(self._parse_terminate_instances)
164
165@@ -142,7 +141,7 @@
166 class Query(object):
167 """A query that may be submitted to EC2."""
168
169- def __init__(self, action, creds, other_params=None, time_tuple=None):
170+ def __init__(self, action, service, other_params=None, time_tuple=None):
171 """Create a Query to submit to EC2."""
172 # Require params (2008-12-01 API):
173 # Version, SignatureVersion, SignatureMethod, Action, AWSAccessKeyId,
174@@ -151,15 +150,12 @@
175 'SignatureVersion': '2',
176 'SignatureMethod': 'HmacSHA1',
177 'Action': action,
178- 'AWSAccessKeyId': creds.access_key,
179+ 'AWSAccessKeyId': service.access_key,
180 'Timestamp': iso8601time(time_tuple),
181 }
182 if other_params:
183 self.params.update(other_params)
184- self.method = 'GET'
185- self.host = 'ec2.amazonaws.com'
186- self.uri = '/'
187- self.creds = creds
188+ self.service = service
189
190 def canonical_query_params(self):
191 """Return the canonical query params (used in signing)."""
192@@ -178,18 +174,19 @@
193
194 def signing_text(self):
195 """Return the text to be signed when signing the query."""
196- result = "%s\n%s\n%s\n%s" % (self.method, self.host, self.uri,
197- self.canonical_query_params())
198+ result = "%s\n%s\n%s\n%s" % (self.service.method, self.service.host,
199+ self.service.endpoint,
200+ self.canonical_query_params())
201 return result
202
203 def sign(self):
204- """Sign this query using its built in credentials.
205+ """Sign this query using its built in service.
206
207 This prepares it to be sent, and should be done as the last step before
208 submitting the query. Signing is done automatically - this is a public
209 method to facilitate testing.
210 """
211- self.params['Signature'] = self.creds.sign(self.signing_text())
212+ self.params['Signature'] = self.service.sign(self.signing_text())
213
214 def sorted_params(self):
215 """Return the query params sorted appropriately for signing."""
216@@ -198,9 +195,8 @@
217 def submit(self):
218 """Submit this query.
219
220- :return: A deferred from twisted.web.client.getPage
221+ @return: A deferred from twisted.web.client.getPage
222 """
223 self.sign()
224- url = 'http://%s%s?%s' % (self.host, self.uri,
225- self.canonical_query_params())
226- return getPage(url, method=self.method)
227+ url = "%s?%s" % (self.service.get_url(), self.canonical_query_params())
228+ return getPage(url, method=self.service.method)
229
230=== modified file 'txaws/ec2/tests/test_client.py'
231--- txaws/ec2/tests/test_client.py 2009-08-18 21:56:36 +0000
232+++ txaws/ec2/tests/test_client.py 2009-08-20 16:47:54 +0000
233@@ -5,8 +5,8 @@
234
235 from twisted.internet.defer import succeed
236
237-from txaws.credentials import AWSCredentials
238 from txaws.ec2 import client
239+from txaws.ec2.service import EC2Service, US_EC2_HOST
240 from txaws.tests import TXAWSTestCase
241
242
243@@ -91,21 +91,21 @@
244 self.assertEquals(reservation.groups, ["one", "two"])
245
246
247-class TestEC2Client(TXAWSTestCase):
248+class EC2ClientTestCase(TXAWSTestCase):
249
250 def test_init_no_creds(self):
251 os.environ['AWS_SECRET_ACCESS_KEY'] = 'foo'
252 os.environ['AWS_ACCESS_KEY_ID'] = 'bar'
253 ec2 = client.EC2Client()
254- self.assertNotEqual(None, ec2.creds)
255+ self.assertNotEqual(None, ec2.service)
256
257 def test_init_no_creds_non_available_errors(self):
258 self.assertRaises(ValueError, client.EC2Client)
259
260- def test_init_explicit_creds(self):
261- creds = 'foo'
262- ec2 = client.EC2Client(creds=creds)
263- self.assertEqual(creds, ec2.creds)
264+ def test_init_explicit_service(self):
265+ service = EC2Service("foo", "bar")
266+ ec2 = client.EC2Client(service=service)
267+ self.assertEqual(service, ec2.service)
268
269 def check_parsed_instances(self, results):
270 instance = results[0]
271@@ -118,33 +118,38 @@
272 self.assertEquals(group, "default")
273
274 def test_parse_reservation(self):
275- ec2 = client.EC2Client(creds='foo')
276+ service = EC2Service("foo", "bar")
277+ ec2 = client.EC2Client(service=service)
278 results = ec2._parse_instances(sample_describe_instances_result)
279 self.check_parsed_instances(results)
280
281 def test_describe_instances(self):
282 class StubQuery(object):
283- def __init__(stub, action, creds):
284+ def __init__(stub, action, service):
285 self.assertEqual(action, 'DescribeInstances')
286- self.assertEqual('foo', creds)
287+ self.assertEqual(service.access_key, "foo")
288+ self.assertEqual(service.secret_key, "bar")
289 def submit(self):
290 return succeed(sample_describe_instances_result)
291- ec2 = client.EC2Client(creds='foo', query_factory=StubQuery)
292+ service = EC2Service("foo", "bar")
293+ ec2 = client.EC2Client(service, query_factory=StubQuery)
294 d = ec2.describe_instances()
295 d.addCallback(self.check_parsed_instances)
296 return d
297
298 def test_terminate_instances(self):
299 class StubQuery(object):
300- def __init__(stub, action, creds, other_params):
301+ def __init__(stub, action, service, other_params):
302 self.assertEqual(action, 'TerminateInstances')
303- self.assertEqual('foo', creds)
304+ self.assertEqual(service.access_key, "foo")
305+ self.assertEqual(service.secret_key, "bar")
306 self.assertEqual(
307 {'InstanceId.1': 'i-1234', 'InstanceId.2': 'i-5678'},
308 other_params)
309 def submit(self):
310 return succeed(sample_terminate_instances_result)
311- ec2 = client.EC2Client(creds='foo', query_factory=StubQuery)
312+ service = EC2Service("foo", "bar")
313+ ec2 = client.EC2Client(service=service, query_factory=StubQuery)
314 d = ec2.terminate_instances('i-1234', 'i-5678')
315 def check_transition(changes):
316 self.assertEqual([('i-1234', 'running', 'shutting-down'),
317@@ -152,14 +157,14 @@
318 return d
319
320
321-class TestQuery(TXAWSTestCase):
322+class QueryTestCase(TXAWSTestCase):
323
324 def setUp(self):
325 TXAWSTestCase.setUp(self)
326- self.creds = AWSCredentials('foo', 'bar')
327+ self.service = EC2Service('foo', 'bar')
328
329 def test_init_minimum(self):
330- query = client.Query('DescribeInstances', self.creds)
331+ query = client.Query('DescribeInstances', self.service)
332 self.assertTrue('Timestamp' in query.params)
333 del query.params['Timestamp']
334 self.assertEqual(
335@@ -173,11 +178,11 @@
336 def test_init_requires_action(self):
337 self.assertRaises(TypeError, client.Query)
338
339- def test_init_requires_creds(self):
340+ def test_init_requires_service_with_creds(self):
341 self.assertRaises(TypeError, client.Query, None)
342
343 def test_init_other_args_are_params(self):
344- query = client.Query('DescribeInstances', self.creds,
345+ query = client.Query('DescribeInstances', self.service,
346 {'InstanceId.0': '12345'},
347 time_tuple=(2007,11,12,13,14,15,0,0,0))
348 self.assertEqual(
349@@ -191,7 +196,7 @@
350 query.params)
351
352 def test_sorted_params(self):
353- query = client.Query('DescribeInstances', self.creds,
354+ query = client.Query('DescribeInstances', self.service,
355 {'fun': 'games'},
356 time_tuple=(2007,11,12,13,14,15,0,0,0))
357 self.assertEqual([
358@@ -207,16 +212,16 @@
359 def test_encode_unreserved(self):
360 all_unreserved = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ'
361 'abcdefghijklmnopqrstuvwxyz0123456789-_.~')
362- query = client.Query('DescribeInstances', self.creds)
363+ query = client.Query('DescribeInstances', self.service)
364 self.assertEqual(all_unreserved, query.encode(all_unreserved))
365
366 def test_encode_space(self):
367 """This may be just 'url encode', but the AWS manual isn't clear."""
368- query = client.Query('DescribeInstances', self.creds)
369+ query = client.Query('DescribeInstances', self.service)
370 self.assertEqual('a%20space', query.encode('a space'))
371
372 def test_canonical_query(self):
373- query = client.Query('DescribeInstances', self.creds,
374+ query = client.Query('DescribeInstances', self.service,
375 {'fu n': 'g/ames', 'argwithnovalue':'',
376 'InstanceId.1': 'i-1234'},
377 time_tuple=(2007,11,12,13,14,15,0,0,0))
378@@ -228,17 +233,17 @@
379 self.assertEqual(expected_query, query.canonical_query_params())
380
381 def test_signing_text(self):
382- query = client.Query('DescribeInstances', self.creds,
383+ query = client.Query('DescribeInstances', self.service,
384 time_tuple=(2007,11,12,13,14,15,0,0,0))
385- signing_text = ('GET\nec2.amazonaws.com\n/\n'
386+ signing_text = ('GET\n%s\n/\n' % US_EC2_HOST +
387 'AWSAccessKeyId=foo&Action=DescribeInstances&'
388 'SignatureMethod=HmacSHA1&SignatureVersion=2&'
389 'Timestamp=2007-11-12T13%3A14%3A15Z&Version=2008-12-01')
390 self.assertEqual(signing_text, query.signing_text())
391
392 def test_sign(self):
393- query = client.Query('DescribeInstances', self.creds,
394+ query = client.Query('DescribeInstances', self.service,
395 time_tuple=(2007,11,12,13,14,15,0,0,0))
396 query.sign()
397- self.assertEqual('4hEtLuZo9i6kuG3TOXvRQNOrE/U=',
398+ self.assertEqual('JuCpwFA2H4OVF3Ql/lAQs+V6iMc=',
399 query.params['Signature'])
400
401=== added file 'txaws/service.py'
402--- txaws/service.py 1970-01-01 00:00:00 +0000
403+++ txaws/service.py 2009-08-20 19:09:56 +0000
404@@ -0,0 +1,70 @@
405+# Copyright (C) 2009 Duncan McGreggor <duncan@canonical.com>
406+# Copyright (C) 2009 Robert Collins <robertc@robertcollins.net>
407+# Licenced under the txaws licence available at /LICENSE in the txaws source.
408+
409+import os
410+
411+from twisted.web.client import _parse
412+
413+from txaws.util import hmac_sha1
414+
415+DEFAULT_PORT = 80
416+ENV_ACCESS_KEY = "AWS_ACCESS_KEY_ID"
417+ENV_SECRET_KEY = "AWS_SECRET_ACCESS_KEY"
418+
419+class AWSService(object):
420+ """
421+ @param access_key: The access key to use. If None the environment
422+ variable AWS_ACCESS_KEY_ID is consulted.
423+ @param secret_key: The secret key to use. If None the environment
424+ variable AWS_SECRET_ACCESS_KEY is consulted.
425+ @param uri: The URL for the service.
426+ @param method: The HTTP method used when accessing a service.
427+ """
428+ default_host = ""
429+ default_schema = "https"
430+
431+ def __init__(self, access_key="", secret_key="", uri="", method="GET"):
432+ self.access_key = access_key
433+ self.secret_key = secret_key
434+ self.schema = ""
435+ self.host = ""
436+ self.port = DEFAULT_PORT
437+ self.endpoint = "/"
438+ self.method = method
439+ self._process_creds()
440+ self._parse_uri(uri)
441+ if not self.host:
442+ self.host = self.default_host
443+ if not self.schema:
444+ self.schema = self.default_schema
445+
446+ def _process_creds(self):
447+ # perform checks for access key
448+ if not self.access_key:
449+ self.access_key = os.environ.get(ENV_ACCESS_KEY)
450+ if not self.access_key:
451+ raise ValueError("Could not find %s" % ENV_ACCESS_KEY)
452+ # perform checks for secret key
453+ if not self.secret_key:
454+ self.secret_key = os.environ.get(ENV_SECRET_KEY)
455+ if not self.secret_key:
456+ raise ValueError("Could not find %s" % ENV_SECRET_KEY)
457+
458+ def _parse_uri(self, uri):
459+ scheme, host, port, endpoint = _parse(uri, defaultPort=DEFAULT_PORT)
460+ self.schema = scheme
461+ self.host = host
462+ self.port = port
463+ self.endpoint = endpoint
464+
465+ def get_uri(self):
466+ """Get a URL representation of the service."""
467+ uri = "%s://%s" % (self.schema, self.host)
468+ if self.port and self.port != DEFAULT_PORT:
469+ uri = "%s:%s" % (uri, self.port)
470+ return uri + self.endpoint
471+
472+ def sign(self, bytes):
473+ """Sign some bytes."""
474+ return hmac_sha1(self.secret_key, bytes)
475
476=== modified file 'txaws/storage/client.py'
477--- txaws/storage/client.py 2009-08-17 11:18:56 +0000
478+++ txaws/storage/client.py 2009-08-20 19:09:56 +0000
479@@ -10,178 +10,170 @@
480 from hashlib import md5
481 from base64 import b64encode
482
483-
484 from epsilon.extime import Time
485
486 from twisted.web.client import getPage
487 from twisted.web.http import datetimeToString
488
489-from txaws.credentials import AWSCredentials
490-from txaws.util import XML
491-
492-
493-def calculateMD5(data):
494- digest = md5(data).digest()
495- return b64encode(digest)
496+from txaws.util import XML, calculate_md5
497+from txaws.service import AWSService
498+
499+
500+name_space = '{http://s3.amazonaws.com/doc/2006-03-01/}'
501
502
503 class S3Request(object):
504- def __init__(self, verb, bucket=None, objectName=None, data='',
505- contentType=None, metadata={}, rootURI='https://s3.amazonaws.com',
506- creds=None):
507+
508+ def __init__(self, verb, bucket=None, object_name=None, data='',
509+ content_type=None, metadata={}, service=None):
510 self.verb = verb
511 self.bucket = bucket
512- self.objectName = objectName
513+ self.object_name = object_name
514 self.data = data
515- self.contentType = contentType
516+ self.content_type = content_type
517 self.metadata = metadata
518- self.rootURI = rootURI
519- self.creds = creds
520+ self.service = service
521+ self.service.endpoint = self.get_path()
522 self.date = datetimeToString()
523
524- def getURIPath(self):
525+ def get_path(self):
526 path = '/'
527 if self.bucket is not None:
528 path += self.bucket
529- if self.objectName is not None:
530- path += '/' + self.objectName
531+ if self.object_name is not None:
532+ path += '/' + self.object_name
533 return path
534
535- def getURI(self):
536- return self.rootURI + self.getURIPath()
537+ def get_uri(self):
538+ return self.service.get_uri()
539
540- def getHeaders(self):
541+ def get_headers(self):
542 headers = {'Content-Length': len(self.data),
543- 'Content-MD5': calculateMD5(self.data),
544+ 'Content-MD5': calculate_md5(self.data),
545 'Date': self.date}
546
547 for key, value in self.metadata.iteritems():
548 headers['x-amz-meta-' + key] = value
549
550- if self.contentType is not None:
551- headers['Content-Type'] = self.contentType
552+ if self.content_type is not None:
553+ headers['Content-Type'] = self.content_type
554
555- if self.creds is not None:
556- signature = self.getSignature(headers)
557+ if self.service is not None:
558+ signature = self.get_signature(headers)
559 headers['Authorization'] = 'AWS %s:%s' % (
560- self.creds.access_key, signature)
561-
562+ self.service.access_key, signature)
563 return headers
564
565- def getCanonicalizedResource(self):
566- return self.getURIPath()
567+ def get_canonicalized_resource(self):
568+ return self.get_path()
569
570- def getCanonicalizedAmzHeaders(self, headers):
571+ def get_canonicalized_amz_headers(self, headers):
572 result = ''
573 headers = [(name.lower(), value) for name, value in headers.iteritems()
574 if name.lower().startswith('x-amz-')]
575 headers.sort()
576 return ''.join('%s:%s\n' % (name, value) for name, value in headers)
577
578- def getSignature(self, headers):
579- text = self.verb + '\n'
580- text += headers.get('Content-MD5', '') + '\n'
581- text += headers.get('Content-Type', '') + '\n'
582- text += headers.get('Date', '') + '\n'
583- text += self.getCanonicalizedAmzHeaders(headers)
584- text += self.getCanonicalizedResource()
585- return self.creds.sign(text)
586+ def get_signature(self, headers):
587+ text = (self.verb + '\n' +
588+ headers.get('Content-MD5', '') + '\n' +
589+ headers.get('Content-Type', '') + '\n' +
590+ headers.get('Date', '') + '\n' +
591+ self.get_canonicalized_amz_headers(headers) +
592+ self.get_canonicalized_resource())
593+ return self.service.sign(text)
594
595 def submit(self):
596- return self.getPage(
597- url=self.getURI(), method=self.verb, postdata=self.data,
598- headers=self.getHeaders())
599+ return self.get_page(url=self.get_uri(), method=self.verb,
600+ postdata=self.data, headers=self.get_headers())
601
602- def getPage(self, *a, **kw):
603+ def get_page(self, *a, **kw):
604 return getPage(*a, **kw)
605
606
607-NS = '{http://s3.amazonaws.com/doc/2006-03-01/}'
608-
609-
610 class S3(object):
611- rootURI = 'https://s3.amazonaws.com/'
612- requestFactory = S3Request
613-
614- def __init__(self, creds):
615- self.creds = creds
616-
617- def makeRequest(self, *a, **kw):
618+
619+ request_factory = S3Request
620+
621+ def __init__(self, service):
622+ self.service = service
623+
624+ def make_request(self, *a, **kw):
625 """
626 Create a request with the arguments passed in.
627
628- This uses the requestFactory attribute, adding the credentials to the
629+ This uses the request_factory attribute, adding the service to the
630 arguments passed in.
631 """
632- return self.requestFactory(creds=self.creds, *a, **kw)
633+ return self.request_factory(service=self.service, *a, **kw)
634
635- def _parseBucketList(self, response):
636+ def _parse_bucket_list(self, response):
637 """
638 Parse XML bucket list response.
639 """
640 root = XML(response)
641- for bucket in root.find(NS + 'Buckets'):
642- timeText = bucket.findtext(NS + 'CreationDate')
643+ for bucket in root.find(name_space + 'Buckets'):
644+ timeText = bucket.findtext(name_space + 'CreationDate')
645 yield {
646- 'name': bucket.findtext(NS + 'Name'),
647+ 'name': bucket.findtext(name_space + 'Name'),
648 'created': Time.fromISO8601TimeAndDate(timeText),
649 }
650
651- def listBuckets(self):
652+ def list_buckets(self):
653 """
654 List all buckets.
655
656 Returns a list of all the buckets owned by the authenticated sender of
657 the request.
658 """
659- d = self.makeRequest('GET').submit()
660- d.addCallback(self._parseBucketList)
661- return d
662+ deferred = self.make_request('GET').submit()
663+ deferred.addCallback(self._parse_bucket_list)
664+ return deferred
665
666- def createBucket(self, bucket):
667+ def create_bucket(self, bucket):
668 """
669 Create a new bucket.
670 """
671- return self.makeRequest('PUT', bucket).submit()
672+ return self.make_request('PUT', bucket).submit()
673
674- def deleteBucket(self, bucket):
675+ def delete_bucket(self, bucket):
676 """
677 Delete a bucket.
678
679 The bucket must be empty before it can be deleted.
680 """
681- return self.makeRequest('DELETE', bucket).submit()
682+ return self.make_request('DELETE', bucket).submit()
683
684- def putObject(self, bucket, objectName, data, contentType=None,
685- metadata={}):
686+ def put_object(self, bucket, object_name, data, content_type=None,
687+ metadata={}):
688 """
689 Put an object in a bucket.
690
691 Any existing object of the same name will be replaced.
692 """
693- return self.makeRequest(
694- 'PUT', bucket, objectName, data, contentType, metadata).submit()
695+ return self.make_request('PUT', bucket, object_name, data,
696+ content_type, metadata).submit()
697
698- def getObject(self, bucket, objectName):
699+ def get_object(self, bucket, object_name):
700 """
701 Get an object from a bucket.
702 """
703- return self.makeRequest('GET', bucket, objectName).submit()
704+ return self.make_request('GET', bucket, object_name).submit()
705
706- def headObject(self, bucket, objectName):
707+ def head_object(self, bucket, object_name):
708 """
709 Retrieve object metadata only.
710
711- This is like getObject, but the object's content is not retrieved.
712+ This is like get_object, but the object's content is not retrieved.
713 Currently the metadata is not returned to the caller either, so this
714 method is mostly useless, and only provided for completeness.
715 """
716- return self.makeRequest('HEAD', bucket, objectName).submit()
717+ return self.make_request('HEAD', bucket, object_name).submit()
718
719- def deleteObject(self, bucket, objectName):
720+ def delete_object(self, bucket, object_name):
721 """
722 Delete an object from a bucket.
723
724 Once deleted, there is no method to restore or undelete an object.
725 """
726- return self.makeRequest('DELETE', bucket, objectName).submit()
727+ return self.make_request('DELETE', bucket, object_name).submit()
728
729=== added file 'txaws/storage/service.py'
730--- txaws/storage/service.py 1970-01-01 00:00:00 +0000
731+++ txaws/storage/service.py 2009-08-20 19:09:56 +0000
732@@ -0,0 +1,19 @@
733+# Copyright (C) 2009 Duncan McGreggor <duncan@canonical.com>
734+# Licenced under the txaws licence available at /LICENSE in the txaws source.
735+
736+from txaws.service import AWSService
737+
738+
739+S3_HOST = "s3.amazonaws.com"
740+
741+
742+class S3Service(AWSService):
743+ """
744+ This service uses the standard S3 host defined with S3_HOST by default. To
745+ override this behaviour, simply pass the desired value in the "host"
746+ keyword parameter.
747+
748+ For more details, see txaws.service.AWSService.
749+ """
750+ default_host = S3_HOST
751+ default_schema = "https"
752
753=== modified file 'txaws/storage/test/test_client.py'
754--- txaws/storage/test/test_client.py 2009-08-15 03:28:45 +0000
755+++ txaws/storage/test/test_client.py 2009-08-20 19:09:56 +0000
756@@ -4,20 +4,23 @@
757
758 from twisted.internet.defer import succeed
759
760-from txaws.credentials import AWSCredentials
761+from txaws.util import calculate_md5
762 from txaws.tests import TXAWSTestCase
763-from txaws.storage.client import S3, S3Request, calculateMD5
764+from txaws.storage.service import S3Service
765+from txaws.storage.client import S3, S3Request
766+
767
768
769 class StubbedS3Request(S3Request):
770- def getPage(self, url, method, postdata, headers):
771+
772+ def get_page(self, url, method, postdata, headers):
773 self.getPageArgs = (url, method, postdata, headers)
774 return succeed('')
775
776
777-class RequestTests(TXAWSTestCase):
778- creds = AWSCredentials(access_key='0PN5J17HBGZHT7JJ3X82',
779- secret_key='uV3F3YluFJax1cknvbcGwgjvx4QpvB+leU8dUj2o')
780+class RequestTestCase(TXAWSTestCase):
781+
782+ service = S3Service(access_key='fookeyid', secret_key='barsecretkey')
783
784 def test_objectRequest(self):
785 """
786@@ -26,20 +29,23 @@
787 DATA = 'objectData'
788 DIGEST = 'zhdB6gwvocWv/ourYUWMxA=='
789
790- request = S3Request(
791- 'PUT', 'somebucket', 'object/name/here', DATA,
792- contentType='text/plain', metadata={'foo': 'bar'})
793+ request = S3Request('PUT', 'somebucket', 'object/name/here', DATA,
794+ content_type='text/plain', metadata={'foo': 'bar'},
795+ service=self.service)
796+ request.get_signature = lambda headers: "TESTINGSIG="
797 self.assertEqual(request.verb, 'PUT')
798 self.assertEqual(
799- request.getURI(),
800+ request.get_uri(),
801 'https://s3.amazonaws.com/somebucket/object/name/here')
802- headers = request.getHeaders()
803+ headers = request.get_headers()
804 self.assertNotEqual(headers.pop('Date'), '')
805- self.assertEqual(headers,
806- {'Content-Type': 'text/plain',
807- 'Content-Length': len(DATA),
808- 'Content-MD5': DIGEST,
809- 'x-amz-meta-foo': 'bar'})
810+ self.assertEqual(
811+ headers, {
812+ 'Authorization': 'AWS fookeyid:TESTINGSIG=',
813+ 'Content-Type': 'text/plain',
814+ 'Content-Length': len(DATA),
815+ 'Content-MD5': DIGEST,
816+ 'x-amz-meta-foo': 'bar'})
817 self.assertEqual(request.data, 'objectData')
818
819 def test_bucketRequest(self):
820@@ -48,42 +54,46 @@
821 """
822 DIGEST = '1B2M2Y8AsgTpgAmY7PhCfg=='
823
824- request = S3Request('GET', 'somebucket')
825+ request = S3Request('GET', 'somebucket', service=self.service)
826+ request.get_signature = lambda headers: "TESTINGSIG="
827 self.assertEqual(request.verb, 'GET')
828 self.assertEqual(
829- request.getURI(), 'https://s3.amazonaws.com/somebucket')
830- headers = request.getHeaders()
831+ request.get_uri(), 'https://s3.amazonaws.com/somebucket')
832+ headers = request.get_headers()
833 self.assertNotEqual(headers.pop('Date'), '')
834- self.assertEqual(headers,
835- {'Content-Length': 0,
836- 'Content-MD5': DIGEST})
837+ self.assertEqual(
838+ headers, {
839+ 'Authorization': 'AWS fookeyid:TESTINGSIG=',
840+ 'Content-Length': 0,
841+ 'Content-MD5': DIGEST})
842 self.assertEqual(request.data, '')
843
844 def test_submit(self):
845 """
846 Submitting the request should invoke getPage correctly.
847 """
848- request = StubbedS3Request('GET', 'somebucket')
849+ request = StubbedS3Request('GET', 'somebucket', service=self.service)
850
851 def _postCheck(result):
852 self.assertEqual(result, '')
853
854 url, method, postdata, headers = request.getPageArgs
855- self.assertEqual(url, request.getURI())
856+ self.assertEqual(url, request.get_uri())
857 self.assertEqual(method, request.verb)
858 self.assertEqual(postdata, request.data)
859- self.assertEqual(headers, request.getHeaders())
860+ self.assertEqual(headers, request.get_headers())
861
862 return request.submit().addCallback(_postCheck)
863
864 def test_authenticationTestCases(self):
865- req = S3Request('GET', creds=self.creds)
866- req.date = 'Wed, 28 Mar 2007 01:29:59 +0000'
867+ request = S3Request('GET', service=self.service)
868+ request.get_signature = lambda headers: "TESTINGSIG="
869+ request.date = 'Wed, 28 Mar 2007 01:29:59 +0000'
870
871- headers = req.getHeaders()
872+ headers = request.get_headers()
873 self.assertEqual(
874- headers['Authorization'],
875- 'AWS 0PN5J17HBGZHT7JJ3X82:jF7L3z/FTV47vagZzhKupJ9oNig=')
876+ headers['Authorization'],
877+ 'AWS fookeyid:TESTINGSIG=')
878
879
880 class InertRequest(S3Request):
881@@ -110,13 +120,13 @@
882 """
883 Testable version of S3.
884
885- This subclass stubs requestFactory to use InertRequest, making it easy to
886+ This subclass stubs request_factory to use InertRequest, making it easy to
887 assert things about the requests that are created in response to various
888 operations.
889 """
890 response = None
891
892- def requestFactory(self, *a, **kw):
893+ def request_factory(self, *a, **kw):
894 req = InertRequest(response=self.response, *a, **kw)
895 self._lastRequest = req
896 return req
897@@ -148,34 +158,34 @@
898
899 def setUp(self):
900 TXAWSTestCase.setUp(self)
901- self.creds = AWSCredentials(
902+ self.service = S3Service(
903 access_key='accessKey', secret_key='secretKey')
904- self.s3 = TestableS3(creds=self.creds)
905+ self.s3 = TestableS3(service=self.service)
906
907- def test_makeRequest(self):
908+ def test_make_request(self):
909 """
910- Test that makeRequest passes in the service credentials.
911+ Test that make_request passes in the service object.
912 """
913 marker = object()
914
915 def _cb(*a, **kw):
916- self.assertEqual(kw['creds'], self.creds)
917+ self.assertEqual(kw['service'], self.service)
918 return marker
919
920- self.s3.requestFactory = _cb
921- self.assertIdentical(self.s3.makeRequest('GET'), marker)
922+ self.s3.request_factory = _cb
923+ self.assertIdentical(self.s3.make_request('GET'), marker)
924
925- def test_listBuckets(self):
926+ def test_list_buckets(self):
927 self.s3.response = samples['ListAllMyBucketsResult']
928- d = self.s3.listBuckets()
929+ d = self.s3.list_buckets()
930
931 req = self.s3._lastRequest
932 self.assertTrue(req.submitted)
933 self.assertEqual(req.verb, 'GET')
934 self.assertEqual(req.bucket, None)
935- self.assertEqual(req.objectName, None)
936+ self.assertEqual(req.object_name, None)
937
938- def _checkResult(buckets):
939+ def _check_result(buckets):
940 self.assertEqual(
941 list(buckets),
942 [{'name': u'quotes',
943@@ -184,61 +194,61 @@
944 {'name': u'samples',
945 'created': Time.fromDatetime(
946 datetime(2006, 2, 3, 16, 41, 58))}])
947- return d.addCallback(_checkResult)
948+ return d.addCallback(_check_result)
949
950- def test_createBucket(self):
951- self.s3.createBucket('foo')
952+ def test_create_bucket(self):
953+ self.s3.create_bucket('foo')
954 req = self.s3._lastRequest
955 self.assertTrue(req.submitted)
956 self.assertEqual(req.verb, 'PUT')
957 self.assertEqual(req.bucket, 'foo')
958- self.assertEqual(req.objectName, None)
959+ self.assertEqual(req.object_name, None)
960
961- def test_deleteBucket(self):
962- self.s3.deleteBucket('foo')
963+ def test_delete_bucket(self):
964+ self.s3.delete_bucket('foo')
965 req = self.s3._lastRequest
966 self.assertTrue(req.submitted)
967 self.assertEqual(req.verb, 'DELETE')
968 self.assertEqual(req.bucket, 'foo')
969- self.assertEqual(req.objectName, None)
970+ self.assertEqual(req.object_name, None)
971
972- def test_putObject(self):
973- self.s3.putObject(
974+ def test_put_object(self):
975+ self.s3.put_object(
976 'foobucket', 'foo', 'data', 'text/plain', {'foo': 'bar'})
977 req = self.s3._lastRequest
978 self.assertTrue(req.submitted)
979 self.assertEqual(req.verb, 'PUT')
980 self.assertEqual(req.bucket, 'foobucket')
981- self.assertEqual(req.objectName, 'foo')
982+ self.assertEqual(req.object_name, 'foo')
983 self.assertEqual(req.data, 'data')
984- self.assertEqual(req.contentType, 'text/plain')
985+ self.assertEqual(req.content_type, 'text/plain')
986 self.assertEqual(req.metadata, {'foo': 'bar'})
987
988- def test_getObject(self):
989- self.s3.getObject('foobucket', 'foo')
990+ def test_get_object(self):
991+ self.s3.get_object('foobucket', 'foo')
992 req = self.s3._lastRequest
993 self.assertTrue(req.submitted)
994 self.assertEqual(req.verb, 'GET')
995 self.assertEqual(req.bucket, 'foobucket')
996- self.assertEqual(req.objectName, 'foo')
997+ self.assertEqual(req.object_name, 'foo')
998
999- def test_headObject(self):
1000- self.s3.headObject('foobucket', 'foo')
1001+ def test_head_object(self):
1002+ self.s3.head_object('foobucket', 'foo')
1003 req = self.s3._lastRequest
1004 self.assertTrue(req.submitted)
1005 self.assertEqual(req.verb, 'HEAD')
1006 self.assertEqual(req.bucket, 'foobucket')
1007- self.assertEqual(req.objectName, 'foo')
1008+ self.assertEqual(req.object_name, 'foo')
1009
1010- def test_deleteObject(self):
1011- self.s3.deleteObject('foobucket', 'foo')
1012+ def test_delete_object(self):
1013+ self.s3.delete_object('foobucket', 'foo')
1014 req = self.s3._lastRequest
1015 self.assertTrue(req.submitted)
1016 self.assertEqual(req.verb, 'DELETE')
1017 self.assertEqual(req.bucket, 'foobucket')
1018- self.assertEqual(req.objectName, 'foo')
1019+ self.assertEqual(req.object_name, 'foo')
1020
1021
1022 class MiscellaneousTests(TXAWSTestCase):
1023 def test_contentMD5(self):
1024- self.assertEqual(calculateMD5('somedata'), 'rvr3UC1SmUw7AZV2NqPN0g==')
1025+ self.assertEqual(calculate_md5('somedata'), 'rvr3UC1SmUw7AZV2NqPN0g==')
1026
1027=== removed file 'txaws/tests/test_credentials.py'
1028--- txaws/tests/test_credentials.py 2009-08-17 11:18:56 +0000
1029+++ txaws/tests/test_credentials.py 1970-01-01 00:00:00 +0000
1030@@ -1,41 +0,0 @@
1031-# Copyright (C) 2009 Robert Collins <robertc@robertcollins.net>
1032-# Licenced under the txaws licence available at /LICENSE in the txaws source.
1033-
1034-import os
1035-
1036-from twisted.trial.unittest import TestCase
1037-from txaws.tests import TXAWSTestCase
1038-
1039-from txaws.credentials import AWSCredentials
1040-
1041-
1042-class TestCredentials(TXAWSTestCase):
1043-
1044- def test_no_access_errors(self):
1045- # Without anything in os.environ, AWSCredentials() blows up
1046- os.environ['AWS_SECRET_ACCESS_KEY'] = 'foo'
1047- self.assertRaises(Exception, AWSCredentials)
1048-
1049- def test_no_secret_errors(self):
1050- # Without anything in os.environ, AWSCredentials() blows up
1051- os.environ['AWS_ACCESS_KEY_ID'] = 'bar'
1052- self.assertRaises(Exception, AWSCredentials)
1053-
1054- def test_found_values_used(self):
1055- os.environ['AWS_SECRET_ACCESS_KEY'] = 'foo'
1056- os.environ['AWS_ACCESS_KEY_ID'] = 'bar'
1057- creds = AWSCredentials()
1058- self.assertEqual('foo', creds.secret_key)
1059- self.assertEqual('bar', creds.access_key)
1060-
1061- def test_explicit_access_key(self):
1062- os.environ['AWS_SECRET_ACCESS_KEY'] = 'foo'
1063- creds = AWSCredentials(access_key='bar')
1064- self.assertEqual('foo', creds.secret_key)
1065- self.assertEqual('bar', creds.access_key)
1066-
1067- def test_explicit_secret_key(self):
1068- os.environ['AWS_ACCESS_KEY_ID'] = 'bar'
1069- creds = AWSCredentials(secret_key='foo')
1070- self.assertEqual('foo', creds.secret_key)
1071- self.assertEqual('bar', creds.access_key)
1072
1073=== added file 'txaws/tests/test_service.py'
1074--- txaws/tests/test_service.py 1970-01-01 00:00:00 +0000
1075+++ txaws/tests/test_service.py 2009-08-20 19:09:56 +0000
1076@@ -0,0 +1,88 @@
1077+# Copyright (C) 2009 Duncan McGreggor <duncan@canonical.com>
1078+# Licenced under the txaws licence available at /LICENSE in the txaws source.
1079+
1080+import os
1081+
1082+from txaws.service import AWSService, ENV_ACCESS_KEY, ENV_SECRET_KEY
1083+from txaws.tests import TXAWSTestCase
1084+
1085+
1086+class AWSServiceTestCase(TXAWSTestCase):
1087+
1088+ def setUp(self):
1089+ self.service = AWSService("fookeyid", "barsecretkey",
1090+ "http://my.service/da_endpoint")
1091+ self.addCleanup(self.clean_environment)
1092+
1093+ def clean_environment(self):
1094+ if os.environ.has_key(ENV_ACCESS_KEY):
1095+ del os.environ[ENV_ACCESS_KEY]
1096+ if os.environ.has_key(ENV_SECRET_KEY):
1097+ del os.environ[ENV_SECRET_KEY]
1098+
1099+ def test_simple_creation(self):
1100+ service = AWSService("fookeyid", "barsecretkey")
1101+ self.assertEquals(service.access_key, "fookeyid")
1102+ self.assertEquals(service.secret_key, "barsecretkey")
1103+ self.assertEquals(service.schema, "https")
1104+ self.assertEquals(service.host, "")
1105+ self.assertEquals(service.port, 80)
1106+ self.assertEquals(service.endpoint, "/")
1107+ self.assertEquals(service.method, "GET")
1108+
1109+ def test_no_access_errors(self):
1110+ # Without anything in os.environ, AWSService() blows up
1111+ os.environ[ENV_SECRET_KEY] = "bar"
1112+ self.assertRaises(ValueError, AWSService)
1113+
1114+ def test_no_secret_errors(self):
1115+ # Without anything in os.environ, AWSService() blows up
1116+ os.environ[ENV_ACCESS_KEY] = "foo"
1117+ self.assertRaises(ValueError, AWSService)
1118+
1119+ def test_found_values_used(self):
1120+ os.environ[ENV_ACCESS_KEY] = "foo"
1121+ os.environ[ENV_SECRET_KEY] = "bar"
1122+ service = AWSService()
1123+ self.assertEqual("foo", service.access_key)
1124+ self.assertEqual("bar", service.secret_key)
1125+ self.clean_environment()
1126+
1127+ def test_explicit_access_key(self):
1128+ os.environ[ENV_SECRET_KEY] = "foo"
1129+ service = AWSService(access_key="bar")
1130+ self.assertEqual("foo", service.secret_key)
1131+ self.assertEqual("bar", service.access_key)
1132+
1133+ def test_explicit_secret_key(self):
1134+ os.environ[ENV_ACCESS_KEY] = "bar"
1135+ service = AWSService(secret_key="foo")
1136+ self.assertEqual("foo", service.secret_key)
1137+ self.assertEqual("bar", service.access_key)
1138+
1139+ def test_parse_uri(self):
1140+ self.assertEquals(self.service.schema, "http")
1141+ self.assertEquals(self.service.host, "my.service")
1142+ self.assertEquals(self.service.port, 80)
1143+ self.assertEquals(self.service.endpoint, "/da_endpoint")
1144+
1145+ def test_parse_uri_https_and_custom_port(self):
1146+ service = AWSService("foo", "bar", "https://my.service:8080/endpoint")
1147+ self.assertEquals(service.schema, "https")
1148+ self.assertEquals(service.host, "my.service")
1149+ self.assertEquals(service.port, 8080)
1150+ self.assertEquals(service.endpoint, "/endpoint")
1151+
1152+ def test_custom_method(self):
1153+ service = AWSService("foo", "bar", "http://service/endpoint", "PUT")
1154+ self.assertEquals(service.method, "PUT")
1155+
1156+ def test_get_uri(self):
1157+ uri = self.service.get_uri()
1158+ self.assertEquals(uri, "http://my.service/da_endpoint")
1159+
1160+ def test_get_uri_custom_port(self):
1161+ uri = "https://my.service:8080/endpoint"
1162+ service = AWSService("foo", "bar", uri)
1163+ new_uri = service.get_uri()
1164+ self.assertEquals(new_uri, uri)
1165
1166=== modified file 'txaws/util.py'
1167--- txaws/util.py 2009-08-17 11:18:56 +0000
1168+++ txaws/util.py 2009-08-18 20:48:59 +0000
1169@@ -4,10 +4,10 @@
1170 services.
1171 """
1172
1173+import time
1174+import hmac
1175+from hashlib import sha1, md5
1176 from base64 import b64encode
1177-from hashlib import sha1
1178-import hmac
1179-import time
1180
1181 # Import XML from somwhere; here in one place to prevent duplication.
1182 try:
1183@@ -19,6 +19,11 @@
1184 __all__ = ['hmac_sha1', 'iso8601time']
1185
1186
1187+def calculate_md5(data):
1188+ digest = md5(data).digest()
1189+ return b64encode(digest)
1190+
1191+
1192 def hmac_sha1(secret, data):
1193 digest = hmac.new(secret, data, sha1).digest()
1194 return b64encode(digest)

Subscribers

People subscribed via source and target branches