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,409 @@
|
|
|
1
|
+
"""The ROI plugin's HTTP surface.
|
|
2
|
+
|
|
3
|
+
Thin on purpose: parse, hand to the repository, translate the outcome. Every
|
|
4
|
+
rule about what an annotation is lives in `operations`/`schema`/`geometry`, and
|
|
5
|
+
every rule about who may overwrite whom lives in `repository`, so there is one
|
|
6
|
+
place to read each and no route can quietly disagree with another.
|
|
7
|
+
|
|
8
|
+
Status codes carry meaning here, because the client acts on them differently:
|
|
9
|
+
|
|
10
|
+
400 the request is wrong -- a malformed geometry, an unknown category.
|
|
11
|
+
Fix the request; retrying it unchanged will fail the same way.
|
|
12
|
+
409 the request was fine but somebody else wrote first. The client has
|
|
13
|
+
work worth keeping, so this opens the conflict prompt rather than
|
|
14
|
+
discarding either side.
|
|
15
|
+
422 the image underneath these annotations has changed. Nothing is
|
|
16
|
+
written and nothing is drawn until the user decides.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
|
|
21
|
+
from flask import Blueprint, Response, jsonify, request
|
|
22
|
+
|
|
23
|
+
from plexora import api
|
|
24
|
+
from plexora.plugins.roi import VERSION
|
|
25
|
+
from plexora.plugins.roi.server import geojson, schema
|
|
26
|
+
from plexora.plugins.roi.server.repository import (
|
|
27
|
+
ConflictError,
|
|
28
|
+
ImageMismatch,
|
|
29
|
+
ROIRepository,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# template_folder/static_folder make this plugin self-contained: Flask's
|
|
33
|
+
# DispatchingJinjaLoader already searches every blueprint's template folder, and
|
|
34
|
+
# the blueprint serves its own assets.
|
|
35
|
+
roi_bp = Blueprint(
|
|
36
|
+
'roi', __name__,
|
|
37
|
+
template_folder='../templates',
|
|
38
|
+
static_folder='../static',
|
|
39
|
+
static_url_path='/static',
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
#: Biggest request body accepted. A batch of edits is kilobytes; a GeoJSON
|
|
43
|
+
#: import of a few thousand traced contours is megabytes. Past this the request
|
|
44
|
+
#: is a mistake, and the client is told so rather than the server parsing it.
|
|
45
|
+
MAX_BODY_BYTES = 64 * 1024 * 1024
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _payload():
|
|
49
|
+
"""The request's JSON body, or a ValueError worth showing the user."""
|
|
50
|
+
if request.content_length and request.content_length > MAX_BODY_BYTES:
|
|
51
|
+
raise ValueError(f"request body is larger than {MAX_BODY_BYTES // (1024 * 1024)} MB")
|
|
52
|
+
body = request.get_json(silent=True)
|
|
53
|
+
if not isinstance(body, dict):
|
|
54
|
+
raise ValueError("expected a JSON object")
|
|
55
|
+
return body
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _repository(datasource):
|
|
59
|
+
"""A repository for a datasource that exists, or a KeyError."""
|
|
60
|
+
if not datasource:
|
|
61
|
+
raise KeyError(datasource)
|
|
62
|
+
api.dataset(datasource) # raises KeyError for an unknown project
|
|
63
|
+
return ROIRepository(datasource)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _mismatch_response(exc):
|
|
67
|
+
return jsonify(
|
|
68
|
+
success=False,
|
|
69
|
+
error="image_dimensions_changed",
|
|
70
|
+
stored_image_size=exc.stored,
|
|
71
|
+
image_size=exc.current,
|
|
72
|
+
), 422
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _image_id_or_none(datasource):
|
|
76
|
+
"""This project's image id, swallowing the ambiguous case.
|
|
77
|
+
|
|
78
|
+
Used by the two read-only routes -- the export and the panel's opening
|
|
79
|
+
question -- where an unresolvable column must not cost the user their
|
|
80
|
+
download or their panel. `map_to_cells` calls `mapping.current_image_id`
|
|
81
|
+
directly and lets the ValueError through, because there the ambiguity is
|
|
82
|
+
the difference between annotating the right cells and the wrong ones.
|
|
83
|
+
"""
|
|
84
|
+
from plexora.plugins.roi.server import mapping
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
return mapping.current_image_id(api.dataset(datasource))
|
|
88
|
+
except (KeyError, ValueError):
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@roi_bp.route('/api/state', methods=['GET'])
|
|
93
|
+
def get_state():
|
|
94
|
+
"""Everything the panel draws from: categories, this image's regions, the
|
|
95
|
+
revision to write against, and whether the image still matches."""
|
|
96
|
+
try:
|
|
97
|
+
repository = _repository(request.args.get('datasource'))
|
|
98
|
+
except KeyError:
|
|
99
|
+
return jsonify(success=False, error="Unknown datasource"), 400
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
state = repository.load()
|
|
103
|
+
except ValueError as exc:
|
|
104
|
+
return jsonify(success=False, error=str(exc)), 400
|
|
105
|
+
|
|
106
|
+
entry = state["images"].get(schema.DEFAULT_IMAGE) or schema.empty_image()
|
|
107
|
+
return api.json_response({
|
|
108
|
+
"success": True,
|
|
109
|
+
"schema_version": state["schema_version"],
|
|
110
|
+
"revision": state["revision"],
|
|
111
|
+
"categories": state["categories"],
|
|
112
|
+
"image": schema.DEFAULT_IMAGE,
|
|
113
|
+
"coordinate_space": entry["coordinate_space"],
|
|
114
|
+
"features": entry["features"],
|
|
115
|
+
**repository.status(state),
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@roi_bp.route('/api/operations', methods=['POST'])
|
|
120
|
+
def post_operations():
|
|
121
|
+
"""Apply a batch of edits, if the client is writing against the revision it
|
|
122
|
+
last read. The batch is all-or-nothing."""
|
|
123
|
+
try:
|
|
124
|
+
body = _payload()
|
|
125
|
+
repository = _repository(body.get('datasource'))
|
|
126
|
+
except ValueError as exc:
|
|
127
|
+
return jsonify(success=False, error=str(exc)), 400
|
|
128
|
+
except KeyError:
|
|
129
|
+
return jsonify(success=False, error="Unknown datasource"), 400
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
revision = repository.apply(body.get('base_revision'), body.get('operations'))
|
|
133
|
+
except ConflictError as exc:
|
|
134
|
+
return jsonify(success=False, error="stale_revision",
|
|
135
|
+
revision=exc.current_revision), 409
|
|
136
|
+
except ImageMismatch as exc:
|
|
137
|
+
return _mismatch_response(exc)
|
|
138
|
+
except ValueError as exc:
|
|
139
|
+
return jsonify(success=False, error=str(exc)), 400
|
|
140
|
+
|
|
141
|
+
return jsonify(success=True, revision=revision)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@roi_bp.route('/api/export.geojson', methods=['GET'])
|
|
145
|
+
def export_geojson():
|
|
146
|
+
"""The whole annotation project as a file.
|
|
147
|
+
|
|
148
|
+
Deliberately has no preconditions beyond the project existing: export is the
|
|
149
|
+
escape hatch, and the moments a user most needs it -- a failed save, a
|
|
150
|
+
conflict, an image that no longer matches -- are exactly the moments a
|
|
151
|
+
stricter route would refuse.
|
|
152
|
+
"""
|
|
153
|
+
datasource = request.args.get('datasource')
|
|
154
|
+
try:
|
|
155
|
+
repository = _repository(datasource)
|
|
156
|
+
state = repository.load()
|
|
157
|
+
except KeyError:
|
|
158
|
+
return jsonify(success=False, error="Unknown datasource"), 400
|
|
159
|
+
except ValueError as exc:
|
|
160
|
+
return jsonify(success=False, error=str(exc)), 400
|
|
161
|
+
|
|
162
|
+
document = geojson.export_document(state, datasource, VERSION,
|
|
163
|
+
image_id=_image_id_or_none(datasource))
|
|
164
|
+
return Response(
|
|
165
|
+
json.dumps(document, indent=2),
|
|
166
|
+
mimetype=geojson.MIME_TYPE,
|
|
167
|
+
headers={"Content-disposition": f"attachment; filename={datasource}_rois.geojson"},
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@roi_bp.route('/api/import', methods=['POST'])
|
|
172
|
+
def import_geojson():
|
|
173
|
+
"""Add a GeoJSON document's regions to this project.
|
|
174
|
+
|
|
175
|
+
A dimension mismatch comes back as `success: false` with a warning rather
|
|
176
|
+
than an error: it is a question for the user, and the answer is normally no.
|
|
177
|
+
Re-post with `accept_dimension_mismatch` to go ahead anyway.
|
|
178
|
+
"""
|
|
179
|
+
try:
|
|
180
|
+
body = _payload()
|
|
181
|
+
repository = _repository(body.get('datasource'))
|
|
182
|
+
except ValueError as exc:
|
|
183
|
+
return jsonify(success=False, error=str(exc)), 400
|
|
184
|
+
except KeyError:
|
|
185
|
+
return jsonify(success=False, error="Unknown datasource"), 400
|
|
186
|
+
|
|
187
|
+
document = body.get('document')
|
|
188
|
+
width, height = repository.image_size()
|
|
189
|
+
errors, warnings = geojson.validate_document(document, image_size=(width, height))
|
|
190
|
+
if errors:
|
|
191
|
+
return jsonify(success=False, error="; ".join(errors)), 400
|
|
192
|
+
if warnings.get("dimension_mismatch") and not body.get("accept_dimension_mismatch"):
|
|
193
|
+
return jsonify(success=False, warning="dimension_mismatch",
|
|
194
|
+
**warnings["dimension_mismatch"])
|
|
195
|
+
|
|
196
|
+
try:
|
|
197
|
+
state = repository.load()
|
|
198
|
+
operation, report = geojson.import_features(state, document)
|
|
199
|
+
revision = repository.apply(body.get('base_revision'), [operation])
|
|
200
|
+
except ConflictError as exc:
|
|
201
|
+
return jsonify(success=False, error="stale_revision",
|
|
202
|
+
revision=exc.current_revision), 409
|
|
203
|
+
except ImageMismatch as exc:
|
|
204
|
+
return _mismatch_response(exc)
|
|
205
|
+
except ValueError as exc:
|
|
206
|
+
return jsonify(success=False, error=str(exc)), 400
|
|
207
|
+
|
|
208
|
+
return jsonify(success=True, revision=revision, operation=operation, **report)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
# -- native export adapters ---------------------------------------------
|
|
212
|
+
#
|
|
213
|
+
# Everything below writes into the file the project was imported from, which is
|
|
214
|
+
# the user's own data. All of it is explicit, none of it happens on a save, and
|
|
215
|
+
# none of it replaces an existing element without being told to.
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
@roi_bp.route('/api/adapters/destination', methods=['GET'])
|
|
219
|
+
def adapter_destination():
|
|
220
|
+
"""Where this project's annotations can go, and what to call them there.
|
|
221
|
+
|
|
222
|
+
One route because it is one question, asked once when the panel opens:
|
|
223
|
+
which native destination this project has (a CSV has none -- a CSV cannot
|
|
224
|
+
hold a polygon, and the honest companion for one is the GeoJSON sidecar),
|
|
225
|
+
what the entry is called by default, which names are already taken in the
|
|
226
|
+
user's file, and which one this project last wrote to.
|
|
227
|
+
|
|
228
|
+
Answering it in pieces meant two round trips and a `window.prompt` that
|
|
229
|
+
re-asked from scratch every time.
|
|
230
|
+
"""
|
|
231
|
+
from plexora.plugins.roi.server import adapters
|
|
232
|
+
|
|
233
|
+
datasource = request.args.get('datasource')
|
|
234
|
+
try:
|
|
235
|
+
dataset = api.dataset(datasource)
|
|
236
|
+
except KeyError:
|
|
237
|
+
return jsonify(success=False, error="Unknown datasource"), 400
|
|
238
|
+
|
|
239
|
+
kind = dataset.source_kind
|
|
240
|
+
payload = {"success": True, "source_kind": kind, "kind": None,
|
|
241
|
+
"label": "", "default_name": "", "existing": [],
|
|
242
|
+
# Whether there are cells to map regions onto at all. Answered
|
|
243
|
+
# here rather than in a route of its own because the panel needs
|
|
244
|
+
# it at the same moment it needs everything else on this payload,
|
|
245
|
+
# and one question the panel asks once should stay one request.
|
|
246
|
+
"has_table": bool(dataset.table.available),
|
|
247
|
+
"image_id": _image_id_or_none(datasource),
|
|
248
|
+
"remembered": ROIRepository(datasource).destination()}
|
|
249
|
+
|
|
250
|
+
# Listing is best-effort by design: it is a convenience for naming, and a
|
|
251
|
+
# file that cannot be listed right now must not cost the user their panel.
|
|
252
|
+
# The write path checks again, for real, and refuses there.
|
|
253
|
+
if kind == 'anndata':
|
|
254
|
+
payload.update(kind="anndata", label="AnnData (.uns)",
|
|
255
|
+
default_name=adapters.DEFAULT_UNS_KEY,
|
|
256
|
+
existing=adapters.existing_anndata_keys(dataset))
|
|
257
|
+
elif kind == 'spatialdata':
|
|
258
|
+
try:
|
|
259
|
+
existing = adapters.existing_shapes(dataset)
|
|
260
|
+
except ValueError:
|
|
261
|
+
existing = []
|
|
262
|
+
payload.update(kind="spatialdata", label="SpatialData shapes",
|
|
263
|
+
default_name=adapters.DEFAULT_ELEMENT, existing=existing)
|
|
264
|
+
|
|
265
|
+
return jsonify(**payload)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
@roi_bp.route('/api/adapters/anndata', methods=['POST'])
|
|
269
|
+
def save_to_anndata():
|
|
270
|
+
"""Write the annotations into the source .h5ad at uns/plexora/<key>.
|
|
271
|
+
|
|
272
|
+
Only that subtree is rewritten -- never a read-and-write-back of the whole
|
|
273
|
+
file, which would rebuild X and obs and turn an annotation export into a
|
|
274
|
+
rewrite of the user's measurements.
|
|
275
|
+
|
|
276
|
+
An existing key is refused unless `replace` says otherwise, so one file can
|
|
277
|
+
carry several annotation passes and no pass can quietly land on another.
|
|
278
|
+
"""
|
|
279
|
+
from plexora.plugins.roi.server import adapters
|
|
280
|
+
|
|
281
|
+
try:
|
|
282
|
+
body = _payload()
|
|
283
|
+
dataset = api.dataset(body.get('datasource'))
|
|
284
|
+
except ValueError as exc:
|
|
285
|
+
return jsonify(success=False, error=str(exc)), 400
|
|
286
|
+
except KeyError:
|
|
287
|
+
return jsonify(success=False, error="Unknown datasource"), 400
|
|
288
|
+
|
|
289
|
+
repository = ROIRepository(dataset.name)
|
|
290
|
+
try:
|
|
291
|
+
result = adapters.save_to_anndata(
|
|
292
|
+
dataset, repository.load(), VERSION,
|
|
293
|
+
key=body.get('key'), replace=bool(body.get('replace')),
|
|
294
|
+
)
|
|
295
|
+
except adapters.KeyExists as exc:
|
|
296
|
+
return jsonify(success=False, error="key_exists",
|
|
297
|
+
keys=exc.existing, suggestion=exc.suggestion)
|
|
298
|
+
except ValueError as exc:
|
|
299
|
+
return jsonify(success=False, error=str(exc)), 400
|
|
300
|
+
|
|
301
|
+
repository.remember_destination(result["name"])
|
|
302
|
+
return jsonify(success=True, **result)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
@roi_bp.route('/api/adapters/spatialdata', methods=['POST'])
|
|
306
|
+
def save_to_spatialdata():
|
|
307
|
+
"""Write the annotations into the store as a shapes element."""
|
|
308
|
+
from plexora.plugins.roi.server import adapters
|
|
309
|
+
|
|
310
|
+
try:
|
|
311
|
+
body = _payload()
|
|
312
|
+
dataset = api.dataset(body.get('datasource'))
|
|
313
|
+
except ValueError as exc:
|
|
314
|
+
return jsonify(success=False, error=str(exc)), 400
|
|
315
|
+
except KeyError:
|
|
316
|
+
return jsonify(success=False, error="Unknown datasource"), 400
|
|
317
|
+
|
|
318
|
+
# Passed through as given: naming, defaulting and validation are the
|
|
319
|
+
# adapter's, so there is one rule rather than two that can disagree.
|
|
320
|
+
repository = ROIRepository(dataset.name)
|
|
321
|
+
try:
|
|
322
|
+
result = adapters.save_to_spatialdata(
|
|
323
|
+
dataset, repository.load(), body.get('element_name'))
|
|
324
|
+
except adapters.ElementExists as exc:
|
|
325
|
+
# Not an error: naming an element that is already there is an ordinary
|
|
326
|
+
# thing to do, and the answer is a different name -- never a silent
|
|
327
|
+
# overwrite of a layer that may be somebody's segmentation. Unlike the
|
|
328
|
+
# AnnData branch there is no `replace`: spatialdata's own writer refuses
|
|
329
|
+
# it, and delete-then-rewrite has a window where the user has neither.
|
|
330
|
+
return jsonify(success=False, error="element_exists",
|
|
331
|
+
elements=exc.existing, suggestion=exc.suggestion)
|
|
332
|
+
except ValueError as exc:
|
|
333
|
+
return jsonify(success=False, error=str(exc)), 400
|
|
334
|
+
|
|
335
|
+
repository.remember_destination(result["name"])
|
|
336
|
+
return jsonify(success=True, **result)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
@roi_bp.route('/api/map_to_cells', methods=['POST'])
|
|
340
|
+
def map_to_cells():
|
|
341
|
+
"""Annotate this project's cells with the regions they fall inside.
|
|
342
|
+
|
|
343
|
+
The other direction from everything above: those routes write the polygons
|
|
344
|
+
into the user's file, this one writes onto the rows. Two columns --
|
|
345
|
+
`<name>_category` and `<name>_name` -- so the result is something a user can
|
|
346
|
+
group by without parsing anything.
|
|
347
|
+
|
|
348
|
+
Two `needs` replies rather than error text. A cell id is what lines a label
|
|
349
|
+
up with a row, and an image id is what stops one image's regions being
|
|
350
|
+
written onto another's cells; when either is unanswered the client asks for
|
|
351
|
+
it through the requirements modal and retries. The client branches on this
|
|
352
|
+
field, never on the message -- wording is not an API.
|
|
353
|
+
"""
|
|
354
|
+
from plexora.plugins.roi.server import adapters, mapping
|
|
355
|
+
|
|
356
|
+
try:
|
|
357
|
+
body = _payload()
|
|
358
|
+
dataset = api.dataset(body.get('datasource'))
|
|
359
|
+
except ValueError as exc:
|
|
360
|
+
return jsonify(success=False, error=str(exc)), 400
|
|
361
|
+
except KeyError:
|
|
362
|
+
return jsonify(success=False, error="Unknown datasource"), 400
|
|
363
|
+
|
|
364
|
+
if not dataset.table.available:
|
|
365
|
+
return jsonify(success=False,
|
|
366
|
+
error="This project has no cell-level data",
|
|
367
|
+
needs="table"), 400
|
|
368
|
+
|
|
369
|
+
schema_ = dataset.schema
|
|
370
|
+
if schema_ is None or not schema_.cell_id:
|
|
371
|
+
return jsonify(success=False,
|
|
372
|
+
error="No cell ID column recorded for this project",
|
|
373
|
+
needs="role:cell_id"), 400
|
|
374
|
+
|
|
375
|
+
try:
|
|
376
|
+
# Deliberately not swallowed the way the read-only routes swallow it: a
|
|
377
|
+
# column that resolves to several images means Plexora cannot tell whose
|
|
378
|
+
# cells these are, and guessing writes labels onto somebody else's.
|
|
379
|
+
mapping.current_image_id(dataset)
|
|
380
|
+
except ValueError as exc:
|
|
381
|
+
return jsonify(success=False, error=str(exc), needs="role:image_id"), 400
|
|
382
|
+
|
|
383
|
+
repository = ROIRepository(dataset.name)
|
|
384
|
+
state = repository.load()
|
|
385
|
+
entry = state["images"].get(schema.DEFAULT_IMAGE) or schema.empty_image()
|
|
386
|
+
if not entry["features"]:
|
|
387
|
+
return jsonify(success=False, error="There are no ROIs to map"), 400
|
|
388
|
+
|
|
389
|
+
frame = dataset.table.frame()
|
|
390
|
+
# numpy rather than to_list(): a real slide is 10^5-10^6 cells, and this is
|
|
391
|
+
# the array shapely wants anyway. A cell with no coordinates arrives as NaN,
|
|
392
|
+
# which is never inside anything -- so it is simply left unassigned rather
|
|
393
|
+
# than needing a case of its own.
|
|
394
|
+
labels, names = mapping.assign(
|
|
395
|
+
entry["features"], state["categories"],
|
|
396
|
+
frame[schema_.x].to_numpy(), frame[schema_.y].to_numpy())
|
|
397
|
+
|
|
398
|
+
try:
|
|
399
|
+
result = adapters.write_cell_columns(
|
|
400
|
+
dataset, labels, names,
|
|
401
|
+
prefix=body.get('name'), replace=bool(body.get('replace')),
|
|
402
|
+
)
|
|
403
|
+
except adapters.ColumnExists as exc:
|
|
404
|
+
return jsonify(success=False, error="column_exists",
|
|
405
|
+
columns=exc.existing, suggestion=exc.suggestion)
|
|
406
|
+
except ValueError as exc:
|
|
407
|
+
return jsonify(success=False, error=str(exc)), 400
|
|
408
|
+
|
|
409
|
+
return jsonify(success=True, n_rois=len(entry["features"]), **result)
|