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
1 change: 1 addition & 0 deletions src/pardner/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
from pardner import exceptions as exceptions
from pardner import services as services
from pardner import verticals as verticals
24 changes: 24 additions & 0 deletions src/pardner/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from pardner.verticals import Vertical


class InsufficientScopeException(Exception):
def __init__(self, *unsupported_verticals: Vertical, service_name: str) -> None:
combined_verticals = ', '.join(unsupported_verticals)
super().__init__(
f'Cannot add {combined_verticals} to {service_name} with current scope.'
)


class UnsupportedVerticalException(Exception):
def __init__(self, *unsupported_verticals: Vertical, service_name: str) -> None:
combined_verticals = ', '.join(unsupported_verticals)
is_more_than_one_vertical = len(unsupported_verticals) > 1
super().__init__(
f'Cannot fetch {combined_verticals} from {service_name} because '
f'{"they are" if is_more_than_one_vertical else "it is"} not supported.'
)


class UnsupportedRequestException(Exception):
def __init__(self, service_name: str, message: str):
super().__init__(f'Cannot fetch data from {service_name}: {message}')
69 changes: 41 additions & 28 deletions src/pardner/services/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,35 +5,11 @@
from requests import Response
from requests_oauthlib import OAuth2Session

from pardner.exceptions import InsufficientScopeException, UnsupportedVerticalException
from pardner.services.utils import scope_as_set, scope_as_string
from pardner.verticals import Vertical


class InsufficientScopeException(Exception):
def __init__(
self, unsupported_verticals: Iterable[Vertical], service_name: str
) -> None:
combined_verticals = ' '.join(unsupported_verticals)
super().__init__(
f'Cannot add {combined_verticals} to {service_name} with current scope.'
)


class UnsupportedVerticalException(Exception):
def __init__(
self, unsupported_verticals: Iterable[Vertical], service_name: str
) -> None:
combined_verticals = ' '.join(unsupported_verticals)
super().__init__(
f'Cannot add {combined_verticals} to {service_name} because they are not supported.'
)


class UnsupportedRequestException(Exception):
def __init__(self, service_name: str, message: str):
super().__init__(f'Cannot fetch data from {service_name}: {message}')


class BaseTransferService(ABC):
"""
A base class to be extended by service-specific classes that implement logic for
Expand Down Expand Up @@ -118,10 +94,12 @@ def verticals(self, verticals: Iterable[Vertical]) -> None:
unsupported_verticals = [
vertical
for vertical in verticals
if vertical not in self._supported_verticals
if not self.is_vertical_supported(vertical)
]
if len(unsupported_verticals) > 0:
raise UnsupportedVerticalException(unsupported_verticals, self.name)
raise UnsupportedVerticalException(
*unsupported_verticals, service_name=self.name
)
self._verticals = set(verticals)

def _get_resource(self, uri: str, params: dict[str, Any] = {}) -> Response:
Expand Down Expand Up @@ -192,7 +170,7 @@ def add_verticals(
new_scopes = self.scope_for_verticals(new_verticals)

if not new_scopes.issubset(self.scope) and not should_reauth:
raise InsufficientScopeException(verticals, self.name)
raise InsufficientScopeException(*verticals, service_name=self.name)
elif not new_scopes.issubset(self.scope):
self.verticals = new_verticals | self.verticals
del self._oAuth2Session.access_token
Expand All @@ -202,6 +180,16 @@ def add_verticals(
self.verticals = new_verticals | self.verticals
return True

def is_vertical_supported(self, vertical: Vertical) -> bool:
"""
Utility for indicating whether ``vertical`` is supported by the service or not.

:param vertical: the ``vertical`` from which validity is checked.

:returns: ``True`` if supported, ``False`` otherwise.
"""
return vertical in self._supported_verticals

def fetch_token(
self,
code: Optional[str] = None,
Expand Down Expand Up @@ -252,3 +240,28 @@ def scope_for_verticals(self, verticals: Iterable[Vertical]) -> set[str]:
:returns: Scope names corresponding to `verticals`.
"""
pass

def fetch(
self, vertical: Vertical, request_params: dict[str, Any] = {}, **params: Any
) -> Any:
"""
Generic method for fetching data of a specific vertical.

:param vertical: the :class:`Vertical` to fetch from the
service.
:param request_params: additional request parameters to be sent with the HTTP
request. Requires familiarity with the API of the service being used.
:param params: optional keyword arguments to be passed to the methods for
fetching ``vertical``.

:returns: the JSON response resulting from making the request.

:raises: :class:`UnsupportedVerticalException` if the vertical is not supported.
"""
if not self.is_vertical_supported(vertical):
raise UnsupportedVerticalException(
vertical, service_name=self._service_name
)

method_name = f'fetch_{vertical.plural}'
getattr(self, method_name)(request_params=request_params, **params)
35 changes: 24 additions & 11 deletions src/pardner/services/groupme.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
from requests import Response
from requests_oauthlib import OAuth2Session

from pardner.services.base import BaseTransferService, UnsupportedRequestException
from pardner.exceptions import UnsupportedRequestException
from pardner.services import BaseTransferService
from pardner.verticals import Vertical


Expand Down Expand Up @@ -113,24 +114,28 @@ def scope_for_verticals(self, verticals: Iterable[Vertical]) -> set[str]:
# GroupMe does not require scope
return set()

def fetch_user_data(self) -> Any:
def fetch_user_data(self, request_params: dict[str, Any] = {}) -> Any:
"""
Fetches user identifiers and profile data. Also sets ``self._user_id``, which
is necessary for most requests.

:returns: user identifiers and profile data in dictionary format.
"""
user_data = self._get_resource_from_path('users/me').json().get('response')
user_data = (
self._get_resource_from_path('users/me', request_params)
.json()
.get('response')
)
self._user_id = user_data['id']
return user_data

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

:returns: a JSON object with the result of the request.
"""
blocked_users = self._fetch_resource_common('blocks')
blocked_users = self._fetch_resource_common('blocks', request_params)

if 'blocks' not in blocked_users:
raise ValueError(
Expand All @@ -139,15 +144,17 @@ def fetch_blocked_users(self) -> Any:

return blocked_users['blocks']

def fetch_chat_bots(self) -> Any:
def fetch_chat_bots(self, request_params: dict[str, Any] = {}) -> 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.
"""
return self._fetch_resource_common('bots')
return self._fetch_resource_common('bots', request_params)

def fetch_conversations_direct(self, count: int = 10) -> Any:
def fetch_conversations_direct(
self, request_params: dict[str, Any] = {}, count: int = 10
) -> 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 @@ -159,13 +166,17 @@ def fetch_conversations_direct(self, count: int = 10) -> Any:
:returns: a JSON object with the result of the request.
"""
if count <= 10:
return self._fetch_resource_common('chats', params={'per_page': count})
return self._fetch_resource_common(
'chats', params={**request_params, 'per_page': count}
)
raise UnsupportedRequestException(
self._service_name,
'can only make a request for at most 10 direct conversations at a time.',
)

def fetch_conversations_group(self, count: int = 10) -> Any:
def fetch_conversations_group(
self, request_params: dict[str, Any] = {}, count: int = 10
) -> Any:
"""
Sends a GET request to fetch the group conversations the authenticated user is
a part of. The response will include metadata associated with the conversation,
Expand All @@ -176,7 +187,9 @@ def fetch_conversations_group(self, count: int = 10) -> Any:
:returns: a JSON object with the result of the request.
"""
if count <= 10:
return self._fetch_resource_common('groups', params={'per_page': count})
return self._fetch_resource_common(
'groups', params={**request_params, 'per_page': count}
)
raise UnsupportedRequestException(
self._service_name,
'can only make a request for at most 10 group conversations at a time.',
Expand Down
17 changes: 8 additions & 9 deletions src/pardner/services/strava.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
from typing import Any, Iterable, Optional, override

from pardner.services.base import (
BaseTransferService,
UnsupportedRequestException,
UnsupportedVerticalException,
)
from pardner.exceptions import UnsupportedRequestException, UnsupportedVerticalException
from pardner.services import BaseTransferService
from pardner.services.utils import scope_as_set, scope_as_string
from pardner.verticals import Vertical

Expand Down Expand Up @@ -59,14 +56,16 @@ def fetch_token(
def scope_for_verticals(self, verticals: Iterable[Vertical]) -> set[str]:
sub_scopes: set[str] = set()
for vertical in verticals:
if vertical not in self._supported_verticals:
raise UnsupportedVerticalException([vertical], self._service_name)
if not self.is_vertical_supported(vertical):
raise UnsupportedVerticalException(
vertical, service_name=self._service_name
)
if vertical == Vertical.PhysicalActivity:
sub_scopes.update(['activity:read', 'profile:read_all'])
return sub_scopes

def fetch_athlete_activities(
self, count: int = 30, request_params: dict[str, Any] = {}
def fetch_physical_activities(
self, request_params: dict[str, Any] = {}, count: int = 30
) -> list[Any]:
"""
Fetches and returns activities completed by the authorized user.
Expand Down
4 changes: 2 additions & 2 deletions src/pardner/services/tumblr.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from typing import Any, Iterable, Optional, override

from pardner.exceptions import UnsupportedRequestException
from pardner.services import BaseTransferService
from pardner.services.base import UnsupportedRequestException
from pardner.verticals import Vertical


Expand Down Expand Up @@ -50,9 +50,9 @@ def fetch_token(

def fetch_feed_posts(
self,
request_params: dict[str, Any] = {},
count: int = 20,
text_only: bool = True,
request_params: dict[str, Any] = {},
) -> list[Any]:
"""
Fetches posts from Tumblr feed for user account whose token was
Expand Down
17 changes: 13 additions & 4 deletions src/pardner/verticals/base.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
from enum import StrEnum
from typing import Optional


class Vertical(StrEnum):
"""
Represents the verticals, or categories of data, that are supported by `pardner`.
Represents the verticals, or categories of data, that are supported by this library.
Not all verticals are supported by every transfer service.
"""

BlockedUser = 'blocked_user'
ChatBot = 'chat_bot'
ConversationDirect = 'conversation_direct'
ConversationGroup = 'conversation_group'
ConversationDirect = 'conversation_direct', 'conversations_direct'
ConversationGroup = 'conversation_group', 'conversations_group'
ConversationMessage = 'conversation_message'
FeedPost = 'feed_post'
PhysicalActivity = 'physical_activity'
PhysicalActivity = 'physical_activity', 'physical_activities'

plural: str

def __new__(cls, singular: str, plural: Optional[str] = None) -> 'Vertical':
vertical_obj = str.__new__(cls, singular)
vertical_obj._value_ = singular
vertical_obj.plural = plural if plural else f'{singular}s'
return vertical_obj
29 changes: 29 additions & 0 deletions tests/test_transfer_services/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,18 @@
# FIXTURES


@pytest.fixture
def mock_nested_dict(mocker):
def nested_dict():
mock_dict = mocker.MagicMock(spec=dict, name='mock_nested_dict')
mock_dict.__getitem__.side_effect = lambda _: nested_dict()
mock_dict.get.side_effect = lambda _, default=None: nested_dict()
mock_dict.__contains__.side_effect = lambda _: True
return mock_dict

return nested_dict()


@pytest.fixture
def mock_oauth2_session_request(mocker):
return mocker.patch('requests_oauthlib.OAuth2Session.request')
Expand Down Expand Up @@ -76,6 +88,23 @@ def mock_oauth2_session_get_bad_response(mocker, mock_oauth2_bad_response):
return oauth2_session_get


@pytest.fixture
def mock_oauth2_good_response(mocker, mock_nested_dict):
mock_response = mocker.create_autospec(Response)
mock_response.ok = True
mock_response.status_code = 200
mock_response.url = 'fake url'
mock_response.json.return_value = mock_nested_dict
return mock_response


@pytest.fixture
def mock_oauth2_session_get_good_response(mocker, mock_oauth2_good_response):
oauth2_session_get = mocker.patch.object(OAuth2Session, 'get', autospec=True)
oauth2_session_get.return_value = mock_oauth2_good_response
return oauth2_session_get


# HELPERS


Expand Down
2 changes: 1 addition & 1 deletion tests/test_transfer_services/test_groupme.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import pytest

from pardner.services.base import UnsupportedRequestException
from pardner.exceptions import UnsupportedRequestException
from tests.test_transfer_services.conftest import mock_oauth2_session_get

USER_ID = 'fake_user_id'
Expand Down
Loading