From 85070aad3aa7dfea43edb82ccb50e82dc855c374 Mon Sep 17 00:00:00 2001 From: spara-earthscope Date: Tue, 11 Aug 2026 13:56:01 -0600 Subject: [PATCH 01/15] add graphviz/seisbench & tests --- geolab-base/environment.yml | 2 + geolab-base/requirements.txt | 4 +- geolab-base/test_notebook.ipynb | 706 +++++++++++++++----------------- 3 files changed, 340 insertions(+), 372 deletions(-) diff --git a/geolab-base/environment.yml b/geolab-base/environment.yml index b315c6e..dd1240b 100644 --- a/geolab-base/environment.yml +++ b/geolab-base/environment.yml @@ -47,6 +47,8 @@ dependencies: - distributed # --- Visualization --- - matplotlib-base + - graphviz + - pygraphviz - altair - hvplot - holoviews diff --git a/geolab-base/requirements.txt b/geolab-base/requirements.txt index bb232c5..67cf038 100644 --- a/geolab-base/requirements.txt +++ b/geolab-base/requirements.txt @@ -6,6 +6,8 @@ earthscope-sdk==1.6.1 earthscope-cli==1.2.0 earthscopestraintools - # --- Jupyter add-ons --- jupyterlab_jupyterbook_navigation + +# --- Geophysics --- +seisbench==0.12.3 diff --git a/geolab-base/test_notebook.ipynb b/geolab-base/test_notebook.ipynb index fb15dd6..ab35f1d 100644 --- a/geolab-base/test_notebook.ipynb +++ b/geolab-base/test_notebook.ipynb @@ -1,373 +1,337 @@ { - "cells": [ - { - "cell_type": "markdown", - "id": "118af38b", - "metadata": {}, - "source": [ - "# Smoke tests for `geolab-base`\n", - "\n", - "For every package in `environment.yml` (conda + pip): try to import it and\n", - "exercise one minimal API call. CLI-only packages get a `which`/`--version`\n", - "check instead. A failure here means something installed but doesn't load,\n", - "which is usually a sign of an ABI mismatch or a missing system library.\n", - "\n", - "Run all cells. The summary at the bottom lists pass/fail per package." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Setup" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import importlib\n", - "import shutil\n", - "import subprocess\n", - "import sys\n", - "\n", - "RESULTS = []\n", - "\n", - "\n", - "def py(modname, alias=None, smoke=None):\n", - " \"\"\"Import `modname` and optionally run `smoke(mod)` as a sanity check.\"\"\"\n", - " label = alias or modname\n", - " try:\n", - " mod = importlib.import_module(modname)\n", - " if smoke is not None:\n", - " smoke(mod)\n", - " version = getattr(mod, '__version__', '')\n", - " RESULTS.append((label, 'OK', str(version), ''))\n", - " except Exception as exc:\n", - " RESULTS.append((label, 'FAIL', '', f'{type(exc).__name__}: {exc}'))\n", - "\n", - "\n", - "def cli(cmd, version_flag='--version'):\n", - " \"\"\"Verify `cmd` is on $PATH and responds to a version flag.\"\"\"\n", - " path = shutil.which(cmd)\n", - " if not path:\n", - " RESULTS.append((cmd, 'FAIL', '', 'not on $PATH'))\n", - " return\n", - " try:\n", - " r = subprocess.run([cmd, version_flag],\n", - " capture_output=True, text=True, timeout=10)\n", - " line = (r.stdout or r.stderr).strip().splitlines()\n", - " version = line[0] if line else 'on PATH'\n", - " RESULTS.append((cmd, 'OK', version[:80], ''))\n", - " except Exception as exc:\n", - " RESULTS.append((cmd, 'OK', 'on PATH', f'{type(exc).__name__}'))\n", - "\n", - "\n", - "print(f'Python {sys.version}')\n", - "print(f'sys.prefix: {sys.prefix}')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Cloud & storage" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "705a7197", - "metadata": {}, - "outputs": [], - "source": [ - "cli('aws')\n", - "py('awswrangler')\n", - "py('boto3', smoke=lambda m: m.client('s3', region_name='us-east-1'))\n", - "py('fsspec', smoke=lambda m: m.filesystem('memory'))\n", - "py('obstore')\n", - "py('s3fs', smoke=lambda m: m.S3FileSystem)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Geospatial" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "84bab05a", - "metadata": {}, - "outputs": [], - "source": [ - "py('cartopy.crs', alias='cartopy',\n", - " smoke=lambda m: m.PlateCarree())\n", - "py('contextily')\n", - "py('fiona', smoke=lambda m: m.supported_drivers)\n", - "py('folium',\n", - " smoke=lambda m: m.Map(location=[0, 0], zoom_start=2))\n", - "py('osgeo.gdal', alias='gdal',\n", - " smoke=lambda m: m.VersionInfo('RELEASE_NAME'))\n", - "py('ipyleaflet', smoke=lambda m: m.Map())\n", - "py('lonboard')\n", - "py('pyproj', smoke=lambda m: m.CRS('EPSG:4326'))\n", - "py('shapely.geometry', alias='shapely',\n", - " smoke=lambda m: m.Point(0, 0).buffer(1).area)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Core scientific stack" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b383bbe0", - "metadata": {}, - "outputs": [], - "source": [ - "py('numpy', smoke=lambda m: int(m.array([1, 2, 3]).sum()))\n", - "py('numba', smoke=lambda m: m.njit(lambda x: x + 1)(1))\n", - "py('scipy.stats', alias='scipy', smoke=lambda m: m.norm.cdf(0))\n", - "py('pandas',\n", - " smoke=lambda m: m.DataFrame({'a': [1, 2]}).shape)\n", - "py('geopandas')\n", - "import matplotlib; matplotlib.use('Agg')\n", - "py('matplotlib', alias='matplotlib-base',\n", - " smoke=lambda m: m.figure.Figure())\n", - "py('xarray',\n", - " smoke=lambda m: m.DataArray([1, 2, 3]).sum().item())\n", - "py('netCDF4', alias='netcdf4')\n", - "py('h5py')\n", - "py('h5netcdf')\n", - "py('pyarrow',\n", - " smoke=lambda m: m.array([1, 2, 3]).to_pylist())\n", - "py('zarr',\n", - " smoke=lambda m: m.zeros((3,), chunks=3, dtype='f4'))\n", - "py('virtualizarr')\n", - "py('bottleneck',\n", - " smoke=lambda m: m.nansum([1.0, 2.0, float('nan'), 3.0]))\n", - "py('flox')\n", - "py('pooch')\n", - "py('dask.array', alias='dask',\n", - " smoke=lambda m: m.ones(10, chunks=5).sum().compute())\n", - "py('distributed')\n", - "py('dask_gateway', alias='dask-gateway')\n", - "py('cvxpy', smoke=lambda m: m.Variable(name='x'))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Geo / geoscience" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "062000c0", - "metadata": {}, - "outputs": [], - "source": [ - "py('dascore')\n", - "cli('gmt', version_flag='--version')\n", - "py('obspy',\n", - " smoke=lambda m: m.UTCDateTime('2020-01-01').timestamp)\n", - "py('obsplus')\n", - "py('pygmt')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Utilities" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "dc28287d", - "metadata": {}, - "outputs": [], - "source": [ - "py('tqdm',\n", - " smoke=lambda m: list(m.tqdm(range(3), disable=True)))\n", - "py('requests')\n", - "py('yaml', alias='pyyaml',\n", - " smoke=lambda m: m.safe_load('a: 1'))\n", - "cli('gs', version_flag='--version') # ghostscript\n", - "cli('ffmpeg', version_flag='-version')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Dev tools" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "cli('gh')\n", - "cli('gh-scoped-creds')\n", - "py('pytest')\n", - "cli('ruff')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Jupyter stack & extensions" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "6ed8a837", - "metadata": {}, - "outputs": [], - "source": [ - "py('jupyterhub')\n", - "py('jupyter_server')\n", - "py('jupyterlab')\n", - "py('ipykernel')\n", - "py('jupyter_resource_usage', alias='jupyter-resource-usage')\n", - "py('jupyter_ruff', alias='jupyter-ruff')\n", - "py('jupyter_server_proxy', alias='jupyter-server-proxy')\n", - "py('jupyterlab_git', alias='jupyterlab-git')\n", - "py('jupyterlab_myst', alias='jupyterlab-myst')\n", - "py('jupyterlab_code_formatter')\n", - "py('jupyterlab_pygments')\n", - "py('nbdime')" - ] - }, - { - "cell_type": "markdown", - "id": "7788e14a", - "metadata": {}, - "source": [ - "## pip packages & visualization" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "824b272d", - "metadata": {}, - "outputs": [], - "source": [ - "# EarthScope --------------------------------------------------\n", - "py('earthscope_sdk', alias='earthscope-sdk')\n", - "cli('es') # earthscope-cli entry point\n", - "py('earthscopestraintools')\n", - "\n", - "# Jupyter add-ons ---------------------------------------------\n", - "py('jupyterlab_jupyterbook_navigation')\n", - "\n", - "# Visualization & data frames ---------------------------------\n", - "py('altair',\n", - " smoke=lambda m: m.Chart())\n", - "py('plotly')\n", - "py('polars',\n", - " smoke=lambda m: m.DataFrame({'a': [1, 2]}))\n", - "py('vegafusion')\n", - "py('vl_convert', alias='vl-convert-python')\n", - "py('ipympl')\n", - "py('hvplot')\n", - "py('holoviews', alias='holoviews',\n", - " smoke=lambda m: m.Curve([1, 2, 3]))\n", - "py('panel')" - ] - }, - { - "cell_type": "markdown", - "id": "5411c0ef", - "metadata": {}, - "source": [ - "## Interactive widgets" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d7ba172f", - "metadata": {}, - "outputs": [], - "source": [ - "py('ipywidgets',\n", - " smoke=lambda m: m.IntSlider(value=5, min=0, max=10))\n", - "py('anywidget')\n", - "py('bqplot')\n", - "py('ipytree', smoke=lambda m: m.Node(name='root'))\n", - "py('itables')\n", - "py('ipydatagrid')\n", - "from sidecar import Sidecar # noqa: F401\n", - "py('sidecar')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Summary" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import pandas as pd\n", - "from IPython.display import display\n", - "\n", - "df = pd.DataFrame(RESULTS,\n", - " columns=['package', 'status', 'version', 'error'])\n", - "\n", - "passed = int((df['status'] == 'OK').sum())\n", - "total = len(df)\n", - "failed = total - passed\n", - "\n", - "print(f'Results: {passed}/{total} OK, {failed} failed')\n", - "if failed:\n", - " print('\\nFailures:')\n", - " for _, row in df[df['status'] == 'FAIL'].iterrows():\n", - " print(f\" {row['package']:35s} {row['error']}\")\n", - "\n", - "df.style.map(\n", - " lambda v: ('color: red; font-weight: bold' if v == 'FAIL'\n", - " else 'color: green'),\n", - " subset=['status']\n", - ")" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "name": "python", - "pygments_lexer": "ipython3" - } + "cells": [ + { + "cell_type": "markdown", + "id": "118af38b", + "metadata": {}, + "source": [ + "# Smoke tests for `geolab-base`\n", + "\n", + "For every package in `environment.yml` (conda + pip): try to import it and\n", + "exercise one minimal API call. CLI-only packages get a `which`/`--version`\n", + "check instead. A failure here means something installed but doesn't load,\n", + "which is usually a sign of an ABI mismatch or a missing system library.\n", + "\n", + "Run all cells. The summary at the bottom lists pass/fail per package." + ] }, - "nbformat": 4, - "nbformat_minor": 5 -} + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import importlib\n", + "import shutil\n", + "import subprocess\n", + "import sys\n", + "\n", + "RESULTS = []\n", + "\n", + "\n", + "def py(modname, alias=None, smoke=None):\n", + " \"\"\"Import `modname` and optionally run `smoke(mod)` as a sanity check.\"\"\"\n", + " label = alias or modname\n", + " try:\n", + " mod = importlib.import_module(modname)\n", + " if smoke is not None:\n", + " smoke(mod)\n", + " version = getattr(mod, '__version__', '')\n", + " RESULTS.append((label, 'OK', str(version), ''))\n", + " except Exception as exc:\n", + " RESULTS.append((label, 'FAIL', '', f'{type(exc).__name__}: {exc}'))\n", + "\n", + "\n", + "def cli(cmd, version_flag='--version'):\n", + " \"\"\"Verify `cmd` is on $PATH and responds to a version flag.\"\"\"\n", + " path = shutil.which(cmd)\n", + " if not path:\n", + " RESULTS.append((cmd, 'FAIL', '', 'not on $PATH'))\n", + " return\n", + " try:\n", + " r = subprocess.run([cmd, version_flag],\n", + " capture_output=True, text=True, timeout=10)\n", + " line = (r.stdout or r.stderr).strip().splitlines()\n", + " version = line[0] if line else 'on PATH'\n", + " RESULTS.append((cmd, 'OK', version[:80], ''))\n", + " except Exception as exc:\n", + " RESULTS.append((cmd, 'OK', 'on PATH', f'{type(exc).__name__}'))\n", + "\n", + "\n", + "print(f'Python {sys.version}')\n", + "print(f'sys.prefix: {sys.prefix}')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Cloud & storage" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "705a7197", + "metadata": {}, + "outputs": [], + "source": [ + "cli('aws')\n", + "py('awswrangler')\n", + "py('boto3', smoke=lambda m: m.client('s3', region_name='us-east-1'))\n", + "py('fsspec', smoke=lambda m: m.filesystem('memory'))\n", + "py('obstore')\n", + "py('s3fs', smoke=lambda m: m.S3FileSystem)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Geospatial" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "84bab05a", + "metadata": {}, + "outputs": [], + "source": [ + "py('cartopy.crs', alias='cartopy',\n", + " smoke=lambda m: m.PlateCarree())\n", + "py('contextily')\n", + "py('fiona', smoke=lambda m: m.supported_drivers)\n", + "py('folium',\n", + " smoke=lambda m: m.Map(location=[0, 0], zoom_start=2))\n", + "py('osgeo.gdal', alias='gdal',\n", + " smoke=lambda m: m.VersionInfo('RELEASE_NAME'))\n", + "py('ipyleaflet', smoke=lambda m: m.Map())\n", + "py('lonboard')\n", + "py('pyproj', smoke=lambda m: m.CRS('EPSG:4326'))\n", + "py('shapely.geometry', alias='shapely',\n", + " smoke=lambda m: m.Point(0, 0).buffer(1).area)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Core scientific stack" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b383bbe0", + "metadata": {}, + "outputs": [], + "source": "py('numpy', smoke=lambda m: int(m.array([1, 2, 3]).sum()))\npy('numba', smoke=lambda m: m.njit(lambda x: x + 1)(1))\npy('scipy.stats', alias='scipy', smoke=lambda m: m.norm.cdf(0))\npy('pandas',\n smoke=lambda m: m.DataFrame({'a': [1, 2]}).shape)\npy('geopandas')\nimport matplotlib; matplotlib.use('Agg')\npy('matplotlib', alias='matplotlib-base',\n smoke=lambda m: m.figure.Figure())\ncli('dot', version_flag='-V')\npy('pygraphviz', smoke=lambda m: m.AGraph().add_node(1))\npy('xarray',\n smoke=lambda m: m.DataArray([1, 2, 3]).sum().item())\npy('netCDF4', alias='netcdf4')\npy('h5py')\npy('h5netcdf')\npy('pyarrow',\n smoke=lambda m: m.array([1, 2, 3]).to_pylist())\npy('zarr',\n smoke=lambda m: m.zeros((3,), chunks=3, dtype='f4'))\npy('virtualizarr')\npy('bottleneck',\n smoke=lambda m: m.nansum([1.0, 2.0, float('nan'), 3.0]))\npy('flox')\npy('pooch')\npy('dask.array', alias='dask',\n smoke=lambda m: m.ones(10, chunks=5).sum().compute())\npy('distributed')\npy('dask_gateway', alias='dask-gateway')\npy('cvxpy', smoke=lambda m: m.Variable(name='x'))" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Geo / geoscience" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "062000c0", + "metadata": {}, + "outputs": [], + "source": "py('dascore')\ncli('gmt', version_flag='--version')\npy('obspy',\n smoke=lambda m: m.UTCDateTime('2020-01-01').timestamp)\npy('obsplus')\npy('pygmt')\npy('seisbench', smoke=lambda m: m.models.PhaseNet())" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Utilities" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dc28287d", + "metadata": {}, + "outputs": [], + "source": [ + "py('tqdm',\n", + " smoke=lambda m: list(m.tqdm(range(3), disable=True)))\n", + "py('requests')\n", + "py('yaml', alias='pyyaml',\n", + " smoke=lambda m: m.safe_load('a: 1'))\n", + "cli('gs', version_flag='--version') # ghostscript\n", + "cli('ffmpeg', version_flag='-version')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Dev tools" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "cli('gh')\n", + "cli('gh-scoped-creds')\n", + "py('pytest')\n", + "cli('ruff')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Jupyter stack & extensions" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6ed8a837", + "metadata": {}, + "outputs": [], + "source": [ + "py('jupyterhub')\n", + "py('jupyter_server')\n", + "py('jupyterlab')\n", + "py('ipykernel')\n", + "py('jupyter_resource_usage', alias='jupyter-resource-usage')\n", + "py('jupyter_ruff', alias='jupyter-ruff')\n", + "py('jupyter_server_proxy', alias='jupyter-server-proxy')\n", + "py('jupyterlab_git', alias='jupyterlab-git')\n", + "py('jupyterlab_myst', alias='jupyterlab-myst')\n", + "py('jupyterlab_code_formatter')\n", + "py('jupyterlab_pygments')\n", + "py('nbdime')" + ] + }, + { + "cell_type": "markdown", + "id": "7788e14a", + "metadata": {}, + "source": [ + "## pip packages & visualization" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "824b272d", + "metadata": {}, + "outputs": [], + "source": [ + "# EarthScope --------------------------------------------------\n", + "py('earthscope_sdk', alias='earthscope-sdk')\n", + "cli('es') # earthscope-cli entry point\n", + "py('earthscopestraintools')\n", + "\n", + "# Jupyter add-ons ---------------------------------------------\n", + "py('jupyterlab_jupyterbook_navigation')\n", + "\n", + "# Visualization & data frames ---------------------------------\n", + "py('altair',\n", + " smoke=lambda m: m.Chart())\n", + "py('plotly')\n", + "py('polars',\n", + " smoke=lambda m: m.DataFrame({'a': [1, 2]}))\n", + "py('vegafusion')\n", + "py('vl_convert', alias='vl-convert-python')\n", + "py('ipympl')\n", + "py('hvplot')\n", + "py('holoviews', alias='holoviews',\n", + " smoke=lambda m: m.Curve([1, 2, 3]))\n", + "py('panel')" + ] + }, + { + "cell_type": "markdown", + "id": "5411c0ef", + "metadata": {}, + "source": [ + "## Interactive widgets" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d7ba172f", + "metadata": {}, + "outputs": [], + "source": [ + "py('ipywidgets',\n", + " smoke=lambda m: m.IntSlider(value=5, min=0, max=10))\n", + "py('anywidget')\n", + "py('bqplot')\n", + "py('ipytree', smoke=lambda m: m.Node(name='root'))\n", + "py('itables')\n", + "py('ipydatagrid')\n", + "from sidecar import Sidecar # noqa: F401\n", + "py('sidecar')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Summary" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "from IPython.display import display\n", + "\n", + "df = pd.DataFrame(RESULTS,\n", + " columns=['package', 'status', 'version', 'error'])\n", + "\n", + "passed = int((df['status'] == 'OK').sum())\n", + "total = len(df)\n", + "failed = total - passed\n", + "\n", + "print(f'Results: {passed}/{total} OK, {failed} failed')\n", + "if failed:\n", + " print('\\nFailures:')\n", + " for _, row in df[df['status'] == 'FAIL'].iterrows():\n", + " print(f\" {row['package']:35s} {row['error']}\")\n", + "\n", + "df.style.map(\n", + " lambda v: ('color: red; font-weight: bold' if v == 'FAIL'\n", + " else 'color: green'),\n", + " subset=['status']\n", + ")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "pygments_lexer": "ipython3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file From 359c78606dd6ed5fbdbbff9d5b664b2e3aa95a63 Mon Sep 17 00:00:00 2001 From: spara-earthscope Date: Mon, 17 Aug 2026 12:32:39 -0600 Subject: [PATCH 02/15] changes for 0.2.0 version --- geolab-base/Dockerfile | 11 +- geolab-base/requirements.txt | 2 +- geolab-base/test_helpers.py | 41 +++ geolab-base/test_notebook.ipynb | 86 ++--- geolab-base/test_packages.py | 593 -------------------------------- 5 files changed, 93 insertions(+), 640 deletions(-) create mode 100644 geolab-base/test_helpers.py delete mode 100644 geolab-base/test_packages.py diff --git a/geolab-base/Dockerfile b/geolab-base/Dockerfile index 14d9039..3053acc 100644 --- a/geolab-base/Dockerfile +++ b/geolab-base/Dockerfile @@ -11,21 +11,26 @@ # docker build --platform linux/amd64 \ # --build-arg IMAGE_TITLE=my-geolab-image \ # --build-arg IMAGE_AUTHORS=you@university.edu \ +# --build-arg GEOLAB_VERSION=1.0.0 \ # -t my-geolab-image . # ────────────────────────────────────────────────────────────── -FROM pangeo/base-image:latest +# Changing this tag is a major (breaking) change under semantic versioning. +FROM pangeo/base-image:04bb14b ARG IMAGE_TITLE="custom-geolab-image" # default if user passes nothing ARG IMAGE_AUTHORS="NoSpecifiedAuthors" # default if user passes nothing +ARG GEOLAB_VERSION="0.9.4" # default if user passes nothing LABEL org.opencontainers.image.title="${IMAGE_TITLE}" \ - org.opencontainers.image.authors="${IMAGE_AUTHORS}" + org.opencontainers.image.authors="${IMAGE_AUTHORS}" \ + org.opencontainers.image.version="${GEOLAB_VERSION}" # Set locations of PROJ/GDAL resource directories ENV PROJ_DATA=/srv/conda/envs/notebook/share/proj \ PROJ_LIB=/srv/conda/envs/notebook/share/proj \ - GDAL_DATA=/srv/conda/envs/notebook/share/gdal + GDAL_DATA=/srv/conda/envs/notebook/share/gdal \ + GEOLAB_VERSION=${GEOLAB_VERSION} # Default command for standalone use. JupyterHub spawning passes # its own command (jupyterhub-singleuser), which start respects. diff --git a/geolab-base/requirements.txt b/geolab-base/requirements.txt index 67cf038..f81ef83 100644 --- a/geolab-base/requirements.txt +++ b/geolab-base/requirements.txt @@ -10,4 +10,4 @@ earthscopestraintools jupyterlab_jupyterbook_navigation # --- Geophysics --- -seisbench==0.12.3 + diff --git a/geolab-base/test_helpers.py b/geolab-base/test_helpers.py new file mode 100644 index 0000000..ac86288 --- /dev/null +++ b/geolab-base/test_helpers.py @@ -0,0 +1,41 @@ +"""Helpers for smoke-testing installed packages and CLI tools.""" + +import importlib +import shutil +import subprocess + +RESULTS = [] + + +def reset(): + """Clear RESULTS — call before a fresh run in a long-lived kernel.""" + RESULTS.clear() + + +def py(modname, alias=None, smoke=None): + """Import `modname` and optionally run `smoke(mod)` as a sanity check.""" + label = alias or modname + try: + mod = importlib.import_module(modname) + if smoke is not None: + smoke(mod) + version = getattr(mod, '__version__', '') + RESULTS.append((label, 'OK', str(version), '')) + except Exception as exc: + RESULTS.append((label, 'FAIL', '', f'{type(exc).__name__}: {exc}')) + + +def cli(cmd, version_flag='--version'): + """Verify `cmd` is on $PATH and responds to a version flag.""" + path = shutil.which(cmd) + if not path: + RESULTS.append((cmd, 'FAIL', '', 'not on $PATH')) + return + try: + r = subprocess.run([cmd, version_flag], + capture_output=True, text=True, timeout=10) + line = (r.stdout or r.stderr).strip().splitlines() + version = line[0] if line else 'on PATH' + RESULTS.append((cmd, 'OK', version[:80], '')) + except Exception as exc: + RESULTS.append((cmd, 'OK', 'on PATH', f'{type(exc).__name__}')) diff --git a/geolab-base/test_notebook.ipynb b/geolab-base/test_notebook.ipynb index ab35f1d..20c08a8 100644 --- a/geolab-base/test_notebook.ipynb +++ b/geolab-base/test_notebook.ipynb @@ -25,49 +25,10 @@ { "cell_type": "code", "execution_count": null, + "id": "618283a2", "metadata": {}, "outputs": [], - "source": [ - "import importlib\n", - "import shutil\n", - "import subprocess\n", - "import sys\n", - "\n", - "RESULTS = []\n", - "\n", - "\n", - "def py(modname, alias=None, smoke=None):\n", - " \"\"\"Import `modname` and optionally run `smoke(mod)` as a sanity check.\"\"\"\n", - " label = alias or modname\n", - " try:\n", - " mod = importlib.import_module(modname)\n", - " if smoke is not None:\n", - " smoke(mod)\n", - " version = getattr(mod, '__version__', '')\n", - " RESULTS.append((label, 'OK', str(version), ''))\n", - " except Exception as exc:\n", - " RESULTS.append((label, 'FAIL', '', f'{type(exc).__name__}: {exc}'))\n", - "\n", - "\n", - "def cli(cmd, version_flag='--version'):\n", - " \"\"\"Verify `cmd` is on $PATH and responds to a version flag.\"\"\"\n", - " path = shutil.which(cmd)\n", - " if not path:\n", - " RESULTS.append((cmd, 'FAIL', '', 'not on $PATH'))\n", - " return\n", - " try:\n", - " r = subprocess.run([cmd, version_flag],\n", - " capture_output=True, text=True, timeout=10)\n", - " line = (r.stdout or r.stderr).strip().splitlines()\n", - " version = line[0] if line else 'on PATH'\n", - " RESULTS.append((cmd, 'OK', version[:80], ''))\n", - " except Exception as exc:\n", - " RESULTS.append((cmd, 'OK', 'on PATH', f'{type(exc).__name__}'))\n", - "\n", - "\n", - "print(f'Python {sys.version}')\n", - "print(f'sys.prefix: {sys.prefix}')" - ] + "source": "import sys\n\nimport test_helpers as test\nfrom test_helpers import RESULTS, cli, py\n\ntest.RESULTS.clear()\n\nprint(f'Python {sys.version}')\nprint(f'sys.prefix: {sys.prefix}')" }, { "cell_type": "markdown", @@ -133,7 +94,38 @@ "id": "b383bbe0", "metadata": {}, "outputs": [], - "source": "py('numpy', smoke=lambda m: int(m.array([1, 2, 3]).sum()))\npy('numba', smoke=lambda m: m.njit(lambda x: x + 1)(1))\npy('scipy.stats', alias='scipy', smoke=lambda m: m.norm.cdf(0))\npy('pandas',\n smoke=lambda m: m.DataFrame({'a': [1, 2]}).shape)\npy('geopandas')\nimport matplotlib; matplotlib.use('Agg')\npy('matplotlib', alias='matplotlib-base',\n smoke=lambda m: m.figure.Figure())\ncli('dot', version_flag='-V')\npy('pygraphviz', smoke=lambda m: m.AGraph().add_node(1))\npy('xarray',\n smoke=lambda m: m.DataArray([1, 2, 3]).sum().item())\npy('netCDF4', alias='netcdf4')\npy('h5py')\npy('h5netcdf')\npy('pyarrow',\n smoke=lambda m: m.array([1, 2, 3]).to_pylist())\npy('zarr',\n smoke=lambda m: m.zeros((3,), chunks=3, dtype='f4'))\npy('virtualizarr')\npy('bottleneck',\n smoke=lambda m: m.nansum([1.0, 2.0, float('nan'), 3.0]))\npy('flox')\npy('pooch')\npy('dask.array', alias='dask',\n smoke=lambda m: m.ones(10, chunks=5).sum().compute())\npy('distributed')\npy('dask_gateway', alias='dask-gateway')\npy('cvxpy', smoke=lambda m: m.Variable(name='x'))" + "source": [ + "py('numpy', smoke=lambda m: int(m.array([1, 2, 3]).sum()))\n", + "py('numba', smoke=lambda m: m.njit(lambda x: x + 1)(1))\n", + "py('scipy.stats', alias='scipy', smoke=lambda m: m.norm.cdf(0))\n", + "py('pandas',\n", + " smoke=lambda m: m.DataFrame({'a': [1, 2]}).shape)\n", + "py('geopandas')\n", + "import matplotlib; matplotlib.use('Agg')\n", + "py('matplotlib', alias='matplotlib-base',\n", + " smoke=lambda m: m.figure.Figure())\n", + "cli('dot', version_flag='-V')\n", + "py('pygraphviz', smoke=lambda m: m.AGraph().add_node(1))\n", + "py('xarray',\n", + " smoke=lambda m: m.DataArray([1, 2, 3]).sum().item())\n", + "py('netCDF4', alias='netcdf4')\n", + "py('h5py')\n", + "py('h5netcdf')\n", + "py('pyarrow',\n", + " smoke=lambda m: m.array([1, 2, 3]).to_pylist())\n", + "py('zarr',\n", + " smoke=lambda m: m.zeros((3,), chunks=3, dtype='f4'))\n", + "py('virtualizarr')\n", + "py('bottleneck',\n", + " smoke=lambda m: m.nansum([1.0, 2.0, float('nan'), 3.0]))\n", + "py('flox')\n", + "py('pooch')\n", + "py('dask.array', alias='dask',\n", + " smoke=lambda m: m.ones(10, chunks=5).sum().compute())\n", + "py('distributed')\n", + "py('dask_gateway', alias='dask-gateway')\n", + "py('cvxpy', smoke=lambda m: m.Variable(name='x'))" + ] }, { "cell_type": "markdown", @@ -148,7 +140,15 @@ "id": "062000c0", "metadata": {}, "outputs": [], - "source": "py('dascore')\ncli('gmt', version_flag='--version')\npy('obspy',\n smoke=lambda m: m.UTCDateTime('2020-01-01').timestamp)\npy('obsplus')\npy('pygmt')\npy('seisbench', smoke=lambda m: m.models.PhaseNet())" + "source": [ + "py('dascore')\n", + "cli('gmt', version_flag='--version')\n", + "py('obspy',\n", + " smoke=lambda m: m.UTCDateTime('2020-01-01').timestamp)\n", + "py('obsplus')\n", + "py('pygmt')\n", + "py('seisbench', smoke=lambda m: m.models.PhaseNet())" + ] }, { "cell_type": "markdown", diff --git a/geolab-base/test_packages.py b/geolab-base/test_packages.py deleted file mode 100644 index a25b8f0..0000000 --- a/geolab-base/test_packages.py +++ /dev/null @@ -1,593 +0,0 @@ -""" -Unit tests for packages installed in geolab-base. - -Run inside the container: - pytest test_packages.py -v - -Run with a filter: - pytest test_packages.py -v -k geospatial # tests with 'geospatial' in name - pytest test_packages.py::test_obspy -v # single test - -Each test exercises one package with a minimal API call. Failures usually -indicate ABI mismatches, missing system libraries, or broken installs -- -NOT just missing imports, which a smoke check would also catch. -""" - -import math -import re -import shutil -import subprocess - -import pytest - -# ─── Helpers ────────────────────────────────────────────────── - - -def _cli_version(cmd, version_flag="--version"): - """Run `cmd --version` and return stdout. Fail if not on $PATH.""" - if not shutil.which(cmd): - pytest.fail(f"{cmd} not on $PATH") - r = subprocess.run( - [cmd, version_flag], - capture_output=True, - text=True, - timeout=10, - ) - return (r.stdout or r.stderr).strip() - - -# ─── Cloud & storage ────────────────────────────────────────── - - -def test_aws_cli(): - out = _cli_version("aws") - assert "aws-cli" in out.lower() - - -def test_awswrangler(): - import awswrangler as wr - - assert wr.__version__ - - -def test_boto3(): - import boto3 - - client = boto3.client("s3", region_name="us-east-1") - assert client.meta.service_model.service_name == "s3" - - -def test_obstore(): - import obstore # noqa: F401 - - -def test_fsspec(): - import fsspec - - fs = fsspec.filesystem("memory") - assert fs.protocol == "memory" - - -def test_s3fs(): - import s3fs - - assert s3fs.S3FileSystem is not None - - -# ─── Geospatial ─────────────────────────────────────────────── - - -def test_cartopy(): - import cartopy.crs as ccrs - - proj = ccrs.PlateCarree() - assert proj.proj4_params is not None - - -def test_contextily(): - import contextily as cx - - assert cx.__version__ - - -def test_fiona(): - import fiona - - drivers = fiona.supported_drivers - assert "GPKG" in drivers - assert "ESRI Shapefile" in drivers - - -def test_folium(): - import folium - - m = folium.Map(location=[0, 0], zoom_start=2) - assert m is not None - - -def test_gdal(): - from osgeo import gdal - - release = gdal.VersionInfo("RELEASE_NAME") - assert release # e.g. "3.8.4" - - -def test_pyproj(): - import pyproj - - crs = pyproj.CRS("EPSG:4326") - assert crs.to_epsg() == 4326 - assert "WGS 84" in crs.name - - -def test_shapely(): - from shapely.geometry import Point - - p = Point(0, 0) - assert p.buffer(1).area == pytest.approx(math.pi, abs=0.1) - - -def test_ipyleaflet(): - import ipyleaflet - - m = ipyleaflet.Map() - assert m is not None - - -def test_lonboard(): - import lonboard - - assert lonboard.__version__ - - -# ─── Core scientific stack ──────────────────────────────────── - - -def test_numpy(): - import numpy as np - - assert int(np.array([1, 2, 3]).sum()) == 6 - - -def test_numba(): - from numba import njit - - @njit - def add_one(x): - return x + 1 - - assert add_one(1) == 2 - - -def test_scipy(): - from scipy import stats - - assert stats.norm.cdf(0) == pytest.approx(0.5) - - -def test_pandas(): - import pandas as pd - - df = pd.DataFrame({"a": [1, 2, 3]}) - assert df.shape == (3, 1) - assert df["a"].sum() == 6 - - -def test_geopandas(): - import geopandas as gpd - from shapely.geometry import Point - - gdf = gpd.GeoDataFrame( - {"name": ["a", "b"]}, - geometry=[Point(0, 0), Point(1, 1)], - crs="EPSG:4326", - ) - assert len(gdf) == 2 - assert gdf.crs.to_epsg() == 4326 - - -def test_matplotlib_base(): - import matplotlib - - matplotlib.use("Agg") - import matplotlib.pyplot as plt - - fig, ax = plt.subplots() - ax.plot([0, 1], [0, 1]) - plt.close(fig) - - -def test_xarray(): - import xarray as xr - - da = xr.DataArray([1, 2, 3], dims="x") - assert da.sum().item() == 6 - - -@pytest.mark.filterwarnings("ignore:numpy.ndarray size changed:RuntimeWarning") -def test_netcdf4(tmp_path): - import netCDF4 - - path = tmp_path / "smoke.nc" - with netCDF4.Dataset(path, "w") as ds: - ds.createDimension("x", 3) - v = ds.createVariable("v", "f4", ("x",)) - v[:] = [1.0, 2.0, 3.0] - with netCDF4.Dataset(path, "r") as ds: - assert ds["v"][:].sum() == pytest.approx(6.0) - - -def test_h5py(tmp_path): - import h5py - import numpy as np - - path = tmp_path / "smoke.h5" - with h5py.File(path, "w") as f: - f["arr"] = np.arange(3) - with h5py.File(path, "r") as f: - assert f["arr"][:].sum() == 3 - - -def test_h5netcdf(tmp_path): - import h5netcdf.legacyapi as netCDF4 - import numpy as np - - path = tmp_path / "smoke_h5netcdf.nc" - with netCDF4.Dataset(path, "w") as ds: - ds.createDimension("x", 3) - v = ds.createVariable("v", "f4", ("x",)) - v[:] = np.array([1.0, 2.0, 3.0]) - with netCDF4.Dataset(path, "r") as ds: - assert ds["v"][:].sum() == pytest.approx(6.0) - - -def test_zarr(): - import zarr - - arr = zarr.zeros((3,), chunks=3, dtype="f4") - arr[:] = [1.0, 2.0, 3.0] - assert arr[:].sum() == pytest.approx(6.0) - - -def test_virtualizarr(): - import virtualizarr # noqa: F401 - - assert virtualizarr.__version__ - - -def test_pooch(): - import pooch - - assert pooch.__version__ - - -def test_pyarrow(): - import pyarrow as pa - - arr = pa.array([1, 2, 3]) - assert arr.to_pylist() == [1, 2, 3] - - -def test_bottleneck(): - import bottleneck as bn - - assert bn.nansum([1.0, 2.0, float("nan"), 3.0]) == pytest.approx(6.0) - - -def test_flox(): - import flox - - assert flox.__version__ - - -# ─── Parallel computing ─────────────────────────────────────── - - -def test_dask(): - import dask.array as da - - assert da.ones(10, chunks=5).sum().compute() == pytest.approx(10.0) - - -def test_dask_gateway(): - import dask_gateway # noqa: F401 - - -def test_distributed(): - import distributed - - assert distributed.__version__ - - -# ─── Geo / geoscience ───────────────────────────────────────── - - -def test_dascore(): - import dascore - - assert hasattr(dascore, "__version__") - - -def test_gmt_cli(): - out = _cli_version("gmt") - assert re.match(r"\d+\.\d+", out.strip()) # gmt --version prints bare "6.6.0" - - -def test_obspy(): - from obspy import UTCDateTime - - t = UTCDateTime("2020-01-01T12:30:45") - assert t.year == 2020 - assert t.month == 1 - assert t.hour == 12 - - -def test_pygmt(): - import pygmt - - assert pygmt.__version__ - - -def test_obsplus(): - import obsplus # noqa: F401 - - -# ─── Optimization ───────────────────────────────────────────── - - -def test_cvxpy(): - import cvxpy as cp - - x = cp.Variable() - prob = cp.Problem(cp.Minimize((x - 2) ** 2)) - prob.solve() - assert x.value == pytest.approx(2.0, abs=1e-3) - - -# ─── Visualization ──────────────────────────────────────────── - - -def test_ipympl(): - import ipympl # noqa: F401 - - -def test_hvplot(): - import hvplot # noqa: F401 - - assert hvplot.__version__ - - -def test_holoviews(): - import holoviews as hv - - curve = hv.Curve([1, 2, 3]) - assert curve is not None - - -def test_panel(): - import panel as pn - - assert pn.__version__ - - -def test_vl_convert(): - import vl_convert as vlc - - assert vlc.__version__ - - -# ─── Media & system tools ───────────────────────────────────── - - -def test_ghostscript_cli(): - out = _cli_version("gs", "--version") - assert any(ch.isdigit() for ch in out) - - -def test_ffmpeg_cli(): - out = _cli_version("ffmpeg", "-version") - assert "ffmpeg" in out.lower() - - -# ─── Utilities ──────────────────────────────────────────────── - - -def test_tqdm(): - from tqdm import tqdm - - assert list(tqdm(range(3), disable=True)) == [0, 1, 2] - - -def test_requests(): - import requests - - assert requests.__version__ - - -def test_pyyaml(): - import yaml - - parsed = yaml.safe_load("a: 1\nb: [2, 3]") - assert parsed == {"a": 1, "b": [2, 3]} - - -# ─── Dev tools ──────────────────────────────────────────────── - - -def test_gh_cli(): - out = _cli_version("gh") - assert "gh version" in out.lower() or "github cli" in out.lower() - - -def test_gh_scoped_creds(): - assert shutil.which("gh-scoped-creds") is not None - - -def test_pytest_self(): - # we're in pytest, so importing it must work - assert pytest.__version__ - - -def test_ruff_cli(): - out = _cli_version("ruff") - assert "ruff" in out.lower() - - -# ─── Jupyter stack & extensions ─────────────────────────────── - - -def test_jupyterhub(): - import jupyterhub - - assert jupyterhub.__version__ - - -def test_jupyter_server(): - import jupyter_server - - assert jupyter_server.__version__ - - -def test_jupyterlab(): - import jupyterlab - - assert jupyterlab.__version__ - - -def test_ipykernel(): - import ipykernel - - assert ipykernel.__version__ - - -def test_jupyter_resource_usage(): - import jupyter_resource_usage # noqa: F401 - - -def test_jupyter_ruff(): - import jupyter_ruff # noqa: F401 - - -def test_jupyter_server_proxy(): - import jupyter_server_proxy # noqa: F401 - - -def test_jupyterlab_git(): - import jupyterlab_git # noqa: F401 - - -def test_jupyterlab_myst(): - import jupyterlab_myst # noqa: F401 - - -def test_jupyterlab_code_formatter(): - import jupyterlab_code_formatter # noqa: F401 - - -def test_jupyterlab_pygments(): - import jupyterlab_pygments # noqa: F401 - - -def test_nbdime(): - import nbdime - - assert nbdime.__version__ - - -def test_nbgitpuller(): - import nbgitpuller - - assert nbgitpuller.__version__ - - -# ─── pip packages ───────────────────────────────────────────── - - -def test_earthscope_sdk(): - import earthscope_sdk # noqa: F401 - - -def test_earthscope_cli(): - # `es` is the earthscope-cli entry point - assert shutil.which("es") is not None - - -def test_earthscopestraintools(): - import earthscopestraintools # noqa: F401 - - -def test_jupyterlab_jupyterbook_navigation(): - import jupyterlab_jupyterbook_navigation # noqa: F401 - - -def test_altair(): - import altair as alt - - chart = alt.Chart() - assert chart is not None - - -def test_plotly(): - import plotly - - assert plotly.__version__ - - -def test_polars(): - import polars as pl - - df = pl.DataFrame({"a": [1, 2, 3]}) - assert df.shape == (3, 1) - assert df["a"].sum() == 6 - - -def test_vegafusion(): - import vegafusion # noqa: F401 - - -# ─── Interactive widgets ─────────────────────────────────────── - - -def test_ipywidgets(): - import ipywidgets - - slider = ipywidgets.IntSlider(value=5, min=0, max=10) - assert slider.value == 5 - - -def test_anywidget(): - import anywidget # noqa: F401 - - assert anywidget.__version__ - - -@pytest.mark.filterwarnings( - "ignore:metadata .* was set from the constructor:DeprecationWarning" -) -def test_bqplot(): - import bqplot # noqa: F401 - - assert bqplot.__version__ - - -def test_ipytree(): - from ipytree import Node - - root = Node(name="root") - assert root.name == "root" - - -def test_itables(): - import itables # noqa: F401 - - assert itables.__version__ - - -def test_ipydatagrid(): - import ipydatagrid # noqa: F401 - - assert ipydatagrid.__version__ - - -def test_sidecar(): - from sidecar import Sidecar # noqa: F401 From 8dbbcf4753b15d03c4f7926ee2814b24b858a485 Mon Sep 17 00:00:00 2001 From: spara-earthscope Date: Tue, 18 Aug 2026 13:58:32 -0600 Subject: [PATCH 03/15] added nano, removed seisbench test --- geolab-base/apt.txt | 1 + geolab-base/test_notebook.ipynb | 10 +--------- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/geolab-base/apt.txt b/geolab-base/apt.txt index b2bdaa3..d59bf56 100644 --- a/geolab-base/apt.txt +++ b/geolab-base/apt.txt @@ -4,3 +4,4 @@ git gmt-dcw gmt-gshhg make +nano \ No newline at end of file diff --git a/geolab-base/test_notebook.ipynb b/geolab-base/test_notebook.ipynb index 20c08a8..81ab7df 100644 --- a/geolab-base/test_notebook.ipynb +++ b/geolab-base/test_notebook.ipynb @@ -140,15 +140,7 @@ "id": "062000c0", "metadata": {}, "outputs": [], - "source": [ - "py('dascore')\n", - "cli('gmt', version_flag='--version')\n", - "py('obspy',\n", - " smoke=lambda m: m.UTCDateTime('2020-01-01').timestamp)\n", - "py('obsplus')\n", - "py('pygmt')\n", - "py('seisbench', smoke=lambda m: m.models.PhaseNet())" - ] + "source": "py('dascore')\ncli('gmt', version_flag='--version')\npy('obspy',\n smoke=lambda m: m.UTCDateTime('2020-01-01').timestamp)\npy('obsplus')\npy('pygmt')" }, { "cell_type": "markdown", From 304e9ad3404c5dbc2f79e111f2df9b83b84234dd Mon Sep 17 00:00:00 2001 From: spara-earthscope Date: Thu, 20 Aug 2026 14:56:46 -0600 Subject: [PATCH 04/15] add viz package and test --- geolab-base/environment.yml | 1 + geolab-base/test_notebook.ipynb | 12 +----------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/geolab-base/environment.yml b/geolab-base/environment.yml index dd1240b..91cc549 100644 --- a/geolab-base/environment.yml +++ b/geolab-base/environment.yml @@ -49,6 +49,7 @@ dependencies: - matplotlib-base - graphviz - pygraphviz + - ipycytoscape - altair - hvplot - holoviews diff --git a/geolab-base/test_notebook.ipynb b/geolab-base/test_notebook.ipynb index 81ab7df..881d643 100644 --- a/geolab-base/test_notebook.ipynb +++ b/geolab-base/test_notebook.ipynb @@ -264,17 +264,7 @@ "id": "d7ba172f", "metadata": {}, "outputs": [], - "source": [ - "py('ipywidgets',\n", - " smoke=lambda m: m.IntSlider(value=5, min=0, max=10))\n", - "py('anywidget')\n", - "py('bqplot')\n", - "py('ipytree', smoke=lambda m: m.Node(name='root'))\n", - "py('itables')\n", - "py('ipydatagrid')\n", - "from sidecar import Sidecar # noqa: F401\n", - "py('sidecar')" - ] + "source": "py('ipywidgets',\n smoke=lambda m: m.IntSlider(value=5, min=0, max=10))\npy('anywidget')\npy('bqplot')\npy('ipytree', smoke=lambda m: m.Node(name='root'))\npy('ipycytoscape', smoke=lambda m: m.CytoscapeWidget())\npy('itables')\npy('ipydatagrid')\nfrom sidecar import Sidecar # noqa: F401\npy('sidecar')" }, { "cell_type": "markdown", From 4a7e66c82c21d40a0dba719d9b9bda6ae4c17ada Mon Sep 17 00:00:00 2001 From: spara-earthscope Date: Thu, 20 Aug 2026 15:49:49 -0600 Subject: [PATCH 05/15] add changelog, update version --- geolab-base/CHANGELOG.md | 20 ++++++++++++++++++++ geolab-base/Dockerfile | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 geolab-base/CHANGELOG.md diff --git a/geolab-base/CHANGELOG.md b/geolab-base/CHANGELOG.md new file mode 100644 index 0000000..5d4169f --- /dev/null +++ b/geolab-base/CHANGELOG.md @@ -0,0 +1,20 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +## [1.0.1] + +### Added + +- pin base image to pangeo-base:04bb14b +- add geolab-base version as ENV GEOLAB_VERSION (must be manually edited) +- add graphviz package +- add pygraphviz package +- add ipycytoscape +- added tests for new packages + +### Changed + +- removed test_packages.py +- moved test_notebook functions to test_helpers.py module to make it easier for users to import when writing tests +- updated test_notebook.ipynb to use test_helpers functions diff --git a/geolab-base/Dockerfile b/geolab-base/Dockerfile index 3053acc..625ea21 100644 --- a/geolab-base/Dockerfile +++ b/geolab-base/Dockerfile @@ -20,7 +20,7 @@ FROM pangeo/base-image:04bb14b ARG IMAGE_TITLE="custom-geolab-image" # default if user passes nothing ARG IMAGE_AUTHORS="NoSpecifiedAuthors" # default if user passes nothing -ARG GEOLAB_VERSION="0.9.4" # default if user passes nothing +ARG GEOLAB_VERSION="1.0.1" # default if user passes nothing LABEL org.opencontainers.image.title="${IMAGE_TITLE}" \ org.opencontainers.image.authors="${IMAGE_AUTHORS}" \ From 4283c643274af2029fb932082e39cef55a4d9fa7 Mon Sep 17 00:00:00 2001 From: spara-earthscope Date: Thu, 27 Aug 2026 12:06:44 -0600 Subject: [PATCH 06/15] Stop overriding manual RELEASE_VERSION with a timestamp USE_TIMESTAMP_VERSION was forcing the shared release job to discard any semantic version entered when manually running the release job, always substituting a UTC timestamp instead. --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 70a974f..1603165 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -25,4 +25,4 @@ variables: CONTAINER_REGISTRY_PLATFORM: "AWS-PUB" DOCKERFILE_RELPATH_IS_IMAGE_NAME: "true" GITLAB_HOSTED_RUNNER_SIZE: "saas-linux-medium-amd64" - USE_TIMESTAMP_VERSION: "true" + USE_TIMESTAMP_VERSION: "false" From 36821ec94c731af2b13e5dd155ddd0584fe79b55 Mon Sep 17 00:00:00 2001 From: spara-earthscope Date: Thu, 27 Aug 2026 12:13:37 -0600 Subject: [PATCH 07/15] Pass GEOLAB_VERSION build-arg for geolab-base 0.2.0 release Bakes the release version into the image's org.opencontainers.image.version label via DOCKER_EXTRA_OPTIONS, alongside the existing IMAGE_AUTHORS/PYTHON_VERSION args. --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 1603165..0d98b8f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -17,7 +17,7 @@ include: .images_matrix: - DOCKERFILE_RELPATH: "geolab-base" - DOCKER_EXTRA_OPTIONS: "--build-arg IMAGE_AUTHORS=geolab@earthscope.org --build-arg PYTHON_VERSION=3.12" + DOCKER_EXTRA_OPTIONS: "--build-arg IMAGE_AUTHORS=geolab@earthscope.org --build-arg PYTHON_VERSION=3.12 --build-arg GEOLAB_VERSION=0.2.0" #- DOCKERFILE_RELPATH: "geolab-gpu" From abbc5169ba104a30de39998ee25d4b4a1e5d5973 Mon Sep 17 00:00:00 2001 From: spara-earthscope Date: Thu, 27 Aug 2026 12:21:34 -0600 Subject: [PATCH 08/15] Fail the build if GEOLAB_VERSION is empty Docker doesn't validate ARG values, so an empty --build-arg or ARG default would silently bake an empty version label/env into the image. --- geolab-base/Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/geolab-base/Dockerfile b/geolab-base/Dockerfile index 625ea21..d071d80 100644 --- a/geolab-base/Dockerfile +++ b/geolab-base/Dockerfile @@ -22,6 +22,8 @@ ARG IMAGE_TITLE="custom-geolab-image" # default if user passes nothing ARG IMAGE_AUTHORS="NoSpecifiedAuthors" # default if user passes nothing ARG GEOLAB_VERSION="1.0.1" # default if user passes nothing +RUN test -n "$GEOLAB_VERSION" || (echo "GEOLAB_VERSION must not be empty" >&2 && exit 1) + LABEL org.opencontainers.image.title="${IMAGE_TITLE}" \ org.opencontainers.image.authors="${IMAGE_AUTHORS}" \ org.opencontainers.image.version="${GEOLAB_VERSION}" From 43b611e3a1aa4087cc7a3e0c1575d29803c28c08 Mon Sep 17 00:00:00 2001 From: spara-earthscope Date: Thu, 27 Aug 2026 12:23:14 -0600 Subject: [PATCH 09/15] Remove default value for GEOLAB_VERSION build-arg No default means an omitted --build-arg now fails the guard added in abbc516 instead of silently baking in a stale version. --- geolab-base/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/geolab-base/Dockerfile b/geolab-base/Dockerfile index d071d80..0394f79 100644 --- a/geolab-base/Dockerfile +++ b/geolab-base/Dockerfile @@ -20,7 +20,7 @@ FROM pangeo/base-image:04bb14b ARG IMAGE_TITLE="custom-geolab-image" # default if user passes nothing ARG IMAGE_AUTHORS="NoSpecifiedAuthors" # default if user passes nothing -ARG GEOLAB_VERSION="1.0.1" # default if user passes nothing +ARG GEOLAB_VERSION # no default; must be passed with --build-arg RUN test -n "$GEOLAB_VERSION" || (echo "GEOLAB_VERSION must not be empty" >&2 && exit 1) From 9ed1c3ff4dab4d80350e515368b71884dd45711b Mon Sep 17 00:00:00 2001 From: spara-earthscope Date: Thu, 27 Aug 2026 12:25:49 -0600 Subject: [PATCH 10/15] Document GEOLAB_VERSION build-arg and manual changelog update Adds a step before building the platform image explaining that CHANGELOG.md must be updated by hand and that GEOLAB_VERSION is now required (no default) rather than optional metadata. --- geolab-base/README.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/geolab-base/README.md b/geolab-base/README.md index 7a6d5b1..0c03851 100644 --- a/geolab-base/README.md +++ b/geolab-base/README.md @@ -17,6 +17,7 @@ Steps: - [Running the local testing image](#running-the-local-testing-image) - [Verifying the installed packages](#verifying-the-installed-packages) - [Building and publishing the image](#building-and-publishing-the-image) + - [Setting the version and updating the changelog](#setting-the-version-and-updating-the-changelog) - [Building the platform image](#building-the-platform-image) - [Publishing the platform image](#publishing-the-platform-image) - [Running your published image in GeoLab](#running-your-published-image-in-geolab) @@ -235,6 +236,13 @@ pytest test_packages.py -v Once your configuration files are ready, you build the image locally *for the GeoLab platform* and push it to a container registry so GeoLab can access it. +### Setting the version and updating the changelog + +Before building, decide on a version number for the image, following [semantic versioning](https://semver.org/) (e.g. `1.2.0`). + +- Update `CHANGELOG.md` with a new entry describing what changed in this version. This must be done by hand — it is not generated automatically from commits or the build. +- Pass the same version to the build with the `GEOLAB_VERSION` build-arg (see below). The Dockerfile has no default for it, so the build fails immediately if it is omitted or empty. + ### Building the platform image The `--platform linux/amd64` flag ensures the image runs on the same platform as GeoLab regardless of your own computer architecture. Name the image using your repository username, a descriptive name and tag to track versions, such as `username/my-geolab-image:0.1.0`. @@ -244,10 +252,11 @@ docker build --no-cache -f Dockerfile \ --platform linux/amd64 \ --build-arg IMAGE_TITLE=my-geolab-image \ --build-arg IMAGE_AUTHORS=you@university.edu \ + --build-arg GEOLAB_VERSION=0.1.0 \ --tag username/my-geolab-image:0.1.0 . ``` -Replace `username` with your Docker Hub username (or your registry path), `my-geolab-image` with your image name, and `0.1.0` with your version tag. The `--build-arg` values for `IMAGE_TITLE` and `IMAGE_AUTHORS` are optional but recommended for image metadata. +Replace `username` with your Docker Hub username (or your registry path), `my-geolab-image` with your image name, and `0.1.0` with your version tag. The `--build-arg` values for `IMAGE_TITLE` and `IMAGE_AUTHORS` are optional but recommended for image metadata; `GEOLAB_VERSION` is required and should match the version you added to `CHANGELOG.md` and the tag you build with. It is baked into the image as the `org.opencontainers.image.version` label and as the `GEOLAB_VERSION` environment variable inside the running container. What does `--no-cache` do? It forces Docker to rerun build steps from scratch, ensuring a clean build when publishing. From 1db849c707c7cc3ecf98e14472e576fbd69003e6 Mon Sep 17 00:00:00 2001 From: spara-earthscope Date: Thu, 27 Aug 2026 12:27:25 -0600 Subject: [PATCH 11/15] update gitlab-ci.yml to set version number by CI --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 0d98b8f..59cd12a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -17,7 +17,7 @@ include: .images_matrix: - DOCKERFILE_RELPATH: "geolab-base" - DOCKER_EXTRA_OPTIONS: "--build-arg IMAGE_AUTHORS=geolab@earthscope.org --build-arg PYTHON_VERSION=3.12 --build-arg GEOLAB_VERSION=0.2.0" + DOCKER_EXTRA_OPTIONS: "--build-arg IMAGE_AUTHORS=geolab@earthscope.org --build-arg PYTHON_VERSION=3.12 --build-arg GEOLAB_VERSION=1.0.1" #- DOCKERFILE_RELPATH: "geolab-gpu" From 562b655f19ef1e3f4b3f2641341a753cefec8e52 Mon Sep 17 00:00:00 2001 From: spara-earthscope Date: Thu, 27 Aug 2026 13:32:22 -0600 Subject: [PATCH 12/15] Make GEOLAB_VERSION a pipeline variable instead of a literal Surfaces it as an editable field on GitLab's "Run pipeline" page, so it can be overridden without editing and committing .gitlab-ci.yml for each release. --- .gitlab-ci.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 59cd12a..43079a6 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -17,7 +17,7 @@ include: .images_matrix: - DOCKERFILE_RELPATH: "geolab-base" - DOCKER_EXTRA_OPTIONS: "--build-arg IMAGE_AUTHORS=geolab@earthscope.org --build-arg PYTHON_VERSION=3.12 --build-arg GEOLAB_VERSION=1.0.1" + DOCKER_EXTRA_OPTIONS: "--build-arg IMAGE_AUTHORS=geolab@earthscope.org --build-arg PYTHON_VERSION=3.12 --build-arg GEOLAB_VERSION=${GEOLAB_VERSION}" #- DOCKERFILE_RELPATH: "geolab-gpu" @@ -26,3 +26,8 @@ variables: DOCKERFILE_RELPATH_IS_IMAGE_NAME: "true" GITLAB_HOSTED_RUNNER_SIZE: "saas-linux-medium-amd64" USE_TIMESTAMP_VERSION: "false" + # Version baked into the geolab-base image (org.opencontainers.image.version + # label and GEOLAB_VERSION env var). Override on the "Run pipeline" page to + # set a different version without editing this file; should match the + # RELEASE_VERSION you enter for the release job. + GEOLAB_VERSION: "1.0.1" From 2bf002734ed8df0c530e960fac416e421616bfac Mon Sep 17 00:00:00 2001 From: spara-earthscope Date: Thu, 27 Aug 2026 13:34:26 -0600 Subject: [PATCH 13/15] Remove default value for GEOLAB_VERSION pipeline variable No default forces it to be set explicitly on the "Run pipeline" page for every build; left blank, the Dockerfile guard fails the build instead of silently baking in a stale version. --- .gitlab-ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 43079a6..3ab8741 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -27,7 +27,7 @@ variables: GITLAB_HOSTED_RUNNER_SIZE: "saas-linux-medium-amd64" USE_TIMESTAMP_VERSION: "false" # Version baked into the geolab-base image (org.opencontainers.image.version - # label and GEOLAB_VERSION env var). Override on the "Run pipeline" page to - # set a different version without editing this file; should match the - # RELEASE_VERSION you enter for the release job. - GEOLAB_VERSION: "1.0.1" + # label and GEOLAB_VERSION env var). No default: must be set on the "Run + # pipeline" page for each build, matching the RELEASE_VERSION you enter for + # the release job. Left empty, the Dockerfile's guard fails the build. + GEOLAB_VERSION: "" From 2c39cb9ed9e72df8ed94a161d1199d445c6852f3 Mon Sep 17 00:00:00 2001 From: spara-earthscope Date: Thu, 27 Aug 2026 13:36:19 -0600 Subject: [PATCH 14/15] Document GEOLAB_VERSION as a required GitLab CI pipeline variable Notes that the official image build sets GEOLAB_VERSION on the "Run pipeline" page rather than via a manual --build-arg, and that it has no default there either. --- geolab-base/README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/geolab-base/README.md b/geolab-base/README.md index 0c03851..e4d5620 100644 --- a/geolab-base/README.md +++ b/geolab-base/README.md @@ -243,6 +243,9 @@ Before building, decide on a version number for the image, following [semantic v - Update `CHANGELOG.md` with a new entry describing what changed in this version. This must be done by hand — it is not generated automatically from commits or the build. - Pass the same version to the build with the `GEOLAB_VERSION` build-arg (see below). The Dockerfile has no default for it, so the build fails immediately if it is omitted or empty. +> [!NOTE] +> When the official `geolab-base` image is built through GitLab CI, `GEOLAB_VERSION` is a pipeline variable (also with no default) rather than a `--build-arg` you type by hand. Set it on the "Run pipeline" page for each run, matching the `RELEASE_VERSION` you enter for the release job — leaving it blank fails the build the same way an empty `--build-arg` does locally. + ### Building the platform image The `--platform linux/amd64` flag ensures the image runs on the same platform as GeoLab regardless of your own computer architecture. Name the image using your repository username, a descriptive name and tag to track versions, such as `username/my-geolab-image:0.1.0`. From 0281add615e05388e999605ae0e6835ea69d792f Mon Sep 17 00:00:00 2001 From: spara-earthscope Date: Thu, 27 Aug 2026 13:49:43 -0600 Subject: [PATCH 15/15] set GEOLAB_VERSION=${IMAGE_VERSION} to set version in image --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 45dca43..22984c0 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -17,7 +17,7 @@ include: .images_matrix: - DOCKERFILE_RELPATH: "geolab-base" - DOCKER_EXTRA_OPTIONS: "--build-arg IMAGE_AUTHORS=geolab@earthscope.org --build-arg PYTHON_VERSION=3.12 --build-arg GEOLAB_VERSION=${GEOLAB_VERSION}" + DOCKER_EXTRA_OPTIONS: "--build-arg IMAGE_AUTHORS=geolab@earthscope.org --build-arg PYTHON_VERSION=3.12 --build-arg GEOLAB_VERSION=${IMAGE_VERSION}" #- DOCKERFILE_RELPATH: "geolab-gpu"