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/api/dataset.py
ADDED
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
"""The dataset a plugin is handed.
|
|
2
|
+
|
|
3
|
+
Every plugin receives image data. It may additionally receive segmentation and
|
|
4
|
+
a feature table (CSV, AnnData or SpatialData), plus metadata naming which
|
|
5
|
+
columns hold the cell id, image id, coordinates and cell type, and which
|
|
6
|
+
columns are markers rather than measurements.
|
|
7
|
+
|
|
8
|
+
Three rules make this contract durable:
|
|
9
|
+
|
|
10
|
+
**Plugins read roles, never column names.** `schema.x` resolves to whatever the
|
|
11
|
+
project recorded for the `x` role. A plugin that hardcodes `"X_centroid"`
|
|
12
|
+
breaks on the next dataset; one that reads `schema.x` does not.
|
|
13
|
+
|
|
14
|
+
**Plugins never touch data_model directly.** This module does, and it is core
|
|
15
|
+
code, so it is free to. That inversion is the whole point: `data_model` holds
|
|
16
|
+
mutable module-level globals mutated under a load lock, with two confusingly
|
|
17
|
+
adjacent loaders -- `_ensure_loaded()` warms the feature table/BallTree while
|
|
18
|
+
`ensure_loaded()` warms the image pyramid and returns load_generation. Handing
|
|
19
|
+
that surface to third parties would freeze it forever and invite the exact race
|
|
20
|
+
its own comments warn about. Handles below call the right one and expose
|
|
21
|
+
neither.
|
|
22
|
+
|
|
23
|
+
**Plugins never read the raw config entry.** They get `Project`
|
|
24
|
+
(server/models/project.py), which is typed and has one definition of every
|
|
25
|
+
field. The handles here are the read-only slice of it a plugin needs; anything
|
|
26
|
+
missing from them is a gap to fill here rather than to route around, since a
|
|
27
|
+
plugin that learns the on-disk shape freezes that shape forever.
|
|
28
|
+
|
|
29
|
+
A role a project has not collected yet resolves to None. That is not an error
|
|
30
|
+
state -- it is what a plugin declares in `Requires` so the host can ask for it
|
|
31
|
+
(see plexora/api/plugin.py).
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
from dataclasses import dataclass, field
|
|
37
|
+
from typing import Any, ClassVar, Mapping
|
|
38
|
+
|
|
39
|
+
from plexora.server.models import data_model
|
|
40
|
+
from plexora.server.models.adapters import MetadataColumn
|
|
41
|
+
from plexora.server.models.project import ROLE_NAMES, Project
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class DatasetSchema:
|
|
46
|
+
"""Role -> column name, as the project recorded it.
|
|
47
|
+
|
|
48
|
+
Cheap and load-free: this reads the project record only, never the table.
|
|
49
|
+
Marker discovery is a property of the data and lives on TableHandle.markers.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
cell_id: str | None = None
|
|
53
|
+
x: str | None = None
|
|
54
|
+
y: str | None = None
|
|
55
|
+
celltype: str | None = None
|
|
56
|
+
image_id: str | None = None
|
|
57
|
+
#: Roles added to the record later land here rather than forcing a
|
|
58
|
+
#: dataclass change that would break every plugin's constructor call.
|
|
59
|
+
extra: Mapping[str, str] = field(default_factory=dict)
|
|
60
|
+
|
|
61
|
+
#: The roles that are proper fields above. Anything in ROLE_NAMES but not
|
|
62
|
+
#: here goes to `extra`.
|
|
63
|
+
_FIELDS: ClassVar[tuple] = ("cell_id", "x", "y", "celltype", "image_id")
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def from_project(cls, project: Project) -> "DatasetSchema | None":
|
|
67
|
+
"""None when the project has no feature table -- there are no columns
|
|
68
|
+
for a role to name."""
|
|
69
|
+
if not project.has_table:
|
|
70
|
+
return None
|
|
71
|
+
roles = project.roles
|
|
72
|
+
extra = {
|
|
73
|
+
role: roles.get(role)
|
|
74
|
+
for role in ROLE_NAMES
|
|
75
|
+
if role not in cls._FIELDS and roles.get(role)
|
|
76
|
+
}
|
|
77
|
+
return cls(**{role: roles.get(role) for role in cls._FIELDS}, extra=extra)
|
|
78
|
+
|
|
79
|
+
def get(self, role: str) -> str | None:
|
|
80
|
+
"""A role by name, including ones that only exist in `extra`."""
|
|
81
|
+
if role in self._FIELDS:
|
|
82
|
+
return getattr(self, role)
|
|
83
|
+
return self.extra.get(role)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass(frozen=True)
|
|
87
|
+
class ImageSource:
|
|
88
|
+
"""Where the image physically is, for the rare plugin that has to open the
|
|
89
|
+
file itself rather than ask the viewer for tiles.
|
|
90
|
+
|
|
91
|
+
The counterpart of `TableSource`, and it exists for the same kind of reason.
|
|
92
|
+
Figure Builder re-renders a captured panel at publication resolution, which
|
|
93
|
+
means reading a rectangle of source pixels at a chosen pyramid level --
|
|
94
|
+
something no amount of tile-serving API can express, since the tile routes
|
|
95
|
+
answer in the viewer's own quantised, screen-sized terms.
|
|
96
|
+
|
|
97
|
+
Exposed as a typed view rather than by handing over the config entry, so the
|
|
98
|
+
on-disk shape stays core's business. Opening it is the caller's job: doing
|
|
99
|
+
that here would drag tifffile and zarr into every plugin that merely asks
|
|
100
|
+
how big the image is.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
path: str
|
|
104
|
+
kind: str
|
|
105
|
+
#: Pyramid levels the file holds, level 0 being full resolution.
|
|
106
|
+
levels: int | None = None
|
|
107
|
+
size: tuple[int | None, int | None] = (None, None)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class ImageHandle:
|
|
111
|
+
"""The one input every plugin is guaranteed."""
|
|
112
|
+
|
|
113
|
+
def __init__(self, project: Project):
|
|
114
|
+
self._project = project
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def source(self) -> ImageSource | None:
|
|
118
|
+
"""The image file itself. None for a project with no image on disk."""
|
|
119
|
+
spec = self._project.image
|
|
120
|
+
if not spec.src:
|
|
121
|
+
return None
|
|
122
|
+
return ImageSource(
|
|
123
|
+
path=spec.src,
|
|
124
|
+
kind=spec.kind,
|
|
125
|
+
levels=spec.max_level,
|
|
126
|
+
size=(spec.width, spec.height),
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def channels(self) -> list[dict]:
|
|
131
|
+
"""Real image channels, excluding the 'Area' placeholder that only
|
|
132
|
+
exists when segmentation was registered."""
|
|
133
|
+
return list(self._project.image.real_channels)
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def channel_names(self) -> list[str]:
|
|
137
|
+
return self._project.image.channel_names
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def kind(self) -> str | None:
|
|
141
|
+
return self._project.image.kind
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def size(self) -> tuple[int | None, int | None]:
|
|
145
|
+
return self._project.image.width, self._project.image.height
|
|
146
|
+
|
|
147
|
+
@property
|
|
148
|
+
def max_level(self) -> int | None:
|
|
149
|
+
return self._project.image.max_level
|
|
150
|
+
|
|
151
|
+
@property
|
|
152
|
+
def tile_size(self) -> tuple[int | None, int | None]:
|
|
153
|
+
return self._project.image.tile_width, self._project.image.tile_height
|
|
154
|
+
|
|
155
|
+
def stats(self, channel: str) -> dict:
|
|
156
|
+
"""Per-channel intensity statistics, including the vmin/vmax hints the
|
|
157
|
+
viewer uses for immediate display before the full GMM fit lands."""
|
|
158
|
+
return data_model.get_image_channel_stats(channel, self._project.name)
|
|
159
|
+
|
|
160
|
+
def quantization_window(self, channel: str) -> tuple:
|
|
161
|
+
"""(qmin, qmax) from FULL-RESOLUTION data. Deliberately split from the
|
|
162
|
+
GMM fit so callers that only need the byte-domain window do not pay the
|
|
163
|
+
~1 s GaussianMixture cost."""
|
|
164
|
+
return data_model.get_channel_quantization_window(channel, self._project.name)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class SegHandle:
|
|
168
|
+
"""Segmentation mask, when the project has one."""
|
|
169
|
+
|
|
170
|
+
def __init__(self, project: Project):
|
|
171
|
+
self._project = project
|
|
172
|
+
|
|
173
|
+
@property
|
|
174
|
+
def available(self) -> bool:
|
|
175
|
+
return self._project.segmentation.available
|
|
176
|
+
|
|
177
|
+
@property
|
|
178
|
+
def pending(self) -> bool:
|
|
179
|
+
"""True while the background mask-conversion job is still running."""
|
|
180
|
+
return self._project.segmentation.pending
|
|
181
|
+
|
|
182
|
+
def centroid_manifest(self) -> dict:
|
|
183
|
+
return data_model.get_centroid_manifest(self._project.name)
|
|
184
|
+
|
|
185
|
+
def centroid_tiles(self, level, tiles, gates=None, max_points=None):
|
|
186
|
+
return data_model.get_centroid_tiles(self._project.name, level, tiles, gates, max_points)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@dataclass(frozen=True)
|
|
190
|
+
class TableSource:
|
|
191
|
+
"""Where the feature table physically lives, for the rare plugin that has
|
|
192
|
+
to open the file itself rather than read rows through `frame()`.
|
|
193
|
+
|
|
194
|
+
Gating needs this: it writes gate thresholds back into the source AnnData's
|
|
195
|
+
`uns`, which no amount of table-reading API can express. Exposed as a typed
|
|
196
|
+
view rather than by handing over the config entry, so the on-disk shape
|
|
197
|
+
stays core's business.
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
kind: str
|
|
201
|
+
path: str
|
|
202
|
+
table: str | None = None
|
|
203
|
+
subset: Mapping[str, Any] = field(default_factory=dict)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class TableHandle:
|
|
207
|
+
"""The feature table, whatever it was imported from.
|
|
208
|
+
|
|
209
|
+
Every method warms the table first, so callers never reason about load
|
|
210
|
+
order or touch data_model's globals.
|
|
211
|
+
"""
|
|
212
|
+
|
|
213
|
+
def __init__(self, project: Project):
|
|
214
|
+
self._project = project
|
|
215
|
+
|
|
216
|
+
@property
|
|
217
|
+
def available(self) -> bool:
|
|
218
|
+
return self._project.has_table
|
|
219
|
+
|
|
220
|
+
@property
|
|
221
|
+
def source_kind(self) -> str:
|
|
222
|
+
"""'csv', 'anndata' or 'spatialdata'."""
|
|
223
|
+
return self._project.source_kind or "csv"
|
|
224
|
+
|
|
225
|
+
@property
|
|
226
|
+
def source(self) -> TableSource | None:
|
|
227
|
+
spec = self._project.dataset
|
|
228
|
+
if spec is None:
|
|
229
|
+
return None
|
|
230
|
+
return TableSource(
|
|
231
|
+
kind=spec.type,
|
|
232
|
+
path=spec.src,
|
|
233
|
+
table=spec.table,
|
|
234
|
+
subset=dict(spec.subset),
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
@property
|
|
238
|
+
def log_transformed(self) -> bool:
|
|
239
|
+
"""Whether `frame()` hands back log1p'd values.
|
|
240
|
+
|
|
241
|
+
The scale, not a formatting detail. Marker intensities are log-normal,
|
|
242
|
+
so anything that fits a distribution to them has to know which side of
|
|
243
|
+
the transform it is standing on -- fit a mixture to raw counts as if
|
|
244
|
+
they were symmetric and the components land in the wrong places, and
|
|
245
|
+
take the log of values that are already logged and they land in
|
|
246
|
+
different wrong places. Gating's auto-threshold reads this to decide
|
|
247
|
+
which, and gets the same answer out of the same data either way.
|
|
248
|
+
|
|
249
|
+
This is the project's recorded answer (the log1p switch beside the
|
|
250
|
+
matrix picker), which is the only thing that knows: nothing about the
|
|
251
|
+
numbers themselves says whether they have been transformed.
|
|
252
|
+
"""
|
|
253
|
+
return self._project.log_transformed
|
|
254
|
+
|
|
255
|
+
def frame(self):
|
|
256
|
+
"""The whole table as a polars DataFrame (None if this project has no
|
|
257
|
+
feature data)."""
|
|
258
|
+
data_model._ensure_loaded(self._project.name)
|
|
259
|
+
return data_model.get_datasource_df()
|
|
260
|
+
|
|
261
|
+
def describe(self) -> dict:
|
|
262
|
+
"""Per-column summary stats plus a 50-bin histogram. Cached per
|
|
263
|
+
datasource by data_model."""
|
|
264
|
+
return data_model.get_datasource_description(self._project.name)
|
|
265
|
+
|
|
266
|
+
@property
|
|
267
|
+
def markers(self) -> list[str]:
|
|
268
|
+
"""Columns a plugin can meaningfully threshold or plot.
|
|
269
|
+
|
|
270
|
+
This is the classification the project recorded at import -- one
|
|
271
|
+
answer, shared by every plugin, so two tools never disagree about
|
|
272
|
+
whether a column is a marker.
|
|
273
|
+
|
|
274
|
+
A structural channel like DNA is commonly a real image channel with no
|
|
275
|
+
feature column, so this is NOT the same list as image.channel_names --
|
|
276
|
+
conflating the two is a long-standing source of bugs here.
|
|
277
|
+
|
|
278
|
+
The histogram fallback covers a project whose columns were never
|
|
279
|
+
classified: better a usable guess than an empty panel. It costs a
|
|
280
|
+
describe(), which is why it is not the primary path.
|
|
281
|
+
"""
|
|
282
|
+
recorded = self._project.columns
|
|
283
|
+
if recorded.classified:
|
|
284
|
+
return list(recorded.markers)
|
|
285
|
+
reserved = {"id"} | {c for c in self._project.roles.to_dict().values() if c}
|
|
286
|
+
description = self.describe()
|
|
287
|
+
return [
|
|
288
|
+
name for name, info in description.items()
|
|
289
|
+
if name not in reserved and info.get("histogram")
|
|
290
|
+
]
|
|
291
|
+
|
|
292
|
+
@property
|
|
293
|
+
def metadata_columns(self) -> list[str]:
|
|
294
|
+
"""The non-marker columns: identifiers, coordinates, morphology,
|
|
295
|
+
annotations.
|
|
296
|
+
|
|
297
|
+
For a CSV that is the recorded half of the marker/metadata split -- the
|
|
298
|
+
file's columns, minus the ones the user called markers.
|
|
299
|
+
|
|
300
|
+
For AnnData and SpatialData it is the file's own `.obs` columns, which
|
|
301
|
+
is NOT the same list as `columns.metadata` for those formats. That field
|
|
302
|
+
holds whatever the loaded table ended up with, and the two registration
|
|
303
|
+
paths disagree about it: the import route stores the obs names there,
|
|
304
|
+
while `register_anndata_datasource` stores the adapter's synthesized
|
|
305
|
+
`id`/`X`/`Y`/`obs_id`. Neither is wrong for its own purpose, and neither
|
|
306
|
+
is what a plugin is asking for -- "which annotations does this project
|
|
307
|
+
have" has one answer, and for these formats it is obs. Reported through
|
|
308
|
+
the same preference `Project.role_columns` already uses, so the list a
|
|
309
|
+
role is chosen from and the list an annotation is chosen from cannot
|
|
310
|
+
drift apart.
|
|
311
|
+
"""
|
|
312
|
+
spec = self._project.dataset
|
|
313
|
+
if spec is not None and spec.obs_columns:
|
|
314
|
+
return [str(column) for column in spec.obs_columns]
|
|
315
|
+
return list(self._project.columns.metadata)
|
|
316
|
+
|
|
317
|
+
def metadata_values(self, column: str) -> MetadataColumn:
|
|
318
|
+
"""One metadata column's values, aligned row-for-row with `frame()`.
|
|
319
|
+
|
|
320
|
+
The format-agnostic way to read an annotation. `metadata_columns` names
|
|
321
|
+
what a project has; this is how a plugin gets the values, without
|
|
322
|
+
needing to know that a CSV keeps them in the loaded frame while AnnData
|
|
323
|
+
and SpatialData keep them in an `.obs` the frame never materialized.
|
|
324
|
+
Alignment is core's problem, not the caller's: the same subset that
|
|
325
|
+
built the table is applied here.
|
|
326
|
+
|
|
327
|
+
Deliberately not `frame()[column]`. That works for a CSV and returns
|
|
328
|
+
nothing at all for the two structural formats, which is the shape of bug
|
|
329
|
+
that passes every test written against sample CSVs.
|
|
330
|
+
|
|
331
|
+
Raises KeyError if this project has no such column.
|
|
332
|
+
"""
|
|
333
|
+
return data_model.get_metadata_column(self._project.name, column)
|
|
334
|
+
|
|
335
|
+
def columns(self, names) -> dict:
|
|
336
|
+
"""Numeric numpy views of the named columns, cached one set at a time
|
|
337
|
+
so repeated range queries reuse the same arrays."""
|
|
338
|
+
data_model._ensure_loaded(self._project.name)
|
|
339
|
+
return data_model.get_filter_columns(self._project.name, list(names))
|
|
340
|
+
|
|
341
|
+
def range_mask(self, gates: Mapping[str, tuple], mode: str = "and"):
|
|
342
|
+
"""Boolean mask over rows for {column: (low, high)} ranges. `mode` is
|
|
343
|
+
'and' (every gate must match) or 'or' (any)."""
|
|
344
|
+
return data_model.apply_range_mask(self.columns(gates.keys()), gates, mode)
|
|
345
|
+
|
|
346
|
+
def ids_matching(self, gates: Mapping[str, tuple], mode: str = "and") -> list:
|
|
347
|
+
"""Cell ids whose rows satisfy the gates, in table order."""
|
|
348
|
+
frame = self.frame()
|
|
349
|
+
if frame is None or not gates:
|
|
350
|
+
return []
|
|
351
|
+
return frame["id"].to_numpy()[self.range_mask(gates, mode)].tolist()
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
@dataclass(frozen=True)
|
|
355
|
+
class Dataset:
|
|
356
|
+
"""Everything the host offers a plugin about one project."""
|
|
357
|
+
|
|
358
|
+
name: str
|
|
359
|
+
image: ImageHandle
|
|
360
|
+
segmentation: SegHandle
|
|
361
|
+
table: TableHandle
|
|
362
|
+
schema: DatasetSchema | None
|
|
363
|
+
project: Project
|
|
364
|
+
|
|
365
|
+
@property
|
|
366
|
+
def source_kind(self) -> str | None:
|
|
367
|
+
return self.table.source_kind if self.table.available else None
|
|
368
|
+
|
|
369
|
+
def cached(self, key, compute):
|
|
370
|
+
"""Memoize an expensive derived value against this datasource.
|
|
371
|
+
|
|
372
|
+
Entries are dropped when the datasource reloads, so a plugin cannot
|
|
373
|
+
serve a result derived from data that has since changed underneath it.
|
|
374
|
+
Intended for genuinely costly work -- a mixture-model fit, a spatial
|
|
375
|
+
index -- not for ordinary lookups.
|
|
376
|
+
|
|
377
|
+
`key` is namespaced per datasource here, so plugins do not have to
|
|
378
|
+
remember to include the project name and cannot collide across
|
|
379
|
+
projects.
|
|
380
|
+
"""
|
|
381
|
+
return data_model.gmm_cache_get_or_set((self.name, key), compute)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def dataset(name: str) -> Dataset:
|
|
385
|
+
"""Build the handle set for a datasource. Raises KeyError if unknown.
|
|
386
|
+
|
|
387
|
+
Construction is cheap -- it reads the project record only. Nothing is
|
|
388
|
+
loaded from disk until a handle method is actually called.
|
|
389
|
+
"""
|
|
390
|
+
project = Project.load(name)
|
|
391
|
+
return Dataset(
|
|
392
|
+
name=name,
|
|
393
|
+
image=ImageHandle(project),
|
|
394
|
+
segmentation=SegHandle(project),
|
|
395
|
+
table=TableHandle(project),
|
|
396
|
+
schema=DatasetSchema.from_project(project),
|
|
397
|
+
project=project,
|
|
398
|
+
)
|
plexora/api/http.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Response helpers for plugin routes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import orjson
|
|
6
|
+
from flask import Response
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def json_response(data) -> Response:
|
|
10
|
+
"""JSON response that can serialize numpy scalars and arrays.
|
|
11
|
+
|
|
12
|
+
Plugin payloads are typically numpy-derived -- cell ids, per-cell values,
|
|
13
|
+
mask indices -- and Flask's `jsonify` refuses those outright. orjson also
|
|
14
|
+
avoids a Python-level encode of what can be millions of rows.
|
|
15
|
+
"""
|
|
16
|
+
return Response(
|
|
17
|
+
orjson.dumps(data, option=orjson.OPT_SERIALIZE_NUMPY),
|
|
18
|
+
mimetype="application/json",
|
|
19
|
+
)
|