ISO 8601 durations and datetimes with isodate: working versions (tested 2026-10-18)
Parse and format ISO 8601 durations and datetimes, use month and year Duration arithmetic with calendar clamping, and catch the parsing errors isodate raises.
passed6 passed · 0 failed2026-10-18 03:56 UTC- Status
- passed
- Last passed
- 2026-10-18 03:56 UTC
- Tests
- 6 / 6
- Language
- Python
- Runtime
- Python 3.13.16
pinned versions2
isodate@0.7.2pytest@9.1.1
run it
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt .venv/bin/python -m pytest -q
test environment
Python 3.13.16 linux 6.17.0-1022-azure x64 runner: github-actions 39095370682
The code
Exactly the files that passed. Your agent can pull them with get_recipe("isodate-duration-parse") (install), or see them on GitHub.
requirements.txt
isodate==0.7.2 pytest==9.1.1
durations.py
from datetime import date, datetime, timedelta
import isodate
from isodate import Duration
def parse_duration(text: str) -> timedelta | Duration:
return isodate.parse_duration(text)
def format_duration(value: timedelta | Duration) -> str:
return isodate.duration_isoformat(value)
def shift(moment: date | datetime, text: str) -> date | datetime:
return moment + isodate.parse_duration(text)
def parse_instant(text: str) -> datetime:
return isodate.parse_datetime(text)
tests/test_durations.py
from datetime import date, datetime, timedelta
import isodate
import pytest
from durations import format_duration, parse_duration, parse_instant, shift
from isodate import Duration
def test_time_only_duration_is_a_timedelta():
assert parse_duration("PT36H") == timedelta(hours=36)
assert parse_duration("P2W") == timedelta(weeks=2)
assert format_duration(timedelta(hours=36)) == "P1DT12H"
def test_month_duration_is_not_a_timedelta():
one_month = parse_duration("P1M")
assert isinstance(one_month, Duration)
assert one_month == Duration(months=1)
assert one_month != timedelta(days=30)
assert isinstance(parse_duration("P0D"), timedelta)
def test_month_and_year_arithmetic_clamps_to_month_end():
assert shift(datetime(2024, 1, 31), "P1M") == datetime(2024, 2, 29)
assert shift(date(2024, 2, 29), "P1Y") == date(2025, 2, 28)
def test_full_duration_round_trips_and_zero_formats_as_p0d():
text = "P1Y2M3DT4H5M6S"
assert format_duration(parse_duration(text)) == text
assert format_duration(timedelta(0)) == "P0D"
def test_datetime_z_parsing_and_date_only_input():
assert parse_instant("2024-01-01T00:00:00Z").utcoffset() == timedelta(0)
assert parse_instant("2024-01-01T00:00:00Z").tzinfo is isodate.UTC
with pytest.raises(isodate.ISO8601Error):
parse_instant("2024-01-01")
assert isodate.parse_date("2024-01-01") == date(2024, 1, 1)
def test_invalid_durations_raise_but_pt_is_silently_zero():
for bad in ("1Y", "P1H", "P"):
with pytest.raises(isodate.ISO8601Error):
parse_duration(bad)
assert parse_duration("PT") == timedelta(0)
pytest.ini
[pytest] pythonpath = .
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
~/.cursor/mcp.json
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }.vscode/mcp.json
{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }~/.codeium/windsurf/mcp_config.json
{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }MCP settings
{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.