Skip to content
Draft
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
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ include LICENSE.txt
include README.rst
include requirements/base.in
recursive-include openedx_events *.py
include openedx_events/py.typed
include requirements/constraints.txt
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
.PHONY: clean clean_tox compile_translations coverage diff_cover docs dummy_translations \
extract_translations fake_translations help pull_translations push_translations \
extract_translations fake_translations help mypy pull_translations push_translations \
quality requirements selfcheck test test-all upgrade validate install_transifex_client

.DEFAULT_GOAL := help
Expand Down Expand Up @@ -61,6 +61,9 @@ upgrade: ## update the requirements/*.txt files with the latest packages satisfy
quality: ## check coding style with pycodestyle and pylint
tox -e quality

mypy: ## run mypy static type checks
mypy openedx_events

piptools: ## install pinned version of pip-compile and pip-sync
pip install -r requirements/pip.txt
pip install -r requirements/pip-tools.txt
Expand Down
109 changes: 109 additions & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
[mypy]
python_version = 3.12
; Start permissive at the project level — tighten per-module as each phase lands
warn_return_any = True
warn_unused_ignores = True
warn_unused_configs = True
warn_redundant_casts = True
disallow_untyped_defs = False
disallow_incomplete_defs = False
check_untyped_defs = True
no_implicit_optional = True
strict_equality = True

plugins =
mypy_django_plugin.main

[mypy.plugins.django-stubs]
django_settings_module = test_utils.test_settings

; -----------------------------------------------------------------------
; Third-party packages without type stubs — silence missing-import errors
; -----------------------------------------------------------------------

[mypy-fastavro.*]
ignore_missing_imports = True

[mypy-ccx_keys.*]
ignore_missing_imports = True

[mypy-opaque_keys.*]
ignore_missing_imports = True
Comment on lines +30 to +31
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

opaque-keys has type info now, so we shouldn't need this.


[mypy-edx_django_utils.*]
ignore_missing_imports = True

[mypy-ddt]
ignore_missing_imports = True


; -----------------------------------------------------------------------
; Per-module strictness overrides
; Flip disallow_untyped_defs = True module-by-module as annotations land.
; -----------------------------------------------------------------------

[mypy-openedx_events.exceptions]
disallow_untyped_defs = True

[mypy-openedx_events.utils]
disallow_untyped_defs = True

[mypy-openedx_events.data]
disallow_untyped_defs = True

[mypy-openedx_events.analytics.data]
disallow_untyped_defs = True

[mypy-openedx_events.enterprise.data]
disallow_untyped_defs = True

[mypy-openedx_events.content_authoring.data]
disallow_untyped_defs = True

[mypy-openedx_events.learning.data]
disallow_untyped_defs = True

[mypy-openedx_events.event_bus.avro.types]
disallow_untyped_defs = True

[mypy-openedx_events.event_bus.avro.custom_serializers]
disallow_untyped_defs = True

[mypy-openedx_events.event_bus.avro.schema]
disallow_untyped_defs = True

[mypy-openedx_events.event_bus.avro.serializer]
disallow_untyped_defs = True

[mypy-openedx_events.event_bus.avro.deserializer]
disallow_untyped_defs = True

[mypy-openedx_events.tooling]
disallow_untyped_defs = True

[mypy-openedx_events.event_bus]
disallow_untyped_defs = True

[mypy-openedx_events.apps]
disallow_untyped_defs = True

[mypy-openedx_events.testing]
disallow_untyped_defs = True

[mypy-openedx_events.management.commands.consume_events]
disallow_untyped_defs = True

[mypy-openedx_events.management.commands.generate_avro_schemas]
disallow_untyped_defs = True

[mypy-openedx_events.analytics.signals]
disallow_untyped_defs = False

[mypy-openedx_events.enterprise.signals]
disallow_untyped_defs = False

[mypy-openedx_events.content_authoring.signals]
disallow_untyped_defs = False

[mypy-openedx_events.learning.signals]
disallow_untyped_defs = False
12 changes: 6 additions & 6 deletions openedx_events/analytics/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@

from datetime import datetime

import attr
import attrs


@attr.s(frozen=True)
@attrs.define(frozen=True)
class TrackingLogData:
"""
Data related to tracking events.
Expand All @@ -24,7 +24,7 @@ class TrackingLogData:
defined in https://docs.openedx.org/en/latest/developers/references/internal_data_formats/index.html
"""

name = attr.ib(type=str)
timestamp = attr.ib(type=datetime)
data = attr.ib(type=str)
context = attr.ib(type=str)
name: str
timestamp: datetime
data: str
context: str
38 changes: 26 additions & 12 deletions openedx_events/apps.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
"""
openedx_events Django application initialization.
"""

import logging
from typing import Any

from django.apps import AppConfig
from django.conf import settings
Expand All @@ -13,11 +15,15 @@
logger = logging.getLogger(__name__)


def general_signal_handler(sender, signal, **kwargs): # pylint: disable=unused-argument
def general_signal_handler( # pylint: disable=unused-argument
sender: Any, signal: OpenEdxPublicSignal, **kwargs: Any
) -> None:
"""
Signal handler for producing events to configured event bus.
"""
event_type_producer_configs = getattr(settings, "EVENT_BUS_PRODUCER_CONFIG", {}).get(signal.event_type, {})
event_type_producer_configs = getattr(
settings, "EVENT_BUS_PRODUCER_CONFIG", {}
).get(signal.event_type, {})
# event_type_producer_configs should look something like
# {
# "topic_a": { "event_key_field": "my.key.field", "enabled": True },
Expand Down Expand Up @@ -50,7 +56,9 @@ class OpenedxEventsConfig(AppConfig):

name = "openedx_events"

def _get_validated_signal_config(self, event_type, configuration):
def _get_validated_signal_config(
self, event_type: str, configuration: Any
) -> OpenEdxPublicSignal:
"""
Validate signal configuration format.

Expand All @@ -66,34 +74,38 @@ def _get_validated_signal_config(self, event_type, configuration):
if not isinstance(configuration, dict):
raise ProducerConfigurationError(
event_type=event_type,
message="Configuration for event_types should be a dict"
message="Configuration for event_types should be a dict",
)
try:
signal = OpenEdxPublicSignal.get_signal_by_type(event_type)
except KeyError as exc:
raise ProducerConfigurationError(message=f"No OpenEdxPublicSignal of type: '{event_type}'.") from exc
raise ProducerConfigurationError(
message=f"No OpenEdxPublicSignal of type: '{event_type}'."
) from exc
for _, topic_configuration in configuration.items():
if not isinstance(topic_configuration, dict):
raise ProducerConfigurationError(
event_type=event_type,
message="One of the configuration objects is not a dictionary"
message="One of the configuration objects is not a dictionary",
)
expected_keys = {"event_key_field": str, "enabled": bool}
for expected_key, expected_type in expected_keys.items():
if expected_key not in topic_configuration.keys():
raise ProducerConfigurationError(
event_type=event_type,
message=f"One of the configuration object is missing '{expected_key}' key."
message=f"One of the configuration object is missing '{expected_key}' key.",
)
if not isinstance(topic_configuration[expected_key], expected_type):
raise ProducerConfigurationError(
event_type=event_type,
message=(f"Expected type: {expected_type} for '{expected_key}', "
f"found: {type(topic_configuration[expected_key])}")
message=(
f"Expected type: {expected_type} for '{expected_key}', "
f"found: {type(topic_configuration[expected_key])}"
),
)
return signal

def ready(self):
def ready(self) -> None:
"""
Read `EVENT_BUS_PRODUCER_CONFIG` setting and connects appropriate handlers to the events based on it.

Expand All @@ -115,8 +127,10 @@ def ready(self):
signals_config = getattr(settings, "EVENT_BUS_PRODUCER_CONFIG", {})
if not isinstance(signals_config, dict):
raise ProducerConfigurationError(
message=("Setting 'EVENT_BUS_PRODUCER_CONFIG' should be a dictionary with event_type as"
" key and list or tuple of config dictionaries as values")
message=(
"Setting 'EVENT_BUS_PRODUCER_CONFIG' should be a dictionary with event_type as"
" key and list or tuple of config dictionaries as values"
)
)
for event_type, configurations in signals_config.items():
signal = self._get_validated_signal_config(event_type, configurations)
Expand Down
Loading