Skip to content

Commit 156e811

Browse files
authored
Merge pull request #867 from planetlabs/issue857take2
Let Session get clients by name
2 parents f4fb73d + 0762454 commit 156e811

File tree

9 files changed

+127
-46
lines changed

9 files changed

+127
-46
lines changed

CHANGES.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
2.0.0-rc.2 (TBD)
2+
3+
Added:
4+
- The Session class can now construct clients by name with its client method
5+
(#858).
6+
17
2.0.0-rc.1 (2023-03-06)
28

39
User Interface Changes:

docs/get-started/upgrading.md

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,30 @@ If you have your API key stored in the `PL_API_KEY` environment variable you wil
1818

1919
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.
2020

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

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

2525
```python
26+
from planet import OrdersClient, Session
27+
2628
async with Session() as session:
2729
client = OrdersClient(session)
2830
result = await client.create_order(order)
2931
# Process result
3032
```
3133

34+
You will see this usage in the project's tests and in the `planet.cli`
35+
package. As a convenience, you may also get a service client instance from a
36+
session's `client()` method.
37+
38+
```python
39+
async with Session() as session:
40+
client = session.client('orders')
41+
result = await client.create_order(order)
42+
# Process result
43+
```
44+
3245
For more information about Session, refer to the [Python SDK User Guide](../../python/sdk-guide/#session).
3346

3447
## Asynchronous Methods
@@ -40,12 +53,12 @@ In V2, all `*Client` methods (for example, `DataClient().search`, `OrderClient()
4053
```python
4154
import asyncio
4255
from datetime import datetime
43-
from planet import Session, DataClient
56+
from planet import Session
4457
from planet import data_filter as filters
4558

4659
async def do_search():
4760
async with Session() as session:
48-
client = DataClient(session)
61+
client = session.client('data')
4962
date_filter = filters.date_range_filter('acquired', gte=datetime.fromisoformat("2022-11-18"), lte=datetime.fromisoformat("2022-11-21"))
5063
cloud_filter = filters.range_filter('cloud_cover', lte=0.1)
5164
download_filter = filters.permission_filter()
@@ -74,11 +87,11 @@ Is now
7487

7588
```python
7689
async with Session() as session:
77-
items = [i async for i in planet.DataClient(session).search(["PSScene"], all_filters)]
90+
items = [i async for i in session.client('data').search(["PSScene"], all_filters)]
7891
```
7992

8093
## Orders API
8194

82-
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`.)
95+
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`.)
8396

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

docs/python/sdk-guide.md

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -112,11 +112,9 @@ with the only difference being the addition of the ability to poll for when an
112112
order is completed and to download an entire order.
113113

114114
```python
115-
from planet import OrdersClient
116-
117115
async def main():
118116
async with Session() as sess:
119-
client = OrdersClient(sess)
117+
client = sess.client('orders')
120118
# perform operations here
121119

122120
asyncio.run(main())
@@ -198,7 +196,7 @@ the context of a `Session` with the `OrdersClient`:
198196
```python
199197
async def main():
200198
async with Session() as sess:
201-
cl = OrdersClient(sess)
199+
cl = sess.client('orders')
202200
order = await cl.create_order(request)
203201

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

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

269267
```python
270268
import asyncio
271-
from planet import collect, OrdersClient, Session
269+
from planet import collect, Session
272270

273271
async def main():
274272
async with Session() as sess:
275-
client = OrdersClient(sess)
273+
client = sess.client('orders')
276274
orders_list = collect(client.list_orders())
277275

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

295293
```python
296-
from planet import DataClient
297-
298294
async def main():
299295
async with Session() as sess:
300-
client = DataClient(sess)
296+
client = sess.client('data')
301297
# perform operations here
302298

303299
asyncio.run(main())
@@ -344,7 +340,7 @@ the context of a `Session` with the `DataClient`:
344340
```python
345341
async def main():
346342
async with Session() as sess:
347-
cl = DataClient(sess)
343+
cl = sess.client('data')
348344
items = [i async for i in cl.search(['PSScene'], sfilter)]
349345

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

369365
# get asset description
370366
item_type_id = 'PSScene'

examples/data_download_multiple_assets.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
"""
2323
import asyncio
2424

25-
from planet import Session, DataClient
25+
from planet import Session
2626

2727
river_item_id = '20221003_002705_38_2461'
2828
river_item_type = 'PSScene'
@@ -33,7 +33,8 @@
3333
wildfire_asset_type = 'basic_analytic_4b'
3434

3535

36-
async def download_and_validate(item_id, item_type_id, asset_type_id, client):
36+
async def download_and_validate(client, item_id, item_type_id, asset_type_id):
37+
"""Activate, download, and validate an asset as a single task."""
3738
# Get asset description
3839
asset = await client.get_asset(item_type_id, item_id, asset_type_id)
3940

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

5253

5354
async def main():
55+
"""Download and validate assets in parallel."""
5456
async with Session() as sess:
55-
# Data client object
56-
client = DataClient(sess)
57-
# Download and validate assets in parallel
57+
client = sess.client('data')
5858
await asyncio.gather(
59-
download_and_validate(river_item_id,
59+
download_and_validate(client,
60+
river_item_id,
6061
river_item_type,
61-
river_asset_type,
62-
client),
63-
download_and_validate(wildfire_item_id,
62+
river_asset_type),
63+
download_and_validate(client,
64+
wildfire_item_id,
6465
wildfire_item_type,
65-
wildfire_asset_type,
66-
client))
66+
wildfire_asset_type))
6767

6868

6969
if __name__ == '__main__':

examples/orders_create_and_download_multiple_orders.py

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828

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

31+
# The Orders API will be asked to mask, or clip, results to
32+
# this area of interest.
3133
iowa_aoi = {
3234
"type":
3335
"Polygon",
@@ -36,11 +38,18 @@
3638
[-91.198465, 42.893071]]]
3739
}
3840

39-
iowa_images = ['20200925_161029_69_2223', '20200925_161027_48_2223']
41+
# In practice, you will use a Data API search to find items, but
42+
# for this example take them as given.
43+
iowa_items = ['20200925_161029_69_2223', '20200925_161027_48_2223']
44+
4045
iowa_order = planet.order_request.build_request(
41-
'iowa_order',
42-
[planet.order_request.product(iowa_images, 'analytic_udm2', 'PSScene')],
43-
tools=[planet.order_request.clip_tool(iowa_aoi)])
46+
name='iowa_order',
47+
products=[
48+
planet.order_request.product(item_ids=iowa_items,
49+
product_bundle='analytic_udm2',
50+
item_type='PSScene')
51+
],
52+
tools=[planet.order_request.clip_tool(aoi=iowa_aoi)])
4453

4554
oregon_aoi = {
4655
"type":
@@ -50,33 +59,34 @@
5059
[-117.558734, 45.229745]]]
5160
}
5261

53-
oregon_images = ['20200909_182525_1014', '20200909_182524_1014']
62+
oregon_items = ['20200909_182525_1014', '20200909_182524_1014']
63+
5464
oregon_order = planet.order_request.build_request(
55-
'oregon_order',
56-
[planet.order_request.product(oregon_images, 'analytic_udm2', 'PSScene')],
57-
tools=[planet.order_request.clip_tool(oregon_aoi)])
65+
name='oregon_order',
66+
products=[
67+
planet.order_request.product(item_ids=oregon_items,
68+
product_bundle='analytic_udm2',
69+
item_type='PSScene')
70+
],
71+
tools=[planet.order_request.clip_tool(aoi=oregon_aoi)])
5872

5973

60-
async def create_and_download(order_detail, directory, client):
74+
async def create_and_download(client, order_detail, directory):
75+
"""Make an order, wait for completion, download files as a single task."""
6176
with planet.reporting.StateBar(state='creating') as reporter:
62-
# create
6377
order = await client.create_order(order_detail)
6478
reporter.update(state='created', order_id=order['id'])
65-
66-
# wait for completion
6779
await client.wait(order['id'], callback=reporter.update_state)
6880

69-
# download
7081
await client.download_order(order['id'], directory, progress_bar=True)
7182

7283

7384
async def main():
74-
async with planet.Session() as ps:
75-
client = planet.OrdersClient(ps)
76-
85+
async with planet.Session() as sess:
86+
client = sess.client('orders')
7787
await asyncio.gather(
78-
create_and_download(iowa_order, DOWNLOAD_DIR, client),
79-
create_and_download(oregon_order, DOWNLOAD_DIR, client),
88+
create_and_download(client, iowa_order, DOWNLOAD_DIR),
89+
create_and_download(client, oregon_order, DOWNLOAD_DIR),
8090
)
8191

8292

planet/clients/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,10 @@
1717
from .subscriptions import SubscriptionsClient
1818

1919
__all__ = ['DataClient', 'OrdersClient', 'SubscriptionsClient']
20+
21+
# Organize client classes by their module name to allow lookup.
22+
_client_directory = {
23+
'data': DataClient,
24+
'orders': OrdersClient,
25+
'subscriptions': SubscriptionsClient
26+
}

planet/http.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@
2222
import random
2323
import time
2424
from typing import AsyncGenerator, Optional
25+
2526
import httpx
27+
from typing_extensions import Literal
2628

2729
from .auth import Auth, AuthType
2830
from . import exceptions, models
@@ -413,6 +415,29 @@ async def stream(
413415
finally:
414416
await response.aclose()
415417

418+
def client(self,
419+
name: Literal['data', 'orders', 'subscriptions'],
420+
base_url: Optional[str] = None) -> object:
421+
"""Get a client by its module name.
422+
423+
Parameters:
424+
name: one of 'data', 'orders', or 'subscriptions'.
425+
426+
Returns:
427+
A client instance.
428+
429+
Raises:
430+
ClientError when no such client can be had.
431+
432+
"""
433+
# To avoid circular dependency.
434+
from planet.clients import _client_directory
435+
436+
try:
437+
return _client_directory[name](self, base_url=base_url)
438+
except KeyError:
439+
raise exceptions.ClientError("No such client.")
440+
416441

417442
class AuthSession(BaseSession):
418443
"""Synchronous connection to the Planet Auth service."""

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
'jsonschema',
3131
'pyjwt>=2.1',
3232
'tqdm>=4.56',
33+
'typing-extensions',
3334
]
3435

3536
test_requires = ['pytest', 'pytest-asyncio==0.16', 'pytest-cov', 'respx==0.19']

tests/unit/test_session.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
"""Session module tests."""
2+
3+
import pytest
4+
5+
from planet import DataClient, OrdersClient, SubscriptionsClient, Session
6+
from planet.exceptions import ClientError
7+
8+
9+
@pytest.mark.parametrize("client_name,client_class",
10+
[('data', DataClient), ('orders', OrdersClient),
11+
('subscriptions', SubscriptionsClient)])
12+
async def test_session_get_client(client_name, client_class):
13+
"""Get a client from a session."""
14+
async with Session() as session:
15+
client = session.client(client_name)
16+
assert isinstance(client, client_class)
17+
18+
19+
async def test_session_get_client_error():
20+
"""Get an exception when no such client exists."""
21+
async with Session() as session:
22+
with pytest.raises(ClientError):
23+
_ = session.client('bogus')

0 commit comments

Comments
 (0)