From 512b61391cd55ae688d5087964b6b0c85a1ef16e Mon Sep 17 00:00:00 2001 From: Gavin Aguiar Date: Thu, 4 May 2023 14:26:42 -0500 Subject: [PATCH 01/13] Retry policy --- azure/functions/decorators/function_app.py | 10 ++++++++++ azure/functions/decorators/timer.py | 11 +++++++++++ 2 files changed, 21 insertions(+) diff --git a/azure/functions/decorators/function_app.py b/azure/functions/decorators/function_app.py index 327c6756..ea42ba99 100644 --- a/azure/functions/decorators/function_app.py +++ b/azure/functions/decorators/function_app.py @@ -407,6 +407,11 @@ def timer_trigger(self, schedule: str, run_on_startup: Optional[bool] = None, use_monitor: Optional[bool] = None, + retry_strategy: Optional[str] = None, + retry_max_retry_count: Optional[int] = None, + retry_delay_interval: Optional[str] = None, + retry_minimum_interval: Optional[str] = None, + retry_maximum_interval: Optional[str] = None, data_type: Optional[Union[DataType, str]] = None, **kwargs: Any) -> Callable[..., Any]: """The schedule or timer decorator adds :class:`TimerTrigger` to the @@ -442,6 +447,11 @@ def decorator(): schedule=schedule, run_on_startup=run_on_startup, use_monitor=use_monitor, + retry_strategy=retry_strategy, + retry_max_retry_count=retry_max_retry_count, + retry_delay_interval=retry_delay_interval, + retry_minimum_interval=retry_minimum_interval, + retry_maximum_interval=retry_maximum_interval, data_type=parse_singular_param_to_enum(data_type, DataType), **kwargs)) diff --git a/azure/functions/decorators/timer.py b/azure/functions/decorators/timer.py index 2d1967b2..26bfbc88 100644 --- a/azure/functions/decorators/timer.py +++ b/azure/functions/decorators/timer.py @@ -16,9 +16,20 @@ def __init__(self, schedule: str, run_on_startup: Optional[bool] = None, use_monitor: Optional[bool] = None, + retry_strategy: Optional[str] = None, + retry_max_retry_count: Optional[int] = None, + retry_delay_interval: Optional[str] = None, + retry_minimum_interval: Optional[str] = None, + retry_maximum_interval: Optional[str] = None, data_type: Optional[DataType] = None, **kwargs) -> None: self.schedule = schedule self.run_on_startup = run_on_startup self.use_monitor = use_monitor + self.retry_strategy = retry_strategy + self.retry_max_retry_count = retry_max_retry_count + self.retry_delay_interval = retry_delay_interval + self.retry_minimum_interval = retry_minimum_interval + self.retry_maximum_interval = retry_maximum_interval + super().__init__(name=name, data_type=data_type) From 274ea2bf8b8c131929a49076ddc9f9a0d594bc2c Mon Sep 17 00:00:00 2001 From: Gavin Aguiar Date: Thu, 8 Jun 2023 10:32:19 -0500 Subject: [PATCH 02/13] Retry policy support for v2 --- azure/functions/__init__.py | 7 +- azure/functions/decorators/__init__.py | 3 +- azure/functions/decorators/constants.py | 2 + azure/functions/decorators/core.py | 29 ++++ azure/functions/decorators/function_app.py | 170 +++++++++++++++----- azure/functions/decorators/function_name.py | 18 +++ azure/functions/decorators/retry_policy.py | 28 ++++ 7 files changed, 213 insertions(+), 44 deletions(-) create mode 100644 azure/functions/decorators/function_name.py create mode 100644 azure/functions/decorators/retry_policy.py diff --git a/azure/functions/__init__.py b/azure/functions/__init__.py index 8d7fb215..46e338c5 100644 --- a/azure/functions/__init__.py +++ b/azure/functions/__init__.py @@ -13,7 +13,7 @@ ExternalHttpFunctionApp) from ._durable_functions import OrchestrationContext, EntityContext from .decorators.function_app import (FunctionRegister, TriggerApi, - BindingApi) + BindingApi, SettingsApi) from .extension import (ExtensionMeta, FunctionExtensionException, FuncExtensionBase, AppExtensionBase) from ._http_wsgi import WsgiMiddleware @@ -35,8 +35,8 @@ from . import servicebus # NoQA from . import timer # NoQA from . import durable_functions # NoQA -from . import sql # NoQA -from . import warmup # NoQA +from . import sql # NoQA +from . import warmup # NoQA __all__ = ( @@ -85,6 +85,7 @@ 'DecoratorApi', 'TriggerApi', 'BindingApi', + 'SettingsApi', 'Blueprint', 'ExternalHttpFunctionApp', 'AsgiFunctionApp', diff --git a/azure/functions/decorators/__init__.py b/azure/functions/decorators/__init__.py index 6dbe5d47..90ff9ac6 100644 --- a/azure/functions/decorators/__init__.py +++ b/azure/functions/decorators/__init__.py @@ -3,7 +3,7 @@ from .core import Cardinality, AccessRights from .function_app import FunctionApp, Function, DecoratorApi, DataType, \ AuthLevel, Blueprint, ExternalHttpFunctionApp, AsgiFunctionApp, \ - WsgiFunctionApp, FunctionRegister, TriggerApi, BindingApi + WsgiFunctionApp, FunctionRegister, TriggerApi, BindingApi, SettingsApi from .http import HttpMethod __all__ = [ @@ -13,6 +13,7 @@ 'DecoratorApi', 'TriggerApi', 'BindingApi', + 'SettingsApi', 'Blueprint', 'ExternalHttpFunctionApp', 'AsgiFunctionApp', diff --git a/azure/functions/decorators/constants.py b/azure/functions/decorators/constants.py index fde60864..adf470cd 100644 --- a/azure/functions/decorators/constants.py +++ b/azure/functions/decorators/constants.py @@ -19,3 +19,5 @@ EVENT_GRID_TRIGGER = "eventGridTrigger" EVENT_GRID = "eventGrid" TABLE = "table" +RETRY_POLICY = "retry_policy" +FUNCTION_NAME = "function_name" diff --git a/azure/functions/decorators/core.py b/azure/functions/decorators/core.py index 14118b21..bb5f4e24 100644 --- a/azure/functions/decorators/core.py +++ b/azure/functions/decorators/core.py @@ -161,3 +161,32 @@ def __init__(self, name: str, data_type: Optional[DataType] = None, type: Optional[str] = None) -> None: super().__init__(direction=BindingDirection.OUT, name=name, data_type=data_type, type=type) + + +class Setting(ABC, metaclass=ABCBuildDictMeta): + + EXCLUDED_INIT_PARAMS = {'self', 'kwargs', 'type'} + + def get_setting_type(self) -> str: + return self.setting_type + + def __init__(self, setting_type: Optional[str] = None) -> None: + if setting_type is not None: + self.setting_type = setting_type + self._dict = {} + + def get_dict_repr(self) -> Dict: + """Build a dictionary of a particular binding. The keys are camel + cased binding field names defined in `init_params` list and + :class:`Binding` class. \n + This method is invoked in function :meth:`get_raw_bindings` of class + :class:`Function` to generate json dict for each binding. + + :return: Dictionary representation of the binding. + """ + params = list(dict.fromkeys(getattr(self, 'init_params', []))) + for p in params: + if p not in Setting.EXCLUDED_INIT_PARAMS: + self._dict[to_camel_case(p)] = getattr(self, p, None) + + return self._dict \ No newline at end of file diff --git a/azure/functions/decorators/function_app.py b/azure/functions/decorators/function_app.py index ea42ba99..c6f4904b 100644 --- a/azure/functions/decorators/function_app.py +++ b/azure/functions/decorators/function_app.py @@ -10,7 +10,7 @@ from azure.functions.decorators.blob import BlobTrigger, BlobInput, BlobOutput from azure.functions.decorators.core import Binding, Trigger, DataType, \ - AuthLevel, SCRIPT_FILE_NAME, Cardinality, AccessRights + AuthLevel, SCRIPT_FILE_NAME, Cardinality, AccessRights, Setting from azure.functions.decorators.cosmosdb import CosmosDBTrigger, \ CosmosDBOutput, CosmosDBInput, CosmosDBTriggerV3, CosmosDBInputV3, \ CosmosDBOutputV3 @@ -29,6 +29,8 @@ parse_iterable_param_to_enums, StringifyEnumJsonEncoder from azure.functions.http import HttpRequest from .generic import GenericInputBinding, GenericTrigger, GenericOutputBinding +from .retry_policy import RetryPolicy +from .function_name import FunctionName from .warmup import WarmUpTrigger from .._http_asgi import AsgiMiddleware from .._http_wsgi import WsgiMiddleware, Context @@ -50,6 +52,7 @@ def __init__(self, func: Callable[..., Any], script_file: str): self._func = func self._trigger: Optional[Trigger] = None self._bindings: List[Binding] = [] + self._settings: List[Setting] = [] self.function_script_file = script_file self.http_type = 'function' self._is_http_function = False @@ -83,14 +86,21 @@ def add_trigger(self, trigger: Trigger) -> None: # function.json is complete self._bindings.append(trigger) - def set_function_name(self, function_name: Optional[str] = None) -> None: - """Set or update the name for the function if :param:`function_name` - is not None. If not set, function name will default to python - function name. - :param function_name: Name the function set to. + def add_setting(self, setting: Setting) -> None: + """Add a setting instance to the function. + + :param setting: The setting object to add """ - if function_name: - self._name = function_name + self._settings.append(setting) + + # def set_function_name(self, function_name: Optional[str] = None) -> None: + # """Set or update the name for the function if :param:`function_name` + # is not None. If not set, function name will default to python + # function name. + # :param function_name: Name the function set to. + # """ + # if function_name: + # self._name = function_name def set_http_type(self, http_type: str) -> None: """Set or update the http type for the function if :param:`http_type` @@ -116,6 +126,23 @@ def get_bindings(self) -> List[Binding]: """ return self._bindings + def get_setting(self, setting_name: str) -> Setting: + """Get a specific setting attached to the function. + + :param setting_name: The name of the setting to search for. + :return: The setting attached to the function (or None if not found). + """ + for setting in self._settings: + if setting.setting_type == setting_name: + return setting + return None + + def get_setting_values(self, setting_name: str) -> str: + """ Gets the values of a specific setting attached to the function. + """ + setting = self.get_setting(setting_name) + return setting.get_dict_repr() if setting else None + def get_raw_bindings(self) -> List[str]: return [json.dumps(b.get_dict_repr(), cls=StringifyEnumJsonEncoder) for b in self._bindings] @@ -145,12 +172,12 @@ def get_user_function(self) -> Callable[..., Any]: """ return self._func - def get_function_name(self) -> str: - """Get the function name. + # def get_function_name(self) -> str: + # """Get the function name. - :return: Function name. - """ - return self._name + # :return: Function name. + # """ + # return self._name def get_function_json(self) -> str: """Get the json stringified form of function. @@ -170,10 +197,10 @@ def __init__(self, func, function_script_file): def __call__(self, *args, **kwargs): pass - def configure_function_name(self, function_name: str) -> 'FunctionBuilder': - self._function.set_function_name(function_name) + # def configure_function_name(self, function_name: str) -> 'FunctionBuilder': + # self._function.set_function_name(function_name) - return self + # return self def configure_http_type(self, http_type: str) -> 'FunctionBuilder': self._function.set_http_type(http_type) @@ -188,6 +215,10 @@ def add_binding(self, binding: Binding) -> 'FunctionBuilder': self._function.add_binding(binding=binding) return self + def add_setting(self, setting: Setting) -> 'FunctionBuilder': + self._function.add_setting(setting=setting) + return self + def _validate_function(self, auth_level: Optional[AuthLevel] = None) -> None: """ @@ -196,7 +227,7 @@ def _validate_function(self, :param auth_level: Http auth level that will be set if http trigger function auth level is None. """ - function_name = self._function.get_function_name() + function_name = self._function.get_setting("function_name") trigger = self._function.get_trigger() if trigger is None: raise ValueError( @@ -288,22 +319,22 @@ def decorator(func): return decorator - def function_name(self, name: str) -> Callable[..., Any]: - """Set name of the :class:`Function` object. + # def function_name(self, name: str) -> Callable[..., Any]: + # """Set name of the :class:`Function` object. - :param name: Name of the function. - :return: Decorator function. - """ + # :param name: Name of the function. + # :return: Decorator function. + # """ - @self._configure_function_builder - def wrap(fb): - def decorator(): - fb.configure_function_name(name) - return fb + # @self._configure_function_builder + # def wrap(fb): + # def decorator(): + # fb.configure_function_name(name) + # return fb - return decorator() + # return decorator() - return wrap + # return wrap def http_type(self, http_type: str) -> Callable[..., Any]: """Set http type of the :class:`Function` object. @@ -407,11 +438,6 @@ def timer_trigger(self, schedule: str, run_on_startup: Optional[bool] = None, use_monitor: Optional[bool] = None, - retry_strategy: Optional[str] = None, - retry_max_retry_count: Optional[int] = None, - retry_delay_interval: Optional[str] = None, - retry_minimum_interval: Optional[str] = None, - retry_maximum_interval: Optional[str] = None, data_type: Optional[Union[DataType, str]] = None, **kwargs: Any) -> Callable[..., Any]: """The schedule or timer decorator adds :class:`TimerTrigger` to the @@ -447,11 +473,6 @@ def decorator(): schedule=schedule, run_on_startup=run_on_startup, use_monitor=use_monitor, - retry_strategy=retry_strategy, - retry_max_retry_count=retry_max_retry_count, - retry_delay_interval=retry_delay_interval, - retry_minimum_interval=retry_minimum_interval, - retry_maximum_interval=retry_maximum_interval, data_type=parse_singular_param_to_enum(data_type, DataType), **kwargs)) @@ -1948,6 +1969,75 @@ def decorator(): return wrap +class SettingsApi(DecoratorApi, ABC): + + def function_name(self, name: str, + setting_extra_fields: Dict[str, Any] = {}, + ) -> Callable[..., Any]: + """Set name of the :class:`Function` object. + + :param name: Name of the function. + :return: Decorator function. + """ + + @self._configure_function_builder + def wrap(fb): + def decorator(): + fb.add_setting(setting=FunctionName( + name=name, + setting_type="setting", + **setting_extra_fields)) + return fb + + return decorator() + + return wrap + + def retry(self, + strategy: str, + max_retry_count: int = 0, + delay_interval: Optional[str] = None, + minimum_interval: Optional[str] = None, + maximum_interval: Optional[str] = None, + setting_extra_fields: Dict[str, Any] = {}, + ) -> Callable[..., Any]: + """The retry decorator adds :class:`RetryPolicy` to the function + settings object for building :class:`Function` object used in worker + function indexing model. This is equivalent to defining RetryPolicy + in the function.json which enables function to retry on failure. + All optional fields will be given default value by function host when + they are parsed by function host. + + Ref: https://aka.ms/azure-function-retry + + :param strategy: The retry strategy to use. + :param max_retry_count: The maximum number of retry attempts. + :param delay_interval: The delay interval between retry attempts. + :param minimum_interval: The minimum delay interval between retry + attempts. + :param maximum_interval: The maximum delay interval between retry + attempts. + :param setting_extra_fields: Keyword arguments for specifying + additional setting fields to include in the setting json. + """ + + @self._configure_function_builder + def wrap(fb): + def decorator(): + fb.add_setting(setting=RetryPolicy( + strategy=strategy, + max_retry_count=max_retry_count, + minimum_interval=minimum_interval, + maximum_interval=maximum_interval, + delay_interval=delay_interval, + **setting_extra_fields)) + return fb + + return decorator() + + return wrap + + class FunctionRegister(DecoratorApi, HttpFunctionsAuthLevelMixin, ABC): def __init__(self, auth_level: Union[AuthLevel, str], *args, **kwargs): """Interface for declaring top level function app class which will @@ -1997,7 +2087,7 @@ def register_functions(self, function_container: DecoratorApi) -> None: register_blueprint = register_functions -class FunctionApp(FunctionRegister, TriggerApi, BindingApi): +class FunctionApp(FunctionRegister, TriggerApi, BindingApi, SettingsApi): """FunctionApp object used by worker function indexing model captures user defined functions and metadata. diff --git a/azure/functions/decorators/function_name.py b/azure/functions/decorators/function_name.py new file mode 100644 index 00000000..64473917 --- /dev/null +++ b/azure/functions/decorators/function_name.py @@ -0,0 +1,18 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +from typing import Optional + +from azure.functions.decorators.constants import FUNCTION_NAME +from azure.functions.decorators.core import Setting + + +class FunctionName(Setting): + + def __init__(self, name:str, + **kwargs): + self.name = name + super().__init__(setting_type=FUNCTION_NAME) + + def get_value(self, name: str) -> str: + return self.get_dict_repr().get(name, None) + \ No newline at end of file diff --git a/azure/functions/decorators/retry_policy.py b/azure/functions/decorators/retry_policy.py new file mode 100644 index 00000000..52dc96d7 --- /dev/null +++ b/azure/functions/decorators/retry_policy.py @@ -0,0 +1,28 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +from typing import Optional + +from azure.functions.decorators.constants import RETRY_POLICY +from azure.functions.decorators.core import Setting + + +class RetryPolicy(Setting): + + def __init__(self, + strategy: str, + max_retry_count: str, + delay_interval: Optional[str], + minimum_interval: Optional[str], + maximum_interval: Optional[str], + **kwargs): + self.strategy = strategy + self.max_retry_count = max_retry_count + self.delay_interval = delay_interval + self.minimum_interval = minimum_interval + self.maximum_interval = maximum_interval + super().__init__(setting_type=RETRY_POLICY) + + def get_value(self, name: str) -> str: + return self.get_dict_repr().get(name, None) + + From c9422897fb13db4d85aec5b6570bba194a57fc4b Mon Sep 17 00:00:00 2001 From: Gavin Aguiar Date: Thu, 15 Jun 2023 16:55:51 -0500 Subject: [PATCH 03/13] Fixing existing tests --- azure/functions/decorators/core.py | 17 +++++++---- azure/functions/decorators/function_app.py | 25 +++++++++-------- azure/functions/decorators/function_name.py | 8 +----- azure/functions/decorators/retry_policy.py | 6 ++-- tests/decorators/test_function_app.py | 31 +++++++-------------- 5 files changed, 37 insertions(+), 50 deletions(-) diff --git a/azure/functions/decorators/core.py b/azure/functions/decorators/core.py index bb5f4e24..478045db 100644 --- a/azure/functions/decorators/core.py +++ b/azure/functions/decorators/core.py @@ -165,16 +165,18 @@ def __init__(self, name: str, data_type: Optional[DataType] = None, class Setting(ABC, metaclass=ABCBuildDictMeta): - EXCLUDED_INIT_PARAMS = {'self', 'kwargs', 'type'} + EXCLUDED_INIT_PARAMS = {'self', 'kwargs', 'setting_type'} def get_setting_type(self) -> str: return self.setting_type - def __init__(self, setting_type: Optional[str] = None) -> None: + def __init__(self, setting_type: str) -> None: if setting_type is not None: - self.setting_type = setting_type - self._dict = {} - + self.setting_type = setting_type + self._dict: Dict = { + "setting_type": self.setting_type + } + def get_dict_repr(self) -> Dict: """Build a dictionary of a particular binding. The keys are camel cased binding field names defined in `init_params` list and @@ -189,4 +191,7 @@ def get_dict_repr(self) -> Dict: if p not in Setting.EXCLUDED_INIT_PARAMS: self._dict[to_camel_case(p)] = getattr(self, p, None) - return self._dict \ No newline at end of file + return self._dict + + def get_settings_value(self, name: str) -> Optional[str]: + return self.get_dict_repr().get(name) diff --git a/azure/functions/decorators/function_app.py b/azure/functions/decorators/function_app.py index c6f4904b..897df9ca 100644 --- a/azure/functions/decorators/function_app.py +++ b/azure/functions/decorators/function_app.py @@ -126,7 +126,7 @@ def get_bindings(self) -> List[Binding]: """ return self._bindings - def get_setting(self, setting_name: str) -> Setting: + def get_setting(self, setting_name: str) -> Optional[Setting]: """Get a specific setting attached to the function. :param setting_name: The name of the setting to search for. @@ -137,10 +137,16 @@ def get_setting(self, setting_name: str) -> Setting: return setting return None - def get_setting_values(self, setting_name: str) -> str: + def get_function_name(self) -> Optional[str]: + function_name_setting = \ + self.get_setting("function_name") + return function_name_setting.get_settings_value("name") \ + if function_name_setting else self._name + + def get_setting_values(self, setting_name: str): """ Gets the values of a specific setting attached to the function. """ - setting = self.get_setting(setting_name) + setting = self.get_setting(setting_name) return setting.get_dict_repr() if setting else None def get_raw_bindings(self) -> List[str]: @@ -197,11 +203,6 @@ def __init__(self, func, function_script_file): def __call__(self, *args, **kwargs): pass - # def configure_function_name(self, function_name: str) -> 'FunctionBuilder': - # self._function.set_function_name(function_name) - - # return self - def configure_http_type(self, http_type: str) -> 'FunctionBuilder': self._function.set_http_type(http_type) @@ -227,7 +228,7 @@ def _validate_function(self, :param auth_level: Http auth level that will be set if http trigger function auth level is None. """ - function_name = self._function.get_setting("function_name") + function_name = self._function.get_function_name() trigger = self._function.get_trigger() if trigger is None: raise ValueError( @@ -1971,9 +1972,9 @@ def decorator(): class SettingsApi(DecoratorApi, ABC): - def function_name(self, name: str, + def function_name(self, name: str, setting_extra_fields: Dict[str, Any] = {}, - ) -> Callable[..., Any]: + ) -> Callable[..., Any]: """Set name of the :class:`Function` object. :param name: Name of the function. @@ -1995,7 +1996,7 @@ def decorator(): def retry(self, strategy: str, - max_retry_count: int = 0, + max_retry_count: str, delay_interval: Optional[str] = None, minimum_interval: Optional[str] = None, maximum_interval: Optional[str] = None, diff --git a/azure/functions/decorators/function_name.py b/azure/functions/decorators/function_name.py index 64473917..9f01a423 100644 --- a/azure/functions/decorators/function_name.py +++ b/azure/functions/decorators/function_name.py @@ -1,18 +1,12 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -from typing import Optional - from azure.functions.decorators.constants import FUNCTION_NAME from azure.functions.decorators.core import Setting class FunctionName(Setting): - def __init__(self, name:str, + def __init__(self, name: str, **kwargs): self.name = name super().__init__(setting_type=FUNCTION_NAME) - - def get_value(self, name: str) -> str: - return self.get_dict_repr().get(name, None) - \ No newline at end of file diff --git a/azure/functions/decorators/retry_policy.py b/azure/functions/decorators/retry_policy.py index 52dc96d7..b266bbf3 100644 --- a/azure/functions/decorators/retry_policy.py +++ b/azure/functions/decorators/retry_policy.py @@ -21,8 +21,6 @@ def __init__(self, self.minimum_interval = minimum_interval self.maximum_interval = maximum_interval super().__init__(setting_type=RETRY_POLICY) - - def get_value(self, name: str) -> str: - return self.get_dict_repr().get(name, None) - + def get_value(self, name: str) -> Optional[str]: + return self.get_dict_repr().get(name) diff --git a/tests/decorators/test_function_app.py b/tests/decorators/test_function_app.py index 6a534a66..87a60450 100644 --- a/tests/decorators/test_function_app.py +++ b/tests/decorators/test_function_app.py @@ -34,14 +34,6 @@ def test_function_creation(self): self.assertEqual(self.func.get_user_function(), self.dummy) self.assertEqual(self.func.function_script_file, "dummy.py") - def test_set_function_name(self): - self.func.set_function_name("func_name") - self.assertEqual(self.func.get_function_name(), "func_name") - self.func.set_function_name() - self.assertEqual(self.func.get_function_name(), "func_name") - self.func.set_function_name("func_name_2") - self.assertEqual(self.func.get_function_name(), "func_name_2") - def test_add_trigger(self): with self.assertRaises(ValueError) as err: trigger1 = HttpTrigger(name="req1", methods=(HttpMethod.GET,), @@ -74,9 +66,8 @@ def test_function_creation_with_binding_and_trigger(self): auth_level=AuthLevel.ANONYMOUS, route="dummy") self.func.add_binding(output) self.func.add_trigger(trigger) - self.func.set_function_name("func_name") - self.assertEqual(self.func.get_function_name(), "func_name") + self.assertEqual(self.func.get_function_name(), "dummy") self.assertEqual(self.func.get_user_function(), self.dummy) assert_json(self, self.func, {"scriptFile": "dummy.py", "bindings": [ @@ -134,7 +125,7 @@ def test_function_builder_creation(self): def test_validate_function_missing_trigger(self): with self.assertRaises(ValueError) as err: - self.fb.configure_function_name('dummy').build() + # self.fb.configure_function_name('dummy').build() self.fb.build() self.assertEqual(err.exception.args[0], @@ -148,7 +139,7 @@ def test_validate_function_trigger_not_in_bindings(self): auth_level=AuthLevel.ANONYMOUS, route='dummy') with self.assertRaises(ValueError) as err: - self.fb.configure_function_name('dummy').add_trigger(trigger) + self.fb.add_trigger(trigger) getattr(self.fb, "_function").get_bindings().clear() self.fb.build() @@ -160,29 +151,29 @@ def test_validate_function_working(self): trigger = HttpTrigger(name='req', methods=(HttpMethod.GET,), data_type=DataType.UNDEFINED, auth_level=AuthLevel.ANONYMOUS) - self.fb.configure_function_name('dummy').add_trigger(trigger) + self.fb.add_trigger(trigger) self.fb.build() def test_build_function_http_route_default(self): trigger = HttpTrigger(name='req', methods=(HttpMethod.GET,), data_type=DataType.UNDEFINED, auth_level=AuthLevel.ANONYMOUS) - self.fb.configure_function_name('dummy_route').add_trigger(trigger) + self.fb.add_trigger(trigger) func = self.fb.build() - self.assertEqual(func.get_trigger().route, "dummy_route") + self.assertEqual(func.get_trigger().route, "dummy") - def test_build_function_with_name_and_bindings(self): + def test_build_function_with_bindings(self): test_trigger = HttpTrigger(name='req', methods=(HttpMethod.GET,), data_type=DataType.UNDEFINED, auth_level=AuthLevel.ANONYMOUS, route='dummy') test_input = HttpOutput(name='out', data_type=DataType.UNDEFINED) - func = self.fb.configure_function_name('func_name').add_trigger( + func = self.fb.add_trigger( test_trigger).add_binding(test_input).build() - self.assertEqual(func.get_function_name(), "func_name") + self.assertEqual(func.get_function_name(), "dummy") assert_json(self, func, { "scriptFile": "dummy.py", "bindings": [ @@ -209,7 +200,7 @@ def test_build_function_with_name_and_bindings(self): def test_build_function_with_function_app_auth_level(self): trigger = HttpTrigger(name='req', methods=(HttpMethod.GET,), data_type=DataType.UNDEFINED) - self.fb.configure_function_name('dummy').add_trigger(trigger) + self.fb.add_trigger(trigger) func = self.fb.build(auth_level=AuthLevel.ANONYMOUS) self.assertEqual(func.get_trigger().auth_level, AuthLevel.ANONYMOUS) @@ -397,7 +388,6 @@ class DummyFunctionApp(DecoratorApi): app = DummyFunctionApp() self.assertEqual(app.app_script_file, SCRIPT_FILE_NAME) - self.assertIsNotNone(getattr(app, "function_name", None)) self.assertIsNotNone(getattr(app, "_validate_type", None)) self.assertIsNotNone(getattr(app, "_configure_function_builder", None)) @@ -417,7 +407,6 @@ class DummyFunctionApp(FunctionRegister): app = DummyFunctionApp(auth_level=AuthLevel.ANONYMOUS) self.assertEqual(app.app_script_file, SCRIPT_FILE_NAME) - self.assertIsNotNone(getattr(app, "function_name", None)) self.assertIsNotNone(getattr(app, "_validate_type", None)) self.assertIsNotNone(getattr(app, "_configure_function_builder", None)) self.assertIsNone(getattr(app, "_require_auth_level")) From b14b1c8c985590b85e18918008e43cb09bb1ff1d Mon Sep 17 00:00:00 2001 From: Gavin Aguiar Date: Fri, 16 Jun 2023 14:16:26 -0500 Subject: [PATCH 04/13] Removing retry options from timer trigger --- azure/functions/decorators/timer.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/azure/functions/decorators/timer.py b/azure/functions/decorators/timer.py index 26bfbc88..a44c55ab 100644 --- a/azure/functions/decorators/timer.py +++ b/azure/functions/decorators/timer.py @@ -16,20 +16,10 @@ def __init__(self, schedule: str, run_on_startup: Optional[bool] = None, use_monitor: Optional[bool] = None, - retry_strategy: Optional[str] = None, - retry_max_retry_count: Optional[int] = None, - retry_delay_interval: Optional[str] = None, - retry_minimum_interval: Optional[str] = None, - retry_maximum_interval: Optional[str] = None, data_type: Optional[DataType] = None, **kwargs) -> None: self.schedule = schedule self.run_on_startup = run_on_startup self.use_monitor = use_monitor - self.retry_strategy = retry_strategy - self.retry_max_retry_count = retry_max_retry_count - self.retry_delay_interval = retry_delay_interval - self.retry_minimum_interval = retry_minimum_interval - self.retry_maximum_interval = retry_maximum_interval super().__init__(name=name, data_type=data_type) From e2ff9c8415b0a6619c668fadb541fe7327045029 Mon Sep 17 00:00:00 2001 From: Gavin Aguiar Date: Tue, 20 Jun 2023 13:31:08 -0500 Subject: [PATCH 05/13] Added new tests --- azure/functions/decorators/function_app.py | 6 ---- azure/functions/decorators/function_name.py | 1 + azure/functions/decorators/retry_policy.py | 9 ++---- azure/functions/decorators/timer.py | 1 - tests/decorators/test_core.py | 32 ++++++++++++++++++++- tests/decorators/test_function_name.py | 16 +++++++++++ tests/decorators/test_retry_policy.py | 31 ++++++++++++++++++++ 7 files changed, 82 insertions(+), 14 deletions(-) create mode 100644 tests/decorators/test_function_name.py create mode 100644 tests/decorators/test_retry_policy.py diff --git a/azure/functions/decorators/function_app.py b/azure/functions/decorators/function_app.py index 897df9ca..79bf8712 100644 --- a/azure/functions/decorators/function_app.py +++ b/azure/functions/decorators/function_app.py @@ -143,12 +143,6 @@ def get_function_name(self) -> Optional[str]: return function_name_setting.get_settings_value("name") \ if function_name_setting else self._name - def get_setting_values(self, setting_name: str): - """ Gets the values of a specific setting attached to the function. - """ - setting = self.get_setting(setting_name) - return setting.get_dict_repr() if setting else None - def get_raw_bindings(self) -> List[str]: return [json.dumps(b.get_dict_repr(), cls=StringifyEnumJsonEncoder) for b in self._bindings] diff --git a/azure/functions/decorators/function_name.py b/azure/functions/decorators/function_name.py index 9f01a423..85e9b580 100644 --- a/azure/functions/decorators/function_name.py +++ b/azure/functions/decorators/function_name.py @@ -1,5 +1,6 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. + from azure.functions.decorators.constants import FUNCTION_NAME from azure.functions.decorators.core import Setting diff --git a/azure/functions/decorators/retry_policy.py b/azure/functions/decorators/retry_policy.py index b266bbf3..a4dacfb0 100644 --- a/azure/functions/decorators/retry_policy.py +++ b/azure/functions/decorators/retry_policy.py @@ -11,9 +11,9 @@ class RetryPolicy(Setting): def __init__(self, strategy: str, max_retry_count: str, - delay_interval: Optional[str], - minimum_interval: Optional[str], - maximum_interval: Optional[str], + delay_interval: Optional[str] = None, + minimum_interval: Optional[str] = None, + maximum_interval: Optional[str] = None, **kwargs): self.strategy = strategy self.max_retry_count = max_retry_count @@ -21,6 +21,3 @@ def __init__(self, self.minimum_interval = minimum_interval self.maximum_interval = maximum_interval super().__init__(setting_type=RETRY_POLICY) - - def get_value(self, name: str) -> Optional[str]: - return self.get_dict_repr().get(name) diff --git a/azure/functions/decorators/timer.py b/azure/functions/decorators/timer.py index a44c55ab..2d1967b2 100644 --- a/azure/functions/decorators/timer.py +++ b/azure/functions/decorators/timer.py @@ -21,5 +21,4 @@ def __init__(self, self.schedule = schedule self.run_on_startup = run_on_startup self.use_monitor = use_monitor - super().__init__(name=name, data_type=data_type) diff --git a/tests/decorators/test_core.py b/tests/decorators/test_core.py index 390d6b9e..528cb8a9 100644 --- a/tests/decorators/test_core.py +++ b/tests/decorators/test_core.py @@ -4,7 +4,7 @@ import unittest from azure.functions.decorators.core import BindingDirection, DataType, \ - InputBinding, OutputBinding, Trigger + InputBinding, OutputBinding, Trigger, Setting class DummyTrigger(Trigger): @@ -31,6 +31,12 @@ def __init__(self, super().__init__(name=name, data_type=data_type) +class DummySetting(Setting): + + def __init__(self, setting_type: str) -> None: + super().__init__(setting_type=setting_type) + + class DummyOutputBinding(OutputBinding): @staticmethod def get_binding_name() -> str: @@ -100,3 +106,27 @@ def test_supported_trigger_types_populated(self): self.assertTrue(len(trigger_type_name) > 0, f"binding_type {trigger_name} can not be " f"empty str!") + + +class TestSettings(unittest.TestCase): + + def test_setting_creation(self): + test_setting = DummySetting(setting_type="TestSetting") + + expected_dict = {'setting_type': "TestSetting"} + + self.assertEqual(test_setting.get_setting_type(), "TestSetting") + self.assertEqual(test_setting.get_dict_repr(), expected_dict) + + def test_get_dict_repr(self): + class NewSetting(DummySetting): + + def __init__(self, name: str): + self.name = name + super().__init__(setting_type="TestSetting") + + test_setting = NewSetting(name="NewSetting") + + expected_dict = {'setting_type': "TestSetting", "name": "NewSetting"} + + self.assertEqual(test_setting.get_dict_repr(), expected_dict) diff --git a/tests/decorators/test_function_name.py b/tests/decorators/test_function_name.py new file mode 100644 index 00000000..94da81c3 --- /dev/null +++ b/tests/decorators/test_function_name.py @@ -0,0 +1,16 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import unittest + +from azure.functions.decorators.function_name import FunctionName + + +class TestFunctionName(unittest.TestCase): + + def test_retry_policy_setting_creation(self): + function_name = FunctionName(name="TestFunctionName") + + self.assertEqual(function_name.get_setting_type(), "function_name") + self.assertEqual(function_name.get_dict_repr(), + {'setting_type': 'function_name', + 'name': 'TestFunctionName'}) diff --git a/tests/decorators/test_retry_policy.py b/tests/decorators/test_retry_policy.py new file mode 100644 index 00000000..0a5c0f97 --- /dev/null +++ b/tests/decorators/test_retry_policy.py @@ -0,0 +1,31 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import unittest + +from azure.functions.decorators.retry_policy import RetryPolicy + + +class TestRetryPolicy(unittest.TestCase): + + def test_retry_policy_setting_creation(self): + retry_policy = RetryPolicy(max_retry_count="1", + strategy="fixed", + delay_interval="5") + + self.assertEqual(retry_policy.get_setting_type(), "retry_policy") + self.assertEqual(retry_policy.get_dict_repr(), + {'setting_type': 'retry_policy', + 'strategy': 'fixed', + 'maxRetryCount': '1', + 'delayInterval': '5'}) + + retry_policy = RetryPolicy(max_retry_count="1", + strategy="exponential", + minimum_interval="5", + maximum_interval="10") + self.assertEqual(retry_policy.get_dict_repr(), + {'setting_type': 'retry_policy', + 'strategy': 'exponential', + 'minimumInterval': '5', + 'maxRetryCount': '1', + 'maximumInterval': '10'}) From e727a6acebbaad3b221ca7d8f21a02b3a194d076 Mon Sep 17 00:00:00 2001 From: Gavin Aguiar Date: Tue, 20 Jun 2023 13:54:56 -0500 Subject: [PATCH 06/13] Added tests for retry decorator --- tests/decorators/test_decorators.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/decorators/test_decorators.py b/tests/decorators/test_decorators.py index 146921af..b039f7a9 100644 --- a/tests/decorators/test_decorators.py +++ b/tests/decorators/test_decorators.py @@ -2093,3 +2093,20 @@ def _test_function_metadata_order(self, app): new_metadata_payload = str(func) self.assertEqual(new_metadata_payload, last_metadata_payload) last_metadata_payload = new_metadata_payload + + def test_function_app_retry_default_args(self): + app = self.func_app + + @app.schedule(arg_name="req", schedule="dummy_schedule") + @app.retry(strategy="fixed", max_retry_count="2", delay_interval="4") + def dummy_func(): + pass + + func = self._get_user_function(app) + self.assertEqual(func.get_function_name(), "dummy_func") + self.assertEqual(func.get_setting("retry_policy").get_dict_repr(), { + 'setting_type': 'retry_policy', + 'strategy': 'fixed', + 'maxRetryCount': '2', + 'delayInterval': '4' + }) From 27c56fea548e22685d5fb4da6505563cb3253295 Mon Sep 17 00:00:00 2001 From: Gavin Aguiar Date: Tue, 20 Jun 2023 14:02:20 -0500 Subject: [PATCH 07/13] Removed old commented method --- azure/functions/decorators/function_app.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/azure/functions/decorators/function_app.py b/azure/functions/decorators/function_app.py index 79bf8712..9079830a 100644 --- a/azure/functions/decorators/function_app.py +++ b/azure/functions/decorators/function_app.py @@ -93,15 +93,6 @@ def add_setting(self, setting: Setting) -> None: """ self._settings.append(setting) - # def set_function_name(self, function_name: Optional[str] = None) -> None: - # """Set or update the name for the function if :param:`function_name` - # is not None. If not set, function name will default to python - # function name. - # :param function_name: Name the function set to. - # """ - # if function_name: - # self._name = function_name - def set_http_type(self, http_type: str) -> None: """Set or update the http type for the function if :param:`http_type` . From 24797d28912e65925d9c750ed97cbc1a67957199 Mon Sep 17 00:00:00 2001 From: Gavin Aguiar Date: Tue, 20 Jun 2023 14:25:42 -0500 Subject: [PATCH 08/13] Updating ProgModelSpec --- docs/ProgModelSpec.pyi | 35 +++++++++++------------------------ 1 file changed, 11 insertions(+), 24 deletions(-) diff --git a/docs/ProgModelSpec.pyi b/docs/ProgModelSpec.pyi index ed3aeea0..9c67f94f 100644 --- a/docs/ProgModelSpec.pyi +++ b/docs/ProgModelSpec.pyi @@ -1,27 +1,14 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -import json from abc import ABC from typing import Callable, Dict, List, Optional, Union, Iterable from azure.functions import AsgiMiddleware, WsgiMiddleware -from azure.functions.decorators.blob import BlobTrigger, BlobInput, BlobOutput from azure.functions.decorators.core import Binding, Trigger, DataType, \ - AuthLevel, SCRIPT_FILE_NAME, Cardinality, AccessRights -from azure.functions.decorators.cosmosdb import CosmosDBTrigger, \ - CosmosDBOutput, CosmosDBInput -from azure.functions.decorators.eventhub import EventHubTrigger, EventHubOutput + AuthLevel, Cardinality, AccessRights, Setting from azure.functions.decorators.function_app import FunctionBuilder -from azure.functions.decorators.http import HttpTrigger, HttpOutput, \ - HttpMethod -from azure.functions.decorators.queue import QueueTrigger, QueueOutput -from azure.functions.decorators.servicebus import ServiceBusQueueTrigger, \ - ServiceBusQueueOutput, ServiceBusTopicTrigger, \ - ServiceBusTopicOutput -from azure.functions.decorators.timer import TimerTrigger -from azure.functions.decorators.utils import parse_singular_param_to_enum, \ - parse_iterable_param_to_enums, StringifyEnumJsonEncoder -from azure.functions.http import HttpRequest +from azure.functions.decorators.http import HttpMethod + class Function(object): """The function object represents a function in Function App. It @@ -54,14 +41,6 @@ class Function(object): pass - def set_function_name(self, function_name: Optional[str] = None) -> None: - """Set or update the name for the function if :param:`function_name` - is not None. If not set, function name will default to python - function name. - :param function_name: Name the function set to. - """ - pass - def get_trigger(self) -> Optional[Trigger]: """Get attached trigger instance of the function. @@ -107,6 +86,14 @@ class Function(object): """ pass + def get_setting(self, setting_name: str) -> Optional[Setting]: + """Get a specific setting attached to the function. + + :param setting_name: The name of the setting to search for. + :return: The setting attached to the function (or None if not found). + """ + pass + def get_function_json(self) -> str: """Get the json stringified form of function. From f89b31e921d007b00f3fd18cc94d7a4b6feff74c Mon Sep 17 00:00:00 2001 From: Gavin Aguiar Date: Wed, 21 Jun 2023 12:57:13 -0500 Subject: [PATCH 09/13] Added method for get_settings_json --- azure/functions/decorators/function_app.py | 13 +++++++++++++ tests/decorators/test_function_app.py | 16 ++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/azure/functions/decorators/function_app.py b/azure/functions/decorators/function_app.py index 9079830a..07c39d67 100644 --- a/azure/functions/decorators/function_app.py +++ b/azure/functions/decorators/function_app.py @@ -128,7 +128,20 @@ def get_setting(self, setting_name: str) -> Optional[Setting]: return setting return None + def get_settings_json(self, setting_name) -> Optional[Dict]: + """Get a dictionary representation of a setting. + + :param: setting_name: The name of the setting to search for. + :return: The dictionary representation of the setting (or None if not + found). + """ + setting = self.get_setting(setting_name) + return setting.get_dict_repr() if setting else None + def get_function_name(self) -> Optional[str]: + """Get the name of the function. + :return: The name of the function. + """ function_name_setting = \ self.get_setting("function_name") return function_name_setting.get_settings_value("name") \ diff --git a/tests/decorators/test_function_app.py b/tests/decorators/test_function_app.py index 87a60450..65349abd 100644 --- a/tests/decorators/test_function_app.py +++ b/tests/decorators/test_function_app.py @@ -17,6 +17,7 @@ TriggerApi, ExternalHttpFunctionApp from azure.functions.decorators.http import HttpTrigger, HttpOutput, \ HttpMethod +from azure.functions.decorators.retry_policy import RetryPolicy from tests.decorators.test_core import DummyTrigger from tests.decorators.testutils import assert_json @@ -205,6 +206,21 @@ def test_build_function_with_function_app_auth_level(self): self.assertEqual(func.get_trigger().auth_level, AuthLevel.ANONYMOUS) + def test_build_function_with_retry_policy_setting(self): + setting = RetryPolicy(strategy="exponential", max_retry_count="2", + minimum_interval="1", maximum_interval="5") + trigger = HttpTrigger(name='req', methods=(HttpMethod.GET,), + data_type=DataType.UNDEFINED, + auth_level=AuthLevel.ANONYMOUS) + self.fb.add_trigger(trigger) + self.fb.add_setting(setting) + func = self.fb.build() + + self.assertEqual(func.get_settings_json("retry_policy"), + {'setting_type': 'retry_policy', + 'strategy': 'exponential', 'maxRetryCount': '2', + 'minimumInterval': '1', 'maximumInterval': '5'}) + class TestScaffold(unittest.TestCase): def setUp(self): From d6915b4a7954ad3667e3b1f08cb04233c8eda60a Mon Sep 17 00:00:00 2001 From: Gavin Aguiar Date: Thu, 22 Jun 2023 14:07:23 -0500 Subject: [PATCH 10/13] Addressed comments --- azure/functions/decorators/core.py | 4 ++++ azure/functions/decorators/function_app.py | 28 +++------------------- tests/decorators/test_function_app.py | 2 +- 3 files changed, 8 insertions(+), 26 deletions(-) diff --git a/azure/functions/decorators/core.py b/azure/functions/decorators/core.py index 478045db..3ef2a79e 100644 --- a/azure/functions/decorators/core.py +++ b/azure/functions/decorators/core.py @@ -164,6 +164,10 @@ def __init__(self, name: str, data_type: Optional[DataType] = None, class Setting(ABC, metaclass=ABCBuildDictMeta): + """ Abstract class for all settings of a function app. + This class represents all the decorators that cannot be + classified as bindings or triggers. e.g function_name, retry etc. + """ EXCLUDED_INIT_PARAMS = {'self', 'kwargs', 'setting_type'} diff --git a/azure/functions/decorators/function_app.py b/azure/functions/decorators/function_app.py index 07c39d67..8a33b4cc 100644 --- a/azure/functions/decorators/function_app.py +++ b/azure/functions/decorators/function_app.py @@ -128,7 +128,7 @@ def get_setting(self, setting_name: str) -> Optional[Setting]: return setting return None - def get_settings_json(self, setting_name) -> Optional[Dict]: + def get_settings_dict(self, setting_name) -> Optional[Dict]: """Get a dictionary representation of a setting. :param: setting_name: The name of the setting to search for. @@ -176,13 +176,6 @@ def get_user_function(self) -> Callable[..., Any]: """ return self._func - # def get_function_name(self) -> str: - # """Get the function name. - - # :return: Function name. - # """ - # return self._name - def get_function_json(self) -> str: """Get the json stringified form of function. @@ -318,23 +311,6 @@ def decorator(func): return decorator - # def function_name(self, name: str) -> Callable[..., Any]: - # """Set name of the :class:`Function` object. - - # :param name: Name of the function. - # :return: Decorator function. - # """ - - # @self._configure_function_builder - # def wrap(fb): - # def decorator(): - # fb.configure_function_name(name) - # return fb - - # return decorator() - - # return wrap - def http_type(self, http_type: str) -> Callable[..., Any]: """Set http type of the :class:`Function` object. @@ -1969,6 +1945,8 @@ def decorator(): class SettingsApi(DecoratorApi, ABC): + """Interface to extend for using existing settings decorator in + functions.""" def function_name(self, name: str, setting_extra_fields: Dict[str, Any] = {}, diff --git a/tests/decorators/test_function_app.py b/tests/decorators/test_function_app.py index 65349abd..0fac2f7c 100644 --- a/tests/decorators/test_function_app.py +++ b/tests/decorators/test_function_app.py @@ -216,7 +216,7 @@ def test_build_function_with_retry_policy_setting(self): self.fb.add_setting(setting) func = self.fb.build() - self.assertEqual(func.get_settings_json("retry_policy"), + self.assertEqual(func.get_settings_dict("retry_policy"), {'setting_type': 'retry_policy', 'strategy': 'exponential', 'maxRetryCount': '2', 'minimumInterval': '1', 'maximumInterval': '5'}) From 3feaca22baeba71cc2b16a7a19dad22f8e8bd1f3 Mon Sep 17 00:00:00 2001 From: Gavin Aguiar Date: Thu, 22 Jun 2023 14:20:17 -0500 Subject: [PATCH 11/13] Added comment for function_name --- azure/functions/decorators/function_app.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/azure/functions/decorators/function_app.py b/azure/functions/decorators/function_app.py index 8a33b4cc..074c6754 100644 --- a/azure/functions/decorators/function_app.py +++ b/azure/functions/decorators/function_app.py @@ -1951,7 +1951,8 @@ class SettingsApi(DecoratorApi, ABC): def function_name(self, name: str, setting_extra_fields: Dict[str, Any] = {}, ) -> Callable[..., Any]: - """Set name of the :class:`Function` object. + """Optional: Sets name of the :class:`Function` object. If not set, + it will default to the name of the method name. :param name: Name of the function. :return: Decorator function. From c639451280dbfa5ca0fd59b254a51329e0939099 Mon Sep 17 00:00:00 2001 From: Gavin Aguiar Date: Mon, 26 Jun 2023 15:50:56 -0500 Subject: [PATCH 12/13] Addressed comments --- azure/functions/decorators/constants.py | 2 -- azure/functions/decorators/core.py | 13 +++++------ azure/functions/decorators/function_app.py | 15 +++++++++---- azure/functions/decorators/function_name.py | 5 +++-- azure/functions/decorators/retry_policy.py | 5 +++-- tests/decorators/test_core.py | 25 ++++++++++++--------- tests/decorators/test_decorators.py | 2 +- tests/decorators/test_function_app.py | 2 +- tests/decorators/test_function_name.py | 4 ++-- tests/decorators/test_retry_policy.py | 6 ++--- 10 files changed, 45 insertions(+), 34 deletions(-) diff --git a/azure/functions/decorators/constants.py b/azure/functions/decorators/constants.py index adf470cd..fde60864 100644 --- a/azure/functions/decorators/constants.py +++ b/azure/functions/decorators/constants.py @@ -19,5 +19,3 @@ EVENT_GRID_TRIGGER = "eventGridTrigger" EVENT_GRID = "eventGrid" TABLE = "table" -RETRY_POLICY = "retry_policy" -FUNCTION_NAME = "function_name" diff --git a/azure/functions/decorators/core.py b/azure/functions/decorators/core.py index 3ef2a79e..d2675f27 100644 --- a/azure/functions/decorators/core.py +++ b/azure/functions/decorators/core.py @@ -169,16 +169,15 @@ class Setting(ABC, metaclass=ABCBuildDictMeta): classified as bindings or triggers. e.g function_name, retry etc. """ - EXCLUDED_INIT_PARAMS = {'self', 'kwargs', 'setting_type'} + EXCLUDED_INIT_PARAMS = {'self', 'kwargs', 'setting_name'} - def get_setting_type(self) -> str: - return self.setting_type + def get_setting_name(self) -> str: + return self.setting_name - def __init__(self, setting_type: str) -> None: - if setting_type is not None: - self.setting_type = setting_type + def __init__(self, setting_name: str) -> None: + self.setting_name = setting_name self._dict: Dict = { - "setting_type": self.setting_type + "setting_name": self.setting_name } def get_dict_repr(self) -> Dict: diff --git a/azure/functions/decorators/function_app.py b/azure/functions/decorators/function_app.py index 074c6754..17b9365a 100644 --- a/azure/functions/decorators/function_app.py +++ b/azure/functions/decorators/function_app.py @@ -47,6 +47,11 @@ def __init__(self, func: Callable[..., Any], script_file: str): :param func: User defined python function instance. :param script_file: File name indexed by worker to find function. + :param trigger: The trigger object of the function. + :param bindings: The list of binding objects of a function. + :param settings: The list of setting objects of a function. + :param http_type: Http function type. + :param is_http_function: Whether the function is a http function. """ self._name: str = func.__name__ self._func = func @@ -124,7 +129,7 @@ def get_setting(self, setting_name: str) -> Optional[Setting]: :return: The setting attached to the function (or None if not found). """ for setting in self._settings: - if setting.setting_type == setting_name: + if setting.setting_name == setting_name: return setting return None @@ -1955,6 +1960,8 @@ def function_name(self, name: str, it will default to the name of the method name. :param name: Name of the function. + :param setting_extra_fields: Keyword arguments for specifying + additional setting fields :return: Decorator function. """ @@ -1963,7 +1970,6 @@ def wrap(fb): def decorator(): fb.add_setting(setting=FunctionName( name=name, - setting_type="setting", **setting_extra_fields)) return fb @@ -1986,7 +1992,7 @@ def retry(self, All optional fields will be given default value by function host when they are parsed by function host. - Ref: https://aka.ms/azure-function-retry + Ref: https://aka.ms/azure_functions_retries :param strategy: The retry strategy to use. :param max_retry_count: The maximum number of retry attempts. @@ -1996,7 +2002,8 @@ def retry(self, :param maximum_interval: The maximum delay interval between retry attempts. :param setting_extra_fields: Keyword arguments for specifying - additional setting fields to include in the setting json. + additional setting fields. + :return: Decorator function. """ @self._configure_function_builder diff --git a/azure/functions/decorators/function_name.py b/azure/functions/decorators/function_name.py index 85e9b580..cf0b562d 100644 --- a/azure/functions/decorators/function_name.py +++ b/azure/functions/decorators/function_name.py @@ -1,13 +1,14 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -from azure.functions.decorators.constants import FUNCTION_NAME from azure.functions.decorators.core import Setting +FUNCTION_NAME = "function_name" + class FunctionName(Setting): def __init__(self, name: str, **kwargs): self.name = name - super().__init__(setting_type=FUNCTION_NAME) + super().__init__(setting_name=FUNCTION_NAME) diff --git a/azure/functions/decorators/retry_policy.py b/azure/functions/decorators/retry_policy.py index a4dacfb0..8c33b427 100644 --- a/azure/functions/decorators/retry_policy.py +++ b/azure/functions/decorators/retry_policy.py @@ -2,9 +2,10 @@ # Licensed under the MIT License. from typing import Optional -from azure.functions.decorators.constants import RETRY_POLICY from azure.functions.decorators.core import Setting +RETRY_POLICY = "retry_policy" + class RetryPolicy(Setting): @@ -20,4 +21,4 @@ def __init__(self, self.delay_interval = delay_interval self.minimum_interval = minimum_interval self.maximum_interval = maximum_interval - super().__init__(setting_type=RETRY_POLICY) + super().__init__(setting_name=RETRY_POLICY) diff --git a/tests/decorators/test_core.py b/tests/decorators/test_core.py index 528cb8a9..0c725b2e 100644 --- a/tests/decorators/test_core.py +++ b/tests/decorators/test_core.py @@ -33,8 +33,8 @@ def __init__(self, class DummySetting(Setting): - def __init__(self, setting_type: str) -> None: - super().__init__(setting_type=setting_type) + def __init__(self, setting_name: str) -> None: + super().__init__(setting_name=setting_name) class DummyOutputBinding(OutputBinding): @@ -111,22 +111,27 @@ def test_supported_trigger_types_populated(self): class TestSettings(unittest.TestCase): def test_setting_creation(self): - test_setting = DummySetting(setting_type="TestSetting") - - expected_dict = {'setting_type': "TestSetting"} - - self.assertEqual(test_setting.get_setting_type(), "TestSetting") - self.assertEqual(test_setting.get_dict_repr(), expected_dict) + """ + Tests that the setting_name is set correctly + """ + # DummySetting is a test setting that inherits from Setting + test_setting = DummySetting(setting_name="TestSetting") + self.assertEqual(test_setting.get_setting_name(), "TestSetting") def test_get_dict_repr(self): + """ + Tests that the get_dict_repr method returns the correct dict + when a new setting is intialized + """ + class NewSetting(DummySetting): def __init__(self, name: str): self.name = name - super().__init__(setting_type="TestSetting") + super().__init__(setting_name="TestSetting") test_setting = NewSetting(name="NewSetting") - expected_dict = {'setting_type': "TestSetting", "name": "NewSetting"} + expected_dict = {'setting_name': "TestSetting", "name": "NewSetting"} self.assertEqual(test_setting.get_dict_repr(), expected_dict) diff --git a/tests/decorators/test_decorators.py b/tests/decorators/test_decorators.py index b039f7a9..028efa88 100644 --- a/tests/decorators/test_decorators.py +++ b/tests/decorators/test_decorators.py @@ -2105,7 +2105,7 @@ def dummy_func(): func = self._get_user_function(app) self.assertEqual(func.get_function_name(), "dummy_func") self.assertEqual(func.get_setting("retry_policy").get_dict_repr(), { - 'setting_type': 'retry_policy', + 'setting_name': 'retry_policy', 'strategy': 'fixed', 'maxRetryCount': '2', 'delayInterval': '4' diff --git a/tests/decorators/test_function_app.py b/tests/decorators/test_function_app.py index 0fac2f7c..3476d1f6 100644 --- a/tests/decorators/test_function_app.py +++ b/tests/decorators/test_function_app.py @@ -217,7 +217,7 @@ def test_build_function_with_retry_policy_setting(self): func = self.fb.build() self.assertEqual(func.get_settings_dict("retry_policy"), - {'setting_type': 'retry_policy', + {'setting_name': 'retry_policy', 'strategy': 'exponential', 'maxRetryCount': '2', 'minimumInterval': '1', 'maximumInterval': '5'}) diff --git a/tests/decorators/test_function_name.py b/tests/decorators/test_function_name.py index 94da81c3..47426c27 100644 --- a/tests/decorators/test_function_name.py +++ b/tests/decorators/test_function_name.py @@ -10,7 +10,7 @@ class TestFunctionName(unittest.TestCase): def test_retry_policy_setting_creation(self): function_name = FunctionName(name="TestFunctionName") - self.assertEqual(function_name.get_setting_type(), "function_name") + self.assertEqual(function_name.get_setting_name(), "function_name") self.assertEqual(function_name.get_dict_repr(), - {'setting_type': 'function_name', + {'setting_name': 'function_name', 'name': 'TestFunctionName'}) diff --git a/tests/decorators/test_retry_policy.py b/tests/decorators/test_retry_policy.py index 0a5c0f97..6bc8f663 100644 --- a/tests/decorators/test_retry_policy.py +++ b/tests/decorators/test_retry_policy.py @@ -12,9 +12,9 @@ def test_retry_policy_setting_creation(self): strategy="fixed", delay_interval="5") - self.assertEqual(retry_policy.get_setting_type(), "retry_policy") + self.assertEqual(retry_policy.get_setting_name(), "retry_policy") self.assertEqual(retry_policy.get_dict_repr(), - {'setting_type': 'retry_policy', + {'setting_name': 'retry_policy', 'strategy': 'fixed', 'maxRetryCount': '1', 'delayInterval': '5'}) @@ -24,7 +24,7 @@ def test_retry_policy_setting_creation(self): minimum_interval="5", maximum_interval="10") self.assertEqual(retry_policy.get_dict_repr(), - {'setting_type': 'retry_policy', + {'setting_name': 'retry_policy', 'strategy': 'exponential', 'minimumInterval': '5', 'maxRetryCount': '1', From 72d353fc5944e04da327ca3c21e053989d75c48e Mon Sep 17 00:00:00 2001 From: Gavin Aguiar Date: Wed, 5 Jul 2023 14:52:57 -0500 Subject: [PATCH 13/13] Addressed comments --- azure/functions/decorators/core.py | 15 +++++++++------ azure/functions/decorators/function_app.py | 4 ++-- azure/functions/decorators/function_name.py | 4 ++-- tests/decorators/test_core.py | 1 + tests/decorators/test_decorators.py | 6 +++--- tests/decorators/test_function_app.py | 4 ++-- tests/decorators/test_function_name.py | 4 ++-- tests/decorators/test_retry_policy.py | 10 +++++----- 8 files changed, 26 insertions(+), 22 deletions(-) diff --git a/azure/functions/decorators/core.py b/azure/functions/decorators/core.py index d2675f27..81e2ff9f 100644 --- a/azure/functions/decorators/core.py +++ b/azure/functions/decorators/core.py @@ -171,15 +171,15 @@ class Setting(ABC, metaclass=ABCBuildDictMeta): EXCLUDED_INIT_PARAMS = {'self', 'kwargs', 'setting_name'} - def get_setting_name(self) -> str: - return self.setting_name - def __init__(self, setting_name: str) -> None: self.setting_name = setting_name self._dict: Dict = { "setting_name": self.setting_name } + def get_setting_name(self) -> str: + return self.setting_name + def get_dict_repr(self) -> Dict: """Build a dictionary of a particular binding. The keys are camel cased binding field names defined in `init_params` list and @@ -192,9 +192,12 @@ def get_dict_repr(self) -> Dict: params = list(dict.fromkeys(getattr(self, 'init_params', []))) for p in params: if p not in Setting.EXCLUDED_INIT_PARAMS: - self._dict[to_camel_case(p)] = getattr(self, p, None) + self._dict[p] = getattr(self, p, None) return self._dict - def get_settings_value(self, name: str) -> Optional[str]: - return self.get_dict_repr().get(name) + def get_settings_value(self, settings_attribute_key: str) -> Optional[str]: + """ + Get the value of a particular setting attribute. + """ + return self.get_dict_repr().get(settings_attribute_key) diff --git a/azure/functions/decorators/function_app.py b/azure/functions/decorators/function_app.py index 17b9365a..3e19d7e3 100644 --- a/azure/functions/decorators/function_app.py +++ b/azure/functions/decorators/function_app.py @@ -149,7 +149,7 @@ def get_function_name(self) -> Optional[str]: """ function_name_setting = \ self.get_setting("function_name") - return function_name_setting.get_settings_value("name") \ + return function_name_setting.get_settings_value("function_name") \ if function_name_setting else self._name def get_raw_bindings(self) -> List[str]: @@ -1969,7 +1969,7 @@ def function_name(self, name: str, def wrap(fb): def decorator(): fb.add_setting(setting=FunctionName( - name=name, + function_name=name, **setting_extra_fields)) return fb diff --git a/azure/functions/decorators/function_name.py b/azure/functions/decorators/function_name.py index cf0b562d..49e4869c 100644 --- a/azure/functions/decorators/function_name.py +++ b/azure/functions/decorators/function_name.py @@ -8,7 +8,7 @@ class FunctionName(Setting): - def __init__(self, name: str, + def __init__(self, function_name: str, **kwargs): - self.name = name + self.function_name = function_name super().__init__(setting_name=FUNCTION_NAME) diff --git a/tests/decorators/test_core.py b/tests/decorators/test_core.py index 0c725b2e..dd400ed1 100644 --- a/tests/decorators/test_core.py +++ b/tests/decorators/test_core.py @@ -135,3 +135,4 @@ def __init__(self, name: str): expected_dict = {'setting_name': "TestSetting", "name": "NewSetting"} self.assertEqual(test_setting.get_dict_repr(), expected_dict) + self.assertEqual(test_setting.get_settings_value("name"), "NewSetting") diff --git a/tests/decorators/test_decorators.py b/tests/decorators/test_decorators.py index 028efa88..bb09fda4 100644 --- a/tests/decorators/test_decorators.py +++ b/tests/decorators/test_decorators.py @@ -61,7 +61,7 @@ def dummy_func(): func = self._get_user_function(app) - self.assertEqual(func.get_function_name(), "dummy_function") + self.assertEqual("dummy_function", func.get_function_name()) self.assertTrue(isinstance(func.get_trigger(), HttpTrigger)) self.assertTrue(func.get_trigger().route, "dummy") @@ -2107,6 +2107,6 @@ def dummy_func(): self.assertEqual(func.get_setting("retry_policy").get_dict_repr(), { 'setting_name': 'retry_policy', 'strategy': 'fixed', - 'maxRetryCount': '2', - 'delayInterval': '4' + 'max_retry_count': '2', + 'delay_interval': '4' }) diff --git a/tests/decorators/test_function_app.py b/tests/decorators/test_function_app.py index 3476d1f6..02448f1d 100644 --- a/tests/decorators/test_function_app.py +++ b/tests/decorators/test_function_app.py @@ -218,8 +218,8 @@ def test_build_function_with_retry_policy_setting(self): self.assertEqual(func.get_settings_dict("retry_policy"), {'setting_name': 'retry_policy', - 'strategy': 'exponential', 'maxRetryCount': '2', - 'minimumInterval': '1', 'maximumInterval': '5'}) + 'strategy': 'exponential', 'max_retry_count': '2', + 'minimum_interval': '1', 'maximum_interval': '5'}) class TestScaffold(unittest.TestCase): diff --git a/tests/decorators/test_function_name.py b/tests/decorators/test_function_name.py index 47426c27..1b8864d9 100644 --- a/tests/decorators/test_function_name.py +++ b/tests/decorators/test_function_name.py @@ -8,9 +8,9 @@ class TestFunctionName(unittest.TestCase): def test_retry_policy_setting_creation(self): - function_name = FunctionName(name="TestFunctionName") + function_name = FunctionName(function_name="TestFunctionName") self.assertEqual(function_name.get_setting_name(), "function_name") self.assertEqual(function_name.get_dict_repr(), {'setting_name': 'function_name', - 'name': 'TestFunctionName'}) + 'function_name': 'TestFunctionName'}) diff --git a/tests/decorators/test_retry_policy.py b/tests/decorators/test_retry_policy.py index 6bc8f663..3051c0b7 100644 --- a/tests/decorators/test_retry_policy.py +++ b/tests/decorators/test_retry_policy.py @@ -16,8 +16,8 @@ def test_retry_policy_setting_creation(self): self.assertEqual(retry_policy.get_dict_repr(), {'setting_name': 'retry_policy', 'strategy': 'fixed', - 'maxRetryCount': '1', - 'delayInterval': '5'}) + 'max_retry_count': '1', + 'delay_interval': '5'}) retry_policy = RetryPolicy(max_retry_count="1", strategy="exponential", @@ -26,6 +26,6 @@ def test_retry_policy_setting_creation(self): self.assertEqual(retry_policy.get_dict_repr(), {'setting_name': 'retry_policy', 'strategy': 'exponential', - 'minimumInterval': '5', - 'maxRetryCount': '1', - 'maximumInterval': '10'}) + 'minimum_interval': '5', + 'max_retry_count': '1', + 'maximum_interval': '10'})