From ae8debff66feeafc06042473989e85746b709c63 Mon Sep 17 00:00:00 2001 From: Evan Roman Date: Fri, 11 Apr 2025 15:01:36 -0500 Subject: [PATCH 01/12] Init --- .../CHANGELOG.md | 0 .../LICENSE | 21 ++ .../MANIFEST.in | 3 + .../README.md | 109 ++++++ .../azurefunctions/__init__.py | 1 + .../azurefunctions/extensions/__init__.py | 1 + .../extensions/bindings/__init__.py | 1 + .../extensions/bindings/cosmos/__init__.py | 16 + .../bindings/cosmos/containerProxy.py | 60 ++++ .../bindings/cosmos/cosmosClient.py | 60 ++++ .../bindings/cosmos/cosmosClientConverter.py | 47 +++ .../bindings/cosmos/databaseProxy.py | 59 ++++ .../extensions/bindings/cosmos/utils.py | 37 ++ .../pyproject.toml | 49 +++ .../samples/README.md | 68 ++++ .../function_app.py | 46 +++ .../cosmos_samples_containerproxy/host.json | 15 + .../local.settings.json | 8 + .../requirements.txt | 6 + .../function_app.py | 52 +++ .../cosmos_samples_cosmosclient/host.json | 15 + .../local.settings.json | 8 + .../requirements.txt | 6 + .../function_app.py | 52 +++ .../cosmos_samples_databaseproxy/host.json | 15 + .../local.settings.json | 8 + .../requirements.txt | 6 + .../tests/__init__.py | 20 ++ .../tests/test_containerproxy.py | 314 +++++++++++++++++ .../tests/test_cosmosclient.py | 288 ++++++++++++++++ .../tests/test_databaseproxy.py | 319 ++++++++++++++++++ eng/ci/ci-cosmos-tests.yml | 35 ++ eng/ci/official-build.yml | 4 + eng/templates/jobs/build.yml | 3 + .../official/jobs/build-artifacts.yml | 3 + .../official/jobs/cosmos-unit-tests.yml | 30 ++ 36 files changed, 1785 insertions(+) create mode 100644 azurefunctions-extensions-bindings-cosmos/CHANGELOG.md create mode 100644 azurefunctions-extensions-bindings-cosmos/LICENSE create mode 100644 azurefunctions-extensions-bindings-cosmos/MANIFEST.in create mode 100644 azurefunctions-extensions-bindings-cosmos/README.md create mode 100644 azurefunctions-extensions-bindings-cosmos/azurefunctions/__init__.py create mode 100644 azurefunctions-extensions-bindings-cosmos/azurefunctions/extensions/__init__.py create mode 100644 azurefunctions-extensions-bindings-cosmos/azurefunctions/extensions/bindings/__init__.py create mode 100644 azurefunctions-extensions-bindings-cosmos/azurefunctions/extensions/bindings/cosmos/__init__.py create mode 100644 azurefunctions-extensions-bindings-cosmos/azurefunctions/extensions/bindings/cosmos/containerProxy.py create mode 100644 azurefunctions-extensions-bindings-cosmos/azurefunctions/extensions/bindings/cosmos/cosmosClient.py create mode 100644 azurefunctions-extensions-bindings-cosmos/azurefunctions/extensions/bindings/cosmos/cosmosClientConverter.py create mode 100644 azurefunctions-extensions-bindings-cosmos/azurefunctions/extensions/bindings/cosmos/databaseProxy.py create mode 100644 azurefunctions-extensions-bindings-cosmos/azurefunctions/extensions/bindings/cosmos/utils.py create mode 100644 azurefunctions-extensions-bindings-cosmos/pyproject.toml create mode 100644 azurefunctions-extensions-bindings-cosmos/samples/README.md create mode 100644 azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_containerproxy/function_app.py create mode 100644 azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_containerproxy/host.json create mode 100644 azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_containerproxy/local.settings.json create mode 100644 azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_containerproxy/requirements.txt create mode 100644 azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_cosmosclient/function_app.py create mode 100644 azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_cosmosclient/host.json create mode 100644 azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_cosmosclient/local.settings.json create mode 100644 azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_cosmosclient/requirements.txt create mode 100644 azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_databaseproxy/function_app.py create mode 100644 azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_databaseproxy/host.json create mode 100644 azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_databaseproxy/local.settings.json create mode 100644 azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_databaseproxy/requirements.txt create mode 100644 azurefunctions-extensions-bindings-cosmos/tests/__init__.py create mode 100644 azurefunctions-extensions-bindings-cosmos/tests/test_containerproxy.py create mode 100644 azurefunctions-extensions-bindings-cosmos/tests/test_cosmosclient.py create mode 100644 azurefunctions-extensions-bindings-cosmos/tests/test_databaseproxy.py create mode 100644 eng/ci/ci-cosmos-tests.yml create mode 100644 eng/templates/official/jobs/cosmos-unit-tests.yml diff --git a/azurefunctions-extensions-bindings-cosmos/CHANGELOG.md b/azurefunctions-extensions-bindings-cosmos/CHANGELOG.md new file mode 100644 index 0000000..e69de29 diff --git a/azurefunctions-extensions-bindings-cosmos/LICENSE b/azurefunctions-extensions-bindings-cosmos/LICENSE new file mode 100644 index 0000000..63447fd --- /dev/null +++ b/azurefunctions-extensions-bindings-cosmos/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-cosmos/MANIFEST.in b/azurefunctions-extensions-bindings-cosmos/MANIFEST.in new file mode 100644 index 0000000..e1ae5ad --- /dev/null +++ b/azurefunctions-extensions-bindings-cosmos/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-cosmos/README.md b/azurefunctions-extensions-bindings-cosmos/README.md new file mode 100644 index 0000000..d38e335 --- /dev/null +++ b/azurefunctions-extensions-bindings-cosmos/README.md @@ -0,0 +1,109 @@ +# Azure Functions Extensions Bindings Cosmos library for Python +This library allows Cosmos Input bindings in Python Function Apps to recognize and bind to client types from the +Azure Cosmos sdk. + +Cosmos client types can be generated from: + +* Cosmos Input + +[Source code](https://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-bindings-cosmos) +[Package (PyPi)](https://pypi.org/project/azurefunctions-extensions-bindings-cosmos/) +| API reference documentation +| Product documentation +| [Samples](hhttps://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-bindings-cosmos/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 Cosmos library for Python with pip: + +```bash +pip install azurefunctions-extensions-bindings-cosmos +``` + +### 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 Cosmos library for Python allows you to create a function app with +Cosmos Input and define the type as a CosmosClient, DatabaseProxy, or ContainerProxy. 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 Cosmos 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 [Cosmos samples](hhttps://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-bindings-cosmos/samples). + +Several samples are available in this GitHub repository. These samples provide example code for additional scenarios commonly encountered while working with Cosmos: + +* [cosmos_samples_cosmosclient](https://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-binding-cosmos/samples/cosmos_samples_cosmosclient) - Examples for using the CosmosClient type: + * From CosmosInput + +* [cosmos_samples_databaseproxy](https://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_databaseproxy) - Examples for using the DatabaseProxy type: + * From CosmosInput + +* [cosmos_samples_containerclient](https://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_containerproxy) - Examples for using the ContainerProxy type: + * From CosmosInput + +### Additional documentation +For more information on the Azure Cosmos SDK, see the [Azure Cosmos DB documentation](https://learn.microsoft.com/en-us/azure/cosmos-db/) on learn.microsoft.com +and the [Azure Cosmos DB README](https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/cosmos/azure-cosmos). + +## 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-cosmos/azurefunctions/__init__.py b/azurefunctions-extensions-bindings-cosmos/azurefunctions/__init__.py new file mode 100644 index 0000000..8db66d3 --- /dev/null +++ b/azurefunctions-extensions-bindings-cosmos/azurefunctions/__init__.py @@ -0,0 +1 @@ +__path__ = __import__("pkgutil").extend_path(__path__, __name__) diff --git a/azurefunctions-extensions-bindings-cosmos/azurefunctions/extensions/__init__.py b/azurefunctions-extensions-bindings-cosmos/azurefunctions/extensions/__init__.py new file mode 100644 index 0000000..8db66d3 --- /dev/null +++ b/azurefunctions-extensions-bindings-cosmos/azurefunctions/extensions/__init__.py @@ -0,0 +1 @@ +__path__ = __import__("pkgutil").extend_path(__path__, __name__) diff --git a/azurefunctions-extensions-bindings-cosmos/azurefunctions/extensions/bindings/__init__.py b/azurefunctions-extensions-bindings-cosmos/azurefunctions/extensions/bindings/__init__.py new file mode 100644 index 0000000..8db66d3 --- /dev/null +++ b/azurefunctions-extensions-bindings-cosmos/azurefunctions/extensions/bindings/__init__.py @@ -0,0 +1 @@ +__path__ = __import__("pkgutil").extend_path(__path__, __name__) diff --git a/azurefunctions-extensions-bindings-cosmos/azurefunctions/extensions/bindings/cosmos/__init__.py b/azurefunctions-extensions-bindings-cosmos/azurefunctions/extensions/bindings/cosmos/__init__.py new file mode 100644 index 0000000..ecc2595 --- /dev/null +++ b/azurefunctions-extensions-bindings-cosmos/azurefunctions/extensions/bindings/cosmos/__init__.py @@ -0,0 +1,16 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from .cosmosClient import CosmosClient +from .databaseProxy import DatabaseProxy +from .containerProxy import ContainerProxy +from .cosmosClientConverter import CosmosClientConverter + +__all__ = [ + "CosmosClient", + "DatabaseProxy", + "ContainerProxy", + "CosmosClientConverter" +] + +__version__ = "1.0.0b1" diff --git a/azurefunctions-extensions-bindings-cosmos/azurefunctions/extensions/bindings/cosmos/containerProxy.py b/azurefunctions-extensions-bindings-cosmos/azurefunctions/extensions/bindings/cosmos/containerProxy.py new file mode 100644 index 0000000..67ad604 --- /dev/null +++ b/azurefunctions-extensions-bindings-cosmos/azurefunctions/extensions/bindings/cosmos/containerProxy.py @@ -0,0 +1,60 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import json + +from azure.identity import DefaultAzureCredential +from azure.cosmos import CosmosClient as CosmosClientSdk, ContainerProxy as ContainerProxySdk +from azurefunctions.extensions.base import Datum, SdkType +from .utils import get_connection_string, using_managed_identity + + +class ContainerProxy(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._database_name = None + self._container_name = None + self._connection = None + self._using_managed_identity = False + self._partition_key = None + self._sql_query = None + self._preferred_locations = None + if self._data: + self._version = data.version + self._source = data.source + self._content_type = data.content_type + content_json = json.loads(data.content) + self._database_name = content_json.get("DatabaseName") + self._container_name = content_json.get("ContainerName") + self._connection = get_connection_string(content_json.get("Connection")) + self._using_managed_identity = using_managed_identity( + content_json.get("Connection") + ) + self._preferred_locations = content_json.get("PreferredLocations") + + def get_sdk_type(self) -> ContainerProxySdk: + """ + When using Managed Identity, the only way to create a BlobClient is + through a BlobServiceClient. There are two ways to create a + BlobServiceClient: + 1. Through the constructor: this is the only option when using Managed Identity + 2. Through from_connection_string: this is the only option when not using Managed Identity + + We track if Managed Identity is being used through a flag. + """ + if self._data: + cosmos_client = ( + CosmosClientSdk( + url=self._connection, credential=DefaultAzureCredential() + ) + if self._using_managed_identity + else CosmosClientSdk.from_connection_string(self._connection) + ) + db_client = cosmos_client.get_database_client(self._database_name) + return db_client.get_container_client(self._container_name) + else: + return None diff --git a/azurefunctions-extensions-bindings-cosmos/azurefunctions/extensions/bindings/cosmos/cosmosClient.py b/azurefunctions-extensions-bindings-cosmos/azurefunctions/extensions/bindings/cosmos/cosmosClient.py new file mode 100644 index 0000000..518d796 --- /dev/null +++ b/azurefunctions-extensions-bindings-cosmos/azurefunctions/extensions/bindings/cosmos/cosmosClient.py @@ -0,0 +1,60 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import json + +from azure.identity import DefaultAzureCredential +from azure.cosmos import CosmosClient as CosmosClientSdk +from azurefunctions.extensions.base import Datum, SdkType +from .utils import get_connection_string, using_managed_identity + + +class CosmosClient(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._database_name = None + self._container_name = None + self._connection = None + self._using_managed_identity = False + self._id = None + self._partition_key = None + self._sql_query = None + self._preferred_locations = None + if self._data: + self._version = data.version + self._source = data.source + self._content_type = data.content_type + content_json = json.loads(data.content) + self._database_name = content_json.get("DatabaseName") + self._container_name = content_json.get("ContainerName") + self._connection = get_connection_string(content_json.get("Connection")) + self._using_managed_identity = using_managed_identity( + content_json.get("Connection") + ) + self._preferred_locations = content_json.get("PreferredLocations") + + def get_sdk_type(self) -> CosmosClientSdk: + """ + When using Managed Identity, the only way to create a BlobClient is + through a BlobServiceClient. There are two ways to create a + BlobServiceClient: + 1. Through the constructor: this is the only option when using Managed Identity + 2. Through from_connection_string: this is the only option when not using Managed Identity + + We track if Managed Identity is being used through a flag. + """ + if self._data: + cosmos_client = ( + CosmosClientSdk( + url=self._connection, credential=DefaultAzureCredential() + ) + if self._using_managed_identity + else CosmosClientSdk.from_connection_string(self._connection) + ) + return cosmos_client + else: + return None diff --git a/azurefunctions-extensions-bindings-cosmos/azurefunctions/extensions/bindings/cosmos/cosmosClientConverter.py b/azurefunctions-extensions-bindings-cosmos/azurefunctions/extensions/bindings/cosmos/cosmosClientConverter.py new file mode 100644 index 0000000..fa3e8cb --- /dev/null +++ b/azurefunctions-extensions-bindings-cosmos/azurefunctions/extensions/bindings/cosmos/cosmosClientConverter.py @@ -0,0 +1,47 @@ +# 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 .cosmosClient import CosmosClient +from .databaseProxy import DatabaseProxy +from .containerProxy import ContainerProxy + + +class CosmosClientConverter( + InConverter, + OutConverter, + binding="cosmosDB" +): + @classmethod + def check_input_type_annotation(cls, pytype: type) -> bool: + return issubclass( + pytype, (CosmosClient, DatabaseProxy, ContainerProxy) + ) + + @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 "Cosmos" binding ' + f": {data_type!r}" + ) + + # Determines which sdk type to return based on pytype + if pytype == CosmosClient: + return CosmosClient(data=data).get_sdk_type() + elif pytype == DatabaseProxy: + return DatabaseProxy(data=data).get_sdk_type() + elif pytype == ContainerProxy: + return ContainerProxy(data=data).get_sdk_type() + else: + return None diff --git a/azurefunctions-extensions-bindings-cosmos/azurefunctions/extensions/bindings/cosmos/databaseProxy.py b/azurefunctions-extensions-bindings-cosmos/azurefunctions/extensions/bindings/cosmos/databaseProxy.py new file mode 100644 index 0000000..816fd19 --- /dev/null +++ b/azurefunctions-extensions-bindings-cosmos/azurefunctions/extensions/bindings/cosmos/databaseProxy.py @@ -0,0 +1,59 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import json + +from azure.identity import DefaultAzureCredential +from azure.cosmos import CosmosClient as CosmosClientSdk, DatabaseProxy as DatabaseProxySdk +from azurefunctions.extensions.base import Datum, SdkType +from .utils import get_connection_string, using_managed_identity + + +class DatabaseProxy(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._database_name = None + self._container_name = None + self._connection = None + self._using_managed_identity = False + self._partition_key = None + self._sql_query = None + self._preferred_locations = None + if self._data: + self._version = data.version + self._source = data.source + self._content_type = data.content_type + content_json = json.loads(data.content) + self._database_name = content_json.get("DatabaseName") + self._container_name = content_json.get("ContainerName") + self._connection = get_connection_string(content_json.get("Connection")) + self._using_managed_identity = using_managed_identity( + content_json.get("Connection") + ) + self._preferred_locations = content_json.get("PreferredLocations") + + def get_sdk_type(self) -> DatabaseProxySdk: + """ + When using Managed Identity, the only way to create a BlobClient is + through a BlobServiceClient. There are two ways to create a + BlobServiceClient: + 1. Through the constructor: this is the only option when using Managed Identity + 2. Through from_connection_string: this is the only option when not using Managed Identity + + We track if Managed Identity is being used through a flag. + """ + if self._data: + cosmos_client = ( + CosmosClientSdk( + url=self._connection, credential=DefaultAzureCredential() + ) + if self._using_managed_identity + else CosmosClientSdk.from_connection_string(self._connection) + ) + return cosmos_client.get_database_client(self._database_name) + else: + return None diff --git a/azurefunctions-extensions-bindings-cosmos/azurefunctions/extensions/bindings/cosmos/utils.py b/azurefunctions-extensions-bindings-cosmos/azurefunctions/extensions/bindings/cosmos/utils.py new file mode 100644 index 0000000..4df0934 --- /dev/null +++ b/azurefunctions-extensions-bindings-cosmos/azurefunctions/extensions/bindings/cosmos/utils.py @@ -0,0 +1,37 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import os + + +def get_connection_string(connection_string: str) -> str: + """ + Validates and returns the Cosmos DB connection string or endpoint URI. + Supports both App Settings and managed identity-based configurations. + + Expected formats: + 1. Not using managed identity: the environment variable exists as is. + 2. Using managed identity: __accountEndpoint must be appended. + 3. None of these cases existed, so the connection variable is invalid. + """ + if connection_string is None: + raise ValueError( + "Cosmos DB connection string cannot be None. " + "Please provide a connection string or account endpoint." + ) + elif connection_string in os.environ: + return os.getenv(connection_string) + elif connection_string + "__accountEndpoint" in os.environ: + return os.getenv(connection_string + "__accountEndpoint") + else: + raise ValueError( + f"Cosmos DB connection string {connection_string} does not exist. " + f"Please make sure that it is a defined App Setting." + ) + + +def using_managed_identity(connection_name: str) -> bool: + """ + Determines if managed identity is being used for Cosmos DB access + by checking for a __accountEndpoint suffix. + """ + return os.getenv(connection_name + "__accountEndpoint") is not None diff --git a/azurefunctions-extensions-bindings-cosmos/pyproject.toml b/azurefunctions-extensions-bindings-cosmos/pyproject.toml new file mode 100644 index 0000000..1eb8393 --- /dev/null +++ b/azurefunctions-extensions-bindings-cosmos/pyproject.toml @@ -0,0 +1,49 @@ +[build-system] +requires = ["setuptools >= 61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "azurefunctions-extensions-bindings-cosmos" +dynamic = ["version"] +requires-python = ">=3.9" +authors = [{ name = "Azure Functions team at Microsoft Corp.", email = "azurefunctions@microsoft.com"}] +description = "Cosmos 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-cosmos~=4.9.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.cosmos.__version__"} + +[tool.setuptools.packages.find] +exclude = [ + 'azurefunctions.extensions.bindings','azurefunctions.extensions', + 'azurefunctions', 'tests', 'samples' + ] + diff --git a/azurefunctions-extensions-bindings-cosmos/samples/README.md b/azurefunctions-extensions-bindings-cosmos/samples/README.md new file mode 100644 index 0000000..8241bb9 --- /dev/null +++ b/azurefunctions-extensions-bindings-cosmos/samples/README.md @@ -0,0 +1,68 @@ +--- +page_type: sample +languages: + - python +products: + - azure + - azure-functions + - azure-functions-extensions + - azurefunctions-extensions-bindings-blob +urlFragment: extension-blob-samples +--- + +# Azure Functions Extension Blob library for Python samples + +These are code samples that show common scenario operations with the Azure Functions Extension Blob library. + +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: + * 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 + +## 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 Blob library for Python with [pip](https://pypi.org/project/pip/): + +```bash +pip install azurefunctions-extensions-bindings-blob +``` + +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 either sending an HTTP request to the local endpoint or uploading a blob to the specified directory, +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 +[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-cosmos/samples/cosmos_samples_containerproxy/function_app.py b/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_containerproxy/function_app.py new file mode 100644 index 0000000..5bba218 --- /dev/null +++ b/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_containerproxy/function_app.py @@ -0,0 +1,46 @@ +# 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.blob as blob + +app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) + +""" +FOLDER: blob_samples_containerclient +DESCRIPTION: + These samples demonstrate how to obtain a ContainerClient from a Blob Trigger + or Blob Input function app binding. +USAGE: + Set the environment variables with your own values before running the + sample: + 1) AzureWebJobsStorage - the connection string to your storage account + + Set CONTAINER to the path to the container you want to trigger or serve as + input to the function. +""" + + +@app.blob_trigger(arg_name="client", path="CONTAINER", connection="AzureWebJobsStorage") +def blob_trigger(client: blob.ContainerClient): + logging.info( + f"Python blob trigger function processed blob \n" + f"Properties: {client.get_container_properties()}\n" + ) + + +@app.route(route="file") +@app.blob_input(arg_name="client", path="CONTAINER", 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_container_properties()}\n" + ) + return "ok" diff --git a/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_containerproxy/host.json b/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_containerproxy/host.json new file mode 100644 index 0000000..9df9136 --- /dev/null +++ b/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_containerproxy/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-cosmos/samples/cosmos_samples_containerproxy/local.settings.json b/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_containerproxy/local.settings.json new file mode 100644 index 0000000..c3c2a89 --- /dev/null +++ b/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_containerproxy/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-cosmos/samples/cosmos_samples_containerproxy/requirements.txt b/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_containerproxy/requirements.txt new file mode 100644 index 0000000..abd6bc4 --- /dev/null +++ b/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_containerproxy/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-blob \ No newline at end of file diff --git a/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_cosmosclient/function_app.py b/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_cosmosclient/function_app.py new file mode 100644 index 0000000..4d9e590 --- /dev/null +++ b/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_cosmosclient/function_app.py @@ -0,0 +1,52 @@ +# 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.blob as blob + +app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) + +""" +FOLDER: blob_samples_blobclient +DESCRIPTION: + These samples demonstrate how to obtain a BlobClient from a Blob Trigger + or Blob Input function app binding. +USAGE: + Set the environment variables with your own values before running the + sample: + 1) AzureWebJobsStorage - the connection string to your storage account + + Set PATH/TO/BLOB to the path to the blob you want to trigger or serve as + input to the function. +""" + + +@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().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().read(size=1)}" + ) + return "ok" diff --git a/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_cosmosclient/host.json b/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_cosmosclient/host.json new file mode 100644 index 0000000..9df9136 --- /dev/null +++ b/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_cosmosclient/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-cosmos/samples/cosmos_samples_cosmosclient/local.settings.json b/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_cosmosclient/local.settings.json new file mode 100644 index 0000000..c3c2a89 --- /dev/null +++ b/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_cosmosclient/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-cosmos/samples/cosmos_samples_cosmosclient/requirements.txt b/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_cosmosclient/requirements.txt new file mode 100644 index 0000000..abd6bc4 --- /dev/null +++ b/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_cosmosclient/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-blob \ No newline at end of file diff --git a/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_databaseproxy/function_app.py b/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_databaseproxy/function_app.py new file mode 100644 index 0000000..451c8af --- /dev/null +++ b/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_databaseproxy/function_app.py @@ -0,0 +1,52 @@ +# 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.blob as blob + +app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) + +""" +FOLDER: blob_samples_storagestreamdownloader +DESCRIPTION: + These samples demonstrate how to obtain a StorageStreamDownloader from a + Blob Trigger or Blob Input function app binding. +USAGE: + Set the environment variables with your own values before running the + sample: + 1) AzureWebJobsStorage - the connection string to your storage account + + Set PATH/TO/BLOB to the path to the blob you want to trigger or serve as + input to the function. +""" + + +@app.blob_trigger( + arg_name="stream", path="PATH/TO/BLOB", connection="AzureWebJobsStorage" +) +def blob_trigger(stream: blob.StorageStreamDownloader): + for chunk in stream.chunks(): + logging.info( + f"Python blob trigger function processed blob chunk \n" + f"Chunk: {chunk.decode()}" + ) + + +@app.route(route="file") +@app.blob_input( + arg_name="stream", path="PATH/TO/BLOB", connection="AzureWebJobsStorage" +) +def blob_input(req: func.HttpRequest, stream: blob.StorageStreamDownloader): + for chunk in stream.chunks(): + logging.info( + f"Python blob input function processed blob chunk \n" + f"Chunk: {chunk.decode()}" + ) + return "ok" diff --git a/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_databaseproxy/host.json b/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_databaseproxy/host.json new file mode 100644 index 0000000..9df9136 --- /dev/null +++ b/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_databaseproxy/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-cosmos/samples/cosmos_samples_databaseproxy/local.settings.json b/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_databaseproxy/local.settings.json new file mode 100644 index 0000000..c3c2a89 --- /dev/null +++ b/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_databaseproxy/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-cosmos/samples/cosmos_samples_databaseproxy/requirements.txt b/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_databaseproxy/requirements.txt new file mode 100644 index 0000000..abd6bc4 --- /dev/null +++ b/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_databaseproxy/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-blob \ No newline at end of file diff --git a/azurefunctions-extensions-bindings-cosmos/tests/__init__.py b/azurefunctions-extensions-bindings-cosmos/tests/__init__.py new file mode 100644 index 0000000..528a01b --- /dev/null +++ b/azurefunctions-extensions-bindings-cosmos/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-cosmos/tests/test_containerproxy.py b/azurefunctions-extensions-bindings-cosmos/tests/test_containerproxy.py new file mode 100644 index 0000000..2188542 --- /dev/null +++ b/azurefunctions-extensions-bindings-cosmos/tests/test_containerproxy.py @@ -0,0 +1,314 @@ +# # 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.storage.blob import ContainerClient as ContainerClientSdk +# from azurefunctions.extensions.base import Datum + +# from azurefunctions.extensions.bindings.blob import BlobClientConverter, ContainerClient + + +# # 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 TestContainerClient(unittest.TestCase): +# def test_input_type(self): +# check_input_type = BlobClientConverter.check_input_type_annotation +# self.assertTrue(check_input_type(ContainerClient)) +# self.assertFalse(check_input_type(str)) +# self.assertFalse(check_input_type(bytes)) +# self.assertFalse(check_input_type(bytearray)) + +# def test_input_none(self): +# result = BlobClientConverter.decode( +# data=None, trigger_metadata=None, pytype=ContainerClient +# ) +# self.assertIsNone(result) + +# datum: Datum = Datum(value=b"string_content", type=None) +# result = BlobClientConverter.decode( +# data=datum, trigger_metadata=None, pytype=ContainerClient +# ) +# self.assertIsNone(result) + +# def test_input_incorrect_type(self): +# datum: Datum = Datum(value=b"string_content", type="bytearray") +# with self.assertRaises(ValueError): +# BlobClientConverter.decode( +# data=datum, trigger_metadata=None, pytype=ContainerClient +# ) + +# def test_input_empty(self): +# datum: Datum = Datum(value={}, type="model_binding_data") +# result: ContainerClient = BlobClientConverter.decode( +# data=datum, trigger_metadata=None, pytype=ContainerClient +# ) +# self.assertIsNone(result) + +# def test_input_populated(self): +# content = { +# "Connection": "AzureWebJobsStorage", +# "ContainerName": "test-blob", +# "BlobName": "text.txt", +# } +# sample_mbd = MockMBD( +# version="1.0", +# source="AzureStorageBlobs", +# content_type="application/json", +# content=json.dumps(content), +# ) + +# datum: Datum = Datum(value=sample_mbd, type="model_binding_data") +# result: ContainerClient = BlobClientConverter.decode( +# data=datum, trigger_metadata=None, pytype=ContainerClient +# ) + +# self.assertIsNotNone(result) +# self.assertIsInstance(result, ContainerClientSdk) + +# sdk_result = ContainerClient(data=datum.value).get_sdk_type() +# self.assertIsNotNone(sdk_result) +# self.assertIsInstance(sdk_result, ContainerClientSdk) + +# def test_invalid_input_populated(self): +# content = { +# "Connection": "NotARealConnectionString", +# "ContainerName": "test-blob", +# "BlobName": "text.txt", +# } + +# sample_mbd = MockMBD( +# version="1.0", +# source="AzureStorageBlobs", +# content_type="application/json", +# content=json.dumps(content), +# ) + +# with self.assertRaises(ValueError) as e: +# datum: Datum = Datum(value=sample_mbd, type="model_binding_data") +# result: ContainerClient = BlobClientConverter.decode( +# data=datum, trigger_metadata=None, pytype=ContainerClient +# ) +# 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): +# content = { +# "Connection": None, +# "ContainerName": "test-blob", +# "BlobName": "text.txt", +# } + +# sample_mbd = MockMBD( +# version="1.0", +# source="AzureStorageBlobs", +# content_type="application/json", +# content=json.dumps(content), +# ) + +# with self.assertRaises(ValueError) as e: +# datum: Datum = Datum(value=sample_mbd, type="model_binding_data") +# result: ContainerClient = BlobClientConverter.decode( +# data=datum, trigger_metadata=None, pytype=ContainerClient +# ) +# self.assertEqual( +# e.exception.args[0], +# "Storage account connection string cannot be None. Please provide a connection string.", +# ) + +# def test_input_populated_managed_identity_input(self): +# content = { +# "Connection": "input", +# "ContainerName": "test-blob", +# "BlobName": "text.txt", +# } + +# sample_mbd = MockMBD( +# version="1.0", +# source="AzureStorageBlobs", +# content_type="application/json", +# content=json.dumps(content), +# ) + +# datum: Datum = Datum(value=sample_mbd, type="model_binding_data") +# result: ContainerClient = BlobClientConverter.decode( +# data=datum, trigger_metadata=None, pytype=ContainerClient +# ) + +# self.assertIsNotNone(result) +# self.assertIsInstance(result, ContainerClientSdk) + +# sdk_result = ContainerClient(data=datum.value).get_sdk_type() + +# self.assertIsNotNone(sdk_result) +# self.assertIsInstance(sdk_result, ContainerClientSdk) + +# def test_input_populated_managed_identity_trigger(self): +# content = { +# "Connection": "trigger", +# "ContainerName": "test-blob", +# "BlobName": "text.txt", +# } + +# sample_mbd = MockMBD( +# version="1.0", +# source="AzureStorageBlobs", +# content_type="application/json", +# content=json.dumps(content), +# ) + +# datum: Datum = Datum(value=sample_mbd, type="model_binding_data") +# result: ContainerClient = BlobClientConverter.decode( +# data=datum, trigger_metadata=None, pytype=ContainerClient +# ) + +# self.assertIsNotNone(result) +# self.assertIsInstance(result, ContainerClientSdk) + +# sdk_result = ContainerClient(data=datum.value).get_sdk_type() + +# self.assertIsNotNone(sdk_result) +# self.assertIsInstance(sdk_result, ContainerClientSdk) + +# def test_input_invalid_pytype(self): +# content = { +# "Connection": "AzureWebJobsStorage", +# "ContainerName": "test-blob", +# "BlobName": "text.txt", +# } + +# sample_mbd = MockMBD( +# version="1.0", +# source="AzureStorageBlobs", +# content_type="application/json", +# content=json.dumps(content), +# ) + +# datum: Datum = Datum(value=sample_mbd, type="model_binding_data") +# result: ContainerClient = BlobClientConverter.decode( +# data=datum, trigger_metadata=None, pytype="str" +# ) + +# self.assertIsNone(result) + +# def test_container_client_invalid_creation(self): +# # Create test binding +# mock_blob = MockBinding( +# name="blob", direction=MockBindingDirection.IN, data_type=None, type="blob" +# ) + +# # Create test input_types dict +# mock_input_types = { +# "blob": MockParamTypeInfo(binding_name="blobTrigger", pytype=bytes) +# } + +# # Create test indexed_function +# mock_indexed_functions = MockFunction(bindings=[mock_blob]) + +# dict_repr, logs = BlobClientConverter.get_raw_bindings( +# mock_indexed_functions, mock_input_types +# ) + +# self.assertEqual( +# dict_repr, +# [ +# '{"direction": "MockBindingDirection.IN", ' +# '"type": "blob", ' +# '"properties": ' +# '{"SupportsDeferredBinding": false}}' +# ], +# ) + +# self.assertEqual(logs, {"blob": {bytes: "False"}}) + +# def test_container_client_valid_creation(self): +# # Create test binding +# mock_blob = MockBinding( +# name="client", +# direction=MockBindingDirection.IN, +# data_type=None, +# type="blob", +# ) + +# # Create test input_types dict +# mock_input_types = { +# "client": MockParamTypeInfo( +# binding_name="blobTrigger", pytype=ContainerClient +# ) +# } + +# # Create test indexed_function +# mock_indexed_functions = MockFunction(bindings=[mock_blob]) + +# dict_repr, logs = BlobClientConverter.get_raw_bindings( +# mock_indexed_functions, mock_input_types +# ) + +# self.assertEqual( +# dict_repr, +# [ +# '{"direction": "MockBindingDirection.IN", ' +# '"type": "blob", ' +# '"properties": ' +# '{"SupportsDeferredBinding": true}}' +# ], +# ) + +# self.assertEqual(logs, {"client": {ContainerClient: "True"}}) diff --git a/azurefunctions-extensions-bindings-cosmos/tests/test_cosmosclient.py b/azurefunctions-extensions-bindings-cosmos/tests/test_cosmosclient.py new file mode 100644 index 0000000..6b98d3a --- /dev/null +++ b/azurefunctions-extensions-bindings-cosmos/tests/test_cosmosclient.py @@ -0,0 +1,288 @@ +# 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.cosmos import CosmosClient as CosmosClientSdk +from azurefunctions.extensions.base import Datum + +from azurefunctions.extensions.bindings.cosmos import CosmosClient, CosmosClientConverter + + +# 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 = CosmosClientConverter.check_input_type_annotation + self.assertTrue(check_input_type(CosmosClient)) + self.assertFalse(check_input_type(str)) + self.assertFalse(check_input_type(bytes)) + self.assertFalse(check_input_type(bytearray)) + + def test_input_none(self): + result = CosmosClientConverter.decode( + data=None, trigger_metadata=None, pytype=CosmosClient + ) + self.assertIsNone(result) + + datum: Datum = Datum(value=b"string_content", type=None) + result = CosmosClientConverter.decode( + data=datum, trigger_metadata=None, pytype=CosmosClient + ) + self.assertIsNone(result) + + def test_input_incorrect_type(self): + datum: Datum = Datum(value=b"string_content", type="bytearray") + with self.assertRaises(ValueError): + CosmosClientConverter.decode( + data=datum, trigger_metadata=None, pytype=CosmosClient + ) + + def test_input_empty(self): + datum: Datum = Datum(value={}, type="model_binding_data") + result: CosmosClient = CosmosClientConverter.decode( + data=datum, trigger_metadata=None, pytype=CosmosClient + ) + self.assertIsNone(result) + + def test_input_populated(self): + content = { + "DatabaseName": "test-db", + "ContainerName": "test-items", + "Connection": "AzureWebJobsStorage" + } + + sample_mbd = MockMBD( + version="1.0", + source="CosmosDB", + content_type="application/json", + content=json.dumps(content), + ) + + datum: Datum = Datum(value=sample_mbd, type="model_binding_data") + result: CosmosClient = CosmosClientConverter.decode( + data=datum, trigger_metadata=None, pytype=CosmosClient + ) + + self.assertIsNotNone(result) + self.assertIsInstance(result, CosmosClientSdk) + + sdk_result = CosmosClient(data=datum.value).get_sdk_type() + + self.assertIsNotNone(sdk_result) + self.assertIsInstance(sdk_result, CosmosClientSdk) + + def test_invalid_input_populated(self): + content = { + "DatabaseName": "test-db", + "ContainerName": "test-items", + "Connection": "NotARealConnectionString", + } + + sample_mbd = MockMBD( + version="1.0", + source="CosmosDB", + content_type="application/json", + content=json.dumps(content), + ) + + with self.assertRaises(ValueError) as e: + datum: Datum = Datum(value=sample_mbd, type="model_binding_data") + result: CosmosClient = CosmosClientConverter.decode( + data=datum, trigger_metadata=None, pytype=CosmosClient + ) + 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): + content = { + "DatabaseName": "test-db", + "ContainerName": "test-items", + "Connection": None, + } + + sample_mbd = MockMBD( + version="1.0", + source="CosmosDB", + content_type="application/json", + content=json.dumps(content), + ) + + with self.assertRaises(ValueError) as e: + datum: Datum = Datum(value=sample_mbd, type="model_binding_data") + result: CosmosClient = CosmosClientConverter.decode( + data=datum, trigger_metadata=None, pytype=CosmosClient + ) + self.assertEqual( + e.exception.args[0], + "Cosmos DB connection string cannot be None. " + "Please provide a connection string or account endpoint.", + ) + + def test_input_populated_managed_identity_input(self): + content = { + "DatabaseName": "test-db", + "ContainerName": "test-items", + "Connection": "input", + } + + sample_mbd = MockMBD( + version="1.0", + source="CosmosDB", + content_type="application/json", + content=json.dumps(content), + ) + + datum: Datum = Datum(value=sample_mbd, type="model_binding_data") + result: CosmosClient = CosmosClientConverter.decode( + data=datum, trigger_metadata=None, pytype=CosmosClient + ) + + self.assertIsNotNone(result) + self.assertIsInstance(result, CosmosClientSdk) + + sdk_result = CosmosClient(data=datum.value).get_sdk_type() + + self.assertIsNotNone(sdk_result) + self.assertIsInstance(sdk_result, CosmosClientSdk) + + def test_input_invalid_pytype(self): + content = { + "DatabaseName": "test-db", + "ContainerName": "test-items", + "Connection": "AzureWebJobsStorage", + } + + sample_mbd = MockMBD( + version="1.0", + source="CosmosDB", + content_type="application/json", + content=json.dumps(content), + ) + + datum: Datum = Datum(value=sample_mbd, type="model_binding_data") + result: CosmosClient = CosmosClientConverter.decode( + data=datum, trigger_metadata=None, pytype="str" + ) + + self.assertIsNone(result) + + def test_cosmos_client_invalid_creation(self): + # Create test binding + mock_blob = MockBinding( + name="cosmosDB", direction=MockBindingDirection.IN, data_type=None, type="cosmosDB" + ) + + # Create test input_types dict + mock_input_types = { + "cosmosDB": MockParamTypeInfo(binding_name="cosmosDB", pytype=bytes) + } + + # Create test indexed_function + mock_indexed_functions = MockFunction(bindings=[mock_blob]) + + dict_repr, logs = CosmosClientConverter.get_raw_bindings( + mock_indexed_functions, mock_input_types + ) + + self.assertEqual( + dict_repr, + [ + '{"direction": "MockBindingDirection.IN", ' + '"type": "cosmosDB", ' + '"properties": ' + '{"SupportsDeferredBinding": false}}' + ], + ) + + self.assertEqual(logs, {"cosmosDB": {bytes: "False"}}) + + def test_blob_client_valid_creation(self): + # Create test binding + mock_blob = MockBinding( + name="client", + direction=MockBindingDirection.IN, + data_type=None, + type="cosmosDB", + ) + + # Create test input_types dict + mock_input_types = { + "client": MockParamTypeInfo(binding_name="cosmosDB", pytype=CosmosClient) + } + + # Create test indexed_function + mock_indexed_functions = MockFunction(bindings=[mock_blob]) + + dict_repr, logs = CosmosClientConverter.get_raw_bindings( + mock_indexed_functions, mock_input_types + ) + + self.assertEqual( + dict_repr, + [ + '{"direction": "MockBindingDirection.IN", ' + '"type": "cosmosDB", ' + '"properties": ' + '{"SupportsDeferredBinding": true}}' + ], + ) + + self.assertEqual(logs, {"client": {CosmosClient: "True"}}) diff --git a/azurefunctions-extensions-bindings-cosmos/tests/test_databaseproxy.py b/azurefunctions-extensions-bindings-cosmos/tests/test_databaseproxy.py new file mode 100644 index 0000000..4f030a6 --- /dev/null +++ b/azurefunctions-extensions-bindings-cosmos/tests/test_databaseproxy.py @@ -0,0 +1,319 @@ +# # 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.storage.blob import StorageStreamDownloader as SSDSdk +# from azurefunctions.extensions.base import Datum + +# from azurefunctions.extensions.bindings.blob import ( +# BlobClientConverter, +# StorageStreamDownloader, +# ) + + +# # 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 TestStorageStreamDownloader(unittest.TestCase): +# def test_input_type(self): +# check_input_type = BlobClientConverter.check_input_type_annotation +# self.assertTrue(check_input_type(StorageStreamDownloader)) +# self.assertFalse(check_input_type(str)) +# self.assertFalse(check_input_type(bytes)) +# self.assertFalse(check_input_type(bytearray)) + +# def test_input_none(self): +# result = BlobClientConverter.decode( +# data=None, trigger_metadata=None, pytype=StorageStreamDownloader +# ) +# self.assertIsNone(result) + +# datum: Datum = Datum(value=b"string_content", type=None) +# result = BlobClientConverter.decode( +# data=datum, trigger_metadata=None, pytype=StorageStreamDownloader +# ) +# self.assertIsNone(result) + +# def test_input_incorrect_type(self): +# datum: Datum = Datum(value=b"string_content", type="bytearray") +# with self.assertRaises(ValueError): +# BlobClientConverter.decode( +# data=datum, trigger_metadata=None, pytype=StorageStreamDownloader +# ) + +# def test_input_empty(self): +# datum: Datum = Datum(value={}, type="model_binding_data") +# result: StorageStreamDownloader = BlobClientConverter.decode( +# data=datum, trigger_metadata=None, pytype=StorageStreamDownloader +# ) +# self.assertIsNone(result) + +# def test_input_populated(self): +# content = { +# "Connection": "AzureWebJobsStorage", +# "ContainerName": "test-blob", +# "BlobName": "text.txt", +# } + +# sample_mbd = MockMBD( +# version="1.0", +# source="AzureStorageBlobs", +# content_type="application/json", +# content=json.dumps(content), +# ) + +# datum: Datum = Datum(value=sample_mbd, type="model_binding_data") +# result: StorageStreamDownloader = BlobClientConverter.decode( +# data=datum, trigger_metadata=None, pytype=StorageStreamDownloader +# ) + +# self.assertIsNotNone(result) +# self.assertIsInstance(result, SSDSdk) + +# sdk_result = StorageStreamDownloader(data=datum.value).get_sdk_type() + +# self.assertIsNotNone(sdk_result) +# self.assertIsInstance(sdk_result, SSDSdk) + +# def test_invalid_input_populated(self): +# content = { +# "Connection": "NotARealConnectionString", +# "ContainerName": "test-blob", +# "BlobName": "text.txt", +# } + +# sample_mbd = MockMBD( +# version="1.0", +# source="AzureStorageBlobs", +# content_type="application/json", +# content=json.dumps(content), +# ) + +# with self.assertRaises(ValueError) as e: +# datum: Datum = Datum(value=sample_mbd, type="model_binding_data") +# result: StorageStreamDownloader = BlobClientConverter.decode( +# data=datum, trigger_metadata=None, pytype=StorageStreamDownloader +# ) +# 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): +# content = { +# "Connection": None, +# "ContainerName": "test-blob", +# "BlobName": "text.txt", +# } + +# sample_mbd = MockMBD( +# version="1.0", +# source="AzureStorageBlobs", +# content_type="application/json", +# content=json.dumps(content), +# ) + +# with self.assertRaises(ValueError) as e: +# datum: Datum = Datum(value=sample_mbd, type="model_binding_data") +# result: StorageStreamDownloader = BlobClientConverter.decode( +# data=datum, trigger_metadata=None, pytype=StorageStreamDownloader +# ) +# self.assertEqual( +# e.exception.args[0], +# "Storage account connection string cannot be None. Please provide a connection string.", +# ) + +# def test_input_populated_managed_identity_input(self): +# content = { +# "Connection": "input", +# "ContainerName": "test-blob", +# "BlobName": "text.txt", +# } + +# sample_mbd = MockMBD( +# version="1.0", +# source="AzureStorageBlobs", +# content_type="application/json", +# content=json.dumps(content), +# ) + +# datum: Datum = Datum(value=sample_mbd, type="model_binding_data") +# result: StorageStreamDownloader = BlobClientConverter.decode( +# data=datum, trigger_metadata=None, pytype=StorageStreamDownloader +# ) + +# self.assertIsNotNone(result) +# self.assertIsInstance(result, SSDSdk) + +# sdk_result = StorageStreamDownloader(data=datum.value).get_sdk_type() + +# self.assertIsNotNone(sdk_result) +# self.assertIsInstance(sdk_result, SSDSdk) + +# def test_input_populated_managed_identity_trigger(self): +# content = { +# "Connection": "trigger", +# "ContainerName": "test-blob", +# "BlobName": "text.txt", +# } + +# sample_mbd = MockMBD( +# version="1.0", +# source="AzureStorageBlobs", +# content_type="application/json", +# content=json.dumps(content), +# ) + +# datum: Datum = Datum(value=sample_mbd, type="model_binding_data") +# result: StorageStreamDownloader = BlobClientConverter.decode( +# data=datum, trigger_metadata=None, pytype=StorageStreamDownloader +# ) + +# self.assertIsNotNone(result) +# self.assertIsInstance(result, SSDSdk) + +# sdk_result = StorageStreamDownloader(data=datum.value).get_sdk_type() + +# self.assertIsNotNone(sdk_result) +# self.assertIsInstance(sdk_result, SSDSdk) + +# def test_input_invalid_pytype(self): +# content = { +# "Connection": "AzureWebJobsStorage", +# "ContainerName": "test-blob", +# "BlobName": "text.txt", +# } + +# sample_mbd = MockMBD( +# version="1.0", +# source="AzureStorageBlobs", +# content_type="application/json", +# content=json.dumps(content), +# ) + +# datum: Datum = Datum(value=sample_mbd, type="model_binding_data") +# result: StorageStreamDownloader = BlobClientConverter.decode( +# data=datum, trigger_metadata=None, pytype="str" +# ) + +# self.assertIsNone(result) + +# def test_ssd_invalid_creation(self): +# # Create test binding +# mock_blob = MockBinding( +# name="blob", direction=MockBindingDirection.IN, data_type=None, type="blob" +# ) + +# # Create test input_types dict +# mock_input_types = { +# "blob": MockParamTypeInfo(binding_name="blobTrigger", pytype=bytes) +# } + +# # Create test indexed_function +# mock_indexed_functions = MockFunction(bindings=[mock_blob]) + +# dict_repr, logs = BlobClientConverter.get_raw_bindings( +# mock_indexed_functions, mock_input_types +# ) + +# self.assertEqual( +# dict_repr, +# [ +# '{"direction": "MockBindingDirection.IN", ' +# '"type": "blob", ' +# '"properties": ' +# '{"SupportsDeferredBinding": false}}' +# ], +# ) + +# self.assertEqual(logs, {"blob": {bytes: "False"}}) + +# def test_ssd_valid_creation(self): +# # Create test binding +# mock_blob = MockBinding( +# name="client", +# direction=MockBindingDirection.IN, +# data_type=None, +# type="blob", +# ) + +# # Create test input_types dict +# mock_input_types = { +# "client": MockParamTypeInfo( +# binding_name="blobTrigger", pytype=StorageStreamDownloader +# ) +# } + +# # Create test indexed_function +# mock_indexed_functions = MockFunction(bindings=[mock_blob]) + +# dict_repr, logs = BlobClientConverter.get_raw_bindings( +# mock_indexed_functions, mock_input_types +# ) + +# self.assertEqual( +# dict_repr, +# [ +# '{"direction": "MockBindingDirection.IN", ' +# '"type": "blob", ' +# '"properties": ' +# '{"SupportsDeferredBinding": true}}' +# ], +# ) + +# self.assertEqual(logs, {"client": {StorageStreamDownloader: "True"}}) diff --git a/eng/ci/ci-cosmos-tests.yml b/eng/ci/ci-cosmos-tests.yml new file mode 100644 index 0000000..3edac9d --- /dev/null +++ b/eng/ci/ci-cosmos-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: RunCosmosUnitTests + jobs: + - template: /eng/templates/official/jobs/cosmos-unit-tests.yml@self diff --git a/eng/ci/official-build.yml b/eng/ci/official-build.yml index 28f6ec8..df6e0e4 100644 --- a/eng/ci/official-build.yml +++ b/eng/ci/official-build.yml @@ -51,6 +51,10 @@ extends: dependsOn: Build jobs: - template: /eng/templates/official/jobs/blob-unit-tests.yml@self + - stage: RunCosmosTests + dependsOn: Build + jobs: + - template: /eng/templates/official/jobs/cosmos-unit-tests.yml@self - stage: RunFastApiTests dependsOn: Build jobs: diff --git a/eng/templates/jobs/build.yml b/eng/templates/jobs/build.yml index 3c3c7c4..981e82b 100644 --- a/eng/templates/jobs/build.yml +++ b/eng/templates/jobs/build.yml @@ -10,6 +10,9 @@ jobs: blob_extension: EXTENSION_DIRECTORY: 'azurefunctions-extensions-bindings-blob' EXTENSION_NAME: 'Blob' + cosmos_extension: + EXTENSION_DIRECTORY: 'azurefunctions-extensions-bindings-cosmos' + EXTENSION_NAME: 'Cosmos' eventhub_extension: EXTENSION_DIRECTORY: 'azurefunctions-extensions-bindings-eventhub' EXTENSION_NAME: 'EventHub' diff --git a/eng/templates/official/jobs/build-artifacts.yml b/eng/templates/official/jobs/build-artifacts.yml index f41a08e..7c43d0e 100644 --- a/eng/templates/official/jobs/build-artifacts.yml +++ b/eng/templates/official/jobs/build-artifacts.yml @@ -10,6 +10,9 @@ jobs: blob_extension: EXTENSION_DIRECTORY: 'azurefunctions-extensions-bindings-blob' EXTENSION_NAME: 'Blob' + cosmos_extension: + EXTENSION_DIRECTORY: 'azurefunctions-extensions-bindings-cosmos' + EXTENSION_NAME: 'Cosmos' eventhub_extension: EXTENSION_DIRECTORY: 'azurefunctions-extensions-bindings-eventhub' EXTENSION_NAME: 'EventHub' diff --git a/eng/templates/official/jobs/cosmos-unit-tests.yml b/eng/templates/official/jobs/cosmos-unit-tests.yml new file mode 100644 index 0000000..00a0abe --- /dev/null +++ b/eng/templates/official/jobs/cosmos-unit-tests.yml @@ -0,0 +1,30 @@ +jobs: + - job: "TestPython" + displayName: "Run Cosmos 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-cosmos + python -m pip install -U -e .[dev] + displayName: 'Install dependencies' + - bash: | + python -m pytest -q --instafail azurefunctions-extensions-bindings-cosmos/tests/ + env: + AzureWebJobsStorage: $(AzureWebJobsStorage) + input: $(input__accountEndpoint) + displayName: "Running Cosmos $(PYTHON_VERSION) Python Extension Tests" From 08967717e4a669932f97e7d0098f9af55e5b271a Mon Sep 17 00:00:00 2001 From: Evan Roman Date: Fri, 11 Apr 2025 15:46:59 -0500 Subject: [PATCH 02/12] Fix --- azurefunctions-extensions-bindings-cosmos/pyproject.toml | 4 +++- azurefunctions-extensions-bindings-cosmos/tests/__init__.py | 3 --- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/azurefunctions-extensions-bindings-cosmos/pyproject.toml b/azurefunctions-extensions-bindings-cosmos/pyproject.toml index 1eb8393..841ceeb 100644 --- a/azurefunctions-extensions-bindings-cosmos/pyproject.toml +++ b/azurefunctions-extensions-bindings-cosmos/pyproject.toml @@ -35,7 +35,9 @@ dev = [ 'pytest-cov', 'coverage', 'pytest-instafail', - 'pre-commit' + 'pre-commit', + 'mypy', + 'flake8' ] [tool.setuptools.dynamic] diff --git a/azurefunctions-extensions-bindings-cosmos/tests/__init__.py b/azurefunctions-extensions-bindings-cosmos/tests/__init__.py index 528a01b..3a41690 100644 --- a/azurefunctions-extensions-bindings-cosmos/tests/__init__.py +++ b/azurefunctions-extensions-bindings-cosmos/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(): From b075c1709d7a556a442050f64645e663c3723109 Mon Sep 17 00:00:00 2001 From: Evan Roman Date: Fri, 11 Apr 2025 15:54:05 -0500 Subject: [PATCH 03/12] Add code qual --- .../tests/test_code_quality.py | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 azurefunctions-extensions-bindings-cosmos/tests/test_code_quality.py diff --git a/azurefunctions-extensions-bindings-cosmos/tests/test_code_quality.py b/azurefunctions-extensions-bindings-cosmos/tests/test_code_quality.py new file mode 100644 index 0000000..19a4e65 --- /dev/null +++ b/azurefunctions-extensions-bindings-cosmos/tests/test_code_quality.py @@ -0,0 +1,53 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import pathlib +import subprocess +import sys +import unittest + +ROOT_PATH = pathlib.Path(__file__).parent.parent.parent + + +class TestCodeQuality(unittest.TestCase): + def test_mypy(self): + try: + import mypy # NoQA + except ImportError as e: + raise unittest.SkipTest('mypy module is missing') from e + + try: + subprocess.run( + [sys.executable, '-m', 'mypy', '-m', + 'azurefunctions-extensions-bindings-cosmos'], + check=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + cwd=str(ROOT_PATH)) + except subprocess.CalledProcessError as ex: + output = ex.output.decode() + raise AssertionError( + f'mypy validation failed:\n{output}') from None + + def test_flake8(self): + try: + import flake8 # NoQA + except ImportError as e: + raise unittest.SkipTest('flake8 module is missing') from e + + config_path = ROOT_PATH / '.flake8' + if not config_path.exists(): + raise unittest.SkipTest('could not locate the .flake8 file') + + try: + subprocess.run( + [sys.executable, '-m', 'flake8', + 'azurefunctions-extensions-bindings-cosmos', + '--config', str(config_path)], + check=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + cwd=str(ROOT_PATH)) + except subprocess.CalledProcessError as ex: + output = ex.output.decode() + raise AssertionError( + f'flake8 validation failed:\n{output}') from None From 859ed625ff4ab2b1e347ea1b716335899c91e067 Mon Sep 17 00:00:00 2001 From: Evan Roman Date: Mon, 14 Apr 2025 10:58:03 -0500 Subject: [PATCH 04/12] Add docs --- .../samples/README.md | 35 +- .../function_app.py | 36 +- .../local.settings.json | 3 +- .../requirements.txt | 2 +- .../function_app.py | 51 +- .../local.settings.json | 3 +- .../function_app.py | 51 +- .../local.settings.json | 3 +- .../tests/test_containerproxy.py | 602 +++++++++-------- .../tests/test_databaseproxy.py | 608 +++++++++--------- 10 files changed, 643 insertions(+), 751 deletions(-) diff --git a/azurefunctions-extensions-bindings-cosmos/samples/README.md b/azurefunctions-extensions-bindings-cosmos/samples/README.md index 8241bb9..7f8525f 100644 --- a/azurefunctions-extensions-bindings-cosmos/samples/README.md +++ b/azurefunctions-extensions-bindings-cosmos/samples/README.md @@ -6,28 +6,25 @@ products: - azure - azure-functions - azure-functions-extensions - - azurefunctions-extensions-bindings-blob -urlFragment: extension-blob-samples + - azurefunctions-extensions-bindings-cosmos +urlFragment: extension-cosmos-samples --- -# Azure Functions Extension Blob library for Python samples +# Azure Functions Extension Cosmos library for Python samples -These are code samples that show common scenario operations with the Azure Functions Extension Blob library. +These are code samples that show common scenario operations with the Azure Functions Extension Cosmos library. -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) +These samples relate to the Azure Storage Cosmos client library being used as part of a Python Function App. For +examples on how to use the Azure Storage Cosmos client library, please see [Azure Cosmos samples](https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/cosmos/azure-cosmos/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: - * From BlobTrigger - * From BlobInput +* [cosmos_samples_cosmosclient](https://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-binding-cosmos/samples/cosmos_samples_cosmosclient) - Examples for using the CosmosClient type: + * From CosmosInput -* [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 +* [cosmos_samples_databaseproxy](https://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_databaseproxy) - Examples for using the DatabaseProxy type: + * From CosmosInput -* [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 +* [cosmos_samples_containerclient](https://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_containerproxy) - Examples for using the ContainerProxy type: + * From CosmosInput ## 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). @@ -40,7 +37,7 @@ examples on how to use the Azure Storage Blob client library, please see [Azure 2. Install the Azure Functions Extension Blob library for Python with [pip](https://pypi.org/project/pip/): ```bash -pip install azurefunctions-extensions-bindings-blob +pip install azurefunctions-extensions-bindings-cosmos ``` 3. Clone or download this sample repository @@ -58,11 +55,11 @@ pip install -r requirements.txt ```bash func start ``` -5. Execute the function by either sending an HTTP request to the local endpoint or uploading a blob to the specified directory, +5. Execute the function by sending an HTTP request to the local endpoint, 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 -[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 +[API reference documentation](https://learn.microsoft.com/en-us/python/api/azure-cosmos/azure.cosmos?view=azure-python) to learn more about +what you can do with the Azure Cosmos client library. \ No newline at end of file diff --git a/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_containerproxy/function_app.py b/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_containerproxy/function_app.py index 5bba218..596c299 100644 --- a/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_containerproxy/function_app.py +++ b/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_containerproxy/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 @@ -9,38 +7,32 @@ import logging import azure.functions as func -import azurefunctions.extensions.bindings.blob as blob +import azurefunctions.extensions.bindings.cosmos as cosmos app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) """ -FOLDER: blob_samples_containerclient +FOLDER: cosmos_samples_containerproxy DESCRIPTION: - These samples demonstrate how to obtain a ContainerClient from a Blob Trigger - or Blob Input function app binding. + These samples demonstrate how to obtain a ContainerProxy from a Cosmos Input function app binding. USAGE: Set the environment variables with your own values before running the sample: 1) AzureWebJobsStorage - the connection string to your storage account - Set CONTAINER to the path to the container you want to trigger or serve as - input to the function. + Set database_name and container_name to the path to the container you want to use + as inputs to the function (required). """ -@app.blob_trigger(arg_name="client", path="CONTAINER", connection="AzureWebJobsStorage") -def blob_trigger(client: blob.ContainerClient): - logging.info( - f"Python blob trigger function processed blob \n" - f"Properties: {client.get_container_properties()}\n" - ) - +@app.route(route="container") +@app.cosmos_db_input(arg_name="container", + connection="AzureWebJobsStorage", + database_name="db_name", + container_name="container_name") +def get_docs(req: func.HttpRequest, container: cosmos.ContainerProxy): + docs = container.query_items(query="SELECT * FROM c", enable_cross_partition_query=True) + for d in docs: + logging.info(f"Found document: {d}") -@app.route(route="file") -@app.blob_input(arg_name="client", path="CONTAINER", 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_container_properties()}\n" - ) return "ok" diff --git a/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_containerproxy/local.settings.json b/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_containerproxy/local.settings.json index c3c2a89..6dc40bb 100644 --- a/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_containerproxy/local.settings.json +++ b/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_containerproxy/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-cosmos/samples/cosmos_samples_containerproxy/requirements.txt b/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_containerproxy/requirements.txt index abd6bc4..b4b4870 100644 --- a/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_containerproxy/requirements.txt +++ b/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_containerproxy/requirements.txt @@ -3,4 +3,4 @@ # Manually managing azure-functions-worker may cause unexpected issues azure-functions -azurefunctions-extensions-bindings-blob \ No newline at end of file +azurefunctions-extensions-bindings-cosmos \ No newline at end of file diff --git a/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_cosmosclient/function_app.py b/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_cosmosclient/function_app.py index 4d9e590..f98bf08 100644 --- a/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_cosmosclient/function_app.py +++ b/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_cosmosclient/function_app.py @@ -1,52 +1,33 @@ -# 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.blob as blob +import azurefunctions.extensions.bindings.cosmos as cosmos app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) + """ -FOLDER: blob_samples_blobclient +FOLDER: cosmos_samples_cosmosclient DESCRIPTION: - These samples demonstrate how to obtain a BlobClient from a Blob Trigger - or Blob Input function app binding. + These samples demonstrate how to obtain a CosmosClient from a Cosmos Input function app binding. USAGE: Set the environment variables with your own values before running the sample: 1) AzureWebJobsStorage - the connection string to your storage account - Set PATH/TO/BLOB to the path to the blob you want to trigger or serve as - input to the function. + Set database_name and container_name to the path to the container you want to use + as inputs to the function (required). """ -@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().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().read(size=1)}" - ) +@app.route(route="cosmos") +@app.cosmos_db_input(arg_name="container", + connection="AzureWebJobsStorage", + database_name="db_name", + container_name="container_name") +def get_docs(req: func.HttpRequest, client: cosmos.CosmosClient): + databases = client.list_databases() + for db in databases: + logging.info(f"Found database with ID: {db.get('id')}") + return "ok" diff --git a/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_cosmosclient/local.settings.json b/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_cosmosclient/local.settings.json index c3c2a89..6dc40bb 100644 --- a/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_cosmosclient/local.settings.json +++ b/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_cosmosclient/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-cosmos/samples/cosmos_samples_databaseproxy/function_app.py b/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_databaseproxy/function_app.py index 451c8af..5f16830 100644 --- a/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_databaseproxy/function_app.py +++ b/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_databaseproxy/function_app.py @@ -1,52 +1,33 @@ -# 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.blob as blob +import azurefunctions.extensions.bindings.cosmos as cosmos app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) + """ -FOLDER: blob_samples_storagestreamdownloader +FOLDER: cosmos_samples_databaseproxy DESCRIPTION: - These samples demonstrate how to obtain a StorageStreamDownloader from a - Blob Trigger or Blob Input function app binding. + These samples demonstrate how to obtain a DatabaseProxy from a Cosmos Input function app binding. USAGE: Set the environment variables with your own values before running the sample: 1) AzureWebJobsStorage - the connection string to your storage account - Set PATH/TO/BLOB to the path to the blob you want to trigger or serve as - input to the function. + Set database_name and container_name to the path to the container you want to use + as inputs to the function (required). """ -@app.blob_trigger( - arg_name="stream", path="PATH/TO/BLOB", connection="AzureWebJobsStorage" -) -def blob_trigger(stream: blob.StorageStreamDownloader): - for chunk in stream.chunks(): - logging.info( - f"Python blob trigger function processed blob chunk \n" - f"Chunk: {chunk.decode()}" - ) - - -@app.route(route="file") -@app.blob_input( - arg_name="stream", path="PATH/TO/BLOB", connection="AzureWebJobsStorage" -) -def blob_input(req: func.HttpRequest, stream: blob.StorageStreamDownloader): - for chunk in stream.chunks(): - logging.info( - f"Python blob input function processed blob chunk \n" - f"Chunk: {chunk.decode()}" - ) +@app.route(route="database") +@app.cosmos_db_input(arg_name="container", + connection="AzureWebJobsStorage", + database_name="db_name", + container_name="container_name") +def get_docs(req: func.HttpRequest, database: cosmos.DatabaseProxy): + containers = database.list_containers() + for c in containers: + logging.info(f"Found container with ID: {c.get('id')}") + return "ok" diff --git a/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_databaseproxy/local.settings.json b/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_databaseproxy/local.settings.json index c3c2a89..6dc40bb 100644 --- a/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_databaseproxy/local.settings.json +++ b/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_databaseproxy/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-cosmos/tests/test_containerproxy.py b/azurefunctions-extensions-bindings-cosmos/tests/test_containerproxy.py index 2188542..742c72c 100644 --- a/azurefunctions-extensions-bindings-cosmos/tests/test_containerproxy.py +++ b/azurefunctions-extensions-bindings-cosmos/tests/test_containerproxy.py @@ -1,314 +1,288 @@ -# # 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.storage.blob import ContainerClient as ContainerClientSdk -# from azurefunctions.extensions.base import Datum - -# from azurefunctions.extensions.bindings.blob import BlobClientConverter, ContainerClient - - -# # 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 TestContainerClient(unittest.TestCase): -# def test_input_type(self): -# check_input_type = BlobClientConverter.check_input_type_annotation -# self.assertTrue(check_input_type(ContainerClient)) -# self.assertFalse(check_input_type(str)) -# self.assertFalse(check_input_type(bytes)) -# self.assertFalse(check_input_type(bytearray)) - -# def test_input_none(self): -# result = BlobClientConverter.decode( -# data=None, trigger_metadata=None, pytype=ContainerClient -# ) -# self.assertIsNone(result) - -# datum: Datum = Datum(value=b"string_content", type=None) -# result = BlobClientConverter.decode( -# data=datum, trigger_metadata=None, pytype=ContainerClient -# ) -# self.assertIsNone(result) - -# def test_input_incorrect_type(self): -# datum: Datum = Datum(value=b"string_content", type="bytearray") -# with self.assertRaises(ValueError): -# BlobClientConverter.decode( -# data=datum, trigger_metadata=None, pytype=ContainerClient -# ) - -# def test_input_empty(self): -# datum: Datum = Datum(value={}, type="model_binding_data") -# result: ContainerClient = BlobClientConverter.decode( -# data=datum, trigger_metadata=None, pytype=ContainerClient -# ) -# self.assertIsNone(result) - -# def test_input_populated(self): -# content = { -# "Connection": "AzureWebJobsStorage", -# "ContainerName": "test-blob", -# "BlobName": "text.txt", -# } -# sample_mbd = MockMBD( -# version="1.0", -# source="AzureStorageBlobs", -# content_type="application/json", -# content=json.dumps(content), -# ) - -# datum: Datum = Datum(value=sample_mbd, type="model_binding_data") -# result: ContainerClient = BlobClientConverter.decode( -# data=datum, trigger_metadata=None, pytype=ContainerClient -# ) - -# self.assertIsNotNone(result) -# self.assertIsInstance(result, ContainerClientSdk) - -# sdk_result = ContainerClient(data=datum.value).get_sdk_type() -# self.assertIsNotNone(sdk_result) -# self.assertIsInstance(sdk_result, ContainerClientSdk) - -# def test_invalid_input_populated(self): -# content = { -# "Connection": "NotARealConnectionString", -# "ContainerName": "test-blob", -# "BlobName": "text.txt", -# } - -# sample_mbd = MockMBD( -# version="1.0", -# source="AzureStorageBlobs", -# content_type="application/json", -# content=json.dumps(content), -# ) - -# with self.assertRaises(ValueError) as e: -# datum: Datum = Datum(value=sample_mbd, type="model_binding_data") -# result: ContainerClient = BlobClientConverter.decode( -# data=datum, trigger_metadata=None, pytype=ContainerClient -# ) -# 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): -# content = { -# "Connection": None, -# "ContainerName": "test-blob", -# "BlobName": "text.txt", -# } - -# sample_mbd = MockMBD( -# version="1.0", -# source="AzureStorageBlobs", -# content_type="application/json", -# content=json.dumps(content), -# ) - -# with self.assertRaises(ValueError) as e: -# datum: Datum = Datum(value=sample_mbd, type="model_binding_data") -# result: ContainerClient = BlobClientConverter.decode( -# data=datum, trigger_metadata=None, pytype=ContainerClient -# ) -# self.assertEqual( -# e.exception.args[0], -# "Storage account connection string cannot be None. Please provide a connection string.", -# ) - -# def test_input_populated_managed_identity_input(self): -# content = { -# "Connection": "input", -# "ContainerName": "test-blob", -# "BlobName": "text.txt", -# } - -# sample_mbd = MockMBD( -# version="1.0", -# source="AzureStorageBlobs", -# content_type="application/json", -# content=json.dumps(content), -# ) - -# datum: Datum = Datum(value=sample_mbd, type="model_binding_data") -# result: ContainerClient = BlobClientConverter.decode( -# data=datum, trigger_metadata=None, pytype=ContainerClient -# ) - -# self.assertIsNotNone(result) -# self.assertIsInstance(result, ContainerClientSdk) - -# sdk_result = ContainerClient(data=datum.value).get_sdk_type() - -# self.assertIsNotNone(sdk_result) -# self.assertIsInstance(sdk_result, ContainerClientSdk) - -# def test_input_populated_managed_identity_trigger(self): -# content = { -# "Connection": "trigger", -# "ContainerName": "test-blob", -# "BlobName": "text.txt", -# } - -# sample_mbd = MockMBD( -# version="1.0", -# source="AzureStorageBlobs", -# content_type="application/json", -# content=json.dumps(content), -# ) - -# datum: Datum = Datum(value=sample_mbd, type="model_binding_data") -# result: ContainerClient = BlobClientConverter.decode( -# data=datum, trigger_metadata=None, pytype=ContainerClient -# ) - -# self.assertIsNotNone(result) -# self.assertIsInstance(result, ContainerClientSdk) - -# sdk_result = ContainerClient(data=datum.value).get_sdk_type() - -# self.assertIsNotNone(sdk_result) -# self.assertIsInstance(sdk_result, ContainerClientSdk) - -# def test_input_invalid_pytype(self): -# content = { -# "Connection": "AzureWebJobsStorage", -# "ContainerName": "test-blob", -# "BlobName": "text.txt", -# } - -# sample_mbd = MockMBD( -# version="1.0", -# source="AzureStorageBlobs", -# content_type="application/json", -# content=json.dumps(content), -# ) - -# datum: Datum = Datum(value=sample_mbd, type="model_binding_data") -# result: ContainerClient = BlobClientConverter.decode( -# data=datum, trigger_metadata=None, pytype="str" -# ) - -# self.assertIsNone(result) - -# def test_container_client_invalid_creation(self): -# # Create test binding -# mock_blob = MockBinding( -# name="blob", direction=MockBindingDirection.IN, data_type=None, type="blob" -# ) - -# # Create test input_types dict -# mock_input_types = { -# "blob": MockParamTypeInfo(binding_name="blobTrigger", pytype=bytes) -# } - -# # Create test indexed_function -# mock_indexed_functions = MockFunction(bindings=[mock_blob]) - -# dict_repr, logs = BlobClientConverter.get_raw_bindings( -# mock_indexed_functions, mock_input_types -# ) - -# self.assertEqual( -# dict_repr, -# [ -# '{"direction": "MockBindingDirection.IN", ' -# '"type": "blob", ' -# '"properties": ' -# '{"SupportsDeferredBinding": false}}' -# ], -# ) - -# self.assertEqual(logs, {"blob": {bytes: "False"}}) - -# def test_container_client_valid_creation(self): -# # Create test binding -# mock_blob = MockBinding( -# name="client", -# direction=MockBindingDirection.IN, -# data_type=None, -# type="blob", -# ) - -# # Create test input_types dict -# mock_input_types = { -# "client": MockParamTypeInfo( -# binding_name="blobTrigger", pytype=ContainerClient -# ) -# } - -# # Create test indexed_function -# mock_indexed_functions = MockFunction(bindings=[mock_blob]) - -# dict_repr, logs = BlobClientConverter.get_raw_bindings( -# mock_indexed_functions, mock_input_types -# ) - -# self.assertEqual( -# dict_repr, -# [ -# '{"direction": "MockBindingDirection.IN", ' -# '"type": "blob", ' -# '"properties": ' -# '{"SupportsDeferredBinding": true}}' -# ], -# ) - -# self.assertEqual(logs, {"client": {ContainerClient: "True"}}) +# 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.cosmos import ContainerProxy as ContainerProxySdk +from azurefunctions.extensions.base import Datum + +from azurefunctions.extensions.bindings.cosmos import CosmosClientConverter, ContainerProxy + + +# 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 TestContainerProxy(unittest.TestCase): + def test_input_type(self): + check_input_type = CosmosClientConverter.check_input_type_annotation + self.assertTrue(check_input_type(ContainerProxy)) + self.assertFalse(check_input_type(str)) + self.assertFalse(check_input_type(bytes)) + self.assertFalse(check_input_type(bytearray)) + + def test_input_none(self): + result = CosmosClientConverter.decode( + data=None, trigger_metadata=None, pytype=ContainerProxy + ) + self.assertIsNone(result) + + datum: Datum = Datum(value=b"string_content", type=None) + result = CosmosClientConverter.decode( + data=datum, trigger_metadata=None, pytype=ContainerProxy + ) + self.assertIsNone(result) + + def test_input_incorrect_type(self): + datum: Datum = Datum(value=b"string_content", type="bytearray") + with self.assertRaises(ValueError): + CosmosClientConverter.decode( + data=datum, trigger_metadata=None, pytype=ContainerProxy + ) + + def test_input_empty(self): + datum: Datum = Datum(value={}, type="model_binding_data") + result: ContainerProxy = CosmosClientConverter.decode( + data=datum, trigger_metadata=None, pytype=ContainerProxy + ) + self.assertIsNone(result) + + def test_input_populated(self): + content = { + "DatabaseName": "test-db", + "ContainerName": "test-items", + "Connection": "AzureWebJobsStorage" + } + + sample_mbd = MockMBD( + version="1.0", + source="CosmosDB", + content_type="application/json", + content=json.dumps(content), + ) + + datum: Datum = Datum(value=sample_mbd, type="model_binding_data") + result: ContainerProxy = CosmosClientConverter.decode( + data=datum, trigger_metadata=None, pytype=ContainerProxy + ) + + self.assertIsNotNone(result) + self.assertIsInstance(result, ContainerProxySdk) + + sdk_result = ContainerProxy(data=datum.value).get_sdk_type() + self.assertIsNotNone(sdk_result) + self.assertIsInstance(sdk_result, ContainerProxySdk) + + def test_invalid_input_populated(self): + content = { + "DatabaseName": "test-db", + "ContainerName": "test-items", + "Connection": "NotARealConnectionString" + } + + sample_mbd = MockMBD( + version="1.0", + source="CosmosDB", + content_type="application/json", + content=json.dumps(content), + ) + + with self.assertRaises(ValueError) as e: + datum: Datum = Datum(value=sample_mbd, type="model_binding_data") + result: ContainerProxy = CosmosClientConverter.decode( + data=datum, trigger_metadata=None, pytype=ContainerProxy + ) + 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): + content = { + "DatabaseName": "test-db", + "ContainerName": "test-items", + "Connection": None + } + + sample_mbd = MockMBD( + version="1.0", + source="CosmosDB", + content_type="application/json", + content=json.dumps(content), + ) + + with self.assertRaises(ValueError) as e: + datum: Datum = Datum(value=sample_mbd, type="model_binding_data") + result: ContainerProxy = CosmosClientConverter.decode( + data=datum, trigger_metadata=None, pytype=ContainerProxy + ) + self.assertEqual( + e.exception.args[0], + "Storage account connection string cannot be None. Please provide a connection string.", + ) + + def test_input_populated_managed_identity_input(self): + content = { + "DatabaseName": "test-db", + "ContainerName": "test-items", + "Connection": "input" + } + + sample_mbd = MockMBD( + version="1.0", + source="CosmosDB", + content_type="application/json", + content=json.dumps(content), + ) + + datum: Datum = Datum(value=sample_mbd, type="model_binding_data") + result: ContainerProxy = CosmosClientConverter.decode( + data=datum, trigger_metadata=None, pytype=ContainerProxy + ) + + self.assertIsNotNone(result) + self.assertIsInstance(result, ContainerProxySdk) + + sdk_result = ContainerProxy(data=datum.value).get_sdk_type() + + self.assertIsNotNone(sdk_result) + self.assertIsInstance(sdk_result, ContainerProxySdk) + + def test_input_invalid_pytype(self): + content = { + "DatabaseName": "test-db", + "ContainerName": "test-items", + "Connection": "AzureWebJobsStorage" + } + + sample_mbd = MockMBD( + version="1.0", + source="CosmosDB", + content_type="application/json", + content=json.dumps(content), + ) + + datum: Datum = Datum(value=sample_mbd, type="model_binding_data") + result: ContainerProxy = CosmosClientConverter.decode( + data=datum, trigger_metadata=None, pytype="str" + ) + + self.assertIsNone(result) + + def test_container_proxy_invalid_creation(self): + # Create test binding + mock_blob = MockBinding( + name="cosmosDB", direction=MockBindingDirection.IN, data_type=None, type="cosmosDB" + ) + + # Create test input_types dict + mock_input_types = { + "cosmosDB": MockParamTypeInfo(binding_name="cosmosDB", pytype=bytes) + } + + # Create test indexed_function + mock_indexed_functions = MockFunction(bindings=[mock_blob]) + + dict_repr, logs = CosmosClientConverter.get_raw_bindings( + mock_indexed_functions, mock_input_types + ) + + self.assertEqual( + dict_repr, + [ + '{"direction": "MockBindingDirection.IN", ' + '"type": "cosmosDB", ' + '"properties": ' + '{"SupportsDeferredBinding": false}}' + ], + ) + + self.assertEqual(logs, {"cosmosDB": {bytes: "False"}}) + + def test_container_proxy_valid_creation(self): + # Create test binding + mock_blob = MockBinding( + name="client", + direction=MockBindingDirection.IN, + data_type=None, + type="cosmosDB", + ) + + # Create test input_types dict + mock_input_types = { + "client": MockParamTypeInfo( + binding_name="cosmosDB", pytype=ContainerProxy + ) + } + + # Create test indexed_function + mock_indexed_functions = MockFunction(bindings=[mock_blob]) + + dict_repr, logs = CosmosClientConverter.get_raw_bindings( + mock_indexed_functions, mock_input_types + ) + + self.assertEqual( + dict_repr, + [ + '{"direction": "MockBindingDirection.IN", ' + '"type": "cosmosDB", ' + '"properties": ' + '{"SupportsDeferredBinding": true}}' + ], + ) + + self.assertEqual(logs, {"client": {ContainerProxy: "True"}}) diff --git a/azurefunctions-extensions-bindings-cosmos/tests/test_databaseproxy.py b/azurefunctions-extensions-bindings-cosmos/tests/test_databaseproxy.py index 4f030a6..3059eb4 100644 --- a/azurefunctions-extensions-bindings-cosmos/tests/test_databaseproxy.py +++ b/azurefunctions-extensions-bindings-cosmos/tests/test_databaseproxy.py @@ -1,319 +1,289 @@ -# # 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.storage.blob import StorageStreamDownloader as SSDSdk -# from azurefunctions.extensions.base import Datum - -# from azurefunctions.extensions.bindings.blob import ( -# BlobClientConverter, -# StorageStreamDownloader, -# ) - - -# # 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 TestStorageStreamDownloader(unittest.TestCase): -# def test_input_type(self): -# check_input_type = BlobClientConverter.check_input_type_annotation -# self.assertTrue(check_input_type(StorageStreamDownloader)) -# self.assertFalse(check_input_type(str)) -# self.assertFalse(check_input_type(bytes)) -# self.assertFalse(check_input_type(bytearray)) - -# def test_input_none(self): -# result = BlobClientConverter.decode( -# data=None, trigger_metadata=None, pytype=StorageStreamDownloader -# ) -# self.assertIsNone(result) - -# datum: Datum = Datum(value=b"string_content", type=None) -# result = BlobClientConverter.decode( -# data=datum, trigger_metadata=None, pytype=StorageStreamDownloader -# ) -# self.assertIsNone(result) - -# def test_input_incorrect_type(self): -# datum: Datum = Datum(value=b"string_content", type="bytearray") -# with self.assertRaises(ValueError): -# BlobClientConverter.decode( -# data=datum, trigger_metadata=None, pytype=StorageStreamDownloader -# ) - -# def test_input_empty(self): -# datum: Datum = Datum(value={}, type="model_binding_data") -# result: StorageStreamDownloader = BlobClientConverter.decode( -# data=datum, trigger_metadata=None, pytype=StorageStreamDownloader -# ) -# self.assertIsNone(result) - -# def test_input_populated(self): -# content = { -# "Connection": "AzureWebJobsStorage", -# "ContainerName": "test-blob", -# "BlobName": "text.txt", -# } - -# sample_mbd = MockMBD( -# version="1.0", -# source="AzureStorageBlobs", -# content_type="application/json", -# content=json.dumps(content), -# ) - -# datum: Datum = Datum(value=sample_mbd, type="model_binding_data") -# result: StorageStreamDownloader = BlobClientConverter.decode( -# data=datum, trigger_metadata=None, pytype=StorageStreamDownloader -# ) - -# self.assertIsNotNone(result) -# self.assertIsInstance(result, SSDSdk) - -# sdk_result = StorageStreamDownloader(data=datum.value).get_sdk_type() - -# self.assertIsNotNone(sdk_result) -# self.assertIsInstance(sdk_result, SSDSdk) - -# def test_invalid_input_populated(self): -# content = { -# "Connection": "NotARealConnectionString", -# "ContainerName": "test-blob", -# "BlobName": "text.txt", -# } - -# sample_mbd = MockMBD( -# version="1.0", -# source="AzureStorageBlobs", -# content_type="application/json", -# content=json.dumps(content), -# ) - -# with self.assertRaises(ValueError) as e: -# datum: Datum = Datum(value=sample_mbd, type="model_binding_data") -# result: StorageStreamDownloader = BlobClientConverter.decode( -# data=datum, trigger_metadata=None, pytype=StorageStreamDownloader -# ) -# 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): -# content = { -# "Connection": None, -# "ContainerName": "test-blob", -# "BlobName": "text.txt", -# } - -# sample_mbd = MockMBD( -# version="1.0", -# source="AzureStorageBlobs", -# content_type="application/json", -# content=json.dumps(content), -# ) - -# with self.assertRaises(ValueError) as e: -# datum: Datum = Datum(value=sample_mbd, type="model_binding_data") -# result: StorageStreamDownloader = BlobClientConverter.decode( -# data=datum, trigger_metadata=None, pytype=StorageStreamDownloader -# ) -# self.assertEqual( -# e.exception.args[0], -# "Storage account connection string cannot be None. Please provide a connection string.", -# ) - -# def test_input_populated_managed_identity_input(self): -# content = { -# "Connection": "input", -# "ContainerName": "test-blob", -# "BlobName": "text.txt", -# } - -# sample_mbd = MockMBD( -# version="1.0", -# source="AzureStorageBlobs", -# content_type="application/json", -# content=json.dumps(content), -# ) - -# datum: Datum = Datum(value=sample_mbd, type="model_binding_data") -# result: StorageStreamDownloader = BlobClientConverter.decode( -# data=datum, trigger_metadata=None, pytype=StorageStreamDownloader -# ) - -# self.assertIsNotNone(result) -# self.assertIsInstance(result, SSDSdk) - -# sdk_result = StorageStreamDownloader(data=datum.value).get_sdk_type() - -# self.assertIsNotNone(sdk_result) -# self.assertIsInstance(sdk_result, SSDSdk) - -# def test_input_populated_managed_identity_trigger(self): -# content = { -# "Connection": "trigger", -# "ContainerName": "test-blob", -# "BlobName": "text.txt", -# } - -# sample_mbd = MockMBD( -# version="1.0", -# source="AzureStorageBlobs", -# content_type="application/json", -# content=json.dumps(content), -# ) - -# datum: Datum = Datum(value=sample_mbd, type="model_binding_data") -# result: StorageStreamDownloader = BlobClientConverter.decode( -# data=datum, trigger_metadata=None, pytype=StorageStreamDownloader -# ) - -# self.assertIsNotNone(result) -# self.assertIsInstance(result, SSDSdk) - -# sdk_result = StorageStreamDownloader(data=datum.value).get_sdk_type() - -# self.assertIsNotNone(sdk_result) -# self.assertIsInstance(sdk_result, SSDSdk) - -# def test_input_invalid_pytype(self): -# content = { -# "Connection": "AzureWebJobsStorage", -# "ContainerName": "test-blob", -# "BlobName": "text.txt", -# } - -# sample_mbd = MockMBD( -# version="1.0", -# source="AzureStorageBlobs", -# content_type="application/json", -# content=json.dumps(content), -# ) - -# datum: Datum = Datum(value=sample_mbd, type="model_binding_data") -# result: StorageStreamDownloader = BlobClientConverter.decode( -# data=datum, trigger_metadata=None, pytype="str" -# ) - -# self.assertIsNone(result) - -# def test_ssd_invalid_creation(self): -# # Create test binding -# mock_blob = MockBinding( -# name="blob", direction=MockBindingDirection.IN, data_type=None, type="blob" -# ) - -# # Create test input_types dict -# mock_input_types = { -# "blob": MockParamTypeInfo(binding_name="blobTrigger", pytype=bytes) -# } - -# # Create test indexed_function -# mock_indexed_functions = MockFunction(bindings=[mock_blob]) - -# dict_repr, logs = BlobClientConverter.get_raw_bindings( -# mock_indexed_functions, mock_input_types -# ) - -# self.assertEqual( -# dict_repr, -# [ -# '{"direction": "MockBindingDirection.IN", ' -# '"type": "blob", ' -# '"properties": ' -# '{"SupportsDeferredBinding": false}}' -# ], -# ) - -# self.assertEqual(logs, {"blob": {bytes: "False"}}) - -# def test_ssd_valid_creation(self): -# # Create test binding -# mock_blob = MockBinding( -# name="client", -# direction=MockBindingDirection.IN, -# data_type=None, -# type="blob", -# ) - -# # Create test input_types dict -# mock_input_types = { -# "client": MockParamTypeInfo( -# binding_name="blobTrigger", pytype=StorageStreamDownloader -# ) -# } - -# # Create test indexed_function -# mock_indexed_functions = MockFunction(bindings=[mock_blob]) - -# dict_repr, logs = BlobClientConverter.get_raw_bindings( -# mock_indexed_functions, mock_input_types -# ) - -# self.assertEqual( -# dict_repr, -# [ -# '{"direction": "MockBindingDirection.IN", ' -# '"type": "blob", ' -# '"properties": ' -# '{"SupportsDeferredBinding": true}}' -# ], -# ) - -# self.assertEqual(logs, {"client": {StorageStreamDownloader: "True"}}) +# 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.cosmos import DatabaseProxy as DatabaseProxySdk +from azurefunctions.extensions.base import Datum + +from azurefunctions.extensions.bindings.cosmos import CosmosClientConverter, DatabaseProxy + + +# 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 TestDatabaseProxy(unittest.TestCase): + def test_input_type(self): + check_input_type = CosmosClientConverter.check_input_type_annotation + self.assertTrue(check_input_type(DatabaseProxy)) + self.assertFalse(check_input_type(str)) + self.assertFalse(check_input_type(bytes)) + self.assertFalse(check_input_type(bytearray)) + + def test_input_none(self): + result = CosmosClientConverter.decode( + data=None, trigger_metadata=None, pytype=DatabaseProxy + ) + self.assertIsNone(result) + + datum: Datum = Datum(value=b"string_content", type=None) + result = CosmosClientConverter.decode( + data=datum, trigger_metadata=None, pytype=DatabaseProxy + ) + self.assertIsNone(result) + + def test_input_incorrect_type(self): + datum: Datum = Datum(value=b"string_content", type="bytearray") + with self.assertRaises(ValueError): + CosmosClientConverter.decode( + data=datum, trigger_metadata=None, pytype=DatabaseProxy + ) + + def test_input_empty(self): + datum: Datum = Datum(value={}, type="model_binding_data") + result: DatabaseProxy = CosmosClientConverter.decode( + data=datum, trigger_metadata=None, pytype=DatabaseProxy + ) + self.assertIsNone(result) + + def test_input_populated(self): + content = { + "DatabaseName": "test-db", + "ContainerName": "test-items", + "Connection": "AzureWebJobsStorage" + } + + sample_mbd = MockMBD( + version="1.0", + source="CosmosDB", + content_type="application/json", + content=json.dumps(content), + ) + + datum: Datum = Datum(value=sample_mbd, type="model_binding_data") + result: DatabaseProxy = CosmosClientConverter.decode( + data=datum, trigger_metadata=None, pytype=DatabaseProxy + ) + + self.assertIsNotNone(result) + self.assertIsInstance(result, DatabaseProxySdk) + + sdk_result = DatabaseProxy(data=datum.value).get_sdk_type() + + self.assertIsNotNone(sdk_result) + self.assertIsInstance(sdk_result, DatabaseProxySdk) + + def test_invalid_input_populated(self): + content = { + "DatabaseName": "test-db", + "ContainerName": "test-items", + "Connection": "NotARealConnectionString" + } + + sample_mbd = MockMBD( + version="1.0", + source="CosmosDB", + content_type="application/json", + content=json.dumps(content), + ) + + with self.assertRaises(ValueError) as e: + datum: Datum = Datum(value=sample_mbd, type="model_binding_data") + result: DatabaseProxy = CosmosClientConverter.decode( + data=datum, trigger_metadata=None, pytype=DatabaseProxy + ) + 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): + content = { + "DatabaseName": "test-db", + "ContainerName": "test-items", + "Connection": None + } + + sample_mbd = MockMBD( + version="1.0", + source="CosmosDB", + content_type="application/json", + content=json.dumps(content), + ) + + with self.assertRaises(ValueError) as e: + datum: Datum = Datum(value=sample_mbd, type="model_binding_data") + result: DatabaseProxy = CosmosClientConverter.decode( + data=datum, trigger_metadata=None, pytype=DatabaseProxy + ) + self.assertEqual( + e.exception.args[0], + "Storage account connection string cannot be None. Please provide a connection string.", + ) + + def test_input_populated_managed_identity_input(self): + content = { + "DatabaseName": "test-db", + "ContainerName": "test-items", + "Connection": "input" + } + + sample_mbd = MockMBD( + version="1.0", + source="CosmosDB", + content_type="application/json", + content=json.dumps(content), + ) + + datum: Datum = Datum(value=sample_mbd, type="model_binding_data") + result: DatabaseProxy = CosmosClientConverter.decode( + data=datum, trigger_metadata=None, pytype=DatabaseProxy + ) + + self.assertIsNotNone(result) + self.assertIsInstance(result, DatabaseProxySdk) + + sdk_result = DatabaseProxy(data=datum.value).get_sdk_type() + + self.assertIsNotNone(sdk_result) + self.assertIsInstance(sdk_result, DatabaseProxySdk) + + def test_input_invalid_pytype(self): + content = { + "DatabaseName": "test-db", + "ContainerName": "test-items", + "Connection": "AzureWebJobsStorage" + } + + sample_mbd = MockMBD( + version="1.0", + source="CosmosDB", + content_type="application/json", + content=json.dumps(content), + ) + + datum: Datum = Datum(value=sample_mbd, type="model_binding_data") + result: DatabaseProxy = CosmosClientConverter.decode( + data=datum, trigger_metadata=None, pytype="str" + ) + + self.assertIsNone(result) + + def test_database_proxy_invalid_creation(self): + # Create test binding + mock_blob = MockBinding( + name="cosmosDB", direction=MockBindingDirection.IN, data_type=None, type="cosmosDB" + ) + + # Create test input_types dict + mock_input_types = { + "cosmosDB": MockParamTypeInfo(binding_name="cosmosDB", pytype=bytes) + } + + # Create test indexed_function + mock_indexed_functions = MockFunction(bindings=[mock_blob]) + + dict_repr, logs = CosmosClientConverter.get_raw_bindings( + mock_indexed_functions, mock_input_types + ) + + self.assertEqual( + dict_repr, + [ + '{"direction": "MockBindingDirection.IN", ' + '"type": "cosmosDB", ' + '"properties": ' + '{"SupportsDeferredBinding": false}}' + ], + ) + + self.assertEqual(logs, {"cosmosDB": {bytes: "False"}}) + + def test_database_proxy_valid_creation(self): + # Create test binding + mock_blob = MockBinding( + name="client", + direction=MockBindingDirection.IN, + data_type=None, + type="cosmosDB", + ) + + # Create test input_types dict + mock_input_types = { + "client": MockParamTypeInfo( + binding_name="cosmosDB", pytype=DatabaseProxy + ) + } + + # Create test indexed_function + mock_indexed_functions = MockFunction(bindings=[mock_blob]) + + dict_repr, logs = CosmosClientConverter.get_raw_bindings( + mock_indexed_functions, mock_input_types + ) + + self.assertEqual( + dict_repr, + [ + '{"direction": "MockBindingDirection.IN", ' + '"type": "cosmosDB", ' + '"properties": ' + '{"SupportsDeferredBinding": true}}' + ], + ) + + self.assertEqual(logs, {"client": {DatabaseProxy: "True"}}) From 3e9c8b712614bd66bc899ffb7f975e76c3d09aad Mon Sep 17 00:00:00 2001 From: Evan Roman Date: Mon, 14 Apr 2025 11:19:57 -0500 Subject: [PATCH 05/12] Fix --- .../README.md | 34 ++++++++----------- .../samples/README.md | 2 +- 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/azurefunctions-extensions-bindings-cosmos/README.md b/azurefunctions-extensions-bindings-cosmos/README.md index d38e335..c6209d7 100644 --- a/azurefunctions-extensions-bindings-cosmos/README.md +++ b/azurefunctions-extensions-bindings-cosmos/README.md @@ -53,25 +53,21 @@ properties and methods available as seen in the Azure Storage Cosmos library for ```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.cosmos as cosmos + +app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) + +@app.route(route="cosmos") +@app.cosmos_db_input(arg_name="container", + connection="AzureWebJobsStorage", + database_name="db_name", + container_name="container_name") +def get_docs(req: func.HttpRequest, client: cosmos.CosmosClient): + databases = client.list_databases() + for db in databases: + logging.info(f"Found database with ID: {db.get('id')}") + + return "ok" ``` ## Troubleshooting diff --git a/azurefunctions-extensions-bindings-cosmos/samples/README.md b/azurefunctions-extensions-bindings-cosmos/samples/README.md index 7f8525f..964b715 100644 --- a/azurefunctions-extensions-bindings-cosmos/samples/README.md +++ b/azurefunctions-extensions-bindings-cosmos/samples/README.md @@ -34,7 +34,7 @@ examples on how to use the Azure Storage Cosmos client library, please see [Azur ## 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 Blob library for Python with [pip](https://pypi.org/project/pip/): +2. Install the Azure Functions Extension Cosmos library for Python with [pip](https://pypi.org/project/pip/): ```bash pip install azurefunctions-extensions-bindings-cosmos From 9c42d1bbf895ec25734a83646faef8bb1777f038 Mon Sep 17 00:00:00 2001 From: Evan Roman Date: Mon, 14 Apr 2025 11:31:34 -0500 Subject: [PATCH 06/12] Chng docs --- .../extensions/bindings/cosmos/containerProxy.py | 4 +--- .../extensions/bindings/cosmos/cosmosClient.py | 5 +---- .../extensions/bindings/cosmos/databaseProxy.py | 4 +--- 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/azurefunctions-extensions-bindings-cosmos/azurefunctions/extensions/bindings/cosmos/containerProxy.py b/azurefunctions-extensions-bindings-cosmos/azurefunctions/extensions/bindings/cosmos/containerProxy.py index 67ad604..4f2cd40 100644 --- a/azurefunctions-extensions-bindings-cosmos/azurefunctions/extensions/bindings/cosmos/containerProxy.py +++ b/azurefunctions-extensions-bindings-cosmos/azurefunctions/extensions/bindings/cosmos/containerProxy.py @@ -38,9 +38,7 @@ def __init__(self, *, data: Datum) -> None: def get_sdk_type(self) -> ContainerProxySdk: """ - When using Managed Identity, the only way to create a BlobClient is - through a BlobServiceClient. There are two ways to create a - BlobServiceClient: + There are two ways to create a CosmosClient: 1. Through the constructor: this is the only option when using Managed Identity 2. Through from_connection_string: this is the only option when not using Managed Identity diff --git a/azurefunctions-extensions-bindings-cosmos/azurefunctions/extensions/bindings/cosmos/cosmosClient.py b/azurefunctions-extensions-bindings-cosmos/azurefunctions/extensions/bindings/cosmos/cosmosClient.py index 518d796..f5ec4d9 100644 --- a/azurefunctions-extensions-bindings-cosmos/azurefunctions/extensions/bindings/cosmos/cosmosClient.py +++ b/azurefunctions-extensions-bindings-cosmos/azurefunctions/extensions/bindings/cosmos/cosmosClient.py @@ -20,7 +20,6 @@ def __init__(self, *, data: Datum) -> None: self._container_name = None self._connection = None self._using_managed_identity = False - self._id = None self._partition_key = None self._sql_query = None self._preferred_locations = None @@ -39,9 +38,7 @@ def __init__(self, *, data: Datum) -> None: def get_sdk_type(self) -> CosmosClientSdk: """ - When using Managed Identity, the only way to create a BlobClient is - through a BlobServiceClient. There are two ways to create a - BlobServiceClient: + There are two ways to create a CosmosClient: 1. Through the constructor: this is the only option when using Managed Identity 2. Through from_connection_string: this is the only option when not using Managed Identity diff --git a/azurefunctions-extensions-bindings-cosmos/azurefunctions/extensions/bindings/cosmos/databaseProxy.py b/azurefunctions-extensions-bindings-cosmos/azurefunctions/extensions/bindings/cosmos/databaseProxy.py index 816fd19..0053fbe 100644 --- a/azurefunctions-extensions-bindings-cosmos/azurefunctions/extensions/bindings/cosmos/databaseProxy.py +++ b/azurefunctions-extensions-bindings-cosmos/azurefunctions/extensions/bindings/cosmos/databaseProxy.py @@ -38,9 +38,7 @@ def __init__(self, *, data: Datum) -> None: def get_sdk_type(self) -> DatabaseProxySdk: """ - When using Managed Identity, the only way to create a BlobClient is - through a BlobServiceClient. There are two ways to create a - BlobServiceClient: + There are two ways to create a CosmosClient: 1. Through the constructor: this is the only option when using Managed Identity 2. Through from_connection_string: this is the only option when not using Managed Identity From 64dde80db522552277cbf494e7810cc05c8e1696 Mon Sep 17 00:00:00 2001 From: Evan Roman Date: Mon, 14 Apr 2025 11:36:03 -0500 Subject: [PATCH 07/12] Chng test --- .../cosmos_samples_cosmosclient/requirements.txt | 2 +- .../cosmos_samples_databaseproxy/requirements.txt | 2 +- .../tests/test_containerproxy.py | 10 +++++----- .../tests/test_cosmosclient.py | 12 ++++++------ .../tests/test_databaseproxy.py | 8 ++++---- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_cosmosclient/requirements.txt b/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_cosmosclient/requirements.txt index abd6bc4..b4b4870 100644 --- a/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_cosmosclient/requirements.txt +++ b/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_cosmosclient/requirements.txt @@ -3,4 +3,4 @@ # Manually managing azure-functions-worker may cause unexpected issues azure-functions -azurefunctions-extensions-bindings-blob \ No newline at end of file +azurefunctions-extensions-bindings-cosmos \ No newline at end of file diff --git a/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_databaseproxy/requirements.txt b/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_databaseproxy/requirements.txt index abd6bc4..b4b4870 100644 --- a/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_databaseproxy/requirements.txt +++ b/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_databaseproxy/requirements.txt @@ -3,4 +3,4 @@ # Manually managing azure-functions-worker may cause unexpected issues azure-functions -azurefunctions-extensions-bindings-blob \ No newline at end of file +azurefunctions-extensions-bindings-cosmos \ No newline at end of file diff --git a/azurefunctions-extensions-bindings-cosmos/tests/test_containerproxy.py b/azurefunctions-extensions-bindings-cosmos/tests/test_containerproxy.py index 742c72c..cc239e3 100644 --- a/azurefunctions-extensions-bindings-cosmos/tests/test_containerproxy.py +++ b/azurefunctions-extensions-bindings-cosmos/tests/test_containerproxy.py @@ -105,7 +105,7 @@ def test_input_populated(self): "ContainerName": "test-items", "Connection": "AzureWebJobsStorage" } - + sample_mbd = MockMBD( version="1.0", source="CosmosDB", @@ -224,7 +224,7 @@ def test_input_invalid_pytype(self): def test_container_proxy_invalid_creation(self): # Create test binding - mock_blob = MockBinding( + mock_cosmos = MockBinding( name="cosmosDB", direction=MockBindingDirection.IN, data_type=None, type="cosmosDB" ) @@ -234,7 +234,7 @@ def test_container_proxy_invalid_creation(self): } # Create test indexed_function - mock_indexed_functions = MockFunction(bindings=[mock_blob]) + mock_indexed_functions = MockFunction(bindings=[mock_cosmos]) dict_repr, logs = CosmosClientConverter.get_raw_bindings( mock_indexed_functions, mock_input_types @@ -254,7 +254,7 @@ def test_container_proxy_invalid_creation(self): def test_container_proxy_valid_creation(self): # Create test binding - mock_blob = MockBinding( + mock_cosmos = MockBinding( name="client", direction=MockBindingDirection.IN, data_type=None, @@ -269,7 +269,7 @@ def test_container_proxy_valid_creation(self): } # Create test indexed_function - mock_indexed_functions = MockFunction(bindings=[mock_blob]) + mock_indexed_functions = MockFunction(bindings=[mock_cosmos]) dict_repr, logs = CosmosClientConverter.get_raw_bindings( mock_indexed_functions, mock_input_types diff --git a/azurefunctions-extensions-bindings-cosmos/tests/test_cosmosclient.py b/azurefunctions-extensions-bindings-cosmos/tests/test_cosmosclient.py index 6b98d3a..9085741 100644 --- a/azurefunctions-extensions-bindings-cosmos/tests/test_cosmosclient.py +++ b/azurefunctions-extensions-bindings-cosmos/tests/test_cosmosclient.py @@ -65,7 +65,7 @@ def __init__(self, bindings: MockBinding): self._bindings = bindings -class TestBlobClient(unittest.TestCase): +class TestCosmosClient(unittest.TestCase): def test_input_type(self): check_input_type = CosmosClientConverter.check_input_type_annotation self.assertTrue(check_input_type(CosmosClient)) @@ -226,7 +226,7 @@ def test_input_invalid_pytype(self): def test_cosmos_client_invalid_creation(self): # Create test binding - mock_blob = MockBinding( + mock_cosmos = MockBinding( name="cosmosDB", direction=MockBindingDirection.IN, data_type=None, type="cosmosDB" ) @@ -236,7 +236,7 @@ def test_cosmos_client_invalid_creation(self): } # Create test indexed_function - mock_indexed_functions = MockFunction(bindings=[mock_blob]) + mock_indexed_functions = MockFunction(bindings=[mock_cosmos]) dict_repr, logs = CosmosClientConverter.get_raw_bindings( mock_indexed_functions, mock_input_types @@ -254,9 +254,9 @@ def test_cosmos_client_invalid_creation(self): self.assertEqual(logs, {"cosmosDB": {bytes: "False"}}) - def test_blob_client_valid_creation(self): + def test_cosmos_client_valid_creation(self): # Create test binding - mock_blob = MockBinding( + mock_cosmos = MockBinding( name="client", direction=MockBindingDirection.IN, data_type=None, @@ -269,7 +269,7 @@ def test_blob_client_valid_creation(self): } # Create test indexed_function - mock_indexed_functions = MockFunction(bindings=[mock_blob]) + mock_indexed_functions = MockFunction(bindings=[mock_cosmos]) dict_repr, logs = CosmosClientConverter.get_raw_bindings( mock_indexed_functions, mock_input_types diff --git a/azurefunctions-extensions-bindings-cosmos/tests/test_databaseproxy.py b/azurefunctions-extensions-bindings-cosmos/tests/test_databaseproxy.py index 3059eb4..207c4eb 100644 --- a/azurefunctions-extensions-bindings-cosmos/tests/test_databaseproxy.py +++ b/azurefunctions-extensions-bindings-cosmos/tests/test_databaseproxy.py @@ -225,7 +225,7 @@ def test_input_invalid_pytype(self): def test_database_proxy_invalid_creation(self): # Create test binding - mock_blob = MockBinding( + mock_cosmos = MockBinding( name="cosmosDB", direction=MockBindingDirection.IN, data_type=None, type="cosmosDB" ) @@ -235,7 +235,7 @@ def test_database_proxy_invalid_creation(self): } # Create test indexed_function - mock_indexed_functions = MockFunction(bindings=[mock_blob]) + mock_indexed_functions = MockFunction(bindings=[mock_cosmos]) dict_repr, logs = CosmosClientConverter.get_raw_bindings( mock_indexed_functions, mock_input_types @@ -255,7 +255,7 @@ def test_database_proxy_invalid_creation(self): def test_database_proxy_valid_creation(self): # Create test binding - mock_blob = MockBinding( + mock_cosmos = MockBinding( name="client", direction=MockBindingDirection.IN, data_type=None, @@ -270,7 +270,7 @@ def test_database_proxy_valid_creation(self): } # Create test indexed_function - mock_indexed_functions = MockFunction(bindings=[mock_blob]) + mock_indexed_functions = MockFunction(bindings=[mock_cosmos]) dict_repr, logs = CosmosClientConverter.get_raw_bindings( mock_indexed_functions, mock_input_types From e37aa73ed1c4db70fbfcc4488838b60eeded7a7e Mon Sep 17 00:00:00 2001 From: Evan Roman Date: Wed, 23 Apr 2025 14:47:04 -0500 Subject: [PATCH 08/12] Rename --- .../CHANGELOG.md | 0 .../LICENSE | 0 .../MANIFEST.in | 0 .../README.md | 48 +++++++++---------- .../azurefunctions/__init__.py | 0 .../azurefunctions/extensions/__init__.py | 0 .../extensions/bindings/__init__.py | 0 .../extensions/bindings/cosmosdb}/__init__.py | 0 .../bindings/cosmosdb}/containerProxy.py | 0 .../bindings/cosmosdb}/cosmosClient.py | 0 .../cosmosdb}/cosmosClientConverter.py | 0 .../bindings/cosmosdb}/databaseProxy.py | 0 .../extensions/bindings/cosmosdb}/utils.py | 0 .../pyproject.toml | 6 +-- .../samples/README.md | 30 ++++++------ .../function_app.py | 8 ++-- .../host.json | 0 .../local.settings.json | 2 +- .../requirements.txt | 2 +- .../function_app.py | 8 ++-- .../cosmosdb_samples_cosmosclient}/host.json | 0 .../local.settings.json | 2 +- .../requirements.txt | 2 +- .../function_app.py | 8 ++-- .../cosmosdb_samples_databaseproxy}/host.json | 0 .../local.settings.json | 2 +- .../requirements.txt | 2 +- .../tests/__init__.py | 0 .../tests/test_code_quality.py | 4 +- .../tests/test_containerproxy.py | 2 +- .../tests/test_cosmosclient.py | 2 +- .../tests/test_databaseproxy.py | 2 +- ...cosmos-tests.yml => ci-cosmosdb-tests.yml} | 4 +- eng/ci/official-build.yml | 4 +- eng/templates/jobs/build.yml | 6 +-- .../official/jobs/build-artifacts.yml | 6 +-- ...unit-tests.yml => cosmosdb-unit-tests.yml} | 8 ++-- 37 files changed, 79 insertions(+), 79 deletions(-) delete mode 100644 azurefunctions-extensions-bindings-cosmos/CHANGELOG.md rename {azurefunctions-extensions-bindings-cosmos => azurefunctions-extensions-bindings-cosmosdb}/LICENSE (100%) rename {azurefunctions-extensions-bindings-cosmos => azurefunctions-extensions-bindings-cosmosdb}/MANIFEST.in (100%) rename {azurefunctions-extensions-bindings-cosmos => azurefunctions-extensions-bindings-cosmosdb}/README.md (66%) rename {azurefunctions-extensions-bindings-cosmos => azurefunctions-extensions-bindings-cosmosdb}/azurefunctions/__init__.py (100%) rename {azurefunctions-extensions-bindings-cosmos => azurefunctions-extensions-bindings-cosmosdb}/azurefunctions/extensions/__init__.py (100%) rename {azurefunctions-extensions-bindings-cosmos => azurefunctions-extensions-bindings-cosmosdb}/azurefunctions/extensions/bindings/__init__.py (100%) rename {azurefunctions-extensions-bindings-cosmos/azurefunctions/extensions/bindings/cosmos => azurefunctions-extensions-bindings-cosmosdb/azurefunctions/extensions/bindings/cosmosdb}/__init__.py (100%) rename {azurefunctions-extensions-bindings-cosmos/azurefunctions/extensions/bindings/cosmos => azurefunctions-extensions-bindings-cosmosdb/azurefunctions/extensions/bindings/cosmosdb}/containerProxy.py (100%) rename {azurefunctions-extensions-bindings-cosmos/azurefunctions/extensions/bindings/cosmos => azurefunctions-extensions-bindings-cosmosdb/azurefunctions/extensions/bindings/cosmosdb}/cosmosClient.py (100%) rename {azurefunctions-extensions-bindings-cosmos/azurefunctions/extensions/bindings/cosmos => azurefunctions-extensions-bindings-cosmosdb/azurefunctions/extensions/bindings/cosmosdb}/cosmosClientConverter.py (100%) rename {azurefunctions-extensions-bindings-cosmos/azurefunctions/extensions/bindings/cosmos => azurefunctions-extensions-bindings-cosmosdb/azurefunctions/extensions/bindings/cosmosdb}/databaseProxy.py (100%) rename {azurefunctions-extensions-bindings-cosmos/azurefunctions/extensions/bindings/cosmos => azurefunctions-extensions-bindings-cosmosdb/azurefunctions/extensions/bindings/cosmosdb}/utils.py (100%) rename {azurefunctions-extensions-bindings-cosmos => azurefunctions-extensions-bindings-cosmosdb}/pyproject.toml (86%) rename {azurefunctions-extensions-bindings-cosmos => azurefunctions-extensions-bindings-cosmosdb}/samples/README.md (56%) rename {azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_containerproxy => azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_containerproxy}/function_app.py (86%) rename {azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_containerproxy => azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_containerproxy}/host.json (100%) rename {azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_containerproxy => azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_containerproxy}/local.settings.json (60%) rename {azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_containerproxy => azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_containerproxy}/requirements.txt (82%) rename {azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_cosmosclient => azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_cosmosclient}/function_app.py (82%) rename {azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_cosmosclient => azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_cosmosclient}/host.json (100%) rename {azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_cosmosclient => azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_cosmosclient}/local.settings.json (60%) rename {azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_cosmosclient => azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_cosmosclient}/requirements.txt (82%) rename {azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_databaseproxy => azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_databaseproxy}/function_app.py (82%) rename {azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_databaseproxy => azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_databaseproxy}/host.json (100%) rename {azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_databaseproxy => azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_databaseproxy}/local.settings.json (60%) rename {azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_databaseproxy => azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_databaseproxy}/requirements.txt (82%) rename {azurefunctions-extensions-bindings-cosmos => azurefunctions-extensions-bindings-cosmosdb}/tests/__init__.py (100%) rename {azurefunctions-extensions-bindings-cosmos => azurefunctions-extensions-bindings-cosmosdb}/tests/test_code_quality.py (92%) rename {azurefunctions-extensions-bindings-cosmos => azurefunctions-extensions-bindings-cosmosdb}/tests/test_containerproxy.py (98%) rename {azurefunctions-extensions-bindings-cosmos => azurefunctions-extensions-bindings-cosmosdb}/tests/test_cosmosclient.py (98%) rename {azurefunctions-extensions-bindings-cosmos => azurefunctions-extensions-bindings-cosmosdb}/tests/test_databaseproxy.py (98%) rename eng/ci/{ci-cosmos-tests.yml => ci-cosmosdb-tests.yml} (84%) rename eng/templates/official/jobs/{cosmos-unit-tests.yml => cosmosdb-unit-tests.yml} (77%) diff --git a/azurefunctions-extensions-bindings-cosmos/CHANGELOG.md b/azurefunctions-extensions-bindings-cosmos/CHANGELOG.md deleted file mode 100644 index e69de29..0000000 diff --git a/azurefunctions-extensions-bindings-cosmos/LICENSE b/azurefunctions-extensions-bindings-cosmosdb/LICENSE similarity index 100% rename from azurefunctions-extensions-bindings-cosmos/LICENSE rename to azurefunctions-extensions-bindings-cosmosdb/LICENSE diff --git a/azurefunctions-extensions-bindings-cosmos/MANIFEST.in b/azurefunctions-extensions-bindings-cosmosdb/MANIFEST.in similarity index 100% rename from azurefunctions-extensions-bindings-cosmos/MANIFEST.in rename to azurefunctions-extensions-bindings-cosmosdb/MANIFEST.in diff --git a/azurefunctions-extensions-bindings-cosmos/README.md b/azurefunctions-extensions-bindings-cosmosdb/README.md similarity index 66% rename from azurefunctions-extensions-bindings-cosmos/README.md rename to azurefunctions-extensions-bindings-cosmosdb/README.md index c6209d7..1cee615 100644 --- a/azurefunctions-extensions-bindings-cosmos/README.md +++ b/azurefunctions-extensions-bindings-cosmosdb/README.md @@ -1,16 +1,16 @@ -# Azure Functions Extensions Bindings Cosmos library for Python -This library allows Cosmos Input bindings in Python Function Apps to recognize and bind to client types from the -Azure Cosmos sdk. +# Azure Functions Extensions Bindings Cosmos DB library for Python +This library allows Cosmos DB Input bindings in Python Function Apps to recognize and bind to client types from the +Azure Cosmos DB SDK. -Cosmos client types can be generated from: +Cosmos DB client types can be generated from: -* Cosmos Input +* Cosmos DB Input -[Source code](https://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-bindings-cosmos) -[Package (PyPi)](https://pypi.org/project/azurefunctions-extensions-bindings-cosmos/) +[Source code](https://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-bindings-cosmosdb) +[Package (PyPi)](https://pypi.org/project/azurefunctions-extensions-bindings-cosmosdb/) | API reference documentation | Product documentation -| [Samples](hhttps://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-bindings-cosmos/samples) +| [Samples](hhttps://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-bindings-cosmosdb/samples) ## Getting started @@ -22,10 +22,10 @@ Cosmos 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 Cosmos library for Python with pip: +Install the Azure Functions Extensions Bindings Cosmos DB library for Python with pip: ```bash -pip install azurefunctions-extensions-bindings-cosmos +pip install azurefunctions-extensions-bindings-cosmosdb ``` ### Create a storage account @@ -44,22 +44,22 @@ az storage account create -n my-storage-account-name -g my-resource-group ``` ### Bind to the SDK-type -The Azure Functions Extensions Bindings Cosmos library for Python allows you to create a function app with -Cosmos Input and define the type as a CosmosClient, DatabaseProxy, or ContainerProxy. Instead of receiving +The Azure Functions Extensions Bindings Cosmos DB library for Python allows you to create a function app with +Cosmos DB Input and define the type as a CosmosClient, DatabaseProxy, or ContainerProxy. 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 Cosmos library for Python. +properties and methods available as seen in the Azure Storage Cosmos DB library for Python. ```python import logging import azure.functions as func -import azurefunctions.extensions.bindings.cosmos as cosmos +import azurefunctions.extensions.bindings.cosmosdb as cosmos app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) @app.route(route="cosmos") @app.cosmos_db_input(arg_name="container", - connection="AzureWebJobsStorage", + connection="CosmosDBConnection", database_name="db_name", container_name="container_name") def get_docs(req: func.HttpRequest, client: cosmos.CosmosClient): @@ -80,21 +80,21 @@ This list can be used for reference to catch thrown exceptions. To get the speci ### More sample code -Get started with our [Cosmos samples](hhttps://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-bindings-cosmos/samples). +Get started with our [Cosmos DB samples](hhttps://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-bindings-cosmosdb/samples). -Several samples are available in this GitHub repository. These samples provide example code for additional scenarios commonly encountered while working with Cosmos: +Several samples are available in this GitHub repository. These samples provide example code for additional scenarios commonly encountered while working with Cosmos DB: -* [cosmos_samples_cosmosclient](https://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-binding-cosmos/samples/cosmos_samples_cosmosclient) - Examples for using the CosmosClient type: - * From CosmosInput +* [cosmosdb_samples_cosmosclient](https://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-binding-cosmosdb/samples/cosmosdb_samples_cosmosclient) - Examples for using the CosmosClient type: + * From CosmosDBInput -* [cosmos_samples_databaseproxy](https://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_databaseproxy) - Examples for using the DatabaseProxy type: - * From CosmosInput +* [cosmosdb_samples_databaseproxy](https://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_databaseproxy) - Examples for using the DatabaseProxy type: + * From CosmosDBInput -* [cosmos_samples_containerclient](https://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_containerproxy) - Examples for using the ContainerProxy type: - * From CosmosInput +* [cosmosdb_samples_containerclient](https://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_containerproxy) - Examples for using the ContainerProxy type: + * From CosmosDBInput ### Additional documentation -For more information on the Azure Cosmos SDK, see the [Azure Cosmos DB documentation](https://learn.microsoft.com/en-us/azure/cosmos-db/) on learn.microsoft.com +For more information on the Azure Cosmos DB SDK, see the [Azure Cosmos DB documentation](https://learn.microsoft.com/en-us/azure/cosmos-db/) on learn.microsoft.com and the [Azure Cosmos DB README](https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/cosmos/azure-cosmos). ## Contributing diff --git a/azurefunctions-extensions-bindings-cosmos/azurefunctions/__init__.py b/azurefunctions-extensions-bindings-cosmosdb/azurefunctions/__init__.py similarity index 100% rename from azurefunctions-extensions-bindings-cosmos/azurefunctions/__init__.py rename to azurefunctions-extensions-bindings-cosmosdb/azurefunctions/__init__.py diff --git a/azurefunctions-extensions-bindings-cosmos/azurefunctions/extensions/__init__.py b/azurefunctions-extensions-bindings-cosmosdb/azurefunctions/extensions/__init__.py similarity index 100% rename from azurefunctions-extensions-bindings-cosmos/azurefunctions/extensions/__init__.py rename to azurefunctions-extensions-bindings-cosmosdb/azurefunctions/extensions/__init__.py diff --git a/azurefunctions-extensions-bindings-cosmos/azurefunctions/extensions/bindings/__init__.py b/azurefunctions-extensions-bindings-cosmosdb/azurefunctions/extensions/bindings/__init__.py similarity index 100% rename from azurefunctions-extensions-bindings-cosmos/azurefunctions/extensions/bindings/__init__.py rename to azurefunctions-extensions-bindings-cosmosdb/azurefunctions/extensions/bindings/__init__.py diff --git a/azurefunctions-extensions-bindings-cosmos/azurefunctions/extensions/bindings/cosmos/__init__.py b/azurefunctions-extensions-bindings-cosmosdb/azurefunctions/extensions/bindings/cosmosdb/__init__.py similarity index 100% rename from azurefunctions-extensions-bindings-cosmos/azurefunctions/extensions/bindings/cosmos/__init__.py rename to azurefunctions-extensions-bindings-cosmosdb/azurefunctions/extensions/bindings/cosmosdb/__init__.py diff --git a/azurefunctions-extensions-bindings-cosmos/azurefunctions/extensions/bindings/cosmos/containerProxy.py b/azurefunctions-extensions-bindings-cosmosdb/azurefunctions/extensions/bindings/cosmosdb/containerProxy.py similarity index 100% rename from azurefunctions-extensions-bindings-cosmos/azurefunctions/extensions/bindings/cosmos/containerProxy.py rename to azurefunctions-extensions-bindings-cosmosdb/azurefunctions/extensions/bindings/cosmosdb/containerProxy.py diff --git a/azurefunctions-extensions-bindings-cosmos/azurefunctions/extensions/bindings/cosmos/cosmosClient.py b/azurefunctions-extensions-bindings-cosmosdb/azurefunctions/extensions/bindings/cosmosdb/cosmosClient.py similarity index 100% rename from azurefunctions-extensions-bindings-cosmos/azurefunctions/extensions/bindings/cosmos/cosmosClient.py rename to azurefunctions-extensions-bindings-cosmosdb/azurefunctions/extensions/bindings/cosmosdb/cosmosClient.py diff --git a/azurefunctions-extensions-bindings-cosmos/azurefunctions/extensions/bindings/cosmos/cosmosClientConverter.py b/azurefunctions-extensions-bindings-cosmosdb/azurefunctions/extensions/bindings/cosmosdb/cosmosClientConverter.py similarity index 100% rename from azurefunctions-extensions-bindings-cosmos/azurefunctions/extensions/bindings/cosmos/cosmosClientConverter.py rename to azurefunctions-extensions-bindings-cosmosdb/azurefunctions/extensions/bindings/cosmosdb/cosmosClientConverter.py diff --git a/azurefunctions-extensions-bindings-cosmos/azurefunctions/extensions/bindings/cosmos/databaseProxy.py b/azurefunctions-extensions-bindings-cosmosdb/azurefunctions/extensions/bindings/cosmosdb/databaseProxy.py similarity index 100% rename from azurefunctions-extensions-bindings-cosmos/azurefunctions/extensions/bindings/cosmos/databaseProxy.py rename to azurefunctions-extensions-bindings-cosmosdb/azurefunctions/extensions/bindings/cosmosdb/databaseProxy.py diff --git a/azurefunctions-extensions-bindings-cosmos/azurefunctions/extensions/bindings/cosmos/utils.py b/azurefunctions-extensions-bindings-cosmosdb/azurefunctions/extensions/bindings/cosmosdb/utils.py similarity index 100% rename from azurefunctions-extensions-bindings-cosmos/azurefunctions/extensions/bindings/cosmos/utils.py rename to azurefunctions-extensions-bindings-cosmosdb/azurefunctions/extensions/bindings/cosmosdb/utils.py diff --git a/azurefunctions-extensions-bindings-cosmos/pyproject.toml b/azurefunctions-extensions-bindings-cosmosdb/pyproject.toml similarity index 86% rename from azurefunctions-extensions-bindings-cosmos/pyproject.toml rename to azurefunctions-extensions-bindings-cosmosdb/pyproject.toml index 841ceeb..0683fac 100644 --- a/azurefunctions-extensions-bindings-cosmos/pyproject.toml +++ b/azurefunctions-extensions-bindings-cosmosdb/pyproject.toml @@ -3,11 +3,11 @@ requires = ["setuptools >= 61.0"] build-backend = "setuptools.build_meta" [project] -name = "azurefunctions-extensions-bindings-cosmos" +name = "azurefunctions-extensions-bindings-cosmosdb" dynamic = ["version"] requires-python = ">=3.9" authors = [{ name = "Azure Functions team at Microsoft Corp.", email = "azurefunctions@microsoft.com"}] -description = "Cosmos Python worker extension for Azure Functions." +description = "Cosmos DB Python worker extension for Azure Functions." readme = "README.md" license = {text = "MIT License"} classifiers= [ @@ -41,7 +41,7 @@ dev = [ ] [tool.setuptools.dynamic] -version = {attr = "azurefunctions.extensions.bindings.cosmos.__version__"} +version = {attr = "azurefunctions.extensions.bindings.cosmosdb.__version__"} [tool.setuptools.packages.find] exclude = [ diff --git a/azurefunctions-extensions-bindings-cosmos/samples/README.md b/azurefunctions-extensions-bindings-cosmosdb/samples/README.md similarity index 56% rename from azurefunctions-extensions-bindings-cosmos/samples/README.md rename to azurefunctions-extensions-bindings-cosmosdb/samples/README.md index 964b715..b9996af 100644 --- a/azurefunctions-extensions-bindings-cosmos/samples/README.md +++ b/azurefunctions-extensions-bindings-cosmosdb/samples/README.md @@ -6,25 +6,25 @@ products: - azure - azure-functions - azure-functions-extensions - - azurefunctions-extensions-bindings-cosmos -urlFragment: extension-cosmos-samples + - azurefunctions-extensions-bindings-cosmosdb +urlFragment: extension-cosmosdb-samples --- -# Azure Functions Extension Cosmos library for Python samples +# Azure Functions Extension Cosmos DB library for Python samples -These are code samples that show common scenario operations with the Azure Functions Extension Cosmos library. +These are code samples that show common scenario operations with the Azure Functions Extension Cosmos DB library. -These samples relate to the Azure Storage Cosmos client library being used as part of a Python Function App. For -examples on how to use the Azure Storage Cosmos client library, please see [Azure Cosmos samples](https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/cosmos/azure-cosmos/samples) +These samples relate to the Azure Storage Cosmos DB client library being used as part of a Python Function App. For +examples on how to use the Azure Storage Cosmos DB client library, please see [Azure Cosmos samples](https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/cosmos/azure-cosmos/samples) -* [cosmos_samples_cosmosclient](https://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-binding-cosmos/samples/cosmos_samples_cosmosclient) - Examples for using the CosmosClient type: - * From CosmosInput +* [cosmosdb_samples_cosmosclient](https://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-binding-cosmosdb/samples/cosmosdb_samples_cosmosclient) - Examples for using the CosmosClient type: + * From CosmosDBInput -* [cosmos_samples_databaseproxy](https://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_databaseproxy) - Examples for using the DatabaseProxy type: - * From CosmosInput +* [cosmosdb_samples_databaseproxy](https://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_databaseproxy) - Examples for using the DatabaseProxy type: + * From CosmosDBInput -* [cosmos_samples_containerclient](https://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_containerproxy) - Examples for using the ContainerProxy type: - * From CosmosInput +* [cosmosdb_samples_containerclient](https://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_containerproxy) - Examples for using the ContainerProxy type: + * From CosmosDBInput ## 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). @@ -34,10 +34,10 @@ examples on how to use the Azure Storage Cosmos client library, please see [Azur ## 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 Cosmos library for Python with [pip](https://pypi.org/project/pip/): +2. Install the Azure Functions Extension Cosmos DB library for Python with [pip](https://pypi.org/project/pip/): ```bash -pip install azurefunctions-extensions-bindings-cosmos +pip install azurefunctions-extensions-bindings-cosmosdb ``` 3. Clone or download this sample repository @@ -62,4 +62,4 @@ based on the type of function you wish to execute. 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://learn.microsoft.com/en-us/python/api/azure-cosmos/azure.cosmos?view=azure-python) to learn more about -what you can do with the Azure Cosmos client library. \ No newline at end of file +what you can do with the Azure Cosmos DB client library. \ No newline at end of file diff --git a/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_containerproxy/function_app.py b/azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_containerproxy/function_app.py similarity index 86% rename from azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_containerproxy/function_app.py rename to azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_containerproxy/function_app.py index 596c299..8035066 100644 --- a/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_containerproxy/function_app.py +++ b/azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_containerproxy/function_app.py @@ -7,14 +7,14 @@ import logging import azure.functions as func -import azurefunctions.extensions.bindings.cosmos as cosmos +import azurefunctions.extensions.bindings.cosmosdb as cosmos app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) """ -FOLDER: cosmos_samples_containerproxy +FOLDER: cosmosdb_samples_containerproxy DESCRIPTION: - These samples demonstrate how to obtain a ContainerProxy from a Cosmos Input function app binding. + These samples demonstrate how to obtain a ContainerProxy from a Cosmos DB Input function app binding. USAGE: Set the environment variables with your own values before running the sample: @@ -27,7 +27,7 @@ @app.route(route="container") @app.cosmos_db_input(arg_name="container", - connection="AzureWebJobsStorage", + connection="CosmosDBConnection", database_name="db_name", container_name="container_name") def get_docs(req: func.HttpRequest, container: cosmos.ContainerProxy): diff --git a/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_containerproxy/host.json b/azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_containerproxy/host.json similarity index 100% rename from azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_containerproxy/host.json rename to azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_containerproxy/host.json diff --git a/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_containerproxy/local.settings.json b/azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_containerproxy/local.settings.json similarity index 60% rename from azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_containerproxy/local.settings.json rename to azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_containerproxy/local.settings.json index 6dc40bb..9606630 100644 --- a/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_containerproxy/local.settings.json +++ b/azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_containerproxy/local.settings.json @@ -2,6 +2,6 @@ "IsEncrypted": false, "Values": { "FUNCTIONS_WORKER_RUNTIME": "python", - "AzureWebJobsStorage": "UseDevelopmentStorage=true" + "CosmosDBConnection": "" } } \ No newline at end of file diff --git a/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_containerproxy/requirements.txt b/azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_containerproxy/requirements.txt similarity index 82% rename from azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_containerproxy/requirements.txt rename to azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_containerproxy/requirements.txt index b4b4870..3d1ba04 100644 --- a/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_containerproxy/requirements.txt +++ b/azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_containerproxy/requirements.txt @@ -3,4 +3,4 @@ # Manually managing azure-functions-worker may cause unexpected issues azure-functions -azurefunctions-extensions-bindings-cosmos \ No newline at end of file +azurefunctions-extensions-bindings-cosmosdb \ No newline at end of file diff --git a/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_cosmosclient/function_app.py b/azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_cosmosclient/function_app.py similarity index 82% rename from azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_cosmosclient/function_app.py rename to azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_cosmosclient/function_app.py index f98bf08..40f3768 100644 --- a/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_cosmosclient/function_app.py +++ b/azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_cosmosclient/function_app.py @@ -1,15 +1,15 @@ import logging import azure.functions as func -import azurefunctions.extensions.bindings.cosmos as cosmos +import azurefunctions.extensions.bindings.cosmosdb as cosmos app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) """ -FOLDER: cosmos_samples_cosmosclient +FOLDER: cosmosdb_samples_cosmosclient DESCRIPTION: - These samples demonstrate how to obtain a CosmosClient from a Cosmos Input function app binding. + These samples demonstrate how to obtain a CosmosClient from a Cosmos DB Input function app binding. USAGE: Set the environment variables with your own values before running the sample: @@ -22,7 +22,7 @@ @app.route(route="cosmos") @app.cosmos_db_input(arg_name="container", - connection="AzureWebJobsStorage", + connection="CosmosDBConnection", database_name="db_name", container_name="container_name") def get_docs(req: func.HttpRequest, client: cosmos.CosmosClient): diff --git a/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_cosmosclient/host.json b/azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_cosmosclient/host.json similarity index 100% rename from azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_cosmosclient/host.json rename to azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_cosmosclient/host.json diff --git a/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_cosmosclient/local.settings.json b/azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_cosmosclient/local.settings.json similarity index 60% rename from azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_cosmosclient/local.settings.json rename to azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_cosmosclient/local.settings.json index 6dc40bb..9606630 100644 --- a/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_cosmosclient/local.settings.json +++ b/azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_cosmosclient/local.settings.json @@ -2,6 +2,6 @@ "IsEncrypted": false, "Values": { "FUNCTIONS_WORKER_RUNTIME": "python", - "AzureWebJobsStorage": "UseDevelopmentStorage=true" + "CosmosDBConnection": "" } } \ No newline at end of file diff --git a/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_cosmosclient/requirements.txt b/azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_cosmosclient/requirements.txt similarity index 82% rename from azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_cosmosclient/requirements.txt rename to azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_cosmosclient/requirements.txt index b4b4870..3d1ba04 100644 --- a/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_cosmosclient/requirements.txt +++ b/azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_cosmosclient/requirements.txt @@ -3,4 +3,4 @@ # Manually managing azure-functions-worker may cause unexpected issues azure-functions -azurefunctions-extensions-bindings-cosmos \ No newline at end of file +azurefunctions-extensions-bindings-cosmosdb \ No newline at end of file diff --git a/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_databaseproxy/function_app.py b/azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_databaseproxy/function_app.py similarity index 82% rename from azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_databaseproxy/function_app.py rename to azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_databaseproxy/function_app.py index 5f16830..d793882 100644 --- a/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_databaseproxy/function_app.py +++ b/azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_databaseproxy/function_app.py @@ -1,15 +1,15 @@ import logging import azure.functions as func -import azurefunctions.extensions.bindings.cosmos as cosmos +import azurefunctions.extensions.bindings.cosmosdb as cosmos app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) """ -FOLDER: cosmos_samples_databaseproxy +FOLDER: cosmosdb_samples_databaseproxy DESCRIPTION: - These samples demonstrate how to obtain a DatabaseProxy from a Cosmos Input function app binding. + These samples demonstrate how to obtain a DatabaseProxy from a Cosmos DB Input function app binding. USAGE: Set the environment variables with your own values before running the sample: @@ -22,7 +22,7 @@ @app.route(route="database") @app.cosmos_db_input(arg_name="container", - connection="AzureWebJobsStorage", + connection="CosmosDBConnection", database_name="db_name", container_name="container_name") def get_docs(req: func.HttpRequest, database: cosmos.DatabaseProxy): diff --git a/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_databaseproxy/host.json b/azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_databaseproxy/host.json similarity index 100% rename from azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_databaseproxy/host.json rename to azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_databaseproxy/host.json diff --git a/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_databaseproxy/local.settings.json b/azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_databaseproxy/local.settings.json similarity index 60% rename from azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_databaseproxy/local.settings.json rename to azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_databaseproxy/local.settings.json index 6dc40bb..9606630 100644 --- a/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_databaseproxy/local.settings.json +++ b/azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_databaseproxy/local.settings.json @@ -2,6 +2,6 @@ "IsEncrypted": false, "Values": { "FUNCTIONS_WORKER_RUNTIME": "python", - "AzureWebJobsStorage": "UseDevelopmentStorage=true" + "CosmosDBConnection": "" } } \ No newline at end of file diff --git a/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_databaseproxy/requirements.txt b/azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_databaseproxy/requirements.txt similarity index 82% rename from azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_databaseproxy/requirements.txt rename to azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_databaseproxy/requirements.txt index b4b4870..3d1ba04 100644 --- a/azurefunctions-extensions-bindings-cosmos/samples/cosmos_samples_databaseproxy/requirements.txt +++ b/azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_databaseproxy/requirements.txt @@ -3,4 +3,4 @@ # Manually managing azure-functions-worker may cause unexpected issues azure-functions -azurefunctions-extensions-bindings-cosmos \ No newline at end of file +azurefunctions-extensions-bindings-cosmosdb \ No newline at end of file diff --git a/azurefunctions-extensions-bindings-cosmos/tests/__init__.py b/azurefunctions-extensions-bindings-cosmosdb/tests/__init__.py similarity index 100% rename from azurefunctions-extensions-bindings-cosmos/tests/__init__.py rename to azurefunctions-extensions-bindings-cosmosdb/tests/__init__.py diff --git a/azurefunctions-extensions-bindings-cosmos/tests/test_code_quality.py b/azurefunctions-extensions-bindings-cosmosdb/tests/test_code_quality.py similarity index 92% rename from azurefunctions-extensions-bindings-cosmos/tests/test_code_quality.py rename to azurefunctions-extensions-bindings-cosmosdb/tests/test_code_quality.py index 19a4e65..9d08a97 100644 --- a/azurefunctions-extensions-bindings-cosmos/tests/test_code_quality.py +++ b/azurefunctions-extensions-bindings-cosmosdb/tests/test_code_quality.py @@ -18,7 +18,7 @@ def test_mypy(self): try: subprocess.run( [sys.executable, '-m', 'mypy', '-m', - 'azurefunctions-extensions-bindings-cosmos'], + 'azurefunctions-extensions-bindings-cosmosdb'], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, @@ -41,7 +41,7 @@ def test_flake8(self): try: subprocess.run( [sys.executable, '-m', 'flake8', - 'azurefunctions-extensions-bindings-cosmos', + 'azurefunctions-extensions-bindings-cosmosdb', '--config', str(config_path)], check=True, stdout=subprocess.PIPE, diff --git a/azurefunctions-extensions-bindings-cosmos/tests/test_containerproxy.py b/azurefunctions-extensions-bindings-cosmosdb/tests/test_containerproxy.py similarity index 98% rename from azurefunctions-extensions-bindings-cosmos/tests/test_containerproxy.py rename to azurefunctions-extensions-bindings-cosmosdb/tests/test_containerproxy.py index cc239e3..8f8b0a2 100644 --- a/azurefunctions-extensions-bindings-cosmos/tests/test_containerproxy.py +++ b/azurefunctions-extensions-bindings-cosmosdb/tests/test_containerproxy.py @@ -9,7 +9,7 @@ from azure.cosmos import ContainerProxy as ContainerProxySdk from azurefunctions.extensions.base import Datum -from azurefunctions.extensions.bindings.cosmos import CosmosClientConverter, ContainerProxy +from azurefunctions.extensions.bindings.cosmosdb import CosmosClientConverter, ContainerProxy # Mock classes for testing diff --git a/azurefunctions-extensions-bindings-cosmos/tests/test_cosmosclient.py b/azurefunctions-extensions-bindings-cosmosdb/tests/test_cosmosclient.py similarity index 98% rename from azurefunctions-extensions-bindings-cosmos/tests/test_cosmosclient.py rename to azurefunctions-extensions-bindings-cosmosdb/tests/test_cosmosclient.py index 9085741..b88a37e 100644 --- a/azurefunctions-extensions-bindings-cosmos/tests/test_cosmosclient.py +++ b/azurefunctions-extensions-bindings-cosmosdb/tests/test_cosmosclient.py @@ -9,7 +9,7 @@ from azure.cosmos import CosmosClient as CosmosClientSdk from azurefunctions.extensions.base import Datum -from azurefunctions.extensions.bindings.cosmos import CosmosClient, CosmosClientConverter +from azurefunctions.extensions.bindings.cosmosdb import CosmosClient, CosmosClientConverter # Mock classes for testing diff --git a/azurefunctions-extensions-bindings-cosmos/tests/test_databaseproxy.py b/azurefunctions-extensions-bindings-cosmosdb/tests/test_databaseproxy.py similarity index 98% rename from azurefunctions-extensions-bindings-cosmos/tests/test_databaseproxy.py rename to azurefunctions-extensions-bindings-cosmosdb/tests/test_databaseproxy.py index 207c4eb..bc67be3 100644 --- a/azurefunctions-extensions-bindings-cosmos/tests/test_databaseproxy.py +++ b/azurefunctions-extensions-bindings-cosmosdb/tests/test_databaseproxy.py @@ -9,7 +9,7 @@ from azure.cosmos import DatabaseProxy as DatabaseProxySdk from azurefunctions.extensions.base import Datum -from azurefunctions.extensions.bindings.cosmos import CosmosClientConverter, DatabaseProxy +from azurefunctions.extensions.bindings.cosmosdb import CosmosClientConverter, DatabaseProxy # Mock classes for testing diff --git a/eng/ci/ci-cosmos-tests.yml b/eng/ci/ci-cosmosdb-tests.yml similarity index 84% rename from eng/ci/ci-cosmos-tests.yml rename to eng/ci/ci-cosmosdb-tests.yml index 3edac9d..ef98ed2 100644 --- a/eng/ci/ci-cosmos-tests.yml +++ b/eng/ci/ci-cosmosdb-tests.yml @@ -30,6 +30,6 @@ extends: os: windows stages: - - stage: RunCosmosUnitTests + - stage: RunCosmosDBUnitTests jobs: - - template: /eng/templates/official/jobs/cosmos-unit-tests.yml@self + - template: /eng/templates/official/jobs/cosmosdb-unit-tests.yml@self diff --git a/eng/ci/official-build.yml b/eng/ci/official-build.yml index df6e0e4..0d6424c 100644 --- a/eng/ci/official-build.yml +++ b/eng/ci/official-build.yml @@ -51,10 +51,10 @@ extends: dependsOn: Build jobs: - template: /eng/templates/official/jobs/blob-unit-tests.yml@self - - stage: RunCosmosTests + - stage: RunCosmosDBTests dependsOn: Build jobs: - - template: /eng/templates/official/jobs/cosmos-unit-tests.yml@self + - template: /eng/templates/official/jobs/cosmosdb-unit-tests.yml@self - stage: RunFastApiTests dependsOn: Build jobs: diff --git a/eng/templates/jobs/build.yml b/eng/templates/jobs/build.yml index 981e82b..9cf5066 100644 --- a/eng/templates/jobs/build.yml +++ b/eng/templates/jobs/build.yml @@ -10,9 +10,9 @@ jobs: blob_extension: EXTENSION_DIRECTORY: 'azurefunctions-extensions-bindings-blob' EXTENSION_NAME: 'Blob' - cosmos_extension: - EXTENSION_DIRECTORY: 'azurefunctions-extensions-bindings-cosmos' - EXTENSION_NAME: 'Cosmos' + cosmosdb_extension: + EXTENSION_DIRECTORY: 'azurefunctions-extensions-bindings-cosmosdb' + EXTENSION_NAME: 'CosmosDB' eventhub_extension: EXTENSION_DIRECTORY: 'azurefunctions-extensions-bindings-eventhub' EXTENSION_NAME: 'EventHub' diff --git a/eng/templates/official/jobs/build-artifacts.yml b/eng/templates/official/jobs/build-artifacts.yml index 7c43d0e..2e473a4 100644 --- a/eng/templates/official/jobs/build-artifacts.yml +++ b/eng/templates/official/jobs/build-artifacts.yml @@ -10,9 +10,9 @@ jobs: blob_extension: EXTENSION_DIRECTORY: 'azurefunctions-extensions-bindings-blob' EXTENSION_NAME: 'Blob' - cosmos_extension: - EXTENSION_DIRECTORY: 'azurefunctions-extensions-bindings-cosmos' - EXTENSION_NAME: 'Cosmos' + cosmosdb_extension: + EXTENSION_DIRECTORY: 'azurefunctions-extensions-bindings-cosmosdb' + EXTENSION_NAME: 'CosmosDB' eventhub_extension: EXTENSION_DIRECTORY: 'azurefunctions-extensions-bindings-eventhub' EXTENSION_NAME: 'EventHub' diff --git a/eng/templates/official/jobs/cosmos-unit-tests.yml b/eng/templates/official/jobs/cosmosdb-unit-tests.yml similarity index 77% rename from eng/templates/official/jobs/cosmos-unit-tests.yml rename to eng/templates/official/jobs/cosmosdb-unit-tests.yml index 00a0abe..02dc1f4 100644 --- a/eng/templates/official/jobs/cosmos-unit-tests.yml +++ b/eng/templates/official/jobs/cosmosdb-unit-tests.yml @@ -1,6 +1,6 @@ jobs: - job: "TestPython" - displayName: "Run Cosmos Tests" + displayName: "Run Cosmos DB Tests" strategy: matrix: @@ -19,12 +19,12 @@ jobs: versionSpec: $(PYTHON_VERSION) - bash: | python -m pip install --upgrade pip - cd azurefunctions-extensions-bindings-cosmos + cd azurefunctions-extensions-bindings-cosmosdb python -m pip install -U -e .[dev] displayName: 'Install dependencies' - bash: | - python -m pytest -q --instafail azurefunctions-extensions-bindings-cosmos/tests/ + python -m pytest -q --instafail azurefunctions-extensions-bindings-cosmosdb/tests/ env: AzureWebJobsStorage: $(AzureWebJobsStorage) input: $(input__accountEndpoint) - displayName: "Running Cosmos $(PYTHON_VERSION) Python Extension Tests" + displayName: "Running Cosmos DB $(PYTHON_VERSION) Python Extension Tests" From 05d5f4791a0443b4f0cb2eaff088fee694929f01 Mon Sep 17 00:00:00 2001 From: Evan Roman Date: Wed, 23 Apr 2025 16:09:45 -0500 Subject: [PATCH 09/12] Fix --- .../README.md | 2 +- .../extensions/bindings/blob/blobClient.py | 2 +- .../bindings/blob/containerClient.py | 2 +- .../bindings/blob/storageStreamDownloader.py | 2 +- .../blob_samples_blobclient/function_app.py | 2 +- .../function_app.py | 2 +- .../function_app.py | 2 +- .../README.md | 18 ++++++++---------- .../extensions/bindings/cosmosdb/__init__.py | 2 +- .../bindings/cosmosdb/containerProxy.py | 2 +- .../bindings/cosmosdb/cosmosClient.py | 2 +- .../bindings/cosmosdb/databaseProxy.py | 2 +- .../samples/README.md | 8 ++++---- .../function_app.py | 2 +- .../function_app.py | 2 +- .../function_app.py | 2 +- .../README.md | 2 +- .../extensions/bindings/eventhub/eventData.py | 4 ++-- .../eventhub_samples_eventdata/function_app.py | 2 +- .../README.md | 2 +- .../function_app.py | 2 +- .../function_app.py | 2 +- ...-cosmosdb-tests.yml => ci-cosmos-tests.yml} | 0 23 files changed, 33 insertions(+), 35 deletions(-) rename eng/ci/{ci-cosmosdb-tests.yml => ci-cosmos-tests.yml} (100%) diff --git a/azurefunctions-extensions-bindings-blob/README.md b/azurefunctions-extensions-bindings-blob/README.md index 447bd21..eb40c8b 100644 --- a/azurefunctions-extensions-bindings-blob/README.md +++ b/azurefunctions-extensions-bindings-blob/README.md @@ -56,7 +56,7 @@ import logging import azure.functions as func import azurefunctions.extensions.bindings.blob as blob -app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) +app = func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION) @app.blob_trigger(arg_name="client", path="PATH/TO/BLOB", 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 b985d84..c5a2bdf 100644 --- a/azurefunctions-extensions-bindings-blob/azurefunctions/extensions/bindings/blob/blobClient.py +++ b/azurefunctions-extensions-bindings-blob/azurefunctions/extensions/bindings/blob/blobClient.py @@ -56,4 +56,4 @@ def get_sdk_type(self): blob=self._blobName, ) else: - return None + raise ValueError(f"Unable to create {self.__class__.__name__} SDK type.") 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 7de6679..8f83bd4 100644 --- a/azurefunctions-extensions-bindings-blob/azurefunctions/extensions/bindings/blob/containerClient.py +++ b/azurefunctions-extensions-bindings-blob/azurefunctions/extensions/bindings/blob/containerClient.py @@ -46,4 +46,4 @@ def get_sdk_type(self): container=self._containerName ) else: - return None + raise ValueError(f"Unable to create {self.__class__.__name__} SDK type.") 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 2aa254b..a21c3ca 100644 --- a/azurefunctions-extensions-bindings-blob/azurefunctions/extensions/bindings/blob/storageStreamDownloader.py +++ b/azurefunctions-extensions-bindings-blob/azurefunctions/extensions/bindings/blob/storageStreamDownloader.py @@ -48,4 +48,4 @@ def get_sdk_type(self): blob=self._blobName, ).download_blob() else: - return None + raise ValueError(f"Unable to create {self.__class__.__name__} SDK type.") 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 1fd36ef..4287403 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 @@ -9,7 +9,7 @@ import azure.functions as func import azurefunctions.extensions.bindings.blob as blob -app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) +app = func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION) """ FOLDER: blob_samples_blobclient 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 2b63d7a..5205ed8 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 @@ -9,7 +9,7 @@ import azure.functions as func import azurefunctions.extensions.bindings.blob as blob -app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) +app = func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION) """ FOLDER: blob_samples_containerclient 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 e029731..51b22ae 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 @@ -9,7 +9,7 @@ import azure.functions as func import azurefunctions.extensions.bindings.blob as blob -app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) +app = func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION) """ FOLDER: blob_samples_storagestreamdownloader diff --git a/azurefunctions-extensions-bindings-cosmosdb/README.md b/azurefunctions-extensions-bindings-cosmosdb/README.md index 1cee615..176292d 100644 --- a/azurefunctions-extensions-bindings-cosmosdb/README.md +++ b/azurefunctions-extensions-bindings-cosmosdb/README.md @@ -6,11 +6,9 @@ Cosmos DB client types can be generated from: * Cosmos DB Input -[Source code](https://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-bindings-cosmosdb) +[Source code](https://github.com/Azure/azure-functions-python-extensions/tree/dev/azurefunctions-extensions-bindings-cosmosdb) [Package (PyPi)](https://pypi.org/project/azurefunctions-extensions-bindings-cosmosdb/) -| API reference documentation -| Product documentation -| [Samples](hhttps://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-bindings-cosmosdb/samples) +| [Samples](https://github.com/Azure/azure-functions-python-extensions/tree/dev/azurefunctions-extensions-bindings-cosmosdb/samples) ## Getting started @@ -46,7 +44,7 @@ az storage account create -n my-storage-account-name -g my-resource-group ### Bind to the SDK-type The Azure Functions Extensions Bindings Cosmos DB library for Python allows you to create a function app with Cosmos DB Input and define the type as a CosmosClient, DatabaseProxy, or ContainerProxy. Instead of receiving -an InputStream, when the function is executed, the type returned will be the defined SDK-type and have all of the +a DocumentList, 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 Cosmos DB library for Python. @@ -55,7 +53,7 @@ import logging import azure.functions as func import azurefunctions.extensions.bindings.cosmosdb as cosmos -app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) +app = func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION) @app.route(route="cosmos") @app.cosmos_db_input(arg_name="container", @@ -80,17 +78,17 @@ This list can be used for reference to catch thrown exceptions. To get the speci ### More sample code -Get started with our [Cosmos DB samples](hhttps://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-bindings-cosmosdb/samples). +Get started with our [Cosmos DB samples](https://github.com/Azure/azure-functions-python-extensions/tree/dev/azurefunctions-extensions-bindings-cosmosdb/samples). Several samples are available in this GitHub repository. These samples provide example code for additional scenarios commonly encountered while working with Cosmos DB: -* [cosmosdb_samples_cosmosclient](https://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-binding-cosmosdb/samples/cosmosdb_samples_cosmosclient) - Examples for using the CosmosClient type: +* [cosmosdb_samples_cosmosclient](https://github.com/Azure/azure-functions-python-extensions/tree/dev/azurefunctions-extensions-binding-cosmosdb/samples/cosmosdb_samples_cosmosclient) - Examples for using the CosmosClient type: * From CosmosDBInput -* [cosmosdb_samples_databaseproxy](https://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_databaseproxy) - Examples for using the DatabaseProxy type: +* [cosmosdb_samples_databaseproxy](https://github.com/Azure/azure-functions-python-extensions/tree/dev/azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_databaseproxy) - Examples for using the DatabaseProxy type: * From CosmosDBInput -* [cosmosdb_samples_containerclient](https://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_containerproxy) - Examples for using the ContainerProxy type: +* [cosmosdb_samples_containerclient](https://github.com/Azure/azure-functions-python-extensions/tree/dev/azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_containerproxy) - Examples for using the ContainerProxy type: * From CosmosDBInput ### Additional documentation diff --git a/azurefunctions-extensions-bindings-cosmosdb/azurefunctions/extensions/bindings/cosmosdb/__init__.py b/azurefunctions-extensions-bindings-cosmosdb/azurefunctions/extensions/bindings/cosmosdb/__init__.py index ecc2595..6fbb89a 100644 --- a/azurefunctions-extensions-bindings-cosmosdb/azurefunctions/extensions/bindings/cosmosdb/__init__.py +++ b/azurefunctions-extensions-bindings-cosmosdb/azurefunctions/extensions/bindings/cosmosdb/__init__.py @@ -13,4 +13,4 @@ "CosmosClientConverter" ] -__version__ = "1.0.0b1" +__version__ = "1.0.0a1" diff --git a/azurefunctions-extensions-bindings-cosmosdb/azurefunctions/extensions/bindings/cosmosdb/containerProxy.py b/azurefunctions-extensions-bindings-cosmosdb/azurefunctions/extensions/bindings/cosmosdb/containerProxy.py index 4f2cd40..ff9f13d 100644 --- a/azurefunctions-extensions-bindings-cosmosdb/azurefunctions/extensions/bindings/cosmosdb/containerProxy.py +++ b/azurefunctions-extensions-bindings-cosmosdb/azurefunctions/extensions/bindings/cosmosdb/containerProxy.py @@ -55,4 +55,4 @@ def get_sdk_type(self) -> ContainerProxySdk: db_client = cosmos_client.get_database_client(self._database_name) return db_client.get_container_client(self._container_name) else: - return None + raise ValueError(f"Unable to create {self.__class__.__name__} SDK type.") diff --git a/azurefunctions-extensions-bindings-cosmosdb/azurefunctions/extensions/bindings/cosmosdb/cosmosClient.py b/azurefunctions-extensions-bindings-cosmosdb/azurefunctions/extensions/bindings/cosmosdb/cosmosClient.py index f5ec4d9..cd58947 100644 --- a/azurefunctions-extensions-bindings-cosmosdb/azurefunctions/extensions/bindings/cosmosdb/cosmosClient.py +++ b/azurefunctions-extensions-bindings-cosmosdb/azurefunctions/extensions/bindings/cosmosdb/cosmosClient.py @@ -54,4 +54,4 @@ def get_sdk_type(self) -> CosmosClientSdk: ) return cosmos_client else: - return None + raise ValueError(f"Unable to create {self.__class__.__name__} SDK type.") diff --git a/azurefunctions-extensions-bindings-cosmosdb/azurefunctions/extensions/bindings/cosmosdb/databaseProxy.py b/azurefunctions-extensions-bindings-cosmosdb/azurefunctions/extensions/bindings/cosmosdb/databaseProxy.py index 0053fbe..c4003c8 100644 --- a/azurefunctions-extensions-bindings-cosmosdb/azurefunctions/extensions/bindings/cosmosdb/databaseProxy.py +++ b/azurefunctions-extensions-bindings-cosmosdb/azurefunctions/extensions/bindings/cosmosdb/databaseProxy.py @@ -54,4 +54,4 @@ def get_sdk_type(self) -> DatabaseProxySdk: ) return cosmos_client.get_database_client(self._database_name) else: - return None + raise ValueError(f"Unable to create {self.__class__.__name__} SDK type.") diff --git a/azurefunctions-extensions-bindings-cosmosdb/samples/README.md b/azurefunctions-extensions-bindings-cosmosdb/samples/README.md index b9996af..dec6d4e 100644 --- a/azurefunctions-extensions-bindings-cosmosdb/samples/README.md +++ b/azurefunctions-extensions-bindings-cosmosdb/samples/README.md @@ -17,13 +17,13 @@ These are code samples that show common scenario operations with the Azure Funct These samples relate to the Azure Storage Cosmos DB client library being used as part of a Python Function App. For examples on how to use the Azure Storage Cosmos DB client library, please see [Azure Cosmos samples](https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/cosmos/azure-cosmos/samples) -* [cosmosdb_samples_cosmosclient](https://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-binding-cosmosdb/samples/cosmosdb_samples_cosmosclient) - Examples for using the CosmosClient type: +* [cosmosdb_samples_cosmosclient](https://github.com/Azure/azure-functions-python-extensions/tree/dev/azurefunctions-extensions-binding-cosmosdb/samples/cosmosdb_samples_cosmosclient) - Examples for using the CosmosClient type: * From CosmosDBInput -* [cosmosdb_samples_databaseproxy](https://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_databaseproxy) - Examples for using the DatabaseProxy type: +* [cosmosdb_samples_databaseproxy](https://github.com/Azure/azure-functions-python-extensions/tree/dev/azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_databaseproxy) - Examples for using the DatabaseProxy type: * From CosmosDBInput -* [cosmosdb_samples_containerclient](https://github.com/Azure/azure-functions-python-extensions/tree/main/azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_containerproxy) - Examples for using the ContainerProxy type: +* [cosmosdb_samples_containerclient](https://github.com/Azure/azure-functions-python-extensions/tree/dev/azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_containerproxy) - Examples for using the ContainerProxy type: * From CosmosDBInput ## Prerequisites @@ -60,6 +60,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://learn.microsoft.com/en-us/python/api/azure-cosmos/azure.cosmos?view=azure-python) to learn more about what you can do with the Azure Cosmos DB client library. \ No newline at end of file diff --git a/azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_containerproxy/function_app.py b/azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_containerproxy/function_app.py index 8035066..af186e4 100644 --- a/azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_containerproxy/function_app.py +++ b/azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_containerproxy/function_app.py @@ -9,7 +9,7 @@ import azure.functions as func import azurefunctions.extensions.bindings.cosmosdb as cosmos -app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) +app = func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION) """ FOLDER: cosmosdb_samples_containerproxy diff --git a/azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_cosmosclient/function_app.py b/azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_cosmosclient/function_app.py index 40f3768..285fec8 100644 --- a/azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_cosmosclient/function_app.py +++ b/azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_cosmosclient/function_app.py @@ -3,7 +3,7 @@ import azure.functions as func import azurefunctions.extensions.bindings.cosmosdb as cosmos -app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) +app = func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION) """ diff --git a/azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_databaseproxy/function_app.py b/azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_databaseproxy/function_app.py index d793882..3d6e80c 100644 --- a/azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_databaseproxy/function_app.py +++ b/azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_databaseproxy/function_app.py @@ -3,7 +3,7 @@ import azure.functions as func import azurefunctions.extensions.bindings.cosmosdb as cosmos -app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) +app = func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION) """ diff --git a/azurefunctions-extensions-bindings-eventhub/README.md b/azurefunctions-extensions-bindings-eventhub/README.md index 7df067f..8a83345 100644 --- a/azurefunctions-extensions-bindings-eventhub/README.md +++ b/azurefunctions-extensions-bindings-eventhub/README.md @@ -51,7 +51,7 @@ import logging import azure.functions as func import azurefunctions.extensions.bindings.eventhub as eh -app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) +app = func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION) @app.event_hub_message_trigger( arg_name="event", event_hub_name="EVENTHUB_NAME", connection="AzureWebJobsStorage" 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 ffd0f0c..91bbcf0 100644 --- a/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventData.py +++ b/azurefunctions-extensions-bindings-eventhub/azurefunctions/extensions/bindings/eventhub/eventData.py @@ -48,5 +48,5 @@ def get_sdk_type(self) -> Optional[EventDataSDK]: # https://github.com/Azure/azure-sdk-for-python/issues/39711 if self.decoded_message: return EventDataSDK._from_message(self.decoded_message) - - return None + else: + raise ValueError(f"Unable to create {self.__class__.__name__} SDK type.") 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 c9cc972..401f3b6 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 @@ -10,7 +10,7 @@ import azure.functions as func import azurefunctions.extensions.bindings.eventhub as eh -app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) +app = func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION) """ FOLDER: eventhub_samples_eventdata diff --git a/azurefunctions-extensions-http-fastapi/README.md b/azurefunctions-extensions-http-fastapi/README.md index b0200ab..643e737 100644 --- a/azurefunctions-extensions-http-fastapi/README.md +++ b/azurefunctions-extensions-http-fastapi/README.md @@ -35,7 +35,7 @@ The Azure Functions Extensions Http FastApi library for Python allows you to cre import azure.functions as func from azurefunctions.extensions.http.fastapi import Request, JSONResponse -app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) +app = func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION) @app.route(route="streaming_upload", methods=[func.HttpMethod.POST]) async def streaming_upload(req: Request) -> JSONResponse: diff --git a/azurefunctions-extensions-http-fastapi/samples/fastapi_samples_streaming_download/function_app.py b/azurefunctions-extensions-http-fastapi/samples/fastapi_samples_streaming_download/function_app.py index 3ee5a55..b6d3b3c 100644 --- a/azurefunctions-extensions-http-fastapi/samples/fastapi_samples_streaming_download/function_app.py +++ b/azurefunctions-extensions-http-fastapi/samples/fastapi_samples_streaming_download/function_app.py @@ -7,7 +7,7 @@ import azure.functions as func from azurefunctions.extensions.http.fastapi import Request, StreamingResponse -app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) +app = func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION) def generate_sensor_data(): diff --git a/azurefunctions-extensions-http-fastapi/samples/fastapi_samples_streaming_upload/function_app.py b/azurefunctions-extensions-http-fastapi/samples/fastapi_samples_streaming_upload/function_app.py index de3410f..1cfd8d4 100644 --- a/azurefunctions-extensions-http-fastapi/samples/fastapi_samples_streaming_upload/function_app.py +++ b/azurefunctions-extensions-http-fastapi/samples/fastapi_samples_streaming_upload/function_app.py @@ -5,7 +5,7 @@ import azure.functions as func from azurefunctions.extensions.http.fastapi import JSONResponse, Request -app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) +app = func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION) @app.route(route="streaming_upload", methods=[func.HttpMethod.POST]) diff --git a/eng/ci/ci-cosmosdb-tests.yml b/eng/ci/ci-cosmos-tests.yml similarity index 100% rename from eng/ci/ci-cosmosdb-tests.yml rename to eng/ci/ci-cosmos-tests.yml From 75d5f8bc5343e4d02d88b0faba60007393cd62ee Mon Sep 17 00:00:00 2001 From: Evan Roman Date: Thu, 24 Apr 2025 17:28:24 -0500 Subject: [PATCH 10/12] Fix --- .../tests/test_blobclient.py | 8 +++---- .../tests/test_containerclient.py | 8 +++---- .../tests/test_ssd.py | 8 +++---- .../bindings/cosmosdb/containerProxy.py | 22 ++++++------------- .../bindings/cosmosdb/cosmosClient.py | 18 ++++----------- .../bindings/cosmosdb/databaseProxy.py | 20 +++++------------ .../extensions/bindings/cosmosdb/utils.py | 18 +++++++++++++++ .../pyproject.toml | 1 + .../README.md | 8 +++++-- .../function_app.py | 4 ++-- .../local.settings.json | 3 ++- .../tests/test_eventdata.py | 16 +++++++------- 12 files changed, 66 insertions(+), 68 deletions(-) diff --git a/azurefunctions-extensions-bindings-blob/tests/test_blobclient.py b/azurefunctions-extensions-bindings-blob/tests/test_blobclient.py index ca3782d..68b66aa 100644 --- a/azurefunctions-extensions-bindings-blob/tests/test_blobclient.py +++ b/azurefunctions-extensions-bindings-blob/tests/test_blobclient.py @@ -94,10 +94,10 @@ def test_input_incorrect_type(self): def test_input_empty(self): datum: Datum = Datum(value={}, type="model_binding_data") - result: BlobClient = BlobClientConverter.decode( - data=datum, trigger_metadata=None, pytype=BlobClient - ) - self.assertIsNone(result) + with self.assertRaises(ValueError): + BlobClientConverter.decode( + data=datum, trigger_metadata=None, pytype=BlobClient + ) def test_input_populated(self): content = { diff --git a/azurefunctions-extensions-bindings-blob/tests/test_containerclient.py b/azurefunctions-extensions-bindings-blob/tests/test_containerclient.py index ad9d801..2f7c0e1 100644 --- a/azurefunctions-extensions-bindings-blob/tests/test_containerclient.py +++ b/azurefunctions-extensions-bindings-blob/tests/test_containerclient.py @@ -94,10 +94,10 @@ def test_input_incorrect_type(self): def test_input_empty(self): datum: Datum = Datum(value={}, type="model_binding_data") - result: ContainerClient = BlobClientConverter.decode( - data=datum, trigger_metadata=None, pytype=ContainerClient - ) - self.assertIsNone(result) + with self.assertRaises(ValueError): + BlobClientConverter.decode( + data=datum, trigger_metadata=None, pytype=ContainerClient + ) def test_input_populated(self): content = { diff --git a/azurefunctions-extensions-bindings-blob/tests/test_ssd.py b/azurefunctions-extensions-bindings-blob/tests/test_ssd.py index fbd16bc..494a8fa 100644 --- a/azurefunctions-extensions-bindings-blob/tests/test_ssd.py +++ b/azurefunctions-extensions-bindings-blob/tests/test_ssd.py @@ -97,10 +97,10 @@ def test_input_incorrect_type(self): def test_input_empty(self): datum: Datum = Datum(value={}, type="model_binding_data") - result: StorageStreamDownloader = BlobClientConverter.decode( - data=datum, trigger_metadata=None, pytype=StorageStreamDownloader - ) - self.assertIsNone(result) + with self.assertRaises(ValueError): + BlobClientConverter.decode( + data=datum, trigger_metadata=None, pytype=StorageStreamDownloader + ) def test_input_populated(self): content = { diff --git a/azurefunctions-extensions-bindings-cosmosdb/azurefunctions/extensions/bindings/cosmosdb/containerProxy.py b/azurefunctions-extensions-bindings-cosmosdb/azurefunctions/extensions/bindings/cosmosdb/containerProxy.py index ff9f13d..ba4cadc 100644 --- a/azurefunctions-extensions-bindings-cosmosdb/azurefunctions/extensions/bindings/cosmosdb/containerProxy.py +++ b/azurefunctions-extensions-bindings-cosmosdb/azurefunctions/extensions/bindings/cosmosdb/containerProxy.py @@ -4,9 +4,9 @@ import json from azure.identity import DefaultAzureCredential -from azure.cosmos import CosmosClient as CosmosClientSdk, ContainerProxy as ContainerProxySdk +from azure.cosmos import ContainerProxy as ContainerProxySdk from azurefunctions.extensions.base import Datum, SdkType -from .utils import get_connection_string, using_managed_identity +from .utils import get_connection_string, using_managed_identity, get_cosmos_client class ContainerProxy(SdkType): @@ -20,8 +20,6 @@ def __init__(self, *, data: Datum) -> None: self._container_name = None self._connection = None self._using_managed_identity = False - self._partition_key = None - self._sql_query = None self._preferred_locations = None if self._data: self._version = data.version @@ -44,15 +42,9 @@ def get_sdk_type(self) -> ContainerProxySdk: We track if Managed Identity is being used through a flag. """ - if self._data: - cosmos_client = ( - CosmosClientSdk( - url=self._connection, credential=DefaultAzureCredential() - ) - if self._using_managed_identity - else CosmosClientSdk.from_connection_string(self._connection) - ) - db_client = cosmos_client.get_database_client(self._database_name) - return db_client.get_container_client(self._container_name) - else: + if not self._data: raise ValueError(f"Unable to create {self.__class__.__name__} SDK type.") + + cosmos_client = get_cosmos_client(self._using_managed_identity, self._connection, self._preferred_locations) + db_client = cosmos_client.get_database_client(self._database_name) + return db_client.get_container_client(self._container_name) diff --git a/azurefunctions-extensions-bindings-cosmosdb/azurefunctions/extensions/bindings/cosmosdb/cosmosClient.py b/azurefunctions-extensions-bindings-cosmosdb/azurefunctions/extensions/bindings/cosmosdb/cosmosClient.py index cd58947..bb33cac 100644 --- a/azurefunctions-extensions-bindings-cosmosdb/azurefunctions/extensions/bindings/cosmosdb/cosmosClient.py +++ b/azurefunctions-extensions-bindings-cosmosdb/azurefunctions/extensions/bindings/cosmosdb/cosmosClient.py @@ -3,10 +3,9 @@ import json -from azure.identity import DefaultAzureCredential from azure.cosmos import CosmosClient as CosmosClientSdk from azurefunctions.extensions.base import Datum, SdkType -from .utils import get_connection_string, using_managed_identity +from .utils import get_connection_string, using_managed_identity, get_cosmos_client class CosmosClient(SdkType): @@ -20,8 +19,6 @@ def __init__(self, *, data: Datum) -> None: self._container_name = None self._connection = None self._using_managed_identity = False - self._partition_key = None - self._sql_query = None self._preferred_locations = None if self._data: self._version = data.version @@ -44,14 +41,7 @@ def get_sdk_type(self) -> CosmosClientSdk: We track if Managed Identity is being used through a flag. """ - if self._data: - cosmos_client = ( - CosmosClientSdk( - url=self._connection, credential=DefaultAzureCredential() - ) - if self._using_managed_identity - else CosmosClientSdk.from_connection_string(self._connection) - ) - return cosmos_client - else: + if not self._data: raise ValueError(f"Unable to create {self.__class__.__name__} SDK type.") + + return get_cosmos_client(self._using_managed_identity, self._connection, self._preferred_locations) diff --git a/azurefunctions-extensions-bindings-cosmosdb/azurefunctions/extensions/bindings/cosmosdb/databaseProxy.py b/azurefunctions-extensions-bindings-cosmosdb/azurefunctions/extensions/bindings/cosmosdb/databaseProxy.py index c4003c8..4c90908 100644 --- a/azurefunctions-extensions-bindings-cosmosdb/azurefunctions/extensions/bindings/cosmosdb/databaseProxy.py +++ b/azurefunctions-extensions-bindings-cosmosdb/azurefunctions/extensions/bindings/cosmosdb/databaseProxy.py @@ -4,9 +4,9 @@ import json from azure.identity import DefaultAzureCredential -from azure.cosmos import CosmosClient as CosmosClientSdk, DatabaseProxy as DatabaseProxySdk +from azure.cosmos import DatabaseProxy as DatabaseProxySdk from azurefunctions.extensions.base import Datum, SdkType -from .utils import get_connection_string, using_managed_identity +from .utils import get_connection_string, using_managed_identity, get_cosmos_client class DatabaseProxy(SdkType): @@ -20,8 +20,6 @@ def __init__(self, *, data: Datum) -> None: self._container_name = None self._connection = None self._using_managed_identity = False - self._partition_key = None - self._sql_query = None self._preferred_locations = None if self._data: self._version = data.version @@ -44,14 +42,8 @@ def get_sdk_type(self) -> DatabaseProxySdk: We track if Managed Identity is being used through a flag. """ - if self._data: - cosmos_client = ( - CosmosClientSdk( - url=self._connection, credential=DefaultAzureCredential() - ) - if self._using_managed_identity - else CosmosClientSdk.from_connection_string(self._connection) - ) - return cosmos_client.get_database_client(self._database_name) - else: + if not self._data: raise ValueError(f"Unable to create {self.__class__.__name__} SDK type.") + + cosmos_client = get_cosmos_client(self._using_managed_identity, self._connection, self._preferred_locations) + return cosmos_client.get_database_client(self._database_name) diff --git a/azurefunctions-extensions-bindings-cosmosdb/azurefunctions/extensions/bindings/cosmosdb/utils.py b/azurefunctions-extensions-bindings-cosmosdb/azurefunctions/extensions/bindings/cosmosdb/utils.py index 4df0934..7e73449 100644 --- a/azurefunctions-extensions-bindings-cosmosdb/azurefunctions/extensions/bindings/cosmosdb/utils.py +++ b/azurefunctions-extensions-bindings-cosmosdb/azurefunctions/extensions/bindings/cosmosdb/utils.py @@ -1,6 +1,8 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. import os +from azure.identity import DefaultAzureCredential +from azure.cosmos import CosmosClient as CosmosClientSdk def get_connection_string(connection_string: str) -> str: @@ -35,3 +37,19 @@ def using_managed_identity(connection_name: str) -> bool: by checking for a __accountEndpoint suffix. """ return os.getenv(connection_name + "__accountEndpoint") is not None + + +def get_cosmos_client(using_managed_identity: bool, connection: str, preferred_locations: str) -> CosmosClientSdk: + pl = [] + if preferred_locations: + pl = [l.strip() for l in preferred_locations.split(",")] + + cosmos_client = ( + CosmosClientSdk( + url=connection, credential=DefaultAzureCredential(), + preferred_locations=pl + ) + if using_managed_identity + else CosmosClientSdk.from_connection_string(connection, preferred_locations=pl) + ) + return cosmos_client diff --git a/azurefunctions-extensions-bindings-cosmosdb/pyproject.toml b/azurefunctions-extensions-bindings-cosmosdb/pyproject.toml index 0683fac..00eed2f 100644 --- a/azurefunctions-extensions-bindings-cosmosdb/pyproject.toml +++ b/azurefunctions-extensions-bindings-cosmosdb/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', diff --git a/azurefunctions-extensions-bindings-eventhub/README.md b/azurefunctions-extensions-bindings-eventhub/README.md index 8a83345..6221d2f 100644 --- a/azurefunctions-extensions-bindings-eventhub/README.md +++ b/azurefunctions-extensions-bindings-eventhub/README.md @@ -4,10 +4,14 @@ Azure EventHub sdk (EventData). EventHub types can be generated from: +* EventHub Triggers + +The supported EventHub SDK types include: + +* EventData + [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) 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 401f3b6..dc5da6b 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 @@ -30,7 +30,7 @@ @app.event_hub_message_trigger( - arg_name="event", event_hub_name="EVENTHUB_NAME", connection="AzureWebJobsStorage" + arg_name="event", event_hub_name="EVENTHUB_NAME", connection="EventHubConnection" ) def eventhub_trigger(event: eh.EventData): logging.info( @@ -40,7 +40,7 @@ def eventhub_trigger(event: eh.EventData): @app.event_hub_message_trigger( - arg_name="events", event_hub_name="EVENTHUB_NAME", connection="AzureWebJobsStorage", cardinality="many" + arg_name="events", event_hub_name="EVENTHUB_NAME", connection="EventHubConnection", cardinality="many" ) def eventhub_trigger(events: List[eh.EventData]): for event in events: 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 index 6dc40bb..e02e8b6 100644 --- a/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventdata/local.settings.json +++ b/azurefunctions-extensions-bindings-eventhub/samples/eventhub_samples_eventdata/local.settings.json @@ -1,7 +1,8 @@ { "IsEncrypted": false, "Values": { + "AzureWebJobsStorage": "UseDevelopmentStorage=true", "FUNCTIONS_WORKER_RUNTIME": "python", - "AzureWebJobsStorage": "UseDevelopmentStorage=true" + "EventHubConnection": "" } } \ No newline at end of file diff --git a/azurefunctions-extensions-bindings-eventhub/tests/test_eventdata.py b/azurefunctions-extensions-bindings-eventhub/tests/test_eventdata.py index aed9ee6..dceaf76 100644 --- a/azurefunctions-extensions-bindings-eventhub/tests/test_eventdata.py +++ b/azurefunctions-extensions-bindings-eventhub/tests/test_eventdata.py @@ -77,18 +77,18 @@ def test_input_incorrect_type(self): 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) + with self.assertRaises(ValueError): + EventDataConverter.decode( + data=datum, trigger_metadata=None, pytype=EventData + ) def test_input_empty_cmbd(self): datum: Datum = Datum(value=MockCMBD([None]), type="collection_model_binding_data") - result: EventData = EventDataConverter.decode( - data=datum, trigger_metadata=None, pytype=EventData - ) - self.assertEqual(result, [None]) + with self.assertRaises(ValueError): + EventDataConverter.decode( + data=datum, trigger_metadata=None, pytype=EventData + ) def test_input_populated_mbd(self): sample_mbd = MockMBD( From 8a06cea57cebe97878cecce993278d342b4b1c2c Mon Sep 17 00:00:00 2001 From: Evan Roman Date: Thu, 24 Apr 2025 23:47:15 -0500 Subject: [PATCH 11/12] Fix --- .../extensions/bindings/cosmosdb/utils.py | 1 + .../tests/test_containerproxy.py | 16 ++++++++-------- .../tests/test_cosmosclient.py | 14 +++++++------- .../tests/test_databaseproxy.py | 14 +++++++------- 4 files changed, 23 insertions(+), 22 deletions(-) diff --git a/azurefunctions-extensions-bindings-cosmosdb/azurefunctions/extensions/bindings/cosmosdb/utils.py b/azurefunctions-extensions-bindings-cosmosdb/azurefunctions/extensions/bindings/cosmosdb/utils.py index 7e73449..3d6525a 100644 --- a/azurefunctions-extensions-bindings-cosmosdb/azurefunctions/extensions/bindings/cosmosdb/utils.py +++ b/azurefunctions-extensions-bindings-cosmosdb/azurefunctions/extensions/bindings/cosmosdb/utils.py @@ -52,4 +52,5 @@ def get_cosmos_client(using_managed_identity: bool, connection: str, preferred_l if using_managed_identity else CosmosClientSdk.from_connection_string(connection, preferred_locations=pl) ) + return cosmos_client diff --git a/azurefunctions-extensions-bindings-cosmosdb/tests/test_containerproxy.py b/azurefunctions-extensions-bindings-cosmosdb/tests/test_containerproxy.py index 8f8b0a2..31e18e8 100644 --- a/azurefunctions-extensions-bindings-cosmosdb/tests/test_containerproxy.py +++ b/azurefunctions-extensions-bindings-cosmosdb/tests/test_containerproxy.py @@ -94,16 +94,16 @@ def test_input_incorrect_type(self): def test_input_empty(self): datum: Datum = Datum(value={}, type="model_binding_data") - result: ContainerProxy = CosmosClientConverter.decode( - data=datum, trigger_metadata=None, pytype=ContainerProxy - ) - self.assertIsNone(result) + with self.assertRaises(ValueError): + CosmosClientConverter.decode( + data=datum, trigger_metadata=None, pytype=ContainerProxy + ) def test_input_populated(self): content = { "DatabaseName": "test-db", "ContainerName": "test-items", - "Connection": "AzureWebJobsStorage" + "Connection": "CosmosDBConnection" } sample_mbd = MockMBD( @@ -146,7 +146,7 @@ def test_invalid_input_populated(self): ) self.assertEqual( e.exception.args[0], - "Storage account connection string NotARealConnectionString does not exist. " + "Cosmos DB connection string NotARealConnectionString does not exist. " "Please make sure that it is a defined App Setting.", ) @@ -171,7 +171,7 @@ def test_none_input_populated(self): ) self.assertEqual( e.exception.args[0], - "Storage account connection string cannot be None. Please provide a connection string.", + "Cosmos DB connection string cannot be None. Please provide a connection string.", ) def test_input_populated_managed_identity_input(self): @@ -205,7 +205,7 @@ def test_input_invalid_pytype(self): content = { "DatabaseName": "test-db", "ContainerName": "test-items", - "Connection": "AzureWebJobsStorage" + "Connection": "CosmosDBConnection" } sample_mbd = MockMBD( diff --git a/azurefunctions-extensions-bindings-cosmosdb/tests/test_cosmosclient.py b/azurefunctions-extensions-bindings-cosmosdb/tests/test_cosmosclient.py index b88a37e..382f87c 100644 --- a/azurefunctions-extensions-bindings-cosmosdb/tests/test_cosmosclient.py +++ b/azurefunctions-extensions-bindings-cosmosdb/tests/test_cosmosclient.py @@ -94,16 +94,16 @@ def test_input_incorrect_type(self): def test_input_empty(self): datum: Datum = Datum(value={}, type="model_binding_data") - result: CosmosClient = CosmosClientConverter.decode( - data=datum, trigger_metadata=None, pytype=CosmosClient - ) - self.assertIsNone(result) + with self.assertRaises(ValueError): + CosmosClientConverter.decode( + data=datum, trigger_metadata=None, pytype=CosmosClient + ) def test_input_populated(self): content = { "DatabaseName": "test-db", "ContainerName": "test-items", - "Connection": "AzureWebJobsStorage" + "Connection": "CosmosDBConnection" } sample_mbd = MockMBD( @@ -147,7 +147,7 @@ def test_invalid_input_populated(self): ) self.assertEqual( e.exception.args[0], - "Storage account connection string NotARealConnectionString does not exist. " + "Cosmos DB connection string NotARealConnectionString does not exist. " "Please make sure that it is a defined App Setting.", ) @@ -207,7 +207,7 @@ def test_input_invalid_pytype(self): content = { "DatabaseName": "test-db", "ContainerName": "test-items", - "Connection": "AzureWebJobsStorage", + "Connection": "CosmosDBConnection", } sample_mbd = MockMBD( diff --git a/azurefunctions-extensions-bindings-cosmosdb/tests/test_databaseproxy.py b/azurefunctions-extensions-bindings-cosmosdb/tests/test_databaseproxy.py index bc67be3..cce8eee 100644 --- a/azurefunctions-extensions-bindings-cosmosdb/tests/test_databaseproxy.py +++ b/azurefunctions-extensions-bindings-cosmosdb/tests/test_databaseproxy.py @@ -94,10 +94,10 @@ def test_input_incorrect_type(self): def test_input_empty(self): datum: Datum = Datum(value={}, type="model_binding_data") - result: DatabaseProxy = CosmosClientConverter.decode( - data=datum, trigger_metadata=None, pytype=DatabaseProxy - ) - self.assertIsNone(result) + with self.assertRaises(ValueError): + CosmosClientConverter.decode( + data=datum, trigger_metadata=None, pytype=DatabaseProxy + ) def test_input_populated(self): content = { @@ -147,7 +147,7 @@ def test_invalid_input_populated(self): ) self.assertEqual( e.exception.args[0], - "Storage account connection string NotARealConnectionString does not exist. " + "Cosmos DB connection string NotARealConnectionString does not exist. " "Please make sure that it is a defined App Setting.", ) @@ -172,7 +172,7 @@ def test_none_input_populated(self): ) self.assertEqual( e.exception.args[0], - "Storage account connection string cannot be None. Please provide a connection string.", + "Cosmos DB connection string cannot be None. Please provide a connection string.", ) def test_input_populated_managed_identity_input(self): @@ -206,7 +206,7 @@ def test_input_invalid_pytype(self): content = { "DatabaseName": "test-db", "ContainerName": "test-items", - "Connection": "AzureWebJobsStorage" + "Connection": "CosmosDBConnection" } sample_mbd = MockMBD( From ea4600d4f88f6b536215f66a00588b34c378ced5 Mon Sep 17 00:00:00 2001 From: Evan Roman Date: Mon, 5 May 2025 12:02:36 -0500 Subject: [PATCH 12/12] Fix --- .../function_app.py | 4 ++-- .../function_app.py | 11 ++++----- .../function_app.py | 7 +++--- eng/ci/public-build.yml | 24 +++++++++++++++++++ 4 files changed, 33 insertions(+), 13 deletions(-) diff --git a/azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_containerproxy/function_app.py b/azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_containerproxy/function_app.py index af186e4..9150a44 100644 --- a/azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_containerproxy/function_app.py +++ b/azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_containerproxy/function_app.py @@ -18,9 +18,9 @@ USAGE: Set the environment variables with your own values before running the sample: - 1) AzureWebJobsStorage - the connection string to your storage account + 1) CosmosDBConnection - the connection string to your Cosmos DB instance - Set database_name and container_name to the path to the container you want to use + Set database_name and container_name to the database name the and container name you want to use as inputs to the function (required). """ diff --git a/azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_cosmosclient/function_app.py b/azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_cosmosclient/function_app.py index 285fec8..47912f8 100644 --- a/azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_cosmosclient/function_app.py +++ b/azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_cosmosclient/function_app.py @@ -13,18 +13,15 @@ USAGE: Set the environment variables with your own values before running the sample: - 1) AzureWebJobsStorage - the connection string to your storage account - - Set database_name and container_name to the path to the container you want to use - as inputs to the function (required). + 1) CosmosDBConnection - the connection string to your Cosmos DB instance """ @app.route(route="cosmos") -@app.cosmos_db_input(arg_name="container", +@app.cosmos_db_input(arg_name="client", connection="CosmosDBConnection", - database_name="db_name", - container_name="container_name") + database_name=None, + container_name=None) def get_docs(req: func.HttpRequest, client: cosmos.CosmosClient): databases = client.list_databases() for db in databases: diff --git a/azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_databaseproxy/function_app.py b/azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_databaseproxy/function_app.py index 3d6e80c..056e951 100644 --- a/azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_databaseproxy/function_app.py +++ b/azurefunctions-extensions-bindings-cosmosdb/samples/cosmosdb_samples_databaseproxy/function_app.py @@ -13,10 +13,9 @@ USAGE: Set the environment variables with your own values before running the sample: - 1) AzureWebJobsStorage - the connection string to your storage account + 1) CosmosDBConnection - the connection string to your Cosmos DB instance - Set database_name and container_name to the path to the container you want to use - as inputs to the function (required). + Set database_name to the database you want to use as an input to the function (required). """ @@ -24,7 +23,7 @@ @app.cosmos_db_input(arg_name="container", connection="CosmosDBConnection", database_name="db_name", - container_name="container_name") + container_name=None) def get_docs(req: func.HttpRequest, database: cosmos.DatabaseProxy): containers = database.list_containers() for c in containers: diff --git a/eng/ci/public-build.yml b/eng/ci/public-build.yml index 61ed7a9..9d69944 100644 --- a/eng/ci/public-build.yml +++ b/eng/ci/public-build.yml @@ -43,3 +43,27 @@ extends: - stage: Build jobs: - template: /eng/templates/jobs/build.yml@self + - stage: RunBaseTests + dependsOn: Build + jobs: + - template: /eng/templates/official/jobs/base-unit-tests.yml@self + - stage: RunBlobTests + dependsOn: Build + jobs: + - template: /eng/templates/official/jobs/blob-unit-tests.yml@self + - stage: RunCosmosDBTests + dependsOn: Build + jobs: + - template: /eng/templates/official/jobs/cosmosdb-unit-tests.yml@self + - stage: RunEventHubTests + dependsOn: Build + jobs: + - template: /eng/templates/official/jobs/eventhub-unit-tests.yml@self + - stage: RunServiceBusTests + dependsOn: Build + jobs: + - template: /eng/templates/official/jobs/servicebus-unit-tests.yml@self + - stage: RunFastApiTests + dependsOn: Build + jobs: + - template: /eng/templates/official/jobs/fastapi-unit-tests.yml@self