Skip to content

Commit 5b922d8

Browse files
authored
Check stac version format, v3.10.1, update to pyproject.toml (#268)
* check stac version format * v3.10.2, update to pyproject
1 parent 19df38f commit 5b922d8

File tree

9 files changed

+272
-67
lines changed

9 files changed

+272
-67
lines changed

.github/workflows/publish.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,11 @@ jobs:
2121
- name: Install dependencies
2222
run: |
2323
python -m pip install --upgrade pip
24-
pip install setuptools wheel twine
24+
pip install setuptools wheel twine build
2525
2626
- name: Build package
2727
run: |
28-
python setup.py sdist bdist_wheel
28+
python -m build
2929
3030
- name: Publish package to PyPI
3131
env:

.github/workflows/test-runner.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ jobs:
2929
if: matrix.python-version == '3.12'
3030
run: |
3131
pip install .
32-
pip install -r requirements-dev.txt
32+
pip install -e .[dev]
3333
mypy stac_validator/
3434
3535
- name: Run pre-commit

CHANGELOG.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@ The format is (loosely) based on [Keep a Changelog](http://keepachangelog.com/)
66

77
## [Unreleased]
88

9+
## [v3.10.2] - 2025-11-16
10+
11+
### Fixed
12+
- 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)
13+
14+
### Changed
15+
- 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)
16+
917
## [v3.10.1] - 2025-07-26
1018

1119
### Fixed
@@ -302,7 +310,8 @@ The format is (loosely) based on [Keep a Changelog](http://keepachangelog.com/)
302310
- 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.
303311
- Logging. Various additions were made here depending on the options selected. This was done to help assist people to update their STAC collections.
304312

305-
[Unreleased]: https://github.com/sparkgeo/stac-validator/compare/v3.10.1..main
313+
[Unreleased]: https://github.com/sparkgeo/stac-validator/compare/v3.10.2..main
314+
[v3.10.2]: https://github.com/sparkgeo/stac-validator/compare/v3.10.1..v3.10.2
306315
[v3.10.1]: https://github.com/sparkgeo/stac-validator/compare/v3.10.0..v3.10.1
307316
[v3.10.0]: https://github.com/sparkgeo/stac-validator/compare/v3.9.3..v3.10.0
308317
[v3.9.3]: https://github.com/sparkgeo/stac-validator/compare/v3.9.2..v3.9.3

pyproject.toml

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,62 @@
11
[build-system]
2-
requires = [
3-
"requests",
4-
"jsonschema",
5-
"click",
6-
"setuptools"
2+
requires = ["setuptools>=61.0", "wheel"]
3+
build-backend = "setuptools.build_meta"
4+
5+
[project]
6+
name = "stac_validator"
7+
version = "3.10.2"
8+
description = "A package to validate STAC files"
9+
authors = [
10+
{name = "James Banting"},
11+
{name = "Jonathan Healy", email = "[email protected]"}
712
]
8-
build-backend = "setuptools.build_meta"
13+
license = {text = "Apache-2.0"}
14+
classifiers = [
15+
"Intended Audience :: Information Technology",
16+
"Intended Audience :: Science/Research",
17+
"License :: OSI Approved :: Apache Software License",
18+
"Programming Language :: Python :: 3.8",
19+
"Topic :: Scientific/Engineering :: GIS",
20+
]
21+
keywords = ["STAC", "validation", "raster"]
22+
requires-python = ">=3.8"
23+
dependencies = [
24+
"requests>=2.32.3",
25+
"jsonschema>=4.23.0",
26+
"click>=8.1.8",
27+
"referencing>=0.35.1",
28+
"pyYAML>=6.0.1",
29+
]
30+
optional-dependencies.dev = [
31+
"black",
32+
"pytest",
33+
"pytest-mypy",
34+
"pre-commit",
35+
"requests-mock",
36+
"types-setuptools",
37+
"stac-pydantic>=3.3.0",
38+
"mypy",
39+
"types-attrs",
40+
"types-requests",
41+
"types-jsonschema"
42+
]
43+
optional-dependencies.pydantic = [
44+
"stac-pydantic>=3.3.0"
45+
]
46+
47+
[project.urls]
48+
Homepage = "https://github.com/stac-utils/stac-validator"
49+
Repository = "https://github.com/stac-utils/stac-validator"
50+
51+
[project.readme]
52+
file = "README.md"
53+
content-type = "text/markdown"
54+
55+
[tool.setuptools]
56+
packages = ["stac_validator"]
57+
58+
[project.scripts]
59+
stac-validator = "stac_validator.stac_validator:main"
60+
61+
[tool.setuptools.package-data]
62+
stac_validator = ["*.yaml"]

requirements-dev.txt

Lines changed: 0 additions & 6 deletions
This file was deleted.

setup.py

Lines changed: 0 additions & 47 deletions
This file was deleted.

stac_validator/utilities.py

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import json
33
import os
44
import ssl
5-
from typing import Dict, Optional
5+
from typing import Dict, Optional, Tuple
66
from urllib.parse import urlparse
77
from urllib.request import Request, urlopen
88

@@ -25,6 +25,71 @@
2525
]
2626

2727

28+
def validate_stac_version_field(stac_content: Dict) -> Tuple[bool, str, str]:
29+
"""Validate the stac_version field in STAC content.
30+
31+
Args:
32+
stac_content (dict): The STAC content dictionary.
33+
34+
Returns:
35+
Tuple[bool, str, str]: (is_valid, error_type, error_message)
36+
- is_valid: True if the version is valid
37+
- error_type: Error type string if invalid, empty string if valid
38+
- error_message: Error message if invalid, empty string if valid
39+
"""
40+
version = stac_content.get("stac_version", "")
41+
42+
# Check if version is present and not empty
43+
if not version or not isinstance(version, str) or version.strip() == "":
44+
error_type = "MissingSTACVersion"
45+
error_msg = (
46+
"The 'stac_version' field is missing or empty. "
47+
"Please ensure your STAC object includes a valid 'stac_version' field "
48+
"(e.g., '1.0.0', '1.1.0'). This field is required for proper schema validation."
49+
)
50+
return False, error_type, error_msg
51+
52+
# Validate version format
53+
format_valid, format_error = validate_version_format(version)
54+
if not format_valid:
55+
return False, "InvalidSTACVersionFormat", format_error
56+
57+
return True, "", ""
58+
59+
60+
def validate_version_format(version: str) -> Tuple[bool, str]:
61+
"""Validate that a STAC version string has the correct format.
62+
63+
Args:
64+
version (str): The version string to validate.
65+
66+
Returns:
67+
Tuple[bool, str]: (is_valid, error_message)
68+
- is_valid: True if the version format is valid
69+
- error_message: Description of the issue if invalid, empty string if valid
70+
71+
Valid formats:
72+
- Standard semver: "1.0.0", "1.1.0", "0.9.0"
73+
- Pre-release versions: "1.0.0-beta.1", "1.0.0-rc.1"
74+
"""
75+
if not version:
76+
return False, "Version is empty"
77+
78+
import re
79+
80+
# Regex for semantic versioning: major.minor.patch with optional pre-release
81+
semver_pattern = r"^\d+\.\d+\.\d+(-[\w\.\-]+)?$"
82+
83+
if not re.match(semver_pattern, version):
84+
return False, (
85+
f"Version '{version}' does not match expected format. "
86+
"STAC versions should be in semantic versioning format (e.g., '1.0.0', '1.1.0', '1.0.0-beta.1'). "
87+
"Please check your 'stac_version' field."
88+
)
89+
90+
return True, ""
91+
92+
2893
def is_url(url: str) -> bool:
2994
"""Checks whether the input string is a valid URL.
3095

stac_validator/validate.py

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
link_request,
1919
load_schema_config,
2020
set_schema_addr,
21+
validate_stac_version_field,
2122
validate_with_ref_resolver,
2223
)
2324

@@ -247,6 +248,7 @@ def create_err_msg(
247248
err_msg: str,
248249
error_obj: Optional[Exception] = None,
249250
schema_uri: str = "",
251+
version: Optional[str] = None,
250252
) -> Dict[str, Union[str, bool, List[str], Dict[str, Any]]]:
251253
"""
252254
Create a standardized error message dictionary and mark validation as failed.
@@ -256,6 +258,7 @@ def create_err_msg(
256258
err_msg (str): The error message.
257259
error_obj (Optional[Exception]): The raw exception object for verbose details.
258260
schema_uri (str, optional): The URI of the schema that failed validation.
261+
version (Optional[str]): Override version to use in the error message.
259262
260263
Returns:
261264
dict: Dictionary containing error information.
@@ -268,9 +271,16 @@ def create_err_msg(
268271
if not isinstance(err_msg, str):
269272
err_msg = str(err_msg)
270273

274+
# Use provided version or fall back to self.version
275+
version_to_use = (
276+
version
277+
if version is not None
278+
else (str(self.version) if hasattr(self, "version") else "")
279+
)
280+
271281
# Initialize the message with common fields
272282
message: Dict[str, Any] = {
273-
"version": str(self.version) if hasattr(self, "version") else "",
283+
"version": version_to_use,
274284
"path": str(self.stac_file) if hasattr(self, "stac_file") else "",
275285
"schema": (
276286
[self._original_schema_paths.get(self.schema, self.schema)]
@@ -306,7 +316,7 @@ def create_err_msg(
306316

307317
# Initialize the error message with common fields
308318
error_message: Dict[str, Union[str, bool, List[str], Dict[str, Any]]] = {
309-
"version": str(self.version) if self.version is not None else "",
319+
"version": version_to_use,
310320
"path": str(self.stac_file) if self.stac_file is not None else "",
311321
"schema": schema_field, # All schemas that were checked
312322
"valid_stac": False,
@@ -950,7 +960,25 @@ def run(self) -> bool:
950960
self.stac_content = fetch_and_parse_file(self.stac_file, self.headers)
951961

952962
stac_type = get_stac_type(self.stac_content).upper()
953-
self.version = self.stac_content["stac_version"]
963+
version = self.stac_content.get("stac_version", "")
964+
965+
# Validate stac_version field comprehensively
966+
version_valid, version_error_type, version_error_msg = (
967+
validate_stac_version_field(self.stac_content)
968+
)
969+
if not version_valid:
970+
message.update(
971+
self.create_err_msg(
972+
err_type=version_error_type,
973+
err_msg=version_error_msg,
974+
schema_uri="",
975+
version=version, # Pass the version we extracted
976+
)
977+
)
978+
self.message.append(message)
979+
return self.valid
980+
981+
self.version = version
954982

955983
if self.core:
956984
message = self.create_message(stac_type, "core")

0 commit comments

Comments
 (0)