DEV Community

Bidyut Mahanta
Bidyut Mahanta

Posted on

Zeitwerkzeug – Contextual Time Scheduling for Python

Zeitwerkzeug
Photo by Aron Visuals on Unsplash

Zeitwerkzeug – Contextual Time Scheduling for Python

Time is a tool. Most schedulers treat it as a fixed timestamp – a rigid point on a line. But in the real world, time changes depending on where you are, who you are, and what's happening.

Today I'm excited to announce the alpha release of Zeitwerkzeug (German for "time tool") – a Python library that models time through:

☀️ Solar geometry – sunrise, sunset, golden hour, and custom angles

🧑‍💼 Human personas – wake/sleep rhythms, weekend shifts, proportional time blocks

🌦️ Environmental conditions – weather, sun altitude, and logical combinators

Async execution – drifting schedules that recalibrate over time

It's built for automation, IoT, and context‑aware scheduling – anywhere you need to adapt to changing conditions.


The Problem with Clock‑Based Scheduling

Most scheduling systems assume time is absolute. You tell them: "Run at 7:00 AM every day." But what if you want to water your garden when the sun rises, but only if it's not cloudy? Or what if you need to schedule tasks around a night‑shift worker's sleep cycle?

Traditional cron jobs can't handle:

  • Solar‑dependent events (dawn, dusk) that shift daily.
  • Human rhythms (wake/sleep) that vary on weekends.
  • Live conditions (weather, visibility) that determine if a task makes sense.

Zeitwerkzeug solves this by treating time as something that can be resolved lazily – a schedule is defined as a target (sunrise, 2 hours after waking, etc.) and then evaluated at runtime with real context.


How It Works

1. Solar‑Based Triggers

Define a schedule that follows the sun:

from zeitwerkzeug import Location, schedule, SolarEvent

location = Location(lat=34.6937, lon=135.5020, timezone="Asia/Tokyo")

# Daily sunrise trigger
trigger = schedule.at(SolarEvent.SUNRISE, location=location)
Enter fullscreen mode Exit fullscreen mode

You can also use golden hour, sunset, or custom angles:

from zeitwerkzeug.astro import SolarAngle

golden_hour = SolarAngle(altitude=-4.0, rising=True)
trigger = schedule.at(golden_hour, location=location)
Enter fullscreen mode Exit fullscreen mode

2. Human Personas

Model a person's daily rhythm:

from zeitwerkzeug.personas import StandardWorker
from datetime import timedelta

persona = StandardWorker(tz="Asia/Tokyo")

# Run 2 hours after waking
trigger = schedule.at(
    lambda t: persona.wake_datetime(t) + timedelta(hours=2)
)
Enter fullscreen mode Exit fullscreen mode

Built‑in personas: StandardWorker, NightShift. You can also create custom profiles.

3. Context‑Aware Conditions

Add conditions that must be true before execution:

from zeitwerkzeug import SunAltitudeAbove, TimeWindow, All
from datetime import time

location = Location(lat=34.6937, lon=135.5020, timezone="Asia/Tokyo")

trigger = schedule.at(SolarEvent.SUNSET, location=location).require(
    All((SunAltitudeAbove(location, min_altitude=-6.0),  # civil twilight
        TimeWindow(start=time(18, 0), end=time(23, 0), tz="Asia/Tokyo"),))
)
Enter fullscreen mode Exit fullscreen mode

4. Weather Integration (Open‑Meteo)

Check cloud cover before watering:

from zeitwerkzeug import ClearWeather

trigger = schedule.at(SolarEvent.SUNRISE, location=location).require(
    ClearWeather(lat=location.lat, lon=location.lon, max_cloud_cover=40)
)
Enter fullscreen mode Exit fullscreen mode

The free tier is rate‑limited with built‑in safety margins. Commercial API keys are also supported.

5. Retry Policies

Handle failures gracefully:

from datetime import timedelta

trigger = schedule.at(SolarEvent.SUNRISE, location=location).on_fail(
    retry_interval=timedelta(minutes=15),
    max_attempts=3,
    limit=timedelta(hours=2),
)
Enter fullscreen mode Exit fullscreen mode

6. Async Execution Loop

The ExecutionLoop runs the scheduler asynchronously:

from zeitwerkzeug import FuzzyCron, ExecutionLoop
from datetime import UTC, datetime

cron = FuzzyCron()
cron.register(water_plants, trigger)

loop = ExecutionLoop(registry=cron, max_concurrency=5)
await loop.run(until=datetime(2026, 1, 1, tzinfo=UTC))
Enter fullscreen mode Exit fullscreen mode

It automatically recalibrates at midnight, handles concurrency, and keeps a history of executions.


Real‑World Example: Garden Irrigation

Here's a complete snippet that waters the garden at sunrise, but only if cloud cover is under 40%:

#!/usr/bin/env python3
import asyncio
from datetime import timedelta

from zeitwerkzeug import (
    Location, schedule, FuzzyCron, ExecutionLoop,
    SolarEvent, All, SunAltitudeAbove, ClearWeather,
)

LOCATION = Location(lat=34.6937, lon=135.5020, timezone="Asia/Tokyo")


def water_plants(ctx):
    print(f"💧 Watering at {ctx.triggered_at} (attempt {ctx.attempt})")


async def main():
    trigger = (
        schedule.at(SolarEvent.SUNRISE, location=LOCATION)
        .require(
            All((ClearWeather(lat=LOCATION.lat, lon=LOCATION.lon, max_cloud_cover=40),
                SunAltitudeAbove(LOCATION, min_altitude=-6.0),))
        )
        .on_fail(
            retry_interval=timedelta(minutes=15),
            max_attempts=3,
            limit=timedelta(hours=2),
        )
    )

    cron = FuzzyCron()
    cron.register(water_plants, trigger)

    loop = ExecutionLoop(registry=cron)
    await loop.run()


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

Installation

pip install zeitwerkzeug
Enter fullscreen mode Exit fullscreen mode

For weather:

pip install "zeitwerkzeug[weather]"
Enter fullscreen mode Exit fullscreen mode

Project Status

Alpha release (v0.0.3) – the API is stable for experiments, but breaking changes may occur before 1.0. We're looking for early adopters to test use cases and provide feedback.

Top comments (0)