diff --git a/boxsdk/client/client.py b/boxsdk/client/client.py index 01a74735f..0df5fea85 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 d24d8ca41..724801864 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 582322293..442a792d1 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 000000000..b8eb841ff --- /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 07b8eed43..2a569fbb4 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 3d62b4370..456582457 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 5bd378aaa..fb9afc1da 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 d50209456..38d10b224 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 7786ac90d..13bf17fa4 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 bd2a09ade..9d58814f0 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 c2e9a89bc..3b5363eef 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 7d5adee82..dd4e39288 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 eaee934ab..14fb59afd 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 32219d4f1..52cca9d00 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 000000000..d6e6083c7 --- /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 diff --git a/test/functional/test_object_clone.py b/test/functional/test_object_clone.py new file mode 100644 index 000000000..edd7fdae4 --- /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 d069754f0..6c82453e4 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 6226cefe8..7d0c9cbe0 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