├── reproschema ├── tests │ ├── __init__.py │ ├── data │ │ ├── README.md │ │ ├── responses │ │ │ ├── response1.jsonld │ │ │ └── responseActivity1.jsonld │ │ ├── activities │ │ │ ├── items │ │ │ │ ├── activity1_total_score │ │ │ │ ├── item2.jsonld │ │ │ │ └── item1.jsonld │ │ │ ├── activity1.jsonld │ │ │ └── activity1_embed.jsonld │ │ └── protocols │ │ │ ├── protocol1.jsonld │ │ │ └── protocol1_embed.jsonld │ ├── test_rs2redcap_data │ │ └── test_redcap2rs │ │ │ ├── activities │ │ │ ├── autism_parenting_stress_index_apsi │ │ │ │ └── items │ │ │ │ │ ├── apsi_date │ │ │ │ │ ├── apsi_total │ │ │ │ │ ├── record_id │ │ │ │ │ ├── apsi_name_of_child │ │ │ │ │ ├── apsi_person_completing │ │ │ │ │ ├── apsi_accepted │ │ │ │ │ ├── apsi_bowel │ │ │ │ │ ├── apsi_diet │ │ │ │ │ ├── apsi_potty │ │ │ │ │ ├── apsi_sleep │ │ │ │ │ ├── apsi_tantrums │ │ │ │ │ ├── apsi_agressive │ │ │ │ │ ├── apsi_communicate │ │ │ │ │ ├── apsi_independently │ │ │ │ │ ├── apsi_not_close │ │ │ │ │ ├── apsi_self_injure │ │ │ │ │ ├── apsi_social_dev │ │ │ │ │ └── apsi_transitions │ │ │ └── cognitive_and_affective_mindfulness_scalerevised_c │ │ │ │ ├── items │ │ │ │ ├── cams_r_1 │ │ │ │ ├── cams_r_10 │ │ │ │ ├── cams_r_11 │ │ │ │ ├── cams_r_12 │ │ │ │ ├── cams_r_2 │ │ │ │ ├── cams_r_3 │ │ │ │ ├── cams_r_4 │ │ │ │ ├── cams_r_5 │ │ │ │ ├── cams_r_6 │ │ │ │ ├── cams_r_7 │ │ │ │ ├── cams_r_8 │ │ │ │ └── cams_r_9 │ │ │ │ └── cognitive_and_affective_mindfulness_scalerevised_c_schema │ │ │ └── test_redcap2rs │ │ │ └── test_redcap2rs_schema │ ├── test_validate.py │ ├── test_redcap2rs_data │ │ └── redcap2rs.yaml │ ├── data_test_nimh-minimal │ │ ├── test_nimh-minimal.yaml │ │ └── nimh_minimal │ │ │ ├── nimh_minimal │ │ │ ├── README.md │ │ │ └── nimh_minimal_schema │ │ │ └── activities │ │ │ └── demo │ │ │ └── demo_schema │ ├── contexts │ │ ├── base │ │ └── generic │ ├── test_reproschema2fhir_data │ │ └── activities │ │ │ └── q_generic_gad7_anxiety │ │ │ ├── items │ │ │ ├── gad_7_started_at │ │ │ ├── gad_7_completed_at │ │ │ ├── gad_7_duration │ │ │ ├── gad_7_session_id │ │ │ ├── trouble_relaxing │ │ │ ├── easily_agitated │ │ │ ├── worry_too_much │ │ │ ├── afraid_of_things │ │ │ ├── cant_control_worry │ │ │ ├── hard_to_sit_still │ │ │ ├── tough_to_work │ │ │ └── nervous_anxious │ │ │ └── q_generic_gad7_anxiety_schema │ ├── test_reproschema2fhir.py │ ├── test_convert.py │ ├── test_reproschema2redcap.py │ ├── test_redcap2reproschema.py │ ├── test_output2redcap.py │ ├── test_process_csv.py │ ├── test_output2redcap │ │ └── activity_0.jsonld │ ├── test_process_choices.py │ └── test_field_property.py ├── models │ ├── tests │ │ ├── __init__.py │ │ └── test_schema.py │ ├── __init__.py │ └── utils.py ├── context_url.py ├── example │ ├── redcap │ │ └── redcap_output │ │ │ └── redcap.csv │ └── fhir │ │ ├── reproschema2fhir_data_example │ │ └── activities │ │ │ └── q_generic_gad7_anxiety │ │ │ ├── items │ │ │ ├── gad_7_started_at │ │ │ ├── gad_7_completed_at │ │ │ ├── gad_7_duration │ │ │ ├── gad_7_session_id │ │ │ ├── trouble_relaxing │ │ │ ├── easily_agitated │ │ │ ├── worry_too_much │ │ │ ├── afraid_of_things │ │ │ ├── cant_control_worry │ │ │ ├── hard_to_sit_still │ │ │ ├── tough_to_work │ │ │ └── nervous_anxious │ │ │ └── q_generic_gad7_anxiety_schema │ │ └── fhir_output │ │ └── q_generic_gad7_anxiety │ │ └── q_generic_gad7_anxiety.json ├── __init__.py ├── migrate.py ├── output2redcap.py ├── redcap_mappings.py ├── validate.py ├── utils.py ├── jsonldutils.py └── convertutils.py ├── codespell_ignore_words.txt ├── docs ├── user_guide │ ├── index.md │ ├── output2redcap.md │ ├── reproschema2fhir.md │ ├── cli_usage.md │ ├── reproschema2redcap.md │ └── redcap2reproschema.md ├── index.md ├── about.md └── installation.md ├── .autorc ├── .flake8 ├── mkdocs.yml ├── templates └── redcap2rs.yaml ├── .github └── workflows │ ├── deploy_mkdocs.yml │ ├── publish.yml │ ├── package.yml │ └── release.yml ├── README.md ├── .pre-commit-config.yaml ├── pyproject.toml ├── .gitignore └── CHANGELOG.md /reproschema/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /reproschema/models/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /codespell_ignore_words.txt: -------------------------------------------------------------------------------- 1 | datas 2 | Varios 3 | deamon 4 | -------------------------------------------------------------------------------- /reproschema/tests/data/README.md: -------------------------------------------------------------------------------- 1 | This file should be ignored during validation. 2 | -------------------------------------------------------------------------------- /docs/user_guide/index.md: -------------------------------------------------------------------------------- 1 | # User Guide 2 | 3 | This section provides a detailed guide to using the `reproschema` command-line interface (CLI). 4 | -------------------------------------------------------------------------------- /reproschema/context_url.py: -------------------------------------------------------------------------------- 1 | # this is automatically updated after reproschema new release 2 | CONTEXTFILE_URL = "https://raw.githubusercontent.com/ReproNim/reproschema/main/releases/1.0.0/reproschema" 3 | -------------------------------------------------------------------------------- /.autorc: -------------------------------------------------------------------------------- 1 | { 2 | "onlyPublishWithReleaseLabel": true, 3 | "baseBranch": "main", 4 | "author": "Repronim developers ", 5 | "noVersionPrefix": true, 6 | "plugins": ["git-tag"] 7 | } 8 | -------------------------------------------------------------------------------- /reproschema/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .model import ( 2 | Activity, 3 | Item, 4 | Protocol, 5 | Response, 6 | ResponseActivity, 7 | ResponseOption, 8 | ) 9 | from .utils import identify_model_class, write_obj_jsonld 10 | -------------------------------------------------------------------------------- /reproschema/example/redcap/redcap_output/redcap.csv: -------------------------------------------------------------------------------- 1 | record_id,gad7_1,gad7_2,gad7_3,gad7_4,gad7_5,gad7_6,gad7_7,GAD7_schema_start_time,GAD7_schema_end_time,GAD7_schema_duration 2 | reproschema_ui_to_redcap_example,1,0,1,1,3,1,2,2025-02-24T16:55:03.139Z,2025-02-24T16:55:14.139Z,0 3 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | doctests = True 3 | exclude = 4 | **/__init__.py 5 | **/tests/* 6 | *build/ 7 | docs/sphinxext/ 8 | docs/tools/ 9 | reproschema/_version.py 10 | reproschema/models/model.py 11 | max-line-length=79 12 | extend-ignore = B001, B006, B016, E501, E722, F821 13 | -------------------------------------------------------------------------------- /reproschema/tests/test_rs2redcap_data/test_redcap2rs/activities/autism_parenting_stress_index_apsi/items/apsi_date: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", 3 | "@type": "reproschema:Field", 4 | "ui": { 5 | "inputType": "text" 6 | }, 7 | "responseOptions": { 8 | "valueType": "xsd:string" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /reproschema/tests/test_rs2redcap_data/test_redcap2rs/activities/autism_parenting_stress_index_apsi/items/apsi_total: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", 3 | "@type": "reproschema:Field", 4 | "ui": { 5 | "inputType": "text" 6 | }, 7 | "responseOptions": { 8 | "valueType": "xsd:string" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /reproschema/tests/test_rs2redcap_data/test_redcap2rs/activities/autism_parenting_stress_index_apsi/items/record_id: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", 3 | "@type": "reproschema:Field", 4 | "ui": { 5 | "inputType": "text" 6 | }, 7 | "responseOptions": { 8 | "valueType": "xsd:string" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /reproschema/tests/test_rs2redcap_data/test_redcap2rs/activities/autism_parenting_stress_index_apsi/items/apsi_name_of_child: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", 3 | "@type": "reproschema:Field", 4 | "ui": { 5 | "inputType": "text" 6 | }, 7 | "responseOptions": { 8 | "valueType": "xsd:string" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /reproschema/tests/test_rs2redcap_data/test_redcap2rs/activities/autism_parenting_stress_index_apsi/items/apsi_person_completing: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", 3 | "@type": "reproschema:Field", 4 | "ui": { 5 | "inputType": "text" 6 | }, 7 | "responseOptions": { 8 | "valueType": "xsd:string" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /reproschema/tests/data/responses/response1.jsonld: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "../../contexts/generic", 3 | "@type": "reproschema:Response", 4 | "@id": "uuid:fc963765-1f1b-4ad9-bc4b-0d21b9de5acb", 5 | "wasAttributedTo": { 6 | "@id": "uuid:2b0aab3d-495f-4eee-98a5-4284b3268a56", 7 | "subject_id": "6aabb03d-9e5f-ae5e-c4a5-21b9b326868a" 8 | }, 9 | "isAbout": "../activities/items/item1.jsonld", 10 | "value": 1 11 | } 12 | -------------------------------------------------------------------------------- /docs/user_guide/output2redcap.md: -------------------------------------------------------------------------------- 1 | ## `output2redcap` 2 | The `output2redcap` function is designed to process the output from reproschema-ui into a REDCap CSV file as seen [here](reproschema/example/redcap). 3 | 4 | 5 | ### CLI Usage 6 | 7 | The `output2redcap` function has been integrated into a CLI tool, use the following command: 8 | ```bash 9 | reproschema output2redcap ./path/to/your_reproschema-ui_files ./path/to/directory_you_want_to_save_output 10 | ``` 11 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: reproschema-py 2 | nav: 3 | - Home: index.md 4 | - Installation: installation.md 5 | - User Guide: 6 | - Introduction: user_guide/index.md 7 | - CLI Usage: user_guide/cli_usage.md 8 | - Reproschema to REDCap: user_guide/reproschema2redcap.md 9 | - REDCap to Reproschema: user_guide/redcap2reproschema.md 10 | - Output to REDCap: user_guide/output2redcap.md 11 | - Reproschema to FHIR: user_guide/reproschema2fhir.md 12 | - About: about.md 13 | 14 | theme: readthedocs 15 | -------------------------------------------------------------------------------- /reproschema/tests/test_validate.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | 5 | from ..validate import validate, validate_dir 6 | 7 | 8 | def test_validate(): 9 | os.chdir(os.path.dirname(__file__)) 10 | assert validate_dir("data") 11 | 12 | 13 | def test_type_error(): 14 | os.chdir(os.path.dirname(__file__)) 15 | with pytest.raises(ValueError): 16 | validate_dir("contexts") 17 | 18 | 19 | def test_url(): 20 | url = "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc1/examples/activities/activity1.jsonld" 21 | assert validate(url) 22 | -------------------------------------------------------------------------------- /docs/user_guide/reproschema2fhir.md: -------------------------------------------------------------------------------- 1 | ## `reproschema2fhir` 2 | The `reproschema2fhir` function is designed to convert reproschema activities and items into a FHIR Questionnaire resource as seen [here](reproschema/example/fhir). 3 | 4 | ### CLI Usage 5 | 6 | The `reproschema2fhir` function has been integrated into a CLI tool, use the following command: 7 | ```bash 8 | reproschema reproschema2fhir ./path/to/your_reproschema_activities ./path/to/directory_you_want_to_save_output 9 | ``` 10 | ### Notes 11 | 1. The script requires an active internet connection to access the GitHub repository. 12 | -------------------------------------------------------------------------------- /reproschema/tests/data/activities/items/activity1_total_score: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "../../../contexts/generic", 3 | "@type": "reproschema:Field", 4 | "@id": "activity1_total_score", 5 | "prefLabel": "activity1_total_score", 6 | "description": "Score item for Activity 1", 7 | "schemaVersion": "1.0.0-rc4", 8 | "version": "0.0.1", 9 | "ui": { 10 | "inputType": "number", 11 | "readonlyValue": true 12 | }, 13 | "responseOptions": { 14 | "valueType": "xsd:integer", 15 | "minValue": 0, 16 | "maxValue": 3 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /reproschema/tests/test_redcap2rs_data/redcap2rs.yaml: -------------------------------------------------------------------------------- 1 | # Reproschema Protocol Configuration 2 | 3 | # Protocol Name: 4 | # Use underscores for spaces and avoid special characters. 5 | # This is the unique identifier for your protocol. 6 | protocol_name: "test_redcap2rs" # Example: "My_Protocol" 7 | 8 | # Protocol Display Name: 9 | # This name will be displayed in the application. 10 | protocol_display_name: "redcap protocols" 11 | 12 | # Protocol Description: 13 | # Provide a brief description of your protocol. 14 | protocol_description: "testing" # Example: "This protocol is for ..." 15 | 16 | redcap_version: "3.0.0" 17 | -------------------------------------------------------------------------------- /templates/redcap2rs.yaml: -------------------------------------------------------------------------------- 1 | # Reproschema Protocol Configuration 2 | 3 | # Protocol Name: 4 | # Use underscores for spaces and avoid special characters. 5 | # This is the unique identifier for your protocol. 6 | protocol_name: "your_protocol_name" # Example: "My_Protocol" 7 | 8 | # Protocol Display Name: 9 | # This name will be displayed in the application. 10 | protocol_display_name: "Your protocol display name" 11 | 12 | # Protocol Description: 13 | # Provide a brief description of your protocol. 14 | protocol_description: "Description for your protocol" # Example: "This protocol is for ..." 15 | 16 | redcap_version: "x.y.z" # Example: "3.0.0" 17 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Welcome to Reproschema-py 2 | 3 | The `reproschema-py` library provides a Python interface and a command-line tool to work with ReproSchema, a YAML-based framework for creating and managing reproducible research protocols. 4 | 5 | This documentation will guide you through the installation process, explain how to use the command-line interface, and provide information for developers who want to contribute to the project. 6 | 7 | ## Getting Started 8 | 9 | If you are new to `reproschema-py`, we recommend starting with the **[Installation](installation.md)** guide, followed by the **[User Guide](user_guide/index.md)** to learn about the available commands. 10 | -------------------------------------------------------------------------------- /reproschema/tests/data_test_nimh-minimal/test_nimh-minimal.yaml: -------------------------------------------------------------------------------- 1 | # Reproschema Protocol Configuration 2 | 3 | # Protocol Name: 4 | # Use underscores for spaces and avoid special characters. 5 | # This is the unique identifier for your protocol. 6 | protocol_name: "nimh_minimal" 7 | 8 | # Protocol Display Name: 9 | # This name will be displayed in the application. 10 | protocol_display_name: "NIMH collection" 11 | 12 | # Protocol Description: 13 | # Provide a brief description of your protocol. 14 | protocol_description: "Minimal list of data collection instruments that would be ideal for use by all mental health researchers conducting clinical research to facilitate and harmonize mental health data collection." 15 | 16 | redcap_version: "unknown" 17 | -------------------------------------------------------------------------------- /reproschema/tests/data/responses/responseActivity1.jsonld: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "../../contexts/generic", 3 | "@type": "reproschema:ResponseActivity", 4 | "@id": "uuid:96496a96-50d6-4b87-a65e-468420e7c80d", 5 | "used": [ 6 | "../activities/items/item1.jsonld", 7 | "../activities/activity1.jsonld", 8 | "../protocols/protocol1.jsonld" 9 | ], 10 | "inLanguage": "en", 11 | "startedAtTime": "2020-07-23T22:15:37.675Z", 12 | "endedAtTime": "2020-07-23T22:15:50.293Z", 13 | "wasAssociatedWith": { 14 | "version": "0.0.1", 15 | "url": "https://schema.repronim.org/ui/#/", 16 | "@id": "https://github.com/ReproNim/reproschema-ui" 17 | }, 18 | "prov:generated": "uuid:85d18360-1787-4d3d-b72e-b543ca9b4b1a" 19 | } 20 | -------------------------------------------------------------------------------- /.github/workflows/deploy_mkdocs.yml: -------------------------------------------------------------------------------- 1 | name: Publish docs via GitHub Pages 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | jobs: 8 | build: 9 | name: Deploy docs 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: write 13 | steps: 14 | - name: Checkout main 15 | uses: actions/checkout@v4 16 | 17 | - name: Setup Python 18 | uses: actions/setup-python@v5 19 | with: 20 | python-version: '3.10' 21 | 22 | - name: Install dependencies 23 | run: | 24 | pip install mkdocs mkdocs-material 25 | 26 | - name: Deploy docs 27 | run: | 28 | mkdocs gh-deploy --force --clean 29 | env: 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | -------------------------------------------------------------------------------- /reproschema/tests/contexts/base: -------------------------------------------------------------------------------- 1 | { 2 | "@context": { 3 | "@version": 1.1, 4 | "dct": "http://purl.org/dc/terms/", 5 | "owl": "http://www.w3.org/2002/07/owl#", 6 | "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", 7 | "rdfa": "http://www.w3.org/ns/rdfa#", 8 | "rdfs": "http://www.w3.org/2000/01/rdf-schema#", 9 | "schema": "http://schema.org/", 10 | "xsd": "http://www.w3.org/2001/XMLSchema#", 11 | "skos": "http://www.w3.org/2004/02/skos/core#", 12 | "prov": "http://www.w3.org/ns/prov#", 13 | "pav": "http://purl.org/pav/", 14 | "nidm": "http://purl.org/nidash/nidm#", 15 | "uuid": "http://uuid.repronim.org/", 16 | "reproschema": "http://schema.repronim.org/" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /reproschema/tests/test_reproschema2fhir_data/activities/q_generic_gad7_anxiety/items/gad_7_started_at: -------------------------------------------------------------------------------- 1 | { 2 | "id": "gad_7_started_at", 3 | "category": "reproschema:Item", 4 | "additionalNotesObj": [ 5 | { 6 | "column": "Question Number (surveys only)", 7 | "source": "redcap", 8 | "value": NaN 9 | } 10 | ], 11 | "prefLabel": { 12 | "en": "gad_7_started_at" 13 | }, 14 | "question": { 15 | "en": "Questionnaire Started At" 16 | }, 17 | "responseOptions": { 18 | "valueType": [ 19 | "xsd:string" 20 | ] 21 | }, 22 | "ui": { 23 | "inputType": "text", 24 | "readonlyValue": true 25 | }, 26 | "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/main/releases/1.0.0/reproschema" 27 | } 28 | -------------------------------------------------------------------------------- /reproschema/example/fhir/reproschema2fhir_data_example/activities/q_generic_gad7_anxiety/items/gad_7_started_at: -------------------------------------------------------------------------------- 1 | { 2 | "id": "gad_7_started_at", 3 | "category": "reproschema:Item", 4 | "additionalNotesObj": [ 5 | { 6 | "column": "Question Number (surveys only)", 7 | "source": "redcap", 8 | "value": NaN 9 | } 10 | ], 11 | "prefLabel": { 12 | "en": "gad_7_started_at" 13 | }, 14 | "question": { 15 | "en": "Questionnaire Started At" 16 | }, 17 | "responseOptions": { 18 | "valueType": [ 19 | "xsd:string" 20 | ] 21 | }, 22 | "ui": { 23 | "inputType": "text", 24 | "readonlyValue": true 25 | }, 26 | "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/main/releases/1.0.0/reproschema" 27 | } 28 | -------------------------------------------------------------------------------- /reproschema/tests/test_reproschema2fhir_data/activities/q_generic_gad7_anxiety/items/gad_7_completed_at: -------------------------------------------------------------------------------- 1 | { 2 | "id": "gad_7_completed_at", 3 | "category": "reproschema:Item", 4 | "additionalNotesObj": [ 5 | { 6 | "column": "Question Number (surveys only)", 7 | "source": "redcap", 8 | "value": NaN 9 | } 10 | ], 11 | "prefLabel": { 12 | "en": "gad_7_completed_at" 13 | }, 14 | "question": { 15 | "en": "Questionnaire Completed At" 16 | }, 17 | "responseOptions": { 18 | "valueType": [ 19 | "xsd:string" 20 | ] 21 | }, 22 | "ui": { 23 | "inputType": "text", 24 | "readonlyValue": true 25 | }, 26 | "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/main/releases/1.0.0/reproschema" 27 | } 28 | -------------------------------------------------------------------------------- /reproschema/example/fhir/reproschema2fhir_data_example/activities/q_generic_gad7_anxiety/items/gad_7_completed_at: -------------------------------------------------------------------------------- 1 | { 2 | "id": "gad_7_completed_at", 3 | "category": "reproschema:Item", 4 | "additionalNotesObj": [ 5 | { 6 | "column": "Question Number (surveys only)", 7 | "source": "redcap", 8 | "value": NaN 9 | } 10 | ], 11 | "prefLabel": { 12 | "en": "gad_7_completed_at" 13 | }, 14 | "question": { 15 | "en": "Questionnaire Completed At" 16 | }, 17 | "responseOptions": { 18 | "valueType": [ 19 | "xsd:string" 20 | ] 21 | }, 22 | "ui": { 23 | "inputType": "text", 24 | "readonlyValue": true 25 | }, 26 | "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/main/releases/1.0.0/reproschema" 27 | } 28 | -------------------------------------------------------------------------------- /reproschema/tests/data_test_nimh-minimal/nimh_minimal/nimh_minimal/README.md: -------------------------------------------------------------------------------- 1 |
3 |
10 |
11 |
12 |
13 |
14 | 15 | # Welcome to NIMH Required Data Elements Collection 16 | 17 | Minimal list of data collection instruments that would be ideal for use by all mental 18 | health researchers conducting clinical research to facilitate and harmonize mental health 19 | data collection. 20 | -------------------------------------------------------------------------------- /docs/about.md: -------------------------------------------------------------------------------- 1 | # About this documentation 2 | 3 | This section provides information about the structure of this documentation site. 4 | 5 | ## Project layout 6 | 7 | The documentation is organized as follows: 8 | 9 | mkdocs.yml # The MkDocs configuration file. 10 | docs/ 11 | index.md # The documentation homepage. 12 | installation.md # Installation instructions. 13 | about.md # Information about this documentation. 14 | user_guide/ 15 | index.md # Introduction to the user guide. 16 | cli_usage.md # General CLI usage. 17 | reproschema2redcap.md # reproschema to REDCap conversion. 18 | redcap2reproschema.md # REDCap to reproschema conversion. 19 | output2redcap.md # output to REDCap conversion. 20 | reproschema2fhir.md # reproschema to FHIR conversion. 21 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | ## Installation 2 | 3 | Use the following command to install reproschema: 4 | 5 | ``` 6 | pip install reproschema 7 | ``` 8 | 9 | ### Developer installation 10 | 11 | Fork this repo to your own GitHub account, then clone and install your forked repo in the developer mode: 12 | 13 | ``` 14 | git clone https://github.com//reproschema-py.git 15 | cd reproschema-py 16 | pip install -e . 17 | ``` 18 | #### Style 19 | This repo uses pre-commit to check styling. 20 | - Install pre-commit with pip: `pip install pre-commit` 21 | - In order to use it with the repository, you have to run `pre-commit install` in the root directory the first time you use it. 22 | 23 | When pre-commit is used, you may have to run git commit twice, 24 | since pre-commit may make additional changes to your code for styling and will 25 | not commit these changes by default. 26 | -------------------------------------------------------------------------------- /reproschema/tests/test_reproschema2fhir_data/activities/q_generic_gad7_anxiety/items/gad_7_duration: -------------------------------------------------------------------------------- 1 | { 2 | "id": "gad_7_duration", 3 | "category": "reproschema:Item", 4 | "additionalNotesObj": [ 5 | { 6 | "column": "Question Number (surveys only)", 7 | "source": "redcap", 8 | "value": NaN 9 | } 10 | ], 11 | "prefLabel": { 12 | "en": "gad_7_duration" 13 | }, 14 | "question": { 15 | "en": "Questionnaire Duration (seconds)" 16 | }, 17 | "responseOptions": { 18 | "maxValue": NaN, 19 | "valueType": [ 20 | "xsd:decimal" 21 | ] 22 | }, 23 | "ui": { 24 | "inputType": "float", 25 | "readonlyValue": true 26 | }, 27 | "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/main/releases/1.0.0/reproschema" 28 | } 29 | -------------------------------------------------------------------------------- /reproschema/example/fhir/reproschema2fhir_data_example/activities/q_generic_gad7_anxiety/items/gad_7_duration: -------------------------------------------------------------------------------- 1 | { 2 | "id": "gad_7_duration", 3 | "category": "reproschema:Item", 4 | "additionalNotesObj": [ 5 | { 6 | "column": "Question Number (surveys only)", 7 | "source": "redcap", 8 | "value": NaN 9 | } 10 | ], 11 | "prefLabel": { 12 | "en": "gad_7_duration" 13 | }, 14 | "question": { 15 | "en": "Questionnaire Duration (seconds)" 16 | }, 17 | "responseOptions": { 18 | "maxValue": NaN, 19 | "valueType": [ 20 | "xsd:decimal" 21 | ] 22 | }, 23 | "ui": { 24 | "inputType": "float", 25 | "readonlyValue": true 26 | }, 27 | "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/main/releases/1.0.0/reproschema" 28 | } 29 | -------------------------------------------------------------------------------- /reproschema/tests/test_reproschema2fhir_data/activities/q_generic_gad7_anxiety/items/gad_7_session_id: -------------------------------------------------------------------------------- 1 | { 2 | "id": "gad_7_session_id", 3 | "category": "reproschema:Item", 4 | "additionalNotesObj": [ 5 | { 6 | "column": "Question Number (surveys only)", 7 | "source": "redcap", 8 | "value": NaN 9 | } 10 | ], 11 | "preamble": { 12 | "en": "Questionnaire - Metadata" 13 | }, 14 | "prefLabel": { 15 | "en": "gad_7_session_id" 16 | }, 17 | "question": { 18 | "en": "Session ID" 19 | }, 20 | "responseOptions": { 21 | "valueType": [ 22 | "xsd:string" 23 | ] 24 | }, 25 | "ui": { 26 | "inputType": "text", 27 | "readonlyValue": true 28 | }, 29 | "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/main/releases/1.0.0/reproschema" 30 | } 31 | -------------------------------------------------------------------------------- /reproschema/example/fhir/reproschema2fhir_data_example/activities/q_generic_gad7_anxiety/items/gad_7_session_id: -------------------------------------------------------------------------------- 1 | { 2 | "id": "gad_7_session_id", 3 | "category": "reproschema:Item", 4 | "additionalNotesObj": [ 5 | { 6 | "column": "Question Number (surveys only)", 7 | "source": "redcap", 8 | "value": NaN 9 | } 10 | ], 11 | "preamble": { 12 | "en": "Questionnaire - Metadata" 13 | }, 14 | "prefLabel": { 15 | "en": "gad_7_session_id" 16 | }, 17 | "question": { 18 | "en": "Session ID" 19 | }, 20 | "responseOptions": { 21 | "valueType": [ 22 | "xsd:string" 23 | ] 24 | }, 25 | "ui": { 26 | "inputType": "text", 27 | "readonlyValue": true 28 | }, 29 | "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/main/releases/1.0.0/reproschema" 30 | } 31 | -------------------------------------------------------------------------------- /reproschema/tests/test_rs2redcap_data/test_redcap2rs/activities/cognitive_and_affective_mindfulness_scalerevised_c/items/cams_r_1: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", 3 | "@type": "reproschema:Field", 4 | "ui": { 5 | "inputType": "radio" 6 | }, 7 | "responseOptions": { 8 | "valueType": "xsd:string", 9 | "choices": [ 10 | { 11 | "value": 1, 12 | "name": "1 - Rarely/Not at all " 13 | }, 14 | { 15 | "value": 2, 16 | "name": "2 - Sometimes " 17 | }, 18 | { 19 | "value": 3, 20 | "name": "3 - Often " 21 | }, 22 | { 23 | "value": 4, 24 | "name": "4 - Almost Always" 25 | } 26 | ] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /reproschema/tests/test_rs2redcap_data/test_redcap2rs/activities/cognitive_and_affective_mindfulness_scalerevised_c/items/cams_r_10: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", 3 | "@type": "reproschema:Field", 4 | "ui": { 5 | "inputType": "radio" 6 | }, 7 | "responseOptions": { 8 | "valueType": "xsd:string", 9 | "choices": [ 10 | { 11 | "value": 1, 12 | "name": "1 - Rarely/Not at all " 13 | }, 14 | { 15 | "value": 2, 16 | "name": "2 - Sometimes " 17 | }, 18 | { 19 | "value": 3, 20 | "name": "3 - Often " 21 | }, 22 | { 23 | "value": 4, 24 | "name": "4 - Almost Always" 25 | } 26 | ] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /reproschema/tests/test_rs2redcap_data/test_redcap2rs/activities/cognitive_and_affective_mindfulness_scalerevised_c/items/cams_r_11: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", 3 | "@type": "reproschema:Field", 4 | "ui": { 5 | "inputType": "radio" 6 | }, 7 | "responseOptions": { 8 | "valueType": "xsd:string", 9 | "choices": [ 10 | { 11 | "value": 1, 12 | "name": "1 - Rarely/Not at all " 13 | }, 14 | { 15 | "value": 2, 16 | "name": "2 - Sometimes " 17 | }, 18 | { 19 | "value": 3, 20 | "name": "3 - Often " 21 | }, 22 | { 23 | "value": 4, 24 | "name": "4 - Almost Always" 25 | } 26 | ] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /reproschema/tests/test_rs2redcap_data/test_redcap2rs/activities/cognitive_and_affective_mindfulness_scalerevised_c/items/cams_r_12: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", 3 | "@type": "reproschema:Field", 4 | "ui": { 5 | "inputType": "radio" 6 | }, 7 | "responseOptions": { 8 | "valueType": "xsd:string", 9 | "choices": [ 10 | { 11 | "value": 1, 12 | "name": "1 - Rarely/Not at all " 13 | }, 14 | { 15 | "value": 2, 16 | "name": "2 - Sometimes " 17 | }, 18 | { 19 | "value": 3, 20 | "name": "3 - Often " 21 | }, 22 | { 23 | "value": 4, 24 | "name": "4 - Almost Always" 25 | } 26 | ] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /reproschema/tests/test_rs2redcap_data/test_redcap2rs/activities/cognitive_and_affective_mindfulness_scalerevised_c/items/cams_r_2: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", 3 | "@type": "reproschema:Field", 4 | "ui": { 5 | "inputType": "radio" 6 | }, 7 | "responseOptions": { 8 | "valueType": "xsd:string", 9 | "choices": [ 10 | { 11 | "value": 1, 12 | "name": "1 - Rarely/Not at all " 13 | }, 14 | { 15 | "value": 2, 16 | "name": "2 - Sometimes " 17 | }, 18 | { 19 | "value": 3, 20 | "name": "3 - Often " 21 | }, 22 | { 23 | "value": 4, 24 | "name": "4 - Almost Always" 25 | } 26 | ] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /reproschema/tests/test_rs2redcap_data/test_redcap2rs/activities/cognitive_and_affective_mindfulness_scalerevised_c/items/cams_r_3: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", 3 | "@type": "reproschema:Field", 4 | "ui": { 5 | "inputType": "radio" 6 | }, 7 | "responseOptions": { 8 | "valueType": "xsd:string", 9 | "choices": [ 10 | { 11 | "value": 1, 12 | "name": "1 - Rarely/Not at all " 13 | }, 14 | { 15 | "value": 2, 16 | "name": "2 - Sometimes " 17 | }, 18 | { 19 | "value": 3, 20 | "name": "3 - Often " 21 | }, 22 | { 23 | "value": 4, 24 | "name": "4 - Almost Always" 25 | } 26 | ] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /reproschema/tests/test_rs2redcap_data/test_redcap2rs/activities/cognitive_and_affective_mindfulness_scalerevised_c/items/cams_r_4: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", 3 | "@type": "reproschema:Field", 4 | "ui": { 5 | "inputType": "radio" 6 | }, 7 | "responseOptions": { 8 | "valueType": "xsd:string", 9 | "choices": [ 10 | { 11 | "value": 1, 12 | "name": "1 - Rarely/Not at all " 13 | }, 14 | { 15 | "value": 2, 16 | "name": "2 - Sometimes " 17 | }, 18 | { 19 | "value": 3, 20 | "name": "3 - Often " 21 | }, 22 | { 23 | "value": 4, 24 | "name": "4 - Almost Always" 25 | } 26 | ] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /reproschema/tests/test_rs2redcap_data/test_redcap2rs/activities/cognitive_and_affective_mindfulness_scalerevised_c/items/cams_r_5: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", 3 | "@type": "reproschema:Field", 4 | "ui": { 5 | "inputType": "radio" 6 | }, 7 | "responseOptions": { 8 | "valueType": "xsd:string", 9 | "choices": [ 10 | { 11 | "value": 1, 12 | "name": "1 - Rarely/Not at all " 13 | }, 14 | { 15 | "value": 2, 16 | "name": "2 - Sometimes " 17 | }, 18 | { 19 | "value": 3, 20 | "name": "3 - Often " 21 | }, 22 | { 23 | "value": 4, 24 | "name": "4 - Almost Always" 25 | } 26 | ] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /reproschema/tests/test_rs2redcap_data/test_redcap2rs/activities/cognitive_and_affective_mindfulness_scalerevised_c/items/cams_r_6: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", 3 | "@type": "reproschema:Field", 4 | "ui": { 5 | "inputType": "radio" 6 | }, 7 | "responseOptions": { 8 | "valueType": "xsd:string", 9 | "choices": [ 10 | { 11 | "value": 1, 12 | "name": "1 - Rarely/Not at all " 13 | }, 14 | { 15 | "value": 2, 16 | "name": "2 - Sometimes " 17 | }, 18 | { 19 | "value": 3, 20 | "name": "3 - Often " 21 | }, 22 | { 23 | "value": 4, 24 | "name": "4 - Almost Always" 25 | } 26 | ] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /reproschema/tests/test_rs2redcap_data/test_redcap2rs/activities/cognitive_and_affective_mindfulness_scalerevised_c/items/cams_r_7: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", 3 | "@type": "reproschema:Field", 4 | "ui": { 5 | "inputType": "radio" 6 | }, 7 | "responseOptions": { 8 | "valueType": "xsd:string", 9 | "choices": [ 10 | { 11 | "value": 1, 12 | "name": "1 - Rarely/Not at all " 13 | }, 14 | { 15 | "value": 2, 16 | "name": "2 - Sometimes " 17 | }, 18 | { 19 | "value": 3, 20 | "name": "3 - Often " 21 | }, 22 | { 23 | "value": 4, 24 | "name": "4 - Almost Always" 25 | } 26 | ] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /reproschema/tests/test_rs2redcap_data/test_redcap2rs/activities/cognitive_and_affective_mindfulness_scalerevised_c/items/cams_r_8: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", 3 | "@type": "reproschema:Field", 4 | "ui": { 5 | "inputType": "radio" 6 | }, 7 | "responseOptions": { 8 | "valueType": "xsd:string", 9 | "choices": [ 10 | { 11 | "value": 1, 12 | "name": "1 - Rarely/Not at all " 13 | }, 14 | { 15 | "value": 2, 16 | "name": "2 - Sometimes " 17 | }, 18 | { 19 | "value": 3, 20 | "name": "3 - Often " 21 | }, 22 | { 23 | "value": 4, 24 | "name": "4 - Almost Always" 25 | } 26 | ] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /reproschema/tests/test_rs2redcap_data/test_redcap2rs/activities/cognitive_and_affective_mindfulness_scalerevised_c/items/cams_r_9: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", 3 | "@type": "reproschema:Field", 4 | "ui": { 5 | "inputType": "radio" 6 | }, 7 | "responseOptions": { 8 | "valueType": "xsd:string", 9 | "choices": [ 10 | { 11 | "value": 1, 12 | "name": "1 - Rarely/Not at all " 13 | }, 14 | { 15 | "value": 2, 16 | "name": "2 - Sometimes " 17 | }, 18 | { 19 | "value": 3, 20 | "name": "3 - Often " 21 | }, 22 | { 23 | "value": 4, 24 | "name": "4 - Almost Always" 25 | } 26 | ] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # This workflows will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | name: Upload Python Package 5 | 6 | on: 7 | release: 8 | types: [published] 9 | 10 | jobs: 11 | deploy: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: Set up Python 18 | uses: actions/setup-python@v4 19 | with: 20 | python-version: '3.x' 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install setuptools twine pyyaml build 25 | - name: Build and publish 26 | env: 27 | TWINE_USERNAME: __token__ 28 | TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} 29 | run: | 30 | python -m build 31 | twine upload dist/* 32 | -------------------------------------------------------------------------------- /reproschema/tests/test_rs2redcap_data/test_redcap2rs/activities/autism_parenting_stress_index_apsi/items/apsi_accepted: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", 3 | "@type": "reproschema:Field", 4 | "ui": { 5 | "inputType": "radio" 6 | }, 7 | "responseOptions": { 8 | "valueType": "xsd:string", 9 | "choices": [ 10 | { 11 | "value": 0, 12 | "name": "0 - Not stressful " 13 | }, 14 | { 15 | "value": 1, 16 | "name": "1 - Sometimes creates stress " 17 | }, 18 | { 19 | "value": 2, 20 | "name": "2 - Often creates stress " 21 | }, 22 | { 23 | "value": 3, 24 | "name": "3 - Very stressful on a daily basis " 25 | }, 26 | { 27 | "value": 5, 28 | "name": "5 - So stressful sometimes we feel we can't cope" 29 | } 30 | ] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /reproschema/tests/test_rs2redcap_data/test_redcap2rs/activities/autism_parenting_stress_index_apsi/items/apsi_bowel: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", 3 | "@type": "reproschema:Field", 4 | "ui": { 5 | "inputType": "radio" 6 | }, 7 | "responseOptions": { 8 | "valueType": "xsd:string", 9 | "choices": [ 10 | { 11 | "value": 0, 12 | "name": "0 - Not stressful " 13 | }, 14 | { 15 | "value": 1, 16 | "name": "1 - Sometimes creates stress " 17 | }, 18 | { 19 | "value": 2, 20 | "name": "2 - Often creates stress " 21 | }, 22 | { 23 | "value": 3, 24 | "name": "3 - Very stressful on a daily basis " 25 | }, 26 | { 27 | "value": 5, 28 | "name": "5 - So stressful sometimes we feel we can't cope" 29 | } 30 | ] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /reproschema/tests/test_rs2redcap_data/test_redcap2rs/activities/autism_parenting_stress_index_apsi/items/apsi_diet: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", 3 | "@type": "reproschema:Field", 4 | "ui": { 5 | "inputType": "radio" 6 | }, 7 | "responseOptions": { 8 | "valueType": "xsd:string", 9 | "choices": [ 10 | { 11 | "value": 0, 12 | "name": "0 - Not stressful " 13 | }, 14 | { 15 | "value": 1, 16 | "name": "1 - Sometimes creates stress " 17 | }, 18 | { 19 | "value": 2, 20 | "name": "2 - Often creates stress " 21 | }, 22 | { 23 | "value": 3, 24 | "name": "3 - Very stressful on a daily basis " 25 | }, 26 | { 27 | "value": 5, 28 | "name": "5 - So stressful sometimes we feel we can't cope" 29 | } 30 | ] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /reproschema/tests/test_rs2redcap_data/test_redcap2rs/activities/autism_parenting_stress_index_apsi/items/apsi_potty: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", 3 | "@type": "reproschema:Field", 4 | "ui": { 5 | "inputType": "radio" 6 | }, 7 | "responseOptions": { 8 | "valueType": "xsd:string", 9 | "choices": [ 10 | { 11 | "value": 0, 12 | "name": "0 - Not stressful " 13 | }, 14 | { 15 | "value": 1, 16 | "name": "1 - Sometimes creates stress " 17 | }, 18 | { 19 | "value": 2, 20 | "name": "2 - Often creates stress " 21 | }, 22 | { 23 | "value": 3, 24 | "name": "3 - Very stressful on a daily basis " 25 | }, 26 | { 27 | "value": 5, 28 | "name": "5 - So stressful sometimes we feel we can't cope" 29 | } 30 | ] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /reproschema/tests/test_rs2redcap_data/test_redcap2rs/activities/autism_parenting_stress_index_apsi/items/apsi_sleep: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", 3 | "@type": "reproschema:Field", 4 | "ui": { 5 | "inputType": "radio" 6 | }, 7 | "responseOptions": { 8 | "valueType": "xsd:string", 9 | "choices": [ 10 | { 11 | "value": 0, 12 | "name": "0 - Not stressful " 13 | }, 14 | { 15 | "value": 1, 16 | "name": "1 - Sometimes creates stress " 17 | }, 18 | { 19 | "value": 2, 20 | "name": "2 - Often creates stress " 21 | }, 22 | { 23 | "value": 3, 24 | "name": "3 - Very stressful on a daily basis " 25 | }, 26 | { 27 | "value": 5, 28 | "name": "5 - So stressful sometimes we feel we can't cope" 29 | } 30 | ] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /reproschema/tests/test_rs2redcap_data/test_redcap2rs/activities/autism_parenting_stress_index_apsi/items/apsi_tantrums: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", 3 | "@type": "reproschema:Field", 4 | "ui": { 5 | "inputType": "radio" 6 | }, 7 | "responseOptions": { 8 | "valueType": "xsd:string", 9 | "choices": [ 10 | { 11 | "value": 0, 12 | "name": "0 - Not stressful " 13 | }, 14 | { 15 | "value": 1, 16 | "name": "1 - Sometimes creates stress " 17 | }, 18 | { 19 | "value": 2, 20 | "name": "2 - Often creates stress " 21 | }, 22 | { 23 | "value": 3, 24 | "name": "3 - Very stressful on a daily basis " 25 | }, 26 | { 27 | "value": 5, 28 | "name": "5 - So stressful sometimes we feel we can't cope" 29 | } 30 | ] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /reproschema/tests/test_rs2redcap_data/test_redcap2rs/activities/autism_parenting_stress_index_apsi/items/apsi_agressive: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", 3 | "@type": "reproschema:Field", 4 | "ui": { 5 | "inputType": "radio" 6 | }, 7 | "responseOptions": { 8 | "valueType": "xsd:string", 9 | "choices": [ 10 | { 11 | "value": 0, 12 | "name": "0 - Not stressful " 13 | }, 14 | { 15 | "value": 1, 16 | "name": "1 - Sometimes creates stress " 17 | }, 18 | { 19 | "value": 2, 20 | "name": "2 - Often creates stress " 21 | }, 22 | { 23 | "value": 3, 24 | "name": "3 - Very stressful on a daily basis " 25 | }, 26 | { 27 | "value": 5, 28 | "name": "5 - So stressful sometimes we feel we can't cope" 29 | } 30 | ] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /reproschema/tests/test_rs2redcap_data/test_redcap2rs/activities/autism_parenting_stress_index_apsi/items/apsi_communicate: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", 3 | "@type": "reproschema:Field", 4 | "ui": { 5 | "inputType": "radio" 6 | }, 7 | "responseOptions": { 8 | "valueType": "xsd:string", 9 | "choices": [ 10 | { 11 | "value": 0, 12 | "name": "0 - Not stressful " 13 | }, 14 | { 15 | "value": 1, 16 | "name": "1 - Sometimes creates stress " 17 | }, 18 | { 19 | "value": 2, 20 | "name": "2 - Often creates stress " 21 | }, 22 | { 23 | "value": 3, 24 | "name": "3 - Very stressful on a daily basis " 25 | }, 26 | { 27 | "value": 5, 28 | "name": "5 - So stressful sometimes we feel we can't cope" 29 | } 30 | ] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /reproschema/tests/test_rs2redcap_data/test_redcap2rs/activities/autism_parenting_stress_index_apsi/items/apsi_independently: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", 3 | "@type": "reproschema:Field", 4 | "ui": { 5 | "inputType": "radio" 6 | }, 7 | "responseOptions": { 8 | "valueType": "xsd:string", 9 | "choices": [ 10 | { 11 | "value": 0, 12 | "name": "0 - Not stressful " 13 | }, 14 | { 15 | "value": 1, 16 | "name": "1 - Sometimes creates stress " 17 | }, 18 | { 19 | "value": 2, 20 | "name": "2 - Often creates stress " 21 | }, 22 | { 23 | "value": 3, 24 | "name": "3 - Very stressful on a daily basis " 25 | }, 26 | { 27 | "value": 5, 28 | "name": "5 - So stressful sometimes we feel we can't cope" 29 | } 30 | ] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /reproschema/tests/test_rs2redcap_data/test_redcap2rs/activities/autism_parenting_stress_index_apsi/items/apsi_not_close: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", 3 | "@type": "reproschema:Field", 4 | "ui": { 5 | "inputType": "radio" 6 | }, 7 | "responseOptions": { 8 | "valueType": "xsd:string", 9 | "choices": [ 10 | { 11 | "value": 0, 12 | "name": "0 - Not stressful " 13 | }, 14 | { 15 | "value": 1, 16 | "name": "1 - Sometimes creates stress " 17 | }, 18 | { 19 | "value": 2, 20 | "name": "2 - Often creates stress " 21 | }, 22 | { 23 | "value": 3, 24 | "name": "3 - Very stressful on a daily basis " 25 | }, 26 | { 27 | "value": 5, 28 | "name": "5 - So stressful sometimes we feel we can't cope" 29 | } 30 | ] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /reproschema/tests/test_rs2redcap_data/test_redcap2rs/activities/autism_parenting_stress_index_apsi/items/apsi_self_injure: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", 3 | "@type": "reproschema:Field", 4 | "ui": { 5 | "inputType": "radio" 6 | }, 7 | "responseOptions": { 8 | "valueType": "xsd:string", 9 | "choices": [ 10 | { 11 | "value": 0, 12 | "name": "0 - Not stressful " 13 | }, 14 | { 15 | "value": 1, 16 | "name": "1 - Sometimes creates stress " 17 | }, 18 | { 19 | "value": 2, 20 | "name": "2 - Often creates stress " 21 | }, 22 | { 23 | "value": 3, 24 | "name": "3 - Very stressful on a daily basis " 25 | }, 26 | { 27 | "value": 5, 28 | "name": "5 - So stressful sometimes we feel we can't cope" 29 | } 30 | ] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /reproschema/tests/test_rs2redcap_data/test_redcap2rs/activities/autism_parenting_stress_index_apsi/items/apsi_social_dev: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", 3 | "@type": "reproschema:Field", 4 | "ui": { 5 | "inputType": "radio" 6 | }, 7 | "responseOptions": { 8 | "valueType": "xsd:string", 9 | "choices": [ 10 | { 11 | "value": 0, 12 | "name": "0 - Not stressful " 13 | }, 14 | { 15 | "value": 1, 16 | "name": "1 - Sometimes creates stress " 17 | }, 18 | { 19 | "value": 2, 20 | "name": "2 - Often creates stress " 21 | }, 22 | { 23 | "value": 3, 24 | "name": "3 - Very stressful on a daily basis " 25 | }, 26 | { 27 | "value": 5, 28 | "name": "5 - So stressful sometimes we feel we can't cope" 29 | } 30 | ] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /reproschema/tests/test_rs2redcap_data/test_redcap2rs/activities/autism_parenting_stress_index_apsi/items/apsi_transitions: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", 3 | "@type": "reproschema:Field", 4 | "ui": { 5 | "inputType": "radio" 6 | }, 7 | "responseOptions": { 8 | "valueType": "xsd:string", 9 | "choices": [ 10 | { 11 | "value": 0, 12 | "name": "0 - Not stressful " 13 | }, 14 | { 15 | "value": 1, 16 | "name": "1 - Sometimes creates stress " 17 | }, 18 | { 19 | "value": 2, 20 | "name": "2 - Often creates stress " 21 | }, 22 | { 23 | "value": 3, 24 | "name": "3 - Very stressful on a daily basis " 25 | }, 26 | { 27 | "value": 5, 28 | "name": "5 - So stressful sometimes we feel we can't cope" 29 | } 30 | ] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /docs/user_guide/cli_usage.md: -------------------------------------------------------------------------------- 1 | ## CLI usage 2 | 3 | This package installs `reproschema` Command Line Interface (CLI). 4 | 5 | ``` 6 | $ reproschema --help 7 | 8 | $ A client to support interactions with ReproSchema 9 | 10 | To see help for a specific command, run 11 | 12 | reproschema COMMAND --help e.g. reproschema validate --help 13 | 14 | Options: 15 | --version 16 | -l, --log-level [DEBUG|INFO|WARNING|ERROR|CRITICAL] 17 | Log level name [default: INFO] 18 | --help Show this message and exit. 19 | 20 | Commands: 21 | convert Converts a path to a different format, jsonld,... 22 | create 23 | migrate Updates to a new reproschema version 24 | redcap2reproschema Converts REDCap CSV files to Reproschema format. 25 | reproschema2redcap Converts reproschema protocol to REDCap CSV format. 26 | serve 27 | validate Validates if the path has a valid reproschema format 28 | reproschema2fhir Generates FHIR questionnaire resources from reproschema activities 29 | output2redcap Generates redcap csv given the audio and survey data from reproschema ui 30 | ``` 31 | -------------------------------------------------------------------------------- /reproschema/tests/test_reproschema2fhir.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | from shutil import copytree, rmtree 4 | 5 | import pytest 6 | from click.testing import CliRunner 7 | 8 | from ..cli import main 9 | 10 | 11 | def test_reproschemaui2redcap(tmpdir): 12 | runner = CliRunner() 13 | 14 | with runner.isolated_filesystem(): 15 | # Copy necessary test data into the isolated filesystem 16 | original_data_dir = os.path.join( 17 | os.path.dirname(__file__), "test_reproschema2fhir_data/activities" 18 | ) 19 | copytree(original_data_dir, "input_data") 20 | 21 | input_path = Path("input_data") 22 | output_path = os.path.join(tmpdir) 23 | 24 | result = runner.invoke( 25 | main, ["reproschema2fhir", str(input_path), str(output_path)] 26 | ) 27 | print("input", original_data_dir) 28 | print("output", output_path) 29 | 30 | assert result.exit_code == 0 31 | 32 | assert os.path.exists(output_path) 33 | assert len(os.listdir(output_path)) > 0 34 | 35 | # Clean up temporary directory after use (optional) 36 | # rmtree(tmpdir) 37 | -------------------------------------------------------------------------------- /reproschema/tests/data/activities/items/item2.jsonld: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "../../../contexts/generic", 3 | "@type": "reproschema:Field", 4 | "@id": "item2.jsonld", 5 | "prefLabel": "item2", 6 | "description": "Q2 of example 1", 7 | "schemaVersion": "1.0.0-rc4", 8 | "version": "0.0.1", 9 | "question": { 10 | "en": "Current temperature.", 11 | "es": "Fiebre actual." 12 | }, 13 | "video": { 14 | "@type": "VideoObject", 15 | "contentUrl": "http://media.freesound.org/data/0/previews/719__elmomo__12oclock_girona_preview.mp4" 16 | }, 17 | "imageUrl": "http://example.com/sample-image.jpg", 18 | "ui": { 19 | "inputType": "float" 20 | }, 21 | "responseOptions": { 22 | "valueType": "xsd:float", 23 | "unitOptions": [ 24 | { 25 | "prefLabel": { 26 | "en": "Fahrenheit", 27 | "es": "Fahrenheit" 28 | }, 29 | "value": "°F" 30 | }, 31 | { 32 | "prefLabel": { 33 | "en": "Celsius", 34 | "es": "Celsius" 35 | }, 36 | "value": "°C" 37 | } 38 | ] 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /reproschema/tests/data_test_nimh-minimal/nimh_minimal/activities/demo/demo_schema: -------------------------------------------------------------------------------- 1 | { 2 | "@context": [ 3 | "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", 4 | { 5 | "demo": "https://raw.githubusercontent.com/ReproNim/reproschema-library/80867e36fb2c00563290486bf3f3bbeb3198f5cb/activities/NDA/items/" 6 | } 7 | ], 8 | "@type": "reproschema:Activity", 9 | "@id": "demo_schema", 10 | "prefLabel": {"en": "Demographics" }, 11 | "description": "Demographic information", 12 | "preamble": " ", 13 | "schemaVersion": "1.0.0-rc4", 14 | "version": "0.0.1", 15 | "ui": { 16 | "addProperties": [ 17 | { 18 | "isAbout": "demo:interview_age", 19 | "variableName": "interview_age" 20 | }, 21 | { 22 | "isAbout": "demo:sex", 23 | "variableName": "sex" 24 | } 25 | ], 26 | "order": [ 27 | "demo:interview_age", 28 | "demo:sex" 29 | ], 30 | "shuffle": false, 31 | "allow": [ 32 | "reproschema:AutoAdvance", 33 | "reproschema:AllowExport" 34 | ] 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /.github/workflows/package.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Python package 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ${{ matrix.os }} 16 | strategy: 17 | matrix: 18 | os: [macos-latest, ubuntu-latest] 19 | python-version: ["3.10", "3.11", "3.12", "3.13"] 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | - name: Set up Python ${{ matrix.python-version }} 24 | uses: actions/setup-python@v5 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | allow-prereleases: true 28 | - name: Install dependencies 29 | run: | 30 | python -m pip install --upgrade pip setuptools 31 | pip install .[test] 32 | - name: Test with pytest 33 | run: | 34 | reproschema -l DEBUG validate reproschema/tests/data 35 | reproschema -l DEBUG convert reproschema/tests/data/protocols/protocol1.jsonld 36 | pytest reproschema 37 | -------------------------------------------------------------------------------- /reproschema/tests/data/protocols/protocol1.jsonld: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "../../contexts/generic", 3 | "@type": "reproschema:Protocol", 4 | "@id": "protocol1.jsonld", 5 | "prefLabel": { 6 | "en": "Protocol1", 7 | "es": "Protocol1_es" 8 | }, 9 | "description": "example Protocol", 10 | "schemaVersion": "1.0.0-rc4", 11 | "version": "0.0.1", 12 | "landingPage": {"@id": "http://example.com/sample-readme.md", 13 | "inLanguage": "en"}, 14 | "messages": [ 15 | { 16 | "message": "Test message: Triggered when item1 value is greater than 0", 17 | "jsExpression": "item1 > 0" 18 | } 19 | ], 20 | "ui": { 21 | "addProperties": [ 22 | { 23 | "isAbout": "../activities/activity1.jsonld", 24 | "variableName": "activity1", 25 | "prefLabel": { 26 | "en": "Screening", 27 | "es": "Screening_es" 28 | }, 29 | "isVis": true, 30 | "schedule": "R5/2008-01-01T13:00:00Z/P1Y2M10DT2H30M", 31 | "randomMaxDelay": "PT12H", 32 | "limit": "P1W/2020-08-01T13:00:00Z" 33 | } 34 | ], 35 | "order": [ 36 | "../activities/activity1.jsonld" 37 | ], 38 | "shuffle": false, 39 | "allow": [ 40 | "reproschema:AutoAdvance", 41 | "reproschema:DisableBack", 42 | "reproschema:AllowExport" 43 | ] 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /docs/user_guide/reproschema2redcap.md: -------------------------------------------------------------------------------- 1 | ## `reproschema2redcap` 2 | 3 | ### CLI Usage 4 | 5 | You can use this feature directly from the command line. To convert ReproSchema protocol to REDCap CSV format, use the following command 6 | 7 | ``` 8 | reproschema reproschema2redcap 9 | ``` 10 | 11 | - ``: The path to the root folder of a protocol. For example, to convert the reproschema-demo-protocol provided by ReproNim, you can use the following commands: 12 | ```bash 13 | git clone https://github.com/ReproNim/reproschema-demo-protocol.git 14 | cd reproschema-demo-protocol 15 | pwd 16 | ``` 17 | In this case, the output from `pwd` (which shows your current directory path) should be your ``. 18 | - ``: The name of the output CSV file where the converted data will be saved. 19 | 20 | ### Python Function Usage 21 | 22 | You can also use the `reproschema2redcap` function from the `reproschema-py` package in your Python code. 23 | 24 | ```python 25 | from reproschema import reproschema2redcap 26 | 27 | input_dir_path = "path-to/reproschema-demo-protocol" 28 | output_csv_filename = "output.csv" 29 | 30 | reproschema2redcap(input_dir_path, output_csv_filename) 31 | ``` 32 | -------------------------------------------------------------------------------- /reproschema/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | from reproschema._version import __version__ 5 | 6 | # 7 | # Basic logger configuration 8 | # 9 | 10 | 11 | def get_logger(name=None): 12 | """Return a logger to use""" 13 | return logging.getLogger("reproschema" + (".%s" % name if name else "")) 14 | 15 | 16 | def set_logger_level(lgr, level): 17 | if isinstance(level, int): 18 | pass 19 | elif level.isnumeric(): 20 | level = int(level) 21 | elif level.isalpha(): 22 | level = getattr(logging, level) 23 | else: 24 | lgr.warning("Do not know how to treat loglevel %s" % level) 25 | return 26 | lgr.setLevel(level) 27 | 28 | 29 | _DEFAULT_LOG_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" 30 | 31 | lgr = get_logger() 32 | # Basic settings for output, for now just basic 33 | set_logger_level(lgr, os.environ.get("REPROSCHEMA_LOG_LEVEL", logging.INFO)) 34 | FORMAT = "%(asctime)-15s [%(levelname)8s] %(message)s" 35 | logging.basicConfig(format=FORMAT) 36 | 37 | try: 38 | import etelemetry 39 | 40 | etelemetry.check_available_version( 41 | "repronim/reproschema-py", __version__, lgr=lgr 42 | ) 43 | except Exception as exc: 44 | lgr.warning( 45 | "Failed to check for a more recent version available with etelemetry: %s", 46 | exc, 47 | ) 48 | -------------------------------------------------------------------------------- /reproschema/tests/test_convert.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | 5 | from ..jsonldutils import to_newformat 6 | 7 | 8 | @pytest.fixture 9 | def filename(): 10 | ldfile = os.path.join( 11 | os.path.dirname(__file__), "data", "protocols", "protocol1.jsonld" 12 | ) 13 | cwd = os.getcwd() 14 | ldfile = ldfile.replace(f"{cwd}/", "") 15 | return ldfile 16 | 17 | 18 | def test_jsonld(filename): 19 | value = to_newformat(filename, "jsonld") 20 | import json 21 | 22 | json.loads(value) 23 | 24 | 25 | def test_ntriples(filename): 26 | value = to_newformat(filename, "n-triples") 27 | for line in value.splitlines(): 28 | if line: 29 | assert line.strip().endswith(".") 30 | 31 | 32 | def test_turtle(filename): 33 | value = to_newformat(filename, "turtle") 34 | import rdflib as rl 35 | 36 | g = rl.Graph() 37 | g.parse(data=value, format="turtle") 38 | 39 | 40 | def test_type_error(filename): 41 | with pytest.raises(ValueError): 42 | to_newformat(filename, "snapturtle") 43 | 44 | 45 | def test_convert_url(): 46 | url = "https://raw.githubusercontent.com/ReproNim/reproschema-py/master/reproschema/tests/data/activities/activity1.jsonld" 47 | value = to_newformat(url, "turtle") 48 | import rdflib as rl 49 | 50 | g = rl.Graph() 51 | g.parse(data=value, format="turtle") 52 | -------------------------------------------------------------------------------- /reproschema/tests/test_reproschema2redcap.py: -------------------------------------------------------------------------------- 1 | import csv 2 | import os 3 | from pathlib import Path 4 | from shutil import copytree, rmtree 5 | 6 | import pytest 7 | from click.testing import CliRunner 8 | 9 | from ..cli import main 10 | 11 | 12 | def test_reproschema2redcap(tmpdir): 13 | runner = CliRunner() 14 | 15 | with runner.isolated_filesystem(): 16 | # Copy necessary test data into the isolated filesystem 17 | original_data_dir = os.path.join( 18 | os.path.dirname(__file__), "test_rs2redcap_data", "test_redcap2rs" 19 | ) 20 | copytree(original_data_dir, "input_data") 21 | 22 | input_path = Path("input_data") 23 | output_csv_path = os.path.join(tmpdir, "output.csv") 24 | 25 | result = runner.invoke( 26 | main, ["reproschema2redcap", str(input_path), output_csv_path] 27 | ) 28 | print("input", original_data_dir) 29 | print("output", output_csv_path) 30 | 31 | assert result.exit_code == 0 32 | 33 | assert os.path.exists(output_csv_path) 34 | 35 | with open(output_csv_path, "r", encoding="utf-8") as csv_file: 36 | reader = csv.reader(csv_file) 37 | csv_contents = list(reader) 38 | 39 | assert ( 40 | len(csv_contents) > 1 41 | ) # More than one row indicates content beyond headers 42 | 43 | # Clean up temporary directory after use (optional) 44 | # rmtree(tmpdir) 45 | -------------------------------------------------------------------------------- /reproschema/models/utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from .model import ( 4 | Activity, 5 | Item, 6 | Protocol, 7 | Response, 8 | ResponseActivity, 9 | ResponseOption, 10 | ) 11 | 12 | 13 | def identify_model_class(category): 14 | if ( 15 | category == "http://schema.repronim.org/Field" 16 | or category == "http://schema.repronim.org/Item" 17 | ): 18 | model_class = Item 19 | elif category == "http://schema.repronim.org/ResponseOption": 20 | model_class = ResponseOption 21 | elif category == "http://schema.repronim.org/Activity": 22 | model_class = Activity 23 | elif category == "http://schema.repronim.org/Protocol": 24 | model_class = Protocol 25 | elif category == "http://schema.repronim.org/ResponseActivity": 26 | model_class = ResponseActivity 27 | elif category == "http://schema.repronim.org/Response": 28 | model_class = Response 29 | else: 30 | raise ValueError(f"Unknown type: {category}") 31 | return model_class 32 | 33 | 34 | def write_obj_jsonld(model_obj, path, contextfile_url=None): 35 | """Write a pydantic model object to a jsonld file.""" 36 | # TODO: perhaps automatically should take contextfile 37 | model_dict = model_obj.model_dump( 38 | exclude_unset=True, 39 | ) 40 | model_dict["@context"] = contextfile_url 41 | 42 | with open(path, "w") as f: 43 | json.dump(model_dict, f, indent=4) 44 | return path 45 | -------------------------------------------------------------------------------- /reproschema/tests/test_reproschema2fhir_data/activities/q_generic_gad7_anxiety/items/trouble_relaxing: -------------------------------------------------------------------------------- 1 | { 2 | "id": "trouble_relaxing", 3 | "category": "reproschema:Item", 4 | "additionalNotesObj": [ 5 | { 6 | "column": "Question Number (surveys only)", 7 | "source": "redcap", 8 | "value": NaN 9 | } 10 | ], 11 | "prefLabel": { 12 | "en": "trouble_relaxing" 13 | }, 14 | "question": { 15 | "en": "Trouble relaxing." 16 | }, 17 | "responseOptions": { 18 | "choices": [ 19 | { 20 | "name": { 21 | "en": "Not at all" 22 | }, 23 | "value": 0 24 | }, 25 | { 26 | "name": { 27 | "en": "Several days" 28 | }, 29 | "value": 1 30 | }, 31 | { 32 | "name": { 33 | "en": "More than half the days" 34 | }, 35 | "value": 2 36 | }, 37 | { 38 | "name": { 39 | "en": "Nearly every day" 40 | }, 41 | "value": 3 42 | } 43 | ], 44 | "valueType": [ 45 | "xsd:integer" 46 | ] 47 | }, 48 | "ui": { 49 | "inputType": "radio" 50 | }, 51 | "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/main/releases/1.0.0/reproschema" 52 | } 53 | -------------------------------------------------------------------------------- /reproschema/example/fhir/reproschema2fhir_data_example/activities/q_generic_gad7_anxiety/items/trouble_relaxing: -------------------------------------------------------------------------------- 1 | { 2 | "id": "trouble_relaxing", 3 | "category": "reproschema:Item", 4 | "additionalNotesObj": [ 5 | { 6 | "column": "Question Number (surveys only)", 7 | "source": "redcap", 8 | "value": NaN 9 | } 10 | ], 11 | "prefLabel": { 12 | "en": "trouble_relaxing" 13 | }, 14 | "question": { 15 | "en": "Trouble relaxing." 16 | }, 17 | "responseOptions": { 18 | "choices": [ 19 | { 20 | "name": { 21 | "en": "Not at all" 22 | }, 23 | "value": 0 24 | }, 25 | { 26 | "name": { 27 | "en": "Several days" 28 | }, 29 | "value": 1 30 | }, 31 | { 32 | "name": { 33 | "en": "More than half the days" 34 | }, 35 | "value": 2 36 | }, 37 | { 38 | "name": { 39 | "en": "Nearly every day" 40 | }, 41 | "value": 3 42 | } 43 | ], 44 | "valueType": [ 45 | "xsd:integer" 46 | ] 47 | }, 48 | "ui": { 49 | "inputType": "radio" 50 | }, 51 | "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/main/releases/1.0.0/reproschema" 52 | } 53 | -------------------------------------------------------------------------------- /reproschema/tests/test_reproschema2fhir_data/activities/q_generic_gad7_anxiety/items/easily_agitated: -------------------------------------------------------------------------------- 1 | { 2 | "id": "easily_agitated", 3 | "category": "reproschema:Item", 4 | "additionalNotesObj": [ 5 | { 6 | "column": "Question Number (surveys only)", 7 | "source": "redcap", 8 | "value": NaN 9 | } 10 | ], 11 | "prefLabel": { 12 | "en": "easily_agitated" 13 | }, 14 | "question": { 15 | "en": "Becoming easily annoyed or irritable." 16 | }, 17 | "responseOptions": { 18 | "choices": [ 19 | { 20 | "name": { 21 | "en": "Not at all" 22 | }, 23 | "value": 0 24 | }, 25 | { 26 | "name": { 27 | "en": "Several days" 28 | }, 29 | "value": 1 30 | }, 31 | { 32 | "name": { 33 | "en": "More than half the days" 34 | }, 35 | "value": 2 36 | }, 37 | { 38 | "name": { 39 | "en": "Nearly every day" 40 | }, 41 | "value": 3 42 | } 43 | ], 44 | "valueType": [ 45 | "xsd:integer" 46 | ] 47 | }, 48 | "ui": { 49 | "inputType": "radio" 50 | }, 51 | "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/main/releases/1.0.0/reproschema" 52 | } 53 | -------------------------------------------------------------------------------- /reproschema/tests/test_reproschema2fhir_data/activities/q_generic_gad7_anxiety/items/worry_too_much: -------------------------------------------------------------------------------- 1 | { 2 | "id": "worry_too_much", 3 | "category": "reproschema:Item", 4 | "additionalNotesObj": [ 5 | { 6 | "column": "Question Number (surveys only)", 7 | "source": "redcap", 8 | "value": NaN 9 | } 10 | ], 11 | "prefLabel": { 12 | "en": "worry_too_much" 13 | }, 14 | "question": { 15 | "en": "Worrying too much about different things." 16 | }, 17 | "responseOptions": { 18 | "choices": [ 19 | { 20 | "name": { 21 | "en": "Not at all" 22 | }, 23 | "value": 0 24 | }, 25 | { 26 | "name": { 27 | "en": "Several days" 28 | }, 29 | "value": 1 30 | }, 31 | { 32 | "name": { 33 | "en": "More than half the days" 34 | }, 35 | "value": 2 36 | }, 37 | { 38 | "name": { 39 | "en": "Nearly every day" 40 | }, 41 | "value": 3 42 | } 43 | ], 44 | "valueType": [ 45 | "xsd:integer" 46 | ] 47 | }, 48 | "ui": { 49 | "inputType": "radio" 50 | }, 51 | "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/main/releases/1.0.0/reproschema" 52 | } 53 | -------------------------------------------------------------------------------- /reproschema/example/fhir/reproschema2fhir_data_example/activities/q_generic_gad7_anxiety/items/easily_agitated: -------------------------------------------------------------------------------- 1 | { 2 | "id": "easily_agitated", 3 | "category": "reproschema:Item", 4 | "additionalNotesObj": [ 5 | { 6 | "column": "Question Number (surveys only)", 7 | "source": "redcap", 8 | "value": NaN 9 | } 10 | ], 11 | "prefLabel": { 12 | "en": "easily_agitated" 13 | }, 14 | "question": { 15 | "en": "Becoming easily annoyed or irritable." 16 | }, 17 | "responseOptions": { 18 | "choices": [ 19 | { 20 | "name": { 21 | "en": "Not at all" 22 | }, 23 | "value": 0 24 | }, 25 | { 26 | "name": { 27 | "en": "Several days" 28 | }, 29 | "value": 1 30 | }, 31 | { 32 | "name": { 33 | "en": "More than half the days" 34 | }, 35 | "value": 2 36 | }, 37 | { 38 | "name": { 39 | "en": "Nearly every day" 40 | }, 41 | "value": 3 42 | } 43 | ], 44 | "valueType": [ 45 | "xsd:integer" 46 | ] 47 | }, 48 | "ui": { 49 | "inputType": "radio" 50 | }, 51 | "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/main/releases/1.0.0/reproschema" 52 | } 53 | -------------------------------------------------------------------------------- /reproschema/example/fhir/reproschema2fhir_data_example/activities/q_generic_gad7_anxiety/items/worry_too_much: -------------------------------------------------------------------------------- 1 | { 2 | "id": "worry_too_much", 3 | "category": "reproschema:Item", 4 | "additionalNotesObj": [ 5 | { 6 | "column": "Question Number (surveys only)", 7 | "source": "redcap", 8 | "value": NaN 9 | } 10 | ], 11 | "prefLabel": { 12 | "en": "worry_too_much" 13 | }, 14 | "question": { 15 | "en": "Worrying too much about different things." 16 | }, 17 | "responseOptions": { 18 | "choices": [ 19 | { 20 | "name": { 21 | "en": "Not at all" 22 | }, 23 | "value": 0 24 | }, 25 | { 26 | "name": { 27 | "en": "Several days" 28 | }, 29 | "value": 1 30 | }, 31 | { 32 | "name": { 33 | "en": "More than half the days" 34 | }, 35 | "value": 2 36 | }, 37 | { 38 | "name": { 39 | "en": "Nearly every day" 40 | }, 41 | "value": 3 42 | } 43 | ], 44 | "valueType": [ 45 | "xsd:integer" 46 | ] 47 | }, 48 | "ui": { 49 | "inputType": "radio" 50 | }, 51 | "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/main/releases/1.0.0/reproschema" 52 | } 53 | -------------------------------------------------------------------------------- /reproschema/tests/test_reproschema2fhir_data/activities/q_generic_gad7_anxiety/items/afraid_of_things: -------------------------------------------------------------------------------- 1 | { 2 | "id": "afraid_of_things", 3 | "category": "reproschema:Item", 4 | "additionalNotesObj": [ 5 | { 6 | "column": "Question Number (surveys only)", 7 | "source": "redcap", 8 | "value": NaN 9 | } 10 | ], 11 | "prefLabel": { 12 | "en": "afraid_of_things" 13 | }, 14 | "question": { 15 | "en": "Feeling afraid, as if something awful might happen." 16 | }, 17 | "responseOptions": { 18 | "choices": [ 19 | { 20 | "name": { 21 | "en": "Not at all" 22 | }, 23 | "value": 0 24 | }, 25 | { 26 | "name": { 27 | "en": "Several days" 28 | }, 29 | "value": 1 30 | }, 31 | { 32 | "name": { 33 | "en": "More than half the days" 34 | }, 35 | "value": 2 36 | }, 37 | { 38 | "name": { 39 | "en": "Nearly every day" 40 | }, 41 | "value": 3 42 | } 43 | ], 44 | "valueType": [ 45 | "xsd:integer" 46 | ] 47 | }, 48 | "ui": { 49 | "inputType": "radio" 50 | }, 51 | "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/main/releases/1.0.0/reproschema" 52 | } 53 | -------------------------------------------------------------------------------- /reproschema/tests/test_reproschema2fhir_data/activities/q_generic_gad7_anxiety/items/cant_control_worry: -------------------------------------------------------------------------------- 1 | { 2 | "id": "cant_control_worry", 3 | "category": "reproschema:Item", 4 | "additionalNotesObj": [ 5 | { 6 | "column": "Question Number (surveys only)", 7 | "source": "redcap", 8 | "value": NaN 9 | } 10 | ], 11 | "prefLabel": { 12 | "en": "cant_control_worry" 13 | }, 14 | "question": { 15 | "en": "Not being able to stop or control worrying." 16 | }, 17 | "responseOptions": { 18 | "choices": [ 19 | { 20 | "name": { 21 | "en": "Not at all" 22 | }, 23 | "value": 0 24 | }, 25 | { 26 | "name": { 27 | "en": "Several days" 28 | }, 29 | "value": 1 30 | }, 31 | { 32 | "name": { 33 | "en": "More than half the days" 34 | }, 35 | "value": 2 36 | }, 37 | { 38 | "name": { 39 | "en": "Nearly every day" 40 | }, 41 | "value": 3 42 | } 43 | ], 44 | "valueType": [ 45 | "xsd:integer" 46 | ] 47 | }, 48 | "ui": { 49 | "inputType": "radio" 50 | }, 51 | "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/main/releases/1.0.0/reproschema" 52 | } 53 | -------------------------------------------------------------------------------- /reproschema/tests/test_reproschema2fhir_data/activities/q_generic_gad7_anxiety/items/hard_to_sit_still: -------------------------------------------------------------------------------- 1 | { 2 | "id": "hard_to_sit_still", 3 | "category": "reproschema:Item", 4 | "additionalNotesObj": [ 5 | { 6 | "column": "Question Number (surveys only)", 7 | "source": "redcap", 8 | "value": NaN 9 | } 10 | ], 11 | "prefLabel": { 12 | "en": "hard_to_sit_still" 13 | }, 14 | "question": { 15 | "en": "Being so restless that it is hard to sit still." 16 | }, 17 | "responseOptions": { 18 | "choices": [ 19 | { 20 | "name": { 21 | "en": "Not at all" 22 | }, 23 | "value": 0 24 | }, 25 | { 26 | "name": { 27 | "en": "Several days" 28 | }, 29 | "value": 1 30 | }, 31 | { 32 | "name": { 33 | "en": "More than half the days" 34 | }, 35 | "value": 2 36 | }, 37 | { 38 | "name": { 39 | "en": "Nearly every day" 40 | }, 41 | "value": 3 42 | } 43 | ], 44 | "valueType": [ 45 | "xsd:integer" 46 | ] 47 | }, 48 | "ui": { 49 | "inputType": "radio" 50 | }, 51 | "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/main/releases/1.0.0/reproschema" 52 | } 53 | -------------------------------------------------------------------------------- /reproschema/example/fhir/reproschema2fhir_data_example/activities/q_generic_gad7_anxiety/items/afraid_of_things: -------------------------------------------------------------------------------- 1 | { 2 | "id": "afraid_of_things", 3 | "category": "reproschema:Item", 4 | "additionalNotesObj": [ 5 | { 6 | "column": "Question Number (surveys only)", 7 | "source": "redcap", 8 | "value": NaN 9 | } 10 | ], 11 | "prefLabel": { 12 | "en": "afraid_of_things" 13 | }, 14 | "question": { 15 | "en": "Feeling afraid, as if something awful might happen." 16 | }, 17 | "responseOptions": { 18 | "choices": [ 19 | { 20 | "name": { 21 | "en": "Not at all" 22 | }, 23 | "value": 0 24 | }, 25 | { 26 | "name": { 27 | "en": "Several days" 28 | }, 29 | "value": 1 30 | }, 31 | { 32 | "name": { 33 | "en": "More than half the days" 34 | }, 35 | "value": 2 36 | }, 37 | { 38 | "name": { 39 | "en": "Nearly every day" 40 | }, 41 | "value": 3 42 | } 43 | ], 44 | "valueType": [ 45 | "xsd:integer" 46 | ] 47 | }, 48 | "ui": { 49 | "inputType": "radio" 50 | }, 51 | "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/main/releases/1.0.0/reproschema" 52 | } 53 | -------------------------------------------------------------------------------- /reproschema/example/fhir/reproschema2fhir_data_example/activities/q_generic_gad7_anxiety/items/cant_control_worry: -------------------------------------------------------------------------------- 1 | { 2 | "id": "cant_control_worry", 3 | "category": "reproschema:Item", 4 | "additionalNotesObj": [ 5 | { 6 | "column": "Question Number (surveys only)", 7 | "source": "redcap", 8 | "value": NaN 9 | } 10 | ], 11 | "prefLabel": { 12 | "en": "cant_control_worry" 13 | }, 14 | "question": { 15 | "en": "Not being able to stop or control worrying." 16 | }, 17 | "responseOptions": { 18 | "choices": [ 19 | { 20 | "name": { 21 | "en": "Not at all" 22 | }, 23 | "value": 0 24 | }, 25 | { 26 | "name": { 27 | "en": "Several days" 28 | }, 29 | "value": 1 30 | }, 31 | { 32 | "name": { 33 | "en": "More than half the days" 34 | }, 35 | "value": 2 36 | }, 37 | { 38 | "name": { 39 | "en": "Nearly every day" 40 | }, 41 | "value": 3 42 | } 43 | ], 44 | "valueType": [ 45 | "xsd:integer" 46 | ] 47 | }, 48 | "ui": { 49 | "inputType": "radio" 50 | }, 51 | "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/main/releases/1.0.0/reproschema" 52 | } 53 | -------------------------------------------------------------------------------- /reproschema/example/fhir/reproschema2fhir_data_example/activities/q_generic_gad7_anxiety/items/hard_to_sit_still: -------------------------------------------------------------------------------- 1 | { 2 | "id": "hard_to_sit_still", 3 | "category": "reproschema:Item", 4 | "additionalNotesObj": [ 5 | { 6 | "column": "Question Number (surveys only)", 7 | "source": "redcap", 8 | "value": NaN 9 | } 10 | ], 11 | "prefLabel": { 12 | "en": "hard_to_sit_still" 13 | }, 14 | "question": { 15 | "en": "Being so restless that it is hard to sit still." 16 | }, 17 | "responseOptions": { 18 | "choices": [ 19 | { 20 | "name": { 21 | "en": "Not at all" 22 | }, 23 | "value": 0 24 | }, 25 | { 26 | "name": { 27 | "en": "Several days" 28 | }, 29 | "value": 1 30 | }, 31 | { 32 | "name": { 33 | "en": "More than half the days" 34 | }, 35 | "value": 2 36 | }, 37 | { 38 | "name": { 39 | "en": "Nearly every day" 40 | }, 41 | "value": 3 42 | } 43 | ], 44 | "valueType": [ 45 | "xsd:integer" 46 | ] 47 | }, 48 | "ui": { 49 | "inputType": "radio" 50 | }, 51 | "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/main/releases/1.0.0/reproschema" 52 | } 53 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Auto-release on PR merge 2 | 3 | on: 4 | # ATM, this is the closest trigger to a PR merging 5 | push: 6 | branches: 7 | - main 8 | 9 | env: 10 | AUTO_VERSION: v11.0.5 11 | 12 | jobs: 13 | auto-release: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: write 17 | pull-requests: write 18 | issues: write 19 | if: "!contains(github.event.head_commit.message, 'ci skip') && !contains(github.event.head_commit.message, 'skip ci')" 20 | steps: 21 | - uses: actions/checkout@v3 22 | 23 | - name: Prepare repository 24 | # Fetch full git history and tags 25 | run: git fetch --unshallow --tags 26 | 27 | - name: Unset header 28 | # checkout@v2 adds a header that makes branch protection report errors 29 | # because the Github action bot is not a collaborator on the repo 30 | run: git config --local --unset http.https://github.com/.extraheader 31 | 32 | - name: Set up Python 33 | uses: actions/setup-python@v4 34 | with: 35 | python-version: "3.11" 36 | 37 | - name: Download auto 38 | run: | 39 | auto_download_url="$(curl -fsSL https://api.github.com/repos/intuit/auto/releases/tags/$AUTO_VERSION | jq -r '.assets[] | select(.name == "auto-linux.gz") | .browser_download_url')" 40 | wget -O- "$auto_download_url" | gunzip > ~/auto 41 | chmod a+x ~/auto 42 | 43 | - name: Create release 44 | run: | 45 | ~/auto shipit -vv 46 | env: 47 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 48 | -------------------------------------------------------------------------------- /reproschema/tests/test_redcap2reproschema.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | 4 | import yaml 5 | from click.testing import CliRunner 6 | 7 | from ..cli import main 8 | 9 | CSV_FILE_NAME = "redcap_hbn_sample.csv" 10 | YAML_FILE_NAME = "redcap2rs.yaml" 11 | CSV_TEST_FILE = os.path.join( 12 | os.path.dirname(__file__), "test_redcap2rs_data", CSV_FILE_NAME 13 | ) 14 | YAML_TEST_FILE = os.path.join( 15 | os.path.dirname(__file__), "test_redcap2rs_data", YAML_FILE_NAME 16 | ) 17 | 18 | 19 | def test_redcap2reproschema(tmpdir): 20 | runner = CliRunner() 21 | 22 | temp_csv_file = tmpdir.join(CSV_FILE_NAME) 23 | temp_yaml_file = tmpdir.join(YAML_FILE_NAME) 24 | 25 | shutil.copy(CSV_TEST_FILE, str(temp_csv_file)) 26 | shutil.copy(YAML_TEST_FILE, str(temp_yaml_file)) 27 | 28 | with tmpdir.as_cwd(): 29 | # Read YAML to find the expected output directory name 30 | with open(str(temp_yaml_file), "r") as file: 31 | protocol = yaml.safe_load(file) 32 | protocol_name = protocol.get("protocol_name", "").replace(" ", "_") 33 | 34 | result = runner.invoke( 35 | main, 36 | [ 37 | "redcap2reproschema", 38 | str(temp_csv_file), 39 | str(temp_yaml_file), 40 | ], 41 | ) 42 | 43 | print("Command output:", result.output) # Add debug output 44 | 45 | assert result.exit_code == 0, f"Command failed with: {result.output}" 46 | assert os.path.isdir( 47 | protocol_name 48 | ), f"Expected output directory '{protocol_name}' does not exist" 49 | -------------------------------------------------------------------------------- /reproschema/tests/test_reproschema2fhir_data/activities/q_generic_gad7_anxiety/items/tough_to_work: -------------------------------------------------------------------------------- 1 | { 2 | "id": "tough_to_work", 3 | "category": "reproschema:Item", 4 | "additionalNotesObj": [ 5 | { 6 | "column": "Question Number (surveys only)", 7 | "source": "redcap", 8 | "value": NaN 9 | } 10 | ], 11 | "prefLabel": { 12 | "en": "tough_to_work" 13 | }, 14 | "question": { 15 | "en": "How difficult have they made it for you to do your work, take care of things at home, or get along with other people?" 16 | }, 17 | "responseOptions": { 18 | "choices": [ 19 | { 20 | "name": { 21 | "en": "Not difficult at all" 22 | }, 23 | "value": 0 24 | }, 25 | { 26 | "name": { 27 | "en": "Somewhat difficult" 28 | }, 29 | "value": 1 30 | }, 31 | { 32 | "name": { 33 | "en": "Very difficult" 34 | }, 35 | "value": 2 36 | }, 37 | { 38 | "name": { 39 | "en": "Extremely difficult" 40 | }, 41 | "value": 3 42 | } 43 | ], 44 | "valueType": [ 45 | "xsd:integer" 46 | ] 47 | }, 48 | "ui": { 49 | "inputType": "radio" 50 | }, 51 | "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/main/releases/1.0.0/reproschema" 52 | } 53 | -------------------------------------------------------------------------------- /reproschema/example/fhir/reproschema2fhir_data_example/activities/q_generic_gad7_anxiety/items/tough_to_work: -------------------------------------------------------------------------------- 1 | { 2 | "id": "tough_to_work", 3 | "category": "reproschema:Item", 4 | "additionalNotesObj": [ 5 | { 6 | "column": "Question Number (surveys only)", 7 | "source": "redcap", 8 | "value": NaN 9 | } 10 | ], 11 | "prefLabel": { 12 | "en": "tough_to_work" 13 | }, 14 | "question": { 15 | "en": "How difficult have they made it for you to do your work, take care of things at home, or get along with other people?" 16 | }, 17 | "responseOptions": { 18 | "choices": [ 19 | { 20 | "name": { 21 | "en": "Not difficult at all" 22 | }, 23 | "value": 0 24 | }, 25 | { 26 | "name": { 27 | "en": "Somewhat difficult" 28 | }, 29 | "value": 1 30 | }, 31 | { 32 | "name": { 33 | "en": "Very difficult" 34 | }, 35 | "value": 2 36 | }, 37 | { 38 | "name": { 39 | "en": "Extremely difficult" 40 | }, 41 | "value": 3 42 | } 43 | ], 44 | "valueType": [ 45 | "xsd:integer" 46 | ] 47 | }, 48 | "ui": { 49 | "inputType": "radio" 50 | }, 51 | "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/main/releases/1.0.0/reproschema" 52 | } 53 | -------------------------------------------------------------------------------- /reproschema/tests/test_output2redcap.py: -------------------------------------------------------------------------------- 1 | import csv 2 | import os 3 | from pathlib import Path 4 | from shutil import copytree, rmtree 5 | 6 | import pytest 7 | from click.testing import CliRunner 8 | 9 | from ..cli import main 10 | 11 | 12 | def test_reproschemaui2redcap(tmpdir): 13 | runner = CliRunner() 14 | 15 | with runner.isolated_filesystem(): 16 | # Copy necessary test data into the isolated filesystem 17 | original_data_dir = os.path.join( 18 | os.path.dirname(__file__), "test_output2redcap" 19 | ) 20 | copytree(original_data_dir, "input_data") 21 | 22 | input_path = Path("input_data") 23 | output_csv_path = os.path.join(tmpdir) 24 | 25 | result = runner.invoke( 26 | main, 27 | [ 28 | "output2redcap", 29 | str(input_path), 30 | str(output_csv_path), 31 | ], 32 | ) 33 | print("input", original_data_dir) 34 | print("output", output_csv_path) 35 | 36 | assert result.exit_code == 0 37 | 38 | assert os.path.exists(output_csv_path) 39 | output_file = os.path.join(output_csv_path, "redcap.csv") 40 | assert os.path.exists(output_file) 41 | 42 | with open(output_file, "r", encoding="utf-8") as csv_file: 43 | reader = csv.reader(csv_file) 44 | csv_contents = list(reader) 45 | 46 | assert ( 47 | len(csv_contents) > 1 48 | ) # More than one row indicates content beyond headers 49 | 50 | # Clean up temporary directory after use (optional) 51 | # rmtree(tmpdir) 52 | -------------------------------------------------------------------------------- /reproschema/migrate.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import shutil 4 | from pathlib import Path 5 | 6 | from .jsonldutils import load_file 7 | from .utils import fixing_old_schema 8 | 9 | 10 | def migrate2newschema(path, inplace=False, fixed_path=None): 11 | path = Path(path).resolve() 12 | if path.is_file(): 13 | print(f"migration of file: {path}") 14 | new_path = migrate2newschema_file(path, inplace, fixed_path) 15 | else: # path.is_dir 16 | if inplace: 17 | new_path = path 18 | elif fixed_path: 19 | new_path = Path(fixed_path).resolve() 20 | shutil.copytree(path, new_path) 21 | else: 22 | new_path = path.parent / f"{path.name}_after_migration" 23 | shutil.copytree(path, new_path) 24 | # fixing all files in new_path 25 | all_files = Path(new_path).rglob("*") 26 | for file in all_files: 27 | if file.is_file(): 28 | migrate2newschema_file(jsonld_path=file, inplace=True) 29 | return new_path 30 | 31 | 32 | def migrate2newschema_file(jsonld_path, inplace=False, fixed_path=None): 33 | print(f"Fixing {jsonld_path}") 34 | data = load_file(jsonld_path, started=False) 35 | data_fixed = [fixing_old_schema(data, copy_data=True)] 36 | if inplace: 37 | new_filename = jsonld_path 38 | elif fixedjsonld_path: 39 | new_filename = fixed_path 40 | else: 41 | root, ext = os.path.splitext(jsonld_path) 42 | new_filename = f"{root}_after_migration{ext}" 43 | with open(new_filename, "w") as f: 44 | json.dump(data_fixed, f, indent=4) 45 | return new_filename 46 | -------------------------------------------------------------------------------- /reproschema/tests/test_reproschema2fhir_data/activities/q_generic_gad7_anxiety/items/nervous_anxious: -------------------------------------------------------------------------------- 1 | { 2 | "id": "nervous_anxious", 3 | "category": "reproschema:Item", 4 | "additionalNotesObj": [ 5 | { 6 | "column": "Question Number (surveys only)", 7 | "source": "redcap", 8 | "value": NaN 9 | } 10 | ], 11 | "preamble": { 12 | "en": "Over the last two weeks, how often have you been bothered by the following problems?" 13 | }, 14 | "prefLabel": { 15 | "en": "nervous_anxious" 16 | }, 17 | "question": { 18 | "en": "Feeling nervous, anxious, or on edge." 19 | }, 20 | "responseOptions": { 21 | "choices": [ 22 | { 23 | "name": { 24 | "en": "Not at all" 25 | }, 26 | "value": 0 27 | }, 28 | { 29 | "name": { 30 | "en": "Several days" 31 | }, 32 | "value": 1 33 | }, 34 | { 35 | "name": { 36 | "en": "More than half the days" 37 | }, 38 | "value": 2 39 | }, 40 | { 41 | "name": { 42 | "en": "Nearly every day" 43 | }, 44 | "value": 3 45 | } 46 | ], 47 | "valueType": [ 48 | "xsd:integer" 49 | ] 50 | }, 51 | "ui": { 52 | "inputType": "radio" 53 | }, 54 | "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/main/releases/1.0.0/reproschema" 55 | } 56 | -------------------------------------------------------------------------------- /reproschema/example/fhir/reproschema2fhir_data_example/activities/q_generic_gad7_anxiety/items/nervous_anxious: -------------------------------------------------------------------------------- 1 | { 2 | "id": "nervous_anxious", 3 | "category": "reproschema:Item", 4 | "additionalNotesObj": [ 5 | { 6 | "column": "Question Number (surveys only)", 7 | "source": "redcap", 8 | "value": NaN 9 | } 10 | ], 11 | "preamble": { 12 | "en": "Over the last two weeks, how often have you been bothered by the following problems?" 13 | }, 14 | "prefLabel": { 15 | "en": "nervous_anxious" 16 | }, 17 | "question": { 18 | "en": "Feeling nervous, anxious, or on edge." 19 | }, 20 | "responseOptions": { 21 | "choices": [ 22 | { 23 | "name": { 24 | "en": "Not at all" 25 | }, 26 | "value": 0 27 | }, 28 | { 29 | "name": { 30 | "en": "Several days" 31 | }, 32 | "value": 1 33 | }, 34 | { 35 | "name": { 36 | "en": "More than half the days" 37 | }, 38 | "value": 2 39 | }, 40 | { 41 | "name": { 42 | "en": "Nearly every day" 43 | }, 44 | "value": 3 45 | } 46 | ], 47 | "valueType": [ 48 | "xsd:integer" 49 | ] 50 | }, 51 | "ui": { 52 | "inputType": "radio" 53 | }, 54 | "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/main/releases/1.0.0/reproschema" 55 | } 56 | -------------------------------------------------------------------------------- /reproschema/tests/test_rs2redcap_data/test_redcap2rs/test_redcap2rs/test_redcap2rs_schema: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", 3 | "@type": "reproschema:Protocol", 4 | "@id": "test_redcap2rs_schema", 5 | "prefLabel": "redcap protocols", 6 | "altLabel": "test_redcap2rs_schema", 7 | "description": "testing", 8 | "schemaVersion": "1.0.0-rc4", 9 | "version": "0.0.1", 10 | "ui": { 11 | "addProperties": [ 12 | { 13 | "isAbout": "../activities/autism_parenting_stress_index_apsi/autism_parenting_stress_index_apsi_schema", 14 | "variableName": "autism_parenting_stress_index_apsi_schema", 15 | "prefLabel": "Autism Parenting Stress Index Apsi" 16 | }, 17 | { 18 | "isAbout": "../activities/cognitive_and_affective_mindfulness_scalerevised_c/cognitive_and_affective_mindfulness_scalerevised_c_schema", 19 | "variableName": "cognitive_and_affective_mindfulness_scalerevised_c_schema", 20 | "prefLabel": "Cognitive And Affective Mindfulness Scalerevised C" 21 | } 22 | ], 23 | "order": [ 24 | "../activities/autism_parenting_stress_index_apsi/autism_parenting_stress_index_apsi_schema", 25 | "../activities/cognitive_and_affective_mindfulness_scalerevised_c/cognitive_and_affective_mindfulness_scalerevised_c_schema" 26 | ], 27 | "shuffle": false, 28 | "visibility": { 29 | "autism_parenting_stress_index_apsi": true, 30 | "cognitive_and_affective_mindfulness_scalerevised_c": true 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Python package](https://github.com/ReproNim/reproschema-py/actions/workflows/package.yml/badge.svg) 2 | 3 | # Reproschema Python library and Command Line Interface (CLI) 4 | 5 | The `reproschema-py` library provides a Python interface and a command-line tool to work with ReproSchema, a YAML-based framework for creating and managing reproducible research protocols. 6 | 7 | For more information, see the [full documentation](https://ReproNim.github.io/reproschema-py/). 8 | 9 | ## Installation 10 | 11 | reproschema requires Python 3.10+. 12 | 13 | ``` 14 | pip install reproschema 15 | ``` 16 | 17 | ## Developer Guide 18 | 19 | ### Developer installation 20 | 21 | Fork this repo to your own GitHub account, then clone and install your forked repo in the developer mode: 22 | 23 | ``` 24 | git clone https://github.com//reproschema-py.git 25 | cd reproschema-py 26 | pip install -e . 27 | ``` 28 | #### Notes on the reproschema model 29 | This repository uses the `pydantic` representation of the `reproschema` model, defined in [model.py](https://github.com/ReproNim/reproschema-py/blob/main/reproschema/models/model.py). 30 | The `pydantic` model is automatically generated from the LinkML model maintained in the [ReproNim/reproschema repository](https://github.com/ReproNim/reproschema). 31 | **All changes to the model should be made in the LinkML source in that repository.** 32 | 33 | #### Style 34 | This repo uses pre-commit to check styling. 35 | - Install pre-commit with pip: `pip install pre-commit` 36 | - In order to use it with the repository, you have to run `run pre-commit install` in the root directory the first time you use it. 37 | 38 | When pre-commit is used, you may have to run git commit twice, 39 | since pre-commit may make additional changes to your code for styling and will 40 | not commit these changes by default. 41 | -------------------------------------------------------------------------------- /docs/user_guide/redcap2reproschema.md: -------------------------------------------------------------------------------- 1 | ## `redcap2reproschema` 2 | The `redcap2reproschema` function is designed to process a given REDCap CSV file and YAML configuration to generate the output in the reproschema format. 3 | 4 | ### Prerequisites 5 | Before the conversion, ensure you have the following: 6 | 7 | **YAML Configuration File**: 8 | - Download [templates/redcap2rs.yaml](templates/redcap2rs.yaml) and fill it out with your protocol details. 9 | 10 | ### YAML File Configuration 11 | In the `templates/redcap2rs.yaml` file, provide the following information: 12 | 13 | - **protocol_name**: A unique identifier for your protocol. Use underscores for spaces and avoid special characters. 14 | - **protocol_display_name**: Name that will appear in the application. 15 | - **protocol_description**: A brief description of your protocol. 16 | - **redcap_version**: Version of your redcap file (you can customize it). 17 | 18 | Example: 19 | ```yaml 20 | protocol_name: "My_Protocol" 21 | protocol_display_name: "Assessment Protocol" 22 | protocol_description: "This protocol is for assessing cognitive skills." 23 | redcap_version: "X.XX.X" 24 | ``` 25 | ### CLI Usage 26 | 27 | The `redcap2reproschema` function has been integrated into a CLI tool, use the following command: 28 | ```bash 29 | reproschema redcap2reproschema path/to/your_redcap_data_dic.csv path/to/your_redcap2rs.yaml 30 | ``` 31 | 32 | Optionally you can provide a path to the output directory (default is the current directory) by adding the option: `--output-path PATH` 33 | ### Python Function Usage 34 | 35 | You can also use the `redcap2reproschema` function from the `reproschema-py` package in your Python code. 36 | 37 | ```python 38 | from reproschema import redcap2reproschema 39 | 40 | csv_path = "path-to/your_redcap_data_dic.csv" 41 | yaml_path = "path-to/your_redcap2rs.yaml" 42 | output_path = "path-to/directory_you_want_to_save_output" 43 | 44 | redcap2reproschema(csv_path, yaml_path, output_path) 45 | ``` 46 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | 4 | # excluding fixes for csv data files 5 | exclude: ".*\\.csv$" 6 | 7 | repos: 8 | - repo: https://github.com/pre-commit/pre-commit-hooks 9 | rev: v6.0.0 10 | hooks: 11 | - id: trailing-whitespace 12 | - id: end-of-file-fixer 13 | - id: check-yaml 14 | - id: check-added-large-files 15 | 16 | # Sorts Python imports alphabetically and by section with `isort`. 17 | - repo: https://github.com/pycqa/isort 18 | rev: 6.0.1 19 | hooks: 20 | - id: isort 21 | args: [--profile, black, --settings-path, pyproject.toml] 22 | 23 | - repo: https://github.com/psf/black 24 | rev: 25.9.0 25 | hooks: 26 | - id: black 27 | 28 | # Checks for spelling errors 29 | - repo: https://github.com/codespell-project/codespell 30 | rev: v2.4.1 31 | hooks: 32 | - id: codespell 33 | args: [--toml, pyproject.toml, "--skip=CHANGELOG.md"] 34 | additional_dependencies: [tomli] 35 | 36 | # Format TOML files 37 | - repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks 38 | rev: v2.15.0 39 | hooks: 40 | - id: pretty-format-toml 41 | args: [--autofix, --indent, '4'] 42 | 43 | # Check that Python code complies with PEP8 guidelines 44 | # flake8 uses pydocstyle to check docstrings: https://flake8.pycqa.org/en/latest/ 45 | # flake8-docstrings: https://pypi.org/project/flake8-docstrings/ 46 | # flake8-use-fstring forces to use fstrings: https://pypi.org/project/flake8-use-fstring/ 47 | # flake8-functions checks functions quality: https://pypi.org/project/flake8-functions/ 48 | # flake8-bugbear detects some common bugs: https://github.com/PyCQA/flake8-bugbear 49 | - repo: https://github.com/pyCQA/flake8 50 | rev: 7.3.0 51 | hooks: 52 | - id: flake8 53 | args: [--config, .flake8, --verbose, reproschema] 54 | additional_dependencies: [flake8-bugbear] 55 | -------------------------------------------------------------------------------- /reproschema/tests/data/activities/activity1.jsonld: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "../../contexts/generic", 3 | "@type": "reproschema:Activity", 4 | "@id": "activity1.jsonld", 5 | "prefLabel": "Example 1", 6 | "description": "Activity example 1", 7 | "schemaVersion": "1.0.0-rc4", 8 | "version": "0.0.1", 9 | "citation": "https://www.ncbi.nlm.nih.gov/pmc/articles/PMC1495268/", 10 | "image": { 11 | "@type": "AudioObject", 12 | "contentUrl": "http://example.com/sample-image.png" 13 | }, 14 | "preamble": { 15 | "en": "Over the last 2 weeks, how often have you been bothered by any of the following problems?", 16 | "es": "Durante las últimas 2 semanas, ¿con qué frecuencia le han molestado los siguintes problemas?" 17 | }, 18 | "compute": [ 19 | { 20 | "variableName": "activity1_total_score", 21 | "jsExpression": "item1 + item2" 22 | } 23 | ], 24 | "messages": [ 25 | { 26 | "message": "Test message: Triggered when item1 value is greater than 1", 27 | "jsExpression": "item1 > 1" 28 | } 29 | ], 30 | "ui": { 31 | "addProperties": [ 32 | { "isAbout": "items/item1.jsonld", 33 | "variableName": "item1", 34 | "requiredValue": true, 35 | "isVis": true, 36 | "randomMaxDelay": "PT2H", 37 | "limit": "P2D", 38 | "schedule": "R/2020-08-01T08:00:00Z/P1D" 39 | }, 40 | { "isAbout": "items/item2.jsonld", 41 | "variableName": "item2", 42 | "requiredValue": true, 43 | "isVis": true, 44 | "allow": ["reproschema:Skipped"] 45 | }, 46 | { "isAbout": "items/activity1_total_score", 47 | "variableName": "activity1_total_score", 48 | "requiredValue": true, 49 | "isVis": false 50 | } 51 | ], 52 | "order": [ 53 | "items/item1.jsonld", 54 | "items/item2.jsonld", 55 | "items/activity1_total_score" 56 | ], 57 | "shuffle": false 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /reproschema/tests/data/activities/items/item1.jsonld: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "../../../contexts/generic", 3 | "@type": "reproschema:Field", 4 | "@id": "item1.jsonld", 5 | "prefLabel": "item1", 6 | "description": "Q1 of example 1", 7 | "schemaVersion": "1.0.0-rc4", 8 | "version": "0.0.1", 9 | "audio": { 10 | "@type": "AudioObject", 11 | "contentUrl": "http://media.freesound.org/sample-file.mp4" 12 | }, 13 | "image": { 14 | "@type": "ImageObject", 15 | "contentUrl": "http://example.com/sample-image.jpg" 16 | }, 17 | "question": { 18 | "en": "Little interest or pleasure in doing things", 19 | "es": "Poco interés o placer en hacer cosas" 20 | }, 21 | "ui": { 22 | "inputType": "radio" 23 | }, 24 | "responseOptions": { 25 | "valueType": "xsd:integer", 26 | "minValue": 0, 27 | "maxValue": 3, 28 | "multipleChoice": false, 29 | "choices": [ 30 | { 31 | "name": { 32 | "en": "Not at all", 33 | "es": "Para nada" 34 | }, 35 | "value": 0 36 | }, 37 | { 38 | "name": { 39 | "en": "Several days", 40 | "es": "Varios días" 41 | }, 42 | "value": "a" 43 | }, 44 | { 45 | "name": { 46 | "en": "More than half the days", 47 | "es": "Más de la mitad de los días" 48 | }, 49 | "value": {"@id": "http://example.com/choice3" } 50 | }, 51 | { 52 | "name": { 53 | "en": "Nearly everyday", 54 | "es": "Casi todos los días" 55 | }, 56 | "value": {"@value": "choice-with-lang", "@language": "en"} 57 | } 58 | ] 59 | }, 60 | "additionalNotesObj": [ 61 | { 62 | "source": "redcap", 63 | "column": "notes", 64 | "value": "some extra note" 65 | }, 66 | { 67 | "source": "redcap", 68 | "column": "notes", 69 | "value": {"@id": "http://example.com/iri-example"} 70 | } 71 | ] 72 | 73 | } 74 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = "hatchling.build" 3 | requires = ["hatchling", "hatch-vcs"] 4 | 5 | [project] 6 | authors = [{name = "Repronim developers", email = "info@repronim.org"}] 7 | dependencies = [ 8 | "click", 9 | "etelemetry", 10 | "pyshacl", 11 | "PyLD", 12 | "requests", 13 | "requests_cache", 14 | "pyyaml", 15 | "beautifulsoup4", 16 | "lxml", 17 | "pydantic >= 2.0", 18 | "pandas", 19 | "fhir.resources>=v8.0.0" 20 | ] 21 | description = "Reproschema Python library" 22 | # Version from setuptools_scm 23 | dynamic = ["version"] 24 | license = {text = "Apache License, 2.0"} 25 | maintainers = [{name = "Repronim developers", email = "info@repronim.org"}] 26 | name = "reproschema" 27 | readme = "README.md" 28 | requires-python = ">=3.10" 29 | 30 | [project.optional-dependencies] 31 | all = ["reproschema[doc]", "reproschema[test]"] 32 | dev = ["reproschema[doc]", "reproschema[test]", "black", "pre-commit"] 33 | doc = [ 34 | "packaging", 35 | "sphinx >= 2.1.2", 36 | "sphinx_rtd_theme", 37 | "sphinxcontrib-apidoc ~= 0.3.0", 38 | "sphinxcontrib-napoleon", 39 | "sphinxcontrib-versioning" 40 | ] 41 | docs = ["reproschema[doc]"] 42 | # For running unit and docstring tests 43 | test = [ 44 | "pytest >= 4.4.0", 45 | "pytest-cov", 46 | "pytest-env", 47 | "pytest-xdist", 48 | "pytest-rerunfailures", 49 | "codecov" 50 | ] 51 | tests = ["reproschema[test]"] 52 | 53 | [project.scripts] 54 | reproschema = "reproschema.cli:main" 55 | 56 | [project.urls] 57 | Homepage = "https://github.com/ReproNim/reproschema-py" 58 | 59 | [tool.black] 60 | line-length = 79 61 | 62 | [tool.codespell] 63 | exclude-file = "CHANGELOG.md" 64 | ignore-words = "codespell_ignore_words.txt" 65 | skip = "./.git" 66 | 67 | [tool.hatch.build.hooks.vcs] 68 | version-file = "reproschema/_version.py" 69 | 70 | [tool.hatch.build.targets.wheel] 71 | packages = ["reproschema"] 72 | 73 | [tool.hatch.version] 74 | source = "vcs" 75 | 76 | [tool.isort] 77 | combine_as_imports = true 78 | line_length = 79 79 | profile = "black" 80 | skip_gitignore = true 81 | 82 | [tool.pytest.ini_options] 83 | addopts = "-ra --strict-config --strict-markers --doctest-modules --showlocals -v" 84 | doctest_optionflags = "NORMALIZE_WHITESPACE ELLIPSIS" 85 | junit_family = "xunit2" 86 | minversion = "6.0" 87 | xfail_strict = true 88 | -------------------------------------------------------------------------------- /reproschema/tests/test_process_csv.py: -------------------------------------------------------------------------------- 1 | import tempfile 2 | from pathlib import Path 3 | 4 | import pytest 5 | 6 | from ..convertutils import normalize_condition 7 | from ..redcap2reproschema import process_csv 8 | 9 | 10 | def test_process_csv(): 11 | csv_data = """Form Name,Variable / Field Name,Field Type,Field Label,Field Annotation,"Choices, Calculations, OR Slider Labels" 12 | form1,field1,text,,, 13 | form1,field2,calc,,,[field1] 14 | form1,field3,text,,@CALCTEXT(3*3), 15 | form2,field4,text,,, 16 | ,field5,text,,,""" 17 | 18 | with tempfile.TemporaryDirectory() as tmpdir: 19 | csv_path = Path(tmpdir) / "test.csv" 20 | csv_path.write_text(csv_data) 21 | 22 | datas, order = process_csv(csv_path) 23 | 24 | assert set(datas.keys()) == {"form1", "form2"} 25 | assert order == ["form1", "form2"] 26 | 27 | assert datas["form1"]["order"] == [ 28 | "items/field1" 29 | ] # both field2 and field3 go to compute 30 | assert datas["form2"]["order"] == ["items/field4"] 31 | 32 | assert len(datas["form1"]["compute"]) == 2 33 | assert any( 34 | item["variableName"] == "field2" 35 | for item in datas["form1"]["compute"] 36 | ) 37 | assert any( 38 | item["variableName"] == "field3" 39 | for item in datas["form1"]["compute"] 40 | ) 41 | 42 | 43 | def test_process_csv_missing_columns(): 44 | csv_data = "Column1,Column2\na,b" 45 | with tempfile.TemporaryDirectory() as tmpdir: 46 | csv_path = Path(tmpdir) / "test.csv" 47 | csv_path.write_text(csv_data) 48 | 49 | with pytest.raises(ValueError): 50 | process_csv(csv_path) 51 | 52 | 53 | @pytest.mark.parametrize( 54 | "condition_str,expected", 55 | [ 56 | ("[field1] + [field2]", "field1 + field2"), 57 | ("[total]*100", "total * 100"), 58 | ("2+2", "2 + 2"), 59 | ("3*3", "3 * 3"), 60 | ("[age] = 1", "age == 1"), 61 | ("[field1] = 1 or [field2] = 2", "field1 == 1 || field2 == 2"), 62 | ("[age] > 18", "age > 18"), 63 | ("[some_other_condition] = 1", "some_other_condition == 1"), 64 | ("[weight] > 0 and [height] > 0", "weight > 0 && height > 0"), 65 | ], 66 | ) 67 | def test_normalize_condition(condition_str, expected): 68 | # Test calc expressions 69 | assert normalize_condition(condition_str) == expected 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # autogenerated by hacthling 2 | reproschema/_version.py 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | pip-wheel-metadata/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | .DS_Store 33 | 34 | # PyInstaller 35 | # Usually these files are written by a python script from a template 36 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 37 | *.manifest 38 | *.spec 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .nox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *.cover 54 | *.py,cover 55 | .hypothesis/ 56 | .pytest_cache/ 57 | 58 | # Translations 59 | *.mo 60 | *.pot 61 | 62 | # Django stuff: 63 | *.log 64 | local_settings.py 65 | db.sqlite3 66 | db.sqlite3-journal 67 | 68 | # Flask stuff: 69 | instance/ 70 | .webassets-cache 71 | 72 | # Scrapy stuff: 73 | .scrapy 74 | 75 | # Sphinx documentation 76 | docs/_build/ 77 | 78 | # PyBuilder 79 | target/ 80 | 81 | # Jupyter Notebook 82 | .ipynb_checkpoints 83 | 84 | # IPython 85 | profile_default/ 86 | ipython_config.py 87 | 88 | # pyenv 89 | .python-version 90 | 91 | # pipenv 92 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 93 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 94 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 95 | # install all needed dependencies. 96 | #Pipfile.lock 97 | 98 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 99 | __pypackages__/ 100 | 101 | # Celery stuff 102 | celerybeat-schedule 103 | celerybeat.pid 104 | 105 | # SageMath parsed files 106 | *.sage.py 107 | 108 | # Environments 109 | .env 110 | .venv 111 | env/ 112 | venv/ 113 | ENV/ 114 | env.bak/ 115 | venv.bak/ 116 | 117 | # Spyder project settings 118 | .spyderproject 119 | .spyproject 120 | 121 | # Rope project settings 122 | .ropeproject 123 | 124 | # mkdocs documentation 125 | /site 126 | 127 | # mypy 128 | .mypy_cache/ 129 | .dmypy.json 130 | dmypy.json 131 | 132 | # Pyre type checker 133 | .pyre/ 134 | 135 | # Pycharm 136 | .idea/ 137 | 138 | .serena/ 139 | 140 | # Internal documentation 141 | internal/ 142 | 143 | # Claude configuration 144 | CLAUDE.md 145 | -------------------------------------------------------------------------------- /reproschema/tests/data/activities/activity1_embed.jsonld: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "../../contexts/generic", 3 | "@type": "reproschema:Activity", 4 | "@id": "activity1_embed.jsonld", 5 | "prefLabel": "Example 1", 6 | "description": "Activity example 1", 7 | "schemaVersion": "1.0.0-rc4", 8 | "version": "0.0.1", 9 | "citation": "https://www.ncbi.nlm.nih.gov/pmc/articles/PMC1495268/", 10 | "preamble": { 11 | "en": "Over the last 2 weeks, how often have you been bothered by any of the following problems?", 12 | "es": "Durante las últimas 2 semanas, ¿con qué frecuencia le han molestado los siguintes problemas?" 13 | }, 14 | "ui": { 15 | "addProperties": [ 16 | { "isAbout": "items/item1.jsonld", 17 | "variableName": "item1", 18 | "requiredValue": true, 19 | "isVis": true} 20 | ], 21 | "order": [ 22 | { 23 | "@type": "reproschema:Field", 24 | "@id": "items/item1.jsonld", 25 | "prefLabel": "item1", 26 | "description": "Q1 of example 1", 27 | "schemaVersion": "1.0.0-rc4", 28 | "version": "0.0.1", 29 | "question": { 30 | "en": "Little interest or pleasure in doing things", 31 | "es": "Poco interés o placer en hacer cosas" 32 | }, 33 | "ui": { 34 | "inputType": "radio" 35 | }, 36 | "responseOptions": { 37 | "valueType": "xsd:integer", 38 | "minValue": 0, 39 | "maxValue": 3, 40 | "multipleChoice": false, 41 | "choices": [ 42 | { 43 | "name": { 44 | "en": "Not at all", 45 | "es": "Para nada" 46 | }, 47 | "value": 0 48 | }, 49 | { 50 | "name": { 51 | "en": "Several days", 52 | "es": "Varios días" 53 | }, 54 | "value": 1 55 | }, 56 | { 57 | "name": { 58 | "en": "More than half the days", 59 | "es": "Más de la mitad de los días" 60 | }, 61 | "value": 2 62 | }, 63 | { 64 | "name": { 65 | "en": "Nearly everyday", 66 | "es": "Casi todos los días" 67 | }, 68 | "value": 3 69 | } 70 | ] 71 | } 72 | } 73 | ], 74 | "shuffle": false 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.9.0 (Wed Dec 04 2024) 2 | 3 | #### 🚀 Enhancement 4 | 5 | - fix isVis based on Field annotation [#80](https://github.com/ReproNim/reproschema-py/pull/80) ([@yibeichan](https://github.com/yibeichan) [@pre-commit-ci[bot]](https://github.com/pre-commit-ci[bot])) 6 | 7 | #### Authors: 2 8 | 9 | - [@pre-commit-ci[bot]](https://github.com/pre-commit-ci[bot]) 10 | - Yibei Chen ([@yibeichan](https://github.com/yibeichan)) 11 | 12 | --- 13 | 14 | # 0.8.0 (Thu Nov 21 2024) 15 | 16 | #### 🚀 Enhancement 17 | 18 | - fix strip() issue for choices [#78](https://github.com/ReproNim/reproschema-py/pull/78) ([@yibeichan](https://github.com/yibeichan) [@pre-commit-ci[bot]](https://github.com/pre-commit-ci[bot])) 19 | 20 | #### 🐛 Bug Fix 21 | 22 | - adding id check to the validation [#68](https://github.com/ReproNim/reproschema-py/pull/68) ([@djarecka](https://github.com/djarecka) [@satra](https://github.com/satra)) 23 | - [pre-commit.ci] pre-commit autoupdate [#76](https://github.com/ReproNim/reproschema-py/pull/76) ([@pre-commit-ci[bot]](https://github.com/pre-commit-ci[bot])) 24 | - [MAINT] support python 3.13 [#77](https://github.com/ReproNim/reproschema-py/pull/77) ([@Remi-Gau](https://github.com/Remi-Gau)) 25 | - [pre-commit.ci] pre-commit autoupdate [#72](https://github.com/ReproNim/reproschema-py/pull/72) ([@pre-commit-ci[bot]](https://github.com/pre-commit-ci[bot])) 26 | - [FIX] debugging redcap2reproschema during converting HBCD [#61](https://github.com/ReproNim/reproschema-py/pull/61) ([@yibeichan](https://github.com/yibeichan) [@pre-commit-ci[bot]](https://github.com/pre-commit-ci[bot])) 27 | - [pre-commit.ci] pre-commit autoupdate [#69](https://github.com/ReproNim/reproschema-py/pull/69) ([@pre-commit-ci[bot]](https://github.com/pre-commit-ci[bot])) 28 | - ignore CHANGELOG.md for codespell [#71](https://github.com/ReproNim/reproschema-py/pull/71) ([@yibeichan](https://github.com/yibeichan) [@pre-commit-ci[bot]](https://github.com/pre-commit-ci[bot])) 29 | - fix typos [#70](https://github.com/ReproNim/reproschema-py/pull/70) ([@yibeichan](https://github.com/yibeichan)) 30 | 31 | #### Authors: 5 32 | 33 | - [@pre-commit-ci[bot]](https://github.com/pre-commit-ci[bot]) 34 | - Dorota Jarecka ([@djarecka](https://github.com/djarecka)) 35 | - Remi Gau ([@Remi-Gau](https://github.com/Remi-Gau)) 36 | - Satrajit Ghosh ([@satra](https://github.com/satra)) 37 | - Yibei Chen ([@yibeichan](https://github.com/yibeichan)) 38 | 39 | --- 40 | 41 | # 0.7.2 (Thu Jul 11 2024) 42 | 43 | #### 🐛 Bug Fix 44 | 45 | - enable org auto token [#67](https://github.com/ReproNim/reproschema-py/pull/67) ([@satra](https://github.com/satra)) 46 | - Update .autorc to fix author issue [#66](https://github.com/ReproNim/reproschema-py/pull/66) ([@satra](https://github.com/satra)) 47 | - Fixed bug in redcap2reproschema with items id not matching item filname and redcap variable… [#65](https://github.com/ReproNim/reproschema-py/pull/65) ([@Evan8456](https://github.com/Evan8456) [@pre-commit-ci[bot]](https://github.com/pre-commit-ci[bot])) 48 | 49 | #### Authors: 3 50 | 51 | - [@pre-commit-ci[bot]](https://github.com/pre-commit-ci[bot]) 52 | - Evan Ng ([@Evan8456](https://github.com/Evan8456)) 53 | - Satrajit Ghosh ([@satra](https://github.com/satra)) 54 | -------------------------------------------------------------------------------- /reproschema/tests/data/protocols/protocol1_embed.jsonld: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "../../contexts/generic", 3 | "@type": "reproschema:Protocol", 4 | "@id": "protocol1_embed.jsonld", 5 | "prefLabel": { 6 | "en": "Protocol1", 7 | "es": "Protocol1_es" 8 | }, 9 | "description": "example Protocol", 10 | "schemaVersion": "1.0.0-rc4", 11 | "version": "0.0.1", 12 | "ui": { 13 | "addProperties": [ 14 | { 15 | "isAbout": "../activities/activity1.jsonld", 16 | "variableName": "activity1", 17 | "prefLabel": { 18 | "en": "Screening", 19 | "es": "Screening_es" 20 | }, 21 | "isVis": true 22 | } 23 | ], 24 | "order": [ 25 | { 26 | "@type": "reproschema:Activity", 27 | "@id": "../activities/activity1.jsonld", 28 | "prefLabel": "Example 1", 29 | "description": "Activity example 1", 30 | "schemaVersion": "1.0.0-rc4", 31 | "version": "0.0.1", 32 | "citation": "https://www.ncbi.nlm.nih.gov/pmc/articles/PMC1495268/", 33 | "preamble": { 34 | "en": "Over the last 2 weeks, how often have you been bothered by any of the following problems?", 35 | "es": "Durante las últimas 2 semanas, ¿con qué frecuencia le han molestado los siguintes problemas?" 36 | }, 37 | "ui": { 38 | "addProperties": [ 39 | { 40 | "isAbout": "../activities/items/item1.jsonld", 41 | "variableName": "item1", 42 | "requiredValue": true, 43 | "isVis": true 44 | } 45 | ], 46 | "order": [ 47 | { 48 | "@type": "reproschema:Field", 49 | "@id": "../activities/items/item1.jsonld", 50 | "prefLabel": "item1", 51 | "description": "Q1 of example 1", 52 | "schemaVersion": "1.0.0-rc4", 53 | "version": "0.0.1", 54 | "question": { 55 | "en": "Little interest or pleasure in doing things", 56 | "es": "Poco interés o placer en hacer cosas" 57 | }, 58 | "ui": { 59 | "inputType": "radio" 60 | }, 61 | "responseOptions": { 62 | "valueType": "xsd:integer", 63 | "minValue": 0, 64 | "maxValue": 3, 65 | "multipleChoice": false, 66 | "choices": [ 67 | { 68 | "name": { 69 | "en": "Not at all", 70 | "es": "Para nada" 71 | }, 72 | "value": 0 73 | }, 74 | { 75 | "name": { 76 | "en": "Several days", 77 | "es": "Varios días" 78 | }, 79 | "value": 1 80 | }, 81 | { 82 | "name": { 83 | "en": "More than half the days", 84 | "es": "Más de la mitad de los días" 85 | }, 86 | "value": 2 87 | }, 88 | { 89 | "name": { 90 | "en": "Nearly everyday", 91 | "es": "Casi todos los días" 92 | }, 93 | "value": 3 94 | } 95 | ] 96 | } 97 | } 98 | ], 99 | "shuffle": false 100 | } 101 | } 102 | ], 103 | "shuffle": false, 104 | "allow": [ 105 | "reproschema:AutoAdvance", 106 | "reproschema:DisableBack", 107 | "reproschema:AllowExport" 108 | ] 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /reproschema/output2redcap.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from datetime import datetime 3 | 4 | import pandas as pd 5 | import requests 6 | 7 | _LOGGER = logging.getLogger(__name__) 8 | 9 | 10 | def fetch_json_options_number(raw_url): 11 | """ 12 | Function to retrieve how many options a specific reproschema item contains. 13 | 14 | Args: 15 | raw_url: The github raw url to the given reproschema item. 16 | """ 17 | try: 18 | # fix url due to the split 19 | raw_url = raw_url.replace("combined", "questionnaires") 20 | # Make a GET request to the raw URL 21 | response = requests.get(raw_url, verify=True) 22 | response.raise_for_status() # Raise an error for bad responses (4xx, 5xx) 23 | 24 | # Parse the JSON data 25 | json_data = response.json() 26 | return len(json_data["responseOptions"]["choices"]) 27 | 28 | except requests.exceptions.RequestException as e: 29 | _LOGGER.info(f"Error fetching data: {e}") 30 | return 31 | except ValueError: 32 | _LOGGER.info("Error parsing JSON data") 33 | 34 | 35 | def parse_survey(survey_data, record_id, session_path): 36 | """ 37 | Function that generates a list of data frames in order to generate a redcap csv 38 | Args: 39 | survey_data is the raw json generated from reproschema ui 40 | record_id is the id that identifies the participant 41 | session_path is the path containing the session id 42 | """ 43 | questionnaire_name = survey_data[0]["used"][1].split("/")[-1] 44 | questions_answers = dict() 45 | questions_answers["record_id"] = [record_id] 46 | # questions_answers["redcap_repeat_instrument"] = [questionnaire_name] 47 | # questions_answers["redcap_repeat_instance"] = [1] 48 | start_time = survey_data[0]["startedAtTime"] 49 | end_time = survey_data[0]["endedAtTime"] 50 | for i in range(len(survey_data)): 51 | if survey_data[i]["@type"] == "reproschema:Response": 52 | question = survey_data[i]["isAbout"].split("/")[-1] 53 | answer = survey_data[i]["value"] 54 | if not isinstance(answer, list): 55 | questions_answers[question] = [str(answer).capitalize()] 56 | 57 | else: 58 | num = fetch_json_options_number(survey_data[i]["isAbout"]) 59 | 60 | for options in range(num): 61 | if options in answer: 62 | questions_answers[f"""{question}___{options}"""] = [ 63 | "Checked" 64 | ] 65 | 66 | else: 67 | questions_answers[f"""{question}___{options}"""] = [ 68 | "Unchecked" 69 | ] 70 | 71 | else: 72 | end_time = survey_data[i]["endedAtTime"] 73 | # Adding metadata values for redcap 74 | questions_answers[f"{questionnaire_name}_start_time"] = [start_time] 75 | questions_answers[f"{questionnaire_name}_end_time"] = [end_time] 76 | 77 | # Convert the time strings to datetime objects with UTC format 78 | time_format = "%Y-%m-%dT%H:%M:%S.%fZ" 79 | start = datetime.strptime(start_time, time_format) 80 | end = datetime.strptime(end_time, time_format) 81 | duration = end - start 82 | # convert to milliseconds 83 | duration = duration.microseconds // 1000 84 | 85 | questions_answers[f"{questionnaire_name}_duration"] = [duration] 86 | 87 | df = pd.DataFrame(questions_answers) 88 | return [df] 89 | -------------------------------------------------------------------------------- /reproschema/tests/test_output2redcap/activity_0.jsonld: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0/contexts/reproschema", 4 | "@type": "reproschema:ResponseActivity", 5 | "@id": "uuid:fa53fe26-fb71-4ae0-9e3d-8edc28b6e311", 6 | "used": [ 7 | "https://raw.githubusercontent.com/ReproNim/demo-protocol/main/activities/responsesReferenceActivity/activities/conditionalRefereeActivity_schema", 8 | "https://raw.githubusercontent.com/ReproNim/demo-protocol/main/activities/responsesReferenceActivity/responsesReferenceActivity_schema", 9 | "https://raw.githubusercontent.com/ReproNim/demo-protocol/main/DemoProtocol/DemoProtocol_schema" 10 | ], 11 | "inLanguage": "en", 12 | "startedAtTime": "2025-02-19T16:58:20.339Z", 13 | "endedAtTime": "2025-02-19T16:58:22.752Z", 14 | "wasAssociatedWith": { 15 | "version": "1.0.0", 16 | "url": "https://www.repronim.org/reproschema-ui/", 17 | "@id": "https://github.com/ReproNim/reproschema-ui" 18 | }, 19 | "generated": "uuid:5166d0e9-f5a2-4f9b-bced-7ead6f0b5ab2" 20 | }, 21 | { 22 | "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0/contexts/reproschema", 23 | "@type": "reproschema:Response", 24 | "@id": "uuid:5166d0e9-f5a2-4f9b-bced-7ead6f0b5ab2", 25 | "wasAttributedTo": { 26 | "@id": "9047bf9a-9b2f-4763-ac90-e23a8ffb0c28" 27 | }, 28 | "isAbout": "https://raw.githubusercontent.com/ReproNim/demo-protocol/main/activities/responsesReferenceActivity/activities/conditionalRefereeActivity_schema", 29 | "value": { 30 | "https://raw.githubusercontent.com/ReproNim/demo-protocol/main/activities/selectActivity/items/radio_item": 1 31 | } 32 | }, 33 | { 34 | "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0/contexts/reproschema", 35 | "@type": "reproschema:ResponseActivity", 36 | "@id": "uuid:f5b1b4cd-cd72-41ad-b362-cda8881df4f8", 37 | "used": [ 38 | "https://raw.githubusercontent.com/ReproNim/demo-protocol/main/activities/responsesReferenceActivity/activities/conditionalRefererActivity_schema", 39 | "https://raw.githubusercontent.com/ReproNim/demo-protocol/main/activities/responsesReferenceActivity/responsesReferenceActivity_schema", 40 | "https://raw.githubusercontent.com/ReproNim/demo-protocol/main/DemoProtocol/DemoProtocol_schema" 41 | ], 42 | "inLanguage": "en", 43 | "startedAtTime": "2025-02-19T16:58:22.752Z", 44 | "endedAtTime": "2025-02-19T16:58:33.990Z", 45 | "wasAssociatedWith": { 46 | "version": "1.0.0", 47 | "url": "https://www.repronim.org/reproschema-ui/", 48 | "@id": "https://github.com/ReproNim/reproschema-ui" 49 | }, 50 | "generated": "uuid:00e0e09e-8426-4d86-946c-3f7cdd6aa85d" 51 | }, 52 | { 53 | "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0/contexts/reproschema", 54 | "@type": "reproschema:Response", 55 | "@id": "uuid:00e0e09e-8426-4d86-946c-3f7cdd6aa85d", 56 | "wasAttributedTo": { 57 | "@id": "9047bf9a-9b2f-4763-ac90-e23a8ffb0c28" 58 | }, 59 | "isAbout": "https://raw.githubusercontent.com/ReproNim/demo-protocol/main/activities/responsesReferenceActivity/activities/conditionalRefererActivity_schema", 60 | "value": { 61 | "https://raw.githubusercontent.com/ReproNim/demo-protocol/main/activities/Activity3/items/text_item": "test" 62 | } 63 | } 64 | ] 65 | -------------------------------------------------------------------------------- /reproschema/tests/test_reproschema2fhir_data/activities/q_generic_gad7_anxiety/q_generic_gad7_anxiety_schema: -------------------------------------------------------------------------------- 1 | { 2 | "id": "q_generic_gad7_anxiety_schema", 3 | "category": "reproschema:Activity", 4 | "prefLabel": { 5 | "en": "q_generic_gad7_anxiety" 6 | }, 7 | "schemaVersion": "1.0.0", 8 | "ui": { 9 | "order": [ 10 | "items/gad_7_session_id", 11 | "items/gad_7_started_at", 12 | "items/gad_7_completed_at", 13 | "items/gad_7_duration", 14 | "items/nervous_anxious", 15 | "items/cant_control_worry", 16 | "items/worry_too_much", 17 | "items/trouble_relaxing", 18 | "items/hard_to_sit_still", 19 | "items/easily_agitated", 20 | "items/afraid_of_things", 21 | "items/tough_to_work" 22 | ], 23 | "addProperties": [ 24 | { 25 | "isAbout": "items/gad_7_session_id", 26 | "isVis": false, 27 | "valueRequired": true, 28 | "variableName": "gad_7_session_id" 29 | }, 30 | { 31 | "isAbout": "items/gad_7_started_at", 32 | "isVis": false, 33 | "valueRequired": true, 34 | "variableName": "gad_7_started_at" 35 | }, 36 | { 37 | "isAbout": "items/gad_7_completed_at", 38 | "isVis": false, 39 | "valueRequired": true, 40 | "variableName": "gad_7_completed_at" 41 | }, 42 | { 43 | "isAbout": "items/gad_7_duration", 44 | "isVis": false, 45 | "variableName": "gad_7_duration" 46 | }, 47 | { 48 | "isAbout": "items/nervous_anxious", 49 | "isVis": true, 50 | "valueRequired": true, 51 | "variableName": "nervous_anxious" 52 | }, 53 | { 54 | "isAbout": "items/cant_control_worry", 55 | "isVis": true, 56 | "valueRequired": true, 57 | "variableName": "cant_control_worry" 58 | }, 59 | { 60 | "isAbout": "items/worry_too_much", 61 | "isVis": true, 62 | "valueRequired": true, 63 | "variableName": "worry_too_much" 64 | }, 65 | { 66 | "isAbout": "items/trouble_relaxing", 67 | "isVis": true, 68 | "valueRequired": true, 69 | "variableName": "trouble_relaxing" 70 | }, 71 | { 72 | "isAbout": "items/hard_to_sit_still", 73 | "isVis": true, 74 | "valueRequired": true, 75 | "variableName": "hard_to_sit_still" 76 | }, 77 | { 78 | "isAbout": "items/easily_agitated", 79 | "isVis": true, 80 | "valueRequired": true, 81 | "variableName": "easily_agitated" 82 | }, 83 | { 84 | "isAbout": "items/afraid_of_things", 85 | "isVis": true, 86 | "valueRequired": true, 87 | "variableName": "afraid_of_things" 88 | }, 89 | { 90 | "isAbout": "items/tough_to_work", 91 | "isVis": "nervous_anxious > 0 || cant_control_worry > 0 || worry_too_much > 0 || trouble_relaxing > 0 || hard_to_sit_still > 0 || easily_agitated > 0 || afraid_of_things > 0", 92 | "variableName": "tough_to_work" 93 | } 94 | ], 95 | "shuffle": false 96 | }, 97 | "version": "3.20.0", 98 | "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/main/releases/1.0.0/reproschema" 99 | } 100 | -------------------------------------------------------------------------------- /reproschema/example/fhir/reproschema2fhir_data_example/activities/q_generic_gad7_anxiety/q_generic_gad7_anxiety_schema: -------------------------------------------------------------------------------- 1 | { 2 | "id": "q_generic_gad7_anxiety_schema", 3 | "category": "reproschema:Activity", 4 | "prefLabel": { 5 | "en": "q_generic_gad7_anxiety" 6 | }, 7 | "schemaVersion": "1.0.0", 8 | "ui": { 9 | "order": [ 10 | "items/gad_7_session_id", 11 | "items/gad_7_started_at", 12 | "items/gad_7_completed_at", 13 | "items/gad_7_duration", 14 | "items/nervous_anxious", 15 | "items/cant_control_worry", 16 | "items/worry_too_much", 17 | "items/trouble_relaxing", 18 | "items/hard_to_sit_still", 19 | "items/easily_agitated", 20 | "items/afraid_of_things", 21 | "items/tough_to_work" 22 | ], 23 | "addProperties": [ 24 | { 25 | "isAbout": "items/gad_7_session_id", 26 | "isVis": false, 27 | "valueRequired": true, 28 | "variableName": "gad_7_session_id" 29 | }, 30 | { 31 | "isAbout": "items/gad_7_started_at", 32 | "isVis": false, 33 | "valueRequired": true, 34 | "variableName": "gad_7_started_at" 35 | }, 36 | { 37 | "isAbout": "items/gad_7_completed_at", 38 | "isVis": false, 39 | "valueRequired": true, 40 | "variableName": "gad_7_completed_at" 41 | }, 42 | { 43 | "isAbout": "items/gad_7_duration", 44 | "isVis": false, 45 | "variableName": "gad_7_duration" 46 | }, 47 | { 48 | "isAbout": "items/nervous_anxious", 49 | "isVis": true, 50 | "valueRequired": true, 51 | "variableName": "nervous_anxious" 52 | }, 53 | { 54 | "isAbout": "items/cant_control_worry", 55 | "isVis": true, 56 | "valueRequired": true, 57 | "variableName": "cant_control_worry" 58 | }, 59 | { 60 | "isAbout": "items/worry_too_much", 61 | "isVis": true, 62 | "valueRequired": true, 63 | "variableName": "worry_too_much" 64 | }, 65 | { 66 | "isAbout": "items/trouble_relaxing", 67 | "isVis": true, 68 | "valueRequired": true, 69 | "variableName": "trouble_relaxing" 70 | }, 71 | { 72 | "isAbout": "items/hard_to_sit_still", 73 | "isVis": true, 74 | "valueRequired": true, 75 | "variableName": "hard_to_sit_still" 76 | }, 77 | { 78 | "isAbout": "items/easily_agitated", 79 | "isVis": true, 80 | "valueRequired": true, 81 | "variableName": "easily_agitated" 82 | }, 83 | { 84 | "isAbout": "items/afraid_of_things", 85 | "isVis": true, 86 | "valueRequired": true, 87 | "variableName": "afraid_of_things" 88 | }, 89 | { 90 | "isAbout": "items/tough_to_work", 91 | "isVis": "nervous_anxious > 0 || cant_control_worry > 0 || worry_too_much > 0 || trouble_relaxing > 0 || hard_to_sit_still > 0 || easily_agitated > 0 || afraid_of_things > 0", 92 | "variableName": "tough_to_work" 93 | } 94 | ], 95 | "shuffle": false 96 | }, 97 | "version": "3.20.0", 98 | "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/main/releases/1.0.0/reproschema" 99 | } 100 | -------------------------------------------------------------------------------- /reproschema/redcap_mappings.py: -------------------------------------------------------------------------------- 1 | # All the mapping used in the code 2 | REDCAP_COLUMN_MAP = { 3 | "Variable / Field Name": "item_name", # column A 4 | "Form Name": "activity_name", # column B 5 | "Section Header": "preamble", # column C 6 | "Field Type": "inputType", # column D 7 | "Field Label": "question", # column E 8 | "Choices, Calculations, OR Slider Labels": "choices", # column F 9 | "Field Note": "note", # column G 10 | "Text Validation Type OR Show Slider Number": "validation", # column H 11 | "Text Validation Min": "minValue", # column I 12 | "Text Validation Max": "maxValue", # column J 13 | "Identifier?": "identifiable", # column K 14 | "Branching Logic (Show field only if...)": "visibility", # column L 15 | "Required Field?": "valueRequired", # column M 16 | "Custom Alignment": "customAlignment", # column N 17 | "Question Number (surveys only)": "questionNumber", # column O 18 | "Matrix Group Name": "matrixGroup", # column P 19 | "Matrix Ranking?": "matrixRanking", # column Q 20 | "Field Annotation": "annotation", # column R 21 | } 22 | REDCAP_COLUMN_MAP_REVERSE = {v: k for k, v in REDCAP_COLUMN_MAP.items()} 23 | 24 | REDCAP_COLUMN_REQUIRED = [ 25 | "Variable / Field Name", 26 | "Form Name", 27 | "Field Type", 28 | "Field Label", 29 | "Choices, Calculations, OR Slider Labels", 30 | ] 31 | 32 | INPUT_TYPE_MAP = { 33 | "calc": "number", 34 | "sql": "number", 35 | "yesno": "radio", 36 | "radio": "radio", 37 | "truefalse": "radio", 38 | "checkbox": "radio", 39 | "descriptive": "static", 40 | "dropdown": "select", 41 | "text": "text", 42 | "notes": "text", 43 | "file": "documentUpload", 44 | "slider": "slider", 45 | } 46 | 47 | # Map certain field types directly to xsd types 48 | VALUE_TYPE_MAP = { 49 | # Basic types 50 | "text": "xsd:string", 51 | "email": "xsd:string", 52 | "phone": "xsd:string", 53 | "signature": "xsd:string", 54 | "zipcode": "xsd:string", 55 | "autocomplete": "xsd:string", 56 | # Numeric types 57 | "number": "xsd:decimal", # This includes both integer and float, redcap use for both 58 | "float": "xsd:decimal", 59 | "integer": "xsd:integer", 60 | # Date and time types will be handled by pattern matching in process_input_value_types 61 | # These entries are kept for backward compatibility 62 | "date_": "xsd:date", 63 | "time_": "xsd:time", 64 | } 65 | 66 | # field types that should be used as compute 67 | COMPUTE_LIST = ["calc", "sql"] 68 | RESPONSE_COND = ["minValue", "maxValue"] 69 | ADDITIONAL_NOTES_LIST = [ 70 | "Field Note", 71 | "Question Number (surveys only)", 72 | "Matrix Group Name", 73 | "Matrix Ranking?", 74 | "Text Validation Type OR Show Slider Number", 75 | "Text Validation Min", 76 | "Text Validation Max", 77 | "Identifier?", 78 | "Custom Alignment", 79 | "Question Number (surveys only)", 80 | "Field Annotation", 81 | ] 82 | 83 | 84 | def get_value_type(validation_type): 85 | """ 86 | Determine the XSD value type based on REDCap validation type 87 | 88 | Args: 89 | validation_type (str): Validation type from REDCap 90 | 91 | Returns: 92 | str: XSD value type for ReproSchema 93 | """ 94 | # Handle date and time formats with pattern matching 95 | if validation_type.startswith("date_"): 96 | return "xsd:date" 97 | elif validation_type.startswith("datetime_"): 98 | return "xsd:dateTime" 99 | elif validation_type.startswith("time"): 100 | return "xsd:time" 101 | elif validation_type in VALUE_TYPE_MAP: 102 | return VALUE_TYPE_MAP[validation_type] 103 | else: 104 | raise ValueError( 105 | f"Validation type: {validation_type} is not supported yet. " 106 | "Please add it to VALUE_TYPE_MAP." 107 | ) 108 | -------------------------------------------------------------------------------- /reproschema/validate.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pathlib import Path 3 | 4 | from .jsonldutils import load_file, validate_data 5 | from .utils import lgr, start_server, stop_server 6 | 7 | DIR_TO_SKIP = [ 8 | ".git", 9 | ".github", 10 | "__pycache__", 11 | "env", 12 | "venv", 13 | ] 14 | FILES_TO_SKIP = [ 15 | ".DS_Store", 16 | ".gitignore", 17 | ".flake8", 18 | ".autorc", 19 | "LICENSE", 20 | "Makefile", 21 | ] 22 | SUPPORTED_EXTENSIONS = [ 23 | ".jsonld", 24 | "json", 25 | "js", 26 | "", 27 | ] 28 | 29 | 30 | def validate_dir( 31 | directory: str, 32 | started: bool = False, 33 | http_kwargs: None | dict[str, int] = None, 34 | stop=None, 35 | ): 36 | """Validate a directory containing JSONLD documents against the ReproSchema pydantic model. 37 | 38 | Recursively goes through the directory tree and validates files with the allowed extensions. 39 | 40 | Parameters 41 | ---------- 42 | directory: str 43 | Path to directory to walk for validation 44 | 45 | started : bool 46 | Whether an http server exists or not 47 | 48 | http_kwargs : dict or None 49 | Keyword arguments for the http server. Valid keywords are: port, path 50 | and tmpdir 51 | 52 | stop: None or function 53 | Function to use to stop the HTTP server 54 | 55 | Returns 56 | ------- 57 | conforms: bool 58 | Whether the document is conformant with the shape. Raises an exception 59 | if any document is non-conformant. 60 | 61 | """ 62 | if http_kwargs is None: 63 | http_kwargs = {} 64 | 65 | directory = Path(directory) 66 | 67 | if not directory.is_dir(): 68 | if stop is not None: 69 | stop_server(stop) 70 | raise Exception(f"{str(directory)} is not a directory") 71 | 72 | if directory.name in DIR_TO_SKIP: 73 | lgr.info(f"Skipping directory {directory}") 74 | return True 75 | 76 | lgr.info(f"Validating directory {directory}") 77 | 78 | files_to_validate = [ 79 | str(x) 80 | for x in directory.iterdir() 81 | if x.is_file() 82 | and x.name not in FILES_TO_SKIP 83 | and x.suffix in SUPPORTED_EXTENSIONS 84 | ] 85 | 86 | for name in files_to_validate: 87 | lgr.debug(f"Validating file {name}") 88 | 89 | try: 90 | data = load_file(name, started=started, http_kwargs=http_kwargs) 91 | if len(data) == 0: 92 | if stop is not None: 93 | stop_server(stop) 94 | raise ValueError(f"Empty data graph in file {name}") 95 | conforms, vtext = validate_data(data, schemaname=Path(name).name) 96 | except (ValueError, json.JSONDecodeError): 97 | if stop is not None: 98 | stop_server(stop) 99 | raise 100 | else: 101 | if not conforms: 102 | lgr.critical(f"File {name} has validation errors.") 103 | stop_server(stop) 104 | raise ValueError(vtext) 105 | 106 | dirs_to_validate = [ 107 | str(x) 108 | for x in directory.iterdir() 109 | if x.is_dir() and x.name not in DIR_TO_SKIP 110 | ] 111 | 112 | for dir in dirs_to_validate: 113 | conforms, stop = validate_dir( 114 | dir, started=started, http_kwargs=http_kwargs, stop=stop 115 | ) 116 | 117 | return True, stop 118 | 119 | 120 | def validate(path): 121 | """Helper function to validate directory or path 122 | 123 | Parameters 124 | ---------- 125 | path : path-like 126 | Path to folder or file containing JSONLD documents. 127 | 128 | Returns 129 | ------- 130 | conforms : bool 131 | Returns true if the folder or file conforms else raises ValueError 132 | exception. 133 | 134 | """ 135 | if Path(path).is_dir(): 136 | lgr.info(f"Validating directory {path}") 137 | stop, port = start_server() 138 | http_kwargs = {"port": port} 139 | started = True 140 | conforms, _ = validate_dir( 141 | path, started=started, http_kwargs=http_kwargs, stop=stop 142 | ) 143 | stop_server(stop) 144 | else: 145 | if Path(path).name in FILES_TO_SKIP: 146 | lgr.info(f"Skipping file {path}") 147 | return True 148 | data = load_file(path, started=False) 149 | conforms, vtext = validate_data(data, schemaname=Path(path).name) 150 | if not conforms: 151 | lgr.critical(f"File {path} has validation errors.") 152 | raise ValueError(vtext) 153 | 154 | lgr.info(f"{path} conforms.") 155 | 156 | return conforms 157 | -------------------------------------------------------------------------------- /reproschema/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import threading 3 | from copy import deepcopy 4 | from http.server import HTTPServer, SimpleHTTPRequestHandler 5 | from tempfile import mkdtemp 6 | 7 | import requests_cache 8 | 9 | from . import get_logger 10 | 11 | lgr = get_logger() 12 | 13 | 14 | class LoggingRequestHandler(SimpleHTTPRequestHandler): 15 | def log_message(self, format, *args): 16 | lgr.debug(format % args) 17 | 18 | 19 | def simple_http_server(host="localhost", port=4001, path="."): 20 | """ 21 | From: https://stackoverflow.com/a/38943044 22 | """ 23 | 24 | server = HTTPServer((host, port), LoggingRequestHandler) 25 | thread = threading.Thread(target=server.serve_forever) 26 | thread.deamon = True 27 | 28 | cwd = os.getcwd() 29 | 30 | def start(): 31 | os.chdir(path) 32 | thread.start() 33 | lgr.debug("starting server on port {}".format(server.server_port)) 34 | 35 | def stop(): 36 | os.chdir(cwd) 37 | server.shutdown() 38 | server.socket.close() 39 | lgr.debug("stopping server on port {}".format(server.server_port)) 40 | 41 | return start, stop, port 42 | 43 | 44 | def start_server(port=8000, path=None, tmpdir=None): 45 | if path is None: 46 | path = os.getcwd() 47 | requests_cache.install_cache(tmpdir or mkdtemp()) 48 | start, stop, port = simple_http_server(port=port, path=path) 49 | start() 50 | return stop, port 51 | 52 | 53 | def stop_server(stop): 54 | stop() 55 | requests_cache.clear() 56 | 57 | 58 | # items that have to be fixed in the old schema 59 | LANG_FIX = [ 60 | "http://schema.org/schemaVersion", 61 | "http://schema.org/version", 62 | "http://schema.repronim.org/limit", 63 | "http://schema.repronim.org/randomMaxDelay", 64 | "http://schema.org/inLanguage", 65 | "http://schema.repronim.org/schedule", 66 | ] 67 | BOOL_FIX = [ 68 | "http://schema.repronim.org/shuffle", 69 | "http://schema.org/readonlyValue", 70 | "http://schema.repronim.org/multipleChoice", 71 | "http://schema.org/valueRequired", 72 | ] 73 | 74 | ALLOWTYPE_FIX = ["http://schema.repronim.org/allow"] 75 | ALLOWTYPE_MAPPING = { 76 | "http://schema.repronim.org/Skipped": "http://schema.repronim.org/AllowSkip", 77 | "http://schema.repronim.org/DontKnow": "http://schema.repronim.org/AllowAltResponse", 78 | } 79 | 80 | IMAGE_FIX = ["http://schema.org/image"] 81 | 82 | 83 | def _lang_fix(data_el): 84 | if isinstance(data_el, dict): 85 | data_el.pop("@language", None) 86 | elif isinstance(data_el, list) and len(data_el) == 1: 87 | data_el = data_el[0] 88 | data_el.pop("@language", None) 89 | else: 90 | raise Exception(f"expected a list or dictionary, got {data_el}") 91 | return data_el 92 | 93 | 94 | def _image_fix(data_el): 95 | if isinstance(data_el, dict): 96 | if "@id" not in data_el and "@value" in data_el: 97 | data_el["@id"] = data_el.pop("@value") 98 | data_el.pop("@language", None) 99 | elif isinstance(data_el, list) and len(data_el) == 1: 100 | data_el = data_el[0] 101 | data_el = _image_fix(data_el) 102 | else: 103 | raise Exception(f"expected a list or dictionary, got {data_el}") 104 | return data_el 105 | 106 | 107 | def _bool_fix(data_el): 108 | if isinstance(data_el, dict): 109 | data_el["@type"] = "http://www.w3.org/2001/XMLSchema#boolean" 110 | elif isinstance(data_el, list): 111 | for el in data_el: 112 | _bool_fix(el) 113 | else: 114 | raise Exception(f"expected a list or dictionary, got {data_el}") 115 | 116 | 117 | def _allowtype_fix(data_el): 118 | if isinstance(data_el, dict): 119 | if data_el["@id"] in ALLOWTYPE_MAPPING: 120 | data_el["@id"] = ALLOWTYPE_MAPPING[data_el["@id"]] 121 | elif isinstance(data_el, list): 122 | for el in data_el: 123 | _allowtype_fix(el) 124 | else: 125 | raise Exception(f"expected a list or dictionary, got {data_el}") 126 | 127 | 128 | def fixing_old_schema(data, copy_data=False): 129 | """Fixes the old schema so it can be load to the new model""" 130 | if copy_data: 131 | data = deepcopy(data) 132 | for key, val in data.items(): 133 | if key in LANG_FIX: 134 | data[key] = _lang_fix(val) 135 | elif key in BOOL_FIX: 136 | _bool_fix(val) 137 | elif key in ALLOWTYPE_FIX: 138 | _allowtype_fix(val) 139 | elif key in IMAGE_FIX: 140 | data[key] = _image_fix(val) 141 | elif isinstance(val, (str, bool, int, float)): 142 | pass 143 | elif isinstance(val, dict): 144 | fixing_old_schema(val) 145 | elif isinstance(val, list): 146 | for el in val: 147 | if isinstance(el, (str, bool, int, float)): 148 | pass 149 | elif isinstance(el, dict): 150 | fixing_old_schema(el) 151 | else: 152 | raise Exception( 153 | f"expected a list, str, bool or numerics, got {data_el}" 154 | ) 155 | else: 156 | raise Exception(f"type {type(val)} not supported yet") 157 | return data 158 | -------------------------------------------------------------------------------- /reproschema/tests/test_rs2redcap_data/test_redcap2rs/activities/cognitive_and_affective_mindfulness_scalerevised_c/cognitive_and_affective_mindfulness_scalerevised_c_schema: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", 3 | "@type": "reproschema:Activity", 4 | "@id": "cognitive_and_affective_mindfulness_scalerevised_c_schema", 5 | "prefLabel": "cognitive_and_affective_mindfulness_scalerevised_c", 6 | "description": "Default description", 7 | "schemaVersion": "1.0.0-rc4", 8 | "version": "0.0.1", 9 | "ui": { 10 | "order": [ 11 | "items/cams_r_1", 12 | "items/cams_r_1", 13 | "items/cams_r_2", 14 | "items/cams_r_1", 15 | "items/cams_r_2", 16 | "items/cams_r_3", 17 | "items/cams_r_1", 18 | "items/cams_r_2", 19 | "items/cams_r_3", 20 | "items/cams_r_4", 21 | "items/cams_r_1", 22 | "items/cams_r_2", 23 | "items/cams_r_3", 24 | "items/cams_r_4", 25 | "items/cams_r_5", 26 | "items/cams_r_1", 27 | "items/cams_r_2", 28 | "items/cams_r_3", 29 | "items/cams_r_4", 30 | "items/cams_r_5", 31 | "items/cams_r_6", 32 | "items/cams_r_1", 33 | "items/cams_r_2", 34 | "items/cams_r_3", 35 | "items/cams_r_4", 36 | "items/cams_r_5", 37 | "items/cams_r_6", 38 | "items/cams_r_7", 39 | "items/cams_r_1", 40 | "items/cams_r_2", 41 | "items/cams_r_3", 42 | "items/cams_r_4", 43 | "items/cams_r_5", 44 | "items/cams_r_6", 45 | "items/cams_r_7", 46 | "items/cams_r_8", 47 | "items/cams_r_1", 48 | "items/cams_r_2", 49 | "items/cams_r_3", 50 | "items/cams_r_4", 51 | "items/cams_r_5", 52 | "items/cams_r_6", 53 | "items/cams_r_7", 54 | "items/cams_r_8", 55 | "items/cams_r_9", 56 | "items/cams_r_1", 57 | "items/cams_r_2", 58 | "items/cams_r_3", 59 | "items/cams_r_4", 60 | "items/cams_r_5", 61 | "items/cams_r_6", 62 | "items/cams_r_7", 63 | "items/cams_r_8", 64 | "items/cams_r_9", 65 | "items/cams_r_10", 66 | "items/cams_r_1", 67 | "items/cams_r_2", 68 | "items/cams_r_3", 69 | "items/cams_r_4", 70 | "items/cams_r_5", 71 | "items/cams_r_6", 72 | "items/cams_r_7", 73 | "items/cams_r_8", 74 | "items/cams_r_9", 75 | "items/cams_r_10", 76 | "items/cams_r_11", 77 | "items/cams_r_1", 78 | "items/cams_r_2", 79 | "items/cams_r_3", 80 | "items/cams_r_4", 81 | "items/cams_r_5", 82 | "items/cams_r_6", 83 | "items/cams_r_7", 84 | "items/cams_r_8", 85 | "items/cams_r_9", 86 | "items/cams_r_10", 87 | "items/cams_r_11", 88 | "items/cams_r_12" 89 | ], 90 | "addProperties": [ 91 | { 92 | "variableName": "cams_r_1", 93 | "isAbout": "items/cams_r_1", 94 | "isVis": true 95 | }, 96 | { 97 | "variableName": "cams_r_2", 98 | "isAbout": "items/cams_r_2", 99 | "isVis": true 100 | }, 101 | { 102 | "variableName": "cams_r_3", 103 | "isAbout": "items/cams_r_3", 104 | "isVis": true 105 | }, 106 | { 107 | "variableName": "cams_r_4", 108 | "isAbout": "items/cams_r_4", 109 | "isVis": true 110 | }, 111 | { 112 | "variableName": "cams_r_5", 113 | "isAbout": "items/cams_r_5", 114 | "isVis": true 115 | }, 116 | { 117 | "variableName": "cams_r_6", 118 | "isAbout": "items/cams_r_6", 119 | "isVis": true 120 | }, 121 | { 122 | "variableName": "cams_r_7", 123 | "isAbout": "items/cams_r_7", 124 | "isVis": true 125 | }, 126 | { 127 | "variableName": "cams_r_8", 128 | "isAbout": "items/cams_r_8", 129 | "isVis": true 130 | }, 131 | { 132 | "variableName": "cams_r_9", 133 | "isAbout": "items/cams_r_9", 134 | "isVis": true 135 | }, 136 | { 137 | "variableName": "cams_r_10", 138 | "isAbout": "items/cams_r_10", 139 | "isVis": true 140 | }, 141 | { 142 | "variableName": "cams_r_11", 143 | "isAbout": "items/cams_r_11", 144 | "isVis": true 145 | }, 146 | { 147 | "variableName": "cams_r_12", 148 | "isAbout": "items/cams_r_12", 149 | "isVis": true 150 | } 151 | ], 152 | "shuffle": false 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /reproschema/tests/test_process_choices.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | 4 | import pytest 5 | import yaml 6 | from click.testing import CliRunner 7 | 8 | from ..cli import main 9 | from ..redcap2reproschema import process_choices 10 | 11 | 12 | def test_process_choices_numeric_codes(): 13 | # Test standard numeric codes with descriptions 14 | choices_str = "1, Male | 2, Female | 3, Other" 15 | choices, value_types = process_choices(choices_str, "gender") 16 | assert choices == [ 17 | {"name": {"en": "Male"}, "value": 1}, 18 | {"name": {"en": "Female"}, "value": 2}, 19 | {"name": {"en": "Other"}, "value": 3}, 20 | ] 21 | assert value_types == ["xsd:integer"] 22 | 23 | 24 | def test_process_choices_boolean(): 25 | # Test boolean choices (Yes/No) 26 | choices_str = "1, Yes | 0, No" 27 | choices, value_types = process_choices(choices_str, "boolean_field") 28 | assert choices == [ 29 | {"name": {"en": "Yes"}, "value": 1}, 30 | {"name": {"en": "No"}, "value": 0}, 31 | ] 32 | assert value_types == ["xsd:integer"] 33 | 34 | 35 | def test_process_choices_special_characters(): 36 | # Test choices with special characters 37 | choices_str = "1, Option A | 2, \"Option B\" | 3, Option C with 'quotes'" 38 | choices, value_types = process_choices(choices_str, "special_chars") 39 | assert choices == [ 40 | {"name": {"en": "Option A"}, "value": 1}, 41 | {"name": {"en": '"Option B"'}, "value": 2}, 42 | {"name": {"en": "Option C with 'quotes'"}, "value": 3}, 43 | ] 44 | assert value_types == ["xsd:integer"] 45 | 46 | 47 | def test_process_choices_with_missing_values(): 48 | # Test choices with a missing value (commonly used for "Not applicable" or "Prefer not to say") 49 | choices_str = "1, Yes | 2, No | 99, Not applicable" 50 | choices, value_types = process_choices(choices_str, "missing_values") 51 | assert choices == [ 52 | {"name": {"en": "Yes"}, "value": 1}, 53 | {"name": {"en": "No"}, "value": 2}, 54 | {"name": {"en": "Not applicable"}, "value": 99}, 55 | ] 56 | assert value_types == ["xsd:integer"] 57 | 58 | 59 | def test_process_choices_with_unicode(): 60 | # Test choices with Unicode characters (e.g., accents, symbols) 61 | choices_str = "1, Café | 2, Niño | 3, Résumé | 4, ☺" 62 | choices, value_types = process_choices(choices_str, "unicode_field") 63 | assert choices == [ 64 | {"name": {"en": "Café"}, "value": 1}, 65 | {"name": {"en": "Niño"}, "value": 2}, 66 | {"name": {"en": "Résumé"}, "value": 3}, 67 | {"name": {"en": "☺"}, "value": 4}, 68 | ] 69 | assert value_types == ["xsd:integer"] 70 | 71 | 72 | def test_process_choices_alpha_codes(): 73 | # Test alpha codes (e.g., categorical text codes) 74 | choices_str = "A, Apple | B, Banana | C, Cherry" 75 | choices, value_types = process_choices(choices_str, "alpha_codes") 76 | assert choices == [ 77 | {"name": {"en": "Apple"}, "value": "A"}, 78 | {"name": {"en": "Banana"}, "value": "B"}, 79 | {"name": {"en": "Cherry"}, "value": "C"}, 80 | ] 81 | assert sorted(value_types) == ["xsd:string"] 82 | 83 | 84 | def test_process_choices_incomplete_values(): 85 | # Test choices with missing descriptions 86 | choices_str = "1, Yes | 2, | 3, No" 87 | choices, value_types = process_choices(choices_str, "incomplete_values") 88 | assert choices == [ 89 | {"name": {"en": "Yes"}, "value": 1}, 90 | {"name": {"en": "No"}, "value": 3}, 91 | ] 92 | assert value_types == ["xsd:integer"] 93 | 94 | 95 | def test_process_choices_numeric_strings(): 96 | # Test numeric strings as values (e.g., not converted to integers) 97 | choices_str = "001, Option 001 | 002, Option 002 | 003, Option 003" 98 | choices, value_types = process_choices(choices_str, "numeric_strings") 99 | assert choices == [ 100 | {"name": {"en": "Option 001"}, "value": "001"}, 101 | {"name": {"en": "Option 002"}, "value": "002"}, 102 | {"name": {"en": "Option 003"}, "value": "003"}, 103 | ] 104 | assert sorted(value_types) == ["xsd:string"] 105 | 106 | 107 | def test_process_choices_spaces_in_values(): 108 | # Test choices with spaces in values and names 109 | choices_str = "A B, Choice AB | C D, Choice CD" 110 | choices, value_types = process_choices(choices_str, "spaces_in_values") 111 | assert choices == [ 112 | {"name": {"en": "Choice AB"}, "value": "A B"}, 113 | {"name": {"en": "Choice CD"}, "value": "C D"}, 114 | ] 115 | assert sorted(value_types) == ["xsd:string"] 116 | 117 | 118 | def test_process_choices_likert_scale(): 119 | # Test Likert scale choices with number prefixes in descriptions 120 | choices_str = "0, 0=None (Not at all) | 1, 1=Slight (Rare, less than a day or two) | 2, 2=Mild (Several days) | 3, 3=Moderate (More than half the days) | 4, 4=Severe (Nearly every day)" 121 | choices, value_types = process_choices(choices_str, "likert_scale") 122 | assert choices == [ 123 | {"name": {"en": "0=None (Not at all)"}, "value": 0}, 124 | { 125 | "name": {"en": "1=Slight (Rare, less than a day or two)"}, 126 | "value": 1, 127 | }, 128 | {"name": {"en": "2=Mild (Several days)"}, "value": 2}, 129 | {"name": {"en": "3=Moderate (More than half the days)"}, "value": 3}, 130 | {"name": {"en": "4=Severe (Nearly every day)"}, "value": 4}, 131 | ] 132 | assert value_types == ["xsd:integer"] 133 | 134 | 135 | # Run pytest if script is called directly 136 | if __name__ == "__main__": 137 | pytest.main() 138 | -------------------------------------------------------------------------------- /reproschema/tests/data_test_nimh-minimal/nimh_minimal/nimh_minimal/nimh_minimal_schema: -------------------------------------------------------------------------------- 1 | { 2 | "@context": [ 3 | "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", 4 | { 5 | "activity_path": "https://raw.githubusercontent.com/ReproNim/reproschema-library/main/activities/" 6 | } 7 | ], 8 | "@type": "reproschema:Protocol", 9 | "@id": "nimh_minimal_schema", 10 | "prefLabel": "NIMH collection", 11 | "description": "Minimal list of data collection instruments that would be ideal for use by all mental health researchers conducting clinical research to facilitate and harmonize mental health data collection.", 12 | "schemaVersion": "1.0.0-rc4", 13 | "version": "0.0.1", 14 | "landingPage": [ 15 | { 16 | "@id": "README.md", 17 | "inLanguage": "en" 18 | } 19 | ], 20 | "ui": { 21 | "addProperties": [ 22 | { 23 | "isAbout": "../activities/demo/demo_schema", 24 | "variableName": "demo_schema", 25 | "prefLabel": { 26 | "en": "Demographics" 27 | } 28 | }, 29 | { 30 | "isAbout": "activity_path:DSM-5_A/DSM5_crosscutting_adult_schema", 31 | "variableName": "DSM5_crosscutting_adult_schema", 32 | "prefLabel": { 33 | "en": "DSM5 Adult" 34 | }, 35 | "isVis": "Number(demo_schema.interview_age) >= 12*18" 36 | }, 37 | { 38 | "variableName": "WHODAS12_schema", 39 | "isAbout": "activity_path:WHODAS12/WHODAS12_schema", 40 | "prefLabel": { 41 | "en": "WHODAS12" 42 | }, 43 | "isVis": "Number(demo_schema.interview_age) >= 12*18" 44 | }, 45 | { 46 | "variableName": "PHQ9_schema", 47 | "isAbout": "activity_path:PHQ-9/PHQ9_schema", 48 | "prefLabel": { 49 | "en": "PHQ9" 50 | }, 51 | "isVis": "Number(demo_schema.interview_age) >= 12*18" 52 | }, 53 | { 54 | "variableName": "GAD7_schema", 55 | "isAbout": "activity_path:GAD7/GAD7_schema", 56 | "prefLabel": { 57 | "en": "GAD7" 58 | }, 59 | "isVis": "Number(demo_schema.interview_age) >= 12*18" 60 | }, 61 | { 62 | "isAbout": "activity_path:dsm_5_parent_guardian_rated_level_1_crosscutting_s/dsm_5_parent_guardian_rated_level_1_crosscutting_s_schema_first_19", 63 | "variableName": "dsm_5_parent_guardian_rated_level_1_crosscutting_s_schema_first_19", 64 | "prefLabel": { 65 | "en": "DSM5 Parent first 19" 66 | }, 67 | "isVis": "Number(demo_schema_interview_age) > 12*6 && Number(demo_schema_interview_age) < 12*18" 68 | }, 69 | { 70 | "isAbout": "activity_path:dsm_5_parent_guardian_rated_level_1_crosscutting_s/dsm_5_parent_guardian_rated_level_1_crosscutting_s_schema_20_to_25", 71 | "variableName": "dsm_5_parent_guardian_rated_level_1_crosscutting_s_schema_20_to_25", 72 | "prefLabel": { 73 | "en": "DSM5 Parent 20 to 25" 74 | }, 75 | "isVis": "Number(demo_schema_interview_age) > 12*6 && Number(demo_schema_interview_age) < 12*18" 76 | }, 77 | { 78 | "isAbout": "activity_path:DSM-5_Y/DSM5_crosscutting_youth_schema", 79 | "variableName": "DSM5_crosscutting_youth_schema", 80 | "prefLabel": { 81 | "en": "DSM5 Youth" 82 | }, 83 | "isVis": "Number(demo_schema_interview_age) > 12*6 && Number(demo_schema_interview_age) < 12*18" 84 | }, 85 | { 86 | "isAbout": "activity_path:RCADS-25-C/RCADS25_caregiver_administered_schema", 87 | "variableName": "RCADS25_caregiver_administered_schema", 88 | "prefLabel": { 89 | "en": "RCADS-25 Caregiver" 90 | }, 91 | "isVis": "Number(demo_schema_interview_age) > 12*4 && Number(demo_schema_interview_age) < 12*18" 92 | }, 93 | { 94 | "isAbout": "activity_path:RCADS-25-Y/RCADS25_youth_administered_schema", 95 | "variableName": "RCADS25_youth_administered_schema", 96 | "prefLabel": { 97 | "en": "RCADS-25 Youth" 98 | }, 99 | "isVis": "Number(demo_schema_interview_age) > 12*4 && Number(demo_schema_interview_age) < 12*18" 100 | }, 101 | { 102 | "isAbout": "activity_path:ThankYou/ThankYou_schema", 103 | "variableName": "ThankYou_schema", 104 | "prefLabel": { 105 | "en": "Submit responses" 106 | } 107 | } 108 | ], 109 | "order": [ 110 | "../activities/demo/demo_schema", 111 | "activity_path:DSM-5_A/DSM5_crosscutting_adult_schema", 112 | "activity_path:WHODAS12/WHODAS12_schema", 113 | "activity_path:PHQ-9/PHQ9_schema", 114 | "activity_path:GAD7/GAD7_schema", 115 | "activity_path:dsm_5_parent_guardian_rated_level_1_crosscutting_s/dsm_5_parent_guardian_rated_level_1_crosscutting_s_schema_first_19", 116 | "activity_path:dsm_5_parent_guardian_rated_level_1_crosscutting_s/dsm_5_parent_guardian_rated_level_1_crosscutting_s_schema_20_to_25", 117 | "activity_path:DSM-5_Y/DSM5_crosscutting_youth_schema", 118 | "activity_path:RCADS-25-C/RCADS25_caregiver_administered_schema", 119 | "activity_path:RCADS-25-Y/RCADS25_youth_administered_schema", 120 | "activity_path:ThankYou/ThankYou_schema" 121 | ], 122 | "shuffle": false, 123 | "allow": [ 124 | "reproschema:AllowExport", 125 | "reproschema:AutoAdvance" 126 | ] 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /reproschema/tests/test_field_property.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from ..redcap2reproschema import process_row 4 | from ..redcap_mappings import REDCAP_COLUMN_MAP 5 | 6 | 7 | def update_keys(data): 8 | """Update keys in the dictionary to match the expected keys in the reproschema""" 9 | # Add default value for "Field Label" if it is not present 10 | if "Field Label" not in data: 11 | data["Field Label"] = "question" 12 | # Update keys to match the expected keys in the reproschema 13 | updated_data = {} 14 | for key, value in data.items(): 15 | updated_data[REDCAP_COLUMN_MAP[key]] = value 16 | return updated_data 17 | 18 | 19 | @pytest.mark.parametrize( 20 | "field_data,expected", 21 | [ 22 | # Test case 1: No branching logic or annotations 23 | ({"Variable / Field Name": "test_field"}, True), 24 | # Test case 2: With branching logic 25 | ( 26 | { 27 | "Variable / Field Name": "test_field", 28 | "Branching Logic (Show field only if...)": "[age] > 18", 29 | }, 30 | "age > 18", 31 | ), 32 | # Test case 3: With @HIDDEN annotation 33 | ( 34 | { 35 | "Variable / Field Name": "test_field", 36 | "Field Annotation": "@HIDDEN", 37 | }, 38 | False, 39 | ), 40 | # Test case 4: With both branching logic and @HIDDEN 41 | ( 42 | { 43 | "Variable / Field Name": "test_field", 44 | "Branching Logic (Show field only if...)": "[age] > 18", 45 | "Field Annotation": "@HIDDEN", 46 | }, 47 | False, 48 | ), 49 | ], 50 | ) 51 | def test_process_field_properties_visibility(field_data, expected): 52 | # Test case 1: No branching logic or annotations 53 | _, _, _, add_prop = process_row(update_keys(field_data)) 54 | if expected is True: 55 | assert add_prop.get("isVis", True) is True # defaults is True 56 | else: 57 | assert add_prop.get("isVis") == expected 58 | 59 | 60 | @pytest.mark.parametrize( 61 | "input,expected", 62 | [ 63 | # CALCTEXT with conditional logic 64 | ( 65 | { 66 | "Variable / Field Name": "parkinsons_diagnosis", 67 | "Required Field?": "", 68 | "Field Annotation": "@CALCTEXT(if(([diagnosis_parkinsons_gsd_category_1(bradykinesia)] && ([diagnosis_parkinsons_gsd_category_1(tremor)] || [diagnosis_parkinsons_gsd_category_1(rigidity)])), 'Yes', 'No'))", 69 | "Branching Logic (Show field only if...)": "[some_other_condition] = 1", 70 | }, 71 | { 72 | "variableName": "parkinsons_diagnosis", 73 | "isAbout": "items/parkinsons_diagnosis", 74 | "isVis": False, 75 | }, 76 | ), 77 | # CALCTEXT with numerical operations 78 | ( 79 | { 80 | "Variable / Field Name": "bmi", 81 | "Required Field?": "", 82 | "Field Annotation": "@CALCTEXT([weight]/([height]*[height]))", 83 | "Branching Logic (Show field only if...)": "[weight] > 0 and [height] > 0", 84 | }, 85 | { 86 | "variableName": "bmi", 87 | "isAbout": "items/bmi", 88 | "isVis": False, 89 | }, 90 | ), 91 | # CALCTEXT with multiple nested conditions 92 | ( 93 | { 94 | "Variable / Field Name": "complex_score", 95 | "Required Field?": "", 96 | "Field Annotation": "@CALCTEXT(if([score1] > 10 && [score2] < 5, 'High', if([score1] > 5, 'Medium', 'Low')))", 97 | "Branching Logic (Show field only if...)": "", 98 | }, 99 | { 100 | "variableName": "complex_score", 101 | "isAbout": "items/complex_score", 102 | "isVis": False, 103 | }, 104 | ), 105 | ], 106 | ) 107 | def test_process_field_properties_calctext(input, expected): 108 | """Test different CALCTEXT annotations with realistic examples""" 109 | _, _, _, add_prop = process_row(update_keys(input)) 110 | for key, expected_value in expected.items(): 111 | assert ( 112 | add_prop[key] == expected_value 113 | ), f"Failed for {key} in test case with annotation: {input['Field Annotation']}" 114 | 115 | 116 | @pytest.mark.parametrize( 117 | "input,expected", 118 | [ 119 | # CALCTEXT with READONLY 120 | ( 121 | { 122 | "Variable / Field Name": "test_var", 123 | "Required Field?": "", 124 | "Field Annotation": "@CALCTEXT @READONLY", 125 | "Branching Logic (Show field only if...)": "", 126 | }, 127 | {"isVis": False}, 128 | ), 129 | # CALCTEXT with HIDDEN 130 | ( 131 | { 132 | "Variable / Field Name": "test_var", 133 | "Required Field?": "", 134 | "Field Annotation": "@HIDDEN @CALCTEXT(if([var1] > 0, 1, 0))", 135 | "Branching Logic (Show field only if...)": "", 136 | }, 137 | {"isVis": False}, 138 | ), 139 | # Complex CALCTEXT with other annotations 140 | ( 141 | { 142 | "Variable / Field Name": "test_var", 143 | "Required Field?": "", 144 | "Field Annotation": "@CALCTEXT(if(([var1] && [var2]), 'Yes', 'No')) @READONLY @HIDDEN-SURVEY", 145 | "Branching Logic (Show field only if...)": "[condition] = 1", 146 | }, 147 | {"isVis": False}, 148 | ), 149 | ], 150 | ) 151 | def test_process_field_properties_mixed_annotations(input, expected): 152 | """Test fields with multiple annotations""" 153 | _, _, _, add_prop = process_row(update_keys(input)) 154 | for key, expected_value in expected.items(): 155 | assert ( 156 | add_prop[key] == expected_value 157 | ), f"Failed for {key} in test case with annotation: {input['Field Annotation']}" 158 | -------------------------------------------------------------------------------- /reproschema/models/tests/test_schema.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from pathlib import Path 4 | 5 | import pytest 6 | from pyld import jsonld 7 | 8 | from ...jsonldutils import load_file 9 | from ...utils import start_server, stop_server 10 | from .. import Activity, Item, Protocol, ResponseOption 11 | from ..utils import write_obj_jsonld 12 | 13 | contextfile_url = "https://raw.githubusercontent.com/ReproNim/reproschema/ref/linkml/contexts/reproschema" 14 | 15 | 16 | @pytest.fixture 17 | def server_http_kwargs(request): 18 | http_kwargs = {} 19 | stop, port = start_server() 20 | http_kwargs["port"] = port 21 | 22 | olddir = os.getcwd() 23 | os.chdir(os.path.dirname(__file__)) 24 | 25 | def stoping_server(): 26 | stop_server(stop) 27 | os.chdir(olddir) 28 | 29 | request.addfinalizer(stoping_server) 30 | return http_kwargs 31 | 32 | 33 | @pytest.mark.parametrize( 34 | "model_class", [Protocol, Activity, Item, ResponseOption] 35 | ) 36 | def test_constructors(model_class): 37 | ob = model_class() 38 | assert hasattr(ob, "id") 39 | assert hasattr(ob, "category") 40 | 41 | 42 | def test_protocol(tmp_path, server_http_kwargs): 43 | """check if protocol is created correctly for a simple example 44 | and if it can be written to the file as jsonld. 45 | """ 46 | protocol_dict = { 47 | "category": "Protocol", 48 | "id": "protocol1.jsonld", 49 | "prefLabel": {"en": "Protocol1", "es": "Protocol1_es"}, 50 | "description": {"en": "example Protocol"}, 51 | "schemaVersion": "1.0.0-rc4", 52 | "version": "0.0.1", 53 | "messages": [ 54 | { 55 | "message": { 56 | "en": "Test message: Triggered when item1 value is greater than 0" 57 | }, 58 | "jsExpression": "item1 > 0", 59 | } 60 | ], 61 | } 62 | protocol_obj = Protocol(**protocol_dict) 63 | 64 | # writing to the file 65 | file_path = tmp_path / "protocol1.jsonld" 66 | write_obj_jsonld(protocol_obj, file_path, contextfile_url) 67 | 68 | # loading data from the file and checking if this is the same as initial dictionary 69 | data_comp = load_file( 70 | file_path, 71 | started=True, 72 | http_kwargs=server_http_kwargs, 73 | compact=True, 74 | compact_context=contextfile_url, 75 | ) 76 | del data_comp["@context"] 77 | assert protocol_dict == data_comp 78 | 79 | 80 | def test_activity(tmp_path, server_http_kwargs): 81 | """check if activity is created correctly for a simple example 82 | and if it can be written to the file as jsonld.""" 83 | activity_dict = { 84 | "category": "Activity", 85 | "id": "activity1.jsonld", 86 | "prefLabel": {"en": "Example 1"}, 87 | "description": {"en": "Activity example 1"}, 88 | "schemaVersion": "1.0.0-rc4", 89 | "version": "0.0.1", 90 | "image": { 91 | "category": "AudioObject", 92 | "contentUrl": "http://example.com/sample-image.png", 93 | }, 94 | "preamble": { 95 | "en": "Over the last 2 weeks, how often have you been bothered by any of the following problems?", 96 | "es": "Durante las últimas 2 semanas, ¿con qué frecuencia le han molestado los siguintes problemas?", 97 | }, 98 | "compute": [ 99 | { 100 | "variableName": "activity1_total_score", 101 | "jsExpression": "item1 + item2", 102 | } 103 | ], 104 | } 105 | activity_obj = Activity(**activity_dict) 106 | 107 | file_path = tmp_path / "activity1.jsonld" 108 | write_obj_jsonld(activity_obj, file_path, contextfile_url) 109 | 110 | # loading data from the file and checking if this is the same as initial dictionary 111 | data_comp = load_file( 112 | file_path, 113 | started=True, 114 | http_kwargs=server_http_kwargs, 115 | compact=True, 116 | compact_context=contextfile_url, 117 | ) 118 | del data_comp["@context"] 119 | assert activity_dict == data_comp 120 | 121 | 122 | def test_item(tmp_path, server_http_kwargs): 123 | """check if item is created correctly for a simple example" 124 | and if it can be written to the file as jsonld.""" 125 | 126 | item_dict = { 127 | "category": "Field", 128 | "id": "item1.jsonld", 129 | "prefLabel": {"en": "item1"}, 130 | "altLabel": {"en": "item1_alt"}, 131 | "description": {"en": "Q1 of example 1"}, 132 | "schemaVersion": "1.0.0-rc4", 133 | "version": "0.0.1", 134 | "audio": { 135 | "category": "AudioObject", 136 | "contentUrl": "http://media.freesound.org/sample-file.mp4", 137 | }, 138 | "image": { 139 | "category": "ImageObject", 140 | "contentUrl": "http://example.com/sample-image.jpg", 141 | }, 142 | "question": { 143 | "en": "Little interest or pleasure in doing things", 144 | "es": "Poco interés o placer en hacer cosas", 145 | }, 146 | # "ui": {"inputType": "radio"}, 147 | "responseOptions": { 148 | "minValue": 0, 149 | "maxValue": 3, 150 | "multipleChoice": False, 151 | "choices": [ 152 | { 153 | "name": {"en": "Not at all", "es": "Para nada"}, 154 | "value": "a", 155 | }, 156 | { 157 | "name": {"en": "Several days", "es": "Varios días"}, 158 | "value": "b", 159 | }, 160 | ], 161 | }, 162 | } 163 | 164 | item_obj = Item(**item_dict) 165 | 166 | file_path = tmp_path / "item1.jsonld" 167 | write_obj_jsonld(item_obj, file_path, contextfile_url) 168 | 169 | # loading data from the file and checking if this is the same as initial dictionary 170 | data_comp = load_file( 171 | file_path, 172 | started=True, 173 | http_kwargs=server_http_kwargs, 174 | compact=True, 175 | compact_context=contextfile_url, 176 | ) 177 | del data_comp["@context"] 178 | assert item_dict == data_comp 179 | -------------------------------------------------------------------------------- /reproschema/example/fhir/fhir_output/q_generic_gad7_anxiety/q_generic_gad7_anxiety.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Questionnaire", 3 | "id": "qgenericgad7anxietyschema", 4 | "title": "q_generic_gad7_anxiety_schema", 5 | "version": "1.4.0", 6 | "status": "active", 7 | "date": "2025-02-24T16:41:06Z", 8 | "item": [ 9 | { 10 | "linkId": "gad_7_session_id", 11 | "type": "string", 12 | "text": "Questionnaire - Metadata: Session ID" 13 | }, 14 | { 15 | "linkId": "gad_7_started_at", 16 | "type": "string", 17 | "text": "Questionnaire Started At" 18 | }, 19 | { 20 | "linkId": "gad_7_completed_at", 21 | "type": "string", 22 | "text": "Questionnaire Completed At" 23 | }, 24 | { 25 | "linkId": "gad_7_duration", 26 | "type": "string", 27 | "text": "Questionnaire Duration (seconds)" 28 | }, 29 | { 30 | "linkId": "nervous_anxious", 31 | "type": "choice", 32 | "text": "Over the last two weeks, how often have you been bothered by the following problems?: Feeling nervous, anxious, or on edge.", 33 | "answerOption": [ 34 | { 35 | "valueString": "Not at all" 36 | }, 37 | { 38 | "valueString": "Several days" 39 | }, 40 | { 41 | "valueString": "More than half the days" 42 | }, 43 | { 44 | "valueString": "Nearly every day" 45 | } 46 | ] 47 | }, 48 | { 49 | "linkId": "cant_control_worry", 50 | "type": "choice", 51 | "text": "Not being able to stop or control worrying.", 52 | "answerOption": [ 53 | { 54 | "valueString": "Not at all" 55 | }, 56 | { 57 | "valueString": "Several days" 58 | }, 59 | { 60 | "valueString": "More than half the days" 61 | }, 62 | { 63 | "valueString": "Nearly every day" 64 | } 65 | ] 66 | }, 67 | { 68 | "linkId": "worry_too_much", 69 | "type": "choice", 70 | "text": "Worrying too much about different things.", 71 | "answerOption": [ 72 | { 73 | "valueString": "Not at all" 74 | }, 75 | { 76 | "valueString": "Several days" 77 | }, 78 | { 79 | "valueString": "More than half the days" 80 | }, 81 | { 82 | "valueString": "Nearly every day" 83 | } 84 | ] 85 | }, 86 | { 87 | "linkId": "trouble_relaxing", 88 | "type": "choice", 89 | "text": "Trouble relaxing.", 90 | "answerOption": [ 91 | { 92 | "valueString": "Not at all" 93 | }, 94 | { 95 | "valueString": "Several days" 96 | }, 97 | { 98 | "valueString": "More than half the days" 99 | }, 100 | { 101 | "valueString": "Nearly every day" 102 | } 103 | ] 104 | }, 105 | { 106 | "linkId": "hard_to_sit_still", 107 | "type": "choice", 108 | "text": "Being so restless that it is hard to sit still.", 109 | "answerOption": [ 110 | { 111 | "valueString": "Not at all" 112 | }, 113 | { 114 | "valueString": "Several days" 115 | }, 116 | { 117 | "valueString": "More than half the days" 118 | }, 119 | { 120 | "valueString": "Nearly every day" 121 | } 122 | ] 123 | }, 124 | { 125 | "linkId": "easily_agitated", 126 | "type": "choice", 127 | "text": "Becoming easily annoyed or irritable.", 128 | "answerOption": [ 129 | { 130 | "valueString": "Not at all" 131 | }, 132 | { 133 | "valueString": "Several days" 134 | }, 135 | { 136 | "valueString": "More than half the days" 137 | }, 138 | { 139 | "valueString": "Nearly every day" 140 | } 141 | ] 142 | }, 143 | { 144 | "linkId": "afraid_of_things", 145 | "type": "choice", 146 | "text": "Feeling afraid, as if something awful might happen.", 147 | "answerOption": [ 148 | { 149 | "valueString": "Not at all" 150 | }, 151 | { 152 | "valueString": "Several days" 153 | }, 154 | { 155 | "valueString": "More than half the days" 156 | }, 157 | { 158 | "valueString": "Nearly every day" 159 | } 160 | ] 161 | }, 162 | { 163 | "linkId": "tough_to_work", 164 | "type": "choice", 165 | "text": "How difficult have they made it for you to do your work, take care of things at home, or get along with other people?", 166 | "answerOption": [ 167 | { 168 | "valueString": "Not difficult at all" 169 | }, 170 | { 171 | "valueString": "Somewhat difficult" 172 | }, 173 | { 174 | "valueString": "Very difficult" 175 | }, 176 | { 177 | "valueString": "Extremely difficult" 178 | } 179 | ], 180 | "enableWhen": [ 181 | { 182 | "question": "nervous_anxious", 183 | "operator": ">", 184 | "answerString": "0" 185 | }, 186 | { 187 | "question": "cant_control_worry", 188 | "operator": ">", 189 | "answerString": "0" 190 | }, 191 | { 192 | "question": "worry_too_much", 193 | "operator": ">", 194 | "answerString": "0" 195 | }, 196 | { 197 | "question": "trouble_relaxing", 198 | "operator": ">", 199 | "answerString": "0" 200 | }, 201 | { 202 | "question": "hard_to_sit_still", 203 | "operator": ">", 204 | "answerString": "0" 205 | }, 206 | { 207 | "question": "easily_agitated", 208 | "operator": ">", 209 | "answerString": "0" 210 | }, 211 | { 212 | "question": "afraid_of_things", 213 | "operator": ">", 214 | "answerString": "0" 215 | } 216 | ], 217 | "enableBehavior": "any" 218 | } 219 | ] 220 | } 221 | -------------------------------------------------------------------------------- /reproschema/jsonldutils.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from urllib.parse import urlparse 4 | 5 | import requests 6 | from pyld import jsonld 7 | 8 | from .context_url import CONTEXTFILE_URL 9 | from .models import identify_model_class 10 | from .utils import fixing_old_schema, lgr, start_server, stop_server 11 | 12 | 13 | def _is_url(path): 14 | """ 15 | Determine whether the given path is a URL. 16 | """ 17 | parsed = urlparse(str(path)) 18 | return parsed.scheme in ("http", "https", "ftp", "ftps") 19 | 20 | 21 | def _is_file(path): 22 | """ 23 | Determine whether the given path is a valid file path. 24 | """ 25 | return os.path.isfile(path) 26 | 27 | 28 | def _fetch_jsonld_context(url): 29 | response = requests.get(url) 30 | return response.json() 31 | 32 | 33 | def load_file( 34 | path_or_url, 35 | started=False, 36 | http_kwargs=None, 37 | compact=False, 38 | compact_context=None, 39 | fixoldschema=False, 40 | ): 41 | """Load a file or URL and return the expanded JSON-LD data.""" 42 | path_or_url = str(path_or_url) 43 | if http_kwargs is None: 44 | http_kwargs = {} 45 | if _is_url(path_or_url): 46 | data = jsonld.expand(path_or_url) 47 | if len(data) == 1: 48 | if "@id" not in data[0] and "id" not in data[0]: 49 | data[0]["@id"] = path_or_url 50 | elif _is_file(path_or_url): 51 | lgr.debug("Reloading with local server") 52 | root = os.path.dirname(path_or_url) 53 | if not started: 54 | stop, port = start_server(**http_kwargs) 55 | else: 56 | if "port" not in http_kwargs: 57 | raise KeyError("port key missing in http_kwargs") 58 | port = http_kwargs["port"] 59 | base_url = f"http://localhost:{port}/" 60 | if root: 61 | base_url += f"{root}/" 62 | with open(path_or_url) as json_file: 63 | try: 64 | data = json.load(json_file) 65 | except json.JSONDecodeError as e: 66 | raise json.JSONDecodeError( 67 | f"Error parsing JSON file {json_file}: {e.msg}", 68 | e.doc, 69 | e.pos, 70 | ) from e 71 | try: 72 | data = jsonld.expand(data, options={"base": base_url}) 73 | except: 74 | raise 75 | finally: 76 | if not started: 77 | stop_server(stop) 78 | if len(data) == 1: 79 | if "@id" not in data[0] and "id" not in data[0]: 80 | data[0]["@id"] = base_url + os.path.basename(path_or_url) 81 | else: 82 | raise Exception(f"{path_or_url} is not a valid URL or file path") 83 | 84 | if isinstance(data, list) and len(data) == 1: 85 | data = data[0] 86 | 87 | if fixoldschema: 88 | data = fixing_old_schema(data, copy_data=True) 89 | if compact: 90 | if compact_context: 91 | context = read_contextfile(compact_context) 92 | if _is_file(path_or_url): 93 | data = jsonld.compact( 94 | data, ctx=context, options={"base": base_url} 95 | ) 96 | else: 97 | data = jsonld.compact(data, ctx=context) 98 | 99 | return data 100 | 101 | 102 | def validate_data(data, schemaname): 103 | """Validate an expanded jsonld document against the pydantic model. 104 | 105 | Parameters 106 | ---------- 107 | data : dict 108 | Python dictionary containing JSONLD object 109 | schemaname : str 110 | Name of the schema (name of the file) being validated 111 | 112 | Returns 113 | ------- 114 | conforms: bool 115 | Whether the document is conformant with the shape 116 | v_text: str 117 | Validation errors if any returned by pydantic 118 | 119 | """ 120 | obj_type = identify_model_class(data["@type"][0]) 121 | data_fixed = [fixing_old_schema(data, copy_data=True)] 122 | context = read_contextfile(CONTEXTFILE_URL) 123 | data_fixed_comp = jsonld.compact(data_fixed, context) 124 | del data_fixed_comp["@context"] 125 | if obj_type.__name__ in ["Item", "Activity", "Protocol"]: 126 | if data_fixed_comp["id"].split("/")[-1] != schemaname: 127 | raise Exception( 128 | f"Document {data['@id']} does not match the schema name {schemaname}" 129 | ) 130 | conforms = False 131 | v_text = "" 132 | try: 133 | obj_type(**data_fixed_comp) 134 | conforms = True 135 | except Exception as e: 136 | v_text = str(e) 137 | return conforms, v_text 138 | 139 | 140 | def read_contextfile(contextfile): 141 | """Read a context file and return the context.""" 142 | if _is_file(contextfile): 143 | with open(contextfile) as fp: 144 | context = json.load(fp) 145 | elif _is_url(contextfile): 146 | context = _fetch_jsonld_context(contextfile) 147 | else: 148 | raise Exception( 149 | f"compact_context has tobe a file or url, but {contextfile} provided" 150 | ) 151 | return context 152 | 153 | 154 | def get_context_version(contextfile): 155 | """Get the version from the context file path""" 156 | from packaging.version import InvalidVersion, Version 157 | 158 | if contextfile.split("/")[-3] != "releases": 159 | raise ValueError( 160 | f"Can't get the version from {contextfile}, expected to have releases in the path" 161 | ) 162 | else: 163 | try: 164 | Version(contextfile.split("/")[-2]) 165 | return contextfile.split("/")[-2] 166 | except InvalidVersion: 167 | raise ValueError( 168 | f"Can't get the version from {contextfile}, " 169 | f"expected to have a valid version in the path, " 170 | f"but got {contextfile.split('/')[-2]}" 171 | ) 172 | 173 | 174 | def to_newformat(path, format, prefixfile=None, contextfile=None): 175 | """Convert a JSONLD document to n-triples format 176 | 177 | Since PyLD requires an http url, a local server is started to serve the 178 | document. 179 | 180 | Parameters 181 | ---------- 182 | path : str 183 | A local path or remote url to convert to n-triples 184 | format: str of enum 185 | Returned format jsonld, n-triples, turtle 186 | prefixfile: str 187 | Prefixes to use when converting to turtle (ignored otherwise) 188 | contextfile: str 189 | Context to use for compaction when returning jsonld. If not provided, 190 | a jsonld graph is returned. 191 | 192 | Returns 193 | ------- 194 | normalized : str 195 | A normalized document 196 | 197 | """ 198 | supported_formats = ["jsonld", "n-triples", "turtle"] 199 | if format not in supported_formats: 200 | raise ValueError(f"{format} not in {supported_formats}") 201 | data = load_file(path) 202 | if format == "jsonld": 203 | if contextfile is not None: 204 | context = read_contextfile(contextfile) 205 | data = jsonld.compact(data, context) 206 | return json.dumps(data, indent=2) 207 | kwargs = {"algorithm": "URDNA2015", "format": "application/n-quads"} 208 | nt = jsonld.normalize(data, kwargs) 209 | if format == "n-triples": 210 | return nt 211 | import rdflib as rl 212 | 213 | g = rl.Graph() 214 | g.bind("rs", "http://schema.repronim.org/") 215 | g.bind("sdo", "http://schema.org/") 216 | g.bind("nidm", "http://purl.org/nidash/nidm#") 217 | g.bind("skos", "http://www.w3.org/2004/02/skos/core#") 218 | g.bind("prov", "http://www.w3.org/ns/prov#") 219 | if prefixfile is not None: 220 | with open(prefixfile) as fp: 221 | prefixes = json.load(fp) 222 | for key, value in prefixes.items(): 223 | g.bind(key, value) 224 | g.parse(data=nt, format="nt") 225 | return g.serialize(format=format) 226 | -------------------------------------------------------------------------------- /reproschema/convertutils.py: -------------------------------------------------------------------------------- 1 | import re 2 | from pathlib import Path 3 | from typing import Any, Dict, List 4 | 5 | import yaml 6 | from bs4 import BeautifulSoup 7 | 8 | from .context_url import CONTEXTFILE_URL 9 | from .jsonldutils import get_context_version 10 | from .models import Activity, Item, Protocol, write_obj_jsonld 11 | 12 | PROTOCOL_KEYS_REQUIRED = [ 13 | "protocol_name", 14 | "protocol_display_name", 15 | "redcap_version", 16 | ] 17 | 18 | 19 | def read_check_yaml_config(yaml_path: str) -> Dict[str, Any]: 20 | """Read and check the YAML configuration file.""" 21 | try: 22 | with open(yaml_path, "r", encoding="utf-8") as f: 23 | protocol = yaml.safe_load(f) 24 | except yaml.YAMLError as e: 25 | raise ValueError(f"Invalid YAML file: {str(e)}") 26 | if set(PROTOCOL_KEYS_REQUIRED) - set(protocol.keys()): 27 | raise ValueError( 28 | f"Missing required keys in YAML file: {set(PROTOCOL_KEYS_REQUIRED) - set(protocol.keys())}" 29 | ) 30 | return protocol 31 | 32 | 33 | def normalize_condition(condition_str, field_type=None): 34 | """Normalize condition strings with specific handling for calc fields.""" 35 | 36 | # Handle boolean values 37 | if isinstance(condition_str, bool): 38 | return condition_str 39 | if isinstance(condition_str, str): 40 | if condition_str.lower() == "true": 41 | return True 42 | if condition_str.lower() == "false": 43 | return False 44 | 45 | # Convert to string if needed 46 | if not isinstance(condition_str, str): 47 | try: 48 | condition_str = str(condition_str) 49 | except: 50 | raise ValueError("Condition must be a string or boolean") 51 | 52 | # Clean HTML 53 | condition_str = BeautifulSoup(condition_str, "html.parser").get_text() 54 | condition_str = condition_str.strip() 55 | 56 | if condition_str is None: 57 | return None 58 | 59 | # Common operator normalizations for all types 60 | operator_replacements = [ 61 | (r"\s*\+\s*", " + "), # Normalize spacing around + 62 | (r"\s*-\s*", " - "), # Normalize spacing around - 63 | (r"\s*\*\s*", " * "), # Normalize spacing around * 64 | (r"\s*\/\s*", " / "), # Normalize spacing around / 65 | (r"\s*\(\s*", "("), # Remove spaces after opening parenthesis 66 | (r"\s*\)\s*", ")"), # Remove spaces before closing parenthesis 67 | (r"\s*,\s*", ","), # Normalize spaces around commas 68 | (r"\s+", " "), # Normalize multiple spaces 69 | ] 70 | 71 | # Apply operator normalizations first 72 | for pattern, repl in operator_replacements: 73 | condition_str = re.sub(pattern, repl, condition_str) 74 | 75 | # Then apply type-specific replacements 76 | if field_type in ["sql", "calc"]: 77 | # For calc fields, just remove brackets from field references 78 | condition_str = re.sub(r"\[([^\]]+)\]", r"\1", condition_str) 79 | else: 80 | # For branching logic 81 | replacements = [ 82 | (r"\(([0-9]*)\)", r"___\1"), 83 | (r"([^>|<])=", r"\1=="), 84 | (r"\[([^\]]*)\]", r"\1"), # Remove brackets and extra spaces 85 | (r"\bor\b", "||"), 86 | (r"\band\b", "&&"), 87 | (r'"', "'"), 88 | ] 89 | for pattern, repl in replacements: 90 | condition_str = re.sub(pattern, repl, condition_str) 91 | 92 | result = condition_str.strip() 93 | return result 94 | 95 | 96 | def parse_html(input_string, default_language="en"): 97 | """ 98 | Parse HTML content and extract language-specific text. 99 | 100 | Args: 101 | input_string: The HTML string to parse 102 | default_language: Default language code (default: "en") 103 | 104 | Returns: 105 | dict: Dictionary of language codes to text content, or None if invalid 106 | """ 107 | try: 108 | result = {} 109 | 110 | # Handle non-string input 111 | if not isinstance(input_string, str): 112 | try: 113 | input_string = str(input_string) 114 | except: 115 | return None 116 | 117 | # Clean input string 118 | input_string = input_string.strip() 119 | if not input_string: 120 | return None 121 | 122 | # Parse HTML 123 | soup = BeautifulSoup(input_string, "html.parser") 124 | 125 | # Find elements with lang attribute 126 | lang_elements = soup.find_all(True, {"lang": True}) 127 | 128 | if lang_elements: 129 | # Process elements with language tags 130 | for element in lang_elements: 131 | lang = element.get("lang", default_language).lower() 132 | text = element.get_text(strip=True) 133 | if text: 134 | result[lang] = text 135 | 136 | # If no text was extracted but elements exist, try getting default text 137 | if not result: 138 | text = soup.get_text(strip=True) 139 | if text: 140 | result[default_language] = text 141 | else: 142 | # No language tags found, use default language 143 | text = soup.get_text(strip=True) 144 | if text: 145 | result[default_language] = text 146 | 147 | return result if result else None 148 | 149 | except Exception as e: 150 | print(f"Error parsing HTML: {str(e)}, trying plain text") 151 | # Try to return plain text if HTML parsing fails 152 | try: 153 | if isinstance(input_string, str) and input_string.strip(): 154 | return {default_language: input_string.strip()} 155 | except: 156 | raise ValueError(f"Invalid input for HTML parsing: {input_string}") 157 | 158 | 159 | def create_activity_schema( 160 | activity_name: str, 161 | activity_data: Dict[str, Any], 162 | output_path: Path, 163 | redcap_version: str, 164 | contextfile_url: str = CONTEXTFILE_URL, 165 | ): 166 | json_ld = { 167 | "category": "reproschema:Activity", 168 | "id": f"{activity_name}_schema", 169 | "prefLabel": {"en": activity_name}, 170 | "schemaVersion": get_context_version(contextfile_url), 171 | "version": redcap_version, 172 | "ui": { 173 | "order": activity_data[ 174 | "order" 175 | ], # TODO spr czy to jest "clean order" i "clean bl list"? 176 | "addProperties": activity_data["addProperties"], 177 | "shuffle": False, 178 | }, 179 | } 180 | 181 | if activity_data["compute"]: 182 | json_ld["compute"] = activity_data["compute"] 183 | if activity_data.get("preamble"): 184 | json_ld["preamble"] = activity_data["preamble"] 185 | act = Activity(**json_ld) 186 | path = output_path / "activities" / activity_name 187 | path.mkdir(parents=True, exist_ok=True) 188 | write_obj_jsonld( 189 | act, 190 | path / f"{activity_name}_schema", 191 | contextfile_url=contextfile_url, 192 | ) 193 | 194 | items_path = path / "items" 195 | items_path.mkdir(parents=True, exist_ok=True) 196 | 197 | for item in activity_data["items"]: 198 | item_path = items_path / item["id"] 199 | item_path.parent.mkdir(parents=True, exist_ok=True) 200 | write_obj_jsonld( 201 | Item(**item), item_path, contextfile_url=CONTEXTFILE_URL 202 | ) 203 | print(f"{activity_name} Instrument schema created") 204 | 205 | 206 | def create_protocol_schema( 207 | protocol_data: Dict[str, Any], 208 | activities: List[str], 209 | output_path: Path, 210 | contextfile_url: str = CONTEXTFILE_URL, 211 | ): 212 | protocol_name = protocol_data["protocol_name"].strip().replace(" ", "_") 213 | protocol_schema = { 214 | "category": "reproschema:Protocol", 215 | "id": f"{protocol_name}_schema", 216 | "prefLabel": {"en": protocol_data["protocol_display_name"]}, 217 | "description": {"en": protocol_data.get("protocol_description", "")}, 218 | "schemaVersion": get_context_version(contextfile_url), 219 | "version": protocol_data["redcap_version"], 220 | "ui": { 221 | "addProperties": [ 222 | { 223 | "isAbout": f"../activities/{activity}/{activity}_schema", 224 | "variableName": f"{activity}_schema", 225 | "prefLabel": {"en": activity.replace("_", " ").title()}, 226 | "isVis": True, 227 | } 228 | for activity in activities 229 | ], 230 | "order": [ 231 | f"../activities/{activity}/{activity}_schema" 232 | for activity in activities 233 | ], 234 | "shuffle": False, 235 | }, 236 | } 237 | 238 | protocol_dir = output_path / protocol_name 239 | protocol_dir.mkdir(parents=True, exist_ok=True) 240 | write_obj_jsonld( 241 | Protocol(**protocol_schema), 242 | protocol_dir / f"{protocol_name}_schema", 243 | contextfile_url=contextfile_url, 244 | ) 245 | print(f"Protocol schema created in {protocol_dir}") 246 | -------------------------------------------------------------------------------- /reproschema/tests/contexts/generic: -------------------------------------------------------------------------------- 1 | { 2 | "@context": [ "base", 3 | { 4 | "@version": 1.1, 5 | "@language": "en", 6 | "description": { 7 | "@id": "schema:description", 8 | "@container": "@language" 9 | }, 10 | "name": { 11 | "@id": "schema:name", 12 | "@container": "@language" 13 | }, 14 | "value": { 15 | "@id": "reproschema:value" 16 | }, 17 | "image": { 18 | "@id": "schema:image" 19 | }, 20 | "imageUrl": { 21 | "@id": "schema:image", 22 | "@type": "@id" 23 | }, 24 | "audio": { 25 | "@id": "schema:audio" 26 | }, 27 | "video": { 28 | "@id": "schema:video" 29 | }, 30 | "contentUrl": { 31 | "@id": "schema:contentUrl", 32 | "@type": "@id" 33 | }, 34 | "VideoObject": "schema:VideoObject", 35 | "AudioObject": "schema:AudioObject", 36 | "inLanguage": { 37 | "@id": "schema:inLanguage" 38 | }, 39 | "url": { 40 | "@id": "schema:url", 41 | "@type": "@id" 42 | }, 43 | "citation": { 44 | "@id": "schema:citation", 45 | "@container": "@language" 46 | }, 47 | "version": { 48 | "@id": "schema:version" 49 | }, 50 | "schemaVersion": { 51 | "@id": "schema:schemaVersion" 52 | }, 53 | "prefLabel": { 54 | "@id": "skos:prefLabel", 55 | "@container": "@language" 56 | }, 57 | "altLabel": { 58 | "@id": "skos:altLabel", 59 | "@container": "@language" 60 | }, 61 | "preamble": { 62 | "@id": "reproschema:preamble", 63 | "@container": "@language" 64 | }, 65 | "valueType": { 66 | "@id": "reproschema:valueType", 67 | "@type": "@id", 68 | "@container": "@set" 69 | }, 70 | "landingPage": { 71 | "@id": "reproschema:landingPage", 72 | "@type": "@id", 73 | "@container": "@set" 74 | }, 75 | "question": { 76 | "@id": "schema:question", 77 | "@container": "@language" 78 | }, 79 | "choices": { 80 | "@id": "reproschema:choices" 81 | }, 82 | "valueRequired": "schema:valueRequired", 83 | "multipleChoice": { 84 | "@id": "reproschema:multipleChoice", 85 | "@type": "schema:Boolean" 86 | }, 87 | "responseOptions": { 88 | "@id": "reproschema:responseOptions", 89 | "@container": "@set", 90 | "@type": "@id" 91 | }, 92 | "dataType": { 93 | "@id": "schema:DataType", 94 | "@type": "@id" 95 | }, 96 | "minValue": { 97 | "@id": "schema:minValue" 98 | }, 99 | "maxValue": { 100 | "@id": "schema:maxValue" 101 | }, 102 | "unitCode": { 103 | "@id": "schema:unitCode", 104 | "@container": "@set" 105 | }, 106 | "unitOptions": { 107 | "@id": "reproschema:unitOptions", 108 | "@container": "@set" 109 | }, 110 | "ui" : "@nest", 111 | "order": { 112 | "@id": "reproschema:order", 113 | "@container": "@list", 114 | "@type": "@id", 115 | "@nest": "ui" 116 | }, 117 | "addProperties": { 118 | "@id": "reproschema:addProperties", 119 | "@type": "reproschema:AdditionalProperty", 120 | "@container": "@set", 121 | "@nest": "ui" 122 | }, 123 | "overrideProperties": { 124 | "@id": "reproschema:overrideProperties", 125 | "@type": "reproschema:OverrideProperty", 126 | "@container": "@set", 127 | "@nest": "ui" 128 | }, 129 | "message": { 130 | "@id": "reproschema:message", 131 | "@container": "@language" 132 | }, 133 | "shuffle": { 134 | "@id": "reproschema:shuffle", 135 | "@type": "schema:Boolean", 136 | "@nest": "ui" 137 | }, 138 | "inputOptions": { 139 | "@id": "reproschema:inputs", 140 | "@container": "@index", 141 | "@nest": "ui" 142 | }, 143 | "inputType": { 144 | "@id": "reproschema:inputType", 145 | "@type": "xsd:string", 146 | "@nest": "ui" 147 | }, 148 | "readonlyValue": { 149 | "@id": "schema:readonlyValue", 150 | "@nest": "ui" 151 | }, 152 | "compute": { 153 | "@id": "reproschema:compute", 154 | "@container": "@set" 155 | }, 156 | "messages": { 157 | "@id": "reproschema:messages", 158 | "@container": "@set" 159 | }, 160 | "jsExpression": { 161 | "@id": "reproschema:jsExpression" 162 | }, 163 | "isVis": { 164 | "@id": "reproschema:isVis" 165 | }, 166 | "variableName": "reproschema:variableName", 167 | "isAbout": { 168 | "@id": "reproschema:isAbout", 169 | "@type": "@id" 170 | }, 171 | "allow": { 172 | "@id": "reproschema:allow", 173 | "@container": "@set", 174 | "@type": "@id", 175 | "@nest": "ui" 176 | }, 177 | "reproschema:Skipped": {"@id": "reproschema:Skipped"}, 178 | "reproschema:DontKnow": {"@id": "reproschema:DontKnow"}, 179 | "reproschema:TimedOut": {"@id": "reproschema:TimedOut"}, 180 | "reproschema:AutoAdvance": {"@id": "reproschema:AutoAdvance"}, 181 | "reproschema:DisableBack": {"@id": "reproschema:DisableBack"}, 182 | "reproschema:AllowExport": {"@id": "reproschema:AllowExport"}, 183 | "reproschema:AllowReplay": {"@id": "reproschema:AllowReplay"}, 184 | "timer": { 185 | "@id": "reproschema:timer", 186 | "@type": "@id", 187 | "@nest": "ui" 188 | }, 189 | "delay": { 190 | "@id": "reproschema:delay", 191 | "@type": "@id", 192 | "@nest": "ui" 193 | }, 194 | "additionalNotesObj": { 195 | "@id": "reproschema:additionalNotesObj", 196 | "@type": "reproschema:AdditionalNoteObj", 197 | "@container": "@set" 198 | }, 199 | "source": { 200 | "@id": "reproschema:source", 201 | "@type": "xsd:string" 202 | }, 203 | "column": { 204 | "@id": "reproschema:column", 205 | "@type": "xsd:string" 206 | }, 207 | "randomMaxDelay": { 208 | "@id": "reproschema:randomMaxDelay" 209 | }, 210 | "schedule": { 211 | "@id": "reproschema:schedule" 212 | }, 213 | "limit": { 214 | "@id": "reproschema:limit" 215 | }, 216 | "maxRetakes": { 217 | "@id": "reproschema:maxRetakes" 218 | }, 219 | "importedFrom": { 220 | "@id": "pav:importedFrom", 221 | "@type": "@id" 222 | }, 223 | "importedBy": { 224 | "@id": "pav:importedBy", 225 | "@type": "@id" 226 | }, 227 | "createdWith": { 228 | "@id": "pav:createdWith", 229 | "@type": "@id" 230 | }, 231 | "createdBy": { 232 | "@id": "pav:createdBy", 233 | "@type": "@id" 234 | }, 235 | "createdOn": { 236 | "@id": "pav:createdOn", 237 | "@type": "xsd:dateTime" 238 | }, 239 | "previousVersion": { 240 | "@id": "pav:previousVersion", 241 | "@type": "@id" 242 | }, 243 | "lastUpdateOn": { 244 | "@id": "pav:lastUpdateOn", 245 | "@type": "xsd:dateTime" 246 | }, 247 | "derivedFrom": { 248 | "@id": "prov:wasDerivedFrom", 249 | "@type": "@id" 250 | }, 251 | "used": { 252 | "@id": "prov:used", 253 | "@container": "@set", 254 | "@type": "@id" 255 | }, 256 | "wasAttributedTo": { 257 | "@id": "prov:wasAttributedTo", 258 | "@type": "reproschema:Participant", 259 | "@container": "@set" 260 | }, 261 | "wasAssociatedWith": { 262 | "@id": "prov:wasAssociatedWith", 263 | "@type": "reproschema:SoftwareAgent", 264 | "@container": "@set" 265 | }, 266 | "subject_id": { 267 | "@id": "nidm:subject_id" 268 | }, 269 | "startedAtTime": { 270 | "@id": "prov:startedAtTime", 271 | "@type": "xsd:dateTime" 272 | }, 273 | "endedAtTime": { 274 | "@id": "prov:endedAtTime", 275 | "@type": "xsd:dateTime" 276 | } 277 | } 278 | ] 279 | } 280 | --------------------------------------------------------------------------------