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