Skip to content

Commit abb48e7

Browse files
authored
Merge branch 'dev' into pthummar/eventGrid_E2E_tests
2 parents 027b62a + ef37809 commit abb48e7

File tree

37 files changed

+1086
-28
lines changed

37 files changed

+1086
-28
lines changed

azure_functions_worker/bindings/datumdef.py

Lines changed: 85 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
11
# Copyright (c) Microsoft Corporation. All rights reserved.
22
# Licensed under the MIT License.
3-
3+
import logging
44
from typing import Any, Optional
55
import json
66
from .. import protos
77
from ..logging import logger
8+
from typing import List
9+
try:
10+
from http.cookies import SimpleCookie
11+
except ImportError:
12+
from Cookie import SimpleCookie
13+
from dateutil import parser
14+
from dateutil.parser import ParserError
15+
from .nullable_converters import to_nullable_bool, to_nullable_string, \
16+
to_nullable_double, to_nullable_timestamp
817

918

1019
class Datum:
@@ -99,8 +108,8 @@ def from_rpc_shared_memory(
99108
shmem: protos.RpcSharedMemory,
100109
shmem_mgr) -> Optional['Datum']:
101110
"""
102-
Reads the specified shared memory region and converts the read data into
103-
a datum object of the corresponding type.
111+
Reads the specified shared memory region and converts the read data
112+
into a datum object of the corresponding type.
104113
"""
105114
if shmem is None:
106115
logger.warning('Cannot read from shared memory. '
@@ -183,10 +192,83 @@ def datum_as_proto(datum: Datum) -> protos.TypedData:
183192
k: v.value
184193
for k, v in datum.value['headers'].items()
185194
},
195+
cookies=parse_to_rpc_http_cookie_list(datum.value['cookies']),
186196
enable_content_negotiation=False,
187197
body=datum_as_proto(datum.value['body']),
188198
))
189199
else:
190200
raise NotImplementedError(
191201
'unexpected Datum type: {!r}'.format(datum.type)
192202
)
203+
204+
205+
def parse_to_rpc_http_cookie_list(cookies: Optional[List[SimpleCookie]]):
206+
if cookies is None:
207+
return cookies
208+
209+
rpc_http_cookies = []
210+
211+
for cookie in cookies:
212+
for name, cookie_entity in cookie.items():
213+
rpc_http_cookies.append(
214+
protos.RpcHttpCookie(name=name,
215+
value=cookie_entity.value,
216+
domain=to_nullable_string(
217+
cookie_entity['domain'],
218+
'cookie.domain'),
219+
path=to_nullable_string(
220+
cookie_entity['path'], 'cookie.path'),
221+
expires=to_nullable_timestamp(
222+
parse_cookie_attr_expires(
223+
cookie_entity), 'cookie.expires'),
224+
secure=to_nullable_bool(
225+
bool(cookie_entity['secure']),
226+
'cookie.secure'),
227+
http_only=to_nullable_bool(
228+
bool(cookie_entity['httponly']),
229+
'cookie.httpOnly'),
230+
same_site=parse_cookie_attr_same_site(
231+
cookie_entity),
232+
max_age=to_nullable_double(
233+
cookie_entity['max-age'],
234+
'cookie.maxAge')))
235+
236+
return rpc_http_cookies
237+
238+
239+
def parse_cookie_attr_expires(cookie_entity):
240+
expires = cookie_entity['expires']
241+
242+
if expires is not None and len(expires) != 0:
243+
try:
244+
return parser.parse(expires)
245+
except ParserError:
246+
logging.error(
247+
f"Can not parse value {expires} of expires in the cookie "
248+
f"due to invalid format.")
249+
raise
250+
except OverflowError:
251+
logging.error(
252+
f"Can not parse value {expires} of expires in the cookie "
253+
f"because the parsed date exceeds the largest valid C "
254+
f"integer on your system.")
255+
raise
256+
257+
return None
258+
259+
260+
def parse_cookie_attr_same_site(cookie_entity):
261+
same_site = getattr(protos.RpcHttpCookie.SameSite, "None")
262+
try:
263+
raw_same_site_str = cookie_entity['samesite'].lower()
264+
265+
if raw_same_site_str == 'lax':
266+
same_site = protos.RpcHttpCookie.SameSite.Lax
267+
elif raw_same_site_str == 'strict':
268+
same_site = protos.RpcHttpCookie.SameSite.Strict
269+
elif raw_same_site_str == 'none':
270+
same_site = protos.RpcHttpCookie.SameSite.ExplicitNone
271+
except Exception:
272+
return same_site
273+
274+
return same_site
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
from datetime import datetime
2+
from typing import Optional, Union
3+
4+
from google.protobuf.timestamp_pb2 import Timestamp
5+
6+
from azure_functions_worker import protos
7+
8+
9+
def to_nullable_string(nullable: Optional[str], property_name: str) -> \
10+
Optional[protos.NullableString]:
11+
"""Converts string input to an 'NullableString' to be sent through the
12+
RPC layer. Input that is not a string but is also not null or undefined
13+
logs a function app level warning.
14+
15+
:param nullable Input to be converted to an NullableString if it is a
16+
valid string
17+
:param property_name The name of the property that the caller will
18+
assign the output to. Used for debugging.
19+
"""
20+
if isinstance(nullable, str):
21+
return protos.NullableString(value=nullable)
22+
23+
if nullable is not None:
24+
raise TypeError(
25+
f"A 'str' type was expected instead of a '{type(nullable)}' "
26+
f"type. Cannot parse value {nullable} of '{property_name}'.")
27+
28+
return None
29+
30+
31+
def to_nullable_bool(nullable: Optional[bool], property_name: str) -> \
32+
Optional[protos.NullableBool]:
33+
"""Converts boolean input to an 'NullableBool' to be sent through the
34+
RPC layer. Input that is not a boolean but is also not null or undefined
35+
logs a function app level warning.
36+
37+
:param nullable Input to be converted to an NullableBool if it is a
38+
valid boolean
39+
:param property_name The name of the property that the caller will
40+
assign the output to. Used for debugging.
41+
"""
42+
if isinstance(nullable, bool):
43+
return protos.NullableBool(value=nullable)
44+
45+
if nullable is not None:
46+
raise TypeError(
47+
f"A 'bool' type was expected instead of a '{type(nullable)}' "
48+
f"type. Cannot parse value {nullable} of '{property_name}'.")
49+
50+
return None
51+
52+
53+
def to_nullable_double(nullable: Optional[Union[str, int, float]],
54+
property_name: str) -> \
55+
Optional[protos.NullableDouble]:
56+
"""Converts int or float or str that parses to a number to an
57+
'NullableDouble' to be sent through the RPC layer. Input that is not a
58+
valid number but is also not null or undefined logs a function app level
59+
warning.
60+
:param nullable Input to be converted to an NullableDouble if it is a
61+
valid number
62+
:param property_name The name of the property that the caller will
63+
assign the output to. Used for debugging.
64+
"""
65+
if isinstance(nullable, int) or isinstance(nullable, float):
66+
return protos.NullableDouble(value=nullable)
67+
elif isinstance(nullable, str):
68+
if len(nullable) == 0:
69+
return None
70+
71+
try:
72+
return protos.NullableDouble(value=float(nullable))
73+
except Exception:
74+
raise TypeError(
75+
f"Cannot parse value {nullable} of '{property_name}' to "
76+
f"float.")
77+
78+
if nullable is not None:
79+
raise TypeError(
80+
f"A 'int' or 'float'"
81+
f" type was expected instead of a '{type(nullable)}' "
82+
f"type. Cannot parse value {nullable} of '{property_name}'.")
83+
84+
return None
85+
86+
87+
def to_nullable_timestamp(date_time: Optional[Union[datetime, int]],
88+
property_name: str) -> protos.NullableTimestamp:
89+
"""Converts Date or number input to an 'NullableTimestamp' to be sent
90+
through the RPC layer. Input that is not a Date or number but is also
91+
not null or undefined logs a function app level warning.
92+
93+
:param date_time Input to be converted to an NullableTimestamp if it is
94+
valid input
95+
:param property_name The name of the property that the caller will
96+
assign the output to. Used for debugging.
97+
"""
98+
if date_time is not None:
99+
try:
100+
time_in_seconds = date_time if isinstance(date_time,
101+
int) else \
102+
date_time.timestamp()
103+
104+
return protos.NullableTimestamp(
105+
value=Timestamp(seconds=int(time_in_seconds)))
106+
except Exception:
107+
raise TypeError(
108+
f"A 'datetime' or 'int'"
109+
f" type was expected instead of a '{type(date_time)}' "
110+
f"type. Cannot parse value {date_time} of '{property_name}'.")
111+
return None

azure_functions_worker/dispatcher.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -206,12 +206,6 @@ def on_logging(self, record: logging.LogRecord, formatted_msg: str) -> None:
206206
if invocation_id is not None:
207207
log['invocation_id'] = invocation_id
208208

209-
# XXX: When an exception field is set in RpcLog, WebHost doesn't
210-
# wait for the call result and simply aborts the execution.
211-
#
212-
# if record.exc_info and record.exc_info[1] is not None:
213-
# log['exception'] = self._serialize_exception(record.exc_info[1])
214-
215209
self._grpc_resp_queue.put_nowait(
216210
protos.StreamingMessage(
217211
request_id=self.request_id,

azure_functions_worker/loader.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -143,18 +143,19 @@ def index_function_app(function_path: str):
143143
module_name = pathlib.Path(function_path).stem
144144
imported_module = importlib.import_module(module_name)
145145

146-
from azure.functions import FunctionApp
147-
app: Optional[FunctionApp] = None
146+
from azure.functions import FunctionRegister
147+
app: Optional[FunctionRegister] = None
148148
for i in imported_module.__dir__():
149-
if isinstance(getattr(imported_module, i, None), FunctionApp):
149+
if isinstance(getattr(imported_module, i, None), FunctionRegister):
150150
if not app:
151151
app = getattr(imported_module, i, None)
152152
else:
153153
raise ValueError(
154-
"Multiple instances of FunctionApp are defined")
154+
f"More than one {app.__class__.__name__} or other top "
155+
f"level function app instances are defined.")
155156

156157
if not app:
157-
raise ValueError("Could not find instance of FunctionApp in "
158+
raise ValueError("Could not find top level function app instances in "
158159
f"{SCRIPT_FILE_NAME}.")
159160

160161
return app.get_functions()

azure_functions_worker/protos/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,18 @@
2424
ParameterBinding,
2525
TypedData,
2626
RpcHttp,
27+
RpcHttpCookie,
2728
RpcLog,
2829
RpcSharedMemory,
2930
RpcDataType,
3031
CloseSharedMemoryResourcesRequest,
3132
CloseSharedMemoryResourcesResponse,
3233
FunctionsMetadataRequest,
3334
FunctionMetadataResponse)
35+
36+
from .shared.NullableTypes_pb2 import (
37+
NullableString,
38+
NullableBool,
39+
NullableDouble,
40+
NullableTimestamp
41+
)

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@
105105
"grpcio~=1.43.0",
106106
"grpcio-tools~=1.43.0",
107107
"protobuf~=3.19.3",
108-
'azure-functions==1.11.3b2',
108+
"azure-functions==1.11.3b2",
109109
"python-dateutil~=2.8.2"
110110
]
111111

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import logging
2+
3+
import azure.functions as func
4+
5+
bp = func.Blueprint()
6+
7+
8+
@bp.route(route="default_template")
9+
def default_template(req: func.HttpRequest) -> func.HttpResponse:
10+
logging.info('Python HTTP trigger function processed a request.')
11+
12+
name = req.params.get('name')
13+
if not name:
14+
try:
15+
req_body = req.get_json()
16+
except ValueError:
17+
pass
18+
else:
19+
name = req_body.get('name')
20+
21+
if name:
22+
return func.HttpResponse(
23+
f"Hello, {name}. This HTTP triggered function "
24+
f"executed successfully.")
25+
else:
26+
return func.HttpResponse(
27+
"This HTTP triggered function executed successfully. "
28+
"Pass a name in the query string or in the request body for a"
29+
" personalized response.",
30+
status_code=200
31+
)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import azure.functions as func
2+
from blueprint import bp
3+
4+
app = func.FunctionApp()
5+
6+
app.register_functions(bp)
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import logging
2+
3+
import azure.functions as func
4+
5+
bp = func.Blueprint()
6+
7+
8+
@bp.route(route="default_template")
9+
def default_template(req: func.HttpRequest) -> func.HttpResponse:
10+
logging.info('Python HTTP trigger function processed a request.')
11+
12+
name = req.params.get('name')
13+
if not name:
14+
try:
15+
req_body = req.get_json()
16+
except ValueError:
17+
pass
18+
else:
19+
name = req_body.get('name')
20+
21+
if name:
22+
return func.HttpResponse(
23+
f"Hello, {name}. This HTTP triggered function "
24+
f"executed successfully.")
25+
else:
26+
return func.HttpResponse(
27+
"This HTTP triggered function executed successfully. "
28+
"Pass a name in the query string or in the request body for a"
29+
" personalized response.",
30+
status_code=200
31+
)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import azure.functions as func
2+
3+
from blueprint import bp
4+
5+
app = func.FunctionApp()
6+
7+
app.register_blueprint(bp)
8+
9+
10+
@app.route(route="return_http")
11+
def return_http(req: func.HttpRequest):
12+
return func.HttpResponse('<h1>Hello World™</h1>',
13+
mimetype='text/html')

0 commit comments

Comments
 (0)