solara-ui 1.39.0__py2.py3-none-any.whl → 1.41.0__py2.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.
- solara/__init__.py +1 -1
- solara/__main__.py +23 -10
- solara/components/__init__.py +1 -0
- solara/components/component_vue.py +3 -2
- solara/components/input_text_area.py +86 -0
- solara/components/markdown.py +1 -1
- solara/hooks/use_thread.py +4 -4
- solara/lab/components/chat.py +8 -2
- solara/server/assets/style.css +4 -1
- solara/server/flask.py +1 -1
- solara/server/jupyter/server_extension.py +11 -1
- solara/server/jupyter/solara.py +91 -0
- solara/server/patch.py +1 -0
- solara/server/pyinstaller/__init__.py +9 -0
- solara/server/pyinstaller/hook-ipyreact.py +5 -0
- solara/server/pyinstaller/hook-ipyvuetify.py +5 -0
- solara/server/pyinstaller/hook-solara.py +9 -0
- solara/server/qt.py +113 -0
- solara/server/server.py +6 -1
- solara/server/settings.py +1 -0
- solara/server/starlette.py +18 -6
- solara/server/static/highlight-dark.css +1 -1
- solara/server/static/main-vuetify.js +11 -1
- solara/server/static/solara_bootstrap.py +1 -1
- solara/server/templates/loader-solara.html +1 -1
- solara/server/templates/solara.html.j2 +36 -7
- solara/website/assets/custom.css +20 -57
- solara/website/components/__init__.py +2 -2
- solara/website/components/algolia_api.vue +23 -6
- solara/website/components/breadcrumbs.py +28 -0
- solara/website/components/contact.py +144 -0
- solara/website/components/docs.py +11 -9
- solara/website/components/header.py +31 -20
- solara/website/components/markdown.py +12 -1
- solara/website/components/markdown_nav.vue +34 -0
- solara/website/components/sidebar.py +9 -1
- solara/website/pages/__init__.py +93 -254
- solara/website/pages/about/__init__.py +9 -0
- solara/website/pages/about/about.md +3 -0
- solara/website/pages/careers/__init__.py +27 -0
- solara/website/pages/changelog/__init__.py +2 -2
- solara/website/pages/changelog/changelog.md +12 -0
- solara/website/pages/contact/__init__.py +30 -6
- solara/website/pages/documentation/__init__.py +25 -33
- solara/website/pages/documentation/advanced/content/10-howto/40-embed.md +2 -1
- solara/website/pages/documentation/advanced/content/15-reference/41-asset-files.md +1 -1
- solara/website/pages/documentation/advanced/content/20-understanding/40-routing.md +17 -1
- solara/website/pages/documentation/advanced/content/20-understanding/50-solara-server.md +2 -1
- solara/website/pages/documentation/advanced/content/30-enterprise/00-overview.md +1 -1
- solara/website/pages/documentation/advanced/content/30-enterprise/10-oauth.md +5 -2
- solara/website/pages/documentation/api/hooks/use_thread.md +6 -0
- solara/website/pages/documentation/components/data/pivot_table.py +2 -2
- solara/website/pages/documentation/components/input/input.py +2 -0
- solara/website/pages/documentation/components/output/sql_code.py +3 -3
- solara/website/pages/documentation/examples/__init__.py +14 -22
- solara/website/pages/documentation/examples/ai/chatbot.py +1 -1
- solara/website/pages/documentation/examples/general/vue_component.py +1 -1
- solara/website/pages/documentation/examples/libraries/altair.py +1 -0
- solara/website/pages/documentation/examples/libraries/bqplot.py +1 -1
- solara/website/pages/documentation/examples/libraries/ipyleaflet.py +1 -1
- solara/website/pages/documentation/examples/libraries/ipyleaflet_advanced.py +1 -1
- solara/website/pages/documentation/examples/utilities/countdown_timer.py +18 -20
- solara/website/pages/documentation/examples/visualization/annotator.py +1 -3
- solara/website/pages/documentation/examples/visualization/linked_views.py +4 -4
- solara/website/pages/documentation/getting_started/content/00-quickstart.md +18 -0
- solara/website/pages/documentation/getting_started/content/04-tutorials/_jupyter_dashboard_1.ipynb +2 -2
- solara/website/pages/documentation/getting_started/content/05-fundamentals/10-components.md +19 -14
- solara/website/pages/documentation/getting_started/content/05-fundamentals/50-state-management.md +205 -15
- solara/website/pages/documentation/getting_started/content/07-deploying/10-self-hosted.md +3 -1
- solara/website/pages/home.vue +1199 -0
- solara/website/pages/our_team/__init__.py +83 -0
- solara/website/pages/pricing/__init__.py +31 -0
- solara/website/pages/roadmap/__init__.py +11 -0
- solara/website/pages/roadmap/roadmap.md +41 -0
- solara/website/pages/scale_ipywidgets.py +45 -0
- solara/widgets/vue/navigator.vue +46 -16
- solara/widgets/vue/vegalite.vue +18 -0
- {solara_ui-1.39.0.dist-info → solara_ui-1.41.0.dist-info}/METADATA +2 -2
- {solara_ui-1.39.0.dist-info → solara_ui-1.41.0.dist-info}/RECORD +83 -66
- solara/website/components/hero.py +0 -15
- solara/website/pages/contact/contact.md +0 -17
- {solara_ui-1.39.0.data → solara_ui-1.41.0.data}/data/etc/jupyter/jupyter_notebook_config.d/solara.json +0 -0
- {solara_ui-1.39.0.data → solara_ui-1.41.0.data}/data/etc/jupyter/jupyter_server_config.d/solara.json +0 -0
- {solara_ui-1.39.0.dist-info → solara_ui-1.41.0.dist-info}/WHEEL +0 -0
- {solara_ui-1.39.0.dist-info → solara_ui-1.41.0.dist-info}/licenses/LICENSE +0 -0
solara/__init__.py
CHANGED
solara/__main__.py
CHANGED
|
@@ -261,6 +261,12 @@ if "SOLARA_MODE" in os.environ:
|
|
|
261
261
|
default=True,
|
|
262
262
|
help="Check installed version again pypi version.",
|
|
263
263
|
)
|
|
264
|
+
@click.option(
|
|
265
|
+
"--qt",
|
|
266
|
+
is_flag=True,
|
|
267
|
+
default=False,
|
|
268
|
+
help="Instead of opening a browser, open a Qt window. Will also stop the server when the window is closed. (experimental)",
|
|
269
|
+
)
|
|
264
270
|
def run(
|
|
265
271
|
app,
|
|
266
272
|
host,
|
|
@@ -290,6 +296,7 @@ def run(
|
|
|
290
296
|
ssg: bool,
|
|
291
297
|
search: bool,
|
|
292
298
|
check_version: bool = True,
|
|
299
|
+
qt=False,
|
|
293
300
|
):
|
|
294
301
|
"""Run a Solara app."""
|
|
295
302
|
if dev is not None:
|
|
@@ -365,9 +372,16 @@ def run(
|
|
|
365
372
|
while not failed and (server is None or not server.started):
|
|
366
373
|
time.sleep(0.1)
|
|
367
374
|
if not failed:
|
|
368
|
-
|
|
375
|
+
if qt:
|
|
376
|
+
from .server.qt import run_qt
|
|
369
377
|
|
|
370
|
-
|
|
378
|
+
run_qt(url)
|
|
379
|
+
else:
|
|
380
|
+
webbrowser.open(url)
|
|
381
|
+
|
|
382
|
+
# with qt, we open the browser in the main thread (qt wants that)
|
|
383
|
+
# otherwise, we open the browser in a separate thread
|
|
384
|
+
if open and not qt:
|
|
371
385
|
threading.Thread(target=open_browser, daemon=True).start()
|
|
372
386
|
|
|
373
387
|
rich.print(f"Solara server is starting at {url}")
|
|
@@ -397,7 +411,7 @@ def run(
|
|
|
397
411
|
settings.main.timing = timing
|
|
398
412
|
items = (
|
|
399
413
|
"theme_variant_user_selectable dark theme_variant theme_loader use_pdb server open_browser open url failed dev tracer"
|
|
400
|
-
" timing ssg search check_version production".split()
|
|
414
|
+
" timing ssg search check_version production qt".split()
|
|
401
415
|
)
|
|
402
416
|
for item in items:
|
|
403
417
|
del kwargs[item]
|
|
@@ -451,14 +465,13 @@ def run(
|
|
|
451
465
|
|
|
452
466
|
build_index("")
|
|
453
467
|
|
|
454
|
-
start_server()
|
|
455
|
-
|
|
456
468
|
# TODO: if we want to use webview, it should be sth like this
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
469
|
+
if qt:
|
|
470
|
+
server_thread = threading.Thread(target=start_server, daemon=True)
|
|
471
|
+
server_thread.start()
|
|
472
|
+
open_browser()
|
|
473
|
+
else:
|
|
474
|
+
start_server()
|
|
462
475
|
# server_thread.join()
|
|
463
476
|
|
|
464
477
|
|
solara/components/__init__.py
CHANGED
|
@@ -33,6 +33,7 @@ from .togglebuttons import ( # noqa: F401 F403
|
|
|
33
33
|
ToggleButtonsSingle,
|
|
34
34
|
)
|
|
35
35
|
from .input import InputText, InputFloat, InputInt # noqa: F401 F403
|
|
36
|
+
from .input_text_area import InputTextArea # noqa: F401 F403
|
|
36
37
|
from .pivot_table import PivotTableView, PivotTable, PivotTableCard # noqa: F401 F403
|
|
37
38
|
from .head import Head # noqa: F401 F403
|
|
38
39
|
from .title import Title # noqa: F401 F403
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import inspect
|
|
2
|
+
import os
|
|
2
3
|
from typing import Any, Callable, Dict, Type
|
|
3
4
|
|
|
4
5
|
import ipyvue as vue
|
|
@@ -48,10 +49,10 @@ def _widget_from_signature(classname, base_class: Type[widgets.Widget], func: Ca
|
|
|
48
49
|
def _widget_vue(vue_path: str, vuetify=True) -> Callable[[Callable[P, None]], Type[v.VuetifyTemplate]]:
|
|
49
50
|
def decorator(func: Callable[P, None]):
|
|
50
51
|
class VuetifyWidgetSolara(v.VuetifyTemplate):
|
|
51
|
-
template_file = (inspect.getfile(func), vue_path)
|
|
52
|
+
template_file = (os.path.abspath(inspect.getfile(func)), vue_path)
|
|
52
53
|
|
|
53
54
|
class VueWidgetSolara(vue.VueTemplate):
|
|
54
|
-
template_file = (inspect.getfile(func), vue_path)
|
|
55
|
+
template_file = (os.path.abspath(inspect.getfile(func)), vue_path)
|
|
55
56
|
|
|
56
57
|
base_class = VuetifyWidgetSolara if vuetify else VueWidgetSolara
|
|
57
58
|
widget_class = _widget_from_signature("VueWidgetSolaraSub", base_class, func, "vue_")
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from typing import Callable, Optional, Union, List
|
|
2
|
+
from .input import use_change
|
|
3
|
+
import solara
|
|
4
|
+
from solara.alias import rv as v
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@solara.component
|
|
8
|
+
def InputTextArea(
|
|
9
|
+
label: str,
|
|
10
|
+
value: Union[str, solara.Reactive[str]] = "",
|
|
11
|
+
on_value: Callable[[str], None] = None,
|
|
12
|
+
disabled: bool = False,
|
|
13
|
+
continuous_update: bool = False,
|
|
14
|
+
update_events: List[str] = ["focusout"],
|
|
15
|
+
error: Union[bool, str] = False,
|
|
16
|
+
message: Optional[str] = None,
|
|
17
|
+
auto_grow: bool = True,
|
|
18
|
+
rows: int = 5,
|
|
19
|
+
):
|
|
20
|
+
r"""Free form text area input.
|
|
21
|
+
|
|
22
|
+
### Basic example:
|
|
23
|
+
|
|
24
|
+
```solara
|
|
25
|
+
import solara
|
|
26
|
+
|
|
27
|
+
text = solara.reactive("Hello\nWorld\n!!!")
|
|
28
|
+
continuous_update = solara.reactive(True)
|
|
29
|
+
|
|
30
|
+
@solara.component
|
|
31
|
+
def Page():
|
|
32
|
+
solara.Checkbox(label="Continuous update", value=continuous_update)
|
|
33
|
+
solara.InputTextArea("Enter some text", value=text, continuous_update=continuous_update.value)
|
|
34
|
+
with solara.Row():
|
|
35
|
+
solara.Button("Clear", on_click=lambda: text.set(""))
|
|
36
|
+
solara.Button("Reset", on_click=lambda: text.set("Hello\nWorld\n!!!"))
|
|
37
|
+
solara.Markdown(f"**You entered**: {text.value}")
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
## Arguments
|
|
42
|
+
|
|
43
|
+
* `label`: Label to display next to the slider.
|
|
44
|
+
* `value`: The currently entered value.
|
|
45
|
+
* `on_value`: Callback to call when the value changes.
|
|
46
|
+
* `disabled`: Whether the input is disabled.
|
|
47
|
+
* `continuous_update`: Whether to call the `on_value` callback on every change or only when the input loses focus or the enter key is pressed.
|
|
48
|
+
* `update_events`: A list of events that should trigger `on_value`. If continuous update is enabled, this will effectively be ignored,
|
|
49
|
+
since updates will happen every change.
|
|
50
|
+
* `auto_grow`: Whether the text area auto grows with more text.
|
|
51
|
+
* `rows`: Number of empty rows to display.
|
|
52
|
+
* `error`: If truthy, show the input as having an error (in red). If a string is passed, it will be shown as the error message.
|
|
53
|
+
* `message`: Message to show below the input. If `error` is a string, this will be ignored.
|
|
54
|
+
* `classes`: List of CSS classes to apply to the input.
|
|
55
|
+
* `style`: CSS style to apply to the input.
|
|
56
|
+
"""
|
|
57
|
+
reactive_value = solara.use_reactive(value, on_value)
|
|
58
|
+
del value, on_value
|
|
59
|
+
|
|
60
|
+
def set_value_cast(value):
|
|
61
|
+
reactive_value.value = str(value)
|
|
62
|
+
|
|
63
|
+
def on_v_model(value):
|
|
64
|
+
if continuous_update:
|
|
65
|
+
set_value_cast(value)
|
|
66
|
+
|
|
67
|
+
messages = []
|
|
68
|
+
if error and isinstance(error, str):
|
|
69
|
+
messages.append(error)
|
|
70
|
+
elif message:
|
|
71
|
+
messages.append(message)
|
|
72
|
+
text_area = v.Textarea(
|
|
73
|
+
v_model=reactive_value.value,
|
|
74
|
+
on_v_model=on_v_model,
|
|
75
|
+
label=label,
|
|
76
|
+
disabled=disabled,
|
|
77
|
+
error=bool(error),
|
|
78
|
+
messages=messages,
|
|
79
|
+
solo=True,
|
|
80
|
+
hide_details=True,
|
|
81
|
+
outlined=True,
|
|
82
|
+
rows=rows,
|
|
83
|
+
auto_grow=auto_grow,
|
|
84
|
+
)
|
|
85
|
+
use_change(text_area, set_value_cast, enabled=not continuous_update, update_events=update_events)
|
|
86
|
+
return text_area
|
solara/components/markdown.py
CHANGED
|
@@ -128,7 +128,7 @@ module.exports = {
|
|
|
128
128
|
href = location.pathname + href.substr(1);
|
|
129
129
|
a.attributes['href'].href = href;
|
|
130
130
|
}
|
|
131
|
-
let authLink = href.
|
|
131
|
+
let authLink = href.startsWith("/_solara/auth/");
|
|
132
132
|
if( (href.startsWith("./") || href.startsWith("/")) && !authLink) {
|
|
133
133
|
a.onclick = e => {
|
|
134
134
|
console.log("clicked", href)
|
solara/hooks/use_thread.py
CHANGED
|
@@ -15,7 +15,7 @@ logger = logging.getLogger("solara.hooks.use_thread")
|
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
def use_thread(
|
|
18
|
-
callback
|
|
18
|
+
callback: Union[
|
|
19
19
|
Callable[[threading.Event], T],
|
|
20
20
|
Iterator[Callable[[threading.Event], T]],
|
|
21
21
|
Callable[[], T],
|
|
@@ -69,11 +69,11 @@ def use_thread(
|
|
|
69
69
|
# result.current = None
|
|
70
70
|
set_result_state(ResultState.RUNNING)
|
|
71
71
|
|
|
72
|
-
sig = inspect.signature(callback)
|
|
72
|
+
sig = inspect.signature(callback) # type: ignore
|
|
73
73
|
if sig.parameters:
|
|
74
|
-
f = functools.partial(callback, cancel)
|
|
74
|
+
f = functools.partial(callback, cancel) # type: ignore
|
|
75
75
|
else:
|
|
76
|
-
f = callback
|
|
76
|
+
f = callback # type: ignore
|
|
77
77
|
try:
|
|
78
78
|
try:
|
|
79
79
|
# we only use the cancel_guard context manager around
|
solara/lab/components/chat.py
CHANGED
|
@@ -46,7 +46,9 @@ def ChatInput(
|
|
|
46
46
|
send_callback: Optional[Callable] = None,
|
|
47
47
|
disabled: bool = False,
|
|
48
48
|
style: Optional[Union[str, Dict[str, str]]] = None,
|
|
49
|
+
input_text_style: Optional[Union[str, Dict[str, str]]] = None,
|
|
49
50
|
classes: List[str] = [],
|
|
51
|
+
input_text_classes: List[str] = [],
|
|
50
52
|
):
|
|
51
53
|
"""
|
|
52
54
|
The ChatInput component renders a text input together with a send button.
|
|
@@ -56,11 +58,14 @@ def ChatInput(
|
|
|
56
58
|
* `send_callback`: A callback function for when the user presses enter or clicks the send button.
|
|
57
59
|
* `disabled`: Whether the input should be disabled. Useful for disabling sending further messages while a chatbot is replying,
|
|
58
60
|
among other things.
|
|
59
|
-
* `style`: CSS styles to apply to the
|
|
61
|
+
* `style`: CSS styles to apply to the `solara.Row` containing the input field and submit button. Either a string or a dictionary.
|
|
62
|
+
* `input_text_style`: CSS styles to apply to the `InputText` part of the component. Either a string or a dictionary.
|
|
60
63
|
* `classes`: A list of CSS classes to apply to the component. Also applied to the container.
|
|
64
|
+
* `input_text_classes`: A list of CSS classes to apply to the `InputText` part of the component.
|
|
61
65
|
"""
|
|
62
66
|
message, set_message = solara.use_state("") # type: ignore
|
|
63
67
|
style_flat = solara.util._flatten_style(style)
|
|
68
|
+
input_text_style_flat = solara.util._flatten_style(input_text_style)
|
|
64
69
|
|
|
65
70
|
if "align-items" not in style_flat:
|
|
66
71
|
style_flat += " align-items: center;"
|
|
@@ -79,8 +84,9 @@ def ChatInput(
|
|
|
79
84
|
rounded=True,
|
|
80
85
|
filled=True,
|
|
81
86
|
hide_details=True,
|
|
82
|
-
style_="flex-grow: 1;",
|
|
87
|
+
style_="flex-grow: 1;" + input_text_style_flat,
|
|
83
88
|
disabled=disabled,
|
|
89
|
+
class_=" ".join(input_text_classes),
|
|
84
90
|
)
|
|
85
91
|
|
|
86
92
|
use_change(message_input, send, update_events=["keyup.enter"])
|
solara/server/assets/style.css
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
html {
|
|
2
2
|
/* override vuetify's css reset ress.css */
|
|
3
3
|
overflow-y: auto;
|
|
4
|
+
scroll-behavior: smooth;
|
|
4
5
|
}
|
|
5
6
|
|
|
6
7
|
.jupyter-widgets code {
|
|
@@ -75,7 +76,8 @@ div.solara-markdown-output {
|
|
|
75
76
|
|
|
76
77
|
|
|
77
78
|
div.highlight {
|
|
78
|
-
|
|
79
|
+
border: 1px solid var(--color-border-appbar);
|
|
80
|
+
background-color: #fafafa;
|
|
79
81
|
padding: 30px;
|
|
80
82
|
border-radius: 20px;
|
|
81
83
|
margin-top: 15px;
|
|
@@ -123,6 +125,7 @@ div.highlight {
|
|
|
123
125
|
|
|
124
126
|
.solara-autorouter-content {
|
|
125
127
|
height: 100%;
|
|
128
|
+
max-width: 100%;
|
|
126
129
|
}
|
|
127
130
|
|
|
128
131
|
/* originally from index.css */
|
solara/server/flask.py
CHANGED
|
@@ -192,7 +192,7 @@ def assets(path):
|
|
|
192
192
|
return flask.Response("not found", status=404)
|
|
193
193
|
|
|
194
194
|
|
|
195
|
-
@blueprint.route("/
|
|
195
|
+
@blueprint.route("/jupyter/nbextensions/<dir>/<filename>")
|
|
196
196
|
def nbext(dir, filename):
|
|
197
197
|
if not allowed():
|
|
198
198
|
abort(401)
|
|
@@ -2,6 +2,7 @@ from jupyter_server.utils import url_path_join
|
|
|
2
2
|
|
|
3
3
|
from solara.server.cdn_helper import cdn_url_path
|
|
4
4
|
from solara.server.jupyter.cdn_handler import CdnHandler
|
|
5
|
+
from .solara import SolaraHandler, Assets, ReadyZ
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
def _jupyter_server_extension_paths():
|
|
@@ -9,6 +10,11 @@ def _jupyter_server_extension_paths():
|
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
def _load_jupyter_server_extension(server_app):
|
|
13
|
+
# a dummy app, so that server.read_root can be used
|
|
14
|
+
import solara.server.app
|
|
15
|
+
|
|
16
|
+
solara.server.app.apps["__default__"] = solara.server.app.AppScript("solara.server.jupyter.solara:Page")
|
|
17
|
+
|
|
12
18
|
web_app = server_app.web_app
|
|
13
19
|
|
|
14
20
|
host_pattern = ".*$"
|
|
@@ -17,7 +23,11 @@ def _load_jupyter_server_extension(server_app):
|
|
|
17
23
|
web_app.add_handlers(
|
|
18
24
|
host_pattern,
|
|
19
25
|
[
|
|
20
|
-
(url_path_join(base_url, f"/{cdn_url_path}/(.*)"), CdnHandler, {}),
|
|
26
|
+
(url_path_join(base_url, f"/{cdn_url_path}/(.*)"), CdnHandler, {}), # kept for backward compatibility
|
|
27
|
+
(url_path_join(base_url, f"/solara/{cdn_url_path}/(.*)"), CdnHandler, {}),
|
|
28
|
+
(url_path_join(base_url, "/solara/static/assets/(.*)"), Assets, {}),
|
|
29
|
+
(url_path_join(base_url, "/solara/readyz"), ReadyZ, {}),
|
|
30
|
+
(url_path_join(base_url, "/solara(.*)"), SolaraHandler, {}),
|
|
21
31
|
],
|
|
22
32
|
)
|
|
23
33
|
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import tornado.web
|
|
7
|
+
from jupyter_server.base.handlers import JupyterHandler
|
|
8
|
+
import solara.server.server as server
|
|
9
|
+
|
|
10
|
+
from solara.server.utils import path_is_child_of
|
|
11
|
+
import solara
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger("solara.server.jupyter.solara")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@solara.component
|
|
17
|
+
def Page():
|
|
18
|
+
solara.Error("Hi, you should not see this, we only support ipypopout for now")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class SolaraHandler(JupyterHandler):
|
|
22
|
+
async def get(self, path=None):
|
|
23
|
+
try:
|
|
24
|
+
# base url ends with /
|
|
25
|
+
base_url = self.settings["base_url"]
|
|
26
|
+
# root_path's do not end with /
|
|
27
|
+
jupyter_root_path = ""
|
|
28
|
+
if base_url and base_url.endswith("/"):
|
|
29
|
+
jupyter_root_path = base_url[:-1]
|
|
30
|
+
root_path = f"{jupyter_root_path}/solara"
|
|
31
|
+
content = server.read_root(path="", root_path=root_path, jupyter_root_path=jupyter_root_path)
|
|
32
|
+
except Exception as e:
|
|
33
|
+
logger.exception(e)
|
|
34
|
+
raise tornado.web.HTTPError(500)
|
|
35
|
+
|
|
36
|
+
if content is None:
|
|
37
|
+
raise tornado.web.HTTPError(404)
|
|
38
|
+
|
|
39
|
+
self.set_header("Content-Type", "text/html")
|
|
40
|
+
self.write(content)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# similar to voila
|
|
44
|
+
class MultiStaticFileHandler(tornado.web.StaticFileHandler):
|
|
45
|
+
"""A static file handler that 'merges' a list of directories
|
|
46
|
+
|
|
47
|
+
If initialized like this::
|
|
48
|
+
|
|
49
|
+
application = web.Application([
|
|
50
|
+
(r"/content/(.*)", web.MultiStaticFileHandler, {"paths": ["/var/1", "/var/2"]}),
|
|
51
|
+
])
|
|
52
|
+
|
|
53
|
+
A file will be looked up in /var/1 first, then in /var/2.
|
|
54
|
+
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def initialize(self, paths, default_filename=None): # type: ignore
|
|
58
|
+
self.roots = paths
|
|
59
|
+
super().initialize(path=paths[0], default_filename=default_filename)
|
|
60
|
+
|
|
61
|
+
def get_absolute_path(self, root: str, path: str) -> str: # type: ignore
|
|
62
|
+
# find the first absolute path that exists
|
|
63
|
+
self.root = self.roots[0]
|
|
64
|
+
abspath = os.path.abspath(os.path.join(root, path))
|
|
65
|
+
for root in self.roots[1:]:
|
|
66
|
+
abspath = os.path.abspath(os.path.join(root, path))
|
|
67
|
+
if os.path.exists(abspath):
|
|
68
|
+
self.root = root # make sure all the other methods in the base class know how to find the file
|
|
69
|
+
break
|
|
70
|
+
|
|
71
|
+
# tornado probably already does a version of this, to make sure it behaves as the rest of the solara
|
|
72
|
+
# server, we do it again
|
|
73
|
+
if not path_is_child_of(Path(abspath), Path(self.root)):
|
|
74
|
+
raise PermissionError(f"Trying to read from outside of cache directory: {abspath} is not a subdir of {self.root}")
|
|
75
|
+
|
|
76
|
+
return abspath
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class Assets(MultiStaticFileHandler):
|
|
80
|
+
def initialize(self): # type: ignore
|
|
81
|
+
super().initialize(server.asset_directories())
|
|
82
|
+
logging.error("Using %r as assets directories", self.roots)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class ReadyZ(JupyterHandler):
|
|
86
|
+
def get(self):
|
|
87
|
+
json_data, status = server.readyz()
|
|
88
|
+
json_response = json.dumps(json_data)
|
|
89
|
+
self.set_header("Content-Type", "application/json")
|
|
90
|
+
self.set_status(status)
|
|
91
|
+
self.write(json_response)
|
solara/server/patch.py
CHANGED
|
@@ -38,6 +38,7 @@ class FakeIPython:
|
|
|
38
38
|
# needed for the pyplot interface of matplotlib
|
|
39
39
|
# (although we don't really support it)
|
|
40
40
|
self.events = mock.MagicMock()
|
|
41
|
+
self.user_ns: Dict[Any, Any] = {}
|
|
41
42
|
|
|
42
43
|
def enable_gui(self, gui):
|
|
43
44
|
logger.error("ignoring call to enable_gui(%s)", gui)
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
from PyInstaller.utils.hooks import collect_data_files, copy_metadata, collect_submodules
|
|
2
|
+
|
|
3
|
+
hiddenimports = collect_submodules("ipyvuetify")
|
|
4
|
+
datas = collect_data_files("ipyvuetify") # codespell:ignore datas
|
|
5
|
+
datas += copy_metadata("ipyvuetify") # codespell:ignore datas
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from PyInstaller.utils.hooks import collect_data_files, collect_submodules
|
|
2
|
+
|
|
3
|
+
hiddenimports = collect_submodules("solara")
|
|
4
|
+
datas = collect_data_files("solara") # codespell:ignore datas
|
|
5
|
+
datas += collect_data_files("solara-ui") # codespell:ignore datas
|
|
6
|
+
|
|
7
|
+
# this makes sure that inspect.getfile works, which is used for the
|
|
8
|
+
# vue component decorator
|
|
9
|
+
module_collection_mode = "pyz+py"
|
solara/server/qt.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from typing import List
|
|
3
|
+
import webbrowser
|
|
4
|
+
from qtpy.QtWidgets import QApplication
|
|
5
|
+
from qtpy.QtWebEngineWidgets import QWebEngineView
|
|
6
|
+
from qtpy.QtWebChannel import QWebChannel
|
|
7
|
+
from qtpy import QtCore, QtGui
|
|
8
|
+
import signal
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
HERE = Path(__file__).parent
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# setUrlRequestInterceptor, navigationRequested and acceptNavigationRequest
|
|
15
|
+
# all trigger the websocket to disconnect, so we need to block cross origin
|
|
16
|
+
# requests on the frontend/browser side by intercepting clicks on links
|
|
17
|
+
|
|
18
|
+
cross_origin_block_js = """
|
|
19
|
+
var script = document.createElement('script');
|
|
20
|
+
script.src = 'qrc:///qtwebchannel/qwebchannel.js';
|
|
21
|
+
document.head.appendChild(script);
|
|
22
|
+
script.onload = function() {
|
|
23
|
+
new QWebChannel(qt.webChannelTransport, function(channel) {
|
|
24
|
+
let py_callback = channel.objects.py_callback;
|
|
25
|
+
|
|
26
|
+
document.addEventListener('click', function(event) {
|
|
27
|
+
let target = event.target;
|
|
28
|
+
while (target && target.tagName !== 'A') {
|
|
29
|
+
target = target.parentNode;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (target && target.tagName === 'A') {
|
|
33
|
+
const linkOrigin = new URL(target.href).origin;
|
|
34
|
+
const currentOrigin = window.location.origin;
|
|
35
|
+
|
|
36
|
+
if (linkOrigin !== currentOrigin) {
|
|
37
|
+
event.preventDefault();
|
|
38
|
+
console.log("Blocked cross-origin navigation to:", target.href);
|
|
39
|
+
py_callback.open_link(target.href); // Call Python method
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}, true);
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class PyCallback(QtCore.QObject):
|
|
49
|
+
@QtCore.Slot(str)
|
|
50
|
+
def open_link(self, url):
|
|
51
|
+
webbrowser.open(url)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class QWebEngineViewWithPopup(QWebEngineView):
|
|
55
|
+
# keep a strong reference to all windows
|
|
56
|
+
windows: List = []
|
|
57
|
+
|
|
58
|
+
def __init__(self):
|
|
59
|
+
super().__init__()
|
|
60
|
+
self.page().newWindowRequested.connect(self.handle_new_window_request)
|
|
61
|
+
|
|
62
|
+
# Set up WebChannel and py_callback object
|
|
63
|
+
self.py_callback = PyCallback()
|
|
64
|
+
self.channel = QWebChannel()
|
|
65
|
+
self.channel.registerObject("py_callback", self.py_callback)
|
|
66
|
+
self.page().setWebChannel(self.channel)
|
|
67
|
+
|
|
68
|
+
self.loadFinished.connect(self._inject_javascript)
|
|
69
|
+
|
|
70
|
+
def _inject_javascript(self, ok):
|
|
71
|
+
self.page().runJavaScript(cross_origin_block_js)
|
|
72
|
+
|
|
73
|
+
def handle_new_window_request(self, info):
|
|
74
|
+
webview = QWebEngineViewWithPopup()
|
|
75
|
+
geometry = info.requestedGeometry()
|
|
76
|
+
webview.resize(geometry.width(), geometry.height())
|
|
77
|
+
webview.setUrl(info.requestedUrl())
|
|
78
|
+
webview.show()
|
|
79
|
+
QWebEngineViewWithPopup.windows.append(webview)
|
|
80
|
+
return webview
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def run_qt(url):
|
|
84
|
+
app = QApplication([])
|
|
85
|
+
web = QWebEngineViewWithPopup()
|
|
86
|
+
web.setUrl(QtCore.QUrl(url))
|
|
87
|
+
web.resize(1024, 1024)
|
|
88
|
+
web.show()
|
|
89
|
+
|
|
90
|
+
app_name = "Solara"
|
|
91
|
+
app.setApplicationDisplayName(app_name)
|
|
92
|
+
app.setApplicationName(app_name)
|
|
93
|
+
web.setWindowTitle(app_name)
|
|
94
|
+
app.setWindowIcon(QtGui.QIcon(str(HERE.parent / "website/public/logo.svg")))
|
|
95
|
+
if sys.platform.startswith("darwin"):
|
|
96
|
+
# Set app name, if PyObjC is installed
|
|
97
|
+
# Python 2 has PyObjC preinstalled
|
|
98
|
+
# Python 3: pip3 install pyobjc-framework-Cocoa
|
|
99
|
+
try:
|
|
100
|
+
from Foundation import NSBundle
|
|
101
|
+
|
|
102
|
+
bundle = NSBundle.mainBundle()
|
|
103
|
+
if bundle:
|
|
104
|
+
app_info = bundle.localizedInfoDictionary() or bundle.infoDictionary()
|
|
105
|
+
if app_info is not None:
|
|
106
|
+
app_info["CFBundleName"] = app_name
|
|
107
|
+
app_info["CFBundleDisplayName"] = app_name
|
|
108
|
+
except ModuleNotFoundError:
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
# without this, ctrl-c does not work in the terminal
|
|
112
|
+
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
|
113
|
+
app.exec_()
|
solara/server/server.py
CHANGED
|
@@ -6,7 +6,7 @@ import os
|
|
|
6
6
|
import sys
|
|
7
7
|
import time
|
|
8
8
|
from pathlib import Path
|
|
9
|
-
from typing import Dict, List, Optional, Tuple, TypeVar
|
|
9
|
+
from typing import Dict, List, Optional, Tuple, TypeVar, Union
|
|
10
10
|
|
|
11
11
|
import ipykernel
|
|
12
12
|
import ipyvue
|
|
@@ -268,6 +268,7 @@ def asset_directories():
|
|
|
268
268
|
def read_root(
|
|
269
269
|
path: str,
|
|
270
270
|
root_path: str = "",
|
|
271
|
+
jupyter_root_path: Union[str, None] = None,
|
|
271
272
|
render_kwargs={},
|
|
272
273
|
use_nbextensions=True,
|
|
273
274
|
ssg_data=None,
|
|
@@ -373,10 +374,14 @@ def read_root(
|
|
|
373
374
|
else:
|
|
374
375
|
cdn = solara.settings.assets.cdn
|
|
375
376
|
|
|
377
|
+
if jupyter_root_path is None:
|
|
378
|
+
jupyter_root_path = f"{root_path}/jupyter"
|
|
379
|
+
|
|
376
380
|
render_settings = {
|
|
377
381
|
"title": title,
|
|
378
382
|
"path": path,
|
|
379
383
|
"root_path": root_path,
|
|
384
|
+
"jupyter_root_path": jupyter_root_path,
|
|
380
385
|
"resources": resources,
|
|
381
386
|
"theme": settings.theme.dict(),
|
|
382
387
|
"production": settings.main.mode == "production",
|