plexora 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- plexora/__init__.py +129 -0
- plexora/__main__.py +15 -0
- plexora/_url.py +105 -0
- plexora/api/__init__.py +55 -0
- plexora/api/dataset.py +398 -0
- plexora/api/http.py +19 -0
- plexora/api/plugin.py +541 -0
- plexora/api/store.py +221 -0
- plexora/cli.py +777 -0
- plexora/client/dist/354_bundle.js +2 -0
- plexora/client/dist/354_bundle.js.LICENSE.txt +20 -0
- plexora/client/dist/418_bundle.js +2 -0
- plexora/client/dist/418_bundle.js.LICENSE.txt +1 -0
- plexora/client/dist/693b935cb1f907814be9c6199a68c217.svg +19 -0
- plexora/client/dist/713c134ff47cd328a5b711526f151082.svg +18 -0
- plexora/client/dist/770_bundle.js +1 -0
- plexora/client/dist/ff743f408972e0e96ca9153ed5ae4c83.svg +23 -0
- plexora/client/dist/vendor_bundle.js +2 -0
- plexora/client/dist/vendor_bundle.js.LICENSE.txt +308 -0
- plexora/client/external/openseadragon-bin-2.4.0/LICENSE.txt +28 -0
- plexora/client/external/openseadragon-bin-2.4.0/canvas-overlay-hd.js +140 -0
- plexora/client/external/openseadragon-bin-2.4.0/changelog.txt +501 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/.gitattributes +17 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/LICENSE.txt +116 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/Toolbar.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/fullpage.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/home.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/next.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/previous.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/rotateleft.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/rotateright.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/zoom.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/README.md +9 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-scalebar.js +562 -0
- plexora/client/src/css/import.css +474 -0
- plexora/client/src/css/main.css +903 -0
- plexora/client/src/css/openProject.css +363 -0
- plexora/client/src/css/quickView.css +166 -0
- plexora/client/src/css/tokens.css +51 -0
- plexora/client/src/css/viewer.css +1655 -0
- plexora/client/src/img/apple-touch-icon.png +0 -0
- plexora/client/src/img/favicon.ico +0 -0
- plexora/client/src/img/logo.ai +8892 -33
- plexora/client/src/img/logo.svg +1 -0
- plexora/client/src/img/logo_with_text.ai +9011 -34
- plexora/client/src/img/logo_with_text.svg +1 -0
- plexora/client/src/js/main.js +704 -0
- plexora/client/src/js/pluginRegistry.js +108 -0
- plexora/client/src/js/services/appStatus.js +286 -0
- plexora/client/src/js/services/browsePicker.js +73 -0
- plexora/client/src/js/services/dataLayer.js +400 -0
- plexora/client/src/js/services/datasetContext.js +143 -0
- plexora/client/src/js/services/glRenderer.js +143 -0
- plexora/client/src/js/services/importFormValidation.js +257 -0
- plexora/client/src/js/services/numericData.js +100 -0
- plexora/client/src/js/services/passVariablesToFrontend.js +20 -0
- plexora/client/src/js/services/simpleEventHandler.js +25 -0
- plexora/client/src/js/vendor.js +29 -0
- plexora/client/src/js/views/channelList.js +663 -0
- plexora/client/src/js/views/colorSwatchPicker.js +202 -0
- plexora/client/src/js/views/columnClassifier.js +146 -0
- plexora/client/src/js/views/coordinateField.js +133 -0
- plexora/client/src/js/views/dataSourceField.js +276 -0
- plexora/client/src/js/views/imageViewer.js +3306 -0
- plexora/client/src/js/views/miniMap.js +787 -0
- plexora/client/src/js/views/navbarControls.js +178 -0
- plexora/client/src/js/views/openProjectPage.js +239 -0
- plexora/client/src/js/views/projectEdit.js +335 -0
- plexora/client/src/js/views/quickViewLanding.js +136 -0
- plexora/client/src/js/views/rainbow.js +326 -0
- plexora/client/src/js/views/requirementsModal.js +572 -0
- plexora/client/src/js/views/rgbImageViewer.js +115 -0
- plexora/client/src/js/views/searchableSelect.js +411 -0
- plexora/client/src/js/views/segmentationProgress.js +122 -0
- plexora/client/src/js/views/toolLoader.js +747 -0
- plexora/client/src/js/views/viewerControls.js +689 -0
- plexora/client/src/js/views/viewerManager.js +434 -0
- plexora/client/src/js/views/viewerSidebar.js +1117 -0
- plexora/client/src/js/workers/tileDecoder.js +130 -0
- plexora/client/src/shaders/frag.glsl +528 -0
- plexora/client/src/shaders/vert.glsl +12 -0
- plexora/client/templates/base.html +212 -0
- plexora/client/templates/index.html +238 -0
- plexora/client/templates/open_project.html +96 -0
- plexora/client/templates/project_columns.html +86 -0
- plexora/client/templates/project_edit.html +196 -0
- plexora/client/templates/upload.html +118 -0
- plexora/connect.py +528 -0
- plexora/datasource.py +782 -0
- plexora/jupyter.py +406 -0
- plexora/notebook_env.py +232 -0
- plexora/paths.py +486 -0
- plexora/plugins/__init__.py +12 -0
- plexora/plugins/cell_explorer/__init__.py +100 -0
- plexora/plugins/cell_explorer/server/__init__.py +11 -0
- plexora/plugins/cell_explorer/server/routes.py +167 -0
- plexora/plugins/cell_explorer/server/state.py +281 -0
- plexora/plugins/cell_explorer/server/values.py +145 -0
- plexora/plugins/cell_explorer/server/variables.py +367 -0
- plexora/plugins/cell_explorer/static/cellExplorerApi.js +158 -0
- plexora/plugins/cell_explorer/static/cellExplorerColors.js +316 -0
- plexora/plugins/cell_explorer/static/cellExplorerContinuous.js +403 -0
- plexora/plugins/cell_explorer/static/cellExplorerFigureBridge.js +133 -0
- plexora/plugins/cell_explorer/static/cellExplorerLegend.js +188 -0
- plexora/plugins/cell_explorer/static/cellExplorerRoiBridge.js +822 -0
- plexora/plugins/cell_explorer/static/cellExplorerSidebarController.js +826 -0
- plexora/plugins/cell_explorer/static/cellExplorerState.js +372 -0
- plexora/plugins/cell_explorer/static/cell_explorer.css +752 -0
- plexora/plugins/cell_explorer/templates/cell_explorer/panel.html +137 -0
- plexora/plugins/figure_builder/__init__.py +125 -0
- plexora/plugins/figure_builder/server/__init__.py +7 -0
- plexora/plugins/figure_builder/server/compose.py +365 -0
- plexora/plugins/figure_builder/server/export.py +784 -0
- plexora/plugins/figure_builder/server/export_jobs.py +148 -0
- plexora/plugins/figure_builder/server/operations.py +556 -0
- plexora/plugins/figure_builder/server/pixels.py +142 -0
- plexora/plugins/figure_builder/server/provenance.py +174 -0
- plexora/plugins/figure_builder/server/render.py +278 -0
- plexora/plugins/figure_builder/server/repository.py +701 -0
- plexora/plugins/figure_builder/server/routes.py +594 -0
- plexora/plugins/figure_builder/server/schema.py +1023 -0
- plexora/plugins/figure_builder/server/sources.py +120 -0
- plexora/plugins/figure_builder/server/textmetrics.py +155 -0
- plexora/plugins/figure_builder/static/figureActions.js +363 -0
- plexora/plugins/figure_builder/static/figureBuilderApi.js +255 -0
- plexora/plugins/figure_builder/static/figureCanvas.js +2362 -0
- plexora/plugins/figure_builder/static/figureCaptureBoxes.js +369 -0
- plexora/plugins/figure_builder/static/figureCaptureDock.js +466 -0
- plexora/plugins/figure_builder/static/figureCaptureTool.js +1034 -0
- plexora/plugins/figure_builder/static/figureConfirm.js +136 -0
- plexora/plugins/figure_builder/static/figureContextBar.js +1193 -0
- plexora/plugins/figure_builder/static/figureContextMenu.js +253 -0
- plexora/plugins/figure_builder/static/figureDocumentState.js +275 -0
- plexora/plugins/figure_builder/static/figureExportUi.js +174 -0
- plexora/plugins/figure_builder/static/figureLibrary.js +247 -0
- plexora/plugins/figure_builder/static/figureQuickEdit.js +628 -0
- plexora/plugins/figure_builder/static/figureRichText.js +634 -0
- plexora/plugins/figure_builder/static/figureSceneSnapshot.js +361 -0
- plexora/plugins/figure_builder/static/figureSchema.js +218 -0
- plexora/plugins/figure_builder/static/figureSidebarController.js +1159 -0
- plexora/plugins/figure_builder/static/figureTextEditor.js +503 -0
- plexora/plugins/figure_builder/static/figureTextPanel.js +491 -0
- plexora/plugins/figure_builder/static/figureViewOptions.js +273 -0
- plexora/plugins/figure_builder/static/figureWorkspace.js +2030 -0
- plexora/plugins/figure_builder/static/figure_builder.css +3292 -0
- plexora/plugins/figure_builder/templates/figure_builder/library.html +74 -0
- plexora/plugins/figure_builder/templates/figure_builder/workspace.html +27 -0
- plexora/plugins/figure_builder/templates/figure_builder/workspace_body.html +458 -0
- plexora/plugins/gating/__init__.py +76 -0
- plexora/plugins/gating/server/__init__.py +0 -0
- plexora/plugins/gating/server/anndata_gates.py +367 -0
- plexora/plugins/gating/server/database.py +16 -0
- plexora/plugins/gating/server/model.py +320 -0
- plexora/plugins/gating/server/routes.py +235 -0
- plexora/plugins/gating/static/csvGatingList.js +947 -0
- plexora/plugins/gating/static/gating.css +289 -0
- plexora/plugins/gating/static/gatingApi.js +221 -0
- plexora/plugins/gating/static/gatingSidebarController.js +513 -0
- plexora/plugins/gating/templates/gating/legacy.html +26 -0
- plexora/plugins/gating/templates/gating/panel.html +69 -0
- plexora/plugins/roi/__init__.py +95 -0
- plexora/plugins/roi/server/__init__.py +8 -0
- plexora/plugins/roi/server/adapters.py +655 -0
- plexora/plugins/roi/server/geojson.py +267 -0
- plexora/plugins/roi/server/geometry.py +181 -0
- plexora/plugins/roi/server/mapping.py +169 -0
- plexora/plugins/roi/server/operations.py +293 -0
- plexora/plugins/roi/server/repository.py +215 -0
- plexora/plugins/roi/server/routes.py +409 -0
- plexora/plugins/roi/server/schema.py +323 -0
- plexora/plugins/roi/static/roi.css +583 -0
- plexora/plugins/roi/static/roiApi.js +177 -0
- plexora/plugins/roi/static/roiFigureBridge.js +107 -0
- plexora/plugins/roi/static/roiGeometry.js +350 -0
- plexora/plugins/roi/static/roiRenderer.js +278 -0
- plexora/plugins/roi/static/roiSidebarController.js +1038 -0
- plexora/plugins/roi/static/roiState.js +518 -0
- plexora/plugins/roi/static/roiTools.js +993 -0
- plexora/plugins/roi/templates/roi/panel.html +240 -0
- plexora/proxy.py +57 -0
- plexora/server/models/adapters/__init__.py +94 -0
- plexora/server/models/adapters/anndata_adapter.py +388 -0
- plexora/server/models/adapters/base.py +93 -0
- plexora/server/models/adapters/classify.py +192 -0
- plexora/server/models/adapters/csv_adapter.py +105 -0
- plexora/server/models/adapters/inspection.py +265 -0
- plexora/server/models/adapters/spatialdata_adapter.py +248 -0
- plexora/server/models/centroid_tiles.py +305 -0
- plexora/server/models/data_model.py +1659 -0
- plexora/server/models/database_model.py +169 -0
- plexora/server/models/project.py +1270 -0
- plexora/server/plugins.py +199 -0
- plexora/server/routes/browse_routes.py +41 -0
- plexora/server/routes/data_routes.py +294 -0
- plexora/server/routes/import_routes.py +603 -0
- plexora/server/routes/page_routes.py +139 -0
- plexora/server/routes/project_routes.py +438 -0
- plexora/server/routes/quick_view_routes.py +62 -0
- plexora/server/routes/system_routes.py +39 -0
- plexora/server/routes/tool_routes.py +418 -0
- plexora/server/utils/addHEColumns.py +11 -0
- plexora/server/utils/fast_png.py +97 -0
- plexora/server/utils/fullConversion.py +50 -0
- plexora/server/utils/native_dialog.py +168 -0
- plexora/server/utils/pre_normalization.py +47 -0
- plexora/server/utils/segmentation_pyramid.py +598 -0
- plexora/server/utils/smallestenclosingcircle.py +126 -0
- plexora/server/utils/tiffsurgeon.py +372 -0
- plexora/server_cli.py +48 -0
- plexora/test.py +0 -0
- plexora-0.0.1.dist-info/METADATA +341 -0
- plexora-0.0.1.dist-info/RECORD +248 -0
- plexora-0.0.1.dist-info/WHEEL +5 -0
- plexora-0.0.1.dist-info/entry_points.txt +12 -0
- plexora-0.0.1.dist-info/licenses/LICENSE +207 -0
- plexora-0.0.1.dist-info/top_level.txt +1 -0
plexora/api/store.py
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""Per-datasource, per-plugin persistence.
|
|
2
|
+
|
|
3
|
+
Plugins are expected to write results back -- derived measurements,
|
|
4
|
+
annotations, classifications, and their own UI state. They do that here rather
|
|
5
|
+
than by reaching into `database_model`, for two reasons.
|
|
6
|
+
|
|
7
|
+
**Namespacing.** Every table this store creates is prefixed
|
|
8
|
+
`plugin_<plugin>_<name>`, so two plugins can both persist "results" without
|
|
9
|
+
colliding, and `drop_all()` can remove exactly one plugin's data on uninstall.
|
|
10
|
+
Before this existed, the gating module wrote a bare `gatinglist` table straight
|
|
11
|
+
into the shared per-datasource file, which then outlived the module: switch to
|
|
12
|
+
a build without gating and the table stayed behind with nothing able to name
|
|
13
|
+
it, let alone remove it.
|
|
14
|
+
|
|
15
|
+
A plugin may declare the table it used before namespacing. State there is
|
|
16
|
+
adopted on first read and the old table is then dropped, so an existing project
|
|
17
|
+
ends up fully converted rather than carrying a dead table forever.
|
|
18
|
+
|
|
19
|
+
**Injection.** `database_model` interpolates the table name directly into SQL,
|
|
20
|
+
which was safe while every table name was a literal in first-party source. With
|
|
21
|
+
third-party plugins the name derives from a package the host did not write, so
|
|
22
|
+
identifiers are validated here rather than trusted.
|
|
23
|
+
|
|
24
|
+
Storage reuses `database_model`'s engine unchanged -- one sqlite file per
|
|
25
|
+
datasource, one row per (table, datasource), lazily created. Tabular values are
|
|
26
|
+
serialized as Parquet so dtypes survive the round trip; opaque state is stored
|
|
27
|
+
as raw bytes exactly as handed over.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import io
|
|
33
|
+
import re
|
|
34
|
+
import sqlite3
|
|
35
|
+
|
|
36
|
+
import polars as pl
|
|
37
|
+
|
|
38
|
+
from plexora.server.models import database_model
|
|
39
|
+
|
|
40
|
+
#: Table and plugin names become SQL identifiers, so they are restricted rather
|
|
41
|
+
#: than escaped: lowercase, digits and underscore, starting with a letter.
|
|
42
|
+
_SAFE_NAME = re.compile(r"^[a-z][a-z0-9_]*$")
|
|
43
|
+
|
|
44
|
+
_PREFIX = "plugin_"
|
|
45
|
+
|
|
46
|
+
#: Name of the per-plugin table holding opaque plugin state, as opposed to a
|
|
47
|
+
#: named result table.
|
|
48
|
+
_STATE = "state"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _validate(kind, value):
|
|
52
|
+
if not isinstance(value, str) or not _SAFE_NAME.match(value):
|
|
53
|
+
raise ValueError(
|
|
54
|
+
f"invalid {kind} name {value!r}: expected lowercase letters, digits "
|
|
55
|
+
"and underscores, starting with a letter"
|
|
56
|
+
)
|
|
57
|
+
return value
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _model_for(table):
|
|
61
|
+
"""database_model keys everything off a class attribute, so give it one."""
|
|
62
|
+
return type("PluginTable", (), {"__tablename__": table})
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class PluginStore:
|
|
66
|
+
"""One plugin's storage for one datasource."""
|
|
67
|
+
|
|
68
|
+
def __init__(self, datasource: str, plugin: str, legacy_state_table: str | None = None):
|
|
69
|
+
self._datasource = datasource
|
|
70
|
+
self._plugin = _validate("plugin", plugin)
|
|
71
|
+
#: Pre-namespacing table this plugin's state used to live in. Read once
|
|
72
|
+
#: as a fallback so upgrading the host does not lose saved work.
|
|
73
|
+
self._legacy_state_table = legacy_state_table
|
|
74
|
+
|
|
75
|
+
# -- naming ---------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def prefix(self) -> str:
|
|
79
|
+
return f"{_PREFIX}{self._plugin}_"
|
|
80
|
+
|
|
81
|
+
def table_name(self, name: str) -> str:
|
|
82
|
+
return f"{self.prefix}{_validate('table', name)}"
|
|
83
|
+
|
|
84
|
+
# -- opaque state ---------------------------------------------------
|
|
85
|
+
|
|
86
|
+
def get_state(self) -> bytes | None:
|
|
87
|
+
"""This plugin's saved state, or None if it has never saved any.
|
|
88
|
+
|
|
89
|
+
Adopts pre-namespacing state on first read -- see `_adopt_legacy_state`.
|
|
90
|
+
"""
|
|
91
|
+
row = database_model.get(_model_for(self.table_name(_STATE)), datasource=self._datasource)
|
|
92
|
+
if row is not None:
|
|
93
|
+
return row.cells
|
|
94
|
+
if self._legacy_state_table is None:
|
|
95
|
+
return None
|
|
96
|
+
return self._adopt_legacy_state()
|
|
97
|
+
|
|
98
|
+
def _adopt_legacy_state(self) -> bytes | None:
|
|
99
|
+
"""Move state out of a plugin's pre-namespacing table, once.
|
|
100
|
+
|
|
101
|
+
Copies the row into the namespaced table and drops the old one, so the
|
|
102
|
+
upgrade is complete rather than permanent: no project keeps a table
|
|
103
|
+
that nothing can name. Idempotent -- once the old table is gone this
|
|
104
|
+
does nothing, and the namespaced read above short-circuits first.
|
|
105
|
+
|
|
106
|
+
Existence is checked directly rather than via `database_model.get`,
|
|
107
|
+
which creates the table it queries. Going through it would recreate the
|
|
108
|
+
very table being retired on every fresh project.
|
|
109
|
+
"""
|
|
110
|
+
if not self._has_table(self._legacy_state_table):
|
|
111
|
+
return None
|
|
112
|
+
legacy = database_model.get(
|
|
113
|
+
_model_for(self._legacy_state_table), datasource=self._datasource
|
|
114
|
+
)
|
|
115
|
+
cells = legacy.cells if legacy is not None else None
|
|
116
|
+
if cells is not None:
|
|
117
|
+
self.put_state(cells)
|
|
118
|
+
self._drop_tables([self._legacy_state_table])
|
|
119
|
+
return cells
|
|
120
|
+
|
|
121
|
+
def put_state(self, blob: bytes) -> None:
|
|
122
|
+
if not isinstance(blob, (bytes, bytearray)):
|
|
123
|
+
raise TypeError(f"state must be bytes, got {type(blob).__name__}")
|
|
124
|
+
database_model.save_list(
|
|
125
|
+
_model_for(self.table_name(_STATE)), datasource=self._datasource, cells=bytes(blob)
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# -- result tables --------------------------------------------------
|
|
129
|
+
|
|
130
|
+
def put_table(self, name: str, frame: pl.DataFrame) -> None:
|
|
131
|
+
"""Store a result table -- measurements, annotations, classifications.
|
|
132
|
+
|
|
133
|
+
Parquet rather than CSV or pickle: dtypes survive, it is compact, and
|
|
134
|
+
it is readable by anything, which matters for data a user may want to
|
|
135
|
+
get back out without running Plexora.
|
|
136
|
+
"""
|
|
137
|
+
if not isinstance(frame, pl.DataFrame):
|
|
138
|
+
raise TypeError(f"expected a polars DataFrame, got {type(frame).__name__}")
|
|
139
|
+
buffer = io.BytesIO()
|
|
140
|
+
frame.write_parquet(buffer)
|
|
141
|
+
database_model.save_list(
|
|
142
|
+
_model_for(self.table_name(name)), datasource=self._datasource, cells=buffer.getvalue()
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
def get_table(self, name: str) -> pl.DataFrame | None:
|
|
146
|
+
row = database_model.get(_model_for(self.table_name(name)), datasource=self._datasource)
|
|
147
|
+
if row is None:
|
|
148
|
+
return None
|
|
149
|
+
return pl.read_parquet(io.BytesIO(row.cells))
|
|
150
|
+
|
|
151
|
+
# -- file storage ---------------------------------------------------
|
|
152
|
+
|
|
153
|
+
def directory(self):
|
|
154
|
+
"""A directory this plugin owns for this datasource, created on demand.
|
|
155
|
+
|
|
156
|
+
For inputs a plugin collects itself -- an uploaded CSV, a cached model,
|
|
157
|
+
anything that is a file rather than a table. Scoped per plugin so an
|
|
158
|
+
uninstall knows what to remove and two plugins cannot overwrite each
|
|
159
|
+
other's uploads, which writing directly into the datasource directory
|
|
160
|
+
allowed.
|
|
161
|
+
"""
|
|
162
|
+
path = (
|
|
163
|
+
database_model._db_path_for_datasource(self._datasource).parent
|
|
164
|
+
/ "plugins"
|
|
165
|
+
/ self._plugin
|
|
166
|
+
)
|
|
167
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
168
|
+
return path
|
|
169
|
+
|
|
170
|
+
# -- introspection and uninstall ------------------------------------
|
|
171
|
+
|
|
172
|
+
def _existing_tables(self, like: str) -> list[str]:
|
|
173
|
+
db_file = database_model._db_path_for_datasource(self._datasource)
|
|
174
|
+
if not db_file.exists():
|
|
175
|
+
return []
|
|
176
|
+
conn = sqlite3.connect(str(db_file), timeout=10)
|
|
177
|
+
try:
|
|
178
|
+
rows = conn.execute(
|
|
179
|
+
"SELECT name FROM sqlite_master WHERE type = 'table' AND name LIKE ?", (like,)
|
|
180
|
+
).fetchall()
|
|
181
|
+
except sqlite3.DatabaseError:
|
|
182
|
+
return []
|
|
183
|
+
finally:
|
|
184
|
+
conn.close()
|
|
185
|
+
return sorted(name for (name,) in rows)
|
|
186
|
+
|
|
187
|
+
def _has_table(self, name: str) -> bool:
|
|
188
|
+
return name in self._existing_tables(name)
|
|
189
|
+
|
|
190
|
+
def _drop_tables(self, names) -> None:
|
|
191
|
+
db_file = database_model._db_path_for_datasource(self._datasource)
|
|
192
|
+
if not db_file.exists():
|
|
193
|
+
return
|
|
194
|
+
conn = sqlite3.connect(str(db_file), timeout=10)
|
|
195
|
+
try:
|
|
196
|
+
for name in names:
|
|
197
|
+
conn.execute(f'DROP TABLE IF EXISTS "{name}"')
|
|
198
|
+
conn.commit()
|
|
199
|
+
finally:
|
|
200
|
+
conn.close()
|
|
201
|
+
|
|
202
|
+
def list_tables(self) -> list[str]:
|
|
203
|
+
"""Names this plugin has stored, without the namespace prefix."""
|
|
204
|
+
return [name[len(self.prefix):] for name in self._existing_tables(self.prefix + "%")]
|
|
205
|
+
|
|
206
|
+
def drop_all(self) -> None:
|
|
207
|
+
"""Remove every table this plugin owns for this datasource.
|
|
208
|
+
|
|
209
|
+
The uninstall path that did not exist before namespacing. Includes the
|
|
210
|
+
plugin's pre-namespacing table when it declared one: that table is this
|
|
211
|
+
plugin's too, so leaving it behind would defeat the point.
|
|
212
|
+
"""
|
|
213
|
+
names = [self.table_name(name) for name in self.list_tables()]
|
|
214
|
+
if self._legacy_state_table and self._has_table(self._legacy_state_table):
|
|
215
|
+
names.append(self._legacy_state_table)
|
|
216
|
+
self._drop_tables(names)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def store(datasource: str, plugin: str, legacy_state_table: str | None = None) -> PluginStore:
|
|
220
|
+
"""Storage handle for one plugin against one datasource."""
|
|
221
|
+
return PluginStore(datasource, plugin, legacy_state_table=legacy_state_table)
|