Skip to content

Commit a346132

Browse files
authored
Merge branch 'dev' into wangbill/asgi-wsgi-fix
2 parents 2419f37 + 715580f commit a346132

File tree

8 files changed

+227
-6
lines changed

8 files changed

+227
-6
lines changed

CODEOWNERS

+3-1
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,6 @@
99
# AZURE FUNCTIONS TEAM
1010
# For all file changes, github would automatically include the following people in the PRs.
1111
#
12-
* @vrdmr @gavin-aguiar @YunchuWang @pdthummar @hallvictoria
12+
13+
* @vrdmr @gavin-aguiar @YunchuWang @pdthummar @hallvictoria
14+

azure/functions/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -97,4 +97,4 @@
9797
'HttpMethod'
9898
)
9999

100-
__version__ = '1.19.0'
100+
__version__ = '1.20.0b1'

azure/functions/_http_asgi.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import logging
66
import asyncio
77
from asyncio import Event, Queue
8+
from urllib.parse import ParseResult, urlparse
89
from warnings import warn
910
from wsgiref.headers import Headers
1011

@@ -22,6 +23,8 @@ def __init__(self, func_req: HttpRequest,
2223
self.asgi_version = ASGI_VERSION
2324
self.asgi_spec_version = ASGI_SPEC_VERSION
2425
self._headers = func_req.headers
26+
url: ParseResult = urlparse(func_req.url)
27+
self.asgi_url_scheme = url.scheme
2528
super().__init__(func_req, func_ctx)
2629

2730
def _get_encoded_http_headers(self) -> List[Tuple[bytes, bytes]]:
@@ -49,7 +52,7 @@ def to_asgi_http_scope(self):
4952
"asgi.spec_version": self.asgi_spec_version,
5053
"http_version": "1.1",
5154
"method": self.request_method,
52-
"scheme": "https",
55+
"scheme": self.asgi_url_scheme,
5356
"path": self.path_info,
5457
"raw_path": _raw_path,
5558
"query_string": _query_string,

azure/functions/decorators/constants.py

+4
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,7 @@
3030
DAPR_INVOKE = "daprInvoke"
3131
DAPR_PUBLISH = "daprPublish"
3232
DAPR_BINDING = "daprBinding"
33+
ORCHESTRATION_TRIGGER = "orchestrationTrigger"
34+
ACTIVITY_TRIGGER = "activityTrigger"
35+
ENTITY_TRIGGER = "entityTrigger"
36+
DURABLE_CLIENT = "durableClient"

azure/functions/decorators/function_app.py

+120
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,42 @@ def __init__(self, *args, **kwargs):
277277
self._function_builders: List[FunctionBuilder] = []
278278
self._app_script_file: str = SCRIPT_FILE_NAME
279279

280+
def _invoke_df_decorator(self, df_decorator):
281+
"""
282+
Invoke a Durable Functions decorator from the DF SDK, and store the
283+
resulting :class:`FunctionBuilder` object within the `DecoratorApi`.
284+
285+
"""
286+
287+
@self._configure_function_builder
288+
def wrap(fb):
289+
def decorator():
290+
function_builder = df_decorator(fb._function._func)
291+
292+
# remove old function builder from `self` and replace
293+
# it with the result of the DF decorator
294+
self._function_builders.pop()
295+
self._function_builders.append(function_builder)
296+
return function_builder
297+
return decorator()
298+
return wrap
299+
300+
def _get_durable_blueprint(self):
301+
"""Attempt to import the Durable Functions SDK from which DF decorators are
302+
implemented.
303+
"""
304+
305+
try:
306+
import azure.durable_functions as df
307+
df_bp = df.Blueprint()
308+
return df_bp
309+
except ImportError:
310+
error_message = "Attempted to use a Durable Functions decorator, "\
311+
"but the `azure-functions-durable` SDK package could not be "\
312+
"found. Please install `azure-functions-durable` to use "\
313+
"Durable Functions."
314+
raise Exception(error_message)
315+
280316
@property
281317
def app_script_file(self) -> str:
282318
"""Name of function app script file in which all the functions
@@ -443,6 +479,59 @@ def decorator():
443479

444480
return wrap
445481

482+
def orchestration_trigger(self, context_name: str,
483+
orchestration: Optional[str] = None):
484+
"""Register an Orchestrator Function.
485+
486+
Parameters
487+
----------
488+
context_name: str
489+
Parameter name of the DurableOrchestrationContext object.
490+
orchestration: Optional[str]
491+
Name of Orchestrator Function.
492+
By default, the name of the method is used.
493+
"""
494+
df_bp = self._get_durable_blueprint()
495+
df_decorator = df_bp.orchestration_trigger(context_name,
496+
orchestration)
497+
result = self._invoke_df_decorator(df_decorator)
498+
return result
499+
500+
def entity_trigger(self, context_name: str,
501+
entity_name: Optional[str] = None):
502+
"""Register an Entity Function.
503+
504+
Parameters
505+
----------
506+
context_name: str
507+
Parameter name of the Entity input.
508+
entity_name: Optional[str]
509+
Name of Entity Function.
510+
"""
511+
512+
df_bp = self._get_durable_blueprint()
513+
df_decorator = df_bp.entity_trigger(context_name,
514+
entity_name)
515+
result = self._invoke_df_decorator(df_decorator)
516+
return result
517+
518+
def activity_trigger(self, input_name: str,
519+
activity: Optional[str] = None):
520+
"""Register an Activity Function.
521+
522+
Parameters
523+
----------
524+
input_name: str
525+
Parameter name of the Activity input.
526+
activity: Optional[str]
527+
Name of Activity Function.
528+
"""
529+
530+
df_bp = self._get_durable_blueprint()
531+
df_decorator = df_bp.activity_trigger(input_name, activity)
532+
result = self._invoke_df_decorator(df_decorator)
533+
return result
534+
446535
def timer_trigger(self,
447536
arg_name: str,
448537
schedule: str,
@@ -1350,6 +1439,37 @@ def decorator():
13501439
class BindingApi(DecoratorApi, ABC):
13511440
"""Interface to extend for using existing binding decorator functions."""
13521441

1442+
def durable_client_input(self,
1443+
client_name: str,
1444+
task_hub: Optional[str] = None,
1445+
connection_name: Optional[str] = None
1446+
):
1447+
"""Register a Durable-client Function.
1448+
1449+
Parameters
1450+
----------
1451+
client_name: str
1452+
Parameter name of durable client.
1453+
task_hub: Optional[str]
1454+
Used in scenarios where multiple function apps share the
1455+
same storage account but need to be isolated from each other.
1456+
If not specified, the default value from host.json is used.
1457+
This value must match the value used by the target
1458+
orchestrator functions.
1459+
connection_name: Optional[str]
1460+
The name of an app setting that contains a storage account
1461+
connection string. The storage account represented by this
1462+
connection string must be the same one used by the target
1463+
orchestrator functions. If not specified, the default storage
1464+
account connection string for the function app is used.
1465+
"""
1466+
df_bp = self._get_durable_blueprint()
1467+
df_decorator = df_bp.durable_client_input(client_name,
1468+
task_hub,
1469+
connection_name)
1470+
result = self._invoke_df_decorator(df_decorator)
1471+
return result
1472+
13531473
def service_bus_queue_output(self,
13541474
arg_name: str,
13551475
connection: str,

setup.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
'pytest',
1313
'pytest-cov',
1414
'requests==2.*',
15-
'coverage'
15+
'coverage',
16+
'azure-functions-durable'
1617
]
1718
}
1819

tests/decorators/test_decorators.py

+80-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
HTTP_OUTPUT, QUEUE, QUEUE_TRIGGER, SERVICE_BUS, SERVICE_BUS_TRIGGER, \
77
EVENT_HUB, EVENT_HUB_TRIGGER, COSMOS_DB, COSMOS_DB_TRIGGER, BLOB, \
88
BLOB_TRIGGER, EVENT_GRID_TRIGGER, EVENT_GRID, TABLE, WARMUP_TRIGGER, \
9-
SQL, SQL_TRIGGER
9+
SQL, SQL_TRIGGER, ORCHESTRATION_TRIGGER, ACTIVITY_TRIGGER, \
10+
ENTITY_TRIGGER, DURABLE_CLIENT
1011
from azure.functions.decorators.core import DataType, AuthLevel, \
1112
BindingDirection, AccessRights, Cardinality
1213
from azure.functions.decorators.function_app import FunctionApp
@@ -160,6 +161,84 @@ def dummy():
160161
]
161162
})
162163

164+
def test_orchestration_trigger(self):
165+
app = self.func_app
166+
167+
@app.orchestration_trigger("context")
168+
def dummy1(context):
169+
pass
170+
171+
func = self._get_user_function(app)
172+
assert_json(self, func, {
173+
"scriptFile": "function_app.py",
174+
"bindings": [
175+
{
176+
"name": "context",
177+
"type": ORCHESTRATION_TRIGGER,
178+
"direction": BindingDirection.IN
179+
}
180+
]
181+
})
182+
183+
def test_activity_trigger(self):
184+
app = self.func_app
185+
186+
@app.activity_trigger("arg")
187+
def dummy2(arg):
188+
pass
189+
190+
func = self._get_user_function(app)
191+
assert_json(self, func, {
192+
"scriptFile": "function_app.py",
193+
"bindings": [
194+
{
195+
"name": "arg",
196+
"type": ACTIVITY_TRIGGER,
197+
"direction": BindingDirection.IN
198+
}
199+
]
200+
})
201+
202+
def test_entity_trigger(self):
203+
app = self.func_app
204+
205+
@app.entity_trigger("context")
206+
def dummy3(context):
207+
pass
208+
209+
func = self._get_user_function(app)
210+
assert_json(self, func, {
211+
"scriptFile": "function_app.py",
212+
"bindings": [
213+
{
214+
"name": "context",
215+
"type": ENTITY_TRIGGER,
216+
"direction": BindingDirection.IN,
217+
}
218+
]
219+
})
220+
221+
def test_durable_client(self):
222+
app = self.func_app
223+
224+
@app.generic_trigger(arg_name="req", type=HTTP_TRIGGER)
225+
@app.durable_client_input(client_name="client")
226+
def dummy(client):
227+
pass
228+
229+
func = self._get_user_function(app)
230+
231+
self.assertEqual(len(func.get_bindings()), 2)
232+
self.assertTrue(func.is_http_function())
233+
234+
output = func.get_bindings()[0]
235+
236+
self.assertEqual(output.get_dict_repr(), {
237+
"direction": BindingDirection.IN,
238+
"type": DURABLE_CLIENT,
239+
"name": "client"
240+
})
241+
163242
def test_route_default_args(self):
164243
app = self.func_app
165244

tests/test_http_asgi.py

+13-1
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,19 @@ def test_middleware_calls_app(self):
198198
test_body = b'Hello world!'
199199
app.response_body = test_body
200200
app.response_code = 200
201-
req = func.HttpRequest(method='get', url='/test', body=b'')
201+
req = self._generate_func_request()
202+
response = AsgiMiddleware(app).handle(req)
203+
204+
# Verify asserted
205+
self.assertEqual(response.status_code, 200)
206+
self.assertEqual(response.get_body(), test_body)
207+
208+
def test_middleware_calls_app_http(self):
209+
app = MockAsgiApplication()
210+
test_body = b'Hello world!'
211+
app.response_body = test_body
212+
app.response_code = 200
213+
req = self._generate_func_request(url="http://a.b.com")
202214
response = AsgiMiddleware(app).handle(req)
203215

204216
# Verify asserted

0 commit comments

Comments
 (0)