Skip to content

Commit d3ec73d

Browse files
authored
Merge pull request #219 from stac-utils/item_collection
Validate item collections
2 parents f00a07d + 0c18266 commit d3ec73d

16 files changed

+1047
-60
lines changed

.pre-commit-config.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
repos:
2-
- repo: https://gitlab.com/pycqa/flake8
2+
- repo: https://github.com/PyCQA/flake8
33
rev: 3.9.1
44
hooks:
55
- id: flake8

CHANGELOG.md

+12-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file.
44

55
The format is (loosely) based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/).
66

7+
## [v3.3.0] - 2022-11-28
8+
9+
### Added
10+
11+
- Added --item-collection to validate local and remote item collections https://github.com/stac-utils/stac-validator/pull/219
12+
- Added --pages to validate additional items retrieved via pagination links https://github.com/stac-utils/stac-validator/pull/219
13+
714
## [v3.2.0] - 2022-09-20
815

916
### Added
@@ -164,7 +171,11 @@ The format is (loosely) based on [Keep a Changelog](http://keepachangelog.com/)
164171
- 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.
165172
- Logging. Various additions were made here depending on the options selected. This was done to help assist people to update their STAC collections.
166173

167-
[v3.0.0]: <https://github.com/sparkgeo/stac-validator/compare/v2.5.0..main>
174+
[Unreleased]: <https://github.com/sparkgeo/stac-validator/compare/v3.3.0..main>
175+
[v3.3.0]: <https://github.com/sparkgeo/stac-validator/compare/v3.2.0..v3.3.0>
176+
[v3.2.0]: <https://github.com/sparkgeo/stac-validator/compare/v3.1.0..v3.2.0>
177+
[v3.1.0]: <https://github.com/sparkgeo/stac-validator/compare/v3.0.0..v3.1.0>
178+
[v3.0.0]: <https://github.com/sparkgeo/stac-validator/compare/v2.5.0..v3.0.0>
168179
[v2.5.0]: <https://github.com/sparkgeo/stac-validator/compare/v2.4.3..v2.5.0>
169180
[v2.4.3]: <https://github.com/sparkgeo/stac-validator/compare/v2.3.0..v2.4.0>
170181
[v2.4.2]: <https://github.com/sparkgeo/stac-validator/compare/v2.4.1..v2.4.2>

README.md

+19
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,10 @@ Options:
102102
-m, --max-depth INTEGER Maximum depth to traverse when recursing. Omit this
103103
argument to get full recursion. Ignored if
104104
`recursive == False`.
105+
--item-collection Validate item collection response. Can be combined
106+
with --pages. Defaults to one page.
107+
-p, --pages INTEGER Maximum number of pages to validate via --item-
108+
collection. Defaults to one page.
105109
-v, --verbose Enables verbose output for recursive mode.
106110
--no_output Do not print output to console.
107111
--log_file TEXT Save full recursive output to log file (local
@@ -200,6 +204,16 @@ stac = stac_validator.StacValidate()
200204
stac.validate_dict(dictionary)
201205
print(stac.message)
202206
```
207+
208+
**Item Collection**
209+
210+
```python
211+
from stac_validator import stac_validator
212+
213+
stac = stac_validator.StacValidate()
214+
stac.validate_item_collection_dict(item_collection_dict)
215+
print(stac.message)
216+
```
203217
---
204218
205219
# Testing
@@ -305,3 +319,8 @@ stac-validator https://spot-canada-ortho.s3.amazonaws.com/catalog.json --recursi
305319
}
306320
]
307321
```
322+
**--item-collection**
323+
324+
```bash
325+
stac-validator https://earth-search.aws.element84.com/v0/collections/sentinel-s2-l2a/items --item_collection --pages 2
326+
```

setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from setuptools import setup
44

5-
__version__ = "3.2.0"
5+
__version__ = "3.3.0"
66

77
with open("README.md", "r") as fh:
88
long_description = fh.read()

stac_validator/stac_validator.py

+33-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,17 @@ def print_update_message(version):
1818
click.secho()
1919

2020

21+
def item_collection_summary(message):
22+
valid_count = 0
23+
for item in message:
24+
if "valid_stac" in item and item["valid_stac"] is True:
25+
valid_count = valid_count + 1
26+
click.secho()
27+
click.secho("--item-collection summary", bold=True)
28+
click.secho(f"items_validated: {len(message)}")
29+
click.secho(f"valid_items: {valid_count}")
30+
31+
2132
@click.command()
2233
@click.argument("stac_file")
2334
@click.option(
@@ -52,6 +63,17 @@ def print_update_message(version):
5263
type=int,
5364
help="Maximum depth to traverse when recursing. Omit this argument to get full recursion. Ignored if `recursive == False`.",
5465
)
66+
@click.option(
67+
"--item-collection",
68+
is_flag=True,
69+
help="Validate item collection response. Can be combined with --pages. Defaults to one page.",
70+
)
71+
@click.option(
72+
"--pages",
73+
"-p",
74+
type=int,
75+
help="Maximum number of pages to validate via --item-collection. Defaults to one page.",
76+
)
5577
@click.option(
5678
"-v", "--verbose", is_flag=True, help="Enables verbose output for recursive mode."
5779
)
@@ -64,6 +86,8 @@ def print_update_message(version):
6486
@click.version_option(version=pkg_resources.require("stac-validator")[0].version)
6587
def main(
6688
stac_file,
89+
item_collection,
90+
pages,
6791
recursive,
6892
max_depth,
6993
core,
@@ -79,6 +103,8 @@ def main(
79103
valid = True
80104
stac = StacValidate(
81105
stac_file=stac_file,
106+
item_collection=item_collection,
107+
pages=pages,
82108
recursive=recursive,
83109
max_depth=max_depth,
84110
core=core,
@@ -90,7 +116,10 @@ def main(
90116
no_output=no_output,
91117
log=log_file,
92118
)
93-
valid = stac.run()
119+
if not item_collection:
120+
valid = stac.run()
121+
else:
122+
stac.validate_item_collection()
94123

95124
message = stac.message
96125
if "version" in message[0]:
@@ -99,6 +128,9 @@ def main(
99128
if no_output is False:
100129
click.echo(json.dumps(message, indent=4))
101130

131+
if item_collection:
132+
item_collection_summary(message)
133+
102134
sys.exit(0 if valid else 1)
103135

104136

stac_validator/validate.py

+83-48
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import json
22
import os
33
from json.decoder import JSONDecodeError
4-
from typing import List, Optional
4+
from typing import Optional
55
from urllib.error import HTTPError, URLError
66

77
import click # type: ignore
@@ -22,7 +22,9 @@
2222
class StacValidate:
2323
def __init__(
2424
self,
25-
stac_file: str = None,
25+
stac_file: Optional[str] = None,
26+
item_collection: bool = False,
27+
pages: Optional[int] = None,
2628
recursive: bool = False,
2729
max_depth: Optional[int] = None,
2830
core: bool = False,
@@ -35,6 +37,8 @@ def __init__(
3537
log: str = "",
3638
):
3739
self.stac_file = stac_file
40+
self.item_collection = item_collection
41+
self.pages = pages
3842
self.message: list = []
3943
self.custom = custom
4044
self.links = links
@@ -64,10 +68,10 @@ def create_err_msg(self, err_type: str, err_msg: str) -> dict:
6468
}
6569

6670
def create_links_message(self):
67-
format_valid: List[str] = []
68-
format_invalid: List[str] = []
69-
request_valid: List[str] = []
70-
request_invalid: List[str] = []
71+
format_valid = []
72+
format_invalid = []
73+
request_valid = []
74+
request_invalid = []
7175
return {
7276
"format_valid": format_valid,
7377
"format_invalid": format_invalid,
@@ -267,75 +271,106 @@ def recursive_validator(self, stac_type: str) -> bool:
267271
click.echo(json.dumps(message, indent=4))
268272
return True
269273

270-
def validate_dict(cls, stac_content):
271-
cls.stac_content = stac_content
272-
return cls.run()
274+
def validate_dict(self, stac_content):
275+
self.stac_content = stac_content
276+
return self.run()
273277

274-
def run(cls):
278+
def validate_item_collection_dict(self, item_collection):
279+
for item in item_collection["features"]:
280+
self.custom = ""
281+
self.validate_dict(item)
282+
283+
def validate_item_collection(self):
284+
page = 1
285+
print(f"processing page {page}")
286+
item_collection = fetch_and_parse_file(self.stac_file)
287+
self.validate_item_collection_dict(item_collection)
288+
try:
289+
if self.pages is not None:
290+
for _ in range(self.pages - 1):
291+
if "links" in item_collection:
292+
for link in item_collection["links"]:
293+
if link["rel"] == "next":
294+
page = page + 1
295+
print(f"processing page {page}")
296+
next_link = link["href"]
297+
self.stac_file = next_link
298+
item_collection = fetch_and_parse_file(self.stac_file)
299+
self.validate_item_collection_dict(item_collection)
300+
break
301+
except Exception as e:
302+
message = {}
303+
message["pagination_error"] = (
304+
f"Validating the item collection failed on page {page}: ",
305+
str(e),
306+
)
307+
self.message.append(message)
308+
309+
def run(self):
275310
message = {}
276311
try:
277-
if cls.stac_file is not None:
278-
cls.stac_content = fetch_and_parse_file(cls.stac_file)
279-
stac_type = get_stac_type(cls.stac_content).upper()
280-
cls.version = cls.stac_content["stac_version"]
312+
if self.stac_file is not None and self.item_collection is False:
313+
self.stac_content = fetch_and_parse_file(self.stac_file)
314+
stac_type = get_stac_type(self.stac_content).upper()
315+
self.version = self.stac_content["stac_version"]
281316

282-
if cls.core is True:
283-
message = cls.create_message(stac_type, "core")
284-
cls.core_validator(stac_type)
285-
message["schema"] = [cls.custom]
286-
cls.valid = True
287-
elif cls.custom != "":
288-
message = cls.create_message(stac_type, "custom")
289-
message["schema"] = [cls.custom]
290-
cls.custom_validator()
291-
cls.valid = True
292-
elif cls.recursive:
293-
cls.valid = cls.recursive_validator(stac_type)
294-
elif cls.extensions is True:
295-
message = cls.extensions_validator(stac_type)
317+
if self.core is True:
318+
message = self.create_message(stac_type, "core")
319+
self.core_validator(stac_type)
320+
message["schema"] = [self.custom]
321+
self.valid = True
322+
elif self.custom != "":
323+
message = self.create_message(stac_type, "custom")
324+
message["schema"] = [self.custom]
325+
self.custom_validator()
326+
self.valid = True
327+
elif self.recursive:
328+
self.valid = self.recursive_validator(stac_type)
329+
elif self.extensions is True:
330+
message = self.extensions_validator(stac_type)
296331
else:
297-
cls.valid = True
298-
message = cls.default_validator(stac_type)
332+
self.valid = True
333+
message = self.default_validator(stac_type)
299334

300335
except URLError as e:
301-
message.update(cls.create_err_msg("URLError", str(e)))
336+
message.update(self.create_err_msg("URLError", str(e)))
302337
except JSONDecodeError as e:
303-
message.update(cls.create_err_msg("JSONDecodeError", str(e)))
338+
message.update(self.create_err_msg("JSONDecodeError", str(e)))
304339
except ValueError as e:
305-
message.update(cls.create_err_msg("ValueError", str(e)))
340+
message.update(self.create_err_msg("ValueError", str(e)))
306341
except TypeError as e:
307-
message.update(cls.create_err_msg("TypeError", str(e)))
342+
message.update(self.create_err_msg("TypeError", str(e)))
308343
except FileNotFoundError as e:
309-
message.update(cls.create_err_msg("FileNotFoundError", str(e)))
344+
message.update(self.create_err_msg("FileNotFoundError", str(e)))
310345
except ConnectionError as e:
311-
message.update(cls.create_err_msg("ConnectionError", str(e)))
346+
message.update(self.create_err_msg("ConnectionError", str(e)))
312347
except exceptions.SSLError as e:
313-
message.update(cls.create_err_msg("SSLError", str(e)))
348+
message.update(self.create_err_msg("SSLError", str(e)))
314349
except OSError as e:
315-
message.update(cls.create_err_msg("OSError", str(e)))
350+
message.update(self.create_err_msg("OSError", str(e)))
316351
except jsonschema.exceptions.ValidationError as e:
317352
if e.absolute_path:
318353
err_msg = f"{e.message}. Error is in {' -> '.join([str(i) for i in e.absolute_path])} "
319354
else:
320355
err_msg = f"{e.message} of the root of the STAC object"
321-
message.update(cls.create_err_msg("JSONSchemaValidationError", err_msg))
356+
message.update(self.create_err_msg("JSONSchemaValidationError", err_msg))
322357
except KeyError as e:
323-
message.update(cls.create_err_msg("KeyError", str(e)))
358+
message.update(self.create_err_msg("KeyError", str(e)))
324359
except HTTPError as e:
325-
message.update(cls.create_err_msg("HTTPError", str(e)))
360+
message.update(self.create_err_msg("HTTPError", str(e)))
326361
except Exception as e:
327-
message.update(cls.create_err_msg("Exception", str(e)))
362+
message.update(self.create_err_msg("Exception", str(e)))
328363

329364
if len(message) > 0:
330-
message["valid_stac"] = cls.valid
331-
cls.message.append(message)
365+
message["valid_stac"] = self.valid
366+
self.message.append(message)
332367

333-
if cls.log != "":
334-
f = open(cls.log, "w")
335-
f.write(json.dumps(cls.message, indent=4))
368+
if self.log != "":
369+
f = open(self.log, "w")
370+
f.write(json.dumps(self.message, indent=4))
336371
f.close()
337372

338-
if cls.valid:
373+
if self.valid:
339374
return True
340375
else:
341376
return False

tests/test_assets.py

-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
Description: Test --links option
33
44
"""
5-
__authors__ = "James Banting", "Jonathan Healy"
65

76
import json
87

tests/test_core.py

-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
Description: Test the validator for core functionality without validating extensions
33
44
"""
5-
__authors__ = "James Banting", "Jonathan Healy"
65

76
from stac_validator import stac_validator
87

tests/test_custom.py

-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
Description: Test the custom option for custom schemas
33
44
"""
5-
__authors__ = "James Banting", "Jonathan Healy"
65

76
from stac_validator import stac_validator
87

0 commit comments

Comments
 (0)