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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: evo-widgets
3
- Version: 0.2.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.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
- "evo.compute.tasks.kriging",
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><td>{col.output_unit_id}</td></tr>"
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 _format_single_task_result_inner(result: Any, index: int | None = None) -> str:
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 None
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
- if index is not None:
693
- title = f"#{index} ✓ {result_type} Result"
694
- else:
695
- title = f"✓ {result_type} Result"
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
- target_name = getattr(result, "target_name", None)
698
- if target_name is not None:
699
- schema_display = _get_schema_display(result)
700
- attribute_name = getattr(result, "attribute_name", "")
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
- ("Target:", target_name),
703
- ("Schema:", schema_display),
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
- else:
707
- rows = []
708
-
709
- table_rows = [build_table_row(label, value) for label, value in rows]
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 KrigingResult or any other task result with a target as HTML.
800
+ """Format a compute task result as HTML, using its result-specific fields.
723
801
 
724
- Displays the task completion status, target information, and Portal links.
802
+ Displays the task completion status, available details, and related links.
725
803
 
726
- :param result: A KrigingResult object with message, target_name, schema,
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
- html = STYLESHEET
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 as an individually-formatted card (reusing the
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
- if links:
57
- link_html = " | ".join([f'<a href="{escape(url)}" target="_blank">{escape(label)}</a>' for label, url in links])
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