Skip to content

Commit 5813ee6

Browse files
stanlp1esezen
andauthored
[Csl-878] quizzes library support python (#33)
* Add quizzes * lint changes * Fix exceptions * Fix catalog test exception method * Add version_id param * Tests pass * Update finalize function name * Update test names * Organize get_quiz_results tests * Add get_quiz_results tests * Remove todo Co-authored-by: Enes Kutay SEZEN <eneskutaysezen@gmail.com>
1 parent 56940c6 commit 5813ee6

3 files changed

Lines changed: 266 additions & 0 deletions

File tree

constructor_io/constructor_io.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@
55
from constructor_io.modules.autocomplete import Autocomplete
66
from constructor_io.modules.browse import Browse
77
from constructor_io.modules.catalog import Catalog
8+
from constructor_io.modules.quizzes import Quizzes
89
from constructor_io.modules.recommendations import Recommendations
910
from constructor_io.modules.search import Search
1011
from constructor_io.modules.tasks import Tasks
1112

1213

1314
class ConstructorIO:
1415
# pylint: disable=too-few-public-methods
16+
# pylint: disable=too-many-instance-attributes
1517
'''
1618
ConstructorIO Python Client
1719
@@ -49,6 +51,7 @@ def __init__(self, options) -> None:
4951
self.recommendations = Recommendations(self.__options)
5052
self.catalog = Catalog(self.__options)
5153
self.tasks = Tasks(self.__options)
54+
self.quizzes = Quizzes(self.__options)
5255

5356
def get_options(self):
5457
'''Get client options'''

constructor_io/modules/quizzes.py

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
'''Quizzes Module'''
2+
3+
from time import time
4+
from urllib.parse import quote, urlencode
5+
6+
import requests as r
7+
8+
from constructor_io.helpers.exception import ConstructorException
9+
from constructor_io.helpers.utils import (clean_params, create_auth_header,
10+
create_request_headers,
11+
create_shared_query_params,
12+
throw_http_exception_from_response)
13+
14+
15+
def _create_quizzes_url(quiz_id, parameters, user_parameters, options, path):
16+
# pylint: disable=too-many-branches
17+
'''Create URL from supplied quiz_id and parameters'''
18+
quiz_service_url = 'https://quizzes.cnstrc.com'
19+
query_params = create_shared_query_params(options, {}, user_parameters)
20+
ans_query_string = ''
21+
22+
if not quiz_id or not isinstance(quiz_id, str):
23+
raise ConstructorException('quiz_id is a required parameter of type str')
24+
25+
if path == 'finalize' and (not isinstance(parameters.get('a'), list) or len(parameters.get('a')) == 0): # pylint: disable=line-too-long
26+
raise ConstructorException('a is a required parameter of type list')
27+
28+
if parameters:
29+
if parameters.get('section'):
30+
query_params['section'] = parameters.get('section')
31+
32+
if parameters.get('version_id'):
33+
query_params['version_id'] = parameters.get('version_id')
34+
35+
if parameters.get('a'):
36+
answers_param = []
37+
answers = parameters.get('a')
38+
39+
for question_answer in answers:
40+
answers_param.append(','.join(map(str, question_answer)))
41+
42+
ans_query_string = urlencode({'a': answers_param}, doseq=True)
43+
44+
query_params['_dt'] = int(time()*1000.0)
45+
query_params = clean_params(query_params)
46+
query_string = urlencode(query_params, doseq=True)
47+
48+
return f'{quiz_service_url}/v1/quizzes/{quote(quiz_id)}/{quote(path)}?{query_string}&{ans_query_string}'
49+
50+
class Quizzes:
51+
# pylint: disable=too-few-public-methods
52+
'''Quizzes Class'''
53+
54+
def __init__(self, options):
55+
self.__options = options or {}
56+
57+
def get_next_question(self, quiz_id, parameters=None, user_parameters=None):
58+
'''
59+
Retrieve next question from API
60+
61+
:param str quiz_id: Quiz Id
62+
:param dict parameters: Additional parameters to determine next quiz
63+
:param list parameters.a: 2d Array of quiz answers in the format [[1],[1,2]]
64+
:param str parameters.section: Section for customer's product catalog
65+
:param str parameters.version_id: Specific version_id for the quiz
66+
:param dict user_parameters: Parameters relevant to the user request
67+
:param int user_parameters.session_id: Session ID, utilized to personalize results
68+
:param str user_parameters.client_id: Client ID, utilized to personalize results
69+
:param str user_parameters.user_ip: Origin user IP, from client
70+
:param str user_parameters.user_agent: Origin user agent, from client
71+
:return: dict
72+
'''
73+
74+
if not parameters:
75+
parameters = {}
76+
if not user_parameters:
77+
user_parameters = {}
78+
79+
request_url = _create_quizzes_url(quiz_id, parameters, user_parameters, self.__options, 'next') # pylint: disable=line-too-long
80+
requests = self.__options.get('requests') or r
81+
82+
response = requests.get(
83+
request_url,
84+
auth=create_auth_header(self.__options),
85+
headers=create_request_headers(self.__options, user_parameters)
86+
)
87+
88+
if not response.ok:
89+
throw_http_exception_from_response(response)
90+
91+
json = response.json()
92+
93+
if json:
94+
if json.get('version_id'):
95+
return json
96+
97+
raise ConstructorException('get_next_question response data is malformed')
98+
99+
def get_quiz_results(self, quiz_id, parameters=None, user_parameters=None):
100+
'''
101+
Retrieve quiz results from API
102+
103+
:param str quiz_id: Quiz Id
104+
:param dict parameters: Additional parameters to determine next quiz
105+
:param list parameters.a: 2d Array of quiz answers in the format [[1],[1,2]]
106+
:param str parameters.section: Section for customer's product catalog
107+
:param str parameters.version_id: Specific version_id for the quiz
108+
:param dict user_parameters: Parameters relevant to the user request
109+
:param int user_parameters.session_id: Session ID, utilized to personalize results
110+
:param str user_parameters.client_id: Client ID, utilized to personalize results
111+
:param str user_parameters.user_ip: Origin user IP, from client
112+
:param str user_parameters.user_agent: Origin user agent, from client
113+
:return: dict
114+
'''
115+
116+
if not parameters:
117+
parameters = {}
118+
if not user_parameters:
119+
user_parameters = {}
120+
121+
request_url = _create_quizzes_url(quiz_id, parameters, user_parameters, self.__options, 'finalize') #pylint: disable=line-too-long
122+
requests = self.__options.get('requests') or r
123+
124+
response = requests.get(
125+
request_url,
126+
auth=create_auth_header(self.__options),
127+
headers=create_request_headers(self.__options, user_parameters)
128+
)
129+
130+
if not response.ok:
131+
throw_http_exception_from_response(response)
132+
133+
json = response.json()
134+
135+
if json:
136+
if json.get('version_id'):
137+
return json
138+
139+
raise ConstructorException('get_quiz_results response data is malformed')

tests/modules/test_quizzes.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
'''ConstructorIO Python Client - Quizzes Tests'''
2+
3+
from os import environ
4+
5+
from pytest import raises
6+
7+
from constructor_io.constructor_io import ConstructorIO
8+
from constructor_io.helpers.exception import (ConstructorException,
9+
HttpException)
10+
11+
TEST_API_KEY = environ['TEST_API_KEY']
12+
TEST_API_TOKEN = environ['TEST_API_TOKEN']
13+
QUIZ_ID = 'test-quiz'
14+
VALID_QUIZ_ANS = [[1], [1, 2], ['seen']]
15+
VALID_OPTIONS = { 'api_key': TEST_API_KEY, 'api_token': TEST_API_TOKEN}
16+
17+
def test_get_next_question_with_valid_parameters():
18+
'''Should return a response with a valid quiz_id'''
19+
20+
quizzes = ConstructorIO(VALID_OPTIONS).quizzes
21+
response = quizzes.get_next_question('test-quiz')
22+
23+
assert isinstance(response.get('version_id'), str)
24+
assert isinstance(response.get('next_question'), dict)
25+
26+
def test_get_next_question_with_answer_parameter():
27+
'''Should return a response with a valid quiz_id and answer parameter'''
28+
29+
quizzes = ConstructorIO(VALID_OPTIONS).quizzes
30+
response = quizzes.get_next_question('test-quiz', { 'a': VALID_QUIZ_ANS })
31+
32+
assert isinstance(response.get('version_id'), str)
33+
assert isinstance(response.get('next_question'), dict)
34+
assert response.get('next_question').get('id') == 4
35+
36+
def test_get_next_question_with_no_quiz_id():
37+
'''Should raise an exception with no quiz_id'''
38+
39+
with raises(
40+
ConstructorException,
41+
match=r'quiz_id is a required parameter of type str'
42+
):
43+
quizzes = ConstructorIO(VALID_OPTIONS).quizzes
44+
quizzes.get_next_question(None)
45+
46+
def test_get_next_question_with_invalid_quiz_id():
47+
'''Should raise an exception with invalid quiz_id'''
48+
49+
with raises(
50+
ConstructorException,
51+
match=r'The quiz you requested, "abcd" was not found, please specify a valid quiz id before trying again.' # pylint: disable=line-too-long
52+
):
53+
quizzes = ConstructorIO(VALID_OPTIONS).quizzes
54+
quizzes.get_next_question('abcd')
55+
56+
def test_get_next_question_with_invalid_key():
57+
'''Should raise an exception given invalid index_key/api_key'''
58+
59+
with raises(
60+
ConstructorException,
61+
match=r'The quiz you requested, "test-quiz" was not found, please specify a valid quiz id before trying again.' # pylint: disable=line-too-long
62+
):
63+
quizzes = ConstructorIO({'api_key': 'notavalidkey', 'api_token': TEST_API_TOKEN}).quizzes
64+
quizzes.get_next_question(QUIZ_ID, {'a': VALID_QUIZ_ANS})
65+
66+
def test_get_quiz_results_with_valid_parameters():
67+
'''Should return a response with a valid quiz_id, a(answers)'''
68+
69+
quizzes = ConstructorIO(VALID_OPTIONS).quizzes
70+
response = quizzes.get_quiz_results(QUIZ_ID, {'a': VALID_QUIZ_ANS})
71+
72+
assert isinstance(response.get('version_id'), str)
73+
assert isinstance(response.get('result'), dict)
74+
assert isinstance(response.get('result').get('results_url'), str)
75+
76+
def test_get_quiz_results_with_no_quiz_id():
77+
'''Should raise an exception with no quiz_id'''
78+
79+
with raises(
80+
ConstructorException,
81+
match=r'quiz_id is a required parameter of type str'
82+
):
83+
quizzes = ConstructorIO(VALID_OPTIONS).quizzes
84+
quizzes.get_quiz_results(None)
85+
86+
def test_get_quiz_results_with_invalid_quiz_id():
87+
'''Should raise an exception given invalid quiz_id'''
88+
89+
with raises(
90+
HttpException,
91+
match=r'The quiz you requested, "abcd" was not found, please specify a valid quiz id before trying again.' # pylint: disable=line-too-long
92+
):
93+
quizzes = ConstructorIO({'api_key': 'notavalidkey', 'api_token': TEST_API_TOKEN}).quizzes
94+
quizzes.get_quiz_results('abcd', {'a': VALID_QUIZ_ANS})
95+
96+
def test_get_quiz_results_with_invalid_key():
97+
'''Should raise an exception given invalid index_key/api_key'''
98+
99+
with raises(
100+
HttpException,
101+
match=r'The quiz you requested, "test-quiz" was not found, please specify a valid quiz id before trying again.' # pylint: disable=line-too-long
102+
):
103+
quizzes = ConstructorIO({'api_key': 'notavalidkey', 'api_token': TEST_API_TOKEN}).quizzes
104+
quizzes.get_quiz_results(QUIZ_ID, {'a': VALID_QUIZ_ANS})
105+
106+
def test_get_quiz_results_with_empty_answers():
107+
'''Should raise an exception given an empty answers parameter'''
108+
109+
with raises(
110+
ConstructorException,
111+
match=r'a is a required parameter of type list'
112+
):
113+
quizzes = ConstructorIO(VALID_OPTIONS).quizzes
114+
quizzes.get_quiz_results(QUIZ_ID, {'a': []})
115+
116+
def test_get_quiz_results_with_no_answers():
117+
'''Should raise an exception given an nonexistent answers parameter'''
118+
119+
with raises(
120+
ConstructorException,
121+
match=r'a is a required parameter of type list'
122+
):
123+
quizzes = ConstructorIO(VALID_OPTIONS).quizzes
124+
quizzes.get_quiz_results(QUIZ_ID, { })

0 commit comments

Comments
 (0)