[Git][debian-gis-team/stac-validator][master] 8 commits: New upstream version 4.2.2

Antonio Valentino (@antonio.valentino) gitlab at salsa.debian.org
Wed Apr 29 20:53:05 BST 2026



Antonio Valentino pushed to branch master at Debian GIS Project / stac-validator


Commits:
183b54ad by Antonio Valentino at 2026-04-29T19:06:31+00:00
New upstream version 4.2.2
- - - - -
58735bee by Antonio Valentino at 2026-04-29T19:07:31+00:00
New upstream release

- - - - -
4c47f3a0 by Antonio Valentino at 2026-04-29T19:12:57+00:00
Update d/copyright

- - - - -
8691bd38 by Antonio Valentino at 2026-04-29T19:15:26+00:00
Add dependency on python3-fastjsonschema

- - - - -
143e0d06 by Antonio Valentino at 2026-04-29T19:18:46+00:00
Refresh all patches

- - - - -
8cb348b0 by Antonio Valentino at 2026-04-29T19:44:17+00:00
New d/man/stac-valid.1

- - - - -
a1eb0347 by Antonio Valentino at 2026-04-29T19:51:20+00:00
Update d/python3-stac-validator.lintian-overrides

- - - - -
6cf80b47 by Antonio Valentino at 2026-04-29T19:51:20+00:00
Set distribution to unstable

- - - - -


20 changed files:

- + .github/dependabot.yml
- .github/workflows/docs.yml
- .github/workflows/publish.yml
- .github/workflows/test-runner.yml
- .gitignore
- CHANGELOG.md
- LICENSE
- README.md
- cdk-deployment/requirements.txt
- debian/changelog
- debian/control
- debian/copyright
- + debian/man/stac-valid.1
- debian/patches/0001-No-network.patch
- debian/python3-stac-validator.lintian-overrides
- docs/requirements.txt
- pyproject.toml
- + stac_validator/fast_validator.py
- stac_validator/stac_validator.py
- + tests/test_fast_validator.py


Changes:

=====================================
.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 v3
+    - uses: actions/checkout at v6
     - name: Set up Python
-      uses: actions/setup-python at v4
+      uses: actions/setup-python at v6
       with:
         python-version: '3.8'
     - name: Install dependencies
@@ -34,7 +34,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
@@ -49,12 +49,12 @@ 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
       - name: Deploy to GitHub Pages
-        uses: peaceiris/actions-gh-pages at v3
+        uses: peaceiris/actions-gh-pages at v4
         with:
           github_token: ${{ secrets.GITHUB_TOKEN }}
           publish_dir: ./docs-build


=====================================
.github/workflows/publish.yml
=====================================
@@ -11,10 +11,10 @@ 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"
 


=====================================
.github/workflows/test-runner.yml
=====================================
@@ -19,7 +19,7 @@ jobs:
         python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
 
     steps:
-      - uses: actions/checkout at v2
+      - uses: actions/checkout at v6
       - name: Set up Python ${{ matrix.python-version }}
         uses: actions/setup-python at main
         with:


=====================================
.gitignore
=====================================
@@ -116,4 +116,7 @@ venv.bak/
 cdk-deployment/lambda/stac_validator/
 cdk-deployment/build-libraries/libraries.zip
 
-Pipfile*
\ No newline at end of file
+Pipfile*
+
+# local schemas
+*.schemas
\ No newline at end of file


=====================================
CHANGELOG.md
=====================================
@@ -16,15 +16,46 @@ The format is (loosely) based on [Keep a Changelog](http://keepachangelog.com/)
 
 ### Updated
 
+## [v4.2.2] - 2026-04-29
+
+### Added
+
+- Added message output for fast validator command [#292](https://github.com/stac-utils/stac-validator/pull/292)
+
+### Removed
+
+- Deprecation warnings for `stac-validator` installation [#292](https://github.com/stac-utils/stac-validator/pull/292)
+
+## [v4.2.1] - 2026-04-27
+
+### Added
+
+- Deprecation warning for `stac-validator` command, directing users to use `stac-valid` instead [#290](https://github.com/stac-utils/stac-validator/pull/290)
+
+## [v4.2.0] - 2026-04-27
+
+### Added
+
+- **High-Speed Validation Engine:** Added new `stac-validator fast` command for ultra-high performance validation using `fastjsonschema`.
+  - Up to 50x faster execution per object compared to standard `jsonschema` validation.
+  - **Persistent Schema Caching:** Local disk-based schema cache in `local_schemas/.schemas` to support offline validation and eliminate network latency.
+  - **Dynamic Multi-Level Caching:** Implements RAM -> Disk -> Network fetch hierarchy for JSON schemas.
+  - **Robust Fallback:** Automated fallback to standard `jsonschema` for complex schemas that fail to compile under `fastjsonschema` (e.g., Item Assets extension).
+  - **Intelligent Output:** New summary report including execution metrics (setup time vs. execution time) and consolidated error breakdowns.
+  - Added `--verbose` (`-v`) and `--quiet` (`-q`) support to the `fast` command for configurable output density. [#276](https://github.com/stac-utils/stac-validator/pull/276)
+- Added dependabot updates [#276](https://github.com/stac-utils/stac-validator/pull/276)
+
+### Changed
+
+- Renamed project in pyproject.toml from `stac-validator` to `stac-valid` [#276](https://github.com/stac-utils/stac-validator/pull/276)
+
 ## [v4.1.0] - 2026-04-01
 
 ### Added
 
 - Added benchmark_validation.py script for comparing batch vs legacy validation performance [#274](https://github.com/stac-utils/stac-validator/pull/274)
-
 - Added `--batch-size` option to `batch` command for configurable chunk processing (default: 2000) [#274](https://github.com/stac-utils/stac-validator/pull/274)
 
-
 ### Changed
 
 - Implemented compiled validator caching using `@functools.lru_cache` to avoid recompiling JSON schemas [#274](https://github.com/stac-utils/stac-validator/pull/274)
@@ -413,7 +444,10 @@ The format is (loosely) based on [Keep a Changelog](http://keepachangelog.com/)
 - With the newest version - 1.0.0-beta.2 - items will run through jsonchema validation before the PySTAC validation. The reason for this is that jsonschema will give more informative error messages. This should be addressed better in the future. This is not the case with the --recursive option as time can be a concern here with larger collections.
 - Logging. Various additions were made here depending on the options selected. This was done to help assist people to update their STAC collections.
 
-[Unreleased]: https://github.com/sparkgeo/stac-validator/compare/v4.1.0..main
+[Unreleased]: https://github.com/sparkgeo/stac-validator/compare/v4.2.2..main
+[v4.2.2]: https://github.com/sparkgeo/stac-validator/compare/v4.2.1..v4.2.2
+[v4.2.1]: https://github.com/sparkgeo/stac-validator/compare/v4.2.0..v4.2.1
+[v4.2.0]: https://github.com/sparkgeo/stac-validator/compare/v4.1.0..v4.2.0
 [v4.1.0]: https://github.com/sparkgeo/stac-validator/compare/v4.0.1..v4.1.0
 [v4.0.1]: https://github.com/sparkgeo/stac-validator/compare/v4.0.0..v4.0.1
 [v4.0.0]: https://github.com/sparkgeo/stac-validator/compare/v3.11.0..v4.0.0


=====================================
LICENSE
=====================================
@@ -187,6 +187,7 @@
       identification within third-party archives.
 
    Copyright 2019 Sparkgeo
+   Copyright 2020-2026 Jonathan Healy
 
    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.


=====================================
README.md
=====================================
@@ -13,6 +13,27 @@
    [![PyPI version](https://img.shields.io/pypi/v/stac-validator.svg?color=blue)](https://pypi.org/project/stac-validator/)
   [![STAC](https://img.shields.io/badge/STAC-1.1.0-blue.svg)](https://github.com/radiantearth/stac-spec/tree/v1.1.0)
 
+
+
+> ⚠️ **IMPORTANT NOTICE: PyPI Package Renamed** ⚠️
+>
+> Due to an administrative transition, the PyPI package name for this project has changed. The `stac-validator` package on PyPI is no longer maintained. 
+>
+> To get the latest updates, bug fixes, and features (v4.2.0 and beyond), you must now install **`stac-valid`**.
+>
+> ### 🛠️ What you need to do
+> 
+> You only need to update your installation command or `requirements.txt`:
+> ```bash
+> # OLD
+> pip install stac-validator
+> 
+> # NEW
+> pip install stac-valid
+> ```
+> 
+> **Your existing Python code and CLI scripts DO NOT need to change.** > You will still use `import stac_validator` and the `stac-validator` CLI command exactly as you did before.
+
 ## Table of Contents
 
 - [Overview](#overview)
@@ -24,6 +45,7 @@
   - [CLI](#cli)
     - [Legacy Validation](#legacy-validation)
   - [Batch Validation](#batch-validation)
+  - [Fast Validation](#fast-validation)
   - [Python](#python)
 - [Schema Cache Settings](#schema-cache-settings)
 - [Performance Benchmarking](#performance-benchmarking)
@@ -97,7 +119,7 @@ https://github.com/stac-utils/stac-check
 ### Installation from PyPi
 
 ```bash
-$ pip install stac-validator
+$ pip install stac-valid
 ```
 
 ### Installation from Repo
@@ -273,6 +295,24 @@ Options:
   --help                Show this message and exit.
 ```
 
+**Fast Command**  
+
+```bash
+$ stac-valid fast --help
+```
+
+```bash
+Usage: stac-valid fast [OPTIONS] STAC_FILE
+
+  High-speed validation using fastjsonschema and local caching.
+
+Options:
+  -q, --quiet    Suppress individual item logs.
+  -v, --verbose  Show full validation logs for all items. By default, only
+                 invalid items are shown.
+  --help         Show this message and exit.
+```
+
 #### Legacy Validation
 
 The `validate` command is the main legacy validation tool with comprehensive options:
@@ -460,6 +500,174 @@ for result in results:
 - **Migration validation:** Verify all items when upgrading STAC versions
 - **API preprocessing:** Validate incoming FeatureCollections before storage (see FastAPI integration)
 
+#### Fast Validation
+
+The `fast` command provides ultra-high-speed validation using `fastjsonschema` with intelligent caching. It's optimized for large FeatureCollections, delivering **batch-like performance with significantly lower memory overhead** by using a single-threaded, compiled validator approach instead of multiprocessing.
+
+**Key Features:**
+
+- **fastjsonschema:** Compiled validators for 10-100x faster validation than standard jsonschema
+- **Single-threaded:** No multiprocessing overhead - ideal for memory-constrained environments
+- **Batch-comparable speed:** Similar throughput to batch command but with minimal memory footprint
+- **Multi-tier caching:** RAM → Disk → Network with automatic fallback
+- **Local schema storage:** Schemas cached locally under `local_schemas/.schemas` directory for instant reuse
+- **Automatic detection:** Detects STAC type (Item, Collection, Catalog, FeatureCollection) automatically
+- **Detailed metrics:** Shows setup time, execution time, and cache hit status for each item
+- **Error grouping:** Groups validation errors by type and shows affected items
+
+**Basic Usage**
+
+```bash
+# Validate a single STAC Item
+$ stac-validator fast item.json
+
+# Validate a FeatureCollection (validates each feature)
+$ stac-validator fast collection.json
+
+# Validate a STAC Collection
+$ stac-validator fast collection-metadata.json
+
+# Validate a STAC Catalog
+$ stac-validator fast catalog.json
+```
+
+**Options**
+
+```bash
+# Suppress output (only show summary)
+$ stac-validator fast item.json --quiet
+
+# Show detailed output for all items (default shows first 5)
+$ stac-validator fast collection.json --verbose
+
+# Combine options
+$ stac-validator fast collection.json --verbose --quiet  # Quiet takes precedence
+```
+
+**Example Output**
+
+```bash
+$ stac-validator fast sample_data/sentinel-cogs_0_100.json
+
+📂 Loading: sample_data/sentinel-cogs_0_100.json
+📦 Detected FeatureCollection (100 Items)
+
+[1] ID: S2B_1CDH_20191211_0_L2A | Type: Item | Cache 🐌 | Setup: 125.34ms | Exec:  2.45ms | ✅ VALID
+[2] ID: S2B_1CDH_20191220_0_L2A | Type: Item | Cache ⚡ | Setup:   0.12ms | Exec:  1.89ms | ✅ VALID
+[3] ID: S2B_1CDH_20191230_0_L2A | Type: Item | Cache ⚡ | Setup:   0.08ms | Exec:  2.12ms | ❌ INVALID
+[4] ID: S2B_1CCV_20191029_0_L2A | Type: Item | Cache ⚡ | Setup:   0.09ms | Exec:  1.95ms | ✅ VALID
+[5] ID: S2B_1CCV_20191128_0_L2A | Type: Item | Cache ⚡ | Setup:   0.07ms | Exec:  2.03ms | ✅ VALID
+... silencing output for remaining items (validating at maximum speed) ...
+
+=======================================================
+📊 VALIDATION SUMMARY
+=======================================================
+Total Objects Processed : 100
+Valid Objects           : 46
+Invalid Objects         : 54
+-------------------------------------------------------
+Total Setup Time        : 144.35 ms
+Total Execution Time    : 25.74 ms
+Average Exec per Object : 0.257 ms
+=======================================================
+🚨 ERROR BREAKDOWN
+=======================================================
+
+❌ STAC Spec Violation: Missing {'rel': 'collection'} in links array.
+   Affected Items: 54
+   Examples:       S2B_1CCV_20190102_0_L2A, S2B_1CCV_20190122_0_L2A, S2B_1CCV_20191029_0_L2A ... (and 51 more)
+```
+
+**Cache Indicators**
+
+- **🐌 (Slow):** First validation - schemas being fetched and compiled
+- **⚡ (Lightning):** Cached validator - instant execution using pre-compiled validator
+
+**Schema Caching & Performance Improvement**
+
+The fast validator uses a multi-tier caching strategy that dramatically speeds up subsequent runs:
+
+1. **First Run:** Schemas are downloaded from the network and compiled with `fastjsonschema`
+   - Setup time: 5-6 seconds (includes network fetch for all schemas + compilation)
+   - Per-item execution: 0.25ms
+   - Total for 100 items: ~5.4 seconds
+   - Total for 1000 items: ~10-11 seconds
+
+2. **Subsequent Runs:** Schemas are loaded from local disk cache (`.schemas/` directory)
+   - Setup time: 100-200ms (instant disk read + compilation from cache)
+   - Per-item execution: 0.25ms
+   - Total for 100 items: ~150-200ms
+   - Total for 1000 items: ~400-500ms (0.4-0.5 seconds)
+   - **Speedup: 20-25x faster** compared to first run
+
+**Cache Storage:**
+
+Schemas are automatically cached in `local_schemas/.schemas/` directory:
+```
+local_schemas/
+└── .schemas/
+```
+
+This means:
+- ✅ No network calls on subsequent validations
+- ✅ Instant schema availability across multiple runs
+- ✅ Persistent cache survives process restarts
+- ✅ Minimal disk space usage (schemas are typically <100KB each)
+
+**Performance Characteristics**
+
+The fast validator is optimized for:
+- Large FeatureCollections (100-100,000+ items)
+- Memory-constrained environments (single-threaded, no multiprocessing)
+- Rapid iteration during development
+- CI/CD pipelines where speed and memory efficiency are critical
+
+Typical performance (subsequent runs with disk cache):
+- **Setup time:** 100-200ms (disk read + compilation from cached schemas)
+- **Per-item execution:** 0.23-0.25ms (compiled fastjsonschema validator)
+- **Total time for 100 items:** ~150-200ms
+- **Total time for 1000 items:** ~400-500ms (0.4-0.5 seconds)
+- **Memory footprint:** Single process, minimal overhead (vs. batch's multiprocessing overhead)
+
+**Batch vs. Fast Comparison**
+
+| Metric | Batch | Fast |
+|--------|-------|------|
+| Memory | High (multiprocessing) | Low (single-threaded) |
+| CPU cores | All available | Single core |
+| Best for | Large systems with many cores | Memory-constrained or single-core systems |
+| FeatureCollections | Excellent | Excellent |
+| Large datasets | Excellent | Good |
+
+**Python API**
+
+```python
+from stac_validator.fast_validator import FastValidator
+
+# Validate a file
+validator = FastValidator("item.json", quiet=False, verbose=False)
+validator.run()
+
+# Check if valid
+if validator.valid:
+    print("✅ Valid STAC")
+else:
+    print("❌ Invalid STAC")
+```
+
+**Choosing the Right Command**
+
+| Use Case | Command | Why |
+|----------|---------|-----|
+| Single file validation | `validate` | Full feature set, detailed error messages, extensions support |
+| Large FeatureCollections (memory-constrained) | `fast` | Batch-like speed, single-threaded, minimal memory overhead |
+| Bulk validation (1000+ files, multi-core systems) | `batch` | Multiprocessing, utilizes all CPU cores, scales linearly |
+| Quick validation with detailed metrics | `fast` | Ultra-fast with fastjsonschema, timing info per item, auto-detection |
+| CI/CD pipelines (memory-limited) | `fast` | Fast, low memory, suitable for containers and serverless |
+| CI/CD pipelines (resource-rich) | `batch` | Parallelization, maximum throughput, consistent output |
+| Development/testing | `fast` | Instant feedback, detailed metrics, minimal overhead |
+| Complex validation rules | `validate` | Full control over validation options, recursive validation |
+
 ### Python
 
 **Single File Validation**
@@ -603,6 +811,54 @@ stac.validate_item_collection_dict(item_collection_dict)
 print(stac.message)
 ```
 
+**Fast Validation (High-Speed with Local Caching)**
+
+For large FeatureCollections or when you need maximum speed with minimal memory overhead:
+
+```python
+from stac_validator.fast_validator import FastValidator
+import json
+
+# Validate a FeatureCollection or single STAC object
+fv = FastValidator("large_collection.json", quiet=True)
+fv.run()
+
+# Access validation results via the message attribute
+print(json.dumps(fv.message, indent=2))
+
+# Example output:
+# {
+#   "path": "large_collection.json",
+#   "valid_stac": true,
+#   "stac_versions": ["1.0.0", "1.1.0"],
+#   "schemas_checked": [
+#     "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json",
+#     "https://schemas.stacspec.org/v1.1.0/item-spec/json-schema/item.json",
+#     "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
+#   ],
+#   "total_objects": 1000,
+#   "valid_objects": 998,
+#   "invalid_objects": 2,
+#   "setup_time_ms": 145.32,
+#   "execution_time_ms": 245.67,
+#   "errors": [
+#     {
+#       "error_message": "STAC Spec Violation: Missing {'rel': 'collection'} in links array.",
+#       "affected_items": ["item_1", "item_2"],
+#       "count": 2
+#     }
+#   ]
+# }
+
+# Check validity
+if fv.valid:
+    print(f"✅ All {fv.message[0]['valid_objects']} items are valid")
+else:
+    print(f"❌ {fv.message[0]['invalid_objects']} items failed validation")
+    for error in fv.message[0]['errors']:
+        print(f"  - {error['error_message']}: {error['count']} items affected")
+```
+
 **Configure Schema Cache Size**
 
 ```python
@@ -933,7 +1189,7 @@ The paths in the config file can be absolute or relative to the config file's lo
 The `--pydantic` option provides enhanced validation using stac-pydantic models, which offer stronger type checking and more detailed error messages. To use this feature, you need to install the optional dependency:
 
 ```bash
-$ pip install stac-validator[pydantic]
+$ pip install stac-valid[pydantic]
 ```
 
 Then you can validate your STAC objects using Pydantic models:


=====================================
cdk-deployment/requirements.txt
=====================================
@@ -1,4 +1,4 @@
 aws-cdk.core==1.101.0
-aws-cdk.aws-lambda==1.101.0
-aws-cdk.aws_apigateway==1.101.0
+aws-cdk.aws-lambda==1.204.0
+aws-cdk.aws_apigateway==1.204.0
 


=====================================
debian/changelog
=====================================
@@ -1,3 +1,16 @@
+stac-validator (4.2.2-1) unstable; urgency=medium
+
+  * New upstream release.
+  * Update d/copyright.
+  * debian/control:
+    - Add dependency on python3-fastjsonschema.
+  * debian/patches:
+    - Refresh all patches.
+  * New d/man/stac-valid.1.
+  * Update d/python3-stac-validator.lintian-overrides.
+
+ -- Antonio Valentino <antonio.valentino at tiscali.it>  Wed, 29 Apr 2026 19:44:25 +0000
+
 stac-validator (4.1.0-2) unstable; urgency=medium
 
   * debian/control:


=====================================
debian/control
=====================================
@@ -7,6 +7,7 @@ Build-Depends: debhelper-compat (= 13),
                pybuild-plugin-pyproject,
                python3-all,
                python3-click,
+               python3-fastjsonschema,
                python3-jsonschema,
                python3-pytest <!nocheck>,
                python3-requests,


=====================================
debian/copyright
=====================================
@@ -5,7 +5,7 @@ Source: https://github.com/stac-utils/stac-validator
 
 Files: *
 Copyright: 2019, Sparkgeo
-           2023, Jonathan Healy
+           2020-2026, Jonathan Healy
 License: Apache-2.0
 
 Files: debian/*


=====================================
debian/man/stac-valid.1
=====================================
@@ -0,0 +1,56 @@
+.TH STAC-VALID "1" "August 2023" "stac-valid, version 3.3.1" "User Commands"
+.SH NAME
+stac-valid \- Validate STAC files
+.SH SYNOPSIS
+.B stac-valid
+[\fI\,OPTIONS\/\fR] \fI\,STAC_FILE\/\fR
+.SH OPTIONS
+.TP
+\fB\-\-core\fR
+Validate core stac object only without extensions.
+.TP
+\fB\-\-extensions\fR
+Validate extensions only.
+.TP
+\fB\-\-links\fR
+Additionally validate links. Only works with
+default mode.
+.TP
+\fB\-\-assets\fR
+Additionally validate assets. Only works with
+default mode.
+.TP
+\fB\-c\fR, \fB\-\-custom\fR TEXT
+Validate against a custom schema (local filepath or
+remote schema).
+.TP
+\fB\-r\fR, \fB\-\-recursive\fR
+Recursively validate all related stac objects.
+.TP
+\fB\-m\fR, \fB\-\-max\-depth\fR INTEGER
+Maximum depth to traverse when recursing. Omit this
+argument to get full recursion. Ignored if
+`recursive == False`.
+.TP
+\fB\-\-item\-collection\fR
+Validate item collection response. Can be combined
+with \fB\-\-pages\fR. Defaults to one page.
+.TP
+\fB\-p\fR, \fB\-\-pages\fR INTEGER
+Maximum number of pages to validate via \fB\-\-itemcollection\fR. Defaults to one page.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+Enables verbose output for recursive mode.
+.TP
+\fB\-\-no_output\fR
+Do not print output to console.
+.TP
+\fB\-\-log_file\fR TEXT
+Save full recursive output to log file (local
+filepath).
+.TP
+\fB\-\-version\fR
+Show the version and exit.
+.TP
+\fB\-\-help\fR
+Show this message and exit.


=====================================
debian/patches/0001-No-network.patch
=====================================
@@ -12,6 +12,7 @@ Forwarded: not-needed
  tests/test_custom.py                   |  7 +++++++
  tests/test_default.py                  | 11 +++++++++++
  tests/test_extensions.py               | 12 ++++++++++++
+ tests/test_fast_validator.py           | 18 ++++++++++++++++++
  tests/test_header.py                   |  2 ++
  tests/test_links.py                    |  4 ++++
  tests/test_pydantic.py                 |  3 +++
@@ -21,14 +22,14 @@ Forwarded: not-needed
  tests/test_validate_collections.py     |  3 +++
  tests/test_validate_dict.py            |  4 ++++
  tests/test_validate_item_collection.py |  7 +++++++
- 17 files changed, 109 insertions(+), 1 deletion(-)
+ 18 files changed, 127 insertions(+), 1 deletion(-)
 
 diff --git a/pyproject.toml b/pyproject.toml
-index 84a02d5..f7ecf98 100644
+index 409125c..7359c6d 100644
 --- a/pyproject.toml
 +++ b/pyproject.toml
-@@ -61,4 +61,9 @@ packages = ["stac_validator"]
- stac-validator = "stac_validator.stac_validator:cli"
+@@ -65,4 +65,9 @@ stac-validator = "stac_validator.stac_validator:cli"
+ stac-valid = "stac_validator.stac_validator:cli"
  
  [tool.setuptools.package-data]
 -stac_validator = ["*.yaml"]
@@ -505,6 +506,148 @@ index 2b03907..5612c05 100644
  def test_verbose_mode_output():
      """Test that verbose mode provides detailed error information in the expected format."""
      stac_file = "tests/test_data/v100/bad-item.json"
+diff --git a/tests/test_fast_validator.py b/tests/test_fast_validator.py
+index 919e8d1..3ee43d5 100644
+--- a/tests/test_fast_validator.py
++++ b/tests/test_fast_validator.py
+@@ -129,36 +129,42 @@ def invalid_feature_collection(tmp_path):
+ class TestFastValidatorBasic:
+     """Test basic functionality of FastValidator."""
+ 
++    @pytest.mark.network
+     def test_valid_item(self, valid_item):
+         """Test validation of a valid STAC Item."""
+         fv = FastValidator(valid_item, quiet=True)
+         fv.run()
+         assert fv.valid is True
+ 
++    @pytest.mark.network
+     def test_valid_collection(self, valid_collection):
+         """Test validation of a valid STAC Collection."""
+         fv = FastValidator(valid_collection, quiet=True)
+         fv.run()
+         assert fv.valid is True
+ 
++    @pytest.mark.network
+     def test_valid_catalog(self, valid_catalog):
+         """Test validation of a valid STAC Catalog."""
+         fv = FastValidator(valid_catalog, quiet=True)
+         fv.run()
+         assert fv.valid is True
+ 
++    @pytest.mark.network
+     def test_valid_feature_collection(self, valid_feature_collection):
+         """Test validation of a valid FeatureCollection."""
+         fv = FastValidator(valid_feature_collection, quiet=True)
+         fv.run()
+         assert fv.valid is True
+ 
++    @pytest.mark.network
+     def test_invalid_item(self, invalid_item):
+         """Test that invalid items are detected."""
+         fv = FastValidator(invalid_item, quiet=True)
+         fv.run()
+         assert fv.valid is False
+ 
++    @pytest.mark.network
+     def test_invalid_feature_collection(self, invalid_feature_collection):
+         """Test that FeatureCollections with invalid items are detected."""
+         fv = FastValidator(invalid_feature_collection, quiet=True)
+@@ -169,6 +175,7 @@ class TestFastValidatorBasic:
+ class TestFastValidatorOptions:
+     """Test FastValidator options."""
+ 
++    @pytest.mark.network
+     def test_quiet_mode(self, valid_item, capsys):
+         """Test quiet mode suppresses item-level output."""
+         fv = FastValidator(valid_item, quiet=True)
+@@ -176,6 +183,7 @@ class TestFastValidatorOptions:
+         captured = capsys.readouterr()
+         assert "VALIDATION SUMMARY" in captured.out
+ 
++    @pytest.mark.network
+     def test_verbose_mode(self, valid_feature_collection, capsys):
+         """Test verbose mode shows all items."""
+         fv = FastValidator(valid_feature_collection, quiet=False, verbose=True)
+@@ -184,6 +192,7 @@ class TestFastValidatorOptions:
+         assert "[1]" in captured.out
+         assert "[5]" in captured.out
+ 
++    @pytest.mark.network
+     def test_non_verbose_mode(self, tmp_path, capsys):
+         """Test non-verbose mode shows first 5 items and silences rest."""
+         fc_path = tmp_path / "large_fc.json"
+@@ -214,6 +223,7 @@ class TestFastValidatorOptions:
+ class TestFastValidatorDetection:
+     """Test STAC type detection."""
+ 
++    @pytest.mark.network
+     def test_detects_item(self, valid_item, capsys):
+         """Test detection of STAC Item."""
+         fv = FastValidator(valid_item, quiet=False)
+@@ -221,6 +231,7 @@ class TestFastValidatorDetection:
+         captured = capsys.readouterr()
+         assert "Item" in captured.out or "Feature" in captured.out
+ 
++    @pytest.mark.network
+     def test_detects_collection(self, valid_collection, capsys):
+         """Test detection of STAC Collection."""
+         fv = FastValidator(valid_collection, quiet=False)
+@@ -228,6 +239,7 @@ class TestFastValidatorDetection:
+         captured = capsys.readouterr()
+         assert "Collection" in captured.out
+ 
++    @pytest.mark.network
+     def test_detects_catalog(self, valid_catalog, capsys):
+         """Test detection of STAC Catalog."""
+         fv = FastValidator(valid_catalog, quiet=False)
+@@ -235,6 +247,7 @@ class TestFastValidatorDetection:
+         captured = capsys.readouterr()
+         assert "Catalog" in captured.out
+ 
++    @pytest.mark.network
+     def test_detects_feature_collection(self, valid_feature_collection, capsys):
+         """Test detection of FeatureCollection."""
+         fv = FastValidator(valid_feature_collection, quiet=False)
+@@ -275,12 +288,14 @@ class TestFastValidatorErrorHandling:
+ class TestFastValidatorPerformance:
+     """Test performance characteristics."""
+ 
++    @pytest.mark.network
+     def test_caching_works(self, valid_feature_collection):
+         """Test that validator caching works."""
+         fv = FastValidator(valid_feature_collection, quiet=True)
+         fv.run()
+         assert fv.valid is True
+ 
++    @pytest.mark.network
+     def test_large_feature_collection(self, tmp_path):
+         """Test validation of a large FeatureCollection."""
+         fc_path = tmp_path / "large_fc.json"
+@@ -305,6 +320,7 @@ class TestFastValidatorPerformance:
+         fv.run()
+         assert fv.valid is True
+ 
++    @pytest.mark.network
+     def test_message_attribute_structure(self, valid_item):
+         """Test that the message attribute has the correct structure."""
+         fv = FastValidator(valid_item, quiet=True)
+@@ -340,6 +356,7 @@ class TestFastValidatorPerformance:
+         assert isinstance(msg["execution_time_ms"], float)
+         assert isinstance(msg["errors"], list)
+ 
++    @pytest.mark.network
+     def test_message_attribute_valid_items(self, valid_feature_collection):
+         """Test message attribute for valid items."""
+         fv = FastValidator(valid_feature_collection, quiet=True)
+@@ -359,6 +376,7 @@ class TestFastValidatorPerformance:
+         assert len(msg["schemas_checked"]) > 0
+         assert "1.0.0" in msg["stac_versions"]
+ 
++    @pytest.mark.network
+     def test_message_attribute_invalid_items(self, tmp_path):
+         """Test message attribute for invalid items."""
+         fc_path = tmp_path / "invalid_fc.json"
 diff --git a/tests/test_header.py b/tests/test_header.py
 index 6203bd0..872048b 100644
 --- a/tests/test_header.py


=====================================
debian/python3-stac-validator.lintian-overrides
=====================================
@@ -1,2 +1,2 @@
 # False positive
-package-contains-documentation-outside-usr-share-doc [usr/lib/python3/dist-packages/stac_validator-*.*-info/*]
+package-contains-documentation-outside-usr-share-doc [usr/lib/python3/dist-packages/stac_valid*-*.*-info/*]


=====================================
docs/requirements.txt
=====================================
@@ -1,4 +1,4 @@
-sphinx>=4.0.0
-sphinx_rtd_theme>=1.0.0
+sphinx>=7.4.7
+sphinx_rtd_theme>=3.1.0
 myst-parser>=0.15.0
 sphinx-autodoc-typehints>=1.12.0
\ No newline at end of file


=====================================
pyproject.toml
=====================================
@@ -3,12 +3,12 @@ requires = ["setuptools>=61.0", "wheel"]
 build-backend = "setuptools.build_meta"
 
 [project]
-name = "stac_validator"
-version = "4.1.0"
+name = "stac_valid"
+version = "4.2.2"
 description = "A package to validate STAC files"
 authors = [
     {name = "James Banting"},
-    {name = "Jonathan Healy", email = "jonathan.d.healy at gmail.com"}
+    {name = "Jonathan Healy", email = "jon at healy-hyperspatial.dev"}
 ]
 license = {text = "Apache-2.0"}
 classifiers = [
@@ -23,12 +23,15 @@ requires-python = ">=3.8"
 dependencies = [
     "requests>=2.32.3",
     "jsonschema>=4.23.0",
+    "fastjsonschema>=2.21.1",
     "click>=8.1.8",
     "referencing>=0.35.1",
     "pyYAML>=6.0.1",
     "tqdm>=4.66.0",
 ]
-optional-dependencies.dev = [
+
+[project.optional-dependencies]
+dev = [
     "black",
     "pytest",
     "pytest-mypy",
@@ -42,7 +45,7 @@ optional-dependencies.dev = [
     "types-jsonschema",
     "types-tqdm"
 ]
-optional-dependencies.pydantic = [
+pydantic = [
     "stac-pydantic>=3.3.0"
 ]
 
@@ -59,6 +62,7 @@ packages = ["stac_validator"]
 
 [project.scripts]
 stac-validator = "stac_validator.stac_validator:cli"
+stac-valid = "stac_validator.stac_validator:cli"
 
 [tool.setuptools.package-data]
 stac_validator = ["*.yaml"]
\ No newline at end of file


=====================================
stac_validator/fast_validator.py
=====================================
@@ -0,0 +1,359 @@
+import json
+import os
+import time
+import urllib.error
+import urllib.request
+from typing import Any, Dict, List
+
+import click
+import fastjsonschema  # type: ignore
+
+# --- Caches & Config ---
+SCHEMA_CACHE: Dict[str, Any] = {}
+VALIDATOR_CACHE: Dict[Any, Any] = {}
+QUIET_MODE: bool = False
+# Store cached schemas inside the repository under local_schemas/.schemas (project-root relative)
+LOCAL_SCHEMA_DIR = os.path.join(
+    os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
+    "local_schemas",
+    ".schemas",
+)
+
+
+def get_local_path_for_uri(uri: str) -> str:
+    """Creates a safe local filepath for a cached schema URL."""
+    safe_filename = uri.replace("https://", "").replace("http://", "").replace("/", "_")
+    return os.path.join(LOCAL_SCHEMA_DIR, safe_filename)
+
+
+def fetch_schema(uri: str) -> Dict[str, Any]:
+    """The Ultimate Handler: RAM -> Disk -> Network -> Disk -> RAM"""
+
+    # 1. RAM Cache
+    if uri in SCHEMA_CACHE:
+        return SCHEMA_CACHE[uri]
+
+    local_path = get_local_path_for_uri(uri)
+
+    # 2. Disk Cache
+    if os.path.exists(local_path):
+        try:
+            with open(local_path, "r") as f:
+                schema_dict = json.load(f)
+                SCHEMA_CACHE[uri] = schema_dict
+                return schema_dict
+        except Exception:
+            pass  # If corrupted, fallback to network
+
+    # 3. Network Fetch
+    if not QUIET_MODE:
+        click.secho(f"    [Network] Fetching: {uri}", fg="yellow", dim=True)
+    req = urllib.request.Request(uri, headers={"User-Agent": "stac-fast-cli/5.0"})
+    try:
+        with urllib.request.urlopen(req) as response:
+            schema_dict = json.loads(response.read().decode("utf-8"))
+    except urllib.error.URLError as e:
+        raise RuntimeError(f"Could not resolve schema: {uri}. Reason: {e}")
+
+    # 4. Save to Disk Cache
+    os.makedirs(os.path.dirname(local_path), exist_ok=True)
+    try:
+        with open(local_path, "w") as f:
+            json.dump(schema_dict, f)
+    except IOError:
+        pass  # If we can't write to disk, no big deal, keep going
+
+    # 5. Save to RAM Cache
+    SCHEMA_CACHE[uri] = schema_dict
+    return schema_dict
+
+
+def get_validator(stac_type: str, stac_version: str, extensions: List[str]):
+    """Builds and caches a validator based on Object Type, Version, and Extensions."""
+    ext_key = tuple(sorted(extensions))
+    cache_key = (stac_type, stac_version, ext_key)
+
+    if cache_key in VALIDATOR_CACHE:
+        return VALIDATOR_CACHE[cache_key], True
+
+    # Determine base schema URI
+    stac_type_lower = stac_type.lower()
+    if stac_type_lower in ["item", "feature"]:
+        base_uri = f"https://schemas.stacspec.org/v{stac_version}/item-spec/json-schema/item.json"
+    elif stac_type_lower == "collection":
+        base_uri = f"https://schemas.stacspec.org/v{stac_version}/collection-spec/json-schema/collection.json"
+    elif stac_type_lower == "catalog":
+        base_uri = f"https://schemas.stacspec.org/v{stac_version}/catalog-spec/json-schema/catalog.json"
+    else:
+        raise ValueError(f"Unknown STAC type for validation: {stac_type}")
+
+    schema_fragments: List[Dict[str, str]] = [{"$ref": base_uri}]
+    for ext in extensions:
+        schema_fragments.append({"$ref": ext})
+    dynamic_schema = {
+        "$schema": "http://json-schema.org/draft-07/schema#",
+        "allOf": schema_fragments,
+    }
+
+    try:
+        validator = fastjsonschema.compile(
+            dynamic_schema, handlers={"http": fetch_schema, "https": fetch_schema}
+        )
+    except Exception:
+        # FALLBACK: Some schemas (like Item Assets) cause fastjsonschema to generate invalid python code.
+        # We fall back to the standard jsonschema library.
+        click.secho(
+            "    [Fallback] fastjsonschema compile failed. Using python-jsonschema.",
+            fg="yellow",
+            dim=True,
+        )
+        import jsonschema
+
+        # Create a validator using the same custom logic
+        def fallback_validator(data):
+            # We need a resolver to handle the remote $refs
+            resolver = jsonschema.RefResolver(
+                base_uri="",
+                referrer=dynamic_schema,
+                handlers={"http": fetch_schema, "https": fetch_schema},
+            )
+            jsonschema.validate(data, dynamic_schema, resolver=resolver)
+
+        validator = fallback_validator
+
+    VALIDATOR_CACHE[cache_key] = validator
+    return validator, False
+
+
+class FastValidator:
+    def __init__(self, stac_file: str, quiet: bool = False, verbose: bool = False):
+        global QUIET_MODE
+        self.stac_file = stac_file
+        self.quiet = quiet
+        self.valid = True
+        self.verbose = verbose
+        self.message: List[Dict[str, Any]] = []
+        QUIET_MODE = quiet
+
+    def run(self):
+        """Universal high-speed STAC Validator (Items, Collections, Catalogs, FeatureCollections)"""
+        if not self.quiet:
+            click.secho(f"\n📂 Loading: {self.stac_file}", fg="blue", bold=True)
+
+        try:
+            if self.stac_file.startswith("http"):
+                req = urllib.request.Request(
+                    self.stac_file, headers={"User-Agent": "stac-fast-cli/5.0"}
+                )
+                with urllib.request.urlopen(req) as response:
+                    data = json.loads(response.read().decode("utf-8"))
+            else:
+                with open(self.stac_file, "r") as f:
+                    data = json.load(f)
+        except Exception as e:
+            click.secho(f"❌ Error reading {self.stac_file}: {e}", fg="red", bold=True)
+            self.valid = False
+            return
+
+        # Detect payload structure
+        obj_type = data.get("type", "")
+        items_to_validate = []
+
+        if obj_type == "FeatureCollection":
+            items_to_validate = data.get("features", [])
+            if not self.quiet:
+                click.secho(
+                    f"📦 Detected FeatureCollection ({len(items_to_validate)} Items)\n",
+                    fg="cyan",
+                )
+        elif obj_type == "Feature":
+            items_to_validate = [data]
+            if not self.quiet:
+                click.secho("📄 Detected: STAC Item\n", fg="cyan")
+        elif obj_type == "Collection":
+            items_to_validate = [data]
+            if not self.quiet:
+                click.secho("📚 Detected: STAC Collection\n", fg="cyan")
+        elif obj_type == "Catalog" or ("id" in data and "description" in data):
+            # Fallback for old catalogs missing the 'type' field
+            data["type"] = "Catalog"
+            items_to_validate = [data]
+            if not self.quiet:
+                click.secho("🗂️  Detected: STAC Catalog\n", fg="cyan")
+        else:
+            if "type" in data:
+                click.secho(
+                    f"❌ Unknown JSON type. Unsupported 'type' value: {obj_type!r}.",
+                    fg="red",
+                    bold=True,
+                )
+            else:
+                click.secho(
+                    "❌ Unknown JSON type. Missing 'type' field.", fg="red", bold=True
+                )
+            self.valid = False
+            return
+
+        # --- Metrics ---
+        total_setup_ms = 0.0
+        total_exec_ms = 0.0
+        valid_count = 0
+        invalid_count = 0
+        error_registry: Dict[str, List[str]] = {}
+        stac_versions_found: set = set()
+        schemas_checked: set = set()
+
+        for index, item in enumerate(items_to_validate):
+            # Determine specific STAC attributes for this object
+            item_id = item.get("id", f"unknown-{index}")
+            stac_version = item.get("stac_version", "1.0.0")
+            extensions = item.get("stac_extensions", [])
+
+            # Track versions and schemas
+            stac_versions_found.add(stac_version)
+
+            # Map Feature->Item, others keep their type
+            actual_type = (
+                "Item" if item.get("type") == "Feature" else item.get("type", "Catalog")
+            )
+
+            # Build schema URI for this object type
+            stac_type_lower = actual_type.lower()
+            if stac_type_lower in ["item", "feature"]:
+                base_schema = f"https://schemas.stacspec.org/v{stac_version}/item-spec/json-schema/item.json"
+            elif stac_type_lower == "collection":
+                base_schema = f"https://schemas.stacspec.org/v{stac_version}/collection-spec/json-schema/collection.json"
+            elif stac_type_lower == "catalog":
+                base_schema = f"https://schemas.stacspec.org/v{stac_version}/catalog-spec/json-schema/catalog.json"
+            else:
+                base_schema = ""
+
+            if base_schema:
+                schemas_checked.add(base_schema)
+
+            # Track extensions
+            for ext in extensions:
+                schemas_checked.add(ext)
+
+            # --- Setup Timer ---
+            t0 = time.perf_counter()
+            try:
+                validator, is_cached = get_validator(
+                    actual_type, stac_version, extensions
+                )
+            except Exception as e:
+                if not self.quiet:
+                    click.secho(f"❌ Setup failed for {item_id}: {e}", fg="red")
+                invalid_count += 1
+                self.valid = False
+                continue
+            t1 = time.perf_counter()
+            setup_time = (t1 - t0) * 1000
+            total_setup_ms += setup_time
+
+            # --- Execution Timer ---
+            t2 = time.perf_counter()
+            try:
+                validator(item)
+                t3 = time.perf_counter()
+                exec_time = (t3 - t2) * 1000
+                total_exec_ms += exec_time
+                valid_count += 1
+                status_text = click.style("✅ VALID", fg="green")
+
+            except fastjsonschema.JsonSchemaValueException as e:
+                t3 = time.perf_counter()
+                exec_time = (t3 - t2) * 1000
+                total_exec_ms += exec_time
+                invalid_count += 1
+                self.valid = False
+
+                # --- The STAC Error Translator ---
+                error_msg = f"{e.name} {e.message.replace(e.name, '').strip()}"
+                if "disallowed definition" in error_msg:
+                    if "collection" in error_msg:
+                        error_msg = "STAC Spec Violation: Missing {'rel': 'collection'} in links array."
+                    else:
+                        error_msg = (
+                            f"{e.name} violated a 'not' rule. Value: {repr(e.value)}"
+                        )
+
+                # Group errors
+                if error_msg not in error_registry:
+                    error_registry[error_msg] = []
+                error_registry[error_msg].append(item_id)
+                status_text = click.style("❌ INVALID", fg="red")
+
+            if not self.quiet:
+                if self.verbose or index < 5 or (len(items_to_validate) < 20):
+                    cache_icon = "⚡" if is_cached else "🐌"
+                    click.echo(
+                        f"[{index + 1}] ID: {item_id} | Type: {actual_type} | Cache {cache_icon} | Setup: {setup_time:>6.2f}ms | Exec: {exec_time:>5.2f}ms | {status_text}"
+                    )
+                elif index == 5:
+                    click.secho(
+                        "... silencing output for remaining items (validating at maximum speed) ...",
+                        dim=True,
+                    )
+
+        # --- Summary Report ---
+        click.echo("\n" + "=" * 55)
+        click.secho("📊 VALIDATION SUMMARY", bold=True)
+        click.echo("=" * 55)
+        click.echo(f"Total Objects Processed : {len(items_to_validate)}")
+        click.echo(
+            f"Valid Objects           : {click.style(str(valid_count), fg='green')}"
+        )
+
+        invalid_color = "red" if invalid_count > 0 else "green"
+        click.echo(
+            f"Invalid Objects         : {click.style(str(invalid_count), fg=invalid_color)}"
+        )
+
+        click.echo("-" * 55)
+        click.echo(f"Total Setup Time        : {total_setup_ms:.2f} ms")
+        click.echo(f"Total Execution Time    : {total_exec_ms:.2f} ms")
+        if len(items_to_validate) > 0:
+            click.echo(
+                f"Average Exec per Object : {(total_exec_ms / len(items_to_validate)):.3f} ms"
+            )
+
+        if invalid_count > 0:
+            click.echo("=" * 55)
+            click.secho("🚨 ERROR BREAKDOWN", bold=True, fg="red")
+            click.echo("=" * 55)
+            for err_msg, affected_ids in error_registry.items():
+                count = len(affected_ids)
+                click.echo(f"\n❌ {click.style(err_msg, fg='yellow', bold=True)}")
+                click.echo(
+                    f"   Affected Items: {click.style(str(count), fg='red', bold=True)}"
+                )
+                sample_ids = ", ".join(affected_ids[:3])
+                if count > 3:
+                    sample_ids += f" ... (and {count - 3} more)"
+                click.echo(f"   Examples:       {sample_ids}")
+
+        # Populate the message attribute for API usage (similar to StacValidate)
+        self.message = [
+            {
+                "path": self.stac_file,
+                "valid_stac": self.valid,
+                "stac_versions": sorted(list(stac_versions_found)),
+                "schemas_checked": sorted(list(schemas_checked)),
+                "total_objects": len(items_to_validate),
+                "valid_objects": valid_count,
+                "invalid_objects": invalid_count,
+                "setup_time_ms": total_setup_ms,
+                "execution_time_ms": total_exec_ms,
+                "errors": [
+                    {
+                        "error_message": err_msg,
+                        "affected_items": affected_ids,
+                        "count": len(affected_ids),
+                    }
+                    for err_msg, affected_ids in error_registry.items()
+                ],
+            }
+        ]
+
+        click.echo("\n")


=====================================
stac_validator/stac_validator.py
=====================================
@@ -6,6 +6,7 @@ from typing import Any, Dict, List, Optional, Tuple
 import click  # type: ignore
 
 from .batch_validator import validate_concurrently
+from .fast_validator import FastValidator
 from .utilities import set_schema_cache_size
 from .validate import StacValidate
 
@@ -524,6 +525,31 @@ def batch(
         sys.exit(1)
 
 
+ at click.command()
+ at click.argument("stac_file")
+ at click.option(
+    "--quiet",
+    "-q",
+    is_flag=True,
+    help="Suppress individual item logs.",
+)
+ at click.option(
+    "--verbose",
+    "-v",
+    is_flag=True,
+    help="Show full validation logs for all items. By default, a limited sample of item logs is shown.",
+)
+def fast(stac_file: str, quiet: bool, verbose: bool):
+    """High-speed validation using fastjsonschema and local caching."""
+    try:
+        fv = FastValidator(stac_file, quiet=quiet, verbose=verbose)
+        fv.run()
+        sys.exit(0 if fv.valid else 1)
+    except RuntimeError as e:
+        click.secho(f"\n🚨 FATAL ERROR: {e}", fg="red", bold=True)
+        sys.exit(1)
+
+
 @click.group()
 def cli():
     """STAC Validator - Validate STAC files against the STAC specification.
@@ -533,6 +559,7 @@ def cli():
       stac-validator validate <file> [options]
       stac-validator batch <files> [options]
       stac-validator batch <file> --feature-collection [options]
+      stac-validator fast <file> [options]
     """
     pass
 
@@ -540,6 +567,7 @@ def cli():
 # Register commands
 cli.add_command(main, name="validate")
 cli.add_command(batch, name="batch")
+cli.add_command(fast, name="fast")
 
 
 if __name__ == "__main__":


=====================================
tests/test_fast_validator.py
=====================================
@@ -0,0 +1,408 @@
+import json
+
+import pytest
+
+from stac_validator.fast_validator import FastValidator
+
+
+ at pytest.fixture
+def valid_item(tmp_path):
+    """Create a valid STAC Item."""
+    item_path = tmp_path / "valid_item.json"
+    item_data = {
+        "stac_version": "1.0.0",
+        "type": "Feature",
+        "id": "test-item",
+        "geometry": None,
+        "properties": {"datetime": "2023-01-01T00:00:00Z"},
+        "links": [{"rel": "self", "href": "http://example.com"}],
+        "assets": {},
+    }
+    item_path.write_text(json.dumps(item_data))
+    return str(item_path)
+
+
+ at pytest.fixture
+def valid_collection(tmp_path):
+    """Create a valid STAC Collection."""
+    coll_path = tmp_path / "valid_collection.json"
+    coll_data = {
+        "stac_version": "1.0.0",
+        "type": "Collection",
+        "id": "test-collection",
+        "description": "Test collection",
+        "license": "MIT",
+        "extent": {
+            "spatial": {"bbox": [[-180, -90, 180, 90]]},
+            "temporal": {"interval": [["2023-01-01T00:00:00Z", None]]},
+        },
+        "links": [],
+    }
+    coll_path.write_text(json.dumps(coll_data))
+    return str(coll_path)
+
+
+ at pytest.fixture
+def valid_catalog(tmp_path):
+    """Create a valid STAC Catalog."""
+    cat_path = tmp_path / "valid_catalog.json"
+    cat_data = {
+        "stac_version": "1.0.0",
+        "type": "Catalog",
+        "id": "test-catalog",
+        "description": "Test catalog",
+        "links": [],
+    }
+    cat_path.write_text(json.dumps(cat_data))
+    return str(cat_path)
+
+
+ at pytest.fixture
+def valid_feature_collection(tmp_path):
+    """Create a valid FeatureCollection with multiple items."""
+    fc_path = tmp_path / "valid_fc.json"
+    fc_data = {
+        "type": "FeatureCollection",
+        "features": [
+            {
+                "stac_version": "1.0.0",
+                "type": "Feature",
+                "id": f"item-{i}",
+                "geometry": None,
+                "properties": {"datetime": "2023-01-01T00:00:00Z"},
+                "links": [{"rel": "self", "href": "http://example.com"}],
+                "assets": {},
+            }
+            for i in range(5)
+        ],
+    }
+    fc_path.write_text(json.dumps(fc_data))
+    return str(fc_path)
+
+
+ at pytest.fixture
+def invalid_item(tmp_path):
+    """Create an invalid STAC Item (missing required 'id')."""
+    item_path = tmp_path / "invalid_item.json"
+    item_data = {
+        "stac_version": "1.0.0",
+        "type": "Feature",
+        "geometry": None,
+        "properties": {"datetime": "2023-01-01T00:00:00Z"},
+        "links": [],
+        "assets": {},
+    }
+    item_path.write_text(json.dumps(item_data))
+    return str(item_path)
+
+
+ at pytest.fixture
+def invalid_feature_collection(tmp_path):
+    """Create a FeatureCollection with some invalid items."""
+    fc_path = tmp_path / "invalid_fc.json"
+    fc_data = {
+        "type": "FeatureCollection",
+        "features": [
+            {
+                "stac_version": "1.0.0",
+                "type": "Feature",
+                "id": "valid-item",
+                "geometry": None,
+                "properties": {"datetime": "2023-01-01T00:00:00Z"},
+                "links": [{"rel": "self", "href": "http://example.com"}],
+                "assets": {},
+            },
+            {
+                "stac_version": "1.0.0",
+                "type": "Feature",
+                "geometry": None,
+                "properties": {"datetime": "2023-01-01T00:00:00Z"},
+                "links": [],
+                "assets": {},
+            },
+        ],
+    }
+    fc_path.write_text(json.dumps(fc_data))
+    return str(fc_path)
+
+
+class TestFastValidatorBasic:
+    """Test basic functionality of FastValidator."""
+
+    def test_valid_item(self, valid_item):
+        """Test validation of a valid STAC Item."""
+        fv = FastValidator(valid_item, quiet=True)
+        fv.run()
+        assert fv.valid is True
+
+    def test_valid_collection(self, valid_collection):
+        """Test validation of a valid STAC Collection."""
+        fv = FastValidator(valid_collection, quiet=True)
+        fv.run()
+        assert fv.valid is True
+
+    def test_valid_catalog(self, valid_catalog):
+        """Test validation of a valid STAC Catalog."""
+        fv = FastValidator(valid_catalog, quiet=True)
+        fv.run()
+        assert fv.valid is True
+
+    def test_valid_feature_collection(self, valid_feature_collection):
+        """Test validation of a valid FeatureCollection."""
+        fv = FastValidator(valid_feature_collection, quiet=True)
+        fv.run()
+        assert fv.valid is True
+
+    def test_invalid_item(self, invalid_item):
+        """Test that invalid items are detected."""
+        fv = FastValidator(invalid_item, quiet=True)
+        fv.run()
+        assert fv.valid is False
+
+    def test_invalid_feature_collection(self, invalid_feature_collection):
+        """Test that FeatureCollections with invalid items are detected."""
+        fv = FastValidator(invalid_feature_collection, quiet=True)
+        fv.run()
+        assert fv.valid is False
+
+
+class TestFastValidatorOptions:
+    """Test FastValidator options."""
+
+    def test_quiet_mode(self, valid_item, capsys):
+        """Test quiet mode suppresses item-level output."""
+        fv = FastValidator(valid_item, quiet=True)
+        fv.run()
+        captured = capsys.readouterr()
+        assert "VALIDATION SUMMARY" in captured.out
+
+    def test_verbose_mode(self, valid_feature_collection, capsys):
+        """Test verbose mode shows all items."""
+        fv = FastValidator(valid_feature_collection, quiet=False, verbose=True)
+        fv.run()
+        captured = capsys.readouterr()
+        assert "[1]" in captured.out
+        assert "[5]" in captured.out
+
+    def test_non_verbose_mode(self, tmp_path, capsys):
+        """Test non-verbose mode shows first 5 items and silences rest."""
+        fc_path = tmp_path / "large_fc.json"
+        fc_data = {
+            "type": "FeatureCollection",
+            "features": [
+                {
+                    "stac_version": "1.0.0",
+                    "type": "Feature",
+                    "id": f"item-{i}",
+                    "geometry": None,
+                    "properties": {"datetime": "2023-01-01T00:00:00Z"},
+                    "links": [{"rel": "self", "href": "http://example.com"}],
+                    "assets": {},
+                }
+                for i in range(20)
+            ],
+        }
+        fc_path.write_text(json.dumps(fc_data))
+
+        fv = FastValidator(str(fc_path), quiet=False, verbose=False)
+        fv.run()
+        captured = capsys.readouterr()
+        assert "[1]" in captured.out
+        assert "silencing output" in captured.out
+
+
+class TestFastValidatorDetection:
+    """Test STAC type detection."""
+
+    def test_detects_item(self, valid_item, capsys):
+        """Test detection of STAC Item."""
+        fv = FastValidator(valid_item, quiet=False)
+        fv.run()
+        captured = capsys.readouterr()
+        assert "Item" in captured.out or "Feature" in captured.out
+
+    def test_detects_collection(self, valid_collection, capsys):
+        """Test detection of STAC Collection."""
+        fv = FastValidator(valid_collection, quiet=False)
+        fv.run()
+        captured = capsys.readouterr()
+        assert "Collection" in captured.out
+
+    def test_detects_catalog(self, valid_catalog, capsys):
+        """Test detection of STAC Catalog."""
+        fv = FastValidator(valid_catalog, quiet=False)
+        fv.run()
+        captured = capsys.readouterr()
+        assert "Catalog" in captured.out
+
+    def test_detects_feature_collection(self, valid_feature_collection, capsys):
+        """Test detection of FeatureCollection."""
+        fv = FastValidator(valid_feature_collection, quiet=False)
+        fv.run()
+        captured = capsys.readouterr()
+        assert "FeatureCollection" in captured.out
+
+
+class TestFastValidatorErrorHandling:
+    """Test error handling."""
+
+    def test_file_not_found(self):
+        """Test handling of missing file."""
+        fv = FastValidator("/nonexistent/path/file.json", quiet=True)
+        fv.run()
+        assert fv.valid is False
+
+    def test_invalid_json(self, tmp_path):
+        """Test handling of invalid JSON."""
+        bad_json_path = tmp_path / "bad.json"
+        bad_json_path.write_text("{ invalid json }")
+
+        fv = FastValidator(str(bad_json_path), quiet=True)
+        fv.run()
+        assert fv.valid is False
+
+    def test_unknown_type(self, tmp_path):
+        """Test handling of unknown STAC type."""
+        unknown_path = tmp_path / "unknown.json"
+        unknown_data = {"type": "UnknownType", "id": "test"}
+        unknown_path.write_text(json.dumps(unknown_data))
+
+        fv = FastValidator(str(unknown_path), quiet=True)
+        fv.run()
+        assert fv.valid is False
+
+
+class TestFastValidatorPerformance:
+    """Test performance characteristics."""
+
+    def test_caching_works(self, valid_feature_collection):
+        """Test that validator caching works."""
+        fv = FastValidator(valid_feature_collection, quiet=True)
+        fv.run()
+        assert fv.valid is True
+
+    def test_large_feature_collection(self, tmp_path):
+        """Test validation of a large FeatureCollection."""
+        fc_path = tmp_path / "large_fc.json"
+        fc_data = {
+            "type": "FeatureCollection",
+            "features": [
+                {
+                    "stac_version": "1.0.0",
+                    "type": "Feature",
+                    "id": f"item-{i}",
+                    "geometry": None,
+                    "properties": {"datetime": "2023-01-01T00:00:00Z"},
+                    "links": [{"rel": "self", "href": "http://example.com"}],
+                    "assets": {},
+                }
+                for i in range(100)
+            ],
+        }
+        fc_path.write_text(json.dumps(fc_data))
+
+        fv = FastValidator(str(fc_path), quiet=True)
+        fv.run()
+        assert fv.valid is True
+
+    def test_message_attribute_structure(self, valid_item):
+        """Test that the message attribute has the correct structure."""
+        fv = FastValidator(valid_item, quiet=True)
+        fv.run()
+
+        # Verify message is a list with one dict
+        assert isinstance(fv.message, list)
+        assert len(fv.message) == 1
+
+        msg = fv.message[0]
+
+        # Verify required fields exist
+        assert "path" in msg
+        assert "valid_stac" in msg
+        assert "stac_versions" in msg
+        assert "schemas_checked" in msg
+        assert "total_objects" in msg
+        assert "valid_objects" in msg
+        assert "invalid_objects" in msg
+        assert "setup_time_ms" in msg
+        assert "execution_time_ms" in msg
+        assert "errors" in msg
+
+        # Verify field types
+        assert isinstance(msg["path"], str)
+        assert isinstance(msg["valid_stac"], bool)
+        assert isinstance(msg["stac_versions"], list)
+        assert isinstance(msg["schemas_checked"], list)
+        assert isinstance(msg["total_objects"], int)
+        assert isinstance(msg["valid_objects"], int)
+        assert isinstance(msg["invalid_objects"], int)
+        assert isinstance(msg["setup_time_ms"], float)
+        assert isinstance(msg["execution_time_ms"], float)
+        assert isinstance(msg["errors"], list)
+
+    def test_message_attribute_valid_items(self, valid_feature_collection):
+        """Test message attribute for valid items."""
+        fv = FastValidator(valid_feature_collection, quiet=True)
+        fv.run()
+
+        msg = fv.message[0]
+
+        # For valid items
+        assert msg["valid_stac"] is True
+        assert msg["total_objects"] == 5
+        assert msg["valid_objects"] == 5
+        assert msg["invalid_objects"] == 0
+        assert len(msg["errors"]) == 0
+
+        # Verify versions and schemas are tracked
+        assert len(msg["stac_versions"]) > 0
+        assert len(msg["schemas_checked"]) > 0
+        assert "1.0.0" in msg["stac_versions"]
+
+    def test_message_attribute_invalid_items(self, tmp_path):
+        """Test message attribute for invalid items."""
+        fc_path = tmp_path / "invalid_fc.json"
+        fc_data = {
+            "type": "FeatureCollection",
+            "features": [
+                {
+                    "stac_version": "1.0.0",
+                    "type": "Feature",
+                    "id": "item-1",
+                    "geometry": None,
+                    "properties": {"datetime": "2023-01-01T00:00:00Z"},
+                    "links": [{"rel": "self", "href": "http://example.com"}],
+                    "assets": {},
+                },
+                {
+                    "stac_version": "1.0.0",
+                    "type": "Feature",
+                    "id": "item-2",
+                    "geometry": None,
+                    # Missing required 'properties' field
+                    "links": [{"rel": "self", "href": "http://example.com"}],
+                    "assets": {},
+                },
+            ],
+        }
+        fc_path.write_text(json.dumps(fc_data))
+
+        fv = FastValidator(str(fc_path), quiet=True)
+        fv.run()
+
+        msg = fv.message[0]
+
+        # For mixed valid/invalid items
+        assert msg["valid_stac"] is False
+        assert msg["total_objects"] == 2
+        assert msg["valid_objects"] == 1
+        assert msg["invalid_objects"] == 1
+        assert len(msg["errors"]) > 0
+
+        # Verify error structure
+        for error in msg["errors"]:
+            assert "error_message" in error
+            assert "affected_items" in error
+            assert "count" in error
+            assert isinstance(error["affected_items"], list)
+            assert error["count"] == len(error["affected_items"])



View it on GitLab: https://salsa.debian.org/debian-gis-team/stac-validator/-/compare/c580fc6f5b4df44a902ee1353cb8cf08a7f96e50...6cf80b475c0aebe6451b571e57a97aeffae9bfae

-- 
View it on GitLab: https://salsa.debian.org/debian-gis-team/stac-validator/-/compare/c580fc6f5b4df44a902ee1353cb8cf08a7f96e50...6cf80b475c0aebe6451b571e57a97aeffae9bfae
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/20260429/fddb4cce/attachment-0001.htm>


More information about the Pkg-grass-devel mailing list