import logging, pkg_resources
from datetime import datetime
from json.decoder import JSONDecodeError
from typing import Union
from requests import Session
from requests.exceptions import RequestException
try:
__version__ = pkg_resources.get_distribution("nagerapi").version
except pkg_resources.DistributionNotFound:
__version__ = ""
__author__ = "Nathan Taggart"
__credits__ = "meisnate12"
__package_name__ = "nagerapi"
__project_name__ = "NagerAPI"
__description__ = "Python wrapper for Nager API https://date.nager.at/Api"
__url__ = "https://github.com/meisnate12/NagerAPI"
__email__ = 'meisnate12@gmail.com'
__license__ = 'MIT License'
__all__ = ["NagerObjectAPI", "NagerRawAPI", "NagerException", "Country", "Weekend", "Holiday"]
base_url = "https://date.nager.at/api/v3/"
logger = logging.getLogger(__name__)
[docs]class NagerException(Exception):
""" Base class for all Nagar exceptions. """
pass
class NagerBase:
def __init__(self, nager):
self._nager = nager
self._loading = True
def __setattr__(self, key, value):
if key.startswith("_") or self._loading:
super().__setattr__(key, value)
else:
raise AttributeError("Attributes cannot be edited")
def __repr__(self):
return self.__str__()
[docs]class Weekend(NagerBase):
""" Represents a single Long Weekend.
Attributes:
start_date (datetime): Start Date of the Long Weekend.
end_date (datetime): End Date of the Long Weekend.
day_count (int): Days in the Long Weekend.
need_bridge_day (bool): Is a Bridge Day needed for the Long Weekend.
"""
def __init__(self, nager, data):
super().__init__(nager)
self.start_date = datetime.strptime(data["startDate"], "%Y-%m-%d") if data["startDate"] else None
self.end_date = datetime.strptime(data["endDate"], "%Y-%m-%d") if data["endDate"] else None
self.day_count = data["dayCount"]
self.need_bridge_day = data["needBridgeDay"]
self._loading = False
def __str__(self):
return f"{self.start_date.strftime('%Y-%m-%d')} --> {self.end_date.strftime('%Y-%m-%d')}"
[docs]class Holiday(NagerBase):
""" Represents a single Holiday.
Attributes:
name (str): English Name of the Holiday.
local_name (str): Local name of the Holiday.
date (datetime): Date of the Holiday.
code (str): ISO 3166-1 alpha-2 Country Code for the Holiday.
country (Country): Country Object for the ISO 3166-1 alpha-2 Country Code.
fixed_holiday (bool): Is this public holiday every year on the same date.
global_holiday (bool): Is this public holiday in every county (Federal State).
counties (List[str]): List of Counties (ISO-3166-2 - Federal States).
launch_year (int): The launch year of the public holiday.
types (List[str]): A list of types the public holiday it is valid.
is_public (bool): "Public" type is in types.
is_bank (bool): "Bank" type is in types.
is_school (bool): "School" type is in types.
is_authorities (bool): "Authorities" type is in types.
is_optional (bool): "Optional" type is in types.
is_observance (bool): "Observance" type is in types.
"""
def __init__(self, nager, data):
super().__init__(nager)
self.name = data["name"]
self.local_name = data["localName"]
self.date = datetime.strptime(data["date"], "%Y-%m-%d") if data["date"] else None
self.code = data["countryCode"]
self.country = self._nager.country(self.code, load=False)
self.fixed_holiday = data["fixed"]
self.global_holiday = data["global"]
self.counties = data["counties"]
self.launch_year = data["launchYear"]
self.types = data["types"]
self.is_public = "Public" in self.types
self.is_bank = "Bank" in self.types
self.is_school = "School" in self.types
self.is_authorities = "Authorities" in self.types
self.is_optional = "Optional" in self.types
self.is_observance = "Observance" in self.types
self._loading = False
def __str__(self):
return f"{self.name} ({self.date.strftime('%Y-%m-%d')})"
[docs]class Country(NagerBase):
""" Represents a single Holiday.
Attributes:
name (str): Common Name of the Country.
code (str): ISO 3166-1 alpha-2 Country Code.
official (str): Official Name of the Country.
region (str): Region of the Country.
borders (List[Country]): List of Counties the border the Country.
"""
def __init__(self, nager, data):
super().__init__(nager)
self._load(data)
def _load(self, data):
self._loading = True
self.name = data["name"] if "name" in data else data["commonName"]
self.code = data["countryCode"]
self.official = data["officialName"] if "officialName" in data else None
self.region = data["region"] if "region" in data else None
self.borders = [Country(self._nager, c) for c in data["borders"]] if "borders" in data and data["borders"] else None
self._full = "region" in data
self._loading = False
def __str__(self):
return self.name
def __eq__(self, other):
if type(self) is type(other):
return self.name == other.name and self.code == other.code
else:
return str(self.code) == str(other).upper()
def __getattribute__(self, item):
value = super().__getattribute__(item)
if value is not None or self._full:
return value
self.load_details()
return super().__getattribute__(item)
[docs] def load_details(self):
""" Loads the details for the country. """
self._load(self._nager.api.get_country_info(self.code))
[docs] def long_weekends(self, year: int):
""" Alias for :meth:`~NagerObjectAPI.long_weekends` for this country. """
return [Weekend(self._nager, w) for w in self._nager.api.get_long_weekend(year, self.code)]
[docs] def public_holidays(self, year: int):
""" Alias for :meth:`~NagerObjectAPI.public_holidays` for this country. """
return [Holiday(self._nager, h) for h in self._nager.api.get_public_holidays(year, self.code)]
[docs] def is_today_public_holiday(self, offset: int = None):
""" Alias for :meth:`~NagerObjectAPI.is_today_public_holiday` for this country. """
return self._nager.api.get_is_today_public_holiday(self.code, offset=offset)
[docs] def next_public_holidays(self):
""" Alias for :meth:`~NagerObjectAPI.next_public_holidays` for this country. """
return [Holiday(self._nager, h) for h in self._nager.api.get_next_public_holidays(self.code)]
[docs]class NagerObjectAPI:
""" Main Object API Class
Parameters:
session (Session): Use your own Session object.
default_country (Union[str, Country]): Default Country to use in any method where country isn't provided.
Attributes:
name (str): Name of the API.
version (int): Version of the API.
default_country (Country): Default Country to use.
"""
def __init__(self, session: Session = None, default_country: Union[str, Country] = None):
self._loading = True
self.api = NagerRawAPI(session=session)
data = self.api.get_version()
self.name = data["name"]
self.version = data["version"]
self._countries = None
self.default_country = self.country(default_country) if default_country else None # noqa
self._loading = False
def __setattr__(self, key, value):
if key.startswith("_") or self._loading:
super().__setattr__(key, value)
else:
raise AttributeError("Attributes cannot be edited")
[docs] def country(self, country: Union[Country, str] = None, load=True):
""" :class:`~Country` Object with details.
Parameters:
country (Union[Country, str]): ISO 3166-1 alpha-2 Country Code.
load (bool): Load Full Details.
Returns:
:class:`~Country`
Raises:
:class:`NagerException`: When an Invalid Country Code is provided.
"""
if not country:
if not self.default_country:
raise NagerException("No Country Provided")
country = self.default_country
if not isinstance(country, Country):
code = str(country).upper()
if code not in self.available_countries:
raise NagerException(f"Invalid Country Code: {code}. Options: {[c for c in self.available_countries]}")
country = self.available_countries[self.available_countries.index(code)] # noqa
if not country._full and load: # noqa
country.load_details()
return country
@property
def available_countries(self):
""" All available :class:`~Country` objects.
Returns:
List[:class:`~Country`]
"""
if self._countries is None:
self._countries = [Country(self, c) for c in self.api.get_available_countries()]
return self._countries
[docs] def long_weekends(self, year: int, country: Union[Country, str] = None):
""" All available :class:`~Weekend` Objects for a country in a given year.
Parameters:
year (int): Year to look at.
country (Union[Country, str]): ISO 3166-1 alpha-2 Country Code.
Returns:
List[:class:`~Weekend`]
Raises:
:class:`NagerException`: When an Invalid Country Code or year is provided.
"""
return self.country(country, load=False).long_weekends(year)
[docs] def public_holidays(self, year: int, country: Union[Country, str] = None):
""" All available public :class:`~Holiday` Objects for a country in a given year.
Parameters:
year (int): Year to look at.
country (Union[Country, str]): ISO 3166-1 alpha-2 Country Code.
Returns:
List[:class:`~Holiday`]
Raises:
:class:`NagerException`: When an Invalid Country Code or year is provided.
"""
return self.country(country, load=False).public_holidays(year)
[docs] def is_today_public_holiday(self, country: Union[Country, str] = None, offset: int = None):
""" Is today a public Holiday for the given country.
Parameters:
country (Union[Country, str]): ISO 3166-1 alpha-2 Country Code.
offset (int): UTC timezone offset.
Returns:
bool
Raises:
:class:`NagerException`: When an Invalid Country Code is provided.
"""
return self.country(country, load=False).is_today_public_holiday(offset=offset)
[docs] def next_public_holidays(self, country: Union[Country, str] = None):
""" Returns the upcoming public :class:`~Holiday` Objects for the next 365 days for the given country.
Parameters:
country (Union[Country, str]): ISO 3166-1 alpha-2 Country Code.
Returns:
List[:class:`~Holiday`]
Raises:
:class:`NagerException`: When an Invalid Country Code is provided.
"""
return self.country(country, load=False).next_public_holidays()
[docs] def next_public_worldwide_holidays(self):
""" Returns the upcoming public :class:`~Holiday` Objects for the next 7 days.
Returns:
List[:class:`~Holiday`]
"""
return [Holiday(self, h) for h in self.api.get_next_public_worldwide_holidays()]
[docs]class NagerRawAPI:
""" Main Raw API Class
Parameters:
session (Session): Use your own Session object.
"""
def __init__(self, session: Session = None):
self._session = Session() if session is None else session
def _request(self, request_url, status_bool=False, **kwargs):
""" process request. """
url_params = {}
for key, value in kwargs.items():
if value is not None:
url_params[key] = value
logger.debug(f"Request URL: {request_url}")
if url_params:
logger.debug(f"Request Params: {url_params}")
try:
self.response = self._session.get(request_url, params=url_params)
except RequestException as e:
raise NagerException(f"Failed to Connect to {request_url}: {e}")
if status_bool:
if self.response.status_code == 200:
return True
elif self.response.status_code == 204:
return False
try:
response_json = self.response.json()
except JSONDecodeError as e:
raise NagerException(f"Failed to Decode Response JSON{request_url}: {e}\nContent: {self.response.content}")
logger.debug(f"Response ({self.response.status_code} [{self.response.reason}]) {response_json}")
if self.response.status_code == 404:
raise NagerException(f"({self.response.status_code} [{self.response.reason}]) Country Code Invalid")
elif self.response.status_code >= 400:
raise NagerException(f"({self.response.status_code} [{self.response.reason}]) {response_json}")
return response_json
[docs] def get_country_info(self, country: str):
""" `GET CountryInfo <https://date.nager.at/swagger/index.html>`__: Get country info for the given country.
Parameters:
country (str): ISO 3166-1 alpha-2 Country Code.
Returns:
Dict
Raises:
:class:`NagerException`: When an Invalid Country Code or year is provided.
"""
return self._request(f"{base_url}CountryInfo/{country}")
[docs] def get_available_countries(self):
""" `GET AvailableCountries <https://date.nager.at/swagger/index.html>`__: Get all available countries.
Returns:
List[Dict]
"""
return self._request(f"{base_url}AvailableCountries")
[docs] def get_long_weekend(self, year: int, country: str):
""" `GET LongWeekend <https://date.nager.at/swagger/index.html>`__: Get long weekends for a given country
Parameters:
year (int): Year to look at.
country (str): ISO 3166-1 alpha-2 Country Code.
Returns:
List[Dict]
Raises:
:class:`NagerException`: When an Invalid Country Code or year is provided.
"""
return self._request(f"{base_url}LongWeekend/{year}/{country}")
[docs] def get_public_holidays(self, year: int, country: str):
""" `GET PublicHolidays <https://date.nager.at/swagger/index.html>`__: Get public holidays.
Parameters:
year (int): Year to look at.
country (str): ISO 3166-1 alpha-2 Country Code.
Returns:
List[Dict]
Raises:
:class:`NagerException`: When an Invalid Country Code or year is provided.
"""
return self._request(f"{base_url}PublicHolidays/{year}/{country}")
[docs] def get_is_today_public_holiday(self, country: str, offset: int = None):
""" `GET IsTodayPublicHoliday <https://date.nager.at/swagger/index.html>`__: Is today a public holiday.
Parameters:
country (str): ISO 3166-1 alpha-2 Country Code.
offset (int): UTC timezone offset.
Returns:
bool
Raises:
:class:`NagerException`: When an Invalid Country Code is provided.
"""
params = {} if offset is None else {"offset": offset}
return self._request(f"{base_url}IsTodayPublicHoliday/{country}", status_bool=True, **params)
[docs] def get_next_public_holidays(self, country: str):
""" `GET NextPublicHolidays <https://date.nager.at/swagger/index.html>`__: Returns the upcoming public holidays for the next 365 days for the given country.
Parameters:
country (str): ISO 3166-1 alpha-2 Country Code.
Returns:
List[Dict]
Raises:
:class:`NagerException`: When an Invalid Country Code is provided.
"""
return self._request(f"{base_url}NextPublicHolidays/{country}")
[docs] def get_next_public_worldwide_holidays(self):
""" `GET NextPublicHolidaysWorldwide <https://date.nager.at/swagger/index.html>`__: Returns the upcoming public holidays for the next 7 days.
Returns:
List[Dict]
"""
return self._request(f"{base_url}NextPublicHolidaysWorldwide")
[docs] def get_version(self):
""" `GET Version <https://date.nager.at/swagger/index.html>`__: Get version of the used Nager.Date library.
Returns:
Dict
"""
return self._request(f"{base_url}Version")