Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
15 changes: 15 additions & 0 deletions src/pardner/services/base.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from abc import ABC, abstractmethod
from typing import Any, Iterable, Optional

from requests import Response
from requests_oauthlib import OAuth2Session

from pardner.services.utils import scope_as_set, scope_as_string
Expand Down Expand Up @@ -122,6 +123,20 @@ def verticals(self, verticals: Iterable[Vertical]) -> None:
raise UnsupportedVerticalException(unsupported_verticals, self.name)
self._verticals = set(verticals)

def _get_resource(self, uri: str, params: dict[str, Any] = {}) -> Response:
"""
Sends a GET request to `uri` using `OAuth2Session`.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI if/when we get to generating documentation, the backtick looks for references - and I don't think params like 'uri' work as references so they cause errors

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good to know, thanks! Seems like double backticks is what I'm looking for. #52 for the future


:param uri: the destination of the request (a URI).
:param params: the extra parameters to be send with the request, optionally.

:returns: The `requests.Response` object obtained from making the request.
"""
dashboard_response = self._oAuth2Session.get(uri, params=params)
Comment thread
aborem marked this conversation as resolved.
Outdated
if not dashboard_response.ok:
dashboard_response.raise_for_status()
return dashboard_response

def add_verticals(
self, verticals: Iterable[Vertical], should_reauth: bool = False
) -> bool:
Expand Down
44 changes: 41 additions & 3 deletions src/pardner/services/strava.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
from typing import Any, Iterable, Optional, override
from urllib.parse import urljoin

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

Expand All @@ -13,6 +18,7 @@ class StravaTransferService(BaseTransferService):
"""

_authorization_url = 'https://www.strava.com/oauth/authorize'
_base_url = 'https://www.strava.com/'
_token_url = 'https://www.strava.com/oauth/token'

def __init__(
Expand All @@ -29,7 +35,7 @@ def __init__(
client_secret=client_secret,
redirect_uri=redirect_uri,
state=state,
supported_verticals={Vertical.FeedPost},
supported_verticals={Vertical.PhysicalActivity},
verticals=verticals,
)

Expand All @@ -56,6 +62,38 @@ def scope_for_verticals(self, verticals: Iterable[Vertical]) -> set[str]:
for vertical in verticals:
if vertical not in self._supported_verticals:
raise UnsupportedVerticalException([vertical], self._service_name)
if vertical == Vertical.FeedPost:
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] = {}
) -> list[Any]:
"""
Fetches and returns activities completed by the authorized user.

:param count: number of activities to request. At most 30 at a time.
:param request_params: any other endpoint-specific parameters to be sent
to the endpoint. Depending on the parameters passed, this could override
the other arguments to this method.

:returns: a list of dictionary objects with information for the activities from
the authorized user.

:raises: :class:`UnsupportedRequestException` if the request is unable to be
made.
"""
max_count = 30
if count <= max_count:
athlete_activities_uri = urljoin(
self._base_url, '/api/v3/athlete/activities'
)
return list(
self._get_resource(
athlete_activities_uri, params={'per_page': count, **request_params}
).json()
)
raise UnsupportedRequestException(
self._service_name,
f'can only make a request for at most {max_count} activities at a time.',
)
6 changes: 2 additions & 4 deletions src/pardner/services/tumblr.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,17 +73,15 @@ def fetch_feed_posts(
"""
dashboard_uri = urljoin(self._base_url, '/v2/user/dashboard')
if count <= 20:
dashboard_response = self._oAuth2Session.get(
dashboard_response = self._get_resource(
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,
Expand Down
1 change: 1 addition & 0 deletions src/pardner/verticals/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ class Vertical(StrEnum):
"""

FeedPost = 'feed_post'
PhysicalActivity = 'physical_activity'
26 changes: 25 additions & 1 deletion tests/test_transfer_services/conftest.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import pytest
from requests import Response
from requests_oauthlib import OAuth2Session

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

# FIXTURES


@pytest.fixture
def mock_oauth2_session_request(mocker):
Expand Down Expand Up @@ -32,12 +35,33 @@ def mock_tumblr_transfer_service(verticals=[Vertical.FeedPost]):


@pytest.fixture
def mock_strava_transfer_service(verticals=[Vertical.FeedPost]):
def mock_strava_transfer_service(verticals=[Vertical.PhysicalActivity]):
return StravaTransferService(
'fake_client_id', 'fake_client_secret', 'https://redirect_uri', None, verticals
)


@pytest.fixture
def mock_oauth2_bad_response(mocker):
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)
return mock_response


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


# HELPERS


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
Expand Down
38 changes: 34 additions & 4 deletions tests/test_transfer_services/test_strava.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import pytest
from requests import HTTPError

from pardner.services.base import UnsupportedVerticalException
from pardner.services.base import (
UnsupportedRequestException,
UnsupportedVerticalException,
)
from pardner.verticals import Vertical

sample_scope = {'fake', 'scope'}
from tests.test_transfer_services.conftest import mock_oauth2_session_get


def test_scope(mock_strava_transfer_service):
Expand All @@ -12,7 +15,7 @@ def test_scope(mock_strava_transfer_service):

@pytest.mark.parametrize(
['verticals', 'expected_scope'],
[([], set()), ([Vertical.FeedPost], {'activity:read', 'profile:read_all'})],
[([], set()), ([Vertical.PhysicalActivity], {'activity:read', 'profile:read_all'})],
)
def test_scope_for_verticals(mock_strava_transfer_service, verticals, expected_scope):
assert mock_strava_transfer_service.scope_for_verticals(verticals) == expected_scope
Expand All @@ -21,3 +24,30 @@ def test_scope_for_verticals(mock_strava_transfer_service, verticals, expected_s
def test_scope_for_verticals_raises_error(mock_strava_transfer_service, mock_vertical):
with pytest.raises(UnsupportedVerticalException):
mock_strava_transfer_service.scope_for_verticals([Vertical.NEW_VERTICAL])


def test_fetch_athlete_activities_raises_exception(mock_strava_transfer_service):
with pytest.raises(UnsupportedRequestException):
mock_strava_transfer_service.fetch_athlete_activities(count=31)


def test_fetch_athlete_activities_raises_http_exception(
mock_strava_transfer_service, mock_oauth2_session_get_bad_response
):
with pytest.raises(HTTPError):
mock_strava_transfer_service.fetch_athlete_activities()


def test_fetch_athlete_activities(mocker, mock_strava_transfer_service):
sample_response = [{'object': 1}, {'object': 2}]

response_object = mocker.MagicMock()
response_object.json.return_value = sample_response

oauth2_session_get = mock_oauth2_session_get(mocker, response_object)

assert mock_strava_transfer_service.fetch_athlete_activities() == sample_response
assert (
oauth2_session_get.call_args.args[1]
== 'https://www.strava.com/api/v3/athlete/activities'
)
17 changes: 4 additions & 13 deletions tests/test_transfer_services/test_tumblr.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import pytest
from requests import HTTPError, Response
from requests import HTTPError

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'}


@pytest.mark.parametrize(
['verticals', 'expected_scope'], [([], {'basic'}), ([Vertical.FeedPost, {'basic'}])]
Expand All @@ -20,16 +18,9 @@ def test_fetch_feed_posts_raises_exception(mock_tumblr_transfer_service):
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)

def test_fetch_feed_posts_raises_http_exception(
mock_tumblr_transfer_service, mock_oauth2_session_get_bad_response
):
with pytest.raises(HTTPError):
mock_tumblr_transfer_service.fetch_feed_posts()

Expand Down