---------------------------------------------------------------------- This is the API documentation for the cellview library. ---------------------------------------------------------------------- ## Python API Loading plate measurements into pandas. cellview_load_data(*plate_ids: int, experiment: str | int | None = None) -> tuple[pandas.core.frame.DataFrame, list[str]] Load data from a plate or a CSV file into the database (dependency injection version). This function loads data from a plate or a CSV file into the database using dependency injection for better testability and thread safety. Args: *plate_ids: The plate IDs to load data from. Ignored if experiment is provided. experiment: Optional experiment identifier. Can be either: - An integer (experiment_id) - A string (experiment_name) If provided, loads all plates from this experiment. Returns: A tuple containing a pandas DataFrame and a list of variable names. Examples: # Load specific plates df, vars = cellview_load_data(123, 456) # Load all plates from experiment by ID df, vars = cellview_load_data(experiment=6) # Load all plates from experiment by name df, vars = cellview_load_data(experiment="palb_washout_recovery") ---------------------------------------------------------------------- This is the CLI documentation for the package. ---------------------------------------------------------------------- ## CLI: cellview ``` Usage: cellview [OPTIONS] COMMAND [ARGS]... CellView: manage and explore single-cell measurement data. Import measurements produced by the OMERO-Screen pipeline into a local DuckDB database, inspect projects, experiments and plates, and launch notebooks for interactive analysis. Options: --db PATH Path to the DuckDB database file. Defaults to ~/.cellview/cellview.duckdb --version Show the version and exit. --help Show this message and exit. Commands: clean Clean up orphaned records in the database. curate Curate tracked lineages through a replayable edit log. delete Delete data from the database. edit Edit project or experiment metadata. experiment Show plates, channels and variables for experiment ID. explore Launch a Jupyter notebook for interactive data exploration. export Export the measurements for plate ID. import Import data from a CSV file, an OMERO plate, or a screen. plate Show summary, conditions and measurements for plate ID. project Show experiments and plates for project ID. projects List all projects with experiment counts. repair-tracks Repair the tracked lineages of plate ID from... review-queue Sample cells per well and write a review queue for the... template Manage analysis notebook templates. ``` ### cellview projects ``` Usage: cellview projects [OPTIONS] List all projects with experiment counts. Options: --help Show this message and exit. ``` ### cellview project ``` Usage: cellview project [OPTIONS] ID Show experiments and plates for project ID. Options: --help Show this message and exit. ``` ### cellview experiment ``` Usage: cellview experiment [OPTIONS] ID Show plates, channels and variables for experiment ID. Options: --help Show this message and exit. ``` ### cellview plate ``` Usage: cellview plate [OPTIONS] ID Show summary, conditions and measurements for plate ID. Options: --help Show this message and exit. ``` ### cellview import ``` Usage: cellview import [OPTIONS] COMMAND [ARGS]... Import data from a CSV file, an OMERO plate, or a screen. Options: --help Show this message and exit. Commands: csv Import measurements from the CSV file at PATH. plate Import one or more plates by ID. screen Import every plate belonging to screen ID. ``` ### cellview import csv ``` Usage: cellview import csv [OPTIONS] PATH Import measurements from the CSV file at PATH. Options: --nucleus-channel CH Name of the nucleus (DNA-segmentation) channel as it appears in the input data, e.g. 'DAPI', 'Hoechst', 'H2B_RFP'. Plate/screen routes default to the plate's channel annotation. CSV route prompts interactively when omitted. Use this flag to override the default or to run non- interactively. --project PROJECT_ID Existing project ID to import into. Skips the interactive project prompt — useful when importing several plates in one go. --experiment EXPERIMENT_ID Existing experiment ID to import into. Skips the interactive experiment prompt. Implies its parent project, so --project is optional; when both are given they must agree. --help Show this message and exit. ``` ### cellview import plate ``` Usage: cellview import plate [OPTIONS] IDS... Import one or more plates by ID. Several plates given at once must belong to the same screen; this is checked before anything is written. Options: --interactive Force interactive project/experiment selection. --nucleus-channel CH Name of the nucleus (DNA-segmentation) channel as it appears in the input data, e.g. 'DAPI', 'Hoechst', 'H2B_RFP'. Plate/screen routes default to the plate's channel annotation. CSV route prompts interactively when omitted. Use this flag to override the default or to run non- interactively. --project PROJECT_ID Existing project ID to import into. Skips the interactive project prompt — useful when importing several plates in one go. --experiment EXPERIMENT_ID Existing experiment ID to import into. Skips the interactive experiment prompt. Implies its parent project, so --project is optional; when both are given they must agree. --help Show this message and exit. ``` ### cellview import screen ``` Usage: cellview import screen [OPTIONS] ID Import every plate belonging to screen ID. Options: --interactive Force interactive project/experiment selection. --nucleus-channel CH Name of the nucleus (DNA-segmentation) channel as it appears in the input data, e.g. 'DAPI', 'Hoechst', 'H2B_RFP'. Plate/screen routes default to the plate's channel annotation. CSV route prompts interactively when omitted. Use this flag to override the default or to run non- interactively. --project PROJECT_ID Existing project ID to import into. Skips the interactive project prompt — useful when importing several plates in one go. --experiment EXPERIMENT_ID Existing experiment ID to import into. Skips the interactive experiment prompt. Implies its parent project, so --project is optional; when both are given they must agree. --help Show this message and exit. ``` ### cellview edit ``` Usage: cellview edit [OPTIONS] COMMAND [ARGS]... Edit project or experiment metadata. Options: --help Show this message and exit. Commands: experiment Edit an experiment's name and description. project Edit a project's name and description. ``` ### cellview edit project ``` Usage: cellview edit project [OPTIONS] ID Edit a project's name and description. Options: --help Show this message and exit. ``` ### cellview edit experiment ``` Usage: cellview edit experiment [OPTIONS] ID Edit an experiment's name and description. Options: --help Show this message and exit. ``` ### cellview export ``` Usage: cellview export [OPTIONS] ID Export the measurements for plate ID. Options: --help Show this message and exit. ``` ### cellview repair-tracks ``` Usage: cellview repair-tracks [OPTIONS] ID Repair the tracked lineages of plate ID from segmentation flicker. Reads the tracker's immutable track_id_raw / parent_track_id_raw and writes the curated track_id / parent_track_id, so re-running gives the same result. Only plates with a mitotic marker (geminin) are repaired. Options: --well TEXT Restrict to this well (repeatable). Default: every well. --marker TEXT Channel whose nuclear signal is degraded at anaphase (PIP- FUCCI: geminin). [default: Geminin] --dry-run Report what would change; write nothing. --events FILE Write every repair decision (rule, frame, position) to this CSV. --help Show this message and exit. ``` ### cellview review-queue ``` Usage: cellview review-queue [OPTIONS] ID Sample cells per well and write a review queue for the Track Review widget. Cells present at --start are followed to --stop on the curated lineage (automatic repair plus --log). A fixed-seed random sample per well is drawn; flagged cells and an --audit fraction of unflagged ones are queued. Options: --well TEXT Well to sample (repeatable). [required] --start INTEGER Frame at which cells are sampled. [default: 72] --stop INTEGER Last frame cells are followed to. [default: 168] --sample INTEGER RANGE Cells per well. [default: 50; x>=1] --audit FLOAT RANGE Fraction of unflagged sampled cells queued as a random check. [default: 0.2; 0<=x<=1] --seed INTEGER [default: 0] --mode [human|agent] human: the agent proposes and the reviewer confirms. agent: the agent edits and decides alone (use a separate --out to compare runs). [default: human] --stationary-warn FLOAT RANGE Warn when this fraction of a well's start cells is stationary yet expresses a reporter. [default: 0.05; 0<=x<=1] --log FILE Edit log to replay before following cells. --marker TEXT [default: Geminin] --out DIRECTORY Directory for queue.json and sample.csv. [required] --help Show this message and exit. ``` ### cellview curate ``` Usage: cellview curate [OPTIONS] COMMAND [ARGS]... Curate tracked lineages through a replayable edit log. Every correction is appended to a JSON-lines log and the curated lineage is rebuilt by replaying it on the automatic repair. Cells are named by an anchor FRAME:LABEL, the raw mask label of the cell in that frame. Options: --help Show this message and exit. Commands: add Validate an edit against the plate and append it to LOG. replay Rebuild curated lineages: automatic repair plus every edit in LOG. show List the entries of LOG, marking reverted ones. undo Revert the last live edit for a well (appends a revert entry). ``` ### cellview curate add ``` Usage: cellview curate add [OPTIONS] LOG {link|unlink|set_parent|clear_parent| swap|absorb|drop|event|set_outcome|exclude|note} Validate an edit against the plate and append it to LOG. Options: --plate INTEGER OMERO plate id. [required] --well TEXT Well the edit applies to. [required] --cell TEXT Anchor FRAME:LABEL of the cell. [required] --frame INTEGER Frame (link, unlink, swap, event). --frames TEXT Frame range FIRST:LAST (absorb, drop). --label INTEGER Raw label the cell continues as (link). --parent TEXT Anchor of the parent (set_parent). --other TEXT Anchor of the other cell (swap). --kind [mitosis|death|slippage] Event kind. --outcome TEXT Outcome (set_outcome). --text TEXT Note text, or exclusion reason. --reason TEXT Why the edit is made. --author [human|agent] [default: human] --confirmed-by [human] Who approved an agent's edit. --marker TEXT Mitotic marker channel for the repair. [default: Geminin] --help Show this message and exit. ``` ### cellview curate undo ``` Usage: cellview curate undo [OPTIONS] LOG Revert the last live edit for a well (appends a revert entry). Options: --plate INTEGER OMERO plate id. [required] --well TEXT Well whose last edit to revert. [required] --marker TEXT Mitotic marker channel for the repair. [default: Geminin] --help Show this message and exit. ``` ### cellview curate replay ``` Usage: cellview curate replay [OPTIONS] LOG Rebuild curated lineages: automatic repair plus every edit in LOG. Options: --plate INTEGER OMERO plate id. [required] --well TEXT Restrict to this well (repeatable). --write Write curated track_id / parent_track_id to CellView. --annotations FILE Write per-track annotations (events, outcome, exclusion, notes) to this JSON file. --marker TEXT Mitotic marker channel for the repair. [default: Geminin] --help Show this message and exit. ``` ### cellview curate show ``` Usage: cellview curate show [OPTIONS] LOG List the entries of LOG, marking reverted ones. Options: --well TEXT Only this well. --help Show this message and exit. ``` ### cellview delete ``` Usage: cellview delete [OPTIONS] COMMAND [ARGS]... Delete data from the database. Options: --help Show this message and exit. Commands: plate Delete one or more plates and all their associated data. ``` ### cellview delete plate ``` Usage: cellview delete plate [OPTIONS] ID... Delete one or more plates and all their associated data. Plates are deleted in the order given, then orphaned records are cleaned up in a single pass. Options: --help Show this message and exit. ``` ### cellview clean ``` Usage: cellview clean [OPTIONS] Clean up orphaned records in the database. Options: --help Show this message and exit. ``` ### cellview explore ``` Usage: cellview explore [OPTIONS] [PLATE_IDS]... Launch a Jupyter notebook for interactive data exploration. PLATE_IDS are plate IDs, or a notebook name such as plates_3602_3603 from which the IDs are extracted. Options: --experiment EXPERIMENT Explore all plates from an experiment (name or ID). --template NAME Template notebook to use. [default: cellcycle] --fresh Regenerate the notebook even if it already exists. --no-napari Skip launching napari. --code Open the notebook folder in VS Code instead of JupyterLab. --json Print a JSON context snapshot (schema, conditions, stats, notebooks) to stdout and exit. Used by the agentic skill. --help Show this message and exit. ``` ### cellview template ``` Usage: cellview template [OPTIONS] COMMAND [ARGS]... Manage analysis notebook templates. With no subcommand this lists the registered templates, matching the behaviour of 'cellview template list'. Options: --help Show this message and exit. Commands: add Register the template file at PATH in the database. list List all registered templates. remove Remove template NAME from the database. show Show details for template NAME. sync Scan the filesystem and register all discovered templates. ``` ### cellview template list ``` Usage: cellview template list [OPTIONS] List all registered templates. Options: --help Show this message and exit. ``` ### cellview template add ``` Usage: cellview template add [OPTIONS] PATH Register the template file at PATH in the database. Options: --name TEXT Override the template name (default: filename stem). --description TEXT Short description shown in listings. --help Show this message and exit. ``` ### cellview template remove ``` Usage: cellview template remove [OPTIONS] NAME Remove template NAME from the database. The template file itself is left on disk. Options: --help Show this message and exit. ``` ### cellview template show ``` Usage: cellview template show [OPTIONS] NAME Show details for template NAME. Options: --help Show this message and exit. ``` ### cellview template sync ``` Usage: cellview template sync [OPTIONS] Scan the filesystem and register all discovered templates. Options: --help Show this message and exit. ``` ---------------------------------------------------------------------- This is the User Guide documentation for the package. ---------------------------------------------------------------------- ## Guide ### Overview CellView is a local DuckDB-backed database that sits between the OMERO-Screen analysis pipeline and your downstream data analysis. It stores single-cell measurements from high-content immunofluorescence screens in a structured, queryable form so you can browse, filter, and export your data without touching the OMERO server again. ## Workflow overview ```text OMERO-Screen (per-plate CSV results) | v [ import ] cellview import csv / plate / screen | v [ explore ] cellview projects / experiment / plate | v [ Python API ] cellview_load_data(plate_id, ...) | v [ plots ] omero-screen-plots / your own notebooks ``` ## Key capabilities * **Import** single-cell measurements from CSV files, individual OMERO plates, or entire OMERO screens into a structured DuckDB database. * **Browse** projects, experiments, plates, and conditions via a rich terminal interface. * **Edit** project and experiment metadata interactively. * **Delete** plate records and all associated measurements safely. * **Export** plate data as pandas or polars DataFrames for analysis. * **Explore** data interactively by launching a pre-populated Jupyter notebook from customisable templates, with an optional Napari viewer. * **Templates** — ship your own analysis notebooks and launch them with a single command. * **Python API** — load data directly into DataFrames from scripts or notebooks with a single function call. ### User guide CellView is a local database for storing, browsing, and exporting the single-cell measurements produced by the OMERO-Screen analysis pipeline. This guide walks through every command you will use day-to-day, with concrete examples. No programming experience is required — just a terminal and data to analyse. Every flag and option is listed in the generated [CLI reference](../reference/cli/index.html). ## Getting started CellView is part of the `omero-screen` workspace. If the workspace is already installed you do not need to install anything separately. To confirm that CellView is available, run: ```bash cellview --help ``` You should see a list of subcommands. If you see a "command not found" error, make sure the virtual environment is activated: ```bash source .venv/bin/activate ``` **Default database location** CellView stores everything in a single DuckDB file. By default that file lives at: ```text ~/.cellview/cellview.duckdb ``` The directory is created automatically the first time you run a command that writes to the database. **Using a custom database path** Every CellView command accepts a `--db` flag so you can point to a different file — useful when you want to keep separate databases for different projects or when working on a shared server: ```bash cellview --db /scratch/myproject/myproject.duckdb projects ``` ## Browsing your data CellView has four read-only commands for inspecting what is in the database. They do not change anything, so you can run them as often as you like. ### `cellview projects` List every project in the database together with the number of experiments it contains. ```bash cellview projects ``` Example output (formatted as a rich table in your terminal): ```text ┌────┬──────────────────────┬────────────────┐ │ ID │ Name │ Experiments │ ├────┼──────────────────────┼────────────────┤ │ 1 │ PALB2 washout │ 3 │ │ 2 │ CDK inhibitor screen │ 1 │ └────┴──────────────────────┴────────────────┘ ``` Use the IDs shown here as arguments to the commands below. ### `cellview project ` Show the experiments and plates that belong to one project. ```bash cellview project 1 ``` This prints each experiment within the project along with the plates it contains, so you can quickly find the plate IDs you need for export or further analysis. ### `cellview experiment ` Show the plates in an experiment together with the measurement channels and any condition variables (for example drug, timepoint, concentration) that were recorded. ```bash cellview experiment 3 ``` This is useful for checking whether an import completed correctly: you can confirm that all the expected plates are listed and that the channel and condition information looks right. ### `cellview plate ` Show a full summary for one plate: every condition and the number of cells measured per condition, the channel layout, and basic measurement statistics. ```bash cellview plate 12345 ``` Use this command after importing a plate to verify the data landed correctly before running any analysis. ## Importing data CellView supports three import routes. All of them walk you through an interactive prompt to assign the plate to a project and experiment. If the OMERO plate already has tags called `Project: ` and `Experiment: `, CellView reads them automatically and skips the interactive step. **Naming the target up front** Every import route accepts `--project` and `--experiment`, which take existing IDs and skip the prompt altogether: ```bash cellview import plate 12345 --experiment 7 cellview import plate 12345 12346 12347 --experiment 7 ``` This is worth doing whenever you already know where the data belongs — and especially when importing several plates, since otherwise you answer the same prompt once per plate. An experiment already implies its project, so `--project` is optional; give it on its own to pick the project but still resolve the experiment from the plate's own metadata. Both IDs are checked before any plate is touched, so a typo fails immediately rather than half-way through a multi-plate import, and a mismatched pair is rejected. ### From a CSV file Use this route when you have already run the OMERO-Screen pipeline and have a CSV file of single-cell measurements on disk. ```bash cellview import csv /data/final_data_cc.csv ``` **Required columns** The CSV must contain at least the following columns. Any additional numeric columns are treated as measurements and stored in the database. * `plate_id` — the OMERO plate ID (integer) * `cell_line` — cell line name (e.g. `RPE`, `HeLa`) * `condition` — treatment condition (e.g. `control`, `drug_10uM`) ::: {.callout-note} Column names are case-sensitive. Check that your CSV uses exactly these names before importing. The pipeline output file is usually called `final_data_cc.csv` and already has the correct column names. ::: **Interactive prompts** After reading the CSV, CellView will ask you to select or create a project and experiment for the plate: ```text Select a project: 1. PALB2 washout 2. CDK inhibitor screen 3. [Create new project] > ``` Follow the prompts to assign the plate. If you make a mistake you can always edit the metadata afterwards (see [Editing metadata](#editing-metadata)) or delete the plate and re-import it (see [Deleting data](#deleting-data)). **Skipping the prompts with OMERO tags** If the plate on the OMERO server has map annotations (key-value pairs) with keys `Project` and `Experiment`, CellView reads them automatically: ```text Project: PALB2 washout Experiment: siRNA timecourse ``` When these tags are present the interactive step is skipped entirely. ### From OMERO plates Use this route to import plates directly from the OMERO server without first exporting a CSV. You will need network access to the OMERO server. ```bash # Import a single plate cellview import plate 12345 # Import several plates that belong to the same screen cellview import plate 12345 12346 12347 ``` ::: {.callout-note} When importing multiple plates they must all come from the same OMERO screen. CellView uses the screen to determine which experiment the plates belong to. ::: **Forcing interactive selection** If you want to override the automatic project/experiment detection (for example to move plates into a different experiment), add the `--interactive` flag: ```bash cellview import plate 12345 --interactive ``` ### From an entire OMERO screen Import every plate in a screen at once. This is the most convenient route when you have just finished a full screen and want to load all the data in one step. ```bash cellview import screen 456 ``` Add `--interactive` to override automatic project/experiment detection: ```bash cellview import screen 456 --interactive ``` ## Editing metadata You can rename projects and experiments or update their descriptions at any time. These changes are safe — they only update text fields and do not touch any measurement data. ### `cellview edit project ` ```bash cellview edit project 1 ``` CellView will show the current name and description and prompt you to enter new values. Press Enter to keep the existing value for any field. ### `cellview edit experiment ` ```bash cellview edit experiment 3 ``` Works the same way as editing a project. ::: {.callout-note} To find the numeric IDs for projects and experiments, run `cellview projects` first, then `cellview project ` to list experiments within it. ::: ## Exporting data ### `cellview export ` Print a summary of a plate's measurements to the terminal. ```bash cellview export 12345 ``` This is useful for a quick sanity-check. For full programmatic access to the data — for example to load it into a pandas DataFrame for plotting — use the Python API instead (see [Python API](api.qmd)). **Condition variables in exports** When you import a plate, CellView stores extra per-condition columns such as drug name, timepoint, and concentration. In the exported DataFrame these appear as individual columns alongside the measurement columns, so you can filter and group by them directly in Python without any extra wrangling. ## Deleting data ### `cellview delete plate [ ...]` ```bash cellview delete plate 12345 cellview delete plate 12345 12346 12347 ``` This removes the plate record and **all** associated data: conditions, condition variables, and every single-cell measurement row. Several plates can be given at once; they are deleted in the order listed. A single clean-up pass runs automatically afterwards to remove any orphaned records. ::: {.callout-warning} Deletion is permanent. There is no undo. Before deleting a plate, make sure you still have the original CSV file or the data is still accessible on the OMERO server, in case you need to re-import it. ::: If you only want to fix the metadata (wrong project or experiment name), use `cellview edit` instead — that is non-destructive. ## Database maintenance ### `cellview clean` Run an orphan-removal pass over the database. Orphaned records are rows that no longer have a valid parent — for example an experiment that contains no plates, or conditions that point to a plate that was deleted. ```bash cellview clean ``` You do not normally need to run this manually because plate deletion triggers a clean-up automatically. Run it if you have edited the database directly (for example with a DuckDB client) or if something looks inconsistent when browsing. ## Environment variables | Variable | Description | |---|---| | `DATABASE_PATH` | Path to the default DuckDB file. When set, this overrides the built-in default of `~/.cellview/cellview.duckdb`. You can still override this per-command with `--db`. | | `TEST_DATABASE` | Set to `true` to use a separate test database at `~/.cellview/cellview-test.duckdb`. Useful when trying out imports without affecting your production data. | | `CELLVIEW_EDITOR` | Controls which editor opens notebooks launched by `cellview explore`. Set to `vscode` to open in VS Code; leave unset to use JupyterLab. See [Exploring data](explore.qmd) for details. | ## Overview `cellview explore` is the quickest way to go from a plate ID to a working analysis notebook. It copies a pre-built template notebook into your personal explore library (`~/.cellview/explore/`), injects the plate IDs you care about, and opens the notebook in JupyterLab or VS Code — ready to run. You do not need to know where the data lives, how to connect to the database, or how to write loading code. CellView handles all of that; you start with data already in a `DataFrame`. Every flag accepted by `cellview explore` and `cellview template` is listed in the generated [CLI reference](../reference/cli/index.html). ## Launching the explorer ### By plate ID If you know which plate (or plates) you want to look at, pass the IDs directly: ```bash cellview explore 12345 cellview explore 12345 12346 12347 ``` When you provide multiple plate IDs, CellView creates a single combined notebook that loads all of them together. The notebook is saved at: ```text ~/.cellview/explore/plates/12345/explore_plate_12345.ipynb ``` (For multiple plates the directory is named after the first ID in the list.) JupyterLab opens automatically at that notebook. ### By experiment If you want to load all plates belonging to an experiment, use `--experiment`. You can identify the experiment by name or by its numeric ID: ```bash cellview explore --experiment palb_washout cellview explore --experiment 6 ``` The notebook is placed in a folder that mirrors the project/experiment hierarchy in the database: ```text ~/.cellview/explore/MyProject/palb_washout/explore_exp_6.ipynb ``` ::: {.callout-note} Use `cellview experiment` to see the names and IDs of all experiments currently in your database. ::: ### Opening in VS Code instead of JupyterLab Pass `--code` to open the notebook in VS Code rather than JupyterLab: ```bash cellview explore 12345 --code ``` VS Code opens with the entire `~/.cellview/explore/` library as the workspace (so you can see all your notebooks in the sidebar) and jumps directly to the new notebook. This works particularly well with AI coding assistants such as Claude Code or GitHub Copilot, because a `CLAUDE.md` file is placed in the explore library automatically — it documents the full plotting API and is read by the assistant the moment it opens the workspace. To make VS Code your default editor without typing `--code` every time, set the environment variable in your shell profile: ```bash export CELLVIEW_EDITOR=vscode ``` ## What happens when you run `explore` Here is what CellView does behind the scenes each time you run the command: 1. **Looks up plates.** If you used `--experiment`, CellView queries the database for all plate IDs that belong to that experiment. If you gave plate IDs directly, it uses those. 2. **Checks for an existing notebook.** If a notebook for this exact set of plates or this experiment already exists in the explore library, CellView reuses it. This means you can close the editor, come back later, and run the same command to reopen exactly where you left off. Use `--fresh` to override this behaviour. 3. **Copies and patches the template.** The chosen template is copied to the correct subfolder in `~/.cellview/explore/` and the `PLATE_IDS` line is replaced with the actual IDs. 4. **Migrates legacy notebooks (if needed).** Older flat-layout notebooks (created before the project/experiment subfolder structure was introduced) are moved into the new layout automatically. 5. **Launches the editor.** JupyterLab or VS Code opens with the notebook ready to run. 6. **Optionally launches Napari.** Unless you passed `--no-napari`, Napari starts in the background so you can view raw microscopy images alongside your analysis. ## The explore library and `CLAUDE.md` `~/.cellview/explore/` is your personal analysis library. Every notebook you create with `cellview explore` is stored here, organised into subfolders by project and experiment (or by plate ID). Nothing is deleted automatically — old notebooks accumulate over time and you can return to any of them at any point. A `CLAUDE.md` file is placed in this directory when it is first created. It contains complete documentation of: - `cellview_load_data` and its arguments - All plot functions from `omero_screen_plots` with their parameters - Common patterns for filtering and grouping data When you open the explore library in VS Code with `--code`, any AI coding assistant that supports project context files (Claude Code, GitHub Copilot, Cursor, etc.) reads this file automatically. The assistant then knows the entire CellView API without you needing to explain it — you can ask questions like "plot the cell cycle distribution for RPE cells only" and get working code immediately. ::: {.callout-note} The `CLAUDE.md` file is updated when you upgrade CellView. If you customise it, your changes may be overwritten on upgrade. Keep personal notes in a separate file in the same directory. ::: ## Notebook structure A freshly created notebook from the default `cellcycle` template contains the following cells, in order: ```text [Markdown] # Explore — Plate 12345 [Code] PLATE_IDS = [12345] [Code] from cellview.api import cellview_load_data [Code] df, variable_names = cellview_load_data(*PLATE_IDS) [Code] ... template-specific analysis cells ... ``` The `PLATE_IDS` cell is the only part of the template that CellView modifies. Everything else comes from the template as-is, so what you see next depends on which template you chose. The default `cellcycle` template adds cells that produce: - A summary table of cell counts per condition - A combined cell cycle plot (stacked bar + phase quantification) - A feature comparison plot for nucleus area You can edit, delete, or add cells freely — the notebook is yours. ## The `--fresh` flag By default, running `cellview explore 12345` a second time simply reopens the existing notebook. This is intentional: it lets you accumulate your own analysis in the notebook without losing it. Use `--fresh` when you want to start again from a clean template: ```bash cellview explore 12345 --fresh ``` This is also useful after a CellView upgrade that ships an improved template — `--fresh` gives you the new version. ::: {.callout-note} `--fresh` overwrites the existing notebook. If you want to keep your previous work, rename or copy the file before running `--fresh`. ::: ## Using a Marimo notebook CellView templates can be Marimo reactive notebooks (`*.py` files) as well as standard Jupyter notebooks. Marimo notebooks re-run cells automatically when their inputs change, which can be useful for interactive exploration. If the selected template is a Marimo file, CellView opens it with: ```bash marimo edit explore_plate_12345.py ``` Everything else works the same way — the `PLATE_IDS` line is patched, the file is placed in the correct subfolder, and the editor is launched for you. ::: {.callout-note} Marimo must be installed separately. Run `uv add marimo` (or `pip install marimo`) if you see a "command not found" error. ::: ## Managing templates ### Listing templates To see all templates that CellView knows about, including their format and a short description: ```bash cellview template list ``` ### Using a different template Pass `--template` with the template name: ```bash cellview explore 12345 --template drug_screen ``` ### Adding your own template You can register a notebook you have already created as a named template: ```bash cellview template add ~/my_analysis.ipynb --name drug_screen \ --description "Dose-response analysis" ``` Alternatively, drop the file directly into `~/.cellview/templates/`. CellView checks this directory in addition to its built-in templates, and user templates take priority when names collide. After dropping a file there, register it with: ```bash cellview template sync ``` ### Template conventions For CellView to inject plate IDs correctly, your template must contain exactly this line somewhere near the top: ```python PLATE_IDS: list[int] = [] ``` CellView replaces the right-hand side with the actual list of plate IDs at launch time. Everything else in the file is left untouched. The first markdown cell in the notebook is used as the short description shown by `cellview template list`. ### Syncing templates to the database If you add template files to `~/.cellview/templates/` manually (without using `cellview template add`), run: ```bash cellview template sync ``` This scans both the built-in package templates and your personal template directory, and registers any new files in the database. ### Removing a template record ```bash cellview template remove drug_screen ``` This removes the database record for the named template. The file itself is **not** deleted — only the registration entry is removed. ### Showing template details ```bash cellview template show drug_screen ``` Displays the full path, format, description, and registration date for a specific template. ## Example: creating your own template The following walkthrough shows how to create a reusable template from scratch for a dose-response experiment. **Step 1 — Create the templates directory** (if it does not already exist): ```bash mkdir -p ~/.cellview/templates ``` **Step 2 — Start from an existing notebook or create a new one.** If you already have a working notebook that you want to reuse, copy it: ```bash cp ~/notebooks/dose_response_analysis.ipynb \ ~/.cellview/templates/dose_response.ipynb ``` Or open JupyterLab and create a new notebook in `~/.cellview/templates/`. **Step 3 — Add the required** `PLATE_IDS` **line** near the top of the notebook, in its own code cell: ```python PLATE_IDS: list[int] = [] ``` Make sure this line appears exactly as shown — CellView uses it as a target for injection. **Step 4 — Add a markdown description** as the first cell of the notebook. This text is shown by `cellview template list`: ```text # Dose-Response Analysis Analyse viability and cell cycle distribution across drug concentrations. ``` **Step 5 — Save the file** as `~/.cellview/templates/dose_response.ipynb`. **Step 6 — Test it:** ```bash cellview explore 12345 --template dose_response ``` CellView will copy the template, inject the plate IDs, and open the notebook. If everything looks correct, register it properly so it appears in the list: ```bash cellview template add ~/.cellview/templates/dose_response.ipynb \ --name dose_response \ --description "Dose-response viability and cell cycle analysis" ``` ### Python API CellView provides a Python API for loading plate data directly into pandas DataFrames from your Jupyter notebooks or analysis scripts. This page covers everything you need to get your data into Python and start analysing it. Full signatures and argument docs are in the generated [API reference](../reference/index.html). ## Loading data The primary function you will use is `cellview_load_data`. Call it with one or more OMERO plate IDs, or pass an experiment name or ID to load all plates from that experiment at once. ```python from cellview.api import cellview_load_data # Load a single plate df, variable_names = cellview_load_data(12345) # Load several plates together (useful for combining biological replicates) df, variable_names = cellview_load_data(12345, 12346, 12347) # Load all plates from an experiment by name df, variable_names = cellview_load_data(experiment="palb_washout") # Load all plates from an experiment by numeric ID df, variable_names = cellview_load_data(experiment=6) ``` ## Auto-import behaviour ::: {.callout-note} If a plate ID you request is not yet in your local CellView database, `cellview_load_data` will attempt to import it from OMERO automatically. This requires network access to your OMERO server and valid credentials configured in your `.env` file. The import runs silently in the background — if it succeeds, you get your data as normal; if it fails (e.g. no network access), an error is raised with a clear message. To import data manually before a session without network access, use the CLI: `cellview import plate `. ::: ## Understanding the return values `cellview_load_data` always returns a two-element tuple: `(df, variable_names)`. **The DataFrame (df)** `df` is a standard [pandas DataFrame](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.html). Each row represents one segmented cell. The following columns are always present: - `plate_id` — the OMERO plate ID (integer) the cell came from - `cell_line` — the name of the cell line (e.g. `"RPE"`, `"HeLa"`) - `well_id` — the well identifier (e.g. `"B03"`) - `experiment` — the experiment name the plate belongs to - `condition` — the treatment condition label for that well If the experiment included an EdU channel (S-phase marker), two additional columns are present: - `cell_cycle` — cell cycle phase assigned to each cell: `G1`, `S`, `G2`, `Polyploid`, or `SubG1` - `cell_cycle_detailed` — same as `cell_cycle` but with `Mitotic` as a subclassification within G2/M (requires H3P channel) All single-cell measurement columns follow a naming pattern of `{measurement}_{channel}_{compartment}`, for example: - `area_nucleus` — nucleus area in pixels - `intensity_mean_DAPI_nucleus` — mean DAPI intensity in the nucleus - `integrated_int_DAPI_norm` — DAPI integrated intensity, normalised to mode (proxy for DNA content) **The variable names list (variable_names)** `variable_names` is a Python list of strings. It contains the names of any flexible experimental metadata variables that were stored when the plate was imported — for example `["Drug", "Concentration", "Timepoint"]`. These variables become regular columns in `df`, so you can use them directly for filtering and grouping. The list is returned separately because it tells you *which* columns are experimental metadata versus measurement columns — useful when you want to loop over conditions programmatically. ```python df, variable_names = cellview_load_data(12345, 12346) print(variable_names) # ['Drug', 'Concentration'] # Filter to control wells ctrl = df[df["Drug"] == "DMSO"] # Group by condition and compute median nucleus area df.groupby("Drug")["area_nucleus"].median() ``` ## Combining with omero-screen-plots The DataFrame returned by `cellview_load_data` is designed to work directly with the `omero-screen-plots` package. Here is a short complete example that loads two replicate plates, inspects the condition variables, then generates a combined cell cycle plot and a nucleus area comparison. ```python from cellview.api import cellview_load_data from omero_screen_plots import combplot_cellcycle, feature_plot # Load two replicate plates from the same experiment df, variable_names = cellview_load_data(12345, 12346) # Inspect what condition variables are available print(variable_names) # e.g. ['Drug', 'Concentration'] print(df["Drug"].unique()) # e.g. ['DMSO', 'Drug_A', 'Drug_B'] # Define which conditions to compare (order controls plot order) conditions = ["DMSO", "Drug_A", "Drug_B"] # Combined cell cycle plot: scatter + stacked bar in one figure fig, axes = combplot_cellcycle( df=df, conditions=conditions, condition_col="Drug", selector_col="cell_line", selector_val="RPE", save=True, file_format="svg", ) # Nucleus area boxplot across the same conditions fig, ax = feature_plot( df=df, feature="area_nucleus", conditions=conditions, condition_col="Drug", ) ``` ::: {.callout-note} If you have an AI coding assistant (such as Claude) available in your notebook environment, it can help you write plotting calls based on the explore guide context. Run `cellview explore` from the terminal to launch a pre-configured notebook environment where AI assistants automatically receive the plotting API documentation. ::: ## Advanced: dependency injection API `cellview_load_data_with_injection` is the underlying implementation called by `cellview_load_data`. It accepts an optional `conn` parameter, which allows you to pass an explicit database connection — useful when writing tests or when you need fine-grained control over which database is used. In normal analysis workflows, prefer `cellview_load_data`. ## Legacy API `cellview_load_data_legacy` is a singleton-based version kept for backward compatibility with older notebooks. Prefer `cellview_load_data` for all new code. ### Data structure CellView organises high-content screening data using a hierarchical structure that maps to OMERO's data model but is optimised for analysis queries. ## Hierarchy Data is organised in five levels: 1. **Project** — The top-level container. A project groups related experiments (e.g. "DNA Damage Response"). 2. **Experiment** — A logical grouping of plates that belong to the same experimental series (e.g. "PALB2 washout recovery"). 3. **Plate (Repeat)** — A physical plate (e.g. a 96-well plate). In the database schema this level is called `repeats` because each plate is typically a biological repeat of the same experiment. 4. **Well (Condition)** — A specific well on a plate. This is where experimental conditions are defined: cell line, antibodies, drug concentrations, and any additional variables. 5. **Measurement** — Individual single-cell data points: intensities, areas, cell-cycle phase assignments, and more. ```text Project └── Experiment └── Plate (Repeat) └── Well (Condition) ├── condition_variables (key-value pairs) └── Measurements (one row per cell) ``` ## Inferring hierarchy from OMERO When importing data from OMERO, CellView can automatically infer the Project and Experiment names from **Tags** attached to the Plate object: - **Project**: A tag with the format `Project: ` sets the project. - **Experiment**: A tag with the format `Experiment: ` sets the experiment. If these tags are not present, you will be prompted to select or create the project and experiment interactively (or you can force this with the `--interactive` flag). ## Database schema CellView uses DuckDB as its storage engine. The schema consists of seven tables: ### `projects` ```sql project_id INTEGER PRIMARY KEY -- auto-increment project_name TEXT UNIQUE description TEXT ``` ### `experiments` ```sql experiment_id INTEGER PRIMARY KEY -- auto-increment project_id INTEGER REFERENCES projects experiment_name TEXT description TEXT ``` ### `repeats` Stores per-plate metadata including channel assignments. ```sql repeat_id INTEGER PRIMARY KEY -- auto-increment experiment_id INTEGER REFERENCES experiments plate_id INTEGER date DATE lab_member TEXT channel_0 TEXT NOT NULL channel_1 TEXT channel_2 TEXT channel_3 TEXT classifier TEXT ``` ### `conditions` One row per well, linked to a plate (repeat). ```sql condition_id INTEGER PRIMARY KEY -- auto-increment repeat_id INTEGER REFERENCES repeats well TEXT well_id TEXT cell_line TEXT antibody TEXT antibody_1 TEXT antibody_2 TEXT antibody_3 TEXT UNIQUE(repeat_id, well) ``` ### `condition_variables` Flexible key-value pairs for additional per-well metadata (e.g. drug concentrations, timepoints, siRNA targets). These are pivoted into separate DataFrame columns when data is exported. ```sql variable_id INTEGER PRIMARY KEY -- auto-increment condition_id INTEGER REFERENCES conditions variable_name TEXT variable_value TEXT ``` ### `measurements` Single-cell data. Each row corresponds to one segmented cell or nucleus. ```sql measurement_id INTEGER PRIMARY KEY -- auto-increment condition_id INTEGER REFERENCES conditions image_id INTEGER timepoint INTEGER cell_cycle TEXT cell_cycle_detailed TEXT label FLOAT area_nucleus FLOAT centroid-0-nuc FLOAT centroid-1-nuc FLOAT intensity_min_DAPI_nucleus FLOAT intensity_mean_DAPI_nucleus FLOAT intensity_max_DAPI_nucleus FLOAT integrated_int_DAPI_norm FLOAT Cyto_ID FLOAT area_cell FLOAT area_cyto FLOAT ... (additional per-channel intensity columns) ``` The measurement columns are created dynamically based on the channels present in the imported data. Columns for each channel are generated per compartment (nucleus, cell, cytoplasm) with `intensity_min`, `intensity_mean`, and `intensity_max` prefixes. ### `templates` Stores the template registry used by `cellview explore` and `cellview template`. This table is added automatically to existing databases the first time CellView connects after upgrading — you do not need to run any migration manually. ```sql template_id INTEGER PRIMARY KEY -- auto-increment name TEXT UNIQUE -- e.g. "cellcycle", "drug_screen" description TEXT format TEXT DEFAULT 'jupyter' -- 'jupyter' or 'marimo' path TEXT -- absolute path to the template file created_at TIMESTAMPTZ updated_at TIMESTAMPTZ parent_template_id INTEGER REFERENCES templates -- optional inheritance ``` ## Data flow **CSV import path**: 1. CSV is loaded and cleaned (column normalisation, duplicate removal). 2. Project and experiment are selected or created. 3. A `repeats` record is created with plate metadata and channel mapping. 4. `conditions` rows are created — one per unique well. 5. Extra metadata columns are pivoted into `condition_variables`. 6. Single-cell rows are bulk-inserted into `measurements`. **OMERO import path**: 1. CSV attachment is downloaded from the OMERO plate object. 2. Metadata is read from OMERO annotations or tags. 3. The remaining steps follow the CSV import path above. **Export path**: 1. `repeats` + `conditions` + `measurements` are joined. 2. `condition_variables` are pivoted back into separate columns. 3. A pandas (or polars) DataFrame is returned along with the list of variable names. ### Developer guide This guide is for anyone who wants to understand, extend, or contribute to the CellView package. It explains the overall architecture, the key design patterns, and how the main subsystems fit together. ## Architecture overview CellView is organised into a small set of focused modules. The diagram below shows the top-level layout and what each file is responsible for. ```text cellview/ ├── cli.py Click command surface — the `cli` group ├── main.py handlers — the work each command delegates to ├── api.py public Python API (cellview_load_data) ├── db/ │ ├── db.py CellViewDB — connection, schema creation, migrations │ ├── display.py Rich table display for all browse commands │ ├── edit.py edit_project / edit_experiment │ ├── clean_up.py orphan removal, plate deletion │ └── templates.py CRUD for the templates registry table ├── importers/ CSV and OMERO import pipeline ├── exporters/ pandas / polars export ├── explore/ notebook launch, template registry, JSON context └── utils/ state, UI, error classes ``` The entry point for the CLI is the `cli` group in `cli.py`. The public Python API (used from notebooks and scripts) lives in `api.py` and calls into the same exporters that the CLI uses. There is deliberately no business logic in `cli.py` — it only defines the commands and their options; all logic lives in `main.py` and the modules below it. ## Database layer (db/) **CellViewDB** `CellViewDB` (`db/db.py`) wraps a DuckDB connection. It is opened once per CLI invocation and passed by reference to every handler function. This means there is a single connection per process — no connection pooling is needed because DuckDB is an embedded database. On first run, `CellViewDB` creates the full schema. On subsequent runs it calls `ensure_templates_table()` to apply any schema migrations idempotently. This approach means existing databases are never broken by upgrades that add new tables. **Database hierarchy** Data is stored in a five-level hierarchy: ```text projects └── experiments └── repeats (one per imported plate) └── conditions (one per well) ├── condition_variables (key-value metadata rows) └── measurements (one row per segmented cell) ``` `condition_variables` stores flexible experimental metadata (e.g. `Drug = "DMSO"`, `Concentration = "10uM"`) as key-value rows rather than fixed columns. On export, `PlateParser` pivots these back into regular DataFrame columns and returns their names as `variable_names`. **Other db/ modules** - `display.py` — `Rich`-formatted table output for the browse commands. Each display function takes a `CellViewDB` and returns nothing (it prints directly). - `edit.py` — interactive project and experiment renaming via `Rich` prompts. - `clean_up.py` — orphan record removal (conditions with no measurements, repeats with no conditions, etc.) and full plate deletion. - `templates.py` — CRUD operations for the `templates` table used by the explore system. ## Dependency injection pattern CellView uses dependency injection throughout. `CellViewDB` is created once per invocation and passed down to every handler function rather than being accessed via a global singleton. This keeps each function testable in isolation — a test can pass a real in-memory DuckDB connection and the function under test behaves identically to production. ```python # cli.py — simplified @cli.command() @click.argument("plate_id", metavar="ID", type=int) @click.pass_obj def plate(obj: Context, plate_id: int) -> None: """Show a full summary for one plate.""" from cellview.db.display import display_plate display_plate(obj.conn, plate_id) ``` The `api.py` function `cellview_load_data_with_injection` follows the same pattern — it accepts an optional `conn` argument so tests can pass a pre-configured in-memory database without touching the filesystem. ## Import pipeline (importers/) When a user runs `cellview import csv ` or `cellview import plate `, the following sequence runs: 1. **ProjectManager.select_or_create_project()** — presents a Rich prompt listing existing projects; the user either picks one or creates a new one. 2. **ExperimentManager.select_or_create_experiment()** — same pattern within the chosen project. 3. **RepeatsManager** — creates the plate record (a *repeat* in database terms) and stores channel metadata. 4. **import_conditions()** — iterates over wells in the CSV or OMERO plate, creates one `conditions` row per well, and extracts flexible `condition_variables` key-value pairs. 5. **import_measurements()** — bulk-inserts single-cell measurement rows into the `measurements` table. The CSV importer expects specific columns (`plate_id`, `cell_line`, `condition`, and all measurement columns). The OMERO importer downloads the final-data CSV attached to the OMERO plate object and then feeds it through the same CSV path. ## Export pipeline (exporters/) `PlateParser` (`exporters/plate_queries.py`) is the shared query engine used by both the pandas exporter (`db_to_pandas.py`) and the polars exporter (`db_to_polars.py`). It runs a single join across: ```text repeats → conditions → condition_variables → measurements ``` It pivots `condition_variables` key-value rows back into columns using DuckDB's `PIVOT` statement, then returns a tuple of `(DataFrame, variable_names)`. The pandas and polars exporters are thin wrappers that call `PlateParser` and return the result in the requested format. ## Explore system (explore/) The explore system launches pre-configured Jupyter notebooks for interactive data analysis. It consists of four modules: - `_registry.py` — computes the notebook output directory (`EXPLORE_DIR`) and subdirectory logic based on the plate or experiment being explored. - `_template_registry.py` — discovers `.ipynb` and `.py` templates, syncs them to the `templates` database table, and injects `PLATE_IDS` into notebooks before launching. - `_cli.py` — the `launch_explore()` orchestrator: resolves the template, injects plate IDs, opens the notebook in JupyterLab (or another editor via `_open_editor()`), and calls `_ensure_claude_assets()` to copy `CLAUDE.md` and the bundled `.claude/` skills package on first run. - `_explore_json.py` — `explore_json()` produces a JSON snapshot of the current database state for use by agentic workflows (e.g. AI assistants that need to know which plates and experiments exist). **Templates** Built-in templates live in `explore/templates/` and are packaged with CellView. User-defined templates can be placed in `~/.cellview/templates/`. On first run of `cellview explore`, a `CLAUDE.md` file is copied from `explore/templates/` into `EXPLORE_DIR`. This file gives AI coding assistants the plotting API context automatically, so they can write correct `omero-screen-plots` calls without needing to search the documentation. ## Templates table (db/templates.py) The `templates` table was added after the initial schema, so it is created by `ensure_templates_table()` in `CellViewDB` rather than in the main schema creation block. This function is safe to call on every connection — it uses `CREATE TABLE IF NOT EXISTS` and is therefore idempotent. The public interface consists of four functions: - `upsert_template(conn, record)` — insert or update a template record. - `get_template_record(conn, name)` — retrieve a single `TemplateRecord` by name. - `list_template_records(conn)` — return all registered templates. - `delete_template(conn, name)` — remove a template by name. `TemplateRecord` is a plain Python dataclass with fields `name`, `path`, `description`, and `last_synced`. ## Error classes (utils/error_classes.py) CellView defines a small exception hierarchy so callers can catch errors at the right level of specificity: ```text CellViewError (base — catch this to handle any CellView error) ├── DBError (database connection or query failure) ├── DataError (invalid or missing data in an import or export) └── StateError (invalid application state, e.g. missing config) ``` Always raise one of these rather than a bare `Exception` or `ValueError`. This makes it straightforward for callers (including tests) to assert on the specific error type. ## Testing Unit tests live in `tests/unit_tests/cellview_tests/`. Each subdirectory must contain an `__init__.py` to avoid pytest module name collisions when multiple test files share the same filename. Run the tests with: ```bash pytest tests/unit_tests/cellview_tests/ -v ``` **Key testing patterns** *In-memory DuckDB*: Pass `duckdb.connect()` (no path argument) to get a fresh in-memory database. This is fast and leaves no files on disk. *FK constraint fixture*: DuckDB enforces foreign key constraints strictly. When testing orphan-removal functions you need to insert rows that would normally violate FK constraints. Use a `db_no_fk` fixture that creates the schema without `REFERENCES` clauses and opens the connection via raw `duckdb.connect()`. *Mocking DuckDB connections*: DuckDB connection objects are C extension objects and are read-only — `patch.object(conn, "execute", ...)` will raise an `AttributeError`. The workaround is a thin `ConnWrapper` class that delegates all attribute access to the real connection via `__getattr__` but overrides `execute()` so you can intercept calls in tests. ```python class ConnWrapper: def __init__(self, real_conn): self._conn = real_conn self.execute_calls = [] def __getattr__(self, name): return getattr(self._conn, name) def execute(self, sql, params=None): self.execute_calls.append((sql, params)) return self._conn.execute(sql, params) ``` ## Adding a new CLI subcommand Follow these three steps to add a new subcommand (e.g. `cellview mycommand`): 1. **Define the command** in `cli.py` by decorating a thin callback with `@cli.command()`. The docstring becomes the `--help` text and the entry in the generated CLI reference, so write it for users. ```python @cli.command() @click.option("--option", type=str, help="An option.") @click.pass_obj def mycommand(obj: Context, option: str | None) -> None: """Brief description of what mycommand does.""" from cellview.main import handle_mycommand handle_mycommand(obj.conn, option) ``` 2. **Write a handler function** in `main.py`. The handler receives plain values and a connection, and contains all the business logic. ```python def handle_mycommand(conn: CellViewDB, option: str | None) -> None: result = some_db_query(conn, option) print_result(result) ``` 3. **Write tests** in `tests/unit_tests/cellview_tests/test_main.py`. Drive the command with Click's `CliRunner` (no mocking needed for the parsing layer) and test the handler with a mocked or in-memory `CellViewDB`. ::: {.callout-note} Keep `cli.py` free of business logic. Its only job is to declare the command surface and delegate. Handler imports are deliberately lazy so that `--help` and documentation discovery do not pull in DuckDB, pandas or the OMERO stack. All logic belongs in `main.py` or the module the command delegates to (`db/`, `importers/`, etc.). This separation makes the command surface trivially testable without any mocking. :::