-
Notifications
You must be signed in to change notification settings - Fork 20
Andystaples/add functions support #75
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 10 commits
7ff1525
552a2dd
af0e3c2
86ee081
3497148
57de878
18145f8
9965ba4
209443e
cc005ae
bf6d6f2
0a03bd1
1176d03
7bf763a
811653e
068cb43
827d201
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | |||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,126 @@ | |||||||||||||||||||||||
| name: Durable Task Scheduler SDK (durabletask-azurefunctions) | |||||||||||||||||||||||
|
|
|||||||||||||||||||||||
| on: | |||||||||||||||||||||||
| push: | |||||||||||||||||||||||
| branches: | |||||||||||||||||||||||
| - "main" | |||||||||||||||||||||||
| tags: | |||||||||||||||||||||||
| - "azurefunctions-v*" # Only run for tags starting with "azurefunctions-v" | |||||||||||||||||||||||
| pull_request: | |||||||||||||||||||||||
| branches: | |||||||||||||||||||||||
| - "main" | |||||||||||||||||||||||
|
|
|||||||||||||||||||||||
| 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 | |||||||||||||||||||||||
| - name: Install dependencies | |||||||||||||||||||||||
| working-directory: durabletask-azurefunctions | |||||||||||||||||||||||
| run: | | |||||||||||||||||||||||
| python -m pip install --upgrade pip | |||||||||||||||||||||||
| pip install setuptools wheel tox | |||||||||||||||||||||||
| pip install flake8 | |||||||||||||||||||||||
| - name: Run flake8 Linter | |||||||||||||||||||||||
| working-directory: durabletask-azurefunctions | |||||||||||||||||||||||
| run: flake8 . | |||||||||||||||||||||||
| - name: Run flake8 Linter | |||||||||||||||||||||||
| working-directory: tests/durabletask-azurefunctions | |||||||||||||||||||||||
| run: flake8 . | |||||||||||||||||||||||
|
|
|||||||||||||||||||||||
| run-docker-tests: | |||||||||||||||||||||||
| strategy: | |||||||||||||||||||||||
| fail-fast: false | |||||||||||||||||||||||
| matrix: | |||||||||||||||||||||||
| python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] | |||||||||||||||||||||||
| env: | |||||||||||||||||||||||
| EMULATOR_VERSION: "latest" | |||||||||||||||||||||||
| needs: lint | |||||||||||||||||||||||
| runs-on: ubuntu-latest | |||||||||||||||||||||||
| steps: | |||||||||||||||||||||||
| - name: Checkout repository | |||||||||||||||||||||||
| uses: actions/checkout@v4 | |||||||||||||||||||||||
|
|
|||||||||||||||||||||||
| - name: Pull Docker image | |||||||||||||||||||||||
| run: docker pull mcr.microsoft.com/dts/dts-emulator:$EMULATOR_VERSION | |||||||||||||||||||||||
|
|
|||||||||||||||||||||||
| - name: Run Docker container | |||||||||||||||||||||||
| run: | | |||||||||||||||||||||||
| docker run --name dtsemulator -d -p 8080:8080 mcr.microsoft.com/dts/dts-emulator:$EMULATOR_VERSION | |||||||||||||||||||||||
|
|
|||||||||||||||||||||||
| - name: Wait for container to be ready | |||||||||||||||||||||||
| run: sleep 10 # Adjust if your service needs more time to start | |||||||||||||||||||||||
|
|
|||||||||||||||||||||||
| - name: Set environment variables | |||||||||||||||||||||||
| run: | | |||||||||||||||||||||||
| echo "TASKHUB=default" >> $GITHUB_ENV | |||||||||||||||||||||||
| echo "ENDPOINT=http://localhost:8080" >> $GITHUB_ENV | |||||||||||||||||||||||
|
|
|||||||||||||||||||||||
| - name: Install durabletask dependencies | |||||||||||||||||||||||
| run: | | |||||||||||||||||||||||
| python -m pip install --upgrade pip | |||||||||||||||||||||||
| pip install flake8 pytest | |||||||||||||||||||||||
| pip install -r requirements.txt | |||||||||||||||||||||||
|
|
|||||||||||||||||||||||
| - name: Install durabletask-azurefunctions dependencies | |||||||||||||||||||||||
| working-directory: examples | |||||||||||||||||||||||
| run: | | |||||||||||||||||||||||
| python -m pip install --upgrade pip | |||||||||||||||||||||||
| pip install -r requirements.txt | |||||||||||||||||||||||
|
|
|||||||||||||||||||||||
| - name: Install durabletask-azurefunctions locally | |||||||||||||||||||||||
| working-directory: durabletask-azurefunctions | |||||||||||||||||||||||
| run: | | |||||||||||||||||||||||
| pip install . --no-deps --force-reinstall | |||||||||||||||||||||||
|
|
|||||||||||||||||||||||
| - name: Install durabletask locally | |||||||||||||||||||||||
| run: | | |||||||||||||||||||||||
| pip install . --no-deps --force-reinstall | |||||||||||||||||||||||
|
|
|||||||||||||||||||||||
| - name: Run the tests | |||||||||||||||||||||||
| working-directory: tests/durabletask-azurefunctions | |||||||||||||||||||||||
| run: | | |||||||||||||||||||||||
| pytest -m "dts" --verbose | |||||||||||||||||||||||
|
|
|||||||||||||||||||||||
| publish: | |||||||||||||||||||||||
|
|||||||||||||||||||||||
| @@ -1,4 +1,6 @@ | ||
| name: Durable Task Scheduler SDK (durabletask-azurefunctions) | ||
| permissions: | ||
| contents: read | ||
|
|
||
| on: | ||
| push: |
Check warning
Code scanning / CodeQL
Workflow does not contain permissions Medium
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 3 hours ago
To fix this issue, explicitly set a permissions block for the whole workflow or for each job as needed. The ideal minimum is to set contents: read, unless the job requires other types of access. For the publish-release job, releasing to PyPI and not creating tags, releases, or manipulating pull requests, contents: read is sufficient (it only reads package files, not writing to the repo).
We should therefore add the following block near the top of the workflow (to affect all jobs, unless overridden):
permissions:
contents: readAlternatively, it could be added inside the publish-release job if only that job requires explicit restriction, but in almost all cases, setting it globally is clearer and safer (unless other jobs in the workflow require additional write privileges).
This change is made at the root of the YAML file, directly after the name: block and before the on: block.
No other code changes or external dependencies are required.
-
Copy modified lines R2-R3
| @@ -1,4 +1,6 @@ | ||
| name: Durable Task Scheduler SDK (durabletask-azurefunctions) | ||
| permissions: | ||
| contents: read | ||
|
|
||
| on: | ||
| push: |
| 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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| # Copyright (c) Microsoft Corporation. | ||
| # Licensed under the MIT License. | ||
|
|
||
| from durabletask.azurefunctions.decorators.durable_app import Blueprint, DFApp | ||
| from durabletask.azurefunctions.client import DurableFunctionsClient | ||
|
|
||
| __all__ = ["Blueprint", "DFApp", "DurableFunctionsClient"] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| # Copyright (c) Microsoft Corporation. | ||
| # Licensed under the MIT License. | ||
|
|
||
| import json | ||
|
|
||
| from datetime import timedelta | ||
| from typing import Any, Optional | ||
| import azure.functions as func | ||
| from urllib.parse import urlparse, quote | ||
|
|
||
| from durabletask.entities import EntityInstanceId | ||
| from durabletask.client import TaskHubGrpcClient | ||
| from durabletask.azurefunctions.internal.azurefunctions_grpc_interceptor import AzureFunctionsDefaultClientInterceptorImpl | ||
| from durabletask.azurefunctions.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] | ||
|
Comment on lines
+27
to
+28
|
||
| 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. | ||
| """ | ||
| 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)) | ||
andystaples marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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 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=501, | ||
| 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 | ||
| 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.""" | ||
andystaples marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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. |
Check warning
Code scanning / CodeQL
Workflow does not contain permissions Medium