Skip to content

Commit b44a026

Browse files
authored
GroupMe fetch methods return vertical model instances (#73)
Closes #63 Implements parsing for all the verticals supported by GroupMe: - BlockedUser - ChatBot - ConversationDirect - ConversationGroup Also added `name` field to ChatBot since bots can have names. Also modifies README with updated vertical-service support.
1 parent 7b6b70e commit b44a026

File tree

7 files changed

+664
-183
lines changed

7 files changed

+664
-183
lines changed

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -83,12 +83,12 @@ additional ✓ (as in the [Quickstart example](#quickstart-example) above).
8383

8484
| | GroupMe | Strava | Tumblr |
8585
| ------------------ | ------- | ------ | ------ |
86-
| BlockedUser | | | 👀 |
87-
| ChatBot | | | |
88-
| ConversationDirect | | | |
89-
| ConversationGroup | | | |
86+
| BlockedUser | | | 👀 |
87+
| ChatBot | | | |
88+
| ConversationDirect | | | |
89+
| ConversationGroup | | | |
9090
| Message | | | |
91-
| PhysicalActivity | | | |
91+
| PhysicalActivity | | | |
9292
| SocialPosting | | ✓✓ ||
9393

9494
The transfer services are defined in

src/pardner/services/groupme.py

Lines changed: 176 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import json
2+
from collections import defaultdict
23
from typing import Any, Iterable, Optional, override
34
from urllib.parse import parse_qs, urlparse
45

@@ -15,6 +16,7 @@
1516
ConversationGroupVertical,
1617
Vertical,
1718
)
19+
from pardner.verticals.sub_verticals import AssociatedMediaSubVertical
1820

1921

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

138-
def fetch_blocked_user_vertical(self, request_params: dict[str, Any] = {}) -> Any:
140+
def parse_blocked_user_vertical(self, raw_data: Any) -> BlockedUserVertical | None:
141+
"""
142+
Given the response from the API request, creates a
143+
:class:`BlockedUserVertical` model object, if possible.
144+
145+
:param raw_data: the JSON representation of the data returned by the request.
146+
147+
:returns: :class:`BlockedUserVertical` or ``None``, depending on whether it
148+
was possible to extract data from the response
149+
"""
150+
if not isinstance(raw_data, dict):
151+
return None
152+
raw_data_dict = defaultdict(dict, raw_data)
153+
return BlockedUserVertical(
154+
service=self._service_name,
155+
creator_user_id=raw_data_dict.get('user_id'),
156+
data_owner_id=raw_data_dict.get('user_id', self._user_id),
157+
blocked_user_id=raw_data_dict.get('blocked_user_id'),
158+
created_at=raw_data_dict.get('created_at'),
159+
)
160+
161+
def fetch_blocked_user_vertical(
162+
self, request_params: dict[str, Any] = {}
163+
) -> tuple[list[BlockedUserVertical | None], Any]:
139164
"""
140165
Sends a GET request to fetch the users blocked by the authenticated user.
141166
142-
:returns: a JSON object with the result of the request.
167+
:returns: two elements: the first, a list of :class:`BlockedUserVertical`s
168+
or ``None``, if unable to parse; the second, the raw response from making the
169+
request.
143170
"""
144171
blocked_users = self._fetch_resource_common('blocks', request_params)
145-
146172
if 'blocks' not in blocked_users:
147173
raise ValueError(
148174
f'Unexpected response format: {json.dumps(blocked_users, indent=2)}'
149175
)
176+
return [
177+
self.parse_blocked_user_vertical(blocked_dict)
178+
for blocked_dict in blocked_users['blocks']
179+
], blocked_users
180+
181+
def parse_chat_bot_vertical(self, raw_data: Any) -> ChatBotVertical | None:
182+
"""
183+
Given the response from the API request, creates a
184+
:class:`ChatBotVertical` model object, if possible.
185+
186+
:param raw_data: the JSON representation of the data returned by the request.
150187
151-
return blocked_users['blocks']
188+
:returns: :class:`ChatBotVertical` or ``None``, depending on whether it
189+
was possible to extract data from the response
190+
"""
191+
if not isinstance(raw_data, dict):
192+
return None
193+
raw_data_dict = defaultdict(dict, raw_data)
194+
user_id = raw_data_dict.get('user_id', self._user_id)
195+
return ChatBotVertical(
196+
service=self._service_name,
197+
service_object_id=raw_data_dict.get('bot_id'),
198+
creator_user_id=user_id,
199+
data_owner_id=user_id,
200+
name=raw_data_dict.get('name'),
201+
)
152202

153-
def fetch_chat_bot_vertical(self, request_params: dict[str, Any] = {}) -> Any:
203+
def fetch_chat_bot_vertical(
204+
self, request_params: dict[str, Any] = {}
205+
) -> tuple[list[ChatBotVertical | None], Any]:
154206
"""
155207
Sends a GET request to fetch the chat bots created by the authenticated user.
156208
157-
:returns: a JSON object with the result of the request.
209+
:returns: two elements: the first, a list of :class:`ChatBotVertical`s
210+
or ``None``, if unable to parse; the second, the raw response from making the
211+
request.
158212
"""
159-
return self._fetch_resource_common('bots', request_params)
213+
bots_response = self._fetch_resource_common('bots', request_params)
214+
if not isinstance(bots_response, list):
215+
raise ValueError(
216+
f'Unexpected response format: {json.dumps(bots_response, indent=2)}'
217+
)
218+
return [
219+
self.parse_chat_bot_vertical(chat_bot_data)
220+
for chat_bot_data in bots_response
221+
], bots_response
222+
223+
def parse_conversation_direct_vertical(
224+
self, raw_data: Any
225+
) -> ConversationDirectVertical | None:
226+
"""
227+
Given the response from the API request, creates a
228+
:class:`ConversationDirectVertical` model object, if possible.
229+
230+
:param raw_data: the JSON representation of the data returned by the request.
231+
232+
:returns: :class:`ConversationDirectVertical` or ``None``, depending on
233+
whether it was possible to extract data from the response
234+
"""
235+
if not isinstance(raw_data, dict):
236+
return None
237+
raw_data_dict = defaultdict(dict, raw_data)
238+
return ConversationDirectVertical(
239+
service=self._service_name,
240+
service_object_id=raw_data_dict.get('id'),
241+
data_owner_id=self._user_id,
242+
member_user_ids=[self._user_id, raw_data_dict['other_user'].get('id')],
243+
messages_count=raw_data_dict.get('messages_count'),
244+
created_at=raw_data_dict.get('created_at'),
245+
)
160246

161247
def fetch_conversation_direct_vertical(
162248
self, request_params: dict[str, Any] = {}, count: int = 10
163-
) -> Any:
249+
) -> tuple[list[ConversationDirectVertical | None], Any]:
164250
"""
165251
Sends a GET request to fetch the conversations the authenticated user is a part
166252
of with only one other member (i.e., a direct message). The response will
@@ -169,15 +255,72 @@ def fetch_conversation_direct_vertical(
169255
170256
:param count: the number of conversations to fetch. Defaults to 10.
171257
172-
:returns: a JSON object with the result of the request.
258+
:returns: two elements: the first, a list of
259+
:class:`ConversationDirectVertical`s or ``None``, if unable to parse; the
260+
second, the raw response from making the request.
173261
"""
174-
if count <= 10:
175-
return self._fetch_resource_common(
176-
'chats', params={**request_params, 'per_page': count}
262+
if count > 10:
263+
raise UnsupportedRequestException(
264+
self._service_name,
265+
'can only make a request for at most 10 direct conversations at a time.',
177266
)
178-
raise UnsupportedRequestException(
179-
self._service_name,
180-
'can only make a request for at most 10 direct conversations at a time.',
267+
conversation_direct_raw_response = self._fetch_resource_common(
268+
'chats', params={**request_params, 'per_page': count}
269+
)
270+
if not isinstance(conversation_direct_raw_response, list):
271+
raise ValueError(
272+
'Unexpected response format. Expected list, '
273+
f'got: {json.dumps(conversation_direct_raw_response, indent=2)}'
274+
)
275+
return [
276+
self.parse_conversation_direct_vertical(conversation_direct_data)
277+
for conversation_direct_data in conversation_direct_raw_response
278+
], conversation_direct_raw_response
279+
280+
def parse_conversation_group_vertical(
281+
self, raw_data: Any
282+
) -> ConversationGroupVertical | None:
283+
"""
284+
Given the response from the API request, creates a
285+
:class:`ConversationGroupVertical` model object, if possible.
286+
287+
:param raw_data: the JSON representation of the data returned by the request.
288+
289+
:returns: :class:`ConversationGroupVertical` or ``None``, depending on
290+
whether it was possible to extract data from the response
291+
"""
292+
if not isinstance(raw_data, dict):
293+
return None
294+
raw_data_dict = defaultdict(dict, raw_data)
295+
296+
members_list = raw_data_dict.get('members', [])
297+
member_user_ids = []
298+
for member in members_list:
299+
if isinstance(member, dict) and 'user_id' in member:
300+
member_user_ids.append(member['user_id'])
301+
302+
associated_media = []
303+
image_url = raw_data_dict.get('image_url', None)
304+
if image_url:
305+
associated_media = [AssociatedMediaSubVertical(image_url=image_url)]
306+
307+
is_private = None
308+
conversation_type = raw_data_dict.get('type')
309+
if isinstance(conversation_type, str):
310+
is_private = conversation_type == 'private'
311+
312+
return ConversationGroupVertical(
313+
service=self._service_name,
314+
service_object_id=raw_data_dict.get('id'),
315+
data_owner_id=self._user_id,
316+
creator_user_id=raw_data_dict.get('creator_user_id'),
317+
title=raw_data_dict.get('name'),
318+
member_user_ids=member_user_ids,
319+
members_count=len(members_list),
320+
messages_count=raw_data_dict['messages'].get('count'),
321+
associated_media=associated_media,
322+
created_at=raw_data_dict.get('created_at'),
323+
is_private=is_private,
181324
)
182325

183326
def fetch_conversation_group_vertical(
@@ -190,13 +333,24 @@ def fetch_conversation_group_vertical(
190333
191334
:param count: the number of conversations to fetch. Defaults to 10.
192335
193-
:returns: a JSON object with the result of the request.
336+
:returns: two elements: the first, a list of
337+
:class:`ConversationGroupVertical`s or ``None``, if unable to parse; the
338+
second, the raw response from making the request.
194339
"""
195-
if count <= 10:
196-
return self._fetch_resource_common(
197-
'groups', params={**request_params, 'per_page': count}
340+
if count > 10:
341+
raise UnsupportedRequestException(
342+
self._service_name,
343+
'can only make a request for at most 10 group conversations at a time.',
198344
)
199-
raise UnsupportedRequestException(
200-
self._service_name,
201-
'can only make a request for at most 10 group conversations at a time.',
345+
conversation_group_raw_response = self._fetch_resource_common(
346+
'groups', params={**request_params, 'per_page': count}
202347
)
348+
if not isinstance(conversation_group_raw_response, list):
349+
raise ValueError(
350+
'Unexpected response format. Expected list, '
351+
f'got: {json.dumps(conversation_group_raw_response, indent=2)}'
352+
)
353+
return [
354+
self.parse_conversation_group_vertical(conversation_group_data)
355+
for conversation_group_data in conversation_group_raw_response
356+
], conversation_group_raw_response

src/pardner/verticals/chat_bot.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,5 @@ class ChatBotVertical(BaseVertical):
55
"""An instance of a chatbot created by ``creator_user_id``."""
66

77
vertical_name: str = 'chat_bot'
8+
9+
name: str | None = None

tests/test_transfer_services/conftest.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def mock_strava_transfer_service(verticals=[PhysicalActivityVertical]):
6868

6969

7070
@pytest.fixture
71-
def mock_groupme_transfer_service(
71+
def groupme_transfer_service(
7272
verticals=[BlockedUserVertical, ConversationDirectVertical],
7373
):
7474
groupme = GroupMeTransferService(
@@ -123,3 +123,14 @@ def mock_oauth2_session_get(mocker, response_object=None):
123123
oauth2_session_get = mocker.patch.object(OAuth2Session, 'get', autospec=True)
124124
oauth2_session_get.return_value = response_object
125125
return oauth2_session_get
126+
127+
128+
def dump_and_filter_model_objs(model_objs):
129+
"""
130+
``pardner_object_id`` is auto-generated by pardner so we don't need to compare
131+
it when testing.
132+
"""
133+
model_obj_dumps = [model_obj.model_dump() for model_obj in model_objs]
134+
for model_obj_dump in model_obj_dumps:
135+
del model_obj_dump['pardner_object_id']
136+
return model_obj_dumps

0 commit comments

Comments
 (0)