Skip to content

Commit

Permalink
Utility for resetting a term's stats.
Browse files Browse the repository at this point in the history
  • Loading branch information
lourot authored and lancelote committed Jan 9, 2018
1 parent 80973c2 commit 56e6f0c
Show file tree
Hide file tree
Showing 8 changed files with 275 additions and 48 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 = 96
fail_under = 97
22 changes: 15 additions & 7 deletions quizler/lib.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"""Additional none-user-visible utilities."""

from typing import Dict
import json
import os

import requests


Expand All @@ -14,14 +15,21 @@ def get_api_envs():
return client_id, user_id


def api_call(end_point, client_id, user_id):
def api_call(method: str, end_point: str, params: Dict[str, str], client_id: str):
"""Call given API end_point with API keys."""
url = 'https://api.quizlet.com/2.0/users/{}/{}'.format(user_id, end_point)
params = {'client_id': client_id}
response = requests.get(url, params)
if response.status_code != 200:
url = 'https://api.quizlet.com/2.0/{}'.format(end_point)
params['client_id'] = client_id
# pylint: disable=too-many-function-args
response = requests.request(method, url, params=params)
# pylint: enable=too-many-function-args
# pylint: disable=no-member
if int(response.status_code / 100) != 2:
# pylint: enable=no-member
raise ValueError(
'Unknown end point, server returns {}'.format(response.status_code)
)
else:

try:
return response.json()
except json.decoder.JSONDecodeError:
pass
49 changes: 47 additions & 2 deletions quizler/models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,51 @@
"""OOP models for Quizlet terms abstractions."""


class Image:
"""Quizlet image abstraction."""

def __init__(self, url, width, height):
self.url = url
self.width = width
self.height = height

@staticmethod
def from_dict(raw_data):
"""Create Image from raw dictionary data."""
url = None
width = None
height = None
try:
url = raw_data['url']
width = raw_data['width']
height = raw_data['height']
except KeyError:
raise ValueError('Unexpected image json structure')
except TypeError:
# Happens when raw_data is None, i.e. when a term has no image:
pass
return Image(url, width, height)

def to_dict(self):
"""Convert Image into raw dictionary data."""
if not self.url:
return None
return {
'url': self.url,
'width': self.width,
'height': self.height
}

def __eq__(self, other):
if not isinstance(other, Image):
raise ValueError
return all((
self.url == other.url,
self.width == other.width,
self.height == other.height
))


class Term:
"""Quizlet term abstraction."""

Expand All @@ -17,7 +62,7 @@ def from_dict(raw_data):
try:
definition = raw_data['definition']
term_id = raw_data['id']
image = raw_data['image']
image = Image.from_dict(raw_data['image'])
rank = raw_data['rank']
term = raw_data['term']
return Term(definition, term_id, image, rank, term)
Expand All @@ -29,7 +74,7 @@ def to_dict(self):
return {
'definition': self.definition,
'id': self.term_id,
'image': self.image,
'image': self.image.to_dict(),
'rank': self.rank,
'term': self.term
}
Expand Down
44 changes: 39 additions & 5 deletions quizler/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,12 @@
from typing import List, Tuple, Set

from quizler.lib import api_call
from quizler.models import WordSet
from quizler.models import Term, WordSet


def get_user_sets(*api_envs):
def get_user_sets(client_id, user_id):
"""Find all user sets."""
# pylint: disable=no-value-for-parameter
data = api_call('sets', *api_envs)
# pylint: enable=no-value-for-parameter
data = api_call('get', 'users/{}/sets'.format(user_id), {}, client_id)
return [WordSet.from_dict(wordset) for wordset in data]


Expand All @@ -32,7 +30,9 @@ def print_user_sets(wordsets: List[WordSet], print_terms: bool):
def get_common_terms(*api_envs) -> List[Tuple[str, str, Set[str]]]:
"""Get all term duplicates across all user word sets."""
common_terms = []
# pylint: disable=no-value-for-parameter
wordsets = get_user_sets(*api_envs)
# pylint: enable=no-value-for-parameter

for wordset1, wordset2 in combinations(wordsets, 2):
common = wordset1.has_common(wordset2)
Expand All @@ -53,6 +53,40 @@ def print_common_terms(common_terms: List[Tuple[str, str, Set[str]]]):
print(' {}'.format(term))


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


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


def reset_term_stats(set_id, term_id, client_id, user_id):
"""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]
if len(found_sets) != 1:
raise ValueError('{} set(s) found with id {}'.format(len(found_sets), set_id))
found_terms = [term for term in found_sets[0].terms if term.term_id == term_id]
if len(found_terms) != 1:
raise ValueError('{} term(s) found with id {}'.format(len(found_terms), term_id))
term = found_terms[0]

if term.image.url:
# Creating a term with an image requires an "image identifier", which you get by uploading
# an image via https://quizlet.com/api/2.0/docs/images , which can only be used by Quizlet
# PLUS members.
raise NotImplementedError('"{}" has an image and is thus not supported'.format(term))

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


def apply_regex(pattern, repl, set_name, *api_envs):
"""Apply regex replace to all terms in word set."""
print('{}, {}, {}, {}'.format(pattern, repl, set_name, api_envs))
Expand Down
11 changes: 10 additions & 1 deletion tests/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,23 @@
from quizler import models


class ImageFactory(factory.Factory):
class Meta:
model = models.Image

url = None
width = None
height = None


class TermFactory(factory.Factory):
class Meta:
model = models.Term

definition = factory.LazyAttribute(
lambda obj: 'definition{}'.format(obj.term_id))
term_id = factory.Sequence(lambda n: n)
image = None
image = ImageFactory()
rank = 0
term = factory.LazyAttribute(lambda obj: 'term{}'.format(obj.term_id))

Expand Down
33 changes: 17 additions & 16 deletions tests/quizler/test_lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,25 +33,26 @@ def test_all_envs_are_set(self):

class TestApiCall(unittest.TestCase):

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

@mock.patch('requests.get')
def test_correct_url_was_called(self, mock_get):
mock_get.return_value.status_code = 200
api_call('end_point', 'client_id', 'user_id')
mock_get.assert_called_once_with(
'https://api.quizlet.com/2.0/users/user_id/end_point',
{'client_id': 'client_id'}
api_call('get', 'unknown_end_point', {}, 'client_id')

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

@mock.patch('requests.get')
def test_correct_output_was_returned(self, mock_get):
@mock.patch('requests.request')
def test_correct_output_was_returned(self, mock_request):
response = mock.Mock()
response.status_code = 200
mock_get.return_value = response
data = api_call('end_point', 'client_id', 'user_id')
mock_request.return_value = response
data = api_call('get', 'end_point', {}, 'client_id')
self.assertEqual(data, response.json())
70 changes: 58 additions & 12 deletions tests/quizler/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,54 @@
import unittest

import pytest
from quizler.models import Term, WordSet
from tests.factories import TermFactory, WordSetFactory
from quizler.models import Image, Term, WordSet
from tests.factories import ImageFactory, TermFactory, WordSetFactory


class TestImage(unittest.TestCase):

def test_from_dict(self):
raw_data = {
'url': 'http://domain.com/myimage.png',
'width': 200,
'height': 100
}
image = Image.from_dict(raw_data)
assert image.url == 'http://domain.com/myimage.png'
assert image.width == 200
assert image.height == 100

def test_wrong_image_json_structure(self):
with self.assertRaises(ValueError):
Image.from_dict({'some data': '142% unexpected'})

def test_correct_init(self):
image = Image('http://domain.com/myimage.png', 200, 100)
assert image.url == 'http://domain.com/myimage.png'
assert image.width == 200
assert image.height == 100

def test_to_dict(self):
image = Image('http://domain.com/myimage.png', 200, 100)
assert image.to_dict() == {
'url': 'http://domain.com/myimage.png',
'width': 200,
'height': 100
}

def test_equal_images(self):
image0 = Image('http://domain.com/myimage.png', 200, 100)
image1 = Image('http://domain.com/myimage.png', 200, 100)
assert image0 == image1

def test_unequal_images(self):
image0 = Image('http://domain.com/myimage.png', 200, 100)
image1 = Image('http://domain.com/myimage.png', 201, 100)
assert image0 != image1

def test_wrong_equality_type(self):
with pytest.raises(ValueError):
assert ImageFactory() == 1


class TestTerm(unittest.TestCase):
Expand All @@ -20,7 +66,7 @@ def test_from_dict(self):
term = Term.from_dict(raw_data)
assert term.definition == 'term definition'
assert term.term_id == 12345
assert term.image is None
assert term.image.url is None
assert term.rank == 0
assert term.term == 'term'

Expand All @@ -29,14 +75,14 @@ def test_wrong_term_json_structure(self):
Term.from_dict({'some data': '142% unexpected'})

def test_correct_init(self):
term = Term('definition', 1, None, 0, 'term')
term = Term('definition', 1, ImageFactory(), 0, 'term')
assert term.definition == 'definition'
assert term.term_id == 1
assert term.image is None
assert term.image.url is None
assert term.term == 'term'

def test_to_dict(self):
term = Term('definition1', 1, None, 1, 'term1')
term = Term('definition1', 1, ImageFactory(), 1, 'term1')
assert term.to_dict() == {
'definition': 'definition1',
'id': 1,
Expand All @@ -46,13 +92,13 @@ def test_to_dict(self):
}

def test_equal_terms(self):
term0 = Term('definition', 0, None, 0, 'term')
term1 = Term('definition', 0, None, 0, 'term')
term0 = Term('definition', 0, ImageFactory(), 0, 'term')
term1 = Term('definition', 0, ImageFactory(), 0, 'term')
assert term0 == term1

def test_unequal_terms(self):
term0 = Term('definition0', 0, None, 0, 'term0')
term1 = Term('definition1', 1, None, 0, 'term1')
term0 = Term('definition0', 0, ImageFactory(), 0, 'term0')
term1 = Term('definition1', 1, ImageFactory(), 0, 'term1')
assert term0 != term1

def test_wrong_equality_type(self):
Expand Down Expand Up @@ -98,14 +144,14 @@ def test_from_dict(self):
wordset = WordSet.from_dict(raw_data)
assert wordset.set_id == 0
assert wordset.title == 'title0'
assert wordset.terms == [Term('definition0', 0, None, 0, 'term0')]
assert wordset.terms == [Term('definition0', 0, Image(None, None, None), 0, 'term0')]

def test_from_bad_dict(self):
with pytest.raises(ValueError):
WordSet.from_dict({})

def test_to_dict(self):
wordset = WordSet(0, 'title0', [Term('def0', 0, None, 0, 'term0')])
wordset = WordSet(0, 'title0', [Term('def0', 0, ImageFactory(), 0, 'term0')])
assert wordset.to_dict() == {
'id': 0,
'title': 'title0',
Expand Down
Loading

0 comments on commit 56e6f0c

Please sign in to comment.