Skip to content

Commit ca33d85

Browse files
Features client (planetlabs#1073)
* WIP Features client: list_collections, create_collection, list_features, create_features * Apply suggestions from code review * added docstrings for features methods * updates to methods, tests, docs and sync client * fix typo in features client docstring * features cli and tests * make sure create tests are post requests * update cli commands * add compact mode to collections list * add get_feature functionality * use items instead of "features" * combine cli decorators, remove geojson from docs * update yapf config and yapf everything * yapf everything for new rule * fix top level values * use Union instead of union operator * update orders formatting * fix formatting with yapf v0.43.0 * pin yapf to 0.43.0 * don't use a session fixture for features tests * explicitly set API Key for test sessions * split out collections and items into subgroups * lint fixes * add limit to list functions * remove geojson from docs * add limit to sync features client * format * update help text * add Features API to sdk guides * update actions/cache gh action * refactor decorator stack * remove geom type handling, additions to sdk guide * lint fix * add note about collection_id * add note about reservations * link to features API docs * assertion no longer always true * put back Geometry > Feature upgrade * lint --------- Co-authored-by: Steve Hillier <steve.hillier@planet.com>
1 parent dc51fcd commit ca33d85

29 files changed

+1355
-219
lines changed

.github/workflows/autopublish-testpypi.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jobs:
1919
python-version: 3.12
2020

2121
- name: Pip cache
22-
uses: actions/cache@v2
22+
uses: actions/cache@v4
2323
with:
2424
path: ~/.cache/pip
2525
key: ${{ runner.os }}-pip

.github/workflows/publish-pypi.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ jobs:
1717
python-version: 3.12
1818

1919
- name: Pip cache
20-
uses: actions/cache@v2
20+
uses: actions/cache@v4
2121
with:
2222
path: ~/.cache/pip
2323
key: ${{ runner.os }}-pip

.github/workflows/test.yml

+5-5
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
with:
1515
python-version: 3.12
1616
- name: Pip cache
17-
uses: actions/cache@v2
17+
uses: actions/cache@v4
1818
with:
1919
path: ~/.cache/pip
2020
key: ${{ runner.os }}-pip
@@ -36,7 +36,7 @@ jobs:
3636
with:
3737
python-version: 3.12
3838
- name: Pip cache
39-
uses: actions/cache@v2
39+
uses: actions/cache@v4
4040
with:
4141
path: ~/.cache/pip
4242
key: ${{ runner.os }}-pip
@@ -64,7 +64,7 @@ jobs:
6464
python-version: ${{ matrix.python-version }}
6565
allow-prereleases: true
6666
- name: Pip cache
67-
uses: actions/cache@v2
67+
uses: actions/cache@v4
6868
with:
6969
path: ~/.cache/pip
7070
key: ${{ runner.os }}-pip
@@ -86,7 +86,7 @@ jobs:
8686
with:
8787
python-version: 3.12
8888
- name: Pip cache
89-
uses: actions/cache@v2
89+
uses: actions/cache@v4
9090
with:
9191
path: ~/.cache/pip
9292
key: ${{ runner.os }}-pip
@@ -108,7 +108,7 @@ jobs:
108108
with:
109109
python-version: 3.12
110110
- name: Pip cache
111-
uses: actions/cache@v2
111+
uses: actions/cache@v4
112112
with:
113113
path: ~/.cache/pip
114114
key: ${{ runner.os }}-pip

docs/python/async-sdk-guide.md

+53
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,59 @@ async def download_and_validate():
458458
cl.validate_checksum(asset, path)
459459
```
460460

461+
### Features API Collections and Features
462+
463+
The Python SDK now supports Features API Collections and Features (note: in the SDK and API, Features are often referred to as items in a collection).
464+
465+
Collections and Features/items that you create in in the SDK will be visible in Features API and Features Manager.
466+
467+
#### Creating a collection
468+
469+
You can use the Python SDK to create Features API collections.
470+
471+
```python
472+
async with Session() as sess:
473+
client = FeaturesClient(sess)
474+
new_collection = await client.create_collection(title="my collection", description="a new collection")
475+
```
476+
477+
#### Listing collections
478+
479+
```python
480+
async with Session() as sess:
481+
client = FeaturesClient(sess)
482+
collections = client.list_collections()
483+
async for collection in collections:
484+
print(collection)
485+
```
486+
487+
#### Listing features/items in a collection
488+
489+
```python
490+
async with Session() as sess:
491+
client = FeaturesClient(sess)
492+
items = client.list_items(collection_id)
493+
async for item in items:
494+
print(items)
495+
```
496+
497+
#### Using items as geometries for other methods
498+
499+
You can pass collection items/features directly to other SDK methods. Any method that requires a geometry will accept
500+
a Features API Feature.
501+
502+
```python
503+
async with Session() as sess:
504+
client = FeaturesClient(sess)
505+
items = client.list_items(collection_id)
506+
example_feature = await anext(items)
507+
508+
data_client = DataClient(sess)
509+
results = data_client.search(["PSScene"], geometry=example_feature, limit=1)
510+
async for result in results:
511+
print(result)
512+
```
513+
461514
## API Exceptions
462515

463516
When errors occur, the Planet SDK for Python exception hierarchy is as follows:

docs/python/sdk-guide.md

+50
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,56 @@ delivery = amazon_s3(ACCESS_KEY_ID, SECRET_ACCESS_KEY, "test", "us-east-1")
264264
subscription = pl.subscriptions.create_subscription(request)
265265
```
266266

267+
### Features API Collections and Features
268+
269+
The Python SDK now supports [Features API](https://developers.planet.com/docs/apis/features/) Collections and Features (note: in the SDK and API, Features are often referred to as items in a collection).
270+
271+
Collections and Features/items that you create in in the SDK will be visible in Features API and Features Manager.
272+
273+
#### Creating a collection
274+
275+
You can use the Python SDK to create feature collections in the Features API.
276+
277+
```python
278+
new_collection = pl.features.create_collection(title="my collection", description="a new collection")
279+
```
280+
281+
#### Listing collections
282+
283+
```python
284+
collections = pl.features.list_collections()
285+
for collection in collections:
286+
print(collection)
287+
```
288+
289+
#### Listing features/items in a collection
290+
291+
```python
292+
items = pl.features.list_items(collection_id)
293+
for item in items:
294+
print(item)
295+
296+
```
297+
298+
#### Using items as geometries for other methods
299+
300+
You can pass collection items/features directly to other SDK methods. Any method that requires a geometry will accept
301+
a Features API Feature.
302+
303+
!!!note
304+
When passing a Features API Feature to other methods, the [feature ref](https://developers.planet.com/docs/apis/features/feature-references/) will be used. This means any searches or subscriptions you create will be linked to your feature.
305+
306+
```python
307+
# collection_id: the ID of a collection in Features API
308+
309+
items = pl.features.list_items(collection_id)
310+
example_feature = next(items)
311+
results = pl.data.search(["PSScene"], geometry=example_feature)
312+
```
313+
314+
!!!note
315+
Reserving quota for features is currently not supported in the SDK. However, you may create features within the SDK and then use [Features Manager](https://planet.com/features) to reserve quota.
316+
267317
## API Exceptions
268318

269319
When errors occur, the Planet SDK for Python exception hierarchy is as follows:

examples/orders_create_and_download_multiple_orders.py

+2-4
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,7 @@ def create_requests():
3333
# The Orders API will be asked to mask, or clip, results to
3434
# this area of interest.
3535
iowa_aoi = {
36-
"type":
37-
"Polygon",
36+
"type": "Polygon",
3837
"coordinates": [[[-91.198465, 42.893071], [-91.121931, 42.893071],
3938
[-91.121931, 42.946205], [-91.198465, 42.946205],
4039
[-91.198465, 42.893071]]]
@@ -54,8 +53,7 @@ def create_requests():
5453
tools=[planet.order_request.clip_tool(aoi=iowa_aoi)])
5554

5655
oregon_aoi = {
57-
"type":
58-
"Polygon",
56+
"type": "Polygon",
5957
"coordinates": [[[-117.558734, 45.229745], [-117.452447, 45.229745],
6058
[-117.452447, 45.301865], [-117.558734, 45.301865],
6159
[-117.558734, 45.229745]]]

planet/__init__.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from . import data_filter, order_request, reporting, subscription_request
1717
from .__version__ import __version__ # NOQA
1818
from .auth import Auth
19-
from .clients import DataClient, OrdersClient, SubscriptionsClient # NOQA
19+
from .clients import DataClient, FeaturesClient, OrdersClient, SubscriptionsClient # NOQA
2020
from .io import collect
2121
from .sync import Planet
2222

@@ -25,6 +25,7 @@
2525
'collect',
2626
'DataClient',
2727
'data_filter',
28+
'FeaturesClient',
2829
'OrdersClient',
2930
'order_request',
3031
'Planet',

planet/auth.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ def decode_response(response):
187187
return jwt.decode(token, options={'verify_signature': False})
188188

189189

190-
class APIKeyAuthException(Exception):
190+
class APIKeyAuthException(AuthException):
191191
"""exceptions thrown by APIKeyAuth"""
192192
pass
193193

planet/cli/cli.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020

2121
import planet
2222

23-
from . import auth, collect, data, orders, subscriptions
23+
from . import auth, collect, data, orders, subscriptions, features
2424

2525
LOGGER = logging.getLogger(__name__)
2626

@@ -78,3 +78,4 @@ def _configure_logging(verbosity):
7878
main.add_command(orders.orders) # type: ignore
7979
main.add_command(subscriptions.subscriptions) # type: ignore
8080
main.add_command(collect.collect) # type: ignore
81+
main.add_command(features.features)

planet/cli/cmds.py

+62-1
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,71 @@
1414
"""Decorators for Click commands"""
1515
import asyncio
1616
from functools import wraps
17+
from typing import Callable, Optional
1718

1819
import click
1920

2021
from planet import exceptions
22+
from planet.cli.options import pretty
23+
24+
25+
def command(group: click.Group,
26+
name: Optional[str] = None,
27+
extra_args: list[Callable] = []):
28+
"""a decorator that adds common utilities/options to a click command
29+
30+
usage:
31+
32+
@command(features) # pass a group created with @click.group
33+
def my_command():
34+
...
35+
36+
this single decorator replaces a list of decorators we would otherwise
37+
add to every function e.g.:
38+
39+
@features.command
40+
@coro
41+
@translate_exceptions
42+
@click.pass_context
43+
def my_command():
44+
...
45+
"""
46+
47+
# the decorators to add to the function **when the function is run**
48+
# (as opposed to when the function is registered as a click command)
49+
decorators = [
50+
coro,
51+
translate_exceptions,
52+
click.pass_context,
53+
pretty,
54+
] + extra_args
55+
56+
# since we want to use `command` as a function with an arg: `@command(group)`,
57+
# we need to create and return an "real" decorator that takes the function as its
58+
# arg.
59+
def decorator(f):
60+
61+
# run any click-specific registration decorators
62+
for fn in decorators:
63+
f = fn(f)
64+
65+
@wraps(f)
66+
def wrapper(*args, **kwargs):
67+
cmd = f
68+
69+
# wrap cmd with all the default decorators
70+
for d in decorators:
71+
cmd = d(f)
72+
73+
return cmd(*args, **kwargs)
74+
75+
# register the whole thing as a Click command.
76+
# Doing this last (outside the wrapper) allows click to
77+
# pick up the options/arguments added to the command by e.g.
78+
# `@click.option()`
79+
return group.command(name=name)(wrapper)
80+
81+
return decorator
2182

2283

2384
# https://github.com/pallets/click/issues/85#issuecomment-503464628
@@ -60,6 +121,6 @@ def wrapper(*args, **kwargs):
60121
'Auth information does not exist or is corrupted. Initialize '
61122
'with `planet auth init`.')
62123
except exceptions.PlanetError as ex:
63-
raise click.ClickException(ex)
124+
raise click.ClickException(str(ex))
64125

65126
return wrapper

planet/cli/data.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ async def data_client(ctx):
5252
@click.option('-u',
5353
'--base-url',
5454
default=None,
55-
help='Assign custom base Orders API URL.')
55+
help='Assign custom base Data API URL.')
5656
def data(ctx, base_url):
5757
"""Commands for interacting with the Data API"""
5858
ctx.obj['BASE_URL'] = base_url

0 commit comments

Comments
 (0)