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,418 @@
1
+ """Opening a plugin's tool, and collecting whatever it still needs.
2
+
3
+ A plugin declares what it needs (plexora/api/plugin.py's Requires). Core works
4
+ out what the project already has, asks only for the difference, and stores the
5
+ answers on the project -- so the next plugin to want the same thing never asks
6
+ again. That is the whole mechanism: core never names a concrete plugin, and a
7
+ plugin never grows its own "please type a column name" box.
8
+
9
+ Opening a tool has three outcomes:
10
+
11
+ OPEN -- everything it needs is there.
12
+ COLLECT -- installed and compatible, but something is missing. Recoverable, so
13
+ the client is told what to ask for and re-enters here once it has
14
+ the answers. This is why the Tools menu lists compatible-but-not-
15
+ ready plugins at all: hiding them hides the only route to making
16
+ them work.
17
+ OFFER -- nothing is blocking, but there is something to offer once. A plugin
18
+ whose inputs are ALL optional would otherwise never reach the modal
19
+ at all: `optional_missing_from` exists and is documented as "offered
20
+ once", and without this outcome nothing ever consults it.
21
+ FALLBACK -- unknown datasource, uninstalled tool, or permanently incompatible.
22
+ Stale and bookmarked links land here, so it must not error.
23
+ """
24
+
25
+ from flask import jsonify, redirect, render_template, request
26
+
27
+ from plexora import app
28
+ from plexora.server import plugins as plugin_registry
29
+ from plexora.server.models import data_model
30
+ from plexora.server.models.project import ROLE_LABELS, ROLE_NAMES, Project
31
+ from plexora.server.routes.page_routes import template_data
32
+
33
+ OPEN, COLLECT, OFFER, FALLBACK = "open", "collect", "offer", "fallback"
34
+
35
+
36
+ def _resolve(datasource, tool_name):
37
+ """(outcome, plugin, project) for opening `tool_name` on `datasource`.
38
+
39
+ Whether a tool applies is the plugin's own declaration, not a rule core
40
+ hardcodes -- core used to test `image_kind == 'rgb'` directly, which only
41
+ ever encoded what gating in particular could not handle.
42
+ """
43
+ project = Project.find(datasource)
44
+ plugin = plugin_registry.find(app, tool_name)
45
+ if project is None or plugin is None or not plugin.requires.applies_to(project):
46
+ return FALLBACK, None, project
47
+ # Two reasons to collect, not one. Something absent is the obvious case; a
48
+ # value the column predictor guessed and nobody ever looked at is the other,
49
+ # and it is the more common one -- a conventionally-named table leaves
50
+ # nothing missing at all, so without this the first launch would silently
51
+ # run on five guesses.
52
+ if plugin.requires.missing_from(project) or plugin.requires.unconfirmed_from(project):
53
+ return COLLECT, plugin, project
54
+ # Nothing blocks, but something has never been put in front of the user.
55
+ # Kept as its own outcome rather than folded into COLLECT because the two
56
+ # callers below want opposite things from it: the panel fetch shows the
57
+ # form, the plain <a href> opens the tool. A plugin that requires nothing
58
+ # at all -- ROI -- reaches the modal only through here.
59
+ if plugin.requires.optional_missing_from(project):
60
+ return OFFER, plugin, project
61
+ return OPEN, plugin, project
62
+
63
+
64
+ def _needs(plugin, project):
65
+ """What the client renders a form from.
66
+
67
+ Everything here is generic: a list of typed requirements plus the material
68
+ needed to answer them (the project's columns, its current roles). The
69
+ modal builds inputs from `kind` without knowing which plugin asked.
70
+
71
+ Three lists, because the user is in three different positions:
72
+
73
+ `missing` -- nothing is stored; the field starts empty.
74
+ `confirm` -- something is stored but was guessed; the field is prefilled
75
+ and the user is being shown it once.
76
+ `optional` -- absent and not blocking; offered while the form is open.
77
+
78
+ A requirement the user has already answered appears in none of them. That
79
+ is the property the whole design turns on, and it is why an answer is
80
+ recorded on the project rather than by the plugin that asked for it.
81
+ """
82
+ from plexora.server.routes.project_routes import known_layers, known_obsm
83
+
84
+ # Filled in from the file for a project imported before layers or obsm were
85
+ # recorded, which is precisely the project whose matrix and coordinate
86
+ # choices are wrong and cannot be corrected. Deferred import:
87
+ # project_routes imports this module.
88
+ project = known_obsm(known_layers(project))
89
+ return {
90
+ "tool": plugin.name,
91
+ "label": plugin.label,
92
+ # Why anyone would fill in a form that blocks nothing. Core's own
93
+ # wording covers the blocking case for every plugin; this one cannot be
94
+ # written generically, so the plugin supplies it (Plugin.intro).
95
+ "intro": plugin.intro,
96
+ "missing": [r.describe() for r in plugin.requires.missing_from(project)],
97
+ "confirm": [r.describe() for r in plugin.requires.unconfirmed_from(project)],
98
+ "optional": [r.describe() for r in plugin.requires.optional_missing_from(project)],
99
+ "columns": {
100
+ "markers": list(project.columns.markers),
101
+ "metadata": list(project.columns.metadata),
102
+ },
103
+ # What a role select is made of: the columns to offer, the answer to
104
+ # preselect, and what a blank one falls back to. All three come from the
105
+ # project rather than being assembled client-side, because for AnnData
106
+ # and SpatialData the question is asked about the source file's obs
107
+ # columns and answered into the read spec -- a translation the modal has
108
+ # no business knowing about (see Project.role_columns).
109
+ "roleColumns": project.role_columns,
110
+ "roleAnswers": project.role_answers,
111
+ "roleDefaults": project.role_defaults,
112
+ # What the coordinate field is made of: every obsm array the file
113
+ # carries (with its shape), every obs column, and the source recorded
114
+ # now. Assembled here rather than client-side for the same reason the
115
+ # role keys are -- the answer is a read spec, not a column name.
116
+ "coordinateOptions": project.coordinate_options,
117
+ # Whether the user has said this table covers a single image, which is
118
+ # an answer to the image-id question and not an absence of one.
119
+ "singleImage": bool(project.dataset and project.dataset.single_image),
120
+ # The same thing for the cell id: whether the user has said to number
121
+ # the rows, which is an answer and not a blank left alone.
122
+ "rowNumberIds": bool(project.dataset and project.dataset.row_number_ids),
123
+ # What the "Expression values" field is made of: every matrix the file
124
+ # carries, which one is read now, and whether it is log1p'd on the way
125
+ # in. Same three keys the edit page renders from.
126
+ "featureOptions": project.feature_options,
127
+ "featureSource": project.feature_source,
128
+ "featureLog": project.log_transformed,
129
+ # The whole role vocabulary, so every surface words a role the same way
130
+ # and core owns the wording rather than each of them. The modal reads a
131
+ # label off the requirement itself and does not need this today; it is
132
+ # kept because a client asking what this project's roles are called
133
+ # should not have to know which of them happened to be asked for.
134
+ "roleLabels": dict(ROLE_LABELS),
135
+ "hasTable": project.has_table,
136
+ "segmentationPending": project.segmentation.pending,
137
+ }
138
+
139
+
140
+ @app.route('/<string:datasource>/tools/<string:tool_name>')
141
+ def open_tool(datasource, tool_name):
142
+ """The plain <a href> path, for a client that did not intercept the click.
143
+
144
+ Hands off to the edit page rather than the upload page: the edit page is
145
+ generated from the same requirements, so there is one surface to maintain
146
+ and the user lands on the specific fields this tool is missing.
147
+
148
+ OFFER is deliberately not COLLECT here. Nothing is blocking, so sending the
149
+ user to the edit page would be a detour to answer a question they are
150
+ entitled to ignore -- and this is the path taken when the client could not
151
+ intercept the click, which is exactly when a detour is hardest to undo.
152
+ """
153
+ base_url = app.config.get('PLEXORA_BASE_URL', '')
154
+ outcome, _, _ = _resolve(datasource, tool_name)
155
+
156
+ if outcome == COLLECT:
157
+ return redirect(f"{base_url}/edit_config/{datasource}?needs={tool_name}")
158
+ if outcome == FALLBACK:
159
+ return redirect(f"{base_url}/{datasource}")
160
+ return redirect(f"{base_url}/{datasource}?tool={tool_name}")
161
+
162
+
163
+ @app.route('/<string:datasource>/tools/<string:tool_name>/panel')
164
+ def tool_panel(datasource, tool_name):
165
+ """Fetched by toolLoader.js the first time a tool is opened mid-session
166
+ (plain viewer already loaded, no navigation) -- mirrors open_tool()'s
167
+ checks above, but returns JSON for client-side injection instead of a
168
+ redirect, so the viewer/OpenSeadragon instance already on the page is never
169
+ torn down.
170
+
171
+ When something is missing this returns `needs` rather than a redirect. That
172
+ is the point of the modal: navigating away to collect a column name would
173
+ rebuild the whole viewer to answer one question.
174
+
175
+ OFFER is served the same way. The form it produces has no empty blocking
176
+ field and its Cancel is a real answer, so a user who wants none of it is one
177
+ click from the panel -- which is what makes offering it acceptable at all.
178
+ """
179
+ base_url = app.config.get('PLEXORA_BASE_URL', '')
180
+ outcome, plugin, project = _resolve(datasource, tool_name)
181
+
182
+ if outcome in (COLLECT, OFFER):
183
+ return jsonify({"needs": _needs(plugin, project)})
184
+ if outcome == FALLBACK:
185
+ return jsonify({"redirect": f"{base_url}/{datasource}"}), 400
186
+
187
+ data = template_data(datasource=datasource, active_tool=tool_name)
188
+ fragments = {
189
+ slot: render_template(template_path, data=data)
190
+ for slot, template_path in plugin.panels.items()
191
+ }
192
+ return jsonify({
193
+ "fragments": fragments,
194
+ "scripts": plugin.asset_urls("scripts", base_url),
195
+ "styles": plugin.asset_urls("styles", base_url),
196
+ })
197
+
198
+
199
+ @app.route('/<string:datasource>/tools/<string:tool_name>/requirements')
200
+ def tool_requirements(datasource, tool_name):
201
+ """What a tool still needs, without opening it.
202
+
203
+ Lets a plugin ask for something mid-session -- gating wants an image-id
204
+ column only when the user chooses to write gates back to the source file,
205
+ long after the panel opened.
206
+
207
+ `?keys=a,b` adds a `requested` list holding exactly those inputs, if the
208
+ project still cannot answer them. That is a different question from the
209
+ lists above: those are what the user has not been asked yet, this is what a
210
+ specific action needs right now, and an optional field they were offered
211
+ and skipped shows up only in the second.
212
+ """
213
+ outcome, plugin, project = _resolve(datasource, tool_name)
214
+ if outcome == FALLBACK:
215
+ return jsonify(success=False, error="Unknown tool or datasource"), 404
216
+
217
+ payload = _needs(plugin, project)
218
+ keys = [key for key in (request.args.get("keys") or "").split(",") if key]
219
+ if keys:
220
+ payload["requested"] = [
221
+ r.describe() for r in plugin.requires.requested_from(project, keys)
222
+ ]
223
+ return jsonify(success=True, **payload)
224
+
225
+
226
+ @app.route('/<string:datasource>/requirements', methods=['POST'])
227
+ def satisfy_requirements(datasource):
228
+ """Record answers to whatever was missing.
229
+
230
+ Everything reusable lands on the project, not in the asking plugin's own
231
+ storage: which column holds the cell id is a fact about the dataset, and a
232
+ second plugin needing it must find it already answered. Plugin-private
233
+ state goes through plexora.api.store instead, which is namespaced per
234
+ plugin precisely because it is nobody else's business.
235
+
236
+ Accepts any subset -- the modal posts once with whatever the user filled
237
+ in, and the reply says what is still outstanding.
238
+ """
239
+ from plexora.server.routes import import_routes
240
+
241
+ project = Project.find(datasource)
242
+ if project is None:
243
+ return jsonify(success=False, error="Unknown datasource"), 404
244
+
245
+ payload = request.get_json(silent=True) or {}
246
+ reload_needed = False
247
+
248
+ try:
249
+ if payload.get("data"):
250
+ import_routes.replace_project_data(datasource, payload["data"], payload)
251
+ reload_needed = True
252
+ if payload.get("segmentation"):
253
+ import_routes.attach_segmentation(datasource, payload["segmentation"])
254
+ if not reload_needed:
255
+ # Skipped when the data file itself just changed:
256
+ # replace_project_data already read this answer out of the same
257
+ # payload, and re-applying it would check the choice against the
258
+ # file that has just been replaced.
259
+ from plexora.server.routes.project_routes import apply_feature_choice
260
+
261
+ reload_needed = apply_feature_choice(
262
+ Project.find(datasource) or project, payload)
263
+ answers = ("roles", "columns", "confirm", "coordinates", "single_image",
264
+ "row_number_ids")
265
+ if any(payload.get(key) for key in answers):
266
+ Project.mutate(datasource, lambda p: _apply(p, payload))
267
+ # `single_image` is absent here on purpose: it records what the
268
+ # table already was, so nothing about how it is read has changed.
269
+ # `coordinates` very much has -- it repoints the adapter at a
270
+ # different array -- and so does `row_number_ids`, which clears the
271
+ # obs column the identifier was being read from.
272
+ reload_needed = reload_needed or bool(
273
+ payload.get("roles") or payload.get("columns")
274
+ or payload.get("coordinates") or payload.get("row_number_ids"))
275
+ except ValueError as exc:
276
+ return jsonify(success=False, error=str(exc)), 400
277
+
278
+ if reload_needed:
279
+ error = _reload_or_restore(datasource, project)
280
+ if error:
281
+ return jsonify(success=False, error=error), 400
282
+
283
+ project = Project.find(datasource)
284
+ tool_name = payload.get("tool")
285
+ plugin = plugin_registry.find(app, tool_name) if tool_name else None
286
+ # Everything the tool would still stop for, so the modal knows whether to
287
+ # close or re-render. Unconfirmed items count: naming a data file makes a
288
+ # whole set of column questions askable that were not askable before, and
289
+ # those arrive unconfirmed.
290
+ still_missing = (
291
+ [r.describe() for r in plugin.requires.missing_from(project)]
292
+ + [r.describe() for r in plugin.requires.unconfirmed_from(project)]
293
+ if plugin else []
294
+ )
295
+ # What is newly offerable, which is not the same list and does not belong in
296
+ # it. Naming a data file makes "which column holds the cell id" ASKABLE for
297
+ # the first time, and for a plugin that only ever offers that question it
298
+ # arrives here rather than in `stillMissing` -- so a modal that closed on
299
+ # `stillMissing` alone would shut the moment the file was attached, one
300
+ # question short. Nothing blocks on these: they keep the form open, they do
301
+ # not keep the tool shut.
302
+ still_optional = (
303
+ [r.describe() for r in plugin.requires.optional_missing_from(project)]
304
+ if plugin else []
305
+ )
306
+ return jsonify(
307
+ success=True,
308
+ stillMissing=still_missing,
309
+ stillOptional=still_optional,
310
+ # Whether the datasource was re-read. The client holds a snapshot of the
311
+ # table's per-column statistics taken at page load, and a tool is drawn
312
+ # from that snapshot -- so an answer that changes which numbers are read
313
+ # (a different matrix, the log transform, a swapped file) leaves the
314
+ # panel showing the old ones unless the client is told to re-fetch.
315
+ reloaded=reload_needed,
316
+ segmentationPending=project.segmentation.pending,
317
+ )
318
+
319
+
320
+ def _reload_or_restore(datasource, previous):
321
+ """Reload the datasource, putting `previous` back if it will not load.
322
+
323
+ An answer is only really accepted once the file can be read with it. A role
324
+ can rewrite the read spec, and some answers are ones the adapter refuses --
325
+ an obs column whose name collides with one it synthesizes, most obviously --
326
+ and it only says so here. Storing such an answer and leaving it stored gives
327
+ the user a project that no longer opens at all, which is a far worse outcome
328
+ than the question they were trying to answer.
329
+
330
+ Scoped to ValueError, which is how an adapter says "I cannot read the file
331
+ this way" and is already what these routes treat as the user's to fix.
332
+ Anything else is a bug rather than a rejected answer, and swallowing it here
333
+ would turn it into a silent no-op save.
334
+
335
+ Returns an error message, or None when the reload was fine.
336
+ """
337
+ try:
338
+ data_model.load_datasource(datasource, reload=True)
339
+ return None
340
+ except ValueError as exc:
341
+ previous.save()
342
+ try:
343
+ data_model.load_datasource(datasource, reload=True)
344
+ except ValueError:
345
+ # The project was already unloadable before this request. Nothing
346
+ # to restore it to, and the message below is still the useful half.
347
+ pass
348
+ return str(exc)
349
+
350
+
351
+ def _apply(project, payload):
352
+ columns = payload.get("columns") or {}
353
+ if columns:
354
+ project = project.with_columns(columns.get("markers") or [],
355
+ columns.get("metadata") or [])
356
+ project = apply_column_answers(project, payload)
357
+ # Confirmed last, from two sources. `confirm` is the list of keys the form
358
+ # actually showed: an optional field left blank still counts, because the
359
+ # user was asked and declined, and re-asking on every open is what this
360
+ # list exists to prevent. `_supplied_keys` covers whatever arrived in the
361
+ # payload regardless, so a caller that answers without saying which
362
+ # requirement it was answering does not leave the question open.
363
+ return project.with_confirmed(
364
+ list(payload.get("confirm") or ()) + _supplied_keys(payload))
365
+
366
+
367
+ def apply_column_answers(project, payload):
368
+ """Record the answers a form gives about a project's columns.
369
+
370
+ Shared by the requirements modal and the project edit page so the two
371
+ cannot drift: both post `roles`, `coordinates`, `single_image` and
372
+ `row_number_ids`, and both have to mean the same thing by them.
373
+
374
+ Order matters. The two flags are applied after the roles because each is an
375
+ alternative answer to a question a role also answers -- naming an image-id
376
+ column retracts a previous "only one image", naming an id column retracts
377
+ "number the rows" (see Project.with_role_answers) -- so a payload carrying
378
+ both means the user has just switched to the flag.
379
+ """
380
+ project = project.with_role_answers(payload.get("roles") or {})
381
+ coordinates = payload.get("coordinates")
382
+ if coordinates:
383
+ project = project.with_coordinates(coordinates)
384
+ if payload.get("single_image"):
385
+ project = project.with_single_image(True)
386
+ if payload.get("row_number_ids"):
387
+ project = project.with_row_number_ids(True)
388
+ return project
389
+
390
+
391
+ def _supplied_keys(payload):
392
+ """The requirement keys this payload carries an answer for."""
393
+ keys = []
394
+ if payload.get("data"):
395
+ keys.append("table")
396
+ if payload.get("segmentation"):
397
+ keys.append("segmentation")
398
+ if payload.get("columns"):
399
+ keys.append("markers")
400
+ if payload.get("features_layer") or "features_log" in payload:
401
+ keys.append("features")
402
+ # Filtered against ROLE_NAMES, the same gate with_role_answers applies to
403
+ # the answers themselves. Without it a payload naming a role core cannot
404
+ # store still marked a key confirmed -- so the answer was dropped and the
405
+ # question was never asked again, which is exactly how a project ended up
406
+ # keeping a cell id nobody chose.
407
+ keys.extend(f"role:{role}" for role, column in (payload.get("roles") or {}).items()
408
+ if column and role in ROLE_NAMES)
409
+ if payload.get("coordinates"):
410
+ keys.append("coordinates")
411
+ if payload.get("single_image"):
412
+ # The other answer to the image-id question, and the one that names no
413
+ # column -- so without this the question would be asked again forever.
414
+ keys.append("role:image_id")
415
+ if payload.get("row_number_ids"):
416
+ # Likewise for the cell id.
417
+ keys.append("role:cell_id")
418
+ return keys
@@ -0,0 +1,11 @@
1
+ #%%
2
+ import polars as pl
3
+
4
+ data = pl.read_csv('./unmicst-WD-76845-097-ij_subtracted_50.csv')
5
+ data.head()
6
+ #%%
7
+ data = data.with_columns(pl.lit(0).alias('HE_r'), pl.lit(0).alias('HE_g'), pl.lit(0).alias('HE_b'))
8
+ data.columns
9
+ #%%
10
+ data.write_csv('./unmicst-WD-76845-097-ij_subtracted_50-jj.csv')
11
+ #%%
@@ -0,0 +1,97 @@
1
+ """Fast, lossless PNG encoding using a libdeflate-backed compressor instead
2
+ of PIL's stdlib-zlib encoder.
3
+
4
+ PIL's PNG encoder is stdlib-zlib-backed, which is markedly slower than a
5
+ libdeflate-backed compressor for the same compression ratio on this kind of
6
+ data (benchmarked on 1024x1024 tiles: PIL compress_level=9 takes ~207ms for
7
+ ~1.17MB of 16-bit grayscale channel data that imagecodecs' zlib-ng backed
8
+ compressor gets in ~25ms; on RGBA segmentation-mask tiles, PIL
9
+ compress_level=0 -- current production -- takes ~20ms for ~4.1MB where this
10
+ module gets ~56KB in ~13ms). PIL has no way to select a different
11
+ compression backend, so this module hand-assembles minimal, spec-valid PNGs
12
+ instead of going through PIL at all.
13
+
14
+ Used for:
15
+ - the "HD" full-precision channel-tile quality mode (encode_gray16_png)
16
+ - segmentation/label tiles (encode_rgba8_png) -- safe to use PNG's own
17
+ fast path here despite segmentation's alpha=0-everywhere data (unlike
18
+ WebP, PNG decode never goes through a premultiplied-alpha compositing
19
+ step; the frontend parses PNG bytes directly via UPNG.js, not a canvas)
20
+ """
21
+ import struct
22
+ import zlib
23
+
24
+ import imagecodecs
25
+ import numpy as np
26
+
27
+ _PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n"
28
+ _ZLIBNG_LEVEL = 6
29
+
30
+
31
+ def _chunk(chunk_type: bytes, data: bytes) -> bytes:
32
+ return (
33
+ struct.pack(">I", len(data))
34
+ + chunk_type
35
+ + data
36
+ + struct.pack(">I", zlib.crc32(chunk_type + data))
37
+ )
38
+
39
+
40
+ def _encode_png(width: int, height: int, bit_depth: int, color_type: int, row_bytes: np.ndarray,
41
+ compress_level: int = _ZLIBNG_LEVEL) -> bytes:
42
+ """row_bytes: 2D uint8 array, one PNG scanline (without a filter byte) per row.
43
+
44
+ compress_level=0 emits stored (uncompressed) deflate blocks. That is the
45
+ right choice whenever the decoder is JavaScript: pako's inflate dominates
46
+ client-side decode cost, and a stored stream is copied rather than
47
+ Huffman-decoded. It is still a spec-valid PNG.
48
+ """
49
+ stride = row_bytes.shape[1]
50
+ filtered = np.empty((height, stride + 1), dtype="u1")
51
+ filtered[:, 0] = 0 # filter type 0 ("None") on every scanline
52
+ filtered[:, 1:] = row_bytes
53
+
54
+ idat_data = imagecodecs.zlibng_encode(filtered.tobytes(), level=compress_level)
55
+ ihdr = struct.pack(">IIBBBBB", width, height, bit_depth, color_type, 0, 0, 0)
56
+
57
+ return (
58
+ _PNG_SIGNATURE
59
+ + _chunk(b"IHDR", ihdr)
60
+ + _chunk(b"IDAT", idat_data)
61
+ + _chunk(b"IEND", b"")
62
+ )
63
+
64
+
65
+ def encode_gray16_png(tile: np.ndarray, compress_level: int = _ZLIBNG_LEVEL) -> bytes:
66
+ """Encode a 2D uint16 array as a minimal 16-bit grayscale PNG (IHDR
67
+ bit depth 16, color type 0), byte-for-byte lossless.
68
+ """
69
+ if tile.ndim != 2:
70
+ raise ValueError(f"expected a 2D array, got shape {tile.shape}")
71
+ if tile.dtype != np.uint16:
72
+ tile = tile.astype(np.uint16)
73
+
74
+ height, width = tile.shape
75
+ # PNG samples are big-endian regardless of host byte order.
76
+ be_tile = np.ascontiguousarray(tile).astype(">u2")
77
+ stride = width * 2
78
+ row_bytes = np.frombuffer(be_tile.tobytes(), dtype="u1").reshape(height, stride)
79
+
80
+ return _encode_png(width, height, bit_depth=16, color_type=0, row_bytes=row_bytes,
81
+ compress_level=compress_level)
82
+
83
+
84
+ def encode_rgba8_png(tile: np.ndarray) -> bytes:
85
+ """Encode a (H, W, 4) uint8 array as a minimal 8-bit RGBA PNG (IHDR bit
86
+ depth 8, color type 6), byte-for-byte lossless. Used for segmentation
87
+ tiles (packed uint32 label IDs, alpha channel unused/zero).
88
+ """
89
+ if tile.ndim != 3 or tile.shape[2] != 4:
90
+ raise ValueError(f"expected an (H, W, 4) array, got shape {tile.shape}")
91
+ if tile.dtype != np.uint8:
92
+ tile = tile.astype(np.uint8)
93
+
94
+ height, width, _ = tile.shape
95
+ row_bytes = np.ascontiguousarray(tile).reshape(height, width * 4)
96
+
97
+ return _encode_png(width, height, bit_depth=8, color_type=6, row_bytes=row_bytes)
@@ -0,0 +1,50 @@
1
+ import os
2
+ # set Libvips path for windows. If mac, please run `brew install vips`
3
+ if os.name == 'nt':
4
+ vipshome = os.path.dirname(os.path.abspath(__file__)) + r"\..\static\external\vips-dev-8.10\bin"
5
+ os.environ['PATH'] = vipshome + ';' + os.environ['PATH']
6
+
7
+ from PIL import Image
8
+
9
+ Image.MAX_IMAGE_PIXELS = 1000000000
10
+
11
+ # def convertTifToPyramid(channel_img, destFile, isLabelImg):
12
+ # img_dim = channel_img.shape[:2]
13
+ # print('img shape', img_dim, channel_img.dtype)
14
+ # channel_img = channel_img.astype('uint32')
15
+ # imgR = ((channel_img >> 16) % 256).astype('uint8')
16
+ # imgG = ((channel_img >> 8) % 256).astype('uint8') # high bits
17
+ # imgB = (channel_img % 256).astype('uint8') # low bits
18
+ # channel_img = cv2.merge((imgB, imgG, imgR))
19
+ #
20
+ # if channel_img is not None:
21
+ # print('Destfile', destFile)
22
+ # cv2.imwrite(destFile + '.png', channel_img) # [cv2.IMWRITE_PNG_COMPRESSION, 9]
23
+ # image = pyvips.Image.new_from_file(destFile + '.png', access='sequential')
24
+ # # Remove any existing directory with the name of the channel
25
+ # if os.path.exists(destFile):
26
+ # shutil.rmtree(destFile)
27
+ # if not isLabelImg:
28
+ # image.dzsave(destFile, tile_size=128, overlap=2, suffix='.png')
29
+ # else:
30
+ # image.dzsave(destFile, tile_size=128, overlap=2, region_shrink='nearest', suffix='.png')
31
+
32
+ #
33
+ # def convertOmeTiff(filePath, fileName, isLabelImg=False):
34
+ # channelNames = []
35
+ # file_path = str(Path(filePath) / fileName)
36
+ # ome = tf.imread(file_path, is_ome=True)
37
+ # for channel in range(np.shape(ome)[0]):
38
+ # img = ome[channel, :, :]
39
+ # channelName = re.sub(r'\.ome|\.tiff|\.tif|\.png', '', fileName) + "_" + str(channel)
40
+ # channel_path = str(Path(filePath) / channelName)
41
+ # convertTifToPyramid(img, channel_path, isLabelImg)
42
+ # channelNames.append(channelName)
43
+ # return channelNames
44
+
45
+ #
46
+ # def convertChannel(filePath, isLabelImg):
47
+ # channel_img = imread(filePath)
48
+ # channel_img = np.squeeze(channel_img) # Remove any unnecessary single dimensions
49
+ # channelPath = re.sub(r'\.ome|\.tiff|\.tif|\.png', '', filePath)
50
+ # convertTifToPyramid(channel_img, channelPath, isLabelImg)