From f1564f952deb4699764a5c31ac2cf02cdf8915b4 Mon Sep 17 00:00:00 2001 From: Patrick King Date: Wed, 1 Nov 2023 11:09:29 -0400 Subject: [PATCH 1/6] Add export_instruments --- redcap/methods/base.py | 1 + redcap/methods/instruments.py | 41 ++++++++++++++++++------ tests/integration/test_long_project.py | 6 ++++ tests/integration/test_simple_project.py | 6 ++++ tests/unit/callback_utils.py | 26 +++++++++++++++ tests/unit/test_long_project.py | 6 ++++ tests/unit/test_simple_project.py | 6 ++++ 7 files changed, 82 insertions(+), 10 deletions(-) diff --git a/redcap/methods/base.py b/redcap/methods/base.py index afa9f4f..a3800be 100644 --- a/redcap/methods/base.py +++ b/redcap/methods/base.py @@ -341,6 +341,7 @@ def _return_data( "event", "exportFieldNames", "formEventMapping", + "instrument", "log", "metadata", "participantList", diff --git a/redcap/methods/instruments.py b/redcap/methods/instruments.py index b8af596..89d4c0d 100644 --- a/redcap/methods/instruments.py +++ b/redcap/methods/instruments.py @@ -1,14 +1,5 @@ """REDCap API methods for Project instruments""" -from typing import ( - TYPE_CHECKING, - Any, - Dict, - List, - Literal, - Optional, - Union, - cast, -) +from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Union, cast from redcap.methods.base import Base from redcap.request import Json @@ -20,6 +11,36 @@ class Instruments(Base): """Responsible for all API methods under 'Instruments' in the API Playground""" + def export_instruments( + self, + format_type: Literal["json", "csv", "xml", "df"] = "json", + ): + """ + Export the Instruments of the Project + + Args: + format_type: + Response return format + + Returns: + Union[List[Dict[str, Any]], str, pandas.DataFrame]: List of Instruments + + Examples: + >>> proj.export_instruments() + [{'instrument_name': 'demo', 'instrument_label': 'Demographics'}] + """ + payload = self._initialize_payload( + content="instrument", format_type=format_type + ) + 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="instrument", + format_type=format_type, + ) + def export_instrument_event_mappings( self, format_type: Literal["json", "csv", "xml", "df"] = "json", diff --git a/tests/integration/test_long_project.py b/tests/integration/test_long_project.py index c8c97f6..c8b321b 100644 --- a/tests/integration/test_long_project.py +++ b/tests/integration/test_long_project.py @@ -248,3 +248,9 @@ def test_events_delete(long_project): response = long_project.export_events() assert len(response) == 16 + + +@pytest.mark.integration +def test_export_instruments(long_project): + response = long_project.export_events() + assert len(response) == 9 diff --git a/tests/integration/test_simple_project.py b/tests/integration/test_simple_project.py index 9395cc4..d061b03 100644 --- a/tests/integration/test_simple_project.py +++ b/tests/integration/test_simple_project.py @@ -216,6 +216,12 @@ def test_export_field_names_df(simple_project): assert all(field_names.columns == ["choice_value", "export_field_name"]) +@pytest.mark.integration +def test_export_instruments(simple_project): + events = simple_project.export_events() + assert len(events) == 1 + + @pytest.mark.integration def test_export_and_import_metadata(simple_project): original_metadata = simple_project.export_metadata() diff --git a/tests/unit/callback_utils.py b/tests/unit/callback_utils.py index d183351..2c9704b 100644 --- a/tests/unit/callback_utils.py +++ b/tests/unit/callback_utils.py @@ -215,6 +215,30 @@ def handle_simple_project_form_event_mapping_request(**kwargs) -> Any: return (201, headers, json.dumps(resp)) +def handle_simple_project_instruments_request(**kwargs) -> Any: + """Handle Instrument requests for simple project""" + headers = kwargs["headers"] + + # Instrument export (JSON only) + resp = [{"instrument_name": "form_1", "instrument_label": "Form 1"}] + + return (201, headers, json.dumps(resp)) + + +def handle_long_project_instruments_request(**kwargs) -> Any: + """Handle Instrument requests for long project""" + headers = kwargs["headers"] + + # Instrument export (JSON only) + resp = [ + {"instrument_name": "form_1", "instrument_label": "Form 1"}, + {"instrument_name": "form_2", "instrument_label": "Form 2"}, + {"instrument_name": "form_3", "instrument_label": "Form 3"}, + ] + + return (201, headers, json.dumps(resp)) + + def handle_long_project_form_event_mapping_request(**kwargs) -> Any: """Give back list of events for long project""" headers = kwargs["headers"] @@ -709,6 +733,7 @@ def get_simple_project_request_handler(request_type: str) -> Callable: "file": handle_simple_project_file_request, "formEventMapping": handle_simple_project_form_event_mapping_request, "generateNextRecordName": handle_generate_next_record_name_request, + "instrument": handle_simple_project_instruments_request, "log": handle_logging_request, "metadata": handle_simple_project_metadata_request, "project": handle_project_info_request, @@ -731,6 +756,7 @@ def get_long_project_request_handler(request_type: str) -> Callable: "event": handle_long_project_events_request, "file": handle_long_project_file_request, "formEventMapping": handle_long_project_form_event_mapping_request, + "instrument": handle_long_project_instruments_request, "repeatingFormsEvents": handle_long_project_repeating_form_request, "metadata": handle_long_project_metadata_request, "participantList": handle_long_project_survey_participants_request, diff --git a/tests/unit/test_long_project.py b/tests/unit/test_long_project.py index c0932f6..f160bb1 100644 --- a/tests/unit/test_long_project.py +++ b/tests/unit/test_long_project.py @@ -81,6 +81,12 @@ def test_is_longitudinal(long_project): assert long_project.is_longitudinal +def test_instruments_export(long_project): + response = long_project.export_instruments() + + assert len(response) == 3 + + def test_export_with_events(long_project): events = long_project.export_instrument_event_mappings() unique_event = events[0]["unique_event_name"] diff --git a/tests/unit/test_simple_project.py b/tests/unit/test_simple_project.py index 01787c9..4b51c82 100644 --- a/tests/unit/test_simple_project.py +++ b/tests/unit/test_simple_project.py @@ -463,6 +463,12 @@ def test_export_records_strictly_enforces_format(simple_project): simple_project.export_records(format_type="unsupported") +def test_instruments_export(simple_project): + response = simple_project.export_instruments() + + assert len(response) == 1 + + def test_fem_export_passes_filters_as_arrays(simple_project, mocker): mocked_api_call = mocker.patch.object( simple_project, "_call_api", return_value=None From 4a6c28895666faa7805435482e5baeea89d9c4d3 Mon Sep 17 00:00:00 2001 From: Patrick King Date: Wed, 1 Nov 2023 14:04:53 -0400 Subject: [PATCH 2/6] Add insturment-event mapping import --- redcap/methods/instruments.py | 48 ++++++++++++++++++++++++ tests/integration/test_long_project.py | 37 ++++++++++++++++++ tests/integration/test_simple_project.py | 6 +++ tests/unit/callback_utils.py | 14 +++++-- tests/unit/test_long_project.py | 11 ++++++ tests/unit/test_simple_project.py | 5 +++ 6 files changed, 117 insertions(+), 4 deletions(-) diff --git a/redcap/methods/instruments.py b/redcap/methods/instruments.py index 89d4c0d..7e40d00 100644 --- a/redcap/methods/instruments.py +++ b/redcap/methods/instruments.py @@ -83,3 +83,51 @@ def export_instrument_event_mappings( format_type=format_type, df_kwargs=df_kwargs, ) + + def import_instrument_event_mappings( + 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", + ): + # pylint: disable=line-too-long + """ + Import the project's instrument to event mapping + + 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, import_format + will be json-encoded + + Returns: + Union[int, str]: Number of instrument-event mappings imported + + Examples: + Import instrument-event mappings + >>> instrument_event_mappings = [{"arm_num": "1", "unique_event_name": "event_1_arm_1", "form": "form_1"}] + >>> proj.import_instrument_event_mappings(instrument_event_mappings) + 1 + """ + payload = self._initialize_import_payload( + to_import=to_import, + import_format=import_format, + return_format_type=return_format_type, + content="formEventMapping", + ) + payload["action"] = "import" + + 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 diff --git a/tests/integration/test_long_project.py b/tests/integration/test_long_project.py index c8b321b..cf1d171 100644 --- a/tests/integration/test_long_project.py +++ b/tests/integration/test_long_project.py @@ -254,3 +254,40 @@ def test_events_delete(long_project): def test_export_instruments(long_project): response = long_project.export_events() assert len(response) == 9 + + +@pytest.mark.integration +def test_fem_export(long_project): + response = long_project.export_instrument_event_mappings() + + assert len(response) == 44 + + +@pytest.mark.integration +def test_fem_import(long_project): + # Cache current instrument-event mappings, so they can be restored for subsequent tests + current_fem = long_project.export_instrument_event_mappings() + + instrument_event_mappings = [ + { + "arm_num": "1", + "unique_event_name": "enrollment_arm_1", + "form": "demographics", + } + ] + response = long_project.import_instrument_event_mappings(instrument_event_mappings) + assert response == 1 + + response = long_project.export_instrument_event_mappings() + assert len(response) == 1 + + fem_arm_nums = [fem["arm_num"] for fem in response] + fem_unique_event_names = [fem["unique_event_name"] for fem in response] + fem_forms = [fem["form"] for fem in response] + + assert fem_arm_nums == [1] + assert fem_unique_event_names == ["enrollment_arm_1"] + assert fem_forms == ["demographics"] + + response = long_project.import_instrument_event_mappings(current_fem) + assert response == 44 diff --git a/tests/integration/test_simple_project.py b/tests/integration/test_simple_project.py index d061b03..9cf8c7f 100644 --- a/tests/integration/test_simple_project.py +++ b/tests/integration/test_simple_project.py @@ -285,3 +285,9 @@ def test_export_arms(simple_project): def test_export_events(simple_project): with pytest.raises(RedcapError): simple_project.export_events() + + +@pytest.mark.integration +def test_export_instrument_event_mapping(simple_project): + with pytest.raises(RedcapError): + simple_project.export_instrument_event_mappings() diff --git a/tests/unit/callback_utils.py b/tests/unit/callback_utils.py index 2c9704b..e6cc387 100644 --- a/tests/unit/callback_utils.py +++ b/tests/unit/callback_utils.py @@ -210,9 +210,9 @@ def handle_export_field_names_request(**kwargs) -> Any: def handle_simple_project_form_event_mapping_request(**kwargs) -> Any: """Handle events export, used at project initialization""" headers = kwargs["headers"] - resp = {"error": "no events"} + resp = {"error": "You cannot export form/event mappings for classic projects"} - return (201, headers, json.dumps(resp)) + return (400, headers, json.dumps(resp)) def handle_simple_project_instruments_request(**kwargs) -> Any: @@ -240,9 +240,15 @@ def handle_long_project_instruments_request(**kwargs) -> Any: def handle_long_project_form_event_mapping_request(**kwargs) -> Any: - """Give back list of events for long project""" + """Handle instrument-event mappings for long project""" headers = kwargs["headers"] - resp = [{"unique_event_name": "raw"}] + data = kwargs["data"] + # FEM import (JSON only) + if "data" in str(data): + resp = 1 + # FEM export (JSON only) + else: + resp = [{"arm_num": 1, "unique_event_name": "raw", "form": "form_1"}] return (201, headers, json.dumps(resp)) diff --git a/tests/unit/test_long_project.py b/tests/unit/test_long_project.py index f160bb1..cc1300b 100644 --- a/tests/unit/test_long_project.py +++ b/tests/unit/test_long_project.py @@ -106,12 +106,23 @@ def test_fem_export(long_project): for arm in fem: assert isinstance(arm, dict) + assert len(fem) == 1 + def test_fem_export_stricly_enforces_format(long_project): with pytest.raises(ValueError): long_project.export_instrument_event_mappings(format_type="unsupported") +def test_fem_import(long_project): + instrument_event_mappings = [ + {"arm_num": "1", "unique_event_name": "event_1_arm_1", "form": "form_2"} + ] + res = long_project.import_instrument_event_mappings(instrument_event_mappings) + + assert res == 1 + + def test_export_to_df_gives_multi_index(long_project): long_dataframe = long_project.export_records(format_type="df", event_name="raw") diff --git a/tests/unit/test_simple_project.py b/tests/unit/test_simple_project.py index 4b51c82..07d2a6d 100644 --- a/tests/unit/test_simple_project.py +++ b/tests/unit/test_simple_project.py @@ -634,3 +634,8 @@ def test_arms_export_throws_exception(simple_project): def test_events_export_throws_exception(simple_project): with pytest.raises(RedcapError): simple_project.export_events() + + +def test_instrument_event_mapping_export_throws_exception(simple_project): + with pytest.raises(RedcapError): + simple_project.export_instrument_event_mappings() From 0d356982b070409447fb8ef453d8ffb9467bd003 Mon Sep 17 00:00:00 2001 From: Patrick King Date: Wed, 1 Nov 2023 16:27:02 -0400 Subject: [PATCH 3/6] Add pdf export --- docs/quickstart.md | 10 +++- redcap/methods/instruments.py | 70 +++++++++++++++++++++++- tests/integration/test_long_project.py | 7 +++ tests/integration/test_simple_project.py | 8 ++- tests/unit/callback_utils.py | 18 ++++++ tests/unit/test_long_project.py | 26 +++++++++ tests/unit/test_simple_project.py | 6 ++ 7 files changed, 142 insertions(+), 3 deletions(-) diff --git a/docs/quickstart.md b/docs/quickstart.md index f3dee47..ab8f79b 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -35,7 +35,7 @@ Export a file ```python content, headers = project.export_file('1', 'file') -with open(headers['name'], 'w') as fobj: +with open(headers['name'], 'wb') as fobj: fobj.write(content) ``` @@ -50,3 +50,11 @@ except ValueError: # You screwed up and gave it a bad field name, etc pass ``` + +Export a PDF file of all instruments (blank) + +```python +content, _headers = project.export_pdf() +with open('all_instruments_blank.pdf', 'wb') as fobj: + fobj.write(content) +``` diff --git a/redcap/methods/instruments.py b/redcap/methods/instruments.py index 7e40d00..a2aeef4 100644 --- a/redcap/methods/instruments.py +++ b/redcap/methods/instruments.py @@ -1,7 +1,7 @@ """REDCap API methods for Project instruments""" from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Union, cast -from redcap.methods.base import Base +from redcap.methods.base import Base, FileMap from redcap.request import Json if TYPE_CHECKING: @@ -41,6 +41,74 @@ def export_instruments( format_type=format_type, ) + def export_pdf( + self, + record: Optional[str] = None, + event: Optional[str] = None, + instrument: Optional[str] = None, + repeat_instance: Optional[int] = None, + all_records: Optional[bool] = None, + compact_display: Optional[bool] = None, + ) -> FileMap: + """ + Export PDF file of instruments, either as blank or with data + + Args: + record: Record ID + event: For longitudinal projects, the unique event name + instrument: Unique instrument name + repeat_instance: + (Only for projects with repeating instruments/events) + The repeat instance number of the repeating event (if longitudinal) + or the repeating instrument (if classic or longitudinal). + all_records: + If True, then all records will be exported as a single PDF file. + Note: If this is True, then record, event, and instrument parameters + are all ignored. + compact_display: + If True, then the PDF will be exported in compact display mode. + + Returns: + Content of the file + + Examples: + >>> proj.export_pdf() + b'%PDF-1.3\n3 0 obj\n...' + """ + # load up payload + payload = self._initialize_payload(content="pdf", return_format_type="json") + # there's no format field in this call + payload["action"] = "export" + if record: + payload["record"] = record + if event: + payload["event"] = event + if instrument: + payload["instrument"] = instrument + if repeat_instance: + payload["repeat_instance"] = str(repeat_instance) + if all_records: + payload["allRecords"] = str(all_records == "True") + if compact_display: + payload["compactDisplay"] = str(compact_display == "True") + content, headers = cast( + FileMap, self._call_api(payload=payload, return_type="file_map") + ) + # REDCap adds some useful things in content-type + content_map = {} + if "content-type" in headers: + splat = [ + key_values.strip() for key_values in headers["content-type"].split(";") + ] + key_values = [ + (key_values.split("=")[0], key_values.split("=")[1].replace('"', "")) + for key_values in splat + if "=" in key_values + ] + content_map = dict(key_values) + + return content, content_map + def export_instrument_event_mappings( self, format_type: Literal["json", "csv", "xml", "df"] = "json", diff --git a/tests/integration/test_long_project.py b/tests/integration/test_long_project.py index cf1d171..cb98e24 100644 --- a/tests/integration/test_long_project.py +++ b/tests/integration/test_long_project.py @@ -256,6 +256,13 @@ def test_export_instruments(long_project): assert len(response) == 9 +@pytest.mark.integration +def test_export_pdf(long_project): + content, _ = long_project.export_pdf() + + assert isinstance(content, bytes) + + @pytest.mark.integration def test_fem_export(long_project): response = long_project.export_instrument_event_mappings() diff --git a/tests/integration/test_simple_project.py b/tests/integration/test_simple_project.py index 9cf8c7f..cfe0475 100644 --- a/tests/integration/test_simple_project.py +++ b/tests/integration/test_simple_project.py @@ -1,7 +1,6 @@ """Test suite for simple REDCap Project against real REDCap server""" # pylint: disable=missing-function-docstring import os - from io import StringIO import pandas as pd @@ -222,6 +221,13 @@ def test_export_instruments(simple_project): assert len(events) == 1 +@pytest.mark.integration +def test_export_pdf(simple_project): + content, _ = simple_project.export_pdf() + + assert isinstance(content, bytes) + + @pytest.mark.integration def test_export_and_import_metadata(simple_project): original_metadata = simple_project.export_metadata() diff --git a/tests/unit/callback_utils.py b/tests/unit/callback_utils.py index e6cc387..3175a2a 100644 --- a/tests/unit/callback_utils.py +++ b/tests/unit/callback_utils.py @@ -225,6 +225,14 @@ def handle_simple_project_instruments_request(**kwargs) -> Any: return (201, headers, json.dumps(resp)) +def handle_simple_project_pdf_request(**kwargs) -> Any: + """Handle PDF requests for simple project""" + headers = kwargs["headers"] + resp = {} + + return (201, headers, json.dumps(resp)) + + def handle_long_project_instruments_request(**kwargs) -> Any: """Handle Instrument requests for long project""" headers = kwargs["headers"] @@ -239,6 +247,14 @@ def handle_long_project_instruments_request(**kwargs) -> Any: return (201, headers, json.dumps(resp)) +def handle_long_project_pdf_request(**kwargs) -> Any: + """Handle PDF requests for long project""" + headers = kwargs["headers"] + resp = {} + + return (201, headers, json.dumps(resp)) + + def handle_long_project_form_event_mapping_request(**kwargs) -> Any: """Handle instrument-event mappings for long project""" headers = kwargs["headers"] @@ -742,6 +758,7 @@ def get_simple_project_request_handler(request_type: str) -> Callable: "instrument": handle_simple_project_instruments_request, "log": handle_logging_request, "metadata": handle_simple_project_metadata_request, + "pdf": handle_simple_project_pdf_request, "project": handle_project_info_request, "record": handle_simple_project_records_request, "report": handle_simple_project_reports_request, @@ -766,6 +783,7 @@ def get_long_project_request_handler(request_type: str) -> Callable: "repeatingFormsEvents": handle_long_project_repeating_form_request, "metadata": handle_long_project_metadata_request, "participantList": handle_long_project_survey_participants_request, + "pdf": handle_long_project_pdf_request, "record": handle_long_project_records_request, "report": handle_long_project_reports_request, "version": handle_long_project_version_request, diff --git a/tests/unit/test_long_project.py b/tests/unit/test_long_project.py index cc1300b..c283336 100644 --- a/tests/unit/test_long_project.py +++ b/tests/unit/test_long_project.py @@ -87,6 +87,32 @@ def test_instruments_export(long_project): assert len(response) == 3 +def test_pdf_export(long_project): + content, _ = long_project.export_pdf() + + assert isinstance(content, bytes) + + +def test_pdf_export_specify(long_project): + content, _ = long_project.export_pdf( + record="1", event="raw", instrument="test", repeat_instance=1 + ) + + assert isinstance(content, bytes) + + +def test_pdf_export_all_records(long_project): + content, _ = long_project.export_pdf(all_records=True) + + assert isinstance(content, bytes) + + +def test_pdf_export_compact_display(long_project): + content, _ = long_project.export_pdf(compact_display=True) + + assert isinstance(content, bytes) + + def test_export_with_events(long_project): events = long_project.export_instrument_event_mappings() unique_event = events[0]["unique_event_name"] diff --git a/tests/unit/test_simple_project.py b/tests/unit/test_simple_project.py index 07d2a6d..64af1bf 100644 --- a/tests/unit/test_simple_project.py +++ b/tests/unit/test_simple_project.py @@ -469,6 +469,12 @@ def test_instruments_export(simple_project): assert len(response) == 1 +def test_pdf_export(simple_project): + content, _ = simple_project.export_pdf() + + assert isinstance(content, bytes) + + def test_fem_export_passes_filters_as_arrays(simple_project, mocker): mocked_api_call = mocker.patch.object( simple_project, "_call_api", return_value=None From fd810d06dd501608787c3ae1db385f1dc3f030dd Mon Sep 17 00:00:00 2001 From: Paul Wildenhain Date: Fri, 3 Nov 2023 10:29:00 -0400 Subject: [PATCH 4/6] :bug: Fix integration tests + doctests --- redcap/methods/instruments.py | 6 +++--- tests/integration/test_long_project.py | 17 +++++++++++++---- tests/integration/test_simple_project.py | 4 ++-- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/redcap/methods/instruments.py b/redcap/methods/instruments.py index a2aeef4..038aa2d 100644 --- a/redcap/methods/instruments.py +++ b/redcap/methods/instruments.py @@ -27,7 +27,7 @@ def export_instruments( Examples: >>> proj.export_instruments() - [{'instrument_name': 'demo', 'instrument_label': 'Demographics'}] + [{'instrument_name': 'form_1', 'instrument_label': 'Form 1'}] """ payload = self._initialize_payload( content="instrument", format_type=format_type @@ -69,11 +69,11 @@ def export_pdf( If True, then the PDF will be exported in compact display mode. Returns: - Content of the file + Content of the file and dictionary of useful metadata Examples: >>> proj.export_pdf() - b'%PDF-1.3\n3 0 obj\n...' + (b'%PDF-1.3\\n3 0 obj\\n..., {...}) """ # load up payload payload = self._initialize_payload(content="pdf", return_format_type="json") diff --git a/tests/integration/test_long_project.py b/tests/integration/test_long_project.py index cb98e24..adb041e 100644 --- a/tests/integration/test_long_project.py +++ b/tests/integration/test_long_project.py @@ -184,8 +184,12 @@ def test_arms_delete(long_project): @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() + # Cache current events, so they can be restored for subsequent tests, because arms, events, + # and mappings are deleted when the 'override' parameter is used. + state_dict = { + "events": long_project.export_events(), + "form_event_map": long_project.export_instrument_event_mappings(), + } new_arms = [{"arm_num": 3, "name": "Drug C"}] response = long_project.import_arms(new_arms) @@ -206,9 +210,14 @@ def test_arms_import_override(long_project): with pytest.raises(RedcapError): response = long_project.export_arms() - response = long_project.import_events(current_events) + response = long_project.import_events(state_dict["events"]) assert response == 16 + response = long_project.import_instrument_event_mappings( + state_dict["form_event_map"] + ) + assert response == 44 + response = long_project.export_arms() assert len(response) == 2 @@ -252,7 +261,7 @@ def test_events_delete(long_project): @pytest.mark.integration def test_export_instruments(long_project): - response = long_project.export_events() + response = long_project.export_instruments() assert len(response) == 9 diff --git a/tests/integration/test_simple_project.py b/tests/integration/test_simple_project.py index cfe0475..176f3f2 100644 --- a/tests/integration/test_simple_project.py +++ b/tests/integration/test_simple_project.py @@ -217,8 +217,8 @@ def test_export_field_names_df(simple_project): @pytest.mark.integration def test_export_instruments(simple_project): - events = simple_project.export_events() - assert len(events) == 1 + instruments = simple_project.export_instruments() + assert len(instruments) == 1 @pytest.mark.integration From 155a7bb0734f0004d56bdfd0f1e2b1b6b30cdd6a Mon Sep 17 00:00:00 2001 From: Paul Wildenhain Date: Fri, 3 Nov 2023 10:29:50 -0400 Subject: [PATCH 5/6] :gear: Use triggering_actor for CI --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 00cf7fe..70866b1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,7 +37,7 @@ jobs: poetry install -E data_science - name: Run doctests # Forks can't run doctests, requires super user token - if: github.actor == 'pwildenhain' + if: github.triggering_actor == 'pwildenhain' run: | poetry run pytest --doctest-only --doctest-plus - name: Run tests From e59af4411513cbe205e93bf9e32f6d28cd28bebe Mon Sep 17 00:00:00 2001 From: Patrick King Date: Fri, 3 Nov 2023 14:01:47 -0400 Subject: [PATCH 6/6] Update how payload is set --- redcap/methods/instruments.py | 38 +++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/redcap/methods/instruments.py b/redcap/methods/instruments.py index 038aa2d..1a9e930 100644 --- a/redcap/methods/instruments.py +++ b/redcap/methods/instruments.py @@ -41,6 +41,8 @@ def export_instruments( format_type=format_type, ) + #### pylint: disable=too-many-locals + def export_pdf( self, record: Optional[str] = None, @@ -77,20 +79,28 @@ def export_pdf( """ # load up payload payload = self._initialize_payload(content="pdf", return_format_type="json") - # there's no format field in this call + keys_to_add = ( + record, + event, + instrument, + repeat_instance, + all_records, + compact_display, + ) + str_keys = ( + "record", + "event", + "instrument", + "repeat_instance", + "allRecords", + "compactDisplay", + ) + for key, data in zip(str_keys, keys_to_add): + data = cast(str, data) + if data: + payload[key] = data payload["action"] = "export" - if record: - payload["record"] = record - if event: - payload["event"] = event - if instrument: - payload["instrument"] = instrument - if repeat_instance: - payload["repeat_instance"] = str(repeat_instance) - if all_records: - payload["allRecords"] = str(all_records == "True") - if compact_display: - payload["compactDisplay"] = str(compact_display == "True") + content, headers = cast( FileMap, self._call_api(payload=payload, return_type="file_map") ) @@ -109,6 +119,8 @@ def export_pdf( return content, content_map + #### pylint: enable=too-many-locals + def export_instrument_event_mappings( self, format_type: Literal["json", "csv", "xml", "df"] = "json",