From fa2a4e918e6f74a6218812905e6538276b564d03 Mon Sep 17 00:00:00 2001 From: Troy Sankey Date: Wed, 31 Jul 2024 14:19:10 -0700 Subject: [PATCH 1/2] feat: emit learner credit unenrollment event event type: org.openedx.enterprise.learner_credit_course_enrollment.revoked.v1 event_name: LEARNER_CREDIT_COURSE_ENROLLMENT_REVOKED The motivation for emitting this event is to support event-driven creation of openedx_ledger.Reversal objects. ENT-9213 --- CHANGELOG.rst | 4 ++ enterprise/__init__.py | 2 +- .../views/enterprise_subsidy_fulfillment.py | 3 + enterprise/event_bus.py | 70 +++++++++++++++++++ enterprise/models.py | 10 +++ tests/test_enterprise/api/test_views.py | 13 ++-- tests/test_enterprise/test_event_bus.py | 48 +++++++++++++ 7 files changed, 145 insertions(+), 5 deletions(-) create mode 100644 enterprise/event_bus.py create mode 100644 tests/test_enterprise/test_event_bus.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 4a3663f42..3a0d19895 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -17,6 +17,10 @@ Unreleased ---------- * nothing unreleased +[4.25.0] +---------- +* feat: emit learner credit unenrollment event + [4.24.0] ---------- * fix: customer sorting error in customer support tool endpoint and added user query param diff --git a/enterprise/__init__.py b/enterprise/__init__.py index d77b40cea..23df90d8a 100644 --- a/enterprise/__init__.py +++ b/enterprise/__init__.py @@ -2,4 +2,4 @@ Your project description goes here. """ -__version__ = "4.24.0" +__version__ = "4.25.0" diff --git a/enterprise/api/v1/views/enterprise_subsidy_fulfillment.py b/enterprise/api/v1/views/enterprise_subsidy_fulfillment.py index 8ad00a828..7c5a412e0 100644 --- a/enterprise/api/v1/views/enterprise_subsidy_fulfillment.py +++ b/enterprise/api/v1/views/enterprise_subsidy_fulfillment.py @@ -234,6 +234,9 @@ def unenrolled(self, request, *args, **kwargs): retrieve_licensed_enrollments (bool): If true, return data related to licensed enrollments instead of learner credit """ + LOGGER.warning( + "[DEPRECATION] This view is deprecated for lack of purpose. Logging to confirm utilization drop-off.", + ) queryset = self._get_unenrolled_fulfillments() serializer_class = self.get_unenrolled_fulfillment_serializer_class() serializer = serializer_class(queryset, many=True) diff --git a/enterprise/event_bus.py b/enterprise/event_bus.py new file mode 100644 index 000000000..f618140d1 --- /dev/null +++ b/enterprise/event_bus.py @@ -0,0 +1,70 @@ +""" +Functions for serializing and emiting Open edX event bus signals. +""" +from openedx_events.enterprise.data import ( + EnterpriseCourseEnrollment, + EnterpriseCustomerUser, + LearnerCreditEnterpriseCourseEnrollment, +) +from openedx_events.enterprise.signals import LEARNER_CREDIT_COURSE_ENROLLMENT_REVOKED + + +def serialize_learner_credit_course_enrollment(learner_credit_course_enrollment): + """ + Serializes the ``LearnerCreditEnterpriseCourseEnrollment`` into a defined set of attributes + for use in the event-bus signal. + """ + enterprise_course_enrollment = learner_credit_course_enrollment.enterprise_course_enrollment + enterprise_customer_user = enterprise_course_enrollment.enterprise_customer_user + + enterprise_customer_user_data = EnterpriseCustomerUser( + id=enterprise_customer_user.id, + created=enterprise_customer_user.created, + modified=enterprise_customer_user.modified, + enterprise_customer_uuid=enterprise_customer_user.enterprise_customer.uuid, + user_id=enterprise_customer_user.user_id, + active=enterprise_customer_user.active, + linked=enterprise_customer_user.linked, + is_relinkable=enterprise_customer_user.is_relinkable, + invite_key=enterprise_customer_user.invite_key.uuid if enterprise_customer_user.invite_key else None, + should_inactivate_other_customers=enterprise_customer_user.should_inactivate_other_customers, + ) + enterprise_course_enrollment_data = EnterpriseCourseEnrollment( + id=enterprise_course_enrollment.id, + created=enterprise_course_enrollment.created, + modified=enterprise_course_enrollment.modified, + enterprise_customer_user=enterprise_customer_user_data, + course_id=enterprise_course_enrollment.course_id, + saved_for_later=enterprise_course_enrollment.saved_for_later, + source_slug=enterprise_course_enrollment.source.slug if enterprise_course_enrollment.source else None, + unenrolled=enterprise_course_enrollment.unenrolled, + unenrolled_at=enterprise_course_enrollment.unenrolled_at, + ) + data = LearnerCreditEnterpriseCourseEnrollment( + uuid=learner_credit_course_enrollment.uuid, + created=learner_credit_course_enrollment.created, + modified=learner_credit_course_enrollment.modified, + fulfillment_type=learner_credit_course_enrollment.fulfillment_type, + enterprise_course_entitlement_uuid=( + learner_credit_course_enrollment.enterprise_course_entitlement.uuid + if learner_credit_course_enrollment.enterprise_course_entitlement + else None + ), + enterprise_course_enrollment=enterprise_course_enrollment_data, + is_revoked=learner_credit_course_enrollment.is_revoked, + transaction_id=learner_credit_course_enrollment.transaction_id, + ) + return data + + +def send_learner_credit_course_enrollment_revoked_event(learner_credit_course_enrollment): + """ + Sends the LEARNER_CREDIT_COURSE_ENROLLMENT_REVOKED openedx event. + + Args: + learner_credit_course_enrollment (enterprise.models.LearnerCreditEnterpriseCourseEnrollment): + An enterprise learner credit fulfillment record that was revoked. + """ + LEARNER_CREDIT_COURSE_ENROLLMENT_REVOKED.send_event( + learner_credit_course_enrollment=serialize_learner_credit_course_enrollment(learner_credit_course_enrollment), + ) diff --git a/enterprise/models.py b/enterprise/models.py index f161048af..f5758a1a4 100644 --- a/enterprise/models.py +++ b/enterprise/models.py @@ -60,6 +60,7 @@ json_serialized_course_modes, ) from enterprise.errors import LinkUserToEnterpriseError +from enterprise.event_bus import send_learner_credit_course_enrollment_revoked_event from enterprise.logging import getEnterpriseLogger from enterprise.tasks import send_enterprise_email_notification from enterprise.utils import ( @@ -2287,6 +2288,8 @@ def revoke(self): Marks this object as revoked and marks the associated EnterpriseCourseEnrollment as "saved for later". This object and the associated EnterpriseCourseEnrollment are both saved. + Subclasses may override this function to additionally emit revocation events. + TODO: revoke entitlements as well? """ if self.enterprise_course_enrollment: @@ -2330,6 +2333,13 @@ class LearnerCreditEnterpriseCourseEnrollment(EnterpriseFulfillmentSource): .. no_pii: """ + def revoke(self): + """ + Revoke this LearnerCreditEnterpriseCourseEnrollment, and emit a revoked event. + """ + super().revoke() + send_learner_credit_course_enrollment_revoked_event(self) + def reactivate(self, transaction_id=None, **kwargs): """ Idmpotently reactivates this LearnerCreditEnterpriseCourseEnrollment. diff --git a/tests/test_enterprise/api/test_views.py b/tests/test_enterprise/api/test_views.py index 50e1918c3..6e8efe7e4 100644 --- a/tests/test_enterprise/api/test_views.py +++ b/tests/test_enterprise/api/test_views.py @@ -4401,9 +4401,9 @@ def test_unsupported_methods(self): assert update_response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED @mock.patch("enterprise.api.v1.views.enterprise_subsidy_fulfillment.enrollment_api") - def test_successful_cancel_fulfillment(self, mock_enrollment_api): + def test_successful_cancel_licensed_fulfillment(self, mock_enrollment_api): """ - Test that we can successfully cancel both licensed and learner credit fulfillments. + Test that we can successfully cancel licensed fulfillments. """ mock_enrollment_api.update_enrollment.return_value = mock.Mock() self.licensed_course_enrollment.is_revoked = False @@ -4424,8 +4424,12 @@ def test_successful_cancel_fulfillment(self, mock_enrollment_api): 'is_active': False, } - mock_enrollment_api.reset_mock() - + @mock.patch("enterprise.models.send_learner_credit_course_enrollment_revoked_event") + @mock.patch("enterprise.api.v1.views.enterprise_subsidy_fulfillment.enrollment_api") + def test_successful_cancel_learner_credit_fulfillment(self, mock_enrollment_api, mock_send_revoked_event): + """ + Test that we can successfully cancel learner credit fulfillments, and an openedx event is emitted. + """ self.learner_credit_course_enrollment.is_revoked = False self.learner_credit_course_enrollment.save() response = self.client.post( @@ -4443,6 +4447,7 @@ def test_successful_cancel_fulfillment(self, mock_enrollment_api): assert mock_enrollment_api.update_enrollment.call_args.kwargs == { 'is_active': False, } + mock_send_revoked_event.assert_called_once_with(self.learner_credit_course_enrollment) @mock.patch("enterprise.api.v1.views.enterprise_subsidy_fulfillment.enrollment_api") def test_idempotent_cancel_fulfillment(self, mock_enrollment_api): diff --git a/tests/test_enterprise/test_event_bus.py b/tests/test_enterprise/test_event_bus.py new file mode 100644 index 000000000..a2a72d57b --- /dev/null +++ b/tests/test_enterprise/test_event_bus.py @@ -0,0 +1,48 @@ +""" +Tests for enterprise/event_bus.py +""" +import unittest + +import ddt +from pytest import mark + +from enterprise.event_bus import serialize_learner_credit_course_enrollment +from enterprise.models import EnterpriseEnrollmentSource +from test_utils import factories + + +@ddt.ddt +@mark.django_db +class TestEventBusSerializers(unittest.TestCase): + """ + Test serializers for use with openedx-events events ("event bus"). + """ + + def setUp(self): + super().setUp() + + self.user = factories.UserFactory(is_active=True) + self.enterprise_customer = factories.EnterpriseCustomerFactory() + self.enterprise_user = factories.EnterpriseCustomerUserFactory( + user_id=self.user.id, + enterprise_customer=self.enterprise_customer, + ) + self.enterprise_course_enrollment = factories.EnterpriseCourseEnrollmentFactory( + enterprise_customer_user=self.enterprise_user, + source=EnterpriseEnrollmentSource.get_source(EnterpriseEnrollmentSource.API), + ) + self.learner_credit_course_enrollment = factories.LearnerCreditEnterpriseCourseEnrollmentFactory( + enterprise_course_enrollment=self.enterprise_course_enrollment, + ) + + def test_serialize_learner_credit_course_enrollment(self): + """ + Perform a basic test that the serializer drills down two levels into the enterprise user correctly. + """ + data = serialize_learner_credit_course_enrollment(self.learner_credit_course_enrollment) + assert data.uuid == self.learner_credit_course_enrollment.uuid + assert data.enterprise_course_enrollment.id == self.enterprise_course_enrollment.id + assert data.enterprise_course_enrollment.source_slug == self.enterprise_course_enrollment.source.slug + assert data.enterprise_course_enrollment.enterprise_customer_user.id == self.enterprise_user.id + assert data.enterprise_course_enrollment.enterprise_customer_user.enterprise_customer_uuid == \ + self.enterprise_customer.uuid From a61ee2d07beabb2c94378e97ad04b1ce0cca2e7c Mon Sep 17 00:00:00 2001 From: Troy Sankey Date: Wed, 4 Sep 2024 18:10:49 -0700 Subject: [PATCH 2/2] chore: make upgrade to get new openedx-events --- requirements/base.in | 1 + requirements/common_constraints.txt | 3 +++ requirements/dev.txt | 33 +++++++++++++++++++---- requirements/doc.txt | 24 ++++++++++++++--- requirements/edx-platform-constraints.txt | 6 ++--- requirements/js_test.txt | 4 +-- requirements/test-master.txt | 18 +++++++++++++ requirements/test.txt | 18 ++++++++++++- 8 files changed, 92 insertions(+), 15 deletions(-) diff --git a/requirements/base.in b/requirements/base.in index d0b454396..7a439b12c 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -31,6 +31,7 @@ edx-tincan-py35 edx-toggles jsondiff jsonfield +openedx-events paramiko path.py pillow diff --git a/requirements/common_constraints.txt b/requirements/common_constraints.txt index 8b10b353a..bca5dac0b 100644 --- a/requirements/common_constraints.txt +++ b/requirements/common_constraints.txt @@ -1,4 +1,7 @@ + + + # A central location for most common version constraints # (across edx repos) for pip-installation. # diff --git a/requirements/dev.txt b/requirements/dev.txt index ea963c55e..33d855b98 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -65,6 +65,7 @@ attrs==24.2.0 # -r requirements/test-master.txt # -r requirements/test.txt # aiohttp + # openedx-events # pytest babel==2.16.0 # via @@ -221,6 +222,7 @@ django==4.2.15 # edx-rbac # edx-toggles # jsonfield + # openedx-events django-cache-memoize==0.2.0 # via # -r requirements/doc.txt @@ -315,9 +317,9 @@ dnspython==2.6.1 # -r requirements/test-master.txt # -r requirements/test.txt # pymongo -doc8==1.1.1 +doc8==1.1.2 # via -r requirements/doc.txt -docutils==0.20.1 +docutils==0.21.2 # via # -r requirements/doc.txt # doc8 @@ -347,6 +349,12 @@ edx-braze-client==0.2.5 # -r requirements/doc.txt # -r requirements/test-master.txt # -r requirements/test.txt +edx-ccx-keys==1.3.0 + # via + # -r requirements/doc.txt + # -r requirements/test-master.txt + # -r requirements/test.txt + # openedx-events edx-django-utils==5.15.0 # via # -r requirements/doc.txt @@ -356,6 +364,7 @@ edx-django-utils==5.15.0 # edx-drf-extensions # edx-rest-api-client # edx-toggles + # openedx-events edx-drf-extensions==10.3.0 # via # -r requirements/doc.txt @@ -371,7 +380,9 @@ edx-opaque-keys[django]==2.10.0 # -r requirements/doc.txt # -r requirements/test-master.txt # -r requirements/test.txt + # edx-ccx-keys # edx-drf-extensions + # openedx-events edx-rbac==1.9.0 # via # -r requirements/doc.txt @@ -397,11 +408,17 @@ factory-boy==3.3.1 # -c requirements/constraints.txt # -r requirements/doc.txt # -r requirements/test.txt -faker==28.0.0 +faker==28.4.1 # via # -r requirements/doc.txt # -r requirements/test.txt # factory-boy +fastavro==1.9.5 + # via + # -r requirements/doc.txt + # -r requirements/test-master.txt + # -r requirements/test.txt + # openedx-events filelock==3.15.4 # via # -r requirements/doc.txt @@ -520,6 +537,11 @@ openai==0.28.1 # -r requirements/doc.txt # -r requirements/test-master.txt # -r requirements/test.txt +openedx-events==9.12.0 + # via + # -r requirements/doc.txt + # -r requirements/test-master.txt + # -r requirements/test.txt packaging==24.1 # via # -r requirements/doc.txt @@ -643,7 +665,7 @@ pyjwt[crypto]==2.9.0 # edx-drf-extensions # edx-rest-api-client # snowflake-connector-python -pylint==3.2.6 +pylint==3.2.7 # via # edx-lint # pylint-celery @@ -729,7 +751,7 @@ pyyaml==6.0.2 # drf-yasg # edx-i18n-tools # jsondiff -readme-renderer==43.0 +readme-renderer==44.0 # via -r requirements/doc.txt requests==2.32.3 # via @@ -773,6 +795,7 @@ six==1.16.0 # -r requirements/test-master.txt # -r requirements/test.txt # bleach + # edx-ccx-keys # edx-lint # edx-rbac # freezegun diff --git a/requirements/doc.txt b/requirements/doc.txt index 9ecda5a33..0fe56cf47 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -41,6 +41,7 @@ attrs==24.2.0 # via # -r requirements/test-master.txt # aiohttp + # openedx-events # pytest babel==2.16.0 # via @@ -138,6 +139,7 @@ django==4.2.15 # edx-rbac # edx-toggles # jsonfield + # openedx-events django-cache-memoize==0.2.0 # via -r requirements/test-master.txt django-config-models==2.7.0 @@ -190,9 +192,9 @@ dnspython==2.6.1 # via # -r requirements/test-master.txt # pymongo -doc8==1.1.1 +doc8==1.1.2 # via -r requirements/doc.in -docutils==0.20.1 +docutils==0.21.2 # via # -r requirements/doc.in # doc8 @@ -212,6 +214,10 @@ edx-api-doc-tools==1.8.0 # via -r requirements/test-master.txt edx-braze-client==0.2.5 # via -r requirements/test-master.txt +edx-ccx-keys==1.3.0 + # via + # -r requirements/test-master.txt + # openedx-events edx-django-utils==5.15.0 # via # -r requirements/test-master.txt @@ -219,6 +225,7 @@ edx-django-utils==5.15.0 # edx-drf-extensions # edx-rest-api-client # edx-toggles + # openedx-events edx-drf-extensions==10.3.0 # via # -r requirements/test-master.txt @@ -226,7 +233,9 @@ edx-drf-extensions==10.3.0 edx-opaque-keys[django]==2.10.0 # via # -r requirements/test-master.txt + # edx-ccx-keys # edx-drf-extensions + # openedx-events edx-rbac==1.9.0 # via -r requirements/test-master.txt edx-rest-api-client==5.7.1 @@ -239,8 +248,12 @@ factory-boy==3.3.1 # via # -c requirements/constraints.txt # -r requirements/doc.in -faker==28.0.0 +faker==28.4.1 # via factory-boy +fastavro==1.9.5 + # via + # -r requirements/test-master.txt + # openedx-events filelock==3.15.4 # via # -r requirements/test-master.txt @@ -302,6 +315,8 @@ oauthlib==3.2.2 # django-oauth-toolkit openai==0.28.1 # via -r requirements/test-master.txt +openedx-events==9.12.0 + # via -r requirements/test-master.txt packaging==24.1 # via # -r requirements/test-master.txt @@ -409,7 +424,7 @@ pyyaml==6.0.2 # code-annotations # drf-yasg # jsondiff -readme-renderer==43.0 +readme-renderer==44.0 # via -r requirements/doc.in requests==2.32.3 # via @@ -433,6 +448,7 @@ six==1.16.0 # via # -r requirements/test-master.txt # bleach + # edx-ccx-keys # edx-rbac # python-dateutil slumber==0.7.1 diff --git a/requirements/edx-platform-constraints.txt b/requirements/edx-platform-constraints.txt index 944589d21..4294872c7 100644 --- a/requirements/edx-platform-constraints.txt +++ b/requirements/edx-platform-constraints.txt @@ -455,7 +455,7 @@ edx-drf-extensions==10.3.0 # edx-when # edxval # openedx-learning -edx-enterprise==4.23.9 +edx-enterprise==4.24.0 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/kernel.in @@ -795,7 +795,7 @@ openedx-django-require==2.1.0 # via -r requirements/edx/kernel.in openedx-django-wiki==2.1.0 # via -r requirements/edx/kernel.in -openedx-events==9.11.0 +openedx-events==9.12.0 # via # -r requirements/edx/kernel.in # edx-event-bus-kafka @@ -807,7 +807,7 @@ openedx-filters==1.9.0 # -r requirements/edx/kernel.in # lti-consumer-xblock # ora2 -openedx-learning==0.11.1 +openedx-learning==0.11.2 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/kernel.in diff --git a/requirements/js_test.txt b/requirements/js_test.txt index 8414c91ba..ebc3cf909 100644 --- a/requirements/js_test.txt +++ b/requirements/js_test.txt @@ -12,7 +12,7 @@ autocommand==2.2.2 # via jaraco-text backports-tarfile==1.2.0 # via jaraco-context -certifi==2024.7.4 +certifi==2024.8.30 # via selenium cheroot==10.0.1 # via cherrypy @@ -67,7 +67,7 @@ python-dateutil==2.9.0.post0 # via tempora pyyaml==6.0.2 # via jasmine -selenium==4.23.1 +selenium==4.24.0 # via jasmine six==1.16.0 # via python-dateutil diff --git a/requirements/test-master.txt b/requirements/test-master.txt index d5da0754f..28370133f 100644 --- a/requirements/test-master.txt +++ b/requirements/test-master.txt @@ -35,6 +35,7 @@ attrs==24.2.0 # via # -c requirements/edx-platform-constraints.txt # aiohttp + # openedx-events bcrypt==4.2.0 # via # -c requirements/edx-platform-constraints.txt @@ -123,6 +124,7 @@ django==4.2.15 # edx-rbac # edx-toggles # jsonfield + # openedx-events django-cache-memoize==0.2.0 # via # -c requirements/edx-platform-constraints.txt @@ -215,6 +217,10 @@ edx-braze-client==0.2.5 # via # -c requirements/edx-platform-constraints.txt # -r requirements/base.in +edx-ccx-keys==1.3.0 + # via + # -c requirements/edx-platform-constraints.txt + # openedx-events edx-django-utils==5.15.0 # via # -c requirements/edx-platform-constraints.txt @@ -223,6 +229,7 @@ edx-django-utils==5.15.0 # edx-drf-extensions # edx-rest-api-client # edx-toggles + # openedx-events edx-drf-extensions==10.3.0 # via # -c requirements/edx-platform-constraints.txt @@ -232,7 +239,9 @@ edx-opaque-keys[django]==2.10.0 # via # -c requirements/edx-platform-constraints.txt # -r requirements/base.in + # edx-ccx-keys # edx-drf-extensions + # openedx-events edx-rbac==1.9.0 # via # -c requirements/edx-platform-constraints.txt @@ -249,6 +258,10 @@ edx-toggles==5.2.0 # via # -c requirements/edx-platform-constraints.txt # -r requirements/base.in +fastavro==1.9.5 + # via + # -c requirements/edx-platform-constraints.txt + # openedx-events filelock==3.15.4 # via # -c requirements/edx-platform-constraints.txt @@ -307,6 +320,10 @@ openai==0.28.1 # via # -c requirements/edx-platform-constraints.txt # -r requirements/base.in +openedx-events==9.12.0 + # via + # -c requirements/edx-platform-constraints.txt + # -r requirements/base.in packaging==24.1 # via # -c requirements/edx-platform-constraints.txt @@ -423,6 +440,7 @@ six==1.16.0 # via # -c requirements/edx-platform-constraints.txt # bleach + # edx-ccx-keys # edx-rbac # python-dateutil slumber==0.7.1 diff --git a/requirements/test.txt b/requirements/test.txt index c5525e74d..6d0d66852 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -36,6 +36,7 @@ attrs==24.2.0 # via # -r requirements/test-master.txt # aiohttp + # openedx-events # pytest bcrypt==4.2.0 # via @@ -129,6 +130,7 @@ diff-cover==9.1.1 # edx-rbac # edx-toggles # jsonfield + # openedx-events django-cache-memoize==0.2.0 # via -r requirements/test-master.txt django-config-models==2.7.0 @@ -194,6 +196,10 @@ edx-api-doc-tools==1.8.0 # via -r requirements/test-master.txt edx-braze-client==0.2.5 # via -r requirements/test-master.txt +edx-ccx-keys==1.3.0 + # via + # -r requirements/test-master.txt + # openedx-events edx-django-utils==5.15.0 # via # -r requirements/test-master.txt @@ -201,6 +207,7 @@ edx-django-utils==5.15.0 # edx-drf-extensions # edx-rest-api-client # edx-toggles + # openedx-events edx-drf-extensions==10.3.0 # via # -r requirements/test-master.txt @@ -208,7 +215,9 @@ edx-drf-extensions==10.3.0 edx-opaque-keys[django]==2.10.0 # via # -r requirements/test-master.txt + # edx-ccx-keys # edx-drf-extensions + # openedx-events edx-rbac==1.9.0 # via -r requirements/test-master.txt edx-rest-api-client==5.7.1 @@ -221,8 +230,12 @@ factory-boy==3.3.1 # via # -c requirements/constraints.txt # -r requirements/test.in -faker==28.0.0 +faker==28.4.1 # via factory-boy +fastavro==1.9.5 + # via + # -r requirements/test-master.txt + # openedx-events filelock==3.15.4 # via # -r requirements/test-master.txt @@ -287,6 +300,8 @@ oauthlib==3.2.2 # django-oauth-toolkit openai==0.28.1 # via -r requirements/test-master.txt +openedx-events==9.12.0 + # via -r requirements/test-master.txt packaging==24.1 # via # -r requirements/test-master.txt @@ -416,6 +431,7 @@ six==1.16.0 # via # -r requirements/test-master.txt # bleach + # edx-ccx-keys # edx-rbac # freezegun # mock