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.
Files changed (248) hide show
  1. plexora/__init__.py +129 -0
  2. plexora/__main__.py +15 -0
  3. plexora/_url.py +105 -0
  4. plexora/api/__init__.py +55 -0
  5. plexora/api/dataset.py +398 -0
  6. plexora/api/http.py +19 -0
  7. plexora/api/plugin.py +541 -0
  8. plexora/api/store.py +221 -0
  9. plexora/cli.py +777 -0
  10. plexora/client/dist/354_bundle.js +2 -0
  11. plexora/client/dist/354_bundle.js.LICENSE.txt +20 -0
  12. plexora/client/dist/418_bundle.js +2 -0
  13. plexora/client/dist/418_bundle.js.LICENSE.txt +1 -0
  14. plexora/client/dist/693b935cb1f907814be9c6199a68c217.svg +19 -0
  15. plexora/client/dist/713c134ff47cd328a5b711526f151082.svg +18 -0
  16. plexora/client/dist/770_bundle.js +1 -0
  17. plexora/client/dist/ff743f408972e0e96ca9153ed5ae4c83.svg +23 -0
  18. plexora/client/dist/vendor_bundle.js +2 -0
  19. plexora/client/dist/vendor_bundle.js.LICENSE.txt +308 -0
  20. plexora/client/external/openseadragon-bin-2.4.0/LICENSE.txt +28 -0
  21. plexora/client/external/openseadragon-bin-2.4.0/canvas-overlay-hd.js +140 -0
  22. plexora/client/external/openseadragon-bin-2.4.0/changelog.txt +501 -0
  23. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/.gitattributes +17 -0
  24. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/LICENSE.txt +116 -0
  25. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/Toolbar.png +0 -0
  26. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/fullpage.psd +0 -0
  27. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/home.psd +0 -0
  28. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/next.psd +0 -0
  29. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/previous.psd +0 -0
  30. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/rotateleft.psd +0 -0
  31. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/rotateright.psd +0 -0
  32. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/zoom.psd +0 -0
  33. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/README.md +9 -0
  34. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_grouphover.png +0 -0
  35. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_hover.png +0 -0
  36. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_pressed.png +0 -0
  37. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_rest.png +0 -0
  38. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_grouphover.png +0 -0
  39. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_hover.png +0 -0
  40. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_pressed.png +0 -0
  41. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_rest.png +0 -0
  42. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_grouphover.png +0 -0
  43. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_hover.png +0 -0
  44. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_pressed.png +0 -0
  45. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_rest.png +0 -0
  46. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_grouphover.png +0 -0
  47. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_hover.png +0 -0
  48. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_pressed.png +0 -0
  49. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_rest.png +0 -0
  50. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_grouphover.png +0 -0
  51. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_hover.png +0 -0
  52. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_pressed.png +0 -0
  53. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_rest.png +0 -0
  54. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_grouphover.png +0 -0
  55. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_hover.png +0 -0
  56. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_pressed.png +0 -0
  57. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_rest.png +0 -0
  58. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_grouphover.png +0 -0
  59. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_hover.png +0 -0
  60. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_pressed.png +0 -0
  61. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_rest.png +0 -0
  62. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_grouphover.png +0 -0
  63. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_hover.png +0 -0
  64. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_pressed.png +0 -0
  65. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_rest.png +0 -0
  66. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-scalebar.js +562 -0
  67. plexora/client/src/css/import.css +474 -0
  68. plexora/client/src/css/main.css +903 -0
  69. plexora/client/src/css/openProject.css +363 -0
  70. plexora/client/src/css/quickView.css +166 -0
  71. plexora/client/src/css/tokens.css +51 -0
  72. plexora/client/src/css/viewer.css +1655 -0
  73. plexora/client/src/img/apple-touch-icon.png +0 -0
  74. plexora/client/src/img/favicon.ico +0 -0
  75. plexora/client/src/img/logo.ai +8892 -33
  76. plexora/client/src/img/logo.svg +1 -0
  77. plexora/client/src/img/logo_with_text.ai +9011 -34
  78. plexora/client/src/img/logo_with_text.svg +1 -0
  79. plexora/client/src/js/main.js +704 -0
  80. plexora/client/src/js/pluginRegistry.js +108 -0
  81. plexora/client/src/js/services/appStatus.js +286 -0
  82. plexora/client/src/js/services/browsePicker.js +73 -0
  83. plexora/client/src/js/services/dataLayer.js +400 -0
  84. plexora/client/src/js/services/datasetContext.js +143 -0
  85. plexora/client/src/js/services/glRenderer.js +143 -0
  86. plexora/client/src/js/services/importFormValidation.js +257 -0
  87. plexora/client/src/js/services/numericData.js +100 -0
  88. plexora/client/src/js/services/passVariablesToFrontend.js +20 -0
  89. plexora/client/src/js/services/simpleEventHandler.js +25 -0
  90. plexora/client/src/js/vendor.js +29 -0
  91. plexora/client/src/js/views/channelList.js +663 -0
  92. plexora/client/src/js/views/colorSwatchPicker.js +202 -0
  93. plexora/client/src/js/views/columnClassifier.js +146 -0
  94. plexora/client/src/js/views/coordinateField.js +133 -0
  95. plexora/client/src/js/views/dataSourceField.js +276 -0
  96. plexora/client/src/js/views/imageViewer.js +3306 -0
  97. plexora/client/src/js/views/miniMap.js +787 -0
  98. plexora/client/src/js/views/navbarControls.js +178 -0
  99. plexora/client/src/js/views/openProjectPage.js +239 -0
  100. plexora/client/src/js/views/projectEdit.js +335 -0
  101. plexora/client/src/js/views/quickViewLanding.js +136 -0
  102. plexora/client/src/js/views/rainbow.js +326 -0
  103. plexora/client/src/js/views/requirementsModal.js +572 -0
  104. plexora/client/src/js/views/rgbImageViewer.js +115 -0
  105. plexora/client/src/js/views/searchableSelect.js +411 -0
  106. plexora/client/src/js/views/segmentationProgress.js +122 -0
  107. plexora/client/src/js/views/toolLoader.js +747 -0
  108. plexora/client/src/js/views/viewerControls.js +689 -0
  109. plexora/client/src/js/views/viewerManager.js +434 -0
  110. plexora/client/src/js/views/viewerSidebar.js +1117 -0
  111. plexora/client/src/js/workers/tileDecoder.js +130 -0
  112. plexora/client/src/shaders/frag.glsl +528 -0
  113. plexora/client/src/shaders/vert.glsl +12 -0
  114. plexora/client/templates/base.html +212 -0
  115. plexora/client/templates/index.html +238 -0
  116. plexora/client/templates/open_project.html +96 -0
  117. plexora/client/templates/project_columns.html +86 -0
  118. plexora/client/templates/project_edit.html +196 -0
  119. plexora/client/templates/upload.html +118 -0
  120. plexora/connect.py +528 -0
  121. plexora/datasource.py +782 -0
  122. plexora/jupyter.py +406 -0
  123. plexora/notebook_env.py +232 -0
  124. plexora/paths.py +486 -0
  125. plexora/plugins/__init__.py +12 -0
  126. plexora/plugins/cell_explorer/__init__.py +100 -0
  127. plexora/plugins/cell_explorer/server/__init__.py +11 -0
  128. plexora/plugins/cell_explorer/server/routes.py +167 -0
  129. plexora/plugins/cell_explorer/server/state.py +281 -0
  130. plexora/plugins/cell_explorer/server/values.py +145 -0
  131. plexora/plugins/cell_explorer/server/variables.py +367 -0
  132. plexora/plugins/cell_explorer/static/cellExplorerApi.js +158 -0
  133. plexora/plugins/cell_explorer/static/cellExplorerColors.js +316 -0
  134. plexora/plugins/cell_explorer/static/cellExplorerContinuous.js +403 -0
  135. plexora/plugins/cell_explorer/static/cellExplorerFigureBridge.js +133 -0
  136. plexora/plugins/cell_explorer/static/cellExplorerLegend.js +188 -0
  137. plexora/plugins/cell_explorer/static/cellExplorerRoiBridge.js +822 -0
  138. plexora/plugins/cell_explorer/static/cellExplorerSidebarController.js +826 -0
  139. plexora/plugins/cell_explorer/static/cellExplorerState.js +372 -0
  140. plexora/plugins/cell_explorer/static/cell_explorer.css +752 -0
  141. plexora/plugins/cell_explorer/templates/cell_explorer/panel.html +137 -0
  142. plexora/plugins/figure_builder/__init__.py +125 -0
  143. plexora/plugins/figure_builder/server/__init__.py +7 -0
  144. plexora/plugins/figure_builder/server/compose.py +365 -0
  145. plexora/plugins/figure_builder/server/export.py +784 -0
  146. plexora/plugins/figure_builder/server/export_jobs.py +148 -0
  147. plexora/plugins/figure_builder/server/operations.py +556 -0
  148. plexora/plugins/figure_builder/server/pixels.py +142 -0
  149. plexora/plugins/figure_builder/server/provenance.py +174 -0
  150. plexora/plugins/figure_builder/server/render.py +278 -0
  151. plexora/plugins/figure_builder/server/repository.py +701 -0
  152. plexora/plugins/figure_builder/server/routes.py +594 -0
  153. plexora/plugins/figure_builder/server/schema.py +1023 -0
  154. plexora/plugins/figure_builder/server/sources.py +120 -0
  155. plexora/plugins/figure_builder/server/textmetrics.py +155 -0
  156. plexora/plugins/figure_builder/static/figureActions.js +363 -0
  157. plexora/plugins/figure_builder/static/figureBuilderApi.js +255 -0
  158. plexora/plugins/figure_builder/static/figureCanvas.js +2362 -0
  159. plexora/plugins/figure_builder/static/figureCaptureBoxes.js +369 -0
  160. plexora/plugins/figure_builder/static/figureCaptureDock.js +466 -0
  161. plexora/plugins/figure_builder/static/figureCaptureTool.js +1034 -0
  162. plexora/plugins/figure_builder/static/figureConfirm.js +136 -0
  163. plexora/plugins/figure_builder/static/figureContextBar.js +1193 -0
  164. plexora/plugins/figure_builder/static/figureContextMenu.js +253 -0
  165. plexora/plugins/figure_builder/static/figureDocumentState.js +275 -0
  166. plexora/plugins/figure_builder/static/figureExportUi.js +174 -0
  167. plexora/plugins/figure_builder/static/figureLibrary.js +247 -0
  168. plexora/plugins/figure_builder/static/figureQuickEdit.js +628 -0
  169. plexora/plugins/figure_builder/static/figureRichText.js +634 -0
  170. plexora/plugins/figure_builder/static/figureSceneSnapshot.js +361 -0
  171. plexora/plugins/figure_builder/static/figureSchema.js +218 -0
  172. plexora/plugins/figure_builder/static/figureSidebarController.js +1159 -0
  173. plexora/plugins/figure_builder/static/figureTextEditor.js +503 -0
  174. plexora/plugins/figure_builder/static/figureTextPanel.js +491 -0
  175. plexora/plugins/figure_builder/static/figureViewOptions.js +273 -0
  176. plexora/plugins/figure_builder/static/figureWorkspace.js +2030 -0
  177. plexora/plugins/figure_builder/static/figure_builder.css +3292 -0
  178. plexora/plugins/figure_builder/templates/figure_builder/library.html +74 -0
  179. plexora/plugins/figure_builder/templates/figure_builder/workspace.html +27 -0
  180. plexora/plugins/figure_builder/templates/figure_builder/workspace_body.html +458 -0
  181. plexora/plugins/gating/__init__.py +76 -0
  182. plexora/plugins/gating/server/__init__.py +0 -0
  183. plexora/plugins/gating/server/anndata_gates.py +367 -0
  184. plexora/plugins/gating/server/database.py +16 -0
  185. plexora/plugins/gating/server/model.py +320 -0
  186. plexora/plugins/gating/server/routes.py +235 -0
  187. plexora/plugins/gating/static/csvGatingList.js +947 -0
  188. plexora/plugins/gating/static/gating.css +289 -0
  189. plexora/plugins/gating/static/gatingApi.js +221 -0
  190. plexora/plugins/gating/static/gatingSidebarController.js +513 -0
  191. plexora/plugins/gating/templates/gating/legacy.html +26 -0
  192. plexora/plugins/gating/templates/gating/panel.html +69 -0
  193. plexora/plugins/roi/__init__.py +95 -0
  194. plexora/plugins/roi/server/__init__.py +8 -0
  195. plexora/plugins/roi/server/adapters.py +655 -0
  196. plexora/plugins/roi/server/geojson.py +267 -0
  197. plexora/plugins/roi/server/geometry.py +181 -0
  198. plexora/plugins/roi/server/mapping.py +169 -0
  199. plexora/plugins/roi/server/operations.py +293 -0
  200. plexora/plugins/roi/server/repository.py +215 -0
  201. plexora/plugins/roi/server/routes.py +409 -0
  202. plexora/plugins/roi/server/schema.py +323 -0
  203. plexora/plugins/roi/static/roi.css +583 -0
  204. plexora/plugins/roi/static/roiApi.js +177 -0
  205. plexora/plugins/roi/static/roiFigureBridge.js +107 -0
  206. plexora/plugins/roi/static/roiGeometry.js +350 -0
  207. plexora/plugins/roi/static/roiRenderer.js +278 -0
  208. plexora/plugins/roi/static/roiSidebarController.js +1038 -0
  209. plexora/plugins/roi/static/roiState.js +518 -0
  210. plexora/plugins/roi/static/roiTools.js +993 -0
  211. plexora/plugins/roi/templates/roi/panel.html +240 -0
  212. plexora/proxy.py +57 -0
  213. plexora/server/models/adapters/__init__.py +94 -0
  214. plexora/server/models/adapters/anndata_adapter.py +388 -0
  215. plexora/server/models/adapters/base.py +93 -0
  216. plexora/server/models/adapters/classify.py +192 -0
  217. plexora/server/models/adapters/csv_adapter.py +105 -0
  218. plexora/server/models/adapters/inspection.py +265 -0
  219. plexora/server/models/adapters/spatialdata_adapter.py +248 -0
  220. plexora/server/models/centroid_tiles.py +305 -0
  221. plexora/server/models/data_model.py +1659 -0
  222. plexora/server/models/database_model.py +169 -0
  223. plexora/server/models/project.py +1270 -0
  224. plexora/server/plugins.py +199 -0
  225. plexora/server/routes/browse_routes.py +41 -0
  226. plexora/server/routes/data_routes.py +294 -0
  227. plexora/server/routes/import_routes.py +603 -0
  228. plexora/server/routes/page_routes.py +139 -0
  229. plexora/server/routes/project_routes.py +438 -0
  230. plexora/server/routes/quick_view_routes.py +62 -0
  231. plexora/server/routes/system_routes.py +39 -0
  232. plexora/server/routes/tool_routes.py +418 -0
  233. plexora/server/utils/addHEColumns.py +11 -0
  234. plexora/server/utils/fast_png.py +97 -0
  235. plexora/server/utils/fullConversion.py +50 -0
  236. plexora/server/utils/native_dialog.py +168 -0
  237. plexora/server/utils/pre_normalization.py +47 -0
  238. plexora/server/utils/segmentation_pyramid.py +598 -0
  239. plexora/server/utils/smallestenclosingcircle.py +126 -0
  240. plexora/server/utils/tiffsurgeon.py +372 -0
  241. plexora/server_cli.py +48 -0
  242. plexora/test.py +0 -0
  243. plexora-0.0.1.dist-info/METADATA +341 -0
  244. plexora-0.0.1.dist-info/RECORD +248 -0
  245. plexora-0.0.1.dist-info/WHEEL +5 -0
  246. plexora-0.0.1.dist-info/entry_points.txt +12 -0
  247. plexora-0.0.1.dist-info/licenses/LICENSE +207 -0
  248. 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)