Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
44b60a3
add export / import functionality for repeating instruments
Mar 16, 2022
b5b558c
fix typos in docstrings
Mar 16, 2022
465e974
Add repeating instruments to Readme.md
Mar 16, 2022
7441da7
fix CI configuration to skip doctests from forks
Mar 17, 2022
e3536cc
:gear: Require doctests to be enabled manually
pwildenhain Mar 17, 2022
b2c0706
:truck: Move repeating instrument API methods to separate file
pwildenhain Mar 17, 2022
90b7b93
Merge branch 'master' into pr/210
pwildenhain Mar 18, 2022
48679b5
:robot: Update deps
pwildenhain Mar 18, 2022
cdb91c2
Merge pull request #1 from redcap-tools/pr/210
JuliaSprenger Mar 21, 2022
878734c
Use `typing` module instead of `typing-extensions`
Mar 21, 2022
3ba29ab
Adding tests for repeating instruments import and export
Mar 22, 2022
1e6c7af
:x: Remove changes to test_long_project.xml
pwildenhain Mar 22, 2022
baf2d76
:recycle: Refactor how df_kwargs is handled by default
pwildenhain Mar 22, 2022
08ef860
:white_check_mark: Add repeating forms to long project xml
pwildenhain Mar 22, 2022
29196ae
:recycle: Refactor conversion to csv for df imports
pwildenhain Mar 22, 2022
51c78b5
:recycle: Change param name for initialize_import_payload
pwildenhain Mar 22, 2022
777ebb2
:truck: Move repeating form tests from unit to integration
pwildenhain Mar 22, 2022
0ec926d
:sparkles: Add repeating forms to doctests
pwildenhain Mar 22, 2022
6498af8
:memo: Add repeating API to docs
pwildenhain Mar 22, 2022
b492a01
Merge pull request #2 from redcap-tools/pr/210
JuliaSprenger Mar 24, 2022
c35bbb6
simplify test
Mar 24, 2022
9de0492
Add repeatingFormsEvents unit tests
Mar 24, 2022
bc09c8a
:recycle: Modify repeating form callback to accept imports
pwildenhain Mar 24, 2022
4fe146c
:joy: Un-simplify repeat form integration test
pwildenhain Mar 24, 2022
35e31a4
Merge pull request #3 from redcap-tools/pr/210
JuliaSprenger Mar 29, 2022
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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@ jobs:
poetry install -E data_science
- name: Run doctests
# Forks can't run doctests, requires super user token
if: github.repository == 'redcap-tools/PyCap'
if: github.actor == 'pwildenhain'
run: |
poetry run pytest --doctest-only
poetry run pytest --doctest-only --doctest-plus
- name: Run tests
run: |
poetry run pytest
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ Currently, these API calls are available:

* Field names
* Instrument-event mapping
* Repeating instruments and events
* File
* Metadata
* Project Info
Expand All @@ -57,6 +58,7 @@ Currently, these API calls are available:
* File
* Metadata
* Records
* Repeating instruments and events

### Delete

Expand Down
5 changes: 5 additions & 0 deletions docs/api_reference/repeating.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Repeating

::: redcap.methods.repeating
selection:
inherited_members: true
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ nav:
- Instruments: api_reference/instruments.md
- Metadata: api_reference/metadata.md
- Project Info: api_reference/project_info.md
- Repeating: api_reference/repeating.md
- Records: api_reference/records.md
- Reports: api_reference/reports.md
- Surveys: api_reference/surveys.md
Expand Down
349 changes: 175 additions & 174 deletions poetry.lock

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ mkdocs = "^1.2.3"
mkdocs-material = "^8.1.3"
mkdocstrings = "^0.17.0"
pytest-doctestplus = "^0.11.2"
typing-extensions = "^4.0.1"

[tool.poetry.extras]
data_science = ["pandas"]
Expand Down
1 change: 0 additions & 1 deletion pytest.ini
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
[pytest]
doctest_optionflags = NORMALIZE_WHITESPACE ELLIPSIS FAIL_FAST REPORT_NDIFF
doctest_plus = enabled
addopts = -rsxX -l --tb=short --strict --pylint --black --cov=redcap --cov-report=xml
markers =
integration: test connects to redcapdemo.vanderbilt.edu server
Expand Down
1 change: 1 addition & 0 deletions redcap/methods/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import redcap.methods.metadata
import redcap.methods.project_info
import redcap.methods.records
import redcap.methods.repeating
import redcap.methods.reports
import redcap.methods.surveys
import redcap.methods.users
Expand Down
33 changes: 14 additions & 19 deletions redcap/methods/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ def _initialize_import_payload(
to_import: List[dict],
import_format: Literal["json"],
return_format_type: Literal["json", "csv", "xml"],
data_type: Literal["record", "metadata"],
content: str,
) -> Dict[str, Any]:
...

Expand All @@ -265,7 +265,7 @@ def _initialize_import_payload(
to_import: str,
import_format: Literal["csv", "xml"],
return_format_type: Literal["json", "csv", "xml"],
data_type: Literal["record", "metadata"],
content: str,
) -> Dict[str, Any]:
...

Expand All @@ -275,7 +275,7 @@ def _initialize_import_payload(
to_import: "pd.DataFrame",
import_format: Literal["df"],
return_format_type: Literal["json", "csv", "xml"],
data_type: Literal["record", "metadata"],
content: str,
) -> Dict[str, Any]:
...

Expand All @@ -284,32 +284,27 @@ def _initialize_import_payload(
to_import: Union[List[dict], str, "pd.DataFrame"],
import_format: Literal["json", "csv", "xml", "df"],
return_format_type: Literal["json", "csv", "xml"],
data_type: Literal["record", "metadata"],
content: str,
):
"""Standardize the data to be imported and add it to the payload

Args:
to_import: array of dicts, csv/xml string, ``pandas.DataFrame``
import_format: Format of incoming data.
data_type: The kind of data that are imported
import_format: Format of incoming data
return_format_type: Format of outgoing (returned) data
content: The kind of data that are imported

Returns:
payload: The initialized payload dictionary and updated format
"""

payload = self._initialize_payload(
content=data_type, return_format_type=return_format_type
content=content, return_format_type=return_format_type
)
if import_format == "df":
buf = StringIO()
if data_type == "record":
if self.is_longitudinal:
csv_kwargs = {"index_label": [self.def_field, "redcap_event_name"]}
else:
csv_kwargs = {"index_label": self.def_field}
elif data_type == "metadata":
csv_kwargs = {"index_label": "field_name"}
to_import.to_csv(buf, **csv_kwargs)
has_named_index = to_import.index.name is not None
to_import.to_csv(buf, index=has_named_index)
payload["data"] = buf.getvalue()
buf.close()
import_format = "csv"
Expand Down Expand Up @@ -425,10 +420,7 @@ def _return_data(
return response

if not df_kwargs:
if (
content in ["formEventMapping", "participantList", "project", "user"]
or record_type == "eav"
):
if record_type == "eav":
df_kwargs = {}
elif content == "exportFieldNames":
df_kwargs = {"index_col": "original_field_name"}
Expand All @@ -439,6 +431,9 @@ def _return_data(
df_kwargs = {"index_col": [self.def_field, "redcap_event_name"]}
else:
df_kwargs = {"index_col": self.def_field}
# catchall for other endpoints
else:
df_kwargs = {}

buf = StringIO(response)
dataframe = self._read_csv(buf, **df_kwargs)
Expand Down
4 changes: 2 additions & 2 deletions redcap/methods/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ def import_metadata(
to_import: array of dicts, csv/xml string, `pandas.DataFrame`
Note:
If you pass a csv or xml string, you should use the
`format` parameter appropriately.
`import_format` parameter appropriately.
return_format_type:
Response format. By default, response will be json-decoded.
import_format:
Expand All @@ -150,7 +150,7 @@ def import_metadata(
to_import=to_import,
import_format=import_format,
return_format_type=return_format_type,
data_type="metadata",
content="metadata",
)

# pylint: disable=unsupported-assignment-operation
Expand Down
30 changes: 18 additions & 12 deletions redcap/methods/records.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,15 +165,18 @@ def export_records(

Examples:
>>> proj.export_records()
[{'record_id': '1', 'redcap_event_name': 'event_1_arm_1', 'field_1': '1',
[{'record_id': '1', 'redcap_event_name': 'event_1_arm_1', 'redcap_repeat_instrument': '',
'redcap_repeat_instance': 1, 'field_1': '1',
'checkbox_field___1': '0', 'checkbox_field___2': '1', 'upload_field': 'test_upload.txt',
'form_1_complete': '2'},
{'record_id': '2', 'redcap_event_name': 'event_1_arm_1', 'field_1': '0',
{'record_id': '2', 'redcap_event_name': 'event_1_arm_1', 'redcap_repeat_instrument': '',
'redcap_repeat_instance': 1, 'field_1': '0',
'checkbox_field___1': '0', 'checkbox_field___2': '0', 'upload_field': 'myupload.txt',
'form_1_complete': '0'}]

>>> proj.export_records(filter_logic="[field_1] = 1")
[{'record_id': '1', 'redcap_event_name': 'event_1_arm_1', 'field_1': '1',
[{'record_id': '1', 'redcap_event_name': 'event_1_arm_1', 'redcap_repeat_instrument': '',
'redcap_repeat_instance': 1, 'field_1': '1',
'checkbox_field___1': '0', 'checkbox_field___2': '1', 'upload_field': 'test_upload.txt',
'form_1_complete': '2'}]

Expand All @@ -187,10 +190,10 @@ def export_records(
>>> import pandas as pd
>>> pd.set_option("display.max_columns", 3)
>>> proj.export_records(format_type="df")
field_1 ... form_1_complete
record_id redcap_event_name ...
1 event_1_arm_1 1 ... 2
2 event_1_arm_1 0 ... 0
redcap_repeat_instrument ... form_1_complete
record_id redcap_event_name ...
1 event_1_arm_1 NaN ... 2
2 event_1_arm_1 NaN ... 0
...
"""
# pylint: enable=line-too-long
Expand Down Expand Up @@ -322,7 +325,7 @@ def import_records(
to_import:
Note:
If you pass a df, csv, or xml string, you should use the
`format` parameter appropriately.
`import_format` parameter appropriately.
Note:
Keys of the dictionaries should be subset of project's,
fields, but this isn't a requirement. If you provide keys
Expand Down Expand Up @@ -359,15 +362,15 @@ def import_records(
Union[Dict, str]: response from REDCap API, json-decoded if `return_format` == `'json'`

Examples:
>>> new_record = [{"record_id": 3, "field_1": 1}]
>>> new_record = [{"record_id": 3, "redcap_repeat_instance": 1, "field_1": 1}]
>>> proj.import_records(new_record)
{'count': 1}
"""
payload = self._initialize_import_payload(
to_import=to_import,
import_format=import_format,
return_format_type=return_format_type,
data_type="record",
content="record",
)

# pylint: disable=unsupported-assignment-operation
Expand Down Expand Up @@ -413,8 +416,11 @@ def delete_records(
Union[int, str]: Number of records deleted

Examples:
>>> new_record = [{"record_id": 3, "field_1": 1}, {"record_id": 4}]
>>> proj.import_records(new_record)
>>> new_records = [
... {"record_id": 3, "redcap_repeat_instance": 1, "field_1": 1},
... {"record_id": 4, "redcap_repeat_instance": 1}
... ]
>>> proj.import_records(new_records)
{'count': 2}
>>> proj.delete_records(["3", "4"])
2
Expand Down
92 changes: 92 additions & 0 deletions redcap/methods/repeating.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
"""REDCap API methods for Project repeating instruments"""
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union, Literal

from redcap.methods.base import Base

if TYPE_CHECKING:
import pandas as pd


class Repeating(Base):
"""Responsible for all API methods under 'Repeating Instruments and Events'
in the API Playground
"""

def export_repeating_instruments_events(
self,
format_type: Literal["json", "csv", "xml", "df"] = "json",
df_kwargs: Optional[Dict[str, Any]] = None,
):
"""
Export the project's repeating instruments and events settings

Args:
format_type:
Return the repeating instruments and events in native objects,
csv or xml, `'df''` will return a `pandas.DataFrame`
df_kwargs:
Passed to pandas.read_csv to control construction of
returned DataFrame

Returns:
Union[str, List[Dict[str, Any]], pd.DataFrame]: Repeating instruments and events
for the project

Examples:
>>> proj.export_repeating_instruments_events()
[{'event_name': 'event_1_arm_1', 'form_name': '', 'custom_form_label': ''}]
"""
payload = self._initialize_payload(
content="repeatingFormsEvents", format_type=format_type
)

return_type = self._lookup_return_type(format_type, request_type="export")
response = self._call_api(payload, return_type)

return self._return_data(
response=response,
content="repeatingFormsEvents",
format_type=format_type,
df_kwargs=df_kwargs,
)

def import_repeating_instruments_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",
):
"""
Import repeating instrument and event settings into the REDCap Project

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

Returns:
Union[int, str]: The number of repeated instruments activated

Examples:
>>> rep_instruments = proj.export_repeating_instruments_events(format_type="csv")
>>> proj.import_repeating_instruments_events(rep_instruments, import_format="csv")
1
"""
payload = self._initialize_import_payload(
to_import=to_import,
import_format=import_format,
return_format_type=return_format_type,
content="repeatingFormsEvents",
)

return_type = self._lookup_return_type(
format_type=return_format_type, request_type="import"
)
response = self._call_api(payload, return_type)

return response
1 change: 1 addition & 0 deletions redcap/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class Project(
methods.metadata.Metadata,
methods.project_info.ProjectInfo,
methods.records.Records,
methods.repeating.Repeating,
methods.reports.Reports,
methods.surveys.Surveys,
methods.users.Users,
Expand Down
Loading