11# Copyright (c) Microsoft Corporation. All rights reserved.
22# Licensed under the MIT License.
3+ import base64
4+ from functools import wraps
5+
6+ from durabletask .internal .orchestrator_service_pb2 import OrchestratorRequest , OrchestratorResponse
37from .metadata import OrchestrationTrigger , ActivityTrigger , EntityTrigger , \
48 DurableClient
59from typing import Callable , Optional
610from typing import Union
7- from azure .functions import FunctionRegister , TriggerApi , BindingApi , AuthLevel , OrchestrationContext
11+ from azure .functions import FunctionRegister , TriggerApi , BindingApi , AuthLevel
12+
13+ # TODO: Use __init__.py to optimize imports
14+ from durabletask .azurefunctions .client import DurableFunctionsClient
15+ from durabletask .azurefunctions .worker import DurableFunctionsWorker
16+ from durabletask .azurefunctions .internal .azurefunctions_null_stub import AzureFunctionsNullStub
817
918
1019class Blueprint (TriggerApi , BindingApi ):
@@ -37,9 +46,6 @@ def __init__(self,
3746 def _configure_orchestrator_callable (self , wrap ) -> Callable :
3847 """Obtain decorator to construct an Orchestrator class from a user-defined Function.
3948
40- In the old programming model, this decorator's logic was unavoidable boilerplate
41- in user-code. Now, this is handled internally by the framework.
42-
4349 Parameters
4450 ----------
4551 wrap: Callable
@@ -54,14 +60,31 @@ def _configure_orchestrator_callable(self, wrap) -> Callable:
5460 def decorator (orchestrator_func ):
5561 # Construct an orchestrator based on the end-user code
5662
57- # TODO: Extract this logic (?)
58- def handle (context : OrchestrationContext ) -> str :
63+ # TODO: Move this logic somewhere better
64+ def handle (context ) -> str :
5965 context_body = getattr (context , "body" , None )
6066 if context_body is None :
6167 context_body = context
6268 orchestration_context = context_body
63- # TODO: Run the orchestration using the context
64- return ""
69+ request = OrchestratorRequest ()
70+ request .ParseFromString (base64 .b64decode (orchestration_context ))
71+ stub = AzureFunctionsNullStub ()
72+ worker = DurableFunctionsWorker ()
73+ response : Optional [OrchestratorResponse ] = None
74+
75+ def stub_complete (stub_response ):
76+ nonlocal response
77+ response = stub_response
78+ stub .CompleteOrchestratorTask = stub_complete
79+ execution_started_events = [e for e in [e1 for e1 in request .newEvents ] + [e2 for e2 in request .pastEvents ] if e .HasField ("executionStarted" )]
80+ function_name = execution_started_events [- 1 ].executionStarted .name
81+ worker .add_named_orchestrator (function_name , orchestrator_func )
82+ worker ._execute_orchestrator (request , stub , None )
83+
84+ if response is None :
85+ raise Exception ("Orchestrator execution did not produce a response." )
86+ # The Python worker returns the input as type "json", so double-encoding is necessary
87+ return '"' + base64 .b64encode (response .SerializeToString ()).decode ('utf-8' ) + '"'
6588
6689 handle .orchestrator_function = orchestrator_func
6790
@@ -71,6 +94,55 @@ def handle(context: OrchestrationContext) -> str:
7194
7295 return decorator
7396
97+ def _configure_entity_callable (self , wrap ) -> Callable :
98+ """Obtain decorator to construct an Entity class from a user-defined Function.
99+
100+ Parameters
101+ ----------
102+ wrap: Callable
103+ The next decorator to be applied.
104+
105+ Returns
106+ -------
107+ Callable
108+ The function to construct an Entity class from the user-defined Function,
109+ wrapped by the next decorator in the sequence.
110+ """
111+ def decorator (entity_func ):
112+ # TODO: Implement entity support - similar to orchestrators (?)
113+ raise NotImplementedError ()
114+
115+ return decorator
116+
117+ def _add_rich_client (self , fb , parameter_name ,
118+ client_constructor ):
119+ # Obtain user-code and force type annotation on the client-binding parameter to be `str`.
120+ # This ensures a passing type-check of that specific parameter,
121+ # circumventing a limitation of the worker in type-checking rich DF Client objects.
122+ # TODO: Once rich-binding type checking is possible, remove the annotation change.
123+ user_code = fb ._function ._func
124+ user_code .__annotations__ [parameter_name ] = str
125+
126+ # `wraps` This ensures we re-export the same method-signature as the decorated method
127+ @wraps (user_code )
128+ async def df_client_middleware (* args , ** kwargs ):
129+
130+ # Obtain JSON-string currently passed as DF Client,
131+ # construct rich object from it,
132+ # and assign parameter to that rich object
133+ starter = kwargs [parameter_name ]
134+ client = client_constructor (starter )
135+ kwargs [parameter_name ] = client
136+
137+ # Invoke user code with rich DF Client binding
138+ return await user_code (* args , ** kwargs )
139+
140+ # TODO: Is there a better way to support retrieving the unwrapped user code?
141+ df_client_middleware .client_function = fb ._function ._func # type: ignore
142+
143+ user_code_with_rich_client = df_client_middleware
144+ fb ._function ._func = user_code_with_rich_client
145+
74146 def orchestration_trigger (self , context_name : str ,
75147 orchestration : Optional [str ] = None ):
76148 """Register an Orchestrator Function.
@@ -133,6 +205,7 @@ def entity_trigger(self, context_name: str,
133205 Name of Entity Function.
134206 The value is None by default, in which case the name of the method is used.
135207 """
208+ @self ._configure_entity_callable
136209 @self ._configure_function_builder
137210 def wrap (fb ):
138211 def decorator ():
@@ -171,7 +244,7 @@ def durable_client_input(self,
171244 @self ._configure_function_builder
172245 def wrap (fb ):
173246 def decorator ():
174- # self._add_rich_client(fb, client_name, DurableOrchestrationClient )
247+ self ._add_rich_client (fb , client_name , DurableFunctionsClient )
175248
176249 fb .add_binding (
177250 binding = DurableClient (name = client_name ,
0 commit comments