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

Planet sync client #1049

merged 23 commits into from
Dec 12, 2024

Conversation

stephenhillier
Copy link
Contributor

@stephenhillier stephenhillier commented Jul 3, 2024

Proposed Changes:

Adds a Planet sync client that has feature parity with the existing async clients.

Diff of User Interface

New behavior:

The Planet client has members data, subscriptions, orders. Users can interact with these APIs without setting up individual clients or a Session.

Example:

from planet import Planet

client = Planet()

for item in client.subscriptions.list_subscriptions():
    print(item)

results = client.data.search(["PSScene"], limit=10)
for item in results:
    print(item.get("id"))

Minor changes from the async clients to the sync client:

  • for the Subscriptions client, instead of having both get_results and get_results_csv methods, the sync client has one method that optionally accepts format="csv" as a parameter. It defaults to the normal json output.

At the risk of scope creep, one additional nice-to-have for an initial MVP sync client would be some simple dataclass/attrs based return types instead of dicts. That way it's easier for users to understand what they're getting back in e.g. a search or order.

@ischneider
Copy link
Member

Nice work!

I'm definitely a big 👍 on the single Planet class.

I did a small test listing my searches and then concurrently executing each search using both sync and async (threadpool and queue/task approach) and found the performance to be essentially equivalent w/ both...


I'm less keen on duplication of all the documentation and boiler plate in testing, though I understand this is due to function "color".

One approach I just tested was using a metaclass to dynamically generate sync wrappers. The advantage of this is documentation is mirrored and there's much less code. One disadvantage is type hints aren't handled properly - the actual return types are correct but IDE doc shows up incorrectly... Would be happy to share this but I'm not satisfied with the lack of type hint propagation (and maybe this can be addressed?) but otherwise, beyond some trickery, it's simple and it works.


Another approach we might consider is top-level "request objects" [1] - this way the interface to the request (the name of the operation and the arguments) can always be sync and the results are where dispatch can vary. This way the docs for the operation parameters, exceptions, narrative etc are all in one spot and not repeated and the only code variation is essentially the call_sync (as it is now).

So list_searches might be implemented like

search = data.ListSearches(session, limit=500)
# sync
searches = search.get_data()
# async
searches = await search.aget_data()

Advantage here is parity w/ SH API.

Another approach might be to use a request object but require a specific client, e.g.

client = AsyncClient(session)
# list returns an async generator
searches = await client.list(data.ListSearches(limit=500))

The downside is we still have the old client classes, docs, etc. to maintain for some period of time...

[1] see SentinelHubRequest https://sentinelhub-py.readthedocs.io/en/latest/examples/process_request.html

@ischneider
Copy link
Member

Another approach - by extending DataClient and overriding every function (as sync and with altered type hints), we get the best of both (can inherit doc strings and get correct type hints) with less boilerplate...

vscode at least makes the overriding part simpler but then there's still maintenance (adding an optional parameter, for example) and the boilerplate of testing though at least w/ some creativity, I'd guess we could reduce this some...

@stephenhillier
Copy link
Contributor Author

I do like the idea of inheriting the docstrings where possible. If we can give ourselves a headstart on a simple MVP wrapper, with minimal duplication of what's already there, then that sounds good to me.

The reason I chose to start with the explicit approach is because this is a standalone client and it shouldn't necessarily be a violation of DRY merely to reuse the descriptions (IMO). They will probably need to be modified independently of each other as changes are made.

That being said I will see what I can come up with for sharing the docstrings, I should have some time to round off the week to try a couple approaches 👍

@stephenhillier
Copy link
Contributor Author

It occurs to me while going down the path of creating shared docstrings, that we might be needlessly coupling the sync and async implementations together in order to save space on the docstrings. Yes each method calls out to the async method, but the implementation of any method can be changed (if needed), optional args added, and async or sync-specific examples can be added to the docstrings. Examples are sorely needed and those can't really be shared.

The one-time port is simple and all the docs and type hints are correct (and the grunt work has been done). Thoughts?

planet/client.py Outdated Show resolved Hide resolved
Comment on lines 560 to 571
"""Get order details by Order ID.

Parameters:
order_id: The ID of the order

Returns:
JSON description of the order

Raises:
planet.exceptions.ClientError: If order_id is not a valid UUID.
planet.exceptions.APIError: On API error.
"""
Copy link
Contributor

@asonnenschein asonnenschein Nov 25, 2024

Choose a reason for hiding this comment

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

This docstring is exactly the same as the docstring for it's async counterpart, OrdersClient().get_order(). I wonder if we could simplify things by applying the docstring from OrdersClient.get_order() to this method so that the same docstring isn't defined in two places? By 'apply' I mean something like 'share' the docstring between the two methods rather than 'inherit' because the implementation in this MR doesn't follow a standard Python inheritance pattern; the OrdersClient is passed to OrdersAPI via the OrdersAPI._client property. Here's an example of what I am thinking:

class OrdersClient:
    def get_order(self):
        """Docstring for get_order()."""
        pass

class OrdersAPI:
    def get_order(self):
        self.get_order.__doc__ = self._client.get_order.__doc__
        # Add any additional docstring here
        pass

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 tend to think the negatives outweigh the positives here. Readers of the code would not have a docstring to refer to in the usual place and we also would not be able to use examples in shared docstrings since the methods are called differently.

Copy link
Contributor

Choose a reason for hiding this comment

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

Understood! FWIW I don't think dynamic vs statically defined docstrings should block this MR. Check out #1077 where I implemented a dynamic approach. If that dynamic approach passes review, we can merge to main and ship it as an enhancement at a later date.

Comment on lines +75 to +77
@pytest.fixture(scope="module")
def data_api():
return DataAPI(Session(), TEST_URL)
Copy link
Contributor

Choose a reason for hiding this comment

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

The sync DataAPI client here is instantiated and accessed directly from the DataAPI object, whereas the sync SubscriptionsAPI and OrdersAPI clients are instantiated and accessed from the Planet() wrapper class in their respective tests. Should the sync DataAPI tests follow the same pattern as the sync SubscriptionsAPI/OrdersAPI tests?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed, it makes sense to access the Data API methods the same way.

and fix spelling in async client docstring
@ischneider ischneider marked this pull request as ready for review December 12, 2024 21:23
Copy link
Member

@ischneider ischneider left a comment

Choose a reason for hiding this comment

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

Let's squash and merge this and move forward!

@ischneider ischneider merged commit 9d16dd9 into main Dec 12, 2024
9 checks passed
@ischneider ischneider deleted the steve/sync-client branch December 12, 2024 22:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants