Skip to content

Commit 7855334

Browse files
#221 Deliver a 2.0.0 version (#228)
* #226 Added the search messages by query endpoint (#227) * Bumped version to 2.0.0
1 parent 48a68f6 commit 7855334

File tree

5 files changed

+153
-2
lines changed

5 files changed

+153
-2
lines changed

docsrc/markdown/message_service.md

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ More precisely:
55
* [Get a message](https://developers.symphony.com/restapi/reference#get-message-v1)
66
* [Get messages](https://developers.symphony.com/restapi/reference#messages-v4)
77
* [Get message IDs by timestamp](https://developers.symphony.com/restapi/reference#get-message-ids-by-timestamp)
8+
* [Search messages](https://developers.symphony.com/restapi/reference#message-search-post)
89
* [Send message](https://developers.symphony.com/restapi/reference#create-message-v4)
910
* [Import messages](https://developers.symphony.com/restapi/reference#import-message-v4)
1011
* [Get attachment](https://developers.symphony.com/restapi/reference#attachment)

examples/services/messages.py

+5
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from symphony.bdk.core.config.loader import BdkConfigLoader
66
from symphony.bdk.core.service.message.model import Message
77
from symphony.bdk.core.symphony_bdk import SymphonyBdk
8+
from symphony.bdk.gen.agent_model.message_search_query import MessageSearchQuery
89

910

1011
async def run():
@@ -28,6 +29,10 @@ async def run():
2829
message = Message(content="<messageML>Hello, World!</messageML>", attachments=[(attachment, preview)])
2930
await message_service.blast_message([stream_id_1, stream_id_2], message)
3031

32+
async for m in await message_service.search_all_messages(MessageSearchQuery(text="some_text",
33+
stream_id=stream_id_1)):
34+
logging.debug(m.message_id)
35+
3136
logging.info("Obo example:")
3237
obo_auth_session = bdk.obo(username="username")
3338
async with bdk.obo_services(obo_auth_session) as obo_services:

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "symphony_bdk_python"
3-
version = "2.0b6"
3+
version = "2.0.0"
44
license = "Apache-2.0"
55
description = "Symphony Bot Development Kit for Python"
66
readme = "README.md"

symphony/bdk/core/service/message/message_service.py

+58-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
from typing import Union, List, Tuple, IO
1+
from typing import Union, List, Tuple, IO, AsyncGenerator
22

33
from symphony.bdk.core.auth.auth_session import AuthSession
44
from symphony.bdk.core.config.model.bdk_retry_config import BdkRetryConfig
55
from symphony.bdk.core.service.message.model import Message
66
from symphony.bdk.core.service.message.multi_attachments_messages_api import MultiAttachmentsMessagesApi
7+
from symphony.bdk.core.service.pagination import offset_based_pagination
78
from symphony.bdk.gen.agent_api.attachments_api import AttachmentsApi
9+
from symphony.bdk.gen.agent_model.message_search_query import MessageSearchQuery
810
from symphony.bdk.gen.agent_model.v4_import_response import V4ImportResponse
911
from symphony.bdk.gen.agent_model.v4_imported_message import V4ImportedMessage
1012
from symphony.bdk.gen.agent_model.v4_message import V4Message
@@ -419,3 +421,58 @@ async def get_message_relationships(
419421
"user_agent": ""
420422
}
421423
return await self._default_api.v1_admin_messages_message_id_metadata_relationships_get(**params)
424+
425+
@retry
426+
async def search_messages(self, query: MessageSearchQuery, sort_dir: str = "desc", skip: int = 0,
427+
limit: int = 50) -> List[V4Message]:
428+
"""Searches for messages in the context of a specified user, given an argument-based query and pagination
429+
attributes (skip and limit parameters).
430+
See: `Message Search (using POST) <https://developers.symphony.com/restapi/reference#message-search-post>`_
431+
432+
:param query: The search query arguments
433+
:param sort_dir: Sorting direction for response. Possible values are desc (default) and asc.
434+
:param skip: Number of messages to skip. Default: 0
435+
:param limit: Maximum number of messages to return. Default: 50
436+
:return: The list of matching messages in the stream
437+
"""
438+
MessageService._validate_message_search_query(query)
439+
params = {
440+
"session_token": await self._auth_session.session_token,
441+
"key_manager_token": await self._auth_session.key_manager_token,
442+
"query": query,
443+
"sort_dir": sort_dir,
444+
"skip": skip,
445+
"limit": limit
446+
}
447+
message_list = await self._messages_api.v1_message_search_post(**params)
448+
return message_list.value # endpoint returns empty list when no values found
449+
450+
async def search_all_messages(self, query: MessageSearchQuery, sort_dir: str = "desc", chunk_size: int = 50,
451+
max_number: int = None) -> AsyncGenerator[V4Message, None]:
452+
"""Searches for messages in the context of a specified user, given an argument-based query.
453+
See: `Message Search (using POST) <https://developers.symphony.com/restapi/reference#message-search-post>`_
454+
455+
:param query: The search query arguments
456+
:param sort_dir: Sorting direction for response. Possible values are desc (default) and asc.
457+
:param chunk_size: the maximum number of elements to retrieve in one underlying HTTP call
458+
:param max_number: the total maximum number of elements to retrieve
459+
:return: an asynchronous generator of matching messages
460+
"""
461+
462+
async def search_messages_one_page(skip, limit):
463+
return await self.search_messages(query, sort_dir, skip, limit)
464+
465+
return offset_based_pagination(search_messages_one_page, chunk_size, max_number)
466+
467+
@staticmethod
468+
def _validate_message_search_query(query: MessageSearchQuery):
469+
# Check streamType value among accepted ones if specified
470+
if query.stream_type is not None and query.stream_type not in ["CHAT", "IM", "MIM", "ROOM", "POST"]:
471+
raise ValueError(f"Wrong stream type {query.stream_type}. "
472+
f"Accepted values are: CHAT (1-1 instant messages and multi-party instant messages), "
473+
f"IM (1-1 instant message), MIM (multi-party instant message), "
474+
f"ROOM or POST (user profile wall posts)")
475+
476+
# Text queries require streamId to be provided
477+
if query.text is not None and query.stream_id is None:
478+
raise ValueError("Message text queries require a stream_id to be provided.")

tests/core/service/message/message_service_test.py

+88
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from symphony.bdk.core.service.message.model import Message
99
from symphony.bdk.core.service.message.multi_attachments_messages_api import MultiAttachmentsMessagesApi
1010
from symphony.bdk.gen.agent_api.attachments_api import AttachmentsApi
11+
from symphony.bdk.gen.agent_model.message_search_query import MessageSearchQuery
1112
from symphony.bdk.gen.agent_model.v4_import_response_list import V4ImportResponseList
1213
from symphony.bdk.gen.agent_model.v4_imported_message import V4ImportedMessage
1314
from symphony.bdk.gen.agent_model.v4_message import V4Message
@@ -324,3 +325,90 @@ async def test_get_message_relationships(mocked_api_client, message_service):
324325
assert message_relationships.message_id == "TYgOZ65dVsu3SeK7u2YdfH///o6fzBu"
325326
assert message_relationships.parent.message_id == "/rbLQW5UHKZffM0FlLO2rn///o6vTck"
326327
assert message_relationships.parent.relationship_type == "REPLY"
328+
329+
330+
@pytest.mark.asyncio
331+
async def test_search_messages_with_hashtag(mocked_api_client, message_service):
332+
mocked_api_client.call_api.return_value = \
333+
get_deserialized_object_from_resource(V4MessageList, "message_response/list_messages.json")
334+
335+
messages = await message_service.search_messages(MessageSearchQuery(hashtag="tag"))
336+
assert len(messages) == 1
337+
assert messages[0].message_id == "test-message1"
338+
339+
340+
@pytest.mark.asyncio
341+
@pytest.mark.parametrize("stream_type", ["CHAT", "IM", "MIM", "ROOM", "POST"])
342+
async def test_search_messages_with_valid_stream_type(mocked_api_client, message_service, stream_type):
343+
mocked_api_client.call_api.return_value = \
344+
get_deserialized_object_from_resource(V4MessageList, "message_response/list_messages.json")
345+
346+
messages = await message_service.search_messages(MessageSearchQuery(stream_type=stream_type))
347+
assert len(messages) == 1
348+
assert messages[0].message_id == "test-message1"
349+
350+
351+
@pytest.mark.asyncio
352+
async def test_search_messages_with_invalid_stream_type(mocked_api_client, message_service):
353+
mocked_api_client.call_api.return_value = \
354+
get_deserialized_object_from_resource(V4MessageList, "message_response/list_messages.json")
355+
356+
with pytest.raises(ValueError):
357+
await message_service.search_messages(MessageSearchQuery(stream_type="invalid"))
358+
359+
360+
@pytest.mark.asyncio
361+
async def test_search_messages_with_text_and_sid(mocked_api_client, message_service):
362+
mocked_api_client.call_api.return_value = \
363+
get_deserialized_object_from_resource(V4MessageList, "message_response/list_messages.json")
364+
365+
messages = await message_service.search_messages(MessageSearchQuery(text="some text", stream_id="sid"))
366+
assert len(messages) == 1
367+
assert messages[0].message_id == "test-message1"
368+
369+
370+
@pytest.mark.asyncio
371+
async def test_search_messages_with_text_and_no_sid(mocked_api_client, message_service):
372+
mocked_api_client.call_api.return_value = \
373+
get_deserialized_object_from_resource(V4MessageList, "message_response/list_messages.json")
374+
375+
with pytest.raises(ValueError):
376+
await message_service.search_messages(MessageSearchQuery(text="some text"))
377+
378+
379+
@pytest.mark.asyncio
380+
@pytest.mark.parametrize("stream_type", ["CHAT", "IM", "MIM", "ROOM", "POST"])
381+
async def test_search_messages_with_stream_type_text_and_sid(mocked_api_client, message_service, stream_type):
382+
mocked_api_client.call_api.return_value = \
383+
get_deserialized_object_from_resource(V4MessageList, "message_response/list_messages.json")
384+
385+
messages = await message_service.search_messages(
386+
MessageSearchQuery(text="some text", stream_id="sid", stream_type=stream_type))
387+
assert len(messages) == 1
388+
assert messages[0].message_id == "test-message1"
389+
390+
391+
@pytest.mark.asyncio
392+
@pytest.mark.parametrize("stream_type", ["CHAT", "IM", "MIM", "ROOM", "POST"])
393+
async def test_search_messages_with_stream_type_text_and_no_sid(mocked_api_client, message_service, stream_type):
394+
mocked_api_client.call_api.return_value = \
395+
get_deserialized_object_from_resource(V4MessageList, "message_response/list_messages.json")
396+
397+
with pytest.raises(ValueError):
398+
await message_service.search_messages(MessageSearchQuery(text="some text", stream_type=stream_type))
399+
400+
401+
@pytest.mark.asyncio
402+
async def test_search_all_messages(mocked_api_client, message_service):
403+
mocked_api_client.call_api.side_effect = \
404+
[get_deserialized_object_from_resource(V4MessageList, "message_response/list_messages.json"),
405+
V4MessageList(value=[])]
406+
chunk_size = 1
407+
408+
message_generator = await message_service.search_all_messages(MessageSearchQuery(hashtag="tag"),
409+
chunk_size=chunk_size)
410+
messages = [m async for m in message_generator]
411+
assert len(messages) == 1
412+
assert messages[0].message_id == "test-message1"
413+
414+
assert mocked_api_client.call_api.call_count == 2

0 commit comments

Comments
 (0)