From 6b0d453325add7f86fc1ebe13e272ccbec25580b Mon Sep 17 00:00:00 2001 From: patking02 Date: Tue, 31 Oct 2023 14:27:22 -0400 Subject: [PATCH] :sparkles: Add Arm and Event API (#278) --- README.md | 6 + docs/api_reference/arms.md | 5 + docs/api_reference/events.md | 5 + mkdocs.yml | 2 + redcap/methods/__init__.py | 2 + redcap/methods/arms.py | 155 +++++++++++++++++++++++ redcap/methods/base.py | 2 + redcap/methods/events.py | 155 +++++++++++++++++++++++ redcap/methods/user_roles.py | 4 +- redcap/methods/users.py | 4 +- redcap/project.py | 2 + tests/integration/test_long_project.py | 141 +++++++++++++++++++++ tests/integration/test_simple_project.py | 13 ++ tests/unit/callback_utils.py | 95 +++++++++++++- tests/unit/test_long_project.py | 73 +++++++++++ tests/unit/test_simple_project.py | 10 ++ 16 files changed, 668 insertions(+), 6 deletions(-) create mode 100644 docs/api_reference/arms.md create mode 100644 docs/api_reference/events.md create mode 100644 redcap/methods/arms.py create mode 100644 redcap/methods/events.py diff --git a/README.md b/README.md index 95db5abe..a3618e9e 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,9 @@ Currently, these API calls are available: ### Export +* Arms * Data Access Groups +* Events * Field names * Instrument-event mapping * File @@ -60,7 +62,9 @@ Currently, these API calls are available: ### Import +* Arms * Data Access Groups +* Events * File * Metadata * Records @@ -72,7 +76,9 @@ Currently, these API calls are available: ### Delete +* Arms * Data Access Groups +* Events * File * Records * Users diff --git a/docs/api_reference/arms.md b/docs/api_reference/arms.md new file mode 100644 index 00000000..d314f0f2 --- /dev/null +++ b/docs/api_reference/arms.md @@ -0,0 +1,5 @@ +# Arms + +::: redcap.methods.arms + selection: + inherited_members: true diff --git a/docs/api_reference/events.md b/docs/api_reference/events.md new file mode 100644 index 00000000..89fa85bf --- /dev/null +++ b/docs/api_reference/events.md @@ -0,0 +1,5 @@ +# Events + +::: redcap.methods.events + selection: + inherited_members: true diff --git a/mkdocs.yml b/mkdocs.yml index b0d5bfda..f9566019 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -10,7 +10,9 @@ nav: - Using PyCap in an app/package: using-in-app-or-package.md - API Reference: - Project: api_reference/project.md + - Arms: api_reference/arms.md - Data Access Groups: api_reference/data_access_groups.md + - Events: api_reference/events.md - Field Names: api_reference/field_names.md - Files: api_reference/files.md - Instruments: api_reference/instruments.md diff --git a/redcap/methods/__init__.py b/redcap/methods/__init__.py index af928e03..491aac01 100644 --- a/redcap/methods/__init__.py +++ b/redcap/methods/__init__.py @@ -1,6 +1,8 @@ """Make the method modules available upon import""" +import redcap.methods.arms import redcap.methods.data_access_groups +import redcap.methods.events import redcap.methods.field_names import redcap.methods.files import redcap.methods.instruments diff --git a/redcap/methods/arms.py b/redcap/methods/arms.py new file mode 100644 index 00000000..b19ff141 --- /dev/null +++ b/redcap/methods/arms.py @@ -0,0 +1,155 @@ +"""REDCap API methods for Project arms""" +from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Union, cast + +from redcap.methods.base import Base, Json + +if TYPE_CHECKING: + import pandas as pd + + +class Arms(Base): + """Responsible for all API methods under 'Arms' in the API Playground""" + + def export_arms( + self, + format_type: Literal["json", "csv", "xml", "df"] = "json", + arms: Optional[List[str]] = None, + ): + # pylint: disable=line-too-long + """ + Export the Arms of the Project + + Note: + This only works for longitudinal projects. + + Args: + format_type: + Response return format + arms: + An array of arm numbers that you wish to pull arms for + (by default, all arms are pulled) + + Returns: + Union[List[Dict[str, Any]], str, pandas.DataFrame]: List of Arms + + Examples: + >>> proj.export_arms() + [{'arm_num': 1, 'name': 'Arm 1'}] + """ + # pylint:enable=line-too-long + payload = self._initialize_payload(content="arm", format_type=format_type) + if arms: + # Turn list of arms into dict, and append to payload + arms_dict = {f"arms[{ idx }]": arm for idx, arm in enumerate(arms)} + payload.update(arms_dict) + return_type = self._lookup_return_type(format_type, request_type="export") + response = cast(Union[Json, str], self._call_api(payload, return_type)) + + return self._return_data( + response=response, + content="arm", + format_type=format_type, + ) + + def import_arms( + self, + to_import: Union[str, List[Dict[str, Any]], "pd.DataFrame"], + return_format_type: Literal["json", "csv", "xml"] = "json", + import_format: Literal["json", "csv", "xml", "df"] = "json", + override: Optional[int] = 0, + ): + """ + Import Arms into the REDCap Project + + Note: + This only works for longitudinal projects. + + Args: + to_import: array of dicts, csv/xml string, `pandas.DataFrame` + Note: + If you pass a csv or xml string, you should use the + `import format` parameter appropriately. + return_format_type: + Response format. By default, response will be json-decoded. + import_format: + Format of incoming data. By default, to_import will be json-encoded + override: + 0 - false [default], 1 - true + You may use override=1 as a 'delete all + import' action in order to + erase all existing Arms in the project while importing new Arms. + If override=0, then you can only add new Arms or rename existing ones. + + Returns: + Union[int, str]: Number of Arms added or updated + + Examples: + Create a new arm + >>> new_arm = [{"arm_num": 2, "name": "Arm 2"}] + >>> proj.import_arms(new_arm) + 1 + """ + payload = self._initialize_import_payload( + to_import=to_import, + import_format=import_format, + return_format_type=return_format_type, + content="arm", + ) + payload["action"] = "import" + payload["override"] = override + + return_type = self._lookup_return_type( + format_type=return_format_type, request_type="import" + ) + response = cast(Union[Json, str], self._call_api(payload, return_type)) + + return response + + def delete_arms( + self, + arms: List[str], + return_format_type: Literal["json", "csv", "xml"] = "json", + ): + """ + Delete Arms from the Project + + Note: + Because of this method's destructive nature, it is only available + for use for projects in Development status. + Additionally, please be aware that deleting an arm also automatically + deletes all events that belong to that arm, and will also automatically + delete any records/data that have been collected under that arm + (this is non-reversible data loss). + This only works for longitudinal projects. + + Args: + arms: List of arm numbers to delete from the project + return_format_type: + Response format. By default, response will be json-decoded. + + Returns: + Union[int, str]: Number of arms deleted + + Examples: + Create a new arm + >>> new_arm = [{"arm_num": 2, "name": "Arm 2"}] + >>> proj.import_arms(new_arm) + 1 + + Delete the new arm + >>> proj.delete_arms([2]) + 1 + """ + payload = self._initialize_payload( + content="arm", return_format_type=return_format_type + ) + payload["action"] = "delete" + # Turn list of arms into dict, and append to payload + arms_dict = {f"arms[{ idx }]": arm for idx, arm in enumerate(arms)} + payload.update(arms_dict) + + return_type = self._lookup_return_type( + format_type=return_format_type, request_type="delete" + ) + response = cast(Union[Json, str], self._call_api(payload, return_type)) + + return response diff --git a/redcap/methods/base.py b/redcap/methods/base.py index 7dc1d3a2..afa9f4fa 100644 --- a/redcap/methods/base.py +++ b/redcap/methods/base.py @@ -336,7 +336,9 @@ def _return_data( self, response: Union[Json, str], content: Literal[ + "arm", "dag", + "event", "exportFieldNames", "formEventMapping", "log", diff --git a/redcap/methods/events.py b/redcap/methods/events.py new file mode 100644 index 00000000..e8ea429a --- /dev/null +++ b/redcap/methods/events.py @@ -0,0 +1,155 @@ +"""REDCap API methods for Project events""" +from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Union, cast + +from redcap.methods.base import Base, Json + +if TYPE_CHECKING: + import pandas as pd + + +class Events(Base): + """Responsible for all API methods under 'Events' in the API Playground""" + + def export_events( + self, + format_type: Literal["json", "csv", "xml", "df"] = "json", + arms: Optional[List[str]] = None, + ): + # pylint: disable=line-too-long + """ + Export the Events of the Project + + Note: + This only works for longitudinal projects. + + Args: + format_type: + Response return format + arms: + An array of arm numbers that you wish to pull events for + (by default, all events are pulled) + + Returns: + Union[List[Dict[str, Any]], str, pandas.DataFrame]: List of Events + + Examples: + >>> proj.export_events() + [{'event_name': 'Event 1', 'arm_num': 1, 'unique_event_name': 'event_1_arm_1', + 'custom_event_label': '', 'event_id': ...}, {'event_name': 'Event 2', ...}] + """ + # pylint:enable=line-too-long + payload = self._initialize_payload(content="event", format_type=format_type) + if arms: + # Turn list of arms into dict, and append to payload + arms_dict = {f"arms[{ idx }]": arm for idx, arm in enumerate(arms)} + payload.update(arms_dict) + return_type = self._lookup_return_type(format_type, request_type="export") + response = cast(Union[Json, str], self._call_api(payload, return_type)) + + return self._return_data( + response=response, + content="event", + format_type=format_type, + ) + + def import_events( + self, + to_import: Union[str, List[Dict[str, Any]], "pd.DataFrame"], + return_format_type: Literal["json", "csv", "xml"] = "json", + import_format: Literal["json", "csv", "xml", "df"] = "json", + override: Optional[int] = 0, + ): + """ + Import Events into the REDCap Project + + Note: + This only works for longitudinal projects. + + Args: + to_import: array of dicts, csv/xml string, `pandas.DataFrame` + Note: + If you pass a csv or xml string, you should use the + `import format` parameter appropriately. + return_format_type: + Response format. By default, response will be json-decoded. + import_format: + Format of incoming data. By default, to_import will be json-encoded + override: + 0 - false [default], 1 - true + You may use override=1 as a 'delete all + import' action in order to + erase all existing Events in the project while importing new Events. + If override=0, then you can only add new Events or rename existing ones. + + Returns: + Union[int, str]: Number of Events added or updated + + Examples: + Create a new event + >>> new_event = [{"event_name": "Event 2", "arm_num": "1"}] + >>> proj.import_events(new_event) + 1 + """ + payload = self._initialize_import_payload( + to_import=to_import, + import_format=import_format, + return_format_type=return_format_type, + content="event", + ) + payload["action"] = "import" + payload["override"] = override + + return_type = self._lookup_return_type( + format_type=return_format_type, request_type="import" + ) + response = cast(Union[Json, str], self._call_api(payload, return_type)) + + return response + + def delete_events( + self, + events: List[str], + return_format_type: Literal["json", "csv", "xml"] = "json", + ): + """ + Delete Events from the Project + + Note: + Because of this method's destructive nature, it is only available + for use for projects in Development status. + Additionally, please be aware that deleting an event will automatically + delete any records/data that have been collected under that event + (this is non-reversible data loss). + This only works for longitudinal projects. + + Args: + events: List of unique event names to delete from the project + return_format_type: + Response format. By default, response will be json-decoded. + + Returns: + Union[int, str]: Number of events deleted + + Examples: + Create a new event + >>> new_event = [{"event_name": "Event 2", "arm_num": "1"}] + >>> proj.import_events(new_event) + 1 + + Delete the new event + >>> proj.delete_events(["event_2_arm_1"]) + 1 + """ + payload = self._initialize_payload( + content="event", return_format_type=return_format_type + ) + payload["action"] = "delete" + # Turn list of events into dict, and append to payload + events_dict = {f"events[{ idx }]": event for idx, event in enumerate(events)} + payload.update(events_dict) + + return_type = self._lookup_return_type( + format_type=return_format_type, request_type="delete" + ) + response = cast(Union[Json, str], self._call_api(payload, return_type)) + + return response diff --git a/redcap/methods/user_roles.py b/redcap/methods/user_roles.py index 06e8d551..09532e92 100644 --- a/redcap/methods/user_roles.py +++ b/redcap/methods/user_roles.py @@ -40,8 +40,8 @@ def export_user_roles( Examples: >>> proj.export_user_roles() - [{'unique_role_name': ..., 'role_label': 'Test role', 'design': '0', 'user_rights': '0', - 'data_access_groups': '0', 'reports': '0', 'stats_and_charts': '0', + [{'unique_role_name': ..., 'role_label': 'Test role', 'design': '0', 'alerts': '0', + 'user_rights': '0', 'data_access_groups': '0', 'reports': '0', 'stats_and_charts': '0', 'manage_survey_participants': '0', 'calendar': '0', 'data_import_tool': '0', 'data_comparison_tool': '0', 'logging': '0', 'file_repository': '0', 'data_quality_create': '0', 'data_quality_execute': '0', 'api_export': '0', diff --git a/redcap/methods/users.py b/redcap/methods/users.py index 9ed20841..cc886094 100644 --- a/redcap/methods/users.py +++ b/redcap/methods/users.py @@ -31,8 +31,8 @@ def export_users( Examples: >>> proj.export_users() [{'username': ..., 'email': ..., 'expiration': '', 'data_access_group': '', - 'data_access_group_id': '', 'design': 1, 'user_rights': 1, 'data_access_groups': 1, - 'reports': 1, ...}] + 'data_access_group_id': '', 'design': 1, 'alerts': 1, 'user_rights': 1, + 'data_access_groups': 1, 'reports': 1, ...}] """ payload = self._initialize_payload(content="user", format_type=format_type) return_type = self._lookup_return_type(format_type, request_type="export") diff --git a/redcap/project.py b/redcap/project.py index 8fd83a8b..e839d572 100755 --- a/redcap/project.py +++ b/redcap/project.py @@ -19,7 +19,9 @@ class Project( + methods.arms.Arms, methods.data_access_groups.DataAccessGroups, + methods.events.Events, methods.field_names.FieldNames, methods.files.Files, methods.instruments.Instruments, diff --git a/tests/integration/test_long_project.py b/tests/integration/test_long_project.py index 6a3248ff..c8c97f62 100644 --- a/tests/integration/test_long_project.py +++ b/tests/integration/test_long_project.py @@ -4,6 +4,8 @@ import pytest +from redcap import RedcapError + if not os.getenv("REDCAPDEMO_SUPERUSER_TOKEN"): pytest.skip( @@ -107,3 +109,142 @@ def test_limit_export_records_forms_and_fields(long_project): assert long_project.def_field in records_df.index.names assert complete_cols == ["baseline_data_complete"] + + +@pytest.mark.integration +def test_arms_export(long_project): + response = long_project.export_arms() + + assert len(response) == 2 + + arm_nums = [arm["arm_num"] for arm in response] + arm_names = [arm["name"] for arm in response] + + assert arm_nums == [1, 2] + assert arm_names == ["Drug A", "Drug B"] + + +@pytest.mark.integration +def test_arms_import(long_project): + new_arms = [{"arm_num": 3, "name": "Drug C"}] + response = long_project.import_arms(new_arms) + + assert response == 1 + + # REDCap will not return an Arm unless it has an event associated with it + # Need to add an event to the newly created Arm + new_events = [{"event_name": "new_event", "arm_num": "3"}] + response = long_project.import_events(new_events) + + response = long_project.export_arms() + assert len(response) == 3 + + arm_nums = [arm["arm_num"] for arm in response] + arm_names = [arm["name"] for arm in response] + + assert arm_nums == [1, 2, 3] + assert arm_names == ["Drug A", "Drug B", "Drug C"] + + +@pytest.mark.integration +def test_arms_import_rename(long_project): + new_arms = [{"arm_num": 1, "name": "Drug Alpha"}] + response = long_project.import_arms(new_arms) + + assert response == 1 + + response = long_project.export_arms() + + assert len(response) == 3 + + arm_nums = [arm["arm_num"] for arm in response] + arm_names = [arm["name"] for arm in response] + + assert arm_nums == [1, 2, 3] + assert arm_names == ["Drug Alpha", "Drug B", "Drug C"] + + +@pytest.mark.integration +def test_arms_delete(long_project): + arms = [3] + response = long_project.delete_arms(arms) + + assert response == 1 + + response = long_project.export_arms() + + assert len(response) == 2 + + arm_nums = [arm["arm_num"] for arm in response] + arm_names = [arm["name"] for arm in response] + + assert arm_nums == [1, 2] + assert arm_names == ["Drug Alpha", "Drug B"] + + +@pytest.mark.integration +def test_arms_import_override(long_project): + # Cache current events, so they can be restored for subsequent tests + current_events = long_project.export_events() + + new_arms = [{"arm_num": 3, "name": "Drug C"}] + response = long_project.import_arms(new_arms) + assert response == 1 + # Add event for new arm + new_event = [{"event_name": "new_event", "arm_num": "3"}] + response = long_project.import_events(new_event) + + response = long_project.export_arms() + + assert len(response) == 3 + + new_arms = [{"arm_num": 1, "name": "Drug A"}, {"arm_num": 2, "name": "Drug B"}] + response = long_project.import_arms(new_arms, override=1) + + assert response == 2 + # Confirm that there are no events associated with new override arms + with pytest.raises(RedcapError): + response = long_project.export_arms() + + response = long_project.import_events(current_events) + assert response == 16 + + response = long_project.export_arms() + assert len(response) == 2 + + arm_nums = [arm["arm_num"] for arm in response] + arm_names = [arm["name"] for arm in response] + + assert arm_nums == [1, 2] + assert arm_names == ["Drug A", "Drug B"] + + +@pytest.mark.integration +def test_events_export(long_project): + response = long_project.export_events() + + assert len(response) == 16 + + +@pytest.mark.integration +def test_events_import(long_project): + new_events = [{"event_name": "XYZ", "arm_num": "2"}] + response = long_project.import_events(new_events) + + assert response == 1 + + response = long_project.export_events() + + assert len(response) == 17 + + +@pytest.mark.integration +def test_events_delete(long_project): + events = ["xyz_arm_2"] + response = long_project.delete_events(events) + + assert response == 1 + + response = long_project.export_events() + + assert len(response) == 16 diff --git a/tests/integration/test_simple_project.py b/tests/integration/test_simple_project.py index de62084d..9395cc4b 100644 --- a/tests/integration/test_simple_project.py +++ b/tests/integration/test_simple_project.py @@ -8,6 +8,7 @@ import pytest import semantic_version +from redcap import RedcapError if not os.getenv("REDCAPDEMO_SUPERUSER_TOKEN"): pytest.skip( @@ -266,3 +267,15 @@ def test_export_logging(simple_project): logs = simple_project.export_logging(log_type="manage") first_log = logs.pop() assert "manage/design" in first_log["action"].lower() + + +@pytest.mark.integration +def test_export_arms(simple_project): + with pytest.raises(RedcapError): + simple_project.export_arms() + + +@pytest.mark.integration +def test_export_events(simple_project): + with pytest.raises(RedcapError): + simple_project.export_events() diff --git a/tests/unit/callback_utils.py b/tests/unit/callback_utils.py index c4b3b329..d1833515 100644 --- a/tests/unit/callback_utils.py +++ b/tests/unit/callback_utils.py @@ -38,10 +38,98 @@ def parse_request(req: Any) -> List[Union[dict, str]]: return [data, headers, request_type] +def handle_simple_project_arms_request(**kwargs) -> Any: + """Handle Arm requests for simple project""" + headers = kwargs["headers"] + resp = {"error": "You cannot export arms for classic projects"} + + return (400, headers, json.dumps(resp)) + + def handle_long_project_arms_request(**kwargs) -> Any: - """Give back list of arms for long project""" + """Handle Arm requests for long project""" + headers = kwargs["headers"] + data = kwargs["data"] + # Arm import (JSON only) + if "data" in str(data): + override = json.loads(data["override"][0]) + if override == 0: + resp = 1 + elif override == 1: + resp = 2 + else: + resp = {"error": "test error"} + # Arm delete (JSON only) + elif "delete" in str(data): + resp = 1 + # Arm export (JSON only) + else: + # Check for optional arms arg + arms = data.get("arms[0]", []) + if len(arms): + resp = [{"arm_num": 2, "name": "test_2"}] + else: + resp = [{"arm_num": 1, "name": "test_1"}] + + return (201, headers, json.dumps(resp)) + + +def handle_simple_project_events_request(**kwargs) -> Any: + """Handle Event requests for simple project""" + headers = kwargs["headers"] + resp = {"error": "You cannot export events for classic projects"} + + return (400, headers, json.dumps(resp)) + + +def handle_long_project_events_request(**kwargs) -> Any: + """Handle Event requests for long project""" headers = kwargs["headers"] - resp = [{"arm_num": 1, "name": "test"}] + data = kwargs["data"] + # Event import (JSON only) + if "data" in str(data): + override = json.loads(data["override"][0]) + if override == 0: + resp = 1 + elif override == 1: + resp = 2 + else: + resp = {"error": "test error"} + # Event delete (JSON only) + elif "delete" in str(data): + resp = 1 + # Event export (JSON only) + else: + # Check for optional arms arg + arms = data.get("arms[0]", []) + if len(arms): + resp = [ + { + "event_name": "Event 1", + "arm_num": 1, + "unique_event_name": "event_1_arm_1", + "custom_event_label": "", + "event_id": 1, + }, + { + "event_name": "Event 2", + "arm_num": 1, + "unique_event_name": "event_2_arm_1", + "custom_event_label": "", + "event_id": 2, + }, + ] + + else: + resp = [ + { + "event_name": "Event 1", + "arm_num": 1, + "unique_event_name": "event_1_arm_1", + "custom_event_label": "", + "event_id": 1, + } + ] return (201, headers, json.dumps(resp)) @@ -614,7 +702,9 @@ def handle_long_project_survey_participants_request(**kwargs) -> Any: def get_simple_project_request_handler(request_type: str) -> Callable: """Given a request type, extract the handler function""" handlers_dict = { + "arm": handle_simple_project_arms_request, "dag": handle_dag_request, + "event": handle_simple_project_events_request, "exportFieldNames": handle_export_field_names_request, "file": handle_simple_project_file_request, "formEventMapping": handle_simple_project_form_event_mapping_request, @@ -638,6 +728,7 @@ def get_long_project_request_handler(request_type: str) -> Callable: """Given a request type, extract the handler function""" handlers_dict = { "arm": handle_long_project_arms_request, + "event": handle_long_project_events_request, "file": handle_long_project_file_request, "formEventMapping": handle_long_project_form_event_mapping_request, "repeatingFormsEvents": handle_long_project_repeating_form_request, diff --git a/tests/unit/test_long_project.py b/tests/unit/test_long_project.py index 1f718bd9..c0932f66 100644 --- a/tests/unit/test_long_project.py +++ b/tests/unit/test_long_project.py @@ -138,3 +138,76 @@ def test_import_export_repeating_forms(long_project): to_import=rep, import_format="json" ) assert res == 1 + + +def test_arms_export(long_project): + response = long_project.export_arms() + + assert len(response) == 1 + + +def test_arms_import(long_project): + new_arms = [{"arm_num": 2, "name": "test_2"}] + response = long_project.import_arms(new_arms) + + assert response == 1 + + +def test_arms_export_specify_arm(long_project): + response = long_project.export_arms(arms=[2]) + + assert len(response) == 1 + + assert any(arm["name"] == "test_2" for arm in response) + + +def test_arms_import_override(long_project): + new_arms = [{"arm_num": 3, "name": "test_3"}, {"arm_num": 4, "name": "test_4"}] + response = long_project.import_arms(new_arms, override=1) + + assert response == 2 + + +def test_arms_delete(long_project): + arms = [3] + response = long_project.delete_arms(arms) + + assert response == 1 + + +def test_events_export(long_project): + response = long_project.export_events() + + assert len(response) == 1 + + +def test_events_import(long_project): + new_events = [{"event_name": "Event 2", "arm_num": "1"}] + response = long_project.import_events(new_events) + + assert response == 1 + + +def test_events_export_specify_arm(long_project): + response = long_project.export_events(arms=[1]) + + assert len(response) == 2 + + assert any(event["arm_num"] == 1 for event in response) + + +def test_events_import_override(long_project): + new_events = [ + {"event_name": "Event 3", "arm_num": "1"}, + {"event_name": "Event 4", "arm_num": "1"}, + ] + response = long_project.import_events(new_events, override=1) + + assert response == 2 + + +def test_events_delete(long_project): + events = ["event_4_arm_1"] + response = long_project.delete_events(events) + + assert response == 1 diff --git a/tests/unit/test_simple_project.py b/tests/unit/test_simple_project.py index d1436b4a..01787c96 100644 --- a/tests/unit/test_simple_project.py +++ b/tests/unit/test_simple_project.py @@ -618,3 +618,13 @@ def test_reports_df_export(simple_project): def test_reports_export_stricly_enforces_format(simple_project): with pytest.raises(ValueError): simple_project.export_report(report_id="1", format_type="unsupported") + + +def test_arms_export_throws_exception(simple_project): + with pytest.raises(RedcapError): + simple_project.export_arms() + + +def test_events_export_throws_exception(simple_project): + with pytest.raises(RedcapError): + simple_project.export_events()