evo-widgets 0.2.0__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.0 → evo_widgets-0.2.2}/PKG-INFO +3 -3
- {evo_widgets-0.2.0 → evo_widgets-0.2.2}/pyproject.toml +3 -3
- {evo_widgets-0.2.0 → evo_widgets-0.2.2}/src/evo/widgets/__init__.py +8 -1
- {evo_widgets-0.2.0 → evo_widgets-0.2.2}/src/evo/widgets/formatters.py +83 -25
- {evo_widgets-0.2.0 → evo_widgets-0.2.2}/src/evo/widgets/html.py +52 -24
- {evo_widgets-0.2.0 → evo_widgets-0.2.2}/src/evo/widgets/urls.py +2 -2
- {evo_widgets-0.2.0 → evo_widgets-0.2.2}/.gitignore +0 -0
- {evo_widgets-0.2.0 → evo_widgets-0.2.2}/LICENSE.md +0 -0
- {evo_widgets-0.2.0 → evo_widgets-0.2.2}/src/evo/widgets/assets/styles.css +0 -0
- {evo_widgets-0.2.0 → 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
|
|
@@ -9,7 +9,7 @@ Project-URL: Documentation, https://developer.seequent.com/
|
|
|
9
9
|
Author-email: Seequent <support@seequent.com>
|
|
10
10
|
License-File: LICENSE.md
|
|
11
11
|
Requires-Python: >=3.10
|
|
12
|
-
Requires-Dist: evo-sdk-common>=0.5.
|
|
12
|
+
Requires-Dist: evo-sdk-common>=0.5.25
|
|
13
13
|
Requires-Dist: ipython>=7.0.0
|
|
14
14
|
Description-Content-Type: text/markdown
|
|
15
15
|
|
|
@@ -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"]
|
|
@@ -10,7 +10,7 @@ authors = [
|
|
|
10
10
|
]
|
|
11
11
|
|
|
12
12
|
dependencies = [
|
|
13
|
-
"evo-sdk-common>=0.5.
|
|
13
|
+
"evo-sdk-common>=0.5.25",
|
|
14
14
|
"ipython>=7.0.0",
|
|
15
15
|
]
|
|
16
16
|
|
|
@@ -29,7 +29,7 @@ test = [
|
|
|
29
29
|
dev = [
|
|
30
30
|
"bumpver",
|
|
31
31
|
"coverage[toml]",
|
|
32
|
-
"ruff==0.
|
|
32
|
+
"ruff==0.15.16",
|
|
33
33
|
{include-group="test"},
|
|
34
34
|
]
|
|
35
35
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright ©
|
|
1
|
+
# Copyright © 2026 Bentley Systems, Incorporated
|
|
2
2
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
3
3
|
# you may not use this file except in compliance with the License.
|
|
4
4
|
# You may obtain a copy of the License at
|
|
@@ -39,6 +39,7 @@ from .formatters import (
|
|
|
39
39
|
format_block_model,
|
|
40
40
|
format_block_model_attributes,
|
|
41
41
|
format_block_model_version,
|
|
42
|
+
format_downhole_collection,
|
|
42
43
|
format_report,
|
|
43
44
|
format_report_result,
|
|
44
45
|
format_task_result_list,
|
|
@@ -159,6 +160,12 @@ def _register_formatters(ipython: InteractiveShell) -> None:
|
|
|
159
160
|
format_block_model_attributes,
|
|
160
161
|
)
|
|
161
162
|
|
|
163
|
+
html_formatter.for_type_by_name(
|
|
164
|
+
"evo.objects.typed.downhole_collection",
|
|
165
|
+
"DownholeCollection",
|
|
166
|
+
format_downhole_collection,
|
|
167
|
+
)
|
|
168
|
+
|
|
162
169
|
# Register formatters for compute task results
|
|
163
170
|
|
|
164
171
|
html_formatter.for_type_by_name(
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright ©
|
|
1
|
+
# Copyright © 2026 Bentley Systems, Incorporated
|
|
2
2
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
3
3
|
# you may not use this file except in compliance with the License.
|
|
4
4
|
# You may obtain a copy of the License at
|
|
@@ -17,7 +17,8 @@ These formatters are registered with IPython when the extension is loaded.
|
|
|
17
17
|
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
|
|
20
|
-
from
|
|
20
|
+
from html import escape
|
|
21
|
+
from typing import TYPE_CHECKING, Any
|
|
21
22
|
|
|
22
23
|
from .html import (
|
|
23
24
|
STYLESHEET,
|
|
@@ -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,
|
|
@@ -33,12 +35,17 @@ from .urls import (
|
|
|
33
35
|
get_viewer_url_for_object,
|
|
34
36
|
)
|
|
35
37
|
|
|
38
|
+
if TYPE_CHECKING:
|
|
39
|
+
from evo.objects.typed import DownholeCollection
|
|
40
|
+
|
|
41
|
+
|
|
36
42
|
__all__ = [
|
|
37
43
|
"format_attributes_collection",
|
|
38
44
|
"format_base_object",
|
|
39
45
|
"format_block_model",
|
|
40
46
|
"format_block_model_attributes",
|
|
41
47
|
"format_block_model_version",
|
|
48
|
+
"format_downhole_collection",
|
|
42
49
|
"format_report",
|
|
43
50
|
"format_report_result",
|
|
44
51
|
"format_task_result_list",
|
|
@@ -98,9 +105,9 @@ def _format_bounding_box(bbox: dict[str, Any]) -> str:
|
|
|
98
105
|
:return: HTML string for the bounding box table.
|
|
99
106
|
"""
|
|
100
107
|
bbox_rows = [
|
|
101
|
-
["<strong>X:</strong>", f"{bbox.get('min_x', 0):.2f}", f"{bbox.get('max_x', 0):.2f}"],
|
|
102
|
-
["<strong>Y:</strong>", f"{bbox.get('min_y', 0):.2f}", f"{bbox.get('max_y', 0):.2f}"],
|
|
103
|
-
["<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}"],
|
|
104
111
|
]
|
|
105
112
|
return build_nested_table(["", "Min", "Max"], bbox_rows)
|
|
106
113
|
|
|
@@ -149,6 +156,34 @@ def _build_html_from_rows(
|
|
|
149
156
|
return html
|
|
150
157
|
|
|
151
158
|
|
|
159
|
+
def _format_attributes_spec(attributes) -> str:
|
|
160
|
+
attr_rows = []
|
|
161
|
+
for attr in attributes:
|
|
162
|
+
attr_info = attr.as_dict()
|
|
163
|
+
attr_name = attr_info.get("name", "Unknown")
|
|
164
|
+
attr_type = attr_info.get("attribute_type", "Unknown")
|
|
165
|
+
attr_rows.append([attr_name, attr_type])
|
|
166
|
+
|
|
167
|
+
return build_nested_table(["Attribute", "Type"], attr_rows)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _format_all_attribute_specs(obj, rows) -> None:
|
|
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
|
+
|
|
177
|
+
sub_models = getattr(obj, "_sub_models", [])
|
|
178
|
+
for dataset_name in sub_models:
|
|
179
|
+
dataset = getattr(obj, dataset_name, None)
|
|
180
|
+
if dataset and hasattr(dataset, "attributes") and len(dataset.attributes) > 0:
|
|
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)))
|
|
185
|
+
|
|
186
|
+
|
|
152
187
|
def format_base_object(obj: Any) -> str:
|
|
153
188
|
"""Format a BaseObject (or subclass) as HTML.
|
|
154
189
|
|
|
@@ -169,21 +204,7 @@ def format_base_object(obj: Any) -> str:
|
|
|
169
204
|
if crs := doc.get("coordinate_reference_system"):
|
|
170
205
|
rows.append(("CRS:", _format_crs(crs)))
|
|
171
206
|
|
|
172
|
-
|
|
173
|
-
sub_models = getattr(obj, "_sub_models", [])
|
|
174
|
-
for dataset_name in sub_models:
|
|
175
|
-
dataset = getattr(obj, dataset_name, None)
|
|
176
|
-
if dataset and hasattr(dataset, "attributes") and len(dataset.attributes) > 0:
|
|
177
|
-
# Build attribute rows
|
|
178
|
-
attr_rows = []
|
|
179
|
-
for attr in dataset.attributes:
|
|
180
|
-
attr_info = attr.as_dict()
|
|
181
|
-
attr_name = attr_info.get("name", "Unknown")
|
|
182
|
-
attr_type = attr_info.get("attribute_type", "Unknown")
|
|
183
|
-
attr_rows.append([attr_name, attr_type])
|
|
184
|
-
|
|
185
|
-
attrs_table = build_nested_table(["Attribute", "Type"], attr_rows)
|
|
186
|
-
rows.append((f"{dataset_name}:", attrs_table))
|
|
207
|
+
_format_all_attribute_specs(obj, rows)
|
|
187
208
|
|
|
188
209
|
return _build_html_from_rows(name, title_links, rows)
|
|
189
210
|
|
|
@@ -301,6 +322,43 @@ def format_variogram(obj: Any) -> str:
|
|
|
301
322
|
return _build_html_from_rows(name, title_links, rows, extra_content)
|
|
302
323
|
|
|
303
324
|
|
|
325
|
+
def _format_downhole_collection_collections(obj, rows):
|
|
326
|
+
for i, collection in enumerate(obj.collections):
|
|
327
|
+
name = f"Collection {i + 1}"
|
|
328
|
+
rows.append((f"{name} name:", collection.name))
|
|
329
|
+
rows.append((f"{name} type:", collection.collection_type))
|
|
330
|
+
if collection.collection_type == "distance":
|
|
331
|
+
rows.append((f"{name} distance unit:", collection.distance.unit))
|
|
332
|
+
if attributes := collection.distance.attributes:
|
|
333
|
+
rows.append((f"{name} attributes:", _format_attributes_spec(attributes)))
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def format_downhole_collection(obj: DownholeCollection) -> str:
|
|
337
|
+
"""Format a DownholeCollection object as HTML.
|
|
338
|
+
|
|
339
|
+
This formatter renders a downhole collection by extracting metadata and rendering it as a styled HTML table with
|
|
340
|
+
Portal/Viewer links.
|
|
341
|
+
|
|
342
|
+
:param obj: A DownholeCollection object from evo.objects.typed.downhole_collection.
|
|
343
|
+
:return: HTML string representation.
|
|
344
|
+
"""
|
|
345
|
+
doc = obj.as_dict()
|
|
346
|
+
name, title_links, rows = _get_base_metadata(obj)
|
|
347
|
+
rows.append(("Bounding box:", _format_bounding_box(doc["bounding_box"])))
|
|
348
|
+
rows.append(("CRS:", _format_crs(doc["coordinate_reference_system"])))
|
|
349
|
+
|
|
350
|
+
# DownholeCollection-specific scalar properties
|
|
351
|
+
rows.append(("Distance unit:", str(obj.distance_unit)))
|
|
352
|
+
rows.append(("Desurvey method:", str(obj.desurvey)))
|
|
353
|
+
rows.append(("Type:", obj.type))
|
|
354
|
+
rows.append(("Number of holes:", str(obj.location.hole_id.length)))
|
|
355
|
+
|
|
356
|
+
_format_all_attribute_specs(obj, rows)
|
|
357
|
+
_format_downhole_collection_collections(obj, rows)
|
|
358
|
+
|
|
359
|
+
return _build_html_from_rows(name, title_links, rows)
|
|
360
|
+
|
|
361
|
+
|
|
304
362
|
def format_block_model_version(obj: Any) -> str:
|
|
305
363
|
"""Format a block model Version object as HTML.
|
|
306
364
|
|
|
@@ -521,17 +579,17 @@ def format_block_model(obj: Any) -> str:
|
|
|
521
579
|
# Add geometry info
|
|
522
580
|
geom = obj.geometry
|
|
523
581
|
geom_rows = [
|
|
524
|
-
["<strong>Origin:</strong>", f"({geom.origin.x:.2f}, {geom.origin.y:.2f}, {geom.origin.z:.2f})"],
|
|
525
|
-
["<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})"],
|
|
526
584
|
[
|
|
527
|
-
"<strong>Block Size:</strong>",
|
|
585
|
+
markup("<strong>Block Size:</strong>"),
|
|
528
586
|
f"({geom.block_size.dx:.2f}, {geom.block_size.dy:.2f}, {geom.block_size.dz:.2f})",
|
|
529
587
|
],
|
|
530
588
|
]
|
|
531
589
|
if geom.rotation:
|
|
532
590
|
geom_rows.append(
|
|
533
591
|
[
|
|
534
|
-
"<strong>Rotation:</strong>",
|
|
592
|
+
markup("<strong>Rotation:</strong>"),
|
|
535
593
|
f"({geom.rotation.dip_azimuth:.2f}, {geom.rotation.dip:.2f}, {geom.rotation.pitch:.2f})",
|
|
536
594
|
]
|
|
537
595
|
)
|
|
@@ -643,7 +701,7 @@ def _format_single_task_result_inner(result: Any, index: int | None = None) -> s
|
|
|
643
701
|
rows = [
|
|
644
702
|
("Target:", target_name),
|
|
645
703
|
("Schema:", schema_display),
|
|
646
|
-
("Attribute:", f'<span class="attr-highlight">{attribute_name}</span>'),
|
|
704
|
+
("Attribute:", markup(f'<span class="attr-highlight">{escape(attribute_name)}</span>')),
|
|
647
705
|
]
|
|
648
706
|
else:
|
|
649
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
|