diff --git a/CHANGELOG.rst b/CHANGELOG.rst index cec6dffa..7c19a366 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -13,6 +13,9 @@ Change Log Unreleased ---------- +Added +~~~~~ +* Added new event TRACKING_EVENT_EMITTED. [8.0.1] - 2023-05-16 -------------------- diff --git a/openedx_events/analytics/__init__.py b/openedx_events/analytics/__init__.py new file mode 100644 index 00000000..5410a5f4 --- /dev/null +++ b/openedx_events/analytics/__init__.py @@ -0,0 +1,6 @@ +""" +Package where events related to the analytics subdomain are implemented. + +The analytics subdomain corresponds to {Architecture Subdomain} defined in +the OEP-41. +""" diff --git a/openedx_events/analytics/data.py b/openedx_events/analytics/data.py new file mode 100644 index 00000000..9d668a17 --- /dev/null +++ b/openedx_events/analytics/data.py @@ -0,0 +1,33 @@ +""" +Data attributes for events within the architecture subdomain ``analytics``. + +These attributes follow the form of attr objects specified in OEP-49 data +pattern. + +The attributes for the events come from the CourseDetailView in the LMS, with some unused fields removed +(see deprecation proposal at https://github.com/openedx/public-engineering/issues/160) +""" + +from datetime import datetime + +import attr + +from typing import Dict + + +@attr.s(frozen=True) +class TrackingLogData: + """ + Data describing tracking log data. + + Arguments: + name (str): name + timestamp (datetime): course start date + data (dict): dictionary of extra data (optional), e.g. {"course_id": "course-v1:edX+DemoX+Demo_Course"} + context (dict): dictionary of context data, defined in https://edx.readthedocs.io/projects/devdata/en/latest/internal_data_formats/tracking_logs/common_fields.html + """ + + name = attr.ib(type=str) + timestamp = attr.ib(type=datetime) + data = attr.ib(type=str, default='') + context = attr.ib(type=dict, factory=dict) diff --git a/openedx_events/analytics/signals.py b/openedx_events/analytics/signals.py new file mode 100644 index 00000000..d1badd02 --- /dev/null +++ b/openedx_events/analytics/signals.py @@ -0,0 +1,23 @@ +""" +Standardized signals definitions for events within the architecture subdomain ``analytics``. + +All signals defined in this module must follow the name and versioning +conventions specified in OEP-41. + +They also must comply with the payload definition specified in +docs/decisions/0003-events-payload.rst +""" + +from openedx_events.analytics.data import TrackingLogData +from openedx_events.tooling import OpenEdxPublicSignal + +# .. event_type: org.openedx.analytics.event_tracking.emitted.v1 +# .. event_name: TRACKING_EVENT_EMITTED +# .. event_description: emitted when a tracking log is created. +# .. event_data: TrackingLogData +TRACKING_EVENT_EMITTED = OpenEdxPublicSignal( + event_type="org.openedx.analytics.event_tracking.emitted.v1", + data={ + "tracking_log": TrackingLogData, + } +) diff --git a/openedx_events/event_bus/avro/custom_serializers.py b/openedx_events/event_bus/avro/custom_serializers.py index 2b18905f..c9d381dd 100644 --- a/openedx_events/event_bus/avro/custom_serializers.py +++ b/openedx_events/event_bus/avro/custom_serializers.py @@ -10,6 +10,8 @@ from openedx_events.event_bus.avro.types import PYTHON_TYPE_TO_AVRO_MAPPING +import json + class BaseCustomTypeAvroSerializer(ABC): """ Used by openedx_events.avro_utilities class to serialize/deserialize custom types. @@ -71,6 +73,26 @@ def deserialize(data: str): return datetime.fromisoformat(data) +class DictionaryAvroSerializer(BaseCustomTypeAvroSerializer): + """ + CustomTypeAvroSerializer for dictionary class. + """ + + cls = dict + field_type = PYTHON_TYPE_TO_AVRO_MAPPING[dict] + + @staticmethod + def serialize(obj) -> str: + """Serialize obj into str.""" + return obj + + @staticmethod + def deserialize(data: str): + """Deserialize dict into obj.""" + return json.loads(data) + + + class UsageKeyAvroSerializer(BaseCustomTypeAvroSerializer): """ CustomTypeAvroSerializer for UsageKey class. @@ -90,4 +112,4 @@ def deserialize(data: str): return UsageKey.from_string(data) -DEFAULT_CUSTOM_SERIALIZERS = [CourseKeyAvroSerializer, DatetimeAvroSerializer, UsageKeyAvroSerializer] +DEFAULT_CUSTOM_SERIALIZERS = [CourseKeyAvroSerializer, DatetimeAvroSerializer, UsageKeyAvroSerializer, DictionaryAvroSerializer] diff --git a/openedx_events/event_bus/avro/deserializer.py b/openedx_events/event_bus/avro/deserializer.py index 442a9780..16fe21b5 100644 --- a/openedx_events/event_bus/avro/deserializer.py +++ b/openedx_events/event_bus/avro/deserializer.py @@ -51,6 +51,17 @@ def _deserialized_avro_record_dict_to_object(data: dict, data_type, deserializer # check whether list items type is in basic types. if arg_data_type[0] in SIMPLE_PYTHON_TYPE_TO_AVRO_MAPPING: return data + elif data_type_origin == dict: + # returns types of dict contents + # if data_type == Dict[str, int], arg_data_type = (str, int) + arg_data_type = get_args(data_type) + if not arg_data_type: + raise TypeError( + "Dict without annotation type is not supported. The argument should be a type, for eg., Dict[str, int]" + ) + # check whether dict items type is in basic types. + if arg_data_type[1] in SIMPLE_PYTHON_TYPE_TO_AVRO_MAPPING: + return data elif hasattr(data_type, "__attrs_attrs__"): transformed = {} for attribute in data_type.__attrs_attrs__: diff --git a/openedx_events/event_bus/avro/schema.py b/openedx_events/event_bus/avro/schema.py index afb03086..e16ee405 100644 --- a/openedx_events/event_bus/avro/schema.py +++ b/openedx_events/event_bus/avro/schema.py @@ -83,6 +83,21 @@ def _create_avro_field_definition(data_key, data_type, previously_seen_types, f" {set(SIMPLE_PYTHON_TYPE_TO_AVRO_MAPPING.keys())}" ) field["type"] = {"type": PYTHON_TYPE_TO_AVRO_MAPPING[data_type_origin], "items": avro_type} + elif data_type_origin == dict: + # returns types of dict contents + # if data_type == Dict[str, int], arg_data_type = (str, int) + arg_data_type = get_args(data_type) + if not arg_data_type: + raise TypeError( + "Dict without annotation type is not supported. The argument should be a type, for eg., Dict[str, int]" + ) + avro_type = SIMPLE_PYTHON_TYPE_TO_AVRO_MAPPING.get(arg_data_type[1]) + if avro_type is None: + raise TypeError( + "Only following types are supported for dict arguments:" + f" {set(SIMPLE_PYTHON_TYPE_TO_AVRO_MAPPING.keys())}" + ) + field["type"] = {"type": PYTHON_TYPE_TO_AVRO_MAPPING[data_type_origin], "values": avro_type} # Case 3: data_type is an attrs class elif hasattr(data_type, "__attrs_attrs__"): # Inner Attrs Class diff --git a/openedx_events/event_bus/avro/serializer.py b/openedx_events/event_bus/avro/serializer.py index bd57183c..12e09720 100644 --- a/openedx_events/event_bus/avro/serializer.py +++ b/openedx_events/event_bus/avro/serializer.py @@ -69,6 +69,7 @@ def _event_data_to_avro_record_dict(event_data, serializers=None): def value_to_dict(value): # Case 1: Value is an instance of an attrs-decorated class if hasattr(value, "__attrs_attrs__"): + print("\n\n MY VALUE IN VALUE TO DICT", value, "\n\n") return attr.asdict(value, value_serializer=_get_non_attrs_serializer(serializers)) return _get_non_attrs_serializer(serializers)(None, None, value)