Repository: box/box-python-sdk
Pull request #154 (2016-08-01 01:03:24)

Cloneable
Includes proof of concept from https://github.com/Jeff-Meadows/box-python-sdk/tree/api_wrapper.  New api_call decorator can be used to allow methods that make an API call to accept an extra parameter, to be used for sending low level request parameters to Requests or whatever network layer is being used.

boxcla: Verified that @kelseymorris95 has signed the CLA. Thanks for the pull request!
Jeff-Meadows: 👍 
jmoldow: Looks good to me! There are a few minor things I pointed out here, but we can deal with them in another PR. 👍 

Patch:
diff --git a/boxsdk/client/client.py b/boxsdk/client/client.py
index 01a7473..0df5fea 100644
--- a/boxsdk/client/client.py
+++ b/boxsdk/client/client.py
@@ -6,13 +6,15 @@
 from ..config import API
 from ..session.box_session import BoxSession
 from ..network.default_network import DefaultNetwork
+from ..object.cloneable import Cloneable
+from ..util.api_call_decorator import api_call
 from ..object.search import Search
 from ..object.events import Events
 from ..util.shared_link import get_shared_link_header
 from ..util.translator import Translator
 
 
-class Client(object):
+class Client(Cloneable):
 
     def __init__(
             self,
@@ -34,6 +36,7 @@ def __init__(
         :type session:
             :class:`BoxSession`
         """
+        super(Client, self).__init__()
         network_layer = network_layer or DefaultNetwork()
         self._oauth = oauth
         self._network = network_layer
@@ -49,6 +52,16 @@ def auth(self):
         """
         return self._oauth
 
+    @property
+    def session(self):
+        """
+        Get the :class:`BoxSession` instance the client is using.
+
+        :rtype:
+            :class:`BoxSession`
+        """
+        return self._session
+
     def folder(self, folder_id):
         """
         Initialize a :class:`Folder` object, whose box id is folder_id.
@@ -124,6 +137,7 @@ def collaboration(self, collab_id):
         """
         return Translator().translate('collaboration')(session=self._session, object_id=collab_id)
 
+    @api_call
     def users(self, limit=None, offset=0, filter_term=None):
         """
         Get a list of all users for the Enterprise along with their user_id, public_name, and login.
@@ -160,6 +174,7 @@ def users(self, limit=None, offset=0, filter_term=None):
             response_object=item,
         ) for item in response['entries']]
 
+    @api_call
     def search(
             self,
             query,
@@ -246,6 +261,7 @@ def group_membership(self, group_membership_id):
             object_id=group_membership_id,
         )
 
+    @api_call
     def groups(self):
         """
         Get a list of all groups for the current user.
@@ -265,6 +281,7 @@ def groups(self):
             response_object=item,
         ) for item in response['entries']]
 
+    @api_call
     def create_group(self, name):
         """
         Create a group with the given name.
@@ -292,6 +309,7 @@ def create_group(self, name):
             response_object=response,
         )
 
+    @api_call
     def get_shared_item(self, shared_link, password=None):
         """
         Get information about a Box shared link. https://box-content.readme.io/reference#get-a-shared-item
@@ -322,6 +340,7 @@ def get_shared_item(self, shared_link, password=None):
             response_object=response,
         )
 
+    @api_call
     def make_request(self, method, url, **kwargs):
         """
         Make an authenticated request to the Box API.
@@ -343,6 +362,7 @@ def make_request(self, method, url, **kwargs):
         """
         return self._session.request(method, url, **kwargs)
 
+    @api_call
     def create_user(self, name, login=None, **user_attributes):
         """
         Create a new user. Can only be used if the current user is an enterprise admin, or the current authorization
@@ -375,35 +395,9 @@ def create_user(self, name, login=None, **user_attributes):
             response_object=response,
         )
 
-    def as_user(self, user):
-        """
-        Returns a new client object with default headers set up to make requests as the specified user.
-
-        :param user:
-            The user to impersonate when making API requests.
-        :type user:
-            :class:`User`
-        """
-        return self.__class__(self._oauth, self._network, self._session.as_user(user))
-
-    def with_shared_link(self, shared_link, shared_link_password):
-        """
-        Returns a new client object with default headers set up to make requests using the shared link for auth.
-
-        :param shared_link:
-            The shared link.
-        :type shared_link:
-            `unicode`
-        :param shared_link_password:
-            The password for the shared link.
-        :type shared_link_password:
-            `unicode`
-        """
-        return self.__class__(
-            self._oauth,
-            self._network,
-            self._session.with_shared_link(shared_link, shared_link_password),
-        )
+    def clone(self, session=None):
+        """Base class override."""
+        return self.__class__(self._oauth, self._network, session or self._session)
 
     def get_url(self, endpoint, *args):
         """
diff --git a/boxsdk/object/base_endpoint.py b/boxsdk/object/base_endpoint.py
index d24d8ca..7248018 100644
--- a/boxsdk/object/base_endpoint.py
+++ b/boxsdk/object/base_endpoint.py
@@ -2,8 +2,10 @@
 
 from __future__ import unicode_literals, absolute_import
 
+from .cloneable import Cloneable
 
-class BaseEndpoint(object):
+
+class BaseEndpoint(Cloneable):
     """A Box API endpoint."""
 
     def __init__(self, session, **kwargs):
@@ -20,6 +22,16 @@ def __init__(self, session, **kwargs):
         super(BaseEndpoint, self).__init__(**kwargs)
         self._session = session
 
+    @property
+    def session(self):
+        """
+        Get the :class:`BoxSession` instance the object is using.
+
+        :rtype:
+            :class:`BoxSession`
+        """
+        return self._session
+
     def get_url(self, endpoint, *args):
         """
         Return the URL used to access the endpoint.
@@ -38,28 +50,13 @@ def get_url(self, endpoint, *args):
         # pylint:disable=no-self-use
         return self._session.get_url(endpoint, *args)
 
-    def as_user(self, user):
-        """
-        Returns a new endpoint object with default headers set up to make requests as the specified user.
-
-        :param user:
-            The user to impersonate when making API requests.
-        :type user:
-            :class:`User`
+    def clone(self, session=None):
         """
-        return self.__class__(self._session.as_user(user))
+        Returns a copy of this cloneable object using the specified session.
 
-    def with_shared_link(self, shared_link, shared_link_password):
-        """
-        Returns a new endpoint object with default headers set up to make requests using the shared link for auth.
-
-        :param shared_link:
-            The shared link.
-        :type shared_link:
-            `unicode`
-        :param shared_link_password:
-            The password for the shared link.
-        :type shared_link_password:
-            `unicode`
+        :param session:
+            The Box session used to make requests.
+        :type session:
+            :class:`BoxSession`
         """
-        return self.__class__(self._session.with_shared_link(shared_link, shared_link_password))
+        return self.__class__(session or self._session)
diff --git a/boxsdk/object/base_object.py b/boxsdk/object/base_object.py
index 5823222..442a792 100644
--- a/boxsdk/object/base_object.py
+++ b/boxsdk/object/base_object.py
@@ -6,6 +6,7 @@
 from .base_endpoint import BaseEndpoint
 from .base_api_json_object import BaseAPIJSONObject
 from ..util.translator import Translator
+from ..util.api_call_decorator import api_call
 
 
 class BaseObject(BaseEndpoint, BaseAPIJSONObject):
@@ -62,6 +63,7 @@ def object_id(self):
         """
         return self._object_id
 
+    @api_call
     def get(self, fields=None, headers=None):
         """
         Get information about the object, specified by fields. If fields is None, return the default fields.
@@ -84,6 +86,7 @@ def get(self, fields=None, headers=None):
         box_response = self._session.get(url, params=params, headers=headers)
         return self.__class__(self._session, self._object_id, box_response.json())
 
+    @api_call
     def update_info(self, data, params=None, headers=None, **kwargs):
         """Update information about this object.
 
@@ -127,6 +130,7 @@ def update_info(self, data, params=None, headers=None, **kwargs):
             response_object=response,
         )
 
+    @api_call
     def delete(self, params=None, headers=None):
         """ Delete the object.
 
@@ -212,14 +216,10 @@ def _paging_wrapper(self, url, starting_index, limit, factory=None):
             if current_index >= response['total_count']:
                 break
 
-    def as_user(self, user):
-        """ Base class override. """
-        return self.__class__(self._session.as_user(user), self._object_id, self._response_object)
-
-    def with_shared_link(self, shared_link, shared_link_password):
-        """ Base class override. """
+    def clone(self, session=None):
+        """Base class override."""
         return self.__class__(
-            self._session.with_shared_link(shared_link, shared_link_password),
+            session or self._session,
             self._object_id,
             self._response_object,
         )
diff --git a/boxsdk/object/cloneable.py b/boxsdk/object/cloneable.py
new file mode 100644
index 0000000..b8eb841
--- /dev/null
+++ b/boxsdk/object/cloneable.py
@@ -0,0 +1,54 @@
+# coding: utf-8
+
+from __future__ import unicode_literals, absolute_import
+
+
+class Cloneable(object):
+    """
+    Cloneable interface to be implemented by endpoint objects that should have ability to be cloned, but with a
+    different session member if desired.
+    """
+
+    def as_user(self, user):
+        """
+        Returns a new endpoint object with default headers set up to make requests as the specified user.
+        :param user:
+            The user to impersonate when making API requests.
+        :type user:
+            :class:`User`
+        """
+        return self.clone(self.session.as_user(user))
+
+    def with_shared_link(self, shared_link, shared_link_password):
+        """
+        Returns a new endpoint object with default headers set up to make requests using the shared link for auth.
+        :param shared_link:
+            The shared link.
+        :type shared_link:
+            `unicode`
+        :param shared_link_password:
+            The password for the shared link.
+        :type shared_link_password:
+            `unicode`
+        """
+        return self.clone(self.session.with_shared_link(shared_link, shared_link_password))
+
+    def clone(self, session=None):
+        """
+        Returns a copy of this cloneable object using the specified session.
+        :param session:
+            The Box session used to make requests.
+        :type session:
+            :class:`BoxSession`
+        """
+        raise NotImplementedError
+
+    @property
+    def session(self):
+        """
+        Return the Box session being used to make requests.
+
+        :rtype:
+            :class:`BoxSession`
+        """
+        raise NotImplementedError
diff --git a/boxsdk/object/collaboration.py b/boxsdk/object/collaboration.py
index 07b8eed..2a569fb 100644
--- a/boxsdk/object/collaboration.py
+++ b/boxsdk/object/collaboration.py
@@ -1,9 +1,10 @@
 # coding: utf-8
 
-from __future__ import unicode_literals
+from __future__ import unicode_literals, absolute_import
 
 from boxsdk.object.base_object import BaseObject
 from boxsdk.util.text_enum import TextEnum
+from ..util.api_call_decorator import api_call
 
 
 class CollaborationRole(TextEnum):
@@ -32,6 +33,7 @@ class Collaboration(BaseObject):
     _item_type = 'collaboration'
 
     # pylint:disable=arguments-differ
+    @api_call
     def update_info(self, role=None, status=None):
         """Edit an existing collaboration on Box
 
diff --git a/boxsdk/object/events.py b/boxsdk/object/events.py
index 3d62b43..4565824 100644
--- a/boxsdk/object/events.py
+++ b/boxsdk/object/events.py
@@ -5,6 +5,7 @@
 from six import with_metaclass
 
 from .base_endpoint import BaseEndpoint
+from ..util.api_call_decorator import api_call
 from ..util.enum import ExtendableEnumMeta
 from ..util.lru_cache import LRUCache
 from ..util.text_enum import TextEnum
@@ -57,6 +58,7 @@ def get_url(self, *args):
         """Base class override."""
         return super(Events, self).get_url('events', *args)
 
+    @api_call
     def get_events(self, limit=100, stream_position=0, stream_type=UserEventsStreamType.ALL):
         """
         Get Box events from a given stream position for a given stream type.
@@ -95,6 +97,7 @@ def get_events(self, limit=100, stream_position=0, stream_type=UserEventsStreamT
             response['entries'] = [Translator().translate(item['type'])(item) for item in response['entries']]
         return response
 
+    @api_call
     def get_latest_stream_position(self, stream_type=UserEventsStreamType.ALL):
         """
         Get the latest stream position. The return value can be used with :meth:`get_events` or
@@ -136,6 +139,7 @@ def _get_all_events_since(self, stream_position, stream_type=UserEventsStreamTyp
             if len(events) < 100:
                 return
 
+    @api_call
     def long_poll(self, options, stream_position):
         """
         Set up a long poll connection at the specified url.
@@ -163,6 +167,7 @@ def long_poll(self, options, stream_position):
         )
         return long_poll_response
 
+    @api_call
     def generate_events_with_long_polling(self, stream_position=None, stream_type=UserEventsStreamType.ALL):
         """
         Subscribe to events from the given stream position.
@@ -212,6 +217,7 @@ def generate_events_with_long_polling(self, stream_position=None, stream_type=Us
                     else:
                         break
 
+    @api_call
     def get_long_poll_options(self, stream_type=UserEventsStreamType.ALL):
         """
         Get the url and retry timeout for setting up a long polling connection.
diff --git a/boxsdk/object/file.py b/boxsdk/object/file.py
index 5bd378a..fb9afc1 100644
--- a/boxsdk/object/file.py
+++ b/boxsdk/object/file.py
@@ -5,6 +5,7 @@
 from boxsdk.config import API
 from .item import Item
 from .metadata import Metadata
+from ..util.api_call_decorator import api_call
 
 
 class File(Item):
@@ -44,6 +45,7 @@ def _get_accelerator_upload_url_for_update(self):
         """
         return self._get_accelerator_upload_url(file_id=self._object_id)
 
+    @api_call
     def content(self):
         """
         Get the content of a file on Box.
@@ -57,6 +59,7 @@ def content(self):
         box_response = self._session.get(url, expect_json_response=False)
         return box_response.content
 
+    @api_call
     def download_to(self, writeable_stream):
         """
         Download the file; write it to the given stream.
@@ -71,6 +74,7 @@ def download_to(self, writeable_stream):
         for chunk in box_response.network_response.response_as_stream.stream(decode_content=True):
             writeable_stream.write(chunk)
 
+    @api_call
     def update_contents_with_stream(
             self,
             file_stream,
@@ -133,6 +137,7 @@ def update_contents_with_stream(
             response_object=self._session.post(url, expect_json_response=False, files=files, headers=headers).json(),
         )
 
+    @api_call
     def update_contents(
             self,
             file_path,
@@ -186,6 +191,7 @@ def update_contents(
                 upload_using_accelerator=upload_using_accelerator,
             )
 
+    @api_call
     def lock(self, prevent_download=False):
         """
         Lock a file, preventing others from modifying (or possibly even downloading) it.
@@ -207,6 +213,7 @@ def lock(self, prevent_download=False):
         }
         return self.update_info(data)
 
+    @api_call
     def unlock(self):
         """
         Unlock a file, releasing any restrictions that the lock maintained.
@@ -239,6 +246,7 @@ def metadata(self, scope='global', template='properties'):
         """
         return Metadata(self._session, self, scope, template)
 
+    @api_call
     def get_shared_link_download_url(
             self,
             access=None,
diff --git a/boxsdk/object/folder.py b/boxsdk/object/folder.py
index d502094..38d10b2 100644
--- a/boxsdk/object/folder.py
+++ b/boxsdk/object/folder.py
@@ -9,6 +9,7 @@
 from boxsdk.object.group import Group
 from boxsdk.object.item import Item
 from boxsdk.object.user import User
+from boxsdk.util.api_call_decorator import api_call
 from boxsdk.util.text_enum import TextEnum
 from boxsdk.util.translator import Translator
 
@@ -122,6 +123,7 @@ def _get_accelerator_upload_url_fow_new_uploads(self):
         """
         return self._get_accelerator_upload_url()
 
+    @api_call
     def get_items(self, limit, offset=0, fields=None):
         """Get the items in a folder.
 
@@ -153,6 +155,7 @@ def get_items(self, limit, offset=0, fields=None):
         response = box_response.json()
         return [Translator().translate(item['type'])(self._session, item['id'], item) for item in response['entries']]
 
+    @api_call
     def upload_stream(
             self,
             file_stream,
@@ -221,6 +224,7 @@ def upload_stream(
             response_object=file_response,
         )
 
+    @api_call
     def upload(
             self,
             file_path=None,
@@ -276,6 +280,7 @@ def upload(
                 upload_using_accelerator=upload_using_accelerator,
             )
 
+    @api_call
     def create_subfolder(self, name):
         """
         Create a subfolder with the given name in the folder.
@@ -300,6 +305,7 @@ def create_subfolder(self, name):
             response_object=response,
         )
 
+    @api_call
     def update_sync_state(self, sync_state):
         """Update the ``sync_state`` attribute of this folder.
 
@@ -320,6 +326,7 @@ def update_sync_state(self, sync_state):
         }
         return self.update_info(data=data)
 
+    @api_call
     def add_collaborator(self, collaborator, role, notify=False):
         """Add a collaborator to the folder
 
@@ -363,6 +370,7 @@ def add_collaborator(self, collaborator, role, notify=False):
             response_object=collaboration_response,
         )
 
+    @api_call
     def delete(self, recursive=True, etag=None):
         """Base class override. Delete the folder.
 
diff --git a/boxsdk/object/group.py b/boxsdk/object/group.py
index 7786ac9..13bf17f 100644
--- a/boxsdk/object/group.py
+++ b/boxsdk/object/group.py
@@ -7,6 +7,7 @@
 from .base_object import BaseObject
 from ..config import API
 from ..util.translator import Translator
+from ..util.api_call_decorator import api_call
 
 
 class Group(BaseObject):
@@ -14,6 +15,7 @@ class Group(BaseObject):
 
     _item_type = 'group'
 
+    @api_call
     def membership(self, starting_index=0, limit=100, include_page_info=False):
         """
         A generator over all the members of this Group. The paging in the API is transparently implemented
@@ -54,6 +56,7 @@ def membership(self, starting_index=0, limit=100, include_page_info=False):
                 group_membership, _, _ = group_membership_tuple
                 yield group_membership
 
+    @api_call
     def add_member(self, user, role):
         """
         Add the given user to this group under the given role
diff --git a/boxsdk/object/group_membership.py b/boxsdk/object/group_membership.py
index bd2a09a..9d58814 100644
--- a/boxsdk/object/group_membership.py
+++ b/boxsdk/object/group_membership.py
@@ -75,20 +75,10 @@ def _init_user_and_group_instances(session, response_object, user, group):
 
         return user, group
 
-    def as_user(self, user):
-        """ Base class override. """
+    def clone(self, session=None):
+        """Base class override."""
         return self.__class__(
-            self._session.as_user(user),
-            self._object_id,
-            self._response_object,
-            self.user,
-            self.group,
-        )
-
-    def with_shared_link(self, shared_link, shared_link_password):
-        """ Base class override. """
-        return self.__class__(
-            self._session.with_shared_link(shared_link, shared_link_password),
+            session or self._session,
             self._object_id,
             self._response_object,
             self.user,
diff --git a/boxsdk/object/item.py b/boxsdk/object/item.py
index c2e9a89..3b5363e 100644
--- a/boxsdk/object/item.py
+++ b/boxsdk/object/item.py
@@ -6,6 +6,7 @@
 from .base_object import BaseObject
 from ..config import API
 from ..exception import BoxAPIException
+from ..util.api_call_decorator import api_call
 
 
 class Item(BaseObject):
@@ -74,6 +75,7 @@ def _preflight_check(self, size, name=None, file_id=None, parent_id=None):
             data=json.dumps(data),
         )
 
+    @api_call
     def update_info(self, data, etag=None):
         """Baseclass override.
 
@@ -93,6 +95,7 @@ def update_info(self, data, etag=None):
         headers = {'If-Match': etag} if etag is not None else None
         return super(Item, self).update_info(data, headers=headers)
 
+    @api_call
     def rename(self, name):
         """
         Rename the item to a new name.
@@ -107,6 +110,7 @@ def rename(self, name):
         }
         return self.update_info(data)
 
+    @api_call
     def get(self, fields=None, etag=None):
         """Base class override.
 
@@ -128,6 +132,7 @@ def get(self, fields=None, etag=None):
         headers = {'If-None-Match': etag} if etag is not None else None
         return super(Item, self).get(fields=fields, headers=headers)
 
+    @api_call
     def copy(self, parent_folder):
         """Copy the item to the given folder.
 
@@ -148,6 +153,7 @@ def copy(self, parent_folder):
             response_object=response,
         )
 
+    @api_call
     def move(self, parent_folder):
         """
         Move the item to the given folder.
@@ -162,6 +168,7 @@ def move(self, parent_folder):
         }
         return self.update_info(data)
 
+    @api_call
     def create_shared_link(
             self,
             access=None,
@@ -231,6 +238,7 @@ def create_shared_link(
 
         return self.update_info(data, etag=etag)
 
+    @api_call
     def get_shared_link(
             self,
             access=None,
@@ -289,6 +297,7 @@ def get_shared_link(
         )
         return item.shared_link['url']  # pylint:disable=no-member
 
+    @api_call
     def remove_shared_link(self, etag=None):
         """Delete the shared link for the item.
 
@@ -306,6 +315,7 @@ def remove_shared_link(self, etag=None):
         item = self.update_info(data, etag=etag)
         return item.shared_link is None  # pylint:disable=no-member
 
+    @api_call
     def delete(self, params=None, etag=None):
         """Delete the item.
 
diff --git a/boxsdk/object/metadata.py b/boxsdk/object/metadata.py
index 7d5adee..dd4e392 100644
--- a/boxsdk/object/metadata.py
+++ b/boxsdk/object/metadata.py
@@ -3,6 +3,7 @@
 from __future__ import unicode_literals, absolute_import
 import json
 from boxsdk.object.base_endpoint import BaseEndpoint
+from ..util.api_call_decorator import api_call
 
 
 class MetadataUpdate(object):
@@ -139,6 +140,7 @@ def start_update():
         """
         return MetadataUpdate()
 
+    @api_call
     def update(self, metadata_update):
         """
         Update the key/value pairs associated with this metadata object.
@@ -159,6 +161,7 @@ def update(self, metadata_update):
             headers={b'Content-Type': b'application/json-patch+json'},
         ).json()
 
+    @api_call
     def get(self):
         """
         Get the key/value pairs that make up this metadata instance.
@@ -170,6 +173,7 @@ def get(self):
         """
         return self._session.get(self.get_url()).json()
 
+    @api_call
     def delete(self):
         """
         Delete the metadata object.
@@ -181,6 +185,7 @@ def delete(self):
         """
         return self._session.delete(self.get_url()).ok
 
+    @api_call
     def create(self, metadata):
         """
         Create the metadata instance on Box. If the instance already exists, use :meth:`update` instead.
@@ -200,15 +205,6 @@ def create(self, metadata):
             headers={b'Content-Type': b'application/json'},
         ).json()
 
-    def as_user(self, user):
+    def clone(self, session=None):
         """ Base class override. """
-        return self.__class__(self._session.as_user(user), self._object, self._scope, self._template)
-
-    def with_shared_link(self, shared_link, shared_link_password):
-        """ Base class override. """
-        return self.__class__(
-            self._session.with_shared_link(shared_link, shared_link_password),
-            self._object,
-            self._scope,
-            self._template
-        )
+        return self.__class__(session or self._session, self._object, self._scope, self._template)
diff --git a/boxsdk/object/search.py b/boxsdk/object/search.py
index eaee934..14fb59a 100644
--- a/boxsdk/object/search.py
+++ b/boxsdk/object/search.py
@@ -6,6 +6,7 @@
 
 from .base_endpoint import BaseEndpoint
 from ..util.translator import Translator
+from ..util.api_call_decorator import api_call
 
 
 class MetadataSearchFilter(object):
@@ -156,6 +157,7 @@ def make_single_metadata_filter(template_key, scope):
         """
         return MetadataSearchFilter(template_key, scope)
 
+    @api_call
     def search(
             self,
             query,
diff --git a/boxsdk/session/box_session.py b/boxsdk/session/box_session.py
index 32219d4..52cca9d 100644
--- a/boxsdk/session/box_session.py
+++ b/boxsdk/session/box_session.py
@@ -65,7 +65,7 @@ class BoxSession(object):
     Box API session. Provides auth, automatic retry of failed requests, and session renewal.
     """
 
-    def __init__(self, oauth, network_layer, default_headers=None):
+    def __init__(self, oauth, network_layer, default_headers=None, default_network_request_kwargs=None):
         """
         :param oauth:
             OAuth2 object used by the session to authorize requests.
@@ -79,12 +79,21 @@ def __init__(self, oauth, network_layer, default_headers=None):
             A dictionary containing default values to be used as headers when this session makes an API request.
         :type default_headers:
             `dict` or None
+        :param default_network_request_kwargs:
+            A dictionary containing default values to be passed to the network layer
+            when this session makes an API request.
+        :type default_network_request_kwargs:
+            `dict` or None
         """
+        super(BoxSession, self).__init__()
         self._oauth = oauth
         self._network_layer = network_layer
         self._default_headers = {'User-Agent': Client.USER_AGENT_STRING}
+        self._default_network_request_kwargs = {}
         if default_headers:
             self._default_headers.update(default_headers)
+        if default_network_request_kwargs:
+            self._default_network_request_kwargs.update(default_network_request_kwargs)
 
     def get_url(self, endpoint, *args):
         """
@@ -117,7 +126,7 @@ def as_user(self, user):
         """
         headers = self._default_headers.copy()
         headers['As-User'] = user.object_id
-        return self.__class__(self._oauth, self._network_layer, headers)
+        return self.__class__(self._oauth, self._network_layer, headers, self._default_network_request_kwargs.copy())
 
     def with_shared_link(self, shared_link, shared_link_password=None):
         """
@@ -134,7 +143,10 @@ def with_shared_link(self, shared_link, shared_link_password=None):
         """
         headers = self._default_headers.copy()
         headers.update(get_shared_link_header(shared_link, shared_link_password))
-        return self.__class__(self._oauth, self._network_layer, headers)
+        return self.__class__(self._oauth, self._network_layer, headers, self._default_network_request_kwargs.copy())
+
+    def with_default_network_request_kwargs(self, extra_network_parameters):
+        return self.__class__(self._oauth, self._network_layer, self._default_headers.copy(), extra_network_parameters)
 
     def _renew_session(self, access_token_used):
         """
@@ -347,10 +359,10 @@ def _make_request(
 
         # Reset stream positions to what they were when the request was made so the same data is sent even if this
         # is a retried attempt.
-        request_kwargs = kwargs
         files, file_stream_positions = kwargs.get('files'), kwargs.pop('file_stream_positions')
+        request_kwargs = self._default_network_request_kwargs.copy()
+        request_kwargs.update(kwargs)
         if files and file_stream_positions:
-            request_kwargs = kwargs.copy()
             for name, position in file_stream_positions.items():
                 files[name][1].seek(position)
             data = request_kwargs.pop('data', {})
diff --git a/boxsdk/util/api_call_decorator.py b/boxsdk/util/api_call_decorator.py
new file mode 100644
index 0000000..d6e6083
--- /dev/null
+++ b/boxsdk/util/api_call_decorator.py
@@ -0,0 +1,58 @@
+# coding: utf-8
+
+from __future__ import unicode_literals, absolute_import
+
+from functools import update_wrapper, wraps
+
+
+def api_call(method):
+    """
+    Designates the decorated method as one that makes a Box API call.
+    The decorated method can then accept a new keyword argument `extra_network_parameters`,
+    a dictionary of key-value pairs to be passed to the network layer for API
+    calls made by the method.
+
+    The decorated method must belong to a subclass of `Cloneable` as using this
+    decorator and then passing a `extra_network_parameters` parameter to the method will cause
+    the object's clone method to be called.
+
+    :param method:
+        The method to decorate.
+    :type method:
+        `callable`
+    :return:
+        A wrapped method that can pass extra request data to the network layer.
+    :rtype:
+        :class:`APICallWrapper`
+    """
+    return APICallWrapper(method)
+
+
+class APICallWrapper(object):
+
+    def __init__(self, func_that_makes_an_api_call):
+        super(APICallWrapper, self).__init__()
+        self._func_that_makes_an_api_call = func_that_makes_an_api_call
+        update_wrapper(self, func_that_makes_an_api_call)
+
+    def __get__(self, _instance, owner):
+        @wraps(self._func_that_makes_an_api_call)
+        def call(*args, **kwargs):
+            instance = _instance
+            if instance is None:
+                # If this is being called as an unbound method, the instance is the first arg.
+                if owner is not None and len(args) > 0 and isinstance(args[0], owner):
+                    instance = args[0]
+                    args = args[1:]
+                else:
+                    raise TypeError
+            extra_network_parameters = kwargs.pop('extra_network_parameters', None)
+            if extra_network_parameters:
+                # If extra_network_parameters is specified, then clone the instance, and specify the parameters
+                # as the defaults to be used.
+                # pylint: disable=protected-access
+                instance = instance.clone(instance._session.with_default_network_request_kwargs(extra_network_parameters))
+                # pylint: enable=protected-access
+            response = self._func_that_makes_an_api_call(instance, *args, **kwargs)
+            return response
+        return call


Test patch:
diff --git a/test/functional/test_object_clone.py b/test/functional/test_object_clone.py
new file mode 100644
index 0000000..edd7fda
--- /dev/null
+++ b/test/functional/test_object_clone.py
@@ -0,0 +1,44 @@
+# coding: utf-8
+
+from __future__ import unicode_literals, absolute_import
+from boxsdk.object.folder import FolderSyncState
+import pytest
+
+
+@pytest.fixture()
+def extra_network_parameters():
+    return {'timeout': 1}
+
+
+def test_folder_clone_during_create_subfolder(created_subfolder, extra_network_parameters):
+    # pylint:disable=redefined-outer-name
+    # pylint:disable=protected-access
+    original_folder = created_subfolder
+    original_folder_id = original_folder._object_id
+    original_folder_response_object = original_folder._response_object
+    original_session = original_folder._session
+    returned_folder = original_folder.create_subfolder('subfolder', extra_network_parameters=extra_network_parameters)
+    returned_session = returned_folder._session
+
+    assert original_session is not returned_session
+    assert original_session._default_network_request_kwargs == {}
+    assert returned_session._default_network_request_kwargs == {'timeout': 1}
+    assert original_folder._object_id == original_folder_id
+    assert original_folder._response_object == original_folder_response_object
+
+
+def test_folder_clone_during_update_sync_state(created_subfolder, extra_network_parameters):
+    # pylint:disable=redefined-outer-name
+    # pylint:disable=protected-access
+    original_folder = created_subfolder
+    original_folder_id = original_folder._object_id
+    original_folder_response_object = original_folder._response_object
+    original_session = original_folder._session
+    returned_folder = original_folder.update_sync_state(FolderSyncState.IS_SYNCED, extra_network_parameters=extra_network_parameters)
+    returned_session = returned_folder._session
+
+    assert original_session is not returned_session
+    assert original_session._default_network_request_kwargs == {}
+    assert returned_session._default_network_request_kwargs == {'timeout': 1}
+    assert original_folder._object_id == original_folder_id
+    assert original_folder._response_object == original_folder_response_object
diff --git a/test/unit/conftest.py b/test/unit/conftest.py
index d069754..6c82453 100644
--- a/test/unit/conftest.py
+++ b/test/unit/conftest.py
@@ -17,6 +17,13 @@ def mock_box_session():
     return mock_session
 
 
+@pytest.fixture()
+def mock_box_session_2():
+    mock_session = MagicMock(BoxSession)
+    mock_session.get_url.side_effect = lambda *args, **kwargs: BoxSession.get_url(mock_session, *args, **kwargs)
+    return mock_session
+
+
 @pytest.fixture(scope='function')
 def mock_network_layer():
     mock_network = Mock(DefaultNetwork)
diff --git a/test/unit/object/test_item.py b/test/unit/object/test_item.py
index 6226cef..7d0c9cb 100644
--- a/test/unit/object/test_item.py
+++ b/test/unit/object/test_item.py
@@ -25,6 +25,21 @@ def test_update_info(test_item_and_response, mock_box_session, etag, if_match_he
     assert update_response.object_id == test_item.object_id
 
 
+def test_update_info_with_default_request_kwargs(test_item_and_response, mock_box_session, mock_box_session_2):
+    # pylint:disable=redefined-outer-name, protected-access
+    test_item, mock_item_response = test_item_and_response
+    expected_url = test_item.get_url()
+    mock_box_session.with_default_network_request_kwargs.return_value = mock_box_session_2
+    mock_box_session_2.put.return_value = mock_item_response
+    data = {'foo': 'bar', 'baz': {'foo': 'bar'}, 'num': 4}
+    extra_network_parameters = {'timeout': 1}
+    update_response = test_item.update_info(data, extra_network_parameters=extra_network_parameters)
+    mock_box_session.with_default_network_request_kwargs.assert_called_once_with({'timeout': 1})
+    mock_box_session_2.put.assert_called_once_with(expected_url, data=json.dumps(data), headers=None, params=None)
+    assert isinstance(update_response, test_item.__class__)
+    assert update_response.object_id == test_item.object_id
+
+
 def test_rename_item(test_item_and_response, mock_box_session):
     # pylint:disable=redefined-outer-name, protected-access
     test_item, mock_item_response = test_item_and_response