diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 01d7190..39743c0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,3 +17,20 @@ jobs: run: uv run ruff check - name: Format run: uv run ruff format --check + + helm: + name: Helm chart + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: azure/setup-helm@v4 + - name: Register dependency repos + run: | + helm repo add eoapi https://developmentseed.org/eoapi-k8s/ + helm repo add stac-manager https://stac-manager.ds.io/ + - name: Build chart dependencies + run: helm dependency build infrastructure/charts/eoapi-workshop + - name: Lint + run: helm lint infrastructure/charts/eoapi-workshop + - name: Render checks + run: ./infrastructure/charts/eoapi-workshop/tests/render-checks.sh diff --git a/.github/workflows/publish-workshop-image.yml b/.github/workflows/publish-workshop-image.yml new file mode 100644 index 0000000..204f86a --- /dev/null +++ b/.github/workflows/publish-workshop-image.yml @@ -0,0 +1,53 @@ +name: Publish workshop image + +# Builds the JupyterLab workshop image (Dockerfile.local + environment.yml) and +# pushes it to GHCR. Consumed by the Helm chart's per-participant Labs +# (infrastructure/charts/eoapi-workshop, values key `jupyter.image`). +on: + push: + branches: + - main + - foss4geu-helmchart # workshop branch — image is built from here for now + paths: + - Dockerfile.local + - environment.yml + - docs/** + - .github/workflows/publish-workshop-image.yml + workflow_dispatch: {} # manual run (once this workflow is on the default branch) + +jobs: + publish: + name: Build and push + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v5 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Image metadata + id: meta + uses: docker/metadata-action@v5 + with: + # Portable: resolves to ghcr.io/developmentseed/eoapi-workshop in this repo. + images: ghcr.io/${{ github.repository_owner }}/eoapi-workshop + tags: | + type=raw,value=latest + type=sha,format=long + type=ref,event=branch + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile.local + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/.gitignore b/.gitignore index 5fea259..36dc738 100644 --- a/.gitignore +++ b/.gitignore @@ -178,3 +178,8 @@ cdk.context.json node_modules/ config.yaml + +# Helm: pulled chart dependencies (rebuilt with `helm dependency update`). +# Chart.lock IS tracked for reproducible builds. +charts/*/charts/ +charts/**/*.tgz diff --git a/docker-compose.yml b/docker-compose.yml index 8e2a8b5..f6a1b63 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ services: database: - image: ghcr.io/stac-utils/pgstac:v0.9.8 + image: ghcr.io/stac-utils/pgstac:v0.9.10 environment: - POSTGRES_USER=username - POSTGRES_PASSWORD=password @@ -59,8 +59,34 @@ services: start_period: 120s + # Load a public STAC collection from the MAAP STAC into pgstac so notebook 04 + # §4.5 has data — mirrors the 2i2c deploy step and the Helm chart's + # features-loader Job (stac-loader container). Idempotent: skips if present. + stac-loader: + image: python:3.12-slim + depends_on: + database: + condition: service_started + environment: + - PGHOST=database + - PGUSER=username + - PGPASSWORD=password + - PGDATABASE=postgis + - PGPORT=5432 + restart: "no" + command: > + bash -c ' + set -euo pipefail && + pip install -q "pypgstac[psycopg]==0.9.10" && + sleep 5 && + python3 /opt/load_stac.py + ' + configs: + - source: load_stac + target: /opt/load_stac.py + stac-fastapi: - image: ghcr.io/stac-utils/stac-fastapi-pgstac:6.0.2 + image: ghcr.io/stac-utils/stac-fastapi-pgstac:6.2.2 ports: - 8081:8081 environment: @@ -107,16 +133,16 @@ services: titiler-pgstac: platform: linux/amd64 - image: ghcr.io/stac-utils/titiler-pgstac:1.9.0 + image: ghcr.io/stac-utils/titiler-pgstac:3.0.0 ports: - 8082:8082 environment: - # Postgres connection - - POSTGRES_USER=username - - POSTGRES_PASS=password - - POSTGRES_DBNAME=postgis - - POSTGRES_HOST=database - - POSTGRES_PORT=5432 + # Postgres connection — titiler-pgstac 3.x reads PG* (POSTGRES_* was dropped) + - PGUSER=username + - PGPASSWORD=password + - PGDATABASE=postgis + - PGHOST=database + - PGPORT=5432 - DB_MIN_CONN_SIZE=1 - DB_MAX_CONN_SIZE=10 # - DB_MAX_QUERIES=10 @@ -134,9 +160,11 @@ services: # TiTiler Config - MOSAIC_CONCURRENCY=1 - TITILER_PGSTAC_API_ENABLE_EXTERNAL_DATASET_ENDPOINTS=True - # AWS S3 endpoint config + # AWS S3 endpoint config — the workshop's s3:// assets (glad collection) + # are in public buckets; read unsigned unless real credentials are provided - AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} - AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} + - AWS_NO_SIGN_REQUEST=${AWS_NO_SIGN_REQUEST:-YES} depends_on: database: condition: service_started @@ -146,7 +174,7 @@ services: bash -c "uvicorn titiler.pgstac.main:app --host 0.0.0.0 --port 8082" tipg: - image: ghcr.io/developmentseed/tipg:1.1.2 + image: ghcr.io/developmentseed/tipg:1.4.0 ports: - 8083:8083 environment: @@ -178,6 +206,8 @@ services: - database stac-manager: + # no arm64 manifest published — run the amd64 image under emulation + platform: linux/amd64 image: ghcr.io/developmentseed/stac-manager:1.0.3 ports: - 8086:8080 @@ -215,6 +245,38 @@ services: - STAC_BROWSER_ENDPOINT=http://localhost:8080 - STAC_MANAGER_ENDPOINT=http://localhost:8086 +configs: + # same loader as the Helm chart's stac-loader container — keep in sync + load_stac: + content: | + import json + import sys + import urllib.request + + from pypgstac.db import PgstacDB + from pypgstac.load import Loader, Methods + + SRC = "https://stac.maap-project.org" + COLLECTION = "glad-global-forest-change-1.11" + LIMIT = 100 + + db = PgstacDB() + if db.query_one("select 1 from pgstac.collections where id=%s", [COLLECTION]): + print(f"{COLLECTION} already present -- skipping load.") + sys.exit(0) + + urllib.request.urlretrieve(f"{SRC}/collections/{COLLECTION}", "/tmp/collection.json") + with urllib.request.urlopen(f"{SRC}/search?collections={COLLECTION}&limit={LIMIT}") as r: + features = json.load(r)["features"] + with open("/tmp/items.ndjson", "w") as f: + for feat in features: + f.write(json.dumps(feat) + "\n") + + loader = Loader(db=db) + loader.load_collections("/tmp/collection.json", Methods.upsert) + loader.load_items("/tmp/items.ndjson", Methods.upsert) + print(f"{COLLECTION}: collection + {len(features)} items loaded.") + volumes: pgdata: feature-loader-state: diff --git a/docs/02-database.ipynb b/docs/02-database.ipynb index ca7897c..fd8e138 100644 --- a/docs/02-database.ipynb +++ b/docs/02-database.ipynb @@ -39,10 +39,18 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "68in9iet0cb", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "✓ Database credentials already configured\n" + ] + } + ], "source": [ "from workshop_setup import setup, get_random_point\n", "\n", @@ -61,7 +69,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "id": "196e7c1d-b82b-4d66-bf96-4942cd02392c", "metadata": { "editable": true, @@ -72,7 +80,50 @@ "hide-input" ] }, - "outputs": [], + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "76374355631d418bb25463a889f3f999", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Text(value='dark-dream-2670', description='username:', placeholder='Enter your username')" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "fe5330f700de4ef09602fdb283341537", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "BoundedFloatText(value=-33.17, description='latitude:', max=90.0, min=-90.0)" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "6b7dd0353648444c9e4aaffe3fd54172", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "BoundedFloatText(value=135.72, description='longitude:', max=180.0, min=-180.0)" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "import os\n", "\n", @@ -122,10 +173,273 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "id": "1a09b4e5-1727-4ee2-96ef-e0772267829d", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "
\n", + "
\n", + " <Collection id=dark-dream-2670-sentinel-2-c1-l2a>\n", + "
\n", + "\n", + "
" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "import pystac_client\n", "from pystac import Collection, Extent, SpatialExtent, TemporalExtent\n", @@ -165,7 +479,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "id": "0536d319-0b51-40b4-8fe2-4ceb3ceacfec", "metadata": {}, "outputs": [], @@ -186,7 +500,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "id": "7ae2471c-8164-466f-a56d-745213cb7316", "metadata": {}, "outputs": [], @@ -204,10 +518,21 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "id": "7d71b6b1-70c3-4e5d-9444-f1dddb4e98c3", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "[('dark-dream-2670-sentinel-2-c1-l2a',)]" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "list(db.query(f\"SELECT id from collections where id = '{my_collection.id}';\"))" ] @@ -226,10 +551,18 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "id": "6a858a2d-57b7-40e9-8235-2c341f43a8de", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "948\n" + ] + } + ], "source": [ "source_client = pystac_client.Client.open(\"https://earth-search.aws.element84.com/v1\")\n", "\n", @@ -255,10 +588,7713 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "id": "a410af0b-6d3c-4a8a-87ee-b16265afcd6a", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "
\n", + "
\n", + " <Item id=S2B_T53HLB_20250416T010240_L2A>\n", + "
\n", + "\n", + "
" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# override the collection id to match your new collection\n", "for item in items:\n", @@ -277,7 +8313,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "id": "82443eb2-0d59-46b2-9178-25acb3d0593e", "metadata": {}, "outputs": [], @@ -295,10 +8331,18 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 10, "id": "48133d14-e301-45b4-973d-1b846a6c503a", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "there are 948 items\n" + ] + } + ], "source": [ "n_items = db.query_one(\n", " f\"SELECT COUNT(*) FROM items where collection = '{my_collection.id}';\"\n", @@ -318,17 +8362,50 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 11, "id": "ba5f6278-4d92-4386-af1d-94ca5aed6c30", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "from IPython.display import IFrame\n", "\n", - "stac_api_endpoint = os.getenv(\"STAC_API_ENDPOINT\").replace(\"stac-auth-proxy:8000\", \"localhost:8084\")\n", + "# Use the stack's own STAC Browser when deployed (its catalog is already this\n", + "# STAC API); otherwise fall back to the public STAC Browser in external mode.\n", + "stac_browser_endpoint = os.getenv(\"STAC_BROWSER_ENDPOINT\")\n", + "\n", + "if stac_browser_endpoint:\n", + " browser_url = f\"{stac_browser_endpoint}/#/collections/{my_collection.id}\"\n", + "else:\n", + " browser_stac_url = os.getenv(\"STAC_API_ENDPOINT\").replace(\n", + " \"stac-auth-proxy:8000\", \"localhost:8084\"\n", + " )\n", + " browser_url = f\"https://radiantearth.github.io/stac-browser/#/external/{browser_stac_url}/collections/{my_collection.id}\"\n", "\n", "IFrame(\n", - " f\"https://radiantearth.github.io/stac-browser/#/external/{stac_api_endpoint}/collections/{my_collection.id}\",\n", + " browser_url,\n", " 1200,\n", " 800,\n", ")" @@ -348,7 +8425,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 12, "id": "4c6782bf-b043-49a0-a338-7676ff52d005", "metadata": {}, "outputs": [], @@ -362,10 +8439,18 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 13, "id": "d3030aa7-4aaa-4ead-8589-b88759968f99", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "now there are 947 items\n" + ] + } + ], "source": [ "new_n_items = db.query_one(\n", " f\"SELECT COUNT(*) FROM items where collection = '{my_collection.id}';\"\n", @@ -375,7 +8460,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 14, "id": "dd34e829-0fc7-4abd-88ed-6fce8e73d1bf", "metadata": {}, "outputs": [], @@ -396,12 +8481,20 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 15, "id": "7137a06e-135c-478a-a79f-d7134bd48d3d", "metadata": { "scrolled": true }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{\"type\":\"FeatureCollection\",\"links\":[{\"rel\":\"root\",\"href\":\".\",\"type\":\"application/json\"},{\"rel\":\"self\",\"href\":\"./search\",\"type\":\"application/json\"},{\"rel\":\"next\",\"href\":\"./search?token=next:dark-dream-2670-sentinel-2-c1-l2a:S2B_T53HLB_20250416T010240_L2A\",\"type\":\"application/geo+json\",\"method\":\"GET\"}],\"features\":[{\"id\":\"S2B_T53HLB_20250416T010240_L2A\",\"bbox\":[132.986678,-35.327182,134.019373,-34.329311],\"type\":\"Feature\",\"links\":[{\"rel\":\"self\",\"href\":\"https://earth-search.aws.element84.com/v1/collections/sentinel-2-c1-l2a/items/S2B_T53HLB_20250416T010240_L2A\",\"type\":\"application/geo+json\"},{\"rel\":\"canonical\",\"href\":\"s3://e84-earth-search-sentinel-data/sentinel-2-c1-l2a/53/H/LB/2025/4/S2B_T53HLB_20250416T010240_L2A/S2B_T53HLB_20250416T010240_L2A.json\",\"type\":\"application/json\"},{\"rel\":\"via\",\"href\":\"s3://sentinel-s2-l2a/tiles/53/H/LB/2025/4/16/0/metadata.xml\",\"type\":\"application/xml\",\"title\":\"Granule Metadata in Sinergize RODA Archive\"},{\"rel\":\"parent\",\"href\":\"https://earth-search.aws.element84.com/v1/collections/sentinel-2-c1-l2a\",\"type\":\"application/json\"},{\"rel\":\"root\",\"href\":\"https://earth-search.aws.element84.com/v1\",\"type\":\"application/json\",\"title\":\"Earth Search by Element 84\"},{\"rel\":\"thumbnail\",\"href\":\"https://earth-search.aws.element84.com/v1/collections/sentinel-2-c1-l2a/items/S2B_T53HLB_20250416T010240_L2A/thumbnail\"},{\"rel\":\"collection\",\"type\":\"application/json\"}],\"assets\":{\"aot\":{\"gsd\":20,\"href\":\"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/53/H/LB/2025/4/S2B_T53HLB_20250416T010240_L2A/AOT.tif\",\"type\":\"image/tiff; application=geotiff; profile=cloud-optimized\",\"roles\":[\"data\"],\"title\":\"Aerosol optical thickness (AOT)\",\"file:size\":273323,\"proj:shape\":[5490,5490],\"raster:bands\":[{\"scale\":0.001,\"nodata\":0,\"offset\":0,\"data_type\":\"uint16\",\"spatial_resolution\":20}],\"file:checksum\":\"12204952da596a5c851876f3cd466e374f1f29cc6da8a69d863d2228966972539891\",\"proj:transform\":[20,0,300000,0,-20,6200020]},\"nir\":{\"gsd\":10,\"href\":\"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/53/H/LB/2025/4/S2B_T53HLB_20250416T010240_L2A/B08.tif\",\"type\":\"image/tiff; application=geotiff; profile=cloud-optimized\",\"roles\":[\"data\",\"reflectance\"],\"title\":\"NIR 1 - 10m\",\"eo:bands\":[{\"name\":\"B08\",\"common_name\":\"nir\",\"center_wavelength\":0.842,\"full_width_half_max\":0.145}],\"file:size\":133559679,\"proj:shape\":[10980,10980],\"raster:bands\":[{\"scale\":0.0001,\"nodata\":0,\"offset\":-0.1,\"data_type\":\"uint16\",\"spatial_resolution\":10}],\"file:checksum\":\"1220554285286e6dc529d78438689e81762a9aee8595b3cf80c7424e8154c6cb5a84\",\"proj:transform\":[10,0,300000,0,-10,6200020]},\"red\":{\"gsd\":10,\"href\":\"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/53/H/LB/2025/4/S2B_T53HLB_20250416T010240_L2A/B04.tif\",\"type\":\"image/tiff; application=geotiff; profile=cloud-optimized\",\"roles\":[\"data\",\"reflectance\"],\"title\":\"Red - 10m\",\"eo:bands\":[{\"name\":\"B04\",\"common_name\":\"red\",\"center_wavelength\":0.665,\"full_width_half_max\":0.038}],\"file:size\":133807213,\"proj:shape\":[10980,10980],\"raster:bands\":[{\"scale\":0.0001,\"nodata\":0,\"offset\":-0.1,\"data_type\":\"uint16\",\"spatial_resolution\":10}],\"file:checksum\":\"1220c430e71e9c3ac487660cbf5b4f5a5151ef0a9a53cb3964143394386e5e6696ee\",\"proj:transform\":[10,0,300000,0,-10,6200020]},\"scl\":{\"gsd\":20,\"href\":\"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/53/H/LB/2025/4/S2B_T53HLB_20250416T010240_L2A/SCL.tif\",\"type\":\"image/tiff; application=geotiff; profile=cloud-optimized\",\"roles\":[\"data\"],\"title\":\"Scene classification map (SCL)\",\"file:size\":1195551,\"proj:shape\":[5490,5490],\"raster:bands\":[{\"nodata\":0,\"data_type\":\"uint8\",\"spatial_resolution\":20}],\"file:checksum\":\"12201803d9dedbdcf77cfa05815bf082381857ca898ccd2fb98370387a8287361e6f\",\"proj:transform\":[20,0,300000,0,-20,6200020]},\"wvp\":{\"gsd\":20,\"href\":\"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/53/H/LB/2025/4/S2B_T53HLB_20250416T010240_L2A/WVP.tif\",\"type\":\"image/tiff; application=geotiff; profile=cloud-optimized\",\"roles\":[\"data\"],\"title\":\"Water Vapour (WVP)\",\"file:size\":186898,\"proj:shape\":[5490,5490],\"raster:bands\":[{\"unit\":\"cm\",\"scale\":0.001,\"nodata\":0,\"offset\":0,\"data_type\":\"uint16\",\"spatial_resolution\":20}],\"file:checksum\":\"1220fa2cbfb26c752d559b1b568e507c4ee8367fd1bd8aa638add4ed021cf5401d10\",\"proj:transform\":[20,0,300000,0,-20,6200020]},\"blue\":{\"gsd\":10,\"href\":\"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/53/H/LB/2025/4/S2B_T53HLB_20250416T010240_L2A/B02.tif\",\"type\":\"image/tiff; application=geotiff; profile=cloud-optimized\",\"roles\":[\"data\",\"reflectance\"],\"title\":\"Blue - 10m\",\"eo:bands\":[{\"name\":\"B02\",\"common_name\":\"blue\",\"center_wavelength\":0.49,\"full_width_half_max\":0.098}],\"file:size\":139809856,\"proj:shape\":[10980,10980],\"raster:bands\":[{\"scale\":0.0001,\"nodata\":0,\"offset\":-0.1,\"data_type\":\"uint16\",\"spatial_resolution\":10}],\"file:checksum\":\"12208f0dcac15d931e1a916f60f956926425ef868510ebaca4d9c6299a0f7a25c4e4\",\"proj:transform\":[10,0,300000,0,-10,6200020]},\"snow\":{\"href\":\"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/53/H/LB/2025/4/S2B_T53HLB_20250416T010240_L2A/SNW_20m.tif\",\"type\":\"image/tiff; application=geotiff; profile=cloud-optimized\",\"roles\":[\"data\",\"snow-ice\"],\"title\":\"Snow Probabilities\",\"file:size\":53931,\"proj:shape\":[5490,5490],\"raster:bands\":[{\"nodata\":0,\"data_type\":\"uint8\",\"spatial_resolution\":20}],\"file:checksum\":\"1220a2e0bc57410a89813d86952206c620f6b0c29e580ea1b602a31a9f8f18b3f204\",\"proj:transform\":[20,0,300000,0,-20,6200020]},\"cloud\":{\"gsd\":20,\"href\":\"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/53/H/LB/2025/4/S2B_T53HLB_20250416T010240_L2A/CLD_20m.tif\",\"type\":\"image/tiff; application=geotiff; profile=cloud-optimized\",\"roles\":[\"data\",\"cloud\"],\"title\":\"Cloud Probabilities\",\"file:size\":3905464,\"proj:shape\":[5490,5490],\"raster:bands\":[{\"nodata\":0,\"data_type\":\"uint8\",\"spatial_resolution\":20}],\"file:checksum\":\"12203b479cd7a439ec4de573b5d80748eb7e8535ce7815e33f749c20d92f1e7f3b62\",\"proj:transform\":[20,0,300000,0,-20,6200020]},\"green\":{\"gsd\":10,\"href\":\"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/53/H/LB/2025/4/S2B_T53HLB_20250416T010240_L2A/B03.tif\",\"type\":\"image/tiff; application=geotiff; profile=cloud-optimized\",\"roles\":[\"data\",\"reflectance\"],\"title\":\"Green - 10m\",\"eo:bands\":[{\"name\":\"B03\",\"common_name\":\"green\",\"center_wavelength\":0.56,\"full_width_half_max\":0.045}],\"file:size\":136213923,\"proj:shape\":[10980,10980],\"raster:bands\":[{\"scale\":0.0001,\"nodata\":0,\"offset\":-0.1,\"data_type\":\"uint16\",\"spatial_resolution\":10}],\"file:checksum\":\"12203c216da945d32b1e2175ae1d17ab5bb2c163fa88a205e83dfb674383a0da7f58\",\"proj:transform\":[10,0,300000,0,-10,6200020]},\"nir08\":{\"gsd\":20,\"href\":\"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/53/H/LB/2025/4/S2B_T53HLB_20250416T010240_L2A/B8A.tif\",\"type\":\"image/tiff; application=geotiff; profile=cloud-optimized\",\"roles\":[\"data\",\"reflectance\"],\"title\":\"NIR 2 - 20m\",\"eo:bands\":[{\"name\":\"B8A\",\"common_name\":\"nir08\",\"center_wavelength\":0.865,\"full_width_half_max\":0.033}],\"file:size\":35952693,\"proj:shape\":[5490,5490],\"raster:bands\":[{\"scale\":0.0001,\"nodata\":0,\"offset\":-0.1,\"data_type\":\"uint16\",\"spatial_resolution\":20}],\"file:checksum\":\"1220a6fc63bf322fc11ce9ac689970bd8e88d01d09efc4107eca2b7df9b99aec6f81\",\"proj:transform\":[20,0,300000,0,-20,6200020]},\"nir09\":{\"gsd\":60,\"href\":\"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/53/H/LB/2025/4/S2B_T53HLB_20250416T010240_L2A/B09.tif\",\"type\":\"image/tiff; application=geotiff; profile=cloud-optimized\",\"roles\":[\"data\",\"reflectance\"],\"title\":\"NIR 3 - 60m\",\"eo:bands\":[{\"name\":\"B09\",\"common_name\":\"nir09\",\"center_wavelength\":0.945,\"full_width_half_max\":0.026}],\"file:size\":4399445,\"proj:shape\":[1830,1830],\"raster:bands\":[{\"scale\":0.0001,\"nodata\":0,\"offset\":-0.1,\"data_type\":\"uint16\",\"spatial_resolution\":60}],\"file:checksum\":\"12209778a02fd1bd25ca48b3397d6783d71165b741a577aa0a5386de7e10b78a9a3a\",\"proj:transform\":[60,0,300000,0,-60,6200020]},\"swir16\":{\"gsd\":20,\"href\":\"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/53/H/LB/2025/4/S2B_T53HLB_20250416T010240_L2A/B11.tif\",\"type\":\"image/tiff; application=geotiff; profile=cloud-optimized\",\"roles\":[\"data\",\"reflectance\"],\"title\":\"SWIR 1.6μm - 20m\",\"eo:bands\":[{\"name\":\"B11\",\"common_name\":\"swir16\",\"center_wavelength\":1.61,\"full_width_half_max\":0.143}],\"file:size\":35152883,\"proj:shape\":[5490,5490],\"raster:bands\":[{\"scale\":0.0001,\"nodata\":0,\"offset\":-0.1,\"data_type\":\"uint16\",\"spatial_resolution\":20}],\"file:checksum\":\"122006417729758845a7f6bae55f5fd3b67e9039157d90fc0aedef4080ec5840ba78\",\"proj:transform\":[20,0,300000,0,-20,6200020]},\"swir22\":{\"gsd\":20,\"href\":\"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/53/H/LB/2025/4/S2B_T53HLB_20250416T010240_L2A/B12.tif\",\"type\":\"image/tiff; application=geotiff; profile=cloud-optimized\",\"roles\":[\"data\",\"reflectance\"],\"title\":\"SWIR 2.2μm - 20m\",\"eo:bands\":[{\"name\":\"B12\",\"common_name\":\"swir22\",\"center_wavelength\":2.19,\"full_width_half_max\":0.242}],\"file:size\":35968600,\"proj:shape\":[5490,5490],\"raster:bands\":[{\"scale\":0.0001,\"nodata\":0,\"offset\":-0.1,\"data_type\":\"uint16\",\"spatial_resolution\":20}],\"file:checksum\":\"12206c533c7371eded58f9e85b640cbbda9f69be10bb4dce7144b2843a4eae92e92c\",\"proj:transform\":[20,0,300000,0,-20,6200020]},\"visual\":{\"gsd\":10,\"href\":\"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/53/H/LB/2025/4/S2B_T53HLB_20250416T010240_L2A/TCI.tif\",\"type\":\"image/tiff; application=geotiff; profile=cloud-optimized\",\"roles\":[\"visual\"],\"title\":\"True color image\",\"eo:bands\":[{\"name\":\"B04\",\"common_name\":\"red\",\"center_wavelength\":0.665,\"full_width_half_max\":0.038},{\"name\":\"B03\",\"common_name\":\"green\",\"center_wavelength\":0.56,\"full_width_half_max\":0.045},{\"name\":\"B02\",\"common_name\":\"blue\",\"center_wavelength\":0.49,\"full_width_half_max\":0.098}],\"file:size\":134313884,\"proj:shape\":[10980,10980],\"raster:bands\":[{\"nodata\":0,\"data_type\":\"uint8\",\"spatial_resolution\":10},{\"nodata\":0,\"data_type\":\"uint8\",\"spatial_resolution\":10},{\"nodata\":0,\"data_type\":\"uint8\",\"spatial_resolution\":10}],\"file:checksum\":\"1220d594a4d8fbae5afcde075c02868b80ddf94049f832b427cad44c2bc2ec48cea1\",\"proj:transform\":[10,0,300000,0,-10,6200020]},\"coastal\":{\"gsd\":60,\"href\":\"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/53/H/LB/2025/4/S2B_T53HLB_20250416T010240_L2A/B01.tif\",\"type\":\"image/tiff; application=geotiff; profile=cloud-optimized\",\"roles\":[\"data\",\"reflectance\"],\"title\":\"Coastal - 60m\",\"eo:bands\":[{\"name\":\"B01\",\"common_name\":\"coastal\",\"center_wavelength\":0.443,\"full_width_half_max\":0.027}],\"file:size\":4041730,\"proj:shape\":[1830,1830],\"raster:bands\":[{\"scale\":0.0001,\"nodata\":0,\"offset\":-0.1,\"data_type\":\"uint16\",\"spatial_resolution\":60}],\"file:checksum\":\"1220bf8b9d3d17783c45ba128c64da4b821f121a817b769615894279df9307f408aa\",\"proj:transform\":[60,0,300000,0,-60,6200020]},\"preview\":{\"href\":\"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/53/H/LB/2025/4/S2B_T53HLB_20250416T010240_L2A/L2A_PVI.tif\",\"type\":\"image/tiff; application=geotiff; profile=cloud-optimized\",\"roles\":[\"overview\"],\"title\":\"True color preview\",\"eo:bands\":[{\"name\":\"B04\",\"common_name\":\"red\",\"center_wavelength\":0.665,\"full_width_half_max\":0.038},{\"name\":\"B03\",\"common_name\":\"green\",\"center_wavelength\":0.56,\"full_width_half_max\":0.045},{\"name\":\"B02\",\"common_name\":\"blue\",\"center_wavelength\":0.49,\"full_width_half_max\":0.098}],\"file:size\":143296,\"file:checksum\":\"1220a6d5e1e1df4e2decbcb1d180295019e3c23a3c650cd3a25c5397b09ac541199c\"},\"rededge1\":{\"gsd\":20,\"href\":\"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/53/H/LB/2025/4/S2B_T53HLB_20250416T010240_L2A/B05.tif\",\"type\":\"image/tiff; application=geotiff; profile=cloud-optimized\",\"roles\":[\"data\",\"reflectance\"],\"title\":\"Red Edge 1 - 20m\",\"eo:bands\":[{\"name\":\"B05\",\"common_name\":\"rededge\",\"center_wavelength\":0.704,\"full_width_half_max\":0.019}],\"file:size\":36112592,\"proj:shape\":[5490,5490],\"raster:bands\":[{\"scale\":0.0001,\"nodata\":0,\"offset\":-0.1,\"data_type\":\"uint16\",\"spatial_resolution\":20}],\"file:checksum\":\"1220d7ed056f05d925711c31aa1eceb4bf339b9db4c4c0591c4058a62b9b785062e2\",\"proj:transform\":[20,0,300000,0,-20,6200020]},\"rededge2\":{\"gsd\":20,\"href\":\"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/53/H/LB/2025/4/S2B_T53HLB_20250416T010240_L2A/B06.tif\",\"type\":\"image/tiff; application=geotiff; profile=cloud-optimized\",\"roles\":[\"data\",\"reflectance\"],\"title\":\"Red Edge 2 - 20m\",\"eo:bands\":[{\"name\":\"B06\",\"common_name\":\"rededge\",\"center_wavelength\":0.74,\"full_width_half_max\":0.018}],\"file:size\":35889857,\"proj:shape\":[5490,5490],\"raster:bands\":[{\"scale\":0.0001,\"nodata\":0,\"offset\":-0.1,\"data_type\":\"uint16\",\"spatial_resolution\":20}],\"file:checksum\":\"12206511a70a2f30a4b168533c1c8c70bcce53836d19e19cd29d941ac689b0540c44\",\"proj:transform\":[20,0,300000,0,-20,6200020]},\"rededge3\":{\"gsd\":20,\"href\":\"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/53/H/LB/2025/4/S2B_T53HLB_20250416T010240_L2A/B07.tif\",\"type\":\"image/tiff; application=geotiff; profile=cloud-optimized\",\"roles\":[\"data\",\"reflectance\"],\"title\":\"Red Edge 3 - 20m\",\"eo:bands\":[{\"name\":\"B07\",\"common_name\":\"rededge\",\"center_wavelength\":0.783,\"full_width_half_max\":0.028}],\"file:size\":36093258,\"proj:shape\":[5490,5490],\"raster:bands\":[{\"scale\":0.0001,\"nodata\":0,\"offset\":-0.1,\"data_type\":\"uint16\",\"spatial_resolution\":20}],\"file:checksum\":\"12208ba064ff4d4d2f65903a1c3d9084a2b82e5a848b4b39e8ea9e8b9a409fc33069\",\"proj:transform\":[20,0,300000,0,-20,6200020]},\"thumbnail\":{\"href\":\"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/53/H/LB/2025/4/S2B_T53HLB_20250416T010240_L2A/L2A_PVI.jpg\",\"type\":\"image/jpeg\",\"roles\":[\"thumbnail\"],\"title\":\"Thumbnail of preview image\",\"file:size\":28745,\"file:checksum\":\"12209e0e170b9f5c28771c86eb8c35b3ade75e847b0eee99747c939044eeb0ae71d6\"},\"granule_metadata\":{\"href\":\"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/53/H/LB/2025/4/S2B_T53HLB_20250416T010240_L2A/metadata.xml\",\"type\":\"application/xml\",\"roles\":[\"metadata\"],\"file:size\":375533,\"file:checksum\":\"1220703c9620d14c017a8cb80af8c19b53d94287f0c202b2ec5e05db23be749c6957\"},\"product_metadata\":{\"href\":\"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/53/H/LB/2025/4/S2B_T53HLB_20250416T010240_L2A/product_metadata.xml\",\"type\":\"application/xml\",\"roles\":[\"metadata\"],\"file:size\":54893,\"file:checksum\":\"12204173b8a9029b04fa726763c0051e84654f5e1780840107bd37f79633391ef702\"},\"tileinfo_metadata\":{\"href\":\"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/53/H/LB/2025/4/S2B_T53HLB_20250416T010240_L2A/tileInfo.json\",\"type\":\"application/json\",\"roles\":[\"metadata\"],\"file:size\":1521,\"file:checksum\":\"1220e35fa9e4140350903268c9db4cb83c5582827bd26ea1eea7e61b52041c50ab50\"}},\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[[[133.2986412494336,-34.329310504040116],[132.98667821085152,-35.31448967467239],[134.00756378262122,-35.32718167573166],[134.0193728890354,-34.33720451171367],[133.2986412494336,-34.329310504040116]]]},\"collection\":\"dark-dream-2670-sentinel-2-c1-l2a\",\"properties\":{\"created\":\"2025-04-16T03:28:24.759Z\",\"updated\":\"2025-04-16T03:28:24.759Z\",\"datetime\":\"2025-04-16T01:07:00.210000Z\",\"platform\":\"sentinel-2b\",\"grid:code\":\"MGRS-53HLB\",\"proj:code\":\"EPSG:32753\",\"s2:tile_id\":\"S2B_OPER_MSI_L2A_TL_2BPS_20250416T023215_A042356_T53HLB_N05.11\",\"instruments\":[\"msi\"],\"view:azimuth\":104.01898444691977,\"constellation\":\"sentinel-2\",\"mgrs:utm_zone\":53,\"proj:centroid\":{\"lat\":-34.85543,\"lon\":133.5735},\"eo:cloud_cover\":36.831665,\"s2:datatake_id\":\"GS2B_20250416T005709_042356_N05.11\",\"s2:product_uri\":\"S2B_MSIL2A_20250416T005709_N0511_R002_T53HLB_20250416T023215.SAFE\",\"storage:region\":\"us-west-2\",\"s2:datastrip_id\":\"S2B_OPER_MSI_L2A_DS_2BPS_20250416T023215_S20250416T010240_N05.11\",\"s2:product_type\":\"S2MSI2A\",\"mgrs:grid_square\":\"LB\",\"s2:datatake_type\":\"INS-NOBS\",\"storage:platform\":\"AWS\",\"view:sun_azimuth\":37.707085408929,\"mgrs:latitude_band\":\"H\",\"s2:generation_time\":\"2025-04-16T02:32:15.000000Z\",\"view:sun_elevation\":36.9090104766897,\"processing:software\":{\"sentinel-2-c1-l2a-to-stac\":\"v2024.02.01\"},\"s2:water_percentage\":63.167995,\"view:incidence_angle\":8.65244882282184,\"earthsearch:payload_id\":\"roda-sentinel-2-c1-l2a/workflow-sentinel-2-c1-l2a-to-stac/28166453882f488743e2ffeb07d3f531\",\"s2:processing_baseline\":\"05.11\",\"s2:snow_ice_percentage\":0,\"storage:requester_pays\":false,\"s2:vegetation_percentage\":0.000348,\"s2:thin_cirrus_percentage\":0.000658,\"s2:cloud_shadow_percentage\":0,\"s2:nodata_pixel_percentage\":27.439335,\"s2:unclassified_percentage\":0,\"s2:not_vegetated_percentage\":0,\"s2:degraded_msi_data_percentage\":0.0099,\"s2:high_proba_clouds_percentage\":25.317863,\"s2:reflectance_conversion_factor\":0.995650530204258,\"s2:medium_proba_clouds_percentage\":11.513144,\"s2:saturated_defective_pixel_percentage\":0},\"stac_version\":\"1.1.0\",\"stac_extensions\":[\"https://stac-extensions.github.io/eo/v1.1.0/schema.json\",\"https://stac-extensions.github.io/file/v2.1.0/schema.json\",\"https://stac-extensions.github.io/grid/v1.1.0/schema.json\",\"https://stac-extensions.github.io/mgrs/v1.0.0/schema.json\",\"https://stac-extensions.github.io/processing/v1.1.0/schema.json\",\"https://stac-extensions.github.io/projection/v2.0.0/schema.json\",\"https://stac-extensions.github.io/raster/v1.1.0/schema.json\",\"https://stac-extensions.github.io/sentinel-2/v1.0.0/schema.json\",\"https://stac-extensions.github.io/storage/v1.0.0/schema.json\",\"https://stac-extensions.github.io/view/v1.0.0/schema.json\"]}],\"numberReturned\":1}\n" + ] + } + ], "source": [ "search_results = db.search(query={\"collections\": [my_collection.id], \"limit\": 1})\n", "\n", @@ -420,10 +8513,21 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 16, "id": "81f8addb-d890-434f-984e-b4ea591e94ef", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "('quiet-surf-4719-sentinel-2-c1-l2a', 2521)\n", + "('glad-global-forest-change-1.11', 100)\n", + "('super-mouse-3144-sentinel-2-c1-l2a', 12)\n", + "('dark-dream-2670-sentinel-2-c1-l2a', 948)\n" + ] + } + ], "source": [ "summary_query = db.query(\n", " \"SELECT collection, COUNT(*) as count FROM items GROUP BY collection;\"\n", @@ -434,7 +8538,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 17, "id": "a9438ee2-50ee-48e6-907f-4b7e6c6ac151", "metadata": {}, "outputs": [], @@ -467,7 +8571,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.11" + "version": "3.12.13" } }, "nbformat": 4, diff --git a/docs/03-stac_fastapi_pgstac.ipynb b/docs/03-stac_fastapi_pgstac.ipynb index 4ebe046..548cefa 100644 --- a/docs/03-stac_fastapi_pgstac.ipynb +++ b/docs/03-stac_fastapi_pgstac.ipynb @@ -1,553 +1,27820 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# 3. The STAC API: stac-fastapi-pgstac\n", - "\n", - "The STAC API provided by eoAPI is [stac-fastapi-pgstac](https://github.com/stac-utils/stac-fastapi-pgstac): a stac-fastapi application with a pgstac backend.\n", - "stac-fastapi-pgstac translates STAC API requests into pgstac queries and returns the results to the requester.\n", - "\n", - "The stac-fastapi-pgstac STAC API can be accessed using any HTTP client but STAC API clients like `pystac-client` provide a more intuitive interface. In this tutorial you will learn how to use HTTP requests via `httpx` as well as `pystac-client` methods." - ], - "id": "7f4f9109-ac1f-473b-b30b-c35294ae426d" - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 3.1 stac-fastapi-pgstac structure\n", - "\n", - "A standard eoAPI deployment will run an unmodified version of the FastAPI application defined in `stac_fastapi.pgstac.app:app` ([source](https://github.com/stac-utils/stac-fastapi-pgstac/blob/main/stac_fastapi/pgstac/app.py)). Unless otherwise specified, all of the extensions except the `transaction` and `bulk-transaction` extensions will be enabled but be sure to double check this in your own deployment.\n", - "\n", - "
\n", - "Warning: Do not turn on the transaction or bulk transactions extensions for a public-facing STAC API without some kind of auth layer enabled! Enabling the transactions extensions enables users to POST collections or items to the database via stac-fastapi-pgstac.\n", - "
\n", - "\n", - "stac-fastapi-pgstac implements a pgstac client that is capable of serving the routes defined by stac-fastapi's base `StacApi` factory class ([source](https://github.com/stac-utils/stac-fastapi/blob/main/stac_fastapi/api/stac_fastapi/api/app.py)). The pgstac client's methods contain the logic for translating API requests into pgstac database queries.\n", - "\n", - "For example, a search request for items in the \"amazing\" collection where the item bounding box intersects (0, 0, 10, 10) would get converted to a PostgreSQL query like this pseudo-sql:\n", - "```sql\n", - "SELECT * FROM items\n", - "WHERE \n", - " collection = 'amazing' AND\n", - " ST_Intersects(bbox, ST_MakeEnvelope(0, 0, 10, 10));\n", - "```\n", - "stac-fastapi-pgstac transforms the search results into the format expected in the API response and return it to the user. If you want to see how the actual SQL queries look in `pgstac`, check out the [pgstac source code](https://github.com/stac-utils/pgstac/tree/main/src/pgstac/sql).\n", - "\n", - "### 3.1.1 Customization\n", - "There are several options in the default stac-fastapi-pgstac application that are configurable at run time via environment variables (using [pydantic's settings features](https://docs.pydantic.dev/latest/concepts/pydantic_settings/)):\n", - "- the `ENABLED_EXTENSIONS` environment variable controls which extensions are enabled\n", - "- `pgstac` database credentials are set by `POSTGRES_*` environment variables ([source](https://github.com/stac-utils/stac-fastapi-pgstac/blob/main/stac_fastapi/pgstac/config.py))\n", - "- take a look at [stac_fastapi/pgstac/config.py](https://github.com/stac-utils/stac-fastapi-pgstac/blob/main/stac_fastapi/pgstac/config.py) for the settings module.\n", - "\n", - "Any other modifications to the default application will require a custom runtime in your eoAPI deployment. If you do this you will need to provide the full custom runtime (application code and handler) via a Dockerfile. Check out [eoapi-devseed](https://github.com/developmentseed/eoapi-devseed) for an example of building custom runtimes for eoAPI services.\n", - "\n", - "### 3.1.2 Authentication\n", - "stac-fastapi-pgstac does not contain any authentication mechanism out-of-the-box, meaning your STAC API will be accessible to anyone if it is deployed to a public web address. If you want to make your STAC API accessible only with a username/password or token, check out the [FastAPI docs](https://fastapi.tiangolo.com/tutorial/security) for examples of how to add them to the application in a custom runtime.\n", - "\n", - "There is a new project called [stac-auth-proxy](https://github.com/developmentseed/stac-auth-proxy) that can provide fine-grained access controls to a STAC API by adding a proxy layer between users and the actual STAC API. In this workshop stack, the STAC API is exposed through stac-auth-proxy at `http://localhost:8084`.\n", - "\n", - "See [6. STAC Transactions with Authentication](06-stac_transactions_auth.ipynb) for a hands-on exercise with authenticated writes.\n", - "\n", - "### 3.1.3 STAC API interface\n", - "Once your STAC API is up and running, its capabilities will be described in the `/conformance` endpoint response:" - ], - "id": "c8ec4c5b-1156-4608-8ab9-b2cdcbcc47e1" - }, - { - "cell_type": "code", - "metadata": { - "editable": true, - "slideshow": { - "slide_type": "" - }, - "tags": [] - }, - "source": [ - "import json\n", - "import os\n", - "\n", - "import httpx\n", - "\n", - "stac_api_endpoint = os.getenv(\"STAC_API_ENDPOINT\")\n", - "\n", - "conformance_response = httpx.get(f\"{stac_api_endpoint}/conformance\").json()\n", - "\n", - "print(stac_api_endpoint)\n", - "print(json.dumps(conformance_response, indent=2))" - ], - "execution_count": null, - "outputs": [], - "id": "3d4e7fe4-c21b-45c8-a56c-9b649f9f4924" - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The result is hard (for a human) to read, but these conformance classes help client applications (like `pystac-client` or STAC Browser) understand the API's capabilities. The list will change as you enable/disable various extensions or endpoints.\n", - "\n", - "
\n", - "Note: If you visit the urls listed in the conformance classes you may get a 404 - this is expected\n", - "
" - ], - "id": "94af52ad-87b1-461c-8d74-fc0158e488e5" - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 3.2 Collections\n", - "\n", - "The `/collections` endpoint is useful for finding collections in the catalog. To retrieve all collections in the catalog you can simply send a GET request to the `/collections` endpoint. This will return a paginated list (length of each page is set by the `limit` parameter) of all of the collections in the catalog." - ], - "id": "09e419d9-42e8-4b4b-9fdc-8d118bcc87c1" - }, - { - "cell_type": "code", - "metadata": { - "scrolled": true - }, - "source": [ - "collections_response = httpx.get(\n", - " f\"{stac_api_endpoint}/collections\", params={\"limit\": 2}\n", - ").json()\n", - "\n", - "print(json.dumps(collections_response, indent=2))" - ], - "execution_count": null, - "outputs": [], - "id": "ca0033ed-4898-4c21-8aa9-aaf79fc8d570" - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 3.2.1 All Collections\n", - "You can retrieve all of a catalog's collection using the `get_all_collections` method from `pystac-client`:" - ], - "id": "ad74c861-7b4b-491e-9e17-82bcafd1cec4" - }, - { - "cell_type": "code", - "metadata": {}, - "source": [ - "import pystac_client\n", - "\n", - "client = pystac_client.Client.open(stac_api_endpoint)\n", - "\n", - "collections = list(client.get_all_collections())\n", - "for collection in collections:\n", - " print(collection.id)" - ], - "execution_count": null, - "outputs": [], - "id": "48664f4b-453d-47b8-9e3b-24a13c11cfcd" - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 3.2.2 Collection Search Query\n", - "Some APIs contain many many collections so, if the `collection-search` extension is enabled, it can be helpful to apply filters using the available query parameters like:\n", - "- `q`: free-text search parameter\n", - "- `datetime`: temporal filters\n", - "- `bbox`: spatial filters\n", - "- `filter`: cql2-text filters\n", - "\n", - "To check if any STAC API has the `collection-search` extension enabled, you can look for it in the `/conformance` endpoint response." - ], - "id": "c066ffe0-5506-4268-be8b-26329b4e70fd" - }, - { - "cell_type": "code", - "metadata": { - "scrolled": true - }, - "source": [ - "for conformance_class in conformance_response[\"conformsTo\"]:\n", - " if \"collection-search\" in conformance_class:\n", - " print(conformance_class)" - ], - "execution_count": null, - "outputs": [], - "id": "6a463071-ade7-4baf-bba5-500a9304d145" - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Since the `collection-search` base conformance class is listed that means we can pass the `bbox` and `datetime` parameters to the `/collections` endpoint. Additional parameters are unlocked by the various extensions that are implemented alongside the `collection-search` extension. For example, you can also see `https://api.stacspec.org/v1.0.0-rc.1/collection-search#filter` which means we can use the `filter` parameter in requests to the `/collections` endpoint!\n", - "\n", - "
\n", - "stac-fastapi-pgstac ships with the `collection-search` extension paired with the `free-text` extension which enables simple text searches against the collection title, description, and keywords fields.\n", - "
\n", - "\n", - "For a nice view of the available query parameters for the `/collections` endpoint, check out the spiffy API documentation that the `stac-fastapi-pgstac` application generates using `FastAPI`." - ], - "id": "b03ff54f-0548-443c-86f0-1f064f3fe7cf" - }, - { - "cell_type": "code", - "metadata": {}, - "source": [ - "from IPython.display import IFrame\n", - "\n", - "local_stac_api_endpoint = stac_api_endpoint.replace(\"stac-auth-proxy:8000\", \"localhost:8084\")\n", - "api_docs = (\n", - " f\"{local_stac_api_endpoint}/api.html#/default/Get_Collections_collections_get\"\n", - ")\n", - "print(api_docs)\n", - "\n", - "IFrame(\n", - " api_docs,\n", - " 1200,\n", - " 800,\n", - ")" - ], - "execution_count": null, - "outputs": [], - "id": "8201551a-b51d-4ee5-b238-4fd79547afe1" - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Try applying the `filter` parameter to do a cql2-text query on the id field to find the collection you created in the `database` exercies.\n", - "\n", - "
\n", - "Tip: Try out the CQL2 Playground to learn how to write cql2-text or cql2-json queries\n", - "
\n", - "\n" - ], - "id": "5a9f5c87-ef5b-49d3-b6b8-e8fdca6cef1e" - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "If you didn't run Part 2 on Databases, you can either go back to make a username or you can copy one from section 3.2.1 collection ids." - ], - "id": "fb94fe39-5091-41c1-8c2c-a227eb1b5c3c" - }, - { - "cell_type": "code", - "metadata": {}, - "source": [ - "import ipywidgets as widgets\n", - "from IPython.display import display\n", - "\n", - "username_input = widgets.Text(\n", - " value=None,\n", - " placeholder=\"Enter your username\",\n", - " description=\"username:\",\n", - " disabled=False,\n", - ")\n", - "\n", - "display(username_input)" - ], - "execution_count": null, - "outputs": [], - "id": "30844f03-1834-4e93-9494-a59abdc4f651" - }, - { - "cell_type": "code", - "metadata": {}, - "source": [ - "# using pystac-client\n", - "my_collection_search = client.collection_search(\n", - " filter=f\"id LIKE '%{username_input.value}%'\"\n", - ")\n", - "\n", - "results = my_collection_search.collection_list()\n", - "\n", - "if results:\n", - " my_collection = results[0]\n", - " display(my_collection)" - ], - "execution_count": null, - "outputs": [], - "id": "8d9d740d-1e2d-45c4-8fcc-1c7cb31a19cc" - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "
\n", - "Note: If your collection did not appear, try adjusting your collection search terms!\n", - "
" - ], - "id": "1d7fe9b0-f05c-4e0b-a24a-e8040b9d09c4" + "cells": [ + { + "cell_type": "markdown", + "id": "7f4f9109-ac1f-473b-b30b-c35294ae426d", + "metadata": {}, + "source": [ + "# 3. The STAC API: stac-fastapi-pgstac\n", + "\n", + "The STAC API provided by eoAPI is [stac-fastapi-pgstac](https://github.com/stac-utils/stac-fastapi-pgstac): a stac-fastapi application with a pgstac backend.\n", + "stac-fastapi-pgstac translates STAC API requests into pgstac queries and returns the results to the requester.\n", + "\n", + "The stac-fastapi-pgstac STAC API can be accessed using any HTTP client but STAC API clients like `pystac-client` provide a more intuitive interface. In this tutorial you will learn how to use HTTP requests via `httpx` as well as `pystac-client` methods." + ] + }, + { + "cell_type": "markdown", + "id": "c8ec4c5b-1156-4608-8ab9-b2cdcbcc47e1", + "metadata": {}, + "source": [ + "## 3.1 stac-fastapi-pgstac structure\n", + "\n", + "A standard eoAPI deployment will run an unmodified version of the FastAPI application defined in `stac_fastapi.pgstac.app:app` ([source](https://github.com/stac-utils/stac-fastapi-pgstac/blob/main/stac_fastapi/pgstac/app.py)). Unless otherwise specified, all of the extensions except the `transaction` and `bulk-transaction` extensions will be enabled but be sure to double check this in your own deployment.\n", + "\n", + "
\n", + "Warning: Do not turn on the transaction or bulk transactions extensions for a public-facing STAC API without some kind of auth layer enabled! Enabling the transactions extensions enables users to POST collections or items to the database via stac-fastapi-pgstac.\n", + "
\n", + "\n", + "stac-fastapi-pgstac implements a pgstac client that is capable of serving the routes defined by stac-fastapi's base `StacApi` factory class ([source](https://github.com/stac-utils/stac-fastapi/blob/main/stac_fastapi/api/stac_fastapi/api/app.py)). The pgstac client's methods contain the logic for translating API requests into pgstac database queries.\n", + "\n", + "For example, a search request for items in the \"amazing\" collection where the item bounding box intersects (0, 0, 10, 10) would get converted to a PostgreSQL query like this pseudo-sql:\n", + "```sql\n", + "SELECT * FROM items\n", + "WHERE \n", + " collection = 'amazing' AND\n", + " ST_Intersects(bbox, ST_MakeEnvelope(0, 0, 10, 10));\n", + "```\n", + "stac-fastapi-pgstac transforms the search results into the format expected in the API response and return it to the user. If you want to see how the actual SQL queries look in `pgstac`, check out the [pgstac source code](https://github.com/stac-utils/pgstac/tree/main/src/pgstac/sql).\n", + "\n", + "### 3.1.1 Customization\n", + "There are several options in the default stac-fastapi-pgstac application that are configurable at run time via environment variables (using [pydantic's settings features](https://docs.pydantic.dev/latest/concepts/pydantic_settings/)):\n", + "- the `ENABLED_EXTENSIONS` environment variable controls which extensions are enabled\n", + "- `pgstac` database credentials are set by `POSTGRES_*` environment variables ([source](https://github.com/stac-utils/stac-fastapi-pgstac/blob/main/stac_fastapi/pgstac/config.py))\n", + "- take a look at [stac_fastapi/pgstac/config.py](https://github.com/stac-utils/stac-fastapi-pgstac/blob/main/stac_fastapi/pgstac/config.py) for the settings module.\n", + "\n", + "Any other modifications to the default application will require a custom runtime in your eoAPI deployment. If you do this you will need to provide the full custom runtime (application code and handler) via a Dockerfile. Check out [eoapi-devseed](https://github.com/developmentseed/eoapi-devseed) for an example of building custom runtimes for eoAPI services.\n", + "\n", + "### 3.1.2 Authentication\n", + "stac-fastapi-pgstac does not contain any authentication mechanism out-of-the-box, meaning your STAC API will be accessible to anyone if it is deployed to a public web address. If you want to make your STAC API accessible only with a username/password or token, check out the [FastAPI docs](https://fastapi.tiangolo.com/tutorial/security) for examples of how to add them to the application in a custom runtime.\n", + "\n", + "There is a new project called [stac-auth-proxy](https://github.com/developmentseed/stac-auth-proxy) that can provide fine-grained access controls to a STAC API by adding a proxy layer between users and the actual STAC API. In this workshop stack, the STAC API is exposed through stac-auth-proxy at `http://localhost:8084`.\n", + "\n", + "See [6. STAC Transactions with Authentication](06-stac_transactions_auth.ipynb) for a hands-on exercise with authenticated writes.\n", + "\n", + "### 3.1.3 STAC API interface\n", + "Once your STAC API is up and running, its capabilities will be described in the `/conformance` endpoint response:" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "3d4e7fe4-c21b-45c8-a56c-9b649f9f4924", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" }, + "tags": [] + }, + "outputs": [ { - "cell_type": "code", - "metadata": { - "scrolled": true - }, - "source": [ - "# using http client\n", - "print(\n", - " json.dumps(\n", - " httpx.get(\n", - " f\"{stac_api_endpoint}/collections\",\n", - " params={\"filter\": f\"id LIKE '%{username_input.value}%'\"},\n", - " ).json(),\n", - " indent=2,\n", - " )\n", - ")" - ], - "execution_count": null, - "outputs": [], - "id": "5f75438e-cca6-4447-b9cb-f3349ae04c76" - }, + "name": "stdout", + "output_type": "stream", + "text": [ + "http://stac-auth-proxy:8000\n", + "{\n", + " \"conformsTo\": [\n", + " \"http://www.opengis.net/spec/cql2/1.0/conf/basic-cql2\",\n", + " \"http://www.opengis.net/spec/cql2/1.0/conf/cql2-json\",\n", + " \"http://www.opengis.net/spec/cql2/1.0/conf/cql2-text\",\n", + " \"http://www.opengis.net/spec/ogcapi-common-2/1.0/conf/simple-query\",\n", + " \"http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/core\",\n", + " \"http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/geojson\",\n", + " \"http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/oas30\",\n", + " \"http://www.opengis.net/spec/ogcapi-features-3/1.0/conf/features-filter\",\n", + " \"http://www.opengis.net/spec/ogcapi-features-3/1.0/conf/filter\",\n", + " \"https://api.stacspec.org/v1.0.0-rc.1/collection-search\",\n", + " \"https://api.stacspec.org/v1.0.0-rc.1/collection-search#fields\",\n", + " \"https://api.stacspec.org/v1.0.0-rc.1/collection-search#filter\",\n", + " \"https://api.stacspec.org/v1.0.0-rc.1/collection-search#free-text\",\n", + " \"https://api.stacspec.org/v1.0.0-rc.1/collection-search#query\",\n", + " \"https://api.stacspec.org/v1.0.0-rc.1/collection-search#sort\",\n", + " \"https://api.stacspec.org/v1.0.0-rc.2/item-search#filter\",\n", + " \"https://api.stacspec.org/v1.0.0/collections\",\n", + " \"https://api.stacspec.org/v1.0.0/collections/extensions/transaction\",\n", + " \"https://api.stacspec.org/v1.0.0/core\",\n", + " \"https://api.stacspec.org/v1.0.0/item-search\",\n", + " \"https://api.stacspec.org/v1.0.0/item-search#fields\",\n", + " \"https://api.stacspec.org/v1.0.0/item-search#query\",\n", + " \"https://api.stacspec.org/v1.0.0/item-search#sort\",\n", + " \"https://api.stacspec.org/v1.0.0/ogcapi-features\",\n", + " \"https://api.stacspec.org/v1.0.0/ogcapi-features#fields\",\n", + " \"https://api.stacspec.org/v1.0.0/ogcapi-features#query\",\n", + " \"https://api.stacspec.org/v1.0.0/ogcapi-features#sort\",\n", + " \"https://api.stacspec.org/v1.0.0/ogcapi-features/extensions/transaction\"\n", + " ]\n", + "}\n" + ] + } + ], + "source": [ + "import json\n", + "import os\n", + "\n", + "import httpx\n", + "\n", + "stac_api_endpoint = os.getenv(\"STAC_API_ENDPOINT\")\n", + "\n", + "conformance_response = httpx.get(f\"{stac_api_endpoint}/conformance\").json()\n", + "\n", + "print(stac_api_endpoint)\n", + "print(json.dumps(conformance_response, indent=2))" + ] + }, + { + "cell_type": "markdown", + "id": "94af52ad-87b1-461c-8d74-fc0158e488e5", + "metadata": {}, + "source": [ + "The result is hard (for a human) to read, but these conformance classes help client applications (like `pystac-client` or STAC Browser) understand the API's capabilities. The list will change as you enable/disable various extensions or endpoints.\n", + "\n", + "
\n", + "Note: If you visit the urls listed in the conformance classes you may get a 404 - this is expected\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "09e419d9-42e8-4b4b-9fdc-8d118bcc87c1", + "metadata": {}, + "source": [ + "## 3.2 Collections\n", + "\n", + "The `/collections` endpoint is useful for finding collections in the catalog. To retrieve all collections in the catalog you can simply send a GET request to the `/collections` endpoint. This will return a paginated list (length of each page is set by the `limit` parameter) of all of the collections in the catalog." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "ca0033ed-4898-4c21-8aa9-aaf79fc8d570", + "metadata": { + "scrolled": true + }, + "outputs": [ { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now that you found your collection, you have what you need to do an effective item search within your collection! " - ], - "id": "7eae366c-977d-45af-bf19-275e67ee8058" - }, + "name": "stdout", + "output_type": "stream", + "text": [ + "{\n", + " \"links\": [\n", + " {\n", + " \"rel\": \"next\",\n", + " \"type\": \"application/geo+json\",\n", + " \"method\": \"GET\",\n", + " \"href\": \"http://stac-auth-proxy:8000/collections?limit=2&offset=2\"\n", + " },\n", + " {\n", + " \"rel\": \"root\",\n", + " \"type\": \"application/json\",\n", + " \"href\": \"http://stac-auth-proxy:8000/\"\n", + " },\n", + " {\n", + " \"rel\": \"self\",\n", + " \"type\": \"application/json\",\n", + " \"href\": \"http://stac-auth-proxy:8000/collections?limit=2\"\n", + " }\n", + " ],\n", + " \"collections\": [\n", + " {\n", + " \"id\": \"dark-dream-2670-sentinel-2-c1-l2a\",\n", + " \"type\": \"Collection\",\n", + " \"links\": [\n", + " {\n", + " \"rel\": \"items\",\n", + " \"type\": \"application/geo+json\",\n", + " \"href\": \"http://stac-auth-proxy:8000/collections/dark-dream-2670-sentinel-2-c1-l2a/items\"\n", + " },\n", + " {\n", + " \"rel\": \"parent\",\n", + " \"type\": \"application/json\",\n", + " \"href\": \"http://stac-auth-proxy:8000/\"\n", + " },\n", + " {\n", + " \"rel\": \"root\",\n", + " \"type\": \"application/json\",\n", + " \"href\": \"http://stac-auth-proxy:8000/\"\n", + " },\n", + " {\n", + " \"rel\": \"self\",\n", + " \"type\": \"application/json\",\n", + " \"href\": \"http://stac-auth-proxy:8000/collections/dark-dream-2670-sentinel-2-c1-l2a\"\n", + " },\n", + " {\n", + " \"rel\": \"http://www.opengis.net/def/rel/ogc/1.0/queryables\",\n", + " \"type\": \"application/schema+json\",\n", + " \"title\": \"Queryables\",\n", + " \"href\": \"http://stac-auth-proxy:8000/collections/dark-dream-2670-sentinel-2-c1-l2a/queryables\"\n", + " }\n", + " ],\n", + " \"extent\": {\n", + " \"spatial\": {\n", + " \"bbox\": [\n", + " [\n", + " 133.72,\n", + " -35.17,\n", + " 137.72,\n", + " -31.17\n", + " ]\n", + " ]\n", + " },\n", + " \"temporal\": {\n", + " \"interval\": [\n", + " [\n", + " \"2025-01-01T00:00:00Z\",\n", + " \"2025-04-18T00:00:00Z\"\n", + " ]\n", + " ]\n", + " }\n", + " },\n", + " \"license\": \"other\",\n", + " \"description\": \"dark-dream-2670's personal Sentinel-2 L2A collection\",\n", + " \"stac_version\": \"1.1.0\"\n", + " },\n", + " {\n", + " \"id\": \"glad-global-forest-change-1.11\",\n", + " \"type\": \"Collection\",\n", + " \"links\": [\n", + " {\n", + " \"rel\": \"items\",\n", + " \"type\": \"application/geo+json\",\n", + " \"href\": \"http://stac-auth-proxy:8000/collections/glad-global-forest-change-1.11/items\"\n", + " },\n", + " {\n", + " \"rel\": \"parent\",\n", + " \"type\": \"application/json\",\n", + " \"href\": \"http://stac-auth-proxy:8000/\"\n", + " },\n", + " {\n", + " \"rel\": \"root\",\n", + " \"type\": \"application/json\",\n", + " \"href\": \"http://stac-auth-proxy:8000/\"\n", + " },\n", + " {\n", + " \"rel\": \"self\",\n", + " \"type\": \"application/json\",\n", + " \"href\": \"http://stac-auth-proxy:8000/collections/glad-global-forest-change-1.11\"\n", + " },\n", + " {\n", + " \"rel\": \"license\",\n", + " \"href\": \"https://creativecommons.org/licenses/by/4.0/\",\n", + " \"type\": \"text/html\",\n", + " \"title\": \"CC-BY-4.0 license\"\n", + " },\n", + " {\n", + " \"rel\": \"cite-as\",\n", + " \"href\": \"https://doi.org/10.1126/science.1244693\"\n", + " },\n", + " {\n", + " \"rel\": \"http://www.opengis.net/def/rel/ogc/1.0/queryables\",\n", + " \"href\": \"https://stac.maap-project.org/collections/glad-global-forest-change-1.11/queryables\",\n", + " \"type\": \"application/schema+json\",\n", + " \"title\": \"Queryables\"\n", + " },\n", + " {\n", + " \"rel\": \"http://www.opengis.net/def/rel/ogc/1.0/queryables\",\n", + " \"href\": \"https://stac.maap-project.org/collections/glad-global-forest-change-1.11/queryables\",\n", + " \"type\": \"application/schema+json\",\n", + " \"title\": \"Queryables\"\n", + " },\n", + " {\n", + " \"rel\": \"http://www.opengis.net/def/rel/ogc/1.0/queryables\",\n", + " \"href\": \"https://stac.maap-project.org/collections/glad-global-forest-change-1.11/queryables\",\n", + " \"type\": \"application/schema+json\",\n", + " \"title\": \"Queryables\"\n", + " },\n", + " {\n", + " \"rel\": \"http://www.opengis.net/def/rel/ogc/1.0/queryables\",\n", + " \"href\": \"https://stac.maap-project.org/collections/glad-global-forest-change-1.11/queryables\",\n", + " \"type\": \"application/schema+json\",\n", + " \"title\": \"Queryables\"\n", + " },\n", + " {\n", + " \"rel\": \"tilejson\",\n", + " \"href\": \"https://titiler-pgstac.maap-project.org/collections/glad-global-forest-change-1.11/WebMercatorQuad/tilejson.json?colormap=%7B%220%22%3A+%5B0%2C+0%2C+0%5D%2C+%221%22%3A+%5B255%2C+255%2C+0%5D%2C+%222%22%3A+%5B255%2C+243%2C+0%5D%2C+%223%22%3A+%5B255%2C+230%2C+0%5D%2C+%224%22%3A+%5B255%2C+217%2C+0%5D%2C+%225%22%3A+%5B255%2C+204%2C+0%5D%2C+%226%22%3A+%5B255%2C+192%2C+0%5D%2C+%227%22%3A+%5B255%2C+179%2C+0%5D%2C+%228%22%3A+%5B255%2C+166%2C+0%5D%2C+%229%22%3A+%5B255%2C+153%2C+0%5D%2C+%2210%22%3A+%5B255%2C+140%2C+0%5D%2C+%2211%22%3A+%5B255%2C+128%2C+0%5D%2C+%2212%22%3A+%5B255%2C+116%2C+0%5D%2C+%2213%22%3A+%5B255%2C+105%2C+0%5D%2C+%2214%22%3A+%5B255%2C+93%2C+0%5D%2C+%2215%22%3A+%5B255%2C+81%2C+0%5D%2C+%2216%22%3A+%5B255%2C+70%2C+0%5D%2C+%2217%22%3A+%5B255%2C+58%2C+0%5D%2C+%2218%22%3A+%5B255%2C+46%2C+0%5D%2C+%2219%22%3A+%5B255%2C+35%2C+0%5D%2C+%2220%22%3A+%5B255%2C+23%2C+0%5D%2C+%2221%22%3A+%5B255%2C+12%2C+0%5D%2C+%2222%22%3A+%5B255%2C+0%2C+0%5D%2C+%2223%22%3A+%5B0%2C+255%2C+255%5D%7D&assets=lossyear\",\n", + " \"title\": \"TileJSON link for lossyear visualization\"\n", + " },\n", + " {\n", + " \"rel\": \"http://www.opengis.net/def/rel/ogc/1.0/queryables\",\n", + " \"href\": \"https://stac.maap-project.org/collections/glad-global-forest-change-1.11/queryables\",\n", + " \"type\": \"application/schema+json\",\n", + " \"title\": \"Queryables\"\n", + " },\n", + " {\n", + " \"rel\": \"http://www.opengis.net/def/rel/ogc/1.0/queryables\",\n", + " \"type\": \"application/schema+json\",\n", + " \"title\": \"Queryables\",\n", + " \"href\": \"http://stac-auth-proxy:8000/collections/glad-global-forest-change-1.11/queryables\"\n", + " }\n", + " ],\n", + " \"title\": \"GLAD: Global Forest Change 2000-2023 v1.11\",\n", + " \"extent\": {\n", + " \"spatial\": {\n", + " \"bbox\": [\n", + " [\n", + " -180.0,\n", + " -90.0,\n", + " 180.0,\n", + " 90.0\n", + " ]\n", + " ]\n", + " },\n", + " \"temporal\": {\n", + " \"interval\": [\n", + " [\n", + " \"2000-01-01T00:00:00Z\",\n", + " \"2023-12-31T23:59:59Z\"\n", + " ]\n", + " ]\n", + " }\n", + " },\n", + " \"license\": \"CC-BY-4.0\",\n", + " \"renders\": {\n", + " \"gain\": {\n", + " \"assets\": [\n", + " \"gain\"\n", + " ],\n", + " \"colormap\": {\n", + " \"0\": [\n", + " 0,\n", + " 0,\n", + " 0\n", + " ],\n", + " \"1\": [\n", + " 0,\n", + " 0,\n", + " 255\n", + " ]\n", + " }\n", + " },\n", + " \"lossyear\": {\n", + " \"assets\": [\n", + " \"lossyear\"\n", + " ],\n", + " \"colormap\": {\n", + " \"0\": [\n", + " 0,\n", + " 0,\n", + " 0\n", + " ],\n", + " \"1\": [\n", + " 255,\n", + " 255,\n", + " 0\n", + " ],\n", + " \"2\": [\n", + " 255,\n", + " 243,\n", + " 0\n", + " ],\n", + " \"3\": [\n", + " 255,\n", + " 230,\n", + " 0\n", + " ],\n", + " \"4\": [\n", + " 255,\n", + " 217,\n", + " 0\n", + " ],\n", + " \"5\": [\n", + " 255,\n", + " 204,\n", + " 0\n", + " ],\n", + " \"6\": [\n", + " 255,\n", + " 192,\n", + " 0\n", + " ],\n", + " \"7\": [\n", + " 255,\n", + " 179,\n", + " 0\n", + " ],\n", + " \"8\": [\n", + " 255,\n", + " 166,\n", + " 0\n", + " ],\n", + " \"9\": [\n", + " 255,\n", + " 153,\n", + " 0\n", + " ],\n", + " \"10\": [\n", + " 255,\n", + " 140,\n", + " 0\n", + " ],\n", + " \"11\": [\n", + " 255,\n", + " 128,\n", + " 0\n", + " ],\n", + " \"12\": [\n", + " 255,\n", + " 116,\n", + " 0\n", + " ],\n", + " \"13\": [\n", + " 255,\n", + " 105,\n", + " 0\n", + " ],\n", + " \"14\": [\n", + " 255,\n", + " 93,\n", + " 0\n", + " ],\n", + " \"15\": [\n", + " 255,\n", + " 81,\n", + " 0\n", + " ],\n", + " \"16\": [\n", + " 255,\n", + " 70,\n", + " 0\n", + " ],\n", + " \"17\": [\n", + " 255,\n", + " 58,\n", + " 0\n", + " ],\n", + " \"18\": [\n", + " 255,\n", + " 46,\n", + " 0\n", + " ],\n", + " \"19\": [\n", + " 255,\n", + " 35,\n", + " 0\n", + " ],\n", + " \"20\": [\n", + " 255,\n", + " 23,\n", + " 0\n", + " ],\n", + " \"21\": [\n", + " 255,\n", + " 12,\n", + " 0\n", + " ],\n", + " \"22\": [\n", + " 255,\n", + " 0,\n", + " 0\n", + " ],\n", + " \"23\": [\n", + " 0,\n", + " 255,\n", + " 255\n", + " ]\n", + " }\n", + " },\n", + " \"treecover2000\": {\n", + " \"assets\": [\n", + " \"treecover2000\"\n", + " ],\n", + " \"colormap\": {\n", + " \"0\": [\n", + " 0,\n", + " 0,\n", + " 0\n", + " ],\n", + " \"1\": [\n", + " 0,\n", + " 2,\n", + " 0\n", + " ],\n", + " \"2\": [\n", + " 0,\n", + " 5,\n", + " 0\n", + " ],\n", + " \"3\": [\n", + " 0,\n", + " 7,\n", + " 0\n", + " ],\n", + " \"4\": [\n", + " 0,\n", + " 10,\n", + " 0\n", + " ],\n", + " \"5\": [\n", + " 0,\n", + " 12,\n", + " 0\n", + " ],\n", + " \"6\": [\n", + " 0,\n", + " 15,\n", + " 0\n", + " ],\n", + " \"7\": [\n", + " 0,\n", + " 17,\n", + " 0\n", + " ],\n", + " \"8\": [\n", + " 0,\n", + " 20,\n", + " 0\n", + " ],\n", + " \"9\": [\n", + " 0,\n", + " 22,\n", + " 0\n", + " ],\n", + " \"10\": [\n", + " 0,\n", + " 25,\n", + " 0\n", + " ],\n", + " \"11\": [\n", + " 0,\n", + " 28,\n", + " 0\n", + " ],\n", + " \"12\": [\n", + " 0,\n", + " 30,\n", + " 0\n", + " ],\n", + " \"13\": [\n", + " 0,\n", + " 33,\n", + " 0\n", + " ],\n", + " \"14\": [\n", + " 0,\n", + " 35,\n", + " 0\n", + " ],\n", + " \"15\": [\n", + " 0,\n", + " 38,\n", + " 0\n", + " ],\n", + " \"16\": [\n", + " 0,\n", + " 40,\n", + " 0\n", + " ],\n", + " \"17\": [\n", + " 0,\n", + " 43,\n", + " 0\n", + " ],\n", + " \"18\": [\n", + " 0,\n", + " 45,\n", + " 0\n", + " ],\n", + " \"19\": [\n", + " 0,\n", + " 48,\n", + " 0\n", + " ],\n", + " \"20\": [\n", + " 0,\n", + " 51,\n", + " 0\n", + " ],\n", + " \"21\": [\n", + " 0,\n", + " 53,\n", + " 0\n", + " ],\n", + " \"22\": [\n", + " 0,\n", + " 56,\n", + " 0\n", + " ],\n", + " \"23\": [\n", + " 0,\n", + " 58,\n", + " 0\n", + " ],\n", + " \"24\": [\n", + " 0,\n", + " 61,\n", + " 0\n", + " ],\n", + " \"25\": [\n", + " 0,\n", + " 63,\n", + " 0\n", + " ],\n", + " \"26\": [\n", + " 0,\n", + " 66,\n", + " 0\n", + " ],\n", + " \"27\": [\n", + " 0,\n", + " 68,\n", + " 0\n", + " ],\n", + " \"28\": [\n", + " 0,\n", + " 71,\n", + " 0\n", + " ],\n", + " \"29\": [\n", + " 0,\n", + " 73,\n", + " 0\n", + " ],\n", + " \"30\": [\n", + " 0,\n", + " 76,\n", + " 0\n", + " ],\n", + " \"31\": [\n", + " 0,\n", + " 79,\n", + " 0\n", + " ],\n", + " \"32\": [\n", + " 0,\n", + " 81,\n", + " 0\n", + " ],\n", + " \"33\": [\n", + " 0,\n", + " 84,\n", + " 0\n", + " ],\n", + " \"34\": [\n", + " 0,\n", + " 86,\n", + " 0\n", + " ],\n", + " \"35\": [\n", + " 0,\n", + " 89,\n", + " 0\n", + " ],\n", + " \"36\": [\n", + " 0,\n", + " 91,\n", + " 0\n", + " ],\n", + " \"37\": [\n", + " 0,\n", + " 94,\n", + " 0\n", + " ],\n", + " \"38\": [\n", + " 0,\n", + " 96,\n", + " 0\n", + " ],\n", + " \"39\": [\n", + " 0,\n", + " 99,\n", + " 0\n", + " ],\n", + " \"40\": [\n", + " 0,\n", + " 102,\n", + " 0\n", + " ],\n", + " \"41\": [\n", + " 0,\n", + " 104,\n", + " 0\n", + " ],\n", + " \"42\": [\n", + " 0,\n", + " 107,\n", + " 0\n", + " ],\n", + " \"43\": [\n", + " 0,\n", + " 109,\n", + " 0\n", + " ],\n", + " \"44\": [\n", + " 0,\n", + " 112,\n", + " 0\n", + " ],\n", + " \"45\": [\n", + " 0,\n", + " 114,\n", + " 0\n", + " ],\n", + " \"46\": [\n", + " 0,\n", + " 117,\n", + " 0\n", + " ],\n", + " \"47\": [\n", + " 0,\n", + " 119,\n", + " 0\n", + " ],\n", + " \"48\": [\n", + " 0,\n", + " 122,\n", + " 0\n", + " ],\n", + " \"49\": [\n", + " 0,\n", + " 124,\n", + " 0\n", + " ],\n", + " \"50\": [\n", + " 0,\n", + " 127,\n", + " 0\n", + " ],\n", + " \"51\": [\n", + " 0,\n", + " 130,\n", + " 0\n", + " ],\n", + " \"52\": [\n", + " 0,\n", + " 132,\n", + " 0\n", + " ],\n", + " \"53\": [\n", + " 0,\n", + " 135,\n", + " 0\n", + " ],\n", + " \"54\": [\n", + " 0,\n", + " 137,\n", + " 0\n", + " ],\n", + " \"55\": [\n", + " 0,\n", + " 140,\n", + " 0\n", + " ],\n", + " \"56\": [\n", + " 0,\n", + " 142,\n", + " 0\n", + " ],\n", + " \"57\": [\n", + " 0,\n", + " 145,\n", + " 0\n", + " ],\n", + " \"58\": [\n", + " 0,\n", + " 147,\n", + " 0\n", + " ],\n", + " \"59\": [\n", + " 0,\n", + " 150,\n", + " 0\n", + " ],\n", + " \"60\": [\n", + " 0,\n", + " 153,\n", + " 0\n", + " ],\n", + " \"61\": [\n", + " 0,\n", + " 155,\n", + " 0\n", + " ],\n", + " \"62\": [\n", + " 0,\n", + " 158,\n", + " 0\n", + " ],\n", + " \"63\": [\n", + " 0,\n", + " 160,\n", + " 0\n", + " ],\n", + " \"64\": [\n", + " 0,\n", + " 163,\n", + " 0\n", + " ],\n", + " \"65\": [\n", + " 0,\n", + " 165,\n", + " 0\n", + " ],\n", + " \"66\": [\n", + " 0,\n", + " 168,\n", + " 0\n", + " ],\n", + " \"67\": [\n", + " 0,\n", + " 170,\n", + " 0\n", + " ],\n", + " \"68\": [\n", + " 0,\n", + " 173,\n", + " 0\n", + " ],\n", + " \"69\": [\n", + " 0,\n", + " 175,\n", + " 0\n", + " ],\n", + " \"70\": [\n", + " 0,\n", + " 178,\n", + " 0\n", + " ],\n", + " \"71\": [\n", + " 0,\n", + " 181,\n", + " 0\n", + " ],\n", + " \"72\": [\n", + " 0,\n", + " 183,\n", + " 0\n", + " ],\n", + " \"73\": [\n", + " 0,\n", + " 186,\n", + " 0\n", + " ],\n", + " \"74\": [\n", + " 0,\n", + " 188,\n", + " 0\n", + " ],\n", + " \"75\": [\n", + " 0,\n", + " 191,\n", + " 0\n", + " ],\n", + " \"76\": [\n", + " 0,\n", + " 193,\n", + " 0\n", + " ],\n", + " \"77\": [\n", + " 0,\n", + " 196,\n", + " 0\n", + " ],\n", + " \"78\": [\n", + " 0,\n", + " 198,\n", + " 0\n", + " ],\n", + " \"79\": [\n", + " 0,\n", + " 201,\n", + " 0\n", + " ],\n", + " \"80\": [\n", + " 0,\n", + " 204,\n", + " 0\n", + " ],\n", + " \"81\": [\n", + " 0,\n", + " 206,\n", + " 0\n", + " ],\n", + " \"82\": [\n", + " 0,\n", + " 209,\n", + " 0\n", + " ],\n", + " \"83\": [\n", + " 0,\n", + " 211,\n", + " 0\n", + " ],\n", + " \"84\": [\n", + " 0,\n", + " 214,\n", + " 0\n", + " ],\n", + " \"85\": [\n", + " 0,\n", + " 216,\n", + " 0\n", + " ],\n", + " \"86\": [\n", + " 0,\n", + " 219,\n", + " 0\n", + " ],\n", + " \"87\": [\n", + " 0,\n", + " 221,\n", + " 0\n", + " ],\n", + " \"88\": [\n", + " 0,\n", + " 224,\n", + " 0\n", + " ],\n", + " \"89\": [\n", + " 0,\n", + " 226,\n", + " 0\n", + " ],\n", + " \"90\": [\n", + " 0,\n", + " 229,\n", + " 0\n", + " ],\n", + " \"91\": [\n", + " 0,\n", + " 232,\n", + " 0\n", + " ],\n", + " \"92\": [\n", + " 0,\n", + " 234,\n", + " 0\n", + " ],\n", + " \"93\": [\n", + " 0,\n", + " 237,\n", + " 0\n", + " ],\n", + " \"94\": [\n", + " 0,\n", + " 239,\n", + " 0\n", + " ],\n", + " \"95\": [\n", + " 0,\n", + " 242,\n", + " 0\n", + " ],\n", + " \"96\": [\n", + " 0,\n", + " 244,\n", + " 0\n", + " ],\n", + " \"97\": [\n", + " 0,\n", + " 247,\n", + " 0\n", + " ],\n", + " \"98\": [\n", + " 0,\n", + " 249,\n", + " 0\n", + " ],\n", + " \"99\": [\n", + " 0,\n", + " 252,\n", + " 0\n", + " ],\n", + " \"100\": [\n", + " 0,\n", + " 255,\n", + " 0\n", + " ]\n", + " }\n", + " }\n", + " },\n", + " \"version\": \"1.11\",\n", + " \"keywords\": [\n", + " \"forest\",\n", + " \"change\",\n", + " \"deforestation\"\n", + " ],\n", + " \"providers\": [\n", + " {\n", + " \"url\": \"https://glad.umd.edu/\",\n", + " \"name\": \"UMD/GLAD\",\n", + " \"roles\": [\n", + " \"producer\",\n", + " \"processor\",\n", + " \"licensor\"\n", + " ],\n", + " \"description\": \"University of Maryland Global Land Analysis & Discovery laboratory (GLAD)\"\n", + " },\n", + " {\n", + " \"url\": \"https://maap-project.org\",\n", + " \"name\": \"NASA/MAAP\",\n", + " \"roles\": [\n", + " \"processor\",\n", + " \"host\"\n", + " ],\n", + " \"description\": \"The ESA-NASA Multi-Mission Algorithm and Analysis Platform\"\n", + " }\n", + " ],\n", + " \"description\": \"Results from time-series analysis of Landsat images in characterizing global forest extent and change from 2000 through 2023. For additional information about these results, please see the associated journal article (Hansen et al., Science 2013).\\n\\nWeb-based visualizations of these results are also available at our main site:\\n\\n> https://glad.earthengine.app/view/global-forest-change\\n\\nPlease use that URL when linking to this dataset.\\n\\nWe anticipate releasing updated versions of this dataset. To keep up to date with the latest updates, and to help us better understand how these data are used, please [register as a user](https://docs.google.com/a/google.com/forms/d/1AkAUb4kfF7pUTOADPGEW7Do2oRhdDUPYb2mzIr8OIx4/viewform) . Thanks! \",\n", + " \"item_assets\": {\n", + " \"gain\": {\n", + " \"type\": \"image/tiff; application=geotiff; profile=cloud-optimized\",\n", + " \"roles\": [\n", + " \"data\"\n", + " ],\n", + " \"title\": \"Global forest cover gain 2000-2012\",\n", + " \"description\": \"Forest gain during the period 2000-2012, defined as the inverse of loss, or a non-forest to forest change entirely within the study period. Encoded as either 1 (gain) or 0 (no gain).\",\n", + " \"raster:bands\": [\n", + " {\n", + " \"scale\": 1.0,\n", + " \"offset\": 0.0,\n", + " \"sampling\": \"area\",\n", + " \"data_type\": \"uint8\"\n", + " }\n", + " ],\n", + " \"classification:classes\": [\n", + " {\n", + " \"name\": \"no-gain\",\n", + " \"value\": 0,\n", + " \"color_hint\": \"000000\",\n", + " \"description\": \"no forest gain between 2000 and 2012\"\n", + " },\n", + " {\n", + " \"name\": \"gain\",\n", + " \"value\": 1,\n", + " \"color_hint\": \"0000ff\",\n", + " \"description\": \"forest loss between 2000 and 2023\"\n", + " }\n", + " ]\n", + " },\n", + " \"datamask\": {\n", + " \"type\": \"image/tiff; application=geotiff; profile=cloud-optimized\",\n", + " \"roles\": [\n", + " \"data\"\n", + " ],\n", + " \"title\": \"Data mask\",\n", + " \"description\": \"Three values representing areas of no data (0), mapped land surface (1), and persistent water bodies (2) based on 2000-2012.\",\n", + " \"raster:bands\": [\n", + " {\n", + " \"scale\": 1.0,\n", + " \"offset\": 0.0,\n", + " \"sampling\": \"area\",\n", + " \"data_type\": \"uint8\"\n", + " }\n", + " ],\n", + " \"classification:classes\": [\n", + " {\n", + " \"name\": \"nodata\",\n", + " \"value\": 0,\n", + " \"nodata\": true,\n", + " \"description\": \"no data\"\n", + " },\n", + " {\n", + " \"name\": \"maaped-land-surface\",\n", + " \"value\": 1,\n", + " \"description\": \"mapped land survace\"\n", + " },\n", + " {\n", + " \"name\": \"persistent-water-bodies\",\n", + " \"value\": 2,\n", + " \"description\": \"persistent water bodies\"\n", + " }\n", + " ]\n", + " },\n", + " \"lossyear\": {\n", + " \"type\": \"image/tiff; application=geotiff; profile=cloud-optimized\",\n", + " \"roles\": [\n", + " \"data\"\n", + " ],\n", + " \"title\": \"Year of gross forest cover loss event\",\n", + " \"description\": \"Forest loss during the period 2000-2023, defined as a stand-replacement disturbance, or a change from a forest to non-forest state. Encoded as either 0 (no loss) or else a value in the range 1-20, representing loss detected primarily in the year 2001-2023, respectively.\",\n", + " \"raster:bands\": [\n", + " {\n", + " \"scale\": 1.0,\n", + " \"offset\": 0.0,\n", + " \"sampling\": \"area\",\n", + " \"data_type\": \"uint8\"\n", + " }\n", + " ],\n", + " \"classification:classes\": [\n", + " {\n", + " \"name\": \"no-loss\",\n", + " \"value\": 0,\n", + " \"description\": \"no loss\"\n", + " },\n", + " {\n", + " \"name\": \"forest-lost-2001\",\n", + " \"value\": 1,\n", + " \"color_hint\": \"ffff00\",\n", + " \"description\": \"forest lost in 2001\"\n", + " },\n", + " {\n", + " \"name\": \"forest-lost-2002\",\n", + " \"value\": 2,\n", + " \"color_hint\": \"fff300\",\n", + " \"description\": \"forest lost in 2002\"\n", + " },\n", + " {\n", + " \"name\": \"forest-lost-2003\",\n", + " \"value\": 3,\n", + " \"color_hint\": \"ffe600\",\n", + " \"description\": \"forest lost in 2003\"\n", + " },\n", + " {\n", + " \"name\": \"forest-lost-2004\",\n", + " \"value\": 4,\n", + " \"color_hint\": \"ffd900\",\n", + " \"description\": \"forest lost in 2004\"\n", + " },\n", + " {\n", + " \"name\": \"forest-lost-2005\",\n", + " \"value\": 5,\n", + " \"color_hint\": \"ffcc00\",\n", + " \"description\": \"forest lost in 2005\"\n", + " },\n", + " {\n", + " \"name\": \"forest-lost-2006\",\n", + " \"value\": 6,\n", + " \"color_hint\": \"ffc000\",\n", + " \"description\": \"forest lost in 2006\"\n", + " },\n", + " {\n", + " \"name\": \"forest-lost-2007\",\n", + " \"value\": 7,\n", + " \"color_hint\": \"ffb300\",\n", + " \"description\": \"forest lost in 2007\"\n", + " },\n", + " {\n", + " \"name\": \"forest-lost-2008\",\n", + " \"value\": 8,\n", + " \"color_hint\": \"ffa600\",\n", + " \"description\": \"forest lost in 2008\"\n", + " },\n", + " {\n", + " \"name\": \"forest-lost-2009\",\n", + " \"value\": 9,\n", + " \"color_hint\": \"ff9900\",\n", + " \"description\": \"forest lost in 2009\"\n", + " },\n", + " {\n", + " \"name\": \"forest-lost-2010\",\n", + " \"value\": 10,\n", + " \"color_hint\": \"ff8c00\",\n", + " \"description\": \"forest lost in 2010\"\n", + " },\n", + " {\n", + " \"name\": \"forest-lost-2011\",\n", + " \"value\": 11,\n", + " \"color_hint\": \"ff8000\",\n", + " \"description\": \"forest lost in 2011\"\n", + " },\n", + " {\n", + " \"name\": \"forest-lost-2012\",\n", + " \"value\": 12,\n", + " \"color_hint\": \"ff7400\",\n", + " \"description\": \"forest lost in 2012\"\n", + " },\n", + " {\n", + " \"name\": \"forest-lost-2013\",\n", + " \"value\": 13,\n", + " \"color_hint\": \"ff6900\",\n", + " \"description\": \"forest lost in 2013\"\n", + " },\n", + " {\n", + " \"name\": \"forest-lost-2014\",\n", + " \"value\": 14,\n", + " \"color_hint\": \"ff5d00\",\n", + " \"description\": \"forest lost in 2014\"\n", + " },\n", + " {\n", + " \"name\": \"forest-lost-2015\",\n", + " \"value\": 15,\n", + " \"color_hint\": \"ff5100\",\n", + " \"description\": \"forest lost in 2015\"\n", + " },\n", + " {\n", + " \"name\": \"forest-lost-2016\",\n", + " \"value\": 16,\n", + " \"color_hint\": \"ff4600\",\n", + " \"description\": \"forest lost in 2016\"\n", + " },\n", + " {\n", + " \"name\": \"forest-lost-2017\",\n", + " \"value\": 17,\n", + " \"color_hint\": \"ff3a00\",\n", + " \"description\": \"forest lost in 2017\"\n", + " },\n", + " {\n", + " \"name\": \"forest-lost-2018\",\n", + " \"value\": 18,\n", + " \"color_hint\": \"ff2e00\",\n", + " \"description\": \"forest lost in 2018\"\n", + " },\n", + " {\n", + " \"name\": \"forest-lost-2019\",\n", + " \"value\": 19,\n", + " \"color_hint\": \"ff2300\",\n", + " \"description\": \"forest lost in 2019\"\n", + " },\n", + " {\n", + " \"name\": \"forest-lost-2020\",\n", + " \"value\": 20,\n", + " \"color_hint\": \"ff1700\",\n", + " \"description\": \"forest lost in 2020\"\n", + " },\n", + " {\n", + " \"name\": \"forest-lost-2021\",\n", + " \"value\": 21,\n", + " \"color_hint\": \"ff0c00\",\n", + " \"description\": \"forest lost in 2021\"\n", + " },\n", + " {\n", + " \"name\": \"forest-lost-2022\",\n", + " \"value\": 22,\n", + " \"color_hint\": \"ff0000\",\n", + " \"description\": \"forest lost in 2022\"\n", + " },\n", + " {\n", + " \"name\": \"forest-lost-2023\",\n", + " \"value\": 23,\n", + " \"color_hint\": \"00ffff\",\n", + " \"description\": \"forest lost in 2023\"\n", + " }\n", + " ]\n", + " },\n", + " \"treecover2000\": {\n", + " \"type\": \"image/tiff; application=geotiff; profile=cloud-optimized\",\n", + " \"roles\": [\n", + " \"data\"\n", + " ],\n", + " \"title\": \"Tree canopy cover for year 2000\",\n", + " \"description\": \"Tree cover in the year 2000, defined as canopy closure for all vegetation taller than 5m in height. Encoded as a percentage per output grid cell, in the range 0-100.\",\n", + " \"raster:bands\": [\n", + " {\n", + " \"scale\": 1.0,\n", + " \"offset\": 0.0,\n", + " \"sampling\": \"area\",\n", + " \"data_type\": \"uint8\"\n", + " }\n", + " ]\n", + " }\n", + " },\n", + " \"stac_version\": \"1.1.0\",\n", + " \"stac_extensions\": [\n", + " \"https://stac-extensions.github.io/version/v1.2.0/schema.json\",\n", + " \"https://stac-extensions.github.io/scientific/v1.0.0/schema.json\",\n", + " \"https://stac-extensions.github.io/raster/v1.1.0/schema.json\",\n", + " \"https://stac-extensions.github.io/render/v2.0.0/schema.json\",\n", + " \"https://stac-extensions.github.io/web-map-links/v1.3.0/schema.json\"\n", + " ],\n", + " \"sci:publications\": [\n", + " {\n", + " \"doi\": \"10.1126/science.1244693\",\n", + " \"citation\": \"Hansen, M. C., P. V. Potapov, R. Moore, M. Hancher, S. A. Turubanova, A. Tyukavina, D. Thau, S. V. Stehman, S. J. Goetz, T. R. Loveland, A. Kommareddy, A. Egorov, L. Chini, C. O. Justice, and J. R. G. Townshend. 2013. High-Resolution Global Maps of 21st-Century Forest Cover Change. Science 342 (15 November): 850-53. Data available on-line from: https://glad.earthengine.app/view/global-forest-change.\"\n", + " }\n", + " ]\n", + " }\n", + " ],\n", + " \"numberMatched\": 4,\n", + " \"numberReturned\": 2,\n", + " \"stac_extensions\": [\n", + " \"https://stac-extensions.github.io/authentication/v1.1.0/schema.json\"\n", + " ],\n", + " \"auth:schemes\": {\n", + " \"oidc\": {\n", + " \"type\": \"openIdConnect\",\n", + " \"openIdConnectUrl\": \"http://localhost:8085/.well-known/openid-configuration\"\n", + " }\n", + " }\n", + "}\n" + ] + } + ], + "source": [ + "collections_response = httpx.get(\n", + " f\"{stac_api_endpoint}/collections\", params={\"limit\": 2}\n", + ").json()\n", + "\n", + "print(json.dumps(collections_response, indent=2))" + ] + }, + { + "cell_type": "markdown", + "id": "ad74c861-7b4b-491e-9e17-82bcafd1cec4", + "metadata": {}, + "source": [ + "### 3.2.1 All Collections\n", + "You can retrieve all of a catalog's collection using the `get_all_collections` method from `pystac-client`:" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "48664f4b-453d-47b8-9e3b-24a13c11cfcd", + "metadata": {}, + "outputs": [ { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 3.3 Items\n", - "\n", - "Once you have the collection ID there are several ways to perform an effective item search:\n", - "- GET request to `/collections/{collection_id}/items`\n", - "- GET or POST request to `/search`\n", - "\n", - "There are not any particular advantages to either approach unless you want to search for items using an intersection with a geometry in which case you should use a POST request to `/search` with the `intersects` parameter in the request body (instead of url-encoding a geojson!).\n", - "\n", - "Item search request responses will be returned in pages with `{limit}` results. If your search returns more than a single page of results, the next page will be retrievable via the `next` link in the list of `links`." - ], - "id": "56657240-5905-4ddf-a76a-afd2a28959b0" - }, + "name": "stdout", + "output_type": "stream", + "text": [ + "dark-dream-2670-sentinel-2-c1-l2a\n", + "glad-global-forest-change-1.11\n", + "quiet-surf-4719-sentinel-2-c1-l2a\n", + "super-mouse-3144-sentinel-2-c1-l2a\n" + ] + } + ], + "source": [ + "import pystac_client\n", + "\n", + "client = pystac_client.Client.open(stac_api_endpoint)\n", + "\n", + "collections = list(client.get_all_collections())\n", + "for collection in collections:\n", + " print(collection.id)" + ] + }, + { + "cell_type": "markdown", + "id": "c066ffe0-5506-4268-be8b-26329b4e70fd", + "metadata": {}, + "source": [ + "### 3.2.2 Collection Search Query\n", + "Some APIs contain many many collections so, if the `collection-search` extension is enabled, it can be helpful to apply filters using the available query parameters like:\n", + "- `q`: free-text search parameter\n", + "- `datetime`: temporal filters\n", + "- `bbox`: spatial filters\n", + "- `filter`: cql2-text filters\n", + "\n", + "To check if any STAC API has the `collection-search` extension enabled, you can look for it in the `/conformance` endpoint response." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "6a463071-ade7-4baf-bba5-500a9304d145", + "metadata": { + "scrolled": true + }, + "outputs": [ { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 3.3.1 Item Search\n", - "\n", - "Use the `/search` endpoint to find all items in your collection with a timestamp after April 4, 2025" - ], - "id": "74d1fb0b-e830-4fb0-8619-f9cfd3019111" - }, + "name": "stdout", + "output_type": "stream", + "text": [ + "https://api.stacspec.org/v1.0.0-rc.1/collection-search\n", + "https://api.stacspec.org/v1.0.0-rc.1/collection-search#fields\n", + "https://api.stacspec.org/v1.0.0-rc.1/collection-search#filter\n", + "https://api.stacspec.org/v1.0.0-rc.1/collection-search#free-text\n", + "https://api.stacspec.org/v1.0.0-rc.1/collection-search#query\n", + "https://api.stacspec.org/v1.0.0-rc.1/collection-search#sort\n" + ] + } + ], + "source": [ + "for conformance_class in conformance_response[\"conformsTo\"]:\n", + " if \"collection-search\" in conformance_class:\n", + " print(conformance_class)" + ] + }, + { + "cell_type": "markdown", + "id": "b03ff54f-0548-443c-86f0-1f064f3fe7cf", + "metadata": {}, + "source": [ + "Since the `collection-search` base conformance class is listed that means we can pass the `bbox` and `datetime` parameters to the `/collections` endpoint. Additional parameters are unlocked by the various extensions that are implemented alongside the `collection-search` extension. For example, you can also see `https://api.stacspec.org/v1.0.0-rc.1/collection-search#filter` which means we can use the `filter` parameter in requests to the `/collections` endpoint!\n", + "\n", + "
\n", + "stac-fastapi-pgstac ships with the `collection-search` extension paired with the `free-text` extension which enables simple text searches against the collection title, description, and keywords fields.\n", + "
\n", + "\n", + "For a nice view of the available query parameters for the `/collections` endpoint, check out the spiffy API documentation that the `stac-fastapi-pgstac` application generates using `FastAPI`." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "8201551a-b51d-4ee5-b238-4fd79547afe1", + "metadata": {}, + "outputs": [ { - "cell_type": "code", - "metadata": {}, - "source": [ - "from datetime import datetime, UTC\n", - "\n", - "search = client.search(\n", - " collections=[my_collection.id],\n", - " datetime=[datetime(2025, 1, 4), None],\n", - ")\n", - "\n", - "items = search.item_collection()\n", - "\n", - "print(f\"found {len(items)} items\")\n", - "items[0]" - ], - "execution_count": null, - "outputs": [], - "id": "e73a9ef7-d8f8-407d-965e-c549f112899c" + "name": "stdout", + "output_type": "stream", + "text": [ + "http://localhost:8084/api.html#/default/Get_Collections_collections_get\n" + ] }, { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The same query can be made with an HTTP client:" + "data": { + "text/html": [ + "\n", + " \n", + " " ], - "id": "3e504a62-0c1e-4f00-aa54-e286252c493b" - }, + "text/plain": [ + "" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from IPython.display import IFrame\n", + "\n", + "local_stac_api_endpoint = os.getenv(\n", + " \"STAC_API_BROWSER_URL\"\n", + ") or stac_api_endpoint.replace(\"stac-auth-proxy:8000\", \"localhost:8084\")\n", + "api_docs = (\n", + " f\"{local_stac_api_endpoint}/api.html#/default/Get_Collections_collections_get\"\n", + ")\n", + "print(api_docs)\n", + "\n", + "IFrame(\n", + " api_docs,\n", + " 1200,\n", + " 800,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "5a9f5c87-ef5b-49d3-b6b8-e8fdca6cef1e", + "metadata": {}, + "source": [ + "Try applying the `filter` parameter to do a cql2-text query on the id field to find the collection you created in the `database` exercies.\n", + "\n", + "
\n", + "Tip: Try out the CQL2 Playground to learn how to write cql2-text or cql2-json queries\n", + "
\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "fb94fe39-5091-41c1-8c2c-a227eb1b5c3c", + "metadata": {}, + "source": [ + "If you didn't run Part 2 on Databases, you can either go back to make a username or you can copy one from section 3.2.1 collection ids." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "30844f03-1834-4e93-9494-a59abdc4f651", + "metadata": {}, + "outputs": [ { - "cell_type": "code", - "metadata": { - "scrolled": true + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "37bee02111a6435f86b44e27a42b2683", + "version_major": 2, + "version_minor": 0 }, - "source": [ - "datetime_string = datetime(2025, 1, 4, tzinfo=UTC).isoformat()\n", - "\n", - "item_search_request = httpx.get(\n", - " f\"{stac_api_endpoint}/search\",\n", - " params={\n", - " \"collections\": my_collection.id,\n", - " \"datetime\": f\"{datetime_string}/..\", # open interval from 2025-04-04 forward\n", - " \"limit\": 1, # one result per page for brevity in this example\n", - " },\n", - ")\n", - "\n", - "print(json.dumps(item_search_request.json(), indent=2))" - ], - "execution_count": null, - "outputs": [], - "id": "57405533-6bbf-402c-9b26-99b97ab234ec" - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "stac-fastapi-pgstac constructs the `next` link using a token that it can pass to a `pgstac` query to retrieve the next page of results from this search. STAC API clients like `pystac-client` use these links to concatenate paginated results without any additional input from the user." - ], - "id": "3625785f-8aca-4465-93bc-3c1370c0e2fb" - }, + "text/plain": [ + "Text(value='super-mouse-3144', description='username:', placeholder='Enter your username')" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import ipywidgets as widgets\n", + "from IPython.display import display\n", + "\n", + "# Prefill with the username of the most recent sentinel-2 collection in the\n", + "# catalog (created in notebook 02). If you picked your own username there,\n", + "# replace the value below - but keep in mind that notebook 02 generates a\n", + "# random default (e.g. \"quiet-surf-4719\"), so what you type here must match\n", + "# the collection id that actually exists in the catalog (see section 3.2.1).\n", + "_collections = (\n", + " httpx.get(f\"{stac_api_endpoint}/collections\", timeout=30)\n", + " .json()\n", + " .get(\"collections\", [])\n", + ")\n", + "_usernames = [\n", + " c[\"id\"].removesuffix(\"-sentinel-2-c1-l2a\")\n", + " for c in _collections\n", + " if c[\"id\"].endswith(\"-sentinel-2-c1-l2a\")\n", + "]\n", + "\n", + "username_input = widgets.Text(\n", + " value=_usernames[-1] if _usernames else None,\n", + " placeholder=\"Enter your username\",\n", + " description=\"username:\",\n", + " disabled=False,\n", + ")\n", + "\n", + "display(username_input)" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "8d9d740d-1e2d-45c4-8fcc-1c7cb31a19cc", + "metadata": {}, + "outputs": [ { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now limit the search to items where `eo:cloud_cover` is less than 10" + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "
\n", + "
\n", + " <Collection id=dark-dream-2670-sentinel-2-c1-l2a>\n", + "
\n", + "\n", + "
" ], - "id": "87cf6d06-94f7-4925-8652-79008ad0e0d3" - }, + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# using pystac-client\n", + "my_collection_search = client.collection_search(\n", + " filter=f\"id LIKE '%{username_input.value}%'\"\n", + ")\n", + "\n", + "results = my_collection_search.collection_list()\n", + "\n", + "if results:\n", + " my_collection = results[0]\n", + " display(my_collection)\n", + "else:\n", + " all_ids = [c.id for c in client.get_all_collections()]\n", + " print(\n", + " f\"No collections matched {username_input.value!r}. \"\n", + " f\"Collection ids in the catalog: {all_ids}\"\n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "1d7fe9b0-f05c-4e0b-a24a-e8040b9d09c4", + "metadata": {}, + "source": [ + "
\n", + "Note: If your collection did not appear, try adjusting your collection search terms!\n", + "
" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "5f75438e-cca6-4447-b9cb-f3349ae04c76", + "metadata": { + "scrolled": true + }, + "outputs": [ { - "cell_type": "code", - "metadata": {}, - "source": [ - "search = client.search(\n", - " collections=[my_collection.id],\n", - " filter={\n", - " \"op\": \"lt\",\n", - " \"args\": [\n", - " {\"property\": \"eo:cloud_cover\"},\n", - " 10,\n", - " ],\n", - " },\n", - " max_items=10,\n", - ")\n", - "\n", - "items = search.item_collection()\n", - "\n", - "print(f\"found {len(items)} items\")\n", - "items[-1]" - ], - "execution_count": null, - "outputs": [], - "id": "e117cea2-8bf2-49cf-9d36-aba3e709d819" - }, + "name": "stdout", + "output_type": "stream", + "text": [ + "{\n", + " \"links\": [\n", + " {\n", + " \"rel\": \"root\",\n", + " \"type\": \"application/json\",\n", + " \"href\": \"http://stac-auth-proxy:8000/\"\n", + " },\n", + " {\n", + " \"rel\": \"self\",\n", + " \"type\": \"application/json\",\n", + " \"href\": \"http://stac-auth-proxy:8000/collections?filter=id+LIKE+%27%25super-mouse-3144%25%27\"\n", + " }\n", + " ],\n", + " \"collections\": [\n", + " {\n", + " \"id\": \"super-mouse-3144-sentinel-2-c1-l2a\",\n", + " \"type\": \"Collection\",\n", + " \"links\": [\n", + " {\n", + " \"rel\": \"items\",\n", + " \"type\": \"application/geo+json\",\n", + " \"href\": \"http://stac-auth-proxy:8000/collections/super-mouse-3144-sentinel-2-c1-l2a/items\"\n", + " },\n", + " {\n", + " \"rel\": \"parent\",\n", + " \"type\": \"application/json\",\n", + " \"href\": \"http://stac-auth-proxy:8000/\"\n", + " },\n", + " {\n", + " \"rel\": \"root\",\n", + " \"type\": \"application/json\",\n", + " \"href\": \"http://stac-auth-proxy:8000/\"\n", + " },\n", + " {\n", + " \"rel\": \"self\",\n", + " \"type\": \"application/json\",\n", + " \"href\": \"http://stac-auth-proxy:8000/collections/super-mouse-3144-sentinel-2-c1-l2a\"\n", + " },\n", + " {\n", + " \"rel\": \"http://www.opengis.net/def/rel/ogc/1.0/queryables\",\n", + " \"type\": \"application/schema+json\",\n", + " \"title\": \"Queryables\",\n", + " \"href\": \"http://stac-auth-proxy:8000/collections/super-mouse-3144-sentinel-2-c1-l2a/queryables\"\n", + " }\n", + " ],\n", + " \"extent\": {\n", + " \"spatial\": {\n", + " \"bbox\": [\n", + " [\n", + " 71.01,\n", + " -86.82,\n", + " 75.01,\n", + " -82.82\n", + " ]\n", + " ]\n", + " },\n", + " \"temporal\": {\n", + " \"interval\": [\n", + " [\n", + " \"2025-01-01T00:00:00Z\",\n", + " \"2025-04-18T00:00:00Z\"\n", + " ]\n", + " ]\n", + " }\n", + " },\n", + " \"license\": \"other\",\n", + " \"description\": \"super-mouse-3144's personal Sentinel-2 L2A collection\",\n", + " \"stac_version\": \"1.1.0\"\n", + " }\n", + " ],\n", + " \"numberMatched\": 1,\n", + " \"numberReturned\": 1,\n", + " \"stac_extensions\": [\n", + " \"https://stac-extensions.github.io/authentication/v1.1.0/schema.json\"\n", + " ],\n", + " \"auth:schemes\": {\n", + " \"oidc\": {\n", + " \"type\": \"openIdConnect\",\n", + " \"openIdConnectUrl\": \"http://localhost:8085/.well-known/openid-configuration\"\n", + " }\n", + " }\n", + "}\n" + ] + } + ], + "source": [ + "# using http client\n", + "print(\n", + " json.dumps(\n", + " httpx.get(\n", + " f\"{stac_api_endpoint}/collections\",\n", + " params={\"filter\": f\"id LIKE '%{username_input.value}%'\"},\n", + " ).json(),\n", + " indent=2,\n", + " )\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "7eae366c-977d-45af-bf19-275e67ee8058", + "metadata": {}, + "source": [ + "Now that you found your collection, you have what you need to do an effective item search within your collection! " + ] + }, + { + "cell_type": "markdown", + "id": "56657240-5905-4ddf-a76a-afd2a28959b0", + "metadata": {}, + "source": [ + "## 3.3 Items\n", + "\n", + "Once you have the collection ID there are several ways to perform an effective item search:\n", + "- GET request to `/collections/{collection_id}/items`\n", + "- GET or POST request to `/search`\n", + "\n", + "There are not any particular advantages to either approach unless you want to search for items using an intersection with a geometry in which case you should use a POST request to `/search` with the `intersects` parameter in the request body (instead of url-encoding a geojson!).\n", + "\n", + "Item search request responses will be returned in pages with `{limit}` results. If your search returns more than a single page of results, the next page will be retrievable via the `next` link in the list of `links`." + ] + }, + { + "cell_type": "markdown", + "id": "74d1fb0b-e830-4fb0-8619-f9cfd3019111", + "metadata": {}, + "source": [ + "### 3.3.1 Item Search\n", + "\n", + "Use the `/search` endpoint to find all items in your collection with a timestamp after April 4, 2025" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "e73a9ef7-d8f8-407d-965e-c549f112899c", + "metadata": {}, + "outputs": [ { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 3.3.2 All Items\n", - "\n", - "The API `/collections/{collection_id}/items` endpoint will get you all items in a collection.\n", - "\n", - "You can also run the same search but instead of passing `collections` as a query parameter you can include `collection_id` as a path parameter in the request URL itself. All of the other query parameters for the `/search` GET request will be available." - ], - "id": "61d01f82-94f8-4071-b398-2508bc84c56a" + "name": "stdout", + "output_type": "stream", + "text": [ + "found 10 items\n" + ] }, { - "cell_type": "code", - "metadata": {}, - "source": [ - "datetime_string = datetime(2025, 1, 4, tzinfo=UTC).isoformat()\n", - "\n", - "item_search_request = httpx.get(\n", - " f\"{stac_api_endpoint}/collections/{my_collection.id}/items\",\n", - " params={\n", - " \"datetime\": f\"{datetime_string}/..\", # open interval from 2025-04-04 forward\n", - " \"limit\": 1000,\n", - " \"filter\": \"eo:cloud_cover < 10\", # less than 10% cloud cover\n", - " },\n", - ")\n", - "response = item_search_request.json()\n", - "print(f\"found {len(response['features'])} items\")" + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "
\n", + "
\n", + " <Item id=S2B_T43CEJ_20250129T232126_L2A>\n", + "
\n", + "\n", + "
" ], - "execution_count": null, - "outputs": [], - "id": "2bfec9f1-5bd6-4ea9-90bf-66a2ddf90f36" - }, + "text/plain": [ + "" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from datetime import datetime, UTC\n", + "\n", + "search = client.search(\n", + " collections=[my_collection.id],\n", + " datetime=[datetime(2025, 1, 4), None],\n", + ")\n", + "\n", + "items = search.item_collection()\n", + "\n", + "print(f\"found {len(items)} items\")\n", + "items[0]" + ] + }, + { + "cell_type": "markdown", + "id": "3e504a62-0c1e-4f00-aa54-e286252c493b", + "metadata": {}, + "source": [ + "The same query can be made with an HTTP client:" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "57405533-6bbf-402c-9b26-99b97ab234ec", + "metadata": { + "scrolled": true + }, + "outputs": [ { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 3.3.3 Single Item by ID\n", - "\n", - "To retrieve a specific item from the catalog, you can use the `/collections/{collection_id}/items/{item_id}` endpoint." - ], - "id": "a1de0ea6-51b5-4b12-88e0-67c2d49012bb" - }, + "name": "stdout", + "output_type": "stream", + "text": [ + "{\n", + " \"type\": \"FeatureCollection\",\n", + " \"links\": [\n", + " {\n", + " \"rel\": \"root\",\n", + " \"type\": \"application/json\",\n", + " \"href\": \"http://stac-auth-proxy:8000/\"\n", + " },\n", + " {\n", + " \"rel\": \"self\",\n", + " \"type\": \"application/geo+json\",\n", + " \"href\": \"http://stac-auth-proxy:8000/search\"\n", + " },\n", + " {\n", + " \"rel\": \"next\",\n", + " \"type\": \"application/geo+json\",\n", + " \"method\": \"GET\",\n", + " \"href\": \"http://stac-auth-proxy:8000/search?collections=super-mouse-3144-sentinel-2-c1-l2a&datetime=2025-01-04T00%3A00%3A00%2B00%3A00%2F..&limit=1&token=next%3Asuper-mouse-3144-sentinel-2-c1-l2a%3AS2B_T43CEJ_20250129T232126_L2A\"\n", + " }\n", + " ],\n", + " \"features\": [\n", + " {\n", + " \"id\": \"S2B_T43CEJ_20250129T232126_L2A\",\n", + " \"bbox\": [\n", + " 74.998637,\n", + " -82.829004,\n", + " 76.476391,\n", + " -81.953754\n", + " ],\n", + " \"type\": \"Feature\",\n", + " \"links\": [\n", + " {\n", + " \"rel\": \"collection\",\n", + " \"type\": \"application/json\",\n", + " \"href\": \"http://stac-auth-proxy:8000/collections/super-mouse-3144-sentinel-2-c1-l2a\"\n", + " },\n", + " {\n", + " \"rel\": \"parent\",\n", + " \"type\": \"application/json\",\n", + " \"href\": \"http://stac-auth-proxy:8000/collections/super-mouse-3144-sentinel-2-c1-l2a\"\n", + " },\n", + " {\n", + " \"rel\": \"root\",\n", + " \"type\": \"application/json\",\n", + " \"href\": \"http://stac-auth-proxy:8000/\"\n", + " },\n", + " {\n", + " \"rel\": \"self\",\n", + " \"type\": \"application/geo+json\",\n", + " \"href\": \"http://stac-auth-proxy:8000/collections/super-mouse-3144-sentinel-2-c1-l2a/items/S2B_T43CEJ_20250129T232126_L2A\"\n", + " },\n", + " {\n", + " \"rel\": \"canonical\",\n", + " \"href\": \"s3://e84-earth-search-sentinel-data/sentinel-2-c1-l2a/43/C/EJ/2025/1/S2B_T43CEJ_20250129T232126_L2A/S2B_T43CEJ_20250129T232126_L2A.json\",\n", + " \"type\": \"application/json\"\n", + " },\n", + " {\n", + " \"rel\": \"via\",\n", + " \"href\": \"s3://sentinel-s2-l2a/tiles/43/C/EJ/2025/1/29/1/metadata.xml\",\n", + " \"type\": \"application/xml\",\n", + " \"title\": \"Granule Metadata in Sinergize RODA Archive\"\n", + " },\n", + " {\n", + " \"rel\": \"thumbnail\",\n", + " \"href\": \"https://earth-search.aws.element84.com/v1/collections/sentinel-2-c1-l2a/items/S2B_T43CEJ_20250129T232126_L2A/thumbnail\"\n", + " }\n", + " ],\n", + " \"assets\": {\n", + " \"aot\": {\n", + " \"gsd\": 20,\n", + " \"href\": \"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/43/C/EJ/2025/1/S2B_T43CEJ_20250129T232126_L2A/AOT.tif\",\n", + " \"type\": \"image/tiff; application=geotiff; profile=cloud-optimized\",\n", + " \"roles\": [\n", + " \"data\"\n", + " ],\n", + " \"title\": \"Aerosol optical thickness (AOT)\",\n", + " \"file:size\": 144362,\n", + " \"proj:shape\": [\n", + " 5490,\n", + " 5490\n", + " ],\n", + " \"raster:bands\": [\n", + " {\n", + " \"scale\": 0.001,\n", + " \"nodata\": 0,\n", + " \"offset\": 0,\n", + " \"data_type\": \"uint16\",\n", + " \"spatial_resolution\": 20\n", + " }\n", + " ],\n", + " \"file:checksum\": \"12207e6fe3a9869218db7dbac14cbcfbc7b7b3278f508589e8f2800cd6399eddb4ff\",\n", + " \"proj:transform\": [\n", + " 20,\n", + " 0,\n", + " 499980,\n", + " 0,\n", + " -20,\n", + " 900040\n", + " ]\n", + " },\n", + " \"nir\": {\n", + " \"gsd\": 10,\n", + " \"href\": \"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/43/C/EJ/2025/1/S2B_T43CEJ_20250129T232126_L2A/B08.tif\",\n", + " \"type\": \"image/tiff; application=geotiff; profile=cloud-optimized\",\n", + " \"roles\": [\n", + " \"data\",\n", + " \"reflectance\"\n", + " ],\n", + " \"title\": \"NIR 1 - 10m\",\n", + " \"eo:bands\": [\n", + " {\n", + " \"name\": \"B08\",\n", + " \"common_name\": \"nir\",\n", + " \"center_wavelength\": 0.842,\n", + " \"full_width_half_max\": 0.145\n", + " }\n", + " ],\n", + " \"file:size\": 27234331,\n", + " \"proj:shape\": [\n", + " 10980,\n", + " 10980\n", + " ],\n", + " \"raster:bands\": [\n", + " {\n", + " \"scale\": 0.0001,\n", + " \"nodata\": 0,\n", + " \"offset\": -0.1,\n", + " \"data_type\": \"uint16\",\n", + " \"spatial_resolution\": 10\n", + " }\n", + " ],\n", + " \"file:checksum\": \"1220dc7fd081c4f715e34d263a1ba65fe5364dc33ad5e4ddf0b8ca3040afc3e28b33\",\n", + " \"proj:transform\": [\n", + " 10,\n", + " 0,\n", + " 499980,\n", + " 0,\n", + " -10,\n", + " 900040\n", + " ]\n", + " },\n", + " \"red\": {\n", + " \"gsd\": 10,\n", + " \"href\": \"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/43/C/EJ/2025/1/S2B_T43CEJ_20250129T232126_L2A/B04.tif\",\n", + " \"type\": \"image/tiff; application=geotiff; profile=cloud-optimized\",\n", + " \"roles\": [\n", + " \"data\",\n", + " \"reflectance\"\n", + " ],\n", + " \"title\": \"Red - 10m\",\n", + " \"eo:bands\": [\n", + " {\n", + " \"name\": \"B04\",\n", + " \"common_name\": \"red\",\n", + " \"center_wavelength\": 0.665,\n", + " \"full_width_half_max\": 0.038\n", + " }\n", + " ],\n", + " \"file:size\": 26188526,\n", + " \"proj:shape\": [\n", + " 10980,\n", + " 10980\n", + " ],\n", + " \"raster:bands\": [\n", + " {\n", + " \"scale\": 0.0001,\n", + " \"nodata\": 0,\n", + " \"offset\": -0.1,\n", + " \"data_type\": \"uint16\",\n", + " \"spatial_resolution\": 10\n", + " }\n", + " ],\n", + " \"file:checksum\": \"122013838a1c4909487be8c047f95162861620f252878eb7120c37c66f9917cf1171\",\n", + " \"proj:transform\": [\n", + " 10,\n", + " 0,\n", + " 499980,\n", + " 0,\n", + " -10,\n", + " 900040\n", + " ]\n", + " },\n", + " \"scl\": {\n", + " \"gsd\": 20,\n", + " \"href\": \"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/43/C/EJ/2025/1/S2B_T43CEJ_20250129T232126_L2A/SCL.tif\",\n", + " \"type\": \"image/tiff; application=geotiff; profile=cloud-optimized\",\n", + " \"roles\": [\n", + " \"data\"\n", + " ],\n", + " \"title\": \"Scene classification map (SCL)\",\n", + " \"file:size\": 204389,\n", + " \"proj:shape\": [\n", + " 5490,\n", + " 5490\n", + " ],\n", + " \"raster:bands\": [\n", + " {\n", + " \"nodata\": 0,\n", + " \"data_type\": \"uint8\",\n", + " \"spatial_resolution\": 20\n", + " }\n", + " ],\n", + " \"file:checksum\": \"122068060b58a8450ef80f55b6866bc20481512d83c1091b9e2f6c3042ac9bf34749\",\n", + " \"proj:transform\": [\n", + " 20,\n", + " 0,\n", + " 499980,\n", + " 0,\n", + " -20,\n", + " 900040\n", + " ]\n", + " },\n", + " \"wvp\": {\n", + " \"gsd\": 20,\n", + " \"href\": \"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/43/C/EJ/2025/1/S2B_T43CEJ_20250129T232126_L2A/WVP.tif\",\n", + " \"type\": \"image/tiff; application=geotiff; profile=cloud-optimized\",\n", + " \"roles\": [\n", + " \"data\"\n", + " ],\n", + " \"title\": \"Water Vapour (WVP)\",\n", + " \"file:size\": 2402873,\n", + " \"proj:shape\": [\n", + " 5490,\n", + " 5490\n", + " ],\n", + " \"raster:bands\": [\n", + " {\n", + " \"unit\": \"cm\",\n", + " \"scale\": 0.001,\n", + " \"nodata\": 0,\n", + " \"offset\": 0,\n", + " \"data_type\": \"uint16\",\n", + " \"spatial_resolution\": 20\n", + " }\n", + " ],\n", + " \"file:checksum\": \"122092f3ad25ed396943c4c12a60d346bff4e8b81afb6c79dfa5a93d6d9b4960630f\",\n", + " \"proj:transform\": [\n", + " 20,\n", + " 0,\n", + " 499980,\n", + " 0,\n", + " -20,\n", + " 900040\n", + " ]\n", + " },\n", + " \"blue\": {\n", + " \"gsd\": 10,\n", + " \"href\": \"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/43/C/EJ/2025/1/S2B_T43CEJ_20250129T232126_L2A/B02.tif\",\n", + " \"type\": \"image/tiff; application=geotiff; profile=cloud-optimized\",\n", + " \"roles\": [\n", + " \"data\",\n", + " \"reflectance\"\n", + " ],\n", + " \"title\": \"Blue - 10m\",\n", + " \"eo:bands\": [\n", + " {\n", + " \"name\": \"B02\",\n", + " \"common_name\": \"blue\",\n", + " \"center_wavelength\": 0.49,\n", + " \"full_width_half_max\": 0.098\n", + " }\n", + " ],\n", + " \"file:size\": 25522677,\n", + " \"proj:shape\": [\n", + " 10980,\n", + " 10980\n", + " ],\n", + " \"raster:bands\": [\n", + " {\n", + " \"scale\": 0.0001,\n", + " \"nodata\": 0,\n", + " \"offset\": -0.1,\n", + " \"data_type\": \"uint16\",\n", + " \"spatial_resolution\": 10\n", + " }\n", + " ],\n", + " \"file:checksum\": \"1220569c69f6ccc05d007a28c0aada66ce751b614e46f5218d5cf7e0dd79e216f796\",\n", + " \"proj:transform\": [\n", + " 10,\n", + " 0,\n", + " 499980,\n", + " 0,\n", + " -10,\n", + " 900040\n", + " ]\n", + " },\n", + " \"snow\": {\n", + " \"href\": \"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/43/C/EJ/2025/1/S2B_T43CEJ_20250129T232126_L2A/SNW_20m.tif\",\n", + " \"type\": \"image/tiff; application=geotiff; profile=cloud-optimized\",\n", + " \"roles\": [\n", + " \"data\",\n", + " \"snow-ice\"\n", + " ],\n", + " \"title\": \"Snow Probabilities\",\n", + " \"file:size\": 195641,\n", + " \"proj:shape\": [\n", + " 5490,\n", + " 5490\n", + " ],\n", + " \"raster:bands\": [\n", + " {\n", + " \"nodata\": 0,\n", + " \"data_type\": \"uint8\",\n", + " \"spatial_resolution\": 20\n", + " }\n", + " ],\n", + " \"file:checksum\": \"12201f7ee663d311aa0bf2c63ff5a2071988188f918f1b2b5d859df8381f7df0c257\",\n", + " \"proj:transform\": [\n", + " 20,\n", + " 0,\n", + " 499980,\n", + " 0,\n", + " -20,\n", + " 900040\n", + " ]\n", + " },\n", + " \"cloud\": {\n", + " \"gsd\": 20,\n", + " \"href\": \"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/43/C/EJ/2025/1/S2B_T43CEJ_20250129T232126_L2A/CLD_20m.tif\",\n", + " \"type\": \"image/tiff; application=geotiff; profile=cloud-optimized\",\n", + " \"roles\": [\n", + " \"data\",\n", + " \"cloud\"\n", + " ],\n", + " \"title\": \"Cloud Probabilities\",\n", + " \"file:size\": 187672,\n", + " \"proj:shape\": [\n", + " 5490,\n", + " 5490\n", + " ],\n", + " \"raster:bands\": [\n", + " {\n", + " \"nodata\": 0,\n", + " \"data_type\": \"uint8\",\n", + " \"spatial_resolution\": 20\n", + " }\n", + " ],\n", + " \"file:checksum\": \"1220ade4f35896e65b165267fcfd8082a5c320471ff319eaa26006555920c3ab437b\",\n", + " \"proj:transform\": [\n", + " 20,\n", + " 0,\n", + " 499980,\n", + " 0,\n", + " -20,\n", + " 900040\n", + " ]\n", + " },\n", + " \"green\": {\n", + " \"gsd\": 10,\n", + " \"href\": \"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/43/C/EJ/2025/1/S2B_T43CEJ_20250129T232126_L2A/B03.tif\",\n", + " \"type\": \"image/tiff; application=geotiff; profile=cloud-optimized\",\n", + " \"roles\": [\n", + " \"data\",\n", + " \"reflectance\"\n", + " ],\n", + " \"title\": \"Green - 10m\",\n", + " \"eo:bands\": [\n", + " {\n", + " \"name\": \"B03\",\n", + " \"common_name\": \"green\",\n", + " \"center_wavelength\": 0.56,\n", + " \"full_width_half_max\": 0.045\n", + " }\n", + " ],\n", + " \"file:size\": 25854114,\n", + " \"proj:shape\": [\n", + " 10980,\n", + " 10980\n", + " ],\n", + " \"raster:bands\": [\n", + " {\n", + " \"scale\": 0.0001,\n", + " \"nodata\": 0,\n", + " \"offset\": -0.1,\n", + " \"data_type\": \"uint16\",\n", + " \"spatial_resolution\": 10\n", + " }\n", + " ],\n", + " \"file:checksum\": \"12203c82c4a91c05a8514d11dad105280eaef375096cea35ea11b0390fe14eb23378\",\n", + " \"proj:transform\": [\n", + " 10,\n", + " 0,\n", + " 499980,\n", + " 0,\n", + " -10,\n", + " 900040\n", + " ]\n", + " },\n", + " \"nir08\": {\n", + " \"gsd\": 20,\n", + " \"href\": \"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/43/C/EJ/2025/1/S2B_T43CEJ_20250129T232126_L2A/B8A.tif\",\n", + " \"type\": \"image/tiff; application=geotiff; profile=cloud-optimized\",\n", + " \"roles\": [\n", + " \"data\",\n", + " \"reflectance\"\n", + " ],\n", + " \"title\": \"NIR 2 - 20m\",\n", + " \"eo:bands\": [\n", + " {\n", + " \"name\": \"B8A\",\n", + " \"common_name\": \"nir08\",\n", + " \"center_wavelength\": 0.865,\n", + " \"full_width_half_max\": 0.033\n", + " }\n", + " ],\n", + " \"file:size\": 8974769,\n", + " \"proj:shape\": [\n", + " 5490,\n", + " 5490\n", + " ],\n", + " \"raster:bands\": [\n", + " {\n", + " \"scale\": 0.0001,\n", + " \"nodata\": 0,\n", + " \"offset\": -0.1,\n", + " \"data_type\": \"uint16\",\n", + " \"spatial_resolution\": 20\n", + " }\n", + " ],\n", + " \"file:checksum\": \"122064cf5894c15636b1337eeaa0732cfec31f89e730f72583c726716f8e7a9342ec\",\n", + " \"proj:transform\": [\n", + " 20,\n", + " 0,\n", + " 499980,\n", + " 0,\n", + " -20,\n", + " 900040\n", + " ]\n", + " },\n", + " \"nir09\": {\n", + " \"gsd\": 60,\n", + " \"href\": \"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/43/C/EJ/2025/1/S2B_T43CEJ_20250129T232126_L2A/B09.tif\",\n", + " \"type\": \"image/tiff; application=geotiff; profile=cloud-optimized\",\n", + " \"roles\": [\n", + " \"data\",\n", + " \"reflectance\"\n", + " ],\n", + " \"title\": \"NIR 3 - 60m\",\n", + " \"eo:bands\": [\n", + " {\n", + " \"name\": \"B09\",\n", + " \"common_name\": \"nir09\",\n", + " \"center_wavelength\": 0.945,\n", + " \"full_width_half_max\": 0.026\n", + " }\n", + " ],\n", + " \"file:size\": 1034798,\n", + " \"proj:shape\": [\n", + " 1830,\n", + " 1830\n", + " ],\n", + " \"raster:bands\": [\n", + " {\n", + " \"scale\": 0.0001,\n", + " \"nodata\": 0,\n", + " \"offset\": -0.1,\n", + " \"data_type\": \"uint16\",\n", + " \"spatial_resolution\": 60\n", + " }\n", + " ],\n", + " \"file:checksum\": \"12206c9f352ad396fa0e5aef97d89c643fc32dbe2505e69abdcff49e9329878be8dc\",\n", + " \"proj:transform\": [\n", + " 60,\n", + " 0,\n", + " 499980,\n", + " 0,\n", + " -60,\n", + " 900040\n", + " ]\n", + " },\n", + " \"swir16\": {\n", + " \"gsd\": 20,\n", + " \"href\": \"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/43/C/EJ/2025/1/S2B_T43CEJ_20250129T232126_L2A/B11.tif\",\n", + " \"type\": \"image/tiff; application=geotiff; profile=cloud-optimized\",\n", + " \"roles\": [\n", + " \"data\",\n", + " \"reflectance\"\n", + " ],\n", + " \"title\": \"SWIR 1.6\\u03bcm - 20m\",\n", + " \"eo:bands\": [\n", + " {\n", + " \"name\": \"B11\",\n", + " \"common_name\": \"swir16\",\n", + " \"center_wavelength\": 1.61,\n", + " \"full_width_half_max\": 0.143\n", + " }\n", + " ],\n", + " \"file:size\": 7220773,\n", + " \"proj:shape\": [\n", + " 5490,\n", + " 5490\n", + " ],\n", + " \"raster:bands\": [\n", + " {\n", + " \"scale\": 0.0001,\n", + " \"nodata\": 0,\n", + " \"offset\": -0.1,\n", + " \"data_type\": \"uint16\",\n", + " \"spatial_resolution\": 20\n", + " }\n", + " ],\n", + " \"file:checksum\": \"12203a21f6087fa74dc41617215fae038da3d2c89723ab9ed8693bbeeadcb68da54f\",\n", + " \"proj:transform\": [\n", + " 20,\n", + " 0,\n", + " 499980,\n", + " 0,\n", + " -20,\n", + " 900040\n", + " ]\n", + " },\n", + " \"swir22\": {\n", + " \"gsd\": 20,\n", + " \"href\": \"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/43/C/EJ/2025/1/S2B_T43CEJ_20250129T232126_L2A/B12.tif\",\n", + " \"type\": \"image/tiff; application=geotiff; profile=cloud-optimized\",\n", + " \"roles\": [\n", + " \"data\",\n", + " \"reflectance\"\n", + " ],\n", + " \"title\": \"SWIR 2.2\\u03bcm - 20m\",\n", + " \"eo:bands\": [\n", + " {\n", + " \"name\": \"B12\",\n", + " \"common_name\": \"swir22\",\n", + " \"center_wavelength\": 2.19,\n", + " \"full_width_half_max\": 0.242\n", + " }\n", + " ],\n", + " \"file:size\": 7342247,\n", + " \"proj:shape\": [\n", + " 5490,\n", + " 5490\n", + " ],\n", + " \"raster:bands\": [\n", + " {\n", + " \"scale\": 0.0001,\n", + " \"nodata\": 0,\n", + " \"offset\": -0.1,\n", + " \"data_type\": \"uint16\",\n", + " \"spatial_resolution\": 20\n", + " }\n", + " ],\n", + " \"file:checksum\": \"122077b4b06c2edf7892340f4eb95afe2e5f7e97bd555d89bf22a84ee1ed7f20e999\",\n", + " \"proj:transform\": [\n", + " 20,\n", + " 0,\n", + " 499980,\n", + " 0,\n", + " -20,\n", + " 900040\n", + " ]\n", + " },\n", + " \"visual\": {\n", + " \"gsd\": 10,\n", + " \"href\": \"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/43/C/EJ/2025/1/S2B_T43CEJ_20250129T232126_L2A/TCI.tif\",\n", + " \"type\": \"image/tiff; application=geotiff; profile=cloud-optimized\",\n", + " \"roles\": [\n", + " \"visual\"\n", + " ],\n", + " \"title\": \"True color image\",\n", + " \"eo:bands\": [\n", + " {\n", + " \"name\": \"B04\",\n", + " \"common_name\": \"red\",\n", + " \"center_wavelength\": 0.665,\n", + " \"full_width_half_max\": 0.038\n", + " },\n", + " {\n", + " \"name\": \"B03\",\n", + " \"common_name\": \"green\",\n", + " \"center_wavelength\": 0.56,\n", + " \"full_width_half_max\": 0.045\n", + " },\n", + " {\n", + " \"name\": \"B02\",\n", + " \"common_name\": \"blue\",\n", + " \"center_wavelength\": 0.49,\n", + " \"full_width_half_max\": 0.098\n", + " }\n", + " ],\n", + " \"file:size\": 586020,\n", + " \"proj:shape\": [\n", + " 10980,\n", + " 10980\n", + " ],\n", + " \"raster:bands\": [\n", + " {\n", + " \"nodata\": 0,\n", + " \"data_type\": \"uint8\",\n", + " \"spatial_resolution\": 10\n", + " },\n", + " {\n", + " \"nodata\": 0,\n", + " \"data_type\": \"uint8\",\n", + " \"spatial_resolution\": 10\n", + " },\n", + " {\n", + " \"nodata\": 0,\n", + " \"data_type\": \"uint8\",\n", + " \"spatial_resolution\": 10\n", + " }\n", + " ],\n", + " \"file:checksum\": \"1220d55f6c195f7b5d6fae0b1794d03c0fbda5d0cd8bf1dd4e27c8d456bf3d9a505a\",\n", + " \"proj:transform\": [\n", + " 10,\n", + " 0,\n", + " 499980,\n", + " 0,\n", + " -10,\n", + " 900040\n", + " ]\n", + " },\n", + " \"coastal\": {\n", + " \"gsd\": 60,\n", + " \"href\": \"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/43/C/EJ/2025/1/S2B_T43CEJ_20250129T232126_L2A/B01.tif\",\n", + " \"type\": \"image/tiff; application=geotiff; profile=cloud-optimized\",\n", + " \"roles\": [\n", + " \"data\",\n", + " \"reflectance\"\n", + " ],\n", + " \"title\": \"Coastal - 60m\",\n", + " \"eo:bands\": [\n", + " {\n", + " \"name\": \"B01\",\n", + " \"common_name\": \"coastal\",\n", + " \"center_wavelength\": 0.443,\n", + " \"full_width_half_max\": 0.027\n", + " }\n", + " ],\n", + " \"file:size\": 1022784,\n", + " \"proj:shape\": [\n", + " 1830,\n", + " 1830\n", + " ],\n", + " \"raster:bands\": [\n", + " {\n", + " \"scale\": 0.0001,\n", + " \"nodata\": 0,\n", + " \"offset\": -0.1,\n", + " \"data_type\": \"uint16\",\n", + " \"spatial_resolution\": 60\n", + " }\n", + " ],\n", + " \"file:checksum\": \"1220c2d452409df7803b66cf99e8fbd9acd64a1ef0a465208740a4d0dddcdb5589c0\",\n", + " \"proj:transform\": [\n", + " 60,\n", + " 0,\n", + " 499980,\n", + " 0,\n", + " -60,\n", + " 900040\n", + " ]\n", + " },\n", + " \"preview\": {\n", + " \"href\": \"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/43/C/EJ/2025/1/S2B_T43CEJ_20250129T232126_L2A/L2A_PVI.tif\",\n", + " \"type\": \"image/tiff; application=geotiff; profile=cloud-optimized\",\n", + " \"roles\": [\n", + " \"overview\"\n", + " ],\n", + " \"title\": \"True color preview\",\n", + " \"eo:bands\": [\n", + " {\n", + " \"name\": \"B04\",\n", + " \"common_name\": \"red\",\n", + " \"center_wavelength\": 0.665,\n", + " \"full_width_half_max\": 0.038\n", + " },\n", + " {\n", + " \"name\": \"B03\",\n", + " \"common_name\": \"green\",\n", + " \"center_wavelength\": 0.56,\n", + " \"full_width_half_max\": 0.045\n", + " },\n", + " {\n", + " \"name\": \"B02\",\n", + " \"common_name\": \"blue\",\n", + " \"center_wavelength\": 0.49,\n", + " \"full_width_half_max\": 0.098\n", + " }\n", + " ],\n", + " \"file:size\": 5582,\n", + " \"file:checksum\": \"1220d384769e22f70ae6b676c30396bf38056fbc13fb83b5e186f0914105c0d9b17e\"\n", + " },\n", + " \"rededge1\": {\n", + " \"gsd\": 20,\n", + " \"href\": \"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/43/C/EJ/2025/1/S2B_T43CEJ_20250129T232126_L2A/B05.tif\",\n", + " \"type\": \"image/tiff; application=geotiff; profile=cloud-optimized\",\n", + " \"roles\": [\n", + " \"data\",\n", + " \"reflectance\"\n", + " ],\n", + " \"title\": \"Red Edge 1 - 20m\",\n", + " \"eo:bands\": [\n", + " {\n", + " \"name\": \"B05\",\n", + " \"common_name\": \"rededge\",\n", + " \"center_wavelength\": 0.704,\n", + " \"full_width_half_max\": 0.019\n", + " }\n", + " ],\n", + " \"file:size\": 8927582,\n", + " \"proj:shape\": [\n", + " 5490,\n", + " 5490\n", + " ],\n", + " \"raster:bands\": [\n", + " {\n", + " \"scale\": 0.0001,\n", + " \"nodata\": 0,\n", + " \"offset\": -0.1,\n", + " \"data_type\": \"uint16\",\n", + " \"spatial_resolution\": 20\n", + " }\n", + " ],\n", + " \"file:checksum\": \"1220b7081230c8a4811d340b2db6d031679f30b3b1e9fca297e508b9fa93ca6f89c7\",\n", + " \"proj:transform\": [\n", + " 20,\n", + " 0,\n", + " 499980,\n", + " 0,\n", + " -20,\n", + " 900040\n", + " ]\n", + " },\n", + " \"rededge2\": {\n", + " \"gsd\": 20,\n", + " \"href\": \"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/43/C/EJ/2025/1/S2B_T43CEJ_20250129T232126_L2A/B06.tif\",\n", + " \"type\": \"image/tiff; application=geotiff; profile=cloud-optimized\",\n", + " \"roles\": [\n", + " \"data\",\n", + " \"reflectance\"\n", + " ],\n", + " \"title\": \"Red Edge 2 - 20m\",\n", + " \"eo:bands\": [\n", + " {\n", + " \"name\": \"B06\",\n", + " \"common_name\": \"rededge\",\n", + " \"center_wavelength\": 0.74,\n", + " \"full_width_half_max\": 0.018\n", + " }\n", + " ],\n", + " \"file:size\": 8943424,\n", + " \"proj:shape\": [\n", + " 5490,\n", + " 5490\n", + " ],\n", + " \"raster:bands\": [\n", + " {\n", + " \"scale\": 0.0001,\n", + " \"nodata\": 0,\n", + " \"offset\": -0.1,\n", + " \"data_type\": \"uint16\",\n", + " \"spatial_resolution\": 20\n", + " }\n", + " ],\n", + " \"file:checksum\": \"1220324f5c66fecabe6161d09bde40a5aef4e1b8c26b0390f0789020f66d833be1d0\",\n", + " \"proj:transform\": [\n", + " 20,\n", + " 0,\n", + " 499980,\n", + " 0,\n", + " -20,\n", + " 900040\n", + " ]\n", + " },\n", + " \"rededge3\": {\n", + " \"gsd\": 20,\n", + " \"href\": \"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/43/C/EJ/2025/1/S2B_T43CEJ_20250129T232126_L2A/B07.tif\",\n", + " \"type\": \"image/tiff; application=geotiff; profile=cloud-optimized\",\n", + " \"roles\": [\n", + " \"data\",\n", + " \"reflectance\"\n", + " ],\n", + " \"title\": \"Red Edge 3 - 20m\",\n", + " \"eo:bands\": [\n", + " {\n", + " \"name\": \"B07\",\n", + " \"common_name\": \"rededge\",\n", + " \"center_wavelength\": 0.783,\n", + " \"full_width_half_max\": 0.028\n", + " }\n", + " ],\n", + " \"file:size\": 8923378,\n", + " \"proj:shape\": [\n", + " 5490,\n", + " 5490\n", + " ],\n", + " \"raster:bands\": [\n", + " {\n", + " \"scale\": 0.0001,\n", + " \"nodata\": 0,\n", + " \"offset\": -0.1,\n", + " \"data_type\": \"uint16\",\n", + " \"spatial_resolution\": 20\n", + " }\n", + " ],\n", + " \"file:checksum\": \"122047533eecbdb7ef5f2bd8144449e599499bf5e60c4c1c0708c06737179cdc0081\",\n", + " \"proj:transform\": [\n", + " 20,\n", + " 0,\n", + " 499980,\n", + " 0,\n", + " -20,\n", + " 900040\n", + " ]\n", + " },\n", + " \"thumbnail\": {\n", + " \"href\": \"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/43/C/EJ/2025/1/S2B_T43CEJ_20250129T232126_L2A/L2A_PVI.jpg\",\n", + " \"type\": \"image/jpeg\",\n", + " \"roles\": [\n", + " \"thumbnail\"\n", + " ],\n", + " \"title\": \"Thumbnail of preview image\",\n", + " \"file:size\": 3576,\n", + " \"file:checksum\": \"12201ab9d91d8e86bc48302fab736121f8a86588b5f7a6edbc67d550804665faa384\"\n", + " },\n", + " \"granule_metadata\": {\n", + " \"href\": \"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/43/C/EJ/2025/1/S2B_T43CEJ_20250129T232126_L2A/metadata.xml\",\n", + " \"type\": \"application/xml\",\n", + " \"roles\": [\n", + " \"metadata\"\n", + " ],\n", + " \"file:size\": 344855,\n", + " \"file:checksum\": \"1220edd5bd5fa6235f1a96682d5804ff162541d55f791141c3231871ded4cc41f1b2\"\n", + " },\n", + " \"product_metadata\": {\n", + " \"href\": \"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/43/C/EJ/2025/1/S2B_T43CEJ_20250129T232126_L2A/product_metadata.xml\",\n", + " \"type\": \"application/xml\",\n", + " \"roles\": [\n", + " \"metadata\"\n", + " ],\n", + " \"file:size\": 55392,\n", + " \"file:checksum\": \"1220b87a0742b4aca28a2be5081d68db0c3b8ef075488608d49b992e397bf137925d\"\n", + " },\n", + " \"tileinfo_metadata\": {\n", + " \"href\": \"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/43/C/EJ/2025/1/S2B_T43CEJ_20250129T232126_L2A/tileInfo.json\",\n", + " \"type\": \"application/json\",\n", + " \"roles\": [\n", + " \"metadata\"\n", + " ],\n", + " \"file:size\": 1771,\n", + " \"file:checksum\": \"1220174722f463c1aabcacfdb9888c7de0ed40c1f57610e8fab5770defa955fa7ef0\"\n", + " }\n", + " },\n", + " \"geometry\": {\n", + " \"type\": \"Polygon\",\n", + " \"coordinates\": [\n", + " [\n", + " [\n", + " 74.99878374283573,\n", + " -81.95639085890919\n", + " ],\n", + " [\n", + " 74.99863740990479,\n", + " -82.82513944666067\n", + " ],\n", + " [\n", + " 76.07398049288716,\n", + " -82.82900375273907\n", + " ],\n", + " [\n", + " 76.2054060931808,\n", + " -82.59361541363332\n", + " ],\n", + " [\n", + " 76.27657124570325,\n", + " -82.5917114984714\n", + " ],\n", + " [\n", + " 76.30097560235568,\n", + " -82.52864714405649\n", + " ],\n", + " [\n", + " 76.29319356693986,\n", + " -82.35754146790732\n", + " ],\n", + " [\n", + " 76.37236705276067,\n", + " -82.14201868935132\n", + " ],\n", + " [\n", + " 76.43421609158189,\n", + " -82.14068493795945\n", + " ],\n", + " [\n", + " 76.47639068984915,\n", + " -81.95375433709442\n", + " ],\n", + " [\n", + " 74.99878374283573,\n", + " -81.95639085890919\n", + " ]\n", + " ]\n", + " ]\n", + " },\n", + " \"collection\": \"super-mouse-3144-sentinel-2-c1-l2a\",\n", + " \"properties\": {\n", + " \"created\": \"2025-01-30T01:00:51.795Z\",\n", + " \"updated\": \"2025-01-30T01:00:51.795Z\",\n", + " \"datetime\": \"2025-01-29T23:21:31.918000Z\",\n", + " \"platform\": \"sentinel-2b\",\n", + " \"grid:code\": \"MGRS-43CEJ\",\n", + " \"proj:code\": \"EPSG:32743\",\n", + " \"s2:tile_id\": \"S2B_OPER_MSI_L2A_TL_2BPS_20250129T235902_A041268_T43CEJ_N05.11\",\n", + " \"instruments\": [\n", + " \"msi\"\n", + " ],\n", + " \"view:azimuth\": 184.13570996684024,\n", + " \"constellation\": \"sentinel-2\",\n", + " \"mgrs:utm_zone\": 43,\n", + " \"proj:centroid\": {\n", + " \"lat\": -82.36988,\n", + " \"lon\": 75.65354\n", + " },\n", + " \"eo:cloud_cover\": 28.13631,\n", + " \"s2:datatake_id\": \"GS2B_20250129T232129_041268_N05.11\",\n", + " \"s2:product_uri\": \"S2B_MSIL2A_20250129T232129_N0511_R058_T43CEJ_20250129T235902.SAFE\",\n", + " \"storage:region\": \"us-west-2\",\n", + " \"s2:datastrip_id\": \"S2B_OPER_MSI_L2A_DS_2BPS_20250129T235902_S20250129T232126_N05.11\",\n", + " \"s2:product_type\": \"S2MSI2A\",\n", + " \"mgrs:grid_square\": \"EJ\",\n", + " \"s2:datatake_type\": \"INS-NOBS\",\n", + " \"storage:platform\": \"AWS\",\n", + " \"view:sun_azimuth\": 116.171761867649,\n", + " \"mgrs:latitude_band\": \"C\",\n", + " \"s2:generation_time\": \"2025-01-29T23:59:02.000000Z\",\n", + " \"view:sun_elevation\": 14.427876039008694,\n", + " \"processing:software\": {\n", + " \"sentinel-2-c1-l2a-to-stac\": \"v2024.02.01\"\n", + " },\n", + " \"s2:water_percentage\": 0,\n", + " \"view:incidence_angle\": 7.893555169805119,\n", + " \"earthsearch:payload_id\": \"roda-sentinel-2-c1-l2a/workflow-sentinel-2-c1-l2a-to-stac/10bf4270276e9ff9f1e9066d5d480b8e\",\n", + " \"s2:processing_baseline\": \"05.11\",\n", + " \"s2:snow_ice_percentage\": 71.863687,\n", + " \"storage:requester_pays\": false,\n", + " \"s2:vegetation_percentage\": 0,\n", + " \"s2:thin_cirrus_percentage\": 0,\n", + " \"s2:cloud_shadow_percentage\": 0,\n", + " \"s2:nodata_pixel_percentage\": 84.266186,\n", + " \"s2:unclassified_percentage\": 0,\n", + " \"s2:not_vegetated_percentage\": 0,\n", + " \"s2:degraded_msi_data_percentage\": 0.0219,\n", + " \"s2:high_proba_clouds_percentage\": 0,\n", + " \"s2:reflectance_conversion_factor\": 1.0316374930781,\n", + " \"s2:medium_proba_clouds_percentage\": 28.13631,\n", + " \"s2:saturated_defective_pixel_percentage\": 0\n", + " },\n", + " \"stac_version\": \"1.1.0\",\n", + " \"stac_extensions\": [\n", + " \"https://stac-extensions.github.io/eo/v1.1.0/schema.json\",\n", + " \"https://stac-extensions.github.io/file/v2.1.0/schema.json\",\n", + " \"https://stac-extensions.github.io/grid/v1.1.0/schema.json\",\n", + " \"https://stac-extensions.github.io/mgrs/v1.0.0/schema.json\",\n", + " \"https://stac-extensions.github.io/processing/v1.1.0/schema.json\",\n", + " \"https://stac-extensions.github.io/projection/v2.0.0/schema.json\",\n", + " \"https://stac-extensions.github.io/raster/v1.1.0/schema.json\",\n", + " \"https://stac-extensions.github.io/sentinel-2/v1.0.0/schema.json\",\n", + " \"https://stac-extensions.github.io/storage/v1.0.0/schema.json\",\n", + " \"https://stac-extensions.github.io/view/v1.0.0/schema.json\"\n", + " ]\n", + " }\n", + " ],\n", + " \"numberReturned\": 1,\n", + " \"stac_extensions\": [\n", + " \"https://stac-extensions.github.io/authentication/v1.1.0/schema.json\"\n", + " ],\n", + " \"auth:schemes\": {\n", + " \"oidc\": {\n", + " \"type\": \"openIdConnect\",\n", + " \"openIdConnectUrl\": \"http://localhost:8085/.well-known/openid-configuration\"\n", + " }\n", + " }\n", + "}\n" + ] + } + ], + "source": [ + "datetime_string = datetime(2025, 1, 4, tzinfo=UTC).isoformat()\n", + "\n", + "item_search_request = httpx.get(\n", + " f\"{stac_api_endpoint}/search\",\n", + " params={\n", + " \"collections\": my_collection.id,\n", + " \"datetime\": f\"{datetime_string}/..\", # open interval from 2025-04-04 forward\n", + " \"limit\": 1, # one result per page for brevity in this example\n", + " },\n", + ")\n", + "\n", + "print(json.dumps(item_search_request.json(), indent=2))" + ] + }, + { + "cell_type": "markdown", + "id": "3625785f-8aca-4465-93bc-3c1370c0e2fb", + "metadata": {}, + "source": [ + "stac-fastapi-pgstac constructs the `next` link using a token that it can pass to a `pgstac` query to retrieve the next page of results from this search. STAC API clients like `pystac-client` use these links to concatenate paginated results without any additional input from the user." + ] + }, + { + "cell_type": "markdown", + "id": "87cf6d06-94f7-4925-8652-79008ad0e0d3", + "metadata": {}, + "source": [ + "Now limit the search to items where `eo:cloud_cover` is less than 10" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "e117cea2-8bf2-49cf-9d36-aba3e709d819", + "metadata": {}, + "outputs": [ { - "cell_type": "code", - "metadata": { - "scrolled": true - }, - "source": [ - "item_id = response[\"features\"][0][\"id\"]\n", - "item_request = httpx.get(\n", - " f\"{stac_api_endpoint}/collections/{my_collection.id}/items/{item_id}\"\n", - ")\n", - "print(json.dumps(item_request.json(), indent=2))" - ], - "execution_count": null, - "outputs": [], - "id": "fc47ce1f-68ac-42f2-a9f3-becddb5e2683" + "name": "stdout", + "output_type": "stream", + "text": [ + "found 6 items\n" + ] }, { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "`pystac-client` can do the same thing" + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "
\n", + "
\n", + " <Item id=S2B_T42CWN_20250102T233129_L2A>\n", + "
\n", + "\n", + "
" ], - "id": "13efb26d-e63e-45ed-ab55-5af00e77fbeb" - }, + "text/plain": [ + "" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "search = client.search(\n", + " collections=[my_collection.id],\n", + " filter={\n", + " \"op\": \"lt\",\n", + " \"args\": [\n", + " {\"property\": \"eo:cloud_cover\"},\n", + " 10,\n", + " ],\n", + " },\n", + " max_items=10,\n", + ")\n", + "\n", + "items = search.item_collection()\n", + "\n", + "print(f\"found {len(items)} items\")\n", + "items[-1]" + ] + }, + { + "cell_type": "markdown", + "id": "61d01f82-94f8-4071-b398-2508bc84c56a", + "metadata": {}, + "source": [ + "### 3.3.2 All Items\n", + "\n", + "The API `/collections/{collection_id}/items` endpoint will get you all items in a collection.\n", + "\n", + "You can also run the same search but instead of passing `collections` as a query parameter you can include `collection_id` as a path parameter in the request URL itself. All of the other query parameters for the `/search` GET request will be available." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "2bfec9f1-5bd6-4ea9-90bf-66a2ddf90f36", + "metadata": {}, + "outputs": [ { - "cell_type": "code", - "metadata": {}, - "source": [ - "collection_client = client.get_collection(my_collection.id)\n", - "\n", - "collection_client.get_item(item_id)" - ], - "execution_count": null, - "outputs": [], - "id": "ba409044-3f40-4d53-9256-f21aaa4b9da0" - }, + "name": "stdout", + "output_type": "stream", + "text": [ + "found 5 items\n" + ] + } + ], + "source": [ + "datetime_string = datetime(2025, 1, 4, tzinfo=UTC).isoformat()\n", + "\n", + "item_search_request = httpx.get(\n", + " f\"{stac_api_endpoint}/collections/{my_collection.id}/items\",\n", + " params={\n", + " \"datetime\": f\"{datetime_string}/..\", # open interval from 2025-04-04 forward\n", + " \"limit\": 1000,\n", + " \"filter\": \"eo:cloud_cover < 10\", # less than 10% cloud cover\n", + " },\n", + ")\n", + "response = item_search_request.json()\n", + "print(f\"found {len(response['features'])} items\")" + ] + }, + { + "cell_type": "markdown", + "id": "a1de0ea6-51b5-4b12-88e0-67c2d49012bb", + "metadata": {}, + "source": [ + "### 3.3.3 Single Item by ID\n", + "\n", + "To retrieve a specific item from the catalog, you can use the `/collections/{collection_id}/items/{item_id}` endpoint." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "fc47ce1f-68ac-42f2-a9f3-becddb5e2683", + "metadata": { + "scrolled": true + }, + "outputs": [ { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Conclusion\n", - "\n", - "That's it! You have taken a full tour of the stac-fastapi-pgstac STAC API. Here is a look at the full API documentation for the deployed API:" - ], - "id": "7795389e-2007-4e79-b1ef-a788ac259694" - }, + "name": "stdout", + "output_type": "stream", + "text": [ + "{\n", + " \"id\": \"S2B_T42CWN_20250129T232126_L2A\",\n", + " \"bbox\": [\n", + " 74.401618,\n", + " -82.829135,\n", + " 76.092082,\n", + " -82.80145\n", + " ],\n", + " \"type\": \"Feature\",\n", + " \"links\": [\n", + " {\n", + " \"rel\": \"collection\",\n", + " \"type\": \"application/json\",\n", + " \"href\": \"http://stac-auth-proxy:8000/collections/super-mouse-3144-sentinel-2-c1-l2a\"\n", + " },\n", + " {\n", + " \"rel\": \"parent\",\n", + " \"type\": \"application/json\",\n", + " \"href\": \"http://stac-auth-proxy:8000/collections/super-mouse-3144-sentinel-2-c1-l2a\"\n", + " },\n", + " {\n", + " \"rel\": \"root\",\n", + " \"type\": \"application/json\",\n", + " \"href\": \"http://stac-auth-proxy:8000/\"\n", + " },\n", + " {\n", + " \"rel\": \"self\",\n", + " \"type\": \"application/geo+json\",\n", + " \"href\": \"http://stac-auth-proxy:8000/collections/super-mouse-3144-sentinel-2-c1-l2a/items/S2B_T42CWN_20250129T232126_L2A\"\n", + " },\n", + " {\n", + " \"rel\": \"canonical\",\n", + " \"href\": \"s3://e84-earth-search-sentinel-data/sentinel-2-c1-l2a/42/C/WN/2025/1/S2B_T42CWN_20250129T232126_L2A/S2B_T42CWN_20250129T232126_L2A.json\",\n", + " \"type\": \"application/json\"\n", + " },\n", + " {\n", + " \"rel\": \"via\",\n", + " \"href\": \"s3://sentinel-s2-l2a/tiles/42/C/WN/2025/1/29/0/metadata.xml\",\n", + " \"type\": \"application/xml\",\n", + " \"title\": \"Granule Metadata in Sinergize RODA Archive\"\n", + " },\n", + " {\n", + " \"rel\": \"thumbnail\",\n", + " \"href\": \"https://earth-search.aws.element84.com/v1/collections/sentinel-2-c1-l2a/items/S2B_T42CWN_20250129T232126_L2A/thumbnail\"\n", + " }\n", + " ],\n", + " \"assets\": {\n", + " \"aot\": {\n", + " \"gsd\": 20,\n", + " \"href\": \"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/42/C/WN/2025/1/S2B_T42CWN_20250129T232126_L2A/AOT.tif\",\n", + " \"type\": \"image/tiff; application=geotiff; profile=cloud-optimized\",\n", + " \"roles\": [\n", + " \"data\"\n", + " ],\n", + " \"title\": \"Aerosol optical thickness (AOT)\",\n", + " \"file:size\": 97290,\n", + " \"proj:shape\": [\n", + " 5490,\n", + " 5490\n", + " ],\n", + " \"raster:bands\": [\n", + " {\n", + " \"scale\": 0.001,\n", + " \"nodata\": 0,\n", + " \"offset\": 0,\n", + " \"data_type\": \"uint16\",\n", + " \"spatial_resolution\": 20\n", + " }\n", + " ],\n", + " \"file:checksum\": \"1220786825f52e35b66b8bc070f835e601325bbcf2f6f45af203187291f247f69371\",\n", + " \"proj:transform\": [\n", + " 20,\n", + " 0,\n", + " 499980,\n", + " 0,\n", + " -20,\n", + " 800020\n", + " ]\n", + " },\n", + " \"nir\": {\n", + " \"gsd\": 10,\n", + " \"href\": \"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/42/C/WN/2025/1/S2B_T42CWN_20250129T232126_L2A/B08.tif\",\n", + " \"type\": \"image/tiff; application=geotiff; profile=cloud-optimized\",\n", + " \"roles\": [\n", + " \"data\",\n", + " \"reflectance\"\n", + " ],\n", + " \"title\": \"NIR 1 - 10m\",\n", + " \"eo:bands\": [\n", + " {\n", + " \"name\": \"B08\",\n", + " \"common_name\": \"nir\",\n", + " \"center_wavelength\": 0.842,\n", + " \"full_width_half_max\": 0.145\n", + " }\n", + " ],\n", + " \"file:size\": 1021169,\n", + " \"proj:shape\": [\n", + " 10980,\n", + " 10980\n", + " ],\n", + " \"raster:bands\": [\n", + " {\n", + " \"scale\": 0.0001,\n", + " \"nodata\": 0,\n", + " \"offset\": -0.1,\n", + " \"data_type\": \"uint16\",\n", + " \"spatial_resolution\": 10\n", + " }\n", + " ],\n", + " \"file:checksum\": \"12206a5c2546eaaa78ca2d65b089510cbb4d18fb511a3691a4c91441591c7afbe177\",\n", + " \"proj:transform\": [\n", + " 10,\n", + " 0,\n", + " 499980,\n", + " 0,\n", + " -10,\n", + " 800020\n", + " ]\n", + " },\n", + " \"red\": {\n", + " \"gsd\": 10,\n", + " \"href\": \"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/42/C/WN/2025/1/S2B_T42CWN_20250129T232126_L2A/B04.tif\",\n", + " \"type\": \"image/tiff; application=geotiff; profile=cloud-optimized\",\n", + " \"roles\": [\n", + " \"data\",\n", + " \"reflectance\"\n", + " ],\n", + " \"title\": \"Red - 10m\",\n", + " \"eo:bands\": [\n", + " {\n", + " \"name\": \"B04\",\n", + " \"common_name\": \"red\",\n", + " \"center_wavelength\": 0.665,\n", + " \"full_width_half_max\": 0.038\n", + " }\n", + " ],\n", + " \"file:size\": 985898,\n", + " \"proj:shape\": [\n", + " 10980,\n", + " 10980\n", + " ],\n", + " \"raster:bands\": [\n", + " {\n", + " \"scale\": 0.0001,\n", + " \"nodata\": 0,\n", + " \"offset\": -0.1,\n", + " \"data_type\": \"uint16\",\n", + " \"spatial_resolution\": 10\n", + " }\n", + " ],\n", + " \"file:checksum\": \"12203163d3c2dfcc1a7019d7868e82e9d0d4295d6356b851780b51e6323f5fe46f96\",\n", + " \"proj:transform\": [\n", + " 10,\n", + " 0,\n", + " 499980,\n", + " 0,\n", + " -10,\n", + " 800020\n", + " ]\n", + " },\n", + " \"scl\": {\n", + " \"gsd\": 20,\n", + " \"href\": \"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/42/C/WN/2025/1/S2B_T42CWN_20250129T232126_L2A/SCL.tif\",\n", + " \"type\": \"image/tiff; application=geotiff; profile=cloud-optimized\",\n", + " \"roles\": [\n", + " \"data\"\n", + " ],\n", + " \"title\": \"Scene classification map (SCL)\",\n", + " \"file:size\": 55281,\n", + " \"proj:shape\": [\n", + " 5490,\n", + " 5490\n", + " ],\n", + " \"raster:bands\": [\n", + " {\n", + " \"nodata\": 0,\n", + " \"data_type\": \"uint8\",\n", + " \"spatial_resolution\": 20\n", + " }\n", + " ],\n", + " \"file:checksum\": \"1220c2cbf485c1fcb193b34dd3ef5f2b42357fb5e499eae719fa3e4290179e30730d\",\n", + " \"proj:transform\": [\n", + " 20,\n", + " 0,\n", + " 499980,\n", + " 0,\n", + " -20,\n", + " 800020\n", + " ]\n", + " },\n", + " \"wvp\": {\n", + " \"gsd\": 20,\n", + " \"href\": \"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/42/C/WN/2025/1/S2B_T42CWN_20250129T232126_L2A/WVP.tif\",\n", + " \"type\": \"image/tiff; application=geotiff; profile=cloud-optimized\",\n", + " \"roles\": [\n", + " \"data\"\n", + " ],\n", + " \"title\": \"Water Vapour (WVP)\",\n", + " \"file:size\": 183850,\n", + " \"proj:shape\": [\n", + " 5490,\n", + " 5490\n", + " ],\n", + " \"raster:bands\": [\n", + " {\n", + " \"unit\": \"cm\",\n", + " \"scale\": 0.001,\n", + " \"nodata\": 0,\n", + " \"offset\": 0,\n", + " \"data_type\": \"uint16\",\n", + " \"spatial_resolution\": 20\n", + " }\n", + " ],\n", + " \"file:checksum\": \"1220a54a7d41990b2612b6dd0c5830a365ffa2a7d2d9f1b5e9464bff3f350829e86e\",\n", + " \"proj:transform\": [\n", + " 20,\n", + " 0,\n", + " 499980,\n", + " 0,\n", + " -20,\n", + " 800020\n", + " ]\n", + " },\n", + " \"blue\": {\n", + " \"gsd\": 10,\n", + " \"href\": \"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/42/C/WN/2025/1/S2B_T42CWN_20250129T232126_L2A/B02.tif\",\n", + " \"type\": \"image/tiff; application=geotiff; profile=cloud-optimized\",\n", + " \"roles\": [\n", + " \"data\",\n", + " \"reflectance\"\n", + " ],\n", + " \"title\": \"Blue - 10m\",\n", + " \"eo:bands\": [\n", + " {\n", + " \"name\": \"B02\",\n", + " \"common_name\": \"blue\",\n", + " \"center_wavelength\": 0.49,\n", + " \"full_width_half_max\": 0.098\n", + " }\n", + " ],\n", + " \"file:size\": 958799,\n", + " \"proj:shape\": [\n", + " 10980,\n", + " 10980\n", + " ],\n", + " \"raster:bands\": [\n", + " {\n", + " \"scale\": 0.0001,\n", + " \"nodata\": 0,\n", + " \"offset\": -0.1,\n", + " \"data_type\": \"uint16\",\n", + " \"spatial_resolution\": 10\n", + " }\n", + " ],\n", + " \"file:checksum\": \"12207119e632a28411b979a318ff8b5b936ecab85378229848f90d0d1fc3f29c6ce0\",\n", + " \"proj:transform\": [\n", + " 10,\n", + " 0,\n", + " 499980,\n", + " 0,\n", + " -10,\n", + " 800020\n", + " ]\n", + " },\n", + " \"snow\": {\n", + " \"href\": \"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/42/C/WN/2025/1/S2B_T42CWN_20250129T232126_L2A/SNW_20m.tif\",\n", + " \"type\": \"image/tiff; application=geotiff; profile=cloud-optimized\",\n", + " \"roles\": [\n", + " \"data\",\n", + " \"snow-ice\"\n", + " ],\n", + " \"title\": \"Snow Probabilities\",\n", + " \"file:size\": 55796,\n", + " \"proj:shape\": [\n", + " 5490,\n", + " 5490\n", + " ],\n", + " \"raster:bands\": [\n", + " {\n", + " \"nodata\": 0,\n", + " \"data_type\": \"uint8\",\n", + " \"spatial_resolution\": 20\n", + " }\n", + " ],\n", + " \"file:checksum\": \"1220861dad31e60aca91d848cf402b043beb094ca878071007fc90a5f848b19de524\",\n", + " \"proj:transform\": [\n", + " 20,\n", + " 0,\n", + " 499980,\n", + " 0,\n", + " -20,\n", + " 800020\n", + " ]\n", + " },\n", + " \"cloud\": {\n", + " \"gsd\": 20,\n", + " \"href\": \"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/42/C/WN/2025/1/S2B_T42CWN_20250129T232126_L2A/CLD_20m.tif\",\n", + " \"type\": \"image/tiff; application=geotiff; profile=cloud-optimized\",\n", + " \"roles\": [\n", + " \"data\",\n", + " \"cloud\"\n", + " ],\n", + " \"title\": \"Cloud Probabilities\",\n", + " \"file:size\": 53931,\n", + " \"proj:shape\": [\n", + " 5490,\n", + " 5490\n", + " ],\n", + " \"raster:bands\": [\n", + " {\n", + " \"nodata\": 0,\n", + " \"data_type\": \"uint8\",\n", + " \"spatial_resolution\": 20\n", + " }\n", + " ],\n", + " \"file:checksum\": \"1220af3f56aeb8c3e1558b6fe1e7413158b3204e33f17a9b46047c2bfe4dcea97100\",\n", + " \"proj:transform\": [\n", + " 20,\n", + " 0,\n", + " 499980,\n", + " 0,\n", + " -20,\n", + " 800020\n", + " ]\n", + " },\n", + " \"green\": {\n", + " \"gsd\": 10,\n", + " \"href\": \"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/42/C/WN/2025/1/S2B_T42CWN_20250129T232126_L2A/B03.tif\",\n", + " \"type\": \"image/tiff; application=geotiff; profile=cloud-optimized\",\n", + " \"roles\": [\n", + " \"data\",\n", + " \"reflectance\"\n", + " ],\n", + " \"title\": \"Green - 10m\",\n", + " \"eo:bands\": [\n", + " {\n", + " \"name\": \"B03\",\n", + " \"common_name\": \"green\",\n", + " \"center_wavelength\": 0.56,\n", + " \"full_width_half_max\": 0.045\n", + " }\n", + " ],\n", + " \"file:size\": 976749,\n", + " \"proj:shape\": [\n", + " 10980,\n", + " 10980\n", + " ],\n", + " \"raster:bands\": [\n", + " {\n", + " \"scale\": 0.0001,\n", + " \"nodata\": 0,\n", + " \"offset\": -0.1,\n", + " \"data_type\": \"uint16\",\n", + " \"spatial_resolution\": 10\n", + " }\n", + " ],\n", + " \"file:checksum\": \"1220d0f99f6735633e741883aad9a0b3839ab1f93aad2d0884c2e92637d18b7ef69f\",\n", + " \"proj:transform\": [\n", + " 10,\n", + " 0,\n", + " 499980,\n", + " 0,\n", + " -10,\n", + " 800020\n", + " ]\n", + " },\n", + " \"nir08\": {\n", + " \"gsd\": 20,\n", + " \"href\": \"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/42/C/WN/2025/1/S2B_T42CWN_20250129T232126_L2A/B8A.tif\",\n", + " \"type\": \"image/tiff; application=geotiff; profile=cloud-optimized\",\n", + " \"roles\": [\n", + " \"data\",\n", + " \"reflectance\"\n", + " ],\n", + " \"title\": \"NIR 2 - 20m\",\n", + " \"eo:bands\": [\n", + " {\n", + " \"name\": \"B8A\",\n", + " \"common_name\": \"nir08\",\n", + " \"center_wavelength\": 0.865,\n", + " \"full_width_half_max\": 0.033\n", + " }\n", + " ],\n", + " \"file:size\": 321172,\n", + " \"proj:shape\": [\n", + " 5490,\n", + " 5490\n", + " ],\n", + " \"raster:bands\": [\n", + " {\n", + " \"scale\": 0.0001,\n", + " \"nodata\": 0,\n", + " \"offset\": -0.1,\n", + " \"data_type\": \"uint16\",\n", + " \"spatial_resolution\": 20\n", + " }\n", + " ],\n", + " \"file:checksum\": \"122094b86882e31cd0e4daa393d762c6bdd6a991dc503a8eae5ebaac79660d3a4ccf\",\n", + " \"proj:transform\": [\n", + " 20,\n", + " 0,\n", + " 499980,\n", + " 0,\n", + " -20,\n", + " 800020\n", + " ]\n", + " },\n", + " \"nir09\": {\n", + " \"gsd\": 60,\n", + " \"href\": \"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/42/C/WN/2025/1/S2B_T42CWN_20250129T232126_L2A/B09.tif\",\n", + " \"type\": \"image/tiff; application=geotiff; profile=cloud-optimized\",\n", + " \"roles\": [\n", + " \"data\",\n", + " \"reflectance\"\n", + " ],\n", + " \"title\": \"NIR 3 - 60m\",\n", + " \"eo:bands\": [\n", + " {\n", + " \"name\": \"B09\",\n", + " \"common_name\": \"nir09\",\n", + " \"center_wavelength\": 0.945,\n", + " \"full_width_half_max\": 0.026\n", + " }\n", + " ],\n", + " \"file:size\": 46582,\n", + " \"proj:shape\": [\n", + " 1830,\n", + " 1830\n", + " ],\n", + " \"raster:bands\": [\n", + " {\n", + " \"scale\": 0.0001,\n", + " \"nodata\": 0,\n", + " \"offset\": -0.1,\n", + " \"data_type\": \"uint16\",\n", + " \"spatial_resolution\": 60\n", + " }\n", + " ],\n", + " \"file:checksum\": \"12201ce66972bf52b27b1603281e1bada703282a44386519a34ff84999b383276c3c\",\n", + " \"proj:transform\": [\n", + " 60,\n", + " 0,\n", + " 499980,\n", + " 0,\n", + " -60,\n", + " 800020\n", + " ]\n", + " },\n", + " \"swir16\": {\n", + " \"gsd\": 20,\n", + " \"href\": \"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/42/C/WN/2025/1/S2B_T42CWN_20250129T232126_L2A/B11.tif\",\n", + " \"type\": \"image/tiff; application=geotiff; profile=cloud-optimized\",\n", + " \"roles\": [\n", + " \"data\",\n", + " \"reflectance\"\n", + " ],\n", + " \"title\": \"SWIR 1.6\\u03bcm - 20m\",\n", + " \"eo:bands\": [\n", + " {\n", + " \"name\": \"B11\",\n", + " \"common_name\": \"swir16\",\n", + " \"center_wavelength\": 1.61,\n", + " \"full_width_half_max\": 0.143\n", + " }\n", + " ],\n", + " \"file:size\": 289791,\n", + " \"proj:shape\": [\n", + " 5490,\n", + " 5490\n", + " ],\n", + " \"raster:bands\": [\n", + " {\n", + " \"scale\": 0.0001,\n", + " \"nodata\": 0,\n", + " \"offset\": -0.1,\n", + " \"data_type\": \"uint16\",\n", + " \"spatial_resolution\": 20\n", + " }\n", + " ],\n", + " \"file:checksum\": \"12205930e99f68fc4cc913cd2d7480fd942b36bc042226257a691c2a4516715389cb\",\n", + " \"proj:transform\": [\n", + " 20,\n", + " 0,\n", + " 499980,\n", + " 0,\n", + " -20,\n", + " 800020\n", + " ]\n", + " },\n", + " \"swir22\": {\n", + " \"gsd\": 20,\n", + " \"href\": \"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/42/C/WN/2025/1/S2B_T42CWN_20250129T232126_L2A/B12.tif\",\n", + " \"type\": \"image/tiff; application=geotiff; profile=cloud-optimized\",\n", + " \"roles\": [\n", + " \"data\",\n", + " \"reflectance\"\n", + " ],\n", + " \"title\": \"SWIR 2.2\\u03bcm - 20m\",\n", + " \"eo:bands\": [\n", + " {\n", + " \"name\": \"B12\",\n", + " \"common_name\": \"swir22\",\n", + " \"center_wavelength\": 2.19,\n", + " \"full_width_half_max\": 0.242\n", + " }\n", + " ],\n", + " \"file:size\": 292540,\n", + " \"proj:shape\": [\n", + " 5490,\n", + " 5490\n", + " ],\n", + " \"raster:bands\": [\n", + " {\n", + " \"scale\": 0.0001,\n", + " \"nodata\": 0,\n", + " \"offset\": -0.1,\n", + " \"data_type\": \"uint16\",\n", + " \"spatial_resolution\": 20\n", + " }\n", + " ],\n", + " \"file:checksum\": \"122028203f2b1cccc5e85cb91ad9e0b2c85032fd3918fe81118ec627455921f1fe8f\",\n", + " \"proj:transform\": [\n", + " 20,\n", + " 0,\n", + " 499980,\n", + " 0,\n", + " -20,\n", + " 800020\n", + " ]\n", + " },\n", + " \"visual\": {\n", + " \"gsd\": 10,\n", + " \"href\": \"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/42/C/WN/2025/1/S2B_T42CWN_20250129T232126_L2A/TCI.tif\",\n", + " \"type\": \"image/tiff; application=geotiff; profile=cloud-optimized\",\n", + " \"roles\": [\n", + " \"visual\"\n", + " ],\n", + " \"title\": \"True color image\",\n", + " \"eo:bands\": [\n", + " {\n", + " \"name\": \"B04\",\n", + " \"common_name\": \"red\",\n", + " \"center_wavelength\": 0.665,\n", + " \"full_width_half_max\": 0.038\n", + " },\n", + " {\n", + " \"name\": \"B03\",\n", + " \"common_name\": \"green\",\n", + " \"center_wavelength\": 0.56,\n", + " \"full_width_half_max\": 0.045\n", + " },\n", + " {\n", + " \"name\": \"B02\",\n", + " \"common_name\": \"blue\",\n", + " \"center_wavelength\": 0.49,\n", + " \"full_width_half_max\": 0.098\n", + " }\n", + " ],\n", + " \"file:size\": 517940,\n", + " \"proj:shape\": [\n", + " 10980,\n", + " 10980\n", + " ],\n", + " \"raster:bands\": [\n", + " {\n", + " \"nodata\": 0,\n", + " \"data_type\": \"uint8\",\n", + " \"spatial_resolution\": 10\n", + " },\n", + " {\n", + " \"nodata\": 0,\n", + " \"data_type\": \"uint8\",\n", + " \"spatial_resolution\": 10\n", + " },\n", + " {\n", + " \"nodata\": 0,\n", + " \"data_type\": \"uint8\",\n", + " \"spatial_resolution\": 10\n", + " }\n", + " ],\n", + " \"file:checksum\": \"12204405a14f7113a9de2187d0ace792de98ff1745100ce42ac4f270319db51c8cf6\",\n", + " \"proj:transform\": [\n", + " 10,\n", + " 0,\n", + " 499980,\n", + " 0,\n", + " -10,\n", + " 800020\n", + " ]\n", + " },\n", + " \"coastal\": {\n", + " \"gsd\": 60,\n", + " \"href\": \"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/42/C/WN/2025/1/S2B_T42CWN_20250129T232126_L2A/B01.tif\",\n", + " \"type\": \"image/tiff; application=geotiff; profile=cloud-optimized\",\n", + " \"roles\": [\n", + " \"data\",\n", + " \"reflectance\"\n", + " ],\n", + " \"title\": \"Coastal - 60m\",\n", + " \"eo:bands\": [\n", + " {\n", + " \"name\": \"B01\",\n", + " \"common_name\": \"coastal\",\n", + " \"center_wavelength\": 0.443,\n", + " \"full_width_half_max\": 0.027\n", + " }\n", + " ],\n", + " \"file:size\": 45968,\n", + " \"proj:shape\": [\n", + " 1830,\n", + " 1830\n", + " ],\n", + " \"raster:bands\": [\n", + " {\n", + " \"scale\": 0.0001,\n", + " \"nodata\": 0,\n", + " \"offset\": -0.1,\n", + " \"data_type\": \"uint16\",\n", + " \"spatial_resolution\": 60\n", + " }\n", + " ],\n", + " \"file:checksum\": \"1220ea28e3476a036f55ccd70923db085351ab06924b5e0df10aade68b2e6be2a9e1\",\n", + " \"proj:transform\": [\n", + " 60,\n", + " 0,\n", + " 499980,\n", + " 0,\n", + " -60,\n", + " 800020\n", + " ]\n", + " },\n", + " \"preview\": {\n", + " \"href\": \"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/42/C/WN/2025/1/S2B_T42CWN_20250129T232126_L2A/L2A_PVI.tif\",\n", + " \"type\": \"image/tiff; application=geotiff; profile=cloud-optimized\",\n", + " \"roles\": [\n", + " \"overview\"\n", + " ],\n", + " \"title\": \"True color preview\",\n", + " \"eo:bands\": [\n", + " {\n", + " \"name\": \"B04\",\n", + " \"common_name\": \"red\",\n", + " \"center_wavelength\": 0.665,\n", + " \"full_width_half_max\": 0.038\n", + " },\n", + " {\n", + " \"name\": \"B03\",\n", + " \"common_name\": \"green\",\n", + " \"center_wavelength\": 0.56,\n", + " \"full_width_half_max\": 0.045\n", + " },\n", + " {\n", + " \"name\": \"B02\",\n", + " \"common_name\": \"blue\",\n", + " \"center_wavelength\": 0.49,\n", + " \"full_width_half_max\": 0.098\n", + " }\n", + " ],\n", + " \"file:size\": 4728,\n", + " \"file:checksum\": \"12208baf394b6c39139ff869369f0b4bb2ab8189d53ed6a718690d4ebf8fc57cc526\"\n", + " },\n", + " \"rededge1\": {\n", + " \"gsd\": 20,\n", + " \"href\": \"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/42/C/WN/2025/1/S2B_T42CWN_20250129T232126_L2A/B05.tif\",\n", + " \"type\": \"image/tiff; application=geotiff; profile=cloud-optimized\",\n", + " \"roles\": [\n", + " \"data\",\n", + " \"reflectance\"\n", + " ],\n", + " \"title\": \"Red Edge 1 - 20m\",\n", + " \"eo:bands\": [\n", + " {\n", + " \"name\": \"B05\",\n", + " \"common_name\": \"rededge\",\n", + " \"center_wavelength\": 0.704,\n", + " \"full_width_half_max\": 0.019\n", + " }\n", + " ],\n", + " \"file:size\": 319661,\n", + " \"proj:shape\": [\n", + " 5490,\n", + " 5490\n", + " ],\n", + " \"raster:bands\": [\n", + " {\n", + " \"scale\": 0.0001,\n", + " \"nodata\": 0,\n", + " \"offset\": -0.1,\n", + " \"data_type\": \"uint16\",\n", + " \"spatial_resolution\": 20\n", + " }\n", + " ],\n", + " \"file:checksum\": \"1220e36385e16d9117946b3c89266353c4f02459aef6489d447780ab391cf27e0a55\",\n", + " \"proj:transform\": [\n", + " 20,\n", + " 0,\n", + " 499980,\n", + " 0,\n", + " -20,\n", + " 800020\n", + " ]\n", + " },\n", + " \"rededge2\": {\n", + " \"gsd\": 20,\n", + " \"href\": \"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/42/C/WN/2025/1/S2B_T42CWN_20250129T232126_L2A/B06.tif\",\n", + " \"type\": \"image/tiff; application=geotiff; profile=cloud-optimized\",\n", + " \"roles\": [\n", + " \"data\",\n", + " \"reflectance\"\n", + " ],\n", + " \"title\": \"Red Edge 2 - 20m\",\n", + " \"eo:bands\": [\n", + " {\n", + " \"name\": \"B06\",\n", + " \"common_name\": \"rededge\",\n", + " \"center_wavelength\": 0.74,\n", + " \"full_width_half_max\": 0.018\n", + " }\n", + " ],\n", + " \"file:size\": 320120,\n", + " \"proj:shape\": [\n", + " 5490,\n", + " 5490\n", + " ],\n", + " \"raster:bands\": [\n", + " {\n", + " \"scale\": 0.0001,\n", + " \"nodata\": 0,\n", + " \"offset\": -0.1,\n", + " \"data_type\": \"uint16\",\n", + " \"spatial_resolution\": 20\n", + " }\n", + " ],\n", + " \"file:checksum\": \"12208810f6ea7a551e6cbefa9f1868085b9564d41b8303d5cdb4a4a3d12bb1549a7b\",\n", + " \"proj:transform\": [\n", + " 20,\n", + " 0,\n", + " 499980,\n", + " 0,\n", + " -20,\n", + " 800020\n", + " ]\n", + " },\n", + " \"rededge3\": {\n", + " \"gsd\": 20,\n", + " \"href\": \"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/42/C/WN/2025/1/S2B_T42CWN_20250129T232126_L2A/B07.tif\",\n", + " \"type\": \"image/tiff; application=geotiff; profile=cloud-optimized\",\n", + " \"roles\": [\n", + " \"data\",\n", + " \"reflectance\"\n", + " ],\n", + " \"title\": \"Red Edge 3 - 20m\",\n", + " \"eo:bands\": [\n", + " {\n", + " \"name\": \"B07\",\n", + " \"common_name\": \"rededge\",\n", + " \"center_wavelength\": 0.783,\n", + " \"full_width_half_max\": 0.028\n", + " }\n", + " ],\n", + " \"file:size\": 319859,\n", + " \"proj:shape\": [\n", + " 5490,\n", + " 5490\n", + " ],\n", + " \"raster:bands\": [\n", + " {\n", + " \"scale\": 0.0001,\n", + " \"nodata\": 0,\n", + " \"offset\": -0.1,\n", + " \"data_type\": \"uint16\",\n", + " \"spatial_resolution\": 20\n", + " }\n", + " ],\n", + " \"file:checksum\": \"1220e25efa4a7007f9161c25f8bc767cdb6e1d237a7c1f648ea0afded714c326f7fa\",\n", + " \"proj:transform\": [\n", + " 20,\n", + " 0,\n", + " 499980,\n", + " 0,\n", + " -20,\n", + " 800020\n", + " ]\n", + " },\n", + " \"thumbnail\": {\n", + " \"href\": \"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/42/C/WN/2025/1/S2B_T42CWN_20250129T232126_L2A/L2A_PVI.jpg\",\n", + " \"type\": \"image/jpeg\",\n", + " \"roles\": [\n", + " \"thumbnail\"\n", + " ],\n", + " \"title\": \"Thumbnail of preview image\",\n", + " \"file:size\": 2919,\n", + " \"file:checksum\": \"122098b8cedb52c6653bd3af9dd9ca1030b0e0c7d9333fe9cb20f82df266d15a5704\"\n", + " },\n", + " \"granule_metadata\": {\n", + " \"href\": \"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/42/C/WN/2025/1/S2B_T42CWN_20250129T232126_L2A/metadata.xml\",\n", + " \"type\": \"application/xml\",\n", + " \"roles\": [\n", + " \"metadata\"\n", + " ],\n", + " \"file:size\": 100326,\n", + " \"file:checksum\": \"1220eb0b5891af76377240867bc7acb8be589ab533db9749348a1d1b6f51ea15a5da\"\n", + " },\n", + " \"product_metadata\": {\n", + " \"href\": \"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/42/C/WN/2025/1/S2B_T42CWN_20250129T232126_L2A/product_metadata.xml\",\n", + " \"type\": \"application/xml\",\n", + " \"roles\": [\n", + " \"metadata\"\n", + " ],\n", + " \"file:size\": 54454,\n", + " \"file:checksum\": \"122046d99e0679f5615bde4c82bbc8e501ccffd2f58c98e2e814f6159a5603843e01\"\n", + " },\n", + " \"tileinfo_metadata\": {\n", + " \"href\": \"https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/42/C/WN/2025/1/S2B_T42CWN_20250129T232126_L2A/tileInfo.json\",\n", + " \"type\": \"application/json\",\n", + " \"roles\": [\n", + " \"metadata\"\n", + " ],\n", + " \"file:size\": 1567,\n", + " \"file:checksum\": \"12201050d1b5306817aae5dbf90388de067253fec2342e29847f7d9e7debf7ba2cee\"\n", + " }\n", + " },\n", + " \"geometry\": {\n", + " \"type\": \"Polygon\",\n", + " \"coordinates\": [\n", + " [\n", + " [\n", + " 75.34511242157045,\n", + " -82.82696622572307\n", + " ],\n", + " [\n", + " 76.06609450832383,\n", + " -82.82913498215767\n", + " ],\n", + " [\n", + " 76.09208230786994,\n", + " -82.8014501300728\n", + " ],\n", + " [\n", + " 74.40161757155647,\n", + " -82.82206660762367\n", + " ],\n", + " [\n", + " 75.34511242157045,\n", + " -82.82696622572307\n", + " ]\n", + " ]\n", + " ]\n", + " },\n", + " \"collection\": \"super-mouse-3144-sentinel-2-c1-l2a\",\n", + " \"properties\": {\n", + " \"created\": \"2025-01-30T00:47:19.713Z\",\n", + " \"updated\": \"2025-01-30T00:47:19.713Z\",\n", + " \"datetime\": \"2025-01-29T23:21:30.814000Z\",\n", + " \"platform\": \"sentinel-2b\",\n", + " \"grid:code\": \"MGRS-42CWN\",\n", + " \"proj:code\": \"EPSG:32742\",\n", + " \"s2:tile_id\": \"S2B_OPER_MSI_L2A_TL_2BPS_20250129T235902_A041268_T42CWN_N05.11\",\n", + " \"instruments\": [\n", + " \"msi\"\n", + " ],\n", + " \"view:azimuth\": 9.509084113642079,\n", + " \"constellation\": \"sentinel-2\",\n", + " \"mgrs:utm_zone\": 42,\n", + " \"proj:centroid\": {\n", + " \"lat\": -82.81782,\n", + " \"lon\": 75.51218\n", + " },\n", + " \"eo:cloud_cover\": 0,\n", + " \"s2:datatake_id\": \"GS2B_20250129T232129_041268_N05.11\",\n", + " \"s2:product_uri\": \"S2B_MSIL2A_20250129T232129_N0511_R058_T42CWN_20250129T235902.SAFE\",\n", + " \"storage:region\": \"us-west-2\",\n", + " \"s2:datastrip_id\": \"S2B_OPER_MSI_L2A_DS_2BPS_20250129T235902_S20250129T232126_N05.11\",\n", + " \"s2:product_type\": \"S2MSI2A\",\n", + " \"mgrs:grid_square\": \"WN\",\n", + " \"s2:datatake_type\": \"INS-NOBS\",\n", + " \"storage:platform\": \"AWS\",\n", + " \"view:sun_azimuth\": 121.297984565818,\n", + " \"mgrs:latitude_band\": \"C\",\n", + " \"s2:generation_time\": \"2025-01-29T23:59:02.000000Z\",\n", + " \"view:sun_elevation\": 14.264799050438398,\n", + " \"processing:software\": {\n", + " \"sentinel-2-c1-l2a-to-stac\": \"v2024.02.01\"\n", + " },\n", + " \"s2:water_percentage\": 0,\n", + " \"view:incidence_angle\": 11.886886391067208,\n", + " \"earthsearch:payload_id\": \"roda-sentinel-2-c1-l2a/workflow-sentinel-2-c1-l2a-to-stac/ade38bf372145bb11c132565da8879da\",\n", + " \"s2:processing_baseline\": \"05.11\",\n", + " \"s2:snow_ice_percentage\": 100,\n", + " \"storage:requester_pays\": false,\n", + " \"s2:vegetation_percentage\": 0,\n", + " \"s2:thin_cirrus_percentage\": 0,\n", + " \"s2:cloud_shadow_percentage\": 0,\n", + " \"s2:nodata_pixel_percentage\": 99.621159,\n", + " \"s2:unclassified_percentage\": 0,\n", + " \"s2:not_vegetated_percentage\": 0,\n", + " \"s2:degraded_msi_data_percentage\": 0,\n", + " \"s2:high_proba_clouds_percentage\": 0,\n", + " \"s2:reflectance_conversion_factor\": 1.0316374930781,\n", + " \"s2:medium_proba_clouds_percentage\": 0,\n", + " \"s2:saturated_defective_pixel_percentage\": 0,\n", + " \"auth:schemes\": {\n", + " \"oidc\": {\n", + " \"type\": \"openIdConnect\",\n", + " \"openIdConnectUrl\": \"http://localhost:8085/.well-known/openid-configuration\"\n", + " }\n", + " }\n", + " },\n", + " \"stac_version\": \"1.1.0\",\n", + " \"stac_extensions\": [\n", + " \"https://stac-extensions.github.io/eo/v1.1.0/schema.json\",\n", + " \"https://stac-extensions.github.io/file/v2.1.0/schema.json\",\n", + " \"https://stac-extensions.github.io/grid/v1.1.0/schema.json\",\n", + " \"https://stac-extensions.github.io/mgrs/v1.0.0/schema.json\",\n", + " \"https://stac-extensions.github.io/processing/v1.1.0/schema.json\",\n", + " \"https://stac-extensions.github.io/projection/v2.0.0/schema.json\",\n", + " \"https://stac-extensions.github.io/raster/v1.1.0/schema.json\",\n", + " \"https://stac-extensions.github.io/sentinel-2/v1.0.0/schema.json\",\n", + " \"https://stac-extensions.github.io/storage/v1.0.0/schema.json\",\n", + " \"https://stac-extensions.github.io/view/v1.0.0/schema.json\",\n", + " \"https://stac-extensions.github.io/authentication/v1.1.0/schema.json\"\n", + " ]\n", + "}\n" + ] + } + ], + "source": [ + "item_id = response[\"features\"][0][\"id\"]\n", + "item_request = httpx.get(\n", + " f\"{stac_api_endpoint}/collections/{my_collection.id}/items/{item_id}\"\n", + ")\n", + "print(json.dumps(item_request.json(), indent=2))" + ] + }, + { + "cell_type": "markdown", + "id": "13efb26d-e63e-45ed-ab55-5af00e77fbeb", + "metadata": {}, + "source": [ + "`pystac-client` can do the same thing" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "ba409044-3f40-4d53-9256-f21aaa4b9da0", + "metadata": {}, + "outputs": [ { - "cell_type": "code", - "metadata": {}, - "source": [ - "api_docs = f\"{local_stac_api_endpoint}/api.html\"\n", - "print(api_docs)\n", - "IFrame(api_docs, 1200, 800)" + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "
\n", + "
\n", + " <Item id=S2B_T42CWN_20250129T232126_L2A>\n", + "
\n", + "\n", + "
" ], - "execution_count": null, - "outputs": [], - "id": "5972eaaf-d6b3-4bcf-b2fd-f7d451100581" - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.12.11" + "text/plain": [ + "" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" } + ], + "source": [ + "collection_client = client.get_collection(my_collection.id)\n", + "\n", + "collection_client.get_item(item_id)" + ] + }, + { + "cell_type": "markdown", + "id": "7795389e-2007-4e79-b1ef-a788ac259694", + "metadata": {}, + "source": [ + "## Conclusion\n", + "\n", + "That's it! You have taken a full tour of the stac-fastapi-pgstac STAC API. Here is a look at the full API documentation for the deployed API:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5972eaaf-d6b3-4bcf-b2fd-f7d451100581", + "metadata": {}, + "outputs": [], + "source": [ + "api_docs = f\"{local_stac_api_endpoint}/api.html\"\n", + "print(api_docs)\n", + "IFrame(api_docs, 1200, 800)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" }, - "nbformat": 4, - "nbformat_minor": 5 -} \ No newline at end of file + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/04-titiler_pgstac.ipynb b/docs/04-titiler_pgstac.ipynb index 95a9bb9..2c92037 100644 --- a/docs/04-titiler_pgstac.ipynb +++ b/docs/04-titiler_pgstac.ipynb @@ -38,11 +38,27 @@ "metadata": {}, "outputs": [], "source": [ + "import os\n", + "\n", + "import httpx\n", "import ipywidgets as widgets\n", "from IPython.display import display\n", "\n", + "# Prefill with the username of the most recent sentinel-2 collection in the\n", + "# catalog (created in notebook 02) — replace it with your own if it differs.\n", + "_collections = (\n", + " httpx.get(f\"{os.getenv('STAC_API_ENDPOINT')}/collections\", timeout=30)\n", + " .json()\n", + " .get(\"collections\", [])\n", + ")\n", + "_usernames = [\n", + " c[\"id\"].removesuffix(\"-sentinel-2-c1-l2a\")\n", + " for c in _collections\n", + " if c[\"id\"].endswith(\"-sentinel-2-c1-l2a\")\n", + "]\n", + "\n", "username_input = widgets.Text(\n", - " value=None,\n", + " value=_usernames[-1] if _usernames else None,\n", " placeholder=\"Enter your username\",\n", " description=\"username:\",\n", " disabled=False,\n", @@ -70,10 +86,13 @@ "\n", "from IPython.display import IFrame, Image\n", "\n", - "titiler_pgstac_endpoint = os.getenv(\"TITILER_PGSTAC_API_ENDPOINT\").replace(\n", - " \"titiler-pgstac\", \"localhost\"\n", - ")\n", - "api_docs = f\"{titiler_pgstac_endpoint}/api.html\"\n", + "titiler_pgstac_endpoint = os.getenv(\"TITILER_PGSTAC_API_ENDPOINT\")\n", + "# browser-facing URL for the IFrame/map cells (the user's browser can't reach\n", + "# the server-side endpoint above when running on Kubernetes or docker-compose)\n", + "titiler_browser_endpoint = os.getenv(\n", + " \"TITILER_BROWSER_URL\"\n", + ") or titiler_pgstac_endpoint.replace(\"titiler-pgstac\", \"localhost\")\n", + "api_docs = f\"{titiler_browser_endpoint}/api.html\"\n", "print(api_docs)\n", "\n", "IFrame(\n", @@ -152,7 +171,7 @@ " - Leaflet\n", " - Mapbox\n", "\n", - "For now you can take a shortcut to view the map directly in this notebook using the `/map` endpoint which will generate an HTML file with a Leaflet map that can be rendered directly in the notebook.\n", + "For now you can take a shortcut to view the map directly in this notebook using the `/map.html` endpoint which will generate an HTML file with a Leaflet map that can be rendered directly in the notebook.\n", "\n", "
\n", "It may take a while to render tiles for the full view because the titiler-pgstac container is downloading data from S3 in order to render the images - zoom in to have a better browsing experience. This performance can be improved when deploying your own eoAPI through careful preparation of data and sourcing of hardware resources, the demo runs on a very small server.\n", @@ -167,7 +186,7 @@ "outputs": [], "source": [ "IFrame(\n", - " f\"{titiler_pgstac_endpoint}/collections/{collection_id}/WebMercatorQuad/map?{urlencode(params, doseq=True)}\",\n", + " f\"{titiler_browser_endpoint}/collections/{collection_id}/WebMercatorQuad/map.html?{urlencode(params, doseq=True)}\",\n", " 1200,\n", " 800,\n", ")" @@ -180,8 +199,8 @@ "source": [ "### How does it work?\n", "\n", - "- titiler-pgstac is running as a Lambda (serverless) function in AWS that started up when you made the request for the `/map` endpoint.\n", - "- The `/map` endpoint returns an HTML file that is pre-populated with some map code that includes the layer that you specified with the request parameters\n", + "- titiler-pgstac is running as a Lambda (serverless) function in AWS that started up when you made the request for the `/map.html` endpoint.\n", + "- The `/map.html` endpoint returns an HTML file that is pre-populated with some map code that includes the layer that you specified with the request parameters\n", "- As you browse the map, the map is sending XYZ tile requests to titiler-pgstac function in AWS\n", "- Each request contains the information titiler-pgstac needs to search for items in the pgstac database and how to construct an image from the items' assets\n", " - `collection_id`: by specifying the collection ID in the request path you are instructing titiler-pgstac to search for items from a specific STAC collection. Unless otherwise specified, pgstac will retrieve the STAC items in descending order by datetime and it will stop returning results when a tile's geometry is completely covered.\n", @@ -247,7 +266,7 @@ "source": [ "The response comes back with an `id` which uniquely identifies this search and a handful of useful links associated with our newly registered search.\n", "\n", - "Now you can browse the results of this search with the `/map` endpoint like you did earlier." + "Now you can browse the results of this search with the `/map.html` endpoint like you did earlier." ] }, { @@ -267,7 +286,7 @@ ")\n", "\n", "IFrame(\n", - " f\"{titiler_pgstac_endpoint}/searches/{search_id}/WebMercatorQuad/map?{urlencode(params, doseq=True)}\",\n", + " f\"{titiler_browser_endpoint}/searches/{search_id}/WebMercatorQuad/map.html?{urlencode(params, doseq=True)}\",\n", " 1200,\n", " 800,\n", ")" @@ -303,6 +322,8 @@ "outputs": [], "source": [ "params = (\n", + " (\"assets\", \"nir\"),\n", + " (\"assets\", \"red\"),\n", " (\"asset_as_band\", \"True\"),\n", " (\"expression\", \"(nir - red) / (nir + red)\"),\n", " (\"colormap_name\", \"viridis\"),\n", @@ -310,7 +331,7 @@ ")\n", "\n", "IFrame(\n", - " f\"{titiler_pgstac_endpoint}/searches/{search_id}/WebMercatorQuad/map?{urlencode(params, doseq=True)}\",\n", + " f\"{titiler_browser_endpoint}/searches/{search_id}/WebMercatorQuad/map.html?{urlencode(params, doseq=True)}\",\n", " 1200,\n", " 800,\n", ")" @@ -366,6 +387,7 @@ " params={\n", " \"url\": cog_href,\n", " },\n", + " timeout=None,\n", ")\n", "\n", "print(json.dumps(cog_info_request.json(), indent=2))" @@ -394,6 +416,7 @@ " \"url\": cog_href,\n", " \"maxsize\": 2048,\n", " },\n", + " timeout=None,\n", ")\n", "\n", "Image(preview_request.content)" @@ -506,7 +529,7 @@ "outputs": [], "source": [ "map_request = httpx.get(\n", - " f\"{titiler_pgstac_endpoint}/external/WebMercatorQuad/map\",\n", + " f\"{titiler_pgstac_endpoint}/external/WebMercatorQuad/map.html\",\n", " params={\n", " \"url\": cog_href,\n", " \"maxsize\": 2048,\n", @@ -517,7 +540,7 @@ "\n", "\n", "IFrame(\n", - " map_request.url,\n", + " str(map_request.url).replace(titiler_pgstac_endpoint, titiler_browser_endpoint),\n", " 1200,\n", " 800,\n", ")" @@ -541,7 +564,7 @@ "outputs": [], "source": [ "map_request = httpx.get(\n", - " f\"{titiler_pgstac_endpoint}/collections/glad-global-forest-change-1.11/WebMercatorQuad/map\",\n", + " f\"{titiler_pgstac_endpoint}/collections/glad-global-forest-change-1.11/WebMercatorQuad/map.html\",\n", " params={\n", " \"assets\": \"lossyear\",\n", " \"colormap\": json.dumps({i: rgb for i, rgb in colormap.items()}),\n", @@ -551,7 +574,7 @@ "\n", "\n", "IFrame(\n", - " map_request.url,\n", + " str(map_request.url).replace(titiler_pgstac_endpoint, titiler_browser_endpoint),\n", " 1200,\n", " 800,\n", ")" diff --git a/docs/05-tipg.ipynb b/docs/05-tipg.ipynb index 800f0e8..b3b9b90 100644 --- a/docs/05-tipg.ipynb +++ b/docs/05-tipg.ipynb @@ -60,6 +60,11 @@ "import httpx\n", "\n", "tipg_endpoint = os.getenv(\"TIPG_API_ENDPOINT\")\n", + "# browser-facing URL for the IFrame/viewer cells (the user's browser can't\n", + "# reach the server-side endpoint above when running on Kubernetes)\n", + "tipg_browser_endpoint = os.getenv(\"TIPG_BROWSER_URL\") or tipg_endpoint.replace(\n", + " \"tipg\", \"localhost\"\n", + ")\n", "\n", "collections_request = httpx.get(f\"{tipg_endpoint}/collections\")\n", "\n", @@ -76,9 +81,9 @@ "- `/collections/{collection_id}/items`: where features can be accessed\n", "- `/collections/{collection_id}/tiles`: list of tile matrix set IDs that are available for tile requests\n", "- `/collections/{collection_id}/tiles/{tileMatrixSetId}`: returns a tilejson for a vector tile layer\n", - "- `/collections/{collection_id}/tiles/{tileMatrixSetId}/viewer`: interactive map of the collection\n", + "- `/collections/{collection_id}/tiles/{tileMatrixSetId}/map.html`: interactive map of the collection\n", "\n", - "The `/items`, `/tiles/{tileMatrixSetId}`, and `/tiles/{tileMatrixSetId}/viewer` endpoints will all accept field filters in the form of `{queryable}={value}` where `queryable` is one of the fields listed in the `/queryables` response for that collection." + "The `/items`, `/tiles/{tileMatrixSetId}`, and `/tiles/{tileMatrixSetId}/map.html` endpoints will all accept field filters in the form of `{queryable}={value}` where `queryable` is one of the fields listed in the `/queryables` response for that collection." ] }, { @@ -261,7 +266,7 @@ " },\n", ")\n", "\n", - "local_url = str(bbox_filtered_request.url).replace(\"tipg\", \"localhost\")\n", + "local_url = str(bbox_filtered_request.url).replace(tipg_endpoint, tipg_browser_endpoint)\n", "\n", "IFrame(\n", " local_url,\n", @@ -285,7 +290,7 @@ "metadata": {}, "outputs": [], "source": [ - "local_tipg_endpoint = tipg_endpoint.replace(\"tipg\", \"localhost\")\n", + "local_tipg_endpoint = tipg_browser_endpoint\n", "IFrame(\n", " f\"{local_tipg_endpoint}/api.html#OGC Features API/items_collections__collectionId__items_get\",\n", " width=1200,\n", @@ -374,11 +379,11 @@ "outputs": [], "source": [ "viewer_request = httpx.get(\n", - " f\"{tipg_endpoint}/collections/{collection_id}/tiles/WebMercatorQuad/viewer\",\n", + " f\"{tipg_endpoint}/collections/{collection_id}/tiles/WebMercatorQuad/map.html\",\n", ")\n", "\n", "IFrame(\n", - " str(viewer_request.url).replace(\"tipg\", \"localhost\"),\n", + " str(viewer_request.url).replace(tipg_endpoint, tipg_browser_endpoint),\n", " width=1200,\n", " height=800,\n", ")" @@ -400,14 +405,14 @@ "outputs": [], "source": [ "filtered_viewer_request = httpx.get(\n", - " f\"{tipg_endpoint}/collections/{collection_id}/tiles/WebMercatorQuad/viewer\",\n", + " f\"{tipg_endpoint}/collections/{collection_id}/tiles/WebMercatorQuad/map.html\",\n", " params={\n", " \"na_l2name\": \"MEDITERRANEAN CALIFORNIA\",\n", " },\n", ")\n", "\n", "IFrame(\n", - " str(filtered_viewer_request.url).replace(\"tipg\", \"localhost\"),\n", + " str(filtered_viewer_request.url).replace(tipg_endpoint, tipg_browser_endpoint),\n", " width=1200,\n", " height=800,\n", ")" diff --git a/docs/06-stac_transactions_auth.ipynb b/docs/06-stac_transactions_auth.ipynb index f0e4dc9..f2e6ee3 100644 --- a/docs/06-stac_transactions_auth.ipynb +++ b/docs/06-stac_transactions_auth.ipynb @@ -32,7 +32,7 @@ "source": [ "## 6.1 Get an access token\n", "\n", - "To write to the API you need to prove who you are. The stack runs a small [mock OIDC server](https://github.com/alukach/mock-oidc-server) that stands in for a real identity provider (like Auth0, Keycloak, or Cognito). You ask it for a token and it hands one back \u2014 no password required, since it is only for local testing.\n", + "To write to the API you need to prove who you are. The stack runs a small [mock OIDC server](https://github.com/alukach/mock-oidc-server) that stands in for a real identity provider (like Auth0, Keycloak, or Cognito). You ask it for a token and it hands one back — no password required, since it is only for local testing.\n", "\n", "The `stac_auth` helper module wraps that exchange so the notebook stays focused on STAC:\n", "\n", @@ -95,7 +95,7 @@ "source": [ "## 6.2 Confirm the transactions extension is enabled\n", "\n", - "The transactions endpoints only exist if the API was deployed with the extension turned on. As with any STAC capability, you can check the `/conformance` response before relying on it \u2014 look for conformance classes containing `transaction`.\n", + "The transactions endpoints only exist if the API was deployed with the extension turned on. As with any STAC capability, you can check the `/conformance` response before relying on it — look for conformance classes containing `transaction`.\n", "\n", "
\n", "Warning: Never enable the transactions extension on a public API without an auth layer. Doing so lets anyone write to your catalog. That is exactly why this deployment sits behind stac-auth-proxy.\n", @@ -130,8 +130,8 @@ "\n", "We `POST` the same collection twice:\n", "\n", - "1. **Without a token** \u2014 the proxy rejects it with `401`/`403`\n", - "2. **With our bearer token** \u2014 the write succeeds with `201`\n", + "1. **Without a token** — the proxy rejects it with `401`/`403`\n", + "2. **With our bearer token** — the write succeeds with `201`\n", "\n", "Then we `GET` the collection back with no token, confirming reads stay public." ] @@ -256,7 +256,9 @@ "updated_item = httpx.put(item_url, headers=write_headers, json=stored_item, timeout=10)\n", "print(f\"PUT item with token -> {updated_item.status_code} (updated)\")\n", "assert updated_item.status_code == 200, updated_item.text\n", - "assert updated_item.json()[\"properties\"][\"description\"] == \"Updated by authenticated PUT\"" + "assert (\n", + " updated_item.json()[\"properties\"][\"description\"] == \"Updated by authenticated PUT\"\n", + ")" ] }, { @@ -298,7 +300,7 @@ "\n", "Finally, delete the temporary collection so we leave the catalog as we found it. Deleting a collection is a write too, so it also needs the token.\n", "\n", - "You have now exercised the full transaction life cycle \u2014 create, read, update, delete \u2014 and seen the auth proxy enforce the read/write boundary at every step." + "You have now exercised the full transaction life cycle — create, read, update, delete — and seen the auth proxy enforce the read/write boundary at every step." ] }, { @@ -311,7 +313,9 @@ "collection_url = f\"{stac_api_endpoint}/collections/{collection_id}\"\n", "\n", "denied_collection_delete = httpx.delete(collection_url, timeout=10)\n", - "print(f\"DELETE collection without token -> {denied_collection_delete.status_code} (rejected)\")\n", + "print(\n", + " f\"DELETE collection without token -> {denied_collection_delete.status_code} (rejected)\"\n", + ")\n", "assert denied_collection_delete.status_code in (401, 403)\n", "\n", "deleted_collection = httpx.delete(collection_url, headers=write_headers, timeout=10)\n", diff --git a/docs/stac_auth.py b/docs/stac_auth.py index eee7c70..27be225 100644 --- a/docs/stac_auth.py +++ b/docs/stac_auth.py @@ -13,9 +13,7 @@ import httpx -_TOKEN_PATTERN = re.compile( - r']*id="token"[^>]*>(.*?)', re.S -) +_TOKEN_PATTERN = re.compile(r']*id="token"[^>]*>(.*?)', re.S) def stac_endpoint() -> str: diff --git a/infrastructure/charts/eoapi-workshop/.gitignore b/infrastructure/charts/eoapi-workshop/.gitignore new file mode 100644 index 0000000..e2cdd2e --- /dev/null +++ b/infrastructure/charts/eoapi-workshop/.gitignore @@ -0,0 +1,6 @@ +# Host-specific install overrides generated by deploy.sh — never committed. +.deploy/ + +# Vendored chart dependencies — rebuilt from Chart.lock by `helm dependency +# build` (deploy.sh runs it). Chart.lock IS tracked; the archives are not. +charts/ diff --git a/infrastructure/charts/eoapi-workshop/.helmignore b/infrastructure/charts/eoapi-workshop/.helmignore new file mode 100644 index 0000000..f6bf03f --- /dev/null +++ b/infrastructure/charts/eoapi-workshop/.helmignore @@ -0,0 +1,13 @@ +# Patterns to ignore when building Helm packages. +.DS_Store +.git/ +.gitignore +*.tmproj +*.swp +*.bak +*.orig +.idea/ +.vscode/ +# Local tooling / generated artifacts — not part of the packaged chart. +deploy.sh +.deploy/ diff --git a/infrastructure/charts/eoapi-workshop/Chart.lock b/infrastructure/charts/eoapi-workshop/Chart.lock new file mode 100644 index 0000000..63780a7 --- /dev/null +++ b/infrastructure/charts/eoapi-workshop/Chart.lock @@ -0,0 +1,9 @@ +dependencies: +- name: eoapi + repository: https://developmentseed.org/eoapi-k8s/ + version: 0.13.1 +- name: stac-manager + repository: https://stac-manager.ds.io/ + version: 1.0.3 +digest: sha256:c266058775f9745e48df835657208d23f5785875904049c880093faa5a7886bd +generated: "2026-07-01T17:27:13.34478+03:00" diff --git a/infrastructure/charts/eoapi-workshop/Chart.yaml b/infrastructure/charts/eoapi-workshop/Chart.yaml new file mode 100644 index 0000000..ea356bc --- /dev/null +++ b/infrastructure/charts/eoapi-workshop/Chart.yaml @@ -0,0 +1,27 @@ +apiVersion: v2 +name: eoapi-workshop +description: Minimal, docker-compose-aligned eoAPI deployment for the workshop (no observability/monitoring stack) +type: application +version: 0.1.0 +# appVersion tracks the eoAPI application shipped by the eoapi dependency below. +appVersion: "6.3.1" +icon: https://eoapi.dev/img/eoAPI.png +home: https://github.com/developmentseed/eoapi-workshop +sources: + - https://github.com/developmentseed/eoapi-workshop + - https://github.com/developmentseed/eoapi-k8s +dependencies: + # Upstream eoAPI chart. Its packaged .tgz vendors its own subcharts + # (postgrescluster, stac-auth-proxy, prometheus, grafana, knative, ...). + # Disabled components are turned off via values; their `condition` flags + # keep them from rendering, so only the workshop services are deployed. + # NOTE: devseed.com/eoapi-k8s/ 301-redirects to developmentseed.org/eoapi-k8s/. + - name: eoapi + version: 0.13.1 + repository: https://developmentseed.org/eoapi-k8s/ + # stac-manager: STAC collection/item editing UI (deployed the EOEPCA way — + # its published chart). Routed at /manager via the passthrough ingress. + - name: stac-manager + version: 1.0.3 + repository: https://stac-manager.ds.io/ + condition: stac-manager.enabled diff --git a/infrastructure/charts/eoapi-workshop/README.md b/infrastructure/charts/eoapi-workshop/README.md new file mode 100644 index 0000000..b0d906f --- /dev/null +++ b/infrastructure/charts/eoapi-workshop/README.md @@ -0,0 +1,158 @@ +# eoapi-workshop Helm chart + +A docker-compose-aligned Helm deployment of [eoAPI](https://eoapi.dev) for the +workshop: an *umbrella* chart over the upstream +[`eoapi`](https://github.com/developmentseed/eoapi-k8s) and +[`stac-manager`](https://github.com/developmentseed/stac-manager) charts, plus +per-participant **JupyterLab** environments — no observability/monitoring stack. + +Every service is served at the **root of its own subdomain** under a wildcard +domain (`*.`, default `eoapi-workshop.ds.io`). + +## What gets deployed + +| Component | Subdomain of `eoapi-workshop.ds.io` | Notes | +|---|---|---| +| STAC API (via stac-auth-proxy) | `stac.` | pgstac + stac-fastapi, fronted by the auth proxy | +| Raster (titiler-pgstac) | `raster.` | | +| Vector (tipg) | `vector.` | serves `features.ecoregions` (loaded by the features-loader Job) | +| STAC Browser | `browser.` | root-serving `radiantearth/stac-browser` | +| STAC Manager (editing UI) | `manager.` | `stac-manager` chart 1.0.3 | +| Mock OIDC server | `mock-oidc.` | test-only auth | +| JupyterLab × N | `lab-01.`…`lab-05.` | one isolated pod + PVC + token each | +| Database (pgstac) | in-cluster only | Crunchy `PostgresCluster` | + +Disabled (unlike upstream `experimental.yaml`): `multidim`, `docServer`, +`eoapi-notifier`, `knative`, `monitoring.*`, `observability.grafana`, autoscaling. + +## Contracts (read first) + +- **Wildcard DNS required** — `*.` must A-record to the ingress + LoadBalancer IP (check: `dig +short stac.eoapi-workshop.ds.io`). +- **Release name and namespace must both be `eoapi`** — the proxy's in-cluster + OIDC URL (`eoapi-mock-oidc-server.eoapi.svc…`) is derived from them. `deploy.sh` + defaults to this. +- **Test-only auth, http by default** — the mock OIDC ships `test-client` / + `test-secret` and reads are public (`DEFAULT_PUBLIC=true`). STAC Manager (and + Browser) *login/editing* needs a secure context, so enable `routing.tls` for + HTTPS; over http the UIs are browse/read-only. Not for production. + +## Prerequisites + +Kubernetes 1.23+ with an **NGINX ingress controller**, the **Crunchy Postgres +Operator (PGO)** (hard requirement — `postgrescluster` only reconciles if PGO/CRDs +are installed), Helm 3.8+, and the wildcard DNS above. `deploy.sh` installs the +two operators for you (unless `SKIP_PREREQS=1`). + +## Deploy + +`deploy.sh` installs prerequisites, generates host overrides (per-subdomain URLs + +a stable per-participant token), installs the release, waits for rollouts, and +verifies end-to-end. Idempotent — tokens/URLs stay stable across re-runs. + +```bash +cd infrastructure/charts/eoapi-workshop +./deploy.sh deploy # prerequisites + chart + verify +./deploy.sh verify # re-run endpoint/auth checks, print Lab URLs +./deploy.sh urls # print participant Lab URLs (+ tokens) +./deploy.sh teardown [--all] # remove release (--all also removes operators) +``` + +Env vars: `BASE_DOMAIN` (default `eoapi-workshop.ds.io`), `SKIP_PREREQS=1`, +`GHCR_USER`+`GHCR_TOKEN` (pull secret for a private image — see +[Participant JupyterLabs](#participant-jupyterlabs)). `RELEASE`/`NAMESPACE` must +stay `eoapi`. + +The pgstac DB is created asynchronously by PGO and seeded with sample STAC data, +so API pods may restart a few times before `Ready` on first install. + +To install without `deploy.sh`: `helm dependency update`, then `helm install eoapi +. -n eoapi --create-namespace` with a `-f` overrides file (generate one for a +non-default domain via `BASE_DOMAIN=… ./deploy.sh overrides`). + +## Routing + +All routing is one Ingress (`templates/subdomain-ingress.yaml`): a host rule per +service, each serving at `/` with no rewrite. The upstream path-based ingress is +off and each app serves at its subdomain root — stac/raster/vector with +`--root-path=`, proxy `ROOT_PATH=""`, browser via the root-serving +`radiantearth/stac-browser`, Labs without `--ServerApp.base_url`. Per-subdomain +URLs default to the workshop domain in `values.yaml`; `deploy.sh` rewrites them for +another `BASE_DOMAIN` via the gitignored `.deploy/overrides.yaml`. + +## Verify + +`./deploy.sh verify` checks every service subdomain, runs the auth test, and prints +the Lab URLs. Manually: + +```bash +kubectl -n eoapi get pods +curl -s http://stac.eoapi-workshop.ds.io/healthz # also raster. / vector. +curl -s http://stac.eoapi-workshop.ds.io/collections # sample items +# UIs: browser. manager. mock-oidc./.well-known/openid-configuration +``` + +## Participant JupyterLabs + +`jupyter.participants` (default `lab-01`…`lab-05`; edit for any N) → one Deployment ++ Service + PVC each at `.`, running the GHCR image +`ghcr.io/developmentseed/eoapi-workshop` (built by +`.github/workflows/publish-workshop-image.yml`). Each Lab gets the eoAPI endpoints ++ DB creds injected (from the `eoapi-pguser-eoapi` PGO secret) and an access token +(`./deploy.sh urls` prints them). + +- **Persistence:** notebooks come fresh from the image (`/home/jovyan/docs`) on + every start, so updates always appear; only `/home/jovyan/work` persists (save + work there — edits to the provided notebooks reset on restart). +- **Private image:** GHCR packages are private by default. Either make the package + public, or pass a pull token — `GHCR_USER= GHCR_TOKEN= + ./deploy.sh deploy` creates the `ghcr-pull` secret and wires it to the default + ServiceAccount before the Labs start. + +## Testing auth + +`stac-auth-proxy` fronts STAC at `stac.`: **GET is public, mutations +need a bearer token** from the mock OIDC server (`jq` required). + +```bash +b=eoapi-workshop.ds.io +curl -s -o/dev/null -w '%{http_code}\n' http://stac.$b/collections # 200 (public read) +curl -s -o/dev/null -w '%{http_code}\n' -X POST http://stac.$b/collections \ + -H 'Content-Type: application/json' -d '{}' # 401 (no token) +TOKEN=$(curl -s http://mock-oidc.$b/ \ + --data-raw 'username=testuser&scopes=openid+stac:read+stac:write' \ + -H 'Accept: application/json' | jq -r .token) +curl -s -o/dev/null -w '%{http_code}\n' -X POST http://stac.$b/collections \ + -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{}' # NOT 401 +``` + +**401 without a token, non-401 with one** = working. If it stays 401, check +`kubectl -n eoapi logs deploy/eoapi-stac-auth-proxy` (usual cause: release/namespace +not `eoapi`). + +## Upgrade / uninstall + +```bash +./deploy.sh deploy # idempotent re-deploy (tokens preserved) +helm uninstall eoapi -n eoapi # or ./deploy.sh teardown +kubectl -n eoapi delete pvc --all # PVCs (DB + Lab work) are retained by design +``` + +## Notebook data + +The workshop notebooks (`docs/00`–`06`) run in the Labs against this deployment: +- `pgstacBootstrap.loadSamples` is **off** — the upstream sample collection + `noaa-emergency-response` is stored without a STAC `type` field and breaks + `pystac_client` (notebook 03). The notebooks create their own STAC data. +- the **features-loader Job** (`featuresLoader.enabled`) loads the NA CEC Level III + Ecoregions into `features.ecoregions`, and tipg is configured with + `TIPG_DB_SCHEMAS=["features","public"]`, so notebook 05 has vector data. + +## Limitations +- **UI login needs TLS** — STAC Manager / Browser OIDC login uses PKCE (needs + HTTPS); over http they're read-only. Enable `routing.tls`. (Browser's + `redirect_uri` also still derives from the apex host upstream.) +- **Capacity** — N always-on Labs at `limit 2 CPU / 4Gi` (default 5 ≈ ≤10 CPU / + 20Gi) + stac-manager's ~4Gi startup build + the backend. Size nodes to N. +- **Not production** — test auth, single 1-replica DB (5Gi), http. For production + use the CDK/AWS stack in [`DEPLOYMENT.md`](../../../DEPLOYMENT.md). diff --git a/infrastructure/charts/eoapi-workshop/deploy.sh b/infrastructure/charts/eoapi-workshop/deploy.sh new file mode 100755 index 0000000..109780e --- /dev/null +++ b/infrastructure/charts/eoapi-workshop/deploy.sh @@ -0,0 +1,230 @@ +#!/usr/bin/env bash +# +# Reproducible deploy for the eoapi-workshop chart (subdomain-per-service). +# +# Every service is served at the root of its own subdomain under a wildcard +# domain: stac. raster. vector. browser. manager. mock-oidc. lab-01..NN. of +# ${BASE_DOMAIN}. A wildcard DNS record `*.${BASE_DOMAIN}` must point at the +# ingress LoadBalancer. +# +# Usage: +# ./deploy.sh deploy # prerequisites + chart + verify (idempotent) +# ./deploy.sh verify # re-run endpoint/auth checks + print Lab URLs +# ./deploy.sh urls # print the participant Lab URLs (with tokens) +# ./deploy.sh overrides # (re)generate + print .deploy/overrides.yaml, no deploy +# ./deploy.sh teardown # remove the release, PVCs and namespace +# ./deploy.sh teardown --all # also remove ingress-nginx + PGO +# +# Env: +# RELEASE Helm release name (default: eoapi) -- see OIDC contract below +# NAMESPACE target namespace (default: eoapi) -- see OIDC contract below +# BASE_DOMAIN wildcard base domain (default: eoapi-workshop.ds.io) +# SKIP_PREREQS=1 skip the ingress-nginx + PGO install +# GHCR_TOKEN token with read:packages → create an imagePullSecret so the +# cluster can pull a PRIVATE workshop image (with GHCR_USER). +# Omit if the GHCR package is public. +# +# !!! OIDC CONTRACT !!! The proxy's OIDC_DISCOVERY_INTERNAL_URL is pinned to the +# Service DNS name eoapi-mock-oidc-server.eoapi.svc.cluster.local, derived from +# RELEASE + NAMESPACE. Both MUST stay "eoapi" or in-cluster OIDC discovery breaks. +set -euo pipefail + +RELEASE="${RELEASE:-eoapi}" +NAMESPACE="${NAMESPACE:-eoapi}" +BASE_DOMAIN="${BASE_DOMAIN:-eoapi-workshop.ds.io}" +CHART_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +OVERRIDES="${CHART_DIR}/.deploy/overrides.yaml" + +log() { printf '\n\033[1;34m==> %s\033[0m\n' "$*" >&2; } + +install_prereqs() { + if [[ "${SKIP_PREREQS:-0}" == "1" ]]; then log "Skipping prerequisites (SKIP_PREREQS=1)"; return; fi + if kubectl get ingressclass nginx >/dev/null 2>&1; then + log "An 'nginx' ingressclass already exists — leaving ingress-nginx untouched" + else + log "Installing NGINX ingress controller" + helm upgrade --install ingress-nginx ingress-nginx \ + --repo https://kubernetes.github.io/ingress-nginx \ + --namespace ingress-nginx --create-namespace --wait --timeout 5m + fi + log "Installing Crunchy Postgres Operator (PGO)" + helm upgrade --install pgo oci://registry.developers.crunchydata.com/crunchydata/pgo \ + --namespace postgres-operator --create-namespace --wait --timeout 5m +} + +# Participant names, read from the rendered chart (single source of truth = values). +participant_names() { + helm template "$RELEASE" "$CHART_DIR" -n "$NAMESPACE" \ + --show-only templates/jupyter.yaml 2>/dev/null \ + | grep -oE "^ name: ${RELEASE}-[a-z0-9-]+" | sed "s/ name: ${RELEASE}-//" | sort -u +} + +# Reuse a participant's token from the existing overrides (idempotent URLs across +# re-deploys); prints nothing and returns 1 if not present. +existing_token() { # + [[ -f "$OVERRIDES" ]] || return 1 + local t; t="$(grep -E "name: $1, token:" "$OVERRIDES" 2>/dev/null | sed -E 's/.*token: "([^"]+)".*/\1/' | head -1)" + [[ -n "$t" ]] && printf '%s' "$t" +} + +# Host-specific overrides — derived, NEVER committed (gitignored .deploy/). +write_overrides() { + mkdir -p "$(dirname "$OVERRIDES")" + local tmp; tmp="$(mktemp)" + { + echo "# Generated by deploy.sh — DO NOT COMMIT. baseDomain=${BASE_DOMAIN}" + echo "routing:" + echo " baseDomain: \"${BASE_DOMAIN}\"" + echo "eoapi:" + echo " browser:" + echo " catalogUrl: \"http://stac.${BASE_DOMAIN}\"" + echo " oidcDiscoveryUrl: \"http://mock-oidc.${BASE_DOMAIN}/.well-known/openid-configuration\"" + # NOTE: stac-auth-proxy OIDC_DISCOVERY_URL is intentionally NOT overridden — + # it must stay the in-cluster URL (the proxy fetches JWKS from that origin; + # an external LB URL hairpins and fails). It is domain-independent. + echo " testing:" + echo " mockOidcServer:" + echo " extraEnv:" # list: restate in full + echo " - name: ISSUER" + echo " value: \"http://mock-oidc.${BASE_DOMAIN}\"" + echo " - name: SCOPES" + echo " value: \"stac:read,stac:write\"" + echo "stac-manager:" + echo " publicUrl: \"http://manager.${BASE_DOMAIN}\"" + echo " stacApi: \"http://stac.${BASE_DOMAIN}\"" + echo " stacBrowser: \"http://browser.${BASE_DOMAIN}\"" + echo " oidc:" + echo " authority: \"http://mock-oidc.${BASE_DOMAIN}\"" + echo "jupyter:" + echo " participants:" + local name tok + while read -r name; do + [[ -n "$name" ]] || continue + tok="$(existing_token "$name" || true)"; [[ -n "$tok" ]] || tok="$(openssl rand -hex 16)" + echo " - { name: ${name}, token: \"${tok}\" }" + done < <(participant_names) + } > "$tmp" + mv "$tmp" "$OVERRIDES" +} + +# Optional: let the cluster pull a PRIVATE workshop image. Set GHCR_TOKEN (a +# token with read:packages) + GHCR_USER to create an imagePullSecret and attach +# it to the namespace's default ServiceAccount (which the Labs use). Must run +# BEFORE the Lab pods are created so the secret is injected at creation time. +# Not needed if the GHCR package is public. +setup_pull_secret() { + if [[ -z "${GHCR_TOKEN:-}" ]]; then + log "No GHCR_TOKEN set — assuming the workshop image is public (skipping pull secret)" + return + fi + log "Creating GHCR pull secret + attaching it to the default ServiceAccount" + kubectl create namespace "$NAMESPACE" --dry-run=client -o yaml | kubectl apply -f - >/dev/null + kubectl -n "$NAMESPACE" create secret docker-registry ghcr-pull \ + --docker-server=ghcr.io --docker-username="${GHCR_USER:-$USER}" --docker-password="$GHCR_TOKEN" \ + --dry-run=client -o yaml | kubectl apply -f - >/dev/null + kubectl -n "$NAMESPACE" patch serviceaccount default \ + -p '{"imagePullSecrets":[{"name":"ghcr-pull"}]}' >/dev/null +} + +deploy_chart() { + log "Building chart dependencies" + # Register the dependency repos so `helm dependency build` can resolve them + # from Chart.lock on a fresh machine (the vendored .tgz are gitignored). + helm repo add eoapi https://developmentseed.org/eoapi-k8s/ --force-update >/dev/null 2>&1 || true + helm repo add stac-manager https://stac-manager.ds.io/ --force-update >/dev/null 2>&1 || true + helm repo update eoapi stac-manager >/dev/null 2>&1 || true + helm dependency build "$CHART_DIR" >/dev/null + log "Writing host overrides for ${BASE_DOMAIN} (tokens preserved across re-deploys)" + write_overrides + echo " overrides: ${OVERRIDES}" + setup_pull_secret # before helm upgrade, so new Lab pods inherit the secret + log "Deploying release '${RELEASE}' in namespace '${NAMESPACE}'" + helm upgrade --install "$RELEASE" "$CHART_DIR" \ + -n "$NAMESPACE" --create-namespace -f "$OVERRIDES" + log "Waiting for deployments (the database is created asynchronously by PGO)" + local d + for d in stac raster vector browser stac-auth-proxy mock-oidc-server stac-manager $(participant_names); do + kubectl -n "$NAMESPACE" rollout status "deploy/${RELEASE}-${d}" --timeout=300s || true + done +} + +# curl a URL until it returns the expected code (nginx warmup can lag). +_expect() { # -> echoes actual code, returns 0 if matched + local url="$1" want="$2" code="" _ + for _ in 1 2 3 4 5 6 7 8; do + code="$(curl -s -o /dev/null -w '%{http_code}' --max-time 15 "$url" || true)" + [[ "$code" == "$want" ]] && break; sleep 3 + done + printf '%s' "$code" +} + +verify() { + local b="$BASE_DOMAIN" ok=1 code + log "Verifying service subdomains at *.$b" + declare -a checks=( + "http://stac.$b/healthz|200|stac" + "http://stac.$b/collections|200|stac collections" + "http://raster.$b/healthz|200|raster" + "http://vector.$b/healthz|200|vector" + "http://browser.$b/|200|browser" + "http://manager.$b/|200|manager" + "http://mock-oidc.$b/.well-known/openid-configuration|200|mock-oidc" + ) + local c url want name + for c in "${checks[@]}"; do + IFS='|' read -r url want name <<<"$c" + code="$(_expect "$url" "$want")" + printf ' %-16s %-52s %s\n' "$name" "$url" "$code" + [[ "$code" == "$want" ]] || ok=0 + done + + log "Verifying auth (expect 401 without a token, non-401 with one)" + local no_tok token with_tok + no_tok="$(curl -s -o /dev/null -w '%{http_code}' --max-time 15 \ + -X POST "http://stac.$b/collections" -H 'Content-Type: application/json' -d '{}' || true)" + token="$(curl -s --max-time 15 "http://mock-oidc.$b/" \ + --data-raw 'username=testuser&scopes=openid+stac:read+stac:write' \ + -H 'Accept: application/json' | sed -n 's/.*"token":"\([^"]*\)".*/\1/p')" + with_tok="$(curl -s -o /dev/null -w '%{http_code}' --max-time 15 \ + -X POST "http://stac.$b/collections" -H "Authorization: Bearer ${token}" \ + -H 'Content-Type: application/json' -d '{}' || true)" + printf ' POST without token: %s with token: %s\n' "$no_tok" "$with_tok" + [[ "$no_tok" == "401" && "$with_tok" != "401" && -n "$with_tok" ]] || ok=0 + + print_urls + if [[ "$ok" == 1 ]]; then log "OK — services reachable and auth enforced."; else log "FAILED — see codes above"; exit 1; fi +} + +print_urls() { + log "Participant JupyterLab URLs" + local name tok + while read -r name; do + [[ -n "$name" ]] || continue + tok="$(existing_token "$name" || true)" + printf ' %-8s http://%s.%s/lab?token=%s\n' "$name" "$name" "$BASE_DOMAIN" "$tok" + done < <(participant_names) + printf ' %-8s http://manager.%s/\n' "manager" "$BASE_DOMAIN" + printf ' %-8s http://browser.%s/\n' "browser" "$BASE_DOMAIN" +} + +teardown() { + log "Uninstalling release '${RELEASE}'" + helm uninstall "$RELEASE" -n "$NAMESPACE" 2>/dev/null || true + kubectl -n "$NAMESPACE" delete pvc --all 2>/dev/null || true + kubectl delete namespace "$NAMESPACE" --timeout=180s 2>/dev/null || true + if [[ "${1:-}" == "--all" ]]; then + log "Removing prerequisites (ingress-nginx + PGO)" + helm uninstall ingress-nginx -n ingress-nginx 2>/dev/null || true + helm uninstall pgo -n postgres-operator 2>/dev/null || true + kubectl delete namespace ingress-nginx postgres-operator --timeout=180s 2>/dev/null || true + fi +} + +case "${1:-deploy}" in + deploy) install_prereqs; deploy_chart; verify ;; + verify) verify ;; + urls) print_urls ;; + overrides) write_overrides; echo "written: ${OVERRIDES}"; cat "$OVERRIDES" ;; + teardown) teardown "${2:-}" ;; + *) echo "Usage: $0 {deploy|verify|urls|teardown [--all]}" >&2; exit 2 ;; +esac diff --git a/infrastructure/charts/eoapi-workshop/templates/NOTES.txt b/infrastructure/charts/eoapi-workshop/templates/NOTES.txt new file mode 100644 index 0000000..f5709cf --- /dev/null +++ b/infrastructure/charts/eoapi-workshop/templates/NOTES.txt @@ -0,0 +1,29 @@ +eoAPI workshop — release {{ .Release.Name }} in namespace {{ .Release.Namespace }}. + +Every service is served at the root of its own subdomain under +*.{{ .Values.routing.baseDomain }} (requires a wildcard DNS record pointing at +the ingress LoadBalancer): + + STAC API http://stac.{{ .Values.routing.baseDomain }} + Raster http://raster.{{ .Values.routing.baseDomain }} + Vector http://vector.{{ .Values.routing.baseDomain }} + Browser http://browser.{{ .Values.routing.baseDomain }} +{{- $sm := index .Values "stac-manager" }} +{{- if and $sm $sm.enabled }} + Manager http://manager.{{ .Values.routing.baseDomain }} +{{- end }} + Mock OIDC http://mock-oidc.{{ .Values.routing.baseDomain }} (test-only auth) + +{{- if .Values.jupyter.enabled }} + +Participant JupyterLabs: +{{- range .Values.jupyter.participants }} + {{ .name }} http://{{ .name }}.{{ $.Values.routing.baseDomain }}/lab{{ if .token }}?token={{ .token }}{{ end }} +{{- end }} +{{- if not (first .Values.jupyter.participants).token }} + (tokens are empty — deploy with ./deploy.sh so each Lab gets an access token) +{{- end }} +{{- end }} + +Verify endpoints + auth and (re)print the Lab URLs: + ./deploy.sh verify diff --git a/infrastructure/charts/eoapi-workshop/templates/features-loader-job.yaml b/infrastructure/charts/eoapi-workshop/templates/features-loader-job.yaml new file mode 100644 index 0000000..f0442d1 --- /dev/null +++ b/infrastructure/charts/eoapi-workshop/templates/features-loader-job.yaml @@ -0,0 +1,122 @@ +{{/* +k8s equivalent of the docker-compose `features-loader` + `stac-loader`: load +the NA CEC Level III Ecoregions shapefile into features.ecoregions (notebook +05 / tipg) and the glad STAC collection from the MAAP STAC into pgstac +(notebook 04 §4.5 / titiler). Runs as a post-install/post-upgrade hook, +idempotently (each container skips if its data is already present). Uses the +PGO superuser secret to CREATE SCHEMA and grants read to all users so tipg can +serve it. Loads into the `eoapi` database (what tipg and pgstac serve). +*/}} +{{- if .Values.featuresLoader.enabled }} +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ .Release.Name }}-features-loader + labels: + app: {{ .Release.Name }}-features-loader + app.kubernetes.io/component: workshop-features-loader + annotations: + helm.sh/hook: post-install,post-upgrade + helm.sh/hook-weight: "10" # after the DB / pgstac bootstrap + helm.sh/hook-delete-policy: before-hook-creation +spec: + backoffLimit: 3 + template: + metadata: + labels: + app: {{ .Release.Name }}-features-loader + spec: + restartPolicy: Never + containers: + - name: features-loader + image: {{ .Values.featuresLoader.image | quote }} + command: + - bash + - -c + - | + set -euo pipefail + apt-get update -qq && apt-get install -y -qq postgresql-client >/dev/null + for _ in $(seq 1 60); do pg_isready -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -d "$PGDATABASE" && break; echo "waiting for database..."; sleep 5; done + if psql -tAc "select 1 from information_schema.tables where table_schema='features' and table_name='ecoregions'" | grep -q 1; then + echo "features.ecoregions already present — skipping load."; exit 0 + fi + psql -v ON_ERROR_STOP=1 -c "CREATE SCHEMA IF NOT EXISTS features;" + ogr2ogr -f PostgreSQL "PG:dbname=$PGDATABASE host=$PGHOST port=$PGPORT user=$PGUSER password=$PGPASSWORD" \ + {{ .Values.featuresLoader.shapefileUrl | quote }} \ + -nln features.ecoregions -t_srs EPSG:4326 \ + -lco GEOMETRY_NAME=geom -lco FID=id -lco PRECISION=NO -nlt PROMOTE_TO_MULTI + psql -v ON_ERROR_STOP=1 -c "GRANT USAGE ON SCHEMA features TO PUBLIC; GRANT SELECT ON ALL TABLES IN SCHEMA features TO PUBLIC;" + echo "features.ecoregions loaded." + env: + # Superuser creds (needed for CREATE SCHEMA); DB is pinned to `eoapi` + # (what tipg queries), not the secret's dbname. + - name: PGDATABASE + value: "eoapi" + - name: PGHOST + valueFrom: { secretKeyRef: { name: {{ .Release.Name }}-pguser-postgres, key: host } } + - name: PGPORT + valueFrom: { secretKeyRef: { name: {{ .Release.Name }}-pguser-postgres, key: port } } + - name: PGUSER + valueFrom: { secretKeyRef: { name: {{ .Release.Name }}-pguser-postgres, key: user } } + - name: PGPASSWORD + valueFrom: { secretKeyRef: { name: {{ .Release.Name }}-pguser-postgres, key: password } } + resources: + {{- toYaml .Values.featuresLoader.resources | nindent 12 }} + - name: stac-loader + image: {{ .Values.featuresLoader.stac.image | quote }} + command: + - bash + - -c + - | + set -euo pipefail + pip install -q "pypgstac[psycopg]=={{ .Values.featuresLoader.stac.pypgstacVersion }}" + python3 - <<'PY' + import json + import sys + import urllib.request + + from pypgstac.db import PgstacDB + from pypgstac.load import Loader, Methods + + SRC = {{ .Values.featuresLoader.stac.source | quote }} + COLLECTION = {{ .Values.featuresLoader.stac.collection | quote }} + LIMIT = {{ .Values.featuresLoader.stac.itemLimit }} + + db = PgstacDB() + if db.query_one( + "select 1 from pgstac.collections where id=%s", [COLLECTION] + ): + print(f"{COLLECTION} already present -- skipping load.") + sys.exit(0) + + urllib.request.urlretrieve( + f"{SRC}/collections/{COLLECTION}", "/tmp/collection.json" + ) + with urllib.request.urlopen( + f"{SRC}/search?collections={COLLECTION}&limit={LIMIT}" + ) as r: + features = json.load(r)["features"] + with open("/tmp/items.ndjson", "w") as f: + for feat in features: + f.write(json.dumps(feat) + "\n") + + loader = Loader(db=db) + loader.load_collections("/tmp/collection.json", Methods.upsert) + loader.load_items("/tmp/items.ndjson", Methods.upsert) + print(f"{COLLECTION}: collection + {len(features)} items loaded.") + PY + env: + - name: PGDATABASE + value: "eoapi" + - name: PGHOST + valueFrom: { secretKeyRef: { name: {{ .Release.Name }}-pguser-postgres, key: host } } + - name: PGPORT + valueFrom: { secretKeyRef: { name: {{ .Release.Name }}-pguser-postgres, key: port } } + - name: PGUSER + valueFrom: { secretKeyRef: { name: {{ .Release.Name }}-pguser-postgres, key: user } } + - name: PGPASSWORD + valueFrom: { secretKeyRef: { name: {{ .Release.Name }}-pguser-postgres, key: password } } + resources: + requests: { cpu: "100m", memory: "256Mi" } + limits: { cpu: "500m", memory: "1Gi" } +{{- end }} diff --git a/infrastructure/charts/eoapi-workshop/templates/jupyter.yaml b/infrastructure/charts/eoapi-workshop/templates/jupyter.yaml new file mode 100644 index 0000000..ea8ace4 --- /dev/null +++ b/infrastructure/charts/eoapi-workshop/templates/jupyter.yaml @@ -0,0 +1,139 @@ +{{/* +Per-participant JupyterLab environments (one isolated Deployment+Service+PVC +each), served at the ROOT of . and routed by +templates/subdomain-ingress.yaml (no path prefix, no base_url). Notebooks come +from the image; only /home/jovyan/work is persisted. Endpoints + DB creds are +injected to mirror the docker-compose `jupyterhub` service so the workshop +notebooks run unchanged. +*/}} +{{- if .Values.jupyter.enabled }} +{{- $root := . }} +{{- $j := .Values.jupyter }} +{{- $base := .Values.routing.baseDomain }} +{{- range $j.participants }} +{{- $name := .name }} +{{- $token := .token | default "" }} +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ $root.Release.Name }}-{{ $name }} + labels: + app: {{ $root.Release.Name }}-{{ $name }} + app.kubernetes.io/component: workshop-jupyterlab +spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: {{ $j.storage.size | quote }} +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ $root.Release.Name }}-{{ $name }} + labels: + app: {{ $root.Release.Name }}-{{ $name }} + app.kubernetes.io/component: workshop-jupyterlab +spec: + selector: + app: {{ $root.Release.Name }}-{{ $name }} + ports: + - name: http + port: 8888 + targetPort: 8888 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ $root.Release.Name }}-{{ $name }} + labels: + app: {{ $root.Release.Name }}-{{ $name }} + app.kubernetes.io/component: workshop-jupyterlab +spec: + replicas: 1 + strategy: + type: Recreate # RWO PVC: the new pod can't mount until the old one releases it + selector: + matchLabels: + app: {{ $root.Release.Name }}-{{ $name }} + template: + metadata: + labels: + app: {{ $root.Release.Name }}-{{ $name }} + spec: + securityContext: + fsGroup: 1000 # jovyan GID — makes the work PVC writable by the notebook user + # Notebooks live in the image at /home/jovyan/docs and come FRESH on every + # start, so image/notebook updates always appear. Only /home/jovyan/work is + # a persistent PVC — no seed initContainer, no home shadowing. (Trade-off: + # edits to the provided notebooks reset on pod restart; save work under work/.) + containers: + - name: jupyterlab + image: "{{ $j.image.repository }}:{{ $j.image.tag }}" + imagePullPolicy: {{ $j.image.pullPolicy }} + # args only (NO command): keep the image ENTRYPOINT (/entrypoint.sh + # activates the conda env) and just override the launch command. + # Served at the root of . — no base_url prefix. + args: + - jupyter + - lab + - --ServerApp.token={{ $token }} + - --ip=0.0.0.0 + - --port=8888 + - --no-browser + ports: + - name: http + containerPort: 8888 + env: + # eoAPI endpoints (in-cluster) — mirrors the docker-compose jupyterhub service. + - { name: STAC_API_ENDPOINT, value: "http://{{ $root.Release.Name }}-stac-auth-proxy:8080" } + - { name: STAC_AUTH_PROXY_ENDPOINT, value: "http://{{ $root.Release.Name }}-stac-auth-proxy:8080" } + - { name: TITILER_PGSTAC_API_ENDPOINT, value: "http://{{ $root.Release.Name }}-raster:8080" } + - { name: TIPG_API_ENDPOINT, value: "http://{{ $root.Release.Name }}-vector:8080" } + - { name: MOCK_OIDC_ENDPOINT, value: "http://{{ $root.Release.Name }}-mock-oidc-server:8080" } + - { name: STAC_BROWSER_ENDPOINT, value: "http://browser.{{ $base }}" } + # Browser-facing API URLs (external subdomains) — the notebooks' IFrame/ + # viewer cells hand these to the user's browser, which cannot reach the + # in-cluster Service DNS above. Server-side httpx calls keep the *_ENDPOINT + # vars; absent these (compose/2i2c) the notebooks fall back to .replace(). + - { name: STAC_API_BROWSER_URL, value: "http://stac.{{ $base }}" } + - { name: TITILER_BROWSER_URL, value: "http://raster.{{ $base }}" } + - { name: TIPG_BROWSER_URL, value: "http://vector.{{ $base }}" } + # DB creds from the PGO-generated secret, using the DIRECT primary keys + # (host/port) — NOT pgbouncer-*, whose transaction pooling breaks the + # DDL/COPY in 02-database.ipynb. Secret name is release-derived + # (release/namespace contract: both must be "eoapi"). + - name: PGHOST + valueFrom: { secretKeyRef: { name: {{ $root.Release.Name }}-pguser-eoapi, key: host } } + - name: PGPORT + valueFrom: { secretKeyRef: { name: {{ $root.Release.Name }}-pguser-eoapi, key: port } } + - name: PGDATABASE + valueFrom: { secretKeyRef: { name: {{ $root.Release.Name }}-pguser-eoapi, key: dbname } } + - name: PGUSER + valueFrom: { secretKeyRef: { name: {{ $root.Release.Name }}-pguser-eoapi, key: user } } + - name: PGPASSWORD + valueFrom: { secretKeyRef: { name: {{ $root.Release.Name }}-pguser-eoapi, key: password } } + volumeMounts: + - name: work + mountPath: /home/jovyan/work + # JupyterLab has no unauthenticated HTTP health endpoint → probe TCP. + # Generous startupProbe so the first (large) image pull doesn't trip liveness. + startupProbe: + tcpSocket: { port: 8888 } + periodSeconds: 10 + failureThreshold: 30 + readinessProbe: + tcpSocket: { port: 8888 } + periodSeconds: 10 + livenessProbe: + tcpSocket: { port: 8888 } + periodSeconds: 30 + failureThreshold: 3 + resources: + {{- toYaml $j.resources | nindent 12 }} + volumes: + - name: work + persistentVolumeClaim: + claimName: {{ $root.Release.Name }}-{{ $name }} +{{- end }} +{{- end }} diff --git a/infrastructure/charts/eoapi-workshop/templates/subdomain-ingress.yaml b/infrastructure/charts/eoapi-workshop/templates/subdomain-ingress.yaml new file mode 100644 index 0000000..56b2aa7 --- /dev/null +++ b/infrastructure/charts/eoapi-workshop/templates/subdomain-ingress.yaml @@ -0,0 +1,69 @@ +{{/* +Subdomain-per-service ingress. Each enabled service is exposed at +