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

Proposed by Robert Collins
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

This proposal supersedes a proposal from 2009-08-20.

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

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

This branch adds 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 : Posted in a previous version of this proposal

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 : Posted in a previous version of this proposal

> 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.

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

> 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.

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

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

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

On Sun, 2009-08-23 at 21:30 +0000, Robert Collins wrote:
> === modified file 'txaws/client/gui/gtk.py'
> --- txaws/client/gui/gtk.py 2009-08-18 22:53:53 +0000
> +++ txaws/client/gui/gtk.py 2009-08-20 19:14:32 +0000
> @@ -8,7 +8,7 @@
> import gobject
> import gtk
>
> -from txaws.credentials import AWSCredentials
> +from txaws.ec2.service import EC2Service
>
>
> __all__ = ['main']
> @@ -27,10 +27,10 @@
> # Nested import because otherwise we get 'reactor already
> installed'.
> self.password_dialog = None
> try:
> - creds = AWSCredentials()
> + service = AWSService()

This is going to be a NameError :P

...
> === added file 'txaws/credentials.py'

this file already exists. I think you've done something weird in your
branch. lets review once thats fixed. Find me in #bzr :P

review needsfixing

--

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=== added file 'txaws/credentials.py'
72--- txaws/credentials.py 1970-01-01 00:00:00 +0000
73+++ txaws/credentials.py 2009-08-21 14:50:25 +0000
74@@ -0,0 +1,42 @@
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 hmac_sha1
83+
84+
85+__all__ = ['AWSCredentials']
86+
87+
88+ENV_ACCESS_KEY = "AWS_ACCESS_KEY_ID"
89+ENV_SECRET_KEY = "AWS_SECRET_ACCESS_KEY"
90+
91+
92+class AWSCredentials(object):
93+
94+ def __init__(self, access_key="", secret_key=""):
95+ """Create an AWSCredentials object.
96+
97+ @param access_key: The access key to use. If None the environment
98+ variable AWS_ACCESS_KEY_ID is consulted.
99+ @param secret_key: The secret key to use. If None the environment
100+ variable AWS_SECRET_ACCESS_KEY is consulted.
101+ """
102+ self.access_key = access_key
103+ self.secret_key = secret_key
104+ if not self.access_key:
105+ self.access_key = os.environ.get(ENV_ACCESS_KEY)
106+ if not self.access_key:
107+ raise ValueError("Could not find %s" % ENV_ACCESS_KEY)
108+ # perform checks for secret key
109+ if not self.secret_key:
110+ self.secret_key = os.environ.get(ENV_SECRET_KEY)
111+ if not self.secret_key:
112+ raise ValueError("Could not find %s" % ENV_SECRET_KEY)
113+
114+ def sign(self, bytes):
115+ """Sign some bytes."""
116+ return hmac_sha1(self.secret_key, bytes)
117
118=== removed file 'txaws/credentials.py'
119--- txaws/credentials.py 2009-08-17 11:18:56 +0000
120+++ txaws/credentials.py 1970-01-01 00:00:00 +0000
121@@ -1,37 +0,0 @@
122-# Copyright (C) 2009 Robert Collins <robertc@robertcollins.net>
123-# Licenced under the txaws licence available at /LICENSE in the txaws source.
124-
125-"""Credentials for accessing AWS services."""
126-
127-import os
128-
129-from txaws.util import *
130-
131-
132-__all__ = ['AWSCredentials']
133-
134-
135-class AWSCredentials(object):
136-
137- def __init__(self, access_key=None, secret_key=None):
138- """Create an AWSCredentials object.
139-
140- :param access_key: The access key to use. If None the environment
141- variable AWS_ACCESS_KEY_ID is consulted.
142- :param secret_key: The secret key to use. If None the environment
143- variable AWS_SECRET_ACCESS_KEY is consulted.
144- """
145- self.secret_key = secret_key
146- if self.secret_key is None:
147- self.secret_key = os.environ.get('AWS_SECRET_ACCESS_KEY')
148- if self.secret_key is None:
149- raise ValueError('Could not find AWS_SECRET_ACCESS_KEY')
150- self.access_key = access_key
151- if self.access_key is None:
152- self.access_key = os.environ.get('AWS_ACCESS_KEY_ID')
153- if self.access_key is None:
154- raise ValueError('Could not find AWS_ACCESS_KEY_ID')
155-
156- def sign(self, bytes):
157- """Sign some bytes."""
158- return hmac_sha1(self.secret_key, bytes)
159
160=== modified file 'txaws/ec2/client.py'
161--- txaws/ec2/client.py 2009-08-21 03:26:35 +0000
162+++ txaws/ec2/client.py 2009-08-21 14:50:25 +0000
163@@ -8,7 +8,8 @@
164
165 from twisted.web.client import getPage
166
167-from txaws import credentials
168+from txaws.credentials import AWSCredentials
169+from txaws.service import AWSServiceEndpoint
170 from txaws.util import iso8601time, XML
171
172
173@@ -77,16 +78,16 @@
174
175 name_space = '{http://ec2.amazonaws.com/doc/2008-12-01/}'
176
177- def __init__(self, creds=None, query_factory=None):
178+ def __init__(self, creds=None, endpoint=None, query_factory=None):
179 """Create an EC2Client.
180
181- @param creds: Explicit credentials to use. If None, credentials are
182- inferred as per txaws.credentials.AWSCredentials.
183+ @param creds: User authentication credentials to use.
184+ @param endpoint: The service URI.
185+ @param query_factory: The class or function that produces a query
186+ object for making requests to the EC2 service.
187 """
188- if creds is None:
189- self.creds = credentials.AWSCredentials()
190- else:
191- self.creds = creds
192+ self.creds = creds or AWSCredentials()
193+ self.endpoint = endpoint or AWSServiceEndpoint()
194 if query_factory is None:
195 self.query_factory = Query
196 else:
197@@ -177,7 +178,8 @@
198 instanceset = {}
199 for pos, instance_id in enumerate(instance_ids):
200 instanceset["InstanceId.%d" % (pos+1)] = instance_id
201- q = self.query_factory('TerminateInstances', self.creds, instanceset)
202+ q = self.query_factory('TerminateInstances', self.creds, self.endpoint,
203+ instanceset)
204 d = q.submit()
205 return d.addCallback(self._parse_terminate_instances)
206
207@@ -200,24 +202,24 @@
208 class Query(object):
209 """A query that may be submitted to EC2."""
210
211- def __init__(self, action, creds, other_params=None, time_tuple=None):
212+ def __init__(self, action, creds, endpoint, other_params=None,
213+ time_tuple=None):
214 """Create a Query to submit to EC2."""
215+ self.creds = creds
216+ self.endpoint = endpoint
217 # Require params (2008-12-01 API):
218 # Version, SignatureVersion, SignatureMethod, Action, AWSAccessKeyId,
219 # Timestamp || Expires, Signature,
220- self.params = {'Version': '2008-12-01',
221+ self.params = {
222+ 'Version': '2008-12-01',
223 'SignatureVersion': '2',
224 'SignatureMethod': 'HmacSHA1',
225 'Action': action,
226- 'AWSAccessKeyId': creds.access_key,
227+ 'AWSAccessKeyId': self.creds.access_key,
228 'Timestamp': iso8601time(time_tuple),
229 }
230 if other_params:
231 self.params.update(other_params)
232- self.method = 'GET'
233- self.host = 'ec2.amazonaws.com'
234- self.uri = '/'
235- self.creds = creds
236
237 def canonical_query_params(self):
238 """Return the canonical query params (used in signing)."""
239@@ -230,18 +232,19 @@
240 """Encode a_string as per the canonicalisation encoding rules.
241
242 See the AWS dev reference page 90 (2008-12-01 version).
243- :return: a_string encoded.
244+ @return: a_string encoded.
245 """
246 return quote(a_string, safe='~')
247
248 def signing_text(self):
249 """Return the text to be signed when signing the query."""
250- result = "%s\n%s\n%s\n%s" % (self.method, self.host, self.uri,
251- self.canonical_query_params())
252+ result = "%s\n%s\n%s\n%s" % (self.endpoint.method, self.endpoint.host,
253+ self.endpoint.path,
254+ self.canonical_query_params())
255 return result
256
257 def sign(self):
258- """Sign this query using its built in credentials.
259+ """Sign this query using its built in service.
260
261 This prepares it to be sent, and should be done as the last step before
262 submitting the query. Signing is done automatically - this is a public
263@@ -256,9 +259,9 @@
264 def submit(self):
265 """Submit this query.
266
267- :return: A deferred from twisted.web.client.getPage
268+ @return: A deferred from twisted.web.client.getPage
269 """
270 self.sign()
271- url = 'http://%s%s?%s' % (self.host, self.uri,
272- self.canonical_query_params())
273- return getPage(url, method=self.method)
274+ url = "%s?%s" % (self.endpoint.get_uri(),
275+ self.canonical_query_params())
276+ return getPage(url, method=self.service.method)
277
278=== modified file 'txaws/ec2/tests/test_client.py'
279--- txaws/ec2/tests/test_client.py 2009-08-21 03:26:35 +0000
280+++ txaws/ec2/tests/test_client.py 2009-08-21 14:50:25 +0000
281@@ -7,6 +7,7 @@
282
283 from txaws.credentials import AWSCredentials
284 from txaws.ec2 import client
285+from txaws.service import AWSServiceEndpoint, EC2_ENDPOINT_US
286 from txaws.tests import TXAWSTestCase
287
288
289@@ -117,7 +118,7 @@
290 self.assertEquals(instance.ramdisk_id, "id4")
291
292
293-class TestEC2Client(TXAWSTestCase):
294+class EC2ClientTestCase(TXAWSTestCase):
295
296 def test_init_no_creds(self):
297 os.environ['AWS_SECRET_ACCESS_KEY'] = 'foo'
298@@ -129,7 +130,7 @@
299 self.assertRaises(ValueError, client.EC2Client)
300
301 def test_init_explicit_creds(self):
302- creds = 'foo'
303+ creds = AWSCredentials("foo", "bar")
304 ec2 = client.EC2Client(creds=creds)
305 self.assertEqual(creds, ec2.creds)
306
307@@ -162,7 +163,8 @@
308
309
310 def test_parse_reservation(self):
311- ec2 = client.EC2Client(creds='foo')
312+ creds = AWSCredentials("foo", "bar")
313+ ec2 = client.EC2Client(creds=creds)
314 results = ec2._parse_instances(sample_describe_instances_result)
315 self.check_parsed_instances(results)
316
317@@ -170,25 +172,31 @@
318 class StubQuery(object):
319 def __init__(stub, action, creds):
320 self.assertEqual(action, 'DescribeInstances')
321- self.assertEqual('foo', creds)
322+ self.assertEqual(creds.access_key, "foo")
323+ self.assertEqual(creds.secret_key, "bar")
324 def submit(self):
325 return succeed(sample_describe_instances_result)
326- ec2 = client.EC2Client(creds='foo', query_factory=StubQuery)
327+ creds = AWSCredentials("foo", "bar")
328+ ec2 = client.EC2Client(creds, query_factory=StubQuery)
329 d = ec2.describe_instances()
330 d.addCallback(self.check_parsed_instances)
331 return d
332
333 def test_terminate_instances(self):
334 class StubQuery(object):
335- def __init__(stub, action, creds, other_params):
336+ def __init__(stub, action, creds, endpoint, other_params):
337 self.assertEqual(action, 'TerminateInstances')
338- self.assertEqual('foo', creds)
339+ self.assertEqual(creds.access_key, "foo")
340+ self.assertEqual(creds.secret_key, "bar")
341 self.assertEqual(
342 {'InstanceId.1': 'i-1234', 'InstanceId.2': 'i-5678'},
343 other_params)
344 def submit(self):
345 return succeed(sample_terminate_instances_result)
346- ec2 = client.EC2Client(creds='foo', query_factory=StubQuery)
347+ creds = AWSCredentials("foo", "bar")
348+ endpoint = AWSServiceEndpoint(uri=EC2_ENDPOINT_US)
349+ ec2 = client.EC2Client(creds=creds, endpoint=endpoint,
350+ query_factory=StubQuery)
351 d = ec2.terminate_instances('i-1234', 'i-5678')
352 def check_transition(changes):
353 self.assertEqual([('i-1234', 'running', 'shutting-down'),
354@@ -196,14 +204,15 @@
355 return d
356
357
358-class TestQuery(TXAWSTestCase):
359+class QueryTestCase(TXAWSTestCase):
360
361 def setUp(self):
362 TXAWSTestCase.setUp(self)
363 self.creds = AWSCredentials('foo', 'bar')
364+ self.endpoint = AWSServiceEndpoint(uri=EC2_ENDPOINT_US)
365
366 def test_init_minimum(self):
367- query = client.Query('DescribeInstances', self.creds)
368+ query = client.Query('DescribeInstances', self.creds, self.endpoint)
369 self.assertTrue('Timestamp' in query.params)
370 del query.params['Timestamp']
371 self.assertEqual(
372@@ -221,7 +230,7 @@
373 self.assertRaises(TypeError, client.Query, None)
374
375 def test_init_other_args_are_params(self):
376- query = client.Query('DescribeInstances', self.creds,
377+ query = client.Query('DescribeInstances', self.creds, self.endpoint,
378 {'InstanceId.0': '12345'},
379 time_tuple=(2007,11,12,13,14,15,0,0,0))
380 self.assertEqual(
381@@ -235,7 +244,7 @@
382 query.params)
383
384 def test_sorted_params(self):
385- query = client.Query('DescribeInstances', self.creds,
386+ query = client.Query('DescribeInstances', self.creds, self.endpoint,
387 {'fun': 'games'},
388 time_tuple=(2007,11,12,13,14,15,0,0,0))
389 self.assertEqual([
390@@ -251,16 +260,16 @@
391 def test_encode_unreserved(self):
392 all_unreserved = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ'
393 'abcdefghijklmnopqrstuvwxyz0123456789-_.~')
394- query = client.Query('DescribeInstances', self.creds)
395+ query = client.Query('DescribeInstances', self.creds, self.endpoint)
396 self.assertEqual(all_unreserved, query.encode(all_unreserved))
397
398 def test_encode_space(self):
399 """This may be just 'url encode', but the AWS manual isn't clear."""
400- query = client.Query('DescribeInstances', self.creds)
401+ query = client.Query('DescribeInstances', self.creds, self.endpoint)
402 self.assertEqual('a%20space', query.encode('a space'))
403
404 def test_canonical_query(self):
405- query = client.Query('DescribeInstances', self.creds,
406+ query = client.Query('DescribeInstances', self.creds, self.endpoint,
407 {'fu n': 'g/ames', 'argwithnovalue':'',
408 'InstanceId.1': 'i-1234'},
409 time_tuple=(2007,11,12,13,14,15,0,0,0))
410@@ -272,17 +281,17 @@
411 self.assertEqual(expected_query, query.canonical_query_params())
412
413 def test_signing_text(self):
414- query = client.Query('DescribeInstances', self.creds,
415+ query = client.Query('DescribeInstances', self.creds, self.endpoint,
416 time_tuple=(2007,11,12,13,14,15,0,0,0))
417- signing_text = ('GET\nec2.amazonaws.com\n/\n'
418+ signing_text = ('GET\n%s\n/\n' % self.endpoint.host +
419 'AWSAccessKeyId=foo&Action=DescribeInstances&'
420 'SignatureMethod=HmacSHA1&SignatureVersion=2&'
421 'Timestamp=2007-11-12T13%3A14%3A15Z&Version=2008-12-01')
422 self.assertEqual(signing_text, query.signing_text())
423
424 def test_sign(self):
425- query = client.Query('DescribeInstances', self.creds,
426+ query = client.Query('DescribeInstances', self.creds, self.endpoint,
427 time_tuple=(2007,11,12,13,14,15,0,0,0))
428 query.sign()
429- self.assertEqual('4hEtLuZo9i6kuG3TOXvRQNOrE/U=',
430+ self.assertEqual('JuCpwFA2H4OVF3Ql/lAQs+V6iMc=',
431 query.params['Signature'])
432
433=== added file 'txaws/ec2/tests/test_service.py'
434=== added file 'txaws/service.py'
435--- txaws/service.py 1970-01-01 00:00:00 +0000
436+++ txaws/service.py 2009-08-21 20:50:36 +0000
437@@ -0,0 +1,94 @@
438+# Copyright (C) 2009 Duncan McGreggor <duncan@canonical.com>
439+# Copyright (C) 2009 Robert Collins <robertc@robertcollins.net>
440+# Licenced under the txaws licence available at /LICENSE in the txaws source.
441+
442+import os
443+
444+from twisted.web.client import _parse
445+
446+
447+
448+__all__ = ["AWSServiceEndpoint", "AWSServiceRegion"]
449+
450+
451+REGION_US = "US"
452+REGION_EU = "EU"
453+EC2_ENDPOINT_US = "https://us-east-1.ec2.amazonaws.com/"
454+EC2_ENDPOINT_EU = "https://eu-west-1.ec2.amazonaws.com/"
455+DEFAULT_PORT = 80
456+
457+
458+class AWSServiceEndpoint(object):
459+ """
460+ @param uri: The URL for the service.
461+ @param method: The HTTP method used when accessing a service.
462+ """
463+
464+ def __init__(self, uri="", method="GET"):
465+ self.host = ""
466+ self.port = DEFAULT_PORT
467+ self.path = "/"
468+ self.method = method
469+ self._parse_uri(uri)
470+ if not self.scheme:
471+ self.scheme = "http"
472+
473+ def _parse_uri(self, uri):
474+ scheme, host, port, path = _parse(
475+ str(uri), defaultPort=DEFAULT_PORT)
476+ self.scheme = scheme
477+ self.host = host
478+ self.port = port
479+ self.path = path
480+
481+ def set_path(self, path):
482+ self.path = path
483+
484+ def get_uri(self):
485+ """Get a URL representation of the service."""
486+ uri = "%s://%s" % (self.scheme, self.host)
487+ if self.port and self.port != DEFAULT_PORT:
488+ uri = "%s:%s" % (uri, self.port)
489+ return uri + self.path
490+
491+
492+class AWSServiceRegion(object):
493+ """
494+ This object represents a collection of client factories that use the same
495+ credentials. With Amazon, this collection is associated with a region
496+ (e.g., US or EU).
497+ """
498+ def __init__(self, creds=None, region=REGION_US):
499+ self.creds = creds
500+ self._clients = {}
501+ if region == REGION_US:
502+ ec2_endpoint = EC2_ENDPOINT_US
503+ elif region == REGION_EU:
504+ ec2_endpoint = EC2_ENDPOINT_EU
505+ self.ec2_endpoint = AWSServiceEndpoint(uri=ec2_endpoint)
506+
507+ def get_client(self, cls, *args, **kwds):
508+ key = str(cls) + str(args) + str(kwds)
509+ instance = self._clients.get(key)
510+ if not instance:
511+ instance = cls(*args, **kwds)
512+ self._clients[key] = instance
513+ return instance
514+
515+ def get_ec2_client(self, creds=None):
516+ from txaws.ec2.client import EC2Client
517+
518+ if creds:
519+ self.creds = creds
520+ return self.get_client(EC2Client, creds=creds,
521+ endpoint=self.ec2_endpoint, query_factory=None)
522+
523+ def get_s3_client(self):
524+ raise NotImplementedError
525+
526+ def get_simpledb_client(self):
527+ raise NotImplementedError
528+
529+ def get_sqs_client(self):
530+ raise NotImplementedError
531+
532
533=== modified file 'txaws/storage/client.py'
534--- txaws/storage/client.py 2009-08-20 12:15:12 +0000
535+++ txaws/storage/client.py 2009-08-21 14:50:25 +0000
536@@ -25,19 +25,18 @@
537 class S3Request(object):
538
539 def __init__(self, verb, bucket=None, object_name=None, data='',
540- content_type=None,
541- metadata={}, root_uri='https://s3.amazonaws.com', creds=None):
542+ content_type=None, metadata={}, creds=None, endpoint=None):
543 self.verb = verb
544 self.bucket = bucket
545 self.object_name = object_name
546 self.data = data
547 self.content_type = content_type
548 self.metadata = metadata
549- self.root_uri = root_uri
550 self.creds = creds
551+ self.endpoint = endpoint or self.get_uri()
552 self.date = datetimeToString()
553
554- def get_uri_path(self):
555+ def get_path(self):
556 path = '/'
557 if self.bucket is not None:
558 path += self.bucket
559@@ -46,7 +45,8 @@
560 return path
561
562 def get_uri(self):
563- return self.root_uri + self.get_uri_path()
564+ self.endpoint.set_path(self.get_path())
565+ return self.endpoint.get_uri()
566
567 def get_headers(self):
568 headers = {'Content-Length': len(self.data),
569@@ -66,7 +66,7 @@
570 return headers
571
572 def get_canonicalized_resource(self):
573- return self.get_uri_path()
574+ return self.get_path()
575
576 def get_canonicalized_amz_headers(self, headers):
577 result = ''
578@@ -76,12 +76,12 @@
579 return ''.join('%s:%s\n' % (name, value) for name, value in headers)
580
581 def get_signature(self, headers):
582- text = self.verb + '\n'
583- text += headers.get('Content-MD5', '') + '\n'
584- text += headers.get('Content-Type', '') + '\n'
585- text += headers.get('Date', '') + '\n'
586- text += self.get_canonicalized_amz_headers(headers)
587- text += self.get_canonicalized_resource()
588+ text = (self.verb + '\n' +
589+ headers.get('Content-MD5', '') + '\n' +
590+ headers.get('Content-Type', '') + '\n' +
591+ headers.get('Date', '') + '\n' +
592+ self.get_canonicalized_amz_headers(headers) +
593+ self.get_canonicalized_resource())
594 return self.creds.sign(text)
595
596 def submit(self):
597@@ -94,20 +94,21 @@
598
599 class S3(object):
600
601- root_uri = 'https://s3.amazonaws.com/'
602 request_factory = S3Request
603
604- def __init__(self, creds):
605+ def __init__(self, creds, endpoint):
606 self.creds = creds
607+ self.endpoint = endpoint
608
609 def make_request(self, *a, **kw):
610 """
611 Create a request with the arguments passed in.
612
613- This uses the request_factory attribute, adding the credentials to the
614- arguments passed in.
615+ This uses the request_factory attribute, adding the creds and endpoint
616+ to the arguments passed in.
617 """
618- return self.request_factory(creds=self.creds, *a, **kw)
619+ return self.request_factory(creds=self.creds, endpoint=self.endpoint,
620+ *a, **kw)
621
622 def _parse_bucket_list(self, response):
623 """
624
625=== modified file 'txaws/storage/tests/test_client.py'
626--- txaws/storage/tests/test_client.py 2009-08-20 12:15:12 +0000
627+++ txaws/storage/tests/test_client.py 2009-08-21 14:50:25 +0000
628@@ -5,6 +5,7 @@
629 from twisted.internet.defer import succeed
630
631 from txaws.credentials import AWSCredentials
632+from txaws.service import AWSServiceEndpoint
633 from txaws.storage.client import S3, S3Request
634 from txaws.tests import TXAWSTestCase
635 from txaws.util import calculate_md5
636@@ -18,10 +19,10 @@
637 return succeed('')
638
639
640-class RequestTests(TXAWSTestCase):
641+class RequestTestCase(TXAWSTestCase):
642
643- creds = AWSCredentials(access_key='0PN5J17HBGZHT7JJ3X82',
644- secret_key='uV3F3YluFJax1cknvbcGwgjvx4QpvB+leU8dUj2o')
645+ creds = AWSCredentials(access_key='fookeyid', secret_key='barsecretkey')
646+ endpoint = AWSServiceEndpoint("https://s3.amazonaws.com/")
647
648 def test_objectRequest(self):
649 """
650@@ -31,18 +32,22 @@
651 DIGEST = 'zhdB6gwvocWv/ourYUWMxA=='
652
653 request = S3Request('PUT', 'somebucket', 'object/name/here', DATA,
654- content_type='text/plain', metadata={'foo': 'bar'})
655+ content_type='text/plain', metadata={'foo': 'bar'},
656+ creds=self.creds, endpoint=self.endpoint)
657+ request.get_signature = lambda headers: "TESTINGSIG="
658 self.assertEqual(request.verb, 'PUT')
659 self.assertEqual(
660 request.get_uri(),
661 'https://s3.amazonaws.com/somebucket/object/name/here')
662 headers = request.get_headers()
663 self.assertNotEqual(headers.pop('Date'), '')
664- self.assertEqual(headers,
665- {'Content-Type': 'text/plain',
666- 'Content-Length': len(DATA),
667- 'Content-MD5': DIGEST,
668- 'x-amz-meta-foo': 'bar'})
669+ self.assertEqual(
670+ headers, {
671+ 'Authorization': 'AWS fookeyid:TESTINGSIG=',
672+ 'Content-Type': 'text/plain',
673+ 'Content-Length': len(DATA),
674+ 'Content-MD5': DIGEST,
675+ 'x-amz-meta-foo': 'bar'})
676 self.assertEqual(request.data, 'objectData')
677
678 def test_bucketRequest(self):
679@@ -51,22 +56,27 @@
680 """
681 DIGEST = '1B2M2Y8AsgTpgAmY7PhCfg=='
682
683- request = S3Request('GET', 'somebucket')
684+ request = S3Request('GET', 'somebucket', creds=self.creds,
685+ endpoint=self.endpoint)
686+ request.get_signature = lambda headers: "TESTINGSIG="
687 self.assertEqual(request.verb, 'GET')
688 self.assertEqual(
689 request.get_uri(), 'https://s3.amazonaws.com/somebucket')
690 headers = request.get_headers()
691 self.assertNotEqual(headers.pop('Date'), '')
692- self.assertEqual(headers,
693- {'Content-Length': 0,
694- 'Content-MD5': DIGEST})
695+ self.assertEqual(
696+ headers, {
697+ 'Authorization': 'AWS fookeyid:TESTINGSIG=',
698+ 'Content-Length': 0,
699+ 'Content-MD5': DIGEST})
700 self.assertEqual(request.data, '')
701
702 def test_submit(self):
703 """
704 Submitting the request should invoke getPage correctly.
705 """
706- request = StubbedS3Request('GET', 'somebucket')
707+ request = StubbedS3Request('GET', 'somebucket', creds=self.creds,
708+ endpoint=self.endpoint)
709
710 def _postCheck(result):
711 self.assertEqual(result, '')
712@@ -80,13 +90,14 @@
713 return request.submit().addCallback(_postCheck)
714
715 def test_authenticationTestCases(self):
716- req = S3Request('GET', creds=self.creds)
717- req.date = 'Wed, 28 Mar 2007 01:29:59 +0000'
718+ request = S3Request('GET', creds=self.creds, endpoint=self.endpoint)
719+ request.get_signature = lambda headers: "TESTINGSIG="
720+ request.date = 'Wed, 28 Mar 2007 01:29:59 +0000'
721
722- headers = req.get_headers()
723+ headers = request.get_headers()
724 self.assertEqual(
725 headers['Authorization'],
726- 'AWS 0PN5J17HBGZHT7JJ3X82:jF7L3z/FTV47vagZzhKupJ9oNig=')
727+ 'AWS fookeyid:TESTINGSIG=')
728
729
730 class InertRequest(S3Request):
731@@ -153,16 +164,18 @@
732 TXAWSTestCase.setUp(self)
733 self.creds = AWSCredentials(
734 access_key='accessKey', secret_key='secretKey')
735- self.s3 = TestableS3(creds=self.creds)
736+ self.endpoint = AWSServiceEndpoint()
737+ self.s3 = TestableS3(creds=self.creds, endpoint=self.endpoint)
738
739 def test_make_request(self):
740 """
741- Test that make_request passes in the service credentials.
742+ Test that make_request passes in the credentials object.
743 """
744 marker = object()
745
746 def _cb(*a, **kw):
747 self.assertEqual(kw['creds'], self.creds)
748+ self.assertEqual(kw['endpoint'], self.endpoint)
749 return marker
750
751 self.s3.request_factory = _cb
752
753=== added file 'txaws/tests/test_credentials.py'
754--- txaws/tests/test_credentials.py 1970-01-01 00:00:00 +0000
755+++ txaws/tests/test_credentials.py 2009-08-21 14:50:25 +0000
756@@ -0,0 +1,53 @@
757+# Copyright (C) 2009 Robert Collins <robertc@robertcollins.net>
758+# Licenced under the txaws licence available at /LICENSE in the txaws source.
759+
760+import os
761+
762+from twisted.trial.unittest import TestCase
763+
764+from txaws.credentials import AWSCredentials, ENV_ACCESS_KEY, ENV_SECRET_KEY
765+from txaws.tests import TXAWSTestCase
766+
767+from txaws.tests import TXAWSTestCase
768+
769+
770+class TestCredentials(TXAWSTestCase):
771+
772+ def setUp(self):
773+ self.addCleanup(self.clean_environment)
774+
775+ def clean_environment(self):
776+ if os.environ.has_key(ENV_ACCESS_KEY):
777+ del os.environ[ENV_ACCESS_KEY]
778+ if os.environ.has_key(ENV_SECRET_KEY):
779+ del os.environ[ENV_SECRET_KEY]
780+
781+ def test_no_access_errors(self):
782+ # Without anything in os.environ, AWSService() blows up
783+ os.environ[ENV_SECRET_KEY] = "bar"
784+ self.assertRaises(ValueError, AWSCredentials)
785+
786+ def test_no_secret_errors(self):
787+ # Without anything in os.environ, AWSService() blows up
788+ os.environ[ENV_ACCESS_KEY] = "foo"
789+ self.assertRaises(ValueError, AWSCredentials)
790+
791+ def test_found_values_used(self):
792+ os.environ[ENV_ACCESS_KEY] = "foo"
793+ os.environ[ENV_SECRET_KEY] = "bar"
794+ service = AWSCredentials()
795+ self.assertEqual("foo", service.access_key)
796+ self.assertEqual("bar", service.secret_key)
797+ self.clean_environment()
798+
799+ def test_explicit_access_key(self):
800+ os.environ[ENV_SECRET_KEY] = "foo"
801+ service = AWSCredentials(access_key="bar")
802+ self.assertEqual("foo", service.secret_key)
803+ self.assertEqual("bar", service.access_key)
804+
805+ def test_explicit_secret_key(self):
806+ os.environ[ENV_ACCESS_KEY] = "bar"
807+ service = AWSCredentials(secret_key="foo")
808+ self.assertEqual("foo", service.secret_key)
809+ self.assertEqual("bar", service.access_key)
810
811=== removed file 'txaws/tests/test_credentials.py'
812--- txaws/tests/test_credentials.py 2009-08-20 12:15:12 +0000
813+++ txaws/tests/test_credentials.py 1970-01-01 00:00:00 +0000
814@@ -1,41 +0,0 @@
815-# Copyright (C) 2009 Robert Collins <robertc@robertcollins.net>
816-# Licenced under the txaws licence available at /LICENSE in the txaws source.
817-
818-import os
819-
820-from twisted.trial.unittest import TestCase
821-
822-from txaws.credentials import AWSCredentials
823-from txaws.tests import TXAWSTestCase
824-
825-
826-class TestCredentials(TXAWSTestCase):
827-
828- def test_no_access_errors(self):
829- # Without anything in os.environ, AWSCredentials() blows up
830- os.environ['AWS_SECRET_ACCESS_KEY'] = 'foo'
831- self.assertRaises(Exception, AWSCredentials)
832-
833- def test_no_secret_errors(self):
834- # Without anything in os.environ, AWSCredentials() blows up
835- os.environ['AWS_ACCESS_KEY_ID'] = 'bar'
836- self.assertRaises(Exception, AWSCredentials)
837-
838- def test_found_values_used(self):
839- os.environ['AWS_SECRET_ACCESS_KEY'] = 'foo'
840- os.environ['AWS_ACCESS_KEY_ID'] = 'bar'
841- creds = AWSCredentials()
842- self.assertEqual('foo', creds.secret_key)
843- self.assertEqual('bar', creds.access_key)
844-
845- def test_explicit_access_key(self):
846- os.environ['AWS_SECRET_ACCESS_KEY'] = 'foo'
847- creds = AWSCredentials(access_key='bar')
848- self.assertEqual('foo', creds.secret_key)
849- self.assertEqual('bar', creds.access_key)
850-
851- def test_explicit_secret_key(self):
852- os.environ['AWS_ACCESS_KEY_ID'] = 'bar'
853- creds = AWSCredentials(secret_key='foo')
854- self.assertEqual('foo', creds.secret_key)
855- self.assertEqual('bar', creds.access_key)
856
857=== added file 'txaws/tests/test_service.py'
858--- txaws/tests/test_service.py 1970-01-01 00:00:00 +0000
859+++ txaws/tests/test_service.py 2009-08-21 21:13:27 +0000
860@@ -0,0 +1,105 @@
861+# Copyright (C) 2009 Duncan McGreggor <duncan@canonical.com>
862+# Licenced under the txaws licence available at /LICENSE in the txaws source.
863+
864+import os
865+
866+from txaws.credentials import AWSCredentials
867+from txaws.ec2.client import EC2Client
868+from txaws.service import AWSServiceEndpoint, AWSServiceRegion, EC2_ENDPOINT_US
869+from txaws.tests import TXAWSTestCase
870+
871+class AWSServiceEndpointTestCase(TXAWSTestCase):
872+
873+ def setUp(self):
874+ self.endpoint = AWSServiceEndpoint(uri="http://my.service/da_endpoint")
875+
876+ def test_simple_creation(self):
877+ endpoint = AWSServiceEndpoint()
878+ self.assertEquals(endpoint.scheme, "http")
879+ self.assertEquals(endpoint.host, "")
880+ self.assertEquals(endpoint.port, 80)
881+ self.assertEquals(endpoint.path, "/")
882+ self.assertEquals(endpoint.method, "GET")
883+
884+ def test_parse_uri(self):
885+ self.assertEquals(self.endpoint.scheme, "http")
886+ self.assertEquals(self.endpoint.host, "my.service")
887+ self.assertEquals(self.endpoint.port, 80)
888+ self.assertEquals(self.endpoint.path, "/da_endpoint")
889+
890+ def test_parse_uri_https_and_custom_port(self):
891+ endpoint = AWSServiceEndpoint(uri="https://my.service:8080/endpoint")
892+ self.assertEquals(endpoint.scheme, "https")
893+ self.assertEquals(endpoint.host, "my.service")
894+ self.assertEquals(endpoint.port, 8080)
895+ self.assertEquals(endpoint.path, "/endpoint")
896+
897+ def test_custom_method(self):
898+ endpoint = AWSServiceEndpoint(uri="http://service/endpoint",
899+ method="PUT")
900+ self.assertEquals(endpoint.method, "PUT")
901+
902+ def test_get_uri(self):
903+ uri = self.endpoint.get_uri()
904+ self.assertEquals(uri, "http://my.service/da_endpoint")
905+
906+ def test_get_uri_custom_port(self):
907+ uri = "https://my.service:8080/endpoint"
908+ endpoint = AWSServiceEndpoint(uri=uri)
909+ new_uri = endpoint.get_uri()
910+ self.assertEquals(new_uri, uri)
911+
912+ def test_set_path(self):
913+ original_path = self.endpoint.path
914+ self.endpoint.set_path("/newpath")
915+ self.assertEquals(
916+ self.endpoint.get_uri(),
917+ "http://my.service/newpath")
918+
919+
920+class AWSServiceRegionTestCase(TXAWSTestCase):
921+
922+ def setUp(self):
923+ self.creds = AWSCredentials("foo", "bar")
924+ self.region = AWSServiceRegion(creds=self.creds)
925+
926+ def test_simple_creation(self):
927+ self.assertEquals(self.creds, self.region.creds)
928+ self.assertEquals(self.region._clients, {})
929+ self.assertEquals(self.region.ec2_endpoint.get_uri(), EC2_ENDPOINT_US)
930+
931+ def test_get_client_with_empty_cache(self):
932+ key = str(EC2Client) + str(self.creds) + str(self.region.ec2_endpoint)
933+ original_client = self.region._clients.get(key)
934+ new_client = self.region.get_client(
935+ EC2Client, self.creds, self.region.ec2_endpoint)
936+ self.assertEquals(original_client, None)
937+ self.assertNotEquals(original_client, new_client)
938+ self.assertTrue(isinstance(new_client, EC2Client))
939+
940+ def test_get_client_from_cache(self):
941+ client1 = self.region.get_client(
942+ EC2Client, self.creds, self.region.ec2_endpoint)
943+ client2 = self.region.get_client(
944+ EC2Client, self.creds, self.region.ec2_endpoint)
945+ self.assertTrue(isinstance(client1, EC2Client))
946+ self.assertTrue(isinstance(client2, EC2Client))
947+ self.assertEquals(client2, client2)
948+
949+ def test_get_ec2_client_from_cache(self):
950+ client1 = self.region.get_ec2_client(self.creds)
951+ client2 = self.region.get_ec2_client(self.creds)
952+ self.assertEquals(self.creds, self.region.creds)
953+ self.assertTrue(isinstance(client1, EC2Client))
954+ self.assertTrue(isinstance(client2, EC2Client))
955+ self.assertEquals(client2, client2)
956+
957+
958+ def test_get_s3_client(self):
959+ self.assertRaises(NotImplementedError, self.region.get_s3_client)
960+
961+ def test_get_simpledb_client(self):
962+ self.assertRaises(NotImplementedError, self.region.get_simpledb_client)
963+
964+ def test_get_sqs_client(self):
965+ self.assertRaises(NotImplementedError, self.region.get_sqs_client)

Subscribers

People subscribed via source and target branches