Repository: cloudant/python-cloudant (python)

Issue:
Querying a view with a boolean type key raises a CloudantArgumentError
## Bug Description

### 1. Steps to reproduce and the simplest code sample possible to demonstrate the issue

Create a cloudant database view, and attempt to query the view using the python-cloudant library as follows:
```
results = database.get_view_result('_design/doc', 'view-name', raw_result=True, key=False)
```
The above code will raise a CloudantArgumentError due to the validation on this line https://github.com/cloudant/python-cloudant/blob/33781463ff85c8bb535bd6ede2049e4d3bec6c4c/src/cloudant/_common_util.py#L192

### 2. What you expected to happen

The expected outcome would be that the view get filtered by the boolean type key instead of raising the error

I removed the validation locally and reinitiated the view query, and the key filtered fine from the CouchDB library end i.e. their library accepted a boolean type for the key argument

### 3. What actually happened

A CloudantArgumentError was raised when trying to filter by a boolean value for the argument key

## Environment details
<!--
- Version(s) that are affected by this issue.
    > 2.0.2
- Python version
    > 3.6.2
-->

Python 3.9.2
Database server: IBM Cloudant


Pull request:
Support boolean type for `key`, `endkey`, and `startkey` in view requests
<!--
Thanks for your hard work, please ensure all items are complete before opening.
-->
## Checklist

- [x] Tick to sign-off your agreement to the [Developer Certificate of Origin (DCO) 1.1](../blob/master/DCO1.1.txt)
- [x] Added tests for code changes _or_ test/build only changes
- [x] Updated the change log file (`CHANGES.md`) _or_ test/build only changes
- [x] Completed the PR template below:

## Description

Fixes #494 

## Approach

Allow bool type for `endkey`, `key`, and `startkey`. 

## Schema & API Changes

- No change

## Security and Privacy

- No change


## Testing

Edited the `invalid` tests to use `{'foo': 'bar'}` invalid values instead of booleans, and extended the existing tests to validate boolean types.

## Monitoring and Logging

- No change


Patch:
diff --git a/CHANGES.md b/CHANGES.md
index 522d5ed..5670050 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -6,6 +6,7 @@
 - [FIXED] Incorrect use of username as account name in `Cloudant.bluemix()`.
 - [IMPROVED] Documented use of None account name and url override for `Cloudant.iam()`.
 - [FIXED] Use custom encoder (if provided) for all view `key` params not just `keys`.
+- [FIXED] Support boolean type for `key`, `endkey`, and `startkey` in view requests.
 
 # 2.14.0 (2020-08-17)
 
diff --git a/src/cloudant/_common_util.py b/src/cloudant/_common_util.py
index 962f1de..adce56f 100644
--- a/src/cloudant/_common_util.py
+++ b/src/cloudant/_common_util.py
@@ -56,20 +56,20 @@ ANY_TYPE = object()
 
 RESULT_ARG_TYPES = {
     'descending': (bool,),
-    'endkey': (int, LONGTYPE, STRTYPE, Sequence,),
+    'endkey': (int, LONGTYPE, STRTYPE, Sequence, bool,),
     'endkey_docid': (STRTYPE,),
     'group': (bool,),
     'group_level': (int, LONGTYPE, NONETYPE,),
     'include_docs': (bool,),
     'inclusive_end': (bool,),
-    'key': (int, LONGTYPE, STRTYPE, Sequence,),
+    'key': (int, LONGTYPE, STRTYPE, Sequence, bool,),
     'keys': (list,),
     'limit': (int, LONGTYPE, NONETYPE,),
     'reduce': (bool,),
     'skip': (int, LONGTYPE, NONETYPE,),
     'stable': (bool,),
     'stale': (STRTYPE,),
-    'startkey': (int, LONGTYPE, STRTYPE, Sequence,),
+    'startkey': (int, LONGTYPE, STRTYPE, Sequence, bool,),
     'startkey_docid': (STRTYPE,),
     'update': (STRTYPE,),
 }
@@ -191,12 +191,13 @@ def py_to_couch_validate(key, val):
     # Validate argument values and ensure that a boolean is not passed in
     # if an integer is expected
     if (not isinstance(val, RESULT_ARG_TYPES[key]) or
-            (type(val) is bool and int in RESULT_ARG_TYPES[key])):
+            (type(val) is bool and bool not in RESULT_ARG_TYPES[key] and
+             int in RESULT_ARG_TYPES[key])):
         raise CloudantArgumentError(117, key, RESULT_ARG_TYPES[key])
     if key == 'keys':
         for key_list_val in val:
             if (not isinstance(key_list_val, RESULT_ARG_TYPES['key']) or
-                    type(key_list_val) is bool):
+                    isinstance(key_list_val, bool)):
                 raise CloudantArgumentError(134, RESULT_ARG_TYPES['key'])
     if key == 'stale':
         if val not in ('ok', 'update_after'):


Test patch:
diff --git a/tests/unit/param_translation_tests.py b/tests/unit/param_translation_tests.py
index 6aac721..fda3c00 100644
--- a/tests/unit/param_translation_tests.py
+++ b/tests/unit/param_translation_tests.py
@@ -55,6 +55,10 @@ class PythonToCouchTests(unittest.TestCase):
             python_to_couch({'endkey': ['foo', 10]}),
             {'endkey': '["foo", 10]'}
         )
+        self.assertEqual(
+            python_to_couch({'endkey': True}),
+            {'endkey': 'true'}
+        )
 
     def test_valid_endkey_docid(self):
         """
@@ -128,6 +132,10 @@ class PythonToCouchTests(unittest.TestCase):
             python_to_couch({'key': ['foo', 10]}),
             {'key': '["foo", 10]'}
         )
+        self.assertEqual(
+            python_to_couch({'key': True}),
+            {'key': 'true'}
+        )
 
     def test_valid_keys(self):
         """
@@ -205,6 +213,10 @@ class PythonToCouchTests(unittest.TestCase):
             python_to_couch({'startkey': ['foo', 10]}),
             {'startkey': '["foo", 10]'}
         )
+        self.assertEqual(
+            python_to_couch({'startkey': True}),
+            {'startkey': 'true'}
+        )
 
     def test_valid_startkey_docid(self):
         """
@@ -247,7 +259,7 @@ class PythonToCouchTests(unittest.TestCase):
         """
         msg = 'Argument endkey not instance of expected type:'
         with self.assertRaises(CloudantArgumentError) as cm:
-            python_to_couch({'endkey': True})
+            python_to_couch({'endkey': {'foo': 'bar'}})
         self.assertTrue(str(cm.exception).startswith(msg))
 
     def test_invalid_endkey_docid(self):
@@ -302,7 +314,7 @@ class PythonToCouchTests(unittest.TestCase):
         """
         msg = 'Argument key not instance of expected type:'
         with self.assertRaises(CloudantArgumentError) as cm:
-            python_to_couch({'key': True})
+            python_to_couch({'key': {'foo': 'bar'}})
         self.assertTrue(str(cm.exception).startswith(msg))
 
     def test_invalid_keys_not_list(self):
@@ -372,7 +384,7 @@ class PythonToCouchTests(unittest.TestCase):
         """
         msg = 'Argument startkey not instance of expected type:'
         with self.assertRaises(CloudantArgumentError) as cm:
-            python_to_couch({'startkey': True})
+            python_to_couch({'startkey': {'foo': 'bar'}})
         self.assertTrue(str(cm.exception).startswith(msg))
 
     def test_invalid_startkey_docid(self):