Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,12 @@ additional ✓ (as in the [Quickstart example](#quickstart-example) above).

| | GroupMe | Strava | Tumblr |
| ------------------ | ------- | ------ | ------ |
| BlockedUser | ✓ | | 👀 |
| ChatBot | ✓ | | |
| ConversationDirect | ✓ | | |
| ConversationGroup | ✓ | | |
| BlockedUser | ✓ | | 👀 |
| ChatBot | ✓ | | |
| ConversationDirect | ✓ | | |
| ConversationGroup | ✓ | | |
| Message | | | |
| PhysicalActivity | | ✓ | |
| PhysicalActivity | | ✓ | |
| SocialPosting | | ✓✓ | ✓ |

The transfer services are defined in
Expand Down
198 changes: 176 additions & 22 deletions src/pardner/services/groupme.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import json
from collections import defaultdict
from typing import Any, Iterable, Optional, override
from urllib.parse import parse_qs, urlparse

Expand All @@ -15,6 +16,7 @@
ConversationGroupVertical,
Vertical,
)
from pardner.verticals.sub_verticals import AssociatedMediaSubVertical


class GroupMeTransferService(BaseTransferService):
Expand Down Expand Up @@ -135,32 +137,116 @@ def fetch_user_data(self, request_params: dict[str, Any] = {}) -> Any:
self._user_id = user_data['id']
return user_data

def fetch_blocked_user_vertical(self, request_params: dict[str, Any] = {}) -> Any:
def parse_blocked_user_vertical(self, raw_data: Any) -> BlockedUserVertical | None:
"""
Given the response from the API request, creates a
:class:`BlockedUserVertical` model object, if possible.

:param raw_data: the JSON representation of the data returned by the request.

:returns: :class:`BlockedUserVertical` or ``None``, depending on whether it
was possible to extract data from the response
"""
if not isinstance(raw_data, dict):
return None
raw_data_dict = defaultdict(dict, raw_data)
return BlockedUserVertical(
service=self._service_name,
creator_user_id=raw_data_dict.get('user_id'),
data_owner_id=raw_data_dict.get('user_id', self._user_id),
blocked_user_id=raw_data_dict.get('blocked_user_id'),
created_at=raw_data_dict.get('created_at'),
)

def fetch_blocked_user_vertical(
self, request_params: dict[str, Any] = {}
) -> tuple[list[BlockedUserVertical | None], Any]:
"""
Sends a GET request to fetch the users blocked by the authenticated user.

:returns: a JSON object with the result of the request.
:returns: two elements: the first, a list of :class:`BlockedUserVertical`s
or ``None``, if unable to parse; the second, the raw response from making the
request.
"""
blocked_users = self._fetch_resource_common('blocks', request_params)

if 'blocks' not in blocked_users:
raise ValueError(
f'Unexpected response format: {json.dumps(blocked_users, indent=2)}'
)
return [
self.parse_blocked_user_vertical(blocked_dict)
for blocked_dict in blocked_users['blocks']
], blocked_users

def parse_chat_bot_vertical(self, raw_data: Any) -> ChatBotVertical | None:
"""
Given the response from the API request, creates a
:class:`ChatBotVertical` model object, if possible.

:param raw_data: the JSON representation of the data returned by the request.

return blocked_users['blocks']
:returns: :class:`ChatBotVertical` or ``None``, depending on whether it
was possible to extract data from the response
"""
if not isinstance(raw_data, dict):
return None
raw_data_dict = defaultdict(dict, raw_data)
user_id = raw_data_dict.get('user_id', self._user_id)
return ChatBotVertical(
service=self._service_name,
service_object_id=raw_data_dict.get('bot_id'),
creator_user_id=user_id,
data_owner_id=user_id,
name=raw_data_dict.get('name'),
)

def fetch_chat_bot_vertical(self, request_params: dict[str, Any] = {}) -> Any:
def fetch_chat_bot_vertical(
self, request_params: dict[str, Any] = {}
) -> tuple[list[ChatBotVertical | None], Any]:
"""
Sends a GET request to fetch the chat bots created by the authenticated user.

:returns: a JSON object with the result of the request.
:returns: two elements: the first, a list of :class:`ChatBotVertical`s
or ``None``, if unable to parse; the second, the raw response from making the
request.
"""
return self._fetch_resource_common('bots', request_params)
bots_response = self._fetch_resource_common('bots', request_params)
if not isinstance(bots_response, list):
raise ValueError(
f'Unexpected response format: {json.dumps(bots_response, indent=2)}'
)
return [
self.parse_chat_bot_vertical(chat_bot_data)
for chat_bot_data in bots_response
], bots_response

def parse_conversation_direct_vertical(
self, raw_data: Any
) -> ConversationDirectVertical | None:
"""
Given the response from the API request, creates a
:class:`ConversationDirectVertical` model object, if possible.

:param raw_data: the JSON representation of the data returned by the request.

:returns: :class:`ConversationDirectVertical` or ``None``, depending on
whether it was possible to extract data from the response
"""
if not isinstance(raw_data, dict):
return None
raw_data_dict = defaultdict(dict, raw_data)
return ConversationDirectVertical(
service=self._service_name,
service_object_id=raw_data_dict.get('id'),
data_owner_id=self._user_id,
member_user_ids=[self._user_id, raw_data_dict['other_user'].get('id')],
messages_count=raw_data_dict.get('messages_count'),
created_at=raw_data_dict.get('created_at'),
)

def fetch_conversation_direct_vertical(
self, request_params: dict[str, Any] = {}, count: int = 10
) -> Any:
) -> tuple[list[ConversationDirectVertical | None], Any]:
"""
Sends a GET request to fetch the conversations the authenticated user is a part
of with only one other member (i.e., a direct message). The response will
Expand All @@ -169,15 +255,72 @@ def fetch_conversation_direct_vertical(

:param count: the number of conversations to fetch. Defaults to 10.

:returns: a JSON object with the result of the request.
:returns: two elements: the first, a list of
:class:`ConversationDirectVertical`s or ``None``, if unable to parse; the
second, the raw response from making the request.
"""
if count <= 10:
return self._fetch_resource_common(
'chats', params={**request_params, 'per_page': count}
if count > 10:
raise UnsupportedRequestException(
self._service_name,
'can only make a request for at most 10 direct conversations at a time.',
)
raise UnsupportedRequestException(
self._service_name,
'can only make a request for at most 10 direct conversations at a time.',
conversation_direct_raw_response = self._fetch_resource_common(
'chats', params={**request_params, 'per_page': count}
)
if not isinstance(conversation_direct_raw_response, list):
raise ValueError(
'Unexpected response format. Expected list, '
f'got: {json.dumps(conversation_direct_raw_response, indent=2)}'
)
return [
self.parse_conversation_direct_vertical(conversation_direct_data)
for conversation_direct_data in conversation_direct_raw_response
], conversation_direct_raw_response

def parse_conversation_group_vertical(
self, raw_data: Any
) -> ConversationGroupVertical | None:
"""
Given the response from the API request, creates a
:class:`ConversationGroupVertical` model object, if possible.

:param raw_data: the JSON representation of the data returned by the request.

:returns: :class:`ConversationGroupVertical` or ``None``, depending on
whether it was possible to extract data from the response
"""
if not isinstance(raw_data, dict):
return None
raw_data_dict = defaultdict(dict, raw_data)

members_list = raw_data_dict.get('members', [])
member_user_ids = []
for member in members_list:
if isinstance(member, dict) and 'user_id' in member:
member_user_ids.append(member['user_id'])

associated_media = []
image_url = raw_data_dict.get('image_url', None)
if image_url:
associated_media = [AssociatedMediaSubVertical(image_url=image_url)]

is_private = None
conversation_type = raw_data_dict.get('type')
if isinstance(conversation_type, str):
is_private = conversation_type == 'private'

return ConversationGroupVertical(
service=self._service_name,
service_object_id=raw_data_dict.get('id'),
data_owner_id=self._user_id,
creator_user_id=raw_data_dict.get('creator_user_id'),
title=raw_data_dict.get('name'),
member_user_ids=member_user_ids,
members_count=len(members_list),
messages_count=raw_data_dict['messages'].get('count'),
associated_media=associated_media,
created_at=raw_data_dict.get('created_at'),
is_private=is_private,
)

def fetch_conversation_group_vertical(
Expand All @@ -190,13 +333,24 @@ def fetch_conversation_group_vertical(

:param count: the number of conversations to fetch. Defaults to 10.

:returns: a JSON object with the result of the request.
:returns: two elements: the first, a list of
:class:`ConversationGroupVertical`s or ``None``, if unable to parse; the
second, the raw response from making the request.
"""
if count <= 10:
return self._fetch_resource_common(
'groups', params={**request_params, 'per_page': count}
if count > 10:
raise UnsupportedRequestException(
self._service_name,
'can only make a request for at most 10 group conversations at a time.',
)
raise UnsupportedRequestException(
self._service_name,
'can only make a request for at most 10 group conversations at a time.',
conversation_group_raw_response = self._fetch_resource_common(
'groups', params={**request_params, 'per_page': count}
)
if not isinstance(conversation_group_raw_response, list):
raise ValueError(
'Unexpected response format. Expected list, '
f'got: {json.dumps(conversation_group_raw_response, indent=2)}'
)
return [
self.parse_conversation_group_vertical(conversation_group_data)
for conversation_group_data in conversation_group_raw_response
], conversation_group_raw_response
2 changes: 2 additions & 0 deletions src/pardner/verticals/chat_bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ class ChatBotVertical(BaseVertical):
"""An instance of a chatbot created by ``creator_user_id``."""

vertical_name: str = 'chat_bot'

name: str | None = None
13 changes: 12 additions & 1 deletion tests/test_transfer_services/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ def mock_strava_transfer_service(verticals=[PhysicalActivityVertical]):


@pytest.fixture
def mock_groupme_transfer_service(
def groupme_transfer_service(
verticals=[BlockedUserVertical, ConversationDirectVertical],
):
groupme = GroupMeTransferService(
Expand Down Expand Up @@ -123,3 +123,14 @@ def mock_oauth2_session_get(mocker, response_object=None):
oauth2_session_get = mocker.patch.object(OAuth2Session, 'get', autospec=True)
oauth2_session_get.return_value = response_object
return oauth2_session_get


def dump_and_filter_model_objs(model_objs):
"""
``pardner_object_id`` is auto-generated by pardner so we don't need to compare
it when testing.
"""
model_obj_dumps = [model_obj.model_dump() for model_obj in model_objs]
for model_obj_dump in model_obj_dumps:
del model_obj_dump['pardner_object_id']
return model_obj_dumps
Loading