Skip to content

Commit 644a9f4

Browse files
author
Andy Gaither
committed
Allow use of feature references
1 parent dd1cb0e commit 644a9f4

File tree

9 files changed

+121
-68
lines changed

9 files changed

+121
-68
lines changed

docs/cli/cli-tips-tricks.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,18 @@ getting the geometry input for searching or clipping. Hand-editing GeoJSON is a
3939
people will open up a desktop tool like QGIS or ArcGIS Pro and save the file. But there are a few
4040
tools that can get you back into the CLI workflow more quickly.
4141

42+
#### Use the Features API
43+
Rather than using GeoJSON in the SDK, upload your GeoJSON to the [Features API](https://developers.planet.com/docs/apis/features/) and use references
44+
across the system with the sdk.
45+
References are used in the geometry block of our services like:
46+
```json
47+
"geometry":
48+
{
49+
"content": "pl:features/my/[collection-id]/[feature-id]",
50+
"type": "ref"
51+
}
52+
```
53+
4254
#### Draw with GeoJSON.io
4355

4456
One great tool for quickly drawing on a map and getting GeoJSON output is

planet/data_filter.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -238,9 +238,10 @@ def geometry_filter(geom: dict) -> dict:
238238
geom: GeoJSON describing the filter geometry, feature, or feature
239239
collection.
240240
"""
241-
return _field_filter('GeometryFilter',
242-
field_name='geometry',
243-
config=geojson.as_geom(geom))
241+
geom_filter = _field_filter('GeometryFilter',
242+
field_name='geometry',
243+
config=geojson.validate_geom_as_geojson(geom))
244+
return geom_filter
244245

245246

246247
def number_in_filter(field_name: str, values: List[float]) -> dict:

planet/exceptions.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,3 +90,7 @@ class PagingError(ClientError):
9090

9191
class GeoJSONError(ClientError):
9292
"""Errors that occur due to invalid GeoJSON"""
93+
94+
class FeatureError(ClientError):
95+
"""Errors that occur due to incorrectly formatted feature reference"""
96+

planet/geojson.py

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
1313
# License for the specific language governing permissions and limitations under
1414
# the License.
15-
"""Functionality for interacting with GeoJSON."""
15+
"""Functionality for interacting with GeoJSON and planet references."""
1616
import json
1717
import logging
1818
import typing
@@ -21,14 +21,14 @@
2121
from jsonschema import Draft7Validator
2222

2323
from .constants import DATA_DIR
24-
from .exceptions import GeoJSONError
24+
from .exceptions import GeoJSONError, FeatureError
2525

26-
GEOJSON_TYPES = ['Feature']
26+
GEOJSON_TYPES = ["Feature"]
2727

2828
LOGGER = logging.getLogger(__name__)
2929

3030

31-
def as_geom(data: dict) -> dict:
31+
def as_geom_or_ref(data: dict) -> dict:
3232
"""Extract the geometry from GeoJSON and validate.
3333
3434
Parameters:
@@ -42,13 +42,30 @@ def as_geom(data: dict) -> dict:
4242
or FeatureCollection or if more than one Feature is in a
4343
FeatureCollection.
4444
"""
45-
geom = geom_from_geojson(data)
46-
validate_geom(geom)
47-
return geom
45+
geom_type = data['type']
46+
if geom_type == 'ref':
47+
return as_ref(data)
48+
else:
49+
geom = geom_from_geojson(data)
50+
validate_geom_as_geojson(geom)
51+
return geom
52+
53+
54+
def as_ref(data: dict) -> dict:
55+
geom_type = data['type']
56+
if geom_type.lower() != 'ref':
57+
raise FeatureError(
58+
f'Invalid geometry reference: {geom_type} is not a reference (the type should be "ref").'
59+
)
60+
if "content" not in data:
61+
raise FeatureError(
62+
'Invalid geometry reference: Missing content block that contains the reference.'
63+
)
64+
return data
4865

4966

5067
def as_polygon(data: dict) -> dict:
51-
geom = as_geom(data)
68+
geom = as_geom_or_ref(data)
5269
geom_type = geom['type']
5370
if geom_type.lower() != 'polygon':
5471
raise GeoJSONError(
@@ -75,7 +92,7 @@ def geom_from_geojson(data: dict) -> dict:
7592
else:
7693
try:
7794
# feature
78-
ret = as_geom(data['geometry'])
95+
ret = as_geom_or_ref(data['geometry'])
7996
except KeyError:
8097
try:
8198
# FeatureCollection
@@ -88,11 +105,11 @@ def geom_from_geojson(data: dict) -> dict:
88105
'FeatureCollection has multiple features. Only one feature'
89106
' can be used to get geometry.')
90107

91-
ret = as_geom(features[0])
108+
ret = as_geom_or_ref(features[0])
92109
return ret
93110

94111

95-
def validate_geom(data: dict):
112+
def validate_geom_as_geojson(data: dict):
96113
"""Validate GeoJSON geometry.
97114
98115
Parameters:
@@ -101,23 +118,26 @@ def validate_geom(data: dict):
101118
Raises:
102119
planet.exceptions.GeoJSONError: If data is not a valid GeoJSON
103120
geometry.
121+
Returns:
122+
GeoJSON
104123
"""
124+
data = geom_from_geojson(data)
105125
if 'type' not in data:
106-
raise GeoJSONError("Missing 'type' key.")
126+
raise GeoJSONError('Missing "type" key.')
107127
if 'coordinates' not in data:
108-
raise GeoJSONError("Missing 'coordinates' key.")
128+
raise GeoJSONError('Missing "coordinates" key.')
109129

110130
try:
111-
cls = getattr(gj, data["type"])
112-
obj = cls(data["coordinates"])
131+
cls = getattr(gj, data['type'])
132+
obj = cls(data['coordinates'])
113133
if not obj.is_valid:
114134
raise GeoJSONError(obj.errors())
115135
except AttributeError as err:
116-
raise GeoJSONError("Not a GeoJSON geometry type") from err
136+
raise GeoJSONError('Not a GeoJSON geometry type') from err
117137
except ValueError as err:
118-
raise GeoJSONError("Not a GeoJSON coordinate value") from err
138+
raise GeoJSONError('Not a GeoJSON coordinate value') from err
119139

120-
return
140+
return data
121141

122142

123143
def as_featurecollection(features: typing.List[dict]) -> dict:

planet/order_request.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -356,9 +356,9 @@ def clip_tool(aoi: dict) -> dict:
356356
planet.exceptions.ClientError: If GeoJSON is not a valid polygon or
357357
multipolygon.
358358
"""
359-
valid_types = ['Polygon', 'MultiPolygon']
359+
valid_types = ['Polygon', 'MultiPolygon', 'ref']
360360

361-
geom = geojson.as_geom(aoi)
361+
geom = geojson.as_geom_or_ref(aoi)
362362
if geom['type'].lower() not in [v.lower() for v in valid_types]:
363363
raise ClientError(
364364
f'Invalid geometry type: {geom["type"]} is not in {valid_types}.')

planet/subscription_request.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ def catalog_source(
250250
parameters = {
251251
"item_types": item_types,
252252
"asset_types": asset_types,
253-
"geometry": geojson.as_geom(dict(geometry)),
253+
"geometry": geojson.as_geom_or_ref(dict(geometry)),
254254
}
255255

256256
try:
@@ -355,7 +355,7 @@ def planetary_variable_source(
355355

356356
parameters = {
357357
"id": var_id,
358-
"geometry": geojson.as_geom(dict(geometry)),
358+
"geometry": geojson.as_geom_or_ref(dict(geometry)),
359359
}
360360

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

601-
geom = geojson.as_geom(dict(aoi))
601+
geom = geojson.as_geom_or_ref(dict(aoi))
602602
if geom['type'].lower() not in [v.lower() for v in valid_types]:
603603
raise ClientError(
604604
f'Invalid geometry type: {geom["type"]} is not in {valid_types}.')

tests/conftest.py

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -108,14 +108,24 @@ def geom_geojson():
108108
# these need to be tuples, not list, or they will be changed
109109
# by shapely
110110
return {
111-
"type":
112-
"Polygon",
113-
"coordinates":
114-
[[[37.791595458984375, 14.84923123791421],
115-
[37.90214538574219, 14.84923123791421],
116-
[37.90214538574219, 14.945448293647944],
117-
[37.791595458984375, 14.945448293647944],
118-
[37.791595458984375, 14.84923123791421]]]
111+
"type": "Polygon",
112+
"coordinates": [
113+
[
114+
[37.791595458984375, 14.84923123791421],
115+
[37.90214538574219, 14.84923123791421],
116+
[37.90214538574219, 14.945448293647944],
117+
[37.791595458984375, 14.945448293647944],
118+
[37.791595458984375, 14.84923123791421],
119+
]
120+
],
121+
} # yapf: disable
122+
123+
124+
@pytest.fixture
125+
def geom_reference():
126+
return {
127+
"type": "ref",
128+
"content": "pl:features/my/water-fields-RqB0NZ5/rmQEGqm",
119129
} # yapf: disable
120130

121131

tests/integration/test_subscriptions_cli.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -373,19 +373,21 @@ def test_request_catalog_success(invoke, geom_geojson):
373373
@res_api_mock
374374
def test_subscriptions_results_csv(invoke):
375375
"""Get results as CSV."""
376-
result = invoke(['results', 'test', '--csv'])
376+
result = invoke(["results", "test", "--csv"])
377377
assert result.exit_code == 0 # success.
378-
assert result.output.splitlines() == ['id,status', '1234-abcd,SUCCESS']
378+
assert result.output.splitlines() == ["id,status", "1234-abcd,SUCCESS"]
379379

380380

381-
def test_request_pv_success(invoke, geom_geojson):
381+
@pytest.mark.parametrize("geom", ["geom_geojson", "geom_reference"])
382+
def test_request_pv_success(invoke, geom, request):
382383
"""Request-pv command succeeds"""
384+
geom = request.getfixturevalue(geom)
383385
result = invoke([
384-
'request-pv',
385-
'--var-type=biomass_proxy',
386-
'--var-id=BIOMASS-PROXY_V3.0_10',
387-
f"--geometry={json.dumps(geom_geojson)}",
388-
'--start-time=2021-03-01T00:00:00'
386+
"request-pv",
387+
"--var-type=biomass_proxy",
388+
"--var-id=BIOMASS-PROXY_V3.0_10",
389+
f"--geometry={json.dumps(geom)}",
390+
"--start-time=2021-03-01T00:00:00",
389391
])
390392

391393
assert result.exit_code == 0 # success.

tests/unit/test_geojson.py

Lines changed: 29 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def test_geom_from_geojson_success(geom_geojson,
4040
feature_geojson,
4141
featurecollection_geojson,
4242
assert_geom_equal):
43-
ggeo = geojson.as_geom(geom_geojson)
43+
ggeo = geojson.as_geom_or_ref(geom_geojson)
4444
assert_geom_equal(ggeo, geom_geojson)
4545

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

5252

5353
def test_geom_from_geojson_no_geometry(feature_geojson):
54-
feature_geojson.pop('geometry')
54+
feature_geojson.pop("geometry")
5555
with pytest.raises(exceptions.GeoJSONError):
5656
_ = geojson.geom_from_geojson(feature_geojson)
5757

5858

5959
def test_geom_from_geojson_missing_coordinates(geom_geojson):
60-
geom_geojson.pop('coordinates')
60+
geom_geojson.pop("coordinates")
6161
with pytest.raises(exceptions.GeoJSONError):
6262
_ = geojson.geom_from_geojson(geom_geojson)
6363

6464

6565
def test_geom_from_geojson_missing_type(geom_geojson):
66-
geom_geojson.pop('type')
66+
geom_geojson.pop("type")
6767
with pytest.raises(exceptions.GeoJSONError):
6868
_ = geojson.geom_from_geojson(geom_geojson)
6969

7070

7171
def test_geom_from_geojson_multiple_features(featurecollection_geojson):
7272
# duplicate the feature
7373
featurecollection_geojson[
74-
'features'] = 2 * featurecollection_geojson['features']
74+
"features"] = 2 * featurecollection_geojson["features"]
7575
with pytest.raises(geojson.GeoJSONError):
7676
_ = geojson.geom_from_geojson(featurecollection_geojson)
7777

7878

79-
def test_validate_geom_invalid_type(geom_geojson):
80-
geom_geojson['type'] = 'invalid'
79+
def test_validate_geom_as_geojson_invalid_type(geom_geojson):
80+
geom_geojson["type"] = "invalid"
8181
with pytest.raises(exceptions.GeoJSONError):
82-
_ = geojson.validate_geom(geom_geojson)
82+
_ = geojson.validate_geom_as_geojson(geom_geojson)
8383

8484

85-
def test_validate_geom_wrong_type(geom_geojson):
86-
geom_geojson['type'] = 'point'
85+
def test_validate_geom_as_geojson_wrong_type(geom_geojson):
86+
geom_geojson["type"] = "point"
8787
with pytest.raises(exceptions.GeoJSONError):
88-
_ = geojson.validate_geom(geom_geojson)
88+
_ = geojson.validate_geom_as_geojson(geom_geojson)
8989

9090

91-
def test_validate_geom_invalid_coordinates(geom_geojson):
92-
geom_geojson['coordinates'] = 'invalid'
91+
def test_validate_geom_as_geojson_invalid_coordinates(geom_geojson):
92+
geom_geojson["coordinates"] = "invalid"
9393
with pytest.raises(exceptions.GeoJSONError):
94-
_ = geojson.validate_geom(geom_geojson)
94+
_ = geojson.validate_geom_as_geojson(geom_geojson)
9595

9696

97-
def test_validate_geom_empty_coordinates(geom_geojson):
98-
geom_geojson['coordinates'] = []
99-
_ = geojson.validate_geom(geom_geojson)
97+
def test_validate_geom_as_geojson_empty_coordinates(geom_geojson):
98+
geom_geojson["coordinates"] = []
99+
_ = geojson.validate_geom_as_geojson(geom_geojson)
100100

101101

102-
def test_as_geom(geom_geojson):
103-
assert geojson.as_geom(geom_geojson) == geom_geojson
102+
def test_as_geom_or_ref(geom_geojson):
103+
assert geojson.as_geom_or_ref(geom_geojson) == geom_geojson
104104

105105

106106
def test_as_polygon(geom_geojson):
107107
assert geojson.as_polygon(geom_geojson) == geom_geojson
108108

109109

110+
def test_as_reference(geom_reference):
111+
assert geojson.as_ref(geom_reference) == geom_reference
112+
113+
110114
def test_as_polygon_wrong_type(point_geom_geojson):
111115
with pytest.raises(exceptions.GeoJSONError):
112116
_ = geojson.as_polygon(point_geom_geojson)
113117

114118

115119
def test_as_featurecollection_success(feature_geojson):
116120
feature2 = feature_geojson.copy()
117-
feature2['properties'] = {'foo': 'bar'}
121+
feature2["properties"] = {"foo": "bar"}
118122
values = [feature_geojson, feature2]
119123
res = geojson.as_featurecollection(values)
120124

121-
expected = {'type': 'FeatureCollection', 'features': values}
125+
expected = {"type": "FeatureCollection", "features": values}
122126
assert res == expected
123127

124128

125129
def test__is_instance_of_success(feature_geojson):
126-
assert geojson._is_instance_of(feature_geojson, 'Feature')
130+
assert geojson._is_instance_of(feature_geojson, "Feature")
127131

128132
feature2 = feature_geojson.copy()
129-
feature2['properties'] = {'foo': 'bar'}
130-
assert geojson._is_instance_of(feature2, 'Feature')
133+
feature2["properties"] = {"foo": "bar"}
134+
assert geojson._is_instance_of(feature2, "Feature")
131135

132136

133137
def test__is_instance_of_does_not_exist(feature_geojson):
134138
with pytest.raises(exceptions.GeoJSONError):
135-
geojson._is_instance_of(feature_geojson, 'Foobar')
139+
geojson._is_instance_of(feature_geojson, "Foobar")

0 commit comments

Comments
 (0)