From 9d5e7049e15b1b3346519c6b85f3399dafdca812 Mon Sep 17 00:00:00 2001 From: Cristhian Garcia Date: Mon, 30 Oct 2023 16:55:08 -0500 Subject: [PATCH] feat: refactor EventsRouter bettween sync and async router --- .../backends/async_events_router.py | 49 ++ .../backends/events_router.py | 46 +- .../backends/sync_events_router.py | 51 ++ .../backends/tests/test_events_router.py | 549 ++++++++++++++---- event_routing_backends/helpers.py | 2 +- .../commands/transform_tracking_logs.py | 2 +- event_routing_backends/settings/common.py | 194 ++++--- event_routing_backends/tasks.py | 28 +- requirements/base.txt | 64 +- requirements/ci.txt | 8 +- requirements/dev.txt | 110 ++-- requirements/doc.txt | 102 ++-- requirements/pip-tools.txt | 8 +- requirements/pip.txt | 4 +- requirements/quality.txt | 93 +-- requirements/test.txt | 84 ++- 16 files changed, 1003 insertions(+), 391 deletions(-) create mode 100644 event_routing_backends/backends/async_events_router.py create mode 100644 event_routing_backends/backends/sync_events_router.py diff --git a/event_routing_backends/backends/async_events_router.py b/event_routing_backends/backends/async_events_router.py new file mode 100644 index 00000000..c25e938d --- /dev/null +++ b/event_routing_backends/backends/async_events_router.py @@ -0,0 +1,49 @@ +""" +Events router to send events to hosts via celery. + +This events router will trigger a celery task to send the events to the +configured hosts. +""" +from event_routing_backends.backends.events_router import EventsRouter +from event_routing_backends.tasks import dispatch_bulk_events, dispatch_event, dispatch_event_persistent + + +class AsyncEventsRouter(EventsRouter): + """ + Router to send events to hosts via celery using requests library. + """ + + def dispatch_event(self, event_name, updated_event, router_type, host_configurations): + """ + Dispatch the event to the configured router. + + Arguments: + event_name (str): name of the original event. + updated_event (dict): processed event dictionary + router_type (str): type of the router + host_configurations (dict): host configurations dict + """ + dispatch_event.delay(event_name, updated_event, router_type, host_configurations) + + def dispatch_bulk_events(self, events, router_type, host_configurations): + """ + Dispatch the a list of events to the configured router in bulk. + + Arguments: + events (list[dict]): list of processed event dictionaries + router_type (str): type of the router + host_configurations (dict): host configurations dict + """ + dispatch_bulk_events.delay(events, router_type, host_configurations) + + def dispatch_event_persistent(self, event_name, updated_event, router_type, host_configurations): + """ + Dispatch the event to the configured router providing persistent storage. + + Arguments: + event_name (str): name of the original event. + updated_event (dict): processed event dictionary + router_type (str): type of the router + host_configurations (dict): host configurations dict + """ + dispatch_event_persistent.delay(event_name, updated_event, router_type, host_configurations) diff --git a/event_routing_backends/backends/events_router.py b/event_routing_backends/backends/events_router.py index 8a8b774f..85832f4d 100644 --- a/event_routing_backends/backends/events_router.py +++ b/event_routing_backends/backends/events_router.py @@ -7,7 +7,6 @@ from event_routing_backends.helpers import get_business_critical_events from event_routing_backends.models import RouterConfiguration -from event_routing_backends.tasks import dispatch_bulk_events, dispatch_event, dispatch_event_persistent logger = logging.getLogger(__name__) @@ -138,7 +137,7 @@ def bulk_send(self, events): prepared_events.append(updated_event) if prepared_events: # pragma: no cover - dispatch_bulk_events.delay( + self.dispatch_bulk_events( prepared_events, host['router_type'], host['host_configurations'] @@ -160,18 +159,18 @@ def send(self, event): for events_for_route in event_routes.values(): for event_name, updated_event, host, is_business_critical in events_for_route: if is_business_critical: - dispatch_event_persistent.delay( + self.dispatch_event_persistent( event_name, updated_event, host['router_type'], - host['host_configurations'] + host['host_configurations'], ) else: - dispatch_event.delay( + self.dispatch_event( event_name, updated_event, host['router_type'], - host['host_configurations'] + host['host_configurations'], ) def process_event(self, event): @@ -215,3 +214,38 @@ def overwrite_event_data(self, event, host, event_name): host['override_args'] )) return event + + def dispatch_event(self, event_name, updated_event, router_type, host_configurations): + """ + Dispatch the event to the configured router. + + Arguments: + event_name (str): name of the original event. + updated_event (dict): processed event dictionary + router_type (str): type of the router + host_configurations (dict): host configurations dict + """ + raise NotImplementedError('dispatch_event is not implemented') + + def dispatch_bulk_events(self, events, router_type, host_configurations): + """ + Dispatch the a list of events to the configured router in bulk. + + Arguments: + events (list[dict]): list of processed event dictionaries + router_type (str): type of the router + host_configurations (dict): host configurations dict + """ + raise NotImplementedError('dispatch_bulk_events is not implemented') + + def dispatch_event_persistent(self, event_name, updated_event, router_type, host_configurations): + """ + Dispatch the event to the configured router providing persistent storage. + + Arguments: + event_name (str): name of the original event. + updated_event (dict): processed event dictionary + router_type (str): type of the router + host_configurations (dict): host configurations dict + """ + raise NotImplementedError('dispatch_event_persistent is not implemented') diff --git a/event_routing_backends/backends/sync_events_router.py b/event_routing_backends/backends/sync_events_router.py new file mode 100644 index 00000000..8fe7f68d --- /dev/null +++ b/event_routing_backends/backends/sync_events_router.py @@ -0,0 +1,51 @@ +""" +Events router to send events to hosts in sync mode. + +This router is expected to be used with the event bus, which +can be configured to use this router to send events to hosts +in the same thread as it process the events. +""" +from event_routing_backends.backends.events_router import EventsRouter +from event_routing_backends.tasks import bulk_send_events, send_event + + +class SyncEventsRouter(EventsRouter): + """ + Router to send events to hosts using requests library. + """ + + def dispatch_event(self, event_name, updated_event, router_type, host_configurations): + """ + Dispatch the event to the configured router. + + Arguments: + event_name (str): name of the original event. + updated_event (dict): processed event dictionary + router_type (str): type of the router + host_configurations (dict): host configurations dict + """ + send_event(None, event_name, updated_event, router_type, host_configurations) + + def dispatch_bulk_events(self, events, router_type, host_configurations): + """ + Dispatch the a list of events to the configured router in bulk. + + Arguments: + events (list[dict]): list of processed event dictionaries + router_type (str): type of the router + host_configurations (dict): host configurations dict + """ + bulk_send_events(None, events, router_type, host_configurations) + + def dispatch_event_persistent(self, event_name, updated_event, router_type, host_configurations): + """ + Dispatch the event to the configured router providing persistent storage. + In this case, the event bus is expected to provide the persistent storage layer. + + Arguments: + event_name (str): name of the original event. + updated_event (dict): processed event dictionary + router_type (str): type of the router + host_configurations (dict): host configurations dict + """ + self.dispatch_event(event_name, updated_event, router_type, host_configurations) diff --git a/event_routing_backends/backends/tests/test_events_router.py b/event_routing_backends/backends/tests/test_events_router.py index a8bc1ccc..45cc35ff 100644 --- a/event_routing_backends/backends/tests/test_events_router.py +++ b/event_routing_backends/backends/tests/test_events_router.py @@ -10,7 +10,9 @@ from eventtracking.processors.exceptions import EventEmissionExit from tincan.statement import Statement +from event_routing_backends.backends.async_events_router import AsyncEventsRouter from event_routing_backends.backends.events_router import EventsRouter +from event_routing_backends.backends.sync_events_router import SyncEventsRouter from event_routing_backends.helpers import get_business_critical_events from event_routing_backends.models import RouterConfiguration from event_routing_backends.processors.transformer_utils.exceptions import EventNotDispatched @@ -146,8 +148,6 @@ def setUp(self): } ] - self.router = EventsRouter(processors=[], backend_name='test') - @patch('event_routing_backends.utils.http_client.requests.post') @patch('event_routing_backends.backends.events_router.logger') @patch('event_routing_backends.models.RouterConfiguration.get_enabled_routers') @@ -177,31 +177,6 @@ def test_with_processor_exception(self, mocked_get_enabled_routers, mocked_logge exc_info=True ), mocked_logger.error.mock_calls) - @patch.dict('event_routing_backends.tasks.ROUTER_STRATEGY_MAPPING', { - 'AUTH_HEADERS': MagicMock(side_effect=EventNotDispatched) - }) - @patch('event_routing_backends.utils.http_client.requests.post') - @patch('event_routing_backends.tasks.logger') - def test_generic_exception_business_critical_event(self, mocked_logger, mocked_post): - RouterConfigurationFactory.create( - backend_name=RouterConfiguration.XAPI_BACKEND, - enabled=True, - route_url='http://test3.com', - auth_scheme=RouterConfiguration.AUTH_BEARER, - auth_key='test_key', - configurations=ROUTER_CONFIG_FIXTURE[0] - ) - - router = EventsRouter(processors=[], backend_name=RouterConfiguration.CALIPER_BACKEND) - event_data = self.transformed_event.copy() - business_critical_events = get_business_critical_events() - event_data['name'] = business_critical_events[0] - router.send(event_data) - - self.assertEqual(mocked_logger.exception.call_count, - getattr(settings, 'EVENT_ROUTING_BACKEND_COUNTDOWN', 3) + 1) - mocked_post.assert_not_called() - @patch('event_routing_backends.utils.http_client.requests.post') @patch('event_routing_backends.backends.events_router.logger') def test_with_no_router_configurations_available(self, mocked_logger, mocked_post): @@ -216,27 +191,81 @@ def test_with_no_router_configurations_available(self, mocked_logger, mocked_pos ) @patch('event_routing_backends.utils.http_client.requests.post') - @patch('event_routing_backends.tasks.logger') - def test_with_unsupported_routing_strategy(self, mocked_logger, mocked_post): - RouterConfigurationFactory.create( + @patch('event_routing_backends.backends.events_router.logger') + def test_with_no_available_hosts(self, mocked_logger, mocked_post): + router_config = RouterConfigurationFactory.create( backend_name='test_backend', enabled=True, route_url='http://test3.com', - auth_scheme=RouterConfiguration.AUTH_BEARER, - auth_key='test_key', - configurations=ROUTER_CONFIG_FIXTURE[0] + configurations=ROUTER_CONFIG_FIXTURE[1] ) router = EventsRouter(processors=[], backend_name='test_backend') TieredCache.dangerous_clear_all_tiers() router.send(self.transformed_event) - mocked_logger.error.assert_called_once_with('Unsupported routing strategy detected: INVALID_TYPE') mocked_post.assert_not_called() + self.assertIn( + call( + 'Event %s is not allowed to be sent to any host for router ID %s with backend "%s"', + self.transformed_event['name'], router_config.pk, 'test_backend' + ), + mocked_logger.info.mock_calls + ) + + def test_with_non_dict_event(self): + RouterConfigurationFactory.create( + backend_name=RouterConfiguration.XAPI_BACKEND, + enabled=True, + route_url='http://test3.com', + configurations=ROUTER_CONFIG_FIXTURE[3] + ) + router = EventsRouter(processors=[], backend_name=RouterConfiguration.XAPI_BACKEND) + transformed_event = Statement() + with self.assertRaises(ValueError): + router.send(transformed_event) + + def test_unsuccessful_routing_of_event(self): + host_configurations = { + 'url': 'http://test3.com', + 'version': '1.0.1', + 'auth_scheme': 'bearer', + 'auth_key': 'key', + } + client = LrsClient(**host_configurations) + with self.assertRaises(EventNotDispatched): + client.send(event_name='test', statement_data={}) + + @patch('event_routing_backends.utils.xapi_lrs_client.logger') + def test_duplicate_xapi_event_id(self, mocked_logger): + """ + Test that when we receive a 409 response when inserting an XAPI statement + we do not raise an exception, but do log it. + """ + mock_duplicate_return = MagicMock() + mock_duplicate_return.success = False + mock_duplicate_return.response.status = 409 + + client = LrsClient({}) + client.lrs_client = MagicMock() + client.lrs_client.save_statement.return_value = mock_duplicate_return + + client.send(event_name='test', statement_data={}) + self.assertIn( + call('Event test received a 409 error indicating the event id already exists.'), + mocked_logger.info.mock_calls + ) + + +@ddt.ddt +class TestAsyncEventsRouter(TestEventsRouter): # pylint: disable=test-inherits-tests + """ + Test the AsyncEventsRouter + """ @patch('event_routing_backends.utils.http_client.requests.post') @patch('event_routing_backends.tasks.logger') - def test_bulk_with_unsupported_routing_strategy(self, mocked_logger, mocked_post): + def test_with_unsupported_routing_strategy(self, mocked_logger, mocked_post): RouterConfigurationFactory.create( backend_name='test_backend', enabled=True, @@ -246,37 +275,32 @@ def test_bulk_with_unsupported_routing_strategy(self, mocked_logger, mocked_post configurations=ROUTER_CONFIG_FIXTURE[0] ) - router = EventsRouter(processors=[], backend_name='test_backend') + router = AsyncEventsRouter(processors=[], backend_name='test_backend') TieredCache.dangerous_clear_all_tiers() - router.bulk_send([self.transformed_event]) + router.send(self.transformed_event) mocked_logger.error.assert_called_once_with('Unsupported routing strategy detected: INVALID_TYPE') mocked_post.assert_not_called() @patch('event_routing_backends.utils.http_client.requests.post') - @patch('event_routing_backends.backends.events_router.logger') - def test_with_no_available_hosts(self, mocked_logger, mocked_post): - router_config = RouterConfigurationFactory.create( + @patch('event_routing_backends.tasks.logger') + def test_bulk_with_unsupported_routing_strategy(self, mocked_logger, mocked_post): + RouterConfigurationFactory.create( backend_name='test_backend', enabled=True, route_url='http://test3.com', - configurations=ROUTER_CONFIG_FIXTURE[1] + auth_scheme=RouterConfiguration.AUTH_BEARER, + auth_key='test_key', + configurations=ROUTER_CONFIG_FIXTURE[0] ) - router = EventsRouter(processors=[], backend_name='test_backend') + router = AsyncEventsRouter(processors=[], backend_name='test_backend') TieredCache.dangerous_clear_all_tiers() - router.send(self.transformed_event) + router.bulk_send([self.transformed_event]) + mocked_logger.error.assert_called_once_with('Unsupported routing strategy detected: INVALID_TYPE') mocked_post.assert_not_called() - self.assertIn( - call( - 'Event %s is not allowed to be sent to any host for router ID %s with backend "%s"', - self.transformed_event['name'], router_config.pk, 'test_backend' - ), - mocked_logger.info.mock_calls - ) - @ddt.data( ( RouterConfiguration.XAPI_BACKEND, @@ -299,7 +323,7 @@ def test_generic_exception(self, backend_name, mocked_logger, mocked_post): configurations=ROUTER_CONFIG_FIXTURE[2] ) - router = EventsRouter(processors=[], backend_name=backend_name) + router = AsyncEventsRouter(processors=[], backend_name=backend_name) router.send(self.transformed_event) if backend_name == RouterConfiguration.CALIPER_BACKEND: self.assertEqual(mocked_logger.exception.call_count, @@ -325,7 +349,7 @@ def test_failed_bulk_post(self, mocked_logger, mocked_post): configurations=ROUTER_CONFIG_FIXTURE[2] ) - router = EventsRouter(processors=[], backend_name=RouterConfiguration.CALIPER_BACKEND) + router = AsyncEventsRouter(processors=[], backend_name=RouterConfiguration.CALIPER_BACKEND) router.bulk_send([self.transformed_event]) self.assertEqual(mocked_logger.exception.call_count, @@ -350,7 +374,7 @@ def test_failed_post(self, mocked_logger, mocked_post): configurations=ROUTER_CONFIG_FIXTURE[2] ) - router = EventsRouter(processors=[], backend_name=RouterConfiguration.CALIPER_BACKEND) + router = AsyncEventsRouter(processors=[], backend_name=RouterConfiguration.CALIPER_BACKEND) router.send(self.transformed_event) self.assertEqual(mocked_logger.exception.call_count, @@ -377,7 +401,7 @@ def test_failed_bulk_routing(self, mocked_logger, mocked_remote_lrs): configurations=ROUTER_CONFIG_FIXTURE[2] ) - router = EventsRouter(processors=[], backend_name=RouterConfiguration.XAPI_BACKEND) + router = AsyncEventsRouter(processors=[], backend_name=RouterConfiguration.XAPI_BACKEND) router.bulk_send([self.transformed_event]) self.assertEqual(mocked_logger.exception.call_count, @@ -404,7 +428,7 @@ def test_failed_routing(self, mocked_logger, mocked_remote_lrs): configurations=ROUTER_CONFIG_FIXTURE[2] ) - router = EventsRouter(processors=[], backend_name=RouterConfiguration.XAPI_BACKEND) + router = AsyncEventsRouter(processors=[], backend_name=RouterConfiguration.XAPI_BACKEND) router.send(self.transformed_event) self.assertEqual(mocked_logger.exception.call_count, @@ -431,7 +455,7 @@ def test_duplicate_ids_in_bulk(self, mocked_logger, mocked_remote_lrs): configurations=ROUTER_CONFIG_FIXTURE[2] ) - router = EventsRouter(processors=[], backend_name=RouterConfiguration.XAPI_BACKEND) + router = AsyncEventsRouter(processors=[], backend_name=RouterConfiguration.XAPI_BACKEND) router.bulk_send([self.transformed_event]) self.assertEqual(mocked_logger.exception.call_count, 0) @@ -459,7 +483,7 @@ def test_bulk_generic_exception(self, backend_name, mocked_logger, mocked_post): configurations=ROUTER_CONFIG_FIXTURE[2] ) - router = EventsRouter(processors=[], backend_name=backend_name) + router = AsyncEventsRouter(processors=[], backend_name=backend_name) router.bulk_send([self.transformed_event]) if backend_name == RouterConfiguration.CALIPER_BACKEND: self.assertEqual(mocked_logger.exception.call_count, @@ -468,17 +492,30 @@ def test_bulk_generic_exception(self, backend_name, mocked_logger, mocked_post): else: mocked_logger.exception.assert_not_called() - def test_with_non_dict_event(self): + @patch.dict('event_routing_backends.tasks.ROUTER_STRATEGY_MAPPING', { + 'AUTH_HEADERS': MagicMock(side_effect=EventNotDispatched) + }) + @patch('event_routing_backends.utils.http_client.requests.post') + @patch('event_routing_backends.tasks.logger') + def test_generic_exception_business_critical_event(self, mocked_logger, mocked_post): RouterConfigurationFactory.create( backend_name=RouterConfiguration.XAPI_BACKEND, enabled=True, route_url='http://test3.com', - configurations=ROUTER_CONFIG_FIXTURE[3] + auth_scheme=RouterConfiguration.AUTH_BEARER, + auth_key='test_key', + configurations=ROUTER_CONFIG_FIXTURE[0] ) - router = EventsRouter(processors=[], backend_name=RouterConfiguration.XAPI_BACKEND) - transformed_event = Statement() - with self.assertRaises(ValueError): - router.send(transformed_event) + + router = AsyncEventsRouter(processors=[], backend_name=RouterConfiguration.CALIPER_BACKEND) + event_data = self.transformed_event.copy() + business_critical_events = get_business_critical_events() + event_data['name'] = business_critical_events[0] + router.send(event_data) + + self.assertEqual(mocked_logger.exception.call_count, + getattr(settings, 'EVENT_ROUTING_BACKEND_COUNTDOWN', 3) + 1) + mocked_post.assert_not_called() @ddt.data( (RouterConfiguration.AUTH_BASIC, @@ -563,7 +600,7 @@ def test_successful_routing_of_event( configurations=ROUTER_CONFIG_FIXTURE[0] ) - router = EventsRouter(processors=[], backend_name=backend_name) + router = AsyncEventsRouter(processors=[], backend_name=backend_name) with patch.dict('event_routing_backends.tasks.ROUTER_STRATEGY_MAPPING', MOCKED_MAP): router.send(self.transformed_event) @@ -611,37 +648,6 @@ def test_successful_routing_of_event( # test mocked oauth client mocked_oauth_client.assert_not_called() - def test_unsuccessful_routing_of_event(self): - host_configurations = { - 'url': 'http://test3.com', - 'version': '1.0.1', - 'auth_scheme': 'bearer', - 'auth_key': 'key', - } - client = LrsClient(**host_configurations) - with self.assertRaises(EventNotDispatched): - client.send(event_name='test', statement_data={}) - - @patch('event_routing_backends.utils.xapi_lrs_client.logger') - def test_duplicate_xapi_event_id(self, mocked_logger): - """ - Test that when we receive a 409 response when inserting an XAPI statement - we do not raise an exception, but do log it. - """ - mock_duplicate_return = MagicMock() - mock_duplicate_return.success = False - mock_duplicate_return.response.status = 409 - - client = LrsClient({}) - client.lrs_client = MagicMock() - client.lrs_client.save_statement.return_value = mock_duplicate_return - - client.send(event_name='test', statement_data={}) - self.assertIn( - call('Event test received a 409 error indicating the event id already exists.'), - mocked_logger.info.mock_calls - ) - @patch('event_routing_backends.utils.http_client.requests.post') def test_unsuccessful_routing_of_event_http(self, mocked_post): mock_response = MagicMock() @@ -742,7 +748,311 @@ def test_successful_routing_of_bulk_events( configurations=ROUTER_CONFIG_FIXTURE[0] ) - router = EventsRouter(processors=[], backend_name=backend_name) + router = AsyncEventsRouter(processors=[], backend_name=backend_name) + + with patch.dict('event_routing_backends.tasks.ROUTER_STRATEGY_MAPPING', MOCKED_MAP): + router.bulk_send(self.bulk_transformed_events) + + overridden_events = self.bulk_transformed_events.copy() + + for event in overridden_events: + event['new_key'] = 'new_value' + + if backend_name == RouterConfiguration.XAPI_BACKEND: + # test LRS Client + mocked_lrs().save_statements.assert_has_calls([ + call(overridden_events), + ]) + else: + # test the HTTP client + if auth_scheme == RouterConfiguration.AUTH_BASIC: + mocked_post.assert_has_calls([ + call( + url=route_url, + json=overridden_events, + headers={ + }, + auth=(username, password) + ), + ]) + elif auth_scheme == RouterConfiguration.AUTH_BEARER: + mocked_post.assert_has_calls([ + call( + url=route_url, + json=overridden_events, + headers={ + 'Authorization': RouterConfiguration.AUTH_BEARER + ' ' + auth_key + } + ), + ]) + else: + mocked_post.assert_has_calls([ + call( + url=route_url, + json=overridden_events, + headers={ + }, + ), + ]) + + # test mocked oauth client + mocked_oauth_client.assert_not_called() + + +@ddt.ddt +class TestSyncEventsRouter(TestEventsRouter): # pylint: disable=test-inherits-tests + """ + Test the SyncEventsRouter + """ + @patch.dict('event_routing_backends.tasks.ROUTER_STRATEGY_MAPPING', { + 'AUTH_HEADERS': MagicMock(side_effect=EventNotDispatched) + }) + @patch('event_routing_backends.utils.http_client.requests.post') + def test_generic_exception_business_critical_event(self, mocked_post): + RouterConfigurationFactory.create( + backend_name=RouterConfiguration.XAPI_BACKEND, + enabled=True, + route_url='http://test3.com', + auth_scheme=RouterConfiguration.AUTH_BEARER, + auth_key='test_key', + configurations=ROUTER_CONFIG_FIXTURE[0] + ) + + router = SyncEventsRouter(processors=[], backend_name=RouterConfiguration.CALIPER_BACKEND) + event_data = self.transformed_event.copy() + business_critical_events = get_business_critical_events() + event_data['name'] = business_critical_events[0] + + router.send(event_data) + mocked_post.assert_not_called() + + @ddt.data( + (RouterConfiguration.AUTH_BASIC, + None, + 'abc', + 'xyz', + RouterConfiguration.CALIPER_BACKEND, + 'http://test1.com' + ), + ( + RouterConfiguration.AUTH_BEARER, + 'test_key', + None, + None, + RouterConfiguration.CALIPER_BACKEND, + 'http://test2.com' + ), + ( + None, + None, + None, + None, + RouterConfiguration.CALIPER_BACKEND, + 'http://test3.com' + ), + (RouterConfiguration.AUTH_BASIC, + None, + 'abc', + 'xyz', + RouterConfiguration.XAPI_BACKEND, + 'http://test1.com' + ), + ( + RouterConfiguration.AUTH_BEARER, + 'test_key', + None, + None, + RouterConfiguration.XAPI_BACKEND, + 'http://test2.com' + ), + ( + None, + None, + None, + None, + RouterConfiguration.XAPI_BACKEND, + 'http://test3.com' + ), + ) + @patch('event_routing_backends.utils.http_client.requests.post') + @patch('event_routing_backends.utils.xapi_lrs_client.RemoteLRS') + @ddt.unpack + def test_successful_routing_of_event( + self, + auth_scheme, + auth_key, + username, + password, + backend_name, + route_url, + mocked_lrs, + mocked_post, + ): + TieredCache.dangerous_clear_all_tiers() + mocked_oauth_client = MagicMock() + mocked_api_key_client = MagicMock() + + MOCKED_MAP = { + 'AUTH_HEADERS': HttpClient, + 'OAUTH2': mocked_oauth_client, + 'API_KEY': mocked_api_key_client, + 'XAPI_LRS': LrsClient, + } + RouterConfigurationFactory.create( + backend_name=backend_name, + enabled=True, + route_url=route_url, + auth_scheme=auth_scheme, + auth_key=auth_key, + username=username, + password=password, + configurations=ROUTER_CONFIG_FIXTURE[0] + ) + + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.request.method = "POST" + mocked_post.return_value = mock_response + + router = SyncEventsRouter(processors=[], backend_name=backend_name) + + self.transformed_event["name"] = get_business_critical_events()[0] + + with patch.dict('event_routing_backends.tasks.ROUTER_STRATEGY_MAPPING', MOCKED_MAP): + router.send(self.transformed_event) + + overridden_event = self.transformed_event.copy() + overridden_event['new_key'] = 'new_value' + + if backend_name == RouterConfiguration.XAPI_BACKEND: + # test LRS Client + mocked_lrs().save_statement.assert_has_calls([ + call(overridden_event), + ]) + else: + # test the HTTP client + if auth_scheme == RouterConfiguration.AUTH_BASIC: + mocked_post.assert_has_calls([ + call( + url=route_url, + json=overridden_event, + headers={ + }, + auth=(username, password) + ), + ]) + elif auth_scheme == RouterConfiguration.AUTH_BEARER: + mocked_post.assert_has_calls([ + call( + url=route_url, + json=overridden_event, + headers={ + 'Authorization': RouterConfiguration.AUTH_BEARER + ' ' + auth_key + } + ), + ]) + else: + mocked_post.assert_has_calls([ + call( + url=route_url, + json=overridden_event, + headers={ + }, + ), + ]) + + # test mocked oauth client + mocked_oauth_client.assert_not_called() + + @ddt.data( + (RouterConfiguration.AUTH_BASIC, + None, + 'abc', + 'xyz', + RouterConfiguration.CALIPER_BACKEND, + 'http://test1.com' + ), + ( + RouterConfiguration.AUTH_BEARER, + 'test_key', + None, + None, + RouterConfiguration.CALIPER_BACKEND, + 'http://test2.com' + ), + ( + None, + None, + None, + None, + RouterConfiguration.CALIPER_BACKEND, + 'http://test3.com' + ), + (RouterConfiguration.AUTH_BASIC, + None, + 'abc', + 'xyz', + RouterConfiguration.XAPI_BACKEND, + 'http://test1.com' + ), + ( + RouterConfiguration.AUTH_BEARER, + 'test_key', + None, + None, + RouterConfiguration.XAPI_BACKEND, + 'http://test2.com' + ), + ( + None, + None, + None, + None, + RouterConfiguration.XAPI_BACKEND, + 'http://test3.com' + ), + ) + @patch('event_routing_backends.utils.http_client.requests.post') + @patch('event_routing_backends.utils.xapi_lrs_client.RemoteLRS') + @ddt.unpack + def test_successful_routing_of_bulk_events( + self, + auth_scheme, + auth_key, + username, + password, + backend_name, + route_url, + mocked_lrs, + mocked_post, + ): + TieredCache.dangerous_clear_all_tiers() + mocked_oauth_client = MagicMock() + mocked_api_key_client = MagicMock() + + MOCKED_MAP = { + 'AUTH_HEADERS': HttpClient, + 'OAUTH2': mocked_oauth_client, + 'API_KEY': mocked_api_key_client, + 'XAPI_LRS': LrsClient, + } + RouterConfigurationFactory.create( + backend_name=backend_name, + enabled=True, + route_url=route_url, + auth_scheme=auth_scheme, + auth_key=auth_key, + username=username, + password=password, + configurations=ROUTER_CONFIG_FIXTURE[0] + ) + + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.request.method = "POST" + mocked_post.return_value = mock_response + + router = SyncEventsRouter(processors=[], backend_name=backend_name) with patch.dict('event_routing_backends.tasks.ROUTER_STRATEGY_MAPPING', MOCKED_MAP): router.bulk_send(self.bulk_transformed_events) @@ -791,3 +1101,48 @@ def test_successful_routing_of_bulk_events( # test mocked oauth client mocked_oauth_client.assert_not_called() + + @patch('event_routing_backends.utils.xapi_lrs_client.RemoteLRS') + @ddt.unpack + def test_failed_bulk_routing(self, mocked_remote_lrs): + mock_response = MagicMock() + mock_response.success = False + mock_response.data = "Fake response data" + mock_response.response.code = 500 + mock_response.request.method = "POST" + mock_response.request.content = "Fake request content" + + mocked_remote_lrs.return_value.save_statements.return_value = mock_response + RouterConfigurationFactory.create( + backend_name=RouterConfiguration.XAPI_BACKEND, + enabled=True, + route_url='http://test3.com', + configurations=ROUTER_CONFIG_FIXTURE[2] + ) + + router = SyncEventsRouter(processors=[], backend_name=RouterConfiguration.XAPI_BACKEND) + with self.assertRaises(EventNotDispatched): + router.bulk_send([self.transformed_event]) + + @patch('event_routing_backends.utils.xapi_lrs_client.RemoteLRS') + @ddt.unpack + def test_failed_routing(self, mocked_remote_lrs): + mock_response = MagicMock() + mock_response.success = False + mock_response.data = "Fake response data" + mock_response.response.code = 500 + mock_response.request.method = "POST" + mock_response.request.content = "Fake request content" + mocked_remote_lrs.side_effect = EventNotDispatched + + mocked_remote_lrs.return_value.save_statements.return_value = mock_response + RouterConfigurationFactory.create( + backend_name=RouterConfiguration.XAPI_BACKEND, + enabled=True, + route_url='http://test3.com', + configurations=ROUTER_CONFIG_FIXTURE[2] + ) + + router = SyncEventsRouter(processors=[], backend_name=RouterConfiguration.XAPI_BACKEND) + with self.assertRaises(EventNotDispatched): + router.send(self.transformed_event) diff --git a/event_routing_backends/helpers.py b/event_routing_backends/helpers.py index 8784aeb4..788db60f 100644 --- a/event_routing_backends/helpers.py +++ b/event_routing_backends/helpers.py @@ -107,7 +107,7 @@ def get_user(username_or_id): if username and not user: try: user = get_potentially_retired_user_by_username(username) - except Exception as ex: # pylint: disable=broad-except + except Exception as ex: logger.info('User with username "%s" does not exist.%s', username, ex) return user diff --git a/event_routing_backends/management/commands/transform_tracking_logs.py b/event_routing_backends/management/commands/transform_tracking_logs.py index 28e08a89..b11df861 100644 --- a/event_routing_backends/management/commands/transform_tracking_logs.py +++ b/event_routing_backends/management/commands/transform_tracking_logs.py @@ -41,7 +41,7 @@ def _get_chunks(source, file, start_byte, end_byte): break # Catching all exceptions here because there's no telling what all # the possible errors from different libcloud providers are. - except Exception as e: # pylint: disable=broad-exception-caught + except Exception as e: print(e) if try_number == num_retries: print(f"Try {try_number}: Error occurred downloading, giving up.") diff --git a/event_routing_backends/settings/common.py b/event_routing_backends/settings/common.py index 22fe40d4..d859e887 100644 --- a/event_routing_backends/settings/common.py +++ b/event_routing_backends/settings/common.py @@ -58,6 +58,101 @@ def plugin_settings(settings): 'edx.course.enrollment.deactivated', 'edx.course.grade.passed.first_time' ] + allowed_xapi_events = [ + 'edx.course.enrollment.activated', + 'edx.course.enrollment.deactivated', + 'edx.course.enrollment.mode_changed', + 'edx.grades.subsection.grade_calculated', + 'edx.grades.course.grade_calculated', + 'edx.special_exam.timed.attempt.created', + 'edx.special_exam.timed.attempt.submitted', + 'edx.special_exam.practice.attempt.created', + 'edx.special_exam.practice.attempt.submitted', + 'edx.special_exam.proctored.attempt.created', + 'edx.special_exam.proctored.attempt.submitted', + 'edx.completion.block_completion.changed', + 'edx.forum.thread.created', + 'edx.forum.thread.deleted', + 'edx.forum.thread.edited', + 'edx.forum.thread.viewed', + 'edx.forum.thread.reported', + 'edx.forum.thread.unreported', + 'edx.forum.thread.voted', + 'edx.forum.response.created', + 'edx.forum.response.deleted', + 'edx.forum.response.edited', + 'edx.forum.response.reported', + 'edx.forum.response.unreported', + 'edx.forum.response.voted', + 'edx.forum.comment.created', + 'edx.forum.comment.deleted', + 'edx.forum.comment.edited', + 'edx.forum.comment.reported', + 'edx.forum.comment.unreported', + 'edx.ui.lms.link_clicked', + 'edx.ui.lms.sequence.outline.selected', + 'edx.ui.lms.outline.selected', + 'edx.ui.lms.sequence.next_selected', + 'edx.ui.lms.sequence.previous_selected', + 'edx.ui.lms.sequence.tab_selected', + 'showanswer', + 'edx.problem.hint.demandhint_displayed', + 'problem_check', + 'load_video', + 'edx.video.loaded', + 'play_video', + 'edx.video.played', + 'complete_video', + 'edx.video.completed', + 'stop_video', + 'edx.video.stopped', + 'pause_video', + 'edx.video.paused', + 'seek_video', + 'edx.video.position.changed', + 'hide_transcript', + 'edx.video.transcript.hidden', + 'show_transcript', + 'edx.video.transcript.shown', + 'speed_change_video', + 'video_hide_cc_menu', + 'edx.video.closed_captions.shown', + 'edx.video.closed_captions.hidden', + 'edx.video.language_menu.hidden', + 'video_show_cc_menu', + 'edx.video.language_menu.shown', + 'edx.course.grade.passed.first_time' + ] + allowed_caliper_events = [ + 'edx.course.enrollment.activated', + 'edx.course.enrollment.deactivated', + 'edx.ui.lms.link_clicked', + 'edx.ui.lms.sequence.outline.selected', + 'edx.ui.lms.outline.selected', + 'edx.ui.lms.sequence.next_selected', + 'edx.ui.lms.sequence.previous_selected', + 'edx.ui.lms.sequence.tab_selected', + 'showanswer', + 'edx.problem.hint.demandhint_displayed', + 'problem_check', + 'load_video', + 'edx.video.loaded', + 'play_video', + 'edx.video.played', + 'complete_video', + 'edx.video.completed', + 'stop_video', + 'edx.video.stopped', + 'pause_video', + 'edx.video.paused', + 'seek_video', + 'edx.video.position.changed', + 'edx.course.grade.passed.first_time', + 'edx.course.grade.now_passed', + 'edx.course.grade.now_failed' + ] + + settings.EVENT_BUS_TRACKING_LOGS = set(allowed_xapi_events + allowed_caliper_events) settings.EVENT_TRACKING_BACKENDS.update({ 'xapi': { @@ -68,77 +163,13 @@ def plugin_settings(settings): { 'ENGINE': 'eventtracking.processors.whitelist.NameWhitelistProcessor', 'OPTIONS': { - 'whitelist': [ - 'edx.course.enrollment.activated', - 'edx.course.enrollment.deactivated', - 'edx.course.enrollment.mode_changed', - 'edx.grades.subsection.grade_calculated', - 'edx.grades.course.grade_calculated', - 'edx.special_exam.timed.attempt.created', - 'edx.special_exam.timed.attempt.submitted', - 'edx.special_exam.practice.attempt.created', - 'edx.special_exam.practice.attempt.submitted', - 'edx.special_exam.proctored.attempt.created', - 'edx.special_exam.proctored.attempt.submitted', - 'edx.completion.block_completion.changed', - 'edx.forum.thread.created', - 'edx.forum.thread.deleted', - 'edx.forum.thread.edited', - 'edx.forum.thread.viewed', - 'edx.forum.thread.reported', - 'edx.forum.thread.unreported', - 'edx.forum.thread.voted', - 'edx.forum.response.created', - 'edx.forum.response.deleted', - 'edx.forum.response.edited', - 'edx.forum.response.reported', - 'edx.forum.response.unreported', - 'edx.forum.response.voted', - 'edx.forum.comment.created', - 'edx.forum.comment.deleted', - 'edx.forum.comment.edited', - 'edx.forum.comment.reported', - 'edx.forum.comment.unreported', - 'edx.ui.lms.link_clicked', - 'edx.ui.lms.sequence.outline.selected', - 'edx.ui.lms.outline.selected', - 'edx.ui.lms.sequence.next_selected', - 'edx.ui.lms.sequence.previous_selected', - 'edx.ui.lms.sequence.tab_selected', - 'showanswer', - 'edx.problem.hint.demandhint_displayed', - 'problem_check', - 'load_video', - 'edx.video.loaded', - 'play_video', - 'edx.video.played', - 'complete_video', - 'edx.video.completed', - 'stop_video', - 'edx.video.stopped', - 'pause_video', - 'edx.video.paused', - 'seek_video', - 'edx.video.position.changed', - 'hide_transcript', - 'edx.video.transcript.hidden', - 'show_transcript', - 'edx.video.transcript.shown', - 'speed_change_video', - 'video_hide_cc_menu', - 'edx.video.closed_captions.shown', - 'edx.video.closed_captions.hidden', - 'edx.video.language_menu.hidden', - 'video_show_cc_menu', - 'edx.video.language_menu.shown', - 'edx.course.grade.passed.first_time' - ] + 'whitelist': allowed_xapi_events } }, ], 'backends': { 'xapi': { - 'ENGINE': 'event_routing_backends.backends.events_router.EventsRouter', + 'ENGINE': 'event_routing_backends.backends.async_events_router.AsyncEventsRouter', 'OPTIONS': { 'processors': [ { @@ -161,40 +192,13 @@ def plugin_settings(settings): { "ENGINE": "eventtracking.processors.whitelist.NameWhitelistProcessor", "OPTIONS": { - "whitelist": [ - 'edx.course.enrollment.activated', - 'edx.course.enrollment.deactivated', - 'edx.ui.lms.link_clicked', - 'edx.ui.lms.sequence.outline.selected', - 'edx.ui.lms.outline.selected', - 'edx.ui.lms.sequence.next_selected', - 'edx.ui.lms.sequence.previous_selected', - 'edx.ui.lms.sequence.tab_selected', - 'showanswer', - 'edx.problem.hint.demandhint_displayed', - 'problem_check', - 'load_video', - 'edx.video.loaded', - 'play_video', - 'edx.video.played', - 'complete_video', - 'edx.video.completed', - 'stop_video', - 'edx.video.stopped', - 'pause_video', - 'edx.video.paused', - 'seek_video', - 'edx.video.position.changed', - 'edx.course.grade.passed.first_time', - 'edx.course.grade.now_passed', - 'edx.course.grade.now_failed' - ] + "whitelist": allowed_caliper_events } } ], "backends": { "caliper": { - "ENGINE": "event_routing_backends.backends.events_router.EventsRouter", + 'ENGINE': 'event_routing_backends.backends.async_events_router.AsyncEventsRouter', "OPTIONS": { "processors": [ { diff --git a/event_routing_backends/tasks.py b/event_routing_backends/tasks.py index 19e17e3c..58c70298 100644 --- a/event_routing_backends/tasks.py +++ b/event_routing_backends/tasks.py @@ -53,11 +53,11 @@ def send_event(task, event_name, event, router_type, host_config): Send event to configured client. Arguments: - task (object) : celery task object to perform celery actions - event_name (str) : name of the original event - event (dict) : event dictionary to be delivered. - router_type (str) : decides the client to use for sending the event - host_config (dict) : contains configurations for the host. + task (object, optional) : celery task object to perform celery actions + event_name (str) : name of the original event + event (dict) : event dictionary to be delivered. + router_type (str) : decides the client to use for sending the event + host_config (dict) : contains configurations for the host. """ try: client_class = ROUTER_STRATEGY_MAPPING[router_type] @@ -82,6 +82,11 @@ def send_event(task, event_name, event, router_type, host_config): ), exc_info=True ) + # If this function is called synchronously, we want to raise the exception + # to inform about errors. If it's called asynchronously, we want to retry + # the celery task till it succeeds or reaches max retries. + if not task: + raise exc raise task.retry(exc=exc, countdown=getattr(settings, 'EVENT_ROUTING_BACKEND_COUNTDOWN', 30), max_retries=getattr(settings, '' 'EVENT_ROUTING_BACKEND_MAX_RETRIES', 3)) @@ -106,10 +111,10 @@ def bulk_send_events(task, events, router_type, host_config): Send event to configured client. Arguments: - task (object) : celery task object to perform celery actions - events (list[dict]) : list of event dictionaries to be delivered. - router_type (str) : decides the client to use for sending the event - host_config (dict) : contains configurations for the host. + task (object, optional) : celery task object to perform celery actions + events (list[dict]) : list of event dictionaries to be delivered. + router_type (str) : decides the client to use for sending the event + host_config (dict) : contains configurations for the host. """ try: client_class = ROUTER_STRATEGY_MAPPING[router_type] @@ -134,5 +139,10 @@ def bulk_send_events(task, events, router_type, host_config): ), exc_info=True ) + # If this function is called synchronously, we want to raise the exception + # to inform about errors. If it's called asynchronously, we want to retry + # the celery task till it succeeds or reaches max retries. + if not task: + raise exc raise task.retry(exc=exc, countdown=getattr(settings, 'EVENT_ROUTING_BACKEND_COUNTDOWN', 30), max_retries=getattr(settings, 'EVENT_ROUTING_BACKEND_MAX_RETRIES', 3)) diff --git a/requirements/base.txt b/requirements/base.txt index 41daa4cf..231a8394 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -12,6 +12,8 @@ apache-libcloud==3.8.0 # via -r requirements/base.in asgiref==3.7.2 # via django +attrs==23.2.0 + # via openedx-events backports-zoneinfo[tzdata]==0.2.1 # via # celery @@ -22,7 +24,7 @@ celery==5.3.6 # via # edx-celeryutils # event-tracking -certifi==2023.11.17 +certifi==2024.2.2 # via requests cffi==1.16.0 # via @@ -44,11 +46,11 @@ click-plugins==1.1.1 # via celery click-repl==0.3.0 # via celery -code-annotations==1.5.0 +code-annotations==1.6.0 # via edx-toggles -cryptography==41.0.7 +cryptography==42.0.3 # via django-fernet-fields-v2 -django==3.2.23 +django==3.2.24 # via # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt # -r requirements/base.in @@ -63,6 +65,7 @@ django==3.2.23 # edx-toggles # event-tracking # jsonfield + # openedx-events # openedx-filters django-config-models==2.5.1 # via -r requirements/base.in @@ -72,64 +75,75 @@ django-crum==0.7.9 # edx-toggles django-fernet-fields-v2==0.9 # via -r requirements/base.in -django-model-utils==4.3.1 +django-model-utils==4.4.0 # via edx-celeryutils -django-waffle==4.0.0 +django-waffle==4.1.0 # via # edx-django-utils # edx-toggles djangorestframework==3.14.0 # via django-config-models -edx-celeryutils==1.2.3 +edx-celeryutils==1.2.5 # via -r requirements/base.in -edx-django-utils==5.9.0 +edx-django-utils==5.10.1 # via # django-config-models # edx-toggles # event-tracking -edx-toggles==5.1.0 - # via -r requirements/base.in -event-tracking==2.2.0 + # openedx-events +edx-opaque-keys[django]==2.5.1 + # via openedx-events +edx-toggles==5.1.1 + # via + # -r requirements/base.in + # event-tracking +event-tracking==2.3.0 # via -r requirements/base.in +fastavro==1.9.4 + # via openedx-events fasteners==0.19 # via -r requirements/base.in idna==3.6 # via requests isodate==0.6.1 # via -r requirements/base.in -jinja2==3.1.2 +jinja2==3.1.3 # via code-annotations jsonfield==3.1.0 # via # -r requirements/base.in # edx-celeryutils -kombu==5.3.4 +kombu==5.3.5 # via celery -markupsafe==2.1.3 +markupsafe==2.1.5 # via jinja2 -newrelic==9.3.0 +newrelic==9.6.0 # via edx-django-utils +openedx-events==9.5.1 + # via event-tracking openedx-filters==1.6.0 # via -r requirements/base.in pbr==6.0.0 # via stevedore -prompt-toolkit==3.0.41 +prompt-toolkit==3.0.43 # via click-repl -psutil==5.9.6 +psutil==5.9.8 # via edx-django-utils pycparser==2.21 # via cffi pymongo==3.13.0 - # via event-tracking + # via + # edx-opaque-keys + # event-tracking pynacl==1.5.0 # via edx-django-utils python-dateutil==2.8.2 # via # -r requirements/base.in # celery -python-slugify==8.0.1 +python-slugify==8.0.4 # via code-annotations -pytz==2023.3.post1 +pytz==2024.1 # via # -r requirements/base.in # django @@ -153,24 +167,26 @@ stevedore==5.1.0 # via # code-annotations # edx-django-utils + # edx-opaque-keys text-unidecode==1.3 # via python-slugify tincan==1.0.0 # via -r requirements/base.in -typing-extensions==4.8.0 +typing-extensions==4.9.0 # via # asgiref + # edx-opaque-keys # kombu -tzdata==2023.3 +tzdata==2024.1 # via # backports-zoneinfo # celery -urllib3==2.1.0 +urllib3==2.2.0 # via requests vine==5.1.0 # via # amqp # celery # kombu -wcwidth==0.2.12 +wcwidth==0.2.13 # via prompt-toolkit diff --git a/requirements/ci.txt b/requirements/ci.txt index efd08147..742e39c1 100644 --- a/requirements/ci.txt +++ b/requirements/ci.txt @@ -10,7 +10,7 @@ chardet==5.2.0 # via tox colorama==0.4.6 # via tox -distlib==0.3.7 +distlib==0.3.8 # via virtualenv filelock==3.13.1 # via @@ -20,11 +20,11 @@ packaging==23.2 # via # pyproject-api # tox -platformdirs==4.1.0 +platformdirs==4.2.0 # via # tox # virtualenv -pluggy==1.3.0 +pluggy==1.4.0 # via tox pyproject-api==1.6.1 # via tox @@ -32,7 +32,7 @@ tomli==2.0.1 # via # pyproject-api # tox -tox==4.11.4 +tox==4.12.1 # via -r requirements/ci.in virtualenv==20.25.0 # via tox diff --git a/requirements/dev.txt b/requirements/dev.txt index 1fbcb828..c7207bb8 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -20,15 +20,18 @@ asgiref==3.7.2 # via # -r requirements/quality.txt # django -astroid==3.0.1 +astroid==3.0.3 # via # -r requirements/quality.txt # pylint # pylint-celery +attrs==23.2.0 + # via + # -r requirements/quality.txt + # openedx-events backports-zoneinfo[tzdata]==0.2.1 # via # -r requirements/quality.txt - # backports-zoneinfo # celery # kombu billiard==4.2.0 @@ -48,7 +51,7 @@ celery==5.3.6 # -r requirements/quality.txt # edx-celeryutils # event-tracking -certifi==2023.11.17 +certifi==2024.2.2 # via # -r requirements/quality.txt # requests @@ -94,7 +97,7 @@ click-repl==0.3.0 # via # -r requirements/quality.txt # celery -code-annotations==1.5.0 +code-annotations==1.6.0 # via # -r requirements/quality.txt # edx-lint @@ -103,30 +106,29 @@ colorama==0.4.6 # via # -r requirements/ci.txt # tox -coverage[toml]==7.3.2 +coverage[toml]==7.4.1 # via # -r requirements/quality.txt - # coverage # pytest-cov -cryptography==41.0.7 +cryptography==42.0.3 # via # -r requirements/quality.txt # django-fernet-fields-v2 -ddt==1.7.0 +ddt==1.7.1 # via -r requirements/quality.txt diff-cover==4.0.0 # via # -c requirements/constraints.txt # -r requirements/dev.in -dill==0.3.7 +dill==0.3.8 # via # -r requirements/quality.txt # pylint -distlib==0.3.7 +distlib==0.3.8 # via # -r requirements/ci.txt # virtualenv -django==3.2.23 +django==3.2.24 # via # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt # -r requirements/quality.txt @@ -142,6 +144,7 @@ django==3.2.23 # edx-toggles # event-tracking # jsonfield + # openedx-events # openedx-filters django-config-models==2.5.1 # via -r requirements/quality.txt @@ -152,11 +155,11 @@ django-crum==0.7.9 # edx-toggles django-fernet-fields-v2==0.9 # via -r requirements/quality.txt -django-model-utils==4.3.1 +django-model-utils==4.4.0 # via # -r requirements/quality.txt # edx-celeryutils -django-waffle==4.0.0 +django-waffle==4.1.0 # via # -r requirements/quality.txt # edx-django-utils @@ -165,21 +168,28 @@ djangorestframework==3.14.0 # via # -r requirements/quality.txt # django-config-models -edx-celeryutils==1.2.3 +edx-celeryutils==1.2.5 # via -r requirements/quality.txt -edx-django-utils==5.9.0 +edx-django-utils==5.10.1 # via # -r requirements/quality.txt # django-config-models # edx-toggles # event-tracking + # openedx-events edx-i18n-tools==1.3.0 # via -r requirements/dev.in edx-lint==5.3.6 # via -r requirements/quality.txt -edx-toggles==5.1.0 - # via -r requirements/quality.txt -event-tracking==2.2.0 +edx-opaque-keys[django]==2.5.1 + # via + # -r requirements/quality.txt + # openedx-events +edx-toggles==5.1.1 + # via + # -r requirements/quality.txt + # event-tracking +event-tracking==2.3.0 # via -r requirements/quality.txt exceptiongroup==1.2.0 # via @@ -187,10 +197,14 @@ exceptiongroup==1.2.0 # pytest factory-boy==3.3.0 # via -r requirements/quality.txt -faker==20.1.0 +faker==23.2.1 # via # -r requirements/quality.txt # factory-boy +fastavro==1.9.4 + # via + # -r requirements/quality.txt + # openedx-events fasteners==0.19 # via -r requirements/quality.txt filelock==3.13.1 @@ -202,7 +216,7 @@ idna==3.6 # via # -r requirements/quality.txt # requests -importlib-metadata==7.0.0 +importlib-metadata==7.0.1 # via # -r requirements/pip-tools.txt # build @@ -214,11 +228,11 @@ iniconfig==2.0.0 # pytest isodate==0.6.1 # via -r requirements/quality.txt -isort==5.12.0 +isort==5.13.2 # via # -r requirements/quality.txt # pylint -jinja2==3.1.2 +jinja2==3.1.3 # via # -r requirements/quality.txt # code-annotations @@ -230,13 +244,13 @@ jsonfield==3.1.0 # via # -r requirements/quality.txt # edx-celeryutils -kombu==5.3.4 +kombu==5.3.5 # via # -r requirements/quality.txt # celery -lxml==4.9.3 +lxml==5.1.0 # via edx-i18n-tools -markupsafe==2.1.3 +markupsafe==2.1.5 # via # -r requirements/quality.txt # jinja2 @@ -246,10 +260,14 @@ mccabe==0.7.0 # pylint mock==5.1.0 # via -r requirements/quality.txt -newrelic==9.3.0 +newrelic==9.6.0 # via # -r requirements/quality.txt # edx-django-utils +openedx-events==9.5.1 + # via + # -r requirements/quality.txt + # event-tracking openedx-filters==1.6.0 # via -r requirements/quality.txt packaging==23.2 @@ -261,22 +279,22 @@ packaging==23.2 # pyproject-api # pytest # tox -path==16.9.0 +path==16.10.0 # via edx-i18n-tools pbr==6.0.0 # via # -r requirements/quality.txt # stevedore -pip-tools==7.3.0 +pip-tools==7.4.0 # via -r requirements/pip-tools.txt -platformdirs==4.1.0 +platformdirs==4.2.0 # via # -r requirements/ci.txt # -r requirements/quality.txt # pylint # tox # virtualenv -pluggy==1.3.0 +pluggy==1.4.0 # via # -r requirements/ci.txt # -r requirements/quality.txt @@ -285,11 +303,11 @@ pluggy==1.3.0 # tox polib==1.2.0 # via edx-i18n-tools -prompt-toolkit==3.0.41 +prompt-toolkit==3.0.43 # via # -r requirements/quality.txt # click-repl -psutil==5.9.6 +psutil==5.9.8 # via # -r requirements/quality.txt # edx-django-utils @@ -299,15 +317,15 @@ pycparser==2.21 # via # -r requirements/quality.txt # cffi -pydantic==2.5.2 +pydantic==2.6.1 # via inflect -pydantic-core==2.14.5 +pydantic-core==2.16.2 # via pydantic pydocstyle==6.3.0 # via -r requirements/quality.txt pygments==2.17.2 # via diff-cover -pylint==3.0.2 +pylint==3.0.3 # via # -r requirements/quality.txt # edx-lint @@ -330,6 +348,7 @@ pylint-plugin-utils==0.8.2 pymongo==3.13.0 # via # -r requirements/quality.txt + # edx-opaque-keys # event-tracking pynacl==1.5.0 # via @@ -343,25 +362,26 @@ pyproject-hooks==1.0.0 # via # -r requirements/pip-tools.txt # build -pytest==7.4.3 + # pip-tools +pytest==8.0.0 # via # -r requirements/quality.txt # pytest-cov # pytest-django pytest-cov==4.1.0 # via -r requirements/quality.txt -pytest-django==4.7.0 +pytest-django==4.8.0 # via -r requirements/quality.txt python-dateutil==2.8.2 # via # -r requirements/quality.txt # celery # faker -python-slugify==8.0.1 +python-slugify==8.0.4 # via # -r requirements/quality.txt # code-annotations -pytz==2023.3.post1 +pytz==2024.1 # via # -r requirements/quality.txt # django @@ -397,6 +417,7 @@ stevedore==5.1.0 # -r requirements/quality.txt # code-annotations # edx-django-utils + # edx-opaque-keys text-unidecode==1.3 # via # -r requirements/quality.txt @@ -420,26 +441,27 @@ tomlkit==0.12.3 # via # -r requirements/quality.txt # pylint -tox==4.11.4 +tox==4.12.1 # via -r requirements/ci.txt -typing-extensions==4.8.0 +typing-extensions==4.9.0 # via # -r requirements/quality.txt # annotated-types # asgiref # astroid + # edx-opaque-keys # faker # inflect # kombu # pydantic # pydantic-core # pylint -tzdata==2023.3 +tzdata==2024.1 # via # -r requirements/quality.txt # backports-zoneinfo # celery -urllib3==2.1.0 +urllib3==2.2.0 # via # -r requirements/quality.txt # requests @@ -453,7 +475,7 @@ virtualenv==20.25.0 # via # -r requirements/ci.txt # tox -wcwidth==0.2.12 +wcwidth==0.2.13 # via # -r requirements/quality.txt # prompt-toolkit diff --git a/requirements/doc.txt b/requirements/doc.txt index fa831e0f..2513b488 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -22,17 +22,20 @@ asgiref==3.7.2 # via # -r requirements/test.txt # django -babel==2.13.1 +attrs==23.2.0 + # via + # -r requirements/test.txt + # openedx-events +babel==2.14.0 # via # pydata-sphinx-theme # sphinx backports-zoneinfo[tzdata]==0.2.1 # via # -r requirements/test.txt - # backports-zoneinfo # celery # kombu -beautifulsoup4==4.12.2 +beautifulsoup4==4.12.3 # via pydata-sphinx-theme billiard==4.2.0 # via @@ -45,7 +48,7 @@ celery==5.3.6 # -r requirements/test.txt # edx-celeryutils # event-tracking -certifi==2023.11.17 +certifi==2024.2.2 # via # -r requirements/test.txt # requests @@ -79,22 +82,22 @@ click-repl==0.3.0 # via # -r requirements/test.txt # celery -code-annotations==1.5.0 +code-annotations==1.6.0 # via # -r requirements/test.txt # edx-toggles -coverage[toml]==7.3.2 +coverage[toml]==7.4.1 # via # -r requirements/test.txt - # coverage # pytest-cov -cryptography==41.0.7 +cryptography==42.0.3 # via # -r requirements/test.txt # django-fernet-fields-v2 -ddt==1.7.0 + # secretstorage +ddt==1.7.1 # via -r requirements/test.txt -django==3.2.23 +django==3.2.24 # via # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt # -r requirements/test.txt @@ -109,6 +112,7 @@ django==3.2.23 # edx-toggles # event-tracking # jsonfield + # openedx-events # openedx-filters django-config-models==2.5.1 # via -r requirements/test.txt @@ -119,11 +123,11 @@ django-crum==0.7.9 # edx-toggles django-fernet-fields-v2==0.9 # via -r requirements/test.txt -django-model-utils==4.3.1 +django-model-utils==4.4.0 # via # -r requirements/test.txt # edx-celeryutils -django-waffle==4.0.0 +django-waffle==4.1.0 # via # -r requirements/test.txt # edx-django-utils @@ -144,17 +148,24 @@ docutils==0.17.1 # readme-renderer # restructuredtext-lint # sphinx -edx-celeryutils==1.2.3 +edx-celeryutils==1.2.5 # via -r requirements/test.txt -edx-django-utils==5.9.0 +edx-django-utils==5.10.1 # via # -r requirements/test.txt # django-config-models # edx-toggles # event-tracking -edx-toggles==5.1.0 - # via -r requirements/test.txt -event-tracking==2.2.0 + # openedx-events +edx-opaque-keys[django]==2.5.1 + # via + # -r requirements/test.txt + # openedx-events +edx-toggles==5.1.1 + # via + # -r requirements/test.txt + # event-tracking +event-tracking==2.3.0 # via -r requirements/test.txt exceptiongroup==1.2.0 # via @@ -162,10 +173,14 @@ exceptiongroup==1.2.0 # pytest factory-boy==3.3.0 # via -r requirements/test.txt -faker==20.1.0 +faker==23.2.1 # via # -r requirements/test.txt # factory-boy +fastavro==1.9.4 + # via + # -r requirements/test.txt + # openedx-events fasteners==0.19 # via -r requirements/test.txt idna==3.6 @@ -174,7 +189,7 @@ idna==3.6 # requests imagesize==1.4.1 # via sphinx -importlib-metadata==7.0.0 +importlib-metadata==7.0.1 # via # build # keyring @@ -187,9 +202,13 @@ iniconfig==2.0.0 # pytest isodate==0.6.1 # via -r requirements/test.txt -jaraco-classes==3.3.0 +jaraco-classes==3.3.1 # via keyring -jinja2==3.1.2 +jeepney==0.8.0 + # via + # keyring + # secretstorage +jinja2==3.1.3 # via # -r requirements/test.txt # code-annotations @@ -200,13 +219,13 @@ jsonfield==3.1.0 # edx-celeryutils keyring==24.3.0 # via twine -kombu==5.3.4 +kombu==5.3.5 # via # -r requirements/test.txt # celery markdown-it-py==3.0.0 # via rich -markupsafe==2.1.3 +markupsafe==2.1.5 # via # -r requirements/test.txt # jinja2 @@ -214,14 +233,18 @@ mdurl==0.1.2 # via markdown-it-py mock==5.1.0 # via -r requirements/test.txt -more-itertools==10.1.0 +more-itertools==10.2.0 # via jaraco-classes -newrelic==9.3.0 +newrelic==9.6.0 # via # -r requirements/test.txt # edx-django-utils nh3==0.2.15 # via readme-renderer +openedx-events==9.5.1 + # via + # -r requirements/test.txt + # event-tracking openedx-filters==1.6.0 # via -r requirements/test.txt packaging==23.2 @@ -237,15 +260,15 @@ pbr==6.0.0 # stevedore pkginfo==1.9.6 # via twine -pluggy==1.3.0 +pluggy==1.4.0 # via # -r requirements/test.txt # pytest -prompt-toolkit==3.0.41 +prompt-toolkit==3.0.43 # via # -r requirements/test.txt # click-repl -psutil==5.9.6 +psutil==5.9.8 # via # -r requirements/test.txt # edx-django-utils @@ -266,6 +289,7 @@ pygments==2.17.2 pymongo==3.13.0 # via # -r requirements/test.txt + # edx-opaque-keys # event-tracking pynacl==1.5.0 # via @@ -273,25 +297,25 @@ pynacl==1.5.0 # edx-django-utils pyproject-hooks==1.0.0 # via build -pytest==7.4.3 +pytest==8.0.0 # via # -r requirements/test.txt # pytest-cov # pytest-django pytest-cov==4.1.0 # via -r requirements/test.txt -pytest-django==4.7.0 +pytest-django==4.8.0 # via -r requirements/test.txt python-dateutil==2.8.2 # via # -r requirements/test.txt # celery # faker -python-slugify==8.0.1 +python-slugify==8.0.4 # via # -r requirements/test.txt # code-annotations -pytz==2023.3.post1 +pytz==2024.1 # via # -r requirements/test.txt # babel @@ -320,6 +344,8 @@ rfc3986==2.0.0 # via twine rich==13.7.0 # via twine +secretstorage==3.3.3 + # via keyring six==1.16.0 # via # -r requirements/test.txt @@ -360,6 +386,7 @@ stevedore==5.1.0 # code-annotations # doc8 # edx-django-utils + # edx-opaque-keys text-unidecode==1.3 # via # -r requirements/test.txt @@ -373,22 +400,23 @@ tomli==2.0.1 # coverage # pyproject-hooks # pytest -twine==4.0.2 +twine==5.0.0 # via -r requirements/doc.in -typing-extensions==4.8.0 +typing-extensions==4.9.0 # via # -r requirements/test.txt # asgiref + # edx-opaque-keys # faker # kombu # pydata-sphinx-theme # rich -tzdata==2023.3 +tzdata==2024.1 # via # -r requirements/test.txt # backports-zoneinfo # celery -urllib3==2.1.0 +urllib3==2.2.0 # via # -r requirements/test.txt # requests @@ -399,7 +427,7 @@ vine==5.1.0 # amqp # celery # kombu -wcwidth==0.2.12 +wcwidth==0.2.13 # via # -r requirements/test.txt # prompt-toolkit diff --git a/requirements/pip-tools.txt b/requirements/pip-tools.txt index 93a9cee2..44c48d99 100644 --- a/requirements/pip-tools.txt +++ b/requirements/pip-tools.txt @@ -8,14 +8,16 @@ build==1.0.3 # via pip-tools click==8.1.7 # via pip-tools -importlib-metadata==7.0.0 +importlib-metadata==7.0.1 # via build packaging==23.2 # via build -pip-tools==7.3.0 +pip-tools==7.4.0 # via -r requirements/pip-tools.in pyproject-hooks==1.0.0 - # via build + # via + # build + # pip-tools tomli==2.0.1 # via # build diff --git a/requirements/pip.txt b/requirements/pip.txt index 14cb99cd..71954cc6 100644 --- a/requirements/pip.txt +++ b/requirements/pip.txt @@ -8,7 +8,7 @@ wheel==0.42.0 # via -r requirements/pip.in # The following packages are considered to be unsafe in a requirements file: -pip==23.3.1 +pip==24.0 # via -r requirements/pip.in -setuptools==69.0.2 +setuptools==69.1.0 # via -r requirements/pip.in diff --git a/requirements/quality.txt b/requirements/quality.txt index 2a88e430..f665ed61 100644 --- a/requirements/quality.txt +++ b/requirements/quality.txt @@ -18,14 +18,17 @@ asgiref==3.7.2 # via # -r requirements/test.txt # django -astroid==3.0.1 +astroid==3.0.3 # via # pylint # pylint-celery +attrs==23.2.0 + # via + # -r requirements/test.txt + # openedx-events backports-zoneinfo[tzdata]==0.2.1 # via # -r requirements/test.txt - # backports-zoneinfo # celery # kombu billiard==4.2.0 @@ -37,7 +40,7 @@ celery==5.3.6 # -r requirements/test.txt # edx-celeryutils # event-tracking -certifi==2023.11.17 +certifi==2024.2.2 # via # -r requirements/test.txt # requests @@ -75,25 +78,24 @@ click-repl==0.3.0 # via # -r requirements/test.txt # celery -code-annotations==1.5.0 +code-annotations==1.6.0 # via # -r requirements/test.txt # edx-lint # edx-toggles -coverage[toml]==7.3.2 +coverage[toml]==7.4.1 # via # -r requirements/test.txt - # coverage # pytest-cov -cryptography==41.0.7 +cryptography==42.0.3 # via # -r requirements/test.txt # django-fernet-fields-v2 -ddt==1.7.0 +ddt==1.7.1 # via -r requirements/test.txt -dill==0.3.7 +dill==0.3.8 # via pylint -django==3.2.23 +django==3.2.24 # via # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt # -r requirements/test.txt @@ -108,6 +110,7 @@ django==3.2.23 # edx-toggles # event-tracking # jsonfield + # openedx-events # openedx-filters django-config-models==2.5.1 # via -r requirements/test.txt @@ -118,11 +121,11 @@ django-crum==0.7.9 # edx-toggles django-fernet-fields-v2==0.9 # via -r requirements/test.txt -django-model-utils==4.3.1 +django-model-utils==4.4.0 # via # -r requirements/test.txt # edx-celeryutils -django-waffle==4.0.0 +django-waffle==4.1.0 # via # -r requirements/test.txt # edx-django-utils @@ -131,19 +134,26 @@ djangorestframework==3.14.0 # via # -r requirements/test.txt # django-config-models -edx-celeryutils==1.2.3 +edx-celeryutils==1.2.5 # via -r requirements/test.txt -edx-django-utils==5.9.0 +edx-django-utils==5.10.1 # via # -r requirements/test.txt # django-config-models # edx-toggles # event-tracking + # openedx-events edx-lint==5.3.6 # via -r requirements/quality.in -edx-toggles==5.1.0 - # via -r requirements/test.txt -event-tracking==2.2.0 +edx-opaque-keys[django]==2.5.1 + # via + # -r requirements/test.txt + # openedx-events +edx-toggles==5.1.1 + # via + # -r requirements/test.txt + # event-tracking +event-tracking==2.3.0 # via -r requirements/test.txt exceptiongroup==1.2.0 # via @@ -151,10 +161,14 @@ exceptiongroup==1.2.0 # pytest factory-boy==3.3.0 # via -r requirements/test.txt -faker==20.1.0 +faker==23.2.1 # via # -r requirements/test.txt # factory-boy +fastavro==1.9.4 + # via + # -r requirements/test.txt + # openedx-events fasteners==0.19 # via -r requirements/test.txt idna==3.6 @@ -167,11 +181,11 @@ iniconfig==2.0.0 # pytest isodate==0.6.1 # via -r requirements/test.txt -isort==5.12.0 +isort==5.13.2 # via # -r requirements/quality.in # pylint -jinja2==3.1.2 +jinja2==3.1.3 # via # -r requirements/test.txt # code-annotations @@ -179,11 +193,11 @@ jsonfield==3.1.0 # via # -r requirements/test.txt # edx-celeryutils -kombu==5.3.4 +kombu==5.3.5 # via # -r requirements/test.txt # celery -markupsafe==2.1.3 +markupsafe==2.1.5 # via # -r requirements/test.txt # jinja2 @@ -191,10 +205,14 @@ mccabe==0.7.0 # via pylint mock==5.1.0 # via -r requirements/test.txt -newrelic==9.3.0 +newrelic==9.6.0 # via # -r requirements/test.txt # edx-django-utils +openedx-events==9.5.1 + # via + # -r requirements/test.txt + # event-tracking openedx-filters==1.6.0 # via -r requirements/test.txt packaging==23.2 @@ -205,17 +223,17 @@ pbr==6.0.0 # via # -r requirements/test.txt # stevedore -platformdirs==4.1.0 +platformdirs==4.2.0 # via pylint -pluggy==1.3.0 +pluggy==1.4.0 # via # -r requirements/test.txt # pytest -prompt-toolkit==3.0.41 +prompt-toolkit==3.0.43 # via # -r requirements/test.txt # click-repl -psutil==5.9.6 +psutil==5.9.8 # via # -r requirements/test.txt # edx-django-utils @@ -227,7 +245,7 @@ pycparser==2.21 # cffi pydocstyle==6.3.0 # via -r requirements/quality.in -pylint==3.0.2 +pylint==3.0.3 # via # edx-lint # pylint-celery @@ -244,30 +262,31 @@ pylint-plugin-utils==0.8.2 pymongo==3.13.0 # via # -r requirements/test.txt + # edx-opaque-keys # event-tracking pynacl==1.5.0 # via # -r requirements/test.txt # edx-django-utils -pytest==7.4.3 +pytest==8.0.0 # via # -r requirements/test.txt # pytest-cov # pytest-django pytest-cov==4.1.0 # via -r requirements/test.txt -pytest-django==4.7.0 +pytest-django==4.8.0 # via -r requirements/test.txt python-dateutil==2.8.2 # via # -r requirements/test.txt # celery # faker -python-slugify==8.0.1 +python-slugify==8.0.4 # via # -r requirements/test.txt # code-annotations -pytz==2023.3.post1 +pytz==2024.1 # via # -r requirements/test.txt # django @@ -300,6 +319,7 @@ stevedore==5.1.0 # -r requirements/test.txt # code-annotations # edx-django-utils + # edx-opaque-keys text-unidecode==1.3 # via # -r requirements/test.txt @@ -314,20 +334,21 @@ tomli==2.0.1 # pytest tomlkit==0.12.3 # via pylint -typing-extensions==4.8.0 +typing-extensions==4.9.0 # via # -r requirements/test.txt # asgiref # astroid + # edx-opaque-keys # faker # kombu # pylint -tzdata==2023.3 +tzdata==2024.1 # via # -r requirements/test.txt # backports-zoneinfo # celery -urllib3==2.1.0 +urllib3==2.2.0 # via # -r requirements/test.txt # requests @@ -337,7 +358,7 @@ vine==5.1.0 # amqp # celery # kombu -wcwidth==0.2.12 +wcwidth==0.2.13 # via # -r requirements/test.txt # prompt-toolkit diff --git a/requirements/test.txt b/requirements/test.txt index e87b91a5..d87bcc3f 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -18,10 +18,13 @@ asgiref==3.7.2 # via # -r requirements/base.txt # django +attrs==23.2.0 + # via + # -r requirements/base.txt + # openedx-events backports-zoneinfo[tzdata]==0.2.1 # via # -r requirements/base.txt - # backports-zoneinfo # celery # kombu billiard==4.2.0 @@ -33,7 +36,7 @@ celery==5.3.6 # -r requirements/base.txt # edx-celeryutils # event-tracking -certifi==2023.11.17 +certifi==2024.2.2 # via # -r requirements/base.txt # requests @@ -67,20 +70,18 @@ click-repl==0.3.0 # via # -r requirements/base.txt # celery -code-annotations==1.5.0 +code-annotations==1.6.0 # via # -r requirements/base.txt # -r requirements/test.in # edx-toggles -coverage[toml]==7.3.2 - # via - # coverage - # pytest-cov -cryptography==41.0.7 +coverage[toml]==7.4.1 + # via pytest-cov +cryptography==42.0.3 # via # -r requirements/base.txt # django-fernet-fields-v2 -ddt==1.7.0 +ddt==1.7.1 # via -r requirements/test.in # via # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt @@ -96,6 +97,7 @@ ddt==1.7.0 # edx-toggles # event-tracking # jsonfield + # openedx-events # openedx-filters django-config-models==2.5.1 # via -r requirements/base.txt @@ -106,11 +108,11 @@ django-crum==0.7.9 # edx-toggles django-fernet-fields-v2==0.9 # via -r requirements/base.txt -django-model-utils==4.3.1 +django-model-utils==4.4.0 # via # -r requirements/base.txt # edx-celeryutils -django-waffle==4.0.0 +django-waffle==4.1.0 # via # -r requirements/base.txt # edx-django-utils @@ -119,24 +121,35 @@ djangorestframework==3.14.0 # via # -r requirements/base.txt # django-config-models -edx-celeryutils==1.2.3 +edx-celeryutils==1.2.5 # via -r requirements/base.txt -edx-django-utils==5.9.0 +edx-django-utils==5.10.1 # via # -r requirements/base.txt # django-config-models # edx-toggles # event-tracking -edx-toggles==5.1.0 - # via -r requirements/base.txt -event-tracking==2.2.0 + # openedx-events +edx-opaque-keys[django]==2.5.1 + # via + # -r requirements/base.txt + # openedx-events +edx-toggles==5.1.1 + # via + # -r requirements/base.txt + # event-tracking +event-tracking==2.3.0 # via -r requirements/base.txt exceptiongroup==1.2.0 # via pytest factory-boy==3.3.0 # via -r requirements/test.in -faker==20.1.0 +faker==23.2.1 # via factory-boy +fastavro==1.9.4 + # via + # -r requirements/base.txt + # openedx-events fasteners==0.19 # via -r requirements/base.txt idna==3.6 @@ -147,7 +160,7 @@ iniconfig==2.0.0 # via pytest isodate==0.6.1 # via -r requirements/base.txt -jinja2==3.1.2 +jinja2==3.1.3 # via # -r requirements/base.txt # code-annotations @@ -155,20 +168,24 @@ jsonfield==3.1.0 # via # -r requirements/base.txt # edx-celeryutils -kombu==5.3.4 +kombu==5.3.5 # via # -r requirements/base.txt # celery -markupsafe==2.1.3 +markupsafe==2.1.5 # via # -r requirements/base.txt # jinja2 mock==5.1.0 # via -r requirements/test.in -newrelic==9.3.0 +newrelic==9.6.0 # via # -r requirements/base.txt # edx-django-utils +openedx-events==9.5.1 + # via + # -r requirements/base.txt + # event-tracking openedx-filters==1.6.0 # via -r requirements/base.txt packaging==23.2 @@ -177,13 +194,13 @@ pbr==6.0.0 # via # -r requirements/base.txt # stevedore -pluggy==1.3.0 +pluggy==1.4.0 # via pytest -prompt-toolkit==3.0.41 +prompt-toolkit==3.0.43 # via # -r requirements/base.txt # click-repl -psutil==5.9.6 +psutil==5.9.8 # via # -r requirements/base.txt # edx-django-utils @@ -194,29 +211,30 @@ pycparser==2.21 pymongo==3.13.0 # via # -r requirements/base.txt + # edx-opaque-keys # event-tracking pynacl==1.5.0 # via # -r requirements/base.txt # edx-django-utils -pytest==7.4.3 +pytest==8.0.0 # via # pytest-cov # pytest-django pytest-cov==4.1.0 # via -r requirements/test.in -pytest-django==4.7.0 +pytest-django==4.8.0 # via -r requirements/test.in python-dateutil==2.8.2 # via # -r requirements/base.txt # celery # faker -python-slugify==8.0.1 +python-slugify==8.0.4 # via # -r requirements/base.txt # code-annotations -pytz==2023.3.post1 +pytz==2024.1 # via # -r requirements/base.txt # django @@ -246,6 +264,7 @@ stevedore==5.1.0 # -r requirements/base.txt # code-annotations # edx-django-utils + # edx-opaque-keys text-unidecode==1.3 # via # -r requirements/base.txt @@ -256,18 +275,19 @@ tomli==2.0.1 # via # coverage # pytest -typing-extensions==4.8.0 +typing-extensions==4.9.0 # via # -r requirements/base.txt # asgiref + # edx-opaque-keys # faker # kombu -tzdata==2023.3 +tzdata==2024.1 # via # -r requirements/base.txt # backports-zoneinfo # celery -urllib3==2.1.0 +urllib3==2.2.0 # via # -r requirements/base.txt # requests @@ -277,7 +297,7 @@ vine==5.1.0 # amqp # celery # kombu -wcwidth==0.2.12 +wcwidth==0.2.13 # via # -r requirements/base.txt # prompt-toolkit