Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test-runner.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 10 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
66 changes: 60 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,8 +1,62 @@
[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"
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 = [
"black",
"pytest",
"pytest-mypy",
"pre-commit",
"requests-mock",
"types-setuptools",
"stac-pydantic>=3.3.0",
"mypy",
"types-attrs",
"types-requests",
"types-jsonschema"
]
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"]
6 changes: 0 additions & 6 deletions requirements-dev.txt

This file was deleted.

47 changes: 0 additions & 47 deletions setup.py

This file was deleted.

67 changes: 66 additions & 1 deletion stac_validator/utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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.

Expand Down
34 changes: 31 additions & 3 deletions stac_validator/validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
link_request,
load_schema_config,
set_schema_addr,
validate_stac_version_field,
validate_with_ref_resolver,
)

Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -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)]
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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")
Expand Down
Loading