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.
- {evo_widgets-0.2.1 → evo_widgets-0.2.2}/PKG-INFO +2 -2
- {evo_widgets-0.2.1 → evo_widgets-0.2.2}/pyproject.toml +1 -1
- {evo_widgets-0.2.1 → evo_widgets-0.2.2}/src/evo/widgets/formatters.py +21 -13
- {evo_widgets-0.2.1 → evo_widgets-0.2.2}/src/evo/widgets/html.py +52 -24
- {evo_widgets-0.2.1 → evo_widgets-0.2.2}/src/evo/widgets/urls.py +2 -2
- {evo_widgets-0.2.1 → evo_widgets-0.2.2}/.gitignore +0 -0
- {evo_widgets-0.2.1 → evo_widgets-0.2.2}/LICENSE.md +0 -0
- {evo_widgets-0.2.1 → evo_widgets-0.2.2}/src/evo/widgets/__init__.py +0 -0
- {evo_widgets-0.2.1 → evo_widgets-0.2.2}/src/evo/widgets/assets/styles.css +0 -0
- {evo_widgets-0.2.1 → evo_widgets-0.2.2}/src/evo/widgets/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: evo-widgets
|
|
3
|
-
Version: 0.2.
|
|
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.
|
|
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
|
|
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
|
-
#
|
|
175
|
-
|
|
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(
|
|
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
|
-
|
|
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:
|
|
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
|
|
52
|
-
:param is_last:
|
|
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
|
|
63
|
-
:param is_last:
|
|
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(
|
|
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 =
|
|
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(
|
|
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
|
|
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 = [
|
|
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"?
|
|
124
|
+
url += f"?{urlencode({'result_id': result_id})}"
|
|
125
125
|
return url
|
|
126
126
|
|
|
127
127
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|