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/jupyter.py
ADDED
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
import atexit
|
|
2
|
+
import html
|
|
3
|
+
import os
|
|
4
|
+
import socket
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
8
|
+
import urllib.request
|
|
9
|
+
import webbrowser
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from plexora._url import join_display
|
|
13
|
+
from plexora.datasource import register_datasource, register_anndata_datasource
|
|
14
|
+
from plexora.notebook_env import COLAB, PORT_PLACEHOLDER, colab_origin, resolve_display
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
_SERVERS = {}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _default_data_dir():
|
|
21
|
+
"""Where a notebook viewer keeps its data when the caller names nowhere.
|
|
22
|
+
|
|
23
|
+
This used to be `Path(__file__).parent / "data"` -- i.e. inside the
|
|
24
|
+
installed package. Under a real install that is site-packages: read-only
|
|
25
|
+
for a system or conda install, destroyed by `pip install -U`, and invisible
|
|
26
|
+
to `pip uninstall`. The one resolver decides now, exactly as it does for
|
|
27
|
+
the CLI, so a notebook and a terminal see the same projects.
|
|
28
|
+
"""
|
|
29
|
+
from plexora import paths
|
|
30
|
+
|
|
31
|
+
return paths.data_root()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _free_port():
|
|
35
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
|
36
|
+
sock.bind(("127.0.0.1", 0))
|
|
37
|
+
return sock.getsockname()[1]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ServerStartError(RuntimeError):
|
|
41
|
+
"""The sidecar process could not be started, with a reason worth reading."""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _wait_until_ready(port, timeout=30, process=None):
|
|
45
|
+
"""Block until the sidecar answers, or explain why it never will.
|
|
46
|
+
|
|
47
|
+
`process` is what makes this useful. Without it a child that died on its
|
|
48
|
+
first line -- the overwhelmingly common case being a `plexora` that is not
|
|
49
|
+
importable from the interpreter running the notebook -- was indistinguishable
|
|
50
|
+
from one that was merely slow, so the notebook cell sat there for the full
|
|
51
|
+
30 seconds and then reported a timeout, which is the one explanation that
|
|
52
|
+
was not true.
|
|
53
|
+
"""
|
|
54
|
+
deadline = time.time() + timeout
|
|
55
|
+
url = f"http://127.0.0.1:{port}/config"
|
|
56
|
+
while time.time() < deadline:
|
|
57
|
+
if process is not None and process.poll() is not None:
|
|
58
|
+
raise ServerStartError(
|
|
59
|
+
f"The Plexora server process exited with code "
|
|
60
|
+
f"{process.returncode} before it was ready.\n"
|
|
61
|
+
f"The usual cause is that `{sys.executable}` cannot import "
|
|
62
|
+
f"plexora -- install it into the environment running this "
|
|
63
|
+
f"kernel, e.g. `pip install -e .` from a source checkout."
|
|
64
|
+
)
|
|
65
|
+
try:
|
|
66
|
+
with urllib.request.urlopen(url, timeout=1) as response:
|
|
67
|
+
if response.status < 500:
|
|
68
|
+
return
|
|
69
|
+
except Exception:
|
|
70
|
+
time.sleep(0.25)
|
|
71
|
+
raise ServerStartError(f"Plexora server did not become ready on port {port}")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _server_key(data_dir, base_url_template, plugins):
|
|
75
|
+
"""What makes two viewers able to share one sidecar.
|
|
76
|
+
|
|
77
|
+
`base_url_template`, not the base URL: in proxy mode the URL contains the
|
|
78
|
+
port, which is freshly chosen on every call, so keying on it meant the
|
|
79
|
+
lookup could never hit and each `plexora.view()` in a notebook started
|
|
80
|
+
another server. Harmless when proxying was opt-in and rare; not harmless
|
|
81
|
+
now that hosted notebooks reach it by default.
|
|
82
|
+
"""
|
|
83
|
+
return (str(Path(data_dir).expanduser().resolve()), base_url_template, plugins)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _start_server(data_dir, base_url_template, port=None, plugins=None):
|
|
87
|
+
"""Start (or reuse) a sidecar; returns `(port, base_url)`.
|
|
88
|
+
|
|
89
|
+
`base_url_template` may contain `{port}`, which is filled in once the port
|
|
90
|
+
is settled -- the proxied mount path has to name the port it is proxying,
|
|
91
|
+
but the port is not known until after the cache has been consulted.
|
|
92
|
+
"""
|
|
93
|
+
key = _server_key(data_dir, base_url_template, plugins)
|
|
94
|
+
existing = _SERVERS.get(key)
|
|
95
|
+
if existing and existing.poll() is None:
|
|
96
|
+
return existing._plexora_port, existing._plexora_base_url
|
|
97
|
+
|
|
98
|
+
# One retry, because the gap between _free_port() releasing a port and the
|
|
99
|
+
# child binding it is a real window on a busy machine, and losing that race
|
|
100
|
+
# is both plausible and entirely recoverable.
|
|
101
|
+
attempts = 2 if port is None else 1
|
|
102
|
+
last = None
|
|
103
|
+
for attempt in range(attempts):
|
|
104
|
+
chosen = port or _free_port()
|
|
105
|
+
base_url = base_url_template.replace(PORT_PLACEHOLDER, str(chosen))
|
|
106
|
+
try:
|
|
107
|
+
process = _spawn_server(data_dir, base_url, chosen, plugins)
|
|
108
|
+
except ServerStartError as exc:
|
|
109
|
+
last = exc
|
|
110
|
+
continue
|
|
111
|
+
process._plexora_port = chosen
|
|
112
|
+
process._plexora_base_url = base_url
|
|
113
|
+
_SERVERS[key] = process
|
|
114
|
+
return chosen, base_url
|
|
115
|
+
raise last
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _spawn_server(data_dir, base_url, port, plugins):
|
|
119
|
+
resolved_data_dir = str(Path(data_dir).expanduser().resolve())
|
|
120
|
+
cmd = [
|
|
121
|
+
sys.executable,
|
|
122
|
+
"-m",
|
|
123
|
+
"plexora.server_cli",
|
|
124
|
+
"--host",
|
|
125
|
+
"127.0.0.1",
|
|
126
|
+
"--port",
|
|
127
|
+
str(port),
|
|
128
|
+
"--data-dir",
|
|
129
|
+
resolved_data_dir,
|
|
130
|
+
"--base-url",
|
|
131
|
+
base_url,
|
|
132
|
+
"--notebook-mode",
|
|
133
|
+
]
|
|
134
|
+
if plugins is not None:
|
|
135
|
+
cmd.extend(["--plugins", plugins])
|
|
136
|
+
# The FLAG is what actually decides the child's plugins: server_cli.main()
|
|
137
|
+
# writes it into its own os.environ before `from plexora import app`, which
|
|
138
|
+
# is in-process and therefore exact. The environment variable below is
|
|
139
|
+
# belt-and-braces and cannot be the mechanism, because `plugins=""` -- a
|
|
140
|
+
# deliberate core-only build -- does not survive a process boundary on
|
|
141
|
+
# Windows at all: setting a variable to "" there deletes it, and the child
|
|
142
|
+
# would read "unset", which means activate everything.
|
|
143
|
+
#
|
|
144
|
+
# The data path has no such constraint either way -- it resolves on demand
|
|
145
|
+
# -- but is passed both ways so parent and child cannot disagree about
|
|
146
|
+
# which directory this viewer is for.
|
|
147
|
+
env = os.environ.copy()
|
|
148
|
+
env["PLEXORA_DATA_PATH"] = resolved_data_dir
|
|
149
|
+
env["PLEXORA_BASE_URL"] = base_url
|
|
150
|
+
env["PLEXORA_NOTEBOOK_MODE"] = "1"
|
|
151
|
+
if plugins is not None:
|
|
152
|
+
env["PLEXORA_PLUGINS"] = plugins
|
|
153
|
+
# No cwd: this used to be the package's parent, which is the repository
|
|
154
|
+
# root only when running from a checkout and site-packages otherwise.
|
|
155
|
+
# Nothing the child does is relative to its working directory any more.
|
|
156
|
+
process = subprocess.Popen(cmd, env=env)
|
|
157
|
+
try:
|
|
158
|
+
_wait_until_ready(port, process=process)
|
|
159
|
+
except ServerStartError:
|
|
160
|
+
if process.poll() is None:
|
|
161
|
+
process.terminate()
|
|
162
|
+
raise
|
|
163
|
+
return process
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _cleanup_servers():
|
|
167
|
+
for process in _SERVERS.values():
|
|
168
|
+
if process.poll() is None:
|
|
169
|
+
process.terminate()
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
atexit.register(_cleanup_servers)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class PlexoraViewer:
|
|
176
|
+
def __init__(
|
|
177
|
+
self,
|
|
178
|
+
datasource,
|
|
179
|
+
data_dir=None,
|
|
180
|
+
proxy="auto",
|
|
181
|
+
height=850,
|
|
182
|
+
width="100%",
|
|
183
|
+
base_url=None,
|
|
184
|
+
plugins=None,
|
|
185
|
+
start=True,
|
|
186
|
+
):
|
|
187
|
+
"""`proxy` is one of:
|
|
188
|
+
|
|
189
|
+
- "auto" (default) -- look at the environment and decide. Local Jupyter
|
|
190
|
+
and VS Code Remote get a direct localhost URL exactly as before;
|
|
191
|
+
JupyterHub, Open OnDemand and Colab get the proxied form they need.
|
|
192
|
+
- True -- always proxy through the notebook server.
|
|
193
|
+
- False -- always use a direct 127.0.0.1 URL.
|
|
194
|
+
|
|
195
|
+
The default changed from False. That was only ever right when the
|
|
196
|
+
browser and the kernel were the same machine; anywhere else it produced
|
|
197
|
+
an iframe pointing at the user's own laptop and rendered blank.
|
|
198
|
+
"""
|
|
199
|
+
self.datasource = datasource
|
|
200
|
+
self.data_dir = Path(data_dir or os.environ.get("PLEXORA_DATA_PATH", _default_data_dir())).expanduser().resolve()
|
|
201
|
+
self.proxy = proxy
|
|
202
|
+
self.height = height
|
|
203
|
+
self.width = width
|
|
204
|
+
self._base_url = base_url
|
|
205
|
+
# Kept as-is (not truthy-or) so plugins="" -- explicitly core-only --
|
|
206
|
+
# stays distinguishable from "not passed, use whatever is installed".
|
|
207
|
+
self.plugins = plugins
|
|
208
|
+
self._port = None
|
|
209
|
+
self._display_base = None
|
|
210
|
+
if start:
|
|
211
|
+
self.start()
|
|
212
|
+
|
|
213
|
+
@classmethod
|
|
214
|
+
def from_files(
|
|
215
|
+
cls,
|
|
216
|
+
name,
|
|
217
|
+
image,
|
|
218
|
+
segmentation,
|
|
219
|
+
features,
|
|
220
|
+
x,
|
|
221
|
+
y,
|
|
222
|
+
id_column="CellID",
|
|
223
|
+
celltype_column=None,
|
|
224
|
+
channel_names=None,
|
|
225
|
+
copy=False,
|
|
226
|
+
data_dir=None,
|
|
227
|
+
**viewer_kwargs,
|
|
228
|
+
):
|
|
229
|
+
resolved_data_dir = Path(data_dir or os.environ.get("PLEXORA_DATA_PATH", _default_data_dir())).expanduser().resolve()
|
|
230
|
+
register_datasource(
|
|
231
|
+
name=name,
|
|
232
|
+
image=image,
|
|
233
|
+
segmentation=segmentation,
|
|
234
|
+
features=features,
|
|
235
|
+
x=x,
|
|
236
|
+
y=y,
|
|
237
|
+
id_column=id_column,
|
|
238
|
+
celltype_column=celltype_column,
|
|
239
|
+
channel_names=channel_names,
|
|
240
|
+
copy=copy,
|
|
241
|
+
data_dir=resolved_data_dir,
|
|
242
|
+
)
|
|
243
|
+
return cls(datasource=name, data_dir=resolved_data_dir, **viewer_kwargs)
|
|
244
|
+
|
|
245
|
+
@classmethod
|
|
246
|
+
def from_anndata(
|
|
247
|
+
cls,
|
|
248
|
+
name,
|
|
249
|
+
image,
|
|
250
|
+
features=None,
|
|
251
|
+
adata=None,
|
|
252
|
+
segmentation=None,
|
|
253
|
+
coordinate_source=None,
|
|
254
|
+
obsm_key=None,
|
|
255
|
+
x=None,
|
|
256
|
+
y=None,
|
|
257
|
+
feature_source="X",
|
|
258
|
+
layer=None,
|
|
259
|
+
feature_obs_columns=None,
|
|
260
|
+
obs_id_field=None,
|
|
261
|
+
celltype_column=None,
|
|
262
|
+
subset_by=None,
|
|
263
|
+
subset_value=None,
|
|
264
|
+
channel_names=None,
|
|
265
|
+
copy=False,
|
|
266
|
+
data_dir=None,
|
|
267
|
+
**viewer_kwargs,
|
|
268
|
+
):
|
|
269
|
+
resolved_data_dir = Path(data_dir or os.environ.get("PLEXORA_DATA_PATH", _default_data_dir())).expanduser().resolve()
|
|
270
|
+
register_anndata_datasource(
|
|
271
|
+
name=name,
|
|
272
|
+
image=image,
|
|
273
|
+
features=features,
|
|
274
|
+
adata=adata,
|
|
275
|
+
segmentation=segmentation,
|
|
276
|
+
coordinate_source=coordinate_source,
|
|
277
|
+
obsm_key=obsm_key,
|
|
278
|
+
x=x,
|
|
279
|
+
y=y,
|
|
280
|
+
feature_source=feature_source,
|
|
281
|
+
layer=layer,
|
|
282
|
+
feature_obs_columns=feature_obs_columns,
|
|
283
|
+
obs_id_field=obs_id_field,
|
|
284
|
+
celltype_column=celltype_column,
|
|
285
|
+
subset_by=subset_by,
|
|
286
|
+
subset_value=subset_value,
|
|
287
|
+
channel_names=channel_names,
|
|
288
|
+
copy=copy,
|
|
289
|
+
data_dir=resolved_data_dir,
|
|
290
|
+
)
|
|
291
|
+
return cls(datasource=name, data_dir=resolved_data_dir, **viewer_kwargs)
|
|
292
|
+
|
|
293
|
+
def start(self):
|
|
294
|
+
"""Resolve where this viewer lives, then start or reuse a server.
|
|
295
|
+
|
|
296
|
+
Resolution happens against the PORT PLACEHOLDER rather than a real
|
|
297
|
+
port, so a second view() in the same notebook produces the same cache
|
|
298
|
+
key and reuses the first sidecar instead of spawning another one.
|
|
299
|
+
"""
|
|
300
|
+
if self._port is not None:
|
|
301
|
+
return self._port
|
|
302
|
+
server_base, display_base = resolve_display(self.proxy, self._base_url)
|
|
303
|
+
self._port, _ = _start_server(
|
|
304
|
+
self.data_dir, server_base, plugins=self.plugins
|
|
305
|
+
)
|
|
306
|
+
# Substituted only now, against whichever port the sidecar actually
|
|
307
|
+
# ended up on -- which is not necessarily the one this call would have
|
|
308
|
+
# picked, since it may have reused a server started by an earlier cell.
|
|
309
|
+
if display_base is COLAB:
|
|
310
|
+
self._display_base = colab_origin(self._port)
|
|
311
|
+
else:
|
|
312
|
+
self._display_base = display_base.replace(
|
|
313
|
+
PORT_PLACEHOLDER, str(self._port)
|
|
314
|
+
)
|
|
315
|
+
return self._port
|
|
316
|
+
|
|
317
|
+
@property
|
|
318
|
+
def url(self):
|
|
319
|
+
self.start()
|
|
320
|
+
if self._display_base is None:
|
|
321
|
+
raise RuntimeError(
|
|
322
|
+
"Colab did not return a public URL for this port. That needs a "
|
|
323
|
+
"connected notebook frontend, so it fails under 'Run all' or a "
|
|
324
|
+
"reconnect. Use viewer.iframe(), which does not, or re-run this "
|
|
325
|
+
"cell on its own."
|
|
326
|
+
)
|
|
327
|
+
return join_display(self._display_base, self.datasource)
|
|
328
|
+
|
|
329
|
+
def _colab_iframe(self):
|
|
330
|
+
"""Let Colab's frontend work out the URL, since the kernel could not.
|
|
331
|
+
|
|
332
|
+
This is the fallback for `colab_origin()` returning None, and it is not
|
|
333
|
+
a lesser version of it -- it is the more reliable one. The helper emits
|
|
334
|
+
Javascript that calls `proxyPort` in the notebook frontend, so it needs
|
|
335
|
+
no kernel-to-frontend round trip and works under "Run all" and after a
|
|
336
|
+
reconnect. It is only the fallback because it displays its own output
|
|
337
|
+
rather than returning a URL, which `.url` and `.open()` need.
|
|
338
|
+
"""
|
|
339
|
+
from google.colab.output import serve_kernel_port_as_iframe
|
|
340
|
+
|
|
341
|
+
return serve_kernel_port_as_iframe(
|
|
342
|
+
self._port,
|
|
343
|
+
path=f"/{self.datasource}",
|
|
344
|
+
width=str(self.width),
|
|
345
|
+
height=str(self.height),
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
def iframe(self):
|
|
349
|
+
self.start()
|
|
350
|
+
if self._display_base is None:
|
|
351
|
+
return self._colab_iframe()
|
|
352
|
+
try:
|
|
353
|
+
from IPython.display import HTML
|
|
354
|
+
except ImportError:
|
|
355
|
+
return self._repr_html_()
|
|
356
|
+
return HTML(self._repr_html_())
|
|
357
|
+
|
|
358
|
+
def _ipython_display_(self):
|
|
359
|
+
"""Render when the viewer is the last expression in a cell.
|
|
360
|
+
|
|
361
|
+
Defined rather than leaving IPython to find `_repr_html_`, because the
|
|
362
|
+
Colab fallback cannot be expressed as HTML at all: a <script> in cell
|
|
363
|
+
output runs inside Colab's sandboxed output frame, where
|
|
364
|
+
`google.colab.kernel` does not exist. Only frontend Javascript can
|
|
365
|
+
resolve the port, and only this hook can emit it.
|
|
366
|
+
"""
|
|
367
|
+
self.start()
|
|
368
|
+
if self._display_base is None:
|
|
369
|
+
self._colab_iframe()
|
|
370
|
+
return
|
|
371
|
+
from IPython.display import display, HTML
|
|
372
|
+
|
|
373
|
+
display(HTML(self._repr_html_()))
|
|
374
|
+
|
|
375
|
+
def open(self):
|
|
376
|
+
"""Open the viewer in a browser, where that means anything.
|
|
377
|
+
|
|
378
|
+
A hosted notebook's URL is a path on the hub's origin, which this
|
|
379
|
+
process cannot turn into something webbrowser could open -- and if it
|
|
380
|
+
could, the browser here is on the wrong machine anyway. Printing it is
|
|
381
|
+
the honest outcome.
|
|
382
|
+
"""
|
|
383
|
+
url = self.url
|
|
384
|
+
if not url.lower().startswith(("http://", "https://")):
|
|
385
|
+
print(f"Open this under your Jupyter server's address: {url}")
|
|
386
|
+
return url
|
|
387
|
+
webbrowser.open(url)
|
|
388
|
+
return url
|
|
389
|
+
|
|
390
|
+
def _repr_html_(self):
|
|
391
|
+
self.start()
|
|
392
|
+
if self._display_base is None:
|
|
393
|
+
# Reachable only when something bypassed _ipython_display_ -- a
|
|
394
|
+
# message beats an exception raised from inside a repr, which
|
|
395
|
+
# IPython would show as a traceback about formatting.
|
|
396
|
+
return (
|
|
397
|
+
"<pre>Plexora is running, but Colab did not return a public URL "
|
|
398
|
+
"for it.\nCall viewer.iframe() to display it.</pre>"
|
|
399
|
+
)
|
|
400
|
+
src = html.escape(self.url, quote=True)
|
|
401
|
+
width = html.escape(str(self.width), quote=True)
|
|
402
|
+
height = int(self.height)
|
|
403
|
+
return (
|
|
404
|
+
f'<iframe src="{src}" width="{width}" height="{height}" '
|
|
405
|
+
'style="border: 0; width: 100%;" allowfullscreen></iframe>'
|
|
406
|
+
)
|
plexora/notebook_env.py
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"""Working out what kind of notebook this is, and therefore what URL works.
|
|
2
|
+
|
|
3
|
+
`PlexoraViewer` starts a server on 127.0.0.1 and shows it in an iframe. That
|
|
4
|
+
is correct exactly when the browser rendering the notebook is on the same
|
|
5
|
+
machine as the kernel -- local Jupyter, and (because it forwards ports for you)
|
|
6
|
+
VS Code Remote. Everywhere else it produces a URL pointing at the user's own
|
|
7
|
+
laptop, where nothing is listening, and the cell renders a blank box with no
|
|
8
|
+
error anywhere.
|
|
9
|
+
|
|
10
|
+
"Everywhere else" is most of the interesting places: JupyterHub, Open OnDemand,
|
|
11
|
+
Colab, a plain `jupyter lab` on a workstation the user ssh'd into. Each needs a
|
|
12
|
+
different URL, and each can be recognised from the environment, so the viewer
|
|
13
|
+
asks here instead of making the user pass `proxy=True` after finding out the
|
|
14
|
+
hard way that they needed to.
|
|
15
|
+
|
|
16
|
+
Two ideas do most of the work:
|
|
17
|
+
|
|
18
|
+
**A hosted URL is a PATH, not a host.** Under jupyter-server-proxy the viewer
|
|
19
|
+
is reachable at `<notebook prefix>proxy/<port>` on the notebook's OWN origin --
|
|
20
|
+
which is the origin holding the user's auth cookie, and the only one that will
|
|
21
|
+
be allowed to load. So the display base stays path-only and an iframe resolves
|
|
22
|
+
it. Writing in a hostname would be both wrong and unauthenticated.
|
|
23
|
+
|
|
24
|
+
**Colab is the exception that proves it.** `proxyPort()` returns a whole
|
|
25
|
+
separate `https://…googleusercontent.com` origin, which is why `join_display`
|
|
26
|
+
accepts a full origin at all and why `clean_prefix` refuses one.
|
|
27
|
+
|
|
28
|
+
Imports only `plexora._url`, and everything that touches an optional
|
|
29
|
+
third-party module does so behind `_module_available` inside a function -- this
|
|
30
|
+
is consulted on the import path of a notebook helper, and a hard dependency on
|
|
31
|
+
`jupyter_server` would make `import plexora` fail on a machine that has no
|
|
32
|
+
Jupyter at all.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import importlib.util
|
|
38
|
+
import os
|
|
39
|
+
import sys
|
|
40
|
+
|
|
41
|
+
from plexora._url import is_full_origin, prefix_with_slash
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _module_available(name):
|
|
45
|
+
"""Whether `name` could be imported, without importing it. Never raises.
|
|
46
|
+
|
|
47
|
+
`find_spec` alone is not enough, and its failure modes are exactly the ones
|
|
48
|
+
that turn up in notebooks: it raises ValueError for a module already in
|
|
49
|
+
sys.modules whose `__spec__` is None (dynamically created modules, some
|
|
50
|
+
frozen ones, anything a hub's startup code injected), and
|
|
51
|
+
ModuleNotFoundError when an intermediate package is missing. Every caller
|
|
52
|
+
here is on the display path of a notebook cell, where an exception would
|
|
53
|
+
replace the viewer with a traceback about introspection.
|
|
54
|
+
"""
|
|
55
|
+
if name in sys.modules:
|
|
56
|
+
return True
|
|
57
|
+
try:
|
|
58
|
+
return importlib.util.find_spec(name) is not None
|
|
59
|
+
except Exception:
|
|
60
|
+
return False
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
#: Variables that mean "the kernel is not on the machine holding the screen".
|
|
64
|
+
#: The scheduler ones are not padding: Open OnDemand runs Jupyter inside a
|
|
65
|
+
#: batch job and sets no hub variable of its own, so its job variables are the
|
|
66
|
+
#: only evidence available that a direct localhost URL cannot work.
|
|
67
|
+
REMOTE_ENV_VARS = (
|
|
68
|
+
"SSH_CONNECTION",
|
|
69
|
+
"SSH_CLIENT",
|
|
70
|
+
"SSH_TTY",
|
|
71
|
+
"SLURM_JOB_ID",
|
|
72
|
+
"PBS_JOBID",
|
|
73
|
+
"LSB_JOBID",
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
HUB_ENV_VARS = ("JUPYTERHUB_SERVICE_PREFIX", "JUPYTERHUB_USER", "JUPYTERHUB_API_URL")
|
|
77
|
+
|
|
78
|
+
#: Stands in for a port that has not been chosen yet, so `resolve_display` can
|
|
79
|
+
#: be asked its question BEFORE a server exists -- which is what lets the
|
|
80
|
+
#: sidecar cache be keyed on the answer. Same spelling as the placeholder
|
|
81
|
+
#: `plexora/proxy.py` hands jupyter-server-proxy, for one convention rather
|
|
82
|
+
#: than two.
|
|
83
|
+
PORT_PLACEHOLDER = "{port}"
|
|
84
|
+
|
|
85
|
+
#: Returned as the display base when only the Colab FRONTEND can answer, and
|
|
86
|
+
#: only once a real port exists. Every other case resolves to a string that a
|
|
87
|
+
#: port can simply be substituted into; Colab needs a round trip, so it gets a
|
|
88
|
+
#: sentinel and the caller makes that call after starting the server.
|
|
89
|
+
COLAB = "colab"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def looks_remote(env=None):
|
|
93
|
+
env = os.environ if env is None else env
|
|
94
|
+
return any(env.get(name) for name in REMOTE_ENV_VARS)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def in_hub(env=None):
|
|
98
|
+
env = os.environ if env is None else env
|
|
99
|
+
return any(env.get(name) for name in HUB_ENV_VARS)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def in_colab():
|
|
103
|
+
return _module_available("google.colab")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def colab_origin(port):
|
|
107
|
+
"""The public origin Colab will proxy `port` on, or None.
|
|
108
|
+
|
|
109
|
+
None is a normal outcome, not just an error path: `eval_js` runs Javascript
|
|
110
|
+
in the notebook FRONTEND and waits for an answer, so it needs a browser
|
|
111
|
+
actually connected to this kernel. Under "Run all", a reconnect, or a
|
|
112
|
+
headless execution there is nobody to ask, and it hangs or raises. The
|
|
113
|
+
caller falls back to Colab's own iframe helper, which does not need a
|
|
114
|
+
round trip.
|
|
115
|
+
"""
|
|
116
|
+
try:
|
|
117
|
+
from google.colab.output import eval_js
|
|
118
|
+
except Exception:
|
|
119
|
+
return None
|
|
120
|
+
try:
|
|
121
|
+
origin = eval_js(f"google.colab.kernel.proxyPort({int(port)})")
|
|
122
|
+
except Exception:
|
|
123
|
+
return None
|
|
124
|
+
if not origin or not is_full_origin(origin):
|
|
125
|
+
return None
|
|
126
|
+
return str(origin).rstrip("/")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def discover_jupyter_prefix(env=None, echo=print):
|
|
130
|
+
"""The notebook server's base_url (e.g. `/user/me/`), or None.
|
|
131
|
+
|
|
132
|
+
JUPYTERHUB_SERVICE_PREFIX is asked first because a hub sets it in the
|
|
133
|
+
kernel's own environment and it is definitive. Failing that, ask the
|
|
134
|
+
jupyter_server library which servers are running on this machine -- which
|
|
135
|
+
is how Open OnDemand is found, since it configures a random prefix per job
|
|
136
|
+
and advertises it nowhere else.
|
|
137
|
+
"""
|
|
138
|
+
env = os.environ if env is None else env
|
|
139
|
+
from_hub = env.get("JUPYTERHUB_SERVICE_PREFIX")
|
|
140
|
+
if from_hub:
|
|
141
|
+
return prefix_with_slash(from_hub)
|
|
142
|
+
|
|
143
|
+
if not _module_available("jupyter_server"):
|
|
144
|
+
return None
|
|
145
|
+
try:
|
|
146
|
+
from jupyter_server.serverapp import list_running_servers
|
|
147
|
+
|
|
148
|
+
servers = [entry for entry in list_running_servers() if entry.get("base_url")]
|
|
149
|
+
except Exception:
|
|
150
|
+
return None
|
|
151
|
+
if not servers:
|
|
152
|
+
return None
|
|
153
|
+
if len(servers) > 1:
|
|
154
|
+
echo(
|
|
155
|
+
"Plexora found several running Jupyter servers and is assuming "
|
|
156
|
+
f"{servers[0]['base_url']!r}. Pass base_url=... to plexora.view() "
|
|
157
|
+
"if that is the wrong one."
|
|
158
|
+
)
|
|
159
|
+
return prefix_with_slash(servers[0]["base_url"])
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def proxy_hint_if_missing(echo=print):
|
|
163
|
+
"""Mention jupyter-server-proxy if it looks absent. Never raises.
|
|
164
|
+
|
|
165
|
+
Only a hint, and only ever a hint: on several hubs the kernel runs in a
|
|
166
|
+
different environment from the notebook server, so the package being
|
|
167
|
+
missing HERE says nothing about whether the proxy will work THERE. Turning
|
|
168
|
+
that guess into an exception would break setups that were fine.
|
|
169
|
+
"""
|
|
170
|
+
if not _module_available("jupyter_server_proxy"):
|
|
171
|
+
echo(
|
|
172
|
+
"Note: jupyter-server-proxy does not appear to be installed in this "
|
|
173
|
+
"environment. If the viewer below does not load, install it in the "
|
|
174
|
+
"environment running your Jupyter server:\n"
|
|
175
|
+
" pip install jupyter-server-proxy"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def resolve_display(proxy="auto", base_url=None, port=PORT_PLACEHOLDER, env=None,
|
|
180
|
+
echo=print):
|
|
181
|
+
"""`(server_base_url, display_base)` -- how to mount, and how to reach it.
|
|
182
|
+
|
|
183
|
+
The two differ whenever a proxy is involved, and conflating them is the
|
|
184
|
+
original bug: the sidecar has to generate links under the path the proxy
|
|
185
|
+
exposes it at, while the notebook has to load that same path against the
|
|
186
|
+
hub's origin.
|
|
187
|
+
|
|
188
|
+
`port` may be `PORT_PLACEHOLDER`, and normally is: the caller wants this
|
|
189
|
+
answer before it has picked a port, so that the answer can decide whether
|
|
190
|
+
an already-running sidecar will do. Both returned strings then carry the
|
|
191
|
+
placeholder through for the caller to substitute. `COLAB` is the one
|
|
192
|
+
display base that cannot work that way -- see the constant.
|
|
193
|
+
|
|
194
|
+
First match wins, and the order encodes what beats what:
|
|
195
|
+
|
|
196
|
+
1. An explicit `base_url` is an instruction; nothing overrides it.
|
|
197
|
+
2. `proxy=False` is also an instruction -- the pre-existing default, kept
|
|
198
|
+
working verbatim for every notebook that passes it today.
|
|
199
|
+
3. Colab, whose proxy is a whole origin rather than a path (returns the
|
|
200
|
+
`COLAB` sentinel; ask `colab_origin(real_port)` once you have one).
|
|
201
|
+
4. A discoverable notebook prefix, but only with a reason to use it:
|
|
202
|
+
`proxy=True`, or evidence the kernel is not local.
|
|
203
|
+
5. Direct localhost. Deliberately last and deliberately not an error --
|
|
204
|
+
this is plain local Jupyter, and it is also VS Code Remote, which
|
|
205
|
+
forwards the port itself and would be broken by "helpfully" proxying.
|
|
206
|
+
"""
|
|
207
|
+
env = os.environ if env is None else env
|
|
208
|
+
|
|
209
|
+
if base_url is not None:
|
|
210
|
+
if is_full_origin(base_url):
|
|
211
|
+
# A caller who already knows the public origin (a bespoke reverse
|
|
212
|
+
# proxy, a tunnel they set up) -- the server still mounts at root.
|
|
213
|
+
return "", str(base_url).rstrip("/")
|
|
214
|
+
mounted = f"{prefix_with_slash(base_url)}proxy/{port}"
|
|
215
|
+
return mounted, mounted
|
|
216
|
+
|
|
217
|
+
if proxy is False:
|
|
218
|
+
return "", f"http://127.0.0.1:{port}"
|
|
219
|
+
|
|
220
|
+
if in_colab():
|
|
221
|
+
# Colab maps the whole port onto a subdomain root, so there is no path
|
|
222
|
+
# to mount under -- the server stays at "/" and only the display
|
|
223
|
+
# changes.
|
|
224
|
+
return "", COLAB
|
|
225
|
+
|
|
226
|
+
prefix = discover_jupyter_prefix(env, echo=echo)
|
|
227
|
+
if prefix and (proxy is True or looks_remote(env) or in_hub(env)):
|
|
228
|
+
proxy_hint_if_missing(echo=echo)
|
|
229
|
+
mounted = f"{prefix}proxy/{port}"
|
|
230
|
+
return mounted, mounted
|
|
231
|
+
|
|
232
|
+
return "", f"http://127.0.0.1:{port}"
|