solara-ui 1.42.0__py2.py3-none-any.whl → 1.43.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 +10 -5
- solara/_stores.py +14 -10
- solara/components/__init__.py +18 -1
- solara/components/datatable.py +4 -4
- solara/components/input.py +5 -1
- solara/components/markdown.py +46 -10
- solara/components/misc.py +2 -2
- solara/lab/components/__init__.py +1 -0
- solara/lab/components/chat.py +3 -3
- solara/lab/components/input_time.py +133 -0
- solara/lab/hooks/dataframe.py +1 -0
- solara/lab/utils/dataframe.py +11 -1
- solara/server/app.py +63 -30
- solara/server/flask.py +12 -2
- solara/server/jupyter/server_extension.py +1 -0
- solara/server/kernel.py +50 -3
- solara/server/kernel_context.py +66 -7
- solara/server/patch.py +25 -29
- solara/server/server.py +15 -5
- solara/server/settings.py +11 -0
- solara/server/shell.py +19 -1
- solara/server/starlette.py +37 -9
- solara/server/static/solara_bootstrap.py +1 -1
- solara/settings.py +3 -0
- solara/tasks.py +18 -8
- solara/test/pytest_plugin.py +1 -0
- solara/toestand.py +33 -2
- solara/util.py +18 -0
- solara/website/components/docs.py +4 -0
- solara/website/components/markdown.py +17 -3
- solara/website/pages/changelog/changelog.md +9 -1
- solara/website/pages/documentation/advanced/content/20-understanding/50-solara-server.md +10 -0
- solara/website/pages/documentation/api/routing/route.py +10 -12
- solara/website/pages/documentation/api/routing/use_route.py +26 -30
- solara/website/pages/documentation/components/advanced/link.py +6 -8
- solara/website/pages/documentation/components/advanced/meta.py +6 -9
- solara/website/pages/documentation/components/advanced/style.py +7 -9
- solara/website/pages/documentation/components/input/file_browser.py +12 -14
- solara/website/pages/documentation/components/lab/input_time.py +15 -0
- solara/website/pages/documentation/components/layout/columns_responsive.py +37 -39
- solara/website/pages/documentation/components/layout/gridfixed.py +4 -6
- solara/website/pages/documentation/components/output/html.py +1 -3
- solara/website/pages/documentation/components/page/head.py +4 -7
- solara/website/pages/documentation/examples/__init__.py +9 -0
- solara/website/pages/documentation/examples/ai/chatbot.py +60 -44
- solara/website/pages/documentation/examples/general/live_update.py +1 -0
- solara/website/pages/documentation/faq/content/99-faq.md +9 -0
- solara/website/pages/documentation/getting_started/content/00-quickstart.md +1 -1
- solara/website/pages/documentation/getting_started/content/04-tutorials/_jupyter_dashboard_1.ipynb +23 -19
- solara/website/pages/documentation/getting_started/content/07-deploying/10-self-hosted.md +2 -2
- solara/website/pages/roadmap/roadmap.md +3 -0
- {solara_ui-1.42.0.dist-info → solara_ui-1.43.0.dist-info}/METADATA +2 -2
- {solara_ui-1.42.0.dist-info → solara_ui-1.43.0.dist-info}/RECORD +58 -56
- {solara_ui-1.42.0.data → solara_ui-1.43.0.data}/data/etc/jupyter/jupyter_notebook_config.d/solara.json +0 -0
- {solara_ui-1.42.0.data → solara_ui-1.43.0.data}/data/etc/jupyter/jupyter_server_config.d/solara.json +0 -0
- {solara_ui-1.42.0.dist-info → solara_ui-1.43.0.dist-info}/WHEEL +0 -0
- {solara_ui-1.42.0.dist-info → solara_ui-1.43.0.dist-info}/licenses/LICENSE +0 -0
solara/server/starlette.py
CHANGED
|
@@ -15,6 +15,7 @@ import anyio
|
|
|
15
15
|
import starlette.websockets
|
|
16
16
|
import uvicorn.server
|
|
17
17
|
import websockets.legacy.http
|
|
18
|
+
import websockets.exceptions
|
|
18
19
|
|
|
19
20
|
from solara.server.utils import path_is_child_of
|
|
20
21
|
|
|
@@ -121,37 +122,55 @@ class WebsocketWrapper(websocket.WebsocketWrapper):
|
|
|
121
122
|
while len(self.to_send) > 0:
|
|
122
123
|
first = self.to_send.pop(0)
|
|
123
124
|
if isinstance(first, bytes):
|
|
124
|
-
await self.
|
|
125
|
+
await self._send_bytes_exc(first)
|
|
125
126
|
else:
|
|
126
|
-
await self.
|
|
127
|
+
await self._send_text_exc(first)
|
|
128
|
+
|
|
129
|
+
async def _send_bytes_exc(self, data: bytes):
|
|
130
|
+
# make sures we catch the starlette/websockets specific exception
|
|
131
|
+
# and re-raise it as a websocket.WebSocketDisconnect
|
|
132
|
+
try:
|
|
133
|
+
await self.ws.send_bytes(data)
|
|
134
|
+
except (websockets.exceptions.ConnectionClosed, starlette.websockets.WebSocketDisconnect, RuntimeError) as e:
|
|
135
|
+
# starlette throws a RuntimeError once you call send after the connection is closed
|
|
136
|
+
raise websocket.WebSocketDisconnect() from e
|
|
137
|
+
|
|
138
|
+
async def _send_text_exc(self, data: str):
|
|
139
|
+
# make sures we catch the starlette/websockets specific exception
|
|
140
|
+
# and re-raise it as a websocket.WebSocketDisconnect
|
|
141
|
+
try:
|
|
142
|
+
await self.ws.send_text(data)
|
|
143
|
+
except (websockets.exceptions.ConnectionClosed, starlette.websockets.WebSocketDisconnect, RuntimeError) as e:
|
|
144
|
+
# starlette throws a RuntimeError once you call send after the connection is closed
|
|
145
|
+
raise websocket.WebSocketDisconnect() from e
|
|
127
146
|
|
|
128
147
|
def close(self):
|
|
129
148
|
if self.portal is None:
|
|
130
149
|
asyncio.ensure_future(self.ws.close())
|
|
131
150
|
else:
|
|
132
|
-
self.portal.call(self.ws.close)
|
|
151
|
+
self.portal.call(self.ws.close)
|
|
133
152
|
|
|
134
153
|
def send_text(self, data: str) -> None:
|
|
135
154
|
if self.portal is None:
|
|
136
|
-
task = self.event_loop.create_task(self.
|
|
155
|
+
task = self.event_loop.create_task(self._send_text_exc(data))
|
|
137
156
|
self.tasks.add(task)
|
|
138
157
|
task.add_done_callback(self.tasks.discard)
|
|
139
158
|
else:
|
|
140
159
|
if settings.main.experimental_performance:
|
|
141
160
|
self.to_send.append(data)
|
|
142
161
|
else:
|
|
143
|
-
self.portal.call(self.
|
|
162
|
+
self.portal.call(self._send_text_exc, data)
|
|
144
163
|
|
|
145
164
|
def send_bytes(self, data: bytes) -> None:
|
|
146
165
|
if self.portal is None:
|
|
147
|
-
task = self.event_loop.create_task(self.
|
|
166
|
+
task = self.event_loop.create_task(self._send_bytes_exc(data))
|
|
148
167
|
self.tasks.add(task)
|
|
149
168
|
task.add_done_callback(self.tasks.discard)
|
|
150
169
|
else:
|
|
151
170
|
if settings.main.experimental_performance:
|
|
152
171
|
self.to_send.append(data)
|
|
153
172
|
else:
|
|
154
|
-
self.portal.call(self.
|
|
173
|
+
self.portal.call(self._send_bytes_exc, data)
|
|
155
174
|
|
|
156
175
|
async def receive(self):
|
|
157
176
|
if self.portal is None:
|
|
@@ -159,9 +178,9 @@ class WebsocketWrapper(websocket.WebsocketWrapper):
|
|
|
159
178
|
else:
|
|
160
179
|
if hasattr(self.portal, "start_task_soon"):
|
|
161
180
|
# version 3+
|
|
162
|
-
fut = self.portal.start_task_soon(self.ws.receive)
|
|
181
|
+
fut = self.portal.start_task_soon(self.ws.receive)
|
|
163
182
|
else:
|
|
164
|
-
fut = self.portal.spawn_task(self.ws.receive)
|
|
183
|
+
fut = self.portal.spawn_task(self.ws.receive)
|
|
165
184
|
|
|
166
185
|
message = await asyncio.wrap_future(fut)
|
|
167
186
|
if "text" in message:
|
|
@@ -444,6 +463,7 @@ See also https://solara.dev/documentation/getting_started/deploying/self-hosted
|
|
|
444
463
|
session_id = request.cookies.get(server.COOKIE_KEY_SESSION_ID) or str(uuid4())
|
|
445
464
|
samesite = "lax"
|
|
446
465
|
secure = False
|
|
466
|
+
httponly = settings.session.http_only
|
|
447
467
|
# we want samesite, so we can set a cookie when embedded in an iframe, such as on huggingface
|
|
448
468
|
# however, samesite=none requires Secure https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite
|
|
449
469
|
# when hosted on the localhost domain we can always set the Secure flag
|
|
@@ -469,6 +489,7 @@ Also check out the following Solara documentation:
|
|
|
469
489
|
expires="Fri, 01 Jan 2038 00:00:00 GMT",
|
|
470
490
|
samesite=samesite, # type: ignore
|
|
471
491
|
secure=secure, # type: ignore
|
|
492
|
+
httponly=httponly, # type: ignore
|
|
472
493
|
) # type: ignore
|
|
473
494
|
return response
|
|
474
495
|
|
|
@@ -549,12 +570,19 @@ class StaticCdn(StaticFilesOptionalAuth):
|
|
|
549
570
|
|
|
550
571
|
|
|
551
572
|
def on_startup():
|
|
573
|
+
appmod.ensure_apps_initialized()
|
|
552
574
|
# TODO: configure and set max number of threads
|
|
553
575
|
# see https://github.com/encode/starlette/issues/1724
|
|
554
576
|
telemetry.server_start()
|
|
555
577
|
|
|
556
578
|
|
|
557
579
|
def on_shutdown():
|
|
580
|
+
# shutdown all kernels
|
|
581
|
+
for context in list(kernel_context.contexts.values()):
|
|
582
|
+
try:
|
|
583
|
+
context.close()
|
|
584
|
+
except: # noqa
|
|
585
|
+
logger.exception("error closing kernel on shutdown")
|
|
558
586
|
telemetry.server_stop()
|
|
559
587
|
|
|
560
588
|
|
|
@@ -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.43.0-py2.py3-none-any.whl", keep_going=True)
|
|
123
123
|
import solara
|
|
124
124
|
|
|
125
125
|
el = solara.Warning("lala")
|
solara/settings.py
CHANGED
|
@@ -54,6 +54,9 @@ class Assets(BaseSettings):
|
|
|
54
54
|
|
|
55
55
|
class MainSettings(BaseSettings):
|
|
56
56
|
check_hooks: str = "warn"
|
|
57
|
+
allow_reactive_boolean: bool = True
|
|
58
|
+
# TODO: also change default_container in solara/components/__init__.py
|
|
59
|
+
default_container: Optional[str] = "Column"
|
|
57
60
|
|
|
58
61
|
class Config:
|
|
59
62
|
env_prefix = "solara_"
|
solara/tasks.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import sys
|
|
1
2
|
import abc
|
|
2
3
|
import asyncio
|
|
3
4
|
import dataclasses
|
|
@@ -27,6 +28,12 @@ from solara.toestand import Singleton
|
|
|
27
28
|
|
|
28
29
|
from .toestand import Ref as ref
|
|
29
30
|
|
|
31
|
+
if sys.version_info >= (3, 8):
|
|
32
|
+
from typing import Literal
|
|
33
|
+
else:
|
|
34
|
+
from typing_extensions import Literal
|
|
35
|
+
|
|
36
|
+
|
|
30
37
|
R = TypeVar("R")
|
|
31
38
|
T = TypeVar("T")
|
|
32
39
|
P = typing_extensions.ParamSpec("P")
|
|
@@ -686,24 +693,27 @@ def task(
|
|
|
686
693
|
return wrapper(f)
|
|
687
694
|
|
|
688
695
|
|
|
696
|
+
# Quotes around Task[...] are needed in Python <= 3.9, since ParamSpec doesn't properly support non-type arguments
|
|
697
|
+
# i.e. [] is taken as a value instead of a type
|
|
698
|
+
# See https://github.com/python/typing_extensions/issues/126 and related issues
|
|
689
699
|
@overload
|
|
690
700
|
def use_task(
|
|
691
701
|
f: None = None,
|
|
692
702
|
*,
|
|
693
|
-
dependencies: None = ...,
|
|
703
|
+
dependencies: Literal[None] = ...,
|
|
694
704
|
raise_error=...,
|
|
695
705
|
prefer_threaded=...,
|
|
696
|
-
) -> Callable[[Callable[
|
|
706
|
+
) -> Callable[[Callable[[], R]], "Task[[], R]"]: ...
|
|
697
707
|
|
|
698
708
|
|
|
699
709
|
@overload
|
|
700
710
|
def use_task(
|
|
701
|
-
f: Callable[
|
|
711
|
+
f: Callable[[], R],
|
|
702
712
|
*,
|
|
703
|
-
dependencies: None = ...,
|
|
713
|
+
dependencies: Literal[None] = ...,
|
|
704
714
|
raise_error=...,
|
|
705
715
|
prefer_threaded=...,
|
|
706
|
-
) -> Task[
|
|
716
|
+
) -> "Task[[], R]": ...
|
|
707
717
|
|
|
708
718
|
|
|
709
719
|
@overload
|
|
@@ -727,12 +737,12 @@ def use_task(
|
|
|
727
737
|
|
|
728
738
|
|
|
729
739
|
def use_task(
|
|
730
|
-
f: Union[None, Callable[
|
|
740
|
+
f: Union[None, Callable[[], R]] = None,
|
|
731
741
|
*,
|
|
732
742
|
dependencies: Union[None, List] = [],
|
|
733
743
|
raise_error=True,
|
|
734
744
|
prefer_threaded=True,
|
|
735
|
-
) -> Union[Callable[[Callable[
|
|
745
|
+
) -> Union[Callable[[Callable[[], R]], "Task[[], R]"], "Task[[], R]"]:
|
|
736
746
|
"""A hook that runs a function or coroutine function as a task and returns the result.
|
|
737
747
|
|
|
738
748
|
Allows you to run code in the background, with the UI available to the user. This is useful for long running tasks,
|
|
@@ -811,7 +821,7 @@ def use_task(
|
|
|
811
821
|
"""
|
|
812
822
|
|
|
813
823
|
def wrapper(f):
|
|
814
|
-
def create_task() -> Task[
|
|
824
|
+
def create_task() -> "Task[[], R]":
|
|
815
825
|
return task(f, prefer_threaded=prefer_threaded)
|
|
816
826
|
|
|
817
827
|
task_instance = solara.use_memo(create_task, dependencies=[])
|
solara/test/pytest_plugin.py
CHANGED
solara/toestand.py
CHANGED
|
@@ -98,6 +98,37 @@ class ValueBase(Generic[T]):
|
|
|
98
98
|
self.listeners: Dict[str, Set[Tuple[Callable[[T], None], Optional[ContextManager]]]] = defaultdict(set)
|
|
99
99
|
self.listeners2: Dict[str, Set[Tuple[Callable[[T, T], None], Optional[ContextManager]]]] = defaultdict(set)
|
|
100
100
|
|
|
101
|
+
# make sure all boolean operations give type errors
|
|
102
|
+
if not solara.settings.main.allow_reactive_boolean:
|
|
103
|
+
|
|
104
|
+
def __bool__(self):
|
|
105
|
+
raise TypeError("Reactive vars are not allowed in boolean expressions, did you mean to use .value?")
|
|
106
|
+
|
|
107
|
+
def __eq__(self, other):
|
|
108
|
+
raise TypeError(f"'==' not supported between a Reactive and {other.__class__.__name__}, did you mean to use .value?")
|
|
109
|
+
|
|
110
|
+
def __ne__(self, other):
|
|
111
|
+
raise TypeError(f"'!=' not supported between a Reactive and {other.__class__.__name__}, did you mean to use .value?")
|
|
112
|
+
|
|
113
|
+
# If we explicitly define __eq__, we need to explicitly define __hash__ as well
|
|
114
|
+
# Otherwise our class is marked unhashable
|
|
115
|
+
__hash__ = object.__hash__
|
|
116
|
+
|
|
117
|
+
def __lt__(self, other):
|
|
118
|
+
raise TypeError(f"'<' not supported between a Reactive and {other.__class__.__name__}, did you mean to use .value?")
|
|
119
|
+
|
|
120
|
+
def __le__(self, other):
|
|
121
|
+
raise TypeError(f"'<=' not supported between a Reactive and {other.__class__.__name__}, did you mean to use .value?")
|
|
122
|
+
|
|
123
|
+
def __gt__(self, other):
|
|
124
|
+
raise TypeError(f"'>' not supported between a Reactive and {other.__class__.__name__}, did you mean to use .value?")
|
|
125
|
+
|
|
126
|
+
def __ge__(self, other):
|
|
127
|
+
raise TypeError(f"'>=' not supported between a Reactive and {other.__class__.__name__}, did you mean to use .value?")
|
|
128
|
+
|
|
129
|
+
def __len__(self):
|
|
130
|
+
raise TypeError("'len(...)' is not supported for a Reactive, did you mean to use .value?")
|
|
131
|
+
|
|
101
132
|
@property
|
|
102
133
|
def lock(self):
|
|
103
134
|
raise NotImplementedError
|
|
@@ -410,10 +441,10 @@ class KernelStoreFactory(KernelStore[S]):
|
|
|
410
441
|
|
|
411
442
|
def mutation_detection_storage(default_value: S, key=None, equals=None) -> ValueBase[S]:
|
|
412
443
|
from solara.util import equals_pickle as default_equals
|
|
413
|
-
from ._stores import MutateDetectorStore, StoreValue, _PublicValueNotSet
|
|
444
|
+
from ._stores import MutateDetectorStore, StoreValue, _PublicValueNotSet, _SetValueNotSet
|
|
414
445
|
|
|
415
446
|
kernel_store = KernelStoreValue[StoreValue[S]](
|
|
416
|
-
StoreValue[S](private=default_value, public=_PublicValueNotSet(), get_traceback=None, set_value=
|
|
447
|
+
StoreValue[S](private=default_value, public=_PublicValueNotSet(), get_traceback=None, set_value=_SetValueNotSet(), set_traceback=None),
|
|
417
448
|
key=key,
|
|
418
449
|
unwrap=lambda x: x.private,
|
|
419
450
|
)
|
solara/util.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import base64
|
|
2
2
|
import contextlib
|
|
3
|
+
import functools
|
|
3
4
|
import gzip
|
|
4
5
|
import hashlib
|
|
5
6
|
import json
|
|
@@ -328,3 +329,20 @@ def is_running_in_vscode():
|
|
|
328
329
|
|
|
329
330
|
def is_running_in_voila():
|
|
330
331
|
return os.environ.get("SERVER_SOFTWARE", "").startswith("voila")
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def once(f):
|
|
335
|
+
called = False
|
|
336
|
+
return_value = None
|
|
337
|
+
|
|
338
|
+
@functools.wraps(f)
|
|
339
|
+
def wrapper():
|
|
340
|
+
nonlocal called
|
|
341
|
+
nonlocal return_value
|
|
342
|
+
if called:
|
|
343
|
+
return return_value
|
|
344
|
+
called = True
|
|
345
|
+
return_value = f()
|
|
346
|
+
return return_value
|
|
347
|
+
|
|
348
|
+
return wrapper
|
|
@@ -5,6 +5,8 @@ from .breadcrumbs import BreadCrumbs
|
|
|
5
5
|
|
|
6
6
|
@solara.component
|
|
7
7
|
def Gallery(route_external=None):
|
|
8
|
+
from ..pages.documentation.examples import pycafe_projects
|
|
9
|
+
|
|
8
10
|
if route_external is not None:
|
|
9
11
|
route_current = route_external
|
|
10
12
|
else:
|
|
@@ -49,6 +51,8 @@ def Gallery(route_external=None):
|
|
|
49
51
|
image_url = "https://dxhl76zpt6fap.cloudfront.net/public/api/" + child.path + ".gif"
|
|
50
52
|
elif child.path in ["card", "dataframe", "pivot_table", "slider"]:
|
|
51
53
|
image_url = "https://dxhl76zpt6fap.cloudfront.net/public/api/" + child.path + ".png"
|
|
54
|
+
elif child.path in pycafe_projects:
|
|
55
|
+
image_url = f"https://py.cafe/preview/solara/{child.path}"
|
|
52
56
|
else:
|
|
53
57
|
image_url = "https://dxhl76zpt6fap.cloudfront.net/public/logo.svg"
|
|
54
58
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import Dict, List, Union
|
|
1
|
+
from typing import Callable, Dict, List, Union, cast
|
|
2
2
|
|
|
3
3
|
import yaml
|
|
4
4
|
import markdown
|
|
@@ -12,6 +12,7 @@ from solara.components.markdown import formatter, _no_deep_copy_emojione
|
|
|
12
12
|
# We want to separate metadata from the markdown files before rendering them, which solara.Markdown doesn't support
|
|
13
13
|
@solara.component
|
|
14
14
|
def MarkdownWithMetadata(content: str, unsafe_solara_execute=True):
|
|
15
|
+
cleanups = solara.use_ref(cast(List[Callable[[], None]], []))
|
|
15
16
|
if "---" in content:
|
|
16
17
|
pre_content, raw_metadata, post_content = content.split("---")
|
|
17
18
|
metadata: Dict[str, Union[str, List[str]]] = yaml.safe_load(raw_metadata)
|
|
@@ -56,13 +57,17 @@ def MarkdownWithMetadata(content: str, unsafe_solara_execute=True):
|
|
|
56
57
|
"name": "solara",
|
|
57
58
|
"class": "",
|
|
58
59
|
"validator": mkdocs_pycafe.validator,
|
|
59
|
-
"format": mkdocs_pycafe.formatter(
|
|
60
|
+
"format": mkdocs_pycafe.formatter(
|
|
61
|
+
type="solara", next_formatter=formatter(unsafe_solara_execute, cleanups.current), inside_last_div=False
|
|
62
|
+
),
|
|
60
63
|
},
|
|
61
64
|
{
|
|
62
65
|
"name": "python",
|
|
63
66
|
"class": "highlight",
|
|
64
67
|
"validator": mkdocs_pycafe.validator,
|
|
65
|
-
"format": mkdocs_pycafe.formatter(
|
|
68
|
+
"format": mkdocs_pycafe.formatter(
|
|
69
|
+
type="solara", next_formatter=formatter(unsafe_solara_execute, cleanups.current), inside_last_div=False
|
|
70
|
+
),
|
|
66
71
|
},
|
|
67
72
|
],
|
|
68
73
|
},
|
|
@@ -71,6 +76,15 @@ def MarkdownWithMetadata(content: str, unsafe_solara_execute=True):
|
|
|
71
76
|
|
|
72
77
|
md_parser = solara.use_memo(make_markdown_object, dependencies=[unsafe_solara_execute])
|
|
73
78
|
|
|
79
|
+
def cleanup_wrapper():
|
|
80
|
+
def cleanup():
|
|
81
|
+
for cleanup in cleanups.current:
|
|
82
|
+
cleanup()
|
|
83
|
+
|
|
84
|
+
return cleanup
|
|
85
|
+
|
|
86
|
+
solara.use_effect(cleanup_wrapper, [])
|
|
87
|
+
|
|
74
88
|
with solara.v.Html(
|
|
75
89
|
tag="div",
|
|
76
90
|
style_="display: flex; flex-direction: row; justify-content: center; gap: 15px; max-width: 90%; margin: 0 auto;",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Solara Changelog
|
|
2
2
|
|
|
3
|
-
## Version 1.
|
|
3
|
+
## Version 1.42.0
|
|
4
4
|
* Feature: Mutation detection is now available under the `SOLARA_STORAGE_MUTATION_DETECTION` environmental variable. [#595](https://github.com/widgetti/solara/pull/595).
|
|
5
5
|
* Feature: Autofocusing text inputs is now supported. [#788](https://github.com/widgetti/solara/pull/788).
|
|
6
6
|
* Feature: Custom colours are now supported for the Solara loading spinner. [#858](https://github.com/widgetti/solara/pull/858)
|
|
@@ -9,6 +9,14 @@
|
|
|
9
9
|
* Bug Fix: Solara apps running in qt mode (`--qt`) should now always work correctly. [#856](https://github.com/widgetti/solara/pull/856).
|
|
10
10
|
* Bug Fix: Hot reloading of files outside working directory would crash app. [069a205](https://github.com/widgetti/solara/commit/069a205c88a8cbcb0b0ca23f4d56889c8ad6134a) and [#869](https://github.com/widgetti/solara/pull/869).
|
|
11
11
|
|
|
12
|
+
## Version 1.41.0
|
|
13
|
+
* Feature: Support automatic resizing of Altair (Vega-Lite) figures. [#833](https://github.com/widgetti/solara/pull/833).
|
|
14
|
+
* Feature (Experimental): Support running Solara applications as standalone QT apps. [#835](https://github.com/widgetti/solara/pull/835).
|
|
15
|
+
* Feature: Add option to hide "This website runs on Solara"-banner. [#836](https://github.com/widgetti/solara/pull/836).
|
|
16
|
+
* Feature: Support navigating to hashes. [#814](https://github.com/widgetti/solara/pull/814).
|
|
17
|
+
* Bug Fix: Chunks and assets in nbextensions would fail to load. [9efe26c](https://github.com/widgetti/solara/commit/9efe26cbe00210163a6e8ef251ebfe50ca87fce2).
|
|
18
|
+
* Bug Fix: Vue widget decorator now always uses absolute paths. [#826](https://github.com/widgetti/solara/pull/826).
|
|
19
|
+
|
|
12
20
|
## Version 1.40.0
|
|
13
21
|
* Feature: In Jupyter Notebook and Lab, Solara (server) now renders the [ipypopout](https://github.com/widgetti/ipypopout) window instead of Voila [#805](render ipypopout content in jupyter notebook and lab)
|
|
14
22
|
* Feature: Support styling input field of [ChatInput component](https://solara.dev/documentation/components/lab/chat). [#800](https://github.com/widgetti/solara/pull/800).
|
|
@@ -51,6 +51,7 @@ browser pages. This can be used to store state that outlives a page refresh.
|
|
|
51
51
|
We recommend storing the state in an external database, especially in the case of multiple workers/nodes. If you want to store state associated to a session in-memory, make sure to set up sticky sessions.
|
|
52
52
|
|
|
53
53
|
|
|
54
|
+
The `solara-session-id` cookie is accessible in the browser using JavaScript. If you deem this a security risk, you can disable the cookie by setting the `SOLARA_SESSION_HTTP_ONLY` environment variable to `True`.
|
|
54
55
|
|
|
55
56
|
|
|
56
57
|
## Readiness check
|
|
@@ -78,7 +79,16 @@ $ curl http://localhost:8765/resourcez\?verbose
|
|
|
78
79
|
|
|
79
80
|
The JSON format may be subject to change.
|
|
80
81
|
|
|
82
|
+
## Ignoring notebook extensions
|
|
81
83
|
|
|
84
|
+
Not all (classic) jupyter notebook extensions are compatible with Solara, and there is not way to distinguish between notebook extensions that are needed for widgets and those that are not.
|
|
85
|
+
To ignore notebook extensions, you can set the `SOLARA_SERVER_IGNORE_NBEXTENSIONS` environment variable. This is a comma separated list of notebook extensions to ignore. For example, to ignore the `dash/main` and `foo/bar` extensions, you can run:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
$ SOLARA_SERVER_IGNORE_NBEXTENSIONS="dash/main,foo/bar" solara run nogit/sol.py -a
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Note that these error are not fatal, and the Solara app will still run.
|
|
82
92
|
|
|
83
93
|
## Production mode
|
|
84
94
|
|
|
@@ -14,18 +14,16 @@ routes = [
|
|
|
14
14
|
@solara.component
|
|
15
15
|
def Page():
|
|
16
16
|
route_current, routes = solara.use_route()
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
solara.Info(f"Go to solara.Route(path={route.path!r})")
|
|
28
|
-
return main
|
|
17
|
+
|
|
18
|
+
solara.Markdown("*Click on one of the links below to change the route and see the url in your browser change, and match the route.*")
|
|
19
|
+
with solara.VBox():
|
|
20
|
+
for route in routes:
|
|
21
|
+
with solara.Link(route):
|
|
22
|
+
current = route_current is route
|
|
23
|
+
if current:
|
|
24
|
+
solara.Success(f"You are at solara.Route(path={route.path!r})")
|
|
25
|
+
else:
|
|
26
|
+
solara.Info(f"Go to solara.Route(path={route.path!r})")
|
|
29
27
|
|
|
30
28
|
|
|
31
29
|
__doc__ += apidoc(solara.Route, full=True) # type: ignore
|
|
@@ -23,43 +23,39 @@ def Fruit():
|
|
|
23
23
|
solara.Button("Choose a fruit, I recomment banana")
|
|
24
24
|
return main
|
|
25
25
|
|
|
26
|
-
with solara.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
solara.Button(route_fruit.path)
|
|
26
|
+
with solara.Row():
|
|
27
|
+
for route_fruit in routes[1:]:
|
|
28
|
+
with solara.Link(solara.resolve_path(route_fruit)):
|
|
29
|
+
solara.Button(route_fruit.path)
|
|
31
30
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
return main
|
|
31
|
+
with solara.Link("/documentation/api/routing/use_route/fruit/nofruit", nofollow=True):
|
|
32
|
+
solara.Button("Wrong fruit")
|
|
33
|
+
with solara.Link("/documentation/api/routing/use_route/not-routed", nofollow=True):
|
|
34
|
+
solara.Button("Wrong url")
|
|
35
|
+
solara.Success(f"You chose {route.path}")
|
|
38
36
|
|
|
39
37
|
|
|
40
38
|
@solara.component
|
|
41
39
|
def Page():
|
|
42
40
|
# this gets the top level routes, '/' and 'fruit'
|
|
43
41
|
route_current, routes_all = solara.use_route()
|
|
44
|
-
with solara.
|
|
45
|
-
with solara.
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
solara.Error(f"Unknown route: {route_current.path}")
|
|
62
|
-
return main
|
|
42
|
+
with solara.Card("Navigation using buttons"):
|
|
43
|
+
with solara.Row():
|
|
44
|
+
for route in routes_all:
|
|
45
|
+
with solara.Link(route):
|
|
46
|
+
solara.Button(route.path, color="red" if route_current == route else None)
|
|
47
|
+
with solara.Card("Content decided by route:"):
|
|
48
|
+
if route_current is None:
|
|
49
|
+
solara.Error("Page does not exist")
|
|
50
|
+
with solara.Link("fruit/kiwi"):
|
|
51
|
+
solara.Button("Go to fruit/kiwi")
|
|
52
|
+
elif route_current.path == "/":
|
|
53
|
+
with solara.Link("fruit/banana"):
|
|
54
|
+
solara.Button("Go to fruit/banana")
|
|
55
|
+
elif route_current.path == "fruit":
|
|
56
|
+
Fruit()
|
|
57
|
+
else:
|
|
58
|
+
solara.Error(f"Unknown route: {route_current.path}")
|
|
63
59
|
|
|
64
60
|
|
|
65
61
|
routes = [
|
|
@@ -14,14 +14,12 @@ routes = [
|
|
|
14
14
|
@solara.component
|
|
15
15
|
def Page():
|
|
16
16
|
route_current, routes = solara.use_route()
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
solara.Button(f"Go to {route.path}", color="red" if current else None)
|
|
24
|
-
return main
|
|
17
|
+
solara.Info("Note the address bar in the browser. It should change to the path of the link.")
|
|
18
|
+
with solara.Row():
|
|
19
|
+
for route in routes:
|
|
20
|
+
with solara.Link(route):
|
|
21
|
+
current = route_current is route
|
|
22
|
+
solara.Button(f"Go to {route.path}", color="red" if current else None)
|
|
25
23
|
|
|
26
24
|
|
|
27
25
|
__doc__ += apidoc(solara.Link.f) # type: ignore
|
|
@@ -6,15 +6,12 @@ from solara.website.utils import apidoc
|
|
|
6
6
|
|
|
7
7
|
@solara.component
|
|
8
8
|
def Page():
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
)
|
|
16
|
-
|
|
17
|
-
return main
|
|
9
|
+
solara.Info("Nothing to see here, only in this page's source code, or by looking at the google search results for this page.")
|
|
10
|
+
with solara.Head():
|
|
11
|
+
solara.Meta(
|
|
12
|
+
name="description",
|
|
13
|
+
content="The Meta component can be used to set the description of a page. This is useful for SEO, or crawlers that index your page.",
|
|
14
|
+
)
|
|
18
15
|
|
|
19
16
|
|
|
20
17
|
__doc__ += apidoc(solara.Meta.f) # type: ignore
|
|
@@ -26,20 +26,18 @@ def Page():
|
|
|
26
26
|
}
|
|
27
27
|
"""
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
f"""
|
|
29
|
+
solara.Checkbox(label="Use CSS", value=insert_css, on_value=set_insert_css)
|
|
30
|
+
solara.Markdown(
|
|
31
|
+
f"""
|
|
33
32
|
## CSS Example that styles the button below
|
|
34
33
|
```css
|
|
35
34
|
{css}
|
|
36
35
|
```
|
|
37
36
|
"""
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
return main
|
|
37
|
+
)
|
|
38
|
+
if insert_css:
|
|
39
|
+
solara.Style(css)
|
|
40
|
+
solara.Button(label="Advanced users might want to style this", icon_name="mdi-thumb-up", classes=["mybutton"])
|
|
43
41
|
|
|
44
42
|
|
|
45
43
|
__doc__ += apidoc(solara.Style.f) # type: ignore
|
|
@@ -13,20 +13,18 @@ def Page():
|
|
|
13
13
|
path, set_path = solara.use_state(cast(Optional[Path], None))
|
|
14
14
|
directory, set_directory = solara.use_state(Path("~").expanduser())
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
solara.Info(f"You opened file: {file}")
|
|
29
|
-
return main
|
|
16
|
+
can_select = solara.ui_checkbox("Enable select")
|
|
17
|
+
|
|
18
|
+
def reset_path():
|
|
19
|
+
set_path(None)
|
|
20
|
+
set_file(None)
|
|
21
|
+
|
|
22
|
+
# reset path and file when can_select changes
|
|
23
|
+
solara.use_memo(reset_path, [can_select])
|
|
24
|
+
solara.FileBrowser(directory, on_directory_change=set_directory, on_path_select=set_path, on_file_open=set_file, can_select=can_select)
|
|
25
|
+
solara.Info(f"You are in directory: {directory}")
|
|
26
|
+
solara.Info(f"You selected path: {path}")
|
|
27
|
+
solara.Info(f"You opened file: {file}")
|
|
30
28
|
|
|
31
29
|
|
|
32
30
|
__doc__ += apidoc(solara.FileBrowser.f) # type: ignore
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""
|
|
2
|
+
# InputTime
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import solara
|
|
6
|
+
from solara.website.components import NoPage
|
|
7
|
+
from solara.website.utils import apidoc
|
|
8
|
+
|
|
9
|
+
title = "InputTime"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
__doc__ += apidoc(solara.lab.components.input_time.InputTime.f) # type: ignore
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
Page = NoPage
|