Repository: boto/botocore (python)

Issue:
Can't create a client when using Cross-account role (datetime.datetime' object has no attribute 'read')
Get an 'AttributeError: 'datetime.datetime' object has no attribute 'read'' exception when creating a client using a Cross-Account role:

```
import botocore.session

session = botocore.session.get_session()
client = session.create_client('ec2')
```

```
Traceback (most recent call last):
  File "test_botocore.py", line 4, in <module>
    client = session.create_client('ec2',region_name='ap-southeast-2')
  File "/usr/local/lib/python2.7/dist-packages/botocore-1.3.15-py2.7.egg/botocore/session.py", line 788, in create_client
    credentials = self.get_credentials()
  File "/usr/local/lib/python2.7/dist-packages/botocore-1.3.15-py2.7.egg/botocore/session.py", line 434, in get_credentials
    'credential_provider').load_credentials()
  File "/usr/local/lib/python2.7/dist-packages/botocore-1.3.15-py2.7.egg/botocore/credentials.py", line 869, in load_credentials
    creds = provider.load()
  File "/usr/local/lib/python2.7/dist-packages/botocore-1.3.15-py2.7.egg/botocore/credentials.py", line 637, in load
    return self._load_creds_via_assume_role()
  File "/usr/local/lib/python2.7/dist-packages/botocore-1.3.15-py2.7.egg/botocore/credentials.py", line 656, in _load_creds_via_assume_role
    creds, response = self._retrieve_temp_credentials()
  File "/usr/local/lib/python2.7/dist-packages/botocore-1.3.15-py2.7.egg/botocore/credentials.py", line 765, in _retrieve_temp_credentials
    creds = self._create_creds_from_response(response)
  File "/usr/local/lib/python2.7/dist-packages/botocore-1.3.15-py2.7.egg/botocore/credentials.py", line 742, in _create_creds_from_response
    expiry_time=parse(response['Credentials']['Expiration']),
  File "/usr/local/lib/python2.7/dist-packages/dateutil/parser.py", line 1008, in parse
    return DEFAULTPARSER.parse(timestr, **kwargs)
  File "/usr/local/lib/python2.7/dist-packages/dateutil/parser.py", line 392, in parse
    res = self._parse(timestr, **kwargs)
  File "/usr/local/lib/python2.7/dist-packages/dateutil/parser.py", line 492, in _parse
    l = _timelex.split(timestr)         # Splits the timestr into tokens
  File "/usr/local/lib/python2.7/dist-packages/dateutil/parser.py", line 174, in split
    return list(cls(s))
  File "/usr/local/lib/python2.7/dist-packages/dateutil/parser.py", line 171, in next
    return self.__next__()  # Python 2.x support
  File "/usr/local/lib/python2.7/dist-packages/dateutil/parser.py", line 164, in __next__
    token = self.get_token()
  File "/usr/local/lib/python2.7/dist-packages/dateutil/parser.py", line 82, in get_token
    nextchar = self.instream.read(1)
AttributeError: 'datetime.datetime' object has no attribute 'read'
```

My credentials file (which works perfectly using the CLI) looks like this:

```
[Master]                                                                                                                                                                                                                                                               
aws_access_key_id = XXXXXXXXXXXXXX
aws_secret_access_key = YYYYYYYYYYYYYYYYYYY

[Test]
role_arn = arn:aws:iam::zzzzzzzzzzzz:role/MyRoleOnMaster
source_profile = Master
```

Using the Master profile works ok, the problem is using the Test profile.

Python 2.7.6
Botocore 1.3.15


Pull request:
Account for datetime objects in assume role cred provider
I think a better long term solution will involve allowing
a client to configure how timestamps are parsed, but that
will require a much larger change.  For now, I'd like to
fix the immediate bug.

Fixes #745 

Also manually confirmed this works in botocore/CLI.

cc @kyleknap @mtdowling @rayluo @JordonPhillips 

Patch:
diff --git a/botocore/credentials.py b/botocore/credentials.py
index ab8e02323..22163c0ee 100644
--- a/botocore/credentials.py
+++ b/botocore/credentials.py
@@ -109,6 +109,18 @@ def _local_now():
     return datetime.datetime.now(tzlocal())
 
 
+def _parse_if_needed(value):
+    if isinstance(value, datetime.datetime):
+        return value
+    return parse(value)
+
+
+def _serialize_if_needed(value):
+    if isinstance(value, datetime.datetime):
+        return value.strftime('%Y-%m-%dT%H:%M:%SZ')
+    return value
+
+
 def create_assume_role_refresher(client, params):
     def refresh():
         response = client.assume_role(**params)
@@ -119,7 +131,7 @@ def create_assume_role_refresher(client, params):
             'access_key': credentials['AccessKeyId'],
             'secret_key': credentials['SecretAccessKey'],
             'token': credentials['SessionToken'],
-            'expiry_time': credentials['Expiration'],
+            'expiry_time': _serialize_if_needed(credentials['Expiration']),
         }
     return refresh
 
@@ -739,7 +751,8 @@ class AssumeRoleProvider(CredentialProvider):
             secret_key=response['Credentials']['SecretAccessKey'],
             token=response['Credentials']['SessionToken'],
             method=self.METHOD,
-            expiry_time=parse(response['Credentials']['Expiration']),
+            expiry_time=_parse_if_needed(
+                response['Credentials']['Expiration']),
             refresh_using=refresh_func)
 
     def _create_client_from_config(self, config):


Test patch:
diff --git a/tests/unit/test_credentials.py b/tests/unit/test_credentials.py
index 36f7894be..7cade701c 100644
--- a/tests/unit/test_credentials.py
+++ b/tests/unit/test_credentials.py
@@ -701,6 +701,31 @@ class TestAssumeRoleCredentialProvider(unittest.TestCase):
         self.assertEqual(creds.secret_key, 'bar')
         self.assertEqual(creds.token, 'baz')
 
+    def test_assume_role_with_datetime(self):
+        response = {
+            'Credentials': {
+                'AccessKeyId': 'foo',
+                'SecretAccessKey': 'bar',
+                'SessionToken': 'baz',
+                # Note the lack of isoformat(), we're using
+                # a datetime.datetime type.  This will ensure
+                # we test both parsing as well as serializing
+                # from a given datetime because the credentials
+                # are immediately expired.
+                'Expiration': datetime.now(tzlocal())
+            },
+        }
+        client_creator = self.create_client_creator(with_response=response)
+        provider = credentials.AssumeRoleProvider(
+            self.create_config_loader(),
+            client_creator, cache={}, profile_name='development')
+
+        creds = provider.load()
+
+        self.assertEqual(creds.access_key, 'foo')
+        self.assertEqual(creds.secret_key, 'bar')
+        self.assertEqual(creds.token, 'baz')
+
     def test_assume_role_retrieves_from_cache(self):
         date_in_future = datetime.utcnow() + timedelta(seconds=1000)
         utc_timestamp = date_in_future.isoformat() + 'Z'