Skip to content

feat: supporting EventHub trigger SDK bindings #101

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 28 commits into from
Apr 2, 2025
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
# Licensed under the MIT License.

import abc
import inspect
import collections.abc
import json
from typing import Any, Dict, Mapping, Optional, Tuple, Union
from typing import Any, Dict, Mapping, Optional, Tuple, Union, get_args, get_origin

from . import sdkType, utils

Expand Down Expand Up @@ -87,10 +87,34 @@ def get_raw_bindings(cls, indexed_function, input_types):
return utils.get_raw_bindings(indexed_function, input_types)

@classmethod
def check_supported_type(cls, subclass: type) -> bool:
if subclass is not None and inspect.isclass(subclass):
return issubclass(subclass, sdkType.SdkType)
return False
def check_supported_type(cls, annotation: type) -> bool:
if annotation is None:
return False

# The annotation is a class/type (not an object) - not iterable
if (isinstance(annotation, type)
and issubclass(annotation, sdkType.SdkType)):
return True

# An iterable who only has one inner type and is a subclass of SdkType
return cls._is_iterable_supported_type(annotation)

@classmethod
def _is_iterable_supported_type(cls, annotation: type) -> bool:
# Check base type from type hint. Ex: List from List[SdkType]
base_type = get_origin(annotation)
if (base_type is None
or not issubclass(base_type, collections.abc.Iterable)):
return False

inner_types = get_args(annotation)
if inner_types is None or len(inner_types) != 1:
return False

inner_type = inner_types[0]

return (isinstance(inner_type, type)
and issubclass(inner_type, sdkType.SdkType))

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

data_type = data.type
if data_type == "model_binding_data":
if (data_type == "model_binding_data"
or data_type == "collection_model_binding_data"):
result = data.value
elif data_type is None:
return None
Expand Down
2 changes: 0 additions & 2 deletions azurefunctions-extensions-base/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

"""Bootstrap for '$ python setup.py test' command."""

import os.path
import sys
import unittest
Expand Down
26 changes: 20 additions & 6 deletions azurefunctions-extensions-base/tests/test_meta.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
import sys
import unittest
from typing import List, Mapping
from unittest.mock import patch
Expand Down Expand Up @@ -149,10 +150,17 @@ class MockIndexedFunction:
self.assertEqual(registry.get_raw_bindings(MockIndexedFunction, []), ([], {}))

self.assertFalse(registry.check_supported_type(None))
self.assertFalse(registry.has_trigger_support(MockIndexedFunction))
self.assertFalse(registry.check_supported_type("hello"))
self.assertTrue(registry.check_supported_type(sdkType.SdkType))
self.assertTrue(registry.check_supported_type(List[sdkType.SdkType]))

self.assertFalse(registry.has_trigger_support(MockIndexedFunction))
# Generic types are not subscriptable in Python <3.9
if sys.version_info >= (3, 9):
self.assertTrue(registry.check_supported_type(list[sdkType.SdkType]))
self.assertTrue(registry.check_supported_type(tuple[sdkType.SdkType]))
self.assertTrue(registry.check_supported_type(set[sdkType.SdkType]))
self.assertFalse(registry.check_supported_type(dict[str, sdkType.SdkType]))

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

# Case 3: data.type is None
# Case 3: data.type is collection_model_binding_data
datum_cmbd = meta.Datum(value="{}", type="collection_model_binding_data")
self.assertEqual(
meta._BaseConverter._decode_typed_data(datum_cmbd, python_type=str), "{}"
)

# Case 4: data.type is None
datum_none = meta.Datum(value="{}", type=None)
self.assertIsNone(
meta._BaseConverter._decode_typed_data(datum_none, python_type=str)
)

# Case 4: data.type is unsupported
# Case 5: data.type is unsupported
datum_unsupp = meta.Datum(value="{}", type=dict)
with self.assertRaises(ValueError):
meta._BaseConverter._decode_typed_data(datum_unsupp, python_type=str)

# Case 5: can't coerce
# Case 6: can't coerce
datum_coerce_fail = meta.Datum(value="{}", type="model_binding_data")
with self.assertRaises(ValueError):
meta._BaseConverter._decode_typed_data(
datum_coerce_fail, python_type=(tuple, list, dict)
)

# Case 6: attempt coerce & fail
# Case 7: attempt coerce & fail
datum_attempt_coerce = meta.Datum(value=1, type="model_binding_data")
with self.assertRaises(ValueError):
meta._BaseConverter._decode_typed_data(
datum_attempt_coerce, python_type=dict
)

# Case 7: attempt to coerce and pass
# Case 8: attempt to coerce and pass
datum_coerce_pass = meta.Datum(value=1, type="model_binding_data")
self.assertEqual(
meta._BaseConverter._decode_typed_data(datum_coerce_pass, python_type=str),
Expand Down
14 changes: 8 additions & 6 deletions azurefunctions-extensions-bindings-blob/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ Blob client types can be generated from:
* Blob Triggers
* Blob Input

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


## Getting started
Expand Down Expand Up @@ -56,6 +56,8 @@ import logging
import azure.functions as func
import azurefunctions.extensions.bindings.blob as blob

app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS)

@app.blob_trigger(arg_name="client",
path="PATH/TO/BLOB",
connection="AzureWebJobsStorage")
Expand Down Expand Up @@ -85,19 +87,19 @@ This list can be used for reference to catch thrown exceptions. To get the speci

### More sample code

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

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

* [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:
* [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:
* From BlobTrigger
* From BlobInput

* [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:
* [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:
* From BlobTrigger
* From BlobInput

* [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:
* [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:
* From BlobTrigger
* From BlobInput

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
# Licensed under the MIT License.

import json
from typing import Union

from azure.identity import DefaultAzureCredential
from azure.storage.blob import BlobServiceClient
Expand All @@ -11,7 +10,7 @@


class BlobClient(SdkType):
def __init__(self, *, data: Union[bytes, Datum]) -> None:
def __init__(self, *, data: Datum) -> None:
# model_binding_data properties
self._data = data
self._using_managed_identity = False
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
# Licensed under the MIT License.

import json
from typing import Union

from azure.identity import DefaultAzureCredential
from azure.storage.blob import BlobServiceClient
Expand All @@ -11,7 +10,7 @@


class ContainerClient(SdkType):
def __init__(self, *, data: Union[bytes, Datum]) -> None:
def __init__(self, *, data: Datum) -> None:
# model_binding_data properties
self._data = data
self._using_managed_identity = False
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
# Licensed under the MIT License.

import json
from typing import Union

from azure.identity import DefaultAzureCredential
from azure.storage.blob import BlobServiceClient
Expand All @@ -11,7 +10,7 @@


class StorageStreamDownloader(SdkType):
def __init__(self, *, data: Union[bytes, Datum]) -> None:
def __init__(self, *, data: Datum) -> None:
# model_binding_data properties
self._data = data
self._using_managed_identity = False
Expand Down
8 changes: 4 additions & 4 deletions azurefunctions-extensions-bindings-blob/samples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@ These are code samples that show common scenario operations with the Azure Funct
These samples relate to the Azure Storage Blob client library being used as part of a Python Function App. For
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)

* [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:
* [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:
* From BlobTrigger
* From BlobInput

* [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:
* [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:
* From BlobTrigger
* From BlobInput

* [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:
* [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:
* From BlobTrigger
* From BlobInput

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

## Next steps

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
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
[API reference documentation](https://aka.ms/azsdk-python-storage-blob-ref) to learn more about
what you can do with the Azure Storage Blob client library.
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
# coding: utf-8

# -------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
"IsEncrypted": false,
"Values": {
"FUNCTIONS_WORKER_RUNTIME": "python",
"AzureWebJobsStorage": "",
"AzureWebJobsFeatureFlags": "EnableWorkerIndexing"
"AzureWebJobsStorage": "UseDevelopmentStorage=true"
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
# coding: utf-8

# -------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
"IsEncrypted": false,
"Values": {
"FUNCTIONS_WORKER_RUNTIME": "python",
"AzureWebJobsStorage": "",
"AzureWebJobsFeatureFlags": "EnableWorkerIndexing"
"AzureWebJobsStorage": "UseDevelopmentStorage=true"
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
# coding: utf-8

# -------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
"IsEncrypted": false,
"Values": {
"FUNCTIONS_WORKER_RUNTIME": "python",
"AzureWebJobsStorage": "",
"AzureWebJobsFeatureFlags": "EnableWorkerIndexing"
"AzureWebJobsStorage": "UseDevelopmentStorage=true"
}
}
3 changes: 0 additions & 3 deletions azurefunctions-extensions-bindings-blob/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

"""Bootstrap for '$ python setup.py test' command."""

import os.path
import sys
import unittest
import unittest.runner


def suite():
Expand Down
Empty file.
21 changes: 21 additions & 0 deletions azurefunctions-extensions-bindings-eventhub/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
Copyright (c) Microsoft Corporation.

MIT License

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
3 changes: 3 additions & 0 deletions azurefunctions-extensions-bindings-eventhub/MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
recursive-include azure *.py *.pyi
recursive-include tests *.py
include LICENSE README.md
Loading