Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
2.0.0-rc.2 (TBD)

Added:
- The Session class can now construct clients by name with its client method
(#858).

2.0.0-rc.1 (2023-03-06)

User Interface Changes:
Expand Down
23 changes: 18 additions & 5 deletions docs/get-started/upgrading.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,30 @@ If you have your API key stored in the `PL_API_KEY` environment variable you wil

In Version 2, sessions are used to manage all communication with the Planet APIs. This provides for multiple asynchronous connections. For each API, there is a specific client object. This client manages polling and downloading, along with any other capabilities provided by the API.

Each client now requires a `Session` object, which stores connection information and authentication.
Each client now requires a `Session` object, which stores connection information and authentication and manages an HTTP connection pool.

The best way of doing this is wrapping any code that invokes a client class in a block like so:

```python
from planet import OrdersClient, Session

async with Session() as session:
client = OrdersClient(session)
result = await client.create_order(order)
# Process result
```

You will see this usage in the project's tests and in the `planet.cli`
package. As a convenience, you may also get a service client instance from a
session's `client()` method.

```python
async with Session() as session:
client = session.client('orders')
result = await client.create_order(order)
# Process result
```

For more information about Session, refer to the [Python SDK User Guide](../../python/sdk-guide/#session).

## Asynchronous Methods
Expand All @@ -40,12 +53,12 @@ In V2, all `*Client` methods (for example, `DataClient().search`, `OrderClient()
```python
import asyncio
from datetime import datetime
from planet import Session, DataClient
from planet import Session
from planet import data_filter as filters

async def do_search():
async with Session() as session:
client = DataClient(session)
client = session.client('data')
date_filter = filters.date_range_filter('acquired', gte=datetime.fromisoformat("2022-11-18"), lte=datetime.fromisoformat("2022-11-21"))
cloud_filter = filters.range_filter('cloud_cover', lte=0.1)
download_filter = filters.permission_filter()
Expand Down Expand Up @@ -74,11 +87,11 @@ Is now

```python
async with Session() as session:
items = [i async for i in planet.DataClient(session).search(["PSScene"], all_filters)]
items = [i async for i in session.client('data').search(["PSScene"], all_filters)]
```

## Orders API

The Orders API capabilities in V1 were quite primitive, but those that did exist have been retained in much the same form; `ClientV1().create_order` becomes `OrderClient(session).create_order`. (As with the `DataClient`, you must also use `async` and `Session` with `OrderClient`.)
The Orders API capabilities in V1 were quite primitive, but those that did exist have been retained in much the same form; `ClientV1().create_order` becomes `OrdersClient(session).create_order`. (As with the `DataClient`, you must also use `async` and `Session` with `OrdersClient`.)

Additionally, there is now also an order builder in `planet.order_request`, similar to the preexisting search filter builder. For more details on this, refer to the [Creating an Order](../../python/sdk-guide/#creating-an-order).
20 changes: 8 additions & 12 deletions docs/python/sdk-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,11 +112,9 @@ with the only difference being the addition of the ability to poll for when an
order is completed and to download an entire order.

```python
from planet import OrdersClient

async def main():
async with Session() as sess:
client = OrdersClient(sess)
client = sess.client('orders')
# perform operations here

asyncio.run(main())
Expand Down Expand Up @@ -198,7 +196,7 @@ the context of a `Session` with the `OrdersClient`:
```python
async def main():
async with Session() as sess:
cl = OrdersClient(sess)
cl = sess.client('orders')
order = await cl.create_order(request)

asyncio.run(main())
Expand All @@ -222,7 +220,7 @@ from planet import reporting

async def create_wait_and_download():
async with Session() as sess:
cl = OrdersClient(sess)
cl = sess.client('orders')
with reporting.StateBar(state='creating') as bar:
# create order
order = await cl.create_order(request)
Expand Down Expand Up @@ -268,11 +266,11 @@ Otherwise, the JSON blob is a list of the individual results.

```python
import asyncio
from planet import collect, OrdersClient, Session
from planet import collect, Session

async def main():
async with Session() as sess:
client = OrdersClient(sess)
client = sess.client('orders')
orders_list = collect(client.list_orders())

asyncio.run(main())
Expand All @@ -293,11 +291,9 @@ with the only difference being the addition of functionality to activate an
asset, poll for when activation is complete, and download the asset.

```python
from planet import DataClient

async def main():
async with Session() as sess:
client = DataClient(sess)
client = sess.client('data')
# perform operations here

asyncio.run(main())
Expand Down Expand Up @@ -344,7 +340,7 @@ the context of a `Session` with the `DataClient`:
```python
async def main():
async with Session() as sess:
cl = DataClient(sess)
cl = sess.client('data')
items = [i async for i in cl.search(['PSScene'], sfilter)]

asyncio.run(main())
Expand All @@ -364,7 +360,7 @@ print command to report wait status. `download_asset` has reporting built in.
```python
async def download_and_validate():
async with Session() as sess:
cl = DataClient(sess)
cl = sess.client('data')

# get asset description
item_type_id = 'PSScene'
Expand Down
22 changes: 11 additions & 11 deletions examples/data_download_multiple_assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"""
import asyncio

from planet import Session, DataClient
from planet import Session

river_item_id = '20221003_002705_38_2461'
river_item_type = 'PSScene'
Expand All @@ -33,7 +33,8 @@
wildfire_asset_type = 'basic_analytic_4b'


async def download_and_validate(item_id, item_type_id, asset_type_id, client):
async def download_and_validate(client, item_id, item_type_id, asset_type_id):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the positioning of client here, like self. I think you mentioned that in a comment somewhere. Great call.

"""Activate, download, and validate an asset as a single task."""
# Get asset description
asset = await client.get_asset(item_type_id, item_id, asset_type_id)

Expand All @@ -51,19 +52,18 @@ async def download_and_validate(item_id, item_type_id, asset_type_id, client):


async def main():
"""Download and validate assets in parallel."""
async with Session() as sess:
# Data client object
client = DataClient(sess)
# Download and validate assets in parallel
client = sess.client('data')
await asyncio.gather(
download_and_validate(river_item_id,
download_and_validate(client,
river_item_id,
river_item_type,
river_asset_type,
client),
download_and_validate(wildfire_item_id,
river_asset_type),
download_and_validate(client,
wildfire_item_id,
wildfire_item_type,
wildfire_asset_type,
client))
wildfire_asset_type))


if __name__ == '__main__':
Expand Down
46 changes: 28 additions & 18 deletions examples/orders_create_and_download_multiple_orders.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we get rid of the 2020 copyright on line 1?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nope, according to @cholmes investigation, we just add a line above

DOWNLOAD_DIR = os.getenv('TEST_DOWNLOAD_DIR', '.')

# The Orders API will be asked to mask, or clip, results to
# this area of interest.
iowa_aoi = {
"type":
"Polygon",
Expand All @@ -36,11 +38,18 @@
[-91.198465, 42.893071]]]
}

iowa_images = ['20200925_161029_69_2223', '20200925_161027_48_2223']
# In practice, you will use a Data API search to find items, but
# for this example take them as given.
iowa_items = ['20200925_161029_69_2223', '20200925_161027_48_2223']

iowa_order = planet.order_request.build_request(
'iowa_order',
[planet.order_request.product(iowa_images, 'analytic_udm2', 'PSScene')],
tools=[planet.order_request.clip_tool(iowa_aoi)])
name='iowa_order',
products=[
planet.order_request.product(item_ids=iowa_items,
product_bundle='analytic_udm2',
item_type='PSScene')
],
Copy link
Contributor Author

@sgillies sgillies Mar 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using product()'s keyword arguments makes the calls more understandable and the whole example more instructive.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes it much more readable, thank you!

tools=[planet.order_request.clip_tool(aoi=iowa_aoi)])

oregon_aoi = {
"type":
Expand All @@ -50,33 +59,34 @@
[-117.558734, 45.229745]]]
}

oregon_images = ['20200909_182525_1014', '20200909_182524_1014']
oregon_items = ['20200909_182525_1014', '20200909_182524_1014']
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whoopsie, the images suffix was from my old days in astronomy, thanks for catching this.


oregon_order = planet.order_request.build_request(
'oregon_order',
[planet.order_request.product(oregon_images, 'analytic_udm2', 'PSScene')],
tools=[planet.order_request.clip_tool(oregon_aoi)])
name='oregon_order',
products=[
planet.order_request.product(item_ids=oregon_items,
product_bundle='analytic_udm2',
item_type='PSScene')
],
tools=[planet.order_request.clip_tool(aoi=oregon_aoi)])


async def create_and_download(order_detail, directory, client):
async def create_and_download(client, order_detail, directory):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved the client argument to the head of the list to make the signature of this function more like that of client.download_order(), where client (aka self) is implicitly the first argument.

"""Make an order, wait for completion, download files as a single task."""
with planet.reporting.StateBar(state='creating') as reporter:
# create
order = await client.create_order(order_detail)
reporter.update(state='created', order_id=order['id'])

# wait for completion
await client.wait(order['id'], callback=reporter.update_state)

# download
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed obvious comments and rolled them up into a docstring.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's do the same thing for the data example.

await client.download_order(order['id'], directory, progress_bar=True)


async def main():
async with planet.Session() as ps:
client = planet.OrdersClient(ps)

async with planet.Session() as sess:
client = sess.client('orders')
await asyncio.gather(
create_and_download(iowa_order, DOWNLOAD_DIR, client),
create_and_download(oregon_order, DOWNLOAD_DIR, client),
create_and_download(client, iowa_order, DOWNLOAD_DIR),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comment above.

create_and_download(client, oregon_order, DOWNLOAD_DIR),
)


Expand Down
7 changes: 7 additions & 0 deletions planet/clients/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,10 @@
from .subscriptions import SubscriptionsClient

__all__ = ['DataClient', 'OrdersClient', 'SubscriptionsClient']

# Organize client classes by their module name to allow lookup.
_client_directory = {
'data': DataClient,
'orders': OrdersClient,
'subscriptions': SubscriptionsClient
}
25 changes: 25 additions & 0 deletions planet/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@
import random
import time
from typing import AsyncGenerator, Optional

import httpx
from typing_extensions import Literal

from .auth import Auth, AuthType
from . import exceptions, models
Expand Down Expand Up @@ -413,6 +415,29 @@ async def stream(
finally:
await response.aclose()

def client(self,
name: Literal['data', 'orders', 'subscriptions'],
base_url: Optional[str] = None) -> object:
"""Get a client by its module name.

Parameters:
name: one of 'data', 'orders', or 'subscriptions'.

Returns:
A client instance.

Raises:
ClientError when no such client can be had.

"""
# To avoid circular dependency.
from planet.clients import _client_directory

try:
return _client_directory[name](self, base_url=base_url)
except KeyError:
raise exceptions.ClientError("No such client.")


class AuthSession(BaseSession):
"""Synchronous connection to the Planet Auth service."""
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
'jsonschema',
'pyjwt>=2.1',
'tqdm>=4.56',
'typing-extensions',
]

test_requires = ['pytest', 'pytest-asyncio==0.16', 'pytest-cov', 'respx==0.19']
Expand Down
23 changes: 23 additions & 0 deletions tests/unit/test_session.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"""Session module tests."""

import pytest

from planet import DataClient, OrdersClient, SubscriptionsClient, Session
from planet.exceptions import ClientError


@pytest.mark.parametrize("client_name,client_class",
[('data', DataClient), ('orders', OrdersClient),
('subscriptions', SubscriptionsClient)])
async def test_session_get_client(client_name, client_class):
"""Get a client from a session."""
async with Session() as session:
client = session.client(client_name)
assert isinstance(client, client_class)


async def test_session_get_client_error():
"""Get an exception when no such client exists."""
async with Session() as session:
with pytest.raises(ClientError):
_ = session.client('bogus')