Skip to content

Commit 78233cc

Browse files
author
Hanzhang Zeng (Roger)
committed
Merge branch 'release/1.3.1'
2 parents 01075c7 + 03ddadf commit 78233cc

File tree

8 files changed

+306
-22
lines changed

8 files changed

+306
-22
lines changed

CODEOWNERS

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# See https://help.github.com/articles/about-codeowners/
2+
# for more info about CODEOWNERS file
3+
#
4+
# It uses the same pattern rule for gitignore file
5+
# https://git-scm.com/docs/gitignore#_pattern_format
6+
#
7+
8+
#
9+
# AZURE FUNCTIONS TEAM
10+
# For all file changes, github would automatically include the following people in the PRs.
11+
#
12+
* @anirudhgarg @Hazhzeng @vrdmr

azure/functions/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,4 @@
5555
'WsgiMiddleware'
5656
)
5757

58-
__version__ = '1.3.0'
58+
__version__ = '1.3.1'

azure/functions/_servicebus.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,18 @@ def delivery_count(self) -> typing.Optional[int]:
3131
"""Number of times delivery has been attempted."""
3232
pass
3333

34+
@property
35+
@abc.abstractmethod
36+
def enqueued_time_utc(self) -> typing.Optional[datetime.datetime]:
37+
"""The date and time in UTC at which the message is enqueued"""
38+
pass
39+
40+
@property
41+
@abc.abstractmethod
42+
def expires_at_utc(self) -> typing.Optional[datetime.datetime]:
43+
"""The date and time in UTC at which the message is set to expire."""
44+
pass
45+
3446
@property
3547
@abc.abstractmethod
3648
def expiration_time(self) -> typing.Optional[datetime.datetime]:

azure/functions/blob.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@ def encode(cls, obj: Any, *,
7878

7979
@classmethod
8080
def decode(cls, data: meta.Datum, *, trigger_metadata) -> Any:
81+
if data is None or data.type is None:
82+
return None
83+
8184
data_type = data.type
8285

8386
if data_type == 'string':

azure/functions/eventhub.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ def decode_single_event(
125125
body=body,
126126
trigger_metadata=trigger_metadata,
127127
enqueued_time=cls._parse_datetime_metadata(
128-
trigger_metadata, 'EnqueuedTime'),
128+
trigger_metadata, 'EnqueuedTimeUtc'),
129129
partition_key=cls._decode_trigger_metadata_field(
130130
trigger_metadata, 'PartitionKey', python_type=str),
131131
sequence_number=cls._decode_trigger_metadata_field(

tests/test_blob.py

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
from typing import Dict, Any
5+
import unittest
6+
7+
import azure.functions as func
8+
import azure.functions.blob as afb
9+
from azure.functions.meta import Datum
10+
from azure.functions.blob import InputStream
11+
12+
13+
class TestBlob(unittest.TestCase):
14+
def test_blob_input_type(self):
15+
check_input_type = afb.BlobConverter.check_input_type_annotation
16+
self.assertTrue(check_input_type(str))
17+
self.assertTrue(check_input_type(bytes))
18+
self.assertTrue(check_input_type(InputStream))
19+
self.assertFalse(check_input_type(bytearray))
20+
21+
def test_blob_input_none(self):
22+
result: func.DocumentList = afb.BlobConverter.decode(
23+
data=None, trigger_metadata=None)
24+
self.assertIsNone(result)
25+
26+
def test_blob_input_string_no_metadata(self):
27+
datum: Datum = Datum(value='string_content', type='string')
28+
result: InputStream = afb.BlobConverter.decode(
29+
data=datum, trigger_metadata=None)
30+
self.assertIsNotNone(result)
31+
32+
# Verify result metadata
33+
self.assertIsInstance(result, InputStream)
34+
self.assertIsNone(result.name)
35+
self.assertIsNone(result.length)
36+
self.assertIsNone(result.uri)
37+
self.assertTrue(result.readable())
38+
self.assertFalse(result.seekable())
39+
self.assertFalse(result.writable())
40+
41+
# Verify result content
42+
content: bytes = result.read()
43+
self.assertEqual(content, b'string_content')
44+
45+
def test_blob_input_bytes_no_metadata(self):
46+
datum: Datum = Datum(value=b'bytes_content', type='bytes')
47+
result: InputStream = afb.BlobConverter.decode(
48+
data=datum, trigger_metadata=None)
49+
self.assertIsNotNone(result)
50+
51+
# Verify result metadata
52+
self.assertIsInstance(result, InputStream)
53+
self.assertIsNone(result.name)
54+
self.assertIsNone(result.length)
55+
self.assertIsNone(result.uri)
56+
self.assertTrue(result.readable())
57+
self.assertFalse(result.seekable())
58+
self.assertFalse(result.writable())
59+
60+
# Verify result content
61+
content: bytes = result.read()
62+
self.assertEqual(content, b'bytes_content')
63+
64+
def test_blob_input_with_metadata(self):
65+
datum: Datum = Datum(value=b'blob_content', type='bytes')
66+
metadata: Dict[str, Any] = {
67+
'Properties': Datum('{"Length": "12"}', 'json'),
68+
'BlobTrigger': Datum('blob_trigger_name', 'string'),
69+
'Uri': Datum('https://test.io/blob_trigger', 'string')
70+
}
71+
result: InputStream = afb.BlobConverter.decode(
72+
data=datum, trigger_metadata=metadata)
73+
74+
# Verify result metadata
75+
self.assertIsInstance(result, InputStream)
76+
self.assertEqual(result.name, 'blob_trigger_name')
77+
self.assertEqual(result.length, len(b'blob_content'))
78+
self.assertEqual(result.uri, 'https://test.io/blob_trigger')
79+
80+
def test_blob_incomplete_read(self):
81+
datum: Datum = Datum(value=b'blob_content', type='bytes')
82+
result: InputStream = afb.BlobConverter.decode(
83+
data=datum, trigger_metadata=None)
84+
85+
self.assertEqual(result.read(size=3), b'blo')
86+
87+
def test_blob_output_custom_output_content(self):
88+
class CustomOutput:
89+
def read(self) -> bytes:
90+
return b'custom_output_content'
91+
92+
# Try encoding a custom instance as an output return
93+
out = CustomOutput()
94+
result: Datum = afb.BlobConverter.encode(obj=out, expected_type=None)
95+
self.assertEqual(result.value, b'custom_output_content')
96+
self.assertEqual(result.type, 'bytes')
97+
98+
def test_blob_output_custom_output_without_read_method(self):
99+
class CustomOutput:
100+
def _read(self) -> bytes:
101+
return b'should_not_be_called'
102+
103+
# Try encoding a custom instance without read() method
104+
# This should raise an error when an unknown output is returned
105+
out = CustomOutput()
106+
with self.assertRaises(NotImplementedError):
107+
afb.BlobConverter.encode(obj=out, expected_type=None)
108+
109+
def test_blob_output_string(self):
110+
out: str = 'blob_output_string'
111+
result: Datum = afb.BlobConverter.encode(obj=out, expected_type=None)
112+
self.assertEqual(result.value, 'blob_output_string')
113+
self.assertEqual(result.type, 'string')
114+
115+
def test_blob_output_bytes(self):
116+
out: bytes = b'blob_output_bytes'
117+
result: Datum = afb.BlobConverter.encode(obj=out, expected_type=None)
118+
self.assertEqual(result.value, b'blob_output_bytes')
119+
self.assertEqual(result.type, 'bytes')
120+
121+
def test_blob_output_type(self):
122+
check_output_type = afb.BlobConverter.check_output_type_annotation
123+
self.assertTrue(check_output_type(str))
124+
self.assertTrue(check_output_type(bytes))
125+
self.assertTrue(check_output_type(bytearray))
126+
self.assertTrue(check_output_type(InputStream))
127+
128+
def test_blob_output_custom_type(self):
129+
class CustomOutput:
130+
def read(self) -> Datum:
131+
return Datum(b'custom_output_content', 'types')
132+
133+
check_output_type = afb.BlobConverter.check_output_type_annotation
134+
self.assertTrue(check_output_type(CustomOutput))

tests/test_eventhub.py

Lines changed: 75 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Copyright (c) Microsoft Corporation. All rights reserved.
22
# Licensed under the MIT License.
33

4-
from typing import List
4+
from typing import List, Mapping
55
import unittest
66
import json
77
from unittest.mock import patch
@@ -194,7 +194,7 @@ def test_single_eventhub_trigger_metadata_field(self):
194194
self.assertIsNotNone(metadata_dict.get('SystemProperties'))
195195

196196
# EnqueuedTime should be in iso8601 string format
197-
self.assertEqual(metadata_dict['EnqueuedTime'],
197+
self.assertEqual(metadata_dict['EnqueuedTimeUtc'],
198198
self.MOCKED_ENQUEUE_TIME.isoformat())
199199
self.assertEqual(metadata_dict['SystemProperties'][
200200
'iothub-connection-device-id'
@@ -224,6 +224,78 @@ def test_multiple_eventhub_triggers_metadata_field(self):
224224
'iothub-connection-device-id'
225225
], 'MyTestDevice1')
226226

227+
def test_eventhub_properties(self):
228+
"""Test if properties from public interface _eventhub.py returns
229+
the correct values from metadata"""
230+
231+
result = azf_eh.EventHubTriggerConverter.decode(
232+
data=meta.Datum(b'body_bytes', 'bytes'),
233+
trigger_metadata=self._generate_full_metadata()
234+
)
235+
236+
self.assertEqual(result.get_body(), b'body_bytes')
237+
self.assertIsNone(result.partition_key)
238+
self.assertDictEqual(result.iothub_metadata,
239+
{'connection-device-id': 'awesome-device-id'})
240+
self.assertEqual(result.sequence_number, 47)
241+
self.assertEqual(result.enqueued_time.isoformat(),
242+
'2020-07-14T01:27:55.627000+00:00')
243+
self.assertEqual(result.offset, '3696')
244+
245+
def _generate_full_metadata(self):
246+
mocked_metadata: Mapping[str, meta.Datum] = {}
247+
mocked_metadata['Offset'] = meta.Datum(type='string', value='3696')
248+
mocked_metadata['EnqueuedTimeUtc'] = meta.Datum(
249+
type='string', value='2020-07-14T01:27:55.627Z')
250+
mocked_metadata['SequenceNumber'] = meta.Datum(type='int', value=47)
251+
mocked_metadata['Properties'] = meta.Datum(type='json', value='{}')
252+
mocked_metadata['sys'] = meta.Datum(type='json', value='''
253+
{
254+
"MethodName":"metadata_trigger",
255+
"UtcNow":"2020-07-14T01:27:55.8940305Z",
256+
"RandGuid":"db413fd6-8411-4e51-844c-c9b5345e537d"
257+
}''')
258+
mocked_metadata['SystemProperties'] = meta.Datum(type='json', value='''
259+
{
260+
"x-opt-sequence-number":47,
261+
"x-opt-offset":"3696",
262+
"x-opt-enqueued-time":"2020-07-14T01:27:55.627Z",
263+
"SequenceNumber":47,
264+
"Offset":"3696",
265+
"PartitionKey":null,
266+
"EnqueuedTimeUtc":"2020-07-14T01:27:55.627Z",
267+
"iothub-connection-device-id":"awesome-device-id"
268+
}''')
269+
mocked_metadata['PartitionContext'] = meta.Datum(type='json', value='''
270+
{
271+
"CancellationToken":{
272+
"IsCancellationRequested":false,
273+
"CanBeCanceled":true,
274+
"WaitHandle":{
275+
"Handle":{
276+
"value":2472
277+
},
278+
"SafeWaitHandle":{
279+
"IsInvalid":false,
280+
"IsClosed":false
281+
}
282+
}
283+
},
284+
"ConsumerGroupName":"$Default",
285+
"EventHubPath":"python-worker-ci-eventhub-one-metadata",
286+
"PartitionId":"0",
287+
"Owner":"88cec2e2-94c9-4e08-acb6-4f2b97cd888e",
288+
"RuntimeInformation":{
289+
"PartitionId":"0",
290+
"LastSequenceNumber":0,
291+
"LastEnqueuedTimeUtc":"0001-01-01T00:00:00",
292+
"LastEnqueuedOffset":null,
293+
"RetrievalTime":"0001-01-01T00:00:00"
294+
}
295+
}''')
296+
297+
return mocked_metadata
298+
227299
def _generate_single_iothub_datum(self, datum_type='json'):
228300
datum = '{"device-status": "good"}'
229301
if datum_type == 'bytes':
@@ -248,7 +320,7 @@ def _generate_multiple_iothub_data(self, data_type='json'):
248320

249321
def _generate_single_trigger_metadatum(self):
250322
return {
251-
'EnqueuedTime': meta.Datum(
323+
'EnqueuedTimeUtc': meta.Datum(
252324
f'"{self.MOCKED_ENQUEUE_TIME.isoformat()}"', 'json'
253325
),
254326
'SystemProperties': meta.Datum(

0 commit comments

Comments
 (0)