Skip to content

Commit cb8a6e3

Browse files
authored
fix: test fixes for EventHub (#108)
* misc. small fixes * fix pyproject format * fix notequal
1 parent 59ec1ec commit cb8a6e3

File tree

8 files changed

+122
-54
lines changed

8 files changed

+122
-54
lines changed

azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventData.py

+8-8
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from typing import Optional
55
import uamqp
66

7-
from azure.eventhub import EventData
7+
from azure.eventhub import EventData as EventDataSDK
88
from azurefunctions.extensions.base import Datum, SdkType
99

1010

@@ -23,30 +23,30 @@ def __init__(self, *, data: Datum) -> None:
2323
self._content_type = data.content_type
2424
self._content = data.content
2525
self.decoded_message = self._get_eventhub_content(self._content)
26-
26+
2727
def _get_eventhub_content(self, content):
2828
"""
2929
When receiving the EventBindingData, the content field is in the form of bytes.
30-
This content must be decoded in order to construct an EventData object from the azure.eventhub SDK.
31-
The .NET worker uses the Azure.Core.Amqp library to do this:
30+
This content must be decoded in order to construct an EventData object from the
31+
azure.eventhub SDK. The .NET worker uses the Azure.Core.Amqp library to do this:
3232
https://github.com/Azure/azure-functions-dotnet-worker/blob/main/extensions/Worker.Extensions.EventHubs/src/EventDataConverter.cs#L45
3333
"""
3434
if content:
3535
try:
3636
return uamqp.Message().decode_from_bytes(content)
3737
except Exception as e:
3838
raise ValueError(f"Failed to decode EventHub content: {e}") from e
39-
39+
4040
return None
4141

42-
def get_sdk_type(self) -> Optional[EventData]:
42+
def get_sdk_type(self) -> Optional[EventDataSDK]:
4343
"""
4444
When receiving an EventHub message, the content portion after being decoded
4545
is used in the constructor to create an EventData object. This will contain
4646
fields such as message, enqueued_time, and more.
4747
"""
4848
# https://github.com/Azure/azure-sdk-for-python/issues/39711
4949
if self.decoded_message:
50-
return EventData._from_message(self.decoded_message)
51-
50+
return EventDataSDK._from_message(self.decoded_message)
51+
5252
return None

azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventDataConverter.py

+18-14
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,15 @@ class EventDataConverter(
1818
def check_input_type_annotation(cls, pytype: type) -> bool:
1919
if pytype is None:
2020
return False
21-
21+
2222
# The annotation is a class/type (not an object) - not iterable
2323
if (isinstance(pytype, type)
2424
and issubclass(pytype, EventData)):
2525
return True
26-
26+
2727
# An iterable who only has one inner type and is a subclass of SdkType
2828
return cls._is_iterable_supported_type(pytype)
29-
29+
3030
@classmethod
3131
def _is_iterable_supported_type(cls, annotation: type) -> bool:
3232
# Check base type from type hint. Ex: List from List[SdkType]
@@ -38,37 +38,41 @@ def _is_iterable_supported_type(cls, annotation: type) -> bool:
3838
inner_types = get_args(annotation)
3939
if inner_types is None or len(inner_types) != 1:
4040
return False
41-
41+
4242
inner_type = inner_types[0]
4343

44-
return (isinstance(inner_type, type)
44+
return (isinstance(inner_type, type)
4545
and issubclass(inner_type, EventData))
4646

4747
@classmethod
4848
def decode(cls, data: Datum, *, trigger_metadata, pytype) -> Optional[Any]:
4949
"""
50-
EventHub allows for batches to be sent. This means the cardinality can be one or many
50+
EventHub allows for batches. This means the cardinality can be one or many.
5151
When the cardinality is one:
52-
- The data is of type "model_binding_data" - each event is an independent function invocation
52+
- The data is of type "model_binding_data" - each event is an independent
53+
function invocation
5354
When the cardinality is many:
54-
- The data is of type "collection_model_binding_data" - all events are sent in a single function invocation
55+
- The data is of type "collection_model_binding_data" - all events are sent
56+
in a single function invocation
5557
- collection_model_binding_data has 1 or more model_binding_data objects
5658
"""
57-
if data is None or data.type is None or pytype != EventData:
59+
if data is None or data.type is None:
5860
return None
5961

6062
# Process each model_binding_data in the collection
6163
if data.type == "collection_model_binding_data":
6264
try:
63-
return [EventData(data=mbd).get_sdk_type() for mbd in data.value.model_binding_data]
65+
return [EventData(data=mbd).get_sdk_type()
66+
for mbd in data.value.model_binding_data]
6467
except Exception as e:
65-
raise ValueError("Failed to decode incoming EventHub batch: " + repr(e)) from e
66-
68+
raise ValueError("Failed to decode incoming EventHub batch: "
69+
+ repr(e)) from e
70+
6771
# Get model_binding_data fields directly
6872
if data.type == "model_binding_data":
6973
return EventData(data=data.value).get_sdk_type()
70-
74+
7175
raise ValueError(
7276
"Unexpected type of data received for the 'eventhub' binding: "
7377
+ repr(data.type)
74-
)
78+
)

azurefunctions-extensions-bindings-eventhub/pyproject.toml

+5-2
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ classifiers= [
2626
]
2727
dependencies = [
2828
'azurefunctions-extensions-base',
29-
'azure-eventhub~=5.13.0'
29+
'azure-eventhub~=5.13.0',
30+
'uamqp~=1.0'
3031
]
3132

3233
[project.optional-dependencies]
@@ -35,7 +36,9 @@ dev = [
3536
'pytest-cov',
3637
'coverage',
3738
'pytest-instafail',
38-
'pre-commit'
39+
'pre-commit',
40+
'mypy',
41+
'flake8'
3942
]
4043

4144
[tool.setuptools.dynamic]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
import pathlib
4+
import subprocess
5+
import sys
6+
import unittest
7+
8+
ROOT_PATH = pathlib.Path(__file__).parent.parent.parent
9+
10+
11+
class TestCodeQuality(unittest.TestCase):
12+
def test_mypy(self):
13+
try:
14+
import mypy # NoQA
15+
except ImportError as e:
16+
raise unittest.SkipTest('mypy module is missing') from e
17+
18+
try:
19+
subprocess.run(
20+
[sys.executable, '-m', 'mypy', '-m',
21+
'azurefunctions-extensions-bindings-eventhub'],
22+
check=True,
23+
stdout=subprocess.PIPE,
24+
stderr=subprocess.PIPE,
25+
cwd=str(ROOT_PATH))
26+
except subprocess.CalledProcessError as ex:
27+
output = ex.output.decode()
28+
raise AssertionError(
29+
f'mypy validation failed:\n{output}') from None
30+
31+
def test_flake8(self):
32+
try:
33+
import flake8 # NoQA
34+
except ImportError as e:
35+
raise unittest.SkipTest('flake8 module is missing') from e
36+
37+
config_path = ROOT_PATH / '.flake8'
38+
if not config_path.exists():
39+
raise unittest.SkipTest('could not locate the .flake8 file')
40+
41+
try:
42+
subprocess.run(
43+
[sys.executable, '-m', 'flake8',
44+
'azurefunctions-extensions-bindings-eventhub',
45+
'--config', str(config_path)],
46+
check=True,
47+
stdout=subprocess.PIPE,
48+
stderr=subprocess.PIPE,
49+
cwd=str(ROOT_PATH))
50+
except subprocess.CalledProcessError as ex:
51+
output = ex.output.decode()
52+
raise AssertionError(
53+
f'flake8 validation failed:\n{output}') from None

azurefunctions-extensions-bindings-eventhub/tests/test_eventdata.py

+31-29
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
# Copyright (c) Microsoft Corporation. All rights reserved.
22
# Licensed under the MIT License.
33

4-
import sys
54
import unittest
65
from typing import List, Optional
76

@@ -10,7 +9,8 @@
109

1110
from azurefunctions.extensions.bindings.eventhub import EventData, EventDataConverter
1211

13-
EVENTHUB_SAMPLE_CONTENT = b"\x00Sr\xc1\x8e\x08\xa3\x1bx-opt-sequence-number-epochT\xff\xa3\x15x-opt-sequence-numberU\x04\xa3\x0cx-opt-offset\x81\x00\x00\x00\x01\x00\x00\x010\xa3\x13x-opt-enqueued-time\x00\xa3\x1dcom.microsoft:datetime-offset\x81\x08\xddW\x05\xc3Q\xcf\x10\x00St\xc1I\x02\xa1\rDiagnostic-Id\xa1700-bdc3fde4889b4e907e0c9dcb46ff8d92-21f637af293ef13b-00\x00Su\xa0\x08message1"
12+
EVENTHUB_SAMPLE_CONTENT = b"\x00Sr\xc1\x8e\x08\xa3\x1bx-opt-sequence-number-epochT\xff\xa3\x15x-opt-sequence-numberU\x04\xa3\x0cx-opt-offset\x81\x00\x00\x00\x01\x00\x00\x010\xa3\x13x-opt-enqueued-time\x00\xa3\x1dcom.microsoft:datetime-offset\x81\x08\xddW\x05\xc3Q\xcf\x10\x00St\xc1I\x02\xa1\rDiagnostic-Id\xa1700-bdc3fde4889b4e907e0c9dcb46ff8d92-21f637af293ef13b-00\x00Su\xa0\x08message1" # noqa: E501
13+
1414

1515
# Mock classes for testing
1616
class MockMBD:
@@ -27,8 +27,8 @@ def data_type(self) -> Optional[int]:
2727
@property
2828
def direction(self) -> int:
2929
return self._direction.value
30-
31-
30+
31+
3232
class MockCMBD:
3333
def __init__(self, model_binding_data_list: List[MockMBD]):
3434
self.model_binding_data = model_binding_data_list
@@ -83,18 +83,19 @@ def test_input_empty_mbd(self):
8383
self.assertIsNone(result)
8484

8585
def test_input_empty_cmbd(self):
86-
datum: Datum = Datum(value={}, type="collection_model_binding_data")
86+
datum: Datum = Datum(value=MockCMBD([None]),
87+
type="collection_model_binding_data")
8788
result: EventData = EventDataConverter.decode(
8889
data=datum, trigger_metadata=None, pytype=EventData
8990
)
90-
self.assertIsNone(result)
91+
self.assertEqual(result, [None])
9192

9293
def test_input_populated_mbd(self):
9394
sample_mbd = MockMBD(
9495
version="1.0",
9596
source="AzureEventHubsEventData",
9697
content_type="application/octet-stream",
97-
content = EVENTHUB_SAMPLE_CONTENT
98+
content=EVENTHUB_SAMPLE_CONTENT
9899
)
99100

100101
datum: Datum = Datum(value=sample_mbd, type="model_binding_data")
@@ -115,33 +116,34 @@ def test_input_populated_cmbd(self):
115116
version="1.0",
116117
source="AzureEventHubsEventData",
117118
content_type="application/octet-stream",
118-
content = EVENTHUB_SAMPLE_CONTENT
119+
content=EVENTHUB_SAMPLE_CONTENT
119120
)
120121

121-
datum: Datum = Datum(value=MockCMBD([sample_mbd, sample_mbd]), type="collection_model_binding_data")
122+
datum: Datum = Datum(value=MockCMBD([sample_mbd, sample_mbd]),
123+
type="collection_model_binding_data")
122124
result: EventData = EventDataConverter.decode(
123125
data=datum, trigger_metadata=None, pytype=EventData
124126
)
125127

126128
self.assertIsNotNone(result)
127-
self.assertIsInstance(result, EventDataSdk)
128-
129-
sdk_result = EventData(data=datum.value).get_sdk_type()
130-
131-
self.assertIsNotNone(sdk_result)
132-
self.assertIsInstance(sdk_result, EventDataSdk)
133-
134-
def test_input_invalid_pytype(self):
135-
sample_mbd = MockMBD(
136-
version="1.0",
137-
source="AzureEventHubsEventData",
138-
content_type="application/octet-stream",
139-
content = EVENTHUB_SAMPLE_CONTENT
140-
)
141-
142-
datum: Datum = Datum(value=sample_mbd, type="model_binding_data")
143-
result: EventData = EventDataConverter.decode(
144-
data=datum, trigger_metadata=None, pytype="str"
129+
for event_data in result:
130+
self.assertIsInstance(event_data, EventDataSdk)
131+
132+
sdk_results = []
133+
for mbd in datum.value.model_binding_data:
134+
sdk_results.append(EventData(data=mbd).get_sdk_type())
135+
136+
self.assertNotEqual(sdk_results, [None, None])
137+
for event_data in sdk_results:
138+
self.assertIsInstance(event_data, EventDataSdk)
139+
140+
def test_input_invalid_datum_type(self):
141+
with self.assertRaises(ValueError) as e:
142+
datum: Datum = Datum(value="hello", type="str")
143+
_: EventData = EventDataConverter.decode(
144+
data=datum, trigger_metadata=None, pytype=""
145+
)
146+
self.assertEqual(
147+
e.exception.args[0],
148+
"Unexpected type of data received for the 'eventhub' binding: 'str'",
145149
)
146-
147-
self.assertIsNone(result)

eng/templates/jobs/build.yml

+3
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ jobs:
1010
blob_extension:
1111
EXTENSION_DIRECTORY: 'azurefunctions-extensions-bindings-blob'
1212
EXTENSION_NAME: 'Blob'
13+
eventhub_extension:
14+
EXTENSION_DIRECTORY: 'azurefunctions-extensions-bindings-eventhub'
15+
EXTENSION_NAME: 'EventHub'
1316
fastapi_extension:
1417
EXTENSION_DIRECTORY: 'azurefunctions-extensions-http-fastapi'
1518
EXTENSION_NAME: 'Http'

eng/templates/official/jobs/build-artifacts.yml

+3
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ jobs:
1010
blob_extension:
1111
EXTENSION_DIRECTORY: 'azurefunctions-extensions-bindings-blob'
1212
EXTENSION_NAME: 'Blob'
13+
eventhub_extension:
14+
EXTENSION_DIRECTORY: 'azurefunctions-extensions-bindings-eventhub'
15+
EXTENSION_NAME: 'EventHub'
1316
fastapi_extension:
1417
EXTENSION_DIRECTORY: 'azurefunctions-extensions-http-fastapi'
1518
EXTENSION_NAME: 'FastAPI'

eng/templates/official/jobs/eventhub-unit-tests.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
python -m pip install -U -e .[dev]
2424
displayName: 'Install dependencies'
2525
- bash: |
26-
python -m pytest -q --instafail azurefunctions-extensions-bindings-eventhub/tests/ --ignore='azurefunctions-extensions-base', --ignore='azurefunctions-extensions-http-fastapi', --ignore='azurefunctions-extensions-bindings-blob'
26+
python -m pytest -q --instafail azurefunctions-extensions-bindings-eventhub/tests/
2727
env:
2828
AzureWebJobsStorage: $(AzureWebJobsStorage)
2929
displayName: "Running EventHub $(PYTHON_VERSION) Python Extension Tests"

0 commit comments

Comments
 (0)