"""ISO 3166 country database with community naming conventions."""
import os
import logging
import pycountry
logger = logging.getLogger(__name__)
# `nomenclature` uses pycountry to (optionally) add all countries and ISO3 codes
# For readability and in line with conventions of the IAMC community,
# several "standard" country names are shortened
# Please keep this list in sync with `templates/model-registration-template.xlsx`
PYCOUNTRY_NAME_OVERRIDE = {
"Bolivia, Plurinational State of": "Bolivia",
"Holy See (Vatican City State)": "Vatican",
"Micronesia, Federated States of": "Micronesia",
"Congo, The Democratic Republic of the": "Democratic Republic of the Congo",
"Iran, Islamic Republic of": "Iran",
"Korea, Republic of": "South Korea",
"Korea, Democratic People's Republic of": "North Korea",
"Lao People's Democratic Republic": "Laos",
"Syrian Arab Republic": "Syria",
"Moldova, Republic of": "Moldova",
"Tanzania, United Republic of": "Tanzania",
"Venezuela, Bolivarian Republic of": "Venezuela",
"Palestine, State of": "Palestine",
"Taiwan, Province of China": "Taiwan",
"Türkiye": "Turkey", # Revert change in pycountry on Sep 29, 2023
"Virgin Islands, British": "British Virgin Islands",
"Virgin Islands, U.S.": "United States Virgin Islands",
}
PYCOUNTRY_NAME_ADD = [
dict(
name="Kosovo",
alpha_3="KOS", # See https://olympics.com/ioc/kosovo
alpha_2="XK", # See https://en.wikipedia.org/wiki/XK_(user_assigned_code)
note="Kosovo is recognized under UNSC resolution 1244, "
"using preliminary alpha_3 and alpha_2 codes (ISO-3166-1 not assigned)",
),
]
# The European Commission uses alternative ISO2 codes
ALTERNATIVE_ALPHA2_CODES = {
"EL": "GR",
"UK": "GB",
}
[docs]
class Countries(pycountry.ExistingCountries):
"""List of countries based on simplified ISO 3166 database
This list follows the :class:`nomenclature.countries` module based on
country names using ISO 3166-1, but it simplifies several country names
for readability and in line with conventions of the IAMC community.
"""
def __init__(self):
super().__init__(os.path.join(pycountry.DATABASE_DIR, "iso3166-1.json"))
# Modify country names
for iso_name, nc_name in PYCOUNTRY_NAME_OVERRIDE.items():
obj = self.get(name=iso_name)
obj.name = nc_name
obj.iso_name = iso_name
obj.note = "Name changed from ISO 3166 in line with community standards"
self.indices["name"][nc_name.lower()] = obj
# Add countries that are not officially recognized
# but relevant for IAM community
for entry in PYCOUNTRY_NAME_ADD:
obj = self.data_class(**entry)
self.objects.append(obj)
for key, value in entry.items():
# Lookups and searches are case insensitive. Normalize here.
value = value.lower()
if key in self.no_index:
continue
index = self.indices.setdefault(key, {})
if value in index:
raise ValueError(f"Index conflict for country {key}: {value}")
index[value] = obj
[docs]
def get(self, **kwargs):
"""Get a specific country by attribute
Parameters
----------
name : str
Country name
alpha_3 : str
Alpha-3 code (ISO3)
alpha_2 : str
Alpha-2 code (ISO2)
Returns
-------
:class:`pycountry.db.Country`
"""
country = super().get(**kwargs)
# Special handling for alpha-2 codes used by the European Commission
if country is None and "alpha_2" in kwargs:
country = super().get(alpha_2=ALTERNATIVE_ALPHA2_CODES[kwargs["alpha_2"]])
if country is not None:
logger.info(
f"You are using non-standard alpha-2 codes for {country.name}."
)
return country
[docs]
def get_mapping(self, from_attr: str, to_attr: str) -> dict[str, str]:
"""Get a mapping from one country attribute to another
Parameters
----------
from_attr : str
Source attribute (one of "name", "alpha_3", "alpha_2")
to_attr : str
Target attribute (one of "name", "alpha_3", "alpha_2")
Returns
-------
dict
Mapping from source attribute to target attribute for all countries
Examples
--------
>>> countries.get_mapping("alpha_3", "name")
{'USA': 'United States', 'DEU': 'Germany', ...}
>>> countries.get_mapping("alpha_2", "alpha_3")
{'US': 'USA', 'DE': 'DEU', ...}
"""
valid_attrs = {"name", "alpha_3", "alpha_2"}
if from_attr not in valid_attrs or to_attr not in valid_attrs:
raise ValueError(
f"Attributes must be one of {valid_attrs}, "
f"got from_attr='{from_attr}', to_attr='{to_attr}'"
)
return {
getattr(country, from_attr): getattr(country, to_attr)
for country in self.objects
if hasattr(country, from_attr) and hasattr(country, to_attr)
}
@property
def names(self) -> list[str]:
return [country.name for country in self.objects]
# Initialize `countries` for direct access via API and in codelist module
countries = Countries()