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