Skip to content
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

Cag/add support for dicts #232

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ Change Log

Unreleased
----------
Added
~~~~~
* Added new event TRACKING_EVENT_EMITTED.

[8.0.1] - 2023-05-16
--------------------
Expand Down
6 changes: 6 additions & 0 deletions openedx_events/analytics/__init__.py
Original file line number Diff line number Diff line change
@@ -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.
"""
33 changes: 33 additions & 0 deletions openedx_events/analytics/data.py
Original file line number Diff line number Diff line change
@@ -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)
23 changes: 23 additions & 0 deletions openedx_events/analytics/signals.py
Original file line number Diff line number Diff line change
@@ -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,
}
)
24 changes: 23 additions & 1 deletion openedx_events/event_bus/avro/custom_serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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]
11 changes: 11 additions & 0 deletions openedx_events/event_bus/avro/deserializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__:
Expand Down
15 changes: 15 additions & 0 deletions openedx_events/event_bus/avro/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions openedx_events/event_bus/avro/serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down