From 0b0f69b41d85b9320f4c8382f65ae32516efc4a7 Mon Sep 17 00:00:00 2001 From: Sean Gillies Date: Sun, 26 Feb 2023 16:58:40 -0700 Subject: [PATCH 1/8] Add client method to Session class It gets a client by name. Resolves #857 --- planet/__init__.py | 3 ++- planet/clients/__init__.py | 2 ++ planet/http.py | 27 +++++++++++++++++++++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/planet/__init__.py b/planet/__init__.py index 6f081a6e8..fcaf68200 100644 --- a/planet/__init__.py +++ b/planet/__init__.py @@ -16,7 +16,7 @@ from . import order_request, reporting from .__version__ import __version__ # NOQA from .auth import Auth -from .clients import DataClient, OrdersClient # NOQA +from .clients import DataClient, OrdersClient, SubscriptionsClient # NOQA from .io import collect __all__ = [ @@ -24,6 +24,7 @@ 'collect', 'DataClient' 'OrdersClient', + 'SubscriptionsClient', 'order_request', 'reporting', 'Session', diff --git a/planet/clients/__init__.py b/planet/clients/__init__.py index 9fb246c9f..d73d88515 100644 --- a/planet/clients/__init__.py +++ b/planet/clients/__init__.py @@ -14,8 +14,10 @@ # limitations under the License. from .data import DataClient from .orders import OrdersClient +from .subscriptions import SubscriptionsClient __all__ = [ 'DataClient', 'OrdersClient', + 'SubscriptionsClient', ] diff --git a/planet/http.py b/planet/http.py index 97983c07d..a3a2e66a5 100644 --- a/planet/http.py +++ b/planet/http.py @@ -22,6 +22,7 @@ import random import time from typing import AsyncGenerator, Optional + import httpx from .auth import Auth, AuthType @@ -413,6 +414,32 @@ async def stream( finally: await response.aclose() + def client(self, name: str, base_url: Optional[str] = None) -> object: + """Get a client by its name. + + Parameters: + name: the name of the client module: data, orders, or + subscriptions. + + Returns: + A client instance. + + Raises: + ClientError when no such client can be had. + + """ + # To avoid circular dependency. + from planet.clients.data import DataClient + from planet.clients.orders import OrdersClient + from planet.clients.subscriptions import SubscriptionsClient + + client_map = { + 'data': DataClient, + 'orders': OrdersClient, + 'subscriptions': SubscriptionsClient + } + return client_map[name](self, base_url=base_url) + class AuthSession(BaseSession): """Synchronous connection to the Planet Auth service.""" From 469184040626d2298899263ebae1a685991289ce Mon Sep 17 00:00:00 2001 From: Sean Gillies Date: Sun, 26 Feb 2023 18:02:02 -0700 Subject: [PATCH 2/8] Add test for the no such client case --- planet/http.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/planet/http.py b/planet/http.py index a3a2e66a5..555ff9d0e 100644 --- a/planet/http.py +++ b/planet/http.py @@ -415,7 +415,7 @@ async def stream( await response.aclose() def client(self, name: str, base_url: Optional[str] = None) -> object: - """Get a client by its name. + """Get a client by its module name. Parameters: name: the name of the client module: data, orders, or @@ -433,12 +433,14 @@ def client(self, name: str, base_url: Optional[str] = None) -> object: from planet.clients.orders import OrdersClient from planet.clients.subscriptions import SubscriptionsClient - client_map = { - 'data': DataClient, - 'orders': OrdersClient, - 'subscriptions': SubscriptionsClient - } - return client_map[name](self, base_url=base_url) + try: + return { + 'data': DataClient, + 'orders': OrdersClient, + 'subscriptions': SubscriptionsClient + }[name](self, base_url=base_url) + except KeyError: + raise exceptions.ClientError("No such client.") class AuthSession(BaseSession): From f1b7b659c7b868fceb567e231b0d305da6cf2699 Mon Sep 17 00:00:00 2001 From: Sean Gillies Date: Sun, 26 Feb 2023 18:03:15 -0700 Subject: [PATCH 3/8] Add session unit tests --- tests/unit/test_session.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 tests/unit/test_session.py diff --git a/tests/unit/test_session.py b/tests/unit/test_session.py new file mode 100644 index 000000000..bf5baca2c --- /dev/null +++ b/tests/unit/test_session.py @@ -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)]) +def test_session_get_client(client_name, client_class): + """Get a client from a session.""" + session = Session() + client = session.client(client_name) + assert isinstance(client, client_class) + + +def test_session_get_client_error(): + """Get an exception when no such client exists.""" + session = Session() + with pytest.raises(ClientError): + _ = session.client('bogus') From 762345aeea2593a8387c0aa93f1dbd386c099289 Mon Sep 17 00:00:00 2001 From: Sean Gillies Date: Sun, 26 Feb 2023 19:56:58 -0700 Subject: [PATCH 4/8] Update docs --- docs/get-started/upgrading.md | 10 +++++----- docs/python/sdk-guide.md | 14 +++++++------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/get-started/upgrading.md b/docs/get-started/upgrading.md index 7edf21f66..a82fafa2d 100644 --- a/docs/get-started/upgrading.md +++ b/docs/get-started/upgrading.md @@ -24,7 +24,7 @@ The best way of doing this is wrapping any code that invokes a client class in a ```python async with Session() as session: - client = OrdersClient(session) + client = session.client('orders') result = await client.create_order(order) # Process result ``` @@ -40,12 +40,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() @@ -74,11 +74,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). diff --git a/docs/python/sdk-guide.md b/docs/python/sdk-guide.md index 4e1ac2c02..ba7a0e07e 100644 --- a/docs/python/sdk-guide.md +++ b/docs/python/sdk-guide.md @@ -116,7 +116,7 @@ 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()) @@ -198,7 +198,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()) @@ -222,7 +222,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) @@ -272,7 +272,7 @@ from planet import collect, OrdersClient, 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()) @@ -297,7 +297,7 @@ 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()) @@ -344,7 +344,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()) @@ -364,7 +364,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' From 1ec36f8ace76bd366481e642b3676f9a8140f8c1 Mon Sep 17 00:00:00 2001 From: Sean Gillies Date: Mon, 27 Feb 2023 10:45:05 -0700 Subject: [PATCH 5/8] Use Literal type (from Python 3.8) --- planet/http.py | 8 +++++--- setup.py | 1 + 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/planet/http.py b/planet/http.py index 555ff9d0e..05b381e6e 100644 --- a/planet/http.py +++ b/planet/http.py @@ -24,6 +24,7 @@ from typing import AsyncGenerator, Optional import httpx +from typing_extensions import Literal from .auth import Auth, AuthType from . import exceptions, models @@ -414,12 +415,13 @@ async def stream( finally: await response.aclose() - def client(self, name: str, base_url: Optional[str] = None) -> object: + def client(self, + name: Literal['data', 'orders', 'subscriptions'], + base_url: Optional[str] = None) -> object: """Get a client by its module name. Parameters: - name: the name of the client module: data, orders, or - subscriptions. + name: one of 'data', 'orders', or 'subscriptions'. Returns: A client instance. diff --git a/setup.py b/setup.py index 125515461..85b94f932 100644 --- a/setup.py +++ b/setup.py @@ -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'] From 184f538fc4ec856d90824b1ca3b3aa7e3a854f2e Mon Sep 17 00:00:00 2001 From: Sean Gillies Date: Mon, 27 Feb 2023 10:53:15 -0700 Subject: [PATCH 6/8] Add a change log entry --- CHANGES.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 0e5970a70..fb8f86469 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,9 @@ +2.0b2 (TBD) + +Added: +- The Session class can now construct clients by name with its client method + (#858). + 2.0.0-beta.1 (2022-12-07) Changed: From 72f18512d41d012a7f52a8f9aaaa3aebb857fd1d Mon Sep 17 00:00:00 2001 From: Sean Gillies Date: Tue, 28 Feb 2023 12:03:41 -0700 Subject: [PATCH 7/8] Move client directory to planet/clients/__init__.py --- planet/clients/__init__.py | 7 +++++++ planet/http.py | 10 ++-------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/planet/clients/__init__.py b/planet/clients/__init__.py index d73d88515..a3d9eeb8b 100644 --- a/planet/clients/__init__.py +++ b/planet/clients/__init__.py @@ -21,3 +21,10 @@ 'OrdersClient', 'SubscriptionsClient', ] + +# Organize client classes by their module name to allow concise lookup. +client_directory = { + 'data': DataClient, + 'orders': OrdersClient, + 'subscriptions': SubscriptionsClient +} diff --git a/planet/http.py b/planet/http.py index 05b381e6e..e51288fed 100644 --- a/planet/http.py +++ b/planet/http.py @@ -431,16 +431,10 @@ def client(self, """ # To avoid circular dependency. - from planet.clients.data import DataClient - from planet.clients.orders import OrdersClient - from planet.clients.subscriptions import SubscriptionsClient + from planet.clients import client_directory try: - return { - 'data': DataClient, - 'orders': OrdersClient, - 'subscriptions': SubscriptionsClient - }[name](self, base_url=base_url) + return client_directory[name](self, base_url=base_url) except KeyError: raise exceptions.ClientError("No such client.") From 977d51a57b2292ad601d5746eb6c43b9a0e7cdea Mon Sep 17 00:00:00 2001 From: Sean Gillies Date: Tue, 28 Feb 2023 12:16:51 -0700 Subject: [PATCH 8/8] Make client directory private --- planet/clients/__init__.py | 3 ++- planet/http.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/planet/clients/__init__.py b/planet/clients/__init__.py index a3d9eeb8b..138726a7e 100644 --- a/planet/clients/__init__.py +++ b/planet/clients/__init__.py @@ -12,6 +12,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. + from .data import DataClient from .orders import OrdersClient from .subscriptions import SubscriptionsClient @@ -23,7 +24,7 @@ ] # Organize client classes by their module name to allow concise lookup. -client_directory = { +_client_directory = { 'data': DataClient, 'orders': OrdersClient, 'subscriptions': SubscriptionsClient diff --git a/planet/http.py b/planet/http.py index e51288fed..0f649ea5e 100644 --- a/planet/http.py +++ b/planet/http.py @@ -431,10 +431,10 @@ def client(self, """ # To avoid circular dependency. - from planet.clients import client_directory + from planet.clients import _client_directory try: - return client_directory[name](self, base_url=base_url) + return _client_directory[name](self, base_url=base_url) except KeyError: raise exceptions.ClientError("No such client.")