diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b5251eca..285f99d7 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -13,6 +13,10 @@ Change Log Unreleased ~~~~~~~~~~ +Added +_____ +* Add tooling needed to create and trigger events in Open edX platform + [0.2.0] - 2021-07-28 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/openedx_events/data.py b/openedx_events/data.py new file mode 100644 index 00000000..8d1f9069 --- /dev/null +++ b/openedx_events/data.py @@ -0,0 +1,62 @@ +""" +Data attributes for events within the architecture subdomain `learning`. + +These attributes follow the form of attr objects specified in OEP-49 data +pattern. +""" +import socket +from datetime import datetime +from uuid import UUID, uuid1 + +import attr +from django.conf import settings + +import openedx_events + + +@attr.s(frozen=True) +class EventsMetadata: + """ + Attributes defined for Open edX Events metadata object. + + The attributes defined in this class are a subset of the + OEP-41: Asynchronous Server Event Message Format. + + Arguments: + id (UUID): event identifier. + event_type (str): name of the event. + minorversion (int): version of the event type. + source (str): logical source of an event. + sourcehost (str): physical source of the event. + time (datetime): timestamp when the event was sent. + sourcelib (str): Open edX Events library version. + """ + + id = attr.ib(type=UUID, init=False) + event_type = attr.ib(type=str) + minorversion = attr.ib(type=int, converter=attr.converters.default_if_none(0)) + source = attr.ib(type=str, init=False) + sourcehost = attr.ib(type=str, init=False) + time = attr.ib(type=datetime, init=False) + sourcelib = attr.ib(type=tuple, init=False) + + def __attrs_post_init__(self): + """ + Post-init hook that generates metadata for the Open edX Event. + """ + # Have to use this to get around the fact that the class is frozen + # (which we almost always want, but not while we're initializing it). + # Taken from edX Learning Sequences data file. + object.__setattr__(self, "id", uuid1()) + object.__setattr__( + self, + "source", + "openedx/{service}/web".format( + service=getattr(settings, "SERVICE_VARIANT", "") + ), + ) + object.__setattr__(self, "sourcehost", socket.gethostname()) + object.__setattr__(self, "time", datetime.utcnow()) + object.__setattr__( + self, "sourcelib", tuple(map(int, openedx_events.__version__.split("."))) + ) diff --git a/openedx_events/exceptions.py b/openedx_events/exceptions.py new file mode 100644 index 00000000..8b864252 --- /dev/null +++ b/openedx_events/exceptions.py @@ -0,0 +1,69 @@ +""" +Custom exceptions thrown by Open edX events tooling. +""" + + +class OpenEdxEventException(Exception): + """ + Base class for Open edX Events exceptions. + """ + + def __init__(self, message=""): + """ + Init method for OpenEdxEventException base class. + + Arguments: + message (str): message describing why the exception was raised. + """ + super().__init__() + self.message = message + + def __str__(self): + """ + Show string representation of OpenEdxEventException using its message. + """ + return self.message + + +class InstantiationError(OpenEdxEventException): + """ + Describes errors that occur while instantiating events. + + This exception is raised when there's an error instantiating an Open edX + event, it can be that a required argument for the event definition is + missing. + """ + + def __init__(self, event_type="", message=""): + """ + Init method for InstantiationError custom exception class. + + Arguments: + event_type (str): name of the event raising the exception. + message (str): message describing why the exception was raised. + """ + super().__init__( + message="InstantiationError {event_type}: {message}".format( + event_type=event_type, message=message + ) + ) + + +class SenderValidationError(OpenEdxEventException): + """ + Describes errors that occur while validating arguments of send methods. + """ + + def __init__(self, event_type="", message=""): + """ + Init method for SenderValidationError custom exception class. + + Arguments: + event_type (str): name of the event raising the exception. + message (str): message describing why the exception was raised. + """ + super().__init__( + message="SenderValidationError {event_type}: {message}".format( + event_type=event_type, message=message + ) + ) diff --git a/openedx_events/tests/test_tooling.py b/openedx_events/tests/test_tooling.py new file mode 100644 index 00000000..d7e9465c --- /dev/null +++ b/openedx_events/tests/test_tooling.py @@ -0,0 +1,193 @@ +"""This file contains all test for the tooling.py file. + +Classes: + EventsToolingTest: Test events tooling. +""" +from unittest.mock import Mock, patch + +import attr +import ddt +from django.test import TestCase, override_settings + +from openedx_events.exceptions import InstantiationError, SenderValidationError +from openedx_events.tooling import OpenEdxPublicSignal + + +@ddt.ddt +class OpenEdxPublicSignalTest(TestCase): + """ + Test cases for Open edX events base class. + """ + + def setUp(self): + """ + Setup common conditions for every test case. + """ + super().setUp() + self.event_type = "org.openedx.learning.session.login.completed.v1" + self.user_mock = Mock() + self.data_attr = { + "user": Mock, + } + self.public_signal = OpenEdxPublicSignal( + event_type=self.event_type, + data=self.data_attr, + ) + + def test_string_representation(self): + """ + This methods checks the string representation for events base class. + + Expected behavior: + The representation contains the event_type. + """ + self.assertIn(self.event_type, str(self.public_signal)) + + @override_settings(SERVICE_VARIANT="lms") + @patch("openedx_events.data.openedx_events") + @patch("openedx_events.data.socket") + def test_get_signal_metadata(self, socket_mock, events_package_mock): + """ + This methods tests getting the generated metadata for an event. + + Expected behavior: + Returns the metadata containing information about the event. + """ + events_package_mock.__version__ = "0.1.0" + socket_mock.gethostname.return_value = "edx.devstack.lms" + expected_metadata = { + "event_type": self.event_type, + "minorversion": 0, + "source": "openedx/lms/web", + "sourcehost": "edx.devstack.lms", + "sourcelib": [0, 1, 0], + } + + metadata = self.public_signal.generate_signal_metadata() + + self.assertDictContainsSubset(expected_metadata, attr.asdict(metadata)) + + @ddt.data( + ("", {"user": Mock()}, "event_type"), + ("org.openedx.learning.session.login.completed.v1", None, "data"), + ) + @ddt.unpack + def test_event_instantiation_exception( + self, event_type, event_data, missing_argument + ): + """ + This method tests when an event is instantiated without event_type or + event data. + + Expected behavior: + An InstantiationError exception is raised. + """ + exception_message = "InstantiationError {event_type}: Missing required argument '{missing_argument}'".format( + event_type=event_type, missing_argument=missing_argument + ) + + with self.assertRaisesMessage(InstantiationError, exception_message): + OpenEdxPublicSignal(event_type=event_type, data=event_data) + + @patch("openedx_events.tooling.OpenEdxPublicSignal.generate_signal_metadata") + @patch("openedx_events.tooling.Signal.send") + def test_send_event_successfully(self, send_mock, fake_metadata): + """ + This method tests the process of sending an event. + + Expected behavior: + The event is sent as a django signal. + """ + expected_metadata = { + "some_data": "data", + "raise_exception": True, + } + fake_metadata.return_value = expected_metadata + + self.public_signal.send_event(user=self.user_mock) + + send_mock.assert_called_once_with( + sender=None, + user=self.user_mock, + metadata=expected_metadata, + ) + + @patch("openedx_events.tooling.OpenEdxPublicSignal.generate_signal_metadata") + @patch("openedx_events.tooling.Signal.send_robust") + def test_send_robust_event_successfully(self, send_robust_mock, fake_metadata): + """ + This method tests the process of sending an event. + + Expected behavior: + The event is sent as a django signal. + """ + expected_metadata = { + "some_data": "data", + "raise_exception": True, + } + fake_metadata.return_value = expected_metadata + + self.public_signal.send_event(user=self.user_mock, send_robust=True) + + send_robust_mock.assert_called_once_with( + sender=None, + user=self.user_mock, + metadata=expected_metadata, + ) + + @ddt.data( + ( + {"student": Mock()}, + "SenderValidationError org.openedx.learning.session.login.completed.v1: " + "Missing required argument 'user'", + ), + ( + {"user": {"student": Mock()}}, + "SenderValidationError org.openedx.learning.session.login.completed.v1: " + "The argument 'user' is not instance of the Class Attribute 'type'", + ), + ( + {"student": Mock(), "user": Mock()}, + "SenderValidationError org.openedx.learning.session.login.completed.v1: " + "There's a mismatch between initialization data and send_event arguments", + ), + ) + @ddt.unpack + def test_invalid_sender(self, send_arguments, exception_message): + """ + This method tests sending an event with invalid setup on the sender + side. + + Expected behavior: + A SenderValidationError exception is raised. + """ + with self.assertRaisesMessage(SenderValidationError, exception_message): + self.public_signal.send_event(**send_arguments) + + def test_send_event_with_django(self): + """ + This method tests sending an event using the `send` built-in Django + method. + + Expected behavior: + A warning is showed advicing to use Open edX events custom + send_signal method. + """ + message = "Please, use 'send_event' when triggering an Open edX event." + + with self.assertWarns(Warning, msg=message): + self.public_signal.send(sender=Mock()) + + def test_send_robust_event_with_django(self): + """ + This method tests sending an event using the `send` built-in Django + method. + + Expected behavior: + A warning is showed advicing to use Open edX events custom + send_signal method. + """ + message = "Please, use 'send_event' with send_robust equals to True when triggering an Open edX event." + + with self.assertWarns(Warning, msg=message): + self.public_signal.send_robust(sender=Mock()) diff --git a/openedx_events/tooling.py b/openedx_events/tooling.py new file mode 100644 index 00000000..b9ceffb8 --- /dev/null +++ b/openedx_events/tooling.py @@ -0,0 +1,150 @@ +""" +Tooling necessary to use Open edX events. +""" +import warnings + +from django.dispatch import Signal + +from openedx_events.data import EventsMetadata +from openedx_events.exceptions import InstantiationError, SenderValidationError + + +class OpenEdxPublicSignal(Signal): + """ + Custom class used to create Open edX events. + """ + + def __init__(self, event_type, data, minor_version=0): + """ + Init method for OpenEdxPublicSignal definition class. + + Arguments: + event_type (str): name of the event. + data (dict): attributes passed to the event. + minor_version (int): version of the event type. + """ + if not event_type: + raise InstantiationError( + message="Missing required argument 'event_type'" + ) + if not data: + raise InstantiationError( + event_type=event_type, message="Missing required argument 'data'" + ) + self.init_data = data + self.event_type = event_type + self.minor_version = minor_version + super().__init__() + + def __repr__(self): + """ + Represent OpenEdxPublicSignal as a string. + """ + return "".format(event_type=self.event_type) + + def generate_signal_metadata(self): + """ + Generate signal metadata when an event is sent. + + These fields are generated on the fly and are a subset of the Event + Message defined in the OEP-41. + + Example usage: + >>> metadata = \ + STUDENT_REGISTRATION_COMPLETED.generate_signal_metadata() + attr.asdict(metadata) + { + 'event_type': '...learning.student.registration.completed.v1', + 'minorversion': 0, + 'time': '2021-06-09T14:12:45.320819Z', + 'source': 'openedx/lms/web', + 'sourcehost': 'edx.devstack.lms', + 'specversion': '1.0', + 'sourcelib: (0,1,0,), + } + """ + return EventsMetadata( + event_type=self.event_type, + minorversion=self.minor_version, + ) + + def send_event(self, send_robust=False, **kwargs): + """ + Send events to all connected receivers. + + Used to send events just like Django signals are sent. In addition, + some validations are run on the arguments, and then relevant metadata + that can be used for logging or debugging purposes is generated. + Besides this behavior, send_event behaves just like the send method. + + Example usage: + >>> STUDENT_REGISTRATION_COMPLETED.send_event( + user=user_data, registration=registration_data, + ) + [(, 'callback response')] + + Keyword arguments: + send_robust (bool): determines whether the Django signal will be + sent using the method `send` or `send_robust`. + + Returns: + list: response of each receiver following the format + [(receiver, response), ... ] + + Exceptions raised: + SenderValidationError: raised when there's a mismatch between + arguments passed to this method and arguments used to initialize + the event. + """ + + def validate_sender(): + """ + Run validations over the send arguments. + + The validation checks whether the send arguments match the + arguments used when instantiating the event. If they don't a + validation error is raised. + """ + if len(kwargs) != len(self.init_data): + raise SenderValidationError( + event_type=self.event_type, + message="There's a mismatch between initialization data and send_event arguments", + ) + + for key, value in self.init_data.items(): + argument = kwargs.get(key) + if not argument: + raise SenderValidationError( + event_type=self.event_type, + message="Missing required argument '{key}'".format(key=key), + ) + if not isinstance(argument, value): + raise SenderValidationError( + event_type=self.event_type, + message="The argument '{key}' is not instance of the Class Attribute '{attr}'".format( + key=key, attr=value.__class__.__name__ + ), + ) + + validate_sender() + + kwargs["metadata"] = self.generate_signal_metadata() + kwargs["metadata"]["raise_exception"] = not send_robust + + if send_robust: + return super().send_robust(sender=None, **kwargs) + return super().send(sender=None, **kwargs) + + def send(self, sender, **kwargs): # pylint: disable=unused-argument + """ + Override method used to recommend the sender to adopt our custom send. + """ + warnings.warn("Please, use 'send_event' when triggering an Open edX event.") + + def send_robust(self, sender, **kwargs): # pylint: disable=unused-argument + """ + Override method used to recommend the sender to adopt our custom send. + """ + warnings.warn( + "Please, use 'send_event' with send_robust equals to True when triggering an Open edX event." + ) diff --git a/requirements/base.in b/requirements/base.in index d2c3bbf5..0c4377c4 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -2,3 +2,4 @@ -c constraints.txt django +attrs diff --git a/requirements/base.txt b/requirements/base.txt index 57c6c39b..d8c1296f 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -4,6 +4,8 @@ # # make upgrade # +attrs==21.2.0 + # via -r requirements/base.in django==2.2.24 # via # -c requirements/constraints.txt diff --git a/requirements/ci.txt b/requirements/ci.txt index daae3eae..dd546497 100644 --- a/requirements/ci.txt +++ b/requirements/ci.txt @@ -4,11 +4,11 @@ # # make upgrade # -appdirs==1.4.4 +backports.entry-points-selectable==1.1.0 # via virtualenv certifi==2021.5.30 # via requests -chardet==4.0.0 +charset-normalizer==2.0.3 # via requests codecov==2.1.11 # via -r requirements/ci.in @@ -20,17 +20,19 @@ filelock==3.0.12 # via # tox # virtualenv -idna==2.10 +idna==3.2 # via requests packaging==21.0 # via tox +platformdirs==2.1.0 + # via virtualenv pluggy==0.13.1 # via tox py==1.10.0 # via tox pyparsing==2.4.7 # via packaging -requests==2.25.1 +requests==2.26.0 # via codecov six==1.16.0 # via @@ -38,9 +40,9 @@ six==1.16.0 # virtualenv toml==0.10.2 # via tox -tox==3.23.1 +tox==3.24.0 # via -r requirements/ci.in urllib3==1.26.6 # via requests -virtualenv==20.4.7 +virtualenv==20.6.0 # via tox diff --git a/requirements/dev.txt b/requirements/dev.txt index ae5a9f23..23adb671 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -4,11 +4,7 @@ # # make upgrade # -appdirs==1.4.4 - # via - # -r requirements/ci.txt - # virtualenv -astroid==2.6.2 +astroid==2.6.5 # via # -r requirements/quality.txt # pylint @@ -17,7 +13,11 @@ attrs==21.2.0 # via # -r requirements/quality.txt # pytest -bleach==3.3.0 +backports.entry-points-selectable==1.1.0 + # via + # -r requirements/ci.txt + # virtualenv +bleach==3.3.1 # via # -r requirements/quality.txt # readme-renderer @@ -26,15 +26,16 @@ certifi==2021.5.30 # -r requirements/ci.txt # -r requirements/quality.txt # requests -cffi==1.14.5 +cffi==1.14.6 # via # -r requirements/quality.txt # cryptography chardet==4.0.0 + # via diff-cover +charset-normalizer==2.0.3 # via # -r requirements/ci.txt # -r requirements/quality.txt - # diff-cover # requests click==8.0.1 # via @@ -48,7 +49,7 @@ click-log==0.3.2 # via # -r requirements/quality.txt # edx-lint -code-annotations==1.1.2 +code-annotations==1.2.0 # via # -r requirements/quality.txt # edx-lint @@ -68,7 +69,9 @@ cryptography==3.4.7 # via # -r requirements/quality.txt # secretstorage -diff-cover==6.0.0 +ddt==1.4.2 + # via -r requirements/quality.txt +diff-cover==6.2.0 # via -r requirements/dev.in distlib==0.3.2 # via @@ -90,7 +93,7 @@ filelock==3.0.12 # -r requirements/ci.txt # tox # virtualenv -idna==2.10 +idna==3.2 # via # -r requirements/ci.txt # -r requirements/quality.txt @@ -110,7 +113,7 @@ isort==5.9.2 # via # -r requirements/quality.txt # pylint -jeepney==0.6.0 +jeepney==0.7.0 # via # -r requirements/quality.txt # keyring @@ -150,16 +153,20 @@ pbr==5.6.0 # via # -r requirements/quality.txt # stevedore -pep517==0.10.0 +pep517==0.11.0 # via # -r requirements/pip-tools.txt # pip-tools pip-tools==6.2.0 # via -r requirements/pip-tools.txt -pkginfo==1.7.0 +pkginfo==1.7.1 # via # -r requirements/quality.txt # twine +platformdirs==2.1.0 + # via + # -r requirements/ci.txt + # virtualenv pluggy==0.13.1 # via # -r requirements/ci.txt @@ -186,7 +193,7 @@ pygments==2.9.0 # -r requirements/quality.txt # diff-cover # readme-renderer -pylint==2.9.3 +pylint==2.9.5 # via # -r requirements/quality.txt # edx-lint @@ -236,7 +243,7 @@ readme-renderer==29.0 # via # -r requirements/quality.txt # twine -requests==2.25.1 +requests==2.26.0 # via # -r requirements/ci.txt # -r requirements/quality.txt @@ -283,14 +290,16 @@ text-unidecode==1.3 toml==0.10.2 # via # -r requirements/ci.txt - # -r requirements/pip-tools.txt # -r requirements/quality.txt - # pep517 # pylint # pytest # pytest-cov # tox -tox==3.23.1 +tomli==1.1.0 + # via + # -r requirements/pip-tools.txt + # pep517 +tox==3.24.0 # via # -r requirements/ci.txt # tox-battery @@ -300,14 +309,14 @@ tqdm==4.61.2 # via # -r requirements/quality.txt # twine -twine==3.4.1 +twine==3.4.2 # via -r requirements/quality.txt urllib3==1.26.6 # via # -r requirements/ci.txt # -r requirements/quality.txt # requests -virtualenv==20.4.7 +virtualenv==20.6.0 # via # -r requirements/ci.txt # tox diff --git a/requirements/doc.txt b/requirements/doc.txt index 3b1fab84..9366d7ff 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -12,29 +12,29 @@ attrs==21.2.0 # pytest babel==2.9.1 # via sphinx -bleach==3.3.0 +bleach==3.3.1 # via readme-renderer certifi==2021.5.30 # via requests -chardet==4.0.0 - # via - # doc8 - # requests +charset-normalizer==2.0.3 + # via requests click==8.0.1 # via # -r requirements/test.txt # code-annotations -code-annotations==1.1.2 +code-annotations==1.2.0 # via -r requirements/test.txt coverage==5.5 # via # -r requirements/test.txt # pytest-cov +ddt==1.4.2 + # via -r requirements/test.txt django==2.2.24 # via # -c requirements/constraints.txt # -r requirements/test.txt -doc8==0.8.1 +doc8==0.9.0 # via -r requirements/doc.in docutils==0.17.1 # via @@ -44,7 +44,7 @@ docutils==0.17.1 # sphinx edx-sphinx-theme==3.0.0 # via -r requirements/doc.in -idna==2.10 +idna==3.2 # via requests imagesize==1.2.0 # via sphinx @@ -112,19 +112,18 @@ pyyaml==5.4.1 # code-annotations readme-renderer==29.0 # via -r requirements/doc.in -requests==2.25.1 +requests==2.26.0 # via sphinx restructuredtext-lint==1.3.2 # via doc8 six==1.16.0 # via # bleach - # doc8 # edx-sphinx-theme # readme-renderer snowballstemmer==2.1.0 # via sphinx -sphinx==4.0.3 +sphinx==4.1.2 # via # -r requirements/doc.in # edx-sphinx-theme diff --git a/requirements/pip-tools.txt b/requirements/pip-tools.txt index 9f861d0f..c7404f38 100644 --- a/requirements/pip-tools.txt +++ b/requirements/pip-tools.txt @@ -6,11 +6,11 @@ # click==8.0.1 # via pip-tools -pep517==0.10.0 +pep517==0.11.0 # via pip-tools pip-tools==6.2.0 # via -r requirements/pip-tools.in -toml==0.10.2 +tomli==1.1.0 # via pep517 wheel==0.36.2 # via pip-tools diff --git a/requirements/pip.txt b/requirements/pip.txt index 26f5430d..3e7ecb8b 100644 --- a/requirements/pip.txt +++ b/requirements/pip.txt @@ -8,7 +8,7 @@ wheel==0.36.2 # via -r requirements/pip.in # The following packages are considered to be unsafe in a requirements file: -pip==21.1.3 +pip==21.2.1 # via -r requirements/pip.in -setuptools==57.1.0 +setuptools==57.4.0 # via -r requirements/pip.in diff --git a/requirements/quality.txt b/requirements/quality.txt index 6ad94e03..248c3c1e 100644 --- a/requirements/quality.txt +++ b/requirements/quality.txt @@ -4,7 +4,7 @@ # # make upgrade # -astroid==2.6.2 +astroid==2.6.5 # via # pylint # pylint-celery @@ -12,13 +12,13 @@ attrs==21.2.0 # via # -r requirements/test.txt # pytest -bleach==3.3.0 +bleach==3.3.1 # via readme-renderer certifi==2021.5.30 # via requests -cffi==1.14.5 +cffi==1.14.6 # via cryptography -chardet==4.0.0 +charset-normalizer==2.0.3 # via requests click==8.0.1 # via @@ -28,7 +28,7 @@ click==8.0.1 # edx-lint click-log==0.3.2 # via edx-lint -code-annotations==1.1.2 +code-annotations==1.2.0 # via # -r requirements/test.txt # edx-lint @@ -40,6 +40,8 @@ coverage==5.5 # pytest-cov cryptography==3.4.7 # via secretstorage +ddt==1.4.2 + # via -r requirements/test.txt django==2.2.24 # via # -c requirements/constraints.txt @@ -49,7 +51,7 @@ docutils==0.17.1 # via readme-renderer edx-lint==5.0.0 # via -r requirements/quality.in -idna==2.10 +idna==3.2 # via requests importlib-metadata==4.6.1 # via @@ -63,7 +65,7 @@ isort==5.9.2 # via # -r requirements/quality.in # pylint -jeepney==0.6.0 +jeepney==0.7.0 # via # keyring # secretstorage @@ -90,7 +92,7 @@ pbr==5.6.0 # via # -r requirements/test.txt # stevedore -pkginfo==1.7.0 +pkginfo==1.7.1 # via twine pluggy==0.13.1 # via @@ -108,7 +110,7 @@ pydocstyle==6.1.1 # via -r requirements/quality.in pygments==2.9.0 # via readme-renderer -pylint==2.9.3 +pylint==2.9.5 # via # edx-lint # pylint-celery @@ -149,7 +151,7 @@ pyyaml==5.4.1 # code-annotations readme-renderer==29.0 # via twine -requests==2.25.1 +requests==2.26.0 # via # requests-toolbelt # twine @@ -186,7 +188,7 @@ toml==0.10.2 # pytest-cov tqdm==4.61.2 # via twine -twine==3.4.1 +twine==3.4.2 # via -r requirements/quality.in urllib3==1.26.6 # via requests @@ -196,3 +198,6 @@ wrapt==1.12.1 # via astroid zipp==3.5.0 # via importlib-metadata + +# The following packages are considered to be unsafe in a requirements file: +# setuptools diff --git a/requirements/test.in b/requirements/test.in index 6797160b..3af36bf2 100644 --- a/requirements/test.in +++ b/requirements/test.in @@ -3,6 +3,7 @@ -r base.txt # Core dependencies for this package +ddt # A library to multiply test cases pytest-cov # pytest extension for code coverage statistics pytest-django # pytest extension for better Django support code-annotations # provides commands used by the pii_check make target. diff --git a/requirements/test.txt b/requirements/test.txt index 6ef5356f..828d7f5e 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -5,13 +5,17 @@ # make upgrade # attrs==21.2.0 - # via pytest + # via + # -r requirements/base.txt + # pytest click==8.0.1 # via code-annotations -code-annotations==1.1.2 +code-annotations==1.2.0 # via -r requirements/test.in coverage==5.5 # via pytest-cov +ddt==1.4.2 + # via -r requirements/test.in django==2.2.24 # via # -c requirements/constraints.txt