Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

permissions: {}

jobs:
quality:
name: Quality checks
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false

- name: Install uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
version: "0.11.*"
enable-cache: false

- name: Set up Python
run: uv python install 3.14

- name: Run pre-commit
run: uv run pre-commit run --all-files

test:
name: Test Python ${{ matrix.python-version }}
runs-on: ubuntu-latest
permissions:
contents: read
strategy:
matrix:
python-version: ["3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false

- name: Install uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
version: "0.11.*"
enable-cache: false

- name: Set up Python
run: uv python install ${{ matrix.python-version }}

- name: Run tests
run: uv run pytest
23 changes: 23 additions & 0 deletions .github/workflows/conventional-commits-prs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: PR Conventional Commit Validation

# NOTE: pull_request_target runs in the base repo context with access to secrets.
# Do NOT checkout untrusted code in this workflow.
on:
# zizmor: ignore[dangerous-triggers]
# Safe: this workflow does not check out any code; it only validates
# the PR title via the GitHub API.
pull_request_target:
types: [opened, synchronize, reopened, edited]

permissions: {}

jobs:
validate-pr-title:
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- name: PR Conventional Commit Validation
uses: ytanikin/pr-conventional-commits@639145d78959c53c43112365837e3abd21ed67c1 # 1.5.2
with:
task_types: '["feat","fix","docs","test","ci","refactor","perf","chore","revert"]'
24 changes: 24 additions & 0 deletions .github/workflows/release-please.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: Release Please

on:
push:
branches: [main]

permissions:
contents: write
pull-requests: write
issues: write

concurrency:
group: release-please-${{ github.ref }}
cancel-in-progress: false

jobs:
release-please:
name: Create release PR or GitHub release
runs-on: ubuntu-latest
steps:
- name: Run release-please
uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
3 changes: 3 additions & 0 deletions .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
".": "0.1.0"
}
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Changelog

## [0.1.0] - 2025-12-12

Initial release of the MAAP DPS algorithm for generating STAC item metadata for existing Cloud Optimized GeoTIFF assets in object storage.

### Added

- Added the `GenerateCogStacItems` DPS algorithm at version `v0.1`.
- Added direct Python invocation through `main.py` for local STAC item generation.
- Added object storage listing for source locations such as S3 paths.
- Added filtering for GeoTIFF assets before STAC item creation.
- Added STAC item generation with `rio-stac` and catalog output with `pystac`.
- Added projection and raster metadata handling for generated STAC items.
- Added MAAP DPS wrapper scripts, including positional and named-argument execution paths.
- Added support for direct bucket access and MAAP-compatible cloud raster defaults.

### Fixed

- Improved generated STAC item IDs.
- Fixed Fmask nodata handling.
- Fixed bbox serialization.
- Set missing `proj:transform` and `proj:shape` values when available.

### Documentation

- Documented local usage with `uv run main.py`.
- Documented required inputs and generated STAC catalog outputs.
44 changes: 32 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
# DPS Import COGs
# DPS STAC Item Generator

Create STAC items for raster files in object storage
Create STAC items for selected files in object storage.

## About

This DPS algorithm creates STAC metadata for existing raster files (GeoTIFFs) stored in object storage (S3, Azure, GCS, etc.). The workflow:
This DPS algorithm creates STAC metadata for existing files stored in object storage (S3, Azure, GCS, etc.). The workflow:

1. Lists all files at the specified source location
2. Filters for `.tif` files
3. Creates a STAC item for each raster using `rio-stac`
2. Filters files by configurable include/exclude extension lists
3. Creates a STAC item for each matching asset using `rio-stac`
4. Exports a self-contained STAC catalog with all items

This tool is useful for importing existing COG (Cloud Optimized GeoTIFF) datasets into STAC format for better discoverability and interoperability.
By default, the algorithm includes GeoTIFF (`.tif`, `.tiff`) and NetCDF (`.nc`) files. This tool is useful for importing existing geospatial datasets into STAC format for better discoverability and interoperability.
The STAC items will be uploaded to the DPS User STAC in a collection associated with your username, the algorithm name/version, and the job tag.

## Usage
Expand All @@ -27,7 +27,7 @@ from maap.maap import MAAP
maap = MAAP(maap_host="api.maap-project.org")

job = maap.submitJob(
algo_id="GenerateCogStacItems",
algo_id="GenerateStacItems",
version="v0.1",
identifier="test-run",
queue="maap-dps-worker-8gb",
Expand All @@ -36,9 +36,9 @@ job = maap.submitJob(

```

Each job will produce a STAC item for each .tif file that exists under the provided `source`. The STAC items will be uploaded to the DPS User STAC catalog automatically after job completion. All jobs associated with the same algorithm, version, username, and job tag/identifier will be added to the same collection with the following format:
Each job will produce a STAC item for each file under the provided `source` whose extension matches the configured filters. The STAC items will be uploaded to the DPS User STAC catalog automatically after job completion. All jobs associated with the same algorithm, version, username, and job tag/identifier will be added to the same collection with the following format:

`{username}__GenerateCogStacItems__v0.1__{identifier}`
`{username}__GenerateStacItems__v0.1__{identifier}`


You can access the items following this pattern:
Expand All @@ -62,19 +62,39 @@ To customize the visualization, you can add all of the familiar visualization pa
uv run main.py \
--source "s3://bucket/path/to/files/" \
--output_dir "/tmp/output" \
--region "us-west-2"
--region "us-west-2" \
--include-extensions ".tif,.tiff,.nc" \
--exclude-extensions ""
```

### Local format smoke test

To generate local fixtures in `/tmp` and run the generator against them:

```bash
uv run python scripts/local_format_smoke_test.py
```

The script creates tiny GeoTIFF, COG, NetCDF, HDF5, JPEG 2000, PNG, and JPEG fixtures when the required GDAL/HDF5 command-line tools are available. It logs the `/tmp/dps-stac-local-*` input and output directories when it finishes.

To exercise the DPS wrapper instead of calling `main.py` directly:

```bash
uv run python scripts/local_format_smoke_test.py --runner run-sh
```

### Parameters

- `--source`: Source location of the files for which you want to generate STAC items (e.g., `s3://bucket/path/to/files/`)
- `--output_dir`: Directory where the STAC catalog will be saved
- `--region`: AWS region where the storage container exists (default: `us-west-2`)
- `--include-extensions`: Comma-separated extensions to include (default: `.tif,.tiff,.nc`). Use an empty string to include all files.
- `--exclude-extensions`: Comma-separated extensions to exclude. Exclusions override inclusions.

## Output

The tool generates a self-contained STAC catalog in the output directory containing:

- A `catalog.json` file with metadata about the collection
- Individual STAC item JSON files for each raster
- Each STAC item includes projection (`proj`) and raster band information
- Individual STAC item JSON files for each matching asset
- Each STAC item includes projection (`proj`) and raster band information when `rio-stac` can derive it from the source asset
14 changes: 11 additions & 3 deletions algorithm-config.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
algorithm_description: Generate STAC item metadata for COGs in cloud storage
algorithm_name: GenerateCogStacItems
algorithm_description: Generate STAC item metadata for selected files in cloud storage
algorithm_name: GenerateStacItems
algorithm_version: v0.1
build_command: dps-import-cogs/build-env.sh
disk_space: 8GB
Expand All @@ -9,13 +9,21 @@ inputs:
file: []
positional:
- default: ''
description: ' source location for the COGs in object storage, e.g. s3://bucket/path/to/folder/'
description: ' source location for the files in object storage, e.g. s3://bucket/path/to/folder/'
name: source
required: true
- default: ''
description: ' region in which the storage container exists'
name: region
required: false
- default: '.tif,.tiff,.nc'
description: ' comma-separated file extensions to include, e.g. .tif,.tiff,.nc. Leave empty to include all files.'
name: include_extensions
required: false
- default: ''
description: ' comma-separated file extensions to exclude. Exclusions override inclusions.'
name: exclude_extensions
required: false
queue: maap-dps-worker-8gb
repository_url: https://github.com/MAAP-Project/dps-import-cogs
run_command: dps-import-cogs/run.sh
86 changes: 3 additions & 83 deletions main.py
Original file line number Diff line number Diff line change
@@ -1,87 +1,7 @@
"""Create STAC items for all raster files in object storage that match a prefix"""
"""Compatibility entry point for the DPS STAC item generator."""

import argparse
import logging
import os
from pathlib import Path

import obstore
from obstore.store import from_url
from pystac import Catalog, CatalogType, MediaType
from rio_cogeo.cogeo import cog_validate
from rio_stac import create_stac_item

logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(name)s - %(message)s"
)
logging.getLogger("botocore").setLevel(logging.WARNING)
logger = logging.getLogger(__name__)

DEFAULT_REGION = "us-west-2"


def run(
source: str,
output_dir: Path,
region: str | None = None,
):
source_store = from_url(source, region=region or DEFAULT_REGION)

catalog = Catalog(
id="DPS",
description="DPS",
catalog_type=CatalogType.SELF_CONTAINED,
)

stream = obstore.list(source_store)
for batch in stream:
for obj in batch:
obj_key = os.path.join(source, obj["path"])
if not obj_key.endswith(".tif"):
logging.info(f"skipping {obj_key} because it is not a .tif")
continue

logging.info(f"processing {obj_key}")
is_cog = cog_validate(obj_key)

item = create_stac_item(
source=obj_key,
id=obj["path"].replace(".tif", ""),
with_proj=True,
with_raster=True,
asset_media_type=MediaType.COG if is_cog else MediaType.GEOTIFF,
)

catalog.add_item(item)

catalog.normalize_and_save(
root_href=str(output_dir),
catalog_type=CatalogType.SELF_CONTAINED,
)
from dps_stac_item_generator.cli import main


if __name__ == "__main__":
parse = argparse.ArgumentParser(
description="Queries the HLS STAC geoparquet archive and writes the result to a file"
)
parse.add_argument(
"--source",
help="Source location of the files for which you want to generate STAC items."
" e.g. 's3://bucket/path/to/files/'",
required=True,
)
parse.add_argument(
"--output_dir", help="Directory in which to save output", required=True
)
parse.add_argument(
"--region",
help="region in which the storage container exists. e.g. 'us-west-2'",
default="us-west-2",
)
args = parse.parse_args()

output_dir = Path(args.output_dir)
run(
source=args.source,
output_dir=output_dir,
)
main()
22 changes: 20 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"

[project]
name = "dps-import-cogs"
description = "DPS algorithm for generating STAC item metadata for COGs from external sources"
name = "dps-stac-item-generator"
description = "DPS algorithm for generating STAC item metadata for selected files from external sources"
readme = "README.md"
requires-python = ">=3.12"
dynamic = ["version"]
Expand All @@ -12,18 +16,32 @@ dependencies = [
"rio-stac>=0.10.1",
]

[project.scripts]
dps-stac-item-generator = "dps_stac_item_generator.cli:main"

[dependency-groups]
dev = [
"ipython>=9.8.0",
"maap-py>=4.2.0",
"mypy>=1.18.2",
"pre-commit>=4.3.0",
"pytest>=9.1.1",
"ruff>=0.14.3",
]

[tool.pdm.version]
source = "scm"

[tool.setuptools.packages.find]
where = ["src"]

[tool.mypy]
ignore_missing_imports = true

[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-vv"

[tool.ruff]

[tool.ruff.format]
Loading