azure-functions-runtime 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- azure_functions_runtime/__init__.py +21 -0
- azure_functions_runtime/bindings/context.py +66 -0
- azure_functions_runtime/bindings/datumdef.py +232 -0
- azure_functions_runtime/bindings/generic.py +75 -0
- azure_functions_runtime/bindings/meta.py +288 -0
- azure_functions_runtime/bindings/nullable_converters.py +126 -0
- azure_functions_runtime/bindings/out.py +15 -0
- azure_functions_runtime/bindings/retrycontext.py +47 -0
- azure_functions_runtime/bindings/tracecontext.py +43 -0
- azure_functions_runtime/functions.py +435 -0
- azure_functions_runtime/handle_event.py +448 -0
- azure_functions_runtime/http_v2.py +291 -0
- azure_functions_runtime/loader.py +219 -0
- azure_functions_runtime/logging.py +16 -0
- azure_functions_runtime/otel.py +116 -0
- azure_functions_runtime/utils/__init__.py +2 -0
- azure_functions_runtime/utils/app_setting_manager.py +102 -0
- azure_functions_runtime/utils/constants.py +63 -0
- azure_functions_runtime/utils/executor.py +34 -0
- azure_functions_runtime/utils/helpers.py +40 -0
- azure_functions_runtime/utils/threadpool.py +88 -0
- azure_functions_runtime/utils/tracing.py +70 -0
- azure_functions_runtime/utils/typing_inspect.py +295 -0
- azure_functions_runtime/utils/validators.py +19 -0
- azure_functions_runtime/utils/wrappers.py +52 -0
- azure_functions_runtime/version.py +4 -0
- azure_functions_runtime-1.0.0.dist-info/METADATA +120 -0
- azure_functions_runtime-1.0.0.dist-info/RECORD +30 -0
- azure_functions_runtime-1.0.0.dist-info/WHEEL +5 -0
- azure_functions_runtime-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation. All rights reserved.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
from .handle_event import (worker_init_request,
|
|
4
|
+
functions_metadata_request,
|
|
5
|
+
function_environment_reload_request,
|
|
6
|
+
invocation_request,
|
|
7
|
+
function_load_request)
|
|
8
|
+
from .utils.threadpool import (
|
|
9
|
+
start_threadpool_executor,
|
|
10
|
+
stop_threadpool_executor,
|
|
11
|
+
get_threadpool_executor,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = ('worker_init_request',
|
|
15
|
+
'functions_metadata_request',
|
|
16
|
+
'function_environment_reload_request',
|
|
17
|
+
'invocation_request',
|
|
18
|
+
'function_load_request',
|
|
19
|
+
'start_threadpool_executor',
|
|
20
|
+
'stop_threadpool_executor',
|
|
21
|
+
'get_threadpool_executor')
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation. All rights reserved.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
import threading
|
|
4
|
+
|
|
5
|
+
from .retrycontext import RetryContext
|
|
6
|
+
from .tracecontext import TraceContext
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Context:
|
|
10
|
+
def __init__(self,
|
|
11
|
+
func_name: str,
|
|
12
|
+
func_dir: str,
|
|
13
|
+
invocation_id: str,
|
|
14
|
+
thread_local_storage: threading.local,
|
|
15
|
+
trace_context: TraceContext,
|
|
16
|
+
retry_context: RetryContext) -> None:
|
|
17
|
+
self.__func_name = func_name
|
|
18
|
+
self.__func_dir = func_dir
|
|
19
|
+
self.__invocation_id = invocation_id
|
|
20
|
+
self.__thread_local_storage = thread_local_storage
|
|
21
|
+
self.__trace_context = trace_context
|
|
22
|
+
self.__retry_context = retry_context
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def invocation_id(self) -> str:
|
|
26
|
+
return self.__invocation_id
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def thread_local_storage(self) -> threading.local:
|
|
30
|
+
return self.__thread_local_storage
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def function_name(self) -> str:
|
|
34
|
+
return self.__func_name
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def function_directory(self) -> str:
|
|
38
|
+
return self.__func_dir
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def trace_context(self) -> TraceContext:
|
|
42
|
+
return self.__trace_context
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def retry_context(self) -> RetryContext:
|
|
46
|
+
return self.__retry_context
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def get_context(invoc_request, name: str,
|
|
50
|
+
directory: str) -> Context:
|
|
51
|
+
""" For more information refer:
|
|
52
|
+
https://aka.ms/azfunc-invocation-context
|
|
53
|
+
"""
|
|
54
|
+
trace_context = TraceContext(
|
|
55
|
+
invoc_request.trace_context.trace_parent,
|
|
56
|
+
invoc_request.trace_context.trace_state,
|
|
57
|
+
invoc_request.trace_context.attributes)
|
|
58
|
+
|
|
59
|
+
retry_context = RetryContext(
|
|
60
|
+
invoc_request.retry_context.retry_count,
|
|
61
|
+
invoc_request.retry_context.max_retry_count,
|
|
62
|
+
invoc_request.retry_context.exception)
|
|
63
|
+
|
|
64
|
+
return Context(
|
|
65
|
+
name, directory, invoc_request.invocation_id,
|
|
66
|
+
threading.local(), trace_context, retry_context)
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation. All rights reserved.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from typing import Any, List, Optional
|
|
8
|
+
|
|
9
|
+
from .nullable_converters import (
|
|
10
|
+
to_nullable_bool,
|
|
11
|
+
to_nullable_double,
|
|
12
|
+
to_nullable_string,
|
|
13
|
+
to_nullable_timestamp,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
from http.cookies import SimpleCookie
|
|
18
|
+
except ImportError:
|
|
19
|
+
from Cookie import SimpleCookie # type: ignore
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Datum:
|
|
23
|
+
def __init__(self, value, type):
|
|
24
|
+
self.value = value
|
|
25
|
+
self.type = type
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def python_value(self) -> Any:
|
|
29
|
+
if self.value is None or self.type is None:
|
|
30
|
+
return None
|
|
31
|
+
elif self.type in ('bytes', 'string', 'int', 'double'):
|
|
32
|
+
return self.value
|
|
33
|
+
elif self.type == 'json':
|
|
34
|
+
return json.loads(self.value)
|
|
35
|
+
elif self.type == 'collection_string':
|
|
36
|
+
return [v for v in self.value.string]
|
|
37
|
+
elif self.type == 'collection_bytes':
|
|
38
|
+
return [v for v in self.value.bytes]
|
|
39
|
+
elif self.type == 'collection_double':
|
|
40
|
+
return [v for v in self.value.double]
|
|
41
|
+
elif self.type == 'collection_sint64':
|
|
42
|
+
return [v for v in self.value.sint64]
|
|
43
|
+
else:
|
|
44
|
+
return self.value
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def python_type(self) -> type:
|
|
48
|
+
return type(self.python_value)
|
|
49
|
+
|
|
50
|
+
def __eq__(self, other):
|
|
51
|
+
if not isinstance(other, type(self)):
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
return self.value == other.value and self.type == other.type
|
|
55
|
+
|
|
56
|
+
def __hash__(self):
|
|
57
|
+
return hash((type(self), (self.value, self.type)))
|
|
58
|
+
|
|
59
|
+
def __repr__(self):
|
|
60
|
+
val_repr = repr(self.value)
|
|
61
|
+
if len(val_repr) > 10:
|
|
62
|
+
val_repr = val_repr[:10] + '...'
|
|
63
|
+
return '<Datum ' + str(self.type) + val_repr + '>'
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def from_typed_data(cls, protos):
|
|
67
|
+
try:
|
|
68
|
+
td = protos.TypedData
|
|
69
|
+
except Exception:
|
|
70
|
+
td = protos
|
|
71
|
+
tt = td.WhichOneof('data')
|
|
72
|
+
if tt == 'http':
|
|
73
|
+
http = td.http
|
|
74
|
+
val = dict(
|
|
75
|
+
method=Datum(http.method, 'string'),
|
|
76
|
+
url=Datum(http.url, 'string'),
|
|
77
|
+
headers={
|
|
78
|
+
k: Datum(v, 'string') for k, v in http.headers.items()
|
|
79
|
+
},
|
|
80
|
+
body=(
|
|
81
|
+
Datum.from_typed_data(http.body)
|
|
82
|
+
or Datum(type='bytes', value=b'')
|
|
83
|
+
),
|
|
84
|
+
params={
|
|
85
|
+
k: Datum(v, 'string') for k, v in http.params.items()
|
|
86
|
+
},
|
|
87
|
+
query={
|
|
88
|
+
k: Datum(v, 'string') for k, v in http.query.items()
|
|
89
|
+
},
|
|
90
|
+
)
|
|
91
|
+
elif tt == 'string':
|
|
92
|
+
val = td.string
|
|
93
|
+
elif tt == 'bytes':
|
|
94
|
+
val = td.bytes
|
|
95
|
+
elif tt == 'json':
|
|
96
|
+
val = td.json
|
|
97
|
+
elif tt == 'collection_bytes':
|
|
98
|
+
val = td.collection_bytes
|
|
99
|
+
elif tt == 'collection_string':
|
|
100
|
+
val = td.collection_string
|
|
101
|
+
elif tt == 'collection_sint64':
|
|
102
|
+
val = td.collection_sint64
|
|
103
|
+
elif tt == 'model_binding_data':
|
|
104
|
+
val = td.model_binding_data
|
|
105
|
+
elif tt == 'collection_model_binding_data':
|
|
106
|
+
val = td.collection_model_binding_data
|
|
107
|
+
elif tt is None:
|
|
108
|
+
return None
|
|
109
|
+
else:
|
|
110
|
+
raise NotImplementedError(
|
|
111
|
+
'unsupported TypeData kind: %s' % tt
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
return cls(val, tt)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def datum_as_proto(datum: Datum, protos):
|
|
118
|
+
if datum.type == 'string':
|
|
119
|
+
return protos.TypedData(string=datum.value)
|
|
120
|
+
elif datum.type == 'bytes':
|
|
121
|
+
return protos.TypedData(bytes=datum.value)
|
|
122
|
+
elif datum.type == 'json':
|
|
123
|
+
return protos.TypedData(json=datum.value)
|
|
124
|
+
elif datum.type == 'http':
|
|
125
|
+
return protos.TypedData(http=protos.RpcHttp(
|
|
126
|
+
status_code=datum.value['status_code'].value,
|
|
127
|
+
headers={
|
|
128
|
+
k: v.value
|
|
129
|
+
for k, v in datum.value['headers'].items()
|
|
130
|
+
},
|
|
131
|
+
cookies=parse_to_rpc_http_cookie_list(datum.value.get('cookies'), protos),
|
|
132
|
+
enable_content_negotiation=False,
|
|
133
|
+
body=datum_as_proto(datum.value['body'], protos),
|
|
134
|
+
))
|
|
135
|
+
elif datum.type is None:
|
|
136
|
+
return None
|
|
137
|
+
elif datum.type == 'dict':
|
|
138
|
+
# TypedData doesn't support dict, so we return it as json
|
|
139
|
+
return protos.TypedData(json=json.dumps(datum.value))
|
|
140
|
+
elif datum.type == 'list':
|
|
141
|
+
# TypedData doesn't support list, so we return it as json
|
|
142
|
+
return protos.TypedData(json=json.dumps(datum.value))
|
|
143
|
+
elif datum.type == 'int':
|
|
144
|
+
return protos.TypedData(int=datum.value)
|
|
145
|
+
elif datum.type == 'double':
|
|
146
|
+
return protos.TypedData(double=datum.value)
|
|
147
|
+
elif datum.type == 'bool':
|
|
148
|
+
# TypedData doesn't support bool, so we return it as an int
|
|
149
|
+
return protos.TypedData(int=int(datum.value))
|
|
150
|
+
else:
|
|
151
|
+
raise NotImplementedError(
|
|
152
|
+
'unexpected Datum type: %s' % datum.type
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def parse_to_rpc_http_cookie_list(cookies: Optional[List[SimpleCookie]], protos):
|
|
157
|
+
if cookies is None:
|
|
158
|
+
return cookies
|
|
159
|
+
|
|
160
|
+
rpc_http_cookies = []
|
|
161
|
+
|
|
162
|
+
for cookie in cookies:
|
|
163
|
+
for name, cookie_entity in cookie.items():
|
|
164
|
+
rpc_http_cookies.append(
|
|
165
|
+
protos.RpcHttpCookie(name=name,
|
|
166
|
+
value=cookie_entity.value,
|
|
167
|
+
domain=to_nullable_string(
|
|
168
|
+
cookie_entity['domain'],
|
|
169
|
+
'cookie.domain',
|
|
170
|
+
protos),
|
|
171
|
+
path=to_nullable_string(
|
|
172
|
+
cookie_entity['path'],
|
|
173
|
+
'cookie.path',
|
|
174
|
+
protos),
|
|
175
|
+
expires=to_nullable_timestamp(
|
|
176
|
+
parse_cookie_attr_expires(
|
|
177
|
+
cookie_entity), 'cookie.expires',
|
|
178
|
+
protos),
|
|
179
|
+
secure=to_nullable_bool(
|
|
180
|
+
bool(cookie_entity['secure']),
|
|
181
|
+
'cookie.secure',
|
|
182
|
+
protos),
|
|
183
|
+
http_only=to_nullable_bool(
|
|
184
|
+
bool(cookie_entity['httponly']),
|
|
185
|
+
'cookie.httpOnly',
|
|
186
|
+
protos),
|
|
187
|
+
same_site=parse_cookie_attr_same_site(
|
|
188
|
+
cookie_entity, protos),
|
|
189
|
+
max_age=to_nullable_double(
|
|
190
|
+
cookie_entity['max-age'],
|
|
191
|
+
'cookie.maxAge',
|
|
192
|
+
protos)))
|
|
193
|
+
|
|
194
|
+
return rpc_http_cookies
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def parse_cookie_attr_expires(cookie_entity):
|
|
198
|
+
expires = cookie_entity['expires']
|
|
199
|
+
|
|
200
|
+
if expires is not None and len(expires) != 0:
|
|
201
|
+
try:
|
|
202
|
+
return datetime.strptime(expires, "%a, %d %b %Y %H:%M:%S GMT")
|
|
203
|
+
except ValueError:
|
|
204
|
+
logging.error(
|
|
205
|
+
"Can not parse value %s of expires in the cookie "
|
|
206
|
+
"due to invalid format.", expires)
|
|
207
|
+
raise
|
|
208
|
+
except OverflowError:
|
|
209
|
+
logging.error(
|
|
210
|
+
"Can not parse value %s of expires in the cookie "
|
|
211
|
+
"because the parsed date exceeds the largest valid C "
|
|
212
|
+
"integer on your system.", expires)
|
|
213
|
+
raise
|
|
214
|
+
|
|
215
|
+
return None
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def parse_cookie_attr_same_site(cookie_entity, protos):
|
|
219
|
+
same_site = getattr(protos.RpcHttpCookie.SameSite, "None")
|
|
220
|
+
try:
|
|
221
|
+
raw_same_site_str = cookie_entity['samesite'].lower()
|
|
222
|
+
|
|
223
|
+
if raw_same_site_str == 'lax':
|
|
224
|
+
same_site = protos.RpcHttpCookie.SameSite.Lax
|
|
225
|
+
elif raw_same_site_str == 'strict':
|
|
226
|
+
same_site = protos.RpcHttpCookie.SameSite.Strict
|
|
227
|
+
elif raw_same_site_str == 'none':
|
|
228
|
+
same_site = protos.RpcHttpCookie.SameSite.ExplicitNone
|
|
229
|
+
except Exception:
|
|
230
|
+
return same_site
|
|
231
|
+
|
|
232
|
+
return same_site
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation. All rights reserved.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
import typing
|
|
4
|
+
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
from .datumdef import Datum
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class GenericBinding:
|
|
11
|
+
|
|
12
|
+
@classmethod
|
|
13
|
+
def has_trigger_support(cls) -> bool:
|
|
14
|
+
return False
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def check_input_type_annotation(cls, pytype: type) -> bool:
|
|
18
|
+
return issubclass(pytype, (str, bytes))
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def check_output_type_annotation(cls, pytype: type) -> bool:
|
|
22
|
+
return issubclass(pytype, (str, bytes, bytearray))
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def encode(cls, obj: Any, *,
|
|
26
|
+
expected_type: Optional[type]) -> Datum:
|
|
27
|
+
if isinstance(obj, str):
|
|
28
|
+
return Datum(type='string', value=obj)
|
|
29
|
+
|
|
30
|
+
elif isinstance(obj, (bytes, bytearray)):
|
|
31
|
+
return Datum(type='bytes', value=bytes(obj))
|
|
32
|
+
elif obj is None:
|
|
33
|
+
return Datum(type=None, value=obj)
|
|
34
|
+
elif isinstance(obj, dict):
|
|
35
|
+
return Datum(type='dict', value=obj)
|
|
36
|
+
elif isinstance(obj, list):
|
|
37
|
+
return Datum(type='list', value=obj)
|
|
38
|
+
elif isinstance(obj, int):
|
|
39
|
+
return Datum(type='int', value=obj)
|
|
40
|
+
elif isinstance(obj, float):
|
|
41
|
+
return Datum(type='double', value=obj)
|
|
42
|
+
elif isinstance(obj, bool):
|
|
43
|
+
return Datum(type='bool', value=obj)
|
|
44
|
+
else:
|
|
45
|
+
raise NotImplementedError
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def decode(cls, data: Datum, *, trigger_metadata) -> typing.Any:
|
|
49
|
+
# Enabling support for Dapr bindings
|
|
50
|
+
# https://github.com/Azure/azure-functions-python-worker/issues/1316
|
|
51
|
+
if data is None:
|
|
52
|
+
return None
|
|
53
|
+
data_type = data.type
|
|
54
|
+
|
|
55
|
+
if data_type == 'string':
|
|
56
|
+
result = data.value
|
|
57
|
+
elif data_type == 'bytes':
|
|
58
|
+
result = data.value
|
|
59
|
+
elif data_type == 'json':
|
|
60
|
+
result = data.value
|
|
61
|
+
elif data_type is None:
|
|
62
|
+
result = None
|
|
63
|
+
else:
|
|
64
|
+
raise ValueError(
|
|
65
|
+
'unexpected type of data received for the "generic" binding ',
|
|
66
|
+
repr(data_type)
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
return result
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
def has_implicit_output(cls, bind_name: Optional[str]) -> bool:
|
|
73
|
+
if bind_name == 'durableClient':
|
|
74
|
+
return False
|
|
75
|
+
return True
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation. All rights reserved.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
# mypy: disable-error-code="attr-defined"
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from typing import Any, Dict, Optional, Union
|
|
8
|
+
|
|
9
|
+
from .datumdef import Datum, datum_as_proto
|
|
10
|
+
from .generic import GenericBinding
|
|
11
|
+
|
|
12
|
+
from ..http_v2 import HttpV2Registry
|
|
13
|
+
from ..logging import logger
|
|
14
|
+
from ..utils.constants import (
|
|
15
|
+
CUSTOMER_PACKAGES_PATH,
|
|
16
|
+
HTTP,
|
|
17
|
+
HTTP_TRIGGER,
|
|
18
|
+
)
|
|
19
|
+
from ..utils.helpers import set_sdk_version
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
PB_TYPE = 'rpc_data'
|
|
23
|
+
PB_TYPE_DATA = 'data'
|
|
24
|
+
PB_TYPE_RPC_SHARED_MEMORY = 'rpc_shared_memory'
|
|
25
|
+
|
|
26
|
+
BINDING_REGISTRY = None
|
|
27
|
+
DEFERRED_BINDING_REGISTRY = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _check_http_input_type_annotation(bind_name: str, pytype: type,
|
|
31
|
+
is_deferred_binding: bool) -> bool:
|
|
32
|
+
if HttpV2Registry.http_v2_enabled():
|
|
33
|
+
return HttpV2Registry.ext_base().RequestTrackerMeta \
|
|
34
|
+
.check_type(pytype)
|
|
35
|
+
|
|
36
|
+
binding = get_binding(bind_name, is_deferred_binding)
|
|
37
|
+
return binding.check_input_type_annotation(pytype)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _check_http_output_type_annotation(bind_name: str, pytype: type) -> bool:
|
|
41
|
+
if HttpV2Registry.http_v2_enabled():
|
|
42
|
+
return HttpV2Registry.ext_base().ResponseTrackerMeta.check_type(pytype)
|
|
43
|
+
|
|
44
|
+
binding = get_binding(bind_name)
|
|
45
|
+
return binding.check_output_type_annotation(pytype)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
INPUT_TYPE_CHECK_OVERRIDE_MAP = {
|
|
49
|
+
HTTP_TRIGGER: _check_http_input_type_annotation
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
OUTPUT_TYPE_CHECK_OVERRIDE_MAP = {
|
|
53
|
+
HTTP: _check_http_output_type_annotation
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def load_binding_registry() -> None:
|
|
58
|
+
"""
|
|
59
|
+
Tries to load azure-functions from the customer's BYO. If it's
|
|
60
|
+
not found, it loads the builtin. If the BINDING_REGISTRY is None,
|
|
61
|
+
azure-functions hasn't been loaded in properly.
|
|
62
|
+
|
|
63
|
+
Tries to load the base extension.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
func = sys.modules.get('azure.functions')
|
|
67
|
+
|
|
68
|
+
if func is None:
|
|
69
|
+
import azure.functions as func
|
|
70
|
+
|
|
71
|
+
set_sdk_version(func.__version__) # type: ignore
|
|
72
|
+
|
|
73
|
+
global BINDING_REGISTRY
|
|
74
|
+
BINDING_REGISTRY = func.get_binding_registry() # type: ignore
|
|
75
|
+
|
|
76
|
+
if BINDING_REGISTRY is None:
|
|
77
|
+
raise AttributeError('BINDING_REGISTRY is None. azure-functions '
|
|
78
|
+
'library not found. Sys Path: %s. '
|
|
79
|
+
'Sys Modules: %s. '
|
|
80
|
+
'python-packages Path exists: %s.',
|
|
81
|
+
sys.path, sys.modules,
|
|
82
|
+
os.path.exists(CUSTOMER_PACKAGES_PATH))
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
import azurefunctions.extensions.base as clients
|
|
86
|
+
global DEFERRED_BINDING_REGISTRY
|
|
87
|
+
DEFERRED_BINDING_REGISTRY = clients.get_binding_registry()
|
|
88
|
+
except ImportError:
|
|
89
|
+
logger.debug('Base extension not found. '
|
|
90
|
+
'Python version: 3.%s, Sys path: %s, '
|
|
91
|
+
'Sys Module: %s, python-packages Path exists: %s.',
|
|
92
|
+
sys.version_info.minor, sys.path,
|
|
93
|
+
sys.modules, os.path.exists(CUSTOMER_PACKAGES_PATH))
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def get_binding(bind_name: str,
|
|
97
|
+
is_deferred_binding: Optional[bool] = False)\
|
|
98
|
+
-> object:
|
|
99
|
+
"""
|
|
100
|
+
First checks if the binding is a non-deferred binding. This is
|
|
101
|
+
the most common case.
|
|
102
|
+
Second checks if the binding is a deferred binding.
|
|
103
|
+
If the binding is neither, it's a generic type.
|
|
104
|
+
"""
|
|
105
|
+
binding = None
|
|
106
|
+
if binding is None and not is_deferred_binding:
|
|
107
|
+
binding = BINDING_REGISTRY.get(bind_name) # type: ignore
|
|
108
|
+
if binding is None and is_deferred_binding:
|
|
109
|
+
binding = DEFERRED_BINDING_REGISTRY.get(bind_name) # type: ignore
|
|
110
|
+
if binding is None:
|
|
111
|
+
binding = GenericBinding
|
|
112
|
+
return binding
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def is_trigger_binding(bind_name: str) -> bool:
|
|
116
|
+
binding = get_binding(bind_name)
|
|
117
|
+
return binding.has_trigger_support()
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def check_input_type_annotation(bind_name: str,
|
|
121
|
+
pytype: type,
|
|
122
|
+
is_deferred_binding: bool) -> bool:
|
|
123
|
+
global INPUT_TYPE_CHECK_OVERRIDE_MAP
|
|
124
|
+
if bind_name in INPUT_TYPE_CHECK_OVERRIDE_MAP:
|
|
125
|
+
return INPUT_TYPE_CHECK_OVERRIDE_MAP[bind_name](bind_name, pytype,
|
|
126
|
+
is_deferred_binding)
|
|
127
|
+
|
|
128
|
+
binding = get_binding(bind_name, is_deferred_binding)
|
|
129
|
+
|
|
130
|
+
return binding.check_input_type_annotation(pytype)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def check_output_type_annotation(bind_name: str, pytype: type) -> bool:
|
|
134
|
+
global OUTPUT_TYPE_CHECK_OVERRIDE_MAP
|
|
135
|
+
if bind_name in OUTPUT_TYPE_CHECK_OVERRIDE_MAP:
|
|
136
|
+
return OUTPUT_TYPE_CHECK_OVERRIDE_MAP[bind_name](bind_name, pytype)
|
|
137
|
+
|
|
138
|
+
binding = get_binding(bind_name)
|
|
139
|
+
return binding.check_output_type_annotation(pytype)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def has_implicit_output(bind_name: str) -> bool:
|
|
143
|
+
binding = get_binding(bind_name)
|
|
144
|
+
|
|
145
|
+
# Need to pass in bind_name to exempt Durable Functions
|
|
146
|
+
if binding is GenericBinding:
|
|
147
|
+
return (getattr(binding, 'has_implicit_output', lambda: False)
|
|
148
|
+
(bind_name)) # type: ignore
|
|
149
|
+
|
|
150
|
+
else:
|
|
151
|
+
# If the binding does not have metaclass of meta.InConverter
|
|
152
|
+
# The implicit_output does not exist
|
|
153
|
+
return getattr(binding, 'has_implicit_output', lambda: False)()
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def from_incoming_proto(
|
|
157
|
+
binding: str,
|
|
158
|
+
pb, *,
|
|
159
|
+
pytype: Optional[type],
|
|
160
|
+
trigger_metadata: Optional[Dict[str, Any]],
|
|
161
|
+
function_name: str,
|
|
162
|
+
is_deferred_binding: Optional[bool] = False) -> Any:
|
|
163
|
+
binding_obj = get_binding(binding, is_deferred_binding)
|
|
164
|
+
if trigger_metadata:
|
|
165
|
+
metadata = {
|
|
166
|
+
k: Datum.from_typed_data(v)
|
|
167
|
+
for k, v in trigger_metadata.items()
|
|
168
|
+
}
|
|
169
|
+
else:
|
|
170
|
+
metadata = {}
|
|
171
|
+
|
|
172
|
+
pb_type = pb.WhichOneof(PB_TYPE)
|
|
173
|
+
if pb_type == PB_TYPE_DATA:
|
|
174
|
+
val = pb.data
|
|
175
|
+
datum = Datum.from_typed_data(val)
|
|
176
|
+
else:
|
|
177
|
+
raise TypeError('Unknown ParameterBindingType: %s' % pb_type)
|
|
178
|
+
|
|
179
|
+
try:
|
|
180
|
+
# if the binding is an sdk type binding
|
|
181
|
+
if is_deferred_binding:
|
|
182
|
+
return deferred_bindings_decode(binding=binding_obj,
|
|
183
|
+
pb=pb,
|
|
184
|
+
pytype=pytype,
|
|
185
|
+
datum=datum,
|
|
186
|
+
metadata=metadata,
|
|
187
|
+
function_name=function_name)
|
|
188
|
+
return binding_obj.decode(datum, trigger_metadata=metadata)
|
|
189
|
+
except NotImplementedError:
|
|
190
|
+
# Binding does not support the data.
|
|
191
|
+
dt = val.WhichOneof('data')
|
|
192
|
+
raise TypeError(
|
|
193
|
+
'unable to decode incoming TypedData: '
|
|
194
|
+
'unsupported combination of TypedData field %s '
|
|
195
|
+
'and expected binding type %s' % (repr(dt), binding_obj))
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def get_datum(binding: str, obj: Any,
|
|
199
|
+
pytype: Optional[type]) -> Union[Datum, None]:
|
|
200
|
+
"""
|
|
201
|
+
Convert an object to a datum with the specified type.
|
|
202
|
+
"""
|
|
203
|
+
binding_obj = get_binding(binding)
|
|
204
|
+
try:
|
|
205
|
+
datum = binding_obj.encode(obj, expected_type=pytype)
|
|
206
|
+
except NotImplementedError:
|
|
207
|
+
# Binding does not support the data.
|
|
208
|
+
raise TypeError(
|
|
209
|
+
'unable to encode outgoing TypedData: '
|
|
210
|
+
'unsupported type "%s" for '
|
|
211
|
+
'Python type "%s"' % (binding, type(obj).__name__))
|
|
212
|
+
return datum
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _does_datatype_support_caching(datum: Datum):
|
|
216
|
+
supported_datatypes = ('bytes', 'string')
|
|
217
|
+
return datum.type in supported_datatypes
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def to_outgoing_proto(binding: str, obj: Any, *,
|
|
221
|
+
pytype: Optional[type],
|
|
222
|
+
protos):
|
|
223
|
+
datum = get_datum(binding, obj, pytype)
|
|
224
|
+
return datum_as_proto(datum, protos) # type: ignore
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def to_outgoing_param_binding(binding: str, obj: Any, *,
|
|
228
|
+
pytype: Optional[type],
|
|
229
|
+
out_name: str,
|
|
230
|
+
protos):
|
|
231
|
+
datum = get_datum(binding, obj, pytype)
|
|
232
|
+
# If not, send it as part of the response message over RPC
|
|
233
|
+
# rpc_val can be None here as we now support a None return type
|
|
234
|
+
rpc_val = datum_as_proto(datum, protos) # type: ignore
|
|
235
|
+
return protos.ParameterBinding(
|
|
236
|
+
name=out_name,
|
|
237
|
+
data=rpc_val)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def deferred_bindings_decode(binding: Any,
|
|
241
|
+
pb: Any, *,
|
|
242
|
+
pytype: Optional[type],
|
|
243
|
+
datum: Any,
|
|
244
|
+
metadata: Any,
|
|
245
|
+
function_name: str):
|
|
246
|
+
"""
|
|
247
|
+
The extension manages a cache for clients (ie. BlobClient, ContainerClient)
|
|
248
|
+
That have already been created, so that the worker can reuse the
|
|
249
|
+
previously created type without creating a new one.
|
|
250
|
+
|
|
251
|
+
For async types, the function_name is needed as a key to differentiate.
|
|
252
|
+
This prevents a known SDK issue where reusing a client across functions
|
|
253
|
+
can lose the session context and cause an error.
|
|
254
|
+
|
|
255
|
+
"""
|
|
256
|
+
|
|
257
|
+
deferred_binding_type = binding.decode(datum,
|
|
258
|
+
trigger_metadata=metadata,
|
|
259
|
+
pytype=pytype)
|
|
260
|
+
|
|
261
|
+
return deferred_binding_type
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def check_deferred_bindings_enabled(param_anno: Union[type, None],
|
|
265
|
+
deferred_bindings_enabled: bool) -> Any:
|
|
266
|
+
"""
|
|
267
|
+
Checks if deferred bindings is enabled at fx and single binding level
|
|
268
|
+
|
|
269
|
+
The first bool represents if deferred bindings is enabled at a fx level
|
|
270
|
+
The second represents if the current binding is deferred binding
|
|
271
|
+
"""
|
|
272
|
+
if (DEFERRED_BINDING_REGISTRY is not None
|
|
273
|
+
and DEFERRED_BINDING_REGISTRY.check_supported_type(param_anno)):
|
|
274
|
+
return True, True
|
|
275
|
+
else:
|
|
276
|
+
return deferred_bindings_enabled, False
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def get_deferred_raw_bindings(indexed_function, input_types):
|
|
280
|
+
"""
|
|
281
|
+
Calls a method from the base extension that generates the raw bindings
|
|
282
|
+
for a given function. It also returns logs for that function including
|
|
283
|
+
the defined binding type and if deferred bindings is enabled for that
|
|
284
|
+
binding.
|
|
285
|
+
"""
|
|
286
|
+
raw_bindings, bindings_logs = DEFERRED_BINDING_REGISTRY.get_raw_bindings(
|
|
287
|
+
indexed_function, input_types)
|
|
288
|
+
return raw_bindings, bindings_logs
|