The Metadata API enables applications to:

  • CRUD Metadata on a file
  • Search by Metadata value

Terminology:

  • Metadata properties are grouped into Templates (e.g. Contracts, Invoices)
  • Templates are identified by template key and scope
  • A metadata instance is attached to a file, and conforms to a template’s schema. A file can have multiple instances, but only one from each template.

Setting up our client, we want to subclass the DefaultNetwork to add illustrative logging to our output:

In [4]:
from boxsdk.network.default_network import DefaultNetwork
from pprint import pformat

class LoggingNetwork(DefaultNetwork):
    def request(self, method, url, access_token, **kwargs):
        print '\x1b[36m{} {} {}\x1b[0m'.format(method, url, pformat(kwargs))
        response = super(LoggingNetwork, self).request(
            method, url, access_token, **kwargs
        )
        if response.ok:
            print '\x1b[32m{}\x1b[0m'.format(response.content)
        else:
            print '\x1b[31m{}\n{}\n{}\x1b[0m'.format(
                response.status_code,
                response.headers,
                pformat(response.content),
            )
        return response

Fill in your client_id, client_secret, and developer token that you obtained from the developers site:

In [5]:
from boxsdk import OAuth2
from boxsdk import Client
oauth = OAuth2(client_id='YOUR_CLIENT_ID', client_secret='YOUR_CLIENT_SECRET', access_token='YOUR_DEVELOPER_TOKEN')
client = Client(oauth, LoggingNetwork())

I'll demonstrate the CRUD abilities of the Metadata API. Using the Python SDK makes the API dead simple to use.

First, let's assume our account already has a file with ID 28792944858 and a template named "mortgageContarct". We'll create a metadata instance of that type:

In [12]:
metadata = client.file(28792944858).metadata('enterprise', 'mortgageApplication')
metadata.create({'borrowerName':'Alice', 'amount':8888888, 'status':'Pending Approval'})
POST https://api.box.com/2.0/files/28792944858/metadata/enterprise/mortgageApplication {'data': '{"status": "Pending Approval", "amount": 8888888, "borrowerName": "Alice"}',
 'headers': {u'Authorization': u'Bearer bljFP0m8vTdsKK3gWY0pnI2ETkluAAhW',
             'Content-Type': 'application/json'}}
{"status":"Pending Approval","amount":8888888,"borrowerName":"Alice","$type":"mortgageApplication-a2387be8-1279-4249-853b-f4a7cb6e0a54","$parent":"file_28792944858","$id":"657378d4-0617-4a04-9067-7e625076a0c2"}
Out[12]:
{u'$id': u'657378d4-0617-4a04-9067-7e625076a0c2',
 u'$parent': u'file_28792944858',
 u'$type': u'mortgageApplication-a2387be8-1279-4249-853b-f4a7cb6e0a54',
 u'amount': 8888888,
 u'borrowerName': u'Alice',
 u'status': u'Pending Approval'}

The metadata instance has been created and returned to you.

Next, we'll demonstrate the Metadata Search API. The Metadata Search API allows clients to search for files that match certain metadata values.

For example, our user's enterprise has a Mortgage Application metadata template, and we've attached an instance of the Mortgage Application template to a file in the user's account. We can search for this file by a specific field, status. In this example, the template is identified by template_key and scope, and the value itself is identified by the field_key.

In [14]:
from boxsdk.object.search import Search
metadata_filters = Search.start_metadata_filters()
metadata_filter = Search.make_single_metadata_filter(template_key='mortgageApplication', scope='enterprise')
metadata_filter.add_value_based_filter(field_key='status', value='Pending Approval')
metadata_filters.add_filter(metadata_filter)
client.search('pdf', limit=1, offset=0, metadata_filters=metadata_filters)
GET https://api.box.com/2.0/search {'headers': {u'Authorization': u'Bearer bljFP0m8vTdsKK3gWY0pnI2ETkluAAhW'},
 'params': {u'limit': 1,
            u'mdfilters': '[{"templateKey": "mortgageApplication", "scope": "enterprise", "filters": {"status": "Pending Approval"}}]',
            u'offset': 0,
            u'query': 'pdf'}}
{"total_count":1,"entries":[{"type":"file","id":"28792944858","file_version":{"type":"file_version","id":"27449889040","sha1":"34a17c1adb83874c40b4e4bb228d5d8299055fc0"},"sequence_id":"1","etag":"1","sha1":"34a17c1adb83874c40b4e4bb228d5d8299055fc0","name":"Alice Mortgage Application.pdf","description":"","size":382898,"path_collection":{"total_count":2,"entries":[{"type":"folder","id":"0","sequence_id":null,"etag":null,"name":"All Files"},{"type":"folder","id":"3442170474","sequence_id":"0","etag":"0","name":"Box Dev 2015 Demo"}]},"created_at":"2015-04-14T22:11:54-07:00","modified_at":"2015-04-14T22:12:39-07:00","trashed_at":null,"purged_at":null,"content_created_at":"2015-02-20T20:51:40-08:00","content_modified_at":"2015-02-20T20:51:40-08:00","created_by":{"type":"user","id":"236030416","name":"Carol","login":"hieu+metadata-boxdev2015@box.com"},"modified_by":{"type":"user","id":"236030416","name":"Carol","login":"hieu+metadata-boxdev2015@box.com"},"owned_by":{"type":"user","id":"236030416","name":"Carol","login":"hieu+metadata-boxdev2015@box.com"},"shared_link":null,"parent":{"type":"folder","id":"3442170474","sequence_id":"0","etag":"0","name":"Box Dev 2015 Demo"},"item_status":"active"}],"limit":1,"offset":0}
Out[14]:
[<boxsdk.object.file.File at 0x10e2e88d0>]

Next, we'll update the metadata instance, testing that the value is still what we expect it to be:

In [15]:
from boxsdk.object.metadata import Metadata
update = Metadata.start_update()
update.update('/status', 'Approved', 'Pending Approval')
metadata.update(update)
PUT https://api.box.com/2.0/files/28792944858/metadata/enterprise/mortgageApplication {'data': '[{"path": "/status", "value": "Pending Approval", "op": "test"}, {"path": "/status", "value": "Approved", "op": "replace"}]',
 'headers': {u'Authorization': u'Bearer bljFP0m8vTdsKK3gWY0pnI2ETkluAAhW',
             'Content-Type': 'application/json-patch+json'}}
{"status":"Approved","amount":8888888,"borrowerName":"Alice","$type":"mortgageApplication-a2387be8-1279-4249-853b-f4a7cb6e0a54","$parent":"file_28792944858","$id":"657378d4-0617-4a04-9067-7e625076a0c2"}
Out[15]:
{u'$id': u'657378d4-0617-4a04-9067-7e625076a0c2',
 u'$parent': u'file_28792944858',
 u'$type': u'mortgageApplication-a2387be8-1279-4249-853b-f4a7cb6e0a54',
 u'amount': 8888888,
 u'borrowerName': u'Alice',
 u'status': u'Approved'}

Notice the PUT body is an array of 2 operations: test and replace. The PUT body conforms to the JSON Patch specification for updating JSON objects.

If we attempt to issue the same patch again, the operation will fail because the value is not what we expected it to be:

In [16]:
update = Metadata.start_update()
update.update('/status', 'Approved', 'Pending Approval')
metadata.update(update)
PUT https://api.box.com/2.0/files/28792944858/metadata/enterprise/mortgageApplication {'data': '[{"path": "/status", "value": "Pending Approval", "op": "test"}, {"path": "/status", "value": "Approved", "op": "replace"}]',
 'headers': {u'Authorization': u'Bearer bljFP0m8vTdsKK3gWY0pnI2ETkluAAhW',
             'Content-Type': 'application/json-patch+json'}}
409
{'content-length': '123', 'age': '1', 'server': 'ATS', 'connection': 'keep-alive', 'date': 'Wed, 22 Apr 2015 16:28:46 GMT', 'content-type': 'application/json'}
'{"message":"value differs from expectations","code":"failed_json_patch_application","request_id":"11962628355537cc3dd9baa"}'
---------------------------------------------------------------------------
BoxAPIException                           Traceback (most recent call last)
<ipython-input-16-0e3a050a8302> in <module>()
      1 update = Metadata.start_update()
      2 update.update('/status', 'Approved', 'Pending Approval')
----> 3 metadata.update(update)

/Users/hnguyen/Code/box-python-sdk-hieu/boxsdk/object/metadata.pyc in update(self, metadata_update)
    155             self.get_url(),
    156             data=json.dumps(metadata_update.ops),
--> 157             headers={b'Content-Type': b'application/json-patch+json'},
    158         ).json()
    159 

/Users/hnguyen/Code/box-python-sdk-hieu/boxsdk/session/box_session.pyc in put(self, url, **kwargs)
    347             `unicode`
    348         """
--> 349         response = self._prepare_and_send_request('PUT', url, **kwargs)
    350         return BoxResponse(response)
    351 

/Users/hnguyen/Code/box-python-sdk-hieu/boxsdk/session/box_session.pyc in _prepare_and_send_request(self, method, url, headers, auto_session_renewal, expect_json_response, attempt_number, **kwargs)
    236             attempt_number,
    237             file_stream_positions=file_stream_positions,
--> 238             **kwargs
    239         )
    240 

/Users/hnguyen/Code/box-python-sdk-hieu/boxsdk/session/box_session.pyc in _make_request(self, method, url, headers, auto_session_renewal, expect_json_response, attempt_number, **kwargs)
    313         )
    314 
--> 315         self._raise_on_unsuccessful_request(network_response, expect_json_response, method, url)
    316 
    317         return network_response

/Users/hnguyen/Code/box-python-sdk-hieu/boxsdk/session/box_session.pyc in _raise_on_unsuccessful_request(self, network_response, expect_json_response, method, url)
    175                 request_id=response_json.get('request_id', None),
    176                 url=url,
--> 177                 method=method,
    178             )
    179         if expect_json_response and not self._is_json_response(network_response):

BoxAPIException: 
Message: value differs from expectations
Status: 409
Code: failed_json_patch_application
Request id: 11962628355537cc3dd9baa
Headers: {'content-length': '123', 'age': '1', 'server': 'ATS', 'connection': 'keep-alive', 'date': 'Wed, 22 Apr 2015 16:28:46 GMT', 'content-type': 'application/json'}
URL: https://api.box.com/2.0/files/28792944858/metadata/enterprise/mortgageApplication
Method: PUT

Now, we'll retrieve the metadata again to demonstrate the GET operation:

In [17]:
metadata.get()
GET https://api.box.com/2.0/files/28792944858/metadata/enterprise/mortgageApplication {'headers': {u'Authorization': u'Bearer bljFP0m8vTdsKK3gWY0pnI2ETkluAAhW'}}
{"status":"Approved","amount":8888888,"borrowerName":"Alice","$type":"mortgageApplication-a2387be8-1279-4249-853b-f4a7cb6e0a54","$parent":"file_28792944858","$id":"657378d4-0617-4a04-9067-7e625076a0c2"}
Out[17]:
{u'$id': u'657378d4-0617-4a04-9067-7e625076a0c2',
 u'$parent': u'file_28792944858',
 u'$type': u'mortgageApplication-a2387be8-1279-4249-853b-f4a7cb6e0a54',
 u'amount': 8888888,
 u'borrowerName': u'Alice',
 u'status': u'Approved'}

Finally, we'll delete the metadata on the file:

In [18]:
metadata.delete()
DELETE https://api.box.com/2.0/files/28792944858/metadata/enterprise/mortgageApplication {'headers': {u'Authorization': u'Bearer bljFP0m8vTdsKK3gWY0pnI2ETkluAAhW'}}

Out[18]:
True

If we attempt to retrieve the metadata again, then the metadata will not be found:

In [19]:
metadata.get()
GET https://api.box.com/2.0/files/28792944858/metadata/enterprise/mortgageApplication {'headers': {u'Authorization': u'Bearer bljFP0m8vTdsKK3gWY0pnI2ETkluAAhW'}}
404
{'content-length': '180', 'content-encoding': 'gzip', 'age': '0', 'vary': 'Accept-Encoding', 'server': 'ATS', 'connection': 'keep-alive', 'date': 'Wed, 22 Apr 2015 16:28:56 GMT', 'content-type': 'application/json'}
'{"message":"Instance not found for \'\\"parented\\", \\"file_28792944858\\", \\"mortgageApplication-a2387be8-1279-4249-853b-f4a7cb6e0a54\\"\'","code":"tuple_not_found","request_id":"19099247955537cc48197fe"}'
---------------------------------------------------------------------------
BoxAPIException                           Traceback (most recent call last)
<ipython-input-19-5912b71dad7f> in <module>()
----> 1 metadata.get()

/Users/hnguyen/Code/box-python-sdk-hieu/boxsdk/object/metadata.pyc in get(self)
    167             :class:`Metadata`
    168         """
--> 169         return self._session.get(self.get_url()).json()
    170 
    171     def delete(self):

/Users/hnguyen/Code/box-python-sdk-hieu/boxsdk/session/box_session.pyc in get(self, url, **kwargs)
    325             `unicode`
    326         """
--> 327         response = self._prepare_and_send_request('GET', url, **kwargs)
    328         return BoxResponse(response)
    329 

/Users/hnguyen/Code/box-python-sdk-hieu/boxsdk/session/box_session.pyc in _prepare_and_send_request(self, method, url, headers, auto_session_renewal, expect_json_response, attempt_number, **kwargs)
    236             attempt_number,
    237             file_stream_positions=file_stream_positions,
--> 238             **kwargs
    239         )
    240 

/Users/hnguyen/Code/box-python-sdk-hieu/boxsdk/session/box_session.pyc in _make_request(self, method, url, headers, auto_session_renewal, expect_json_response, attempt_number, **kwargs)
    313         )
    314 
--> 315         self._raise_on_unsuccessful_request(network_response, expect_json_response, method, url)
    316 
    317         return network_response

/Users/hnguyen/Code/box-python-sdk-hieu/boxsdk/session/box_session.pyc in _raise_on_unsuccessful_request(self, network_response, expect_json_response, method, url)
    175                 request_id=response_json.get('request_id', None),
    176                 url=url,
--> 177                 method=method,
    178             )
    179         if expect_json_response and not self._is_json_response(network_response):

BoxAPIException: 
Message: Instance not found for '"parented", "file_28792944858", "mortgageApplication-a2387be8-1279-4249-853b-f4a7cb6e0a54"'
Status: 404
Code: tuple_not_found
Request id: 19099247955537cc48197fe
Headers: {'content-length': '180', 'content-encoding': 'gzip', 'age': '0', 'vary': 'Accept-Encoding', 'server': 'ATS', 'connection': 'keep-alive', 'date': 'Wed, 22 Apr 2015 16:28:56 GMT', 'content-type': 'application/json'}
URL: https://api.box.com/2.0/files/28792944858/metadata/enterprise/mortgageApplication
Method: GET

That's it! Now you'll be able to easily CRUD metadata and search for files with metadata using the Python SDK.

This notebook is available for download: http://opensource.box.com/box-python-sdk/tutorials/metadata.ipynb

Its content is licensed under the Apache License 2.0.

To reproduce the presentation, execute the following commands:

curl -O http://opensource.box.com/box-python-sdk/tutorials/metadata.ipynb
pip install boxsdk ipython[notebook]
ipython notebook metadata.ipynb