diff --git a/azurefunctions-extensions-base/azurefunctions/extensions/base/meta.py b/azurefunctions-extensions-base/azurefunctions/extensions/base/meta.py index a701ad2..a771a9f 100644 --- a/azurefunctions-extensions-base/azurefunctions/extensions/base/meta.py +++ b/azurefunctions-extensions-base/azurefunctions/extensions/base/meta.py @@ -2,9 +2,9 @@ # Licensed under the MIT License. import abc -import inspect +import collections.abc import json -from typing import Any, Dict, Mapping, Optional, Tuple, Union +from typing import Any, Dict, Mapping, Optional, Tuple, Union, get_args, get_origin from . import sdkType, utils @@ -87,10 +87,34 @@ def get_raw_bindings(cls, indexed_function, input_types): return utils.get_raw_bindings(indexed_function, input_types) @classmethod - def check_supported_type(cls, subclass: type) -> bool: - if subclass is not None and inspect.isclass(subclass): - return issubclass(subclass, sdkType.SdkType) - return False + def check_supported_type(cls, annotation: type) -> bool: + if annotation is None: + return False + + # The annotation is a class/type (not an object) - not iterable + if (isinstance(annotation, type) + and issubclass(annotation, sdkType.SdkType)): + return True + + # An iterable who only has one inner type and is a subclass of SdkType + return cls._is_iterable_supported_type(annotation) + + @classmethod + def _is_iterable_supported_type(cls, annotation: type) -> bool: + # Check base type from type hint. Ex: List from List[SdkType] + base_type = get_origin(annotation) + if (base_type is None + or not issubclass(base_type, collections.abc.Iterable)): + return False + + inner_types = get_args(annotation) + if inner_types is None or len(inner_types) != 1: + return False + + inner_type = inner_types[0] + + return (isinstance(inner_type, type) + and issubclass(inner_type, sdkType.SdkType)) def has_trigger_support(cls) -> bool: return cls._trigger is not None # type: ignore @@ -110,7 +134,8 @@ def _decode_typed_data( return None data_type = data.type - if data_type == "model_binding_data": + if (data_type == "model_binding_data" + or data_type == "collection_model_binding_data"): result = data.value elif data_type is None: return None 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-base/tests/test_meta.py b/azurefunctions-extensions-base/tests/test_meta.py index a322058..cb493ff 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,10 +150,17 @@ class MockIndexedFunction: self.assertEqual(registry.get_raw_bindings(MockIndexedFunction, []), ([], {})) self.assertFalse(registry.check_supported_type(None)) + self.assertFalse(registry.has_trigger_support(MockIndexedFunction)) self.assertFalse(registry.check_supported_type("hello")) self.assertTrue(registry.check_supported_type(sdkType.SdkType)) + self.assertTrue(registry.check_supported_type(List[sdkType.SdkType])) - self.assertFalse(registry.has_trigger_support(MockIndexedFunction)) + # Generic types are not subscriptable in Python <3.9 + if sys.version_info >= (3, 9): + self.assertTrue(registry.check_supported_type(list[sdkType.SdkType])) + self.assertTrue(registry.check_supported_type(tuple[sdkType.SdkType])) + self.assertTrue(registry.check_supported_type(set[sdkType.SdkType])) + self.assertFalse(registry.check_supported_type(dict[str, sdkType.SdkType])) def test_decode_typed_data(self): # Case 1: data is None @@ -166,32 +174,38 @@ def test_decode_typed_data(self): meta._BaseConverter._decode_typed_data(datum_mbd, python_type=str), "{}" ) - # Case 3: data.type is None + # Case 3: data.type is collection_model_binding_data + datum_cmbd = meta.Datum(value="{}", type="collection_model_binding_data") + self.assertEqual( + meta._BaseConverter._decode_typed_data(datum_cmbd, python_type=str), "{}" + ) + + # Case 4: data.type is None datum_none = meta.Datum(value="{}", type=None) self.assertIsNone( meta._BaseConverter._decode_typed_data(datum_none, python_type=str) ) - # Case 4: data.type is unsupported + # Case 5: data.type is unsupported datum_unsupp = meta.Datum(value="{}", type=dict) with self.assertRaises(ValueError): meta._BaseConverter._decode_typed_data(datum_unsupp, python_type=str) - # Case 5: can't coerce + # Case 6: can't coerce datum_coerce_fail = meta.Datum(value="{}", type="model_binding_data") with self.assertRaises(ValueError): meta._BaseConverter._decode_typed_data( datum_coerce_fail, python_type=(tuple, list, dict) ) - # Case 6: attempt coerce & fail + # Case 7: attempt coerce & fail datum_attempt_coerce = meta.Datum(value=1, type="model_binding_data") with self.assertRaises(ValueError): meta._BaseConverter._decode_typed_data( datum_attempt_coerce, python_type=dict ) - # Case 7: attempt to coerce and pass + # Case 8: attempt to coerce and pass datum_coerce_pass = meta.Datum(value=1, type="model_binding_data") self.assertEqual( meta._BaseConverter._decode_typed_data(datum_coerce_pass, python_type=str), diff --git a/azurefunctions-extensions-bindings-blob/README.md b/azurefunctions-extensions-bindings-blob/README.md index 659f440..447bd21 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 @@ -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") @@ -85,19 +87,19 @@ This list can be used for reference to catch thrown exceptions. To get the speci ### More sample code -Get started with our [Blob samples](hhttps://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-bindings-blob/samples). +Get started with our [Blob samples](https://github.com/Azure/azure-functions-python-extensions/tree/dev/azurefunctions-extensions-bindings-blob/samples). Several samples are available in this GitHub repository. These samples provide example code for additional scenarios commonly encountered while working with Storage Blobs: -* [blob_samples_blobclient](https://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-bindings-blob/samples/blob_samples_blobclient) - Examples for using the BlobClient type: +* [blob_samples_blobclient](https://github.com/Azure/azure-functions-python-extensions/tree/dev/azurefunctions-extensions-bindings-blob/samples/blob_samples_blobclient) - Examples for using the BlobClient type: * From BlobTrigger * From BlobInput -* [blob_samples_containerclient](https://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-bindings-blob/samples/blob_samples_containerclient) - Examples for using the ContainerClient type: +* [blob_samples_containerclient](https://github.com/Azure/azure-functions-python-extensions/tree/dev/azurefunctions-extensions-bindings-blob/samples/blob_samples_containerclient) - Examples for using the ContainerClient type: * From BlobTrigger * From BlobInput -* [blob_samples_storagestreamdownloader](https://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-bindings-blob/samples/blob_samples_storagestreamdownloader) - Examples for using the StorageStreamDownloader type: +* [blob_samples_storagestreamdownloader](https://github.com/Azure/azure-functions-python-extensions/tree/dev/azurefunctions-extensions-bindings-blob/samples/blob_samples_storagestreamdownloader) - Examples for using the StorageStreamDownloader type: * From BlobTrigger * From BlobInput 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 bec7403..b985d84 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 @@ -11,7 +10,7 @@ class BlobClient(SdkType): - def __init__(self, *, data: Union[bytes, Datum]) -> None: + def __init__(self, *, data: Datum) -> None: # model_binding_data properties self._data = data self._using_managed_identity = False 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/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-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/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..7df067f --- /dev/null +++ b/azurefunctions-extensions-bindings-eventhub/README.md @@ -0,0 +1,103 @@ +# Azure Functions Extensions Bindings EventHub library for Python +This library allows an EventHub Trigger binding in Python Function Apps to recognize and bind to the types from the +Azure EventHub sdk (EventData). + +EventHub types can be generated from: + +[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](https://github.com/Azure/azure-functions-python-extensions/tree/dev/azurefunctions-extensions-bindings-eventhub/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 EventHub library for Python with pip: + +```bash +pip install azurefunctions-extensions-bindings-eventhub +``` + +### 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 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 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 +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="event", event_hub_name="EVENTHUB_NAME", connection="AzureWebJobsStorage" +) +def eventhub_trigger(event: eh.EventData): + logging.info( + "Python EventHub trigger processed an event %s", + 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 +### 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 [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_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 +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. + +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..7117144 --- /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 .eventData import EventData +from .eventDataConverter import EventDataConverter + +__all__ = [ + "EventData", + "EventDataConverter", +] + +__version__ = "1.0.0b1" diff --git a/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventData.py b/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventData.py new file mode 100644 index 0000000..c44ef07 --- /dev/null +++ b/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventData.py @@ -0,0 +1,52 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from typing import Optional +import uamqp + +from azure.eventhub import EventData +from azurefunctions.extensions.base import Datum, SdkType + + +class EventData(SdkType): + def __init__(self, *, data: 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): + """ + 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: + 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) -> 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, enqueued_time, and more. + """ + # https://github.com/Azure/azure-sdk-for-python/issues/39711 + if self.decoded_message: + return EventData._from_message(self.decoded_message) + + 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 new file mode 100644 index 0000000..c01bc6c --- /dev/null +++ b/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventDataConverter.py @@ -0,0 +1,74 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import collections.abc +from typing import Any, Optional, get_args, get_origin + +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: + if pytype is None: + return False + + # 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 None + or not issubclass(base_type, collections.abc.Iterable)): + return False + + inner_types = get_args(annotation) + if inner_types is None or len(inner_types) != 1: + return False + + inner_type = inner_types[0] + + return (isinstance(inner_type, type) + and issubclass(inner_type, 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/pyproject.toml b/azurefunctions-extensions-bindings-eventhub/pyproject.toml new file mode 100644 index 0000000..cf42ee8 --- /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', + 'Programming Language :: Python :: 3.12', + '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' + ] + +[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..2fd1d7b --- /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) + +* [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 +* 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](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/samples/eventhub_samples_eventdata/function_app.py b/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventdata/function_app.py new file mode 100644 index 0000000..c9cc972 --- /dev/null +++ b/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventdata/function_app.py @@ -0,0 +1,50 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- + +import logging +from typing import List + +import azure.functions as func +import azurefunctions.extensions.bindings.eventhub as eh + +app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) + +""" +FOLDER: eventhub_samples_eventdata +DESCRIPTION: + 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 + + 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="event", event_hub_name="EVENTHUB_NAME", connection="AzureWebJobsStorage" +) +def eventhub_trigger(event: eh.EventData): + logging.info( + "Python EventHub trigger processed an event %s", + 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() + ) diff --git a/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventdata/host.json b/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventdata/host.json new file mode 100644 index 0000000..9df9136 --- /dev/null +++ b/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventdata/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_eventdata/local.settings.json b/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventdata/local.settings.json new file mode 100644 index 0000000..6dc40bb --- /dev/null +++ b/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventdata/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/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventdata/requirements.txt b/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventdata/requirements.txt new file mode 100644 index 0000000..6b7c0aa --- /dev/null +++ b/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventdata/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..3a41690 --- /dev/null +++ b/azurefunctions-extensions-bindings-eventhub/tests/__init__.py @@ -0,0 +1,17 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os.path +import sys +import unittest + + +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_eventdata.py b/azurefunctions-extensions-bindings-eventhub/tests/test_eventdata.py new file mode 100644 index 0000000..b4fcae8 --- /dev/null +++ b/azurefunctions-extensions-bindings-eventhub/tests/test_eventdata.py @@ -0,0 +1,147 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import sys +import unittest +from typing import List, Optional + +from azure.eventhub import EventData as EventDataSdk +from azurefunctions.extensions.base import Datum + +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" + +# 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 + + @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 MockCMBD: + def __init__(self, model_binding_data_list: List[MockMBD]): + self.model_binding_data = model_binding_data_list + + @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 TestEventData(unittest.TestCase): + def test_input_type(self): + 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("hello")) + self.assertFalse(check_input_type(bytes)) + self.assertFalse(check_input_type(bytearray)) + 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( + data=None, trigger_metadata=None, pytype=EventData + ) + self.assertIsNone(result) + + datum: Datum = Datum(value=b"string_content", type=None) + 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): + EventDataConverter.decode( + data=datum, trigger_metadata=None, pytype=EventData + ) + + def test_input_empty_mbd(self): + datum: Datum = Datum(value={}, type="model_binding_data") + 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_mbd(self): + sample_mbd = MockMBD( + version="1.0", + source="AzureEventHubsEventData", + content_type="application/octet-stream", + content = EVENTHUB_SAMPLE_CONTENT + ) + + datum: Datum = Datum(value=sample_mbd, type="model_binding_data") + result: EventData = EventDataConverter.decode( + data=datum, trigger_metadata=None, pytype=EventData + ) + + self.assertIsNotNone(result) + self.assertIsInstance(result, EventDataSdk) + + sdk_result = EventData(data=datum.value).get_sdk_type() + + self.assertIsNotNone(sdk_result) + self.assertIsInstance(sdk_result, EventDataSdk) + + 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=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, EventDataSdk) + + sdk_result = EventData(data=datum.value).get_sdk_type() + + self.assertIsNotNone(sdk_result) + self.assertIsInstance(sdk_result, EventDataSdk) + + def test_input_invalid_pytype(self): + sample_mbd = MockMBD( + version="1.0", + source="AzureEventHubsEventData", + content_type="application/octet-stream", + content = EVENTHUB_SAMPLE_CONTENT + ) + + datum: Datum = Datum(value=sample_mbd, type="model_binding_data") + result: EventData = EventDataConverter.decode( + data=datum, trigger_metadata=None, pytype="str" + ) + + self.assertIsNone(result) diff --git a/azurefunctions-extensions-http-fastapi/README.md b/azurefunctions-extensions-http-fastapi/README.md index 9f64bb6..b0200ab 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](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/dev/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. ## 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-http-fastapi/tests/__init__.py b/azurefunctions-extensions-http-fastapi/tests/__init__.py index 528a01b..3a41690 100644 --- a/azurefunctions-extensions-http-fastapi/tests/__init__.py +++ b/azurefunctions-extensions-http-fastapi/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/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/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/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..5a5d098 --- /dev/null +++ b/eng/templates/official/jobs/eventhub-unit-tests.yml @@ -0,0 +1,29 @@ +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) + 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