Source code for stac_check.cli

import importlib.metadata
import json
import sys
from typing import Optional

import click
import requests
from stac_validator.utilities import is_valid_url

from stac_check.api_lint import ApiLinter
from stac_check.display_messages import (
    cli_message,
    collections_message,
    intro_message,
    item_collection_message,
    recursive_message,
)
from stac_check.lint import Linter
from stac_check.utilities import handle_output


[docs] def is_item_collection(file: str, headers: dict = None) -> bool: """Detect if a file is an item collection (FeatureCollection with features). Args: file: Path or URL to the file headers: Optional HTTP headers for URL requests Returns: True if the file is an item collection, False otherwise """ try: if is_valid_url(file): resp = requests.get(file, headers=headers or {}) data = resp.json() else: with open(file) as f: data = json.load(f) # Check if it's a FeatureCollection with features return ( isinstance(data, dict) and data.get("type") == "FeatureCollection" and "features" in data and isinstance(data.get("features"), list) and len(data.get("features", [])) > 0 ) except Exception: # If we can't determine, return False return False
@click.option( "--collections", is_flag=True, help="Validate collections endpoint response. Can be combined with --pages. Defaults to one page.", ) @click.option( "--item-collection", is_flag=True, help="Validate item collection response. Can be combined with --pages. Defaults to one page.", ) @click.option( "--pages", "-p", type=int, help="Maximum number of pages to validate via --item-collection or --collections. Defaults to one page.", ) @click.option( "--recursive", "-r", is_flag=True, help="Recursively validate all related stac objects.", ) @click.option( "--max-depth", "-m", type=int, help="Maximum depth to traverse when recursing. Omit this argument to get full recursion. Ignored if `recursive == False`.", ) @click.option( "-a", "--assets", is_flag=True, help="Validate assets for format and response." ) @click.option( "-l", "--links", is_flag=True, help="Validate links for format and response." ) @click.option( "--output", "-o", type=click.Path(dir_okay=False, writable=True), help="Save output to the specified file. Only works with --collections, --item-collection, or --recursive.", ) @click.option( "--no-assets-urls", is_flag=True, help="Disables the opening of href links when validating assets (enabled by default).", ) @click.option( "--header", type=(str, str), multiple=True, help="HTTP header to include in the requests. Can be used multiple times.", ) @click.option( "--pydantic", is_flag=True, help="Use pydantic validation (requires stac-pydantic to be installed).", ) @click.option( "--verbose", "-v", is_flag=True, help="Enable verbose output.", ) @click.option( "--fast", is_flag=True, help="Use FastJSONSchema for high-speed validation. Skips geometry checks and linting for maximum performance.", ) @click.option( "--fast-linting", is_flag=True, help="Use FastJSONSchema for high-speed validation with linting. Skips geometry checks for maximum performance.", ) @click.command() @click.argument("file") @click.version_option(version=importlib.metadata.distribution("stac-check").version) def main( file: str, collections: bool, item_collection: bool, pages: Optional[int], recursive: bool, max_depth: Optional[int], assets: bool, links: bool, no_assets_urls: bool, header: tuple[tuple[str, str], ...], pydantic: bool, verbose: bool, output: Optional[str], fast: bool, fast_linting: bool, ) -> None: """Main entry point for the stac-check CLI. Args: file: The STAC file or URL to validate collections: Validate a collections endpoint item_collection: Validate an item collection pages: Number of pages to validate (for API endpoints) recursive: Recursively validate linked STAC objects max_depth: Maximum depth for recursive validation assets: Validate assets links: Validate links no_assets_urls: Disable URL validation for assets header: Additional HTTP headers pydantic: Use stac-pydantic for validation verbose: Show verbose output output: Save output to file (only with --collections, --item-collection, or --recursive) fast: Fast validation mode (skips geometry checks and linting for maximum performance) fast_linting: Fast validation mode with linting (skips geometry checks for maximum performance) """ # Resolve fast/fast-linting flags if fast_linting: fast = True # Check if output is used without --collections, --item-collection, or --recursive if output and not any([collections, item_collection, recursive]): click.echo( "Error: --output can only be used with --collections, --item-collection, or --recursive", err=True, ) sys.exit(1) # Check if pydantic validation is requested but not installed if pydantic: try: importlib.import_module("stac_pydantic") except ImportError: click.secho( "Warning: stac-pydantic is not installed. Pydantic validation will be disabled.\n" "To enable pydantic validation, install it with: pip install stac-check[pydantic]", fg="yellow", ) pydantic = False # Auto-detect item collection if no explicit flag is set if not collections and not item_collection and not recursive: if is_item_collection(file, headers=dict(header)): item_collection = True if collections or item_collection: # Handle API-based validation (collections or item collections) api_linter = ApiLinter( source=file, object_list_key="collections" if collections else "features", pages=pages if pages else 1, headers=dict(header), verbose=verbose, fast=fast, fast_linting=fast_linting, ) results = api_linter.lint_all() # Create a dummy Linter instance for display purposes display_linter = Linter( file, assets=assets, links=links, headers=dict(header), pydantic=pydantic, verbose=verbose, fast=fast, fast_linting=fast_linting, ) # Show intro message in the terminal intro_message(display_linter) # Define output generation function (without intro message since we already showed it) def generate_output(): if collections: collections_message( api_linter, results=results, cli_message_func=cli_message, verbose=verbose, ) elif item_collection: item_collection_message( api_linter, results=results, cli_message_func=cli_message, verbose=verbose, ) # Handle output (without duplicating the intro message) handle_output(output, generate_output) sys.exit(0 if all(msg.get("valid_stac") is True for msg in results) else 1) else: # Handle file-based validation (single file or recursive) linter = Linter( file, assets=assets, links=links, recursive=recursive, max_depth=max_depth, assets_open_urls=not no_assets_urls, headers=dict(header), pydantic=pydantic, verbose=verbose, fast=fast, fast_linting=fast_linting, ) intro_message(linter) # Define output generation function (without intro message since we already showed it) def generate_output(): if recursive: recursive_message(linter, cli_message_func=cli_message, verbose=verbose) elif fast: # For fast mode, use item_collection_message to show compact summary # even for single items from stac_check.display_messages import _display_fast_validation_summary result = { "path": file, "valid_stac": linter.valid_stac, "asset_type": linter.asset_type, "version": linter.version, "validation_method": "FastJSONSchema", "error_type": linter.error_type, "error_message": linter.error_msg, "best_practices": linter.best_practices_msg, "geometry_errors": [], "schema": linter.schema, "original_object": linter.data, } results = [result] _display_fast_validation_summary( results, total_time=( linter.total_time if hasattr(linter, "total_time") else 0 ), schemas=( linter.schemas_checked if hasattr(linter, "schemas_checked") else None ), ) else: cli_message(linter) # Handle output (without duplicating the intro message) handle_output(output if recursive else None, generate_output) sys.exit(0 if linter.valid_stac else 1)