Repository: nylas/nylas-python

Issue:
RFE: Allow to control `threads.search` threads view
## Issue Overview

Thanks for maintaining this damn awesome SDK! While implementing [email search functionality for Code Inbox](https://github.com/coder-inbox/code-inbox-server/blob/6ef01f0d207cbae10315a5f109646f5dc1bd34bd/src/nylas/router.py#L316) using this SDK, I encountered a limitation in the SDK. The current implementation allows searching for threads with no control over the `view` of the found threads, so there's no direct way to expand messages in searched threads. The following is the current signature of the `thread.search` function:

```python
search(q, limit=None, offset=None)
```

As you can see, it is missing an important argument, namely `view` like `view=expanded`, that allows users to view details about the found messages in a particular thread. Currently, it returns the IDs of the found messages, `message_ids`. Here is an example:

```sh
[{'id': 'akdfn6d3z1qnb4ms3bnsygtlb', 'cls': <class 'nylas.client.restful_models.Thread'>, 'api': <nylas.client.client.APIClient object at 0x7f4c1241f670>, 'draft_ids': [], 'message_ids': ['4th3z0d2h2pmus8kin6lqpcu7'], 'account_id': '48bkmeljel3rleswxmlzdgzqx', 'object': 'thread', 'participants': [{'email': 'mahmoudddharmouchhh@gmail.com', 'name': ''}, {'email': 'yo@dev.to', 'name': 'The DEV Team'}], 'snippet': "We're partying...", 'subject': 'Weekly Wins – Open Source Spotlight & Hacktoberfest Highlights', 'last_message_timestamp': 1697220577, 'first_message_timestamp': 1697220577, 'last_message_received_timestamp': 1697220577, 'last_message_sent_timestamp': None, 'unread': True, 'starred': False, 'version': 0, '_labels': [{'display_name': 'Inbox', 'id': 'f0p7g3g1i5ikzge3h2pef2i3j', 'name': 'inbox'}, {'display_name': 'All Mail', 'id': '49n5iwureouzre0qc1he7u7ql', 'name': 'all'}, {'display_name': 'Category Updates', 'id': '3s46jis0l07vrq5xc334f4id9', 'name': 'updates'}], 'has_attachments': False, 'first_message_at': datetime.datetime(2023, 10, 13, 18, 9, 37), 'last_message_at': datetime.datetime(2023, 10, 13, 18, 9, 37), 'last_message_received_at': datetime.datetime(2023, 10, 13, 18, 9, 37)}]
```

But what is useful is the details of the messages in `_messages` in this array instead:

```sh
[{'id': '89jw3cpqe0hpf8qbxtw0n9wpg', 'cls': <class 'nylas.client.restful_models.Thread'>, 'api': <nylas.client.client.APIClient object at 0x7f887b57a640>, 'account_id': '48bkmeljel3rleswxmlzdgzqx', 'object': 'thread', 'participants': [{'email': 'mahmoudddharmouchhh@gmail.com', 'name': ''}, {'email': 'la-minute-culture-generale-space@quora.com', 'name': 'La Minute Culture Générale'}], 'snippet': 'Pouvez-vous dire...', 'last_message_timestamp': 1697289258, 'first_message_timestamp': 1697289258, 'last_message_received_timestamp': 1697289258, 'last_message_sent_timestamp': None, 'unread': True, 'starred': False, 'version': 0, '_labels': [{'display_name': 'Category Promotions', 'id': '38o9uy1kv5f9k0lw14zji3ehf', 'name': 'promotions'}, {'display_name': 'Inbox', 'id': 'f0p7g3g1i5ikzge3h2pef2i3j', 'name': 'inbox'}, {'display_name': 'All Mail', 'id': '49n5iwureouzre0qc1he7u7ql', 'name': 'all'}], '_messages': [{'account_id': '48bkmeljel3rleswxmlzdgzqx', 'bcc': [], 'cc': [], 'date': 1697289258, 'files': [], 'from': [{'email': 'la-minute-culture-generale-space@quora.com', 'name': 'La Minute Culture Générale'}], 'id': '699vqk8pkci75zza719up4nc', 'labels': [{'display_name': 'Inbox', 'id': 'f0p7g3g1i5ikzge3h2pef2i3j', 'name': 'inbox'}, {'display_name': 'Category Promotions', 'id': '38o9uy1kv5f9k0lw14zji3ehf', 'name': 'promotions'}, {'display_name': 'All Mail', 'id': '49n5iwureouzre0qc1he7u7ql', 'name': 'all'}], 'object': 'message', 'reply_to': [], 'snippet': 'Pouvez-vous dire une phrase que vous pourriez dire naturellement à votre travail mais qui est incompréhensible pour tout le reste du monde ? La Minute Culture Générale • 130 k abonnés Amélior', 'starred': False, 'subject': 'Pouvez-vous dire une phrase que vous pourriez dire naturellement à votre travail mais qui est incompréhensible pour tout le reste du monde ?', 'thread_id': '89jw3cpqe0hpf8qbxtw0n9wpg', 'to': [{'email': 'mahmoudddharmouchhh@gmail.com', 'name': ''}], 'unread': True}], 'has_attachments': False, 'first_message_at': datetime.datetime(2023, 10, 14, 13, 14, 18), 'last_message_at': datetime.datetime(2023, 10, 14, 13, 14, 18), 'last_message_received_at': datetime.datetime(2023, 10, 14, 13, 14, 18)}]

```

I managed to work around this limitation by [manually constructing threads from the found emails](https://github.com/coder-inbox/code-inbox-server/blob/6ef01f0d207cbae10315a5f109646f5dc1bd34bd/src/nylas/router.py#L329-L342), but this workaround results in a performance issue and slow response times.

https://github.com/nylas/nylas-python/assets/62179149/8923fbc6-6a20-4c3e-a79b-7323dbab8684

To improve the SDK's usability and performance, I propose adding a new argument to the `nylas.threads.search` function, namely `view`.

## Motivation

### Current Limitation
This SDK provides a powerful and convenient way to search for emails using `nylas.messages.search`. However, when it comes to searching for threads, we need to retrieve a list of emails with details, manually identify related emails, and construct threads. This process is cumbersome and resource-intensive, especially when dealing with a large number of emails.

### Proposed Solution
The proposed `view` argument to the `nylas.threads.search` method would simplify the process of searching for threads and message details. This new attribute would take a set of search parameters, execute the search on the Nylas API, and return a list of threads matching the criteria. This would significantly enhance the efficiency and usability of the SDK for platforms like Code Inbox.

## Benefits

1. **Usability**: Adding the `view` argument to the `nylas.threads.search` method would make it easier for developers to search for threads and emails details without needing to manually construct them from emails.

2. **Performance**: The SDK can optimize the process of searching for threads, resulting in faster response times and improved performance.

3. **Consistency**: This new argument would maintain consistency with the existing `threads.where(view="expanded")` method, making it more intuitive for developers already familiar with the SDK.

## Drawbacks

1. **API Changes**: Adding this method would require changes to the Nylas API, which might impact existing integrations.

## Conclusion

The addition of the `view` argument to the `nylas.threads.search` method would enhance this SDK's capabilities and improve the performance of applications that rely on thread-based email processing. It aligns with the existing functionality of searching for emails and promotes consistency within the SDK.

I'm open to feedback and discussions regarding the implementation of this feature. Please share your thoughts and concerns, and let's work together to make this damn SDK even more powerful and user-friendly.

## References

- [Nylas search SDK Documentation](https://developer.nylas.com/docs/api/v2/#get-/threads/search)

Thank you for considering this RFE. Your feedback and input are highly valued.

Comments:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8427d09..a9c651a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,10 @@
 nylas-python Changelog
 ======================
 
+Unreleased
+----------------
+* Add support for `view` parameter in `Threads.search()`
+
 v5.14.1
 ----------------
 * Fix error when trying to iterate on list after calling count
diff --git a/nylas/client/restful_model_collection.py b/nylas/client/restful_model_collection.py
index 1ccb928..408e4e0 100644
--- a/nylas/client/restful_model_collection.py
+++ b/nylas/client/restful_model_collection.py
@@ -110,7 +110,9 @@ class RestfulModelCollection(object):
     def delete(self, id, data=None, **kwargs):
         return self.api._delete_resource(self.model_class, id, data=data, **kwargs)
 
-    def search(self, q, limit=None, offset=None):  # pylint: disable=invalid-name
+    def search(
+        self, q, limit=None, offset=None, view=None
+    ):  # pylint: disable=invalid-name
         from nylas.client.restful_models import (
             Message,
             Thread,
@@ -122,6 +124,8 @@ class RestfulModelCollection(object):
                 kwargs["limit"] = limit
             if offset is not None:
                 kwargs["offset"] = offset
+            if view is not None and self.model_class is Thread:
+                kwargs["view"] = view
             return self.api._get_resources(self.model_class, extra="search", **kwargs)
         else:
             raise Exception("Searching is only allowed on Thread and Message models")


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index 4eeeadd..8ead8d4 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1005,7 +1005,7 @@ def mock_thread_search_response(mocked_responses, api_url):
 
     mocked_responses.add(
         responses.GET,
-        api_url + "/threads/search?q=Helena",
+        re.compile(api_url + "/threads/search\?q=Helena.*"),
         body=response_body,
         status=200,
         content_type="application/json",
diff --git a/tests/test_search.py b/tests/test_search.py
index 52aa733..367d388 100644
--- a/tests/test_search.py
+++ b/tests/test_search.py
@@ -23,6 +23,20 @@ def test_search_messages_with_limit_offset(mocked_responses, api_client):
     assert request.path_url == "/messages/search?q=Pinot&limit=10&offset=0"
 
 
+@pytest.mark.usefixtures("mock_message_search_response")
+def test_search_messages_with_view_should_not_appear(mocked_responses, api_client):
+    api_client.messages.search("Pinot", view="expanded")
+    request = mocked_responses.calls[0].request
+    assert request.path_url == "/messages/search?q=Pinot"
+
+
+@pytest.mark.usefixtures("mock_thread_search_response")
+def test_search_messages_with_view_should_appear(mocked_responses, api_client):
+    api_client.threads.search("Helena", view="expanded")
+    request = mocked_responses.calls[0].request
+    assert request.path_url == "/threads/search?q=Helena&view=expanded"
+
+
 @pytest.mark.usefixtures("mock_message_search_response")
 def test_search_drafts(api_client):
     with pytest.raises(Exception):