diff --git a/openedx_events/event_bus/avro/custom_serializers.py b/openedx_events/event_bus/avro/custom_serializers.py index 37a8a150..d41616c2 100644 --- a/openedx_events/event_bus/avro/custom_serializers.py +++ b/openedx_events/event_bus/avro/custom_serializers.py @@ -5,6 +5,7 @@ from abc import ABC, abstractmethod from datetime import datetime +from ccx_keys.locator import CCXLocator from opaque_keys.edx.keys import CourseKey, UsageKey from opaque_keys.edx.locator import LibraryLocatorV2, LibraryUsageLocatorV2 @@ -49,6 +50,25 @@ def deserialize(data: str): return CourseKey.from_string(data) +class CcxCourseLocatorAvroSerializer(BaseCustomTypeAvroSerializer): + """ + CustomTypeAvroSerializer for CCXLocator class. + """ + + cls = CCXLocator + field_type = PYTHON_TYPE_TO_AVRO_MAPPING[str] + + @staticmethod + def serialize(obj) -> str: + """Serialize obj into string.""" + return str(obj) + + @staticmethod + def deserialize(data: str): + """Deserialize string into obj.""" + return CCXLocator.from_string(data) + + class DatetimeAvroSerializer(BaseCustomTypeAvroSerializer): """ CustomTypeAvroSerializer for datetime class. @@ -131,6 +151,7 @@ def deserialize(data: str): DEFAULT_CUSTOM_SERIALIZERS = [ CourseKeyAvroSerializer, + CcxCourseLocatorAvroSerializer, DatetimeAvroSerializer, LibraryLocatorV2AvroSerializer, LibraryUsageLocatorV2AvroSerializer, diff --git a/openedx_events/event_bus/avro/tests/schemas/org+openedx+learning+badge+awarded+v1_schema.avsc b/openedx_events/event_bus/avro/tests/schemas/org+openedx+learning+badge+awarded+v1_schema.avsc new file mode 100644 index 00000000..6bac2197 --- /dev/null +++ b/openedx_events/event_bus/avro/tests/schemas/org+openedx+learning+badge+awarded+v1_schema.avsc @@ -0,0 +1,100 @@ +{ + "name": "CloudEvent", + "type": "record", + "doc": "Avro Event Format for CloudEvents created with openedx_events/schema", + "fields": [ + { + "name": "badge", + "type": { + "name": "BadgeData", + "type": "record", + "fields": [ + { + "name": "uuid", + "type": "string" + }, + { + "name": "user", + "type": { + "name": "UserData", + "type": "record", + "fields": [ + { + "name": "id", + "type": "long" + }, + { + "name": "is_active", + "type": "boolean" + }, + { + "name": "pii", + "type": { + "name": "UserPersonalData", + "type": "record", + "fields": [ + { + "name": "username", + "type": "string" + }, + { + "name": "email", + "type": "string" + }, + { + "name": "name", + "type": "string" + } + ] + } + } + ] + } + }, + { + "name": "template", + "type": { + "name": "BadgeTemplateData", + "type": "record", + "fields": [ + { + "name": "uuid", + "type": "string" + }, + { + "name": "origin", + "type": "string" + }, + { + "name": "name", + "type": [ + "null", + "string" + ], + "default": null + }, + { + "name": "description", + "type": [ + "null", + "string" + ], + "default": null + }, + { + "name": "image_url", + "type": [ + "null", + "string" + ], + "default": null + } + ] + } + } + ] + } + } + ], + "namespace": "org.openedx.learning.badge.awarded.v1" +} \ No newline at end of file diff --git a/openedx_events/event_bus/avro/tests/schemas/org+openedx+learning+badge+revoked+v1_schema.avsc b/openedx_events/event_bus/avro/tests/schemas/org+openedx+learning+badge+revoked+v1_schema.avsc new file mode 100644 index 00000000..0cb96323 --- /dev/null +++ b/openedx_events/event_bus/avro/tests/schemas/org+openedx+learning+badge+revoked+v1_schema.avsc @@ -0,0 +1,100 @@ +{ + "name": "CloudEvent", + "type": "record", + "doc": "Avro Event Format for CloudEvents created with openedx_events/schema", + "fields": [ + { + "name": "badge", + "type": { + "name": "BadgeData", + "type": "record", + "fields": [ + { + "name": "uuid", + "type": "string" + }, + { + "name": "user", + "type": { + "name": "UserData", + "type": "record", + "fields": [ + { + "name": "id", + "type": "long" + }, + { + "name": "is_active", + "type": "boolean" + }, + { + "name": "pii", + "type": { + "name": "UserPersonalData", + "type": "record", + "fields": [ + { + "name": "username", + "type": "string" + }, + { + "name": "email", + "type": "string" + }, + { + "name": "name", + "type": "string" + } + ] + } + } + ] + } + }, + { + "name": "template", + "type": { + "name": "BadgeTemplateData", + "type": "record", + "fields": [ + { + "name": "uuid", + "type": "string" + }, + { + "name": "origin", + "type": "string" + }, + { + "name": "name", + "type": [ + "null", + "string" + ], + "default": null + }, + { + "name": "description", + "type": [ + "null", + "string" + ], + "default": null + }, + { + "name": "image_url", + "type": [ + "null", + "string" + ], + "default": null + } + ] + } + } + ] + } + } + ], + "namespace": "org.openedx.learning.badge.revoked.v1" +} \ No newline at end of file diff --git a/openedx_events/event_bus/avro/tests/schemas/org+openedx+learning+ccx+course+passing+status+updated+v1_schema.avsc b/openedx_events/event_bus/avro/tests/schemas/org+openedx+learning+ccx+course+passing+status+updated+v1_schema.avsc new file mode 100644 index 00000000..f538f2ef --- /dev/null +++ b/openedx_events/event_bus/avro/tests/schemas/org+openedx+learning+ccx+course+passing+status+updated+v1_schema.avsc @@ -0,0 +1,108 @@ +{ + "name": "CloudEvent", + "type": "record", + "doc": "Avro Event Format for CloudEvents created with openedx_events/schema", + "fields": [ + { + "name": "course_passing_status", + "type": { + "name": "CcxCoursePassingStatusData", + "type": "record", + "fields": [ + { + "name": "is_passing", + "type": "boolean" + }, + { + "name": "user", + "type": { + "name": "UserData", + "type": "record", + "fields": [ + { + "name": "id", + "type": "long" + }, + { + "name": "is_active", + "type": "boolean" + }, + { + "name": "pii", + "type": { + "name": "UserPersonalData", + "type": "record", + "fields": [ + { + "name": "username", + "type": "string" + }, + { + "name": "email", + "type": "string" + }, + { + "name": "name", + "type": "string" + } + ] + } + } + ] + } + }, + { + "name": "course", + "type": { + "name": "CcxCourseData", + "type": "record", + "fields": [ + { + "name": "ccx_course_key", + "type": "string" + }, + { + "name": "master_course_key", + "type": "string" + }, + { + "name": "display_name", + "type": "string" + }, + { + "name": "coach_email", + "type": "string" + }, + { + "name": "start", + "type": [ + "null", + "string" + ], + "default": null + }, + { + "name": "end", + "type": [ + "null", + "string" + ], + "default": null + }, + { + "name": "max_students_allowed", + "type": [ + "null", + "long" + ], + "default": null + } + ] + } + } + ] + } + } + ], + "namespace": "org.openedx.learning.ccx.course.passing.status.updated.v1" +} \ No newline at end of file diff --git a/openedx_events/event_bus/avro/tests/schemas/org+openedx+learning+course+passing+status+updated+v1_schema.avsc b/openedx_events/event_bus/avro/tests/schemas/org+openedx+learning+course+passing+status+updated+v1_schema.avsc new file mode 100644 index 00000000..89da6104 --- /dev/null +++ b/openedx_events/event_bus/avro/tests/schemas/org+openedx+learning+course+passing+status+updated+v1_schema.avsc @@ -0,0 +1,92 @@ +{ + "name": "CloudEvent", + "type": "record", + "doc": "Avro Event Format for CloudEvents created with openedx_events/schema", + "fields": [ + { + "name": "course_passing_status", + "type": { + "name": "CoursePassingStatusData", + "type": "record", + "fields": [ + { + "name": "is_passing", + "type": "boolean" + }, + { + "name": "course", + "type": { + "name": "CourseData", + "type": "record", + "fields": [ + { + "name": "course_key", + "type": "string" + }, + { + "name": "display_name", + "type": "string" + }, + { + "name": "start", + "type": [ + "null", + "string" + ], + "default": null + }, + { + "name": "end", + "type": [ + "null", + "string" + ], + "default": null + } + ] + } + }, + { + "name": "user", + "type": { + "name": "UserData", + "type": "record", + "fields": [ + { + "name": "id", + "type": "long" + }, + { + "name": "is_active", + "type": "boolean" + }, + { + "name": "pii", + "type": { + "name": "UserPersonalData", + "type": "record", + "fields": [ + { + "name": "username", + "type": "string" + }, + { + "name": "email", + "type": "string" + }, + { + "name": "name", + "type": "string" + } + ] + } + } + ] + } + } + ] + } + } + ], + "namespace": "org.openedx.learning.course.passing.status.updated.v1" +} \ No newline at end of file diff --git a/openedx_events/event_bus/avro/tests/test_avro.py b/openedx_events/event_bus/avro/tests/test_avro.py index bf2f1836..b42a39e8 100644 --- a/openedx_events/event_bus/avro/tests/test_avro.py +++ b/openedx_events/event_bus/avro/tests/test_avro.py @@ -5,6 +5,7 @@ from typing import List from unittest import TestCase +from ccx_keys.locator import CCXLocator from fastavro import schemaless_reader, schemaless_writer from fastavro.repository.base import SchemaRepositoryError from fastavro.schema import load_schema @@ -107,6 +108,7 @@ def generate_test_event_data_for_data_type(data_type): # pragma: no cover LibraryUsageLocatorV2: LibraryUsageLocatorV2.from_string('lb:MITx:reallyhardproblems:problem:problem1'), List[int]: [1, 2, 3], datetime: datetime.now(), + CCXLocator: CCXLocator(org='edx', course='DemoX', run='Demo_course', ccx='1'), } data_dict = {} for attribute in data_type.__attrs_attrs__: diff --git a/openedx_events/event_bus/avro/tests/test_custom_serializers.py b/openedx_events/event_bus/avro/tests/test_custom_serializers.py new file mode 100644 index 00000000..ab852fc0 --- /dev/null +++ b/openedx_events/event_bus/avro/tests/test_custom_serializers.py @@ -0,0 +1,30 @@ +"""Test custom servializers""" +from unittest import TestCase + +from ccx_keys.locator import CCXLocator + +from openedx_events.event_bus.avro.custom_serializers import CcxCourseLocatorAvroSerializer + + +class TestCCXLocatorSerailizer(TestCase): + """Test case for CCXLocator serializer.""" + + def test_serialize(self): + """ + Test case for serializing CCXLocator object. + """ + + obj1 = CCXLocator(org="edx", course="DemoX", run="Demo_course", ccx="1") + expected1 = "ccx-v1:edx+DemoX+Demo_course+ccx@1" + result1 = CcxCourseLocatorAvroSerializer.serialize(obj1) + self.assertEqual(result1, expected1) + + def test_deseialize(self): + """ + Test case for deserializing CCXLocator object. + """ + + data1 = "ccx-v1:edx+DemoX+Demo_course+ccx@1" + expected1 = CCXLocator(org="edx", course="DemoX", run="Demo_course", ccx="1") + result1 = CcxCourseLocatorAvroSerializer.deserialize(data1) + self.assertEqual(result1, expected1) diff --git a/openedx_events/learning/data.py b/openedx_events/learning/data.py index 43e7e0bf..ba22df9a 100644 --- a/openedx_events/learning/data.py +++ b/openedx_events/learning/data.py @@ -8,6 +8,7 @@ from typing import List, Optional import attr +from ccx_keys.locator import CCXLocator from opaque_keys.edx.keys import CourseKey, UsageKey @@ -74,6 +75,31 @@ class CourseData: end = attr.ib(type=datetime, default=None) +@attr.s(frozen=True) +class CcxCourseData: + """ + Represents data for a CCX (Custom Courses for edX) course. + + Attributes: + ccx_course_key (CCXLocator): The unique identifier for the CCX course. + master_course_key (CourseKey): The unique identifier for the original course from which the CCX is derived. + display_name (str): The name of the CCX course as it should appear to users. + coach_email (str): The email address of the coach (instructor) for the CCX course. + start (str, optional): The start date of the CCX course. Defaults to None, indicating no specific start date. + end (str, optional): The end date of the CCX course. Defaults to None, indicating no specific end date. + max_students_allowed (int, optional): The maximum number of students that can enroll in the CCX course. + Defaults to None, indicating no limit. + """ + + ccx_course_key = attr.ib(type=CCXLocator) + master_course_key = attr.ib(type=CourseKey) + display_name = attr.ib(type=str, factory=str) + coach_email = attr.ib(type=str, factory=str) + start = attr.ib(type=str, default=None) + end = attr.ib(type=str, default=None) + max_students_allowed = attr.ib(type=int, default=None) + + @attr.s(frozen=True) class CourseEnrollmentData: """ @@ -493,3 +519,73 @@ class ORASubmissionData: created_at = attr.ib(type=datetime) submitted_at = attr.ib(type=datetime) answer = attr.ib(type=ORASubmissionAnswer) + + +@attr.s(frozen=True) +class CoursePassingStatusData: + """ + Represents the event data when a user's grade is updated, indicates if current grade is enough for course passing. + + Attributes: + is_passing (bool): Indicates whether the user's grade is enough to pass the course. + user (UserData): An instance of UserData containing information about the user whose grade was updated. + course (CourseData): An instance of CourseData containing details about the course + in which the grade was updated. + """ + + is_passing = attr.ib(type=bool) + course = attr.ib(type=CourseData) + user = attr.ib(type=UserData) + + +@attr.s(frozen=True) +class CcxCoursePassingStatusData(CoursePassingStatusData): + """ + Extends CoursePassingStatusData for CCX courses, specifying CCX course data. + + This class is used for events where a user's grade crosses a threshold specifically in a CCX course, + providing a custom course attribute suited for CCX course instances. + + Attributes: + course (CcxCourseData): An instance of CcxCourseData containing details about the CCX course + in which the grade threshold was crossed. + All other attributes are inherited from CoursePassingStatusData. + """ + + course = attr.ib(type=CcxCourseData) + + +@attr.s(frozen=True) +class BadgeTemplateData: + """ + Attributes defined for Open edX badge template data object. + + Arguments: + uuid (str): UUID of the badge template + origin (str): type of badge template + name (str): badge name + description (str): badge description + image_url (str): badge image url + """ + + uuid = attr.ib(type=str) + origin = attr.ib(type=str) + name = attr.ib(type=str, default=None) + description = attr.ib(type=str, default=None) + image_url = attr.ib(type=str, default=None) + + +@attr.s(frozen=True) +class BadgeData: + """ + Attributes defined for the Open edX badge data object. + + Arguments: + uuid (str): the UUID of the badge + user (UserData): user associated with the badge + template (BadgeTemplateData): badge template data + """ + + uuid = attr.ib(type=str) + user = attr.ib(type=UserData) + template = attr.ib(type=BadgeTemplateData) diff --git a/openedx_events/learning/signals.py b/openedx_events/learning/signals.py index 9459d891..d3970066 100644 --- a/openedx_events/learning/signals.py +++ b/openedx_events/learning/signals.py @@ -9,12 +9,15 @@ """ from openedx_events.learning.data import ( + BadgeData, + CcxCoursePassingStatusData, CertificateData, CohortData, CourseAccessRoleData, CourseDiscussionConfigurationData, CourseEnrollmentData, CourseNotificationData, + CoursePassingStatusData, DiscussionThreadData, ExamAttemptData, ORASubmissionData, @@ -350,3 +353,51 @@ "submission": ORASubmissionData, }, ) + + +# .. event_type: org.openedx.learning.course.passing.status.updated.v1 +# .. event_name: COURSE_PASSING_STATUS_UPDATED +# .. event_description: Emitted when course grade updates. +# .. event_data: CoursePassingStatusData +COURSE_PASSING_STATUS_UPDATED = OpenEdxPublicSignal( + event_type="org.openedx.learning.course.passing.status.updated.v1", + data={ + "course_passing_status": CoursePassingStatusData, + } +) + + +# .. event_type: org.openedx.learning.ccx.course.passing.status.updated.v1 +# .. event_name: CCX_COURSE_PASSING_STATUS_UPDATED +# .. event_description: Emitted when a CCX course grade updates. +# .. event_data: CcxCoursePassingStatusData +CCX_COURSE_PASSING_STATUS_UPDATED = OpenEdxPublicSignal( + event_type="org.openedx.learning.ccx.course.passing.status.updated.v1", + data={ + "course_passing_status": CcxCoursePassingStatusData, + } +) + + +# .. event_type: org.openedx.learning.badge.awarded.v1 +# .. event_name: BADGE_AWARDED +# .. event_description: Emit when a badge is awarded to a learner +# .. event_data: BadgeData +BADGE_AWARDED = OpenEdxPublicSignal( + event_type="org.openedx.learning.badge.awarded.v1", + data={ + "badge": BadgeData, + } +) + + +# .. event_type: org.openedx.learning.badge.revoked.v1 +# .. event_name: BADGE_REVOKED +# .. event_description: Emit when a badge is revoked for a learner +# .. event_data: BadgeData +BADGE_REVOKED = OpenEdxPublicSignal( + event_type="org.openedx.learning.badge.revoked.v1", + data={ + "badge": BadgeData, + } +) diff --git a/requirements/base.in b/requirements/base.in index 5afd59f7..5d6be5bc 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -6,3 +6,4 @@ django attrs fastavro edx-opaque-keys[django] +edx-ccx-keys diff --git a/requirements/base.txt b/requirements/base.txt index 0b3f8a4f..decac25d 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -16,7 +16,7 @@ cffi==1.16.0 # via pynacl click==8.1.7 # via edx-django-utils -django==4.2.11 +django==4.2.13 # via # -c https://raw.githubusercontent.com/openedx/edx-lint/master/edx_lint/files/common_constraints.txt # -r requirements/base.in @@ -29,10 +29,14 @@ django-waffle==4.1.0 # via edx-django-utils dnspython==2.6.1 # via pymongo +edx-ccx-keys==1.3.0 + # via -r requirements/base.in edx-django-utils==5.13.0 # via -r requirements/base.in edx-opaque-keys[django]==2.9.0 - # via -r requirements/base.in + # via + # -r requirements/base.in + # edx-ccx-keys fastavro==1.9.4 # via -r requirements/base.in newrelic==9.9.0 @@ -47,6 +51,8 @@ pymongo==4.4.0 # via edx-opaque-keys pynacl==1.5.0 # via edx-django-utils +six==1.16.0 + # via edx-ccx-keys sqlparse==0.5.0 # via django stevedore==5.2.0 diff --git a/requirements/dev.txt b/requirements/dev.txt index dd80c7e7..35095dde 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -40,7 +40,6 @@ certifi==2024.2.2 cffi==1.16.0 # via # -r requirements/quality.txt - # cryptography # pynacl chardet==5.2.0 # via @@ -76,10 +75,6 @@ coverage[toml]==7.5.1 # via # -r requirements/quality.txt # pytest-cov -cryptography==42.0.6 - # via - # -r requirements/quality.txt - # secretstorage ddt==1.7.2 # via -r requirements/quality.txt diff-cover==9.0.0 @@ -92,7 +87,7 @@ distlib==0.3.8 # via # -r requirements/ci.txt # virtualenv -django==4.2.11 +django==4.2.13 # via # -c https://raw.githubusercontent.com/openedx/edx-lint/master/edx_lint/files/common_constraints.txt # -r requirements/quality.txt @@ -115,12 +110,16 @@ docutils==0.20.1 # via # -r requirements/quality.txt # readme-renderer +edx-ccx-keys==1.3.0 + # via -r requirements/quality.txt edx-django-utils==5.13.0 # via -r requirements/quality.txt edx-lint==5.3.6 # via -r requirements/quality.txt edx-opaque-keys[django]==2.9.0 - # via -r requirements/quality.txt + # via + # -r requirements/quality.txt + # edx-ccx-keys exceptiongroup==1.2.1 # via # -r requirements/quality.txt @@ -168,11 +167,6 @@ jaraco-functools==4.0.1 # via # -r requirements/quality.txt # keyring -jeepney==0.8.0 - # via - # -r requirements/quality.txt - # keyring - # secretstorage jinja2==3.1.4 # via # -r requirements/quality.txt @@ -341,13 +335,10 @@ rich==13.7.1 # via # -r requirements/quality.txt # twine -secretstorage==3.3.3 - # via - # -r requirements/quality.txt - # keyring six==1.16.0 # via # -r requirements/quality.txt + # edx-ccx-keys # edx-lint snowballstemmer==2.2.0 # via diff --git a/requirements/doc.txt b/requirements/doc.txt index 949ba3ef..405d3d03 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -34,7 +34,6 @@ certifi==2024.2.2 cffi==1.16.0 # via # -r requirements/test.txt - # cryptography # pynacl charset-normalizer==3.3.2 # via requests @@ -51,11 +50,9 @@ coverage[toml]==7.5.1 # via # -r requirements/test.txt # pytest-cov -cryptography==42.0.6 - # via secretstorage ddt==1.7.2 # via -r requirements/test.txt -django==4.2.11 +django==4.2.13 # via # -c https://raw.githubusercontent.com/openedx/edx-lint/master/edx_lint/files/common_constraints.txt # -r requirements/test.txt @@ -83,10 +80,14 @@ docutils==0.19 # readme-renderer # restructuredtext-lint # sphinx +edx-ccx-keys==1.3.0 + # via -r requirements/test.txt edx-django-utils==5.13.0 # via -r requirements/test.txt edx-opaque-keys[django]==2.9.0 - # via -r requirements/test.txt + # via + # -r requirements/test.txt + # edx-ccx-keys exceptiongroup==1.2.1 # via # -r requirements/test.txt @@ -116,10 +117,6 @@ jaraco-context==5.3.0 # via keyring jaraco-functools==4.0.1 # via keyring -jeepney==0.8.0 - # via - # keyring - # secretstorage jinja2==3.1.4 # via # -r requirements/test.txt @@ -226,10 +223,11 @@ rfc3986==2.0.0 # via twine rich==13.7.1 # via twine -secretstorage==3.3.3 - # via keyring six==1.16.0 - # via livereload + # via + # -r requirements/test.txt + # edx-ccx-keys + # livereload snowballstemmer==2.2.0 # via sphinx soupsieve==2.5 diff --git a/requirements/quality.txt b/requirements/quality.txt index f6b2ff6d..7c841d18 100644 --- a/requirements/quality.txt +++ b/requirements/quality.txt @@ -27,7 +27,6 @@ certifi==2024.2.2 cffi==1.16.0 # via # -r requirements/test.txt - # cryptography # pynacl charset-normalizer==3.3.2 # via requests @@ -48,13 +47,11 @@ coverage[toml]==7.5.1 # via # -r requirements/test.txt # pytest-cov -cryptography==42.0.6 - # via secretstorage ddt==1.7.2 # via -r requirements/test.txt dill==0.3.8 # via pylint -django==4.2.11 +django==4.2.13 # via # -c https://raw.githubusercontent.com/openedx/edx-lint/master/edx_lint/files/common_constraints.txt # -r requirements/test.txt @@ -75,12 +72,16 @@ dnspython==2.6.1 # pymongo docutils==0.20.1 # via readme-renderer +edx-ccx-keys==1.3.0 + # via -r requirements/test.txt edx-django-utils==5.13.0 # via -r requirements/test.txt edx-lint==5.3.6 # via -r requirements/quality.in edx-opaque-keys[django]==2.9.0 - # via -r requirements/test.txt + # via + # -r requirements/test.txt + # edx-ccx-keys exceptiongroup==1.2.1 # via # -r requirements/test.txt @@ -110,10 +111,6 @@ jaraco-context==5.3.0 # via keyring jaraco-functools==4.0.1 # via keyring -jeepney==0.8.0 - # via - # keyring - # secretstorage jinja2==3.1.4 # via # -r requirements/test.txt @@ -225,10 +222,11 @@ rfc3986==2.0.0 # via twine rich==13.7.1 # via twine -secretstorage==3.3.3 - # via keyring six==1.16.0 - # via edx-lint + # via + # -r requirements/test.txt + # edx-ccx-keys + # edx-lint snowballstemmer==2.2.0 # via pydocstyle sqlparse==0.5.0 diff --git a/requirements/test.txt b/requirements/test.txt index 4ba8c661..9b3eeee0 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -48,10 +48,14 @@ dnspython==2.6.1 # via # -r requirements/base.txt # pymongo +edx-ccx-keys==1.3.0 + # via -r requirements/base.txt edx-django-utils==5.13.0 # via -r requirements/base.txt edx-opaque-keys[django]==2.9.0 - # via -r requirements/base.txt + # via + # -r requirements/base.txt + # edx-ccx-keys exceptiongroup==1.2.1 # via pytest fastavro==1.9.4 @@ -102,6 +106,10 @@ python-slugify==8.0.4 # via code-annotations pyyaml==6.0.1 # via code-annotations +six==1.16.0 + # via + # -r requirements/base.txt + # edx-ccx-keys sqlparse==0.5.0 # via # -r requirements/base.txt