Merge pull request #20393 from edx/nedbat/drf-yasg

Use drf-yasg for Open API documentation
This commit is contained in:
Ned Batchelder
2019-06-11 13:58:13 -04:00
committed by GitHub
27 changed files with 105 additions and 46 deletions

View File

@@ -125,7 +125,7 @@ class CourseRunImageSerializer(serializers.Serializer):
class CourseRunSerializerCommonFieldsMixin(serializers.Serializer):
schedule = CourseRunScheduleSerializer(source='*', required=False)
pacing_type = CourseRunPacingTypeField(source='self_paced', required=False,
choices=(('instructor_paced', False), ('self_paced', True),))
choices=((False, 'instructor_paced'), (True, 'self_paced'),))
class CourseRunSerializer(CourseRunSerializerCommonFieldsMixin, CourseRunTeamSerializerMixin, serializers.Serializer):

View File

@@ -680,7 +680,7 @@ STATICFILES_DIRS = [
# Locale/Internationalization
CELERY_TIMEZONE = 'UTC'
TIME_ZONE = 'America/New_York' # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
TIME_ZONE = 'UTC'
LANGUAGE_CODE = 'en' # http://www.i18nguy.com/unicode/language-identifiers.html
LANGUAGES_BIDI = lms.envs.common.LANGUAGES_BIDI
@@ -1173,7 +1173,7 @@ INSTALLED_APPS = [
'pipeline_mako',
# API Documentation
'rest_framework_swagger',
'drf_yasg',
'openedx.features.course_duration_limits',
'openedx.features.content_type_gating',

View File

@@ -108,6 +108,10 @@ def should_show_debug_toolbar(request):
DEBUG_TOOLBAR_MONGO_STACKTRACES = False
########################### API DOCS #################################
FEATURES['ENABLE_API_DOCS'] = True
################################ MILESTONES ################################
FEATURES['MILESTONES_APP'] = True

View File

@@ -3,7 +3,7 @@ from django.conf.urls import include, url
from django.conf.urls.static import static
from django.contrib.admin import autodiscover as django_autodiscover
from django.utils.translation import ugettext_lazy as _
from rest_framework_swagger.views import get_swagger_view
from openedx.core.openapi import schema_view
import contentstore.views
from cms.djangoapps.contentstore.views.organization import OrganizationListView
@@ -266,7 +266,9 @@ urlpatterns += [
if settings.FEATURES.get('ENABLE_API_DOCS'):
urlpatterns += [
url(r'^api-docs/$', get_swagger_view(title='Studio API')),
url(r'^swagger(?P<format>\.json|\.yaml)$', schema_view.without_ui(cache_timeout=0), name='schema-json'),
url(r'^swagger/$', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
url(r'^api-docs/$', schema_view.with_ui('swagger', cache_timeout=0)),
]
from openedx.core.djangoapps.plugins import constants as plugin_constants, plugin_urls

View File

@@ -28,6 +28,10 @@ class CourseModeSerializer(serializers.Serializer):
sku = serializers.CharField(required=False)
bulk_sku = serializers.CharField(required=False)
class Meta(object):
# For disambiguating within the drf-yasg swagger schema
ref_name = 'course_modes.CourseMode'
def create(self, validated_data):
"""
This method must be implemented for use in our

View File

@@ -30,7 +30,6 @@ class EntitlementsSerializerTests(ModuleStoreTestCase):
'course_uuid': str(entitlement.course_uuid),
'mode': entitlement.mode,
'refund_locked': False,
'enrollment_course_run': None,
'order_number': entitlement.order_number,
'created': entitlement.created.strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
'modified': entitlement.modified.strftime('%Y-%m-%dT%H:%M:%S.%fZ'),

View File

@@ -2,6 +2,8 @@
Default unit test configuration and fixtures.
"""
from __future__ import absolute_import, unicode_literals
from unittest import TestCase
import pytest
# Import hooks and fixture overrides from the cms package to
@@ -9,6 +11,10 @@ import pytest
from cms.conftest import _django_clear_site_cache, pytest_configure # pylint: disable=unused-import
# When using self.assertEquals, diffs are truncated. We don't want that, always
# show the whole diff.
TestCase.maxDiff = None
@pytest.fixture(autouse=True)
def no_webpack_loader(monkeypatch):

View File

@@ -36,6 +36,8 @@ class CourseModeSerializer(serializers.ModelSerializer):
class Meta(object):
model = CourseMode
fields = ('name', 'currency', 'price', 'sku', 'bulk_sku', 'expires')
# For disambiguating within the drf-yasg swagger schema
ref_name = 'commerce.CourseMode'
def validate_course_id(course_id):
@@ -77,6 +79,10 @@ class CourseSerializer(serializers.Serializer):
verification_deadline = PossiblyUndefinedDateTimeField(format=None, allow_null=True, required=False)
modes = CourseModeSerializer(many=True)
class Meta(object):
# For disambiguating within the drf-yasg swagger schema
ref_name = 'commerce.Course'
def validate(self, attrs):
""" Ensure the verification deadline occurs AFTER the course mode enrollment deadlines. """
verification_deadline = attrs.get('verification_deadline', None)

View File

@@ -61,6 +61,7 @@ from openedx.core.djangoapps.django_comment_common.signals import (
thread_voted
)
from openedx.core.djangoapps.django_comment_common.utils import get_course_discussion_settings
from openedx.core.djangoapps.user_api.accounts.api import get_account_settings
from openedx.core.djangoapps.user_api.accounts.views import AccountViewSet
from openedx.core.lib.exceptions import CourseNotFoundError, DiscussionNotFoundError, PageNotFoundError
@@ -365,10 +366,11 @@ def _get_user_profile_dict(request, usernames):
A dict with username as key and user profile details as value.
"""
request.GET = request.GET.copy() # Make a mutable copy of the GET parameters.
request.GET['username'] = usernames
user_profile_details = AccountViewSet.as_view({'get': 'list'})(request).data
if usernames:
username_list = usernames.split(",")
else:
username_list = []
user_profile_details = get_account_settings(request, username_list)
return {user['username']: user for user in user_profile_details}

View File

@@ -122,7 +122,7 @@ class GradingPolicyTestMixin(object):
"""
The view should return HTTP status 401 if user is unauthenticated.
"""
self.assert_get_for_course(expected_status_code=401, HTTP_AUTHORIZATION=None)
self.assert_get_for_course(expected_status_code=401, HTTP_AUTHORIZATION="")
def test_staff_authorized(self):
"""

View File

@@ -147,3 +147,5 @@ class UserSerializer(serializers.ModelSerializer):
model = User
fields = ('id', 'username', 'email', 'name', 'course_enrollments')
lookup_field = 'username'
# For disambiguating within the drf-yasg swagger schema
ref_name = 'mobile_api.User'

View File

@@ -1738,8 +1738,8 @@ class ProgramCourseEnrollmentOverviewViewTests(ProgramCacheTestCaseMixin, Shared
course_run_overview = response.data['course_runs'][0]
self.assertEqual(course_run_overview['start_date'], '2018-12-31T05:00:00Z')
self.assertEqual(course_run_overview['end_date'], '2019-01-02T05:00:00Z')
self.assertEqual(course_run_overview['start_date'], '2018-12-31T00:00:00Z')
self.assertEqual(course_run_overview['end_date'], '2019-01-02T00:00:00Z')
# course run end date may not exist
self.course_overview.end = None

View File

@@ -1749,8 +1749,8 @@ class RegistrationCodeRedemptionCourseEnrollment(SharedModuleStoreTestCase):
response = self.client.post(url)
self.assertEquals(response.status_code, 403)
# now reset the time to 5 mins from now in future in order to unblock
reset_time = datetime.now(UTC) + timedelta(seconds=300)
# now reset the time to 6 mins from now in future in order to unblock
reset_time = datetime.now(UTC) + timedelta(seconds=361)
with freeze_time(reset_time):
response = self.client.post(url)
self.assertEquals(response.status_code, 404)
@@ -1773,8 +1773,8 @@ class RegistrationCodeRedemptionCourseEnrollment(SharedModuleStoreTestCase):
response = self.client.get(url)
self.assertEquals(response.status_code, 403)
# now reset the time to 5 mins from now in future in order to unblock
reset_time = datetime.now(UTC) + timedelta(seconds=300)
# now reset the time to 6 mins from now in future in order to unblock
reset_time = datetime.now(UTC) + timedelta(seconds=361)
with freeze_time(reset_time):
response = self.client.get(url)
self.assertEquals(response.status_code, 404)

View File

@@ -997,7 +997,7 @@ MEDIA_URL = '/media/'
# Locale/Internationalization
CELERY_TIMEZONE = 'UTC'
TIME_ZONE = 'America/New_York' # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
TIME_ZONE = 'UTC'
LANGUAGE_CODE = 'en' # http://www.i18nguy.com/unicode/language-identifiers.html
# these languages display right to left
LANGUAGES_BIDI = ("he", "ar", "fa", "ur", "fa-ir", "rtl")
@@ -2150,6 +2150,8 @@ INSTALLED_APPS = [
# User API
'rest_framework',
'drf_yasg',
'openedx.core.djangoapps.user_api',
# Shopping cart

View File

@@ -8,7 +8,6 @@ from django.conf.urls.static import static
from django.contrib.admin import autodiscover as django_autodiscover
from django.utils.translation import ugettext_lazy as _
from django.views.generic.base import RedirectView
from rest_framework_swagger.views import get_swagger_view
from branding import views as branding_views
from config_models.views import ConfigurationModelCurrentAPIView
@@ -42,6 +41,7 @@ from openedx.core.djangoapps.programs.models import ProgramsApiConfig
from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.core.djangoapps.verified_track_content import views as verified_track_content_views
from openedx.core.openapi import schema_view
from openedx.features.enterprise_support.api import enterprise_enabled
from ratelimitbackend import admin
from static_template_view import views as static_template_view_views
@@ -964,7 +964,9 @@ if settings.BRANCH_IO_KEY:
if settings.FEATURES.get('ENABLE_API_DOCS'):
urlpatterns += [
url(r'^api-docs/$', get_swagger_view(title='LMS API')),
url(r'^swagger(?P<format>\.json|\.yaml)$', schema_view.without_ui(cache_timeout=0), name='schema-json'),
url(r'^swagger/$', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
url(r'^api-docs/$', schema_view.with_ui('swagger', cache_timeout=0)),
]
# edx-drf-extensions csrf app

View File

@@ -45,6 +45,10 @@ class CourseSerializer(serializers.Serializer): # pylint: disable=abstract-meth
invite_only = serializers.BooleanField(source="invitation_only")
course_modes = serializers.SerializerMethodField()
class Meta(object):
# For disambiguating within the drf-yasg swagger schema
ref_name = 'enrollment.Course'
def __init__(self, *args, **kwargs):
self.include_expired = kwargs.pop("include_expired", False)
super(CourseSerializer, self).__init__(*args, **kwargs)

View File

@@ -551,7 +551,7 @@ class TestAccountsAPI(CacheIsolationTestCase, UserAPITestCase):
("level_of_education", "none", u"ȻħȺɍłɇs", u'"ȻħȺɍłɇs" is not a valid choice.'),
("country", "GB", "XY", u'"XY" is not a valid choice.'),
("year_of_birth", 2009, "not_an_int", u"A valid integer is required."),
("name", "bob", "z" * 256, u"Ensure this value has at most 255 characters (it has 256)."),
("name", "bob", "z" * 256, u"Ensure this field has no more than 255 characters."),
("name", u"ȻħȺɍłɇs", " ", u"The name field must be at least 1 character long."),
("goals", "Smell the roses"),
("mailing_address", "Sesame Street"),

View File

@@ -470,7 +470,7 @@ def get_expected_validation_developer_message(preference_key, preference_value):
preference_key=preference_key,
preference_value=preference_value,
error={
"key": [u"Ensure this value has at most 255 characters (it has 256)."]
"key": [u"Ensure this field has no more than 255 characters."]
}
)

View File

@@ -34,6 +34,8 @@ class UserSerializer(serializers.HyperlinkedModelSerializer):
# This list is the minimal set required by the notification service
fields = ("id", "url", "email", "name", "username", "preferences")
read_only_fields = ("id", "email", "username")
# For disambiguating within the drf-yasg swagger schema
ref_name = 'user_api.User'
class UserPreferenceSerializer(serializers.HyperlinkedModelSerializer):

20
openedx/core/openapi.py Normal file
View File

@@ -0,0 +1,20 @@
"""
Open API support.
"""
from rest_framework import permissions
from drf_yasg.views import get_schema_view
from drf_yasg import openapi
schema_view = get_schema_view(
openapi.Info(
title="Open edX API",
default_version="v1",
description="APIs for access to Open edX information",
#terms_of_service="https://www.google.com/policies/terms/", # TODO: Do we have these?
contact=openapi.Contact(email="oscm@edx.org"),
#license=openapi.License(name="BSD License"), # TODO: What does this mean?
),
public=True,
permission_classes=(permissions.AllowAny,),
)

View File

@@ -207,7 +207,7 @@ class TestContentTypeGatingConfig(CacheIsolationTestCase):
all_configs[CourseLocator('7-True', 'test_course', 'run-None')],
{
'enabled': (True, Provenance.org),
'enabled_as_of': (datetime(2018, 1, 1, 5, tzinfo=pytz.UTC), Provenance.run),
'enabled_as_of': (datetime(2018, 1, 1, 0, tzinfo=pytz.UTC), Provenance.run),
'studio_override_enabled': (None, Provenance.default),
}
)
@@ -215,7 +215,7 @@ class TestContentTypeGatingConfig(CacheIsolationTestCase):
all_configs[CourseLocator('7-True', 'test_course', 'run-False')],
{
'enabled': (False, Provenance.run),
'enabled_as_of': (datetime(2018, 1, 1, 5, tzinfo=pytz.UTC), Provenance.run),
'enabled_as_of': (datetime(2018, 1, 1, 0, tzinfo=pytz.UTC), Provenance.run),
'studio_override_enabled': (None, Provenance.default),
}
)
@@ -223,7 +223,7 @@ class TestContentTypeGatingConfig(CacheIsolationTestCase):
all_configs[CourseLocator('7-None', 'test_course', 'run-None')],
{
'enabled': (True, Provenance.site),
'enabled_as_of': (datetime(2018, 1, 1, 5, tzinfo=pytz.UTC), Provenance.run),
'enabled_as_of': (datetime(2018, 1, 1, 0, tzinfo=pytz.UTC), Provenance.run),
'studio_override_enabled': (None, Provenance.default),
}
)

View File

@@ -236,21 +236,21 @@ class TestCourseDurationLimitConfig(CacheIsolationTestCase):
all_configs[CourseLocator('7-True', 'test_course', 'run-None')],
{
'enabled': (True, Provenance.org),
'enabled_as_of': (datetime(2018, 1, 1, 5, tzinfo=pytz.UTC), Provenance.run),
'enabled_as_of': (datetime(2018, 1, 1, 0, tzinfo=pytz.UTC), Provenance.run),
}
)
self.assertEqual(
all_configs[CourseLocator('7-True', 'test_course', 'run-False')],
{
'enabled': (False, Provenance.run),
'enabled_as_of': (datetime(2018, 1, 1, 5, tzinfo=pytz.UTC), Provenance.run),
'enabled_as_of': (datetime(2018, 1, 1, 0, tzinfo=pytz.UTC), Provenance.run),
}
)
self.assertEqual(
all_configs[CourseLocator('7-None', 'test_course', 'run-None')],
{
'enabled': (True, Provenance.site),
'enabled_as_of': (datetime(2018, 1, 1, 5, tzinfo=pytz.UTC), Provenance.run),
'enabled_as_of': (datetime(2018, 1, 1, 0, tzinfo=pytz.UTC), Provenance.run),
}
)

View File

@@ -37,7 +37,7 @@ celery==3.1.25 # Asynchronous task execution library
defusedxml
Django==1.11.21 # Web application framework
django-babel-underscore # underscore template extractor for django-babel (internationalization utilities)
django-config-models>=0.2.2 # Configuration models for Django allowing config management with auditing
django-config-models>=1.0.0 # Configuration models for Django allowing config management with auditing
django-cors-headers==2.1.0 # Used to allow to configure CORS headers for cross-domain requests
django-countries==4.6.1 # Country data for Django forms and model fields
django-crum # Middleware that stores the current request and user in thread local storage
@@ -54,7 +54,6 @@ django-pyfs
django-ratelimit
django-ratelimit-backend==1.1.1
django-require
django-rest-swagger # API documentation
django-sekizai
django-ses==0.8.4
django-simple-history
@@ -64,7 +63,9 @@ django-storages==1.4.1
django-user-tasks
django-waffle==0.12.0
django-webpack-loader # Used to wire webpack bundles into the django asset pipeline
djangorestframework==3.7.7
djangorestframework-jwt
drf-yasg # Replacement for django-rest-swagger
edx-ace==0.1.10
edx-analytics-data-api-client
edx-ccx-keys

View File

@@ -43,8 +43,8 @@ certifi==2019.3.9
cffi==1.12.3
chardet==3.0.4
click==7.0 # via user-util
coreapi==2.3.3 # via django-rest-swagger, openapi-codec
coreschema==0.0.4 # via coreapi
coreapi==2.3.3 # via drf-yasg
coreschema==0.0.4 # via coreapi, drf-yasg
git+https://github.com/edx/crowdsourcehinter.git@518605f0a95190949fe77bd39158450639e2e1dc#egg=crowdsourcehinter-xblock==0.1
cryptography==2.7
cssutils==1.0.2 # via pynliner
@@ -77,7 +77,6 @@ django-pyfs==2.0
django-ratelimit-backend==1.1.1
django-ratelimit==2.0.0
django-require==1.0.11
django-rest-swagger==2.2.0
django-sekizai==1.0.0
django-ses==0.8.4
django-simple-history==2.7.0
@@ -91,9 +90,10 @@ django==1.11.21
djangorestframework-jwt==1.11.0
git+https://github.com/edx/django-rest-framework-oauth.git@0a43e8525f1e3048efe4bc70c03de308a277197c#egg=djangorestframework-oauth==1.1.1
djangorestframework-xml==1.3.0 # via edx-enterprise
git+https://github.com/edx/django-rest-framework.git@1ceda7c086fddffd1c440cc86856441bbf0bd9cb#egg=djangorestframework==3.6.3
djangorestframework==3.7.7
docopt==0.6.2
docutils==0.14 # via botocore
drf-yasg==1.15.0
edx-ace==0.1.10
edx-analytics-data-api-client==0.15.3
edx-ccx-keys==0.2.1
@@ -136,6 +136,7 @@ help-tokens==1.0.3
html5lib==1.0.1
httplib2==0.13.0 # via oauth2, zendesk
idna==2.8
inflection==0.3.1 # via drf-yasg
ipaddress==1.0.22
isodate==0.6.0 # via python3-saml
itypes==1.1.0 # via coreapi
@@ -168,7 +169,6 @@ nodeenv==1.1.1
numpy==1.16.4
oauth2==1.9.0.post1
oauthlib==2.1.0
openapi-codec==1.3.2 # via django-rest-swagger
git+https://github.com/edx/edx-ora2.git@2.2.3#egg=ora2==2.2.3
path.py==8.2.1
pathtools==0.1.2
@@ -210,6 +210,8 @@ requests-oauthlib==1.1.0
requests==2.22.0
rest-condition==1.0.3
rfc6266-parser==0.0.5.post2
ruamel.ordereddict==0.4.13 # via ruamel.yaml
ruamel.yaml==0.15.96 # via drf-yasg
rules==2.0.1
s3transfer==0.1.13 # via boto3
sailthru-client==2.2.3
@@ -217,7 +219,7 @@ scipy==1.2.1
semantic-version==2.6.0 # via edx-drf-extensions
shapely==1.6.4.post2
shortuuid==0.5.0 # via edx-django-oauth2-provider
simplejson==3.16.0 # via django-rest-swagger, mailsnake, sailthru-client, zendesk
simplejson==3.16.0 # via mailsnake, sailthru-client, zendesk
singledispatch==3.4.0.3
six==1.11.0
slumber==0.7.1 # via edx-rest-api-client
@@ -231,7 +233,7 @@ stevedore==1.30.1
sympy==1.4
tincan==0.0.5 # via edx-enterprise
unicodecsv==0.14.1
uritemplate==3.0.0 # via coreapi
uritemplate==3.0.0 # via coreapi, drf-yasg
urllib3==1.23
user-util==0.1.5
voluptuous==0.11.5

View File

@@ -97,7 +97,6 @@ django-pyfs==2.0
django-ratelimit-backend==1.1.1
django-ratelimit==2.0.0
django-require==1.0.11
django-rest-swagger==2.2.0
django-sekizai==1.0.0
django-ses==0.8.4
django-simple-history==2.7.0
@@ -111,9 +110,10 @@ django==1.11.21
djangorestframework-jwt==1.11.0
git+https://github.com/edx/django-rest-framework-oauth.git@0a43e8525f1e3048efe4bc70c03de308a277197c#egg=djangorestframework-oauth==1.1.1
djangorestframework-xml==1.3.0
git+https://github.com/edx/django-rest-framework.git@1ceda7c086fddffd1c440cc86856441bbf0bd9cb#egg=djangorestframework==3.6.3
djangorestframework==3.7.7
docopt==0.6.2
docutils==0.14
drf-yasg==1.15.0
edx-ace==0.1.10
edx-analytics-data-api-client==0.15.3
edx-ccx-keys==0.2.1
@@ -173,6 +173,7 @@ idna==2.8
imagesize==1.1.0 # via sphinx
importlib-metadata==0.17
inflect==2.1.0
inflection==0.3.1
ipaddress==1.0.22
isodate==0.6.0
isort==4.3.20
@@ -214,7 +215,6 @@ nodeenv==1.1.1
numpy==1.16.4
oauth2==1.9.0.post1
oauthlib==2.1.0
openapi-codec==1.3.2
git+https://github.com/edx/edx-ora2.git@2.2.3#egg=ora2==2.2.3
packaging==19.0
path.py==8.2.1
@@ -279,6 +279,8 @@ requests-oauthlib==1.1.0
requests==2.22.0
rest-condition==1.0.3
rfc6266-parser==0.0.5.post2
ruamel.ordereddict==0.4.13
ruamel.yaml==0.15.96
rules==2.0.1
s3transfer==0.1.13
sailthru-client==2.2.3

View File

@@ -66,9 +66,6 @@ git+https://github.com/edx/MongoDBProxy.git@25b99097615bda06bd7cdfe5669ed80dc2a7
# This can go away when we update auth to not use django-rest-framework-oauth
git+https://github.com/edx/django-oauth-plus.git@01ec2a161dfc3465f9d35b9211ae790177418316#egg=django-oauth-plus==2.2.9.edx-1
# Why a DRF fork? See: https://openedx.atlassian.net/browse/PLAT-1581
git+https://github.com/edx/django-rest-framework.git@1ceda7c086fddffd1c440cc86856441bbf0bd9cb#egg=djangorestframework==3.6.3
# Why a drf-oauth fork? To add Django 1.11 compatibility to the abandoned repo.
# This dependency will be removed by this work: https://openedx.atlassian.net/browse/PLAT-1660
git+https://github.com/edx/django-rest-framework-oauth.git@0a43e8525f1e3048efe4bc70c03de308a277197c#egg=djangorestframework-oauth==1.1.1

View File

@@ -94,7 +94,6 @@ django-pyfs==2.0
django-ratelimit-backend==1.1.1
django-ratelimit==2.0.0
django-require==1.0.11
django-rest-swagger==2.2.0
django-sekizai==1.0.0
django-ses==0.8.4
django-simple-history==2.7.0
@@ -107,9 +106,10 @@ django-webpack-loader==0.6.0
djangorestframework-jwt==1.11.0
git+https://github.com/edx/django-rest-framework-oauth.git@0a43e8525f1e3048efe4bc70c03de308a277197c#egg=djangorestframework-oauth==1.1.1
djangorestframework-xml==1.3.0
git+https://github.com/edx/django-rest-framework.git@1ceda7c086fddffd1c440cc86856441bbf0bd9cb#egg=djangorestframework==3.6.3
djangorestframework==3.7.7
docopt==0.6.2
docutils==0.14
drf-yasg==1.15.0
edx-ace==0.1.10
edx-analytics-data-api-client==0.15.3
edx-ccx-keys==0.2.1
@@ -167,6 +167,7 @@ httpretty==0.9.6
idna==2.8
importlib-metadata==0.17 # via pluggy
inflect==2.1.0
inflection==0.3.1
ipaddress==1.0.22
isodate==0.6.0
isort==4.3.20
@@ -207,7 +208,6 @@ nodeenv==1.1.1
numpy==1.16.4
oauth2==1.9.0.post1
oauthlib==2.1.0
openapi-codec==1.3.2
git+https://github.com/edx/edx-ora2.git@2.2.3#egg=ora2==2.2.3
packaging==19.0 # via caniusepython3
path.py==8.2.1
@@ -270,6 +270,8 @@ requests-oauthlib==1.1.0
requests==2.22.0
rest-condition==1.0.3
rfc6266-parser==0.0.5.post2
ruamel.ordereddict==0.4.13
ruamel.yaml==0.15.96
rules==2.0.1
s3transfer==0.1.13
sailthru-client==2.2.3