diff --git a/CHANGELOG.md b/CHANGELOG.md index 65b9d92..6425f95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,18 @@ The format is (loosely) based on [Keep a Changelog](http://keepachangelog.com/) ## Unreleased +## [v1.9.1] - 2025-06-16 + +### Added + +- Added display of failed schema information in the validation output ([#134](https://github.com/stac-utils/stac-check/pull/134)) +- Added recommendation messages to guide users when validation fails ([#134](https://github.com/stac-utils/stac-check/pull/134)) +- Added disclaimer about schema-based STAC validation being an initial indicator of validity only ([#134](https://github.com/stac-utils/stac-check/pull/134)) + +### Changed + +- Updated validation output to show "Passed" instead of "Valid" for accuracy ([#134](https://github.com/stac-utils/stac-check/pull/134)) + ## [v1.9.0] - 2025-06-13 @@ -252,7 +264,8 @@ 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.9.0...main +[Unreleased]: https://github.com/stac-utils/stac-check/compare/v1.9.1...main +[v1.9.1]: https://github.com/stac-utils/stac-check/compare/v1.9.0...v1.9.1 [v1.9.0]: https://github.com/stac-utils/stac-check/compare/v1.8.0...v1.9.0 [v1.8.0]: https://github.com/stac-utils/stac-check/compare/v1.7.0...v1.8.0 [v1.7.0]: https://github.com/stac-utils/stac-check/compare/v1.6.0...v1.7.0 diff --git a/sample_files/1.1.0/collection.json b/sample_files/1.1.0/collection.json new file mode 100644 index 0000000..5c42d18 --- /dev/null +++ b/sample_files/1.1.0/collection.json @@ -0,0 +1,112 @@ +{ + "id": "simple-collection", + "type": "Collection", + "stac_extensions": [ + "https://stac-extensions.github.io/eo/v2.0.0/schema.json", + "https://stac-extensions.github.io/projection/v2.0.0/schema.json", + "https://stac-extensions.github.io/view/v1.0.0/schema.json" + ], + "stac_version": "1.1.0", + "description": "A simple collection demonstrating core catalog fields with links to a couple of items", + "title": "Simple Example Collection", + "keywords": ["simple", "example", "collection"], + "providers": [ + { + "name": "Remote Data, Inc", + "description": "Producers of awesome spatiotemporal assets", + "roles": ["producer", "processor"], + "url": "http://remotedata.io" + } + ], + "extent": { + "spatial": { + "bbox": [ + [ + 172.91173669923782, 1.3438851951615003, 172.95469614953714, + 1.3690476620161975 + ] + ] + }, + "temporal": { + "interval": [["2020-12-11T22:38:32.125Z", "2020-12-14T18:02:31.437Z"]] + } + }, + "license": "CC-BY-4.0", + "summaries": { + "platform": ["cool_sat1", "cool_sat2"], + "constellation": ["ion"], + "instruments": ["cool_sensor_v1", "cool_sensor_v2"], + "gsd": { + "minimum": 0.512, + "maximum": 0.66 + }, + "eo:cloud_cover": { + "minimum": 1.2, + "maximum": 1.2 + }, + "proj:cpde": ["EPSG:32659"], + "view:sun_elevation": { + "minimum": 54.9, + "maximum": 54.9 + }, + "view:off_nadir": { + "minimum": 3.8, + "maximum": 3.8 + }, + "view:sun_azimuth": { + "minimum": 135.7, + "maximum": 135.7 + }, + "statistics": { + "type": "object", + "properties": { + "vegetation": { + "description": "Percentage of pixels that are detected as vegetation, e.g. forests, grasslands, etc.", + "minimum": 0, + "maximum": 100 + }, + "water": { + "description": "Percentage of pixels that are detected as water, e.g. rivers, oceans and ponds.", + "minimum": 0, + "maximum": 100 + }, + "urban": { + "description": "Percentage of pixels that detected as urban, e.g. roads and buildings.", + "minimum": 0, + "maximum": 100 + } + } + } + }, + "links": [ + { + "rel": "root", + "href": "./collection.json", + "type": "application/json", + "title": "Simple Example Collection" + }, + { + "rel": "item", + "href": "./simple-item.json", + "type": "application/geo+json", + "title": "Simple Item" + }, + { + "rel": "item", + "href": "./core-item.json", + "type": "application/geo+json", + "title": "Core Item" + }, + { + "rel": "item", + "href": "./extended-item.json", + "type": "application/geo+json", + "title": "Extended Item" + }, + { + "rel": "self", + "href": "https://raw.githubusercontent.com/radiantearth/stac-spec/v1.1.0/examples/collection.json", + "type": "application/json" + } + ] +} diff --git a/sample_files/1.1.0/extended-item.json b/sample_files/1.1.0/extended-item.json new file mode 100644 index 0000000..a682842 --- /dev/null +++ b/sample_files/1.1.0/extended-item.json @@ -0,0 +1,210 @@ +{ + "stac_version": "1.1.0", + "stac_extensions": [ + "https://stac-extensions.github.io/eo/v2.0.0/schema.json", + "https://stac-extensions.github.io/projection/v2.0.0/schema.json", + "https://stac-extensions.github.io/scientific/v1.0.0/schema.json", + "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, + 1.3438851951615003, + 172.95469614953714, + 1.3690476620161975 + ], + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 172.91173669923782, + 1.3438851951615003 + ], + [ + 172.95469614953714, + 1.3438851951615003 + ], + [ + 172.95469614953714, + 1.3690476620161975 + ], + [ + 172.91173669923782, + 1.3690476620161975 + ], + [ + 172.91173669923782, + 1.3438851951615003 + ] + ] + ] + }, + "properties": { + "title": "Extended Item", + "description": "A sample STAC Item that includes a variety of examples from the stable extensions", + "keywords": [ + "extended", + "example", + "item" + ], + "datetime": "2020-12-14T18:02:31.437000Z", + "created": "2020-12-15T01:48:13.725Z", + "updated": "2020-12-15T01:48:13.725Z", + "platform": "cool_sat2", + "instruments": [ + "cool_sensor_v2" + ], + "gsd": 0.66, + "eo:cloud_cover": 1.2, + "eo:snow_cover": 0, + "statistics": { + "vegetation": 12.57, + "water": 1.23, + "urban": 26.2 + }, + "proj:code": "EPSG:32659", + "proj:shape": [ + 5558, + 9559 + ], + "proj:transform": [ + 0.5, + 0, + 712710, + 0, + -0.5, + 151406, + 0, + 0, + 1 + ], + "view:sun_elevation": 54.9, + "view:off_nadir": 3.8, + "view:sun_azimuth": 135.7, + "rd:type": "scene", + "rd:anomalous_pixels": 0.14, + "rd:earth_sun_distance": 1.014156, + "rd:sat_id": "cool_sat2", + "rd:product_level": "LV3A", + "sci:doi": "10.5061/dryad.s2v81.2/27.2" + }, + "collection": "simple-collection", + "links": [ + { + "rel": "collection", + "href": "./collection.json", + "type": "application/json", + "title": "Simple Example Collection" + }, + { + "rel": "root", + "href": "./collection.json", + "type": "application/json", + "title": "Simple Example Collection" + }, + { + "rel": "parent", + "href": "./collection.json", + "type": "application/json", + "title": "Simple Example Collection" + }, + { + "rel": "alternate", + "type": "text/html", + "href": "http://remotedata.io/catalog/20201211_223832_CS2/index.html", + "title": "HTML version of this STAC Item" + } + ], + "assets": { + "analytic": { + "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2_analytic.tif", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "4-Band Analytic", + "roles": [ + "data" + ], + "bands": [ + { + "name": "band1", + "eo:common_name": "blue", + "eo:center_wavelength": 0.47, + "eo:full_width_half_max": 70 + }, + { + "name": "band2", + "eo:common_name": "green", + "eo:center_wavelength": 0.56, + "eo:full_width_half_max": 80 + }, + { + "name": "band3", + "eo:common_name": "red", + "eo:center_wavelength": 0.645, + "eo:full_width_half_max": 90 + }, + { + "name": "band4", + "eo:common_name": "nir", + "eo:center_wavelength": 0.8, + "eo:full_width_half_max": 152 + } + ] + }, + "thumbnail": { + "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.jpg", + "title": "Thumbnail", + "type": "image/png", + "roles": [ + "thumbnail" + ] + }, + "visual": { + "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.tif", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "3-Band Visual", + "roles": [ + "visual" + ], + "bands": [ + { + "name": "band3", + "eo:common_name": "red", + "eo:center_wavelength": 0.645, + "eo:full_width_half_max": 90 + }, + { + "name": "band2", + "eo:common_name": "green", + "eo:center_wavelength": 0.56, + "eo:full_width_half_max": 80 + }, + { + "name": "band1", + "eo:common_name": "blue", + "eo:center_wavelength": 0.47, + "eo:full_width_half_max": 70 + } + ] + }, + "udm": { + "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2_analytic_udm.tif", + "title": "Unusable Data Mask", + "type": "image/tiff; application=geotiff" + }, + "json-metadata": { + "href": "http://remotedata.io/catalog/20201211_223832_CS2/extended-metadata.json", + "title": "Extended Metadata", + "type": "application/json", + "roles": [ + "metadata" + ] + }, + "ephemeris": { + "href": "http://cool-sat.com/catalog/20201211_223832_CS2/20201211_223832_CS2.EPH", + "title": "Satellite Ephemeris Metadata" + } + } + } \ No newline at end of file diff --git a/sample_files/1.1.0/simple-item.json b/sample_files/1.1.0/simple-item.json new file mode 100644 index 0000000..df84754 --- /dev/null +++ b/sample_files/1.1.0/simple-item.json @@ -0,0 +1,60 @@ +{ + "stac_version": "1.1.0", + "stac_extensions": [], + "type": "Feature", + "id": "20201211_223832_CS2", + "bbox": [ + 172.91173669923782, 1.3438851951615003, 172.95469614953714, + 1.3690476620161975 + ], + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [172.91173669923782, 1.3438851951615003], + [172.95469614953714, 1.3438851951615003], + [172.95469614953714, 1.3690476620161975], + [172.91173669923782, 1.3690476620161975], + [172.91173669923782, 1.3438851951615003] + ] + ] + }, + "properties": { + "datetime": "2020-12-11T22:38:32.125000Z" + }, + "collection": "simple-collection", + "links": [ + { + "rel": "collection", + "href": "./collection.json", + "type": "application/json", + "title": "Simple Example Collection" + }, + { + "rel": "root", + "href": "./collection.json", + "type": "application/json", + "title": "Simple Example Collection" + }, + { + "rel": "parent", + "href": "./collection.json", + "type": "application/json", + "title": "Simple Example Collection" + } + ], + "assets": { + "visual": { + "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.tif", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "3-Band Visual", + "roles": ["visual"] + }, + "thumbnail": { + "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.jpg", + "title": "Thumbnail", + "type": "image/jpeg", + "roles": ["thumbnail"] + } + } +} diff --git a/sample_files/1.1.0/test-sar-item-invalid.json b/sample_files/1.1.0/test-sar-item-invalid.json new file mode 100644 index 0000000..9863e8d --- /dev/null +++ b/sample_files/1.1.0/test-sar-item-invalid.json @@ -0,0 +1,415 @@ + +{ + "type": "Feature", + "stac_version": "1.1.0", + "stac_extensions": [ + "https://stac-extensions.github.io/product/v0.1.0/schema.json", + "https://stac-extensions.github.io/sar/v1.1.0/schema.json", + "https://stac-extensions.github.io/altimetry/v0.1.0/schema.json", + "https://stac-extensions.github.io/projection/v2.0.0/schema.json", + "https://stac-extensions.github.io/sat/v1.1.0/schema.json", + "https://stac-extensions.github.io/sentinel-1/v0.2.0/schema.json", + "https://stac-extensions.github.io/processing/v1.2.0/schema.json", + "https://stac-extensions.github.io/storage/v2.0.0/schema.json", + "https://stac-extensions.github.io/ceos-ard/v0.2.0/schema.json" + ], + "id": "OPERA_L2_RTC-S1_T070-149822-IW3_20220101T124811Z_20250611T234746Z_S1A_20_v0.1", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 164.5738105155539, + -77.09744736586842 + ], + [ + 164.5841107737331, + -77.06092164981267 + ], + [ + 164.5943055896268, + -77.02456861667501 + ], + [ + 164.6043976249054, + -76.98838437391879 + ], + [ + 164.6143886247731, + -76.95236515068486 + ], + [ + 164.6242810255355, + -76.91650731426346 + ], + [ + 164.6340769950861, + -76.88080735064491 + ], + [ + 164.6437788191498, + -76.84526186443392 + ], + [ + 164.6533879540521, + -76.80986756023115 + ], + [ + 164.6629066156633, + -76.77462126015926 + ], + [ + 164.6723366189091, + -76.73951988279993 + ], + [ + 164.6816798029899, + -76.70456044480147 + ], + [ + 164.6909381371846, + -76.66974005821108 + ], + [ + 164.7001127849088, + -76.63505591330626 + ], + [ + 164.7092056716351, + -76.60050529702578 + ], + [ + 164.7182183385663, + -76.56608557323291 + ], + [ + 164.7271523618029, + -76.53179418528892 + ], + [ + 164.7360094562833, + -76.49762865434306 + ], + [ + 164.7447905508336, + -76.46358656324253 + ], + [ + 164.7534973361782, + -76.42966557554502 + ], + [ + 164.7620626941835, + -76.39613193601812 + ], + [ + 163.9577752171824, + -76.3834733821606 + ], + [ + 163.9472896009472, + -76.41697662475501 + ], + [ + 163.9366312297678, + -76.45086671776188 + ], + [ + 163.9258819911961, + -76.48487764991665 + ], + [ + 163.9150401310552, + -76.51901175603307 + ], + [ + 163.9041039255227, + -76.55327144154155 + ], + [ + 163.8930712657604, + -76.58765917815546 + ], + [ + 163.8819407934205, + -76.62217752659461 + ], + [ + 163.8707103472533, + -76.65682911282163 + ], + [ + 163.8593778902213, + -76.69161664748461 + ], + [ + 163.8479411325865, + -76.7265429229195 + ], + [ + 163.8363984494731, + -76.7616108328091 + ], + [ + 163.8247474153511, + -76.79682335070363 + ], + [ + 163.8129857401177, + -76.83218355043469 + ], + [ + 163.8011110570616, + -76.86769460684839 + ], + [ + 163.7891207073698, + -76.90335979673557 + ], + [ + 163.7770126897196, + -76.93918252025401 + ], + [ + 163.7647841747692, + -76.97516628039226 + ], + [ + 163.7524324309662, + -77.01131470459156 + ], + [ + 163.7399544370051, + -77.04763154391978 + ], + [ + 163.7273478409169, + -77.08412069598076 + ], + [ + 164.5738105155539, + -77.09744736586842 + ] + ] + ] + }, + "bbox": [ + 163.7273478409169, + -77.09744736586842, + 164.7620626941835, + -76.3834733821606 + ], + "properties": { + "gsd": 20.0, + "constellation": "Sentinel-1", + "platform": "Sentinel-1A", + "instruments": [ + "Sentinel-1A CSAR" + ], + "created": "2025-06-11T23:47:50.172887Z", + "start_datetime": "2022-01-01T12:48:11.446000Z", + "end_datetime": "2022-01-01T12:48:14.539612Z", + "odc:product": "ga_s1_iw_hh_c1", + "odc:product_family": "sar_ard", + "odc:region_code": "t070_149822_iw3", + "product:type": "RTC_S1", + "ceosard:type": "radar", + "ceosard:specification": "NRB", + "ceosard:specification_version": "1.1", + "proj:code": "EPSG:3031", + "proj:bbox": [ + 368760.0, + -1438080.0, + 416760.0, + -1347300.0 + ], + "sar:frequency_band": "C", + "sar:center_frequency": 5405000454.33435, + "sar:polarizations": [ + "HH" + ], + "sar:observation_direction": "right", + "sar:relative_burst": "t070_149822_iw3", + "sar:beam_ids": "IW3", + "altm:instrument_type": "sar", + "altm:instrument_mode": "IW", + "sat:orbit_state": "descending", + "sat:absolute_orbit": 41267, + "sat:relative_orbit": 70, + "sat:orbit_cycle": "12", + "sat:osv": [ + "S1A_OPER_AUX_POEORB_OPOD_20220121T121549_V20211231T225942_20220102T005942.EOF" + ], + "sat:orbit_state_vectors": "TODO", + "s1:orbit_source": "POE precise orbit", + "processing:level": "L2", + "processing:facility": "Geoscience Australia", + "processing:datetime": "2025-06-11T23:47:50.172887Z", + "processing:version": 0.1, + "processing:software": { + "isce3": "0.15.0", + "s1Reader": "0.2.5", + "OPERA-adt/RTC": "1.0.4", + "sar-pipeline": "0.2.2b1.dev26+g043d201.d20250613", + "dem-handler": "0.2.2" + }, + "sarard:source_id": [ + "S1A_IW_SLC__1SSH_20220101T124744_20220101T124814_041267_04E7A2_1DAD.SAFE" + ], + "sarard:scene_id": "S1A_IW_SLC__1SSH_20220101T124744_20220101T124814_041267_04E7A2_1DAD", + "sarard:pixel_spacing_x": 20.0, + "sarard:pixel_spacing_y": 20.0, + "sarard:resolution_x": 20.0, + "sarard:resolution_y": 20.0, + "sarard:speckle_filter_applied": false, + "sarard:speckle_filter_type": "", + "sarard:speckle_filter_window": [], + "sarard:measurement_type": "gamma0", + "sarard:measurement_convention": "linear backscatter intensity", + "sarard:conversion_eq": "10*log10(backscatter_linear)", + "sarard:noise_removal_applied": true, + "sarard:static_tropospheric_correction_applied": true, + "sarard:wet_tropospheric_correction_applied": false, + "sarard:bistatic_correction_applied": true, + "sarard:ionospheric_correction_applied": false, + "sarard:geometric_accuracy_ALE": "TODO", + "sarard:geometric_accuracy_rmse": "TODO", + "sarard:geometric_accuracy_range": "TODO", + "sarard:geometric_accuracy_azimuth": "TODO", + "storage:schemes": { + "aws-std": { + "type": "aws-s3", + "platform": "https://{bucket}.s3.{region}.amazonaws.com", + "bucket": "deant-data-public-dev", + "region": "ap-southeast-2", + "requester_pays": true + } + }, + "datetime": "2022-01-01T12:48:11.446000Z" + }, + "links": [ + { + "rel": "ceos-ard-specification", + "href": "https://ceos.org/ard/files/PFS/SAR/v1.1/CEOS-ARD_PFS_Synthetic_Aperture_Radar_v1.1.pdf", + "type": "application/pdf" + }, + { + "rel": "geoid-source", + "href": "https://aria-geoid.s3.us-west-2.amazonaws.com/us_nga_egm2008_1_4326__agisoft.tif" + }, + { + "rel": "derived_from", + "href": "https://datapool.asf.alaska.edu/SLC/SA/S1A_IW_SLC__1SSH_20220101T124744_20220101T124814_041267_04E7A2_1DAD.zip" + }, + { + "rel": "dem-source", + "href": "https://registry.opendata.aws/copernicus-dem/" + }, + { + "rel": "rtc-algorithm", + "href": "https://doi.org/10.1109/TGRS.2022.3147472" + }, + { + "rel": "geocoding-algorithm", + "href": "https://doi.org/10.1109/TGRS.2022.3147472" + }, + { + "rel": "noise-correction", + "href": "https://sentinels.copernicus.eu/documents/247904/2142675/Thermal-Denoising-of-Products-Generated-by-Sentinel-1-IPF.pdf" + }, + { + "rel": "additional-metadata", + "href": "https://deant-data-public-dev.s3.ap-southeast-2.amazonaws.com/experimental/s1_rtc_c1/t070_149822_iw3/2022/1/1/OPERA_L2_RTC-S1_T070-149822-IW3_20220101T124811Z_20250611T234746Z_S1A_20_v0.1.h5" + }, + { + "rel": "self", + "href": "https://deant-data-public-dev.s3.ap-southeast-2.amazonaws.com/experimental/s1_rtc_c1/t070_149822_iw3/2022/1/1/metadata.json", + "type": "application/json" + }, + { + "rel": "collection", + "href": "./collection.json", + "type": "application/json" + } + ], + "assets": { + "HH": { + "href": "https://deant-data-public-dev.s3.ap-southeast-2.amazonaws.com/experimental/s1_rtc_c1/t070_149822_iw3/2022/1/1/OPERA_L2_RTC-S1_T070-149822-IW3_20220101T124811Z_20250611T234746Z_S1A_20_v0.1_HH.tif", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "HH", + "description": "HH polarised backscatter", + "proj:shape": [ + 4539, + 2400 + ], + "proj:transform": [ + 20.0, + 0.0, + 368760.0, + 0.0, + -20.0, + -1347300.0, + 0.0, + 0.0, + 1.0 + ], + "proj:code": "EPSG:3031", + "raster:data_type": "float32", + "raster:sampling": "Area", + "raster:nodata": "nan", + "processing:level": "L2", + "roles": [ + "data", + "backscatter" + ] + }, + "mask": { + "href": "https://deant-data-public-dev.s3.ap-southeast-2.amazonaws.com/experimental/s1_rtc_c1/t070_149822_iw3/2022/1/1/OPERA_L2_RTC-S1_T070-149822-IW3_20220101T124811Z_20250611T234746Z_S1A_20_v0.1_mask.tif", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "mask", + "description": "shadow layover data mask", + "proj:shape": [ + 4539, + 2400 + ], + "proj:transform": [ + 20.0, + 0.0, + 368760.0, + 0.0, + -20.0, + -1347300.0, + 0.0, + 0.0, + 1.0 + ], + "proj:code": "EPSG:3031", + "raster:data_type": "uint8", + "raster:sampling": "Area", + "raster:nodata": 255.0, + "raster:values": { + "shadow": 1, + "layover": 2, + "shadow_and_layover": 3, + "invalid_sample": 255 + }, + "roles": [ + "data", + "auxiliary", + "mask", + "shadow", + "layover" + ] + }, + "thumbnail": { + "href": "https://deant-data-public-dev.s3.ap-southeast-2.amazonaws.com/experimental/s1_rtc_c1/t070_149822_iw3/2022/1/1/OPERA_L2_RTC-S1_T070-149822-IW3_20220101T124811Z_20250611T234746Z_S1A_20_v0.1.png", + "type": "image/png", + "title": "thumbnail", + "description": "thumbnail image for backscatter", + "roles": [ + "thumbnail" + ] + } + }, + "collection": "s1_rtc_c1" +} diff --git a/setup.py b/setup.py index fd02b52..da3bcf1 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ from setuptools import find_packages, setup -__version__ = "1.9.0" +__version__ = "1.9.1" with open("README.md", "r") as fh: long_description = fh.read() @@ -20,7 +20,7 @@ "requests>=2.32.3", "jsonschema>=4.23.0", "click>=8.1.8", - "stac-validator~=3.9.0", + "stac-validator~=3.9.1", "PyYAML", "python-dotenv", ], @@ -29,7 +29,7 @@ "pytest", "requests-mock", "types-setuptools", - "stac-validator[pydantic]~=3.9.0", + "stac-validator[pydantic]~=3.9.1", ], "docs": [ "sphinx>=4.0.0", @@ -37,7 +37,7 @@ "myst-parser>=0.18.0", "sphinx-autodoc-typehints>=1.18.0", ], - "pydantic": ["stac-validator[pydantic]~=3.9.0"], + "pydantic": ["stac-validator[pydantic]~=3.9.1"], }, entry_points={"console_scripts": ["stac-check=stac_check.cli:main"]}, author="Jonathan Healy", diff --git a/stac_check/cli.py b/stac_check/cli.py index db977c6..f2c6dec 100644 --- a/stac_check/cli.py +++ b/stac_check/cli.py @@ -52,7 +52,7 @@ def recursive_message(linter: Linter) -> None: cli_message(recursive_linter) else: click.secho(f"Valid: {msg['valid_stac']}", fg="red") - click.secho("Schemas validated: ", fg="blue") + click.secho("Schemas checked: ", fg="blue") for schema in msg["schema"]: click.secho(f" {schema}") click.secho(f"Error Type: {msg['error_type']}", fg="red") @@ -75,7 +75,7 @@ def intro_message(linter: Linter) -> None: Returns: None. """ - click.secho(logo) + click.secho(logo, bold=True, fg="bright_black") click.secho("stac-check: STAC spec validation and linting tool", bold=True) @@ -86,8 +86,6 @@ def intro_message(linter: Linter) -> None: else: click.secho(linter.set_update_message(), fg="red") - click.secho() - click.secho( f"\n Validator: stac-validator {linter.validator_version}", bold=True, @@ -99,8 +97,8 @@ def intro_message(linter: Linter) -> None: validation_method = ( "Pydantic" if hasattr(linter, "pydantic") and linter.pydantic else "JSONSchema" ) - click.secho() - click.secho(f"\n Validation method: {validation_method}", bg="black", fg="white") + + click.secho(f"\n Validation method: {validation_method}", bg="cyan", fg="white") click.secho() @@ -116,9 +114,13 @@ def cli_message(linter: Linter) -> None: None """ if linter.valid_stac == True: - click.secho(f"Valid {linter.asset_type}: {linter.valid_stac}", fg="green") + click.secho( + f"{linter.asset_type} Passed: {linter.valid_stac}", fg="green", bold=True + ) else: - click.secho(f"Valid {linter.asset_type}: {linter.valid_stac}", fg="red") + click.secho( + f"{linter.asset_type} Passed: {linter.valid_stac}", fg="red", bold=True + ) """ schemas validated for core object """ click.secho() @@ -128,31 +130,41 @@ def cli_message(linter: Linter) -> None: # For Pydantic validation, always show the appropriate schema model if using_pydantic: - click.secho("Schemas validated: ", fg="blue") + click.secho("Schemas checked: ", fg="blue") asset_type = linter.asset_type.capitalize() if linter.asset_type else "Item" click.secho(f" stac-pydantic {asset_type} model") # For JSONSchema validation or when schemas are available elif len(linter.schema) > 0: - click.secho("Schemas validated: ", fg="blue") + click.secho("Schemas checked: ", fg="blue") for schema in linter.schema: click.secho(f" {schema}") + click.secho() + if linter.failed_schema != "": + click.secho("Failed Schema: ", fg="blue") + click.secho(f" {linter.failed_schema}") + click.secho() + if linter.recommendation != "": + click.secho("Recommendation: ", fg="blue") + click.secho(f" {linter.recommendation}") + """ best practices message""" click.secho() for message in linter.best_practices_msg: if message == linter.best_practices_msg[0]: click.secho("\n " + message, bg="blue") + click.secho() else: - click.secho(message, fg="red") + click.secho(message, fg="black") """ geometry validation errors """ if linter.geometry_errors_msg: - click.secho() for message in linter.geometry_errors_msg: if message == linter.geometry_errors_msg[0]: - click.secho("\n " + message, bg="yellow", fg="black") + click.secho("\n " + message, bg="magenta", fg="black") + click.secho() else: - click.secho(message, fg="red") + click.secho(message, fg="black") if linter.validate_all == True: click.secho() @@ -180,8 +192,8 @@ def cli_message(linter: Linter) -> None: link_asset_message(linter.invalid_link_request, "link", "request", True) if linter.error_type != "": - click.secho() click.secho("\n Validation Errors: ", fg="white", bold=True, bg="black") + click.secho() click.secho("Validation error type: ", fg="red") click.secho(f" {linter.error_type}") click.secho() @@ -193,12 +205,10 @@ def cli_message(linter: Linter) -> None: if linter.error_msg != "" and linter.verbose_error_msg == "": click.secho("Refer to --verbose for more details.", fg="blue") - click.secho() if linter.verbose_error_msg: + click.secho("\n Verbose Validation Output: ", fg="white", bg="cyan") click.secho() - click.secho("\n Verbose Validation Output: ", fg="white", bg="red") - if isinstance(linter.verbose_error_msg, dict): formatted_error = format_verbose_error(linter.verbose_error_msg) else: @@ -206,11 +216,17 @@ def cli_message(linter: Linter) -> None: click.secho(formatted_error) - click.secho() click.secho() click.secho("\n Additional Information: ", bg="green", fg="white") + click.secho() click.secho(f"This object has {len(linter.data['links'])} links", bold=True) + if not using_pydantic: + click.secho() + click.secho( + "Disclaimer: Schema-based STAC validation may be incomplete and should only be considered as a first indicator of validity.\nSee: https://github.com/radiantearth/stac-spec/discussions/1242" + ) + click.secho() # Stac validator response for reference diff --git a/stac_check/lint.py b/stac_check/lint.py index 78ede16..2e94887 100644 --- a/stac_check/lint.py +++ b/stac_check/lint.py @@ -161,18 +161,30 @@ def __post_init__(self): self.data = self.load_data(self.item) self.message = self.validate_file(self.item) self.config = self.parse_config(self.config_file) - self.asset_type = ( - self.message["asset_type"] if "asset_type" in self.message else "" - ) - self.version = self.message["version"] if "version" in self.message else "" + + from .utilities import determine_asset_type + + # Set message fields using the get_message_field method + self.asset_type = self.get_message_field("asset_type") + + # If asset_type is not in message, determine it from the data + if self.asset_type == "" and isinstance(self.data, dict): + self.asset_type = determine_asset_type(self.data) + + 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.validate_all = self.recursive_validation(self.item) - self.valid_stac = self.message["valid_stac"] - self.error_type = self.check_error_type() - self.error_msg = self.check_error_message() - self.verbose_error_msg = self.check_verbose_error_message() + + # Set error and info fields + self.error_type = self.get_message_field("error_type") + self.error_msg = self.get_message_field("error_message") + self.failed_schema = self.get_message_field("failed_schema") + self.recommendation = self.get_message_field("recommendation") + self.verbose_error_msg = self.get_message_field("error_verbose") self.invalid_asset_format = ( self.check_links_assets(10, "assets", "format") if self.assets else None ) @@ -325,8 +337,7 @@ def validate_file(self, file: Union[str, dict]) -> Dict[str, Any]: else: raise ValueError("Input must be a file path or STAC dictionary.") - message = stac.message[0] - return message + return stac.message[0] def recursive_validation(self, file: Union[str, Dict[str, Any]]) -> str: """Recursively validate a STAC item or catalog file and its child items. @@ -401,40 +412,16 @@ def check_links_assets( return links return links - def check_error_type(self) -> str: - """Returns the error type of a STAC validation if it exists in the validation message, - and an empty string otherwise. - - Returns: - str: A string containing the error type of a STAC validation if it exists in the validation message, and an - empty string otherwise. - """ - if "error_type" in self.message: - return self.message["error_type"] - else: - return "" - - def check_error_message(self) -> str: - """Checks whether the `message` attribute contains an `error_message` field. + def get_message_field(self, field_name: str) -> str: + """Get a field from the validation message. - Returns: - A string containing the value of the `error_message` field, or an empty string if the field is not present. - """ - if "error_message" in self.message: - return self.message["error_message"] - else: - return "" - - def check_verbose_error_message(self) -> str: - """Checks whether the `message` attribute contains an `verbose_error_message` field. + Args: + field_name: The name of the field to retrieve (e.g., 'error_type', 'error_message') Returns: - A string containing the value of the `verbose_error_message` field, or an empty string if the field is not present. + The value of the field if it exists, otherwise an empty string. """ - if "error_verbose" in self.message: - return self.message["error_verbose"] - else: - return "" + return self.message.get(field_name, "") def check_summaries(self) -> bool: """Check if a Collection asset has a "summaries" property. @@ -1080,7 +1067,7 @@ def create_best_practices_msg(self) -> List[str]: for _, v in filtered_dict.items(): for value in v: - best_practices.extend([" " + value]) + best_practices.extend([value]) best_practices.extend([""]) return best_practices @@ -1124,7 +1111,7 @@ def create_geometry_errors_msg(self) -> List[str]: for _, v in geometry_dict.items(): for value in v: - geometry_errors.extend([" " + value]) + geometry_errors.extend([value]) geometry_errors.extend([""]) return geometry_errors diff --git a/stac_check/utilities.py b/stac_check/utilities.py index 07f398b..1cf72b1 100644 --- a/stac_check/utilities.py +++ b/stac_check/utilities.py @@ -1,3 +1,40 @@ +def determine_asset_type(data): + """Determine the STAC asset type from the given data dictionary. + + This function identifies the type of STAC object based on its structure and content. + It handles all STAC object types including Item, Collection, Catalog, and FeatureCollection. + + Args: + data (dict): The STAC data dictionary + + Returns: + str: The asset type in uppercase (e.g., 'ITEM', 'COLLECTION', 'FEATURECOLLECTION', 'CATALOG') + or an empty string if the type cannot be determined. + """ + if not isinstance(data, dict): + return "" + + # Check for STAC Item types + if data.get("type") == "Feature": + return "ITEM" + elif data.get("type") == "FeatureCollection": + return "FEATURECOLLECTION" + elif data.get("type") == "Collection": + return "COLLECTION" + + # For STAC Catalog/Collection without explicit type or with older STAC versions + if "stac_version" in data and "id" in data: + if data.get("type") == "Catalog": + return "CATALOG" + # If type is not explicitly set, determine based on structure + if "extent" in data and "links" in data: + return "COLLECTION" + return "CATALOG" + + # If we can't determine the type + return "" + + def format_verbose_error(error_data): """Format verbose error data into a human-readable string.""" if not error_data or not isinstance(error_data, dict): diff --git a/tests/test_lint.py b/tests/test_lint.py index fe9f6d2..7529ad1 100644 --- a/tests/test_lint.py +++ b/tests/test_lint.py @@ -102,10 +102,10 @@ def test_linter_collection_no_summaries(): assert linter.check_summaries() == False assert linter.best_practices_msg == [ "STAC Best Practices: ", - " Object should be called 'collection.json' not 'collection-no-summaries.json'", + "Object should be called 'collection.json' not 'collection-no-summaries.json'", "", - " A STAC collection should contain a summaries field", - " It is recommended to store information like eo:bands in summaries", + "A STAC collection should contain a summaries field", + "It is recommended to store information like eo:bands in summaries", "", ]