Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions .github/ci-local.sh
Comment thread
AlexisJanin marked this conversation as resolved.
Outdated
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#!/usr/bin/env bash
#
# Run what CI runs, before pushing. Mirrors ci.yml next door, including its split into
# independent groups: CI has two jobs so a lint failure cannot hide a test failure, so this
# runs every check and reports them together rather than stopping at the first one.
#
# CI runs the suite on Python 3.11 and 3.13; this covers whichever interpreter is active.

set -uo pipefail
cd "$(dirname "${BASH_SOURCE[0]}")/.." || exit 1

bold=$(tput bold 2>/dev/null || true)
reset=$(tput sgr0 2>/dev/null || true)
failed=""

run() {
local name="$1"
shift
printf '\n%s==> %s%s\n' "$bold" "$name" "$reset"
"$@" || failed="$failed '$name'"
}

# Recorded first, as in CI: ruff is capped to one minor line because select = ["ALL"] opts into
# every rule a new one adds, so a version mismatch is what to check when a green branch reddens.
if ! version=$(ruff --version 2>/dev/null); then
echo "ruff not found — activate the project venv, then: pip install -e .[dev]" >&2
exit 1
fi
echo "$version"
case "$version" in
"ruff 0.16."*) ;;
*) echo "${bold}warning:${reset} CI pins ruff to 0.16.x — '$version' will disagree with it" >&2 ;;
esac

run "Ruff format check" ruff format --check .
run "Ruff lint check" ruff check .
run "Tests" pytest

if [ -z "$failed" ]; then
printf '\n%sAll CI checks passed.%s\n' "$bold" "$reset"
exit 0
fi
printf '\n%sFailed:%s%s\n' "$bold" "$failed" "$reset"
exit 1
46 changes: 46 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,49 @@ jobs:
files: ${{ matrix.artifact_name }}.zip
draft: true
make_latest: false

# The demo dataset, published as a release asset so `clinical-scope --demo` has something to
# fetch. The standalone bundle still carries its own copy (assemble_bundle.py); this exists
# for pip installs, which package no example at all.
# The asset name is pinned by constants.DEMO_ARCHIVE_URL, which every published wheel already
# carries: renaming it here breaks `--demo` for every version already installed.
example:
name: clinical-scope-example.zip
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

# Staged through a filtered copy so the per-run parquet cache and macOS metadata stay out
# of the archive. Same list as _TREE_IGNORE in assemble_bundle.py; keep the two in step.
- name: Build the example archive
run: |
python3 - <<'EOF'
import shutil, tempfile
from pathlib import Path

with tempfile.TemporaryDirectory() as tmp:
staged = Path(tmp) / "example"
shutil.copytree(
"example",
staged,
ignore=shutil.ignore_patterns("clinical_scope_output", ".DS_Store", "__MACOSX"),
)
shutil.make_archive("clinical-scope-example", "zip", root_dir=staged)
EOF

- name: Upload artifact (workflow_dispatch)
if: github.event_name == 'workflow_dispatch'
uses: actions/upload-artifact@v4
with:
name: clinical-scope-example
path: clinical-scope-example.zip
retention-days: 7

- name: Attach to GitHub Release (tag push)
if: github.event_name == 'push'
uses: softprops/action-gh-release@v2
with:
files: clinical-scope-example.zip
draft: true
make_latest: false
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ logs/

# Local testing and example
**/clinical_scope_output/
/clinical-scope-example.zip
LOCAL_SCRIPT/*
*_from_xlsx.json

Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ All notable changes to this project will be documented in this file.
## [Unreleased]

### Added
- **`clinical-scope --demo` downloads the demo dataset.** Installing with `pip` gives you the application but none of the example data — the demo recording ships with the standalone application and with a source checkout, and until now a `pip` user had no way to get it. The command downloads it once, into `~/.clinical_scope/example/`, and prints the data folder and config paths the app asks for. Repeating it costs nothing: an existing demo is left alone unless you pass `--force`.

`clinical-scope --help` now lists what the command line offers, and `clinical-scope --version` prints the installed version. Launching the app is still just `clinical-scope`.

- **A 📖 Docs link in the app.** It sits under the ⚙ Settings button in the top-right corner and opens the user guide in a new tab, always at its newest version. Previously the guide could only be found by going to the project page, which a `pip` install never sends you to.

- **The version badge says when a newer release is out.** The badge in the top-right corner has always named the version you are running. It now also asks PyPI what the newest published version is, and when you are behind it gains a link — `API Version: 1.2.0 | 1.3.0 available ↗` — pointing at that release's notes and downloads. Until now the only way to learn that a new version existed was to be told by someone.

When the check cannot reach PyPI, or cannot make sense of what it gets back, the badge shows `API Version: 1.2.0 | releases ↗` instead: it will not claim you are behind, but the page it points at is where the answer is. An install that is genuinely up to date gets no link at all, so the quiet case stays quiet.
Expand All @@ -20,8 +26,12 @@ All notable changes to this project will be documented in this file.

- **The hover panel style now applies to PSD plots too**, where several spectra share one frequency axis and reading them at one frequency is the point. A spectrogram's hover also follows the *Hover: x-axis time format* setting, instead of always printing the full date and time.

### Fixed
- **The project page on PyPI now links to the documentation.** Its description is the project README, whose links all pointed at files inside the repository — on PyPI those led nowhere, and the demo animation did not appear at all. Every link is now absolute, and the page carries *Documentation*, *Changelog* and *Issues* links in its sidebar.

### Documentation
- The README's PyPI section says how to upgrade an existing install, not only how to make a new one.
- The user guide has a *Trying the Demo Dataset* section, covering both the copy bundled with the standalone application and the `--demo` download.

---

Expand Down
3 changes: 1 addition & 2 deletions CONTRIBUTING.md
Comment thread
AlexisJanin marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,7 @@ Then use the `/new-datasource` skill from within Claude Code — it walks throug
**Branch naming:** `<type>/<short-description>` — e.g. `feat/mindray-ecg`, `fix/eit-timezone`, `docs/contributing`.

**Before opening a PR:**
- All tests pass (`pytest`)
- Linting is clean (`ruff check .` and `ruff format --check .`)
- [`./.github/ci-local.sh`](.github/ci-local.sh) passes — it runs the three checks CI runs (`ruff format --check .`, `ruff check .`, `pytest`) against your active interpreter, and reports all three rather than stopping at the first failure. CI additionally runs the suite on both Python 3.11 and 3.13.
- New datasources include example data and snapshot tests

**PR description should include:**
Expand Down
36 changes: 25 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<a href="https://pypi.org/project/clinical-scope/">
<img src="https://img.shields.io/pypi/pyversions/clinical-scope" alt="Python versions" />
</a>
<a href="LICENSE">
<a href="https://github.com/larib-data/clinical-scope/blob/main/LICENSE">
<img src="https://img.shields.io/badge/license-Apache%202.0-blue" alt="License: Apache 2.0" />
</a>
<a href="https://doi.org/10.5281/zenodo.20830140">
Expand Down Expand Up @@ -78,23 +78,37 @@ pip install -e .
clinical-scope
```

For the full developer setup (tests, linting, adding a datasource), see [CONTRIBUTING.md](CONTRIBUTING.md).
For the full developer setup (tests, linting, adding a datasource), see [CONTRIBUTING.md](https://github.com/larib-data/clinical-scope/blob/main/CONTRIBUTING.md).

## Demo

![ClinicalScope demo](docs/user_guide/images/demo.gif)
![ClinicalScope demo](https://raw.githubusercontent.com/larib-data/clinical-scope/main/docs/user_guide/images/demo.gif)

## Quickstart

1. **Install and run** — see [Installation](#installation) above; your browser opens at `http://127.0.0.1:8050`
2. **Load config** — click **Default visualization (all sources)** to use built-in defaults, or upload a `database_options.json` / `.xlsx` config file
3. **Set data folder** — enter the path to your patient folder (or point to the bundled `demo_database/demo_patient/` to try it immediately; for the demo, set the EIT *day* to `2004-09-15` and the EDF *recording start* to `2004-09-15 10:12:33` so every source lines up)
3. **Set data folder** — enter the path to your patient folder. No data of your own yet? See [Trying the demo](#trying-the-demo) below (for the demo, set the EIT *day* to `2004-09-15` and the EDF *recording start* to `2004-09-15 10:12:33` so every source lines up)
4. **Process** — click **Process visualization**; interactive plots appear in the browser
5. **Annotate** — draw time events, windows, or point annotations, then click **Save**

## Trying the demo

ClinicalScope ships a small demo recording — one patient, every supported data source — so you can see a full visualization before preparing any data of your own.

A `pip install` does not include it, so download it once:

```bash
clinical-scope --demo
```

That prints the folder it landed in, plus the `demo_patient/` path to paste into the app's **Data folder** field. A source checkout already carries the same data under `example/demo_database/`; the standalone application puts it in `demo_database/`, next to the executable.

Run `clinical-scope --help` for the full list of commands.

## Documentation

The **[user guide](docs/user_guide/tutorial.md)** is the primary reference for everything beyond the Quickstart: data folder layout, `database_options` config files, annotation tools, inspection view, CLI scripts, and the Python API.
The **[user guide](https://github.com/larib-data/clinical-scope/blob/main/docs/user_guide/tutorial.md)** is the primary reference for everything beyond the Quickstart: data folder layout, `database_options` config files, annotation tools, inspection view, CLI scripts, and the Python API.

## Supported Data Sources

Expand All @@ -110,7 +124,7 @@ The **[user guide](docs/user_guide/tutorial.md)** is the primary reference for e
| EDF / EDF+ | Amplifiers and polygraphic recorders | `.edf` | Any EDF-exported signal, typically EEG |
| Other (Generic) | Any CSV / Parquet | `.parquet`, `.csv` | Any time-series with a datetime column — one independent entry per file |

Each patient folder should contain one subfolder per data source. The [user guide](docs/user_guide/tutorial.md) → *Patient Data & Supported Data Sources* gives the folder keyword for each source, the naming rules, and the configuration details.
Each patient folder should contain one subfolder per data source. The [user guide](https://github.com/larib-data/clinical-scope/blob/main/docs/user_guide/tutorial.md) → *Patient Data & Supported Data Sources* gives the folder keyword for each source, the naming rules, and the configuration details.

## Standalone Data Processing

Expand All @@ -124,8 +138,8 @@ from clinical_scope import extract_datasource, extract_patient, batch_extract
from clinical_scope.config.parsing import load_database_options_from_path

db_options = load_database_options_from_path(Path("database_options.json"))
# No config of your own yet? The shipped demo works as-is, no UI needed:
# load_database_options_from_path(Path("example/demo_database/database_options.json"))
# No config of your own yet? The demo config works as-is, no UI needed — run
# `clinical-scope --demo`, then point at the database_options.json it reports.

# 1. Single datasource subfolder (auto-detects type from folder name)
df = extract_datasource(
Expand Down Expand Up @@ -182,7 +196,7 @@ Omit `--database-options` to use all available datasources with their defaults.

## Contributing

Contributions are welcome — bug reports, new data sources, and documentation improvements. See [CONTRIBUTING.md](CONTRIBUTING.md).
Contributions are welcome — bug reports, new data sources, and documentation improvements. See [CONTRIBUTING.md](https://github.com/larib-data/clinical-scope/blob/main/CONTRIBUTING.md).

## Citation

Expand All @@ -199,7 +213,7 @@ If you use ClinicalScope in academic work, please cite:
}
```

A [`CITATION.cff`](CITATION.cff) file is also provided for GitHub's *Cite this repository* button.
A [`CITATION.cff`](https://github.com/larib-data/clinical-scope/blob/main/CITATION.cff) file is also provided for GitHub's *Cite this repository* button.

## Disclaimer

Expand All @@ -215,6 +229,6 @@ This software processes physiological signals that may constitute health data

## License

ClinicalScope is licensed under the [Apache License 2.0](LICENSE).
ClinicalScope is licensed under the [Apache License 2.0](https://github.com/larib-data/clinical-scope/blob/main/LICENSE).

Copyright © 2026 Assistance Publique – Hôpitaux de Paris. Developed by Alexis Janin.
8 changes: 5 additions & 3 deletions docs/RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@ Release checklist for `clinical-scope`, starting from a `main` branch you're hap
```
→ run `clinical-scope`, check the example.

3. **Tag and push** — this triggers [`build.yml`](../.github/workflows/build.yml), which drafts a GitHub Release with the standalone executables attached.
3. **Tag and push** — this triggers [`build.yml`](../.github/workflows/build.yml), which drafts a GitHub Release with the standalone executables and `clinical-scope-example.zip` attached.
```bash
git tag vX.Y.Z && git push origin vX.Y.Z
```
→ review the draft Release: executables attached, no build warnings.
→ review the draft Release: executables **and the example archive** attached, no build warnings.
Comment thread
AlexisJanin marked this conversation as resolved.
Outdated

`clinical-scope --demo` downloads that archive from `releases/latest/download/clinical-scope-example.zip`. It has to be on the newest release under exactly that name: publishing without it, or renaming it, breaks the demo command for every version already installed.
Comment thread
AlexisJanin marked this conversation as resolved.
Outdated

4. **Dry-run on TestPyPI** — run **Publish to TestPyPI** manually from the Actions tab, then install from it (project from TestPyPI, dependencies from real PyPI):
```bash
Expand All @@ -35,7 +37,7 @@ Release checklist for `clinical-scope`, starting from a `main` branch you're hap
```bash
pip install clinical-scope==X.Y.Z
```
→ run `clinical-scope`, check the example.
→ run `clinical-scope`, check the example. A pip install ships none, so fetch it first with `clinical-scope --demo` — which also confirms the release asset is reachable.
Comment thread
AlexisJanin marked this conversation as resolved.
Outdated

**Note:** versions can't be reused — TestPyPI and PyPI both reject re-uploading a version that already exists. Bump to a `.devN` (e.g. `X.Y.Z.dev0`) if you need to re-run the TestPyPI dry-run.

Expand Down
25 changes: 24 additions & 1 deletion docs/user_guide/tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,29 @@ The bundle also ships this user guide and a template folder for organizing patie

To **close** ClinicalScope, close the terminal window that opened with it — the application runs inside that window. If the window is hidden, end the `ClinicalScope` process from your system's process manager.

## Trying the Demo Dataset

ClinicalScope comes with a small demo recording — a single patient, with one example of every supported data source — so you can see a complete visualization before preparing any data of your own.

**With the standalone application**, the demo is already there: look for the `demo_database/` folder next to the executable.

**With a `pip` install**, the demo is not part of the package. Download it once from a terminal:

```bash
clinical-scope --demo
```

The command prints the folder it downloaded into, along with the two paths the app asks for. It is safe to repeat: an already-downloaded demo is left alone. Run `clinical-scope --help` to see everything the command line offers.

Then, in the application:

1. Click **Default visualization (all sources)**, or upload the demo's `database_options.json`.
2. Set **Data folder** to the demo's `demo_patient` folder.
3. Set the EIT **day** to `2004-09-15`, and the EDF **recording start** to `2004-09-15 10:12:33` — the demo's sources were recorded at different times, and these line them up on one timeline.
Comment thread
AlexisJanin marked this conversation as resolved.
Outdated
4. Click **Process visualization**.
Comment thread
AlexisJanin marked this conversation as resolved.
Outdated

The demo is synthetic data for learning the interface. It is not a real recording, and nothing in it should be read clinically.

## Application Overview

The interface is organized top-to-bottom in the following order:
Expand All @@ -122,7 +145,7 @@ The interface is organized top-to-bottom in the following order:
status badges, column tables, and a CSV download.
6. **Visualization Area** -- Interactive plots.

A **⚙ Settings** button sits at the top right, above the Database Options row. It opens your personal display and export settings, which apply to every patient you open — see [Settings](#settings).
A **⚙ Settings** button sits at the top right, above the Database Options row. It opens your personal display and export settings, which apply to every patient you open — see [Settings](#settings). Below it, **📖 Docs** opens this guide in a new browser tab, always at its newest version.

![Application main interface](images/AppMainScreen.png){ width=100% }

Expand Down
6 changes: 5 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ dependencies = [
]

[project.scripts]
clinical-scope = "clinical_scope.dash_api.core_api:main"
clinical-scope = "clinical_scope.cli:main"
Comment thread
AlexisJanin marked this conversation as resolved.

[project.optional-dependencies]
# ruff is capped to a minor line: the formatter and new lint rules change in minors, and select = ["ALL"] opts into every rule ruff adds.
Expand All @@ -63,4 +63,8 @@ markers = [
[project.urls]
Homepage = "https://github.com/larib-data/clinical-scope"
Repository = "https://github.com/larib-data/clinical-scope.git"
# PyPI renders these as its sidebar: for a pip install, that page is the only entry point.
Comment thread
AlexisJanin marked this conversation as resolved.
Outdated
Documentation = "https://github.com/larib-data/clinical-scope/blob/main/docs/user_guide/tutorial.md"
Changelog = "https://github.com/larib-data/clinical-scope/blob/main/CHANGELOG.md"
Issues = "https://github.com/larib-data/clinical-scope/issues"
DOI = "https://doi.org/10.5281/zenodo.20830140"
4 changes: 4 additions & 0 deletions ruff.toml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ unfixable = []
"src/clinical_scope/build_info/*" = [
"T201", # build CLI prints progress to the console
]
"src/clinical_scope/cli.py" = [
"T201", # the console script's stdout is its interface
"PLC0415", # imports are deferred so --help and --demo never build the Dash layout
]

[format]
quote-style = "double"
Expand Down
2 changes: 1 addition & 1 deletion src/clinical_scope/build_info/assemble_bundle.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
("example/template_patient_data_structure", "tree"),
("example/demo_database", "tree"),
]
_TREE_IGNORE = shutil.ignore_patterns("clinical_scope_output")
_TREE_IGNORE = shutil.ignore_patterns("clinical_scope_output", ".DS_Store", "__MACOSX")


def copy_assets(bundle_root: Path) -> list[str]:
Expand Down
Loading
Loading