Skip to content

Commit 228c6a9

Browse files
Add metadata query functionality (#574)
1 parent bc5ad87 commit 228c6a9

File tree

6 files changed

+242
-5
lines changed

6 files changed

+242
-5
lines changed

HISTORY.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,13 @@
33
Release History
44
---------------
55

6+
Next Release
7+
++++++++
8+
9+
**New Features and Enhancements:**
10+
11+
- Add metadata query functionality (`#574 <https://github.com/box/box-python-sdk/pull/574>`_)
12+
613
2.11.0 (2021-01-11)
714
++++++++
815

boxsdk/object/search.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from .base_endpoint import BaseEndpoint
88
from ..pagination.limit_offset_based_object_collection import LimitOffsetBasedObjectCollection
9+
from ..pagination.marker_based_object_collection import MarkerBasedObjectCollection
910
from ..util.api_call_decorator import api_call
1011
from ..util.text_enum import TextEnum
1112

@@ -310,3 +311,75 @@ def query(
310311
additional_params=additional_params,
311312
return_full_pages=False,
312313
)
314+
315+
@api_call
316+
def metadata_query(self, from_template, ancestor_folder_id, query=None, query_params=None, use_index=None, order_by=None,
317+
marker=None, limit=None, fields=None):
318+
# pylint: disable=arguments-differ
319+
"""Query Box items by their metadata.
320+
321+
:param from_template:
322+
The template used in the query. Must be in the form scope.templateKey.
323+
:type from_template:
324+
`unicode`
325+
:param ancestor_folder_id:
326+
The folder_id to which to restrain the query
327+
:type ancestor_folder_id:
328+
`unicode`
329+
:param query:
330+
The logical expression of the query
331+
:type query:
332+
`unicode` or None
333+
:param query_params:
334+
Required if query present. The arguments for the query.
335+
:type query_params:
336+
`dict` or None
337+
:param use_index:
338+
The name of the index to use
339+
:type use_index:
340+
`unicode` or None
341+
:param order_by:
342+
The field_key(s) to order on and the corresponding direction(s)
343+
:type order_by:
344+
`list` of `dict`
345+
:param marker:
346+
The marker to use for requesting the next page
347+
:type marker:
348+
`unicode` or None
349+
:param limit:
350+
Max results to return for a single request (0-100 inclusive)
351+
:type limit:
352+
`int`
353+
:param fields:
354+
List of fields to request
355+
:type fields:
356+
`Iterable` of `unicode` or None
357+
:returns:
358+
An iterator of the item search results
359+
:rtype:
360+
:class:`BoxObjectCollection`
361+
"""
362+
url = super(Search, self).get_url('metadata_queries/execute_read')
363+
data = {
364+
'from': from_template,
365+
'ancestor_folder_id': ancestor_folder_id
366+
}
367+
if query is not None:
368+
data['query'] = query
369+
if query_params is not None:
370+
data['query_params'] = query_params
371+
if use_index is not None:
372+
data['use_index'] = use_index
373+
if order_by is not None:
374+
data['order_by'] = order_by
375+
376+
return MarkerBasedObjectCollection(
377+
session=self._session,
378+
url=url,
379+
limit=limit,
380+
marker=marker,
381+
fields=fields,
382+
additional_params=data,
383+
return_full_pages=False,
384+
use_post=True
385+
)

boxsdk/pagination/box_object_collection.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import unicode_literals
44

5+
import json
56
import sys
67
from abc import ABCMeta, abstractmethod
78

@@ -42,6 +43,7 @@ def __init__(
4243
fields=None,
4344
additional_params=None,
4445
return_full_pages=False,
46+
use_post=False
4547
):
4648
"""
4749
:param session:
@@ -71,6 +73,11 @@ def __init__(
7173
call to next(). If False, the iterator will return a single Box object on each next() call.
7274
:type return_full_pages:
7375
`bool`
76+
:param use_post:
77+
If True, then the returned iterator will make POST requests with all the data in the body on each
78+
call to next(). If False, the iterator will make GET requets with all the data as query params on each call to next().
79+
:type use_post:
80+
`bool`
7481
"""
7582
super(BoxObjectCollection, self).__init__()
7683
self._session = session
@@ -81,6 +88,7 @@ def __init__(
8188
self._return_full_pages = return_full_pages
8289
self._has_retrieved_all_items = False
8390
self._all_items = None
91+
self._use_post = use_post
8492

8593
def next(self):
8694
"""
@@ -135,12 +143,17 @@ def _load_next_page(self):
135143
params = {}
136144
if self._limit is not None:
137145
params['limit'] = self._limit
138-
if self._fields:
139-
params['fields'] = ','.join(self._fields)
140146
if self._additional_params:
141147
params.update(self._additional_params)
142148
params.update(self._next_page_pointer_params())
143-
box_response = self._session.get(self._url, params=params)
149+
if self._use_post:
150+
if self._fields:
151+
params['fields'] = self._fields
152+
box_response = self._session.post(self._url, data=json.dumps(params), headers={b'Content-Type': b'application/json'})
153+
else:
154+
if self._fields:
155+
params['fields'] = ','.join(self._fields)
156+
box_response = self._session.get(self._url, params=params)
144157
return box_response.json()
145158

146159
@abstractmethod

boxsdk/pagination/marker_based_object_collection.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ def __init__(
2323
return_full_pages=False,
2424
marker=None,
2525
supports_limit_offset_paging=False,
26+
use_post=False
2627
):
2728
"""
2829
:param marker:
@@ -34,6 +35,11 @@ def __init__(
3435
the endpoints that support both require an special extra request parameter.
3536
:type supports_limit_offset_paging:
3637
`bool`
38+
:param use_post:
39+
If True, then the returned iterator will make POST requests with all the data in the body on each
40+
call to next(). If False, the iterator will make GET requets with all the data as query params on each call to next().
41+
:type use_post:
42+
`bool`
3743
"""
3844
super(MarkerBasedObjectCollection, self).__init__(
3945
session,
@@ -42,6 +48,7 @@ def __init__(
4248
fields=fields,
4349
additional_params=additional_params,
4450
return_full_pages=return_full_pages,
51+
use_post=use_post
4552
)
4653
self._marker = marker
4754
self._supports_limit_offset_paging = supports_limit_offset_paging

docs/usage/search.md

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
Search
22
======
33

4-
The search endpoint provides a powerful way of finding items that are accessible by a single user or an entire
5-
enterprise. Leverage the parameters listed below to generate targeted advanced searches.
4+
Search provides a powerful way of finding items that are accessible by a single user or an entire
5+
enterprise.
6+
7+
- [Search for Content](#search-for-content)
8+
- [Metadata Query](#metadata-query)
69

710
Search for Content
811
------------------
@@ -46,3 +49,39 @@ client.search().query(None, limit=100, offset=0, metadata_filters=metadata_searc
4649
[metadata_search_filters]: https://box-python-sdk.readthedocs.io/en/latest/boxsdk.object.html#boxsdk.object.search.MetadataSearchFilters
4750
[add_value_based_filter]: https://box-python-sdk.readthedocs.io/en/latest/boxsdk.object.html#boxsdk.object.search.MetadataSearchFilter.add_value_based_filter
4851
[add_filter]: https://box-python-sdk.readthedocs.io/en/latest/boxsdk.object.html#boxsdk.object.search.MetadataSearchFilters.add_filter
52+
53+
Metadata Query
54+
--------------
55+
To search using SQL-like syntax to return items that match specific metadata, call `search.metadata_query(from_template, ancestor_folder_id, query=None, query_params=None, use_index=None, order_by=None, marker=None, limit=None, fields=None)`
56+
57+
By default, this method returns only the most basic info about the items for which the query matches. To get additional fields for each item, including any of the metadata, use the fields parameter.
58+
59+
```python
60+
from_template = 'enterprise_12345.someTemplate'
61+
ancestor_folder_id = '5555'
62+
query = 'amount >= :arg'
63+
query_params = {'arg': 100}
64+
use_index = 'amountAsc'
65+
order_by = [
66+
{
67+
'field_key': 'amount',
68+
'direction': 'asc'
69+
}
70+
]
71+
fields = ['type', 'id', 'name', 'metadata.enterprise_67890.catalogImages.$parent']
72+
limit = 2
73+
marker = 'AAAAAmVYB1FWec8GH6yWu2nwmanfMh07IyYInaa7DZDYjgO1H4KoLW29vPlLY173OKs'
74+
items = client.search().metadata_query(
75+
from_template,
76+
ancestor_folder_id,
77+
query,
78+
query_params,
79+
use_index,
80+
order_by,
81+
marker,
82+
limit,
83+
fields
84+
)
85+
for item in items:
86+
print('The item ID is {0} and the item name is {1}'.format(item.id, item.name))
87+
```

test/unit/object/test_search.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import pytest
88

9+
from boxsdk.config import API
910
from boxsdk.object.file import File
1011
from boxsdk.object.user import User
1112
from boxsdk.object.search import MetadataSearchFilters, MetadataSearchFilter, SearchScope, TrashContent
@@ -78,6 +79,48 @@ def search_response():
7879
}
7980

8081

82+
@pytest.fixture
83+
def metadata_query_response():
84+
return {
85+
'entries': [
86+
{
87+
'type': 'file',
88+
'id': '1244738582',
89+
'name': 'Very Important.docx',
90+
'metadata': {
91+
'enterprise_67890': {
92+
'catalogImages': {
93+
'$parent': 'file_50347290',
94+
'$version': 2,
95+
'$template': 'catalogImages',
96+
'$scope': 'enterprise_67890',
97+
'photographer': 'Bob Dylan'
98+
}
99+
}
100+
}
101+
},
102+
{
103+
'type': 'folder',
104+
'id': '124242482',
105+
'name': 'Also Important.docx',
106+
'metadata': {
107+
'enterprise_67890': {
108+
'catalogImages': {
109+
'$parent': 'file_50427291',
110+
'$version': 2,
111+
'$template': 'catalogImages',
112+
'$scope': 'enterprise_67890',
113+
'photographer': 'Bob Dylan'
114+
}
115+
}
116+
}
117+
}
118+
],
119+
'limit': 2,
120+
'next_marker': ''
121+
}
122+
123+
81124
class Matcher(object):
82125
def __init__(self, compare, some_obj):
83126
self.compare = compare
@@ -257,6 +300,61 @@ def test_query_with_owner_users(
257300
)
258301

259302

303+
def test_metadata_query(
304+
mock_box_session,
305+
make_mock_box_request,
306+
test_search,
307+
metadata_query_response
308+
):
309+
# pylint:disable=redefined-outer-name
310+
expected_url = '{0}/metadata_queries/execute_read'.format(API.BASE_API_URL)
311+
from_template = 'enterprise_12345.someTemplate'
312+
ancestor_folder_id = '5555'
313+
query = 'amount >= :arg'
314+
query_params = {'arg': 100}
315+
use_index = 'amountAsc'
316+
order_by = [
317+
{
318+
'field_key': 'amount',
319+
'direction': 'asc'
320+
}
321+
]
322+
fields = ['type', 'id', 'name', 'metadata.enterprise_67890.catalogImages.$parent']
323+
limit = 2
324+
marker = 'AAAAAmVYB1FWec8GH6yWu2nwmanfMh07IyYInaa7DZDYjgO1H4KoLW29vPlLY173OKs'
325+
expected_data = {
326+
'limit': limit,
327+
'from': from_template,
328+
'ancestor_folder_id': ancestor_folder_id,
329+
'query': query,
330+
'query_params': query_params,
331+
'use_index': use_index,
332+
'order_by': order_by,
333+
'marker': marker,
334+
'fields': fields
335+
}
336+
expected_headers = {b'Content-Type': b'application/json'}
337+
mock_box_session.post.return_value, _ = make_mock_box_request(response=metadata_query_response)
338+
items = test_search.metadata_query(
339+
from_template,
340+
ancestor_folder_id,
341+
query,
342+
query_params,
343+
use_index,
344+
order_by,
345+
marker,
346+
limit,
347+
fields
348+
)
349+
item1 = items.next()
350+
item2 = items.next()
351+
mock_box_session.post.assert_called_once_with(expected_url, data=json.dumps(expected_data), headers=expected_headers)
352+
assert item1['type'] == 'file'
353+
assert item1['metadata']['enterprise_67890']['catalogImages']['$parent'] == 'file_50347290'
354+
assert item2['type'] == 'folder'
355+
assert item2['metadata']['enterprise_67890']['catalogImages']['$parent'] == 'file_50427291'
356+
357+
260358
def test_range_filter_without_gt_and_lt_will_fail_validation():
261359
metadata_filter = MetadataSearchFilter(template_key='mytemplate', scope='enterprise')
262360
with pytest.raises(ValueError):

0 commit comments

Comments
 (0)