evo-widgets 0.2.1__tar.gz → 0.2.2__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
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: evo-widgets
3
- Version: 0.2.1
3
+ Version: 0.2.2
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.1"
4
+ version = "0.2.2"
5
5
  requires-python = ">=3.10"
6
6
  license-files = ["LICENSE.md"]
7
7
  dynamic = ["readme"]
@@ -17,6 +17,7 @@ These formatters are registered with IPython when the extension is loaded.
17
17
 
18
18
  from __future__ import annotations
19
19
 
20
+ from html import escape
20
21
  from typing import TYPE_CHECKING, Any
21
22
 
22
23
  from .html import (
@@ -25,6 +26,7 @@ from .html import (
25
26
  build_table_row,
26
27
  build_table_row_vtop,
27
28
  build_title,
29
+ markup,
28
30
  )
29
31
  from .urls import (
30
32
  get_blocksync_block_model_url_from_environment,
@@ -103,9 +105,9 @@ def _format_bounding_box(bbox: dict[str, Any]) -> str:
103
105
  :return: HTML string for the bounding box table.
104
106
  """
105
107
  bbox_rows = [
106
- ["<strong>X:</strong>", f"{bbox.get('min_x', 0):.2f}", f"{bbox.get('max_x', 0):.2f}"],
107
- ["<strong>Y:</strong>", f"{bbox.get('min_y', 0):.2f}", f"{bbox.get('max_y', 0):.2f}"],
108
- ["<strong>Z:</strong>", f"{bbox.get('min_z', 0):.2f}", f"{bbox.get('max_z', 0):.2f}"],
108
+ [markup("<strong>X:</strong>"), f"{bbox.get('min_x', 0):.2f}", f"{bbox.get('max_x', 0):.2f}"],
109
+ [markup("<strong>Y:</strong>"), f"{bbox.get('min_y', 0):.2f}", f"{bbox.get('max_y', 0):.2f}"],
110
+ [markup("<strong>Z:</strong>"), f"{bbox.get('min_z', 0):.2f}", f"{bbox.get('max_z', 0):.2f}"],
109
111
  ]
110
112
  return build_nested_table(["", "Min", "Max"], bbox_rows)
111
113
 
@@ -154,7 +156,7 @@ def _build_html_from_rows(
154
156
  return html
155
157
 
156
158
 
157
- def _format_attributes_spec(attributes, rows) -> str:
159
+ def _format_attributes_spec(attributes) -> str:
158
160
  attr_rows = []
159
161
  for attr in attributes:
160
162
  attr_info = attr.as_dict()
@@ -167,12 +169,19 @@ def _format_attributes_spec(attributes, rows) -> str:
167
169
 
168
170
  def _format_all_attribute_specs(obj, rows) -> None:
169
171
  # Build datasets section - add as rows to the main table
172
+ top_level_attrs = None
173
+ if hasattr(obj, "attributes") and len(obj.attributes) > 0:
174
+ top_level_attrs = obj.attributes
175
+ rows.append(("Attributes:", _format_attributes_spec(obj.attributes)))
176
+
170
177
  sub_models = getattr(obj, "_sub_models", [])
171
178
  for dataset_name in sub_models:
172
179
  dataset = getattr(obj, dataset_name, None)
173
180
  if dataset and hasattr(dataset, "attributes") and len(dataset.attributes) > 0:
174
- # Build attribute rows
175
- rows.append((f"{dataset_name}", _format_attributes_spec(dataset.attributes, rows)))
181
+ # Skip sub-model attributes that are an alias for the already-rendered top-level attributes
182
+ if dataset.attributes is top_level_attrs:
183
+ continue
184
+ rows.append((f"{dataset_name} attributes:", _format_attributes_spec(dataset.attributes)))
176
185
 
177
186
 
178
187
  def format_base_object(obj: Any) -> str:
@@ -321,7 +330,7 @@ def _format_downhole_collection_collections(obj, rows):
321
330
  if collection.collection_type == "distance":
322
331
  rows.append((f"{name} distance unit:", collection.distance.unit))
323
332
  if attributes := collection.distance.attributes:
324
- rows.append({f"{name} attributes:", _format_attributes_spec(attributes, rows)})
333
+ rows.append((f"{name} attributes:", _format_attributes_spec(attributes)))
325
334
 
326
335
 
327
336
  def format_downhole_collection(obj: DownholeCollection) -> str:
@@ -345,7 +354,6 @@ def format_downhole_collection(obj: DownholeCollection) -> str:
345
354
  rows.append(("Number of holes:", str(obj.location.hole_id.length)))
346
355
 
347
356
  _format_all_attribute_specs(obj, rows)
348
- _format_all_attribute_specs(obj.location, rows)
349
357
  _format_downhole_collection_collections(obj, rows)
350
358
 
351
359
  return _build_html_from_rows(name, title_links, rows)
@@ -571,17 +579,17 @@ def format_block_model(obj: Any) -> str:
571
579
  # Add geometry info
572
580
  geom = obj.geometry
573
581
  geom_rows = [
574
- ["<strong>Origin:</strong>", f"({geom.origin.x:.2f}, {geom.origin.y:.2f}, {geom.origin.z:.2f})"],
575
- ["<strong>N Blocks:</strong>", f"({geom.n_blocks.nx}, {geom.n_blocks.ny}, {geom.n_blocks.nz})"],
582
+ [markup("<strong>Origin:</strong>"), f"({geom.origin.x:.2f}, {geom.origin.y:.2f}, {geom.origin.z:.2f})"],
583
+ [markup("<strong>N Blocks:</strong>"), f"({geom.n_blocks.nx}, {geom.n_blocks.ny}, {geom.n_blocks.nz})"],
576
584
  [
577
- "<strong>Block Size:</strong>",
585
+ markup("<strong>Block Size:</strong>"),
578
586
  f"({geom.block_size.dx:.2f}, {geom.block_size.dy:.2f}, {geom.block_size.dz:.2f})",
579
587
  ],
580
588
  ]
581
589
  if geom.rotation:
582
590
  geom_rows.append(
583
591
  [
584
- "<strong>Rotation:</strong>",
592
+ markup("<strong>Rotation:</strong>"),
585
593
  f"({geom.rotation.dip_azimuth:.2f}, {geom.rotation.dip:.2f}, {geom.rotation.pitch:.2f})",
586
594
  ]
587
595
  )
@@ -693,7 +701,7 @@ def _format_single_task_result_inner(result: Any, index: int | None = None) -> s
693
701
  rows = [
694
702
  ("Target:", target_name),
695
703
  ("Schema:", schema_display),
696
- ("Attribute:", f'<span class="attr-highlight">{attribute_name}</span>'),
704
+ ("Attribute:", markup(f'<span class="attr-highlight">{escape(attribute_name)}</span>')),
697
705
  ]
698
706
  else:
699
707
  rows = []
@@ -11,6 +11,8 @@
11
11
 
12
12
  """Shared HTML styles for Jupyter notebook representations across all Evo SDK packages."""
13
13
 
14
+ from collections.abc import Sequence
15
+ from html import escape
14
16
  from pathlib import Path
15
17
 
16
18
  # Load CSS from external file
@@ -21,14 +23,27 @@ _CSS_CONTENT = _CSS_PATH.read_text(encoding="utf-8")
21
23
  STYLESHEET = f"<style>\n{_CSS_CONTENT}</style>\n"
22
24
 
23
25
 
24
- def build_container(content: str, css_class: str = "evo") -> str:
26
+ class HtmlMarkup(str):
27
+ """Markup deliberately produced by this package and safe to embed in HTML."""
28
+
29
+
30
+ def markup(value: str) -> HtmlMarkup:
31
+ """Mark package-generated HTML for explicit insertion by the HTML builders."""
32
+ return HtmlMarkup(value)
33
+
34
+
35
+ def _html_text(value: object) -> str:
36
+ return str(value) if isinstance(value, HtmlMarkup) else escape(str(value))
37
+
38
+
39
+ def build_container(content: str | HtmlMarkup, css_class: str = "evo") -> str:
25
40
  """Wrap content in a styled container div.
26
41
 
27
- :param content: HTML content to wrap.
42
+ :param content: Content to wrap, escaped unless wrapped with :func:`markup`.
28
43
  :param css_class: CSS class for the container (default: "evo").
29
44
  :return: Wrapped HTML string.
30
45
  """
31
- return f'{STYLESHEET}<div class="{css_class}">{content}</div>'
46
+ return f'{STYLESHEET}<div class="{escape(css_class)}">{_html_text(content)}</div>'
32
47
 
33
48
 
34
49
  def build_title(text: str, links: list[tuple[str, str]] | None = None) -> str:
@@ -39,31 +54,31 @@ def build_title(text: str, links: list[tuple[str, str]] | None = None) -> str:
39
54
  :return: HTML string.
40
55
  """
41
56
  if links:
42
- link_html = " | ".join([f'<a href="{url}" target="_blank">{label}</a>' for label, url in links])
43
- return f'<div class="title"><span>{text}</span><span class="title-links">{link_html}</span></div>'
44
- return f'<div class="title">{text}</div>'
57
+ link_html = " | ".join([f'<a href="{escape(url)}" target="_blank">{escape(label)}</a>' for label, url in links])
58
+ return f'<div class="title"><span>{escape(text)}</span><span class="title-links">{link_html}</span></div>'
59
+ return f'<div class="title">{escape(text)}</div>'
45
60
 
46
61
 
47
- def build_table_row(label: str, value: str, is_last: bool = False) -> str:
62
+ def build_table_row(label: str, value: str | HtmlMarkup, is_last: bool = False) -> str:
48
63
  """Create a table row with label and value.
49
64
 
50
65
  :param label: Label text.
51
- :param value: Value text (can contain HTML).
52
- :param is_last: If True, don't add bottom border.
66
+ :param value: Value text, escaped unless wrapped with :func:`markup`.
67
+ :param is_last: Retained for backwards compatibility; has no effect.
53
68
  :return: HTML string.
54
69
  """
55
- return f'<tr><td class="label">{label}</td><td class="value">{value}</td></tr>'
70
+ return f'<tr><td class="label">{_html_text(label)}</td><td class="value">{_html_text(value)}</td></tr>'
56
71
 
57
72
 
58
- def build_table_row_vtop(label: str, value: str, is_last: bool = False) -> str:
73
+ def build_table_row_vtop(label: str, value: str | HtmlMarkup, is_last: bool = False) -> str:
59
74
  """Create a table row with label and value (label top-aligned).
60
75
 
61
76
  :param label: Label text.
62
- :param value: Value text (can contain HTML).
63
- :param is_last: If True, don't add bottom border.
77
+ :param value: Value text, escaped unless wrapped with :func:`markup`.
78
+ :param is_last: Retained for backwards compatibility; has no effect.
64
79
  :return: HTML string.
65
80
  """
66
- return f'<tr><td class="label-vtop">{label}</td><td class="value">{value}</td></tr>'
81
+ return f'<tr><td class="label-vtop">{_html_text(label)}</td><td class="value">{_html_text(value)}</td></tr>'
67
82
 
68
83
 
69
84
  def build_section_divider(title: str) -> str:
@@ -72,10 +87,10 @@ def build_section_divider(title: str) -> str:
72
87
  :param title: Section title.
73
88
  :return: HTML string.
74
89
  """
75
- return f'<div class="section"><div class="section-heading">{title}</div>'
90
+ return f'<div class="section"><div class="section-heading">{_html_text(title)}</div>'
76
91
 
77
92
 
78
- def build_table(rows: list[tuple[str, str]]) -> str:
93
+ def build_table(rows: list[tuple[str, str | HtmlMarkup]]) -> str:
79
94
  """Build an HTML table from rows of (label, value) tuples.
80
95
 
81
96
  :param rows: List of (label, value) tuples.
@@ -85,7 +100,9 @@ def build_table(rows: list[tuple[str, str]]) -> str:
85
100
  return f"<table>{''.join(table_rows)}</table>"
86
101
 
87
102
 
88
- def build_nested_table(headers: list[str], rows: list[list[str]], css_class: str = "") -> str:
103
+ def build_nested_table(
104
+ headers: Sequence[str], rows: Sequence[Sequence[str | int | float]], css_class: str = ""
105
+ ) -> HtmlMarkup:
89
106
  """Build a nested HTML table with headers and data rows.
90
107
 
91
108
  :param headers: List of header strings.
@@ -93,12 +110,12 @@ def build_nested_table(headers: list[str], rows: list[list[str]], css_class: str
93
110
  :param css_class: Additional CSS classes to add to the table (optional).
94
111
  :return: HTML string for a nested table.
95
112
  """
96
- class_attr = f' class="nested {css_class}"' if css_class else ' class="nested"'
113
+ class_attr = f' class="nested {escape(css_class)}"' if css_class else ' class="nested"'
97
114
 
98
115
  # Build header row
99
116
  header_cells = []
100
117
  for i, header in enumerate(headers):
101
- header_cells.append(f"<th>{header}</th>")
118
+ header_cells.append(f"<th>{_html_text(header)}</th>")
102
119
 
103
120
  # Build data rows
104
121
  data_rows = []
@@ -109,20 +126,31 @@ def build_nested_table(headers: list[str], rows: list[list[str]], css_class: str
109
126
  if isinstance(cell, (int, float)):
110
127
  formatted_cell = f"{cell:.2f}"
111
128
  else:
112
- formatted_cell = str(cell)
129
+ formatted_cell = _html_text(cell)
113
130
  cells.append(f"<td>{formatted_cell}</td>")
114
131
  data_rows.append(f"<tr>{''.join(cells)}</tr>")
115
132
 
116
- return f"<table{class_attr}><tr>{''.join(header_cells)}</tr>{''.join(data_rows)}</table>"
133
+ return markup(f"<table{class_attr}><tr>{''.join(header_cells)}</tr>{''.join(data_rows)}</table>")
117
134
 
118
135
 
119
- def build_object_html(title: str, rows: list[tuple[str, str]], extra_content: str = "") -> str:
136
+ def build_object_html(
137
+ title: str,
138
+ rows: list[tuple[str, str | HtmlMarkup]],
139
+ extra_content: str | HtmlMarkup = "",
140
+ ) -> str:
120
141
  """Build a complete object HTML representation.
121
142
 
122
143
  :param title: Object title/name.
123
144
  :param rows: List of (label, value) tuples for the main properties table.
124
- :param extra_content: Additional HTML content to append after the table (optional).
145
+ :param extra_content: Additional content, escaped unless wrapped with :func:`markup`.
125
146
  :return: Complete HTML string with stylesheet.
126
147
  """
127
- html_parts = [STYLESHEET, '<div class="evo">', build_title(title), build_table(rows), extra_content, "</div>"]
148
+ html_parts = [
149
+ STYLESHEET,
150
+ '<div class="evo">',
151
+ build_title(title),
152
+ build_table(rows),
153
+ _html_text(extra_content),
154
+ "</div>",
155
+ ]
128
156
  return "".join(html_parts)
@@ -17,7 +17,7 @@ This module provides functions to generate URLs for viewing objects in the Evo P
17
17
  from __future__ import annotations
18
18
 
19
19
  from typing import TYPE_CHECKING, Any
20
- from urllib.parse import urlparse
20
+ from urllib.parse import urlencode, urlparse
21
21
  from uuid import UUID
22
22
 
23
23
  from evo.objects import ObjectReference
@@ -121,7 +121,7 @@ def get_blocksync_report_url(
121
121
  base_url = get_blocksync_base_url("")
122
122
  url = f"{base_url}/{org_id}/{hub_code}/{workspace_id}/blockmodel/{block_model_id}/reports/{report_id}"
123
123
  if result_id:
124
- url += f"?result_id={result_id}"
124
+ url += f"?{urlencode({'result_id': result_id})}"
125
125
  return url
126
126
 
127
127
 
File without changes
File without changes