def get_token(auth_url: str, username: str, password: str = None) -> str: """ Generate a token from the GBDX authorization service. :param auth_url: URL to authorization service, generally "https://geobigdata.io/auth/v1/oauth/token" :param username: GBDX username. :param password: GBDX password. :return: GBDX token. :raises CatalogError: if user is unauthorized, or any other kind of error generating token. """ # GBDX auth service successful response with status 200: # { # "access_token": "eyJ0eXAiOiJKV1QiLCJhbGci..." # "expires_in": 604800, # "token_type": "Bearer", # "scope": "openid email offline_access", # "refresh_token": "B35E3o-Kn7LSW5WmjIkdZP3F65inHZrWpGJx7yx_mBsGT" # } # # GBDX auth service failed response with status 400: # { # "Error": "400 Bad Request: Invalid POST data" # } params = { 'grant_type': 'password', 'username': username, 'password': password } Catalog.logger.info(f'Requesting token from {auth_url}') response = requests.post(auth_url, data=params) try: body = json.loads(response.text) except Exception as exp: raise CatalogError( 'Error requesting GBDX token. Response from GBDX authorization service is not JSON.' ) from exp token = body.get('access_token') if token: Catalog.logger.info('Token successfully received.') return token error = body.get('Error') if error: raise CatalogError(f'Error requesting GBDX token: {error}') raise CatalogError( f'Error requesting GBDX token. Unexpected response from service: {body}' )
def get_item( self, item_id: str, collection_id: str = None ) -> Union[maxarcat_client.models.Item, None]: """ Return a single item given its ID. :param item_id: Item ID :param collection_id: Optional collection ID :return: Item if it exists, else None. """ if not item_id: raise CatalogError('Required parameter item_id is empty.') if collection_id: Catalog.logger.info( f'Get item {item_id} in collection {collection_id}') try: return self._call_api( self._item_api.get_item_stac_with_http_info, collection_id=collection_id, item_id=item_id) except CatalogError as exp: if exp.status == 404: return None raise Catalog.logger.info(f'Get item {item_id}') response: maxarcat_client.models.ItemCollection = self._call_api( self._stac_api.get_search_stac_with_http_info, ids=item_id) if response.features: return response.features[0] else: return None
def _call_api(self, function, *args, **kwargs): """ Call a Swagger client method and process its response. :param function: Swagger client method to call :param args: Arbitrary arguments to pass to function :param kwargs: Arbitrar keyword arguments to pass to function :raises CatalogError: if error returned by web service :return: Service response parsed as a model object """ try: with Catalog.timer(): (body, status_code, headers) = function(*args, **kwargs) Catalog.logger.info(f'HTTP Status: {status_code}') request_id = headers.get('X-Maxar-RequestId') if request_id: Catalog.logger.info(f'Request ID: {request_id}') # Secret feature: Stash response so user can examine it in case of error self.last_response = body return body except maxarcat_client.rest.ApiException as exp: # Upon errors the Catalog methods generally return JSON with a "message" property. # API Gateway as well sometimes returns JSON with a "message" property. # So try to raise our own exception with the message if possible. Catalog.logger.info(f'HTTP Status: {exp.status}') try: body = json.loads(exp.body) message = body['message'] request_id = exp.headers.get('X-Maxar-RequestId') except Exception: # Fallback on generic error raise CatalogError( f'Service error: HTTP status {exp.status} returned.', status=exp.status) from exp else: raise CatalogError(message, status=exp.status, request_id=request_id) from exp
def get_url_json(self, url: str): """ Perform an HTTP GET on a URL from an item's assets, and parse the response as JSON. :param url: URL from an item's asset's href property :return: Parsed JSON, generally returning a dict """ logging.info(f'GET {url}') response = self._request_url(requests.get, url) try: return json.loads(str(response, encoding='utf-8')) except Exception as exp: raise CatalogError( f'Response from URL is not JSON: {url}') from exp
def _request_url(self, function, *args, **kwargs): """ Process a call to a requests method :param function: Method in the requests package, e.g. requests.get :param args: Arbitrary arguments to pass to function :param kwargs: Arbitrar keyword arguments to pass to function :return: Response body """ try: headers = {'Authorization': f'Bearer {self._token}'} with Catalog.timer(): response = function(*args, **kwargs, headers=headers) except Exception as exp: Catalog.logger.error(exp) raise Catalog.logger.info(f'HTTP Status: {response.status_code}') request_id = response.headers.get('X-Maxar-RequestId') if request_id: Catalog.logger.info(f'Request ID: {request_id}') if 200 <= response.status_code < 300: return response.content # For errors we expect the response to have a JSON properties "message" and "request_id" try: body = json.loads(response.text) message = body['message'] except Exception: # Fallback for any unrecognized error raise CatalogError( f'Service error: HTTP status {response.status_code} returned.', response, request_id) else: raise CatalogError(message, response, request_id)
def connect(username: str = None, password: str = None, **kwargs): """ Construct and return a Catalog using a GBDX token generated from the given credentials. Prompt the user to input username and password if they are not provided. :param username: GBDX username :param password: GBDX password :param kwargs: Additional keyword args to pass to Catalog constructor :return: New Catalog """ if not username and password: raise CatalogError('Cannot specify password without username') if not username: username = input('Username: '******'Password: ') token = Catalog.get_token(Catalog.default_auth_url, username, password) del password return Catalog(token, **kwargs)
def get_collection( self, collection_id=None ) -> Union[maxarcat_client.models.Collection, None]: """ Get a collection by its ID. :param collection_id: Collection to get. :raises CatalogError: On error during request. :return: Collection model, or None if collection does not exist. """ if not collection_id: raise CatalogError('Required parameter collection_id is empty') Catalog.logger.info(f'Get collection {collection_id}') try: return self._call_api( self._coll_api.get_collection_stac_with_http_info, collection_id=collection_id) except CatalogError as exp: if exp.status == 404: return None raise
def search(self, collections: list = None, bbox: list = None, intersects: dict = None, start_datetime: datetime = None, end_datetime: datetime = None, item_ids: list = None, where: str = None, orderby: str = None, limit: int = None, page: int = None, complete: bool = None) -> maxarcat_client.models.ItemCollection: """ Query the Maxar catalog. :param collections: A list of collections to query against :param bbox: Bounding box in degrees to search by. Format is a sequence of the form [west, south, east, north] or [west, south, zmin, east, north, zmax]. Optional. :param intersects: Geometry to search by. Dict of GeoJSON. Optional. :param start_datetime: :param end_datetime: :param item_ids: List of item IDs to query. :param where: STAC item properties filter. :param orderby: Columns to order result by. :param limit: Maximum number of items to return. :param page: Page number to return, starting at 1. :param complete: If False then include incomplete features in the search. These are features added to the catalog but with incomplete metadata. Most users should only request complete features. :return: GeoJSON FeatureCollection as dict """ body = {} if collections: body['collections'] = collections Catalog.logger.info(f'Search collections: {collections}') if bbox: body['bbox'] = bbox Catalog.logger.info(f'Search bbox: {bbox}') if intersects: body['intersects'] = intersects Catalog.logger.info('Search geometry: {} ...'.format( json.dumps(intersects)[:100])) # Handle date range date_range = None if start_datetime and end_datetime: date_range = '{}/{}'.format( Catalog.format_datetime_iso8601(start_datetime), Catalog.format_datetime_iso8601(end_datetime)) elif start_datetime: date_range = '{}/..'.format( Catalog.format_datetime_iso8601(start_datetime)) elif end_datetime: date_range = '../{}'.format( Catalog.format_datetime_iso8601(end_datetime)) if date_range: body['datetime'] = date_range Catalog.logger.info(f'Search time range: {date_range}') if item_ids: # Guard against a single image ID being passed in, which as a string would be interpreted as a sequence. if not isinstance(item_ids, abc.Sequence): raise CatalogError('item_ids must be a sequence') body['ids'] = list(item_ids) if where: body['where'] = where Catalog.logger.info(f'Search where: {where}') if orderby: body['orderby'] = orderby Catalog.logger.info(f'Search order by: {orderby}') if limit: body['limit'] = limit Catalog.logger.info(f'Search limit: {limit}') if page: body['page'] = page Catalog.logger.info(f'Search page: {page}') if complete is not None: complete_value = True if complete else False body['complete'] = complete_value Catalog.logger.info(f'Complete: {complete_value}') return self._call_api(self._stac_api.post_search_stac_with_http_info, body=body)