Skip to content

Commit

Permalink
Implements #22 - Support for user-authenticated API calls
Browse files Browse the repository at this point in the history
  • Loading branch information
lourot authored and lancelote committed Jan 9, 2018
1 parent 56e6f0c commit 3cde73a
Show file tree
Hide file tree
Showing 6 changed files with 112 additions and 42 deletions.
2 changes: 1 addition & 1 deletion .coveragerc
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
[report]
fail_under = 97
fail_under = 98
47 changes: 36 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,55 @@

Collection of utils for Quizlet flash cards

## Requirements
## Installation

```bash
$ pip install quizler
```

Tested on:

- macOS / Python 3.6.1
- Ubuntu 14.04 / Python 3.5.3

To install Python requirements (virtualenv is recommended):
## Usage

### Command-line interface

On the command-line Quizler can be used to access public information read-only:

```bash
pip install -r requirements.txt
$ export USER_ID=john
$ export CLIENT_ID=Ab0Cd1Ef2G
$ quizler sets --terms
Found sets: 2
German
Hello = Hallo
How are you? = Wie geht's?
French
Hello = Bonjour
How are you? = Comment ça va?
```
## Usage

Quizler relies onto environment variables to operate. Currently there're two:
- `USER_ID` - Quizlet username whose information you want to access (your own username can be viewed
right at your avatar in the top right corner on quizlet.com)
- `CLIENT_ID` - Quizlet Client ID which can be obtained on the
[Quizlet API dashboard](https://quizlet.com/api-dashboard)
- `USER_ID` - your username on quizlet, it can be viewed right at your avatar in the top right on quizlet.com
- `CLIENT_ID` - Quizlet Client ID can be obtained in [Quizlet API dashboard](https://quizlet.com/api-dashboard)
### Python library
Set this two before using the script. To work with quizler just invoke CLI, e.g.:
As a Python library Quizler can also access private information:
```bash
python main.py common
```
>>> from quizler.utils import reset_term_stats
>>> reset_term_stats(set_id=123456789, term_id=1234567890, client_id='Ab0Cd1Ef2G', user_id='john', access_token='46a54395f3d1108feca56c7f6ca8dd3d')
Deleting "Hello = Hallo"...
Re-creating "Hello = Hallo"...
Done
```
The access token can be obtained by a web app implementing
[Quizlet's authentication flow](https://quizlet.com/api/2.0/docs/authorization-code-flow).
[Here is an example](https://github.com/quizl/backend).

<a href="https://quizlet.com/"><img src="https://quizlet.com/static/ThisUsesQuizlet-White.png" alt="Powered by Quizlet" align="right"/></a>
18 changes: 15 additions & 3 deletions quizler/lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,25 @@ def get_api_envs():
return client_id, user_id


def api_call(method: str, end_point: str, params: Dict[str, str], client_id: str):
def api_call(method: str, end_point: str, params: Dict[str, str] = None, client_id: str = None,
access_token: str = None):
"""Call given API end_point with API keys."""
if bool(client_id) == bool(access_token):
raise ValueError('Either client_id or access_token')

url = 'https://api.quizlet.com/2.0/{}'.format(end_point)
params['client_id'] = client_id

if not params:
params = {}
if client_id:
params['client_id'] = client_id

headers = {'Authorization': 'Bearer {}'.format(access_token)} if access_token else None

# pylint: disable=too-many-function-args
response = requests.request(method, url, params=params)
response = requests.request(method, url, params=params, headers=headers)
# pylint: enable=too-many-function-args

# pylint: disable=no-member
if int(response.status_code / 100) != 2:
# pylint: enable=no-member
Expand Down
16 changes: 8 additions & 8 deletions quizler/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

def get_user_sets(client_id, user_id):
"""Find all user sets."""
data = api_call('get', 'users/{}/sets'.format(user_id), {}, client_id)
data = api_call('get', 'users/{}/sets'.format(user_id), client_id=client_id)
return [WordSet.from_dict(wordset) for wordset in data]


Expand Down Expand Up @@ -53,17 +53,17 @@ def print_common_terms(common_terms: List[Tuple[str, str, Set[str]]]):
print(' {}'.format(term))


def delete_term(set_id, term_id, client_id):
def delete_term(set_id, term_id, access_token):
"""Delete the given term."""
api_call('delete', 'sets/{}/terms/{}'.format(set_id, term_id), {}, client_id)
api_call('delete', 'sets/{}/terms/{}'.format(set_id, term_id), access_token=access_token)


def add_term(set_id, term: Term, client_id):
def add_term(set_id, term: Term, access_token):
"""Add the given term to the given set."""
api_call('post', 'sets/{}/terms'.format(set_id), term.to_dict(), client_id)
api_call('post', 'sets/{}/terms'.format(set_id), term.to_dict(), access_token=access_token)


def reset_term_stats(set_id, term_id, client_id, user_id):
def reset_term_stats(set_id, term_id, client_id, user_id, access_token):
"""Reset the stats of a term by deleting and re-creating it."""
found_sets = [user_set for user_set in get_user_sets(client_id, user_id)
if user_set.set_id == set_id]
Expand All @@ -81,9 +81,9 @@ def reset_term_stats(set_id, term_id, client_id, user_id):
raise NotImplementedError('"{}" has an image and is thus not supported'.format(term))

print('Deleting "{}"...'.format(term))
delete_term(set_id, term_id, client_id)
delete_term(set_id, term_id, access_token)
print('Re-creating "{}"...'.format(term))
add_term(set_id, term, client_id)
add_term(set_id, term, access_token)
print('Done')


Expand Down
42 changes: 36 additions & 6 deletions tests/quizler/test_lib.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# pylint: disable=no-self-use,missing-docstring,invalid-name

import json
import unittest
from unittest import mock

Expand Down Expand Up @@ -33,26 +34,55 @@ def test_all_envs_are_set(self):

class TestApiCall(unittest.TestCase):

def test_no_client_id_or_access_token(self):
with self.assertRaises(ValueError):
api_call('get', 'end_point')

def test_client_id_and_access_token(self):
with self.assertRaises(ValueError):
api_call('get', 'end_point', client_id='client_id', access_token='token')

@mock.patch('requests.request')
def test_unknown_endpoint(self, mock_request):
mock_request.return_value.status_code = 404
with self.assertRaises(ValueError):
api_call('get', 'unknown_end_point', {}, 'client_id')
api_call('get', 'unknown_end_point', client_id='client_id')

@mock.patch('requests.request')
def test_correct_url_was_called_with_client_id(self, mock_request):
mock_request.return_value.status_code = 200
api_call('get', 'end_point', client_id='client_id')
mock_request.assert_called_once_with(
'get',
'https://api.quizlet.com/2.0/end_point',
params={'client_id': 'client_id'},
headers=None
)

@mock.patch('requests.request')
def test_correct_url_was_called(self, mock_request):
def test_correct_url_was_called_with_access_token(self, mock_request):
mock_request.return_value.status_code = 200
api_call('get', 'end_point', {}, 'client_id')
api_call('get', 'end_point', access_token='token')
mock_request.assert_called_once_with(
'get',
'https://api.quizlet.com/2.0/end_point',
params={'client_id': 'client_id'}
params={},
headers={'Authorization': 'Bearer token'}
)

@mock.patch('requests.request')
def test_correct_output_was_returned(self, mock_request):
def test_json_output_was_returned(self, mock_request):
response = mock.Mock()
response.status_code = 200
mock_request.return_value = response
data = api_call('get', 'end_point', {}, 'client_id')
data = api_call('get', 'end_point', client_id='client_id')
self.assertEqual(data, response.json())

@mock.patch('requests.request')
def test_non_json_output(self, mock_request):
response = mock.Mock()
response.status_code = 200
response.json = mock.Mock(side_effect=json.decoder.JSONDecodeError('', '', 1))
mock_request.return_value = response
data = api_call('get', 'end_point', client_id='client_id')
self.assertEqual(data, None)
29 changes: 16 additions & 13 deletions tests/quizler/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,13 +118,12 @@ class TestDeleteTerm(unittest.TestCase):
def test_one_term(self, mock_api_call):
set_id = 1
term_id = 2
client_id = 3
delete_term(set_id, term_id, client_id)
access_token = 'token'
delete_term(set_id, term_id, access_token)
mock_api_call.assert_called_once_with(
'delete',
'sets/{}/terms/{}'.format(set_id, term_id),
{},
client_id
access_token=access_token
)


Expand All @@ -134,13 +133,13 @@ class TestAddTerm(unittest.TestCase):
def test_one_term(self, mock_api_call):
set_id = 1
term = TermFactory()
client_id = 3
add_term(set_id, term, client_id)
access_token = 'token'
add_term(set_id, term, access_token)
mock_api_call.assert_called_once_with(
'post',
'sets/{}/terms'.format(set_id),
term.to_dict(),
client_id
access_token=access_token
)


Expand All @@ -162,26 +161,29 @@ def test_set_not_found(self, mock_get_user_sets):
term_id = self.term0.term_id
client_id = 1
user_id = 2
access_token = 'token'
with self.assertRaises(ValueError):
reset_term_stats(unknown_set_id, term_id, client_id, user_id)
reset_term_stats(unknown_set_id, term_id, client_id, user_id, access_token)

def test_term_not_found(self, mock_get_user_sets):
mock_get_user_sets.return_value = self.wordsets
set_id = self.wordset0.set_id
unknown_term_id = -1
client_id = 1
user_id = 2
access_token = 'token'
with self.assertRaises(ValueError):
reset_term_stats(set_id, unknown_term_id, client_id, user_id)
reset_term_stats(set_id, unknown_term_id, client_id, user_id, access_token)

def test_term_has_image(self, mock_get_user_sets):
mock_get_user_sets.return_value = self.wordsets
set_id = self.wordset0.set_id
term_id = self.term0.term_id
client_id = 1
user_id = 2
access_token = 'token'
with self.assertRaises(NotImplementedError):
reset_term_stats(set_id, term_id, client_id, user_id)
reset_term_stats(set_id, term_id, client_id, user_id, access_token)

@mock.patch('quizler.utils.add_term')
@mock.patch('quizler.utils.delete_term')
Expand All @@ -191,6 +193,7 @@ def test_one_term(self, mock_delete_term, mock_add_term, mock_get_user_sets):
term_id = self.term1.term_id
client_id = 1
user_id = 2
reset_term_stats(set_id, term_id, client_id, user_id)
mock_delete_term.assert_called_once_with(set_id, term_id, client_id)
mock_add_term.assert_called_once_with(set_id, self.term1, client_id)
access_token = 'token'
reset_term_stats(set_id, term_id, client_id, user_id, access_token)
mock_delete_term.assert_called_once_with(set_id, term_id, access_token)
mock_add_term.assert_called_once_with(set_id, self.term1, access_token)

0 comments on commit 3cde73a

Please sign in to comment.