Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-analytics-data-api-client
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
OpenEdx
edx-analytics-data-api-client
Commits
5b5825b0
Commit
5b5825b0
authored
Jul 21, 2014
by
Clinton Blackburn
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #9 from edx/enrollment-trends
Updated client to support enrollment date ranges
parents
c14f3047
0dc8f482
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
98 additions
and
54 deletions
+98
-54
analyticsclient/client.py
+10
-4
analyticsclient/course.py
+28
-3
analyticsclient/demographic.py
+1
-0
analyticsclient/exceptions.py
+6
-0
analyticsclient/tests/test_client.py
+27
-8
analyticsclient/tests/test_course.py
+26
-39
No files found.
analyticsclient/client.py
View file @
5b5825b0
...
...
@@ -4,7 +4,7 @@ import requests
import
requests.exceptions
from
analyticsclient.course
import
Course
from
analyticsclient.exceptions
import
ClientError
,
InvalidRequestError
,
NotFoundError
from
analyticsclient.exceptions
import
ClientError
,
InvalidRequestError
,
NotFoundError
,
TimeoutError
from
analyticsclient.status
import
Status
...
...
@@ -81,6 +81,7 @@ class Client(object):
except
ClientError
:
return
False
# pylint: disable=no-member
def
_request
(
self
,
resource
,
timeout
=
None
):
if
timeout
is
None
:
timeout
=
self
.
timeout
...
...
@@ -96,14 +97,14 @@ class Client(object):
response
=
requests
.
get
(
uri
,
headers
=
headers
,
timeout
=
timeout
)
status
=
response
.
status_code
if
status
!=
requests
.
codes
.
ok
:
# pylint: disable=no-member
if
status
!=
requests
.
codes
.
ok
:
message
=
'Resource "{0}" returned status code {1}'
.
format
(
resource
,
status
)
error_class
=
ClientError
if
status
==
requests
.
codes
.
bad_request
:
# pylint: disable=no-member
if
status
==
requests
.
codes
.
bad_request
:
message
=
'The request to {0} was invalid.'
.
format
(
uri
)
error_class
=
InvalidRequestError
elif
status
==
requests
.
codes
.
not_found
:
# pylint: disable=no-member
elif
status
==
requests
.
codes
.
not_found
:
message
=
'Resource {0} was not found on the API server.'
.
format
(
uri
)
error_class
=
NotFoundError
...
...
@@ -112,6 +113,11 @@ class Client(object):
return
response
except
requests
.
exceptions
.
Timeout
:
message
=
"Response from {0} exceeded timeout of {1}s."
log
.
exception
(
message
)
raise
TimeoutError
(
message
)
except
requests
.
exceptions
.
RequestException
:
message
=
'Unable to retrieve resource'
log
.
exception
(
message
)
...
...
analyticsclient/course.py
View file @
5b5825b0
import
urllib
import
analyticsclient.activity_type
as
at
...
...
@@ -17,14 +18,38 @@ class Course(object):
self
.
client
=
client
self
.
course_id
=
unicode
(
course_id
)
def
enrollment
(
self
,
demographic
):
def
enrollment
(
self
,
demographic
=
None
,
start_date
=
None
,
end_date
=
None
):
"""
Get course enrollment data grouped by demographic.
Get course enrollment data.
Specify a start or end date to retrieve all data for the date range. If no start or end date is specifying, data
for the most-recent date will be returned. All dates are in the UTC timezone and should be formatted as
YYYY-mm-dd (e.g. 2014-01-31).
Specify a demographic to retrieve data grouped by the specified demographic. If no demographic is specified,
data will be across all demographics.
Arguments:
demographic (str): Demographic by which enrollment data should be grouped.
start_date (str): Minimum date for returned enrollment data
end_date (str): Maxmimum date for returned enrollment data
"""
return
self
.
client
.
get
(
'courses/{0}/enrollment/{1}/'
.
format
(
self
.
course_id
,
demographic
))
path
=
'courses/{0}/enrollment/'
.
format
(
self
.
course_id
)
if
demographic
:
path
+=
'{0}/'
.
format
(
demographic
)
params
=
{}
if
start_date
:
params
[
'start_date'
]
=
start_date
if
end_date
:
params
[
'end_date'
]
=
end_date
querystring
=
urllib
.
urlencode
(
params
)
if
querystring
:
path
+=
'?{0}'
.
format
(
querystring
)
return
self
.
client
.
get
(
path
)
def
recent_activity
(
self
,
activity_type
=
at
.
ANY
):
"""
...
...
analyticsclient/demographic.py
View file @
5b5825b0
...
...
@@ -3,3 +3,4 @@
BIRTH_YEAR
=
'birth_year'
EDUCATION
=
'education'
GENDER
=
'gender'
LOCATION
=
'location'
analyticsclient/exceptions.py
View file @
5b5825b0
...
...
@@ -14,3 +14,9 @@ class InvalidRequestError(ClientError):
""" The API request was invalid. """
pass
class
TimeoutError
(
ClientError
):
""" The API server did not respond before the timeout expired. """
pass
analyticsclient/tests/test_client.py
View file @
5b5825b0
import
json
import
httpretty
import
mock
import
requests.exceptions
from
analyticsclient.client
import
Client
from
analyticsclient.exceptions
import
ClientError
from
analyticsclient.exceptions
import
ClientError
,
TimeoutError
from
analyticsclient.tests
import
ClientTestCase
...
...
@@ -12,33 +14,36 @@ class ClientTests(ClientTestCase):
super
(
ClientTests
,
self
)
.
setUp
()
httpretty
.
enable
()
self
.
test_endpoint
=
'test'
self
.
test_ur
i
=
self
.
get_api_url
(
self
.
test_endpoint
)
self
.
test_ur
l
=
self
.
get_api_url
(
self
.
test_endpoint
)
def
tearDown
(
self
):
httpretty
.
disable
()
def
test_has_resource
(
self
):
httpretty
.
register_uri
(
httpretty
.
GET
,
self
.
test_ur
i
,
body
=
''
)
httpretty
.
register_uri
(
httpretty
.
GET
,
self
.
test_ur
l
,
body
=
''
)
self
.
assertEquals
(
self
.
client
.
has_resource
(
self
.
test_endpoint
),
True
)
def
test_missing_resource
(
self
):
httpretty
.
register_uri
(
httpretty
.
GET
,
self
.
test_ur
i
,
body
=
''
,
status
=
404
)
httpretty
.
register_uri
(
httpretty
.
GET
,
self
.
test_ur
l
,
body
=
''
,
status
=
404
)
self
.
assertEquals
(
self
.
client
.
has_resource
(
self
.
test_endpoint
),
False
)
def
test_failed_authentication
(
self
):
client
=
Client
(
base_url
=
self
.
api_url
,
auth_token
=
'atoken'
)
httpretty
.
register_uri
(
httpretty
.
GET
,
self
.
test_ur
i
,
body
=
''
,
status
=
401
)
httpretty
.
register_uri
(
httpretty
.
GET
,
self
.
test_ur
l
,
body
=
''
,
status
=
401
)
self
.
assertEquals
(
client
.
has_resource
(
self
.
test_endpoint
),
False
)
self
.
assertEquals
(
httpretty
.
last_request
()
.
headers
[
'Authorization'
],
'Token atoken'
)
def
test_get
(
self
):
data
=
{
'foo'
:
'bar'
}
httpretty
.
register_uri
(
httpretty
.
GET
,
self
.
test_ur
i
,
body
=
json
.
dumps
(
data
))
httpretty
.
register_uri
(
httpretty
.
GET
,
self
.
test_ur
l
,
body
=
json
.
dumps
(
data
))
self
.
assertEquals
(
self
.
client
.
get
(
self
.
test_endpoint
),
data
)
# Bad JSON
httpretty
.
register_uri
(
httpretty
.
GET
,
self
.
test_uri
,
body
=
json
.
dumps
(
data
)[:
6
])
def
test_get_invalid_response_body
(
self
):
""" Verify that client raises a ClientError if the response body cannot be properly parsed. """
data
=
{
'foo'
:
'bar'
}
httpretty
.
register_uri
(
httpretty
.
GET
,
self
.
test_url
,
body
=
json
.
dumps
(
data
)[:
6
])
with
self
.
assertRaises
(
ClientError
):
self
.
client
.
get
(
self
.
test_endpoint
)
...
...
@@ -50,3 +55,17 @@ class ClientTests(ClientTestCase):
url_with_slash
=
'http://example.com/'
client
=
Client
(
url_with_slash
)
self
.
assertEqual
(
client
.
base_url
,
url
)
# pylint: disable=protected-access
@mock.patch
(
'requests.get'
,
side_effect
=
requests
.
exceptions
.
Timeout
)
def
test_request_timeout
(
self
,
mock_get
):
url
=
self
.
test_url
timeout
=
None
self
.
assertRaises
(
TimeoutError
,
self
.
client
.
_request
,
self
.
test_endpoint
,
timeout
=
timeout
)
headers
=
{
'Accept'
:
'application/json'
}
mock_get
.
assert_called_once_with
(
url
,
headers
=
headers
,
timeout
=
self
.
client
.
timeout
)
mock_get
.
reset_mock
()
timeout
=
10
self
.
assertRaises
(
TimeoutError
,
self
.
client
.
_request
,
self
.
test_endpoint
,
timeout
=
timeout
)
mock_get
.
assert_called_once_with
(
url
,
headers
=
headers
,
timeout
=
timeout
)
analyticsclient/tests/test_course.py
View file @
5b5825b0
...
...
@@ -20,12 +20,28 @@ class CoursesTests(ClientTestCase):
super
(
CoursesTests
,
self
)
.
tearDown
()
httpretty
.
disable
()
def
assertEnrollmentResponseData
(
self
,
course
,
data
,
demographic
=
None
):
def
assertCorrectEnrollmentUrl
(
self
,
course
,
demographic
=
None
):
""" Verifies that the enrollment URL is correct. """
uri
=
self
.
get_api_url
(
'courses/{0}/enrollment/'
.
format
(
course
.
course_id
))
if
demographic
:
uri
+=
'
%
s/'
%
demographic
httpretty
.
register_uri
(
httpretty
.
GET
,
uri
,
body
=
json
.
dumps
(
data
))
self
.
assertDictEqual
(
data
,
course
.
enrollment
(
demographic
))
httpretty
.
register_uri
(
httpretty
.
GET
,
uri
,
body
=
'{}'
)
course
.
enrollment
(
demographic
)
date
=
'2014-01-01'
httpretty
.
reset
()
httpretty
.
register_uri
(
httpretty
.
GET
,
'{0}?start_date={1}'
.
format
(
uri
,
date
),
body
=
'{}'
)
course
.
enrollment
(
demographic
,
start_date
=
date
)
httpretty
.
reset
()
httpretty
.
register_uri
(
httpretty
.
GET
,
'{0}?end_date={1}'
.
format
(
uri
,
date
),
body
=
'{}'
)
course
.
enrollment
(
demographic
,
end_date
=
date
)
httpretty
.
reset
()
httpretty
.
register_uri
(
httpretty
.
GET
,
'{0}?start_date={1}&end_date={1}'
.
format
(
uri
,
date
),
body
=
'{}'
)
course
.
enrollment
(
demographic
,
start_date
=
date
,
end_date
=
date
)
@httpretty.activate
def
assertRecentActivityResponseData
(
self
,
course
,
activity_type
):
...
...
@@ -41,42 +57,6 @@ class CoursesTests(ClientTestCase):
httpretty
.
register_uri
(
httpretty
.
GET
,
uri
,
body
=
json
.
dumps
(
body
))
self
.
assertDictEqual
(
body
,
self
.
course
.
recent_activity
(
activity_type
))
def
test_enrollment_birth_year
(
self
):
data
=
{
u'birth_years'
:
{
u'1894'
:
13
,
u'1895'
:
19
}
}
self
.
assertEnrollmentResponseData
(
self
.
course
,
data
,
demo
.
BIRTH_YEAR
)
def
test_enrollment_education
(
self
):
data
=
{
u'education_levels'
:
{
u'none'
:
667
,
u'junior_secondary'
:
6051
,
u'primary'
:
981
,
u'associates'
:
12255
,
u'bachelors'
:
70885
,
u'masters'
:
53216
,
u'doctorate'
:
9940
,
u'other'
:
5722
,
u'secondary'
:
51591
}
}
self
.
assertEnrollmentResponseData
(
self
.
course
,
data
,
demo
.
EDUCATION
)
def
test_enrollment_gender
(
self
):
data
=
{
u'genders'
:
{
u'm'
:
133240
,
u'o'
:
423
,
u'f'
:
77495
}
}
self
.
assertEnrollmentResponseData
(
self
.
course
,
data
,
demo
.
GENDER
)
def
test_recent_activity
(
self
):
self
.
assertRecentActivityResponseData
(
self
.
course
,
at
.
ANY
)
self
.
assertRecentActivityResponseData
(
self
.
course
,
at
.
ATTEMPTED_PROBLEM
)
...
...
@@ -104,3 +84,10 @@ class CoursesTests(ClientTestCase):
self
.
assertRaises
(
InvalidRequestError
,
self
.
course
.
recent_activity
,
'not-a-an-activity-type'
)
self
.
assertRaises
(
InvalidRequestError
,
self
.
course
.
enrollment
,
'not-a-demographic'
)
def
test_enrollment
(
self
):
self
.
assertCorrectEnrollmentUrl
(
self
.
course
,
None
)
self
.
assertCorrectEnrollmentUrl
(
self
.
course
,
demo
.
BIRTH_YEAR
)
self
.
assertCorrectEnrollmentUrl
(
self
.
course
,
demo
.
EDUCATION
)
self
.
assertCorrectEnrollmentUrl
(
self
.
course
,
demo
.
GENDER
)
self
.
assertCorrectEnrollmentUrl
(
self
.
course
,
demo
.
LOCATION
)
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment