Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
7ff1525
Storing changes commit
andystaples Nov 6, 2025
552a2dd
Working orchestrators + activities
andystaples Nov 21, 2025
af0e3c2
Nitpicks and cleanup
andystaples Nov 21, 2025
86ee081
Merge branch 'main' into andystaples/add-functions-support
andystaples Nov 21, 2025
3497148
Save-all nits
andystaples Nov 21, 2025
57de878
Add entity support (needs extension change)
andystaples Nov 24, 2025
18145f8
Refine entity support
andystaples Dec 3, 2025
9965ba4
Finish entity support
andystaples Dec 3, 2025
209443e
Fixes and improvements
andystaples Dec 4, 2025
cc005ae
Bump durabletask version, fix metadata
andystaples Dec 4, 2025
bf6d6f2
Use Protocol for stubs
andystaples Dec 5, 2025
0a03bd1
Merge branch 'main' into andystaples/add-functions-support
andystaples Dec 5, 2025
1176d03
Update to new workflow pattern
andystaples Dec 5, 2025
7bf763a
Rename stub file
andystaples Dec 5, 2025
811653e
Fix import
andystaples Dec 5, 2025
068cb43
Merge branch 'main' into andystaples/add-functions-support
andystaples Dec 5, 2025
827d201
Experimental dependency revision
andystaples Dec 5, 2025
fde02c5
Update to match changes in functions SDK
andystaples Dec 11, 2025
5b9644d
Merge branch 'main' into andystaples/add-functions-support
andystaples Dec 12, 2025
2df96dc
Merge issue fix
andystaples Dec 12, 2025
eac9efd
Various
andystaples Jan 6, 2026
20aacab
Add Functions to requirements
andystaples Jan 6, 2026
5df87b1
Rename to azure-functions-durable v2
andystaples Jan 30, 2026
ea5c2b0
Re-add Orchestrator object/model
andystaples Feb 9, 2026
2d5fd7a
Merge remote-tracking branch 'origin/main' into andystaples/add-funct…
andystaples Jun 23, 2026
0505a0d
Modernize pipelines for functions package
andystaples Jun 23, 2026
c9ea2fb
Cleanup pyright errors
andystaples Jun 23, 2026
5b906bd
Remove non-existent extension call
andystaples Jun 23, 2026
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
67 changes: 67 additions & 0 deletions .github/workflows/durabletask-azurefunctions.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
name: Durable Task Scheduler SDK (azure-functions-durable)

on:
push:
branches:
- "main"
tags:
- "azurefunctions-v*" # Only run for tags starting with "azurefunctions-v"
pull_request:
branches:
- "main"

permissions:
contents: read

jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.14
uses: actions/setup-python@v5
with:
python-version: 3.14
Comment on lines +21 to +24
- name: Install dependencies
working-directory: azure-functions-durable
run: |
python -m pip install --upgrade pip
pip install setuptools wheel tox
pip install flake8
- name: Run flake8 Linter
working-directory: azure-functions-durable
run: flake8 .
- name: Run flake8 Linter
working-directory: tests/azure-functions-durable
run: flake8 .

run-tests:
strategy:
fail-fast: false
matrix:
python-version: ["3.13", "3.14"]
needs: lint
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- name: Install durabletask locally
run: |
python -m pip install --upgrade pip
pip install pytest
pip install . --force-reinstall

- name: Install azure-functions-durable locally
run: |
pip install ./azure-functions-durable --force-reinstall

- name: Run unit tests
working-directory: tests/azure-functions-durable
run: |
pytest -m "not dts and not azurite" --verbose
23 changes: 23 additions & 0 deletions .github/workflows/typecheck.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ on:
tags:
- "v*"
- "azuremanaged-v*"
- "azurefunctions-v*"
pull_request:
branches:
- "main"
Expand Down Expand Up @@ -36,3 +37,25 @@ jobs:

- name: Run pyright (strict, Python 3.10)
run: pyright

pyright-azurefunctions:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Python 3.13 (lowest supported by azure-functions-durable)
uses: actions/setup-python@v5
with:
python-version: "3.13"

- name: Install packages and dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install -e ".[azure-blob-payloads,opentelemetry]"
pip install -e ./azure-functions-durable
pip install pyright

- name: Run pyright (strict, Python 3.13)
run: pyright -p azure-functions-durable/pyrightconfig.json
10 changes: 10 additions & 0 deletions azure-functions-durable/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## v0.1.0

- Initial implementation
Comment on lines +8 to +10
17 changes: 17 additions & 0 deletions azure-functions-durable/azure/durable_functions/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

from .internal.functions_json import install_custom_serialization
from .decorators.durable_app import Blueprint, DFApp
from .client import DurableFunctionsClient
from .orchestrator import Orchestrator

# Ensure the durabletask JSON encoder/decoder is replaced as soon as the
# durable_functions package is imported.
install_custom_serialization()

# IMPORTANT: DO NOT REMOVE. `azure-functions` relies on the presence and value of this variable
# for version detection
version = "2.x"

__all__ = ["Blueprint", "DFApp", "DurableFunctionsClient", "Orchestrator", "version"]
115 changes: 115 additions & 0 deletions azure-functions-durable/azure/durable_functions/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

import json

from datetime import timedelta
import azure.functions as func
from urllib.parse import urlparse, quote

from durabletask.client import TaskHubGrpcClient
from .internal.azurefunctions_grpc_interceptor import AzureFunctionsDefaultClientInterceptorImpl
from .http import HttpManagementPayload


# Client class used for Durable Functions
class DurableFunctionsClient(TaskHubGrpcClient):
"""A gRPC client passed to Durable Functions durable client bindings.

Connects to the Durable Functions runtime using gRPC and provides methods
for creating and managing Durable orchestrations, interacting with Durable entities,
and creating HTTP management payloads and check status responses for use with Durable Functions invocations.
"""
taskHubName: str
connectionName: str
creationUrls: dict[str, str]
managementUrls: dict[str, str]
baseUrl: str
requiredQueryStringParameters: str
rpcBaseUrl: str
httpBaseUrl: str
maxGrpcMessageSizeInBytes: int
grpcHttpClientTimeout: timedelta

def __init__(self, client_as_string: str):
"""Initializes a DurableFunctionsClient instance from a JSON string.

This string will be provided by the Durable Functions host extension upon invocation of the client trigger.

Args:
client_as_string (str): A JSON string containing the Durable Functions client configuration.

Raises:
json.JSONDecodeError: If the provided string is not valid JSON.
"""
self._parse_client_configuration(client_as_string)

interceptors = [AzureFunctionsDefaultClientInterceptorImpl(self.taskHubName, self.requiredQueryStringParameters)]

# We pass in None for the metadata so we don't construct an additional interceptor in the parent class
# Since the parent class doesn't use anything metadata for anything else, we can set it as None
super().__init__(
host_address=self.rpcBaseUrl,
secure_channel=False,
metadata=None,
interceptors=interceptors)

def _parse_client_configuration(self, client_as_string: str) -> None:
"""Parses the client configuration JSON string and sets instance variables.

Args:
client_as_string (str): A JSON string containing the Durable Functions client configuration.

Raises:
json.JSONDecodeError: If the provided string is not valid JSON.
"""
client = json.loads(client_as_string)

self.taskHubName = client.get("taskHubName", "")
self.connectionName = client.get("connectionName", "")
self.creationUrls = client.get("creationUrls", {})
self.managementUrls = client.get("managementUrls", {})
self.baseUrl = client.get("baseUrl", "")
self.requiredQueryStringParameters = client.get("requiredQueryStringParameters", "")
self.rpcBaseUrl = client.get("rpcBaseUrl", "")
self.httpBaseUrl = client.get("httpBaseUrl", "")
self.maxGrpcMessageSizeInBytes = client.get("maxGrpcMessageSizeInBytes", 0)
# TODO: convert the string value back to timedelta - annoying regex?
self.grpcHttpClientTimeout = client.get("grpcHttpClientTimeout", timedelta(seconds=30))

def create_check_status_response(self, request: func.HttpRequest, instance_id: str) -> func.HttpResponse:
"""Creates an HTTP response for checking the status of a Durable Function instance.

Args:
request (func.HttpRequest): The incoming HTTP request.
instance_id (str): The ID of the Durable Function instance.
"""
location_url = self._get_instance_status_url(request, instance_id)
return func.HttpResponse(
body=str(self._get_client_response_links(request, instance_id)),
status_code=202,
headers={
'content-type': 'application/json',
'Location': location_url,
},
)

def create_http_management_payload(self, request: func.HttpRequest, instance_id: str) -> HttpManagementPayload:
"""Creates an HTTP management payload for a Durable Function instance.

Args:
instance_id (str): The ID of the Durable Function instance.
"""
return self._get_client_response_links(request, instance_id)

def _get_client_response_links(self, request: func.HttpRequest, instance_id: str) -> HttpManagementPayload:
instance_status_url = self._get_instance_status_url(request, instance_id)
return HttpManagementPayload(instance_id, instance_status_url, self.requiredQueryStringParameters)

@staticmethod
def _get_instance_status_url(request: func.HttpRequest, instance_id: str) -> str:
request_url = urlparse(request.url)
location_url = f"{request_url.scheme}://{request_url.netloc}"
encoded_instance_id = quote(instance_id)
location_url = location_url + "/runtime/webhooks/durabletask/instances/" + encoded_instance_id
return location_url
8 changes: 8 additions & 0 deletions azure-functions-durable/azure/durable_functions/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

"""Constants used to determine the local running context."""
ORCHESTRATION_TRIGGER = "orchestrationTrigger"
ACTIVITY_TRIGGER = "activityTrigger"
ENTITY_TRIGGER = "entityTrigger"
DURABLE_CLIENT = "durableClient"
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
Loading
Loading