Skip to content

Commit

Permalink
order contract id (#93)
Browse files Browse the repository at this point in the history
* api_key + basic auth deprecation notes in client docstring

* charge order to explicitly provided contract
  • Loading branch information
grmpflh27 authored Dec 23, 2024
1 parent a7ecf5e commit 83aa6c5
Show file tree
Hide file tree
Showing 2 changed files with 49 additions and 25 deletions.
51 changes: 33 additions & 18 deletions capella_console_client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,22 +48,24 @@ class CapellaConsoleClient:
API docs: https://docs.capellaspace.com/accessing-data/searching-for-data
Args:
email: email on api.capellaspace.com
password: password on api.capellaspace.com
token: valid JWT access token
email: email on api.capellaspace.com [will be deprecated in 2025]
password: password on api.capellaspace.com [will be deprecated in 2025]
token: JWT access token
api_key: api key for api.capellaspace.com
verbose: flag to enable verbose logging
no_token_check: do not check if provided JWT token or API KEY is valid
base_url: Capella console API base URL override
search_url: Capella catalog/search/ override
no_auth: bypass authentication
NOTE:
not providing either email and password or a jwt token for authentication
will prompt you for email and password, which is not what you want in a script
not providing either `email` & `password` or `token` or `api_key`
will prompt you for email and password, which is not what you want in a script [will be deprecated in 2025]
NOTE: Precedence order (high to low)
1. email and password
NOTE: precedence order (high to low)
1. email and password [will be deprecated in 2025]
2. JWT token
3. API key
"""

def __init__(
Expand Down Expand Up @@ -319,6 +321,7 @@ def review_order(
self,
stac_ids: Optional[List[str]] = None,
items: Optional[Union[List[Dict[str, Any]], SearchResult]] = None,
contract_id: Optional[str] = None,
) -> Dict[str, Any]:
stac_ids = _validate_stac_id_or_stac_items(stac_ids, items)

Expand All @@ -332,7 +335,7 @@ def review_order(
raise NoValidStacIdsError(f"No valid STAC IDs in {', '.join(stac_ids)}")

# review order
order_payload = self._construct_order_payload(stac_items)
order_payload = self._construct_order_payload(stac_items, contract_id)
review_order_response = self._sesh.post("/orders/review", json=order_payload).json()

if not review_order_response.get("authorized", False):
Expand All @@ -346,6 +349,7 @@ def submit_order(
check_active_orders: bool = False,
omit_search: bool = False,
omit_review: bool = False,
contract_id: Optional[str] = None,
) -> str:
"""
submit an order by `stac_ids` or `items`.
Expand All @@ -362,6 +366,7 @@ def submit_order(
if False: submits a new order and returns new order ID
omit_search: omit search to ensure provided STAC IDs are valid - only works if `items` are provided
omit_review: omit review stage
contract_id: charge order on explicit contract (if omitted default contract is used)
Returns:
str: order UUID
"""
Expand All @@ -386,10 +391,10 @@ def submit_order(
raise NoValidStacIdsError(f"No valid STAC IDs in {', '.join(stac_ids)}")

if not omit_review:
self.review_order(items=stac_items)
self.review_order(items=stac_items, contract_id=contract_id)

logger.info(f"submitting order for {', '.join(stac_ids)}")
order_payload = self._construct_order_payload(stac_items)
order_payload = self._construct_order_payload(stac_items, contract_id)
res_order = self._sesh.post("/orders", json=order_payload)

con = res_order.json()
Expand All @@ -400,15 +405,20 @@ def submit_order(
logger.info(f"successfully submitted order {order_id}")
return order_id # type: ignore

def _construct_order_payload(self, stac_items):
def _construct_order_payload(self, stac_items, contract_id: Optional[str] = None):
by_collect_id = defaultdict(list)
for item in stac_items:
by_collect_id[item["collection"]].append(item["id"])

order_items = []
for collection, stac_ids_of_coll in by_collect_id.items():
order_items.extend([{"collectionId": collection, "granuleId": stac_id} for stac_id in stac_ids_of_coll])
return {"items": order_items}

payload = {"items": order_items}
if contract_id:
payload["contractId"] = contract_id # type: ignore

return payload

def _find_active_order(self, stac_ids: List[str]) -> Optional[str]:
"""
Expand Down Expand Up @@ -568,6 +578,7 @@ def download_products(
show_progress: bool = False,
separate_dirs: bool = True,
product_types: List[Union[str, ProductType]] = None,
contract_id: Optional[str] = None,
) -> Dict[str, Dict[str, Path]]:
"""
download all assets of multiple products
Expand Down Expand Up @@ -608,6 +619,7 @@ def download_products(
/tmp/<stac_id_2>.tif
...
product_types: filter by product type, e.g. ["SLC", "GEO"]
contract_id: charge order on explicit contract (if omitted default contract is used)
Returns:
Dict[str, Dict[str, Path]]: Local paths of downloaded files keyed by STAC id and asset type, e.g.
Expand All @@ -634,7 +646,9 @@ def download_products(
exclude = _validate_and_filter_asset_types(exclude)

if not items_presigned:
items_presigned = self._resolve_items_presigned(order_id, tasking_request_id, collect_id, product_types)
items_presigned = self._resolve_items_presigned(
order_id, tasking_request_id, collect_id, product_types, contract_id
)

len_items_presigned = len(items_presigned)
suffix = "s" if len_items_presigned > 1 else ""
Expand Down Expand Up @@ -675,6 +689,7 @@ def _resolve_items_presigned(
tasking_request_id: Optional[str] = None,
collect_id: Optional[str] = None,
product_types: List[str] = None,
contract_id: Optional[str] = None,
) -> List[Dict[str, Any]]:
stac_ids = None

Expand All @@ -685,18 +700,18 @@ def _resolve_items_presigned(
# 2 - submit order for tasking_request_id
if tasking_request_id:
_validate_uuid(tasking_request_id)
order_id, stac_ids = self._order_products_for_task(tasking_request_id, product_types) # type: ignore
order_id, stac_ids = self._order_products_for_task(tasking_request_id, product_types, contract_id) # type: ignore
# 3 - submit order for collect_id
else:
_validate_uuid(collect_id)
order_id, stac_ids = self._order_products_for_collect_ids(
collect_ids=[collect_id], product_types=product_types # type: ignore
collect_ids=[collect_id], product_types=product_types, contract_id=contract_id # type: ignore
)

return self.get_presigned_items(order_id, stac_ids) # type: ignore

def _order_products_for_task(
self, tasking_request_id: str, product_types: List[str] = None
self, tasking_request_id: str, product_types: List[str] = None, contract_id: Optional[str] = None
) -> Tuple[str, List[str]]:
"""
order all products associated with a tasking request
Expand All @@ -709,7 +724,7 @@ def _order_products_for_task(
return self._order_products_for_collect_ids(collect_ids, product_types)

def _order_products_for_collect_ids(
self, collect_ids: List[str], product_types: List[str] = None
self, collect_ids: List[str], product_types: List[str] = None, contract_id: Optional[str] = None
) -> Tuple[str, List[str]]:
search_kwargs = dict(
collect_id__in=collect_ids,
Expand All @@ -722,7 +737,7 @@ def _order_products_for_collect_ids(
logger.warning("No STAC items found ... aborting")
sys.exit(0)

order_id = self.submit_order(items=result, omit_search=True, check_active_orders=True)
order_id = self.submit_order(items=result, omit_search=True, check_active_orders=True, contract_id=contract_id)
return order_id, result.stac_ids

def download_product(
Expand Down
23 changes: 16 additions & 7 deletions capella_console_client/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ class CapellaConsoleClientError(Exception):

def __init__(self, message=None, code=None, data={}, response=None):
self.response = response
if message:
self.message = message
if code:
self.code = code
if data:
self.data = data
self.message = message or None
self.code = code or None
self.data = data or {}

self.args = self._set_args()

def _set_args(self):
return [cur for cur in (self.message, self.code, self.data) if cur]


class AuthenticationError(CapellaConsoleClientError):
Expand Down Expand Up @@ -64,12 +66,17 @@ class RepeatRequestPayloadValidationError(CapellaConsoleClientError):
pass


class ContractNotFoundError(CapellaConsoleClientError):
pass


DEFAULT_ERROR_CODE = "GENERAL_API_ERROR"
INVALID_TOKEN_ERROR_CODE = "INVALID_TOKEN"
ORDER_EXPIRED_ERROR_CODE = "ORDER_EXPIRED"
COLLECTION_ACCESS_DENIED_ERROR_CODE = "COLLECTION_ACCESS_DENIED"
NOT_AUTHORIZED_ERROR_CODE = "NOT_AUTHORIZED"
ORDER_VALIDATION_ERROR_CODE = "ORDER_VALIDATION_ERROR"
CONTRACT_NOT_FOUND = "CONTRACT_NOT_FOUND"

UNAUTHORIZED_MESSAGE = "unauthorized"

Expand All @@ -79,6 +86,7 @@ class RepeatRequestPayloadValidationError(CapellaConsoleClientError):
COLLECTION_ACCESS_DENIED_ERROR_CODE: CollectionAccessDeniedError,
NOT_AUTHORIZED_ERROR_CODE: AuthorizationError,
ORDER_VALIDATION_ERROR_CODE: OrderValidationError,
CONTRACT_NOT_FOUND: ContractNotFoundError,
}

ERROR_CODES_BY_MESSAGE_SNIP = {
Expand All @@ -98,6 +106,7 @@ def handle_error_response_and_raise(response):
if not response.is_stream_consumed:
response.read()
error = response.json()

try:
if "error" in error:
error = error["error"]
Expand All @@ -123,4 +132,4 @@ def handle_error_response_and_raise(response):
except StopIteration:
pass
exc = ERROR_CODES.get(code, CapellaConsoleClientError)(message=message, code=code, data=data, response=response)
raise exc
raise exc from None

0 comments on commit 83aa6c5

Please sign in to comment.