[Git][debian-gis-team/stac-check][upstream] New upstream version 1.14.0

Antonio Valentino (@antonio.valentino) gitlab at salsa.debian.org
Sun May 10 10:33:03 BST 2026



Antonio Valentino pushed to branch upstream at Debian GIS Project / stac-check


Commits:
265c746a by Antonio Valentino at 2026-05-10T09:06:48+00:00
New upstream version 1.14.0
- - - - -


17 changed files:

- + .github/PULL_REQUEST_TEMPLATE.md
- + .github/dependabot.yml
- .github/workflows/docs.yml
- .github/workflows/publish.yml
- .github/workflows/test-runner.yml
- CHANGELOG.md
- README.md
- + pyproject.toml
- sample_files/1.1.0/extended-item.json
- + sample_files/sentinel_s2l2a_cogs_100.json
- − setup.py
- stac_check/api_lint.py
- stac_check/cli.py
- stac_check/display_messages.py
- + stac_check/fast_validator_wrapper.py
- stac_check/lint.py
- tests/test_cli.py


Changes:

=====================================
.github/PULL_REQUEST_TEMPLATE.md
=====================================
@@ -0,0 +1,24 @@
+## Technical Context
+- **Related Issues:** Fixes # (issue number)
+- **Breaking Changes:** [Yes/No] (If yes, please describe the impact and migration path)
+
+## Description
+> Provide a brief summary of the changes, the rationale behind them, and any specific areas you'd like the reviewers to focus on.
+
+---
+
+## Checklist
+- [ ] **Linting:** Code is formatted and linted.
+- [ ] **Tests:** Tests pass. I have included new tests for these changes where applicable.
+- [ ] **Edge Cases:** I have manually verified "unhappy paths" and edge cases beyond the basic success criteria (e.g., database connection timeouts, malformed input, strict mapping rejections).
+- [ ] **Documentation:** I have updated `README.md` to reflect any new environment variables, configuration changes, or breaking updates.
+- [ ] **Accountability:** I can explain the implementation logic for every line of code submitted and take full responsibility for its maintenance.
+
+---
+
+## AI Policy Compliance
+- [ ] I confirm this PR meets the following standards:
+
+> **Policy:** We require a "human-in-the-loop." You are the author and are fully accountable for all submitted code. Please ensure all tool-generated content is thoroughly reviewed before submission to ensure it is not an "extractive contribution" that squanders maintainer time.
+
+For more information, see our [AI contribution policy](https://stac-utils.github.io/ai-contribution-policy).
\ No newline at end of file


=====================================
.github/dependabot.yml
=====================================
@@ -0,0 +1,31 @@
+version: 2
+updates:
+  - package-ecosystem: "pip"
+    directory: "/"
+    schedule:
+      interval: "weekly"
+      day: "monday"
+      time: "03:00"
+    open-pull-requests-limit: 10
+    reviewers:
+      - "jonhealy1"
+    commit-message:
+      prefix: "chore(deps):"
+      include: "scope"
+    pull-request-branch-name:
+      separator: "/"
+    allow:
+      - dependency-type: "all"
+
+  - package-ecosystem: "github-actions"
+    directory: "/"
+    schedule:
+      interval: "weekly"
+      day: "monday"
+      time: "04:00"
+    open-pull-requests-limit: 5
+    reviewers:
+      - "jonhealy1"
+    commit-message:
+      prefix: "ci(deps):"
+      include: "scope"
\ No newline at end of file


=====================================
.github/workflows/docs.yml
=====================================
@@ -20,9 +20,9 @@ jobs:
   build:
     runs-on: ubuntu-latest
     steps:
-    - uses: actions/checkout at v4
+    - uses: actions/checkout at v6
     - name: Set up Python
-      uses: actions/setup-python at v5
+      uses: actions/setup-python at v6
       with:
         python-version: '3.11'
     - name: Install dependencies
@@ -33,7 +33,7 @@ jobs:
       run: |
         sphinx-build -b html docs/ docs/_build/html
     - name: Upload documentation artifact
-      uses: actions/upload-artifact at v4
+      uses: actions/upload-artifact at v7
       with:
         name: documentation
         path: docs/_build/html
@@ -48,7 +48,7 @@ jobs:
       contents: write
     steps:
       - name: Download built documentation
-        uses: actions/download-artifact at v4
+        uses: actions/download-artifact at v8
         with:
           name: documentation
           path: ./docs-build


=====================================
.github/workflows/publish.yml
=====================================
@@ -11,21 +11,21 @@ jobs:
     runs-on: ubuntu-latest
 
     steps:
-      - uses: actions/checkout at v4
+      - uses: actions/checkout at v6
 
       - name: Set up Python 3.10
-        uses: actions/setup-python at v5
+        uses: actions/setup-python at v6
         with:
           python-version: "3.10"
 
       - name: Install dependencies
         run: |
           python -m pip install --upgrade pip
-          pip install setuptools wheel twine
+          pip install build twine
 
       - name: Build package
         run: |
-          python setup.py sdist bdist_wheel
+          python -m build
 
       - name: Publish package to PyPI
         env:


=====================================
.github/workflows/test-runner.yml
=====================================
@@ -16,10 +16,10 @@ jobs:
     runs-on: ubuntu-latest
 
     steps:
-      - uses: actions/checkout at v4
+      - uses: actions/checkout at v6
 
       - name: Set up Python 3.12
-        uses: actions/setup-python at v5
+        uses: actions/setup-python at v6
         with:
           python-version: "3.12"
 
@@ -44,10 +44,10 @@ jobs:
         python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
 
     steps:
-      - uses: actions/checkout at v4
+      - uses: actions/checkout at v6
 
       - name: Set up Python ${{ matrix.python-version }}
-        uses: actions/setup-python at v5
+        uses: actions/setup-python at v6
         with:
           python-version: ${{ matrix.python-version }}
 


=====================================
CHANGELOG.md
=====================================
@@ -6,6 +6,36 @@ The format is (loosely) based on [Keep a Changelog](http://keepachangelog.com/)
 
 ## Unreleased
 
+## [v1.14.0] - 2025-05-02
+
+### Added
+
+- Added `--fast-linting` flag for fast validation with best practices linting (skips geometry checks) ([#152](https://github.com/stac-utils/stac-check/pull/152))
+- Pull request template ([#153](https://github.com/stac-utils/stac-check/pull/153))
+
+## [v1.13.0] - 2026-04-29
+
+### Added
+
+- Support for FastJSONSchema validation method with --fast flag to validate items, item collections ([#150](https://github.com/stac-utils/stac-check/pull/150))
+
+### Updated
+
+- Updated publish script to use pyproject.toml instead of setup.py ([#149](https://github.com/stac-utils/stac-check/pull/149))
+
+## [v1.12.0] - 2026-04-28
+
+### Added
+
+- Support for stac-validator v4 (pip install stac-valid~=4.2.0)([#143](https://github.com/stac-utils/stac-check/pull/143))
+- pyproject.toml configuration file ([#143](https://github.com/stac-utils/stac-check/pull/143))
+- Dependabot configuration for automated dependency updates ([#143](https://github.com/stac-utils/stac-check/pull/143))
+
+### Changed
+
+- Updated dependencies and build system to use pyproject.toml ([#143](https://github.com/stac-utils/stac-check/pull/143))
+- Allowed automatic check for validating item collections without needing to specify --item-collection flag ([#143](https://github.com/stac-utils/stac-check/pull/143))
+
 ## [v1.11.1] - 2025-07-29
 
 ### Updated
@@ -305,7 +335,10 @@ The format is (loosely) based on [Keep a Changelog](http://keepachangelog.com/)
 - Validation from stac-validator 2.3.0
 - Links and assets validation checks
 
-[Unreleased]: https://github.com/stac-utils/stac-check/compare/v1.11.1...main
+[Unreleased]: https://github.com/stac-utils/stac-check/compare/v1.14.0...main
+[v1.14.0]: https://github.com/stac-utils/stac-check/compare/v1.13.0...v1.14.0
+[v1.13.0]: https://github.com/stac-utils/stac-check/compare/v1.12.0...v1.13.0
+[v1.12.0]: https://github.com/stac-utils/stac-check/compare/v1.11.1...v1.12.0
 [v1.11.1]: https://github.com/stac-utils/stac-check/compare/v1.11.0...v1.11.1
 [v1.11.0]: https://github.com/stac-utils/stac-check/compare/v1.10.1...v1.11.0
 [v1.10.1]: https://github.com/stac-utils/stac-check/compare/v1.10.0...v1.10.1


=====================================
README.md
=====================================
@@ -35,6 +35,7 @@ The intent of this project is to provide a validation tool that also follows the
   - [Link and Asset Validation](#link-and-asset-validation)
   - [Invalid STAC](#invalid-stac)
   - [Using HTTP Headers](#using-http-headers)
+  - [Fast Validation](#fast-validation)
   - [STAC API Validation](#stac-api-validation)
 - [Development](#development)
   - [Building Documentation](#building-documentation)
@@ -94,6 +95,10 @@ Options:
   --pydantic               Use stac-pydantic for enhanced validation with Pydantic models.
   --verbose                Show verbose error messages.
   -o, --output FILE        Save output to the specified file.
+  --fast                   Use FastJSONSchema for high-speed validation. Skips geometry
+                           checks and best practices linting for maximum performance.
+  --fast-linting           Use FastJSONSchema for high-speed validation with best
+                           practices linting. Skips geometry checks for maximum performance.
   --item-collection        Validate item collection response. Can be combined with
                            --pages. Defaults to one page.
   --collections            Validate collections endpoint response. Can be combined with
@@ -211,7 +216,7 @@ stac-check sample_files/0.9.0/landsat8-sample.json
 
 Please upgrade from version 0.9.0 to version 1.1.0!
 
-Validator: stac-validator 3.9.1
+Validator: stac-validator 4.2.1
 
 Valid ITEM: True
 
@@ -243,7 +248,7 @@ stac-check https://raw.githubusercontent.com/stac-utils/pystac/main/tests/data-f
 
 Please upgrade from version 0.9.0 to version 1.1.0!
 
-Validator: stac-validator 3.9.1
+Validator: stac-validator 4.2.1
 
 
 Recursive: Validate all assets in a collection or catalog
@@ -289,7 +294,7 @@ stac-check sample_files/1.0.0/core-item.json --assets
 
 Please upgrade from version 1.0.0 to version 1.1.0!
 
-Validator: stac-validator 3.9.1
+Validator: stac-validator 4.2.1
 
 Valid ITEM: True
 
@@ -327,7 +332,7 @@ stac-check sample_files/1.0.0/core-item-bad-links.json --links --assets
 
 Please upgrade from version 1.0.0 to version 1.1.0!
 
-Validator: stac-validator 3.9.1
+Validator: stac-validator 4.2.1
 
 Valid ITEM: True
 
@@ -374,7 +379,7 @@ stac-check sample_files/0.9.0/bad-item.json
 
 Please upgrade from version 0.9.0 to version 1.1.0!
 
-Validator: stac-validator 3.9.1
+Validator: stac-validator 4.2.1
 
 Valid : False
 
@@ -403,7 +408,7 @@ stac-check https://stac-catalog.eu/collections/sentinel-s2-l2a/items/item1 --ass
 
 Please upgrade from version 1.0.0 to version 1.1.0!
 
-Validator: stac-validator 3.9.1
+Validator: stac-validator 4.2.1
 
 Valid ITEM: True
 
@@ -421,6 +426,75 @@ No ASSET format errors!
 This object has 4 links
 </pre>
 
+### Fast Validation
+
+For large STAC collections or when you need maximum validation speed, use the `--fast` or `--fast-linting` flags to enable FastJSONSchema validation. Both modes skip geometry checks for maximum performance.
+
+**Two Fast Validation Modes:**
+
+- **`--fast`**: Fastest mode - skips geometry checks and best practices linting, focuses only on STAC spec compliance
+- **`--fast-linting`**: Fast mode with linting - skips geometry checks but includes best practices checks for code quality
+
+**Fast Validation (Spec Only) of a Large Item Collection:**
+
+```bash
+stac-check large_collection.json --fast
+```
+
+<pre><b>stac-check: STAC spec validation and linting tool</b>
+
+Validator: stac-valid 4.2.1
+
+Validation method: FastJSONSchema
+
+ Validation Summary
+
+✅ Passed: 998/1000
+❌ Failed: 2/1000
+
+⚡ Timing Information:
+  Total Validation Time: 245.67 ms
+  Average per Object: 0.246 ms
+
+Schemas checked:
+    https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json
+    https://stac-extensions.github.io/eo/v1.0.0/schema.json
+    https://stac-extensions.github.io/projection/v1.0.0/schema.json
+
+ Validation Errors
+
+❌ STAC Spec Violation: Missing {'rel': 'collection'} in links array.
+   Affected Items: 2 | Examples: item_1, item_2
+</pre>
+
+**Fast Validation with Best Practices Linting:**
+
+Use `--fast-linting` to include best practices checks while maintaining fast performance:
+
+```bash
+stac-check large_collection.json --fast-linting
+```
+
+This will show both validation errors and best practices warnings in a compact, grouped format.
+
+**Performance Comparison:**
+
+- **`--fast` mode**: ~0.25ms per item (spec compliance only)
+- **`--fast-linting` mode**: ~0.5ms per item (spec + best practices)
+- **Regular mode**: ~1-2ms per item (includes geometry checks)
+- **Speedup**: 4-8x faster for large collections with `--fast`
+
+Use `--fast` when:
+- Validating large collections (100+ items)
+- You only need STAC spec compliance
+- You want maximum validation speed
+
+Use `--fast-linting` when:
+- Validating large collections with best practices checks
+- You want to ensure code quality without geometry validation
+- You need a balance between speed and thoroughness
+- Performance is critical
+
 ### STAC API Validation
 
 stac-check can validate STAC API endpoints, including item collections and collections endpoints. It supports pagination and can validate multiple pages of results.
@@ -433,7 +507,7 @@ stac-check https://stac.geobon.org/collections/chelsa-clim/items --item-collecti
 
 <pre><b>stac-check: STAC spec validation and linting tool</b>
 
-Validator: stac-validator 3.9.1
+Validator: stac-validator 4.2.1
 
 Item Collection: Validate all assets in a feature collection
 Pages = 1
@@ -454,7 +528,7 @@ stac-check https://stac.geobon.org/collections/chelsa-clim/items --item-collecti
 
 <pre><b>stac-check: STAC spec validation and linting tool</b>
 
-Validator: stac-validator 3.9.1
+Validator: stac-validator 4.2.1
 
 Item Collection: Validate all assets in a feature collection
 Pages = 3
@@ -475,7 +549,7 @@ stac-check https://stac.geobon.org/collections --collections
 
 <pre><b>stac-check: STAC spec validation and linting tool</b>
 
-Validator: stac-validator 3.9.1
+Validator: stac-validator 4.2.1
 
 Collections: Validate all collections in a STAC API
 Pages = 1


=====================================
pyproject.toml
=====================================
@@ -0,0 +1,50 @@
+[build-system]
+requires = ["setuptools>=45", "wheel"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "stac_check"
+version = "1.14.0"
+description = "Linting and validation tool for STAC assets"
+readme = "README.md"
+requires-python = ">=3.9"
+license = {text = "MIT"}
+authors = [
+    {name = "Jonathan Healy", email = "jonathan.d.healy at gmail.com"}
+]
+dependencies = [
+    "requests>=2.32.4",
+    "jsonschema>=4.25.0",
+    "click>=8.1.8",
+    "stac-valid~=4.2.2",
+    "PyYAML",
+    "python-dotenv",
+]
+
+[project.optional-dependencies]
+dev = [
+    "pytest",
+    "requests-mock",
+    "types-setuptools",
+    "stac-valid[pydantic]~=4.2.2",
+]
+docs = [
+    "sphinx>=8.2.3",
+    "sphinx-click>=6.0.0",
+    "sphinx_rtd_theme>=3.0.2",
+    "myst-parser>=4.0.1",
+    "sphinx-autodoc-typehints>=3.2.0",
+]
+pydantic = ["stac-valid[pydantic]~=4.2.2"]
+
+[project.urls]
+Homepage = "https://github.com/stac-utils/stac-check"
+
+[project.scripts]
+stac-check = "stac_check.cli:main"
+
+[tool.setuptools]
+packages = ["stac_check"]
+
+[tool.setuptools.package-data]
+stac_check = ["**/*"]


=====================================
sample_files/1.1.0/extended-item.json
=====================================
@@ -7,7 +7,6 @@
       "https://stac-extensions.github.io/view/v1.0.0/schema.json",
       "https://stac-extensions.github.io/remote-data/v1.0.0/schema.json"
     ],
-    "type": "Feature",
     "id": "20201211_223832_CS2",
     "bbox": [
       172.91173669923782,


=====================================
sample_files/sentinel_s2l2a_cogs_100.json
=====================================
The diff for this file was not included because it is too large.

=====================================
setup.py deleted
=====================================
@@ -1,50 +0,0 @@
-"""stac-check setup.py"""
-
-from setuptools import find_packages, setup
-
-__version__ = "1.11.1"
-
-with open("README.md", "r") as fh:
-    long_description = fh.read()
-
-setup(
-    name="stac_check",
-    version=__version__,
-    description="Linting and validation tool for STAC assets",
-    url="https://github.com/stac-utils/stac-check",
-    packages=find_packages(exclude=("tests",)),
-    include_package_data=True,
-    setup_requires=["setuptools"],
-    install_requires=[
-        "requests>=2.32.4",
-        "jsonschema>=4.25.0",
-        "click>=8.1.8",
-        "stac-validator~=3.10.1",
-        "PyYAML",
-        "python-dotenv",
-    ],
-    extras_require={
-        "dev": [
-            "pytest",
-            "requests-mock",
-            "types-setuptools",
-            "stac-validator[pydantic]~=3.10.1",
-        ],
-        "docs": [
-            "sphinx>=8.2.3",
-            "sphinx-click>=6.0.0",
-            "sphinx_rtd_theme>=3.0.2",
-            "myst-parser>=4.0.1",
-            "sphinx-autodoc-typehints>=3.2.0",
-        ],
-        "pydantic": ["stac-validator[pydantic]~=3.10.1"],
-    },
-    entry_points={"console_scripts": ["stac-check=stac_check.cli:main"]},
-    author="Jonathan Healy",
-    author_email="jonathan.d.healy at gmail.com",
-    license="MIT",
-    long_description=long_description,
-    long_description_content_type="text/markdown",
-    python_requires=">=3.9",
-    tests_require=["pytest"],
-)


=====================================
stac_check/api_lint.py
=====================================
@@ -1,9 +1,11 @@
+import time
 from dataclasses import dataclass
 from typing import Dict, Generator, List, Optional, Tuple
 from urllib.parse import urlparse, urlunparse
 
 from stac_validator.utilities import fetch_and_parse_file
 
+from stac_check.fast_validator_wrapper import validate_collection_fast
 from stac_check.lint import Linter
 
 
@@ -38,14 +40,21 @@ class ApiLinter:
         pages: Optional[int] = 1,
         headers: Optional[Dict] = None,
         verbose: bool = False,
+        fast: bool = False,
+        fast_linting: bool = False,
     ):
         self.source = source
         self.object_list_key = object_list_key
         self.pages = pages if pages is not None else 1
         self.headers = headers or {}
         self.verbose = verbose
+        self.fast = fast
+        self.fast_linting = fast_linting
         self.version = None
         self.validator_version = self._get_validator_version()
+        self.start_time = time.time()
+        self.total_time = 0.0
+        self.schemas_checked: List[str] = []
 
     def _get_validator_version(self) -> str:
         """Get the version of stac-validator being used.
@@ -148,9 +157,35 @@ class ApiLinter:
                 matching the message structure of the Linter class.
         """
         results_by_url = {}
+
+        # In fast mode with a file path, validate with FastValidator for better performance
+        if (
+            self.fast
+            and isinstance(self.source, str)
+            and self.source.endswith((".json", ".geojson"))
+        ):
+            try:
+                results, total_time, schemas = validate_collection_fast(
+                    self.source, Linter, self.verbose, self.fast_linting
+                )
+
+                # Store results and metadata
+                self.total_time = total_time
+                self.schemas_checked = schemas
+
+                # Set version from first result
+                if results and self.version is None:
+                    self.version = results[0].get("version")
+
+                return results
+            except Exception:
+                # Fall back to regular validation if fast validation fails
+                pass
+
+        # Regular validation path (non-fast or when FastValidator unavailable)
         for obj, obj_url in self.iterate_objects():
             try:
-                linter = Linter(obj, verbose=self.verbose)
+                linter = Linter(obj, verbose=self.verbose, fast=self.fast)
                 msg = dict(linter.message)
                 msg["path"] = obj_url
                 msg["best_practices"] = linter.best_practices_msg
@@ -180,4 +215,10 @@ class ApiLinter:
                     "failed_schema": None,
                     "original_object": obj,  # Still include the original object even if validation failed
                 }
+
+        # Calculate total validation time
+        self.total_time = (
+            time.time() - self.start_time
+        ) * 1000  # Convert to milliseconds
+
         return list(results_by_url.values())


=====================================
stac_check/cli.py
=====================================
@@ -1,8 +1,11 @@
 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 (
@@ -16,6 +19,37 @@ from stac_check.lint import Linter
 from stac_check.utilities import handle_output
 
 
+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,
@@ -78,6 +112,16 @@ from stac_check.utilities import handle_output
     is_flag=True,
     help="Enable verbose output.",
 )
+ at click.option(
+    "--fast",
+    is_flag=True,
+    help="Use FastJSONSchema for high-speed validation. Skips geometry checks and linting for maximum performance.",
+)
+ at 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)
@@ -95,6 +139,8 @@ def main(
     pydantic: bool,
     verbose: bool,
     output: Optional[str],
+    fast: bool,
+    fast_linting: bool,
 ) -> None:
     """Main entry point for the stac-check CLI.
 
@@ -112,7 +158,13 @@ def main(
         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(
@@ -132,6 +184,11 @@ def main(
             )
             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(
@@ -140,6 +197,8 @@ def main(
             pages=pages if pages else 1,
             headers=dict(header),
             verbose=verbose,
+            fast=fast,
+            fast_linting=fast_linting,
         )
         results = api_linter.lint_all()
 
@@ -151,6 +210,8 @@ def main(
             headers=dict(header),
             pydantic=pydantic,
             verbose=verbose,
+            fast=fast,
+            fast_linting=fast_linting,
         )
 
         # Show intro message in the terminal
@@ -188,6 +249,8 @@ def main(
             headers=dict(header),
             pydantic=pydantic,
             verbose=verbose,
+            fast=fast,
+            fast_linting=fast_linting,
         )
 
         intro_message(linter)
@@ -196,6 +259,36 @@ def main(
         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)
 


=====================================
stac_check/display_messages.py
=====================================
@@ -1,3 +1,4 @@
+import re
 from typing import Any, Callable, Dict, List, Optional
 
 import click
@@ -111,6 +112,10 @@ def _display_geometry_errors(linter: Linter) -> None:
     Args:
         linter: The Linter object containing geometry error information
     """
+    # Skip geometry errors display in fast mode
+    if hasattr(linter, "fast") and linter.fast:
+        return
+
     if linter.geometry_errors_msg:
         click.secho("\n " + linter.geometry_errors_msg[0], bg="magenta", fg="black")
         click.secho()
@@ -216,19 +221,152 @@ def _display_disclaimer() -> None:
     click.secho()
 
 
+def _display_fast_validation_summary(
+    results: List[Dict[str, Any]],
+    total_time: float = 0.0,
+    schemas: Optional[List[str]] = None,
+) -> None:
+    """Display a compact validation summary for fast mode with large datasets.
+
+    Args:
+        results: List of validation result dictionaries
+        total_time: Total validation time in milliseconds
+        schemas: List of schemas that were checked
+    """
+    passed = 0
+    failed = []
+    error_registry: Dict[str, List[str]] = {}
+    best_practices_issues: List[tuple] = []
+
+    for result in results:
+        if result.get("valid_stac"):
+            passed += 1
+        else:
+            failed.append(result)
+            # Group errors by message
+            error_msg = result.get("error_message", "Unknown error")
+            if error_msg not in error_registry:
+                error_registry[error_msg] = []
+            # Extract item ID from path
+            path = result.get("path", "unknown")
+            item_id = path.split("/")[-1] if "/" in path else path
+            error_registry[error_msg].append(item_id)
+
+        # Collect best practices issues (filter out empty/base messages)
+        best_practices = result.get("best_practices", [])
+        if best_practices:
+            # Filter out the base string and empty messages
+            filtered_practices = [
+                msg
+                for msg in best_practices
+                if msg and msg.strip() and msg.strip() != "STAC Best Practices:"
+            ]
+            if filtered_practices:
+                path = result.get("path", "unknown")
+                best_practices_issues.append((path, filtered_practices))
+
+    click.secho()
+    click.secho("\n Validation Summary", bold=True, bg="black", fg="white")
+    click.secho()
+    click.secho(f"✅ Passed: {passed}/{len(results)}")
+
+    if len(failed) > 0:
+        click.secho(f"❌ Failed: {len(failed)}/{len(results)}", fg="red")
+
+    if total_time > 0:
+        click.secho()
+        click.secho("⚡ Timing Information:", bold=True, fg="cyan")
+        click.secho(f"  Total Validation Time: {total_time:.2f} ms")
+        if len(results) > 0:
+            avg_time = total_time / len(results)
+            click.secho(f"  Average per Object: {avg_time:.3f} ms")
+
+    # Display schemas checked
+    if schemas and len(schemas) > 0:
+        click.secho()
+        click.secho("Schemas checked:", bold=True)
+        for schema in schemas:
+            click.secho(f"    {schema}")
+
+    # Display grouped errors
+    if error_registry:
+        click.secho()
+        click.secho("\n Validation Errors", bg="red", fg="white")
+        click.secho()
+        for err_msg, affected_ids in error_registry.items():
+            count = len(affected_ids)
+            click.secho(f"❌ {err_msg}", fg="red")
+            sample_ids = ", ".join(affected_ids[:3])
+            if count > 3:
+                sample_ids += f" ... (and {count - 3} more)"
+            click.secho(
+                f"   Affected Items: {count} | Examples: {sample_ids}", fg="red"
+            )
+            click.secho()
+
+    # Display best practices issues (grouped by message type)
+    if best_practices_issues:
+        click.secho()
+        click.secho("\n Best Practices Warnings", bg="blue", fg="white")
+        click.secho()
+
+        # Group best practices by message type (normalize item-specific messages)
+        practices_registry: Dict[str, List[str]] = {}
+        for path, messages in best_practices_issues:
+            # Extract item ID from path
+            item_id = path.split("/")[-1] if "/" in path else path
+            for msg in messages:
+                # Normalize messages that contain item IDs to group them together
+                # e.g., "Item name 'S2B_1CCV_20200317_0_L2A' should only contain..."
+                # becomes "Item name should only contain Searchable identifiers"
+                normalized_msg = msg
+                # Replace specific item/file names in single quotes with generic placeholder
+                # Handles patterns like: "Item name 'xyz' should...", "Item file names should match their ids: 'xyz' not equal to 'abc'"
+                # Use negative lookbehind to avoid matching apostrophes in contractions (e.g., "item's")
+                normalized_msg = re.sub(
+                    r"(?<![a-zA-Z])'[^']+'", "'<id>'", normalized_msg
+                )
+
+                if normalized_msg not in practices_registry:
+                    practices_registry[normalized_msg] = []
+                practices_registry[normalized_msg].append(item_id)
+
+        # Display grouped best practices
+        for practice_msg, affected_ids in practices_registry.items():
+            count = len(affected_ids)
+            click.secho(f"⚠️  {practice_msg}", fg="blue")
+            sample_ids = ", ".join(affected_ids[:3])
+            if count > 3:
+                sample_ids += f" ... (and {count - 3} more)"
+            click.secho(
+                f"   Affected Items: {count} | Examples: {sample_ids}", fg="blue"
+            )
+            click.secho()
+
+    click.secho()
+
+
 def _display_validation_summary(
-    results: List[Dict[str, Any]], verbose: bool = False
+    results: List[Dict[str, Any]],
+    verbose: bool = False,
+    fast: bool = False,
+    total_time: float = 0.0,
 ) -> None:
     """Display a summary of validation results, including warnings and best practice issues.
 
     Args:
         results: List of validation result dictionaries
         verbose: Whether to show detailed output
+        fast: Whether fast mode is enabled (for compact output and timing display)
+        total_time: Total validation time in milliseconds (for fast mode)
     """
     passed = 0
     failed = []
     warnings = []
     all_paths = []
+    total_setup_time = 0.0
+    total_exec_time = 0.0
+    error_registry: Dict[str, List[str]] = {}
 
     for result in results:
         path = result.get("path", "unknown")
@@ -239,6 +377,13 @@ def _display_validation_summary(
             passed += 1
         else:
             failed.append(path)
+            # Group errors by message for fast mode
+            error_msg = result.get("error_message", "Unknown error")
+            if error_msg not in error_registry:
+                error_registry[error_msg] = []
+            # Extract item ID from path
+            item_id = path.split("/")[-1] if "/" in path else path
+            error_registry[error_msg].append(item_id)
 
         # Check for best practice warnings in the result
         best_practices = []
@@ -264,36 +409,71 @@ def _display_validation_summary(
         if best_practices:
             warnings.append((path, best_practices))
 
-    click.secho("\n Validation Summary", bold=True, bg="black", fg="white")
-    click.secho()
-    click.secho(f"✅ Passed: {passed}/{len(all_paths)}")
-
-    if failed:
-        click.secho(f"❌ Failed: {len(failed)}/{len(all_paths)}", fg="red")
-        click.secho("\nFailed Assets:", fg="red")
-        for path in failed:
-            click.secho(f"  - {path}")
+        # Accumulate timing information if available
+        if fast and result.get("fast_setup_time"):
+            try:
+                setup_str = result.get("fast_setup_time", "0").split()[0]
+                total_setup_time += float(setup_str)
+            except (ValueError, IndexError):
+                pass
+        if fast and result.get("fast_exec_time"):
+            try:
+                exec_str = result.get("fast_exec_time", "0").split()[0]
+                total_exec_time += float(exec_str)
+            except (ValueError, IndexError):
+                pass
+
+    # Use fast validation summary for fast mode
+    if fast:
+        # Collect all unique schemas from results
+        all_schemas = set()
+        for result in results:
+            if result.get("schema"):
+                all_schemas.update(result.get("schema", []))
+        schemas_list = sorted(list(all_schemas)) if all_schemas else None
+        _display_fast_validation_summary(results, total_time, schemas_list)
+    else:
+        # Standard output for non-fast mode or small datasets
+        click.secho("\n Validation Summary", bold=True, bg="black", fg="white")
+        click.secho()
+        click.secho(f"✅ Passed: {passed}/{len(all_paths)}")
+
+        if failed:
+            click.secho(f"❌ Failed: {len(failed)}/{len(all_paths)}", fg="red")
+            click.secho("\nFailed Assets:", fg="red")
+            for path in failed:
+                click.secho(f"  - {path}")
+
+        if warnings:
+            click.secho(
+                f"\n⚠️  Best Practice Warnings ({len(warnings)} assets)", fg="yellow"
+            )
+            if verbose or len(warnings) <= 12:
+                for path, msgs in warnings:
+                    click.secho(f"\n  {path}:", fg="yellow")
+                    for msg in msgs:
+                        click.secho(f"    • {msg}", fg="yellow")
+            else:
+                click.secho("  (Use --verbose to see details)", fg="yellow")
 
-    if warnings:
-        click.secho(
-            f"\n⚠️  Best Practice Warnings ({len(warnings)} assets)", fg="yellow"
-        )
-        if verbose or len(warnings) <= 12:
-            for path, msgs in warnings:
-                click.secho(f"\n  {path}:", fg="yellow")
-                for msg in msgs:
-                    click.secho(f"    • {msg}", fg="yellow")
+        click.secho(f"\n🔍 All {len(all_paths)} Assets Checked")
+        if verbose or len(all_paths) <= 12:
+            for path in all_paths:
+                click.secho(f"  - {path}")
         else:
-            click.secho("  (Use --verbose to see details)", fg="yellow")
+            click.secho("  (Use --verbose to see all assets)", fg="yellow")
+
+        # Display timing information if in fast mode
+        if fast and (total_setup_time > 0 or total_exec_time > 0):
+            click.secho()
+            click.secho("⚡ FastValidator Timing:", bold=True, fg="cyan")
+            click.secho(f"  Total Setup Time: {total_setup_time:.2f} ms")
+            click.secho(f"  Total Execution Time: {total_exec_time:.2f} ms")
+            if len(all_paths) > 0:
+                avg_time = total_exec_time / len(all_paths)
+                click.secho(f"  Average per Item: {avg_time:.3f} ms")
 
-    click.secho(f"\n🔍 All {len(all_paths)} Assets Checked")
-    if verbose or len(all_paths) <= 12:
-        for path in all_paths:
-            click.secho(f"  - {path}")
-    else:
-        click.secho("  (Use --verbose to see all assets)", fg="yellow")
-
-    click.secho()
+        click.secho()
 
 
 def _display_validation_results(
@@ -303,6 +483,8 @@ def _display_validation_results(
     cli_message_func: Optional[Callable[[Linter], None]] = None,
     create_linter_func: Optional[Callable[[Dict[str, Any]], Linter]] = None,
     verbose: bool = False,
+    fast: bool = False,
+    total_time: float = 0.0,
 ) -> None:
     """Shared helper function to display validation results consistently.
 
@@ -325,63 +507,85 @@ def _display_validation_results(
     if cli_message_func is None:
         cli_message_func = cli_message
 
-    click.secho()
-    click.secho(title, bold=True)
-
-    # Display any metadata provided
-    if metadata:
-        for key, value in metadata.items():
-            click.secho(f"{key} = {value}")
+    # In fast mode with many items, show first 5 items then silence
+    show_all_items = not (fast and len(results) > 20)
+    items_shown = 0
+    max_items_to_show = 5
 
-    click.secho("-------------------------")
-    for count, msg in enumerate(results):
-        # Get the path or use a fallback
-        path = msg.get("path", f"(unknown-{count + 1})")
-        click.secho(f"\n Asset {count + 1}: {path}", bg="white", fg="black")
+    if show_all_items:
         click.secho()
+        click.secho(title, bold=True)
 
-        try:
-            # Try to create a Linter instance using the provided function
-            if create_linter_func:
-                item_linter = create_linter_func(msg)
+        # Display any metadata provided
+        if metadata:
+            for key, value in metadata.items():
+                click.secho(f"{key} = {value}")
 
-                # If create_linter_func returns None (for recursive validation), use fallback
-                if item_linter is None:
-                    _display_fallback_message(msg)
+        click.secho("-------------------------")
+
+    for count, msg in enumerate(results):
+        # In fast mode with many items, show only first 5 then silence
+        if not show_all_items and items_shown >= max_items_to_show:
+            if items_shown == max_items_to_show:
+                click.secho(
+                    "... silencing output for remaining items (validating at maximum speed) ...",
+                    dim=True,
+                )
+            items_shown += 1
+            continue
+
+        if show_all_items:
+            # Get the path or use a fallback
+            path = msg.get("path", f"(unknown-{count + 1})")
+            click.secho(f"\n Asset {count + 1}: {path}", bg="white", fg="black")
+            click.secho()
+
+            try:
+                # Try to create a Linter instance using the provided function
+                if create_linter_func:
+                    item_linter = create_linter_func(msg)
+
+                    # If create_linter_func returns None (for recursive validation), use fallback
+                    if item_linter is None:
+                        _display_fallback_message(msg)
+                    else:
+                        # Set validation status and error info for invalid items
+                        if not msg.get("valid_stac", True):
+                            item_linter.valid_stac = False
+                            item_linter.error_type = msg.get("error_type")
+                            item_linter.error_msg = msg.get("error_message")
+
+                        # Ensure best practices are included in the result
+                        if (
+                            hasattr(item_linter, "best_practices_msg")
+                            and item_linter.best_practices_msg
+                        ):
+                            # Skip the first line which is just the header
+                            bp_msgs = [
+                                msg
+                                for msg in item_linter.best_practices_msg[1:]
+                                if msg.strip()
+                            ]
+                            if bp_msgs:
+                                msg["best_practices"] = bp_msgs
+
+                        # Display using the provided message function
+                        cli_message_func(item_linter)
                 else:
-                    # Set validation status and error info for invalid items
-                    if not msg.get("valid_stac", True):
-                        item_linter.valid_stac = False
-                        item_linter.error_type = msg.get("error_type")
-                        item_linter.error_msg = msg.get("error_message")
-
-                    # Ensure best practices are included in the result
-                    if (
-                        hasattr(item_linter, "best_practices_msg")
-                        and item_linter.best_practices_msg
-                    ):
-                        # Skip the first line which is just the header
-                        bp_msgs = [
-                            msg
-                            for msg in item_linter.best_practices_msg[1:]
-                            if msg.strip()
-                        ]
-                        if bp_msgs:
-                            msg["best_practices"] = bp_msgs
-
-                    # Display using the provided message function
-                    cli_message_func(item_linter)
-            else:
-                # No linter creation function provided, use fallback
-                _display_fallback_message(msg)
-        except Exception as e:
-            # Fall back to basic display if creating the Linter fails
-            _display_fallback_message(msg, e)
+                    # No linter creation function provided, use fallback
+                    _display_fallback_message(msg)
+            except Exception as e:
+                # Fall back to basic display if creating the Linter fails
+                _display_fallback_message(msg, e)
 
-        click.secho("-------------------------")
+            click.secho("-------------------------")
+        else:
+            items_shown += 1
 
     # Display summary at the end for better visibility with many items
-    _display_validation_summary(results, verbose=verbose)
+    _display_validation_summary(
+        results, verbose=verbose, fast=fast, total_time=total_time
+    )
 
 
 def item_collection_message(
@@ -412,10 +616,16 @@ def item_collection_message(
     if results is None:
         results = linter.lint_all()
 
+    # In fast mode, use compact display
+    if linter.fast:
+        schemas = linter.schemas_checked if hasattr(linter, "schemas_checked") else None
+        _display_fast_validation_summary(results, linter.total_time, schemas)
+        return
+
     # Define a function to create Linter instances from API results
     def create_api_linter(msg):
         if msg.get("original_object"):
-            return Linter(msg.get("original_object"))
+            return Linter(msg.get("original_object"), fast=linter.fast)
         raise ValueError("No original object available")
 
     # Display the results using the shared helper
@@ -426,6 +636,8 @@ def item_collection_message(
         cli_message_func=cli_message_func,
         create_linter_func=create_api_linter,
         verbose=verbose,
+        fast=linter.fast,
+        total_time=linter.total_time,
     )
 
 
@@ -524,7 +736,7 @@ def collections_message(
     # Define a function to create Linter instances from API results
     def create_collection_linter(msg):
         if msg.get("original_object"):
-            return Linter(msg.get("original_object"))
+            return Linter(msg.get("original_object"), fast=linter.fast)
         raise ValueError("No original object available")
 
     # Display the results using the shared helper
@@ -535,6 +747,8 @@ def collections_message(
         cli_message_func=cli_message_func,
         create_linter_func=create_collection_linter,
         verbose=verbose,
+        fast=linter.fast,
+        total_time=linter.total_time,
     )
 
 
@@ -629,16 +843,19 @@ def intro_message(linter: Linter) -> None:
         click.secho(linter.set_update_message(), fg="red")
 
     click.secho(
-        f"\n Validator: stac-validator {linter.validator_version}",
+        f"\n Validator: stac-valid {linter.validator_version}",
         bold=True,
         bg="black",
         fg="white",
     )
 
     # Always show validation method
-    validation_method = (
-        "Pydantic" if hasattr(linter, "pydantic") and linter.pydantic else "JSONSchema"
-    )
+    if hasattr(linter, "fast") and linter.fast:
+        validation_method = "FastJSONSchema"
+    elif hasattr(linter, "pydantic") and linter.pydantic:
+        validation_method = "Pydantic"
+    else:
+        validation_method = "JSONSchema"
 
     click.secho(f"\n Validation method: {validation_method}", bg="cyan", fg="white")
 


=====================================
stac_check/fast_validator_wrapper.py
=====================================
@@ -0,0 +1,129 @@
+"""Fast validation wrapper for item collections using FastValidator."""
+
+import json
+import time
+from typing import Any, Dict, List
+
+
+def extract_schemas(obj: Dict) -> List[str]:
+    """Extract schemas from a STAC object.
+
+    Args:
+        obj: A STAC object (Item, Collection, etc.)
+
+    Returns:
+        List of schema URLs
+    """
+    schemas = []
+    if isinstance(obj, dict):
+        stac_version = obj.get("stac_version", "1.0.0")
+        item_type = obj.get("type", "Item")
+
+        # Add base schema (handle both "Item" and "Feature" types for GeoJSON)
+        if item_type in ("Item", "Feature"):
+            schemas.append(
+                f"https://schemas.stacspec.org/v{stac_version}/item-spec/json-schema/item.json"
+            )
+        elif item_type == "Collection":
+            schemas.append(
+                f"https://schemas.stacspec.org/v{stac_version}/collection-spec/json-schema/collection.json"
+            )
+
+        # Add extension schemas
+        for ext in obj.get("stac_extensions", []):
+            schemas.append(ext)
+
+    return schemas
+
+
+def validate_collection_fast(
+    source: str, linter_class: Any, verbose: bool = False, fast_linting: bool = False
+) -> tuple[List[Dict], float, List[str]]:
+    """Validate a collection file using FastValidator.
+
+    Uses the updated FastValidator that exposes valid/invalid counts and error details
+    in the message dict, eliminating the need for temp files.
+
+    Args:
+        source: Path to the STAC collection file
+        linter_class: The Linter class to use for validation
+        verbose: Whether to show verbose output
+        fast_linting: Whether to include linting checks in fast mode
+
+    Returns:
+        Tuple of (results list, total_time in ms, schemas_checked list)
+    """
+    start_time = time.time()
+    results_by_url = {}
+
+    # Parse the source file to get items
+    with open(source) as f:
+        data = json.load(f)
+
+    items = (
+        data.get("features", []) if data.get("type") == "FeatureCollection" else [data]
+    )
+
+    # Validate the collection file with FastValidator
+    # FastValidator now exposes valid_count, invalid_count, and error details
+    linter = linter_class(source, verbose=verbose, fast=True)
+
+    # FastValidator returns message as a list with one dict
+    if isinstance(linter.message, list) and len(linter.message) > 0:
+        msg = linter.message[0]
+    else:
+        msg = linter.message if isinstance(linter.message, dict) else {}
+
+    # Extract validation results from FastValidator message
+    schemas_checked = msg.get("schemas_checked", [])
+    errors = msg.get("errors", [])
+
+    # Build a map of failed item IDs for quick lookup
+    failed_items: Dict[str, List[str]] = {}
+    for error in errors:
+        for item_id in error.get("affected_items", []):
+            if item_id not in failed_items:
+                failed_items[item_id] = []
+            failed_items[item_id].append(error.get("error_message", ""))
+
+    # Create per-item results
+    for idx, obj in enumerate(items):
+        item_id = obj.get("id", f"unknown-{idx}")
+        obj_url = f"{source}/{item_id}"
+        item_schemas = extract_schemas(obj)
+
+        # Determine if item is valid
+        is_valid = item_id not in failed_items
+        error_messages = failed_items.get(item_id, [])
+
+        # Get best practices for this item (linting checks only, no re-validation)
+        # Only run linting if fast_linting is enabled
+        best_practices = []
+        if fast_linting:
+            try:
+                item_linter = linter_class(
+                    obj, verbose=verbose, fast=True, fast_linting=True
+                )
+                best_practices = item_linter.best_practices_msg
+            except Exception:
+                best_practices = []
+
+        result = {
+            "path": obj_url,
+            "valid_stac": is_valid,
+            "asset_type": "",
+            "version": obj.get("stac_version", ""),
+            "validation_method": "FastJSONSchema",
+            "error_type": "FastValidationError" if not is_valid else "",
+            "error_message": error_messages[0] if error_messages else "",
+            "best_practices": best_practices,
+            "geometry_errors": [],
+            "schema": item_schemas,
+            "original_object": obj,
+        }
+        results_by_url[obj_url] = result
+
+    # Calculate total validation time
+    total_time = (time.time() - start_time) * 1000
+
+    return list(results_by_url.values()), total_time, schemas_checked


=====================================
stac_check/lint.py
=====================================
@@ -29,6 +29,7 @@ class Linter:
         headers (dict): HTTP headers to include in the requests.
         pydantic (bool, optional): A boolean value indicating whether to use pydantic validation. Defaults to False.
         verbose (bool, optional): A boolean value indicating whether to enable verbose output. Defaults to False.
+        fast (bool, optional): A boolean value indicating whether to use fast validation mode (skips geometry checks for performance). Defaults to False.
 
     Attributes:
         data (dict): A dictionary representing the STAC JSON file.
@@ -141,6 +142,8 @@ class Linter:
     headers: Dict = field(default_factory=dict)
     pydantic: bool = False
     verbose: bool = False
+    fast: bool = False
+    fast_linting: bool = False
 
     def __post_init__(self):
         # Check if pydantic validation is requested but not installed
@@ -174,9 +177,7 @@ class Linter:
         self.version = self.get_message_field("version")
         self.valid_stac = self.get_message_field("valid_stac")
 
-        self.validator_version = importlib.metadata.distribution(
-            "stac-validator"
-        ).version
+        self.validator_version = importlib.metadata.distribution("stac-valid").version
         self.validate_all = self.recursive_validation(self.item)
 
         # Set error and info fields
@@ -203,6 +204,10 @@ class Linter:
         self.best_practices_msg = self.create_best_practices_msg()
         self.geometry_errors_msg = self.create_geometry_errors_msg()
 
+        # Extract timing information from FastValidator if available
+        self.fast_setup_time = self.get_message_field("fast_setup_time")
+        self.fast_exec_time = self.get_message_field("fast_exec_time")
+
     @staticmethod
     def parse_config(config_file: Optional[str] = None) -> Dict:
         """Parse the configuration file for STAC checks.
@@ -306,6 +311,75 @@ class Linter:
         Raises:
             ValueError: If `file` is not a valid file path or STAC dictionary.
         """
+        # Use FastValidator for fast mode with file paths
+        if self.fast and isinstance(file, str):
+            try:
+                import io
+                import sys
+
+                from stac_validator.fast_validator import FastValidator
+
+                # Suppress FastValidator output
+                old_stdout = sys.stdout
+                sys.stdout = io.StringIO()
+
+                fast_validator = FastValidator(file, quiet=False, verbose=self.verbose)
+                fast_validator.run()
+
+                # Restore stdout
+                sys.stdout = old_stdout
+
+                # Use the message attribute directly from FastValidator if available
+                # FastValidator.message is a list with one dict containing all validation info
+                fv_msg = {}
+                if hasattr(fast_validator, "message"):
+                    if (
+                        isinstance(fast_validator.message, list)
+                        and len(fast_validator.message) > 0
+                    ):
+                        fv_msg = fast_validator.message[0]
+                    elif isinstance(fast_validator.message, dict):
+                        fv_msg = fast_validator.message
+
+                # Extract error message from the first error if any
+                error_message = ""
+                if not fast_validator.valid and "errors" in fv_msg:
+                    errors = fv_msg.get("errors", [])
+                    if errors and len(errors) > 0:
+                        error_message = errors[0].get("error_message", "")
+
+                # Convert FastValidator result to StacValidate message format
+                return {
+                    "valid_stac": fast_validator.valid,
+                    "asset_type": "",
+                    "version": "",
+                    "validation_method": "FastJSONSchema",
+                    "error_type": (
+                        "FastValidationError" if not fast_validator.valid else ""
+                    ),
+                    "error_message": error_message,
+                    "fast_setup_time": fv_msg.get("setup_time_ms", ""),
+                    "fast_exec_time": fv_msg.get("execution_time_ms", ""),
+                    "valid_objects": fv_msg.get("valid_objects", 0),
+                    "invalid_objects": fv_msg.get("invalid_objects", 0),
+                    "schemas_checked": fv_msg.get("schemas_checked", []),
+                    "errors": fv_msg.get("errors", []),
+                }
+            except ImportError:
+                pass
+
+        # In fast mode with dict, skip validation (already done by FastValidator)
+        # and only run linting checks
+        if self.fast and isinstance(file, dict):
+            return {
+                "valid_stac": True,
+                "asset_type": "",
+                "version": file.get("stac_version", ""),
+                "validation_method": "FastJSONSchema",
+                "error_type": "",
+                "error_message": "",
+            }
+
         if isinstance(file, str):
             stac = StacValidate(
                 file,
@@ -1043,6 +1117,10 @@ class Linter:
         base_string = "STAC Best Practices: "
         best_practices.append(base_string)
 
+        # Skip best practices checks in fast mode (unless fast_linting is enabled)
+        if self.fast and not self.fast_linting:
+            return best_practices
+
         best_practices_dict = self.create_best_practices_dict()
 
         # Filter out geometry-related errors as they will be displayed separately
@@ -1075,6 +1153,10 @@ class Linter:
             'Geometry Validation Errors [BETA]:' base string and is followed by specific details. Each message is indented
             with four spaces, and there is an empty string between each message for readability.
         """
+        # Skip geometry validation in fast mode
+        if self.fast:
+            return []
+
         # Check if geometry validation is enabled
         geometry_config = self.config.get("geometry_validation", {})
         if not geometry_config.get("enabled", True):


=====================================
tests/test_cli.py
=====================================
@@ -1,5 +1,6 @@
 """Tests for the stac-check CLI."""
 
+import json
 import os
 import tempfile
 from unittest.mock import MagicMock, patch
@@ -7,6 +8,7 @@ from unittest.mock import MagicMock, patch
 import pytest
 from click.testing import CliRunner
 
+from stac_check.cli import is_item_collection
 from stac_check.cli import main as cli_main
 
 
@@ -75,12 +77,16 @@ def test_cli_output_to_file(runner):
 
 def test_cli_collections(runner):
     """Test --collections flag with mock."""
-    with patch("stac_check.cli.ApiLinter") as mock_api_linter, patch(
-        "stac_check.cli.Linter"
-    ) as mock_linter:
+    with (
+        patch("stac_check.cli.ApiLinter") as mock_api_linter,
+        patch("stac_check.cli.Linter") as mock_linter,
+    ):
         # Mock ApiLinter instance
         mock_api_instance = MagicMock()
         mock_api_instance.lint_all.return_value = [{"valid_stac": True}]
+        mock_api_instance.total_time = 0.0
+        mock_api_instance.schemas_checked = []
+        mock_api_instance.fast = False
         mock_api_linter.return_value = mock_api_instance
 
         # Mock Linter instance used for display
@@ -99,17 +105,23 @@ def test_cli_collections(runner):
             pages=1,
             headers={},
             verbose=False,
+            fast=False,
+            fast_linting=False,
         )
 
 
 def test_cli_item_collection(runner):
     """Test --item-collection flag with mock."""
-    with patch("stac_check.cli.ApiLinter") as mock_api_linter, patch(
-        "stac_check.cli.Linter"
-    ) as mock_linter:
+    with (
+        patch("stac_check.cli.ApiLinter") as mock_api_linter,
+        patch("stac_check.cli.Linter") as mock_linter,
+    ):
         # Mock ApiLinter instance
         mock_api_instance = MagicMock()
         mock_api_instance.lint_all.return_value = [{"valid_stac": True}]
+        mock_api_instance.total_time = 0.0
+        mock_api_instance.schemas_checked = []
+        mock_api_instance.fast = False
         mock_api_linter.return_value = mock_api_instance
 
         # Mock Linter instance used for display
@@ -127,6 +139,8 @@ def test_cli_item_collection(runner):
             pages=2,
             headers={},
             verbose=False,
+            fast=False,
+            fast_linting=False,
         )
 
 
@@ -193,8 +207,9 @@ def test_cli_headers(runner):
 
 def test_cli_pydantic_flag(runner):
     """Test that the --pydantic flag is passed correctly."""
-    with patch("stac_check.cli.Linter") as mock_linter, patch(
-        "stac_check.cli.importlib.import_module"
+    with (
+        patch("stac_check.cli.Linter") as mock_linter,
+        patch("stac_check.cli.importlib.import_module"),
     ):
         mock_instance = MagicMock()
         mock_linter.return_value = mock_instance
@@ -218,3 +233,178 @@ def test_cli_pydantic_flag(runner):
         assert result.exit_code == 0
         mock_linter.assert_called_once()
         assert mock_linter.call_args[1]["pydantic"] is False
+
+
+def test_is_item_collection_with_valid_file():
+    """Test is_item_collection with a valid item collection file."""
+    test_file = os.path.join(
+        os.path.dirname(__file__), "../sample_files/1.0.0/feature_collection.json"
+    )
+    assert is_item_collection(test_file) is True
+
+
+def test_is_item_collection_with_regular_item():
+    """Test is_item_collection with a regular item file."""
+    test_file = os.path.join(
+        os.path.dirname(__file__), "../sample_files/1.0.0/core-item.json"
+    )
+    assert is_item_collection(test_file) is False
+
+
+def test_is_item_collection_with_collection():
+    """Test is_item_collection with a collection file."""
+    test_file = os.path.join(
+        os.path.dirname(__file__), "../sample_files/1.0.0/collection.json"
+    )
+    assert is_item_collection(test_file) is False
+
+
+def test_is_item_collection_with_invalid_file():
+    """Test is_item_collection with a non-existent file."""
+    assert is_item_collection("/nonexistent/file.json") is False
+
+
+def test_is_item_collection_with_empty_features():
+    """Test is_item_collection with a FeatureCollection that has no features."""
+    with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as tmp:
+        json.dump({"type": "FeatureCollection", "features": []}, tmp)
+        tmp_path = tmp.name
+
+    try:
+        assert is_item_collection(tmp_path) is False
+    finally:
+        os.unlink(tmp_path)
+
+
+def test_is_item_collection_with_mock_url():
+    """Test is_item_collection with a mocked URL."""
+    mock_response = MagicMock()
+    mock_response.json.return_value = {
+        "type": "FeatureCollection",
+        "features": [{"type": "Feature", "properties": {}}],
+    }
+
+    with (
+        patch("stac_check.cli.requests.get", return_value=mock_response),
+        patch("stac_check.cli.is_valid_url", return_value=True),
+    ):
+        assert is_item_collection("https://example.com/items") is True
+
+
+def test_cli_auto_detect_item_collection(runner):
+    """Test that the CLI automatically detects and validates item collections."""
+    test_file = os.path.join(
+        os.path.dirname(__file__), "../sample_files/1.0.0/feature_collection.json"
+    )
+    result = runner.invoke(cli_main, [test_file])
+
+    assert result.exit_code == 0
+    # Should automatically detect and show item collection validation
+    assert "Item Collection" in result.output or "Assets Checked" in result.output
+
+
+def test_cli_auto_detect_respects_explicit_flag(runner):
+    """Test that explicit --item-collection flag still works."""
+    test_file = os.path.join(
+        os.path.dirname(__file__), "../sample_files/1.0.0/feature_collection.json"
+    )
+    result = runner.invoke(cli_main, [test_file, "--item-collection"])
+
+    assert result.exit_code == 0
+    # Should validate as item collection
+    assert "Item Collection" in result.output or "Assets Checked" in result.output
+
+
+def test_cli_auto_detect_does_not_trigger_with_recursive(runner):
+    """Test that auto-detection doesn't trigger when --recursive is used."""
+    test_file = os.path.join(
+        os.path.dirname(__file__), "../sample_files/1.0.0/core-item.json"
+    )
+    with patch("stac_check.cli.Linter") as mock_linter:
+        mock_instance = MagicMock()
+        mock_instance.valid_stac = True
+        mock_linter.return_value = mock_instance
+
+        runner.invoke(cli_main, [test_file, "--recursive"])
+
+        # Should use Linter, not ApiLinter (auto-detection disabled with --recursive)
+        assert mock_linter.called
+
+
+def test_cli_fast_validation_single_file(runner):
+    """Test --fast flag with a single file."""
+    test_file = os.path.join(
+        os.path.dirname(__file__), "../sample_files/1.0.0/core-item.json"
+    )
+    result = runner.invoke(cli_main, [test_file, "--fast"])
+    assert result.exit_code == 0
+    assert "FastJSONSchema" in result.output
+
+
+def test_cli_fast_validation_item_collection(runner):
+    """Test --fast flag with an item collection."""
+    test_file = os.path.join(
+        os.path.dirname(__file__), "../sample_files/1.0.0/feature_collection.json"
+    )
+    result = runner.invoke(cli_main, [test_file, "--fast"])
+    assert result.exit_code == 0
+    assert "FastJSONSchema" in result.output
+    assert "Validation Summary" in result.output
+    assert "Schemas checked:" in result.output
+
+
+def test_cli_fast_validation_shows_timing(runner):
+    """Test that --fast flag shows timing information."""
+    test_file = os.path.join(
+        os.path.dirname(__file__), "../sample_files/1.0.0/feature_collection.json"
+    )
+    result = runner.invoke(cli_main, [test_file, "--fast"])
+    assert result.exit_code == 0
+    assert "Timing Information" in result.output or "Validation Time" in result.output
+
+
+def test_cli_fast_validation_passes_fast_flag(runner):
+    """Test that --fast flag is passed to Linter for fast validation."""
+    test_file = os.path.join(
+        os.path.dirname(__file__), "../sample_files/1.0.0/core-item.json"
+    )
+    with patch("stac_check.cli.Linter") as mock_linter:
+        mock_instance = MagicMock()
+        mock_instance.fast = True
+        mock_instance.valid_stac = True
+        mock_instance.best_practices_msg = []
+        mock_instance.geometry_errors_msg = []
+        mock_linter.return_value = mock_instance
+
+        runner.invoke(cli_main, [test_file, "--fast"])
+
+        # Verify Linter was called with fast=True
+        assert mock_linter.called
+        call_kwargs = mock_linter.call_args[1]
+        assert call_kwargs.get("fast") is True
+
+
+def test_cli_fast_linting_validation(runner):
+    """Test that --fast-linting flag is passed to Linter with fast=True and fast_linting=True."""
+    test_file = os.path.join(
+        os.path.dirname(__file__), "../sample_files/1.0.0/core-item.json"
+    )
+    with patch("stac_check.cli.Linter") as mock_linter:
+        mock_instance = MagicMock()
+        mock_instance.fast = True
+        mock_instance.fast_linting = True
+        mock_instance.valid_stac = True
+        mock_instance.best_practices_msg = [
+            "STAC Best Practices: ",
+            "Test best practice",
+        ]
+        mock_instance.geometry_errors_msg = []
+        mock_linter.return_value = mock_instance
+
+        runner.invoke(cli_main, [test_file, "--fast-linting"])
+
+        # Verify Linter was called with fast=True and fast_linting=True
+        assert mock_linter.called
+        call_kwargs = mock_linter.call_args[1]
+        assert call_kwargs.get("fast") is True
+        assert call_kwargs.get("fast_linting") is True



View it on GitLab: https://salsa.debian.org/debian-gis-team/stac-check/-/commit/265c746a05d71fb55802113b08a20c8a82286ba0

-- 
View it on GitLab: https://salsa.debian.org/debian-gis-team/stac-check/-/commit/265c746a05d71fb55802113b08a20c8a82286ba0
You're receiving this email because of your account on salsa.debian.org. Manage all notifications: https://salsa.debian.org/-/profile/notifications | Help: https://salsa.debian.org/help


-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/pkg-grass-devel/attachments/20260510/e357a7a2/attachment-0001.htm>


More information about the Pkg-grass-devel mailing list