Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
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
edx
edx-platform
Commits
9fa2dc03
Commit
9fa2dc03
authored
May 26, 2016
by
Renzo Lucioni
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #12565 from edx/renzo/program-by-id
Extend edX API utility to support retrieval of specific resources
parents
3c9a328b
62403eea
Show whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
193 additions
and
93 deletions
+193
-93
openedx/core/lib/edx_api_utils.py
+45
-25
openedx/core/lib/tests/test_edx_api_utils.py
+148
-68
No files found.
openedx/core/lib/edx_api_utils.py
View file @
9fa2dc03
...
@@ -11,23 +11,25 @@ from openedx.core.lib.token_utils import get_id_token
...
@@ -11,23 +11,25 @@ from openedx.core.lib.token_utils import get_id_token
log
=
logging
.
getLogger
(
__name__
)
log
=
logging
.
getLogger
(
__name__
)
def
get_edx_api_data
(
api_config
,
user
,
resource
,
querystring
=
None
,
cache_key
=
None
):
def
get_edx_api_data
(
api_config
,
user
,
resource
,
resource_id
=
None
,
querystring
=
None
,
cache_key
=
None
):
"""Fetch data from an API using provided API configuration and resource
"""GET data from an edX REST API.
name.
DRY utility for handling caching and pagination.
Arguments:
Arguments:
api_config (ConfigurationModel): The configuration model governing
api_config (ConfigurationModel): The configuration model governing interaction with the API.
interaction with the API.
user (User): The user to authenticate as when requesting data.
user (User): The user to authenticate as when requesting data.
resource(str): Name of the API resource for which data is being
resource (str): Name of the API resource being requested.
requested.
querystring(dict): Querystring parameters that might be required to
Keyword Arguments:
request data.
resource_id (int or str): Identifies a specific resource to be retrieved.
cache_key(str): Where to cache retrieved data. Omitting this will cause the
querystring (dict): Optional query string parameters.
cache to be bypassed.
cache_key (str): Where to cache retrieved data. The cache will be ignored if this is omitted
(neither inspected nor updated).
Returns:
Returns:
list of dict, representing data returned by the API.
Data returned by the API. When hitting a list endpoint, extracts "results" (list of dict)
returned by DRF-powered APIs.
"""
"""
no_data
=
[]
no_data
=
[]
...
@@ -36,34 +38,52 @@ def get_edx_api_data(api_config, user, resource, querystring=None, cache_key=Non
...
@@ -36,34 +38,52 @@ def get_edx_api_data(api_config, user, resource, querystring=None, cache_key=Non
return
no_data
return
no_data
if
cache_key
:
if
cache_key
:
cache_key
=
'{}.{}'
.
format
(
cache_key
,
resource_id
)
if
resource_id
else
cache_key
cached
=
cache
.
get
(
cache_key
)
cached
=
cache
.
get
(
cache_key
)
if
cached
is
not
None
:
if
cached
:
return
cached
return
cached
try
:
try
:
jwt
=
get_id_token
(
user
,
api_config
.
OAUTH2_CLIENT_NAME
)
jwt
=
get_id_token
(
user
,
api_config
.
OAUTH2_CLIENT_NAME
)
api
=
EdxRestApiClient
(
api_config
.
internal_api_url
,
jwt
=
jwt
)
api
=
EdxRestApiClient
(
api_config
.
internal_api_url
,
jwt
=
jwt
)
except
Exception
:
# pylint: disable=broad
-except
except
:
# pylint: disable=bare
-except
log
.
exception
(
'Failed to initialize the
%
s API client.'
,
api_config
.
API_NAME
)
log
.
exception
(
'Failed to initialize the
%
s API client.'
,
api_config
.
API_NAME
)
return
no_data
return
no_data
try
:
try
:
querystring
=
{}
if
not
querystring
else
querystring
endpoint
=
getattr
(
api
,
resource
)
response
=
getattr
(
api
,
resource
)
.
get
(
**
querystring
)
querystring
=
querystring
if
querystring
else
{}
response
=
endpoint
(
resource_id
)
.
get
(
**
querystring
)
if
resource_id
:
results
=
response
else
:
results
=
_traverse_pagination
(
response
,
endpoint
,
querystring
,
no_data
)
except
:
# pylint: disable=bare-except
log
.
exception
(
'Failed to retrieve data from the
%
s API.'
,
api_config
.
API_NAME
)
return
no_data
if
cache_key
:
cache
.
set
(
cache_key
,
results
,
api_config
.
cache_ttl
)
return
results
def
_traverse_pagination
(
response
,
endpoint
,
querystring
,
no_data
):
"""Traverse a paginated API response.
Extracts and concatenates "results" (list of dict) returned by DRF-powered APIs.
"""
results
=
response
.
get
(
'results'
,
no_data
)
results
=
response
.
get
(
'results'
,
no_data
)
page
=
1
page
=
1
next_page
=
response
.
get
(
'next'
,
None
)
next_page
=
response
.
get
(
'next'
)
while
next_page
:
while
next_page
:
page
+=
1
page
+=
1
querystring
[
'page'
]
=
page
querystring
[
'page'
]
=
page
response
=
getattr
(
api
,
resource
)
.
get
(
**
querystring
)
response
=
endpoint
.
get
(
**
querystring
)
results
+=
response
.
get
(
'results'
,
no_data
)
results
+=
response
.
get
(
'results'
,
no_data
)
next_page
=
response
.
get
(
'next'
,
None
)
next_page
=
response
.
get
(
'next'
)
except
Exception
:
# pylint: disable=broad-except
log
.
exception
(
'Failed to retrieve data from the
%
s API.'
,
api_config
.
API_NAME
)
return
no_data
if
cache_key
:
cache
.
set
(
cache_key
,
results
,
api_config
.
cache_ttl
)
return
results
return
results
openedx/core/lib/tests/test_edx_api_utils.py
View file @
9fa2dc03
"""Tests covering Api utils."""
"""Tests covering edX API utilities."""
import
json
import
unittest
import
unittest
from
django.conf
import
settings
from
django.core.cache
import
cache
from
django.core.cache
import
cache
from
django.test
import
TestCase
import
httpretty
import
httpretty
import
mock
import
mock
from
nose.plugins.attrib
import
attr
from
nose.plugins.attrib
import
attr
from
edx_oauth2_provider.tests.factories
import
ClientFactory
from
edx_oauth2_provider.tests.factories
import
ClientFactory
from
provider.constants
import
CONFIDENTIAL
from
provider.constants
import
CONFIDENTIAL
from
testfixtures
import
LogCapture
from
openedx.core.djangoapps.credentials.models
import
CredentialsApiConfig
from
openedx.core.djangoapps.credentials.tests.mixins
import
CredentialsApiConfigMixin
,
CredentialsDataMixin
from
openedx.core.djangoapps.programs.models
import
ProgramsApiConfig
from
openedx.core.djangoapps.programs.models
import
ProgramsApiConfig
from
openedx.core.djangoapps.programs.tests.mixins
import
ProgramsApiConfigMixin
,
ProgramsDataMixin
from
openedx.core.djangoapps.programs.tests.mixins
import
ProgramsApiConfigMixin
from
openedx.core.djangolib.testing.utils
import
CacheIsolationTestCase
from
openedx.core.djangolib.testing.utils
import
CacheIsolationTestCase
from
openedx.core.lib.edx_api_utils
import
get_edx_api_data
from
openedx.core.lib.edx_api_utils
import
get_edx_api_data
from
student.tests.factories
import
UserFactory
from
student.tests.factories
import
UserFactory
LOGGER_NAM
E
=
'openedx.core.lib.edx_api_utils'
UTILITY_MODUL
E
=
'openedx.core.lib.edx_api_utils'
@attr
(
'shard_2'
)
@attr
(
'shard_2'
)
class
TestApiDataRetrieval
(
CredentialsApiConfigMixin
,
CredentialsDataMixin
,
ProgramsApiConfigMixin
,
ProgramsDataMixin
,
@httpretty.activate
CacheIsolationTestCase
):
class
TestGetEdxApiData
(
ProgramsApiConfigMixin
,
CacheIsolationTestCase
):
"""Test
utility for API data retrieval
."""
"""Test
s for edX API data retrieval utility
."""
ENABLED_CACHES
=
[
'default'
]
ENABLED_CACHES
=
[
'default'
]
def
setUp
(
self
):
def
setUp
(
self
):
super
(
TestApiDataRetrieval
,
self
)
.
setUp
()
super
(
TestGetEdxApiData
,
self
)
.
setUp
()
ClientFactory
(
name
=
CredentialsApiConfig
.
OAUTH2_CLIENT_NAME
,
client_type
=
CONFIDENTIAL
)
ClientFactory
(
name
=
ProgramsApiConfig
.
OAUTH2_CLIENT_NAME
,
client_type
=
CONFIDENTIAL
)
self
.
user
=
UserFactory
()
self
.
user
=
UserFactory
()
ClientFactory
(
name
=
ProgramsApiConfig
.
OAUTH2_CLIENT_NAME
,
client_type
=
CONFIDENTIAL
)
cache
.
clear
()
cache
.
clear
()
@httpretty.activate
def
_mock_programs_api
(
self
,
responses
,
url
=
None
):
def
test_get_edx_api_data_programs
(
self
):
"""Helper for mocking out Programs API URLs."""
"""Verify programs data can be retrieved using get_edx_api_data."""
self
.
assertTrue
(
httpretty
.
is_enabled
(),
msg
=
'httpretty must be enabled to mock Programs API calls.'
)
url
=
url
if
url
else
ProgramsApiConfig
.
current
()
.
internal_api_url
.
strip
(
'/'
)
+
'/programs/'
httpretty
.
register_uri
(
httpretty
.
GET
,
url
,
responses
=
responses
)
def
_assert_num_requests
(
self
,
count
):
"""DRY helper for verifying request counts."""
self
.
assertEqual
(
len
(
httpretty
.
httpretty
.
latest_requests
),
count
)
def
test_get_unpaginated_data
(
self
):
"""Verify that unpaginated data can be retrieved."""
program_config
=
self
.
create_programs_config
()
program_config
=
self
.
create_programs_config
()
self
.
mock_programs_api
()
actual
=
get_edx_api_data
(
program_config
,
self
.
user
,
'programs'
)
expected_collection
=
[
'some'
,
'test'
,
'data'
]
self
.
assertEqual
(
data
=
{
actual
,
'next'
:
None
,
self
.
PROGRAMS_API_RESPONSE
[
'results'
]
'results'
:
expected_collection
,
}
self
.
_mock_programs_api
(
[
httpretty
.
Response
(
body
=
json
.
dumps
(
data
),
content_type
=
'application/json'
)]
)
)
# Verify the API was actually hit (not the cache).
actual_collection
=
get_edx_api_data
(
program_config
,
self
.
user
,
'programs'
)
self
.
assertEqual
(
len
(
httpretty
.
httpretty
.
latest_requests
),
1
)
self
.
assertEqual
(
actual_collection
,
expected_collection
)
def
test_get_edx_api_data_disable_config
(
self
):
# Verify the API was actually hit (not the cache)
"""Verify no data is retrieved if configuration is disabled."""
self
.
_assert_num_requests
(
1
)
program_config
=
self
.
create_programs_config
(
enabled
=
False
)
actual
=
get_edx_api_data
(
program_config
,
self
.
user
,
'programs'
)
def
test_get_paginated_data
(
self
):
self
.
assertEqual
(
actual
,
[])
"""Verify that paginated data can be retrieved."""
program_config
=
self
.
create_programs_config
()
expected_collection
=
[
'some'
,
'test'
,
'data'
]
url
=
ProgramsApiConfig
.
current
()
.
internal_api_url
.
strip
(
'/'
)
+
'/programs/?page={}'
responses
=
[]
for
page
,
record
in
enumerate
(
expected_collection
,
start
=
1
):
data
=
{
'next'
:
url
.
format
(
page
+
1
)
if
page
<
len
(
expected_collection
)
else
None
,
'results'
:
[
record
],
}
body
=
json
.
dumps
(
data
)
responses
.
append
(
httpretty
.
Response
(
body
=
body
,
content_type
=
'application/json'
)
)
self
.
_mock_programs_api
(
responses
)
actual_collection
=
get_edx_api_data
(
program_config
,
self
.
user
,
'programs'
)
self
.
assertEqual
(
actual_collection
,
expected_collection
)
self
.
_assert_num_requests
(
len
(
expected_collection
))
def
test_get_specific_resource
(
self
):
"""Verify that a specific resource can be retrieved."""
program_config
=
self
.
create_programs_config
()
resource_id
=
1
url
=
'{api_root}/programs/{resource_id}/'
.
format
(
api_root
=
ProgramsApiConfig
.
current
()
.
internal_api_url
.
strip
(
'/'
),
resource_id
=
resource_id
,
)
expected_resource
=
{
'key'
:
'value'
}
self
.
_mock_programs_api
(
[
httpretty
.
Response
(
body
=
json
.
dumps
(
expected_resource
),
content_type
=
'application/json'
)],
url
=
url
)
actual_resource
=
get_edx_api_data
(
program_config
,
self
.
user
,
'programs'
,
resource_id
=
resource_id
)
self
.
assertEqual
(
actual_resource
,
expected_resource
)
self
.
_assert_num_requests
(
1
)
@httpretty.activate
def
test_cache_utilization
(
self
):
def
test_get_edx_api_data_cache
(
self
):
"""Verify that when enabled, the cache is used."""
"""Verify that when enabled, the cache is used."""
program_config
=
self
.
create_programs_config
(
cache_ttl
=
1
)
program_config
=
self
.
create_programs_config
(
cache_ttl
=
5
)
self
.
mock_programs_api
()
expected_collection
=
[
'some'
,
'test'
,
'data'
]
data
=
{
'next'
:
None
,
'results'
:
expected_collection
,
}
self
.
_mock_programs_api
(
[
httpretty
.
Response
(
body
=
json
.
dumps
(
data
),
content_type
=
'application/json'
)],
)
resource_id
=
1
url
=
'{api_root}/programs/{resource_id}/'
.
format
(
api_root
=
ProgramsApiConfig
.
current
()
.
internal_api_url
.
strip
(
'/'
),
resource_id
=
resource_id
,
)
expected_resource
=
{
'key'
:
'value'
}
self
.
_mock_programs_api
(
[
httpretty
.
Response
(
body
=
json
.
dumps
(
expected_resource
),
content_type
=
'application/json'
)],
url
=
url
)
cache_key
=
ProgramsApiConfig
.
current
()
.
CACHE_KEY
# Warm up the cache.
# Warm up the cache.
get_edx_api_data
(
program_config
,
self
.
user
,
'programs'
,
cache_key
=
'test.key'
)
get_edx_api_data
(
program_config
,
self
.
user
,
'programs'
,
cache_key
=
cache_key
)
get_edx_api_data
(
program_config
,
self
.
user
,
'programs'
,
resource_id
=
resource_id
,
cache_key
=
cache_key
)
# Hit the cache.
# Hit the cache.
get_edx_api_data
(
program_config
,
self
.
user
,
'programs'
,
cache_key
=
'test.key'
)
actual_collection
=
get_edx_api_data
(
program_config
,
self
.
user
,
'programs'
,
cache_key
=
cache_key
)
self
.
assertEqual
(
actual_collection
,
expected_collection
)
actual_resource
=
get_edx_api_data
(
program_config
,
self
.
user
,
'programs'
,
resource_id
=
resource_id
,
cache_key
=
cache_key
)
self
.
assertEqual
(
actual_resource
,
expected_resource
)
# Verify that only two requests were made, not four.
self
.
_assert_num_requests
(
2
)
@mock.patch
(
UTILITY_MODULE
+
'.log.warning'
)
def
test_api_config_disabled
(
self
,
mock_warning
):
"""Verify that no data is retrieved if the provided config model is disabled."""
program_config
=
self
.
create_programs_config
(
enabled
=
False
)
# Verify only one request was made.
actual
=
get_edx_api_data
(
program_config
,
self
.
user
,
'programs'
)
self
.
assertEqual
(
len
(
httpretty
.
httpretty
.
latest_requests
),
1
)
self
.
assertTrue
(
mock_warning
.
called
)
self
.
assertEqual
(
actual
,
[])
@mock.patch
(
'edx_rest_api_client.client.EdxRestApiClient.__init__'
)
@mock.patch
(
'edx_rest_api_client.client.EdxRestApiClient.__init__'
)
def
test_get_edx_api_data_client_initialization_failure
(
self
,
mock_init
):
@mock.patch
(
UTILITY_MODULE
+
'.log.exception'
)
"""Verify no data is retrieved and exception logged when API client
def
test_client_initialization_failure
(
self
,
mock_exception
,
mock_init
):
fails to initialize.
"""Verify that an exception is logged when the API client fails to initialize."""
"""
program_config
=
self
.
create_programs_config
()
mock_init
.
side_effect
=
Exception
mock_init
.
side_effect
=
Exception
with
LogCapture
(
LOGGER_NAME
)
as
logger
:
program_config
=
self
.
create_programs_config
()
actual
=
get_edx_api_data
(
program_config
,
self
.
user
,
'programs'
)
actual
=
get_edx_api_data
(
program_config
,
self
.
user
,
'programs'
)
logger
.
check
(
(
LOGGER_NAME
,
'ERROR'
,
u'Failed to initialize the programs API client.'
)
self
.
assertTrue
(
mock_exception
.
called
)
)
self
.
assertEqual
(
actual
,
[])
self
.
assertEqual
(
actual
,
[])
self
.
assertTrue
(
mock_init
.
called
)
@
httpretty.activate
@
mock.patch
(
UTILITY_MODULE
+
'.log.exception'
)
def
test_
get_edx_api_data_retrieval_failure
(
self
):
def
test_
data_retrieval_failure
(
self
,
mock_exception
):
"""Verify
exception is logged when data can't be retrieved from API
."""
"""Verify
that an exception is logged when data can't be retrieved
."""
program_config
=
self
.
create_programs_config
()
program_config
=
self
.
create_programs_config
()
self
.
mock_programs_api
(
status_code
=
500
)
with
LogCapture
(
LOGGER_NAME
)
as
logger
:
self
.
_mock_programs_api
(
actual
=
get_edx_api_data
(
program_config
,
self
.
user
,
'programs'
)
[
httpretty
.
Response
(
body
=
'clunk'
,
content_type
=
'application/json'
,
status_code
=
500
)]
logger
.
check
(
(
LOGGER_NAME
,
'ERROR'
,
u'Failed to retrieve data from the programs API.'
)
)
)
self
.
assertEqual
(
actual
,
[])
# this test is skipped under cms because the credentials app is only installed under LMS.
actual
=
get_edx_api_data
(
program_config
,
self
.
user
,
'programs'
)
@unittest.skipUnless
(
settings
.
ROOT_URLCONF
==
'lms.urls'
,
'Test only valid in lms'
)
@httpretty.activate
self
.
assertTrue
(
mock_exception
.
called
)
def
test_get_edx_api_data_multiple_page
(
self
):
self
.
assertEqual
(
actual
,
[])
"""Verify that all data is retrieve for multiple page response."""
credentials_config
=
self
.
create_credentials_config
()
self
.
mock_credentials_api
(
self
.
user
,
is_next_page
=
True
)
querystring
=
{
'username'
:
self
.
user
.
username
}
actual
=
get_edx_api_data
(
credentials_config
,
self
.
user
,
'user_credentials'
,
querystring
=
querystring
)
expected_data
=
self
.
CREDENTIALS_NEXT_API_RESPONSE
[
'results'
]
+
self
.
CREDENTIALS_API_RESPONSE
[
'results'
]
self
.
assertEqual
(
actual
,
expected_data
)
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