| A metapackage that installs all available Seequent Evo SDKs, including Jupyter notebook examples. |
| evo-sdk-common ([discovery](evo-python-sdk/evo-sdk-common/discovery) and [workspaces](evo-python-sdk/evo-sdk-common/workspaces)) | | A shared library that provides common functionality for integrating with Seequent's client SDKs. |
| [evo-files](evo-python-sdk/evo-files) | | A service client for interacting with the Evo File API. |
-| evo-objects | | A geoscience object service client library designed to help get up and running with the Geoscience Object API. |
+| [evo-objects](evo-python-sdk/evo-objects) | | A geoscience object service client library designed to help get up and running with the Geoscience Object API. |
| [evo-colormaps](evo-python-sdk/evo-colormaps) | | A service client to create colour mappings and associate them to geoscience data with the Colormap API.|
| [evo-blockmodels](evo-python-sdk/evo-blockmodels) | | The Block Model API provides the ability to manage and report on block models in your Evo workspaces. |
+| [evo-widgets](evo-python-sdk/evo-widgets) | | Widgets and presentation layer — rich HTML rendering of typed geoscience objects in Jupyter notebooks. |
| [evo-compute](evo-python-sdk/evo-compute) | | A service client to send jobs to the Compute Tasks API.|
### Getting started
@@ -57,7 +58,8 @@ For next steps and more information about using Evo, see:
* `evo-sdk-common` ([`discovery`](evo-python-sdk/evo-sdk-common/discovery) and [`workspaces`](evo-python-sdk/evo-sdk-common/workspaces)): providing the foundation for all Evo SDKs, as well as tools
for performing arbitrary Seequent Evo API requests
* [`evo-files`](evo-python-sdk/evo-files): for interacting with the File API
-* `evo-objects`: for interacting with the Geoscience Object API
+* [`evo-objects`](evo-python-sdk/evo-objects): for interacting with the Geoscience Object API
* [`evo-colormaps`](evo-python-sdk/evo-colormaps): for interacting with the Colormap API
* [`evo-blockmodels`](evo-python-sdk/evo-blockmodels): for interacting with the Block Model API
+* [`evo-widgets`](evo-python-sdk/evo-widgets): for rich HTML rendering of typed geoscience objects in Jupyter notebooks
* [`evo-compute`](evo-python-sdk/evo-compute): for interacting with the Compute Tasks API
diff --git a/mkdocs/docs/packages/evo-sdk-common/discovery.md b/mkdocs/docs/packages/evo-sdk-common/discovery/DiscoveryAPIClient.md
similarity index 100%
rename from mkdocs/docs/packages/evo-sdk-common/discovery.md
rename to mkdocs/docs/packages/evo-sdk-common/discovery/DiscoveryAPIClient.md
diff --git a/mkdocs/docs/packages/evo-sdk-common/workspaces.md b/mkdocs/docs/packages/evo-sdk-common/workspaces/WorkspaceAPIClient.md
similarity index 100%
rename from mkdocs/docs/packages/evo-sdk-common/workspaces.md
rename to mkdocs/docs/packages/evo-sdk-common/workspaces/WorkspaceAPIClient.md
diff --git a/mkdocs/docs/packages/evo-widgets/Introduction.md b/mkdocs/docs/packages/evo-widgets/Introduction.md
new file mode 100644
index 00000000..3b7f5fd1
--- /dev/null
+++ b/mkdocs/docs/packages/evo-widgets/Introduction.md
@@ -0,0 +1,57 @@
+# evo-widgets
+
+[GitHub source](https://github.com/SeequentEvo/evo-python-sdk/blob/main/packages/evo-widgets/src/evo/widgets/__init__.py)
+
+Widgets and presentation layer for the Evo Python SDK — HTML rendering, URL generation, and IPython formatters for Jupyter notebooks.
+
+## Usage
+
+Load the IPython extension in your notebook to enable rich HTML rendering for all Evo SDK typed objects:
+
+```python
+%load_ext evo.widgets
+```
+
+After loading, typed objects like `PointSet`, `Regular3DGrid`, `TensorGrid`, and `BlockModel` will automatically render with formatted metadata tables, clickable Portal/Viewer links, and bounding box information.
+
+## URL Functions
+
+Generate URLs to view objects in the Evo Portal and Viewer:
+
+```python
+from evo.widgets import (
+ get_portal_url_for_object,
+ get_viewer_url_for_object,
+ get_viewer_url_for_objects,
+)
+
+# Get Portal URL for a single object
+portal_url = get_portal_url_for_object(grid)
+
+# Get Viewer URL for a single object
+viewer_url = get_viewer_url_for_object(grid)
+
+# View multiple objects together in the Viewer
+url = get_viewer_url_for_objects(manager, [grid, pointset, tensor_grid])
+```
+
+## Formatters
+
+Rich HTML representations for all typed geoscience objects:
+
+- `PointSet`, `Regular3DGrid`, `TensorGrid`, `BlockModel`
+- `Variogram`
+- `Attributes` collections
+- `Report` and `ReportResult`
+- `TaskResult` and `TaskResults` (compute results)
+
+All formatters are registered automatically when you load the extension with `%load_ext evo.widgets`. They support light/dark mode via Jupyter theme CSS variables.
+
+## How It Works
+
+When you run `%load_ext evo.widgets`, the extension registers HTML formatters with IPython using `for_type_by_name`. This approach:
+
+1. **Avoids hard dependencies** — The widgets package doesn't import model classes directly
+2. **Works with all typed objects** — Formatters are registered for the base class, so all subclasses are covered
+3. **Lazy loading** — Formatters only activate when the relevant types are actually used
+
diff --git a/mkdocs/gen_api_docs.py b/mkdocs/gen_api_docs.py
index fdf3018c..ad07361f 100644
--- a/mkdocs/gen_api_docs.py
+++ b/mkdocs/gen_api_docs.py
@@ -18,26 +18,27 @@
log = logging.getLogger("mkdocs.gen_api_docs")
-def on_startup(command: str, dirty: bool) -> None:
- mkdocs_dir = Path(__file__).parent
- docs_packages_dir = mkdocs_dir / "docs" / "packages"
-
- api_clients_file = mkdocs_dir / "api_clients.txt"
- api_clients = [line.strip() for line in api_clients_file.read_text().splitlines() if line.strip()]
- log.info(f"Loaded {len(api_clients)} API clients from {api_clients_file.relative_to(mkdocs_dir)}")
-
- for old_md in docs_packages_dir.rglob("*.md"):
- if old_md.name != "evo-python-sdk.md":
- old_md.unlink()
- log.info(f"Deleted old doc: {old_md.relative_to(mkdocs_dir)}")
+def _parse_api_entries(lines: list[str]) -> dict[str, list[tuple[str, str, str, str]]]:
+ """Parse api_clients.txt lines into (class_name, module_path, github_url, namespace) grouped by doc_dir.
- entries_by_dir = defaultdict(list)
- for module_path in api_clients:
+ The doc_dir determines the directory/file structure:
+ - evo-sdk-common entries use ``/`` (e.g. ``evo-sdk-common/discovery``)
+ - All other entries use the package name (e.g. ``evo-objects``, ``evo-blockmodels``)
+ """
+ entries_by_dir: dict[str, list[tuple[str, str, str, str]]] = defaultdict(list)
+ for module_path in lines:
module_parts = module_path.split(".")
- _, package, _, _, sub_package, *rest = module_parts
- doc_dir = f"{package}/{sub_package}" if package == "evo-sdk-common" else package
+ # packages..src.<...>.
+ package = module_parts[1] # e.g. "evo-objects", "evo-sdk-common"
class_name = module_parts[-1]
+ if package == "evo-sdk-common":
+ # evo-sdk-common uses sub-package directories: evo-sdk-common/discovery, evo-sdk-common/workspaces
+ sub_package = module_parts[4] # packages.evo-sdk-common.src.evo..*
+ doc_dir = f"{package}/{sub_package}"
+ else:
+ doc_dir = package
+
file_path_parts = module_parts[:-1]
source_file_path = "/".join(file_path_parts) + ".py"
github_url = f"{GITHUB_BASE_URL}/{source_file_path}"
@@ -45,14 +46,89 @@ def on_startup(command: str, dirty: bool) -> None:
src_idx = module_parts.index("src")
namespace = ".".join(module_parts[src_idx + 1 :])
entries_by_dir[doc_dir].append((class_name, module_path, github_url, namespace))
+ return entries_by_dir
+
+
+def _parse_typed_entries(lines: list[str]) -> dict[str, list[tuple[str, str]]]:
+ """Parse typed_objects.txt lines into (class_name, namespace) grouped by package name.
+
+ All typed object entries for a package are collected into a single TypedObjects.md.
+ """
+ entries_by_package: dict[str, list[tuple[str, str]]] = defaultdict(list)
+ for module_path in lines:
+ module_parts = module_path.split(".")
+ package = module_parts[1] # e.g. "evo-objects", "evo-blockmodels"
+ class_name = module_parts[-1]
+
+ src_idx = module_parts.index("src")
+ namespace = ".".join(module_parts[src_idx + 1 :])
+ entries_by_package[package].append((class_name, namespace))
+ return entries_by_package
+
- for doc_dir, entries in entries_by_dir.items():
+def on_startup(command: str, dirty: bool) -> None:
+ mkdocs_dir = Path(__file__).parent
+ docs_packages_dir = mkdocs_dir / "docs" / "packages"
+
+ # --- Load API clients ---
+ api_clients_file = mkdocs_dir / "api_clients.txt"
+ api_clients = [line.strip() for line in api_clients_file.read_text().splitlines() if line.strip()]
+ log.info(f"Loaded {len(api_clients)} API clients from {api_clients_file.relative_to(mkdocs_dir)}")
+ api_entries = _parse_api_entries(api_clients)
+
+ # --- Load typed objects ---
+ typed_objects_file = mkdocs_dir / "typed_objects.txt"
+ typed_objects: list[str] = []
+ if typed_objects_file.exists():
+ typed_objects = [line.strip() for line in typed_objects_file.read_text().splitlines() if line.strip()]
+ log.info(f"Loaded {len(typed_objects)} typed objects from {typed_objects_file.relative_to(mkdocs_dir)}")
+ typed_entries = _parse_typed_entries(typed_objects)
+
+ # --- Compute all auto-generated paths ---
+ auto_generated_paths: set[Path] = set()
+
+ # API client docs: always placed inside package directories as .md
+ for doc_dir, entries in api_entries.items():
+ for class_name, *_ in entries:
+ doc_path = docs_packages_dir / f"{doc_dir}/{class_name}.md"
+ auto_generated_paths.add(doc_path.resolve())
+
+ # Typed object docs: one TypedObjects.md per package directory
+ for package in typed_entries:
+ doc_path = docs_packages_dir / f"{package}/TypedObjects.md"
+ auto_generated_paths.add(doc_path.resolve())
+
+ # --- Clean up only auto-generated files ---
+ for old_md in docs_packages_dir.rglob("*.md"):
+ if old_md.name == "evo-python-sdk.md":
+ continue
+ if old_md.resolve() in auto_generated_paths:
+ old_md.unlink()
+ log.info(f"Deleted auto-generated doc: {old_md.relative_to(mkdocs_dir)}")
+ else:
+ log.info(f"Preserved manual doc: {old_md.relative_to(mkdocs_dir)}")
+
+ # --- Generate API client docs ---
+ for doc_dir, entries in api_entries.items():
for class_name, module_path, github_url, namespace in entries:
- doc_path = (
- docs_packages_dir / f"{doc_dir}.md"
- if len(entries) == 1
- else docs_packages_dir / f"{doc_dir}/{class_name}.md"
- )
+ doc_path = docs_packages_dir / f"{doc_dir}/{class_name}.md"
doc_path.parent.mkdir(parents=True, exist_ok=True)
doc_path.write_text(f"[GitHub source]({github_url})\n::: {namespace}\n")
- log.info(f"Generated: {doc_path.relative_to(mkdocs_dir)}")
+ log.info(f"Generated API doc: {doc_path.relative_to(mkdocs_dir)}")
+
+ # --- Generate typed object docs ---
+ for package, entries in typed_entries.items():
+ doc_path = docs_packages_dir / f"{package}/TypedObjects.md"
+ doc_path.parent.mkdir(parents=True, exist_ok=True)
+
+ lines = ["# Typed Objects\n"]
+ for class_name, namespace in entries:
+ lines.append(f"::: {namespace}")
+ lines.append(" options:")
+ lines.append(" show_root_heading: true")
+ lines.append(" show_source: false")
+ lines.append("")
+
+ doc_path.write_text("\n".join(lines))
+ log.info(f"Generated typed objects doc: {doc_path.relative_to(mkdocs_dir)}")
+
diff --git a/mkdocs/site/packages/evo-blockmodels.html b/mkdocs/site/packages/evo-blockmodels/BlockModelAPIClient.html
similarity index 98%
rename from mkdocs/site/packages/evo-blockmodels.html
rename to mkdocs/site/packages/evo-blockmodels/BlockModelAPIClient.html
index c0819c40..8e2b2ab7 100644
--- a/mkdocs/site/packages/evo-blockmodels.html
+++ b/mkdocs/site/packages/evo-blockmodels/BlockModelAPIClient.html
@@ -7,17 +7,17 @@
-
- Evo blockmodels - Evo Python SDK
-
-
-
-
-
-
+
+ BlockModelAPIClient - Evo Python SDK
+
+
+
+
+
+
-
+
@@ -25,7 +25,7 @@