Repository: box/box-python-sdk
Pull request #359 (2018-10-29 19:12:05)

File endpoints

boxcla: Verified that @mattwiller has signed the CLA. Thanks for the pull request!
coveralls: ## Pull Request Test Coverage Report for [Build 1271](https://coveralls.io/builds/19790410)

* **87** of **89**   **(97.75%)**  changed or added relevant lines in **6** files are covered.
* No unchanged relevant lines lost coverage.
* Overall coverage increased (+**0.02%**) to **96.987%**

---

|  Changes Missing Coverage | Covered Lines | Changed/Added Lines | % |
| :-----|--------------|--------|---: |
| [boxsdk/object/file.py](https://coveralls.io/builds/19790410/source?filename=boxsdk%2Fobject%2Ffile.py#L662) | 66 | 67 | 98.51%
| [boxsdk/session/box_response.py](https://coveralls.io/builds/19790410/source?filename=boxsdk%2Fsession%2Fbox_response.py#L56) | 1 | 2 | 50.0%
<!-- | **Total:** | **87** | **89** | **97.75%** | -->


|  Totals | [![Coverage Status](https://coveralls.io/builds/19790410/badge)](https://coveralls.io/builds/19790410) |
| :-- | --: |
| Change from base [Build 1269](https://coveralls.io/builds/19789357): |  0.02% |
| Covered Lines: | 2479 |
| Relevant Lines: | 2556 |

---
##### 💛  - [Coveralls](https://coveralls.io)


Patch:
diff --git a/boxsdk/client/client.py b/boxsdk/client/client.py
index 83c7606..226e663 100644
--- a/boxsdk/client/client.py
+++ b/boxsdk/client/client.py
@@ -108,6 +108,21 @@ def file(self, file_id):
         """
         return self.translator.get('file')(session=self._session, object_id=file_id)
 
+    def file_version(self, version_id):
+        """
+        Initialize a :class:`FileVersion` object, whose box id is version_id.
+
+        :param version_id:
+            The box id of the :class:`FileVersion` object.
+        :type version_id:
+            `unicode`
+        :return:
+            A :class:`FileVersion` object with the given file version id.
+        :rtype:
+            :class:`FileVersion`
+        """
+        return self.translator.get('file_version')(session=self._session, object_id=version_id)
+
     def upload_session(self, session_id):
         """
         Initialize a :class:`UploadSession` object, whose box id is session_id.
diff --git a/boxsdk/object/__init__.py b/boxsdk/object/__init__.py
index cf472a7..f79867f 100644
--- a/boxsdk/object/__init__.py
+++ b/boxsdk/object/__init__.py
@@ -17,6 +17,7 @@
     'events',
     'event',
     'file',
+    'file_version',
     'file_version_retention',
     'folder',
     'group',
diff --git a/boxsdk/object/file.py b/boxsdk/object/file.py
index 72fde9c..2028015 100644
--- a/boxsdk/object/file.py
+++ b/boxsdk/object/file.py
@@ -78,22 +78,54 @@ def _get_accelerator_upload_url_for_update(self):
         """
         return self._get_accelerator_upload_url(file_id=self._object_id)
 
+    @staticmethod
+    def _construct_range_header(boundaries):
+        """
+        Construct the correct value for the Range header, given a closed or open-ended range.
+
+        :param boundaries:
+            The range of bytes (inclusive)
+        :type boundaries:
+            (`int`) or (`int`, `int`)
+        :returns:
+            The value for the Range header
+        :rtype:
+            `unicode`
+        :raises ValueError:
+        """
+        if len(boundaries) == 1:
+            return 'bytes={0}-'.format(*boundaries)
+        elif len(boundaries) == 2:
+            return 'bytes={0}-{1}'.format(*boundaries)
+        else:
+            raise ValueError('Expected a 1-tuple or 2-tuple for byte range')
+
     @api_call
-    def content(self):
+    def content(self, file_version=None, byte_range=None):
         """
         Get the content of a file on Box.
 
+        :param file_version:
+            The specific version of the file to retrieve the contents of.
+        :type file_version:
+            :class:`FileVersion` or None
+        :param byte_range:
+            A tuple of inclusive byte offsets to download, e.g. (100, 199) to download the second 100 bytes of a file
+        :type byte_range:
+            (`int`, `int`)
         :returns:
             File content as bytes.
         :rtype:
             `bytes`
         """
         url = self.get_url('content')
-        box_response = self._session.get(url, expect_json_response=False)
+        params = {'version': file_version.object_id} if file_version is not None else None
+        headers = {'Range': self._construct_range_header(byte_range)} if byte_range is not None else None
+        box_response = self._session.get(url, expect_json_response=False, params=params, headers=headers)
         return box_response.content
 
     @api_call
-    def download_to(self, writeable_stream):
+    def download_to(self, writeable_stream, file_version=None, byte_range=None):
         """
         Download the file; write it to the given stream.
 
@@ -101,12 +133,34 @@ def download_to(self, writeable_stream):
             A file-like object where bytes can be written into.
         :type writeable_stream:
             `file`
+        :param file_version:
+            The specific version of the file to retrieve the contents of.
+        :type file_version:
+            :class:`FileVersion` or None
+        :param byte_range:
+            A tuple of inclusive byte offsets to download, e.g. (100, 199) to download the second 100 bytes of a file
+        :type byte_range:
+            (`int`, `int`)
         """
         url = self.get_url('content')
-        box_response = self._session.get(url, expect_json_response=False, stream=True)
+        params = {'version': file_version.object_id} if file_version is not None else None
+        headers = {'Range': self._construct_range_header(byte_range)} if byte_range is not None else None
+        box_response = self._session.get(url, expect_json_response=False, stream=True, params=params, headers=headers)
         for chunk in box_response.network_response.response_as_stream.stream(decode_content=True):
             writeable_stream.write(chunk)
 
+    @api_call
+    def get_download_url(self, file_version=None):
+        url = self.get_url('content')
+        params = {'version': file_version.object_id} if file_version is not None else None
+        box_response = self._session.get(
+            url,
+            params=params,
+            expect_json_response=False,
+            allow_redirects=False,
+        )
+        return box_response.headers['location']
+
     @api_call
     def update_contents_with_stream(
             self,
@@ -231,7 +285,7 @@ def update_contents(
             )
 
     @api_call
-    def lock(self, prevent_download=False):
+    def lock(self, prevent_download=False, expire_time=None):
         """
         Lock a file, preventing others from modifying (or possibly even downloading) it.
 
@@ -239,6 +293,10 @@ def lock(self, prevent_download=False):
             Whether or not the lock should prevent other users from downloading the file.
         :type prevent_download:
             `bool`
+        :param expire_time:
+            The RFC-3339 datetime when the lock should automatically expire, unlocking the file.
+        :type expire_time:
+            `unicode` or None
         :return:
             A new :class:`File` instance reflecting that the file has been locked.
         :rtype:
@@ -246,10 +304,12 @@ def lock(self, prevent_download=False):
         """
         data = {
             'lock': {
-                'is_download_prevented': prevent_download,
                 'type': 'lock',
+                'is_download_prevented': prevent_download,
             }
         }
+        if expire_time is not None:
+            data['lock']['expires_at'] = expire_time
         return self.update_info(data)
 
     @api_call
@@ -433,3 +493,199 @@ def get_tasks(self, fields=None):
             fields=fields,
             return_full_pages=False,
         )
+
+    @api_call
+    def get_previous_versions(self, limit=None, offset=None, fields=None):
+        """
+        Get previous versions of the file.
+
+        :param limit:
+            The maximum number of items to return per page. If not specified, then will use the server-side default.
+        :type limit:
+            `int` or None
+        :param offset:
+            The index at which to start returning items.
+        :type offset:
+            `int`
+        :param fields:
+            List of fields to request.
+        :type fields:
+            `Iterable` of `unicode`
+        :returns:
+            An iterator of the previous versions of the file.
+        :rtype:
+            :class:`BoxObjectCollection`
+        """
+        return LimitOffsetBasedObjectCollection(
+            session=self.session,
+            url=self.get_url('versions'),
+            limit=limit,
+            fields=fields,
+            offset=offset,
+            return_full_pages=False,
+        )
+
+    @api_call
+    def promote_version(self, file_version):
+        """
+        Promote a file version to become the current version of this file.  This will create a new file version
+        identical to the previous version as the new current version.
+
+        :param file_version:
+            The file version to promote.
+        :type file_version:
+            :class:`FileVersion`
+        :returns:
+            The new file version created as the current.
+        :rtype:
+            :class:`FileVersion`
+        """
+        url = self.get_url('versions', 'current')
+        body = {
+            'type': 'file_version',
+            'id': file_version.object_id,
+        }
+        response = self._session.post(url, data=json.dumps(body)).json()
+        return self.translator.translate(
+            session=self._session,
+            response_object=response,
+        )
+
+    @api_call
+    def delete_version(self, file_version, etag=None):
+        """
+        Delete a specific version of a file.
+
+        :param file_version:
+            The file version to delete.
+        :type file_version:
+            :class:`FileVersion`
+        :param etag:
+            If specified, instruct the Box API to update the item only if the current version's etag matches.
+        :type etag:
+            `unicode` or None
+        :returns:
+            Whether the operation succeeded.
+        :rtype:
+            `boolean`
+        """
+        url = self.get_url('versions', file_version.object_id)
+        headers = {'If-Match': etag} if etag is not None else None
+        response = self._session.delete(url, expect_json_response=False, headers=headers)
+        return response.ok
+
+    @api_call
+    def get_embed_url(self):
+        """
+        Get a URL suitable for embedding the file in an iframe in a web application.
+
+        :returns:
+            The embed URL.
+        :rtype:
+            `unicode`
+        """
+        url = self.get_url()
+        params = {'fields': 'expiring_embed_link'}
+        response = self._session.get(url, params=params).json()
+        return response['expiring_embed_link']['url']
+
+    @api_call
+    def get_representation_info(self, rep_hints=None):
+        """
+        Get information about the representations available for a file.
+
+        :param rep_hints:
+            A formatted string describing which representations are desired.
+        :type rep_hints:
+            `unicode` or None
+        :returns:
+            The representation information
+        :rtype:
+            `list` of `dict`
+        """
+        url = self.get_url()
+        params = {'fields': 'representations'}
+        headers = {'X-Rep-Hints': rep_hints} if rep_hints is not None else None
+        response = self._session.get(url, params=params, headers=headers).json()
+        return response['representations']['entries']
+
+    @api_call
+    def get_thumbnail(self, extension='png', min_width=None, min_height=None, max_width=None, max_height=None):
+        """
+        Retrieve a thumbnail image for the file.
+
+        :param extension:
+            The file extension for the thumbnail, e.g. 'png' or 'jpg'
+        :type extension:
+            `unicode`
+        :param min_width:
+            The minimum width required for the thumbnail image
+        :type min_width:
+            `int` or None
+        :param min_height:
+            The minimum height required for the thumbnail image
+        :type min_height:
+            `int` or None
+        :param max_width:
+            The maximum width required for the thumbnail image
+        :type max_width:
+            `int` or None
+        :param max_height:
+            The maximum height required for the thumbnail image
+        :type max_height:
+            `int` or None
+        :returns:
+            The file contents of the thumbnail image
+        :rtype:
+            `bytes`
+        """
+        url = self.get_url('thumbnail.' + extension)
+        params = {}
+        if min_width is not None:
+            params['min_width'] = min_width
+        if min_height is not None:
+            params['min_height'] = min_height
+        if max_width is not None:
+            params['max_width'] = max_width
+        if max_height is not None:
+            params['max_height'] = max_height
+
+        response = self._session.get(url, params=params, expect_json_response=False)
+        return response.content
+
+    def copy(self, parent_folder, name=None, file_version=None):
+        # pylint: disable=arguments-differ
+        """Copy the item to the given folder.
+
+        :param parent_folder:
+            The folder to which the item should be copied.
+        :type parent_folder:
+            :class:`Folder`
+        :param name:
+            A new name for the item, in case there is already another item in the new parent folder with the same name.
+        :type name:
+            `unicode` or None
+        :param file_version:
+            A specific version of the file to copy
+        :type file_version:
+            :class:`FileVersion`
+        :returns:
+            The copy of the file
+        :rtype:
+            :class:`File`
+        """
+        url = self.get_url('copy')
+        data = {
+            'parent': {'id': parent_folder.object_id}
+        }
+        if name is not None:
+            data['name'] = name
+        if file_version is not None:
+            data['version'] = file_version.object_id
+        box_response = self._session.post(url, data=json.dumps(data))
+        response = box_response.json()
+        return self.__class__(
+            session=self._session,
+            object_id=response['id'],
+            response_object=response,
+        )
diff --git a/boxsdk/object/file_version.py b/boxsdk/object/file_version.py
new file mode 100644
index 0000000..be1809c
--- /dev/null
+++ b/boxsdk/object/file_version.py
@@ -0,0 +1,9 @@
+# coding: utf-8
+from __future__ import unicode_literals, absolute_import
+
+from .base_object import BaseObject
+
+
+class FileVersion(BaseObject):
+    """Represents a Box file version."""
+    _item_type = 'file_version'
diff --git a/boxsdk/object/item.py b/boxsdk/object/item.py
index 08ad8b6..deb00a1 100644
--- a/boxsdk/object/item.py
+++ b/boxsdk/object/item.py
@@ -7,6 +7,7 @@
 from ..exception import BoxAPIException
 from .metadata import Metadata
 from ..util.api_call_decorator import api_call
+from ..pagination.marker_based_dict_collection import MarkerBasedDictCollection
 from ..pagination.marker_based_object_collection import MarkerBasedObjectCollection
 
 
@@ -134,18 +135,24 @@ def get(self, fields=None, etag=None):
         return super(Item, self).get(fields=fields, headers=headers)
 
     @api_call
-    def copy(self, parent_folder):
+    def copy(self, parent_folder, name=None):
         """Copy the item to the given folder.
 
         :param parent_folder:
             The folder to which the item should be copied.
         :type parent_folder:
             :class:`Folder`
+        :param name:
+            A new name for the item, in case there is already another item in the new parent folder with the same name.
+        :type name:
+            `unicode` or None
         """
         url = self.get_url('copy')
         data = {
             'parent': {'id': parent_folder.object_id}
         }
+        if name is not None:
+            data['name'] = name
         box_response = self._session.post(url, data=json.dumps(data))
         response = box_response.json()
         return self.__class__(
@@ -155,18 +162,24 @@ def copy(self, parent_folder):
         )
 
     @api_call
-    def move(self, parent_folder):
+    def move(self, parent_folder, name=None):
         """
         Move the item to the given folder.
 
         :param parent_folder:
             The parent `Folder` object, where the item will be moved to.
         :type parent_folder:
-            `Folder`
+            :class:`Folder`
+        :param name:
+            A new name for the item, in case there is already another item in the new parent folder with the same name.
+        :type name:
+            `unicode` or None
         """
         data = {
             'parent': {'id': parent_folder.object_id}
         }
+        if name is not None:
+            data['name'] = name
         return self.update_info(data)
 
     @api_call
@@ -360,6 +373,18 @@ def metadata(self, scope='global', template='properties'):
         """
         return Metadata(self._session, self, scope, template)
 
+    def get_all_metadata(self):
+        """
+        Get all metadata attached to the item.
+        """
+        return MarkerBasedDictCollection(
+            session=self._session,
+            url=self.get_url('metadata'),
+            limit=None,
+            marker=None,
+            return_full_pages=False,
+        )
+
     @api_call
     def get_watermark(self):
         """
diff --git a/boxsdk/pagination/limit_offset_based_dict_collection.py b/boxsdk/pagination/limit_offset_based_dict_collection.py
index 487667e..e8001ad 100644
--- a/boxsdk/pagination/limit_offset_based_dict_collection.py
+++ b/boxsdk/pagination/limit_offset_based_dict_collection.py
@@ -7,4 +7,5 @@
 
 
 class LimitOffsetBasedDictCollection(LimitOffsetBasedObjectCollection):
+    """Represents a limit/offset-based collection of simple dicts, which are not translated into objects."""
     _page_constructor = DictPage
diff --git a/boxsdk/pagination/marker_based_dict_collection.py b/boxsdk/pagination/marker_based_dict_collection.py
new file mode 100644
index 0000000..ecf8493
--- /dev/null
+++ b/boxsdk/pagination/marker_based_dict_collection.py
@@ -0,0 +1,11 @@
+# coding: utf-8
+
+from __future__ import unicode_literals, absolute_import
+
+from .dict_page import DictPage
+from .marker_based_object_collection import MarkerBasedObjectCollection
+
+
+class MarkerBasedDictCollection(MarkerBasedObjectCollection):
+    """Represents a marker-based collection of simple dicts, which are not translated into objects."""
+    _page_constructor = DictPage
diff --git a/boxsdk/session/box_response.py b/boxsdk/session/box_response.py
index 252c14e..b1d97e2 100644
--- a/boxsdk/session/box_response.py
+++ b/boxsdk/session/box_response.py
@@ -45,6 +45,16 @@ def status_code(self):
         """
         return self._network_response.status_code
 
+    @property
+    def headers(self):
+        """
+        Get the response headers.
+
+        :rtype:
+            `dict`
+        """
+        return self._network_response.headers
+
     @property
     def network_response(self):
         """Return the underlying network response.
diff --git a/docs/file.md b/docs/file.md
deleted file mode 100644
index fb28d01..0000000
--- a/docs/file.md
+++ /dev/null
@@ -1,122 +0,0 @@
-Chunked Upload
---------------
-
-For large files or in cases where the network connection is less reliable,
-you may want to upload the file in parts.  This allows a single part to fail
-without aborting the entire upload, and failed parts can then be retried.
-
-<!-- START doctoc generated TOC please keep comment here to allow auto update -->
-<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
-
-
-- [Manual Process](#manual-process)
-  - [Create Upload Session for File Version](#create-upload-session-for-file-version)
-  - [Create Upload Session for File](#create-upload-session-for-file)
-  - [Upload Part](#upload-part)
-  - [Commit Upload Session](#commit-upload-session)
-  - [Abort Upload Session](#abort-upload-session)
-  - [List Upload Parts](#list-upload-parts)
-
-<!-- END doctoc generated TOC please keep comment here to allow auto update -->
-
-### Manual Process
-
-For more complicated upload scenarios, such as those being coordinated across multiple processes or when an unrecoverable error occurs with the automatic uploader, the endpoints for chunked upload operations are also exposed directly.
-
-For example, this is roughly how a chunked upload is done manually:
-
-```python
-import hashlib
-import os
-
-
-test_file_path = '/path/to/large_file.mp4'
-total_size = os.stat(test_file_path).st_size
-sha1 = hashlib.sha1()
-content_stream = open(test_file_path, 'rb')
-upload_session = client.folder(folder_id='11111').create_upload_session(file_size=total_size, file_name='test_file_name.mp4')
-part_array = []
-
-for part_num in range(upload_session.total_parts):
-
-    copied_length = 0
-    chunk = b''
-    while copied_length < upload_session.part_size:
-        bytes_read = content_stream.read(upload_session.part_size - copied_length)
-        if bytes_read is None:
-            # stream returns none when no bytes are ready currently but there are 
-            # potentially more bytes in the stream to be read.
-            continue
-        if len(bytes_read) == 0:
-            # stream is exhausted.
-            break
-        chunk += bytes_read
-        copied_length += len(bytes_read)
-
-    uploaded_part = upload_session.upload_part_bytes(chunk, part_num*upload_session.part_size, total_size)
-    part_array.append(uploaded_part)
-    updated_sha1 = sha1.update(chunk)
-content_sha1 = sha1.digest()
-uploaded_file = upload_session.commit(content_sha1=content_sha1, parts=part_array)
-print('File ID: {0} and File Name: {1}'.format(uploaded_file.id, uploaded_file.name))
-```
-
-The individual endpoint methods are detailed below:
-
-#### Create Upload Session for File Version
-
-To create an upload session for uploading a large version, use `file.create_upload_session(file_size, file_name=None)`
-
-```python
-file_size = 26000000
-upload_session = client.file('11111').create_upload_session(file_size=file_size)
-```
-
-#### Create Upload Session for File
-
-To create an upload session for uploading a large file, use
-`folder.create_upload_session(file_size, file_name)`
-
-```python
-file_size = 26000000
-file_name = 'test_file.pdf'
-upload_session = client.folder('22222').create_upload_session(file_size, file_name)
-```
-
-#### Upload Part
-
-To upload a part of the file to this session, use `upload_session.upload_part(part_bytes, offset, total_size, part_content_sha1=None)`
-
-```python
-offset = 25165824
-part_bytes = b'abcdefgh'
-upload_session = client.upload_session('11493C07ED3EABB6E59874D3A1EF3581')
-chunk = part_bytes.read(upload_session.part_size)
-upload_part = upload_session.upload_part(chunk, offset, total_size)
-```
-
-#### Commit Upload Session
-
-To commit the upload session to Box, use `upload_session.commit(content_sha1, parts=None, file_attributes=None, etag=None)`.
-
-```python
-import hashlib
-parts = client.upload_session('11493C07ED3EABB6E59874D3A1EF3581').get_parts()
-uploaded_file = client.upload_session('11493C07ED3EABB6E59874D3A1EF3581').commit(parts, sha1.digest())
-```
-
-#### Abort Upload Session
-
-To abort a chunked upload, use `upload_session.abort()`.
-
-```python
-client.upload_session('11493C07ED3EABB6E59874D3A1EF3581').abort()
-```
-
-#### List Upload Parts
-
-To return the list of parts uploaded so far, use `upload_session.get_parts(limit=None, offset=None)`.
-
-```python
-parts = client.upload_session('11493C07ED3EABB6E59874D3A1EF3581').get_parts()
-```
diff --git a/docs/files.md b/docs/files.md
new file mode 100644
index 0000000..eb3b787
--- /dev/null
+++ b/docs/files.md
@@ -0,0 +1,672 @@
+Files
+=====
+
+File objects represent individual files in Box. They can be used to download a
+file's contents, upload new versions, and perform other common file operations
+(move, copy, delete, etc.).
+
+<!-- START doctoc generated TOC please keep comment here to allow auto update -->
+<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
+
+
+- [Get a File's Information](#get-a-files-information)
+- [Update a File's Information](#update-a-files-information)
+- [Download a File](#download-a-file)
+- [Get Download URL](#get-download-url)
+- [Upload a File](#upload-a-file)
+- [Chunked Upload](#chunked-upload)
+  - [Manual Process](#manual-process)
+    - [Create Upload Session for File Version](#create-upload-session-for-file-version)
+    - [Create Upload Session for File](#create-upload-session-for-file)
+    - [Upload Part](#upload-part)
+    - [Commit Upload Session](#commit-upload-session)
+    - [Abort Upload Session](#abort-upload-session)
+    - [List Upload Parts](#list-upload-parts)
+- [Move a File](#move-a-file)
+- [Copy a File](#copy-a-file)
+- [Delete a File](#delete-a-file)
+- [Get Previous Versions of a File](#get-previous-versions-of-a-file)
+- [Upload a New Version of a File](#upload-a-new-version-of-a-file)
+- [Promote a Previous Version of a File](#promote-a-previous-version-of-a-file)
+- [Delete a Previous Version of a File](#delete-a-previous-version-of-a-file)
+- [Lock a File](#lock-a-file)
+- [Unlock a File](#unlock-a-file)
+- [Create a Shared Link](#create-a-shared-link)
+- [Get an Embed Link](#get-an-embed-link)
+- [Get File Representations](#get-file-representations)
+- [Get Thumbnail](#get-thumbnail)
+- [Add Metadata](#add-metadata)
+- [Get Metadata](#get-metadata)
+- [Update Metadata Values](#update-metadata-values)
+- [Remove Metadata](#remove-metadata)
+- [Get All Metadata](#get-all-metadata)
+
+<!-- END doctoc generated TOC please keep comment here to allow auto update -->
+
+Get a File's Information
+------------------------
+
+Calling [`file.get(fields=None)`][get_info] on a [`File`][file_class] retrieves information about the file from the API.
+This method returns a new [`File`][file_class] object populated with the information retrieved. 
+
+You can specify an `Iterable` of fields to retrieve from the API in the `fields` parameter.
+
+```python
+file_id = '11111'
+file_info = client.file(file_id).get()
+print('File "{0}" has a size of {1} bytes'.format(file_info.name, file_info.size))
+```
+
+[get_info]: https://box-python-sdk.readthedocs.io/en/latest/boxsdk.object.html#boxsdk.object.base_object.BaseObject.get
+[file_class]: https://box-python-sdk.readthedocs.io/en/latest/boxsdk.object.html#boxsdk.object.file.File
+
+Update a File's Information
+---------------------------
+
+To update fields on the [`File`][file_class] object, call [`file.update_info(data)`][update_info] with a `dict` of
+fields to update.  This method returns the updated [`File`][file_class] object, leaving the original it was called on
+unmodified.
+
+```python
+file_id = '11111'
+updated_file = client.file(file_id).update_info({'description': 'My file'})
+```
+
+Download a File
+---------------
+
+A file can be downloaded in two ways: by returning the entire contents of the file as `bytes` or by providing an output
+stream to which the contents of the file will be written.  For both methods, you can optionally download a specific
+version of the file by passing the desired [`FileVersion`][file_version_class] in the `file_version` parameter.  You may
+also wish to download only a certain chunk of the file by passing a tuple of byte offsets via the `byte_range`
+parameter — the lower and upper bounds you wish to download.
+
+To get the entire contents of the file as `bytes`, call [`file.content(file_version=None, byte_range=None)`][content].
+
+```python
+file_id = '11111'
+
+# Download the entire file into memory
+file_content = client.file(file_id).content()
+
+# Download a specific file verison
+file_version = client.file_version('12345')
+version_content = client.file(file_id).content(file_version=file_version)
+
+# Download the first 100 bytes of the file
+beginning_of_file_content = client.file(file_id).content(byte_range=(0,99))
+```
+
+To download the file contents to an output stream, call
+[`file.download_to(writeable_stream, file_version=None, byte_range=None)`][download_to] with the stream.
+
+```python
+file_id = '11111'
+
+# Write the Box file contents to disk
+output_file = open('file.pdf', 'wb')
+client.file(file_id).download_to(output_file)
+```
+
+[file_version_class]: https://box-python-sdk.readthedocs.io/en/latest/boxsdk.object.html#boxsdk.object.file_version.FileVersion
+[content]: https://box-python-sdk.readthedocs.io/en/latest/boxsdk.object.html#boxsdk.object.file.File.content
+[download_to]: https://box-python-sdk.readthedocs.io/en/latest/boxsdk.object.html#boxsdk.object.file.File.download_to
+
+Get Download URL
+----------------
+
+To get a download URL suitable for passing to a web browser or other application, which will allow someone to download
+the file, call [`file.get_download_url(file_version=None)`][get_download_url].  The will return a `unicode` string
+containing the file's download URL.  You can optionally pass a [`FileVersion`][file_version_class] via the
+`file_version` parameter to get a download URL for a specific version of the file.
+
+```python
+file_id = '11111'
+download_url = client.file(file_id).get_download_url()
+print('The file\'s download URL is: {0}'.format(download_url))
+```
+
+[get_download_url]: https://box-python-sdk.readthedocs.io/en/latest/boxsdk.object.html#boxsdk.object.file.File.get_download_url
+
+Upload a File
+-------------
+
+Files are uploaded to a folder in one of two ways: by providing a path to a file on disk, or via a readable stream
+containing the file contents.
+
+To upload a file from a path on disk, call the
+[`folder.upload(file_path, file_name=None, preflight_check=False, preflight_expected_size=0)`][upload] method
+on the [`Folder`][folder_class] you want to upload the file into.  By default, the file uploaded to Box will have the
+same file name as the one on disk; you can override this by passing a different name in the `file_name` parameter.
+This method returns a [`File`][file_class] object representing the newly-uploaded file.
+
+```python
+folder_id = '22222'
+new_file = client.folder(folder_id).upload('/home/me/document.pdf')
+print('File "{0}" uploaded to Box with file ID {1}'.format(new_file.name, new_file.id))
+```
+
+To upload a file from a readable stream, call
+[`folder.upload_stream(file_stream, file_name, preflight_check=False, preflight_expected_size=0)`][upload_stream]
+with the stream and a name for the file.  This method returns a [`File`][file_class] object representing the
+newly-uploaded file.
+
+```python
+file_name = 'file.pdf'
+stream = open('/path/to/file.pdf', 'rb')
+
+folder_id = '22222'
+new_file = client.folder(folder_id).uplaod_stream(stream, file_name)
+print('File "{0}" uploaded to Box with file ID {1}'.format(new_file.name, new_file.id))
+```
+
+[folder_class]: https://box-python-sdk.readthedocs.io/en/latest/boxsdk.object.html#boxsdk.object.folder.Folder
+[upload]: https://box-python-sdk.readthedocs.io/en/latest/boxsdk.object.html#boxsdk.object.folder.Folder.upload
+[upload_stream]: https://box-python-sdk.readthedocs.io/en/latest/boxsdk.object.html#boxsdk.object.folder.Folder.upload_stream
+
+Chunked Upload
+--------------
+
+For large files or in cases where the network connection is less reliable,
+you may want to upload the file in parts.  This allows a single part to fail
+without aborting the entire upload, and failed parts can then be retried.
+
+### Manual Process
+
+For more complicated upload scenarios, such as those being coordinated across multiple processes or when an unrecoverable error occurs with the automatic uploader, the endpoints for chunked upload operations are also exposed directly.
+
+For example, this is roughly how a chunked upload is done manually:
+
+```python
+import hashlib
+import os
+
+
+test_file_path = '/path/to/large_file.mp4'
+total_size = os.stat(test_file_path).st_size
+sha1 = hashlib.sha1()
+content_stream = open(test_file_path, 'rb')
+upload_session = client.folder(folder_id='11111').create_upload_session(file_size=total_size, file_name='test_file_name.mp4')
+part_array = []
+
+for part_num in range(upload_session.total_parts):
+
+    copied_length = 0
+    chunk = b''
+    while copied_length < upload_session.part_size:
+        bytes_read = content_stream.read(upload_session.part_size - copied_length)
+        if bytes_read is None:
+            # stream returns none when no bytes are ready currently but there are 
+            # potentially more bytes in the stream to be read.
+            continue
+        if len(bytes_read) == 0:
+            # stream is exhausted.
+            break
+        chunk += bytes_read
+        copied_length += len(bytes_read)
+
+    uploaded_part = upload_session.upload_part_bytes(chunk, part_num*upload_session.part_size, total_size)
+    part_array.append(uploaded_part)
+    updated_sha1 = sha1.update(chunk)
+content_sha1 = sha1.digest()
+uploaded_file = upload_session.commit(content_sha1=content_sha1, parts=part_array)
+print('File ID: {0} and File Name: {1}'.format(uploaded_file.id, uploaded_file.name))
+```
+
+The individual endpoint methods are detailed below:
+
+#### Create Upload Session for File Version
+
+To create an upload session for uploading a large version, call
+[`file.create_upload_session(file_size, file_name=None)`][create_version_upload_session] with the size of the file to be
+uploaded.  You can optionally specify a new `file_name` to rename the file on upload.  This method returns an
+[`UploadSession`][upload_session_class] object representing the created upload session.
+
+```python
+file_size = 26000000
+upload_session = client.file('11111').create_upload_session(file_size)
+print('Created upload session {0} with chunk size of {1} bytes'.format(upload_session.id, uplaod_session.part_size))
+```
+
+[create_version_upload_session]: https://box-python-sdk.readthedocs.io/en/latest/boxsdk.object.html#boxsdk.object.file.File.create_upload_session
+[upload_session_class]: https://box-python-sdk.readthedocs.io/en/latest/boxsdk.object.html#boxsdk.object.upload_session.UploadSession
+
+#### Create Upload Session for File
+
+To create an upload session for uploading a new large file, call
+[`folder.create_upload_session(file_size, file_name)`][create_upload_session] with the size and filename of the file
+to be uploaded.  This method returns an [`UploadSession`][upload_session_class] object representing the created upload
+session.
+
+```python
+file_size = 26000000
+file_name = 'test_file.pdf'
+upload_session = client.folder('22222').create_upload_session(file_size, file_name)
+print('Created upload session {0} with chunk size of {1} bytes'.format(upload_session.id, uplaod_session.part_size))
+```
+
+[create_upload_session]: https://box-python-sdk.readthedocs.io/en/latest/boxsdk.object.html#boxsdk.object.folder.Folder.create_upload_session
+
+#### Upload Part
+
+To upload a part of the file to this session, call
+[`upload_session.upload_part_bytes(part_bytes, offset, total_size, part_content_sha1=None)`][upload_part_bytes] with
+the `bytes` to be uploaded, the byte offset within the file (which should be a multiple of the upload session
+`part_size`), and the total size of the file being uploaded.  This method returns a `dict` for the part record; these
+records should be kept for the commit operation.
+
+> __Note:__ The number of bytes uploaded for each part must be exactly `upload_sesion.part_size`, except for the last
+> part (which just includes however many bytes are left in the file).
+
+```python
+upload_session = client.upload_session('11493C07ED3EABB6E59874D3A1EF3581')
+offset = upload_session.part_size * 3
+total_size = 26000000
+part_bytes = b'abcdefgh'
+part = upload_session.upload_part_bytes(part_bytes, offset, total_size)
+print('Successfully uploaded part ID {0}'.format(part['part_id']))
+```
+
+[upload_part_bytes]: https://box-python-sdk.readthedocs.io/en/latest/boxsdk.object.html#boxsdk.object.upload_session.UploadSession.upload_part_bytes
+
+#### Commit Upload Session
+
+After uploading all parts of the file, commit the upload session to Box by calling
+[`upload_session.commit(content_sha1, parts=None, file_attributes=None, etag=None)`][commit] with the SHA1 hash of the
+entire file.  For best consistency guarantees, you should also pass an `Iterable` of the parts `dict`s via the `parts`
+parameter; otherwise, the list of parts will be retrieved from the API.  You may also pass a `dict` of `file_attributes`
+to set on the new file.
+
+```python
+import hashlib
+
+sha1 = hashlib.sha1()
+# sha1 should have been updated with all the bytes of the file
+
+file_atributes = {
+    'description': 'A file uploaded via Chunked Upload',
+}
+
+upload_session = client.upload_session('11493C07ED3EABB6E59874D3A1EF3581')
+uploaded_file = upload_session.commit(sha1.digest(), file_atributes=file_atributes)
+print('Successfully uploaded file {0} with description {1}'.format(uplaoded_file.id, uploaded_file.description))
+```
+
+[commit]: https://box-python-sdk.readthedocs.io/en/latest/boxsdk.object.html#boxsdk.object.upload_session.UploadSession.commit
+
+#### Abort Upload Session
+
+To abort a chunked upload and lose all uploaded file parts, call [`upload_session.abort()`][abort].  This method returns
+`True` to indicate that the deletion succeeded.
+
+```python
+client.upload_session('11493C07ED3EABB6E59874D3A1EF3581').abort()
+print('Upload was successfully canceled')
+```
+
+[abort]: https://box-python-sdk.readthedocs.io/en/latest/boxsdk.object.html#boxsdk.object.upload_session.UploadSession.abort
+
+#### List Upload Parts
+
+To return the list of parts uploaded so far, call [`upload_session.get_parts(limit=None, offset=None)`][get_parts].
+This method returns a `BoxObjectCollection` that allows you to iterate over the part `dict`s in the collection.
+
+```python
+parts = client.upload_session('11493C07ED3EABB6E59874D3A1EF3581').get_parts()
+for part in parts:
+    print('Part {0} at offset {1} has already been uploaded'.format(part['part_id'], part['offset']))
+```
+
+[get_parts]: https://box-python-sdk.readthedocs.io/en/latest/boxsdk.object.html#boxsdk.object.upload_session.UploadSession.get_parts
+
+Move a File
+-----------
+
+To move a file from one folder into another, call [`file.move(parent_folder, name=None)`][move] with the destination
+folder to move the file into.  You can optionally provide a `name` parameter to automatically rename the file in case
+of a name conflict in the destination folder.  This method returns the updated [`File`][file_class] object in the new
+folder.
+
+```python
+file_id = '11111'
+destination_folder_id = '44444'
+
+file_to_move = client.file(file_id)
+destination_folder = client.folder(destination_folder_id)
+
+moved_file = file_to_move.move(destination_folder)
+print('File "{0}" has been moved into folder "{1}"'.format(moved_file.name, moved_file.parent.name))
+```
+
+[move]: https://box-python-sdk.readthedocs.io/en/latest/boxsdk.object.html#boxsdk.object.item.Item.move
+
+Copy a File
+-----------
+
+A file can be copied to a new folder by calling [`file.copy(parent_folder, name=None)`][copy] with the destination
+folder and an optional new name for the file in case there is a name conflict in the destination folder.  This method
+returns a [`File`][file_class] object representing the copy of the file in the destination folder.
+
+```python
+file_id = '11111'
+destination_folder_id = '44444'
+
+file_to_copy = client.file(file_id)
+destination_folder = client.folder(destination_folder_id)
+
+file_copy = file_to_copy.copy(destination_folder)
+print('File "{0}" has been copied into folder "{1}"'.format(file_copy.name, file_copy.parent.name))
+```
+
+[copy]: https://box-python-sdk.readthedocs.io/en/latest/boxsdk.object.html#boxsdk.object.item.Item.copy
+
+Delete a File
+-------------
+
+Calling the [`file.delete()`][delete] method will delete the file.  Depending on enterprise settings, this will either move
+the file to the user's trash or permanently delete the file.  This method returns `True` to signify that the deletion
+was successful.
+
+```python
+client.file(file_id='11111').delete()
+```
+
+[delete]: https://box-python-sdk.readthedocs.io/en/latest/boxsdk.object.html#boxsdk.object.item.Item.delete
+
+Get Previous Versions of a File
+-------------------------------
+
+Previous versions of a file can be retrieved with the
+[`file.get_previous_versions(limit=None, offset=None, fields=None)`][get_previous_versions] method.  This method returns
+a [`BoxObjectCollection`][box_object_collection] that can iterate over the [`FileVersion`][file_version_class] objects
+in the collection.
+
+```python
+file_id = '11111'
+
+file_versions = client.file(file_id).get_previous_versions()
+for version in file_versions:
+    print('File version {0} was created at {1}'.format(version.id, version.created_at))
+```
+
+[get_previous_versions]: https://box-python-sdk.readthedocs.io/en/latest/boxsdk.object.html#boxsdk.object.file.File.get_previous_versions
+[box_object_collection]: https://box-python-sdk.readthedocs.io/en/latest/boxsdk.pagination.html#boxsdk.pagination.box_object_collection.BoxObjectCollection
+
+Upload a New Version of a File
+------------------------------
+
+New versions of a file can be uploaded in one of two ways: by providing a path to a file on disk, or via a readable
+stream containing the file contents.
+
+To upload a new file version from a path on disk, call the
+[`file.update_contents(file_path, etag=None, preflight_check=False, preflight_expected_size=0)`][update_contents]
+method.  This method returns a [`File`][file_class] object representing the updated file.
+
+```python
+file_id = '11111'
+file_path = '/path/to/file.pdf'
+
+updated_file = client.file(file_id).update_contents(file_path)
+print('File "{0}" has been updated'.format(updated_file.name))
+```
+
+To upload a file version from a readable stream, call
+[`file.update_contents_with_stream(file_stream, etag=None, preflight_check=False, preflight_expected_size=0)`][update_contents_with_stream]
+with the stream.  This method returns a [`File`][file_class] object representing the
+newly-uploaded file.
+
+```python
+file_id = '11111'
+stream = open('/path/to/file.pdf', 'rb')
+
+updated_file = client.file(file_id).update_contents_with_stream(stream)
+print('File "{0}" has been updated'.format(updated_file.name))
+```
+
+[update_contents]: https://box-python-sdk.readthedocs.io/en/latest/boxsdk.object.html#boxsdk.object.file.File.update_contents
+[update_contents_with_stream]: https://box-python-sdk.readthedocs.io/en/latest/boxsdk.object.html#boxsdk.object.file.File.update_contents_with_stream
+
+Promote a Previous Version of a File
+------------------------------------
+
+A previous version of a file can be promoted by calling the [`file.promote_version(file_version)`][promote_version]
+method to become the current version of the file with the [`FileVersion`][file_version_class] to promote.  This create a
+copy of the old file version and puts it on the top of the versions stack.  This method returns the new copy
+[`FileVersion`][file_version_class] object.
+
+```python
+file_id = '11111'
+file_version_id = '12345'
+
+version_to_promote = client.file_version(file_version_id)
+
+new_version = client.file(file_id).promote_version(version_to_promote)
+print('Version {0} promoted; new version {1} created'.format(file_version_id, new_version.id))
+```
+
+[promote_version]: https://box-python-sdk.readthedocs.io/en/latest/boxsdk.object.html#boxsdk.object.file.File.promote_version
+
+Delete a Previous Version of a File
+-----------------------------------
+
+A version of a file can be deleted and moved to the trash by calling
+[`file.delete_version(file_version, etag=None)`][delete_version] with the [`FileVersion`] to delete.
+
+```python
+file_id = '11111'
+version_id = '12345'
+
+version_to_delete = client.file_version(version_id)
+client.file(file_id).delete_version(version_to_delete)
+```
+
+[delete_version]: https://box-python-sdk.readthedocs.io/en/latest/boxsdk.object.html#boxsdk.object.file.File.delete_version
+
+Lock a File
+-----------
+
+A locked file cannot be modified by any other user until it is unlocked.  This is useful if you want to "check out" a
+file while you're working on it, to ensure that other collaborators do not make changes while your changes are in
+progress.
+
+To lock a file, call [`file.lock(prevent_download=False, expire_time=None)`][lock].  You can optionally prevent other
+users from downloading the file while it is locked by passing `True` for the `prevent_download` parameter.  You can also
+set an expiration time for the lock, which will automatically release the lock at the specified time.  The expiration
+time is formatted as an [RFC3339 datetime][rfc3339].
+
+This method returns the updated [`File`][file_class] object.
+
+```python
+file_id = '11111'
+
+updated_file = client.file(file_id).lock(expiration_time='2020-01-01T00:00:00-08:00')
+print('File "{0}" has been locked!'.format(updated_file.name))
+```
+
+[lock]: https://box-python-sdk.readthedocs.io/en/latest/boxsdk.object.html#boxsdk.object.file.File.lock
+[rfc3339]: https://tools.ietf.org/html/rfc3339#section-5.8
+
+Unlock a File
+-------------
+
+A locked file can be unlocked by calling [`file.unlock()`][unlock].  This method returns the updated
+[`File`][file_class] object.
+
+```python
+file_id = '11111'
+
+updated_file = client.file(file_id).unlock()
+print('File "{0}" has been unlocked!'.format(updated_file.name))
+```
+
+[unlock]: https://box-python-sdk.readthedocs.io/en/latest/boxsdk.object.html#boxsdk.object.file.File.unlock
+
+Create a Shared Link
+--------------------
+
+A shared link for a file can be generated by calling
+[`file.get_shared_link(access=None, etag=None, unshared_at=None, allow_download=None, allow_preview=None, password=None)`][get_shared_link].
+This method returns a `unicode` string containing the shared link URL.
+
+```python
+file_id = '11111'
+
+url = client.file(file_id).get_shared_link()
+print('The file shared link URL is: {0}'.format(url))
+```
+
+[get_shared_link]: https://box-python-sdk.readthedocs.io/en/latest/boxsdk.object.html#boxsdk.object.item.Item.get_shared_link
+
+Get an Embed Link
+-----------------
+
+A file embed URL can be generated by calling [`file.get_embed_url()`][get_embed_url].  This method returns a `unicode`
+string containing a URL suitable for embedding in an `<iframe>` to embed the a file viewer in a web page.
+
+```python
+file_id = '11111'
+
+embed_url = client.file(file_id).get_embed_url()
+print('<iframe src="{0}"></iframe>'.format(embed_url))
+```
+
+[get_embed_url]: https://box-python-sdk.readthedocs.io/en/latest/boxsdk.object.html#boxsdk.object.file.File.get_embed_url
+
+Get File Representations
+------------------------
+
+To get the preview representations of a file, call the
+[`file.get_representation_info(rep_hints=None)`][get_representation_info] method with the
+[representation hints][rep_hints] to fetch — if no hints are provided, limited information about all available
+representations will be returned.  This method returns a `list` of `dict`s containing the information about the
+requested [representations][rep_api_obj].
+
+Note that this method only provides information about a set of available representations; your
+application will need to handle checking the status of the representations and downlaoding them
+via the provided content URL template.
+
+```python
+file_id = '11111'
+rep_hints = '[pdf][extracted_text]'
+
+representations = client.file(file_id).get_representation_info(rep_hints)
+for rep in representations:
+    print('{0} representation has status {1}'.format(rep['representation'], rep['status']['state']))
+    print('Info URL for this representation is: {0}'.format(rep['info']['url']))
+    print('Content URL template is: {0}'.format(rep['content']['url_template']))
+```
+
+[get_representation_info]: https://box-python-sdk.readthedocs.io/en/latest/boxsdk.object.html#boxsdk.object.file.File.get_representation_info
+[rep_hints]: https://developer.box.com/v2.0/reference/#section-x-rep-hints-header
+[rep_api_obj]: https://developer.box.com/v2.0/reference/#representations
+
+Get Thumbnail
+-------------
+
+A thumbnail for a file can be retrieved by calling
+[`file.get_thumbnail(extension='png', min_width=None, min_height=None, max_width=None, max_height=None)`][get_thumbnail].
+This method returns the `bytes` of the thumbnail image.
+
+```python
+file_id = '11111'
+
+thumbnail = client.file(file_id).get_thumbnail(extension='jpg')
+```
+
+[get_thumbnail]: https://box-python-sdk.readthedocs.io/en/latest/boxsdk.object.html#boxsdk.object.file.File.get_thumbnail
+
+Add Metadata
+------------
+
+Metadata can be added to a file either as free-form key/value pairs or from an existing template.  To add metadata to
+a file, first call [`file.metadata(scope='global', template='properties')`][metadata] to specify the scope and
+template key of the metadata template to attach (or use the default values to attach free-form keys and values).  Then,
+call [`metadata.create(data)`][metadata_create] with the key/value pairs to attach.  This method can only be used to
+attach a given metadata template to the file for the first time, and returns a `dict` containing the applied metadata
+instance.
+
+```python
+metadata = {
+    'foo': 'bar',
+    'baz': 'quux',
+}
+
+applied_metadata = client.file(file_id='11111').metadata().create(metadata)
+print('Applied metadata in instance ID {0}'.format(applied_metadata['$id']))
+```
+
+[metadata]: https://box-python-sdk.readthedocs.io/en/latest/boxsdk.object.html#boxsdk.object.item.Item.metadata
+[metadata_create]: https://box-python-sdk.readthedocs.io/en/latest/boxsdk.object.html#boxsdk.object.metadata.Metadata.create
+
+Get Metadata
+------------
+
+To retrieve the metadata instance on a file for a specific metadata template, first call
+[`file.metadata(scope='global', template='properties')`][metadata] to specify the scope and template key of the
+metadata template to retrieve, then call [`metadata.get()`][metadata_get] to retrieve the metadata values attached to
+the file.  This method returns a `dict` containing the applied metadata instance.
+
+```python
+metadata = client.file(file_id='11111').metadata(scope='enterprise', template='myMetadata').get()
+print('Got metadata instance {0}'.format(metadata['$id']))
+```
+
+[metadata_get]: https://box-python-sdk.readthedocs.io/en/latest/boxsdk.object.html#boxsdk.object.metadata.Metadata.get
+
+Update Metadata Values
+----------------------
+
+Updating metadata values is performed via a series of discrete operations, which are applied atomically against the
+existing file metadata.  First, specify which metadata will be updated by calling
+[`file.metadata(scope='global', template='properties')`][metadata].  Then, start an update sequence by calling
+[`metadata.start_update()`][metadata_start_update] and add update operations to the returned
+[`MetadataUpdate`][metadata_update_obj].  Finally, perform the update by calling
+[`metadata.update(metadata_update)`][metadata_update].  This final method returns a `dict` of the updated metadata
+instance.
+
+```python
+file_obj = client.file(file_id='11111')
+file_metadata = file_obj.metadata(scope='enterprise', template='myMetadata')
+
+updates = file_metadata.start_update()
+updates.add('/foo', 'bar')
+updates.update('/baz', 'murp', old_value='quux')  # Ensure the old value was "quux" before updating to "murp"
+
+updated_metadata = file_metadata.update(updates)
+print('Updated metadata on file!')
+print('foo is now {0} and baz is now {1}'.format(updated_metadata['foo'], updated_metadata['baz']))
+```
+
+[metadata_start_update]: https://box-python-sdk.readthedocs.io/en/latest/boxsdk.object.html#boxsdk.object.metadata.Metadata.start_update
+[metadata_update_obj]: https://box-python-sdk.readthedocs.io/en/latest/boxsdk.object.html#boxsdk.object.metadata.MetadataUpdate
+[metadata_update]: https://box-python-sdk.readthedocs.io/en/latest/boxsdk.object.html#boxsdk.object.metadata.Metadata.update
+
+Remove Metadata
+---------------
+
+To remove a metadata instance from a file, call
+[`file.metadata(scope='global', template='properties')`][metadata] to specify the scope and template key of the
+metadata template to remove, then call [`metadata.delete()`][metadata_delete] to remove the metadata from the file.
+This method returns `True` to indicate that the removal succeeded.
+
+```python
+client.file(file_id='11111').metadata(scope='enterprise', template='myMetadata').delete()
+```
+
+[metadata_delete]: https://box-python-sdk.readthedocs.io/en/latest/boxsdk.object.html#boxsdk.object.metadata.Metadata.delete
+
+Get All Metadata
+----------------
+
+To retrieve all metadata attached to a file, call [`file.get_all_metadata()`][get_all_metadata].  This method returns a
+[`BoxObjectCollection`][box_object_collection] that can be used to iterate over the `dict`s representing each metadata instance attached to the
+file.
+
+```python
+file_metadata = client.file(file_id='11111').get_all_metadata()
+for instance in file_metadata:
+    if 'foo' in instance:
+        print('Metadata instance {0} has value "{1}" for foo'.format(instance['id'], instance['foo']))
+```
+
+[get_all_metadata]: https://box-python-sdk.readthedocs.io/en/latest/boxsdk.object.html#boxsdk.object.item.Item.get_all_metadata


Test patch:
diff --git a/test/unit/client/test_client.py b/test/unit/client/test_client.py
index 1312885..4749c91 100644
--- a/test/unit/client/test_client.py
+++ b/test/unit/client/test_client.py
@@ -30,6 +30,7 @@
 from boxsdk.object.events import Events
 from boxsdk.object.folder import Folder
 from boxsdk.object.file import File
+from boxsdk.object.file_version import FileVersion
 from boxsdk.object.group import Group
 from boxsdk.object.invite import Invite
 from boxsdk.object.storage_policy import StoragePolicy
@@ -391,6 +392,7 @@ def device_pins_response(device_pin_id_1, device_pin_id_2):
     (Enterprise, 'enterprise'),
     (Folder, 'folder'),
     (File, 'file'),
+    (FileVersion, 'file_version'),
     (Invite, 'invite'),
     (User, 'user'),
     (Group, 'group'),
diff --git a/test/unit/object/test_file.py b/test/unit/object/test_file.py
index 969700f..bdb30ca 100644
--- a/test/unit/object/test_file.py
+++ b/test/unit/object/test_file.py
@@ -9,6 +9,7 @@
 from boxsdk.exception import BoxAPIException
 from boxsdk.object.comment import Comment
 from boxsdk.object.file import File
+from boxsdk.object.file_version import FileVersion
 from boxsdk.object.task import Task
 from boxsdk.object.upload_session import UploadSession
 
@@ -32,6 +33,11 @@ def mock_accelerator_response_for_update(make_mock_box_request, mock_accelerator
     return mock_response
 
 
+@pytest.fixture()
+def test_file_version(mock_box_session):
+    return FileVersion(mock_box_session, 'file_version_id')
+
+
 def test_delete_file(test_file, mock_box_session, etag, if_match_header):
     test_file.delete(etag=etag)
     expected_url = test_file.get_url()
@@ -131,22 +137,103 @@ def test_get_tasks(test_file, mock_box_session):
     assert task.item['id'] == task_body['item']['id']
 
 
-def test_download_to(test_file, mock_box_session, mock_content_response):
-    expected_url = test_file.get_url('content')
+def test_get_download_url(test_file, mock_box_session):
+    expected_url = '{0}/files/{1}/content'.format(API.BASE_API_URL, test_file.object_id)
+    download_url = 'https://dl.boxcloud.com/sdjhfgksdjfgshdbg'
+    mock_box_session.get.return_value.headers = {
+        'location': download_url
+    }
+    url = test_file.get_download_url()
+    mock_box_session.get.assert_called_once_with(
+        expected_url,
+        params=None,
+        expect_json_response=False,
+        allow_redirects=False
+    )
+    assert url == download_url
+
+
+def test_get_download_url_file_version(test_file, test_file_version, mock_box_session):
+    expected_url = '{0}/files/{1}/content'.format(API.BASE_API_URL, test_file.object_id)
+    download_url = 'https://dl.boxcloud.com/sdjhfgksdjfgshdbg'
+    mock_box_session.get.return_value.headers = {
+        'location': download_url
+    }
+    url = test_file.get_download_url(file_version=test_file_version)
+    mock_box_session.get.assert_called_once_with(
+        expected_url,
+        params={'version': test_file_version.object_id},
+        expect_json_response=False,
+        allow_redirects=False
+    )
+    assert url == download_url
+
+
+@pytest.mark.parametrize('params,expected_query,expected_headers', [
+    ({}, None, None),
+    ({'byte_range': (100, 199)}, None, {'Range': 'bytes=100-199'}),
+    ({'byte_range': (100,)}, None, {'Range': 'bytes=100-'}),
+])
+def test_download_to(test_file, mock_box_session, mock_content_response, params, expected_query, expected_headers):
+    expected_url = '{0}/files/{1}/content'.format(API.BASE_API_URL, test_file.object_id)
+    mock_box_session.get.return_value = mock_content_response
+    mock_writeable_stream = BytesIO()
+    test_file.download_to(mock_writeable_stream, **params)
+    mock_writeable_stream.seek(0)
+    assert mock_writeable_stream.read() == mock_content_response.content
+    mock_box_session.get.assert_called_once_with(
+        expected_url,
+        expect_json_response=False,
+        stream=True,
+        params=expected_query,
+        headers=expected_headers
+    )
+
+
+def test_download_to_file_version(test_file, test_file_version, mock_box_session, mock_content_response):
+    expected_url = '{0}/files/{1}/content'.format(API.BASE_API_URL, test_file.object_id)
     mock_box_session.get.return_value = mock_content_response
     mock_writeable_stream = BytesIO()
-    test_file.download_to(mock_writeable_stream)
+    test_file.download_to(mock_writeable_stream, file_version=test_file_version)
     mock_writeable_stream.seek(0)
     assert mock_writeable_stream.read() == mock_content_response.content
-    mock_box_session.get.assert_called_once_with(expected_url, expect_json_response=False, stream=True)
+    mock_box_session.get.assert_called_once_with(
+        expected_url,
+        expect_json_response=False,
+        stream=True,
+        headers=None,
+        params={'version': test_file_version.object_id}
+    )
 
 
-def test_get_content(test_file, mock_box_session, mock_content_response):
+@pytest.mark.parametrize('params,expected_query,expected_headers', [
+    ({}, None, None),
+    ({'byte_range': (100, 199)}, None, {'Range': 'bytes=100-199'}),
+])
+def test_get_content(test_file, mock_box_session, mock_content_response, params, expected_query, expected_headers):
     expected_url = test_file.get_url('content')
     mock_box_session.get.return_value = mock_content_response
-    file_content = test_file.content()
+    file_content = test_file.content(**params)
     assert file_content == mock_content_response.content
-    mock_box_session.get.assert_called_once_with(expected_url, expect_json_response=False)
+    mock_box_session.get.assert_called_once_with(
+        expected_url,
+        expect_json_response=False,
+        params=expected_query,
+        headers=expected_headers
+    )
+
+
+def test_get_content_file_version(test_file, mock_box_session, mock_content_response, test_file_version):
+    expected_url = test_file.get_url('content')
+    mock_box_session.get.return_value = mock_content_response
+    file_content = test_file.content(file_version=test_file_version)
+    assert file_content == mock_content_response.content
+    mock_box_session.get.assert_called_once_with(
+        expected_url,
+        expect_json_response=False,
+        params={'version': test_file_version.object_id},
+        headers=None
+    )
 
 
 @pytest.mark.parametrize('is_stream', (True, False))
@@ -266,14 +353,28 @@ def test_update_contents_does_preflight_check_if_specified(
             assert not test_file.preflight_check.called
 
 
-@pytest.mark.parametrize('prevent_download', (True, False))
-def test_lock(test_file, mock_box_session, mock_file_response, prevent_download):
+@pytest.mark.parametrize('params,expected_data', [
+    ({}, {'is_download_prevented': False}),
+    ({'prevent_download': False}, {'is_download_prevented': False}),
+    ({'prevent_download': True}, {'is_download_prevented': True}),
+    ({'expire_time': '2018-11-06T19:40:00-08:00'}, {
+        'is_download_prevented': False,
+        'expires_at': '2018-11-06T19:40:00-08:00',
+    }),
+])
+def test_lock(test_file, mock_box_session, mock_file_response, params, expected_data):
     expected_url = test_file.get_url()
+    expected_body = {
+        'lock': {
+            'type': 'lock',
+        }
+    }
+    expected_body['lock'].update(expected_data)
     mock_box_session.put.return_value = mock_file_response
-    test_file.lock(prevent_download)
+    test_file.lock(**params)
     mock_box_session.put.assert_called_once_with(
         expected_url,
-        data=json.dumps({'lock': {'is_download_prevented': prevent_download, 'type': 'lock'}}),
+        data=json.dumps(expected_body),
         params=None,
         headers=None,
     )
@@ -420,3 +521,149 @@ def test_add_comment(test_file, mock_box_session, comment_params):
     mock_box_session.post.assert_called_once_with(expected_url, data=json.dumps(expected_data))
     assert isinstance(comment, Comment)
     assert comment.object_id == comment_id
+
+
+def test_get_previous_versions(test_file, mock_box_session):
+    expected_url = '{0}/files/{1}/versions'.format(API.BASE_API_URL, test_file.object_id)
+    mock_version1 = {
+        'type': 'file_version',
+        'id': '11111',
+        'sha1': '4788db35f85f87acaaa5ba82cc99d72c9323281f',
+    }
+    mock_version2 = {
+        'type': 'comment',
+        'id': '22222',
+        'sha1': '4788db35f85f87acaaa5ba82cc99d72c9323281f',
+    }
+    mock_box_session.get.return_value.json.return_value = {
+        'total_count': 2,
+        'offset': 0,
+        'limit': 100,
+        'entries': [mock_version1, mock_version2]
+    }
+    versions = test_file.get_previous_versions()
+    version1 = versions.next()
+    mock_box_session.get.assert_called_once_with(expected_url, params={'offset': None})
+    assert version1.object_id == mock_version1['id']
+    assert version1.sha1 == mock_version1['sha1']
+
+    version2 = versions.next()
+    assert version2.object_id == mock_version2['id']
+    assert version2.sha1 == mock_version2['sha1']
+
+
+def test_promote_version(test_file, test_file_version, mock_box_session):
+    expected_url = '{0}/files/{1}/versions/current'.format(API.BASE_API_URL, test_file.object_id)
+    sha1 = '12039d6dd9a7e6eefc78846802e'
+    expected_body = {
+        'type': 'file_version',
+        'id': test_file_version.object_id,
+    }
+    mock_box_session.post.return_value.json.return_value = {
+        'type': 'file_version',
+        'id': '77777',
+        'sha1': sha1,
+    }
+    new_version = test_file.promote_version(test_file_version)
+    mock_box_session.post.assert_called_once_with(expected_url, data=json.dumps(expected_body))
+    assert isinstance(new_version, FileVersion)
+    assert new_version.object_id == '77777'
+    assert new_version.sha1 == sha1
+
+
+@pytest.mark.parametrize('params,expected_headers', [
+    ({}, None),
+    ({'etag': 'foobar'}, {'If-Match': 'foobar'}),
+])
+def test_delete_version(test_file, test_file_version, mock_box_session, params, expected_headers):
+    expected_url = '{0}/files/{1}/versions/{2}'.format(API.BASE_API_URL, test_file.object_id, test_file_version.object_id)
+    mock_box_session.delete.return_value.ok = True
+    is_success = test_file.delete_version(test_file_version, **params)
+    mock_box_session.delete.assert_called_once_with(expected_url, expect_json_response=False, headers=expected_headers)
+    assert is_success is True
+
+
+def test_get_embed_url(test_file, mock_box_session):
+    expected_url = '{0}/files/{1}'.format(API.BASE_API_URL, test_file.object_id)
+    expected_params = {
+        'fields': 'expiring_embed_link'
+    }
+    embed_url = 'https://app.box.com/preview/mystuff'
+    mock_box_session.get.return_value.json.return_value = {
+        'type': 'file',
+        'id': test_file.object_id,
+        'expiring_embed_link': {
+            'url': embed_url,
+        },
+    }
+
+    url = test_file.get_embed_url()
+    mock_box_session.get.assert_called_once_with(expected_url, params=expected_params)
+    assert url == embed_url
+
+
+@pytest.mark.parametrize('rep_hints,expected_headers', [
+    (None, None),
+    ('[pdf]', {'X-Rep-Hints': '[pdf]'}),
+])
+def test_get_representation_info(test_file, mock_box_session, rep_hints, expected_headers):
+    expected_url = '{0}/files/{1}'.format(API.BASE_API_URL, test_file.object_id)
+    expected_params = {'fields': 'representations'}
+
+    info_url = 'https://api.box.com/2.0/representations/pdf'
+    mock_box_session.get.return_value.json.return_value = {
+        'type': 'file',
+        'id': test_file.object_id,
+        'representations': {
+            'total_count': 1,
+            'entries': [
+                {
+                    'representation': 'pdf',
+                    'info': {
+                        'url': info_url,
+                    },
+                },
+            ],
+        },
+    }
+
+    reps = test_file.get_representation_info(rep_hints=rep_hints)
+    mock_box_session.get.assert_called_once_with(expected_url, params=expected_params, headers=expected_headers)
+    assert isinstance(reps, list)
+    assert len(reps) == 1
+    rep = reps[0]
+    assert rep['representation'] == 'pdf'
+    assert rep['info']['url'] == info_url
+
+
+@pytest.mark.parametrize('extension,min_width,min_height,max_width,max_height,expected_params', [
+    ('png', None, None, None, None, {}),
+    ('png', None, None, None, None, {}),
+    ('jpg', None, None, None, None, {}),
+    ('png', 1, 2, None, None, {'min_width': 1, 'min_height': 2}),
+    ('png', 1, 2, 3, 4, {'min_width': 1, 'min_height': 2, 'max_width': 3, 'max_height': 4}),
+])
+def test_get_thumbnail(
+        test_file,
+        mock_box_session,
+        mock_content_response,
+        extension,
+        min_width,
+        min_height,
+        max_width,
+        max_height,
+        expected_params,
+):
+    expected_url = '{0}/files/{1}/thumbnail.{2}'.format(API.BASE_API_URL, test_file.object_id, extension)
+    mock_box_session.get.return_value = mock_content_response
+
+    thumb = test_file.get_thumbnail(
+        extension=extension,
+        min_width=min_width,
+        min_height=min_height,
+        max_width=max_width,
+        max_height=max_height,
+    )
+
+    mock_box_session.get.assert_called_once_with(expected_url, expect_json_response=False, params=expected_params)
+    assert thumb == mock_content_response.content
diff --git a/test/unit/object/test_item.py b/test/unit/object/test_item.py
index 8d3163c..2eb1dfc 100644
--- a/test/unit/object/test_item.py
+++ b/test/unit/object/test_item.py
@@ -85,23 +85,39 @@ def test_rename_item(test_item_and_response, mock_box_session):
     assert isinstance(rename_response, test_item.__class__)
 
 
-def test_copy_item(test_item_and_response, mock_box_session, test_folder, mock_object_id):
+@pytest.mark.parametrize('params, expected_data', [
+    ({}, {}),
+    ({'name': 'New name.pdf'}, {'name': 'New name.pdf'})
+])
+def test_copy_item(test_item_and_response, mock_box_session, test_folder, mock_object_id, params, expected_data):
     # pylint:disable=redefined-outer-name, protected-access
     test_item, mock_item_response = test_item_and_response
     expected_url = test_item.get_url('copy')
+    expected_body = {
+        'parent': {'id': mock_object_id},
+    }
+    expected_body.update(expected_data)
     mock_box_session.post.return_value = mock_item_response
-    copy_response = test_item.copy(test_folder)
-    mock_box_session.post.assert_called_once_with(expected_url, data=json.dumps({'parent': {'id': mock_object_id}}))
+    copy_response = test_item.copy(test_folder, **params)
+    mock_box_session.post.assert_called_once_with(expected_url, data=json.dumps(expected_body))
     assert isinstance(copy_response, test_item.__class__)
 
 
-def test_move_item(test_item_and_response, mock_box_session, test_folder, mock_object_id):
+@pytest.mark.parametrize('params, expected_data', [
+    ({}, {}),
+    ({'name': 'New name.pdf'}, {'name': 'New name.pdf'})
+])
+def test_move_item(test_item_and_response, mock_box_session, test_folder, mock_object_id, params, expected_data):
     # pylint:disable=redefined-outer-name, protected-access
     test_item, mock_item_response = test_item_and_response
     expected_url = test_item.get_url()
+    expected_body = {
+        'parent': {'id': mock_object_id},
+    }
+    expected_body.update(expected_data)
     mock_box_session.put.return_value = mock_item_response
-    move_response = test_item.move(test_folder)
-    mock_box_session.put.assert_called_once_with(expected_url, data=json.dumps({'parent': {'id': mock_object_id}}), params=None, headers=None)
+    move_response = test_item.move(test_folder, **params)
+    mock_box_session.put.assert_called_once_with(expected_url, data=json.dumps(expected_body), params=None, headers=None)
     assert isinstance(move_response, test_item.__class__)
 
 
@@ -405,3 +421,32 @@ def test_collaborations(test_item_and_response, mock_box_session):
     assert collaboration.type == mock_collaboration['type']
     assert collaboration['created_by']['type'] == 'user'
     assert collaboration['created_by']['id'] == '33333'
+
+
+def test_get_all_metadata(test_item_and_response, mock_box_session):
+    test_item, _ = test_item_and_response
+    expected_url = '{0}/{1}s/{2}/metadata'.format(API.BASE_API_URL, test_item.object_type, test_item.object_id)
+    mock_metadata = {
+        'currentDocumentStage': 'prioritization',
+        'needsApprovalFrom': 'planning team',
+        '$type': 'documentFlow-452b4c9d-c3ad-4ac7-b1ad-9d5192f2fc5f',
+        '$parent': 'folder_998951261',
+        '$id': 'e57f90ff-0044-48c2-807d-06b908765baf',
+        '$version': 1,
+        '$typeVersion': 2,
+        'maximumDaysAllowedInCurrentStage': 5,
+        '$template': 'documentFlow',
+        '$scope': 'enterprise_12345',
+    }
+    mock_box_session.get.return_value.json.return_value = {
+        'limit': 100,
+        'entries': [mock_metadata]
+    }
+
+    all_metadata = test_item.get_all_metadata()
+    metadata = all_metadata.next()
+
+    mock_box_session.get.assert_called_once_with(expected_url, params={})
+    assert isinstance(metadata, dict)
+    for key in metadata:
+        assert mock_metadata[key] == mock_metadata[key]