├── conftest.py ├── stac_check ├── __init__.py ├── logo.py ├── stac-check.config.yml ├── utilities.py ├── cli.py └── api_lint.py ├── MANIFEST.in ├── .env-example ├── docs ├── _static │ ├── custom.css │ ├── stac-check.png │ └── radiant-earth.webp ├── requirements.txt ├── api.rst ├── cli.rst ├── index.rst ├── Makefile ├── make.bat └── conf.py ├── assets ├── stac-check.png └── radiant-earth.webp ├── .readthedocs.yml ├── Dockerfile ├── sample_files ├── 1.0.0 │ ├── invalid-antimeridian-bbox.json │ ├── catalog-with-bad-item.json │ ├── catalog.json │ ├── collection-no-summaries.json │ ├── bad-item.json │ ├── collection-no-title.json │ ├── collection.json │ ├── core-item-unlocated-null-bbox.json │ ├── core-item-unlocated.json │ ├── 20201211_223832_cs2.json │ ├── core-item.json │ ├── core-item-bad-links.json │ ├── core-item-invalid-id.json │ ├── core-item-large-thumbnail.json │ ├── core-item-null-datetime.json │ ├── collectionless-item.json │ ├── catalog_many_links.json │ ├── core-item-bloated.json │ └── feature_collection.json ├── 1.1.0 │ ├── simple-item.json │ ├── collection.json │ └── extended-item.json └── 0.9.0 │ ├── bad-item.json │ └── landsat8-sample.json ├── Makefile ├── .github └── workflows │ ├── publish.yml │ ├── test-runner.yml │ └── docs.yml ├── .pre-commit-config.yaml ├── LICENSE ├── tests ├── test_config.py ├── test.config.yml ├── test_lint_pydantic.py ├── test_lint_assets.py ├── test_lint.py ├── test_lint_dictionary.py ├── test_cli.py ├── test_lint_recursion.py ├── test_lint_stac_api.py └── test_lint_geometry.py ├── setup.py ├── .gitignore └── CHANGELOG.md /conftest.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /stac_check/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include stac_check/stac-check.config.yml -------------------------------------------------------------------------------- /.env-example: -------------------------------------------------------------------------------- 1 | STAC_CHECK_CONFIG="stac_check/stac-check.config.yml" -------------------------------------------------------------------------------- /docs/_static/custom.css: -------------------------------------------------------------------------------- 1 | .wy-nav-content { 2 | max-width: 75% !important; 3 | } -------------------------------------------------------------------------------- /assets/stac-check.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stac-utils/stac-check/HEAD/assets/stac-check.png -------------------------------------------------------------------------------- /assets/radiant-earth.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stac-utils/stac-check/HEAD/assets/radiant-earth.webp -------------------------------------------------------------------------------- /docs/_static/stac-check.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stac-utils/stac-check/HEAD/docs/_static/stac-check.png -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # Build documentation in the docs/ directory with Sphinx 2 | sphinx: 3 | configuration: docs/conf.py -------------------------------------------------------------------------------- /docs/_static/radiant-earth.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stac-utils/stac-check/HEAD/docs/_static/radiant-earth.webp -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx>=8.2.3 2 | sphinx_rtd_theme>=3.0.2 3 | myst-parser>=4.0.1 4 | sphinx-autodoc-typehints>=3.2.0 -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | API Reference 2 | ============ 3 | 4 | .. automodule:: stac_check.lint 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/cli.rst: -------------------------------------------------------------------------------- 1 | CLI Reference 2 | ============ 3 | 4 | .. automodule:: stac_check.cli 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | .. click:: stac_check.cli:main 10 | :prog: stac-check 11 | :nested: full 12 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9-slim 2 | 3 | WORKDIR /app 4 | 5 | # Install build tools and dependencies 6 | RUN apt-get update && \ 7 | apt-get install -y --no-install-recommends \ 8 | make \ 9 | build-essential \ 10 | && rm -rf /var/lib/apt/lists/* 11 | 12 | COPY . . 13 | 14 | # Install the package in development mode with dev and docs extras 15 | RUN pip install -e ".[dev,docs]" 16 | 17 | CMD ["stac_check"] -------------------------------------------------------------------------------- /stac_check/logo.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | 3 | logo = r""" 4 | ________________________________________________________ 5 | ____ ____ __ ___ ___ _ _ ____ ___ __ _ 6 | / ___)(_ _)/ _\ / __)___ / __)/ )( \( __)/ __)( / ) 7 | \___ \ )( / \( (__(___)( (__ ) __ ( ) _)( (__ ) ( 8 | (____/ (__)\_/\_/ \___) \___)\_)(_/(____)\___)(__\_) 9 | ________________________________________________________ 10 | """ 11 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | ``stac-check`` documentation 2 | ############################ 3 | 4 | .. include:: ../README.md 5 | :parser: myst_parser.sphinx_ 6 | :start-after: # stac-check 7 | :end-before: ## License 8 | 9 | For more detailed documentation, please see the following pages: 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | :caption: Contents: 14 | 15 | cli 16 | api 17 | 18 | Indices and tables 19 | ================== 20 | 21 | * :ref:`genindex` 22 | * :ref:`modindex` 23 | * :ref:`search` 24 | -------------------------------------------------------------------------------- /sample_files/1.0.0/invalid-antimeridian-bbox.json: -------------------------------------------------------------------------------- 1 | { 2 | "stac_version": "1.0.0", 3 | "type": "Feature", 4 | "id": "invalid-antimeridian-bbox", 5 | "bbox": [-170, -10, 170, 10], 6 | "geometry": { 7 | "type": "Polygon", 8 | "coordinates": [ 9 | [ 10 | [-170, -10], 11 | [170, -10], 12 | [170, 10], 13 | [-170, 10], 14 | [-170, -10] 15 | ] 16 | ] 17 | }, 18 | "properties": { 19 | "datetime": "2023-01-01T00:00:00Z" 20 | }, 21 | "links": [], 22 | "assets": {} 23 | } -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build 2 | build: 3 | docker build \ 4 | -t stac_check . 5 | 6 | run_docker = docker run -it --rm \ 7 | stac_check 8 | 9 | .PHONY: shell 10 | shell: 11 | $(run_docker) /bin/bash 12 | 13 | .PHONY: docs 14 | docs: ## Build documentation locally 15 | pip install -e ".[docs]" 16 | sphinx-build -b html -E docs/ docs/_build/html 17 | @echo "Documentation built in docs/_build/html" 18 | 19 | .PHONY: docker-docs 20 | docker-docs: ## Build documentation inside Docker container 21 | docker build -t stac_check . 22 | docker run --rm -v $(PWD)/docs/_build:/app/docs/_build stac_check sphinx-build -b html -E docs/ docs/_build/html 23 | @echo "Documentation built in docs/_build/html" 24 | @echo "Docker documentation build complete." -------------------------------------------------------------------------------- /sample_files/1.0.0/catalog-with-bad-item.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "examples", 3 | "type": "Catalog", 4 | "title": "Example Catalog", 5 | "stac_version": "1.0.0", 6 | "description": "This is a valid catalog with links to one valid item and one invalid item.", 7 | "links": [ 8 | { 9 | "rel": "root", 10 | "href": "./catalog.json", 11 | "type": "application/json" 12 | }, 13 | { 14 | "rel": "item", 15 | "href": "./bad-item.json", 16 | "type": "application/json", 17 | "title": "Invalid item" 18 | }, 19 | { 20 | "rel": "item", 21 | "href": "./collectionless-item.json", 22 | "type": "application/json", 23 | "title": "Collection with no items (standalone)" 24 | }, 25 | { 26 | "rel": "self", 27 | "href": "https://raw.githubusercontent.com/radiantearth/stac-spec/v1.0.0/examples/catalog.json", 28 | "type": "application/json" 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*.*.*" # Triggers when a tag starting with 'v' followed by version numbers is pushed 7 | 8 | jobs: 9 | build-and-publish: 10 | name: Build and Publish to PyPI 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Set up Python 3.10 17 | uses: actions/setup-python@v5 18 | with: 19 | python-version: "3.10" 20 | 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install setuptools wheel twine 25 | 26 | - name: Build package 27 | run: | 28 | python setup.py sdist bdist_wheel 29 | 30 | - name: Publish package to PyPI 31 | env: 32 | TWINE_USERNAME: "__token__" 33 | TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} 34 | run: | 35 | twine upload dist/* 36 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/PyCQA/flake8 3 | rev: 7.3.0 4 | hooks: 5 | - id: flake8 6 | args: 7 | - --ignore=E501,E712,W503 8 | - repo: https://github.com/timothycrosley/isort 9 | rev: 6.0.1 10 | hooks: 11 | - id: isort 12 | args: ["--profile", "black"] 13 | - repo: https://github.com/psf/black 14 | rev: 25.1.0 15 | hooks: 16 | - id: black 17 | language_version: python3.12 18 | - repo: https://github.com/pre-commit/mirrors-mypy 19 | rev: v1.17.0 20 | hooks: 21 | - id: mypy 22 | exclude: /tests/ 23 | # --strict 24 | args: 25 | [ 26 | --no-strict-optional, 27 | --ignore-missing-imports, 28 | --implicit-reexport, 29 | --explicit-package-bases, 30 | ] 31 | additional_dependencies: 32 | ["types-attrs", "types-requests", "types-setuptools", "types-PyYAML"] 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Jonathan Healy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/test_config.py: -------------------------------------------------------------------------------- 1 | from stac_check.lint import Linter 2 | 3 | 4 | def test_linter_config_file(): 5 | file = "sample_files/1.0.0/core-item.json" 6 | linter = Linter(file) 7 | 8 | # Use defualt config 9 | assert linter.config["linting"]["searchable_identifiers"] == True 10 | assert linter.create_best_practices_dict()["searchable_identifiers"] == [ 11 | f"Item name '{linter.object_id}' should only contain Searchable identifiers", 12 | "Identifiers should consist of only lowercase characters, numbers, '_', and '-'", 13 | ] 14 | 15 | # Load config file 16 | linter = Linter(file, config_file="tests/test.config.yml") 17 | 18 | assert linter.config["linting"]["searchable_identifiers"] == True 19 | # Since searchable_identifiers is True, the error should be in the best practices dict 20 | assert "searchable_identifiers" in linter.create_best_practices_dict() 21 | 22 | 23 | def test_linter_max_links(): 24 | file = "sample_files/1.0.0/core-item-bloated.json" 25 | linter = Linter(file) 26 | 27 | assert linter.check_bloated_links() == True 28 | assert len(linter.data["links"]) > 20 29 | 30 | # Load config file 31 | linter = Linter(file, config_file="tests/test.config.yml") 32 | # Since bloated_links is True in the config and the file has more links than max_links, 33 | # bloated_links should be in the best practices dict 34 | assert "bloated_links" in linter.create_best_practices_dict() 35 | -------------------------------------------------------------------------------- /sample_files/1.0.0/catalog.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "examples", 3 | "type": "Catalog", 4 | "title": "Example Catalog", 5 | "stac_version": "1.0.0", 6 | "description": "This catalog is a simple demonstration of an example catalog that is used to organize a hierarchy of collections and their items.", 7 | "links": [ 8 | { 9 | "rel": "root", 10 | "href": "./catalog.json", 11 | "type": "application/json" 12 | }, 13 | { 14 | "rel": "child", 15 | "href": "./extensions-collection/collection.json", 16 | "type": "application/json", 17 | "title": "Collection Demonstrating STAC Extensions" 18 | }, 19 | { 20 | "rel": "child", 21 | "href": "./collection-only/collection.json", 22 | "type": "application/json", 23 | "title": "Collection with no items (standalone)" 24 | }, 25 | { 26 | "rel": "child", 27 | "href": "./collection-only/collection-with-schemas.json", 28 | "type": "application/json", 29 | "title": "Collection with no items (standalone with JSON Schemas)" 30 | }, 31 | { 32 | "rel": "item", 33 | "href": "./collectionless-item.json", 34 | "type": "application/json", 35 | "title": "Collection with no items (standalone)" 36 | }, 37 | { 38 | "rel": "self", 39 | "href": "https://raw.githubusercontent.com/radiantearth/stac-spec/v1.0.0/examples/catalog.json", 40 | "type": "application/json" 41 | } 42 | ] 43 | } -------------------------------------------------------------------------------- /.github/workflows/test-runner.yml: -------------------------------------------------------------------------------- 1 | name: Test Runner 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - dev 8 | pull_request: 9 | branches: 10 | - main 11 | - dev 12 | 13 | jobs: 14 | pre-commit: 15 | name: Run pre-commit checks 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | 21 | - name: Set up Python 3.12 22 | uses: actions/setup-python@v5 23 | with: 24 | python-version: "3.12" 25 | 26 | - name: Install dependencies 27 | run: | 28 | python -m pip install --upgrade pip 29 | pip install '.[dev]' 30 | 31 | - name: Run pre-commit checks 32 | uses: pre-commit/action@v3.0.1 33 | with: 34 | extra_args: --all-files 35 | env: 36 | PRE_COMMIT_HOME: ~/.cache/pre-commit 37 | 38 | test: 39 | needs: pre-commit # This ensures tests run after pre-commit checks 40 | name: Execute tests 41 | runs-on: ubuntu-latest 42 | strategy: 43 | matrix: 44 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] 45 | 46 | steps: 47 | - uses: actions/checkout@v4 48 | 49 | - name: Set up Python ${{ matrix.python-version }} 50 | uses: actions/setup-python@v5 51 | with: 52 | python-version: ${{ matrix.python-version }} 53 | 54 | - name: Install dependencies 55 | run: | 56 | python -m pip install --upgrade pip 57 | pip install '.[dev]' 58 | 59 | - name: Run unit tests 60 | run: pytest -v 61 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """stac-check setup.py""" 2 | 3 | from setuptools import find_packages, setup 4 | 5 | __version__ = "1.11.1" 6 | 7 | with open("README.md", "r") as fh: 8 | long_description = fh.read() 9 | 10 | setup( 11 | name="stac_check", 12 | version=__version__, 13 | description="Linting and validation tool for STAC assets", 14 | url="https://github.com/stac-utils/stac-check", 15 | packages=find_packages(exclude=("tests",)), 16 | include_package_data=True, 17 | setup_requires=["setuptools"], 18 | install_requires=[ 19 | "requests>=2.32.4", 20 | "jsonschema>=4.25.0", 21 | "click>=8.1.8", 22 | "stac-validator~=3.10.1", 23 | "PyYAML", 24 | "python-dotenv", 25 | ], 26 | extras_require={ 27 | "dev": [ 28 | "pytest", 29 | "requests-mock", 30 | "types-setuptools", 31 | "stac-validator[pydantic]~=3.10.1", 32 | ], 33 | "docs": [ 34 | "sphinx>=8.2.3", 35 | "sphinx-click>=6.0.0", 36 | "sphinx_rtd_theme>=3.0.2", 37 | "myst-parser>=4.0.1", 38 | "sphinx-autodoc-typehints>=3.2.0", 39 | ], 40 | "pydantic": ["stac-validator[pydantic]~=3.10.1"], 41 | }, 42 | entry_points={"console_scripts": ["stac-check=stac_check.cli:main"]}, 43 | author="Jonathan Healy", 44 | author_email="jonathan.d.healy@gmail.com", 45 | license="MIT", 46 | long_description=long_description, 47 | long_description_content_type="text/markdown", 48 | python_requires=">=3.9", 49 | tests_require=["pytest"], 50 | ) 51 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Documentation 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | paths: 7 | - 'docs/**' 8 | - 'stac_check/**/*.py' 9 | - 'README.md' 10 | pull_request: 11 | branches: [ main ] 12 | paths: 13 | - 'docs/**' 14 | - 'stac_check/**/*.py' 15 | - 'README.md' 16 | # Allow manual triggering 17 | workflow_dispatch: 18 | 19 | jobs: 20 | build: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v4 24 | - name: Set up Python 25 | uses: actions/setup-python@v5 26 | with: 27 | python-version: '3.11' 28 | - name: Install dependencies 29 | run: | 30 | python -m pip install --upgrade pip 31 | pip install -e ".[docs]" 32 | - name: Build documentation 33 | run: | 34 | sphinx-build -b html docs/ docs/_build/html 35 | - name: Upload documentation artifact 36 | uses: actions/upload-artifact@v4 37 | with: 38 | name: documentation 39 | path: docs/_build/html 40 | retention-days: 7 41 | 42 | # Only deploy when pushing to main (not on PRs) 43 | deploy: 44 | needs: build 45 | if: github.event_name == 'push' && github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch' 46 | runs-on: ubuntu-latest 47 | permissions: 48 | contents: write 49 | steps: 50 | - name: Download built documentation 51 | uses: actions/download-artifact@v4 52 | with: 53 | name: documentation 54 | path: ./docs-build 55 | - name: Deploy to GitHub Pages 56 | uses: peaceiris/actions-gh-pages@v4 57 | with: 58 | github_token: ${{ secrets.GITHUB_TOKEN }} 59 | publish_dir: ./docs-build 60 | force_orphan: true 61 | commit_message: "Update documentation [skip ci]" 62 | -------------------------------------------------------------------------------- /sample_files/1.1.0/simple-item.json: -------------------------------------------------------------------------------- 1 | { 2 | "stac_version": "1.1.0", 3 | "stac_extensions": [], 4 | "type": "Feature", 5 | "id": "20201211_223832_CS2", 6 | "bbox": [ 7 | 172.91173669923782, 1.3438851951615003, 172.95469614953714, 8 | 1.3690476620161975 9 | ], 10 | "geometry": { 11 | "type": "Polygon", 12 | "coordinates": [ 13 | [ 14 | [172.91173669923782, 1.3438851951615003], 15 | [172.95469614953714, 1.3438851951615003], 16 | [172.95469614953714, 1.3690476620161975], 17 | [172.91173669923782, 1.3690476620161975], 18 | [172.91173669923782, 1.3438851951615003] 19 | ] 20 | ] 21 | }, 22 | "properties": { 23 | "datetime": "2020-12-11T22:38:32.125000Z" 24 | }, 25 | "collection": "simple-collection", 26 | "links": [ 27 | { 28 | "rel": "collection", 29 | "href": "./collection.json", 30 | "type": "application/json", 31 | "title": "Simple Example Collection" 32 | }, 33 | { 34 | "rel": "root", 35 | "href": "./collection.json", 36 | "type": "application/json", 37 | "title": "Simple Example Collection" 38 | }, 39 | { 40 | "rel": "parent", 41 | "href": "./collection.json", 42 | "type": "application/json", 43 | "title": "Simple Example Collection" 44 | } 45 | ], 46 | "assets": { 47 | "visual": { 48 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.tif", 49 | "type": "image/tiff; application=geotiff; profile=cloud-optimized", 50 | "title": "3-Band Visual", 51 | "roles": ["visual"] 52 | }, 53 | "thumbnail": { 54 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.jpg", 55 | "title": "Thumbnail", 56 | "type": "image/jpeg", 57 | "roles": ["thumbnail"] 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tests/test.config.yml: -------------------------------------------------------------------------------- 1 | linting: 2 | # Identifiers should consist of only lowercase characters, numbers, '_', and '-' 3 | searchable_identifiers: true 4 | # Item name '{self.object_id}' should not contain ':' or '/' 5 | percent_encoded: true 6 | # Item file names should match their ids 7 | item_id_file_name: true 8 | # Collections and catalogs should be named collection.json and catalog.json 9 | catalog_id_file_name: true 10 | # A STAC collection should contain a summaries field 11 | check_summaries: true 12 | # Datetime fields should not be set to null 13 | null_datetime: true 14 | # best practices - check unlocated items to make sure bbox field is not set 15 | check_unlocated: true 16 | # best practices - recommend items have a geometry 17 | check_geometry: true 18 | # check to see if there are too many links 19 | bloated_links: true 20 | # best practices - check for bloated metadata in properties 21 | bloated_metadata: true 22 | # best practices - ensure thumbnail is a small file size ["png", "jpeg", "jpg", "webp"] 23 | check_thumbnail: true 24 | # best practices - ensure that links in catalogs and collections include a title field 25 | links_title: true 26 | # best practices - ensure that links in catalogs and collections include self link 27 | links_self: true 28 | 29 | # Geometry validation settings [BETA] 30 | geometry_validation: 31 | # Master switch to enable/disable all geometry validation checks 32 | enabled: true 33 | # check if geometry coordinates are potentially ordered incorrectly (longitude, latitude) 34 | geometry_coordinates_order: true 35 | # check if geometry coordinates contain definite errors (latitude > ±90°, longitude > ±180°) 36 | geometry_coordinates_definite_errors: true 37 | # check if bbox matches the bounds of the geometry 38 | bbox_geometry_match: true 39 | # check if a bbox that crosses the antimeridian is correctly formatted 40 | bbox_antimeridian: true 41 | 42 | settings: 43 | # number of links before the bloated links warning is shown 44 | max_links: 20 45 | # number of properties before the bloated metadata warning is shown 46 | max_properties: 20 -------------------------------------------------------------------------------- /stac_check/stac-check.config.yml: -------------------------------------------------------------------------------- 1 | linting: 2 | # Identifiers should consist of only lowercase characters, numbers, '_', and '-' 3 | searchable_identifiers: true 4 | # Item name '{self.object_id}' should not contain ':' or '/' 5 | percent_encoded: true 6 | # Item file names should match their ids 7 | item_id_file_name: true 8 | # Collections and catalogs should be named collection.json and catalog.json 9 | catalog_id_file_name: true 10 | # A STAC collection should contain a summaries field 11 | check_summaries: true 12 | # Datetime fields should not be set to null 13 | null_datetime: true 14 | # best practices - check unlocated items to make sure bbox field is not set 15 | check_unlocated: true 16 | # best practices - recommend items have a geometry 17 | check_geometry: true 18 | # check to see if there are too many links 19 | bloated_links: true 20 | # best practices - check for bloated metadata in properties 21 | bloated_metadata: true 22 | # best practices - ensure thumbnail is a small file size ["png", "jpeg", "jpg", "webp"] 23 | check_thumbnail: true 24 | # best practices - ensure that links in catalogs and collections include a title field 25 | links_title: true 26 | # best practices - ensure that links in catalogs and collections include self link 27 | links_self: true 28 | 29 | # Geometry validation settings [BETA] 30 | geometry_validation: 31 | # Master switch to enable/disable all geometry validation checks 32 | enabled: true 33 | # check if geometry coordinates are potentially ordered incorrectly (longitude, latitude) 34 | geometry_coordinates_order: true 35 | # check if geometry coordinates contain definite errors (latitude > ±90°, longitude > ±180°) 36 | geometry_coordinates_definite_errors: true 37 | # check if bbox matches the bounds of the geometry 38 | bbox_geometry_match: true 39 | # check if a bbox that crosses the antimeridian is correctly formatted 40 | bbox_antimeridian: true 41 | 42 | settings: 43 | # number of links before the bloated links warning is shown 44 | max_links: 20 45 | # number of properties before the bloated metadata warning is shown 46 | max_properties: 20 -------------------------------------------------------------------------------- /sample_files/1.0.0/collection-no-summaries.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "simple-collection", 3 | "type": "Collection", 4 | "stac_extensions": [ 5 | "https://stac-extensions.github.io/eo/v1.0.0/schema.json", 6 | "https://stac-extensions.github.io/projection/v1.0.0/schema.json", 7 | "https://stac-extensions.github.io/view/v1.0.0/schema.json" 8 | ], 9 | "stac_version": "1.0.0", 10 | "description": "A simple collection demonstrating core catalog fields with links to a couple of items", 11 | "title": "Simple Example Collection", 12 | "providers": [ 13 | { 14 | "name": "Remote Data, Inc", 15 | "description": "Producers of awesome spatiotemporal assets", 16 | "roles": [ 17 | "producer", 18 | "processor" 19 | ], 20 | "url": "http://remotedata.io" 21 | } 22 | ], 23 | "extent": { 24 | "spatial": { 25 | "bbox": [ 26 | [ 27 | 172.91173669923782, 28 | 1.3438851951615003, 29 | 172.95469614953714, 30 | 1.3690476620161975 31 | ] 32 | ] 33 | }, 34 | "temporal": { 35 | "interval": [ 36 | [ 37 | "2020-12-11T22:38:32.125Z", 38 | "2020-12-14T18:02:31.437Z" 39 | ] 40 | ] 41 | } 42 | }, 43 | "license": "CC-BY-4.0", 44 | "links": [ 45 | { 46 | "rel": "root", 47 | "href": "./collection.json", 48 | "type": "application/json", 49 | "title": "Simple Example Collection" 50 | }, 51 | { 52 | "rel": "item", 53 | "href": "./simple-item.json", 54 | "type": "application/geo+json", 55 | "title": "Simple Item" 56 | }, 57 | { 58 | "rel": "item", 59 | "href": "./core-item.json", 60 | "type": "application/geo+json", 61 | "title": "Core Item" 62 | }, 63 | { 64 | "rel": "item", 65 | "href": "./extended-item.json", 66 | "type": "application/geo+json", 67 | "title": "Extended Item" 68 | }, 69 | { 70 | "rel": "self", 71 | "href": "https://raw.githubusercontent.com/radiantearth/stac-spec/v1.0.0/examples/collection.json", 72 | "type": "application/json" 73 | } 74 | ] 75 | } -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # For the full list of built-in configuration values, see the documentation: 4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | 6 | # -- Project information ----------------------------------------------------- 7 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 8 | 9 | # -- Options for including images from README ---------------------------------- 10 | import os 11 | import shutil 12 | 13 | project = "stac-check" 14 | copyright = "2025, Jonathan Healy" 15 | author = "Jonathan Healy" 16 | release = "1.11.1" 17 | 18 | # -- General configuration --------------------------------------------------- 19 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 20 | 21 | extensions = [ 22 | "sphinx.ext.autodoc", 23 | "sphinx.ext.viewcode", 24 | "sphinx.ext.napoleon", 25 | "sphinx_rtd_theme", 26 | "sphinx.ext.intersphinx", 27 | "myst_parser", 28 | "sphinx_click", 29 | ] 30 | 31 | templates_path = ["_templates"] 32 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 33 | 34 | # -- Options for HTML output ------------------------------------------------- 35 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 36 | 37 | html_theme = "sphinx_rtd_theme" 38 | html_static_path = [] 39 | 40 | # Create _static directory if it doesn't exist 41 | static_dir = os.path.join(os.path.dirname(__file__), "_static") 42 | if not os.path.exists(static_dir): 43 | os.makedirs(static_dir) 44 | 45 | # Copy assets from project root to _static directory 46 | assets_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "assets") 47 | if os.path.exists(assets_dir): 48 | for file in os.listdir(assets_dir): 49 | src = os.path.join(assets_dir, file) 50 | dst = os.path.join(static_dir, file) 51 | if os.path.isfile(src): 52 | shutil.copy2(src, dst) 53 | 54 | # Now that we've copied files, update static path 55 | html_static_path = ["_static"] 56 | 57 | myst_heading_anchors = 3 # Generate anchors for h1, h2, and h3 58 | suppress_warnings = ["myst.header", "myst.xref_missing"] 59 | 60 | # Configure myst-parser to handle images 61 | myst_url_schemes = ("http", "https", "mailto", "ftp") 62 | 63 | html_css_files = [ 64 | "custom.css", 65 | ] 66 | -------------------------------------------------------------------------------- /tests/test_lint_pydantic.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from stac_check.lint import Linter 4 | 5 | # Check if stac-pydantic is available 6 | try: 7 | import importlib 8 | 9 | importlib.import_module("stac_pydantic") 10 | PYDANTIC_AVAILABLE = True 11 | except ImportError: 12 | PYDANTIC_AVAILABLE = False 13 | 14 | # Test decorator for pydantic tests 15 | pytest.mark.pydantic = pytest.mark.skipif( 16 | not PYDANTIC_AVAILABLE, 17 | reason="stac-pydantic is not installed. Run 'pip install -e .[dev]' to install test dependencies.", 18 | ) 19 | 20 | 21 | @pytest.mark.pydantic 22 | def test_lint_pydantic_validation_valid(): 23 | """Test pydantic validation with a valid STAC item.""" 24 | file = "sample_files/1.0.0/core-item.json" 25 | linter = Linter(file, pydantic=True) 26 | 27 | assert linter.valid_stac is True 28 | assert linter.asset_type == "ITEM" 29 | assert any("stac-pydantic" in schema for schema in linter.message["schema"]) 30 | assert linter.message["validation_method"] == "pydantic" 31 | 32 | 33 | @pytest.mark.pydantic 34 | def test_lint_pydantic_validation_invalid(): 35 | """Test pydantic validation with an invalid STAC item (missing required fields).""" 36 | file = "sample_files/1.0.0/bad-item.json" 37 | linter = Linter(file, pydantic=True) 38 | 39 | assert linter.valid_stac is False 40 | assert "PydanticValidationError" in linter.message["error_type"] 41 | assert "id: Field required" in linter.message["error_message"] 42 | 43 | 44 | def test_pydantic_fallback_without_import(monkeypatch): 45 | """Test that pydantic validation falls back to JSONSchema when stac-pydantic is not available.""" 46 | # Skip this test if stac-pydantic is actually installed 47 | if PYDANTIC_AVAILABLE: 48 | pytest.skip("stac-pydantic is installed, skipping fallback test") 49 | 50 | # Test that pydantic=False works without stac-pydantic 51 | file = "sample_files/1.0.0/core-item.json" 52 | linter = Linter(file, pydantic=False) 53 | assert linter.valid_stac is True 54 | assert linter.asset_type == "ITEM" 55 | assert linter.message["validation_method"] == "default" 56 | 57 | # Test that pydantic=True falls back to JSONSchema when stac-pydantic is not available 58 | linter = Linter(file, pydantic=True) 59 | assert linter.valid_stac is True 60 | assert linter.asset_type == "ITEM" 61 | assert linter.message["validation_method"] == "default" 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | **/.DS_Store -------------------------------------------------------------------------------- /sample_files/1.0.0/bad-item.json: -------------------------------------------------------------------------------- 1 | { 2 | "stac_version": "1.0.0", 3 | "stac_extensions": [ 4 | "https://stac-extensions.github.io/eo/v1.0.0/schema.json" 5 | ], 6 | "type": "Feature", 7 | "bbox": [ 8 | -122.59750209, 9 | 37.48803556, 10 | -122.2880486, 11 | 37.613531207 12 | ], 13 | "geometry": { 14 | "type": "Polygon", 15 | "coordinates": [ 16 | [ 17 | [ 18 | -122.308150179, 19 | 37.488035566 20 | ], 21 | [ 22 | -122.597502109, 23 | 37.538869539 24 | ], 25 | [ 26 | -122.576687533, 27 | 37.613537207 28 | ], 29 | [ 30 | -122.2880486, 31 | 37.562818007 32 | ], 33 | [ 34 | -122.308150179, 35 | 37.488035566 36 | ] 37 | ] 38 | ] 39 | }, 40 | "properties": { 41 | "title": "Full Item", 42 | "description": "A STAC item without an id", 43 | "datetime": null, 44 | "start_datetime": "2016-05-03T13:22:30Z", 45 | "end_datetime": "2016-05-03T13:27:30Z", 46 | "created": "2016-05-04T00:00:01Z", 47 | "updated": "2017-01-01T00:30:55Z", 48 | "license": "various", 49 | "providers": [ 50 | { 51 | "name": "Remote Data, Inc", 52 | "description": "Producers of awesome spatiotemporal assets", 53 | "roles": [ 54 | "producer", 55 | "processor" 56 | ], 57 | "url": "http://remotedata.it" 58 | } 59 | ], 60 | "platform": "cool_sat2", 61 | "instruments": [ 62 | "cool_sensor_v1" 63 | ] 64 | }, 65 | "links": [ 66 | { 67 | "rel": "root", 68 | "href": "./catalog.json", 69 | "type": "application/json", 70 | "title": "Example Catalog" 71 | }, 72 | { 73 | "rel": "parent", 74 | "href": "./catalog.json", 75 | "type": "application/json", 76 | "title": "Example Catalog" 77 | }, 78 | { 79 | "rel": "alternate", 80 | "type": "text/html", 81 | "href": "http://cool-sat.com/catalog/CS3-20160503_132130_04/CS3-20160503_132130_04.html", 82 | "title": "HTML representation of this STAC Item" 83 | }, 84 | { 85 | "rel": "license", 86 | "type": "text/html", 87 | "href": "http://remotedata.io/license.html", 88 | "title": "Data License for Remote Data, Inc." 89 | } 90 | ], 91 | "assets": { 92 | "analytic": { 93 | "href": "http://cool-sat.com/catalog/CS3-20160503_132130_04/analytic.tif", 94 | "title": "4-Band Analytic", 95 | "eo:bands": [ 96 | { 97 | "name": "band1" 98 | }, 99 | { 100 | "name": "band1" 101 | }, 102 | { 103 | "name": "band2" 104 | }, 105 | { 106 | "name": "band3" 107 | } 108 | ] 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /sample_files/1.0.0/collection-no-title.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "simple-collection", 3 | "type": "Collection", 4 | "stac_extensions": [ 5 | "https://stac-extensions.github.io/eo/v1.0.0/schema.json", 6 | "https://stac-extensions.github.io/projection/v1.0.0/schema.json", 7 | "https://stac-extensions.github.io/view/v1.0.0/schema.json" 8 | ], 9 | "stac_version": "1.0.0", 10 | "description": "A simple collection demonstrating core catalog fields with links to a couple of items", 11 | "title": "Simple Example Collection", 12 | "providers": [ 13 | { 14 | "name": "Remote Data, Inc", 15 | "description": "Producers of awesome spatiotemporal assets", 16 | "roles": [ 17 | "producer", 18 | "processor" 19 | ], 20 | "url": "http://remotedata.io" 21 | } 22 | ], 23 | "extent": { 24 | "spatial": { 25 | "bbox": [ 26 | [ 27 | 172.91173669923782, 28 | 1.3438851951615003, 29 | 172.95469614953714, 30 | 1.3690476620161975 31 | ] 32 | ] 33 | }, 34 | "temporal": { 35 | "interval": [ 36 | [ 37 | "2020-12-11T22:38:32.125Z", 38 | "2020-12-14T18:02:31.437Z" 39 | ] 40 | ] 41 | } 42 | }, 43 | "license": "CC-BY-4.0", 44 | "summaries": { 45 | "platform": [ 46 | "cool_sat1", 47 | "cool_sat2" 48 | ], 49 | "constellation": [ 50 | "ion" 51 | ], 52 | "instruments": [ 53 | "cool_sensor_v1", 54 | "cool_sensor_v2" 55 | ], 56 | "gsd": { 57 | "minimum": 0.512, 58 | "maximum": 0.66 59 | }, 60 | "eo:cloud_cover": { 61 | "minimum": 1.2, 62 | "maximum": 1.2 63 | }, 64 | "proj:epsg": { 65 | "minimum": 32659, 66 | "maximum": 32659 67 | }, 68 | "view:sun_elevation": { 69 | "minimum": 54.9, 70 | "maximum": 54.9 71 | }, 72 | "view:off_nadir": { 73 | "minimum": 3.8, 74 | "maximum": 3.8 75 | }, 76 | "view:sun_azimuth": { 77 | "minimum": 135.7, 78 | "maximum": 135.7 79 | } 80 | }, 81 | "links": [ 82 | { 83 | "rel": "root", 84 | "href": "./collection.json", 85 | "type": "application/json", 86 | "title": "Simple Example Collection" 87 | }, 88 | { 89 | "rel": "item", 90 | "href": "./simple-item.json", 91 | "type": "application/geo+json", 92 | "title": "Simple Item" 93 | }, 94 | { 95 | "rel": "item", 96 | "href": "./core-item.json", 97 | "type": "application/geo+json" 98 | }, 99 | { 100 | "rel": "item", 101 | "href": "./extended-item.json", 102 | "type": "application/geo+json", 103 | "title": "Extended Item" 104 | } 105 | ] 106 | } -------------------------------------------------------------------------------- /sample_files/0.9.0/bad-item.json: -------------------------------------------------------------------------------- 1 | { 2 | "stac_version": "0.9.0", 3 | "stac_extensions": [ 4 | "eo", 5 | "view", 6 | "https://example.com/cs-extension/1.0/schema.json" 7 | ], 8 | "type": "Feature", 9 | "bbox": [-122.59750209, 37.48803556, -122.2880486, 37.613537207], 10 | "geometry": { 11 | "type": "Polygon", 12 | "coordinates": [ 13 | [ 14 | [-122.308150179, 37.488035566], 15 | [-122.597502109, 37.538869539], 16 | [-122.576687533, 37.613537207], 17 | [-122.288048600, 37.562818007], 18 | [-122.308150179, 37.488035566] 19 | ] 20 | ] 21 | }, 22 | "properties": { 23 | "datetime": "2016-05-03T13:22:30Z", 24 | "title": "A CS3 item", 25 | "license": "PDDL-1.0", 26 | "providers": [ 27 | { 28 | "name": "CoolSat", 29 | "roles": [ 30 | "producer", 31 | "licensor" 32 | ], 33 | "url": "https://cool-sat.com/" 34 | } 35 | ], 36 | "created": "2016-05-04T00:00:01Z", 37 | "updated": "2017-01-01T00:30:55Z", 38 | "view:sun_azimuth": 168.7, 39 | "eo:cloud_cover": 0.12, 40 | "view:off_nadir": 1.4, 41 | "platform": "coolsat2", 42 | "instruments": ["cool_sensor_v1"], 43 | "eo:bands": [], 44 | "view:sun_elevation": 33.4, 45 | "eo:gsd": 0.512, 46 | "cs:type": "scene", 47 | "cs:anomalous_pixels": 0.14, 48 | "cs:earth_sun_distance": 1.0141560, 49 | "cs:sat_id": "CS3", 50 | "cs:product_level": "LV1B" 51 | }, 52 | "collection": "CS3", 53 | "links": [ 54 | {"rel": "self", "href": "http://cool-sat.com/catalog/CS3-20160503_132130_04/CS3-20160503_132130_04.json"}, 55 | {"rel": "root", "href": "http://cool-sat.com/catalog/catalog.json"}, 56 | {"rel": "parent", "href": "http://cool-sat.com/catalog/CS3-20160503_132130_04/catalog.json"}, 57 | {"rel": "collection", "href": "http://cool-sat.com/catalog/CS3-20160503_132130_04/catalog.json"}, 58 | {"rel": "acquisition", "href": "http://cool-sat.com/catalog/acquisitions/20160503_56"} 59 | ], 60 | "assets": { 61 | "analytic": { 62 | "href": "http://cool-sat.com/catalog/CS3-20160503_132130_04/analytic.tif", 63 | "title": "4-Band Analytic" 64 | }, 65 | "thumbnail": { 66 | "href": "http://cool-sat.com/catalog/CS3-20160503_132130_04/thumbnail.png", 67 | "title": "Thumbnail", 68 | "type": "image/png", 69 | "roles": [ "thumbnail" ] 70 | }, 71 | "udm": { 72 | "href": "http://cool-sat.com/catalog/CS3-20160503_132130_04/UDM.tif", 73 | "title": "Unusable Data Mask" 74 | }, 75 | "json-metadata": { 76 | "href": "http://cool-sat.com/catalog/CS3-20160503_132130_04/extended-metadata.json", 77 | "title": "Extended Metadata", 78 | "type": "application/json", 79 | "roles": [ "thumbnail" ] 80 | }, 81 | "ephemeris": { 82 | "href": "http://cool-sat.com/catalog/CS3-20160503_132130_04/S3-20160503_132130_04.EPH", 83 | "title": "Satellite Ephemeris Metadata" 84 | } 85 | } 86 | 87 | } -------------------------------------------------------------------------------- /sample_files/1.0.0/collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "simple-collection", 3 | "type": "Collection", 4 | "stac_extensions": [ 5 | "https://stac-extensions.github.io/eo/v1.0.0/schema.json", 6 | "https://stac-extensions.github.io/projection/v1.0.0/schema.json", 7 | "https://stac-extensions.github.io/view/v1.0.0/schema.json" 8 | ], 9 | "stac_version": "1.0.0", 10 | "description": "A simple collection demonstrating core catalog fields with links to a couple of items", 11 | "title": "Simple Example Collection", 12 | "providers": [ 13 | { 14 | "name": "Remote Data, Inc", 15 | "description": "Producers of awesome spatiotemporal assets", 16 | "roles": [ 17 | "producer", 18 | "processor" 19 | ], 20 | "url": "http://remotedata.io" 21 | } 22 | ], 23 | "extent": { 24 | "spatial": { 25 | "bbox": [ 26 | [ 27 | 172.91173669923782, 28 | 1.3438851951615003, 29 | 172.95469614953714, 30 | 1.3690476620161975 31 | ] 32 | ] 33 | }, 34 | "temporal": { 35 | "interval": [ 36 | [ 37 | "2020-12-11T22:38:32.125Z", 38 | "2020-12-14T18:02:31.437Z" 39 | ] 40 | ] 41 | } 42 | }, 43 | "license": "CC-BY-4.0", 44 | "summaries": { 45 | "platform": [ 46 | "cool_sat1", 47 | "cool_sat2" 48 | ], 49 | "constellation": [ 50 | "ion" 51 | ], 52 | "instruments": [ 53 | "cool_sensor_v1", 54 | "cool_sensor_v2" 55 | ], 56 | "gsd": { 57 | "minimum": 0.512, 58 | "maximum": 0.66 59 | }, 60 | "eo:cloud_cover": { 61 | "minimum": 1.2, 62 | "maximum": 1.2 63 | }, 64 | "proj:epsg": { 65 | "minimum": 32659, 66 | "maximum": 32659 67 | }, 68 | "view:sun_elevation": { 69 | "minimum": 54.9, 70 | "maximum": 54.9 71 | }, 72 | "view:off_nadir": { 73 | "minimum": 3.8, 74 | "maximum": 3.8 75 | }, 76 | "view:sun_azimuth": { 77 | "minimum": 135.7, 78 | "maximum": 135.7 79 | } 80 | }, 81 | "links": [ 82 | { 83 | "rel": "root", 84 | "href": "./collection.json", 85 | "type": "application/json", 86 | "title": "Simple Example Collection" 87 | }, 88 | { 89 | "rel": "item", 90 | "href": "./simple-item.json", 91 | "type": "application/geo+json", 92 | "title": "Simple Item" 93 | }, 94 | { 95 | "rel": "item", 96 | "href": "./core-item.json", 97 | "type": "application/geo+json", 98 | "title": "Core Item" 99 | }, 100 | { 101 | "rel": "item", 102 | "href": "./extended-item.json", 103 | "type": "application/geo+json", 104 | "title": "Extended Item" 105 | }, 106 | { 107 | "rel": "self", 108 | "href": "https://raw.githubusercontent.com/radiantearth/stac-spec/v1.0.0/examples/collection.json", 109 | "type": "application/json" 110 | } 111 | ] 112 | } -------------------------------------------------------------------------------- /sample_files/1.0.0/core-item-unlocated-null-bbox.json: -------------------------------------------------------------------------------- 1 | { 2 | "stac_version": "1.0.0", 3 | "stac_extensions": [], 4 | "type": "Feature", 5 | "id": "20201211_223832_CS2", 6 | "geometry": null, 7 | "properties": { 8 | "title": "Core Item", 9 | "description": "A sample STAC Item that includes examples of all common metadata", 10 | "datetime": null, 11 | "start_datetime": "2020-12-11T22:38:32.125Z", 12 | "end_datetime": "2020-12-11T22:38:32.327Z", 13 | "created": "2020-12-12T01:48:13.725Z", 14 | "updated": "2020-12-12T01:48:13.725Z", 15 | "platform": "cool_sat1", 16 | "instruments": [ 17 | "cool_sensor_v1" 18 | ], 19 | "constellation": "ion", 20 | "mission": "collection 5624", 21 | "gsd": 0.512 22 | }, 23 | "collection": "simple-collection", 24 | "links": [ 25 | { 26 | "rel": "collection", 27 | "href": "./collection.json", 28 | "type": "application/json", 29 | "title": "Simple Example Collection" 30 | }, 31 | { 32 | "rel": "root", 33 | "href": "./collection.json", 34 | "type": "application/json", 35 | "title": "Simple Example Collection" 36 | }, 37 | { 38 | "rel": "parent", 39 | "href": "./collection.json", 40 | "type": "application/json", 41 | "title": "Simple Example Collection" 42 | }, 43 | { 44 | "rel": "alternate", 45 | "type": "text/html", 46 | "href": "http://remotedata.io/catalog/20201211_223832_CS2/index.html", 47 | "title": "HTML version of this STAC Item" 48 | } 49 | ], 50 | "assets": { 51 | "analytic": { 52 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2_analytic.tif", 53 | "type": "image/tiff; application=geotiff; profile=cloud-optimized", 54 | "title": "4-Band Analytic", 55 | "roles": [ 56 | "data" 57 | ] 58 | }, 59 | "thumbnail": { 60 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.jpg", 61 | "title": "Thumbnail", 62 | "type": "image/jpg", 63 | "roles": [ 64 | "thumbnail" 65 | ] 66 | }, 67 | "visual": { 68 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.tif", 69 | "type": "image/tiff; application=geotiff; profile=cloud-optimized", 70 | "title": "3-Band Visual", 71 | "roles": [ 72 | "visual" 73 | ] 74 | }, 75 | "udm": { 76 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2_analytic_udm.tif", 77 | "title": "Unusable Data Mask", 78 | "type": "image/tiff; application=geotiff;" 79 | }, 80 | "json-metadata": { 81 | "href": "http://remotedata.io/catalog/20201211_223832_CS2/extended-metadata.json", 82 | "title": "Extended Metadata", 83 | "type": "application/json", 84 | "roles": [ 85 | "metadata" 86 | ] 87 | }, 88 | "ephemeris": { 89 | "href": "http://cool-sat.com/catalog/20201211_223832_CS2/20201211_223832_CS2.EPH", 90 | "title": "Satellite Ephemeris Metadata" 91 | } 92 | } 93 | } -------------------------------------------------------------------------------- /sample_files/1.0.0/core-item-unlocated.json: -------------------------------------------------------------------------------- 1 | { 2 | "stac_version": "1.0.0", 3 | "stac_extensions": [], 4 | "type": "Feature", 5 | "id": "20201211_223832_CS2", 6 | "bbox": [ 7 | 172.91173669923782, 8 | 1.3438851951615003, 9 | 172.95469614953714, 10 | 1.3690476620161975 11 | ], 12 | "geometry": null, 13 | "properties": { 14 | "title": "Core Item", 15 | "description": "A sample STAC Item that includes examples of all common metadata", 16 | "datetime": null, 17 | "start_datetime": "2020-12-11T22:38:32.125Z", 18 | "end_datetime": "2020-12-11T22:38:32.327Z", 19 | "created": "2020-12-12T01:48:13.725Z", 20 | "updated": "2020-12-12T01:48:13.725Z", 21 | "platform": "cool_sat1", 22 | "instruments": [ 23 | "cool_sensor_v1" 24 | ], 25 | "constellation": "ion", 26 | "mission": "collection 5624", 27 | "gsd": 0.512 28 | }, 29 | "collection": "simple-collection", 30 | "links": [ 31 | { 32 | "rel": "collection", 33 | "href": "./collection.json", 34 | "type": "application/json", 35 | "title": "Simple Example Collection" 36 | }, 37 | { 38 | "rel": "root", 39 | "href": "./collection.json", 40 | "type": "application/json", 41 | "title": "Simple Example Collection" 42 | }, 43 | { 44 | "rel": "parent", 45 | "href": "./collection.json", 46 | "type": "application/json", 47 | "title": "Simple Example Collection" 48 | }, 49 | { 50 | "rel": "alternate", 51 | "type": "text/html", 52 | "href": "http://remotedata.io/catalog/20201211_223832_CS2/index.html", 53 | "title": "HTML version of this STAC Item" 54 | } 55 | ], 56 | "assets": { 57 | "analytic": { 58 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2_analytic.tif", 59 | "type": "image/tiff; application=geotiff; profile=cloud-optimized", 60 | "title": "4-Band Analytic", 61 | "roles": [ 62 | "data" 63 | ] 64 | }, 65 | "thumbnail": { 66 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.jpg", 67 | "title": "Thumbnail", 68 | "type": "image/png", 69 | "roles": [ 70 | "thumbnail" 71 | ] 72 | }, 73 | "visual": { 74 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.tif", 75 | "type": "image/tiff; application=geotiff; profile=cloud-optimized", 76 | "title": "3-Band Visual", 77 | "roles": [ 78 | "visual" 79 | ] 80 | }, 81 | "udm": { 82 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2_analytic_udm.tif", 83 | "title": "Unusable Data Mask", 84 | "type": "image/tiff; application=geotiff;" 85 | }, 86 | "json-metadata": { 87 | "href": "http://remotedata.io/catalog/20201211_223832_CS2/extended-metadata.json", 88 | "title": "Extended Metadata", 89 | "type": "application/json", 90 | "roles": [ 91 | "metadata" 92 | ] 93 | }, 94 | "ephemeris": { 95 | "href": "http://cool-sat.com/catalog/20201211_223832_CS2/20201211_223832_CS2.EPH", 96 | "title": "Satellite Ephemeris Metadata" 97 | } 98 | } 99 | } -------------------------------------------------------------------------------- /sample_files/1.1.0/collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "simple-collection", 3 | "type": "Collection", 4 | "stac_extensions": [ 5 | "https://stac-extensions.github.io/eo/v2.0.0/schema.json", 6 | "https://stac-extensions.github.io/projection/v2.0.0/schema.json", 7 | "https://stac-extensions.github.io/view/v1.0.0/schema.json" 8 | ], 9 | "stac_version": "1.1.0", 10 | "description": "A simple collection demonstrating core catalog fields with links to a couple of items", 11 | "title": "Simple Example Collection", 12 | "keywords": ["simple", "example", "collection"], 13 | "providers": [ 14 | { 15 | "name": "Remote Data, Inc", 16 | "description": "Producers of awesome spatiotemporal assets", 17 | "roles": ["producer", "processor"], 18 | "url": "http://remotedata.io" 19 | } 20 | ], 21 | "extent": { 22 | "spatial": { 23 | "bbox": [ 24 | [ 25 | 172.91173669923782, 1.3438851951615003, 172.95469614953714, 26 | 1.3690476620161975 27 | ] 28 | ] 29 | }, 30 | "temporal": { 31 | "interval": [["2020-12-11T22:38:32.125Z", "2020-12-14T18:02:31.437Z"]] 32 | } 33 | }, 34 | "license": "CC-BY-4.0", 35 | "summaries": { 36 | "platform": ["cool_sat1", "cool_sat2"], 37 | "constellation": ["ion"], 38 | "instruments": ["cool_sensor_v1", "cool_sensor_v2"], 39 | "gsd": { 40 | "minimum": 0.512, 41 | "maximum": 0.66 42 | }, 43 | "eo:cloud_cover": { 44 | "minimum": 1.2, 45 | "maximum": 1.2 46 | }, 47 | "proj:cpde": ["EPSG:32659"], 48 | "view:sun_elevation": { 49 | "minimum": 54.9, 50 | "maximum": 54.9 51 | }, 52 | "view:off_nadir": { 53 | "minimum": 3.8, 54 | "maximum": 3.8 55 | }, 56 | "view:sun_azimuth": { 57 | "minimum": 135.7, 58 | "maximum": 135.7 59 | }, 60 | "statistics": { 61 | "type": "object", 62 | "properties": { 63 | "vegetation": { 64 | "description": "Percentage of pixels that are detected as vegetation, e.g. forests, grasslands, etc.", 65 | "minimum": 0, 66 | "maximum": 100 67 | }, 68 | "water": { 69 | "description": "Percentage of pixels that are detected as water, e.g. rivers, oceans and ponds.", 70 | "minimum": 0, 71 | "maximum": 100 72 | }, 73 | "urban": { 74 | "description": "Percentage of pixels that detected as urban, e.g. roads and buildings.", 75 | "minimum": 0, 76 | "maximum": 100 77 | } 78 | } 79 | } 80 | }, 81 | "links": [ 82 | { 83 | "rel": "root", 84 | "href": "./collection.json", 85 | "type": "application/json", 86 | "title": "Simple Example Collection" 87 | }, 88 | { 89 | "rel": "item", 90 | "href": "./simple-item.json", 91 | "type": "application/geo+json", 92 | "title": "Simple Item" 93 | }, 94 | { 95 | "rel": "item", 96 | "href": "./core-item.json", 97 | "type": "application/geo+json", 98 | "title": "Core Item" 99 | }, 100 | { 101 | "rel": "item", 102 | "href": "./extended-item.json", 103 | "type": "application/geo+json", 104 | "title": "Extended Item" 105 | }, 106 | { 107 | "rel": "self", 108 | "href": "https://raw.githubusercontent.com/radiantearth/stac-spec/v1.1.0/examples/collection.json", 109 | "type": "application/json" 110 | } 111 | ] 112 | } 113 | -------------------------------------------------------------------------------- /sample_files/1.0.0/20201211_223832_cs2.json: -------------------------------------------------------------------------------- 1 | { 2 | "stac_version": "1.0.0", 3 | "stac_extensions": [], 4 | "type": "Feature", 5 | "id": "20201211_223832_cs2", 6 | "bbox": [ 7 | 172.91173669923782, 8 | 1.3438851951615003, 9 | 172.95469614953714, 10 | 1.3690476620161975 11 | ], 12 | "geometry": { 13 | "type": "Polygon", 14 | "coordinates": [ 15 | [ 16 | [ 17 | 172.91173669923782, 18 | 1.3438851951615003 19 | ], 20 | [ 21 | 172.95469614953714, 22 | 1.3438851951615003 23 | ], 24 | [ 25 | 172.95469614953714, 26 | 1.3690476620161975 27 | ], 28 | [ 29 | 172.91173669923782, 30 | 1.3690476620161975 31 | ], 32 | [ 33 | 172.91173669923782, 34 | 1.3438851951615003 35 | ] 36 | ] 37 | ] 38 | }, 39 | "properties": { 40 | "title": "Core Item", 41 | "description": "A sample STAC Item that includes examples of all common metadata", 42 | "datetime": "2020-12-11T22:38:32.125Z", 43 | "start_datetime": "2020-12-11T22:38:32.125Z", 44 | "end_datetime": "2020-12-11T22:38:32.327Z", 45 | "created": "2020-12-12T01:48:13.725Z", 46 | "updated": "2020-12-12T01:48:13.725Z", 47 | "platform": "cool_sat1", 48 | "instruments": [ 49 | "cool_sensor_v1" 50 | ], 51 | "constellation": "ion", 52 | "mission": "collection 5624", 53 | "gsd": 0.512 54 | }, 55 | "collection": "simple-collection", 56 | "links": [ 57 | { 58 | "rel": "collection", 59 | "href": "./collection.json", 60 | "type": "application/json", 61 | "title": "Simple Example Collection" 62 | }, 63 | { 64 | "rel": "root", 65 | "href": "./collection.json", 66 | "type": "application/json", 67 | "title": "Simple Example Collection" 68 | }, 69 | { 70 | "rel": "self", 71 | "href": "./collection.json", 72 | "type": "application/json", 73 | "title": "Simple Example Collection" 74 | }, 75 | { 76 | "rel": "parent", 77 | "href": "./collection.json", 78 | "type": "application/json", 79 | "title": "Simple Example Collection" 80 | }, 81 | { 82 | "rel": "alternate", 83 | "type": "text/html", 84 | "href": "http://remotedata.io/catalog/20201211_223832_CS2/index.html", 85 | "title": "HTML version of this STAC Item" 86 | } 87 | ], 88 | "assets": { 89 | "analytic": { 90 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2_analytic.tif", 91 | "type": "image/tiff; application=geotiff; profile=cloud-optimized", 92 | "title": "4-Band Analytic", 93 | "roles": [ 94 | "data" 95 | ] 96 | }, 97 | "thumbnail": { 98 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.jpg", 99 | "title": "Thumbnail", 100 | "type": "image/png", 101 | "roles": [ 102 | "thumbnail" 103 | ] 104 | }, 105 | "visual": { 106 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.tif", 107 | "type": "image/tiff; application=geotiff; profile=cloud-optimized", 108 | "title": "3-Band Visual", 109 | "roles": [ 110 | "visual" 111 | ] 112 | }, 113 | "udm": { 114 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2_analytic_udm.tif", 115 | "title": "Unusable Data Mask", 116 | "type": "image/tiff; application=geotiff;" 117 | }, 118 | "json-metadata": { 119 | "href": "http://remotedata.io/catalog/20201211_223832_CS2/extended-metadata.json", 120 | "title": "Extended Metadata", 121 | "type": "application/json", 122 | "roles": [ 123 | "metadata" 124 | ] 125 | }, 126 | "ephemeris": { 127 | "href": "http://cool-sat.com/catalog/20201211_223832_CS2/20201211_223832_CS2.EPH", 128 | "title": "Satellite Ephemeris Metadata" 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /sample_files/1.0.0/core-item.json: -------------------------------------------------------------------------------- 1 | { 2 | "stac_version": "1.0.0", 3 | "stac_extensions": [], 4 | "type": "Feature", 5 | "id": "20201211_223832_CS2", 6 | "bbox": [ 7 | 172.91173669923782, 8 | 1.3438851951615003, 9 | 172.95469614953714, 10 | 1.3690476620161975 11 | ], 12 | "geometry": { 13 | "type": "Polygon", 14 | "coordinates": [ 15 | [ 16 | [ 17 | 172.91173669923782, 18 | 1.3438851951615003 19 | ], 20 | [ 21 | 172.95469614953714, 22 | 1.3438851951615003 23 | ], 24 | [ 25 | 172.95469614953714, 26 | 1.3690476620161975 27 | ], 28 | [ 29 | 172.91173669923782, 30 | 1.3690476620161975 31 | ], 32 | [ 33 | 172.91173669923782, 34 | 1.3438851951615003 35 | ] 36 | ] 37 | ] 38 | }, 39 | "properties": { 40 | "title": "Core Item", 41 | "description": "A sample STAC Item that includes examples of all common metadata", 42 | "datetime": null, 43 | "start_datetime": "2020-12-11T22:38:32.125Z", 44 | "end_datetime": "2020-12-11T22:38:32.327Z", 45 | "created": "2020-12-12T01:48:13.725Z", 46 | "updated": "2020-12-12T01:48:13.725Z", 47 | "platform": "cool_sat1", 48 | "instruments": [ 49 | "cool_sensor_v1" 50 | ], 51 | "constellation": "ion", 52 | "mission": "collection 5624", 53 | "gsd": 0.512 54 | }, 55 | "collection": "simple-collection", 56 | "links": [ 57 | { 58 | "rel": "collection", 59 | "href": "./collection.json", 60 | "type": "application/json", 61 | "title": "Simple Example Collection" 62 | }, 63 | { 64 | "rel": "root", 65 | "href": "./collection.json", 66 | "type": "application/json", 67 | "title": "Simple Example Collection" 68 | }, 69 | { 70 | "rel": "parent", 71 | "href": "./collection.json", 72 | "type": "application/json", 73 | "title": "Simple Example Collection" 74 | }, 75 | { 76 | "rel": "alternate", 77 | "type": "text/html", 78 | "href": "http://remotedata.io/catalog/20201211_223832_CS2/index.html", 79 | "title": "HTML version of this STAC Item" 80 | } 81 | ], 82 | "assets": { 83 | "analytic": { 84 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2_analytic.tif", 85 | "type": "image/tiff; application=geotiff; profile=cloud-optimized", 86 | "title": "4-Band Analytic", 87 | "roles": [ 88 | "data" 89 | ] 90 | }, 91 | "thumbnail": { 92 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.jpg", 93 | "title": "Thumbnail", 94 | "type": "image/jpg", 95 | "roles": [ 96 | "thumbnail" 97 | ] 98 | }, 99 | "visual": { 100 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.tif", 101 | "type": "image/tiff; application=geotiff; profile=cloud-optimized", 102 | "title": "3-Band Visual", 103 | "roles": [ 104 | "visual" 105 | ] 106 | }, 107 | "udm": { 108 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2_analytic_udm.tif", 109 | "title": "Unusable Data Mask", 110 | "type": "image/tiff; application=geotiff;" 111 | }, 112 | "json-metadata": { 113 | "href": "http://remotedata.io/catalog/20201211_223832_CS2/extended-metadata.json", 114 | "title": "Extended Metadata", 115 | "type": "application/json", 116 | "roles": [ 117 | "metadata" 118 | ] 119 | }, 120 | "ephemeris": { 121 | "href": "http://cool-sat.com/catalog/20201211_223832_CS2/20201211_223832_CS2.EPH", 122 | "title": "Satellite Ephemeris Metadata" 123 | } 124 | } 125 | } -------------------------------------------------------------------------------- /sample_files/1.0.0/core-item-bad-links.json: -------------------------------------------------------------------------------- 1 | { 2 | "stac_version": "1.0.0", 3 | "stac_extensions": [], 4 | "type": "Feature", 5 | "id": "20201211_223832_CS2", 6 | "bbox": [ 7 | 172.91173669923782, 8 | 1.3438851951615003, 9 | 172.95469614953714, 10 | 1.3690476620161975 11 | ], 12 | "geometry": { 13 | "type": "Polygon", 14 | "coordinates": [ 15 | [ 16 | [ 17 | 172.91173669923782, 18 | 1.3438851951615003 19 | ], 20 | [ 21 | 172.95469614953714, 22 | 1.3438851951615003 23 | ], 24 | [ 25 | 172.95469614953714, 26 | 1.3690476620161975 27 | ], 28 | [ 29 | 172.91173669923782, 30 | 1.3690476620161975 31 | ], 32 | [ 33 | 172.91173669923782, 34 | 1.3438851951615003 35 | ] 36 | ] 37 | ] 38 | }, 39 | "properties": { 40 | "title": "Core Item", 41 | "description": "A sample STAC Item that includes examples of all common metadata", 42 | "datetime": null, 43 | "start_datetime": "2020-12-11T22:38:32.125Z", 44 | "end_datetime": "2020-12-11T22:38:32.327Z", 45 | "created": "2020-12-12T01:48:13.725Z", 46 | "updated": "2020-12-12T01:48:13.725Z", 47 | "platform": "cool_sat1", 48 | "instruments": [ 49 | "cool_sensor_v1" 50 | ], 51 | "constellation": "ion", 52 | "mission": "collection 5624", 53 | "gsd": 0.512 54 | }, 55 | "collection": "simple-collection", 56 | "links": [ 57 | { 58 | "rel": "collection", 59 | "href": "./collection.json", 60 | "type": "application/json", 61 | "title": "Simple Example Collection" 62 | }, 63 | { 64 | "rel": "root", 65 | "href": "./collection.json", 66 | "type": "application/json", 67 | "title": "Simple Example Collection" 68 | }, 69 | { 70 | "rel": "parent", 71 | "href": "./collection.json", 72 | "type": "application/json", 73 | "title": "Simple Example Collection" 74 | }, 75 | { 76 | "rel": "alternate", 77 | "type": "text/html", 78 | "href": "http:/remotdata.io/catalog/20201211_223832_CS2/index.html", 79 | "title": "HTML version of this STAC Item" 80 | } 81 | ], 82 | "assets": { 83 | "analytic": { 84 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2_analytic.tif", 85 | "type": "image/tiff; application=geotiff; profile=cloud-optimized", 86 | "title": "4-Band Analytic", 87 | "roles": [ 88 | "data" 89 | ] 90 | }, 91 | "thumbnail": { 92 | "href": "https:/storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.jpg", 93 | "title": "Thumbnail", 94 | "type": "image/png", 95 | "roles": [ 96 | "thumbnail" 97 | ] 98 | }, 99 | "visual": { 100 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.tif", 101 | "type": "image/tiff; application=geotiff; profile=cloud-optimized", 102 | "title": "3-Band Visual", 103 | "roles": [ 104 | "visual" 105 | ] 106 | }, 107 | "udm": { 108 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2_analytic_udm.tif", 109 | "title": "Unusable Data Mask", 110 | "type": "image/tiff; application=geotiff;" 111 | }, 112 | "json-metadata": { 113 | "href": "http://remotedata.io/catalog/20201211_223832_CS2/extended-metadata.json", 114 | "title": "Extended Metadata", 115 | "type": "application/json", 116 | "roles": [ 117 | "metadata" 118 | ] 119 | }, 120 | "ephemeris": { 121 | "href": "http://cool-sat.com/catalog/20201211_223832_CS2/20201211_223832_CS2.EPH", 122 | "title": "Satellite Ephemeris Metadata" 123 | } 124 | } 125 | } -------------------------------------------------------------------------------- /sample_files/1.0.0/core-item-invalid-id.json: -------------------------------------------------------------------------------- 1 | { 2 | "stac_version": "1.0.0", 3 | "stac_extensions": [], 4 | "type": "Feature", 5 | "id": "F_20201211_22:3832_CS2", 6 | "bbox": [ 7 | 172.91173669923782, 8 | 1.3438851951615003, 9 | 172.95469614953714, 10 | 1.3690476620161975 11 | ], 12 | "geometry": { 13 | "type": "Polygon", 14 | "coordinates": [ 15 | [ 16 | [ 17 | 172.91173669923782, 18 | 1.3438851951615003 19 | ], 20 | [ 21 | 172.95469614953714, 22 | 1.3438851951615003 23 | ], 24 | [ 25 | 172.95469614953714, 26 | 1.3690476620161975 27 | ], 28 | [ 29 | 172.91173669923782, 30 | 1.3690476620161975 31 | ], 32 | [ 33 | 172.91173669923782, 34 | 1.3438851951615003 35 | ] 36 | ] 37 | ] 38 | }, 39 | "properties": { 40 | "title": "Core Item", 41 | "description": "A sample STAC Item that includes examples of all common metadata", 42 | "datetime": null, 43 | "start_datetime": "2020-12-11T22:38:32.125Z", 44 | "end_datetime": "2020-12-11T22:38:32.327Z", 45 | "created": "2020-12-12T01:48:13.725Z", 46 | "updated": "2020-12-12T01:48:13.725Z", 47 | "platform": "cool_sat1", 48 | "instruments": [ 49 | "cool_sensor_v1" 50 | ], 51 | "constellation": "ion", 52 | "mission": "collection 5624", 53 | "gsd": 0.512 54 | }, 55 | "collection": "simple-collection", 56 | "links": [ 57 | { 58 | "rel": "collection", 59 | "href": "./collection.json", 60 | "type": "application/json", 61 | "title": "Simple Example Collection" 62 | }, 63 | { 64 | "rel": "root", 65 | "href": "./collection.json", 66 | "type": "application/json", 67 | "title": "Simple Example Collection" 68 | }, 69 | { 70 | "rel": "parent", 71 | "href": "./collection.json", 72 | "type": "application/json", 73 | "title": "Simple Example Collection" 74 | }, 75 | { 76 | "rel": "alternate", 77 | "type": "text/html", 78 | "href": "http://remotedata.io/catalog/20201211_223832_CS2/index.html", 79 | "title": "HTML version of this STAC Item" 80 | } 81 | ], 82 | "assets": { 83 | "analytic": { 84 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2_analytic.tif", 85 | "type": "image/tiff; application=geotiff; profile=cloud-optimized", 86 | "title": "4-Band Analytic", 87 | "roles": [ 88 | "data" 89 | ] 90 | }, 91 | "thumbnail": { 92 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.jpg", 93 | "title": "Thumbnail", 94 | "type": "image/png", 95 | "roles": [ 96 | "thumbnail" 97 | ] 98 | }, 99 | "visual": { 100 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.tif", 101 | "type": "image/tiff; application=geotiff; profile=cloud-optimized", 102 | "title": "3-Band Visual", 103 | "roles": [ 104 | "visual" 105 | ] 106 | }, 107 | "udm": { 108 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2_analytic_udm.tif", 109 | "title": "Unusable Data Mask", 110 | "type": "image/tiff; application=geotiff;" 111 | }, 112 | "json-metadata": { 113 | "href": "http://remotedata.io/catalog/20201211_223832_CS2/extended-metadata.json", 114 | "title": "Extended Metadata", 115 | "type": "application/json", 116 | "roles": [ 117 | "metadata" 118 | ] 119 | }, 120 | "ephemeris": { 121 | "href": "http://cool-sat.com/catalog/20201211_223832_CS2/20201211_223832_CS2.EPH", 122 | "title": "Satellite Ephemeris Metadata" 123 | } 124 | } 125 | } -------------------------------------------------------------------------------- /sample_files/1.0.0/core-item-large-thumbnail.json: -------------------------------------------------------------------------------- 1 | { 2 | "stac_version": "1.0.0", 3 | "stac_extensions": [], 4 | "type": "Feature", 5 | "id": "20201211_223832_CS2", 6 | "bbox": [ 7 | 172.91173669923782, 8 | 1.3438851951615003, 9 | 172.95469614953714, 10 | 1.3690476620161975 11 | ], 12 | "geometry": { 13 | "type": "Polygon", 14 | "coordinates": [ 15 | [ 16 | [ 17 | 172.91173669923782, 18 | 1.3438851951615003 19 | ], 20 | [ 21 | 172.95469614953714, 22 | 1.3438851951615003 23 | ], 24 | [ 25 | 172.95469614953714, 26 | 1.3690476620161975 27 | ], 28 | [ 29 | 172.91173669923782, 30 | 1.3690476620161975 31 | ], 32 | [ 33 | 172.91173669923782, 34 | 1.3438851951615003 35 | ] 36 | ] 37 | ] 38 | }, 39 | "properties": { 40 | "title": "Core Item", 41 | "description": "A sample STAC Item that includes examples of all common metadata", 42 | "datetime": null, 43 | "start_datetime": "2020-12-11T22:38:32.125Z", 44 | "end_datetime": "2020-12-11T22:38:32.327Z", 45 | "created": "2020-12-12T01:48:13.725Z", 46 | "updated": "2020-12-12T01:48:13.725Z", 47 | "platform": "cool_sat1", 48 | "instruments": [ 49 | "cool_sensor_v1" 50 | ], 51 | "constellation": "ion", 52 | "mission": "collection 5624", 53 | "gsd": 0.512 54 | }, 55 | "collection": "simple-collection", 56 | "links": [ 57 | { 58 | "rel": "collection", 59 | "href": "./collection.json", 60 | "type": "application/json", 61 | "title": "Simple Example Collection" 62 | }, 63 | { 64 | "rel": "root", 65 | "href": "./collection.json", 66 | "type": "application/json", 67 | "title": "Simple Example Collection" 68 | }, 69 | { 70 | "rel": "parent", 71 | "href": "./collection.json", 72 | "type": "application/json", 73 | "title": "Simple Example Collection" 74 | }, 75 | { 76 | "rel": "alternate", 77 | "type": "text/html", 78 | "href": "http://remotedata.io/catalog/20201211_223832_CS2/index.html", 79 | "title": "HTML version of this STAC Item" 80 | } 81 | ], 82 | "assets": { 83 | "analytic": { 84 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2_analytic.tif", 85 | "type": "image/tiff; application=geotiff; profile=cloud-optimized", 86 | "title": "4-Band Analytic", 87 | "roles": [ 88 | "data" 89 | ] 90 | }, 91 | "thumbnail": { 92 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.jpg", 93 | "title": "Thumbnail", 94 | "type": "image/avi", 95 | "roles": [ 96 | "thumbnail" 97 | ] 98 | }, 99 | "visual": { 100 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.tif", 101 | "type": "image/tiff; application=geotiff; profile=cloud-optimized", 102 | "title": "3-Band Visual", 103 | "roles": [ 104 | "visual" 105 | ] 106 | }, 107 | "udm": { 108 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2_analytic_udm.tif", 109 | "title": "Unusable Data Mask", 110 | "type": "image/tiff; application=geotiff;" 111 | }, 112 | "json-metadata": { 113 | "href": "http://remotedata.io/catalog/20201211_223832_CS2/extended-metadata.json", 114 | "title": "Extended Metadata", 115 | "type": "application/json", 116 | "roles": [ 117 | "metadata" 118 | ] 119 | }, 120 | "ephemeris": { 121 | "href": "http://cool-sat.com/catalog/20201211_223832_CS2/20201211_223832_CS2.EPH", 122 | "title": "Satellite Ephemeris Metadata" 123 | } 124 | } 125 | } -------------------------------------------------------------------------------- /sample_files/1.0.0/core-item-null-datetime.json: -------------------------------------------------------------------------------- 1 | { 2 | "stac_version": "1.0.0", 3 | "stac_extensions": [], 4 | "type": "Feature", 5 | "id": "20201211_223832_CS2", 6 | "bbox": [ 7 | 172.91173669923782, 8 | 1.3438851951615003, 9 | 172.95469614953714, 10 | 1.3690476620161975 11 | ], 12 | "geometry": { 13 | "type": "Polygon", 14 | "coordinates": [ 15 | [ 16 | [ 17 | 172.91173669923782, 18 | 1.3438851951615003 19 | ], 20 | [ 21 | 172.95469614953714, 22 | 1.3438851951615003 23 | ], 24 | [ 25 | 172.95469614953714, 26 | 1.3690476620161975 27 | ], 28 | [ 29 | 172.91173669923782, 30 | 1.3690476620161975 31 | ], 32 | [ 33 | 172.91173669923782, 34 | 1.3438851951615003 35 | ] 36 | ] 37 | ] 38 | }, 39 | "properties": { 40 | "title": "Core Item", 41 | "description": "A sample STAC Item that includes examples of all common metadata", 42 | "datetime": null, 43 | "start_datetime": "2020-12-11T22:38:32.125Z", 44 | "end_datetime": "2020-12-11T22:38:32.327Z", 45 | "created": "2020-12-12T01:48:13.725Z", 46 | "updated": "2020-12-12T01:48:13.725Z", 47 | "platform": "cool_sat1", 48 | "instruments": [ 49 | "cool_sensor_v1" 50 | ], 51 | "constellation": "ion", 52 | "mission": "collection 5624", 53 | "gsd": 0.512 54 | }, 55 | "collection": "simple-collection", 56 | "links": [ 57 | { 58 | "rel": "collection", 59 | "href": "./collection.json", 60 | "type": "application/json", 61 | "title": "Simple Example Collection" 62 | }, 63 | { 64 | "rel": "root", 65 | "href": "./collection.json", 66 | "type": "application/json", 67 | "title": "Simple Example Collection" 68 | }, 69 | { 70 | "rel": "parent", 71 | "href": "./collection.json", 72 | "type": "application/json", 73 | "title": "Simple Example Collection" 74 | }, 75 | { 76 | "rel": "alternate", 77 | "type": "text/html", 78 | "href": "http://remotedata.io/catalog/20201211_223832_CS2/index.html", 79 | "title": "HTML version of this STAC Item" 80 | } 81 | ], 82 | "assets": { 83 | "analytic": { 84 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2_analytic.tif", 85 | "type": "image/tiff; application=geotiff; profile=cloud-optimized", 86 | "title": "4-Band Analytic", 87 | "roles": [ 88 | "data" 89 | ] 90 | }, 91 | "thumbnail": { 92 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.jpg", 93 | "title": "Thumbnail", 94 | "type": "image/png", 95 | "roles": [ 96 | "thumbnail" 97 | ] 98 | }, 99 | "visual": { 100 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.tif", 101 | "type": "image/tiff; application=geotiff; profile=cloud-optimized", 102 | "title": "3-Band Visual", 103 | "roles": [ 104 | "visual" 105 | ] 106 | }, 107 | "udm": { 108 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2_analytic_udm.tif", 109 | "title": "Unusable Data Mask", 110 | "type": "image/tiff; application=geotiff;" 111 | }, 112 | "json-metadata": { 113 | "href": "http://remotedata.io/catalog/20201211_223832_CS2/extended-metadata.json", 114 | "title": "Extended Metadata", 115 | "type": "application/json", 116 | "roles": [ 117 | "metadata" 118 | ] 119 | }, 120 | "ephemeris": { 121 | "href": "http://cool-sat.com/catalog/20201211_223832_CS2/20201211_223832_CS2.EPH", 122 | "title": "Satellite Ephemeris Metadata" 123 | } 124 | } 125 | } -------------------------------------------------------------------------------- /tests/test_lint_assets.py: -------------------------------------------------------------------------------- 1 | from stac_check.lint import Linter 2 | 3 | 4 | def test_lint_assets_no_links(): 5 | file = "sample_files/1.0.0/core-item.json" 6 | linter = Linter(file, assets=True, assets_open_urls=False) 7 | assert linter.message == { 8 | "version": "1.0.0", 9 | "path": file, 10 | "schema": [ 11 | "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json" 12 | ], 13 | "valid_stac": True, 14 | "asset_type": "ITEM", 15 | "validation_method": "default", 16 | "assets_validated": { 17 | "format_valid": [ 18 | "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2_analytic.tif", 19 | "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.jpg", 20 | "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.tif", 21 | "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2_analytic_udm.tif", 22 | "http://remotedata.io/catalog/20201211_223832_CS2/extended-metadata.json", 23 | "http://cool-sat.com/catalog/20201211_223832_CS2/20201211_223832_CS2.EPH", 24 | ], 25 | "format_invalid": [], 26 | "request_valid": [], 27 | "request_invalid": [], 28 | }, 29 | } 30 | 31 | 32 | def test_linter_bad_assets(): 33 | file = "sample_files/1.0.0/core-item-bad-links.json" 34 | linter = Linter(file, assets=True) 35 | asset_format_errors = [ 36 | "https:/storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.jpg" 37 | ] 38 | asset_request_errors = [ 39 | "https:/storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.jpg", 40 | "http://remotedata.io/catalog/20201211_223832_CS2/extended-metadata.json", 41 | "http://cool-sat.com/catalog/20201211_223832_CS2/20201211_223832_CS2.EPH", 42 | ] 43 | assert linter.version == "1.0.0" 44 | assert linter.valid_stac == True 45 | assert linter.asset_type == "ITEM" 46 | assert linter.invalid_asset_format == asset_format_errors 47 | assert linter.invalid_asset_request == asset_request_errors 48 | 49 | 50 | def test_linter_bad_links(): 51 | file = "sample_files/1.0.0/core-item-bad-links.json" 52 | linter = Linter(file, links=True) 53 | link_format_errors = ["http:/remotdata.io/catalog/20201211_223832_CS2/index.html"] 54 | link_request_errors = [ 55 | "http://catalog/collection.json", 56 | "http:/remotdata.io/catalog/20201211_223832_CS2/index.html", 57 | ] 58 | assert linter.version == "1.0.0" 59 | assert linter.valid_stac == True 60 | assert linter.asset_type == "ITEM" 61 | assert len(linter.invalid_link_format) > 0 62 | assert linter.invalid_link_format == link_format_errors 63 | assert linter.invalid_link_request == link_request_errors 64 | 65 | 66 | def test_linter_bad_links_assets(): 67 | file = "sample_files/1.0.0/core-item-bad-links.json" 68 | linter = Linter(file, assets=True, links=True) 69 | asset_format_errors = [ 70 | "https:/storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.jpg" 71 | ] 72 | asset_request_errors = [ 73 | "https:/storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.jpg", 74 | "http://remotedata.io/catalog/20201211_223832_CS2/extended-metadata.json", 75 | "http://cool-sat.com/catalog/20201211_223832_CS2/20201211_223832_CS2.EPH", 76 | ] 77 | link_format_errors = [ 78 | "http:/remotdata.io/catalog/20201211_223832_CS2/index.html", 79 | ] 80 | link_request_errors = [ 81 | "http://catalog/collection.json", 82 | "http:/remotdata.io/catalog/20201211_223832_CS2/index.html", 83 | ] 84 | assert linter.version == "1.0.0" 85 | assert linter.valid_stac == True 86 | assert linter.asset_type == "ITEM" 87 | assert len(linter.invalid_link_format) > 0 88 | assert linter.invalid_asset_format == asset_format_errors 89 | assert linter.invalid_asset_request == asset_request_errors 90 | assert linter.invalid_link_format == link_format_errors 91 | assert linter.invalid_link_request == link_request_errors 92 | -------------------------------------------------------------------------------- /sample_files/1.0.0/collectionless-item.json: -------------------------------------------------------------------------------- 1 | { 2 | "stac_version": "1.0.0", 3 | "stac_extensions": [ 4 | "https://stac-extensions.github.io/eo/v1.0.0/schema.json", 5 | "https://stac-extensions.github.io/view/v1.0.0/schema.json" 6 | ], 7 | "type": "Feature", 8 | "id": "CS3-20160503_132131_08", 9 | "bbox": [ 10 | -122.59750209, 11 | 37.48803556, 12 | -122.2880486, 13 | 37.613537207 14 | ], 15 | "geometry": { 16 | "type": "Polygon", 17 | "coordinates": [ 18 | [ 19 | [ 20 | -122.308150179, 21 | 37.488035566 22 | ], 23 | [ 24 | -122.597502109, 25 | 37.538869539 26 | ], 27 | [ 28 | -122.576687533, 29 | 37.613537207 30 | ], 31 | [ 32 | -122.2880486, 33 | 37.562818007 34 | ], 35 | [ 36 | -122.308150179, 37 | 37.488035566 38 | ] 39 | ] 40 | ] 41 | }, 42 | "properties": { 43 | "title": "Full Item", 44 | "description": "A sample STAC Item demonstrates an Item that does not have a collection, which is not recommended, but allowed by the spec.", 45 | "datetime": null, 46 | "start_datetime": "2016-05-03T13:22:30Z", 47 | "end_datetime": "2016-05-03T13:27:30Z", 48 | "created": "2016-05-04T00:00:01Z", 49 | "updated": "2017-01-01T00:30:55Z", 50 | "license": "various", 51 | "providers": [ 52 | { 53 | "name": "Remote Data, Inc", 54 | "description": "Producers of awesome spatiotemporal assets", 55 | "roles": [ 56 | "producer", 57 | "processor" 58 | ], 59 | "url": "http://remotedata.it" 60 | } 61 | ], 62 | "platform": "cool_sat2", 63 | "instruments": [ 64 | "cool_sensor_v1" 65 | ], 66 | "view:sun_elevation": 33.4, 67 | "gsd": 0.512, 68 | "cs:type": "scene", 69 | "cs:anomalous_pixels": 0.14, 70 | "cs:earth_sun_distance": 1.014156, 71 | "cs:sat_id": "CS3", 72 | "cs:product_level": "LV1B" 73 | }, 74 | "links": [ 75 | { 76 | "rel": "root", 77 | "href": "./catalog.json", 78 | "type": "application/json", 79 | "title": "Example Catalog" 80 | }, 81 | { 82 | "rel": "parent", 83 | "href": "./catalog.json", 84 | "type": "application/json", 85 | "title": "Example Catalog" 86 | }, 87 | { 88 | "rel": "alternate", 89 | "type": "text/html", 90 | "href": "http://cool-sat.com/catalog/CS3-20160503_132130_04/CS3-20160503_132130_04.html", 91 | "title": "HTML representation of this STAC Item" 92 | }, 93 | { 94 | "rel": "license", 95 | "type": "text/html", 96 | "href": "http://remotedata.io/license.html", 97 | "title": "Data License for Remote Data, Inc." 98 | } 99 | ], 100 | "assets": { 101 | "analytic": { 102 | "href": "http://cool-sat.com/catalog/CS3-20160503_132130_04/analytic.tif", 103 | "title": "4-Band Analytic", 104 | "eo:bands": [ 105 | { 106 | "name": "band1" 107 | }, 108 | { 109 | "name": "band1" 110 | }, 111 | { 112 | "name": "band2" 113 | }, 114 | { 115 | "name": "band3" 116 | } 117 | ] 118 | }, 119 | "thumbnail": { 120 | "href": "http://cool-sat.com/catalog/CS3-20160503_132130_04/thumbnail.png", 121 | "title": "Thumbnail", 122 | "type": "image/png", 123 | "roles": [ 124 | "thumbnail" 125 | ] 126 | }, 127 | "udm": { 128 | "href": "http://cool-sat.com/catalog/CS3-20160503_132130_04/UDM.tif", 129 | "title": "Unusable Data Mask" 130 | }, 131 | "json-metadata": { 132 | "href": "http://cool-sat.com/catalog/CS3-20160503_132130_04/extended-metadata.json", 133 | "title": "Extended Metadata", 134 | "type": "application/json", 135 | "roles": [ 136 | "metadata" 137 | ] 138 | }, 139 | "ephemeris": { 140 | "href": "http://cool-sat.com/catalog/CS3-20160503_132130_04/S3-20160503_132130_04.EPH", 141 | "title": "Satellite Ephemeris Metadata" 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /stac_check/utilities.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | from typing import Callable 3 | 4 | import click 5 | 6 | 7 | def determine_asset_type(data): 8 | """Determine the STAC asset type from the given data dictionary. 9 | 10 | This function identifies the type of STAC object based on its structure and content. 11 | It handles all STAC object types including Item, Collection, Catalog, and FeatureCollection. 12 | 13 | Args: 14 | data (dict): The STAC data dictionary 15 | 16 | Returns: 17 | str: The asset type in uppercase (e.g., 'ITEM', 'COLLECTION', 'FEATURECOLLECTION', 'CATALOG') 18 | or an empty string if the type cannot be determined. 19 | """ 20 | if not isinstance(data, dict): 21 | return "" 22 | 23 | # Check for STAC Item types 24 | if data.get("type") == "Feature": 25 | return "ITEM" 26 | elif data.get("type") == "FeatureCollection": 27 | return "FEATURECOLLECTION" 28 | elif data.get("type") == "Collection": 29 | return "COLLECTION" 30 | 31 | # For STAC Catalog/Collection without explicit type or with older STAC versions 32 | if "stac_version" in data and "id" in data: 33 | if data.get("type") == "Catalog": 34 | return "CATALOG" 35 | # If type is not explicitly set, determine based on structure 36 | if "extent" in data and "links" in data: 37 | return "COLLECTION" 38 | return "CATALOG" 39 | 40 | # If we can't determine the type 41 | return "" 42 | 43 | 44 | def handle_output( 45 | output_file: str, callback: Callable[[], None], output_path: str = None 46 | ) -> None: 47 | """Helper function to handle output redirection to a file or stdout. 48 | 49 | Args: 50 | output_file: Path to the output file, or None to use stdout 51 | callback: Function that performs the actual output generation 52 | output_path: Optional path to display in the success message 53 | """ 54 | 55 | if output_file: 56 | with open(output_file, "w") as f: 57 | with contextlib.redirect_stdout(f): 58 | callback() 59 | click.secho( 60 | f"Output written to {output_path or output_file}", 61 | fg="green", 62 | err=True, 63 | bold=True, 64 | ) 65 | click.secho() 66 | else: 67 | callback() 68 | 69 | 70 | def format_verbose_error(error_data): 71 | """Format verbose error data into a human-readable string.""" 72 | if not error_data or not isinstance(error_data, dict): 73 | return str(error_data) 74 | 75 | output = [] 76 | 77 | # Handle validator type 78 | if "validator" in error_data: 79 | output.append(f"Validator: {error_data['validator']}") 80 | 81 | # Handle schema information if available 82 | if "schema" in error_data and error_data["schema"]: 83 | output.append("\nSchema Information:") 84 | if isinstance(error_data["schema"], list): 85 | for schema in error_data["schema"]: 86 | if isinstance(schema, dict): 87 | if "$comment" in schema: 88 | output.append(f"- {schema['$comment']}") 89 | if "required" in schema: 90 | output.append( 91 | f" Required fields: {', '.join(schema['required'])}" 92 | ) 93 | # Handle nested schema requirements 94 | if "properties" in schema and "properties" in schema.get( 95 | "properties", {} 96 | ): 97 | props = schema["properties"]["properties"] 98 | if "allOf" in props: 99 | for item in props["allOf"]: 100 | if "anyOf" in item: 101 | for req in item["anyOf"]: 102 | if "required" in req: 103 | output.append( 104 | f" One of these fields is required: {', '.join(req['required'])}" 105 | ) 106 | 107 | # Handle path information if available 108 | if "path_in_schema" in error_data and error_data["path_in_schema"]: 109 | output.append( 110 | f"\nError Path: {' -> '.join(str(p) for p in error_data['path_in_schema'])}" 111 | ) 112 | 113 | # Handle any other fields we haven't specifically formatted 114 | other_fields = set(error_data.keys()) - { 115 | "validator", 116 | "schema", 117 | "path_in_schema", 118 | "path_in_document", 119 | } 120 | for field in other_fields: 121 | if isinstance(error_data[field], (str, int, float, bool)): 122 | output.append(f"\n{field.replace('_', ' ').title()}: {error_data[field]}") 123 | 124 | return "\n".join(output) 125 | -------------------------------------------------------------------------------- /sample_files/1.0.0/catalog_many_links.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "examples", 3 | "type": "Catalog", 4 | "title": "Example Catalog", 5 | "stac_version": "1.0.0", 6 | "description": "This catalog is a simple demonstration of an example catalog that is used to organize a hierarchy of collections and their items.", 7 | "links": [ 8 | { 9 | "rel": "root", 10 | "href": "./catalog.json", 11 | "type": "application/json" 12 | }, 13 | { 14 | "rel": "child", 15 | "href": "./extensions-collection/collection.json", 16 | "type": "application/json", 17 | "title": "Collection Demonstrating STAC Extensions" 18 | }, 19 | { 20 | "rel": "child", 21 | "href": "./collection-only/collection.json", 22 | "type": "application/json", 23 | "title": "Collection with no items (standalone)" 24 | }, 25 | { 26 | "rel": "child", 27 | "href": "./collection-only/collection-with-schemas.json", 28 | "type": "application/json", 29 | "title": "Collection with no items (standalone with JSON Schemas)" 30 | }, 31 | { 32 | "rel": "item", 33 | "href": "./collectionless-item.json", 34 | "type": "application/json", 35 | "title": "Collection with no items (standalone)" 36 | }, 37 | { 38 | "rel": "self", 39 | "href": "https://raw.githubusercontent.com/radiantearth/stac-spec/v1.0.0/examples/catalog.json", 40 | "type": "application/json" 41 | }, 42 | { 43 | "rel": "root", 44 | "href": "./catalog.json", 45 | "type": "application/json" 46 | }, 47 | { 48 | "rel": "child", 49 | "href": "./extensions-collection/collection.json", 50 | "type": "application/json", 51 | "title": "Collection Demonstrating STAC Extensions" 52 | }, 53 | { 54 | "rel": "child", 55 | "href": "./collection-only/collection.json", 56 | "type": "application/json", 57 | "title": "Collection with no items (standalone)" 58 | }, 59 | { 60 | "rel": "child", 61 | "href": "./collection-only/collection-with-schemas.json", 62 | "type": "application/json", 63 | "title": "Collection with no items (standalone with JSON Schemas)" 64 | }, 65 | { 66 | "rel": "item", 67 | "href": "./collectionless-item.json", 68 | "type": "application/json", 69 | "title": "Collection with no items (standalone)" 70 | }, 71 | { 72 | "rel": "self", 73 | "href": "https://raw.githubusercontent.com/radiantearth/stac-spec/v1.0.0/examples/catalog.json", 74 | "type": "application/json" 75 | }, 76 | { 77 | "rel": "root", 78 | "href": "./catalog.json", 79 | "type": "application/json" 80 | }, 81 | { 82 | "rel": "child", 83 | "href": "./extensions-collection/collection.json", 84 | "type": "application/json", 85 | "title": "Collection Demonstrating STAC Extensions" 86 | }, 87 | { 88 | "rel": "child", 89 | "href": "./collection-only/collection.json", 90 | "type": "application/json", 91 | "title": "Collection with no items (standalone)" 92 | }, 93 | { 94 | "rel": "child", 95 | "href": "./collection-only/collection-with-schemas.json", 96 | "type": "application/json", 97 | "title": "Collection with no items (standalone with JSON Schemas)" 98 | }, 99 | { 100 | "rel": "item", 101 | "href": "./collectionless-item.json", 102 | "type": "application/json", 103 | "title": "Collection with no items (standalone)" 104 | }, 105 | { 106 | "rel": "self", 107 | "href": "https://raw.githubusercontent.com/radiantearth/stac-spec/v1.0.0/examples/catalog.json", 108 | "type": "application/json" 109 | }, 110 | { 111 | "rel": "root", 112 | "href": "./catalog.json", 113 | "type": "application/json" 114 | }, 115 | { 116 | "rel": "child", 117 | "href": "./extensions-collection/collection.json", 118 | "type": "application/json", 119 | "title": "Collection Demonstrating STAC Extensions" 120 | }, 121 | { 122 | "rel": "child", 123 | "href": "./collection-only/collection.json", 124 | "type": "application/json", 125 | "title": "Collection with no items (standalone)" 126 | }, 127 | { 128 | "rel": "child", 129 | "href": "./collection-only/collection-with-schemas.json", 130 | "type": "application/json", 131 | "title": "Collection with no items (standalone with JSON Schemas)" 132 | }, 133 | { 134 | "rel": "item", 135 | "href": "./collectionless-item.json", 136 | "type": "application/json", 137 | "title": "Collection with no items (standalone)" 138 | }, 139 | { 140 | "rel": "self", 141 | "href": "https://raw.githubusercontent.com/radiantearth/stac-spec/v1.0.0/examples/catalog.json", 142 | "type": "application/json" 143 | } 144 | ] 145 | } -------------------------------------------------------------------------------- /sample_files/0.9.0/landsat8-sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "stac_version": "0.9.0", 3 | "stac_extensions": [ 4 | "eo", 5 | "view" 6 | ], 7 | "id": "LC81530252014153LGN00", 8 | "type": "Feature", 9 | "bbox": [ 49.16354,72.27502,51.36812,75.67662 ], 10 | "geometry": { 11 | "type": "Polygon", 12 | "coordinates": [ 13 | [ 14 | [ 15 | 51.33855, 16 | 72.27502 17 | ], 18 | [ 19 | 51.36812, 20 | 75.70821 21 | ], 22 | [ 23 | 49.19092, 24 | 75.67662 25 | ], 26 | [ 27 | 49.16354, 28 | 72.39640 29 | ], 30 | [ 31 | 51.33855, 32 | 72.27502 33 | ] 34 | ] 35 | ] 36 | }, 37 | 38 | "properties": { 39 | "datetime": "2014-06-02T09:22:02Z", 40 | "collection": "L1T", 41 | "eo:gsd": 30.0, 42 | "eo:bands": [1.1], 43 | "eo:cloud_cover" : 10, 44 | "view:off_nadir" : 0.000, 45 | "view:azimuth": 0.00, 46 | "view:sun_azimuth" : 149.01607154, 47 | "view:sun_elevation" : 59.21424700, 48 | "landsat:wrs_path": 153, 49 | "landsat:wrs_row": 25, 50 | "landsat:earth_sun_distance": 1.0141560, 51 | "landsat:ground_control_points_verify": 114, 52 | "landsat:geometric_rmse_model": 7.562, 53 | "landsat:image_quality_tirs": 9, 54 | "landsat:ground_control_points_model": 313, 55 | "landsat:geometric_rmse_model_x": 5.96, 56 | "landsat:geometric_rmse_model_y": 4.654, 57 | "landsat:geometric_rmse_verify": 5.364, 58 | "landsat:image_quality_oli": 9 59 | }, 60 | 61 | "links": [ 62 | { "rel":"self", "href": "http://landsat-pds.s3.amazonaws.com/L8/153/025/LC81530252014153LGN00/LC81530252014153LGN00.json"}, 63 | { "rel":"alternate", "href": "https://landsatonaws.com/L8/153/025/LC81530252014153LGN00", "type": "text/html"}, 64 | { "rel":"root", "href": "http://landsat-pds.s3.amazonaws.com/L8/L1T-collection.json"}, 65 | { "rel":"collection", "href": "http://landsat-pds.s3.amazonaws.com/L8/L1T-collection.json"} 66 | ], 67 | 68 | "assets" :{ 69 | "thumbnail": { 70 | "href": "http://landsat-pds.s3.amazonaws.com/L8/153/025/LC81530252014153LGN00/LC81530252014153LGN00_thumb_large.jpg", 71 | "type": "image/jpeg", 72 | "title": "Thumbnail", 73 | "description": "A medium sized thumbnail", 74 | "roles": [ "thumbnail" ] 75 | }, 76 | "metadata": { 77 | "href": "http://landsat-pds.s3.amazonaws.com/L8/153/025/LC81530252014153LGN00/LC81530252014153LGN00_MTL.txt", 78 | "type": "mtl", 79 | "title": "Original Metadata", 80 | "description": "The original MTL metadata file provided for each Landsat scene", 81 | "roles": ["metadata"] 82 | }, 83 | "B1": { 84 | "href": "http://landsat-pds.s3.amazonaws.com/L8/153/025/LC81530252014153LGN00/LC81530252014153LGN00_B1.TIF", 85 | "type": "image/tiff; application=geotiff", 86 | "eo:bands": [0], 87 | "title": "Coastal Band (B1)", 88 | "description": "Coastal Band Top Of the Atmosphere" 89 | }, 90 | "B2": { 91 | "href": "http://landsat-pds.s3.amazonaws.com/L8/153/025/LC81530252014153LGN00/LC81530252014153LGN00_B2.TIF", 92 | "type": "image/tiff; application=geotiff", 93 | "eo:bands": [1], 94 | "title": "Blue Band (B2)", 95 | "description": "Blue Band Top Of the Atmosphere" 96 | }, 97 | "B3": { 98 | "href": "http://landsat-pds.s3.amazonaws.com/L8/153/025/LC81530252014153LGN00/LC81530252014153LGN00_B3.TIF", 99 | "type": "image/tiff; application=geotiff", 100 | "eo:bands": [2], 101 | "title": "Green Band (B3)", 102 | "description": "Green Band (B3) Top Of the Atmosphere" 103 | }, 104 | "B4": { 105 | "href": "http://landsat-pds.s3.amazonaws.com/L8/153/025/LC81530252014153LGN00/LC81530252014153LGN00_B4.TIF", 106 | "type": "image/tiff; application=geotiff", 107 | "eo:bands": [3], 108 | "title": "Red Band (B4)", 109 | "description": "Red Band (B4) Top Of the Atmosphere" 110 | }, 111 | "B5": { 112 | "href": "http://landsat-pds.s3.amazonaws.com/L8/153/025/LC81530252014153LGN00/LC81530252014153LGN00_B5.TIF", 113 | "type": "image/tiff; application=geotiff", 114 | "eo:bands": [4], 115 | "title": "NIR Band (B5)", 116 | "description": "NIR Band (B5) Top Of the Atmosphere" 117 | }, 118 | "B6": { 119 | "href": "http://landsat-pds.s3.amazonaws.com/L8/153/025/LC81530252014153LGN00/LC81530252014153LGN00_B6.TIF", 120 | "type": "image/tiff; application=geotiff", 121 | "eo:bands": [5], 122 | "title": "SWIR Band (B6)", 123 | "description": "SWIR Band at 1.6um (B6) Top Of the Atmosphere" 124 | }, 125 | "B7": { 126 | "href": "http://landsat-pds.s3.amazonaws.com/L8/153/025/LC81530252014153LGN00/LC81530252014153LGN00_B7.TIF", 127 | "type": "image/tiff; application=geotiff", 128 | "eo:bands": [6], 129 | "title": "SWIR Band (B7)", 130 | "description": "SWIR Band at 2.2um (B7) Top Of the Atmosphere" 131 | }, 132 | "B8": { 133 | "href": "http://landsat-pds.s3.amazonaws.com/L8/153/025/LC81530252014153LGN00/LC81530252014153LGN00_B8.TIF", 134 | "type": "image/tiff; application=geotiff", 135 | "eo:bands": [7], 136 | "title": "Panchromatic Band (B8)", 137 | "description": "Panchromatic Band (B8) Top Of the Atmosphere" 138 | }, 139 | "B9": { 140 | "href": "http://landsat-pds.s3.amazonaws.com/L8/153/025/LC81530252014153LGN00/LC81530252014153LGN00_B9.TIF", 141 | "type": "image/tiff; application=geotiff", 142 | "eo:bands": [8], 143 | "title": "Cirrus Band (B9)", 144 | "description": "Cirrus Band (B9) Top Of the Atmosphere - for cirrus cloud detection" 145 | }, 146 | "B10": { 147 | "href": "http://landsat-pds.s3.amazonaws.com/L8/153/025/LC81530252014153LGN00/LC81530252014153LGN00_B10.TIF", 148 | "type": "image/tiff; application=geotiff", 149 | "eo:bands": [9], 150 | "title": "LWIR Band (B10)", 151 | "description": "Long-wave IR Band at 11um (B10) Top Of the Atmosphere" 152 | }, 153 | "B11": { 154 | "href": "http://landsat-pds.s3.amazonaws.com/L8/153/025/LC81530252014153LGN00/LC81530252014153LGN00_B11.TIF", 155 | "type": "image/tiff; application=geotiff", 156 | "eo:bands": [10], 157 | "title": "LWIR Band (B11)", 158 | "description": "Long-wave IR Band at 12um (B11) Top Of the Atmosphere" 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /sample_files/1.1.0/extended-item.json: -------------------------------------------------------------------------------- 1 | { 2 | "stac_version": "1.1.0", 3 | "stac_extensions": [ 4 | "https://stac-extensions.github.io/eo/v2.0.0/schema.json", 5 | "https://stac-extensions.github.io/projection/v2.0.0/schema.json", 6 | "https://stac-extensions.github.io/scientific/v1.0.0/schema.json", 7 | "https://stac-extensions.github.io/view/v1.0.0/schema.json", 8 | "https://stac-extensions.github.io/remote-data/v1.0.0/schema.json" 9 | ], 10 | "type": "Feature", 11 | "id": "20201211_223832_CS2", 12 | "bbox": [ 13 | 172.91173669923782, 14 | 1.3438851951615003, 15 | 172.95469614953714, 16 | 1.3690476620161975 17 | ], 18 | "geometry": { 19 | "type": "Polygon", 20 | "coordinates": [ 21 | [ 22 | [ 23 | 172.91173669923782, 24 | 1.3438851951615003 25 | ], 26 | [ 27 | 172.95469614953714, 28 | 1.3438851951615003 29 | ], 30 | [ 31 | 172.95469614953714, 32 | 1.3690476620161975 33 | ], 34 | [ 35 | 172.91173669923782, 36 | 1.3690476620161975 37 | ], 38 | [ 39 | 172.91173669923782, 40 | 1.3438851951615003 41 | ] 42 | ] 43 | ] 44 | }, 45 | "properties": { 46 | "title": "Extended Item", 47 | "description": "A sample STAC Item that includes a variety of examples from the stable extensions", 48 | "keywords": [ 49 | "extended", 50 | "example", 51 | "item" 52 | ], 53 | "datetime": "2020-12-14T18:02:31.437000Z", 54 | "created": "2020-12-15T01:48:13.725Z", 55 | "updated": "2020-12-15T01:48:13.725Z", 56 | "platform": "cool_sat2", 57 | "instruments": [ 58 | "cool_sensor_v2" 59 | ], 60 | "gsd": 0.66, 61 | "eo:cloud_cover": 1.2, 62 | "eo:snow_cover": 0, 63 | "statistics": { 64 | "vegetation": 12.57, 65 | "water": 1.23, 66 | "urban": 26.2 67 | }, 68 | "proj:code": "EPSG:32659", 69 | "proj:shape": [ 70 | 5558, 71 | 9559 72 | ], 73 | "proj:transform": [ 74 | 0.5, 75 | 0, 76 | 712710, 77 | 0, 78 | -0.5, 79 | 151406, 80 | 0, 81 | 0, 82 | 1 83 | ], 84 | "view:sun_elevation": 54.9, 85 | "view:off_nadir": 3.8, 86 | "view:sun_azimuth": 135.7, 87 | "rd:type": "scene", 88 | "rd:anomalous_pixels": 0.14, 89 | "rd:earth_sun_distance": 1.014156, 90 | "rd:sat_id": "cool_sat2", 91 | "rd:product_level": "LV3A", 92 | "sci:doi": "10.5061/dryad.s2v81.2/27.2" 93 | }, 94 | "collection": "simple-collection", 95 | "links": [ 96 | { 97 | "rel": "collection", 98 | "href": "./collection.json", 99 | "type": "application/json", 100 | "title": "Simple Example Collection" 101 | }, 102 | { 103 | "rel": "root", 104 | "href": "./collection.json", 105 | "type": "application/json", 106 | "title": "Simple Example Collection" 107 | }, 108 | { 109 | "rel": "parent", 110 | "href": "./collection.json", 111 | "type": "application/json", 112 | "title": "Simple Example Collection" 113 | }, 114 | { 115 | "rel": "alternate", 116 | "type": "text/html", 117 | "href": "http://remotedata.io/catalog/20201211_223832_CS2/index.html", 118 | "title": "HTML version of this STAC Item" 119 | } 120 | ], 121 | "assets": { 122 | "analytic": { 123 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2_analytic.tif", 124 | "type": "image/tiff; application=geotiff; profile=cloud-optimized", 125 | "title": "4-Band Analytic", 126 | "roles": [ 127 | "data" 128 | ], 129 | "bands": [ 130 | { 131 | "name": "band1", 132 | "eo:common_name": "blue", 133 | "eo:center_wavelength": 0.47, 134 | "eo:full_width_half_max": 70 135 | }, 136 | { 137 | "name": "band2", 138 | "eo:common_name": "green", 139 | "eo:center_wavelength": 0.56, 140 | "eo:full_width_half_max": 80 141 | }, 142 | { 143 | "name": "band3", 144 | "eo:common_name": "red", 145 | "eo:center_wavelength": 0.645, 146 | "eo:full_width_half_max": 90 147 | }, 148 | { 149 | "name": "band4", 150 | "eo:common_name": "nir", 151 | "eo:center_wavelength": 0.8, 152 | "eo:full_width_half_max": 152 153 | } 154 | ] 155 | }, 156 | "thumbnail": { 157 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.jpg", 158 | "title": "Thumbnail", 159 | "type": "image/png", 160 | "roles": [ 161 | "thumbnail" 162 | ] 163 | }, 164 | "visual": { 165 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.tif", 166 | "type": "image/tiff; application=geotiff; profile=cloud-optimized", 167 | "title": "3-Band Visual", 168 | "roles": [ 169 | "visual" 170 | ], 171 | "bands": [ 172 | { 173 | "name": "band3", 174 | "eo:common_name": "red", 175 | "eo:center_wavelength": 0.645, 176 | "eo:full_width_half_max": 90 177 | }, 178 | { 179 | "name": "band2", 180 | "eo:common_name": "green", 181 | "eo:center_wavelength": 0.56, 182 | "eo:full_width_half_max": 80 183 | }, 184 | { 185 | "name": "band1", 186 | "eo:common_name": "blue", 187 | "eo:center_wavelength": 0.47, 188 | "eo:full_width_half_max": 70 189 | } 190 | ] 191 | }, 192 | "udm": { 193 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2_analytic_udm.tif", 194 | "title": "Unusable Data Mask", 195 | "type": "image/tiff; application=geotiff" 196 | }, 197 | "json-metadata": { 198 | "href": "http://remotedata.io/catalog/20201211_223832_CS2/extended-metadata.json", 199 | "title": "Extended Metadata", 200 | "type": "application/json", 201 | "roles": [ 202 | "metadata" 203 | ] 204 | }, 205 | "ephemeris": { 206 | "href": "http://cool-sat.com/catalog/20201211_223832_CS2/20201211_223832_CS2.EPH", 207 | "title": "Satellite Ephemeris Metadata" 208 | } 209 | } 210 | } -------------------------------------------------------------------------------- /tests/test_lint.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import requests_mock 4 | 5 | from stac_check.lint import Linter 6 | 7 | 8 | def test_linter_collection(): 9 | file = "sample_files/1.0.0/collection.json" 10 | linter = Linter(file, assets=False, links=False) 11 | assert linter.version == "1.0.0" 12 | assert linter.valid_stac == True 13 | assert linter.asset_type == "COLLECTION" 14 | assert linter.check_summaries() == True 15 | 16 | 17 | def test_linter_collection_no_summaries(): 18 | file = "sample_files/1.0.0/collection-no-summaries.json" 19 | linter = Linter(file, assets=False, links=False) 20 | assert linter.version == "1.0.0" 21 | assert linter.valid_stac == True 22 | assert linter.asset_type == "COLLECTION" 23 | assert linter.check_summaries() == False 24 | assert linter.best_practices_msg == [ 25 | "STAC Best Practices: ", 26 | "Object should be called 'collection.json' not 'collection-no-summaries.json'", 27 | "", 28 | "A STAC collection should contain a summaries field", 29 | "It is recommended to store information like eo:bands in summaries", 30 | "", 31 | ] 32 | 33 | 34 | def test_linter_catalog(): 35 | file = "sample_files/1.0.0/catalog.json" 36 | linter = Linter(file, assets=False, links=False) 37 | assert linter.version == "1.0.0" 38 | assert linter.valid_stac == True 39 | assert linter.asset_type == "CATALOG" 40 | assert linter.check_bloated_links() == False 41 | 42 | 43 | def test_linter_item_id_not_matching_file_name(): 44 | file = "sample_files/1.0.0/core-item.json" 45 | linter = Linter(file) 46 | assert linter.file_name == "core-item" 47 | assert linter.object_id == "20201211_223832_CS2" 48 | assert linter.file_name != linter.object_id 49 | assert linter.check_item_id_file_name() == False 50 | 51 | 52 | def test_linter_collection_catalog_id(): 53 | file = "sample_files/1.0.0/collection-no-title.json" 54 | linter = Linter(file) 55 | assert linter.check_catalog_file_name() == False 56 | 57 | 58 | def test_linter_item_id_format_best_practices(): 59 | file = "sample_files/1.0.0/core-item-invalid-id.json" 60 | linter = Linter(file) 61 | assert linter.check_searchable_identifiers() == False 62 | assert linter.check_percent_encoded() == True 63 | 64 | 65 | def test_datetime_set_to_null(): 66 | file = "sample_files/1.0.0/core-item-null-datetime.json" 67 | linter = Linter(file) 68 | assert linter.check_datetime_null() == True 69 | 70 | 71 | def test_unlocated_item(): 72 | file = "sample_files/1.0.0/core-item-unlocated.json" 73 | linter = Linter(file) 74 | assert linter.check_unlocated() == True 75 | assert linter.check_geometry_null() == True 76 | 77 | file = "sample_files/1.0.0/core-item-unlocated-null-bbox.json" 78 | linter = Linter(file) 79 | assert linter.check_unlocated() == False 80 | assert linter.check_geometry_null() == True 81 | 82 | 83 | def test_bloated_item(): 84 | file = "sample_files/1.0.0/core-item-bloated.json" 85 | linter = Linter(file) 86 | 87 | assert linter.check_bloated_metadata() == True 88 | assert len(linter.data["properties"]) > 20 89 | 90 | assert linter.check_bloated_links() == True 91 | assert len(linter.data["links"]) > 20 92 | 93 | 94 | def test_small_thumbnail(): 95 | file = "sample_files/1.0.0/core-item-large-thumbnail.json" 96 | linter = Linter(file) 97 | 98 | assert linter.check_thumbnail() != True 99 | 100 | file = "sample_files/1.0.0/core-item.json" 101 | linter = Linter(file) 102 | 103 | assert linter.check_thumbnail() == True 104 | 105 | 106 | def test_title_field(): 107 | file = "sample_files/1.0.0/collection-no-title.json" 108 | linter = Linter(file) 109 | 110 | assert linter.check_links_title_field() == False 111 | 112 | 113 | def test_self_in_links(): 114 | file = "sample_files/1.0.0/collection-no-title.json" 115 | linter = Linter(file) 116 | assert linter.check_links_self() == False 117 | 118 | 119 | def test_catalog_name(): 120 | file = "sample_files/1.0.0/catalog.json" 121 | linter = Linter(file) 122 | assert linter.check_catalog_file_name() 123 | file = "sample_files/1.0.0/collection.json" 124 | linter = Linter(file) 125 | assert linter.check_catalog_file_name() 126 | 127 | 128 | def test_lint_header(): 129 | file = "sample_files/1.0.0/core-item.json" 130 | url = "https://localhost/" + file 131 | 132 | no_headers = {} 133 | valid_headers = {"x-api-key": "a-valid-api-key"} 134 | 135 | with requests_mock.Mocker(real_http=True) as mock, open(file) as json_data: 136 | mock.get(url, request_headers=no_headers, status_code=403, json={}) 137 | mock.get(url, request_headers=valid_headers, json=json.load(json_data)) 138 | 139 | linter = Linter(url, assets=False, headers=valid_headers) 140 | assert linter.message == { 141 | "version": "1.0.0", 142 | "path": "https://localhost/sample_files/1.0.0/core-item.json", 143 | "schema": [ 144 | "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json" 145 | ], 146 | "valid_stac": True, 147 | "asset_type": "ITEM", 148 | "validation_method": "default", 149 | } 150 | 151 | linter = Linter(url, assets=False, headers=no_headers) 152 | msg = linter.message 153 | assert msg["valid_stac"] is False 154 | assert msg["error_type"] == "HTTPError" 155 | # Accept either 'message' or 'error_message' as the error string 156 | error_msg = msg.get("error_message") or msg.get("message") 157 | assert ( 158 | error_msg 159 | == "403 Client Error: None for url: https://localhost/sample_files/1.0.0/core-item.json" 160 | ) 161 | # Optionally check path, version, schema if present 162 | if "path" in msg: 163 | assert msg["path"] == "https://localhost/sample_files/1.0.0/core-item.json" 164 | 165 | 166 | def test_verbose_error_message(): 167 | """Test that verbose error messages are properly formatted and included.""" 168 | # Test with a known bad item that will generate validation errors 169 | file = "sample_files/1.0.0/bad-item.json" 170 | linter = Linter(file, verbose=True) 171 | 172 | # Verify the item is invalid 173 | assert linter.valid_stac is False 174 | 175 | # Check that we have the expected error message 176 | assert "id" in linter.error_msg.lower() 177 | assert "required" in linter.error_msg.lower() 178 | 179 | # Check that verbose error message contains expected structure 180 | assert isinstance(linter.verbose_error_msg, dict) 181 | assert "validator" in linter.verbose_error_msg 182 | assert "path_in_schema" in linter.verbose_error_msg 183 | 184 | # Check specific parts of the verbose error message 185 | assert linter.verbose_error_msg.get("validator") == "required" 186 | 187 | # Check path_in_schema - it might contain both strings and integers 188 | path_in_schema = linter.verbose_error_msg.get("path_in_schema", []) 189 | assert any(isinstance(p, (str, int)) for p in path_in_schema) 190 | 191 | # Check that the error message is included in the string representation 192 | verbose_str = str(linter.verbose_error_msg) 193 | assert "required" in verbose_str 194 | assert "id" in verbose_str 195 | -------------------------------------------------------------------------------- /stac_check/cli.py: -------------------------------------------------------------------------------- 1 | import importlib.metadata 2 | import sys 3 | from typing import Optional 4 | 5 | import click 6 | 7 | from stac_check.api_lint import ApiLinter 8 | from stac_check.display_messages import ( 9 | cli_message, 10 | collections_message, 11 | intro_message, 12 | item_collection_message, 13 | recursive_message, 14 | ) 15 | from stac_check.lint import Linter 16 | from stac_check.utilities import handle_output 17 | 18 | 19 | @click.option( 20 | "--collections", 21 | is_flag=True, 22 | help="Validate collections endpoint response. Can be combined with --pages. Defaults to one page.", 23 | ) 24 | @click.option( 25 | "--item-collection", 26 | is_flag=True, 27 | help="Validate item collection response. Can be combined with --pages. Defaults to one page.", 28 | ) 29 | @click.option( 30 | "--pages", 31 | "-p", 32 | type=int, 33 | help="Maximum number of pages to validate via --item-collection or --collections. Defaults to one page.", 34 | ) 35 | @click.option( 36 | "--recursive", 37 | "-r", 38 | is_flag=True, 39 | help="Recursively validate all related stac objects.", 40 | ) 41 | @click.option( 42 | "--max-depth", 43 | "-m", 44 | type=int, 45 | help="Maximum depth to traverse when recursing. Omit this argument to get full recursion. Ignored if `recursive == False`.", 46 | ) 47 | @click.option( 48 | "-a", "--assets", is_flag=True, help="Validate assets for format and response." 49 | ) 50 | @click.option( 51 | "-l", "--links", is_flag=True, help="Validate links for format and response." 52 | ) 53 | @click.option( 54 | "--output", 55 | "-o", 56 | type=click.Path(dir_okay=False, writable=True), 57 | help="Save output to the specified file. Only works with --collections, --item-collection, or --recursive.", 58 | ) 59 | @click.option( 60 | "--no-assets-urls", 61 | is_flag=True, 62 | help="Disables the opening of href links when validating assets (enabled by default).", 63 | ) 64 | @click.option( 65 | "--header", 66 | type=(str, str), 67 | multiple=True, 68 | help="HTTP header to include in the requests. Can be used multiple times.", 69 | ) 70 | @click.option( 71 | "--pydantic", 72 | is_flag=True, 73 | help="Use pydantic validation (requires stac-pydantic to be installed).", 74 | ) 75 | @click.option( 76 | "--verbose", 77 | "-v", 78 | is_flag=True, 79 | help="Enable verbose output.", 80 | ) 81 | @click.command() 82 | @click.argument("file") 83 | @click.version_option(version=importlib.metadata.distribution("stac-check").version) 84 | def main( 85 | file: str, 86 | collections: bool, 87 | item_collection: bool, 88 | pages: Optional[int], 89 | recursive: bool, 90 | max_depth: Optional[int], 91 | assets: bool, 92 | links: bool, 93 | no_assets_urls: bool, 94 | header: tuple[tuple[str, str], ...], 95 | pydantic: bool, 96 | verbose: bool, 97 | output: Optional[str], 98 | ) -> None: 99 | """Main entry point for the stac-check CLI. 100 | 101 | Args: 102 | file: The STAC file or URL to validate 103 | collections: Validate a collections endpoint 104 | item_collection: Validate an item collection 105 | pages: Number of pages to validate (for API endpoints) 106 | recursive: Recursively validate linked STAC objects 107 | max_depth: Maximum depth for recursive validation 108 | assets: Validate assets 109 | links: Validate links 110 | no_assets_urls: Disable URL validation for assets 111 | header: Additional HTTP headers 112 | pydantic: Use stac-pydantic for validation 113 | verbose: Show verbose output 114 | output: Save output to file (only with --collections, --item-collection, or --recursive) 115 | """ 116 | # Check if output is used without --collections, --item-collection, or --recursive 117 | if output and not any([collections, item_collection, recursive]): 118 | click.echo( 119 | "Error: --output can only be used with --collections, --item-collection, or --recursive", 120 | err=True, 121 | ) 122 | sys.exit(1) 123 | # Check if pydantic validation is requested but not installed 124 | if pydantic: 125 | try: 126 | importlib.import_module("stac_pydantic") 127 | except ImportError: 128 | click.secho( 129 | "Warning: stac-pydantic is not installed. Pydantic validation will be disabled.\n" 130 | "To enable pydantic validation, install it with: pip install stac-check[pydantic]", 131 | fg="yellow", 132 | ) 133 | pydantic = False 134 | 135 | if collections or item_collection: 136 | # Handle API-based validation (collections or item collections) 137 | api_linter = ApiLinter( 138 | source=file, 139 | object_list_key="collections" if collections else "features", 140 | pages=pages if pages else 1, 141 | headers=dict(header), 142 | verbose=verbose, 143 | ) 144 | results = api_linter.lint_all() 145 | 146 | # Create a dummy Linter instance for display purposes 147 | display_linter = Linter( 148 | file, 149 | assets=assets, 150 | links=links, 151 | headers=dict(header), 152 | pydantic=pydantic, 153 | verbose=verbose, 154 | ) 155 | 156 | # Show intro message in the terminal 157 | intro_message(display_linter) 158 | 159 | # Define output generation function (without intro message since we already showed it) 160 | def generate_output(): 161 | if collections: 162 | collections_message( 163 | api_linter, 164 | results=results, 165 | cli_message_func=cli_message, 166 | verbose=verbose, 167 | ) 168 | elif item_collection: 169 | item_collection_message( 170 | api_linter, 171 | results=results, 172 | cli_message_func=cli_message, 173 | verbose=verbose, 174 | ) 175 | 176 | # Handle output (without duplicating the intro message) 177 | handle_output(output, generate_output) 178 | sys.exit(0 if all(msg.get("valid_stac") is True for msg in results) else 1) 179 | else: 180 | # Handle file-based validation (single file or recursive) 181 | linter = Linter( 182 | file, 183 | assets=assets, 184 | links=links, 185 | recursive=recursive, 186 | max_depth=max_depth, 187 | assets_open_urls=not no_assets_urls, 188 | headers=dict(header), 189 | pydantic=pydantic, 190 | verbose=verbose, 191 | ) 192 | 193 | intro_message(linter) 194 | 195 | # Define output generation function (without intro message since we already showed it) 196 | def generate_output(): 197 | if recursive: 198 | recursive_message(linter, cli_message_func=cli_message, verbose=verbose) 199 | else: 200 | cli_message(linter) 201 | 202 | # Handle output (without duplicating the intro message) 203 | handle_output(output if recursive else None, generate_output) 204 | 205 | sys.exit(0 if linter.valid_stac else 1) 206 | -------------------------------------------------------------------------------- /tests/test_lint_dictionary.py: -------------------------------------------------------------------------------- 1 | from stac_check.lint import Linter 2 | 3 | 4 | def test_lint_dict_collection(): 5 | file = { 6 | "id": "simple-collection", 7 | "type": "Collection", 8 | "stac_extensions": [ 9 | "https://stac-extensions.github.io/eo/v1.0.0/schema.json", 10 | "https://stac-extensions.github.io/projection/v1.0.0/schema.json", 11 | "https://stac-extensions.github.io/view/v1.0.0/schema.json", 12 | ], 13 | "stac_version": "1.0.0", 14 | "description": "A simple collection demonstrating core catalog fields with links to a couple of items", 15 | "title": "Simple Example Collection", 16 | "providers": [ 17 | { 18 | "name": "Remote Data, Inc", 19 | "description": "Producers of awesome spatiotemporal assets", 20 | "roles": ["producer", "processor"], 21 | "url": "http://remotedata.io", 22 | } 23 | ], 24 | "extent": { 25 | "spatial": { 26 | "bbox": [ 27 | [ 28 | 172.91173669923782, 29 | 1.3438851951615003, 30 | 172.95469614953714, 31 | 1.3690476620161975, 32 | ] 33 | ] 34 | }, 35 | "temporal": { 36 | "interval": [["2020-12-11T22:38:32.125Z", "2020-12-14T18:02:31.437Z"]] 37 | }, 38 | }, 39 | "license": "CC-BY-4.0", 40 | "summaries": { 41 | "platform": ["cool_sat1", "cool_sat2"], 42 | "constellation": ["ion"], 43 | "instruments": ["cool_sensor_v1", "cool_sensor_v2"], 44 | "gsd": {"minimum": 0.512, "maximum": 0.66}, 45 | "eo:cloud_cover": {"minimum": 1.2, "maximum": 1.2}, 46 | "proj:epsg": {"minimum": 32659, "maximum": 32659}, 47 | "view:sun_elevation": {"minimum": 54.9, "maximum": 54.9}, 48 | "view:off_nadir": {"minimum": 3.8, "maximum": 3.8}, 49 | "view:sun_azimuth": {"minimum": 135.7, "maximum": 135.7}, 50 | }, 51 | "links": [ 52 | { 53 | "rel": "root", 54 | "href": "./collection.json", 55 | "type": "application/json", 56 | "title": "Simple Example Collection", 57 | }, 58 | { 59 | "rel": "item", 60 | "href": "./simple-item.json", 61 | "type": "application/geo+json", 62 | "title": "Simple Item", 63 | }, 64 | {"rel": "item", "href": "./core-item.json", "type": "application/geo+json"}, 65 | { 66 | "rel": "item", 67 | "href": "./extended-item.json", 68 | "type": "application/geo+json", 69 | "title": "Extended Item", 70 | }, 71 | ], 72 | } 73 | linter = Linter(file) 74 | assert linter.valid_stac == True 75 | assert linter.asset_type == "COLLECTION" 76 | assert linter.check_catalog_file_name() == True 77 | 78 | 79 | def test_lint_dict_item(): 80 | file = { 81 | "stac_version": "1.0.0", 82 | "stac_extensions": [], 83 | "type": "Feature", 84 | "id": "20201211_223832_CS2", 85 | "bbox": [ 86 | 172.91173669923782, 87 | 1.3438851951615003, 88 | 172.95469614953714, 89 | 1.3690476620161975, 90 | ], 91 | "geometry": { 92 | "type": "Polygon", 93 | "coordinates": [ 94 | [ 95 | [172.91173669923782, 1.3438851951615003], 96 | [172.95469614953714, 1.3438851951615003], 97 | [172.95469614953714, 1.3690476620161975], 98 | [172.91173669923782, 1.3690476620161975], 99 | [172.91173669923782, 1.3438851951615003], 100 | ] 101 | ], 102 | }, 103 | "properties": { 104 | "title": "Core Item", 105 | "description": "A sample STAC Item that includes examples of all common metadata", 106 | "datetime": None, 107 | "start_datetime": "2020-12-11T22:38:32.125Z", 108 | "end_datetime": "2020-12-11T22:38:32.327Z", 109 | "created": "2020-12-12T01:48:13.725Z", 110 | "updated": "2020-12-12T01:48:13.725Z", 111 | "platform": "cool_sat1", 112 | "instruments": ["cool_sensor_v1"], 113 | "constellation": "ion", 114 | "mission": "collection 5624", 115 | "gsd": 0.512, 116 | }, 117 | "collection": "simple-collection", 118 | "links": [ 119 | { 120 | "rel": "collection", 121 | "href": "./collection.json", 122 | "type": "application/json", 123 | "title": "Simple Example Collection", 124 | }, 125 | { 126 | "rel": "root", 127 | "href": "./collection.json", 128 | "type": "application/json", 129 | "title": "Simple Example Collection", 130 | }, 131 | { 132 | "rel": "parent", 133 | "href": "./collection.json", 134 | "type": "application/json", 135 | "title": "Simple Example Collection", 136 | }, 137 | { 138 | "rel": "alternate", 139 | "type": "text/html", 140 | "href": "http://remotedata.io/catalog/20201211_223832_CS2/index.html", 141 | "title": "HTML version of this STAC Item", 142 | }, 143 | ], 144 | "assets": { 145 | "analytic": { 146 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2_analytic.tif", 147 | "type": "image/tiff; application=geotiff; profile=cloud-optimized", 148 | "title": "4-Band Analytic", 149 | "roles": ["data"], 150 | }, 151 | "thumbnail": { 152 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.jpg", 153 | "title": "Thumbnail", 154 | "type": "image/png", 155 | "roles": ["thumbnail"], 156 | }, 157 | "visual": { 158 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.tif", 159 | "type": "image/tiff; application=geotiff; profile=cloud-optimized", 160 | "title": "3-Band Visual", 161 | "roles": ["visual"], 162 | }, 163 | "udm": { 164 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2_analytic_udm.tif", 165 | "title": "Unusable Data Mask", 166 | "type": "image/tiff; application=geotiff;", 167 | }, 168 | "json-metadata": { 169 | "href": "http://remotedata.io/catalog/20201211_223832_CS2/extended-metadata.json", 170 | "title": "Extended Metadata", 171 | "type": "application/json", 172 | "roles": ["metadata"], 173 | }, 174 | "ephemeris": { 175 | "href": "http://cool-sat.com/catalog/20201211_223832_CS2/20201211_223832_CS2.EPH", 176 | "title": "Satellite Ephemeris Metadata", 177 | }, 178 | }, 179 | } 180 | linter = Linter(file) 181 | assert linter.valid_stac == True 182 | assert linter.asset_type == "ITEM" 183 | assert linter.check_datetime_null() == True 184 | assert linter.create_best_practices_dict()["datetime_null"] == [ 185 | "Please avoid setting the datetime field to null, many clients search on this field" 186 | ] 187 | -------------------------------------------------------------------------------- /stac_check/api_lint.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Dict, Generator, List, Optional, Tuple 3 | from urllib.parse import urlparse, urlunparse 4 | 5 | from stac_validator.utilities import fetch_and_parse_file 6 | 7 | from stac_check.lint import Linter 8 | 9 | 10 | @dataclass 11 | class ApiLinter: 12 | """A class for linting paginated STAC endpoints or static files. 13 | 14 | This linter handles item collections, collections, and other STAC endpoints, 15 | with support for pagination and HTTP headers. 16 | 17 | Args: 18 | source (str): URL or file path of the STAC endpoint or static file 19 | (e.g. /items, /collections, or local JSON). 20 | object_list_key (str): Key in response containing list of objects 21 | (e.g. "features", "collections"). 22 | pages (int): Number of pages to fetch. Defaults to 1. 23 | headers (Optional[Dict], optional): Optional headers for HTTP requests. Defaults to None. 24 | 25 | Attributes: 26 | source (str): The source URL or file path. 27 | object_list_key (str): The key for the list of objects in the response. 28 | pages (int): Maximum number of pages to process. 29 | headers (Dict): HTTP headers for requests. 30 | version (Optional[str]): STAC version detected from validated objects. 31 | validator_version (str): Version of stac-validator being used. 32 | """ 33 | 34 | def __init__( 35 | self, 36 | source: str, 37 | object_list_key: str, 38 | pages: Optional[int] = 1, 39 | headers: Optional[Dict] = None, 40 | verbose: bool = False, 41 | ): 42 | self.source = source 43 | self.object_list_key = object_list_key 44 | self.pages = pages if pages is not None else 1 45 | self.headers = headers or {} 46 | self.verbose = verbose 47 | self.version = None 48 | self.validator_version = self._get_validator_version() 49 | 50 | def _get_validator_version(self) -> str: 51 | """Get the version of stac-validator being used. 52 | 53 | Returns: 54 | str: The version string of stac-validator, or "unknown" if not available. 55 | """ 56 | try: 57 | import stac_validator 58 | 59 | return getattr(stac_validator, "__version__", "unknown") 60 | except ImportError: 61 | return "unknown" 62 | 63 | def set_update_message(self) -> str: 64 | """Generate a message for users about their STAC version. 65 | 66 | Returns: 67 | str: A string containing a message about the current STAC version 68 | and recommendation to update if needed. 69 | """ 70 | if not self.version: 71 | return "Please upgrade to STAC version 1.1.0!" 72 | elif self.version != "1.1.0": 73 | return f"Please upgrade from version {self.version} to version 1.1.0!" 74 | else: 75 | return "Thanks for using STAC version 1.1.0!" 76 | 77 | def _fetch_and_parse(self, url: str) -> Dict: 78 | """Fetch and parse a STAC file from a URL. 79 | 80 | Args: 81 | url (str): The URL to fetch the STAC file from. 82 | 83 | Returns: 84 | Dict: The parsed STAC file as a dictionary. 85 | """ 86 | return fetch_and_parse_file(url, self.headers) 87 | 88 | def iterate_objects(self) -> Generator[Tuple[Dict, str], None, None]: 89 | """Iterate through all objects in the endpoint, following pagination if necessary. 90 | 91 | This generator yields each object in the endpoint along with its URL. 92 | It handles pagination by following "next" links and prevents duplicate objects 93 | by tracking seen IDs. 94 | 95 | Yields: 96 | Tuple[Dict, str]: A tuple containing (object_dict, object_url) for each object. 97 | """ 98 | stac_file = self.source 99 | page = 1 100 | seen_ids = set() 101 | 102 | def get_base_url(url: str) -> str: 103 | """Extract the base URL without query parameters or fragments. 104 | 105 | Args: 106 | url (str): The full URL to process. 107 | 108 | Returns: 109 | str: The base URL without query parameters or fragments. 110 | """ 111 | parsed = urlparse(url) 112 | return urlunparse(parsed._replace(query="", fragment="")) 113 | 114 | while stac_file: 115 | response = self._fetch_and_parse(stac_file) 116 | objects = response.get(self.object_list_key, []) 117 | for obj in objects: 118 | obj_id = obj.get("id") 119 | base_url = get_base_url(stac_file) 120 | obj_url = f"{base_url}/{obj_id}" if obj_id else base_url 121 | # Only yield if not seen before (protects against duplicates from bad APIs) 122 | if obj_id not in seen_ids: 123 | seen_ids.add(obj_id) 124 | yield obj, obj_url 125 | # Pagination: look for 'next' link 126 | next_link = None 127 | for link in response.get("links", []): 128 | if link.get("rel") == "next": 129 | next_link = link.get("href") 130 | break 131 | # Check if we should continue to the next page 132 | if next_link and next_link != stac_file and page < self.pages: 133 | stac_file = next_link 134 | page += 1 135 | else: 136 | break 137 | 138 | def lint_all(self) -> List[Dict]: 139 | """Lint all objects in the endpoint, handling pagination if configured. 140 | 141 | This method processes all objects in the endpoint (up to the specified number of pages), 142 | validates each object using the Linter class, and collects the results. 143 | It ensures only one result per asset URL and preserves the original object 144 | for potential further processing. 145 | 146 | Returns: 147 | List[Dict]: A list of validation result dictionaries, one per object, 148 | matching the message structure of the Linter class. 149 | """ 150 | results_by_url = {} 151 | for obj, obj_url in self.iterate_objects(): 152 | try: 153 | linter = Linter(obj, verbose=self.verbose) 154 | msg = dict(linter.message) 155 | msg["path"] = obj_url 156 | msg["best_practices"] = linter.best_practices_msg 157 | msg["geometry_errors"] = linter.geometry_errors_msg 158 | # Store the original object to allow recreation of Linter instance later 159 | msg["original_object"] = obj 160 | results_by_url[obj_url] = msg 161 | 162 | # Set the version from the first valid STAC object if not already set 163 | if self.version is None: 164 | # Get version from the validation message 165 | stac_version = msg.get("version") 166 | if stac_version: 167 | self.version = stac_version 168 | except Exception as e: 169 | results_by_url[obj_url] = { 170 | "path": obj_url, 171 | "valid_stac": False, 172 | "error_type": type(e).__name__, 173 | "error_message": str(e), 174 | "best_practices": [], 175 | "geometry_errors": [], 176 | "version": None, 177 | "schema": [], 178 | "recommendation": None, 179 | "error_verbose": None, 180 | "failed_schema": None, 181 | "original_object": obj, # Still include the original object even if validation failed 182 | } 183 | return list(results_by_url.values()) 184 | -------------------------------------------------------------------------------- /tests/test_cli.py: -------------------------------------------------------------------------------- 1 | """Tests for the stac-check CLI.""" 2 | 3 | import os 4 | import tempfile 5 | from unittest.mock import MagicMock, patch 6 | 7 | import pytest 8 | from click.testing import CliRunner 9 | 10 | from stac_check.cli import main as cli_main 11 | 12 | 13 | @pytest.fixture 14 | def runner(): 15 | """Fixture for invoking command-line interfaces.""" 16 | return CliRunner() 17 | 18 | 19 | def test_cli_help(runner): 20 | """Test the CLI help output.""" 21 | result = runner.invoke(cli_main, ["--help"]) 22 | assert result.exit_code == 0 23 | assert "Show this message and exit." in result.output 24 | 25 | 26 | def test_cli_version(runner): 27 | """Test the --version flag.""" 28 | result = runner.invoke(cli_main, ["--version"]) 29 | assert result.exit_code == 0 30 | # The version output is in the format: main, version X.Y.Z 31 | assert "version" in result.output 32 | 33 | 34 | def test_cli_validate_local_file(runner): 35 | """Test validating a local file.""" 36 | test_file = os.path.join( 37 | os.path.dirname(__file__), "../sample_files/1.0.0/core-item.json" 38 | ) 39 | result = runner.invoke(cli_main, [test_file]) 40 | assert result.exit_code == 0 41 | assert "Passed: True" in result.output 42 | 43 | 44 | def test_cli_validate_recursive(runner): 45 | """Test recursive validation.""" 46 | test_dir = os.path.join( 47 | os.path.dirname(__file__), "../sample_files/1.0.0/catalog-with-bad-item.json" 48 | ) 49 | result = runner.invoke(cli_main, [test_dir, "--recursive"]) 50 | assert result.exit_code == 0 51 | assert "Assets Checked" in result.output 52 | 53 | 54 | def test_cli_output_to_file(runner): 55 | """Test output to file with --output flag.""" 56 | test_file = os.path.join( 57 | os.path.dirname(__file__), "../sample_files/1.0.0/core-item.json" 58 | ) 59 | with tempfile.NamedTemporaryFile(delete=False) as tmp: 60 | output_file = tmp.name 61 | 62 | try: 63 | result = runner.invoke( 64 | cli_main, [test_file, "--recursive", "--output", output_file] 65 | ) 66 | assert result.exit_code == 0 67 | assert os.path.exists(output_file) 68 | with open(output_file, "r") as f: 69 | content = f.read() 70 | assert "Passed: True" in content 71 | finally: 72 | if os.path.exists(output_file): 73 | os.unlink(output_file) 74 | 75 | 76 | def test_cli_collections(runner): 77 | """Test --collections flag with mock.""" 78 | with patch("stac_check.cli.ApiLinter") as mock_api_linter, patch( 79 | "stac_check.cli.Linter" 80 | ) as mock_linter: 81 | # Mock ApiLinter instance 82 | mock_api_instance = MagicMock() 83 | mock_api_instance.lint_all.return_value = [{"valid_stac": True}] 84 | mock_api_linter.return_value = mock_api_instance 85 | 86 | # Mock Linter instance used for display 87 | mock_linter_instance = MagicMock() 88 | mock_linter.return_value = mock_linter_instance 89 | 90 | result = runner.invoke( 91 | cli_main, 92 | ["https://example.com/collections", "--collections", "--pages", "1"], 93 | ) 94 | 95 | assert result.exit_code == 0 96 | mock_api_linter.assert_called_once_with( 97 | source="https://example.com/collections", 98 | object_list_key="collections", 99 | pages=1, 100 | headers={}, 101 | verbose=False, 102 | ) 103 | 104 | 105 | def test_cli_item_collection(runner): 106 | """Test --item-collection flag with mock.""" 107 | with patch("stac_check.cli.ApiLinter") as mock_api_linter, patch( 108 | "stac_check.cli.Linter" 109 | ) as mock_linter: 110 | # Mock ApiLinter instance 111 | mock_api_instance = MagicMock() 112 | mock_api_instance.lint_all.return_value = [{"valid_stac": True}] 113 | mock_api_linter.return_value = mock_api_instance 114 | 115 | # Mock Linter instance used for display 116 | mock_linter_instance = MagicMock() 117 | mock_linter.return_value = mock_linter_instance 118 | 119 | result = runner.invoke( 120 | cli_main, ["https://example.com/items", "--item-collection", "--pages", "2"] 121 | ) 122 | 123 | assert result.exit_code == 0 124 | mock_api_linter.assert_called_once_with( 125 | source="https://example.com/items", 126 | object_list_key="features", 127 | pages=2, 128 | headers={}, 129 | verbose=False, 130 | ) 131 | 132 | 133 | def test_cli_output_without_required_flags(runner): 134 | """Test that --output requires --collections, --item-collection, or --recursive.""" 135 | with tempfile.NamedTemporaryFile() as tmp: 136 | result = runner.invoke( 137 | cli_main, ["https://example.com/catalog.json", "--output", tmp.name] 138 | ) 139 | assert result.exit_code == 1 140 | assert ( 141 | "--output can only be used with --collections, --item-collection, or --recursive" 142 | in result.output 143 | ) 144 | 145 | 146 | def test_cli_verbose_flag(runner): 147 | """Test that --verbose flag is passed correctly.""" 148 | with patch("stac_check.cli.Linter") as mock_linter: 149 | mock_instance = MagicMock() 150 | mock_linter.return_value = mock_instance 151 | 152 | test_file = os.path.join( 153 | os.path.dirname(__file__), "../sample_files/1.0.0/core-item.json" 154 | ) 155 | result = runner.invoke(cli_main, [test_file, "--verbose"]) 156 | 157 | assert result.exit_code == 0 158 | mock_linter.assert_called_once() 159 | assert mock_linter.call_args[1]["verbose"] is True 160 | 161 | 162 | def test_cli_headers(runner): 163 | """Test that custom headers are passed correctly.""" 164 | with patch("stac_check.cli.Linter") as mock_linter: 165 | mock_instance = MagicMock() 166 | mock_linter.return_value = mock_instance 167 | 168 | test_file = os.path.join( 169 | os.path.dirname(__file__), "../sample_files/1.0.0/core-item.json" 170 | ) 171 | # The header format is: --header KEY VALUE (space-separated, not colon-separated) 172 | result = runner.invoke( 173 | cli_main, 174 | [ 175 | test_file, 176 | "--header", 177 | "Authorization", 178 | "Bearer token", 179 | "--header", 180 | "X-Custom", 181 | "value", 182 | ], 183 | ) 184 | 185 | assert result.exit_code == 0 186 | mock_linter.assert_called_once() 187 | # The headers should be passed as a dictionary to the Linter 188 | headers = mock_linter.call_args[1]["headers"] 189 | assert isinstance(headers, dict) 190 | assert headers.get("Authorization") == "Bearer token" 191 | assert headers.get("X-Custom") == "value" 192 | 193 | 194 | def test_cli_pydantic_flag(runner): 195 | """Test that the --pydantic flag is passed correctly.""" 196 | with patch("stac_check.cli.Linter") as mock_linter, patch( 197 | "stac_check.cli.importlib.import_module" 198 | ): 199 | mock_instance = MagicMock() 200 | mock_linter.return_value = mock_instance 201 | 202 | test_file = os.path.join( 203 | os.path.dirname(__file__), "../sample_files/1.0.0/core-item.json" 204 | ) 205 | 206 | # Test with --pydantic flag 207 | result = runner.invoke(cli_main, [test_file, "--pydantic"]) 208 | 209 | assert result.exit_code == 0 210 | mock_linter.assert_called_once() 211 | # Check that pydantic=True was passed to Linter 212 | assert mock_linter.call_args[1]["pydantic"] is True 213 | 214 | # Test without --pydantic flag (should default to False) 215 | mock_linter.reset_mock() 216 | result = runner.invoke(cli_main, [test_file]) 217 | 218 | assert result.exit_code == 0 219 | mock_linter.assert_called_once() 220 | assert mock_linter.call_args[1]["pydantic"] is False 221 | -------------------------------------------------------------------------------- /tests/test_lint_recursion.py: -------------------------------------------------------------------------------- 1 | from stac_check.lint import Linter 2 | 3 | 4 | def test_linter_collection_recursive(): 5 | file = "sample_files/1.0.0/catalog-with-bad-item.json" 6 | linter = Linter(file, assets=False, links=False, recursive=True) 7 | assert linter.version == "1.0.0" 8 | assert linter.recursive == True 9 | msg = linter.validate_all[0] 10 | assert msg["valid_stac"] is False 11 | assert msg["error_type"] == "JSONSchemaValidationError" 12 | # Accept either 'message' or 'error_message' as the error string 13 | error_msg = msg.get("error_message") or msg.get("message", "") 14 | assert "'id' is a required property" in error_msg 15 | # Optionally check path, version, schema if present 16 | if "path" in msg: 17 | assert msg["path"].endswith("bad-item.json") 18 | 19 | 20 | def test_linter_recursive_max_depth_1(): 21 | file = "https://radarstac.s3.amazonaws.com/stac/catalog.json" 22 | stac = Linter(file, assets=False, links=False, recursive=True, max_depth=1) 23 | assert stac.validate_all == [ 24 | { 25 | "version": "0.7.0", 26 | "path": "https://radarstac.s3.amazonaws.com/stac/catalog.json", 27 | "schema": ["https://cdn.staclint.com/v0.7.0/catalog.json"], 28 | "asset_type": "CATALOG", 29 | "validation_method": "recursive", 30 | "validator_engine": "jsonschema", 31 | "valid_stac": True, 32 | } 33 | ] 34 | 35 | 36 | def test_linter_recursive_max_depth_4(): 37 | file = "https://radarstac.s3.amazonaws.com/stac/catalog.json" 38 | stac = Linter(file, assets=False, links=False, recursive=True, max_depth=4) 39 | assert stac.validate_all == [ 40 | { 41 | "version": "0.7.0", 42 | "path": "https://radarstac.s3.amazonaws.com/stac/catalog.json", 43 | "schema": ["https://cdn.staclint.com/v0.7.0/catalog.json"], 44 | "asset_type": "CATALOG", 45 | "validation_method": "recursive", 46 | "validator_engine": "jsonschema", 47 | "valid_stac": True, 48 | }, 49 | { 50 | "version": "0.7.0", 51 | "path": "https://radarstac.s3.amazonaws.com/stac/radarsat-1/collection.json", 52 | "schema": ["https://cdn.staclint.com/v0.7.0/collection.json"], 53 | "asset_type": "COLLECTION", 54 | "validation_method": "recursive", 55 | "validator_engine": "jsonschema", 56 | "valid_stac": True, 57 | }, 58 | { 59 | "version": "0.7.0", 60 | "path": "https://radarstac.s3.amazonaws.com/stac/radarsat-1/slc/catalog.json", 61 | "schema": ["https://cdn.staclint.com/v0.7.0/catalog.json"], 62 | "asset_type": "CATALOG", 63 | "validation_method": "recursive", 64 | "validator_engine": "jsonschema", 65 | "valid_stac": True, 66 | }, 67 | { 68 | "version": "0.7.0", 69 | "path": "https://radarstac.s3.amazonaws.com/stac/radarsat-1/slc/2012-05-13/RS1_M0630938_F2N_20120513_225708_HH_SLC.json", 70 | "schema": ["https://cdn.staclint.com/v0.7.0/item.json"], 71 | "asset_type": "ITEM", 72 | "validation_method": "recursive", 73 | "validator_engine": "jsonschema", 74 | "valid_stac": True, 75 | }, 76 | { 77 | "version": "0.7.0", 78 | "path": "https://radarstac.s3.amazonaws.com/stac/radarsat-1/slc/2012-06-14/RS1_M0634796_F3F_20120614_110317_HH_SLC.json", 79 | "schema": ["https://cdn.staclint.com/v0.7.0/item.json"], 80 | "asset_type": "ITEM", 81 | "validation_method": "recursive", 82 | "validator_engine": "jsonschema", 83 | "valid_stac": True, 84 | }, 85 | { 86 | "version": "0.7.0", 87 | "path": "https://radarstac.s3.amazonaws.com/stac/radarsat-1/slc/2012-06-14/RS1_M0634795_F3F_20120614_110311_HH_SLC.json", 88 | "schema": ["https://cdn.staclint.com/v0.7.0/item.json"], 89 | "asset_type": "ITEM", 90 | "validation_method": "recursive", 91 | "validator_engine": "jsonschema", 92 | "valid_stac": True, 93 | }, 94 | { 95 | "version": "0.7.0", 96 | "path": "https://radarstac.s3.amazonaws.com/stac/radarsat-1/slc/2012-10-12/RS1_M0634798_F3F_20121012_110325_HH_SLC.json", 97 | "schema": ["https://cdn.staclint.com/v0.7.0/item.json"], 98 | "asset_type": "ITEM", 99 | "validation_method": "recursive", 100 | "validator_engine": "jsonschema", 101 | "valid_stac": True, 102 | }, 103 | { 104 | "version": "0.7.0", 105 | "path": "https://radarstac.s3.amazonaws.com/stac/radarsat-1/slc/2012-10-12/RS1_M0634799_F3F_20121012_110331_HH_SLC.json", 106 | "schema": ["https://cdn.staclint.com/v0.7.0/item.json"], 107 | "asset_type": "ITEM", 108 | "validation_method": "recursive", 109 | "validator_engine": "jsonschema", 110 | "valid_stac": True, 111 | }, 112 | { 113 | "version": "0.7.0", 114 | "path": "https://radarstac.s3.amazonaws.com/stac/radarsat-1/raw/catalog.json", 115 | "schema": ["https://cdn.staclint.com/v0.7.0/catalog.json"], 116 | "asset_type": "CATALOG", 117 | "validation_method": "recursive", 118 | "validator_engine": "jsonschema", 119 | "valid_stac": True, 120 | }, 121 | { 122 | "version": "0.7.0", 123 | "path": "https://radarstac.s3.amazonaws.com/stac/radarsat-1/raw/2012-05-13/RS1_M0000676_F2N_20120513_225701_HH_RAW.json", 124 | "schema": ["https://cdn.staclint.com/v0.7.0/item.json"], 125 | "asset_type": "ITEM", 126 | "validation_method": "recursive", 127 | "validator_engine": "jsonschema", 128 | "valid_stac": True, 129 | }, 130 | ] 131 | 132 | 133 | def test_linter_recursive_100(): 134 | file = "https://digital-atlas.s3.amazonaws.com/stac/public_stac/population/worldpop_2020/collection.json" 135 | stac = Linter(file, assets=False, links=False, recursive=True, max_depth=4) 136 | assert stac.validate_all == [ 137 | { 138 | "asset_type": "COLLECTION", 139 | "path": "https://digital-atlas.s3.amazonaws.com/stac/public_stac/population/worldpop_2020/collection.json", 140 | "schema": [ 141 | "https://stac-extensions.github.io/scientific/v1.0.0/schema.json", 142 | "https://schemas.stacspec.org/v1.0.0/collection-spec/json-schema/collection.json", 143 | ], 144 | "valid_stac": True, 145 | "validation_method": "recursive", 146 | "validator_engine": "jsonschema", 147 | "version": "1.0.0", 148 | }, 149 | { 150 | "asset_type": "ITEM", 151 | "path": "https://digital-atlas.s3.amazonaws.com/stac/public_stac/population/worldpop_2020/./pop_2020/pop_2020.json", 152 | "schema": [ 153 | "https://stac-extensions.github.io/projection/v1.1.0/schema.json", 154 | "https://stac-extensions.github.io/file/v2.1.0/schema.json", 155 | "https://stac-extensions.github.io/raster/v1.1.0/schema.json", 156 | "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json", 157 | ], 158 | "valid_stac": True, 159 | "validation_method": "recursive", 160 | "validator_engine": "jsonschema", 161 | "version": "1.0.0", 162 | }, 163 | { 164 | "asset_type": "ITEM", 165 | "path": "https://digital-atlas.s3.amazonaws.com/stac/public_stac/population/worldpop_2020/./popDens_2020/popDens_2020.json", 166 | "schema": [ 167 | "https://stac-extensions.github.io/projection/v1.1.0/schema.json", 168 | "https://stac-extensions.github.io/file/v2.1.0/schema.json", 169 | "https://stac-extensions.github.io/raster/v1.1.0/schema.json", 170 | "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json", 171 | ], 172 | "valid_stac": True, 173 | "validation_method": "recursive", 174 | "validator_engine": "jsonschema", 175 | "version": "1.0.0", 176 | }, 177 | { 178 | "asset_type": "ITEM", 179 | "path": "https://digital-atlas.s3.amazonaws.com/stac/public_stac/population/worldpop_2020/./pop_2020_parquet/pop_2020_parquet.json", 180 | "schema": [ 181 | "https://stac-extensions.github.io/table/v1.2.0/schema.json", 182 | "https://stac-extensions.github.io/file/v2.1.0/schema.json", 183 | "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json", 184 | ], 185 | "valid_stac": True, 186 | "validation_method": "recursive", 187 | "validator_engine": "jsonschema", 188 | "version": "1.0.0", 189 | }, 190 | ] 191 | -------------------------------------------------------------------------------- /tests/test_lint_stac_api.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | import pytest 4 | 5 | from stac_check.api_lint import ApiLinter 6 | from stac_check.display_messages import ( 7 | cli_message, 8 | collections_message, 9 | item_collection_message, 10 | ) 11 | 12 | 13 | @pytest.fixture 14 | def item_collection_url(): 15 | """URL for a real STAC API item collection endpoint.""" 16 | return "https://stac.geobon.org/collections/chelsa-clim/items" 17 | 18 | 19 | @pytest.fixture 20 | def collections_url(): 21 | """URL for a real STAC API collections endpoint.""" 22 | return "https://stac.geobon.org/collections" 23 | 24 | 25 | @pytest.fixture 26 | def invalid_url(): 27 | """URL for a non-existent endpoint to test error handling.""" 28 | return "https://example.com/not-a-real-endpoint" 29 | 30 | 31 | def test_item_collection_validation(item_collection_url): 32 | """Test item collection validation with a real STAC API endpoint.""" 33 | linter = ApiLinter( 34 | source=item_collection_url, 35 | object_list_key="features", 36 | pages=1, 37 | ) 38 | 39 | results = linter.lint_all() 40 | 41 | # Verify we got results 42 | assert len(results) > 0 43 | # Check that items are valid STAC 44 | assert all(result["valid_stac"] for result in results) 45 | # Check that we have original objects 46 | assert all("original_object" in result for result in results) 47 | 48 | # Check specific fields in the results 49 | for result in results: 50 | assert "path" in result 51 | assert "version" in result 52 | assert "schema" in result 53 | assert "asset_type" in result 54 | assert result["asset_type"] == "ITEM" 55 | 56 | 57 | def test_item_collection_with_pages(item_collection_url): 58 | """Test item collection validation with pages parameter.""" 59 | # Test with 1 page 60 | linter_1page = ApiLinter( 61 | source=item_collection_url, 62 | object_list_key="features", 63 | pages=1, 64 | ) 65 | results_1page = linter_1page.lint_all() 66 | 67 | # Test with 2 pages 68 | linter_2pages = ApiLinter( 69 | source=item_collection_url, 70 | object_list_key="features", 71 | pages=2, 72 | ) 73 | results_2pages = linter_2pages.lint_all() 74 | 75 | # Verify we got more results with 2 pages than with 1 page 76 | # This assumes the API returns paginated results 77 | assert len(results_2pages) >= len(results_1page) 78 | 79 | # Check that the pages parameter was respected 80 | assert linter_1page.pages == 1 81 | assert linter_2pages.pages == 2 82 | 83 | 84 | def test_collections_validation(collections_url): 85 | """Test collections validation with a real STAC API endpoint.""" 86 | linter = ApiLinter( 87 | source=collections_url, 88 | object_list_key="collections", 89 | pages=1, 90 | ) 91 | 92 | results = linter.lint_all() 93 | 94 | # Verify we got results 95 | assert len(results) > 0 96 | # Check that we have original objects 97 | assert all("original_object" in result for result in results) 98 | # Check that all collections have an ID 99 | assert all("id" in result["original_object"] for result in results) 100 | 101 | # Check specific fields in the results 102 | for result in results: 103 | assert "path" in result 104 | assert "version" in result 105 | assert "schema" in result 106 | 107 | 108 | def test_api_linter_initialization(item_collection_url, collections_url): 109 | """Test that the ApiLinter can be initialized with different parameters.""" 110 | # Test with item collection 111 | item_linter = ApiLinter( 112 | source=item_collection_url, 113 | object_list_key="features", 114 | pages=1, 115 | ) 116 | assert item_linter.object_list_key == "features" 117 | assert item_linter.pages == 1 118 | assert item_linter.source == item_collection_url 119 | 120 | # Test with collections 121 | collections_linter = ApiLinter( 122 | source=collections_url, 123 | object_list_key="collections", 124 | pages=2, 125 | ) 126 | assert collections_linter.object_list_key == "collections" 127 | assert collections_linter.pages == 2 128 | assert collections_linter.source == collections_url 129 | 130 | # Test with custom headers 131 | headers = {"X-Custom-Header": "test-value"} 132 | linter_with_headers = ApiLinter( 133 | source=collections_url, 134 | object_list_key="collections", 135 | pages=1, 136 | headers=headers, 137 | ) 138 | assert linter_with_headers.headers == headers 139 | 140 | 141 | def test_error_handling_with_individual_items(): 142 | """Test that ApiLinter handles errors with individual items gracefully.""" 143 | # Create a mock that returns a valid response but with an item that will cause an error 144 | with mock.patch("stac_check.api_lint.fetch_and_parse_file") as mock_fetch: 145 | # Return a response with a malformed item that will cause an exception during validation 146 | mock_fetch.return_value = { 147 | "features": [ 148 | { 149 | "id": "valid-item", 150 | "type": "Feature", 151 | "geometry": {}, 152 | "properties": {}, 153 | }, 154 | # This item is missing required fields and will cause validation to fail 155 | {"id": "invalid-item"}, 156 | ] 157 | } 158 | 159 | linter = ApiLinter( 160 | source="https://example.com/items", 161 | object_list_key="features", 162 | pages=1, 163 | ) 164 | 165 | # This should not raise an exception 166 | results = linter.lint_all() 167 | 168 | # Should return results for both items 169 | assert len(results) == 2 170 | 171 | # Find the invalid item result 172 | invalid_result = next(r for r in results if r["path"].endswith("invalid-item")) 173 | 174 | # Check that it was marked as invalid 175 | assert invalid_result["valid_stac"] is False 176 | # Should have an error type 177 | assert "error_type" in invalid_result 178 | # Should have an error message 179 | assert "error_message" in invalid_result 180 | 181 | 182 | def test_item_collection_message_display(item_collection_url): 183 | """Test that item_collection_message correctly displays validation results.""" 184 | linter = ApiLinter( 185 | source=item_collection_url, 186 | object_list_key="features", 187 | pages=1, 188 | ) 189 | 190 | results = linter.lint_all() 191 | 192 | # Mock click.secho to verify it's called with the right arguments 193 | with mock.patch("stac_check.display_messages.click.secho") as mock_secho: 194 | item_collection_message(linter, results, cli_message_func=cli_message) 195 | 196 | # Verify that click.secho was called with expected title 197 | # The actual title in the code is "Item Collection: Validate all assets in a feature collection" 198 | mock_secho.assert_any_call( 199 | "Item Collection: Validate all assets in a feature collection", bold=True 200 | ) 201 | # Verify that pages info was displayed - the format is "Pages = 1" not "Pages: 1" 202 | mock_secho.assert_any_call("Pages = 1") 203 | 204 | 205 | def test_collections_message_display(collections_url): 206 | """Test that collections_message correctly displays validation results.""" 207 | linter = ApiLinter( 208 | source=collections_url, 209 | object_list_key="collections", 210 | pages=1, 211 | ) 212 | 213 | results = linter.lint_all() 214 | 215 | # Mock click.secho to verify it's called with the right arguments 216 | with mock.patch("stac_check.display_messages.click.secho") as mock_secho: 217 | collections_message(linter, results, cli_message_func=cli_message) 218 | 219 | # Verify that click.secho was called with expected title 220 | mock_secho.assert_any_call( 221 | "Collections: Validate all collections in a STAC API", bold=True 222 | ) 223 | # Verify that pages info was displayed - the format is "Pages = 1" not "Pages: 1" 224 | mock_secho.assert_any_call("Pages = 1") 225 | 226 | 227 | def test_pages_parameter_handling(item_collection_url): 228 | """Test that the pages parameter correctly limits pagination.""" 229 | # Test with explicit pages=1 230 | linter_explicit_1 = ApiLinter( 231 | source=item_collection_url, 232 | object_list_key="features", 233 | pages=1, 234 | ) 235 | results_explicit_1 = linter_explicit_1.lint_all() 236 | 237 | # Test with None (should default to 1) 238 | linter_none = ApiLinter( 239 | source=item_collection_url, 240 | object_list_key="features", 241 | pages=None, 242 | ) 243 | results_none = linter_none.lint_all() 244 | 245 | # Test with default (should be 1) 246 | linter_default = ApiLinter( 247 | source=item_collection_url, 248 | object_list_key="features", 249 | ) 250 | results_default = linter_default.lint_all() 251 | 252 | # Test with pages=2 253 | linter_2pages = ApiLinter( 254 | source=item_collection_url, 255 | object_list_key="features", 256 | pages=2, 257 | ) 258 | results_2pages = linter_2pages.lint_all() 259 | 260 | # Verify internal pages parameter is set correctly 261 | assert linter_explicit_1.pages == 1 262 | assert linter_none.pages == 1 # Should default to 1 263 | assert linter_default.pages == 1 # Should default to 1 264 | assert linter_2pages.pages == 2 265 | 266 | # Verify results count is consistent for all 1-page variants 267 | assert len(results_explicit_1) == len(results_none) 268 | assert len(results_explicit_1) == len(results_default) 269 | 270 | # Verify we get more results with 2 pages (if API supports pagination) 271 | # This test might need to be adjusted if the API doesn't have enough items 272 | if len(results_2pages) > len(results_explicit_1): 273 | assert len(results_2pages) > len(results_explicit_1) 274 | -------------------------------------------------------------------------------- /sample_files/1.0.0/core-item-bloated.json: -------------------------------------------------------------------------------- 1 | { 2 | "stac_version": "1.0.0", 3 | "stac_extensions": [], 4 | "type": "Feature", 5 | "id": "20201211_223832_CS2", 6 | "bbox": [ 7 | 172.91173669923782, 8 | 1.3438851951615003, 9 | 172.95469614953714, 10 | 1.3690476620161975 11 | ], 12 | "geometry": { 13 | "type": "Polygon", 14 | "coordinates": [ 15 | [ 16 | [ 17 | 172.91173669923782, 18 | 1.3438851951615003 19 | ], 20 | [ 21 | 172.95469614953714, 22 | 1.3438851951615003 23 | ], 24 | [ 25 | 172.95469614953714, 26 | 1.3690476620161975 27 | ], 28 | [ 29 | 172.91173669923782, 30 | 1.3690476620161975 31 | ], 32 | [ 33 | 172.91173669923782, 34 | 1.3438851951615003 35 | ] 36 | ] 37 | ] 38 | }, 39 | "properties": { 40 | "title": "Core Item", 41 | "description": "A sample STAC Item that includes examples of all common metadata", 42 | "datetime": null, 43 | "start_datetime": "2020-12-11T22:38:32.125Z", 44 | "end_datetime": "2020-12-11T22:38:32.327Z", 45 | "created": "2020-12-12T01:48:13.725Z", 46 | "updated": "2020-12-12T01:48:13.725Z", 47 | "platform": "cool_sat1", 48 | "instruments": [ 49 | "cool_sensor_v1" 50 | ], 51 | "constllation": "ion", 52 | "missin": "collection 5624", 53 | "gs": 0.512, 54 | "tile": "Core Item", 55 | "desciption": "A sample STAC Item that includes examples of all common metadata", 56 | "dattime": null, 57 | "startdatetime": "2020-12-11T22:38:32.125Z", 58 | "end_dtetime": "2020-12-11T22:38:32.327Z", 59 | "creted": "2020-12-12T01:48:13.725Z", 60 | "updted": "2020-12-12T01:48:13.725Z", 61 | "platorm": "cool_sat1", 62 | "instrments": [ 63 | "cool_sensor_v1" 64 | ], 65 | "constellation": "ion", 66 | "mission": "collection 5624", 67 | "gsd": 0.512 68 | }, 69 | "collection": "simple-collection", 70 | "links": [ 71 | { 72 | "rel": "collection", 73 | "href": "./collection.json", 74 | "type": "application/json", 75 | "title": "Simple Example Collection" 76 | }, 77 | { 78 | "rel": "root", 79 | "href": "./collection.json", 80 | "type": "application/json", 81 | "title": "Simple Example Collection" 82 | }, 83 | { 84 | "rel": "parent", 85 | "href": "./collection.json", 86 | "type": "application/json", 87 | "title": "Simple Example Collection" 88 | }, 89 | { 90 | "rel": "alternate", 91 | "type": "text/html", 92 | "href": "http://remotedata.io/catalog/20201211_223832_CS2/index.html", 93 | "title": "HTML version of this STAC Item" 94 | }, 95 | { 96 | "rel": "collection", 97 | "href": "./collection.json", 98 | "type": "application/json", 99 | "title": "Simple Example Collection" 100 | }, 101 | { 102 | "rel": "root", 103 | "href": "./collection.json", 104 | "type": "application/json", 105 | "title": "Simple Example Collection" 106 | }, 107 | { 108 | "rel": "parent", 109 | "href": "./collection.json", 110 | "type": "application/json", 111 | "title": "Simple Example Collection" 112 | }, 113 | { 114 | "rel": "alternate", 115 | "type": "text/html", 116 | "href": "http://remotedata.io/catalog/20201211_223832_CS2/index.html", 117 | "title": "HTML version of this STAC Item" 118 | }, 119 | { 120 | "rel": "collection", 121 | "href": "./collection.json", 122 | "type": "application/json", 123 | "title": "Simple Example Collection" 124 | }, 125 | { 126 | "rel": "root", 127 | "href": "./collection.json", 128 | "type": "application/json", 129 | "title": "Simple Example Collection" 130 | }, 131 | { 132 | "rel": "parent", 133 | "href": "./collection.json", 134 | "type": "application/json", 135 | "title": "Simple Example Collection" 136 | }, 137 | { 138 | "rel": "alternate", 139 | "type": "text/html", 140 | "href": "http://remotedata.io/catalog/20201211_223832_CS2/index.html", 141 | "title": "HTML version of this STAC Item" 142 | }, 143 | { 144 | "rel": "collection", 145 | "href": "./collection.json", 146 | "type": "application/json", 147 | "title": "Simple Example Collection" 148 | }, 149 | { 150 | "rel": "root", 151 | "href": "./collection.json", 152 | "type": "application/json", 153 | "title": "Simple Example Collection" 154 | }, 155 | { 156 | "rel": "parent", 157 | "href": "./collection.json", 158 | "type": "application/json", 159 | "title": "Simple Example Collection" 160 | }, 161 | { 162 | "rel": "alternate", 163 | "type": "text/html", 164 | "href": "http://remotedata.io/catalog/20201211_223832_CS2/index.html", 165 | "title": "HTML version of this STAC Item" 166 | }, 167 | { 168 | "rel": "collection", 169 | "href": "./collection.json", 170 | "type": "application/json", 171 | "title": "Simple Example Collection" 172 | }, 173 | { 174 | "rel": "root", 175 | "href": "./collection.json", 176 | "type": "application/json", 177 | "title": "Simple Example Collection" 178 | }, 179 | { 180 | "rel": "parent", 181 | "href": "./collection.json", 182 | "type": "application/json", 183 | "title": "Simple Example Collection" 184 | }, 185 | { 186 | "rel": "alternate", 187 | "type": "text/html", 188 | "href": "http://remotedata.io/catalog/20201211_223832_CS2/index.html", 189 | "title": "HTML version of this STAC Item" 190 | }, 191 | { 192 | "rel": "collection", 193 | "href": "./collection.json", 194 | "type": "application/json", 195 | "title": "Simple Example Collection" 196 | }, 197 | { 198 | "rel": "root", 199 | "href": "./collection.json", 200 | "type": "application/json", 201 | "title": "Simple Example Collection" 202 | }, 203 | { 204 | "rel": "parent", 205 | "href": "./collection.json", 206 | "type": "application/json", 207 | "title": "Simple Example Collection" 208 | }, 209 | { 210 | "rel": "alternate", 211 | "type": "text/html", 212 | "href": "http://remotedata.io/catalog/20201211_223832_CS2/index.html", 213 | "title": "HTML version of this STAC Item" 214 | }, 215 | { 216 | "rel": "collection", 217 | "href": "./collection.json", 218 | "type": "application/json", 219 | "title": "Simple Example Collection" 220 | }, 221 | { 222 | "rel": "root", 223 | "href": "./collection.json", 224 | "type": "application/json", 225 | "title": "Simple Example Collection" 226 | }, 227 | { 228 | "rel": "parent", 229 | "href": "./collection.json", 230 | "type": "application/json", 231 | "title": "Simple Example Collection" 232 | }, 233 | { 234 | "rel": "alternate", 235 | "type": "text/html", 236 | "href": "http://remotedata.io/catalog/20201211_223832_CS2/index.html", 237 | "title": "HTML version of this STAC Item" 238 | }, 239 | { 240 | "rel": "collection", 241 | "href": "./collection.json", 242 | "type": "application/json", 243 | "title": "Simple Example Collection" 244 | }, 245 | { 246 | "rel": "root", 247 | "href": "./collection.json", 248 | "type": "application/json", 249 | "title": "Simple Example Collection" 250 | }, 251 | { 252 | "rel": "parent", 253 | "href": "./collection.json", 254 | "type": "application/json", 255 | "title": "Simple Example Collection" 256 | }, 257 | { 258 | "rel": "alternate", 259 | "type": "text/html", 260 | "href": "http://remotedata.io/catalog/20201211_223832_CS2/index.html", 261 | "title": "HTML version of this STAC Item" 262 | }, 263 | { 264 | "rel": "collection", 265 | "href": "./collection.json", 266 | "type": "application/json", 267 | "title": "Simple Example Collection" 268 | }, 269 | { 270 | "rel": "root", 271 | "href": "./collection.json", 272 | "type": "application/json", 273 | "title": "Simple Example Collection" 274 | }, 275 | { 276 | "rel": "parent", 277 | "href": "./collection.json", 278 | "type": "application/json", 279 | "title": "Simple Example Collection" 280 | }, 281 | { 282 | "rel": "alternate", 283 | "type": "text/html", 284 | "href": "http://remotedata.io/catalog/20201211_223832_CS2/index.html", 285 | "title": "HTML version of this STAC Item" 286 | }, 287 | { 288 | "rel": "collection", 289 | "href": "./collection.json", 290 | "type": "application/json", 291 | "title": "Simple Example Collection" 292 | }, 293 | { 294 | "rel": "root", 295 | "href": "./collection.json", 296 | "type": "application/json", 297 | "title": "Simple Example Collection" 298 | }, 299 | { 300 | "rel": "parent", 301 | "href": "./collection.json", 302 | "type": "application/json", 303 | "title": "Simple Example Collection" 304 | }, 305 | { 306 | "rel": "alternate", 307 | "type": "text/html", 308 | "href": "http://remotedata.io/catalog/20201211_223832_CS2/index.html", 309 | "title": "HTML version of this STAC Item" 310 | } 311 | ], 312 | "assets": { 313 | "analytic": { 314 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2_analytic.tif", 315 | "type": "image/tiff; application=geotiff; profile=cloud-optimized", 316 | "title": "4-Band Analytic", 317 | "roles": [ 318 | "data" 319 | ] 320 | }, 321 | "thumbnail": { 322 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.jpg", 323 | "title": "Thumbnail", 324 | "type": "image/png", 325 | "roles": [ 326 | "thumbnail" 327 | ] 328 | }, 329 | "visual": { 330 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.tif", 331 | "type": "image/tiff; application=geotiff; profile=cloud-optimized", 332 | "title": "3-Band Visual", 333 | "roles": [ 334 | "visual" 335 | ] 336 | }, 337 | "udm": { 338 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2_analytic_udm.tif", 339 | "title": "Unusable Data Mask", 340 | "type": "image/tiff; application=geotiff;" 341 | }, 342 | "json-metadata": { 343 | "href": "http://remotedata.io/catalog/20201211_223832_CS2/extended-metadata.json", 344 | "title": "Extended Metadata", 345 | "type": "application/json", 346 | "roles": [ 347 | "metadata" 348 | ] 349 | }, 350 | "ephemeris": { 351 | "href": "http://cool-sat.com/catalog/20201211_223832_CS2/20201211_223832_CS2.EPH", 352 | "title": "Satellite Ephemeris Metadata" 353 | } 354 | } 355 | } -------------------------------------------------------------------------------- /sample_files/1.0.0/feature_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "type":"FeatureCollection","context":{"limit":10,"returned":10},"features":[{"id":"bio9","bbox":[-180.00013888885002,-90.00013888885,179.99985967115003,83.99986041515],"type":"Feature","links":[{"rel":"collection","type":"application/json","href":"https://stac.geobon.org/collections/chelsa-clim"},{"rel":"parent","type":"application/json","href":"https://stac.geobon.org/collections/chelsa-clim"},{"rel":"root","type":"application/json","href":"https://stac.geobon.org/"},{"rel":"self","type":"application/geo+json","href":"https://stac.geobon.org/collections/chelsa-clim/items/bio9"}],"assets":{"bio9":{"href":"https://object-arbutus.cloud.computecanada.ca/bq-io/io/CHELSA/climatologies/CHELSA_bio9_1981-2010_V.2.1.tif","type":"image/tiff; application=geotiff; profile=cloud-optimized","raster:bands":[{"unit":"","data_type":"uint16","spatial_resolution":0.0083333333}]}},"geometry":{"type":"Polygon","coordinates":[[[-180.00013888885002,-90.00013888885],[-180.00013888885002,83.99986041515],[179.99985967115003,83.99986041515],[179.99985967115003,-90.00013888885],[-180.00013888885002,-90.00013888885]]]},"collection":"chelsa-clim","properties":{"model":"past","version":2.1,"datetime":"1981-01-01T00:00:00Z","variable":"bio9","proj:epsg":4326,"description":"CHELSA Climatologies bio9","full_filename":"CHELSA_bio9_1981-2010_V.2.1.tif"},"stac_version":"1.0.0","stac_extensions":["https://stac-extensions.github.io/projection/v1.0.0/schema.json"]},{"id":"bio8","bbox":[-180.00013888885002,-90.00013888885,179.99985967115003,83.99986041515],"type":"Feature","links":[{"rel":"collection","type":"application/json","href":"https://stac.geobon.org/collections/chelsa-clim"},{"rel":"parent","type":"application/json","href":"https://stac.geobon.org/collections/chelsa-clim"},{"rel":"root","type":"application/json","href":"https://stac.geobon.org/"},{"rel":"self","type":"application/geo+json","href":"https://stac.geobon.org/collections/chelsa-clim/items/bio8"}],"assets":{"bio8":{"href":"https://object-arbutus.cloud.computecanada.ca/bq-io/io/CHELSA/climatologies/CHELSA_bio8_1981-2010_V.2.1.tif","type":"image/tiff; application=geotiff; profile=cloud-optimized","raster:bands":[{"unit":"","data_type":"uint16","spatial_resolution":0.0083333333}]}},"geometry":{"type":"Polygon","coordinates":[[[-180.00013888885002,-90.00013888885],[-180.00013888885002,83.99986041515],[179.99985967115003,83.99986041515],[179.99985967115003,-90.00013888885],[-180.00013888885002,-90.00013888885]]]},"collection":"chelsa-clim","properties":{"model":"past","version":2.1,"datetime":"1981-01-01T00:00:00Z","variable":"bio8","proj:epsg":4326,"description":"CHELSA Climatologies bio8","full_filename":"CHELSA_bio8_1981-2010_V.2.1.tif"},"stac_version":"1.0.0","stac_extensions":["https://stac-extensions.github.io/projection/v1.0.0/schema.json"]},{"id":"bio7","bbox":[-180.00013888885002,-90.00013888884999,179.99985967115003,83.99986041515001],"type":"Feature","links":[{"rel":"collection","type":"application/json","href":"https://stac.geobon.org/collections/chelsa-clim"},{"rel":"parent","type":"application/json","href":"https://stac.geobon.org/collections/chelsa-clim"},{"rel":"root","type":"application/json","href":"https://stac.geobon.org/"},{"rel":"self","type":"application/geo+json","href":"https://stac.geobon.org/collections/chelsa-clim/items/bio7"}],"assets":{"bio7":{"href":"https://object-arbutus.cloud.computecanada.ca/bq-io/io/CHELSA/climatologies/CHELSA_bio7_1981-2010_V.2.1.tif","type":"image/tiff; application=geotiff; profile=cloud-optimized","raster:bands":[{"unit":"","data_type":"uint16","spatial_resolution":0.0083333333}]}},"geometry":{"type":"Polygon","coordinates":[[[-180.00013888885002,-90.00013888884999],[-180.00013888885002,83.99986041515001],[179.99985967115003,83.99986041515001],[179.99985967115003,-90.00013888884999],[-180.00013888885002,-90.00013888884999]]]},"collection":"chelsa-clim","properties":{"model":"past","version":2.1,"datetime":"1981-01-01T00:00:00Z","variable":"bio7","proj:epsg":4326,"description":"CHELSA Climatologies bio7","full_filename":"CHELSA_bio7_1981-2010_V.2.1.tif"},"stac_version":"1.0.0","stac_extensions":["https://stac-extensions.github.io/projection/v1.0.0/schema.json"]},{"id":"bio6","bbox":[-180.00013888885002,-90.00013888884999,179.99985967115003,83.99986041515001],"type":"Feature","links":[{"rel":"collection","type":"application/json","href":"https://stac.geobon.org/collections/chelsa-clim"},{"rel":"parent","type":"application/json","href":"https://stac.geobon.org/collections/chelsa-clim"},{"rel":"root","type":"application/json","href":"https://stac.geobon.org/"},{"rel":"self","type":"application/geo+json","href":"https://stac.geobon.org/collections/chelsa-clim/items/bio6"}],"assets":{"bio6":{"href":"https://object-arbutus.cloud.computecanada.ca/bq-io/io/CHELSA/climatologies/CHELSA_bio6_1981-2010_V.2.1.tif","type":"image/tiff; application=geotiff; profile=cloud-optimized","raster:bands":[{"unit":"","data_type":"uint16","spatial_resolution":0.0083333333}]}},"geometry":{"type":"Polygon","coordinates":[[[-180.00013888885002,-90.00013888884999],[-180.00013888885002,83.99986041515001],[179.99985967115003,83.99986041515001],[179.99985967115003,-90.00013888884999],[-180.00013888885002,-90.00013888884999]]]},"collection":"chelsa-clim","properties":{"model":"past","version":2.1,"datetime":"1981-01-01T00:00:00Z","variable":"bio6","proj:epsg":4326,"description":"CHELSA Climatologies bio6","full_filename":"CHELSA_bio6_1981-2010_V.2.1.tif"},"stac_version":"1.0.0","stac_extensions":["https://stac-extensions.github.io/projection/v1.0.0/schema.json"]},{"id":"bio5","bbox":[-180.00013888885002,-90.00013888884999,179.99985967115003,83.99986041515001],"type":"Feature","links":[{"rel":"collection","type":"application/json","href":"https://stac.geobon.org/collections/chelsa-clim"},{"rel":"parent","type":"application/json","href":"https://stac.geobon.org/collections/chelsa-clim"},{"rel":"root","type":"application/json","href":"https://stac.geobon.org/"},{"rel":"self","type":"application/geo+json","href":"https://stac.geobon.org/collections/chelsa-clim/items/bio5"}],"assets":{"bio5":{"href":"https://object-arbutus.cloud.computecanada.ca/bq-io/io/CHELSA/climatologies/CHELSA_bio5_1981-2010_V.2.1.tif","type":"image/tiff; application=geotiff; profile=cloud-optimized","raster:bands":[{"unit":"","data_type":"uint16","spatial_resolution":0.0083333333}]}},"geometry":{"type":"Polygon","coordinates":[[[-180.00013888885002,-90.00013888884999],[-180.00013888885002,83.99986041515001],[179.99985967115003,83.99986041515001],[179.99985967115003,-90.00013888884999],[-180.00013888885002,-90.00013888884999]]]},"collection":"chelsa-clim","properties":{"model":"past","version":2.1,"datetime":"1981-01-01T00:00:00Z","variable":"bio5","proj:epsg":4326,"description":"CHELSA Climatologies bio5","full_filename":"CHELSA_bio5_1981-2010_V.2.1.tif"},"stac_version":"1.0.0","stac_extensions":["https://stac-extensions.github.io/projection/v1.0.0/schema.json"]},{"id":"bio4","bbox":[-180.00013888885002,-90.00013888884999,179.99985967115003,83.99986041515001],"type":"Feature","links":[{"rel":"collection","type":"application/json","href":"https://stac.geobon.org/collections/chelsa-clim"},{"rel":"parent","type":"application/json","href":"https://stac.geobon.org/collections/chelsa-clim"},{"rel":"root","type":"application/json","href":"https://stac.geobon.org/"},{"rel":"self","type":"application/geo+json","href":"https://stac.geobon.org/collections/chelsa-clim/items/bio4"}],"assets":{"bio4":{"href":"https://object-arbutus.cloud.computecanada.ca/bq-io/io/CHELSA/climatologies/CHELSA_bio4_1981-2010_V.2.1.tif","type":"image/tiff; application=geotiff; profile=cloud-optimized","raster:bands":[{"unit":"","data_type":"uint16","spatial_resolution":0.0083333333}]}},"geometry":{"type":"Polygon","coordinates":[[[-180.00013888885002,-90.00013888884999],[-180.00013888885002,83.99986041515001],[179.99985967115003,83.99986041515001],[179.99985967115003,-90.00013888884999],[-180.00013888885002,-90.00013888884999]]]},"collection":"chelsa-clim","properties":{"model":"past","version":2.1,"datetime":"1981-01-01T00:00:00Z","variable":"bio4","proj:epsg":4326,"description":"CHELSA Climatologies bio4","full_filename":"CHELSA_bio4_1981-2010_V.2.1.tif"},"stac_version":"1.0.0","stac_extensions":["https://stac-extensions.github.io/projection/v1.0.0/schema.json"]},{"id":"bio3","bbox":[-180.00013888885002,-90.00013888884999,179.99985967115003,83.99986041515001],"type":"Feature","links":[{"rel":"collection","type":"application/json","href":"https://stac.geobon.org/collections/chelsa-clim"},{"rel":"parent","type":"application/json","href":"https://stac.geobon.org/collections/chelsa-clim"},{"rel":"root","type":"application/json","href":"https://stac.geobon.org/"},{"rel":"self","type":"application/geo+json","href":"https://stac.geobon.org/collections/chelsa-clim/items/bio3"}],"assets":{"bio3":{"href":"https://object-arbutus.cloud.computecanada.ca/bq-io/io/CHELSA/climatologies/CHELSA_bio3_1981-2010_V.2.1.tif","type":"image/tiff; application=geotiff; profile=cloud-optimized","raster:bands":[{"unit":"","data_type":"uint16","spatial_resolution":0.0083333333}]}},"geometry":{"type":"Polygon","coordinates":[[[-180.00013888885002,-90.00013888884999],[-180.00013888885002,83.99986041515001],[179.99985967115003,83.99986041515001],[179.99985967115003,-90.00013888884999],[-180.00013888885002,-90.00013888884999]]]},"collection":"chelsa-clim","properties":{"model":"past","version":2.1,"datetime":"1981-01-01T00:00:00Z","variable":"bio3","proj:epsg":4326,"description":"CHELSA Climatologies bio3","full_filename":"CHELSA_bio3_1981-2010_V.2.1.tif"},"stac_version":"1.0.0","stac_extensions":["https://stac-extensions.github.io/projection/v1.0.0/schema.json"]},{"id":"bio2","bbox":[-180.00013888885002,-90.00013888884999,179.99985967115003,83.99986041515001],"type":"Feature","links":[{"rel":"collection","type":"application/json","href":"https://stac.geobon.org/collections/chelsa-clim"},{"rel":"parent","type":"application/json","href":"https://stac.geobon.org/collections/chelsa-clim"},{"rel":"root","type":"application/json","href":"https://stac.geobon.org/"},{"rel":"self","type":"application/geo+json","href":"https://stac.geobon.org/collections/chelsa-clim/items/bio2"}],"assets":{"bio2":{"href":"https://object-arbutus.cloud.computecanada.ca/bq-io/io/CHELSA/climatologies/CHELSA_bio2_1981-2010_V.2.1.tif","type":"image/tiff; application=geotiff; profile=cloud-optimized","raster:bands":[{"unit":"","data_type":"uint16","spatial_resolution":0.0083333333}]}},"geometry":{"type":"Polygon","coordinates":[[[-180.00013888885002,-90.00013888884999],[-180.00013888885002,83.99986041515001],[179.99985967115003,83.99986041515001],[179.99985967115003,-90.00013888884999],[-180.00013888885002,-90.00013888884999]]]},"collection":"chelsa-clim","properties":{"model":"past","version":2.1,"datetime":"1981-01-01T00:00:00Z","variable":"bio2","proj:epsg":4326,"description":"CHELSA Climatologies bio2","full_filename":"CHELSA_bio2_1981-2010_V.2.1.tif"},"stac_version":"1.0.0","stac_extensions":["https://stac-extensions.github.io/projection/v1.0.0/schema.json"]},{"id":"bio19","bbox":[-180.00013888885002,-90.00013888885,179.99985967115003,83.99986041515],"type":"Feature","links":[{"rel":"collection","type":"application/json","href":"https://stac.geobon.org/collections/chelsa-clim"},{"rel":"parent","type":"application/json","href":"https://stac.geobon.org/collections/chelsa-clim"},{"rel":"root","type":"application/json","href":"https://stac.geobon.org/"},{"rel":"self","type":"application/geo+json","href":"https://stac.geobon.org/collections/chelsa-clim/items/bio19"}],"assets":{"bio19":{"href":"https://object-arbutus.cloud.computecanada.ca/bq-io/io/CHELSA/climatologies/CHELSA_bio19_1981-2010_V.2.1.tif","type":"image/tiff; application=geotiff; profile=cloud-optimized","raster:bands":[{"unit":"","data_type":"uint16","spatial_resolution":0.0083333333}]}},"geometry":{"type":"Polygon","coordinates":[[[-180.00013888885002,-90.00013888885],[-180.00013888885002,83.99986041515],[179.99985967115003,83.99986041515],[179.99985967115003,-90.00013888885],[-180.00013888885002,-90.00013888885]]]},"collection":"chelsa-clim","properties":{"model":"past","version":2.1,"datetime":"1981-01-01T00:00:00Z","variable":"bio19","proj:epsg":4326,"description":"CHELSA Climatologies bio19","full_filename":"CHELSA_bio19_1981-2010_V.2.1.tif"},"stac_version":"1.0.0","stac_extensions":["https://stac-extensions.github.io/projection/v1.0.0/schema.json"]},{"id":"bio18","bbox":[-180.00013888885002,-90.00013888885,179.99985967115003,83.99986041515],"type":"Feature","links":[{"rel":"collection","type":"application/json","href":"https://stac.geobon.org/collections/chelsa-clim"},{"rel":"parent","type":"application/json","href":"https://stac.geobon.org/collections/chelsa-clim"},{"rel":"root","type":"application/json","href":"https://stac.geobon.org/"},{"rel":"self","type":"application/geo+json","href":"https://stac.geobon.org/collections/chelsa-clim/items/bio18"}],"assets":{"bio18":{"href":"https://object-arbutus.cloud.computecanada.ca/bq-io/io/CHELSA/climatologies/CHELSA_bio18_1981-2010_V.2.1.tif","type":"image/tiff; application=geotiff; profile=cloud-optimized","raster:bands":[{"unit":"","data_type":"uint16","spatial_resolution":0.0083333333}]}},"geometry":{"type":"Polygon","coordinates":[[[-180.00013888885002,-90.00013888885],[-180.00013888885002,83.99986041515],[179.99985967115003,83.99986041515],[179.99985967115003,-90.00013888885],[-180.00013888885002,-90.00013888885]]]},"collection":"chelsa-clim","properties":{"model":"past","version":2.1,"datetime":"1981-01-01T00:00:00Z","variable":"bio18","proj:epsg":4326,"description":"CHELSA Climatologies bio18","full_filename":"CHELSA_bio18_1981-2010_V.2.1.tif"},"stac_version":"1.0.0","stac_extensions":["https://stac-extensions.github.io/projection/v1.0.0/schema.json"]}],"links":[{"rel":"collection","type":"application/json","href":"https://stac.geobon.org/collections/chelsa-clim"},{"rel":"parent","type":"application/json","href":"https://stac.geobon.org/collections/chelsa-clim"},{"rel":"root","type":"application/json","href":"https://stac.geobon.org/"},{"rel":"self","type":"application/geo+json","href":"https://stac.geobon.org/collections/chelsa-clim/items"},{"rel":"next","type":"application/geo+json","method":"GET","href":"https://stac.geobon.org/collections/chelsa-clim/items?token=next:bio18"}]} -------------------------------------------------------------------------------- /tests/test_lint_geometry.py: -------------------------------------------------------------------------------- 1 | from stac_check.lint import Linter 2 | 3 | 4 | def test_geometry_coordinates_order(): 5 | """Test the check_geometry_coordinates_order method for detecting potentially incorrectly ordered coordinates.""" 6 | # Create a test item with coordinates in the correct order (longitude, latitude) 7 | correct_item = { 8 | "stac_version": "1.0.0", 9 | "stac_extensions": [], 10 | "type": "Feature", 11 | "id": "test-coordinates-correct", 12 | "bbox": [10.0, -10.0, 20.0, 10.0], 13 | "geometry": { 14 | "type": "Polygon", 15 | "coordinates": [ 16 | [ 17 | [10.0, -10.0], # lon, lat 18 | [20.0, -10.0], 19 | [20.0, 10.0], 20 | [10.0, 10.0], 21 | [10.0, -10.0], 22 | ] 23 | ], 24 | }, 25 | "properties": {"datetime": "2023-01-01T00:00:00Z"}, 26 | } 27 | 28 | # Create a test item with coordinates in the wrong order (latitude, longitude) 29 | # but with values that don't trigger the validation checks 30 | undetectable_reversed_item = { 31 | "stac_version": "1.0.0", 32 | "stac_extensions": [], 33 | "type": "Feature", 34 | "id": "test-coordinates-undetectable-reversed", 35 | "bbox": [10.0, -10.0, 20.0, 10.0], 36 | "geometry": { 37 | "type": "Polygon", 38 | "coordinates": [ 39 | [ 40 | [-10.0, 10.0], # lat, lon (reversed) but within valid ranges 41 | [-10.0, 20.0], 42 | [10.0, 20.0], 43 | [10.0, 10.0], 44 | [-10.0, 10.0], 45 | ] 46 | ], 47 | }, 48 | "properties": {"datetime": "2023-01-01T00:00:00Z"}, 49 | } 50 | 51 | # Create a test item with coordinates that are clearly reversed (latitude > 90) 52 | clearly_incorrect_item = { 53 | "stac_version": "1.0.0", 54 | "stac_extensions": [], 55 | "type": "Feature", 56 | "id": "test-coordinates-clearly-incorrect", 57 | "bbox": [10.0, -10.0, 20.0, 10.0], 58 | "geometry": { 59 | "type": "Polygon", 60 | "coordinates": [ 61 | [ 62 | [10.0, 100.0], # Second value (latitude) > 90 63 | [20.0, 100.0], 64 | [20.0, 100.0], 65 | [10.0, 100.0], 66 | [10.0, 100.0], 67 | ] 68 | ], 69 | }, 70 | "properties": {"datetime": "2023-01-01T00:00:00Z"}, 71 | } 72 | 73 | # Create a test item with coordinates that may be reversed based on heuristic 74 | # (first value > 90, second value < 90, first value > second value*2) 75 | heuristic_incorrect_item = { 76 | "stac_version": "1.0.0", 77 | "stac_extensions": [], 78 | "type": "Feature", 79 | "id": "test-coordinates-heuristic-incorrect", 80 | "bbox": [10.0, -10.0, 20.0, 10.0], 81 | "geometry": { 82 | "type": "Polygon", 83 | "coordinates": [ 84 | [ 85 | [120.0, 40.0], # First value > 90, second < 90, first > second*2 86 | [120.0, 40.0], 87 | [120.0, 40.0], 88 | [120.0, 40.0], 89 | [120.0, 40.0], 90 | ] 91 | ], 92 | }, 93 | "properties": {"datetime": "2023-01-01T00:00:00Z"}, 94 | } 95 | 96 | # Test with correct coordinates - this should pass both checks 97 | linter = Linter(correct_item) 98 | assert linter.check_geometry_coordinates_order() == True 99 | assert linter.check_geometry_coordinates_definite_errors() == True 100 | 101 | # Test with reversed coordinates that are within valid ranges 102 | # Current implementation can't detect this case, so both checks pass 103 | linter = Linter(undetectable_reversed_item) 104 | assert ( 105 | linter.check_geometry_coordinates_order() == True 106 | ) # Passes because values are within valid ranges 107 | assert ( 108 | linter.check_geometry_coordinates_definite_errors() == True 109 | ) # Passes because values are within valid ranges 110 | 111 | # Test with clearly incorrect coordinates (latitude > 90) 112 | # This should fail the definite errors check but pass the order check (which now only uses heuristic) 113 | linter = Linter(clearly_incorrect_item) 114 | assert ( 115 | linter.check_geometry_coordinates_order() == True 116 | ) # Now passes because it only checks heuristic 117 | 118 | # Check that definite errors are detected 119 | result = linter.check_geometry_coordinates_definite_errors() 120 | assert result is not True # Should not be True 121 | assert isinstance(result, tuple) # Should be a tuple 122 | assert result[0] is False # First element should be False 123 | assert len(result[1]) > 0 # Should have at least one invalid coordinate 124 | assert result[1][0][1] == 100.0 # The latitude value should be 100.0 125 | assert "latitude > ±90°" in result[1][0][2] # Should indicate latitude error 126 | 127 | # Test with coordinates that trigger the heuristic 128 | # This should fail the order check but pass the definite errors check 129 | linter = Linter(heuristic_incorrect_item) 130 | assert ( 131 | linter.check_geometry_coordinates_order() == False 132 | ) # Fails because of heuristic 133 | assert ( 134 | linter.check_geometry_coordinates_definite_errors() == True 135 | ) # Passes because values are within valid ranges 136 | 137 | # Test that the best practices dictionary contains the appropriate error messages 138 | best_practices = linter.create_best_practices_dict() 139 | 140 | # For heuristic-based detection 141 | linter = Linter(heuristic_incorrect_item) 142 | best_practices = linter.create_best_practices_dict() 143 | assert "geometry_coordinates_order" in best_practices 144 | assert ( 145 | "may be in the wrong order" in best_practices["geometry_coordinates_order"][0] 146 | ) 147 | 148 | # For definite errors detection 149 | linter = Linter(clearly_incorrect_item) 150 | best_practices = linter.create_best_practices_dict() 151 | assert "geometry_coordinates_definite_errors" in best_practices 152 | assert ( 153 | "contain invalid values" 154 | in best_practices["geometry_coordinates_definite_errors"][0] 155 | ) 156 | 157 | 158 | def test_bbox_antimeridian(): 159 | """Test the check_bbox_antimeridian method for detecting incorrectly formatted bboxes that cross the antimeridian.""" 160 | # Create a test item with an incorrectly formatted bbox that belts the globe 161 | # instead of properly crossing the antimeridian 162 | incorrect_item = { 163 | "stac_version": "1.0.0", 164 | "stac_extensions": [], 165 | "type": "Feature", 166 | "id": "test-antimeridian-incorrect", 167 | "bbox": [ 168 | -170.0, # west 169 | -10.0, # south 170 | 170.0, # east (incorrect: this belts the globe instead of crossing the antimeridian) 171 | 10.0, # north 172 | ], 173 | "geometry": { 174 | "type": "Polygon", 175 | "coordinates": [ 176 | [ 177 | [170.0, -10.0], 178 | [-170.0, -10.0], 179 | [-170.0, 10.0], 180 | [170.0, 10.0], 181 | [170.0, -10.0], 182 | ] 183 | ], 184 | }, 185 | "properties": {"datetime": "2023-01-01T00:00:00Z"}, 186 | } 187 | 188 | # Create a test item with a correctly formatted bbox that crosses the antimeridian 189 | # (west > east for antimeridian crossing) 190 | correct_item = { 191 | "stac_version": "1.0.0", 192 | "stac_extensions": [], 193 | "type": "Feature", 194 | "id": "test-antimeridian-correct", 195 | "bbox": [ 196 | 170.0, # west 197 | -10.0, # south 198 | -170.0, # east (west > east indicates antimeridian crossing) 199 | 10.0, # north 200 | ], 201 | "geometry": { 202 | "type": "Polygon", 203 | "coordinates": [ 204 | [ 205 | [170.0, -10.0], 206 | [-170.0, -10.0], 207 | [-170.0, 10.0], 208 | [170.0, 10.0], 209 | [170.0, -10.0], 210 | ] 211 | ], 212 | }, 213 | "properties": {"datetime": "2023-01-01T00:00:00Z"}, 214 | } 215 | 216 | # Test with the incorrect item (belting the globe) 217 | linter = Linter(incorrect_item) 218 | # The check should return False for the incorrectly formatted bbox 219 | assert linter.check_bbox_antimeridian() == False 220 | 221 | # Verify that the best practices dictionary contains the appropriate message 222 | best_practices = linter.create_best_practices_dict() 223 | assert "check_bbox_antimeridian" in best_practices 224 | assert len(best_practices["check_bbox_antimeridian"]) == 2 225 | 226 | # Check that the error messages include the west and east longitude values 227 | west_val = incorrect_item["bbox"][0] 228 | east_val = incorrect_item["bbox"][2] 229 | assert ( 230 | f"(found west={west_val}, east={east_val})" 231 | in best_practices["check_bbox_antimeridian"][0] 232 | ) 233 | 234 | # Test with the correct item - this should pass 235 | linter = Linter(correct_item) 236 | # The check should return True for the correctly formatted bbox 237 | assert linter.check_bbox_antimeridian() == True 238 | 239 | # Test with a normal bbox that doesn't cross the antimeridian 240 | normal_item = { 241 | "stac_version": "1.0.0", 242 | "stac_extensions": [], 243 | "type": "Feature", 244 | "id": "test-normal-bbox", 245 | "bbox": [10.0, -10.0, 20.0, 10.0], # west # south # east # north 246 | "geometry": { 247 | "type": "Polygon", 248 | "coordinates": [ 249 | [ 250 | [10.0, -10.0], 251 | [20.0, -10.0], 252 | [20.0, 10.0], 253 | [10.0, 10.0], 254 | [10.0, -10.0], 255 | ] 256 | ], 257 | }, 258 | "properties": {"datetime": "2023-01-01T00:00:00Z"}, 259 | } 260 | 261 | # Test with a normal bbox - this should pass 262 | linter = Linter(normal_item) 263 | assert linter.check_bbox_antimeridian() == True 264 | 265 | 266 | def test_bbox_matches_geometry(): 267 | # Test with matching bbox and geometry 268 | file = "sample_files/1.0.0/core-item.json" 269 | linter = Linter(file) 270 | assert linter.check_bbox_matches_geometry() is True 271 | 272 | # Test with mismatched bbox and geometry 273 | mismatched_item = { 274 | "stac_version": "1.0.0", 275 | "stac_extensions": [], 276 | "type": "Feature", 277 | "id": "test-item", 278 | "bbox": [100.0, 0.0, 105.0, 1.0], # Deliberately wrong bbox 279 | "geometry": { 280 | "type": "Polygon", 281 | "coordinates": [ 282 | [ 283 | [172.91173669923782, 1.3438851951615003], 284 | [172.95469614953714, 1.3438851951615003], 285 | [172.95469614953714, 1.3690476620161975], 286 | [172.91173669923782, 1.3690476620161975], 287 | [172.91173669923782, 1.3438851951615003], 288 | ] 289 | ], 290 | }, 291 | "properties": {"datetime": "2020-12-11T22:38:32.125Z"}, 292 | } 293 | linter = Linter(mismatched_item) 294 | result = linter.check_bbox_matches_geometry() 295 | 296 | # Check that the result is a tuple and the first element is False 297 | assert isinstance(result, tuple) 298 | assert result[0] is False 299 | 300 | # Check that the tuple contains the expected elements (calculated bbox, actual bbox, differences) 301 | assert len(result) == 4 302 | calc_bbox, actual_bbox, differences = result[1], result[2], result[3] 303 | 304 | # Verify the calculated bbox matches the geometry coordinates 305 | assert calc_bbox == [ 306 | 172.91173669923782, 307 | 1.3438851951615003, 308 | 172.95469614953714, 309 | 1.3690476620161975, 310 | ] 311 | 312 | # Verify the actual bbox is what we provided 313 | assert actual_bbox == [100.0, 0.0, 105.0, 1.0] 314 | 315 | # Verify the differences are calculated correctly 316 | expected_differences = [abs(actual_bbox[i] - calc_bbox[i]) for i in range(4)] 317 | assert differences == expected_differences 318 | 319 | # Test with null geometry (should return True as check is not applicable) 320 | null_geom_item = { 321 | "stac_version": "1.0.0", 322 | "type": "Feature", 323 | "id": "test-item-null-geom", 324 | "bbox": [100.0, 0.0, 105.0, 1.0], 325 | "geometry": None, 326 | "properties": {"datetime": "2020-12-11T22:38:32.125Z"}, 327 | } 328 | linter = Linter(null_geom_item) 329 | assert linter.check_bbox_matches_geometry() is True 330 | 331 | # Test with missing bbox (should return True as check is not applicable) 332 | no_bbox_item = { 333 | "stac_version": "1.0.0", 334 | "type": "Feature", 335 | "id": "test-item-no-bbox", 336 | "geometry": { 337 | "type": "Polygon", 338 | "coordinates": [ 339 | [ 340 | [172.91173669923782, 1.3438851951615003], 341 | [172.95469614953714, 1.3438851951615003], 342 | [172.95469614953714, 1.3690476620161975], 343 | [172.91173669923782, 1.3690476620161975], 344 | [172.91173669923782, 1.3438851951615003], 345 | ] 346 | ], 347 | }, 348 | "properties": {"datetime": "2020-12-11T22:38:32.125Z"}, 349 | } 350 | linter = Linter(no_bbox_item) 351 | assert linter.check_bbox_matches_geometry() is True 352 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## STAC-CHECK Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is (loosely) based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). 6 | 7 | ## Unreleased 8 | 9 | ## [v1.11.1] - 2025-07-29 10 | 11 | ### Updated 12 | 13 | - Updated stac-validator dependency to v3.10.1 ([#140](https://github.com/stac-utils/stac-check/pull/140)) 14 | - Updated Github Actions and documentation dependencies ([#139](https://github.com/stac-utils/stac-check/pull/139)) 15 | 16 | ### Removed 17 | 18 | - Removed pdoc-generated documentation files and references as the project now uses Sphinx exclusively for documentation. ([#141](https://github.com/stac-utils/stac-check/pull/141)) 19 | 20 | ## [v1.11.0] - 2025-06-22 21 | 22 | ### Added 23 | 24 | - Results summary for options that produce numerous results, ie. --collections, --item-collection, --recursive ([#138](https://github.com/stac-utils/stac-check/pull/138)) 25 | - Support for --verbose flag to show verbose results summary ([#138](https://github.com/stac-utils/stac-check/pull/138)) 26 | - Added `--output`/`-o` option to save validation results to a file ([#138](https://github.com/stac-utils/stac-check/pull/138)) 27 | - Tests for CLI options ([#138](https://github.com/stac-utils/stac-check/pull/138)) 28 | 29 | ## [v1.10.1] - 2025-06-21 30 | 31 | ### Fixed 32 | 33 | - Fixed issue where pages parameter was being added to the wrong Linter ([#137](https://github.com/stac-utils/stac-check/pull/137)) 34 | 35 | ## [v1.10.0] - 2025-06-20 36 | 37 | ### Added 38 | 39 | - Created api_lint.py module to handle API linting ([#135](https://github.com/stac-utils/stac-check/pull/135)) 40 | - Added support for --item-collection flag to validate item collection responses ([#135](https://github.com/stac-utils/stac-check/pull/135)) 41 | - Added support for --collections flag to validate collections responses ([#135](https://github.com/stac-utils/stac-check/pull/135)) 42 | - Added support for --pages flag to limit the number of pages to validate ([#135](https://github.com/stac-utils/stac-check/pull/135)) 43 | 44 | 45 | ### Changed 46 | 47 | - Refactored display messages into a dedicated module for better code organization and maintainability ([#135](https://github.com/stac-utils/stac-check/pull/135)) 48 | - Organized test files, added v1.0.0 recursion test ([#135](https://github.com/stac-utils/stac-check/pull/135)) 49 | 50 | ## [v1.9.1] - 2025-06-16 51 | 52 | ### Added 53 | 54 | - Added display of failed schema information in the validation output ([#134](https://github.com/stac-utils/stac-check/pull/134)) 55 | - Added recommendation messages to guide users when validation fails ([#134](https://github.com/stac-utils/stac-check/pull/134)) 56 | - Added disclaimer about schema-based STAC validation being an initial indicator of validity only ([#134](https://github.com/stac-utils/stac-check/pull/134)) 57 | 58 | ### Changed 59 | 60 | - Updated validation output to show "Passed" instead of "Valid" for accuracy ([#134](https://github.com/stac-utils/stac-check/pull/134)) 61 | 62 | 63 | ## [v1.9.0] - 2025-06-13 64 | 65 | ### Added 66 | 67 | - Added support for --verbose flag to show verbose error messages ([#132](https://github.com/stac-utils/stac-check/pull/132)) 68 | 69 | ### Changed 70 | 71 | - Updated stac-validator to v3.9.0 ([#132](https://github.com/stac-utils/stac-check/pull/132)) 72 | - Improved cli output, message formatting ([#132](https://github.com/stac-utils/stac-check/pull/132)) 73 | 74 | ## [v1.8.0] - 2025-06-11 75 | 76 | ### Changed 77 | 78 | - Made `stac-pydantic` an optional dependency ([#129](https://github.com/stac-utils/stac-check/pull/129)) 79 | - `stac-validator` is now installed without the `[pydantic]` extra by default 80 | - Added `stac-check[pydantic]` extra for users who need pydantic validation 81 | - Added graceful fallback to JSONSchema validation when pydantic is not available 82 | - Updated tests to handle both scenarios (with and without pydantic installed) 83 | - Added helpful warning messages when pydantic is requested but not installed 84 | 85 | - Migrated documentation from Read the Docs to GitHub Pages ([#128](https://github.com/stac-utils/stac-check/pull/128)) 86 | - Updated documentation build system to use Sphinx with sphinx_rtd_theme 87 | - Added support for Markdown content in documentation using myst-parser 88 | - Updated README with instructions for building documentation locally 89 | - Added GitHub Actions workflow for automatic documentation deployment 90 | 91 | ## [v1.7.0] - 2025-06-01 92 | 93 | ### Added 94 | 95 | - Added validation for bounding boxes that cross the antimeridian (180°/-180° longitude) ([#121](https://github.com/stac-utils/stac-check/pull/121)) 96 | - Checks that bbox coordinates follow the GeoJSON specification for antimeridian crossing 97 | - Detects and reports cases where a bbox incorrectly "belts the globe" instead of properly crossing the antimeridian 98 | - Provides clear error messages to help users fix incorrectly formatted bboxes 99 | - Added sponsors and supporters section with logos ([#122](https://github.com/stac-utils/stac-check/pull/122)) 100 | - Added check to verify that bbox matches item's polygon geometry ([#123](https://github.com/stac-utils/stac-check/pull/123)) 101 | - Added configuration documentation to README ([#124](https://github.com/stac-utils/stac-check/pull/124)) 102 | - Added validation for geometry coordinates order to detect potentially reversed lat/lon coordinates ([#125](https://github.com/stac-utils/stac-check/pull/125)) 103 | - Checks that coordinates follow the GeoJSON specification with [longitude, latitude] order 104 | - Uses heuristics to identify coordinates that may be reversed or contain errors 105 | - Provides nuanced error messages acknowledging the uncertainty in coordinate validation 106 | - Added validation for definite geometry coordinate errors ([#125](https://github.com/stac-utils/stac-check/pull/125)) 107 | - Detects coordinates with latitude values exceeding ±90 degrees 108 | - Detects coordinates with longitude values exceeding ±180 degrees 109 | - Returns detailed information about invalid coordinates 110 | - Added dedicated geometry validation configuration section ([#125](https://github.com/stac-utils/stac-check/pull/125)) 111 | - Created a new `geometry_validation` section in the configuration file 112 | - Added a master enable/disable switch for all geometry validation checks 113 | - Reorganized geometry validation options into the new section 114 | - Separated geometry validation errors in CLI output with a [BETA] label 115 | - Added detailed documentation for geometry validation features 116 | - Added `--pydantic` option for validating STAC objects using stac-pydantic models, providing enhanced type checking and validation ([#126](https://github.com/stac-utils/stac-check/pull/126)) 117 | 118 | ### Enhanced 119 | 120 | - Improved bbox validation output to show detailed information about mismatches between bbox and geometry bounds, including which specific coordinates differ and by how much ([#126](https://github.com/stac-utils/stac-check/pull/126)) 121 | 122 | ### Fixed 123 | 124 | - Fixed collection summaries check incorrectly showing messages for Item assets ([#121](https://github.com/stac-utils/stac-check/pull/127)) 125 | 126 | ### Updated 127 | 128 | - Improved README with table of contents, better formatting, stac-check logo, and enhanced documentation ([#122](https://github.com/stac-utils/stac-check/pull/122)) 129 | - Enhanced Contributing guidelines with step-by-step instructions ([#122](https://github.com/stac-utils/stac-check/pull/122)) 130 | 131 | ### Removed 132 | 133 | - Support for Python 3.8 ([#121](https://github.com/stac-utils/stac-check/pull/121)) 134 | 135 | ## [v1.6.0] - 2025-03-14 136 | 137 | ### Added 138 | 139 | - Test for Python 3.13 in workflow ([#120](https://github.com/stac-utils/stac-check/pull/120)) 140 | 141 | ### Fixed 142 | 143 | - Prevented `KeyError` in `check_unlocated()` when `bbox` is unset ([#104](https://github.com/stac-utils/stac-check/pull/119)) 144 | 145 | ### Updated 146 | 147 | - Updated stac-validator to v3.6.0 ([#120](https://github.com/stac-utils/stac-check/pull/120)) 148 | 149 | ## [v1.5.0] - 2025-01-17 150 | 151 | ### Added 152 | 153 | - Allow to provide HTTP headers ([#114](https://github.com/stac-utils/stac-check/pull/114)) 154 | - Configure whether to open URLs when validating assets ([#114](https://github.com/stac-utils/stac-check/pull/114)) 155 | 156 | ### Changed 157 | 158 | - No longer use the deprecated pkg-resources package. 159 | It has been replaced with importlib from the Python standard library 160 | ([#112](https://github.com/stac-utils/stac-check/pull/112)) 161 | 162 | ### Updated 163 | 164 | - Updated stac-validator to v3.5.0 and other dependecies as well ([#116](https://github.com/stac-utils/stac-check/pull/116)) 165 | 166 | ## [v1.4.0] - 2024-10-09 167 | 168 | ### Added 169 | 170 | - Added pre-commit config ([#111](https://github.com/stac-utils/stac-check/pull/111)) 171 | - Added publish.yml to automatically publish new releases to PyPI ([#111](https://github.com/stac-utils/stac-check/pull/111)) 172 | 173 | ### Changed 174 | 175 | - Updated stac-validator dependency to ensure STAC v1.1.0 compliance ([#111](https://github.com/stac-utils/stac-check/pull/111)) 176 | 177 | ## [v1.3.3] - 2023-11-17 178 | 179 | ### Changed 180 | 181 | - Development dependencies removed from runtime dependency list 182 | ([#109](https://github.com/stac-utils/stac-check/pull/109)) 183 | 184 | ## [v1.3.2] - 2023-03-23 185 | 186 | ### Added 187 | 188 | - Ability to lint dictionaries https://github.com/stac-utils/stac-check/pull/94 189 | - Docstrings and pdoc api documents 190 | 191 | ### Fixed 192 | 193 | - Fixed the check_catalog_file_name() method to only work on static catalogs https://github.com/stac-utils/stac-check/pull/94 194 | - Jsonschema version to use a released version https://github.com/stac-utils/stac-check/pull/105 195 | 196 | ## [v1.3.1] - 2022-10-05 197 | 198 | ### Changed 199 | 200 | - Changed pin on stac-validator to >=3.1.0 from ==3.2.0 201 | 202 | ## [v1.3.0] - 2022-09-20 203 | 204 | ### Added 205 | 206 | - recursive mode lints assets https://github.com/stac-utils/stac-check/pull/84 207 | 208 | ### Changed 209 | 210 | - recursive mode swaps pystac for stac-validator https://github.com/stac-utils/stac-check/pull/84 211 | 212 | ### Fixed 213 | 214 | - fix catalog file name check https://github.com/stac-utils/stac-check/pull/83 215 | 216 | ## [v1.2.0] - 2022-04-26 217 | 218 | ### Added 219 | 220 | - Option to include a configuration file to ignore selected checks 221 | 222 | ### Changed 223 | 224 | - Change name from stac_check to stac-check in setup for cli 225 | 226 | ### Fixed 227 | 228 | - Fix thumbnail size check 229 | 230 | ## [v1.1.2] - 2022-03-03 231 | 232 | ### Changed 233 | 234 | - Make it easier to export linting messages 235 | - Set stac-validator version to 2.4.0 236 | 237 | ### Fixed 238 | 239 | - Fix self-link test 240 | 241 | ## [v1.0.1] - 2022-02-20 242 | 243 | ### Changed 244 | 245 | - Update readme 246 | - Reorganized code for version 1.0.0 release 247 | 248 | ## [v0.2.0] - 2022-02-02 - 2022-02-19 249 | 250 | ### Added 251 | 252 | - Import main validator as stac-validator was updated to 2.3.0 253 | - Added best practices docuument to repo 254 | - Recommend 'self' link in links 255 | - Check catalogs and collections use 'catalog.json' or 'collection.json' as a file name 256 | - Check that links in collections and catalogs have a title field 257 | - Recommend that eo:bands or similar information is provided in collection summaries 258 | - Check for small thumbnail image file type 259 | 260 | ## [v0.1.3] - 2022-01-23 261 | 262 | ### Added 263 | 264 | - Check for bloated metadata, too many fields in properties 265 | - Check for geometry field, recommend that STAC not be used for non-spatial data 266 | 267 | ### Changed 268 | 269 | - Changed bloated links check to a boolean to mirror bloated metadata 270 | 271 | ## [v0.1.2] - 2022-01-17 - 2022-01-22 272 | 273 | ### Added 274 | 275 | - Check for null datetime 276 | - Check for unlocated items, bbox should be set to null if geometry is 277 | 278 | ## [v0.1.1] - 2021-11-26 - 2021-12-12 279 | 280 | ### Added 281 | 282 | - Added github actions to test and push to pypi 283 | - Added makefile, dockerfile 284 | 285 | ### Changed 286 | 287 | - Removed pipenv 288 | 289 | ## [v0.1.0] - 2021-11-26 - 2021-12-05 290 | 291 | ### Added 292 | 293 | - Best practices - searchable identifiers - lowercase, numbers, '\_' or '-' 294 | for id names 295 | https://github.com/radiantearth/stac-spec/blob/master/best-practices.md#searchable-identifiers 296 | - Best practices ensure item ids don't contain ':' or '/' characters 297 | https://github.com/radiantearth/stac-spec/blob/master/best-practices.md#item-ids 298 | - Best practices check for item ids to see if they match file names 299 | - Add url support, check for valid urls, validate urls 300 | - Add pystac validate_all to new cli option -> recursive 301 | - Update pystac from 0.5.6 to 1.1.0 302 | - Move stac-validator 2.3.0 into repository 303 | - Best practices check for too many links in object 304 | - Best practices check for summaries in collections 305 | - Validation from stac-validator 2.3.0 306 | - Links and assets validation checks 307 | 308 | [Unreleased]: https://github.com/stac-utils/stac-check/compare/v1.11.1...main 309 | [v1.11.1]: https://github.com/stac-utils/stac-check/compare/v1.11.0...v1.11.1 310 | [v1.11.0]: https://github.com/stac-utils/stac-check/compare/v1.10.1...v1.11.0 311 | [v1.10.1]: https://github.com/stac-utils/stac-check/compare/v1.10.0...v1.10.1 312 | [v1.10.0]: https://github.com/stac-utils/stac-check/compare/v1.9.1...v1.10.0 313 | [v1.9.1]: https://github.com/stac-utils/stac-check/compare/v1.9.0...v1.9.1 314 | [v1.9.0]: https://github.com/stac-utils/stac-check/compare/v1.8.0...v1.9.0 315 | [v1.8.0]: https://github.com/stac-utils/stac-check/compare/v1.7.0...v1.8.0 316 | [v1.7.0]: https://github.com/stac-utils/stac-check/compare/v1.6.0...v1.7.0 317 | [v1.6.0]: https://github.com/stac-utils/stac-check/compare/v1.5.0...v1.6.0 318 | [v1.5.0]: https://github.com/stac-utils/stac-check/compare/v1.4.0...v1.5.0 319 | [v1.4.0]: https://github.com/stac-utils/stac-check/compare/v1.3.3...v1.4.0 320 | [v1.3.3]: https://github.com/stac-utils/stac-check/compare/v1.3.2...v1.3.3 321 | [v1.3.2]: https://github.com/stac-utils/stac-check/compare/v1.3.1...v1.3.2 322 | [v1.3.1]: https://github.com/stac-utils/stac-check/compare/v1.3.0...v1.3.1 323 | [v1.3.0]: https://github.com/stac-utils/stac-check/compare/v1.2.0...v1.3.0 324 | [v1.2.0]: https://github.com/stac-utils/stac-check/compare/v1.1.2...v1.2.0 325 | [v1.1.2]: https://github.com/stac-utils/stac-check/compare/v1.0.1...v1.1.2 326 | [v1.0.1]: https://github.com/stac-utils/stac-check/compare/v0.2.0...v1.0.1 327 | [v0.2.0]: https://github.com/stac-utils/stac-check/compare/v0.1.3...v0.2.0 328 | [v0.1.3]: https://github.com/stac-utils/stac-check/compare/v0.1.2...v0.1.3 329 | [v0.1.2]: https://github.com/stac-utils/stac-check/compare/v0.1.1...v0.1.2 330 | [v0.1.1]: https://github.com/stac-utils/stac-check/compare/v0.1.0...v0.1.1 331 | [v0.1.0]: https://github.com/stac-utils/stac-check/releases/tag/v0.1.0 332 | --------------------------------------------------------------------------------