From d571706ea2cfa89c014c4553676e701f10112fce Mon Sep 17 00:00:00 2001 From: jonhealy1 Date: Sun, 16 Nov 2025 12:00:41 +0800 Subject: [PATCH 1/9] check stac version format --- stac_validator/utilities.py | 65 +++++++++++++++++++++++ stac_validator/validate.py | 34 ++++++++++-- tests/test_default.py | 102 ++++++++++++++++++++++++++++++++++++ 3 files changed, 198 insertions(+), 3 deletions(-) diff --git a/stac_validator/utilities.py b/stac_validator/utilities.py index c1baf60c..befccdb1 100644 --- a/stac_validator/utilities.py +++ b/stac_validator/utilities.py @@ -25,6 +25,71 @@ ] +def validate_stac_version_field(stac_content: Dict) -> tuple[bool, str, str]: + """Validate the stac_version field in STAC content. + + Args: + stac_content (dict): The STAC content dictionary. + + Returns: + tuple[bool, str, str]: (is_valid, error_type, error_message) + - is_valid: True if the version is valid + - error_type: Error type string if invalid, empty string if valid + - error_message: Error message if invalid, empty string if valid + """ + version = stac_content.get("stac_version", "") + + # Check if version is present and not empty + if not version or not isinstance(version, str) or version.strip() == "": + error_type = "MissingSTACVersion" + error_msg = ( + "The 'stac_version' field is missing or empty. " + "Please ensure your STAC object includes a valid 'stac_version' field " + "(e.g., '1.0.0', '1.1.0'). This field is required for proper schema validation." + ) + return False, error_type, error_msg + + # Validate version format + format_valid, format_error = validate_version_format(version) + if not format_valid: + return False, "InvalidSTACVersionFormat", format_error + + return True, "", "" + + +def validate_version_format(version: str) -> tuple[bool, str]: + """Validate that a STAC version string has the correct format. + + Args: + version (str): The version string to validate. + + Returns: + tuple[bool, str]: (is_valid, error_message) + - is_valid: True if the version format is valid + - error_message: Description of the issue if invalid, empty string if valid + + Valid formats: + - Standard semver: "1.0.0", "1.1.0", "0.9.0" + - Pre-release versions: "1.0.0-beta.1", "1.0.0-rc.1" + """ + if not version: + return False, "Version is empty" + + import re + + # Regex for semantic versioning: major.minor.patch with optional pre-release + semver_pattern = r"^\d+\.\d+\.\d+(-[\w\.\-]+)?$" + + if not re.match(semver_pattern, version): + return False, ( + f"Version '{version}' does not match expected format. " + "STAC versions should be in semantic versioning format (e.g., '1.0.0', '1.1.0', '1.0.0-beta.1'). " + "Please check your 'stac_version' field." + ) + + return True, "" + + def is_url(url: str) -> bool: """Checks whether the input string is a valid URL. diff --git a/stac_validator/validate.py b/stac_validator/validate.py index 0c68cec8..fd59a97e 100644 --- a/stac_validator/validate.py +++ b/stac_validator/validate.py @@ -18,6 +18,7 @@ link_request, load_schema_config, set_schema_addr, + validate_stac_version_field, validate_with_ref_resolver, ) @@ -247,6 +248,7 @@ def create_err_msg( err_msg: str, error_obj: Optional[Exception] = None, schema_uri: str = "", + version: Optional[str] = None, ) -> Dict[str, Union[str, bool, List[str], Dict[str, Any]]]: """ Create a standardized error message dictionary and mark validation as failed. @@ -256,6 +258,7 @@ def create_err_msg( err_msg (str): The error message. error_obj (Optional[Exception]): The raw exception object for verbose details. schema_uri (str, optional): The URI of the schema that failed validation. + version (Optional[str]): Override version to use in the error message. Returns: dict: Dictionary containing error information. @@ -268,9 +271,16 @@ def create_err_msg( if not isinstance(err_msg, str): err_msg = str(err_msg) + # Use provided version or fall back to self.version + version_to_use = ( + version + if version is not None + else (str(self.version) if hasattr(self, "version") else "") + ) + # Initialize the message with common fields message: Dict[str, Any] = { - "version": str(self.version) if hasattr(self, "version") else "", + "version": version_to_use, "path": str(self.stac_file) if hasattr(self, "stac_file") else "", "schema": ( [self._original_schema_paths.get(self.schema, self.schema)] @@ -306,7 +316,7 @@ def create_err_msg( # Initialize the error message with common fields error_message: Dict[str, Union[str, bool, List[str], Dict[str, Any]]] = { - "version": str(self.version) if self.version is not None else "", + "version": version_to_use, "path": str(self.stac_file) if self.stac_file is not None else "", "schema": schema_field, # All schemas that were checked "valid_stac": False, @@ -950,7 +960,25 @@ def run(self) -> bool: self.stac_content = fetch_and_parse_file(self.stac_file, self.headers) stac_type = get_stac_type(self.stac_content).upper() - self.version = self.stac_content["stac_version"] + version = self.stac_content.get("stac_version", "") + + # Validate stac_version field comprehensively + version_valid, version_error_type, version_error_msg = ( + validate_stac_version_field(self.stac_content) + ) + if not version_valid: + message.update( + self.create_err_msg( + err_type=version_error_type, + err_msg=version_error_msg, + schema_uri="", + version=version, # Pass the version we extracted + ) + ) + self.message.append(message) + return self.valid + + self.version = version if self.core: message = self.create_message(stac_type, "core") diff --git a/tests/test_default.py b/tests/test_default.py index 44c6e13c..d1c496e7 100644 --- a/tests/test_default.py +++ b/tests/test_default.py @@ -180,3 +180,105 @@ def test_default_collection_validates_extensions(): "validation_method": "default", } ] + + +def test_missing_stac_version(): + """Test that missing or empty stac_version provides a clear error message.""" + import json + import tempfile + + # Create a test STAC object with empty stac_version + test_stac = { + "type": "Collection", + "id": "test-collection", + "stac_version": "", # Empty stac_version + "description": "Test collection", + "license": "MIT", + "extent": { + "spatial": {"bbox": [[-180, -90, 180, 90]]}, + "temporal": {"interval": [["2020-01-01T00:00:00Z", None]]}, + }, + "links": [{"rel": "self", "href": "test.json", "type": "application/json"}], + } + + # Write to temp file + with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f: + json.dump(test_stac, f) + temp_file = f.name + + try: + stac = stac_validator.StacValidate(temp_file) + stac.run() + assert stac.message == [ + { + "version": "", + "path": temp_file, + "schema": [], + "valid_stac": False, + "error_type": "MissingSTACVersion", + "error_message": ( + "The 'stac_version' field is missing or empty. " + "Please ensure your STAC object includes a valid 'stac_version' field " + "(e.g., '1.0.0', '1.1.0'). This field is required for proper schema validation." + ), + "failed_schema": "", + "recommendation": "For more accurate error information, rerun with --verbose.", + } + ] + finally: + import os + + os.unlink(temp_file) + + +def test_invalid_stac_version_format(): + """Test that invalid stac_version format provides a clear error message.""" + import json + import tempfile + + # Test cases for invalid formats + invalid_versions = ["1.1", "1", "1.0", "abc", "1.0.0.0"] + + for invalid_version in invalid_versions: + # Create a test STAC object with invalid stac_version format + test_stac = { + "type": "Collection", + "id": "test-collection", + "stac_version": invalid_version, # Invalid format + "description": "Test collection", + "license": "MIT", + "extent": { + "spatial": {"bbox": [[-180, -90, 180, 90]]}, + "temporal": {"interval": [["2020-01-01T00:00:00Z", None]]}, + }, + "links": [{"rel": "self", "href": "test.json", "type": "application/json"}], + } + + # Write to temp file + with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f: + json.dump(test_stac, f) + temp_file = f.name + + try: + stac = stac_validator.StacValidate(temp_file) + stac.run() + assert stac.message == [ + { + "version": invalid_version, + "path": temp_file, + "schema": [], + "valid_stac": False, + "error_type": "InvalidSTACVersionFormat", + "error_message": ( + f"Version '{invalid_version}' does not match expected format. " + "STAC versions should be in semantic versioning format (e.g., '1.0.0', '1.1.0', '1.0.0-beta.1'). " + "Please check your 'stac_version' field." + ), + "failed_schema": "", + "recommendation": "For more accurate error information, rerun with --verbose.", + } + ] + finally: + import os + + os.unlink(temp_file) From 62f0a1718593cc17d3102714ade642a927e8d358 Mon Sep 17 00:00:00 2001 From: jonhealy1 Date: Sun, 16 Nov 2025 12:01:57 +0800 Subject: [PATCH 2/9] v3.10.2, update to pyproject --- CHANGELOG.md | 11 ++++++++- pyproject.toml | 59 +++++++++++++++++++++++++++++++++++++++----- requirements-dev.txt | 6 ----- setup.py | 47 ----------------------------------- 4 files changed, 63 insertions(+), 60 deletions(-) delete mode 100644 requirements-dev.txt delete mode 100644 setup.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b1dfa43..81a36d70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ The format is (loosely) based on [Keep a Changelog](http://keepachangelog.com/) ## [Unreleased] +## [v3.10.2] - 2025-11-16 + +### Fixed +- Added validation for STAC version format to provide clear error messages when stac_version field is missing, empty, or incorrectly formatted (e.g., "1.1" instead of "1.1.0"). [#268](https://github.com/stac-utils/stac-validator/pull/268) + +### Changed +- Migrated from `setup.py` to modern `pyproject.toml` packaging (PEP 621), removing legacy `requirements-dev.txt` file. [#268](https://github.com/stac-utils/stac-validator/pull/268) + ## [v3.10.1] - 2025-07-26 ### Fixed @@ -302,7 +310,8 @@ 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/v3.10.1..main +[Unreleased]: https://github.com/sparkgeo/stac-validator/compare/v3.10.2..main +[v3.10.2]: https://github.com/sparkgeo/stac-validator/compare/v3.10.1..v3.10.2 [v3.10.1]: https://github.com/sparkgeo/stac-validator/compare/v3.10.0..v3.10.1 [v3.10.0]: https://github.com/sparkgeo/stac-validator/compare/v3.9.3..v3.10.0 [v3.9.3]: https://github.com/sparkgeo/stac-validator/compare/v3.9.2..v3.9.3 diff --git a/pyproject.toml b/pyproject.toml index b75f5c12..cd18f47f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,8 +1,55 @@ [build-system] -requires = [ - "requests", - "jsonschema", - "click", - "setuptools" +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "stac_validator" +version = "3.10.2" +description = "A package to validate STAC files" +authors = [ + {name = "James Banting"}, + {name = "Jonathan Healy", email = "jonathan.d.healy@gmail.com"} ] -build-backend = "setuptools.build_meta" \ No newline at end of file +license = {text = "Apache-2.0"} +classifiers = [ + "Intended Audience :: Information Technology", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python :: 3.8", + "Topic :: Scientific/Engineering :: GIS", +] +keywords = ["STAC", "validation", "raster"] +requires-python = ">=3.8" +dependencies = [ + "requests>=2.32.3", + "jsonschema>=4.23.0", + "click>=8.1.8", + "referencing>=0.35.1", + "pyYAML>=6.0.1", +] +optional-dependencies.dev = [ + "pytest", + "requests-mock", + "types-setuptools", + "stac-pydantic>=3.3.0" +] +optional-dependencies.pydantic = [ + "stac-pydantic>=3.3.0" +] + +[project.urls] +Homepage = "https://github.com/stac-utils/stac-validator" +Repository = "https://github.com/stac-utils/stac-validator" + +[project.readme] +file = "README.md" +content-type = "text/markdown" + +[tool.setuptools] +packages = ["stac_validator"] + +[project.scripts] +stac-validator = "stac_validator.stac_validator:main" + +[tool.setuptools.package-data] +stac_validator = ["*.yaml"] \ No newline at end of file diff --git a/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index aec90522..00000000 --- a/requirements-dev.txt +++ /dev/null @@ -1,6 +0,0 @@ -black -pytest -pytest-mypy -pre-commit -requests-mock -types-jsonschema diff --git a/setup.py b/setup.py deleted file mode 100644 index d3d28184..00000000 --- a/setup.py +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env python - -from setuptools import setup - -__version__ = "3.10.1" - -with open("README.md", "r") as fh: - long_description = fh.read() - -setup( - name="stac_validator", - version=__version__, - author="James Banting, Jonathan Healy", - author_email="jonathan.d.healy@gmail.com", - description="A package to validate STAC files", - license="Apache-2.0", - classifiers=[ - "Intended Audience :: Information Technology", - "Intended Audience :: Science/Research", - "License :: OSI Approved :: Apache Software License", - "Programming Language :: Python :: 3.8", - "Topic :: Scientific/Engineering :: GIS", - ], - keywords="STAC validation raster", - long_description=long_description, - long_description_content_type="text/markdown", - url="https://github.com/stac-utils/stac-validator", - install_requires=[ - "requests>=2.32.3", - "jsonschema>=4.23.0", - "click>=8.1.8", - "referencing>=0.35.1", - "pyYAML>=6.0.1", - ], - extras_require={ - "dev": ["pytest", "requests-mock", "types-setuptools", "stac-pydantic>=3.3.0"], - "pydantic": [ - "stac-pydantic>=3.3.0", - ], - }, - packages=["stac_validator"], - entry_points={ - "console_scripts": ["stac-validator = stac_validator.stac_validator:main"] - }, - python_requires=">=3.8", - tests_require=["pytest", "requests-mock"], -) From 7cde4d34f1aaeee58ecf27114822f1a9444d0f99 Mon Sep 17 00:00:00 2001 From: jonhealy1 Date: Sun, 16 Nov 2025 12:05:45 +0800 Subject: [PATCH 3/9] fix gh workflows --- .github/workflows/publish.yml | 4 ++-- .github/workflows/test-runner.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 1d89ac3d..32651036 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -21,11 +21,11 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install setuptools wheel twine + pip install setuptools wheel twine build - name: Build package run: | - python setup.py sdist bdist_wheel + python -m build - name: Publish package to PyPI env: diff --git a/.github/workflows/test-runner.yml b/.github/workflows/test-runner.yml index 7be3c3ed..e56a5250 100644 --- a/.github/workflows/test-runner.yml +++ b/.github/workflows/test-runner.yml @@ -29,7 +29,7 @@ jobs: if: matrix.python-version == '3.12' run: | pip install . - pip install -r requirements-dev.txt + pip install -e .[dev] mypy stac_validator/ - name: Run pre-commit From c9a34873cfa03161bd044253c70d01c964082476 Mon Sep 17 00:00:00 2001 From: jonhealy1 Date: Sun, 16 Nov 2025 12:32:57 +0800 Subject: [PATCH 4/9] install mypy --- pyproject.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index cd18f47f..85163f39 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,10 @@ optional-dependencies.dev = [ "pytest", "requests-mock", "types-setuptools", - "stac-pydantic>=3.3.0" + "stac-pydantic>=3.3.0", + "mypy", + "types-attrs", + "types-requests" ] optional-dependencies.pydantic = [ "stac-pydantic>=3.3.0" From e025da515d544b896b38ca34fff52366139551b2 Mon Sep 17 00:00:00 2001 From: jonhealy1 Date: Sun, 16 Nov 2025 12:53:22 +0800 Subject: [PATCH 5/9] add types jsonschema --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 85163f39..cbe678ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,8 @@ optional-dependencies.dev = [ "stac-pydantic>=3.3.0", "mypy", "types-attrs", - "types-requests" + "types-requests", + "types-jsonschema" ] optional-dependencies.pydantic = [ "stac-pydantic>=3.3.0" From 21e687030b7d51e60bb53db3986f6c0197cd31e0 Mon Sep 17 00:00:00 2001 From: jonhealy1 Date: Sun, 16 Nov 2025 12:55:15 +0800 Subject: [PATCH 6/9] pre-commit --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index cbe678ea..fcc16424 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,10 @@ dependencies = [ "pyYAML>=6.0.1", ] optional-dependencies.dev = [ + "black", "pytest", + "pytest-mypy", + "pre-commit", "requests-mock", "types-setuptools", "stac-pydantic>=3.3.0", From c8326e6d8993b0ec2870ec0adc7ad436ffac973b Mon Sep 17 00:00:00 2001 From: jonhealy1 Date: Sun, 16 Nov 2025 12:58:49 +0800 Subject: [PATCH 7/9] use typing Tuple --- stac_validator/utilities.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stac_validator/utilities.py b/stac_validator/utilities.py index befccdb1..eace2cdf 100644 --- a/stac_validator/utilities.py +++ b/stac_validator/utilities.py @@ -2,7 +2,7 @@ import json import os import ssl -from typing import Dict, Optional +from typing import Dict, Optional, Tuple from urllib.parse import urlparse from urllib.request import Request, urlopen @@ -57,7 +57,7 @@ def validate_stac_version_field(stac_content: Dict) -> tuple[bool, str, str]: return True, "", "" -def validate_version_format(version: str) -> tuple[bool, str]: +def validate_version_format(version: str) -> Tuple[bool, str]: """Validate that a STAC version string has the correct format. Args: From d83c5db1ea1f019795ad9ecdbd8e7574b4744ea0 Mon Sep 17 00:00:00 2001 From: jonhealy1 Date: Sun, 16 Nov 2025 13:01:00 +0800 Subject: [PATCH 8/9] Fix Python 3.8 compatibility: use Tuple from typing instead of tuple[bool, str, str] - Change tuple[bool, str, str] to Tuple[bool, str, str] for validate_stac_version_field - This fixes the TypeError in Python 3.8 where tuple subscript notation is not supported --- stac_validator/utilities.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stac_validator/utilities.py b/stac_validator/utilities.py index eace2cdf..99fff220 100644 --- a/stac_validator/utilities.py +++ b/stac_validator/utilities.py @@ -25,7 +25,7 @@ ] -def validate_stac_version_field(stac_content: Dict) -> tuple[bool, str, str]: +def validate_stac_version_field(stac_content: Dict) -> Tuple[bool, str, str]: """Validate the stac_version field in STAC content. Args: From b234974dc3ab8d9c6c1bb023541a0af97efd72f6 Mon Sep 17 00:00:00 2001 From: jonhealy1 Date: Sun, 16 Nov 2025 13:01:53 +0800 Subject: [PATCH 9/9] Update docstrings to match Python 3.8 compatible type annotations - Change 'tuple[bool, str, str]' to 'Tuple[bool, str, str]' in validate_stac_version_field docstring - Change 'tuple[bool, str]' to 'Tuple[bool, str]' in validate_version_format docstring - Keep docstrings consistent with actual type annotations --- stac_validator/utilities.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stac_validator/utilities.py b/stac_validator/utilities.py index 99fff220..40cbfeeb 100644 --- a/stac_validator/utilities.py +++ b/stac_validator/utilities.py @@ -32,7 +32,7 @@ def validate_stac_version_field(stac_content: Dict) -> Tuple[bool, str, str]: stac_content (dict): The STAC content dictionary. Returns: - tuple[bool, str, str]: (is_valid, error_type, error_message) + Tuple[bool, str, str]: (is_valid, error_type, error_message) - is_valid: True if the version is valid - error_type: Error type string if invalid, empty string if valid - error_message: Error message if invalid, empty string if valid @@ -64,7 +64,7 @@ def validate_version_format(version: str) -> Tuple[bool, str]: version (str): The version string to validate. Returns: - tuple[bool, str]: (is_valid, error_message) + Tuple[bool, str]: (is_valid, error_message) - is_valid: True if the version format is valid - error_message: Description of the issue if invalid, empty string if valid