├── .devcontainer ├── devcontainer.json └── setup.sh ├── .flake8 ├── .github ├── ISSUE_TEMPLATE │ ├── ----feature-request.md │ └── ---bug-report.md ├── policies │ └── resourceManagement.yml └── workflows │ ├── codeQL.yml │ ├── durable_python_action.yml │ ├── submodule-sync.yml │ └── validate.yml ├── .gitignore ├── CHANGELOG.md ├── CODEOWNERS ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── SECURITY.md ├── azure-pipelines-release.yml ├── azure-pipelines.yml ├── azure ├── __init__.py └── durable_functions │ ├── __init__.py │ ├── constants.py │ ├── decorators │ ├── __init__.py │ ├── durable_app.py │ └── metadata.py │ ├── entity.py │ ├── models │ ├── DurableEntityContext.py │ ├── DurableHttpRequest.py │ ├── DurableOrchestrationBindings.py │ ├── DurableOrchestrationClient.py │ ├── DurableOrchestrationContext.py │ ├── DurableOrchestrationStatus.py │ ├── EntityStateResponse.py │ ├── FunctionContext.py │ ├── OrchestrationRuntimeStatus.py │ ├── OrchestratorState.py │ ├── PurgeHistoryResult.py │ ├── ReplaySchema.py │ ├── RetryOptions.py │ ├── RpcManagementOptions.py │ ├── Task.py │ ├── TaskOrchestrationExecutor.py │ ├── TokenSource.py │ ├── __init__.py │ ├── actions │ │ ├── Action.py │ │ ├── ActionType.py │ │ ├── CallActivityAction.py │ │ ├── CallActivityWithRetryAction.py │ │ ├── CallEntityAction.py │ │ ├── CallHttpAction.py │ │ ├── CallSubOrchestratorAction.py │ │ ├── CallSubOrchestratorWithRetryAction.py │ │ ├── CompoundAction.py │ │ ├── ContinueAsNewAction.py │ │ ├── CreateTimerAction.py │ │ ├── NoOpAction.py │ │ ├── SignalEntityAction.py │ │ ├── WaitForExternalEventAction.py │ │ ├── WhenAllAction.py │ │ ├── WhenAnyAction.py │ │ └── __init__.py │ ├── entities │ │ ├── EntityState.py │ │ ├── OperationResult.py │ │ ├── RequestMessage.py │ │ ├── ResponseMessage.py │ │ ├── Signal.py │ │ └── __init__.py │ ├── history │ │ ├── HistoryEvent.py │ │ ├── HistoryEventType.py │ │ └── __init__.py │ └── utils │ │ ├── __init__.py │ │ ├── entity_utils.py │ │ ├── http_utils.py │ │ └── json_utils.py │ ├── orchestrator.py │ └── testing │ ├── OrchestratorGeneratorWrapper.py │ └── __init__.py ├── eng ├── ci │ ├── code-mirror.yml │ └── official-build.yml └── templates │ └── build.yml ├── host.json ├── noxfile.py ├── requirements.txt ├── samples-v2 ├── blueprint │ ├── .funcignore │ ├── .gitignore │ ├── durable_blueprints.py │ ├── function_app.py │ ├── host.json │ ├── requirements.txt │ └── tests │ │ ├── readme.md │ │ ├── test_my_orchestrator.py │ │ ├── test_say_hello.py │ │ └── test_start_orchestrator.py ├── fan_in_fan_out │ ├── .funcignore │ ├── .gitignore │ ├── README.md │ ├── function_app.py │ ├── host.json │ ├── proxies.json │ ├── requirements.txt │ └── tests │ │ ├── readme.md │ │ ├── test_E2_BackupSiteContent.py │ │ ├── test_E2_CopyFileToBlob.py │ │ ├── test_E2_GetFileList.py │ │ └── test_HttpStart.py └── function_chaining │ ├── .funcignore │ ├── .gitignore │ ├── README.md │ ├── function_app.py │ ├── host.json │ ├── proxies.json │ ├── requirements.txt │ └── tests │ ├── readme.md │ ├── test_http_start.py │ ├── test_my_orchestrator.py │ └── test_say_hello.py ├── samples ├── aml_monitoring │ ├── .funcignore │ ├── .gitignore │ ├── aml_durable_orchestrator │ │ ├── __init__.py │ │ └── function.json │ ├── aml_pipeline │ │ ├── __init__.py │ │ └── function.json │ ├── aml_poll_status │ │ ├── __init__.py │ │ └── function.json │ ├── extensions.csproj │ ├── host.json │ ├── local.settings.json │ ├── proxies.json │ ├── requirements.txt │ └── shared │ │ ├── __init__.py │ │ ├── aml_helper.py │ │ └── auth_helper.py ├── counter_entity │ ├── .funcignore │ ├── .gitignore │ ├── Counter │ │ ├── __init__.py │ │ └── function.json │ ├── DurableOrchestration │ │ ├── __init__.py │ │ └── function.json │ ├── DurableTrigger │ │ ├── __init__.py │ │ └── function.json │ ├── README.md │ ├── RetrieveEntity │ │ ├── __init__.py │ │ └── function.json │ ├── host.json │ ├── local.settings.json │ └── requirements.txt ├── fan_in_fan_out │ ├── .funcignore │ ├── .gitignore │ ├── E2_BackupSiteContent │ │ ├── __init__.py │ │ └── function.json │ ├── E2_CopyFileToBlob │ │ ├── __init__.py │ │ └── function.json │ ├── E2_GetFileList │ │ ├── __init__.py │ │ └── function.json │ ├── HttpStart │ │ ├── __init__.py │ │ └── function.json │ ├── README.md │ ├── host.json │ ├── local.settings.json │ ├── proxies.json │ └── requirements.txt ├── function_chaining │ ├── .funcignore │ ├── .gitignore │ ├── E1_HelloSequence │ │ ├── __init__.py │ │ └── function.json │ ├── E1_SayHello │ │ ├── __init__.py │ │ └── function.json │ ├── HttpStart │ │ ├── __init__.py │ │ └── function.json │ ├── README.md │ ├── host.json │ ├── local.settings.json │ ├── proxies.json │ └── requirements.txt ├── function_chaining_custom_status │ ├── .funcignore │ ├── .gitignore │ ├── DurableActivity │ │ ├── __init__.py │ │ └── function.json │ ├── DurableOrchestration │ │ ├── __init__.py │ │ └── function.json │ ├── DurableTrigger │ │ ├── __init__.py │ │ └── function.json │ ├── README.md │ ├── host.json │ ├── local.settings.json │ ├── proxies.json │ └── requirements.txt ├── human_interaction │ ├── .funcignore │ ├── .gitignore │ ├── E4_SMSPhoneVerification │ │ ├── __init__.py │ │ └── function.json │ ├── HttpStart │ │ ├── __init__.py │ │ └── function.json │ ├── README.md │ ├── SendSMSChallenge │ │ ├── __init__.py │ │ └── function.json │ ├── host.json │ ├── local.settings.json │ ├── proxies.json │ └── requirements.txt ├── monitor │ ├── .funcignore │ ├── .gitignore │ ├── E3_Monitor │ │ ├── __init__.py │ │ └── function.json │ ├── E3_SendAlert │ │ ├── __init__.py │ │ └── function.json │ ├── E3_TooManyOpenIssues │ │ ├── __init__.py │ │ └── function.json │ ├── HttpStart │ │ ├── __init__.py │ │ └── function.json │ ├── README.md │ ├── host.json │ ├── local.settings.json │ ├── proxies.json │ └── requirements.txt ├── serialize_arguments │ ├── .gitignore │ ├── DurableActivity │ │ ├── __init__.py │ │ └── function.json │ ├── DurableOrchestration │ │ ├── __init__.py │ │ └── function.json │ ├── DurableTrigger │ │ ├── __init__.py │ │ └── function.json │ ├── README.md │ ├── host.json │ ├── local.settings.json │ ├── requirements.txt │ └── shared_code │ │ └── MyClasses.py └── simple_sub_orchestration │ ├── .funcignore │ ├── .gitignore │ ├── DurableFunctionsHttpStart │ ├── __init__.py │ └── function.json │ ├── DurableOrchestrator │ ├── __init__.py │ └── function.json │ ├── DurableSubOrchestrator │ ├── __init__.py │ └── function.json │ ├── Hello │ ├── __init__.py │ └── function.json │ ├── README.md │ ├── host.json │ ├── local.settings.json │ ├── proxies.json │ └── requirements.txt ├── scripts └── sample_deploy.sh ├── setup.py └── tests ├── __init__.py ├── conftest.py ├── models ├── __init__.py ├── test_DecoratorMetadata.py ├── test_Decorators.py ├── test_DurableOrchestrationBindings.py ├── test_DurableOrchestrationClient.py ├── test_DurableOrchestrationContext.py ├── test_DurableOrchestrationStatus.py ├── test_OrchestrationState.py ├── test_RpcManagementOptions.py └── test_TokenSource.py ├── orchestrator ├── __init__.py ├── models │ └── OrchestrationInstance.py ├── orchestrator_test_utils.py ├── schemas │ └── OrchetrationStateSchema.py ├── test_call_http.py ├── test_continue_as_new.py ├── test_create_timer.py ├── test_entity.py ├── test_external_event.py ├── test_fan_out_fan_in.py ├── test_is_replaying_flag.py ├── test_retries.py ├── test_sequential_orchestrator.py ├── test_sequential_orchestrator_custom_status.py ├── test_sequential_orchestrator_with_retry.py ├── test_serialization.py ├── test_sub_orchestrator.py ├── test_sub_orchestrator_with_retry.py └── test_task_any.py ├── tasks ├── __init__.py ├── tasks_test_utils.py ├── test_long_timers.py └── test_new_uuid.py ├── test_constants.py ├── test_utils ├── ContextBuilder.py ├── EntityContextBuilder.py ├── __init__.py ├── constants.py ├── json_utils.py └── testClasses.py └── utils ├── __init__.py └── test_entity_utils.py /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/alpine 3 | { 4 | "name": "Python, .NET, Azure Functions, and Node.js", 5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 6 | "image": "mcr.microsoft.com/vscode/devcontainers/python:3.8", 7 | 8 | // Features to add to the dev container. More info: https://containers.dev/features. 9 | "features": { 10 | "ghcr.io/devcontainers/features/dotnet:2": { 11 | "version": "8.0" 12 | }, 13 | "ghcr.io/jlaundry/devcontainer-features/azure-functions-core-tools:1": { 14 | "version": "4" 15 | }, 16 | "ghcr.io/devcontainers/features/node:1": { 17 | "version": "lts" 18 | } 19 | }, 20 | 21 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 22 | // "forwardPorts": [], 23 | 24 | // Use 'postCreateCommand' to run commands after the container is created. 25 | "postCreateCommand": "./.devcontainer/setup.sh", 26 | 27 | // Configure tool-specific properties. 28 | "customizations": { 29 | "vscode": { 30 | "extensions": [ 31 | "GitHub.copilot", 32 | "ms-python.python", 33 | "ms-azuretools.vscode-azurestorage", 34 | "njpwerner.autodocstring" 35 | ] 36 | } 37 | } 38 | 39 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 40 | // "remoteUser": "root" 41 | } 42 | -------------------------------------------------------------------------------- /.devcontainer/setup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | pip install -r requirements.txt 4 | 5 | 6 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | # delete D100 for docstring checks, promotes redundant documentation of what's in class docstring 3 | # W503 contradicts with pep8 and will soon be fixed by flake8 4 | ignore = W503, D100 5 | max-line-length = 99 6 | docstring-convention = numpy 7 | exclude = 8 | __pycache__, 9 | azure/durable_functions/grpc/protobuf/ -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/----feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F4A1 Feature request" 3 | about: Suggest an idea for this project! 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 💡 **Feature description** 11 | > What would you like to see, and why? 12 | > What kinds of usage do you expect to see in this feature? 13 | 14 | 💭 **Describe alternatives you've considered** 15 | > What are other ways to achieve this? Have you thought of alternative designs or solutions? 16 | > Any existing workarounds? Why are they not sufficient? We'd like to know! 17 | 18 | **Additional context** 19 | > Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/---bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41B Bug report" 3 | about: Did something not work out as expected? Let us know! 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 17 | 🐛 **Describe the bug** 18 | > A clear and concise description of what the bug is. 19 | 20 | 🤔 **Expected behavior** 21 | > What should have happened? 22 | 23 | ☕ **Steps to reproduce** 24 | > What Durable Functions patterns are you using, if any? 25 | > Any minimal reproducer we can use? 26 | > Are you running this locally or on Azure? 27 | 28 | ⚡**If deployed to Azure** 29 | > We have access to a lot of telemetry that can help with investigations. Please provide as much of the following information as you can to help us investigate! 30 | 31 | - **Timeframe issue observed**: 32 | - **Function App name**: 33 | - **Function name(s)**: 34 | - **Azure region**: 35 | - **Orchestration instance ID(s)**: 36 | - **Azure storage account name**: 37 | 38 | > If you don't want to share your Function App or storage account name GitHub, please at least share the orchestration instance ID. Otherwise it's extremely difficult to look up information. 39 | -------------------------------------------------------------------------------- /.github/workflows/durable_python_action.yml: -------------------------------------------------------------------------------- 1 | name: Azure Durable Functions Python CI Build 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v1 15 | - name: Set up Python 3.6.x 16 | uses: actions/setup-python@v1 17 | with: 18 | python-version: 3.6.x 19 | - name: Install dependencies 20 | run: | 21 | python -m pip install --upgrade pip 22 | pip install -r requirements.txt 23 | - name: Lint with flake8 24 | run: | 25 | # stop the build if there are Python syntax errors or undefined names 26 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 27 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 28 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 29 | - name: Test with pytest 30 | run: | 31 | pytest 32 | -------------------------------------------------------------------------------- /.github/workflows/submodule-sync.yml: -------------------------------------------------------------------------------- 1 | name: Sync submodule pipeline 2 | 3 | on: 4 | push: 5 | branches: [ submodule ] 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | with: 12 | repository: azure-functions-python-library 13 | submodules: true 14 | - id: Go to submodule 15 | run: | 16 | cd azure/functions/durable 17 | git submodule update --remote --merge 18 | git add . 19 | - name: Create Pull Request 20 | id: createPullRequest 21 | uses: peter-evans/create-pull-request@v4 22 | with: 23 | commit-message: Update durable submodule 24 | committer: GitHub 25 | branch: submodule-sync 26 | delete-branch: true 27 | title: 'Update durable submodule' 28 | body: | 29 | Updated submodule 30 | 31 | [1]: https://github.com/peter-evans/create-pull-request 32 | labels: | 33 | required submodule update 34 | automated pr 35 | reviewers: vameru 36 | - name: Check outputs 37 | run: | 38 | echo "Pull Request Number - ${{ steps.createPullRequest.outputs.pull-request-number }}" 39 | echo "Pull Request URL - ${{ steps.createPullRequest.outputs.pull-request-url }}" 40 | -------------------------------------------------------------------------------- /.github/workflows/validate.yml: -------------------------------------------------------------------------------- 1 | name: Validate 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - dev 8 | pull_request: 9 | branches: 10 | - main 11 | - dev 12 | 13 | jobs: 14 | validate: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v2 19 | 20 | - name: Set up Python 21 | uses: actions/setup-python@v2 22 | with: 23 | python-version: 3.9 24 | - name: Install dependencies 25 | run: | 26 | python -m pip install --upgrade pip 27 | pip install -r requirements.txt 28 | - name: Run Linter 29 | run: | 30 | cd azure 31 | flake8 . --count --show-source --statistics 32 | - name: Run tests 33 | run: | 34 | pytest --ignore=samples-v2 35 | 36 | test-samples: 37 | strategy: 38 | matrix: 39 | app_name: [blueprint, fan_in_fan_out, function_chaining] 40 | runs-on: ubuntu-latest 41 | defaults: 42 | run: 43 | working-directory: ./samples-v2/${{ matrix.app_name }} 44 | steps: 45 | - name: Checkout repository 46 | uses: actions/checkout@v2 47 | 48 | - name: Set up Python 49 | uses: actions/setup-python@v2 50 | with: 51 | python-version: 3.9 52 | - name: Install dependencies 53 | run: | 54 | python -m pip install --upgrade pip 55 | pip install -r requirements.txt 56 | pip install -r ../../requirements.txt 57 | pip install ../.. --no-cache-dir --upgrade --no-deps --force-reinstall 58 | - name: Run tests 59 | run: | 60 | python -m pytest 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | # Visual Studio 107 | .vs/ 108 | .vscode/ 109 | 110 | # CSharp 111 | [Oo]bj/ 112 | [Dd]ebug/ 113 | [Rr]elease/ 114 | [Pp]roperties/ 115 | *.dll 116 | *.exe 117 | appsettings.json 118 | appsettings.*.json 119 | 120 | # Certificate files 121 | *.cer 122 | *.pfx 123 | 124 | # pycharm 125 | .idea 126 | .root-refactor 127 | .sample-refactor 128 | .abstract-yield 129 | .bin 130 | 131 | /samples/python_abstract_yield/bin/ 132 | /samples/python_durable_bindings/bin/ 133 | /samples/python_durable_bindings/.vs/ 134 | /samples/python_durable_bindings/obj/ 135 | 136 | # azurite emulator 137 | __azurite_db_*.json 138 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## 1.0.0b6 6 | 7 | - [Create timer](https://github.com/Azure/azure-functions-durable-python/issues/35) functionality available 8 | 9 | ## 1.0.0b5 10 | 11 | - [Object serialization](https://github.com/Azure/azure-functions-durable-python/issues/90) made available 12 | - [Can set custom status](https://github.com/Azure/azure-functions-durable-python/issues/117) of orchestration 13 | 14 | ## 1.0.0b3-b4 15 | - Release to test CD pipeline with push to PyPI 16 | 17 | ## 1.0.0b2 18 | 19 | ### Fixed 20 | - [Remove staticmethod definitions](https://github.com/Azure/azure-functions-durable-python/issues/65) 21 | 22 | ## 1.0.0b1 23 | 24 | ### Added 25 | 26 | The following orchestration patterns are added: 27 | 28 | - Function Chaining 29 | - Fan In Fan Out 30 | - Async HTTP APIs 31 | - Human Interaction 32 | 33 | #### API Parity 34 | - CallActivity 35 | - CallActivityWithRetry 36 | - Task.all 37 | - Task.any 38 | - waitForExternalEvent 39 | - continueAsNew 40 | - callHttp 41 | - currentUtcDateTime 42 | - newUuid 43 | - createCheckStatusResponse 44 | - getStatus 45 | - getStatusAll 46 | - getStatusBy 47 | - purgeInstanceHistory 48 | - purgeInstanceHistoryBy 49 | - raiseEvent 50 | - startNew 51 | - terminate 52 | - waitForCompletionOrCreateCheckStatusResponse 53 | 54 | ### Changed 55 | N/A 56 | 57 | ### Fixed 58 | N/A 59 | 60 | ### Removed 61 | N/A 62 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/about-codeowners/ 2 | # for more info about CODEOWNERS file 3 | # 4 | # It uses the same pattern rule for gitignore file 5 | # https://git-scm.com/docs/gitignore#_pattern_format 6 | # 7 | # 8 | # AZURE FUNCTIONS TEAM 9 | # For all file changes, github would automatically include the following people in the PRs. 10 | # 11 | 12 | * @nytian @cgillum @bachuv @andystaples 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Katy Shimizu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include ./_manifest/* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | |Branch|Status| 2 | |---|---| 3 | |main|[![Build Status](https://azfunc.visualstudio.com/Azure%20Functions/_apis/build/status/Azure.azure-functions-durable-python?branchName=main)](https://azfunc.visualstudio.com/Azure%20Functions/_build/latest?definitionId=58&branchName=main)| 4 | |dev|[![Build Status](https://azfunc.visualstudio.com/Azure%20Functions/_apis/build/status/Azure.azure-functions-durable-python?branchName=dev)](https://azfunc.visualstudio.com/Azure%20Functions/_build/latest?definitionId=58&branchName=dev)| 5 | 6 | # Durable Functions for Python 7 | 8 | [Durable Functions](https://docs.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-overview) is an extension of [Azure Functions](https://docs.microsoft.com/en-us/azure/azure-functions/functions-overview) that lets you write stateful functions in a serverless compute environment. The extension lets you define stateful workflows by writing orchestrator functions and stateful entities by writing entity functions using the Azure Functions programming model. Behind the scenes, the extension manages state, checkpoints, and restarts for you, allowing you to focus on your business logic. 9 | 10 | 🐍 Find us on PyPi [here](https://pypi.org/project/azure-functions-durable/) 🐍 11 | 12 | 13 | You can find more information at the following links: 14 | 15 | * [Azure Functions overview](https://docs.microsoft.com/en-us/azure/azure-functions/functions-overview) 16 | * [Azure Functions Python developers guide](https://docs.microsoft.com/en-us/azure/azure-functions/functions-reference-python) 17 | * [Durable Functions overview](https://docs.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-overview?tabs=python) 18 | * [Core concepts and features overview](https://docs.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-types-features-overview). 19 | 20 | > Durable Functions expects certain programming constraints to be followed. Please read the documentation linked above for more information. 21 | 22 | ## Getting Started 23 | 24 | Follow these instructions to get started with Durable Functions in Python: 25 | 26 | **🚀 [Python Durable Functions quickstart](https://docs.microsoft.com/azure/azure-functions/durable/quickstart-python-vscode)** 27 | 28 | ## Tooling 29 | 30 | * Python Durable Functions requires [Azure Functions Core Tools](https://docs.microsoft.com/en-us/azure/azure-functions/functions-run-local) version 3.0.2630 or higher. 31 | -------------------------------------------------------------------------------- /azure-pipelines-release.yml: -------------------------------------------------------------------------------- 1 | trigger: none 2 | pr: none 3 | 4 | resources: 5 | pipelines: 6 | - pipeline: DurablePyCI 7 | project: "Azure Functions" 8 | source: Azure.azure-functions-durable-python 9 | branch: main 10 | 11 | jobs: 12 | - job: Release 13 | pool: 14 | name: "1ES-Hosted-AzFunc" 15 | demands: 16 | - ImageOverride -equals MMSUbuntu20.04TLS 17 | steps: 18 | - task: UsePythonVersion@0 19 | inputs: 20 | versionSpec: '3.7' 21 | - download: DurablePyCI 22 | - script: "rm -r ./azure_functions_durable/_manifest" 23 | displayName: 'Remove _manifest folder' 24 | workingDirectory: "$(Pipeline.Workspace)/DurablePyCI" 25 | - script: 'pip install twine' 26 | displayName: "Install twine" 27 | - script: "twine upload azure_functions_durable/*" 28 | displayName: "Publish to PyPI" 29 | workingDirectory: "$(Pipeline.Workspace)/DurablePyCI" -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | # Python package 2 | # Create and test a Python package on multiple Python versions. 3 | # Add steps that analyze code, save the dist with the build record, publish to a PyPI-compatible index, and more: 4 | # https://docs.microsoft.com/azure/devops/pipelines/languages/python 5 | 6 | trigger: 7 | branches: 8 | include: 9 | - main 10 | - dev 11 | tags: 12 | include: 13 | - v* 14 | 15 | variables: 16 | python.version: '3.7' 17 | baseFolder: . 18 | componentArtifactName: 'azure_functions_durable' 19 | #componentArtifactName: 'dist' 20 | 21 | 22 | stages: 23 | - stage: Build 24 | displayName: Build PyPi Artifact 25 | jobs: 26 | 27 | - job: Build_Durable_Functions 28 | displayName: Build Python Package 29 | pool: 30 | name: "1ES-Hosted-AzFunc" 31 | demands: 32 | - ImageOverride -equals MMSUbuntu20.04TLS 33 | steps: 34 | - task: UsePythonVersion@0 35 | inputs: 36 | versionSpec: '$(python.version)' 37 | - script: | 38 | python -m pip install --upgrade pip 39 | pip install -r requirements.txt 40 | pip install wheel 41 | workingDirectory: $(baseFolder) 42 | displayName: 'Install dependencies' 43 | 44 | - script: | 45 | cd azure 46 | flake8 . --count --show-source --statistics 47 | displayName: 'Run lint test with flake8' 48 | 49 | - script: | 50 | pip install pytest pytest-azurepipelines 51 | pytest 52 | displayName: 'pytest' 53 | - task: ManifestGeneratorTask@0 54 | displayName: "SBOM Generation Task" 55 | inputs: 56 | BuildComponentPath: '$(baseFolder)' 57 | BuildDropPath: $(baseFolder) 58 | Verbosity: "Information" 59 | - script: | 60 | python setup.py sdist bdist_wheel 61 | workingDirectory: $(baseFolder) 62 | displayName: 'Building' 63 | - task: PublishBuildArtifacts@1 64 | displayName: 'Publish Artifact: dist' 65 | inputs: 66 | PathtoPublish: dist 67 | ArtifactName: $(componentArtifactName) 68 | 69 | 70 | -------------------------------------------------------------------------------- /azure/__init__.py: -------------------------------------------------------------------------------- 1 | """Base module for the Python Durable functions.""" 2 | from pkgutil import extend_path 3 | __path__ = extend_path(__path__, __name__) # type: ignore 4 | -------------------------------------------------------------------------------- /azure/durable_functions/constants.py: -------------------------------------------------------------------------------- 1 | """Constants used to determine the local running context.""" 2 | DEFAULT_LOCAL_HOST: str = 'localhost:7071' 3 | DEFAULT_LOCAL_ORIGIN: str = f'http://{DEFAULT_LOCAL_HOST}' 4 | DATETIME_STRING_FORMAT = '%Y-%m-%dT%H:%M:%S.%fZ' 5 | HTTP_ACTION_NAME = 'BuiltIn::HttpActivity' 6 | ORCHESTRATION_TRIGGER = "orchestrationTrigger" 7 | ACTIVITY_TRIGGER = "activityTrigger" 8 | ENTITY_TRIGGER = "entityTrigger" 9 | DURABLE_CLIENT = "durableClient" 10 | -------------------------------------------------------------------------------- /azure/durable_functions/decorators/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | """Decorator definitions for Durable Functions.""" 4 | -------------------------------------------------------------------------------- /azure/durable_functions/models/DurableHttpRequest.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Union, Optional 2 | 3 | from azure.durable_functions.models.TokenSource import TokenSource 4 | from azure.durable_functions.models.utils.json_utils import add_attrib, add_json_attrib 5 | 6 | 7 | class DurableHttpRequest: 8 | """Data structure representing a durable HTTP request.""" 9 | 10 | def __init__(self, method: str, uri: str, content: Optional[str] = None, 11 | headers: Optional[Dict[str, str]] = None, 12 | token_source: Optional[TokenSource] = None): 13 | self._method: str = method 14 | self._uri: str = uri 15 | self._content: Optional[str] = content 16 | self._headers: Optional[Dict[str, str]] = headers 17 | self._token_source: Optional[TokenSource] = token_source 18 | 19 | @property 20 | def method(self) -> str: 21 | """Get the HTTP request method.""" 22 | return self._method 23 | 24 | @property 25 | def uri(self) -> str: 26 | """Get the HTTP request uri.""" 27 | return self._uri 28 | 29 | @property 30 | def content(self) -> Optional[str]: 31 | """Get the HTTP request content.""" 32 | return self._content 33 | 34 | @property 35 | def headers(self) -> Optional[Dict[str, str]]: 36 | """Get the HTTP request headers.""" 37 | return self._headers 38 | 39 | @property 40 | def token_source(self) -> Optional[TokenSource]: 41 | """Get the source of OAuth token to add to the request.""" 42 | return self._token_source 43 | 44 | def to_json(self) -> Dict[str, Union[str, int]]: 45 | """Convert object into a json dictionary. 46 | 47 | Returns 48 | ------- 49 | Dict[str, Union[str, int]] 50 | The instance of the class converted into a json dictionary 51 | """ 52 | json_dict: Dict[str, Union[str, int]] = {} 53 | add_attrib(json_dict, self, 'method') 54 | add_attrib(json_dict, self, 'uri') 55 | add_attrib(json_dict, self, 'content') 56 | add_attrib(json_dict, self, 'headers') 57 | add_json_attrib(json_dict, self, 'token_source', 'tokenSource') 58 | return json_dict 59 | -------------------------------------------------------------------------------- /azure/durable_functions/models/EntityStateResponse.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | 4 | class EntityStateResponse: 5 | """Entity state response object for [read_entity_state].""" 6 | 7 | def __init__(self, entity_exists: bool, entity_state: Any = None) -> None: 8 | self._entity_exists = entity_exists 9 | self._entity_state = entity_state 10 | 11 | @property 12 | def entity_exists(self) -> bool: 13 | """Get the bool representing whether entity exists.""" 14 | return self._entity_exists 15 | 16 | @property 17 | def entity_state(self) -> Any: 18 | """Get the state of the entity. 19 | 20 | When [entity_exists] is False, this value will be None. 21 | Optional. 22 | """ 23 | return self._entity_state 24 | -------------------------------------------------------------------------------- /azure/durable_functions/models/FunctionContext.py: -------------------------------------------------------------------------------- 1 | class FunctionContext: 2 | """Object to hold any additional function level attributes not used by Durable.""" 3 | 4 | def __init__(self, **kwargs): 5 | if kwargs is not None: 6 | for key, value in kwargs.items(): 7 | self.__setattr__(key, value) 8 | -------------------------------------------------------------------------------- /azure/durable_functions/models/OrchestrationRuntimeStatus.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class OrchestrationRuntimeStatus(Enum): 5 | """The status of an orchestration instance.""" 6 | 7 | Running = 'Running' 8 | """The orchestration instance has started running.""" 9 | 10 | Completed = 'Completed' 11 | """The orchestration instance has completed normally.""" 12 | 13 | ContinuedAsNew = 'ContinuedAsNew' 14 | """The orchestration instance has restarted itself with a new history. 15 | 16 | This is a transient state. 17 | """ 18 | 19 | Failed = 'Failed' 20 | """The orchestration instance failed with an error.""" 21 | 22 | Canceled = 'Canceled' 23 | """The orchestration was canceled gracefully.""" 24 | 25 | Terminated = 'Terminated' 26 | """The orchestration instance was stopped abruptly.""" 27 | 28 | Pending = 'Pending' 29 | """The orchestration instance has been scheduled but has not yet started running.""" 30 | 31 | Suspended = 'Suspended' 32 | """The orchestration instance has been suspended and may go back to running at a later time.""" 33 | -------------------------------------------------------------------------------- /azure/durable_functions/models/PurgeHistoryResult.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Any 2 | 3 | 4 | class PurgeHistoryResult: 5 | """Information provided when the request to purge history has been made.""" 6 | 7 | # parameter names are as defined by JSON schema and do not conform to PEP8 naming conventions 8 | def __init__(self, instancesDeleted: int, **kwargs): 9 | self._instances_deleted: int = instancesDeleted 10 | if kwargs is not None: 11 | for key, value in kwargs.items(): 12 | self.__setattr__(key, value) 13 | 14 | @classmethod 15 | def from_json(cls, json_obj: Dict[Any, Any]): 16 | """Convert the value passed into a new instance of the class. 17 | 18 | Parameters 19 | ---------- 20 | json_obj: Dict[Any, Any] 21 | JSON object to be converted into an instance of the class 22 | 23 | Returns 24 | ------- 25 | PurgeHistoryResult 26 | New instance of the durable orchestration status class 27 | """ 28 | return cls(**json_obj) 29 | 30 | @property 31 | def instances_deleted(self): 32 | """Get the number of deleted instances.""" 33 | return self._instances_deleted 34 | -------------------------------------------------------------------------------- /azure/durable_functions/models/ReplaySchema.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class ReplaySchema(Enum): 5 | """Enum representing the ReplaySchemas supported by this SDK version.""" 6 | 7 | V1 = 0 8 | V2 = 1 9 | V3 = 2 10 | -------------------------------------------------------------------------------- /azure/durable_functions/models/RetryOptions.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Union 2 | 3 | from .utils.json_utils import add_attrib 4 | 5 | 6 | class RetryOptions: 7 | """Retry Options. 8 | 9 | Defines retry policies that can be passed as parameters to various 10 | operations. 11 | """ 12 | 13 | def __init__( 14 | self, 15 | first_retry_interval_in_milliseconds: int, 16 | max_number_of_attempts: int): 17 | self._first_retry_interval_in_milliseconds: int = \ 18 | first_retry_interval_in_milliseconds 19 | self._max_number_of_attempts: int = max_number_of_attempts 20 | 21 | if self._first_retry_interval_in_milliseconds <= 0: 22 | raise ValueError("first_retry_interval_in_milliseconds value" 23 | "must be greater than 0.") 24 | 25 | @property 26 | def first_retry_interval_in_milliseconds(self) -> int: 27 | """Get the first retry interval (ms). 28 | 29 | Must be greater than 0 30 | 31 | Returns 32 | ------- 33 | int 34 | The value indicating the first retry interval 35 | """ 36 | return self._first_retry_interval_in_milliseconds 37 | 38 | @property 39 | def max_number_of_attempts(self) -> int: 40 | """Get Max Number of Attempts. 41 | 42 | Returns 43 | ------- 44 | int 45 | Value indicating the max number of attempts to retry 46 | """ 47 | return self._max_number_of_attempts 48 | 49 | def to_json(self) -> Dict[str, Union[str, int]]: 50 | """Convert object into a json dictionary. 51 | 52 | Returns 53 | ------- 54 | Dict[str, Any] 55 | The instance of the class converted into a json dictionary 56 | """ 57 | json_dict: Dict[str, Union[str, int]] = {} 58 | 59 | add_attrib( 60 | json_dict, 61 | self, 62 | 'first_retry_interval_in_milliseconds', 63 | 'firstRetryIntervalInMilliseconds') 64 | add_attrib( 65 | json_dict, 66 | self, 67 | 'max_number_of_attempts', 68 | 'maxNumberOfAttempts') 69 | return json_dict 70 | -------------------------------------------------------------------------------- /azure/durable_functions/models/TokenSource.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | from typing import Dict, Union 3 | 4 | from azure.durable_functions.models.utils.json_utils import add_attrib 5 | 6 | 7 | class TokenSource(ABC): 8 | """Token Source implementation for [Azure Managed Identities]. 9 | 10 | https://docs.microsoft.com/azure/active-directory/managed-identities-azure-resources/overview. 11 | 12 | @example Get a list of Azure Subscriptions by calling the Azure Resource Manager HTTP API. 13 | ```python 14 | import azure.durable_functions as df 15 | 16 | def generator_function(context): 17 | return yield context.callHttp( 18 | "GET", 19 | "https://management.azure.com/subscriptions?api-version=2019-06-01", 20 | None, 21 | None, 22 | df.ManagedIdentityTokenSource("https://management.core.windows.net")) 23 | """ 24 | 25 | def __init__(self): 26 | super().__init__() 27 | 28 | 29 | class ManagedIdentityTokenSource(TokenSource): 30 | """Returns a `ManagedIdentityTokenSource` object.""" 31 | 32 | def __init__(self, resource: str): 33 | super().__init__() 34 | self._resource: str = resource 35 | self._kind: str = "AzureManagedIdentity" 36 | 37 | @property 38 | def resource(self) -> str: 39 | """Get the Azure Active Directory resource identifier of the web API being invoked. 40 | 41 | For example, `https://management.core.windows.net/` or `https://graph.microsoft.com/`. 42 | """ 43 | return self._resource 44 | 45 | def to_json(self) -> Dict[str, Union[str, int]]: 46 | """Convert object into a json dictionary. 47 | 48 | Returns 49 | ------- 50 | Dict[str, Any] 51 | The instance of the class converted into a json dictionary 52 | """ 53 | json_dict: Dict[str, Union[str, int]] = {} 54 | add_attrib(json_dict, self, 'resource') 55 | json_dict["kind"] = self._kind 56 | return json_dict 57 | -------------------------------------------------------------------------------- /azure/durable_functions/models/__init__.py: -------------------------------------------------------------------------------- 1 | """Model definitions for Durable Functions.""" 2 | from .DurableOrchestrationBindings import DurableOrchestrationBindings 3 | from .DurableOrchestrationClient import DurableOrchestrationClient 4 | from .DurableOrchestrationContext import DurableOrchestrationContext 5 | from .OrchestratorState import OrchestratorState 6 | from .OrchestrationRuntimeStatus import OrchestrationRuntimeStatus 7 | from .PurgeHistoryResult import PurgeHistoryResult 8 | from .RetryOptions import RetryOptions 9 | from .DurableHttpRequest import DurableHttpRequest 10 | from .TokenSource import ManagedIdentityTokenSource 11 | from .DurableEntityContext import DurableEntityContext 12 | from .Task import TaskBase 13 | 14 | __all__ = [ 15 | 'DurableOrchestrationBindings', 16 | 'DurableOrchestrationClient', 17 | 'DurableEntityContext', 18 | 'DurableOrchestrationContext', 19 | 'DurableHttpRequest', 20 | 'ManagedIdentityTokenSource', 21 | 'OrchestratorState', 22 | 'OrchestrationRuntimeStatus', 23 | 'PurgeHistoryResult', 24 | 'RetryOptions', 25 | 'TaskBase' 26 | ] 27 | -------------------------------------------------------------------------------- /azure/durable_functions/models/actions/Action.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Any 2 | from abc import ABC, abstractmethod 3 | 4 | 5 | class Action(ABC): 6 | """Defines the base abstract class for Actions that need to be implemented.""" 7 | 8 | @property 9 | @abstractmethod 10 | def action_type(self) -> int: 11 | """Get the type of action this class represents.""" 12 | pass 13 | 14 | @abstractmethod 15 | def to_json(self) -> Dict[str, Any]: 16 | """Convert object into a json dictionary. 17 | 18 | Returns 19 | ------- 20 | Dict[str, Any] 21 | The instance of the class converted into a json dictionary 22 | """ 23 | pass 24 | -------------------------------------------------------------------------------- /azure/durable_functions/models/actions/ActionType.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | 3 | 4 | class ActionType(IntEnum): 5 | """Defines the values associated to the types of activities that can be scheduled.""" 6 | 7 | CALL_ACTIVITY: int = 0 8 | CALL_ACTIVITY_WITH_RETRY: int = 1 9 | CALL_SUB_ORCHESTRATOR: int = 2 10 | CALL_SUB_ORCHESTRATOR_WITH_RETRY: int = 3 11 | CONTINUE_AS_NEW: int = 4 12 | CREATE_TIMER: int = 5 13 | WAIT_FOR_EXTERNAL_EVENT: int = 6 14 | CALL_ENTITY = 7 15 | CALL_HTTP: int = 8 16 | SIGNAL_ENTITY: int = 9 17 | WHEN_ANY = 11 18 | WHEN_ALL = 12 19 | -------------------------------------------------------------------------------- /azure/durable_functions/models/actions/CallActivityAction.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Union 2 | 3 | from .Action import Action 4 | from .ActionType import ActionType 5 | from ..utils.json_utils import add_attrib 6 | from json import dumps 7 | from azure.functions._durable_functions import _serialize_custom_object 8 | 9 | 10 | class CallActivityAction(Action): 11 | """Defines the structure of the Call Activity object. 12 | 13 | Provides the information needed by the durable extension to be able to schedule the activity. 14 | """ 15 | 16 | def __init__(self, function_name: str, input_=None): 17 | self.function_name: str = function_name 18 | # It appears that `.input_` needs to be JSON-serializable at this point 19 | self.input_ = dumps(input_, default=_serialize_custom_object) 20 | 21 | if not self.function_name: 22 | raise ValueError("function_name cannot be empty") 23 | 24 | @property 25 | def action_type(self) -> int: 26 | """Get the type of action this class represents.""" 27 | return ActionType.CALL_ACTIVITY 28 | 29 | def to_json(self) -> Dict[str, Union[str, int]]: 30 | """Convert object into a json dictionary. 31 | 32 | Returns 33 | ------- 34 | Dict[str, Union[str, int]] 35 | The instance of the class converted into a json dictionary 36 | """ 37 | json_dict: Dict[str, Union[str, int]] = {} 38 | add_attrib(json_dict, self, 'action_type', 'actionType') 39 | add_attrib(json_dict, self, 'function_name', 'functionName') 40 | add_attrib(json_dict, self, 'input_', 'input') 41 | return json_dict 42 | -------------------------------------------------------------------------------- /azure/durable_functions/models/actions/CallActivityWithRetryAction.py: -------------------------------------------------------------------------------- 1 | from json import dumps 2 | from typing import Dict, Union 3 | 4 | from .Action import Action 5 | from .ActionType import ActionType 6 | from ..RetryOptions import RetryOptions 7 | from ..utils.json_utils import add_attrib, add_json_attrib 8 | from azure.functions._durable_functions import _serialize_custom_object 9 | 10 | 11 | class CallActivityWithRetryAction(Action): 12 | """Defines the structure of the Call Activity With Retry object. 13 | 14 | Provides the information needed by the durable extension to be able to schedule the activity. 15 | """ 16 | 17 | def __init__(self, function_name: str, 18 | retry_options: RetryOptions, input_=None): 19 | self.function_name: str = function_name 20 | self.retry_options: RetryOptions = retry_options 21 | self.input_ = dumps(input_, default=_serialize_custom_object) 22 | 23 | if not self.function_name: 24 | raise ValueError("function_name cannot be empty") 25 | 26 | @property 27 | def action_type(self) -> int: 28 | """Get the type of action this class represents.""" 29 | return ActionType.CALL_ACTIVITY_WITH_RETRY 30 | 31 | def to_json(self) -> Dict[str, Union[str, int]]: 32 | """Convert object into a json dictionary. 33 | 34 | Returns 35 | ------- 36 | Dict[str, Union[str, int]] 37 | The instance of the class converted into a json dictionary 38 | """ 39 | json_dict: Dict[str, Union[str, int]] = {} 40 | 41 | add_attrib(json_dict, self, 'action_type', 'actionType') 42 | add_attrib(json_dict, self, 'function_name', 'functionName') 43 | add_attrib(json_dict, self, 'input_', 'input') 44 | add_json_attrib(json_dict, self, 'retry_options', 'retryOptions') 45 | return json_dict 46 | -------------------------------------------------------------------------------- /azure/durable_functions/models/actions/CallEntityAction.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict 2 | 3 | from .Action import Action 4 | from .ActionType import ActionType 5 | from ..utils.json_utils import add_attrib 6 | from json import dumps 7 | from azure.functions._durable_functions import _serialize_custom_object 8 | from ..utils.entity_utils import EntityId 9 | 10 | 11 | class CallEntityAction(Action): 12 | """Defines the structure of the Call Entity object. 13 | 14 | Provides the information needed by the durable extension to be able to call an activity 15 | """ 16 | 17 | def __init__(self, entity_id: EntityId, operation: str, input_=None): 18 | self.entity_id: EntityId = entity_id 19 | 20 | # Validating that EntityId exists before trying to parse its instanceId 21 | if not self.entity_id: 22 | raise ValueError("entity_id cannot be empty") 23 | 24 | self.instance_id: str = EntityId.get_scheduler_id(entity_id) 25 | self.operation: str = operation 26 | self.input_: str = dumps(input_, default=_serialize_custom_object) 27 | 28 | @property 29 | def action_type(self) -> int: 30 | """Get the type of action this class represents.""" 31 | return ActionType.CALL_ENTITY 32 | 33 | def to_json(self) -> Dict[str, Any]: 34 | """Convert object into a json dictionary. 35 | 36 | Returns 37 | ------- 38 | Dict[str, Any] 39 | The instance of the class converted into a json dictionary 40 | """ 41 | json_dict: Dict[str, Any] = {} 42 | add_attrib(json_dict, self, "action_type", "actionType") 43 | add_attrib(json_dict, self, 'instance_id', 'instanceId') 44 | add_attrib(json_dict, self, 'operation', 'operation') 45 | add_attrib(json_dict, self, 'input_', 'input') 46 | return json_dict 47 | -------------------------------------------------------------------------------- /azure/durable_functions/models/actions/CallHttpAction.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict 2 | 3 | from .Action import Action 4 | from .ActionType import ActionType 5 | from ..DurableHttpRequest import DurableHttpRequest 6 | from ..utils.json_utils import add_attrib, add_json_attrib 7 | 8 | 9 | class CallHttpAction(Action): 10 | """Defines the structure of the Call Http object. 11 | 12 | Provides the information needed by the durable extension to be able to schedule the activity. 13 | """ 14 | 15 | def __init__(self, http_request: DurableHttpRequest): 16 | self._action_type: int = ActionType.CALL_HTTP 17 | self.http_request = http_request 18 | 19 | @property 20 | def action_type(self) -> int: 21 | """Get the type of action this class represents.""" 22 | return ActionType.CALL_HTTP 23 | 24 | def to_json(self) -> Dict[str, Any]: 25 | """Convert object into a json dictionary. 26 | 27 | Returns 28 | ------- 29 | Dict[str, Any] 30 | The instance of the class converted into a json dictionary 31 | """ 32 | json_dict: Dict[str, Any] = {} 33 | add_attrib(json_dict, self, 'action_type', 'actionType') 34 | add_json_attrib(json_dict, self, 'http_request', 'httpRequest') 35 | return json_dict 36 | -------------------------------------------------------------------------------- /azure/durable_functions/models/actions/CallSubOrchestratorAction.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, Optional, Union 2 | 3 | from .Action import Action 4 | from .ActionType import ActionType 5 | from ..utils.json_utils import add_attrib 6 | from json import dumps 7 | from azure.functions._durable_functions import _serialize_custom_object 8 | 9 | 10 | class CallSubOrchestratorAction(Action): 11 | """Defines the structure of the Call SubOrchestrator object.""" 12 | 13 | def __init__(self, function_name: str, _input: Optional[Any] = None, 14 | instance_id: Optional[str] = None): 15 | self.function_name: str = function_name 16 | self._input: str = dumps(_input, default=_serialize_custom_object) 17 | self.instance_id: Optional[str] = instance_id 18 | 19 | if not self.function_name: 20 | raise ValueError("function_name cannot be empty") 21 | 22 | @property 23 | def action_type(self) -> int: 24 | """Get the type of action this class represents.""" 25 | return ActionType.CALL_SUB_ORCHESTRATOR 26 | 27 | def to_json(self) -> Dict[str, Union[str, int]]: 28 | """Convert object into a json dictionary. 29 | 30 | Returns 31 | ------- 32 | Dict[str, Union(str, int)] 33 | The instance of the class converted into a json dictionary 34 | """ 35 | json_dict: Dict[str, Union[str, int]] = {} 36 | add_attrib(json_dict, self, 'action_type', 'actionType') 37 | add_attrib(json_dict, self, 'function_name', 'functionName') 38 | add_attrib(json_dict, self, '_input', 'input') 39 | add_attrib(json_dict, self, 'instance_id', 'instanceId') 40 | return json_dict 41 | -------------------------------------------------------------------------------- /azure/durable_functions/models/actions/CallSubOrchestratorWithRetryAction.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, Union, Optional 2 | 3 | from .Action import Action 4 | from .ActionType import ActionType 5 | from ..utils.json_utils import add_attrib, add_json_attrib 6 | from json import dumps 7 | from ..RetryOptions import RetryOptions 8 | from azure.functions._durable_functions import _serialize_custom_object 9 | 10 | 11 | class CallSubOrchestratorWithRetryAction(Action): 12 | """Defines the structure of the Call SubOrchestrator object.""" 13 | 14 | def __init__(self, function_name: str, retry_options: RetryOptions, 15 | _input: Optional[Any] = None, 16 | instance_id: Optional[str] = None): 17 | self.function_name: str = function_name 18 | self._input: str = dumps(_input, default=_serialize_custom_object) 19 | self.retry_options: RetryOptions = retry_options 20 | self.instance_id: Optional[str] = instance_id 21 | 22 | if not self.function_name: 23 | raise ValueError("function_name cannot be empty") 24 | 25 | @property 26 | def action_type(self) -> int: 27 | """Get the type of action this class represents.""" 28 | return ActionType.CALL_SUB_ORCHESTRATOR_WITH_RETRY 29 | 30 | def to_json(self) -> Dict[str, Union[str, int]]: 31 | """Convert object into a json dictionary. 32 | 33 | Returns 34 | ------- 35 | Dict[str, Union(str, int)] 36 | The instance of the class converted into a json dictionary 37 | """ 38 | json_dict: Dict[str, Union[str, int]] = {} 39 | add_attrib(json_dict, self, 'action_type', 'actionType') 40 | add_attrib(json_dict, self, 'function_name', 'functionName') 41 | add_attrib(json_dict, self, '_input', 'input') 42 | add_json_attrib(json_dict, self, 'retry_options', 'retryOptions') 43 | add_attrib(json_dict, self, 'instance_id', 'instanceId') 44 | return json_dict 45 | -------------------------------------------------------------------------------- /azure/durable_functions/models/actions/CompoundAction.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Union 2 | 3 | from .Action import Action 4 | from ..utils.json_utils import add_attrib 5 | from typing import List 6 | from abc import abstractmethod 7 | 8 | 9 | class CompoundAction(Action): 10 | """Defines the structure of the WhenAll Action object. 11 | 12 | Provides the information needed by the durable extension to be able to invoke WhenAll tasks. 13 | """ 14 | 15 | def __init__(self, compound_tasks: List[Action]): 16 | self.compound_tasks = compound_tasks 17 | 18 | @property 19 | @abstractmethod 20 | def action_type(self) -> int: 21 | """Get this object's action type as an integer.""" 22 | ... 23 | 24 | def to_json(self) -> Dict[str, Union[str, int]]: 25 | """Convert object into a json dictionary. 26 | 27 | Returns 28 | ------- 29 | Dict[str, Union[str, int]] 30 | The instance of the class converted into a json dictionary 31 | """ 32 | json_dict: Dict[str, Union[str, int]] = {} 33 | add_attrib(json_dict, self, 'action_type', 'actionType') 34 | json_dict['compoundActions'] = list(map(lambda x: x.to_json(), self.compound_tasks)) 35 | return json_dict 36 | -------------------------------------------------------------------------------- /azure/durable_functions/models/actions/ContinueAsNewAction.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Union 2 | 3 | from .Action import Action 4 | from .ActionType import ActionType 5 | from ..utils.json_utils import add_attrib 6 | from json import dumps 7 | from azure.functions._durable_functions import _serialize_custom_object 8 | 9 | 10 | class ContinueAsNewAction(Action): 11 | """Defines the structure of the Continue As New object. 12 | 13 | Provides the information needed by the durable extension to be able to reset the orchestration 14 | and continue as new. 15 | """ 16 | 17 | def __init__(self, input_=None): 18 | self.input_ = dumps(input_, default=_serialize_custom_object) 19 | 20 | @property 21 | def action_type(self) -> int: 22 | """Get the type of action this class represents.""" 23 | return ActionType.CONTINUE_AS_NEW 24 | 25 | def to_json(self) -> Dict[str, Union[int, str]]: 26 | """Convert object into a json dictionary. 27 | 28 | Returns 29 | ------- 30 | Dict[str, Any] 31 | The instance of the class converted into a json dictionary 32 | """ 33 | json_dict: Dict[str, Union[int, str]] = {} 34 | add_attrib(json_dict, self, 'action_type', 'actionType') 35 | add_attrib(json_dict, self, 'input_', 'input') 36 | return json_dict 37 | -------------------------------------------------------------------------------- /azure/durable_functions/models/actions/CreateTimerAction.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, Union 2 | 3 | from .ActionType import ActionType 4 | from .Action import Action 5 | from ..utils.json_utils import add_attrib, add_datetime_attrib 6 | import datetime 7 | 8 | 9 | class CreateTimerAction(Action): 10 | """Defines the structure of the Create Timer object. 11 | 12 | Returns 13 | ------- 14 | Information needed by durable extension to schedule the activity 15 | 16 | Raises 17 | ------ 18 | ValueError 19 | if the event fired is not of valid datetime object 20 | """ 21 | 22 | def __init__(self, fire_at: datetime.datetime, is_cancelled: bool = False): 23 | self._action_type: ActionType = ActionType.CREATE_TIMER 24 | self.fire_at: datetime.datetime = fire_at 25 | self.is_cancelled: bool = is_cancelled 26 | 27 | if not isinstance(self.fire_at, datetime.date): 28 | raise ValueError("fireAt: Expected valid datetime object but got ", self.fire_at) 29 | 30 | def to_json(self) -> Dict[str, Any]: 31 | """ 32 | Convert object into a json dictionary. 33 | 34 | Returns 35 | ------- 36 | Dict[str, Any] 37 | The instance of the class converted into a json dictionary 38 | """ 39 | json_dict: Dict[str, Union[int, str]] = {} 40 | add_attrib(json_dict, self, 'action_type', 'actionType') 41 | add_datetime_attrib(json_dict, self, 'fire_at', 'fireAt') 42 | add_attrib(json_dict, self, 'is_cancelled', 'isCanceled') 43 | return json_dict 44 | 45 | @property 46 | def action_type(self) -> int: 47 | """Get the type of action this class represents.""" 48 | return ActionType.CREATE_TIMER 49 | -------------------------------------------------------------------------------- /azure/durable_functions/models/actions/NoOpAction.py: -------------------------------------------------------------------------------- 1 | from azure.durable_functions.models.actions.Action import Action 2 | from typing import Any, Dict, Optional 3 | 4 | 5 | class NoOpAction(Action): 6 | """A no-op action, for anonymous tasks only.""" 7 | 8 | def __init__(self, metadata: Optional[str] = None): 9 | """Create a NoOpAction object. 10 | 11 | This is an internal-only action class used to represent cases when intermediate 12 | tasks are used to implement some API. For example, in -WithRetry APIs, intermediate 13 | timers are created. We create this NoOp action to track those the backing actions 14 | of those tasks, which is necessary because we mimic the DF-internal replay algorithm. 15 | 16 | Parameters 17 | ---------- 18 | metadata : Optional[str] 19 | Used for internal debugging: metadata about the action being represented. 20 | """ 21 | self.metadata = metadata 22 | 23 | def action_type(self) -> int: 24 | """Get the type of action this class represents.""" 25 | raise Exception("Attempted to get action type of an anonymous Action") 26 | 27 | def to_json(self) -> Dict[str, Any]: 28 | """Convert object into a json dictionary. 29 | 30 | Returns 31 | ------- 32 | Dict[str, Any] 33 | The instance of the class converted into a json dictionary 34 | """ 35 | raise Exception("Attempted to convert an anonymous Action to JSON") 36 | -------------------------------------------------------------------------------- /azure/durable_functions/models/actions/SignalEntityAction.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict 2 | 3 | from .Action import Action 4 | from .ActionType import ActionType 5 | from ..utils.json_utils import add_attrib 6 | from json import dumps 7 | from azure.functions._durable_functions import _serialize_custom_object 8 | from ..utils.entity_utils import EntityId 9 | 10 | 11 | class SignalEntityAction(Action): 12 | """Defines the structure of the Signal Entity object. 13 | 14 | Provides the information needed by the durable extension to be able to signal an entity 15 | """ 16 | 17 | def __init__(self, entity_id: EntityId, operation: str, input_=None): 18 | self.entity_id: EntityId = entity_id 19 | 20 | # Validating that EntityId exists before trying to parse its instanceId 21 | if not self.entity_id: 22 | raise ValueError("entity_id cannot be empty") 23 | 24 | self.instance_id: str = EntityId.get_scheduler_id(entity_id) 25 | self.operation: str = operation 26 | self.input_: str = dumps(input_, default=_serialize_custom_object) 27 | 28 | @property 29 | def action_type(self) -> int: 30 | """Get the type of action this class represents.""" 31 | return ActionType.SIGNAL_ENTITY 32 | 33 | def to_json(self) -> Dict[str, Any]: 34 | """Convert object into a json dictionary. 35 | 36 | Returns 37 | ------- 38 | Dict[str, Any] 39 | The instance of the class converted into a json dictionary 40 | """ 41 | json_dict: Dict[str, Any] = {} 42 | add_attrib(json_dict, self, "action_type", "actionType") 43 | add_attrib(json_dict, self, 'instance_id', 'instanceId') 44 | add_attrib(json_dict, self, 'operation', 'operation') 45 | add_attrib(json_dict, self, 'input_', 'input') 46 | 47 | return json_dict 48 | -------------------------------------------------------------------------------- /azure/durable_functions/models/actions/WaitForExternalEventAction.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, Union 2 | 3 | from .Action import Action 4 | from .ActionType import ActionType 5 | from ..utils.json_utils import add_attrib 6 | 7 | 8 | class WaitForExternalEventAction(Action): 9 | """Defines the structure of Wait for External Event object. 10 | 11 | Returns 12 | ------- 13 | WaitForExternalEventAction 14 | Returns a WaitForExternalEventAction Class. 15 | 16 | Raises 17 | ------ 18 | ValueError 19 | Raises error if external_event_name is not defined. 20 | """ 21 | 22 | def __init__(self, external_event_name: str): 23 | self.external_event_name: str = external_event_name 24 | self.reason = "ExternalEvent" 25 | 26 | if not self.external_event_name: 27 | raise ValueError("external_event_name cannot be empty") 28 | 29 | @property 30 | def action_type(self) -> int: 31 | """Get the type of action this class represents.""" 32 | return ActionType.WAIT_FOR_EXTERNAL_EVENT 33 | 34 | def to_json(self) -> Dict[str, Any]: 35 | """Convert object into a json dictionary. 36 | 37 | Returns 38 | ------- 39 | Dict[str, Union[str, int]] 40 | The instance of the class converted into a json dictionary 41 | """ 42 | json_dict: Dict[str, Union[str, int]] = {} 43 | 44 | add_attrib(json_dict, self, 'action_type', 'actionType') 45 | add_attrib(json_dict, self, 'external_event_name', 'externalEventName') 46 | add_attrib(json_dict, self, 'reason', 'reason') 47 | return json_dict 48 | 49 | def __eq__(self, other): 50 | """Override the default __eq__ method. 51 | 52 | Returns 53 | ------- 54 | Bool 55 | Returns True if two class instances has same values at all properties, 56 | and returns False otherwise. 57 | """ 58 | if not isinstance(other, WaitForExternalEventAction): 59 | return False 60 | else: 61 | return self.action_type == other.action_type \ 62 | and self.external_event_name == other.external_event_name \ 63 | and self.reason == other.reason 64 | -------------------------------------------------------------------------------- /azure/durable_functions/models/actions/WhenAllAction.py: -------------------------------------------------------------------------------- 1 | from .ActionType import ActionType 2 | from azure.durable_functions.models.actions.CompoundAction import CompoundAction 3 | 4 | 5 | class WhenAllAction(CompoundAction): 6 | """Defines the structure of the WhenAll Action object. 7 | 8 | Provides the information needed by the durable extension to be able to invoke WhenAll tasks. 9 | """ 10 | 11 | @property 12 | def action_type(self) -> int: 13 | """Get the type of action this class represents.""" 14 | return ActionType.WHEN_ALL 15 | -------------------------------------------------------------------------------- /azure/durable_functions/models/actions/WhenAnyAction.py: -------------------------------------------------------------------------------- 1 | from azure.durable_functions.models.actions.CompoundAction import CompoundAction 2 | from .ActionType import ActionType 3 | 4 | 5 | class WhenAnyAction(CompoundAction): 6 | """Defines the structure of the WhenAll Action object. 7 | 8 | Provides the information needed by the durable extension to be able to invoke WhenAll tasks. 9 | """ 10 | 11 | @property 12 | def action_type(self) -> int: 13 | """Get the type of action this class represents.""" 14 | return ActionType.WHEN_ANY 15 | -------------------------------------------------------------------------------- /azure/durable_functions/models/actions/__init__.py: -------------------------------------------------------------------------------- 1 | """Defines the models for the different forms of Activities that can be scheduled.""" 2 | from .Action import Action 3 | from .ActionType import ActionType 4 | from .CallActivityAction import CallActivityAction 5 | from .CallActivityWithRetryAction import CallActivityWithRetryAction 6 | from .CallSubOrchestratorAction import CallSubOrchestratorAction 7 | from .WaitForExternalEventAction import WaitForExternalEventAction 8 | from .CallHttpAction import CallHttpAction 9 | from .CreateTimerAction import CreateTimerAction 10 | from .WhenAllAction import WhenAllAction 11 | from .WhenAnyAction import WhenAnyAction 12 | 13 | __all__ = [ 14 | 'Action', 15 | 'ActionType', 16 | 'CallActivityAction', 17 | 'CallActivityWithRetryAction', 18 | 'CallSubOrchestratorAction', 19 | 'CallHttpAction', 20 | 'WaitForExternalEventAction', 21 | 'CreateTimerAction', 22 | 'WhenAnyAction', 23 | 'WhenAllAction' 24 | ] 25 | -------------------------------------------------------------------------------- /azure/durable_functions/models/entities/EntityState.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional, Dict, Any 2 | from .Signal import Signal 3 | from azure.functions._durable_functions import _serialize_custom_object 4 | from .OperationResult import OperationResult 5 | import json 6 | 7 | 8 | class EntityState: 9 | """Entity State. 10 | 11 | Used to communicate the state of the entity back to the durable extension 12 | """ 13 | 14 | def __init__(self, 15 | results: List[OperationResult], 16 | signals: List[Signal], 17 | entity_exists: bool = False, 18 | state: Optional[str] = None): 19 | self.entity_exists = entity_exists 20 | self.state = state 21 | self._results = results 22 | self._signals = signals 23 | 24 | @property 25 | def results(self) -> List[OperationResult]: 26 | """Get list of results of the entity. 27 | 28 | Returns 29 | ------- 30 | List[OperationResult]: 31 | The results of the entity 32 | """ 33 | return self._results 34 | 35 | @property 36 | def signals(self) -> List[Signal]: 37 | """Get list of signals to the entity. 38 | 39 | Returns 40 | ------- 41 | List[Signal]: 42 | The signals of the entity 43 | """ 44 | return self._signals 45 | 46 | def to_json(self) -> Dict[str, Any]: 47 | """Convert object into a json dictionary. 48 | 49 | Returns 50 | ------- 51 | Dict[str, Any] 52 | The instance of the class converted into a json dictionary 53 | """ 54 | json_dict: Dict[str, Any] = {} 55 | # Serialize the OperationResult list 56 | serialized_results = list(map(lambda x: x.to_json(), self.results)) 57 | 58 | json_dict["entityExists"] = self.entity_exists 59 | json_dict["entityState"] = json.dumps(self.state, default=_serialize_custom_object) 60 | json_dict["results"] = serialized_results 61 | json_dict["signals"] = self.signals 62 | return json_dict 63 | 64 | def to_json_string(self) -> str: 65 | """Convert object into a json string. 66 | 67 | Returns 68 | ------- 69 | str 70 | The instance of the object in json string format 71 | """ 72 | # TODO: Same implementation as in Orchestrator.py, we should refactor to shared a base 73 | json_dict = self.to_json() 74 | return json.dumps(json_dict) 75 | -------------------------------------------------------------------------------- /azure/durable_functions/models/entities/OperationResult.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Dict, Any 2 | from azure.functions._durable_functions import _serialize_custom_object 3 | import json 4 | 5 | 6 | class OperationResult: 7 | """OperationResult. 8 | 9 | The result of an Entity operation. 10 | """ 11 | 12 | def __init__(self, 13 | is_error: bool, 14 | duration: int, 15 | result: Optional[str] = None): 16 | """Instantiate an OperationResult. 17 | 18 | Parameters 19 | ---------- 20 | is_error: bool 21 | Whether or not the operation resulted in an exception. 22 | duration: int 23 | How long the operation took, in milliseconds. 24 | result: Optional[str] 25 | The operation result. Defaults to None. 26 | """ 27 | self._is_error: bool = is_error 28 | self._duration: int = duration 29 | self._result: Optional[str] = result 30 | 31 | @property 32 | def is_error(self) -> bool: 33 | """Determine if the operation resulted in an error. 34 | 35 | Returns 36 | ------- 37 | bool 38 | True if the operation resulted in error. Otherwise False. 39 | """ 40 | return self._is_error 41 | 42 | @property 43 | def duration(self) -> int: 44 | """Get the duration of this operation. 45 | 46 | Returns 47 | ------- 48 | int: 49 | The duration of this operation, in milliseconds 50 | """ 51 | return self._duration 52 | 53 | @property 54 | def result(self) -> Any: 55 | """Get the operation's result. 56 | 57 | Returns 58 | ------- 59 | Any 60 | The operation's result 61 | """ 62 | return self._result 63 | 64 | def to_json(self) -> Dict[str, Any]: 65 | """Represent OperationResult as a JSON-serializable Dict. 66 | 67 | Returns 68 | ------- 69 | Dict[str, Any] 70 | A JSON-serializable Dict of the OperationResult 71 | """ 72 | to_json: Dict[str, Any] = {} 73 | to_json["isError"] = self.is_error 74 | to_json["duration"] = self.duration 75 | to_json["result"] = json.dumps(self.result, default=_serialize_custom_object) 76 | return to_json 77 | -------------------------------------------------------------------------------- /azure/durable_functions/models/entities/RequestMessage.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional, Any 2 | from ..utils.entity_utils import EntityId 3 | import json 4 | 5 | 6 | class RequestMessage: 7 | """RequestMessage. 8 | 9 | Specifies a request to an entity. 10 | """ 11 | 12 | def __init__(self, 13 | id_: str, 14 | name: Optional[str] = None, 15 | signal: Optional[bool] = None, 16 | input_: Optional[str] = None, 17 | arg: Optional[Any] = None, 18 | parent: Optional[str] = None, 19 | lockset: Optional[List[EntityId]] = None, 20 | pos: Optional[int] = None, 21 | **kwargs): 22 | # TODO: this class has too many optionals, may speak to 23 | # over-caution, but it mimics the JS class. Investigate if 24 | # these many Optionals are necessary. 25 | self.id = id_ 26 | self.name = name 27 | self.signal = signal 28 | self.input = input_ 29 | self.arg = arg 30 | self.parent = parent 31 | self.lockset = lockset 32 | self.pos = pos 33 | 34 | @classmethod 35 | def from_json(cls, json_str: str) -> 'RequestMessage': 36 | """Instantiate a RequestMessage object from the durable-extension provided JSON data. 37 | 38 | Parameters 39 | ---------- 40 | json_str: str 41 | A durable-extension provided json-formatted string representation of 42 | a RequestMessage 43 | 44 | Returns 45 | ------- 46 | RequestMessage: 47 | A RequestMessage object from the json_str parameter 48 | """ 49 | # We replace the `id` key for `id_` to avoid clashes with reserved 50 | # identifiers in Python 51 | json_dict = json.loads(json_str) 52 | json_dict["id_"] = json_dict.pop("id") 53 | return cls(**json_dict) 54 | -------------------------------------------------------------------------------- /azure/durable_functions/models/entities/ResponseMessage.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Any 2 | import json 3 | 4 | 5 | class ResponseMessage: 6 | """ResponseMessage. 7 | 8 | Specifies the response of an entity, as processed by the durable-extension. 9 | """ 10 | 11 | def __init__(self, result: str, is_exception: bool = False): 12 | """Instantiate a ResponseMessage. 13 | 14 | Specifies the response of an entity, as processed by the durable-extension. 15 | 16 | Parameters 17 | ---------- 18 | result: str 19 | The result provided by the entity 20 | """ 21 | # The time-out case seems to be handled by the Functions-Host, so 22 | # its result is not doubly-serialized. In this branch, we compensate 23 | # for this by re-serializing the payload. 24 | if result.strip().startswith("Timeout value of"): 25 | is_exception = True 26 | result = json.dumps(result) 27 | 28 | self.result = result 29 | self.is_exception = is_exception 30 | # TODO: JS has an additional exceptionType field, but does not use it 31 | 32 | @classmethod 33 | def from_dict(cls, d: Dict[str, Any]) -> 'ResponseMessage': 34 | """Instantiate a ResponseMessage from a dict of the JSON-response by the extension. 35 | 36 | Parameters 37 | ---------- 38 | d: Dict[str, Any] 39 | The dictionary parsed from the JSON-response by the durable-extension 40 | 41 | Returns 42 | ------- 43 | ResponseMessage: 44 | The ResponseMessage built from the provided dictionary 45 | """ 46 | is_error = "exceptionType" in d.keys() 47 | result = cls(d["result"], is_error) 48 | return result 49 | -------------------------------------------------------------------------------- /azure/durable_functions/models/entities/Signal.py: -------------------------------------------------------------------------------- 1 | from ..utils.entity_utils import EntityId 2 | 3 | 4 | class Signal: 5 | """An EntitySignal. 6 | 7 | Describes a signal call to a Durable Entity. 8 | """ 9 | 10 | def __init__(self, 11 | target: EntityId, 12 | name: str, 13 | input_: str): 14 | """Instantiate an EntitySignal. 15 | 16 | Instantiate a signal call to a Durable Entity. 17 | 18 | Parameters 19 | ---------- 20 | target: EntityId 21 | The target of signal 22 | name: str 23 | The name of the signal 24 | input_: str 25 | The signal's input 26 | """ 27 | self._target = target 28 | self._name = name 29 | self._input = input_ 30 | 31 | @property 32 | def target(self) -> EntityId: 33 | """Get the Signal's target entity. 34 | 35 | Returns 36 | ------- 37 | EntityId 38 | EntityId of the target 39 | """ 40 | return self._target 41 | 42 | @property 43 | def name(self) -> str: 44 | """Get the Signal's name. 45 | 46 | Returns 47 | ------- 48 | str 49 | The Signal's name 50 | """ 51 | return self._name 52 | 53 | @property 54 | def input(self) -> str: 55 | """Get the Signal's input. 56 | 57 | Returns 58 | ------- 59 | str 60 | The Signal's input 61 | """ 62 | return self._input 63 | -------------------------------------------------------------------------------- /azure/durable_functions/models/entities/__init__.py: -------------------------------------------------------------------------------- 1 | """Utility classes used by the Durable Function python library for dealing with entities. 2 | 3 | _Internal Only_ 4 | """ 5 | 6 | from .RequestMessage import RequestMessage 7 | from .OperationResult import OperationResult 8 | from .EntityState import EntityState 9 | from .Signal import Signal 10 | 11 | 12 | __all__ = [ 13 | 'RequestMessage', 14 | 'OperationResult', 15 | 'Signal', 16 | 'EntityState' 17 | ] 18 | -------------------------------------------------------------------------------- /azure/durable_functions/models/history/HistoryEventType.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | 3 | 4 | class HistoryEventType(IntEnum): 5 | """Defines the different types of history events being communicated.""" 6 | 7 | EXECUTION_STARTED = 0 8 | EXECUTION_COMPLETED = 1 9 | EXECUTION_FAILED = 2 10 | EXECUTION_TERMINATED = 3 11 | TASK_SCHEDULED = 4 12 | TASK_COMPLETED = 5 13 | TASK_FAILED = 6 14 | SUB_ORCHESTRATION_INSTANCE_CREATED = 7 15 | SUB_ORCHESTRATION_INSTANCE_COMPLETED = 8 16 | SUB_ORCHESTRATION_INSTANCE_FAILED = 9 17 | TIMER_CREATED = 10 18 | TIMER_FIRED = 11 19 | ORCHESTRATOR_STARTED = 12 20 | ORCHESTRATOR_COMPLETED = 13 21 | EVENT_SENT = 14 22 | EVENT_RAISED = 15 23 | CONTINUE_AS_NEW = 16 24 | GENERIC_EVENT = 17 25 | HISTORY_STATE = 18 26 | EXECUTION_SUSPENDED = 19 27 | EXECUTION_RESUMED = 20 28 | -------------------------------------------------------------------------------- /azure/durable_functions/models/history/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains models related to the orchestration history of the durable functions.""" 2 | from .HistoryEvent import HistoryEvent 3 | from .HistoryEventType import HistoryEventType 4 | 5 | __all__ = [ 6 | 'HistoryEvent', 7 | 'HistoryEventType' 8 | ] 9 | -------------------------------------------------------------------------------- /azure/durable_functions/models/utils/__init__.py: -------------------------------------------------------------------------------- 1 | """Utility functions used by the Durable Function python library. 2 | 3 | _Internal Only_ 4 | """ 5 | from pkgutil import extend_path 6 | import typing 7 | __path__: typing.Iterable[str] = extend_path(__path__, __name__) # type: ignore 8 | -------------------------------------------------------------------------------- /azure/durable_functions/testing/OrchestratorGeneratorWrapper.py: -------------------------------------------------------------------------------- 1 | from typing import Generator, Any, Union 2 | 3 | from azure.durable_functions.models import TaskBase 4 | 5 | 6 | def orchestrator_generator_wrapper( 7 | generator: Generator[TaskBase, Any, Any]) -> Generator[Union[TaskBase, Any], None, None]: 8 | """Wrap a user-defined orchestrator function in a way that simulates the Durable replay logic. 9 | 10 | Parameters 11 | ---------- 12 | generator: Generator[TaskBase, Any, Any] 13 | Generator orchestrator as defined in the user function app. This generator is expected 14 | to yield a series of TaskBase objects and receive the results of these tasks until 15 | returning the result of the orchestrator. 16 | 17 | Returns 18 | ------- 19 | Generator[Union[TaskBase, Any], None, None] 20 | A simplified version of the orchestrator which takes no inputs. This generator will 21 | yield back the TaskBase objects that are yielded from the user orchestrator as well 22 | as the final result of the orchestrator. Exception handling is also simulated here 23 | in the same way as replay, where tasks returning exceptions are thrown back into the 24 | orchestrator. 25 | """ 26 | previous = next(generator) 27 | yield previous 28 | while True: 29 | try: 30 | previous_result = None 31 | try: 32 | previous_result = previous.result 33 | except Exception as e: 34 | # Simulated activity exceptions, timer interrupted exceptions, 35 | # or anytime a task would throw. 36 | previous = generator.throw(e) 37 | else: 38 | previous = generator.send(previous_result) 39 | yield previous 40 | except StopIteration as e: 41 | yield e.value 42 | return 43 | -------------------------------------------------------------------------------- /azure/durable_functions/testing/__init__.py: -------------------------------------------------------------------------------- 1 | """Unit testing utilities for Azure Durable functions.""" 2 | from .OrchestratorGeneratorWrapper import orchestrator_generator_wrapper 3 | 4 | __all__ = [ 5 | 'orchestrator_generator_wrapper' 6 | ] 7 | -------------------------------------------------------------------------------- /eng/ci/code-mirror.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | branches: 3 | include: 4 | # These are the branches we'll mirror to our internal ADO instance 5 | # Keep this set limited as appropriate (don't mirror individual user branches). 6 | - main 7 | - dev 8 | 9 | resources: 10 | repositories: 11 | - repository: eng 12 | type: git 13 | name: engineering 14 | ref: refs/tags/release 15 | 16 | variables: 17 | - template: ci/variables/cfs.yml@eng 18 | 19 | extends: 20 | template: ci/code-mirror.yml@eng 21 | -------------------------------------------------------------------------------- /eng/ci/official-build.yml: -------------------------------------------------------------------------------- 1 | variables: 2 | - template: ci/variables/cfs.yml@eng 3 | 4 | trigger: 5 | batch: true 6 | branches: 7 | include: 8 | - main 9 | - dev 10 | 11 | # CI only, does not trigger on PRs. 12 | pr: none 13 | 14 | schedules: 15 | # Build nightly to catch any new CVEs and report SDL often. 16 | # We are also required to generated CodeQL reports weekly, so this 17 | # helps us meet that. 18 | - cron: "0 0 * * *" 19 | displayName: Nightly Build 20 | branches: 21 | include: 22 | - main 23 | - dev 24 | always: true 25 | 26 | resources: 27 | repositories: 28 | - repository: 1es 29 | type: git 30 | name: 1ESPipelineTemplates/1ESPipelineTemplates 31 | ref: refs/tags/release 32 | - repository: eng 33 | type: git 34 | name: engineering 35 | ref: refs/tags/release 36 | 37 | extends: 38 | template: v1/1ES.Official.PipelineTemplate.yml@1es 39 | parameters: 40 | pool: 41 | name: 1es-pool-azfunc 42 | image: 1es-windows-2022 43 | os: windows 44 | 45 | stages: 46 | - stage: BuildAndSign 47 | dependsOn: [] 48 | jobs: 49 | - template: /eng/templates/build.yml@self -------------------------------------------------------------------------------- /eng/templates/build.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | - job: Build 3 | 4 | templateContext: 5 | outputs: 6 | - output: pipelineArtifact 7 | path: $(build.artifactStagingDirectory) 8 | artifact: drop 9 | sbomBuildDropPath: $(System.DefaultWorkingDirectory) 10 | sbomPackageName: 'Durable Functions Python' 11 | 12 | steps: 13 | - task: UsePythonVersion@0 14 | inputs: 15 | versionSpec: '3.7.x' 16 | - script: | 17 | python -m pip install --upgrade pip 18 | pip install -r requirements.txt 19 | pip install wheel 20 | workingDirectory: $(System.DefaultWorkingDirectory) 21 | displayName: 'Install dependencies' 22 | 23 | - script: | 24 | cd azure 25 | flake8 . --count --show-source --statistics 26 | displayName: 'Run lint test with flake8' 27 | 28 | - script: | 29 | pip install pytest pytest-azurepipelines 30 | pytest 31 | displayName: 'pytest' 32 | - script: | 33 | python setup.py sdist bdist_wheel 34 | workingDirectory: $(System.DefaultWorkingDirectory) 35 | displayName: 'Building' 36 | 37 | - task: CopyFiles@2 38 | displayName: 'Copy publish file to Artifact Staging Directory' 39 | inputs: 40 | SourceFolder: dist 41 | Contents: '**' 42 | TargetFolder: $(Build.ArtifactStagingDirectory) -------------------------------------------------------------------------------- /host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "extensionBundle": { 4 | "id": "Microsoft.Azure.Functions.ExtensionBundle", 5 | "version": "[1.*, 2.0.0)" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /noxfile.py: -------------------------------------------------------------------------------- 1 | import nox 2 | 3 | @nox.session(python=["3.7","3.8"]) 4 | def tests(session): 5 | # same as pip install -r -requirements.txt 6 | session.install("-r", "requirements.txt") 7 | session.install("pytest") 8 | session.run("pytest", "-v", "tests") 9 | 10 | 11 | @nox.session(python=["3.7", "3.8"]) 12 | def lint(session): 13 | session.install("flake8") 14 | session.install("flake8-docstrings") 15 | session.run("flake8", "./azure/") 16 | 17 | @nox.session(python=["3.7", "3.8"]) 18 | def typecheck(session): 19 | session.install("-r", "requirements.txt") 20 | session.install("mypy") 21 | session.run("mypy", "./azure/") 22 | 23 | @nox.session(python=["3.7", "3.8"]) 24 | def autopep(session): 25 | session.install("-r", "requirements.txt") 26 | session.run("autopep8", "--in-place --aggressive --aggressive --recursive \"./azure/\"") -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | flake8==3.7.8 2 | flake8-docstrings==1.5.0 3 | pytest==7.1.2 4 | python-dateutil==2.8.0 5 | requests==2.32.2 6 | jsonschema==3.2.0 7 | aiohttp==3.12.9 8 | azure-functions>=1.11.3b3 9 | nox==2019.11.9 10 | furl==2.1.0 11 | pytest-asyncio==0.20.2 12 | autopep8 13 | types-python-dateutil 14 | opentelemetry-api==1.32.1 15 | opentelemetry-sdk==1.32.1 -------------------------------------------------------------------------------- /samples-v2/blueprint/.funcignore: -------------------------------------------------------------------------------- 1 | .git* 2 | .vscode 3 | __azurite_db*__.json 4 | __blobstorage__ 5 | __queuestorage__ 6 | local.settings.json 7 | test 8 | .venv -------------------------------------------------------------------------------- /samples-v2/blueprint/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # IPython 79 | profile_default/ 80 | ipython_config.py 81 | 82 | # pyenv 83 | .python-version 84 | 85 | # pipenv 86 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 87 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 88 | # having no cross-platform support, pipenv may install dependencies that don’t work, or not 89 | # install all needed dependencies. 90 | #Pipfile.lock 91 | 92 | # celery beat schedule file 93 | celerybeat-schedule 94 | 95 | # SageMath parsed files 96 | *.sage.py 97 | 98 | # Environments 99 | .env 100 | .venv 101 | env/ 102 | venv/ 103 | ENV/ 104 | env.bak/ 105 | venv.bak/ 106 | 107 | # Spyder project settings 108 | .spyderproject 109 | .spyproject 110 | 111 | # Rope project settings 112 | .ropeproject 113 | 114 | # mkdocs documentation 115 | /site 116 | 117 | # mypy 118 | .mypy_cache/ 119 | .dmypy.json 120 | dmypy.json 121 | 122 | # Pyre type checker 123 | .pyre/ 124 | 125 | # Azure Functions artifacts 126 | bin 127 | obj 128 | appsettings.json 129 | local.settings.json 130 | 131 | # Azurite artifacts 132 | __blobstorage__ 133 | __queuestorage__ 134 | __azurite_db*__.json 135 | .python_packages -------------------------------------------------------------------------------- /samples-v2/blueprint/durable_blueprints.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import azure.functions as func 3 | import azure.durable_functions as df 4 | 5 | # To learn more about blueprints in the Python prog model V2, 6 | # see: https://learn.microsoft.com/en-us/azure/azure-functions/functions-reference-python?tabs=asgi%2Capplication-level&pivots=python-mode-decorators#blueprints 7 | 8 | # Note, the `func` namespace does not contain Durable Functions triggers and bindings, so to register blueprints of 9 | # DF we need to use the `df` package's version of blueprints. 10 | bp = df.Blueprint() 11 | 12 | # We define a standard function-chaining DF pattern 13 | 14 | @bp.route(route="startOrchestrator") 15 | @bp.durable_client_input(client_name="client") 16 | async def start_orchestrator(req: func.HttpRequest, client): 17 | instance_id = await client.start_new("my_orchestrator") 18 | 19 | logging.info(f"Started orchestration with ID = '{instance_id}'.") 20 | return client.create_check_status_response(req, instance_id) 21 | 22 | @bp.orchestration_trigger(context_name="context") 23 | def my_orchestrator(context: df.DurableOrchestrationContext): 24 | result1 = yield context.call_activity('say_hello', "Tokyo") 25 | result2 = yield context.call_activity('say_hello', "Seattle") 26 | result3 = yield context.call_activity('say_hello', "London") 27 | return [result1, result2, result3] 28 | 29 | @bp.activity_trigger(input_name="city") 30 | def say_hello(city: str) -> str: 31 | return f"Hello {city}!" -------------------------------------------------------------------------------- /samples-v2/blueprint/function_app.py: -------------------------------------------------------------------------------- 1 | import azure.functions as func 2 | import logging 3 | 4 | from durable_blueprints import bp 5 | 6 | app = func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION) 7 | app.register_functions(bp) # register the DF functions 8 | 9 | # Define a simple HTTP trigger function, to show that you can also 10 | # register functions via the `app` object 11 | @app.route(route="HttpTrigger") 12 | def HttpTrigger(req: func.HttpRequest) -> func.HttpResponse: 13 | logging.info('Python HTTP trigger function processed a request.') 14 | 15 | name = req.params.get('name') 16 | if not name: 17 | try: 18 | req_body = req.get_json() 19 | except ValueError: 20 | pass 21 | else: 22 | name = req_body.get('name') 23 | 24 | if name: 25 | return func.HttpResponse(f"Hello, {name}. This HTTP triggered function executed successfully.") 26 | else: 27 | return func.HttpResponse( 28 | "This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response.", 29 | status_code=200 30 | ) -------------------------------------------------------------------------------- /samples-v2/blueprint/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "logging": { 4 | "applicationInsights": { 5 | "samplingSettings": { 6 | "isEnabled": true, 7 | "excludedTypes": "Request" 8 | } 9 | } 10 | }, 11 | "extensionBundle": { 12 | "id": "Microsoft.Azure.Functions.ExtensionBundle", 13 | "version": "[4.*, 5.0.0)" 14 | } 15 | } -------------------------------------------------------------------------------- /samples-v2/blueprint/requirements.txt: -------------------------------------------------------------------------------- 1 | # DO NOT include azure-functions-worker in this file 2 | # The Python Worker is managed by Azure Functions platform 3 | # Manually managing azure-functions-worker may cause unexpected issues 4 | 5 | azure-functions 6 | azure-functions-durable>=1.2.4 7 | pytest -------------------------------------------------------------------------------- /samples-v2/blueprint/tests/test_my_orchestrator.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import Mock, call, patch 3 | from azure.durable_functions.testing import orchestrator_generator_wrapper 4 | 5 | from durable_blueprints import my_orchestrator 6 | 7 | @patch('azure.durable_functions.models.TaskBase') 8 | def mock_activity(activity_name, input, task): 9 | if activity_name == "say_hello": 10 | task.result = f"Hello {input}!" 11 | return task 12 | raise Exception("Activity not found") 13 | 14 | 15 | class TestFunction(unittest.TestCase): 16 | @patch('azure.durable_functions.DurableOrchestrationContext') 17 | def test_my_orchestrator(self, context): 18 | # Get the original method definition as seen in the function_app.py file 19 | func_call = my_orchestrator.build().get_user_function().orchestrator_function 20 | 21 | context.call_activity = Mock(side_effect=mock_activity) 22 | # Create a generator using the method and mocked context 23 | user_orchestrator = func_call(context) 24 | 25 | # Use orchestrator_generator_wrapper to get the values from the generator. 26 | # Processes the orchestrator in a way that is equivalent to the Durable replay logic 27 | values = [val for val in orchestrator_generator_wrapper(user_orchestrator)] 28 | 29 | expected_activity_calls = [call('say_hello', 'Tokyo'), 30 | call('say_hello', 'Seattle'), 31 | call('say_hello', 'London')] 32 | 33 | self.assertEqual(context.call_activity.call_count, 3) 34 | self.assertEqual(context.call_activity.call_args_list, expected_activity_calls) 35 | self.assertEqual(values[3], ["Hello Tokyo!", "Hello Seattle!", "Hello London!"]) 36 | -------------------------------------------------------------------------------- /samples-v2/blueprint/tests/test_say_hello.py: -------------------------------------------------------------------------------- 1 | # Activity functions require no special implementation aside from standard Azure Functions 2 | # unit testing for Python. As such, no test is implemented here. 3 | # For more information about testing Azure Functions in Python, see the official documentation: 4 | # https://learn.microsoft.com/en-us/azure/azure-functions/functions-reference-python#unit-testing -------------------------------------------------------------------------------- /samples-v2/blueprint/tests/test_start_orchestrator.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import unittest 3 | import azure.functions as func 4 | from unittest.mock import AsyncMock, Mock, patch 5 | 6 | from durable_blueprints import start_orchestrator 7 | 8 | class TestFunction(unittest.TestCase): 9 | @patch('azure.durable_functions.DurableOrchestrationClient') 10 | def test_HttpStart(self, client): 11 | # Get the original method definition as seen in the function_app.py file 12 | func_call = start_orchestrator.build().get_user_function().client_function 13 | 14 | req = func.HttpRequest(method='GET', 15 | body=b'{}', 16 | url='/api/my_second_function') 17 | 18 | client.start_new = AsyncMock(return_value="instance_id") 19 | client.create_check_status_response = Mock(return_value="check_status_response") 20 | 21 | # Execute the function code 22 | result = asyncio.run(func_call(req, client)) 23 | 24 | client.start_new.assert_called_once_with("my_orchestrator") 25 | client.create_check_status_response.assert_called_once_with(req, "instance_id") 26 | self.assertEqual(result, "check_status_response") 27 | -------------------------------------------------------------------------------- /samples-v2/fan_in_fan_out/.funcignore: -------------------------------------------------------------------------------- 1 | .git* 2 | .vscode 3 | local.settings.json 4 | test 5 | .venv -------------------------------------------------------------------------------- /samples-v2/fan_in_fan_out/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # IPython 79 | profile_default/ 80 | ipython_config.py 81 | 82 | # pyenv 83 | .python-version 84 | 85 | # pipenv 86 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 87 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 88 | # having no cross-platform support, pipenv may install dependencies that don’t work, or not 89 | # install all needed dependencies. 90 | #Pipfile.lock 91 | 92 | # celery beat schedule file 93 | celerybeat-schedule 94 | 95 | # SageMath parsed files 96 | *.sage.py 97 | 98 | # Environments 99 | .env 100 | .venv 101 | env/ 102 | venv/ 103 | ENV/ 104 | env.bak/ 105 | venv.bak/ 106 | 107 | # Spyder project settings 108 | .spyderproject 109 | .spyproject 110 | 111 | # Rope project settings 112 | .ropeproject 113 | 114 | # mkdocs documentation 115 | /site 116 | 117 | # mypy 118 | .mypy_cache/ 119 | .dmypy.json 120 | dmypy.json 121 | 122 | # Pyre type checker 123 | .pyre/ 124 | 125 | # Azure Functions artifacts 126 | bin 127 | obj 128 | appsettings.json 129 | local.settings.json 130 | .python_packages -------------------------------------------------------------------------------- /samples-v2/fan_in_fan_out/README.md: -------------------------------------------------------------------------------- 1 | # Fan-Out Fan-In 2 | 3 | This directory contains an executable version of [this](https://docs.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-cloud-backup?tabs=python) tutorial. Please review the link above for instructions on how to run it. -------------------------------------------------------------------------------- /samples-v2/fan_in_fan_out/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "logging": { 4 | "applicationInsights": { 5 | "samplingSettings": { 6 | "isEnabled": true, 7 | "excludedTypes": "Request" 8 | } 9 | } 10 | }, 11 | "extensionBundle": { 12 | "id": "Microsoft.Azure.Functions.ExtensionBundle", 13 | "version": "[4.*, 5.0.0)" 14 | } 15 | } -------------------------------------------------------------------------------- /samples-v2/fan_in_fan_out/proxies.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/proxies", 3 | "proxies": {} 4 | } 5 | -------------------------------------------------------------------------------- /samples-v2/fan_in_fan_out/requirements.txt: -------------------------------------------------------------------------------- 1 | # DO NOT include azure-functions-worker in this file 2 | # The Python Worker is managed by Azure Functions platform 3 | # Manually managing azure-functions-worker may cause unexpected issues 4 | 5 | azure-functions 6 | azure-functions-durable 7 | azure-storage-blob 8 | pytest -------------------------------------------------------------------------------- /samples-v2/fan_in_fan_out/tests/test_E2_BackupSiteContent.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import Mock, call, patch 3 | from azure.durable_functions.testing import orchestrator_generator_wrapper 4 | 5 | from function_app import E2_BackupSiteContent 6 | 7 | 8 | @patch('azure.durable_functions.models.TaskBase') 9 | def create_mock_task(result, task): 10 | task.result = result 11 | return task 12 | 13 | 14 | def mock_activity(activity_name, input): 15 | if activity_name == "E2_GetFileList": 16 | return create_mock_task(["C:/test/E2_Activity.py", "C:/test/E2_Orchestrator.py"]) 17 | elif activity_name == "E2_CopyFileToBlob": 18 | return create_mock_task(1) 19 | raise Exception("Activity not found") 20 | 21 | 22 | def mock_task_all(tasks): 23 | return create_mock_task([t.result for t in tasks]) 24 | 25 | 26 | class TestFunction(unittest.TestCase): 27 | @patch('azure.durable_functions.DurableOrchestrationContext') 28 | def test_E2_BackupSiteContent(self, context): 29 | # Get the original method definition as seen in the function_app.py file 30 | func_call = E2_BackupSiteContent.build().get_user_function().orchestrator_function 31 | 32 | context.get_input = Mock(return_value="C:/test") 33 | context.call_activity = Mock(side_effect=mock_activity) 34 | context.task_all = Mock(side_effect=mock_task_all) 35 | 36 | # Execute the function code 37 | user_orchestrator = func_call(context) 38 | 39 | # Use orchestrator_generator_wrapper to get the values from the generator. 40 | # Processes the orchestrator in a way that is equivalent to the Durable replay logic 41 | values = [val for val in orchestrator_generator_wrapper(user_orchestrator)] 42 | 43 | expected_activity_calls = [call('E2_GetFileList', 'C:/test'), 44 | call('E2_CopyFileToBlob', 'C:/test/E2_Activity.py'), 45 | call('E2_CopyFileToBlob', 'C:/test/E2_Orchestrator.py')] 46 | 47 | self.assertEqual(context.call_activity.call_count, 3) 48 | self.assertEqual(context.call_activity.call_args_list, expected_activity_calls) 49 | 50 | context.task_all.assert_called_once() 51 | # Sums the result of task_all 52 | self.assertEqual(values[2], 2) 53 | -------------------------------------------------------------------------------- /samples-v2/fan_in_fan_out/tests/test_E2_CopyFileToBlob.py: -------------------------------------------------------------------------------- 1 | # Activity functions require no special implementation aside from standard Azure Functions 2 | # unit testing for Python. As such, no test is implemented here. 3 | # For more information about testing Azure Functions in Python, see the official documentation: 4 | # https://learn.microsoft.com/en-us/azure/azure-functions/functions-reference-python#unit-testing -------------------------------------------------------------------------------- /samples-v2/fan_in_fan_out/tests/test_E2_GetFileList.py: -------------------------------------------------------------------------------- 1 | # Activity functions require no special implementation aside from standard Azure Functions 2 | # unit testing for Python. As such, no test is implemented here. 3 | # For more information about testing Azure Functions in Python, see the official documentation: 4 | # https://learn.microsoft.com/en-us/azure/azure-functions/functions-reference-python#unit-testing -------------------------------------------------------------------------------- /samples-v2/fan_in_fan_out/tests/test_HttpStart.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import unittest 3 | import azure.functions as func 4 | from unittest.mock import AsyncMock, Mock, patch 5 | 6 | from function_app import HttpStart 7 | 8 | class TestFunction(unittest.TestCase): 9 | @patch('azure.durable_functions.DurableOrchestrationClient') 10 | def test_HttpStart(self, client): 11 | # Get the original method definition as seen in the function_app.py file 12 | func_call = HttpStart.build().get_user_function().client_function 13 | 14 | req = func.HttpRequest(method='GET', 15 | body=b'{}', 16 | url='/api/my_second_function', 17 | route_params={"functionName": "E2_BackupSiteContent"}) 18 | 19 | client.start_new = AsyncMock(return_value="instance_id") 20 | client.create_check_status_response = Mock(return_value="check_status_response") 21 | 22 | # Execute the function code 23 | result = asyncio.run(func_call(req, client)) 24 | 25 | client.start_new.assert_called_once_with("E2_BackupSiteContent", client_input={}) 26 | client.create_check_status_response.assert_called_once_with(req, "instance_id") 27 | self.assertEqual(result, "check_status_response") 28 | -------------------------------------------------------------------------------- /samples-v2/function_chaining/.funcignore: -------------------------------------------------------------------------------- 1 | .git* 2 | .vscode 3 | local.settings.json 4 | test 5 | .venv -------------------------------------------------------------------------------- /samples-v2/function_chaining/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # IPython 79 | profile_default/ 80 | ipython_config.py 81 | 82 | # pyenv 83 | .python-version 84 | 85 | # pipenv 86 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 87 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 88 | # having no cross-platform support, pipenv may install dependencies that don’t work, or not 89 | # install all needed dependencies. 90 | #Pipfile.lock 91 | 92 | # celery beat schedule file 93 | celerybeat-schedule 94 | 95 | # SageMath parsed files 96 | *.sage.py 97 | 98 | # Environments 99 | .env 100 | .venv 101 | env/ 102 | venv/ 103 | ENV/ 104 | env.bak/ 105 | venv.bak/ 106 | 107 | # Spyder project settings 108 | .spyderproject 109 | .spyproject 110 | 111 | # Rope project settings 112 | .ropeproject 113 | 114 | # mkdocs documentation 115 | /site 116 | 117 | # mypy 118 | .mypy_cache/ 119 | .dmypy.json 120 | dmypy.json 121 | 122 | # Pyre type checker 123 | .pyre/ 124 | 125 | # Azure Functions artifacts 126 | bin 127 | obj 128 | appsettings.json 129 | local.settings.json 130 | .python_packages -------------------------------------------------------------------------------- /samples-v2/function_chaining/README.md: -------------------------------------------------------------------------------- 1 | # Function Chaining 2 | 3 | This directory contains an executable version of [this](https://docs.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-sequence?tabs=python) tutorial. Please review the link above for instructions on how to run it. -------------------------------------------------------------------------------- /samples-v2/function_chaining/function_app.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import azure.functions as func 3 | import azure.durable_functions as df 4 | 5 | myApp = df.DFApp(http_auth_level=func.AuthLevel.ANONYMOUS) 6 | 7 | @myApp.route(route="orchestrators/{functionName}") 8 | @myApp.durable_client_input(client_name="client") 9 | async def http_start(req: func.HttpRequest, client): 10 | function_name = req.route_params.get('functionName') 11 | instance_id = await client.start_new(function_name) 12 | 13 | logging.info(f"Started orchestration with ID = '{instance_id}'.") 14 | return client.create_check_status_response(req, instance_id) 15 | 16 | @myApp.orchestration_trigger(context_name="context") 17 | def my_orchestrator(context: df.DurableOrchestrationContext): 18 | result1 = yield context.call_activity('say_hello', "Tokyo") 19 | result2 = yield context.call_activity('say_hello', "Seattle") 20 | result3 = yield context.call_activity('say_hello', "London") 21 | return [result1, result2, result3] 22 | 23 | @myApp.activity_trigger(input_name="city") 24 | def say_hello(city: str) -> str: 25 | return f"Hello {city}!" -------------------------------------------------------------------------------- /samples-v2/function_chaining/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "logging": { 4 | "applicationInsights": { 5 | "samplingSettings": { 6 | "isEnabled": true, 7 | "excludedTypes": "Request" 8 | } 9 | } 10 | }, 11 | "extensionBundle": { 12 | "id": "Microsoft.Azure.Functions.ExtensionBundle", 13 | "version": "[4.*, 5.0.0)" 14 | } 15 | } -------------------------------------------------------------------------------- /samples-v2/function_chaining/proxies.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/proxies", 3 | "proxies": {} 4 | } 5 | -------------------------------------------------------------------------------- /samples-v2/function_chaining/requirements.txt: -------------------------------------------------------------------------------- 1 | # DO NOT include azure-functions-worker in this file 2 | # The Python Worker is managed by Azure Functions platform 3 | # Manually managing azure-functions-worker may cause unexpected issues 4 | 5 | azure-functions 6 | azure-functions-durable 7 | pytest 8 | -------------------------------------------------------------------------------- /samples-v2/function_chaining/tests/test_http_start.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import unittest 3 | import azure.functions as func 4 | from unittest.mock import AsyncMock, Mock, patch 5 | 6 | from function_app import http_start 7 | 8 | class TestFunction(unittest.TestCase): 9 | @patch('azure.durable_functions.DurableOrchestrationClient') 10 | def test_HttpStart(self, client): 11 | # Get the original method definition as seen in the function_app.py file 12 | func_call = http_start.build().get_user_function().client_function 13 | 14 | req = func.HttpRequest(method='GET', 15 | body=b'{}', 16 | url='/api/my_second_function', 17 | route_params={"functionName": "my_orchestrator"}) 18 | 19 | client.start_new = AsyncMock(return_value="instance_id") 20 | client.create_check_status_response = Mock(return_value="check_status_response") 21 | 22 | # Execute the function code 23 | result = asyncio.run(func_call(req, client)) 24 | 25 | client.start_new.assert_called_once_with("my_orchestrator") 26 | client.create_check_status_response.assert_called_once_with(req, "instance_id") 27 | self.assertEqual(result, "check_status_response") 28 | -------------------------------------------------------------------------------- /samples-v2/function_chaining/tests/test_my_orchestrator.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import Mock, call, patch 3 | from azure.durable_functions.testing import orchestrator_generator_wrapper 4 | 5 | from function_app import my_orchestrator 6 | 7 | 8 | @patch('azure.durable_functions.models.TaskBase') 9 | def mock_activity(activity_name, input, task): 10 | if activity_name == "say_hello": 11 | task.result = f"Hello {input}!" 12 | return task 13 | raise Exception("Activity not found") 14 | 15 | 16 | class TestFunction(unittest.TestCase): 17 | @patch('azure.durable_functions.DurableOrchestrationContext') 18 | def test_chaining_orchestrator(self, context): 19 | # Get the original method definition as seen in the function_app.py file 20 | func_call = my_orchestrator.build().get_user_function().orchestrator_function 21 | 22 | context.call_activity = Mock(side_effect=mock_activity) 23 | 24 | # Create a generator using the method and mocked context 25 | user_orchestrator = func_call(context) 26 | 27 | # Use orchestrator_generator_wrapper to get the values from the generator. 28 | # Processes the orchestrator in a way that is equivalent to the Durable replay logic 29 | values = [val for val in orchestrator_generator_wrapper(user_orchestrator)] 30 | 31 | expected_activity_calls = [call('say_hello', 'Tokyo'), 32 | call('say_hello', 'Seattle'), 33 | call('say_hello', 'London')] 34 | 35 | self.assertEqual(context.call_activity.call_count, 3) 36 | self.assertEqual(context.call_activity.call_args_list, expected_activity_calls) 37 | self.assertEqual(values[3], ["Hello Tokyo!", "Hello Seattle!", "Hello London!"]) 38 | -------------------------------------------------------------------------------- /samples-v2/function_chaining/tests/test_say_hello.py: -------------------------------------------------------------------------------- 1 | # Activity functions require no special implementation aside from standard Azure Functions 2 | # unit testing for Python. As such, no test is implemented here. 3 | # For more information about testing Azure Functions in Python, see the official documentation: 4 | # https://learn.microsoft.com/en-us/azure/azure-functions/functions-reference-python#unit-testing -------------------------------------------------------------------------------- /samples/aml_monitoring/.funcignore: -------------------------------------------------------------------------------- 1 | .git* 2 | .vscode 3 | local.settings.json 4 | test 5 | py36 -------------------------------------------------------------------------------- /samples/aml_monitoring/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # IPython 79 | profile_default/ 80 | ipython_config.py 81 | 82 | # pyenv 83 | .python-version 84 | 85 | # pipenv 86 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 87 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 88 | # having no cross-platform support, pipenv may install dependencies that don’t work, or not 89 | # install all needed dependencies. 90 | #Pipfile.lock 91 | 92 | # celery beat schedule file 93 | celerybeat-schedule 94 | 95 | # SageMath parsed files 96 | *.sage.py 97 | 98 | # Environments 99 | .env 100 | .venv 101 | env/ 102 | venv/ 103 | ENV/ 104 | env.bak/ 105 | venv.bak/ 106 | 107 | # Spyder project settings 108 | .spyderproject 109 | .spyproject 110 | 111 | # Rope project settings 112 | .ropeproject 113 | 114 | # mkdocs documentation 115 | /site 116 | 117 | # mypy 118 | .mypy_cache/ 119 | .dmypy.json 120 | dmypy.json 121 | 122 | # Pyre type checker 123 | .pyre/ 124 | 125 | # Azure Functions artifacts 126 | bin 127 | obj 128 | appsettings.json 129 | .python_packages 130 | 131 | # pycharm 132 | .idea 133 | -------------------------------------------------------------------------------- /samples/aml_monitoring/aml_durable_orchestrator/__init__.py: -------------------------------------------------------------------------------- 1 | import logging,json 2 | import azure.durable_functions as df 3 | from datetime import datetime,timedelta 4 | 5 | def orchestrator_fn(context: df.DurableOrchestrationContext): 6 | pipeline_endpoint = "" 7 | experiment_name = "" 8 | 9 | # Step 1: Kickoff the AML pipeline 10 | input_args= {} 11 | input_args["pipeline_endpoint"] = pipeline_endpoint 12 | input_args["experiment_name"] = experiment_name 13 | input_args["params"] = None 14 | run_id = yield context.call_activity("aml_pipeline",input_args) 15 | polling_interval = 60 16 | expiry_time = context.current_utc_datetime + timedelta(minutes=30) 17 | 18 | # Consider continueAsNew - use this in the samples 19 | # while loop explodes the history table on high scale 20 | while context.current_utc_datetime < expiry_time: 21 | 22 | # Step 2: Poll the status of the pipeline 23 | poll_args = {} 24 | poll_args["run_id"] = run_id 25 | poll_args["experiment_name"] = experiment_name 26 | job_status = yield context.call_activity("aml_poll_status",poll_args) 27 | 28 | # Use native Dictionary fix the generic binding conversion in worker. Can it return a Dict? 29 | activity_status = json.loads(job_status) 30 | if activity_status["status_code"] == 202: 31 | next_check = context.current_utc_datetime + timedelta(minutes=1) 32 | 33 | # Set intermediate status for anyone who wants to poll this durable function 34 | context.set_custom_status(activity_status) 35 | 36 | yield context.create_timer(next_check) 37 | 38 | elif activity_status["status_code"] == 500: 39 | job_completed = True 40 | raise Exception("AML Job Failed/Cancelled...") 41 | else: 42 | job_completed = True 43 | return activity_status 44 | 45 | main = df.Orchestrator.create(orchestrator_fn) 46 | -------------------------------------------------------------------------------- /samples/aml_monitoring/aml_durable_orchestrator/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "__init__.py", 3 | "bindings": [ 4 | { 5 | "name": "context", 6 | "type": "orchestrationTrigger", 7 | "direction": "in" 8 | } 9 | ], 10 | "disabled": false 11 | } 12 | -------------------------------------------------------------------------------- /samples/aml_monitoring/aml_pipeline/__init__.py: -------------------------------------------------------------------------------- 1 | import logging,json 2 | import os 3 | import time 4 | from typing import Dict 5 | import requests 6 | import azure.functions as func 7 | from azureml.core.authentication import ServicePrincipalAuthentication 8 | 9 | from ..shared.auth_helper import get_access_token 10 | 11 | 12 | def trigger_aml_endpoint(pipeline_endpoint, experiment_name, parameter_body, retries=3): 13 | aad_token = get_access_token() 14 | response = requests.post( 15 | pipeline_endpoint, 16 | headers=aad_token, 17 | json={"ExperimentName": experiment_name, 18 | "ParameterAssignments": parameter_body}) 19 | 20 | if response.status_code == 200: 21 | success = True 22 | 23 | return json.loads(response.content) 24 | 25 | # explicitly typing input_args causes exception 26 | def main(name): 27 | input_args = json.loads(name) 28 | try: 29 | response = trigger_aml_endpoint(input_args["pipeline_endpoint"], input_args["experiment_name"], input_args["params"]) 30 | except Exception as exception: 31 | logging.error("Got exception: ", exc_info=True) 32 | return exception 33 | return response["Id"] 34 | -------------------------------------------------------------------------------- /samples/aml_monitoring/aml_pipeline/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "__init__.py", 3 | "bindings": [ 4 | { 5 | "name": "name", 6 | "type": "activityTrigger", 7 | "direction": "in", 8 | "datatype": "string" 9 | } 10 | ], 11 | "disabled": false 12 | } -------------------------------------------------------------------------------- /samples/aml_monitoring/aml_poll_status/__init__.py: -------------------------------------------------------------------------------- 1 | import logging,json 2 | import os 3 | import time 4 | from typing import Dict 5 | import azure.functions as func 6 | import requests 7 | from azureml.core import Experiment, Workspace 8 | from azureml.core.authentication import ServicePrincipalAuthentication 9 | from azureml.pipeline.core import PipelineRun 10 | 11 | from ..shared.aml_helper import get_run_url_from_env, get_run_logs 12 | from ..shared.auth_helper import get_service_principal_auth 13 | 14 | _SUBSCRIPTION_ID_ENV_NAME = "SubscriptionId" 15 | _RESOURCE_GROUP_NAME_ENV_NAME = "ResourceGroupName" 16 | _AML_WORKSPACE_NAME_ENV_NAME = "AMLWorkspaceName" 17 | 18 | 19 | def get_aml_pipeline_run_status(run_id, experiment_name, retries=3): 20 | 21 | svc_pr = get_service_principal_auth() 22 | workspace = Workspace( 23 | subscription_id=os.environ[_SUBSCRIPTION_ID_ENV_NAME], 24 | resource_group=os.environ[_RESOURCE_GROUP_NAME_ENV_NAME], 25 | workspace_name=os.environ[_AML_WORKSPACE_NAME_ENV_NAME], 26 | auth=svc_pr) 27 | 28 | experiment = Experiment(workspace, experiment_name) 29 | pipeline_run = PipelineRun(experiment, run_id) 30 | 31 | response = pipeline_run.get_status() 32 | return response 33 | 34 | 35 | def main(name): 36 | input_args = json.loads(name) 37 | run_id = input_args["run_id"] 38 | experiment_name = input_args["experiment_name"] 39 | status = get_aml_pipeline_run_status(run_id,experiment_name) 40 | run_url = get_run_url_from_env(run_id,experiment_name) 41 | run_logs = get_run_logs(run_id,experiment_name) 42 | status_code_map = {"Finished":200,"Failed":500,"Cancelled":500} 43 | 44 | response_obj = { 45 | "status" : status, 46 | "url" : run_url, 47 | "logs" : run_logs, 48 | "status_code": status_code_map[status] if status in status_code_map else 202 49 | } 50 | return json.dumps(response_obj) 51 | -------------------------------------------------------------------------------- /samples/aml_monitoring/aml_poll_status/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "__init__.py", 3 | "bindings": [ 4 | { 5 | "name": "name", 6 | "type": "activityTrigger", 7 | "direction": "in", 8 | "datatype": "string" 9 | } 10 | ], 11 | "disabled": false 12 | } 13 | -------------------------------------------------------------------------------- /samples/aml_monitoring/extensions.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netstandard2.0 4 | 5 | ** 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /samples/aml_monitoring/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0" 3 | } -------------------------------------------------------------------------------- /samples/aml_monitoring/local.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "IsEncrypted": false, 3 | "Values": { 4 | "AzureWebJobsStorage": "", 5 | "FUNCTIONS_WORKER_RUNTIME": "python" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /samples/aml_monitoring/proxies.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/proxies", 3 | "proxies": {} 4 | } 5 | -------------------------------------------------------------------------------- /samples/aml_monitoring/requirements.txt: -------------------------------------------------------------------------------- 1 | azure-functions 2 | azure-functions-durable>=1.0.0b5 3 | azureml-sdk>=1.0.45 4 | -------------------------------------------------------------------------------- /samples/aml_monitoring/shared/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/azure-functions-durable-python/246e66e42136f132914cbf2f70c9276686bbf22b/samples/aml_monitoring/shared/__init__.py -------------------------------------------------------------------------------- /samples/aml_monitoring/shared/auth_helper.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | import time 4 | 5 | from azureml.core.authentication import ServicePrincipalAuthentication 6 | 7 | _TENANT_ID_ENV_NAME = "TenantId" 8 | _SERVICE_PRINCIPAL_ID_ENV_NAME = "ServicePrincipalId" 9 | _SERVICE_PRINCIPAL_SECRET_ENV_NAME = "ServicePrincipalSecret" 10 | 11 | 12 | def get_service_principal_auth(): 13 | tenant_id = os.environ[_TENANT_ID_ENV_NAME] 14 | service_principal_id = os.environ[_SERVICE_PRINCIPAL_ID_ENV_NAME] 15 | service_principal_password = os.environ[_SERVICE_PRINCIPAL_SECRET_ENV_NAME] 16 | 17 | svc_pr = ServicePrincipalAuthentication( 18 | tenant_id=tenant_id, 19 | service_principal_id=service_principal_id, 20 | service_principal_password=service_principal_password) 21 | 22 | return svc_pr 23 | 24 | 25 | def get_access_token(): 26 | start_time = time.time() 27 | 28 | svc_pr = get_service_principal_auth() 29 | aad_token = svc_pr.get_authentication_header() 30 | 31 | end_time = time.time() 32 | 33 | logging.info('Get Access Token Time: %s seconds', end_time - start_time) 34 | return aad_token 35 | -------------------------------------------------------------------------------- /samples/counter_entity/.funcignore: -------------------------------------------------------------------------------- 1 | venv -------------------------------------------------------------------------------- /samples/counter_entity/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # IPython 79 | profile_default/ 80 | ipython_config.py 81 | 82 | # pyenv 83 | .python-version 84 | 85 | # pipenv 86 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 87 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 88 | # having no cross-platform support, pipenv may install dependencies that don’t work, or not 89 | # install all needed dependencies. 90 | #Pipfile.lock 91 | 92 | # celery beat schedule file 93 | celerybeat-schedule 94 | 95 | # SageMath parsed files 96 | *.sage.py 97 | 98 | # Environments 99 | .env 100 | .venv 101 | env/ 102 | venv/ 103 | ENV/ 104 | env.bak/ 105 | venv.bak/ 106 | 107 | # Spyder project settings 108 | .spyderproject 109 | .spyproject 110 | 111 | # Rope project settings 112 | .ropeproject 113 | 114 | # mkdocs documentation 115 | /site 116 | 117 | # mypy 118 | .mypy_cache/ 119 | .dmypy.json 120 | dmypy.json 121 | 122 | # Pyre type checker 123 | .pyre/ 124 | 125 | # Azure Functions artifacts 126 | bin 127 | obj 128 | appsettings.json 129 | .python_packages 130 | 131 | # pycharm 132 | .idea 133 | -------------------------------------------------------------------------------- /samples/counter_entity/Counter/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import json 3 | 4 | import azure.functions as func 5 | import azure.durable_functions as df 6 | 7 | 8 | def entity_function(context: df.DurableEntityContext): 9 | """A Counter Durable Entity. 10 | 11 | A simple example of a Durable Entity that implements 12 | a simple counter. 13 | 14 | Parameters 15 | ---------- 16 | context (df.DurableEntityContext): 17 | The Durable Entity context, which exports an API 18 | for implementing durable entities. 19 | """ 20 | 21 | current_value = context.get_state(lambda: 0) 22 | operation = context.operation_name 23 | if operation == "add": 24 | amount = context.get_input() 25 | current_value += amount 26 | elif operation == "reset": 27 | current_value = 0 28 | elif operation == "get": 29 | pass 30 | 31 | context.set_state(current_value) 32 | context.set_result(current_value) 33 | 34 | 35 | main = df.Entity.create(entity_function) -------------------------------------------------------------------------------- /samples/counter_entity/Counter/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "__init__.py", 3 | "bindings": [ 4 | { 5 | "name": "context", 6 | "type": "entityTrigger", 7 | "direction": "in" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /samples/counter_entity/DurableOrchestration/__init__.py: -------------------------------------------------------------------------------- 1 | # This function is not intended to be invoked directly. Instead it will be 2 | # triggered by an HTTP starter function. 3 | # Before running this sample, please: 4 | # - create a Durable activity function (default name is "Hello") 5 | # - create a Durable HTTP starter function 6 | # - add azure-functions-durable to requirements.txt 7 | # - run pip install -r requirements.txt 8 | 9 | import logging 10 | import json 11 | 12 | import azure.functions as func 13 | import azure.durable_functions as df 14 | 15 | 16 | def orchestrator_function(context: df.DurableOrchestrationContext): 17 | """This function provides the a simple implementation of an orchestrator 18 | that signals and then calls a counter Durable Entity. 19 | 20 | Parameters 21 | ---------- 22 | context: DurableOrchestrationContext 23 | This context has the past history and the durable orchestration API 24 | 25 | Returns 26 | ------- 27 | state 28 | The state after applying the operation on the Durable Entity 29 | """ 30 | entityId = df.EntityId("Counter", "myCounter") 31 | context.signal_entity(entityId, "add", 3) 32 | state = yield context.call_entity(entityId, "get") 33 | return state 34 | 35 | main = df.Orchestrator.create(orchestrator_function) -------------------------------------------------------------------------------- /samples/counter_entity/DurableOrchestration/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "__init__.py", 3 | "bindings": [ 4 | { 5 | "name": "context", 6 | "type": "orchestrationTrigger", 7 | "direction": "in" 8 | } 9 | ], 10 | "disabled": false 11 | } 12 | -------------------------------------------------------------------------------- /samples/counter_entity/DurableTrigger/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from azure.durable_functions import DurableOrchestrationClient 4 | import azure.functions as func 5 | 6 | 7 | async def main(req: func.HttpRequest, starter: str, message): 8 | """This function starts up the orchestrator from an HTTP endpoint 9 | 10 | starter: str 11 | A JSON-formatted string describing the orchestration context 12 | 13 | message: 14 | An azure functions http output binding, it enables us to establish 15 | an http response. 16 | 17 | Parameters 18 | ---------- 19 | req: func.HttpRequest 20 | An HTTP Request object, it can be used to parse URL 21 | parameters. 22 | """ 23 | 24 | 25 | function_name = req.route_params.get('functionName') 26 | logging.info(starter) 27 | client = DurableOrchestrationClient(starter) 28 | instance_id = await client.start_new(function_name) 29 | response = client.create_check_status_response(req, instance_id) 30 | message.set(response) 31 | -------------------------------------------------------------------------------- /samples/counter_entity/DurableTrigger/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "__init__.py", 3 | "bindings": [ 4 | { 5 | "authLevel": "anonymous", 6 | "name": "req", 7 | "type": "httpTrigger", 8 | "direction": "in", 9 | "route": "orchestrators/{functionName}", 10 | "methods": [ 11 | "post", 12 | "get" 13 | ] 14 | }, 15 | { 16 | "direction": "out", 17 | "name": "message", 18 | "type": "http" 19 | }, 20 | { 21 | "name": "starter", 22 | "type": "durableClient", 23 | "direction": "in", 24 | "datatype": "string" 25 | } 26 | ] 27 | } -------------------------------------------------------------------------------- /samples/counter_entity/README.md: -------------------------------------------------------------------------------- 1 | # Durable Entities - Sample 2 | 3 | This sample exemplifies how to go about using the [Durable Entities](https://docs.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-entities?tabs=csharp) construct in Python Durable Functions. 4 | 5 | ## Usage Instructions 6 | 7 | ### Create a `local.settings.json` file in this directory 8 | This file stores app settings, connection strings, and other settings used by local development tools. Learn more about it [here](https://docs.microsoft.com/en-us/azure/azure-functions/functions-run-local?tabs=windows%2Ccsharp%2Cbash#local-settings-file). 9 | For this sample, you will only need an `AzureWebJobsStorage` connection string, which you can obtain from the Azure portal. 10 | 11 | With you connection string, your `local.settings.json` file should look as follows, with `` replaced with the connection string you obtained from the Azure portal: 12 | 13 | ```json 14 | { 15 | "IsEncrypted": false, 16 | "Values": { 17 | "AzureWebJobsStorage": "", 18 | "FUNCTIONS_WORKER_RUNTIME": "python" 19 | } 20 | } 21 | ``` 22 | 23 | ### Run the Sample 24 | To try this sample, run `func host start` in this directory. If all the system requirements have been met, and 25 | after some initialization logs, you should see something like the following: 26 | 27 | ```bash 28 | Http Functions: 29 | 30 | DurableTrigger: [POST,GET] http://localhost:7071/api/orchestrators/{functionName} 31 | ``` 32 | 33 | This indicates that your `DurableTrigger` function can be reached via a `GET` or `POST` request to that URL. `DurableTrigger` starts the function-chaning orchestrator whose name is passed as a parameter to the URL. So, to start the orchestrator, which is named `DurableOrchestration`, make a GET request to `http://127.0.0.1:7071/api/orchestrators/DurableOrchestration`. 34 | 35 | And that's it! You should see a JSON response with five URLs to monitor the status of the orchestration. 36 | 37 | ### Retrieving the state via the DurableOrchestrationClient 38 | It is possible to retrieve the state of an entity using the `read_entity_state` function. As an example we have the `RetrieveEntity` endpoint which will return the current state of the entity: 39 | 40 | ```bash 41 | Http Functions: 42 | 43 | RetrieveEntity: [GET] http://localhost:7071/api/entity/{entityName}/{entityKey} 44 | ``` 45 | 46 | For our example a call to `http://localhost:7071/api/entity/Counter/myCounter` will return the current state of our counter. -------------------------------------------------------------------------------- /samples/counter_entity/RetrieveEntity/__init__.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | from typing import Any, Dict, Union, cast 4 | 5 | import azure.functions as func 6 | from azure.durable_functions import DurableOrchestrationClient 7 | from azure.durable_functions.models.utils.entity_utils import EntityId 8 | 9 | 10 | async def main(req: func.HttpRequest, starter: str) -> func.HttpResponse: 11 | client = DurableOrchestrationClient(starter) 12 | entity_name, entity_key = req.route_params["entityName"], req.route_params["entityKey"] 13 | 14 | entity_identifier = EntityId(entity_name, entity_key) 15 | 16 | entity_state_response = await client.read_entity_state(entity_identifier) 17 | 18 | if not entity_state_response.entity_exists: 19 | return func.HttpResponse("Entity not found", status_code=404) 20 | 21 | return func.HttpResponse(json.dumps({ 22 | "entity": entity_name, 23 | "key": entity_key, 24 | "state": entity_state_response.entity_state 25 | })) 26 | -------------------------------------------------------------------------------- /samples/counter_entity/RetrieveEntity/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "__init__.py", 3 | "bindings": [ 4 | { 5 | "authLevel": "function", 6 | "name": "req", 7 | "type": "httpTrigger", 8 | "direction": "in", 9 | "route": "entity/{entityName}/{entityKey}", 10 | "methods": [ 11 | "get" 12 | ] 13 | }, 14 | { 15 | "name": "$return", 16 | "type": "http", 17 | "direction": "out" 18 | }, 19 | { 20 | "name": "starter", 21 | "type": "durableClient", 22 | "direction": "in" 23 | } 24 | ] 25 | } -------------------------------------------------------------------------------- /samples/counter_entity/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "logging": { 4 | "applicationInsights": { 5 | "samplingSettings": { 6 | "isEnabled": true, 7 | "excludedTypes": "Request" 8 | } 9 | } 10 | }, 11 | "extensionBundle": { 12 | "id": "Microsoft.Azure.Functions.ExtensionBundle", 13 | "version": "[2.*, 3.0.0)" 14 | } 15 | } -------------------------------------------------------------------------------- /samples/counter_entity/local.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "IsEncrypted": false, 3 | "Values": { 4 | "AzureWebJobsStorage": "UseDevelopmentStorage=true", 5 | "FUNCTIONS_WORKER_RUNTIME": "python" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /samples/counter_entity/requirements.txt: -------------------------------------------------------------------------------- 1 | azure-functions 2 | azure-functions-durable -------------------------------------------------------------------------------- /samples/fan_in_fan_out/.funcignore: -------------------------------------------------------------------------------- 1 | .git* 2 | .vscode 3 | local.settings.json 4 | test 5 | .venv -------------------------------------------------------------------------------- /samples/fan_in_fan_out/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # IPython 79 | profile_default/ 80 | ipython_config.py 81 | 82 | # pyenv 83 | .python-version 84 | 85 | # pipenv 86 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 87 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 88 | # having no cross-platform support, pipenv may install dependencies that don’t work, or not 89 | # install all needed dependencies. 90 | #Pipfile.lock 91 | 92 | # celery beat schedule file 93 | celerybeat-schedule 94 | 95 | # SageMath parsed files 96 | *.sage.py 97 | 98 | # Environments 99 | .env 100 | .venv 101 | env/ 102 | venv/ 103 | ENV/ 104 | env.bak/ 105 | venv.bak/ 106 | 107 | # Spyder project settings 108 | .spyderproject 109 | .spyproject 110 | 111 | # Rope project settings 112 | .ropeproject 113 | 114 | # mkdocs documentation 115 | /site 116 | 117 | # mypy 118 | .mypy_cache/ 119 | .dmypy.json 120 | dmypy.json 121 | 122 | # Pyre type checker 123 | .pyre/ 124 | 125 | # Azure Functions artifacts 126 | bin 127 | obj 128 | appsettings.json 129 | local.settings.json 130 | .python_packages -------------------------------------------------------------------------------- /samples/fan_in_fan_out/E2_BackupSiteContent/__init__.py: -------------------------------------------------------------------------------- 1 | import azure.functions as func 2 | import azure.durable_functions as df 3 | 4 | 5 | def orchestrator_function(context: df.DurableOrchestrationContext): 6 | 7 | root_directory: str = context.get_input() 8 | 9 | if not root_directory: 10 | raise Exception("A directory path is required as input") 11 | 12 | files = yield context.call_activity("E2_GetFileList", root_directory) 13 | tasks = [] 14 | for file in files: 15 | tasks.append(context.call_activity("E2_CopyFileToBlob", file)) 16 | 17 | results = yield context.task_all(tasks) 18 | total_bytes = sum(results) 19 | return total_bytes 20 | 21 | main = df.Orchestrator.create(orchestrator_function) -------------------------------------------------------------------------------- /samples/fan_in_fan_out/E2_BackupSiteContent/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "__init__.py", 3 | "bindings": [ 4 | { 5 | "name": "context", 6 | "type": "orchestrationTrigger", 7 | "direction": "in" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /samples/fan_in_fan_out/E2_CopyFileToBlob/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pathlib 3 | from azure.storage.blob import BlobServiceClient 4 | from azure.core.exceptions import ResourceExistsError 5 | 6 | connect_str = os.getenv('AzureWebJobsStorage') 7 | 8 | def main(filePath: str) -> str: 9 | # Create the BlobServiceClient object which will be used to create a container client 10 | blob_service_client = BlobServiceClient.from_connection_string(connect_str) 11 | 12 | # Create a unique name for the container 13 | container_name = "backups" 14 | 15 | # Create the container if it does not exist 16 | try: 17 | blob_service_client.create_container(container_name) 18 | except ResourceExistsError: 19 | pass 20 | 21 | # Create a blob client using the local file name as the name for the blob 22 | parent_dir, fname = pathlib.Path(filePath).parts[-2:] # Get last two path components 23 | blob_name = parent_dir + "_" + fname 24 | blob_client = blob_service_client.get_blob_client(container=container_name, blob=blob_name) 25 | 26 | # Count bytes in file 27 | byte_count = os.path.getsize(filePath) 28 | 29 | # Upload the created file 30 | with open(filePath, "rb") as data: 31 | blob_client.upload_blob(data) 32 | 33 | return byte_count 34 | -------------------------------------------------------------------------------- /samples/fan_in_fan_out/E2_CopyFileToBlob/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "__init__.py", 3 | "bindings": [ 4 | { 5 | "name": "filePath", 6 | "type": "activityTrigger", 7 | "direction": "in" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /samples/fan_in_fan_out/E2_GetFileList/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | from os.path import dirname 3 | from typing import List 4 | 5 | def main(rootDirectory: str) -> List[str]: 6 | 7 | all_file_paths = [] 8 | # We walk the file system 9 | for path, _, files in os.walk(rootDirectory): 10 | # We copy the code for activities and orchestrators 11 | if "E2_" in path: 12 | # For each file, we add their full-path to the list 13 | for name in files: 14 | if name == "__init__.py" or name == "function.json": 15 | file_path = os.path.join(path, name) 16 | all_file_paths.append(file_path) 17 | 18 | return all_file_paths 19 | -------------------------------------------------------------------------------- /samples/fan_in_fan_out/E2_GetFileList/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "__init__.py", 3 | "bindings": [ 4 | { 5 | "name": "rootDirectory", 6 | "type": "activityTrigger", 7 | "direction": "in" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /samples/fan_in_fan_out/HttpStart/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import json 3 | import azure.functions as func 4 | import azure.durable_functions as df 5 | 6 | 7 | async def main(req: func.HttpRequest, starter: str) -> func.HttpResponse: 8 | client = df.DurableOrchestrationClient(starter) 9 | payload: str = json.loads(req.get_body().decode()) # Load JSON post request data 10 | instance_id = await client.start_new(req.route_params["functionName"], client_input=payload) 11 | 12 | logging.info(f"Started orchestration with ID = '{instance_id}'.") 13 | 14 | return client.create_check_status_response(req, instance_id) -------------------------------------------------------------------------------- /samples/fan_in_fan_out/HttpStart/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "__init__.py", 3 | "bindings": [ 4 | { 5 | "authLevel": "anonymous", 6 | "name": "req", 7 | "type": "httpTrigger", 8 | "direction": "in", 9 | "route": "orchestrators/{functionName}", 10 | "methods": [ 11 | "post", 12 | "get" 13 | ] 14 | }, 15 | { 16 | "name": "$return", 17 | "type": "http", 18 | "direction": "out" 19 | }, 20 | { 21 | "name": "starter", 22 | "type": "orchestrationClient", 23 | "direction": "in" 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /samples/fan_in_fan_out/README.md: -------------------------------------------------------------------------------- 1 | # Fan-Out Fan-In 2 | 3 | This directory contains a executable version of [this](https://docs.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-cloud-backup?tabs=python) tutorial. Please review the link above for instructions on how to run it. -------------------------------------------------------------------------------- /samples/fan_in_fan_out/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "logging": { 4 | "applicationInsights": { 5 | "samplingSettings": { 6 | "isEnabled": true, 7 | "excludedTypes": "Request" 8 | } 9 | } 10 | }, 11 | "extensionBundle": { 12 | "id": "Microsoft.Azure.Functions.ExtensionBundle", 13 | "version": "[2.*, 3.0.0)" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /samples/fan_in_fan_out/local.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "IsEncrypted": false, 3 | "Values": { 4 | "AzureWebJobsStorage": "", 5 | "FUNCTIONS_WORKER_RUNTIME": "python" 6 | } 7 | } -------------------------------------------------------------------------------- /samples/fan_in_fan_out/proxies.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/proxies", 3 | "proxies": {} 4 | } 5 | -------------------------------------------------------------------------------- /samples/fan_in_fan_out/requirements.txt: -------------------------------------------------------------------------------- 1 | # DO NOT include azure-functions-worker in this file 2 | # The Python Worker is managed by Azure Functions platform 3 | # Manually managing azure-functions-worker may cause unexpected issues 4 | 5 | azure-functions 6 | azure-functions-durable 7 | azure-storage-blob -------------------------------------------------------------------------------- /samples/function_chaining/.funcignore: -------------------------------------------------------------------------------- 1 | .git* 2 | .vscode 3 | local.settings.json 4 | test 5 | .venv -------------------------------------------------------------------------------- /samples/function_chaining/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # IPython 79 | profile_default/ 80 | ipython_config.py 81 | 82 | # pyenv 83 | .python-version 84 | 85 | # pipenv 86 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 87 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 88 | # having no cross-platform support, pipenv may install dependencies that don’t work, or not 89 | # install all needed dependencies. 90 | #Pipfile.lock 91 | 92 | # celery beat schedule file 93 | celerybeat-schedule 94 | 95 | # SageMath parsed files 96 | *.sage.py 97 | 98 | # Environments 99 | .env 100 | .venv 101 | env/ 102 | venv/ 103 | ENV/ 104 | env.bak/ 105 | venv.bak/ 106 | 107 | # Spyder project settings 108 | .spyderproject 109 | .spyproject 110 | 111 | # Rope project settings 112 | .ropeproject 113 | 114 | # mkdocs documentation 115 | /site 116 | 117 | # mypy 118 | .mypy_cache/ 119 | .dmypy.json 120 | dmypy.json 121 | 122 | # Pyre type checker 123 | .pyre/ 124 | 125 | # Azure Functions artifacts 126 | bin 127 | obj 128 | appsettings.json 129 | local.settings.json 130 | .python_packages -------------------------------------------------------------------------------- /samples/function_chaining/E1_HelloSequence/__init__.py: -------------------------------------------------------------------------------- 1 | import azure.functions as func 2 | import azure.durable_functions as df 3 | 4 | 5 | def orchestrator_function(context: df.DurableOrchestrationContext): 6 | result1 = yield context.call_activity('E1_SayHello', "Tokyo") 7 | result2 = yield context.call_activity('E1_SayHello', "Seattle") 8 | result3 = yield context.call_activity('E1_SayHello', "London") 9 | return [result1, result2, result3] 10 | 11 | main = df.Orchestrator.create(orchestrator_function) -------------------------------------------------------------------------------- /samples/function_chaining/E1_HelloSequence/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "__init__.py", 3 | "bindings": [ 4 | { 5 | "name": "context", 6 | "type": "orchestrationTrigger", 7 | "direction": "in" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /samples/function_chaining/E1_SayHello/__init__.py: -------------------------------------------------------------------------------- 1 | def main(name: str) -> str: 2 | return f"Hello {name}!" 3 | -------------------------------------------------------------------------------- /samples/function_chaining/E1_SayHello/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "__init__.py", 3 | "bindings": [ 4 | { 5 | "name": "name", 6 | "type": "activityTrigger", 7 | "direction": "in" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /samples/function_chaining/HttpStart/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import azure.functions as func 4 | import azure.durable_functions as df 5 | 6 | 7 | async def main(req: func.HttpRequest, starter: str) -> func.HttpResponse: 8 | client = df.DurableOrchestrationClient(starter) 9 | instance_id = await client.start_new(req.route_params["functionName"], None, None) 10 | 11 | logging.info(f"Started orchestration with ID = '{instance_id}'.") 12 | 13 | return client.create_check_status_response(req, instance_id) -------------------------------------------------------------------------------- /samples/function_chaining/HttpStart/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "__init__.py", 3 | "bindings": [ 4 | { 5 | "authLevel": "anonymous", 6 | "name": "req", 7 | "type": "httpTrigger", 8 | "direction": "in", 9 | "route": "orchestrators/{functionName}", 10 | "methods": [ 11 | "post", 12 | "get" 13 | ] 14 | }, 15 | { 16 | "name": "$return", 17 | "type": "http", 18 | "direction": "out" 19 | }, 20 | { 21 | "name": "starter", 22 | "type": "durableClient", 23 | "direction": "in" 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /samples/function_chaining/README.md: -------------------------------------------------------------------------------- 1 | # Function Chaining 2 | 3 | This directory contains a executable version of [this](https://docs.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-sequence?tabs=python) tutorial. Please review the link above for instructions on how to run it. -------------------------------------------------------------------------------- /samples/function_chaining/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "logging": { 4 | "applicationInsights": { 5 | "samplingSettings": { 6 | "isEnabled": true, 7 | "excludedTypes": "Request" 8 | } 9 | } 10 | }, 11 | "extensionBundle": { 12 | "id": "Microsoft.Azure.Functions.ExtensionBundle", 13 | "version": "[2.*, 3.0.0)" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /samples/function_chaining/local.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "IsEncrypted": false, 3 | "Values": { 4 | "AzureWebJobsStorage": "", 5 | "FUNCTIONS_WORKER_RUNTIME": "python" 6 | } 7 | } -------------------------------------------------------------------------------- /samples/function_chaining/proxies.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/proxies", 3 | "proxies": {} 4 | } 5 | -------------------------------------------------------------------------------- /samples/function_chaining/requirements.txt: -------------------------------------------------------------------------------- 1 | # DO NOT include azure-functions-worker in this file 2 | # The Python Worker is managed by Azure Functions platform 3 | # Manually managing azure-functions-worker may cause unexpected issues 4 | 5 | azure-functions 6 | azure-functions-durable 7 | -------------------------------------------------------------------------------- /samples/function_chaining_custom_status/.funcignore: -------------------------------------------------------------------------------- 1 | .git* 2 | .vscode 3 | local.settings.json 4 | test 5 | .venv 6 | -------------------------------------------------------------------------------- /samples/function_chaining_custom_status/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # IPython 79 | profile_default/ 80 | ipython_config.py 81 | 82 | # pyenv 83 | .python-version 84 | 85 | # pipenv 86 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 87 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 88 | # having no cross-platform support, pipenv may install dependencies that don’t work, or not 89 | # install all needed dependencies. 90 | #Pipfile.lock 91 | 92 | # celery beat schedule file 93 | celerybeat-schedule 94 | 95 | # SageMath parsed files 96 | *.sage.py 97 | 98 | # Environments 99 | .env 100 | .venv 101 | env/ 102 | venv/ 103 | ENV/ 104 | env.bak/ 105 | venv.bak/ 106 | 107 | # Spyder project settings 108 | .spyderproject 109 | .spyproject 110 | 111 | # Rope project settings 112 | .ropeproject 113 | 114 | # mkdocs documentation 115 | /site 116 | 117 | # mypy 118 | .mypy_cache/ 119 | .dmypy.json 120 | dmypy.json 121 | 122 | # Pyre type checker 123 | .pyre/ 124 | 125 | # Azure Functions artifacts 126 | bin 127 | obj 128 | appsettings.json 129 | .python_packages 130 | 131 | # pycharm 132 | .idea 133 | -------------------------------------------------------------------------------- /samples/function_chaining_custom_status/DurableActivity/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | def main(num: int) -> int: 4 | """Activity function performing a specific step in the chain 5 | 6 | Parameters 7 | ---------- 8 | num : int 9 | number whose value to increase by one 10 | 11 | Returns 12 | ------- 13 | int 14 | the input, plus one 15 | """ 16 | 17 | logging.info(f"Activity Triggered: {num}") 18 | return num + 1 19 | 20 | 21 | -------------------------------------------------------------------------------- /samples/function_chaining_custom_status/DurableActivity/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "__init__.py", 3 | "bindings": [ 4 | { 5 | "name": "num", 6 | "type": "activityTrigger", 7 | "direction": "in" 8 | } 9 | ], 10 | "disabled": false 11 | } -------------------------------------------------------------------------------- /samples/function_chaining_custom_status/DurableOrchestration/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import azure.functions as func 4 | import azure.durable_functions as df 5 | 6 | 7 | def orchestrator_function(context: df.DurableOrchestrationContext): 8 | """This function provides the core function chaining orchestration logic 9 | and also sets custom status using which a user can see intermittent status of 10 | the orchestration through get_status client URL. 11 | 12 | Parameters 13 | ---------- 14 | context: DurableOrchestrationContext 15 | This context has the past history 16 | and the durable orchestration API's to chain a set of functions 17 | 18 | Returns 19 | ------- 20 | final_result: str 21 | Returns the final result after the chain completes 22 | 23 | Yields 24 | ------- 25 | call_activity: str 26 | Yields at every step of the function chain orchestration logic 27 | """ 28 | 29 | # Chained functions - output of a function is passed as 30 | # input to the next function in the chain 31 | r1 = yield context.call_activity("DurableActivity", 0) 32 | context.set_custom_status(f'{r1} ->') 33 | r2 = yield context.call_activity("DurableActivity", r1) 34 | context.set_custom_status(f'{r1} -> {r2} ->') 35 | r3 = yield context.call_activity("DurableActivity", r2) 36 | context.set_custom_status(f'{r1} -> {r2} -> {r3}') 37 | return r3 38 | 39 | main = df.Orchestrator.create(orchestrator_function) 40 | -------------------------------------------------------------------------------- /samples/function_chaining_custom_status/DurableOrchestration/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "__init__.py", 3 | "bindings": [ 4 | { 5 | "name": "context", 6 | "type": "orchestrationTrigger", 7 | "direction": "in" 8 | } 9 | ], 10 | "disabled": false 11 | } 12 | -------------------------------------------------------------------------------- /samples/function_chaining_custom_status/DurableTrigger/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import time 3 | from azure.durable_functions import DurableOrchestrationClient 4 | import azure.functions as func 5 | 6 | 7 | async def main(req: func.HttpRequest, starter: str, message): 8 | """This function starts up the orchestrator from an HTTP endpoint 9 | 10 | starter: str 11 | A JSON-formatted string describing the orchestration context 12 | 13 | message: 14 | An azure functions http output binding, it enables us to establish 15 | an http response. 16 | 17 | Parameters 18 | ---------- 19 | req: func.HttpRequest 20 | An HTTP Request object, it can be used to parse URL 21 | parameters. 22 | """ 23 | 24 | 25 | function_name = req.route_params.get('functionName') 26 | logging.info(starter) 27 | client = DurableOrchestrationClient(starter) 28 | instance_id = await client.start_new(function_name) 29 | response = client.create_check_status_response(req, instance_id) 30 | message.set(response) 31 | -------------------------------------------------------------------------------- /samples/function_chaining_custom_status/DurableTrigger/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "__init__.py", 3 | "bindings": [ 4 | { 5 | "authLevel": "anonymous", 6 | "name": "req", 7 | "type": "httpTrigger", 8 | "direction": "in", 9 | "route": "orchestrators/{functionName}", 10 | "methods": [ 11 | "post", 12 | "get" 13 | ] 14 | }, 15 | { 16 | "direction": "out", 17 | "name": "message", 18 | "type": "http" 19 | }, 20 | { 21 | "name": "starter", 22 | "type": "durableClient", 23 | "direction": "in", 24 | "datatype": "string" 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /samples/function_chaining_custom_status/README.md: -------------------------------------------------------------------------------- 1 | # Function Chaining with Custom Status - Sample 2 | 3 | This sample demonstrates how to go about implementing the [Function Chaining](https://docs.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-overview?tabs=csharp#chaining) pattern in Python Durable Functions. 4 | 5 | It additionally demonstrates how to go about setting intermittent status while an orchestation is executing. This enables a user to monitor the status of the orchestration through a custom message set by the user. 6 | 7 | ## Usage Instructions 8 | 9 | ### Create a `local.settings.json` file in this directory 10 | This file stores app settings, connection strings, and other settings used by local development tools. Learn more about it [here](https://docs.microsoft.com/en-us/azure/azure-functions/functions-run-local?tabs=windows%2Ccsharp%2Cbash#local-settings-file). 11 | For this sample, you will only need an `AzureWebJobsStorage` connection string, which you can obtain from the Azure portal. 12 | 13 | With you connection string, your `local.settings.json` file should look as follows, with `` replaced with the connection string you obtained from the Azure portal: 14 | 15 | ```json 16 | { 17 | "IsEncrypted": false, 18 | "Values": { 19 | "AzureWebJobsStorage": "", 20 | "FUNCTIONS_WORKER_RUNTIME": "python" 21 | } 22 | } 23 | ``` 24 | 25 | ### Run the Sample 26 | To try this sample, run `func host start` in this directory. If all the system requirements have been met, and 27 | after some initialization logs, you should see something like the following: 28 | 29 | ```bash 30 | Http Functions: 31 | 32 | DurableTrigger: [POST,GET] http://localhost:7071/api/orchestrators/{functionName} 33 | ``` 34 | 35 | This indicates that your `DurableTrigger` function can be reached via a `GET` or `POST` request to that URL. `DurableTrigger` starts the function-chaning orchestrator whose name is passed as a parameter to the URL. So, to start the orchestrator, which is named `DurableOrchestration`, make a GET request to `http://127.0.0.1:7071/api/orchestrators/DurableOrchestration`. 36 | 37 | And that's it! You should see a JSON response with five URLs to monitor the status of the orchestration. To learn more about this, please read [here](TODO)! -------------------------------------------------------------------------------- /samples/function_chaining_custom_status/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "logging": { 4 | "applicationInsights": { 5 | "samplingSettings": { 6 | "isEnabled": true, 7 | "excludedTypes": "Request" 8 | } 9 | } 10 | }, 11 | "extensionBundle": { 12 | "id": "Microsoft.Azure.Functions.ExtensionBundle", 13 | "version": "[2.*, 3.0.0)" 14 | } 15 | } -------------------------------------------------------------------------------- /samples/function_chaining_custom_status/local.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "IsEncrypted": false, 3 | "Values": { 4 | "AzureWebJobsStorage": "UseDevelopmentStorage=true", 5 | "FUNCTIONS_WORKER_RUNTIME": "python" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /samples/function_chaining_custom_status/proxies.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/proxies", 3 | "proxies": {} 4 | } 5 | -------------------------------------------------------------------------------- /samples/function_chaining_custom_status/requirements.txt: -------------------------------------------------------------------------------- 1 | azure-functions 2 | azure-functions-durable -------------------------------------------------------------------------------- /samples/human_interaction/.funcignore: -------------------------------------------------------------------------------- 1 | .git* 2 | .vscode 3 | local.settings.json 4 | test 5 | .venv -------------------------------------------------------------------------------- /samples/human_interaction/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # IPython 79 | profile_default/ 80 | ipython_config.py 81 | 82 | # pyenv 83 | .python-version 84 | 85 | # pipenv 86 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 87 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 88 | # having no cross-platform support, pipenv may install dependencies that don’t work, or not 89 | # install all needed dependencies. 90 | #Pipfile.lock 91 | 92 | # celery beat schedule file 93 | celerybeat-schedule 94 | 95 | # SageMath parsed files 96 | *.sage.py 97 | 98 | # Environments 99 | .env 100 | .venv 101 | env/ 102 | venv/ 103 | ENV/ 104 | env.bak/ 105 | venv.bak/ 106 | 107 | # Spyder project settings 108 | .spyderproject 109 | .spyproject 110 | 111 | # Rope project settings 112 | .ropeproject 113 | 114 | # mkdocs documentation 115 | /site 116 | 117 | # mypy 118 | .mypy_cache/ 119 | .dmypy.json 120 | dmypy.json 121 | 122 | # Pyre type checker 123 | .pyre/ 124 | 125 | # Azure Functions artifacts 126 | bin 127 | obj 128 | appsettings.json 129 | local.settings.json 130 | .python_packages -------------------------------------------------------------------------------- /samples/human_interaction/E4_SMSPhoneVerification/__init__.py: -------------------------------------------------------------------------------- 1 | import azure.durable_functions as df 2 | from datetime import timedelta 3 | 4 | def is_valid_phone_number(phone_number: str): 5 | has_area_code = phone_number[0] == "+" 6 | is_positive_num = phone_number[1:].isdigit() 7 | return has_area_code and is_positive_num 8 | 9 | def orchestrator_function(context: df.DurableOrchestrationContext): 10 | 11 | phone_number = context.get_input() 12 | 13 | if (not phone_number) or (not is_valid_phone_number(phone_number)): 14 | msg = "Please provide a phone number beginning with an international dialing prefix"+\ 15 | "(+) followed by the country code, and then rest of the phone number. Example:"\ 16 | "'+1425XXXXXXX'" 17 | raise Exception(msg) 18 | 19 | challenge_code = yield context.call_activity("SendSMSChallenge", phone_number) 20 | 21 | expiration = context.current_utc_datetime + timedelta(seconds=180) 22 | timeout_task = context.create_timer(expiration) 23 | 24 | authorized = False 25 | for _ in range(3): 26 | challenge_response_task = context.wait_for_external_event("SmsChallengeResponse") 27 | winner = yield context.task_any([challenge_response_task, timeout_task]) 28 | 29 | if (winner == challenge_response_task): 30 | # We got back a response! Compare it to the challenge code 31 | if (challenge_response_task.result == challenge_code): 32 | authorized = True 33 | break 34 | else: 35 | # Timeout expired 36 | break 37 | 38 | if not timeout_task.is_completed: 39 | # All pending timers must be complete or canceled before the function exits. 40 | timeout_task.cancel() 41 | 42 | return authorized 43 | 44 | main = df.Orchestrator.create(orchestrator_function) -------------------------------------------------------------------------------- /samples/human_interaction/E4_SMSPhoneVerification/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "__init__.py", 3 | "bindings": [ 4 | { 5 | "name": "context", 6 | "type": "orchestrationTrigger", 7 | "direction": "in" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /samples/human_interaction/HttpStart/__init__.py: -------------------------------------------------------------------------------- 1 | # This function an HTTP starter function for Durable Functions. 2 | # Before running this sample, please: 3 | # - create a Durable orchestration function 4 | # - create a Durable activity function (default name is "Hello") 5 | # - add azure-functions-durable to requirements.txt 6 | # - run pip install -r requirements.txt 7 | 8 | import logging 9 | import json 10 | import azure.functions as func 11 | import azure.durable_functions as df 12 | 13 | async def main(req: func.HttpRequest, starter: str) -> func.HttpResponse: 14 | client = df.DurableOrchestrationClient(starter) 15 | 16 | payload: str = json.loads(req.get_body().decode()) 17 | instance_id = await client.start_new(req.route_params["functionName"], client_input=payload) 18 | 19 | logging.info(f"Started orchestration with ID = '{instance_id}'.") 20 | 21 | return client.create_check_status_response(req, instance_id) -------------------------------------------------------------------------------- /samples/human_interaction/HttpStart/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "__init__.py", 3 | "bindings": [ 4 | { 5 | "authLevel": "anonymous", 6 | "name": "req", 7 | "type": "httpTrigger", 8 | "direction": "in", 9 | "route": "orchestrators/{functionName}", 10 | "methods": [ 11 | "post", 12 | "get" 13 | ] 14 | }, 15 | { 16 | "name": "$return", 17 | "type": "http", 18 | "direction": "out" 19 | }, 20 | { 21 | "name": "starter", 22 | "type": "orchestrationClient", 23 | "direction": "in" 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /samples/human_interaction/README.md: -------------------------------------------------------------------------------- 1 | # Human Interaction 2 | 3 | This directory contains a executable version of [this](https://docs.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-phone-verification?tabs=python) tutorial. Please review the link above for instructions on how to run it. -------------------------------------------------------------------------------- /samples/human_interaction/SendSMSChallenge/__init__.py: -------------------------------------------------------------------------------- 1 | import json 2 | import random 3 | 4 | random.seed(10) 5 | 6 | def main(phoneNumber, message): 7 | code = random.randint(0, 10000) 8 | payload = { 9 | "body": f"Your verification code is {code}", 10 | "to": phoneNumber 11 | } 12 | 13 | message.set(json.dumps(payload)) 14 | code_str = str(code) 15 | return code_str 16 | -------------------------------------------------------------------------------- /samples/human_interaction/SendSMSChallenge/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "bindings": [ 3 | { 4 | "name": "phoneNumber", 5 | "type": "activityTrigger", 6 | "direction": "in" 7 | }, 8 | { 9 | "type": "twilioSms", 10 | "name": "message", 11 | "from": "%TwilioPhoneNumber%", 12 | "accountSidSetting": "TwilioAccountSid", 13 | "authTokenSetting": "TwilioAuthToken", 14 | "direction": "out" 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /samples/human_interaction/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "logging": { 4 | "applicationInsights": { 5 | "samplingSettings": { 6 | "isEnabled": true, 7 | "excludedTypes": "Request" 8 | } 9 | } 10 | }, 11 | "extensionBundle": { 12 | "id": "Microsoft.Azure.Functions.ExtensionBundle", 13 | "version": "[2.*, 3.0.0)" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /samples/human_interaction/local.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "IsEncrypted": false, 3 | "Values": { 4 | "AzureWebJobsStorage": "", 5 | "FUNCTIONS_WORKER_RUNTIME": "python", 6 | "TwilioAccountSid": "", 7 | "TwilioAuthToken": "", 8 | "TwilioPhoneNumber": "" 9 | } 10 | } -------------------------------------------------------------------------------- /samples/human_interaction/proxies.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/proxies", 3 | "proxies": {} 4 | } 5 | -------------------------------------------------------------------------------- /samples/human_interaction/requirements.txt: -------------------------------------------------------------------------------- 1 | # DO NOT include azure-functions-worker in this file 2 | # The Python Worker is managed by Azure Functions platform 3 | # Manually managing azure-functions-worker may cause unexpected issues 4 | 5 | azure-functions 6 | azure-functions-durable -------------------------------------------------------------------------------- /samples/monitor/.funcignore: -------------------------------------------------------------------------------- 1 | .git* 2 | .vscode 3 | local.settings.json 4 | test 5 | .venv -------------------------------------------------------------------------------- /samples/monitor/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # IPython 79 | profile_default/ 80 | ipython_config.py 81 | 82 | # pyenv 83 | .python-version 84 | 85 | # pipenv 86 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 87 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 88 | # having no cross-platform support, pipenv may install dependencies that don’t work, or not 89 | # install all needed dependencies. 90 | #Pipfile.lock 91 | 92 | # celery beat schedule file 93 | celerybeat-schedule 94 | 95 | # SageMath parsed files 96 | *.sage.py 97 | 98 | # Environments 99 | .env 100 | .venv 101 | env/ 102 | venv/ 103 | ENV/ 104 | env.bak/ 105 | venv.bak/ 106 | 107 | # Spyder project settings 108 | .spyderproject 109 | .spyproject 110 | 111 | # Rope project settings 112 | .ropeproject 113 | 114 | # mkdocs documentation 115 | /site 116 | 117 | # mypy 118 | .mypy_cache/ 119 | .dmypy.json 120 | dmypy.json 121 | 122 | # Pyre type checker 123 | .pyre/ 124 | 125 | # Azure Functions artifacts 126 | bin 127 | obj 128 | appsettings.json 129 | local.settings.json 130 | .python_packages -------------------------------------------------------------------------------- /samples/monitor/E3_Monitor/__init__.py: -------------------------------------------------------------------------------- 1 | import azure.durable_functions as df 2 | from datetime import timedelta 3 | from typing import Dict 4 | 5 | def orchestrator_function(context: df.DurableOrchestrationContext): 6 | 7 | monitoring_request: Dict[str, str] = context.get_input() 8 | repo_url: str = monitoring_request["repo"] 9 | phone: str = monitoring_request["phone"] 10 | 11 | # Expiration of the repo monitoring 12 | expiry_time = context.current_utc_datetime + timedelta(minutes=5) 13 | while context.current_utc_datetime < expiry_time: 14 | # Count the number of issues in the repo (the GitHub API caps at 30 issues per page) 15 | too_many_issues = yield context.call_activity("E3_TooManyOpenIssues", repo_url) 16 | 17 | # If we detect too many issues, we text the provided phone number 18 | if too_many_issues: 19 | # Extract URLs of GitHub issues, and return them 20 | yield context.call_activity("E3_SendAlert", phone) 21 | break 22 | else: 23 | 24 | # Reporting the number of statuses found 25 | status = f"The repository does not have too many issues, for now ..." 26 | context.set_custom_status(status) 27 | 28 | # Schedule a new "wake up" signal 29 | next_check = context.current_utc_datetime + timedelta(minutes=1) 30 | yield context.create_timer(next_check) 31 | 32 | return "Monitor completed!" 33 | 34 | main = df.Orchestrator.create(orchestrator_function) -------------------------------------------------------------------------------- /samples/monitor/E3_Monitor/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "__init__.py", 3 | "bindings": [ 4 | { 5 | "name": "context", 6 | "type": "orchestrationTrigger", 7 | "direction": "in" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /samples/monitor/E3_SendAlert/__init__.py: -------------------------------------------------------------------------------- 1 | import json 2 | import random 3 | 4 | random.seed(10) 5 | 6 | def main(phoneNumber: str, message): 7 | payload = { 8 | "body": f"Hey! You may want to check on your repo, there are too many open issues", 9 | "to": phoneNumber 10 | } 11 | 12 | message.set(json.dumps(payload)) 13 | return "Message sent!" 14 | -------------------------------------------------------------------------------- /samples/monitor/E3_SendAlert/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "bindings": [ 3 | { 4 | "name": "phoneNumber", 5 | "type": "activityTrigger", 6 | "direction": "in" 7 | }, 8 | { 9 | "type": "twilioSms", 10 | "name": "message", 11 | "from": "%TwilioPhoneNumber%", 12 | "accountSidSetting": "TwilioAccountSid", 13 | "authTokenSetting": "TwilioAuthToken", 14 | "direction": "out" 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /samples/monitor/E3_TooManyOpenIssues/__init__.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | 4 | def main(repoID: str) -> str: 5 | 6 | # We use the GitHub API to count the number of open issues in the repo provided 7 | # Note that the GitHub API only displays at most 30 issues per response, so 8 | # the maximum number this activity will return is 30. That's enough for demo'ing purposes. 9 | [user, repo] = repoID.split("/") 10 | url = f"https://api.github.com/repos/{user}/{repo}/issues?state=open" 11 | res = requests.get(url) 12 | if res.status_code != 200: 13 | error_message = f"Could not find repo {user} under {repo}! API endpoint hit was: {url}" 14 | raise Exception(error_message) 15 | issues = json.loads(res.text) 16 | too_many_issues: bool = len(issues) >= 3 17 | return too_many_issues -------------------------------------------------------------------------------- /samples/monitor/E3_TooManyOpenIssues/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "__init__.py", 3 | "bindings": [ 4 | { 5 | "name": "repoID", 6 | "type": "activityTrigger", 7 | "direction": "in" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /samples/monitor/HttpStart/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import azure.functions as func 4 | import azure.durable_functions as df 5 | import json 6 | 7 | 8 | async def main(req: func.HttpRequest, starter: str) -> func.HttpResponse: 9 | client = df.DurableOrchestrationClient(starter) 10 | payload = json.loads(req.get_body().decode()) 11 | instance_id = await client.start_new(req.route_params["functionName"], client_input=payload) 12 | 13 | logging.info(f"Started orchestration with ID = '{instance_id}'.") 14 | 15 | return client.create_check_status_response(req, instance_id) -------------------------------------------------------------------------------- /samples/monitor/HttpStart/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "__init__.py", 3 | "bindings": [ 4 | { 5 | "authLevel": "anonymous", 6 | "name": "req", 7 | "type": "httpTrigger", 8 | "direction": "in", 9 | "route": "orchestrators/{functionName}", 10 | "methods": [ 11 | "post", 12 | "get" 13 | ] 14 | }, 15 | { 16 | "name": "$return", 17 | "type": "http", 18 | "direction": "out" 19 | }, 20 | { 21 | "name": "starter", 22 | "type": "orchestrationClient", 23 | "direction": "in" 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /samples/monitor/README.md: -------------------------------------------------------------------------------- 1 | # Human Interaction 2 | 3 | This directory contains a executable version of [this](https://docs.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-monitor-python) tutorial. Please review the link above for instructions on how to run it. -------------------------------------------------------------------------------- /samples/monitor/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "logging": { 4 | "applicationInsights": { 5 | "samplingSettings": { 6 | "isEnabled": true, 7 | "excludedTypes": "Request" 8 | } 9 | } 10 | }, 11 | "extensionBundle": { 12 | "id": "Microsoft.Azure.Functions.ExtensionBundle", 13 | "version": "[2.*, 3.0.0)" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /samples/monitor/local.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "IsEncrypted": false, 3 | "Values": { 4 | "AzureWebJobsStorage": "", 5 | "FUNCTIONS_WORKER_RUNTIME": "python", 6 | "TwilioPhoneNumber": "", 7 | "TwilioAccountSid": "", 8 | "TwilioAuthToken": "" 9 | } 10 | } -------------------------------------------------------------------------------- /samples/monitor/proxies.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/proxies", 3 | "proxies": {} 4 | } 5 | -------------------------------------------------------------------------------- /samples/monitor/requirements.txt: -------------------------------------------------------------------------------- 1 | # DO NOT include azure-functions-worker in this file 2 | # The Python Worker is managed by Azure Functions platform 3 | # Manually managing azure-functions-worker may cause unexpected issues 4 | 5 | azure-functions 6 | azure-functions-durable -------------------------------------------------------------------------------- /samples/serialize_arguments/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # IPython 79 | profile_default/ 80 | ipython_config.py 81 | 82 | # pyenv 83 | .python-version 84 | 85 | # pipenv 86 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 87 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 88 | # having no cross-platform support, pipenv may install dependencies that don’t work, or not 89 | # install all needed dependencies. 90 | #Pipfile.lock 91 | 92 | # celery beat schedule file 93 | celerybeat-schedule 94 | 95 | # SageMath parsed files 96 | *.sage.py 97 | 98 | # Environments 99 | .env 100 | .venv 101 | env/ 102 | venv/ 103 | ENV/ 104 | env.bak/ 105 | venv.bak/ 106 | 107 | # Spyder project settings 108 | .spyderproject 109 | .spyproject 110 | 111 | # Rope project settings 112 | .ropeproject 113 | 114 | # mkdocs documentation 115 | /site 116 | 117 | # mypy 118 | .mypy_cache/ 119 | .dmypy.json 120 | dmypy.json 121 | 122 | # Pyre type checker 123 | .pyre/ 124 | 125 | # Azure Functions artifacts 126 | bin 127 | obj 128 | appsettings.json 129 | .python_packages 130 | 131 | # pycharm 132 | .idea 133 | -------------------------------------------------------------------------------- /samples/serialize_arguments/DurableActivity/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | def main(name): 4 | """Activity function performing a specific step in the chain 5 | 6 | Parameters 7 | ---------- 8 | name : str 9 | Name of the item to be hello'ed at 10 | 11 | Returns 12 | ------- 13 | str 14 | Returns a welcome string 15 | """ 16 | logging.warning(f"Activity Triggered: {name}") 17 | return name 18 | 19 | -------------------------------------------------------------------------------- /samples/serialize_arguments/DurableActivity/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "__init__.py", 3 | "bindings": [ 4 | { 5 | "name": "name", 6 | "type": "activityTrigger", 7 | "direction": "in", 8 | "datatype": "string" 9 | } 10 | ], 11 | "disabled": false 12 | } -------------------------------------------------------------------------------- /samples/serialize_arguments/DurableOrchestration/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import azure.functions as func 4 | import azure.durable_functions as df 5 | from ..shared_code.MyClasses import SerializableClass 6 | 7 | def orchestrator_function(context: df.DurableOrchestrationContext): 8 | """This function provides the core function chaining orchestration logic 9 | 10 | Parameters 11 | ---------- 12 | context: DurableOrchestrationContext 13 | This context has the past history and the durable orchestration API's to 14 | create orchestrations 15 | 16 | Returns 17 | ------- 18 | int 19 | The number contained in the SerializableClass input object 20 | """ 21 | input_: SerializableClass = context.get_input() 22 | num1: int = input_.show_number() 23 | 24 | # The custom class is also correctly serialized when calling an activity 25 | num2 = yield context.call_activity("DurableActivity", SerializableClass(5)) 26 | return num1 27 | 28 | main = df.Orchestrator.create(orchestrator_function) 29 | -------------------------------------------------------------------------------- /samples/serialize_arguments/DurableOrchestration/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "__init__.py", 3 | "bindings": [ 4 | { 5 | "name": "context", 6 | "type": "orchestrationTrigger", 7 | "direction": "in" 8 | } 9 | ], 10 | "disabled": false 11 | } 12 | -------------------------------------------------------------------------------- /samples/serialize_arguments/DurableTrigger/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from azure.durable_functions import DurableOrchestrationClient 4 | import azure.functions as func 5 | from ..shared_code.MyClasses import SerializableClass 6 | 7 | 8 | async def main(req: func.HttpRequest, starter: str, message): 9 | """This function starts up the orchestrator from an HTTP endpoint 10 | 11 | starter: str 12 | A JSON-formatted string describing the orchestration context 13 | 14 | message: 15 | An azure functions http output binding, it enables us to establish 16 | an http response. 17 | 18 | Parameters 19 | ---------- 20 | req: func.HttpRequest 21 | An HTTP Request object, it can be used to parse URL 22 | parameters. 23 | """ 24 | 25 | 26 | function_name = req.route_params.get('functionName') 27 | logging.info(starter) 28 | client = DurableOrchestrationClient(starter) 29 | instance_id = await client.start_new(function_name, client_input=SerializableClass(5)) 30 | response = client.create_check_status_response(req, instance_id) 31 | message.set(response) 32 | -------------------------------------------------------------------------------- /samples/serialize_arguments/DurableTrigger/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "__init__.py", 3 | "bindings": [ 4 | { 5 | "authLevel": "anonymous", 6 | "name": "req", 7 | "type": "httpTrigger", 8 | "direction": "in", 9 | "route": "orchestrators/{functionName}", 10 | "methods": [ 11 | "post", 12 | "get" 13 | ] 14 | }, 15 | { 16 | "direction": "out", 17 | "name": "message", 18 | "type": "http" 19 | }, 20 | { 21 | "name": "starter", 22 | "type": "orchestrationClient", 23 | "direction": "in", 24 | "datatype": "string" 25 | } 26 | ] 27 | } -------------------------------------------------------------------------------- /samples/serialize_arguments/README.md: -------------------------------------------------------------------------------- 1 | # Serializing Custom Classes - Sample 2 | 3 | This sample illustrates how to create custom classes that can be serialized for usage in Durable Functions for Python. 4 | To create serializable classes, all we require is for your class to export two static methods: `to_json()` and `from_json()`. 5 | The Durable Functions framework will interally call these classes to serialize and de-serialize your custom class. Therefore, you should 6 | design these two functions such that calling `from_json` on the result of `to_json` is able to reconstruct your class. 7 | 8 | For an example, please review the `shared_code` directory, where we declare a custom class that we call `SerializableClass` which implements 9 | the required static methods. 10 | 11 | ## Usage Instructions 12 | 13 | ### Create a `local.settings.json` file in this directory 14 | This file stores app settings, connection strings, and other settings used by local development tools. Learn more about it [here](https://docs.microsoft.com/en-us/azure/azure-functions/functions-run-local?tabs=windows%2Ccsharp%2Cbash#local-settings-file). 15 | For this sample, you will only need an `AzureWebJobsStorage` connection string, which you can obtain from the Azure portal. 16 | 17 | With you connection string, your `local.settings.json` file should look as follows, with `` replaced with the connection string you obtained from the Azure portal: 18 | 19 | ```json 20 | { 21 | "IsEncrypted": false, 22 | "Values": { 23 | "AzureWebJobsStorage": "", 24 | "FUNCTIONS_WORKER_RUNTIME": "python" 25 | } 26 | } 27 | ``` 28 | 29 | ### Run the Sample 30 | To try this sample, run `func host start` in this directory. If all the system requirements have been met, and 31 | after some initialization logs, you should see something like the following: 32 | 33 | ```bash 34 | Http Functions: 35 | 36 | DurableTrigger: [POST,GET] http://localhost:7071/api/orchestrators/{functionName} 37 | ``` 38 | 39 | This indicates that your `DurableTrigger` function can be reached via a `GET` or `POST` request to that URL. `DurableTrigger` starts the function-chaning orchestrator whose name is passed as a parameter to the URL. So, to start the orchestrator, which is named `DurableOrchestration`, make a GET request to `http://127.0.0.1:7071/api/orchestrators/DurableOrchestration`. 40 | 41 | And that's it! You should see a JSON response with five URLs to monitor the status of the orchestration. To learn more about this, please read [here](TODO)! -------------------------------------------------------------------------------- /samples/serialize_arguments/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "extensionBundle": { 4 | "id": "Microsoft.Azure.Functions.ExtensionBundle", 5 | "version": "[2.*, 3.0.0)" 6 | } 7 | } -------------------------------------------------------------------------------- /samples/serialize_arguments/local.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "IsEncrypted": false, 3 | "Values": { 4 | "AzureWebJobsStorage": "", 5 | "FUNCTIONS_WORKER_RUNTIME": "python" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /samples/serialize_arguments/requirements.txt: -------------------------------------------------------------------------------- 1 | azure-functions 2 | azure-functions-durable>=1.0.0b6 -------------------------------------------------------------------------------- /samples/serialize_arguments/shared_code/MyClasses.py: -------------------------------------------------------------------------------- 1 | import typing 2 | import json 3 | 4 | class SerializableClass(object): 5 | """ Example serializable class. 6 | 7 | For a custom class to be serializable in 8 | Python Durable Functions, we require that 9 | it include both `to_json` and `from_json` 10 | a `@staticmethod`s for serializing to JSON 11 | and back respectively. These get called 12 | internally by the framework. 13 | """ 14 | def __init__(self, number: int): 15 | """ Construct the class 16 | Parameters 17 | ---------- 18 | number: int 19 | A number to encapsulate 20 | """ 21 | self.number = number 22 | 23 | def show_number(self) -> int: 24 | """" Returns the number value""" 25 | return self.number 26 | 27 | @staticmethod 28 | def to_json(obj: object) -> str: 29 | """ Serializes a `SerializableClass` instance 30 | to a JSON string. 31 | 32 | Parameters 33 | ---------- 34 | obj: SerializableClass 35 | The object to serialize 36 | 37 | Returns 38 | ------- 39 | json_str: str 40 | A JSON-encoding of `obj` 41 | """ 42 | return str(obj.number) 43 | 44 | @staticmethod 45 | def from_json(json_str: str) -> object: 46 | """ De-serializes a JSON string to a 47 | `SerializableClass` instance. It assumes 48 | that the JSON string was generated via 49 | `SerializableClass.to_json` 50 | 51 | Parameters 52 | ---------- 53 | json_str: str 54 | The JSON-encoding of a `SerializableClass` instance 55 | 56 | Returns 57 | -------- 58 | obj: SerializableClass 59 | A SerializableClass instance, de-serialized from `json_str` 60 | """ 61 | number = int(json_str) 62 | obj = SerializableClass(number) 63 | return obj 64 | -------------------------------------------------------------------------------- /samples/simple_sub_orchestration/.funcignore: -------------------------------------------------------------------------------- 1 | .git* 2 | .vscode 3 | local.settings.json 4 | test 5 | .venv -------------------------------------------------------------------------------- /samples/simple_sub_orchestration/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # IPython 79 | profile_default/ 80 | ipython_config.py 81 | 82 | # pyenv 83 | .python-version 84 | 85 | # pipenv 86 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 87 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 88 | # having no cross-platform support, pipenv may install dependencies that don’t work, or not 89 | # install all needed dependencies. 90 | #Pipfile.lock 91 | 92 | # celery beat schedule file 93 | celerybeat-schedule 94 | 95 | # SageMath parsed files 96 | *.sage.py 97 | 98 | # Environments 99 | .env 100 | .venv 101 | env/ 102 | venv/ 103 | ENV/ 104 | env.bak/ 105 | venv.bak/ 106 | 107 | # Spyder project settings 108 | .spyderproject 109 | .spyproject 110 | 111 | # Rope project settings 112 | .ropeproject 113 | 114 | # mkdocs documentation 115 | /site 116 | 117 | # mypy 118 | .mypy_cache/ 119 | .dmypy.json 120 | dmypy.json 121 | 122 | # Pyre type checker 123 | .pyre/ 124 | 125 | # Azure Functions artifacts 126 | bin 127 | obj 128 | appsettings.json 129 | local.settings.json 130 | .python_packages -------------------------------------------------------------------------------- /samples/simple_sub_orchestration/DurableFunctionsHttpStart/__init__.py: -------------------------------------------------------------------------------- 1 | # This function an HTTP starter function for Durable Functions. 2 | # Before running this sample, please: 3 | # - create a Durable orchestration function 4 | # - create a Durable activity function (default name is "Hello") 5 | # - add azure-functions-durable to requirements.txt 6 | # - run pip install -r requirements.txt 7 | 8 | import logging 9 | 10 | import azure.functions as func 11 | import azure.durable_functions as df 12 | 13 | 14 | async def main(req: func.HttpRequest, starter: str) -> func.HttpResponse: 15 | client = df.DurableOrchestrationClient(starter) 16 | instance_id = await client.start_new(req.route_params["functionName"], None, None) 17 | 18 | logging.info(f"Started orchestration with ID = '{instance_id}'.") 19 | 20 | return client.create_check_status_response(req, instance_id) -------------------------------------------------------------------------------- /samples/simple_sub_orchestration/DurableFunctionsHttpStart/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "__init__.py", 3 | "bindings": [ 4 | { 5 | "authLevel": "anonymous", 6 | "name": "req", 7 | "type": "httpTrigger", 8 | "direction": "in", 9 | "route": "orchestrators/{functionName}", 10 | "methods": [ 11 | "post", 12 | "get" 13 | ] 14 | }, 15 | { 16 | "name": "$return", 17 | "type": "http", 18 | "direction": "out" 19 | }, 20 | { 21 | "name": "starter", 22 | "type": "orchestrationClient", 23 | "direction": "in" 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /samples/simple_sub_orchestration/DurableOrchestrator/__init__.py: -------------------------------------------------------------------------------- 1 | # This function is not intended to be invoked directly. Instead it will be 2 | # triggered by an HTTP starter function. 3 | # Before running this sample, please: 4 | # - create a Durable activity function (default name is "Hello") 5 | # - create a Durable HTTP starter function 6 | # - add azure-functions-durable to requirements.txt 7 | # - run pip install -r requirements.txt 8 | 9 | import logging 10 | import json 11 | 12 | import azure.functions as func 13 | import azure.durable_functions as df 14 | 15 | 16 | def orchestrator_function(context: df.DurableOrchestrationContext): 17 | result = yield context.call_sub_orchestrator("DurableSubOrchestrator", "Seattle") 18 | return result 19 | 20 | main = df.Orchestrator.create(orchestrator_function) -------------------------------------------------------------------------------- /samples/simple_sub_orchestration/DurableOrchestrator/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "__init__.py", 3 | "bindings": [ 4 | { 5 | "name": "context", 6 | "type": "orchestrationTrigger", 7 | "direction": "in" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /samples/simple_sub_orchestration/DurableSubOrchestrator/__init__.py: -------------------------------------------------------------------------------- 1 | # This function is not intended to be invoked directly. Instead it will be 2 | # triggered by an HTTP starter function. 3 | # Before running this sample, please: 4 | # - create a Durable activity function (default name is "Hello") 5 | # - create a Durable HTTP starter function 6 | # - add azure-functions-durable to requirements.txt 7 | # - run pip install -r requirements.txt 8 | 9 | import logging 10 | import json 11 | 12 | import azure.functions as func 13 | import azure.durable_functions as df 14 | 15 | 16 | def orchestrator_function(context: df.DurableOrchestrationContext): 17 | input_ = context.get_input() 18 | result1 = yield context.call_activity('Hello', input_) 19 | return result1 20 | 21 | main = df.Orchestrator.create(orchestrator_function) -------------------------------------------------------------------------------- /samples/simple_sub_orchestration/DurableSubOrchestrator/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "__init__.py", 3 | "bindings": [ 4 | { 5 | "name": "context", 6 | "type": "orchestrationTrigger", 7 | "direction": "in" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /samples/simple_sub_orchestration/Hello/__init__.py: -------------------------------------------------------------------------------- 1 | # This function is not intended to be invoked directly. Instead it will be 2 | # triggered by an orchestrator function. 3 | # Before running this sample, please: 4 | # - create a Durable orchestration function 5 | # - create a Durable HTTP starter function 6 | # - add azure-functions-durable to requirements.txt 7 | # - run pip install -r requirements.txt 8 | 9 | import logging 10 | 11 | 12 | def main(name: str) -> str: 13 | return f"Hello {name}!" 14 | -------------------------------------------------------------------------------- /samples/simple_sub_orchestration/Hello/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "__init__.py", 3 | "bindings": [ 4 | { 5 | "name": "name", 6 | "type": "activityTrigger", 7 | "direction": "in" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /samples/simple_sub_orchestration/README.md: -------------------------------------------------------------------------------- 1 | # Function Chaining - Sample 2 | 3 | This sample exemplifies how to go about implementing [Sub-Orchestration](https://docs.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-sub-orchestrations?tabs=csharp) in Python Durable Functions. 4 | 5 | ## Usage Instructions 6 | 7 | ### Create a `local.settings.json` file in this directory 8 | This file stores app settings, connection strings, and other settings used by local development tools. Learn more about it [here](https://docs.microsoft.com/en-us/azure/azure-functions/functions-run-local?tabs=windows%2Ccsharp%2Cbash#local-settings-file). 9 | For this sample, you will only need an `AzureWebJobsStorage` connection string, which you can obtain from the Azure portal. 10 | 11 | With you connection string, your `local.settings.json` file should look as follows, with `` replaced with the connection string you obtained from the Azure portal: 12 | 13 | ```json 14 | { 15 | "IsEncrypted": false, 16 | "Values": { 17 | "AzureWebJobsStorage": "", 18 | "FUNCTIONS_WORKER_RUNTIME": "python" 19 | } 20 | } 21 | ``` 22 | 23 | ### Run the Sample 24 | To try this sample, run `func host start` in this directory. If all the system requirements have been met, and 25 | after some initialization logs, you should see something like the following: 26 | 27 | ```bash 28 | Http Functions: 29 | 30 | DurableTrigger: [POST,GET] http://localhost:7071/api/orchestrators/{functionName} 31 | ``` 32 | 33 | This indicates that your `DurableTrigger` function can be reached via a `GET` or `POST` request to that URL. `DurableTrigger` starts the function-chaning orchestrator whose name is passed as a parameter to the URL. So, to start the orchestrator, which is named `DurableOrchestration`, make a GET request to `http://127.0.0.1:7071/api/orchestrators/DurableOrchestration`. 34 | 35 | And that's it! You should see a JSON response with five URLs to monitor the status of the orchestration. To learn more about this, please read [here](TODO)! -------------------------------------------------------------------------------- /samples/simple_sub_orchestration/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "logging": { 4 | "applicationInsights": { 5 | "samplingSettings": { 6 | "isEnabled": true, 7 | "excludedTypes": "Request" 8 | } 9 | } 10 | }, 11 | "extensionBundle": { 12 | "id": "Microsoft.Azure.Functions.ExtensionBundle", 13 | "version": "[2.*, 3.0.0)" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /samples/simple_sub_orchestration/local.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "IsEncrypted": false, 3 | "Values": { 4 | "AzureWebJobsStorage": "", 5 | "FUNCTIONS_WORKER_RUNTIME": "python" 6 | } 7 | } -------------------------------------------------------------------------------- /samples/simple_sub_orchestration/proxies.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/proxies", 3 | "proxies": {} 4 | } 5 | -------------------------------------------------------------------------------- /samples/simple_sub_orchestration/requirements.txt: -------------------------------------------------------------------------------- 1 | azure-functions 2 | azure-functions-durable -------------------------------------------------------------------------------- /scripts/sample_deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Checking for prerequisites..." 4 | if ! type npm > /dev/null; then 5 | echo "Prerequisite Check 1: Install Node.js and NPM" 6 | exit 1 7 | fi 8 | 9 | if ! type dotnet > /dev/null; then 10 | echo "Prerequisite Check 2: Install .NET Core 2.1 SDK or Runtime" 11 | exit 1 12 | fi 13 | 14 | if ! type func > /dev/null; then 15 | echo "Prerequisite Check 3: Install Azure Functions Core Tools" 16 | exit 1 17 | fi 18 | 19 | echo "Pre-requisites satisfied..." 20 | 21 | echo "Creating sample folders..." 22 | DIRECTORY=/tmp/df_test 23 | if [ ! -d "$DIRECTORY" ]; then 24 | mkdir /tmp/df_test 25 | else 26 | rm -rf /tmp/df_test/* 27 | fi 28 | 29 | SAMPLE=function_chaining 30 | cp -r ../samples/$SAMPLE $DIRECTORY/ 31 | cd $DIRECTORY/$SAMPLE 32 | python -m venv env 33 | source env/bin/activate 34 | 35 | echo "Provide local path to azure-functions-durable-python clone:" 36 | read lib_path 37 | pip install $lib_path/azure-functions-durable-python 38 | func init . 39 | func extensions install 40 | echo "Done" 41 | 42 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Unit tests for the durable functions library""" 2 | import os 3 | import sys 4 | import unittest 5 | 6 | 7 | def suite(): 8 | """ 9 | 10 | :return: configuration for the suite of tests 11 | """ 12 | test_loader = unittest.TestLoader() 13 | test_suite = test_loader.discover( 14 | os.path.dirname(__file__), pattern='test_*.py') 15 | return test_suite 16 | 17 | 18 | if __name__ == '__main__': 19 | runner = unittest.runner.TextTestRunner() 20 | result = runner.run(suite()) 21 | sys.exit(not result.wasSuccessful()) 22 | -------------------------------------------------------------------------------- /tests/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/azure-functions-durable-python/246e66e42136f132914cbf2f70c9276686bbf22b/tests/models/__init__.py -------------------------------------------------------------------------------- /tests/models/test_OrchestrationState.py: -------------------------------------------------------------------------------- 1 | from azure.durable_functions.models.ReplaySchema import ReplaySchema 2 | from typing import List 3 | 4 | from azure.durable_functions.models.actions.Action import Action 5 | from azure.durable_functions.models.actions.CallActivityAction \ 6 | import CallActivityAction 7 | from azure.durable_functions.models.OrchestratorState import OrchestratorState 8 | 9 | 10 | def test_empty_state_to_json_string(): 11 | actions: List[List[Action]] = [] 12 | state = OrchestratorState(is_done=False, actions=actions, output=None, replay_schema=ReplaySchema.V1.value) 13 | result = state.to_json_string() 14 | expected_result = '{"isDone": false, "actions": []}' 15 | assert expected_result == result 16 | 17 | 18 | def test_single_action_state_to_json_string(): 19 | actions: List[List[Action]] = [] 20 | action: Action = CallActivityAction( 21 | function_name="MyFunction", input_="AwesomeInput") 22 | actions.append([action]) 23 | state = OrchestratorState(is_done=False, actions=actions, output=None, replay_schema=ReplaySchema.V1.value) 24 | result = state.to_json_string() 25 | expected_result = ('{"isDone": false, "actions": [[{"actionType": 0, ' 26 | '"functionName": "MyFunction", "input": ' 27 | '"\\"AwesomeInput\\""}]]}') 28 | assert expected_result == result 29 | -------------------------------------------------------------------------------- /tests/models/test_TokenSource.py: -------------------------------------------------------------------------------- 1 | from azure.durable_functions.models.TokenSource import ManagedIdentityTokenSource 2 | 3 | def test_serialization_fields(): 4 | """Validates the TokenSource contains the expected fields when serialized to JSON""" 5 | token_source = ManagedIdentityTokenSource(resource="TOKEN_SOURCE") 6 | token_source_json = token_source.to_json() 7 | 8 | # Output JSON should contain a resource field and a kind field set to `AzureManagedIdentity` 9 | assert "resource" in token_source_json.keys() 10 | assert "kind" in token_source_json.keys() 11 | assert token_source_json["kind"] == "AzureManagedIdentity" -------------------------------------------------------------------------------- /tests/orchestrator/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/azure-functions-durable-python/246e66e42136f132914cbf2f70c9276686bbf22b/tests/orchestrator/__init__.py -------------------------------------------------------------------------------- /tests/orchestrator/models/OrchestrationInstance.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | from typing import Any, Dict 3 | 4 | from tests.test_utils.json_utils import add_attrib 5 | 6 | 7 | class OrchestrationInstance: 8 | def __init__(self): 9 | self.instance_id: str = str(uuid.uuid4()) 10 | self.execution_id: str = str(uuid.uuid4()) 11 | 12 | def to_json(self) -> Dict[str, Any]: 13 | json_dict = {} 14 | 15 | add_attrib(json_dict, self, 'instance_id', 'InstanceId') 16 | add_attrib(json_dict, self, 'execution_id', 'ExecutionId') 17 | 18 | return json_dict 19 | -------------------------------------------------------------------------------- /tests/orchestrator/test_serialization.py: -------------------------------------------------------------------------------- 1 | from azure.durable_functions.models.ReplaySchema import ReplaySchema 2 | from tests.test_utils.ContextBuilder import ContextBuilder 3 | from .orchestrator_test_utils \ 4 | import get_orchestration_state_result, assert_orchestration_state_equals, assert_valid_schema 5 | from azure.durable_functions.models.OrchestratorState import OrchestratorState 6 | 7 | def base_expected_state(output=None, replay_schema: ReplaySchema = ReplaySchema.V1) -> OrchestratorState: 8 | return OrchestratorState(is_done=False, actions=[], output=output, replay_schema=replay_schema.value) 9 | 10 | def generator_function(context): 11 | return False 12 | 13 | def test_serialization_of_False(): 14 | """Test that an orchestrator can return False.""" 15 | 16 | context_builder = ContextBuilder("serialize False") 17 | 18 | result = get_orchestration_state_result( 19 | context_builder, generator_function) 20 | 21 | expected_state = base_expected_state(output=False) 22 | 23 | expected_state._is_done = True 24 | expected = expected_state.to_json() 25 | 26 | # Since we're essentially testing the `to_json` functionality, 27 | # we explicitely ensure that the output is set 28 | expected["output"] = False 29 | 30 | assert_valid_schema(result) 31 | assert_orchestration_state_equals(expected, result) -------------------------------------------------------------------------------- /tests/tasks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/azure-functions-durable-python/246e66e42136f132914cbf2f70c9276686bbf22b/tests/tasks/__init__.py -------------------------------------------------------------------------------- /tests/tasks/tasks_test_utils.py: -------------------------------------------------------------------------------- 1 | def assert_tasks_equal(task1, task2): 2 | assert task1.is_completed == task2.is_completed 3 | assert task1.is_faulted == task2.is_faulted 4 | assert task1.result == task2.result 5 | assert task1.timestamp == task2.timestamp 6 | assert task1.id == task2.id 7 | assert task1.action == task2.action 8 | assert str(task1.exception) == str(task2.exception) 9 | 10 | 11 | def assert_taskset_equal(taskset1, taskset2): 12 | assert taskset1.is_completed == taskset2.is_completed 13 | assert taskset1.is_faulted == taskset2.is_faulted 14 | assert taskset1.result == taskset2.result 15 | assert taskset1.actions == taskset2.actions 16 | assert taskset1.timestamp == taskset2.timestamp 17 | assert str(taskset1.exception) == str(taskset2.exception) 18 | -------------------------------------------------------------------------------- /tests/tasks/test_new_uuid.py: -------------------------------------------------------------------------------- 1 | from uuid import uuid1 2 | from typing import List, Any, Dict 3 | from azure.durable_functions.models.DurableOrchestrationContext import DurableOrchestrationContext 4 | from azure.durable_functions.constants import DATETIME_STRING_FORMAT 5 | 6 | 7 | def history_list() -> List[Dict[Any, Any]]: 8 | history = [{'EventType': 12, 'EventId': -1, 'IsPlayed': False, 9 | 'Timestamp': '2019-12-08T23:18:41.3240927Z'}, { 10 | 'OrchestrationInstance': {'InstanceId': '48d0f95957504c2fa579e810a390b938', 11 | 'ExecutionId': 'fd183ee02e4b4fd18c95b773cfb5452b'}, 12 | 'EventType': 0, 'ParentInstance': None, 'Name': 'DurableOrchestratorTrigger', 13 | 'Version': '', 'Input': 'null', 'Tags': None, 'EventId': -1, 'IsPlayed': False, 14 | 'Timestamp': '2019-12-08T23:18:39.756132Z'}] 15 | return history 16 | 17 | 18 | def test_new_uuid(): 19 | instance_id = str(uuid1()) 20 | history = history_list() 21 | context1 = DurableOrchestrationContext(history, instance_id, False, None) 22 | 23 | result1a = context1.new_uuid() 24 | result1b = context1.new_uuid() 25 | 26 | context2 = DurableOrchestrationContext(history, instance_id, False, None) 27 | 28 | result2a = context2.new_uuid() 29 | result2b = context2.new_uuid() 30 | 31 | assert result1a == result2a 32 | assert result1b == result2b 33 | 34 | assert result1a != result1b 35 | assert result2a != result2b -------------------------------------------------------------------------------- /tests/test_constants.py: -------------------------------------------------------------------------------- 1 | """ Validates the constants are set correctly.""" 2 | import unittest 3 | from azure.durable_functions.constants import ( 4 | DEFAULT_LOCAL_HOST, 5 | DEFAULT_LOCAL_ORIGIN) 6 | 7 | 8 | class TestConstants(unittest.TestCase): 9 | def test_default_local_host(self): 10 | self.assertEqual(DEFAULT_LOCAL_HOST, "localhost:7071") 11 | 12 | def test_default_local_origin(self): 13 | self.assertEqual(DEFAULT_LOCAL_ORIGIN, "http://localhost:7071") 14 | -------------------------------------------------------------------------------- /tests/test_utils/EntityContextBuilder.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import Any, List, Dict, Any 3 | 4 | class EntityContextBuilder(): 5 | """Mock class for an EntityContext object, includes a batch field for convenience 6 | """ 7 | def __init__(self, 8 | name: str = "", 9 | key: str = "", 10 | exists: bool = True, 11 | state: Any = None, 12 | batch: List[Dict[str, Any]] = []): 13 | """Construct an EntityContextBuilder 14 | 15 | Parameters 16 | ---------- 17 | name: str: 18 | The name of the entity. Defaults to the empty string. 19 | key: str 20 | The key of the entity. Defaults to the empty string. 21 | exists: bool 22 | Boolean representing if the entity exists, defaults to True. 23 | state: Any 24 | The state of the entity, defaults ot None. 25 | batch: List[Dict[str, Any]] 26 | The upcoming batch of operations for the entity to perform. 27 | Note that the batch is not technically a part of the entity context 28 | and so it is here only for convenience. Defaults to the empty list. 29 | """ 30 | self.name = name 31 | self.key = key 32 | self.exists = exists 33 | self.state = state 34 | self.batch = batch 35 | 36 | def to_json_string(self) -> str: 37 | """Generate a string-representation of the Entity input payload. 38 | 39 | The payload matches the current durable-extension entity-communication 40 | schema. 41 | 42 | Returns 43 | ------- 44 | str: 45 | A JSON-formatted string for an EntityContext to load via `from_json` 46 | """ 47 | context_json = { 48 | "self": { 49 | "name": self.name, 50 | "key": self.key 51 | }, 52 | "state": self.state, 53 | "exists": self.exists, 54 | "batch": self.batch 55 | } 56 | json_string = json.dumps(context_json) 57 | return json_string -------------------------------------------------------------------------------- /tests/test_utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/azure-functions-durable-python/246e66e42136f132914cbf2f70c9276686bbf22b/tests/test_utils/__init__.py -------------------------------------------------------------------------------- /tests/test_utils/constants.py: -------------------------------------------------------------------------------- 1 | RPC_BASE_URL = "http://127.0.0.1:17071/durabletask/" 2 | -------------------------------------------------------------------------------- /tests/test_utils/json_utils.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict 2 | 3 | from azure.durable_functions.models.history.HistoryEvent import HistoryEvent 4 | from azure.durable_functions.models.utils.json_utils \ 5 | import add_attrib, add_json_attrib, add_datetime_attrib 6 | 7 | 8 | def convert_history_event_to_json_dict( 9 | history_event: HistoryEvent) -> Dict[str, Any]: 10 | json_dict = {} 11 | 12 | add_attrib(json_dict, history_event, 'event_id', 'EventId') 13 | add_attrib(json_dict, history_event, 'event_type', 'EventType') 14 | add_attrib(json_dict, history_event, 'is_played', 'IsPlayed') 15 | add_datetime_attrib(json_dict, history_event, 'timestamp', 'Timestamp') 16 | add_attrib(json_dict, history_event, 'Input') 17 | add_attrib(json_dict, history_event, 'Reason') 18 | add_attrib(json_dict, history_event, 'Details') 19 | add_attrib(json_dict, history_event, 'Result') 20 | add_attrib(json_dict, history_event, 'Version') 21 | add_attrib(json_dict, history_event, 'RetryOptions') 22 | add_attrib(json_dict, history_event, 'TaskScheduledId') 23 | add_attrib(json_dict, history_event, 'Tags') 24 | add_attrib(json_dict, history_event, 'FireAt') 25 | add_attrib(json_dict, history_event, 'TimerId') 26 | add_attrib(json_dict, history_event, 'Name') 27 | add_attrib(json_dict, history_event, 'InstanceId') 28 | add_json_attrib(json_dict, history_event, 29 | 'orchestration_instance', 'OrchestrationInstance') 30 | return json_dict 31 | -------------------------------------------------------------------------------- /tests/test_utils/testClasses.py: -------------------------------------------------------------------------------- 1 | class SerializableClass(object): 2 | """Example serializable class. 3 | 4 | For a custom class to be serializable in 5 | Python Durable Functions, we require that 6 | it include both `to_json` and `from_json` 7 | a `@staticmethod`s for serializing to JSON 8 | and back respectively. These get called 9 | internally by the framework. 10 | """ 11 | 12 | def __init__(self, name: str): 13 | """Construct the class. 14 | 15 | Parameters 16 | ---------- 17 | number: int 18 | A number to encapsulate 19 | """ 20 | self.name = name 21 | 22 | @staticmethod 23 | def to_json(obj: object) -> str: 24 | """Serialize a `SerializableClass` instance into a JSON string. 25 | 26 | Parameters 27 | ---------- 28 | obj: SerializableClass 29 | The object to serialize 30 | 31 | Returns 32 | ------- 33 | json_str: str 34 | A JSON-encoding of `obj` 35 | """ 36 | return obj.name 37 | 38 | @staticmethod 39 | def from_json(json_str: str) -> object: 40 | """De-serialize a JSON string to a `SerializableClass` instance. 41 | 42 | It assumes that the JSON string was generated via 43 | `SerializableClass.to_json` 44 | 45 | Parameters 46 | ---------- 47 | json_str: str 48 | The JSON-encoding of a `SerializableClass` instance 49 | 50 | Returns 51 | ------- 52 | obj: SerializableClass 53 | A SerializableClass instance, de-serialized from `json_str` 54 | """ 55 | obj = SerializableClass(json_str) 56 | return obj 57 | -------------------------------------------------------------------------------- /tests/utils/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/utils/test_entity_utils.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from azure.durable_functions.models.utils.entity_utils import EntityId 3 | 4 | @pytest.mark.parametrize( 5 | ("name_e1", "key_1", "name_e2", "key_2", "expected"), 6 | [ 7 | ("name1", "key1", "name1", "key1", True), 8 | ("name1", "key1", "name1", "key2", False), 9 | ("name1", "key1", "name2", "key1", False), 10 | ("name1", "key1", "name2", "key2", False), 11 | ], 12 | ) 13 | def test_equal_entity_by_name_and_key(name_e1, key_1, name_e2, key_2, expected): 14 | 15 | entity1 = EntityId(name_e1, key_1) 16 | entity2 = EntityId(name_e2, key_2) 17 | 18 | assert (entity1 == entity2) == expected 19 | 20 | def test_equality_with_non_entity_id(): 21 | 22 | entity = EntityId("name", "key") 23 | 24 | assert (entity == "not an entity id") == False 25 | --------------------------------------------------------------------------------