2025-06-12 14:10:49 +02:00

123 lines
4.1 KiB
Python

from __future__ import annotations
from typing import Optional, Union
from functools import wraps
import pycountry
import pycountry.db
from . import config
class EmptyCountryException(ValueError):
pass
class Country:
"""
This gets countries based on the ISO 3166-1 standart.
Two examples are:
- Country.from_alpha_2("DE")
- Country.from_alpha_3("DEU")
If the country couldn't be found, it raises a ValueError, or creates an empty object.
Empty objects return for every attribute None
"""
def __init__(self, country: Optional[str] = None, pycountry_object: Optional[pycountry.db.Country] = None) -> None:
if pycountry_object is None:
# search for the country string instead if the pycountry_object isn't given
# this also implements the optional fallback
pycountry_object = self._search_pycountry_object(country=country)
if pycountry_object is None:
raise EmptyCountryException(f"the country {country} was not found and config.fallback_country isn't set")
self.pycountry_object: pycountry.db.Country = pycountry_object
@classmethod
def _search_pycountry_object(cls, country: Optional[str], is_fallback: bool = False) -> Optional[pycountry.db.Country]:
# fallback to configured country if necessary
if country is None:
if is_fallback:
return None
return cls._search_pycountry_object(country=config.fallback_country, is_fallback=True)
pycountry_object = None
# the reason I don't immediately return the result is because then there would be a chance
# I would return None even though a country could be found through fuzzy search
country = country.strip()
if len(country) == 2:
pycountry_object = pycountry.countries.get(alpha_2=country.upper())
elif len(country) == 3:
pycountry_object = pycountry.countries.get(alpha_3=country.upper())
if pycountry_object is not None:
return cls(pycountry_object=pycountry_object) # type: ignore
# fuzzy search if enabled
if config.allow_fuzzy_search:
found_countries = pycountry.countries.search_fuzzy(country)
if len(found_countries):
return cls(pycountry_object=found_countries[0]) # type: ignore
@classmethod
def search(cls, country: Optional[str]) -> Optional[Country]:
return cls(pycountry_object=cls._search_pycountry_object(country=country))
@classmethod
def from_alpha_2(cls, alpha_2: str) -> Country:
return cls(pycountry_object=pycountry.countries.get(alpha_2=alpha_2.upper()))
@classmethod
def from_alpha_3(cls, alpha_3: str) -> Country:
return cls(pycountry_object=pycountry.countries.get(alpha_3=alpha_3.upper()))
@classmethod
def from_fuzzy(cls, fuzzy: str) -> Country:
return cls(pycountry_object=pycountry.countries.search_fuzzy(fuzzy)) # type: ignore
@property
def name(self) -> str:
return self.pycountry_object.name
@property
def alpha_2(self) -> str:
return self.pycountry_object.alpha_2
@property
def alpha_3(self) -> str:
return self.pycountry_object.alpha_3
@property
def numeric(self) -> str:
return self.pycountry_object.numeric
@property
def official_name(self) -> str:
return self.pycountry_object.official_name
def __str__(self) -> str:
return self.pycountry_object.__str__()
def __repr__(self) -> str:
return self.pycountry_object.__repr__()
class EmptyCountry(Country):
"""
This will be used if you don't want to use a fallback country but you still want to be able to not have None
You can access the same attributes but they will just return None
"""
def __init__(self) -> None:
pass
@classmethod
def search(cls, country: Optional[str]) -> Union[Country, EmptyCountry]:
result = super().search(country)
if result is None:
return EmptyCountry()
return result