diff --git a/.gitignore b/.gitignore index 2937d4e..c9af326 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,5 @@ tests/config.py .venv venv + +private \ No newline at end of file diff --git a/ecoscape_layers/redlist.py b/ecoscape_layers/redlist.py index 0e753b5..cbbf6cd 100644 --- a/ecoscape_layers/redlist.py +++ b/ecoscape_layers/redlist.py @@ -13,7 +13,7 @@ def __init__(self, redlist_key: str, ebird_key: str | None = None): Initializes a RedList object. API keys are required to access the IUCN Red List API and eBird API respectively; see the documentation for more information. """ - self.redlist_params = {"token": redlist_key} + self.redlist_params = {"Authorization": redlist_key} self.ebird_key = ebird_key def get_from_redlist(self, url: str) -> dict: @@ -23,14 +23,14 @@ def get_from_redlist(self, url: str) -> dict: :param url: the URL for the request. :return: response for the request. """ - res = requests.get(url, params=self.redlist_params) + res = requests.get(url, headers=self.redlist_params) if res.status_code != 200: raise ValueError(f"Error {res.status_code} in Red List API request") data: dict = res.json() - return data["result"] + return data def get_scientific_name(self, species_code: str) -> str: """Translates eBird codes to scientific names for use in Red List. @@ -70,7 +70,7 @@ def get_scientific_name(self, species_code: str) -> str: return sci_name def get_habitat_data( - self, species_name: str, region=None, ebird_code: bool = False + self, species_name: str, ebird_code: bool = True ) -> dict[int, dict[str, str | bool]]: """Gets habitat assessments for suitability for a given species. This also adds the associated landcover/terrain map's code to the API response, @@ -79,8 +79,7 @@ def get_habitat_data( Args: species_name (str): scientific name of the species. - region (_type_, optional): a specific region to assess habitats in (see https://apiv3.iucnredlist.org/api/v3/docs#regions).. Defaults to None. - ebird_code (bool, optional): If True, reads species_name as an eBird species_code and converts it to a scientific/iucn name. Defaults to False. + ebird_code (bool, optional): If True, reads species_name as an eBird species_code and converts it to a scientific/iucn name. Defaults to True. Raises: ValueError: Errors when the code received from the IUCN Redlist is missing a period or data after a period. @@ -95,20 +94,30 @@ def get_habitat_data( sci_name = self.get_scientific_name(species_name) else: sci_name = species_name - - url = f"https://apiv3.iucnredlist.org/api/v3/habitats/species/name/{sci_name}" - if region is not None: - url += f"/region/{region}" - - habs = self.get_from_redlist(url) + # Split sci_name into genus and species + genus, species = sci_name.split() + + url = f"https://api.iucnredlist.org/api/v4/taxa/scientific_name?genus_name={genus}&species_name={species}" + + assessments = self.get_from_redlist(url)["assessments"] + + # Get assessment code for latest global assessment for species + latest_assessment = [i for i in assessments if ((i["latest"] == True) & (i["scopes"][0]["code"] == '1'))][0]["assessment_id"] + + # Get habitats from latest global assessment for species + url = f"https://api.iucnredlist.org/api/v4/assessment/{latest_assessment}" + + habs = self.get_from_redlist(url)["habitats"] + res = {} for hab in habs: code = str(hab["code"]) + # TODO: is this necessary? Codes seem to be x_x instea of xx.xx # some codes are in the format xx.xx.xx instead of xx.xx # we will truncate xx.xx.xx codes to xx.xx - code_sep = code.split(".") + code_sep = code.split("_") # check that code_sep len is not less than len of 2 if len(code_sep) < 2: @@ -125,7 +134,7 @@ def get_habitat_data( code_sep = map(lambda num_str: num_str.zfill(2), code_sep) # Convert bool like strings to bools - hab["majorimportance"] = hab["majorimportance"] == "Yes" + hab["majorImportance"] = hab["majorImportance"] == "Yes" hab["suitability"] = hab["suitability"] == "Suitable" # create a map_code that is represented by an int diff --git a/ecoscape_layers/utils.py b/ecoscape_layers/utils.py index 96e0fc4..1d76d8c 100644 --- a/ecoscape_layers/utils.py +++ b/ecoscape_layers/utils.py @@ -228,7 +228,7 @@ def default_refinement_method( if map_code not in habitats: return 1.0 - if habitats[map_code]["majorimportance"]: + if habitats[map_code]["majorImportance"]: return 0.0 elif habitats[map_code]["suitability"]: return 0.1 @@ -326,7 +326,7 @@ def get_current_habitat( Inputs for the overrides may consist of integers that represent specific map codes and strings that represent keywords, which will then be converted into map codes. These keywords can take the form of the IUCN Habitat Classification Scheme categories listed in the constants.py file. Additionally, you can - specify "majorimportance" or "suitable" to only use habitats with these qualities for a species. + specify "majorImportance" or "suitable" to only use habitats with these qualities for a species. Examples: overrides_forest308: ["forest", 308] @@ -352,19 +352,19 @@ def get_current_habitat( # replace all IUCN habitat classification scheme keywords with map codes overrides = iucn_habs_to_codes(overrides) - # replace keywords (majorimportance, suitable) with map codes + # replace keywords (majorImportance, suitable) with map codes # search for the keywords and error on invalid keywords major_i = None suit_i = None for i in range(len(overrides)): - if overrides[i] == "majorimportance": + if overrides[i] == "majorImportance": major_i = i elif overrides[i] == "suitable": suit_i = i elif type(overrides[i]) is str: error = f"""\ Keyword {overrides[i]} not found in IUCN habitat classification scheme keywords - and is not 'majorimportance' or 'suitable'.""" + and is not 'majorImportance' or 'suitable'.""" raise KeyError(dedent(error)) # define function to add new codes based on keyword @@ -390,9 +390,9 @@ def replace_keywords(keyword: str, keyword_i: int): # add new codes based on keywords if major_i is not None: - replace_keywords("majorimportance", major_i) + replace_keywords("majorImportance", major_i) if suit_i is not None: - replace_keywords("suitable", suit_i) + replace_keywords("suitability", suit_i) # remove and duplicates due to overlap overrides = list(set(overrides)) @@ -408,7 +408,7 @@ def replace_keywords(keyword: str, keyword_i: int): # The default action is to return map_codes in which habitat is considered major importance by IUCN output = [] for code, hab in habitats.items(): - if hab["majorimportance"]: + if hab["majorImportance"]: output.append(code) return output diff --git a/tests/layers_test.py b/tests/layers_test.py new file mode 100644 index 0000000..36795f7 --- /dev/null +++ b/tests/layers_test.py @@ -0,0 +1,60 @@ +import os +import sys +sys.path.append(".") + +from ecoscape_layers import ( + LayerGenerator, + warp, + RedList, + generate_resistance_table, + in_habs, + default_refinement_method, +) + + + +species_list = ["cowpig1", "ibgshr1"] +# species_list = ["acowoo", "stejay"] + +# Paths +BASE_DIR = "." +DATA_PATH = os.path.join(BASE_DIR, "tests") + +# Load keys +REDLIST_KEY = open(os.path.join(BASE_DIR, "private/iucn_key.txt"), "r").read().strip(r' ') +EBIRD_KEY = open(os.path.join(BASE_DIR, "private/ebird_key.txt"), "r").read().strip(r' ') + +# Initialze redlist object with keys +redlist = RedList(REDLIST_KEY, EBIRD_KEY) + + +landcover_fn = os.path.join(DATA_PATH, "inputs", "test_terrain_spain.tif") + +layer_generator = LayerGenerator(landcover_fn, REDLIST_KEY, EBIRD_KEY) +redlist = RedList(REDLIST_KEY, EBIRD_KEY) + + +habitat_data = {} + +for species_code in species_list: + habitat_fn = os.path.join(DATA_PATH, "outputs", species_code, "habitat_test.tif") + resistance_dict_fn = os.path.join(DATA_PATH, "outputs", species_code, "resistance_test.csv") + range_fn = os.path.join(DATA_PATH, "outputs", species_code, "range_map_2022.gpkg") + + range_src = "ebird" + + # get IUCN Redlist Habitat data + habitat_data = redlist.get_habitat_data(species_code, ebird_code=True) + + # create the resistance csv + generate_resistance_table(habitat_data, resistance_dict_fn) + + # create the habitat layer + layer_generator.generate_habitat( + species_code, + habitat_data, + habitat_fn, + range_fn, + range_src, + current_hab_overrides = ["suitable"] + )