Holidays and Day Info

Pyplanner separates the calendar structure from the holiday data. Any class that implements the right interface can supply per-day metadata (public holidays, transferred workdays, etc.) to the calendar model.

DayInfo

DayInfo is a simple dataclass that carries metadata about a single day:

from pyplanner import DayInfo

info = DayInfo(
    is_off_day=True,
    name="Christmas Day",
    local_name="Boze Narodzenie",
    launch_year=None,
    holiday_types=("Public",),
)

Every field defaults to None, meaning “no data - fall back to default logic”. When is_off_day is None, the calendar uses the weekday’s default weekend rule instead.

Built-in providers

Pyplanner ships two providers that fetch holiday data from free public APIs.

NagerDateProvider

Covers 100+ countries via the Nager.Date API. Returns public holidays with names and types. Free, no API key required.

from pyplanner import Calendar
from pyplanner.providers import NagerDateProvider

provider = NagerDateProvider("PL")
cal = Calendar(firstweekday=0, provider=provider)
year = cal.year(2026)

for day in year.days():
    if day.name:
        print(f"{day.id}  {day.name}")

IsDayOffProvider

Covers Russia, Belarus, Kazakhstan, Uzbekistan and Georgia via the isdayoff.ru API. Returns a binary workday/off-day flag for every day of the year including transferred workdays. Free, no API key required.

from pyplanner.providers import IsDayOffProvider

provider = IsDayOffProvider("RU")
cal = Calendar(firstweekday=0, provider=provider)
year = cal.year(2026)

workdays = sum(1 for d in year.days() if not d.is_off_day)
print(f"Workdays in 2026 (Russia): {workdays}")

Both providers issue a warning and return None when the network request fails. The calendar then falls back to weekday-based weekend rules - no holidays, but no crash either.

Writing a custom provider

A provider is any class with two methods:

  1. __init__(self, country_code: str) - raise ValueError if the country is not supported.

  2. fetch_day_info(self, year: int) -> dict[str, DayInfo] | None - return a

    mapping of YYYY-MM-DD strings to DayInfo instances, or None on failure.

The provider does not need to inherit from DayInfoProvider. Pyplanner discovers providers by duck typing at runtime: if it has fetch_day_info, it qualifies.

Duck typing was chosen over a formal plugin registry (entry points, abstract base class enforcement) because:

  1. It allows standalone single-file providers with zero dependencies on pyplanner.

  2. It avoids the complexity of setuptools entry points for a project that does not publish to PyPI yet.

  3. Duck typing is idiomatic Python and easy to understand.

Here is a minimal standalone provider (e.g. my_holidays.py):

from dataclasses import dataclass

@dataclass
class DayInfo:
    is_off_day: bool | None = None
    name: str | None = None
    local_name: str | None = None
    launch_year: int | None = None
    holiday_types: tuple[str, ...] | None = None

class CompanyHolidayProvider:
    """Mark company-specific days off."""

    def __init__(self, country_code: str) -> None:
        if country_code.upper() != "US":
            raise ValueError(f"Unsupported: {country_code!r}")

    def fetch_day_info(self, year: int):
        return {
            f"{year}-07-04": DayInfo(
                is_off_day=True,
                name="Independence Day",
            ),
            f"{year}-12-25": DayInfo(
                is_off_day=True,
                name="Christmas Day",
            ),
        }

Use it from the CLI:

pyplanner planners/demo --provider my_holidays --country us

Or from code:

from pyplanner import Calendar
from my_holidays import CompanyHolidayProvider

cal = Calendar(
    firstweekday=0,
    provider=CompanyHolidayProvider("US")
)

Loading providers dynamically

DayInfoProvider.load() imports a module by dotted name (or file path) and returns every provider class found in it. This is how the CLI --provider flag works:

from pyplanner import DayInfoProvider

classes = DayInfoProvider.load("pyplanner.providers")
for cls in classes:
    print(cls.__name__)
# IsDayOffProvider
# NagerDateProvider

# Load from a file path:
classes = DayInfoProvider.load("my_holidays.py")

API reference

class pyplanner.DayInfo(is_off_day: bool | None = None, name: str | None = None, local_name: str | None = None, launch_year: int | None = None, holiday_types: tuple[str, ...] | None = None)

Supplementary information about a calendar day.

Every field defaults to None (no data - fall back to default calendar logic). New fields can be added as the framework grows.

holiday_types values (Nager.Date definitions):

  • "Public" - general public holiday, a day off for the population.

  • "Bank" - bank holiday; banks and offices are closed.

  • "School" - school holiday; schools are closed.

  • "Authorities" - government offices are closed.

  • "Optional" - majority of people take a day off but it is not mandatory.

  • "Observance" - optional festivity, no paid day off.

class pyplanner.DayInfoProvider(country_code: str)

Interface for day-information providers.

Custom providers: any importable Python module that contains one or more classes with a fetch_day_info(self, year) method can be loaded via load(). Plugin modules do not need to depend on pyplanner; classes are discovered at runtime by duck typing.

Example standalone plugin (e.g. my_holidays.py):

from dataclasses import dataclass

@dataclass
class DayInfo:
    is_off_day: bool | None = None
    name: str | None = None
    local_name: str | None = None
    launch_year: int | None = None
    holiday_types: tuple[str, ...] | None = None

class MyHolidayProvider:
    def __init__(self, country_code: str) -> None:
        if country_code.upper() != "US":
            raise ValueError(f"Unsupported: {country_code!r}")

    def fetch_day_info(self, year: int):
        return {
            f"{year}-12-25": DayInfo(
                is_off_day=True,
                name="Christmas Day",
            ),
            f"{year}-01-01": DayInfo(
                is_off_day=True,
                name="New Year's Day",
            ),
        }

Usage:

pyplanner planner.html --provider my_holidays --country us
static load(module_name: str) list[type[DayInfoProvider]]

Import module_name and return every provider class found in it.

Provider classes are discovered by duck typing - they do not have to inherit from this class. If the name cannot be imported as an installed package, the method falls back to loading a file from disk (see _load_from_file()).

Parameters:

module_name – Dotted module name or a file path (with or without extension).

Returns:

List of provider classes found in the module.

Raises:
  • TypeError – If the module contains no provider classes.

  • ModuleNotFoundError – If module_name cannot be imported and no matching file is found on disk.

  • ImportError – If a matching file is found but cannot be loaded.

abstractmethod fetch_day_info(year: int) dict[str, DayInfo] | None

Return day information keyed by date id (YYYY-MM-DD).

Parameters:

year – Calendar year to fetch data for.

Returns:

Mapping of YYYY-MM-DD strings to DayInfo instances, or None when the data source is unreachable or the response is unusable. Missing keys are treated as “no extra info” (equivalent to DayInfo()).

class pyplanner.providers.NagerDateProvider(country_code: str, *, timeout: float = 10)

DayInfoProvider backed by the Nager.Date public-holiday API.

Covers 100+ countries (ISO 3166-1 alpha-2 codes). Free, no API key required. https://date.nager.at

fetch_day_info(year: int) dict[str, DayInfo] | None

Fetch public holidays for year from the Nager.Date API.

Parameters:

year – Calendar year to fetch data for.

Returns:

Mapping of YYYY-MM-DD strings to DayInfo instances, or None if the request fails or the response is unusable.

class pyplanner.providers.IsDayOffProvider(country_code: str, *, timeout: float = 10)

DayInfoProvider backed by the isdayoff.ru production-calendar API.

Provides complete workday/off-day data including public holidays and transferred workdays. Free, no API key required.

Supported countries: RU, BY, KZ, UZ, GE.

fetch_day_info(year: int) dict[str, DayInfo] | None

Fetch workday/off-day data for year from the isdayoff.ru API.

Parameters:

year – Calendar year to fetch data for.

Returns:

Mapping of YYYY-MM-DD strings to DayInfo instances, or None if the request fails or the response is unusable.