"""Country configuration setting base class."""
from pathlib import Path
from typing import Dict, List, Optional, Tuple, Union
from pydantic import BaseModel, root_validator, validator
from ochanticipy.utils.io import parse_yaml
[docs]
class CodABConfig(BaseModel):
"""COD AB configuration.
Parameters
----------
admin_level_max: int
The maximum admin level available in the shapefile, cannot be
greater than 4. If ``hdx_resource_name`` is a list, then
``admin_level_max`` must match the number of items in
``hdx_resource_name``.
hdx_resource_name: Union[str, List[str]]
COD AB resource name on HDX. Can be found by taking the filename as it
appears on the COD AB dataset page. If individual COD AB files are
contained in separate resources, provide each filename in a list, where
the index is equivalent to admin level.
layer_base_name: str
The base name of the different admin layers, that presumably only
change by a single custom_layer_number depending on the level. Should
contain {admin_level} in place of the custom_layer_number.
admin{level}_name: str, optional
The names of any admin level layers that do not conform to the
layer_base_name pattern, where {level} ranges from 0 to 4
custom_layer_names: list, optional
Any additional layer names that don't fit into the admin level paradigm
"""
admin_level_max: int
hdx_resource_name: Union[str, List[str]]
layer_base_name: str
admin0_name: Optional[str]
admin1_name: Optional[str]
admin2_name: Optional[str]
admin3_name: Optional[str]
admin4_name: Optional[str]
custom_layer_names: Optional[List[str]]
@validator("hdx_resource_name")
def _validate_hdx_resource_name(cls, hdx_resource_name, values):
"""Ensure hdx_resource_name is str or list for all admin areas."""
if isinstance(hdx_resource_name, list) and (
len(hdx_resource_name) != values["admin_level_max"] + 1
):
raise ValueError(
"In the COD AB section of the country configuration file, "
"hdx_resource_name should be a string or list of length "
"admin_level_max, with a resource provided for each level "
"from 0 to admin_level_max."
)
return hdx_resource_name
@validator("layer_base_name")
def _validate_layer_base_name(cls, layer_base_name):
"""Ensure that the layer basename contains {admin_level}."""
if "{admin_level}" not in layer_base_name:
raise ValueError(
"In the COD AB section of the country configuration file, "
"layer_base_name must contain an {admin_level} placeholder."
)
return layer_base_name
@validator("admin_level_max")
def _validate_admin_level_max(cls, admin_level_max):
"""Ensure that admin_level_max is between 0 and 4."""
if not 0 <= admin_level_max <= 4:
raise ValueError(
"In the COD AB section of the country configuration file, "
"admin_level_max must be between 0 and 4."
)
return admin_level_max
@root_validator(pre=False, skip_on_failure=True)
def _set_admin_levels(cls, values) -> dict:
"""Set admin levels names using layer base name."""
for admin_level in range(values["admin_level_max"] + 1):
if values.get(f"admin{admin_level}_name", None) is None:
values[f"admin{admin_level}_name"] = values[
"layer_base_name"
].format(admin_level=admin_level)
return values
[docs]
class FewsNetConfig(BaseModel):
"""FEWS NET configuration.
Parameters
----------
region_name: str
Name of the region the country belongs to. Needed to download the
regional FEWS NET data
"""
# values dictionary gets build in order attributes are listed
# so first define the dict before the region_name
region_name_code_mapping: Dict[str, str] = {
"caribbean-central-america": "LAC",
"central-asia": "CA",
"east-africa": "EA",
"southern-africa": "SA",
"west-africa": "WA",
}
region_name: str
[docs]
@validator("region_name")
def regionname_valid(cls, v, values):
"""Check that regionname is one of the valid ones."""
valid_regionnames = values["region_name_code_mapping"].keys()
if v not in valid_regionnames:
raise ValueError(
f"Invalid region name: {v}. "
f"Should be one of {', '.join(valid_regionnames)}"
)
return v
@root_validator(pre=False, skip_on_failure=True)
def _set_region_code(cls, values) -> dict:
"""Set region code based on region name."""
values["region_code"] = values["region_name_code_mapping"][
values["region_name"]
]
return values
[docs]
class ReportingPoints(BaseModel):
"""Coordinates of GloFAS reporting points."""
name: str
lon: float
lat: float
[docs]
class GlofasConfig(BaseModel):
"""GloFAS configuration."""
reporting_points: List[ReportingPoints]
[docs]
class UsgsNdviConfig(BaseModel):
"""USGS NDVI configuration.
Parameters
----------
area_name : str
Name of the USGS NDVI coverage area the country belongs to.
Needed to download the regional NDVI data.
"""
area_name_mapping: Dict[str, Tuple[str, str]] = {
"north-africa": ("africa/north", "na"),
"east-africa": ("africa/east", "ea"),
"southern-africa": ("africa/southern", "sa"),
"west-africa": ("africa/west", "wa"),
"central-asia": ("asia/centralasia", "cta"),
"yemen": ("asia/middleeast/yemen", "yem"),
"central-america": ("lac/camcar/centralamerica", "ca"),
"hispaniola": ("lac/camcar/caribbean/hispaniola", "hi"),
}
area_name: str
[docs]
@validator("area_name")
def area_name_valid(cls, v, values) -> str:
"""Check that area_name is valid."""
valid_area_names = values["area_name_mapping"].keys()
if v not in valid_area_names:
raise ValueError(
f"Invalid area name: {v}. "
f"Should be one of {', '.join(valid_area_names)}"
)
return v
@root_validator(pre=False, skip_on_failure=True)
def _set_area_codes(cls, values) -> dict:
"""Set NDVI url and prefix from area."""
values["area_url"], values["area_prefix"] = values[
"area_name_mapping"
][values["area_name"]]
return values
[docs]
class CountryConfig(BaseModel):
"""Country configuration.
Parameters
----------
iso3 : str
Country ISO3, must be exactly 3 letters long
codab: CodABConfig, optional
Configuration object for COD AB
fewsnet: FewsNetConfig, optional
Configuration object for FEWS NET
glofas: GlofasConfig, optional
Configuration object for GloFAS
usgs_ndvi: UsgsNdviConfig, optional
Configuration object for USGS NDVI
"""
iso3: str
codab: Optional[CodABConfig]
fewsnet: Optional[FewsNetConfig]
glofas: Optional[GlofasConfig]
usgs_ndvi: Optional[UsgsNdviConfig]
@validator("iso3")
def _validate_iso3(cls, iso3):
"""Ensure ISO3 is length three and alphabet chars only."""
return _validate_iso3(iso3)
[docs]
def create_country_config(iso3: str) -> CountryConfig:
"""
Return a country configuration object from AA Toolbox.
Parameters
----------
iso3 : str
Country ISO3, must be exactly 3 letters long
Returns
-------
CountryConfig instance
"""
iso3 = _validate_iso3(iso3)
try:
parameters = parse_yaml(
Path(__file__).parent.resolve() / f"countries/{iso3}.yaml"
)
except FileNotFoundError as err:
raise FileNotFoundError(
f"A configuration file for {iso3.upper()} is not yet available "
f"in AA Toolbox. Try using a custom configuration file with "
f"create_custom_country_config instead, or contact us to "
f"request that we add this country."
) from err
return CountryConfig(**parameters)
[docs]
def create_custom_country_config(filepath: Union[str, Path]) -> CountryConfig:
"""
Return a custom country configuration object.
Parameters
----------
filepath: str, pathlib.Path
Path to the configuration file
Returns
-------
CountryConfig instance
"""
return CountryConfig(**parse_yaml(filepath))
def _validate_iso3(iso3: str):
"""Ensure ISO3 is length three and alphabet chars only."""
if len(iso3) != 3 or not str.isalpha(iso3):
raise ValueError("ISO3 must be a three letter string.")
return iso3.lower()