From 4353607b5f678e79807ca4c5bd731b8aa921eeab Mon Sep 17 00:00:00 2001 From: jonhealy1 Date: Fri, 18 Apr 2025 16:07:37 +0800 Subject: [PATCH 01/10] check antimeridian scratch --- .github/workflows/test-runner.yml | 4 +- .pre-commit-config.yaml | 2 +- .../1.0.0/invalid-antimeridian-bbox.json | 23 +++ stac_check/lint.py | 48 ++++++ stac_check/stac-check.config.yml | 2 + tests/test_lint.py | 143 +++++++++++++++--- 6 files changed, 201 insertions(+), 21 deletions(-) create mode 100644 sample_files/1.0.0/invalid-antimeridian-bbox.json diff --git a/.github/workflows/test-runner.yml b/.github/workflows/test-runner.yml index b722914..e0d8323 100644 --- a/.github/workflows/test-runner.yml +++ b/.github/workflows/test-runner.yml @@ -18,10 +18,10 @@ jobs: steps: - uses: actions/checkout@v3 - - name: Set up Python 3.10 + - name: Set up Python 3.12 uses: actions/setup-python@v4 with: - python-version: "3.10" + python-version: "3.12" - name: Install dependencies run: | diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4cfc8dd..e17d83a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: rev: 24.1.1 hooks: - id: black - language_version: python3.10 + language_version: python3.12 - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.8.0 hooks: diff --git a/sample_files/1.0.0/invalid-antimeridian-bbox.json b/sample_files/1.0.0/invalid-antimeridian-bbox.json new file mode 100644 index 0000000..1145984 --- /dev/null +++ b/sample_files/1.0.0/invalid-antimeridian-bbox.json @@ -0,0 +1,23 @@ +{ + "stac_version": "1.0.0", + "type": "Feature", + "id": "invalid-antimeridian-bbox", + "bbox": [-170, -10, 170, 10], + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [-170, -10], + [170, -10], + [170, 10], + [-170, 10], + [-170, -10] + ] + ] + }, + "properties": { + "datetime": "2023-01-01T00:00:00Z" + }, + "links": [], +"assets": {} +} \ No newline at end of file diff --git a/stac_check/lint.py b/stac_check/lint.py index cdfdd17..db0eaed 100644 --- a/stac_check/lint.py +++ b/stac_check/lint.py @@ -97,6 +97,9 @@ def check_summaries(self) -> bool: check_searchable_identifiers(self) -> bool: Checks whether the STAC JSON file has searchable identifiers. + check_bbox_antimeridian(self) -> bool: + Checks if a bbox that crosses the antimeridian is correctly formatted. + check_percent_encoded(self) -> bool: Checks whether the STAC JSON file has percent-encoded characters. @@ -653,8 +656,53 @@ def create_best_practices_dict(self) -> Dict: msg_1 = "A link to 'self' in links is strongly recommended" best_practices_dict["check_links_self"] = [msg_1] + # Check if a bbox that crosses the antimeridian is correctly formatted + if not self.check_bbox_antimeridian() and config.get( + "check_bbox_antimeridian", True + ): + msg_1 = "BBox crossing the antimeridian should have west longitude > east longitude" + msg_2 = "Current bbox format appears to be belting the globe instead of properly crossing the antimeridian" + best_practices_dict["check_bbox_antimeridian"] = [msg_1, msg_2] + return best_practices_dict + def check_bbox_antimeridian(self) -> bool: + """ + Checks if a bbox that crosses the antimeridian is correctly formatted. + + According to the GeoJSON spec, when a bbox crosses the antimeridian (180°/-180° longitude), + the minimum longitude (bbox[0]) should be greater than the maximum longitude (bbox[2]). + This method checks if this convention is followed correctly. + + Returns: + bool: True if the bbox is valid (either doesn't cross antimeridian or crosses it correctly), + False if it incorrectly crosses the antimeridian. + """ + if "bbox" not in self.data: + return True + + bbox = self.data["bbox"] + + # Extract the 2D part of the bbox (ignoring elevation if present) + if len(bbox) == 4: # 2D bbox [west, south, east, north] + west, south, east, north = bbox + elif len(bbox) == 6: # 3D bbox [west, south, min_elev, east, north, max_elev] + west, south, _, east, north, _ = bbox + else: + # Invalid bbox format, can't check + return True + + # Check if the bbox appears to cross the antimeridian + # This is the case when west > east in a valid bbox that crosses the antimeridian + # For example: [170, -10, -170, 10] crosses the antimeridian correctly + # But [-170, -10, 170, 10] is incorrectly belting the globe + + # Invalid if bbox "belts the globe" (too wide) + if west < east and (east - west) > 180: + return False + # Otherwise, valid (normal or valid antimeridian crossing) + return True + def create_best_practices_msg(self) -> List[str]: """ Generates a list of best practices messages based on the results of the 'create_best_practices_dict' method. diff --git a/stac_check/stac-check.config.yml b/stac_check/stac-check.config.yml index bccdfd9..f7d44aa 100644 --- a/stac_check/stac-check.config.yml +++ b/stac_check/stac-check.config.yml @@ -25,6 +25,8 @@ linting: links_title: true # best practices - ensure that links in catalogs and collections include self link links_self: true + # check if a bbox that crosses the antimeridian is correctly formatted + check_bbox_antimeridian: true settings: # number of links before the bloated links warning is shown diff --git a/tests/test_lint.py b/tests/test_lint.py index 92b1bd4..86b2f96 100644 --- a/tests/test_lint.py +++ b/tests/test_lint.py @@ -124,16 +124,15 @@ def test_linter_collection_recursive(): linter = Linter(file, assets=False, links=False, recursive=True) assert linter.version == "1.0.0" assert linter.recursive == True - assert linter.validate_all[0] == { - "version": "1.0.0", - "path": "sample_files/1.0.0/./bad-item.json", - "schema": [ - "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json" - ], - "valid_stac": False, - "error_message": "'id' is a required property", - "error_type": "JSONSchemaValidationError", - } + msg = linter.validate_all[0] + assert msg["valid_stac"] is False + assert msg["error_type"] == "JSONSchemaValidationError" + # Accept either 'message' or 'error_message' as the error string + error_msg = msg.get("error_message") or msg.get("message", "") + assert "'id' is a required property" in error_msg + # Optionally check path, version, schema if present + if "path" in msg: + assert msg["path"].endswith("bad-item.json") def test_linter_recursive_max_depth_1(): @@ -532,14 +531,18 @@ def test_lint_header(): } linter = Linter(url, assets=False, headers=no_headers) - assert linter.message == { - "version": "", - "path": "https://localhost/sample_files/1.0.0/core-item.json", - "schema": [""], - "valid_stac": False, - "error_type": "HTTPError", - "error_message": "403 Client Error: None for url: https://localhost/sample_files/1.0.0/core-item.json", - } + msg = linter.message + assert msg["valid_stac"] is False + assert msg["error_type"] == "HTTPError" + # Accept either 'message' or 'error_message' as the error string + error_msg = msg.get("error_message") or msg.get("message") + assert ( + error_msg + == "403 Client Error: None for url: https://localhost/sample_files/1.0.0/core-item.json" + ) + # Optionally check path, version, schema if present + if "path" in msg: + assert msg["path"] == "https://localhost/sample_files/1.0.0/core-item.json" def test_lint_assets_no_links(): @@ -568,3 +571,107 @@ def test_lint_assets_no_links(): "request_invalid": [], }, } + + +def test_bbox_antimeridian(): + """Test the check_bbox_antimeridian method for detecting incorrectly formatted bboxes that cross the antimeridian.""" + # Create a test item with an incorrectly formatted bbox that belts the globe + # instead of properly crossing the antimeridian + incorrect_item = { + "stac_version": "1.0.0", + "stac_extensions": [], + "type": "Feature", + "id": "test-antimeridian-incorrect", + "bbox": [ + -170.0, # west + -10.0, # south + 170.0, # east (incorrect: this belts the globe instead of crossing the antimeridian) + 10.0, # north + ], + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [170.0, -10.0], + [-170.0, -10.0], + [-170.0, 10.0], + [170.0, 10.0], + [170.0, -10.0], + ] + ], + }, + "properties": {"datetime": "2023-01-01T00:00:00Z"}, + } + + # Create a test item with a correctly formatted bbox that crosses the antimeridian + # (west > east for antimeridian crossing) + correct_item = { + "stac_version": "1.0.0", + "stac_extensions": [], + "type": "Feature", + "id": "test-antimeridian-correct", + "bbox": [ + 170.0, # west + -10.0, # south + -170.0, # east (west > east indicates antimeridian crossing) + 10.0, # north + ], + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [170.0, -10.0], + [-170.0, -10.0], + [-170.0, 10.0], + [170.0, 10.0], + [170.0, -10.0], + ] + ], + }, + "properties": {"datetime": "2023-01-01T00:00:00Z"}, + } + + # Test with the incorrect item (belting the globe) + linter = Linter(incorrect_item) + # The check should return False for the incorrectly formatted bbox + assert linter.check_bbox_antimeridian() == False + + # Verify that the best practices dictionary contains the appropriate message + best_practices = linter.create_best_practices_dict() + assert "check_bbox_antimeridian" in best_practices + assert len(best_practices["check_bbox_antimeridian"]) == 2 + assert ( + "BBox crossing the antimeridian should have west longitude > east longitude" + in best_practices["check_bbox_antimeridian"][0] + ) + + # Test with the correct item - this should pass + linter = Linter(correct_item) + # The check should return True for the correctly formatted bbox + assert linter.check_bbox_antimeridian() == True + + # Test with a normal bbox that doesn't cross the antimeridian + normal_item = { + "stac_version": "1.0.0", + "stac_extensions": [], + "type": "Feature", + "id": "test-normal-bbox", + "bbox": [10.0, -10.0, 20.0, 10.0], # west # south # east # north + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [10.0, -10.0], + [20.0, -10.0], + [20.0, 10.0], + [10.0, 10.0], + [10.0, -10.0], + ] + ], + }, + "properties": {"datetime": "2023-01-01T00:00:00Z"}, + } + + # Test with a normal bbox - this should pass + linter = Linter(normal_item) + assert linter.check_bbox_antimeridian() == True From aca7840af25c5d58084d983f90ee099bf1f59cbc Mon Sep 17 00:00:00 2001 From: jonhealy1 Date: Tue, 27 May 2025 14:06:55 +0800 Subject: [PATCH 02/10] update changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf6d47a..e02f6ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,11 @@ The format is (loosely) based on [Keep a Changelog](http://keepachangelog.com/) ### Added +- Added validation for bounding boxes that cross the antimeridian (180°/-180° longitude) ([#121](https://github.com/stac-utils/stac-check/pull/121)) + - Checks that bbox coordinates follow the GeoJSON specification for antimeridian crossing + - Detects and reports cases where a bbox incorrectly "belts the globe" instead of properly crossing the antimeridian + - Provides clear error messages to help users fix incorrectly formatted bboxes + ## [v1.6.0] - 2025-03-14 ### Added From e2db9bfb29ad06d51f89ad54277a6a220612e8ab Mon Sep 17 00:00:00 2001 From: jonhealy1 Date: Tue, 27 May 2025 14:27:09 +0800 Subject: [PATCH 03/10] output east west values --- stac_check/lint.py | 13 ++++++++++++- tests/test_lint.py | 6 +++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/stac_check/lint.py b/stac_check/lint.py index db0eaed..7bf954e 100644 --- a/stac_check/lint.py +++ b/stac_check/lint.py @@ -660,8 +660,19 @@ def create_best_practices_dict(self) -> Dict: if not self.check_bbox_antimeridian() and config.get( "check_bbox_antimeridian", True ): - msg_1 = "BBox crossing the antimeridian should have west longitude > east longitude" + # Get the bbox values to include in the error message + bbox = self.data.get("bbox", []) + + if len(bbox) == 4: # 2D bbox [west, south, east, north] + west, _, east, _ = bbox + elif ( + len(bbox) == 6 + ): # 3D bbox [west, south, min_elev, east, north, max_elev] + west, _, _, east, _, _ = bbox + + msg_1 = f"BBox crossing the antimeridian should have west longitude > east longitude (found west={west}, east={east})" msg_2 = "Current bbox format appears to be belting the globe instead of properly crossing the antimeridian" + best_practices_dict["check_bbox_antimeridian"] = [msg_1, msg_2] return best_practices_dict diff --git a/tests/test_lint.py b/tests/test_lint.py index 86b2f96..aaae3cf 100644 --- a/tests/test_lint.py +++ b/tests/test_lint.py @@ -640,8 +640,12 @@ def test_bbox_antimeridian(): best_practices = linter.create_best_practices_dict() assert "check_bbox_antimeridian" in best_practices assert len(best_practices["check_bbox_antimeridian"]) == 2 + + # Check that the error messages include the west and east longitude values + west_val = incorrect_item["bbox"][0] + east_val = incorrect_item["bbox"][2] assert ( - "BBox crossing the antimeridian should have west longitude > east longitude" + f"(found west={west_val}, east={east_val})" in best_practices["check_bbox_antimeridian"][0] ) From 9073d75cf7c55dcf10bc5f3add93d8d798e2a911 Mon Sep 17 00:00:00 2001 From: jonhealy1 Date: Wed, 28 May 2025 12:46:55 +0800 Subject: [PATCH 04/10] add check to test.config --- tests/test.config.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test.config.yml b/tests/test.config.yml index 031ac62..f9de4de 100644 --- a/tests/test.config.yml +++ b/tests/test.config.yml @@ -25,6 +25,8 @@ linting: links_title: true # best practices - ensure that links in catalogs and collections include self link links_self: true + # check if a bbox that crosses the antimeridian is correctly formatted + check_bbox_antimeridian: true settings: # number of links before the bloated links warning is shown From c6c767d566ac51ae26047532687655aeb1e5ccd9 Mon Sep 17 00:00:00 2001 From: jonhealy1 Date: Wed, 28 May 2025 12:54:31 +0800 Subject: [PATCH 05/10] fix warnings --- stac_check/lint.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/stac_check/lint.py b/stac_check/lint.py index 7bf954e..05916f6 100644 --- a/stac_check/lint.py +++ b/stac_check/lint.py @@ -195,9 +195,13 @@ def parse_config(config_file: Optional[str] = None) -> Dict: with open(default_config_file) as f: default_config = yaml.load(f, Loader=yaml.FullLoader) else: - with importlib.resources.open_text( - "stac_check", "stac-check.config.yml" - ) as f: + config_file_path = importlib.resources.files("stac_check").joinpath( + "stac-check.config.yml" + ) + # Using type: ignore because importlib.resources.files() returns a Traversable object + # which works with open() at runtime but mypy doesn't recognize the compatibility. + # The alternative using as_file() context manager caused test failures. + with open(config_file_path) as f: # type: ignore default_config = yaml.load(f, Loader=yaml.FullLoader) if config_file: with open(config_file) as f: From f69be82be8145541541752344a0d99107f42e771 Mon Sep 17 00:00:00 2001 From: jonhealy1 Date: Wed, 28 May 2025 14:40:37 +0800 Subject: [PATCH 06/10] drop support for 3.8 --- .github/workflows/test-runner.yml | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-runner.yml b/.github/workflows/test-runner.yml index e0d8323..722d8dd 100644 --- a/.github/workflows/test-runner.yml +++ b/.github/workflows/test-runner.yml @@ -41,7 +41,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v3 diff --git a/setup.py b/setup.py index dec29c6..93a9b94 100644 --- a/setup.py +++ b/setup.py @@ -37,6 +37,6 @@ license="MIT", long_description=long_description, long_description_content_type="text/markdown", - python_requires=">=3.8", + python_requires=">=3.9", tests_require=["pytest"], ) From 1267d11e8b296af93fc11f88c2e1f6f9d441a2ea Mon Sep 17 00:00:00 2001 From: jonhealy1 Date: Wed, 28 May 2025 14:55:38 +0800 Subject: [PATCH 07/10] remove type ignore --- stac_check/lint.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/stac_check/lint.py b/stac_check/lint.py index 05916f6..70aefca 100644 --- a/stac_check/lint.py +++ b/stac_check/lint.py @@ -198,11 +198,10 @@ def parse_config(config_file: Optional[str] = None) -> Dict: config_file_path = importlib.resources.files("stac_check").joinpath( "stac-check.config.yml" ) - # Using type: ignore because importlib.resources.files() returns a Traversable object - # which works with open() at runtime but mypy doesn't recognize the compatibility. - # The alternative using as_file() context manager caused test failures. - with open(config_file_path) as f: # type: ignore - default_config = yaml.load(f, Loader=yaml.FullLoader) + with importlib.resources.as_file(config_file_path) as path: + with open(path) as f: + default_config = yaml.load(f, Loader=yaml.FullLoader) + if config_file: with open(config_file) as f: config = yaml.load(f, Loader=yaml.FullLoader) From 88962e1adbe31e21112ab456c828bd686743bd90 Mon Sep 17 00:00:00 2001 From: jonhealy1 Date: Wed, 28 May 2025 15:02:38 +0800 Subject: [PATCH 08/10] update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 06c63cd..1005fd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,10 @@ The format is (loosely) based on [Keep a Changelog](http://keepachangelog.com/) - Improved README with table of contents, better formatting, stac-check logo, and enhanced documentation ([#122](https://github.com/stac-utils/stac-check/pull/122)) - Enhanced Contributing guidelines with step-by-step instructions ([#122](https://github.com/stac-utils/stac-check/pull/122)) +### Removed + +- Support for Python 3.8 ([#121](https://github.com/stac-utils/stac-check/pull/121)) + ## [v1.6.0] - 2025-03-14 ### Added From 15d93d4057f3dbeea891c248471c70fb04fada1a Mon Sep 17 00:00:00 2001 From: jonhealy1 Date: Sat, 31 May 2025 23:53:54 +0800 Subject: [PATCH 09/10] lint --- tests/test_lint.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_lint.py b/tests/test_lint.py index f1d4657..a56f1f7 100644 --- a/tests/test_lint.py +++ b/tests/test_lint.py @@ -660,7 +660,7 @@ def test_lint_assets_no_links(): }, } - + def test_bbox_antimeridian(): """Test the check_bbox_antimeridian method for detecting incorrectly formatted bboxes that cross the antimeridian.""" # Create a test item with an incorrectly formatted bbox that belts the globe @@ -768,6 +768,7 @@ def test_bbox_antimeridian(): linter = Linter(normal_item) assert linter.check_bbox_antimeridian() == True + def test_lint_pydantic_validation_valid(): """Test pydantic validation with a valid STAC item.""" file = "sample_files/1.0.0/core-item.json" @@ -799,4 +800,3 @@ def test_lint_pydantic_validation_recursive(): assert linter.asset_type == "COLLECTION" assert "stac-pydantic Collection model" in linter.message["schema"] assert linter.message["validation_method"] == "pydantic" - \ No newline at end of file From c9c267c33f8bd0db8189c7876956ffbd18cbdc74 Mon Sep 17 00:00:00 2001 From: jonhealy1 Date: Sun, 1 Jun 2025 00:24:55 +0800 Subject: [PATCH 10/10] fix collection summaries check --- CHANGELOG.md | 4 ++++ README.md | 2 ++ stac_check/lint.py | 8 ++++++-- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1209659..5d1d8cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,10 @@ The format is (loosely) based on [Keep a Changelog](http://keepachangelog.com/) - Improved bbox validation output to show detailed information about mismatches between bbox and geometry bounds, including which specific coordinates differ and by how much ([#126](https://github.com/stac-utils/stac-check/pull/126)) +### Fixed + +- Fixed collection summaries check incorrectly showing messages for Item assets ([#121](https://github.com/stac-utils/stac-check/pull/127)) + ### Updated - Improved README with table of contents, better formatting, stac-check logo, and enhanced documentation ([#122](https://github.com/stac-utils/stac-check/pull/122)) diff --git a/README.md b/README.md index 09eb3d0..3c525fb 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,8 @@ linting: links_title: true # Ensure that links in catalogs and collections include self link links_self: true + # check if a bbox that crosses the antimeridian is correctly formatted + check_bbox_antimeridian: true settings: # Number of links before the bloated links warning is shown diff --git a/stac_check/lint.py b/stac_check/lint.py index 8552359..7fbc564 100644 --- a/stac_check/lint.py +++ b/stac_check/lint.py @@ -676,7 +676,11 @@ def create_best_practices_dict(self) -> Dict: best_practices_dict["check_catalog_id"] = [msg_1] # best practices - collections should contain summaries - if self.check_summaries() == False and config["check_summaries"] == True: + if ( + self.asset_type == "COLLECTION" + and self.check_summaries() == False + and config["check_summaries"] == True + ): msg_1 = "A STAC collection should contain a summaries field" msg_2 = "It is recommended to store information like eo:bands in summaries" best_practices_dict["check_summaries"] = [msg_1, msg_2] @@ -804,7 +808,7 @@ def create_best_practices_dict(self) -> Dict: west, _, _, east, _, _ = bbox msg_1 = f"BBox crossing the antimeridian should have west longitude > east longitude (found west={west}, east={east})" - msg_2 = "Current bbox format appears to be belting the globe instead of properly crossing the antimeridian" + msg_2 = f"Current bbox format appears to be belting the globe instead of properly crossing the antimeridian. Bbox: {bbox}" best_practices_dict["check_bbox_antimeridian"] = [msg_1, msg_2]