Skip to content

Commit

Permalink
refactor: applies functional programming principles to event transfor…
Browse files Browse the repository at this point in the history
…mation

This reduces the side-effects of event transformation, which will allow
us to re-use base event transformation methods when generating multiple
events from a single source event.

* Removes the BaseTransformerMixin.transformed_event instance variable
  in favor of passing an event through to base_transform() to be
  modified and returned.
* Adds BaseTransformerMixin.get_object() so that caliper events don't
  need to reference self.transformed_event when updating the object data.
* Adds BaseTransformerMixin.get_extensions() so that caliper events can
  don't have to hack in their transformerVersion during
  BaseTransformerMixin.transform()
  • Loading branch information
pomegranited committed Jul 31, 2023
1 parent 8415ebf commit 869a9ce
Show file tree
Hide file tree
Showing 7 changed files with 86 additions and 46 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,11 @@ def get_object(self):

# TODO: replace with anonymous enrollment id?
course_root_url = self.get_object_iri('course', self.get_data('data.course_id', True))
caliper_object = {
caliper_object = super().get_object()
caliper_object.update({
'id': course_root_url,
'type': 'CourseOffering',
'name': course['display_name'],
'extensions': {'mode': self.get_data('data.mode')} if self.get_data('data.mode') is not None else None,
}
})
return caliper_object
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def get_object(self):
dict
"""
self.backend_name = 'caliper'
caliper_object = self.transformed_event['object']
caliper_object = super().get_object()

data = self.get_data('data')
extensions = {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ def get_object(self):
else:
iri_url = object_id

caliper_object = self.transformed_event['object']
caliper_object = super().get_object()
caliper_object.update({
'id': self.get_object_iri('xblock', iri_url),
'type': OBJECT_TYPE_MAP.get(key, 'Attempt'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ def get_object(self):
dict
"""
self.backend_name = 'caliper'
caliper_object = self.transformed_event['object']
caliper_object = super().get_object()
data = self.get_data('data')
course_id = self.get_data('context.course_id', True)
video_id = self.get_data('data.id', True)
Expand Down Expand Up @@ -176,10 +176,12 @@ class VideoSpeedChangedTransformer(BaseVideoTransformer):
"""
Transform the event fired when a video's speed is changed.
"""
additional_fields = ('target', 'extensions',)
additional_fields = ('target',)

def get_extensions(self):
return {
extensions = super().get_extensions()
extensions.update({
'oldSpeed': self.get_data('old_speed'),
'newSpeed': self.get_data('new_speed'),
}
})
return extensions
67 changes: 45 additions & 22 deletions event_routing_backends/processors/caliper/transformer.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,58 +19,81 @@ class CaliperTransformer(BaseTransformerMixin):
required_fields = (
'type',
'object',
'action'
'action',
'extensions',
)

def base_transform(self):
def base_transform(self, transformed_event):
"""
Transform common Caliper fields.
"""
self._add_generic_fields()
self._add_actor_info()
self._add_session_info()
transformed_event = super().base_transform(transformed_event)
self._add_generic_fields(transformed_event)
self._add_actor_info(transformed_event)
self._add_session_info(transformed_event)
return transformed_event

def _add_generic_fields(self):
def _add_generic_fields(self, transformed_event):
"""
Add all of the generic fields to the transformed_event object.
"""
self.transformed_event.update({
transformed_event.update({
'@context': CALIPER_EVENT_CONTEXT,
'id': uuid.uuid4().urn,
'eventTime': convert_datetime_to_iso(self.get_data('timestamp', True)),
'extensions': {}
})
self.transformed_event['object'] = {}
course_id = self.get_data('context.course_id')
if course_id is not None:
extensions = {"isPartOf": {}}
extensions['isPartOf']['id'] = self.get_object_iri('course', course_id)
extensions['isPartOf']['type'] = 'CourseOffering'
self.transformed_event['object']['extensions'] = {}
self.transformed_event['object']['extensions'].update(extensions)

def _add_actor_info(self):
def _add_actor_info(self, transformed_event):
"""
Add all generic information related to `actor`.
Add all generic information related to `actor` to the transformed_event.
"""
self.transformed_event['actor'] = {
transformed_event['actor'] = {
'id': self.get_object_iri(
'user',
get_anonymous_user_id(self.extract_username_or_userid(), 'CALIPER'),
),
'type': 'Person'
}

def _add_session_info(self):
def _add_session_info(self, transformed_event):
"""
Add session info related to the event
Add session info related to the transformed_event.
"""
sessionid = self.extract_sessionid()
if sessionid:
self.transformed_event['session'] = {
transformed_event['session'] = {
'id': self.get_object_iri(
'sessions',
sessionid,
),
'type': 'Session'
}

def get_object(self):
"""
Return object for the event.
Returns:
dict
"""
caliper_object = super().get_object()
course_id = self.get_data('context.course_id')
if course_id is not None:
extensions = {"isPartOf": {}}
extensions['isPartOf']['id'] = self.get_object_iri('course', course_id)
extensions['isPartOf']['type'] = 'CourseOffering'
caliper_object['extensions'] = {}
caliper_object['extensions'].update(extensions)

return caliper_object

def get_extensions(self):
"""
Return extensions for the event.
Returns:
dict
"""
return {
'transformerVersion': self.transformer_version,
}
32 changes: 20 additions & 12 deletions event_routing_backends/processors/mixins/base_transformer.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ def __init__(self, event):
event (dict) : event to be transformed
"""
self.event = event.copy()
self.transformed_event = {}

@staticmethod
def find_nested(source_dict, key):
Expand Down Expand Up @@ -65,14 +64,17 @@ def _find_nested(event_dict):

return _find_nested(source_dict)

def base_transform(self):
def base_transform(self, transformed_event):
"""
Transform the fields that are common for all events.
Other classes can override this method to add common transformation
code for events.
Returns:
ANY
"""
return
return transformed_event or {}

@property
def transformer_version(self):
Expand All @@ -92,31 +94,28 @@ def transform(self):
Transform the edX event.
Returns:
dict
ANY
"""
self.base_transform()
transformed_event = self.base_transform({})

transforming_fields = self.required_fields + self.additional_fields
for key in transforming_fields:
if hasattr(self, key):
value = getattr(self, key)
self.transformed_event[key] = value
transformed_event[key] = value
elif hasattr(self, f'get_{key}'):
value = getattr(self, f'get_{key}')()
self.transformed_event[key] = value
transformed_event[key] = value
else:
raise ValueError(
'Cannot find value for "{}" in transformer {} for the edx event "{}"'.format(
key, self.__class__.__name__, self.get_data('name', True)
)
)

if self.backend_name == 'caliper':
self.transformed_event['extensions']['transformerVersion'] = self.transformer_version

self.transformed_event = self.del_none(self.transformed_event)
transformed_event = self.del_none(transformed_event)

return self.transformed_event
return transformed_event

def extract_username_or_userid(self):
"""
Expand Down Expand Up @@ -218,3 +217,12 @@ def get_object_iri(self, object_type, object_id):
object_type=object_type,
object_id=object_id
)

def get_object(self):
"""
Return object for the event.
Returns:
dict
"""
return {}
14 changes: 10 additions & 4 deletions event_routing_backends/processors/xapi/transformer.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,22 +46,28 @@ def transform(self):
transformed_props["version"] = constants.XAPI_SPECIFICATION_VERSION
return Statement(**transformed_props)

def base_transform(self):
def base_transform(self, transformed_event):
"""
Transform the fields that are common for all events.
"""
transformed_event = super().base_transform(transformed_event)
actor = self.get_actor()
event_timestamp = self.get_timestamp()
self.transformed_event = {
transformed_event.update({
'actor': actor,
'context': self.get_context(),
'timestamp': event_timestamp,
}
})
transformed_event['actor'] = self.get_actor()
transformed_event['context'] = self.get_context()
transformed_event['timestamp'] = self.get_timestamp()

# Warning! changing anything in these 2 lines or changing the "base_uuid" can invalidate
# billions of rows in the database. Please have a community discussion first before introducing
# any change in generation of UUID.
uuid_str = f'{actor.to_json()}-{event_timestamp}'
self.transformed_event['id'] = get_uuid5(self.verb.to_json(), uuid_str) # pylint: disable=no-member
transformed_event['id'] = get_uuid5(self.verb.to_json(), uuid_str) # pylint: disable=no-member
return transformed_event

def get_actor(self):
"""
Expand Down

0 comments on commit 869a9ce

Please sign in to comment.