Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ Examples:

## Language and Style

- Python 3.10+ is required.
- Python 3.11+ is required for the core and Azure Managed packages.
- Python 3.13+ is required for the Azure Functions Durable package.
- Use type hints for all public API signatures.
- Follow PEP 8 conventions.
- Use `autopep8` for Python formatting.
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/durabletask-azuremanaged.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.15"]
python-version: ["3.11", "3.12", "3.13", "3.14", "3.15"]
needs: lint
runs-on: ubuntu-latest
steps:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/durabletask.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.15"]
python-version: ["3.11", "3.12", "3.13", "3.14", "3.15"]
needs: lint-and-unit-tests
runs-on: ubuntu-latest
steps:
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/typecheck.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,17 @@ jobs:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Set up Python 3.10 (lowest supported)
- name: Set up Python 3.11 (lowest supported)
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.10"
python-version: "3.11"

- name: Install packages and dependencies
run: |
python -m pip install --upgrade pip
pip install nox

- name: Run pyright (strict, Python 3.10)
- name: Run pyright (strict, Python 3.11)
run: nox -s typecheck_core

pyright-azurefunctions:
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Unreleased

CHANGED

- Dropped Python 3.10 support. Python 3.11 or later is now required.
- Context managers now preserve subclass types in type checkers, including
Azure Managed clients and workers.
- Multiple task completion callback failures now raise the built-in
`ExceptionGroup`, preserving each exception and its traceback. A single callback
failure is still raised directly.

FIXED

- Orchestrator generators are now closed when a dispatch ends, so cleanup in
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ and [Azure Durable Functions](https://learn.microsoft.com/azure/azure-functions/
- [`azure-functions-durable`](./azure-functions-durable/) is a preview Azure
Durable Functions provider built on `durabletask`.

## Python Requirements

The core SDK and Azure Managed provider require Python 3.11 or later.
The Azure Functions Durable provider requires Python 3.13 or later.

## References

- [Supported Patterns](./docs/supported-patterns.md)
Expand Down
6 changes: 6 additions & 0 deletions azure-functions-durable/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ The opt-in uses the shared codec's `ensure_ascii` option available in
CHANGED

- Updated the minimum `azure-functions` dependency to v2.4.0b1.
- With the corresponding core SDK update, client and worker context managers
preserve Functions subclass types in type checkers. Python 3.13 remains the
minimum supported version for this package.
- With the corresponding core SDK update, multiple task completion callback
failures raise the built-in `ExceptionGroup`; a single failure is still raised
directly.

FIXED

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@

import urllib.error
import urllib.request
from datetime import datetime, timedelta, timezone
from datetime import UTC, datetime, timedelta
from email.utils import parsedate_to_datetime
from typing import Any, Generator, Optional
from urllib.parse import urljoin, urlparse
Expand Down Expand Up @@ -258,9 +258,9 @@ def _retry_after_seconds(headers: dict[str, str], now: datetime) -> int:
# header); treat it as UTC. Normalize ``now`` the same way so the
# subtraction is well-defined regardless of the caller's tz-awareness.
if retry_at.tzinfo is None:
retry_at = retry_at.replace(tzinfo=timezone.utc)
retry_at = retry_at.replace(tzinfo=UTC)
if now.tzinfo is None:
now = now.replace(tzinfo=timezone.utc)
now = now.replace(tzinfo=UTC)
return max(int((retry_at - now).total_seconds()), 0)


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# Licensed under the MIT License.

import json
from datetime import datetime, timezone
from datetime import UTC, datetime
from typing import Any, Optional, cast

from durabletask.client import OrchestrationState, OrchestrationStatus
Expand Down Expand Up @@ -211,5 +211,5 @@ def _format_datetime(value: datetime) -> str:
consumers that parse with that exact format working.
"""
if value.tzinfo is not None:
value = value.astimezone(timezone.utc).replace(tzinfo=None)
value = value.astimezone(UTC).replace(tzinfo=None)
return value.strftime("%Y-%m-%dT%H:%M:%S.%f") + "Z"
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import json
from dataclasses import fields, is_dataclass
from datetime import datetime, timezone
from datetime import UTC, datetime
from typing import Any, cast

from durabletask import history, task
Expand Down Expand Up @@ -185,5 +185,5 @@ def _raw_payload(serialized: str) -> Any:

def _format_datetime(value: datetime) -> str:
if value.tzinfo is not None:
value = value.astimezone(timezone.utc).replace(tzinfo=None)
value = value.astimezone(UTC).replace(tzinfo=None)
return value.strftime("%Y-%m-%dT%H:%M:%S.%f") + "Z"
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import inspect
import json
from datetime import datetime, timezone
from datetime import UTC, datetime
from typing import Any, Callable, Generator, Optional, cast
from uuid import UUID

Expand Down Expand Up @@ -74,7 +74,7 @@ def current_utc_datetime(self) -> datetime:
"""
value = self._ctx.current_utc_datetime
if value.tzinfo is None:
return value.replace(tzinfo=timezone.utc)
return value.replace(tzinfo=UTC)
return value

@property
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from __future__ import annotations

from collections.abc import Mapping
from datetime import datetime, timezone
from datetime import UTC, datetime
from typing import Any, Optional, cast

from durabletask.client import (
Expand Down Expand Up @@ -217,7 +217,7 @@ def export_instance_history_client_bound(
def _created_at_from(payload: Mapping[str, Any]) -> datetime:
raw = payload.get("created_at")
parsed = dt_from_iso(raw) if raw else None
return parsed or datetime.now(timezone.utc)
return parsed or datetime.now(UTC)


class FunctionsExportJobEntity(ExportJobEntity):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import inspect
from dataclasses import dataclass, field
from datetime import datetime, timezone
from datetime import UTC, datetime
from typing import Any, Callable, TypeAlias, TypeVar, overload

from durabletask.entities import DurableEntity, EntityContext, EntityInstanceId
Expand Down Expand Up @@ -228,7 +228,7 @@ def _decode_action(
if action.HasField("sendSignal"):
signal = action.sendSignal
scheduled_time = (
signal.scheduledTime.ToDatetime(tzinfo=timezone.utc)
signal.scheduledTime.ToDatetime(tzinfo=UTC)
if signal.HasField("scheduledTime")
else None
)
Expand Down
8 changes: 4 additions & 4 deletions docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ nox -s ci
```

The `ci` session runs linting, strict type checks, core SDK and Azure Managed
tests on Python 3.10, Azure Functions unit tests on Python 3.13, and Azure
tests on Python 3.11, Azure Functions unit tests on Python 3.13, and Azure
Functions end-to-end tests on Python 3.13. It intentionally runs one
representative version rather than the complete CI matrix, keeping routine
local validation fast. To use another supported version (3.10--3.15) for the
local validation fast. To use another supported version (3.11--3.15) for the
core SDK and Azure Managed tests, pass it after `--`:

```sh
Expand Down Expand Up @@ -53,8 +53,8 @@ For a focused change, run just the relevant session:
nox -R -s lint -- durabletask/client.py tests/durabletask/test_client.py
nox -R -s typecheck_core -- durabletask/client.py durabletask/extensions/history_export
nox -R -s typecheck_functions -- azure-functions-durable/azure
nox -R -s core_tests-3.10 -- tests/durabletask/test_client.py::test_get_grpc_channel_insecure
nox -R -s azuremanaged_tests-3.10 -- tests/durabletask-azuremanaged/test_dts_orchestration_e2e.py
nox -R -s core_tests-3.11 -- tests/durabletask/test_client.py::test_get_grpc_channel_insecure
nox -R -s azuremanaged_tests-3.11 -- tests/durabletask-azuremanaged/test_dts_orchestration_e2e.py
nox -R -s functions_unit-3.13 -- tests/azure-functions-durable/test_client_compat.py
nox -R -s functions_e2e -- -k "dtask_client"
```
Expand Down
9 changes: 9 additions & 0 deletions durabletask-azuremanaged/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Unreleased

CHANGED

- Dropped Python 3.10 support. Python 3.11 or later is now required.
- With the corresponding core SDK update, client and worker context managers
preserve Azure Managed subclass types in type checkers.
- With the corresponding core SDK update, multiple task completion callback
failures raise the built-in `ExceptionGroup`; a single failure is still raised
directly.

FIXED

- With the corresponding core `durabletask` SDK fix, orchestrator generators
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# Licensed under the MIT License.
import asyncio
import os
from datetime import datetime, timedelta, timezone
from datetime import UTC, datetime, timedelta
from threading import Lock

from azure.core.credentials import AccessToken, TokenCredential
Expand Down Expand Up @@ -62,14 +62,14 @@ def get_access_token(self) -> AccessToken | None:
def is_token_expired(self) -> bool:
if self.expiry_time is None:
return True
return datetime.now(timezone.utc) >= (self.expiry_time - timedelta(seconds=self._refresh_interval_seconds))
return datetime.now(UTC) >= (self.expiry_time - timedelta(seconds=self._refresh_interval_seconds))

def refresh_token(self):
if self._credential is not None:
self._token = self._credential.get_token(self._scope)

# Convert UNIX timestamp to timezone-aware datetime
self.expiry_time = datetime.fromtimestamp(self._token.expires_on, tz=timezone.utc)
self.expiry_time = datetime.fromtimestamp(self._token.expires_on, tz=UTC)
self._logger.debug(f"Token refreshed. Expires at: {self.expiry_time}")


Expand Down Expand Up @@ -126,13 +126,13 @@ async def get_access_token(self) -> AccessToken | None:
def is_token_expired(self) -> bool:
if self.expiry_time is None:
return True
return datetime.now(timezone.utc) >= (
return datetime.now(UTC) >= (
self.expiry_time - timedelta(seconds=self._refresh_interval_seconds))

async def refresh_token(self):
if self._credential is not None:
self._token = await self._credential.get_token(self._scope)

# Convert UNIX timestamp to timezone-aware datetime
self.expiry_time = datetime.fromtimestamp(self._token.expires_on, tz=timezone.utc)
self.expiry_time = datetime.fromtimestamp(self._token.expires_on, tz=UTC)
self._logger.debug(f"Token refreshed. Expires at: {self.expiry_time}")
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

from typing import Optional, Sequence
from typing import Optional, Self, Sequence

import grpc
from azure.core.credentials import TokenCredential
Expand Down Expand Up @@ -48,7 +48,7 @@ def __init__(
def close(self) -> None:
self._transport.close()

def __enter__(self) -> "SandboxActivitiesClient":
def __enter__(self) -> Self:
return self

def __exit__(self, exc_type: object, exc_value: object, traceback: object) -> None:
Expand Down
3 changes: 1 addition & 2 deletions durabletask-azuremanaged/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,14 @@ keywords = [
classifiers = [
"Development Status :: 5 - Production/Stable",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Programming Language :: Python :: 3.15",
"License :: OSI Approved :: MIT License",
]
requires-python = ">=3.10"
requires-python = ">=3.11"
license = {file = "LICENSE"}
readme = "README.md"
dependencies = [
Expand Down
6 changes: 3 additions & 3 deletions durabletask/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from enum import Enum
from typing import Any, Generic, Protocol, TypeVar, cast, overload
from typing import Any, Generic, Protocol, Self, TypeVar, cast, overload

import grpc
import grpc.aio
Expand Down Expand Up @@ -620,7 +620,7 @@ def close(self) -> None:
retired_channel.close()
current_channel.close()

def __enter__(self) -> "TaskHubGrpcClient":
def __enter__(self) -> Self:
return self

def __exit__(self, *args: object) -> None:
Expand Down Expand Up @@ -1105,7 +1105,7 @@ async def close(self) -> None:
if channel is not None:
await channel.close()

async def __aenter__(self) -> "AsyncTaskHubGrpcClient":
async def __aenter__(self) -> Self:
return self

async def __aexit__(self, *args: object) -> None:
Expand Down
4 changes: 2 additions & 2 deletions durabletask/entities/entity_lock.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from __future__ import annotations

from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Self


if TYPE_CHECKING:
Expand All @@ -15,7 +15,7 @@ class EntityLock:
def __init__(self, context: OrchestrationContext):
self._context = context

def __enter__(self) -> EntityLock:
def __enter__(self) -> Self:
return self

def __exit__(self, *args: object) -> None:
Expand Down
4 changes: 2 additions & 2 deletions durabletask/entities/entity_metadata.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

from datetime import datetime, timezone
from datetime import UTC, datetime
from typing import TYPE_CHECKING, Any, TypeVar, overload
from durabletask.entities.entity_instance_id import EntityInstanceId

Expand Down Expand Up @@ -68,7 +68,7 @@ def from_entity_metadata(entity: pb.EntityMetadata, includes_state: bool,
entity_state = entity.serializedState.value
return EntityMetadata(
id=entity_id,
last_modified=entity.lastModifiedTime.ToDatetime(timezone.utc),
last_modified=entity.lastModifiedTime.ToDatetime(UTC),
backlog_queue_size=entity.backlogQueueSize,
locked_by=entity.lockedBy.value,
includes_state=includes_state,
Expand Down
Loading
Loading