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
6 changes: 6 additions & 0 deletions src/pardner/services/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,19 @@ def __init__(
)


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
OAuth 2.0 and data transfers.
"""

_authorization_url: str
_base_url: str
_client_secret: str
_oAuth2Session: OAuth2Session
_service_name: str
Expand Down
44 changes: 44 additions & 0 deletions src/pardner/services/tumblr.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from typing import Any, Iterable, Optional, override
from urllib.parse import urljoin

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


Expand All @@ -12,6 +14,7 @@ class TumblrTransferService(BaseTransferService):
"""

_authorization_url = 'https://www.tumblr.com/oauth2/authorize'
_base_url = 'https://api.tumblr.com/'
_token_url = 'https://api.tumblr.com/v2/oauth2/token'

def __init__(
Expand Down Expand Up @@ -45,3 +48,44 @@ def fetch_token(
include_client_id: bool = True,
) -> dict[str, Any]:
return super().fetch_token(code, authorization_response, include_client_id)

def fetch_feed_posts(
self,
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
obtained using the Tumblr API.

:param count: number of posts to request.
:param text_only: whether or not to request only text-based posts (`True`) or
not (`False).
:param request_params: any other endpoint-specific parameters to be sent
to the endpoint. Depending on the parameters passed, this could override
`count` and `text_only`.

:returns: a list of dictionary objects with information for the posts in a feed.

:raises: :class:`UnsupportedRequestException` if the request is unable to be
made.
"""
dashboard_uri = urljoin(self._base_url, '/v2/user/dashboard')
if count <= 20:
dashboard_response = self._oAuth2Session.get(
dashboard_uri,
params={
'limit': count,
'npf': True,
'type': 'text' if text_only else '',
**request_params,
},
)
if not dashboard_response.ok:
dashboard_response.raise_for_status()
return list(dashboard_response.json().get('response').get('posts'))
raise UnsupportedRequestException(
self._service_name,
'can only make a request for at most 20 posts at a time.',
)
18 changes: 14 additions & 4 deletions tests/test_transfer_services/conftest.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
import pytest
from requests_oauthlib import OAuth2Session

from pardner.services.strava import StravaTransferService
from pardner.services.tumblr import TumblrTransferService
from pardner.verticals.base import Vertical


@pytest.fixture
def mock_oAuth2Session(mocker):
mock_oauth2session_request = mocker.patch('requests_oauthlib.OAuth2Session.request')
mock_client_parse_request_body_response = mocker.patch(
def mock_oauth2_session_request(mocker):
return mocker.patch('requests_oauthlib.OAuth2Session.request')


@pytest.fixture
def mock_oauth2_session_response(mocker):
return mocker.patch(
'oauthlib.oauth2.rfc6749.clients.WebApplicationClient.parse_request_body_response'
)
return [mock_oauth2session_request, mock_client_parse_request_body_response]


@pytest.fixture
Expand All @@ -32,3 +36,9 @@ def mock_strava_transfer_service(verticals=[Vertical.FeedPost]):
return StravaTransferService(
'fake_client_id', 'fake_client_secret', 'https://redirect_uri', None, verticals
)


def mock_oauth2_session_get(mocker, response_object):
oauth2_session_get = mocker.patch.object(OAuth2Session, 'get', autospec=True)
oauth2_session_get.return_value = response_object
return oauth2_session_get
11 changes: 7 additions & 4 deletions tests/test_transfer_services/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,11 @@ def test_fetch_token_raises_error(mock_transfer_service):
mock_transfer_service.fetch_token()


def test_fetch_token(mock_oAuth2Session, mock_strava_transfer_service):
[mock_request, mock_response] = mock_oAuth2Session
def test_fetch_token(
mock_oauth2_session_request,
mock_oauth2_session_response,
mock_strava_transfer_service,
):
mock_strava_transfer_service.fetch_token(code='123code123')
mock_request.assert_called_once()
mock_response.assert_called_once()
mock_oauth2_session_request.assert_called_once()
mock_oauth2_session_response.assert_called_once()
17 changes: 12 additions & 5 deletions tests/test_transfer_services/test_transfer_services_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,17 @@
'mock_transfer_service_name',
['mock_tumblr_transfer_service', 'mock_strava_transfer_service'],
)
def test_fetch_token(mock_oAuth2Session, request, mock_transfer_service_name):
[mock_request, _] = mock_oAuth2Session
def test_fetch_token(
mock_oauth2_session_request,
mock_oauth2_session_response,
request,
mock_transfer_service_name,
):
mock_transfer_service = request.getfixturevalue(mock_transfer_service_name)
mock_transfer_service.fetch_token(code='123code123')
mock_request.assert_called_once()
assert 'client_id' in mock_request.call_args.kwargs['data']
assert mock_request.call_args.kwargs['data']['client_id'] == 'fake_client_id'
mock_oauth2_session_request.assert_called_once()
assert 'client_id' in mock_oauth2_session_request.call_args.kwargs['data']
assert (
mock_oauth2_session_request.call_args.kwargs['data']['client_id']
== 'fake_client_id'
)
35 changes: 35 additions & 0 deletions tests/test_transfer_services/test_tumblr.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import pytest
from requests import HTTPError, Response

from pardner.services.base import UnsupportedRequestException
from pardner.verticals import Vertical
from tests.test_transfer_services.conftest import mock_oauth2_session_get

sample_scope = {'fake', 'scope'}

Expand All @@ -10,3 +13,35 @@
)
def test_scope_for_vertical(mock_tumblr_transfer_service, verticals, expected_scope):
assert mock_tumblr_transfer_service.scope_for_verticals(verticals) == expected_scope


def test_fetch_feed_posts_raises_exception(mock_tumblr_transfer_service):
with pytest.raises(UnsupportedRequestException):
mock_tumblr_transfer_service.fetch_feed_posts(count=21)


def test_fetch_feed_posts_raises_http_exception(mocker, mock_tumblr_transfer_service):
mock_response = mocker.create_autospec(Response)
mock_response.ok = False
mock_response.status_code = 400
mock_response.reason = 'fake reason'
mock_response.url = 'fake url'
mock_response.raise_for_status = lambda: Response.raise_for_status(mock_response)

mock_oauth2_session_get(mocker, mock_response)

with pytest.raises(HTTPError):
mock_tumblr_transfer_service.fetch_feed_posts()


def test_fetch_feed_posts(mocker, mock_tumblr_transfer_service):
response_object = mocker.MagicMock()
response_object.json.return_value = {'response': {'posts': ['sample', 'posts']}}

oauth2_session_get = mock_oauth2_session_get(mocker, response_object)

assert mock_tumblr_transfer_service.fetch_feed_posts() == ['sample', 'posts']
assert (
oauth2_session_get.call_args.args[1]
== 'https://api.tumblr.com/v2/user/dashboard'
)