Merge lp:~oubiwann/txaws/416109-arbitrary-endpoints into lp:~txawsteam/txaws/trunk
- 416109-arbitrary-endpoints
- Merge into trunk
Status: | Superseded | ||||
---|---|---|---|---|---|
Proposed branch: | lp:~oubiwann/txaws/416109-arbitrary-endpoints | ||||
Merge into: | lp:~txawsteam/txaws/trunk | ||||
Diff against target: | None lines | ||||
To merge this branch: | bzr merge lp:~oubiwann/txaws/416109-arbitrary-endpoints | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Original txAWS Team | Pending | ||
Review via email: mp+10477@code.launchpad.net |
This proposal has been superseded by a proposal from 2009-08-23.
Commit message
Description of the change
Duncan McGreggor (oubiwann) wrote : | # |
Robert Collins (lifeless) wrote : | # |
On Thu, 2009-08-20 at 19:25 +0000, Duncan McGreggor wrote:
> Duncan McGreggor has proposed merging
> lp:~oubiwann/txaws/416109-arbitrary-endpoints into lp:txaws.
>
> Requested reviews:
> txAWS Team (txawsteam)
>
> This branch adds support for a service object that manages host
> endpoints as well as authorization keys (thus obviating the need for
> the AWSCredential object).
Lets be careful to keep space under storage, ec2 etc for server
components. storage.service isn't really a storage service :) Lets call
the description of an end point AWSServiceEndpoint, or something like
that.
local and credentials appear orthogonal to me - for instance,
EC2 EU and EC2 US are different endpoints/services with common
credentials. I think conflating them is unnecessary and undesirable.
Further to that, the AWSCredentials are usable on related services in a
single region - EC2, S3 and so on, so when we're passing around a
description, we probably want to have a region that describes the
endpoints for a collection of services. The goal being able to have a
static object
AWS_US1 = #...
AWS_US2 = #...
and for people to make their own;
my_eucalyptus_
At runtime then, one would ask a region for a client of a particular
service, using some credentials.
AWS_US1.
AWS_US1.
etc.
We could do this without changing the existing clients at all, by just
storing scheme,host tuples in a AWSRegion - but I think it is cleaner to
do the sort of refactoring you have done. I think it would be best by
having an AWSServiceEndpoint which has the scheme and url, and keeping
the creds separate. For instance,
class AWSServiceRegion:
def make_ec2_
return EC2Client(
Also a bit of detail review - 'default_schema = https' - in URL terms
(see http://
_schema_.
review needsfixing
Duncan McGreggor (oubiwann) wrote : | # |
> On Thu, 2009-08-20 at 19:25 +0000, Duncan McGreggor wrote:
> > Duncan McGreggor has proposed merging
> > lp:~oubiwann/txaws/416109-arbitrary-endpoints into lp:txaws.
> >
> > Requested reviews:
> > txAWS Team (txawsteam)
> >
> > This branch adds support for a service object that manages host
> > endpoints as well as authorization keys (thus obviating the need for
> > the AWSCredential object).
>
>
> Lets be careful to keep space under storage, ec2 etc for server
> components. storage.service isn't really a storage service :) Lets call
> the description of an end point AWSServiceEndpoint, or something like
> that.
>
> local and credentials appear orthogonal to me - for instance,
> EC2 EU and EC2 US are different endpoints/services with common
> credentials. I think conflating them is unnecessary and undesirable.
> Further to that, the AWSCredentials are usable on related services in a
> single region - EC2, S3 and so on, so when we're passing around a
> description, we probably want to have a region that describes the
> endpoints for a collection of services. The goal being able to have a
> static object
> AWS_US1 = #...
> AWS_US2 = #...
> and for people to make their own;
> my_eucalyptus_
>
> At runtime then, one would ask a region for a client of a particular
> service, using some credentials.
>
> AWS_US1.
> AWS_US1.
>
> etc.
>
> We could do this without changing the existing clients at all, by just
> storing scheme,host tuples in a AWSRegion - but I think it is cleaner to
> do the sort of refactoring you have done. I think it would be best by
> having an AWSServiceEndpoint which has the scheme and url, and keeping
> the creds separate. For instance,
> class AWSServiceRegion:
> def make_ec2_
> return EC2Client(
>
> Also a bit of detail review - 'default_schema = https' - in URL terms
> (see http://
> _schema_.
>
> review needsfixing
+1 on these suggestions. I'll give it another go with this in mind.
- 18. By Duncan McGreggor
-
A couple tiny tweaks.
- 19. By Duncan McGreggor
-
Added missing service file.
- 20. By Duncan McGreggor
-
Added credentials back.
- 21. By Duncan McGreggor
-
Merged from trunk and resolved conflicts.
Duncan McGreggor (oubiwann) wrote : | # |
> On Thu, 2009-08-20 at 19:25 +0000, Duncan McGreggor wrote:
> > Duncan McGreggor has proposed merging
> > lp:~oubiwann/txaws/416109-arbitrary-endpoints into lp:txaws.
> >
> > Requested reviews:
> > txAWS Team (txawsteam)
> >
> > This branch adds support for a service object that manages host
> > endpoints as well as authorization keys (thus obviating the need for
> > the AWSCredential object).
>
>
> Lets be careful to keep space under storage, ec2 etc for server
> components. storage.service isn't really a storage service :) Lets call
> the description of an end point AWSServiceEndpoint, or something like
> that.
[1] Renamed.
> local and credentials appear orthogonal to me - for instance,
> EC2 EU and EC2 US are different endpoints/services with common
> credentials. I think conflating them is unnecessary and undesirable.
> Further to that, the AWSCredentials are usable on related services in a
> single region - EC2, S3 and so on, so when we're passing around a
> description, we probably want to have a region that describes the
> endpoints for a collection of services.
[2]
Brought the credentials back into the source. Pulled credential code out of service endpoint code.
> The goal being able to have a
> static object
> AWS_US1 = #...
> AWS_US2 = #...
> and for people to make their own;
> my_eucalyptus_
>
> At runtime then, one would ask a region for a client of a particular
> service, using some credentials.
>
> AWS_US1.
> AWS_US1.
>
> etc.
>
> We could do this without changing the existing clients at all, by just
> storing scheme,host tuples in a AWSRegion - but I think it is cleaner to
> do the sort of refactoring you have done. I think it would be best by
> having an AWSServiceEndpoint which has the scheme and url, and keeping
> the creds separate. For instance,
> class AWSServiceRegion:
> def make_ec2_
> return EC2Client(
[3]
I'm got an implementation of this in place right now. It ended up pretty similar to what you suggested. There are some missing unit tests right now -- I'll be hitting those this afternoon.
> Also a bit of detail review - 'default_schema = https' - in URL terms
> (see http://
> _schema_.
[4]
Ugh, thanks. The first place I wrote it was good, then I copied a typo everywhere else. Fixed.
- 22. By Duncan McGreggor
-
- Updated the AWSCredentials with the refactored code that had been written in
the AWService class.
- Renamed the AWSService class to AWSServiceEndpoint (lifeless 1)
- Created and AWSServiceRegion object that acts as a client factory (lifeless 3)
- Added a placeholder for the service unit tests.
- Removed storage service. - 23. By Duncan McGreggor
-
Added a TODO comment for tests.
- 24. By Duncan McGreggor
-
Added missing test for set_path.
Duncan McGreggor (oubiwann) wrote : | # |
Okay! Just pushed up the latest code for the missing unit tests. It's ready for another review :-)
- 25. By Duncan McGreggor
-
Added missing tests for the AWS service region object.
- 26. By Duncan McGreggor
-
Added another check for client caching.
- 27. By Duncan McGreggor
-
Removed new cred files.
- 28. By Duncan McGreggor
-
Reverted to original cred files (-r13..12) in an effort to fix some weirdness
in this branch with those files. - 29. By Duncan McGreggor
-
Reapplied the recent changes to the cred files.
- 30. By Duncan McGreggor
-
- Changed the gtk client to use creds and service region instead of the
no-longer-supported service object.
- Added a cache-purging keyword parameter to the AWS service region's
get_client method.
- Added a docstring.
- Added region string objects to service.__all__.
- Tweaked the gtk code to check for an already-installed gtk Twisted reactor.
- Cleaned up some remaining references to the service object in the client and
replaced those with endpoint references.
- Fixed a stub query signature in a unit test to include a parameter for an
endpoint object. - 31. By Duncan McGreggor
-
- Removed old service unit test file.
- Added unit test for purge client option.
- Fixed typo in client check unit tests. - 32. By Duncan McGreggor
-
- Added access and secret key parameters to the AWSServiceRegion constructor.
- Updated AWSServiceRegion to create creds based on access and secret key if no
creds are supplied.
- Updated docstrings. - 33. By Duncan McGreggor
-
Added a uri parameter for service region creation to ease the creation of
service region objects with non-Amazon endpoints (e.g., in Landscape). - 34. By Duncan McGreggor
-
Swapped the ordering of an import to be in alphabetical order.
- 35. By Duncan McGreggor
-
- Fixed the creds parameter in the get_ec2_client method.
- Removed redundant code in check_parsed_instances.
- Created a testing subpackage for generally useful testing classes.
- Added fake ec2 client and region classes.
- Moved base test case into new testing module. - 36. By Duncan McGreggor
-
- Removed unimplemented methods (jkakar 1).
- Made environment mutation methods private (jkakar 3).
- Tweaked the default values for the FakeEC2Client (jkakar 4).
- Removed unnecessary test case methods (jkakar 5). - 37. By Duncan McGreggor
-
Tweaked the storage request object's enpoint/uri stuff and added some unit
tests (jkakar 2). - 38. By Duncan McGreggor
-
Removed unnecessary region instantiation (therve 3).
Added parse utility function (therve 4). - 39. By Duncan McGreggor
-
Fixed pyflakes (therve 1).
- 40. By Duncan McGreggor
-
Removed unneeded try/except block in gtk client (therve 2).
Unmerged revisions
Preview Diff
1 | === modified file 'txaws/client/gui/gtk.py' | |||
2 | --- txaws/client/gui/gtk.py 2009-08-18 22:53:53 +0000 | |||
3 | +++ txaws/client/gui/gtk.py 2009-08-20 19:14:32 +0000 | |||
4 | @@ -8,7 +8,7 @@ | |||
5 | 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 | === removed file 'txaws/credentials.py' | |||
72 | --- txaws/credentials.py 2009-08-17 11:18:56 +0000 | |||
73 | +++ txaws/credentials.py 1970-01-01 00:00:00 +0000 | |||
74 | @@ -1,37 +0,0 @@ | |||
75 | 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 * | ||
83 | 9 | |||
84 | 10 | |||
85 | 11 | __all__ = ['AWSCredentials'] | ||
86 | 12 | |||
87 | 13 | |||
88 | 14 | class AWSCredentials(object): | ||
89 | 15 | |||
90 | 16 | def __init__(self, access_key=None, secret_key=None): | ||
91 | 17 | """Create an AWSCredentials object. | ||
92 | 18 | |||
93 | 19 | :param access_key: The access key to use. If None the environment | ||
94 | 20 | variable AWS_ACCESS_KEY_ID is consulted. | ||
95 | 21 | :param secret_key: The secret key to use. If None the environment | ||
96 | 22 | variable AWS_SECRET_ACCESS_KEY is consulted. | ||
97 | 23 | """ | ||
98 | 24 | self.secret_key = secret_key | ||
99 | 25 | if self.secret_key is None: | ||
100 | 26 | self.secret_key = os.environ.get('AWS_SECRET_ACCESS_KEY') | ||
101 | 27 | if self.secret_key is None: | ||
102 | 28 | raise ValueError('Could not find AWS_SECRET_ACCESS_KEY') | ||
103 | 29 | self.access_key = access_key | ||
104 | 30 | if self.access_key is None: | ||
105 | 31 | self.access_key = os.environ.get('AWS_ACCESS_KEY_ID') | ||
106 | 32 | if self.access_key is None: | ||
107 | 33 | raise ValueError('Could not find AWS_ACCESS_KEY_ID') | ||
108 | 34 | |||
109 | 35 | def sign(self, bytes): | ||
110 | 36 | """Sign some bytes.""" | ||
111 | 37 | return hmac_sha1(self.secret_key, bytes) | ||
112 | 38 | 0 | ||
113 | === modified file 'txaws/ec2/client.py' | |||
114 | --- txaws/ec2/client.py 2009-08-18 21:56:36 +0000 | |||
115 | +++ txaws/ec2/client.py 2009-08-20 16:47:54 +0000 | |||
116 | @@ -8,7 +8,7 @@ | |||
117 | 8 | 8 | ||
118 | 9 | from twisted.web.client import getPage | 9 | from twisted.web.client import getPage |
119 | 10 | 10 | ||
121 | 11 | from txaws import credentials | 11 | from txaws.ec2.service import EC2Service |
122 | 12 | from txaws.util import iso8601time, XML | 12 | from txaws.util import iso8601time, XML |
123 | 13 | 13 | ||
124 | 14 | 14 | ||
125 | @@ -46,16 +46,15 @@ | |||
126 | 46 | 46 | ||
127 | 47 | name_space = '{http://ec2.amazonaws.com/doc/2008-12-01/}' | 47 | name_space = '{http://ec2.amazonaws.com/doc/2008-12-01/}' |
128 | 48 | 48 | ||
130 | 49 | def __init__(self, creds=None, query_factory=None): | 49 | def __init__(self, service=None, query_factory=None): |
131 | 50 | """Create an EC2Client. | 50 | """Create an EC2Client. |
132 | 51 | 51 | ||
135 | 52 | @param creds: Explicit credentials to use. If None, credentials are | 52 | @param service: Explicit service to use. |
134 | 53 | inferred as per txaws.credentials.AWSCredentials. | ||
136 | 54 | """ | 53 | """ |
139 | 55 | if creds is None: | 54 | if service is None: |
140 | 56 | self.creds = credentials.AWSCredentials() | 55 | self.service = EC2Service() |
141 | 57 | else: | 56 | else: |
143 | 58 | self.creds = creds | 57 | self.service = service |
144 | 59 | if query_factory is None: | 58 | if query_factory is None: |
145 | 60 | self.query_factory = Query | 59 | self.query_factory = Query |
146 | 61 | else: | 60 | else: |
147 | @@ -63,7 +62,7 @@ | |||
148 | 63 | 62 | ||
149 | 64 | def describe_instances(self): | 63 | def describe_instances(self): |
150 | 65 | """Describe current instances.""" | 64 | """Describe current instances.""" |
152 | 66 | q = self.query_factory('DescribeInstances', self.creds) | 65 | q = self.query_factory('DescribeInstances', self.service) |
153 | 67 | d = q.submit() | 66 | d = q.submit() |
154 | 68 | return d.addCallback(self._parse_instances) | 67 | return d.addCallback(self._parse_instances) |
155 | 69 | 68 | ||
156 | @@ -119,7 +118,7 @@ | |||
157 | 119 | instanceset = {} | 118 | instanceset = {} |
158 | 120 | for pos, instance_id in enumerate(instance_ids): | 119 | for pos, instance_id in enumerate(instance_ids): |
159 | 121 | instanceset["InstanceId.%d" % (pos+1)] = instance_id | 120 | instanceset["InstanceId.%d" % (pos+1)] = instance_id |
161 | 122 | q = self.query_factory('TerminateInstances', self.creds, instanceset) | 121 | q = self.query_factory('TerminateInstances', self.service, instanceset) |
162 | 123 | d = q.submit() | 122 | d = q.submit() |
163 | 124 | return d.addCallback(self._parse_terminate_instances) | 123 | return d.addCallback(self._parse_terminate_instances) |
164 | 125 | 124 | ||
165 | @@ -142,7 +141,7 @@ | |||
166 | 142 | class Query(object): | 141 | class Query(object): |
167 | 143 | """A query that may be submitted to EC2.""" | 142 | """A query that may be submitted to EC2.""" |
168 | 144 | 143 | ||
170 | 145 | def __init__(self, action, creds, other_params=None, time_tuple=None): | 144 | def __init__(self, action, service, other_params=None, time_tuple=None): |
171 | 146 | """Create a Query to submit to EC2.""" | 145 | """Create a Query to submit to EC2.""" |
172 | 147 | # Require params (2008-12-01 API): | 146 | # Require params (2008-12-01 API): |
173 | 148 | # Version, SignatureVersion, SignatureMethod, Action, AWSAccessKeyId, | 147 | # Version, SignatureVersion, SignatureMethod, Action, AWSAccessKeyId, |
174 | @@ -151,15 +150,12 @@ | |||
175 | 151 | 'SignatureVersion': '2', | 150 | 'SignatureVersion': '2', |
176 | 152 | 'SignatureMethod': 'HmacSHA1', | 151 | 'SignatureMethod': 'HmacSHA1', |
177 | 153 | 'Action': action, | 152 | 'Action': action, |
179 | 154 | 'AWSAccessKeyId': creds.access_key, | 153 | 'AWSAccessKeyId': service.access_key, |
180 | 155 | 'Timestamp': iso8601time(time_tuple), | 154 | 'Timestamp': iso8601time(time_tuple), |
181 | 156 | } | 155 | } |
182 | 157 | if other_params: | 156 | if other_params: |
183 | 158 | self.params.update(other_params) | 157 | self.params.update(other_params) |
188 | 159 | self.method = 'GET' | 158 | self.service = service |
185 | 160 | self.host = 'ec2.amazonaws.com' | ||
186 | 161 | self.uri = '/' | ||
187 | 162 | self.creds = creds | ||
189 | 163 | 159 | ||
190 | 164 | def canonical_query_params(self): | 160 | def canonical_query_params(self): |
191 | 165 | """Return the canonical query params (used in signing).""" | 161 | """Return the canonical query params (used in signing).""" |
192 | @@ -178,18 +174,19 @@ | |||
193 | 178 | 174 | ||
194 | 179 | def signing_text(self): | 175 | def signing_text(self): |
195 | 180 | """Return the text to be signed when signing the query.""" | 176 | """Return the text to be signed when signing the query.""" |
198 | 181 | result = "%s\n%s\n%s\n%s" % (self.method, self.host, self.uri, | 177 | result = "%s\n%s\n%s\n%s" % (self.service.method, self.service.host, |
199 | 182 | self.canonical_query_params()) | 178 | self.service.endpoint, |
200 | 179 | self.canonical_query_params()) | ||
201 | 183 | return result | 180 | return result |
202 | 184 | 181 | ||
203 | 185 | def sign(self): | 182 | def sign(self): |
205 | 186 | """Sign this query using its built in credentials. | 183 | """Sign this query using its built in service. |
206 | 187 | 184 | ||
207 | 188 | This prepares it to be sent, and should be done as the last step before | 185 | This prepares it to be sent, and should be done as the last step before |
208 | 189 | submitting the query. Signing is done automatically - this is a public | 186 | submitting the query. Signing is done automatically - this is a public |
209 | 190 | method to facilitate testing. | 187 | method to facilitate testing. |
210 | 191 | """ | 188 | """ |
212 | 192 | self.params['Signature'] = self.creds.sign(self.signing_text()) | 189 | self.params['Signature'] = self.service.sign(self.signing_text()) |
213 | 193 | 190 | ||
214 | 194 | def sorted_params(self): | 191 | def sorted_params(self): |
215 | 195 | """Return the query params sorted appropriately for signing.""" | 192 | """Return the query params sorted appropriately for signing.""" |
216 | @@ -198,9 +195,8 @@ | |||
217 | 198 | def submit(self): | 195 | def submit(self): |
218 | 199 | """Submit this query. | 196 | """Submit this query. |
219 | 200 | 197 | ||
221 | 201 | :return: A deferred from twisted.web.client.getPage | 198 | @return: A deferred from twisted.web.client.getPage |
222 | 202 | """ | 199 | """ |
223 | 203 | self.sign() | 200 | self.sign() |
227 | 204 | url = 'http://%s%s?%s' % (self.host, self.uri, | 201 | url = "%s?%s" % (self.service.get_url(), self.canonical_query_params()) |
228 | 205 | self.canonical_query_params()) | 202 | return getPage(url, method=self.service.method) |
226 | 206 | return getPage(url, method=self.method) | ||
229 | 207 | 203 | ||
230 | === modified file 'txaws/ec2/tests/test_client.py' | |||
231 | --- txaws/ec2/tests/test_client.py 2009-08-18 21:56:36 +0000 | |||
232 | +++ txaws/ec2/tests/test_client.py 2009-08-20 16:47:54 +0000 | |||
233 | @@ -5,8 +5,8 @@ | |||
234 | 5 | 5 | ||
235 | 6 | from twisted.internet.defer import succeed | 6 | from twisted.internet.defer import succeed |
236 | 7 | 7 | ||
237 | 8 | from txaws.credentials import AWSCredentials | ||
238 | 9 | from txaws.ec2 import client | 8 | from txaws.ec2 import client |
239 | 9 | from txaws.ec2.service import EC2Service, US_EC2_HOST | ||
240 | 10 | from txaws.tests import TXAWSTestCase | 10 | from txaws.tests import TXAWSTestCase |
241 | 11 | 11 | ||
242 | 12 | 12 | ||
243 | @@ -91,21 +91,21 @@ | |||
244 | 91 | self.assertEquals(reservation.groups, ["one", "two"]) | 91 | self.assertEquals(reservation.groups, ["one", "two"]) |
245 | 92 | 92 | ||
246 | 93 | 93 | ||
248 | 94 | class TestEC2Client(TXAWSTestCase): | 94 | class EC2ClientTestCase(TXAWSTestCase): |
249 | 95 | 95 | ||
250 | 96 | def test_init_no_creds(self): | 96 | def test_init_no_creds(self): |
251 | 97 | os.environ['AWS_SECRET_ACCESS_KEY'] = 'foo' | 97 | os.environ['AWS_SECRET_ACCESS_KEY'] = 'foo' |
252 | 98 | os.environ['AWS_ACCESS_KEY_ID'] = 'bar' | 98 | os.environ['AWS_ACCESS_KEY_ID'] = 'bar' |
253 | 99 | ec2 = client.EC2Client() | 99 | ec2 = client.EC2Client() |
255 | 100 | self.assertNotEqual(None, ec2.creds) | 100 | self.assertNotEqual(None, ec2.service) |
256 | 101 | 101 | ||
257 | 102 | def test_init_no_creds_non_available_errors(self): | 102 | def test_init_no_creds_non_available_errors(self): |
258 | 103 | self.assertRaises(ValueError, client.EC2Client) | 103 | self.assertRaises(ValueError, client.EC2Client) |
259 | 104 | 104 | ||
264 | 105 | def test_init_explicit_creds(self): | 105 | def test_init_explicit_service(self): |
265 | 106 | creds = 'foo' | 106 | service = EC2Service("foo", "bar") |
266 | 107 | ec2 = client.EC2Client(creds=creds) | 107 | ec2 = client.EC2Client(service=service) |
267 | 108 | self.assertEqual(creds, ec2.creds) | 108 | self.assertEqual(service, ec2.service) |
268 | 109 | 109 | ||
269 | 110 | def check_parsed_instances(self, results): | 110 | def check_parsed_instances(self, results): |
270 | 111 | instance = results[0] | 111 | instance = results[0] |
271 | @@ -118,33 +118,38 @@ | |||
272 | 118 | self.assertEquals(group, "default") | 118 | self.assertEquals(group, "default") |
273 | 119 | 119 | ||
274 | 120 | def test_parse_reservation(self): | 120 | def test_parse_reservation(self): |
276 | 121 | ec2 = client.EC2Client(creds='foo') | 121 | service = EC2Service("foo", "bar") |
277 | 122 | ec2 = client.EC2Client(service=service) | ||
278 | 122 | results = ec2._parse_instances(sample_describe_instances_result) | 123 | results = ec2._parse_instances(sample_describe_instances_result) |
279 | 123 | self.check_parsed_instances(results) | 124 | self.check_parsed_instances(results) |
280 | 124 | 125 | ||
281 | 125 | def test_describe_instances(self): | 126 | def test_describe_instances(self): |
282 | 126 | class StubQuery(object): | 127 | class StubQuery(object): |
284 | 127 | def __init__(stub, action, creds): | 128 | def __init__(stub, action, service): |
285 | 128 | self.assertEqual(action, 'DescribeInstances') | 129 | self.assertEqual(action, 'DescribeInstances') |
287 | 129 | self.assertEqual('foo', creds) | 130 | self.assertEqual(service.access_key, "foo") |
288 | 131 | self.assertEqual(service.secret_key, "bar") | ||
289 | 130 | def submit(self): | 132 | def submit(self): |
290 | 131 | return succeed(sample_describe_instances_result) | 133 | return succeed(sample_describe_instances_result) |
292 | 132 | ec2 = client.EC2Client(creds='foo', query_factory=StubQuery) | 134 | service = EC2Service("foo", "bar") |
293 | 135 | ec2 = client.EC2Client(service, query_factory=StubQuery) | ||
294 | 133 | d = ec2.describe_instances() | 136 | d = ec2.describe_instances() |
295 | 134 | d.addCallback(self.check_parsed_instances) | 137 | d.addCallback(self.check_parsed_instances) |
296 | 135 | return d | 138 | return d |
297 | 136 | 139 | ||
298 | 137 | def test_terminate_instances(self): | 140 | def test_terminate_instances(self): |
299 | 138 | class StubQuery(object): | 141 | class StubQuery(object): |
301 | 139 | def __init__(stub, action, creds, other_params): | 142 | def __init__(stub, action, service, other_params): |
302 | 140 | self.assertEqual(action, 'TerminateInstances') | 143 | self.assertEqual(action, 'TerminateInstances') |
304 | 141 | self.assertEqual('foo', creds) | 144 | self.assertEqual(service.access_key, "foo") |
305 | 145 | self.assertEqual(service.secret_key, "bar") | ||
306 | 142 | self.assertEqual( | 146 | self.assertEqual( |
307 | 143 | {'InstanceId.1': 'i-1234', 'InstanceId.2': 'i-5678'}, | 147 | {'InstanceId.1': 'i-1234', 'InstanceId.2': 'i-5678'}, |
308 | 144 | other_params) | 148 | other_params) |
309 | 145 | def submit(self): | 149 | def submit(self): |
310 | 146 | return succeed(sample_terminate_instances_result) | 150 | return succeed(sample_terminate_instances_result) |
312 | 147 | ec2 = client.EC2Client(creds='foo', query_factory=StubQuery) | 151 | service = EC2Service("foo", "bar") |
313 | 152 | ec2 = client.EC2Client(service=service, query_factory=StubQuery) | ||
314 | 148 | d = ec2.terminate_instances('i-1234', 'i-5678') | 153 | d = ec2.terminate_instances('i-1234', 'i-5678') |
315 | 149 | def check_transition(changes): | 154 | def check_transition(changes): |
316 | 150 | self.assertEqual([('i-1234', 'running', 'shutting-down'), | 155 | self.assertEqual([('i-1234', 'running', 'shutting-down'), |
317 | @@ -152,14 +157,14 @@ | |||
318 | 152 | return d | 157 | return d |
319 | 153 | 158 | ||
320 | 154 | 159 | ||
322 | 155 | class TestQuery(TXAWSTestCase): | 160 | class QueryTestCase(TXAWSTestCase): |
323 | 156 | 161 | ||
324 | 157 | def setUp(self): | 162 | def setUp(self): |
325 | 158 | TXAWSTestCase.setUp(self) | 163 | TXAWSTestCase.setUp(self) |
327 | 159 | self.creds = AWSCredentials('foo', 'bar') | 164 | self.service = EC2Service('foo', 'bar') |
328 | 160 | 165 | ||
329 | 161 | def test_init_minimum(self): | 166 | def test_init_minimum(self): |
331 | 162 | query = client.Query('DescribeInstances', self.creds) | 167 | query = client.Query('DescribeInstances', self.service) |
332 | 163 | self.assertTrue('Timestamp' in query.params) | 168 | self.assertTrue('Timestamp' in query.params) |
333 | 164 | del query.params['Timestamp'] | 169 | del query.params['Timestamp'] |
334 | 165 | self.assertEqual( | 170 | self.assertEqual( |
335 | @@ -173,11 +178,11 @@ | |||
336 | 173 | def test_init_requires_action(self): | 178 | def test_init_requires_action(self): |
337 | 174 | self.assertRaises(TypeError, client.Query) | 179 | self.assertRaises(TypeError, client.Query) |
338 | 175 | 180 | ||
340 | 176 | def test_init_requires_creds(self): | 181 | def test_init_requires_service_with_creds(self): |
341 | 177 | self.assertRaises(TypeError, client.Query, None) | 182 | self.assertRaises(TypeError, client.Query, None) |
342 | 178 | 183 | ||
343 | 179 | def test_init_other_args_are_params(self): | 184 | def test_init_other_args_are_params(self): |
345 | 180 | query = client.Query('DescribeInstances', self.creds, | 185 | query = client.Query('DescribeInstances', self.service, |
346 | 181 | {'InstanceId.0': '12345'}, | 186 | {'InstanceId.0': '12345'}, |
347 | 182 | time_tuple=(2007,11,12,13,14,15,0,0,0)) | 187 | time_tuple=(2007,11,12,13,14,15,0,0,0)) |
348 | 183 | self.assertEqual( | 188 | self.assertEqual( |
349 | @@ -191,7 +196,7 @@ | |||
350 | 191 | query.params) | 196 | query.params) |
351 | 192 | 197 | ||
352 | 193 | def test_sorted_params(self): | 198 | def test_sorted_params(self): |
354 | 194 | query = client.Query('DescribeInstances', self.creds, | 199 | query = client.Query('DescribeInstances', self.service, |
355 | 195 | {'fun': 'games'}, | 200 | {'fun': 'games'}, |
356 | 196 | time_tuple=(2007,11,12,13,14,15,0,0,0)) | 201 | time_tuple=(2007,11,12,13,14,15,0,0,0)) |
357 | 197 | self.assertEqual([ | 202 | self.assertEqual([ |
358 | @@ -207,16 +212,16 @@ | |||
359 | 207 | def test_encode_unreserved(self): | 212 | def test_encode_unreserved(self): |
360 | 208 | all_unreserved = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ' | 213 | all_unreserved = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ' |
361 | 209 | 'abcdefghijklmnopqrstuvwxyz0123456789-_.~') | 214 | 'abcdefghijklmnopqrstuvwxyz0123456789-_.~') |
363 | 210 | query = client.Query('DescribeInstances', self.creds) | 215 | query = client.Query('DescribeInstances', self.service) |
364 | 211 | self.assertEqual(all_unreserved, query.encode(all_unreserved)) | 216 | self.assertEqual(all_unreserved, query.encode(all_unreserved)) |
365 | 212 | 217 | ||
366 | 213 | def test_encode_space(self): | 218 | def test_encode_space(self): |
367 | 214 | """This may be just 'url encode', but the AWS manual isn't clear.""" | 219 | """This may be just 'url encode', but the AWS manual isn't clear.""" |
369 | 215 | query = client.Query('DescribeInstances', self.creds) | 220 | query = client.Query('DescribeInstances', self.service) |
370 | 216 | self.assertEqual('a%20space', query.encode('a space')) | 221 | self.assertEqual('a%20space', query.encode('a space')) |
371 | 217 | 222 | ||
372 | 218 | def test_canonical_query(self): | 223 | def test_canonical_query(self): |
374 | 219 | query = client.Query('DescribeInstances', self.creds, | 224 | query = client.Query('DescribeInstances', self.service, |
375 | 220 | {'fu n': 'g/ames', 'argwithnovalue':'', | 225 | {'fu n': 'g/ames', 'argwithnovalue':'', |
376 | 221 | 'InstanceId.1': 'i-1234'}, | 226 | 'InstanceId.1': 'i-1234'}, |
377 | 222 | time_tuple=(2007,11,12,13,14,15,0,0,0)) | 227 | time_tuple=(2007,11,12,13,14,15,0,0,0)) |
378 | @@ -228,17 +233,17 @@ | |||
379 | 228 | self.assertEqual(expected_query, query.canonical_query_params()) | 233 | self.assertEqual(expected_query, query.canonical_query_params()) |
380 | 229 | 234 | ||
381 | 230 | def test_signing_text(self): | 235 | def test_signing_text(self): |
383 | 231 | query = client.Query('DescribeInstances', self.creds, | 236 | query = client.Query('DescribeInstances', self.service, |
384 | 232 | time_tuple=(2007,11,12,13,14,15,0,0,0)) | 237 | time_tuple=(2007,11,12,13,14,15,0,0,0)) |
386 | 233 | signing_text = ('GET\nec2.amazonaws.com\n/\n' | 238 | signing_text = ('GET\n%s\n/\n' % US_EC2_HOST + |
387 | 234 | 'AWSAccessKeyId=foo&Action=DescribeInstances&' | 239 | 'AWSAccessKeyId=foo&Action=DescribeInstances&' |
388 | 235 | 'SignatureMethod=HmacSHA1&SignatureVersion=2&' | 240 | 'SignatureMethod=HmacSHA1&SignatureVersion=2&' |
389 | 236 | 'Timestamp=2007-11-12T13%3A14%3A15Z&Version=2008-12-01') | 241 | 'Timestamp=2007-11-12T13%3A14%3A15Z&Version=2008-12-01') |
390 | 237 | self.assertEqual(signing_text, query.signing_text()) | 242 | self.assertEqual(signing_text, query.signing_text()) |
391 | 238 | 243 | ||
392 | 239 | def test_sign(self): | 244 | def test_sign(self): |
394 | 240 | query = client.Query('DescribeInstances', self.creds, | 245 | query = client.Query('DescribeInstances', self.service, |
395 | 241 | time_tuple=(2007,11,12,13,14,15,0,0,0)) | 246 | time_tuple=(2007,11,12,13,14,15,0,0,0)) |
396 | 242 | query.sign() | 247 | query.sign() |
398 | 243 | self.assertEqual('4hEtLuZo9i6kuG3TOXvRQNOrE/U=', | 248 | self.assertEqual('JuCpwFA2H4OVF3Ql/lAQs+V6iMc=', |
399 | 244 | query.params['Signature']) | 249 | query.params['Signature']) |
400 | 245 | 250 | ||
401 | === added file 'txaws/service.py' | |||
402 | --- txaws/service.py 1970-01-01 00:00:00 +0000 | |||
403 | +++ txaws/service.py 2009-08-20 19:09:56 +0000 | |||
404 | @@ -0,0 +1,70 @@ | |||
405 | 1 | # Copyright (C) 2009 Duncan McGreggor <duncan@canonical.com> | ||
406 | 2 | # Copyright (C) 2009 Robert Collins <robertc@robertcollins.net> | ||
407 | 3 | # Licenced under the txaws licence available at /LICENSE in the txaws source. | ||
408 | 4 | |||
409 | 5 | import os | ||
410 | 6 | |||
411 | 7 | from twisted.web.client import _parse | ||
412 | 8 | |||
413 | 9 | from txaws.util import hmac_sha1 | ||
414 | 10 | |||
415 | 11 | DEFAULT_PORT = 80 | ||
416 | 12 | ENV_ACCESS_KEY = "AWS_ACCESS_KEY_ID" | ||
417 | 13 | ENV_SECRET_KEY = "AWS_SECRET_ACCESS_KEY" | ||
418 | 14 | |||
419 | 15 | class AWSService(object): | ||
420 | 16 | """ | ||
421 | 17 | @param access_key: The access key to use. If None the environment | ||
422 | 18 | variable AWS_ACCESS_KEY_ID is consulted. | ||
423 | 19 | @param secret_key: The secret key to use. If None the environment | ||
424 | 20 | variable AWS_SECRET_ACCESS_KEY is consulted. | ||
425 | 21 | @param uri: The URL for the service. | ||
426 | 22 | @param method: The HTTP method used when accessing a service. | ||
427 | 23 | """ | ||
428 | 24 | default_host = "" | ||
429 | 25 | default_schema = "https" | ||
430 | 26 | |||
431 | 27 | def __init__(self, access_key="", secret_key="", uri="", method="GET"): | ||
432 | 28 | self.access_key = access_key | ||
433 | 29 | self.secret_key = secret_key | ||
434 | 30 | self.schema = "" | ||
435 | 31 | self.host = "" | ||
436 | 32 | self.port = DEFAULT_PORT | ||
437 | 33 | self.endpoint = "/" | ||
438 | 34 | self.method = method | ||
439 | 35 | self._process_creds() | ||
440 | 36 | self._parse_uri(uri) | ||
441 | 37 | if not self.host: | ||
442 | 38 | self.host = self.default_host | ||
443 | 39 | if not self.schema: | ||
444 | 40 | self.schema = self.default_schema | ||
445 | 41 | |||
446 | 42 | def _process_creds(self): | ||
447 | 43 | # perform checks for access key | ||
448 | 44 | if not self.access_key: | ||
449 | 45 | self.access_key = os.environ.get(ENV_ACCESS_KEY) | ||
450 | 46 | if not self.access_key: | ||
451 | 47 | raise ValueError("Could not find %s" % ENV_ACCESS_KEY) | ||
452 | 48 | # perform checks for secret key | ||
453 | 49 | if not self.secret_key: | ||
454 | 50 | self.secret_key = os.environ.get(ENV_SECRET_KEY) | ||
455 | 51 | if not self.secret_key: | ||
456 | 52 | raise ValueError("Could not find %s" % ENV_SECRET_KEY) | ||
457 | 53 | |||
458 | 54 | def _parse_uri(self, uri): | ||
459 | 55 | scheme, host, port, endpoint = _parse(uri, defaultPort=DEFAULT_PORT) | ||
460 | 56 | self.schema = scheme | ||
461 | 57 | self.host = host | ||
462 | 58 | self.port = port | ||
463 | 59 | self.endpoint = endpoint | ||
464 | 60 | |||
465 | 61 | def get_uri(self): | ||
466 | 62 | """Get a URL representation of the service.""" | ||
467 | 63 | uri = "%s://%s" % (self.schema, self.host) | ||
468 | 64 | if self.port and self.port != DEFAULT_PORT: | ||
469 | 65 | uri = "%s:%s" % (uri, self.port) | ||
470 | 66 | return uri + self.endpoint | ||
471 | 67 | |||
472 | 68 | def sign(self, bytes): | ||
473 | 69 | """Sign some bytes.""" | ||
474 | 70 | return hmac_sha1(self.secret_key, bytes) | ||
475 | 0 | 71 | ||
476 | === modified file 'txaws/storage/client.py' | |||
477 | --- txaws/storage/client.py 2009-08-17 11:18:56 +0000 | |||
478 | +++ txaws/storage/client.py 2009-08-20 19:09:56 +0000 | |||
479 | @@ -10,178 +10,170 @@ | |||
480 | 10 | from hashlib import md5 | 10 | from hashlib import md5 |
481 | 11 | from base64 import b64encode | 11 | from base64 import b64encode |
482 | 12 | 12 | ||
483 | 13 | |||
484 | 14 | from epsilon.extime import Time | 13 | from epsilon.extime import Time |
485 | 15 | 14 | ||
486 | 16 | from twisted.web.client import getPage | 15 | from twisted.web.client import getPage |
487 | 17 | from twisted.web.http import datetimeToString | 16 | from twisted.web.http import datetimeToString |
488 | 18 | 17 | ||
496 | 19 | from txaws.credentials import AWSCredentials | 18 | from txaws.util import XML, calculate_md5 |
497 | 20 | from txaws.util import XML | 19 | from txaws.service import AWSService |
498 | 21 | 20 | ||
499 | 22 | 21 | ||
500 | 23 | def calculateMD5(data): | 22 | name_space = '{http://s3.amazonaws.com/doc/2006-03-01/}' |
494 | 24 | digest = md5(data).digest() | ||
495 | 25 | return b64encode(digest) | ||
501 | 26 | 23 | ||
502 | 27 | 24 | ||
503 | 28 | class S3Request(object): | 25 | class S3Request(object): |
507 | 29 | def __init__(self, verb, bucket=None, objectName=None, data='', | 26 | |
508 | 30 | contentType=None, metadata={}, rootURI='https://s3.amazonaws.com', | 27 | def __init__(self, verb, bucket=None, object_name=None, data='', |
509 | 31 | creds=None): | 28 | content_type=None, metadata={}, service=None): |
510 | 32 | self.verb = verb | 29 | self.verb = verb |
511 | 33 | self.bucket = bucket | 30 | self.bucket = bucket |
513 | 34 | self.objectName = objectName | 31 | self.object_name = object_name |
514 | 35 | self.data = data | 32 | self.data = data |
516 | 36 | self.contentType = contentType | 33 | self.content_type = content_type |
517 | 37 | self.metadata = metadata | 34 | self.metadata = metadata |
520 | 38 | self.rootURI = rootURI | 35 | self.service = service |
521 | 39 | self.creds = creds | 36 | self.service.endpoint = self.get_path() |
522 | 40 | self.date = datetimeToString() | 37 | self.date = datetimeToString() |
523 | 41 | 38 | ||
525 | 42 | def getURIPath(self): | 39 | def get_path(self): |
526 | 43 | path = '/' | 40 | path = '/' |
527 | 44 | if self.bucket is not None: | 41 | if self.bucket is not None: |
528 | 45 | path += self.bucket | 42 | path += self.bucket |
531 | 46 | if self.objectName is not None: | 43 | if self.object_name is not None: |
532 | 47 | path += '/' + self.objectName | 44 | path += '/' + self.object_name |
533 | 48 | return path | 45 | return path |
534 | 49 | 46 | ||
537 | 50 | def getURI(self): | 47 | def get_uri(self): |
538 | 51 | return self.rootURI + self.getURIPath() | 48 | return self.service.get_uri() |
539 | 52 | 49 | ||
541 | 53 | def getHeaders(self): | 50 | def get_headers(self): |
542 | 54 | headers = {'Content-Length': len(self.data), | 51 | headers = {'Content-Length': len(self.data), |
544 | 55 | 'Content-MD5': calculateMD5(self.data), | 52 | 'Content-MD5': calculate_md5(self.data), |
545 | 56 | 'Date': self.date} | 53 | 'Date': self.date} |
546 | 57 | 54 | ||
547 | 58 | for key, value in self.metadata.iteritems(): | 55 | for key, value in self.metadata.iteritems(): |
548 | 59 | headers['x-amz-meta-' + key] = value | 56 | headers['x-amz-meta-' + key] = value |
549 | 60 | 57 | ||
552 | 61 | if self.contentType is not None: | 58 | if self.content_type is not None: |
553 | 62 | headers['Content-Type'] = self.contentType | 59 | headers['Content-Type'] = self.content_type |
554 | 63 | 60 | ||
557 | 64 | if self.creds is not None: | 61 | if self.service is not None: |
558 | 65 | signature = self.getSignature(headers) | 62 | signature = self.get_signature(headers) |
559 | 66 | headers['Authorization'] = 'AWS %s:%s' % ( | 63 | headers['Authorization'] = 'AWS %s:%s' % ( |
562 | 67 | self.creds.access_key, signature) | 64 | self.service.access_key, signature) |
561 | 68 | |||
563 | 69 | return headers | 65 | return headers |
564 | 70 | 66 | ||
567 | 71 | def getCanonicalizedResource(self): | 67 | def get_canonicalized_resource(self): |
568 | 72 | return self.getURIPath() | 68 | return self.get_path() |
569 | 73 | 69 | ||
571 | 74 | def getCanonicalizedAmzHeaders(self, headers): | 70 | def get_canonicalized_amz_headers(self, headers): |
572 | 75 | result = '' | 71 | result = '' |
573 | 76 | headers = [(name.lower(), value) for name, value in headers.iteritems() | 72 | headers = [(name.lower(), value) for name, value in headers.iteritems() |
574 | 77 | if name.lower().startswith('x-amz-')] | 73 | if name.lower().startswith('x-amz-')] |
575 | 78 | headers.sort() | 74 | headers.sort() |
576 | 79 | return ''.join('%s:%s\n' % (name, value) for name, value in headers) | 75 | return ''.join('%s:%s\n' % (name, value) for name, value in headers) |
577 | 80 | 76 | ||
586 | 81 | def getSignature(self, headers): | 77 | def get_signature(self, headers): |
587 | 82 | text = self.verb + '\n' | 78 | text = (self.verb + '\n' + |
588 | 83 | text += headers.get('Content-MD5', '') + '\n' | 79 | headers.get('Content-MD5', '') + '\n' + |
589 | 84 | text += headers.get('Content-Type', '') + '\n' | 80 | headers.get('Content-Type', '') + '\n' + |
590 | 85 | text += headers.get('Date', '') + '\n' | 81 | headers.get('Date', '') + '\n' + |
591 | 86 | text += self.getCanonicalizedAmzHeaders(headers) | 82 | self.get_canonicalized_amz_headers(headers) + |
592 | 87 | text += self.getCanonicalizedResource() | 83 | self.get_canonicalized_resource()) |
593 | 88 | return self.creds.sign(text) | 84 | return self.service.sign(text) |
594 | 89 | 85 | ||
595 | 90 | def submit(self): | 86 | def submit(self): |
599 | 91 | return self.getPage( | 87 | return self.get_page(url=self.get_uri(), method=self.verb, |
600 | 92 | url=self.getURI(), method=self.verb, postdata=self.data, | 88 | postdata=self.data, headers=self.get_headers()) |
598 | 93 | headers=self.getHeaders()) | ||
601 | 94 | 89 | ||
603 | 95 | def getPage(self, *a, **kw): | 90 | def get_page(self, *a, **kw): |
604 | 96 | return getPage(*a, **kw) | 91 | return getPage(*a, **kw) |
605 | 97 | 92 | ||
606 | 98 | 93 | ||
607 | 99 | NS = '{http://s3.amazonaws.com/doc/2006-03-01/}' | ||
608 | 100 | |||
609 | 101 | |||
610 | 102 | class S3(object): | 94 | class S3(object): |
618 | 103 | rootURI = 'https://s3.amazonaws.com/' | 95 | |
619 | 104 | requestFactory = S3Request | 96 | request_factory = S3Request |
620 | 105 | 97 | ||
621 | 106 | def __init__(self, creds): | 98 | def __init__(self, service): |
622 | 107 | self.creds = creds | 99 | self.service = service |
623 | 108 | 100 | ||
624 | 109 | def makeRequest(self, *a, **kw): | 101 | def make_request(self, *a, **kw): |
625 | 110 | """ | 102 | """ |
626 | 111 | Create a request with the arguments passed in. | 103 | Create a request with the arguments passed in. |
627 | 112 | 104 | ||
629 | 113 | This uses the requestFactory attribute, adding the credentials to the | 105 | This uses the request_factory attribute, adding the service to the |
630 | 114 | arguments passed in. | 106 | arguments passed in. |
631 | 115 | """ | 107 | """ |
633 | 116 | return self.requestFactory(creds=self.creds, *a, **kw) | 108 | return self.request_factory(service=self.service, *a, **kw) |
634 | 117 | 109 | ||
636 | 118 | def _parseBucketList(self, response): | 110 | def _parse_bucket_list(self, response): |
637 | 119 | """ | 111 | """ |
638 | 120 | Parse XML bucket list response. | 112 | Parse XML bucket list response. |
639 | 121 | """ | 113 | """ |
640 | 122 | root = XML(response) | 114 | root = XML(response) |
643 | 123 | for bucket in root.find(NS + 'Buckets'): | 115 | for bucket in root.find(name_space + 'Buckets'): |
644 | 124 | timeText = bucket.findtext(NS + 'CreationDate') | 116 | timeText = bucket.findtext(name_space + 'CreationDate') |
645 | 125 | yield { | 117 | yield { |
647 | 126 | 'name': bucket.findtext(NS + 'Name'), | 118 | 'name': bucket.findtext(name_space + 'Name'), |
648 | 127 | 'created': Time.fromISO8601TimeAndDate(timeText), | 119 | 'created': Time.fromISO8601TimeAndDate(timeText), |
649 | 128 | } | 120 | } |
650 | 129 | 121 | ||
652 | 130 | def listBuckets(self): | 122 | def list_buckets(self): |
653 | 131 | """ | 123 | """ |
654 | 132 | List all buckets. | 124 | List all buckets. |
655 | 133 | 125 | ||
656 | 134 | Returns a list of all the buckets owned by the authenticated sender of | 126 | Returns a list of all the buckets owned by the authenticated sender of |
657 | 135 | the request. | 127 | the request. |
658 | 136 | """ | 128 | """ |
662 | 137 | d = self.makeRequest('GET').submit() | 129 | deferred = self.make_request('GET').submit() |
663 | 138 | d.addCallback(self._parseBucketList) | 130 | deferred.addCallback(self._parse_bucket_list) |
664 | 139 | return d | 131 | return deferred |
665 | 140 | 132 | ||
667 | 141 | def createBucket(self, bucket): | 133 | def create_bucket(self, bucket): |
668 | 142 | """ | 134 | """ |
669 | 143 | Create a new bucket. | 135 | Create a new bucket. |
670 | 144 | """ | 136 | """ |
672 | 145 | return self.makeRequest('PUT', bucket).submit() | 137 | return self.make_request('PUT', bucket).submit() |
673 | 146 | 138 | ||
675 | 147 | def deleteBucket(self, bucket): | 139 | def delete_bucket(self, bucket): |
676 | 148 | """ | 140 | """ |
677 | 149 | Delete a bucket. | 141 | Delete a bucket. |
678 | 150 | 142 | ||
679 | 151 | The bucket must be empty before it can be deleted. | 143 | The bucket must be empty before it can be deleted. |
680 | 152 | """ | 144 | """ |
682 | 153 | return self.makeRequest('DELETE', bucket).submit() | 145 | return self.make_request('DELETE', bucket).submit() |
683 | 154 | 146 | ||
686 | 155 | def putObject(self, bucket, objectName, data, contentType=None, | 147 | def put_object(self, bucket, object_name, data, content_type=None, |
687 | 156 | metadata={}): | 148 | metadata={}): |
688 | 157 | """ | 149 | """ |
689 | 158 | Put an object in a bucket. | 150 | Put an object in a bucket. |
690 | 159 | 151 | ||
691 | 160 | Any existing object of the same name will be replaced. | 152 | Any existing object of the same name will be replaced. |
692 | 161 | """ | 153 | """ |
695 | 162 | return self.makeRequest( | 154 | return self.make_request('PUT', bucket, object_name, data, |
696 | 163 | 'PUT', bucket, objectName, data, contentType, metadata).submit() | 155 | content_type, metadata).submit() |
697 | 164 | 156 | ||
699 | 165 | def getObject(self, bucket, objectName): | 157 | def get_object(self, bucket, object_name): |
700 | 166 | """ | 158 | """ |
701 | 167 | Get an object from a bucket. | 159 | Get an object from a bucket. |
702 | 168 | """ | 160 | """ |
704 | 169 | return self.makeRequest('GET', bucket, objectName).submit() | 161 | return self.make_request('GET', bucket, object_name).submit() |
705 | 170 | 162 | ||
707 | 171 | def headObject(self, bucket, objectName): | 163 | def head_object(self, bucket, object_name): |
708 | 172 | """ | 164 | """ |
709 | 173 | Retrieve object metadata only. | 165 | Retrieve object metadata only. |
710 | 174 | 166 | ||
712 | 175 | This is like getObject, but the object's content is not retrieved. | 167 | This is like get_object, but the object's content is not retrieved. |
713 | 176 | Currently the metadata is not returned to the caller either, so this | 168 | Currently the metadata is not returned to the caller either, so this |
714 | 177 | method is mostly useless, and only provided for completeness. | 169 | method is mostly useless, and only provided for completeness. |
715 | 178 | """ | 170 | """ |
717 | 179 | return self.makeRequest('HEAD', bucket, objectName).submit() | 171 | return self.make_request('HEAD', bucket, object_name).submit() |
718 | 180 | 172 | ||
720 | 181 | def deleteObject(self, bucket, objectName): | 173 | def delete_object(self, bucket, object_name): |
721 | 182 | """ | 174 | """ |
722 | 183 | Delete an object from a bucket. | 175 | Delete an object from a bucket. |
723 | 184 | 176 | ||
724 | 185 | Once deleted, there is no method to restore or undelete an object. | 177 | Once deleted, there is no method to restore or undelete an object. |
725 | 186 | """ | 178 | """ |
727 | 187 | return self.makeRequest('DELETE', bucket, objectName).submit() | 179 | return self.make_request('DELETE', bucket, object_name).submit() |
728 | 188 | 180 | ||
729 | === added file 'txaws/storage/service.py' | |||
730 | --- txaws/storage/service.py 1970-01-01 00:00:00 +0000 | |||
731 | +++ txaws/storage/service.py 2009-08-20 19:09:56 +0000 | |||
732 | @@ -0,0 +1,19 @@ | |||
733 | 1 | # Copyright (C) 2009 Duncan McGreggor <duncan@canonical.com> | ||
734 | 2 | # Licenced under the txaws licence available at /LICENSE in the txaws source. | ||
735 | 3 | |||
736 | 4 | from txaws.service import AWSService | ||
737 | 5 | |||
738 | 6 | |||
739 | 7 | S3_HOST = "s3.amazonaws.com" | ||
740 | 8 | |||
741 | 9 | |||
742 | 10 | class S3Service(AWSService): | ||
743 | 11 | """ | ||
744 | 12 | This service uses the standard S3 host defined with S3_HOST by default. To | ||
745 | 13 | override this behaviour, simply pass the desired value in the "host" | ||
746 | 14 | keyword parameter. | ||
747 | 15 | |||
748 | 16 | For more details, see txaws.service.AWSService. | ||
749 | 17 | """ | ||
750 | 18 | default_host = S3_HOST | ||
751 | 19 | default_schema = "https" | ||
752 | 0 | 20 | ||
753 | === modified file 'txaws/storage/test/test_client.py' | |||
754 | --- txaws/storage/test/test_client.py 2009-08-15 03:28:45 +0000 | |||
755 | +++ txaws/storage/test/test_client.py 2009-08-20 19:09:56 +0000 | |||
756 | @@ -4,20 +4,23 @@ | |||
757 | 4 | 4 | ||
758 | 5 | from twisted.internet.defer import succeed | 5 | from twisted.internet.defer import succeed |
759 | 6 | 6 | ||
761 | 7 | from txaws.credentials import AWSCredentials | 7 | from txaws.util import calculate_md5 |
762 | 8 | from txaws.tests import TXAWSTestCase | 8 | from txaws.tests import TXAWSTestCase |
764 | 9 | from txaws.storage.client import S3, S3Request, calculateMD5 | 9 | from txaws.storage.service import S3Service |
765 | 10 | from txaws.storage.client import S3, S3Request | ||
766 | 11 | |||
767 | 10 | 12 | ||
768 | 11 | 13 | ||
769 | 12 | class StubbedS3Request(S3Request): | 14 | class StubbedS3Request(S3Request): |
771 | 13 | def getPage(self, url, method, postdata, headers): | 15 | |
772 | 16 | def get_page(self, url, method, postdata, headers): | ||
773 | 14 | self.getPageArgs = (url, method, postdata, headers) | 17 | self.getPageArgs = (url, method, postdata, headers) |
774 | 15 | return succeed('') | 18 | return succeed('') |
775 | 16 | 19 | ||
776 | 17 | 20 | ||
780 | 18 | class RequestTests(TXAWSTestCase): | 21 | class RequestTestCase(TXAWSTestCase): |
781 | 19 | creds = AWSCredentials(access_key='0PN5J17HBGZHT7JJ3X82', | 22 | |
782 | 20 | secret_key='uV3F3YluFJax1cknvbcGwgjvx4QpvB+leU8dUj2o') | 23 | service = S3Service(access_key='fookeyid', secret_key='barsecretkey') |
783 | 21 | 24 | ||
784 | 22 | def test_objectRequest(self): | 25 | def test_objectRequest(self): |
785 | 23 | """ | 26 | """ |
786 | @@ -26,20 +29,23 @@ | |||
787 | 26 | DATA = 'objectData' | 29 | DATA = 'objectData' |
788 | 27 | DIGEST = 'zhdB6gwvocWv/ourYUWMxA==' | 30 | DIGEST = 'zhdB6gwvocWv/ourYUWMxA==' |
789 | 28 | 31 | ||
793 | 29 | request = S3Request( | 32 | request = S3Request('PUT', 'somebucket', 'object/name/here', DATA, |
794 | 30 | 'PUT', 'somebucket', 'object/name/here', DATA, | 33 | content_type='text/plain', metadata={'foo': 'bar'}, |
795 | 31 | contentType='text/plain', metadata={'foo': 'bar'}) | 34 | service=self.service) |
796 | 35 | request.get_signature = lambda headers: "TESTINGSIG=" | ||
797 | 32 | self.assertEqual(request.verb, 'PUT') | 36 | self.assertEqual(request.verb, 'PUT') |
798 | 33 | self.assertEqual( | 37 | self.assertEqual( |
800 | 34 | request.getURI(), | 38 | request.get_uri(), |
801 | 35 | 'https://s3.amazonaws.com/somebucket/object/name/here') | 39 | 'https://s3.amazonaws.com/somebucket/object/name/here') |
803 | 36 | headers = request.getHeaders() | 40 | headers = request.get_headers() |
804 | 37 | self.assertNotEqual(headers.pop('Date'), '') | 41 | self.assertNotEqual(headers.pop('Date'), '') |
810 | 38 | self.assertEqual(headers, | 42 | self.assertEqual( |
811 | 39 | {'Content-Type': 'text/plain', | 43 | headers, { |
812 | 40 | 'Content-Length': len(DATA), | 44 | 'Authorization': 'AWS fookeyid:TESTINGSIG=', |
813 | 41 | 'Content-MD5': DIGEST, | 45 | 'Content-Type': 'text/plain', |
814 | 42 | 'x-amz-meta-foo': 'bar'}) | 46 | 'Content-Length': len(DATA), |
815 | 47 | 'Content-MD5': DIGEST, | ||
816 | 48 | 'x-amz-meta-foo': 'bar'}) | ||
817 | 43 | self.assertEqual(request.data, 'objectData') | 49 | self.assertEqual(request.data, 'objectData') |
818 | 44 | 50 | ||
819 | 45 | def test_bucketRequest(self): | 51 | def test_bucketRequest(self): |
820 | @@ -48,42 +54,46 @@ | |||
821 | 48 | """ | 54 | """ |
822 | 49 | DIGEST = '1B2M2Y8AsgTpgAmY7PhCfg==' | 55 | DIGEST = '1B2M2Y8AsgTpgAmY7PhCfg==' |
823 | 50 | 56 | ||
825 | 51 | request = S3Request('GET', 'somebucket') | 57 | request = S3Request('GET', 'somebucket', service=self.service) |
826 | 58 | request.get_signature = lambda headers: "TESTINGSIG=" | ||
827 | 52 | self.assertEqual(request.verb, 'GET') | 59 | self.assertEqual(request.verb, 'GET') |
828 | 53 | self.assertEqual( | 60 | self.assertEqual( |
831 | 54 | request.getURI(), 'https://s3.amazonaws.com/somebucket') | 61 | request.get_uri(), 'https://s3.amazonaws.com/somebucket') |
832 | 55 | headers = request.getHeaders() | 62 | headers = request.get_headers() |
833 | 56 | self.assertNotEqual(headers.pop('Date'), '') | 63 | self.assertNotEqual(headers.pop('Date'), '') |
837 | 57 | self.assertEqual(headers, | 64 | self.assertEqual( |
838 | 58 | {'Content-Length': 0, | 65 | headers, { |
839 | 59 | 'Content-MD5': DIGEST}) | 66 | 'Authorization': 'AWS fookeyid:TESTINGSIG=', |
840 | 67 | 'Content-Length': 0, | ||
841 | 68 | 'Content-MD5': DIGEST}) | ||
842 | 60 | self.assertEqual(request.data, '') | 69 | self.assertEqual(request.data, '') |
843 | 61 | 70 | ||
844 | 62 | def test_submit(self): | 71 | def test_submit(self): |
845 | 63 | """ | 72 | """ |
846 | 64 | Submitting the request should invoke getPage correctly. | 73 | Submitting the request should invoke getPage correctly. |
847 | 65 | """ | 74 | """ |
849 | 66 | request = StubbedS3Request('GET', 'somebucket') | 75 | request = StubbedS3Request('GET', 'somebucket', service=self.service) |
850 | 67 | 76 | ||
851 | 68 | def _postCheck(result): | 77 | def _postCheck(result): |
852 | 69 | self.assertEqual(result, '') | 78 | self.assertEqual(result, '') |
853 | 70 | 79 | ||
854 | 71 | url, method, postdata, headers = request.getPageArgs | 80 | url, method, postdata, headers = request.getPageArgs |
856 | 72 | self.assertEqual(url, request.getURI()) | 81 | self.assertEqual(url, request.get_uri()) |
857 | 73 | self.assertEqual(method, request.verb) | 82 | self.assertEqual(method, request.verb) |
858 | 74 | self.assertEqual(postdata, request.data) | 83 | self.assertEqual(postdata, request.data) |
860 | 75 | self.assertEqual(headers, request.getHeaders()) | 84 | self.assertEqual(headers, request.get_headers()) |
861 | 76 | 85 | ||
862 | 77 | return request.submit().addCallback(_postCheck) | 86 | return request.submit().addCallback(_postCheck) |
863 | 78 | 87 | ||
864 | 79 | def test_authenticationTestCases(self): | 88 | def test_authenticationTestCases(self): |
867 | 80 | req = S3Request('GET', creds=self.creds) | 89 | request = S3Request('GET', service=self.service) |
868 | 81 | req.date = 'Wed, 28 Mar 2007 01:29:59 +0000' | 90 | request.get_signature = lambda headers: "TESTINGSIG=" |
869 | 91 | request.date = 'Wed, 28 Mar 2007 01:29:59 +0000' | ||
870 | 82 | 92 | ||
872 | 83 | headers = req.getHeaders() | 93 | headers = request.get_headers() |
873 | 84 | self.assertEqual( | 94 | self.assertEqual( |
876 | 85 | headers['Authorization'], | 95 | headers['Authorization'], |
877 | 86 | 'AWS 0PN5J17HBGZHT7JJ3X82:jF7L3z/FTV47vagZzhKupJ9oNig=') | 96 | 'AWS fookeyid:TESTINGSIG=') |
878 | 87 | 97 | ||
879 | 88 | 98 | ||
880 | 89 | class InertRequest(S3Request): | 99 | class InertRequest(S3Request): |
881 | @@ -110,13 +120,13 @@ | |||
882 | 110 | """ | 120 | """ |
883 | 111 | Testable version of S3. | 121 | Testable version of S3. |
884 | 112 | 122 | ||
886 | 113 | This subclass stubs requestFactory to use InertRequest, making it easy to | 123 | This subclass stubs request_factory to use InertRequest, making it easy to |
887 | 114 | assert things about the requests that are created in response to various | 124 | assert things about the requests that are created in response to various |
888 | 115 | operations. | 125 | operations. |
889 | 116 | """ | 126 | """ |
890 | 117 | response = None | 127 | response = None |
891 | 118 | 128 | ||
893 | 119 | def requestFactory(self, *a, **kw): | 129 | def request_factory(self, *a, **kw): |
894 | 120 | req = InertRequest(response=self.response, *a, **kw) | 130 | req = InertRequest(response=self.response, *a, **kw) |
895 | 121 | self._lastRequest = req | 131 | self._lastRequest = req |
896 | 122 | return req | 132 | return req |
897 | @@ -148,34 +158,34 @@ | |||
898 | 148 | 158 | ||
899 | 149 | def setUp(self): | 159 | def setUp(self): |
900 | 150 | TXAWSTestCase.setUp(self) | 160 | TXAWSTestCase.setUp(self) |
902 | 151 | self.creds = AWSCredentials( | 161 | self.service = S3Service( |
903 | 152 | access_key='accessKey', secret_key='secretKey') | 162 | access_key='accessKey', secret_key='secretKey') |
905 | 153 | self.s3 = TestableS3(creds=self.creds) | 163 | self.s3 = TestableS3(service=self.service) |
906 | 154 | 164 | ||
908 | 155 | def test_makeRequest(self): | 165 | def test_make_request(self): |
909 | 156 | """ | 166 | """ |
911 | 157 | Test that makeRequest passes in the service credentials. | 167 | Test that make_request passes in the service object. |
912 | 158 | """ | 168 | """ |
913 | 159 | marker = object() | 169 | marker = object() |
914 | 160 | 170 | ||
915 | 161 | def _cb(*a, **kw): | 171 | def _cb(*a, **kw): |
917 | 162 | self.assertEqual(kw['creds'], self.creds) | 172 | self.assertEqual(kw['service'], self.service) |
918 | 163 | return marker | 173 | return marker |
919 | 164 | 174 | ||
922 | 165 | self.s3.requestFactory = _cb | 175 | self.s3.request_factory = _cb |
923 | 166 | self.assertIdentical(self.s3.makeRequest('GET'), marker) | 176 | self.assertIdentical(self.s3.make_request('GET'), marker) |
924 | 167 | 177 | ||
926 | 168 | def test_listBuckets(self): | 178 | def test_list_buckets(self): |
927 | 169 | self.s3.response = samples['ListAllMyBucketsResult'] | 179 | self.s3.response = samples['ListAllMyBucketsResult'] |
929 | 170 | d = self.s3.listBuckets() | 180 | d = self.s3.list_buckets() |
930 | 171 | 181 | ||
931 | 172 | req = self.s3._lastRequest | 182 | req = self.s3._lastRequest |
932 | 173 | self.assertTrue(req.submitted) | 183 | self.assertTrue(req.submitted) |
933 | 174 | self.assertEqual(req.verb, 'GET') | 184 | self.assertEqual(req.verb, 'GET') |
934 | 175 | self.assertEqual(req.bucket, None) | 185 | self.assertEqual(req.bucket, None) |
936 | 176 | self.assertEqual(req.objectName, None) | 186 | self.assertEqual(req.object_name, None) |
937 | 177 | 187 | ||
939 | 178 | def _checkResult(buckets): | 188 | def _check_result(buckets): |
940 | 179 | self.assertEqual( | 189 | self.assertEqual( |
941 | 180 | list(buckets), | 190 | list(buckets), |
942 | 181 | [{'name': u'quotes', | 191 | [{'name': u'quotes', |
943 | @@ -184,61 +194,61 @@ | |||
944 | 184 | {'name': u'samples', | 194 | {'name': u'samples', |
945 | 185 | 'created': Time.fromDatetime( | 195 | 'created': Time.fromDatetime( |
946 | 186 | datetime(2006, 2, 3, 16, 41, 58))}]) | 196 | datetime(2006, 2, 3, 16, 41, 58))}]) |
948 | 187 | return d.addCallback(_checkResult) | 197 | return d.addCallback(_check_result) |
949 | 188 | 198 | ||
952 | 189 | def test_createBucket(self): | 199 | def test_create_bucket(self): |
953 | 190 | self.s3.createBucket('foo') | 200 | self.s3.create_bucket('foo') |
954 | 191 | req = self.s3._lastRequest | 201 | req = self.s3._lastRequest |
955 | 192 | self.assertTrue(req.submitted) | 202 | self.assertTrue(req.submitted) |
956 | 193 | self.assertEqual(req.verb, 'PUT') | 203 | self.assertEqual(req.verb, 'PUT') |
957 | 194 | self.assertEqual(req.bucket, 'foo') | 204 | self.assertEqual(req.bucket, 'foo') |
959 | 195 | self.assertEqual(req.objectName, None) | 205 | self.assertEqual(req.object_name, None) |
960 | 196 | 206 | ||
963 | 197 | def test_deleteBucket(self): | 207 | def test_delete_bucket(self): |
964 | 198 | self.s3.deleteBucket('foo') | 208 | self.s3.delete_bucket('foo') |
965 | 199 | req = self.s3._lastRequest | 209 | req = self.s3._lastRequest |
966 | 200 | self.assertTrue(req.submitted) | 210 | self.assertTrue(req.submitted) |
967 | 201 | self.assertEqual(req.verb, 'DELETE') | 211 | self.assertEqual(req.verb, 'DELETE') |
968 | 202 | self.assertEqual(req.bucket, 'foo') | 212 | self.assertEqual(req.bucket, 'foo') |
970 | 203 | self.assertEqual(req.objectName, None) | 213 | self.assertEqual(req.object_name, None) |
971 | 204 | 214 | ||
974 | 205 | def test_putObject(self): | 215 | def test_put_object(self): |
975 | 206 | self.s3.putObject( | 216 | self.s3.put_object( |
976 | 207 | 'foobucket', 'foo', 'data', 'text/plain', {'foo': 'bar'}) | 217 | 'foobucket', 'foo', 'data', 'text/plain', {'foo': 'bar'}) |
977 | 208 | req = self.s3._lastRequest | 218 | req = self.s3._lastRequest |
978 | 209 | self.assertTrue(req.submitted) | 219 | self.assertTrue(req.submitted) |
979 | 210 | self.assertEqual(req.verb, 'PUT') | 220 | self.assertEqual(req.verb, 'PUT') |
980 | 211 | self.assertEqual(req.bucket, 'foobucket') | 221 | self.assertEqual(req.bucket, 'foobucket') |
982 | 212 | self.assertEqual(req.objectName, 'foo') | 222 | self.assertEqual(req.object_name, 'foo') |
983 | 213 | self.assertEqual(req.data, 'data') | 223 | self.assertEqual(req.data, 'data') |
985 | 214 | self.assertEqual(req.contentType, 'text/plain') | 224 | self.assertEqual(req.content_type, 'text/plain') |
986 | 215 | self.assertEqual(req.metadata, {'foo': 'bar'}) | 225 | self.assertEqual(req.metadata, {'foo': 'bar'}) |
987 | 216 | 226 | ||
990 | 217 | def test_getObject(self): | 227 | def test_get_object(self): |
991 | 218 | self.s3.getObject('foobucket', 'foo') | 228 | self.s3.get_object('foobucket', 'foo') |
992 | 219 | req = self.s3._lastRequest | 229 | req = self.s3._lastRequest |
993 | 220 | self.assertTrue(req.submitted) | 230 | self.assertTrue(req.submitted) |
994 | 221 | self.assertEqual(req.verb, 'GET') | 231 | self.assertEqual(req.verb, 'GET') |
995 | 222 | self.assertEqual(req.bucket, 'foobucket') | 232 | self.assertEqual(req.bucket, 'foobucket') |
997 | 223 | self.assertEqual(req.objectName, 'foo') | 233 | self.assertEqual(req.object_name, 'foo') |
998 | 224 | 234 | ||
1001 | 225 | def test_headObject(self): | 235 | def test_head_object(self): |
1002 | 226 | self.s3.headObject('foobucket', 'foo') | 236 | self.s3.head_object('foobucket', 'foo') |
1003 | 227 | req = self.s3._lastRequest | 237 | req = self.s3._lastRequest |
1004 | 228 | self.assertTrue(req.submitted) | 238 | self.assertTrue(req.submitted) |
1005 | 229 | self.assertEqual(req.verb, 'HEAD') | 239 | self.assertEqual(req.verb, 'HEAD') |
1006 | 230 | self.assertEqual(req.bucket, 'foobucket') | 240 | self.assertEqual(req.bucket, 'foobucket') |
1008 | 231 | self.assertEqual(req.objectName, 'foo') | 241 | self.assertEqual(req.object_name, 'foo') |
1009 | 232 | 242 | ||
1012 | 233 | def test_deleteObject(self): | 243 | def test_delete_object(self): |
1013 | 234 | self.s3.deleteObject('foobucket', 'foo') | 244 | self.s3.delete_object('foobucket', 'foo') |
1014 | 235 | req = self.s3._lastRequest | 245 | req = self.s3._lastRequest |
1015 | 236 | self.assertTrue(req.submitted) | 246 | self.assertTrue(req.submitted) |
1016 | 237 | self.assertEqual(req.verb, 'DELETE') | 247 | self.assertEqual(req.verb, 'DELETE') |
1017 | 238 | self.assertEqual(req.bucket, 'foobucket') | 248 | self.assertEqual(req.bucket, 'foobucket') |
1019 | 239 | self.assertEqual(req.objectName, 'foo') | 249 | self.assertEqual(req.object_name, 'foo') |
1020 | 240 | 250 | ||
1021 | 241 | 251 | ||
1022 | 242 | class MiscellaneousTests(TXAWSTestCase): | 252 | class MiscellaneousTests(TXAWSTestCase): |
1023 | 243 | def test_contentMD5(self): | 253 | def test_contentMD5(self): |
1025 | 244 | self.assertEqual(calculateMD5('somedata'), 'rvr3UC1SmUw7AZV2NqPN0g==') | 254 | self.assertEqual(calculate_md5('somedata'), 'rvr3UC1SmUw7AZV2NqPN0g==') |
1026 | 245 | 255 | ||
1027 | === removed file 'txaws/tests/test_credentials.py' | |||
1028 | --- txaws/tests/test_credentials.py 2009-08-17 11:18:56 +0000 | |||
1029 | +++ txaws/tests/test_credentials.py 1970-01-01 00:00:00 +0000 | |||
1030 | @@ -1,41 +0,0 @@ | |||
1031 | 1 | # Copyright (C) 2009 Robert Collins <robertc@robertcollins.net> | ||
1032 | 2 | # Licenced under the txaws licence available at /LICENSE in the txaws source. | ||
1033 | 3 | |||
1034 | 4 | import os | ||
1035 | 5 | |||
1036 | 6 | from twisted.trial.unittest import TestCase | ||
1037 | 7 | from txaws.tests import TXAWSTestCase | ||
1038 | 8 | |||
1039 | 9 | from txaws.credentials import AWSCredentials | ||
1040 | 10 | |||
1041 | 11 | |||
1042 | 12 | class TestCredentials(TXAWSTestCase): | ||
1043 | 13 | |||
1044 | 14 | def test_no_access_errors(self): | ||
1045 | 15 | # Without anything in os.environ, AWSCredentials() blows up | ||
1046 | 16 | os.environ['AWS_SECRET_ACCESS_KEY'] = 'foo' | ||
1047 | 17 | self.assertRaises(Exception, AWSCredentials) | ||
1048 | 18 | |||
1049 | 19 | def test_no_secret_errors(self): | ||
1050 | 20 | # Without anything in os.environ, AWSCredentials() blows up | ||
1051 | 21 | os.environ['AWS_ACCESS_KEY_ID'] = 'bar' | ||
1052 | 22 | self.assertRaises(Exception, AWSCredentials) | ||
1053 | 23 | |||
1054 | 24 | def test_found_values_used(self): | ||
1055 | 25 | os.environ['AWS_SECRET_ACCESS_KEY'] = 'foo' | ||
1056 | 26 | os.environ['AWS_ACCESS_KEY_ID'] = 'bar' | ||
1057 | 27 | creds = AWSCredentials() | ||
1058 | 28 | self.assertEqual('foo', creds.secret_key) | ||
1059 | 29 | self.assertEqual('bar', creds.access_key) | ||
1060 | 30 | |||
1061 | 31 | def test_explicit_access_key(self): | ||
1062 | 32 | os.environ['AWS_SECRET_ACCESS_KEY'] = 'foo' | ||
1063 | 33 | creds = AWSCredentials(access_key='bar') | ||
1064 | 34 | self.assertEqual('foo', creds.secret_key) | ||
1065 | 35 | self.assertEqual('bar', creds.access_key) | ||
1066 | 36 | |||
1067 | 37 | def test_explicit_secret_key(self): | ||
1068 | 38 | os.environ['AWS_ACCESS_KEY_ID'] = 'bar' | ||
1069 | 39 | creds = AWSCredentials(secret_key='foo') | ||
1070 | 40 | self.assertEqual('foo', creds.secret_key) | ||
1071 | 41 | self.assertEqual('bar', creds.access_key) | ||
1072 | 42 | 0 | ||
1073 | === added file 'txaws/tests/test_service.py' | |||
1074 | --- txaws/tests/test_service.py 1970-01-01 00:00:00 +0000 | |||
1075 | +++ txaws/tests/test_service.py 2009-08-20 19:09:56 +0000 | |||
1076 | @@ -0,0 +1,88 @@ | |||
1077 | 1 | # Copyright (C) 2009 Duncan McGreggor <duncan@canonical.com> | ||
1078 | 2 | # Licenced under the txaws licence available at /LICENSE in the txaws source. | ||
1079 | 3 | |||
1080 | 4 | import os | ||
1081 | 5 | |||
1082 | 6 | from txaws.service import AWSService, ENV_ACCESS_KEY, ENV_SECRET_KEY | ||
1083 | 7 | from txaws.tests import TXAWSTestCase | ||
1084 | 8 | |||
1085 | 9 | |||
1086 | 10 | class AWSServiceTestCase(TXAWSTestCase): | ||
1087 | 11 | |||
1088 | 12 | def setUp(self): | ||
1089 | 13 | self.service = AWSService("fookeyid", "barsecretkey", | ||
1090 | 14 | "http://my.service/da_endpoint") | ||
1091 | 15 | self.addCleanup(self.clean_environment) | ||
1092 | 16 | |||
1093 | 17 | def clean_environment(self): | ||
1094 | 18 | if os.environ.has_key(ENV_ACCESS_KEY): | ||
1095 | 19 | del os.environ[ENV_ACCESS_KEY] | ||
1096 | 20 | if os.environ.has_key(ENV_SECRET_KEY): | ||
1097 | 21 | del os.environ[ENV_SECRET_KEY] | ||
1098 | 22 | |||
1099 | 23 | def test_simple_creation(self): | ||
1100 | 24 | service = AWSService("fookeyid", "barsecretkey") | ||
1101 | 25 | self.assertEquals(service.access_key, "fookeyid") | ||
1102 | 26 | self.assertEquals(service.secret_key, "barsecretkey") | ||
1103 | 27 | self.assertEquals(service.schema, "https") | ||
1104 | 28 | self.assertEquals(service.host, "") | ||
1105 | 29 | self.assertEquals(service.port, 80) | ||
1106 | 30 | self.assertEquals(service.endpoint, "/") | ||
1107 | 31 | self.assertEquals(service.method, "GET") | ||
1108 | 32 | |||
1109 | 33 | def test_no_access_errors(self): | ||
1110 | 34 | # Without anything in os.environ, AWSService() blows up | ||
1111 | 35 | os.environ[ENV_SECRET_KEY] = "bar" | ||
1112 | 36 | self.assertRaises(ValueError, AWSService) | ||
1113 | 37 | |||
1114 | 38 | def test_no_secret_errors(self): | ||
1115 | 39 | # Without anything in os.environ, AWSService() blows up | ||
1116 | 40 | os.environ[ENV_ACCESS_KEY] = "foo" | ||
1117 | 41 | self.assertRaises(ValueError, AWSService) | ||
1118 | 42 | |||
1119 | 43 | def test_found_values_used(self): | ||
1120 | 44 | os.environ[ENV_ACCESS_KEY] = "foo" | ||
1121 | 45 | os.environ[ENV_SECRET_KEY] = "bar" | ||
1122 | 46 | service = AWSService() | ||
1123 | 47 | self.assertEqual("foo", service.access_key) | ||
1124 | 48 | self.assertEqual("bar", service.secret_key) | ||
1125 | 49 | self.clean_environment() | ||
1126 | 50 | |||
1127 | 51 | def test_explicit_access_key(self): | ||
1128 | 52 | os.environ[ENV_SECRET_KEY] = "foo" | ||
1129 | 53 | service = AWSService(access_key="bar") | ||
1130 | 54 | self.assertEqual("foo", service.secret_key) | ||
1131 | 55 | self.assertEqual("bar", service.access_key) | ||
1132 | 56 | |||
1133 | 57 | def test_explicit_secret_key(self): | ||
1134 | 58 | os.environ[ENV_ACCESS_KEY] = "bar" | ||
1135 | 59 | service = AWSService(secret_key="foo") | ||
1136 | 60 | self.assertEqual("foo", service.secret_key) | ||
1137 | 61 | self.assertEqual("bar", service.access_key) | ||
1138 | 62 | |||
1139 | 63 | def test_parse_uri(self): | ||
1140 | 64 | self.assertEquals(self.service.schema, "http") | ||
1141 | 65 | self.assertEquals(self.service.host, "my.service") | ||
1142 | 66 | self.assertEquals(self.service.port, 80) | ||
1143 | 67 | self.assertEquals(self.service.endpoint, "/da_endpoint") | ||
1144 | 68 | |||
1145 | 69 | def test_parse_uri_https_and_custom_port(self): | ||
1146 | 70 | service = AWSService("foo", "bar", "https://my.service:8080/endpoint") | ||
1147 | 71 | self.assertEquals(service.schema, "https") | ||
1148 | 72 | self.assertEquals(service.host, "my.service") | ||
1149 | 73 | self.assertEquals(service.port, 8080) | ||
1150 | 74 | self.assertEquals(service.endpoint, "/endpoint") | ||
1151 | 75 | |||
1152 | 76 | def test_custom_method(self): | ||
1153 | 77 | service = AWSService("foo", "bar", "http://service/endpoint", "PUT") | ||
1154 | 78 | self.assertEquals(service.method, "PUT") | ||
1155 | 79 | |||
1156 | 80 | def test_get_uri(self): | ||
1157 | 81 | uri = self.service.get_uri() | ||
1158 | 82 | self.assertEquals(uri, "http://my.service/da_endpoint") | ||
1159 | 83 | |||
1160 | 84 | def test_get_uri_custom_port(self): | ||
1161 | 85 | uri = "https://my.service:8080/endpoint" | ||
1162 | 86 | service = AWSService("foo", "bar", uri) | ||
1163 | 87 | new_uri = service.get_uri() | ||
1164 | 88 | self.assertEquals(new_uri, uri) | ||
1165 | 0 | 89 | ||
1166 | === modified file 'txaws/util.py' | |||
1167 | --- txaws/util.py 2009-08-17 11:18:56 +0000 | |||
1168 | +++ txaws/util.py 2009-08-18 20:48:59 +0000 | |||
1169 | @@ -4,10 +4,10 @@ | |||
1170 | 4 | services. | 4 | services. |
1171 | 5 | """ | 5 | """ |
1172 | 6 | 6 | ||
1173 | 7 | import time | ||
1174 | 8 | import hmac | ||
1175 | 9 | from hashlib import sha1, md5 | ||
1176 | 7 | from base64 import b64encode | 10 | from base64 import b64encode |
1177 | 8 | from hashlib import sha1 | ||
1178 | 9 | import hmac | ||
1179 | 10 | import time | ||
1180 | 11 | 11 | ||
1181 | 12 | # Import XML from somwhere; here in one place to prevent duplication. | 12 | # Import XML from somwhere; here in one place to prevent duplication. |
1182 | 13 | try: | 13 | try: |
1183 | @@ -19,6 +19,11 @@ | |||
1184 | 19 | __all__ = ['hmac_sha1', 'iso8601time'] | 19 | __all__ = ['hmac_sha1', 'iso8601time'] |
1185 | 20 | 20 | ||
1186 | 21 | 21 | ||
1187 | 22 | def calculate_md5(data): | ||
1188 | 23 | digest = md5(data).digest() | ||
1189 | 24 | return b64encode(digest) | ||
1190 | 25 | |||
1191 | 26 | |||
1192 | 22 | def hmac_sha1(secret, data): | 27 | def hmac_sha1(secret, data): |
1193 | 23 | digest = hmac.new(secret, data, sha1).digest() | 28 | digest = hmac.new(secret, data, sha1).digest() |
1194 | 24 | return b64encode(digest) | 29 | return b64encode(digest) |
This branch adds support for a service object that manages host endpoints as well as authorization keys (thus obviating the need for the AWSCredential object).