diff --git a/edxval/api.py b/edxval/api.py index 7a272e6f..e8626765 100644 --- a/edxval/api.py +++ b/edxval/api.py @@ -35,6 +35,7 @@ EncodedVideo, Profile, ThirdPartyTranscriptCredentialsState, + TranscriptCredentials, TranscriptPreference, TranscriptProviderType, Video, @@ -42,7 +43,7 @@ VideoTranscript, ) from edxval.serializers import TranscriptPreferenceSerializer, TranscriptSerializer, VideoSerializer -from edxval.transcript_utils import Transcript +from edxval.transcript_utils import Transcript, validate_transcript_credentials from edxval.utils import THIRD_PARTY_TRANSCRIPTION_PLANS, TranscriptFormat, create_file_in_fs, get_transcript_format logger = logging.getLogger(__name__) # pylint: disable=C0103 @@ -1230,3 +1231,19 @@ def create_transcript_objects(xml, edx_video_id, resource_fs, static_dir, extern resource_fs=file_system, static_dir=static_dir ) + + +def create_or_update_transcript_credentials(**credentials): + """ + Internal API method to create or update transcript credentials. + """ + provider = credentials.pop('provider', None) + error_type, error_message, validated_credentials = validate_transcript_credentials( + provider=provider, **credentials + ) + if not error_message: + TranscriptCredentials.objects.update_or_create( + org=validated_credentials.pop('org'), provider=provider, defaults=validated_credentials + ) + + return dict(error_type=error_type, message=error_message) diff --git a/edxval/enum.py b/edxval/enum.py new file mode 100644 index 00000000..1b3ae1e1 --- /dev/null +++ b/edxval/enum.py @@ -0,0 +1,12 @@ +""" +Module containing all the Enumerations used in the API. +""" + + +class TranscriptionProviderErrorType: + """ + Transcription provider's errors enumeration. + """ + INVALID_CREDENTIALS = 1 + INVALID_PROVIDER = 2 + MISSING_REQUIRED_ATTRIBUTES = 3 diff --git a/edxval/settings.py b/edxval/settings.py index 23281751..1b9db652 100644 --- a/edxval/settings.py +++ b/edxval/settings.py @@ -179,3 +179,11 @@ # Set this value in the environment-specific files (e.g. local.py, production.py, test.py) FERNET_KEYS = ['insecure-ferent-key'] + +# Transcript provider settings variables, which will be overridden at deployment if needed +# NOTE: These settings must be added in edx-platform settings to allow deployment override and val integration +CIELO24_SETTINGS = dict( + CIELO24_API_VERSION=1, + CIELO24_BASE_API_URL="https://sandbox.cielo24.com/api", + CIELO24_LOGIN_URL="https://sandbox.cielo24.com/api/account/login" +) diff --git a/edxval/tests/test_api.py b/edxval/tests/test_api.py index a6eaf82e..6271db23 100644 --- a/edxval/tests/test_api.py +++ b/edxval/tests/test_api.py @@ -12,6 +12,7 @@ from tempfile import mkdtemp import mock +import responses import six from ddt import data, ddt, unpack from django.conf import settings @@ -45,6 +46,7 @@ EncodedVideo, Profile, ThirdPartyTranscriptCredentialsState, + TranscriptCredentials, TranscriptPreference, TranscriptProviderType, Video, @@ -3073,3 +3075,50 @@ def test_get_credentials_state(self, org, provider, result): """ credentials_state = api.get_transcript_credentials_state_for_org(org=org, provider=provider) self.assertEqual(credentials_state, result) + + +@ddt +class CreateUpdateTranscriptCredentialsTest(TestCase): + """ + Test Suite for transcript credentials create or update internal API. + """ + CIELO24_LOGIN_URL = "https://sandbox.cielo24.com/api/account/login" + + @data( + { + 'org': 'test', + 'provider': TranscriptProviderType.CIELO24, + 'api_key': 'test-api-key', + 'username': 'test-cielo-user' + }, + { + 'org': 'test', + 'provider': TranscriptProviderType.THREE_PLAY_MEDIA, + 'api_key': 'test-api-key', + 'api_secret_key': 'test-secret-key' + } + ) + @responses.activate + def test_transcript_credentials_success(self, credentials): + """ + Test that creating credentials works as expected with correct set of data. + """ + responses.add( + responses.GET, + self.CIELO24_LOGIN_URL, + body='{"ApiToken": "cielo-api-token"}', + status=status.HTTP_200_OK + ) + + transcript_credentials = TranscriptCredentials.objects.filter( + org=credentials.get('org'), + provider=credentials.get('provider') + ) + self.assertFalse(transcript_credentials.exists()) + + _ = api.create_or_update_transcript_credentials(**credentials) + transcript_credentials = TranscriptCredentials.objects.filter( + org=credentials.get('org'), + provider=credentials.get('provider') + ) + self.assertTrue(transcript_credentials.exists()) diff --git a/edxval/tests/test_transcript_utils.py b/edxval/tests/test_transcript_utils.py index 75784b72..3347b724 100644 --- a/edxval/tests/test_transcript_utils.py +++ b/edxval/tests/test_transcript_utils.py @@ -6,16 +6,21 @@ import json import textwrap -import unittest -import ddt +import responses +from ddt import data, ddt, unpack +from django.test import TestCase +from mock import patch +from rest_framework import status +from edxval.enum import TranscriptionProviderErrorType from edxval.exceptions import TranscriptsGenerationException -from edxval.transcript_utils import Transcript +from edxval.models import TranscriptProviderType +from edxval.transcript_utils import Transcript, validate_transcript_credentials -@ddt.ddt -class TestTranscriptUtils(unittest.TestCase): +@ddt +class TestTranscriptUtils(TestCase): """ Tests transcripts conversion util. """ @@ -50,12 +55,12 @@ def setUp(self): } """).encode('utf8') - @ddt.data( + @data( ('invalid_input_format', 'sjson'), ('sjson', 'invalid_output_format'), ('invalid_input_format', 'invalid_output_format') ) - @ddt.unpack + @unpack def test_invalid_transcript_format(self, input_format, output_format): """ Tests that transcript conversion raises `AssertionError` on invalid input/output formats. @@ -95,3 +100,97 @@ def test_convert_invalid_srt_to_sjson(self): invalid_srt_transcript = b'invalid SubRip file content' with self.assertRaises(TranscriptsGenerationException): Transcript.convert(invalid_srt_transcript, 'srt', 'sjson') + + +@ddt +class TestCredentialsUtils(TestCase): + """ + Test Suite for various transcript credential utilities. + """ + + CIELO24_LOGIN_URL = "https://sandbox.cielo24.com/api/account/login" + + @patch('edxval.transcript_utils.LOGGER') + @responses.activate + def test_cielo24_error(self, mock_logger): + """ + Test that when invalid cielo credentials are supplied, we get correct error response. + """ + expected_error_message = 'Invalid credentials supplied.' + responses.add( + responses.GET, + self.CIELO24_LOGIN_URL, + body=json.dumps({'error': expected_error_message}), + status=status.HTTP_400_BAD_REQUEST + ) + + credentials = { + 'org': 'test', + 'provider': TranscriptProviderType.CIELO24, + 'api_key': 'test-api-key', + 'username': 'test-cielo-user', + 'api_secret_key': '' + } + error_type, error_message, _ = validate_transcript_credentials(**credentials) + self.assertEqual(error_type, TranscriptionProviderErrorType.INVALID_CREDENTIALS) + self.assertEqual(error_message, expected_error_message) + + mock_logger.warning.assert_called_with( + '[Transcript Credentials] Unable to get api token -- response %s -- status %s.', + json.dumps({'error': error_message}), + status.HTTP_400_BAD_REQUEST + ) + + @data( + { + 'provider': 'unsupported-provider' + }, + { + 'org': 'test', + 'api_key': 'test-api-key' + } + ) + def test_transcript_credentials_invalid_provider(self, credentials): + """ + Test that validating credentials gives proper error in case of invalid provider. + """ + provider = credentials.pop('provider', '') + error_type, error_message, _ = validate_transcript_credentials(provider, **credentials) + self.assertEqual(error_type, TranscriptionProviderErrorType.INVALID_PROVIDER) + self.assertEqual(error_message, 'Invalid provider {provider}.'.format(provider=provider)) + + @data( + ( + {'provider': TranscriptProviderType.CIELO24}, + 'org and api_key and username' + ), + ( + {'provider': TranscriptProviderType.THREE_PLAY_MEDIA}, + 'org and api_key and api_secret_key' + ), + ( + {'provider': TranscriptProviderType.CIELO24, 'org': 'test-org'}, + 'api_key and username' + ), + ( + {'provider': TranscriptProviderType.CIELO24, 'org': 'test-org', 'api_key': 'test-api-key'}, + 'username' + ), + ( + {'org': 'test', 'provider': TranscriptProviderType.THREE_PLAY_MEDIA, 'api_key': 'test-api-key'}, + 'api_secret_key' + ) + ) + @unpack + def test_transcript_credentials_error(self, credentials, missing_keys): + """ + Test that validating credentials gives proper error in case of invalid input. + """ + provider = credentials.pop('provider') + expected_error_message = '{missing} must be specified for {provider}.'.format( + provider=provider, + missing=missing_keys + ) + error_type, error_message, _ = validate_transcript_credentials(provider, **credentials) + self.assertEqual(error_type, TranscriptionProviderErrorType.MISSING_REQUIRED_ATTRIBUTES) + self.assertEqual(error_message, expected_error_message) diff --git a/edxval/tests/test_views.py b/edxval/tests/test_views.py index 290a24ea..003b8625 100644 --- a/edxval/tests/test_views.py +++ b/edxval/tests/test_views.py @@ -9,7 +9,15 @@ from django.urls import reverse from rest_framework import status -from edxval.models import CourseVideo, EncodedVideo, Profile, TranscriptProviderType, Video, VideoTranscript +from edxval.models import ( + CourseVideo, + EncodedVideo, + Profile, + TranscriptCredentials, + TranscriptProviderType, + Video, + VideoTranscript, +) from edxval.serializers import TranscriptSerializer from edxval.tests import APIAuthTestCase, constants from edxval.utils import TranscriptFormat @@ -1054,3 +1062,81 @@ def test_update_hls_encodes_for_video(self): self.assertEqual(actual_encoded_video.url, expected_data['encode_data']['url']) self.assertEqual(actual_encoded_video.file_size, expected_data['encode_data']['file_size']) self.assertEqual(actual_encoded_video.bitrate, expected_data['encode_data']['bitrate']) + + +@ddt +class TranscriptCredentialsTest(APIAuthTestCase): + """ + Transcript credentials tests. + """ + + def test_transcript_credentials_unauthorized(self): + """ + Tests that if user is not logged in we get Unauthorized response. + """ + self.client.logout() + url = reverse('transcript-credentials', kwargs={'org': 'test', 'provider': 'provider'}) + response = self.client.get( + url, + content_type='application/json' + ) + response_status_code = response.status_code + self.assertEqual(response_status_code, status.HTTP_401_UNAUTHORIZED) + + @data( + ({'org': '', 'provider': ''}, 'org and provider must be specified.'), + ({'org': 'org', 'provider': ''}, 'provider must be specified.'), + ({'org': '', 'provider': 'provider'}, 'org must be specified.') + ) + @unpack + def test_fetch_credentials_missing_parameters(self, query_params, error_message): + """ + Verify that if query parameters are missing, then request fails with missing params error. + """ + url = reverse('transcript-credentials', kwargs=query_params) + response = self.client.get(url) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + response = json.loads(response.content.decode('utf-8')) + self.assertDictEqual(response, { + 'message': error_message, + }) + + def test_get_non_existent_credentials(self): + """ + Test that fetching a non-existing set of credentials results in failure. + """ + provider, org = "provider", "org" + query_params = {'provider': provider, 'org': org} + url = reverse('transcript-credentials', kwargs=query_params) + response = self.client.get(url) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + response = json.loads(response.content.decode('utf-8')) + self.assertDictEqual(response, { + 'message': "Credentials not found for provider {provider} & organization {org}".format( + provider=provider, + org=org + ), + }) + + def test_successful_fetch(self): + """ + Verify that persistent credentials are returned for correct query params. + """ + provider, org = "provider", "org" + credentials_dict = dict( + provider=provider, + org=org, + api_key='key', + api_secret='secret' + ) + TranscriptCredentials(**credentials_dict).save() + query_params = {'provider': provider, 'org': org} + url = reverse('transcript-credentials', kwargs=query_params) + response = self.client.get(url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + response = json.loads(response.content.decode('utf-8')) + self.assertEqual(response['api_key'], credentials_dict['api_key']) + self.assertEqual(response['api_secret_key'], credentials_dict['api_secret']) diff --git a/edxval/transcript_utils.py b/edxval/transcript_utils.py index 9224340c..d6111edc 100644 --- a/edxval/transcript_utils.py +++ b/edxval/transcript_utils.py @@ -6,14 +6,21 @@ from __future__ import absolute_import import json +import logging +import requests import six +from django.conf import settings from pysrt import SubRipFile, SubRipItem, SubRipTime from pysrt.srtexc import Error from six import text_type from six.moves import range +from edxval.enum import TranscriptionProviderErrorType from edxval.exceptions import TranscriptsGenerationException +from edxval.models import TranscriptProviderType + +LOGGER = logging.getLogger(__name__) class Transcript: @@ -120,3 +127,100 @@ def convert(cls, content, input_format, output_format): if output_format == 'srt': return cls.generate_srt_from_sjson(json.loads(content)) + + +def get_cielo_token_response(username, api_secure_key): + """ + Returns Cielo24 api token. + + Arguments: + username(str): Cielo24 username + api_securekey(str): Cielo24 api key + + Returns: + Response : Http response object + """ + cielo_api_url = settings.CIELO24_SETTINGS.get('CIELO24_LOGIN_URL', "https://sandbox.cielo24.com/api/account/login") + return requests.get(cielo_api_url, params={ + 'v': settings.CIELO24_SETTINGS.get('CIELO24_API_VERSION', 1), + 'username': username, + 'securekey': api_secure_key + }) + + +def get_api_token(username, api_key): + """ + Returns api token if valid credentials are provided. + """ + response = get_cielo_token_response(username=username, api_secure_key=api_key) + if not response.ok: + api_token = None + LOGGER.warning( + '[Transcript Credentials] Unable to get api token -- response %s -- status %s.', + response.text, + response.status_code, + ) + else: + api_token = json.loads(response.content.decode('utf-8'))['ApiToken'] + + return api_token + + +def validate_missing_attributes(provider, attributes, credentials): + """ + Returns error message if provided attributes are not presents in credentials. + """ + error_message, error_type = None, None + + missing = [attr for attr in attributes if attr not in credentials] + if missing: + error_message = '{missing} must be specified for {provider}.'.format( + provider=provider, + missing=' and '.join(missing) + ) + error_type = TranscriptionProviderErrorType.MISSING_REQUIRED_ATTRIBUTES + + return error_type, error_message + + +def validate_transcript_credentials(provider, **credentials): + """ + Validates transcript credentials. + + Validations: + Providers must be either 3PlayMedia or Cielo24. + In case of: + 3PlayMedia - 'api_key' and 'api_secret_key' are required. + Cielo24 - Valid 'api_key' and 'username' are required. + """ + error_type, error_message, validated_credentials = None, '', {} + if provider in [TranscriptProviderType.CIELO24, TranscriptProviderType.THREE_PLAY_MEDIA]: + if provider == TranscriptProviderType.CIELO24: + must_have_props = ('org', 'api_key', 'username') + error_type, error_message = validate_missing_attributes(provider, must_have_props, credentials) + + if not error_message: + # Get cielo api token and store it in api_key. + api_token = get_api_token(credentials['username'], credentials['api_key']) + if api_token: + validated_credentials.update({ + 'org': credentials['org'], + 'api_key': api_token + }) + else: + error_message = 'Invalid credentials supplied.' + error_type = TranscriptionProviderErrorType.INVALID_CREDENTIALS + else: + must_have_props = ('org', 'api_key', 'api_secret_key') + error_type, error_message = validate_missing_attributes(provider, must_have_props, credentials) + if not error_message: + validated_credentials.update({ + 'org': credentials['org'], + 'api_key': credentials['api_key'], + 'api_secret': credentials['api_secret_key'] + }) + else: + error_message = 'Invalid provider {provider}.'.format(provider=provider) + error_type = TranscriptionProviderErrorType.INVALID_PROVIDER + + return error_type, error_message, validated_credentials diff --git a/edxval/urls.py b/edxval/urls.py index b44bba0f..de201d78 100644 --- a/edxval/urls.py +++ b/edxval/urls.py @@ -35,4 +35,9 @@ views.VideoImagesView.as_view(), name='update-video-images' ), + url( + r'^videos/transcript-credentials/(?P[\w]*)/(?P[\w]*)$', + views.TranscriptCredentialsView.as_view(), + name='transcript-credentials' + ) ] diff --git a/edxval/utils.py b/edxval/utils.py index 964b3aad..cf77402a 100644 --- a/edxval/utils.py +++ b/edxval/utils.py @@ -12,6 +12,8 @@ from django.core.files.storage import get_storage_class from fs.path import combine from pysrt import SubRipFile +from rest_framework import status +from rest_framework.response import Response class TranscriptFormat: @@ -265,3 +267,23 @@ def invalidate_fernet_cached_properties(model, fields): del field.fernet except AttributeError: pass + + +def validate_request_params(data, attributes): + """ + Checks if the given set of attributes are missing from request and returns a response if true. + + Arguments: + data(dict): params dict + attributes(list): list of required attributes + Returns: + HTTP Response if params are missing,else None + """ + missing = [attr for attr in attributes if not data[attr]] + if missing: + return Response( + status=status.HTTP_400_BAD_REQUEST, + data={'message': '{missing} must be specified.'.format(missing=' and '.join(missing))} + + ) + return None diff --git a/edxval/views.py b/edxval/views.py index 0ec6365a..bfde9693 100644 --- a/edxval/views.py +++ b/edxval/views.py @@ -21,13 +21,14 @@ CourseVideo, EncodedVideo, Profile, + TranscriptCredentials, TranscriptProviderType, Video, VideoImage, VideoTranscript, ) from edxval.serializers import VideoSerializer -from edxval.utils import TranscriptFormat, validate_generated_images +from edxval.utils import TranscriptFormat, validate_generated_images, validate_request_params LOGGER = logging.getLogger(__name__) @@ -373,3 +374,68 @@ def put(self, request): EncodedVideo.objects.create(video=video, profile=profile, **encode_data) return Response(status=status.HTTP_200_OK) + + +class TranscriptCredentialsView(APIView): + """ + API View to fetch Transcript provider credentials. + """ + authentication_classes = (JwtAuthentication, SessionAuthentication) + permission_classes = (ReadRestrictedDjangoModelPermissions,) + queryset = TranscriptCredentials.objects.all() + + def get(self, request, provider, org): + """ + Retrieves the transcript credentials for a given organization and provider. + + **Example requests**: + + GET api/val/v0/videos/transcript-credentials/{provider}/{org} + + **GET Parameters**: + + The following parameters are required to get the credentials: + + * provider(str): transcript provider, which is either 3PlayMedia or Cielo24. + + * org(str): organization whose credentials are to be fetch. + + **Response Values** + + For a successful request, the following values are returned along with 200 status: + + * api_key(str): provider key + + * api_secret_key(str): provider api secret key(only for 3PlayMedia) + + * provider(str): transcript provider + + * org(str): organization whose credentials are fetched. + + For the error, 400 response code is returned with: + + * message(str): error message + """ + response = validate_request_params(dict(org=org, provider=provider), ['org', 'provider']) + if response: + return response + + try: + credentials = TranscriptCredentials.objects.get( + provider=provider, org=org + ) + status_code = status.HTTP_200_OK + data = dict( + api_key=credentials.api_key, + api_secret_key=credentials.api_secret, + org=credentials.org, + provider=credentials.provider + ) + except TranscriptCredentials.DoesNotExist: + status_code = status.HTTP_400_BAD_REQUEST + data = {'message': "Credentials not found for provider {provider} & organization {org}".format( + provider=provider, + org=org + )} + + return Response(status=status_code, data=data) diff --git a/requirements/dev.txt b/requirements/dev.txt index 5bef6dfe..19fca1d1 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -11,7 +11,7 @@ astroid==2.3.3 # via pylint, pylint-celery atomicwrites==1.3.0 # via pytest attrs==19.3.0 # via pytest backports.functools-lru-cache==1.6.1 # via caniusepython3 -bleach==3.1.3 # via readme-renderer +bleach==3.1.4 # via readme-renderer boto==2.49.0 # via -r requirements/base.in caniusepython3==7.2.0 # via -r requirements/quality.in certifi==2019.11.28 # via requests @@ -37,13 +37,13 @@ drf-jwt==1.14.0 # via edx-drf-extensions edx-django-utils==3.1 # via edx-drf-extensions edx-drf-extensions==5.0.2 # via -r requirements/base.in edx-lint==1.4.1 # via -r requirements/quality.in -edx-opaque-keys==2.0.1 # via edx-drf-extensions +edx-opaque-keys==2.0.2 # via edx-drf-extensions enum34==1.1.10 # via -r requirements/base.in filelock==3.0.12 # via tox, virtualenv fs==2.4.11 # via -r requirements/test.in future==0.18.2 # via pyjwkest idna==2.9 # via requests -importlib-metadata==1.5.0 # via importlib-resources, inflect, pluggy, pytest, tox, virtualenv +importlib-metadata==1.6.0 # via importlib-resources, inflect, pluggy, pytest, tox, virtualenv importlib-resources==1.4.0 # via virtualenv inflect==3.0.2 # via jinja2-pluralize isort==4.3.21 # via -r requirements/quality.in, pylint @@ -85,22 +85,23 @@ python-dateutil==2.8.1 # via edx-drf-extensions pytz==2019.3 # via django, fs readme-renderer==25.0 # via twine requests-toolbelt==0.9.1 # via twine -requests==2.23.0 # via caniusepython3, coveralls, edx-drf-extensions, pyjwkest, requests-toolbelt, twine +requests==2.23.0 # via caniusepython3, coveralls, edx-drf-extensions, pyjwkest, requests-toolbelt, responses, twine +responses==0.10.12 # via -r requirements/test.in rest-condition==1.0.3 # via edx-drf-extensions semantic-version==2.8.4 # via edx-drf-extensions -six==1.14.0 # via astroid, bleach, cryptography, diff-cover, django-waffle, edx-drf-extensions, edx-lint, edx-opaque-keys, fs, mock, more-itertools, packaging, pathlib2, pip-tools, pyjwkest, pytest, python-dateutil, readme-renderer, stevedore, tox, virtualenv +six==1.14.0 # via astroid, bleach, cryptography, diff-cover, django-waffle, edx-drf-extensions, edx-lint, edx-opaque-keys, fs, mock, more-itertools, packaging, pathlib2, pip-tools, pyjwkest, pytest, python-dateutil, readme-renderer, responses, stevedore, tox, virtualenv snowballstemmer==2.0.0 # via pydocstyle sqlparse==0.3.1 # via django stevedore==1.32.0 # via edx-opaque-keys toml==0.10.0 # via tox tox-battery==0.5.2 # via -r requirements/travis.in tox==3.14.6 # via -r requirements/travis.in, tox-battery -tqdm==4.43.0 # via twine +tqdm==4.44.1 # via twine twine==1.15.0 # via -r requirements/quality.in typed-ast==1.4.1 # via astroid typing==3.7.4.1 # via fs urllib3==1.25.8 # via requests -virtualenv==20.0.14 # via tox +virtualenv==20.0.15 # via tox wcwidth==0.1.9 # via pytest webencodings==0.5.1 # via bleach wrapt==1.11.2 # via astroid diff --git a/requirements/quality.txt b/requirements/quality.txt index e4832ca0..5047ba84 100644 --- a/requirements/quality.txt +++ b/requirements/quality.txt @@ -11,7 +11,7 @@ astroid==2.3.3 # via pylint, pylint-celery atomicwrites==1.3.0 # via pytest attrs==19.3.0 # via pytest backports.functools-lru-cache==1.6.1 # via caniusepython3 -bleach==3.1.3 # via readme-renderer +bleach==3.1.4 # via readme-renderer boto==2.49.0 # via -r requirements/base.in caniusepython3==7.2.0 # via -r requirements/quality.in certifi==2019.11.28 # via requests @@ -34,12 +34,12 @@ drf-jwt==1.14.0 # via edx-drf-extensions edx-django-utils==3.1 # via edx-drf-extensions edx-drf-extensions==5.0.2 # via -r requirements/base.in edx-lint==1.4.1 # via -r requirements/quality.in -edx-opaque-keys==2.0.1 # via edx-drf-extensions +edx-opaque-keys==2.0.2 # via edx-drf-extensions enum34==1.1.10 # via -r requirements/base.in fs==2.4.11 # via -r requirements/test.in future==0.18.2 # via pyjwkest idna==2.9 # via requests -importlib-metadata==1.5.0 # via pluggy, pytest +importlib-metadata==1.6.0 # via pluggy, pytest isort==4.3.21 # via -r requirements/quality.in, pylint lazy-object-proxy==1.4.3 # via astroid lxml==4.5.0 # via -r requirements/base.in @@ -75,14 +75,15 @@ python-dateutil==2.8.1 # via edx-drf-extensions pytz==2019.3 # via django, fs readme-renderer==25.0 # via twine requests-toolbelt==0.9.1 # via twine -requests==2.23.0 # via caniusepython3, edx-drf-extensions, pyjwkest, requests-toolbelt, twine +requests==2.23.0 # via caniusepython3, edx-drf-extensions, pyjwkest, requests-toolbelt, responses, twine +responses==0.10.12 # via -r requirements/test.in rest-condition==1.0.3 # via edx-drf-extensions semantic-version==2.8.4 # via edx-drf-extensions -six==1.14.0 # via astroid, bleach, cryptography, django-waffle, edx-drf-extensions, edx-lint, edx-opaque-keys, fs, mock, more-itertools, packaging, pathlib2, pyjwkest, pytest, python-dateutil, readme-renderer, stevedore +six==1.14.0 # via astroid, bleach, cryptography, django-waffle, edx-drf-extensions, edx-lint, edx-opaque-keys, fs, mock, more-itertools, packaging, pathlib2, pyjwkest, pytest, python-dateutil, readme-renderer, responses, stevedore snowballstemmer==2.0.0 # via pydocstyle sqlparse==0.3.1 # via django stevedore==1.32.0 # via edx-opaque-keys -tqdm==4.43.0 # via twine +tqdm==4.44.1 # via twine twine==1.15.0 # via -r requirements/quality.in typed-ast==1.4.1 # via astroid typing==3.7.4.1 # via fs diff --git a/requirements/test.in b/requirements/test.in index bc90f151..a5090072 100644 --- a/requirements/test.in +++ b/requirements/test.in @@ -8,3 +8,4 @@ fs mock pytest-cov pytest-django +responses diff --git a/requirements/test.txt b/requirements/test.txt index babd874a..e0866979 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -23,12 +23,12 @@ djangorestframework==3.9.4 # via -c requirements/constraints.txt, drf-jwt, edx- drf-jwt==1.14.0 # via edx-drf-extensions edx-django-utils==3.1 # via edx-drf-extensions edx-drf-extensions==5.0.2 # via -r requirements/base.in -edx-opaque-keys==2.0.1 # via edx-drf-extensions +edx-opaque-keys==2.0.2 # via edx-drf-extensions enum34==1.1.10 # via -r requirements/base.in fs==2.4.11 # via -r requirements/test.in future==0.18.2 # via pyjwkest idna==2.9 # via requests -importlib-metadata==1.5.0 # via pluggy, pytest +importlib-metadata==1.6.0 # via pluggy, pytest lxml==4.5.0 # via -r requirements/base.in mock==3.0.5 # via -r requirements/test.in more-itertools==5.0.0 # via -c requirements/constraints.txt, pytest @@ -51,10 +51,11 @@ pytest-django==3.8.0 # via -r requirements/test.in pytest==4.6.9 # via -c requirements/constraints.txt, pytest-cov, pytest-django python-dateutil==2.8.1 # via edx-drf-extensions pytz==2019.3 # via django, fs -requests==2.23.0 # via edx-drf-extensions, pyjwkest +requests==2.23.0 # via edx-drf-extensions, pyjwkest, responses +responses==0.10.12 # via -r requirements/test.in rest-condition==1.0.3 # via edx-drf-extensions semantic-version==2.8.4 # via edx-drf-extensions -six==1.14.0 # via cryptography, django-waffle, edx-drf-extensions, edx-opaque-keys, fs, mock, more-itertools, packaging, pathlib2, pyjwkest, pytest, python-dateutil, stevedore +six==1.14.0 # via cryptography, django-waffle, edx-drf-extensions, edx-opaque-keys, fs, mock, more-itertools, packaging, pathlib2, pyjwkest, pytest, python-dateutil, responses, stevedore sqlparse==0.3.1 # via django stevedore==1.32.0 # via edx-opaque-keys typing==3.7.4.1 # via fs diff --git a/requirements/travis.txt b/requirements/travis.txt index 0730a027..956c78f3 100644 --- a/requirements/travis.txt +++ b/requirements/travis.txt @@ -13,7 +13,7 @@ distlib==0.3.0 # via virtualenv docopt==0.6.2 # via coveralls filelock==3.0.12 # via tox, virtualenv idna==2.9 # via requests -importlib-metadata==1.5.0 # via importlib-resources, pluggy, tox, virtualenv +importlib-metadata==1.6.0 # via importlib-resources, pluggy, tox, virtualenv importlib-resources==1.4.0 # via virtualenv more-itertools==5.0.0 # via -c requirements/constraints.txt, zipp packaging==20.3 # via tox @@ -26,5 +26,5 @@ toml==0.10.0 # via tox tox-battery==0.5.2 # via -r requirements/travis.in tox==3.14.6 # via -r requirements/travis.in, tox-battery urllib3==1.25.8 # via requests -virtualenv==20.0.14 # via tox +virtualenv==20.0.15 # via tox zipp==1.0.0 # via -c requirements/constraints.txt, importlib-metadata, importlib-resources diff --git a/setup.py b/setup.py index cc3cd6a4..931bd03f 100644 --- a/setup.py +++ b/setup.py @@ -46,7 +46,7 @@ def load_requirements(*requirements_paths): return list(requirements) -VERSION = '1.2.6' +VERSION = '1.2.7' if sys.argv[-1] == 'tag': print("Tagging the version on github:")