33import urllib .parse
44import warnings
55from collections .abc import Callable , Iterable , Iterator
6- from typing import (
7- Any ,
8- )
6+ from datetime import datetime
7+ from typing import Any
98
109from httpx import URL , Request
1110from httpx ._types import TimeoutTypes
1211from pydantic import AnyUrl
13- from stapi_pydantic import Link , Order , OrderCollection , Product , ProductsCollection
12+ from stapi_pydantic import (
13+ CQL2Filter ,
14+ Link ,
15+ Opportunity ,
16+ OpportunityCollection ,
17+ OpportunityPayload ,
18+ Order ,
19+ OrderCollection ,
20+ OrderPayload ,
21+ Product ,
22+ ProductsCollection ,
23+ )
1424
1525from pystapi_client .conformance import ConformanceClasses
1626from pystapi_client .exceptions import APIError
@@ -237,11 +247,11 @@ def _supports_opportunities(self) -> bool:
237247 def _supports_async_opportunities (self ) -> bool :
238248 return self .has_conformance (ConformanceClasses .ASYNC_OPPORTUNITIES )
239249
240- def get_products (self , limit : int | None = None ) -> Iterator [ProductsCollection ]:
250+ def get_products (self , limit : int | None = None ) -> Iterator [Product ]:
241251 """Get all products from this STAPI API
242252
243253 Returns:
244- ProductsCollection: A collection of STAPI Products
254+ Iterator[Product]: An iterator of STAPI Products
245255 """
246256 products_endpoint = self ._get_products_href ()
247257
@@ -250,11 +260,11 @@ def get_products(self, limit: int | None = None) -> Iterator[ProductsCollection]
250260 else :
251261 parameters = {"limit" : limit }
252262
253- products_collection_iterator = self . stapi_io . get_pages (
254- products_endpoint , parameters = parameters , lookup_key = "products"
255- )
263+ products_link = Link ( href = products_endpoint , method = "GET" , body = parameters , rel = "" )
264+
265+ products_collection_iterator = self . stapi_io . get_pages ( link = products_link , lookup_key = "products" )
256266 for products_collection in products_collection_iterator :
257- yield ProductsCollection .model_validate (products_collection )
267+ yield from ProductsCollection .model_validate (products_collection ). products
258268
259269 def get_product (self , product_id : str ) -> Product :
260270 """Get a single product from this STAPI API
@@ -264,34 +274,102 @@ def get_product(self, product_id: str) -> Product:
264274
265275 Returns:
266276 Product: A STAPI Product
277+ """
278+ product_endpoint = self ._get_products_href (product_id )
279+ product_json = self .stapi_io .read_json (endpoint = product_endpoint )
280+ return Product .model_validate (product_json )
267281
268- Raises:
269- ValueError if product_id does not exist.
282+ def get_product_opportunities (
283+ self ,
284+ product_id : str ,
285+ date_range : tuple [str , str ],
286+ geometry : dict [str , Any ],
287+ cql2_filter : CQL2Filter | None = None , # type: ignore[type-arg]
288+ limit : int = 10 ,
289+ ) -> Iterator [Opportunity ]: # type: ignore[type-arg]
290+ # TODO Update return type after the pydantic model generic type is fixed
291+ """Get all opportunities for a product from this STAPI API
292+ Args:
293+ product_id: The Product ID to get opportunities for
294+ opportunity_parameters: The parameters for the opportunities
295+
296+ Returns:
297+ Iterator[Opportunity]: An iterator of STAPI Opportunities
270298 """
299+ product_opportunities_endpoint = self ._get_products_href (product_id , subpath = "opportunities" )
300+
301+ opportunity_parameters = OpportunityPayload .model_validate (
302+ {
303+ "datetime" : (
304+ datetime .fromisoformat (date_range [0 ]),
305+ datetime .fromisoformat (date_range [1 ]),
306+ ),
307+ "geometry" : geometry ,
308+ "filter" : cql2_filter ,
309+ "limit" : limit ,
310+ }
311+ )
312+ opportunities_first_page_link = Link (
313+ href = product_opportunities_endpoint , method = "POST" , body = opportunity_parameters .model_dump (), rel = ""
314+ )
315+ opportunities_first_page_json , next_link = self .stapi_io ._get_next_page (
316+ opportunities_first_page_link , "features"
317+ )
271318
272- product_endpoint = self ._get_products_href (product_id )
273- product_json = self .stapi_io .read_json (product_endpoint )
319+ if opportunities_first_page_json :
320+ opportunities_first_page = OpportunityCollection .model_validate (opportunities_first_page_json ) # type:ignore[var-annotated]
321+ yield from opportunities_first_page .features
322+ else :
323+ return
274324
275- if product_json is None :
276- raise ValueError ( f"Product { product_id } not found" )
325+ if next_link is None :
326+ return
277327
278- return Product .model_validate (product_json )
328+ product_opportunities_json = self .stapi_io .get_pages (link = next_link , lookup_key = "features" )
329+
330+ for opportunity_collection in product_opportunities_json :
331+ yield from OpportunityCollection .model_validate (opportunity_collection ).features
332+
333+ def create_product_order (self , product_id : str , order_parameters : OrderPayload ) -> Order : # type: ignore[type-arg]
334+ # TODO Update return type after the pydantic model generic type is fixed
335+ """Create an order for a product
336+
337+ Args:
338+ product_id: The Product ID to place an order for
339+ order_parameters: The parameters for the order
340+ """
341+ product_order_endpoint = self ._get_products_href (product_id , subpath = "orders" )
342+ product_order_json = self .stapi_io .read_json (
343+ endpoint = product_order_endpoint , method = "POST" , parameters = order_parameters .model_dump ()
344+ )
279345
280- def _get_products_href (self , product_id : str | None = None ) -> str :
346+ return Order .model_validate (product_order_json )
347+
348+ def _get_products_href (self , product_id : str | None = None , subpath : str | None = None ) -> str :
281349 product_link = self .get_single_link ("products" )
282350 if product_link is None :
283351 raise ValueError ("No products link found" )
284352 product_url = URL (str (product_link .href ))
353+
354+ path = None
355+
285356 if product_id is not None :
286- product_url = product_url .copy_with (path = f"{ product_url .path } /{ product_id } " )
357+ path = f"{ product_url .path } /{ product_id } "
358+
359+ if subpath is not None :
360+ path = f"{ path } /{ subpath } "
361+
362+ if path is not None :
363+ product_url = product_url .copy_with (path = path )
364+
287365 return str (product_url )
288366
289- def get_orders (self , limit : int | None = None ) -> Iterator [OrderCollection ]: # type: ignore[type-arg]
367+ def get_orders (self , limit : int | None = None ) -> Iterator [Order ]: # type: ignore[type-arg]
290368 # TODO Update return type after the pydantic model generic type is fixed
291369 """Get orders from this STAPI API
292370
293371 Returns:
294- OrderCollection: A collection of STAPI Orders
372+ Iterator[Order]: An iterator of STAPI Orders
295373 """
296374 orders_endpoint = self ._get_orders_href ()
297375
@@ -300,11 +378,11 @@ def get_orders(self, limit: int | None = None) -> Iterator[OrderCollection]: #
300378 else :
301379 parameters = {"limit" : limit }
302380
303- orders_collection_iterator = self . stapi_io . get_pages (
304- orders_endpoint , parameters = parameters , lookup_key = "features"
305- )
381+ orders_link = Link ( href = orders_endpoint , method = "GET" , body = parameters , rel = "" )
382+
383+ orders_collection_iterator = self . stapi_io . get_pages ( link = orders_link , lookup_key = "features" )
306384 for orders_collection in orders_collection_iterator :
307- yield OrderCollection .model_validate (orders_collection )
385+ yield from OrderCollection .model_validate (orders_collection ). features
308386
309387 def get_order (self , order_id : str ) -> Order : # type: ignore[type-arg]
310388 # TODO Update return type after the pydantic model generic type is fixed
0 commit comments