Skip to content

Commit 9d16dd9

Browse files
stephenhillierischneiderasonnenschein
authored
Planet sync client (#1049)
adds sync clients and top-level sync facade --------- Co-authored-by: Ian Schneider <planablediglet@gmail.com> Co-authored-by: asonnenschein <adrian.sonnenschein@planet.com>
1 parent f297787 commit 9d16dd9

File tree

17 files changed

+2353
-9
lines changed

17 files changed

+2353
-9
lines changed

CONTRIBUTING.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,15 +141,15 @@ These commands can be performed on the entire repository, when run from the repo
141141
and
142142

143143
```console
144-
$ yapf --diff -r .
144+
$ yapf --in-place -r .
145145
```
146146
The configuration for YAPF is given in `setup.cfg` and `.yapfignore`.
147147
See the YAPF link above for advanced usage.
148148

149149
##### Alternative to YAPF
150150

151151
YAPF is not required to follow the style and formatting guidelines. You can
152-
perform all formatting on your own using the linting output as a guild. Painful,
152+
perform all formatting on your own using the linting output as a guide. Painful,
153153
maybe, but possible!
154154

155155
## Testing
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
---
2+
title: Planet Client Quick Start
3+
---
4+
5+
The Planet SDK for Python makes it easy to access Planet’s massive repository of satellite imagery and add Planet
6+
data to your data ops workflow.
7+
8+
**Note:** This is the new, non-asyncio client. If you want to take advantage of asyncio, check the [asyncio client quick start guide](quick-start-guide.md).
9+
10+
Your feedback on this version of our client is appreciated. Please raise an issue on [GitHub](https://github.com/planetlabs/planet-client-python/issues) if you encounter any problems.
11+
12+
## Dependencies
13+
14+
This package requires [Python 3.9 or greater](https://python.org/downloads/). A virtual environment is strongly recommended.
15+
16+
You will need your Planet API credentials. You can find your API key in [Planet Explorer](https://planet.com/explorer) under Account Settings.
17+
18+
## Installation
19+
20+
Install from PyPI using pip:
21+
22+
```bash
23+
pip install planet
24+
```
25+
26+
## Usage
27+
28+
### Authentication
29+
30+
Use the `PL_API_KEY` environment variable to authenticate with the Planet API.
31+
32+
```bash
33+
export PL_API_KEY=your_api_key
34+
```
35+
36+
These examples will assume you are using the `PL_API_KEY` environment variable. If you are, you can skip to the next section.
37+
38+
#### Authenticate using the Session class
39+
40+
Alternately, you can also authenticate using the `Session` class:
41+
42+
```python
43+
from planet import Auth, Session, Auth
44+
from planet.auth import APIKeyAuth
45+
46+
pl = Planet(session=Session(auth=APIKeyAuth(key='your_api_key')))
47+
```
48+
49+
50+
### The Planet client
51+
52+
The `Planet` class is the main entry point for the Planet SDK. It provides access to the various APIs available on the Planet platform.
53+
54+
```python
55+
from planet import Planet
56+
pl = Planet() # automatically detects PL_API_KEY
57+
```
58+
59+
The Planet client has members `data`, `orders`, and `subscriptions`, which allow you to interact with the Data API, Orders API, and Subscriptions API.
60+
61+
### Search
62+
63+
To search for items in the Planet catalog, use the `data.search()` method on the `Planet` client. The return value is an iterator that yields search
64+
results:
65+
66+
```python
67+
from planet import Planet
68+
69+
pl = Planet()
70+
for item in pl.data.search(['PSScene'], limit=5):
71+
print(item)
72+
```
73+
74+
#### Geometry
75+
76+
Use the `geometry` parameter to filter search results by geometry:
77+
78+
```python
79+
geom = {
80+
"coordinates": [
81+
[
82+
[
83+
-125.41267816101056,
84+
46.38901501783491
85+
],
86+
[
87+
-125.41267816101056,
88+
41.101114161051015
89+
],
90+
[
91+
-115.51426167332103,
92+
41.101114161051015
93+
],
94+
[
95+
-115.51426167332103,
96+
46.38901501783491
97+
],
98+
[
99+
-125.41267816101056,
100+
46.38901501783491
101+
]
102+
]
103+
],
104+
"type": "Polygon"
105+
}
106+
for item in pl.data.search(['PSScene'], geometry=geom, limit=5):
107+
print(item)
108+
```
109+
110+
#### Filters
111+
112+
The Data API allows a wide range of search parameters. Whether using the `.search()` method, or
113+
creating or updating a saved search, or requesting stats, a data search filter
114+
can be provided to the API as a JSON blob. This JSON blob can be built up manually or by using the
115+
`data_filter` module.
116+
117+
An example of creating the request JSON with `data_filter`:
118+
119+
```python
120+
from datetime import datetime
121+
from planet import data_filter
122+
123+
def main():
124+
pl = Planet()
125+
126+
sfilter = data_filter.and_filter([
127+
data_filter.permission_filter(),
128+
data_filter.date_range_filter('acquired', gt=datetime(2022, 6, 1, 1))
129+
])
130+
131+
for item in pl.data.search(['PSScene'], filter=sfilter, limit=10):
132+
print(item["id"])
133+
```
134+
135+
This returns scenes acquired after the provided date that you have permission to download using
136+
your plan.
137+
138+
If you prefer to build the JSON blob manually, the above filter would look like this:
139+
140+
```python
141+
sfilter = {
142+
'type': 'AndFilter',
143+
'config': [
144+
{'type': 'PermissionFilter', 'config': ['assets:download']},
145+
{
146+
'type': 'DateRangeFilter',
147+
'field_name': 'acquired',
148+
'config': {'gt': '2022-06-01T01:00:00Z'}
149+
}
150+
]
151+
}
152+
```
153+
154+
This means that if you already have Data API filters saved as a query, you can copy them directly into the SDK.
155+
156+
### Placing an Order
157+
158+
Once you have a list of scenes you want to download, you can place an order for assets using the Orders API client. Please review
159+
[Items and Assets](https://developers.planet.com/docs/apis/data/items-assets/) in the Developer Center for a refresher on item types
160+
and asset types.
161+
162+
Use the `order_request` module to build an order request, and then use the `orders.create_order()` method to place the order.
163+
164+
Orders take time to process. You can use the `orders.wait()` method to wait for the order to be ready, and then use the `orders.download_order()` method to download the assets.
165+
166+
Warning: running the following code will result in quota usage based on your plan.
167+
168+
```python
169+
from planet import Planet, order_request
170+
171+
def main():
172+
pl = Planet()
173+
image_ids = ["20200925_161029_69_2223"]
174+
request = order_request.build_request(
175+
name='test_order',
176+
products=[
177+
order_request.product(
178+
item_ids=image_ids,
179+
product_bundle='analytic_udm2',
180+
item_type='psscene')
181+
]
182+
)
183+
184+
order = pl.orders.create_order(request)
185+
186+
# wait for the order to be ready
187+
# note: this may take several minutes.
188+
pl.orders.wait(order['id'])
189+
190+
pl.orders.download_order(order['id'], overwrite=True)
191+
```
192+
193+
### Creating a subscription
194+
195+
#### Prerequisites
196+
197+
Subscriptions can be delivered to a destination. The following example uses Amazon S3.
198+
You will need your ACCESS_KEY_ID, SECRET_ACCESS_KEY, bucket and region name.
199+
200+
#### Scene subscription
201+
202+
To subscribe to scenes that match a filter, use the `subscription_request` module to build a request, and
203+
pass it to the `subscriptions.create_subscription()` method of the client.
204+
205+
Warning: the following code will create a subscription, consuming quota based on your plan.
206+
207+
```python
208+
from planet.subscription_request import catalog_source, build_request, amazon_s3
209+
210+
source = catalog_source(
211+
["PSScene"],
212+
["ortho_analytic_4b"],
213+
geometry={
214+
"type": "Polygon",
215+
"coordinates": [
216+
[
217+
[37.791595458984375, 14.84923123791421],
218+
[37.90214538574219, 14.84923123791421],
219+
[37.90214538574219, 14.945448293647944],
220+
[37.791595458984375, 14.945448293647944],
221+
[37.791595458984375, 14.84923123791421],
222+
]
223+
],
224+
},
225+
start_time=datetime.now(),
226+
publishing_stages=["standard"],
227+
time_range_type="acquired",
228+
)
229+
230+
request = build_request("Standard PSScene Ortho Analytic", source=source, delivery={})
231+
232+
# define a delivery method. In this example, we're using AWS S3.
233+
delivery = amazon_s3(ACCESS_KEY_ID, SECRET_ACCESS_KEY, "test", "us-east-1")
234+
235+
# finally, create the subscription
236+
subscription = pl.subscriptions.create_subscription(request)
237+
```

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ plugins:
6767
nav:
6868
- "Get Started":
6969
- get-started/quick-start-guide.md
70+
- get-started/sync-client-quick-start.md
7071
- get-started/get-your-planet-account.md
7172
- get-started/venv-tutorial.md
7273
- get-started/upgrading.md

planet/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from .auth import Auth
1919
from .clients import DataClient, OrdersClient, SubscriptionsClient # NOQA
2020
from .io import collect
21+
from .sync import Planet
2122

2223
__all__ = [
2324
'Auth',
@@ -26,6 +27,7 @@
2627
'data_filter',
2728
'OrdersClient',
2829
'order_request',
30+
'Planet',
2931
'reporting',
3032
'Session',
3133
'SubscriptionsClient',

planet/clients/data.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import logging
1818
from pathlib import Path
1919
import time
20-
from typing import Any, AsyncIterator, Callable, Dict, List, Optional
20+
from typing import Any, AsyncIterator, Awaitable, Callable, Dict, List, Optional, TypeVar
2121
import uuid
2222

2323
from ..data_filter import empty_filter
@@ -52,6 +52,8 @@
5252

5353
LOGGER = logging.getLogger(__name__)
5454

55+
T = TypeVar("T")
56+
5557

5658
class Items(Paged):
5759
"""Asynchronous iterator over items from a paged response."""
@@ -96,6 +98,10 @@ def __init__(self, session: Session, base_url: Optional[str] = None):
9698
if self._base_url.endswith('/'):
9799
self._base_url = self._base_url[:-1]
98100

101+
def call_sync(self, f: Awaitable[T]) -> T:
102+
"""block on an async function call, using the call_sync method of the session"""
103+
return self._session.call_sync(f)
104+
99105
@staticmethod
100106
def _check_search_id(sid):
101107
"""Raises planet.exceptions.ClientError if sid is not a valid UUID"""

planet/clients/orders.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
import asyncio
1717
import logging
1818
import time
19-
from typing import AsyncIterator, Callable, List, Optional, Sequence, Union, Dict
19+
from typing import AsyncIterator, Awaitable, Callable, Dict, List, Optional, Sequence, TypeVar, Union
2020
import uuid
2121
import json
2222
import hashlib
@@ -39,6 +39,8 @@
3939

4040
LOGGER = logging.getLogger(__name__)
4141

42+
T = TypeVar("T")
43+
4244

4345
class Orders(Paged):
4446
"""Asynchronous iterator over Orders from a paged response describing
@@ -97,6 +99,10 @@ def __init__(self, session: Session, base_url: Optional[str] = None):
9799
if self._base_url.endswith('/'):
98100
self._base_url = self._base_url[:-1]
99101

102+
def call_sync(self, f: Awaitable[T]) -> T:
103+
"""block on an async function call, using the call_sync method of the session"""
104+
return self._session.call_sync(f)
105+
100106
@staticmethod
101107
def _check_order_id(oid):
102108
"""Raises planet.exceptions.ClientError if oid is not a valid UUID"""
@@ -435,6 +441,7 @@ async def wait(self,
435441
# loop without end if max_attempts is zero
436442
# otherwise, loop until num_attempts reaches max_attempts
437443
num_attempts = 0
444+
current_state = "UNKNOWN"
438445
while not max_attempts or num_attempts < max_attempts:
439446
t = time.time()
440447

planet/clients/subscriptions.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Planet Subscriptions API Python client."""
22

33
import logging
4-
from typing import AsyncIterator, Optional, Sequence, Dict, Union
4+
from typing import AsyncIterator, Awaitable, Dict, Optional, Sequence, TypeVar, Union
55

66
from typing_extensions import Literal
77

@@ -14,6 +14,8 @@
1414

1515
LOGGER = logging.getLogger()
1616

17+
T = TypeVar("T")
18+
1719

1820
class SubscriptionsClient:
1921
"""A Planet Subscriptions Service API 1.0.0 client.
@@ -59,6 +61,10 @@ def __init__(self,
5961
if self._base_url.endswith('/'):
6062
self._base_url = self._base_url[:-1]
6163

64+
def call_sync(self, f: Awaitable[T]) -> T:
65+
"""block on an async function call, using the call_sync method of the session"""
66+
return self._session.call_sync(f)
67+
6268
async def list_subscriptions(
6369
self,
6470
status: Optional[Sequence[str]] = None,
@@ -72,11 +78,11 @@ async def list_subscriptions(
7278
start_time: Optional[str] = None,
7379
sort_by: Optional[str] = None,
7480
updated: Optional[str] = None) -> AsyncIterator[dict]:
75-
"""Iterate over list of account subscriptions with optional filtering and sorting.
81+
"""Iterate over list of account subscriptions with optional filtering.
7682
7783
Note:
7884
The name of this method is based on the API's method name.
79-
This method provides iteration over subcriptions, it does
85+
This method provides iteration over subscriptions, it does
8086
not return a list.
8187
8288
Args:

0 commit comments

Comments
 (0)