Skip to content

Commit e3a867b

Browse files
authored
Merge pull request #132 from stac-utils/universal-verbose
- Added support for --verbose flag to show verbose error messages - Updated stac-validator to v3.9.0 - Improved cli output, message formatting
2 parents 941d7cc + 70fe7df commit e3a867b

File tree

7 files changed

+174
-12
lines changed

7 files changed

+174
-12
lines changed

CHANGELOG.md

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

77
## Unreleased
88

9+
10+
## [v1.9.0] - 2025-06-13
11+
12+
### Added
13+
14+
- Added support for --verbose flag to show verbose error messages ([#132](https://github.com/stac-utils/stac-check/pull/132))
15+
16+
### Changed
17+
18+
- Updated stac-validator to v3.9.0 ([#132](https://github.com/stac-utils/stac-check/pull/132))
19+
- Improved cli output, message formatting ([#132](https://github.com/stac-utils/stac-check/pull/132))
20+
921
## [v1.8.0] - 2025-06-11
1022

1123
### Changed
@@ -240,7 +252,8 @@ The format is (loosely) based on [Keep a Changelog](http://keepachangelog.com/)
240252
- Validation from stac-validator 2.3.0
241253
- Links and assets validation checks
242254

243-
[Unreleased]: https://github.com/stac-utils/stac-check/compare/v1.8.0...main
255+
[Unreleased]: https://github.com/stac-utils/stac-check/compare/v1.9.0...main
256+
[v1.9.0]: https://github.com/stac-utils/stac-check/compare/v1.8.0...v1.9.0
244257
[v1.8.0]: https://github.com/stac-utils/stac-check/compare/v1.7.0...v1.8.0
245258
[v1.7.0]: https://github.com/stac-utils/stac-check/compare/v1.6.0...v1.7.0
246259
[v1.6.0]: https://github.com/stac-utils/stac-check/compare/v1.5.0...v1.6.0

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ Options:
109109
--header KEY VALUE HTTP header to include in the requests. Can be used
110110
multiple times.
111111
--pydantic Use stac-pydantic for enhanced validation with Pydantic models.
112+
--verbose Show verbose error messages.
112113
--help Show this message and exit.
113114
```
114115

setup.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
from setuptools import find_packages, setup
55

6-
__version__ = "1.8.0"
6+
__version__ = "1.9.0"
77

88
with open("README.md", "r") as fh:
99
long_description = fh.read()
@@ -20,7 +20,7 @@
2020
"requests>=2.32.3",
2121
"jsonschema>=4.23.0",
2222
"click>=8.1.8",
23-
"stac-validator>=3.8.1",
23+
"stac-validator~=3.9.0",
2424
"PyYAML",
2525
"python-dotenv",
2626
],
@@ -29,15 +29,15 @@
2929
"pytest",
3030
"requests-mock",
3131
"types-setuptools",
32-
"stac-validator[pydantic]",
32+
"stac-validator[pydantic]~=3.9.0",
3333
],
3434
"docs": [
3535
"sphinx>=4.0.0",
3636
"sphinx_rtd_theme>=1.0.0",
3737
"myst-parser>=0.18.0",
3838
"sphinx-autodoc-typehints>=1.18.0",
3939
],
40-
"pydantic": ["stac-validator[pydantic]"],
40+
"pydantic": ["stac-validator[pydantic]~=3.9.0"],
4141
},
4242
entry_points={"console_scripts": ["stac-check=stac_check.cli:main"]},
4343
author="Jonathan Healy",

stac_check/cli.py

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from .lint import Linter
66
from .logo import logo
7+
from .utilities import format_verbose_error
78

89

910
def link_asset_message(
@@ -88,15 +89,18 @@ def intro_message(linter: Linter) -> None:
8889
click.secho()
8990

9091
click.secho(
91-
f"Validator: stac-validator {linter.validator_version}", bg="blue", fg="white"
92+
f"\n Validator: stac-validator {linter.validator_version}",
93+
bold=True,
94+
bg="black",
95+
fg="white",
9296
)
9397

9498
# Always show validation method
9599
validation_method = (
96100
"Pydantic" if hasattr(linter, "pydantic") and linter.pydantic else "JSONSchema"
97101
)
98102
click.secho()
99-
click.secho(f"Validation method: {validation_method}", bg="yellow", fg="black")
103+
click.secho(f"\n Validation method: {validation_method}", bg="black", fg="white")
100104

101105
click.secho()
102106

@@ -137,7 +141,7 @@ def cli_message(linter: Linter) -> None:
137141
click.secho()
138142
for message in linter.best_practices_msg:
139143
if message == linter.best_practices_msg[0]:
140-
click.secho(message, bg="blue")
144+
click.secho("\n " + message, bg="blue")
141145
else:
142146
click.secho(message, fg="red")
143147

@@ -146,7 +150,7 @@ def cli_message(linter: Linter) -> None:
146150
click.secho()
147151
for message in linter.geometry_errors_msg:
148152
if message == linter.geometry_errors_msg[0]:
149-
click.secho(message, bg="yellow", fg="black")
153+
click.secho("\n " + message, bg="yellow", fg="black")
150154
else:
151155
click.secho(message, fg="red")
152156

@@ -176,14 +180,36 @@ def cli_message(linter: Linter) -> None:
176180
link_asset_message(linter.invalid_link_request, "link", "request", True)
177181

178182
if linter.error_type != "":
183+
click.secho()
184+
click.secho("\n Validation Errors: ", fg="white", bold=True, bg="black")
179185
click.secho("Validation error type: ", fg="red")
180186
click.secho(f" {linter.error_type}")
187+
click.secho()
181188

182189
if linter.error_msg != "":
183190
click.secho("Validation error message: ", fg="red")
184191
click.secho(f" {linter.error_msg}")
192+
click.secho()
193+
194+
if linter.error_msg != "" and linter.verbose_error_msg == "":
195+
click.secho("Refer to --verbose for more details.", fg="blue")
196+
click.secho()
197+
198+
if linter.verbose_error_msg:
199+
click.secho()
200+
click.secho("\n Verbose Validation Output: ", fg="white", bg="red")
185201

186-
click.secho(f"This object has {len(linter.data['links'])} links")
202+
if isinstance(linter.verbose_error_msg, dict):
203+
formatted_error = format_verbose_error(linter.verbose_error_msg)
204+
else:
205+
formatted_error = str(linter.verbose_error_msg)
206+
207+
click.secho(formatted_error)
208+
209+
click.secho()
210+
click.secho()
211+
click.secho("\n Additional Information: ", bg="green", fg="white")
212+
click.secho(f"This object has {len(linter.data['links'])} links", bold=True)
187213

188214
click.secho()
189215

@@ -225,10 +251,18 @@ def cli_message(linter: Linter) -> None:
225251
is_flag=True,
226252
help="Use pydantic validation (requires stac-pydantic to be installed).",
227253
)
254+
@click.option(
255+
"--verbose",
256+
"-v",
257+
is_flag=True,
258+
help="Enable verbose output.",
259+
)
228260
@click.command()
229261
@click.argument("file")
230262
@click.version_option(version=importlib.metadata.distribution("stac-check").version)
231-
def main(file, recursive, max_depth, assets, links, no_assets_urls, header, pydantic):
263+
def main(
264+
file, recursive, max_depth, assets, links, no_assets_urls, header, pydantic, verbose
265+
):
232266
# Check if pydantic validation is requested but not installed
233267
if pydantic:
234268
try:
@@ -250,6 +284,7 @@ def main(file, recursive, max_depth, assets, links, no_assets_urls, header, pyda
250284
assets_open_urls=not no_assets_urls,
251285
headers=dict(header),
252286
pydantic=pydantic,
287+
verbose=verbose,
253288
)
254289
intro_message(linter)
255290
if recursive > 0:

stac_check/lint.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ class Linter:
2828
assets_open_urls (bool): Whether to open assets URLs when validating assets. Defaults to True.
2929
headers (dict): HTTP headers to include in the requests.
3030
pydantic (bool, optional): A boolean value indicating whether to use pydantic validation. Defaults to False.
31+
verbose (bool, optional): A boolean value indicating whether to enable verbose output. Defaults to False.
3132
3233
Attributes:
3334
data (dict): A dictionary representing the STAC JSON file.
@@ -139,6 +140,7 @@ def check_summaries(self) -> bool:
139140
assets_open_urls: bool = True
140141
headers: Dict = field(default_factory=dict)
141142
pydantic: bool = False
143+
verbose: bool = False
142144

143145
def __post_init__(self):
144146
# Check if pydantic validation is requested but not installed
@@ -170,6 +172,7 @@ def __post_init__(self):
170172
self.valid_stac = self.message["valid_stac"]
171173
self.error_type = self.check_error_type()
172174
self.error_msg = self.check_error_message()
175+
self.verbose_error_msg = self.check_verbose_error_message()
173176
self.invalid_asset_format = (
174177
self.check_links_assets(10, "assets", "format") if self.assets else None
175178
)
@@ -291,7 +294,18 @@ def validate_file(self, file: Union[str, dict]) -> Dict[str, Any]:
291294
Raises:
292295
ValueError: If `file` is not a valid file path or STAC dictionary.
293296
"""
294-
if isinstance(file, str):
297+
if isinstance(file, str) and self.verbose:
298+
stac = StacValidate(
299+
file,
300+
links=self.links,
301+
assets=self.assets,
302+
assets_open_urls=self.assets_open_urls,
303+
headers=self.headers,
304+
pydantic=self.pydantic,
305+
verbose=self.verbose,
306+
)
307+
stac.run()
308+
elif isinstance(file, str):
295309
stac = StacValidate(
296310
file,
297311
links=self.links,
@@ -411,6 +425,17 @@ def check_error_message(self) -> str:
411425
else:
412426
return ""
413427

428+
def check_verbose_error_message(self) -> str:
429+
"""Checks whether the `message` attribute contains an `verbose_error_message` field.
430+
431+
Returns:
432+
A string containing the value of the `verbose_error_message` field, or an empty string if the field is not present.
433+
"""
434+
if "error_verbose" in self.message:
435+
return self.message["error_verbose"]
436+
else:
437+
return ""
438+
414439
def check_summaries(self) -> bool:
415440
"""Check if a Collection asset has a "summaries" property.
416441
@@ -1047,6 +1072,7 @@ def create_best_practices_msg(self) -> List[str]:
10471072
"geometry_coordinates_definite_errors",
10481073
"check_bbox_antimeridian",
10491074
"check_bbox_geometry_match",
1075+
"bbox_geometry_mismatch",
10501076
]
10511077
filtered_dict = {
10521078
k: v for k, v in best_practices_dict.items() if k not in geometry_keys
@@ -1087,6 +1113,7 @@ def create_geometry_errors_msg(self) -> List[str]:
10871113
"geometry_coordinates_definite_errors",
10881114
"check_bbox_antimeridian",
10891115
"check_bbox_geometry_match",
1116+
"bbox_geometry_mismatch",
10901117
]
10911118
geometry_dict = {
10921119
k: v for k, v in best_practices_dict.items() if k in geometry_keys

stac_check/utilities.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
def format_verbose_error(error_data):
2+
"""Format verbose error data into a human-readable string."""
3+
if not error_data or not isinstance(error_data, dict):
4+
return str(error_data)
5+
6+
output = []
7+
8+
# Handle validator type
9+
if "validator" in error_data:
10+
output.append(f"Validator: {error_data['validator']}")
11+
12+
# Handle schema information if available
13+
if "schema" in error_data and error_data["schema"]:
14+
output.append("\nSchema Information:")
15+
if isinstance(error_data["schema"], list):
16+
for schema in error_data["schema"]:
17+
if isinstance(schema, dict):
18+
if "$comment" in schema:
19+
output.append(f"- {schema['$comment']}")
20+
if "required" in schema:
21+
output.append(
22+
f" Required fields: {', '.join(schema['required'])}"
23+
)
24+
# Handle nested schema requirements
25+
if "properties" in schema and "properties" in schema.get(
26+
"properties", {}
27+
):
28+
props = schema["properties"]["properties"]
29+
if "allOf" in props:
30+
for item in props["allOf"]:
31+
if "anyOf" in item:
32+
for req in item["anyOf"]:
33+
if "required" in req:
34+
output.append(
35+
f" One of these fields is required: {', '.join(req['required'])}"
36+
)
37+
38+
# Handle path information if available
39+
if "path_in_schema" in error_data and error_data["path_in_schema"]:
40+
output.append(
41+
f"\nError Path: {' -> '.join(str(p) for p in error_data['path_in_schema'])}"
42+
)
43+
44+
# Handle any other fields we haven't specifically formatted
45+
other_fields = set(error_data.keys()) - {
46+
"validator",
47+
"schema",
48+
"path_in_schema",
49+
"path_in_document",
50+
}
51+
for field in other_fields:
52+
if isinstance(error_data[field], (str, int, float, bool)):
53+
output.append(f"\n{field.replace('_', ' ').title()}: {error_data[field]}")
54+
55+
return "\n".join(output)

tests/test_lint.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -980,3 +980,34 @@ def test_pydantic_fallback_without_import(monkeypatch):
980980
assert linter.valid_stac is True
981981
assert linter.asset_type == "ITEM"
982982
assert linter.message["validation_method"] == "default"
983+
984+
985+
def test_verbose_error_message():
986+
"""Test that verbose error messages are properly formatted and included."""
987+
# Test with a known bad item that will generate validation errors
988+
file = "sample_files/1.0.0/bad-item.json"
989+
linter = Linter(file, verbose=True)
990+
991+
# Verify the item is invalid
992+
assert linter.valid_stac is False
993+
994+
# Check that we have the expected error message
995+
assert "id" in linter.error_msg.lower()
996+
assert "required" in linter.error_msg.lower()
997+
998+
# Check that verbose error message contains expected structure
999+
assert isinstance(linter.verbose_error_msg, dict)
1000+
assert "validator" in linter.verbose_error_msg
1001+
assert "path_in_schema" in linter.verbose_error_msg
1002+
1003+
# Check specific parts of the verbose error message
1004+
assert linter.verbose_error_msg.get("validator") == "required"
1005+
1006+
# Check path_in_schema - it might contain both strings and integers
1007+
path_in_schema = linter.verbose_error_msg.get("path_in_schema", [])
1008+
assert any(isinstance(p, (str, int)) for p in path_in_schema)
1009+
1010+
# Check that the error message is included in the string representation
1011+
verbose_str = str(linter.verbose_error_msg)
1012+
assert "required" in verbose_str
1013+
assert "id" in verbose_str

0 commit comments

Comments
 (0)