Skip to content
This repository was archived by the owner on Aug 30, 2024. It is now read-only.

Commit a4df82b

Browse files
committed
Added support for list function query execution
- Added support for managing list functions in design document - Added HTTP Headers argument to get_docs method - Replaced repeating if statements for sub-object properties with for loop over a frozen set of all properties - Replaced deprecated pylint disable-msg=too-many-branches with disable=too-many-branches
1 parent e744fad commit a4df82b

File tree

7 files changed

+464
-37
lines changed

7 files changed

+464
-37
lines changed

CHANGES.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
==================
33
- [NEW] Added support for Cloudant Search execution.
44
- [NEW] Added support for Cloudant Search index management.
5+
- [NEW] Added support for managing and querying list functions.
56
- [NEW] Added ``rewrites`` accessor property for URL rewriting.
67
- [NEW] Added support for a custom ``requests.HTTPAdapter`` to be configured using an optional ``adapter`` arg e.g.
78
``Cloudant(USERNAME, PASSWORD, account=ACCOUNT_NAME, adapter=Replay429Adapter())``.

src/cloudant/_common_util.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -234,14 +234,15 @@ def codify(code_or_str):
234234
return _Code(code_or_str)
235235
return code_or_str
236236

237-
def get_docs(r_session, url, encoder=None, **params):
237+
def get_docs(r_session, url, encoder=None, headers=None, **params):
238238
"""
239239
Provides a helper for functions that require GET or POST requests
240240
with a JSON, text, or raw response containing documents.
241241
242242
:param r_session: Authentication session from the client
243243
:param str url: URL containing the endpoint
244244
:param JSONEncoder encoder: Custom encoder from the client
245+
:param dict headers: Optional HTTP Headers to send with the request
245246
246247
:returns: Raw response content from the specified endpoint
247248
"""
@@ -253,9 +254,9 @@ def get_docs(r_session, url, encoder=None, **params):
253254

254255
resp = None
255256
if keys:
256-
resp = r_session.post(url, params=f_params, data=keys)
257+
resp = r_session.post(url, headers=headers, params=f_params, data=keys)
257258
else:
258-
resp = r_session.get(url, params=f_params)
259+
resp = r_session.get(url, headers=headers, params=f_params)
259260
resp.raise_for_status()
260261
return resp
261262

src/cloudant/database.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -762,6 +762,49 @@ def view_cleanup(self):
762762

763763
return resp.json()
764764

765+
def get_list_function_result(self, ddoc_id, list_name, view_name, **kwargs):
766+
"""
767+
Retrieves a customized MapReduce view result from the specified
768+
database based on the list function provided. List functions are
769+
used, for example, when you want to access Cloudant directly
770+
from a browser, and need data to be returned in a different
771+
format, such as HTML.
772+
773+
Note: All query parameters for View requests are supported.
774+
See :class:`~cloudant.database.get_view_result` for
775+
all supported query parameters.
776+
777+
For example:
778+
779+
.. code-block:: python
780+
781+
# Assuming that 'view001' exists as part of the
782+
# 'ddoc001' design document in the remote database...
783+
# Retrieve documents where the list function is 'list1'
784+
resp = db.get_list_result('ddoc001', 'list1', 'view001', limit=10)
785+
for row in resp['rows']:
786+
# Process data (in text format).
787+
788+
For more detail on list functions, refer to the
789+
`Cloudant documentation <https://docs.cloudant.com/
790+
design_documents.html#list-functions>`_.
791+
792+
:param str ddoc_id: Design document id used to get result.
793+
:param str list_name: Name used in part to identify the
794+
list function.
795+
:param str view_name: Name used in part to identify the view.
796+
797+
:return: Formatted view result data in text format
798+
"""
799+
ddoc = DesignDocument(self, ddoc_id)
800+
headers = {'Content-Type': 'application/json'}
801+
resp = get_docs(self.r_session,
802+
'/'.join([ddoc.document_url, '_list', list_name, view_name]),
803+
self.client.encoder,
804+
headers,
805+
**kwargs)
806+
return resp.text
807+
765808
class CloudantDatabase(CouchDatabase):
766809
"""
767810
Encapsulates a Cloudant database. A CloudantDatabase object is

src/cloudant/design_document.py

Lines changed: 96 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,19 @@ def __init__(self, database, document_id=None):
4444
if document_id and not document_id.startswith('_design/'):
4545
document_id = '_design/{0}'.format(document_id)
4646
super(DesignDocument, self).__init__(database, document_id)
47-
self.setdefault('views', dict())
48-
self.setdefault('indexes', dict())
47+
self._nested_object_names = frozenset(['views', 'indexes', 'lists'])
48+
for prop in self._nested_object_names:
49+
self.setdefault(prop, dict())
50+
51+
@property
52+
def lists(self):
53+
"""
54+
Provides an accessor property to the lists dictionary in the locally
55+
cached DesignDocument.
56+
57+
:returns: Dictionary containing list names and objects as key/value
58+
"""
59+
return self.get('lists')
4960

5061
@property
5162
def rewrites(self):
@@ -149,6 +160,21 @@ def add_search_index(self, index_name, search_func, analyzer=None):
149160

150161
self.indexes.__setitem__(index_name, search)
151162

163+
def add_list_function(self, list_name, list_func):
164+
"""
165+
Appends a list function to the locally cached DesignDocument
166+
indexes dictionary.
167+
168+
:param str list_name: Name used to identify the list function.
169+
:param str list_func: Javascript list function.
170+
"""
171+
if self.get_list_function(list_name) is not None:
172+
msg = ('A list with name {0} already exists in this design doc'
173+
.format(list_name))
174+
raise CloudantArgumentError(msg)
175+
176+
self.lists.__setitem__(list_name, codify(list_func))
177+
152178
def update_view(self, view_name, map_func, reduce_func=None, **kwargs):
153179
"""
154180
Modifies/overwrites an existing MapReduce view definition in the
@@ -195,6 +221,21 @@ def update_search_index(self, index_name, search_func, analyzer=None):
195221

196222
self.indexes.__setitem__(index_name, search)
197223

224+
def update_list_function(self, list_name, list_func):
225+
"""
226+
Modifies/overwrites an existing list function in the
227+
locally cached DesignDocument indexes dictionary.
228+
229+
:param str list_name: Name used to identify the list function.
230+
:param str list_func: Javascript list function.
231+
"""
232+
if self.get_list_function(list_name) is None:
233+
msg = ('A list with name {0} does not exist in this design doc'
234+
.format(list_name))
235+
raise CloudantArgumentError(msg)
236+
237+
self.lists.__setitem__(list_name, codify(list_func))
238+
198239
def delete_view(self, view_name):
199240
"""
200241
Removes an existing MapReduce view definition from the locally cached
@@ -227,6 +268,15 @@ def delete_index(self, index_name):
227268

228269
self.indexes.__delitem__(index_name)
229270

271+
def delete_list_function(self, list_name):
272+
"""
273+
Removes an existing list function in the locally cached DesignDocument
274+
lists dictionary.
275+
276+
:param str list_name: Name used to identify the list.
277+
"""
278+
self.lists.__delitem__(list_name)
279+
230280
def fetch(self):
231281
"""
232282
Retrieves the remote design document content and populates the locally
@@ -236,10 +286,7 @@ def fetch(self):
236286
``dict`` types.
237287
"""
238288
super(DesignDocument, self).fetch()
239-
if not self.views:
240-
# Ensure views dict exists in locally cached DesignDocument.
241-
self.setdefault('views', dict())
242-
else:
289+
if self.views:
243290
for view_name, view_def in iteritems_(self.get('views', dict())):
244291
if self.get('language', None) != QUERY_LANGUAGE:
245292
self['views'][view_name] = View(
@@ -258,11 +305,11 @@ def fetch(self):
258305
**view_def
259306
)
260307

261-
if not self.indexes:
262-
# Ensure indexes dict exists in locally cached DesignDocument.
263-
self.setdefault('indexes', dict())
308+
for prop in self._nested_object_names:
309+
# Ensure views, indexes, and lists dict exist in locally cached DesignDocument.
310+
getattr(self, prop, self.setdefault(prop, dict()))
264311

265-
# pylint: disable-msg=too-many-branches
312+
# pylint: disable=too-many-branches
266313
def save(self):
267314
"""
268315
Saves changes made to the locally cached DesignDocument object's data
@@ -285,9 +332,6 @@ def save(self):
285332
'View {0} must be of type QueryIndexView.'
286333
).format(view_name)
287334
raise CloudantException(msg)
288-
else:
289-
# Ensure empty views dict is not saved remotely.
290-
self.__delitem__('views')
291335

292336
if self.indexes:
293337
if self.get('language', None) != QUERY_LANGUAGE:
@@ -307,18 +351,17 @@ def save(self):
307351
'be of type dict.'
308352
).format(index_name)
309353
raise CloudantException(msg)
310-
else:
311-
# Ensure empty indexes dict is not saved remotely.
312-
self.__delitem__('indexes')
354+
355+
for prop in self._nested_object_names:
356+
if not getattr(self, prop):
357+
# Ensure empty views, indexes, or lists dict is not saved remotely.
358+
self.__delitem__(prop)
313359

314360
super(DesignDocument, self).save()
315361

316-
if not self.views:
317-
# Ensure views dict exists in locally cached DesignDocument.
318-
self.setdefault('views', dict())
319-
if not self.indexes:
320-
# Ensure indexes dict exists in locally cached DesignDocument.
321-
self.setdefault('indexes', dict())
362+
for prop in self._nested_object_names:
363+
# Ensure views, indexes, and lists dict exist in locally cached DesignDocument.
364+
getattr(self, prop, self.setdefault(prop, dict()))
322365

323366
def __setitem__(self, key, value):
324367
"""
@@ -368,6 +411,17 @@ def iterindexes(self):
368411
for index_name, search_func in iteritems_(self.indexes):
369412
yield index_name, search_func
370413

414+
def iterlists(self):
415+
"""
416+
Provides a way to iterate over the locally cached DesignDocument
417+
lists dictionary.
418+
419+
:returns: Iterable containing list function name and associated
420+
list function
421+
"""
422+
for list_name, list_func in iteritems_(self.lists):
423+
yield list_name, list_func
424+
371425
def list_views(self):
372426
"""
373427
Retrieves a list of available View objects in the locally cached
@@ -386,6 +440,15 @@ def list_indexes(self):
386440
"""
387441
return list(self.indexes.keys())
388442

443+
def list_list_functions(self):
444+
"""
445+
Retrieves a list of available list functions in the locally cached
446+
DesignDocument lists dictionary.
447+
448+
:returns: List of list function names
449+
"""
450+
return list(self.lists.keys())
451+
389452
def get_view(self, view_name):
390453
"""
391454
Retrieves a specific View from the locally cached DesignDocument by
@@ -408,6 +471,17 @@ def get_index(self, index_name):
408471
"""
409472
return self.indexes.get(index_name)
410473

474+
def get_list_function(self, list_name):
475+
"""
476+
Retrieves a specific list function from the locally cached DesignDocument
477+
lists dictionary by name.
478+
479+
:param str list_name: Name used to identify the list function.
480+
481+
:returns: Index dictionary for the specified list function name
482+
"""
483+
return self.lists.get(list_name)
484+
411485
def info(self):
412486
"""
413487
Retrieves the design document view information data, returns dictionary

tests/unit/database_tests.py

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@ def test_retrieve_design_document(self):
254254
# Get an empty design document object that does not exist remotely
255255
local_ddoc = self.db.get_design_document('_design/ddoc01')
256256
self.assertEqual(local_ddoc, {'_id': '_design/ddoc01', 'indexes': {},
257-
'views': {}})
257+
'views': {}, 'lists': {}})
258258
# Add the design document to the database
259259
map_func = 'function(doc) {\n emit(doc._id, 1); \n}'
260260
local_ddoc.add_view('view01', map_func)
@@ -625,6 +625,48 @@ def test_changes_inifinite_feed_call(self):
625625
self.assertFalse(changes._raw_data)
626626
self.assertDictEqual(changes._options, {'feed': 'continuous'})
627627

628+
def test_get_list_function_result_with_invalid_argument(self):
629+
"""
630+
Test get_list_result by passing in invalid arguments
631+
"""
632+
with self.assertRaises(CloudantArgumentError) as cm:
633+
self.db.get_list_function_result('ddoc001', 'list001', 'view001', foo={'bar': 'baz'})
634+
err = cm.exception
635+
self.assertEqual(str(err), 'Invalid argument foo')
636+
637+
def test_get_list_function_result(self):
638+
"""
639+
Test get_list_result executes a list function against a view's MapReduce
640+
function.
641+
"""
642+
self.populate_db_with_documents()
643+
ddoc = DesignDocument(self.db, '_design/ddoc001')
644+
ddoc.add_view('view001', 'function (doc) {\n emit(doc._id, 1);\n}')
645+
ddoc.add_list_function(
646+
'list001',
647+
'function(head, req) { provides(\'html\', function() '
648+
'{var html = \'<html><body><ol>\\n\'; while (row = getRow()) '
649+
'{ html += \'<li>\' + row.key + \':\' + row.value + \'</li>\\n\';} '
650+
'html += \'</ol></body></html>\'; return html; }); }')
651+
ddoc.save()
652+
# Execute list function
653+
resp = self.db.get_list_function_result(
654+
'_design/ddoc001',
655+
'list001',
656+
'view001',
657+
limit=5
658+
)
659+
self.assertEqual(
660+
resp,
661+
'<html><body><ol>\n'
662+
'<li>julia000:1</li>\n'
663+
'<li>julia001:1</li>\n'
664+
'<li>julia002:1</li>\n'
665+
'<li>julia003:1</li>\n'
666+
'<li>julia004:1</li>\n'
667+
'</ol></body></html>'
668+
)
669+
628670
@unittest.skipUnless(
629671
os.environ.get('RUN_CLOUDANT_TESTS') is not None,
630672
'Skipping Cloudant specific Database tests'
@@ -857,6 +899,7 @@ def test_create_json_index(self):
857899
{'_id': index.design_document_id,
858900
'_rev': ddoc['_rev'],
859901
'indexes': {},
902+
'lists': {},
860903
'language': 'query',
861904
'views': {index.name: {'map': {'fields': {'name': 'asc',
862905
'age': 'asc'}},
@@ -883,6 +926,7 @@ def test_create_text_index(self):
883926
'_rev': ddoc['_rev'],
884927
'language': 'query',
885928
'views': {},
929+
'lists': {},
886930
'indexes': {index.name: {'index': {'index_array_lengths': True,
887931
'fields': [{'name': 'name', 'type': 'string'},
888932
{'name': 'age', 'type': 'number'}],
@@ -907,6 +951,7 @@ def test_create_all_fields_text_index(self):
907951
'_rev': ddoc['_rev'],
908952
'language': 'query',
909953
'views': {},
954+
'lists': {},
910955
'indexes': {index.name: {'index': {'index_array_lengths': True,
911956
'fields': 'all_fields',
912957
'default_field': {},
@@ -942,6 +987,7 @@ def test_create_multiple_indexes_one_ddoc(self):
942987
{'_id': '_design/ddoc001',
943988
'_rev': ddoc['_rev'],
944989
'language': 'query',
990+
'lists': {},
945991
'views': {'json-index-001': {
946992
'map': {'fields': {'name': 'asc',
947993
'age': 'asc'}},

0 commit comments

Comments
 (0)