Merge "Create header property"

This commit is contained in:
Jenkins
2015-02-24 13:01:12 +00:00
committed by Gerrit Code Review
7 changed files with 199 additions and 144 deletions

View File

@@ -11,7 +11,6 @@
# License for the specific language governing permissions and limitations
# under the License.
from openstack import exceptions
from openstack.object_store import object_store_service
from openstack import resource
from openstack import utils
@@ -31,20 +30,20 @@ class Container(resource.Resource):
# Account data (when id=None)
#: The transaction date and time.
timestamp = resource.prop("x-timestamp")
timestamp = resource.header("x-timestamp")
#: The total number of bytes that are stored in Object Storage for
#: the account.
account_bytes_used = resource.prop("x-account-bytes-used")
account_bytes_used = resource.header("x-account-bytes-used")
#: The number of containers.
account_container_count = resource.prop("x-account-container-count")
account_container_count = resource.header("x-account-container-count")
#: The number of objects in the account.
account_object_count = resource.prop("x-account-object-count")
account_object_count = resource.header("x-account-object-count")
#: The secret key value for temporary URLs. If not set,
#: this header is not returned by this operation.
meta_temp_url_key = resource.prop("x-account-meta-temp-url-key")
meta_temp_url_key = resource.header("x-account-meta-temp-url-key")
#: A second secret key value for temporary URLs. If not set,
#: this header is not returned by this operation.
meta_temp_url_key_2 = resource.prop("x-account-meta-temp-url-key-2")
meta_temp_url_key_2 = resource.header("x-account-meta-temp-url-key-2")
# Container body data (when id=None)
#: The name of the container.
@@ -57,9 +56,9 @@ class Container(resource.Resource):
# Container metadata (when id=name)
#: The number of objects.
object_count = resource.prop("x-container-object-count")
object_count = resource.header("x-container-object-count")
#: The count of bytes used in total.
bytes_used = resource.prop("x-container-bytes-used")
bytes_used = resource.header("x-container-bytes-used")
# Request headers (when id=None)
#: If set to True, Object Storage queries all replicas to return the
@@ -67,55 +66,38 @@ class Container(resource.Resource):
#: faster after it finds one valid replica. Because setting this
#: header to True is more expensive for the back end, use it only
#: when it is absolutely needed.
newest = resource.prop("x-newest", type=bool)
newest = resource.header("x-newest", type=bool)
# Request headers (when id=name)
#: The ACL that grants read access. If not set, this header is not
#: returned by this operation.
read_ACL = resource.prop("x-container-read")
read_ACL = resource.header("x-container-read")
#: The ACL that grants write access. If not set, this header is not
#: returned by this operation.
write_ACL = resource.prop("x-container-write")
write_ACL = resource.header("x-container-write")
#: The destination for container synchronization. If not set,
#: this header is not returned by this operation.
sync_to = resource.prop("x-container-sync-to")
sync_to = resource.header("x-container-sync-to")
#: The secret key for container synchronization. If not set,
#: this header is not returned by this operation.
sync_key = resource.prop("x-container-sync-key")
sync_key = resource.header("x-container-sync-key")
#: Enables versioning on this container. The value is the name
#: of another container. You must UTF-8-encode and then URL-encode
#: the name before you include it in the header. To disable
#: versioning, set the header to an empty string.
versions_location = resource.prop("x-versions-location")
versions_location = resource.header("x-versions-location")
#: Set to any value to disable versioning.
remove_versions_location = resource.prop("x-remove-versions-location")
remove_versions_location = resource.header("x-remove-versions-location")
#: Changes the MIME type for the object.
content_type = resource.prop("content-type")
content_type = resource.header("content-type")
#: If set to true, Object Storage guesses the content type based
#: on the file extension and ignores the value sent in the
#: Content-Type header, if present.
detect_content_type = resource.prop("x-detect-content-type", type=bool)
detect_content_type = resource.header("x-detect-content-type", type=bool)
#: In combination with Expect: 100-Continue, specify an
#: "If-None-Match: \*" header to query whether the server already
#: has a copy of the object before any data is sent.
if_none_match = resource.prop("if-none-match")
@classmethod
def _do_create_update(cls, method, session, attrs, resource_id):
"""Helper method to call put and post
The internals of create and update are the same exept for
the session method.
"""
url = utils.urljoin(cls.base_path, resource_id)
# Only send actual headers, not potentially set body values.
headers = attrs.copy()
for val in ("name", "count", "bytes"):
headers.pop(val, None)
return method(url, service=cls.service, accept=None,
headers=headers).headers
if_none_match = resource.header("if-none-match")
@classmethod
def update_by_id(cls, session, resource_id, attrs, path_args=None):
@@ -131,14 +113,11 @@ class Container(resource.Resource):
class but is ignored for this method.
:return: A ``dict`` representing the response headers.
:raises: :exc:`~openstack.exceptions.MethodNotSupported` if
:data:`Resource.allow_update` is not set to ``True``.
"""
if not cls.allow_update:
raise exceptions.MethodNotSupported('update')
return cls._do_create_update(session.post, session, attrs,
resource_id)
url = utils.urljoin(cls.base_path, resource_id)
headers = attrs[resource.HEADERS]
return session.post(url, service=cls.service, accept=None,
headers=headers).headers
@classmethod
def create_by_id(cls, session, attrs, resource_id=None):
@@ -152,14 +131,11 @@ class Container(resource.Resource):
the request. The default is ``None``.
:return: A ``dict`` representing the response headers.
:raises: :exc:`~openstack.exceptions.MethodNotSupported` if
:data:`Resource.allow_create` is not set to ``True``.
"""
if not cls.allow_create:
raise exceptions.MethodNotSupported('create')
return cls._do_create_update(session.put, session, attrs,
resource_id)
url = utils.urljoin(cls.base_path, resource_id)
headers = attrs[resource.HEADERS]
return session.put(url, service=cls.service, accept=None,
headers=headers).headers
def create(self, session):
"""Create a Container from this instance.
@@ -169,9 +145,8 @@ class Container(resource.Resource):
:return: This :class:`~openstack.object_store.v1.container.Container`
instance.
:raises: :exc:`~openstack.exceptions.MethodNotSupported` if
:data:`Resource.allow_create` is not set to ``True``.
"""
self.create_by_id(session, self._attrs, self.id)
resp = self.create_by_id(session, self._attrs, self.id)
self.set_headers(resp)
self._reset_dirty()
return self

View File

@@ -11,7 +11,6 @@
# License for the specific language governing permissions and limitations
# under the License.
from openstack import exceptions
from openstack.object_store import object_store_service
from openstack import resource
from openstack import utils
@@ -41,55 +40,55 @@ class Object(resource.Resource):
# Headers for HEAD and GET requests
#: Authentication token.
auth_token = resource.prop("x-auth-token")
auth_token = resource.header("x-auth-token")
#: If set to True, Object Storage queries all replicas to return
#: the most recent one. If you omit this header, Object Storage
#: responds faster after it finds one valid replica. Because
#: setting this header to True is more expensive for the back end,
#: use it only when it is absolutely needed.
newest = resource.prop("x-newest", type=bool)
newest = resource.header("x-newest", type=bool)
#: TODO(briancurtin) there's a lot of content here...
range = resource.prop("range", type=dict)
range = resource.header("range", type=dict)
#: See http://www.ietf.org/rfc/rfc2616.txt.
if_match = resource.prop("if-match", type=dict)
if_match = resource.header("if-match", type=dict)
#: In combination with Expect: 100-Continue, specify an
#: "If-None-Match: \*" header to query whether the server already
#: has a copy of the object before any data is sent.
if_none_match = resource.prop("if-none-match", type=dict)
if_none_match = resource.header("if-none-match", type=dict)
#: See http://www.ietf.org/rfc/rfc2616.txt.
if_modified_since = resource.prop("if-modified-since", type=dict)
if_modified_since = resource.header("if-modified-since", type=dict)
#: See http://www.ietf.org/rfc/rfc2616.txt.
if_unmodified_since = resource.prop("if-unmodified-since", type=dict)
if_unmodified_since = resource.header("if-unmodified-since", type=dict)
# Query parameters
#: Used with temporary URLs to sign the request. For more
#: information about temporary URLs, see OpenStack Object Storage
#: API v1 Reference.
signature = resource.prop("signature")
signature = resource.header("signature")
#: Used with temporary URLs to specify the expiry time of the
#: signature. For more information about temporary URLs, see
#: OpenStack Object Storage API v1 Reference.
expires = resource.prop("expires")
expires = resource.header("expires")
#: If you include the multipart-manifest=get query parameter and
#: the object is a large object, the object contents are not
#: returned. Instead, the manifest is returned in the
#: X-Object-Manifest response header for dynamic large objects
#: or in the response body for static large objects.
multipart_manifest = resource.prop("multipart-manifest")
multipart_manifest = resource.header("multipart-manifest")
# Response headers from HEAD and GET
#: HEAD operations do not return content. However, in this
#: operation the value in the Content-Length header is not the
#: size of the response body. Instead it contains the size of
#: the object, in bytes.
content_length = resource.prop("content-length")
content_length = resource.header("content-length")
#: The MIME type of the object.
content_type = resource.prop("content_type", alias="content-type")
content_type = resource.header("content_type", alias="content-type")
#: The type of ranges that the object accepts.
accept_ranges = resource.prop("accept-ranges")
accept_ranges = resource.header("accept-ranges")
#: The date and time that the object was created or the last
#: time that the metadata was changed.
last_modified = resource.prop("last_modified", alias="last-modified")
last_modified = resource.header("last_modified", alias="last-modified")
#: For objects smaller than 5 GB, this value is the MD5 checksum
#: of the object content. The value is not quoted.
#: For manifest objects, this value is the MD5 checksum of the
@@ -101,39 +100,39 @@ class Object(resource.Resource):
#: the response body as it is received and compare this value
#: with the one in the ETag header. If they differ, the content
#: was corrupted, so retry the operation.
etag = resource.prop("etag")
etag = resource.header("etag")
#: Set to True if this object is a static large object manifest object.
is_static_large_object = resource.prop("x-static-large-object")
is_static_large_object = resource.header("x-static-large-object")
#: The transaction date and time.
date = resource.prop("date")
date = resource.header("date")
#: If set, the value of the Content-Encoding metadata.
#: If not set, this header is not returned by this operation.
content_encoding = resource.prop("content-encoding")
content_encoding = resource.header("content-encoding")
#: If set, specifies the override behavior for the browser.
#: For example, this header might specify that the browser use
#: a download program to save this file rather than show the file,
#: which is the default.
#: If not set, this header is not returned by this operation.
content_disposition = resource.prop("content-disposition")
content_disposition = resource.header("content-disposition")
#: If set, the time when the object will be deleted by the system
#: in the format of a UNIX Epoch timestamp.
#: If not set, this header is not returned by this operation.
delete_at = resource.prop("x-delete-at", type=int)
delete_at = resource.header("x-delete-at", type=int)
#: If set, to this is a dynamic large object manifest object.
#: The value is the container and object name prefix of the
#: segment objects in the form container/prefix.
object_manifest = resource.prop("x-object-manifest")
object_manifest = resource.header("x-object-manifest")
#: The UNIX timestamp of the transaction.
timestamp = resource.prop("x-timestamp")
timestamp = resource.header("x-timestamp")
# Headers for PUT and POST requests
#: Set to chunked to enable chunked transfer encoding. If used,
#: do not set the Content-Length header to a non-zero value.
transfer_encoding = resource.prop("transfer-encoding")
transfer_encoding = resource.header("transfer-encoding")
#: If set to true, Object Storage guesses the content type based
#: on the file extension and ignores the value sent in the
#: Content-Type header, if present.
detect_content_type = resource.prop("x-detect-content-type", type=bool)
detect_content_type = resource.header("x-detect-content-type", type=bool)
#: If set, this is the name of an object used to create the new
#: object by copying the X-Copy-From object. The value is in form
#: {container}/{object}. You must UTF-8-encode and then URL-encode
@@ -141,16 +140,13 @@ class Object(resource.Resource):
#: in the header.
#: Using PUT with X-Copy-From has the same effect as using the
#: COPY operation to copy an object.
copy_from = resource.prop("x-copy-from")
copy_from = resource.header("x-copy-from")
#: Specifies the number of seconds after which the object is
#: removed. Internally, the Object Storage system stores this
#: value in the X-Delete-At metadata item.
delete_after = resource.prop("x-delete-after", type=int)
delete_after = resource.header("x-delete-after", type=int)
def get(self, session):
if not self.allow_retrieve:
raise exceptions.MethodNotSupported('retrieve')
# When joining the base_path part and the id part, base_path's
# leading slash gets dropped off here. Putting an empty leading value
# in front of it causes it to get joined and replaced.
@@ -158,11 +154,7 @@ class Object(resource.Resource):
# Only send actual headers, not potentially set body values and
# query parameters.
headers = self._attrs.copy()
for val in ("container", "name", "hash", "bytes", "signature",
"expires", "multipart_manifest"):
headers.pop(val, None)
headers = self.get_headers()
resp = session.get(url, service=self.service, accept="bytes",
headers=headers).content
@@ -170,9 +162,6 @@ class Object(resource.Resource):
def create(self, session, data=None):
"""Create a remote resource from this instance."""
if not self.allow_create:
raise exceptions.MethodNotSupported('create')
url = utils.urljoin("", self.base_path % self, self.id)
if data is not None:
@@ -181,5 +170,5 @@ class Object(resource.Resource):
else:
resp = session.post(url, service=self.service, data=None,
accept=None).headers
self._attrs.update(resp)
self.set_headers(resp)
return self

View File

@@ -152,6 +152,52 @@ class prop(object):
pass
#: Key in attributes for header properties
HEADERS = 'headers'
class header(prop):
"""A helper for defining header properties in a resource.
This property should be used for values passed in the header of a resource.
Header values are stored in a special 'headers' attribute of a resource.
Using this property will make it easier for users to access those values.
For example, and object store container:
>>> class Container(Resource):
... name = prop("name")
... object_count = header("x-container-object-count")
...
>>> c = Container({name='pix'})
>>> c.head(session)
>>> print c["headers"]["x-container-object-count"]
4
>>> print c.object_count
4
The first print shows accessing the header value without the property
and the second print shows accessing the header with the property helper.
"""
def _get_headers(self, instance):
if instance is None:
return None
if HEADERS in instance:
return instance[HEADERS]
return None
def __get__(self, instance, owner):
headers = self._get_headers(instance)
return super(header, self).__get__(headers, owner)
def __set__(self, instance, value):
headers = self._get_headers(instance)
if headers is None:
headers = instance._attrs[HEADERS] = {}
headers[self.name] = value
instance.set_headers(headers)
@six.add_metaclass(abc.ABCMeta)
class Resource(collections.MutableMapping):
@@ -397,6 +443,15 @@ class Resource(collections.MutableMapping):
for key, value in kwargs.items():
setattr(self, key, value)
def get_headers(self):
if HEADERS in self._attrs:
return self._attrs[HEADERS]
return {}
def set_headers(self, values):
self._attrs[HEADERS] = values
self._dirty.add(HEADERS)
##
# CRUD OPERATIONS
##
@@ -493,7 +548,7 @@ class Resource(collections.MutableMapping):
body = body[cls.resource_key]
if include_headers:
body.update(response.headers)
body[HEADERS] = response.headers
return body
@@ -550,7 +605,7 @@ class Resource(collections.MutableMapping):
a compound URL.
See `How path_args are used`_ for details.
:return: A ``dict`` representing the headers.
:return: A ``dict`` containing the headers.
:raises: :exc:`~openstack.exceptions.MethodNotSupported` if
:data:`Resource.allow_head` is not set to ``True``.
"""
@@ -565,7 +620,7 @@ class Resource(collections.MutableMapping):
data = session.head(url, service=cls.service, accept=None).headers
return data
return {HEADERS: data}
@classmethod
def head_by_id(cls, session, resource_id, path_args=None):

View File

@@ -69,7 +69,7 @@ LIST_EXAMPLE = [
class TestAccount(testtools.TestCase):
def test_make_it(self):
sot = container.Container.new(**ACCOUNT_EXAMPLE)
sot = container.Container.new(**{'headers': ACCOUNT_EXAMPLE})
self.assertIsNone(sot.id)
self.assertEqual(ACCOUNT_EXAMPLE['x-timestamp'], sot.timestamp)
self.assertEqual(ACCOUNT_EXAMPLE['x-account-bytes-used'],
@@ -117,7 +117,7 @@ class TestContainer(testtools.TestCase):
sot = container.Container(CONT_EXAMPLE)
# Update container with HEAD data
sot._attrs.update(HEAD_EXAMPLE)
sot._attrs.update({'headers': HEAD_EXAMPLE})
# Attributes from create
self.assertEqual(CONT_EXAMPLE['name'], sot.id)

View File

@@ -13,7 +13,6 @@
import mock
import testtools
from openstack import exceptions
from openstack.object_store.v1 import obj
@@ -39,17 +38,19 @@ OBJ_EXAMPLE = {
"content_type": "application/octet-stream"
}
HEAD_EXAMPLE = {
'content-length': '252466',
DICT_EXAMPLE = {
'container': CONTAINER_NAME,
'name': OBJECT_NAME,
'accept-ranges': 'bytes',
'last-modified': 'Sun, 13 Jul 2014 18:41:04 GMT',
'etag': '243f87b91224d85722564a80fd3cb1f1',
'x-timestamp': '1405276863.31924',
'date': 'Thu, 28 Aug 2014 14:41:59 GMT',
'content-type': 'application/octet-stream',
'id': 'tx5fb5ad4f4d0846c6b2bc7-0053ff3fb7'
'headers': {
'content-length': '252466',
'accept-ranges': 'bytes',
'last-modified': 'Sun, 13 Jul 2014 18:41:04 GMT',
'etag': '243f87b91224d85722564a80fd3cb1f1',
'x-timestamp': '1405276863.31924',
'date': 'Thu, 28 Aug 2014 14:41:59 GMT',
'content-type': 'application/octet-stream',
'id': 'tx5fb5ad4f4d0846c6b2bc7-0053ff3fb7'
}
}
@@ -83,17 +84,18 @@ class TestObject(testtools.TestCase):
self.assertEqual(CONTAINER_NAME, sot.container)
def test_head(self):
sot = obj.Object.existing(**HEAD_EXAMPLE)
sot = obj.Object.existing(**DICT_EXAMPLE)
# Attributes from header
self.assertEqual(HEAD_EXAMPLE['container'], sot.container)
self.assertEqual(HEAD_EXAMPLE['content-length'], sot.content_length)
self.assertEqual(HEAD_EXAMPLE['accept-ranges'], sot.accept_ranges)
self.assertEqual(HEAD_EXAMPLE['last-modified'], sot.last_modified)
self.assertEqual(HEAD_EXAMPLE['etag'], sot.etag)
self.assertEqual(HEAD_EXAMPLE['x-timestamp'], sot.timestamp)
self.assertEqual(HEAD_EXAMPLE['date'], sot.date)
self.assertEqual(HEAD_EXAMPLE['content-type'], sot.content_type)
self.assertEqual(DICT_EXAMPLE['container'], sot.container)
headers = DICT_EXAMPLE['headers']
self.assertEqual(headers['content-length'], sot.content_length)
self.assertEqual(headers['accept-ranges'], sot.accept_ranges)
self.assertEqual(headers['last-modified'], sot.last_modified)
self.assertEqual(headers['etag'], sot.etag)
self.assertEqual(headers['x-timestamp'], sot.timestamp)
self.assertEqual(headers['date'], sot.date)
self.assertEqual(headers['content-type'], sot.content_type)
def test_get(self):
sot = obj.Object.new(container=CONTAINER_NAME, name=OBJECT_NAME)
@@ -110,8 +112,3 @@ class TestObject(testtools.TestCase):
self.sess.get.assert_called_with(url, service=sot.service,
accept="bytes", headers=headers)
self.assertEqual(rv, self.resp.content)
def test_cant_get(self):
sot = obj.Object.new(container=CONTAINER_NAME, name=OBJECT_NAME)
sot.allow_retrieve = False
self.assertRaises(exceptions.MethodNotSupported, sot.get, self.sess)

View File

@@ -35,15 +35,14 @@ class TestObjectStoreProxy(test_proxy_base.TestProxyBase):
class Test_account_metadata(TestObjectStoreProxy):
@mock.patch("openstack.resource.Resource")
def test_get_account_metadata(self, mock_resource):
@mock.patch("openstack.resource.Resource.head")
def test_get_account_metadata(self, mock_head):
cont = container.Container()
mock_resource = mock.MagicMock()
mock_resource.head.return_value = cont
mock_head.return_value = cont
result = self.proxy.get_account_metadata()
self.assertEqual(result, cont)
self.assertEqual(cont, result)
def test_set_account_metadata(self):
container = mock.MagicMock()

View File

@@ -176,6 +176,45 @@ class PropTests(base.TestCase):
self.assertEqual(sot.attr, val)
class HeaderTests(base.TestCase):
class Test(resource.Resource):
hey = resource.header("vocals")
ho = resource.header("guitar")
letsgo = resource.header("bass")
def test_get(self):
val = "joey"
args = {"vocals": val}
sot = HeaderTests.Test({'headers': args})
self.assertEqual(val, sot.hey)
self.assertEqual(None, sot.ho)
self.assertEqual(None, sot.letsgo)
def test_set_new(self):
args = {"vocals": "joey", "bass": "deedee"}
sot = HeaderTests.Test({'headers': args})
sot._reset_dirty()
sot.ho = "johnny"
self.assertEqual("johnny", sot.ho)
self.assertTrue(sot.is_dirty)
def test_set_old(self):
args = {"vocals": "joey", "bass": "deedee"}
sot = HeaderTests.Test({'headers': args})
sot._reset_dirty()
sot.letsgo = "cj"
self.assertEqual("cj", sot.letsgo)
self.assertTrue(sot.is_dirty)
def test_set_brand_new(self):
sot = HeaderTests.Test({'headers': {}})
sot._reset_dirty()
sot.ho = "johnny"
self.assertEqual("johnny", sot.ho)
self.assertTrue(sot.is_dirty)
self.assertEqual({'headers': {"guitar": "johnny"}}, sot)
class ResourceTests(base.TestTransportBase):
TEST_URL = fakes.FakeAuthenticator.ENDPOINT
@@ -352,7 +391,7 @@ class ResourceTests(base.TestTransportBase):
r_id = "my_id"
resp = FakeResource2.head_data_by_id(sess, resource_id=r_id)
self.assertEqual(resp, response_value)
self.assertEqual({'headers': response_value}, resp)
sess.head.assert_called_with(
utils.urljoin(FakeResource2.base_path, r_id),
service=FakeResource2.service,
@@ -361,7 +400,7 @@ class ResourceTests(base.TestTransportBase):
path_args = {"name": "my_name"}
resp = FakeResource2.head_data_by_id(sess, resource_id=r_id,
path_args=path_args)
self.assertEqual(resp, response_value)
self.assertEqual({'headers': response_value}, resp)
sess.head.assert_called_with(
utils.urljoin(FakeResource2.base_path % path_args, r_id),
service=FakeResource2.service,
@@ -504,8 +543,8 @@ class ResourceTests(base.TestTransportBase):
**headers)
class FakeResource2(FakeResource):
header1 = resource.prop("header1")
header2 = resource.prop("header2")
header1 = resource.header("header1")
header2 = resource.header("header2")
obj = FakeResource2.get_by_id(self.session, fake_id,
path_args=fake_arguments,
@@ -515,8 +554,8 @@ class ResourceTests(base.TestTransportBase):
self.assertEqual(fake_name, obj['name'])
self.assertEqual(fake_attr1, obj['attr1'])
self.assertEqual(fake_attr2, obj['attr2'])
self.assertEqual(header1, obj['header1'])
self.assertEqual(header2, obj['header2'])
self.assertEqual(header1, obj['headers']['header1'])
self.assertEqual(header2, obj['headers']['header2'])
self.assertEqual(fake_name, obj.name)
self.assertEqual(fake_attr1, obj.first)
@@ -526,20 +565,21 @@ class ResourceTests(base.TestTransportBase):
@httpretty.activate
def test_head(self):
class FakeResource2(FakeResource):
header1 = resource.header("header1")
header2 = resource.header("header2")
self.stub_url(httpretty.HEAD, path=[fake_path, fake_id],
name=fake_name,
attr1=fake_attr1,
attr2=fake_attr2)
obj = FakeResource.head_by_id(self.session, fake_id,
path_args=fake_arguments)
header1='one',
header2='two')
obj = FakeResource2.head_by_id(self.session, fake_id,
path_args=fake_arguments)
self.assertEqual(fake_name, obj['name'])
self.assertEqual(fake_attr1, obj['attr1'])
self.assertEqual(fake_attr2, obj['attr2'])
self.assertEqual('one', obj['headers']['header1'])
self.assertEqual('two', obj['headers']['header2'])
self.assertEqual(fake_name, obj.name)
self.assertEqual(fake_attr1, obj.first)
self.assertEqual(fake_attr2, obj.second)
self.assertEqual('one', obj.header1)
self.assertEqual('two', obj.header2)
@httpretty.activate
def test_update(self):