Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use stac-validator for validation #289

Merged
merged 3 commits into from
May 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Use black (instead of yapf) for formatting ([#274](https://github.com/stac-utils/stactools/pull/274))
- stac-check version and lint reporting ([#258](https://github.com/stac-utils/stactools/pull/258))
- Sphinx theme ([#284](https://github.com/stac-utils/stactools/pull/284))
- Use stac-validator for validation ([#289](https://github.com/stac-utils/stactools/pull/289))

### Fixed

Expand Down
1 change: 1 addition & 0 deletions requirements-min.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ pystac[validation] == 1.2
rasterio == 1.2.9
requests == 2.20
stac-check == 1.2.0
stac-validator == 3.1.0
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ install_requires =
rasterio >= 1.2.9
requests >= 2.20
stac-check >= 1.2.0
stac-validator >= 3.1.0

[options.extras_require]
all =
Expand Down
119 changes: 44 additions & 75 deletions src/stactools/cli/commands/validate.py
Original file line number Diff line number Diff line change
@@ -1,97 +1,66 @@
import json
import sys
from typing import List, Optional
from typing import Optional

import click
import pystac
from pystac import Catalog, Collection, Item, STACObject, STACValidationError

from stactools.core.utils import href_exists
from stac_validator.validate import StacValidate


def create_validate_command(cli: click.Group) -> click.Command:
@cli.command("validate", short_help="Validate a stac object.")
@click.argument("href")
@click.option(
"--recurse/--no-recurse",
"--recursive/--no-recursive",
help="Recursively validate all STAC objects in this catalog.",
default=True,
help=(
"If false, do not validate any children "
"(only useful for Catalogs and Collections"
),
)
@click.option(
"--links/--no-links",
default=True,
help=("If false, do not check any of the objects's links."),
"--validate-links/--no-validate-links", help="Validate links.", default=True
)
@click.option(
"--assets/--no-assets",
default=True,
help=("If false, do not check any of the collection's/item's assets."),
"--validate-assets/--no-validate-assets", help="Validate assets.", default=True
)
@click.option("-v", "--verbose", is_flag=True, help="Enables verbose output.")
@click.option(
"--quiet/--no-quiet", help="Do not print output to console.", default=False
)
def validate_command(href: str, recurse: bool, links: bool, assets: bool) -> None:
@click.option(
"--log-file",
help="Save output to file (local filepath).",
)
def validate_command(
href: str,
recursive: bool,
validate_links: bool,
validate_assets: bool,
verbose: bool,
quiet: bool,
log_file: Optional[str],
) -> None:
"""Validates a STAC object.

Prints any validation errors to stdout.
"""
object = pystac.read_file(href)

if isinstance(object, Item):
errors = validate(object, None, False, links, assets)
else:
errors = validate(object, object, recurse, links, assets)
This is a thin wrapper around
[stac-validate](https://github.com/stac-utils/stac-validator). Not all
command-line options are exposed. If you want more control over
validation, use `stac-validator` directly.

if not errors:
click.secho("OK", fg="green", nl=False)
click.echo(f" STAC object at {href} is valid!")
If you'd like linting, use `stac lint`.
"""
validate = StacValidate(
href,
recursive=recursive,
links=validate_links,
assets=validate_assets,
verbose=verbose,
no_output=quiet,
log=log_file or "",
)
is_valid = validate.run()
if not quiet:
click.echo(json.dumps(validate.message, indent=4))
if is_valid:
sys.exit(0)
else:
for error in errors:
click.secho("ERROR", fg="red", nl=False)
click.echo(f" {error}")
sys.exit(1)

return validate_command


def validate(
object: STACObject,
root: Optional[STACObject],
recurse: bool,
links: bool,
assets: bool,
) -> List[str]:
errors: List[str] = []

try:
object.validate()
except FileNotFoundError as e:
errors.append(f"File not found: {e}")
except STACValidationError as e:
errors.append(f"{e}\n{e.source}")

if links:
for link in object.get_links():
href = link.get_absolute_href()
assert href
if not href_exists(href):
errors.append(
f'Missing link in {object.self_href}: "{link.rel}" -> {link.href}'
)

if assets and (isinstance(object, Item) or isinstance(object, Collection)):
for name, asset in object.get_assets().items():
href = asset.get_absolute_href()
assert href
if not href_exists(href):
errors.append(
f"Asset '{name}' does not exist: {asset.get_absolute_href()}"
)

if recurse:
assert isinstance(object, Catalog) or isinstance(object, Collection)
for child in object.get_children():
errors.extend(validate(child, root, recurse, links, assets))
for item in object.get_items():
errors.extend(validate(item, root, False, links, assets))

return errors
30 changes: 17 additions & 13 deletions tests/cli/commands/test_validate.py
Original file line number Diff line number Diff line change
@@ -1,48 +1,52 @@
from typing import Callable, List

from click import Command, Group

from stactools.cli.commands.validate import create_validate_command
from stactools.testing import CliTestCase
from stactools.testing.cli_test import CliTestCase
from tests import test_data


class ValidatateTest(CliTestCase):
def create_subcommand_functions(self) -> List[Callable]:
def create_subcommand_functions(self) -> List[Callable[[Group], Command]]:
return [create_validate_command]

def test_valid_item(self):
def test_valid_item(self) -> None:
path = test_data.get_path(
"data-files/catalogs/test-case-1/country-1/area-1-1/"
"area-1-1-imagery/area-1-1-imagery.json"
)
result = self.run_command(["validate", path, "--no-assets"])
result = self.run_command(f"validate {path} --no-validate-assets")
self.assertEqual(0, result.exit_code)

def test_invalid_item(self):
def test_invalid_item(self) -> None:
path = test_data.get_path(
"data-files/catalogs/test-case-1/country-1/area-1-1/"
"area-1-1-imagery/area-1-1-imagery-invalid.json"
)
result = self.run_command(["validate", path])
result = self.run_command(f"validate {path}")
self.assertEqual(1, result.exit_code)

def test_collection_with_invalid_item(self):
def test_collection_with_invalid_item(self) -> None:
path = test_data.get_path(
"data-files/catalogs/test-case-1/country-1/area-1-1/collection-invalid.json"
)
result = self.run_command(["validate", path])
result = self.run_command(f"validate {path}")
self.assertEqual(1, result.exit_code)

def test_collection_with_invalid_item_no_validate_all(self):
def test_collection_with_invalid_item_no_validate_all(self) -> None:
path = test_data.get_path(
"data-files/catalogs/test-case-1/country-1/area-1-1/collection-invalid.json"
)
result = self.run_command(["validate", path, "--no-recurse"])
result = self.run_command(f"validate {path} --no-recursive")
self.assertEqual(0, result.exit_code)

def test_collection_invalid_asset(self):
def test_collection_invalid_asset(self) -> None:
path = test_data.get_path(
"data-files/catalogs/test-case-1/country-1"
"/area-1-1/area-1-1-imagery/area-1-1-imagery.json"
)
result = self.run_command(["validate", path])
self.assertEqual(1, result.exit_code)
result = self.run_command(f"validate {path}")
self.assertEqual(
0, result.exit_code
) # unreachable links aren't an error in stac-validator