Skip to main content

Composable business calendars for Django with holidays, intraday schedules, and timezone-aware operations.

Project description

django-bizcal

PyPI version Python versions CI License: MIT

django-bizcal is a production-oriented Python library for Django projects that need composable business calendars with official holidays, custom holidays, intraday schedules, timezone-aware arithmetic, and reusable service integration.

It is designed for SLA clocks, operational workflows, due dates, approvals, support desks, tenant-specific calendars, and country-specific business hours.

Highlights

  • Pure domain core with no ORM coupling.
  • Official holidays via holidays.
  • Custom organization or tenant holidays in memory.
  • Intraday schedules with multiple windows per weekday.
  • Calendar composition with union, intersection, difference, and override.
  • Explicit timezone support based on zoneinfo.
  • Reusable Django app with namespaced settings and service helpers.
  • Optional database-backed holiday closures for named Django calendars.
  • Modern packaging with pyproject.toml, wheel/sdist builds, pytest, and GitHub Actions.

Installation

pip install django-bizcal

For local development:

pip install -e ".[dev]"

Quickstart

from datetime import datetime
from zoneinfo import ZoneInfo

from django_bizcal import UnionCalendar, WorkingCalendar

cl = WorkingCalendar.from_country(
    country="CL",
    years=[2026, 2027],
    tz="America/Santiago",
    weekly_schedule={
        0: [("09:00", "13:00"), ("14:00", "18:00")],
        1: [("09:00", "13:00"), ("14:00", "18:00")],
        2: [("09:00", "13:00"), ("14:00", "18:00")],
        3: [("09:00", "13:00"), ("14:00", "18:00")],
        4: [("09:00", "13:00"), ("14:00", "17:00")],
    },
    extra_holidays=["2026-12-24", "2026-12-31"],
)

mx = WorkingCalendar.from_country(
    country="MX",
    years=[2026, 2027],
    tz="America/Mexico_City",
    weekly_schedule={
        0: [("09:00", "18:00")],
        1: [("09:00", "18:00")],
        2: [("09:00", "18:00")],
        3: [("09:00", "18:00")],
        4: [("09:00", "18:00")],
    },
)

regional = UnionCalendar([cl, mx], tz="UTC")

start = datetime(2026, 1, 5, 15, 0, tzinfo=ZoneInfo("UTC"))
deadline = regional.add_business_hours(start, 10)
elapsed = regional.business_minutes_between(start, deadline)

Django integration

Add the reusable app:

INSTALLED_APPS = [
    ...,
    "django_bizcal",
]

Configure a default calendar:

BIZCAL_DEFAULT_TIMEZONE = "America/Santiago"
BIZCAL_DEFAULT_COUNTRY = "CL"
BIZCAL_PRELOAD_YEARS = [2026, 2027]
BIZCAL_DEFAULT_CALENDAR = {
    "type": "working",
    "tz": "America/Santiago",
    "country": "CL",
    "years": [2026, 2027],
    "weekly_schedule": {
        "0": [["09:00", "13:00"], ["14:00", "18:00"]],
        "1": [["09:00", "13:00"], ["14:00", "18:00"]],
        "2": [["09:00", "13:00"], ["14:00", "18:00"]],
        "3": [["09:00", "13:00"], ["14:00", "18:00"]],
        "4": [["09:00", "13:00"], ["14:00", "17:00"]],
    },
    "extra_holidays": ["2026-12-24", "2026-12-31"],
}

Consume it from application code:

from django_bizcal.services import get_default_calendar

calendar = get_default_calendar()
deadline = calendar.add_business_hours(ticket.created_at, 8)

For projects with more than one operational calendar, define a named registry:

BIZCAL_DEFAULT_TIMEZONE = "UTC"
BIZCAL_DEFAULT_CALENDAR_NAME = "support"
BIZCAL_CALENDARS = {
    "support": {
        "type": "working",
        "tz": "UTC",
        "weekly_schedule": {
            "0": [["09:00", "18:00"]],
            "1": [["09:00", "18:00"]],
            "2": [["09:00", "18:00"]],
            "3": [["09:00", "18:00"]],
            "4": [["09:00", "18:00"]],
        },
    },
    "operations_latam": {
        "type": "union",
        "tz": "UTC",
        "children": [
            {
                "type": "working",
                "country": "CL",
                "years": [2026, 2027],
                "tz": "America/Santiago",
                "weekly_schedule": {
                    "0": [["09:00", "18:00"]],
                    "1": [["09:00", "18:00"]],
                    "2": [["09:00", "18:00"]],
                    "3": [["09:00", "18:00"]],
                    "4": [["09:00", "18:00"]],
                },
            },
            {
                "type": "working",
                "country": "MX",
                "years": [2026, 2027],
                "tz": "America/Mexico_City",
                "weekly_schedule": {
                    "0": [["09:00", "18:00"]],
                    "1": [["09:00", "18:00"]],
                    "2": [["09:00", "18:00"]],
                    "3": [["09:00", "18:00"]],
                    "4": [["09:00", "18:00"]],
                },
            },
        ],
    },
}

Use named calendars from application code:

from django_bizcal.services import get_calendar, get_default_calendar

support = get_default_calendar()
regional_ops = get_calendar("operations_latam")

Persisted holiday closures can be enabled for named Django calendars:

BIZCAL_ENABLE_DB_MODELS = True

from datetime import date

from django_bizcal.models import CalendarHoliday
from django_bizcal.services import set_calendar_holiday

CalendarHoliday.objects.create(
    calendar_name="support",
    day=date(2026, 12, 24),
    name="Company shutdown",
)

set_calendar_holiday("support", date(2026, 12, 31), name="Year end close")

Once enabled, get_default_calendar() and get_calendar(name) automatically apply full-day closures from CalendarHoliday rows that match the logical calendar name. The service helpers also clear the cached named calendars automatically after each change.

Calendar builder

from django_bizcal import CalendarBuilder

calendar = CalendarBuilder.from_dict(
    {
        "type": "union",
        "tz": "UTC",
        "children": [
            {
                "type": "working",
                "country": "CL",
                "years": [2026, 2027],
                "tz": "America/Santiago",
                "weekly_schedule": {
                    "0": [["09:00", "18:00"]],
                    "1": [["09:00", "18:00"]],
                    "2": [["09:00", "18:00"]],
                    "3": [["09:00", "18:00"]],
                    "4": [["09:00", "18:00"]],
                },
            },
            {
                "type": "working",
                "country": "MX",
                "years": [2026, 2027],
                "tz": "America/Mexico_City",
                "weekly_schedule": {
                    "0": [["09:00", "18:00"]],
                    "1": [["09:00", "18:00"]],
                    "2": [["09:00", "18:00"]],
                    "3": [["09:00", "18:00"]],
                    "4": [["09:00", "18:00"]],
                },
            },
        ],
    }
)

The builder also supports serialization back to declarative config:

from django_bizcal import CalendarBuilder

config = CalendarBuilder.to_dict(calendar)
restored = CalendarBuilder.from_dict(config)

API ergonomics

Common day- and boundary-level helpers are part of the public API:

  • iter_business_days(...), list_business_days(...), count_business_days(...)
  • next_business_day(...), previous_business_day(...)
  • opening_for_day(...), closing_for_day(...)
  • next_opening_datetime(...), previous_closing_datetime(...)

Typed declarative config helpers are also exported for IDE and static typing support:

  • CalendarConfig
  • WorkingCalendarConfig
  • UnionCalendarConfig
  • IntersectionCalendarConfig
  • DifferenceCalendarConfig
  • OverrideCalendarConfig

Architecture

  • The domain core lives in src/django_bizcal and stays framework-light.
  • WorkingCalendar handles business schedules and holiday lookup.
  • Composition calendars project child windows into a reference timezone.
  • The Django layer only wraps settings, AppConfig, and service helpers.
  • V1 intentionally ships without database models to keep the public core stable and reusable.

See the full documentation in:

Compatibility

  • Python 3.11+
  • Django 4.2, 5.0, 5.1

Limitations

  • Official holiday lookup requires the relevant years to be preloaded.
  • Wall-clock times are interpreted with zoneinfo; DST transitions affect real elapsed durations.
  • V1 does not persist calendar definitions in the database.

Release

python -m build
pytest

The recommended release path uses GitHub Actions plus PyPI Trusted Publishing. Publishing guidance is documented in docs/release.md.

Support

If django-bizcal helps your team, consider sponsoring ongoing maintenance, documentation, and new features through GitHub Sponsors or by reaching out for support and implementation work.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

django_bizcal-0.2.0.tar.gz (34.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

django_bizcal-0.2.0-py3-none-any.whl (29.6 kB view details)

Uploaded Python 3

File details

Details for the file django_bizcal-0.2.0.tar.gz.

File metadata

  • Download URL: django_bizcal-0.2.0.tar.gz
  • Upload date:
  • Size: 34.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for django_bizcal-0.2.0.tar.gz
Algorithm Hash digest
SHA256 d8174179f50e36cadc045a2c7f7210c91d17ea318e5514a4ff08755d74c1e362
MD5 20e0fc49d3261fb5c329884d87ffb74b
BLAKE2b-256 f46480e6a114d27ccfa34248244ce74a56d2b47c0040ebfa7586fdca20875d37

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_bizcal-0.2.0.tar.gz:

Publisher: publish.yml on radix1001/django-bizcal

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file django_bizcal-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: django_bizcal-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 29.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for django_bizcal-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 252f8c1344d73e147be23fd672318ae1d636751aa7576322d220329292b7cb8e
MD5 c8f25caf0b5f6d44af6a4d9a0636a69e
BLAKE2b-256 c46f7db297de99e00fc6c17d485adbef20a8262c1b984174d2754848d892181f

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_bizcal-0.2.0-py3-none-any.whl:

Publisher: publish.yml on radix1001/django-bizcal

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page