diff --git a/CHANGELOG.md b/CHANGELOG.md index df79c319..d0553afd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,52 @@ # Changelog +## evo-sdk@v0.2.0 +### What's Changed +#### evo-sdk +* **Typed object interactions** — Simplified interactions with a subset of Evo geoscience objects (Points, Grids, Block Models, Variograms) aimed at geologists and geostatisticians. Typed object interactions abstract API calls and make it easier to access data. +* **evo-widgets** — New `evo-widgets` package with rich HTML rendering of typed objects in Jupyter notebooks. Load via `%load_ext evo.widgets` for pretty-printed output with clickable links to Evo Portal and Viewer. +* **Kriging compute (preview)** — Support for running a preview version of Kriging estimation via `evo-compute`. +* Updated README with Quick start for notebooks, typed object examples, and links to [simplified object interactions](code-samples/geoscience-objects/simplified-object-interactions/) and [running kriging compute](code-samples/geoscience-objects/running-kriging-compute/) notebooks. + +**Full Changelog**: https://github.com/SeequentEvo/evo-python-sdk/compare/evo-sdk@v0.1.20...evo-sdk@v0.2.0 + +## evo-objects@v0.4.0 +### What's Changed +#### evo-objects +* New typed objects support for simplified interactions with geoscience objects (`PointSet`, grids, variograms). Typed objects abstract API calls and provide intuitive Python classes for data access, including `to_dataframe()`, automatic bounding box calculation, and rich HTML display. + +**Full Changelog**: https://github.com/SeequentEvo/evo-python-sdk/compare/evo-objects@v0.3.2...evo-objects@v0.4.0 + +## evo-blockmodels@v0.2.0 +### What's Changed +#### evo-blockmodels +* Typed object support for interactions with block models and block model reports. + +**Full Changelog**: https://github.com/SeequentEvo/evo-python-sdk/compare/evo-blockmodels@v0.1.0...evo-blockmodels@v0.2.0 + +## evo-widgets@v0.2.0 +### What's Changed +#### evo-widgets +* First published release of `evo-widgets` 🎉 +* Rich HTML rendering of typed geoscience objects in Jupyter notebooks. Load via `%load_ext evo.widgets`. +* Supports `PointSet`, `Regular3DGrid`, `TensorGrid`, `BlockModel`, and other typed objects inheriting from `_BaseObject`. +* URL generation for Evo Portal and Viewer links (`get_portal_url_for_object`, `get_viewer_url_for_object`, `get_viewer_url_for_objects`). +* Light/dark mode support via Jupyter theme CSS variables. + +## evo-compute@v0.0.2 +### What's Changed +#### evo-compute +* First preview compute task: Kriging estimation via Evo Compute. + +**Full Changelog**: https://github.com/SeequentEvo/evo-python-sdk/compare/evo-compute@v0.0.1rc3...evo-compute@v0.0.2 + +## evo-sdk-common@v0.5.19 +### What's Changed +#### evo-sdk-common +* Common typed object definitions moved to `evo-sdk-common` for shared use across packages. + +**Full Changelog**: https://github.com/SeequentEvo/evo-python-sdk/compare/evo-sdk-common@v0.5.18...evo-sdk-common@v0.5.19 + ## evo-files@v0.2.4 ## What's Changed diff --git a/README.md b/README.md index 6807f7e9..52b7ac4d 100644 --- a/README.md +++ b/README.md @@ -38,12 +38,81 @@ Before you get started, make sure you have: `evo-python-sdk` is designed for developers, data scientists, and technical users who want to work with Seequent Evo APIs and geoscience data. +This repository contains a number of sub-packages. You may choose to install the `evo-sdk` package, which includes all sub-packages and optional dependencies (e.g. Jupyter notebook support), or choose a specific package to install: + +| Package | Version | Import | Description | +| --- | --- | --- | --- | +| [evo-sdk](README.md) | PyPI - Version | | A metapackage that installs all available Seequent Evo SDKs, including Jupyter notebook examples. | +| [evo-sdk-common](packages/evo-sdk-common/README.md) | PyPI - Version | `evo.common`, `evo.notebooks` | A shared library that provides common functionality for integrating with Seequent's client SDKs. | +| [evo-files](packages/evo-files/README.md) | PyPI - Version | `evo.files` | A service client for interacting with the Evo File API. | +| [evo-objects](packages/evo-objects/README.md) | PyPI - Version | `evo.objects` | A geoscience object service client library designed to help get up and running with the Geoscience Object API. | +| [evo-colormaps](packages/evo-colormaps/README.md) | PyPI - Version | `evo.colormaps` | A service client to create colour mappings and associate them to geoscience data with the Colormap API.| +| [evo-blockmodels](packages/evo-blockmodels/README.md) | PyPI - Version | `evo.blockmodels` | The Block Model API provides the ability to manage and report on block models in your Evo workspaces. | +| [evo-widgets](packages/evo-widgets/README.md) | PyPI - Version | `evo.widgets` | Widgets and presentation layer — rich HTML rendering of typed geoscience objects in Jupyter notebooks. | +| [evo-compute](packages/evo-compute/README.md) | PyPI - Version | `evo.compute` | A service client to send jobs to the Compute Tasks API.| + * To quickly learn how to use Evo APIs, start with the [Getting started with Evo code samples](#getting-started-with-evo-code-samples) section, which contains practical, end-to-end Jupyter notebook examples for common workflows. Most new users should begin with this section. * If you are interested in the underlying SDKs or need to understand the implementation details, explore the [Getting started with Evo SDK development](#getting-started-with-evo-sdk-development) section, which contains the source code for each Evo SDK. * To learn about contributing to this repository, take a look at the [Contributing](#contributing) section. +## Quick start for notebooks + +Once you have an Evo app registered and the SDK installed, you can load and work with geoscience objects in just a few lines of code: + +```python +# Authenticate with Evo +from evo.notebooks import ServiceManagerWidget + +manager = await ServiceManagerWidget.with_auth_code( + client_id="", + cache_location="./notebook-data", +).login() +``` + +> **Output:** +> +> ![ServiceManagerWidget](docs/img/service-manager-widget.png) +> +> *A browser window opens for authentication. After login, select your organization, hub, and workspace from the dropdowns.* + +```python +# Enable rich HTML display for Evo objects in Jupyter +%load_ext evo.widgets + +# Load an object by file path or UUID +from evo.objects.typed import object_from_uuid, object_from_path + +obj = await object_from_path(manager, "") + +# OR + +obj = await object_from_uuid(manager, "") +obj # Displays object info with links to Evo Portal and Viewer +``` + +> **Output:** +> +> ![PointSet object display](docs/img/pointset-output.png) + +```python +# Get data as a pandas DataFrame +df = await obj.to_dataframe() +df.head() +``` + +> **Output:** +> | | x | y | z | Ag_ppm Values | +> |---|---|---|---|---| +> | 0 | 10584.40 | 100608.98 | 214.70 | 12.5 | +> | 1 | 10590.21 | 100615.43 | 220.15 | 8.3 | +> | ... | ... | ... | ... | ... | + +Typed objects like `PointSet`, `BlockModel`, and `Variogram` provide pretty-printed output in Jupyter with clickable links to view your data in Evo. As support for more geoscience objects is added, geologists and geostatisticians can interact with points, variograms, block models, grids, and more — all through intuitive Python classes. To determine the path or UUID of an object, visit the [Evo Portal](https://evo.seequent.com) or use the `ObjectSearchWidget`. + +For a hands-on introduction, see the [simplified object interactions](code-samples/geoscience-objects/simplified-object-interactions/) notebook. For a complete geostatistical workflow including variogram modelling and kriging estimation, see the [running kriging compute](code-samples/geoscience-objects/running-kriging-compute/) notebook. + ## Getting started with Evo code samples For detailed information about creating Evo apps, the authentication setup, available code samples, and step-by-step guides for working with the Jupyter notebooks, please refer to the [**code-samples/README.md**](code-samples/README.md) file. @@ -52,18 +121,6 @@ This comprehensive guide will walk you through everything required to get starte ## Getting started with Evo SDK development -This repository contains a number of sub-packages. You may choose to install the `evo-sdk` package, which includes all -sub-packages and optional dependencies (e.g. Jupyter notebook support), or choose a specific package to install: - -| Package | Version | Description | -| --- | --- | --- | -| [evo-sdk](README.md) | PyPI - Version | A metapackage that installs all available Seequent Evo SDKs, including Jupyter notebook examples. | -| [evo-sdk-common](packages/evo-sdk-common/README.md) | PyPI - Version | A shared library that provides common functionality for integrating with Seequent's client SDKs. | -| [evo-files](packages/evo-files/README.md) | PyPI - Version | A service client for interacting with the Evo File API. | -| [evo-objects](packages/evo-objects/README.md) | PyPI - Version | A geoscience object service client library designed to help get up and running with the Geoscience Object API. | -| [evo-colormaps](packages/evo-colormaps/README.md) | PyPI - Version | A service client to create colour mappings and associate them to geoscience data with the Colormap API.| -| [evo-blockmodels](packages/evo-blockmodels/README.md) | PyPI - Version | The Block Model API provides the ability to manage and report on block models in your Evo workspaces. | -| [evo-compute](packages/evo-compute/README.md) | PyPI - Version | A service client to send jobs to the Compute Tasks API.| ### Getting started diff --git a/code-samples/geoscience-objects/running-kriging-compute/README.md b/code-samples/geoscience-objects/running-kriging-compute/README.md index 103e01bd..d0527123 100644 --- a/code-samples/geoscience-objects/running-kriging-compute/README.md +++ b/code-samples/geoscience-objects/running-kriging-compute/README.md @@ -33,7 +33,7 @@ The variogram uses two nested spherical structures aligned with the dominant ori - **Long-range structure**: Contribution 0.51, ranges 250m × 180m × 100m - **Anisotropy**: Dip 70°, Azimuth 15° (NNE strike direction) -## WIP: Kriging Compute +## Kriging Compute The notebook includes work-in-progress sections demonstrating: - Creating a target `BlockModel` for estimation diff --git a/code-samples/geoscience-objects/running-kriging-compute/running-kriging-compute.ipynb b/code-samples/geoscience-objects/running-kriging-compute/running-kriging-compute.ipynb index 443e9681..64137142 100644 --- a/code-samples/geoscience-objects/running-kriging-compute/running-kriging-compute.ipynb +++ b/code-samples/geoscience-objects/running-kriging-compute/running-kriging-compute.ipynb @@ -32,10 +32,8 @@ ] }, { - "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], + "cell_type": "code", "source": [ "from evo.notebooks import ServiceManagerWidget\n", "\n", @@ -48,17 +46,19 @@ " redirect_url=redirect_url,\n", " cache_location=\"./notebook-data\",\n", ").login()" - ] + ], + "outputs": [], + "execution_count": null }, { - "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], + "cell_type": "code", "source": [ "# Load the widgets extension for rich HTML display\n", "%load_ext evo.widgets" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -74,9 +74,7 @@ }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "import pandas as pd\n", "\n", @@ -85,58 +83,75 @@ "df = pd.read_csv(input_file)\n", "\n", "print(f\"Loaded {len(df)} sample points from {df['Hole ID'].nunique()} downholes\")\n", + "\n", + "# Select only the columns we need and rename coordinates\n", + "df = df[[\"X\", \"Y\", \"Z\", \"CU_pct\"]].rename(columns={\"X\": \"x\", \"Y\": \"y\", \"Z\": \"z\"})\n", + "\n", + "# Remove rows with null values - compute tasks require non-null values\n", + "original_count = len(df)\n", + "df = df.dropna().reset_index(drop=True)\n", + "removed_count = original_count - len(df)\n", + "if removed_count > 0:\n", + " print(f\"\\nRemoved {removed_count} rows with null values\")\n", + "print(f\"Remaining: {len(df)} sample points\")\n", + "\n", + "# Verify no nulls remain\n", + "assert df.isna().sum().sum() == 0, \"DataFrame still contains null values!\"\n", + "\n", "print(\"\\nSpatial extent:\")\n", - "print(f\" X: {df['X'].min():.1f} to {df['X'].max():.1f} ({df['X'].max() - df['X'].min():.1f}m)\")\n", - "print(f\" Y: {df['Y'].min():.1f} to {df['Y'].max():.1f} ({df['Y'].max() - df['Y'].min():.1f}m)\")\n", - "print(f\" Z: {df['Z'].min():.1f} to {df['Z'].max():.1f} ({df['Z'].max() - df['Z'].min():.1f}m)\")\n", + "print(f\" X: {df['x'].min():.1f} to {df['x'].max():.1f} ({df['x'].max() - df['x'].min():.1f}m)\")\n", + "print(f\" Y: {df['y'].min():.1f} to {df['y'].max():.1f} ({df['y'].max() - df['y'].min():.1f}m)\")\n", + "print(f\" Z: {df['z'].min():.1f} to {df['z'].max():.1f} ({df['z'].max() - df['z'].min():.1f}m)\")\n", "print(\"\\nCopper (CU_pct) statistics:\")\n", "print(f\" Mean: {df['CU_pct'].mean():.3f}%, Variance: {df['CU_pct'].var():.3f}\")\n", "df.head()" - ] + ], + "outputs": [], + "execution_count": null }, { - "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], + "cell_type": "code", "source": [ "from evo.objects.typed import EpsgCode, PointSet, PointSetData\n", "\n", - "# Prepare the DataFrame with required column names (lowercase x, y, z)\n", - "locations_df = df.rename(columns={\"X\": \"x\", \"Y\": \"y\", \"Z\": \"z\"})\n", + "# Create the pointset with coordinates only first\n", + "coords_df = df[[\"x\", \"y\", \"z\", \"CU_pct\"]].copy()\n", + "print(f\"Points to upload: {len(coords_df)}\")\n", "\n", - "# Create the pointset data\n", "pointset_data = PointSetData(\n", " name=\"WP Drill Hole Assays\",\n", - " description=\"Copper and gold assay data from 55 downholes\",\n", - " locations=locations_df,\n", + " description=\"Copper assay data from 55 downholes\",\n", + " locations=coords_df,\n", " coordinate_reference_system=EpsgCode(32650), # UTM Zone 50N\n", ")\n", "\n", "# Create the pointset in Evo\n", "pointset = await PointSet.create(manager, pointset_data)\n", "print(f\"Created pointset with {pointset.num_points} points\")" - ] + ], + "outputs": [], + "execution_count": null }, { - "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], + "cell_type": "code", "source": [ "# Display the pointset with rich HTML formatting\n", "pointset" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# View available attributes\n", "pointset.attributes" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -167,10 +182,8 @@ ] }, { - "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], + "cell_type": "code", "source": [ "from evo.objects.typed import (\n", " Ellipsoid,\n", @@ -230,17 +243,19 @@ "# Create the variogram object in Evo\n", "variogram = await Variogram.create(manager, variogram_data)\n", "print(f\"Created variogram: {variogram.name}\")" - ] + ], + "outputs": [], + "execution_count": null }, { - "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], + "cell_type": "code", "source": [ "# Display the variogram with rich HTML formatting\n", "variogram" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -251,9 +266,7 @@ }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "print(f\"Variogram: {variogram.name}\")\n", "print(f\"Sill: {variogram.sill}\")\n", @@ -272,7 +285,9 @@ " print(\n", " f\" Ranges: major={ranges.get('major')}m, semi_major={ranges.get('semi_major')}m, minor={ranges.get('minor')}m\"\n", " )" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -285,9 +300,7 @@ }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# Get variogram curves for the three principal directions\n", "major, semi_major, minor = variogram.get_principal_directions()\n", @@ -296,13 +309,13 @@ "print(f\"Semi-major direction: range={semi_major.range_value}m\")\n", "print(f\"Minor direction: range={minor.range_value}m\")\n", "print(f\"Points per curve: {len(major.distance)}\")" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "import plotly.graph_objects as go\n", "\n", @@ -362,7 +375,9 @@ ")\n", "\n", "fig.show()" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -377,9 +392,7 @@ }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# Get the ellipsoid from the variogram (uses structure with largest volume by default)\n", "var_ellipsoid = variogram.get_ellipsoid()\n", @@ -393,7 +406,9 @@ "search_ellipsoid = var_ellipsoid.scaled(2.0)\n", "\n", "print(f\"\\nSearch ellipsoid (2x): major={search_ellipsoid.ranges.major}m\")" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -409,9 +424,7 @@ }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# Get pointset data for visualization\n", "points_df = await pointset.to_dataframe()\n", @@ -423,13 +436,13 @@ " points_df[\"z\"].mean(),\n", ")\n", "print(f\"Data centroid: ({center[0]:.1f}, {center[1]:.1f}, {center[2]:.1f})\")" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# Generate surface mesh points for visualization\n", "vx, vy, vz = var_ellipsoid.surface_points(center=center, n_points=25)\n", @@ -509,7 +522,9 @@ ")\n", "\n", "fig.show()" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -522,16 +537,16 @@ }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "from evo.widgets import get_viewer_url_for_objects\n", "\n", "# Generate a viewer URL to see both objects together\n", "viewer_url = get_viewer_url_for_objects(manager, [pointset, variogram])\n", "print(f\"View in Evo Viewer: {viewer_url}\")" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -545,12 +560,11 @@ ] }, { - "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], + "cell_type": "code", "source": [ - "from evo.blockmodels import Point3, RegularBlockModel, RegularBlockModelData, Size3d, Size3i, Units\n", + "from evo.blockmodels.typed import Units\n", + "from evo.objects.typed import BlockModel, Point3, RegularBlockModelData, Size3d, Size3i\n", "\n", "# Define block model covering the drill hole extent\n", "bm_data = RegularBlockModelData(\n", @@ -563,157 +577,211 @@ " size_unit_id=Units.METRES,\n", ")\n", "\n", - "block_model = await RegularBlockModel.create(manager, bm_data)\n", + "block_model = await BlockModel.create_regular(manager, bm_data)\n", "print(f\"Created Block Model: {block_model.name}\")\n", - "print(f\"Block Model ID: {block_model.id}\")" - ] + "print(f\"Block Model UUID: {block_model.block_model_uuid}\")" + ], + "outputs": [], + "execution_count": null }, { - "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], + "cell_type": "code", "source": [ "# Display the block model metadata\n", - "block_model.version" - ] + "block_model" + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## WIP. Define Kriging Parameters\n", + "## 9. Define Kriging Parameters\n", "\n", "Configure the kriging search neighborhood and estimation parameters." ] }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ - "# from evo.compute.tasks import SearchNeighborhood\n", - "# from evo.compute.tasks.kriging import KrigingParameters\n", - "#\n", - "# # Use the search ellipsoid we created earlier (2x variogram range)\n", - "# params = KrigingParameters(\n", - "# source=pointset.attributes[\"CU_pct\"], # Source attribute\n", - "# target=block_model.attributes[f\"CU_samples_{max_samples}\"]\n", - "# variogram=variogram,\n", - "# search=SearchNeighborhood(\n", - "# ellipsoid=search_ellipsoid,\n", - "# max_samples=16, # Maximum samples per estimate\n", - "# min_samples=4, # Minimum samples required\n", - "# ),\n", - "# )\n", - "#\n", - "# print(f\"Kriging source: CU_pct from pointset\")\n", - "# print(f\"Search ellipsoid: major={search_ellipsoid.ranges.major}m\")" - ] + "from evo.compute.tasks import SearchNeighborhood\n", + "from evo.compute.tasks.kriging import KrigingParameters\n", + "\n", + "# Use the search ellipsoid we created earlier (2x variogram range)\n", + "params = KrigingParameters(\n", + " source=pointset.attributes[\"CU_pct\"], # Source attribute\n", + " target=block_model.attributes[\"CU_estimate\"], # Target attribute on block model\n", + " variogram=variogram,\n", + " search=SearchNeighborhood(\n", + " ellipsoid=search_ellipsoid,\n", + " max_samples=16, # Maximum samples per estimate\n", + " min_samples=4, # Minimum samples required\n", + " ),\n", + ")\n", + "\n", + "print(\"Kriging source: CU_pct from pointset\")\n", + "print(f\"Search ellipsoid: major={search_ellipsoid.ranges.major}m\")" + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## WIP. Run Kriging Task\n", + "## 10. Run Kriging Task\n", "\n", "Submit and run the kriging task using Evo Compute." ] }, { "cell_type": "code", - "execution_count": null, "metadata": {}, + "source": [ + "from evo.compute.tasks import run\n", + "\n", + "# Submit kriging task (progress feedback is shown by default)\n", + "print(\"Submitting kriging task...\")\n", + "result = await run(manager, params, preview=True)\n", + "\n", + "print(\"Kriging complete!\")\n", + "print(f\"Result: {result.message}\")" + ], "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "metadata": {}, "source": [ - "# from evo.compute.tasks import run\n", - "#\n", - "# # Submit kriging task (progress feedback is shown by default)\n", - "# print(\"Submitting kriging task...\")\n", - "# results = await run(manager, [params])\n", - "#\n", - "# print(f\"Kriging complete!\")\n", - "# print(f\"Result: {results[0].status}\")" - ] + "# Display the kriging result (pretty-printed in Jupyter)\n", + "result" + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## WIP. View Kriging Results\n", + "## 11. View Kriging Results\n", "\n", "Refresh the block model and view the estimated grades." ] }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# Refresh block model to see new attributes\n", - "await block_model.refresh()\n", + "block_model = await block_model.refresh()\n", "\n", - "# Display the block model version (shows updated columns)\n", - "block_model.version" - ] + "# Display the block model (pretty-printed in Jupyter)\n", + "block_model" + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, + "source": [ + "# View the block model attributes\n", + "block_model.attributes" + ], "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "metadata": {}, "source": [ "# Get the kriged values as a DataFrame\n", - "results_df = block_model.cell_data\n", + "results_df = await block_model.to_dataframe(columns=[\"CU_estimate\"])\n", "\n", "print(f\"Estimated {len(results_df)} blocks\")\n", "print(\"\\nStatistics for CU_estimate:\")\n", "print(results_df[\"CU_estimate\"].describe())" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## WIP. Running Multiple Kriging Scenarios\n", + "## 12. Running Multiple Kriging Scenarios\n", "\n", "Run multiple kriging tasks concurrently to compare different parameters." ] }, { "cell_type": "code", - "execution_count": null, "metadata": {}, + "source": [ + "max_samples_values = [5, 10, 15, 20]\n", + "\n", + "# Create parameter sets for each scenario\n", + "parameter_sets = []\n", + "for max_samples in max_samples_values:\n", + " params = KrigingParameters(\n", + " source=pointset.attributes[\"CU_pct\"],\n", + " target=block_model.attributes[f\"CU_samples_{max_samples}\"],\n", + " variogram=variogram,\n", + " search=SearchNeighborhood(\n", + " ellipsoid=search_ellipsoid,\n", + " max_samples=max_samples,\n", + " ),\n", + " )\n", + " parameter_sets.append(params)\n", + " print(f\"Prepared scenario with max_samples={max_samples}\")\n", + "\n", + "print(f\"\\nCreated {len(parameter_sets)} parameter sets\")" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# Run all scenarios in parallel\n", + "print(f\"Submitting {len(parameter_sets)} kriging tasks...\")\n", + "results = await run(manager, parameter_sets, preview=True)\n", + "\n", + "print(f\"\\nAll {len(results)} scenarios completed!\")" + ], "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "metadata": {}, "source": [ - "# max_samples_values = [5, 10, 15, 20]\n", - "#\n", - "# # Create parameter sets for each scenario\n", - "# parameter_sets = []\n", - "# for max_samples in max_samples_values:\n", - "# params = KrigingParameters(\n", - "# source=pointset.attributes[\"CU_pct\"],\n", - "# target=block_model.attributes[f\"CU_samples_{max_samples}\"],\n", - "# variogram=variogram,\n", - "# search=SearchNeighborhood(\n", - "# ellipsoid=search_ellipsoid,\n", - "# max_samples=max_samples,\n", - "# ),\n", - "# )\n", - "# parameter_sets.append(params)\n", - "#\n", - "# # Run all scenarios in parallel\n", - "# print(f\"Submitting {len(parameter_sets)} kriging tasks...\")\n", - "# results = await run(manager, parameter_sets)\n", - "# print(f\"All {len(results)} scenarios completed!\")\n" - ] + "# Display the results (pretty-printed in Jupyter)\n", + "results" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# Refresh and view block model with all new attributes\n", + "block_model = await block_model.refresh()\n", + "block_model" + ], + "outputs": [], + "execution_count": null } ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -727,7 +795,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.0" + "version": "3.10.19" } }, "nbformat": 4, diff --git a/docs/img/pointset-output.png b/docs/img/pointset-output.png new file mode 100644 index 00000000..2e430d8f Binary files /dev/null and b/docs/img/pointset-output.png differ diff --git a/docs/img/service-manager-widget.png b/docs/img/service-manager-widget.png new file mode 100644 index 00000000..8230ea8a Binary files /dev/null and b/docs/img/service-manager-widget.png differ diff --git a/mkdocs/docs/packages/evo-blockmodels.md b/mkdocs/docs/packages/evo-blockmodels/BlockModelAPIClient.md similarity index 100% rename from mkdocs/docs/packages/evo-blockmodels.md rename to mkdocs/docs/packages/evo-blockmodels/BlockModelAPIClient.md diff --git a/mkdocs/docs/packages/evo-blockmodels/Introduction.md b/mkdocs/docs/packages/evo-blockmodels/Introduction.md new file mode 100644 index 00000000..573d1483 --- /dev/null +++ b/mkdocs/docs/packages/evo-blockmodels/Introduction.md @@ -0,0 +1,88 @@ +# evo-blockmodels + +[GitHub source](https://github.com/SeequentEvo/evo-python-sdk/blob/main/packages/evo-blockmodels/src/evo/blockmodels/) + +The `evo-blockmodels` package provides both a low-level API client and typed Python classes for working with block models in Evo. + +!!! tip "Using block models from typed objects" + The full functionality of `evo-blockmodels` — creating, retrieving, updating attributes, running reports — is accessible directly from the [`BlockModel`](../evo-objects/Introduction.md#blockmodel-via-evo-blockmodels) object in `evo.objects.typed`. When `evo-blockmodels` is installed, `BlockModel` acts as a proxy and delegates data operations to the Block Model Service automatically. + + ```python + from evo.objects.typed import object_from_path + + # Load any block model — full evo-blockmodels functionality is available + bm = await object_from_path(manager, "my-folder/block-model") + df = await bm.to_dataframe() + await bm.add_attribute(data_df, "new_col") + report = await bm.create_report(spec) + ``` + + See the [evo-objects Introduction](../evo-objects/Introduction.md#blockmodel-via-evo-blockmodels) for the full API. + +See the [Typed Objects](TypedObjects.md) page for the full typed API reference. + +## Typed Block Models + +The typed module provides intuitive classes for creating, retrieving, and updating regular block models with pandas DataFrame support. + +### Creating a block model + +```python +from evo.blockmodels.typed import RegularBlockModel, RegularBlockModelData, Point3, Size3d, Size3i + +data = RegularBlockModelData( + name="My Block Model", + origin=Point3(0, 0, 0), + n_blocks=Size3i(10, 10, 10), + block_size=Size3d(1.0, 1.0, 1.0), + cell_data=my_dataframe, +) +block_model = await RegularBlockModel.create(context, data) +``` + +### Retrieving a block model + +```python +block_model = await RegularBlockModel.get(context, block_model_id) +df = block_model.cell_data # pandas DataFrame with all cell attributes +``` + +### Updating attributes + +```python +new_version = await block_model.update_attributes( + updated_dataframe, + new_columns=["new_col"], +) +``` + +## Reports + +Reports provide resource estimation summaries for block models — calculating tonnages, grades, and metal content grouped by category (e.g., geological domains, rock types). + +### Creating and running a report + +```python +from evo.blockmodels.typed import ( + Report, ReportSpecificationData, ReportColumnSpec, ReportCategorySpec, + Aggregation, MassUnits, Units, +) + +spec = ReportSpecificationData( + name="Grade Report", + category=ReportCategorySpec(column_name="domain"), + columns=[ + ReportColumnSpec( + column_name="Au", + aggregation=Aggregation.MASS_AVERAGE, + output_unit_id="g/t", + ), + ], + mass_units=MassUnits.TONNES, +) + +report = await Report.create(context, block_model, spec) +result = await report.run(context) +df = result.to_dataframe() # Tonnages and grades by domain +``` + diff --git a/mkdocs/docs/packages/evo-blockmodels/TypedObjects.md b/mkdocs/docs/packages/evo-blockmodels/TypedObjects.md new file mode 100644 index 00000000..b1ab0a0e --- /dev/null +++ b/mkdocs/docs/packages/evo-blockmodels/TypedObjects.md @@ -0,0 +1,51 @@ +# Typed Objects + +::: evo.blockmodels.typed.regular_block_model.RegularBlockModel + options: + show_root_heading: true + show_source: false + +::: evo.blockmodels.typed.regular_block_model.RegularBlockModelData + options: + show_root_heading: true + show_source: false + +::: evo.blockmodels.typed.report.Report + options: + show_root_heading: true + show_source: false + +::: evo.blockmodels.typed.report.ReportSpecificationData + options: + show_root_heading: true + show_source: false + +::: evo.blockmodels.typed.report.ReportResult + options: + show_root_heading: true + show_source: false + +::: evo.blockmodels.typed.report.ReportColumnSpec + options: + show_root_heading: true + show_source: false + +::: evo.blockmodels.typed.report.ReportCategorySpec + options: + show_root_heading: true + show_source: false + +::: evo.blockmodels.typed.report.Aggregation + options: + show_root_heading: true + show_source: false + +::: evo.blockmodels.typed.units.Units + options: + show_root_heading: true + show_source: false + +::: evo.blockmodels.typed.units.UnitInfo + options: + show_root_heading: true + show_source: false diff --git a/mkdocs/docs/packages/evo-colormaps.md b/mkdocs/docs/packages/evo-colormaps/ColormapAPIClient.md similarity index 100% rename from mkdocs/docs/packages/evo-colormaps.md rename to mkdocs/docs/packages/evo-colormaps/ColormapAPIClient.md diff --git a/mkdocs/docs/packages/evo-compute/Introduction.md b/mkdocs/docs/packages/evo-compute/Introduction.md new file mode 100644 index 00000000..90b2a215 --- /dev/null +++ b/mkdocs/docs/packages/evo-compute/Introduction.md @@ -0,0 +1,142 @@ +# evo-compute + +[GitHub source](https://github.com/SeequentEvo/evo-python-sdk/blob/main/packages/evo-compute/src/evo/compute/) + +The `evo-compute` package provides a client for running compute tasks on Evo. Tasks are submitted to the Compute Tasks API and polled for results. + +See the [Typed Objects](TypedObjects.md) page for the full typed API reference. + +## Running Compute Tasks + +The `run()` function is the main entry point for executing compute tasks. It supports running a single task or multiple tasks concurrently. + +### Single task + +```python +from evo.compute.tasks import run, SearchNeighborhood, Ellipsoid, EllipsoidRanges +from evo.compute.tasks.kriging import KrigingParameters + +params = KrigingParameters( + source=pointset.attributes["grade"], + target=block_model.attributes["kriged_grade"], # Creates if new, updates if exists + variogram=variogram, + search=SearchNeighborhood( + ellipsoid=Ellipsoid(ranges=EllipsoidRanges(200, 150, 100)), + max_samples=20, + ), +) +result = await run(manager, params, preview=True) +``` + +### Multiple tasks + +Run multiple kriging tasks concurrently — for example, estimating different attributes or using different parameters: + +```python +from evo.compute.tasks import run, SearchNeighborhood +from evo.compute.tasks.kriging import KrigingParameters + +results = await run(manager, [ + KrigingParameters( + source=pointset.attributes["Au"], + target=block_model.attributes["Au_kriged"], + variogram=au_variogram, + search=SearchNeighborhood(...), + ), + KrigingParameters( + source=pointset.attributes["Cu"], + target=block_model.attributes["Cu_kriged"], + variogram=cu_variogram, + search=SearchNeighborhood(...), + ), +], preview=True) + +results[0] # First kriging result +results[1] # Second kriging result +``` + +### Working with results + +Task results provide convenient methods to access the output: + +```python +# Pretty-print the result +result # Shows ✓ Kriging Result with target and attribute info + +# Get the target object +target = await result.get_target_object() + +# Get data as a DataFrame +df = await result.to_dataframe() +``` + +For complete examples, see the [kriging notebook](https://github.com/SeequentEvo/evo-python-sdk/blob/main/code-samples/geoscience-objects/running-kriging-compute/running-kriging-compute.ipynb) and the [multiple kriging notebook](https://github.com/SeequentEvo/evo-python-sdk/blob/main/packages/evo-compute/docs/examples/kriging_multiple.ipynb). + +## FAQ + +### How do I run parallel tasks that update the same attribute? + +You can set the target for a compute task using `block_model.attributes["name"]`. If the attribute does not yet exist, it will be **created**; if it already exists, it will be **updated**. This is determined by the local state of the object. + +When the first task creates a new attribute on the server, your **local** object doesn't know about it yet. If you then try to run more tasks targeting the same attribute name, the local object still thinks it doesn't exist and will try to create it again — causing a conflict. + +To avoid this: + +1. Run the **first** task to create the attribute. +2. **Refresh** the local object so it sees the newly created attribute. +3. Run the **remaining** tasks — now `block_model.attributes["kriged_grade"]` resolves to the existing attribute and will update it. + +```python +from evo.compute.tasks import run, SearchNeighborhood +from evo.compute.tasks.kriging import KrigingParameters, RegionFilter + +# Step 1: Run the first task — attribute "kriged_grade" does not exist yet, so it is created +first_result = await run(manager, KrigingParameters( + source=pointset.attributes["grade"], + target=block_model.attributes["kriged_grade"], + variogram=variogram, + search=SearchNeighborhood(...), + target_region_filter=RegionFilter( + attribute=block_model.attributes["domain"], + names=["LMS1"], + ), +), preview=True) + +# Step 2: Refresh so the local object recognises the newly created attribute +block_model = await block_model.refresh() + +# Step 3: Now "kriged_grade" exists locally — remaining tasks will update it +results = await run(manager, [ + KrigingParameters( + source=pointset.attributes["grade"], + target=block_model.attributes["kriged_grade"], # Exists → update + variogram=variogram, + search=SearchNeighborhood(...), + target_region_filter=RegionFilter( + attribute=block_model.attributes["domain"], + names=["LMS2"], + ), + ), + KrigingParameters( + source=pointset.attributes["grade"], + target=block_model.attributes["kriged_grade"], # Exists → update + variogram=variogram, + search=SearchNeighborhood(...), + target_region_filter=RegionFilter( + attribute=block_model.attributes["domain"], + names=["LMS3"], + ), + ), +], preview=True) +``` + +!!! tip + If each task writes to a **different** attribute name, they can all run in parallel without refreshing — the compute service handles concurrent attribute creation on the same target object. See the [multiple kriging notebook](https://github.com/SeequentEvo/evo-python-sdk/blob/main/packages/evo-compute/docs/examples/kriging_multiple.ipynb) for an example. + +!!! note "Preview APIs" + Kriging and other compute tasks are currently preview features. You must pass `preview=True` when calling `run()`. + Preview APIs may change between releases. For more details, see: + + - [Preview APIs](https://developer.seequent.com/docs/api/fundamentals/preview-apis) — how to opt in and what to expect + - [API Lifecycle](https://developer.seequent.com/docs/api/fundamentals/lifecycle) — how Evo APIs evolve from preview to stable + diff --git a/mkdocs/docs/packages/evo-compute.md b/mkdocs/docs/packages/evo-compute/JobClient.md similarity index 100% rename from mkdocs/docs/packages/evo-compute.md rename to mkdocs/docs/packages/evo-compute/JobClient.md diff --git a/mkdocs/docs/packages/evo-compute/TypedObjects.md b/mkdocs/docs/packages/evo-compute/TypedObjects.md new file mode 100644 index 00000000..06e94727 --- /dev/null +++ b/mkdocs/docs/packages/evo-compute/TypedObjects.md @@ -0,0 +1,56 @@ +# Typed Objects + +::: evo.compute.tasks.kriging.KrigingParameters + options: + show_root_heading: true + show_source: false + +::: evo.compute.tasks.kriging.SimpleKriging + options: + show_root_heading: true + show_source: false + +::: evo.compute.tasks.kriging.OrdinaryKriging + options: + show_root_heading: true + show_source: false + +::: evo.compute.tasks.kriging.BlockDiscretisation + options: + show_root_heading: true + show_source: false + +::: evo.compute.tasks.kriging.RegionFilter + options: + show_root_heading: true + show_source: false + +::: evo.compute.tasks.kriging.KrigingResult + options: + show_root_heading: true + show_source: false + +::: evo.compute.tasks.common.results.TaskResult + options: + show_root_heading: true + show_source: false + +::: evo.compute.tasks.common.results.TaskResults + options: + show_root_heading: true + show_source: false + +::: evo.compute.tasks.common.search.SearchNeighborhood + options: + show_root_heading: true + show_source: false + +::: evo.compute.tasks.common.source_target.Source + options: + show_root_heading: true + show_source: false + +::: evo.compute.tasks.common.source_target.Target + options: + show_root_heading: true + show_source: false diff --git a/mkdocs/docs/packages/evo-files.md b/mkdocs/docs/packages/evo-files/FileAPIClient.md similarity index 100% rename from mkdocs/docs/packages/evo-files.md rename to mkdocs/docs/packages/evo-files/FileAPIClient.md diff --git a/mkdocs/docs/packages/evo-objects/Introduction.md b/mkdocs/docs/packages/evo-objects/Introduction.md new file mode 100644 index 00000000..39556b13 --- /dev/null +++ b/mkdocs/docs/packages/evo-objects/Introduction.md @@ -0,0 +1,78 @@ +# evo-objects + +[GitHub source](https://github.com/SeequentEvo/evo-python-sdk/blob/main/packages/evo-objects/src/evo/objects/) + +The `evo-objects` package provides both a low-level API client and typed Python classes for working with geoscience objects in Evo. + +## Typed Objects + +The typed objects module provides intuitive Python classes for working with Evo geoscience objects. Instead of dealing with raw API responses, you work with `PointSet`, `Regular3DGrid`, `Variogram`, and other domain-specific types that provide: + +- Simple property access (e.g., `pointset.num_points`, `grid.bounding_box`) +- `to_dataframe()` for getting data as pandas DataFrames +- Rich HTML display in Jupyter notebooks (via `%load_ext evo.widgets`) +- Clickable links to Evo Portal and Viewer + +See the [Typed Objects](TypedObjects.md) page for the full API reference. + +### Loading objects + +Three convenience functions let you load any typed object by reference, path, or UUID: + +```python +from evo.objects.typed import object_from_path, object_from_uuid, object_from_reference + +# By file path in the workspace +obj = await object_from_path(manager, "my-folder/assay-data") + +# By UUID +obj = await object_from_uuid(manager, "b208a6c9-6881-4b97-b02d-acb5d81299bb") + +# By full object reference URL +obj = await object_from_reference(manager, reference_url) +``` + +The correct typed class (`PointSet`, `Regular3DGrid`, etc.) is selected automatically based on the object's schema. + +### BlockModel (via evo-blockmodels) + +The `BlockModel` type is a geoscience object that acts as a proxy to the Block Model Service. When `evo-blockmodels` is installed (`pip install evo-objects[blockmodels]`), the full range of block model operations is available directly on the `BlockModel` object — no need to use the low-level `BlockModelAPIClient`. + +```python +from evo.objects.typed import BlockModel, RegularBlockModelData, Point3, Size3d, Size3i + +data = RegularBlockModelData( + name="My Block Model", + origin=Point3(x=0, y=0, z=0), + n_blocks=Size3i(nx=10, ny=10, nz=5), + block_size=Size3d(dx=2.5, dy=5.0, dz=5.0), + cell_data=my_dataframe, +) +bm = await BlockModel.create_regular(manager, data) +``` + +```python +# Load an existing block model +bm = await object_from_path(manager, "my-folder/block-model") + +# Get data as a DataFrame +df = await bm.to_dataframe() + +# Add a new attribute +await bm.add_attribute(data_df, "new_attribute", unit="g/t") + +# Create and run a report +report = await bm.create_report(spec) +result = await report.run(manager) +df = result.to_dataframe() +``` + +After a compute task (e.g., kriging) adds attributes on the server, call `refresh()` to update the local object: + +```python +bm = await bm.refresh() +bm.attributes # Now shows newly added attributes +``` + +For the full `evo-blockmodels` typed API (RegularBlockModel, Reports, Units), see the [evo-blockmodels documentation](../evo-blockmodels/Introduction.md). + diff --git a/mkdocs/docs/packages/evo-objects/TypedObjects.md b/mkdocs/docs/packages/evo-objects/TypedObjects.md new file mode 100644 index 00000000..d0fa7d0f --- /dev/null +++ b/mkdocs/docs/packages/evo-objects/TypedObjects.md @@ -0,0 +1,86 @@ +# Typed Objects + +::: evo.objects.typed.base.object_from_path + options: + show_root_heading: true + show_source: false + +::: evo.objects.typed.base.object_from_uuid + options: + show_root_heading: true + show_source: false + +::: evo.objects.typed.base.object_from_reference + options: + show_root_heading: true + show_source: false + +::: evo.objects.typed.pointset.PointSet + options: + show_root_heading: true + show_source: false + +::: evo.objects.typed.pointset.PointSetData + options: + show_root_heading: true + show_source: false + +::: evo.objects.typed.regular_grid.Regular3DGrid + options: + show_root_heading: true + show_source: false + +::: evo.objects.typed.regular_grid.Regular3DGridData + options: + show_root_heading: true + show_source: false + +::: evo.objects.typed.regular_masked_grid.RegularMasked3DGrid + options: + show_root_heading: true + show_source: false + +::: evo.objects.typed.regular_masked_grid.RegularMasked3DGridData + options: + show_root_heading: true + show_source: false + +::: evo.objects.typed.tensor_grid.Tensor3DGrid + options: + show_root_heading: true + show_source: false + +::: evo.objects.typed.tensor_grid.Tensor3DGridData + options: + show_root_heading: true + show_source: false + +::: evo.objects.typed.variogram.Variogram + options: + show_root_heading: true + show_source: false + +::: evo.objects.typed.variogram.VariogramData + options: + show_root_heading: true + show_source: false + +::: evo.objects.typed.block_model_ref.BlockModel + options: + show_root_heading: true + show_source: false + +::: evo.objects.typed.attributes.Attributes + options: + show_root_heading: true + show_source: false + +::: evo.objects.typed.attributes.Attribute + options: + show_root_heading: true + show_source: false + +::: evo.objects.typed.types.BoundingBox + options: + show_root_heading: true + show_source: false diff --git a/mkdocs/docs/packages/evo-python-sdk.md b/mkdocs/docs/packages/evo-python-sdk.md index d7ad2885..25f50415 100644 --- a/mkdocs/docs/packages/evo-python-sdk.md +++ b/mkdocs/docs/packages/evo-python-sdk.md @@ -2,6 +2,9 @@ [GitHub repository](https://github.com/SeequentEvo/evo-python-sdk) +`evo-python-sdk` is designed for developers, data scientists, geologists, and geostatisticians who want to work with Seequent Evo APIs and geoscience data. + + ## Getting started with Evo code samples For detailed information about creating Evo apps, the authentication setup, available code samples, and step-by-step guides for working with the Jupyter notebooks, please refer to the [**Quick start guide**](https://developer.seequent.com/docs/guides/getting-started/quick-start-guide), or [**code-samples**](https://github.com/SeequentEvo/evo-python-sdk/tree/main/code-samples) section of the repository. @@ -16,14 +19,55 @@ sub-packages and optional dependencies (e.g. Jupyter notebook support), or choos | Package | Version | Description | | --- | --- | --- | | evo-sdk | PyPI - Version | 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)) | PyPI - Version | A shared library that provides common functionality for integrating with Seequent's client SDKs. | -| [evo-files](evo-python-sdk/evo-files) | PyPI - Version | A service client for interacting with the Evo File API. | -| evo-objects | PyPI - Version | 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) | PyPI - Version | A service client to create colour mappings and associate them to geoscience data with the Colormap API.| -| [evo-blockmodels](evo-python-sdk/evo-blockmodels) | PyPI - Version | The Block Model API provides the ability to manage and report on block models in your Evo workspaces. | -| [evo-compute](evo-python-sdk/evo-compute) | PyPI - Version | A service client to send jobs to the Compute Tasks API.| +| evo-sdk-common ([discovery](evo-sdk-common/discovery/DiscoveryAPIClient.md) and [workspaces](evo-sdk-common/workspaces/WorkspaceAPIClient.md)) | PyPI - Version | A shared library that provides common functionality for integrating with Seequent's client SDKs. | +| evo-files ([api](evo-files/FileAPIClient.md)) | PyPI - Version | A service client for interacting with the Evo File API. | +| evo-objects ([introduction](evo-objects/Introduction.md), [typed objects](evo-objects/TypedObjects.md), [api](evo-objects/ObjectAPIClient.md)) | PyPI - Version | Typed Python classes and an API client for geoscience objects — points, grids, variograms, and more. | +| evo-colormaps ([api](evo-colormaps/ColormapAPIClient.md)) | PyPI - Version | A service client to create colour mappings and associate them to geoscience data with the Colormap API.| +| evo-blockmodels ([introduction](evo-blockmodels/Introduction.md), [typed objects](evo-blockmodels/TypedObjects.md), [api](evo-blockmodels/BlockModelAPIClient.md)) | PyPI - Version | Typed block model interactions, reports, and an API client for managing block models in Evo. | +| evo-widgets ([introduction](evo-widgets/Introduction.md)) | PyPI - Version | Widgets and presentation layer — rich HTML rendering of typed geoscience objects in Jupyter notebooks. | +| evo-compute ([introduction](evo-compute/Introduction.md), [typed objects](evo-compute/TypedObjects.md), [api](evo-compute/JobClient.md)) | PyPI - Version | Run compute tasks (e.g. kriging estimation) via the Compute Tasks API.| + +### Quick start for notebooks + +Once you have an Evo app registered and the SDK installed, you can load and work with geoscience objects in just a few lines of code: + +```python +# Authenticate with Evo +from evo.notebooks import ServiceManagerWidget + +manager = await ServiceManagerWidget.with_auth_code( + client_id="", + cache_location="./notebook-data", +).login() +``` + +```python +# Enable rich HTML display for Evo objects in Jupyter +%load_ext evo.widgets + +# Load an object by file path or UUID +from evo.objects.typed import object_from_uuid, object_from_path + +obj = await object_from_path(manager, "") -### Getting started +# OR + +obj = await object_from_uuid(manager, "") +obj # Displays object info with links to Evo Portal and Viewer +``` + +```python +# Get data as a pandas DataFrame +df = await obj.to_dataframe() +df.head() +``` + +Typed objects like `PointSet`, `BlockModel`, and `Variogram` provide pretty-printed output in Jupyter with clickable links to view your data in Evo. As support for more geoscience objects is added, geologists and geostatisticians can interact with points, variograms, block models, grids, and more — all through intuitive Python classes. + +For a hands-on introduction, see the [simplified object interactions](https://github.com/SeequentEvo/evo-python-sdk/tree/main/code-samples/geoscience-objects/simplified-object-interactions/) notebook. For a complete geostatistical workflow including variogram modelling and kriging estimation, see the [running kriging compute](https://github.com/SeequentEvo/evo-python-sdk/tree/main/code-samples/geoscience-objects/running-kriging-compute/) notebook. + + +### Getting started with SDK development Now that you have installed the Evo SDK, you can get started by configuring your API connector, and performing a basic API call to list the organizations that you have access to: @@ -52,12 +96,21 @@ async def discovery(): asyncio.run(main()) ``` -For next steps and more information about using Evo, see: +For next steps, start with the packages most relevant to your workflow: + +**Getting started — typed objects & visualisation:** + +* [`evo-objects`](evo-objects/Introduction.md): load and work with points, grids, variograms, and other geoscience objects as typed Python classes +* [`evo-blockmodels`](evo-blockmodels/Introduction.md): create, query, and report on block models with typed interactions +* [`evo-compute`](evo-compute/Introduction.md): run compute tasks such as kriging estimation +* [`evo-widgets`](evo-widgets/Introduction.md): rich HTML rendering of typed geoscience objects in Jupyter notebooks + +**API clients [For developers]:** -* `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-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-compute`](evo-python-sdk/evo-compute): for interacting with the Compute Tasks API +* `evo-sdk-common` ([`discovery`](evo-sdk-common/discovery/DiscoveryAPIClient.md) and [`workspaces`](evo-sdk-common/workspaces/WorkspaceAPIClient.md)): foundation for all Evo SDKs, including arbitrary API requests +* [`evo-files`](evo-files/FileAPIClient.md): low-level File API client +* [`evo-objects` API](evo-objects/ObjectAPIClient.md): low-level Geoscience Object API client +* [`evo-colormaps`](evo-colormaps/ColormapAPIClient.md): Colormap API client +* [`evo-blockmodels` API](evo-blockmodels/BlockModelAPIClient.md): low-level Block Model API client +* [`evo-compute` API](evo-compute/JobClient.md): low-level Compute Tasks API client +* [Seequent Developer Portal](https://developer.seequent.com/docs/guides/getting-started/quick-start-guide): guides, tutorials, and API references 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..c00a1ad7 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,88 @@ 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 95% rename from mkdocs/site/packages/evo-blockmodels.html rename to mkdocs/site/packages/evo-blockmodels/BlockModelAPIClient.html index 447e2113..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 @@ " return html + + +# ============================================================================= +# Compute Task Result Formatters +# ============================================================================= + + +def _get_task_result_portal_url(result: Any) -> str | None: + """Extract Portal URL from a task result's target reference. + + :param result: A TaskResult object with _target.reference attribute. + :return: Portal URL string or None if not available. + """ + # Check if result has _target attribute + target = getattr(result, "_target", None) + if target is None: + return None + + # Check if target has reference attribute + ref = getattr(target, "reference", None) + if not ref or not isinstance(ref, str): + return None + + # Try to generate portal URL from reference + try: + return get_portal_url_from_reference(ref) + except ValueError: + # Invalid reference URL format + return None + + +def format_task_result(result: Any) -> str: + """Format a TaskResult as HTML. + + This formatter handles TaskResult and KrigingResult objects from evo-compute, + displaying the task completion status, target information, and Portal links. + + :param result: A TaskResult object with message, target_name, schema_type, + attribute_name, and _target attributes. + :return: HTML string for the task result. + """ + portal_url = _get_task_result_portal_url(result) + links = [("Portal", portal_url)] if portal_url else None + + # Get result type name (Task, Kriging, etc.) + result_type = result._get_result_type_name() if hasattr(result, "_get_result_type_name") else "Task" + title = f"✓ {result_type} Result" + + rows = [ + ("Target:", result.target_name), + ("Schema:", result.schema_type), + ("Attribute:", f'{result.attribute_name}'), + ] + + table_rows = [build_table_row(label, value) for label, value in rows] + + html = STYLESHEET + html += '
' + html += build_title(title, links) + html += f'
{result.message}
' + html += f"{''.join(table_rows)}
" + html += "
" + + return html + + +def format_task_results(results: Any) -> str: + """Format a TaskResults collection as HTML. + + This formatter handles TaskResults objects from evo-compute, + displaying a table of all completed tasks with their status and Portal links. + + :param results: A TaskResults object with _results list of TaskResult objects. + :return: HTML string for the task results collection. + """ + result_list = results._results + + if not result_list: + return "
No results
" + + # Get result type from first result + result_type = result_list[0]._get_result_type_name() if hasattr(result_list[0], "_get_result_type_name") else "Task" + title = f"✓ {len(result_list)} {result_type} Results" + + # Build table data + headers = ["#", "Target", "Attribute", "Schema", "Link"] + rows = [] + for i, result in enumerate(result_list): + portal_url = _get_task_result_portal_url(result) + link_html = f'Portal' if portal_url else "N/A" + rows.append( + [ + str(i + 1), + result.target_name, + f'{result.attribute_name}', + result.schema_type, + link_html, + ] + ) + + table = build_nested_table(headers, rows) + + html = STYLESHEET + html += '
' + html += build_title(title) + html += table + html += "
" + + return html diff --git a/packages/evo-widgets/tests/test_formatters.py b/packages/evo-widgets/tests/test_formatters.py index 6e81bd0d..cb0a105e 100644 --- a/packages/evo-widgets/tests/test_formatters.py +++ b/packages/evo-widgets/tests/test_formatters.py @@ -20,6 +20,7 @@ _format_bounding_box, _format_crs, _get_base_metadata, + _get_task_result_portal_url, format_attributes_collection, format_base_object, format_block_model, @@ -27,6 +28,8 @@ format_block_model_version, format_report, format_report_result, + format_task_result, + format_task_results, format_variogram, ) @@ -939,5 +942,311 @@ def test_formats_report_result_table(self): self.assertIn("2.5", html) +class TestFormatTaskResult(unittest.TestCase): + """Tests for the format_task_result function.""" + + def _create_mock_task_result(self, **kwargs): + """Create a mock TaskResult object.""" + defaults = { + "message": "Task completed successfully", + "target_name": "Test Grid", + "schema_type": "objects/regular-3d-grid/v1.0.0", + "attribute_name": "kriged_grade", + "target_reference": ( + "https://350mt.api.seequent.com/geoscience-object" + "/orgs/12345678-1234-1234-1234-123456789abc" + "/workspaces/87654321-4321-4321-4321-abcdef123456" + "/objects/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" + ), + } + defaults.update(kwargs) + + obj = MagicMock() + obj.message = defaults["message"] + obj.target_name = defaults["target_name"] + obj.schema_type = defaults["schema_type"] + obj.attribute_name = defaults["attribute_name"] + + # Mock _target with reference for portal URL + obj._target = MagicMock() + obj._target.reference = defaults["target_reference"] + + # Mock _get_result_type_name + obj._get_result_type_name = MagicMock(return_value="Kriging") + + return obj + + def test_formats_task_result_basic_info(self): + """Test formatting a task result with basic information.""" + obj = self._create_mock_task_result() + + html = format_task_result(obj) + + self.assertIn("Kriging Result", html) + self.assertIn("Test Grid", html) + self.assertIn("objects/regular-3d-grid/v1.0.0", html) + self.assertIn("kriged_grade", html) + self.assertIn("Task completed successfully", html) + self.assertIn("attr-highlight", html) # Attribute should be highlighted + + def test_formats_task_result_with_portal_link(self): + """Test formatting a task result includes portal link.""" + obj = self._create_mock_task_result() + + html = format_task_result(obj) + + self.assertIn("Portal", html) + self.assertIn("href=", html) + + def test_formats_task_result_without_portal_link(self): + """Test formatting a task result without reference doesn't fail.""" + obj = self._create_mock_task_result(target_reference=None) + + html = format_task_result(obj) + + # Should still render without crashing + self.assertIn("Kriging Result", html) + self.assertIn("Test Grid", html) + + def test_formats_task_result_checkmark(self): + """Test formatting a task result shows checkmark for success.""" + obj = self._create_mock_task_result() + + html = format_task_result(obj) + + self.assertIn("✓", html) + + def test_formats_task_result_target_row(self): + """Test formatting includes Target row.""" + obj = self._create_mock_task_result() + + html = format_task_result(obj) + + self.assertIn("Target:", html) + self.assertIn("Test Grid", html) + + def test_formats_task_result_schema_row(self): + """Test formatting includes Schema row.""" + obj = self._create_mock_task_result() + + html = format_task_result(obj) + + self.assertIn("Schema:", html) + + def test_formats_task_result_attribute_row(self): + """Test formatting includes Attribute row.""" + obj = self._create_mock_task_result() + + html = format_task_result(obj) + + self.assertIn("Attribute:", html) + + def test_formats_task_result_without_get_result_type_name(self): + """Test formatting a task result that doesn't have _get_result_type_name.""" + obj = self._create_mock_task_result() + del obj._get_result_type_name + + html = format_task_result(obj) + + # Should fall back to "Task" + self.assertIn("Task Result", html) + + +class TestFormatTaskResults(unittest.TestCase): + """Tests for the format_task_results function.""" + + def _create_mock_task_result(self, **kwargs): + """Create a mock TaskResult object.""" + defaults = { + "message": "Task completed successfully", + "target_name": "Test Grid", + "schema_type": "objects/regular-3d-grid/v1.0.0", + "attribute_name": "kriged_grade", + "target_reference": ( + "https://350mt.api.seequent.com/geoscience-object" + "/orgs/12345678-1234-1234-1234-123456789abc" + "/workspaces/87654321-4321-4321-4321-abcdef123456" + "/objects/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" + ), + "result_type": "Kriging", + } + defaults.update(kwargs) + + obj = MagicMock() + obj.message = defaults["message"] + obj.target_name = defaults["target_name"] + obj.schema_type = defaults["schema_type"] + obj.attribute_name = defaults["attribute_name"] + obj._target = MagicMock() + obj._target.reference = defaults["target_reference"] + obj._get_result_type_name = MagicMock(return_value=defaults["result_type"]) + + return obj + + def test_formats_empty_results(self): + """Test formatting an empty results collection.""" + obj = MagicMock() + obj._results = [] + + html = format_task_results(obj) + + self.assertIn("No results", html) + + def test_formats_single_result(self): + """Test formatting a collection with one result.""" + result1 = self._create_mock_task_result(target_name="Grid 1", attribute_name="attr_1") + + obj = MagicMock() + obj._results = [result1] + + html = format_task_results(obj) + + self.assertIn("1 Kriging Results", html) + self.assertIn("Grid 1", html) + self.assertIn("attr_1", html) + self.assertIn("✓", html) + + def test_formats_multiple_results(self): + """Test formatting a collection with multiple results.""" + result1 = self._create_mock_task_result(target_name="Grid 1", attribute_name="attr_1") + result2 = self._create_mock_task_result(target_name="Grid 2", attribute_name="attr_2") + result3 = self._create_mock_task_result(target_name="Grid 3", attribute_name="attr_3") + + obj = MagicMock() + obj._results = [result1, result2, result3] + + html = format_task_results(obj) + + self.assertIn("3 Kriging Results", html) + self.assertIn("Grid 1", html) + self.assertIn("Grid 2", html) + self.assertIn("Grid 3", html) + self.assertIn("attr_1", html) + self.assertIn("attr_2", html) + self.assertIn("attr_3", html) + + def test_formats_results_with_table_headers(self): + """Test formatting includes proper table headers.""" + result1 = self._create_mock_task_result() + + obj = MagicMock() + obj._results = [result1] + + html = format_task_results(obj) + + self.assertIn("#", html) + self.assertIn("Target", html) + self.assertIn("Attribute", html) + self.assertIn("Schema", html) + self.assertIn("Link", html) + + def test_formats_results_with_portal_links(self): + """Test formatting includes portal links for each result.""" + result1 = self._create_mock_task_result(target_name="Grid 1") + result2 = self._create_mock_task_result(target_name="Grid 2") + + obj = MagicMock() + obj._results = [result1, result2] + + html = format_task_results(obj) + + # Should have portal links + self.assertIn("Portal", html) + self.assertIn("href=", html) + + def test_formats_results_without_portal_link(self): + """Test formatting handles results without references.""" + result1 = self._create_mock_task_result(target_reference=None) + + obj = MagicMock() + obj._results = [result1] + + html = format_task_results(obj) + + self.assertIn("N/A", html) + + def test_formats_results_row_numbers(self): + """Test formatting includes sequential row numbers.""" + result1 = self._create_mock_task_result(target_name="Grid 1") + result2 = self._create_mock_task_result(target_name="Grid 2") + + obj = MagicMock() + obj._results = [result1, result2] + + html = format_task_results(obj) + + # Row numbers + self.assertIn(">1<", html) + self.assertIn(">2<", html) + + +class TestGetTaskResultPortalUrl(unittest.TestCase): + """Tests for the _get_task_result_portal_url helper function.""" + + def test_extracts_portal_url_from_valid_reference(self): + """Test extracting portal URL from a valid object reference.""" + result = MagicMock() + result._target = MagicMock() + result._target.reference = ( + "https://350mt.api.seequent.com/geoscience-object" + "/orgs/12345678-1234-1234-1234-123456789abc" + "/workspaces/87654321-4321-4321-4321-abcdef123456" + "/objects/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" + ) + + url = _get_task_result_portal_url(result) + + self.assertIsNotNone(url) + self.assertIn("evo.seequent.com", url) + + def test_returns_none_for_no_reference(self): + """Test returns None when target has no reference.""" + result = MagicMock() + result._target = MagicMock() + result._target.reference = None + + url = _get_task_result_portal_url(result) + + self.assertIsNone(url) + + def test_returns_none_for_invalid_reference(self): + """Test returns None for invalid reference URL.""" + result = MagicMock() + result._target = MagicMock() + result._target.reference = "not-a-valid-url" + + url = _get_task_result_portal_url(result) + + self.assertIsNone(url) + + def test_returns_none_when_no_target(self): + """Test returns None when result has no _target attribute.""" + result = MagicMock(spec=[]) # Empty spec means no attributes + + url = _get_task_result_portal_url(result) + + self.assertIsNone(url) + + def test_returns_none_for_non_string_reference(self): + """Test returns None when reference is not a string.""" + result = MagicMock() + result._target = MagicMock() + result._target.reference = 12345 # Not a string + + url = _get_task_result_portal_url(result) + + self.assertIsNone(url) + + def test_returns_none_for_empty_string_reference(self): + """Test returns None when reference is an empty string.""" + result = MagicMock() + result._target = MagicMock() + result._target.reference = "" + + url = _get_task_result_portal_url(result) + + self.assertIsNone(url) + + if __name__ == "__main__": unittest.main() diff --git a/pyproject.toml b/pyproject.toml index 626ae14a..f9cbfc62 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,16 +1,16 @@ [project] name = "evo-sdk" -version = "0.1.20" +version = "0.2.0" description = "Python SDK for using Seequent Evo" requires-python = ">=3.10" dependencies = [ - "evo-sdk-common[aiohttp,notebooks,jmespath]>=0.5.12", + "evo-sdk-common[aiohttp,notebooks,jmespath]>=0.5.19", "evo-widgets>=0.2.0", "evo-blockmodels[aiohttp,notebooks,pyarrow]>=0.2.0", "evo-objects[aiohttp,notebooks,utils]>=0.4.0", "evo-files[aiohttp,notebooks]>=0.2.3", "evo-colormaps[aiohttp,notebooks]>=0.0.2", - "evo-compute[aiohttp,notebooks]>=0.0.1rc2", + "evo-compute[aiohttp,notebooks]>=0.0.2", "jupyter", ] dynamic = ["readme"] diff --git a/uv.lock b/uv.lock index 5cf4cc7c..178308fe 100644 --- a/uv.lock +++ b/uv.lock @@ -874,11 +874,14 @@ test = [ [[package]] name = "evo-compute" -version = "0.0.1rc3" +version = "0.0.2" source = { editable = "packages/evo-compute" } dependencies = [ + { name = "evo-blockmodels", extra = ["utils"] }, + { name = "evo-objects", extra = ["blockmodels", "utils"] }, { name = "evo-sdk-common" }, { name = "pydantic" }, + { name = "typing-extensions" }, ] [package.optional-dependencies] @@ -906,10 +909,13 @@ test = [ [package.metadata] requires-dist = [ + { name = "evo-blockmodels", extras = ["utils"], editable = "packages/evo-blockmodels" }, + { name = "evo-objects", extras = ["utils", "blockmodels"], editable = "packages/evo-objects" }, { name = "evo-sdk-common", editable = "packages/evo-sdk-common" }, { name = "evo-sdk-common", extras = ["aiohttp"], marker = "extra == 'aiohttp'", editable = "packages/evo-sdk-common" }, { name = "evo-sdk-common", extras = ["notebooks"], marker = "extra == 'notebooks'", editable = "packages/evo-sdk-common" }, { name = "pydantic", specifier = ">=2" }, + { name = "typing-extensions", specifier = ">=4.0" }, ] provides-extras = ["aiohttp", "notebooks"] @@ -1079,7 +1085,7 @@ test = [ [[package]] name = "evo-sdk" -version = "0.1.20" +version = "0.2.0" source = { editable = "." } dependencies = [ { name = "evo-blockmodels", extra = ["aiohttp", "notebooks"] }, @@ -1144,7 +1150,7 @@ test = [ [[package]] name = "evo-sdk-common" -version = "0.5.18" +version = "0.5.19" source = { editable = "packages/evo-sdk-common" } dependencies = [ { name = "pure-interface" },