Skip to content

Add Durable Decorators directly to Python library #207

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Apr 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions azure/functions/decorators/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,7 @@
DAPR_INVOKE = "daprInvoke"
DAPR_PUBLISH = "daprPublish"
DAPR_BINDING = "daprBinding"
ORCHESTRATION_TRIGGER = "orchestrationTrigger"
ACTIVITY_TRIGGER = "activityTrigger"
ENTITY_TRIGGER = "entityTrigger"
DURABLE_CLIENT = "durableClient"
120 changes: 120 additions & 0 deletions azure/functions/decorators/function_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,42 @@ def __init__(self, *args, **kwargs):
self._function_builders: List[FunctionBuilder] = []
self._app_script_file: str = SCRIPT_FILE_NAME

def _invoke_df_decorator(self, df_decorator):
"""
Invoke a Durable Functions decorator from the DF SDK, and store the
resulting :class:`FunctionBuilder` object within the `DecoratorApi`.

"""

@self._configure_function_builder
def wrap(fb):
def decorator():
function_builder = df_decorator(fb._function._func)

# remove old function builder from `self` and replace
# it with the result of the DF decorator
self._function_builders.pop()
self._function_builders.append(function_builder)
return function_builder
return decorator()
return wrap

def _get_durable_blueprint(self):
"""Attempt to import the Durable Functions SDK from which DF decorators are
implemented.
"""

try:
import azure.durable_functions as df
df_bp = df.Blueprint()
return df_bp
except ImportError:
error_message = "Attempted to use a Durable Functions decorator, "\
"but the `azure-functions-durable` SDK package could not be "\
"found. Please install `azure-functions-durable` to use "\
"Durable Functions."
raise Exception(error_message)

@property
def app_script_file(self) -> str:
"""Name of function app script file in which all the functions
Expand Down Expand Up @@ -443,6 +479,59 @@ def decorator():

return wrap

def orchestration_trigger(self, context_name: str,
orchestration: Optional[str] = None):
"""Register an Orchestrator Function.

Parameters
----------
context_name: str
Parameter name of the DurableOrchestrationContext object.
orchestration: Optional[str]
Name of Orchestrator Function.
By default, the name of the method is used.
"""
df_bp = self._get_durable_blueprint()
df_decorator = df_bp.orchestration_trigger(context_name,
orchestration)
result = self._invoke_df_decorator(df_decorator)
return result

def entity_trigger(self, context_name: str,
entity_name: Optional[str] = None):
"""Register an Entity Function.

Parameters
----------
context_name: str
Parameter name of the Entity input.
entity_name: Optional[str]
Name of Entity Function.
"""

df_bp = self._get_durable_blueprint()
df_decorator = df_bp.entity_trigger(context_name,
entity_name)
result = self._invoke_df_decorator(df_decorator)
return result

def activity_trigger(self, input_name: str,
activity: Optional[str] = None):
"""Register an Activity Function.

Parameters
----------
input_name: str
Parameter name of the Activity input.
activity: Optional[str]
Name of Activity Function.
"""

df_bp = self._get_durable_blueprint()
df_decorator = df_bp.activity_trigger(input_name, activity)
result = self._invoke_df_decorator(df_decorator)
return result

def timer_trigger(self,
arg_name: str,
schedule: str,
Expand Down Expand Up @@ -1350,6 +1439,37 @@ def decorator():
class BindingApi(DecoratorApi, ABC):
"""Interface to extend for using existing binding decorator functions."""

def durable_client_input(self,
client_name: str,
task_hub: Optional[str] = None,
connection_name: Optional[str] = None
):
"""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.
"""
df_bp = self._get_durable_blueprint()
df_decorator = df_bp.durable_client_input(client_name,
task_hub,
connection_name)
result = self._invoke_df_decorator(df_decorator)
return result

def service_bus_queue_output(self,
arg_name: str,
connection: str,
Expand Down
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
'pytest',
'pytest-cov',
'requests==2.*',
'coverage'
'coverage',
'azure-functions-durable'
]
}

Expand Down
81 changes: 80 additions & 1 deletion tests/decorators/test_decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
HTTP_OUTPUT, QUEUE, QUEUE_TRIGGER, SERVICE_BUS, SERVICE_BUS_TRIGGER, \
EVENT_HUB, EVENT_HUB_TRIGGER, COSMOS_DB, COSMOS_DB_TRIGGER, BLOB, \
BLOB_TRIGGER, EVENT_GRID_TRIGGER, EVENT_GRID, TABLE, WARMUP_TRIGGER, \
SQL, SQL_TRIGGER
SQL, SQL_TRIGGER, ORCHESTRATION_TRIGGER, ACTIVITY_TRIGGER, \
ENTITY_TRIGGER, DURABLE_CLIENT
from azure.functions.decorators.core import DataType, AuthLevel, \
BindingDirection, AccessRights, Cardinality
from azure.functions.decorators.function_app import FunctionApp
Expand Down Expand Up @@ -160,6 +161,84 @@ def dummy():
]
})

def test_orchestration_trigger(self):
app = self.func_app

@app.orchestration_trigger("context")
def dummy1(context):
pass

func = self._get_user_function(app)
assert_json(self, func, {
"scriptFile": "function_app.py",
"bindings": [
{
"name": "context",
"type": ORCHESTRATION_TRIGGER,
"direction": BindingDirection.IN
}
]
})

def test_activity_trigger(self):
app = self.func_app

@app.activity_trigger("arg")
def dummy2(arg):
pass

func = self._get_user_function(app)
assert_json(self, func, {
"scriptFile": "function_app.py",
"bindings": [
{
"name": "arg",
"type": ACTIVITY_TRIGGER,
"direction": BindingDirection.IN
}
]
})

def test_entity_trigger(self):
app = self.func_app

@app.entity_trigger("context")
def dummy3(context):
pass

func = self._get_user_function(app)
assert_json(self, func, {
"scriptFile": "function_app.py",
"bindings": [
{
"name": "context",
"type": ENTITY_TRIGGER,
"direction": BindingDirection.IN,
}
]
})

def test_durable_client(self):
app = self.func_app

@app.generic_trigger(arg_name="req", type=HTTP_TRIGGER)
@app.durable_client_input(client_name="client")
def dummy(client):
pass

func = self._get_user_function(app)

self.assertEqual(len(func.get_bindings()), 2)
self.assertTrue(func.is_http_function())

output = func.get_bindings()[0]

self.assertEqual(output.get_dict_repr(), {
"direction": BindingDirection.IN,
"type": DURABLE_CLIENT,
"name": "client"
})

def test_route_default_args(self):
app = self.func_app

Expand Down
Loading