evo-widgets 0.2.2__tar.gz → 0.2.3__tar.gz
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.
- {evo_widgets-0.2.2 → evo_widgets-0.2.3}/PKG-INFO +1 -1
- {evo_widgets-0.2.2 → evo_widgets-0.2.3}/pyproject.toml +1 -1
- {evo_widgets-0.2.2 → evo_widgets-0.2.3}/src/evo/widgets/__init__.py +11 -8
- {evo_widgets-0.2.2 → evo_widgets-0.2.3}/src/evo/widgets/formatters.py +124 -52
- {evo_widgets-0.2.2 → evo_widgets-0.2.3}/src/evo/widgets/html.py +14 -2
- {evo_widgets-0.2.2 → evo_widgets-0.2.3}/.gitignore +0 -0
- {evo_widgets-0.2.2 → evo_widgets-0.2.3}/LICENSE.md +0 -0
- {evo_widgets-0.2.2 → evo_widgets-0.2.3}/src/evo/widgets/assets/styles.css +0 -0
- {evo_widgets-0.2.2 → evo_widgets-0.2.3}/src/evo/widgets/py.typed +0 -0
- {evo_widgets-0.2.2 → evo_widgets-0.2.3}/src/evo/widgets/urls.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: evo-widgets
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: Widgets and presentation layer for Evo SDK - HTML rendering and IPython formatters for Jupyter notebooks
|
|
5
5
|
Project-URL: Source, https://github.com/SeequentEvo/evo-python-sdk
|
|
6
6
|
Project-URL: Tracker, https://github.com/SeequentEvo/evo-python-sdk/issues
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "evo-widgets"
|
|
3
3
|
description = "Widgets and presentation layer for Evo SDK - HTML rendering and IPython formatters for Jupyter notebooks"
|
|
4
|
-
version = "0.2.
|
|
4
|
+
version = "0.2.3"
|
|
5
5
|
requires-python = ">=3.10"
|
|
6
6
|
license-files = ["LICENSE.md"]
|
|
7
7
|
dynamic = ["readme"]
|
|
@@ -34,6 +34,7 @@ from __future__ import annotations
|
|
|
34
34
|
from typing import TYPE_CHECKING
|
|
35
35
|
|
|
36
36
|
from .formatters import (
|
|
37
|
+
_TASK_RESULT_FORMATTERS,
|
|
37
38
|
format_attributes_collection,
|
|
38
39
|
format_base_object,
|
|
39
40
|
format_block_model,
|
|
@@ -96,6 +97,13 @@ __all__ = [
|
|
|
96
97
|
]
|
|
97
98
|
|
|
98
99
|
|
|
100
|
+
# IPython resolves these lazily by name, so a module move silently disables the
|
|
101
|
+
# formatter; tests/test_extension.py checks each path still resolves.
|
|
102
|
+
_TASK_RESULT_TYPES: tuple[tuple[str, str], ...] = tuple(_TASK_RESULT_FORMATTERS)
|
|
103
|
+
|
|
104
|
+
_TASK_RESULT_LIST_TYPE: tuple[str, str] = ("evo.compute.tasks.common.results", "TaskResultList")
|
|
105
|
+
|
|
106
|
+
|
|
99
107
|
def _register_formatters(ipython: InteractiveShell) -> None:
|
|
100
108
|
"""Register HTML formatters for Evo SDK types.
|
|
101
109
|
|
|
@@ -167,16 +175,11 @@ def _register_formatters(ipython: InteractiveShell) -> None:
|
|
|
167
175
|
)
|
|
168
176
|
|
|
169
177
|
# Register formatters for compute task results
|
|
178
|
+
for (module_name, class_name), formatter in _TASK_RESULT_FORMATTERS.items():
|
|
179
|
+
html_formatter.for_type_by_name(module_name, class_name, formatter)
|
|
170
180
|
|
|
171
181
|
html_formatter.for_type_by_name(
|
|
172
|
-
|
|
173
|
-
"KrigingResult",
|
|
174
|
-
format_task_result_with_target,
|
|
175
|
-
)
|
|
176
|
-
|
|
177
|
-
html_formatter.for_type_by_name(
|
|
178
|
-
"evo.compute.tasks.common.results",
|
|
179
|
-
"TaskResultList",
|
|
182
|
+
*_TASK_RESULT_LIST_TYPE,
|
|
180
183
|
format_task_result_list,
|
|
181
184
|
)
|
|
182
185
|
|
|
@@ -17,8 +17,9 @@ These formatters are registered with IPython when the extension is loaded.
|
|
|
17
17
|
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
|
|
20
|
+
from functools import partial
|
|
20
21
|
from html import escape
|
|
21
|
-
from typing import TYPE_CHECKING, Any
|
|
22
|
+
from typing import TYPE_CHECKING, Any, Callable
|
|
22
23
|
|
|
23
24
|
from .html import (
|
|
24
25
|
STYLESHEET,
|
|
@@ -422,12 +423,12 @@ def format_report_result(obj: Any) -> str:
|
|
|
422
423
|
|
|
423
424
|
# Build the result table with alternating row colors
|
|
424
425
|
headers = list(df.columns)
|
|
425
|
-
header_html = "".join([f"<th>{h}</th>" for h in headers])
|
|
426
|
+
header_html = "".join([f"<th>{escape(str(h))}</th>" for h in headers])
|
|
426
427
|
|
|
427
428
|
rows_html = []
|
|
428
429
|
for i, (_, row) in enumerate(df.iterrows()):
|
|
429
430
|
row_class = 'class="alt-row"' if i % 2 == 1 else ""
|
|
430
|
-
cells = "".join([f"<td>{v if v is not None and v == v else ''}</td>" for v in row])
|
|
431
|
+
cells = "".join([f"<td>{escape(str(v)) if v is not None and v == v else ''}</td>" for v in row])
|
|
431
432
|
rows_html.append(f"<tr {row_class}>{cells}</tr>")
|
|
432
433
|
|
|
433
434
|
subtitle = f'<div class="subtitle">Created: {obj.created_at.strftime("%Y-%m-%d %H:%M:%S")} | Rows: {len(df)}</div>'
|
|
@@ -474,7 +475,8 @@ def format_report(obj: Any) -> str:
|
|
|
474
475
|
for i, col in enumerate(obj._specification.columns):
|
|
475
476
|
row_class = 'class="alt-row"' if i % 2 == 1 else ""
|
|
476
477
|
col_rows.append(
|
|
477
|
-
f"<tr {row_class}><td>{col.label}</td><td>{col.aggregation}</td
|
|
478
|
+
f"<tr {row_class}><td>{escape(str(col.label))}</td><td>{escape(str(col.aggregation))}</td>"
|
|
479
|
+
f"<td>{escape(str(col.output_unit_id))}</td></tr>"
|
|
478
480
|
)
|
|
479
481
|
columns_html = f"""
|
|
480
482
|
<div style="margin-top: 8px;"><strong>Columns:</strong></div>
|
|
@@ -490,8 +492,8 @@ def format_report(obj: Any) -> str:
|
|
|
490
492
|
cat_rows = []
|
|
491
493
|
for i, cat in enumerate(obj._specification.categories):
|
|
492
494
|
row_class = 'class="alt-row"' if i % 2 == 1 else ""
|
|
493
|
-
values_str = ", ".join(cat.values) if cat.values else "(all)"
|
|
494
|
-
cat_rows.append(f"<tr {row_class}><td>{cat.label}</td><td>{values_str}</td></tr>")
|
|
495
|
+
values_str = ", ".join(str(v) for v in cat.values) if cat.values else "(all)"
|
|
496
|
+
cat_rows.append(f"<tr {row_class}><td>{escape(str(cat.label))}</td><td>{escape(values_str)}</td></tr>")
|
|
495
497
|
categories_html = f"""
|
|
496
498
|
<div style="margin-top: 8px;"><strong>Categories:</strong></div>
|
|
497
499
|
<table class="nested">
|
|
@@ -672,73 +674,143 @@ def _get_schema_display(result: Any) -> str:
|
|
|
672
674
|
return "Unknown"
|
|
673
675
|
|
|
674
676
|
|
|
675
|
-
def
|
|
676
|
-
"""Render the inner HTML for a single task result card.
|
|
677
|
-
|
|
678
|
-
Returns the title, message, and detail table *without* the outer
|
|
679
|
-
``<div class="evo">`` wrapper or the stylesheet so it can be embedded
|
|
680
|
-
inside both :func:`format_task_result_with_target` and
|
|
681
|
-
:func:`format_task_result_list`.
|
|
682
|
-
|
|
683
|
-
:param result: A result object (e.g. ``KrigingResult``).
|
|
684
|
-
:param index: Optional 1-based index to prefix the title with (e.g. ``#1``).
|
|
685
|
-
:return: HTML fragment.
|
|
686
|
-
"""
|
|
677
|
+
def _target_result_details(result: Any) -> tuple[list[tuple[str, str]], list[tuple[str, Any]], str | None]:
|
|
687
678
|
portal_url = _get_task_result_portal_url(result)
|
|
688
|
-
links = [("Portal", portal_url)] if portal_url else
|
|
679
|
+
links = [("Portal", portal_url)] if portal_url else []
|
|
680
|
+
rows: list[tuple[str, Any]] = []
|
|
681
|
+
target_name = getattr(result, "target_name", None)
|
|
682
|
+
if target_name is not None:
|
|
683
|
+
rows.extend([("Target:", target_name), ("Schema:", _get_schema_display(result))])
|
|
684
|
+
attribute_name = getattr(result, "attribute_name", None)
|
|
685
|
+
if attribute_name is not None:
|
|
686
|
+
rows.append(("Attribute:", markup(f'<span class="attr-highlight">{escape(attribute_name)}</span>')))
|
|
687
|
+
return links, rows, getattr(result, "message", None)
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
def _turning_bands_result_details(result: Any) -> tuple[list[tuple[str, str]], list[tuple[str, Any]], str | None]:
|
|
691
|
+
links, rows, message = _target_result_details(result)
|
|
692
|
+
if result.simulations_attribute is not None:
|
|
693
|
+
rows.append(("Simulations:", result.simulations_attribute.name))
|
|
694
|
+
return links, rows, message
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
def _conditional_simulation_result_details(
|
|
698
|
+
result: Any,
|
|
699
|
+
) -> tuple[list[tuple[str, str]], list[tuple[str, Any]], str | None]:
|
|
700
|
+
links, rows, message = _target_result_details(result)
|
|
701
|
+
if result.dashboard_url:
|
|
702
|
+
links.append(("Dashboard", result.dashboard_url))
|
|
703
|
+
rows.extend(
|
|
704
|
+
[
|
|
705
|
+
("Mean attribute:", result.summary_attributes.mean.name),
|
|
706
|
+
("Variance attribute:", result.summary_attributes.variance.name),
|
|
707
|
+
]
|
|
708
|
+
)
|
|
709
|
+
if result.validation_summary is not None:
|
|
710
|
+
rows.extend(
|
|
711
|
+
[
|
|
712
|
+
("Reference mean:", result.validation_summary.reference_mean),
|
|
713
|
+
("Simulated mean:", result.validation_summary.mean),
|
|
714
|
+
]
|
|
715
|
+
)
|
|
716
|
+
return links, rows, message
|
|
689
717
|
|
|
690
|
-
result_type = getattr(result, "TASK_DISPLAY_NAME", "Task")
|
|
691
718
|
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
719
|
+
def _location_wise_result_details(result: Any) -> tuple[list[tuple[str, str]], list[tuple[str, Any]], str | None]:
|
|
720
|
+
links, rows, message = _target_result_details(result)
|
|
721
|
+
if result.attribute_names:
|
|
722
|
+
rows.append(("Attributes:", ", ".join(result.attribute_names)))
|
|
723
|
+
return links, rows, message
|
|
696
724
|
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
725
|
+
|
|
726
|
+
def _distribution_result_details(result: Any) -> tuple[list[tuple[str, str]], list[tuple[str, Any]], str | None]:
|
|
727
|
+
try:
|
|
728
|
+
portal_url = get_portal_url_from_reference(result.distribution_reference)
|
|
729
|
+
except ValueError:
|
|
730
|
+
portal_url = None
|
|
731
|
+
links = [("Portal", portal_url)] if portal_url else []
|
|
732
|
+
rows = [("Distribution:", result.distribution_name), ("Schema:", _get_schema_display(result))]
|
|
733
|
+
return links, rows, result.message
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
def _simulation_report_result_details(result: Any) -> tuple[list[tuple[str, str]], list[tuple[str, Any]], str | None]:
|
|
737
|
+
links = []
|
|
738
|
+
if result.report_reference:
|
|
739
|
+
links.append(("Report", result.report_reference))
|
|
740
|
+
if result.dashboard_url:
|
|
741
|
+
links.append(("Dashboard", result.dashboard_url))
|
|
742
|
+
rows = []
|
|
743
|
+
if result.validation_summary is not None:
|
|
701
744
|
rows = [
|
|
702
|
-
("
|
|
703
|
-
("
|
|
704
|
-
("Attribute:", markup(f'<span class="attr-highlight">{escape(attribute_name)}</span>')),
|
|
745
|
+
("Reference mean:", result.validation_summary.reference_mean),
|
|
746
|
+
("Simulated mean:", result.validation_summary.mean),
|
|
705
747
|
]
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
748
|
+
return links, rows, None
|
|
749
|
+
|
|
750
|
+
|
|
751
|
+
_TASK_RESULT_DETAILS: dict[tuple[str, str], Callable[..., tuple[list, list, str | None]]] = {
|
|
752
|
+
(f"evo.compute.tasks.geostatistics.{module}", class_name): details
|
|
753
|
+
for module, class_name, details in (
|
|
754
|
+
("break_ties", "BreakTiesResult", _target_result_details),
|
|
755
|
+
("conditional_turning_bands", "ConditionalTurningBandsResult", _turning_bands_result_details),
|
|
756
|
+
("conditioned_simulator", "ConSimResult", _conditional_simulation_result_details),
|
|
757
|
+
("continuous_distribution", "ContinuousDistributionResult", _distribution_result_details),
|
|
758
|
+
("declustering", "DeclusteringResult", _target_result_details),
|
|
759
|
+
("idw", "IDWResult", _target_result_details),
|
|
760
|
+
("knn", "KNNResult", _target_result_details),
|
|
761
|
+
("kriging", "KrigingResult", _target_result_details),
|
|
762
|
+
("location_wise", "LocationWiseResult", _location_wise_result_details),
|
|
763
|
+
("loss_calculation", "LossCalculationResult", _target_result_details),
|
|
764
|
+
("normal_score", "NormalScoreResult", _target_result_details),
|
|
765
|
+
("profit_calculation", "ProfitCalculationResult", _target_result_details),
|
|
766
|
+
("simulation_report", "SimulationReportResult", _simulation_report_result_details),
|
|
767
|
+
)
|
|
768
|
+
}
|
|
710
769
|
|
|
711
|
-
html = build_title(title, links)
|
|
712
|
-
message = getattr(result, "message", None)
|
|
713
|
-
if message:
|
|
714
|
-
html += f'<div class="message">{message}</div>'
|
|
715
|
-
if table_rows:
|
|
716
|
-
html += f"<table>{''.join(table_rows)}</table>"
|
|
717
770
|
|
|
771
|
+
def _format_single_task_result_inner(result: Any, index: int | None = None, details: Callable | None = None) -> str:
|
|
772
|
+
"""Render a result fragment, including its result-specific links and rows."""
|
|
773
|
+
result_type = getattr(result, "TASK_DISPLAY_NAME", "Task")
|
|
774
|
+
title = f"#{index} ✓ {result_type} Result" if index is not None else f"✓ {result_type} Result"
|
|
775
|
+
if details is None:
|
|
776
|
+
for result_class in type(result).__mro__:
|
|
777
|
+
details = _TASK_RESULT_DETAILS.get((result_class.__module__, result_class.__name__))
|
|
778
|
+
if details is not None:
|
|
779
|
+
break
|
|
780
|
+
else:
|
|
781
|
+
details = _target_result_details
|
|
782
|
+
links, rows, message = details(result)
|
|
783
|
+
html = build_title(title, links or None)
|
|
784
|
+
html += f'<div class="message">{escape(str(message)) if message else "Task completed"}</div>'
|
|
785
|
+
if rows:
|
|
786
|
+
html += f"<table>{''.join(build_table_row(label, value) for label, value in rows)}</table>"
|
|
718
787
|
return html
|
|
719
788
|
|
|
720
789
|
|
|
790
|
+
def _format_task_result(result: Any, details: Callable | None = None) -> str:
|
|
791
|
+
return STYLESHEET + '<div class="evo">' + _format_single_task_result_inner(result, details=details) + "</div>"
|
|
792
|
+
|
|
793
|
+
|
|
794
|
+
_TASK_RESULT_FORMATTERS = {
|
|
795
|
+
result_type: partial(_format_task_result, details=details) for result_type, details in _TASK_RESULT_DETAILS.items()
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
|
|
721
799
|
def format_task_result_with_target(result: Any) -> str:
|
|
722
|
-
"""Format a
|
|
800
|
+
"""Format a compute task result as HTML, using its result-specific fields.
|
|
723
801
|
|
|
724
|
-
Displays the task completion status,
|
|
802
|
+
Displays the task completion status, available details, and related links.
|
|
725
803
|
|
|
726
|
-
:param result: A
|
|
727
|
-
attribute_name, and _target attributes.
|
|
804
|
+
:param result: A geostatistics task result.
|
|
728
805
|
:return: HTML string for the task result.
|
|
729
806
|
"""
|
|
730
|
-
|
|
731
|
-
html += '<div class="evo">'
|
|
732
|
-
html += _format_single_task_result_inner(result)
|
|
733
|
-
html += "</div>"
|
|
734
|
-
return html
|
|
807
|
+
return _format_task_result(result)
|
|
735
808
|
|
|
736
809
|
|
|
737
810
|
def format_task_result_list(results: Any) -> str:
|
|
738
811
|
"""Format a TaskResultList as styled HTML.
|
|
739
812
|
|
|
740
|
-
Renders each result
|
|
741
|
-
same layout as :func:`format_task_result_with_target`) wrapped in a
|
|
813
|
+
Renders each result with its own shape-specific details, wrapped in a
|
|
742
814
|
single container with a summary title.
|
|
743
815
|
|
|
744
816
|
:param results: A ``TaskResultList`` object with iterable result items.
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
from collections.abc import Sequence
|
|
15
15
|
from html import escape
|
|
16
16
|
from pathlib import Path
|
|
17
|
+
from urllib.parse import urlsplit
|
|
17
18
|
|
|
18
19
|
# Load CSS from external file
|
|
19
20
|
_CSS_PATH = Path(__file__).parent / "assets" / "styles.css"
|
|
@@ -53,8 +54,19 @@ def build_title(text: str, links: list[tuple[str, str]] | None = None) -> str:
|
|
|
53
54
|
:param links: Optional list of (label, url) tuples for links next to the title.
|
|
54
55
|
:return: HTML string.
|
|
55
56
|
"""
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
safe_links = []
|
|
58
|
+
for label, url in links or []:
|
|
59
|
+
try:
|
|
60
|
+
parsed = urlsplit(url)
|
|
61
|
+
if parsed.scheme not in ("http", "https") or not parsed.hostname or any(ord(char) <= 32 for char in url):
|
|
62
|
+
continue
|
|
63
|
+
except ValueError:
|
|
64
|
+
continue
|
|
65
|
+
safe_links.append((label, url))
|
|
66
|
+
if safe_links:
|
|
67
|
+
link_html = " | ".join(
|
|
68
|
+
f'<a href="{escape(url)}" target="_blank">{escape(label)}</a>' for label, url in safe_links
|
|
69
|
+
)
|
|
58
70
|
return f'<div class="title"><span>{escape(text)}</span><span class="title-links">{link_html}</span></div>'
|
|
59
71
|
return f'<div class="title">{escape(text)}</div>'
|
|
60
72
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|