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
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
"""Creating a project, and describing what was imported.
|
|
2
|
+
|
|
3
|
+
One entry point. The upload page asks for an image, optionally a segmentation
|
|
4
|
+
mask, and optionally a data file, and nothing else -- the format is detected
|
|
5
|
+
rather than picked from a tab, and everything the old two-step config forms
|
|
6
|
+
demanded up front is either worked out from the file or deferred until some
|
|
7
|
+
feature actually needs it (see plexora/api/plugin.py's Requires).
|
|
8
|
+
|
|
9
|
+
The one thing that cannot be deferred is anything ambiguous about the *file*:
|
|
10
|
+
a .zarr store with several tables, or a table spanning several images, cannot
|
|
11
|
+
be read at all until the user picks one. Those two inputs appear on the form
|
|
12
|
+
only when they apply, which is what /inspect_data is for.
|
|
13
|
+
|
|
14
|
+
After import, a CSV goes to the column-classification screen -- marker versus
|
|
15
|
+
metadata is a property of the dataset, so it is established once and stored
|
|
16
|
+
centrally. AnnData and SpatialData skip it: `var` and `obs` already draw that
|
|
17
|
+
line, so asking would be asking the user to confirm what the file says.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import shutil
|
|
21
|
+
from dataclasses import replace
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
from flask import jsonify, redirect, render_template, request
|
|
25
|
+
|
|
26
|
+
from plexora import app, get_config, get_config_names, paths
|
|
27
|
+
from plexora.datasource import (
|
|
28
|
+
_dedupe_dataset_name,
|
|
29
|
+
_derive_dataset_name_from_path,
|
|
30
|
+
_segmentation_channel_name,
|
|
31
|
+
_segmentation_config_fields,
|
|
32
|
+
_segmentation_spec,
|
|
33
|
+
register_anndata_datasource,
|
|
34
|
+
register_datasource,
|
|
35
|
+
register_image_datasource,
|
|
36
|
+
)
|
|
37
|
+
from plexora.server.models import data_model
|
|
38
|
+
from plexora.server.models.adapters import (
|
|
39
|
+
SUPPORTED_DATA_DESCRIPTION,
|
|
40
|
+
detect_data_type,
|
|
41
|
+
)
|
|
42
|
+
from plexora.server.models.adapters import inspection as data_inspection
|
|
43
|
+
from plexora.server.models.adapters.spatialdata_adapter import list_spatialdata_tables
|
|
44
|
+
from plexora.server.models.project import (
|
|
45
|
+
IMPORT_ROLES,
|
|
46
|
+
ROLE_LABELS,
|
|
47
|
+
ColumnGroups,
|
|
48
|
+
ColumnRoles,
|
|
49
|
+
DataSpec,
|
|
50
|
+
Project,
|
|
51
|
+
)
|
|
52
|
+
from plexora.server.routes.page_routes import template_data
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _base_url():
|
|
56
|
+
return app.config.get('PLEXORA_BASE_URL', '')
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def trim_filepath_quotes(path):
|
|
60
|
+
"""Drag-and-drop from a file manager often brings the quotes with it, and
|
|
61
|
+
on Windows a copied path is quoted as a matter of course."""
|
|
62
|
+
if not path:
|
|
63
|
+
return path
|
|
64
|
+
path = str(path).strip()
|
|
65
|
+
if len(path) >= 2 and path[0] == path[-1] and path[0] in ('"', "'"):
|
|
66
|
+
path = path[1:-1]
|
|
67
|
+
return path.strip()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _resolved(path):
|
|
71
|
+
return Path(trim_filepath_quotes(path)).expanduser() if path else None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# --------------------------------------------------------------------------
|
|
75
|
+
# Inspecting a data file before it is imported
|
|
76
|
+
# --------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
@app.route('/inspect_data', methods=['POST'])
|
|
79
|
+
def inspect_data():
|
|
80
|
+
"""What the upload form needs to know about the path the user just typed.
|
|
81
|
+
|
|
82
|
+
Called as the Data field changes, and again once a table inside a
|
|
83
|
+
multi-table store has been picked. Answers three questions at once -- is
|
|
84
|
+
this readable, what format is it, and is there anything about it that
|
|
85
|
+
cannot be worked out -- so the form reveals a table, matrix or subset
|
|
86
|
+
picker only when the file genuinely forces the choice. One request rather
|
|
87
|
+
than one per format is what lets the page have a single Data input.
|
|
88
|
+
|
|
89
|
+
`table` names which table inside a .zarr store to look at. It is what makes
|
|
90
|
+
the second call possible: everything after the table choice -- which matrix
|
|
91
|
+
holds the intensities, whether the table spans several images -- is a
|
|
92
|
+
question ABOUT a table, and a store with several has no answer to any of it
|
|
93
|
+
until one is named.
|
|
94
|
+
"""
|
|
95
|
+
payload = request.get_json(silent=True) or {}
|
|
96
|
+
path = _resolved(payload.get('path'))
|
|
97
|
+
chosen_table = (payload.get('table') or '').strip() or None
|
|
98
|
+
if not path:
|
|
99
|
+
return jsonify(ok=False, error="No path given"), 400
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
data_type = detect_data_type(path)
|
|
103
|
+
except ValueError as exc:
|
|
104
|
+
return jsonify(ok=False, error=str(exc)), 200
|
|
105
|
+
|
|
106
|
+
result = {"ok": True, "data_type": data_type, "tables": [], "ambiguous": [],
|
|
107
|
+
"layers": []}
|
|
108
|
+
|
|
109
|
+
if data_type == "csv":
|
|
110
|
+
# Nothing about a CSV is ambiguous at this stage: it has one table and
|
|
111
|
+
# its columns get confirmed on the classification screen anyway.
|
|
112
|
+
return jsonify(result)
|
|
113
|
+
|
|
114
|
+
if data_type == "spatialdata":
|
|
115
|
+
try:
|
|
116
|
+
tables = list_spatialdata_tables(path)
|
|
117
|
+
except Exception as exc: # a malformed store, not a bug
|
|
118
|
+
return jsonify(ok=False, error=f"Could not read the store: {exc}"), 200
|
|
119
|
+
if not tables:
|
|
120
|
+
return jsonify(ok=False, error="This .zarr store holds no tables."), 200
|
|
121
|
+
result["tables"] = tables
|
|
122
|
+
names = [t["name"] for t in tables]
|
|
123
|
+
if chosen_table in names:
|
|
124
|
+
table = chosen_table
|
|
125
|
+
elif len(tables) > 1:
|
|
126
|
+
# Nothing else is answerable yet. The form asks again with the
|
|
127
|
+
# table once the user picks one -- which it must, because
|
|
128
|
+
# otherwise the matrix and subset questions are never put, and an
|
|
129
|
+
# import that should have asked "raw counts or log values?"
|
|
130
|
+
# silently reads X.
|
|
131
|
+
return jsonify(result)
|
|
132
|
+
else:
|
|
133
|
+
table = names[0]
|
|
134
|
+
else:
|
|
135
|
+
table = None
|
|
136
|
+
|
|
137
|
+
try:
|
|
138
|
+
inspection = _inspect(path, data_type, table)
|
|
139
|
+
except Exception as exc:
|
|
140
|
+
return jsonify(ok=False, error=f"Could not read the file: {exc}"), 200
|
|
141
|
+
|
|
142
|
+
proposal = data_inspection.propose_read_spec(inspection)
|
|
143
|
+
result["table"] = table
|
|
144
|
+
# A file with extra expression matrices does not say which one holds the
|
|
145
|
+
# values to threshold on -- raw counts and a log-transformed copy live side
|
|
146
|
+
# by side, and picking X for the user silently decides what every marker
|
|
147
|
+
# histogram in the app is a histogram of.
|
|
148
|
+
result["layers"] = list(inspection.get("layers") or [])
|
|
149
|
+
result["ambiguous"] = [
|
|
150
|
+
{"column": column,
|
|
151
|
+
"values": next((c.get("values") or [] for c in inspection["obs_columns"]
|
|
152
|
+
if c.get("name") == column), [])}
|
|
153
|
+
for column in proposal["ambiguous"]
|
|
154
|
+
]
|
|
155
|
+
return jsonify(result)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@app.route('/list_spatialdata_tables', methods=['POST'])
|
|
159
|
+
def list_spatialdata_tables_route():
|
|
160
|
+
"""The tables inside a .zarr store, for the picker that appears when there
|
|
161
|
+
is more than one."""
|
|
162
|
+
payload = request.get_json(silent=True) or {}
|
|
163
|
+
path = _resolved(payload.get('path'))
|
|
164
|
+
if not path or not path.is_dir():
|
|
165
|
+
return jsonify(ok=False, error="Not a .zarr store"), 200
|
|
166
|
+
try:
|
|
167
|
+
return jsonify(ok=True, tables=list_spatialdata_tables(path))
|
|
168
|
+
except Exception as exc:
|
|
169
|
+
return jsonify(ok=False, error=str(exc)), 200
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _inspect(path, data_type, table=None):
|
|
173
|
+
if data_type == "csv":
|
|
174
|
+
return data_inspection.inspect_csv(path)
|
|
175
|
+
if data_type == "spatialdata":
|
|
176
|
+
return data_inspection.inspect_spatialdata_table(path, table)
|
|
177
|
+
return data_inspection.inspect_anndata(path)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
# --------------------------------------------------------------------------
|
|
181
|
+
# Import
|
|
182
|
+
# --------------------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
def _features_layer(value):
|
|
185
|
+
"""Which matrix the marker intensities come from, as the requirements modal
|
|
186
|
+
and the edit page send it -- never the import form, which does not ask.
|
|
187
|
+
|
|
188
|
+
`"X"` is the main matrix and `"layer:<name>"` names one of `adata.layers`.
|
|
189
|
+
Prefixed rather than sent bare so that a layer called "X" -- which anndata
|
|
190
|
+
permits -- cannot be confused with the main matrix. Returns the layer name,
|
|
191
|
+
or None for X and for a caller that did not name one.
|
|
192
|
+
"""
|
|
193
|
+
value = (value or "").strip()
|
|
194
|
+
return value.split(":", 1)[1] or None if value.startswith("layer:") else None
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _fail(message, form=None):
|
|
198
|
+
"""Re-render the upload page with an error and whatever the user typed, so
|
|
199
|
+
a rejected import does not throw the paths away."""
|
|
200
|
+
return render_template(
|
|
201
|
+
"upload.html",
|
|
202
|
+
data=template_data(error=message, **(form or {})),
|
|
203
|
+
), 400
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@app.route('/import', methods=['POST'])
|
|
207
|
+
def import_project():
|
|
208
|
+
"""Create a project from an image, an optional mask and optional data."""
|
|
209
|
+
form = request.form
|
|
210
|
+
name = (form.get('name') or '').strip()
|
|
211
|
+
image_path = _resolved(form.get('image_file'))
|
|
212
|
+
mask_path = _resolved(form.get('label_file'))
|
|
213
|
+
data_file = _resolved(form.get('data_file'))
|
|
214
|
+
table = (form.get('data_table') or '').strip() or None
|
|
215
|
+
subset_column = (form.get('subset_column') or '').strip() or None
|
|
216
|
+
subset_value = (form.get('subset_value') or '').strip() or None
|
|
217
|
+
keep = {"form_name": name, "form_image": form.get('image_file'),
|
|
218
|
+
"form_mask": form.get('label_file'), "form_data": form.get('data_file')}
|
|
219
|
+
|
|
220
|
+
if not image_path or not image_path.exists():
|
|
221
|
+
return _fail("Provide a valid path to the image file.", keep)
|
|
222
|
+
if not name:
|
|
223
|
+
name = _dedupe_dataset_name(_derive_dataset_name_from_path(image_path),
|
|
224
|
+
get_config_names())
|
|
225
|
+
if name in get_config():
|
|
226
|
+
return _fail(f"A project named {name!r} already exists.", keep)
|
|
227
|
+
if mask_path and not mask_path.exists():
|
|
228
|
+
return _fail("Provide a valid path to the segmentation mask.", keep)
|
|
229
|
+
|
|
230
|
+
if not data_file:
|
|
231
|
+
# Image only, plus a mask if one was given. This is a complete project:
|
|
232
|
+
# everything else is something a feature will ask for later.
|
|
233
|
+
try:
|
|
234
|
+
_register_image_only(name, image_path, mask_path)
|
|
235
|
+
except Exception as exc:
|
|
236
|
+
return _fail(f"Could not register the image: {exc}", keep)
|
|
237
|
+
return redirect(f"{_base_url()}/{name}")
|
|
238
|
+
|
|
239
|
+
if not data_file.exists():
|
|
240
|
+
return _fail(f"No such data file: {data_file}", keep)
|
|
241
|
+
try:
|
|
242
|
+
data_type = detect_data_type(data_file)
|
|
243
|
+
except ValueError as exc:
|
|
244
|
+
return _fail(str(exc), keep)
|
|
245
|
+
|
|
246
|
+
try:
|
|
247
|
+
if data_type == "csv":
|
|
248
|
+
_register_csv(name, image_path, mask_path, data_file)
|
|
249
|
+
# The one screen that survives from the old two-step import, and
|
|
250
|
+
# the only one: which columns are markers is a fact about the data
|
|
251
|
+
# that every plugin then reads, so it is worth one confirmation.
|
|
252
|
+
return redirect(f"{_base_url()}/project/{name}/columns")
|
|
253
|
+
# No features_layer: an import lands on X, and which matrix to read is
|
|
254
|
+
# asked by the first plugin that reads intensities (Requires(features=
|
|
255
|
+
# True)), where it belongs. The layer names are recorded here either
|
|
256
|
+
# way, so the modal can offer them without reopening the file.
|
|
257
|
+
_register_anndata(name, image_path, mask_path, data_file, data_type,
|
|
258
|
+
table, subset_column, subset_value)
|
|
259
|
+
except ValueError as exc:
|
|
260
|
+
return _fail(str(exc), keep)
|
|
261
|
+
except Exception as exc:
|
|
262
|
+
return _fail(f"Could not import {data_file.name}: {exc}", keep)
|
|
263
|
+
|
|
264
|
+
return redirect(f"{_base_url()}/{name}")
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _register_image_only(name, image_path, mask_path):
|
|
268
|
+
register_image_datasource(name=name, image=image_path)
|
|
269
|
+
if mask_path:
|
|
270
|
+
attach_segmentation(name, mask_path)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _register_csv(name, image_path, mask_path, csv_path):
|
|
274
|
+
"""Register a CSV project, copying the table into the project directory."""
|
|
275
|
+
local_csv = _copy_into_project(name, csv_path)
|
|
276
|
+
inspection = data_inspection.inspect_csv(local_csv)
|
|
277
|
+
roles = inspection["roles"]
|
|
278
|
+
register_datasource(
|
|
279
|
+
name=name,
|
|
280
|
+
image=image_path,
|
|
281
|
+
features=local_csv,
|
|
282
|
+
# The predictor's guesses, which the classification screen confirms
|
|
283
|
+
# next. register_datasource validates that they exist.
|
|
284
|
+
x=roles.get("x"),
|
|
285
|
+
y=roles.get("y"),
|
|
286
|
+
id_column=roles.get("cell_id"),
|
|
287
|
+
celltype_column=roles.get("celltype"),
|
|
288
|
+
segmentation=mask_path,
|
|
289
|
+
segmentation_async=bool(mask_path),
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _register_anndata(name, image_path, mask_path, features_path, data_type,
|
|
294
|
+
table, subset_column, subset_value):
|
|
295
|
+
"""Register an .h5ad or one table of a .zarr store, reading `X`.
|
|
296
|
+
|
|
297
|
+
Which matrix to read is deliberately not a parameter. It only matters to a
|
|
298
|
+
plugin that reads marker intensities, so it is that plugin's requirement
|
|
299
|
+
(`Requires(features=True)`) and the modal collects it -- along with whether
|
|
300
|
+
to log-transform -- the first time such a tool opens. Importing is not the
|
|
301
|
+
moment to answer a question about thresholding, and this path asks only what
|
|
302
|
+
registering the project cannot proceed without. The layer names are recorded
|
|
303
|
+
from the inspection below either way, so the modal can offer them without
|
|
304
|
+
reopening the file.
|
|
305
|
+
"""
|
|
306
|
+
if data_type == "spatialdata" and not table:
|
|
307
|
+
tables = list_spatialdata_tables(features_path)
|
|
308
|
+
if len(tables) != 1:
|
|
309
|
+
raise ValueError(
|
|
310
|
+
"This .zarr store holds several tables -- choose which one to load."
|
|
311
|
+
)
|
|
312
|
+
table = tables[0]["name"]
|
|
313
|
+
|
|
314
|
+
inspection = _inspect(features_path, data_type, table)
|
|
315
|
+
proposal = data_inspection.propose_read_spec(inspection)
|
|
316
|
+
if proposal["ambiguous"] and not subset_column:
|
|
317
|
+
raise ValueError(
|
|
318
|
+
f"{features_path.name} spans several images "
|
|
319
|
+
f"(column {proposal['ambiguous'][0]!r}) -- choose which one to load."
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
coordinates = proposal["coordinates"]
|
|
323
|
+
register_anndata_datasource(
|
|
324
|
+
name=name,
|
|
325
|
+
image=image_path,
|
|
326
|
+
features=features_path,
|
|
327
|
+
table=table,
|
|
328
|
+
segmentation=mask_path,
|
|
329
|
+
segmentation_async=bool(mask_path),
|
|
330
|
+
coordinate_source=coordinates.get("source"),
|
|
331
|
+
obsm_key=coordinates.get("obsm_key"),
|
|
332
|
+
x=coordinates.get("x_column"),
|
|
333
|
+
y=coordinates.get("y_column"),
|
|
334
|
+
feature_source="X",
|
|
335
|
+
subset_by=subset_column,
|
|
336
|
+
subset_value=subset_value,
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def attach_segmentation(name, mask_path, mode=None):
|
|
341
|
+
"""Point a project at a segmentation mask and start its conversion.
|
|
342
|
+
|
|
343
|
+
Shared by import, the edit page and the requirements modal, because a mask
|
|
344
|
+
arrives by all three routes and the work is identical: fingerprint the
|
|
345
|
+
source, record it, then let the background job derive the pyramid and patch
|
|
346
|
+
the project when it lands.
|
|
347
|
+
"""
|
|
348
|
+
from plexora.server.utils import segmentation_pyramid
|
|
349
|
+
|
|
350
|
+
dataset_dir = paths.derived_root(name)
|
|
351
|
+
dataset_dir.mkdir(parents=True, exist_ok=True)
|
|
352
|
+
fields, pending = _segmentation_config_fields(
|
|
353
|
+
Path(mask_path) if mask_path else None, dataset_dir,
|
|
354
|
+
segmentation_async=bool(mask_path), segmentation_mode=mode,
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
def _apply(project):
|
|
358
|
+
image = project.image
|
|
359
|
+
channels = list(image.channels)
|
|
360
|
+
has_area = any(c.get("fullname") == "Area" for c in channels)
|
|
361
|
+
if mask_path and not has_area:
|
|
362
|
+
# The viewer expects the mask layer first in imageData.
|
|
363
|
+
label_name = _segmentation_channel_name(mask_path)
|
|
364
|
+
channels.insert(0, {"name": "Area", "fullname": "Area",
|
|
365
|
+
"src": f"/generated/data/{name}/{label_name}/"})
|
|
366
|
+
elif not mask_path and has_area:
|
|
367
|
+
channels = [c for c in channels if c.get("fullname") != "Area"]
|
|
368
|
+
return project.patch(
|
|
369
|
+
image=replace(image, channels=tuple(channels)),
|
|
370
|
+
segmentation=_segmentation_spec(fields),
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
updated = Project.mutate(name, _apply)
|
|
374
|
+
if updated is not None and pending:
|
|
375
|
+
data_model.start_segmentation_job(
|
|
376
|
+
name, pending, dataset_dir, fields["segmentationMode"]
|
|
377
|
+
)
|
|
378
|
+
return updated
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def replace_project_data(name, data_path_str, payload=None):
|
|
382
|
+
"""Attach a feature table to a project, or swap the one it has.
|
|
383
|
+
|
|
384
|
+
Used by the edit page and by the requirements modal, so a project that
|
|
385
|
+
started as an image only becomes a full one by the same route an existing
|
|
386
|
+
project changes its data file by. Re-detects the format rather than
|
|
387
|
+
assuming the new file matches the old: swapping a CSV for an .h5ad is an
|
|
388
|
+
ordinary thing to want, and it is what the old edit path silently
|
|
389
|
+
corrupted.
|
|
390
|
+
|
|
391
|
+
Roles that still name a real column survive the swap; the rest are cleared,
|
|
392
|
+
because a role pointing at a column that no longer exists is worse than an
|
|
393
|
+
unanswered one -- whatever needs it will ask again.
|
|
394
|
+
"""
|
|
395
|
+
payload = payload or {}
|
|
396
|
+
project = Project.find(name)
|
|
397
|
+
if project is None:
|
|
398
|
+
raise ValueError(f"Unknown project: {name!r}")
|
|
399
|
+
|
|
400
|
+
if not data_path_str:
|
|
401
|
+
# Clearing the data file. The image and mask stay; every table-derived
|
|
402
|
+
# fact goes, since none of it describes anything any more.
|
|
403
|
+
return Project.mutate(
|
|
404
|
+
name, lambda p: p.patch(dataset=None).forget_table_answers())
|
|
405
|
+
|
|
406
|
+
source = Path(data_path_str).expanduser()
|
|
407
|
+
if not source.exists():
|
|
408
|
+
raise ValueError(f"No such data file: {source}")
|
|
409
|
+
data_type = detect_data_type(source)
|
|
410
|
+
|
|
411
|
+
table = (payload.get("table") or "").strip() or None
|
|
412
|
+
if data_type == "spatialdata" and not table:
|
|
413
|
+
tables = list_spatialdata_tables(source)
|
|
414
|
+
if len(tables) != 1:
|
|
415
|
+
raise ValueError(
|
|
416
|
+
"This .zarr store holds several tables -- choose which one to load."
|
|
417
|
+
)
|
|
418
|
+
table = tables[0]["name"]
|
|
419
|
+
|
|
420
|
+
inspection = _inspect(source, data_type, table)
|
|
421
|
+
# Both branches produce markers/metadata/roles: inspect_csv classifies the
|
|
422
|
+
# header directly, propose_read_spec folds classify_from_inspection into the
|
|
423
|
+
# read spec. Read the classification off `proposal` in both -- the raw
|
|
424
|
+
# AnnData inspection has var_names/obs_columns and no such keys.
|
|
425
|
+
proposal = (inspection if data_type == "csv"
|
|
426
|
+
else data_inspection.propose_read_spec(inspection))
|
|
427
|
+
|
|
428
|
+
if data_type == "csv":
|
|
429
|
+
source = _copy_into_project(name, source)
|
|
430
|
+
spec_kwargs = {"coordinates": {}, "features": {}, "obs_id_field": None,
|
|
431
|
+
"obs_columns": (), "layers": ()}
|
|
432
|
+
else:
|
|
433
|
+
subset_column = (payload.get("subset_column") or "").strip() or None
|
|
434
|
+
if proposal["ambiguous"] and not subset_column:
|
|
435
|
+
raise ValueError(
|
|
436
|
+
f"{source.name} spans several images "
|
|
437
|
+
f"(column {proposal['ambiguous'][0]!r}) -- choose which one to load."
|
|
438
|
+
)
|
|
439
|
+
layer = _features_layer(payload.get("features_layer"))
|
|
440
|
+
if layer and layer not in (inspection.get("layers") or []):
|
|
441
|
+
raise ValueError(f"{source.name} has no layer named {layer!r}.")
|
|
442
|
+
spec_kwargs = {
|
|
443
|
+
"coordinates": proposal["coordinates"],
|
|
444
|
+
"features": ({"source": "layer", "layer": layer} if layer
|
|
445
|
+
else proposal["features"]),
|
|
446
|
+
"obs_id_field": None,
|
|
447
|
+
"subset": ({"column": subset_column,
|
|
448
|
+
"value": (payload.get("subset_value") or "").strip()}
|
|
449
|
+
if subset_column else {}),
|
|
450
|
+
# The file's own annotation columns, which is what the role
|
|
451
|
+
# questions are asked about for these formats -- unlike `metadata`
|
|
452
|
+
# below, which is what the adapter's table ends up holding.
|
|
453
|
+
"obs_columns": tuple(
|
|
454
|
+
str(c["name"]) for c in (inspection.get("obs_columns") or ())
|
|
455
|
+
if c.get("name")
|
|
456
|
+
),
|
|
457
|
+
"layers": tuple(str(name) for name in (inspection.get("layers") or ())),
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
known = set(proposal["markers"]) | set(proposal["metadata"])
|
|
461
|
+
if data_type != "csv":
|
|
462
|
+
# The adapter synthesizes these regardless of what the file calls them.
|
|
463
|
+
known |= {"id", "X", "Y"}
|
|
464
|
+
|
|
465
|
+
def _apply(current):
|
|
466
|
+
kept = {role: column for role, column in current.roles.to_dict().items()
|
|
467
|
+
if column in known}
|
|
468
|
+
roles = ColumnRoles(**{**proposal["roles"], **kept})
|
|
469
|
+
if data_type != "csv":
|
|
470
|
+
roles = replace(roles, x="X", y="Y", cell_id="id")
|
|
471
|
+
# Whatever the user confirmed about the old table described columns
|
|
472
|
+
# that may not exist in this one, so those answers are dropped and the
|
|
473
|
+
# fresh predictions go back in front of them. The mask and the cell
|
|
474
|
+
# layer are unaffected -- neither is a fact about the table.
|
|
475
|
+
current = current.forget_table_answers()
|
|
476
|
+
return current.patch(dataset=DataSpec(
|
|
477
|
+
type=data_type,
|
|
478
|
+
src=str(source),
|
|
479
|
+
table=table,
|
|
480
|
+
roles=roles,
|
|
481
|
+
columns=ColumnGroups(markers=tuple(proposal["markers"]),
|
|
482
|
+
metadata=tuple(proposal["metadata"])),
|
|
483
|
+
**spec_kwargs,
|
|
484
|
+
))
|
|
485
|
+
|
|
486
|
+
return Project.mutate(name, _apply)
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def _copy_into_project(name, csv_path):
|
|
490
|
+
"""A quantification CSV is small next to the image, and a project that
|
|
491
|
+
keeps working after the user tidies their downloads folder is worth the
|
|
492
|
+
disk. AnnData and SpatialData are referenced in place -- those are not
|
|
493
|
+
small."""
|
|
494
|
+
dataset_dir = paths.project_state_dir(name)
|
|
495
|
+
dataset_dir.mkdir(parents=True, exist_ok=True)
|
|
496
|
+
local = dataset_dir / csv_path.name
|
|
497
|
+
if csv_path.resolve() != local.resolve():
|
|
498
|
+
shutil.copy2(csv_path, local)
|
|
499
|
+
return local
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
# --------------------------------------------------------------------------
|
|
503
|
+
# Column classification -- marker versus metadata
|
|
504
|
+
# --------------------------------------------------------------------------
|
|
505
|
+
|
|
506
|
+
@app.route('/project/<string:name>/columns', methods=['GET'])
|
|
507
|
+
def project_columns_page(name):
|
|
508
|
+
"""Confirm which columns are markers and which are measurements.
|
|
509
|
+
|
|
510
|
+
Only reachable for CSV: the other formats already carry the distinction in
|
|
511
|
+
their own structure. The prediction is made server-side by the same
|
|
512
|
+
classifier every other path uses, so what the user is correcting is the
|
|
513
|
+
same guess the rest of the app would have made.
|
|
514
|
+
"""
|
|
515
|
+
project = Project.find(name)
|
|
516
|
+
if project is None or not project.has_table:
|
|
517
|
+
return redirect(f"{_base_url()}/open_project")
|
|
518
|
+
|
|
519
|
+
columns = project.columns
|
|
520
|
+
if not columns.all:
|
|
521
|
+
inspection = _inspect(Path(project.dataset.src), project.dataset.type,
|
|
522
|
+
project.dataset.table)
|
|
523
|
+
markers, metadata = inspection["markers"], inspection["metadata"]
|
|
524
|
+
else:
|
|
525
|
+
markers, metadata = list(columns.markers), list(columns.metadata)
|
|
526
|
+
|
|
527
|
+
return render_template("project_columns.html", data=template_data(
|
|
528
|
+
datasetName=name,
|
|
529
|
+
markers=markers,
|
|
530
|
+
metadata=metadata,
|
|
531
|
+
roles=project.roles.to_dict(),
|
|
532
|
+
# The labels come from the server so the wording is identical here, in
|
|
533
|
+
# the requirements modal, and on the edit page. Narrowed to the roles
|
|
534
|
+
# this screen is a checkpoint for: the classifier renders one select
|
|
535
|
+
# per label it is handed, and a cell-type column is not something core
|
|
536
|
+
# needs to read the table -- see IMPORT_ROLES.
|
|
537
|
+
roleLabels={role: ROLE_LABELS[role] for role in IMPORT_ROLES},
|
|
538
|
+
segmentation_pending=project.segmentation.pending,
|
|
539
|
+
))
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
@app.route('/project/<string:name>/columns', methods=['POST'])
|
|
543
|
+
def save_project_columns(name):
|
|
544
|
+
"""Store the confirmed split centrally, where every plugin reads it."""
|
|
545
|
+
payload = request.get_json(silent=True) or {}
|
|
546
|
+
updated = Project.mutate(name, lambda p: _apply_columns(p, payload))
|
|
547
|
+
if updated is None:
|
|
548
|
+
return jsonify(success=False, error="Unknown project"), 404
|
|
549
|
+
|
|
550
|
+
data_model.load_datasource(name, reload=True)
|
|
551
|
+
return jsonify(success=True, segmentation_pending=updated.segmentation.pending)
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def _apply_columns(project, payload):
|
|
555
|
+
project = project.with_columns(payload.get("markers") or [],
|
|
556
|
+
payload.get("metadata") or [])
|
|
557
|
+
# Narrowed to what the screen actually puts on it (see IMPORT_ROLES). A
|
|
558
|
+
# role it does not draw a select for is a role the user was never shown,
|
|
559
|
+
# and both of the writes below would be wrong for one: applying it stores
|
|
560
|
+
# an answer nobody gave, and confirming it retires the question for good.
|
|
561
|
+
roles = {role: column for role, column in (payload.get("roles") or {}).items()
|
|
562
|
+
if role in IMPORT_ROLES}
|
|
563
|
+
project = project.with_roles(roles)
|
|
564
|
+
# This screen IS the confirmation for everything it shows, so a plugin
|
|
565
|
+
# opened afterwards must not put the same split and the same role selects
|
|
566
|
+
# in front of the user a second time. Only the roles that came back with a
|
|
567
|
+
# column are confirmed: a select left on "Choose a column..." was not
|
|
568
|
+
# answered, and something may still need it.
|
|
569
|
+
return project.with_confirmed(
|
|
570
|
+
["markers"] + [f"role:{role}" for role, column in roles.items() if column])
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
# --------------------------------------------------------------------------
|
|
574
|
+
# Small checks the upload form makes as the user types
|
|
575
|
+
# --------------------------------------------------------------------------
|
|
576
|
+
|
|
577
|
+
@app.route('/check_file_existence', methods=['POST'])
|
|
578
|
+
def check_file_existence():
|
|
579
|
+
payload = request.get_json(silent=True) or {}
|
|
580
|
+
path = _resolved(payload.get('path'))
|
|
581
|
+
return jsonify(exists=bool(path and path.is_file()))
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
@app.route('/check_path_existence', methods=['POST'])
|
|
585
|
+
def check_path_existence():
|
|
586
|
+
"""Exists as either a file or a directory -- a .zarr store is a directory,
|
|
587
|
+
and the single Data input accepts both."""
|
|
588
|
+
payload = request.get_json(silent=True) or {}
|
|
589
|
+
path = _resolved(payload.get('path'))
|
|
590
|
+
return jsonify(exists=bool(path and path.exists()))
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
@app.route('/dataset_existence', methods=['POST'])
|
|
594
|
+
def check_dataset_exists():
|
|
595
|
+
payload = request.get_json(silent=True) or {}
|
|
596
|
+
return jsonify(exists=(payload.get('datasetName') in get_config()))
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
@app.route('/supported_data_formats')
|
|
600
|
+
def supported_data_formats():
|
|
601
|
+
"""So the form's help text and the server's rejection message cannot
|
|
602
|
+
disagree about what the Data input takes."""
|
|
603
|
+
return jsonify(description=SUPPORTED_DATA_DESCRIPTION)
|