diff --git a/src/pardner/services/base.py b/src/pardner/services/base.py index 728565d..d6c3254 100644 --- a/src/pardner/services/base.py +++ b/src/pardner/services/base.py @@ -27,6 +27,11 @@ 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 @@ -34,6 +39,7 @@ class BaseTransferService(ABC): """ _authorization_url: str + _base_url: str _client_secret: str _oAuth2Session: OAuth2Session _service_name: str diff --git a/src/pardner/services/tumblr.py b/src/pardner/services/tumblr.py index 9bfe945..187e9d3 100644 --- a/src/pardner/services/tumblr.py +++ b/src/pardner/services/tumblr.py @@ -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 @@ -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__( @@ -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.', + ) diff --git a/tests/test_transfer_services/conftest.py b/tests/test_transfer_services/conftest.py index e73784f..20854aa 100644 --- a/tests/test_transfer_services/conftest.py +++ b/tests/test_transfer_services/conftest.py @@ -1,4 +1,5 @@ import pytest +from requests_oauthlib import OAuth2Session from pardner.services.strava import StravaTransferService from pardner.services.tumblr import TumblrTransferService @@ -6,12 +7,15 @@ @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 @@ -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 diff --git a/tests/test_transfer_services/test_base.py b/tests/test_transfer_services/test_base.py index ee35035..93a3dc8 100644 --- a/tests/test_transfer_services/test_base.py +++ b/tests/test_transfer_services/test_base.py @@ -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() diff --git a/tests/test_transfer_services/test_transfer_services_common.py b/tests/test_transfer_services/test_transfer_services_common.py index 5445a57..0d1cd06 100644 --- a/tests/test_transfer_services/test_transfer_services_common.py +++ b/tests/test_transfer_services/test_transfer_services_common.py @@ -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' + ) diff --git a/tests/test_transfer_services/test_tumblr.py b/tests/test_transfer_services/test_tumblr.py index 0e157b7..b8f48a9 100644 --- a/tests/test_transfer_services/test_tumblr.py +++ b/tests/test_transfer_services/test_tumblr.py @@ -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'} @@ -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' + )