DEV Community

Robert Pringle
Robert Pringle

Posted on

Building an SLA Deadline Tracker in Python

Building an SLA Deadline Tracker in Python

"Respond within 24 business hours." Every support team has this commitment. It sounds like a simple timer. In practice, it's one of the most reliably buggy pieces of logic in a ticketing system.

Let's look at what "24 business hours" actually means, why naive implementations fail, and how to build a CLI tool that gets it right.


What "24 Business Hours" Actually Means

If a ticket arrives at 3pm on a Friday, "24 business hours" does not mean Saturday at 3pm. It means:

  • Friday 3pm–5pm = 2 hours
  • Monday 9am–5pm = 8 hours (2 + 8 = 10)
  • Tuesday 9am–5pm = 8 hours (10 + 8 = 18)
  • Wednesday 9am–3pm = 6 hours (18 + 6 = 24)
  • Deadline: Wednesday 3pm

That's a full 5 calendar days after the ticket opened. Any system that just adds timedelta(hours=24) to the open time will set the deadline wrong.

Now add DST. The US switches from EST (UTC-5) to EDT (UTC-4) in March. A ticket opened at 9am on the Sunday before the switch, carrying an SLA through Monday, will have an off-by-one-hour error if your code doesn't handle the transition properly.


The Naive Approach

from datetime import datetime, timedelta

def sla_deadline(opened_at: datetime, sla_hours: int) -> datetime:
    return opened_at + timedelta(hours=sla_hours)

# Ticket opened Friday 3pm
opened = datetime(2024, 3, 15, 15, 0)
print(sla_deadline(opened, 24))
# → 2024-03-16 15:00:00  (Saturday 3pm — wrong)
Enter fullscreen mode Exit fullscreen mode

Some teams add a weekend check:

def sla_deadline_v2(opened_at: datetime, sla_hours: int) -> datetime:
    current = opened_at
    remaining = sla_hours
    while remaining > 0:
        current += timedelta(hours=1)
        if current.weekday() < 5:  # Monday=0, Friday=4
            remaining -= 1
    return current
Enter fullscreen mode Exit fullscreen mode

This is better but still wrong:

  • It counts all hours on weekdays, not just business hours (9am–5pm)
  • It ignores holidays
  • It doesn't account for DST

The Right Tool: BizCal API's /working-hours/deadline

BizCal API has a dedicated endpoint for this: GET /working-hours/deadline.

You pass it:

  • start — the datetime when the SLA clock started (ISO 8601)
  • sla_hours — the number of working hours in the SLA
  • timezone — IANA timezone string (e.g. America/New_York)
  • country — for holiday exclusion
import requests

r = requests.get(
    "https://bizcalapi.com/working-hours/deadline",
    params={
        "start": "2024-03-15T15:00:00",
        "sla_hours": 24,
        "timezone": "America/New_York",
        "country": "US",
    },
    headers={
        "X-RapidAPI-Key": "YOUR_API_KEY",
        "X-RapidAPI-Host": "bizcalapi.com",
    },
)
print(r.json()["deadline"])
# → "2024-03-20T15:00:00-04:00"  (Wednesday 3pm, correctly in EDT after DST)
Enter fullscreen mode Exit fullscreen mode

The response comes back in the correct offset (-04:00 for EDT) — the API handled the DST transition.


Building the CLI Tool

#!/usr/bin/env python3
"""
sla.py — Calculate SLA deadlines in business hours.

Usage:
    python sla.py --start "2024-03-15T15:00:00" --hours 24 --tz "America/New_York" --country US
"""

import argparse
import sys
import requests

API_KEY = "YOUR_API_KEY"
BASE_URL = "https://bizcalapi.com"
HEADERS = {
    "X-RapidAPI-Key": API_KEY,
    "X-RapidAPI-Host": "bizcalapi.com",
}


def calculate_deadline(start: str, sla_hours: int, timezone: str, country: str) -> dict:
    r = requests.get(
        f"{BASE_URL}/working-hours/deadline",
        params={"start": start, "sla_hours": sla_hours, "timezone": timezone, "country": country},
        headers=HEADERS,
        timeout=10,
    )
    r.raise_for_status()
    return r.json()


def format_output(result: dict) -> str:
    lines = [
        f"SLA Start:    {result['start']}",
        f"SLA Hours:    {result['sla_hours']}",
        f"Deadline:     {result['deadline']}",
        f"Timezone:     {result['timezone']}",
        f"Country:      {result['country']}",
    ]
    if result.get("holidays_excluded"):
        lines.append(f"Holidays excluded: {', '.join(result['holidays_excluded'])}")
    return "\n".join(lines)


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--start", required=True)
    parser.add_argument("--hours", type=int, required=True)
    parser.add_argument("--tz", default="UTC")
    parser.add_argument("--country", default="US")
    args = parser.parse_args()

    try:
        result = calculate_deadline(args.start, args.hours, args.tz, args.country)
        print(format_output(result))
    except requests.HTTPError as e:
        print(f"API error: {e.response.status_code}", file=sys.stderr)
        sys.exit(1)


if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode

Run it:

python sla.py --start "2024-03-15T15:00:00" --hours 24 --tz "America/New_York" --country US

# SLA Start:    2024-03-15T15:00:00
# SLA Hours:    24
# Deadline:     2024-03-20T15:00:00-04:00
# Timezone:     America/New_York
# Country:      US

python sla.py --start "2024-12-24T10:00:00" --hours 8 --tz "Europe/London" --country GB

# Deadline:     2024-12-27T10:00:00+00:00
# Holidays excluded: Christmas Day, Boxing Day
Enter fullscreen mode Exit fullscreen mode

The DST Case That Catches Everyone

# Ticket opened Friday March 8 at 4pm EST
# 8-hour SLA carries over the weekend + through DST switch (March 10)
# Naive: Monday March 11 at 4pm EST (timedelta over business hours)
# Correct: Monday March 11 at 4pm EDT (UTC-4, not UTC-5)
Enter fullscreen mode Exit fullscreen mode

The API uses Python's zoneinfo module under the hood, which handles IANA timezone rules correctly across DST transitions. Always pass full IANA strings like America/New_York — never fixed-offset strings like EST.


Integrating Into a Ticket System

def set_sla_deadline(ticket: dict, sla_hours: int, country: str, timezone: str) -> str:
    result = calculate_deadline(
        start=ticket["created_at"],
        sla_hours=sla_hours,
        timezone=timezone,
        country=country,
    )
    return result["deadline"]
Enter fullscreen mode Exit fullscreen mode

Store the deadline in your ticket record. Compare datetime.now(tz) against the parsed deadline on each status check. No custom calendar logic in your codebase.


Get Started

Subscribe on RapidAPI: BizCal API

Free tier: 50 requests/day. Basic is $9.99/mo for 1,000 requests/day across all 15 countries.

Top comments (0)