Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ LavLab Python Utils is designed to streamline common tasks in our research workf
- **Data Processing:** Functions to handle various data formats used in our research, including DICOM, NIfTI, and more.
- **Visualization:** Tools to create publication-quality plots and visualizations.
- **Integration:** Seamless integration with other tools and services used in our lab.
- **OMERO Support:** Connect to and work with OMERO servers for image data management.
- **XNAT Support:** Connect to and work with XNAT servers for neuroimaging data management.

## Installation

Expand All @@ -27,6 +29,11 @@ You can install LavLab Python Utils via pip:
python3 -m pip install https://github.com/laviolette-lab/lavlab-python-utils/releases/latest/download/lavlab_python_utils-latest-py3-none-any.whl
# optional install targets, must install wheel from github using command above first!
python3 -m pip install 'lavlab-python-utils[all]'

# Or install specific features:
python3 -m pip install 'lavlab-python-utils[omero]' # For OMERO support
python3 -m pip install 'lavlab-python-utils[xnat]' # For XNAT support
python3 -m pip install 'lavlab-python-utils[jupyter]' # For Jupyter tools
```

## Documentation
Expand Down
83 changes: 83 additions & 0 deletions docs/xnat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# XNAT Support

This package now includes support for XNAT servers in addition to OMERO servers.

## Installation

To use XNAT features, install with the xnat extra:

```bash
pip install lavlab-python-utils[xnat]
```

## Configuration

Configure XNAT service in your configuration file (`~/.lavlab.yml` or `/etc/lavlab.yml`):

```yaml
histology:
service:
name: 'xnat'
host: 'https://your-xnat-server.org'
username: your_username # optional, will prompt if not provided
passwd: your_password # optional, recommended to use keyring instead
```

## Usage

### Basic Connection

```python
import lavlab.xnat

# Connect using configuration
session = lavlab.xnat.connect()

# Or create service provider directly
provider = lavlab.xnat.XNATServiceProvider()
session = provider.login()
```

### Helper Functions

```python
from lavlab.xnat.helpers import get_projects, get_subjects, get_experiments

# Get available projects
projects = get_projects(session)

# Get subjects for a project
subjects = get_subjects(session, "PROJECT_ID")

# Get experiments for a subject
experiments = get_experiments(session, "PROJECT_ID", "SUBJECT_ID")

# Download scan files
from lavlab.xnat.helpers import download_scan_file

with download_scan_file(session, "EXPERIMENT_ID", "SCAN_ID", "filename.dcm") as file_path:
# Work with downloaded file
pass
# File is automatically cleaned up
```

### Search and Discovery

```python
from lavlab.xnat.helpers import find_experiments_by_type, search_experiments

# Find MR experiments in a project
mr_experiments = find_experiments_by_type(session, "PROJECT_ID", "xnat:mrSessionData")

# Search experiments with custom criteria
results = search_experiments(session, project="PROJECT_ID", modality="MR")
```

## Service Provider Architecture

The XNAT support follows the same service provider pattern as OMERO:

- `XNATServiceProvider` handles authentication and connection
- Credentials are managed through keyring for security
- The service is registered as an entry point for dynamic loading
- Configuration follows the same patterns as other services
111 changes: 111 additions & 0 deletions examples/xnat_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
#!/usr/bin/env python3
"""
Example script demonstrating XNAT support in lavlab-python-utils.

This script shows how to:
1. Configure XNAT service
2. Connect to XNAT server
3. List projects, subjects, and experiments
4. Download scan files

Requirements:
- lavlab-python-utils[xnat] package installed
- XNAT server configuration in ~/.lavlab.yml
"""

import lavlab
from lavlab.xnat import connect
from lavlab.xnat.helpers import (
get_projects,
get_subjects,
get_experiments,
get_scan_files,
download_scan_file
)


def main():
"""Demonstrate XNAT functionality."""

# Configure for XNAT service - this would typically be in ~/.lavlab.yml
# Example configuration:
config = {
"histology": {
"service": {
"name": "xnat",
"host": "https://your-xnat-server.org",
# username/password will be prompted or loaded from keyring
}
}
}

print("XNAT Support Example")
print("=" * 50)

try:
# Connect to XNAT server
print("Connecting to XNAT server...")
session = connect()
print("✓ Connected successfully!")

# List available projects
print("\nAvailable projects:")
projects = get_projects(session)
for i, project_id in enumerate(projects[:5]): # Show first 5
print(f" {i+1}. {project_id}")

if projects:
# Use first project as example
project_id = projects[0]
print(f"\nExploring project: {project_id}")

# List subjects in the project
subjects = get_subjects(session, project_id)
print(f" Subjects found: {len(subjects)}")

if subjects:
# Use first subject as example
subject_id = subjects[0]
print(f" Exploring subject: {subject_id}")

# List experiments for the subject
experiments = get_experiments(session, project_id, subject_id)
print(f" Experiments found: {len(experiments)}")

if experiments:
# Show experiment details
experiment_id = experiments[0]
print(f" Example experiment: {experiment_id}")

# This would typically be used for actual file downloads:
# with download_scan_file(session, experiment_id, scan_id, filename) as file_path:
# print(f"Downloaded file: {file_path}")
# # Process the file here

print("\n✓ XNAT exploration completed successfully!")

except RuntimeError as e:
print(f"✗ Error: {e}")
print("\nMake sure to configure XNAT service in ~/.lavlab.yml:")
print("""
histology:
service:
name: 'xnat'
host: 'https://your-xnat-server.org'
# username and password will be prompted or loaded from keyring
""")

except Exception as e:
print(f"✗ Unexpected error: {e}")

finally:
# Clean up connection
try:
session.disconnect()
print("✓ Disconnected from XNAT server")
except:
pass


if __name__ == "__main__":
main()
15 changes: 10 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,18 +48,22 @@ omero = [
"zeroc-ice @ https://github.com/glencoesoftware/zeroc-ice-py-linux-x86_64/releases/download/20240202/zeroc_ice-3.6.5-cp312-cp312-manylinux_2_28_x86_64.whl#sha256=96fb9066912c52f2503e9f9207f98d51de79c475d19ebd3157aae7bc522b5826 ; python_version == '3.12' and platform_system == 'Linux'",
"omero-py"
]
xnat = [
"xnat"
]
jupyter = [
"jupyter",
"dash-slicer",
"dash-bootstrap-components"
]
all = [
"lavlab-python-utils[omero, jupyter]"
"lavlab-python-utils[omero, xnat, jupyter]"
]

[project.entry-points."lavlab_python_utils.service_providers"]
OMERO = "lavlab.omero:OmeroServiceProvider"
IDR = "lavlab.omero:IDRServiceProvider"
XNAT = "lavlab.xnat:XNATServiceProvider"

[project.urls]
Documentation = "https://github.com/LavLabInfrastructure/lavlab-python-utils#readme"
Expand Down Expand Up @@ -90,7 +94,7 @@ dependencies = []
build = "hatch build && chmod -R 777 dist/*"

[tool.hatch.envs.test]
features = [ "omero", "jupyter" ]
features = [ "omero", "xnat", "jupyter" ]
dependencies = [
"toml",
"pytest",
Expand All @@ -104,7 +108,7 @@ test = "pytest {args:test}"
cov = "pytest --cov=src --cov-report=xml {args:test}"

[tool.hatch.envs.lint]
features = [ "omero", "jupyter" ]
features = [ "omero", "xnat", "jupyter" ]
dependencies = [
"toml",
"pytest",
Expand All @@ -117,15 +121,15 @@ format = "black src test"
check = "black src test --check"

[tool.hatch.envs.types]
features = [ "omero", "jupyter" ]
features = [ "omero", "xnat", "jupyter" ]
dependencies = [
"mypy>=1.0.0",
]
[tool.hatch.envs.types.scripts]
check = "mypy --install-types --non-interactive {args:src/lavlab test}"

[tool.hatch.envs.docs]
features = [ "omero", "jupyter" ]
features = [ "omero", "xnat", "jupyter" ]
dependencies = [
"mkdocs",
"mkdocstrings-python",
Expand Down Expand Up @@ -164,6 +168,7 @@ exclude_lines = [
[tool.mypy.overrides]
module = [
"omero",
"xnat",
"scipy"
]
ignore_missing_imports = true
82 changes: 82 additions & 0 deletions src/lavlab/xnat/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"""XNAT Utility module"""

import logging

import xnat # type: ignore

import lavlab
from lavlab.login import AbstractServiceProvider

LOGGER = lavlab.LOGGER.getChild("xnat")


class XNATServiceProvider(AbstractServiceProvider): # pylint: disable=R0903
"""
Provides a connection to a defined XNAT server using xnatpy.
"""

SERVICE = "XNAT"

def login(self) -> xnat.XNATSession:
"""
Logins into configured XNAT server.

Returns
-------
xnat.XNATSession
XNAT API session

Raises
------
RuntimeError
Could not login to XNAT server.
"""
details = lavlab.ctx.histology.service.copy()
if details.get("username") is None or details.get("passwd") is None:
username, password = self.cred_provider.get_credentials()
details.update({"username": username, "passwd": password})

# Convert field names to match xnatpy expectations
connection_params = {
"server": details.get("host"),
"username": details.get("username"),
"password": details.get("passwd"),
}

try:
session = xnat.connect(**connection_params)
return session
except Exception as e:
raise RuntimeError(f"Unable to connect to XNAT server: {e}") from e


def connect() -> xnat.XNATSession:
"""
Uses the UtilContext to connect to the configured XNAT server

Returns
-------
xnat.XNATSession
XNAT API session
"""
if not lavlab.ctx.histology.service.get("name").upper() == "XNAT":
raise RuntimeError("Service is not XNAT.")
return lavlab.ctx.histology.service_provider.login()


def set_xnat_logging_level(level: str):
"""
Sets a given python logging._Level in all xnat loggers.

Parameters
----------
level: logging._Level

Returns
-------
None
"""
LOGGER.info("Setting XNAT logging level to %s.", level)
for name in logging.root.manager.loggerDict.keys(): # pylint: disable=E1101
if name.startswith("xnat"):
logging.getLogger(name).setLevel(level)
Loading