Contributing¶
Thank you for your interest in contributing to xnatctl. Whether you are filing a bug report, suggesting a feature, or submitting code, your help is welcome.
xnatctl follows a layered architecture – Click CLI commands delegate to a service layer, which in turn calls the httpx-based HTTP client. Pydantic models define the XNAT resource schemas, and Rich handles terminal output. If you are new to the codebase, the Architecture Overview section below is a good place to start.
Reporting Bugs¶
If you encounter a bug, please open an issue on GitHub Issues with as much detail as possible. A good bug report includes:
xnatctl version – run
xnatctl --versionand paste the output.XNAT server version – run
xnatctl health pingorxnatctl api get /xapi/siteConfig/buildInfo/version.Exact command – the full command you ran, e.g.
xnatctl session list -P MY_PROJECT.Full error output – copy the complete traceback or error message.
OS and Python version – e.g. “Ubuntu 22.04, Python 3.12.1”.
Warning
Before pasting any output, make sure to remove or redact credentials, tokens, and any Protected Health Information (PHI). Never include passwords or session tokens in issue reports.
Tip
Running with --verbose often surfaces additional detail that helps with
diagnosis.
Requesting Features¶
Feature requests are welcome. Open a GitHub issue and describe the use case you are trying to solve. Explaining why you need the feature – not just what – helps maintainers evaluate and prioritize the request.
Development Setup¶
The development environment uses uv for fast
dependency resolution and virtual environment management. After cloning the
repository, a single uv sync installs the package in editable mode along
with all development dependencies (pytest, ruff, mypy, pre-commit, and others).
$ git clone https://github.com/rickyltwong/xnatctl.git
$ cd xnatctl
$ uv sync --dev
You can verify that the installation works by running the CLI:
$ uv run xnatctl --help
Running Tests¶
The test suite uses pytest. To run the full suite with verbose output:
$ uv run pytest tests/ -v
With coverage reporting:
$ uv run pytest tests/ --cov=xnatctl --cov-report=html
The tests are organized by layer:
tests/test_cli_*.py– CLI integration tests that invoke Click commands viaCliRunner. These test argument parsing, output formatting, and error handling without making real HTTP requests.tests/test_service_*.py– Service layer unit tests that mockXNATClientto verify business logic, pagination, and error mapping in isolation.Other files cover the core client, configuration, authentication, validation, and upload/download helpers.
To run a single test file:
$ uv run pytest tests/test_cli_project.py -v
To run a single test function:
$ uv run pytest tests/test_cli_project.py::test_project_list_table -v
Tip
Use -k to run tests matching a keyword expression, e.g.
uv run pytest tests/ -k "upload and not dicom" -v.
Linting, Formatting, and Type Checking¶
All three checks must pass before code is merged.
Linting catches style violations, unused imports, and common bugs:
$ uv run ruff check xnatctl
Formatting enforces consistent code layout (line length 100, PEP 8 conventions):
$ uv run ruff format xnatctl
Type checking verifies that type annotations are consistent and catches type errors at development time:
$ uv run mypy xnatctl
Note
Ruff combines the roles of flake8, isort, and Black in a single tool. You do not need to install those separately.
Pre-commit Hooks¶
The project uses pre-commit hooks to catch issues before they reach CI. Install the hooks once after cloning:
$ uv run pre-commit install
$ uv run pre-commit install --hook-type pre-push
Hooks that run on every commit:
ruff check– catches lint violations (unused imports, style issues, potential bugs).ruff format --check– ensures code is formatted consistently.mypy– verifies type annotations are correct.
Hooks that run on push:
pytest– runs the full test suite to prevent broken code from reaching the remote.
Building Documentation¶
The documentation is built with Sphinx. Install the docs dependencies and build HTML output:
$ uv sync --dev --extra docs
$ cd docs
$ make html
The built docs are in docs/_build/html/. Open index.html in a browser to
preview.
Architecture Overview¶
xnatctl follows a layered design that separates concerns into three tiers:
CLI layer (xnatctl/cli/). Each resource type – projects, subjects,
sessions, scans, resources, prearchive, pipelines – has its own Click command
group. Commands parse arguments, set up context, call into the service layer, and
format output. They do not contain business logic or construct HTTP requests
directly.
Service layer (xnatctl/services/). Services encapsulate the XNAT REST API.
Each service extends BaseService, which provides _get, _post,
_paginate, and _extract_results helpers. Services translate between Pydantic
models and raw API responses. For example, ProjectService.list() calls
_get("/data/projects"), extracts the result set, and returns a list of
Project model instances.
Core layer (xnatctl/core/). The XNATClient wraps httpx with retry
logic (exponential backoff on 502/503/504), automatic re-authentication on 401,
pagination support, and session token management. The config module handles
YAML-based profiles and environment variable overrides. The output module uses Rich
to render tables, JSON, and quiet (ID-only) formats.
The CLI decorator stack composes behavior declaratively. A typical command looks like this:
@project.command("list")
@global_options # --profile, --output, --quiet, --verbose
@require_auth # ensures authenticated client; re-auths on expiry
@handle_errors # catches XNATCtlError -> formatted error + sys.exit(1)
def project_list(ctx: Context) -> None:
service = ProjectService(ctx.client)
projects = service.list()
ctx.output(projects)
Destructive commands add @confirm_destructive (for --yes / --dry-run
flags), and batch commands add @parallel_options (for --parallel /
--workers).
Pydantic models (xnatctl/models/) define the schema for each XNAT resource
type. Models use populate_by_name=True to accept XNAT API field aliases (e.g.,
subject_ID), extra="ignore" to tolerate unknown fields, and expose
table_columns() and to_row() methods for Rich table rendering.
You can also use the service layer programmatically outside the CLI:
from xnatctl.core.client import XNATClient
from xnatctl.services.projects import ProjectService
client = XNATClient(base_url="https://xnat.example.org", ...)
client.authenticate()
service = ProjectService(client)
projects = service.list()
Code Style¶
xnatctl targets Python 3.11+ and uses type hints throughout.
Formatting and linting: Ruff handles both, configured with a line length of 100 and rule sets E, F, W, I, B, and UP.
Docstrings: Google-style with
Args,Returns, andRaisessections. Every public function, class, and method must have a docstring.Type checking: mypy with
check_untyped_defsenabled. AvoidAnyunless absolutely necessary.CLI framework: Click for command definitions, argument parsing, and help text.
Data models: Pydantic v2 for XNAT resource schemas with strict validation.
HTTP client: httpx for synchronous HTTP with connection pooling and timeout control.
Terminal output: Rich for tables, progress bars, and styled error messages.
Note
The project uses ruff in place of Black, isort, and flake8. Configuration
lives in pyproject.toml under [tool.ruff].