Skip to content
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ tests/config.py

.venv
venv

private
37 changes: 23 additions & 14 deletions ecoscape_layers/redlist.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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.
Expand Down Expand Up @@ -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,
Expand All @@ -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.
Expand All @@ -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:
Expand All @@ -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
Expand Down
16 changes: 8 additions & 8 deletions ecoscape_layers/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]
Expand All @@ -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
Expand All @@ -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))
Expand All @@ -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
Expand Down
60 changes: 60 additions & 0 deletions tests/layers_test.py
Original file line number Diff line number Diff line change
@@ -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"]
)