From 85070aad3aa7dfea43edb82ccb50e82dc855c374 Mon Sep 17 00:00:00 2001 From: spara-earthscope Date: Tue, 11 Aug 2026 13:56:01 -0600 Subject: [PATCH 01/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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" From 59587f450a0012140588f6f59650406fceddc3bb Mon Sep 17 00:00:00 2001 From: spara-earthscope Date: Thu, 17 Sep 2026 14:10:47 -0500 Subject: [PATCH 16/20] updated README and test_notebook.ipynb --- geolab-base/README.md | 58 +++++++++++++++++++-------------- geolab-base/test_notebook.ipynb | 44 ++++++++++++++++++++++--- 2 files changed, 73 insertions(+), 29 deletions(-) diff --git a/geolab-base/README.md b/geolab-base/README.md index e4d5620..fac8fc9 100644 --- a/geolab-base/README.md +++ b/geolab-base/README.md @@ -75,8 +75,8 @@ Your `my-geolab-image` directory should have the following files: ├── environment.yml ├── requirements.txt ├── start +├── test_helpers.py ├── test_notebook.ipynb -├── test_packages.py └── ... ``` @@ -152,14 +152,14 @@ Some packages are only available on PyPI (Python's package index) and can be ins ```shell # --- EarthScope --- -earthscope-sdk==1.4.1 +earthscope-sdk==1.6.1 earthscope-cli==1.2.0 earthscopestraintools gnss-lib-py <-- NEW ``` > [!TIP] -> Pin versions for packages critical to your workflow (e.g., `earthscope-sdk==1.4.1`). This prevents silent breakage when upstream packages release updates if you rebuild the image. +> Pin versions for packages critical to your workflow (e.g., `earthscope-sdk==1.6.1`). This prevents silent breakage when upstream packages release updates if you rebuild the image. ### Creating a postBuild script @@ -213,22 +213,38 @@ What is the docker run command doing? The `--rm` flag will delete the container ### Verifying the installed packages -The image includes two test options that ensure installed packages import and run. Both are copied into the container at build time, so they are available in the running container. Use them after a build to confirm nothing is broken (a missing system library or version conflict often installs cleanly but fails at import). Adjust as needed for the packages that you added or removed from the build. +The image includes `test_notebook.ipynb`, which ensures installed packages import and run. It is copied into the container at build time, so it is available in the running container. Use it after a build to confirm nothing is broken (a missing system library or version conflict often installs cleanly but fails at import). Adjust as needed for the packages that you added or removed from the build. -In the Jupyter interface at `http://127.0.0.1:8888/lab...`: +The notebook's checks are built on top of `test_helpers.py`, a small module of test helpers (also copied into the container) that the notebook imports rather than duplicating this logic in every cell: -**Option 1 — `test_packages.py` (pytest, fastest).** This runs a minimal API call for each package and prints a pass/fail line per package. +| Function | Use for | What it does | +| --- | --- | --- | +| `py(modname, alias=None, smoke=None)` | Python packages | Imports `modname` and, if given, calls `smoke(mod)` as a minimal sanity check (e.g. constructing an object or calling a function). Records a pass with the package's `__version__`, or a fail with the exception. | +| `cli(cmd, version_flag='--version')` | Command-line tools | Confirms `cmd` is on `$PATH` and responds to `version_flag`. Records a pass with the version string, or a fail if it's missing. | -Create a new Terminal inside a running JupyterLab session (File → New → Terminal): +Each call appends a `(name, status, version, error)` row to the shared `RESULTS` list, which the notebook's final cell renders as a summary table. -```shell -pytest test_packages.py -v +In the Jupyter interface at `http://127.0.0.1:8888/lab...`, open `test_notebook.ipynb` and run all cells (Run → Run All Cells). The notebook is organized into one section per category in `environment.yml`/`requirements.txt` (Cloud & storage, Geospatial, Core scientific stack, etc.), each running `py()`/`cli()` checks for the packages in that category, and ends with a summary table listing the status (and version) of each package, with failures highlighted in red. + +**Adding a test for a new package.** If you add a package to `environment.yml` or `requirements.txt`, add a matching check to `test_notebook.ipynb` so it's covered by the summary table. Pick the section that matches where you added the package (or add a new section), and add a `py()` or `cli()` call. + +For example, adding `seisfetch` (a Python package) to the Geo / geoscience section: + +```python +py('dascore') +cli('gmt', version_flag='--version') +py('obspy', + smoke=lambda m: m.UTCDateTime('2020-01-01').timestamp) +py('obsplus') +py('pygmt') +py('seisfetch', + smoke=lambda m: m.Client()) # replace with a minimal, side-effect-free call ``` -**Option 2 — `test_notebook.ipynb` (interactive).** Open `test_notebook.ipynb`, and run all cells (Run → Run All Cells). The notebook performs the same import-and-exercise checks and ends with a summary table listing the status (and version) of each package, with failures highlighted in red. +The `smoke` argument is optional but recommended — a bare import can succeed even when the package is broken in ways that only show up on first use (e.g. a missing compiled extension). Pick a call that exercises the package without hitting the network or requiring credentials, since the notebook may run without EarthScope services available locally. > [!TIP] -> A failure here points at the package, not your notebook code — usually a missing system dependency (add it to `apt.txt`) or a version conflict between conda and pip packages. If you add or remove a package in `environment.yml` or `requirements.txt`, update the tests to match. +> A failure here points at the package, not your notebook code — usually a missing system dependency (add it to `apt.txt`) or a version conflict between conda and pip packages. If you add or remove a package in `environment.yml` or `requirements.txt`, update the notebook to match. --- @@ -236,16 +252,6 @@ 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. - -> [!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`. @@ -255,13 +261,15 @@ 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; `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. +> [TIP] +> When building an image, setting a version in the image tag is a best practice. Versioning can track image changes to allow reproducibility. If a version tag, e.g. 0.1.0, Docker will automatically tag the image as `latest`. + +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; image tag versions should be added to a `CHANGELOG.md` file which list the changes associated to that version. -What does `--no-cache` do? It forces Docker to rerun build steps from scratch, ensuring a clean build when publishing. +`--no-cache` forces Docker to rerun build steps from scratch, ensuring a clean build when publishing. > [!NOTE] > Images will be cached by different systems, including the image repository and GeoLab. If you are using a version (e.g. `0.1.0`) you should increment it for each build to avoid inadvertently using cached copies. @@ -279,7 +287,7 @@ Replace the details to match the --tag value in the build command. > [!TIP] > If the push results in an error, make sure you are logged into Docker Hub using `docker login` if needed. -Many other image repositories exist. If you use AWS ECR, follow these [instructions](https://docs.aws.amazon.com/AmazonECR/latest/userguide/docker-push-ecr-image.html). +Many other image repositories exist. If you use AWS ECR, follow these [instructions](https://docs.aws.amazon.com/AmazonECR/latest/userguide/docker-push-ecr-image.html). --- diff --git a/geolab-base/test_notebook.ipynb b/geolab-base/test_notebook.ipynb index 881d643..b7269f0 100644 --- a/geolab-base/test_notebook.ipynb +++ b/geolab-base/test_notebook.ipynb @@ -28,10 +28,21 @@ "id": "618283a2", "metadata": {}, "outputs": [], - "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}')" + "source": [ + "import sys\n", + "\n", + "import test_helpers as test\n", + "from test_helpers import RESULTS, cli, py\n", + "\n", + "test.RESULTS.clear()\n", + "\n", + "print(f'Python {sys.version}')\n", + "print(f'sys.prefix: {sys.prefix}')" + ] }, { "cell_type": "markdown", + "id": "586f3ed6", "metadata": {}, "source": [ "## Cloud & storage" @@ -54,6 +65,7 @@ }, { "cell_type": "markdown", + "id": "cbc56927", "metadata": {}, "source": [ "## Geospatial" @@ -83,6 +95,7 @@ }, { "cell_type": "markdown", + "id": "91dba753", "metadata": {}, "source": [ "## Core scientific stack" @@ -129,6 +142,7 @@ }, { "cell_type": "markdown", + "id": "a86a0e2a", "metadata": {}, "source": [ "## Geo / geoscience" @@ -140,10 +154,18 @@ "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')" + "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", + "id": "4d531eaf", "metadata": {}, "source": [ "## Utilities" @@ -167,6 +189,7 @@ }, { "cell_type": "markdown", + "id": "77abdcce", "metadata": {}, "source": [ "## Dev tools" @@ -175,6 +198,7 @@ { "cell_type": "code", "execution_count": null, + "id": "a7ccd9e7", "metadata": {}, "outputs": [], "source": [ @@ -186,6 +210,7 @@ }, { "cell_type": "markdown", + "id": "14ba80ce", "metadata": {}, "source": [ "## Jupyter stack & extensions" @@ -264,7 +289,18 @@ "id": "d7ba172f", "metadata": {}, "outputs": [], - "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')" + "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('ipycytoscape', smoke=lambda m: m.CytoscapeWidget())\n", + "py('itables')\n", + "py('ipydatagrid')\n", + "from sidecar import Sidecar # noqa: F401\n", + "py('sidecar')" + ] }, { "cell_type": "markdown", @@ -316,4 +352,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} \ No newline at end of file +} From 561562840da0b40b4c8940da0bfe9479eb523d3f Mon Sep 17 00:00:00 2001 From: spara-earthscope Date: Thu, 17 Sep 2026 19:54:37 -0500 Subject: [PATCH 17/20] merged README with GeoLab docs Building a Custom Web Image --- geolab-base/README.md | 351 ++++++++++++++++++++++++------------------ 1 file changed, 204 insertions(+), 147 deletions(-) diff --git a/geolab-base/README.md b/geolab-base/README.md index fac8fc9..ad07f93 100644 --- a/geolab-base/README.md +++ b/geolab-base/README.md @@ -1,169 +1,156 @@ # Building Custom GeoLab Images -GeoLab environments run as **containers** based on **images** — self-contained packages that include an operating system, software libraries, Python packages, and more. This guide documents the steps using Docker and git to create such an image. +A GeoLab image is a complete, prepackaged computing environment that runs in JupyterLab, bundling common geophysics Python and scientific packages together. Starting a GeoLab session launches an image. This guide walks through building an image with a customized environment, using Docker and git. Steps: -- [Installing and logging into Docker](#installing-and-logging-into-docker) -- [Installing git](#installing-git) -- [Copy the base template](#copy-the-base-template) -- [Configuring a custom image](#configuring-a-custom-image) - - [Installing system software with apt](#installing-system-software-with-apt) - - [Installing Conda packages](#installing-conda-packages) - - [Installing pip packages](#installing-pip-packages) - - [Creating a postBuild script](#creating-a-postbuild-script) -- [Building and testing the image locally](#building-and-testing-the-image-locally) - - [Building the local testing image](#building-the-local-testing-image) - - [Running the local testing image](#running-the-local-testing-image) +- [How It Works](#how-it-works) +- [Before Starting](#before-starting) +- [Step 1: Get the Template](#step-1-get-the-template) +- [Step 2: Add Your Packages](#step-2-add-your-packages) + - [environment.yml: Your Main Package List](#environmentyml-your-main-package-list) + - [requirements.txt: Packages Only on PyPI](#requirementstxt-packages-only-on-pypi) + - [apt.txt: System Software (Rarely Needed)](#apttxt-system-software-rarely-needed) + - [postBuild: One-Time Setup Commands](#postbuild-one-time-setup-commands) +- [Step 3: Build and Test Locally](#step-3-build-and-test-locally) - [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) +- [Step 4: Publish Your Image](#step-4-publish-your-image) + - [Rebuild for GeoLab's platform](#rebuild-for-geolabs-platform) + - [Push the image](#push-the-image) + - [Publishing to GitHub or AWS Image Repositories](#publishing-to-github-or-aws-image-repositories) +- [Step 5: Launch It in GeoLab](#step-5-launch-it-in-geolab) +- [Making Changes Later](#making-changes-later) +- [Troubleshooting Package Installation](#troubleshooting-package-installation) +- [Quick Reference](#quick-reference) +- [Getting a Personal Access Token (for GHCR)](#getting-a-personal-access-token-for-ghcr) > [!NOTE] -> These instructions are written for macOS, Linux and similar systems. While the same steps can be executed in Windows the details will vary. +> These instructions are written for macOS, Linux and similar systems. While the same steps can be executed on Windows the details will vary. --- -## Installing and logging into Docker +## How It Works -Docker Desktop is the recommended option to build and run images on your computer and to publish them for access by others (and GeoLab itself). Follow the [instructions](https://docs.docker.com/get-started/introduction/) to install it. If you are new to Docker, images, and containers, we recommend working through the [getting-started modules](https://docs.docker.com/get-started/introduction/#modules) to learn how to build, run, and publish images. +Think of an **image** as a recipe, with each Python package as an ingredient. Building the image is cooking the meal from the recipe. A **container** is that meal served on a plate. The recipe doesn't change and you can make the same meal over and over — GeoLab does the same thing, launching a fresh container from your image every time. -> [!TIP] -> Alternatively, Docker Engine (with build plugins) can be used; this is what is often installed on Linux systems. +The `geolab-base` template (used in the steps below) is based on a Pangeo image (`pangeo/base-image`) as its starting point; a custom image is created by modifying the build on top of it. -After starting Docker Desktop, log into Docker, creating an account if needed. This enables pushing (aka publishing) an image to Docker Hub, Docker’s image repository where it is available to others. +Install Python packages in an image by editing plaintext files that list the required software. `Docker` reads those files and builds the image. The image must then be published in an image repository so GeoLab can access it: -> [!TIP] -> Docker Hub is just one of many repositories for images, many others exist and can be used, but Docker Hub is easiest because it is the default repository for components in the ecosystem, including in GeoLab. +``` +Edit config files → Docker builds → Image → Push image to repository → GeoLab runs it +``` --- -## Installing git - -Git is needed to make a copy of the GeoLab repository containing the template for the base image for building custom images. The git program is often already installed, or easily installed on macOS and Linux systems. Follow the [instructions](https://github.com/git-guides/install-git) to check for and install git if needed. +## Before Starting ---- +Two pieces of software must be installed on **your computer**: -## Copy the base template +1. **Docker Desktop.** Download it at [docker.com](https://www.docker.com/products/docker-desktop/), install it, and leave it running in the background. -The `geolab-base` directory in the GeoLab repository contains all the files needed to build a custom GeoLab image. Open a terminal and execute the following commands: - -```shell -cd ~ -git clone --depth 1 https://github.com/EarthScope/GeoLab.git -cp -R GeoLab/geolab-base my-geolab-image -cd my-geolab-image -``` + > [!TIP] + > If you're new to Docker, images, and containers, work through the [getting-started modules](https://docs.docker.com/get-started/introduction/#modules) to learn how to build, run, and publish images. Alternatively, Docker Engine (with build plugins) can be used instead of Docker Desktop; this is what is often installed on Linux systems. -This set of commands does the following: + After starting Docker Desktop, log into Docker, creating an account if needed. This is what enables pushing (publishing) an image to Docker Hub, Docker's image repository, so it's available to others (and to GeoLab itself). -1. Change from the current directory to your home directory. -2. Use git to copy the GeoLab repository, only getting the current state (`--depth 1`) -3. Copy the geolab-base directory to a new directory in your home directory. -4. Change into the newly created directory with a copy of the image template files. +2. **Git client**, to download the GeoLab Dockerfile template. Use the operating system's package manager to install one, or follow the [instructions](https://github.com/git-guides/install-git) to check for and install git if needed. -Your `my-geolab-image` directory should have the following files: +Verify Docker and git are installed and working by opening a terminal and running: -```shell -./my-geolab-image -├── apt.txt -├── README.md -├── Dockerfile -├── environment.yml -├── requirements.txt -├── start -├── test_helpers.py -├── test_notebook.ipynb -└── ... +``` +docker --version +git --version ``` -> [!TIP] -> For image development it is recommended to keep your files in a git repository to track changes, share with others, etc. The sooner this is started the better. This is the right stage to commit these starting files to a new repo. - ---- - -## Configuring a custom image - -A container image is a snapshot of a complete computing environment. When GeoLab launches, it starts a container from such an image. The `geolab-base` image is based on a Pangeo image (`pangeo/base-image`) as its starting point. A custom image can be created by modifying the build of this image. - -The `Dockerfile` specifies how the image is built. To create a custom image, edit the configuration files before building: +If they print a version number, they are installed and working. +In addition to the required software, a **GitHub** account at [github.com](https://github.com), a Docker account, or an AWS account is needed for publishing the image and making it available to GeoLab. -| File | What it controls | -| ------------------ | ------------------------------------------------------------ | -| `apt.txt` | System-level software (installed via `apt`) | -| `environment.yml` | Python from Conda packages and channels | -| `requirements.txt` | Python packages from PyPI (installed via `pip`) | -| `postBuild` | Commands to run after the build completes (create if needed) | -| `start` | Entrypoint script; normally leave unchanged | +--- +## Step 1: Get the Template -### Installing system software with apt +EarthScope provides a starter template. Download it using git to set up a working folder: -`apt` is the Ubuntu package manager — it installs system-level tools like compilers, runtime libraries, and command-line utilities. Edit `apt.txt` to add any packages you need, one per line. Best practice is to list packages in alphabetical order, which makes it easier to find a specific package. +```shell +git clone --depth 1 https://github.com/EarthScope/GeoLab.git +cp -R GeoLab/geolab-base my-geolab-image +cd my-geolab-image +``` -**Example:** Adding Node.js (nodejs) and npm to `apt.txt`: +The `my-geolab-image` folder contains these files: -```shell -build-essential -gfortran -git -gmt-dcw -gmt-gshhg -make -nodejs <-- NEW -npm <-- NEW +``` +my-geolab-image/ +├── Dockerfile ← do not edit this +├── environment.yml ← add your conda packages here +├── requirements.txt ← add PyPI-only packages here +├── apt.txt ← add system software here (rarely needed) +├── start ← do not edit this +├── test_helpers.py ← Python module with testing functions for packages +└── test_notebook.ipynb ← interactive version of the smoke test ``` +> [!NOTE] +> The only files to edit are `environment.yml`, `requirements.txt`, and `apt.txt` (plus an optional `postBuild` script, covered below). Everything else is set up for you. + > [!TIP] -> Only add packages here that aren't available through conda. Most scientific Python libraries are better managed in environment.yml. +> For image development it is recommended to keep your files in a git repository to track changes, share with others, etc. The sooner this is started the better — this is the right stage to commit these starting files to a new repo. + +--- -### Installing Conda packages +## Step 2: Add Your Packages -Conda manages Python (and non-Python) packages within isolated environments. Edit `environment.yml` to add packages by name under the appropriate section (the sections are comments, not important for conda). Always use the `conda-forge` channel for the broadest package availability unless otherwise specified in the package’s installation instructions. +### environment.yml: Your Main Package List -**Example:** Adding SimPEG (`simpeg`) to the Geophysics section of `environment.yml`: +Add conda Python packages from `conda-forge` here. Open the file and add packages under the `dependencies` section: ```yaml -... +channels: + - conda-forge + - nodefaults dependencies: - ... - # ── Geophysics ────────────────────────────────────── - - dascore + - python=3.12 + # --- Geophysics --- - obspy - - obsplus - - gmt - pygmt - - simpeg <-- NEW - ... + # --- Geospatial --- + - cartopy + - geopandas + # add your packages below: + - my-package-name ``` -> [!TIP] -> Prefer conda packages over pip when a package is available in both. Conda resolves environment-wide dependencies more reliably. +Conda packages are preferred, because conda checks that everything works together before installing and reduces the possibility of dependency conflicts among packages. -### Installing pip packages +### requirements.txt: Packages Only on PyPI -Some packages are only available on PyPI (Python's package index) and can be installed with `pip`. Add them to `requirements.txt`, one per line. You can pin a specific version with `==` to ensure reproducibility. +Some packages aren't available through conda-forge and must be installed from PyPI. Add them here, one per line. You can pin a specific version with `==` to ensure reproducibility: -**Example:** Adding `gnss-lib-py`: +``` +earthscope-sdk==1.4.1 +seisbench +``` -```shell -# --- EarthScope --- -earthscope-sdk==1.6.1 -earthscope-cli==1.2.0 -earthscopestraintools -gnss-lib-py <-- NEW +> [!TIP] +> Pin versions for packages critical to your workflow (e.g., `earthscope-sdk==1.4.1`). This prevents silent breakage from upstream releases if you rebuild the image later. + +### apt.txt: System Software (Rarely Needed) + +Most scientific packages go in `environment.yml`. Only use `apt.txt` for low-level system tools that can't be installed any other way: + +``` +build-essential +git ``` > [!TIP] -> Pin versions for packages critical to your workflow (e.g., `earthscope-sdk==1.6.1`). This prevents silent breakage when upstream packages release updates if you rebuild the image. +> Only add packages here that aren't available through conda. Most scientific Python libraries are better managed in `environment.yml`. -### Creating a postBuild script +### postBuild: One-Time Setup Commands -A postBuild script runs automatically after all packages are installed. Use it for one-time setup steps that can't be expressed as package installs — for example, configuring tools, downloading data files, or logging build metadata. +A `postBuild` script (create the file if you need one) runs automatically after all packages are installed. Use it for one-time setup steps that can't be expressed as package installs — for example, configuring tools, downloading data files, or logging build metadata. **Example:** Create a `postBuild` file to record the build timestamp: @@ -181,39 +168,37 @@ echo "Build stage completed successfully." --- -## Building and testing the image locally +## Step 3: Build and Test Locally -Building and testing the image locally is the fastest way to iterate on changes and fix issues. Services that are only available in GeoLab such as direct access to repositories in S3 storage cannot be tested locally. +Building and testing the image locally is the fastest way to iterate on changes and fix issues. Services that are only available in GeoLab, such as direct access to repositories in S3 storage, cannot be tested locally. -### Building the local testing image +**Build the image:** ```shell docker build -f Dockerfile --tag my-geolab-image:0.1.0 . ``` -You may omit the `:0.1.0` part of the tag if you wish. +This reads the config files and assembles the image; it can take several minutes the first time. You may omit the `:0.1.0` part of the tag if you wish. > [!TIP] -> Do not publish and try to run this image in GeoLab, it may not be the correct platform. See the next section for instructions to build the platform image. - -### Running the local testing image +> Do not publish and try to run this image in GeoLab — it may not be the correct platform. See [Step 4](#step-4-publish-your-image) for building the platform image. -Use Docker to run the image with this command: +**Run it locally:** ```shell docker run --rm -p 8888:8888 my-geolab-image:0.1.0 ``` -Copy the URL from the log that looks like: `http://127.0.0.1:8888/lab?token...` (with the token value) and connect to the container with a web browser. +The `--rm` flag deletes the container created from the image after the run completes, keeping repeated commands from piling up containers. The `-p 8888:8888` option maps the container's network port so your computer can reach it. -What is the docker run command doing? The `--rm` flag will delete the container created from the image after the run completes, keeping repeated commands from creating a new container on each run. The `-p 8888:8888` option maps the network port for the service in the container so the local computer can reach it. +Look in the output for a line like `http://127.0.0.1:8888/lab?token=...` and copy that URL into a browser — a JupyterLab session will open. > [!TIP] > This image is not running in the GeoLab platform, so any features only available in GeoLab will not work from this local environment. ### Verifying the installed packages -The image includes `test_notebook.ipynb`, which ensures installed packages import and run. It is copied into the container at build time, so it is available in the running container. Use it after a build to confirm nothing is broken (a missing system library or version conflict often installs cleanly but fails at import). Adjust as needed for the packages that you added or removed from the build. +The image includes `test_notebook.ipynb`, which ensures installed packages import and run. It is copied into the container at build time, so it is available in the running container. Use it after a build to confirm nothing is broken (a missing system library or version conflict often installs cleanly but fails at import). Adjust it as needed for the packages that you added or removed from the build. The notebook's checks are built on top of `test_helpers.py`, a small module of test helpers (also copied into the container) that the notebook imports rather than duplicating this logic in every cell: @@ -224,7 +209,7 @@ The notebook's checks are built on top of `test_helpers.py`, a small module of t Each call appends a `(name, status, version, error)` row to the shared `RESULTS` list, which the notebook's final cell renders as a summary table. -In the Jupyter interface at `http://127.0.0.1:8888/lab...`, open `test_notebook.ipynb` and run all cells (Run → Run All Cells). The notebook is organized into one section per category in `environment.yml`/`requirements.txt` (Cloud & storage, Geospatial, Core scientific stack, etc.), each running `py()`/`cli()` checks for the packages in that category, and ends with a summary table listing the status (and version) of each package, with failures highlighted in red. +In the JupyterLab session at `http://127.0.0.1:8888/lab...`, open `test_notebook.ipynb` and run all cells (Run → Run All Cells). The notebook is organized into one section per category in `environment.yml`/`requirements.txt` (Cloud & storage, Geospatial, Core scientific stack, etc.), each running `py()`/`cli()` checks for the packages in that category, and ends with a summary table listing the status (and version) of each package, with failures highlighted in red. **Adding a test for a new package.** If you add a package to `environment.yml` or `requirements.txt`, add a matching check to `test_notebook.ipynb` so it's covered by the summary table. Pick the section that matches where you added the package (or add a new section), and add a `py()` or `cli()` call. @@ -244,17 +229,17 @@ py('seisfetch', The `smoke` argument is optional but recommended — a bare import can succeed even when the package is broken in ways that only show up on first use (e.g. a missing compiled extension). Pick a call that exercises the package without hitting the network or requiring credentials, since the notebook may run without EarthScope services available locally. > [!TIP] -> A failure here points at the package, not your notebook code — usually a missing system dependency (add it to `apt.txt`) or a version conflict between conda and pip packages. If you add or remove a package in `environment.yml` or `requirements.txt`, update the notebook to match. +> A failure here points at the package, not your notebook code — usually a missing system dependency (add it to `apt.txt`) or a version conflict between conda and pip packages. If something fails, it usually means a package name is misspelled or a version is unavailable, so go back to `environment.yml` or `requirements.txt`, fix it, and rebuild. --- -## Building and publishing the image +## Step 4: Publish Your Image -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. +Once your configuration files are ready and the local test passes, rebuild the image *for the GeoLab platform* and push it to a container registry so GeoLab can access it. -### Building the platform image +### Rebuild for GeoLab's platform -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`. +GeoLab runs on Linux (`linux/amd64`). Depending on your computer's architecture (e.g. Apple Silicon), you may need to rebuild the image for that platform. Name the image using your repository username, a descriptive name, and a tag to track versions, such as `username/my-geolab-image:0.1.0`. ```shell docker build --no-cache -f Dockerfile \ @@ -264,44 +249,116 @@ docker build --no-cache -f Dockerfile \ --tag username/my-geolab-image:0.1.0 . ``` -> [TIP] -> When building an image, setting a version in the image tag is a best practice. Versioning can track image changes to allow reproducibility. If a version tag, e.g. 0.1.0, Docker will automatically tag the image as `latest`. - -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; image tag versions should be added to a `CHANGELOG.md` file which list the changes associated to that version. +> [!TIP] +> Setting a version in the image tag is a best practice — it lets you track changes and reproduce a specific build later. Record what changed for each version in a `CHANGELOG.md` file. Without an explicit tag, Docker defaults to tagging the image `latest`, which makes it hard to tell which build is actually running. -`--no-cache` forces Docker to rerun build steps from scratch, ensuring a clean build when publishing. +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. > [!NOTE] -> Images will be cached by different systems, including the image repository and GeoLab. If you are using a version (e.g. `0.1.0`) you should increment it for each build to avoid inadvertently using cached copies. +> **Why `--platform linux/amd64`?** GeoLab runs on Linux. If you're on a Mac with Apple Silicon, your local machine uses a different architecture — this flag ensures the image works on GeoLab regardless of what you built it on. -### Publishing the platform image +`--no-cache` forces Docker to rerun build steps from scratch, ensuring a clean build when publishing. -Push the image to Docker Hub, AWS ECR, or another registry so GeoLab can access it. If you have logged into your Docker account, you can push the image to Docker Hub with this command: +### Push the image + +If you created an account using Docker Desktop, pushing to Docker Hub does not require additional authentication: ```shell docker push username/my-geolab-image:0.1.0 ``` -Replace the details to match the --tag value in the build command. +Replace the details to match the `--tag` value in the build command. By default, images published to Docker Hub are public and available for use with GeoLab. > [!TIP] -> If the push results in an error, make sure you are logged into Docker Hub using `docker login` if needed. +> If the push results in an error, make sure you are logged into Docker Hub using `docker login`. Many other image repositories exist. If you use AWS ECR, follow these [instructions](https://docs.aws.amazon.com/AmazonECR/latest/userguide/docker-push-ecr-image.html). +### Publishing to GitHub or AWS Image Repositories + +Alternatives to Docker Hub include GitHub Container Registry (ghcr) or AWS Elastic Container Registry (ECR). Choosing an image repository depends on your requirements. GitHub features tight integration with CI (Continuous Integration) through GitHub Actions that can trigger an image build and push to ghcr, automating the process through a `pull request`. AWS ECR offers cloud-scale uploads and downloads to support multiple instances of GeoLab requested by hundreds of users or more. + +Both ghcr and ECR have more stringent authorization practices and controls over publicly available images. For a step-by-step walkthrough for pushing images to either repository, go to [Pushing Images to GitHub or AWS ECR](/geolab/advanced-topics/environments/pushing-to-ghcr-ecr) for detailed instructions. + +--- + +## Step 5: Launch It in GeoLab + +1. Go to [earthscope.org/data/geolab](https://www.earthscope.org/data/geolab/) and click **Launch GeoLab** (or open the [Hub Control Panel](https://geolab.earthscope.cloud/hub/home/) directly). +2. Log in with your [EarthScope account](https://www.earthscope.org/user/login). +3. If a **Stop My Server** button appears, click it first. +4. Click **Start My Server**. +5. Under **Environment**, choose **Other**. +6. In the **Custom image** field, enter the image name from your registry, e.g. `ghcr.io/your-github-username/my-geolab-image:0.1.0` or `username/my-geolab-image:0.1.0` (for Docker Hub). If the image is not in Docker Hub, use the full image reference. +7. Click **Start**. + +GeoLab will pull your image and launch a session from it. The first launch takes a bit longer while it downloads; after that it's cached and starts quickly. + +--- + +## Making Changes Later + +Edit your config files, then rebuild and push with a new version number: + +```shell +docker build --no-cache -f Dockerfile \ + --platform linux/amd64 \ + --tag ghcr.io/your-github-username/my-geolab-image:0.1.1 . + +docker push ghcr.io/your-github-username/my-geolab-image:0.1.1 +``` + +> [!TIP] +> Always use a new version number (`0.1.1`, `0.1.2`, etc.) when you rebuild. Images are cached by different systems, including the image repository and GeoLab — if you reuse the same tag, GeoLab may load the old cached version instead of your new one. + --- -## Running your published image in GeoLab +## Troubleshooting Package Installation + +In general, it's best practice to install packages using the conda package manager for the GeoLab image. Conda checks packages for dependencies, which helps ensure that conflicts are resolved in the environment. Conda has a search function to discover packages. -1. Open [GeoLab's Hub Control Panel](https://geolab.earthscope.cloud/hub/home/) - * Login with your [EarthScope account](https://www.earthscope.org/user/login) - * If "Stop My Server" button is visible, select it to stop your current server - * Select "Start My Server" button -2. Choose **Environment → Other** -3. In **Custom image** enter the image name from your registry, e.g.: `username/my-geolab-image:0.1.0` (for Docker Hub) -4. Select **Start** +Some packages are only available on PyPI and are installed with the pip package manager, which also has a search function. > [!TIP] -> If the image is not in Docker Hub, the **Custom image** value should be the full image reference. +> Keep in mind that installation name and import name can be different (for example, `scikit-learn` vs `sklearn`). + +**Find if a conda package is available:** + +```shell +conda search -c conda-forge #PackageName (e.g., seisbench) +``` + +**Find if a package is available on PyPI:** + +```shell +python -m pip index versions #PackageName (e.g., seisbench) +``` + +--- + +## Quick Reference + +| What you want to do | Where to do it | +|---|---| +| Add a Python package | `environment.yml` under `dependencies` | +| Add a PyPI-only package | `requirements.txt` | +| Add a system tool | `apt.txt` | +| Run one-time setup commands | `postBuild` (create if needed) | +| Build locally for testing | `docker build --tag my-geolab-image:0.1.0 .` | +| Run locally | `docker run --rm -p 8888:8888 my-geolab-image:0.1.0` | +| Test packages | Run `test_notebook.ipynb` (see [Step 3](#step-3-build-and-test-locally)) | +| Build for GeoLab | `docker build --no-cache --platform linux/amd64 --tag username/image:version .` | +| Publish | `docker push username/my-geolab-image:0.1.0` | + +--- + +## Getting a Personal Access Token (for GHCR) + +Before you can push images to GHCR, you need a **Personal Access Token (PAT)** with package permissions: + +1. Go to **GitHub → Settings → Developer settings → Personal access tokens → Tokens (classic)**. +2. Click **Generate new token (classic)**. +3. Give it a name (e.g. `geolab-image`), set an expiration, and check the **`write:packages`** scope. +4. Click **Generate token** and copy it, as you won't be able to see it again. -GeoLab will pull and launch your custom environment. The first launch may take a bit longer while the image is transferred from the repository. +Save your token somewhere safe (a password manager works well). You'll use it to log in to the registry when publishing. From 4c7c1244a21c6700dbf7e84412c638be01a0c071 Mon Sep 17 00:00:00 2001 From: spara-earthscope Date: Fri, 18 Sep 2026 10:47:42 -0500 Subject: [PATCH 18/20] Add README2.md: snapshot of geolab-base README from gmt branch Copy of the restructured geolab-base/README.md from the gmt branch (Step-numbered guide, test_helper.py-based test docs, quick reference, GHCR/ECR publishing, troubleshooting sections) for reference on this branch. Co-Authored-By: Claude Sonnet 5 --- geolab-base/README2.md | 440 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 440 insertions(+) create mode 100644 geolab-base/README2.md diff --git a/geolab-base/README2.md b/geolab-base/README2.md new file mode 100644 index 0000000..59bf413 --- /dev/null +++ b/geolab-base/README2.md @@ -0,0 +1,440 @@ +# Building Custom GeoLab Images + +GeoLab environments run as **containers** based on **images** — self-contained packages that include an operating system, software libraries, Python packages, and more. This guide documents the steps using Docker and git to create such an image. + +Steps: + +- [Before you start](#before-you-start) + - [Installing and logging into Docker](#installing-and-logging-into-docker) + - [Installing git](#installing-git) +- [Step 1: Copying the base template](#step-1-copying-the-base-template) +- [Step 2: Configuring a custom image](#step-2-configuring-a-custom-image) + - [Installing system software with apt](#installing-system-software-with-apt) + - [Installing Conda packages](#installing-conda-packages) + - [Installing pip packages](#installing-pip-packages) + - [Creating a postBuild script](#creating-a-postbuild-script) +- [Step 3: Building and testing the image locally](#step-3-building-and-testing-the-image-locally) + - [Building the local testing image](#building-the-local-testing-image) + - [Running the local testing image](#running-the-local-testing-image) + - [Verifying the installed packages](#verifying-the-installed-packages) + - [Adding a test](#adding-a-test) +- [Step 4: Building and publishing the image](#step-4-building-and-publishing-the-image) + - [Building the platform image](#building-the-platform-image) + - [Publishing the platform image](#publishing-the-platform-image) + - [Publishing to GitHub or AWS image repositories](#publishing-to-github-or-aws-image-repositories) +- [Step 5: Running your published image in GeoLab](#step-5-running-your-published-image-in-geolab) +- [Making changes later](#making-changes-later) +- [Troubleshooting package installation](#troubleshooting-package-installation) +- [Quick reference](#quick-reference) +- [Getting a Personal Access Token (for GHCR)](#getting-a-personal-access-token-for-ghcr) + +> [!NOTE] +> These instructions are written for macOS, Linux and similar systems. While the same steps can be executed in Windows the details will vary. + +--- + +## Before you start + +Building a custom image means: edit a few plaintext configuration files, have Docker build an image from them, then push that image to a repository so GeoLab can pull and run it. You'll need Docker and git installed locally, and (to publish) an account on Docker Hub, GHCR, or another image registry. + +### Installing and logging into Docker + +Docker Desktop is the recommended option to build and run images on your computer and to publish them for access by others (and GeoLab itself). Follow the [instructions](https://docs.docker.com/get-started/introduction/) to install it. If you are new to Docker, images, and containers, we recommend working through the [getting-started modules](https://docs.docker.com/get-started/introduction/#modules) to learn how to build, run, and publish images. + +> [!TIP] +> Alternatively, Docker Engine (with build plugins) can be used; this is what is often installed on Linux systems. + +After starting Docker Desktop, log into Docker, creating an account if needed. This enables pushing (aka publishing) an image to Docker Hub, Docker’s image repository where it is available to others. + +Confirm Docker is installed and running: + +```shell +docker --version +``` + +> [!TIP] +> Docker Hub is just one of many repositories for images, many others exist and can be used, but Docker Hub is easiest because it is the default repository for Docker. + +--- + +### Installing git + +Git is needed to make a copy of the GeoLab repository containing the template for the base image for building custom images. The git program is often already installed, or easily installed on macOS and Linux systems. Follow the [instructions](https://github.com/git-guides/install-git) to check for and install git if needed. + +Confirm git is installed: + +```shell +git --version +``` + +--- + +## Step 1: Copying the base template + +The `geolab-base` directory in the GeoLab repository contains all the files needed to build a custom GeoLab image. Open a terminal and execute the following commands: + +```shell +cd ~ +git clone --depth 1 https://github.com/EarthScope/GeoLab.git +cp -R GeoLab/geolab-base my-geolab-image +cd my-geolab-image +``` + +This set of commands does the following: + +1. Change from the current directory to your home directory. +2. Use git to copy the GeoLab repository, only getting the current state (`--depth 1`) +3. Copy the geolab-base directory to a new directory in your home directory. +4. Change into the newly created directory with a copy of the image template files. + +Your `my-geolab-image` directory should have the following files: + +```shell +./my-geolab-image +├── apt.txt +├── README.md +├── Dockerfile +├── environment.yml +├── requirements.txt +├── start +├── test_helper.py +├── test_notebook.ipynb +├── test_packages.py +└── ... +``` + +> [!TIP] +> For image development it is recommended to keep your files in a git repository to track changes, share with others, etc. The sooner this is started the better. This is the right stage to commit these starting files to a new repo. + +--- + +## Step 2: Configuring a custom image + +A container image is a snapshot of a complete computing environment. When GeoLab launches, it starts a container from such an image. The `geolab-base` image is based on a Pangeo image (`pangeo/base-image`) as its starting point. A custom image can be created by modifying the build of this image. + +The `Dockerfile` specifies how the image is built. To create a custom image, edit the configuration files before building: + +| File | What it controls | +| ------------------ | ------------------------------------------------------------ | +| `apt.txt` | System-level software (installed via `apt`) | +| `environment.yml` | Python from Conda packages and channels | +| `requirements.txt` | Python packages from PyPI (installed via `pip`) | +| `postBuild` | Commands to run after the build completes (create if needed) | +| `start` | Entrypoint script; normally leave unchanged | + +### Installing system software with apt + +`apt` is the Ubuntu package manager — it installs system-level tools like compilers, runtime libraries, and command-line utilities. Edit `apt.txt` to add any packages you need, one per line. Best practice is to list packages in alphabetical order, which makes it easier to find a specific package. + +**Example:** Adding Node.js (nodejs) and npm to `apt.txt`: + +```shell +build-essential +gfortran +git +gmt-dcw +gmt-gshhg +make +nodejs <-- NEW +npm <-- NEW +``` + +> [!TIP] +> Only add packages here that aren't available through conda. Most scientific Python libraries are better managed in environment.yml. + +### Installing Conda packages + +Conda manages Python (and non-Python) packages within isolated environments. Edit `environment.yml` to add packages by name under the appropriate section (the sections are comments, not important for conda). Always use the `conda-forge` channel for the broadest package availability unless otherwise specified in the package’s installation instructions. + +**Example:** Adding SimPEG (`simpeg`) to the Geophysics section of `environment.yml`: + +```yaml +... +dependencies: + ... + # ── Geophysics ────────────────────────────────────── + - dascore + - obspy + - obsplus + - gmt + - pygmt + - simpeg <-- NEW + ... +``` + +> [!TIP] +> Prefer conda packages over pip when a package is available in both. Conda resolves environment-wide dependencies more reliably. + +### Installing pip packages + +Some packages are only available on PyPI (Python's package index) and can be installed with `pip`. Add them to `requirements.txt`, one per line. You can pin a specific version with `==` to ensure reproducibility. + +**Example:** Adding `gnss-lib-py`: + +```shell +# --- EarthScope --- +earthscope-sdk==1.6.1 +earthscope-cli==1.2.0 +earthscopestraintools +gnss-lib-py <-- NEW +``` + +> [!TIP] +> Pin versions for packages critical to your workflow (e.g., `earthscope-sdk==1.6.1`). This prevents silent breakage when upstream packages release updates if you rebuild the image. + +### Creating a postBuild script + +A postBuild script runs automatically after all packages are installed. Use it for one-time setup steps that can't be expressed as package installs — for example, configuring tools, downloading data files, or logging build metadata. + +**Example:** Create a `postBuild` file to record the build timestamp: + +```shell +#!/bin/bash +set -euo pipefail # fail fast: on command error, unset variable, or any pipeline stage failing + +echo "--- Running post-build commands ---" + +# Record when this image was built +date > ${CONDA_DIR}/etc/build_timestamp + +echo "Build stage completed successfully." +``` + +--- + +## Step 3: Building and testing the image locally + +Building and testing the image locally is the fastest way to iterate on changes and fix issues. Services that are only available in GeoLab such as direct access to repositories in S3 storage cannot be tested locally. + +### Building the local testing image + +```shell +docker build -f Dockerfile --tag my-geolab-image:0.1.0 . +``` + +You may omit the `:0.1.0` part of the tag but a version tag is recommended to track changes to an image. + +> [!TIP] +> Do not publish and try to run this image in GeoLab, it may not be the correct platform. See the next section for instructions to build the platform image. + +### Running the local testing image + +Use Docker to run the image with this command: + +```shell +docker run --rm -p 8888:8888 my-geolab-image:0.1.0 +``` + +Copy the URL from the log that looks like: `http://127.0.0.1:8888/lab?token...` (with the token value) and connect to the container with a web browser. + +What is the docker run command doing? The `--rm` flag will delete the container created from the image after the run completes, keeping repeated commands from creating a new container on each run. The `-p 8888:8888` option maps the network port for the service in the container so the local computer can reach it. + +> [!TIP] +> This image is not running in the GeoLab platform, so any features only available in GeoLab will not work from this local environment. + +### Verifying the installed packages + +The image includes two test options that ensure installed packages import and run. Both are copied into the container at build time, so they are available in the running container. Use them after a build to confirm nothing is broken (a missing system library or version conflict often installs cleanly but fails at import). Adjust as needed for the packages that you added or removed from the build. + +In the Jupyter interface at `http://127.0.0.1:8888/lab...`: + +**Option 1 — `test_packages.py` (pytest, fastest).** This runs a minimal API call for each package and prints a pass/fail line per package. + +Create a new Terminal inside a running JupyterLab session (File → New → Terminal): + +```shell +pytest test_packages.py -v +``` + +**Option 2 — `test_notebook.ipynb` (interactive).** Open `test_notebook.ipynb`, and run all cells (Run → Run All Cells). The notebook performs the same import-and-exercise checks and ends with a summary table listing the status (and version) of each package, with failures highlighted in red. + +> [!TIP] +> A failure here points at the package, not your notebook code — usually a missing system dependency (add it to `apt.txt`) or a version conflict between conda and pip packages. If you add or remove a package in `environment.yml` or `requirements.txt`, update the tests to match, as described next. + +### Adding a test + +Whenever you add a package to `environment.yml`, `requirements.txt`, or `apt.txt`, add a matching check to both test files so a future build failure is caught immediately instead of silently passing verification. A good test does more than import the package — it exercises one minimal API call, since a broken ABI or missing system library often lets the import succeed but fails on first use. + +CLI-tool checks (`_cli_version()` in `test_packages.py`, `cli()` in `test_notebook.ipynb`) both run through a single shared helper, `test_helper.py`, so a CLI tool's install is verified the same way from either entry point: + +```python +# test_helper.py +def cli_version(cmd, version_flag="--version"): + """Run `cmd ` and return its output. + + Raises FileNotFoundError if `cmd` isn't on $PATH. + """ + ... +``` + +**In `test_packages.py`:** add a `test_()` function under the relevant section (`# ─── Geophysics ───` etc.): + +```python +def test_simpeg(): + import simpeg + + assert simpeg.__version__ +``` + +For a command-line tool rather than a Python package, call `cli_version()` from `test_helper.py` (already imported at the top of the file): + +```python +def test_nodejs_cli(): + out = cli_version("node") + assert out # e.g. "v20.11.0" +``` + +**In `test_notebook.ipynb`:** add the matching call to the code cell under the same section heading, using the notebook's `py()` helper for Python packages or `cli()` for command-line tools (both of the notebook's helpers are defined in the Setup cell, and `cli()` itself calls `cli_version()` from `test_helper.py`): + +```python +py('simpeg') +cli('node') +``` + +`py()` and `cli()` accept the same kind of minimal smoke check as the pytest helpers — see the existing calls in the notebook for examples that pass an `alias` or a `smoke=` callback. + +Re-run `pytest test_packages.py -v` and the notebook before publishing the image, to confirm the new package (and everything else) still passes. + +> [!TIP] +> Keep the two test files in sync. Pytest is faster for local iteration on the command line; the notebook is what you'll re-run inside a live GeoLab session to debug an environment issue there. Both rely on `test_helper.py` for CLI checks, so a fix there applies to both. + +--- + +## Step 4: Building and publishing the image + +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. + +### 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`. + +```shell +docker build --no-cache -f Dockerfile \ + --platform linux/amd64 \ + --build-arg IMAGE_TITLE=my-geolab-image \ + --build-arg IMAGE_AUTHORS=you@university.edu \ + --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. + +What does `--no-cache` do? It forces Docker to rerun build steps from scratch, ensuring a clean build when publishing. + +> [!NOTE] +> Images will be cached by different systems, including the image repository and GeoLab. If you are using a version (e.g. `0.1.0`) you should increment it for each build to avoid inadvertently using cached copies. + +### Publishing the platform image + +Push the image to Docker Hub, AWS ECR, or another registry so GeoLab can access it. If you have logged into your Docker account, you can push the image to Docker Hub with this command: + +```shell +docker push username/my-geolab-image:0.1.0 +``` + +Replace the details to match the --tag value in the build command. + +> [!TIP] +> If the push results in an error, make sure you are logged into Docker Hub using `docker login` if needed. + +Many other image repositories exist. If you use AWS ECR, follow these [instructions](https://docs.aws.amazon.com/AmazonECR/latest/userguide/docker-push-ecr-image.html). + +### Publishing to GitHub or AWS image repositories + +Docker Hub is the simplest option, but two alternatives are common for teams: GitHub Container Registry (GHCR) and AWS Elastic Container Registry (ECR). + +- **GHCR** integrates tightly with GitHub Actions, so a build-and-push to GHCR can be triggered automatically from a pull request or a push to a branch — useful if several people maintain the same image. +- **AWS ECR** is built for cloud-scale pulls, which matters if the image is used by many GeoLab users at once. + +Both GHCR and ECR have more stringent authorization and access controls than Docker Hub's public images. To push to GHCR, tag and push using the `ghcr.io` registry host and your GitHub username or organization: + +```shell +docker build --no-cache -f Dockerfile \ + --platform linux/amd64 \ + --tag ghcr.io/username/my-geolab-image:0.1.0 . + +docker push ghcr.io/username/my-geolab-image:0.1.0 +``` + +Authenticating to GHCR from the command line requires a personal access token — see [Getting a Personal Access Token (for GHCR)](#getting-a-personal-access-token-for-ghcr) below. For AWS ECR, follow the [ECR push instructions](https://docs.aws.amazon.com/AmazonECR/latest/userguide/docker-push-ecr-image.html), which walk through creating a repository and authenticating with `aws ecr get-login-password`. + +--- + +## Step 5: Running your published image in GeoLab + +1. Open [GeoLab's Hub Control Panel](https://geolab.earthscope.cloud/hub/home/) + * Login with your [EarthScope account](https://www.earthscope.org/user/login) + * If "Stop My Server" button is visible, select it to stop your current server + * Select "Start My Server" button +2. Choose **Environment → Other** +3. In **Custom image** enter the image name from your registry, e.g.: `username/my-geolab-image:0.1.0` (for Docker Hub) +4. Select **Start** + +> [!TIP] +> If the image is not in Docker Hub, the **Custom image** value should be the full image reference. + +GeoLab will pull and launch your custom environment. The first launch may take a bit longer while the image is transferred from the repository. + +--- + +## Making changes later + +To change a published image, edit the configuration files as before, then rebuild and push with a **new** version number: + +```shell +docker build --no-cache -f Dockerfile \ + --platform linux/amd64 \ + --tag ghcr.io/username/my-geolab-image:0.1.1 . + +docker push ghcr.io/username/my-geolab-image:0.1.1 +``` + +> [!NOTE] +> Always use a new version number (`0.1.1`, `0.1.2`, etc.) when you rebuild. If you reuse the same tag, GeoLab (and other systems) may load the old cached version instead of your new one. + +--- + +## Troubleshooting package installation + +If you're not sure whether a package is available, or under what name, check before adding it to a config file. Conda is the safer choice when a package is available both ways, since it checks packages for dependency conflicts across the whole environment rather than installing in isolation. + +- Search conda-forge: `conda search -c conda-forge ` (e.g. `seisbench`) +- Search PyPI: `python -m pip index versions ` (e.g. `seisbench`) + +The name used to *install* a package isn't always the name used to *import* it — for example, `scikit-learn` is installed via that name but imported as `sklearn`, and `opencv-python` is imported as `cv2`. Keep this in mind both when adding a package and when [adding a test](#adding-a-test) for it. + +--- + +## Quick reference + +| Task | Location / command | +| ------------------------- | --------------------------------------------------------------------------------------- | +| Add a Conda package | `environment.yml`, under `dependencies` | +| Add a PyPI-only package | `requirements.txt` | +| Add a system tool | `apt.txt` | +| Run a one-time setup step | `postBuild` | +| Add a package test | `test_packages.py` and `test_notebook.ipynb` (see [Adding a test](#adding-a-test)) | +| Build locally for testing | `docker build -f Dockerfile --tag my-geolab-image:0.1.0 .` | +| Run locally | `docker run --rm -p 8888:8888 my-geolab-image:0.1.0` | +| Verify installed packages | `pytest test_packages.py -v`, or run `test_notebook.ipynb` | +| Build for GeoLab | `docker build --no-cache --platform linux/amd64 --tag username/my-geolab-image:0.1.0 .` | +| Publish | `docker push username/my-geolab-image:0.1.0` | +| Run a published image | GeoLab → **Environment → Other** → **Custom image** | + +--- + +## Getting a Personal Access Token (for GHCR) + +Pushing to GitHub Container Registry from the command line requires a personal access token instead of a password. + +1. On GitHub, go to **Settings → Developer settings → Personal access tokens → Tokens (classic)**. +2. Select **Generate new token (classic)**. +3. Give it a name (e.g. `geolab-image`), set an expiration, and enable the **`write:packages`** scope. +4. Select **Generate token**, and copy it immediately — GitHub only shows it once. + +Save the token somewhere safe (a password manager works well). Use it to log in to GHCR before pushing: + +```shell +docker login ghcr.io -u username +``` + +Enter the personal access token as the password when prompted. From 20accc60b8720269c3b27193511fb6f51c603307 Mon Sep 17 00:00:00 2001 From: spara-earthscope Date: Fri, 18 Sep 2026 12:09:51 -0500 Subject: [PATCH 19/20] check links, add process image, review text --- geolab-base/README.md | 28 +-- geolab-base/README2.md | 440 ---------------------------------- geolab-base/build_process.png | Bin 0 -> 31269 bytes 3 files changed, 13 insertions(+), 455 deletions(-) delete mode 100644 geolab-base/README2.md create mode 100644 geolab-base/build_process.png diff --git a/geolab-base/README.md b/geolab-base/README.md index ad07f93..a55cbf2 100644 --- a/geolab-base/README.md +++ b/geolab-base/README.md @@ -31,15 +31,13 @@ Steps: ## How It Works -Think of an **image** as a recipe, with each Python package as an ingredient. Building the image is cooking the meal from the recipe. A **container** is that meal served on a plate. The recipe doesn't change and you can make the same meal over and over — GeoLab does the same thing, launching a fresh container from your image every time. +Think of an **image** as a recipe, with each Python package as an ingredient. Building the image is cooking the meal from the recipe. A **container** is that meal served on a plate. The recipe doesn't change and you can make the same meal over and over, GeoLab does the same thing, launching a fresh container from your image every time. The `geolab-base` template (used in the steps below) is based on a Pangeo image (`pangeo/base-image`) as its starting point; a custom image is created by modifying the build on top of it. Install Python packages in an image by editing plaintext files that list the required software. `Docker` reads those files and builds the image. The image must then be published in an image repository so GeoLab can access it: -``` -Edit config files → Docker builds → Image → Push image to repository → GeoLab runs it -``` +![Edit config files, Docker builds, Image, Push image to repository, GeoLab runs it](./build_process.png) --- @@ -96,7 +94,7 @@ my-geolab-image/ > The only files to edit are `environment.yml`, `requirements.txt`, and `apt.txt` (plus an optional `postBuild` script, covered below). Everything else is set up for you. > [!TIP] -> For image development it is recommended to keep your files in a git repository to track changes, share with others, etc. The sooner this is started the better — this is the right stage to commit these starting files to a new repo. +> For image development it is recommended to keep your files in a git repository to track changes, share with others, etc. The sooner this is started the better, this is the right stage to commit these starting files to a new repo. --- @@ -150,7 +148,7 @@ git ### postBuild: One-Time Setup Commands -A `postBuild` script (create the file if you need one) runs automatically after all packages are installed. Use it for one-time setup steps that can't be expressed as package installs — for example, configuring tools, downloading data files, or logging build metadata. +A `postBuild` script (create the file if you need one) runs automatically after all packages are installed. Use it for one-time setup steps that can't be expressed as package installs, for example, configuring tools, downloading data files, or logging build metadata. **Example:** Create a `postBuild` file to record the build timestamp: @@ -181,7 +179,7 @@ docker build -f Dockerfile --tag my-geolab-image:0.1.0 . This reads the config files and assembles the image; it can take several minutes the first time. You may omit the `:0.1.0` part of the tag if you wish. > [!TIP] -> Do not publish and try to run this image in GeoLab — it may not be the correct platform. See [Step 4](#step-4-publish-your-image) for building the platform image. +> Do not publish and try to run this image in GeoLab, it may not be the correct platform. See [Step 4](#step-4-publish-your-image) for building the platform image. **Run it locally:** @@ -191,7 +189,7 @@ docker run --rm -p 8888:8888 my-geolab-image:0.1.0 The `--rm` flag deletes the container created from the image after the run completes, keeping repeated commands from piling up containers. The `-p 8888:8888` option maps the container's network port so your computer can reach it. -Look in the output for a line like `http://127.0.0.1:8888/lab?token=...` and copy that URL into a browser — a JupyterLab session will open. +Look in the output for a line like `http://127.0.0.1:8888/lab?token=...` and copy that URL into a browser, a JupyterLab session will open. > [!TIP] > This image is not running in the GeoLab platform, so any features only available in GeoLab will not work from this local environment. @@ -226,10 +224,10 @@ py('seisfetch', smoke=lambda m: m.Client()) # replace with a minimal, side-effect-free call ``` -The `smoke` argument is optional but recommended — a bare import can succeed even when the package is broken in ways that only show up on first use (e.g. a missing compiled extension). Pick a call that exercises the package without hitting the network or requiring credentials, since the notebook may run without EarthScope services available locally. +The `smoke` argument is optional but recommended, a bare import can succeed even when the package is broken in ways that only show up on first use (e.g. a missing compiled extension). Pick a call that exercises the package without hitting the network or requiring credentials, since the notebook may run without EarthScope services available locally. > [!TIP] -> A failure here points at the package, not your notebook code — usually a missing system dependency (add it to `apt.txt`) or a version conflict between conda and pip packages. If something fails, it usually means a package name is misspelled or a version is unavailable, so go back to `environment.yml` or `requirements.txt`, fix it, and rebuild. +> A failure here points at the package, not your notebook code, usually a missing system dependency (add it to `apt.txt`) or a version conflict between conda and pip packages. If something fails, it usually means a package name is misspelled or a version is unavailable, so go back to `environment.yml` or `requirements.txt`, fix it, and rebuild. --- @@ -250,12 +248,12 @@ docker build --no-cache -f Dockerfile \ ``` > [!TIP] -> Setting a version in the image tag is a best practice — it lets you track changes and reproduce a specific build later. Record what changed for each version in a `CHANGELOG.md` file. Without an explicit tag, Docker defaults to tagging the image `latest`, which makes it hard to tell which build is actually running. +> Setting a version in the image tag is a best practice, it lets you track changes and reproduce a specific build later. Record what changed for each version in a `CHANGELOG.md` file. Without an explicit tag, Docker defaults to tagging the image `latest`, which makes it hard to tell which build is actually running. 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. > [!NOTE] -> **Why `--platform linux/amd64`?** GeoLab runs on Linux. If you're on a Mac with Apple Silicon, your local machine uses a different architecture — this flag ensures the image works on GeoLab regardless of what you built it on. +> **Why `--platform linux/amd64`?** GeoLab runs on Linux. If you're on a Mac with Apple Silicon, your local machine uses a different architecture, this flag ensures the image works on GeoLab regardless of what you built it on. `--no-cache` forces Docker to rerun build steps from scratch, ensuring a clean build when publishing. @@ -278,13 +276,13 @@ Many other image repositories exist. If you use AWS ECR, follow these [instructi Alternatives to Docker Hub include GitHub Container Registry (ghcr) or AWS Elastic Container Registry (ECR). Choosing an image repository depends on your requirements. GitHub features tight integration with CI (Continuous Integration) through GitHub Actions that can trigger an image build and push to ghcr, automating the process through a `pull request`. AWS ECR offers cloud-scale uploads and downloads to support multiple instances of GeoLab requested by hundreds of users or more. -Both ghcr and ECR have more stringent authorization practices and controls over publicly available images. For a step-by-step walkthrough for pushing images to either repository, go to [Pushing Images to GitHub or AWS ECR](/geolab/advanced-topics/environments/pushing-to-ghcr-ecr) for detailed instructions. +Both ghcr and ECR have more stringent authorization practices and controls over publicly available images. For a step-by-step walkthrough for pushing images to either repository, go to [Pushing Images to GitHub or AWS ECR](https://docs.earthscope.org/geolab/advanced-topics/environments/pushing-to-ghcr-ecr) for detailed instructions. --- ## Step 5: Launch It in GeoLab -1. Go to [earthscope.org/data/geolab](https://www.earthscope.org/data/geolab/) and click **Launch GeoLab** (or open the [Hub Control Panel](https://geolab.earthscope.cloud/hub/home/) directly). +1. Go to [earthscope.org/data/geolab](https://www.earthscope.org/data/geolab/) and click **Launch GeoLab**. 2. Log in with your [EarthScope account](https://www.earthscope.org/user/login). 3. If a **Stop My Server** button appears, click it first. 4. Click **Start My Server**. @@ -309,7 +307,7 @@ docker push ghcr.io/your-github-username/my-geolab-image:0.1.1 ``` > [!TIP] -> Always use a new version number (`0.1.1`, `0.1.2`, etc.) when you rebuild. Images are cached by different systems, including the image repository and GeoLab — if you reuse the same tag, GeoLab may load the old cached version instead of your new one. +> Always use a new version number (`0.1.1`, `0.1.2`, etc.) when you rebuild. Images are cached by different systems, including the image repository and GeoLab, if you reuse the same tag, GeoLab may load the old cached version instead of your new one. --- diff --git a/geolab-base/README2.md b/geolab-base/README2.md deleted file mode 100644 index 59bf413..0000000 --- a/geolab-base/README2.md +++ /dev/null @@ -1,440 +0,0 @@ -# Building Custom GeoLab Images - -GeoLab environments run as **containers** based on **images** — self-contained packages that include an operating system, software libraries, Python packages, and more. This guide documents the steps using Docker and git to create such an image. - -Steps: - -- [Before you start](#before-you-start) - - [Installing and logging into Docker](#installing-and-logging-into-docker) - - [Installing git](#installing-git) -- [Step 1: Copying the base template](#step-1-copying-the-base-template) -- [Step 2: Configuring a custom image](#step-2-configuring-a-custom-image) - - [Installing system software with apt](#installing-system-software-with-apt) - - [Installing Conda packages](#installing-conda-packages) - - [Installing pip packages](#installing-pip-packages) - - [Creating a postBuild script](#creating-a-postbuild-script) -- [Step 3: Building and testing the image locally](#step-3-building-and-testing-the-image-locally) - - [Building the local testing image](#building-the-local-testing-image) - - [Running the local testing image](#running-the-local-testing-image) - - [Verifying the installed packages](#verifying-the-installed-packages) - - [Adding a test](#adding-a-test) -- [Step 4: Building and publishing the image](#step-4-building-and-publishing-the-image) - - [Building the platform image](#building-the-platform-image) - - [Publishing the platform image](#publishing-the-platform-image) - - [Publishing to GitHub or AWS image repositories](#publishing-to-github-or-aws-image-repositories) -- [Step 5: Running your published image in GeoLab](#step-5-running-your-published-image-in-geolab) -- [Making changes later](#making-changes-later) -- [Troubleshooting package installation](#troubleshooting-package-installation) -- [Quick reference](#quick-reference) -- [Getting a Personal Access Token (for GHCR)](#getting-a-personal-access-token-for-ghcr) - -> [!NOTE] -> These instructions are written for macOS, Linux and similar systems. While the same steps can be executed in Windows the details will vary. - ---- - -## Before you start - -Building a custom image means: edit a few plaintext configuration files, have Docker build an image from them, then push that image to a repository so GeoLab can pull and run it. You'll need Docker and git installed locally, and (to publish) an account on Docker Hub, GHCR, or another image registry. - -### Installing and logging into Docker - -Docker Desktop is the recommended option to build and run images on your computer and to publish them for access by others (and GeoLab itself). Follow the [instructions](https://docs.docker.com/get-started/introduction/) to install it. If you are new to Docker, images, and containers, we recommend working through the [getting-started modules](https://docs.docker.com/get-started/introduction/#modules) to learn how to build, run, and publish images. - -> [!TIP] -> Alternatively, Docker Engine (with build plugins) can be used; this is what is often installed on Linux systems. - -After starting Docker Desktop, log into Docker, creating an account if needed. This enables pushing (aka publishing) an image to Docker Hub, Docker’s image repository where it is available to others. - -Confirm Docker is installed and running: - -```shell -docker --version -``` - -> [!TIP] -> Docker Hub is just one of many repositories for images, many others exist and can be used, but Docker Hub is easiest because it is the default repository for Docker. - ---- - -### Installing git - -Git is needed to make a copy of the GeoLab repository containing the template for the base image for building custom images. The git program is often already installed, or easily installed on macOS and Linux systems. Follow the [instructions](https://github.com/git-guides/install-git) to check for and install git if needed. - -Confirm git is installed: - -```shell -git --version -``` - ---- - -## Step 1: Copying the base template - -The `geolab-base` directory in the GeoLab repository contains all the files needed to build a custom GeoLab image. Open a terminal and execute the following commands: - -```shell -cd ~ -git clone --depth 1 https://github.com/EarthScope/GeoLab.git -cp -R GeoLab/geolab-base my-geolab-image -cd my-geolab-image -``` - -This set of commands does the following: - -1. Change from the current directory to your home directory. -2. Use git to copy the GeoLab repository, only getting the current state (`--depth 1`) -3. Copy the geolab-base directory to a new directory in your home directory. -4. Change into the newly created directory with a copy of the image template files. - -Your `my-geolab-image` directory should have the following files: - -```shell -./my-geolab-image -├── apt.txt -├── README.md -├── Dockerfile -├── environment.yml -├── requirements.txt -├── start -├── test_helper.py -├── test_notebook.ipynb -├── test_packages.py -└── ... -``` - -> [!TIP] -> For image development it is recommended to keep your files in a git repository to track changes, share with others, etc. The sooner this is started the better. This is the right stage to commit these starting files to a new repo. - ---- - -## Step 2: Configuring a custom image - -A container image is a snapshot of a complete computing environment. When GeoLab launches, it starts a container from such an image. The `geolab-base` image is based on a Pangeo image (`pangeo/base-image`) as its starting point. A custom image can be created by modifying the build of this image. - -The `Dockerfile` specifies how the image is built. To create a custom image, edit the configuration files before building: - -| File | What it controls | -| ------------------ | ------------------------------------------------------------ | -| `apt.txt` | System-level software (installed via `apt`) | -| `environment.yml` | Python from Conda packages and channels | -| `requirements.txt` | Python packages from PyPI (installed via `pip`) | -| `postBuild` | Commands to run after the build completes (create if needed) | -| `start` | Entrypoint script; normally leave unchanged | - -### Installing system software with apt - -`apt` is the Ubuntu package manager — it installs system-level tools like compilers, runtime libraries, and command-line utilities. Edit `apt.txt` to add any packages you need, one per line. Best practice is to list packages in alphabetical order, which makes it easier to find a specific package. - -**Example:** Adding Node.js (nodejs) and npm to `apt.txt`: - -```shell -build-essential -gfortran -git -gmt-dcw -gmt-gshhg -make -nodejs <-- NEW -npm <-- NEW -``` - -> [!TIP] -> Only add packages here that aren't available through conda. Most scientific Python libraries are better managed in environment.yml. - -### Installing Conda packages - -Conda manages Python (and non-Python) packages within isolated environments. Edit `environment.yml` to add packages by name under the appropriate section (the sections are comments, not important for conda). Always use the `conda-forge` channel for the broadest package availability unless otherwise specified in the package’s installation instructions. - -**Example:** Adding SimPEG (`simpeg`) to the Geophysics section of `environment.yml`: - -```yaml -... -dependencies: - ... - # ── Geophysics ────────────────────────────────────── - - dascore - - obspy - - obsplus - - gmt - - pygmt - - simpeg <-- NEW - ... -``` - -> [!TIP] -> Prefer conda packages over pip when a package is available in both. Conda resolves environment-wide dependencies more reliably. - -### Installing pip packages - -Some packages are only available on PyPI (Python's package index) and can be installed with `pip`. Add them to `requirements.txt`, one per line. You can pin a specific version with `==` to ensure reproducibility. - -**Example:** Adding `gnss-lib-py`: - -```shell -# --- EarthScope --- -earthscope-sdk==1.6.1 -earthscope-cli==1.2.0 -earthscopestraintools -gnss-lib-py <-- NEW -``` - -> [!TIP] -> Pin versions for packages critical to your workflow (e.g., `earthscope-sdk==1.6.1`). This prevents silent breakage when upstream packages release updates if you rebuild the image. - -### Creating a postBuild script - -A postBuild script runs automatically after all packages are installed. Use it for one-time setup steps that can't be expressed as package installs — for example, configuring tools, downloading data files, or logging build metadata. - -**Example:** Create a `postBuild` file to record the build timestamp: - -```shell -#!/bin/bash -set -euo pipefail # fail fast: on command error, unset variable, or any pipeline stage failing - -echo "--- Running post-build commands ---" - -# Record when this image was built -date > ${CONDA_DIR}/etc/build_timestamp - -echo "Build stage completed successfully." -``` - ---- - -## Step 3: Building and testing the image locally - -Building and testing the image locally is the fastest way to iterate on changes and fix issues. Services that are only available in GeoLab such as direct access to repositories in S3 storage cannot be tested locally. - -### Building the local testing image - -```shell -docker build -f Dockerfile --tag my-geolab-image:0.1.0 . -``` - -You may omit the `:0.1.0` part of the tag but a version tag is recommended to track changes to an image. - -> [!TIP] -> Do not publish and try to run this image in GeoLab, it may not be the correct platform. See the next section for instructions to build the platform image. - -### Running the local testing image - -Use Docker to run the image with this command: - -```shell -docker run --rm -p 8888:8888 my-geolab-image:0.1.0 -``` - -Copy the URL from the log that looks like: `http://127.0.0.1:8888/lab?token...` (with the token value) and connect to the container with a web browser. - -What is the docker run command doing? The `--rm` flag will delete the container created from the image after the run completes, keeping repeated commands from creating a new container on each run. The `-p 8888:8888` option maps the network port for the service in the container so the local computer can reach it. - -> [!TIP] -> This image is not running in the GeoLab platform, so any features only available in GeoLab will not work from this local environment. - -### Verifying the installed packages - -The image includes two test options that ensure installed packages import and run. Both are copied into the container at build time, so they are available in the running container. Use them after a build to confirm nothing is broken (a missing system library or version conflict often installs cleanly but fails at import). Adjust as needed for the packages that you added or removed from the build. - -In the Jupyter interface at `http://127.0.0.1:8888/lab...`: - -**Option 1 — `test_packages.py` (pytest, fastest).** This runs a minimal API call for each package and prints a pass/fail line per package. - -Create a new Terminal inside a running JupyterLab session (File → New → Terminal): - -```shell -pytest test_packages.py -v -``` - -**Option 2 — `test_notebook.ipynb` (interactive).** Open `test_notebook.ipynb`, and run all cells (Run → Run All Cells). The notebook performs the same import-and-exercise checks and ends with a summary table listing the status (and version) of each package, with failures highlighted in red. - -> [!TIP] -> A failure here points at the package, not your notebook code — usually a missing system dependency (add it to `apt.txt`) or a version conflict between conda and pip packages. If you add or remove a package in `environment.yml` or `requirements.txt`, update the tests to match, as described next. - -### Adding a test - -Whenever you add a package to `environment.yml`, `requirements.txt`, or `apt.txt`, add a matching check to both test files so a future build failure is caught immediately instead of silently passing verification. A good test does more than import the package — it exercises one minimal API call, since a broken ABI or missing system library often lets the import succeed but fails on first use. - -CLI-tool checks (`_cli_version()` in `test_packages.py`, `cli()` in `test_notebook.ipynb`) both run through a single shared helper, `test_helper.py`, so a CLI tool's install is verified the same way from either entry point: - -```python -# test_helper.py -def cli_version(cmd, version_flag="--version"): - """Run `cmd ` and return its output. - - Raises FileNotFoundError if `cmd` isn't on $PATH. - """ - ... -``` - -**In `test_packages.py`:** add a `test_()` function under the relevant section (`# ─── Geophysics ───` etc.): - -```python -def test_simpeg(): - import simpeg - - assert simpeg.__version__ -``` - -For a command-line tool rather than a Python package, call `cli_version()` from `test_helper.py` (already imported at the top of the file): - -```python -def test_nodejs_cli(): - out = cli_version("node") - assert out # e.g. "v20.11.0" -``` - -**In `test_notebook.ipynb`:** add the matching call to the code cell under the same section heading, using the notebook's `py()` helper for Python packages or `cli()` for command-line tools (both of the notebook's helpers are defined in the Setup cell, and `cli()` itself calls `cli_version()` from `test_helper.py`): - -```python -py('simpeg') -cli('node') -``` - -`py()` and `cli()` accept the same kind of minimal smoke check as the pytest helpers — see the existing calls in the notebook for examples that pass an `alias` or a `smoke=` callback. - -Re-run `pytest test_packages.py -v` and the notebook before publishing the image, to confirm the new package (and everything else) still passes. - -> [!TIP] -> Keep the two test files in sync. Pytest is faster for local iteration on the command line; the notebook is what you'll re-run inside a live GeoLab session to debug an environment issue there. Both rely on `test_helper.py` for CLI checks, so a fix there applies to both. - ---- - -## Step 4: Building and publishing the image - -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. - -### 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`. - -```shell -docker build --no-cache -f Dockerfile \ - --platform linux/amd64 \ - --build-arg IMAGE_TITLE=my-geolab-image \ - --build-arg IMAGE_AUTHORS=you@university.edu \ - --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. - -What does `--no-cache` do? It forces Docker to rerun build steps from scratch, ensuring a clean build when publishing. - -> [!NOTE] -> Images will be cached by different systems, including the image repository and GeoLab. If you are using a version (e.g. `0.1.0`) you should increment it for each build to avoid inadvertently using cached copies. - -### Publishing the platform image - -Push the image to Docker Hub, AWS ECR, or another registry so GeoLab can access it. If you have logged into your Docker account, you can push the image to Docker Hub with this command: - -```shell -docker push username/my-geolab-image:0.1.0 -``` - -Replace the details to match the --tag value in the build command. - -> [!TIP] -> If the push results in an error, make sure you are logged into Docker Hub using `docker login` if needed. - -Many other image repositories exist. If you use AWS ECR, follow these [instructions](https://docs.aws.amazon.com/AmazonECR/latest/userguide/docker-push-ecr-image.html). - -### Publishing to GitHub or AWS image repositories - -Docker Hub is the simplest option, but two alternatives are common for teams: GitHub Container Registry (GHCR) and AWS Elastic Container Registry (ECR). - -- **GHCR** integrates tightly with GitHub Actions, so a build-and-push to GHCR can be triggered automatically from a pull request or a push to a branch — useful if several people maintain the same image. -- **AWS ECR** is built for cloud-scale pulls, which matters if the image is used by many GeoLab users at once. - -Both GHCR and ECR have more stringent authorization and access controls than Docker Hub's public images. To push to GHCR, tag and push using the `ghcr.io` registry host and your GitHub username or organization: - -```shell -docker build --no-cache -f Dockerfile \ - --platform linux/amd64 \ - --tag ghcr.io/username/my-geolab-image:0.1.0 . - -docker push ghcr.io/username/my-geolab-image:0.1.0 -``` - -Authenticating to GHCR from the command line requires a personal access token — see [Getting a Personal Access Token (for GHCR)](#getting-a-personal-access-token-for-ghcr) below. For AWS ECR, follow the [ECR push instructions](https://docs.aws.amazon.com/AmazonECR/latest/userguide/docker-push-ecr-image.html), which walk through creating a repository and authenticating with `aws ecr get-login-password`. - ---- - -## Step 5: Running your published image in GeoLab - -1. Open [GeoLab's Hub Control Panel](https://geolab.earthscope.cloud/hub/home/) - * Login with your [EarthScope account](https://www.earthscope.org/user/login) - * If "Stop My Server" button is visible, select it to stop your current server - * Select "Start My Server" button -2. Choose **Environment → Other** -3. In **Custom image** enter the image name from your registry, e.g.: `username/my-geolab-image:0.1.0` (for Docker Hub) -4. Select **Start** - -> [!TIP] -> If the image is not in Docker Hub, the **Custom image** value should be the full image reference. - -GeoLab will pull and launch your custom environment. The first launch may take a bit longer while the image is transferred from the repository. - ---- - -## Making changes later - -To change a published image, edit the configuration files as before, then rebuild and push with a **new** version number: - -```shell -docker build --no-cache -f Dockerfile \ - --platform linux/amd64 \ - --tag ghcr.io/username/my-geolab-image:0.1.1 . - -docker push ghcr.io/username/my-geolab-image:0.1.1 -``` - -> [!NOTE] -> Always use a new version number (`0.1.1`, `0.1.2`, etc.) when you rebuild. If you reuse the same tag, GeoLab (and other systems) may load the old cached version instead of your new one. - ---- - -## Troubleshooting package installation - -If you're not sure whether a package is available, or under what name, check before adding it to a config file. Conda is the safer choice when a package is available both ways, since it checks packages for dependency conflicts across the whole environment rather than installing in isolation. - -- Search conda-forge: `conda search -c conda-forge ` (e.g. `seisbench`) -- Search PyPI: `python -m pip index versions ` (e.g. `seisbench`) - -The name used to *install* a package isn't always the name used to *import* it — for example, `scikit-learn` is installed via that name but imported as `sklearn`, and `opencv-python` is imported as `cv2`. Keep this in mind both when adding a package and when [adding a test](#adding-a-test) for it. - ---- - -## Quick reference - -| Task | Location / command | -| ------------------------- | --------------------------------------------------------------------------------------- | -| Add a Conda package | `environment.yml`, under `dependencies` | -| Add a PyPI-only package | `requirements.txt` | -| Add a system tool | `apt.txt` | -| Run a one-time setup step | `postBuild` | -| Add a package test | `test_packages.py` and `test_notebook.ipynb` (see [Adding a test](#adding-a-test)) | -| Build locally for testing | `docker build -f Dockerfile --tag my-geolab-image:0.1.0 .` | -| Run locally | `docker run --rm -p 8888:8888 my-geolab-image:0.1.0` | -| Verify installed packages | `pytest test_packages.py -v`, or run `test_notebook.ipynb` | -| Build for GeoLab | `docker build --no-cache --platform linux/amd64 --tag username/my-geolab-image:0.1.0 .` | -| Publish | `docker push username/my-geolab-image:0.1.0` | -| Run a published image | GeoLab → **Environment → Other** → **Custom image** | - ---- - -## Getting a Personal Access Token (for GHCR) - -Pushing to GitHub Container Registry from the command line requires a personal access token instead of a password. - -1. On GitHub, go to **Settings → Developer settings → Personal access tokens → Tokens (classic)**. -2. Select **Generate new token (classic)**. -3. Give it a name (e.g. `geolab-image`), set an expiration, and enable the **`write:packages`** scope. -4. Select **Generate token**, and copy it immediately — GitHub only shows it once. - -Save the token somewhere safe (a password manager works well). Use it to log in to GHCR before pushing: - -```shell -docker login ghcr.io -u username -``` - -Enter the personal access token as the password when prompted. diff --git a/geolab-base/build_process.png b/geolab-base/build_process.png new file mode 100644 index 0000000000000000000000000000000000000000..06f639f840828b8eb54d3c509a060b4d7b396937 GIT binary patch literal 31269 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 l`=9vxzvu4%XY=Dw`ruOp3AO*nXZZA>_(1J`G4k Date: Fri, 18 Sep 2026 12:13:41 -0500 Subject: [PATCH 20/20] update CHANGELOG --- geolab-base/CHANGELOG.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/geolab-base/CHANGELOG.md b/geolab-base/CHANGELOG.md index 5d4169f..98486de 100644 --- a/geolab-base/CHANGELOG.md +++ b/geolab-base/CHANGELOG.md @@ -2,19 +2,21 @@ All notable changes to this project will be documented in this file. -## [1.0.1] +## [0.2.0] ### Added - pin base image to pangeo-base:04bb14b -- add geolab-base version as ENV GEOLAB_VERSION (must be manually edited) +- add geolab-base version as ENV GEOLAB_VERSION (set during CI build) - add graphviz package - add pygraphviz package - add ipycytoscape - added tests for new packages +- add build_process.png for README ### 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 +- updated README to match Building Custom Images in docs