From 13ed5b4aaa14b409645ca760811e2267d0cb12f1 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 13 Mar 2024 16:10:18 -0700 Subject: [PATCH 01/27] adding http v2 extensions --- .../functions/extension/base/__init__.py | 38 +++++- .../azure/functions/extension/base/web.py | 128 ++++++++++++++++++ .../tests/test_web.py | 0 .../CHANGELOG.md | 0 azure-functions-extension-fastapi/LICENSE | 21 +++ azure-functions-extension-fastapi/MANIFEST.in | 3 + azure-functions-extension-fastapi/README.md | 0 .../azure/__init__.py | 1 + .../azure/functions/__init__.py | 1 + .../azure/functions/extension/__init__.py | 1 + .../functions/extension/fastapi/__init__.py | 18 +++ .../azure/functions/extension/fastapi/web.py | 82 +++++++++++ azure-functions-extension-fastapi/setup.py | 41 ++++++ .../tests/__init__.py | 21 +++ .../tests/test_code_quality.py | 35 +++++ .../tests/test_web.py | 0 16 files changed, 385 insertions(+), 5 deletions(-) create mode 100644 azure-functions-extension-base/azure/functions/extension/base/web.py create mode 100644 azure-functions-extension-base/tests/test_web.py create mode 100644 azure-functions-extension-fastapi/CHANGELOG.md create mode 100644 azure-functions-extension-fastapi/LICENSE create mode 100644 azure-functions-extension-fastapi/MANIFEST.in create mode 100644 azure-functions-extension-fastapi/README.md create mode 100644 azure-functions-extension-fastapi/azure/__init__.py create mode 100644 azure-functions-extension-fastapi/azure/functions/__init__.py create mode 100644 azure-functions-extension-fastapi/azure/functions/extension/__init__.py create mode 100644 azure-functions-extension-fastapi/azure/functions/extension/fastapi/__init__.py create mode 100644 azure-functions-extension-fastapi/azure/functions/extension/fastapi/web.py create mode 100644 azure-functions-extension-fastapi/setup.py create mode 100644 azure-functions-extension-fastapi/tests/__init__.py create mode 100644 azure-functions-extension-fastapi/tests/test_code_quality.py create mode 100644 azure-functions-extension-fastapi/tests/test_web.py diff --git a/azure-functions-extension-base/azure/functions/extension/base/__init__.py b/azure-functions-extension-base/azure/functions/extension/base/__init__.py index fa523e3..06eb59c 100644 --- a/azure-functions-extension-base/azure/functions/extension/base/__init__.py +++ b/azure-functions-extension-base/azure/functions/extension/base/__init__.py @@ -1,10 +1,38 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -from .meta import (Datum, _ConverterMeta, _BaseConverter, - InConverter, OutConverter, get_binding_registry) +from .meta import ( + Datum, + _ConverterMeta, + _BaseConverter, + InConverter, + OutConverter, + get_binding_registry +) from .sdkType import SdkType +from .web import ( + WebServer, + WebApp, + ModuleTrackerMeta, + RequestTrackerMeta, + ResponseTrackerMeta, + http_v2_enabled, + ResponseLabels +) -__all__ = ['Datum', '_ConverterMeta', '_BaseConverter', - 'InConverter', 'OutConverter', - 'SdkType', 'get_binding_registry'] +__all__ = [ + 'Datum', + '_ConverterMeta', + '_BaseConverter', + 'InConverter', + 'OutConverter', + 'SdkType', + 'get_binding_registry', + 'ModuleTrackerMeta', + 'RequestTrackerMeta', + 'ResponseTrackerMeta', + 'http_v2_enabled', + 'ResponseLabels', + 'WebServer', + 'WebApp' +] diff --git a/azure-functions-extension-base/azure/functions/extension/base/web.py b/azure-functions-extension-base/azure/functions/extension/base/web.py new file mode 100644 index 0000000..41ea42b --- /dev/null +++ b/azure-functions-extension-base/azure/functions/extension/base/web.py @@ -0,0 +1,128 @@ +from abc import abstractmethod +from enum import Enum +from typing import Callable + +base_extension_module = __name__ + + +# Base extension pkg +class ModuleTrackerMeta(type): + _module = None + + def __new__(cls, name, bases, dct, **kwargs): + new_class = super().__new__(cls, name, bases, dct) + new_module = dct.get('__module__') + if new_module != base_extension_module: + if cls._module is None: + cls._module = new_module + elif cls._module != new_module: + raise Exception(f'Only one web extension package shall be imported, ' + f'{cls._module} and {new_module} are imported') + return new_class + + @classmethod + def get_module(cls): + return cls._module + + @classmethod + def module_imported(cls): + return cls._module is not None + + +class RequestTrackerMeta(type): + _request_type = None + + def __new__(cls, name, bases, dct, **kwargs): + new_class = super().__new__(cls, name, bases, dct) + + request_type = dct.get('request_type') + + if request_type is None: + raise Exception(f'Request type not provided for class {name}') + + if cls._request_type is not None: + raise Exception(f'Only one request type shall be recorded for class {name} ' + f'but found {cls._request_type} and {request_type}') + cls._request_type = request_type + + return new_class + + @classmethod + def get_request_type(cls): + return cls._request_type + + @classmethod + def check_type(cls, pytype: type) -> bool: + return cls._request_type is not None and issubclass(pytype, cls._request_type) + + +class ResponseTrackerMeta(type): + _response_types = {} + + def __new__(cls, name, bases, dct, **kwargs): + new_class = super().__new__(cls, name, bases, dct) + + label = dct.get('label') + response_type = dct.get('response_type') + + if label is None: + raise Exception(f'Response label not provided for class {name}') + if response_type is None: + raise Exception(f'Response type not provided for class {name}') + if cls._response_types.get(label) is not None: + raise Exception(f'Only one response type shall be recorded for class {name} ' + f'but found {cls._response_types.get(label)} and {response_type}') + + cls._response_types[label] = response_type + + return new_class + + @classmethod + def get_standard_response_type(cls): + return cls.get_response_type(ResponseLabels.STANDARD) + + @classmethod + def get_response_type(cls, label): + return cls._response_types.get(label) + + @classmethod + def check_type(cls, pytype: type) -> bool: + return cls._response_types is not None and any(issubclass(pytype, response_type) + for response_type in cls._response_types.values()) + + +class WebApp(metaclass=ModuleTrackerMeta): + @abstractmethod + def route(self, func: Callable): + pass + + @abstractmethod + def get_app(self): + pass + + +class WebServer(metaclass=ModuleTrackerMeta): + def __init__(self, hostname, port, web_app: WebApp): + self.hostname = hostname + self.port = port + self.web_app = web_app.get_app() + + @abstractmethod + async def serve(self): + pass + + +def http_v2_enabled() -> bool: + return ModuleTrackerMeta.module_imported() + + +class ResponseLabels(Enum): + STANDARD = 'standard' + STREAMING = 'streaming' + FILE = 'file' + HTML = 'html' + JSON = 'json' + ORJSON = 'orjson' + PLAIN_TEXT = 'plain_text' + REDIRECT = 'redirect' + UJSON = 'ujson' diff --git a/azure-functions-extension-base/tests/test_web.py b/azure-functions-extension-base/tests/test_web.py new file mode 100644 index 0000000..e69de29 diff --git a/azure-functions-extension-fastapi/CHANGELOG.md b/azure-functions-extension-fastapi/CHANGELOG.md new file mode 100644 index 0000000..e69de29 diff --git a/azure-functions-extension-fastapi/LICENSE b/azure-functions-extension-fastapi/LICENSE new file mode 100644 index 0000000..63447fd --- /dev/null +++ b/azure-functions-extension-fastapi/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/azure-functions-extension-fastapi/MANIFEST.in b/azure-functions-extension-fastapi/MANIFEST.in new file mode 100644 index 0000000..e1ae5ad --- /dev/null +++ b/azure-functions-extension-fastapi/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/azure-functions-extension-fastapi/README.md b/azure-functions-extension-fastapi/README.md new file mode 100644 index 0000000..e69de29 diff --git a/azure-functions-extension-fastapi/azure/__init__.py b/azure-functions-extension-fastapi/azure/__init__.py new file mode 100644 index 0000000..8db66d3 --- /dev/null +++ b/azure-functions-extension-fastapi/azure/__init__.py @@ -0,0 +1 @@ +__path__ = __import__("pkgutil").extend_path(__path__, __name__) diff --git a/azure-functions-extension-fastapi/azure/functions/__init__.py b/azure-functions-extension-fastapi/azure/functions/__init__.py new file mode 100644 index 0000000..8db66d3 --- /dev/null +++ b/azure-functions-extension-fastapi/azure/functions/__init__.py @@ -0,0 +1 @@ +__path__ = __import__("pkgutil").extend_path(__path__, __name__) diff --git a/azure-functions-extension-fastapi/azure/functions/extension/__init__.py b/azure-functions-extension-fastapi/azure/functions/extension/__init__.py new file mode 100644 index 0000000..8db66d3 --- /dev/null +++ b/azure-functions-extension-fastapi/azure/functions/extension/__init__.py @@ -0,0 +1 @@ +__path__ = __import__("pkgutil").extend_path(__path__, __name__) diff --git a/azure-functions-extension-fastapi/azure/functions/extension/fastapi/__init__.py b/azure-functions-extension-fastapi/azure/functions/extension/fastapi/__init__.py new file mode 100644 index 0000000..3885b65 --- /dev/null +++ b/azure-functions-extension-fastapi/azure/functions/extension/fastapi/__init__.py @@ -0,0 +1,18 @@ +from .web import WebServer, WebApp +from fastapi import Request, Response +from fastapi.responses import ( + StreamingResponse, + HTMLResponse, + PlainTextResponse, + RedirectResponse, + JSONResponse, + UJSONResponse, + ORJSONResponse, + FileResponse, +) + +__all__ = ['WebServer', 'WebApp', 'Request', 'Response', 'StreamingResponse', 'HTMLResponse', + 'PlainTextResponse', 'RedirectResponse', 'JSONResponse', 'UJSONResponse', + 'ORJSONResponse', 'FileResponse'] + +__version__ = "0.0.1" \ No newline at end of file diff --git a/azure-functions-extension-fastapi/azure/functions/extension/fastapi/web.py b/azure-functions-extension-fastapi/azure/functions/extension/fastapi/web.py new file mode 100644 index 0000000..bff0812 --- /dev/null +++ b/azure-functions-extension-fastapi/azure/functions/extension/fastapi/web.py @@ -0,0 +1,82 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from typing import Callable +from fastapi import Request as FastApiRequest, Response as FastApiResponse, FastAPI +from fastapi.responses import ( + StreamingResponse as FastApiStreamingResponse, + HTMLResponse as FastApiHTMLResponse, + PlainTextResponse as FastApiPlainTextResponse, + RedirectResponse as FastApiRedirectResponse, + JSONResponse as FastApiJSONResponse, + UJSONResponse as FastApiUJSONResponse, + ORJSONResponse as FastApiORJSONResponse, + FileResponse as FastApiFileResponse, +) +from azure.functions.extension.base import WebServer, WebApp, RequestTrackerMeta, ResponseTrackerMeta, ResponseLabels +import uvicorn + +class Request(metaclass=RequestTrackerMeta): + request_type = FastApiRequest + +class Response(metaclass=ResponseTrackerMeta): + label = ResponseLabels.STANDARD + response_type = FastApiResponse + +class StreamingResponse(metaclass=ResponseTrackerMeta): + label = ResponseLabels.STREAMING + response_type = FastApiStreamingResponse + +class HTMLResponse(metaclass=ResponseTrackerMeta): + label = ResponseLabels.HTML + response_type = FastApiHTMLResponse + +class PlainTextResponse(metaclass=ResponseTrackerMeta): + label = ResponseLabels.PLAIN_TEXT + response_type = FastApiPlainTextResponse + +class RedirectResponse(metaclass=ResponseTrackerMeta): + label = ResponseLabels.REDIRECT + response_type = FastApiRedirectResponse + +class JSONResponse(metaclass=ResponseTrackerMeta): + label = ResponseLabels.JSON + response_type = FastApiJSONResponse + +class UJSONResponse(metaclass=ResponseTrackerMeta): + label = ResponseLabels.UJSON + response_type = FastApiUJSONResponse + +class ORJSONResponse(metaclass=ResponseTrackerMeta): + label = ResponseLabels.ORJSON + response_type = FastApiORJSONResponse + +class FileResponse(metaclass=ResponseTrackerMeta): + label = ResponseLabels.FILE + response_type = FastApiFileResponse + + +class WebApp(WebApp): + def __init__(self): + self.web_app = FastAPI() + + def route(self, func: Callable): + # Apply the api_route decorator + decorated_function = self.web_app.api_route( + "/{path:path}", + methods=["GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD", "PATCH", "TRACE"] + )(func) + + return decorated_function + + def get_app(self): + return self.web_app + + +class WebServer(WebServer): + async def serve(self): + uvicorn_config = uvicorn.Config(self.web_app, host=self.hostname, port=self.port) + + server = uvicorn.Server(uvicorn_config) + + return await server.serve() \ No newline at end of file diff --git a/azure-functions-extension-fastapi/setup.py b/azure-functions-extension-fastapi/setup.py new file mode 100644 index 0000000..590ba24 --- /dev/null +++ b/azure-functions-extension-fastapi/setup.py @@ -0,0 +1,41 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from setuptools import setup, find_packages +from azure.functions.extension.fastapi import __version__ + +EXTRA_REQUIRES = { + 'dev': [ + 'flake8~=4.0.1', + 'flake8-logging-format', + 'mypy', + 'pytest', + 'pytest-cov', + 'requests==2.*', + 'coverage' + ] +} + +setup( + name='azure-functions-extension-fastapi', + version=__version__, + author='Azure Functions team at Microsoft Corp.', + author_email='azurefunctions@microsoft.com', + description='FastApi Python worker extension for Azure Functions.', + packages=find_packages(exclude=[ + 'azure.functions.extension', 'azure.functions', + 'azure', 'tests', 'samples' + ]), + classifiers=[ + 'Programming Language :: Python :: 3', + 'License :: OSI Approved :: MIT License', + 'Operating System :: OS Independent', + ], + python_requires='>=3.7', + install_requires=[ + 'azure-functions-extension-base', + 'fastapi', + 'uvicorn' + ], + extras_require=EXTRA_REQUIRES, +) diff --git a/azure-functions-extension-fastapi/tests/__init__.py b/azure-functions-extension-fastapi/tests/__init__.py new file mode 100644 index 0000000..d138d18 --- /dev/null +++ b/azure-functions-extension-fastapi/tests/__init__.py @@ -0,0 +1,21 @@ +# 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/azure-functions-extension-fastapi/tests/test_code_quality.py b/azure-functions-extension-fastapi/tests/test_code_quality.py new file mode 100644 index 0000000..0e8ebfd --- /dev/null +++ b/azure-functions-extension-fastapi/tests/test_code_quality.py @@ -0,0 +1,35 @@ +# 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 + + +class TestCodeQuality(unittest.TestCase): + + def test_flake8(self): + try: + import flake8 # NoQA + except ImportError: + raise unittest.SkipTest('flake8 module is missing') + + 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', '--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 diff --git a/azure-functions-extension-fastapi/tests/test_web.py b/azure-functions-extension-fastapi/tests/test_web.py new file mode 100644 index 0000000..e69de29 From 0efa7bc9ca3a9050fe96e60bc03b5cbcad7e2f7d Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 15 Mar 2024 00:01:26 -0700 Subject: [PATCH 02/27] changes for http v2 extensions --- azure-functions-extension-base/README.md | 39 +--- .../functions/extension/base/__init__.py | 2 +- .../azure/functions/extension/base/web.py | 7 +- azure-functions-extension-base/pyproject.toml | 24 -- azure-functions-extension-base/setup.py | 1 + .../tests/test_web.py | 216 ++++++++++++++++++ azure-functions-extension-blob/pyproject.toml | 26 --- .../functions/extension/fastapi/__init__.py | 4 +- .../function_app.py | 23 ++ .../host.json | 15 ++ .../local.settings.json | 8 + .../requirements.txt | 6 + .../function_app.py | 23 ++ .../host.json | 15 ++ .../local.settings.json | 8 + .../requirements.txt | 6 + azure-functions-extension-fastapi/setup.py | 4 +- .../tests/test_web.py | 104 +++++++++ 18 files changed, 434 insertions(+), 97 deletions(-) delete mode 100644 azure-functions-extension-base/pyproject.toml delete mode 100644 azure-functions-extension-blob/pyproject.toml create mode 100644 azure-functions-extension-fastapi/samples/fastapi_samples_streaming_download/function_app.py create mode 100644 azure-functions-extension-fastapi/samples/fastapi_samples_streaming_download/host.json create mode 100644 azure-functions-extension-fastapi/samples/fastapi_samples_streaming_download/local.settings.json create mode 100644 azure-functions-extension-fastapi/samples/fastapi_samples_streaming_download/requirements.txt create mode 100644 azure-functions-extension-fastapi/samples/fastapi_samples_streaming_upload/function_app.py create mode 100644 azure-functions-extension-fastapi/samples/fastapi_samples_streaming_upload/host.json create mode 100644 azure-functions-extension-fastapi/samples/fastapi_samples_streaming_upload/local.settings.json create mode 100644 azure-functions-extension-fastapi/samples/fastapi_samples_streaming_upload/requirements.txt diff --git a/azure-functions-extension-base/README.md b/azure-functions-extension-base/README.md index 80d0c2d..0cfbeb1 100644 --- a/azure-functions-extension-base/README.md +++ b/azure-functions-extension-base/README.md @@ -1,38 +1,3 @@ # Azure Functions Extension Base library for Python -This is the base library for allowing Python Function Apps to recognize and bind to SDk-types. It is not to be used directly. -Instead, please reference one of the extending packages. - -Currently, the supported SDK-type bindings are: - -* Azure Storage Blob - -## Next steps - -### More sample code - -Get started with our [Blob samples](hhttps://github.com/Azure/azure-functions-python-extensions/tree/main/azure-functions-extension-blob/samples). - -Several samples are available in this GitHub repository. These samples provide example code for additional scenarios commonly encountered while working with Storage Blobs: - -* [blob_samples_blobclient](https://github.com/Azure/azure-functions-python-extensions/tree/main/azure-functions-extension-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/azure-functions-extension-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/azure-functions-extension-blob/samples/blob_samples_storagestreamdownloader) - Examples for using the StorageStreamDownloader type: - * From BlobTrigger - * From BlobInput - -### Additional documentation -For more information on the Azure Storage Blob SDK, see the [Azure Blob storage documentation](https://docs.microsoft.com/azure/storage/blobs/) on docs.microsoft.com -and the [Azure Storage Blobs README](https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/storage/azure-storage-blob). - -## Contributing -This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit https://cla.microsoft.com. - -When you submit a pull request, a CLA-bot will automatically determine whether you need to provide a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions provided by the bot. You will only need to do this once across all repos using our CLA. - -This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. \ No newline at end of file +This is the base library for allowing Python Function Apps to recognize and bind to SDk-types and HttpV2-types. It is not to be used directly. +Instead, please reference one of the extending packages. \ No newline at end of file diff --git a/azure-functions-extension-base/azure/functions/extension/base/__init__.py b/azure-functions-extension-base/azure/functions/extension/base/__init__.py index 06eb59c..cf2ef38 100644 --- a/azure-functions-extension-base/azure/functions/extension/base/__init__.py +++ b/azure-functions-extension-base/azure/functions/extension/base/__init__.py @@ -35,4 +35,4 @@ 'ResponseLabels', 'WebServer', 'WebApp' -] +] \ No newline at end of file diff --git a/azure-functions-extension-base/azure/functions/extension/base/web.py b/azure-functions-extension-base/azure/functions/extension/base/web.py index 41ea42b..6ce87f2 100644 --- a/azure-functions-extension-base/azure/functions/extension/base/web.py +++ b/azure-functions-extension-base/azure/functions/extension/base/web.py @@ -40,7 +40,7 @@ def __new__(cls, name, bases, dct, **kwargs): if request_type is None: raise Exception(f'Request type not provided for class {name}') - if cls._request_type is not None: + if cls._request_type is not None and cls._request_type != request_type: raise Exception(f'Only one request type shall be recorded for class {name} ' f'but found {cls._request_type} and {request_type}') cls._request_type = request_type @@ -69,7 +69,7 @@ def __new__(cls, name, bases, dct, **kwargs): raise Exception(f'Response label not provided for class {name}') if response_type is None: raise Exception(f'Response type not provided for class {name}') - if cls._response_types.get(label) is not None: + if cls._response_types.get(label) is not None and cls._response_types.get(label) != response_type: raise Exception(f'Only one response type shall be recorded for class {name} ' f'but found {cls._response_types.get(label)} and {response_type}') @@ -88,8 +88,7 @@ def get_response_type(cls, label): @classmethod def check_type(cls, pytype: type) -> bool: return cls._response_types is not None and any(issubclass(pytype, response_type) - for response_type in cls._response_types.values()) - + for response_type in cls._response_types.values()) class WebApp(metaclass=ModuleTrackerMeta): @abstractmethod diff --git a/azure-functions-extension-base/pyproject.toml b/azure-functions-extension-base/pyproject.toml deleted file mode 100644 index 4311a4b..0000000 --- a/azure-functions-extension-base/pyproject.toml +++ /dev/null @@ -1,24 +0,0 @@ -[build-system] -requires = ["setuptools >= 61.0"] -build-backend = "setuptools.build_meta" - -[project] -name = "azure-functions-extension-base" -dynamic = ["version"] -dependencies = ["flake8~=4.0.1", - "flake8-logging-format", - "mypy", - "pytest", - "pytest-cov", - "requests==2.*", - "coverage"] -requres-python = ">=3.7" -authors = ["Azure Functions team at Microsoft Corp."] -description = "Base Python worker extension for Azure Functions." -readme = "README.md" - -[tool.python-extensions-build] -mypy = false -pyright = false -type_check_samples = false -verifytypes = false \ No newline at end of file diff --git a/azure-functions-extension-base/setup.py b/azure-functions-extension-base/setup.py index 42b49d8..b1a97ec 100644 --- a/azure-functions-extension-base/setup.py +++ b/azure-functions-extension-base/setup.py @@ -31,4 +31,5 @@ ], python_requires='>=3.7', extras_require=EXTRA_REQUIRES, + test_suite='tests', ) diff --git a/azure-functions-extension-base/tests/test_web.py b/azure-functions-extension-base/tests/test_web.py index e69de29..f886e72 100644 --- a/azure-functions-extension-base/tests/test_web.py +++ b/azure-functions-extension-base/tests/test_web.py @@ -0,0 +1,216 @@ +import unittest +from unittest.mock import MagicMock, patch + +from azure.functions.extension.base import (ModuleTrackerMeta, RequestTrackerMeta, + ResponseTrackerMeta, WebApp, WebServer, + http_v2_enabled, ResponseLabels) + + +class TestModuleTrackerMeta(unittest.TestCase): + def setUp(self): + # Reset the _module attribute after each test + ModuleTrackerMeta._module = None + self.assertFalse(http_v2_enabled()) + + def test_classes_imported_from_same_module(self): + class TestClass1(metaclass=ModuleTrackerMeta): + pass + class TestClass2(metaclass=ModuleTrackerMeta): + pass + + self.assertEqual(ModuleTrackerMeta.get_module(), __name__) + self.assertTrue(ModuleTrackerMeta.module_imported()) + self.assertTrue(http_v2_enabled()) + + def test_class_imported_from_a_module(self): + class TestClass1(metaclass=ModuleTrackerMeta): + pass + self.assertEqual(ModuleTrackerMeta.get_module(), __name__) + self.assertTrue(ModuleTrackerMeta.module_imported()) + self.assertTrue(http_v2_enabled()) + + def test_classes_imported_from_different_modules(self): + class TestClass1(metaclass=ModuleTrackerMeta): + __module__ = "module1" + + self.assertEqual(ModuleTrackerMeta.get_module(), "module1") + self.assertTrue(ModuleTrackerMeta.module_imported()) + + with self.assertRaises(Exception) as context: + class TestClass2(metaclass=ModuleTrackerMeta): + __module__ = "module2" + + self.assertEqual(str(context.exception), + 'Only one web extension package shall be imported, ' + 'module1 and module2 are imported') + +class TestRequestTrackerMeta(unittest.TestCase): + class TestRequest1: + pass + + class TestRequest2: + pass + + class TestRequest3: + pass + + def setUp(self): + # Reset _request_type before each test + RequestTrackerMeta._request_type = None + + def test_request_type_not_provided(self): + # Define a class without providing the request_type attribute + with self.assertRaises(Exception) as context: + class TestClass(metaclass=RequestTrackerMeta): + pass + self.assertEqual(str(context.exception), 'Request type not provided for class TestClass') + + def test_single_request_type(self): + # Define a class providing a request_type attribute + class TestClass(metaclass=RequestTrackerMeta): + request_type = self.TestRequest1 + + # Ensure the request_type is correctly recorded + self.assertEqual(RequestTrackerMeta.get_request_type(), self.TestRequest1) + # Ensure check_type returns True for the provided request_type + self.assertTrue(RequestTrackerMeta.check_type(self.TestRequest1)) + + def test_multiple_request_types_same(self): + # Define a class providing the same request_type attribute + class TestClass1(metaclass=RequestTrackerMeta): + request_type = self.TestRequest1 + + # Ensure the request_type is correctly recorded + self.assertEqual(RequestTrackerMeta.get_request_type(), self.TestRequest1) + # Ensure check_type returns True for the provided request_type + self.assertTrue(RequestTrackerMeta.check_type(self.TestRequest1)) + + # Define another class providing the same request_type attribute + class TestClass2(metaclass=RequestTrackerMeta): + request_type = self.TestRequest1 + + # Ensure the request_type remains the same + self.assertEqual(RequestTrackerMeta.get_request_type(), self.TestRequest1) + # Ensure check_type still returns True for the original request_type + self.assertTrue(RequestTrackerMeta.check_type(self.TestRequest1)) + + def test_multiple_request_types_different(self): + # Define a class providing a different request_type attribute + class TestClass1(metaclass=RequestTrackerMeta): + request_type = self.TestRequest1 + + # Ensure the request_type is correctly recorded + self.assertEqual(RequestTrackerMeta.get_request_type(), self.TestRequest1) + # Ensure check_type returns True for the provided request_type + self.assertTrue(RequestTrackerMeta.check_type(self.TestRequest1)) + + # Define another class providing a different request_type attribute + with self.assertRaises(Exception) as context: + class TestClass2(metaclass=RequestTrackerMeta): + request_type = self.TestRequest2 + self.assertEqual(str(context.exception), + f'Only one request type shall be recorded for class TestClass2' + f' but found {self.TestRequest1} and {self.TestRequest2}') + + # Ensure the request_type remains the same after the exception + self.assertEqual(RequestTrackerMeta.get_request_type(), self.TestRequest1) + # Ensure check_type still returns True for the original request_type + self.assertTrue(RequestTrackerMeta.check_type(self.TestRequest1)) + +class TestResponseTrackerMeta(unittest.TestCase): + class MockResponse1: + pass + + class MockResponse2: + pass + + def test_classes_imported_from_same_module(self): + class TestResponse1(metaclass=ResponseTrackerMeta): + label = 'test_label_1' + response_type = self.MockResponse1 + + class TestResponse2(metaclass=ResponseTrackerMeta): + label = 'test_label_2' + response_type = self.MockResponse2 + + self.assertEqual(ResponseTrackerMeta.get_response_type('test_label_1'), self.MockResponse1) + self.assertEqual(ResponseTrackerMeta.get_response_type('test_label_2'), self.MockResponse2) + self.assertIsNone(ResponseTrackerMeta.get_response_type('non_existing_label')) + self.assertTrue(ResponseTrackerMeta.check_type(self.MockResponse1)) + self.assertTrue(ResponseTrackerMeta.check_type(self.MockResponse2)) + + def test_class_imported_from_a_module(self): + class TestResponse1(metaclass=ResponseTrackerMeta): + label = 'test_label_1' + response_type = self.MockResponse1 + + self.assertEqual(ResponseTrackerMeta.get_response_type('test_label_1'), self.MockResponse1) + self.assertIsNone(ResponseTrackerMeta.get_response_type('non_existing_label')) + self.assertTrue(ResponseTrackerMeta.check_type(self.MockResponse1)) + self.assertFalse(ResponseTrackerMeta.check_type(self.MockResponse2)) + + def test_classes_imported_from_different_modules(self): + class TestResponse1(metaclass=ResponseTrackerMeta): + __module__ = "module1" + label = 'test_label_1' + response_type = self.MockResponse1 + + with self.assertRaises(Exception) as context: + class TestResponse2(metaclass=ResponseTrackerMeta): + __module__ = "module2" + label = 'test_label_1' + response_type = self.MockResponse2 + + self.assertEqual(str(context.exception), + 'Only one response type shall be recorded for class TestResponse2 ' + f'but found {self.MockResponse1} and {self.MockResponse2}') + + def test_different_labels(self): + class TestResponse1(metaclass=ResponseTrackerMeta): + label = ResponseLabels.STANDARD + response_type = self.MockResponse1 + + class TestResponse2(metaclass=ResponseTrackerMeta): + label = ResponseLabels.STREAMING + response_type = self.MockResponse2 + + self.assertEqual(ResponseTrackerMeta.get_response_type(ResponseLabels.STANDARD), self.MockResponse1) + self.assertEqual(ResponseTrackerMeta.get_response_type(ResponseLabels.STREAMING), self.MockResponse2) + self.assertTrue(ResponseTrackerMeta.check_type(self.MockResponse1)) + self.assertTrue(ResponseTrackerMeta.check_type(self.MockResponse2)) + +class TestWebApp(unittest.TestCase): + def test_route_and_get_app(self): + class MockWebApp(WebApp): + def route(self, func): + return + + def get_app(self): + return "MockApp" + + app = MockWebApp() + self.assertEqual(app.get_app(), "MockApp") + +class TestWebServer(unittest.TestCase): + def test_web_server_initialization(self): + class MockWebApp(WebApp): + def route(self, func): + pass + + def get_app(self): + return "MockApp" + + mock_web_app = MockWebApp() + server = WebServer("localhost", 8080, mock_web_app) + self.assertEqual(server.hostname, "localhost") + self.assertEqual(server.port, 8080) + self.assertEqual(server.web_app, "MockApp") + +class TestHttpV2Enabled(unittest.TestCase): + @patch('azure.functions.extension.base.ModuleTrackerMeta.module_imported') + def test_http_v2_enabled(self, mock_module_imported): + mock_module_imported.return_value = True + self.assertTrue(http_v2_enabled()) + + mock_module_imported.return_value = False + self.assertFalse(http_v2_enabled()) \ No newline at end of file diff --git a/azure-functions-extension-blob/pyproject.toml b/azure-functions-extension-blob/pyproject.toml deleted file mode 100644 index 799e4bf..0000000 --- a/azure-functions-extension-blob/pyproject.toml +++ /dev/null @@ -1,26 +0,0 @@ -[build-system] -requires = ["setuptools >= 61.0"] -build-backend = "setuptools.build_meta" - -[project] -name = "azure-functions-extension-blob" -dynamic = ["version"] -dependencies = ["flake8~=4.0.1", - "flake8-logging-format", - "mypy", - "pytest", - "pytest-cov", - "requests==2.*", - "coverage", - "azure-functions-extension-base", - "azure-storage-blob"] -requres-python = ">=3.7" -authors = ["Azure Functions team at Microsoft Corp."] -description = "Blob Python worker extension for Azure Functions." -readme = "README.md" - -[tool.python-extensions-build] -mypy = false -pyright = false -type_check_samples = false -verifytypes = false \ No newline at end of file diff --git a/azure-functions-extension-fastapi/azure/functions/extension/fastapi/__init__.py b/azure-functions-extension-fastapi/azure/functions/extension/fastapi/__init__.py index 3885b65..3ac1a9e 100644 --- a/azure-functions-extension-fastapi/azure/functions/extension/fastapi/__init__.py +++ b/azure-functions-extension-fastapi/azure/functions/extension/fastapi/__init__.py @@ -13,6 +13,4 @@ __all__ = ['WebServer', 'WebApp', 'Request', 'Response', 'StreamingResponse', 'HTMLResponse', 'PlainTextResponse', 'RedirectResponse', 'JSONResponse', 'UJSONResponse', - 'ORJSONResponse', 'FileResponse'] - -__version__ = "0.0.1" \ No newline at end of file + 'ORJSONResponse', 'FileResponse'] \ No newline at end of file diff --git a/azure-functions-extension-fastapi/samples/fastapi_samples_streaming_download/function_app.py b/azure-functions-extension-fastapi/samples/fastapi_samples_streaming_download/function_app.py new file mode 100644 index 0000000..48eda6e --- /dev/null +++ b/azure-functions-extension-fastapi/samples/fastapi_samples_streaming_download/function_app.py @@ -0,0 +1,23 @@ +# This Azure Function streams real-time sensor data using Server-Sent Events (SSE). +# It simulates a sensor network transmitting temperature and humidity readings, +# which can be consumed by IoT dashboards or analytics pipelines. + +import time +import azure.functions as func +from azure.functions.extension.fastapi import Request, StreamingResponse + +app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) + +def generate_sensor_data(): + """Generate real-time sensor data.""" + for i in range(10): + # Simulate temperature and humidity readings + temperature = 20 + i + humidity = 50 + i + yield f"data: {{'temperature': {temperature}, 'humidity': {humidity}}}\n\n" + time.sleep(1) + +@app.route(route="stream", methods=[func.HttpMethod.GET]) +async def stream_sensor_data(req: Request) -> StreamingResponse: + """Endpoint to stream real-time sensor data.""" + return StreamingResponse(generate_sensor_data(), media_type='text/event-stream') diff --git a/azure-functions-extension-fastapi/samples/fastapi_samples_streaming_download/host.json b/azure-functions-extension-fastapi/samples/fastapi_samples_streaming_download/host.json new file mode 100644 index 0000000..9df9136 --- /dev/null +++ b/azure-functions-extension-fastapi/samples/fastapi_samples_streaming_download/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/azure-functions-extension-fastapi/samples/fastapi_samples_streaming_download/local.settings.json b/azure-functions-extension-fastapi/samples/fastapi_samples_streaming_download/local.settings.json new file mode 100644 index 0000000..c3c2a89 --- /dev/null +++ b/azure-functions-extension-fastapi/samples/fastapi_samples_streaming_download/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/azure-functions-extension-fastapi/samples/fastapi_samples_streaming_download/requirements.txt b/azure-functions-extension-fastapi/samples/fastapi_samples_streaming_download/requirements.txt new file mode 100644 index 0000000..c871799 --- /dev/null +++ b/azure-functions-extension-fastapi/samples/fastapi_samples_streaming_download/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 +azure-functions-extension-fastapi \ No newline at end of file diff --git a/azure-functions-extension-fastapi/samples/fastapi_samples_streaming_upload/function_app.py b/azure-functions-extension-fastapi/samples/fastapi_samples_streaming_upload/function_app.py new file mode 100644 index 0000000..f03c135 --- /dev/null +++ b/azure-functions-extension-fastapi/samples/fastapi_samples_streaming_upload/function_app.py @@ -0,0 +1,23 @@ +# This Azure Function receives streaming data from a client and processes it in real-time. +# It demonstrates streaming upload capabilities for scenarios such as uploading large files, +# processing continuous data streams, or handling IoT device data. + +import azure.functions as func +from azure.functions.extension.fastapi import Request, JSONResponse + +app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) + +@app.route(route="streaming_upload", methods=[func.HttpMethod.POST]) +async def streaming_upload(req: Request) -> JSONResponse: + """Handle streaming upload requests.""" + # Process each chunk of data as it arrives + async for chunk in req.stream(): + process_data_chunk(chunk) + + # Once all data is received, return a JSON response indicating successful processing + return JSONResponse({"status": "Data uploaded and processed successfully"}) + +def process_data_chunk(chunk: bytes): + """Process each data chunk.""" + # Add custom processing logic here + pass diff --git a/azure-functions-extension-fastapi/samples/fastapi_samples_streaming_upload/host.json b/azure-functions-extension-fastapi/samples/fastapi_samples_streaming_upload/host.json new file mode 100644 index 0000000..9df9136 --- /dev/null +++ b/azure-functions-extension-fastapi/samples/fastapi_samples_streaming_upload/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/azure-functions-extension-fastapi/samples/fastapi_samples_streaming_upload/local.settings.json b/azure-functions-extension-fastapi/samples/fastapi_samples_streaming_upload/local.settings.json new file mode 100644 index 0000000..c3c2a89 --- /dev/null +++ b/azure-functions-extension-fastapi/samples/fastapi_samples_streaming_upload/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/azure-functions-extension-fastapi/samples/fastapi_samples_streaming_upload/requirements.txt b/azure-functions-extension-fastapi/samples/fastapi_samples_streaming_upload/requirements.txt new file mode 100644 index 0000000..c871799 --- /dev/null +++ b/azure-functions-extension-fastapi/samples/fastapi_samples_streaming_upload/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 +azure-functions-extension-fastapi \ No newline at end of file diff --git a/azure-functions-extension-fastapi/setup.py b/azure-functions-extension-fastapi/setup.py index 590ba24..611399c 100644 --- a/azure-functions-extension-fastapi/setup.py +++ b/azure-functions-extension-fastapi/setup.py @@ -2,7 +2,6 @@ # Licensed under the MIT License. from setuptools import setup, find_packages -from azure.functions.extension.fastapi import __version__ EXTRA_REQUIRES = { 'dev': [ @@ -18,7 +17,7 @@ setup( name='azure-functions-extension-fastapi', - version=__version__, + version='0.0.1', author='Azure Functions team at Microsoft Corp.', author_email='azurefunctions@microsoft.com', description='FastApi Python worker extension for Azure Functions.', @@ -38,4 +37,5 @@ 'uvicorn' ], extras_require=EXTRA_REQUIRES, + test_suite='tests' ) diff --git a/azure-functions-extension-fastapi/tests/test_web.py b/azure-functions-extension-fastapi/tests/test_web.py index e69de29..2468a82 100644 --- a/azure-functions-extension-fastapi/tests/test_web.py +++ b/azure-functions-extension-fastapi/tests/test_web.py @@ -0,0 +1,104 @@ +import asyncio +import unittest +from unittest.mock import MagicMock, patch +from fastapi import FastAPI +from azure.functions.extension.base import ResponseLabels, RequestTrackerMeta, ResponseTrackerMeta +from azure.functions.extension.fastapi import ( + WebApp, + WebServer, + Request as FastApiRequest, + Response as FastApiResponse, + JSONResponse, +) + +class TestRequestTrackerMeta(unittest.TestCase): + def test_request_type_defined(self): + class Request(metaclass=RequestTrackerMeta): + request_type = FastApiRequest + + self.assertTrue(hasattr(Request, 'request_type')) + self.assertEqual(Request.request_type, FastApiRequest) + + def test_request_type_undefined(self): + with self.assertRaises(Exception) as context: + class Request(metaclass=RequestTrackerMeta): + pass + + self.assertTrue('Request type not provided' in str(context.exception)) + + +class TestResponseTrackerMeta(unittest.TestCase): + def test_response_labels_defined(self): + class Response(metaclass=ResponseTrackerMeta): + label = ResponseLabels.STANDARD + response_type = FastApiResponse + + self.assertTrue(hasattr(Response, 'label')) + self.assertTrue(hasattr(Response, 'response_type')) + self.assertEqual(Response.label, ResponseLabels.STANDARD) + self.assertEqual(Response.response_type, FastApiResponse) + + def test_response_labels_undefined(self): + with self.assertRaises(Exception) as context: + class Response(metaclass=ResponseTrackerMeta): + pass + + self.assertTrue('Response label not provided' in str(context.exception)) + + def test_multiple_response_labels(self): + with self.assertRaises(Exception) as context: + class Response1(metaclass=ResponseTrackerMeta): + label = ResponseLabels.STANDARD + response_type = FastApiResponse + + class Response2(metaclass=ResponseTrackerMeta): + label = ResponseLabels.STANDARD + response_type = JSONResponse + + self.assertTrue('Only one response type shall be recorded' in str(context.exception)) + + +class TestWebApp(unittest.TestCase): + def setUp(self): + self.web_app = WebApp() + + def test_route(self): + @self.web_app.route + def test_route(request: FastApiRequest): + return {'message': 'Hello'} + + self.assertTrue('/{path:path}' in [endpoint.path for endpoint in self.web_app.web_app.router.routes]) + route = [endpoint for endpoint in self.web_app.web_app.router.routes if endpoint.path == '/{path:path}'][0] + self.assertEqual(route.methods, {"GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD", "PATCH", "TRACE"}) + + + def test_get_app(self): + self.assertIsInstance(self.web_app.get_app(), FastAPI) + +class TestWebServer(unittest.TestCase): + def setUp(self): + class TestApp(): + pass + self.test_app = TestApp() + self.web_app_mock = MagicMock().get_app() + self.web_app_mock.get_app.return_value = self.test_app + self.hostname = 'localhost' + self.port = 8000 + self.web_server = WebServer(self.hostname, self.port, self.web_app_mock) + + @patch('uvicorn.Config') + @patch('uvicorn.Server') + def test_serve(self, server_mock, config_mock): + async def serve(): + await asyncio.sleep(0) + config_instance_mock = config_mock.return_value + server_instance_mock = server_mock.return_value + server_instance_mock.serve = serve # Mock the serve method to return a CoroutineMock + + asyncio.get_event_loop().run_until_complete(self.web_server.serve()) + + config_mock.assert_called_once_with(self.test_app, host=self.hostname, port=self.port) + server_mock.assert_called_once_with(config_instance_mock) + + async def run_serve(self): + await self.web_server.serve() \ No newline at end of file From d614b44fb5480c3fba3bb0accf7411a42ac314f9 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 15 Mar 2024 09:47:32 -0700 Subject: [PATCH 03/27] add ci testing pipeline for base --- .github/workflows/ci_ut_ext_base_workflow.yml | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 .github/workflows/ci_ut_ext_base_workflow.yml diff --git a/.github/workflows/ci_ut_ext_base_workflow.yml b/.github/workflows/ci_ut_ext_base_workflow.yml new file mode 100644 index 0000000..de67465 --- /dev/null +++ b/.github/workflows/ci_ut_ext_base_workflow.yml @@ -0,0 +1,55 @@ +name: UT CI Run for Python Extension Base + +on: + push: + paths: + - 'azure-functions-extension-base/**' + pull_request: + branches: [ dev, master, main, release/* ] + paths: + - 'azure-functions-extension-base/**' + +jobs: + build: + name: "Python Extension UT CI Run" + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: [ 3.7, 3.8, 3.9, "3.10", "3.11" ] + permissions: read-all + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install -U -e .[dev] + + - name: Run Unit Tests + run: | + python -m pytest -q --instafail --cov=. --cov-report xml --cov-branch azure-functions-extension-base/tests + + - name: Upload Coverage to Codecov + uses: codecov/codecov-action@v3 + with: + file: ./coverage.xml + flags: unittests + name: codecov + fail_ci_if_error: false + + - name: Notify dedicated teams channel + uses: jdcargile/ms-teams-notification@v1.4 + with: + github-token: ${{ github.token }} # this will use the runner's token. + ms-teams-webhook-uri: ${{ secrets.MS_TEAMS_WEBHOOK_URI }} + notification-summary: "Python Extension Base UT CI Results" + notification-color: 17a2b8 + timezone: America/Denver + verbose-logging: true From 841a42d467177f4734951e82db85f74772f23c47 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 15 Mar 2024 09:50:44 -0700 Subject: [PATCH 04/27] fix pipeline --- .github/workflows/ci_ut_ext_base_workflow.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci_ut_ext_base_workflow.yml b/.github/workflows/ci_ut_ext_base_workflow.yml index de67465..5c27da0 100644 --- a/.github/workflows/ci_ut_ext_base_workflow.yml +++ b/.github/workflows/ci_ut_ext_base_workflow.yml @@ -28,6 +28,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install dependencies + working-directory: azure-functions-extension-base run: | python -m pip install --upgrade pip python -m pip install -U -e .[dev] @@ -39,7 +40,7 @@ jobs: - name: Upload Coverage to Codecov uses: codecov/codecov-action@v3 with: - file: ./coverage.xml + file: ./azure-functions-extension-base/coverage.xml flags: unittests name: codecov fail_ci_if_error: false From e3dff8ff159a1dc81742182f163759f689a3c642 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 15 Mar 2024 09:52:28 -0700 Subject: [PATCH 05/27] fix --- azure-functions-extension-base/setup.py | 2 +- azure-functions-extension-fastapi/setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/azure-functions-extension-base/setup.py b/azure-functions-extension-base/setup.py index 1947de4..c914ab3 100644 --- a/azure-functions-extension-base/setup.py +++ b/azure-functions-extension-base/setup.py @@ -18,5 +18,5 @@ 'Operating System :: OS Independent', ], python_requires='>=3.9' - test_suite='tests', + test_suite='azure-functions-extension-base/tests', ) diff --git a/azure-functions-extension-fastapi/setup.py b/azure-functions-extension-fastapi/setup.py index 611399c..38be2cd 100644 --- a/azure-functions-extension-fastapi/setup.py +++ b/azure-functions-extension-fastapi/setup.py @@ -37,5 +37,5 @@ 'uvicorn' ], extras_require=EXTRA_REQUIRES, - test_suite='tests' + test_suite='azure-functions-extension-fastapi/tests' ) From 4d0b07f8442f801cb16468f66783b5a36f80960a Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 15 Mar 2024 09:54:30 -0700 Subject: [PATCH 06/27] fix --- azure-functions-extension-base/setup.py | 1 - azure-functions-extension-fastapi/setup.py | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/azure-functions-extension-base/setup.py b/azure-functions-extension-base/setup.py index c914ab3..b24c8a7 100644 --- a/azure-functions-extension-base/setup.py +++ b/azure-functions-extension-base/setup.py @@ -18,5 +18,4 @@ 'Operating System :: OS Independent', ], python_requires='>=3.9' - test_suite='azure-functions-extension-base/tests', ) diff --git a/azure-functions-extension-fastapi/setup.py b/azure-functions-extension-fastapi/setup.py index 38be2cd..4d98c9c 100644 --- a/azure-functions-extension-fastapi/setup.py +++ b/azure-functions-extension-fastapi/setup.py @@ -36,6 +36,5 @@ 'fastapi', 'uvicorn' ], - extras_require=EXTRA_REQUIRES, - test_suite='azure-functions-extension-fastapi/tests' + extras_require=EXTRA_REQUIRES ) From f684979d2c3e25f1e131f4c7a7b553868b72b93c Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 15 Mar 2024 10:08:23 -0700 Subject: [PATCH 07/27] fix --- .github/workflows/ci_ut_ext_base_workflow.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci_ut_ext_base_workflow.yml b/.github/workflows/ci_ut_ext_base_workflow.yml index 5c27da0..0ebd648 100644 --- a/.github/workflows/ci_ut_ext_base_workflow.yml +++ b/.github/workflows/ci_ut_ext_base_workflow.yml @@ -34,8 +34,9 @@ jobs: python -m pip install -U -e .[dev] - name: Run Unit Tests + working-directory: azure-functions-extension-base run: | - python -m pytest -q --instafail --cov=. --cov-report xml --cov-branch azure-functions-extension-base/tests + python -m pytest -q --instafail --cov=. --cov-report xml --cov-branch tests - name: Upload Coverage to Codecov uses: codecov/codecov-action@v3 From dccb68ef50bc2c43a39fc52e3912ce7a1e2ea0a6 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 15 Mar 2024 10:12:00 -0700 Subject: [PATCH 08/27] fix --- azure-functions-extension-base/setup.py | 27 +++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/azure-functions-extension-base/setup.py b/azure-functions-extension-base/setup.py index b24c8a7..da2d1da 100644 --- a/azure-functions-extension-base/setup.py +++ b/azure-functions-extension-base/setup.py @@ -3,6 +3,18 @@ from setuptools import setup, find_packages +EXTRA_REQUIRES = { + 'dev': [ + 'flake8~=4.0.1', + 'flake8-logging-format', + 'mypy', + 'pytest', + 'pytest-cov', + 'requests==2.*', + 'coverage' + ] +} + setup( name='azure-functions-extension-base', version='1.0.0a1', @@ -13,9 +25,20 @@ 'azure.functions.extension', 'azure.functions', 'azure', 'tests' ]), classifiers=[ - 'Programming Language :: Python :: 3', 'License :: OSI Approved :: MIT License', - 'Operating System :: OS Independent', + 'Intended Audience :: Developers', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Operating System :: Microsoft :: Windows', + 'Operating System :: POSIX', + 'Operating System :: MacOS :: MacOS X', + 'Environment :: Web Environment', + 'Development Status :: 5 - Production/Stable', ], + license='MIT', + extras_require=EXTRA_REQUIRES, python_requires='>=3.9' ) From 704a2bed9721fb88cba3f6c05ee847ed46cc0cbe Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 15 Mar 2024 10:12:39 -0700 Subject: [PATCH 09/27] fix --- azure-functions-extension-base/setup.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/azure-functions-extension-base/setup.py b/azure-functions-extension-base/setup.py index da2d1da..06042e7 100644 --- a/azure-functions-extension-base/setup.py +++ b/azure-functions-extension-base/setup.py @@ -28,9 +28,6 @@ 'License :: OSI Approved :: MIT License', 'Intended Audience :: Developers', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Operating System :: Microsoft :: Windows', 'Operating System :: POSIX', From c0313ec4a6cfb09cd7eed93d8b3c81f26ddf96b3 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 15 Mar 2024 10:14:50 -0700 Subject: [PATCH 10/27] fix --- azure-functions-extension-base/setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/azure-functions-extension-base/setup.py b/azure-functions-extension-base/setup.py index 06042e7..18800de 100644 --- a/azure-functions-extension-base/setup.py +++ b/azure-functions-extension-base/setup.py @@ -11,7 +11,8 @@ 'pytest', 'pytest-cov', 'requests==2.*', - 'coverage' + 'coverage', + "pytest-instafail" ] } From 178e4c8a648c178e45776c54e716bf04fce29fd8 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 15 Mar 2024 10:16:38 -0700 Subject: [PATCH 11/27] fix --- .github/workflows/ci_ut_ext_base_workflow.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci_ut_ext_base_workflow.yml b/.github/workflows/ci_ut_ext_base_workflow.yml index 0ebd648..cbd73ce 100644 --- a/.github/workflows/ci_ut_ext_base_workflow.yml +++ b/.github/workflows/ci_ut_ext_base_workflow.yml @@ -2,6 +2,7 @@ name: UT CI Run for Python Extension Base on: push: + branches: [ dev, master, main, release/* ] paths: - 'azure-functions-extension-base/**' pull_request: From 2b3d281afd0b7a3d65348056d96a69cfc5548a3f Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 15 Mar 2024 10:19:32 -0700 Subject: [PATCH 12/27] fix --- .github/workflows/linter.yml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 9fa123f..9f743d6 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -15,7 +15,18 @@ name: Lint Code Base ############################# # Start the job on all push # ############################# -on: [ push, pull_request, workflow_dispatch ] +on: + workflow_dispatch: + push: + branches: + - dev + - main + - 'release/*' + pull_request: + branches: + - dev + - main + - 'release/*' ############### # Set the Job # From b6e0a6e2721a201096549d269884120f260e7f5e Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 15 Mar 2024 10:22:48 -0700 Subject: [PATCH 13/27] fix --- .github/workflows/ci_ut_ext_base_workflow.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci_ut_ext_base_workflow.yml b/.github/workflows/ci_ut_ext_base_workflow.yml index cbd73ce..1981742 100644 --- a/.github/workflows/ci_ut_ext_base_workflow.yml +++ b/.github/workflows/ci_ut_ext_base_workflow.yml @@ -49,10 +49,11 @@ jobs: - name: Notify dedicated teams channel uses: jdcargile/ms-teams-notification@v1.4 + if: failure() with: github-token: ${{ github.token }} # this will use the runner's token. ms-teams-webhook-uri: ${{ secrets.MS_TEAMS_WEBHOOK_URI }} - notification-summary: "Python Extension Base UT CI Results" + notification-summary: "Python Extension Base UT CI Failed ${{ matrix.python-version }}" notification-color: 17a2b8 timezone: America/Denver - verbose-logging: true + verbose-logging: false From 9508d2e02e9753f68e952ae742e646ef926f8910 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 15 Mar 2024 10:25:16 -0700 Subject: [PATCH 14/27] fix --- .github/workflows/ci_ut_ext_base_workflow.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci_ut_ext_base_workflow.yml b/.github/workflows/ci_ut_ext_base_workflow.yml index 1981742..20052ff 100644 --- a/.github/workflows/ci_ut_ext_base_workflow.yml +++ b/.github/workflows/ci_ut_ext_base_workflow.yml @@ -53,7 +53,7 @@ jobs: with: github-token: ${{ github.token }} # this will use the runner's token. ms-teams-webhook-uri: ${{ secrets.MS_TEAMS_WEBHOOK_URI }} - notification-summary: "Python Extension Base UT CI Failed ${{ matrix.python-version }}" + notification-summary: "Python Extension Base UT CI Failed for Python ${{ matrix.python-version }}" notification-color: 17a2b8 timezone: America/Denver verbose-logging: false From 68b7cc7fe0edfb34a75fe2b2556089b0258fcb25 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 15 Mar 2024 10:37:40 -0700 Subject: [PATCH 15/27] fix --- .github/workflows/ci_ut_ext_base_workflow.yml | 2 +- .github/workflows/ci_ut_ext_blob_workflow.yml | 62 +++++++++++++++++++ .../workflows/ci_ut_ext_fastapi_workflow.yml | 60 ++++++++++++++++++ azure-functions-extension-base/setup.py | 2 +- 4 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/ci_ut_ext_blob_workflow.yml create mode 100644 .github/workflows/ci_ut_ext_fastapi_workflow.yml diff --git a/.github/workflows/ci_ut_ext_base_workflow.yml b/.github/workflows/ci_ut_ext_base_workflow.yml index 20052ff..5d40c20 100644 --- a/.github/workflows/ci_ut_ext_base_workflow.yml +++ b/.github/workflows/ci_ut_ext_base_workflow.yml @@ -53,7 +53,7 @@ jobs: with: github-token: ${{ github.token }} # this will use the runner's token. ms-teams-webhook-uri: ${{ secrets.MS_TEAMS_WEBHOOK_URI }} - notification-summary: "Python Extension Base UT CI Failed for Python ${{ matrix.python-version }}" + notification-summary: "Python Extension Base UT CI Failed for Python ${{ matrix.python-version }} \n cc:@python_ext_repo" notification-color: 17a2b8 timezone: America/Denver verbose-logging: false diff --git a/.github/workflows/ci_ut_ext_blob_workflow.yml b/.github/workflows/ci_ut_ext_blob_workflow.yml new file mode 100644 index 0000000..1b5b826 --- /dev/null +++ b/.github/workflows/ci_ut_ext_blob_workflow.yml @@ -0,0 +1,62 @@ +name: UT CI Run for Python Extension Blob + +on: + push: + branches: [ dev, master, main, release/* ] + paths: + - 'azure-functions-extension-base/**' + - 'azure-functions-extension-blob/**' + pull_request: + branches: [ dev, master, main, release/* ] + paths: + - 'azure-functions-extension-base/**' + - 'azure-functions-extension-blob/**' + +jobs: + build: + name: "Python Extension UT CI Run" + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: [ 3.9, "3.10", "3.11" ] + permissions: read-all + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + working-directory: azure-functions-extension-blob + run: | + python -m pip install --upgrade pip + python -m pip install -U -e .[dev] + python -m pip install -e ../azure-functions-extension-base + + - name: Run Unit Tests + working-directory: azure-functions-extension-blob + run: | + python -m pytest -q --instafail --cov=. --cov-report xml --cov-branch tests + + - name: Upload Coverage to Codecov + uses: codecov/codecov-action@v3 + with: + file: ./azure-functions-extension-blob/coverage.xml + flags: unittests + name: codecov + fail_ci_if_error: false + + - name: Notify dedicated teams channel + uses: jdcargile/ms-teams-notification@v1.4 + if: failure() + with: + github-token: ${{ github.token }} # this will use the runner's token. + ms-teams-webhook-uri: ${{ secrets.MS_TEAMS_WEBHOOK_URI }} + notification-summary: "Python Extension Blob UT CI Failed for Python ${{ matrix.python-version }} \n cc:@python_ext_repo" + notification-color: 17a2b8 + timezone: America/Denver + verbose-logging: false diff --git a/.github/workflows/ci_ut_ext_fastapi_workflow.yml b/.github/workflows/ci_ut_ext_fastapi_workflow.yml new file mode 100644 index 0000000..a880ff0 --- /dev/null +++ b/.github/workflows/ci_ut_ext_fastapi_workflow.yml @@ -0,0 +1,60 @@ +name: UT CI Run for Python Extension FastApi + +on: + push: + branches: [ dev, master, main, release/* ] + paths: + - 'azure-functions-extension-fastapi/**' + pull_request: + branches: [ dev, master, main, release/* ] + paths: + - 'azure-functions-extension-fastapi/**' + +jobs: + build: + name: "Python Extension UT CI Run" + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: [ 3.7, 3.8, 3.9, "3.10", "3.11" ] + permissions: read-all + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + working-directory: azure-functions-extension-fastapi + run: | + python -m pip install --upgrade pip + python -m pip install -U -e .[dev] + python -m pip install -e ../azure-functions-extension-base + + - name: Run Unit Tests + working-directory: azure-functions-extension-fastapi + run: | + python -m pytest -q --instafail --cov=. --cov-report xml --cov-branch tests + + - name: Upload Coverage to Codecov + uses: codecov/codecov-action@v3 + with: + file: ./azure-functions-extension-fastapi/coverage.xml + flags: unittests + name: codecov + fail_ci_if_error: false + + - name: Notify dedicated teams channel + uses: jdcargile/ms-teams-notification@v1.4 + if: failure() + with: + github-token: ${{ github.token }} # this will use the runner's token. + ms-teams-webhook-uri: ${{ secrets.MS_TEAMS_WEBHOOK_URI }} + notification-summary: "Python Extension FastApi UT CI Failed for Python ${{ matrix.python-version }} \n cc:@python_ext_repo" + notification-color: 17a2b8 + timezone: America/Denver + verbose-logging: false diff --git a/azure-functions-extension-base/setup.py b/azure-functions-extension-base/setup.py index 18800de..be2bcbb 100644 --- a/azure-functions-extension-base/setup.py +++ b/azure-functions-extension-base/setup.py @@ -38,5 +38,5 @@ ], license='MIT', extras_require=EXTRA_REQUIRES, - python_requires='>=3.9' + python_requires='>=3.7' ) From 35f35153dc8add12682cb2255c507335d898a4dd Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 15 Mar 2024 10:48:10 -0700 Subject: [PATCH 16/27] fix --- azure-functions-extension-blob/setup.py | 25 ++++++++++++++++++++-- azure-functions-extension-fastapi/setup.py | 3 ++- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/azure-functions-extension-blob/setup.py b/azure-functions-extension-blob/setup.py index 2393ce2..f4c38fe 100644 --- a/azure-functions-extension-blob/setup.py +++ b/azure-functions-extension-blob/setup.py @@ -3,6 +3,19 @@ from setuptools import setup, find_packages +EXTRA_REQUIRES = { + 'dev': [ + 'flake8~=4.0.1', + 'flake8-logging-format', + 'mypy', + 'pytest', + 'pytest-cov', + 'requests==2.*', + 'coverage', + "pytest-instafail" + ] +} + setup( name='azure-functions-extension-blob', version='1.0.0a1', @@ -14,10 +27,18 @@ 'azure', 'tests', 'samples' ]), classifiers=[ - 'Programming Language :: Python :: 3', 'License :: OSI Approved :: MIT License', - 'Operating System :: OS Independent', + 'Intended Audience :: Developers', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.9', + 'Operating System :: Microsoft :: Windows', + 'Operating System :: POSIX', + 'Operating System :: MacOS :: MacOS X', + 'Environment :: Web Environment', + 'Development Status :: 5 - Production/Stable', ], + license='MIT', + extras_require=EXTRA_REQUIRES, python_requires='>=3.9', install_requires=[ 'azure-functions-extension-base', diff --git a/azure-functions-extension-fastapi/setup.py b/azure-functions-extension-fastapi/setup.py index 4d98c9c..02e493a 100644 --- a/azure-functions-extension-fastapi/setup.py +++ b/azure-functions-extension-fastapi/setup.py @@ -11,7 +11,8 @@ 'pytest', 'pytest-cov', 'requests==2.*', - 'coverage' + 'coverage', + 'pytest-instafail' ] } From 51f414e13cc0696b3b806e329350e8f539cd7743 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 15 Mar 2024 10:56:36 -0700 Subject: [PATCH 17/27] fix --- .github/workflows/ci_ut_ext_base_workflow.yml | 2 +- .github/workflows/ci_ut_ext_blob_workflow.yml | 2 +- .github/workflows/ci_ut_ext_fastapi_workflow.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci_ut_ext_base_workflow.yml b/.github/workflows/ci_ut_ext_base_workflow.yml index 5d40c20..20052ff 100644 --- a/.github/workflows/ci_ut_ext_base_workflow.yml +++ b/.github/workflows/ci_ut_ext_base_workflow.yml @@ -53,7 +53,7 @@ jobs: with: github-token: ${{ github.token }} # this will use the runner's token. ms-teams-webhook-uri: ${{ secrets.MS_TEAMS_WEBHOOK_URI }} - notification-summary: "Python Extension Base UT CI Failed for Python ${{ matrix.python-version }} \n cc:@python_ext_repo" + notification-summary: "Python Extension Base UT CI Failed for Python ${{ matrix.python-version }}" notification-color: 17a2b8 timezone: America/Denver verbose-logging: false diff --git a/.github/workflows/ci_ut_ext_blob_workflow.yml b/.github/workflows/ci_ut_ext_blob_workflow.yml index 1b5b826..1e5a79f 100644 --- a/.github/workflows/ci_ut_ext_blob_workflow.yml +++ b/.github/workflows/ci_ut_ext_blob_workflow.yml @@ -56,7 +56,7 @@ jobs: with: github-token: ${{ github.token }} # this will use the runner's token. ms-teams-webhook-uri: ${{ secrets.MS_TEAMS_WEBHOOK_URI }} - notification-summary: "Python Extension Blob UT CI Failed for Python ${{ matrix.python-version }} \n cc:@python_ext_repo" + notification-summary: "Python Extension Blob UT CI Failed for Python ${{ matrix.python-version }}" notification-color: 17a2b8 timezone: America/Denver verbose-logging: false diff --git a/.github/workflows/ci_ut_ext_fastapi_workflow.yml b/.github/workflows/ci_ut_ext_fastapi_workflow.yml index a880ff0..bb4ec9e 100644 --- a/.github/workflows/ci_ut_ext_fastapi_workflow.yml +++ b/.github/workflows/ci_ut_ext_fastapi_workflow.yml @@ -54,7 +54,7 @@ jobs: with: github-token: ${{ github.token }} # this will use the runner's token. ms-teams-webhook-uri: ${{ secrets.MS_TEAMS_WEBHOOK_URI }} - notification-summary: "Python Extension FastApi UT CI Failed for Python ${{ matrix.python-version }} \n cc:@python_ext_repo" + notification-summary: "Python Extension FastApi UT CI Failed for Python ${{ matrix.python-version }}" notification-color: 17a2b8 timezone: America/Denver verbose-logging: false From ef6b21e75509f2b30eb91ebcd863d86acc7af8ba Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 15 Mar 2024 11:13:46 -0700 Subject: [PATCH 18/27] fix --- .../azure/functions/extension/base/sdkType.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/azure-functions-extension-base/azure/functions/extension/base/sdkType.py b/azure-functions-extension-base/azure/functions/extension/base/sdkType.py index 53d9995..26eb728 100644 --- a/azure-functions-extension-base/azure/functions/extension/base/sdkType.py +++ b/azure-functions-extension-base/azure/functions/extension/base/sdkType.py @@ -2,11 +2,19 @@ # Licensed under the MIT License. from abc import abstractmethod -from typing import Any, Optional +import sys +from typing import Any, Dict, Optional +if sys.version_info >= (3, 9): + from typing import TypedDict as PyTypedDict +else: + PyTypedDict = dict +class TypedDict(PyTypedDict): + pass + class SdkType: - def __init__(self, *, data: Optional[dict[str, Any]] = None): + def __init__(self, *, data: Optional[TypedDict[str, Any]] = None): self._data = data or {} @abstractmethod From 4a394783d119f8e76fd0a40affd732e4d76af58f Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 15 Mar 2024 11:21:03 -0700 Subject: [PATCH 19/27] fix --- .github/workflows/ci_ut_ext_base_workflow.yml | 2 +- .github/workflows/ci_ut_ext_fastapi_workflow.yml | 2 +- azure-functions-extension-base/setup.py | 2 +- azure-functions-extension-fastapi/setup.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci_ut_ext_base_workflow.yml b/.github/workflows/ci_ut_ext_base_workflow.yml index 20052ff..f9e1ba6 100644 --- a/.github/workflows/ci_ut_ext_base_workflow.yml +++ b/.github/workflows/ci_ut_ext_base_workflow.yml @@ -17,7 +17,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: [ 3.7, 3.8, 3.9, "3.10", "3.11" ] + python-version: [ 3.8, 3.9, "3.10", "3.11" ] permissions: read-all steps: - name: Checkout Repository diff --git a/.github/workflows/ci_ut_ext_fastapi_workflow.yml b/.github/workflows/ci_ut_ext_fastapi_workflow.yml index bb4ec9e..6e82a18 100644 --- a/.github/workflows/ci_ut_ext_fastapi_workflow.yml +++ b/.github/workflows/ci_ut_ext_fastapi_workflow.yml @@ -17,7 +17,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: [ 3.7, 3.8, 3.9, "3.10", "3.11" ] + python-version: [ 3.8, 3.9, "3.10", "3.11" ] permissions: read-all steps: - name: Checkout Repository diff --git a/azure-functions-extension-base/setup.py b/azure-functions-extension-base/setup.py index be2bcbb..17b8a37 100644 --- a/azure-functions-extension-base/setup.py +++ b/azure-functions-extension-base/setup.py @@ -38,5 +38,5 @@ ], license='MIT', extras_require=EXTRA_REQUIRES, - python_requires='>=3.7' + python_requires='>=3.8' ) diff --git a/azure-functions-extension-fastapi/setup.py b/azure-functions-extension-fastapi/setup.py index 02e493a..62c7b6d 100644 --- a/azure-functions-extension-fastapi/setup.py +++ b/azure-functions-extension-fastapi/setup.py @@ -31,7 +31,7 @@ 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', ], - python_requires='>=3.7', + python_requires='>=3.8', install_requires=[ 'azure-functions-extension-base', 'fastapi', From fd9bc0cdba33c41d2914e871fbf33f9354609a4e Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 15 Mar 2024 11:24:37 -0700 Subject: [PATCH 20/27] fix --- .github/workflows/ci_ut_ext_base_workflow.yml | 2 +- .github/workflows/ci_ut_ext_blob_workflow.yml | 2 +- .github/workflows/ci_ut_ext_fastapi_workflow.yml | 2 +- .../azure/functions/extension/base/sdkType.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci_ut_ext_base_workflow.yml b/.github/workflows/ci_ut_ext_base_workflow.yml index f9e1ba6..d8c2db0 100644 --- a/.github/workflows/ci_ut_ext_base_workflow.yml +++ b/.github/workflows/ci_ut_ext_base_workflow.yml @@ -12,7 +12,7 @@ on: jobs: build: - name: "Python Extension UT CI Run" + name: "Python Extension Base UT CI Run" runs-on: ubuntu-latest strategy: fail-fast: false diff --git a/.github/workflows/ci_ut_ext_blob_workflow.yml b/.github/workflows/ci_ut_ext_blob_workflow.yml index 1e5a79f..70b9fb3 100644 --- a/.github/workflows/ci_ut_ext_blob_workflow.yml +++ b/.github/workflows/ci_ut_ext_blob_workflow.yml @@ -14,7 +14,7 @@ on: jobs: build: - name: "Python Extension UT CI Run" + name: "Python Extension Blob UT CI Run" runs-on: ubuntu-latest strategy: fail-fast: false diff --git a/.github/workflows/ci_ut_ext_fastapi_workflow.yml b/.github/workflows/ci_ut_ext_fastapi_workflow.yml index 6e82a18..b252eef 100644 --- a/.github/workflows/ci_ut_ext_fastapi_workflow.yml +++ b/.github/workflows/ci_ut_ext_fastapi_workflow.yml @@ -12,7 +12,7 @@ on: jobs: build: - name: "Python Extension UT CI Run" + name: "Python Extension UT FastApi CI Run" runs-on: ubuntu-latest strategy: fail-fast: false diff --git a/azure-functions-extension-base/azure/functions/extension/base/sdkType.py b/azure-functions-extension-base/azure/functions/extension/base/sdkType.py index 26eb728..b0ba656 100644 --- a/azure-functions-extension-base/azure/functions/extension/base/sdkType.py +++ b/azure-functions-extension-base/azure/functions/extension/base/sdkType.py @@ -5,7 +5,7 @@ import sys from typing import Any, Dict, Optional -if sys.version_info >= (3, 9): +if sys.version_info < (3, 9): from typing import TypedDict as PyTypedDict else: PyTypedDict = dict From 01461d2bcfe1fdc74826956856f2e2d95a0d31c2 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 15 Mar 2024 11:29:52 -0700 Subject: [PATCH 21/27] fix --- .../azure/functions/extension/base/sdkType.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/azure-functions-extension-base/azure/functions/extension/base/sdkType.py b/azure-functions-extension-base/azure/functions/extension/base/sdkType.py index b0ba656..9e94ff4 100644 --- a/azure-functions-extension-base/azure/functions/extension/base/sdkType.py +++ b/azure-functions-extension-base/azure/functions/extension/base/sdkType.py @@ -5,14 +5,9 @@ import sys from typing import Any, Dict, Optional -if sys.version_info < (3, 9): - from typing import TypedDict as PyTypedDict -else: - PyTypedDict = dict - -class TypedDict(PyTypedDict): - pass +from typing import TypedDict + class SdkType: def __init__(self, *, data: Optional[TypedDict[str, Any]] = None): self._data = data or {} From a56827317d24772caa09ad95a46946cad8b58e19 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 15 Mar 2024 11:32:00 -0700 Subject: [PATCH 22/27] fix --- .../azure/functions/extension/base/sdkType.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/azure-functions-extension-base/azure/functions/extension/base/sdkType.py b/azure-functions-extension-base/azure/functions/extension/base/sdkType.py index 9e94ff4..7a50ce8 100644 --- a/azure-functions-extension-base/azure/functions/extension/base/sdkType.py +++ b/azure-functions-extension-base/azure/functions/extension/base/sdkType.py @@ -5,11 +5,14 @@ import sys from typing import Any, Dict, Optional -from typing import TypedDict - +if sys.version_info >= (3, 8): + from typing import TypedDict + TypedDict = TypedDict[str, Any] +else: + TypedDict = dict class SdkType: - def __init__(self, *, data: Optional[TypedDict[str, Any]] = None): + def __init__(self, *, data: Optional[TypedDict] = None): self._data = data or {} @abstractmethod From 038485ee4abd6e2ce014ae3dc7cc86457d188f85 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 15 Mar 2024 11:39:02 -0700 Subject: [PATCH 23/27] fix --- .../azure/functions/extension/base/sdkType.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/azure-functions-extension-base/azure/functions/extension/base/sdkType.py b/azure-functions-extension-base/azure/functions/extension/base/sdkType.py index 7a50ce8..56e474c 100644 --- a/azure-functions-extension-base/azure/functions/extension/base/sdkType.py +++ b/azure-functions-extension-base/azure/functions/extension/base/sdkType.py @@ -5,14 +5,8 @@ import sys from typing import Any, Dict, Optional -if sys.version_info >= (3, 8): - from typing import TypedDict - TypedDict = TypedDict[str, Any] -else: - TypedDict = dict - class SdkType: - def __init__(self, *, data: Optional[TypedDict] = None): + def __init__(self, *, data: dict = None): self._data = data or {} @abstractmethod From c4122c53a23c997a7ae8dd5cd92736a4cca49a90 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 15 Mar 2024 11:56:35 -0700 Subject: [PATCH 24/27] FIX --- .github/workflows/ci_ut_ext_base_workflow.yml | 2 ++ .github/workflows/ci_ut_ext_blob_workflow.yml | 2 ++ .github/workflows/ci_ut_ext_fastapi_workflow.yml | 2 ++ 3 files changed, 6 insertions(+) diff --git a/.github/workflows/ci_ut_ext_base_workflow.yml b/.github/workflows/ci_ut_ext_base_workflow.yml index d8c2db0..96668a7 100644 --- a/.github/workflows/ci_ut_ext_base_workflow.yml +++ b/.github/workflows/ci_ut_ext_base_workflow.yml @@ -36,6 +36,8 @@ jobs: - name: Run Unit Tests working-directory: azure-functions-extension-base + env: + AzureWebJobsStorage: ${{ secrets.AzureWebJobsStorage }} run: | python -m pytest -q --instafail --cov=. --cov-report xml --cov-branch tests diff --git a/.github/workflows/ci_ut_ext_blob_workflow.yml b/.github/workflows/ci_ut_ext_blob_workflow.yml index 70b9fb3..7db0cd8 100644 --- a/.github/workflows/ci_ut_ext_blob_workflow.yml +++ b/.github/workflows/ci_ut_ext_blob_workflow.yml @@ -39,6 +39,8 @@ jobs: - name: Run Unit Tests working-directory: azure-functions-extension-blob + env: + AzureWebJobsStorage: ${{ secrets.AzureWebJobsStorage }} run: | python -m pytest -q --instafail --cov=. --cov-report xml --cov-branch tests diff --git a/.github/workflows/ci_ut_ext_fastapi_workflow.yml b/.github/workflows/ci_ut_ext_fastapi_workflow.yml index b252eef..289b1ab 100644 --- a/.github/workflows/ci_ut_ext_fastapi_workflow.yml +++ b/.github/workflows/ci_ut_ext_fastapi_workflow.yml @@ -37,6 +37,8 @@ jobs: - name: Run Unit Tests working-directory: azure-functions-extension-fastapi + env: + AzureWebJobsStorage: ${{ secrets.AzureWebJobsStorage }} run: | python -m pytest -q --instafail --cov=. --cov-report xml --cov-branch tests From eb531b5fcea3af29357e732569ff14f20cd9841e Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 15 Mar 2024 12:07:38 -0700 Subject: [PATCH 25/27] fix --- azure-functions-extension-blob/tests/test_blobclient.py | 2 +- azure-functions-extension-blob/tests/test_containerclient.py | 2 +- azure-functions-extension-blob/tests/test_ssd.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/azure-functions-extension-blob/tests/test_blobclient.py b/azure-functions-extension-blob/tests/test_blobclient.py index d5d6818..4abdc12 100644 --- a/azure-functions-extension-blob/tests/test_blobclient.py +++ b/azure-functions-extension-blob/tests/test_blobclient.py @@ -102,7 +102,7 @@ def test_input_populated(self): pytype=BlobClient) self.assertIsNotNone(result) - self.assertIsInstance(result, BlobClient) + self.assertIsInstance(result, BlobClientSdk) sdk_result = BlobClient(data=datum).get_sdk_type() diff --git a/azure-functions-extension-blob/tests/test_containerclient.py b/azure-functions-extension-blob/tests/test_containerclient.py index 6dca311..b0aa762 100644 --- a/azure-functions-extension-blob/tests/test_containerclient.py +++ b/azure-functions-extension-blob/tests/test_containerclient.py @@ -102,7 +102,7 @@ def test_input_populated(self): pytype=ContainerClient) self.assertIsNotNone(result) - self.assertIsInstance(result, ContainerClient) + self.assertIsInstance(result, ContainerClientSdk) sdk_result = ContainerClient(data=datum).get_sdk_type() self.assertIsNotNone(sdk_result) diff --git a/azure-functions-extension-blob/tests/test_ssd.py b/azure-functions-extension-blob/tests/test_ssd.py index 70f81a7..9047de0 100644 --- a/azure-functions-extension-blob/tests/test_ssd.py +++ b/azure-functions-extension-blob/tests/test_ssd.py @@ -103,7 +103,7 @@ def test_input_populated(self): pytype=StorageStreamDownloader)) self.assertIsNotNone(result) - self.assertIsInstance(result, StorageStreamDownloader) + self.assertIsInstance(result, SSDSdk) sdk_result = StorageStreamDownloader(data=datum).get_sdk_type() From 754879811d9998f4f6653025b9e247b7e707dcc8 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 15 Mar 2024 12:25:02 -0700 Subject: [PATCH 26/27] fix --- .../tests/test_blobclient.py | 14 +++++++++----- .../tests/test_containerclient.py | 12 ++++++++---- azure-functions-extension-blob/tests/test_ssd.py | 14 +++++++++----- 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/azure-functions-extension-blob/tests/test_blobclient.py b/azure-functions-extension-blob/tests/test_blobclient.py index 4abdc12..8a83848 100644 --- a/azure-functions-extension-blob/tests/test_blobclient.py +++ b/azure-functions-extension-blob/tests/test_blobclient.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +import json import unittest from typing import Optional from enum import Enum @@ -88,13 +89,16 @@ def test_input_empty(self): self.assertIsNone(result) def test_input_populated(self): - # TODO: pass in variable connection string + content = { + "Connection": "AzureWebJobsStorage", + "ContainerName": "test-blob", + "BlobName": "text.txt" + } + sample_mbd = MockMBD(version="1.0", source="AzureStorageBlobs", content_type="application/json", - content="{\"Connection\":\"AzureWebJobsStorage\"," - "\"ContainerName\":\"test-blob\"," - "\"BlobName\":\"test.txt\"}") + content=json.dumps(content)) datum: Datum = Datum(value=sample_mbd, type='model_binding_data') result: BlobClient = BlobClientConverter.decode(data=datum, @@ -104,7 +108,7 @@ def test_input_populated(self): self.assertIsNotNone(result) self.assertIsInstance(result, BlobClientSdk) - sdk_result = BlobClient(data=datum).get_sdk_type() + sdk_result = BlobClient(data=datum.value).get_sdk_type() self.assertIsNotNone(sdk_result) self.assertIsInstance(sdk_result, BlobClientSdk) diff --git a/azure-functions-extension-blob/tests/test_containerclient.py b/azure-functions-extension-blob/tests/test_containerclient.py index b0aa762..1f2ef4a 100644 --- a/azure-functions-extension-blob/tests/test_containerclient.py +++ b/azure-functions-extension-blob/tests/test_containerclient.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +import json import unittest from typing import Optional from enum import Enum @@ -88,12 +89,15 @@ def test_input_empty(self): 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="{\"Connection\":\"AzureWebJobsStorage\"," - "\"ContainerName\":\"test-blob\"," - "\"BlobName\":\"test.txt\"}") + content=json.dumps(content)) datum: Datum = Datum(value=sample_mbd, type='model_binding_data') result: ContainerClient = BlobClientConverter.decode( @@ -104,7 +108,7 @@ def test_input_populated(self): self.assertIsNotNone(result) self.assertIsInstance(result, ContainerClientSdk) - sdk_result = ContainerClient(data=datum).get_sdk_type() + sdk_result = ContainerClient(data=datum.value).get_sdk_type() self.assertIsNotNone(sdk_result) self.assertIsInstance(sdk_result, ContainerClientSdk) diff --git a/azure-functions-extension-blob/tests/test_ssd.py b/azure-functions-extension-blob/tests/test_ssd.py index 9047de0..82d7599 100644 --- a/azure-functions-extension-blob/tests/test_ssd.py +++ b/azure-functions-extension-blob/tests/test_ssd.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +import json import unittest from typing import Optional from enum import Enum @@ -89,12 +90,15 @@ def test_input_empty(self): self.assertIsNone(result) def test_input_populated(self): - # TODO: pass in variable connection string + content = { + "Connection": "AzureWebJobsStorage", + "ContainerName": "test-blob", + "BlobName": "text.txt" + } + sample_mbd = MockMBD(version="1.0", source="AzureStorageBlobs", content_type="application/json", - content="{\"Connection\":\"AzureWebJobsStorage\"," - "\"ContainerName\":\"test-blob\"," - "\"BlobName\":\"test.txt\"}") + content=json.dumps(content)) datum: Datum = Datum(value=sample_mbd, type='model_binding_data') result: StorageStreamDownloader = ( @@ -105,7 +109,7 @@ def test_input_populated(self): self.assertIsNotNone(result) self.assertIsInstance(result, SSDSdk) - sdk_result = StorageStreamDownloader(data=datum).get_sdk_type() + sdk_result = StorageStreamDownloader(data=datum.value).get_sdk_type() self.assertIsNotNone(sdk_result) self.assertIsInstance(sdk_result, SSDSdk) From af3c3d7f33aac18c2c2ae45ee0813c19dc4f7327 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 15 Mar 2024 13:12:37 -0700 Subject: [PATCH 27/27] feedback --- azure-functions-extension-base/setup.py | 1 + azure-functions-extension-blob/setup.py | 2 ++ azure-functions-extension-fastapi/setup.py | 18 ++++++++++++++---- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/azure-functions-extension-base/setup.py b/azure-functions-extension-base/setup.py index 17b8a37..cb1df9e 100644 --- a/azure-functions-extension-base/setup.py +++ b/azure-functions-extension-base/setup.py @@ -29,6 +29,7 @@ 'License :: OSI Approved :: MIT License', 'Intended Audience :: Developers', 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Operating System :: Microsoft :: Windows', 'Operating System :: POSIX', diff --git a/azure-functions-extension-blob/setup.py b/azure-functions-extension-blob/setup.py index f4c38fe..0d695c6 100644 --- a/azure-functions-extension-blob/setup.py +++ b/azure-functions-extension-blob/setup.py @@ -3,6 +3,8 @@ from setuptools import setup, find_packages +# TODO: pin to ext base version after published + EXTRA_REQUIRES = { 'dev': [ 'flake8~=4.0.1', diff --git a/azure-functions-extension-fastapi/setup.py b/azure-functions-extension-fastapi/setup.py index 62c7b6d..d92b7d8 100644 --- a/azure-functions-extension-fastapi/setup.py +++ b/azure-functions-extension-fastapi/setup.py @@ -3,6 +3,8 @@ from setuptools import setup, find_packages +# TODO: pin to ext base version after published + EXTRA_REQUIRES = { 'dev': [ 'flake8~=4.0.1', @@ -27,15 +29,23 @@ 'azure', 'tests', 'samples' ]), classifiers=[ - 'Programming Language :: Python :: 3', 'License :: OSI Approved :: MIT License', - 'Operating System :: OS Independent', + 'Intended Audience :: Developers', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Operating System :: Microsoft :: Windows', + 'Operating System :: POSIX', + 'Operating System :: MacOS :: MacOS X', + 'Environment :: Web Environment', + 'Development Status :: 5 - Production/Stable', ], + license='MIT', python_requires='>=3.8', install_requires=[ 'azure-functions-extension-base', - 'fastapi', - 'uvicorn' + 'fastapi==0.110.0', + 'uvicorn==0.28.0' ], extras_require=EXTRA_REQUIRES )