← Back to team overview

txawsteam team mailing list archive

[Merge] lp:~oubiwann/txaws/416109-arbitrary-endpoints into lp:txaws

 

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).
-- 
https://code.launchpad.net/~oubiwann/txaws/416109-arbitrary-endpoints/+merge/10477
Your team txAWS Team is subscribed to branch lp:txaws.
=== modified file 'txaws/client/gui/gtk.py'
--- txaws/client/gui/gtk.py	2009-08-18 22:53:53 +0000
+++ txaws/client/gui/gtk.py	2009-08-20 19:14:32 +0000
@@ -8,7 +8,7 @@
 import gobject
 import gtk
 
-from txaws.credentials import AWSCredentials
+from txaws.ec2.service import EC2Service
 
 
 __all__ = ['main']
@@ -27,10 +27,10 @@
         # Nested import because otherwise we get 'reactor already installed'.
         self.password_dialog = None
         try:
-            creds = AWSCredentials()
+            service = AWSService()
         except ValueError:
-            creds = self.from_gnomekeyring()
-        self.create_client(creds)
+            service = self.from_gnomekeyring()
+        self.create_client(service)
         menu = '''
             <ui>
              <menubar name="Menubar">
@@ -54,10 +54,10 @@
             '/Menubar/Menu/Stop instances').props.parent
         self.connect('popup-menu', self.on_popup_menu)
 
-    def create_client(self, creds):
+    def create_client(self, service):
         from txaws.ec2.client import EC2Client
-        if creds is not None:
-            self.client = EC2Client(creds=creds)
+        if service is not None:
+            self.client = EC2Client(service=service)
             self.on_activate(None)
         else:
             # waiting on user entered credentials.
@@ -65,7 +65,7 @@
 
     def from_gnomekeyring(self):
         # Try for gtk gui specific credentials.
-        creds = None
+        service = None
         try:
             items = gnomekeyring.find_items_sync(
                 gnomekeyring.ITEM_GENERIC_SECRET,
@@ -78,7 +78,7 @@
             return None
         else:
             key_id, secret_key = items[0].secret.split(':')
-            return AWSCredentials(access_key=key_id, secret_key=secret_key)
+            return EC2Service(access_key=key_id, secret_key=secret_key)
 
     def show_a_password_dialog(self):
         self.password_dialog = gtk.Dialog(
@@ -133,8 +133,8 @@
             content = self.password_dialog.get_content_area()
             key_id = content.get_children()[0].get_children()[1].get_text()
             secret_key = content.get_children()[1].get_children()[1].get_text()
-            creds = AWSCredentials(access_key=key_id, secret_key=secret_key)
-            self.create_client(creds)
+            service = EC2Service(access_key=key_id, secret_key=secret_key)
+            self.create_client(service)
             gnomekeyring.item_create_sync(
                 None,
                 gnomekeyring.ITEM_GENERIC_SECRET,

=== removed file 'txaws/credentials.py'
--- txaws/credentials.py	2009-08-17 11:18:56 +0000
+++ txaws/credentials.py	1970-01-01 00:00:00 +0000
@@ -1,37 +0,0 @@
-# Copyright (C) 2009 Robert Collins <robertc@xxxxxxxxxxxxxxxxx>
-# Licenced under the txaws licence available at /LICENSE in the txaws source.
-
-"""Credentials for accessing AWS services."""
-
-import os
-
-from txaws.util import *
-
-
-__all__ = ['AWSCredentials']
-
-
-class AWSCredentials(object):
-
-    def __init__(self, access_key=None, secret_key=None):
-        """Create an AWSCredentials object.
-
-        :param access_key: The access key to use. If None the environment
-            variable AWS_ACCESS_KEY_ID is consulted.
-        :param secret_key: The secret key to use. If None the environment
-            variable AWS_SECRET_ACCESS_KEY is consulted.
-        """
-        self.secret_key = secret_key
-        if self.secret_key is None:
-            self.secret_key = os.environ.get('AWS_SECRET_ACCESS_KEY')
-        if self.secret_key is None:
-            raise ValueError('Could not find AWS_SECRET_ACCESS_KEY')
-        self.access_key = access_key
-        if self.access_key is None:
-            self.access_key = os.environ.get('AWS_ACCESS_KEY_ID')
-        if self.access_key is None:
-            raise ValueError('Could not find AWS_ACCESS_KEY_ID')
-
-    def sign(self, bytes):
-        """Sign some bytes."""
-        return hmac_sha1(self.secret_key, bytes)

=== modified file 'txaws/ec2/client.py'
--- txaws/ec2/client.py	2009-08-18 21:56:36 +0000
+++ txaws/ec2/client.py	2009-08-20 16:47:54 +0000
@@ -8,7 +8,7 @@
 
 from twisted.web.client import getPage
 
-from txaws import credentials
+from txaws.ec2.service import EC2Service
 from txaws.util import iso8601time, XML
 
 
@@ -46,16 +46,15 @@
 
     name_space = '{http://ec2.amazonaws.com/doc/2008-12-01/}'
 
-    def __init__(self, creds=None, query_factory=None):
+    def __init__(self, service=None, query_factory=None):
         """Create an EC2Client.
 
-        @param creds: Explicit credentials to use. If None, credentials are
-            inferred as per txaws.credentials.AWSCredentials.
+        @param service: Explicit service to use.
         """
-        if creds is None:
-            self.creds = credentials.AWSCredentials()
+        if service is None:
+            self.service = EC2Service()
         else:
-            self.creds = creds
+            self.service = service
         if query_factory is None:
             self.query_factory = Query
         else:
@@ -63,7 +62,7 @@
 
     def describe_instances(self):
         """Describe current instances."""
-        q = self.query_factory('DescribeInstances', self.creds)
+        q = self.query_factory('DescribeInstances', self.service)
         d = q.submit()
         return d.addCallback(self._parse_instances)
 
@@ -119,7 +118,7 @@
         instanceset = {}
         for pos, instance_id in enumerate(instance_ids):
             instanceset["InstanceId.%d" % (pos+1)] = instance_id
-        q = self.query_factory('TerminateInstances', self.creds, instanceset)
+        q = self.query_factory('TerminateInstances', self.service, instanceset)
         d = q.submit()
         return d.addCallback(self._parse_terminate_instances)
 
@@ -142,7 +141,7 @@
 class Query(object):
     """A query that may be submitted to EC2."""
 
-    def __init__(self, action, creds, other_params=None, time_tuple=None):
+    def __init__(self, action, service, other_params=None, time_tuple=None):
         """Create a Query to submit to EC2."""
         # Require params (2008-12-01 API):
         # Version, SignatureVersion, SignatureMethod, Action, AWSAccessKeyId,
@@ -151,15 +150,12 @@
             'SignatureVersion': '2',
             'SignatureMethod': 'HmacSHA1',
             'Action': action,
-            'AWSAccessKeyId': creds.access_key,
+            'AWSAccessKeyId': service.access_key,
             'Timestamp': iso8601time(time_tuple),
             }
         if other_params:
             self.params.update(other_params)
-        self.method = 'GET'
-        self.host = 'ec2.amazonaws.com'
-        self.uri = '/'
-        self.creds = creds
+        self.service = service
 
     def canonical_query_params(self):
         """Return the canonical query params (used in signing)."""
@@ -178,18 +174,19 @@
 
     def signing_text(self):
         """Return the text to be signed when signing the query."""
-        result = "%s\n%s\n%s\n%s" % (self.method, self.host, self.uri,
-            self.canonical_query_params())
+        result = "%s\n%s\n%s\n%s" % (self.service.method, self.service.host,
+                                     self.service.endpoint,
+                                     self.canonical_query_params())
         return result
 
     def sign(self):
-        """Sign this query using its built in credentials.
+        """Sign this query using its built in service.
         
         This prepares it to be sent, and should be done as the last step before
         submitting the query. Signing is done automatically - this is a public
         method to facilitate testing.
         """
-        self.params['Signature'] = self.creds.sign(self.signing_text())
+        self.params['Signature'] = self.service.sign(self.signing_text())
 
     def sorted_params(self):
         """Return the query params sorted appropriately for signing."""
@@ -198,9 +195,8 @@
     def submit(self):
         """Submit this query.
 
-        :return: A deferred from twisted.web.client.getPage
+        @return: A deferred from twisted.web.client.getPage
         """
         self.sign()
-        url = 'http://%s%s?%s' % (self.host, self.uri,
-            self.canonical_query_params())
-        return getPage(url, method=self.method)
+        url = "%s?%s" % (self.service.get_url(), self.canonical_query_params())
+        return getPage(url, method=self.service.method)

=== modified file 'txaws/ec2/tests/test_client.py'
--- txaws/ec2/tests/test_client.py	2009-08-18 21:56:36 +0000
+++ txaws/ec2/tests/test_client.py	2009-08-20 16:47:54 +0000
@@ -5,8 +5,8 @@
 
 from twisted.internet.defer import succeed
 
-from txaws.credentials import AWSCredentials
 from txaws.ec2 import client
+from txaws.ec2.service import EC2Service, US_EC2_HOST
 from txaws.tests import TXAWSTestCase
 
 
@@ -91,21 +91,21 @@
         self.assertEquals(reservation.groups, ["one", "two"])
 
 
-class TestEC2Client(TXAWSTestCase):
+class EC2ClientTestCase(TXAWSTestCase):
     
     def test_init_no_creds(self):
         os.environ['AWS_SECRET_ACCESS_KEY'] = 'foo'
         os.environ['AWS_ACCESS_KEY_ID'] = 'bar'
         ec2 = client.EC2Client()
-        self.assertNotEqual(None, ec2.creds)
+        self.assertNotEqual(None, ec2.service)
 
     def test_init_no_creds_non_available_errors(self):
         self.assertRaises(ValueError, client.EC2Client)
 
-    def test_init_explicit_creds(self):
-        creds = 'foo'
-        ec2 = client.EC2Client(creds=creds)
-        self.assertEqual(creds, ec2.creds)
+    def test_init_explicit_service(self):
+        service = EC2Service("foo", "bar")
+        ec2 = client.EC2Client(service=service)
+        self.assertEqual(service, ec2.service)
 
     def check_parsed_instances(self, results):
         instance = results[0]
@@ -118,33 +118,38 @@
         self.assertEquals(group, "default")
 
     def test_parse_reservation(self):
-        ec2 = client.EC2Client(creds='foo')
+        service = EC2Service("foo", "bar")
+        ec2 = client.EC2Client(service=service)
         results = ec2._parse_instances(sample_describe_instances_result)
         self.check_parsed_instances(results)
 
     def test_describe_instances(self):
         class StubQuery(object):
-            def __init__(stub, action, creds):
+            def __init__(stub, action, service):
                 self.assertEqual(action, 'DescribeInstances')
-                self.assertEqual('foo', creds)
+                self.assertEqual(service.access_key, "foo")
+                self.assertEqual(service.secret_key, "bar")
             def submit(self):
                 return succeed(sample_describe_instances_result)
-        ec2 = client.EC2Client(creds='foo', query_factory=StubQuery)
+        service = EC2Service("foo", "bar")
+        ec2 = client.EC2Client(service, query_factory=StubQuery)
         d = ec2.describe_instances()
         d.addCallback(self.check_parsed_instances)
         return d
 
     def test_terminate_instances(self):
         class StubQuery(object):
-            def __init__(stub, action, creds, other_params):
+            def __init__(stub, action, service, other_params):
                 self.assertEqual(action, 'TerminateInstances')
-                self.assertEqual('foo', creds)
+                self.assertEqual(service.access_key, "foo")
+                self.assertEqual(service.secret_key, "bar")
                 self.assertEqual(
                     {'InstanceId.1': 'i-1234', 'InstanceId.2': 'i-5678'},
                     other_params)
             def submit(self):
                 return succeed(sample_terminate_instances_result)
-        ec2 = client.EC2Client(creds='foo', query_factory=StubQuery)
+        service = EC2Service("foo", "bar")
+        ec2 = client.EC2Client(service=service, query_factory=StubQuery)
         d = ec2.terminate_instances('i-1234', 'i-5678')
         def check_transition(changes):
             self.assertEqual([('i-1234', 'running', 'shutting-down'),
@@ -152,14 +157,14 @@
         return d
 
 
-class TestQuery(TXAWSTestCase):
+class QueryTestCase(TXAWSTestCase):
 
     def setUp(self):
         TXAWSTestCase.setUp(self)
-        self.creds = AWSCredentials('foo', 'bar')
+        self.service = EC2Service('foo', 'bar')
 
     def test_init_minimum(self):
-        query = client.Query('DescribeInstances', self.creds)
+        query = client.Query('DescribeInstances', self.service)
         self.assertTrue('Timestamp' in query.params)
         del query.params['Timestamp']
         self.assertEqual(
@@ -173,11 +178,11 @@
     def test_init_requires_action(self):
         self.assertRaises(TypeError, client.Query)
 
-    def test_init_requires_creds(self):
+    def test_init_requires_service_with_creds(self):
         self.assertRaises(TypeError, client.Query, None)
 
     def test_init_other_args_are_params(self):
-        query = client.Query('DescribeInstances', self.creds,
+        query = client.Query('DescribeInstances', self.service,
             {'InstanceId.0': '12345'},
             time_tuple=(2007,11,12,13,14,15,0,0,0))
         self.assertEqual(
@@ -191,7 +196,7 @@
             query.params)
 
     def test_sorted_params(self):
-        query = client.Query('DescribeInstances', self.creds,
+        query = client.Query('DescribeInstances', self.service,
             {'fun': 'games'},
             time_tuple=(2007,11,12,13,14,15,0,0,0))
         self.assertEqual([
@@ -207,16 +212,16 @@
     def test_encode_unreserved(self):
         all_unreserved = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ'
             'abcdefghijklmnopqrstuvwxyz0123456789-_.~')
-        query = client.Query('DescribeInstances', self.creds)
+        query = client.Query('DescribeInstances', self.service)
         self.assertEqual(all_unreserved, query.encode(all_unreserved))
 
     def test_encode_space(self):
         """This may be just 'url encode', but the AWS manual isn't clear."""
-        query = client.Query('DescribeInstances', self.creds)
+        query = client.Query('DescribeInstances', self.service)
         self.assertEqual('a%20space', query.encode('a space'))
 
     def test_canonical_query(self):
-        query = client.Query('DescribeInstances', self.creds,
+        query = client.Query('DescribeInstances', self.service,
             {'fu n': 'g/ames', 'argwithnovalue':'',
              'InstanceId.1': 'i-1234'},
             time_tuple=(2007,11,12,13,14,15,0,0,0))
@@ -228,17 +233,17 @@
         self.assertEqual(expected_query, query.canonical_query_params())
 
     def test_signing_text(self):
-        query = client.Query('DescribeInstances', self.creds,
+        query = client.Query('DescribeInstances', self.service,
             time_tuple=(2007,11,12,13,14,15,0,0,0))
-        signing_text = ('GET\nec2.amazonaws.com\n/\n'
+        signing_text = ('GET\n%s\n/\n' % US_EC2_HOST + 
             'AWSAccessKeyId=foo&Action=DescribeInstances&'
             'SignatureMethod=HmacSHA1&SignatureVersion=2&'
             'Timestamp=2007-11-12T13%3A14%3A15Z&Version=2008-12-01')
         self.assertEqual(signing_text, query.signing_text())
 
     def test_sign(self):
-        query = client.Query('DescribeInstances', self.creds,
+        query = client.Query('DescribeInstances', self.service,
             time_tuple=(2007,11,12,13,14,15,0,0,0))
         query.sign()
-        self.assertEqual('4hEtLuZo9i6kuG3TOXvRQNOrE/U=',
+        self.assertEqual('JuCpwFA2H4OVF3Ql/lAQs+V6iMc=',
             query.params['Signature'])

=== added file 'txaws/service.py'
--- txaws/service.py	1970-01-01 00:00:00 +0000
+++ txaws/service.py	2009-08-20 19:09:56 +0000
@@ -0,0 +1,70 @@
+# Copyright (C) 2009 Duncan McGreggor <duncan@xxxxxxxxxxxxx>
+# Copyright (C) 2009 Robert Collins <robertc@xxxxxxxxxxxxxxxxx>
+# Licenced under the txaws licence available at /LICENSE in the txaws source.
+
+import os
+
+from twisted.web.client import _parse
+
+from txaws.util import hmac_sha1
+
+DEFAULT_PORT = 80
+ENV_ACCESS_KEY = "AWS_ACCESS_KEY_ID"
+ENV_SECRET_KEY = "AWS_SECRET_ACCESS_KEY"
+
+class AWSService(object):
+    """
+    @param access_key: The access key to use. If None the environment
+        variable AWS_ACCESS_KEY_ID is consulted.
+    @param secret_key: The secret key to use. If None the environment
+        variable AWS_SECRET_ACCESS_KEY is consulted.
+    @param uri: The URL for the service.
+    @param method: The HTTP method used when accessing a service.
+    """
+    default_host = ""
+    default_schema = "https"
+
+    def __init__(self, access_key="", secret_key="", uri="", method="GET"):
+        self.access_key = access_key
+        self.secret_key = secret_key
+        self.schema = ""
+        self.host = ""
+        self.port = DEFAULT_PORT
+        self.endpoint = "/"
+        self.method = method
+        self._process_creds()
+        self._parse_uri(uri)
+        if not self.host:
+            self.host = self.default_host
+        if not self.schema:
+            self.schema = self.default_schema
+
+    def _process_creds(self):
+        # perform checks for access key
+        if not self.access_key:
+            self.access_key = os.environ.get(ENV_ACCESS_KEY)
+        if not self.access_key:
+            raise ValueError("Could not find %s" % ENV_ACCESS_KEY)
+        # perform checks for secret key
+        if not self.secret_key:
+            self.secret_key = os.environ.get(ENV_SECRET_KEY)
+        if not self.secret_key:
+            raise ValueError("Could not find %s" % ENV_SECRET_KEY)
+
+    def _parse_uri(self, uri):
+        scheme, host, port, endpoint = _parse(uri, defaultPort=DEFAULT_PORT)
+        self.schema = scheme
+        self.host = host
+        self.port = port
+        self.endpoint = endpoint
+
+    def get_uri(self):
+        """Get a URL representation of the service."""
+        uri = "%s://%s" % (self.schema, self.host)
+        if self.port and self.port != DEFAULT_PORT:
+            uri = "%s:%s" % (uri, self.port)
+        return uri + self.endpoint
+
+    def sign(self, bytes):
+        """Sign some bytes."""
+        return hmac_sha1(self.secret_key, bytes)

=== modified file 'txaws/storage/client.py'
--- txaws/storage/client.py	2009-08-17 11:18:56 +0000
+++ txaws/storage/client.py	2009-08-20 19:09:56 +0000
@@ -10,178 +10,170 @@
 from hashlib import md5
 from base64 import b64encode
 
-
 from epsilon.extime import Time
 
 from twisted.web.client import getPage
 from twisted.web.http import datetimeToString
 
-from txaws.credentials import AWSCredentials
-from txaws.util import XML
-
-
-def calculateMD5(data):
-    digest = md5(data).digest()
-    return b64encode(digest)
+from txaws.util import XML, calculate_md5
+from txaws.service import AWSService
+
+
+name_space = '{http://s3.amazonaws.com/doc/2006-03-01/}'
 
 
 class S3Request(object):
-    def __init__(self, verb, bucket=None, objectName=None, data='',
-            contentType=None, metadata={}, rootURI='https://s3.amazonaws.com',
-            creds=None):
+
+    def __init__(self, verb, bucket=None, object_name=None, data='',
+                 content_type=None, metadata={}, service=None):
         self.verb = verb
         self.bucket = bucket
-        self.objectName = objectName
+        self.object_name = object_name
         self.data = data
-        self.contentType = contentType
+        self.content_type = content_type
         self.metadata = metadata
-        self.rootURI = rootURI
-        self.creds = creds
+        self.service = service
+        self.service.endpoint = self.get_path()
         self.date = datetimeToString()
 
-    def getURIPath(self):
+    def get_path(self):
         path = '/'
         if self.bucket is not None:
             path += self.bucket
-            if self.objectName is not None:
-                path += '/' + self.objectName
+            if self.object_name is not None:
+                path += '/' + self.object_name
         return path
 
-    def getURI(self):
-        return self.rootURI + self.getURIPath()
+    def get_uri(self):
+        return self.service.get_uri()
 
-    def getHeaders(self):
+    def get_headers(self):
         headers = {'Content-Length': len(self.data),
-                   'Content-MD5': calculateMD5(self.data),
+                   'Content-MD5': calculate_md5(self.data),
                    'Date': self.date}
 
         for key, value in self.metadata.iteritems():
             headers['x-amz-meta-' + key] = value
 
-        if self.contentType is not None:
-            headers['Content-Type'] = self.contentType
+        if self.content_type is not None:
+            headers['Content-Type'] = self.content_type
 
-        if self.creds is not None:
-            signature = self.getSignature(headers)
+        if self.service is not None:
+            signature = self.get_signature(headers)
             headers['Authorization'] = 'AWS %s:%s' % (
-                self.creds.access_key, signature)
-
+                self.service.access_key, signature)
         return headers
 
-    def getCanonicalizedResource(self):
-        return self.getURIPath()
+    def get_canonicalized_resource(self):
+        return self.get_path()
 
-    def getCanonicalizedAmzHeaders(self, headers):
+    def get_canonicalized_amz_headers(self, headers):
         result = ''
         headers = [(name.lower(), value) for name, value in headers.iteritems()
             if name.lower().startswith('x-amz-')]
         headers.sort()
         return ''.join('%s:%s\n' % (name, value) for name, value in headers)
 
-    def getSignature(self, headers):
-        text = self.verb + '\n'
-        text += headers.get('Content-MD5', '') + '\n'
-        text += headers.get('Content-Type', '') + '\n'
-        text += headers.get('Date', '') + '\n'
-        text += self.getCanonicalizedAmzHeaders(headers)
-        text += self.getCanonicalizedResource()
-        return self.creds.sign(text)
+    def get_signature(self, headers):
+        text = (self.verb + '\n' + 
+                headers.get('Content-MD5', '') + '\n' +
+                headers.get('Content-Type', '') + '\n' +
+                headers.get('Date', '') + '\n' +
+                self.get_canonicalized_amz_headers(headers) +
+                self.get_canonicalized_resource())
+        return self.service.sign(text)
 
     def submit(self):
-        return self.getPage(
-            url=self.getURI(), method=self.verb, postdata=self.data,
-            headers=self.getHeaders())
+        return self.get_page(url=self.get_uri(), method=self.verb,
+                             postdata=self.data, headers=self.get_headers())
 
-    def getPage(self, *a, **kw):
+    def get_page(self, *a, **kw):
         return getPage(*a, **kw)
 
 
-NS = '{http://s3.amazonaws.com/doc/2006-03-01/}'
-
-
 class S3(object):
-    rootURI = 'https://s3.amazonaws.com/'
-    requestFactory = S3Request
-
-    def __init__(self, creds):
-        self.creds = creds
-
-    def makeRequest(self, *a, **kw):
+
+    request_factory = S3Request
+
+    def __init__(self, service):
+        self.service = service
+
+    def make_request(self, *a, **kw):
         """
         Create a request with the arguments passed in.
 
-        This uses the requestFactory attribute, adding the credentials to the
+        This uses the request_factory attribute, adding the service to the
         arguments passed in.
         """
-        return self.requestFactory(creds=self.creds, *a, **kw)
+        return self.request_factory(service=self.service, *a, **kw)
 
-    def _parseBucketList(self, response):
+    def _parse_bucket_list(self, response):
         """
         Parse XML bucket list response.
         """
         root = XML(response)
-        for bucket in root.find(NS + 'Buckets'):
-            timeText = bucket.findtext(NS + 'CreationDate')
+        for bucket in root.find(name_space + 'Buckets'):
+            timeText = bucket.findtext(name_space + 'CreationDate')
             yield {
-                'name': bucket.findtext(NS + 'Name'),
+                'name': bucket.findtext(name_space + 'Name'),
                 'created': Time.fromISO8601TimeAndDate(timeText),
                 }
 
-    def listBuckets(self):
+    def list_buckets(self):
         """
         List all buckets.
 
         Returns a list of all the buckets owned by the authenticated sender of
         the request.
         """
-        d = self.makeRequest('GET').submit()
-        d.addCallback(self._parseBucketList)
-        return d
+        deferred = self.make_request('GET').submit()
+        deferred.addCallback(self._parse_bucket_list)
+        return deferred
 
-    def createBucket(self, bucket):
+    def create_bucket(self, bucket):
         """
         Create a new bucket.
         """
-        return self.makeRequest('PUT', bucket).submit()
+        return self.make_request('PUT', bucket).submit()
 
-    def deleteBucket(self, bucket):
+    def delete_bucket(self, bucket):
         """
         Delete a bucket.
 
         The bucket must be empty before it can be deleted.
         """
-        return self.makeRequest('DELETE', bucket).submit()
+        return self.make_request('DELETE', bucket).submit()
 
-    def putObject(self, bucket, objectName, data, contentType=None,
-                  metadata={}):
+    def put_object(self, bucket, object_name, data, content_type=None,
+                   metadata={}):
         """
         Put an object in a bucket.
 
         Any existing object of the same name will be replaced.
         """
-        return self.makeRequest(
-            'PUT', bucket, objectName, data, contentType, metadata).submit()
+        return self.make_request('PUT', bucket, object_name, data,
+                                 content_type, metadata).submit()
 
-    def getObject(self, bucket, objectName):
+    def get_object(self, bucket, object_name):
         """
         Get an object from a bucket.
         """
-        return self.makeRequest('GET', bucket, objectName).submit()
+        return self.make_request('GET', bucket, object_name).submit()
 
-    def headObject(self, bucket, objectName):
+    def head_object(self, bucket, object_name):
         """
         Retrieve object metadata only.
 
-        This is like getObject, but the object's content is not retrieved.
+        This is like get_object, but the object's content is not retrieved.
         Currently the metadata is not returned to the caller either, so this
         method is mostly useless, and only provided for completeness.
         """
-        return self.makeRequest('HEAD', bucket, objectName).submit()
+        return self.make_request('HEAD', bucket, object_name).submit()
 
-    def deleteObject(self, bucket, objectName):
+    def delete_object(self, bucket, object_name):
         """
         Delete an object from a bucket.
 
         Once deleted, there is no method to restore or undelete an object.
         """
-        return self.makeRequest('DELETE', bucket, objectName).submit()
+        return self.make_request('DELETE', bucket, object_name).submit()

=== added file 'txaws/storage/service.py'
--- txaws/storage/service.py	1970-01-01 00:00:00 +0000
+++ txaws/storage/service.py	2009-08-20 19:09:56 +0000
@@ -0,0 +1,19 @@
+# Copyright (C) 2009 Duncan McGreggor <duncan@xxxxxxxxxxxxx>
+# Licenced under the txaws licence available at /LICENSE in the txaws source.
+
+from txaws.service import AWSService
+
+
+S3_HOST = "s3.amazonaws.com"
+
+
+class S3Service(AWSService):
+    """
+    This service uses the standard S3 host defined with S3_HOST by default. To
+    override this behaviour, simply pass the desired value in the "host"
+    keyword parameter.
+
+    For more details, see txaws.service.AWSService.
+    """
+    default_host = S3_HOST
+    default_schema = "https"

=== modified file 'txaws/storage/test/test_client.py'
--- txaws/storage/test/test_client.py	2009-08-15 03:28:45 +0000
+++ txaws/storage/test/test_client.py	2009-08-20 19:09:56 +0000
@@ -4,20 +4,23 @@
 
 from twisted.internet.defer import succeed
 
-from txaws.credentials import AWSCredentials
+from txaws.util import calculate_md5
 from txaws.tests import TXAWSTestCase
-from txaws.storage.client import S3, S3Request, calculateMD5
+from txaws.storage.service import S3Service
+from txaws.storage.client import S3, S3Request
+
 
 
 class StubbedS3Request(S3Request):
-    def getPage(self, url, method, postdata, headers):
+
+    def get_page(self, url, method, postdata, headers):
         self.getPageArgs = (url, method, postdata, headers)
         return succeed('')
 
 
-class RequestTests(TXAWSTestCase):
-    creds = AWSCredentials(access_key='0PN5J17HBGZHT7JJ3X82',
-        secret_key='uV3F3YluFJax1cknvbcGwgjvx4QpvB+leU8dUj2o')
+class RequestTestCase(TXAWSTestCase):
+
+    service = S3Service(access_key='fookeyid', secret_key='barsecretkey')
 
     def test_objectRequest(self):
         """
@@ -26,20 +29,23 @@
         DATA = 'objectData'
         DIGEST = 'zhdB6gwvocWv/ourYUWMxA=='
 
-        request = S3Request(
-            'PUT', 'somebucket', 'object/name/here', DATA, 
-            contentType='text/plain', metadata={'foo': 'bar'})
+        request = S3Request('PUT', 'somebucket', 'object/name/here', DATA,
+                            content_type='text/plain', metadata={'foo': 'bar'},
+                            service=self.service)
+        request.get_signature = lambda headers: "TESTINGSIG="
         self.assertEqual(request.verb, 'PUT')
         self.assertEqual(
-            request.getURI(),
+            request.get_uri(),
             'https://s3.amazonaws.com/somebucket/object/name/here')
-        headers = request.getHeaders()
+        headers = request.get_headers()
         self.assertNotEqual(headers.pop('Date'), '')
-        self.assertEqual(headers,
-                         {'Content-Type': 'text/plain',
-                          'Content-Length': len(DATA),
-                          'Content-MD5': DIGEST,
-                          'x-amz-meta-foo': 'bar'})
+        self.assertEqual(
+            headers, {
+                'Authorization': 'AWS fookeyid:TESTINGSIG=',
+                'Content-Type': 'text/plain',
+                'Content-Length': len(DATA),
+                'Content-MD5': DIGEST,
+                'x-amz-meta-foo': 'bar'})
         self.assertEqual(request.data, 'objectData')
 
     def test_bucketRequest(self):
@@ -48,42 +54,46 @@
         """
         DIGEST = '1B2M2Y8AsgTpgAmY7PhCfg=='
 
-        request = S3Request('GET', 'somebucket')
+        request = S3Request('GET', 'somebucket', service=self.service)
+        request.get_signature = lambda headers: "TESTINGSIG="
         self.assertEqual(request.verb, 'GET')
         self.assertEqual(
-            request.getURI(), 'https://s3.amazonaws.com/somebucket')
-        headers = request.getHeaders()
+            request.get_uri(), 'https://s3.amazonaws.com/somebucket')
+        headers = request.get_headers()
         self.assertNotEqual(headers.pop('Date'), '')
-        self.assertEqual(headers,
-                         {'Content-Length': 0,
-                          'Content-MD5': DIGEST})
+        self.assertEqual(
+            headers, {
+            'Authorization': 'AWS fookeyid:TESTINGSIG=',
+            'Content-Length': 0,
+            'Content-MD5': DIGEST})
         self.assertEqual(request.data, '')
 
     def test_submit(self):
         """
         Submitting the request should invoke getPage correctly.
         """
-        request = StubbedS3Request('GET', 'somebucket')
+        request = StubbedS3Request('GET', 'somebucket', service=self.service)
 
         def _postCheck(result):
             self.assertEqual(result, '')
 
             url, method, postdata, headers = request.getPageArgs
-            self.assertEqual(url, request.getURI())
+            self.assertEqual(url, request.get_uri())
             self.assertEqual(method, request.verb)
             self.assertEqual(postdata, request.data)
-            self.assertEqual(headers, request.getHeaders())
+            self.assertEqual(headers, request.get_headers())
 
         return request.submit().addCallback(_postCheck)
 
     def test_authenticationTestCases(self):
-        req = S3Request('GET', creds=self.creds)
-        req.date = 'Wed, 28 Mar 2007 01:29:59 +0000'
+        request = S3Request('GET', service=self.service)
+        request.get_signature = lambda headers: "TESTINGSIG="
+        request.date = 'Wed, 28 Mar 2007 01:29:59 +0000'
 
-        headers = req.getHeaders()
+        headers = request.get_headers()
         self.assertEqual(
-            headers['Authorization'],
-            'AWS 0PN5J17HBGZHT7JJ3X82:jF7L3z/FTV47vagZzhKupJ9oNig=')
+            headers['Authorization'], 
+            'AWS fookeyid:TESTINGSIG=')
 
 
 class InertRequest(S3Request):
@@ -110,13 +120,13 @@
     """
     Testable version of S3.
 
-    This subclass stubs requestFactory to use InertRequest, making it easy to
+    This subclass stubs request_factory to use InertRequest, making it easy to
     assert things about the requests that are created in response to various
     operations.
     """
     response = None
 
-    def requestFactory(self, *a, **kw):
+    def request_factory(self, *a, **kw):
         req = InertRequest(response=self.response, *a, **kw)
         self._lastRequest = req
         return req
@@ -148,34 +158,34 @@
 
     def setUp(self):
         TXAWSTestCase.setUp(self)
-        self.creds = AWSCredentials(
+        self.service = S3Service(
             access_key='accessKey', secret_key='secretKey')
-        self.s3 = TestableS3(creds=self.creds)
+        self.s3 = TestableS3(service=self.service)
 
-    def test_makeRequest(self):
+    def test_make_request(self):
         """
-        Test that makeRequest passes in the service credentials.
+        Test that make_request passes in the service object.
         """
         marker = object()
 
         def _cb(*a, **kw):
-            self.assertEqual(kw['creds'], self.creds)
+            self.assertEqual(kw['service'], self.service)
             return marker
 
-        self.s3.requestFactory = _cb
-        self.assertIdentical(self.s3.makeRequest('GET'), marker)
+        self.s3.request_factory = _cb
+        self.assertIdentical(self.s3.make_request('GET'), marker)
 
-    def test_listBuckets(self):
+    def test_list_buckets(self):
         self.s3.response = samples['ListAllMyBucketsResult']
-        d = self.s3.listBuckets()
+        d = self.s3.list_buckets()
 
         req = self.s3._lastRequest
         self.assertTrue(req.submitted)
         self.assertEqual(req.verb, 'GET')
         self.assertEqual(req.bucket, None)
-        self.assertEqual(req.objectName, None)
+        self.assertEqual(req.object_name, None)
 
-        def _checkResult(buckets):
+        def _check_result(buckets):
             self.assertEqual(
                 list(buckets),
                 [{'name': u'quotes',
@@ -184,61 +194,61 @@
                  {'name': u'samples',
                   'created': Time.fromDatetime(
                     datetime(2006, 2, 3, 16, 41, 58))}])
-        return d.addCallback(_checkResult)
+        return d.addCallback(_check_result)
 
-    def test_createBucket(self):
-        self.s3.createBucket('foo')
+    def test_create_bucket(self):
+        self.s3.create_bucket('foo')
         req = self.s3._lastRequest
         self.assertTrue(req.submitted)
         self.assertEqual(req.verb, 'PUT')
         self.assertEqual(req.bucket, 'foo')
-        self.assertEqual(req.objectName, None)
+        self.assertEqual(req.object_name, None)
 
-    def test_deleteBucket(self):
-        self.s3.deleteBucket('foo')
+    def test_delete_bucket(self):
+        self.s3.delete_bucket('foo')
         req = self.s3._lastRequest
         self.assertTrue(req.submitted)
         self.assertEqual(req.verb, 'DELETE')
         self.assertEqual(req.bucket, 'foo')
-        self.assertEqual(req.objectName, None)
+        self.assertEqual(req.object_name, None)
 
-    def test_putObject(self):
-        self.s3.putObject(
+    def test_put_object(self):
+        self.s3.put_object(
             'foobucket', 'foo', 'data', 'text/plain', {'foo': 'bar'})
         req = self.s3._lastRequest
         self.assertTrue(req.submitted)
         self.assertEqual(req.verb, 'PUT')
         self.assertEqual(req.bucket, 'foobucket')
-        self.assertEqual(req.objectName, 'foo')
+        self.assertEqual(req.object_name, 'foo')
         self.assertEqual(req.data, 'data')
-        self.assertEqual(req.contentType, 'text/plain')
+        self.assertEqual(req.content_type, 'text/plain')
         self.assertEqual(req.metadata, {'foo': 'bar'})
 
-    def test_getObject(self):
-        self.s3.getObject('foobucket', 'foo')
+    def test_get_object(self):
+        self.s3.get_object('foobucket', 'foo')
         req = self.s3._lastRequest
         self.assertTrue(req.submitted)
         self.assertEqual(req.verb, 'GET')
         self.assertEqual(req.bucket, 'foobucket')
-        self.assertEqual(req.objectName, 'foo')
+        self.assertEqual(req.object_name, 'foo')
 
-    def test_headObject(self):
-        self.s3.headObject('foobucket', 'foo')
+    def test_head_object(self):
+        self.s3.head_object('foobucket', 'foo')
         req = self.s3._lastRequest
         self.assertTrue(req.submitted)
         self.assertEqual(req.verb, 'HEAD')
         self.assertEqual(req.bucket, 'foobucket')
-        self.assertEqual(req.objectName, 'foo')
+        self.assertEqual(req.object_name, 'foo')
 
-    def test_deleteObject(self):
-        self.s3.deleteObject('foobucket', 'foo')
+    def test_delete_object(self):
+        self.s3.delete_object('foobucket', 'foo')
         req = self.s3._lastRequest
         self.assertTrue(req.submitted)
         self.assertEqual(req.verb, 'DELETE')
         self.assertEqual(req.bucket, 'foobucket')
-        self.assertEqual(req.objectName, 'foo')
+        self.assertEqual(req.object_name, 'foo')
 
 
 class MiscellaneousTests(TXAWSTestCase):
     def test_contentMD5(self):
-        self.assertEqual(calculateMD5('somedata'), 'rvr3UC1SmUw7AZV2NqPN0g==')
+        self.assertEqual(calculate_md5('somedata'), 'rvr3UC1SmUw7AZV2NqPN0g==')

=== removed file 'txaws/tests/test_credentials.py'
--- txaws/tests/test_credentials.py	2009-08-17 11:18:56 +0000
+++ txaws/tests/test_credentials.py	1970-01-01 00:00:00 +0000
@@ -1,41 +0,0 @@
-# Copyright (C) 2009 Robert Collins <robertc@xxxxxxxxxxxxxxxxx>
-# Licenced under the txaws licence available at /LICENSE in the txaws source.
-
-import os
-
-from twisted.trial.unittest import TestCase
-from txaws.tests import TXAWSTestCase
-
-from txaws.credentials import AWSCredentials
-
-
-class TestCredentials(TXAWSTestCase):
-
-    def test_no_access_errors(self):
-        # Without anything in os.environ, AWSCredentials() blows up
-        os.environ['AWS_SECRET_ACCESS_KEY'] = 'foo'
-        self.assertRaises(Exception, AWSCredentials)
-
-    def test_no_secret_errors(self):
-        # Without anything in os.environ, AWSCredentials() blows up
-        os.environ['AWS_ACCESS_KEY_ID'] = 'bar'
-        self.assertRaises(Exception, AWSCredentials)
-
-    def test_found_values_used(self):
-        os.environ['AWS_SECRET_ACCESS_KEY'] = 'foo'
-        os.environ['AWS_ACCESS_KEY_ID'] = 'bar'
-        creds = AWSCredentials()
-        self.assertEqual('foo', creds.secret_key)
-        self.assertEqual('bar', creds.access_key)
-
-    def test_explicit_access_key(self):
-        os.environ['AWS_SECRET_ACCESS_KEY'] = 'foo'
-        creds = AWSCredentials(access_key='bar')
-        self.assertEqual('foo', creds.secret_key)
-        self.assertEqual('bar', creds.access_key)
-
-    def test_explicit_secret_key(self):
-        os.environ['AWS_ACCESS_KEY_ID'] = 'bar'
-        creds = AWSCredentials(secret_key='foo')
-        self.assertEqual('foo', creds.secret_key)
-        self.assertEqual('bar', creds.access_key)

=== added file 'txaws/tests/test_service.py'
--- txaws/tests/test_service.py	1970-01-01 00:00:00 +0000
+++ txaws/tests/test_service.py	2009-08-20 19:09:56 +0000
@@ -0,0 +1,88 @@
+# Copyright (C) 2009 Duncan McGreggor <duncan@xxxxxxxxxxxxx>
+# Licenced under the txaws licence available at /LICENSE in the txaws source.
+
+import os
+
+from txaws.service import AWSService, ENV_ACCESS_KEY, ENV_SECRET_KEY
+from txaws.tests import TXAWSTestCase
+
+
+class AWSServiceTestCase(TXAWSTestCase):
+
+    def setUp(self):
+        self.service = AWSService("fookeyid", "barsecretkey",
+                                  "http://my.service/da_endpoint";)
+        self.addCleanup(self.clean_environment)
+
+    def clean_environment(self):
+        if os.environ.has_key(ENV_ACCESS_KEY):
+            del os.environ[ENV_ACCESS_KEY]
+        if os.environ.has_key(ENV_SECRET_KEY):
+            del os.environ[ENV_SECRET_KEY]
+
+    def test_simple_creation(self):
+        service = AWSService("fookeyid", "barsecretkey")
+        self.assertEquals(service.access_key, "fookeyid")
+        self.assertEquals(service.secret_key, "barsecretkey")
+        self.assertEquals(service.schema, "https")
+        self.assertEquals(service.host, "")
+        self.assertEquals(service.port, 80)
+        self.assertEquals(service.endpoint, "/")
+        self.assertEquals(service.method, "GET")
+
+    def test_no_access_errors(self):
+        # Without anything in os.environ, AWSService() blows up
+        os.environ[ENV_SECRET_KEY] = "bar"
+        self.assertRaises(ValueError, AWSService)
+
+    def test_no_secret_errors(self):
+        # Without anything in os.environ, AWSService() blows up
+        os.environ[ENV_ACCESS_KEY] = "foo"
+        self.assertRaises(ValueError, AWSService)
+
+    def test_found_values_used(self):
+        os.environ[ENV_ACCESS_KEY] = "foo"
+        os.environ[ENV_SECRET_KEY] = "bar"
+        service = AWSService()
+        self.assertEqual("foo", service.access_key)
+        self.assertEqual("bar", service.secret_key)
+        self.clean_environment()
+
+    def test_explicit_access_key(self):
+        os.environ[ENV_SECRET_KEY] = "foo"
+        service = AWSService(access_key="bar")
+        self.assertEqual("foo", service.secret_key)
+        self.assertEqual("bar", service.access_key)
+
+    def test_explicit_secret_key(self):
+        os.environ[ENV_ACCESS_KEY] = "bar"
+        service = AWSService(secret_key="foo")
+        self.assertEqual("foo", service.secret_key)
+        self.assertEqual("bar", service.access_key)
+
+    def test_parse_uri(self):
+        self.assertEquals(self.service.schema, "http")
+        self.assertEquals(self.service.host, "my.service")
+        self.assertEquals(self.service.port, 80)
+        self.assertEquals(self.service.endpoint, "/da_endpoint")
+
+    def test_parse_uri_https_and_custom_port(self):
+        service = AWSService("foo", "bar", "https://my.service:8080/endpoint";)
+        self.assertEquals(service.schema, "https")
+        self.assertEquals(service.host, "my.service")
+        self.assertEquals(service.port, 8080)
+        self.assertEquals(service.endpoint, "/endpoint")
+
+    def test_custom_method(self):
+        service = AWSService("foo", "bar", "http://service/endpoint";, "PUT")
+        self.assertEquals(service.method, "PUT")
+
+    def test_get_uri(self):
+        uri = self.service.get_uri()
+        self.assertEquals(uri, "http://my.service/da_endpoint";)
+
+    def test_get_uri_custom_port(self):
+        uri = "https://my.service:8080/endpoint";
+        service = AWSService("foo", "bar", uri)
+        new_uri = service.get_uri()
+        self.assertEquals(new_uri, uri)

=== modified file 'txaws/util.py'
--- txaws/util.py	2009-08-17 11:18:56 +0000
+++ txaws/util.py	2009-08-18 20:48:59 +0000
@@ -4,10 +4,10 @@
 services.
 """
 
+import time
+import hmac
+from hashlib import sha1, md5
 from base64 import b64encode
-from hashlib import sha1
-import hmac
-import time
 
 # Import XML from somwhere; here in one place to prevent duplication.
 try:
@@ -19,6 +19,11 @@
 __all__ = ['hmac_sha1', 'iso8601time']
 
 
+def calculate_md5(data):
+    digest = md5(data).digest()
+    return b64encode(digest)
+
+
 def hmac_sha1(secret, data):
     digest = hmac.new(secret, data, sha1).digest()
     return b64encode(digest)


Follow ups