solara-ui 1.38.0__py2.py3-none-any.whl → 1.40.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 +2 -2
- solara/__main__.py +8 -1
- solara/checks.py +4 -1
- solara/components/__init__.py +1 -0
- solara/components/echarts.py +8 -0
- solara/components/echarts.vue +1 -1
- solara/components/input_text_area.py +86 -0
- solara/components/markdown.py +10 -1
- solara/components/markdown_editor.py +8 -0
- solara/components/markdown_editor.vue +1 -1
- solara/components/sql_code.py +8 -0
- solara/components/sql_code.vue +1 -1
- solara/hooks/use_thread.py +4 -4
- solara/lab/components/chat.py +8 -2
- solara/server/assets/style.css +2 -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/server.py +6 -1
- solara/server/starlette.py +18 -6
- solara/server/static/highlight-dark.css +1 -1
- solara/server/static/main-vuetify.js +1 -1
- solara/server/static/solara_bootstrap.py +1 -1
- solara/server/templates/solara.html.j2 +30 -6
- 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 +7 -1
- solara/website/pages/__init__.py +87 -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 +23 -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/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 +2 -2
- 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/gridlayout.vue +1 -1
- solara/widgets/widgets.py +8 -0
- {solara_ui-1.38.0.dist-info → solara_ui-1.40.0.dist-info}/METADATA +2 -2
- {solara_ui-1.38.0.dist-info → solara_ui-1.40.0.dist-info}/RECORD +75 -59
- solara/website/components/hero.py +0 -15
- solara/website/pages/contact/contact.md +0 -17
- {solara_ui-1.38.0.data → solara_ui-1.40.0.data}/data/etc/jupyter/jupyter_notebook_config.d/solara.json +0 -0
- {solara_ui-1.38.0.data → solara_ui-1.40.0.data}/data/etc/jupyter/jupyter_server_config.d/solara.json +0 -0
- {solara_ui-1.38.0.dist-info → solara_ui-1.40.0.dist-info}/WHEEL +0 -0
- {solara_ui-1.38.0.dist-info → solara_ui-1.40.0.dist-info}/licenses/LICENSE +0 -0
solara/__init__.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Build webapps using IPywidgets"""
|
|
2
2
|
|
|
3
|
-
__version__ = "1.
|
|
3
|
+
__version__ = "1.40.0"
|
|
4
4
|
github_url = "https://github.com/widgetti/solara"
|
|
5
5
|
git_branch = "master"
|
|
6
6
|
|
|
@@ -11,7 +11,7 @@ from . import comm # noqa: F401
|
|
|
11
11
|
def _using_solara_server():
|
|
12
12
|
import sys
|
|
13
13
|
|
|
14
|
-
if "solara.server" in sys.modules:
|
|
14
|
+
if "solara.server.starlette" in sys.modules or "solara.server.flask" in sys.modules:
|
|
15
15
|
return True
|
|
16
16
|
if sys.argv[0].split("/")[-1] == "solara":
|
|
17
17
|
return True
|
solara/__main__.py
CHANGED
|
@@ -17,6 +17,7 @@ from uvicorn.main import LEVEL_CHOICES, LOOP_CHOICES
|
|
|
17
17
|
|
|
18
18
|
import solara
|
|
19
19
|
from solara.server import settings
|
|
20
|
+
import solara.server.threaded
|
|
20
21
|
|
|
21
22
|
from .server import telemetry
|
|
22
23
|
|
|
@@ -130,7 +131,11 @@ if "SOLARA_MODE" in os.environ:
|
|
|
130
131
|
|
|
131
132
|
|
|
132
133
|
@cli.command()
|
|
133
|
-
@click.option(
|
|
134
|
+
@click.option(
|
|
135
|
+
"--port",
|
|
136
|
+
default=int(os.environ.get("PORT", 8765)),
|
|
137
|
+
help="Port to run the server on, 0 for a random free port, default to $PORT or 8765.",
|
|
138
|
+
)
|
|
134
139
|
@click.option(
|
|
135
140
|
"--host",
|
|
136
141
|
default=settings.main.host,
|
|
@@ -302,6 +307,8 @@ def run(
|
|
|
302
307
|
settings.ssg.enabled = ssg
|
|
303
308
|
settings.search.enabled = search
|
|
304
309
|
reload_dirs = restart_dirs if restart_dirs else None
|
|
310
|
+
if port == 0:
|
|
311
|
+
port = solara.server.threaded.get_free_port()
|
|
305
312
|
del restart_dirs
|
|
306
313
|
url = f"http://{host}:{port}"
|
|
307
314
|
|
solara/checks.py
CHANGED
|
@@ -124,6 +124,9 @@ def SolaraCheck():
|
|
|
124
124
|
def getcmdline(pid):
|
|
125
125
|
# for linux
|
|
126
126
|
if sys.platform == "linux":
|
|
127
|
+
# if /proc/{pid}/exe exists, follow the symlink
|
|
128
|
+
if os.path.exists(f"/proc/{pid}/exe"):
|
|
129
|
+
return os.readlink(f"/proc/{pid}/exe")
|
|
127
130
|
with open(f"/proc/{pid}/cmdline", "rb") as f:
|
|
128
131
|
return f.read().split(b"\00")[0].decode("utf-8")
|
|
129
132
|
elif sys.platform == "darwin":
|
|
@@ -176,7 +179,7 @@ libraries_extra = [
|
|
|
176
179
|
{"python": "bqplot", "classic": "bqplot/extension", "lab": "bqplot"},
|
|
177
180
|
{"python": "ipyvolume", "classic": "ipyvolume/extension", "lab": "ipyvolume"},
|
|
178
181
|
{"python": "ipywebrtc", "classic": "jupyter-webrtc", "lab": "jupyter-webrtc"},
|
|
179
|
-
{"python": "ipyleaflet", "classic": "
|
|
182
|
+
{"python": "ipyleaflet", "classic": "jupyter-leaflet/extension", "lab": "jupyter-leaflet"},
|
|
180
183
|
]
|
|
181
184
|
|
|
182
185
|
|
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
|
solara/components/echarts.py
CHANGED
|
@@ -20,6 +20,14 @@ class EchartsWidget(ipyvuetify.VuetifyTemplate):
|
|
|
20
20
|
on_mouseout = traitlets.Callable(None, allow_none=True)
|
|
21
21
|
# same, for performance
|
|
22
22
|
on_mouseout_enabled = traitlets.Bool(False).tag(sync=True)
|
|
23
|
+
cdn = traitlets.Unicode(None, allow_none=True).tag(sync=True)
|
|
24
|
+
|
|
25
|
+
@traitlets.default("cdn")
|
|
26
|
+
def _cdn(self):
|
|
27
|
+
import solara.settings
|
|
28
|
+
|
|
29
|
+
if not solara.settings.assets.proxy:
|
|
30
|
+
return solara.settings.assets.cdn
|
|
23
31
|
|
|
24
32
|
def vue_on_click(self, data):
|
|
25
33
|
if self.on_click is not None:
|
solara/components/echarts.vue
CHANGED
|
@@ -110,7 +110,7 @@ module.exports = {
|
|
|
110
110
|
return base;
|
|
111
111
|
},
|
|
112
112
|
getCdn() {
|
|
113
|
-
return window.solara ? window.solara.cdn : `${this.getJupyterBaseUrl()}_solara/cdn`
|
|
113
|
+
return this.cdn || (window.solara ? window.solara.cdn : `${this.getJupyterBaseUrl()}_solara/cdn`);
|
|
114
114
|
},
|
|
115
115
|
},
|
|
116
116
|
};
|
|
@@ -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
|
@@ -76,6 +76,12 @@ def _markdown_template(
|
|
|
76
76
|
html,
|
|
77
77
|
style="",
|
|
78
78
|
):
|
|
79
|
+
cdn = None
|
|
80
|
+
import solara.settings
|
|
81
|
+
|
|
82
|
+
if not solara.settings.assets.proxy:
|
|
83
|
+
cdn = solara.settings.assets.cdn
|
|
84
|
+
|
|
79
85
|
template = (
|
|
80
86
|
"""
|
|
81
87
|
<template>
|
|
@@ -89,6 +95,9 @@ def _markdown_template(
|
|
|
89
95
|
<script>
|
|
90
96
|
module.exports = {
|
|
91
97
|
async mounted() {
|
|
98
|
+
this.cdn = """
|
|
99
|
+
+ (rf"'{cdn}'" if cdn is not None else r"null")
|
|
100
|
+
+ r""";
|
|
92
101
|
await this.loadRequire();
|
|
93
102
|
this.mermaid = await this.loadMermaid();
|
|
94
103
|
this.mermaid.init();
|
|
@@ -119,7 +128,7 @@ module.exports = {
|
|
|
119
128
|
href = location.pathname + href.substr(1);
|
|
120
129
|
a.attributes['href'].href = href;
|
|
121
130
|
}
|
|
122
|
-
let authLink = href.
|
|
131
|
+
let authLink = href.startsWith("/_solara/auth/");
|
|
123
132
|
if( (href.startsWith("./") || href.startsWith("/")) && !authLink) {
|
|
124
133
|
a.onclick = e => {
|
|
125
134
|
console.log("clicked", href)
|
|
@@ -11,6 +11,14 @@ class MarkdownEditorWidget(ipyvuetify.VuetifyTemplate):
|
|
|
11
11
|
|
|
12
12
|
value = traitlets.Unicode("").tag(sync=True)
|
|
13
13
|
height = traitlets.Unicode("180px").tag(sync=True)
|
|
14
|
+
cdn = traitlets.Unicode(None, allow_none=True).tag(sync=True)
|
|
15
|
+
|
|
16
|
+
@traitlets.default("cdn")
|
|
17
|
+
def _cdn(self):
|
|
18
|
+
import solara.settings
|
|
19
|
+
|
|
20
|
+
if not solara.settings.assets.proxy:
|
|
21
|
+
return solara.settings.assets.cdn
|
|
14
22
|
|
|
15
23
|
|
|
16
24
|
@solara.component
|
|
@@ -264,7 +264,7 @@ module.exports = {
|
|
|
264
264
|
return base
|
|
265
265
|
},
|
|
266
266
|
getCdn() {
|
|
267
|
-
return window.solara ? window.solara.cdn : `${this.getJupyterBaseUrl()}_solara/cdn
|
|
267
|
+
return this.cdn || (window.solara ? window.solara.cdn : `${this.getJupyterBaseUrl()}_solara/cdn`);
|
|
268
268
|
},
|
|
269
269
|
},
|
|
270
270
|
};
|
solara/components/sql_code.py
CHANGED
|
@@ -13,6 +13,14 @@ class SqlCodeWidget(ipyvue.VueTemplate):
|
|
|
13
13
|
query = traitlets.Unicode(allow_none=True, default_value=None).tag(sync=True)
|
|
14
14
|
tables = traitlets.Dict(allow_none=True, default_value=None).tag(sync=True)
|
|
15
15
|
height = traitlets.Unicode("180px").tag(sync=True)
|
|
16
|
+
cdn = traitlets.Unicode(None, allow_none=True).tag(sync=True)
|
|
17
|
+
|
|
18
|
+
@traitlets.default("cdn")
|
|
19
|
+
def _cdn(self):
|
|
20
|
+
import solara.settings
|
|
21
|
+
|
|
22
|
+
if not solara.settings.assets.proxy:
|
|
23
|
+
return solara.settings.assets.cdn
|
|
16
24
|
|
|
17
25
|
|
|
18
26
|
@solara.component
|
solara/components/sql_code.vue
CHANGED
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
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/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",
|
solara/server/starlette.py
CHANGED
|
@@ -391,16 +391,16 @@ See also https://solara.dev/documentation/getting_started/deploying/self-hosted
|
|
|
391
391
|
"""
|
|
392
392
|
if "script-name" in request.headers:
|
|
393
393
|
msg += f"""It looks like the reverse proxy sets the script-name header to {request.headers['script-name']!r}
|
|
394
|
-
|
|
394
|
+
"""
|
|
395
395
|
if "x-script-name" in request.headers:
|
|
396
396
|
msg += f"""It looks like the reverse proxy sets the x-script-name header to {request.headers['x-script-name']!r}
|
|
397
|
-
|
|
397
|
+
"""
|
|
398
398
|
if configured_root_path:
|
|
399
399
|
msg += f"""It looks like the root path was configured to {configured_root_path!r} in the settings
|
|
400
|
-
|
|
400
|
+
"""
|
|
401
401
|
if root_path_asgi:
|
|
402
402
|
msg += f"""It looks like the root path set by the asgi framework was configured to {root_path_asgi!r}
|
|
403
|
-
|
|
403
|
+
"""
|
|
404
404
|
warnings.warn(msg)
|
|
405
405
|
if host and forwarded_host and forwarded_proto:
|
|
406
406
|
port = request.base_url.port
|
|
@@ -451,6 +451,18 @@ See also https://solara.dev/documentation/getting_started/deploying/self-hosted
|
|
|
451
451
|
if request.scope["scheme"] == "https" or request.headers.get("x-forwarded-proto", "http") == "https" or request.base_url.hostname == "localhost":
|
|
452
452
|
samesite = "none"
|
|
453
453
|
secure = True
|
|
454
|
+
elif request.base_url.hostname != "localhost":
|
|
455
|
+
warnings.warn(f"""Cookies with samesite=none require https, but according to the asgi framework, the scheme is {request.scope['scheme']!r}
|
|
456
|
+
and the x-forwarded-proto header is {request.headers.get('x-forwarded-proto', 'http')!r}. We will fallback to samesite=lax.
|
|
457
|
+
|
|
458
|
+
If you embed solara in an iframe, make sure you forward the x-forwarded-proto header correctly so that the session cookie can be set.
|
|
459
|
+
|
|
460
|
+
See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite for more information on samesite cookies.
|
|
461
|
+
|
|
462
|
+
Also check out the following Solara documentation:
|
|
463
|
+
* https://solara.dev/documentation/getting_started/deploying/self-hosted
|
|
464
|
+
* https://solara.dev/documentation/advanced/howto/embed
|
|
465
|
+
""")
|
|
454
466
|
response.set_cookie(
|
|
455
467
|
server.COOKIE_KEY_SESSION_ID,
|
|
456
468
|
value=session_id,
|
|
@@ -483,9 +495,9 @@ class StaticNbFiles(StaticFilesOptionalAuth):
|
|
|
483
495
|
# from https://github.com/encode/starlette/pull/1377/files
|
|
484
496
|
def lookup_path(self, path: str) -> typing.Tuple[str, typing.Optional[os.stat_result]]:
|
|
485
497
|
for directory in self.all_directories:
|
|
498
|
+
directory = os.path.realpath(directory)
|
|
486
499
|
original_path = os.path.join(directory, path)
|
|
487
500
|
full_path = os.path.realpath(original_path)
|
|
488
|
-
directory = os.path.realpath(directory)
|
|
489
501
|
# return early if someone tries to access a file outside of the directory
|
|
490
502
|
if not path_is_child_of(Path(original_path), Path(directory)):
|
|
491
503
|
return "", None
|
|
@@ -668,7 +680,7 @@ routes = [
|
|
|
668
680
|
*([Mount(f"/{cdn_url_path}", app=StaticCdn(directory=settings.assets.proxy_cache_dir))] if solara.settings.assets.proxy else []),
|
|
669
681
|
Mount(f"{prefix}/static/public", app=StaticPublic()),
|
|
670
682
|
Mount(f"{prefix}/static/assets", app=StaticAssets()),
|
|
671
|
-
Mount(f"{prefix}/
|
|
683
|
+
Mount(f"{prefix}/jupyter/nbextensions", app=StaticNbFiles()),
|
|
672
684
|
Mount(f"{prefix}/static", app=StaticFilesOptionalAuth(directory=server.solara_static)),
|
|
673
685
|
Route("/{fullpath:path}", endpoint=root),
|
|
674
686
|
]
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
and then remove the overlapping first lines with highlight.css
|
|
3
3
|
*/
|
|
4
4
|
.theme--dark.v-application .highlight .hll { background-color: #ffffcc }
|
|
5
|
-
.theme--dark.v-application .highlight { background: #
|
|
5
|
+
.theme--dark.v-application .highlight { background: #1d1f22; color: #ABB2BF }
|
|
6
6
|
.theme--dark.v-application .highlight .c { color: #7F848E } /* Comment */
|
|
7
7
|
.theme--dark.v-application .highlight .err { color: #ABB2BF } /* Error */
|
|
8
8
|
.theme--dark.v-application .highlight .esc { color: #ABB2BF } /* Escape */
|
|
@@ -127,7 +127,7 @@ async function solaraInit(mountId, appName) {
|
|
|
127
127
|
window.navigator.sendBeacon(close_url);
|
|
128
128
|
}
|
|
129
129
|
});
|
|
130
|
-
let kernel = await solara.connectKernel(solara.
|
|
130
|
+
let kernel = await solara.connectKernel(solara.jupyterRootPath, kernelId)
|
|
131
131
|
if (!kernel) {
|
|
132
132
|
return;
|
|
133
133
|
}
|
|
@@ -119,7 +119,7 @@ async def main():
|
|
|
119
119
|
]
|
|
120
120
|
for dep in requirements:
|
|
121
121
|
await micropip.install(dep, keep_going=True)
|
|
122
|
-
await micropip.install("/wheels/solara-1.
|
|
122
|
+
await micropip.install("/wheels/solara-1.40.0-py2.py3-none-any.whl", keep_going=True)
|
|
123
123
|
import solara
|
|
124
124
|
|
|
125
125
|
el = solara.Warning("lala")
|