From 4b4f59d5fed3c3bbf01bfd1afc64fa211807e341 Mon Sep 17 00:00:00 2001 From: Evan Roman Date: Fri, 28 Feb 2025 15:41:12 -0600 Subject: [PATCH 01/26] Chng --- .../CHANGELOG.md | 0 .../LICENSE | 21 +++ .../MANIFEST.in | 3 + .../README.md | 113 ++++++++++++ .../azurefunctions/__init__.py | 1 + .../azurefunctions/extensions/__init__.py | 1 + .../extensions/bindings/__init__.py | 1 + .../extensions/bindings/eventhub/__init__.py | 12 ++ .../bindings/eventhub/eventHubData.py | 39 ++++ .../eventhub/eventHubDataConverter.py | 42 +++++ .../pyproject.toml | 49 +++++ .../samples/README.md | 58 ++++++ .../function_app.py | 41 +++++ .../eventhub_samples_eventhubdata/host.json | 15 ++ .../local.settings.json | 8 + .../requirements.txt | 6 + .../tests/__init__.py | 20 ++ .../tests/test_eventhubdata.py | 174 ++++++++++++++++++ function_app.py | 27 +++ host.json | 15 ++ local.settings.json | 7 + requirements.txt | 5 + 22 files changed, 658 insertions(+) create mode 100644 azurefunctions-extensions-bindings-eventhub/CHANGELOG.md create mode 100644 azurefunctions-extensions-bindings-eventhub/LICENSE create mode 100644 azurefunctions-extensions-bindings-eventhub/MANIFEST.in create mode 100644 azurefunctions-extensions-bindings-eventhub/README.md create mode 100644 azurefunctions-extensions-bindings-eventhub/azurefunctions/__init__.py create mode 100644 azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/__init__.py create mode 100644 azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/__init__.py create mode 100644 azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/__init__.py create mode 100644 azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventHubData.py create mode 100644 azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventHubDataConverter.py create mode 100644 azurefunctions-extensions-bindings-eventhub/pyproject.toml create mode 100644 azurefunctions-extensions-bindings-eventhub/samples/README.md create mode 100644 azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventhubdata/function_app.py create mode 100644 azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventhubdata/host.json create mode 100644 azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventhubdata/local.settings.json create mode 100644 azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventhubdata/requirements.txt create mode 100644 azurefunctions-extensions-bindings-eventhub/tests/__init__.py create mode 100644 azurefunctions-extensions-bindings-eventhub/tests/test_eventhubdata.py create mode 100644 function_app.py create mode 100644 host.json create mode 100644 local.settings.json create mode 100644 requirements.txt diff --git a/azurefunctions-extensions-bindings-eventhub/CHANGELOG.md b/azurefunctions-extensions-bindings-eventhub/CHANGELOG.md new file mode 100644 index 0000000..e69de29 diff --git a/azurefunctions-extensions-bindings-eventhub/LICENSE b/azurefunctions-extensions-bindings-eventhub/LICENSE new file mode 100644 index 0000000..63447fd --- /dev/null +++ b/azurefunctions-extensions-bindings-eventhub/LICENSE @@ -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. \ No newline at end of file diff --git a/azurefunctions-extensions-bindings-eventhub/MANIFEST.in b/azurefunctions-extensions-bindings-eventhub/MANIFEST.in new file mode 100644 index 0000000..e1ae5ad --- /dev/null +++ b/azurefunctions-extensions-bindings-eventhub/MANIFEST.in @@ -0,0 +1,3 @@ +recursive-include azure *.py *.pyi +recursive-include tests *.py +include LICENSE README.md \ No newline at end of file diff --git a/azurefunctions-extensions-bindings-eventhub/README.md b/azurefunctions-extensions-bindings-eventhub/README.md new file mode 100644 index 0000000..20897a5 --- /dev/null +++ b/azurefunctions-extensions-bindings-eventhub/README.md @@ -0,0 +1,113 @@ +# Azure Functions Extensions Bindings EventHub library for Python +This library allows Blob Trigger and Blob Input bindings in Python Function Apps to recognize and bind to client types from the +Azure Storage Blob sdk. + +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) +[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) + + +## Getting started + +### Prerequisites +* Python 3.9 or later is required to use this package. For more details, please read our page on [Python Functions version support policy](https://learn.microsoft.com/en-us/azure/azure-functions/functions-versions?tabs=isolated-process%2Cv4&pivots=programming-language-python#languages). + +* You must have an [Azure subscription](https://azure.microsoft.com/free/) and an +[Azure storage account](https://docs.microsoft.com/azure/storage/common/storage-account-overview) to use this package. + +### Install the package +Install the Azure Functions Extensions Bindings Blob library for Python with pip: + +```bash +pip install azurefunctions-extensions-bindings-blob +``` + +### Create a storage account +If you wish to create a new storage account, you can use the +[Azure Portal](https://docs.microsoft.com/azure/storage/common/storage-quickstart-create-account?tabs=azure-portal), +[Azure PowerShell](https://docs.microsoft.com/azure/storage/common/storage-quickstart-create-account?tabs=azure-powershell), +or [Azure CLI](https://docs.microsoft.com/azure/storage/common/storage-quickstart-create-account?tabs=azure-cli): + +```bash +# Create a new resource group to hold the storage account - +# if using an existing resource group, skip this step +az group create --name my-resource-group --location westus2 + +# Create the storage account +az storage account create -n my-storage-account-name -g my-resource-group +``` + +### Bind to the SDK-type +The Azure Functions Extensions Bindings Blob library for Python allows you to create a function app with a Blob Trigger or +Blob Input and define the type as a BlobClient, ContainerClient, or StorageStreamDownloader. Instead of receiving +an InputStream, when the function is executed, the type returned will be the defined SDK-type and have all of the +properties and methods available as seen in the Azure Storage Blob library for Python. + + +```python +import logging +import azure.functions as func +import azurefunctions.extensions.bindings.blob as blob + +@app.blob_trigger(arg_name="client", + path="PATH/TO/BLOB", + connection="AzureWebJobsStorage") +def blob_trigger(client: blob.BlobClient): + logging.info(f"Python blob trigger function processed blob \n" + f"Properties: {client.get_blob_properties()}\n" + f"Blob content head: {client.download_blob(encoding="utf-8").read(size=1)}") + + +@app.route(route="file") +@app.blob_input(arg_name="client", + path="PATH/TO/BLOB", + connection="AzureWebJobsStorage") +def blob_input(req: func.HttpRequest, client: blob.BlobClient): + logging.info(f"Python blob input function processed blob \n" + f"Properties: {client.get_blob_properties()}\n" + f"Blob content head: {client.download_blob(encoding="utf-8").read(size=1)}") +``` + +## Troubleshooting +### General +The SDK-types raise exceptions defined in [Azure Core](https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/core/azure-core/README.md). + +This list can be used for reference to catch thrown exceptions. To get the specific error code of the exception, use the `error_code` attribute, i.e, `exception.error_code`. + +## Next steps + +### More sample code + +Get started with our [Blob samples](hhttps://github.com/Azure/azure-functions-python-extensions/tree/main/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: + * 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: + * 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: + * From BlobTrigger + * From BlobInput + +### Additional documentation +For more information on the Azure Storage Blob SDK, see the [Azure Blob storage documentation](https://docs.microsoft.com/azure/storage/blobs/) on docs.microsoft.com +and the [Azure Storage Blobs README](https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/storage/azure-storage-blob). + +## Contributing +This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit https://cla.microsoft.com. + +When you submit a pull request, a CLA-bot will automatically determine whether you need to provide a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions provided by the bot. You will only need to do this once across all repos using our CLA. + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. \ No newline at end of file diff --git a/azurefunctions-extensions-bindings-eventhub/azurefunctions/__init__.py b/azurefunctions-extensions-bindings-eventhub/azurefunctions/__init__.py new file mode 100644 index 0000000..8db66d3 --- /dev/null +++ b/azurefunctions-extensions-bindings-eventhub/azurefunctions/__init__.py @@ -0,0 +1 @@ +__path__ = __import__("pkgutil").extend_path(__path__, __name__) diff --git a/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/__init__.py b/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/__init__.py new file mode 100644 index 0000000..8db66d3 --- /dev/null +++ b/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/__init__.py @@ -0,0 +1 @@ +__path__ = __import__("pkgutil").extend_path(__path__, __name__) diff --git a/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/__init__.py b/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/__init__.py new file mode 100644 index 0000000..8db66d3 --- /dev/null +++ b/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/__init__.py @@ -0,0 +1 @@ +__path__ = __import__("pkgutil").extend_path(__path__, __name__) diff --git a/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/__init__.py b/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/__init__.py new file mode 100644 index 0000000..bcc445b --- /dev/null +++ b/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/__init__.py @@ -0,0 +1,12 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from .eventHubData import EventHubData +from .eventHubDataConverter import EventHubDataConverter + +__all__ = [ + "EventHubData", + "EventHubDataConverter", +] + +__version__ = "1.0.0b2" diff --git a/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventHubData.py b/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventHubData.py new file mode 100644 index 0000000..574d607 --- /dev/null +++ b/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventHubData.py @@ -0,0 +1,39 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import re +from typing import Union +import uamqp + +from azure.eventhub import EventData +from azurefunctions.extensions.base import Datum, SdkType + + +class EventHubData(SdkType): + def __init__(self, *, data: Union[bytes, Datum]) -> None: + # model_binding_data properties + self._data = data + self._version = None + self._source = None + self._content_type = None + self._content = None + self.decoded_message = None + if self._data: + self._version = data.version + self._source = data.source + self._content_type = data.content_type + self._content = data.content + self.decoded_message = self.__get_eventhub_content(self._content) + + def __get_eventhub_content(self, content): + if content: + return uamqp.Message().decode_from_bytes(content) + else: + return None + + def get_sdk_type(self): + # https://github.com/Azure/azure-sdk-for-python/issues/39711 + if self.decoded_message: + return EventData._from_message(self.decoded_message) + else: + return None diff --git a/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventHubDataConverter.py b/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventHubDataConverter.py new file mode 100644 index 0000000..bf90ffe --- /dev/null +++ b/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventHubDataConverter.py @@ -0,0 +1,42 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from typing import Any + +from azurefunctions.extensions.base import Datum, InConverter, OutConverter + +from .eventHubData import EventHubData + + +class EventHubDataConverter( + InConverter, + OutConverter, + binding="eventHub", + trigger="eventHubTrigger", +): + @classmethod + def check_input_type_annotation(cls, pytype: type) -> bool: + return issubclass( + pytype, (EventHubData) + ) + + @classmethod + def decode(cls, data: Datum, *, trigger_metadata, pytype) -> Any: + if data is None or data.type is None: + return None + + data_type = data.type + + if data_type == "model_binding_data": + data = data.value + else: + raise ValueError( + f'unexpected type of data received for the "eventhub" binding ' + f": {data_type!r}" + ) + + # Determines which sdk type to return based on pytype + if pytype == EventHubData: + return EventHubData(data=data).get_sdk_type() + else: + return None diff --git a/azurefunctions-extensions-bindings-eventhub/pyproject.toml b/azurefunctions-extensions-bindings-eventhub/pyproject.toml new file mode 100644 index 0000000..7b640ee --- /dev/null +++ b/azurefunctions-extensions-bindings-eventhub/pyproject.toml @@ -0,0 +1,49 @@ +[build-system] +requires = ["setuptools >= 61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "azurefunctions-extensions-bindings-eventhub" +dynamic = ["version"] +requires-python = ">=3.9" +authors = [{ name = "Azure Functions team at Microsoft Corp.", email = "azurefunctions@microsoft.com"}] +description = "EventHub Python worker extension for Azure Functions." +readme = "README.md" +license = {text = "MIT License"} +classifiers= [ + 'License :: OSI Approved :: MIT License', + 'Intended Audience :: Developers', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Operating System :: Microsoft :: Windows', + 'Operating System :: POSIX', + 'Operating System :: MacOS :: MacOS X', + 'Environment :: Web Environment', + 'Development Status :: 5 - Production/Stable', + ] +dependencies = [ + 'azurefunctions-extensions-base', + 'azure-eventhub~=5.13.0', + 'azure-identity~=1.19.0' + ] + +[project.optional-dependencies] +dev = [ + 'pytest', + 'pytest-cov', + 'coverage', + 'pytest-instafail', + 'pre-commit' + ] + +[tool.setuptools.dynamic] +version = {attr = "azurefunctions.extensions.bindings.eventhub.__version__"} + +[tool.setuptools.packages.find] +exclude = [ + 'azurefunctions.extensions.bindings','azurefunctions.extensions', + 'azurefunctions', 'tests', 'samples' + ] + diff --git a/azurefunctions-extensions-bindings-eventhub/samples/README.md b/azurefunctions-extensions-bindings-eventhub/samples/README.md new file mode 100644 index 0000000..482aad5 --- /dev/null +++ b/azurefunctions-extensions-bindings-eventhub/samples/README.md @@ -0,0 +1,58 @@ +--- +page_type: sample +languages: + - python +products: + - azure + - azure-functions + - azure-functions-extensions + - azurefunctions-extensions-bindings-eventhub +urlFragment: extension-eventhub-samples +--- + +# Azure Functions Extension EventHub library for Python samples + +These are code samples that show common scenario operations with the Azure Functions Extension EventHub library. + +These samples relate to the Azure EventHub library being used as part of a Python Function App. For +examples on how to use the Azure EventHub library, please see [Azure EventHub samples](https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/eventhub/azure-eventhub/samples) + +* [blob_samples_eventhubdata](https://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventhubdata) - Examples for using the EventData type: + * From EventHubTrigger + +## Prerequisites +* Python 3.9 or later is required to use this package. For more details, please read our page on [Python Functions version support policy](https://learn.microsoft.com/en-us/azure/azure-functions/functions-versions?tabs=isolated-process%2Cv4&pivots=programming-language-python#languages). +* You must have an [Azure subscription](https://azure.microsoft.com/free/) and an +[Azure storage account](https://docs.microsoft.com/azure/storage/common/storage-account-overview) to use this package. + +## Setup + +1. Install [Core Tools](https://learn.microsoft.com/en-us/azure/azure-functions/functions-run-local?tabs=windows%2Cisolated-process%2Cnode-v4%2Cpython-v2%2Chttp-trigger%2Ccontainer-apps&pivots=programming-language-python) +2. Install the Azure Functions Extension EventHub library for Python with [pip](https://pypi.org/project/pip/): + +```bash +pip install azurefunctions-extensions-bindings-eventhub +``` + +3. Clone or download this sample repository +4. Open the sample folder in Visual Studio Code or your IDE of choice. + +## Running the samples + +1. Open a terminal window and `cd` to the directory that the sample you wish to run is saved in. +2. Set the environment variables specified in the sample file you wish to run. +3. Install the required dependencies +```bash +pip install -r requirements.txt +``` +4. Start the Functions runtime +```bash +func start +``` +5. Execute the function by uploading an event to the EventHub that is being targeted. + +## 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 +[API reference documentation](https://aka.ms/azsdk-python-storage-blob-ref) to learn more about +what you can do with the Azure EventHub library. \ No newline at end of file diff --git a/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventhubdata/function_app.py b/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventhubdata/function_app.py new file mode 100644 index 0000000..b21ce93 --- /dev/null +++ b/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventhubdata/function_app.py @@ -0,0 +1,41 @@ +# coding: utf-8 + +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- + +import logging + +import azure.functions as func +import azurefunctions.extensions.bindings.eventhub as eh + +app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) + +""" +FOLDER: eventhub_samples_eventhubdata +DESCRIPTION: + These samples demonstrate how to obtain EventHubData from an EventHub Trigger. +USAGE: + There are different ways to connect to an EventHub via the connection property and + envionrment variables specifiied in local.settings.json + + The connection property can be: + - The name of an application setting containing a connection string + - The name of a shared prefix for multiple application settings, together defining an identity-based connection + + For more information, see: + https://learn.microsoft.com/en-us/azure/azure-functions/functions-bindings-event-hubs-trigger?tabs=python-v2%2Cisolated-process%2Cnodejs-v4%2Cfunctionsv2%2Cextensionv5&pivots=programming-language-python +""" + + +@app.event_hub_message_trigger( + arg_name="eh_data", event_hub_name="EVENTHUB_NAME", connection="AzureWebJobsStorage" +) +def eventhub_trigger(eh_data: eh.EventHubData): + logging.info( + "Python EventHub trigger processed an event %s", + eh_data.body_as_str() + ) + \ No newline at end of file diff --git a/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventhubdata/host.json b/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventhubdata/host.json new file mode 100644 index 0000000..9df9136 --- /dev/null +++ b/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventhubdata/host.json @@ -0,0 +1,15 @@ +{ + "version": "2.0", + "logging": { + "applicationInsights": { + "samplingSettings": { + "isEnabled": true, + "excludedTypes": "Request" + } + } + }, + "extensionBundle": { + "id": "Microsoft.Azure.Functions.ExtensionBundle", + "version": "[4.*, 5.0.0)" + } +} \ No newline at end of file diff --git a/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventhubdata/local.settings.json b/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventhubdata/local.settings.json new file mode 100644 index 0000000..c3c2a89 --- /dev/null +++ b/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventhubdata/local.settings.json @@ -0,0 +1,8 @@ +{ + "IsEncrypted": false, + "Values": { + "FUNCTIONS_WORKER_RUNTIME": "python", + "AzureWebJobsStorage": "", + "AzureWebJobsFeatureFlags": "EnableWorkerIndexing" + } +} \ No newline at end of file diff --git a/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventhubdata/requirements.txt b/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventhubdata/requirements.txt new file mode 100644 index 0000000..6b7c0aa --- /dev/null +++ b/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventhubdata/requirements.txt @@ -0,0 +1,6 @@ +# DO NOT include azure-functions-worker in this file +# The Python Worker is managed by Azure Functions platform +# Manually managing azure-functions-worker may cause unexpected issues + +azure-functions +azurefunctions-extensions-bindings-eventhub \ No newline at end of file diff --git a/azurefunctions-extensions-bindings-eventhub/tests/__init__.py b/azurefunctions-extensions-bindings-eventhub/tests/__init__.py new file mode 100644 index 0000000..528a01b --- /dev/null +++ b/azurefunctions-extensions-bindings-eventhub/tests/__init__.py @@ -0,0 +1,20 @@ +# 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(): + test_loader = unittest.TestLoader() + return test_loader.discover(os.path.dirname(__file__), pattern="test_*.py") + + +if __name__ == "__main__": + runner = unittest.runner.TextTestRunner() + result = runner.run(suite()) + sys.exit(not result.wasSuccessful()) diff --git a/azurefunctions-extensions-bindings-eventhub/tests/test_eventhubdata.py b/azurefunctions-extensions-bindings-eventhub/tests/test_eventhubdata.py new file mode 100644 index 0000000..85fcfe6 --- /dev/null +++ b/azurefunctions-extensions-bindings-eventhub/tests/test_eventhubdata.py @@ -0,0 +1,174 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import json +import unittest +from enum import Enum +from typing import Optional + +from azure.eventhub import EventData +from azurefunctions.extensions.base import Datum + +from azurefunctions.extensions.bindings.blob import BlobClient, BlobClientConverter +from azurefunctions.extensions.bindings.eventhub import EventHubData, EventHubDataConverter + + +# Mock classes for testing +class MockMBD: + def __init__(self, version: str, source: str, content_type: str, content: str): + self.version = version + self.source = source + self.content_type = content_type + self.content = content + + +class MockBindingDirection(Enum): + IN = 0 + OUT = 1 + INOUT = 2 + + +class MockBinding: + def __init__( + self, + name: str, + direction: MockBindingDirection, + data_type=None, + type: Optional[str] = None, + ): # NoQa + self.type = type + self.name = name + self._direction = direction + self._data_type = data_type + self._dict = { + "direction": self._direction, + "dataType": self._data_type, + "type": self.type, + } + + @property + def data_type(self) -> Optional[int]: + return self._data_type.value if self._data_type else None + + @property + def direction(self) -> int: + return self._direction.value + + +class MockParamTypeInfo: + def __init__(self, binding_name: str, pytype: type): + self.binding_name = binding_name + self.pytype = pytype + + +class MockFunction: + def __init__(self, bindings: MockBinding): + self._bindings = bindings + + +class TestBlobClient(unittest.TestCase): + def test_input_type(self): + check_input_type = EventHubDataConverter.check_input_type_annotation + self.assertTrue(check_input_type(EventHubData)) + self.assertFalse(check_input_type(str)) + self.assertFalse(check_input_type(bytes)) + self.assertFalse(check_input_type(bytearray)) + + def test_input_none(self): + result = EventHubDataConverter.decode( + data=None, trigger_metadata=None, pytype=EventHubData + ) + self.assertIsNone(result) + + datum: Datum = Datum(value=b"string_content", type=None) + result = EventHubDataConverter.decode( + data=datum, trigger_metadata=None, pytype=BlobClient + ) + self.assertIsNone(result) + + def test_input_incorrect_type(self): + datum: Datum = Datum(value=b"string_content", type="bytearray") + with self.assertRaises(ValueError): + EventHubDataConverter.decode( + data=datum, trigger_metadata=None, pytype=EventHubData + ) + + def test_input_empty(self): + datum: Datum = Datum(value={}, type="model_binding_data") + result: EventHubData = EventHubDataConverter.decode( + data=datum, trigger_metadata=None, pytype=EventHubData + ) + self.assertIsNone(result) + + def test_input_populated(self): + sample_mbd = MockMBD( + version="1.0", + source="AzureStorageBlobs", + content_type="application/json", + 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' + ) + + datum: Datum = Datum(value=sample_mbd, type="model_binding_data") + result: EventHubData = EventHubDataConverter.decode( + data=datum, trigger_metadata=None, pytype=EventHubData + ) + + self.assertIsNotNone(result) + self.assertIsInstance(result, EventData) + + sdk_result = EventHubData(data=datum.value).get_sdk_type() + + self.assertIsNotNone(sdk_result) + self.assertIsInstance(sdk_result, EventData) + + def test_invalid_input_populated(self): + sample_mbd = MockMBD( + version="1.0", + source="AzureStorageBlobs", + content_type="application/json", + 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' + ) + + with self.assertRaises(ValueError) as e: + datum: Datum = Datum(value=sample_mbd, type="model_binding_data") + result: EventHubData = EventHubDataConverter.decode( + data=datum, trigger_metadata=None, pytype=EventHubData + ) + self.assertEqual( + e.exception.args[0], + "Storage account connection string NotARealConnectionString does not exist. " + "Please make sure that it is a defined App Setting.", + ) + + def test_none_input_populated(self): + sample_mbd = MockMBD( + version="1.0", + source="AzureStorageBlobs", + content_type="application/json", + 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' + ) + + with self.assertRaises(ValueError) as e: + datum: Datum = Datum(value=sample_mbd, type="model_binding_data") + result: EventHubData = EventHubDataConverter.decode( + data=datum, trigger_metadata=None, pytype=EventHubData + ) + self.assertEqual( + e.exception.args[0], + "Storage account connection string cannot be None. Please provide a connection string.", + ) + + def test_input_invalid_pytype(self): + sample_mbd = MockMBD( + version="1.0", + source="AzureStorageBlobs", + content_type="application/json", + 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' + ) + + datum: Datum = Datum(value=sample_mbd, type="model_binding_data") + result: EventHubData = EventHubDataConverter.decode( + data=datum, trigger_metadata=None, pytype="str" + ) + + self.assertIsNone(result) diff --git a/function_app.py b/function_app.py new file mode 100644 index 0000000..01d18b7 --- /dev/null +++ b/function_app.py @@ -0,0 +1,27 @@ +import azure.functions as func +import datetime +import json +import logging + +app = func.FunctionApp() + +@app.route(route="http_trigger", auth_level=func.AuthLevel.ANONYMOUS) +def http_trigger(req: func.HttpRequest) -> func.HttpResponse: + logging.info('Python HTTP trigger function processed a request.') + + name = req.params.get('name') + if not name: + try: + req_body = req.get_json() + except ValueError: + pass + else: + name = req_body.get('name') + + if name: + return func.HttpResponse(f"Hello, {name}. This HTTP triggered function executed successfully.") + else: + return func.HttpResponse( + "This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response.", + status_code=200 + ) \ No newline at end of file diff --git a/host.json b/host.json new file mode 100644 index 0000000..d5f63c0 --- /dev/null +++ b/host.json @@ -0,0 +1,15 @@ +{ + "version": "2.0", + "logging": { + "applicationInsights": { + "samplingSettings": { + "isEnabled": true, + "excludedTypes": "Request" + } + } + }, + "extensionBundle": { + "id": "Microsoft.Azure.Functions.ExtensionBundle", + "version": "[4.*, 5.0.0)" + } +} \ No newline at end of file diff --git a/local.settings.json b/local.settings.json new file mode 100644 index 0000000..2e35cf0 --- /dev/null +++ b/local.settings.json @@ -0,0 +1,7 @@ +{ + "IsEncrypted": false, + "Values": { + "FUNCTIONS_WORKER_RUNTIME": "python", + "AzureWebJobsStorage": "UseDevelopmentStorage=true" + } +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..88745f9 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +# Do not include azure-functions-worker in this file +# The Python Worker is managed by the Azure Functions platform +# Manually managing azure-functions-worker may cause unexpected issues + +azure-functions \ No newline at end of file From 6ad56e0537de6d2b146f8a7be10f4741bbd966a3 Mon Sep 17 00:00:00 2001 From: Evan Roman Date: Fri, 28 Feb 2025 15:53:50 -0600 Subject: [PATCH 02/26] Rm leftovers --- function_app.py | 27 --------------------------- host.json | 15 --------------- local.settings.json | 7 ------- requirements.txt | 5 ----- 4 files changed, 54 deletions(-) delete mode 100644 function_app.py delete mode 100644 host.json delete mode 100644 local.settings.json delete mode 100644 requirements.txt diff --git a/function_app.py b/function_app.py deleted file mode 100644 index 01d18b7..0000000 --- a/function_app.py +++ /dev/null @@ -1,27 +0,0 @@ -import azure.functions as func -import datetime -import json -import logging - -app = func.FunctionApp() - -@app.route(route="http_trigger", auth_level=func.AuthLevel.ANONYMOUS) -def http_trigger(req: func.HttpRequest) -> func.HttpResponse: - logging.info('Python HTTP trigger function processed a request.') - - name = req.params.get('name') - if not name: - try: - req_body = req.get_json() - except ValueError: - pass - else: - name = req_body.get('name') - - if name: - return func.HttpResponse(f"Hello, {name}. This HTTP triggered function executed successfully.") - else: - return func.HttpResponse( - "This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response.", - status_code=200 - ) \ No newline at end of file diff --git a/host.json b/host.json deleted file mode 100644 index d5f63c0..0000000 --- a/host.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "version": "2.0", - "logging": { - "applicationInsights": { - "samplingSettings": { - "isEnabled": true, - "excludedTypes": "Request" - } - } - }, - "extensionBundle": { - "id": "Microsoft.Azure.Functions.ExtensionBundle", - "version": "[4.*, 5.0.0)" - } -} \ No newline at end of file diff --git a/local.settings.json b/local.settings.json deleted file mode 100644 index 2e35cf0..0000000 --- a/local.settings.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "IsEncrypted": false, - "Values": { - "FUNCTIONS_WORKER_RUNTIME": "python", - "AzureWebJobsStorage": "UseDevelopmentStorage=true" - } -} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 88745f9..0000000 --- a/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -# Do not include azure-functions-worker in this file -# The Python Worker is managed by the Azure Functions platform -# Manually managing azure-functions-worker may cause unexpected issues - -azure-functions \ No newline at end of file From 7ebe2d2a25e994b92ad460bf8cf9ab8d62ae20ae Mon Sep 17 00:00:00 2001 From: Evan Roman Date: Mon, 3 Mar 2025 12:35:56 -0600 Subject: [PATCH 03/26] Fix docs --- .../README.md | 67 ++++++------------- .../function_app.py | 1 - .../tests/test_eventhubdata.py | 50 ++------------ 3 files changed, 28 insertions(+), 90 deletions(-) diff --git a/azurefunctions-extensions-bindings-eventhub/README.md b/azurefunctions-extensions-bindings-eventhub/README.md index 20897a5..34bf42c 100644 --- a/azurefunctions-extensions-bindings-eventhub/README.md +++ b/azurefunctions-extensions-bindings-eventhub/README.md @@ -1,17 +1,14 @@ # Azure Functions Extensions Bindings EventHub library for Python -This library allows Blob Trigger and Blob Input bindings in Python Function Apps to recognize and bind to client types from the -Azure Storage Blob sdk. +This library allows an EventHub Trigger binding in Python Function Apps to recognize and bind to the types from the +Azure EventHub sdk (EventData). 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) -[Package (PyPi)](https://pypi.org/project/azurefunctions-extensions-bindings-blob/) +[Source code](https://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-bindings-eventhub) +[Package (PyPi)](https://pypi.org/project/azurefunctions-extensions-bindings-eventhub/) | API reference documentation | Product documentation -| [Samples](hhttps://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-bindings-blob/samples) +| [Samples](hhttps://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-bindings-eventhub/samples) ## Getting started @@ -23,10 +20,10 @@ Blob client types can be generated from: [Azure storage account](https://docs.microsoft.com/azure/storage/common/storage-account-overview) to use this package. ### Install the package -Install the Azure Functions Extensions Bindings Blob library for Python with pip: +Install the Azure Functions Extensions Bindings EventHub library for Python with pip: ```bash -pip install azurefunctions-extensions-bindings-blob +pip install azurefunctions-extensions-bindings-eventhub ``` ### Create a storage account @@ -45,34 +42,23 @@ az storage account create -n my-storage-account-name -g my-resource-group ``` ### Bind to the SDK-type -The Azure Functions Extensions Bindings Blob library for Python allows you to create a function app with a Blob Trigger or -Blob Input and define the type as a BlobClient, ContainerClient, or StorageStreamDownloader. Instead of receiving -an InputStream, when the function is executed, the type returned will be the defined SDK-type and have all of the -properties and methods available as seen in the Azure Storage Blob library for Python. +The Azure Functions Extensions Bindings EventHub library for Python allows you to create a function app with an EventHub Trigger +and define the type as an EventData. Instead of receiving an InputStream, when the function is executed, the type returned will be the defined SDK-type and have all of the properties and methods available as seen in the Azure Storage Blob library for Python. ```python import logging import azure.functions as func -import azurefunctions.extensions.bindings.blob as blob - -@app.blob_trigger(arg_name="client", - path="PATH/TO/BLOB", - connection="AzureWebJobsStorage") -def blob_trigger(client: blob.BlobClient): - logging.info(f"Python blob trigger function processed blob \n" - f"Properties: {client.get_blob_properties()}\n" - f"Blob content head: {client.download_blob(encoding="utf-8").read(size=1)}") - - -@app.route(route="file") -@app.blob_input(arg_name="client", - path="PATH/TO/BLOB", - connection="AzureWebJobsStorage") -def blob_input(req: func.HttpRequest, client: blob.BlobClient): - logging.info(f"Python blob input function processed blob \n" - f"Properties: {client.get_blob_properties()}\n" - f"Blob content head: {client.download_blob(encoding="utf-8").read(size=1)}") +import azurefunctions.extensions.bindings.eventhub as eh + +@app.event_hub_message_trigger( + arg_name="eh_data", event_hub_name="EVENTHUB_NAME", connection="AzureWebJobsStorage" +) +def eventhub_trigger(eh_data: eh.EventHubData): + logging.info( + "Python EventHub trigger processed an event %s", + eh_data.body_as_str() + ) ``` ## Troubleshooting @@ -85,21 +71,12 @@ 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](hhttps://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-bindings-eventhub/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: - * 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: - * 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: - * From BlobTrigger - * From BlobInput +* [blob_samples_eventhubdata](https://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventhubdata) - Examples for using the EventHubData type: + * From EventHubTrigger ### Additional documentation For more information on the Azure Storage Blob SDK, see the [Azure Blob storage documentation](https://docs.microsoft.com/azure/storage/blobs/) on docs.microsoft.com diff --git a/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventhubdata/function_app.py b/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventhubdata/function_app.py index b21ce93..f17515f 100644 --- a/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventhubdata/function_app.py +++ b/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventhubdata/function_app.py @@ -7,7 +7,6 @@ # -------------------------------------------------------------------------- import logging - import azure.functions as func import azurefunctions.extensions.bindings.eventhub as eh diff --git a/azurefunctions-extensions-bindings-eventhub/tests/test_eventhubdata.py b/azurefunctions-extensions-bindings-eventhub/tests/test_eventhubdata.py index 85fcfe6..948c506 100644 --- a/azurefunctions-extensions-bindings-eventhub/tests/test_eventhubdata.py +++ b/azurefunctions-extensions-bindings-eventhub/tests/test_eventhubdata.py @@ -1,17 +1,15 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -import json import unittest -from enum import Enum from typing import Optional from azure.eventhub import EventData from azurefunctions.extensions.base import Datum -from azurefunctions.extensions.bindings.blob import BlobClient, BlobClientConverter from azurefunctions.extensions.bindings.eventhub import EventHubData, EventHubDataConverter +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" # Mock classes for testing class MockMBD: @@ -21,31 +19,6 @@ def __init__(self, version: str, source: str, content_type: str, content: str): self.content_type = content_type self.content = content - -class MockBindingDirection(Enum): - IN = 0 - OUT = 1 - INOUT = 2 - - -class MockBinding: - def __init__( - self, - name: str, - direction: MockBindingDirection, - data_type=None, - type: Optional[str] = None, - ): # NoQa - self.type = type - self.name = name - self._direction = direction - self._data_type = data_type - self._dict = { - "direction": self._direction, - "dataType": self._data_type, - "type": self.type, - } - @property def data_type(self) -> Optional[int]: return self._data_type.value if self._data_type else None @@ -55,17 +28,6 @@ def direction(self) -> int: return self._direction.value -class MockParamTypeInfo: - def __init__(self, binding_name: str, pytype: type): - self.binding_name = binding_name - self.pytype = pytype - - -class MockFunction: - def __init__(self, bindings: MockBinding): - self._bindings = bindings - - class TestBlobClient(unittest.TestCase): def test_input_type(self): check_input_type = EventHubDataConverter.check_input_type_annotation @@ -82,7 +44,7 @@ def test_input_none(self): datum: Datum = Datum(value=b"string_content", type=None) result = EventHubDataConverter.decode( - data=datum, trigger_metadata=None, pytype=BlobClient + data=datum, trigger_metadata=None, pytype=EventHubData ) self.assertIsNone(result) @@ -105,7 +67,7 @@ def test_input_populated(self): version="1.0", source="AzureStorageBlobs", content_type="application/json", - 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' + content = EVENTHUB_SAMPLE_CONTENT ) datum: Datum = Datum(value=sample_mbd, type="model_binding_data") @@ -126,7 +88,7 @@ def test_invalid_input_populated(self): version="1.0", source="AzureStorageBlobs", content_type="application/json", - 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' + content = EVENTHUB_SAMPLE_CONTENT ) with self.assertRaises(ValueError) as e: @@ -145,7 +107,7 @@ def test_none_input_populated(self): version="1.0", source="AzureStorageBlobs", content_type="application/json", - 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' + content = EVENTHUB_SAMPLE_CONTENT ) with self.assertRaises(ValueError) as e: @@ -163,7 +125,7 @@ def test_input_invalid_pytype(self): version="1.0", source="AzureStorageBlobs", content_type="application/json", - 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' + content = EVENTHUB_SAMPLE_CONTENT ) datum: Datum = Datum(value=sample_mbd, type="model_binding_data") From 90d8faef453d2b2e155b3b967b446828190a2329 Mon Sep 17 00:00:00 2001 From: Evan Roman Date: Mon, 3 Mar 2025 13:03:14 -0600 Subject: [PATCH 04/26] Fix docs --- .../README.md | 14 +++++++------- .../samples/README.md | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/azurefunctions-extensions-bindings-eventhub/README.md b/azurefunctions-extensions-bindings-eventhub/README.md index 34bf42c..6be6e05 100644 --- a/azurefunctions-extensions-bindings-eventhub/README.md +++ b/azurefunctions-extensions-bindings-eventhub/README.md @@ -2,7 +2,7 @@ This library allows an EventHub Trigger binding in Python Function Apps to recognize and bind to the types from the Azure EventHub sdk (EventData). -Blob client types can be generated from: +EventHub types can be generated from: [Source code](https://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-bindings-eventhub) [Package (PyPi)](https://pypi.org/project/azurefunctions-extensions-bindings-eventhub/) @@ -43,7 +43,7 @@ az storage account create -n my-storage-account-name -g my-resource-group ### Bind to the SDK-type The Azure Functions Extensions Bindings EventHub library for Python allows you to create a function app with an EventHub Trigger -and define the type as an EventData. Instead of receiving an InputStream, when the function is executed, the type returned will be the defined SDK-type and have all of the properties and methods available as seen in the Azure Storage Blob library for Python. +and define the type as an EventData. Instead of receiving an InputStream, when the function is executed, the type returned will be the defined SDK-type and have all of the properties and methods available as seen in the Azure EventHub library for Python. ```python @@ -71,16 +71,16 @@ 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-eventhub/samples). +Get started with our [EventHub samples](hhttps://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-bindings-eventhub/samples). -Several samples are available in this GitHub repository. These samples provide example code for additional scenarios commonly encountered while working with Storage Blobs: +Several samples are available in this GitHub repository. These samples provide example code for additional scenarios commonly encountered while working with EventHubs: -* [blob_samples_eventhubdata](https://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventhubdata) - Examples for using the EventHubData type: +* [eventhub_samples_eventhubdata](https://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventhubdata) - Examples for using the EventHubData type: * From EventHubTrigger ### Additional documentation -For more information on the Azure Storage Blob SDK, see the [Azure Blob storage documentation](https://docs.microsoft.com/azure/storage/blobs/) on docs.microsoft.com -and the [Azure Storage Blobs README](https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/storage/azure-storage-blob). +For more information on the Azure EventHub SDK, see the [Azure EventHub documentation](https://learn.microsoft.com/en-us/azure/event-hubs/) on learn.microsoft.com +and the [Azure EventHub README](https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/eventhub/azure-eventhub/README.md). ## Contributing This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit https://cla.microsoft.com. diff --git a/azurefunctions-extensions-bindings-eventhub/samples/README.md b/azurefunctions-extensions-bindings-eventhub/samples/README.md index 482aad5..e40a80d 100644 --- a/azurefunctions-extensions-bindings-eventhub/samples/README.md +++ b/azurefunctions-extensions-bindings-eventhub/samples/README.md @@ -17,7 +17,7 @@ These are code samples that show common scenario operations with the Azure Funct These samples relate to the Azure EventHub library being used as part of a Python Function App. For examples on how to use the Azure EventHub library, please see [Azure EventHub samples](https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/eventhub/azure-eventhub/samples) -* [blob_samples_eventhubdata](https://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventhubdata) - Examples for using the EventData type: +* [eventhub_samples_eventhubdata](https://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventhubdata) - Examples for using the EventData type: * From EventHubTrigger ## Prerequisites @@ -54,5 +54,5 @@ func start ## 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 -[API reference documentation](https://aka.ms/azsdk-python-storage-blob-ref) to learn more about +[API reference documentation](https://learn.microsoft.com/en-us/python/api/azure-eventhub/azure.eventhub?view=azure-python) to learn more about what you can do with the Azure EventHub library. \ No newline at end of file From 205c16aee7d50e45d7fcae373210a93dab5c0c3b Mon Sep 17 00:00:00 2001 From: Evan Roman Date: Mon, 3 Mar 2025 13:18:48 -0600 Subject: [PATCH 05/26] Add to CI --- eng/ci/ci-eventhub-tests.yml | 35 +++++++++++++++++++ .../official/jobs/blob-unit-tests.yml | 2 +- .../official/jobs/eventhub-unit-tests.yml | 31 ++++++++++++++++ .../official/jobs/fastapi-unit-tests.yml | 2 +- 4 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 eng/ci/ci-eventhub-tests.yml create mode 100644 eng/templates/official/jobs/eventhub-unit-tests.yml diff --git a/eng/ci/ci-eventhub-tests.yml b/eng/ci/ci-eventhub-tests.yml new file mode 100644 index 0000000..e7dff60 --- /dev/null +++ b/eng/ci/ci-eventhub-tests.yml @@ -0,0 +1,35 @@ +trigger: none # ensure this is not ran as a CI build + +pr: + branches: + include: + - dev + - release/* + +resources: + repositories: + - repository: 1es + type: git + name: 1ESPipelineTemplates/1ESPipelineTemplates + ref: refs/tags/release + - repository: eng + type: git + name: engineering + ref: refs/tags/release + +variables: + - template: /ci/variables/build.yml@eng + - template: /ci/variables/cfs.yml@eng + +extends: + template: v1/1ES.Unofficial.PipelineTemplate.yml@1es + parameters: + pool: + name: 1es-pool-azfunc + image: 1es-windows-2022 + os: windows + + stages: + - stage: RunEventHubUnitTests + jobs: + - template: /eng/templates/official/jobs/eventhub-unit-tests.yml@self diff --git a/eng/templates/official/jobs/blob-unit-tests.yml b/eng/templates/official/jobs/blob-unit-tests.yml index b8499c7..91f8da4 100644 --- a/eng/templates/official/jobs/blob-unit-tests.yml +++ b/eng/templates/official/jobs/blob-unit-tests.yml @@ -23,7 +23,7 @@ jobs: python -m pip install -U -e .[dev] displayName: 'Install dependencies' - bash: | - python -m pytest -q --instafail azurefunctions-extensions-bindings-blob/tests/ --ignore='azurefunctions-extensions-base', --ignore='azurefunctions-extensions-http-fastapi' + python -m pytest -q --instafail azurefunctions-extensions-bindings-blob/tests/ --ignore='azurefunctions-extensions-base', --ignore='azurefunctions-extensions-http-fastapi', --ignore='azurefunctions-extensions-bindings-eventhub' env: AzureWebJobsStorage: $(AzureWebJobsStorage) input: $(input__serviceUri) diff --git a/eng/templates/official/jobs/eventhub-unit-tests.yml b/eng/templates/official/jobs/eventhub-unit-tests.yml new file mode 100644 index 0000000..f017180 --- /dev/null +++ b/eng/templates/official/jobs/eventhub-unit-tests.yml @@ -0,0 +1,31 @@ +jobs: + - job: "TestPython" + displayName: "Run EventHub Tests" + + strategy: + matrix: + python39: + PYTHON_VERSION: '3.9' + python310: + PYTHON_VERSION: '3.10' + python311: + PYTHON_VERSION: '3.11' + python312: + PYTHON_VERSION: '3.12' + + steps: + - task: UsePythonVersion@0 + inputs: + versionSpec: $(PYTHON_VERSION) + - bash: | + python -m pip install --upgrade pip + cd azurefunctions-extensions-bindings-eventhub + python -m pip install -U -e .[dev] + displayName: 'Install dependencies' + - bash: | + python -m pytest -q --instafail azurefunctions-extensions-bindings-eventhub/tests/ --ignore='azurefunctions-extensions-base', --ignore='azurefunctions-extensions-http-fastapi', --ignore='azurefunctions-extensions-bindings-blob' + env: + AzureWebJobsStorage: $(AzureWebJobsStorage) + input: $(input__serviceUri) + trigger: $(trigger__blobServiceUri) + displayName: "Running EventHub $(PYTHON_VERSION) Python Extension Tests" diff --git a/eng/templates/official/jobs/fastapi-unit-tests.yml b/eng/templates/official/jobs/fastapi-unit-tests.yml index 59f0678..d205d86 100644 --- a/eng/templates/official/jobs/fastapi-unit-tests.yml +++ b/eng/templates/official/jobs/fastapi-unit-tests.yml @@ -25,5 +25,5 @@ jobs: python -m pip install -U -e .[dev] displayName: 'Install dependencies' - bash: | - python -m pytest -q --instafail azurefunctions-extensions-http-fastapi/tests/ --ignore='azurefunctions-extensions-base', --ignore='azurefunctions-extensions-bindings-blob' + python -m pytest -q --instafail azurefunctions-extensions-http-fastapi/tests/ --ignore='azurefunctions-extensions-base', --ignore='azurefunctions-extensions-bindings-blob', --ignore='azurefunctions-extensions-bindings-eventhub' displayName: "Running FastApi $(PYTHON_VERSION) Python Extension Tests" \ No newline at end of file From 48af64c050b9c02b600b768f3f6a46f7214153fd Mon Sep 17 00:00:00 2001 From: Evan Roman Date: Mon, 3 Mar 2025 15:11:44 -0600 Subject: [PATCH 06/26] Address --- .../README.md | 12 ++--- .../samples/README.md | 8 ++-- .../README.md | 10 ++-- .../extensions/bindings/eventhub/__init__.py | 2 +- .../bindings/eventhub/eventHubData.py | 1 - .../pyproject.toml | 3 +- .../samples/README.md | 4 +- .../tests/test_eventhubdata.py | 47 ++----------------- .../README.md | 8 ++-- .../official/jobs/base-unit-tests.yml | 2 +- .../official/jobs/eventhub-unit-tests.yml | 2 - 11 files changed, 29 insertions(+), 70 deletions(-) diff --git a/azurefunctions-extensions-bindings-blob/README.md b/azurefunctions-extensions-bindings-blob/README.md index 659f440..02a2662 100644 --- a/azurefunctions-extensions-bindings-blob/README.md +++ b/azurefunctions-extensions-bindings-blob/README.md @@ -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 @@ -85,19 +85,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 diff --git a/azurefunctions-extensions-bindings-blob/samples/README.md b/azurefunctions-extensions-bindings-blob/samples/README.md index 8241bb9..96e0743 100644 --- a/azurefunctions-extensions-bindings-blob/samples/README.md +++ b/azurefunctions-extensions-bindings-blob/samples/README.md @@ -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 @@ -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. \ No newline at end of file diff --git a/azurefunctions-extensions-bindings-eventhub/README.md b/azurefunctions-extensions-bindings-eventhub/README.md index 6be6e05..6a94c1d 100644 --- a/azurefunctions-extensions-bindings-eventhub/README.md +++ b/azurefunctions-extensions-bindings-eventhub/README.md @@ -4,11 +4,11 @@ Azure EventHub sdk (EventData). EventHub types can be generated from: -[Source code](https://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-bindings-eventhub) +[Source code](https://github.com/Azure/azure-functions-python-extensions/tree/dev/azurefunctions-extensions-bindings-eventhub) [Package (PyPi)](https://pypi.org/project/azurefunctions-extensions-bindings-eventhub/) | API reference documentation | Product documentation -| [Samples](hhttps://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-bindings-eventhub/samples) +| [Samples](https://github.com/Azure/azure-functions-python-extensions/tree/dev/azurefunctions-extensions-bindings-eventhub/samples) ## Getting started @@ -43,7 +43,7 @@ az storage account create -n my-storage-account-name -g my-resource-group ### Bind to the SDK-type The Azure Functions Extensions Bindings EventHub library for Python allows you to create a function app with an EventHub Trigger -and define the type as an EventData. Instead of receiving an InputStream, when the function is executed, the type returned will be the defined SDK-type and have all of the properties and methods available as seen in the Azure EventHub library for Python. +and define the type as an EventData. Instead of receiving an EventHubEvent, when the function is executed, the type returned will be the defined SDK-type and have all of the properties and methods available as seen in the Azure EventHub library for Python. ```python @@ -71,11 +71,11 @@ This list can be used for reference to catch thrown exceptions. To get the speci ### More sample code -Get started with our [EventHub samples](hhttps://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-bindings-eventhub/samples). +Get started with our [EventHub samples](https://github.com/Azure/azure-functions-python-extensions/tree/dev/azurefunctions-extensions-bindings-eventhub/samples). Several samples are available in this GitHub repository. These samples provide example code for additional scenarios commonly encountered while working with EventHubs: -* [eventhub_samples_eventhubdata](https://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventhubdata) - Examples for using the EventHubData type: +* [eventhub_samples_eventhubdata](https://github.com/Azure/azure-functions-python-extensions/tree/dev/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventhubdata) - Examples for using the EventHubData type: * From EventHubTrigger ### Additional documentation diff --git a/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/__init__.py b/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/__init__.py index bcc445b..340a9ea 100644 --- a/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/__init__.py +++ b/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/__init__.py @@ -9,4 +9,4 @@ "EventHubDataConverter", ] -__version__ = "1.0.0b2" +__version__ = "1.0.0b1" diff --git a/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventHubData.py b/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventHubData.py index 574d607..78a7e15 100644 --- a/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventHubData.py +++ b/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventHubData.py @@ -1,7 +1,6 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -import re from typing import Union import uamqp diff --git a/azurefunctions-extensions-bindings-eventhub/pyproject.toml b/azurefunctions-extensions-bindings-eventhub/pyproject.toml index 7b640ee..957ec7b 100644 --- a/azurefunctions-extensions-bindings-eventhub/pyproject.toml +++ b/azurefunctions-extensions-bindings-eventhub/pyproject.toml @@ -25,8 +25,7 @@ classifiers= [ ] dependencies = [ 'azurefunctions-extensions-base', - 'azure-eventhub~=5.13.0', - 'azure-identity~=1.19.0' + 'azure-eventhub~=5.13.0' ] [project.optional-dependencies] diff --git a/azurefunctions-extensions-bindings-eventhub/samples/README.md b/azurefunctions-extensions-bindings-eventhub/samples/README.md index e40a80d..75f2911 100644 --- a/azurefunctions-extensions-bindings-eventhub/samples/README.md +++ b/azurefunctions-extensions-bindings-eventhub/samples/README.md @@ -17,7 +17,7 @@ These are code samples that show common scenario operations with the Azure Funct These samples relate to the Azure EventHub library being used as part of a Python Function App. For examples on how to use the Azure EventHub library, please see [Azure EventHub samples](https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/eventhub/azure-eventhub/samples) -* [eventhub_samples_eventhubdata](https://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventhubdata) - Examples for using the EventData type: +* [eventhub_samples_eventhubdata](https://github.com/Azure/azure-functions-python-extensions/tree/dev/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventhubdata) - Examples for using the EventData type: * From EventHubTrigger ## Prerequisites @@ -53,6 +53,6 @@ func start ## 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://learn.microsoft.com/en-us/python/api/azure-eventhub/azure.eventhub?view=azure-python) to learn more about what you can do with the Azure EventHub library. \ No newline at end of file diff --git a/azurefunctions-extensions-bindings-eventhub/tests/test_eventhubdata.py b/azurefunctions-extensions-bindings-eventhub/tests/test_eventhubdata.py index 948c506..b82739b 100644 --- a/azurefunctions-extensions-bindings-eventhub/tests/test_eventhubdata.py +++ b/azurefunctions-extensions-bindings-eventhub/tests/test_eventhubdata.py @@ -28,7 +28,7 @@ def direction(self) -> int: return self._direction.value -class TestBlobClient(unittest.TestCase): +class TestEventHubData(unittest.TestCase): def test_input_type(self): check_input_type = EventHubDataConverter.check_input_type_annotation self.assertTrue(check_input_type(EventHubData)) @@ -65,8 +65,8 @@ def test_input_empty(self): def test_input_populated(self): sample_mbd = MockMBD( version="1.0", - source="AzureStorageBlobs", - content_type="application/json", + source="AzureEventHubsEventData", + content_type="application/octet-stream", content = EVENTHUB_SAMPLE_CONTENT ) @@ -83,48 +83,11 @@ def test_input_populated(self): self.assertIsNotNone(sdk_result) self.assertIsInstance(sdk_result, EventData) - def test_invalid_input_populated(self): - sample_mbd = MockMBD( - version="1.0", - source="AzureStorageBlobs", - content_type="application/json", - content = EVENTHUB_SAMPLE_CONTENT - ) - - with self.assertRaises(ValueError) as e: - datum: Datum = Datum(value=sample_mbd, type="model_binding_data") - result: EventHubData = EventHubDataConverter.decode( - data=datum, trigger_metadata=None, pytype=EventHubData - ) - self.assertEqual( - e.exception.args[0], - "Storage account connection string NotARealConnectionString does not exist. " - "Please make sure that it is a defined App Setting.", - ) - - def test_none_input_populated(self): - sample_mbd = MockMBD( - version="1.0", - source="AzureStorageBlobs", - content_type="application/json", - content = EVENTHUB_SAMPLE_CONTENT - ) - - with self.assertRaises(ValueError) as e: - datum: Datum = Datum(value=sample_mbd, type="model_binding_data") - result: EventHubData = EventHubDataConverter.decode( - data=datum, trigger_metadata=None, pytype=EventHubData - ) - self.assertEqual( - e.exception.args[0], - "Storage account connection string cannot be None. Please provide a connection string.", - ) - def test_input_invalid_pytype(self): sample_mbd = MockMBD( version="1.0", - source="AzureStorageBlobs", - content_type="application/json", + source="AzureEventHubsEventData", + content_type="application/octet-stream", content = EVENTHUB_SAMPLE_CONTENT ) diff --git a/azurefunctions-extensions-http-fastapi/README.md b/azurefunctions-extensions-http-fastapi/README.md index 9f64bb6..801a2c8 100644 --- a/azurefunctions-extensions-http-fastapi/README.md +++ b/azurefunctions-extensions-http-fastapi/README.md @@ -5,7 +5,7 @@ This library contains HttpV2 extensions for FastApi Request/Response types to us | [Package (PyPi)](https://pypi.org/project/azurefunctions-extensions-http-fastapi/) | API reference documentation | Product documentation -| [Samples](hhttps://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-http-fastapi/samples) +| [Samples](https://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-http-fastapi/samples) ## Getting started @@ -57,13 +57,13 @@ def process_data_chunk(chunk: bytes): ### More sample code -Get started with our [FastApi samples](hhttps://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-http-fastapi/samples). +Get started with our [FastApi samples](https://github.com/Azure/azure-functions-python-extensions/tree/dev/azurefunctions-extensions-http-fastapi/samples). Several samples are available in this GitHub repository. These samples provide example code for additional scenarios commonly encountered while working with FastApi: -* [fastapi_samples_streaming_upload](https://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-http-fastapi/samples/fastapi_samples_streaming_upload) - An example on how to send and receiving a streaming request within your function. +* [fastapi_samples_streaming_upload](https://github.com/Azure/azure-functions-python-extensions/tree/dev/azurefunctions-extensions-http-fastapi/samples/fastapi_samples_streaming_upload) - An example on how to send and receiving a streaming request within your function. -* [fastapi_samples_streaming_download](https://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-http-fastapi/samples/fastapi_samples_streaming_download) - An example on how to send your http response via streaming to the caller.t +* [fastapi_samples_streaming_download](https://github.com/Azure/azure-functions-python-extensions/tree/dev/azurefunctions-extensions-http-fastapi/samples/fastapi_samples_streaming_download) - An example on how to send your http response via streaming to the caller.t ## Contributing This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit https://cla.microsoft.com. diff --git a/eng/templates/official/jobs/base-unit-tests.yml b/eng/templates/official/jobs/base-unit-tests.yml index db37bd4..c4d0bdb 100644 --- a/eng/templates/official/jobs/base-unit-tests.yml +++ b/eng/templates/official/jobs/base-unit-tests.yml @@ -25,5 +25,5 @@ jobs: python -m pip install -U -e .[dev] displayName: 'Install dependencies' - bash: | - python -m pytest -q --instafail azurefunctions-extensions-base/tests/ --ignore='azurefunctions-extensions-bindings-blob', --ignore='azurefunctions-extensions-http-fastapi' + python -m pytest -q --instafail azurefunctions-extensions-base/tests/ --ignore='azurefunctions-extensions-bindings-blob', --ignore='azurefunctions-extensions-http-fastapi', --ignore='azurefunctions-extensions-bindings-eventhub' displayName: "Running Base $(PYTHON_VERSION) Python Extension Tests" \ No newline at end of file diff --git a/eng/templates/official/jobs/eventhub-unit-tests.yml b/eng/templates/official/jobs/eventhub-unit-tests.yml index f017180..5a5d098 100644 --- a/eng/templates/official/jobs/eventhub-unit-tests.yml +++ b/eng/templates/official/jobs/eventhub-unit-tests.yml @@ -26,6 +26,4 @@ jobs: python -m pytest -q --instafail azurefunctions-extensions-bindings-eventhub/tests/ --ignore='azurefunctions-extensions-base', --ignore='azurefunctions-extensions-http-fastapi', --ignore='azurefunctions-extensions-bindings-blob' env: AzureWebJobsStorage: $(AzureWebJobsStorage) - input: $(input__serviceUri) - trigger: $(trigger__blobServiceUri) displayName: "Running EventHub $(PYTHON_VERSION) Python Extension Tests" From 3d186b92c00dbd8c0b15f9514eba6e35df9a5362 Mon Sep 17 00:00:00 2001 From: Evan Roman Date: Mon, 3 Mar 2025 15:18:35 -0600 Subject: [PATCH 07/26] Fix docs --- azurefunctions-extensions-http-fastapi/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/azurefunctions-extensions-http-fastapi/README.md b/azurefunctions-extensions-http-fastapi/README.md index 801a2c8..011ab04 100644 --- a/azurefunctions-extensions-http-fastapi/README.md +++ b/azurefunctions-extensions-http-fastapi/README.md @@ -1,11 +1,11 @@ # Azure Functions Extensions Http FastApi library for Python -This library contains HttpV2 extensions for FastApi Request/Response types to use in your function app code. +This library contains HttpV2 extensions for FastApi Request/Response types to use in your function app code. -[Source code](https://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-http-fastapi) +[Source code](https://github.com/Azure/azure-functions-python-extensions/tree/dev/azurefunctions-extensions-http-fastapi) | [Package (PyPi)](https://pypi.org/project/azurefunctions-extensions-http-fastapi/) | API reference documentation | Product documentation -| [Samples](https://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-http-fastapi/samples) +| [Samples](https://github.com/Azure/azure-functions-python-extensions/tree/dev/azurefunctions-extensions-http-fastapi/samples) ## Getting started From ef8590679bec58132143cfbaae375efed487b7ac Mon Sep 17 00:00:00 2001 From: Evan Roman Date: Mon, 3 Mar 2025 15:19:59 -0600 Subject: [PATCH 08/26] Fix docs --- azurefunctions-extensions-http-fastapi/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azurefunctions-extensions-http-fastapi/README.md b/azurefunctions-extensions-http-fastapi/README.md index 011ab04..b0200ab 100644 --- a/azurefunctions-extensions-http-fastapi/README.md +++ b/azurefunctions-extensions-http-fastapi/README.md @@ -63,7 +63,7 @@ Several samples are available in this GitHub repository. These samples provide e * [fastapi_samples_streaming_upload](https://github.com/Azure/azure-functions-python-extensions/tree/dev/azurefunctions-extensions-http-fastapi/samples/fastapi_samples_streaming_upload) - An example on how to send and receiving a streaming request within your function. -* [fastapi_samples_streaming_download](https://github.com/Azure/azure-functions-python-extensions/tree/dev/azurefunctions-extensions-http-fastapi/samples/fastapi_samples_streaming_download) - An example on how to send your http response via streaming to the caller.t +* [fastapi_samples_streaming_download](https://github.com/Azure/azure-functions-python-extensions/tree/dev/azurefunctions-extensions-http-fastapi/samples/fastapi_samples_streaming_download) - An example on how to send your http response via streaming to the caller. ## Contributing This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit https://cla.microsoft.com. From 0c03860b1ad1dde327180c529435ef3c5b9fa929 Mon Sep 17 00:00:00 2001 From: Evan Roman Date: Tue, 11 Mar 2025 16:54:46 -0500 Subject: [PATCH 09/26] Address --- .../extensions/bindings/blob/blobClient.py | 2 +- .../bindings/eventhub/eventHubData.py | 30 ++++++++++++++----- .../eventhub/eventHubDataConverter.py | 22 +++++++------- .../pyproject.toml | 1 + 4 files changed, 34 insertions(+), 21 deletions(-) diff --git a/azurefunctions-extensions-bindings-blob/azurefunctions/extensions/bindings/blob/blobClient.py b/azurefunctions-extensions-bindings-blob/azurefunctions/extensions/bindings/blob/blobClient.py index 269e278..418404d 100644 --- a/azurefunctions-extensions-bindings-blob/azurefunctions/extensions/bindings/blob/blobClient.py +++ b/azurefunctions-extensions-bindings-blob/azurefunctions/extensions/bindings/blob/blobClient.py @@ -11,7 +11,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 diff --git a/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventHubData.py b/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventHubData.py index 78a7e15..4927be1 100644 --- a/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventHubData.py +++ b/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventHubData.py @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -from typing import Union +from typing import Optional, Union import uamqp from azure.eventhub import EventData @@ -9,7 +9,7 @@ class EventHubData(SdkType): - def __init__(self, *, data: Union[bytes, Datum]) -> None: + def __init__(self, *, data: Datum) -> None: # model_binding_data properties self._data = data self._version = None @@ -25,14 +25,28 @@ def __init__(self, *, data: Union[bytes, Datum]) -> None: self.decoded_message = self.__get_eventhub_content(self._content) def __get_eventhub_content(self, content): + """ + When receiving the EventBindingData, the content field is in the form of bytes. + This content must be decoded in order to construct an EventData object from the azure.eventhub SDK. + The .NET worker uses the Azure.Core.Amqp library to do this: + https://github.com/Azure/azure-functions-dotnet-worker/blob/main/extensions/Worker.Extensions.EventHubs/src/EventDataConverter.cs#L45 + """ if content: - return uamqp.Message().decode_from_bytes(content) - else: - return None + try: + return uamqp.Message().decode_from_bytes(content) + except Exception as e: + raise ValueError(f"Failed to decode EventHub content: {e}") from e + + return None - def get_sdk_type(self): + def get_sdk_type(self) -> Optional[EventData]: + """ + When receiving an EventHub message, the content portion after being decoded + is used in the constructor to create an EventData object. This will contains + fields such as message, enqueue_time, and more. + """ # https://github.com/Azure/azure-sdk-for-python/issues/39711 if self.decoded_message: return EventData._from_message(self.decoded_message) - else: - return None + + return None diff --git a/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventHubDataConverter.py b/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventHubDataConverter.py index bf90ffe..b7b916c 100644 --- a/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventHubDataConverter.py +++ b/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventHubDataConverter.py @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -from typing import Any +from typing import Any, Optional from azurefunctions.extensions.base import Datum, InConverter, OutConverter @@ -21,22 +21,20 @@ def check_input_type_annotation(cls, pytype: type) -> bool: ) @classmethod - def decode(cls, data: Datum, *, trigger_metadata, pytype) -> Any: + def decode(cls, data: Datum, *, trigger_metadata, pytype) -> Optional[Any]: if data is None or data.type is None: return None - data_type = data.type - - if data_type == "model_binding_data": - data = data.value - else: + if data.type != "model_binding_data": raise ValueError( - f'unexpected type of data received for the "eventhub" binding ' - f": {data_type!r}" + "Unexpected type of data received for the 'eventhub' binding: " + + repr(data.type) ) + + content = data.value # Determines which sdk type to return based on pytype if pytype == EventHubData: - return EventHubData(data=data).get_sdk_type() - else: - return None + return EventHubData(data=content).get_sdk_type() + + return None diff --git a/azurefunctions-extensions-bindings-eventhub/pyproject.toml b/azurefunctions-extensions-bindings-eventhub/pyproject.toml index 957ec7b..cf42ee8 100644 --- a/azurefunctions-extensions-bindings-eventhub/pyproject.toml +++ b/azurefunctions-extensions-bindings-eventhub/pyproject.toml @@ -17,6 +17,7 @@ classifiers= [ 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', 'Operating System :: Microsoft :: Windows', 'Operating System :: POSIX', 'Operating System :: MacOS :: MacOS X', From 26b891aaad371183bf7897915a0867bd77b6c468 Mon Sep 17 00:00:00 2001 From: Evan Roman Date: Thu, 13 Mar 2025 10:56:07 -0500 Subject: [PATCH 10/26] Address --- azurefunctions-extensions-bindings-blob/README.md | 2 ++ .../azurefunctions/extensions/bindings/blob/blobClient.py | 1 - .../extensions/bindings/blob/containerClient.py | 3 +-- .../extensions/bindings/blob/storageStreamDownloader.py | 3 +-- .../samples/blob_samples_blobclient/function_app.py | 2 -- .../samples/blob_samples_blobclient/local.settings.json | 3 +-- .../samples/blob_samples_containerclient/function_app.py | 2 -- .../samples/blob_samples_containerclient/local.settings.json | 3 +-- .../blob_samples_storagestreamdownloader/function_app.py | 2 -- .../blob_samples_storagestreamdownloader/local.settings.json | 3 +-- azurefunctions-extensions-bindings-blob/tests/__init__.py | 3 --- azurefunctions-extensions-bindings-eventhub/README.md | 2 ++ .../extensions/bindings/eventhub/eventHubData.py | 4 ++-- .../samples/eventhub_samples_eventhubdata/function_app.py | 2 -- .../samples/eventhub_samples_eventhubdata/local.settings.json | 3 +-- azurefunctions-extensions-bindings-eventhub/tests/__init__.py | 3 --- azurefunctions-extensions-http-fastapi/tests/__init__.py | 1 - 17 files changed, 12 insertions(+), 30 deletions(-) diff --git a/azurefunctions-extensions-bindings-blob/README.md b/azurefunctions-extensions-bindings-blob/README.md index 02a2662..447bd21 100644 --- a/azurefunctions-extensions-bindings-blob/README.md +++ b/azurefunctions-extensions-bindings-blob/README.md @@ -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") diff --git a/azurefunctions-extensions-bindings-blob/azurefunctions/extensions/bindings/blob/blobClient.py b/azurefunctions-extensions-bindings-blob/azurefunctions/extensions/bindings/blob/blobClient.py index 418404d..7425439 100644 --- a/azurefunctions-extensions-bindings-blob/azurefunctions/extensions/bindings/blob/blobClient.py +++ b/azurefunctions-extensions-bindings-blob/azurefunctions/extensions/bindings/blob/blobClient.py @@ -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 diff --git a/azurefunctions-extensions-bindings-blob/azurefunctions/extensions/bindings/blob/containerClient.py b/azurefunctions-extensions-bindings-blob/azurefunctions/extensions/bindings/blob/containerClient.py index 7bb0de8..7de6679 100644 --- a/azurefunctions-extensions-bindings-blob/azurefunctions/extensions/bindings/blob/containerClient.py +++ b/azurefunctions-extensions-bindings-blob/azurefunctions/extensions/bindings/blob/containerClient.py @@ -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 @@ -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 diff --git a/azurefunctions-extensions-bindings-blob/azurefunctions/extensions/bindings/blob/storageStreamDownloader.py b/azurefunctions-extensions-bindings-blob/azurefunctions/extensions/bindings/blob/storageStreamDownloader.py index 61d8813..2aa254b 100644 --- a/azurefunctions-extensions-bindings-blob/azurefunctions/extensions/bindings/blob/storageStreamDownloader.py +++ b/azurefunctions-extensions-bindings-blob/azurefunctions/extensions/bindings/blob/storageStreamDownloader.py @@ -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 @@ -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 diff --git a/azurefunctions-extensions-bindings-blob/samples/blob_samples_blobclient/function_app.py b/azurefunctions-extensions-bindings-blob/samples/blob_samples_blobclient/function_app.py index 4d9e590..1fd36ef 100644 --- a/azurefunctions-extensions-bindings-blob/samples/blob_samples_blobclient/function_app.py +++ b/azurefunctions-extensions-bindings-blob/samples/blob_samples_blobclient/function_app.py @@ -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 diff --git a/azurefunctions-extensions-bindings-blob/samples/blob_samples_blobclient/local.settings.json b/azurefunctions-extensions-bindings-blob/samples/blob_samples_blobclient/local.settings.json index c3c2a89..6dc40bb 100644 --- a/azurefunctions-extensions-bindings-blob/samples/blob_samples_blobclient/local.settings.json +++ b/azurefunctions-extensions-bindings-blob/samples/blob_samples_blobclient/local.settings.json @@ -2,7 +2,6 @@ "IsEncrypted": false, "Values": { "FUNCTIONS_WORKER_RUNTIME": "python", - "AzureWebJobsStorage": "", - "AzureWebJobsFeatureFlags": "EnableWorkerIndexing" + "AzureWebJobsStorage": "UseDevelopmentStorage=true" } } \ No newline at end of file diff --git a/azurefunctions-extensions-bindings-blob/samples/blob_samples_containerclient/function_app.py b/azurefunctions-extensions-bindings-blob/samples/blob_samples_containerclient/function_app.py index 5bba218..2b63d7a 100644 --- a/azurefunctions-extensions-bindings-blob/samples/blob_samples_containerclient/function_app.py +++ b/azurefunctions-extensions-bindings-blob/samples/blob_samples_containerclient/function_app.py @@ -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 diff --git a/azurefunctions-extensions-bindings-blob/samples/blob_samples_containerclient/local.settings.json b/azurefunctions-extensions-bindings-blob/samples/blob_samples_containerclient/local.settings.json index c3c2a89..6dc40bb 100644 --- a/azurefunctions-extensions-bindings-blob/samples/blob_samples_containerclient/local.settings.json +++ b/azurefunctions-extensions-bindings-blob/samples/blob_samples_containerclient/local.settings.json @@ -2,7 +2,6 @@ "IsEncrypted": false, "Values": { "FUNCTIONS_WORKER_RUNTIME": "python", - "AzureWebJobsStorage": "", - "AzureWebJobsFeatureFlags": "EnableWorkerIndexing" + "AzureWebJobsStorage": "UseDevelopmentStorage=true" } } \ No newline at end of file diff --git a/azurefunctions-extensions-bindings-blob/samples/blob_samples_storagestreamdownloader/function_app.py b/azurefunctions-extensions-bindings-blob/samples/blob_samples_storagestreamdownloader/function_app.py index 451c8af..e029731 100644 --- a/azurefunctions-extensions-bindings-blob/samples/blob_samples_storagestreamdownloader/function_app.py +++ b/azurefunctions-extensions-bindings-blob/samples/blob_samples_storagestreamdownloader/function_app.py @@ -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 diff --git a/azurefunctions-extensions-bindings-blob/samples/blob_samples_storagestreamdownloader/local.settings.json b/azurefunctions-extensions-bindings-blob/samples/blob_samples_storagestreamdownloader/local.settings.json index c3c2a89..6dc40bb 100644 --- a/azurefunctions-extensions-bindings-blob/samples/blob_samples_storagestreamdownloader/local.settings.json +++ b/azurefunctions-extensions-bindings-blob/samples/blob_samples_storagestreamdownloader/local.settings.json @@ -2,7 +2,6 @@ "IsEncrypted": false, "Values": { "FUNCTIONS_WORKER_RUNTIME": "python", - "AzureWebJobsStorage": "", - "AzureWebJobsFeatureFlags": "EnableWorkerIndexing" + "AzureWebJobsStorage": "UseDevelopmentStorage=true" } } \ No newline at end of file diff --git a/azurefunctions-extensions-bindings-blob/tests/__init__.py b/azurefunctions-extensions-bindings-blob/tests/__init__.py index 528a01b..3a41690 100644 --- a/azurefunctions-extensions-bindings-blob/tests/__init__.py +++ b/azurefunctions-extensions-bindings-blob/tests/__init__.py @@ -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(): diff --git a/azurefunctions-extensions-bindings-eventhub/README.md b/azurefunctions-extensions-bindings-eventhub/README.md index 6a94c1d..a5c17d3 100644 --- a/azurefunctions-extensions-bindings-eventhub/README.md +++ b/azurefunctions-extensions-bindings-eventhub/README.md @@ -51,6 +51,8 @@ import logging import azure.functions as func import azurefunctions.extensions.bindings.eventhub as eh +app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) + @app.event_hub_message_trigger( arg_name="eh_data", event_hub_name="EVENTHUB_NAME", connection="AzureWebJobsStorage" ) diff --git a/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventHubData.py b/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventHubData.py index 4927be1..9c32d34 100644 --- a/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventHubData.py +++ b/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventHubData.py @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -from typing import Optional, Union +from typing import Optional import uamqp from azure.eventhub import EventData @@ -42,7 +42,7 @@ def __get_eventhub_content(self, content): def get_sdk_type(self) -> Optional[EventData]: """ When receiving an EventHub message, the content portion after being decoded - is used in the constructor to create an EventData object. This will contains + is used in the constructor to create an EventData object. This will contain fields such as message, enqueue_time, and more. """ # https://github.com/Azure/azure-sdk-for-python/issues/39711 diff --git a/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventhubdata/function_app.py b/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventhubdata/function_app.py index f17515f..f73bdf7 100644 --- a/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventhubdata/function_app.py +++ b/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventhubdata/function_app.py @@ -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 diff --git a/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventhubdata/local.settings.json b/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventhubdata/local.settings.json index c3c2a89..6dc40bb 100644 --- a/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventhubdata/local.settings.json +++ b/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventhubdata/local.settings.json @@ -2,7 +2,6 @@ "IsEncrypted": false, "Values": { "FUNCTIONS_WORKER_RUNTIME": "python", - "AzureWebJobsStorage": "", - "AzureWebJobsFeatureFlags": "EnableWorkerIndexing" + "AzureWebJobsStorage": "UseDevelopmentStorage=true" } } \ No newline at end of file diff --git a/azurefunctions-extensions-bindings-eventhub/tests/__init__.py b/azurefunctions-extensions-bindings-eventhub/tests/__init__.py index 528a01b..3a41690 100644 --- a/azurefunctions-extensions-bindings-eventhub/tests/__init__.py +++ b/azurefunctions-extensions-bindings-eventhub/tests/__init__.py @@ -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(): diff --git a/azurefunctions-extensions-http-fastapi/tests/__init__.py b/azurefunctions-extensions-http-fastapi/tests/__init__.py index 528a01b..beb657e 100644 --- a/azurefunctions-extensions-http-fastapi/tests/__init__.py +++ b/azurefunctions-extensions-http-fastapi/tests/__init__.py @@ -6,7 +6,6 @@ import os.path import sys import unittest -import unittest.runner def suite(): From 239505b8f01d10556ab7771dbcc70f57d6914857 Mon Sep 17 00:00:00 2001 From: Evan Roman Date: Thu, 13 Mar 2025 10:57:15 -0500 Subject: [PATCH 11/26] Rm --- azurefunctions-extensions-base/tests/__init__.py | 2 -- azurefunctions-extensions-http-fastapi/tests/__init__.py | 2 -- 2 files changed, 4 deletions(-) diff --git a/azurefunctions-extensions-base/tests/__init__.py b/azurefunctions-extensions-base/tests/__init__.py index 528a01b..2b28c76 100644 --- a/azurefunctions-extensions-base/tests/__init__.py +++ b/azurefunctions-extensions-base/tests/__init__.py @@ -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 diff --git a/azurefunctions-extensions-http-fastapi/tests/__init__.py b/azurefunctions-extensions-http-fastapi/tests/__init__.py index beb657e..3a41690 100644 --- a/azurefunctions-extensions-http-fastapi/tests/__init__.py +++ b/azurefunctions-extensions-http-fastapi/tests/__init__.py @@ -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 From e5620bfeb76ca2d9e8504b64513b8895fd57694b Mon Sep 17 00:00:00 2001 From: Evan Roman Date: Tue, 18 Mar 2025 02:10:14 -0500 Subject: [PATCH 12/26] Add --- .../azurefunctions/extensions/base/meta.py | 2 +- .../tests/test_meta.py | 6 ++ .../README.md | 4 +- .../extensions/bindings/eventhub/__init__.py | 8 +- .../{eventHubData.py => eventData.py} | 4 +- .../bindings/eventhub/eventDataConverter.py | 51 ++++++++++++ .../eventhub/eventHubDataConverter.py | 40 ---------- .../samples/README.md | 2 +- .../function_app.py | 4 +- .../host.json | 0 .../local.settings.json | 0 .../requirements.txt | 0 ...test_eventhubdata.py => test_eventdata.py} | 79 ++++++++++++++----- 13 files changed, 130 insertions(+), 70 deletions(-) rename azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/{eventHubData.py => eventData.py} (95%) create mode 100644 azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventDataConverter.py delete mode 100644 azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventHubDataConverter.py rename azurefunctions-extensions-bindings-eventhub/samples/{eventhub_samples_eventhubdata => eventhub_samples_eventdata}/function_app.py (94%) rename azurefunctions-extensions-bindings-eventhub/samples/{eventhub_samples_eventhubdata => eventhub_samples_eventdata}/host.json (100%) rename azurefunctions-extensions-bindings-eventhub/samples/{eventhub_samples_eventhubdata => eventhub_samples_eventdata}/local.settings.json (100%) rename azurefunctions-extensions-bindings-eventhub/samples/{eventhub_samples_eventhubdata => eventhub_samples_eventdata}/requirements.txt (100%) rename azurefunctions-extensions-bindings-eventhub/tests/{test_eventhubdata.py => test_eventdata.py} (51%) diff --git a/azurefunctions-extensions-base/azurefunctions/extensions/base/meta.py b/azurefunctions-extensions-base/azurefunctions/extensions/base/meta.py index a701ad2..a6742c7 100644 --- a/azurefunctions-extensions-base/azurefunctions/extensions/base/meta.py +++ b/azurefunctions-extensions-base/azurefunctions/extensions/base/meta.py @@ -110,7 +110,7 @@ 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 diff --git a/azurefunctions-extensions-base/tests/test_meta.py b/azurefunctions-extensions-base/tests/test_meta.py index a322058..c8c9dd8 100644 --- a/azurefunctions-extensions-base/tests/test_meta.py +++ b/azurefunctions-extensions-base/tests/test_meta.py @@ -166,6 +166,12 @@ def test_decode_typed_data(self): meta._BaseConverter._decode_typed_data(datum_mbd, python_type=str), "{}" ) + # 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 3: data.type is None datum_none = meta.Datum(value="{}", type=None) self.assertIsNone( diff --git a/azurefunctions-extensions-bindings-eventhub/README.md b/azurefunctions-extensions-bindings-eventhub/README.md index a5c17d3..39b4d2d 100644 --- a/azurefunctions-extensions-bindings-eventhub/README.md +++ b/azurefunctions-extensions-bindings-eventhub/README.md @@ -56,7 +56,7 @@ app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) @app.event_hub_message_trigger( arg_name="eh_data", event_hub_name="EVENTHUB_NAME", connection="AzureWebJobsStorage" ) -def eventhub_trigger(eh_data: eh.EventHubData): +def eventhub_trigger(eh_data: eh.EventData): logging.info( "Python EventHub trigger processed an event %s", eh_data.body_as_str() @@ -77,7 +77,7 @@ Get started with our [EventHub samples](https://github.com/Azure/azure-functions Several samples are available in this GitHub repository. These samples provide example code for additional scenarios commonly encountered while working with EventHubs: -* [eventhub_samples_eventhubdata](https://github.com/Azure/azure-functions-python-extensions/tree/dev/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventhubdata) - Examples for using the EventHubData type: +* [eventhub_samples_eventdata](https://github.com/Azure/azure-functions-python-extensions/tree/dev/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventdata) - Examples for using the EventData type: * From EventHubTrigger ### Additional documentation diff --git a/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/__init__.py b/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/__init__.py index 340a9ea..7117144 100644 --- a/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/__init__.py +++ b/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/__init__.py @@ -1,12 +1,12 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -from .eventHubData import EventHubData -from .eventHubDataConverter import EventHubDataConverter +from .eventData import EventData +from .eventDataConverter import EventDataConverter __all__ = [ - "EventHubData", - "EventHubDataConverter", + "EventData", + "EventDataConverter", ] __version__ = "1.0.0b1" diff --git a/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventHubData.py b/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventData.py similarity index 95% rename from azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventHubData.py rename to azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventData.py index 9c32d34..5510ead 100644 --- a/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventHubData.py +++ b/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventData.py @@ -8,7 +8,7 @@ from azurefunctions.extensions.base import Datum, SdkType -class EventHubData(SdkType): +class EventData(SdkType): def __init__(self, *, data: Datum) -> None: # model_binding_data properties self._data = data @@ -43,7 +43,7 @@ def get_sdk_type(self) -> Optional[EventData]: """ When receiving an EventHub message, the content portion after being decoded is used in the constructor to create an EventData object. This will contain - fields such as message, enqueue_time, and more. + fields such as message, enqueued_time, and more. """ # https://github.com/Azure/azure-sdk-for-python/issues/39711 if self.decoded_message: diff --git a/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventDataConverter.py b/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventDataConverter.py new file mode 100644 index 0000000..e862e78 --- /dev/null +++ b/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventDataConverter.py @@ -0,0 +1,51 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import json +from typing import Any, Optional + +from azurefunctions.extensions.base import Datum, InConverter, OutConverter + +from .eventData import EventData + + +class EventDataConverter( + InConverter, + OutConverter, + binding="eventHub", + trigger="eventHubTrigger", +): + @classmethod + def check_input_type_annotation(cls, pytype: type) -> bool: + return issubclass( + pytype, (EventData) + ) + + @classmethod + def decode(cls, data: Datum, *, trigger_metadata, pytype) -> Optional[Any]: + """ + EventHub allows for batches to be sent. This means the cardinality can be one or many + When the cardinality is one: + - The data is of type "model_binding_data" - each event is an independent function invocation + When the cardinality is many: + - The data is of type "collection_model_binding_data" - all events are sent in a single function invocation + - collection_model_binding_data has 1 or more model_binding_data objects + """ + if data is None or data.type is None or pytype != EventData: + return None + + # Process each model_binding_data in the collection + if data.type == "collection_model_binding_data": + try: + return [EventData(data=mbd).get_sdk_type() for mbd in data.value.model_binding_data] + except Exception as e: + raise ValueError("Failed to decode incoming EventHub batch: " + repr(e)) from e + + # Get model_binding_data fields directly + if data.type == "model_binding_data": + return EventData(data=data.value).get_sdk_type() + + raise ValueError( + "Unexpected type of data received for the 'eventhub' binding: " + + repr(data.type) + ) \ No newline at end of file diff --git a/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventHubDataConverter.py b/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventHubDataConverter.py deleted file mode 100644 index b7b916c..0000000 --- a/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventHubDataConverter.py +++ /dev/null @@ -1,40 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from typing import Any, Optional - -from azurefunctions.extensions.base import Datum, InConverter, OutConverter - -from .eventHubData import EventHubData - - -class EventHubDataConverter( - InConverter, - OutConverter, - binding="eventHub", - trigger="eventHubTrigger", -): - @classmethod - def check_input_type_annotation(cls, pytype: type) -> bool: - return issubclass( - pytype, (EventHubData) - ) - - @classmethod - def decode(cls, data: Datum, *, trigger_metadata, pytype) -> Optional[Any]: - if data is None or data.type is None: - return None - - if data.type != "model_binding_data": - raise ValueError( - "Unexpected type of data received for the 'eventhub' binding: " - + repr(data.type) - ) - - content = data.value - - # Determines which sdk type to return based on pytype - if pytype == EventHubData: - return EventHubData(data=content).get_sdk_type() - - return None diff --git a/azurefunctions-extensions-bindings-eventhub/samples/README.md b/azurefunctions-extensions-bindings-eventhub/samples/README.md index 75f2911..2fd1d7b 100644 --- a/azurefunctions-extensions-bindings-eventhub/samples/README.md +++ b/azurefunctions-extensions-bindings-eventhub/samples/README.md @@ -17,7 +17,7 @@ These are code samples that show common scenario operations with the Azure Funct These samples relate to the Azure EventHub library being used as part of a Python Function App. For examples on how to use the Azure EventHub library, please see [Azure EventHub samples](https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/eventhub/azure-eventhub/samples) -* [eventhub_samples_eventhubdata](https://github.com/Azure/azure-functions-python-extensions/tree/dev/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventhubdata) - Examples for using the EventData type: +* [eventhub_samples_eventdata](https://github.com/Azure/azure-functions-python-extensions/tree/dev/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventdata) - Examples for using the EventData type: * From EventHubTrigger ## Prerequisites diff --git a/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventhubdata/function_app.py b/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventdata/function_app.py similarity index 94% rename from azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventhubdata/function_app.py rename to azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventdata/function_app.py index f73bdf7..14179c7 100644 --- a/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventhubdata/function_app.py +++ b/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventdata/function_app.py @@ -11,7 +11,7 @@ app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) """ -FOLDER: eventhub_samples_eventhubdata +FOLDER: eventhub_samples_eventdata DESCRIPTION: These samples demonstrate how to obtain EventHubData from an EventHub Trigger. USAGE: @@ -30,7 +30,7 @@ @app.event_hub_message_trigger( arg_name="eh_data", event_hub_name="EVENTHUB_NAME", connection="AzureWebJobsStorage" ) -def eventhub_trigger(eh_data: eh.EventHubData): +def eventhub_trigger(eh_data: eh.EventData): logging.info( "Python EventHub trigger processed an event %s", eh_data.body_as_str() diff --git a/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventhubdata/host.json b/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventdata/host.json similarity index 100% rename from azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventhubdata/host.json rename to azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventdata/host.json diff --git a/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventhubdata/local.settings.json b/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventdata/local.settings.json similarity index 100% rename from azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventhubdata/local.settings.json rename to azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventdata/local.settings.json diff --git a/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventhubdata/requirements.txt b/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventdata/requirements.txt similarity index 100% rename from azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventhubdata/requirements.txt rename to azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventdata/requirements.txt diff --git a/azurefunctions-extensions-bindings-eventhub/tests/test_eventhubdata.py b/azurefunctions-extensions-bindings-eventhub/tests/test_eventdata.py similarity index 51% rename from azurefunctions-extensions-bindings-eventhub/tests/test_eventhubdata.py rename to azurefunctions-extensions-bindings-eventhub/tests/test_eventdata.py index b82739b..d62ccd9 100644 --- a/azurefunctions-extensions-bindings-eventhub/tests/test_eventhubdata.py +++ b/azurefunctions-extensions-bindings-eventhub/tests/test_eventdata.py @@ -7,7 +7,7 @@ from azure.eventhub import EventData from azurefunctions.extensions.base import Datum -from azurefunctions.extensions.bindings.eventhub import EventHubData, EventHubDataConverter +from azurefunctions.extensions.bindings.eventhub import EventData, EventDataConverter 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" @@ -26,43 +26,65 @@ def data_type(self) -> Optional[int]: @property def direction(self) -> int: return self._direction.value + +class MockCMBD: + def __init__(self, version: str, source: str, content_type: str, content: str): + self.version = version + self.source = source + self.content_type = content_type + self.content = content + + @property + def data_type(self) -> Optional[int]: + return self._data_type.value if self._data_type else None + + @property + def direction(self) -> int: + return self._direction.value -class TestEventHubData(unittest.TestCase): +class TestEventData(unittest.TestCase): def test_input_type(self): - check_input_type = EventHubDataConverter.check_input_type_annotation - self.assertTrue(check_input_type(EventHubData)) + check_input_type = EventDataConverter.check_input_type_annotation + self.assertTrue(check_input_type(EventData)) self.assertFalse(check_input_type(str)) self.assertFalse(check_input_type(bytes)) self.assertFalse(check_input_type(bytearray)) def test_input_none(self): - result = EventHubDataConverter.decode( - data=None, trigger_metadata=None, pytype=EventHubData + result = EventDataConverter.decode( + data=None, trigger_metadata=None, pytype=EventData ) self.assertIsNone(result) datum: Datum = Datum(value=b"string_content", type=None) - result = EventHubDataConverter.decode( - data=datum, trigger_metadata=None, pytype=EventHubData + result = EventDataConverter.decode( + data=datum, trigger_metadata=None, pytype=EventData ) self.assertIsNone(result) def test_input_incorrect_type(self): datum: Datum = Datum(value=b"string_content", type="bytearray") with self.assertRaises(ValueError): - EventHubDataConverter.decode( - data=datum, trigger_metadata=None, pytype=EventHubData + EventDataConverter.decode( + data=datum, trigger_metadata=None, pytype=EventData ) - def test_input_empty(self): + def test_input_empty_mbd(self): datum: Datum = Datum(value={}, type="model_binding_data") - result: EventHubData = EventHubDataConverter.decode( - data=datum, trigger_metadata=None, pytype=EventHubData + result: EventData = EventDataConverter.decode( + data=datum, trigger_metadata=None, pytype=EventData + ) + self.assertIsNone(result) + + def test_input_empty_cmbd(self): + datum: Datum = Datum(value={}, type="collection_model_binding_data") + result: EventData = EventDataConverter.decode( + data=datum, trigger_metadata=None, pytype=EventData ) self.assertIsNone(result) - def test_input_populated(self): + def test_input_populated_mbd(self): sample_mbd = MockMBD( version="1.0", source="AzureEventHubsEventData", @@ -71,14 +93,35 @@ def test_input_populated(self): ) datum: Datum = Datum(value=sample_mbd, type="model_binding_data") - result: EventHubData = EventHubDataConverter.decode( - data=datum, trigger_metadata=None, pytype=EventHubData + result: EventData = EventDataConverter.decode( + data=datum, trigger_metadata=None, pytype=EventData + ) + + self.assertIsNotNone(result) + self.assertIsInstance(result, EventData) + + sdk_result = EventData(data=datum.value).get_sdk_type() + + self.assertIsNotNone(sdk_result) + self.assertIsInstance(sdk_result, EventData) + + def test_input_populated_cmbd(self): + sample_mbd = MockMBD( + version="1.0", + source="AzureEventHubsEventData", + content_type="application/octet-stream", + content = EVENTHUB_SAMPLE_CONTENT + ) + + datum: Datum = Datum(value=[sample_mbd, sample_mbd], type="collection_model_binding_data") + result: EventData = EventDataConverter.decode( + data=datum, trigger_metadata=None, pytype=EventData ) self.assertIsNotNone(result) self.assertIsInstance(result, EventData) - sdk_result = EventHubData(data=datum.value).get_sdk_type() + sdk_result = EventData(data=datum.value).get_sdk_type() self.assertIsNotNone(sdk_result) self.assertIsInstance(sdk_result, EventData) @@ -92,7 +135,7 @@ def test_input_invalid_pytype(self): ) datum: Datum = Datum(value=sample_mbd, type="model_binding_data") - result: EventHubData = EventHubDataConverter.decode( + result: EventData = EventDataConverter.decode( data=datum, trigger_metadata=None, pytype="str" ) From 5cc2b740d86a42f56f09a9d847430ed24e45985e Mon Sep 17 00:00:00 2001 From: Evan Roman Date: Fri, 28 Mar 2025 00:02:13 -0500 Subject: [PATCH 13/26] Add --- .../azurefunctions/extensions/base/meta.py | 18 ++++++++++++++- .../tests/test_meta.py | 10 ++++----- .../bindings/eventhub/eventDataConverter.py | 18 ++++++++++----- .../function_app.py | 2 +- .../tests/test_eventdata.py | 22 +++++++++---------- 5 files changed, 45 insertions(+), 25 deletions(-) diff --git a/azurefunctions-extensions-base/azurefunctions/extensions/base/meta.py b/azurefunctions-extensions-base/azurefunctions/extensions/base/meta.py index a6742c7..883f9b5 100644 --- a/azurefunctions-extensions-base/azurefunctions/extensions/base/meta.py +++ b/azurefunctions-extensions-base/azurefunctions/extensions/base/meta.py @@ -2,9 +2,10 @@ # Licensed under the MIT License. import abc +import collections.abc import inspect 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 @@ -90,7 +91,22 @@ def get_raw_bindings(cls, indexed_function, input_types): def check_supported_type(cls, subclass: type) -> bool: if subclass is not None and inspect.isclass(subclass): return issubclass(subclass, sdkType.SdkType) + if subclass is not None: + return cls.__is_iterable_subclass(subclass) return False + + @classmethod + def __is_iterable_subclass(cls, annotation: type) -> bool: + if not issubclass(get_origin(annotation), collections.abc.Iterable): + return False + + # Extract inner type(s) from iterable + args = get_args(annotation) + if not args: + return False + + return any(isinstance(arg, type) and issubclass(arg, sdkType.SdkType) + for arg in args) def has_trigger_support(cls) -> bool: return cls._trigger is not None # type: ignore diff --git a/azurefunctions-extensions-base/tests/test_meta.py b/azurefunctions-extensions-base/tests/test_meta.py index c8c9dd8..d3ff43b 100644 --- a/azurefunctions-extensions-base/tests/test_meta.py +++ b/azurefunctions-extensions-base/tests/test_meta.py @@ -172,32 +172,32 @@ def test_decode_typed_data(self): meta._BaseConverter._decode_typed_data(datum_cmbd, python_type=str), "{}" ) - # Case 3: data.type is None + # 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), diff --git a/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventDataConverter.py b/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventDataConverter.py index e862e78..1314643 100644 --- a/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventDataConverter.py +++ b/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventDataConverter.py @@ -1,11 +1,10 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -import json -from typing import Any, Optional +import collections.abc +from typing import Any, Optional, get_args, get_origin from azurefunctions.extensions.base import Datum, InConverter, OutConverter - from .eventData import EventData @@ -17,9 +16,16 @@ class EventDataConverter( ): @classmethod def check_input_type_annotation(cls, pytype: type) -> bool: - return issubclass( - pytype, (EventData) - ) + if not issubclass(get_origin(pytype), collections.abc.Iterable): + return issubclass(pytype, EventData) + + # Extract inner type(s) from iterable + args = get_args(pytype) + if not args: + return False + + return any(isinstance(arg, type) and issubclass(arg, EventData) + for arg in args) @classmethod def decode(cls, data: Datum, *, trigger_metadata, pytype) -> Optional[Any]: diff --git a/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventdata/function_app.py b/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventdata/function_app.py index 14179c7..8006311 100644 --- a/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventdata/function_app.py +++ b/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventdata/function_app.py @@ -13,7 +13,7 @@ """ FOLDER: eventhub_samples_eventdata DESCRIPTION: - These samples demonstrate how to obtain EventHubData from an EventHub Trigger. + These samples demonstrate how to obtain EventData from an EventHub Trigger. USAGE: There are different ways to connect to an EventHub via the connection property and envionrment variables specifiied in local.settings.json diff --git a/azurefunctions-extensions-bindings-eventhub/tests/test_eventdata.py b/azurefunctions-extensions-bindings-eventhub/tests/test_eventdata.py index d62ccd9..e9ff5c2 100644 --- a/azurefunctions-extensions-bindings-eventhub/tests/test_eventdata.py +++ b/azurefunctions-extensions-bindings-eventhub/tests/test_eventdata.py @@ -2,9 +2,9 @@ # Licensed under the MIT License. import unittest -from typing import Optional +from typing import List, Optional -from azure.eventhub import EventData +from azure.eventhub import EventData as EventDataSdk from azurefunctions.extensions.base import Datum from azurefunctions.extensions.bindings.eventhub import EventData, EventDataConverter @@ -27,12 +27,10 @@ def data_type(self) -> Optional[int]: def direction(self) -> int: return self._direction.value + class MockCMBD: - def __init__(self, version: str, source: str, content_type: str, content: str): - self.version = version - self.source = source - self.content_type = content_type - self.content = content + def __init__(self, model_binding_data_list: List[MockMBD]): + self.model_binding_data = model_binding_data_list @property def data_type(self) -> Optional[int]: @@ -98,12 +96,12 @@ def test_input_populated_mbd(self): ) self.assertIsNotNone(result) - self.assertIsInstance(result, EventData) + self.assertIsInstance(result, EventDataSdk) sdk_result = EventData(data=datum.value).get_sdk_type() self.assertIsNotNone(sdk_result) - self.assertIsInstance(sdk_result, EventData) + self.assertIsInstance(sdk_result, EventDataSdk) def test_input_populated_cmbd(self): sample_mbd = MockMBD( @@ -113,18 +111,18 @@ def test_input_populated_cmbd(self): content = EVENTHUB_SAMPLE_CONTENT ) - datum: Datum = Datum(value=[sample_mbd, sample_mbd], type="collection_model_binding_data") + datum: Datum = Datum(value=MockCMBD([sample_mbd, sample_mbd]), type="collection_model_binding_data") result: EventData = EventDataConverter.decode( data=datum, trigger_metadata=None, pytype=EventData ) self.assertIsNotNone(result) - self.assertIsInstance(result, EventData) + self.assertIsInstance(result, EventDataSdk) sdk_result = EventData(data=datum.value).get_sdk_type() self.assertIsNotNone(sdk_result) - self.assertIsInstance(sdk_result, EventData) + self.assertIsInstance(sdk_result, EventDataSdk) def test_input_invalid_pytype(self): sample_mbd = MockMBD( From 1a2910ba3f1194cf50507428754dcc469b7ffbce Mon Sep 17 00:00:00 2001 From: Evan Roman Date: Fri, 28 Mar 2025 01:19:19 -0500 Subject: [PATCH 14/26] Fix --- .../azurefunctions/extensions/base/meta.py | 15 +++++++++------ .../bindings/eventhub/eventDataConverter.py | 5 ++++- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/azurefunctions-extensions-base/azurefunctions/extensions/base/meta.py b/azurefunctions-extensions-base/azurefunctions/extensions/base/meta.py index 883f9b5..21d7201 100644 --- a/azurefunctions-extensions-base/azurefunctions/extensions/base/meta.py +++ b/azurefunctions-extensions-base/azurefunctions/extensions/base/meta.py @@ -94,19 +94,21 @@ def check_supported_type(cls, subclass: type) -> bool: if subclass is not None: return cls.__is_iterable_subclass(subclass) return False - + @classmethod def __is_iterable_subclass(cls, annotation: type) -> bool: - if not issubclass(get_origin(annotation), collections.abc.Iterable): + origin = get_origin(annotation) + if (not inspect.isclass(origin) or + not issubclass(origin, collections.abc.Iterable)): return False # Extract inner type(s) from iterable args = get_args(annotation) if not args: return False - - return any(isinstance(arg, type) and issubclass(arg, sdkType.SdkType) - for arg in args) + + return any(isinstance(arg, type) + and issubclass(arg, sdkType.SdkType) for arg in args) def has_trigger_support(cls) -> bool: return cls._trigger is not None # type: ignore @@ -126,7 +128,8 @@ def _decode_typed_data( return None data_type = data.type - if data_type == "model_binding_data" or data_type == "collection_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 diff --git a/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventDataConverter.py b/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventDataConverter.py index 1314643..4d5d9e7 100644 --- a/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventDataConverter.py +++ b/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventDataConverter.py @@ -2,6 +2,7 @@ # Licensed under the MIT License. import collections.abc +import inspect from typing import Any, Optional, get_args, get_origin from azurefunctions.extensions.base import Datum, InConverter, OutConverter @@ -16,7 +17,9 @@ class EventDataConverter( ): @classmethod def check_input_type_annotation(cls, pytype: type) -> bool: - if not issubclass(get_origin(pytype), collections.abc.Iterable): + origin = get_origin(pytype) + if (not inspect.isclass(origin) or + not issubclass(get_origin(pytype), collections.abc.Iterable)): return issubclass(pytype, EventData) # Extract inner type(s) from iterable From 46eb74e740d91e634392ecb27820826da7a0f7b9 Mon Sep 17 00:00:00 2001 From: Evan Roman Date: Fri, 28 Mar 2025 01:35:24 -0500 Subject: [PATCH 15/26] Lint --- .../azurefunctions/extensions/base/meta.py | 11 ++++++----- .../bindings/eventhub/eventDataConverter.py | 3 +-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/azurefunctions-extensions-base/azurefunctions/extensions/base/meta.py b/azurefunctions-extensions-base/azurefunctions/extensions/base/meta.py index 21d7201..98d2d93 100644 --- a/azurefunctions-extensions-base/azurefunctions/extensions/base/meta.py +++ b/azurefunctions-extensions-base/azurefunctions/extensions/base/meta.py @@ -98,8 +98,8 @@ def check_supported_type(cls, subclass: type) -> bool: @classmethod def __is_iterable_subclass(cls, annotation: type) -> bool: origin = get_origin(annotation) - if (not inspect.isclass(origin) or - not issubclass(origin, collections.abc.Iterable)): + if (origin is None + or not issubclass(origin, collections.abc.Iterable)): return False # Extract inner type(s) from iterable @@ -107,7 +107,8 @@ def __is_iterable_subclass(cls, annotation: type) -> bool: if not args: return False - return any(isinstance(arg, type) + return any( + isinstance(arg, type) and issubclass(arg, sdkType.SdkType) for arg in args) def has_trigger_support(cls) -> bool: @@ -128,8 +129,8 @@ def _decode_typed_data( return None data_type = data.type - if (data_type == "model_binding_data" or - data_type == "collection_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 diff --git a/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventDataConverter.py b/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventDataConverter.py index 4d5d9e7..a398e3d 100644 --- a/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventDataConverter.py +++ b/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventDataConverter.py @@ -18,8 +18,7 @@ class EventDataConverter( @classmethod def check_input_type_annotation(cls, pytype: type) -> bool: origin = get_origin(pytype) - if (not inspect.isclass(origin) or - not issubclass(get_origin(pytype), collections.abc.Iterable)): + if origin is None or not issubclass(origin, collections.abc.Iterable): return issubclass(pytype, EventData) # Extract inner type(s) from iterable From 555df8f0a588b781a83a1415f98ae71a5954f31c Mon Sep 17 00:00:00 2001 From: Evan Roman Date: Fri, 28 Mar 2025 01:46:36 -0500 Subject: [PATCH 16/26] Fix --- .../azurefunctions/extensions/base/meta.py | 5 +++-- azurefunctions-extensions-base/tests/test_meta.py | 3 +++ .../extensions/bindings/eventhub/eventDataConverter.py | 1 + 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/azurefunctions-extensions-base/azurefunctions/extensions/base/meta.py b/azurefunctions-extensions-base/azurefunctions/extensions/base/meta.py index 98d2d93..901a0f6 100644 --- a/azurefunctions-extensions-base/azurefunctions/extensions/base/meta.py +++ b/azurefunctions-extensions-base/azurefunctions/extensions/base/meta.py @@ -97,9 +97,10 @@ def check_supported_type(cls, subclass: type) -> bool: @classmethod def __is_iterable_subclass(cls, annotation: type) -> bool: + # Get base type from type hint origin = get_origin(annotation) if (origin is None - or not issubclass(origin, collections.abc.Iterable)): + or not issubclass(origin, collections.abc.Iterable)): return False # Extract inner type(s) from iterable @@ -130,7 +131,7 @@ def _decode_typed_data( data_type = data.type if (data_type == "model_binding_data" - or data_type == "collection_model_binding_data"): + or data_type == "collection_model_binding_data"): result = data.value elif data_type is None: return None diff --git a/azurefunctions-extensions-base/tests/test_meta.py b/azurefunctions-extensions-base/tests/test_meta.py index d3ff43b..1d8f787 100644 --- a/azurefunctions-extensions-base/tests/test_meta.py +++ b/azurefunctions-extensions-base/tests/test_meta.py @@ -151,6 +151,9 @@ class MockIndexedFunction: self.assertFalse(registry.check_supported_type(None)) 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.assertTrue(registry.check_supported_type(list[sdkType.SdkType])) + self.assertTrue(registry.check_supported_type(tuple[sdkType.SdkType])) self.assertFalse(registry.has_trigger_support(MockIndexedFunction)) diff --git a/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventDataConverter.py b/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventDataConverter.py index a398e3d..f88f165 100644 --- a/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventDataConverter.py +++ b/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventDataConverter.py @@ -17,6 +17,7 @@ class EventDataConverter( ): @classmethod def check_input_type_annotation(cls, pytype: type) -> bool: + # Get base type from type hint origin = get_origin(pytype) if origin is None or not issubclass(origin, collections.abc.Iterable): return issubclass(pytype, EventData) From 4ef50196824e4d60b5f5dee3bee289474777f436 Mon Sep 17 00:00:00 2001 From: Evan Roman Date: Fri, 28 Mar 2025 02:02:24 -0500 Subject: [PATCH 17/26] Fix --- .../azurefunctions/extensions/base/meta.py | 4 ++-- azurefunctions-extensions-base/tests/test_meta.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/azurefunctions-extensions-base/azurefunctions/extensions/base/meta.py b/azurefunctions-extensions-base/azurefunctions/extensions/base/meta.py index 901a0f6..a35aa0b 100644 --- a/azurefunctions-extensions-base/azurefunctions/extensions/base/meta.py +++ b/azurefunctions-extensions-base/azurefunctions/extensions/base/meta.py @@ -89,10 +89,10 @@ def get_raw_bindings(cls, 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) if subclass is not None: return cls.__is_iterable_subclass(subclass) + if subclass is not None and inspect.isclass(subclass): + return issubclass(subclass, sdkType.SdkType) return False @classmethod diff --git a/azurefunctions-extensions-base/tests/test_meta.py b/azurefunctions-extensions-base/tests/test_meta.py index 1d8f787..d8dd462 100644 --- a/azurefunctions-extensions-base/tests/test_meta.py +++ b/azurefunctions-extensions-base/tests/test_meta.py @@ -154,6 +154,7 @@ class MockIndexedFunction: self.assertTrue(registry.check_supported_type(List[sdkType.SdkType])) 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.has_trigger_support(MockIndexedFunction)) From f4577f5b6307642f80639442d721bf35f9fc0344 Mon Sep 17 00:00:00 2001 From: Evan Roman Date: Fri, 28 Mar 2025 02:18:05 -0500 Subject: [PATCH 18/26] Fix --- .../azurefunctions/extensions/base/meta.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/azurefunctions-extensions-base/azurefunctions/extensions/base/meta.py b/azurefunctions-extensions-base/azurefunctions/extensions/base/meta.py index a35aa0b..901a0f6 100644 --- a/azurefunctions-extensions-base/azurefunctions/extensions/base/meta.py +++ b/azurefunctions-extensions-base/azurefunctions/extensions/base/meta.py @@ -89,10 +89,10 @@ def get_raw_bindings(cls, indexed_function, input_types): @classmethod def check_supported_type(cls, subclass: type) -> bool: - if subclass is not None: - return cls.__is_iterable_subclass(subclass) if subclass is not None and inspect.isclass(subclass): return issubclass(subclass, sdkType.SdkType) + if subclass is not None: + return cls.__is_iterable_subclass(subclass) return False @classmethod From 3f73d14d9e23c9d235d6fd4915b8ba59a6632f13 Mon Sep 17 00:00:00 2001 From: Evan Roman Date: Mon, 31 Mar 2025 10:53:05 -0500 Subject: [PATCH 19/26] Refactor --- .../azurefunctions/extensions/base/meta.py | 39 +++++++++++-------- .../bindings/eventhub/eventDataConverter.py | 37 ++++++++++++------ .../tests/test_eventdata.py | 4 ++ 3 files changed, 52 insertions(+), 28 deletions(-) diff --git a/azurefunctions-extensions-base/azurefunctions/extensions/base/meta.py b/azurefunctions-extensions-base/azurefunctions/extensions/base/meta.py index 901a0f6..576dd1c 100644 --- a/azurefunctions-extensions-base/azurefunctions/extensions/base/meta.py +++ b/azurefunctions-extensions-base/azurefunctions/extensions/base/meta.py @@ -88,29 +88,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) - if subclass is not None: - return cls.__is_iterable_subclass(subclass) - 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_subclass(cls, annotation: type) -> bool: - # Get base type from type hint - origin = get_origin(annotation) - if (origin is None - or not issubclass(origin, collections.abc.Iterable)): + 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 not None + and issubclass(base_type, collections.abc.Iterable)): return False - # Extract inner type(s) from iterable - args = get_args(annotation) - if not args: + inner_types = get_args(annotation) + if inner_types is None or len(inner_types) != 1: return False + + inner_type = inner_types[0] - return any( - isinstance(arg, type) - and issubclass(arg, sdkType.SdkType) for arg in args) + 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 diff --git a/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventDataConverter.py b/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventDataConverter.py index f88f165..91b8ebd 100644 --- a/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventDataConverter.py +++ b/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventDataConverter.py @@ -2,7 +2,7 @@ # Licensed under the MIT License. import collections.abc -import inspect +from from typing import Any, Optional, get_args, get_origin from azurefunctions.extensions.base import Datum, InConverter, OutConverter @@ -17,18 +17,33 @@ class EventDataConverter( ): @classmethod def check_input_type_annotation(cls, pytype: type) -> bool: - # Get base type from type hint - origin = get_origin(pytype) - if origin is None or not issubclass(origin, collections.abc.Iterable): - return issubclass(pytype, EventData) - - # Extract inner type(s) from iterable - args = get_args(pytype) - if not args: + if pytype is None: return False - return any(isinstance(arg, type) and issubclass(arg, EventData) - for arg in args) + # The annotation is a class/type (not an object) - not iterable + if (isinstance(pytype, type) + and issubclass(pytype, EventData)): + return True + + # An iterable who only has one inner type and is a subclass of SdkType + return cls.__is_iterable_supported_type(pytype) + + @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 not None + and 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, EventData)) @classmethod def decode(cls, data: Datum, *, trigger_metadata, pytype) -> Optional[Any]: diff --git a/azurefunctions-extensions-bindings-eventhub/tests/test_eventdata.py b/azurefunctions-extensions-bindings-eventhub/tests/test_eventdata.py index e9ff5c2..fa2ac37 100644 --- a/azurefunctions-extensions-bindings-eventhub/tests/test_eventdata.py +++ b/azurefunctions-extensions-bindings-eventhub/tests/test_eventdata.py @@ -45,9 +45,13 @@ class TestEventData(unittest.TestCase): def test_input_type(self): check_input_type = EventDataConverter.check_input_type_annotation self.assertTrue(check_input_type(EventData)) + self.assertTrue(check_input_type(List[EventData])) + self.assertTrue(check_input_type(list[EventData])) + self.assertTrue(check_input_type(tuple[EventData])) self.assertFalse(check_input_type(str)) self.assertFalse(check_input_type(bytes)) self.assertFalse(check_input_type(bytearray)) + self.assertFalse(check_input_type(dict[str, EventData])) def test_input_none(self): result = EventDataConverter.decode( From bddbc9adc51237fa9f59a24bfbf0eb541da82dbc Mon Sep 17 00:00:00 2001 From: Evan Roman Date: Mon, 31 Mar 2025 11:04:09 -0500 Subject: [PATCH 20/26] Fix --- .../azurefunctions/extensions/base/meta.py | 13 ++++++------- .../bindings/eventhub/eventDataConverter.py | 4 ++-- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/azurefunctions-extensions-base/azurefunctions/extensions/base/meta.py b/azurefunctions-extensions-base/azurefunctions/extensions/base/meta.py index 576dd1c..6b7b5b1 100644 --- a/azurefunctions-extensions-base/azurefunctions/extensions/base/meta.py +++ b/azurefunctions-extensions-base/azurefunctions/extensions/base/meta.py @@ -3,7 +3,6 @@ import abc import collections.abc -import inspect import json from typing import Any, Dict, Mapping, Optional, Tuple, Union, get_args, get_origin @@ -91,12 +90,12 @@ def get_raw_bindings(cls, indexed_function, input_types): 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) @@ -104,17 +103,17 @@ def check_supported_type(cls, annotation: type) -> bool: 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 not None - and issubclass(base_type, collections.abc.Iterable)): + 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) + return (isinstance(inner_type, type) and issubclass(inner_type, sdkType.SdkType)) def has_trigger_support(cls) -> bool: diff --git a/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventDataConverter.py b/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventDataConverter.py index 91b8ebd..64b90bd 100644 --- a/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventDataConverter.py +++ b/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventDataConverter.py @@ -32,8 +32,8 @@ def check_input_type_annotation(cls, pytype: type) -> bool: 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 not None - and issubclass(base_type, collections.abc.Iterable)): + if (base_type is None + or not issubclass(base_type, collections.abc.Iterable)): return False inner_types = get_args(annotation) From ab3a7d7de780bb5e22e2ecfed0e5c0bf439e1c24 Mon Sep 17 00:00:00 2001 From: Evan Roman Date: Mon, 31 Mar 2025 11:23:47 -0500 Subject: [PATCH 21/26] Fix version --- azurefunctions-extensions-base/tests/test_meta.py | 12 ++++++++---- .../tests/test_eventdata.py | 8 ++++++-- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/azurefunctions-extensions-base/tests/test_meta.py b/azurefunctions-extensions-base/tests/test_meta.py index d8dd462..2f563e6 100644 --- a/azurefunctions-extensions-base/tests/test_meta.py +++ b/azurefunctions-extensions-base/tests/test_meta.py @@ -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 @@ -149,14 +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.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.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 diff --git a/azurefunctions-extensions-bindings-eventhub/tests/test_eventdata.py b/azurefunctions-extensions-bindings-eventhub/tests/test_eventdata.py index fa2ac37..b0a832c 100644 --- a/azurefunctions-extensions-bindings-eventhub/tests/test_eventdata.py +++ b/azurefunctions-extensions-bindings-eventhub/tests/test_eventdata.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +import sys import unittest from typing import List, Optional @@ -44,10 +45,13 @@ def direction(self) -> int: class TestEventData(unittest.TestCase): def test_input_type(self): check_input_type = EventDataConverter.check_input_type_annotation + + # Generic types are not subscriptable in Python < 3.9 + if sys.version_info >= (3, 9): + self.assertTrue(check_input_type(list[EventData])) + self.assertTrue(check_input_type(tuple[EventData])) self.assertTrue(check_input_type(EventData)) self.assertTrue(check_input_type(List[EventData])) - self.assertTrue(check_input_type(list[EventData])) - self.assertTrue(check_input_type(tuple[EventData])) self.assertFalse(check_input_type(str)) self.assertFalse(check_input_type(bytes)) self.assertFalse(check_input_type(bytearray)) From c883fe7306a2a0a6d36bfffe37e7a926f1b2c90d Mon Sep 17 00:00:00 2001 From: Evan Roman Date: Mon, 31 Mar 2025 11:37:54 -0500 Subject: [PATCH 22/26] More tests --- azurefunctions-extensions-base/tests/test_meta.py | 2 +- .../tests/test_eventdata.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/azurefunctions-extensions-base/tests/test_meta.py b/azurefunctions-extensions-base/tests/test_meta.py index 2f563e6..cb493ff 100644 --- a/azurefunctions-extensions-base/tests/test_meta.py +++ b/azurefunctions-extensions-base/tests/test_meta.py @@ -155,7 +155,7 @@ class MockIndexedFunction: self.assertTrue(registry.check_supported_type(sdkType.SdkType)) self.assertTrue(registry.check_supported_type(List[sdkType.SdkType])) - # Generic types are not subscriptable in Python < 3.9 + # 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])) diff --git a/azurefunctions-extensions-bindings-eventhub/tests/test_eventdata.py b/azurefunctions-extensions-bindings-eventhub/tests/test_eventdata.py index b0a832c..6126313 100644 --- a/azurefunctions-extensions-bindings-eventhub/tests/test_eventdata.py +++ b/azurefunctions-extensions-bindings-eventhub/tests/test_eventdata.py @@ -46,16 +46,18 @@ class TestEventData(unittest.TestCase): def test_input_type(self): check_input_type = EventDataConverter.check_input_type_annotation - # Generic types are not subscriptable in Python < 3.9 + # Generic types are not subscriptable in Python <3.9 if sys.version_info >= (3, 9): self.assertTrue(check_input_type(list[EventData])) self.assertTrue(check_input_type(tuple[EventData])) + self.assertTrue(check_input_type(set[EventData])) + self.assertFalse(check_input_type(dict[str, EventData])) self.assertTrue(check_input_type(EventData)) self.assertTrue(check_input_type(List[EventData])) self.assertFalse(check_input_type(str)) + self.assertFalse(check_input_type("hello")) self.assertFalse(check_input_type(bytes)) self.assertFalse(check_input_type(bytearray)) - self.assertFalse(check_input_type(dict[str, EventData])) def test_input_none(self): result = EventDataConverter.decode( From 521872efcd82025afb9456cc5b13de30ca0faf64 Mon Sep 17 00:00:00 2001 From: Evan Roman Date: Mon, 31 Mar 2025 11:38:18 -0500 Subject: [PATCH 23/26] More tests --- .../tests/test_eventdata.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/azurefunctions-extensions-bindings-eventhub/tests/test_eventdata.py b/azurefunctions-extensions-bindings-eventhub/tests/test_eventdata.py index 6126313..0b6b789 100644 --- a/azurefunctions-extensions-bindings-eventhub/tests/test_eventdata.py +++ b/azurefunctions-extensions-bindings-eventhub/tests/test_eventdata.py @@ -45,6 +45,12 @@ def direction(self) -> int: class TestEventData(unittest.TestCase): def test_input_type(self): check_input_type = EventDataConverter.check_input_type_annotation + self.assertTrue(check_input_type(EventData)) + self.assertTrue(check_input_type(List[EventData])) + self.assertFalse(check_input_type(str)) + self.assertFalse(check_input_type("hello")) + self.assertFalse(check_input_type(bytes)) + self.assertFalse(check_input_type(bytearray)) # Generic types are not subscriptable in Python <3.9 if sys.version_info >= (3, 9): @@ -52,12 +58,6 @@ def test_input_type(self): self.assertTrue(check_input_type(tuple[EventData])) self.assertTrue(check_input_type(set[EventData])) self.assertFalse(check_input_type(dict[str, EventData])) - self.assertTrue(check_input_type(EventData)) - self.assertTrue(check_input_type(List[EventData])) - self.assertFalse(check_input_type(str)) - self.assertFalse(check_input_type("hello")) - self.assertFalse(check_input_type(bytes)) - self.assertFalse(check_input_type(bytearray)) def test_input_none(self): result = EventDataConverter.decode( From c04e320b1df934bc82720806b464749fb49015a6 Mon Sep 17 00:00:00 2001 From: Evan Roman Date: Mon, 31 Mar 2025 12:30:09 -0500 Subject: [PATCH 24/26] Add batch sample --- .../README.md | 17 +++++++++++--- .../bindings/eventhub/eventDataConverter.py | 1 - .../function_app.py | 22 ++++++++++++++----- 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/azurefunctions-extensions-bindings-eventhub/README.md b/azurefunctions-extensions-bindings-eventhub/README.md index 39b4d2d..7df067f 100644 --- a/azurefunctions-extensions-bindings-eventhub/README.md +++ b/azurefunctions-extensions-bindings-eventhub/README.md @@ -54,13 +54,24 @@ import azurefunctions.extensions.bindings.eventhub as eh app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) @app.event_hub_message_trigger( - arg_name="eh_data", event_hub_name="EVENTHUB_NAME", connection="AzureWebJobsStorage" + arg_name="event", event_hub_name="EVENTHUB_NAME", connection="AzureWebJobsStorage" ) -def eventhub_trigger(eh_data: eh.EventData): +def eventhub_trigger(event: eh.EventData): logging.info( "Python EventHub trigger processed an event %s", - eh_data.body_as_str() + event.body_as_str() ) + + +@app.event_hub_message_trigger( + arg_name="events", event_hub_name="EVENTHUB_NAME", connection="AzureWebJobsStorage", cardinality="many" +) +def eventhub_trigger(events: List[eh.EventData]): + for event in events: + logging.info( + "Python EventHub trigger processed an event %s", + event.body_as_str() + ) ``` ## Troubleshooting diff --git a/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventDataConverter.py b/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventDataConverter.py index 64b90bd..5f15b70 100644 --- a/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventDataConverter.py +++ b/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventDataConverter.py @@ -2,7 +2,6 @@ # Licensed under the MIT License. import collections.abc -from from typing import Any, Optional, get_args, get_origin from azurefunctions.extensions.base import Datum, InConverter, OutConverter diff --git a/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventdata/function_app.py b/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventdata/function_app.py index 8006311..c9cc972 100644 --- a/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventdata/function_app.py +++ b/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventdata/function_app.py @@ -5,6 +5,8 @@ # -------------------------------------------------------------------------- import logging +from typing import List + import azure.functions as func import azurefunctions.extensions.bindings.eventhub as eh @@ -28,11 +30,21 @@ @app.event_hub_message_trigger( - arg_name="eh_data", event_hub_name="EVENTHUB_NAME", connection="AzureWebJobsStorage" -) -def eventhub_trigger(eh_data: eh.EventData): + arg_name="event", event_hub_name="EVENTHUB_NAME", connection="AzureWebJobsStorage" +) +def eventhub_trigger(event: eh.EventData): logging.info( "Python EventHub trigger processed an event %s", - eh_data.body_as_str() + event.body_as_str() ) - \ No newline at end of file + + +@app.event_hub_message_trigger( + arg_name="events", event_hub_name="EVENTHUB_NAME", connection="AzureWebJobsStorage", cardinality="many" +) +def eventhub_trigger(events: List[eh.EventData]): + for event in events: + logging.info( + "Python EventHub trigger processed an event %s", + event.body_as_str() + ) From 5a7c4b28a5d9be29187b9275aca0376139ca31bd Mon Sep 17 00:00:00 2001 From: Evan Roman Date: Tue, 1 Apr 2025 01:20:43 -0500 Subject: [PATCH 25/26] Rm 39 --- .../tests/test_eventdata.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/azurefunctions-extensions-bindings-eventhub/tests/test_eventdata.py b/azurefunctions-extensions-bindings-eventhub/tests/test_eventdata.py index 0b6b789..b4fcae8 100644 --- a/azurefunctions-extensions-bindings-eventhub/tests/test_eventdata.py +++ b/azurefunctions-extensions-bindings-eventhub/tests/test_eventdata.py @@ -46,18 +46,15 @@ class TestEventData(unittest.TestCase): def test_input_type(self): check_input_type = EventDataConverter.check_input_type_annotation self.assertTrue(check_input_type(EventData)) - self.assertTrue(check_input_type(List[EventData])) self.assertFalse(check_input_type(str)) self.assertFalse(check_input_type("hello")) self.assertFalse(check_input_type(bytes)) self.assertFalse(check_input_type(bytearray)) - - # Generic types are not subscriptable in Python <3.9 - if sys.version_info >= (3, 9): - self.assertTrue(check_input_type(list[EventData])) - self.assertTrue(check_input_type(tuple[EventData])) - self.assertTrue(check_input_type(set[EventData])) - self.assertFalse(check_input_type(dict[str, EventData])) + self.assertTrue(check_input_type(List[EventData])) + self.assertTrue(check_input_type(list[EventData])) + self.assertTrue(check_input_type(tuple[EventData])) + self.assertTrue(check_input_type(set[EventData])) + self.assertFalse(check_input_type(dict[str, EventData])) def test_input_none(self): result = EventDataConverter.decode( From 76c195082caefc3b09ef55266aaedd7bad1217e9 Mon Sep 17 00:00:00 2001 From: Evan Roman Date: Tue, 1 Apr 2025 14:57:04 -0500 Subject: [PATCH 26/26] Lint --- .../azurefunctions/extensions/base/meta.py | 4 ++-- .../azurefunctions/extensions/bindings/eventhub/eventData.py | 4 ++-- .../extensions/bindings/eventhub/eventDataConverter.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/azurefunctions-extensions-base/azurefunctions/extensions/base/meta.py b/azurefunctions-extensions-base/azurefunctions/extensions/base/meta.py index 6b7b5b1..a771a9f 100644 --- a/azurefunctions-extensions-base/azurefunctions/extensions/base/meta.py +++ b/azurefunctions-extensions-base/azurefunctions/extensions/base/meta.py @@ -97,10 +97,10 @@ def check_supported_type(cls, annotation: type) -> bool: return True # An iterable who only has one inner type and is a subclass of SdkType - return cls.__is_iterable_supported_type(annotation) + return cls._is_iterable_supported_type(annotation) @classmethod - def __is_iterable_supported_type(cls, annotation: type) -> bool: + 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 diff --git a/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventData.py b/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventData.py index 5510ead..c44ef07 100644 --- a/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventData.py +++ b/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventData.py @@ -22,9 +22,9 @@ def __init__(self, *, data: Datum) -> None: self._source = data.source self._content_type = data.content_type self._content = data.content - self.decoded_message = self.__get_eventhub_content(self._content) + self.decoded_message = self._get_eventhub_content(self._content) - def __get_eventhub_content(self, content): + def _get_eventhub_content(self, content): """ When receiving the EventBindingData, the content field is in the form of bytes. This content must be decoded in order to construct an EventData object from the azure.eventhub SDK. diff --git a/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventDataConverter.py b/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventDataConverter.py index 5f15b70..c01bc6c 100644 --- a/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventDataConverter.py +++ b/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventDataConverter.py @@ -25,10 +25,10 @@ def check_input_type_annotation(cls, pytype: type) -> bool: return True # An iterable who only has one inner type and is a subclass of SdkType - return cls.__is_iterable_supported_type(pytype) + return cls._is_iterable_supported_type(pytype) @classmethod - def __is_iterable_supported_type(cls, annotation: type) -> bool: + 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