From 70b1f409b725426186ec209a70a9348283bb48fa Mon Sep 17 00:00:00 2001 From: Hannah Hunter <94493363+hhunter-ms@users.noreply.github.com> Date: Thu, 21 May 2026 13:57:08 -0400 Subject: [PATCH] Add Python Hello Cities sample for Durable Functions --- .../python/hello-cities/.gitignore | 3 + .../python/hello-cities/README.md | 66 +++++++++++++++++++ .../python/hello-cities/function_app.py | 55 ++++++++++++++++ .../python/hello-cities/host.json | 21 ++++++ .../python/hello-cities/local.settings.json | 8 +++ .../python/hello-cities/requirements.txt | 2 + 6 files changed, 155 insertions(+) create mode 100644 samples/durable-functions/python/hello-cities/.gitignore create mode 100644 samples/durable-functions/python/hello-cities/README.md create mode 100644 samples/durable-functions/python/hello-cities/function_app.py create mode 100644 samples/durable-functions/python/hello-cities/host.json create mode 100644 samples/durable-functions/python/hello-cities/local.settings.json create mode 100644 samples/durable-functions/python/hello-cities/requirements.txt diff --git a/samples/durable-functions/python/hello-cities/.gitignore b/samples/durable-functions/python/hello-cities/.gitignore new file mode 100644 index 00000000..77ac7549 --- /dev/null +++ b/samples/durable-functions/python/hello-cities/.gitignore @@ -0,0 +1,3 @@ +.venv/ +__pycache__/ +*.pyc diff --git a/samples/durable-functions/python/hello-cities/README.md b/samples/durable-functions/python/hello-cities/README.md new file mode 100644 index 00000000..5f12bd33 --- /dev/null +++ b/samples/durable-functions/python/hello-cities/README.md @@ -0,0 +1,66 @@ +# Hello Cities — Durable Functions Python Quickstart + +Python | Durable Functions + +## Description + +This quickstart demonstrates Durable Functions with Python (v2 programming model) using the Durable Task Scheduler backend. It includes two patterns: + +1. **Function Chaining** — An orchestration that calls three "say hello" activities sequentially +2. **Fan-out/Fan-in** — An orchestration that greets multiple cities in parallel and aggregates results + +## Prerequisites + +1. [Python 3.9+](https://www.python.org/downloads/) +2. [Azure Functions Core Tools v4](https://learn.microsoft.com/azure/azure-functions/functions-run-local) +3. [Docker](https://www.docker.com/products/docker-desktop/) (for the emulator) + +## Quick Run + +1. Start the emulator: + ```bash + docker run -d -p 8080:8080 -p 8082:8082 mcr.microsoft.com/dts/dts-emulator:latest + ``` + +2. Create a virtual environment and install dependencies: + ```bash + cd samples/durable-functions/python/hello-cities + python -m venv .venv + source .venv/bin/activate # On Windows: .venv\Scripts\activate + pip install -r requirements.txt + ``` + +3. Run the function app: + ```bash + func start + ``` + +4. Trigger the function chaining orchestration: + ```bash + curl -X POST http://localhost:7071/api/StartChaining + ``` + +5. Trigger the fan-out/fan-in orchestration: + ```bash + curl -X POST http://localhost:7071/api/StartFanOutFanIn + ``` + +6. View in the dashboard: http://localhost:8082 + +## Expected Output + +The chaining orchestration greets Tokyo, Seattle, and London sequentially: +```json +["Hello Tokyo!", "Hello Seattle!", "Hello London!"] +``` + +The fan-out/fan-in orchestration greets all five cities in parallel and returns combined results: +```json +["Hello Tokyo!", "Hello Seattle!", "Hello London!", "Hello Paris!", "Hello Berlin!"] +``` + +## Learn More + +- [Durable Functions Python API Reference](https://learn.microsoft.com/python/api/azure-functions-durable/azure.durable_functions) +- [Durable Functions Python Quickstart](https://learn.microsoft.com/azure/azure-functions/durable/quickstart-python-vscode) +- [Durable Task Scheduler Documentation](https://aka.ms/dts-documentation) diff --git a/samples/durable-functions/python/hello-cities/function_app.py b/samples/durable-functions/python/hello-cities/function_app.py new file mode 100644 index 00000000..4b658e74 --- /dev/null +++ b/samples/durable-functions/python/hello-cities/function_app.py @@ -0,0 +1,55 @@ +import azure.functions as func +import azure.durable_functions as df +import logging + +app = df.DFApp(http_auth_level=func.AuthLevel.ANONYMOUS) + + +@app.orchestration_trigger(context_name="context") +def chaining_orchestration(context: df.DurableOrchestrationContext): + """Function chaining orchestration: calls activities sequentially.""" + result1 = yield context.call_activity("say_hello", "Tokyo") + result2 = yield context.call_activity("say_hello", "Seattle") + result3 = yield context.call_activity("say_hello", "London") + return [result1, result2, result3] + + +@app.orchestration_trigger(context_name="context") +def fan_out_fan_in_orchestration(context: df.DurableOrchestrationContext): + """Fan-out/Fan-in orchestration: calls activities in parallel.""" + cities = ["Tokyo", "Seattle", "London", "Paris", "Berlin"] + + # Fan-out: schedule all activities in parallel + parallel_tasks = [] + for city in cities: + task = context.call_activity("say_hello", city) + parallel_tasks.append(task) + + # Fan-in: wait for all to complete + results = yield context.task_all(parallel_tasks) + return results + + +@app.activity_trigger(input_name="city") +def say_hello(city: str) -> str: + """Activity function that returns a greeting for a city.""" + logging.info(f"Saying hello to {city}.") + return f"Hello {city}!" + + +@app.route(route="StartChaining", methods=["POST"]) +@app.durable_client_input(client_name="client") +async def start_chaining(req: func.HttpRequest, client) -> func.HttpResponse: + """HTTP trigger to start the function chaining orchestration.""" + instance_id = await client.start_new("chaining_orchestration") + logging.info(f"Started chaining orchestration with ID = '{instance_id}'.") + return client.create_check_status_response(req, instance_id) + + +@app.route(route="StartFanOutFanIn", methods=["POST"]) +@app.durable_client_input(client_name="client") +async def start_fan_out_fan_in(req: func.HttpRequest, client) -> func.HttpResponse: + """HTTP trigger to start the fan-out/fan-in orchestration.""" + instance_id = await client.start_new("fan_out_fan_in_orchestration") + logging.info(f"Started fan-out/fan-in orchestration with ID = '{instance_id}'.") + return client.create_check_status_response(req, instance_id) diff --git a/samples/durable-functions/python/hello-cities/host.json b/samples/durable-functions/python/hello-cities/host.json new file mode 100644 index 00000000..431660d8 --- /dev/null +++ b/samples/durable-functions/python/hello-cities/host.json @@ -0,0 +1,21 @@ +{ + "version": "2.0", + "logging": { + "logLevel": { + "DurableTask.Core": "Warning" + } + }, + "extensions": { + "durableTask": { + "hubName": "default", + "storageProvider": { + "type": "azureManaged", + "connectionStringName": "DURABLE_TASK_SCHEDULER_CONNECTION_STRING" + } + } + }, + "extensionBundle": { + "id": "Microsoft.Azure.Functions.ExtensionBundle", + "version": "[4.*, 5.0.0)" + } +} diff --git a/samples/durable-functions/python/hello-cities/local.settings.json b/samples/durable-functions/python/hello-cities/local.settings.json new file mode 100644 index 00000000..93856c74 --- /dev/null +++ b/samples/durable-functions/python/hello-cities/local.settings.json @@ -0,0 +1,8 @@ +{ + "IsEncrypted": false, + "Values": { + "AzureWebJobsStorage": "UseDevelopmentStorage=true", + "FUNCTIONS_WORKER_RUNTIME": "python", + "DURABLE_TASK_SCHEDULER_CONNECTION_STRING": "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None" + } +} diff --git a/samples/durable-functions/python/hello-cities/requirements.txt b/samples/durable-functions/python/hello-cities/requirements.txt new file mode 100644 index 00000000..4d2b03ac --- /dev/null +++ b/samples/durable-functions/python/hello-cities/requirements.txt @@ -0,0 +1,2 @@ +azure-functions +azure-functions-durable