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

Fix spacenet tutorial, allow for single bbox argument to SpatialExtent, and other fixes. #201

Merged
merged 11 commits into from
Oct 22, 2020
6 changes: 2 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,15 +81,13 @@ PySTAC uses [flake8](http://flake8.pycqa.org/en/latest/) and [yapf](https://gith
To run the flake8 style checks:

```
> flake8 pystac
> flake8 tests
> flake8 pystac tests
```

To format code:

```
> yapf -ipr pystac
> yapf -ipr tests
> yapf -ipr pystac tests
lossyrob marked this conversation as resolved.
Show resolved Hide resolved
```

You can also run the `./scripts/test` script to check flake8 and yapf.
Expand Down
92 changes: 77 additions & 15 deletions docs/tutorials/pystac-spacenet-tutorial.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@
"from botocore.errorfactory import ClientError\n",
"import pystac\n",
"from pystac.extensions import label\n",
"from shapely.geometry import GeometryCollection, Polygon, box, shape"
"from shapely.geometry import GeometryCollection, Polygon, box, shape, mapping"
]
},
{
Expand Down Expand Up @@ -217,7 +217,7 @@
" params['id'] = basename(uri).split('.')[0]\n",
" with rasterio.open(uri) as src:\n",
" params['bbox'] = list(src.bounds)\n",
" params['geometry'] = box(*params['bbox']).__geo_interface__\n",
" params['geometry'] = mapping(box(*params['bbox']))\n",
" params['datetime'] = capture_date\n",
" params['properties'] = {}\n",
" i = pystac.Item(**params)\n",
Expand All @@ -240,24 +240,53 @@
"metadata": {},
"outputs": [],
"source": [
"bounds = GeometryCollection([shape(s.geometry) for s in spacenet.get_all_items()]).bounds\n",
"bounds = [list(GeometryCollection([shape(s.geometry) for s in spacenet.get_all_items()]).bounds)]\n",
"vegas.extent.spatial = pystac.SpatialExtent(bounds)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Currently, this STAC only exists in memory. We need to set all of the paths based on the root directory we want to save off that catalog too, and then save a \"self contained\" catalog, which will have all links be relative and contain no 'self' links. We can do this all in one shot with the `normalize_and_save` method:"
"Currently, this STAC only exists in memory. We need to set all of the paths based on the root directory we want to save off that catalog too, and then save a \"self contained\" catalog, which will have all links be relative and contain no 'self' links. We can do this by using the `normalize` method to set the HREFs of all of our STAC objects. We'll then validate the catalog, and then save:"
]
},
{
"cell_type": "code",
"execution_count": 12,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"<Catalog id=spacenet>"
]
},
"execution_count": 12,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"spacenet.normalize_hrefs('spacenet-stac')"
]
},
{
"cell_type": "code",
"execution_count": 13,
"metadata": {},
"outputs": [],
"source": [
"spacenet.normalize_and_save('spacenet-stac', catalog_type=pystac.CatalogType.SELF_CONTAINED)"
"spacenet.validate_all()"
]
},
{
"cell_type": "code",
"execution_count": 14,
"metadata": {},
"outputs": [],
"source": [
"spacenet.save(catalog_type=pystac.CatalogType.SELF_CONTAINED)"
]
},
{
Expand Down Expand Up @@ -285,7 +314,7 @@
},
{
"cell_type": "code",
"execution_count": 13,
"execution_count": 15,
"metadata": {},
"outputs": [],
"source": [
Expand All @@ -298,7 +327,7 @@
},
{
"cell_type": "code",
"execution_count": 14,
"execution_count": 16,
"metadata": {},
"outputs": [],
"source": [
Expand All @@ -314,7 +343,7 @@
" label_item = pystac.Item(\n",
" id='{}-labels'.format(item.id),\n",
" bbox=item.bbox,\n",
" geometry=box(*item.bbox).__geo_interface__,\n",
" geometry=mapping(box(*item.bbox)),\n",
" datetime=item.datetime,\n",
" properties={},\n",
" stac_extensions=[pystac.Extensions.LABEL]\n",
Expand All @@ -323,7 +352,12 @@
" label_item.ext.label.apply(\n",
" label_description='Building labels for scene {}'.format(item.id),\n",
" label_type=label.LabelType.VECTOR,\n",
" label_properties=['partialBuilding']\n",
" label_properties=['partialBuilding'],\n",
" \n",
" # Label classes is marked as required in 1.0.0-beta.2, so make it up.\n",
" # Once this PR is released, this can be removed:\n",
" # https://github.com/radiantearth/stac-spec/pull/905\n",
" label_classes=[label.LabelClasses.create(classes=['building'], name='partialBuilding')]\n",
" )\n",
" \n",
" label_item.ext.label.add_geojson_labels(href=label_uri)\n",
Expand All @@ -347,7 +381,7 @@
},
{
"cell_type": "code",
"execution_count": 15,
"execution_count": 17,
"metadata": {},
"outputs": [],
"source": [
Expand All @@ -356,7 +390,7 @@
},
{
"cell_type": "code",
"execution_count": 16,
"execution_count": 18,
"metadata": {},
"outputs": [
{
Expand All @@ -383,7 +417,7 @@
},
{
"cell_type": "code",
"execution_count": 17,
"execution_count": 19,
"metadata": {},
"outputs": [
{
Expand Down Expand Up @@ -428,12 +462,40 @@
},
{
"cell_type": "code",
"execution_count": 18,
"execution_count": 20,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"<Catalog id=spacenet>"
]
},
"execution_count": 20,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"spacenet_cog.normalize_hrefs('spacenet-cog-stac')"
]
},
{
"cell_type": "code",
"execution_count": 21,
"metadata": {},
"outputs": [],
"source": [
"spacenet_cog.validate_all()"
]
},
{
"cell_type": "code",
"execution_count": 22,
"metadata": {},
"outputs": [],
"source": [
"spacenet_cog.normalize_and_save('spacenet-cog-stac', \n",
" catalog_type=pystac.CatalogType.SELF_CONTAINED)"
"spacenet_cog.save(catalog_type=pystac.CatalogType.SELF_CONTAINED)"
]
},
{
Expand Down
14 changes: 14 additions & 0 deletions pystac/collection.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import collections.abc
lossyrob marked this conversation as resolved.
Show resolved Hide resolved
from datetime import datetime
import dateutil.parser
from dateutil import tz
Expand Down Expand Up @@ -305,6 +306,12 @@ class SpatialExtent:
2D Collection with only one bbox would be [[xmin, ymin, xmax, ymax]]
"""
def __init__(self, bboxes):
# A common mistake is to pass in a single bbox instead of a list of bboxes.
# Account for this by transforming the input in that case.
if isinstance(bboxes, collections.abc.Sequence):
if not isinstance(bboxes[0], collections.abc.Sequence):
lossyrob marked this conversation as resolved.
Show resolved Hide resolved
bboxes = [bboxes]

self.bboxes = bboxes

def to_dict(self):
Expand Down Expand Up @@ -388,6 +395,13 @@ class TemporalExtent:
Datetimes are required to be in UTC.
"""
def __init__(self, intervals):
# A common mistake is to pass in a single interval instead of a
# list of intervals. Account for this by transforming the input
# in that case.
if isinstance(intervals, collections.abc.Sequence):
if not isinstance(intervals[0], collections.abc.Sequence):
lossyrob marked this conversation as resolved.
Show resolved Hide resolved
intervals = [intervals]

for i in intervals:
if i[0] is None and i[1] is None:
raise STACError('TemporalExtent interval must have either '
Expand Down
2 changes: 1 addition & 1 deletion pystac/link.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ def get_href(self):
else:
href = self.target

if is_absolute_href(href) and self.owner is not None:
if href and is_absolute_href(href) and self.owner is not None:
href = make_relative_href(href, self.owner.get_self_href())
else:
href = self.get_absolute_href()
Expand Down
9 changes: 7 additions & 2 deletions pystac/validation/stac_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,17 @@ def validate(self, stac_dict, stac_object_type, stac_version, extensions, href=N
STACValidator implementation.
"""
results = []
core_result = self.validate_core(stac_dict, stac_object_type, stac_version, href)

# Pass the dict through JSON serialization and parsing, otherwise
# some valid properties can be marked as invalid (e.g. tuples in
# coordinate sequences for geometries).
json_dict = json.loads(json.dumps(stac_dict))
core_result = self.validate_core(json_dict, stac_object_type, stac_version, href)
if core_result is not None:
results.append(core_result)

for extension_id in extensions:
ext_result = self.validate_extension(stac_dict, stac_object_type, stac_version,
ext_result = self.validate_extension(json_dict, stac_object_type, stac_version,
extension_id, href)
if ext_result is not None:
results.append(ext_result)
Expand Down
3 changes: 2 additions & 1 deletion requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
ipython==7.16.1
lossyrob marked this conversation as resolved.
Show resolved Hide resolved
jsonschema==3.2.0
pylint==1.9.1
Sphinx==1.8.0
Expand All @@ -6,5 +7,5 @@ sphinxcontrib-fulltoc==1.2.0
sphinxcontrib-napoleon==0.7
flake8==3.8.*
yapf==0.28.*
nbsphinx==0.4.3
nbsphinx==0.7.1
coverage==5.2.*
41 changes: 35 additions & 6 deletions tests/test_collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from datetime import datetime

import pystac
from pystac.validation import (validate_dict, STACValidationError)
from pystac.validation import validate_dict
from pystac.serialization.identify import STACObjectType
from pystac import (Collection, Item, Extent, SpatialExtent, TemporalExtent, CatalogType)
from pystac.extensions.eo import Band
Expand Down Expand Up @@ -114,11 +114,6 @@ def test_multiple_extents(self):
cloned_ext = ext.clone()
self.assertDictEqual(cloned_ext.to_dict(), multi_ext_dict['extent'])

multi_ext_dict['extent']['spatial']['bbox'] = multi_ext_dict['extent']['spatial']['bbox'][0]
invalid_col = Collection.from_dict(multi_ext_dict)
with self.assertRaises(STACValidationError):
invalid_col.validate()

def test_extra_fields(self):
catalog = TestCases.test_case_2()
collection = catalog.get_child('1a8c1632-fa91-4a62-b33e-3a87c2ebdf16')
Expand Down Expand Up @@ -179,3 +174,37 @@ def test_update_extents(self):
self.assertEqual(
[[item2.common_metadata.start_datetime, base_extent.temporal.intervals[0][1]]],
collection.extent.temporal.intervals)


class ExtentTest(unittest.TestCase):
def test_spatial_allows_single_bbox(self):
temporal_extent = TemporalExtent(intervals=[[datetime.utcnow(), None]])
lossyrob marked this conversation as resolved.
Show resolved Hide resolved

# Pass in a single BBOX
spatial_extent = SpatialExtent(bboxes=RANDOM_BBOX)

collection_extent = Extent(spatial=spatial_extent, temporal=temporal_extent)

collection = Collection(id='test',
description='test',
lossyrob marked this conversation as resolved.
Show resolved Hide resolved
extent=collection_extent,
license='CC-BY-SA-4.0')
lossyrob marked this conversation as resolved.
Show resolved Hide resolved
collection.set_self_href('/usr/collection.json')
lossyrob marked this conversation as resolved.
Show resolved Hide resolved

collection.validate()

def test_temporal_allows_single_interval(self):
spatial_extent = SpatialExtent(bboxes=[RANDOM_BBOX])

# Pass in a single interval
temporal_extent = TemporalExtent(intervals=[datetime.utcnow(), None])
lossyrob marked this conversation as resolved.
Show resolved Hide resolved

collection_extent = Extent(spatial=spatial_extent, temporal=temporal_extent)

collection = Collection(id='test',
description='test',
extent=collection_extent,
license='CC-BY-SA-4.0')
collection.set_self_href('/usr/collection.json')
lossyrob marked this conversation as resolved.
Show resolved Hide resolved

collection.validate()
24 changes: 24 additions & 0 deletions tests/test_link.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from datetime import datetime
import unittest

import pystac
from tests.utils import (RANDOM_BBOX, RANDOM_GEOM)
schwehr marked this conversation as resolved.
Show resolved Hide resolved


class LinkTest(unittest.TestCase):
lossyrob marked this conversation as resolved.
Show resolved Hide resolved
def test_link_doest_fail_if_href_is_none(self):
"""Tests to cover a bug that was uncovered where a non-None HREF
lossyrob marked this conversation as resolved.
Show resolved Hide resolved
was supposed"""
catalog = pystac.Catalog(id='test', description='test')
lossyrob marked this conversation as resolved.
Show resolved Hide resolved
item = pystac.Item(id='test-item',
geometry=RANDOM_GEOM,
bbox=RANDOM_BBOX,
datetime=datetime.utcnow(),
lossyrob marked this conversation as resolved.
Show resolved Hide resolved
properties={'key': 'one'})
lossyrob marked this conversation as resolved.
Show resolved Hide resolved
catalog.add_item(item)
catalog.set_self_href('/some/href')
catalog.make_all_links_relative()

for link in catalog.links:
lossyrob marked this conversation as resolved.
Show resolved Hide resolved
if link.rel == 'item':
self.assertIsNone(link.get_href())
25 changes: 24 additions & 1 deletion tests/validation/test_validate.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from datetime import datetime
import json
import os
import shutil
import json
import unittest
from tempfile import TemporaryDirectory

Expand Down Expand Up @@ -108,3 +109,25 @@ def test_validate_all(self):

with self.assertRaises(STACValidationError):
pystac.validation.validate_all(stac_dict, new_cat_href)

def test_validates_geojson_with_tuple_coordinates(self):
"""This unit tests guards against a bug where if a geometry
dict has tuples instead of lists for the coordinate sequence,
which can be produced by shapely, then the geometry still passses
validation.
"""
geom = {
'type':
'Polygon',
'coordinates': (((-115.3057626, 36.1265426997), (-115.3057626, 36.1282976997),
lossyrob marked this conversation as resolved.
Show resolved Hide resolved
(-115.3075176, 36.1282976997), (-115.3075176, 36.1265426997),
(-115.3057626, 36.1265426997)), )
lossyrob marked this conversation as resolved.
Show resolved Hide resolved
}

item = pystac.Item(id='test-item',
geometry=geom,
bbox=[-115.308, 36.126, -115.305, 36.129],
datetime=datetime.utcnow(),
properties={'key': 'one'})
lossyrob marked this conversation as resolved.
Show resolved Hide resolved

self.assertIsNone(item.validate())