From 000d100303022c423435b337f36a4f993df65135 Mon Sep 17 00:00:00 2001 From: Andy Staples Date: Thu, 25 Jun 2026 11:20:01 -0600 Subject: [PATCH 1/2] Standardize copyright file headers --- .github/copilot-instructions.md | 15 + .../decorators/durable_app.py | 285 ++++++++++++++++++ durabletask-azuremanaged/__init__.py | 2 + .../durabletask/azuremanaged/__init__.py | 2 + durabletask/entities/durable_entity.py | 3 + durabletask/entities/entity_context.py | 2 + durabletask/entities/entity_instance_id.py | 3 + durabletask/entities/entity_lock.py | 3 + durabletask/entities/entity_metadata.py | 3 + .../entity_operation_failed_exception.py | 3 + durabletask/internal/entity_state_shim.py | 3 + durabletask/internal/exceptions.py | 3 + .../internal/json_encode_output_exception.py | 3 + .../internal/orchestration_entity_context.py | 3 + .../proto_task_hub_sidecar_service_stub.py | 3 + examples/activity_sequence.py | 3 + examples/entities/class_based_entity.py | 3 + .../entities/class_based_entity_actions.py | 3 + examples/entities/entity_locking.py | 3 + examples/entities/function_based_entity.py | 3 + .../entities/function_based_entity_actions.py | 3 + examples/fanout_fanin.py | 3 + examples/human_interaction.py | 3 + examples/sandboxes/main_app.py | 3 + examples/sandboxes/remote_worker.py | 3 + .../orchestrator.py | 3 + .../worker.py | 3 + examples/version_aware_orchestrator.py | 3 + examples/work_item_filtering.py | 3 + tests/__init__.py | 2 + tests/durabletask-azuremanaged/__init__.py | 2 + .../entities/__init__.py | 2 + .../test_dts_class_based_entities_e2e.py | 3 + .../test_dts_entity_failure_handling.py | 2 + .../test_dts_function_based_entities_e2e.py | 3 + .../test_dts_activity_sequence.py | 3 + .../test_dts_batch_actions.py | 2 + .../entities/test_entity_id_parsing.py | 3 + tests/durabletask/extensions/__init__.py | 2 + .../extensions/history_export/__init__.py | 2 + tests/durabletask/test_client.py | 3 + tests/durabletask/test_entity_executor.py | 3 + .../test_worker_concurrency_loop.py | 3 + .../test_worker_concurrency_loop_async.py | 3 + tests/durabletask/test_worker_resiliency.py | 3 + 45 files changed, 419 insertions(+) create mode 100644 azure-functions-durable/azure/durable_functions/decorators/durable_app.py diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 86e164a3..52e5b60d 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -36,6 +36,21 @@ Examples: - Follow PEP 8 conventions. - Use `autopep8` for Python formatting. +## Copyright Headers + +Every new Python (`.py`) source file MUST begin with the following copyright +header as the first two lines, followed by a blank line before any code, +docstring, or imports: + +```python +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +``` + +This applies to all hand-written Python files, including `__init__.py` files, +tests, and examples. The only exceptions are auto-generated protobuf files +(`*_pb2.py` and `*_pb2_grpc.py`), which carry their own generated header. + ## Python Type Checking Before linting, check for and fix any Pylance errors in the files you diff --git a/azure-functions-durable/azure/durable_functions/decorators/durable_app.py b/azure-functions-durable/azure/durable_functions/decorators/durable_app.py new file mode 100644 index 00000000..dd17bb2b --- /dev/null +++ b/azure-functions-durable/azure/durable_functions/decorators/durable_app.py @@ -0,0 +1,285 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +from functools import wraps +from typing import Any, Callable, Optional, Union + +from azure.functions import FunctionRegister, TriggerApi, BindingApi, AuthLevel +from azure.functions.decorators.function_app import FunctionBuilder + +from durabletask import task + +from .metadata import OrchestrationTrigger, ActivityTrigger, EntityTrigger, \ + DurableClient +from ..worker import DurableFunctionsWorker +from ..orchestrator import Orchestrator + + +class Blueprint(TriggerApi, BindingApi): + """Durable Functions (DF) Blueprint container. + + It allows functions to be declared via trigger and binding decorators, + but does not automatically index/register these functions. + + To register these functions, utilize the `register_functions` method from any + :class:`FunctionRegister` subclass, such as `DFApp`. + """ + + def __init__(self, + http_auth_level: Union[AuthLevel, str] = AuthLevel.FUNCTION): + """Instantiate a Durable Functions app with which to register Functions. + + Parameters + ---------- + http_auth_level: Union[AuthLevel, str] + Authorization level required for Function invocation. + Defaults to AuthLevel.Function. + + Returns + ------- + DFApp + New instance of a Durable Functions app + """ + # The next-in-MRO base (``DecoratorApi.__init__``) is declared with + # untyped ``*args``/``**kwargs``, so pyright cannot see this call's type. + super().__init__(auth_level=http_auth_level) # pyright: ignore[reportUnknownMemberType] + + def _configure_orchestrator_callable( + self, + wrap: Callable[[Callable[..., Any]], FunctionBuilder] + ) -> Callable[[task.Orchestrator[Any, Any]], FunctionBuilder]: + """Obtain decorator to construct an Orchestrator class from a user-defined Function. + + Parameters + ---------- + wrap: Callable + The next decorator to be applied. + + Returns + ------- + Callable + The function to construct an Orchestrator class from the user-defined Function, + wrapped by the next decorator in the sequence. + """ + def decorator(orchestrator_func: task.Orchestrator[Any, Any]) -> FunctionBuilder: + # Construct an orchestrator based on the end-user code + + handle = Orchestrator.create(orchestrator_func) + + # invoke next decorator, with the Orchestrator as input + handle.__name__ = orchestrator_func.__name__ + return wrap(handle) + + return decorator + + def _configure_entity_callable( + self, + wrap: Callable[[Callable[..., Any]], FunctionBuilder] + ) -> Callable[[task.Entity[Any, Any]], FunctionBuilder]: + """Obtain decorator to construct an Entity class from a user-defined Function. + + Parameters + ---------- + wrap: Callable + The next decorator to be applied. + + Returns + ------- + Callable + The function to construct an Entity class from the user-defined Function, + wrapped by the next decorator in the sequence. + """ + def decorator(entity_func: task.Entity[Any, Any]) -> FunctionBuilder: + # Construct an orchestrator based on the end-user code + + # TODO: Because this handle method is the one actually exposed to the Functions SDK decorator, + # the parameter name will always be "context" here, even if the user specified a different name. + # We need to find a way to allow custom context names (like "ctx"). + def handle(context: Any) -> str: + return DurableFunctionsWorker().execute_entity_batch_request(entity_func, context) + + handle.entity_function = entity_func # pyright: ignore[reportFunctionMemberAccess] + + # invoke next decorator, with the Entity as input + handle.__name__ = entity_func.__name__ + return wrap(handle) + + return decorator + + def _add_rich_client( + self, + fb: FunctionBuilder, + parameter_name: str, + client_constructor: Callable[[Any], Any] + ) -> None: + # Obtain user-code and force type annotation on the client-binding parameter to be `str`. + # This ensures a passing type-check of that specific parameter, + # circumventing a limitation of the worker in type-checking rich DF Client objects. + # TODO: Once rich-binding type checking is possible, remove the annotation change. + # ``FunctionBuilder._function`` and ``Function._func`` are private to + # azure-functions with no public accessor for mutating the wrapped + # user function. Holding it as ``Any`` keeps the single private-access + # waiver here rather than spreading it across each ``._func`` use. + function_obj: Any = fb._function # pyright: ignore[reportPrivateUsage] + user_code = function_obj._func + user_code.__annotations__[parameter_name] = str + + # `wraps` This ensures we re-export the same method-signature as the decorated method + @wraps(user_code) + async def df_client_middleware(*args: Any, **kwargs: Any) -> Any: + + # Obtain JSON-string currently passed as DF Client, + # construct rich object from it, + # and assign parameter to that rich object + starter = kwargs[parameter_name] + client = client_constructor(starter) + kwargs[parameter_name] = client + + # Invoke user code with rich DF Client binding + return await user_code(*args, **kwargs) + + # TODO: Is there a better way to support retrieving the unwrapped user code? + df_client_middleware.client_function = function_obj._func # pyright: ignore[reportAttributeAccessIssue] + + function_obj._func = df_client_middleware + + def _build_function( + self, + wrap: Callable[[FunctionBuilder], FunctionBuilder] + ) -> Callable[[Callable[..., Any]], FunctionBuilder]: + """Typed equivalent of the base ``_configure_function_builder``. + + The inherited method is untyped, which would otherwise propagate + ``Unknown`` types through every decorator below. This mirrors its + behaviour exactly using the typed protected members it relies on. + """ + def decorator(func: Callable[..., Any]) -> FunctionBuilder: + fb = self._validate_type(func) + self._function_builders.append(fb) + return wrap(fb) + + return decorator + + def orchestration_trigger(self, context_name: str, + orchestration: Optional[str] = None + ) -> Callable[[task.Orchestrator[Any, Any]], FunctionBuilder]: + """Register an Orchestrator Function. + + Parameters + ---------- + context_name: str + Parameter name of the DurableOrchestrationContext object. + orchestration: Optional[str] + Name of Orchestrator Function. + The value is None by default, in which case the name of the method is used. + """ + @self._configure_orchestrator_callable + @self._build_function + def wrap(fb: FunctionBuilder) -> FunctionBuilder: + + def decorator() -> FunctionBuilder: + fb.add_trigger( + trigger=OrchestrationTrigger(name=context_name, + orchestration=orchestration)) + return fb + + return decorator() + + return wrap + + def activity_trigger(self, input_name: str, + activity: Optional[str] = None + ) -> Callable[[Callable[..., Any]], FunctionBuilder]: + """Register an Activity Function. + + Parameters + ---------- + input_name: str + Parameter name of the Activity input. + activity: Optional[str] + Name of Activity Function. + The value is None by default, in which case the name of the method is used. + """ + @self._build_function + def wrap(fb: FunctionBuilder) -> FunctionBuilder: + def decorator() -> FunctionBuilder: + fb.add_trigger( + trigger=ActivityTrigger(name=input_name, + activity=activity)) + return fb + + return decorator() + + return wrap + + def entity_trigger(self, + context_name: str, + entity_name: Optional[str] = None + ) -> Callable[[task.Entity[Any, Any]], FunctionBuilder]: + """Register an Entity Function. + + Parameters + ---------- + context_name: str + Parameter name of the Entity input. + entity_name: Optional[str] + Name of Entity Function. + The value is None by default, in which case the name of the method is used. + """ + @self._configure_entity_callable + @self._build_function + def wrap(fb: FunctionBuilder) -> FunctionBuilder: + def decorator() -> FunctionBuilder: + fb.add_trigger( + trigger=EntityTrigger(name=context_name, + entity_name=entity_name)) + return fb + + return decorator() + + return wrap + + def durable_client_input(self, + client_name: str, + task_hub: Optional[str] = None, + connection_name: Optional[str] = None + ) -> Callable[[Callable[..., Any]], FunctionBuilder]: + """Register a Durable-client Function. + + Parameters + ---------- + client_name: str + Parameter name of durable client. + task_hub: Optional[str] + Used in scenarios where multiple function apps share the same storage account + but need to be isolated from each other. If not specified, the default value + from host.json is used. + This value must match the value used by the target orchestrator functions. + connection_name: Optional[str] + The name of an app setting that contains a storage account connection string. + The storage account represented by this connection string must be the same one + used by the target orchestrator functions. If not specified, the default storage + account connection string for the function app is used. + """ + + @self._build_function + def wrap(fb: FunctionBuilder) -> FunctionBuilder: + def decorator() -> FunctionBuilder: + fb.add_binding( + binding=DurableClient(name=client_name, + task_hub=task_hub, + connection_name=connection_name)) + return fb + + return decorator() + + return wrap + + +class DFApp(Blueprint, FunctionRegister): + """Durable Functions (DF) app. + + Exports the decorators required to declare and index DF Function-types. + """ + + pass diff --git a/durabletask-azuremanaged/__init__.py b/durabletask-azuremanaged/__init__.py index e69de29b..59e481eb 100644 --- a/durabletask-azuremanaged/__init__.py +++ b/durabletask-azuremanaged/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. diff --git a/durabletask-azuremanaged/durabletask/azuremanaged/__init__.py b/durabletask-azuremanaged/durabletask/azuremanaged/__init__.py index e69de29b..59e481eb 100644 --- a/durabletask-azuremanaged/durabletask/azuremanaged/__init__.py +++ b/durabletask-azuremanaged/durabletask/azuremanaged/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. diff --git a/durabletask/entities/durable_entity.py b/durabletask/entities/durable_entity.py index 2963a188..b93b21af 100644 --- a/durabletask/entities/durable_entity.py +++ b/durabletask/entities/durable_entity.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + from typing import Any, TypeVar, overload from durabletask.entities.entity_context import EntityContext diff --git a/durabletask/entities/entity_context.py b/durabletask/entities/entity_context.py index ba2e18e6..03ece715 100644 --- a/durabletask/entities/entity_context.py +++ b/durabletask/entities/entity_context.py @@ -1,3 +1,5 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. from typing import Any, TypeVar, overload import uuid diff --git a/durabletask/entities/entity_instance_id.py b/durabletask/entities/entity_instance_id.py index dc2340d7..9c984877 100644 --- a/durabletask/entities/entity_instance_id.py +++ b/durabletask/entities/entity_instance_id.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + class EntityInstanceId: def __init__(self, entity: str, key: str): EntityInstanceId.validate_entity_name(entity) diff --git a/durabletask/entities/entity_lock.py b/durabletask/entities/entity_lock.py index 0b237016..87598964 100644 --- a/durabletask/entities/entity_lock.py +++ b/durabletask/entities/entity_lock.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + from __future__ import annotations from typing import TYPE_CHECKING diff --git a/durabletask/entities/entity_metadata.py b/durabletask/entities/entity_metadata.py index fbbea000..a2ed2191 100644 --- a/durabletask/entities/entity_metadata.py +++ b/durabletask/entities/entity_metadata.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + from datetime import datetime, timezone from typing import Any, TypeVar, overload from durabletask.entities.entity_instance_id import EntityInstanceId diff --git a/durabletask/entities/entity_operation_failed_exception.py b/durabletask/entities/entity_operation_failed_exception.py index a69094ee..e9ebb581 100644 --- a/durabletask/entities/entity_operation_failed_exception.py +++ b/durabletask/entities/entity_operation_failed_exception.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + from durabletask.internal.orchestrator_service_pb2 import TaskFailureDetails from durabletask.entities.entity_instance_id import EntityInstanceId diff --git a/durabletask/internal/entity_state_shim.py b/durabletask/internal/entity_state_shim.py index 130de029..6d6fd25b 100644 --- a/durabletask/internal/entity_state_shim.py +++ b/durabletask/internal/entity_state_shim.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + from typing import Any, TypeVar, overload import durabletask.internal.orchestrator_service_pb2 as pb diff --git a/durabletask/internal/exceptions.py b/durabletask/internal/exceptions.py index 30273a64..276cbe60 100644 --- a/durabletask/internal/exceptions.py +++ b/durabletask/internal/exceptions.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + import durabletask.internal.orchestrator_service_pb2 as pb diff --git a/durabletask/internal/json_encode_output_exception.py b/durabletask/internal/json_encode_output_exception.py index c6c9cc4e..29192c15 100644 --- a/durabletask/internal/json_encode_output_exception.py +++ b/durabletask/internal/json_encode_output_exception.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + from typing import Any diff --git a/durabletask/internal/orchestration_entity_context.py b/durabletask/internal/orchestration_entity_context.py index f4d5d1d5..e47d2ab5 100644 --- a/durabletask/internal/orchestration_entity_context.py +++ b/durabletask/internal/orchestration_entity_context.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + from collections.abc import Generator from datetime import datetime from typing import Any diff --git a/durabletask/internal/proto_task_hub_sidecar_service_stub.py b/durabletask/internal/proto_task_hub_sidecar_service_stub.py index de5a4d5e..d907a4e7 100644 --- a/durabletask/internal/proto_task_hub_sidecar_service_stub.py +++ b/durabletask/internal/proto_task_hub_sidecar_service_stub.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + from collections.abc import Callable from typing import Any, Protocol diff --git a/examples/activity_sequence.py b/examples/activity_sequence.py index b4b92ea7..d031c5c5 100644 --- a/examples/activity_sequence.py +++ b/examples/activity_sequence.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + """End-to-end sample that demonstrates how to configure an orchestrator that calls an activity function in a sequence and prints the outputs.""" import logging diff --git a/examples/entities/class_based_entity.py b/examples/entities/class_based_entity.py index e1b581d5..82749b1b 100644 --- a/examples/entities/class_based_entity.py +++ b/examples/entities/class_based_entity.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + """End-to-end sample that demonstrates how to configure an orchestrator that calls an activity function in a sequence and prints the outputs.""" import os diff --git a/examples/entities/class_based_entity_actions.py b/examples/entities/class_based_entity_actions.py index 5240bb75..8c397673 100644 --- a/examples/entities/class_based_entity_actions.py +++ b/examples/entities/class_based_entity_actions.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + """End-to-end sample that demonstrates how to configure an orchestrator that calls an activity function in a sequence and prints the outputs.""" import os diff --git a/examples/entities/entity_locking.py b/examples/entities/entity_locking.py index c6c1bda9..11d52f8f 100644 --- a/examples/entities/entity_locking.py +++ b/examples/entities/entity_locking.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + """End-to-end sample that demonstrates how to configure an orchestrator that calls an activity function in a sequence and prints the outputs.""" import os diff --git a/examples/entities/function_based_entity.py b/examples/entities/function_based_entity.py index b99c29b4..7e62e891 100644 --- a/examples/entities/function_based_entity.py +++ b/examples/entities/function_based_entity.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + """End-to-end sample that demonstrates how to configure an orchestrator that calls an activity function in a sequence and prints the outputs.""" import os diff --git a/examples/entities/function_based_entity_actions.py b/examples/entities/function_based_entity_actions.py index edcdbee2..42834b6c 100644 --- a/examples/entities/function_based_entity_actions.py +++ b/examples/entities/function_based_entity_actions.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + """End-to-end sample that demonstrates how to configure an orchestrator that calls an activity function in a sequence and prints the outputs.""" import os diff --git a/examples/fanout_fanin.py b/examples/fanout_fanin.py index 0975d924..3c635d10 100644 --- a/examples/fanout_fanin.py +++ b/examples/fanout_fanin.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + """End-to-end sample that demonstrates how to configure an orchestrator that a dynamic number activity functions in parallel, waits for them all to complete, and prints an aggregate summary of the outputs.""" diff --git a/examples/human_interaction.py b/examples/human_interaction.py index 69131006..c0060b62 100644 --- a/examples/human_interaction.py +++ b/examples/human_interaction.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + """End-to-end sample that demonstrates how to configure an orchestrator that waits for an "approval" event before proceding to the next step. If the approval isn't received within a specified timeout, the order that is diff --git a/examples/sandboxes/main_app.py b/examples/sandboxes/main_app.py index 41f6f609..ff234e76 100644 --- a/examples/sandboxes/main_app.py +++ b/examples/sandboxes/main_app.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + """Declarer app for the Durable Task Scheduler sandbox activities sample.""" import os diff --git a/examples/sandboxes/remote_worker.py b/examples/sandboxes/remote_worker.py index afd22b80..515e391c 100644 --- a/examples/sandboxes/remote_worker.py +++ b/examples/sandboxes/remote_worker.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + """Remote worker image entrypoint for the Durable Task Scheduler sandbox activities sample.""" import os diff --git a/examples/sub-orchestrations-with-fan-out-fan-in/orchestrator.py b/examples/sub-orchestrations-with-fan-out-fan-in/orchestrator.py index eef2edc6..c8cfe648 100644 --- a/examples/sub-orchestrations-with-fan-out-fan-in/orchestrator.py +++ b/examples/sub-orchestrations-with-fan-out-fan-in/orchestrator.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + import os from azure.identity import DefaultAzureCredential from durabletask import client diff --git a/examples/sub-orchestrations-with-fan-out-fan-in/worker.py b/examples/sub-orchestrations-with-fan-out-fan-in/worker.py index 45620cda..df2e61f2 100644 --- a/examples/sub-orchestrations-with-fan-out-fan-in/worker.py +++ b/examples/sub-orchestrations-with-fan-out-fan-in/worker.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + import os import random import time diff --git a/examples/version_aware_orchestrator.py b/examples/version_aware_orchestrator.py index b0af11a4..a8fc7ef6 100644 --- a/examples/version_aware_orchestrator.py +++ b/examples/version_aware_orchestrator.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + """End-to-end sample that demonstrates how to configure an orchestrator that a dynamic number activity functions in parallel, waits for them all to complete, and prints an aggregate summary of the outputs.""" diff --git a/examples/work_item_filtering.py b/examples/work_item_filtering.py index 8bccb7f9..06933196 100644 --- a/examples/work_item_filtering.py +++ b/examples/work_item_filtering.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + """End-to-end sample that demonstrates how to use work item filters to control which orchestrations and activities a worker processes.""" import os diff --git a/tests/__init__.py b/tests/__init__.py index e69de29b..59e481eb 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. diff --git a/tests/durabletask-azuremanaged/__init__.py b/tests/durabletask-azuremanaged/__init__.py index e69de29b..59e481eb 100644 --- a/tests/durabletask-azuremanaged/__init__.py +++ b/tests/durabletask-azuremanaged/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. diff --git a/tests/durabletask-azuremanaged/entities/__init__.py b/tests/durabletask-azuremanaged/entities/__init__.py index e69de29b..59e481eb 100644 --- a/tests/durabletask-azuremanaged/entities/__init__.py +++ b/tests/durabletask-azuremanaged/entities/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. diff --git a/tests/durabletask-azuremanaged/entities/test_dts_class_based_entities_e2e.py b/tests/durabletask-azuremanaged/entities/test_dts_class_based_entities_e2e.py index 0910f88e..ff6522f2 100644 --- a/tests/durabletask-azuremanaged/entities/test_dts_class_based_entities_e2e.py +++ b/tests/durabletask-azuremanaged/entities/test_dts_class_based_entities_e2e.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + from datetime import datetime, timezone import os import time diff --git a/tests/durabletask-azuremanaged/entities/test_dts_entity_failure_handling.py b/tests/durabletask-azuremanaged/entities/test_dts_entity_failure_handling.py index b12d158e..5cc1d444 100644 --- a/tests/durabletask-azuremanaged/entities/test_dts_entity_failure_handling.py +++ b/tests/durabletask-azuremanaged/entities/test_dts_entity_failure_handling.py @@ -1,3 +1,5 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. import json import os diff --git a/tests/durabletask-azuremanaged/entities/test_dts_function_based_entities_e2e.py b/tests/durabletask-azuremanaged/entities/test_dts_function_based_entities_e2e.py index 2fbf2018..11d61a92 100644 --- a/tests/durabletask-azuremanaged/entities/test_dts_function_based_entities_e2e.py +++ b/tests/durabletask-azuremanaged/entities/test_dts_function_based_entities_e2e.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + from datetime import datetime, timezone import os import time diff --git a/tests/durabletask-azuremanaged/test_dts_activity_sequence.py b/tests/durabletask-azuremanaged/test_dts_activity_sequence.py index 1a685d0d..3f9f5e64 100644 --- a/tests/durabletask-azuremanaged/test_dts_activity_sequence.py +++ b/tests/durabletask-azuremanaged/test_dts_activity_sequence.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + """End-to-end sample that demonstrates how to configure an orchestrator that calls an activity function in a sequence and prints the outputs.""" import os diff --git a/tests/durabletask-azuremanaged/test_dts_batch_actions.py b/tests/durabletask-azuremanaged/test_dts_batch_actions.py index e709c412..97167b98 100644 --- a/tests/durabletask-azuremanaged/test_dts_batch_actions.py +++ b/tests/durabletask-azuremanaged/test_dts_batch_actions.py @@ -1,3 +1,5 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. import asyncio import logging diff --git a/tests/durabletask/entities/test_entity_id_parsing.py b/tests/durabletask/entities/test_entity_id_parsing.py index c56a8787..77bb339e 100644 --- a/tests/durabletask/entities/test_entity_id_parsing.py +++ b/tests/durabletask/entities/test_entity_id_parsing.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + import pytest from durabletask.entities import EntityInstanceId diff --git a/tests/durabletask/extensions/__init__.py b/tests/durabletask/extensions/__init__.py index e69de29b..59e481eb 100644 --- a/tests/durabletask/extensions/__init__.py +++ b/tests/durabletask/extensions/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. diff --git a/tests/durabletask/extensions/history_export/__init__.py b/tests/durabletask/extensions/history_export/__init__.py index e69de29b..59e481eb 100644 --- a/tests/durabletask/extensions/history_export/__init__.py +++ b/tests/durabletask/extensions/history_export/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. diff --git a/tests/durabletask/test_client.py b/tests/durabletask/test_client.py index 2fe48517..5ccd8958 100644 --- a/tests/durabletask/test_client.py +++ b/tests/durabletask/test_client.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + import asyncio import inspect import json diff --git a/tests/durabletask/test_entity_executor.py b/tests/durabletask/test_entity_executor.py index 851edd7d..c3f369df 100644 --- a/tests/durabletask/test_entity_executor.py +++ b/tests/durabletask/test_entity_executor.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + """Unit tests for the _EntityExecutor class in durabletask.worker.""" import logging diff --git a/tests/durabletask/test_worker_concurrency_loop.py b/tests/durabletask/test_worker_concurrency_loop.py index 199c2198..e7f802a6 100644 --- a/tests/durabletask/test_worker_concurrency_loop.py +++ b/tests/durabletask/test_worker_concurrency_loop.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + import asyncio import threading import time diff --git a/tests/durabletask/test_worker_concurrency_loop_async.py b/tests/durabletask/test_worker_concurrency_loop_async.py index 22ffe237..686e12b8 100644 --- a/tests/durabletask/test_worker_concurrency_loop_async.py +++ b/tests/durabletask/test_worker_concurrency_loop_async.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + import asyncio from durabletask.worker import ConcurrencyOptions, TaskHubGrpcWorker diff --git a/tests/durabletask/test_worker_resiliency.py b/tests/durabletask/test_worker_resiliency.py index a25529fe..1bc637dc 100644 --- a/tests/durabletask/test_worker_resiliency.py +++ b/tests/durabletask/test_worker_resiliency.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + import asyncio import grpc from threading import Event, Timer From 4d39c32e1a340453b7681f8450ade8a316551ffe Mon Sep 17 00:00:00 2001 From: andystaples <77818326+andystaples@users.noreply.github.com> Date: Thu, 25 Jun 2026 11:21:30 -0600 Subject: [PATCH 2/2] Delete azure-functions-durable/azure/durable_functions/decorators/durable_app.py --- .../decorators/durable_app.py | 285 ------------------ 1 file changed, 285 deletions(-) delete mode 100644 azure-functions-durable/azure/durable_functions/decorators/durable_app.py diff --git a/azure-functions-durable/azure/durable_functions/decorators/durable_app.py b/azure-functions-durable/azure/durable_functions/decorators/durable_app.py deleted file mode 100644 index dd17bb2b..00000000 --- a/azure-functions-durable/azure/durable_functions/decorators/durable_app.py +++ /dev/null @@ -1,285 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. - -from functools import wraps -from typing import Any, Callable, Optional, Union - -from azure.functions import FunctionRegister, TriggerApi, BindingApi, AuthLevel -from azure.functions.decorators.function_app import FunctionBuilder - -from durabletask import task - -from .metadata import OrchestrationTrigger, ActivityTrigger, EntityTrigger, \ - DurableClient -from ..worker import DurableFunctionsWorker -from ..orchestrator import Orchestrator - - -class Blueprint(TriggerApi, BindingApi): - """Durable Functions (DF) Blueprint container. - - It allows functions to be declared via trigger and binding decorators, - but does not automatically index/register these functions. - - To register these functions, utilize the `register_functions` method from any - :class:`FunctionRegister` subclass, such as `DFApp`. - """ - - def __init__(self, - http_auth_level: Union[AuthLevel, str] = AuthLevel.FUNCTION): - """Instantiate a Durable Functions app with which to register Functions. - - Parameters - ---------- - http_auth_level: Union[AuthLevel, str] - Authorization level required for Function invocation. - Defaults to AuthLevel.Function. - - Returns - ------- - DFApp - New instance of a Durable Functions app - """ - # The next-in-MRO base (``DecoratorApi.__init__``) is declared with - # untyped ``*args``/``**kwargs``, so pyright cannot see this call's type. - super().__init__(auth_level=http_auth_level) # pyright: ignore[reportUnknownMemberType] - - def _configure_orchestrator_callable( - self, - wrap: Callable[[Callable[..., Any]], FunctionBuilder] - ) -> Callable[[task.Orchestrator[Any, Any]], FunctionBuilder]: - """Obtain decorator to construct an Orchestrator class from a user-defined Function. - - Parameters - ---------- - wrap: Callable - The next decorator to be applied. - - Returns - ------- - Callable - The function to construct an Orchestrator class from the user-defined Function, - wrapped by the next decorator in the sequence. - """ - def decorator(orchestrator_func: task.Orchestrator[Any, Any]) -> FunctionBuilder: - # Construct an orchestrator based on the end-user code - - handle = Orchestrator.create(orchestrator_func) - - # invoke next decorator, with the Orchestrator as input - handle.__name__ = orchestrator_func.__name__ - return wrap(handle) - - return decorator - - def _configure_entity_callable( - self, - wrap: Callable[[Callable[..., Any]], FunctionBuilder] - ) -> Callable[[task.Entity[Any, Any]], FunctionBuilder]: - """Obtain decorator to construct an Entity class from a user-defined Function. - - Parameters - ---------- - wrap: Callable - The next decorator to be applied. - - Returns - ------- - Callable - The function to construct an Entity class from the user-defined Function, - wrapped by the next decorator in the sequence. - """ - def decorator(entity_func: task.Entity[Any, Any]) -> FunctionBuilder: - # Construct an orchestrator based on the end-user code - - # TODO: Because this handle method is the one actually exposed to the Functions SDK decorator, - # the parameter name will always be "context" here, even if the user specified a different name. - # We need to find a way to allow custom context names (like "ctx"). - def handle(context: Any) -> str: - return DurableFunctionsWorker().execute_entity_batch_request(entity_func, context) - - handle.entity_function = entity_func # pyright: ignore[reportFunctionMemberAccess] - - # invoke next decorator, with the Entity as input - handle.__name__ = entity_func.__name__ - return wrap(handle) - - return decorator - - def _add_rich_client( - self, - fb: FunctionBuilder, - parameter_name: str, - client_constructor: Callable[[Any], Any] - ) -> None: - # Obtain user-code and force type annotation on the client-binding parameter to be `str`. - # This ensures a passing type-check of that specific parameter, - # circumventing a limitation of the worker in type-checking rich DF Client objects. - # TODO: Once rich-binding type checking is possible, remove the annotation change. - # ``FunctionBuilder._function`` and ``Function._func`` are private to - # azure-functions with no public accessor for mutating the wrapped - # user function. Holding it as ``Any`` keeps the single private-access - # waiver here rather than spreading it across each ``._func`` use. - function_obj: Any = fb._function # pyright: ignore[reportPrivateUsage] - user_code = function_obj._func - user_code.__annotations__[parameter_name] = str - - # `wraps` This ensures we re-export the same method-signature as the decorated method - @wraps(user_code) - async def df_client_middleware(*args: Any, **kwargs: Any) -> Any: - - # Obtain JSON-string currently passed as DF Client, - # construct rich object from it, - # and assign parameter to that rich object - starter = kwargs[parameter_name] - client = client_constructor(starter) - kwargs[parameter_name] = client - - # Invoke user code with rich DF Client binding - return await user_code(*args, **kwargs) - - # TODO: Is there a better way to support retrieving the unwrapped user code? - df_client_middleware.client_function = function_obj._func # pyright: ignore[reportAttributeAccessIssue] - - function_obj._func = df_client_middleware - - def _build_function( - self, - wrap: Callable[[FunctionBuilder], FunctionBuilder] - ) -> Callable[[Callable[..., Any]], FunctionBuilder]: - """Typed equivalent of the base ``_configure_function_builder``. - - The inherited method is untyped, which would otherwise propagate - ``Unknown`` types through every decorator below. This mirrors its - behaviour exactly using the typed protected members it relies on. - """ - def decorator(func: Callable[..., Any]) -> FunctionBuilder: - fb = self._validate_type(func) - self._function_builders.append(fb) - return wrap(fb) - - return decorator - - def orchestration_trigger(self, context_name: str, - orchestration: Optional[str] = None - ) -> Callable[[task.Orchestrator[Any, Any]], FunctionBuilder]: - """Register an Orchestrator Function. - - Parameters - ---------- - context_name: str - Parameter name of the DurableOrchestrationContext object. - orchestration: Optional[str] - Name of Orchestrator Function. - The value is None by default, in which case the name of the method is used. - """ - @self._configure_orchestrator_callable - @self._build_function - def wrap(fb: FunctionBuilder) -> FunctionBuilder: - - def decorator() -> FunctionBuilder: - fb.add_trigger( - trigger=OrchestrationTrigger(name=context_name, - orchestration=orchestration)) - return fb - - return decorator() - - return wrap - - def activity_trigger(self, input_name: str, - activity: Optional[str] = None - ) -> Callable[[Callable[..., Any]], FunctionBuilder]: - """Register an Activity Function. - - Parameters - ---------- - input_name: str - Parameter name of the Activity input. - activity: Optional[str] - Name of Activity Function. - The value is None by default, in which case the name of the method is used. - """ - @self._build_function - def wrap(fb: FunctionBuilder) -> FunctionBuilder: - def decorator() -> FunctionBuilder: - fb.add_trigger( - trigger=ActivityTrigger(name=input_name, - activity=activity)) - return fb - - return decorator() - - return wrap - - def entity_trigger(self, - context_name: str, - entity_name: Optional[str] = None - ) -> Callable[[task.Entity[Any, Any]], FunctionBuilder]: - """Register an Entity Function. - - Parameters - ---------- - context_name: str - Parameter name of the Entity input. - entity_name: Optional[str] - Name of Entity Function. - The value is None by default, in which case the name of the method is used. - """ - @self._configure_entity_callable - @self._build_function - def wrap(fb: FunctionBuilder) -> FunctionBuilder: - def decorator() -> FunctionBuilder: - fb.add_trigger( - trigger=EntityTrigger(name=context_name, - entity_name=entity_name)) - return fb - - return decorator() - - return wrap - - def durable_client_input(self, - client_name: str, - task_hub: Optional[str] = None, - connection_name: Optional[str] = None - ) -> Callable[[Callable[..., Any]], FunctionBuilder]: - """Register a Durable-client Function. - - Parameters - ---------- - client_name: str - Parameter name of durable client. - task_hub: Optional[str] - Used in scenarios where multiple function apps share the same storage account - but need to be isolated from each other. If not specified, the default value - from host.json is used. - This value must match the value used by the target orchestrator functions. - connection_name: Optional[str] - The name of an app setting that contains a storage account connection string. - The storage account represented by this connection string must be the same one - used by the target orchestrator functions. If not specified, the default storage - account connection string for the function app is used. - """ - - @self._build_function - def wrap(fb: FunctionBuilder) -> FunctionBuilder: - def decorator() -> FunctionBuilder: - fb.add_binding( - binding=DurableClient(name=client_name, - task_hub=task_hub, - connection_name=connection_name)) - return fb - - return decorator() - - return wrap - - -class DFApp(Blueprint, FunctionRegister): - """Durable Functions (DF) app. - - Exports the decorators required to declare and index DF Function-types. - """ - - pass