Merge lp:~oubiwann/txaws/416109-arbitrary-endpoints into lp:~txawsteam/txaws/trunk
- 416109-arbitrary-endpoints
- Merge into trunk
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 | ||||
Related bugs: |
|
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.
Commit message
Description of the change
Duncan McGreggor (oubiwann) wrote : | # |
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_
At runtime then, one would ask a region for a client of a particular
service, using some credentials.
AWS_US1.
AWS_US1.
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_
return EC2Client(
Also a bit of detail review - 'default_schema = https' - in URL terms
(see http://
_schema_.
review needsfixing
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_
>
> At runtime then, one would ask a region for a client of a particular
> service, using some credentials.
>
> AWS_US1.
> AWS_US1.
>
> 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_
> return EC2Client(
>
> Also a bit of detail review - 'default_schema = https' - in URL terms
> (see http://
> _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.
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_
>
> At runtime then, one would ask a region for a client of a particular
> service, using some credentials.
>
> AWS_US1.
> AWS_US1.
>
> 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_
> return EC2Client(
[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://
> _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.
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
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) |
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).