-
Notifications
You must be signed in to change notification settings - Fork 27
feat: add necessary tooling for Open edX events #7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| """ | ||
| Data attributes for events within the architecture subdomain `learning`. | ||
| These attributes follow the form of attr objects specified in OEP-49 data | ||
| pattern. | ||
| """ | ||
| import socket | ||
| from datetime import datetime | ||
| from uuid import UUID, uuid1 | ||
|
|
||
| import attr | ||
| from django.conf import settings | ||
|
|
||
| import openedx_events | ||
|
|
||
|
|
||
| @attr.s(frozen=True) | ||
| class EventsMetadata: | ||
| """ | ||
| Attributes defined for Open edX Events metadata object. | ||
| The attributes defined in this class are a subset of the | ||
| OEP-41: Asynchronous Server Event Message Format. | ||
| Arguments: | ||
| id (UUID): event identifier. | ||
| event_type (str): name of the event. | ||
| minorversion (int): version of the event type. | ||
| source (str): logical source of an event. | ||
| sourcehost (str): physical source of the event. | ||
| time (datetime): timestamp when the event was sent. | ||
| sourcelib (str): Open edX Events library version. | ||
| """ | ||
|
|
||
| id = attr.ib(type=UUID, init=False) | ||
| event_type = attr.ib(type=str) | ||
| minorversion = attr.ib(type=int, converter=attr.converters.default_if_none(0)) | ||
| source = attr.ib(type=str, init=False) | ||
| sourcehost = attr.ib(type=str, init=False) | ||
| time = attr.ib(type=datetime, init=False) | ||
| sourcelib = attr.ib(type=tuple, init=False) | ||
|
|
||
| def __attrs_post_init__(self): | ||
| """ | ||
| Post-init hook that generates metadata for the Open edX Event. | ||
| """ | ||
| # Have to use this to get around the fact that the class is frozen | ||
| # (which we almost always want, but not while we're initializing it). | ||
| # Taken from edX Learning Sequences data file. | ||
| object.__setattr__(self, "id", uuid1()) | ||
| object.__setattr__( | ||
| self, | ||
| "source", | ||
| "openedx/{service}/web".format( | ||
| service=getattr(settings, "SERVICE_VARIANT", "") | ||
| ), | ||
| ) | ||
| object.__setattr__(self, "sourcehost", socket.gethostname()) | ||
| object.__setattr__(self, "time", datetime.utcnow()) | ||
| object.__setattr__( | ||
| self, "sourcelib", tuple(map(int, openedx_events.__version__.split("."))) | ||
| ) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| """ | ||
| Custom exceptions thrown by Open edX events tooling. | ||
| """ | ||
|
|
||
|
|
||
| class OpenEdxEventException(Exception): | ||
| """ | ||
| Base class for Open edX Events exceptions. | ||
| """ | ||
|
|
||
| def __init__(self, message=""): | ||
| """ | ||
| Init method for OpenEdxEventException base class. | ||
| Arguments: | ||
| message (str): message describing why the exception was raised. | ||
| """ | ||
| super().__init__() | ||
| self.message = message | ||
|
|
||
| def __str__(self): | ||
| """ | ||
| Show string representation of OpenEdxEventException using its message. | ||
| """ | ||
| return self.message | ||
|
|
||
|
|
||
| class InstantiationError(OpenEdxEventException): | ||
| """ | ||
| Describes errors that occur while instantiating events. | ||
| This exception is raised when there's an error instantiating an Open edX | ||
| event, it can be that a required argument for the event definition is | ||
| missing. | ||
| """ | ||
|
|
||
| def __init__(self, event_type="", message=""): | ||
| """ | ||
| Init method for InstantiationError custom exception class. | ||
| Arguments: | ||
| event_type (str): name of the event raising the exception. | ||
| message (str): message describing why the exception was raised. | ||
| """ | ||
| super().__init__( | ||
| message="InstantiationError {event_type}: {message}".format( | ||
| event_type=event_type, message=message | ||
| ) | ||
| ) | ||
|
|
||
|
|
||
| class SenderValidationError(OpenEdxEventException): | ||
| """ | ||
| Describes errors that occur while validating arguments of send methods. | ||
| """ | ||
|
|
||
| def __init__(self, event_type="", message=""): | ||
| """ | ||
| Init method for SenderValidationError custom exception class. | ||
| Arguments: | ||
| event_type (str): name of the event raising the exception. | ||
| message (str): message describing why the exception was raised. | ||
| """ | ||
| super().__init__( | ||
| message="SenderValidationError {event_type}: {message}".format( | ||
| event_type=event_type, message=message | ||
| ) | ||
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,193 @@ | ||
| """This file contains all test for the tooling.py file. | ||
| Classes: | ||
| EventsToolingTest: Test events tooling. | ||
| """ | ||
| from unittest.mock import Mock, patch | ||
|
|
||
| import attr | ||
| import ddt | ||
| from django.test import TestCase, override_settings | ||
|
|
||
| from openedx_events.exceptions import InstantiationError, SenderValidationError | ||
| from openedx_events.tooling import OpenEdxPublicSignal | ||
|
|
||
|
|
||
| @ddt.ddt | ||
| class OpenEdxPublicSignalTest(TestCase): | ||
| """ | ||
| Test cases for Open edX events base class. | ||
| """ | ||
|
|
||
| def setUp(self): | ||
| """ | ||
| Setup common conditions for every test case. | ||
| """ | ||
| super().setUp() | ||
| self.event_type = "org.openedx.learning.session.login.completed.v1" | ||
| self.user_mock = Mock() | ||
| self.data_attr = { | ||
| "user": Mock, | ||
| } | ||
| self.public_signal = OpenEdxPublicSignal( | ||
| event_type=self.event_type, | ||
| data=self.data_attr, | ||
| ) | ||
|
|
||
| def test_string_representation(self): | ||
| """ | ||
| This methods checks the string representation for events base class. | ||
| Expected behavior: | ||
| The representation contains the event_type. | ||
| """ | ||
| self.assertIn(self.event_type, str(self.public_signal)) | ||
|
|
||
| @override_settings(SERVICE_VARIANT="lms") | ||
| @patch("openedx_events.data.openedx_events") | ||
| @patch("openedx_events.data.socket") | ||
| def test_get_signal_metadata(self, socket_mock, events_package_mock): | ||
| """ | ||
| This methods tests getting the generated metadata for an event. | ||
| Expected behavior: | ||
| Returns the metadata containing information about the event. | ||
| """ | ||
| events_package_mock.__version__ = "0.1.0" | ||
| socket_mock.gethostname.return_value = "edx.devstack.lms" | ||
| expected_metadata = { | ||
| "event_type": self.event_type, | ||
| "minorversion": 0, | ||
| "source": "openedx/lms/web", | ||
| "sourcehost": "edx.devstack.lms", | ||
| "sourcelib": [0, 1, 0], | ||
| } | ||
|
|
||
| metadata = self.public_signal.generate_signal_metadata() | ||
|
|
||
| self.assertDictContainsSubset(expected_metadata, attr.asdict(metadata)) | ||
|
|
||
| @ddt.data( | ||
| ("", {"user": Mock()}, "event_type"), | ||
| ("org.openedx.learning.session.login.completed.v1", None, "data"), | ||
| ) | ||
| @ddt.unpack | ||
| def test_event_instantiation_exception( | ||
| self, event_type, event_data, missing_argument | ||
| ): | ||
| """ | ||
| This method tests when an event is instantiated without event_type or | ||
| event data. | ||
| Expected behavior: | ||
| An InstantiationError exception is raised. | ||
| """ | ||
| exception_message = "InstantiationError {event_type}: Missing required argument '{missing_argument}'".format( | ||
| event_type=event_type, missing_argument=missing_argument | ||
| ) | ||
|
|
||
| with self.assertRaisesMessage(InstantiationError, exception_message): | ||
| OpenEdxPublicSignal(event_type=event_type, data=event_data) | ||
|
|
||
| @patch("openedx_events.tooling.OpenEdxPublicSignal.generate_signal_metadata") | ||
| @patch("openedx_events.tooling.Signal.send") | ||
| def test_send_event_successfully(self, send_mock, fake_metadata): | ||
| """ | ||
| This method tests the process of sending an event. | ||
| Expected behavior: | ||
| The event is sent as a django signal. | ||
| """ | ||
| expected_metadata = { | ||
| "some_data": "data", | ||
| "raise_exception": True, | ||
| } | ||
| fake_metadata.return_value = expected_metadata | ||
|
|
||
| self.public_signal.send_event(user=self.user_mock) | ||
|
|
||
| send_mock.assert_called_once_with( | ||
| sender=None, | ||
| user=self.user_mock, | ||
| metadata=expected_metadata, | ||
| ) | ||
|
|
||
| @patch("openedx_events.tooling.OpenEdxPublicSignal.generate_signal_metadata") | ||
| @patch("openedx_events.tooling.Signal.send_robust") | ||
| def test_send_robust_event_successfully(self, send_robust_mock, fake_metadata): | ||
| """ | ||
| This method tests the process of sending an event. | ||
| Expected behavior: | ||
| The event is sent as a django signal. | ||
| """ | ||
| expected_metadata = { | ||
| "some_data": "data", | ||
| "raise_exception": True, | ||
| } | ||
| fake_metadata.return_value = expected_metadata | ||
|
|
||
| self.public_signal.send_event(user=self.user_mock, send_robust=True) | ||
|
|
||
| send_robust_mock.assert_called_once_with( | ||
| sender=None, | ||
| user=self.user_mock, | ||
| metadata=expected_metadata, | ||
| ) | ||
|
|
||
| @ddt.data( | ||
| ( | ||
| {"student": Mock()}, | ||
| "SenderValidationError org.openedx.learning.session.login.completed.v1: " | ||
| "Missing required argument 'user'", | ||
| ), | ||
| ( | ||
| {"user": {"student": Mock()}}, | ||
| "SenderValidationError org.openedx.learning.session.login.completed.v1: " | ||
| "The argument 'user' is not instance of the Class Attribute 'type'", | ||
| ), | ||
| ( | ||
| {"student": Mock(), "user": Mock()}, | ||
| "SenderValidationError org.openedx.learning.session.login.completed.v1: " | ||
| "There's a mismatch between initialization data and send_event arguments", | ||
| ), | ||
| ) | ||
| @ddt.unpack | ||
| def test_invalid_sender(self, send_arguments, exception_message): | ||
| """ | ||
| This method tests sending an event with invalid setup on the sender | ||
| side. | ||
| Expected behavior: | ||
| A SenderValidationError exception is raised. | ||
| """ | ||
| with self.assertRaisesMessage(SenderValidationError, exception_message): | ||
| self.public_signal.send_event(**send_arguments) | ||
|
|
||
| def test_send_event_with_django(self): | ||
| """ | ||
| This method tests sending an event using the `send` built-in Django | ||
| method. | ||
| Expected behavior: | ||
| A warning is showed advicing to use Open edX events custom | ||
| send_signal method. | ||
| """ | ||
| message = "Please, use 'send_event' when triggering an Open edX event." | ||
|
|
||
| with self.assertWarns(Warning, msg=message): | ||
| self.public_signal.send(sender=Mock()) | ||
|
|
||
| def test_send_robust_event_with_django(self): | ||
| """ | ||
| This method tests sending an event using the `send` built-in Django | ||
| method. | ||
| Expected behavior: | ||
| A warning is showed advicing to use Open edX events custom | ||
| send_signal method. | ||
| """ | ||
| message = "Please, use 'send_event' with send_robust equals to True when triggering an Open edX event." | ||
|
|
||
| with self.assertWarns(Warning, msg=message): | ||
| self.public_signal.send_robust(sender=Mock()) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mariajgrimaldi: Hello Maria. It seems we went with a tuple instead of a string. I'm curious why we did that. I'm working on the event bus, which is going to need to send this metadata across the wire, so I may need to pass in a string and convert it back to a tuple, which will be a bit strange, but would make the type in this comment more correct.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@robrap Hello there, Robert. After some digging, I found this discussion I had with dave when this PR was open: #7 (comment) If you think there's room for improvement, let me know! -besides updating the docstring 😅 -
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you, Maria. I'm updating in this PR: https://github.com/openedx/openedx-events/pull/168/files#r1083076265