diff --git a/src/temporal/t.stac/libstac/__init__.py b/src/temporal/t.stac/libstac/__init__.py index e69de29bb2..aac562c450 100644 --- a/src/temporal/t.stac/libstac/__init__.py +++ b/src/temporal/t.stac/libstac/__init__.py @@ -0,0 +1 @@ +import staclib as libstac # noqa diff --git a/src/temporal/t.stac/libstac/staclib.py b/src/temporal/t.stac/libstac/staclib.py index ea609d6007..22bdf497f3 100644 --- a/src/temporal/t.stac/libstac/staclib.py +++ b/src/temporal/t.stac/libstac/staclib.py @@ -1,13 +1,197 @@ -import grass.script as gs -from grass.pygrass.gis.region import Region -from grass.pygrass.vector import VectorTopo -from grass.pygrass.vector.geometry import Point, Area, Centroid, Boundary +#!/usr/bin/env python3 + +############################################################################ +# +# MODULE: staclib +# AUTHOR: Corey T. White, OpenPlains Inc. & NCSU +# PURPOSE: Helper library to import STAC data in to GRASS. +# COPYRIGHT: (C) 2024 Corey White +# This program is free software under the GNU General +# Public License (>=v2). Read the file COPYING that +# comes with GRASS for details. +# +############################################################################# + + +import os import base64 import tempfile import json -import os -from pystac_client.conformance import ConformanceClasses -from pystac_client.exceptions import APIError +import grass.script as gs +from grass.exceptions import CalledModuleError +from grass.pygrass.vector import VectorTopo +from grass.pygrass.vector.geometry import Point, Centroid, Boundary +from concurrent.futures import ThreadPoolExecutor + +# Import pystac_client modules +try: + from pystac_client import Client + from pystac_client.exceptions import APIError + from pystac_client.conformance import ConformanceClasses +except ImportError as err: + gs.fatal(_("Unable to import pystac_client: {err}")) + + +def _import_tqdm(error): + """Import tqdm module""" + try: + from tqdm import tqdm + + return tqdm + except ImportError as err: + if error: + raise err + return None + + +def _import_pystac_mediatype(error): + """Import pystac module""" + try: + from pystac import MediaType + + return MediaType + except ImportError as err: + if error: + raise err + return None + + +class STACHelper: + """STAC Helper Class""" + + def __init__(self): + self.client = None + + def connect_to_stac(self, url, headers=None): + """Connect to a STAC catalog.""" + if self.client is None: + try: + self.client = Client.open(url, headers) + return self.client + except APIError as err: + gs.fatal(f"Failed to connect to STAC catalog: {err}") + else: + gs.warning(_("Client already connected.")) + return self.client + + def get_all_collections(self): + """Get a list of collections from STAC Client""" + if self.conforms_to_collections(): + gs.verbose(_("Client conforms to Collection")) + try: + collections = self.client.get_collections() + collection_list = list(collections) + return [i.to_dict() for i in collection_list] + + except APIError as e: + gs.fatal(_("Error getting collections: {}".format(e))) + + def get_collection(self, collection_id): + """Get a collection from STAC Client""" + try: + collection = self.client.get_collection(collection_id) + self.collection = collection.to_dict() + return self.collection + + except APIError as e: + gs.fatal(_("Error getting collection: {}".format(e))) + + def search_api(self, **kwargs): + """Search the STAC API""" + if self.conforms_to_item_search(): + gs.verbose(_("STAC API Conforms to Item Search")) + + if kwargs.get("filter"): + self.conforms_to_filter() + + if kwargs.get("query"): + self.conforms_to_query() + + try: + search = self.client.search(**kwargs) + except APIError as e: + gs.fatal(_("Error searching STAC API: {}".format(e))) + except NotImplementedError as e: + gs.fatal(_("Error searching STAC API: {}".format(e))) + except Exception as e: + gs.fatal(_("Error searching STAC API: {}".format(e))) + + try: + gs.message(_(f"Search Matched: {search.matched()} items")) + except e: + gs.warning(_(f"No items found: {e}")) + return None + + return search + + def report_stac_item(self, item): + """Print a report of the STAC item to the console.""" + gs.message(_(f"Collection ID: {item.collection_id}")) + gs.message(_(f"Item: {item.id}")) + print_attribute(item, "geometry", "Geometry") + gs.message(_(f"Bbox: {item.bbox}")) + + print_attribute(item, "datetime", "Datetime") + print_attribute(item, "start_datetime", "Start Datetime") + print_attribute(item, "end_datetime", "End Datetime") + gs.message(_("Extra Fields:")) + print_summary(item.extra_fields) + + print_list_attribute(item.stac_extensions, "Extensions:") + # libstac.print_attribute(it_import_tqdmem, "stac_extensions", "Extensions") + gs.message(_("Properties:")) + print_summary(item.properties) + + def _check_conformance(self, conformance_class, response="fatal"): + """Check if the STAC API conforms to the given conformance class""" + if not self.client.conforms_to(conformance_class): + if response == "fatal": + gs.fatal(_(f"STAC API does not conform to {conformance_class}")) + return False + elif response == "warning": + gs.warning(_(f"STAC API does not conform to {conformance_class}")) + return True + elif response == "verbose": + gs.verbose(_(f"STAC API does not conform to {conformance_class}")) + return True + elif response == "info": + gs.info(_(f"STAC API does not conform to {conformance_class}")) + return True + elif response == "message": + gs.message(_(f"STAC API does not conform to {conformance_class}")) + return True + + def conforms_to_collections(self): + """Check if the STAC API conforms to the Collections conformance class""" + return self._check_conformance(ConformanceClasses.COLLECTIONS) + + def conforms_to_item_search(self): + """Check if the STAC API conforms to the Item Search conformance class""" + return self._check_conformance(ConformanceClasses.ITEM_SEARCH, "warning") + + def conforms_to_filter(self): + """Check if the STAC API conforms to the Filter conformance class""" + return self._check_conformance(ConformanceClasses.FILTER, response="warning") + + def conforms_to_query(self): + """Check if the STAC API conforms to the Query conformance class""" + return self._check_conformance(ConformanceClasses.QUERY, response="warning") + + def conforms_to_sort(self): + """Check if the STAC API conforms to the Sort conformance class""" + return self._check_conformance(ConformanceClasses.SORT) + + def conforms_to_fields(self): + """Check if the STAC API conforms to the Fields conformance class""" + return self._check_conformance(ConformanceClasses.FIELDS) + + def conforms_to_core(self): + """Check if the STAC API conforms to the Core conformance class""" + return self._check_conformance(ConformanceClasses.CORE) + + def conforms_to_context(self): + """Check if the STAC API conforms to the Context conformance class""" + return self._check_conformance(ConformanceClasses.CONTEXT) def encode_credentials(username, password): @@ -140,6 +324,66 @@ def print_basic_collection_info(collection): gs.message(_(f"{'-' * 75}\n")) +def collection_metadata(collection): + """Get collection""" + + gs.message(_("*" * 80)) + gs.message(_(f"Collection Id: {collection.get('id')}")) + gs.message(_(f"Title: {collection.get('title')}")) + gs.message(_(f"Description: {collection.get('description')}")) + + extent = collection.get("extent") + if extent: + spatial = extent.get("spatial") + if spatial: + bbox = spatial.get("bbox") + if bbox: + gs.message(_(f"bbox: {bbox}")) + temporal = extent.get("temporal") + if temporal: + interval = temporal.get("interval") + if interval: + gs.message(_(f"Temporal Interval: {interval}")) + + gs.message(_(f"License: {collection.get('license')}")) + gs.message(_(f"Keywords: {collection.get('keywords')}")) + # gs.message(_(f"Providers: {collection.get('providers')}")) + gs.message(_(f"Links: {collection.get('links')}")) + gs.message(_(f"Stac Extensions: {collection.get('stac_extensions')}")) + + try: + gs.message(_("\n# Summaries:")) + print_summary(collection.get("summaries")) + except AttributeError: + gs.info(_("Summaries not found.")) + + try: + gs.message(_("\n# Extra Fields:")) + print_summary(collection.get("extra_fields")) + except AttributeError: + gs.info(_("# Extra Fields not found.")) + gs.message(_("*" * 80)) + + +def report_plain_asset_summary(asset): + MediaType = _import_pystac_mediatype(False) + gs.message(_("\nAsset")) + gs.message(_(f"Asset Item Id: {asset.get('item_id')}")) + + gs.message(_(f"Asset Title: {asset.get('title')}")) + gs.message(_(f"Asset Filename: {asset.get('file_name')}")) + gs.message(_(f"Raster bands: {asset.get('raster:bands')}")) + gs.message(_(f"Raster bands: {asset.get('eo:bands')}")) + gs.message(_(f"Asset Description: {asset.get('description')}")) + + if MediaType: + gs.message(_(f"Asset Media Type: { MediaType(asset.get('type')).name}")) + else: + gs.message(_(f"Asset Media Type: {asset.get('type')}")) + gs.message(_(f"Asset Roles: {asset.get('roles')}")) + gs.message(_(f"Asset Href: {asset.get('href')}")) + + def region_to_wgs84_decimal_degrees_bbox(): """convert region bbox to wgs84 decimal degrees bbox""" region = gs.parse_command("g.region", quiet=True, flags="ubg") @@ -289,25 +533,12 @@ def _flatten_dict(d, parent_key="", sep="_"): def create_vector_from_feature_collection(vector, search, limit, max_items): """Create a vector from items in a Feature Collection""" - n_matched = None - try: - n_matched = search.matched() - except Exception: - gs.verbose(_("STAC API doesn't support matched() method.")) - - if n_matched: - pages = (n_matched // max_items) + 1 - else: - # These requests tend to be very slow - pages = len(list(search.pages())) - - gs.message(_(f"Fetching items {n_matched} from {pages} pages.")) feature_collection = {"type": "FeatureCollection", "features": []} # Extract asset information for each item - for page in range(pages): - temp_features = search.item_collection_as_dict() + for page in search.pages_as_dicts(): + temp_features = page for idx, item in enumerate(temp_features["features"]): flattened_assets = _flatten_dict( item["assets"], parent_key="assets", sep="." @@ -466,74 +697,78 @@ def create_metadata_vector(vector, metadata): return metadata -def get_all_collections(client): - """Get a list of collections from STAC Client""" - if conform_to_collections(client): - gs.verbose(_("Client conforms to Collection")) - try: - collections = client.get_collections() - collection_list = list(collections) - return [i.to_dict() for i in collection_list] - - except APIError as e: - gs.fatal(_("Error getting collections: {}".format(e))) +def import_grass_raster(params): + assets, resample_method, extent, resolution, resolution_value, memory = params + gs.message(_(f"Downloading Asset: {assets}")) + input_url = check_url_type(assets["href"]) + gs.message(_(f"Import Url: {input_url}")) - -def _check_conformance(client, conformance_class, response="fatal"): - """Check if the STAC API conforms to the given conformance class""" - if not client.conforms_to(conformance_class): - if response == "fatal": - gs.fatal(_(f"STAC API does not conform to {conformance_class}")) - return False - elif response == "warning": - gs.warning(_(f"STAC API does not conform to {conformance_class}")) - return True - elif response == "verbose": - gs.verbose(_(f"STAC API does not conform to {conformance_class}")) - return True - elif response == "info": - gs.info(_(f"STAC API does not conform to {conformance_class}")) - return True - elif response == "message": - gs.message(_(f"STAC API does not conform to {conformance_class}")) - return True - - -def conform_to_collections(client): - """Check if the STAC API conforms to the Collections conformance class""" - return _check_conformance(client, ConformanceClasses.COLLECTIONS) - - -def conform_to_item_search(client): - """Check if the STAC API conforms to the Item Search conformance class""" - return _check_conformance(client, ConformanceClasses.ITEM_SEARCH) - - -def conform_to_filter(client): - """Check if the STAC API conforms to the Filter conformance class""" - return _check_conformance(client, ConformanceClasses.FILTER) - - -def conform_to_query(client): - """Check if the STAC API conforms to the Query conformance class""" - return _check_conformance(client, ConformanceClasses.QUERY) - - -def conform_to_sort(client): - """Check if the STAC API conforms to the Sort conformance class""" - return _check_conformance(client, ConformanceClasses.SORT) - - -def conform_to_fields(client): - """Check if the STAC API conforms to the Fields conformance class""" - return _check_conformance(client, ConformanceClasses.FIELDS) - - -def conform_to_core(client): - """Check if the STAC API conforms to the Core conformance class""" - return _check_conformance(client, ConformanceClasses.CORE) - - -def conform_to_context(client): - """Check if the STAC API conforms to the Context conformance class""" - return _check_conformance(client, ConformanceClasses.CONTEXT) + try: + gs.message(_(f"Importing: {assets['file_name']}")) + gs.parse_command( + "r.import", + input=input_url, + output=assets["file_name"], + resample=resample_method, + extent=extent, + resolution=resolution, + resolution_value=resolution_value, + title=assets["file_name"], + memory=memory, + quiet=True, + ) + except CalledModuleError as e: + gs.fatal(_("Error importing raster: {}".format(e.stderr))) + + +def download_assets( + assets, + resample_method, + resample_extent, + resolution, + resolution_value, + memory=300, + nprocs=1, +): + """Downloads a list of images from the given URLs to the given filenames.""" + number_of_assets = len(assets) + resample_extent_list = [resample_extent] * number_of_assets + resolution_list = [resolution] * number_of_assets + resolution_value_list = [resolution_value] * number_of_assets + resample_method_list = [resample_method] * number_of_assets + memory_list = [memory] * number_of_assets + max_cpus = os.cpu_count() - 1 + if nprocs > max_cpus: + gs.warning( + _( + "Number of processes {nprocs} is greater than the number of CPUs {max_cpus}." + ) + ) + nprocs = max_cpus + + def execute_import_grass_raster(pbar=None): + with ThreadPoolExecutor(max_workers=nprocs) as executor: + try: + for _a in executor.map( + import_grass_raster, + zip( + assets, + resample_method_list, + resample_extent_list, + resolution_list, + resolution_value_list, + memory_list, + ), + ): + if pbar: + pbar.update(1) + except Exception as e: + gs.fatal(_("Error importing raster: {}".format(str(e)))) + + tqdm = _import_tqdm(False) + if tqdm is None: + gs.warning(_("tqdm module not found. Progress bar will not be displayed.")) + execute_import_grass_raster() + else: + with tqdm(total=number_of_assets, desc="Downloading assets") as pbar: + execute_import_grass_raster(pbar) diff --git a/src/temporal/t.stac/t.stac.catalog/t.stac.catalog.py b/src/temporal/t.stac/t.stac.catalog/t.stac.catalog.py index 0f3835e8f0..76026fe410 100644 --- a/src/temporal/t.stac/t.stac.catalog/t.stac.catalog.py +++ b/src/temporal/t.stac/t.stac.catalog/t.stac.catalog.py @@ -64,10 +64,22 @@ # %end import sys +import json +from contextlib import contextmanager from pprint import pprint import grass.script as gs from grass.pygrass.utils import get_lib_path -import json + + +@contextmanager +def add_sys_path(new_path): + """Add a path to sys.path and remove it when done""" + original_sys_path = sys.path[:] + sys.path.append(new_path) + try: + yield + finally: + sys.path = original_sys_path path = get_lib_path(modname="t.stac", libname="staclib") @@ -78,14 +90,17 @@ def main(): """Main function""" - import staclib as libstac - try: - from pystac_client import Client - from pystac_client.exceptions import APIError - except ImportError: - gs.fatal(_("pystac_client is not installed.")) - return None + # Import dependencies + path = get_lib_path(modname="t.stac", libname="staclib") + if path is None: + gs.fatal("Not able to find the stac library directory.") + + with add_sys_path(path): + try: + import staclib as libstac + except ImportError as err: + gs.fatal(f"Unable to import staclib: {err}") # STAC Client options client_url = options["url"] # required @@ -99,23 +114,24 @@ def main(): req_headers = libstac.set_request_headers(settings) try: - client = Client.open(client_url, headers=req_headers) + stac_helper = libstac.STACHelper() + client = stac_helper.connect_to_stac(client_url, req_headers) # Check if the client conforms to the STAC Item Search # This will exit the program if the client does not conform - libstac.conform_to_item_search(client) + stac_helper.conforms_to_item_search() if format == "plain": gs.message(_(f"Client Id: {client.id}")) gs.message(_(f"Client Title: {client.title}")) gs.message(_(f"Client Description: {client.description}")) gs.message(_(f"Client STAC Extensions: {client.stac_extensions}")) - gs.message(_(f"Client Extra Fields: {client.extra_fields}")) + # gs.message(_(f"Client Extra Fields: {client.extra_fields}")) gs.message(_(f"Client catalog_type: {client.catalog_type}")) gs.message(_(f"{'-' * 75}\n")) # Get all collections - collection_list = libstac.get_all_collections(client) + collection_list = stac_helper.get_all_collections() gs.message(_(f"Collections: {len(collection_list)}\n")) gs.message(_(f"{'-' * 75}\n")) @@ -139,8 +155,8 @@ def main(): json_output = json.dumps(client.to_dict()) return json_output - except APIError as e: - gs.fatal(_("APIError Error opening STAC API: {}".format(e))) + except Exception as e: + gs.fatal(_("Error: {}".format(e))) if __name__ == "__main__": diff --git a/src/temporal/t.stac/t.stac.collection/t.stac.collection.py b/src/temporal/t.stac/t.stac.collection/t.stac.collection.py index 70a38d8268..4c0167c574 100644 --- a/src/temporal/t.stac/t.stac.collection/t.stac.collection.py +++ b/src/temporal/t.stac/t.stac.collection/t.stac.collection.py @@ -72,35 +72,48 @@ # %end import sys +import json from pprint import pprint +from contextlib import contextmanager import grass.script as gs from grass.pygrass.utils import get_lib_path - -from pystac_client import Client -from pystac_client.exceptions import APIError -from pystac_client.conformance import ConformanceClasses - +# Add the stac library to the sys.path path = get_lib_path(modname="t.stac", libname="staclib") if path is None: gs.fatal("Not able to find the stac library directory.") sys.path.append(path) -def get_all_collections(client): - """Get a list of collections from STAC Client""" +@contextmanager +def add_sys_path(new_path): + """Add a path to sys.path and remove it when done""" + original_sys_path = sys.path[:] + sys.path.append(new_path) try: - collections = client.get_collections() - collection_list = list(collections) - return [i.to_dict() for i in collection_list] + yield + finally: + sys.path = original_sys_path + - except APIError as e: - gs.fatal(_("Error getting collections: {}".format(e))) +path = get_lib_path(modname="t.stac", libname="staclib") +if path is None: + gs.fatal("Not able to find the stac library directory.") +sys.path.append(path) def main(): """Main function""" - import staclib as libstac + # Import dependencies + path = get_lib_path(modname="t.stac", libname="staclib") + if path is None: + gs.fatal("Not able to find the stac library directory.") + + with add_sys_path(path): + try: + import staclib as libstac + except ImportError as err: + gs.fatal(f"Unable to import staclib: {err}") # STAC Client options client_url = options["url"] # required @@ -117,36 +130,20 @@ def main(): settings = options["settings"] req_headers = libstac.set_request_headers(settings) - try: - client = Client.open(client_url, headers=req_headers) - except APIError as e: - gs.fatal(_("APIError Error opening STAC API: {}".format(e))) - - if libstac.conform_to_collections(client): - gs.verbose(_("Conforms to STAC Collections")) + # Connect to STAC API + stac_helper = libstac.STACHelper() + stac_helper.connect_to_stac(client_url, req_headers) + stac_helper.conforms_to_collections() if collection_id: - try: - collection = client.get_collection(collection_id) - collection_dict = collection.to_dict() - if format == "json": - gs.message(_(f"collection: {collection}")) - return collection_dict - # return pprint(collection.to_dict()) - elif format == "plain": - if basic_info: - return libstac.print_basic_collection_info(collection_dict) - return libstac.print_summary(collection_dict) - - except APIError as e: - gs.fatal(_("APIError Error getting collection: {}".format(e))) - - # Create metadata vector - # if vector_metadata: - # gs.message(_(f"Outputting metadata to {vector_metadata}")) - # libstac.create_metadata_vector(vector_metadata, collection_list) - # gs.message(_(f"Metadata written to {vector_metadata}")) - # return vector_metadata + collection_dict = stac_helper.get_collection(collection_id) + if format == "json": + return json.dumps(collection_dict) + # return pprint(collection.to_dict()) + elif format == "plain": + if basic_info: + return libstac.print_basic_collection_info(collection_dict) + return libstac.print_summary(collection_dict) if __name__ == "__main__": diff --git a/src/temporal/t.stac/t.stac.item/t.stac.item.py b/src/temporal/t.stac/t.stac.item/t.stac.item.py index 00fbdbc19a..a4ce9a3199 100644 --- a/src/temporal/t.stac/t.stac.item/t.stac.item.py +++ b/src/temporal/t.stac/t.stac.item/t.stac.item.py @@ -261,102 +261,27 @@ import sys from pprint import pprint import json - -# from multiprocessing.pool import ThreadPool -from pystac_client import Client -from pystac_client.exceptions import APIError -from pystac import MediaType -from concurrent.futures import ThreadPoolExecutor -from tqdm import tqdm -import tempfile - +from contextlib import contextmanager import grass.script as gs from grass.pygrass.utils import get_lib_path -from grass.exceptions import CalledModuleError +# Add the stac library to the sys.path path = get_lib_path(modname="t.stac", libname="staclib") if path is None: gs.fatal("Not able to find the stac library directory.") sys.path.append(path) -import staclib as libstac - - -def search_stac_api(client, **kwargs): - """Search the STAC API""" - if libstac.conform_to_item_search(client): - gs.verbose(_("STAC API Conforms to Item Search")) - try: - search = client.search(**kwargs) - except APIError as e: - gs.fatal(_("Error searching STAC API: {}".format(e))) - except NotImplementedError as e: - gs.fatal(_("Error searching STAC API: {}".format(e))) - except Exception as e: - gs.fatal(_("Error searching STAC API: {}".format(e))) +@contextmanager +def add_sys_path(new_path): + """Add a path to sys.path and remove it when done""" + original_sys_path = sys.path[:] + sys.path.append(new_path) try: - gs.message(_(f"Search Matched: {search.matched()} items")) - # These requests tend to be very slow - # gs.message(_(f"Pages: {len(list(search.pages()))}")) - # gs.message(_(f"Max items per page: {len(list(search.items()))}")) - - except e: - gs.warning(_(f"No items found: {e}")) - return None - - return search - - -def collection_metadata(collection): - """Get collection""" - - gs.message(_("*" * 80)) - gs.message(_(f"Collection Id: {collection.id}")) - - libstac.print_attribute(collection, "title", "Collection Title") - libstac.print_attribute(collection, "description", "Description") - gs.message(_(f"Spatial Extent: {collection.extent.spatial.bboxes}")) - gs.message(_(f"Temporal Extent: {collection.extent.temporal.intervals}")) - - libstac.print_attribute(collection, "license") - libstac.print_attribute(collection, "keywords") - libstac.print_attribute(collection, "links") - libstac.print_attribute(collection, "providers") - libstac.print_attribute(collection, "stac_extensions", "Extensions") - - try: - gs.message(_("\n# Summaries:")) - libstac.print_summary(collection.summaries.to_dict()) - except AttributeError: - gs.info(_("Summaries not found.")) - - try: - gs.message(_("\n# Extra Fields:")) - libstac.print_summary(collection.extra_fields) - except AttributeError: - gs.info(_("# Extra Fields not found.")) - gs.message(_("*" * 80)) - - -def report_stac_item(item): - """Print a report of the STAC item to the console.""" - gs.message(_(f"Collection ID: {item.collection_id}")) - gs.message(_(f"Item: {item.id}")) - libstac.print_attribute(item, "geometry", "Geometry") - gs.message(_(f"Bbox: {item.bbox}")) - - libstac.print_attribute(item, "datetime", "Datetime") - libstac.print_attribute(item, "start_datetime", "Start Datetime") - libstac.print_attribute(item, "end_datetime", "End Datetime") - gs.message(_("Extra Fields:")) - libstac.print_summary(item.extra_fields) - - libstac.print_list_attribute(item.stac_extensions, "Extensions:") - # libstac.print_attribute(item, "stac_extensions", "Extensions") - gs.message(_("Properties:")) - libstac.print_summary(item.properties) + yield + finally: + sys.path = original_sys_path def collect_item_assets(item, assset_keys, asset_roles): @@ -381,90 +306,18 @@ def collect_item_assets(item, assset_keys, asset_roles): return asset_dict -def report_plain_asset_summary(asset): - gs.message(_("\nAsset")) - gs.message(_(f"Asset Item Id: {asset.get('item_id')}")) - - gs.message(_(f"Asset Title: {asset.get('title')}")) - gs.message(_(f"Asset Filename: {asset.get('file_name')}")) - gs.message(_(f"Raster bands: {asset.get('raster:bands')}")) - gs.message(_(f"Raster bands: {asset.get('eo:bands')}")) - gs.message(_(f"Asset Description: {asset.get('description')}")) - gs.message(_(f"Asset Media Type: { MediaType(asset.get('type')).name}")) - gs.message(_(f"Asset Roles: {asset.get('roles')}")) - gs.message(_(f"Asset Href: {asset.get('href')}")) - - -def import_grass_raster(params): - assets, resample_method, extent, resolution, resolution_value, memory = params - gs.message(_(f"Downloading Asset: {assets}")) - input_url = libstac.check_url_type(assets["href"]) - gs.message(_(f"Import Url: {input_url}")) - - try: - gs.message(_(f"Importing: {assets['file_name']}")) - gs.parse_command( - "r.import", - input=input_url, - output=assets["file_name"], - resample=resample_method, - extent=extent, - resolution=resolution, - resolution_value=resolution_value, - title=assets["file_name"], - memory=memory, - quiet=True, - ) - except CalledModuleError as e: - gs.fatal(_("Error importing raster: {}".format(e.stderr))) - - -def download_assets( - assets, - resample_method, - resample_extent, - resolution, - resolution_value, - memory=300, - nprocs=1, -): - """Downloads a list of images from the given URLs to the given filenames.""" - number_of_assets = len(assets) - resample_extent_list = [resample_extent] * number_of_assets - resolution_list = [resolution] * number_of_assets - resolution_value_list = [resolution_value] * number_of_assets - resample_method_list = [resample_method] * number_of_assets - memory_list = [memory] * number_of_assets - max_cpus = os.cpu_count() - 1 - if nprocs > max_cpus: - gs.warning( - _( - "Number of processes {nprocs} is greater than the number of CPUs {max_cpus}." - ) - ) - nprocs = max_cpus - - with tqdm(total=number_of_assets, desc="Downloading assets") as pbar: - with ThreadPoolExecutor(max_workers=nprocs) as executor: - try: - for _a in executor.map( - import_grass_raster, - zip( - assets, - resample_method_list, - resample_extent_list, - resolution_list, - resolution_value_list, - memory_list, - ), - ): - pbar.update(1) - except Exception as e: - gs.fatal(_("Error importing raster: {}".format(str(e)))) - - def main(): """Main function""" + # Import dependencies + path = get_lib_path(modname="t.stac", libname="staclib") + if path is None: + gs.fatal("Not able to find the stac library directory.") + + with add_sys_path(path): + try: + import staclib as libstac + except ImportError as err: + gs.fatal(f"Unable to import staclib: {err}") # STAC Client options client_url = options["url"] # required @@ -512,29 +365,23 @@ def main(): search_params = {} # Store STAC API search parameters collection_items_assets = [] - try: + # Set the request headers + settings = options["settings"] + req_headers = libstac.set_request_headers(settings) - # Set the request headers - settings = options["settings"] - req_headers = libstac.set_request_headers(settings) - - client = Client.open(client_url, headers=req_headers) - except APIError as e: - gs.fatal(_("APIError Error opening STAC API: {}".format(e))) - - try: - collection = client.get_collection(collection_id) - except APIError as e: - gs.fatal(_(f"Error getting collection {collection_id}: {e}")) + # Connect to STAC API + stac_helper = libstac.STACHelper() + stac_helper.connect_to_stac(client_url, req_headers) + collection = stac_helper.get_collection(collection_id) if summary_metadata: if format == "plain": - return collection_metadata(collection) + return libstac.collection_metadata(collection) elif format == "json": - return pprint(collection.to_dict()) + return json.dumps(collection) else: # Return plain text by default - return collection_metadata(collection) + return libstac.collection_metadata(collection) # Start item search @@ -573,8 +420,6 @@ def main(): if filter_lang: search_params["filter_lang"] = filter_lang - if libstac.conform_to_query(client): - gs.verbose(_("STAC API Conforms to Item Search Query")) if query: if isinstance(query, str): query = json.loads(query) @@ -591,7 +436,8 @@ def main(): search_params["bbox"] = bbox # Search the STAC API - items_search = search_stac_api(client=client, **search_params) + items_search = stac_helper.search_api(**search_params) + # Create vector layer of items metadata if items_vector: libstac.create_vector_from_feature_collection( @@ -606,10 +452,10 @@ def main(): if format == "plain": gs.message(_(f"Items Found: {len(list(items))}")) for item in items: - report_stac_item(item) + stac_helper.report_stac_item(item) return None if format == "json": - return pprint([item.to_dict() for item in items]) + return json.dumps([item.to_dict() for item in items]) for item in items: asset = collect_item_assets(item, asset_keys, asset_roles=item_roles) @@ -624,13 +470,13 @@ def main(): if asset_metadata: for asset in collection_items_assets: if format == "plain": - report_plain_asset_summary(asset) + libstac.report_plain_asset_summary(asset) if format == "json": - pprint(asset) + return pprint(asset) if download: # Download and Import assets - download_assets( + libstac.download_assets( assets=collection_items_assets, resample_method=method, resample_extent=extent,