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

Planet sync client #1049

Merged
merged 23 commits into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,15 +141,15 @@ These commands can be performed on the entire repository, when run from the repo
and

```console
$ yapf --diff -r .
$ yapf --in-place -r .
```
The configuration for YAPF is given in `setup.cfg` and `.yapfignore`.
See the YAPF link above for advanced usage.

##### Alternative to YAPF

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

## Testing
Expand Down
237 changes: 237 additions & 0 deletions docs/get-started/sync-client-quick-start.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
---
title: Planet Client Quick Start
---

The Planet SDK for Python makes it easy to access Planet’s massive repository of satellite imagery and add Planet
data to your data ops workflow.

**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).

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.

## Dependencies

This package requires [Python 3.9 or greater](https://python.org/downloads/). A virtual environment is strongly recommended.

You will need your Planet API credentials. You can find your API key in [Planet Explorer](https://planet.com/explorer) under Account Settings.

## Installation

Install from PyPI using pip:

```bash
pip install planet
```

## Usage

### Authentication

Use the `PL_API_KEY` environment variable to authenticate with the Planet API.

```bash
export PL_API_KEY=your_api_key
```

These examples will assume you are using the `PL_API_KEY` environment variable. If you are, you can skip to the next section.

#### Authenticate using the Session class

Alternately, you can also authenticate using the `Session` class:

```python
from planet import Auth, Session, Auth
from planet.auth import APIKeyAuth

pl = Planet(session=Session(auth=APIKeyAuth(key='your_api_key')))
```


### The Planet client

The `Planet` class is the main entry point for the Planet SDK. It provides access to the various APIs available on the Planet platform.

```python
from planet import Planet
pl = Planet() # automatically detects PL_API_KEY
```

The Planet client has members `data`, `orders`, and `subscriptions`, which allow you to interact with the Data API, Orders API, and Subscriptions API.

### Search

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
results:

```python
from planet import Planet

pl = Planet()
for item in pl.data.search(['PSScene'], limit=5):
print(item)
```

#### Geometry

Use the `geometry` parameter to filter search results by geometry:

```python
geom = {
"coordinates": [
[
[
-125.41267816101056,
46.38901501783491
],
[
-125.41267816101056,
41.101114161051015
],
[
-115.51426167332103,
41.101114161051015
],
[
-115.51426167332103,
46.38901501783491
],
[
-125.41267816101056,
46.38901501783491
]
]
],
"type": "Polygon"
}
for item in pl.data.search(['PSScene'], geometry=geom, limit=5):
print(item)
```

#### Filters

The Data API allows a wide range of search parameters. Whether using the `.search()` method, or
creating or updating a saved search, or requesting stats, a data search filter
can be provided to the API as a JSON blob. This JSON blob can be built up manually or by using the
`data_filter` module.

An example of creating the request JSON with `data_filter`:

```python
from datetime import datetime
from planet import data_filter

def main():
pl = Planet()

sfilter = data_filter.and_filter([
data_filter.permission_filter(),
data_filter.date_range_filter('acquired', gt=datetime(2022, 6, 1, 1))
])

for item in pl.data.search(['PSScene'], filter=sfilter, limit=10):
print(item["id"])
```

This returns scenes acquired after the provided date that you have permission to download using
your plan.

If you prefer to build the JSON blob manually, the above filter would look like this:

```python
sfilter = {
'type': 'AndFilter',
'config': [
{'type': 'PermissionFilter', 'config': ['assets:download']},
{
'type': 'DateRangeFilter',
'field_name': 'acquired',
'config': {'gt': '2022-06-01T01:00:00Z'}
}
]
}
```

This means that if you already have Data API filters saved as a query, you can copy them directly into the SDK.

### Placing an Order

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
[Items and Assets](https://developers.planet.com/docs/apis/data/items-assets/) in the Developer Center for a refresher on item types
and asset types.

Use the `order_request` module to build an order request, and then use the `orders.create_order()` method to place the order.

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.

Warning: running the following code will result in quota usage based on your plan.

```python
from planet import Planet, order_request

def main():
pl = Planet()
image_ids = ["20200925_161029_69_2223"]
request = order_request.build_request(
name='test_order',
products=[
order_request.product(
item_ids=image_ids,
product_bundle='analytic_udm2',
item_type='psscene')
]
)

order = pl.orders.create_order(request)

# wait for the order to be ready
# note: this may take several minutes.
pl.orders.wait(order['id'])

pl.orders.download_order(order['id'], overwrite=True)
```

### Creating a subscription

#### Prerequisites

Subscriptions can be delivered to a destination. The following example uses Amazon S3.
You will need your ACCESS_KEY_ID, SECRET_ACCESS_KEY, bucket and region name.

#### Scene subscription

To subscribe to scenes that match a filter, use the `subscription_request` module to build a request, and
pass it to the `subscriptions.create_subscription()` method of the client.

Warning: the following code will create a subscription, consuming quota based on your plan.
asonnenschein marked this conversation as resolved.
Show resolved Hide resolved

```python
from planet.subscription_request import catalog_source, build_request, amazon_s3

source = catalog_source(
["PSScene"],
["ortho_analytic_4b"],
geometry={
"type": "Polygon",
"coordinates": [
[
[37.791595458984375, 14.84923123791421],
[37.90214538574219, 14.84923123791421],
[37.90214538574219, 14.945448293647944],
[37.791595458984375, 14.945448293647944],
[37.791595458984375, 14.84923123791421],
]
],
},
start_time=datetime.now(),
publishing_stages=["standard"],
time_range_type="acquired",
)

request = build_request("Standard PSScene Ortho Analytic", source=source, delivery={})

# define a delivery method. In this example, we're using AWS S3.
delivery = amazon_s3(ACCESS_KEY_ID, SECRET_ACCESS_KEY, "test", "us-east-1")

# finally, create the subscription
subscription = pl.subscriptions.create_subscription(request)
```
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ plugins:
nav:
- "Get Started":
- get-started/quick-start-guide.md
- get-started/sync-client-quick-start.md
- get-started/get-your-planet-account.md
- get-started/venv-tutorial.md
- get-started/upgrading.md
Expand Down
2 changes: 2 additions & 0 deletions planet/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from .auth import Auth
from .clients import DataClient, OrdersClient, SubscriptionsClient # NOQA
from .io import collect
from .sync import Planet

__all__ = [
'Auth',
Expand All @@ -26,6 +27,7 @@
'data_filter',
'OrdersClient',
'order_request',
'Planet',
'reporting',
'Session',
'SubscriptionsClient',
Expand Down
8 changes: 7 additions & 1 deletion planet/clients/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import logging
from pathlib import Path
import time
from typing import Any, AsyncIterator, Callable, Dict, List, Optional
from typing import Any, AsyncIterator, Awaitable, Callable, Dict, List, Optional, TypeVar
import uuid

from ..data_filter import empty_filter
Expand Down Expand Up @@ -52,6 +52,8 @@

LOGGER = logging.getLogger(__name__)

T = TypeVar("T")


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

def call_sync(self, f: Awaitable[T]) -> T:
"""block on an async function call, using the call_sync method of the session"""
return self._session.call_sync(f)

@staticmethod
def _check_search_id(sid):
"""Raises planet.exceptions.ClientError if sid is not a valid UUID"""
Expand Down
9 changes: 8 additions & 1 deletion planet/clients/orders.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import asyncio
import logging
import time
from typing import AsyncIterator, Callable, List, Optional, Sequence, Union, Dict
from typing import AsyncIterator, Awaitable, Callable, Dict, List, Optional, Sequence, TypeVar, Union
import uuid
import json
import hashlib
Expand All @@ -39,6 +39,8 @@

LOGGER = logging.getLogger(__name__)

T = TypeVar("T")


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

def call_sync(self, f: Awaitable[T]) -> T:
"""block on an async function call, using the call_sync method of the session"""
return self._session.call_sync(f)

@staticmethod
def _check_order_id(oid):
"""Raises planet.exceptions.ClientError if oid is not a valid UUID"""
Expand Down Expand Up @@ -435,6 +441,7 @@ async def wait(self,
# loop without end if max_attempts is zero
# otherwise, loop until num_attempts reaches max_attempts
num_attempts = 0
current_state = "UNKNOWN"
while not max_attempts or num_attempts < max_attempts:
t = time.time()

Expand Down
12 changes: 9 additions & 3 deletions planet/clients/subscriptions.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Planet Subscriptions API Python client."""

import logging
from typing import AsyncIterator, Optional, Sequence, Dict, Union
from typing import AsyncIterator, Awaitable, Dict, Optional, Sequence, TypeVar, Union

from typing_extensions import Literal

Expand All @@ -14,6 +14,8 @@

LOGGER = logging.getLogger()

T = TypeVar("T")


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

def call_sync(self, f: Awaitable[T]) -> T:
"""block on an async function call, using the call_sync method of the session"""
return self._session.call_sync(f)

async def list_subscriptions(
self,
status: Optional[Sequence[str]] = None,
Expand All @@ -72,11 +78,11 @@ async def list_subscriptions(
start_time: Optional[str] = None,
sort_by: Optional[str] = None,
updated: Optional[str] = None) -> AsyncIterator[dict]:
"""Iterate over list of account subscriptions with optional filtering and sorting.
"""Iterate over list of account subscriptions with optional filtering.

Note:
The name of this method is based on the API's method name.
This method provides iteration over subcriptions, it does
This method provides iteration over subscriptions, it does
not return a list.

Args:
Expand Down
Loading
Loading