plexora 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- plexora/__init__.py +129 -0
- plexora/__main__.py +15 -0
- plexora/_url.py +105 -0
- plexora/api/__init__.py +55 -0
- plexora/api/dataset.py +398 -0
- plexora/api/http.py +19 -0
- plexora/api/plugin.py +541 -0
- plexora/api/store.py +221 -0
- plexora/cli.py +777 -0
- plexora/client/dist/354_bundle.js +2 -0
- plexora/client/dist/354_bundle.js.LICENSE.txt +20 -0
- plexora/client/dist/418_bundle.js +2 -0
- plexora/client/dist/418_bundle.js.LICENSE.txt +1 -0
- plexora/client/dist/693b935cb1f907814be9c6199a68c217.svg +19 -0
- plexora/client/dist/713c134ff47cd328a5b711526f151082.svg +18 -0
- plexora/client/dist/770_bundle.js +1 -0
- plexora/client/dist/ff743f408972e0e96ca9153ed5ae4c83.svg +23 -0
- plexora/client/dist/vendor_bundle.js +2 -0
- plexora/client/dist/vendor_bundle.js.LICENSE.txt +308 -0
- plexora/client/external/openseadragon-bin-2.4.0/LICENSE.txt +28 -0
- plexora/client/external/openseadragon-bin-2.4.0/canvas-overlay-hd.js +140 -0
- plexora/client/external/openseadragon-bin-2.4.0/changelog.txt +501 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/.gitattributes +17 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/LICENSE.txt +116 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/Toolbar.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/fullpage.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/home.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/next.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/previous.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/rotateleft.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/rotateright.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/zoom.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/README.md +9 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-scalebar.js +562 -0
- plexora/client/src/css/import.css +474 -0
- plexora/client/src/css/main.css +903 -0
- plexora/client/src/css/openProject.css +363 -0
- plexora/client/src/css/quickView.css +166 -0
- plexora/client/src/css/tokens.css +51 -0
- plexora/client/src/css/viewer.css +1655 -0
- plexora/client/src/img/apple-touch-icon.png +0 -0
- plexora/client/src/img/favicon.ico +0 -0
- plexora/client/src/img/logo.ai +8892 -33
- plexora/client/src/img/logo.svg +1 -0
- plexora/client/src/img/logo_with_text.ai +9011 -34
- plexora/client/src/img/logo_with_text.svg +1 -0
- plexora/client/src/js/main.js +704 -0
- plexora/client/src/js/pluginRegistry.js +108 -0
- plexora/client/src/js/services/appStatus.js +286 -0
- plexora/client/src/js/services/browsePicker.js +73 -0
- plexora/client/src/js/services/dataLayer.js +400 -0
- plexora/client/src/js/services/datasetContext.js +143 -0
- plexora/client/src/js/services/glRenderer.js +143 -0
- plexora/client/src/js/services/importFormValidation.js +257 -0
- plexora/client/src/js/services/numericData.js +100 -0
- plexora/client/src/js/services/passVariablesToFrontend.js +20 -0
- plexora/client/src/js/services/simpleEventHandler.js +25 -0
- plexora/client/src/js/vendor.js +29 -0
- plexora/client/src/js/views/channelList.js +663 -0
- plexora/client/src/js/views/colorSwatchPicker.js +202 -0
- plexora/client/src/js/views/columnClassifier.js +146 -0
- plexora/client/src/js/views/coordinateField.js +133 -0
- plexora/client/src/js/views/dataSourceField.js +276 -0
- plexora/client/src/js/views/imageViewer.js +3306 -0
- plexora/client/src/js/views/miniMap.js +787 -0
- plexora/client/src/js/views/navbarControls.js +178 -0
- plexora/client/src/js/views/openProjectPage.js +239 -0
- plexora/client/src/js/views/projectEdit.js +335 -0
- plexora/client/src/js/views/quickViewLanding.js +136 -0
- plexora/client/src/js/views/rainbow.js +326 -0
- plexora/client/src/js/views/requirementsModal.js +572 -0
- plexora/client/src/js/views/rgbImageViewer.js +115 -0
- plexora/client/src/js/views/searchableSelect.js +411 -0
- plexora/client/src/js/views/segmentationProgress.js +122 -0
- plexora/client/src/js/views/toolLoader.js +747 -0
- plexora/client/src/js/views/viewerControls.js +689 -0
- plexora/client/src/js/views/viewerManager.js +434 -0
- plexora/client/src/js/views/viewerSidebar.js +1117 -0
- plexora/client/src/js/workers/tileDecoder.js +130 -0
- plexora/client/src/shaders/frag.glsl +528 -0
- plexora/client/src/shaders/vert.glsl +12 -0
- plexora/client/templates/base.html +212 -0
- plexora/client/templates/index.html +238 -0
- plexora/client/templates/open_project.html +96 -0
- plexora/client/templates/project_columns.html +86 -0
- plexora/client/templates/project_edit.html +196 -0
- plexora/client/templates/upload.html +118 -0
- plexora/connect.py +528 -0
- plexora/datasource.py +782 -0
- plexora/jupyter.py +406 -0
- plexora/notebook_env.py +232 -0
- plexora/paths.py +486 -0
- plexora/plugins/__init__.py +12 -0
- plexora/plugins/cell_explorer/__init__.py +100 -0
- plexora/plugins/cell_explorer/server/__init__.py +11 -0
- plexora/plugins/cell_explorer/server/routes.py +167 -0
- plexora/plugins/cell_explorer/server/state.py +281 -0
- plexora/plugins/cell_explorer/server/values.py +145 -0
- plexora/plugins/cell_explorer/server/variables.py +367 -0
- plexora/plugins/cell_explorer/static/cellExplorerApi.js +158 -0
- plexora/plugins/cell_explorer/static/cellExplorerColors.js +316 -0
- plexora/plugins/cell_explorer/static/cellExplorerContinuous.js +403 -0
- plexora/plugins/cell_explorer/static/cellExplorerFigureBridge.js +133 -0
- plexora/plugins/cell_explorer/static/cellExplorerLegend.js +188 -0
- plexora/plugins/cell_explorer/static/cellExplorerRoiBridge.js +822 -0
- plexora/plugins/cell_explorer/static/cellExplorerSidebarController.js +826 -0
- plexora/plugins/cell_explorer/static/cellExplorerState.js +372 -0
- plexora/plugins/cell_explorer/static/cell_explorer.css +752 -0
- plexora/plugins/cell_explorer/templates/cell_explorer/panel.html +137 -0
- plexora/plugins/figure_builder/__init__.py +125 -0
- plexora/plugins/figure_builder/server/__init__.py +7 -0
- plexora/plugins/figure_builder/server/compose.py +365 -0
- plexora/plugins/figure_builder/server/export.py +784 -0
- plexora/plugins/figure_builder/server/export_jobs.py +148 -0
- plexora/plugins/figure_builder/server/operations.py +556 -0
- plexora/plugins/figure_builder/server/pixels.py +142 -0
- plexora/plugins/figure_builder/server/provenance.py +174 -0
- plexora/plugins/figure_builder/server/render.py +278 -0
- plexora/plugins/figure_builder/server/repository.py +701 -0
- plexora/plugins/figure_builder/server/routes.py +594 -0
- plexora/plugins/figure_builder/server/schema.py +1023 -0
- plexora/plugins/figure_builder/server/sources.py +120 -0
- plexora/plugins/figure_builder/server/textmetrics.py +155 -0
- plexora/plugins/figure_builder/static/figureActions.js +363 -0
- plexora/plugins/figure_builder/static/figureBuilderApi.js +255 -0
- plexora/plugins/figure_builder/static/figureCanvas.js +2362 -0
- plexora/plugins/figure_builder/static/figureCaptureBoxes.js +369 -0
- plexora/plugins/figure_builder/static/figureCaptureDock.js +466 -0
- plexora/plugins/figure_builder/static/figureCaptureTool.js +1034 -0
- plexora/plugins/figure_builder/static/figureConfirm.js +136 -0
- plexora/plugins/figure_builder/static/figureContextBar.js +1193 -0
- plexora/plugins/figure_builder/static/figureContextMenu.js +253 -0
- plexora/plugins/figure_builder/static/figureDocumentState.js +275 -0
- plexora/plugins/figure_builder/static/figureExportUi.js +174 -0
- plexora/plugins/figure_builder/static/figureLibrary.js +247 -0
- plexora/plugins/figure_builder/static/figureQuickEdit.js +628 -0
- plexora/plugins/figure_builder/static/figureRichText.js +634 -0
- plexora/plugins/figure_builder/static/figureSceneSnapshot.js +361 -0
- plexora/plugins/figure_builder/static/figureSchema.js +218 -0
- plexora/plugins/figure_builder/static/figureSidebarController.js +1159 -0
- plexora/plugins/figure_builder/static/figureTextEditor.js +503 -0
- plexora/plugins/figure_builder/static/figureTextPanel.js +491 -0
- plexora/plugins/figure_builder/static/figureViewOptions.js +273 -0
- plexora/plugins/figure_builder/static/figureWorkspace.js +2030 -0
- plexora/plugins/figure_builder/static/figure_builder.css +3292 -0
- plexora/plugins/figure_builder/templates/figure_builder/library.html +74 -0
- plexora/plugins/figure_builder/templates/figure_builder/workspace.html +27 -0
- plexora/plugins/figure_builder/templates/figure_builder/workspace_body.html +458 -0
- plexora/plugins/gating/__init__.py +76 -0
- plexora/plugins/gating/server/__init__.py +0 -0
- plexora/plugins/gating/server/anndata_gates.py +367 -0
- plexora/plugins/gating/server/database.py +16 -0
- plexora/plugins/gating/server/model.py +320 -0
- plexora/plugins/gating/server/routes.py +235 -0
- plexora/plugins/gating/static/csvGatingList.js +947 -0
- plexora/plugins/gating/static/gating.css +289 -0
- plexora/plugins/gating/static/gatingApi.js +221 -0
- plexora/plugins/gating/static/gatingSidebarController.js +513 -0
- plexora/plugins/gating/templates/gating/legacy.html +26 -0
- plexora/plugins/gating/templates/gating/panel.html +69 -0
- plexora/plugins/roi/__init__.py +95 -0
- plexora/plugins/roi/server/__init__.py +8 -0
- plexora/plugins/roi/server/adapters.py +655 -0
- plexora/plugins/roi/server/geojson.py +267 -0
- plexora/plugins/roi/server/geometry.py +181 -0
- plexora/plugins/roi/server/mapping.py +169 -0
- plexora/plugins/roi/server/operations.py +293 -0
- plexora/plugins/roi/server/repository.py +215 -0
- plexora/plugins/roi/server/routes.py +409 -0
- plexora/plugins/roi/server/schema.py +323 -0
- plexora/plugins/roi/static/roi.css +583 -0
- plexora/plugins/roi/static/roiApi.js +177 -0
- plexora/plugins/roi/static/roiFigureBridge.js +107 -0
- plexora/plugins/roi/static/roiGeometry.js +350 -0
- plexora/plugins/roi/static/roiRenderer.js +278 -0
- plexora/plugins/roi/static/roiSidebarController.js +1038 -0
- plexora/plugins/roi/static/roiState.js +518 -0
- plexora/plugins/roi/static/roiTools.js +993 -0
- plexora/plugins/roi/templates/roi/panel.html +240 -0
- plexora/proxy.py +57 -0
- plexora/server/models/adapters/__init__.py +94 -0
- plexora/server/models/adapters/anndata_adapter.py +388 -0
- plexora/server/models/adapters/base.py +93 -0
- plexora/server/models/adapters/classify.py +192 -0
- plexora/server/models/adapters/csv_adapter.py +105 -0
- plexora/server/models/adapters/inspection.py +265 -0
- plexora/server/models/adapters/spatialdata_adapter.py +248 -0
- plexora/server/models/centroid_tiles.py +305 -0
- plexora/server/models/data_model.py +1659 -0
- plexora/server/models/database_model.py +169 -0
- plexora/server/models/project.py +1270 -0
- plexora/server/plugins.py +199 -0
- plexora/server/routes/browse_routes.py +41 -0
- plexora/server/routes/data_routes.py +294 -0
- plexora/server/routes/import_routes.py +603 -0
- plexora/server/routes/page_routes.py +139 -0
- plexora/server/routes/project_routes.py +438 -0
- plexora/server/routes/quick_view_routes.py +62 -0
- plexora/server/routes/system_routes.py +39 -0
- plexora/server/routes/tool_routes.py +418 -0
- plexora/server/utils/addHEColumns.py +11 -0
- plexora/server/utils/fast_png.py +97 -0
- plexora/server/utils/fullConversion.py +50 -0
- plexora/server/utils/native_dialog.py +168 -0
- plexora/server/utils/pre_normalization.py +47 -0
- plexora/server/utils/segmentation_pyramid.py +598 -0
- plexora/server/utils/smallestenclosingcircle.py +126 -0
- plexora/server/utils/tiffsurgeon.py +372 -0
- plexora/server_cli.py +48 -0
- plexora/test.py +0 -0
- plexora-0.0.1.dist-info/METADATA +341 -0
- plexora-0.0.1.dist-info/RECORD +248 -0
- plexora-0.0.1.dist-info/WHEEL +5 -0
- plexora-0.0.1.dist-info/entry_points.txt +12 -0
- plexora-0.0.1.dist-info/licenses/LICENSE +207 -0
- plexora-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
"""Figure Builder's HTTP surface.
|
|
2
|
+
|
|
3
|
+
Thin on purpose, like ROI's: parse, hand to the repository, translate the
|
|
4
|
+
outcome. Every rule about what a figure is lives in `schema`/`operations`, and
|
|
5
|
+
every rule about who may overwrite whom lives in `repository`, so there is one
|
|
6
|
+
place to read each and no route can quietly disagree with another.
|
|
7
|
+
|
|
8
|
+
Status codes carry meaning, because the client acts on them differently:
|
|
9
|
+
|
|
10
|
+
400 the request is wrong -- an unknown operation, a panel naming a page
|
|
11
|
+
that does not exist. Retrying it unchanged will fail the same way.
|
|
12
|
+
404 no such figure. Distinct from 400 because the library shows it
|
|
13
|
+
differently: a figure that has been deleted is not a bad request.
|
|
14
|
+
409 the request was fine but somebody else wrote first. The client has
|
|
15
|
+
work worth keeping, so this opens the conflict prompt rather than
|
|
16
|
+
discarding either side.
|
|
17
|
+
422 the stored figure cannot be read by this build -- damaged, or written
|
|
18
|
+
by a newer Plexora. Nothing is written and nothing is drawn.
|
|
19
|
+
|
|
20
|
+
One route here is a PAGE rather than an API: `/figure/<id>` renders the
|
|
21
|
+
workspace for a figure opened without a project. A figure spans datasources, so
|
|
22
|
+
requiring one before it can be opened would make "open my figure" mean "guess
|
|
23
|
+
which of its four images you meant".
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
from flask import Blueprint, Response, jsonify, render_template, request, send_file
|
|
29
|
+
|
|
30
|
+
from plexora import api, app
|
|
31
|
+
from plexora.plugins.figure_builder import PLUGIN, VERSION
|
|
32
|
+
from plexora.plugins.figure_builder.server import pixels, render, repository, schema, sources
|
|
33
|
+
from plexora.plugins.figure_builder.server.repository import ConflictError, UnknownFigure
|
|
34
|
+
|
|
35
|
+
figure_builder_bp = Blueprint(
|
|
36
|
+
'figure_builder', __name__,
|
|
37
|
+
template_folder='../templates',
|
|
38
|
+
static_folder='../static',
|
|
39
|
+
static_url_path='/static',
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
#: Biggest JSON body accepted. A batch of layout edits is kilobytes; a document
|
|
43
|
+
#: replace for a 200-panel figure is a few megabytes. Past this the request is a
|
|
44
|
+
#: mistake, and the client is told so rather than the server parsing it.
|
|
45
|
+
MAX_BODY_BYTES = 64 * 1024 * 1024
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _payload():
|
|
49
|
+
if request.content_length and request.content_length > MAX_BODY_BYTES:
|
|
50
|
+
raise ValueError(f"request body is larger than {MAX_BODY_BYTES // (1024 * 1024)} MB")
|
|
51
|
+
body = request.get_json(silent=True)
|
|
52
|
+
if not isinstance(body, dict):
|
|
53
|
+
raise ValueError("expected a JSON object")
|
|
54
|
+
return body
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _not_found():
|
|
58
|
+
return jsonify(success=False, error="unknown_figure"), 404
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _unreadable(exc):
|
|
62
|
+
return jsonify(success=False, error="unreadable_figure", detail=str(exc)), 422
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _conflict(exc):
|
|
66
|
+
return jsonify(success=False, error="stale_revision", revision=exc.current_revision), 409
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# -- the library ---------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@figure_builder_bp.route('/api/figures', methods=['GET'])
|
|
73
|
+
def list_figures():
|
|
74
|
+
"""Every figure on this machine.
|
|
75
|
+
|
|
76
|
+
Deliberately needs no datasource: the Figures tab is reachable with nothing
|
|
77
|
+
open, and this route existing is also how the Open Project page
|
|
78
|
+
feature-detects the plugin (a 404 means core-only, and the tab stays
|
|
79
|
+
hidden).
|
|
80
|
+
"""
|
|
81
|
+
return api.json_response({"success": True, "figures": repository.list_figures()})
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@figure_builder_bp.route('/api/figures', methods=['POST'])
|
|
85
|
+
def create_figure():
|
|
86
|
+
try:
|
|
87
|
+
body = _payload()
|
|
88
|
+
except ValueError as exc:
|
|
89
|
+
return jsonify(success=False, error=str(exc)), 400
|
|
90
|
+
|
|
91
|
+
figure_id = repository.create(title=body.get('title'))
|
|
92
|
+
return api.json_response({"success": True, "figure_id": figure_id,
|
|
93
|
+
"document": repository.load(figure_id)})
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@figure_builder_bp.route('/api/figures/<figure_id>', methods=['GET'])
|
|
97
|
+
def get_figure(figure_id):
|
|
98
|
+
"""The whole document, plus each source's status right now.
|
|
99
|
+
|
|
100
|
+
Status is computed on read rather than stored, because it is a statement
|
|
101
|
+
about the world outside this figure and the world changes without telling
|
|
102
|
+
the figure. Stored `status` in the document is only ever what the user last
|
|
103
|
+
acknowledged.
|
|
104
|
+
"""
|
|
105
|
+
try:
|
|
106
|
+
schema.validate_figure_id(figure_id)
|
|
107
|
+
document = repository.load(figure_id)
|
|
108
|
+
except ValueError as exc:
|
|
109
|
+
return jsonify(success=False, error=str(exc)), 400
|
|
110
|
+
except UnknownFigure:
|
|
111
|
+
return _not_found()
|
|
112
|
+
except schema.UnreadableFigure as exc:
|
|
113
|
+
return _unreadable(exc)
|
|
114
|
+
|
|
115
|
+
return api.json_response({
|
|
116
|
+
"success": True,
|
|
117
|
+
"document": document,
|
|
118
|
+
"source_status": {source_id: sources.status_of(source)
|
|
119
|
+
for source_id, source in document["sources"].items()},
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@figure_builder_bp.route('/api/figures/<figure_id>', methods=['PATCH'])
|
|
124
|
+
def patch_figure(figure_id):
|
|
125
|
+
"""Apply a batch of edits, if the client is writing against the revision it
|
|
126
|
+
last read. The batch is all-or-nothing, and is one undo step."""
|
|
127
|
+
try:
|
|
128
|
+
schema.validate_figure_id(figure_id)
|
|
129
|
+
body = _payload()
|
|
130
|
+
except ValueError as exc:
|
|
131
|
+
return jsonify(success=False, error=str(exc)), 400
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
revision = repository.apply(figure_id, body.get('base_revision'), body.get('operations'))
|
|
135
|
+
except UnknownFigure:
|
|
136
|
+
return _not_found()
|
|
137
|
+
except ConflictError as exc:
|
|
138
|
+
return _conflict(exc)
|
|
139
|
+
except schema.UnreadableFigure as exc:
|
|
140
|
+
return _unreadable(exc)
|
|
141
|
+
except ValueError as exc:
|
|
142
|
+
return jsonify(success=False, error=str(exc)), 400
|
|
143
|
+
|
|
144
|
+
return jsonify(success=True, revision=revision)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@figure_builder_bp.route('/api/figures/<figure_id>', methods=['PUT'])
|
|
148
|
+
def replace_figure(figure_id):
|
|
149
|
+
"""Store a whole document. For import and recovery only -- ordinary editing
|
|
150
|
+
goes through PATCH, which is undoable and journalled."""
|
|
151
|
+
try:
|
|
152
|
+
schema.validate_figure_id(figure_id)
|
|
153
|
+
body = _payload()
|
|
154
|
+
except ValueError as exc:
|
|
155
|
+
return jsonify(success=False, error=str(exc)), 400
|
|
156
|
+
|
|
157
|
+
try:
|
|
158
|
+
revision = repository.replace(figure_id, body.get('base_revision'), body.get('document'))
|
|
159
|
+
except UnknownFigure:
|
|
160
|
+
return _not_found()
|
|
161
|
+
except ConflictError as exc:
|
|
162
|
+
return _conflict(exc)
|
|
163
|
+
except schema.UnreadableFigure as exc:
|
|
164
|
+
return _unreadable(exc)
|
|
165
|
+
except ValueError as exc:
|
|
166
|
+
return jsonify(success=False, error=str(exc)), 400
|
|
167
|
+
|
|
168
|
+
return jsonify(success=True, revision=revision)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@figure_builder_bp.route('/api/figures/<figure_id>', methods=['DELETE'])
|
|
172
|
+
def delete_figure(figure_id):
|
|
173
|
+
try:
|
|
174
|
+
schema.validate_figure_id(figure_id)
|
|
175
|
+
repository.delete(figure_id)
|
|
176
|
+
except ValueError as exc:
|
|
177
|
+
return jsonify(success=False, error=str(exc)), 400
|
|
178
|
+
except UnknownFigure:
|
|
179
|
+
return _not_found()
|
|
180
|
+
return jsonify(success=True)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@figure_builder_bp.route('/api/figures/<figure_id>/duplicate', methods=['POST'])
|
|
184
|
+
def duplicate_figure(figure_id):
|
|
185
|
+
"""A copy, previews and imported assets included.
|
|
186
|
+
|
|
187
|
+
A file copy rather than a document round trip: re-rendering every preview
|
|
188
|
+
for a duplicate the user made in order to try a different layout would be
|
|
189
|
+
minutes of work to produce pixels that already exist.
|
|
190
|
+
"""
|
|
191
|
+
try:
|
|
192
|
+
schema.validate_figure_id(figure_id)
|
|
193
|
+
body = request.get_json(silent=True) or {}
|
|
194
|
+
new_id = repository.duplicate(figure_id, title=body.get('title'))
|
|
195
|
+
except ValueError as exc:
|
|
196
|
+
return jsonify(success=False, error=str(exc)), 400
|
|
197
|
+
except UnknownFigure:
|
|
198
|
+
return _not_found()
|
|
199
|
+
except schema.UnreadableFigure as exc:
|
|
200
|
+
return _unreadable(exc)
|
|
201
|
+
return jsonify(success=True, figure_id=new_id)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
# -- rasters -------------------------------------------------------------
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _image_response(data, media_type):
|
|
208
|
+
"""A raster the browser may cache only after revalidating.
|
|
209
|
+
|
|
210
|
+
`no-cache` rather than a max-age: a preview is replaced in place whenever
|
|
211
|
+
its panel changes, at the same URL, and a cached copy would show the user
|
|
212
|
+
the view they just edited away from.
|
|
213
|
+
"""
|
|
214
|
+
return Response(data, mimetype=media_type,
|
|
215
|
+
headers={"Cache-Control": "no-cache, must-revalidate"})
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
@figure_builder_bp.route('/api/figures/<figure_id>/previews/<panel_id>', methods=['GET'])
|
|
219
|
+
def get_preview(figure_id, panel_id):
|
|
220
|
+
try:
|
|
221
|
+
schema.validate_figure_id(figure_id)
|
|
222
|
+
found = repository.get_preview(figure_id, panel_id)
|
|
223
|
+
except ValueError as exc:
|
|
224
|
+
return jsonify(success=False, error=str(exc)), 400
|
|
225
|
+
except UnknownFigure:
|
|
226
|
+
return _not_found()
|
|
227
|
+
if found is None:
|
|
228
|
+
return jsonify(success=False, error="no_preview"), 404
|
|
229
|
+
data, fmt, _ = found
|
|
230
|
+
return _image_response(data, f"image/{fmt}")
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@figure_builder_bp.route('/api/figures/<figure_id>/previews/<panel_id>', methods=['POST'])
|
|
234
|
+
def put_preview(figure_id, panel_id):
|
|
235
|
+
"""Store a panel's preview raster.
|
|
236
|
+
|
|
237
|
+
The body is the image itself rather than JSON with a base64 field: a WebP
|
|
238
|
+
goes over the wire as bytes, and base64 would add a third to every panel
|
|
239
|
+
the user captures. The render revision rides on the query string, and a
|
|
240
|
+
render older than the one already stored is refused -- see
|
|
241
|
+
repository.put_preview.
|
|
242
|
+
"""
|
|
243
|
+
try:
|
|
244
|
+
schema.validate_figure_id(figure_id)
|
|
245
|
+
render_revision = int(request.args.get('render_revision', '0'))
|
|
246
|
+
stored = repository.put_preview(
|
|
247
|
+
figure_id, panel_id, render_revision, request.get_data(),
|
|
248
|
+
width=int(request.args.get('width', '0') or 0),
|
|
249
|
+
height=int(request.args.get('height', '0') or 0),
|
|
250
|
+
)
|
|
251
|
+
except (TypeError, ValueError) as exc:
|
|
252
|
+
return jsonify(success=False, error=str(exc)), 400
|
|
253
|
+
except UnknownFigure:
|
|
254
|
+
return _not_found()
|
|
255
|
+
return jsonify(success=True, stored=stored)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
@figure_builder_bp.route('/api/figures/<figure_id>/thumbnail', methods=['GET'])
|
|
259
|
+
def get_thumbnail(figure_id):
|
|
260
|
+
try:
|
|
261
|
+
schema.validate_figure_id(figure_id)
|
|
262
|
+
found = repository.get_thumbnail(figure_id)
|
|
263
|
+
except ValueError as exc:
|
|
264
|
+
return jsonify(success=False, error=str(exc)), 400
|
|
265
|
+
except UnknownFigure:
|
|
266
|
+
return _not_found()
|
|
267
|
+
if found is None:
|
|
268
|
+
return jsonify(success=False, error="no_thumbnail"), 404
|
|
269
|
+
data, fmt = found
|
|
270
|
+
return _image_response(data, f"image/{fmt}")
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
@figure_builder_bp.route('/api/figures/<figure_id>/thumbnail', methods=['PUT'])
|
|
274
|
+
def put_thumbnail(figure_id):
|
|
275
|
+
try:
|
|
276
|
+
schema.validate_figure_id(figure_id)
|
|
277
|
+
repository.put_thumbnail(figure_id, request.get_data())
|
|
278
|
+
except ValueError as exc:
|
|
279
|
+
return jsonify(success=False, error=str(exc)), 400
|
|
280
|
+
except UnknownFigure:
|
|
281
|
+
return _not_found()
|
|
282
|
+
return jsonify(success=True)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
# -- figure-only assets --------------------------------------------------
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
@figure_builder_bp.route('/api/figures/<figure_id>/assets', methods=['POST'])
|
|
289
|
+
def add_asset(figure_id):
|
|
290
|
+
"""Import an image into this figure and nowhere else.
|
|
291
|
+
|
|
292
|
+
The filename comes from the query string and the bytes from the body, so
|
|
293
|
+
nothing here parses a multipart form -- the client already has the file as
|
|
294
|
+
a Blob and this is the cheapest thing to send it as.
|
|
295
|
+
"""
|
|
296
|
+
try:
|
|
297
|
+
schema.validate_figure_id(figure_id)
|
|
298
|
+
asset = repository.import_asset(
|
|
299
|
+
figure_id, request.args.get('filename', ''), request.get_data())
|
|
300
|
+
except ValueError as exc:
|
|
301
|
+
return jsonify(success=False, error=str(exc)), 400
|
|
302
|
+
except UnknownFigure:
|
|
303
|
+
return _not_found()
|
|
304
|
+
return jsonify(success=True, **asset)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
@figure_builder_bp.route('/api/figures/<figure_id>/assets/<asset_id>', methods=['GET'])
|
|
308
|
+
def get_asset(figure_id, asset_id):
|
|
309
|
+
try:
|
|
310
|
+
schema.validate_figure_id(figure_id)
|
|
311
|
+
path = repository.asset_path(figure_id, asset_id)
|
|
312
|
+
except ValueError as exc:
|
|
313
|
+
return jsonify(success=False, error=str(exc)), 400
|
|
314
|
+
if path is None:
|
|
315
|
+
return jsonify(success=False, error="no_such_asset"), 404
|
|
316
|
+
return send_file(str(path), conditional=True)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
# -- sources -------------------------------------------------------------
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
@figure_builder_bp.route('/api/sources/<datasource>', methods=['GET'])
|
|
323
|
+
def describe_source(datasource):
|
|
324
|
+
"""What this project looks like right now, for capture and for relinking.
|
|
325
|
+
|
|
326
|
+
Reads the project record only. Nothing here loads a datasource, so asking
|
|
327
|
+
about eight sources in a row cannot evict the one the user is looking at.
|
|
328
|
+
"""
|
|
329
|
+
try:
|
|
330
|
+
return api.json_response({"success": True, "source": sources.describe(datasource)})
|
|
331
|
+
except KeyError:
|
|
332
|
+
return jsonify(success=False, error="unknown_datasource"), 404
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
# -- source pixels, for Quick Edit ----------------------------------------
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
@figure_builder_bp.route(
|
|
339
|
+
'/api/figures/<figure_id>/sources/<source_id>/pixels', methods=['GET'])
|
|
340
|
+
def source_pixels(figure_id, source_id):
|
|
341
|
+
"""One channel of one region of a figure's source, as raw numbers.
|
|
342
|
+
|
|
343
|
+
For Quick Edit's mini viewer, which composites in the browser so that
|
|
344
|
+
changing a colour or dragging a contrast slider costs no request at all.
|
|
345
|
+
See server/pixels.py for why this exists rather than reusing the viewer's
|
|
346
|
+
own tile routes.
|
|
347
|
+
|
|
348
|
+
The body is uint16 little-endian, row-major, with the shape and the
|
|
349
|
+
actually-read box in headers: a region that ran off the edge of the slide
|
|
350
|
+
has to be drawn in the right PLACE, and a caller given only the pixels
|
|
351
|
+
would centre the wrong thing.
|
|
352
|
+
"""
|
|
353
|
+
try:
|
|
354
|
+
schema.validate_figure_id(figure_id)
|
|
355
|
+
document = repository.load(figure_id)
|
|
356
|
+
except ValueError as exc:
|
|
357
|
+
return jsonify(success=False, error=str(exc)), 400
|
|
358
|
+
except UnknownFigure:
|
|
359
|
+
return _not_found()
|
|
360
|
+
except schema.UnreadableFigure as exc:
|
|
361
|
+
return _unreadable(exc)
|
|
362
|
+
|
|
363
|
+
source = document["sources"].get(source_id)
|
|
364
|
+
if source is None:
|
|
365
|
+
return jsonify(success=False, error="unknown_source"), 404
|
|
366
|
+
if source["kind"] != "plexora_project" or not source["datasource"]:
|
|
367
|
+
return jsonify(success=False, error="not_a_project_image"), 400
|
|
368
|
+
|
|
369
|
+
try:
|
|
370
|
+
channel = request.args.get("channel") or ""
|
|
371
|
+
box = tuple(float(request.args.get(name, 0)) for name in ("x", "y", "w", "h"))
|
|
372
|
+
out = (int(request.args.get("out_w", 0)), int(request.args.get("out_h", 0)))
|
|
373
|
+
except (TypeError, ValueError):
|
|
374
|
+
return jsonify(success=False, error="bad_region"), 400
|
|
375
|
+
if box[2] <= 0 or box[3] <= 0:
|
|
376
|
+
return jsonify(success=False, error="bad_region"), 400
|
|
377
|
+
|
|
378
|
+
try:
|
|
379
|
+
array, clipped = pixels.read_region(source["datasource"], channel, box, out)
|
|
380
|
+
except render.RenderError as exc:
|
|
381
|
+
return jsonify(success=False, error=str(exc)), 400
|
|
382
|
+
except KeyError:
|
|
383
|
+
return jsonify(success=False, error="unknown_datasource"), 404
|
|
384
|
+
|
|
385
|
+
return Response(
|
|
386
|
+
array.astype('<u2').tobytes(), mimetype="application/octet-stream",
|
|
387
|
+
headers={
|
|
388
|
+
"X-Fb-Shape": f"{array.shape[1]}x{array.shape[0]}",
|
|
389
|
+
"X-Fb-Box": ",".join(f"{value:.3f}" for value in clipped),
|
|
390
|
+
# Same reasoning as a preview: the pixels behind one URL change
|
|
391
|
+
# whenever the panel does.
|
|
392
|
+
"Cache-Control": "no-cache, must-revalidate",
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
@figure_builder_bp.route(
|
|
397
|
+
'/api/figures/<figure_id>/sources/<source_id>/pixel_info', methods=['GET'])
|
|
398
|
+
def source_pixel_info(figure_id, source_id):
|
|
399
|
+
"""A channel's intensity range, for a contrast slider's domain."""
|
|
400
|
+
try:
|
|
401
|
+
schema.validate_figure_id(figure_id)
|
|
402
|
+
document = repository.load(figure_id)
|
|
403
|
+
except ValueError as exc:
|
|
404
|
+
return jsonify(success=False, error=str(exc)), 400
|
|
405
|
+
except UnknownFigure:
|
|
406
|
+
return _not_found()
|
|
407
|
+
except schema.UnreadableFigure as exc:
|
|
408
|
+
return _unreadable(exc)
|
|
409
|
+
|
|
410
|
+
source = document["sources"].get(source_id)
|
|
411
|
+
if source is None:
|
|
412
|
+
return jsonify(success=False, error="unknown_source"), 404
|
|
413
|
+
if source["kind"] != "plexora_project" or not source["datasource"]:
|
|
414
|
+
return jsonify(success=False, error="not_a_project_image"), 400
|
|
415
|
+
|
|
416
|
+
try:
|
|
417
|
+
stats = pixels.channel_stats(source["datasource"], request.args.get("channel") or "")
|
|
418
|
+
except render.RenderError as exc:
|
|
419
|
+
return jsonify(success=False, error=str(exc)), 400
|
|
420
|
+
except KeyError:
|
|
421
|
+
return jsonify(success=False, error="unknown_datasource"), 404
|
|
422
|
+
return api.json_response({"success": True, "stats": stats})
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
# -- export --------------------------------------------------------------
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
@figure_builder_bp.route('/api/figures/<figure_id>/export/preflight', methods=['POST'])
|
|
429
|
+
def export_preflight(figure_id):
|
|
430
|
+
"""What the user should know before committing to an export.
|
|
431
|
+
|
|
432
|
+
Answered before a single pixel is rendered, so "this panel is 96 DPI at that
|
|
433
|
+
size" and "this channel is gone" arrive while the dialog is open rather than
|
|
434
|
+
as a warning attached to a file that is already written.
|
|
435
|
+
"""
|
|
436
|
+
from plexora.plugins.figure_builder.server import export
|
|
437
|
+
|
|
438
|
+
try:
|
|
439
|
+
schema.validate_figure_id(figure_id)
|
|
440
|
+
body = _payload()
|
|
441
|
+
document = repository.load(figure_id)
|
|
442
|
+
except ValueError as exc:
|
|
443
|
+
return jsonify(success=False, error=str(exc)), 400
|
|
444
|
+
except UnknownFigure:
|
|
445
|
+
return _not_found()
|
|
446
|
+
except schema.UnreadableFigure as exc:
|
|
447
|
+
return _unreadable(exc)
|
|
448
|
+
|
|
449
|
+
return api.json_response({"success": True, **export.preflight(document, body)})
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
@figure_builder_bp.route('/api/figures/<figure_id>/export', methods=['POST'])
|
|
453
|
+
def start_export(figure_id):
|
|
454
|
+
"""Begin an export and answer with a job id.
|
|
455
|
+
|
|
456
|
+
The document is read HERE and handed to the job, so the export renders one
|
|
457
|
+
revision throughout. A figure edited while an export runs must not produce a
|
|
458
|
+
file that is half of one layout and half of another.
|
|
459
|
+
"""
|
|
460
|
+
from plexora.plugins.figure_builder.server import export, export_jobs
|
|
461
|
+
|
|
462
|
+
try:
|
|
463
|
+
schema.validate_figure_id(figure_id)
|
|
464
|
+
body = _payload()
|
|
465
|
+
document = repository.load(figure_id)
|
|
466
|
+
except ValueError as exc:
|
|
467
|
+
return jsonify(success=False, error=str(exc)), 400
|
|
468
|
+
except UnknownFigure:
|
|
469
|
+
return _not_found()
|
|
470
|
+
except schema.UnreadableFigure as exc:
|
|
471
|
+
return _unreadable(exc)
|
|
472
|
+
|
|
473
|
+
if body.get("format", "pdf") not in export.FORMATS:
|
|
474
|
+
return jsonify(success=False, error="unknown_format",
|
|
475
|
+
formats=list(export.FORMATS)), 400
|
|
476
|
+
if not any(panel["placement"] for panel in document["panels"].values()):
|
|
477
|
+
return jsonify(success=False,
|
|
478
|
+
error="This figure has no panels on a page yet."), 400
|
|
479
|
+
|
|
480
|
+
job_id = export_jobs.start(figure_id, document, body)
|
|
481
|
+
return jsonify(success=True, job_id=job_id)
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
@figure_builder_bp.route('/api/figures/<figure_id>/export/<job_id>', methods=['GET'])
|
|
485
|
+
def export_status(figure_id, job_id):
|
|
486
|
+
from plexora.plugins.figure_builder.server import export_jobs
|
|
487
|
+
|
|
488
|
+
job = export_jobs.get(job_id)
|
|
489
|
+
if job is None or job["figure_id"] != figure_id:
|
|
490
|
+
return jsonify(success=False, error="unknown_job"), 404
|
|
491
|
+
if job["status"] == "unavailable":
|
|
492
|
+
# A format this build cannot write. 501 rather than 500: the request was
|
|
493
|
+
# fine and the answer is an install line, not a bug report.
|
|
494
|
+
return jsonify(success=False, error="format_unavailable",
|
|
495
|
+
detail=job["error"], job=job), 501
|
|
496
|
+
return api.json_response({"success": True, "job": job})
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
@figure_builder_bp.route('/api/figures/<figure_id>/export/<job_id>', methods=['DELETE'])
|
|
500
|
+
def cancel_export(figure_id, job_id):
|
|
501
|
+
"""Ask a running export to stop.
|
|
502
|
+
|
|
503
|
+
Cancelling changes nothing about the figure -- it is a render, not an edit
|
|
504
|
+
-- and anything half-written is removed, because a partial PDF in the
|
|
505
|
+
downloads folder is worse than no PDF: it looks finished.
|
|
506
|
+
"""
|
|
507
|
+
from plexora.plugins.figure_builder.server import export_jobs
|
|
508
|
+
|
|
509
|
+
job = export_jobs.get(job_id)
|
|
510
|
+
if job is None or job["figure_id"] != figure_id:
|
|
511
|
+
return jsonify(success=False, error="unknown_job"), 404
|
|
512
|
+
export_jobs.cancel(job_id)
|
|
513
|
+
return jsonify(success=True)
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
@figure_builder_bp.route('/api/figures/<figure_id>/export/<job_id>/download', methods=['GET'])
|
|
517
|
+
def download_export(figure_id, job_id):
|
|
518
|
+
from plexora.plugins.figure_builder.server import export_jobs
|
|
519
|
+
|
|
520
|
+
job = export_jobs.get(job_id)
|
|
521
|
+
if job is None or job["figure_id"] != figure_id:
|
|
522
|
+
return jsonify(success=False, error="unknown_job"), 404
|
|
523
|
+
path = export_jobs.download_path(job_id)
|
|
524
|
+
if not path:
|
|
525
|
+
return jsonify(success=False, error="not_ready"), 409
|
|
526
|
+
return send_file(path, as_attachment=True)
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
# -- pages of its own ----------------------------------------------------
|
|
530
|
+
#
|
|
531
|
+
# Two pages, neither of which is about a datasource. A figure spans several, so
|
|
532
|
+
# requiring one before it can be opened would make "open my figure" mean "guess
|
|
533
|
+
# which of its four images you meant" -- and the library is reached most often
|
|
534
|
+
# from the state where nothing at all is open.
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
def _page_data(**values):
|
|
538
|
+
"""The context base.html reads, built here rather than through core's
|
|
539
|
+
`template_data`.
|
|
540
|
+
|
|
541
|
+
That looks like duplication and is the boundary working: these are the
|
|
542
|
+
plugin's own pages, base.html is the only thing they inherit, and a plugin
|
|
543
|
+
importing `plexora.server.routes.page_routes` for a helper is a plugin that
|
|
544
|
+
breaks when core reorganises its routes. The keys below are exactly the ones
|
|
545
|
+
base.html reads, and the asset lists come from the descriptor so they cannot
|
|
546
|
+
drift from what the tool path serves.
|
|
547
|
+
"""
|
|
548
|
+
from plexora import get_config_names
|
|
549
|
+
from plexora.server import plugins as plugin_registry
|
|
550
|
+
|
|
551
|
+
base_url = app.config.get('PLEXORA_BASE_URL', '')
|
|
552
|
+
data = {
|
|
553
|
+
'datasource': '',
|
|
554
|
+
'datasources': get_config_names(),
|
|
555
|
+
'is_docker': app.config.get('IS_DOCKER', False),
|
|
556
|
+
'base_url': base_url,
|
|
557
|
+
'image_kind': '',
|
|
558
|
+
'active_tool': '',
|
|
559
|
+
'available_tools': [],
|
|
560
|
+
'active_tool_scripts': PLUGIN.asset_urls('scripts', base_url),
|
|
561
|
+
'active_tool_styles': PLUGIN.asset_urls('styles', base_url),
|
|
562
|
+
'active_tool_panels': {},
|
|
563
|
+
# base.html renders the File menu on every page, this one included --
|
|
564
|
+
# so the entry that leads here has to be present here too, or the menu
|
|
565
|
+
# loses items the moment the user follows one of them.
|
|
566
|
+
'plugin_nav_items': plugin_registry.nav_items(app, base_url),
|
|
567
|
+
'figure_builder_url': f"{base_url}{PLUGIN.url_prefix}",
|
|
568
|
+
'figure_builder_version': VERSION,
|
|
569
|
+
}
|
|
570
|
+
data.update(values)
|
|
571
|
+
return data
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
@figure_builder_bp.route('/figures', methods=['GET'])
|
|
575
|
+
def library_page():
|
|
576
|
+
"""The figure library: every figure on this machine."""
|
|
577
|
+
return render_template('figure_builder/library.html', data=_page_data())
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
@figure_builder_bp.route('/figure/<figure_id>', methods=['GET'])
|
|
581
|
+
def figure_page(figure_id):
|
|
582
|
+
"""One figure, opened with no project."""
|
|
583
|
+
try:
|
|
584
|
+
schema.validate_figure_id(figure_id)
|
|
585
|
+
except ValueError:
|
|
586
|
+
return jsonify(success=False, error="unknown_figure"), 404
|
|
587
|
+
if not repository.exists(figure_id):
|
|
588
|
+
return jsonify(success=False, error="unknown_figure"), 404
|
|
589
|
+
|
|
590
|
+
# `figure_id` rides on the page rather than being fetched, so the workspace
|
|
591
|
+
# knows which figure it is without an extra round trip before it can start
|
|
592
|
+
# the one that matters.
|
|
593
|
+
return render_template('figure_builder/workspace.html',
|
|
594
|
+
data=_page_data(figure_id=figure_id))
|