Skip to content

Commit 59ec1ec

Browse files
authored
feat: supporting EventHub trigger SDK bindings (#101)
Allow customers to bind to the rich EventData type from the azure-eventhubs SDK for EventHub triggered functions Introduce collection_model_binding_data to support batch inputs (for EventHub and ServiceBus only)
1 parent 08f8ab9 commit 59ec1ec

File tree

40 files changed

+760
-58
lines changed

40 files changed

+760
-58
lines changed

azurefunctions-extensions-base/azurefunctions/extensions/base/meta.py

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
# Licensed under the MIT License.
33

44
import abc
5-
import inspect
5+
import collections.abc
66
import json
7-
from typing import Any, Dict, Mapping, Optional, Tuple, Union
7+
from typing import Any, Dict, Mapping, Optional, Tuple, Union, get_args, get_origin
88

99
from . import sdkType, utils
1010

@@ -87,10 +87,34 @@ def get_raw_bindings(cls, indexed_function, input_types):
8787
return utils.get_raw_bindings(indexed_function, input_types)
8888

8989
@classmethod
90-
def check_supported_type(cls, subclass: type) -> bool:
91-
if subclass is not None and inspect.isclass(subclass):
92-
return issubclass(subclass, sdkType.SdkType)
93-
return False
90+
def check_supported_type(cls, annotation: type) -> bool:
91+
if annotation is None:
92+
return False
93+
94+
# The annotation is a class/type (not an object) - not iterable
95+
if (isinstance(annotation, type)
96+
and issubclass(annotation, sdkType.SdkType)):
97+
return True
98+
99+
# An iterable who only has one inner type and is a subclass of SdkType
100+
return cls._is_iterable_supported_type(annotation)
101+
102+
@classmethod
103+
def _is_iterable_supported_type(cls, annotation: type) -> bool:
104+
# Check base type from type hint. Ex: List from List[SdkType]
105+
base_type = get_origin(annotation)
106+
if (base_type is None
107+
or not issubclass(base_type, collections.abc.Iterable)):
108+
return False
109+
110+
inner_types = get_args(annotation)
111+
if inner_types is None or len(inner_types) != 1:
112+
return False
113+
114+
inner_type = inner_types[0]
115+
116+
return (isinstance(inner_type, type)
117+
and issubclass(inner_type, sdkType.SdkType))
94118

95119
def has_trigger_support(cls) -> bool:
96120
return cls._trigger is not None # type: ignore
@@ -110,7 +134,8 @@ def _decode_typed_data(
110134
return None
111135

112136
data_type = data.type
113-
if data_type == "model_binding_data":
137+
if (data_type == "model_binding_data"
138+
or data_type == "collection_model_binding_data"):
114139
result = data.value
115140
elif data_type is None:
116141
return None

azurefunctions-extensions-base/tests/__init__.py

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

4-
"""Bootstrap for '$ python setup.py test' command."""
5-
64
import os.path
75
import sys
86
import unittest

azurefunctions-extensions-base/tests/test_meta.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Copyright (c) Microsoft Corporation. All rights reserved.
22
# Licensed under the MIT License.
3+
import sys
34
import unittest
45
from typing import List, Mapping
56
from unittest.mock import patch
@@ -149,10 +150,17 @@ class MockIndexedFunction:
149150
self.assertEqual(registry.get_raw_bindings(MockIndexedFunction, []), ([], {}))
150151

151152
self.assertFalse(registry.check_supported_type(None))
153+
self.assertFalse(registry.has_trigger_support(MockIndexedFunction))
152154
self.assertFalse(registry.check_supported_type("hello"))
153155
self.assertTrue(registry.check_supported_type(sdkType.SdkType))
156+
self.assertTrue(registry.check_supported_type(List[sdkType.SdkType]))
154157

155-
self.assertFalse(registry.has_trigger_support(MockIndexedFunction))
158+
# Generic types are not subscriptable in Python <3.9
159+
if sys.version_info >= (3, 9):
160+
self.assertTrue(registry.check_supported_type(list[sdkType.SdkType]))
161+
self.assertTrue(registry.check_supported_type(tuple[sdkType.SdkType]))
162+
self.assertTrue(registry.check_supported_type(set[sdkType.SdkType]))
163+
self.assertFalse(registry.check_supported_type(dict[str, sdkType.SdkType]))
156164

157165
def test_decode_typed_data(self):
158166
# Case 1: data is None
@@ -166,32 +174,38 @@ def test_decode_typed_data(self):
166174
meta._BaseConverter._decode_typed_data(datum_mbd, python_type=str), "{}"
167175
)
168176

169-
# Case 3: data.type is None
177+
# Case 3: data.type is collection_model_binding_data
178+
datum_cmbd = meta.Datum(value="{}", type="collection_model_binding_data")
179+
self.assertEqual(
180+
meta._BaseConverter._decode_typed_data(datum_cmbd, python_type=str), "{}"
181+
)
182+
183+
# Case 4: data.type is None
170184
datum_none = meta.Datum(value="{}", type=None)
171185
self.assertIsNone(
172186
meta._BaseConverter._decode_typed_data(datum_none, python_type=str)
173187
)
174188

175-
# Case 4: data.type is unsupported
189+
# Case 5: data.type is unsupported
176190
datum_unsupp = meta.Datum(value="{}", type=dict)
177191
with self.assertRaises(ValueError):
178192
meta._BaseConverter._decode_typed_data(datum_unsupp, python_type=str)
179193

180-
# Case 5: can't coerce
194+
# Case 6: can't coerce
181195
datum_coerce_fail = meta.Datum(value="{}", type="model_binding_data")
182196
with self.assertRaises(ValueError):
183197
meta._BaseConverter._decode_typed_data(
184198
datum_coerce_fail, python_type=(tuple, list, dict)
185199
)
186200

187-
# Case 6: attempt coerce & fail
201+
# Case 7: attempt coerce & fail
188202
datum_attempt_coerce = meta.Datum(value=1, type="model_binding_data")
189203
with self.assertRaises(ValueError):
190204
meta._BaseConverter._decode_typed_data(
191205
datum_attempt_coerce, python_type=dict
192206
)
193207

194-
# Case 7: attempt to coerce and pass
208+
# Case 8: attempt to coerce and pass
195209
datum_coerce_pass = meta.Datum(value=1, type="model_binding_data")
196210
self.assertEqual(
197211
meta._BaseConverter._decode_typed_data(datum_coerce_pass, python_type=str),

azurefunctions-extensions-bindings-blob/README.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ Blob client types can be generated from:
77
* Blob Triggers
88
* Blob Input
99

10-
[Source code](https://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-bindings-blob)
10+
[Source code](https://github.com/Azure/azure-functions-python-extensions/tree/dev/azurefunctions-extensions-bindings-blob)
1111
[Package (PyPi)](https://pypi.org/project/azurefunctions-extensions-bindings-blob/)
1212
| API reference documentation
1313
| Product documentation
14-
| [Samples](hhttps://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-bindings-blob/samples)
14+
| [Samples](https://github.com/Azure/azure-functions-python-extensions/tree/dev/azurefunctions-extensions-bindings-blob/samples)
1515

1616

1717
## Getting started
@@ -56,6 +56,8 @@ import logging
5656
import azure.functions as func
5757
import azurefunctions.extensions.bindings.blob as blob
5858

59+
app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS)
60+
5961
@app.blob_trigger(arg_name="client",
6062
path="PATH/TO/BLOB",
6163
connection="AzureWebJobsStorage")
@@ -85,19 +87,19 @@ This list can be used for reference to catch thrown exceptions. To get the speci
8587

8688
### More sample code
8789

88-
Get started with our [Blob samples](hhttps://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-bindings-blob/samples).
90+
Get started with our [Blob samples](https://github.com/Azure/azure-functions-python-extensions/tree/dev/azurefunctions-extensions-bindings-blob/samples).
8991

9092
Several samples are available in this GitHub repository. These samples provide example code for additional scenarios commonly encountered while working with Storage Blobs:
9193

92-
* [blob_samples_blobclient](https://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-bindings-blob/samples/blob_samples_blobclient) - Examples for using the BlobClient type:
94+
* [blob_samples_blobclient](https://github.com/Azure/azure-functions-python-extensions/tree/dev/azurefunctions-extensions-bindings-blob/samples/blob_samples_blobclient) - Examples for using the BlobClient type:
9395
* From BlobTrigger
9496
* From BlobInput
9597

96-
* [blob_samples_containerclient](https://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-bindings-blob/samples/blob_samples_containerclient) - Examples for using the ContainerClient type:
98+
* [blob_samples_containerclient](https://github.com/Azure/azure-functions-python-extensions/tree/dev/azurefunctions-extensions-bindings-blob/samples/blob_samples_containerclient) - Examples for using the ContainerClient type:
9799
* From BlobTrigger
98100
* From BlobInput
99101

100-
* [blob_samples_storagestreamdownloader](https://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-bindings-blob/samples/blob_samples_storagestreamdownloader) - Examples for using the StorageStreamDownloader type:
102+
* [blob_samples_storagestreamdownloader](https://github.com/Azure/azure-functions-python-extensions/tree/dev/azurefunctions-extensions-bindings-blob/samples/blob_samples_storagestreamdownloader) - Examples for using the StorageStreamDownloader type:
101103
* From BlobTrigger
102104
* From BlobInput
103105

azurefunctions-extensions-bindings-blob/azurefunctions/extensions/bindings/blob/blobClient.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
# Licensed under the MIT License.
33

44
import json
5-
from typing import Union
65

76
from azure.identity import DefaultAzureCredential
87
from azure.storage.blob import BlobServiceClient
@@ -11,7 +10,7 @@
1110

1211

1312
class BlobClient(SdkType):
14-
def __init__(self, *, data: Union[bytes, Datum]) -> None:
13+
def __init__(self, *, data: Datum) -> None:
1514
# model_binding_data properties
1615
self._data = data
1716
self._using_managed_identity = False

azurefunctions-extensions-bindings-blob/azurefunctions/extensions/bindings/blob/containerClient.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
# Licensed under the MIT License.
33

44
import json
5-
from typing import Union
65

76
from azure.identity import DefaultAzureCredential
87
from azure.storage.blob import BlobServiceClient
@@ -11,7 +10,7 @@
1110

1211

1312
class ContainerClient(SdkType):
14-
def __init__(self, *, data: Union[bytes, Datum]) -> None:
13+
def __init__(self, *, data: Datum) -> None:
1514
# model_binding_data properties
1615
self._data = data
1716
self._using_managed_identity = False

azurefunctions-extensions-bindings-blob/azurefunctions/extensions/bindings/blob/storageStreamDownloader.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
# Licensed under the MIT License.
33

44
import json
5-
from typing import Union
65

76
from azure.identity import DefaultAzureCredential
87
from azure.storage.blob import BlobServiceClient
@@ -11,7 +10,7 @@
1110

1211

1312
class StorageStreamDownloader(SdkType):
14-
def __init__(self, *, data: Union[bytes, Datum]) -> None:
13+
def __init__(self, *, data: Datum) -> None:
1514
# model_binding_data properties
1615
self._data = data
1716
self._using_managed_identity = False

azurefunctions-extensions-bindings-blob/samples/README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,15 @@ These are code samples that show common scenario operations with the Azure Funct
1717
These samples relate to the Azure Storage Blob client library being used as part of a Python Function App. For
1818
examples on how to use the Azure Storage Blob client library, please see [Azure Storage Blob samples](https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/storage/azure-storage-blob/samples)
1919

20-
* [blob_samples_blobclient](https://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-bindings-blob/samples/blob_samples_blobclient) - Examples for using the BlobClient type:
20+
* [blob_samples_blobclient](https://github.com/Azure/azure-functions-python-extensions/tree/dev/azurefunctions-extensions-bindings-blob/samples/blob_samples_blobclient) - Examples for using the BlobClient type:
2121
* From BlobTrigger
2222
* From BlobInput
2323

24-
* [blob_samples_containerclient](https://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-bindings-blob/samples/blob_samples_containerclient) - Examples for using the ContainerClient type:
24+
* [blob_samples_containerclient](https://github.com/Azure/azure-functions-python-extensions/tree/dev/azurefunctions-extensions-bindings-blob/samples/blob_samples_containerclient) - Examples for using the ContainerClient type:
2525
* From BlobTrigger
2626
* From BlobInput
2727

28-
* [blob_samples_storagestreamdownloader](https://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-bindings-blob/samples/blob_samples_storagestreamdownloader) - Examples for using the StorageStreamDownloader type:
28+
* [blob_samples_storagestreamdownloader](https://github.com/Azure/azure-functions-python-extensions/tree/dev/azurefunctions-extensions-bindings-blob/samples/blob_samples_storagestreamdownloader) - Examples for using the StorageStreamDownloader type:
2929
* From BlobTrigger
3030
* From BlobInput
3131

@@ -63,6 +63,6 @@ based on the type of function you wish to execute.
6363

6464
## Next steps
6565

66-
Visit the [SDK-type bindings in Python reference documentation]() to learn more about how to use SDK-type bindings in a Python Function App and the
66+
Visit the [SDK-type bindings in Python reference documentation](https://learn.microsoft.com/en-us/azure/azure-functions/functions-reference-python?tabs=get-started%2Casgi%2Capplication-level&pivots=python-mode-decorators#sdk-type-bindings-preview) to learn more about how to use SDK-type bindings in a Python Function App and the
6767
[API reference documentation](https://aka.ms/azsdk-python-storage-blob-ref) to learn more about
6868
what you can do with the Azure Storage Blob client library.

azurefunctions-extensions-bindings-blob/samples/blob_samples_blobclient/function_app.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
# coding: utf-8
2-
31
# -------------------------------------------------------------------------
42
# Copyright (c) Microsoft Corporation. All rights reserved.
53
# Licensed under the MIT License. See License.txt in the project root for

azurefunctions-extensions-bindings-blob/samples/blob_samples_blobclient/local.settings.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
"IsEncrypted": false,
33
"Values": {
44
"FUNCTIONS_WORKER_RUNTIME": "python",
5-
"AzureWebJobsStorage": "",
6-
"AzureWebJobsFeatureFlags": "EnableWorkerIndexing"
5+
"AzureWebJobsStorage": "UseDevelopmentStorage=true"
76
}
87
}

azurefunctions-extensions-bindings-blob/samples/blob_samples_containerclient/function_app.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
# coding: utf-8
2-
31
# -------------------------------------------------------------------------
42
# Copyright (c) Microsoft Corporation. All rights reserved.
53
# Licensed under the MIT License. See License.txt in the project root for

azurefunctions-extensions-bindings-blob/samples/blob_samples_containerclient/local.settings.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
"IsEncrypted": false,
33
"Values": {
44
"FUNCTIONS_WORKER_RUNTIME": "python",
5-
"AzureWebJobsStorage": "",
6-
"AzureWebJobsFeatureFlags": "EnableWorkerIndexing"
5+
"AzureWebJobsStorage": "UseDevelopmentStorage=true"
76
}
87
}

azurefunctions-extensions-bindings-blob/samples/blob_samples_storagestreamdownloader/function_app.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
# coding: utf-8
2-
31
# -------------------------------------------------------------------------
42
# Copyright (c) Microsoft Corporation. All rights reserved.
53
# Licensed under the MIT License. See License.txt in the project root for

azurefunctions-extensions-bindings-blob/samples/blob_samples_storagestreamdownloader/local.settings.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
"IsEncrypted": false,
33
"Values": {
44
"FUNCTIONS_WORKER_RUNTIME": "python",
5-
"AzureWebJobsStorage": "",
6-
"AzureWebJobsFeatureFlags": "EnableWorkerIndexing"
5+
"AzureWebJobsStorage": "UseDevelopmentStorage=true"
76
}
87
}

azurefunctions-extensions-bindings-blob/tests/__init__.py

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

4-
"""Bootstrap for '$ python setup.py test' command."""
5-
64
import os.path
75
import sys
86
import unittest
9-
import unittest.runner
107

118

129
def suite():

azurefunctions-extensions-bindings-eventhub/CHANGELOG.md

Whitespace-only changes.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
Copyright (c) Microsoft Corporation.
2+
3+
MIT License
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
recursive-include azure *.py *.pyi
2+
recursive-include tests *.py
3+
include LICENSE README.md

0 commit comments

Comments
 (0)