Claude Code for Pendulum: Datetime Made Easy — Claude Skills 360 Blog
Blog / AI / Claude Code for Pendulum: Datetime Made Easy
AI

Claude Code for Pendulum: Datetime Made Easy

Published: December 5, 2027
Read time: 5 min read
By: Claude Skills 360

Pendulum extends Python datetime with timezone safety, arithmetic, and human output. pip install pendulum. import pendulum. Now: now = pendulum.now("America/New_York"). Today: pendulum.today("UTC"). UTC: pendulum.now("UTC"). Parse: dt = pendulum.parse("2024-06-15T10:30:00Z"). Instance: dt = pendulum.instance(datetime_obj). Timezone: dt.in_timezone("Europe/London"). Convert: dt.in_tz("Asia/Tokyo"). Create: pendulum.datetime(2024, 6, 15, 10, 30, tz="UTC"). Local: pendulum.local(2024, 6, 15). Add: dt.add(days=7, hours=3). Subtract: dt.subtract(months=1). Diff: dt1.diff(dt2).in_hours(). dt1.diff_for_humans() — “2 hours ago”. Absolute diff: dt1.diff(dt2, absolute=True).in_days(). Start/End: dt.start_of("month"), dt.end_of("week"), dt.start_of("year"). Between: dt.between(start, end). Is past/future: dt.is_past(), dt.is_future(). Is same day: dt.is_same_day(other). Period: period = pendulum.period(start, end), for day in period.range("days"):. Duration: d = pendulum.duration(days=5, hours=3). Format: dt.format("YYYY-MM-DD HH:mm:ss"). ISO: dt.to_iso8601_string(). Date string: dt.to_date_string(). Time string: dt.to_time_string(). Day name: dt.day_of_week, dt.format("dddd"). DST-safe: arithmetic respects DST transitions. Test now: pendulum.set_test_now(pendulum.datetime(2024, 1, 1)). Travel: with pendulum.travel(to=pendulum.datetime(2024, 12, 25)):. Claude Code generates Pendulum timezone-aware scheduling, reporting date ranges, and API timestamp helpers.

CLAUDE.md for Pendulum

## Pendulum Stack
- Version: pendulum >= 3.0
- Import: import pendulum
- Creation: pendulum.now("TZ") | pendulum.datetime(Y,M,D,tz=) | pendulum.parse(str)
- Timezone: .in_timezone("Region/City") | pendulum.timezone("UTC")
- Arithmetic: .add(days=, hours=) | .subtract(months=) — DST-aware
- Diff: .diff(other).in_hours() | .diff_for_humans() for human output
- Period: pendulum.period(start, end).range("days") for iteration
- Testing: pendulum.set_test_now(dt) | pendulum.travel(to=dt)

Pendulum Datetime Pipeline

# utils/pendulum_pipeline.py — datetime handling with Pendulum
from __future__ import annotations
from typing import Generator, Iterator

import pendulum
from pendulum import DateTime, Duration, Period


# ── 0. Creation helpers ───────────────────────────────────────────────────────

def now_utc() -> DateTime:
    """Current UTC datetime."""
    return pendulum.now("UTC")


def now_local(timezone: str = "America/New_York") -> DateTime:
    """Current datetime in a specific timezone."""
    return pendulum.now(timezone)


def parse_datetime(value: str, tz: str = "UTC") -> DateTime:
    """
    Parse a datetime string robustly.
    Handles ISO 8601, Unix timestamps, and many natural formats.
    """
    if isinstance(value, (int, float)):
        return pendulum.from_timestamp(value, tz=tz)
    try:
        dt = pendulum.parse(value, tz=tz)
        return dt
    except Exception:
        # Fallback: try common formats
        for fmt in ["%Y-%m-%d", "%Y/%m/%d", "%d/%m/%Y", "%m/%d/%Y"]:
            try:
                import datetime
                d = datetime.datetime.strptime(str(value), fmt)
                return pendulum.instance(d, tz=tz)
            except ValueError:
                continue
        raise ValueError(f"Cannot parse datetime: {value!r}")


def from_timestamp(ts: float, tz: str = "UTC") -> DateTime:
    """Create DateTime from Unix timestamp."""
    return pendulum.from_timestamp(ts, tz=tz)


# ── 1. Timezone conversion ────────────────────────────────────────────────────

def convert_timezone(dt: DateTime, target_tz: str) -> DateTime:
    """Convert a datetime to a different timezone."""
    return dt.in_timezone(target_tz)


def to_utc(dt: DateTime) -> DateTime:
    """Normalize any timezone-aware datetime to UTC."""
    return dt.in_timezone("UTC")


def localize(dt: DateTime, user_timezone: str) -> str:
    """
    Convert a UTC datetime to user's local time, formatted for display.
    Returns ISO 8601 string with offset (e.g., "2024-06-15T14:30:00+05:30").
    """
    local = dt.in_timezone(user_timezone)
    return local.to_iso8601_string()


def multi_timezone_display(dt: DateTime, timezones: list[str]) -> dict[str, str]:
    """Show a UTC datetime in multiple timezones."""
    result = {}
    for tz in timezones:
        local = dt.in_timezone(tz)
        result[tz] = local.format("YYYY-MM-DD HH:mm:ss z")
    return result


# ── 2. Date arithmetic ────────────────────────────────────────────────────────

def add_business_days(dt: DateTime, n: int) -> DateTime:
    """
    Add N business days (Mon–Fri), skipping weekends.
    Does not account for holidays — use workalendar for that.
    """
    current = dt
    added   = 0
    direction = 1 if n >= 0 else -1
    target    = abs(n)
    while added < target:
        current = current.add(days=direction)
        if current.day_of_week not in (pendulum.SATURDAY, pendulum.SUNDAY):
            added += 1
    return current


def next_occurrence(dt: DateTime, weekday: int) -> DateTime:
    """
    Return the next occurrence of a weekday (0=Mon, 6=Sun) after dt.
    Example: next_occurrence(now, pendulum.FRIDAY)
    """
    days_ahead = (weekday - dt.day_of_week + 7) % 7
    if days_ahead == 0:
        days_ahead = 7
    return dt.add(days=days_ahead).start_of("day")


def quarter_range(year: int, quarter: int) -> tuple[DateTime, DateTime]:
    """Return (start, end) of a fiscal quarter."""
    month_start = (quarter - 1) * 3 + 1
    start = pendulum.datetime(year, month_start, 1, tz="UTC")
    end   = start.add(months=3).subtract(microseconds=1)
    return start, end


def clamp_datetime(
    dt:  DateTime,
    minimum: DateTime | None = None,
    maximum: DateTime | None = None,
) -> DateTime:
    """Clamp a datetime to [minimum, maximum]."""
    if minimum and dt < minimum:
        return minimum
    if maximum and dt > maximum:
        return maximum
    return dt


# ── 3. Periods and ranges ─────────────────────────────────────────────────────

def date_range(
    start:     DateTime,
    end:       DateTime,
    unit:      str = "days",      # "days" | "weeks" | "months" | "hours"
) -> list[DateTime]:
    """Generate a list of datetimes between start and end."""
    period = pendulum.period(start, end)
    return list(period.range(unit))


def monthly_intervals(
    start_date: DateTime,
    n_months:   int,
) -> list[tuple[DateTime, DateTime]]:
    """
    Return a list of (month_start, month_end) tuples.
    Useful for generating monthly report windows.
    """
    intervals = []
    current = start_date.start_of("month")
    for _ in range(n_months):
        month_end = current.end_of("month")
        intervals.append((current, month_end))
        current = current.add(months=1)
    return intervals


def sliding_time_windows(
    start:        DateTime,
    end:          DateTime,
    window_hours: int = 24,
    step_hours:   int = 6,
) -> Generator[tuple[DateTime, DateTime], None, None]:
    """Yield (window_start, window_end) tuples sliding across a period."""
    current = start
    while current + pendulum.duration(hours=window_hours) <= end:
        yield current, current.add(hours=window_hours)
        current = current.add(hours=step_hours)


def is_within_business_hours(
    dt:         DateTime,
    start_hour: int = 9,
    end_hour:   int = 17,
    timezone:   str = "America/New_York",
) -> bool:
    """Check whether a UTC datetime falls within business hours in a timezone."""
    local = dt.in_timezone(timezone)
    is_weekday    = local.day_of_week not in (pendulum.SATURDAY, pendulum.SUNDAY)
    is_work_hours = start_hour <= local.hour < end_hour
    return is_weekday and is_work_hours


# ── 4. Formatting and serialization ──────────────────────────────────────────

def format_duration(d: Duration) -> str:
    """Format a Duration as human-readable string."""
    parts = []
    if abs(d.days) >= 1:
        parts.append(f"{abs(d.days)}d")
    if d.hours:
        parts.append(f"{d.hours}h")
    if d.minutes:
        parts.append(f"{d.minutes}m")
    if d.seconds and not parts:
        parts.append(f"{d.seconds}s")
    return " ".join(parts) or "0s"


def relative_time(dt: DateTime, reference: DateTime = None) -> str:
    """
    Human-readable relative time: "2 hours ago", "in 3 days", etc.
    Uses Pendulum's diff_for_humans.
    """
    ref = reference or pendulum.now("UTC")
    return dt.diff_for_humans(ref)


def to_api_timestamp(dt: DateTime) -> str:
    """Serialize to RFC 3339 / ISO 8601 for API responses."""
    return dt.to_iso8601_string()


def from_api_timestamp(s: str) -> DateTime:
    """Deserialize from API timestamp string."""
    return pendulum.parse(s)


def format_for_display(dt: DateTime, timezone: str, locale: str = "en") -> str:
    """
    Format datetime for user display with timezone abbreviation.
    Examples: "Jun 15, 2024 at 10:30 AM EDT"
    """
    local = dt.in_timezone(timezone)
    return local.format("MMM D, YYYY [at] h:mm A z")


# ── 5. Scheduling helpers ─────────────────────────────────────────────────────

def next_run_times(
    cron_hour:   int,
    cron_minute: int,
    timezone:    str,
    n:           int = 5,
) -> list[DateTime]:
    """
    Return the next N scheduled run times for a daily job at HH:MM in timezone.
    """
    now     = pendulum.now(timezone)
    runs    = []
    current = now.set(hour=cron_hour, minute=cron_minute, second=0, microsecond=0)
    if current <= now:
        current = current.add(days=1)
    for _ in range(n):
        runs.append(current.in_timezone("UTC"))
        current = current.add(days=1)
    return runs


def sla_deadline(
    created_at:    DateTime,
    sla_hours:     int,
    business_only: bool = False,
    timezone:      str  = "America/New_York",
) -> DateTime:
    """
    Compute an SLA deadline, optionally counting only business hours.
    business_only=True: skips weekends.
    """
    if not business_only:
        return created_at.add(hours=sla_hours)
    return add_business_days(created_at, sla_hours // 8)


# ── 6. Testing helpers ────────────────────────────────────────────────────────

class FrozenTime:
    """Context manager to freeze time in tests using pendulum."""

    def __init__(self, frozen_dt: DateTime):
        self.frozen_dt = frozen_dt

    def __enter__(self):
        pendulum.set_test_now(self.frozen_dt)
        return self.frozen_dt

    def __exit__(self, *_):
        pendulum.set_test_now()   # clears test now


# ── Demo ──────────────────────────────────────────────────────────────────────

if __name__ == "__main__":
    print("Pendulum Datetime Demo")
    print("=" * 50)

    # Creation
    now    = now_utc()
    eastern = now_local("America/New_York")
    print(f"\nnow_utc():         {now.to_iso8601_string()}")
    print(f"now Eastern:       {eastern.to_iso8601_string()}")

    # Parse
    parsed = parse_datetime("2024-06-15T10:30:00Z")
    print(f"parsed:            {parsed}")

    # Timezones
    print("\nMulti-timezone display:")
    display = multi_timezone_display(now, ["UTC", "America/Los_Angeles", "Asia/Tokyo", "Europe/Paris"])
    for tz, ts in display.items():
        print(f"  {tz:<25} {ts}")

    # Arithmetic
    deadline = now.add(days=7, hours=9)
    print(f"\n7 days + 9 hours:  {deadline.to_date_string()}")
    bizday = add_business_days(now, 5)
    print(f"5 business days:   {bizday.to_date_string()}")

    # Diff
    past = now.subtract(hours=27)
    print(f"\ndiff_for_humans:   {past.diff_for_humans()}")
    print(f"diff in hours:     {past.diff(now).in_hours():.1f}h")

    # Period ranges
    q_start, q_end = quarter_range(2024, 2)
    print(f"\nQ2 2024:           {q_start.to_date_string()}{q_end.to_date_string()}")

    months = monthly_intervals(pendulum.datetime(2024, 1, 1, tz="UTC"), 3)
    for s, e in months:
        print(f"  Month: {s.format('MMMM YYYY')}: {s.to_date_string()}{e.to_date_string()}")

    # Frozen time for testing
    with FrozenTime(pendulum.datetime(2024, 1, 1, 12, 0, tz="UTC")) as frozen:
        print(f"\nFrozen now():  {pendulum.now('UTC')}")
    print(f"After unfreeze: {pendulum.now('UTC').to_date_string()} (real time)")

    # SLA
    sla = sla_deadline(now, sla_hours=24)
    print(f"\n24h SLA deadline:  {sla.to_iso8601_string()}")
    print(f"Relative:          {relative_time(sla)}")

For the stdlib datetime + pytz alternative — pytz.localize() and datetime.astimezone() require careful pairing to avoid ambiguous DST times while Pendulum’s pendulum.now("America/New_York") always returns an unambiguous timezone-aware datetime, dt.add(months=1) handles month-end correctly (January 31 + 1 month = February 28/29), and dt.diff_for_humans() converts a duration to “3 hours ago” or “in 2 days” without building a formatter. For the dateutil alternative — dateutil.parser.parse is lenient to ambiguous formats while Pendulum’s pendulum.parse is strict by default (raises on ambiguous input), pendulum.period(start, end).range("days") generates timezone-aware date sequences without a timedelta loop, and pendulum.set_test_now(dt) or pendulum.travel(to=dt) makes pendulum.now() return a fixed value in tests without patching datetime.datetime. The Claude Skills 360 bundle includes Pendulum skill sets covering now/parse/instance creation, in_timezone conversions, add/subtract DST-safe arithmetic, diff and diff_for_humans, period.range iteration, monthly_intervals, business day arithmetic, quarter_range, SLA deadline, ISO 8601 serialization, format_for_display locale formatting, FrozenTime test helper, and set_test_now. Start with the free tier to try datetime code generation.

Keep Reading

AI

Claude Code for email.contentmanager: Python Email Content Accessors

Read and write EmailMessage body content with Python's email.contentmanager module and Claude Code — email contentmanager ContentManager for the class that maps content types to get and set handler functions allowing EmailMessage to support get_content and set_content with type-specific behaviour, email contentmanager raw_data_manager for the ContentManager instance that handles raw bytes and str payloads without any conversion, email contentmanager content_manager for the standard ContentManager instance used by email.policy.default that intelligently handles text plain text html multipart and binary content types, email contentmanager get_content_text for the handler that returns the decoded text payload of a text-star message part as a str, email contentmanager get_content_binary for the handler that returns the raw decoded bytes payload of a non-text message part, email contentmanager get_data_manager for the get-handler lookup used by EmailMessage get_content to find the right reader function for the content type, email contentmanager set_content text for the handler that creates and sets a text part correctly choosing charset and transfer encoding, email contentmanager set_content bytes for the handler that creates and sets a binary part with base64 encoding and optional filename Content-Disposition, email contentmanager EmailMessage get_content for the method that reads the message body using the registered content manager handlers, email contentmanager EmailMessage set_content for the method that sets the message body and MIME headers in one call, email contentmanager EmailMessage make_alternative make_mixed make_related for the methods that convert a simple message into a multipart container, email contentmanager EmailMessage add_attachment for the method that attaches a file or bytes to a multipart message, and email contentmanager integration with email.message and email.policy and email.mime and io for building high-level email readers attachment extractors text body accessors HTML readers and policy-aware MIME construction pipelines.

5 min read Feb 12, 2029
AI

Claude Code for email.charset: Python Email Charset Encoding

Control header and body encoding for international email with Python's email.charset module and Claude Code — email charset Charset for the class that wraps a character set name with the encoding rules for header encoding and body encoding describing how to encode text for that charset in email messages, email charset Charset header_encoding for the attribute specifying whether headers using this charset should use QP quoted-printable encoding BASE64 encoding or no encoding, email charset Charset body_encoding for the attribute specifying the Content-Transfer-Encoding to use for message bodies in this charset such as QP or BASE64, email charset Charset output_codec for the attribute giving the Python codec name used to encode the string to bytes for the wire format, email charset Charset input_codec for the attribute giving the Python codec name used to decode incoming bytes to str, email charset Charset get_output_charset for returning the output charset name, email charset Charset header_encode for encoding a header string using the charset's header_encoding method, email charset Charset body_encode for encoding body content using the charset's body_encoding, email charset Charset convert for converting a string from the input_codec to the output_codec, email charset add_charset for registering a new charset with custom encoding rules in the global charset registry, email charset add_alias for adding an alias name that maps to an existing registered charset, email charset add_codec for registering a codec name mapping for use by the charset machinery, and email charset integration with email.message and email.mime and email.policy and email.encoders for building international email senders non-ASCII header encoders Content-Transfer-Encoding selectors charset-aware message constructors and MIME encoding pipelines.

5 min read Feb 11, 2029
AI

Claude Code for email.utils: Python Email Address and Header Utilities

Parse and format RFC 2822 email addresses and dates with Python's email.utils module and Claude Code — email utils parseaddr for splitting a display-name plus angle-bracket address string into a realname and email address tuple, email utils formataddr for combining a realname and address string into a properly quoted RFC 2822 address with angle brackets, email utils getaddresses for parsing a list of raw address header strings each potentially containing multiple comma-separated addresses into a list of realname address tuples, email utils parsedate for parsing an RFC 2822 date string into a nine-tuple compatible with time.mktime, email utils parsedate_tz for parsing an RFC 2822 date string into a ten-tuple that includes the UTC offset timezone in seconds, email utils parsedate_to_datetime for parsing an RFC 2822 date string into an aware datetime object with timezone, email utils formatdate for formatting a POSIX timestamp or the current time as an RFC 2822 date string with optional usegmt and localtime flags, email utils format_datetime for formatting a datetime object as an RFC 2822 date string, email utils make_msgid for generating a globally unique Message-ID string with optional idstring and domain components, email utils decode_rfc2231 for decoding an RFC 2231 encoded parameter value into a tuple of charset language and value, email utils encode_rfc2231 for encoding a string as an RFC 2231 encoded parameter value, email utils collapse_rfc2231_value for collapsing a decoded RFC 2231 tuple to a Unicode string, and email utils integration with email.message and email.headerregistry and datetime and time for building address parsers date formatters message-id generators header extractors and RFC-compliant email construction utilities.

5 min read Feb 10, 2029

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free