Skip to content

Commit

Permalink
Allow use of feature references
Browse files Browse the repository at this point in the history
  • Loading branch information
Andy Gaither committed Apr 15, 2024
1 parent dd1cb0e commit f94d68c
Show file tree
Hide file tree
Showing 9 changed files with 121 additions and 68 deletions.
12 changes: 12 additions & 0 deletions docs/cli/cli-tips-tricks.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,18 @@ getting the geometry input for searching or clipping. Hand-editing GeoJSON is a
people will open up a desktop tool like QGIS or ArcGIS Pro and save the file. But there are a few
tools that can get you back into the CLI workflow more quickly.

#### Use the Features API
Rather than using GeoJSON in the SDK, upload your GeoJSON to the [Features API](https://developers.planet.com/docs/apis/features/) and use references
across the system with the sdk.
References are used in the geometry block of our services like:
```json
"geometry":
{
"content": "pl:features/my/[collection-id]/[feature-id]",
"type": "ref"
}
```

#### Draw with GeoJSON.io

One great tool for quickly drawing on a map and getting GeoJSON output is
Expand Down
7 changes: 4 additions & 3 deletions planet/data_filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,9 +238,10 @@ def geometry_filter(geom: dict) -> dict:
geom: GeoJSON describing the filter geometry, feature, or feature
collection.
"""
return _field_filter('GeometryFilter',
field_name='geometry',
config=geojson.as_geom(geom))
geom_filter = _field_filter('GeometryFilter',
field_name='geometry',
config=geojson.validate_geom_as_geojson(geom))
return geom_filter


def number_in_filter(field_name: str, values: List[float]) -> dict:
Expand Down
4 changes: 4 additions & 0 deletions planet/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,7 @@ class PagingError(ClientError):

class GeoJSONError(ClientError):
"""Errors that occur due to invalid GeoJSON"""


class FeatureError(ClientError):
"""Errors that occur due to incorrectly formatted feature reference"""
56 changes: 38 additions & 18 deletions planet/geojson.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
"""Functionality for interacting with GeoJSON."""
"""Functionality for interacting with GeoJSON and planet references."""
import json
import logging
import typing
Expand All @@ -21,14 +21,14 @@
from jsonschema import Draft7Validator

from .constants import DATA_DIR
from .exceptions import GeoJSONError
from .exceptions import GeoJSONError, FeatureError

GEOJSON_TYPES = ['Feature']
GEOJSON_TYPES = ["Feature"]

LOGGER = logging.getLogger(__name__)


def as_geom(data: dict) -> dict:
def as_geom_or_ref(data: dict) -> dict:
"""Extract the geometry from GeoJSON and validate.
Parameters:
Expand All @@ -42,13 +42,30 @@ def as_geom(data: dict) -> dict:
or FeatureCollection or if more than one Feature is in a
FeatureCollection.
"""
geom = geom_from_geojson(data)
validate_geom(geom)
return geom
geom_type = data['type']
if geom_type == 'ref':
return as_ref(data)
else:
geom = geom_from_geojson(data)
validate_geom_as_geojson(geom)
return geom


def as_ref(data: dict) -> dict:
geom_type = data['type']
if geom_type.lower() != 'ref':
raise FeatureError(
f'Invalid geometry reference: {geom_type} is not a reference (the type should be "ref").'
)
if "content" not in data:
raise FeatureError(
'Invalid geometry reference: Missing content block that contains the reference.'
)
return data


def as_polygon(data: dict) -> dict:
geom = as_geom(data)
geom = as_geom_or_ref(data)
geom_type = geom['type']
if geom_type.lower() != 'polygon':
raise GeoJSONError(
Expand All @@ -75,7 +92,7 @@ def geom_from_geojson(data: dict) -> dict:
else:
try:
# feature
ret = as_geom(data['geometry'])
ret = as_geom_or_ref(data['geometry'])
except KeyError:
try:
# FeatureCollection
Expand All @@ -88,11 +105,11 @@ def geom_from_geojson(data: dict) -> dict:
'FeatureCollection has multiple features. Only one feature'
' can be used to get geometry.')

ret = as_geom(features[0])
ret = as_geom_or_ref(features[0])
return ret


def validate_geom(data: dict):
def validate_geom_as_geojson(data: dict):
"""Validate GeoJSON geometry.
Parameters:
Expand All @@ -101,23 +118,26 @@ def validate_geom(data: dict):
Raises:
planet.exceptions.GeoJSONError: If data is not a valid GeoJSON
geometry.
Returns:
GeoJSON
"""
data = geom_from_geojson(data)
if 'type' not in data:
raise GeoJSONError("Missing 'type' key.")
raise GeoJSONError('Missing "type" key.')
if 'coordinates' not in data:
raise GeoJSONError("Missing 'coordinates' key.")
raise GeoJSONError('Missing "coordinates" key.')

try:
cls = getattr(gj, data["type"])
obj = cls(data["coordinates"])
cls = getattr(gj, data['type'])
obj = cls(data['coordinates'])
if not obj.is_valid:
raise GeoJSONError(obj.errors())
except AttributeError as err:
raise GeoJSONError("Not a GeoJSON geometry type") from err
raise GeoJSONError('Not a GeoJSON geometry type') from err
except ValueError as err:
raise GeoJSONError("Not a GeoJSON coordinate value") from err
raise GeoJSONError('Not a GeoJSON coordinate value') from err

return
return data


def as_featurecollection(features: typing.List[dict]) -> dict:
Expand Down
4 changes: 2 additions & 2 deletions planet/order_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -356,9 +356,9 @@ def clip_tool(aoi: dict) -> dict:
planet.exceptions.ClientError: If GeoJSON is not a valid polygon or
multipolygon.
"""
valid_types = ['Polygon', 'MultiPolygon']
valid_types = ['Polygon', 'MultiPolygon', 'ref']

geom = geojson.as_geom(aoi)
geom = geojson.as_geom_or_ref(aoi)
if geom['type'].lower() not in [v.lower() for v in valid_types]:
raise ClientError(
f'Invalid geometry type: {geom["type"]} is not in {valid_types}.')
Expand Down
8 changes: 4 additions & 4 deletions planet/subscription_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ def catalog_source(
parameters = {
"item_types": item_types,
"asset_types": asset_types,
"geometry": geojson.as_geom(dict(geometry)),
"geometry": geojson.as_geom_or_ref(dict(geometry)),
}

try:
Expand Down Expand Up @@ -355,7 +355,7 @@ def planetary_variable_source(

parameters = {
"id": var_id,
"geometry": geojson.as_geom(dict(geometry)),
"geometry": geojson.as_geom_or_ref(dict(geometry)),
}

try:
Expand Down Expand Up @@ -596,9 +596,9 @@ def clip_tool(aoi: Mapping) -> dict:
planet.exceptions.ClientError: If aoi is not a valid polygon or
multipolygon.
"""
valid_types = ['Polygon', 'MultiPolygon']
valid_types = ['Polygon', 'MultiPolygon', 'ref']

geom = geojson.as_geom(dict(aoi))
geom = geojson.as_geom_or_ref(dict(aoi))
if geom['type'].lower() not in [v.lower() for v in valid_types]:
raise ClientError(
f'Invalid geometry type: {geom["type"]} is not in {valid_types}.')
Expand Down
26 changes: 18 additions & 8 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,14 +108,24 @@ def geom_geojson():
# these need to be tuples, not list, or they will be changed
# by shapely
return {
"type":
"Polygon",
"coordinates":
[[[37.791595458984375, 14.84923123791421],
[37.90214538574219, 14.84923123791421],
[37.90214538574219, 14.945448293647944],
[37.791595458984375, 14.945448293647944],
[37.791595458984375, 14.84923123791421]]]
"type": "Polygon",
"coordinates": [
[
[37.791595458984375, 14.84923123791421],
[37.90214538574219, 14.84923123791421],
[37.90214538574219, 14.945448293647944],
[37.791595458984375, 14.945448293647944],
[37.791595458984375, 14.84923123791421],
]
],
} # yapf: disable


@pytest.fixture
def geom_reference():
return {
"type": "ref",
"content": "pl:features/my/water-fields-RqB0NZ5/rmQEGqm",
} # yapf: disable


Expand Down
18 changes: 10 additions & 8 deletions tests/integration/test_subscriptions_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -373,19 +373,21 @@ def test_request_catalog_success(invoke, geom_geojson):
@res_api_mock
def test_subscriptions_results_csv(invoke):
"""Get results as CSV."""
result = invoke(['results', 'test', '--csv'])
result = invoke(["results", "test", "--csv"])
assert result.exit_code == 0 # success.
assert result.output.splitlines() == ['id,status', '1234-abcd,SUCCESS']
assert result.output.splitlines() == ["id,status", "1234-abcd,SUCCESS"]


def test_request_pv_success(invoke, geom_geojson):
@pytest.mark.parametrize("geom", ["geom_geojson", "geom_reference"])
def test_request_pv_success(invoke, geom, request):
"""Request-pv command succeeds"""
geom = request.getfixturevalue(geom)
result = invoke([
'request-pv',
'--var-type=biomass_proxy',
'--var-id=BIOMASS-PROXY_V3.0_10',
f"--geometry={json.dumps(geom_geojson)}",
'--start-time=2021-03-01T00:00:00'
"request-pv",
"--var-type=biomass_proxy",
"--var-id=BIOMASS-PROXY_V3.0_10",
f"--geometry={json.dumps(geom)}",
"--start-time=2021-03-01T00:00:00",
])

assert result.exit_code == 0 # success.
Expand Down
54 changes: 29 additions & 25 deletions tests/unit/test_geojson.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def test_geom_from_geojson_success(geom_geojson,
feature_geojson,
featurecollection_geojson,
assert_geom_equal):
ggeo = geojson.as_geom(geom_geojson)
ggeo = geojson.as_geom_or_ref(geom_geojson)
assert_geom_equal(ggeo, geom_geojson)

fgeo = geojson.geom_from_geojson(feature_geojson)
Expand All @@ -51,85 +51,89 @@ def test_geom_from_geojson_success(geom_geojson,


def test_geom_from_geojson_no_geometry(feature_geojson):
feature_geojson.pop('geometry')
feature_geojson.pop("geometry")
with pytest.raises(exceptions.GeoJSONError):
_ = geojson.geom_from_geojson(feature_geojson)


def test_geom_from_geojson_missing_coordinates(geom_geojson):
geom_geojson.pop('coordinates')
geom_geojson.pop("coordinates")
with pytest.raises(exceptions.GeoJSONError):
_ = geojson.geom_from_geojson(geom_geojson)


def test_geom_from_geojson_missing_type(geom_geojson):
geom_geojson.pop('type')
geom_geojson.pop("type")
with pytest.raises(exceptions.GeoJSONError):
_ = geojson.geom_from_geojson(geom_geojson)


def test_geom_from_geojson_multiple_features(featurecollection_geojson):
# duplicate the feature
featurecollection_geojson[
'features'] = 2 * featurecollection_geojson['features']
"features"] = 2 * featurecollection_geojson["features"]
with pytest.raises(geojson.GeoJSONError):
_ = geojson.geom_from_geojson(featurecollection_geojson)


def test_validate_geom_invalid_type(geom_geojson):
geom_geojson['type'] = 'invalid'
def test_validate_geom_as_geojson_invalid_type(geom_geojson):
geom_geojson["type"] = "invalid"
with pytest.raises(exceptions.GeoJSONError):
_ = geojson.validate_geom(geom_geojson)
_ = geojson.validate_geom_as_geojson(geom_geojson)


def test_validate_geom_wrong_type(geom_geojson):
geom_geojson['type'] = 'point'
def test_validate_geom_as_geojson_wrong_type(geom_geojson):
geom_geojson["type"] = "point"
with pytest.raises(exceptions.GeoJSONError):
_ = geojson.validate_geom(geom_geojson)
_ = geojson.validate_geom_as_geojson(geom_geojson)


def test_validate_geom_invalid_coordinates(geom_geojson):
geom_geojson['coordinates'] = 'invalid'
def test_validate_geom_as_geojson_invalid_coordinates(geom_geojson):
geom_geojson["coordinates"] = "invalid"
with pytest.raises(exceptions.GeoJSONError):
_ = geojson.validate_geom(geom_geojson)
_ = geojson.validate_geom_as_geojson(geom_geojson)


def test_validate_geom_empty_coordinates(geom_geojson):
geom_geojson['coordinates'] = []
_ = geojson.validate_geom(geom_geojson)
def test_validate_geom_as_geojson_empty_coordinates(geom_geojson):
geom_geojson["coordinates"] = []
_ = geojson.validate_geom_as_geojson(geom_geojson)


def test_as_geom(geom_geojson):
assert geojson.as_geom(geom_geojson) == geom_geojson
def test_as_geom_or_ref(geom_geojson):
assert geojson.as_geom_or_ref(geom_geojson) == geom_geojson


def test_as_polygon(geom_geojson):
assert geojson.as_polygon(geom_geojson) == geom_geojson


def test_as_reference(geom_reference):
assert geojson.as_ref(geom_reference) == geom_reference


def test_as_polygon_wrong_type(point_geom_geojson):
with pytest.raises(exceptions.GeoJSONError):
_ = geojson.as_polygon(point_geom_geojson)


def test_as_featurecollection_success(feature_geojson):
feature2 = feature_geojson.copy()
feature2['properties'] = {'foo': 'bar'}
feature2["properties"] = {"foo": "bar"}
values = [feature_geojson, feature2]
res = geojson.as_featurecollection(values)

expected = {'type': 'FeatureCollection', 'features': values}
expected = {"type": "FeatureCollection", "features": values}
assert res == expected


def test__is_instance_of_success(feature_geojson):
assert geojson._is_instance_of(feature_geojson, 'Feature')
assert geojson._is_instance_of(feature_geojson, "Feature")

feature2 = feature_geojson.copy()
feature2['properties'] = {'foo': 'bar'}
assert geojson._is_instance_of(feature2, 'Feature')
feature2["properties"] = {"foo": "bar"}
assert geojson._is_instance_of(feature2, "Feature")


def test__is_instance_of_does_not_exist(feature_geojson):
with pytest.raises(exceptions.GeoJSONError):
geojson._is_instance_of(feature_geojson, 'Foobar')
geojson._is_instance_of(feature_geojson, "Foobar")

0 comments on commit f94d68c

Please sign in to comment.