edx_api_utils.py 4.13 KB
Newer Older
1 2 3 4
"""Helper functions to get data from APIs"""
from __future__ import unicode_literals
import logging

5
from django.conf import settings
6
from django.core.cache import cache
7
from django.core.exceptions import ImproperlyConfigured
8
from edx_rest_api_client.client import EdxRestApiClient
9
from provider.oauth2.models import Client
10

11
from openedx.core.lib.token_utils import JwtBuilder
12 13 14 15 16


log = logging.getLogger(__name__)


17 18
def get_edx_api_data(api_config, user, resource, api=None, resource_id=None,
                     querystring=None, cache_key=None, many=True, traverse_pagination=True):
19 20 21
    """GET data from an edX REST API.

    DRY utility for handling caching and pagination.
22 23

    Arguments:
24
        api_config (ConfigurationModel): The configuration model governing interaction with the API.
25
        user (User): The user to authenticate as when requesting data.
26 27 28
        resource (str): Name of the API resource being requested.

    Keyword Arguments:
29
        api (APIClient): API client to use for requesting data.
30 31 32 33
        resource_id (int or str): Identifies a specific resource to be retrieved.
        querystring (dict): Optional query string parameters.
        cache_key (str): Where to cache retrieved data. The cache will be ignored if this is omitted
            (neither inspected nor updated).
34 35
        many (bool): Whether the resource requested is a collection of objects, or a single object.
            If false, an empty dict will be returned in cases of failure rather than the default empty list.
36
        traverse_pagination (bool): Whether to traverse pagination or return paginated response..
37 38

    Returns:
39 40
        Data returned by the API. When hitting a list endpoint, extracts "results" (list of dict)
        returned by DRF-powered APIs.
41
    """
42
    no_data = [] if many else {}
43 44

    if not api_config.enabled:
45
        log.warning('%s configuration is disabled.', api_config.API_NAME)
46 47
        return no_data

48
    if cache_key:
49
        cache_key = '{}.{}'.format(cache_key, resource_id) if resource_id is not None else cache_key
50

51
        cached = cache.get(cache_key)
52
        if cached:
53
            return cached
54 55

    try:
56
        if not api:
57 58 59 60 61 62 63 64 65 66 67 68 69 70
            # TODO: Use the system's JWT_AUDIENCE and JWT_SECRET_KEY instead of client ID and name.
            client_name = api_config.OAUTH2_CLIENT_NAME

            try:
                client = Client.objects.get(name=client_name)
            except Client.DoesNotExist:
                raise ImproperlyConfigured(
                    'OAuth2 Client with name [{}] does not exist.'.format(client_name)
                )

            scopes = ['email', 'profile']
            expires_in = settings.OAUTH_ID_TOKEN_EXPIRATION
            jwt = JwtBuilder(user, secret=client.client_secret).build_token(scopes, expires_in, aud=client.client_id)

71
            api = EdxRestApiClient(api_config.internal_api_url, jwt=jwt)
72
    except:  # pylint: disable=bare-except
73
        log.exception('Failed to initialize the %s API client.', api_config.API_NAME)
74 75 76
        return no_data

    try:
77 78 79 80
        endpoint = getattr(api, resource)
        querystring = querystring if querystring else {}
        response = endpoint(resource_id).get(**querystring)

81
        if resource_id is not None:
82
            results = response
83
        elif traverse_pagination:
84
            results = _traverse_pagination(response, endpoint, querystring, no_data)
85 86
        else:
            results = response
87
    except:  # pylint: disable=bare-except
88
        log.exception('Failed to retrieve data from the %s API.', api_config.API_NAME)
89 90
        return no_data

91 92
    if cache_key:
        cache.set(cache_key, results, api_config.cache_ttl)
93 94

    return results
95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113


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)

    page = 1
    next_page = response.get('next')
    while next_page:
        page += 1
        querystring['page'] = page
        response = endpoint.get(**querystring)
        results += response.get('results', no_data)
        next_page = response.get('next')

    return results