plexora 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- plexora/__init__.py +129 -0
- plexora/__main__.py +15 -0
- plexora/_url.py +105 -0
- plexora/api/__init__.py +55 -0
- plexora/api/dataset.py +398 -0
- plexora/api/http.py +19 -0
- plexora/api/plugin.py +541 -0
- plexora/api/store.py +221 -0
- plexora/cli.py +777 -0
- plexora/client/dist/354_bundle.js +2 -0
- plexora/client/dist/354_bundle.js.LICENSE.txt +20 -0
- plexora/client/dist/418_bundle.js +2 -0
- plexora/client/dist/418_bundle.js.LICENSE.txt +1 -0
- plexora/client/dist/693b935cb1f907814be9c6199a68c217.svg +19 -0
- plexora/client/dist/713c134ff47cd328a5b711526f151082.svg +18 -0
- plexora/client/dist/770_bundle.js +1 -0
- plexora/client/dist/ff743f408972e0e96ca9153ed5ae4c83.svg +23 -0
- plexora/client/dist/vendor_bundle.js +2 -0
- plexora/client/dist/vendor_bundle.js.LICENSE.txt +308 -0
- plexora/client/external/openseadragon-bin-2.4.0/LICENSE.txt +28 -0
- plexora/client/external/openseadragon-bin-2.4.0/canvas-overlay-hd.js +140 -0
- plexora/client/external/openseadragon-bin-2.4.0/changelog.txt +501 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/.gitattributes +17 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/LICENSE.txt +116 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/Toolbar.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/fullpage.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/home.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/next.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/previous.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/rotateleft.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/rotateright.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/zoom.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/README.md +9 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-scalebar.js +562 -0
- plexora/client/src/css/import.css +474 -0
- plexora/client/src/css/main.css +903 -0
- plexora/client/src/css/openProject.css +363 -0
- plexora/client/src/css/quickView.css +166 -0
- plexora/client/src/css/tokens.css +51 -0
- plexora/client/src/css/viewer.css +1655 -0
- plexora/client/src/img/apple-touch-icon.png +0 -0
- plexora/client/src/img/favicon.ico +0 -0
- plexora/client/src/img/logo.ai +8892 -33
- plexora/client/src/img/logo.svg +1 -0
- plexora/client/src/img/logo_with_text.ai +9011 -34
- plexora/client/src/img/logo_with_text.svg +1 -0
- plexora/client/src/js/main.js +704 -0
- plexora/client/src/js/pluginRegistry.js +108 -0
- plexora/client/src/js/services/appStatus.js +286 -0
- plexora/client/src/js/services/browsePicker.js +73 -0
- plexora/client/src/js/services/dataLayer.js +400 -0
- plexora/client/src/js/services/datasetContext.js +143 -0
- plexora/client/src/js/services/glRenderer.js +143 -0
- plexora/client/src/js/services/importFormValidation.js +257 -0
- plexora/client/src/js/services/numericData.js +100 -0
- plexora/client/src/js/services/passVariablesToFrontend.js +20 -0
- plexora/client/src/js/services/simpleEventHandler.js +25 -0
- plexora/client/src/js/vendor.js +29 -0
- plexora/client/src/js/views/channelList.js +663 -0
- plexora/client/src/js/views/colorSwatchPicker.js +202 -0
- plexora/client/src/js/views/columnClassifier.js +146 -0
- plexora/client/src/js/views/coordinateField.js +133 -0
- plexora/client/src/js/views/dataSourceField.js +276 -0
- plexora/client/src/js/views/imageViewer.js +3306 -0
- plexora/client/src/js/views/miniMap.js +787 -0
- plexora/client/src/js/views/navbarControls.js +178 -0
- plexora/client/src/js/views/openProjectPage.js +239 -0
- plexora/client/src/js/views/projectEdit.js +335 -0
- plexora/client/src/js/views/quickViewLanding.js +136 -0
- plexora/client/src/js/views/rainbow.js +326 -0
- plexora/client/src/js/views/requirementsModal.js +572 -0
- plexora/client/src/js/views/rgbImageViewer.js +115 -0
- plexora/client/src/js/views/searchableSelect.js +411 -0
- plexora/client/src/js/views/segmentationProgress.js +122 -0
- plexora/client/src/js/views/toolLoader.js +747 -0
- plexora/client/src/js/views/viewerControls.js +689 -0
- plexora/client/src/js/views/viewerManager.js +434 -0
- plexora/client/src/js/views/viewerSidebar.js +1117 -0
- plexora/client/src/js/workers/tileDecoder.js +130 -0
- plexora/client/src/shaders/frag.glsl +528 -0
- plexora/client/src/shaders/vert.glsl +12 -0
- plexora/client/templates/base.html +212 -0
- plexora/client/templates/index.html +238 -0
- plexora/client/templates/open_project.html +96 -0
- plexora/client/templates/project_columns.html +86 -0
- plexora/client/templates/project_edit.html +196 -0
- plexora/client/templates/upload.html +118 -0
- plexora/connect.py +528 -0
- plexora/datasource.py +782 -0
- plexora/jupyter.py +406 -0
- plexora/notebook_env.py +232 -0
- plexora/paths.py +486 -0
- plexora/plugins/__init__.py +12 -0
- plexora/plugins/cell_explorer/__init__.py +100 -0
- plexora/plugins/cell_explorer/server/__init__.py +11 -0
- plexora/plugins/cell_explorer/server/routes.py +167 -0
- plexora/plugins/cell_explorer/server/state.py +281 -0
- plexora/plugins/cell_explorer/server/values.py +145 -0
- plexora/plugins/cell_explorer/server/variables.py +367 -0
- plexora/plugins/cell_explorer/static/cellExplorerApi.js +158 -0
- plexora/plugins/cell_explorer/static/cellExplorerColors.js +316 -0
- plexora/plugins/cell_explorer/static/cellExplorerContinuous.js +403 -0
- plexora/plugins/cell_explorer/static/cellExplorerFigureBridge.js +133 -0
- plexora/plugins/cell_explorer/static/cellExplorerLegend.js +188 -0
- plexora/plugins/cell_explorer/static/cellExplorerRoiBridge.js +822 -0
- plexora/plugins/cell_explorer/static/cellExplorerSidebarController.js +826 -0
- plexora/plugins/cell_explorer/static/cellExplorerState.js +372 -0
- plexora/plugins/cell_explorer/static/cell_explorer.css +752 -0
- plexora/plugins/cell_explorer/templates/cell_explorer/panel.html +137 -0
- plexora/plugins/figure_builder/__init__.py +125 -0
- plexora/plugins/figure_builder/server/__init__.py +7 -0
- plexora/plugins/figure_builder/server/compose.py +365 -0
- plexora/plugins/figure_builder/server/export.py +784 -0
- plexora/plugins/figure_builder/server/export_jobs.py +148 -0
- plexora/plugins/figure_builder/server/operations.py +556 -0
- plexora/plugins/figure_builder/server/pixels.py +142 -0
- plexora/plugins/figure_builder/server/provenance.py +174 -0
- plexora/plugins/figure_builder/server/render.py +278 -0
- plexora/plugins/figure_builder/server/repository.py +701 -0
- plexora/plugins/figure_builder/server/routes.py +594 -0
- plexora/plugins/figure_builder/server/schema.py +1023 -0
- plexora/plugins/figure_builder/server/sources.py +120 -0
- plexora/plugins/figure_builder/server/textmetrics.py +155 -0
- plexora/plugins/figure_builder/static/figureActions.js +363 -0
- plexora/plugins/figure_builder/static/figureBuilderApi.js +255 -0
- plexora/plugins/figure_builder/static/figureCanvas.js +2362 -0
- plexora/plugins/figure_builder/static/figureCaptureBoxes.js +369 -0
- plexora/plugins/figure_builder/static/figureCaptureDock.js +466 -0
- plexora/plugins/figure_builder/static/figureCaptureTool.js +1034 -0
- plexora/plugins/figure_builder/static/figureConfirm.js +136 -0
- plexora/plugins/figure_builder/static/figureContextBar.js +1193 -0
- plexora/plugins/figure_builder/static/figureContextMenu.js +253 -0
- plexora/plugins/figure_builder/static/figureDocumentState.js +275 -0
- plexora/plugins/figure_builder/static/figureExportUi.js +174 -0
- plexora/plugins/figure_builder/static/figureLibrary.js +247 -0
- plexora/plugins/figure_builder/static/figureQuickEdit.js +628 -0
- plexora/plugins/figure_builder/static/figureRichText.js +634 -0
- plexora/plugins/figure_builder/static/figureSceneSnapshot.js +361 -0
- plexora/plugins/figure_builder/static/figureSchema.js +218 -0
- plexora/plugins/figure_builder/static/figureSidebarController.js +1159 -0
- plexora/plugins/figure_builder/static/figureTextEditor.js +503 -0
- plexora/plugins/figure_builder/static/figureTextPanel.js +491 -0
- plexora/plugins/figure_builder/static/figureViewOptions.js +273 -0
- plexora/plugins/figure_builder/static/figureWorkspace.js +2030 -0
- plexora/plugins/figure_builder/static/figure_builder.css +3292 -0
- plexora/plugins/figure_builder/templates/figure_builder/library.html +74 -0
- plexora/plugins/figure_builder/templates/figure_builder/workspace.html +27 -0
- plexora/plugins/figure_builder/templates/figure_builder/workspace_body.html +458 -0
- plexora/plugins/gating/__init__.py +76 -0
- plexora/plugins/gating/server/__init__.py +0 -0
- plexora/plugins/gating/server/anndata_gates.py +367 -0
- plexora/plugins/gating/server/database.py +16 -0
- plexora/plugins/gating/server/model.py +320 -0
- plexora/plugins/gating/server/routes.py +235 -0
- plexora/plugins/gating/static/csvGatingList.js +947 -0
- plexora/plugins/gating/static/gating.css +289 -0
- plexora/plugins/gating/static/gatingApi.js +221 -0
- plexora/plugins/gating/static/gatingSidebarController.js +513 -0
- plexora/plugins/gating/templates/gating/legacy.html +26 -0
- plexora/plugins/gating/templates/gating/panel.html +69 -0
- plexora/plugins/roi/__init__.py +95 -0
- plexora/plugins/roi/server/__init__.py +8 -0
- plexora/plugins/roi/server/adapters.py +655 -0
- plexora/plugins/roi/server/geojson.py +267 -0
- plexora/plugins/roi/server/geometry.py +181 -0
- plexora/plugins/roi/server/mapping.py +169 -0
- plexora/plugins/roi/server/operations.py +293 -0
- plexora/plugins/roi/server/repository.py +215 -0
- plexora/plugins/roi/server/routes.py +409 -0
- plexora/plugins/roi/server/schema.py +323 -0
- plexora/plugins/roi/static/roi.css +583 -0
- plexora/plugins/roi/static/roiApi.js +177 -0
- plexora/plugins/roi/static/roiFigureBridge.js +107 -0
- plexora/plugins/roi/static/roiGeometry.js +350 -0
- plexora/plugins/roi/static/roiRenderer.js +278 -0
- plexora/plugins/roi/static/roiSidebarController.js +1038 -0
- plexora/plugins/roi/static/roiState.js +518 -0
- plexora/plugins/roi/static/roiTools.js +993 -0
- plexora/plugins/roi/templates/roi/panel.html +240 -0
- plexora/proxy.py +57 -0
- plexora/server/models/adapters/__init__.py +94 -0
- plexora/server/models/adapters/anndata_adapter.py +388 -0
- plexora/server/models/adapters/base.py +93 -0
- plexora/server/models/adapters/classify.py +192 -0
- plexora/server/models/adapters/csv_adapter.py +105 -0
- plexora/server/models/adapters/inspection.py +265 -0
- plexora/server/models/adapters/spatialdata_adapter.py +248 -0
- plexora/server/models/centroid_tiles.py +305 -0
- plexora/server/models/data_model.py +1659 -0
- plexora/server/models/database_model.py +169 -0
- plexora/server/models/project.py +1270 -0
- plexora/server/plugins.py +199 -0
- plexora/server/routes/browse_routes.py +41 -0
- plexora/server/routes/data_routes.py +294 -0
- plexora/server/routes/import_routes.py +603 -0
- plexora/server/routes/page_routes.py +139 -0
- plexora/server/routes/project_routes.py +438 -0
- plexora/server/routes/quick_view_routes.py +62 -0
- plexora/server/routes/system_routes.py +39 -0
- plexora/server/routes/tool_routes.py +418 -0
- plexora/server/utils/addHEColumns.py +11 -0
- plexora/server/utils/fast_png.py +97 -0
- plexora/server/utils/fullConversion.py +50 -0
- plexora/server/utils/native_dialog.py +168 -0
- plexora/server/utils/pre_normalization.py +47 -0
- plexora/server/utils/segmentation_pyramid.py +598 -0
- plexora/server/utils/smallestenclosingcircle.py +126 -0
- plexora/server/utils/tiffsurgeon.py +372 -0
- plexora/server_cli.py +48 -0
- plexora/test.py +0 -0
- plexora-0.0.1.dist-info/METADATA +341 -0
- plexora-0.0.1.dist-info/RECORD +248 -0
- plexora-0.0.1.dist-info/WHEEL +5 -0
- plexora-0.0.1.dist-info/entry_points.txt +12 -0
- plexora-0.0.1.dist-info/licenses/LICENSE +207 -0
- plexora-0.0.1.dist-info/top_level.txt +1 -0
plexora/paths.py
ADDED
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
"""Where Plexora's data lives, and who decides.
|
|
2
|
+
|
|
3
|
+
Every path decision in the app comes from here. Before this module the answer
|
|
4
|
+
was computed once, at import time, in `plexora/__init__.py`, and its last
|
|
5
|
+
fallback was `Path("plexora/data").resolve()` -- relative to the *current
|
|
6
|
+
working directory*. That is right only when the process was launched from the
|
|
7
|
+
repository root, which is what `python run.py` does and what the README's
|
|
8
|
+
editable install assumed. For a real `pip install plexora` it meant that
|
|
9
|
+
importing the package from `~/analysis` silently created and used
|
|
10
|
+
`~/analysis/plexora/data`, so running a notebook from a different folder the
|
|
11
|
+
next day made every project look as though it had been deleted.
|
|
12
|
+
|
|
13
|
+
Two properties matter more than the specific locations:
|
|
14
|
+
|
|
15
|
+
**Resolved on demand, never snapshotted.** Everything here is a function.
|
|
16
|
+
Modules that did `from plexora import data_path` captured the value at import,
|
|
17
|
+
which meant any decision made after the first `import plexora` was silently
|
|
18
|
+
ignored -- that is why the Jupyter sidecar has to set real OS environment
|
|
19
|
+
variables before spawning a child rather than just passing `--data-dir`. A
|
|
20
|
+
function can also grow a parameter later; a module constant cannot, and the
|
|
21
|
+
per-user and shared-root work depends on exactly that.
|
|
22
|
+
|
|
23
|
+
**The write root and the read root are different questions.** A project the
|
|
24
|
+
user imported lives in their own root and is theirs to change. A project on a
|
|
25
|
+
site-managed shared root is readable by everyone and writable by nobody, yet a
|
|
26
|
+
user exploring it still needs somewhere to put their gates, ROIs and figures.
|
|
27
|
+
So reads resolve across `roots()` while writes always land in `data_root()`.
|
|
28
|
+
When a project's home root *is* the user root -- the entire single-user case --
|
|
29
|
+
the two collapse to one directory and nothing is different from before.
|
|
30
|
+
|
|
31
|
+
Deliberately a leaf module: it imports nothing from `plexora`, so it cannot
|
|
32
|
+
participate in the import cycle that `plexora/__init__.py` sits at the centre
|
|
33
|
+
of, and it is safe to call from anywhere including the CLI.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
import json
|
|
39
|
+
import os
|
|
40
|
+
import sys
|
|
41
|
+
import threading
|
|
42
|
+
from pathlib import Path
|
|
43
|
+
from typing import NamedTuple
|
|
44
|
+
|
|
45
|
+
from platformdirs import user_config_dir, user_data_dir
|
|
46
|
+
|
|
47
|
+
#: Passed to platformdirs as both the app name and (via appauthor=False) the
|
|
48
|
+
#: whole of the path tail. Without appauthor=False, Windows gets
|
|
49
|
+
#: `AppData\Local\plexora\plexora` -- the vendor directory doubled up, which is
|
|
50
|
+
#: what the old appdirs call produced.
|
|
51
|
+
APP_NAME = "plexora"
|
|
52
|
+
|
|
53
|
+
ENV_DATA_PATH = "PLEXORA_DATA_PATH"
|
|
54
|
+
ENV_SHARED_PATH = "PLEXORA_SHARED_PATH"
|
|
55
|
+
|
|
56
|
+
CONFIG_FILENAME = "config.json"
|
|
57
|
+
SETTINGS_FILENAME = "settings.json"
|
|
58
|
+
|
|
59
|
+
#: Directory under a root holding every figure. Dot-prefixed so a project
|
|
60
|
+
#: literally named "figures" cannot collide with it -- projects are directories
|
|
61
|
+
#: under the same root. See figure_builder's repository module.
|
|
62
|
+
FIGURES_DIRNAME = ".figures"
|
|
63
|
+
|
|
64
|
+
#: Written and removed to prove a root is actually writable. A probe beats
|
|
65
|
+
#: `os.access`, which on Windows reports the DACL rather than the effective
|
|
66
|
+
#: permission and cheerfully says yes for a directory that then refuses the
|
|
67
|
+
#: write.
|
|
68
|
+
_PROBE_PREFIX = ".plexora-write-probe"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _probe_name() -> str:
|
|
72
|
+
"""A probe filename no other prober can be holding.
|
|
73
|
+
|
|
74
|
+
Per process AND per thread, because both really do coincide: Waitress runs
|
|
75
|
+
eight threads and the segmentation job adds more, and a cached miss lets
|
|
76
|
+
two of them probe one root at the same moment. With a shared name, one
|
|
77
|
+
thread's unlink lands between the other's write and its own unlink -- and
|
|
78
|
+
on Windows that surfaces as a PermissionError, which this function would
|
|
79
|
+
have read as "not writable". A root wrongly judged read-only silently
|
|
80
|
+
stops recording config changes, which is a far worse failure than the
|
|
81
|
+
write it was trying to avoid.
|
|
82
|
+
"""
|
|
83
|
+
return f"{_PROBE_PREFIX}.{os.getpid()}.{threading.get_ident()}"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class DataRootError(RuntimeError):
|
|
87
|
+
"""The resolved data root cannot be used.
|
|
88
|
+
|
|
89
|
+
Raised at resolution rather than at first write, so the message names the
|
|
90
|
+
path and the flag that changes it instead of surfacing as an OSError three
|
|
91
|
+
frames down inside a request.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class Resolution(NamedTuple):
|
|
96
|
+
"""A resolved root and the rule that chose it, so `plexora where` can
|
|
97
|
+
explain itself. Users who cannot find their projects need to know *why*
|
|
98
|
+
Plexora picked a directory, not just which one."""
|
|
99
|
+
|
|
100
|
+
path: Path
|
|
101
|
+
rule: str
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
#: Resolution is pure with respect to the environment but does real filesystem
|
|
105
|
+
#: work (mkdir, the write probe), so it is done once per process. `reset()`
|
|
106
|
+
#: clears it; the test suite calls that after repointing the environment.
|
|
107
|
+
_cache: dict[str, object] = {}
|
|
108
|
+
_cache_lock = threading.RLock()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def reset() -> None:
|
|
112
|
+
"""Forget the resolved roots.
|
|
113
|
+
|
|
114
|
+
For tests, which repoint `PLEXORA_DATA_PATH` at a tmp_path per test, and
|
|
115
|
+
for `plexora config set`, which changes the answer underneath a live
|
|
116
|
+
process.
|
|
117
|
+
"""
|
|
118
|
+
with _cache_lock:
|
|
119
|
+
_cache.clear()
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# -- the settings file ---------------------------------------------------
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def settings_path() -> Path:
|
|
126
|
+
"""Where the persistent choice of data directory is recorded.
|
|
127
|
+
|
|
128
|
+
In the config directory, which is emphatically *not* derived from
|
|
129
|
+
`data_root()` -- the file's whole job is to say where the data root is, so
|
|
130
|
+
resolving it through one would be circular.
|
|
131
|
+
|
|
132
|
+
That said, it does not always land somewhere else: Windows and macOS use a
|
|
133
|
+
single per-app directory for both, so by default this file sits inside the
|
|
134
|
+
default data root even though it is not reached through it. The case that
|
|
135
|
+
matters is a user who moves their data elsewhere and later deletes the old
|
|
136
|
+
default directory. They lose the pointer and Plexora returns to the
|
|
137
|
+
default -- but it says so, because an absent config.json is exactly what
|
|
138
|
+
`first_run_notice` reports on.
|
|
139
|
+
"""
|
|
140
|
+
return Path(user_config_dir(APP_NAME, appauthor=False, roaming=False)) / SETTINGS_FILENAME
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def read_settings() -> dict:
|
|
144
|
+
"""The settings file, or {} when there is not one yet.
|
|
145
|
+
|
|
146
|
+
A damaged file reads as {} rather than raising: the fallbacks below are all
|
|
147
|
+
still available, and refusing to start because a preferences file is
|
|
148
|
+
corrupt would be a worse failure than quietly using the default.
|
|
149
|
+
"""
|
|
150
|
+
path = settings_path()
|
|
151
|
+
try:
|
|
152
|
+
text = path.read_text(encoding="utf-8")
|
|
153
|
+
except (OSError, ValueError):
|
|
154
|
+
return {}
|
|
155
|
+
if not text.strip():
|
|
156
|
+
return {}
|
|
157
|
+
try:
|
|
158
|
+
data = json.loads(text)
|
|
159
|
+
except ValueError:
|
|
160
|
+
return {}
|
|
161
|
+
return data if isinstance(data, dict) else {}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def write_settings(data: dict) -> None:
|
|
165
|
+
"""Replace the settings file in one step.
|
|
166
|
+
|
|
167
|
+
Same temp-file-and-rename as the project config, and for the same reason:
|
|
168
|
+
a reader in another process sees the whole previous file or the whole new
|
|
169
|
+
one, never the empty window that `open(path, "w")` leaves open.
|
|
170
|
+
"""
|
|
171
|
+
path = settings_path()
|
|
172
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
173
|
+
tmp = path.with_name(f"{path.name}.{os.getpid()}.tmp")
|
|
174
|
+
try:
|
|
175
|
+
with tmp.open("w", encoding="utf-8") as handle:
|
|
176
|
+
json.dump(data, handle, indent=4)
|
|
177
|
+
handle.flush()
|
|
178
|
+
os.fsync(handle.fileno())
|
|
179
|
+
os.replace(tmp, path)
|
|
180
|
+
finally:
|
|
181
|
+
tmp.unlink(missing_ok=True)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# -- the user's writable root --------------------------------------------
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _candidate_data_root() -> Resolution:
|
|
188
|
+
"""Pick the data root, without touching the filesystem.
|
|
189
|
+
|
|
190
|
+
First hit wins, and the order is deliberate: an explicit instruction for
|
|
191
|
+
this process beats a stored preference, which beats what the build shape
|
|
192
|
+
implies, which beats the platform default.
|
|
193
|
+
"""
|
|
194
|
+
from_env = os.environ.get(ENV_DATA_PATH)
|
|
195
|
+
if from_env and from_env.strip():
|
|
196
|
+
return Resolution(Path(from_env).expanduser().resolve(),
|
|
197
|
+
f"{ENV_DATA_PATH} environment variable")
|
|
198
|
+
|
|
199
|
+
stored = read_settings().get("data_dir")
|
|
200
|
+
if isinstance(stored, str) and stored.strip():
|
|
201
|
+
return Resolution(Path(stored).expanduser().resolve(),
|
|
202
|
+
f"data_dir in {settings_path()}")
|
|
203
|
+
|
|
204
|
+
if getattr(sys, "frozen", False):
|
|
205
|
+
# A portable build keeps its data beside the executable so the whole
|
|
206
|
+
# thing can be moved or handed over on a stick as one unit.
|
|
207
|
+
return Resolution(Path(sys.executable).parent.resolve() / "data",
|
|
208
|
+
"frozen build, beside the executable")
|
|
209
|
+
|
|
210
|
+
return Resolution(Path(user_data_dir(APP_NAME, appauthor=False)).resolve(),
|
|
211
|
+
"platform default")
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _prepare_data_root(resolution: Resolution) -> Resolution:
|
|
215
|
+
"""Create the root and prove it is writable.
|
|
216
|
+
|
|
217
|
+
The probe runs once per process, on the resolution that gets cached. It is
|
|
218
|
+
worth the two syscalls: a read-only or quota-exhausted root otherwise
|
|
219
|
+
presents as a stack trace from whichever request happened to write first,
|
|
220
|
+
which tells the user nothing about what to do.
|
|
221
|
+
"""
|
|
222
|
+
path = resolution.path
|
|
223
|
+
existed = path.exists()
|
|
224
|
+
try:
|
|
225
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
226
|
+
except OSError as exc:
|
|
227
|
+
raise DataRootError(
|
|
228
|
+
f"Plexora's data directory cannot be created: {path}\n"
|
|
229
|
+
f"Chosen by: {resolution.rule}\n"
|
|
230
|
+
f"Point it somewhere writable with 'plexora --data-dir <path>' or "
|
|
231
|
+
f"'plexora config set data-dir <path>'."
|
|
232
|
+
) from exc
|
|
233
|
+
|
|
234
|
+
probe = path / _probe_name()
|
|
235
|
+
try:
|
|
236
|
+
probe.write_text("", encoding="utf-8")
|
|
237
|
+
probe.unlink()
|
|
238
|
+
except OSError as exc:
|
|
239
|
+
raise DataRootError(
|
|
240
|
+
f"Plexora's data directory is not writable: {path}\n"
|
|
241
|
+
f"Chosen by: {resolution.rule}\n"
|
|
242
|
+
f"Point it somewhere writable with 'plexora --data-dir <path>' or "
|
|
243
|
+
f"'plexora config set data-dir <path>'."
|
|
244
|
+
) from exc
|
|
245
|
+
|
|
246
|
+
# "First run" is judged by the absence of a project registry rather than by
|
|
247
|
+
# whether we just created the directory: on a shared machine an admin often
|
|
248
|
+
# makes the directory ahead of time, and the notice is still worth printing
|
|
249
|
+
# for the user who has never seen it.
|
|
250
|
+
with _cache_lock:
|
|
251
|
+
_cache["first_run"] = not (path / CONFIG_FILENAME).exists()
|
|
252
|
+
_cache["created"] = not existed
|
|
253
|
+
return resolution
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def data_root_resolution() -> Resolution:
|
|
257
|
+
"""The user's writable root, with the rule that chose it."""
|
|
258
|
+
with _cache_lock:
|
|
259
|
+
cached = _cache.get("data_root")
|
|
260
|
+
if cached is not None:
|
|
261
|
+
return cached # type: ignore[return-value]
|
|
262
|
+
resolved = _prepare_data_root(_candidate_data_root())
|
|
263
|
+
with _cache_lock:
|
|
264
|
+
_cache["data_root"] = resolved
|
|
265
|
+
return resolved
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def data_root() -> Path:
|
|
269
|
+
"""Where this user's projects, figures and per-project state are written."""
|
|
270
|
+
return data_root_resolution().path
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def first_run_notice() -> str | None:
|
|
274
|
+
"""A one-off line naming the data directory, or None if it is not new.
|
|
275
|
+
|
|
276
|
+
Printed by the CLI. The location is a platform convention directory, which
|
|
277
|
+
is the right default and also the one a user is least likely to guess, so
|
|
278
|
+
saying it once beats making them run `plexora where` to find out where
|
|
279
|
+
their work went.
|
|
280
|
+
"""
|
|
281
|
+
resolution = data_root_resolution()
|
|
282
|
+
with _cache_lock:
|
|
283
|
+
if not _cache.get("first_run"):
|
|
284
|
+
return None
|
|
285
|
+
return (
|
|
286
|
+
f"Plexora will keep your projects in:\n"
|
|
287
|
+
f" {resolution.path}\n"
|
|
288
|
+
f"Move it any time with 'plexora config set data-dir <path>'."
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
# -- shared, read-mostly roots -------------------------------------------
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def shared_root_resolutions() -> list[Resolution]:
|
|
296
|
+
"""Site-managed roots holding projects several users can open.
|
|
297
|
+
|
|
298
|
+
From `PLEXORA_SHARED_PATH` (os.pathsep-separated, like PATH) and then
|
|
299
|
+
`shared_dirs` in the settings file. Neither is created if it is missing:
|
|
300
|
+
a shared root is somebody else's to provision, and silently making an empty
|
|
301
|
+
one would turn a typo into a root that exists and holds nothing.
|
|
302
|
+
|
|
303
|
+
Cached like `data_root`, and for a sharper reason: `shared_roots()` being
|
|
304
|
+
empty is what lets the tile path skip resolving which root a project came
|
|
305
|
+
from, so this is consulted often enough that a settings-file read per call
|
|
306
|
+
would show up.
|
|
307
|
+
"""
|
|
308
|
+
with _cache_lock:
|
|
309
|
+
cached = _cache.get("shared_roots")
|
|
310
|
+
if cached is not None:
|
|
311
|
+
return list(cached) # type: ignore[arg-type]
|
|
312
|
+
|
|
313
|
+
seen: set[Path] = set()
|
|
314
|
+
out: list[Resolution] = []
|
|
315
|
+
|
|
316
|
+
def _add(raw, rule):
|
|
317
|
+
if not isinstance(raw, str) or not raw.strip():
|
|
318
|
+
return
|
|
319
|
+
path = Path(raw).expanduser().resolve()
|
|
320
|
+
if path in seen:
|
|
321
|
+
return
|
|
322
|
+
seen.add(path)
|
|
323
|
+
out.append(Resolution(path, rule))
|
|
324
|
+
|
|
325
|
+
from_env = os.environ.get(ENV_SHARED_PATH) or ""
|
|
326
|
+
for entry in from_env.split(os.pathsep):
|
|
327
|
+
_add(entry, f"{ENV_SHARED_PATH} environment variable")
|
|
328
|
+
|
|
329
|
+
stored = read_settings().get("shared_dirs")
|
|
330
|
+
if isinstance(stored, (list, tuple)):
|
|
331
|
+
for entry in stored:
|
|
332
|
+
_add(entry, f"shared_dirs in {settings_path()}")
|
|
333
|
+
|
|
334
|
+
# The user's own root is never also a shared root, whatever the
|
|
335
|
+
# configuration says: it is already first in roots(), and letting it appear
|
|
336
|
+
# twice would make a project look shared to its own owner.
|
|
337
|
+
own = data_root()
|
|
338
|
+
resolved = [r for r in out if r.path != own]
|
|
339
|
+
with _cache_lock:
|
|
340
|
+
_cache["shared_roots"] = resolved
|
|
341
|
+
return list(resolved)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def shared_roots() -> list[Path]:
|
|
345
|
+
return [resolution.path for resolution in shared_root_resolutions()]
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def roots() -> list[Path]:
|
|
349
|
+
"""Every root a project may be found in, the user's own first.
|
|
350
|
+
|
|
351
|
+
Order is the precedence rule: a name present in more than one root resolves
|
|
352
|
+
to the user's copy. Somebody who has made their own version of a shared
|
|
353
|
+
project means to open theirs.
|
|
354
|
+
"""
|
|
355
|
+
return [data_root(), *shared_roots()]
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
# -- paths within a root -------------------------------------------------
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def config_path(root=None) -> Path:
|
|
362
|
+
"""The project registry for one root. Defaults to the user's own."""
|
|
363
|
+
return Path(root) / CONFIG_FILENAME if root is not None else data_root() / CONFIG_FILENAME
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def project_dir(name, root=None) -> Path:
|
|
367
|
+
"""A project's directory in `root`, or in the user's own root.
|
|
368
|
+
|
|
369
|
+
A pure join -- it does not search and does not create. Callers that need to
|
|
370
|
+
know which root actually owns a project ask the project registry, which is
|
|
371
|
+
the thing that reads config.json.
|
|
372
|
+
"""
|
|
373
|
+
base = Path(root) if root is not None else data_root()
|
|
374
|
+
return base / name
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def project_state_dir(name) -> Path:
|
|
378
|
+
"""Where this user's own state for `name` is written, whoever owns it.
|
|
379
|
+
|
|
380
|
+
Always under the user's root, including for a project whose home is a
|
|
381
|
+
shared root. That is what makes a shared project explorable rather than
|
|
382
|
+
merely visible: the gates, ROIs and plugin tables a user produces while
|
|
383
|
+
looking at somebody else's data are theirs, and they have to land
|
|
384
|
+
somewhere writable.
|
|
385
|
+
"""
|
|
386
|
+
return data_root() / name
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def project_roots(name) -> list[Path]:
|
|
390
|
+
"""Every root with a directory for `name`, the user's own first.
|
|
391
|
+
|
|
392
|
+
Directory existence only -- ownership is a question for the registry. Used
|
|
393
|
+
to find derived artifacts (tile pyramids, centroid tiles) that may have
|
|
394
|
+
been built into either the home root or the user's own.
|
|
395
|
+
"""
|
|
396
|
+
return [root for root in roots() if (root / name).is_dir()]
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def is_writable(root) -> bool:
|
|
400
|
+
"""Whether a root accepts writes, probed once per process per root.
|
|
401
|
+
|
|
402
|
+
Shared roots are usually read-only to the people opening them, and that is
|
|
403
|
+
the whole question for derived artifacts: a tile pyramid that already
|
|
404
|
+
exists beside a shared image should be read where it is, but one that has
|
|
405
|
+
to be built has to go somewhere this user can actually write.
|
|
406
|
+
"""
|
|
407
|
+
path = Path(root)
|
|
408
|
+
key = f"writable:{path}"
|
|
409
|
+
with _cache_lock:
|
|
410
|
+
cached = _cache.get(key)
|
|
411
|
+
if cached is not None:
|
|
412
|
+
return bool(cached)
|
|
413
|
+
probe = path / _probe_name()
|
|
414
|
+
try:
|
|
415
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
416
|
+
probe.write_text("", encoding="utf-8")
|
|
417
|
+
probe.unlink()
|
|
418
|
+
writable = True
|
|
419
|
+
except OSError:
|
|
420
|
+
writable = False
|
|
421
|
+
with _cache_lock:
|
|
422
|
+
_cache[key] = writable
|
|
423
|
+
return writable
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def derived_root(name, home_root=None) -> Path:
|
|
427
|
+
"""Where derived artifacts for `name` are BUILT -- pyramids, tile caches.
|
|
428
|
+
|
|
429
|
+
The project's own root when that can be written to, so two users opening
|
|
430
|
+
the same shared image share one pyramid rather than each spending minutes
|
|
431
|
+
building their own. Otherwise the user's own root, because a derived
|
|
432
|
+
artifact that cannot be written is a feature that does not work.
|
|
433
|
+
|
|
434
|
+
Reading is the other half and is not symmetric -- use `find_derived`, which
|
|
435
|
+
also looks where a previous build may have put something.
|
|
436
|
+
"""
|
|
437
|
+
if home_root is not None and is_writable(home_root):
|
|
438
|
+
return Path(home_root) / name
|
|
439
|
+
for root in project_roots(name):
|
|
440
|
+
if is_writable(root):
|
|
441
|
+
return root / name
|
|
442
|
+
return project_state_dir(name)
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def find_derived(name, *parts, home_root=None) -> Path | None:
|
|
446
|
+
"""An existing derived artifact, or None.
|
|
447
|
+
|
|
448
|
+
Looks in the project's own root before the user's own, so a pyramid the
|
|
449
|
+
site built beside a shared image wins over a stale private copy. Returns
|
|
450
|
+
None rather than a non-existent path: callers use that to decide whether
|
|
451
|
+
to build, and a path that merely might exist cannot answer that.
|
|
452
|
+
"""
|
|
453
|
+
seen: list[Path] = []
|
|
454
|
+
if home_root is not None:
|
|
455
|
+
seen.append(Path(home_root) / name)
|
|
456
|
+
seen.extend(root / name for root in project_roots(name))
|
|
457
|
+
seen.append(project_state_dir(name))
|
|
458
|
+
for base in seen:
|
|
459
|
+
candidate = base.joinpath(*parts) if parts else base
|
|
460
|
+
if candidate.exists():
|
|
461
|
+
return candidate
|
|
462
|
+
return None
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def figures_root() -> Path:
|
|
466
|
+
"""Where this user's figures live.
|
|
467
|
+
|
|
468
|
+
Never a shared root. A figure can span several datasources or none, so no
|
|
469
|
+
project owns one and there is nothing for a site-managed root to hold.
|
|
470
|
+
"""
|
|
471
|
+
return data_root() / FIGURES_DIRNAME
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def describe() -> list[str]:
|
|
475
|
+
"""Human-readable lines for `plexora where`."""
|
|
476
|
+
resolution = data_root_resolution()
|
|
477
|
+
lines = [f"data root: {resolution.path}", f" chosen by: {resolution.rule}"]
|
|
478
|
+
shared = shared_root_resolutions()
|
|
479
|
+
if not shared:
|
|
480
|
+
lines.append("shared roots: (none)")
|
|
481
|
+
return lines
|
|
482
|
+
for entry in shared:
|
|
483
|
+
state = "" if entry.path.is_dir() else " [missing]"
|
|
484
|
+
lines.append(f"shared root: {entry.path}{state}")
|
|
485
|
+
lines.append(f" chosen by: {entry.rule}")
|
|
486
|
+
return lines
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Plugins bundled with Plexora.
|
|
2
|
+
|
|
3
|
+
Each subpackage here is a plugin: a directory holding its own server code,
|
|
4
|
+
client assets, templates and tests, exposing a module-level `PLUGIN` descriptor
|
|
5
|
+
(see plexora.api.plugin.Plugin).
|
|
6
|
+
|
|
7
|
+
They are discovered by scanning this package -- see plexora.server.plugins --
|
|
8
|
+
and are otherwise ordinary plugins. They get no privileges a pip-installed
|
|
9
|
+
third-party plugin lacks, and they consume only the public `plexora.api`
|
|
10
|
+
surface. That is deliberate: a gap in the API becomes a gap in the shipped
|
|
11
|
+
product rather than something only outside authors run into.
|
|
12
|
+
"""
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Cell Explorer: colour every cell on the image by one metadata column.
|
|
2
|
+
|
|
3
|
+
The whole plugin follows from one boundary: **it owns the mapping from metadata
|
|
4
|
+
to colour, and core owns the geometry**. Cell Explorer never learns how a
|
|
5
|
+
segmentation tile is drawn, never renders a centroid, and never decides whether
|
|
6
|
+
cells appear as points, outlines or filled shapes -- it hands core a colour per
|
|
7
|
+
cell id and core draws it whichever way the Cells control is set to. That is
|
|
8
|
+
what keeps this plugin small, and what means the next plugin that wants to
|
|
9
|
+
colour cells gets all three representations for free.
|
|
10
|
+
|
|
11
|
+
Deliberately NOT here: marker expression. `.X` and its layers are what
|
|
12
|
+
Thresholding reads, and they are a different question with a different scale
|
|
13
|
+
problem (see TableHandle.log_transformed). This reads annotations -- phenotype,
|
|
14
|
+
cluster, neighbourhood, confidence, area -- and only through the format-agnostic
|
|
15
|
+
`table.metadata_values`, so a CSV column and an AnnData `.obs` column are the
|
|
16
|
+
same thing here.
|
|
17
|
+
|
|
18
|
+
It also never writes. Colours, hidden categories and ranges are display
|
|
19
|
+
preferences and live in the plugin store; the table is the source of truth and
|
|
20
|
+
is opened read-only.
|
|
21
|
+
|
|
22
|
+
Kept import-light, like every descriptor module: this is imported whenever the
|
|
23
|
+
plugin is activated, and building the Blueprint (which pulls in numpy-heavy
|
|
24
|
+
encoding and the repository) is left to the factory.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from plexora.api.plugin import Plugin, Requires
|
|
28
|
+
|
|
29
|
+
VERSION = "20260822_figure_bridge"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _blueprint():
|
|
33
|
+
from plexora.plugins.cell_explorer.server.routes import cell_explorer_bp
|
|
34
|
+
|
|
35
|
+
return cell_explorer_bp
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
PLUGIN = Plugin(
|
|
39
|
+
name="cell_explorer",
|
|
40
|
+
label="Cell Explorer",
|
|
41
|
+
version=VERSION,
|
|
42
|
+
blueprint_factory=_blueprint,
|
|
43
|
+
# Templates are namespaced by plugin name, so two plugins can both ship a
|
|
44
|
+
# "panel.html" without colliding in Flask's shared template lookup.
|
|
45
|
+
panels={"tool_panel_slot": "cell_explorer/panel.html"},
|
|
46
|
+
# Listed in dependency order for reading, not because the browser needs it:
|
|
47
|
+
# every cross-file reference is inside a method or a constructor, and
|
|
48
|
+
# toolLoader awaits all seven before anything is activated, so the bindings
|
|
49
|
+
# resolve whatever sequence they arrive in. What DOES matter is that all
|
|
50
|
+
# seven are here -- one omitted is a plugin that loads and does nothing,
|
|
51
|
+
# which is what tests/test_cell_explorer_boot.py exists to catch.
|
|
52
|
+
scripts=(
|
|
53
|
+
"cellExplorerColors.js",
|
|
54
|
+
"cellExplorerApi.js",
|
|
55
|
+
"cellExplorerState.js",
|
|
56
|
+
"cellExplorerLegend.js",
|
|
57
|
+
"cellExplorerContinuous.js",
|
|
58
|
+
"cellExplorerRoiBridge.js",
|
|
59
|
+
# Answers Figure Builder's two capture/restore events, the same way
|
|
60
|
+
# the ROI bridge above answers ROI's -- events both ways, no import
|
|
61
|
+
# either way.
|
|
62
|
+
"cellExplorerFigureBridge.js",
|
|
63
|
+
"cellExplorerSidebarController.js",
|
|
64
|
+
),
|
|
65
|
+
styles=("cell_explorer.css",),
|
|
66
|
+
# A table and a cell id are the floor: without them there is nothing to
|
|
67
|
+
# colour and no way to say which cell a value belongs to.
|
|
68
|
+
#
|
|
69
|
+
# Segmentation and the coordinates are OPTIONAL, and that is the important
|
|
70
|
+
# part. Either one is enough to draw cells -- a mask gives outlines and
|
|
71
|
+
# filled shapes, x/y gives centroids -- so requiring the mask would rule out
|
|
72
|
+
# every project that has coordinates and no segmentation, which is a large
|
|
73
|
+
# share of them. `Requires` has no way to express "segmentation OR (x AND
|
|
74
|
+
# y)", so all three are offered and the panel checks for itself that at
|
|
75
|
+
# least one arrived (see `intro`, and the empty state in panel.html).
|
|
76
|
+
#
|
|
77
|
+
# `role:x`/`role:y` rather than `coordinates`: the coordinate question is a
|
|
78
|
+
# CSV one. On AnnData and SpatialData x/y are answered at import, when the
|
|
79
|
+
# adapter is told where to read them from, and asking again would put an
|
|
80
|
+
# obsm picker in front of a user whose coordinates are already resolved.
|
|
81
|
+
# Core translates between the two forms per format -- see plugin.py's
|
|
82
|
+
# _coordinate_keys.
|
|
83
|
+
requires=Requires(
|
|
84
|
+
table=True,
|
|
85
|
+
roles=("cell_id",),
|
|
86
|
+
optional=("segmentation", "role:x", "role:y"),
|
|
87
|
+
),
|
|
88
|
+
# Shown instead of core's generic subtitle, which says Plexora filled these
|
|
89
|
+
# in from the data -- true of a form full of guesses to confirm, and wrong
|
|
90
|
+
# for one that is asking for the thing without which this tool has nothing
|
|
91
|
+
# to draw on.
|
|
92
|
+
intro=("Colouring cells needs a way to draw them: a segmentation mask, or "
|
|
93
|
+
"X/Y coordinates for centroids. Either one is enough."),
|
|
94
|
+
# This is the plugin's whole purpose. It gets a LAYER of its own -- see
|
|
95
|
+
# ImageViewer.registerCellLayer -- with its own colours, mode and opacity,
|
|
96
|
+
# which survive another tool being opened over it. Opening Thresholding
|
|
97
|
+
# switches this layer off; its card's eye turns it back on, and the two then
|
|
98
|
+
# stack: gated cells over the phenotype map.
|
|
99
|
+
owns_cell_layer=True,
|
|
100
|
+
)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Cell Explorer's server half.
|
|
2
|
+
|
|
3
|
+
Four modules, one job each: `variables` decides what can be coloured by and how,
|
|
4
|
+
`values` moves one column's values to the browser, `state` remembers display
|
|
5
|
+
preferences, and `routes` is the thin HTTP surface over the three.
|
|
6
|
+
|
|
7
|
+
Everything reads through `plexora.api` and nothing else. That is not a style
|
|
8
|
+
rule -- `data_model` keeps the loaded table in module globals under a load lock
|
|
9
|
+
with two adjacent loaders whose names differ by one underscore, and the api
|
|
10
|
+
handles call the right one. See plexora/api/__init__.py.
|
|
11
|
+
"""
|