diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2a27233..6a6788e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -36,6 +36,10 @@ jobs: run: | pip install -e .[test] + - name: Minimal test of command-line + run: | + bids2openminds --help + - name: Test installed package run: | pytest -v --cov=bids2openminds --cov-report=lcov @@ -46,3 +50,23 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} path-to-lcov: coverage.lcov + + docs: + runs-on: ubuntu-latest + steps: + + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Set up Python 3.11 + uses: actions/setup-python@v5 + with: + python-version: 3.11 + + - name: Install Sphinx dependencies + run: | + pip install sphinx sphinx-rtd-theme + + - name: Build documentation + run: | + sphinx-build -W docs/source docs/_build/html diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..3fe80c2 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,46 @@ +# Code of Conduct + +Contributing to bids2openminds should be a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community +- Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +- Use of sexualized language or imagery +- Unwelcome sexual attention or advances +- Trolling, insulting/derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information (e.g. email address) without explicit permission +- Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or its community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the openMINDS team at `support@openmetadatainitiative.org`. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The openMINDS team is obligated to maintain confidentiality with regard to the reporter of an incident. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), [version 1.4](http://contributor-covenant.org/version/1/4/). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..1dfeba6 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,19 @@ +# Contributing to bids2openminds + +Full contribution guidelines are in the [documentation](https://bids2openminds.readthedocs.io/en/latest/contributing.html). + +## Quick start + +```bash +git clone --recurse-submodules https://github.com/openMetadataInitiative/bids2openminds.git +cd bids2openminds +pip install -e ".[test]" +pytest +``` + +## Key points + +- Feature branches off `main`; PR required to merge; CI must pass +- Google-style docstrings for all public functions +- Deprecations get a `DeprecationWarning` for one release before removal +- Contributors are expected to abide by the [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) diff --git a/bids2openminds/utility.py b/bids2openminds/utility.py index 756a2c3..18e563f 100644 --- a/bids2openminds/utility.py +++ b/bids2openminds/utility.py @@ -65,6 +65,16 @@ def table_filter(dataframe: pd.DataFrame, filter_str: str, column: str = "suffix def pd_table_value(data_frame, column_name, not_list: bool = True): + """Extract a value from a single-row DataFrame column. + + Parameters: + - data_frame (pd.DataFrame): A DataFrame expected to contain a single row. + - column_name (str): The column to read. + - not_list (bool, optional): If True (default), return the scalar value; if False, return a list. + + Returns: + - The column value as a scalar (not_list=True) or list (not_list=False), or None if the column is absent. + """ try: if column_name in data_frame.columns: value = data_frame[column_name].to_list() @@ -111,6 +121,15 @@ def file_hash(file_path: str, algorithm: str = "MD5"): def file_storage_size(file_path: str): + """Return the storage size of a file as an openMINDS QuantitativeValue and a raw byte count. + + Parameters: + - file_path (str): The path to the file. + + Returns: + - tuple: A (QuantitativeValue, int) pair where the QuantitativeValue expresses the size in bytes + as an openMINDS object and the int is the raw byte count. + """ file_stats = os.stat(file_path) file_size = QuantitativeValue( value=file_stats.st_size, unit=UnitOfMeasurement.by_name("byte")) @@ -118,7 +137,17 @@ def file_storage_size(file_path: str): def detect_nifti_version(file_name, extension, file_size): + """Detect the NIfTI format version of a file by inspecting its header bytes. + Parameters: + - file_name (str): Path to the NIfTI file. + - extension (str): File extension, either ``".nii"`` or ``".nii.gz"``. + - file_size (int): Size of the file in bytes (unused; reserved for future use). + + Returns: + - ContentType or None: The openMINDS ContentType for NIfTI-1 or NIfTI-2, or None if the + header cannot be read or the format is unrecognised. + """ nii1_sizeof_hdr = 348 nii2_sizeof_hdr = 540 diff --git a/docs/source/architecture.rst b/docs/source/architecture.rst new file mode 100644 index 0000000..c47bb6c --- /dev/null +++ b/docs/source/architecture.rst @@ -0,0 +1,99 @@ +Architecture +============ + +bids2openminds converts `BIDS `_ neuroimaging datasets into +`openMINDS `_ metadata collections +serialised as JSON-LD. + +Conversion pipeline +------------------- + +The top-level entry point is :func:`bids2openminds.converter.convert`. +It orchestrates the following steps: + +1. Validate that the input path is a directory and load a BIDS layout via + `ancpBIDS `_. +2. Flatten the layout into a Pandas ``DataFrame`` (one row per file) via the + ``layout_to_df()`` shim in ``converter.py``. +3. Call functions in ``main.py`` to create typed openMINDS objects: + + * Subjects and their biological states + * Persons (authors / contributors) + * Behavioural protocols (tasks) + * File bundles and individual files + * A ``DatasetVersion`` and its parent ``Dataset`` + +4. Validate the resulting ``openminds.Collection`` against the openMINDS schema. +5. Optionally serialise to JSON-LD (single file or one file per object). +6. Print a human-readable conversion report. + +Module descriptions +------------------- + +``converter.py`` +~~~~~~~~~~~~~~~~ + +CLI entry point (Click command) and the ``convert()`` function that orchestrates +the pipeline. Also contains ``layout_to_df()``, a shim that translates the +ancpBIDS object model into a flat Pandas DataFrame so the rest of the code can +query files without depending directly on the ancpBIDS API. + +``main.py`` +~~~~~~~~~~~ + +All object-creation functions that map BIDS concepts to openMINDS schema objects: +``create_subjects()``, ``create_persons()``, ``create_behavioral_protocol()``, +``create_file()``, ``create_dataset_version()``, and ``create_dataset()``. + +``mapping.py`` +~~~~~~~~~~~~~~ + +Controlled-vocabulary dictionaries that translate BIDS terms (file suffixes, +acquisition labels, species identifiers, sex codes, handedness codes) into +openMINDS ``ControlledTerm`` instances. Entries that do not yet have a +corresponding openMINDS instance are marked ``None`` with a ``# TODO`` comment. + +``utility.py`` +~~~~~~~~~~~~~~ + +Helper functions: JSON reading, Pandas DataFrame filtering and value extraction, +file hashing (returns an openMINDS ``Hash`` object), file size (returns an +openMINDS ``QuantitativeValue`` object), and NIfTI version detection. + +``report.py`` +~~~~~~~~~~~~~ + +Generates a human-readable conversion summary printed after a successful run, +showing counts of subjects, files, persons, and protocols. + +openMINDS object model +---------------------- + +All metadata objects are instances from the ``openminds.v4`` package: + +* ``openminds.v4.core`` — ``Dataset``, ``DatasetVersion``, ``SubjectGroup``, + ``Subject``, ``SubjectState``, ``Person``, ``File``, ``FileBundle``, etc. +* ``openminds.v4.controlled_terms`` — ``Species``, ``BiologicalSex``, + ``Handedness``, ``Technique``, ``ExperimentalApproach``, ``UnitOfMeasurement``, etc. + +Objects are added to an ``openminds.Collection``, which enforces the schema and +handles JSON-LD serialisation. + +Test structure +-------------- + +``test/test_bids_examples.py`` + End-to-end conversion of datasets from the `bids-examples + `_ corpus (checked out as a + git submodule at ``bids-examples/``). Each test asserts on the counts of + openMINDS objects produced. + +``test/test_thorough_bids_example.py`` + Deep validation on a more complex BIDS dataset. + +``test_dataset_version.py``, ``test_example_datasets_click.py``, ``test/test_person.py``, ``test/test_task.py``, ``test/test_subject_age.py``, ``test/test_file_bundle.py``, ``test/test_mapping_completeness.py`` + Unit tests for individual mapping and utility functions. + +The ``bids-examples`` submodule must be initialised before running the full test suite:: + + git submodule update --init diff --git a/docs/source/contributing.rst b/docs/source/contributing.rst new file mode 100644 index 0000000..7e298eb --- /dev/null +++ b/docs/source/contributing.rst @@ -0,0 +1,95 @@ +Contributing +============ + +This document describes how to set up a development environment, our workflow conventions, and the standards we follow. + +Development setup +----------------- + +Clone the repository **with its submodules** (the BIDS example test corpus +lives in a submodule):: + + git clone --recurse-submodules https://github.com/openMetadataInitiative/bids2openminds.git + cd bids2openminds + pip install -e ".[test]" + +Run the full test suite:: + + pytest + +If you cloned without ``--recurse-submodules``, initialise the submodule afterwards:: + + git submodule update --init + +Branching model +--------------- + +- All development happens on feature branches created from ``main``. +- Do not push commits directly to ``main``. +- Open a pull request to merge your branch into ``main``. +- At least one maintainer approval is required. +- CI must pass (tests on Linux/Windows/macOS, codespell, docs build) before merging. + +Review process +-------------- + +- Keep pull requests focused; one logical change per PR makes review easier. +- Add an entry to ``CHANGES.rst`` in the *Unreleased* section (or under the next version) + describing the change. +- Respond to review comments; squash fixup commits before the PR is merged if requested. + +Versioning +---------- + +We follow `Semantic Versioning `_ (``MAJOR.MINOR.PATCH``). +While the project is at ``0.x``, minor version bumps may introduce breaking API changes. +Once we reach ``1.0``, breaking changes will only occur in major releases. + +Releasing a new version +----------------------- + +See :doc:`maintenance` for the full release procedure. + +Docstring standard +------------------ + +Use `Google-style docstrings +`_ +for all public functions and classes. A minimal example: + +.. code-block:: python + + def my_function(param: str) -> dict: + """One-line summary ending with a period. + + Parameters: + param (str): Description of the parameter. + + Returns: + dict: Description of the return value. + + Raises: + ValueError: If param is empty. + """ + +Communication +------------- + +- **Bugs and feature requests**: `GitHub Issues `_ +- **Questions and general discussion**: `GitHub Discussions `_ + +Deprecation policy +------------------ + +When a feature is to be removed: + +1. Add a ``DeprecationWarning`` in the current release pointing to the replacement. +2. Document the deprecation in ``CHANGES.rst``. +3. Remove the feature in the next minor (or major) release. + +Code of Conduct +--------------- + +This project follows the `openMINDS Code of Conduct +`_. +All participants are expected to uphold its standards. diff --git a/docs/source/index.rst b/docs/source/index.rst index 9384a15..ea13a37 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -17,4 +17,7 @@ We are currently supporting Linux, Windows and Mac OS. .. toctree:: installation usage + architecture + contributing + maintenance changelog diff --git a/docs/source/installation.rst b/docs/source/installation.rst index 6692f16..d8f7538 100644 --- a/docs/source/installation.rst +++ b/docs/source/installation.rst @@ -4,9 +4,11 @@ Installation Dependencies ############ -- `pybids` : Python tools for querying and manipulating BIDS datasets. -- `openminds` : Python package for the openMINDS metadata models. The package contains all openMINDS schemas as Python classes in addition to schema base classes and utility methods. +- `ancpbids` : Python tools for querying BIDS datasets. +- `openminds` : Python package for the openMINDS metadata models. The package contains all openMINDS schemas as Python classes in addition to schema base classes, utility methods, and the library of openMINDS terminologies. +- `nameparser` : A simple Python module for parsing human names into their individual components. - `pandas` : Flexible and powerful data analysis / manipulation library for Python, providing labeled data structures similar to R data.frame objects, statistical functions, and much more +- `PyYAML` : PyYAML is a YAML parser and emitter for Python. - `click` : Used to create a command-line interface for this function. - `nameparser` : A simple Python module for parsing human names into their individual components. diff --git a/docs/source/maintenance.rst b/docs/source/maintenance.rst new file mode 100644 index 0000000..250ae85 --- /dev/null +++ b/docs/source/maintenance.rst @@ -0,0 +1,59 @@ +Maintenance +=========== + +This page is intended for project maintainers. + +Cutting a release +----------------- + +1. Decide on the new version number following `SemVer `_ + (``MAJOR.MINOR.PATCH``). +2. Update ``version`` in ``pyproject.toml``. +3. Add a dated section to ``CHANGES.rst``:: + + 0.3.0 (YYYY-MM-DD) + ------------------ + + * Description of changes. + +4. Commit the changes:: + + git commit -m "Release 0.3.0" + +5. Tag the commit:: + + git tag v0.3.0 + +6. Push the commit and the tag:: + + git push + git push --tags + +7. Build and upload to PyPI:: + + pip install --upgrade build twine + python -m build + twine upload dist/* + +Building the documentation locally +----------------------------------- + +From the repository root:: + + pip install sphinx sphinx-rtd-theme + sphinx-build -W docs/source docs/_build/html + +Then open ``docs/_build/html/index.html`` in a browser. + +The documentation is also built automatically on every push to ``main`` by +`Read the Docs `_. + +Updating dependencies +--------------------- + +After upgrading a dependency (e.g. testing against a new ``ancpbids`` release): + +1. Run the full test suite: ``pytest``. +2. If the new version introduces breaking changes, update the code accordingly + and bump the lower-bound constraint in ``pyproject.toml``. +3. Update the ``CHANGES.rst`` entry for the release. diff --git a/docs/source/usage.rst b/docs/source/usage.rst index c4b98a8..0f05068 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -34,11 +34,18 @@ Example Usage >>> input_path = "/path/to/BIDS/dataset" >>> collection = converter.convert(input_path, save_output=True, output_path="/path/to/output", multiple_files=False, include_empty_properties=False, quiet=False) -Or one can chose the default parmetrs as following: +Or one can choose the default parameters as follows: >>> import bids2openminds.converter as converter >>> collection = converter.convert("/path/to/BIDS/dataset") +.. note:: + + BIDS datasets may contain quasi-identifying information such as participant + demographics in ``participants.tsv``. bids2openminds propagates these fields + into the openMINDS output without modification. Users are responsible for + ensuring that the output is appropriately anonymised before uploading to a + public repository or sharing outside their institution. Command-Line Interface (CLI) ============================ diff --git a/pyproject.toml b/pyproject.toml index 9f83f85..de6345f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,10 +2,10 @@ name = "bids2openminds" version = "0.2.0" dependencies = [ - "ancpbids", + "ancpbids>=0.3", "openminds >= 0.2.3", "click>=8.1", - "pandas", + "pandas>=1.3", "nameparser >= 1.1.3", "PyYAML >= 6.0" ]