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/__init__.py
CHANGED
solara/__main__.py
CHANGED
|
@@ -21,6 +21,8 @@ import solara.server.threaded
|
|
|
21
21
|
|
|
22
22
|
from .server import telemetry
|
|
23
23
|
|
|
24
|
+
print_mutex = threading.Lock()
|
|
25
|
+
|
|
24
26
|
try:
|
|
25
27
|
from solara_enterprise.ssg import ssg_crawl
|
|
26
28
|
except ImportError:
|
|
@@ -75,13 +77,15 @@ def _check_version():
|
|
|
75
77
|
import requests
|
|
76
78
|
|
|
77
79
|
try:
|
|
78
|
-
response = requests.get("https://pypi.org/pypi/solara/json")
|
|
80
|
+
response = requests.get("https://pypi.org/pypi/solara/json", timeout=0.5)
|
|
79
81
|
latest_version = response.json()["info"]["version"]
|
|
80
82
|
except: # noqa: E722
|
|
83
|
+
# in case of a firewall, or timeout, we just abort
|
|
81
84
|
return
|
|
82
85
|
if latest_version != solara.__version__:
|
|
83
|
-
|
|
84
|
-
|
|
86
|
+
with print_mutex:
|
|
87
|
+
print(f"New version of Solara available: {latest_version}. You have {solara.__version__}. Please upgrade using:") # noqa: T201
|
|
88
|
+
print(f'\t$ pip install "solara=={latest_version}"') # noqa: T201
|
|
85
89
|
|
|
86
90
|
|
|
87
91
|
def find_all_packages_paths():
|
|
@@ -306,7 +310,7 @@ def run(
|
|
|
306
310
|
print("solara: --reload is deprecated, use --auto-restart/-a instead", file=sys.stderr) # noqa: T201
|
|
307
311
|
auto_restart = reload
|
|
308
312
|
if check_version:
|
|
309
|
-
_check_version()
|
|
313
|
+
threading.Thread(target=_check_version, daemon=True).run()
|
|
310
314
|
|
|
311
315
|
# uvicorn calls it reload, we call it auto restart
|
|
312
316
|
reload = auto_restart
|
|
@@ -390,7 +394,8 @@ def run(
|
|
|
390
394
|
if open and not qt:
|
|
391
395
|
threading.Thread(target=open_browser, daemon=True).start()
|
|
392
396
|
|
|
393
|
-
|
|
397
|
+
with print_mutex:
|
|
398
|
+
rich.print(f"Solara server is starting at {url}")
|
|
394
399
|
|
|
395
400
|
if log_level is not None:
|
|
396
401
|
LOGGING_CONFIG["loggers"]["solara"]["level"] = log_level.upper()
|
solara/_stores.py
CHANGED
|
@@ -11,12 +11,16 @@ class _PublicValueNotSet:
|
|
|
11
11
|
pass
|
|
12
12
|
|
|
13
13
|
|
|
14
|
+
class _SetValueNotSet:
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
14
18
|
@dataclasses.dataclass
|
|
15
19
|
class StoreValue(Generic[S]):
|
|
16
20
|
private: S # the internal private value, should never be mutated
|
|
17
21
|
public: Union[S, _PublicValueNotSet] # this is the value that is exposed in .get(), it is a deep copy of private
|
|
18
22
|
get_traceback: Optional[inspect.Traceback]
|
|
19
|
-
set_value:
|
|
23
|
+
set_value: Union[S, _SetValueNotSet] # the value that was set using .set(..), we deepcopy this to set private
|
|
20
24
|
set_traceback: Optional[inspect.Traceback]
|
|
21
25
|
|
|
22
26
|
|
|
@@ -79,7 +83,7 @@ class MutateDetectorStore(ValueBase[S]):
|
|
|
79
83
|
code = "<No code context available>"
|
|
80
84
|
msg += f"The last value was read in the following code:\n" f"{tb.filename}:{tb.lineno}\n" f"{code}"
|
|
81
85
|
raise ValueError(msg)
|
|
82
|
-
elif store_value.set_value
|
|
86
|
+
elif not isinstance(store_value.set_value, _SetValueNotSet) and not self.equals(store_value.set_value, store_value.private):
|
|
83
87
|
tb = store_value.set_traceback
|
|
84
88
|
msg = f"""Reactive variable was set with a value of {store_value.private!r}, but was later mutated mutated to {store_value.set_value!r}.
|
|
85
89
|
|
|
@@ -161,11 +165,11 @@ reactive_df = solara.reactive(df, equals=solara.util.equals_pickle)
|
|
|
161
165
|
def subscribe(self, listener: Callable[[S], None], scope: Optional[ContextManager] = None):
|
|
162
166
|
def listener_wrapper(new: StoreValue[S], previous: StoreValue[S]):
|
|
163
167
|
self._ensure_public_exists()
|
|
164
|
-
assert new.public
|
|
165
|
-
assert previous.public
|
|
166
|
-
previous_value = previous.set_value if previous.set_value
|
|
168
|
+
assert not isinstance(new.public, _PublicValueNotSet)
|
|
169
|
+
assert not isinstance(previous.public, _PublicValueNotSet)
|
|
170
|
+
previous_value = previous.set_value if not isinstance(previous.set_value, _SetValueNotSet) else previous.private
|
|
167
171
|
new_value = new.set_value
|
|
168
|
-
assert new_value
|
|
172
|
+
assert not isinstance(new_value, _SetValueNotSet)
|
|
169
173
|
if not self.equals(new_value, previous_value):
|
|
170
174
|
listener(new_value)
|
|
171
175
|
|
|
@@ -174,11 +178,11 @@ reactive_df = solara.reactive(df, equals=solara.util.equals_pickle)
|
|
|
174
178
|
def subscribe_change(self, listener: Callable[[S, S], None], scope: Optional[ContextManager] = None):
|
|
175
179
|
def listener_wrapper(new: StoreValue[S], previous: StoreValue[S]):
|
|
176
180
|
self._ensure_public_exists()
|
|
177
|
-
assert new.public
|
|
178
|
-
assert previous.public
|
|
179
|
-
previous_value = previous.set_value if previous.set_value
|
|
181
|
+
assert not isinstance(new.public, _PublicValueNotSet)
|
|
182
|
+
assert not isinstance(previous.public, _PublicValueNotSet)
|
|
183
|
+
previous_value = previous.set_value if not isinstance(previous.set_value, _SetValueNotSet) else previous.private
|
|
180
184
|
new_value = new.set_value
|
|
181
|
-
assert new_value
|
|
185
|
+
assert not isinstance(new_value, _SetValueNotSet)
|
|
182
186
|
if not self.equals(new_value, previous_value):
|
|
183
187
|
listener(new_value, previous_value)
|
|
184
188
|
|
solara/components/__init__.py
CHANGED
|
@@ -57,4 +57,21 @@ from .progress import ProgressLinear # noqa: F401 F403
|
|
|
57
57
|
from .component_vue import _component_vue, component_vue # noqa: F401 F403
|
|
58
58
|
import reacton.core
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
try:
|
|
61
|
+
from reacton import Fragment as Fragment # type: ignore
|
|
62
|
+
except ImportError:
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
import logging
|
|
66
|
+
from ..settings import main
|
|
67
|
+
|
|
68
|
+
_container = None
|
|
69
|
+
|
|
70
|
+
if main.default_container in globals():
|
|
71
|
+
_container = globals()[main.default_container]
|
|
72
|
+
else:
|
|
73
|
+
logger = logging.getLogger("solara.components")
|
|
74
|
+
logger.warning(f"Default container {main.default_container} not found in solara.components. Defaulting to Column.")
|
|
75
|
+
|
|
76
|
+
# TODO: When Solara 2.0 releases Column should be replaced with Fragment
|
|
77
|
+
reacton.core._default_container = _container or Column # noqa: F405
|
solara/components/datatable.py
CHANGED
|
@@ -10,14 +10,14 @@ import solara
|
|
|
10
10
|
import solara.hooks.dataframe
|
|
11
11
|
import solara.lab
|
|
12
12
|
import traitlets
|
|
13
|
-
from solara.lab.hooks.dataframe import use_df_column_names
|
|
13
|
+
from solara.lab.hooks.dataframe import use_df_column_names, df_row_names
|
|
14
14
|
from solara.lab.utils.dataframe import df_len, df_records, df_slice
|
|
15
15
|
|
|
16
16
|
from .. import CellAction, ColumnAction
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
def _ensure_dict(d):
|
|
20
|
-
if dataclasses.is_dataclass(d):
|
|
20
|
+
if dataclasses.is_dataclass(d) and not isinstance(d, type): # is_dataclass also returns True for dataclass type, rather than instance
|
|
21
21
|
return dataclasses.asdict(d)
|
|
22
22
|
return d
|
|
23
23
|
|
|
@@ -100,12 +100,12 @@ def DataTable(
|
|
|
100
100
|
i2 = min(total_length, (page + 1) * items_per_page)
|
|
101
101
|
|
|
102
102
|
columns = use_df_column_names(df)
|
|
103
|
-
|
|
103
|
+
rows = df_row_names(df)
|
|
104
104
|
items = []
|
|
105
105
|
dfs = df_slice(df, i1, i2)
|
|
106
106
|
records = df_records(dfs)
|
|
107
107
|
for i in range(i2 - i1):
|
|
108
|
-
item = {"__row__": i + i1} # special key for the row number
|
|
108
|
+
item = {"__row__": format(dfs, columns, i + 1, rows[i + i1])} # special key for the row number
|
|
109
109
|
for column in columns:
|
|
110
110
|
item[column] = format(dfs, column, i + i1, records[i][column])
|
|
111
111
|
items.append(item)
|
solara/components/input.py
CHANGED
|
@@ -373,6 +373,10 @@ def _use_input_type(
|
|
|
373
373
|
error_message = str(e.args[0])
|
|
374
374
|
|
|
375
375
|
def sync_back_input_value():
|
|
376
|
+
# Make sure we update string_value when the effect is rerun,
|
|
377
|
+
# Since the parsing & stringigying functions might have changed
|
|
378
|
+
set_string_value(stringify(reactive_value.value) if reactive_value.value is not None else None)
|
|
379
|
+
|
|
376
380
|
def on_external_value_change(new_value: Optional[T]):
|
|
377
381
|
new_string_value = stringify(new_value)
|
|
378
382
|
try:
|
|
@@ -386,7 +390,7 @@ def _use_input_type(
|
|
|
386
390
|
|
|
387
391
|
return reactive_value.subscribe(on_external_value_change)
|
|
388
392
|
|
|
389
|
-
solara.use_effect(sync_back_input_value, [reactive_value])
|
|
393
|
+
solara.use_effect(sync_back_input_value, [reactive_value, parse, stringify])
|
|
390
394
|
|
|
391
395
|
return string_value, error_message, set_string_value
|
|
392
396
|
|
solara/components/markdown.py
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import functools
|
|
2
1
|
import hashlib
|
|
3
2
|
import html
|
|
4
3
|
import logging
|
|
5
4
|
import textwrap
|
|
6
5
|
import traceback
|
|
7
6
|
import warnings
|
|
8
|
-
from typing import Any, Dict, List, Optional, Union, cast
|
|
7
|
+
from typing import Any, Callable, Dict, List, Optional, Union, cast
|
|
9
8
|
import typing
|
|
10
9
|
|
|
11
10
|
import ipyvuetify as v
|
|
@@ -18,6 +17,7 @@ try:
|
|
|
18
17
|
has_pymdownx = True
|
|
19
18
|
except ModuleNotFoundError:
|
|
20
19
|
has_pymdownx = False
|
|
20
|
+
import reacton.core
|
|
21
21
|
|
|
22
22
|
import solara
|
|
23
23
|
import solara.components.applayout
|
|
@@ -55,7 +55,7 @@ def ExceptionGuard(children=[]):
|
|
|
55
55
|
solara.Column(children=children)
|
|
56
56
|
|
|
57
57
|
|
|
58
|
-
def _run_solara(code):
|
|
58
|
+
def _run_solara(code, cleanups):
|
|
59
59
|
ast = compile(code, "markdown", "exec")
|
|
60
60
|
local_scope: Dict[Any, Any] = {}
|
|
61
61
|
exec(ast, local_scope)
|
|
@@ -68,6 +68,13 @@ def _run_solara(code):
|
|
|
68
68
|
else:
|
|
69
69
|
raise NameError("No Page or app defined")
|
|
70
70
|
box = v.Html(tag="div")
|
|
71
|
+
|
|
72
|
+
rc: reacton.core.RenderContext
|
|
73
|
+
|
|
74
|
+
def cleanup():
|
|
75
|
+
rc.close()
|
|
76
|
+
|
|
77
|
+
cleanups.append(cleanup)
|
|
71
78
|
box, rc = solara.render(cast(solara.Element, app), container=box) # type: ignore
|
|
72
79
|
widget_id = box._model_id
|
|
73
80
|
return (
|
|
@@ -236,9 +243,8 @@ module.exports = {
|
|
|
236
243
|
return template
|
|
237
244
|
|
|
238
245
|
|
|
239
|
-
def _highlight(src, language, unsafe_solara_execute,
|
|
246
|
+
def _highlight(src, language, class_name=None, options=None, md=None, unsafe_solara_execute=False, cleanups=None, **kwargs):
|
|
240
247
|
"""Highlight a block of code"""
|
|
241
|
-
|
|
242
248
|
if not has_pygments:
|
|
243
249
|
warnings.warn("Pygments is not installed, code highlighting will not work, use pip install pygments to install it.")
|
|
244
250
|
src_safe = html.escape(src)
|
|
@@ -255,7 +261,7 @@ def _highlight(src, language, unsafe_solara_execute, *args, **kwargs):
|
|
|
255
261
|
|
|
256
262
|
if run_src_with_solara:
|
|
257
263
|
if unsafe_solara_execute:
|
|
258
|
-
html_widget = _run_solara(src)
|
|
264
|
+
html_widget = _run_solara(src, cleanups)
|
|
259
265
|
return src_html + html_widget
|
|
260
266
|
else:
|
|
261
267
|
return src_html + html_no_execute_enabled
|
|
@@ -263,8 +269,17 @@ def _highlight(src, language, unsafe_solara_execute, *args, **kwargs):
|
|
|
263
269
|
return src_html
|
|
264
270
|
|
|
265
271
|
|
|
266
|
-
def formatter(unsafe_solara_execute: bool):
|
|
267
|
-
|
|
272
|
+
def formatter(unsafe_solara_execute: bool, cleanups: List[Callable[[], None]]):
|
|
273
|
+
def wrapper(*args, **kwargs):
|
|
274
|
+
try:
|
|
275
|
+
kwargs["unsafe_solara_execute"] = unsafe_solara_execute
|
|
276
|
+
kwargs["cleanups"] = cleanups
|
|
277
|
+
return _highlight(*args, **kwargs)
|
|
278
|
+
except Exception as e:
|
|
279
|
+
logger.exception("Error while highlighting code")
|
|
280
|
+
raise e
|
|
281
|
+
|
|
282
|
+
return wrapper
|
|
268
283
|
|
|
269
284
|
|
|
270
285
|
@solara.component
|
|
@@ -276,8 +291,10 @@ def MarkdownIt(md_text: str, highlight: List[int] = [], unsafe_solara_execute: b
|
|
|
276
291
|
from mdit_py_plugins.footnote import footnote_plugin # noqa: F401
|
|
277
292
|
from mdit_py_plugins.front_matter import front_matter_plugin # noqa: F401
|
|
278
293
|
|
|
294
|
+
cleanups = solara.use_ref(cast(List[Callable[[], None]], []))
|
|
295
|
+
|
|
279
296
|
def highlight_code(code, name, attrs):
|
|
280
|
-
return _highlight(code, name, unsafe_solara_execute, attrs)
|
|
297
|
+
return _highlight(cleanups.current, code, name, unsafe_solara_execute, attrs)
|
|
281
298
|
|
|
282
299
|
md = MarkdownItMod(
|
|
283
300
|
"js-default",
|
|
@@ -290,6 +307,15 @@ def MarkdownIt(md_text: str, highlight: List[int] = [], unsafe_solara_execute: b
|
|
|
290
307
|
md = md.use(container.container_plugin, name="note")
|
|
291
308
|
html = md.render(md_text)
|
|
292
309
|
hash = hashlib.sha256((html + str(unsafe_solara_execute) + repr(highlight)).encode("utf-8")).hexdigest()
|
|
310
|
+
|
|
311
|
+
def cleanup_wrapper():
|
|
312
|
+
def cleanup():
|
|
313
|
+
for cleanup in cleanups.current:
|
|
314
|
+
cleanup()
|
|
315
|
+
|
|
316
|
+
return cleanup
|
|
317
|
+
|
|
318
|
+
solara.use_effect(cleanup_wrapper)
|
|
293
319
|
return v.VuetifyTemplate.element(template=_markdown_template(html)).key(hash)
|
|
294
320
|
|
|
295
321
|
|
|
@@ -349,6 +375,7 @@ def Markdown(md_text: str, unsafe_solara_execute=False, style: Union[str, Dict,
|
|
|
349
375
|
|
|
350
376
|
md_text = textwrap.dedent(md_text)
|
|
351
377
|
style = solara.util._flatten_style(style)
|
|
378
|
+
cleanups = solara.use_ref(cast(List[Callable[[], None]], []))
|
|
352
379
|
|
|
353
380
|
def make_markdown_object():
|
|
354
381
|
if md_parser is not None:
|
|
@@ -377,7 +404,7 @@ def Markdown(md_text: str, unsafe_solara_execute=False, style: Union[str, Dict,
|
|
|
377
404
|
{
|
|
378
405
|
"name": "solara",
|
|
379
406
|
"class": "",
|
|
380
|
-
"format": formatter(unsafe_solara_execute),
|
|
407
|
+
"format": formatter(unsafe_solara_execute, cleanups=cleanups.current),
|
|
381
408
|
},
|
|
382
409
|
],
|
|
383
410
|
},
|
|
@@ -399,6 +426,15 @@ def Markdown(md_text: str, unsafe_solara_execute=False, style: Union[str, Dict,
|
|
|
399
426
|
assert md_self is not None
|
|
400
427
|
md_parser = md_self
|
|
401
428
|
html = md_parser.convert(md_text)
|
|
429
|
+
|
|
430
|
+
def cleanup_wrapper():
|
|
431
|
+
def cleanup():
|
|
432
|
+
for cleanup in cleanups.current:
|
|
433
|
+
cleanup()
|
|
434
|
+
|
|
435
|
+
return cleanup
|
|
436
|
+
|
|
437
|
+
solara.use_effect(cleanup_wrapper, [])
|
|
402
438
|
# if we update the template value, the whole vue tree will rerender (ipvue/ipyvuetify issue)
|
|
403
439
|
# however, using the hash we simply generate a new widget each time
|
|
404
440
|
hash = hashlib.sha256((html + str(unsafe_solara_execute)).encode("utf-8")).hexdigest()
|
solara/components/misc.py
CHANGED
|
@@ -136,7 +136,7 @@ def HTML(tag="div", unsafe_innerHTML=None, style: str = None, classes: List[str]
|
|
|
136
136
|
|
|
137
137
|
@solara.component
|
|
138
138
|
def VBox(children=[], grow=True, align_items="stretch", classes: List[str] = []):
|
|
139
|
-
"""Deprecated. Use `
|
|
139
|
+
"""Deprecated. Use `Column` instead."""
|
|
140
140
|
style = f"flex-direction: column; align-items: {align_items};"
|
|
141
141
|
if grow:
|
|
142
142
|
style += "flex-grow: 1;"
|
|
@@ -146,7 +146,7 @@ def VBox(children=[], grow=True, align_items="stretch", classes: List[str] = [])
|
|
|
146
146
|
|
|
147
147
|
@solara.component
|
|
148
148
|
def HBox(children=[], grow=True, align_items="stretch", classes: List[str] = []):
|
|
149
|
-
"""Deprecated. Use `
|
|
149
|
+
"""Deprecated. Use `Row` instead."""
|
|
150
150
|
style = f"flex-direction: row; align-items: {align_items}; "
|
|
151
151
|
if grow:
|
|
152
152
|
style += "flex-grow: 1;"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from .chat import ChatBox, ChatInput, ChatMessage # noqa: F401
|
|
2
2
|
from .confirmation_dialog import ConfirmationDialog # noqa: F401
|
|
3
3
|
from .input_date import InputDate, InputDateRange # noqa: F401
|
|
4
|
+
from .input_time import InputTime as InputTime
|
|
4
5
|
from .menu import ClickMenu, ContextMenu, Menu # noqa: F401 F403
|
|
5
6
|
from .tabs import Tab, Tabs # noqa: F401
|
|
6
7
|
from .theming import ThemeToggle, theme, use_dark_effective # noqa: F401
|
solara/lab/components/chat.py
CHANGED
|
@@ -43,7 +43,7 @@ def ChatBox(
|
|
|
43
43
|
|
|
44
44
|
@solara.component
|
|
45
45
|
def ChatInput(
|
|
46
|
-
send_callback: Optional[Callable] = None,
|
|
46
|
+
send_callback: Optional[Callable[[str], None]] = None,
|
|
47
47
|
disabled: bool = False,
|
|
48
48
|
style: Optional[Union[str, Dict[str, str]]] = None,
|
|
49
49
|
input_text_style: Optional[Union[str, Dict[str, str]]] = None,
|
|
@@ -55,7 +55,7 @@ def ChatInput(
|
|
|
55
55
|
|
|
56
56
|
# Arguments
|
|
57
57
|
|
|
58
|
-
* `send_callback`: A callback function for when the user presses enter or clicks the send button.
|
|
58
|
+
* `send_callback`: A callback function for when the user presses enter or clicks the send button taking the message as an argument.
|
|
59
59
|
* `disabled`: Whether the input should be disabled. Useful for disabling sending further messages while a chatbot is replying,
|
|
60
60
|
among other things.
|
|
61
61
|
* `style`: CSS styles to apply to the `solara.Row` containing the input field and submit button. Either a string or a dictionary.
|
|
@@ -98,7 +98,7 @@ def ChatInput(
|
|
|
98
98
|
|
|
99
99
|
@solara.component
|
|
100
100
|
def ChatMessage(
|
|
101
|
-
children: Union[List[solara.Element], str],
|
|
101
|
+
children: Union[List[solara.Element], str] = [],
|
|
102
102
|
user: bool = False,
|
|
103
103
|
avatar: Union[solara.Element, str, Literal[False], None] = None,
|
|
104
104
|
name: Optional[str] = None,
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import datetime as dt
|
|
2
|
+
from typing import Callable, Dict, List, Optional, Union
|
|
3
|
+
|
|
4
|
+
import solara
|
|
5
|
+
import solara.lab
|
|
6
|
+
from solara.lab.components.input_date import use_close_menu
|
|
7
|
+
from solara.components.input import _use_input_type
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@solara.component
|
|
11
|
+
def InputTime(
|
|
12
|
+
value: Union[solara.Reactive[Optional[dt.time]], Optional[dt.time]],
|
|
13
|
+
on_value: Optional[Callable[[Optional[dt.time]], None]] = None,
|
|
14
|
+
label: str = "Pick a time",
|
|
15
|
+
children: List[solara.Element] = [],
|
|
16
|
+
open_value: Union[solara.Reactive[bool], bool] = False,
|
|
17
|
+
on_open_value: Optional[Callable[[bool], None]] = None,
|
|
18
|
+
optional: bool = False,
|
|
19
|
+
twelve_hour_clock: bool = False,
|
|
20
|
+
use_seconds: bool = False,
|
|
21
|
+
allowed_minutes: Optional[List[int]] = None,
|
|
22
|
+
style: Optional[Union[str, Dict[str, str]]] = None,
|
|
23
|
+
classes: Optional[List[str]] = None,
|
|
24
|
+
):
|
|
25
|
+
"""
|
|
26
|
+
Show a textfield, which when clicked, opens a timepicker. The input time should be of type `datetime.time`.
|
|
27
|
+
|
|
28
|
+
## Basic Example
|
|
29
|
+
|
|
30
|
+
```solara {pycafe-link}
|
|
31
|
+
import solara
|
|
32
|
+
import solara.lab
|
|
33
|
+
import datetime as dt
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@solara.component
|
|
37
|
+
def Page():
|
|
38
|
+
time = solara.use_reactive(dt.time(12, 0))
|
|
39
|
+
|
|
40
|
+
solara.lab.InputTime(time, allowed_minutes=[0, 15, 30, 45])
|
|
41
|
+
solara.Text(str(time.value))
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Arguments
|
|
45
|
+
|
|
46
|
+
* value: Reactive variable of type `datetime.time`, or `None`. This time is selected the first time the component is rendered.
|
|
47
|
+
* on_value: a callback function for when value changes. The callback function receives the new value as an argument.
|
|
48
|
+
* label: Text used to label the text field that triggers the timepicker.
|
|
49
|
+
* children: List of Elements to be rendered under the timepicker. If empty, a close button is rendered.
|
|
50
|
+
* open_value: Controls and communicates the state of the timepicker. If True, the timepicker is open. If False, the timepicker is closed.
|
|
51
|
+
Intended to be used in conjunction with a custom set of controls to close the timepicker.
|
|
52
|
+
* on_open_value: a callback function for when open_value changes. Also receives the new value as an argument.
|
|
53
|
+
* optional: Determines whether to show an error when value is `None`. If `True`, no error is shown.
|
|
54
|
+
* twelve_hour_clock: If `True`, the timepicker will display in 12-hour format. If `False`, the timepicker will display in 24-hour format.
|
|
55
|
+
* use_seconds: If `True`, the timepicker will allow input of seconds.
|
|
56
|
+
* allowed_minutes: List of allowed minutes for the timepicker. Restricts the input to specific minute intervals.
|
|
57
|
+
* style: CSS style to apply to the text field. Either a string or a dictionary of CSS properties (i.e. `{"property": "value"}`).
|
|
58
|
+
* classes: List of CSS classes to apply to the text field.
|
|
59
|
+
"""
|
|
60
|
+
time_format_internal = f"%H:%M{':%S' if use_seconds else ''}"
|
|
61
|
+
time_format_display = f"%H:%M{':%S' if use_seconds else ''}"
|
|
62
|
+
if twelve_hour_clock:
|
|
63
|
+
time_format_display = f"%I:%M{':%S' if use_seconds else ''} %p"
|
|
64
|
+
value_reactive = solara.use_reactive(value, on_value) # type: ignore
|
|
65
|
+
del value, on_value
|
|
66
|
+
timepicker_is_open = solara.use_reactive(open_value, on_open_value) # type: ignore
|
|
67
|
+
del open_value, on_open_value
|
|
68
|
+
|
|
69
|
+
def set_time_typed_cast(value: Optional[str]):
|
|
70
|
+
if value:
|
|
71
|
+
try:
|
|
72
|
+
time_value = dt.datetime.strptime(value, time_format_display).time()
|
|
73
|
+
return time_value
|
|
74
|
+
except ValueError:
|
|
75
|
+
raise ValueError(f"Time {value} does not match format {time_format_display.replace('%', '')}")
|
|
76
|
+
elif optional:
|
|
77
|
+
return None
|
|
78
|
+
else:
|
|
79
|
+
raise ValueError("Time cannot be empty")
|
|
80
|
+
|
|
81
|
+
def time_to_str(time: Optional[dt.time]) -> str:
|
|
82
|
+
if time is not None:
|
|
83
|
+
return time.strftime(time_format_display)
|
|
84
|
+
return ""
|
|
85
|
+
|
|
86
|
+
def set_time_cast(new_value: Optional[str]):
|
|
87
|
+
if new_value:
|
|
88
|
+
time_value = dt.datetime.strptime(new_value, time_format_internal).time()
|
|
89
|
+
value_reactive.value = time_value
|
|
90
|
+
|
|
91
|
+
def standard_strfy(time: Optional[dt.time]):
|
|
92
|
+
if time is None:
|
|
93
|
+
return None
|
|
94
|
+
else:
|
|
95
|
+
return time.strftime(time_format_internal)
|
|
96
|
+
|
|
97
|
+
time_standard_str = standard_strfy(value_reactive.value)
|
|
98
|
+
|
|
99
|
+
style_flat = solara.util._flatten_style(style)
|
|
100
|
+
|
|
101
|
+
internal_value, error_message, set_value_cast = _use_input_type(value_reactive, set_time_typed_cast, time_to_str)
|
|
102
|
+
|
|
103
|
+
if error_message:
|
|
104
|
+
label += f" ({error_message})"
|
|
105
|
+
input = solara.v.TextField(
|
|
106
|
+
label=label,
|
|
107
|
+
v_model=internal_value,
|
|
108
|
+
on_v_model=set_value_cast,
|
|
109
|
+
append_icon="mdi-clock",
|
|
110
|
+
error=bool(error_message),
|
|
111
|
+
style_="min-width: 290px;" + style_flat,
|
|
112
|
+
class_=", ".join(classes) if classes else "",
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
use_close_menu(input, timepicker_is_open)
|
|
116
|
+
|
|
117
|
+
with solara.lab.Menu(
|
|
118
|
+
activator=input,
|
|
119
|
+
close_on_content_click=False,
|
|
120
|
+
open_value=timepicker_is_open,
|
|
121
|
+
use_activator_width=False,
|
|
122
|
+
):
|
|
123
|
+
with solara.v.TimePicker(
|
|
124
|
+
ampm_in_title=twelve_hour_clock,
|
|
125
|
+
v_model=time_standard_str,
|
|
126
|
+
on_v_model=set_time_cast,
|
|
127
|
+
format="24hr" if not twelve_hour_clock else "ampm",
|
|
128
|
+
allowed_minutes=allowed_minutes,
|
|
129
|
+
use_seconds=use_seconds,
|
|
130
|
+
style_="width: 100%;",
|
|
131
|
+
):
|
|
132
|
+
if len(children) > 0:
|
|
133
|
+
solara.display(*children)
|
solara/lab/hooks/dataframe.py
CHANGED
solara/lab/utils/dataframe.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import List
|
|
1
|
+
from typing import List, Union
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
def get_pandas_major():
|
|
@@ -28,6 +28,16 @@ def df_columns(df) -> List[str]:
|
|
|
28
28
|
raise TypeError(f"{type(df)} not supported")
|
|
29
29
|
|
|
30
30
|
|
|
31
|
+
def df_row_names(df) -> List[Union[int, str]]:
|
|
32
|
+
"""Return a list of row names from a dataframe."""
|
|
33
|
+
if df_type(df) == "vaex" or df_type(df) == "polars":
|
|
34
|
+
return list(range(df_len(df)))
|
|
35
|
+
elif df_type(df) == "pandas":
|
|
36
|
+
return df.index.tolist()
|
|
37
|
+
else:
|
|
38
|
+
raise TypeError(f"{type(df)} not supported")
|
|
39
|
+
|
|
40
|
+
|
|
31
41
|
def df_slice(df, start: int, stop: int):
|
|
32
42
|
"""Return a subset of rows from a dataframe."""
|
|
33
43
|
if df_type(df) == "pandas":
|