[Git][debian-gis-team/stac-check][master] 6 commits: New upstream version 1.14.0
Antonio Valentino (@antonio.valentino)
gitlab at salsa.debian.org
Sun May 10 10:32:52 BST 2026
Antonio Valentino pushed to branch master 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
- - - - -
3b206a89 by Antonio Valentino at 2026-05-10T09:06:56+00:00
Update upstream source from tag 'upstream/1.14.0'
Update to upstream version '1.14.0'
with Debian dir 5388d4db9d16f314a9afd835eb23b239eeb914a1
- - - - -
ddcbe2fb by Antonio Valentino at 2026-05-10T09:18:40+00:00
New upstream release
- - - - -
681d7776 by Antonio Valentino at 2026-05-10T09:30:51+00:00
Refresh all patches
- - - - -
30023cf9 by Antonio Valentino at 2026-05-10T09:30:58+00:00
Add dependency on pybuild-plugin-pyproject
- - - - -
61707cfa by Antonio Valentino at 2026-05-10T09:30:58+00:00
Set distribution to unstable
- - - - -
21 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
- debian/changelog
- debian/control
- debian/patches/0001-Fix-privacy-breaches.patch
- debian/patches/0002-No-network.patch
- + 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
=====================================
debian/changelog
=====================================
@@ -1,9 +1,16 @@
-stac-check (1.11.1-5) UNRELEASED; urgency=medium
+stac-check (1.14.0-1) unstable; urgency=medium
- * Team upload.
+ [ Bas Couwenberg ]
* Bump Standards-Version to 4.7.4, no changes.
- -- Bas Couwenberg <sebastic at debian.org> Sat, 04 Apr 2026 10:23:33 +0200
+ [ Antonio Valentino ]
+ * New upstream release (Closes: #1135488).
+ * debian/patches:
+ - Refresh all patches.
+ * debian?control:
+ - Add dependency on pybuild-plugin-pyproject.
+
+ -- Antonio Valentino <antonio.valentino at tiscali.it> Sun, 10 May 2026 09:24:40 +0000
stac-check (1.11.1-4) unstable; urgency=medium
=====================================
debian/control
=====================================
@@ -7,6 +7,7 @@ Build-Depends: debhelper-compat (= 13),
dh-sequence-python3,
dh-sequence-sphinxdoc <!nodoc>,
libjs-highlight.js <!nodoc>,
+ pybuild-plugin-pyproject,
python3-all,
python3-click,
python3-dotenv,
=====================================
debian/patches/0001-Fix-privacy-breaches.patch
=====================================
@@ -8,7 +8,7 @@ Forwarded: not-needed
1 file changed, 1 insertion(+), 17 deletions(-)
diff --git a/README.md b/README.md
-index 542726b..173d890 100644
+index 8148169..af0804e 100644
--- a/README.md
+++ b/README.md
@@ -2,17 +2,6 @@
@@ -29,7 +29,7 @@ index 542726b..173d890 100644
## A linting and validation tool for STAC assets
The intent of this project is to provide a validation tool that also follows the official [STAC Best Practices document](https://github.com/radiantearth/stac-spec/blob/master/best-practices.md)
-@@ -528,11 +517,6 @@ The following organizations have contributed time and/or funding to support the
+@@ -602,11 +591,6 @@ The following organizations have contributed time and/or funding to support the
- [Healy Hyperspatial](https://healy-hyperspatial.github.io/)
- [Radiant Earth Foundation](https://radiant.earth/)
@@ -41,7 +41,7 @@ index 542726b..173d890 100644
We are grateful for the support of our sponsors who help make this project possible. If your organization uses stac-check and would like to become a sponsor, please reach out to us!
## Contributing
-@@ -560,4 +544,4 @@ If you find a bug or have a feature request, please open an issue on the [GitHub
+@@ -634,4 +618,4 @@ If you find a bug or have a feature request, please open an issue on the [GitHub
## License
=====================================
debian/patches/0002-No-network.patch
=====================================
@@ -4,31 +4,33 @@ Subject: No network
Forwarded: not-needed
---
- pyproject.toml | 4 ++++
- tests/test_cli.py | 3 +++
+ pyproject.toml | 5 +++++
+ tests/test_cli.py | 8 ++++++++
tests/test_lint.py | 6 ++++++
tests/test_lint_assets.py | 6 ++++++
tests/test_lint_dictionary.py | 4 ++++
tests/test_lint_recursion.py | 6 ++++++
tests/test_lint_stac_api.py | 6 ++++++
- 7 files changed, 35 insertions(+)
- create mode 100644 pyproject.toml
+ 7 files changed, 41 insertions(+)
diff --git a/pyproject.toml b/pyproject.toml
-new file mode 100644
-index 0000000..c5c76c2
---- /dev/null
+index 62a182b..07da7d6 100644
+--- a/pyproject.toml
+++ b/pyproject.toml
-@@ -0,0 +1,4 @@
+@@ -48,3 +48,8 @@ packages = ["stac_check"]
+
+ [tool.setuptools.package-data]
+ stac_check = ["**/*"]
++
+[tool.pytest.ini_options]
+markers = [
+ "network: test requires network access",
+]
diff --git a/tests/test_cli.py b/tests/test_cli.py
-index 42de225..95a30f8 100644
+index 3d2ff15..797ea24 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
-@@ -31,6 +31,7 @@ def test_cli_version(runner):
+@@ -33,6 +33,7 @@ def test_cli_version(runner):
assert "version" in result.output
@@ -36,7 +38,7 @@ index 42de225..95a30f8 100644
def test_cli_validate_local_file(runner):
"""Test validating a local file."""
test_file = os.path.join(
-@@ -41,6 +42,7 @@ def test_cli_validate_local_file(runner):
+@@ -43,6 +44,7 @@ def test_cli_validate_local_file(runner):
assert "Passed: True" in result.output
@@ -44,7 +46,7 @@ index 42de225..95a30f8 100644
def test_cli_validate_recursive(runner):
"""Test recursive validation."""
test_dir = os.path.join(
-@@ -51,6 +53,7 @@ def test_cli_validate_recursive(runner):
+@@ -53,6 +55,7 @@ def test_cli_validate_recursive(runner):
assert "Assets Checked" in result.output
@@ -52,6 +54,46 @@ index 42de225..95a30f8 100644
def test_cli_output_to_file(runner):
"""Test output to file with --output flag."""
test_file = os.path.join(
+@@ -291,6 +294,7 @@ def test_is_item_collection_with_mock_url():
+ assert is_item_collection("https://example.com/items") is True
+
+
++ at pytest.mark.network
+ def test_cli_auto_detect_item_collection(runner):
+ """Test that the CLI automatically detects and validates item collections."""
+ test_file = os.path.join(
+@@ -303,6 +307,7 @@ def test_cli_auto_detect_item_collection(runner):
+ assert "Item Collection" in result.output or "Assets Checked" in result.output
+
+
++ at pytest.mark.network
+ def test_cli_auto_detect_respects_explicit_flag(runner):
+ """Test that explicit --item-collection flag still works."""
+ test_file = os.path.join(
+@@ -331,6 +336,7 @@ def test_cli_auto_detect_does_not_trigger_with_recursive(runner):
+ assert mock_linter.called
+
+
++ at pytest.mark.network
+ def test_cli_fast_validation_single_file(runner):
+ """Test --fast flag with a single file."""
+ test_file = os.path.join(
+@@ -341,6 +347,7 @@ def test_cli_fast_validation_single_file(runner):
+ assert "FastJSONSchema" in result.output
+
+
++ at pytest.mark.network
+ def test_cli_fast_validation_item_collection(runner):
+ """Test --fast flag with an item collection."""
+ test_file = os.path.join(
+@@ -353,6 +360,7 @@ def test_cli_fast_validation_item_collection(runner):
+ assert "Schemas checked:" in result.output
+
+
++ at pytest.mark.network
+ def test_cli_fast_validation_shows_timing(runner):
+ """Test that --fast flag shows timing information."""
+ test_file = os.path.join(
diff --git a/tests/test_lint.py b/tests/test_lint.py
index b2cdbee..323c25a 100644
--- a/tests/test_lint.py
=====================================
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/-/compare/b81c3046f2b74172aafa59d50dd991bcc09e3b97...61707cfadb4472c2d9090b6b3786ec814a61ba56
--
View it on GitLab: https://salsa.debian.org/debian-gis-team/stac-check/-/compare/b81c3046f2b74172aafa59d50dd991bcc09e3b97...61707cfadb4472c2d9090b6b3786ec814a61ba56
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/1e40628d/attachment-0001.htm>
More information about the Pkg-grass-devel
mailing list