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
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,17 +81,17 @@ 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
```

Note that you may have to use `yapf3` explicitly depending on your environment.

You can also run the `./scripts/test` script to check flake8 and yapf.

### Documentation
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 @@
from collections import abc
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, abc.Sequence):
if not isinstance(bboxes[0], abc.Sequence):
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, abc.Sequence):
if not isinstance(intervals[0], abc.Sequence):
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.*
47 changes: 38 additions & 9 deletions tests/test_collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
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
from pystac.utils import datetime_to_str
from tests.utils import (TestCases, RANDOM_GEOM, RANDOM_BBOX)

TEST_DATETIME = datetime(2020, 3, 14, 16, 32)


class CollectionTest(unittest.TestCase):
def test_spatial_extent_from_coordinates(self):
Expand All @@ -27,14 +29,14 @@ def test_eo_items_are_heritable(self):
item1 = Item(id='test-item-1',
geometry=RANDOM_GEOM,
bbox=RANDOM_BBOX,
datetime=datetime.utcnow(),
datetime=TEST_DATETIME,
properties={'key': 'one'},
stac_extensions=['eo', 'commons'])

item2 = Item(id='test-item-2',
geometry=RANDOM_GEOM,
bbox=RANDOM_BBOX,
datetime=datetime.utcnow(),
datetime=TEST_DATETIME,
properties={'key': 'two'},
stac_extensions=['eo', 'commons'])

Expand Down Expand Up @@ -114,11 +116,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 @@ -147,7 +144,7 @@ def test_update_extents(self):
item1 = Item(id='test-item-1',
geometry=RANDOM_GEOM,
bbox=[-180, -90, 180, 90],
datetime=datetime.utcnow(),
datetime=TEST_DATETIME,
properties={'key': 'one'},
stac_extensions=['eo', 'commons'])

Expand Down Expand Up @@ -179,3 +176,35 @@ 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=[[TEST_DATETIME, None]])

# 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 desc', extent=collection_extent)

# HREF required by validation
collection.set_self_href('https://example.com/collection.json')

collection.validate()

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

# Pass in a single interval
temporal_extent = TemporalExtent(intervals=[TEST_DATETIME, None])

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

collection = Collection(id='test', description='test desc', extent=collection_extent)

# HREF required by validation
collection.set_self_href('https://example.com/collection.json')

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

TEST_DATETIME = datetime(2020, 3, 14, 16, 32)


class LinkTest(unittest.TestCase):
lossyrob marked this conversation as resolved.
Show resolved Hide resolved
def test_link_does_not_fail_if_href_is_none(self):
"""Test to ensure get_href does not fail when the href is None"""
catalog = pystac.Catalog(id='test', description='test desc')
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={})
catalog.add_item(item)
catalog.set_self_href('/some/href')
catalog.make_all_links_relative()

link = catalog.get_single_link('item')
self.assertIsNone(link.get_href())
Loading