openedx-events 11.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. openedx_events/__init__.py +8 -0
  2. openedx_events/analytics/__init__.py +6 -0
  3. openedx_events/analytics/data.py +30 -0
  4. openedx_events/analytics/signals.py +25 -0
  5. openedx_events/apps.py +124 -0
  6. openedx_events/content_authoring/__init__.py +7 -0
  7. openedx_events/content_authoring/data.py +250 -0
  8. openedx_events/content_authoring/signals.py +372 -0
  9. openedx_events/data.py +142 -0
  10. openedx_events/enterprise/__init__.py +6 -0
  11. openedx_events/enterprise/data.py +241 -0
  12. openedx_events/enterprise/signals.py +132 -0
  13. openedx_events/event_bus/__init__.py +228 -0
  14. openedx_events/event_bus/avro/__init__.py +1 -0
  15. openedx_events/event_bus/avro/custom_serializers.py +227 -0
  16. openedx_events/event_bus/avro/deserializer.py +167 -0
  17. openedx_events/event_bus/avro/schema.py +244 -0
  18. openedx_events/event_bus/avro/serializer.py +141 -0
  19. openedx_events/event_bus/avro/types.py +14 -0
  20. openedx_events/exceptions.py +92 -0
  21. openedx_events/learning/__init__.py +6 -0
  22. openedx_events/learning/data.py +695 -0
  23. openedx_events/learning/signals.py +519 -0
  24. openedx_events/management/__init__.py +3 -0
  25. openedx_events/management/commands/__init__.py +5 -0
  26. openedx_events/management/commands/consume_events.py +74 -0
  27. openedx_events/management/commands/generate_avro_schemas.py +91 -0
  28. openedx_events/testing.py +140 -0
  29. openedx_events/tooling.py +340 -0
  30. openedx_events/utils.py +91 -0
  31. openedx_events-11.1.1.dist-info/METADATA +898 -0
  32. openedx_events-11.1.1.dist-info/RECORD +35 -0
  33. openedx_events-11.1.1.dist-info/WHEEL +5 -0
  34. openedx_events-11.1.1.dist-info/licenses/LICENSE.txt +202 -0
  35. openedx_events-11.1.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,8 @@
1
+ """
2
+ Package where Open edX Events and the necessary tooling are implemented.
3
+
4
+ These definitions are part of the Hooks Extension Framework, see OEP-50 for
5
+ more information about the project.
6
+ """
7
+
8
+ __version__ = "11.1.1"
@@ -0,0 +1,6 @@
1
+ """
2
+ Package where events related to the analytics subdomain are implemented.
3
+
4
+ The analytics subdomain corresponds to {Architecture Subdomain} defined in
5
+ the OEP-41.
6
+ """
@@ -0,0 +1,30 @@
1
+ """
2
+ Data attributes for events within the architecture subdomain ``analytics``.
3
+
4
+ These attributes follow the form of attr objects specified in OEP-49 data
5
+ pattern.
6
+ """
7
+
8
+ from datetime import datetime
9
+
10
+ import attr
11
+
12
+
13
+ @attr.s(frozen=True)
14
+ class TrackingLogData:
15
+ """
16
+ Data related to tracking events.
17
+
18
+ Attributes:
19
+ name (str): event name
20
+ timestamp (datetime): timestamp of the event
21
+ data (str): json string representation of a dictionary with extra data (optional), e.g.,
22
+ >>> {"course_id": "course-v1:edX+DemoX+Demo_Course"}
23
+ context (dict): json string representation of a dictionary of context data
24
+ defined in https://docs.openedx.org/en/latest/developers/references/internal_data_formats/index.html
25
+ """
26
+
27
+ name = attr.ib(type=str)
28
+ timestamp = attr.ib(type=datetime)
29
+ data = attr.ib(type=str)
30
+ context = attr.ib(type=str)
@@ -0,0 +1,25 @@
1
+ """
2
+ Standardized signals definitions for events within the architecture subdomain ``analytics``.
3
+
4
+ All signals defined in this module must follow the name and versioning
5
+ conventions specified in OEP-41.
6
+
7
+ They also must comply with the payload definition specified in
8
+ docs/decisions/0003-events-payload.rst
9
+ """
10
+
11
+ from openedx_events.analytics.data import TrackingLogData
12
+ from openedx_events.tooling import OpenEdxPublicSignal
13
+
14
+ # .. event_type: org.openedx.analytics.tracking.event.emitted.v1
15
+ # .. event_name: TRACKING_EVENT_EMITTED
16
+ # .. event_key_field: tracking_log.name
17
+ # .. event_description: Emitted when a tracking log event is emitted.
18
+ # .. event_data: TrackingLogData
19
+ # .. event_trigger_repository: openedx/event-tracking
20
+ TRACKING_EVENT_EMITTED = OpenEdxPublicSignal(
21
+ event_type="org.openedx.analytics.tracking.event.emitted.v1",
22
+ data={
23
+ "tracking_log": TrackingLogData,
24
+ }
25
+ )
openedx_events/apps.py ADDED
@@ -0,0 +1,124 @@
1
+ """
2
+ openedx_events Django application initialization.
3
+ """
4
+ import logging
5
+
6
+ from django.apps import AppConfig
7
+ from django.conf import settings
8
+
9
+ from openedx_events.event_bus import get_producer
10
+ from openedx_events.exceptions import ProducerConfigurationError
11
+ from openedx_events.tooling import SIGNAL_PROCESSED_FROM_EVENT_BUS, OpenEdxPublicSignal, load_all_signals
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ def general_signal_handler(sender, signal, **kwargs): # pylint: disable=unused-argument
17
+ """
18
+ Signal handler for producing events to configured event bus.
19
+ """
20
+ event_type_producer_configs = getattr(settings, "EVENT_BUS_PRODUCER_CONFIG", {}).get(signal.event_type, {})
21
+ # event_type_producer_configs should look something like
22
+ # {
23
+ # "topic_a": { "event_key_field": "my.key.field", "enabled": True },
24
+ # "topic_b": { "event_key_field": "my.key.field", "enabled": False }
25
+ # }"
26
+ if kwargs.get(SIGNAL_PROCESSED_FROM_EVENT_BUS) is True:
27
+ logger.debug(
28
+ "Declining to send signal to the Event Bus since that's "
29
+ f"where it was sent from: {signal.event_type} (preventing recursion)"
30
+ )
31
+ return
32
+
33
+ event_data = {key: kwargs.get(key) for key in signal.init_data}
34
+
35
+ for topic in event_type_producer_configs.keys():
36
+ if event_type_producer_configs[topic]["enabled"] is True:
37
+ get_producer().send(
38
+ signal=signal,
39
+ topic=topic,
40
+ event_key_field=event_type_producer_configs[topic]["event_key_field"],
41
+ event_data=event_data,
42
+ event_metadata=kwargs["metadata"],
43
+ )
44
+
45
+
46
+ class OpenedxEventsConfig(AppConfig):
47
+ """
48
+ Configuration for the openedx_events Django application.
49
+ """
50
+
51
+ name = "openedx_events"
52
+
53
+ def _get_validated_signal_config(self, event_type, configuration):
54
+ """
55
+ Validate signal configuration format.
56
+
57
+ Example expected signal configuration:
58
+ {
59
+ "topic_a": { "event_key_field": "my.key.field", "enabled": True },
60
+ "topic_b": { "event_key_field": "my.key.field", "enabled": False }
61
+ }
62
+
63
+ Raises:
64
+ ProducerConfigurationError: If configuration is not valid.
65
+ """
66
+ if not isinstance(configuration, dict):
67
+ raise ProducerConfigurationError(
68
+ event_type=event_type,
69
+ message="Configuration for event_types should be a dict"
70
+ )
71
+ try:
72
+ signal = OpenEdxPublicSignal.get_signal_by_type(event_type)
73
+ except KeyError as exc:
74
+ raise ProducerConfigurationError(message=f"No OpenEdxPublicSignal of type: '{event_type}'.") from exc
75
+ for _, topic_configuration in configuration.items():
76
+ if not isinstance(topic_configuration, dict):
77
+ raise ProducerConfigurationError(
78
+ event_type=event_type,
79
+ message="One of the configuration objects is not a dictionary"
80
+ )
81
+ expected_keys = {"event_key_field": str, "enabled": bool}
82
+ for expected_key, expected_type in expected_keys.items():
83
+ if expected_key not in topic_configuration.keys():
84
+ raise ProducerConfigurationError(
85
+ event_type=event_type,
86
+ message=f"One of the configuration object is missing '{expected_key}' key."
87
+ )
88
+ if not isinstance(topic_configuration[expected_key], expected_type):
89
+ raise ProducerConfigurationError(
90
+ event_type=event_type,
91
+ message=(f"Expected type: {expected_type} for '{expected_key}', "
92
+ f"found: {type(topic_configuration[expected_key])}")
93
+ )
94
+ return signal
95
+
96
+ def ready(self):
97
+ """
98
+ Read `EVENT_BUS_PRODUCER_CONFIG` setting and connects appropriate handlers to the events based on it.
99
+
100
+ Example expected configuration:
101
+ {
102
+ "org.openedx.content_authoring.xblock.deleted.v1" : {
103
+ "topic_a": { "event_key_field": "xblock_info.usage_key", "enabled": True },
104
+ "topic_b": { "event_key_field": "xblock_info.usage_key", "enabled": False }
105
+ },
106
+ "org.openedx.content_authoring.course.catalog_info.changed.v1" : {
107
+ "topic_c": {"event_key_field": "course_info.course_key", "enabled": True }
108
+ }
109
+ }
110
+
111
+ Raises:
112
+ ProducerConfigurationError: If `EVENT_BUS_PRODUCER_CONFIG` is not valid.
113
+ """
114
+ load_all_signals()
115
+ signals_config = getattr(settings, "EVENT_BUS_PRODUCER_CONFIG", {})
116
+ if not isinstance(signals_config, dict):
117
+ raise ProducerConfigurationError(
118
+ message=("Setting 'EVENT_BUS_PRODUCER_CONFIG' should be a dictionary with event_type as"
119
+ " key and list or tuple of config dictionaries as values")
120
+ )
121
+ for event_type, configurations in signals_config.items():
122
+ signal = self._get_validated_signal_config(event_type, configurations)
123
+ signal.connect(general_signal_handler)
124
+ return super().ready()
@@ -0,0 +1,7 @@
1
+ """
2
+ Package where events related to the ``content_authoring`` subdomain are implemented.
3
+ """
4
+ # Comment broken up to deal with line-length issues with the long URL
5
+
6
+ # The ``content_authoring`` subdomain corresponds to {Architecture Subdomain} defined in OEP-41.
7
+ # https://open-edx-proposals.readthedocs.io/en/latest/architectural-decisions/oep-0041-arch-async-server-event-messaging.html
@@ -0,0 +1,250 @@
1
+ """
2
+ Data attributes for events within the architecture subdomain ``content_authoring``.
3
+
4
+ These attributes follow the form of attr objects specified in OEP-49 data
5
+ pattern.
6
+
7
+ The attributes for the events come from the CourseDetailView in the LMS, with some unused fields removed
8
+ (see deprecation proposal at https://github.com/openedx/public-engineering/issues/160)
9
+ """
10
+ from datetime import datetime
11
+ from typing import BinaryIO, List
12
+
13
+ import attr
14
+ from opaque_keys.edx.keys import CourseKey, UsageKey
15
+ from opaque_keys.edx.locator import (
16
+ LibraryCollectionLocator,
17
+ LibraryContainerLocator,
18
+ LibraryLocatorV2,
19
+ LibraryUsageLocatorV2,
20
+ )
21
+
22
+
23
+ @attr.s(frozen=True)
24
+ class CourseData:
25
+ """
26
+ Data related to a course object.
27
+
28
+ Attributes:
29
+ course_key (CourseKey): identifier of the Course object.
30
+ """
31
+
32
+ course_key = attr.ib(type=CourseKey)
33
+
34
+
35
+ @attr.s(frozen=True)
36
+ class CourseScheduleData:
37
+ """
38
+ Data related to a course schedule.
39
+
40
+ Attributes:
41
+ start (datetime): course start date.
42
+ pacing (str): 'instructor' or 'self'.
43
+ end (datetime): course end date (optional).
44
+ enrollment_start (datetime): start of course enrollment (optional).
45
+ enrollment_end (datetime): end of course enrollment (optional).
46
+ """
47
+
48
+ start = attr.ib(type=datetime)
49
+ pacing = attr.ib(type=str)
50
+ end = attr.ib(type=datetime, default=None)
51
+ enrollment_start = attr.ib(type=datetime, default=None)
52
+ enrollment_end = attr.ib(type=datetime, default=None)
53
+
54
+
55
+ @attr.s(frozen=True)
56
+ class CourseCatalogData:
57
+ """
58
+ Data related to a course catalog entry.
59
+
60
+ Attributes:
61
+ course_key (CourseKey): identifier of the Course object.
62
+ name (str): course name.
63
+ schedule_data (CourseScheduleData): scheduling information for the course.
64
+ hidden (bool): whether the course is hidden from search (optional).
65
+ invitation_only (bool): whether the course requires an invitation to enroll.
66
+ """
67
+
68
+ # basic identifiers
69
+ course_key = attr.ib(type=CourseKey)
70
+ name = attr.ib(type=str)
71
+
72
+ # additional marketing information
73
+ schedule_data = attr.ib(type=CourseScheduleData)
74
+ hidden = attr.ib(type=bool, default=False)
75
+ invitation_only = attr.ib(type=bool, default=False)
76
+
77
+
78
+ @attr.s(frozen=True)
79
+ class XBlockData:
80
+ """
81
+ Data related to an XBlock object.
82
+
83
+ Attributes:
84
+ usage_key (UsageKey): identifier of the XBlock object.
85
+ block_type (str): type of block.
86
+ version (UsageKey): identifier of the XBlock object with branch and version data (optional). This
87
+ could be used to get the exact version of the XBlock object.
88
+ """
89
+
90
+ usage_key = attr.ib(type=UsageKey)
91
+ block_type = attr.ib(type=str)
92
+ version = attr.ib(type=UsageKey, default=None, kw_only=True)
93
+
94
+
95
+ @attr.s(frozen=True)
96
+ class DuplicatedXBlockData(XBlockData):
97
+ """
98
+ Data related to an XBlock object that has been duplicated.
99
+
100
+ This class extends XBlockData to include source_usage_key.
101
+
102
+ Attributes:
103
+ source_usage_key (UsageKey): identifier of the source XBlock object.
104
+ """
105
+
106
+ source_usage_key = attr.ib(type=UsageKey)
107
+
108
+
109
+ @attr.s(frozen=True)
110
+ class CertificateSignatoryData:
111
+ """
112
+ Data related to a certificate signatory. Subset of CertificateSignatory object from the LMS.
113
+
114
+ Attributes:
115
+ image (BinaryIO): certificate signature image. Take care that the image field is BinaryIO.
116
+ name (str): name of signatory.
117
+ organization (str): organization that signatory belongs to.
118
+ title (str): signatory title.
119
+ """
120
+
121
+ # Note: Please take care that the image field is BinaryIO, which means
122
+ # that a file can be passed as an array of bytes. Watch the size of this file.
123
+ # It can potentially be large, making it difficult to pass this data structure through the Event Bus
124
+ # (CloudEvent messages, which should be 64K or less) and store it on disk space.
125
+ # We suggest referring to MAX_ASSET_UPLOAD_FILE_SIZE_IN_MB, i.e. restriction in the Studio for such cases.
126
+ image = attr.ib(type=BinaryIO)
127
+ # end Note
128
+ name = attr.ib(type=str)
129
+ organization = attr.ib(type=str)
130
+ title = attr.ib(type=str)
131
+
132
+
133
+ @attr.s(frozen=True)
134
+ class CertificateConfigData:
135
+ """
136
+ Data related to a certificate configuration. Subset of CertificateConfig object from the LMS.
137
+
138
+ Attributes:
139
+ certificate_type (str): certificate type. Possible types are certificate relevant course modes:
140
+ - credit,
141
+ - verified,
142
+ - professional,
143
+ - no-id-professional,
144
+ - executive-education,
145
+ - paid-executive-education,
146
+ - paid-bootcamp,
147
+ - masters.
148
+ course_key (CourseKey): identifier of the Course object.
149
+ title (str): certificate title.
150
+ signatories (List[CertificateSignatoryData]): contains a collection of signatures
151
+ that belong to the certificate configuration.
152
+ is_active (bool): indicates whether the certifivate configuration is active.
153
+ """
154
+
155
+ certificate_type = attr.ib(type=str)
156
+ course_key = attr.ib(type=CourseKey)
157
+ title = attr.ib(type=str)
158
+ signatories = attr.ib(type=List[CertificateSignatoryData], factory=list)
159
+ is_active = attr.ib(type=bool, default=False)
160
+
161
+
162
+ @attr.s(frozen=True)
163
+ class ContentLibraryData:
164
+ """
165
+ Data related to a content library that has changed.
166
+
167
+ Attributes:
168
+ library_key (LibraryLocatorV2): a key that represents a v2 content library.
169
+ update_blocks (bool): DEPRECATED flag to indicate whether the content library blocks indexes should be updated.
170
+ Now we send individual events for each updated item instead.
171
+ """
172
+
173
+ library_key = attr.ib(type=LibraryLocatorV2)
174
+ update_blocks = attr.ib(type=bool, default=False)
175
+
176
+
177
+ @attr.s(frozen=True)
178
+ class LibraryBlockData:
179
+ """
180
+ Data related to a library block that has changed.
181
+
182
+ Attributes:
183
+ library_key (LibraryLocatorV2): a key that represents a v2 content library.
184
+ usage_key (LibraryUsageLocatorV2): a key that represents a XBlock in a Blockstore-based content library.
185
+ """
186
+
187
+ library_key = attr.ib(type=LibraryLocatorV2)
188
+ usage_key = attr.ib(type=LibraryUsageLocatorV2)
189
+
190
+
191
+ @attr.s(frozen=True)
192
+ class ContentObjectData:
193
+ """
194
+ Data related to a content object.
195
+
196
+ Attributes:
197
+ object_id (str): identifier of the Content object. This represents the id of the course or library block
198
+ as a string. For example:
199
+ >>> block-v1:SampleTaxonomyOrg2+STC1+2023_1+type@vertical+block@f8de78f0897049ce997777a3a31b6ea0
200
+ """
201
+
202
+ object_id = attr.ib(type=str)
203
+
204
+
205
+ @attr.s(frozen=True)
206
+ class ContentObjectChangedData(ContentObjectData):
207
+ """
208
+ Data related to a content object that has changed.
209
+
210
+ Attributes:
211
+ object_id (str): identifier of the Content object. This represents the id of the course or library block
212
+ as a string. For example:
213
+ >>> block-v1:SampleTaxonomyOrg2+STC1+2023_1+type@vertical+block@f8de78f0897049ce997777a3a31b6ea0
214
+ changes: list of changes made to this ContentObject, e.g. "tags", "collections". If list is empty,
215
+ assume everything has changed.
216
+ """
217
+
218
+ changes = attr.ib(type=List[str], factory=list)
219
+
220
+
221
+ @attr.s(frozen=True)
222
+ class LibraryCollectionData:
223
+ """
224
+ Data related to a library collection that has changed.
225
+
226
+ Attributes:
227
+ collection_key (LibraryCollectionLocator): identifies the collection within the library's learning package
228
+ background (bool): **DEPRECATED** field. Indicated the sender doesn't want to wait for handler to finish
229
+ execution. Now instead we recommend that simple handlers are synchronous and the _sender_ of the event should
230
+ send the event(s) from an async celery task if it is expected to result in a lot of handlers being called.
231
+ """
232
+
233
+ collection_key = attr.ib(type=LibraryCollectionLocator)
234
+ background = attr.ib(type=bool, default=False)
235
+
236
+
237
+ @attr.s(frozen=True)
238
+ class LibraryContainerData:
239
+ """
240
+ Data related to a library container that has changed.
241
+
242
+ Attributes:
243
+ container_key (LibraryContainerLocator): identifies the container (e.g. unit, section)
244
+ background (bool): **DEPRECATED** field. Indicated the sender doesn't want to wait for handler to finish
245
+ execution. Now instead we recommend that simple handlers are synchronous and the _sender_ of the event should
246
+ send the event(s) from an async celery task if it is expected to result in a lot of handlers being called.
247
+ """
248
+
249
+ container_key = attr.ib(type=LibraryContainerLocator)
250
+ background = attr.ib(type=bool, default=False)