From c871fdccd0212c0e0c4b7f62f550d9f4c9afabc8 Mon Sep 17 00:00:00 2001 From: peterstone2017 Date: Fri, 18 Mar 2022 12:50:10 -0500 Subject: [PATCH 1/3] Add generic binding decorators --- azure/functions/decorators/blob.py | 9 +- azure/functions/decorators/core.py | 41 +++- azure/functions/decorators/cosmosdb.py | 8 +- azure/functions/decorators/custom.py | 49 ++++ azure/functions/decorators/eventhub.py | 6 +- azure/functions/decorators/function_app.py | 262 ++++++++++++++++++--- azure/functions/decorators/http.py | 6 +- azure/functions/decorators/queue.py | 6 +- azure/functions/decorators/servicebus.py | 12 +- azure/functions/decorators/timer.py | 3 +- azure/functions/decorators/utils.py | 21 +- docs/ProgModelSpec.pyi | 203 ++++++++++++++-- tests/decorators/test_blob.py | 12 +- tests/decorators/test_core.py | 28 ++- tests/decorators/test_cosmosdb.py | 28 +-- tests/decorators/test_custom.py | 69 ++++++ tests/decorators/test_decorators.py | 252 ++++++++++++++++++-- tests/decorators/test_eventhub.py | 8 +- tests/decorators/test_function_app.py | 50 ++-- tests/decorators/test_http.py | 8 +- tests/decorators/test_queue.py | 8 +- tests/decorators/test_servicebus.py | 16 +- tests/decorators/test_timer.py | 4 +- tests/decorators/test_utils.py | 52 +++- 24 files changed, 989 insertions(+), 172 deletions(-) create mode 100644 azure/functions/decorators/custom.py create mode 100644 tests/decorators/test_custom.py diff --git a/azure/functions/decorators/blob.py b/azure/functions/decorators/blob.py index d7e1df5f..35d56780 100644 --- a/azure/functions/decorators/blob.py +++ b/azure/functions/decorators/blob.py @@ -12,7 +12,8 @@ def __init__(self, name: str, path: str, connection: str, - data_type: Optional[DataType] = None): + data_type: Optional[DataType] = None, + **kwargs): self.path = path self.connection = connection super().__init__(name=name, data_type=data_type) @@ -27,7 +28,8 @@ def __init__(self, name: str, path: str, connection: str, - data_type: Optional[DataType] = None): + data_type: Optional[DataType] = None, + **kwargs): self.path = path self.connection = connection super().__init__(name=name, data_type=data_type) @@ -42,7 +44,8 @@ def __init__(self, name: str, path: str, connection: str, - data_type: Optional[DataType] = None): + data_type: Optional[DataType] = None, + **kwargs): self.path = path self.connection = connection super().__init__(name=name, data_type=data_type) diff --git a/azure/functions/decorators/core.py b/azure/functions/decorators/core.py index e3d6249d..526138a2 100644 --- a/azure/functions/decorators/core.py +++ b/azure/functions/decorators/core.py @@ -1,9 +1,9 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. from abc import ABC, abstractmethod -from typing import Dict, Optional +from typing import Dict, Optional, Type -from azure.functions.decorators.utils import to_camel_case, \ +from .utils import to_camel_case, \ ABCBuildDictMeta, StringifyEnum SCRIPT_FILE_NAME = "function_app.py" @@ -73,6 +73,8 @@ class Binding(ABC): attribute in function.json when new binding classes are created. Ref: https://aka.ms/azure-function-binding-http """ + EXCLUDED_INIT_PARAMS = {'self', 'kwargs', 'type', 'data_type', 'direction'} + @staticmethod @abstractmethod def get_binding_name() -> str: @@ -80,10 +82,10 @@ def get_binding_name() -> str: def __init__(self, name: str, direction: BindingDirection, - is_trigger: bool, - data_type: Optional[DataType] = None): - self.type = self.get_binding_name() - self.is_trigger = is_trigger + data_type: Optional[DataType] = None, + type: Optional[str] = None): # NoQa + self.type = self.get_binding_name() \ + if self.get_binding_name() is not None else type self.name = name self._direction = direction self._data_type = data_type @@ -111,7 +113,7 @@ def get_dict_repr(self) -> Dict: :return: Dictionary representation of the binding. """ for p in getattr(self, 'init_params', []): - if p not in ['data_type', 'self']: + if p not in Binding.EXCLUDED_INIT_PARAMS: self._dict[to_camel_case(p)] = getattr(self, p, None) return self._dict @@ -121,24 +123,37 @@ class Trigger(Binding, ABC, metaclass=ABCBuildDictMeta): """Class representation of Azure Function Trigger. \n Ref: https://aka.ms/functions-triggers-bindings-overview """ - def __init__(self, name, data_type) -> None: + + def __init__(self, name: str, data_type: Optional[DataType] = None, + type: Optional[str] = None) -> None: super().__init__(direction=BindingDirection.IN, - name=name, data_type=data_type, is_trigger=True) + name=name, data_type=data_type, type=type) class InputBinding(Binding, ABC, metaclass=ABCBuildDictMeta): """Class representation of Azure Function Input Binding. \n Ref: https://aka.ms/functions-triggers-bindings-overview """ - def __init__(self, name, data_type) -> None: + + def __init__(self, name: str, data_type: Optional[DataType] = None, + type: Optional[str] = None) -> None: super().__init__(direction=BindingDirection.IN, - name=name, data_type=data_type, is_trigger=False) + name=name, data_type=data_type, type=type) class OutputBinding(Binding, ABC, metaclass=ABCBuildDictMeta): """Class representation of Azure Function Output Binding. \n Ref: https://aka.ms/functions-triggers-bindings-overview """ - def __init__(self, name, data_type) -> None: + + 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, is_trigger=False) + name=name, data_type=data_type, type=type) + + +def is_supported_trigger_type(trigger_instance: Trigger, + trigger_type: Type[Trigger]): + return isinstance(trigger_instance, + trigger_type) or \ + trigger_instance.type == trigger_type.get_binding_name() diff --git a/azure/functions/decorators/cosmosdb.py b/azure/functions/decorators/cosmosdb.py index 8ce33930..1ad3443f 100644 --- a/azure/functions/decorators/cosmosdb.py +++ b/azure/functions/decorators/cosmosdb.py @@ -20,7 +20,8 @@ def __init__(self, data_type: Optional[DataType] = None, id: Optional[str] = None, sql_query: Optional[str] = None, - partition_key: Optional[str] = None): + partition_key: Optional[str] = None, + **kwargs): self.database_name = database_name self.collection_name = collection_name self.connection_string_setting = connection_string_setting @@ -45,7 +46,8 @@ def __init__(self, use_multiple_write_locations: Optional[bool] = None, preferred_locations: Optional[str] = None, partition_key: Optional[str] = None, - data_type: Optional[DataType] = None): + data_type: Optional[DataType] = None, + **kwargs): self.database_name = database_name self.collection_name = collection_name self.connection_string_setting = connection_string_setting @@ -83,7 +85,7 @@ def __init__(self, lease_connection_string_setting: Optional[str] = None, lease_database_name: Optional[str] = None, lease_collection_prefix: Optional[str] = None, - ): + **kwargs): self.lease_collection_name = lease_collection_name self.lease_connection_string_setting = lease_connection_string_setting self.lease_database_name = lease_database_name diff --git a/azure/functions/decorators/custom.py b/azure/functions/decorators/custom.py new file mode 100644 index 00000000..0e7ee0b8 --- /dev/null +++ b/azure/functions/decorators/custom.py @@ -0,0 +1,49 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +from typing import Optional + +from azure.functions.decorators.core import Trigger, \ + InputBinding, OutputBinding, DataType + + +class CustomInputBinding(InputBinding): + + @staticmethod + def get_binding_name() -> str: + pass + + def __init__(self, + name: str, + type: str, + data_type: Optional[DataType] = None, + **kwargs): + super().__init__(name=name, data_type=data_type, type=type) + + +class CustomOutputBinding(OutputBinding): + # binding_name: str = "" + + @staticmethod + def get_binding_name() -> str: + pass + + def __init__(self, + name: str, + type: str, + data_type: Optional[DataType] = None, + **kwargs): + super().__init__(name=name, data_type=data_type, type=type) + + +class CustomTrigger(Trigger): + + @staticmethod + def get_binding_name() -> str: + pass + + def __init__(self, + name: str, + type: str, + data_type: Optional[DataType] = None, + **kwargs): + super().__init__(name=name, data_type=data_type, type=type) diff --git a/azure/functions/decorators/eventhub.py b/azure/functions/decorators/eventhub.py index fbf8c004..ef3c79ea 100644 --- a/azure/functions/decorators/eventhub.py +++ b/azure/functions/decorators/eventhub.py @@ -19,7 +19,8 @@ def __init__(self, event_hub_name: str, data_type: Optional[DataType] = None, cardinality: Optional[Cardinality] = None, - consumer_group: Optional[str] = None): + consumer_group: Optional[str] = None, + **kwargs): self.connection = connection self.event_hub_name = event_hub_name self.cardinality = cardinality @@ -37,7 +38,8 @@ def __init__(self, name: str, connection: str, event_hub_name: str, - data_type: Optional[DataType] = None): + data_type: Optional[DataType] = None, + **kwargs): self.connection = connection self.event_hub_name = event_hub_name super().__init__(name=name, data_type=data_type) diff --git a/azure/functions/decorators/function_app.py b/azure/functions/decorators/function_app.py index 62937996..cf755e42 100644 --- a/azure/functions/decorators/function_app.py +++ b/azure/functions/decorators/function_app.py @@ -5,7 +5,8 @@ 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, \ + is_supported_trigger_type from azure.functions.decorators.cosmosdb import CosmosDBTrigger, \ CosmosDBOutput, CosmosDBInput from azure.functions.decorators.eventhub import EventHubTrigger, EventHubOutput @@ -19,6 +20,8 @@ from azure.functions.decorators.utils import parse_singular_param_to_enum, \ parse_iterable_param_to_enums, StringifyEnumJsonEncoder from azure.functions.http import HttpRequest +from .constants import HTTP_TRIGGER +from .custom import CustomInputBinding, CustomTrigger, CustomOutputBinding from .._http_asgi import AsgiMiddleware from .._http_wsgi import WsgiMiddleware, Context @@ -175,8 +178,10 @@ def _validate_function(self) -> None: f"Function {function_name} trigger {trigger} not present" f" in bindings {bindings}") - if isinstance(trigger, HttpTrigger) and trigger.route is None: - trigger.route = self._function.get_function_name() + # Set route to function name if unspecified in the http trigger + if is_supported_trigger_type(trigger, HttpTrigger) \ + and getattr(trigger, 'route', None) is None: + setattr(trigger, 'route', self._function.get_function_name()) def build(self) -> Function: self._validate_function() @@ -321,7 +326,10 @@ def route(self, binding_arg_name: str = '$return', methods: Optional[ Union[Iterable[str], Iterable[HttpMethod]]] = None, - auth_level: Optional[Union[AuthLevel, str]] = None) -> Callable: + auth_level: Optional[Union[AuthLevel, str]] = None, + trigger_extra_fields: Dict = {}, + binding_extra_fields: Dict = {} + ) -> Callable: """The route decorator adds :class:`HttpTrigger` and :class:`HttpOutput` binding to the :class:`FunctionBuilder` object for building :class:`Function` object used in worker function @@ -344,6 +352,12 @@ def route(self, :param auth_level: Determines what keys, if any, need to be present on the request in order to invoke the function. :return: Decorator function. + :param trigger_extra_fields: Additional fields to include in trigger + json. For example, + >>> data_type='STRING' # 'dataType': 'STRING' in trigger json + :param binding_extra_fields: Additional fields to include in binding + json. For example, + >>> data_type='STRING' # 'dataType': 'STRING' in binding json """ @self._configure_function_builder @@ -358,8 +372,9 @@ def decorator(): methods=parse_iterable_param_to_enums(methods, HttpMethod), auth_level=parse_singular_param_to_enum(auth_level, AuthLevel), - route=route)) - fb.add_binding(binding=HttpOutput(name=binding_arg_name)) + route=route, **trigger_extra_fields)) + fb.add_binding(binding=HttpOutput( + name=binding_arg_name, **binding_extra_fields)) return fb return decorator() @@ -371,7 +386,8 @@ def schedule(self, schedule: str, run_on_startup: Optional[bool] = None, use_monitor: Optional[bool] = None, - data_type: Optional[Union[DataType, str]] = None) -> Callable: + data_type: Optional[Union[DataType, str]] = None, + **kwargs) -> Callable: """The schedule decorator adds :class:`TimerTrigger` to the :class:`FunctionBuilder` object for building :class:`Function` object used in worker function @@ -406,7 +422,8 @@ def decorator(): run_on_startup=run_on_startup, use_monitor=use_monitor, data_type=parse_singular_param_to_enum(data_type, - DataType))) + DataType), + **kwargs)) return fb return decorator() @@ -421,8 +438,9 @@ def service_bus_queue_trigger( data_type: Optional[Union[DataType, str]] = None, access_rights: Optional[Union[AccessRights, str]] = None, is_sessions_enabled: Optional[bool] = None, - cardinality: Optional[Union[Cardinality, str]] = None) -> Callable: - """The service_bus_queue_trigger decorator adds + cardinality: Optional[Union[Cardinality, str]] = None, + **kwargs) -> Callable: + """The on_service_bus_queue_change decorator adds :class:`ServiceBusQueueTrigger` to the :class:`FunctionBuilder` object for building :class:`Function` object used in worker function indexing model. This is equivalent to defining ServiceBusQueueTrigger @@ -462,7 +480,8 @@ def decorator(): AccessRights), is_sessions_enabled=is_sessions_enabled, cardinality=parse_singular_param_to_enum(cardinality, - Cardinality))) + Cardinality), + **kwargs)) return fb return decorator() @@ -476,7 +495,9 @@ def write_service_bus_queue(self, data_type: Optional[ Union[DataType, str]] = None, access_rights: Optional[Union[ - AccessRights, str]] = None) -> Callable: + AccessRights, str]] = None, + **kwargs) -> \ + Callable: """The write_service_bus_queue decorator adds :class:`ServiceBusQueueOutput` to the :class:`FunctionBuilder` object for building :class:`Function` object used in worker function @@ -510,7 +531,8 @@ def decorator(): data_type=parse_singular_param_to_enum(data_type, DataType), access_rights=parse_singular_param_to_enum( - access_rights, AccessRights))) + access_rights, AccessRights), + **kwargs)) return fb return decorator() @@ -526,8 +548,9 @@ def service_bus_topic_trigger( data_type: Optional[Union[DataType, str]] = None, access_rights: Optional[Union[AccessRights, str]] = None, is_sessions_enabled: Optional[bool] = None, - cardinality: Optional[Union[Cardinality, str]] = None) -> Callable: - """The service_bus_topic_trigger decorator adds + cardinality: Optional[Union[Cardinality, str]] = None, + **kwargs) -> Callable: + """The on_service_bus_topic_change decorator adds :class:`ServiceBusTopicTrigger` to the :class:`FunctionBuilder` object for building :class:`Function` object used in worker function indexing model. This is equivalent to defining ServiceBusTopicTrigger @@ -569,7 +592,8 @@ def decorator(): AccessRights), is_sessions_enabled=is_sessions_enabled, cardinality=parse_singular_param_to_enum(cardinality, - Cardinality))) + Cardinality), + **kwargs)) return fb return decorator() @@ -584,7 +608,9 @@ def write_service_bus_topic(self, data_type: Optional[ Union[DataType, str]] = None, access_rights: Optional[Union[ - AccessRights, str]] = None) -> Callable: + AccessRights, str]] = None, + **kwargs) -> \ + Callable: """The write_service_bus_topic decorator adds :class:`ServiceBusTopicOutput` to the :class:`FunctionBuilder` object for building :class:`Function` object used in worker function @@ -621,7 +647,8 @@ def decorator(): DataType), access_rights=parse_singular_param_to_enum( access_rights, - AccessRights))) + AccessRights), + **kwargs)) return fb return decorator() @@ -632,7 +659,8 @@ def queue_trigger(self, arg_name: str, queue_name: str, connection: str, - data_type: Optional[DataType] = None) -> Callable: + data_type: Optional[DataType] = None, + **kwargs) -> Callable: """The queue_trigger decorator adds :class:`QueueTrigger` to the :class:`FunctionBuilder` object for building :class:`Function` object used in worker function @@ -663,7 +691,8 @@ def decorator(): queue_name=queue_name, connection=connection, data_type=parse_singular_param_to_enum(data_type, - DataType))) + DataType), + **kwargs)) return fb return decorator() @@ -674,7 +703,8 @@ def write_queue(self, arg_name: str, queue_name: str, connection: str, - data_type: Optional[DataType] = None) -> Callable: + data_type: Optional[DataType] = None, + **kwargs) -> Callable: """The write_queue decorator adds :class:`QueueOutput` to the :class:`FunctionBuilder` object for building :class:`Function` object used in worker function @@ -704,7 +734,8 @@ def decorator(): queue_name=queue_name, connection=connection, data_type=parse_singular_param_to_enum( - data_type, DataType))) + data_type, DataType), + **kwargs)) return fb return decorator() @@ -720,7 +751,8 @@ def event_hub_message_trigger(self, cardinality: Optional[ Union[Cardinality, str]] = None, consumer_group: Optional[ - str] = None) -> Callable: + str] = None, + **kwargs) -> Callable: """The event_hub_message_trigger decorator adds :class:`EventHubTrigger` to the :class:`FunctionBuilder` object @@ -758,7 +790,8 @@ def decorator(): DataType), cardinality=parse_singular_param_to_enum(cardinality, Cardinality), - consumer_group=consumer_group)) + consumer_group=consumer_group, + **kwargs)) return fb return decorator() @@ -770,7 +803,9 @@ def write_event_hub_message(self, connection: str, event_hub_name: str, data_type: Optional[ - Union[DataType, str]] = None) -> Callable: + Union[DataType, str]] = None, + **kwargs) -> \ + Callable: """The write_event_hub_message decorator adds :class:`EventHubOutput` to the :class:`FunctionBuilder` object for building :class:`Function` object used in worker function @@ -801,7 +836,8 @@ def decorator(): connection=connection, event_hub_name=event_hub_name, data_type=parse_singular_param_to_enum(data_type, - DataType))) + DataType), + **kwargs)) return fb return decorator() @@ -831,7 +867,8 @@ def cosmos_db_trigger(self, start_from_beginning: Optional[bool] = None, preferred_locations: Optional[str] = None, data_type: Optional[ - Union[DataType, str]] = None) -> \ + Union[DataType, str]] = None, + **kwargs) -> \ Callable: """The cosmos_db_trigger decorator adds :class:`CosmosDBTrigger` to the :class:`FunctionBuilder` object @@ -916,7 +953,8 @@ def cosmos_db_trigger(self, max_items_per_invocation=max_items_per_invocation, start_from_beginning=start_from_beginning, preferred_locations=preferred_locations, - data_type=parse_singular_param_to_enum(data_type, DataType)) + data_type=parse_singular_param_to_enum(data_type, DataType), + **kwargs) @self._configure_function_builder def wrap(fb): @@ -940,7 +978,8 @@ def write_cosmos_db_documents(self, bool] = None, preferred_locations: Optional[str] = None, data_type: Optional[ - Union[DataType, str]] = None) \ + Union[DataType, str]] = None, + **kwargs) \ -> Callable: """The write_cosmos_db_documents decorator adds :class:`CosmosDBOutput` to the :class:`FunctionBuilder` object @@ -992,7 +1031,8 @@ def decorator(): =use_multiple_write_locations, preferred_locations=preferred_locations, data_type=parse_singular_param_to_enum(data_type, - DataType))) + DataType), + **kwargs)) return fb return decorator() @@ -1008,7 +1048,8 @@ def read_cosmos_db_documents(self, sql_query: Optional[str] = None, partition_key: Optional[str] = None, data_type: Optional[ - Union[DataType, str]] = None) \ + Union[DataType, str]] = None, + **kwargs) \ -> Callable: """The read_cosmos_db_documents decorator adds :class:`CosmosDBInput` to the :class:`FunctionBuilder` object @@ -1050,7 +1091,8 @@ def decorator(): sql_query=sql_query, partition_key=partition_key, data_type=parse_singular_param_to_enum(data_type, - DataType))) + DataType), + **kwargs)) return fb return decorator() @@ -1061,7 +1103,8 @@ def blob_trigger(self, arg_name: str, path: str, connection: str, - data_type: Optional[DataType] = None) -> Callable: + data_type: Optional[DataType] = None, + **kwargs) -> Callable: """ The blob_change_trigger decorator adds :class:`BlobTrigger` to the :class:`FunctionBuilder` object @@ -1093,7 +1136,8 @@ def decorator(): path=path, connection=connection, data_type=parse_singular_param_to_enum(data_type, - DataType))) + DataType), + **kwargs)) return fb return decorator() @@ -1104,7 +1148,8 @@ def read_blob(self, arg_name: str, path: str, connection: str, - data_type: Optional[DataType] = None) -> Callable: + data_type: Optional[DataType] = None, + **kwargs) -> Callable: """ The read_blob decorator adds :class:`BlobInput` to the @@ -1137,7 +1182,8 @@ def decorator(): path=path, connection=connection, data_type=parse_singular_param_to_enum(data_type, - DataType))) + DataType), + **kwargs)) return fb return decorator() @@ -1148,7 +1194,8 @@ def write_blob(self, arg_name: str, path: str, connection: str, - data_type: Optional[DataType] = None) -> Callable: + data_type: Optional[DataType] = None, + **kwargs) -> Callable: """ The write_blob decorator adds :class:`BlobOutput` to the @@ -1181,7 +1228,146 @@ def decorator(): path=path, connection=connection, data_type=parse_singular_param_to_enum(data_type, - DataType))) + DataType), + **kwargs)) + return fb + + return decorator() + + return wrap + + def custom_input_binding(self, + arg_name: str, + type: str, + data_type: Optional[Union[DataType, str]] = None, + **kwargs + ) -> Callable: + """ + The custom_input_binding decorator adds :class:`CustomInputBinding` + to the :class:`FunctionBuilder` object for building :class:`Function` + object used in worker function indexing model. + This is equivalent to defining a custom input binding in the + function.json which enables function to read data from a + custom defined input source. + All optional fields will be given default value by function host when + they are parsed by function host. + + Ref: https://aka.ms/azure-function-binding-custom + + :param arg_name: The name of input parameter in the function code. + :param type: The type of binding. + :param data_type: Defines how Functions runtime should treat the + parameter value. + :param kwargs: Keyword arguments for specifying additional binding + fields to include in the binding json. + + :return: Decorator function. + """ + + @self._configure_function_builder + def wrap(fb): + def decorator(): + fb.add_binding( + binding=CustomInputBinding( + name=arg_name, + type=type, + data_type=parse_singular_param_to_enum(data_type, + DataType), + **kwargs)) + return fb + + return decorator() + + return wrap + + def custom_output_binding(self, + arg_name: str, + type: str, + data_type: Optional[Union[DataType, str]] = None, + **kwargs + ) -> Callable: + """ + The custom_output_binding decorator adds :class:`CustomOutputBinding` + to the :class:`FunctionBuilder` object for building :class:`Function` + object used in worker function indexing model. + This is equivalent to defining a custom output binding in the + function.json which enables function to write data from a + custom defined output source. + All optional fields will be given default value by function host when + they are parsed by function host. + + Ref: https://aka.ms/azure-function-binding-custom + + :param arg_name: The name of output parameter in the function code. + :param type: The type of binding. + :param data_type: Defines how Functions runtime should treat the + parameter value. + :param kwargs: Keyword arguments for specifying additional binding + fields to include in the binding json. + + :return: Decorator function. + """ + + @self._configure_function_builder + def wrap(fb): + def decorator(): + fb.add_binding( + binding=CustomOutputBinding( + name=arg_name, + type=type, + data_type=parse_singular_param_to_enum(data_type, + DataType), + **kwargs)) + return fb + + return decorator() + + return wrap + + def custom_trigger(self, + arg_name: str, + type: str, + data_type: Optional[Union[DataType, str]] = None, + **kwargs + ) -> Callable: + """ + The custom_trigger decorator adds :class:`CustomTrigger` + to the :class:`FunctionBuilder` object for building :class:`Function` + object used in worker function indexing model. + This is equivalent to defining a custom trigger in the + function.json which triggers function to execute when custom trigger + events are received by host. + All optional fields will be given default value by function host when + they are parsed by function host. + + Ref: https://aka.ms/azure-function-binding-custom + + :param arg_name: The name of trigger parameter in the function code. + :param type: The type of binding. + :param data_type: Defines how Functions runtime should treat the + parameter value. + :param kwargs: Keyword arguments for specifying additional binding + fields to include in the binding json. + + :return: Decorator function. + """ + + @self._configure_function_builder + def wrap(fb): + def decorator(): + nonlocal kwargs + if type == HTTP_TRIGGER: + if kwargs.get('auth_level', None) is None: + kwargs['auth_level'] = self.auth_level + if 'route' not in kwargs: + kwargs['route'] = None + fb.add_trigger( + trigger=CustomTrigger( + name=arg_name, + type=type, + data_type=parse_singular_param_to_enum(data_type, + DataType), + **kwargs)) return fb return decorator() diff --git a/azure/functions/decorators/http.py b/azure/functions/decorators/http.py index 8e9788b1..3112efc6 100644 --- a/azure/functions/decorators/http.py +++ b/azure/functions/decorators/http.py @@ -28,7 +28,8 @@ def __init__(self, methods: Optional[Iterable[HttpMethod]] = None, data_type: Optional[DataType] = None, auth_level: Optional[AuthLevel] = None, - route: Optional[str] = None) -> None: + route: Optional[str] = None, + **kwargs) -> None: self.auth_level = auth_level self.methods = methods self.route = route @@ -42,5 +43,6 @@ def get_binding_name() -> str: def __init__(self, name: str, - data_type: Optional[DataType] = None) -> None: + data_type: Optional[DataType] = None, + **kwargs) -> None: super().__init__(name=name, data_type=data_type) diff --git a/azure/functions/decorators/queue.py b/azure/functions/decorators/queue.py index 23657d4c..2129f0a9 100644 --- a/azure/functions/decorators/queue.py +++ b/azure/functions/decorators/queue.py @@ -15,7 +15,8 @@ def __init__(self, name: str, queue_name: str, connection: str, - data_type: Optional[DataType] = None): + data_type: Optional[DataType] = None, + **kwargs): self.queue_name = queue_name self.connection = connection super().__init__(name=name, data_type=data_type) @@ -30,7 +31,8 @@ def __init__(self, name: str, queue_name: str, connection: str, - data_type: Optional[DataType] = None): + data_type: Optional[DataType] = None, + **kwargs): self.queue_name = queue_name self.connection = connection super().__init__(name=name, data_type=data_type) diff --git a/azure/functions/decorators/servicebus.py b/azure/functions/decorators/servicebus.py index 122f95ef..05d573b2 100644 --- a/azure/functions/decorators/servicebus.py +++ b/azure/functions/decorators/servicebus.py @@ -20,7 +20,8 @@ def __init__(self, data_type: Optional[DataType] = None, access_rights: Optional[AccessRights] = None, is_sessions_enabled: Optional[bool] = None, - cardinality: Optional[Cardinality] = None): + cardinality: Optional[Cardinality] = None, + **kwargs): self.connection = connection self.queue_name = queue_name self.access_rights = access_rights @@ -39,7 +40,8 @@ def __init__(self, connection: str, queue_name: str, data_type: Optional[DataType] = None, - access_rights: Optional[AccessRights] = None): + access_rights: Optional[AccessRights] = None, + **kwargs): self.connection = connection self.queue_name = queue_name self.access_rights = access_rights @@ -59,7 +61,8 @@ def __init__(self, data_type: Optional[DataType] = None, access_rights: Optional[AccessRights] = None, is_sessions_enabled: Optional[bool] = None, - cardinality: Optional[Cardinality] = None): + cardinality: Optional[Cardinality] = None, + **kwargs): self.connection = connection self.topic_name = topic_name self.subscription_name = subscription_name @@ -80,7 +83,8 @@ def __init__(self, topic_name: str, subscription_name: Optional[str] = None, data_type: Optional[DataType] = None, - access_rights: Optional[AccessRights] = None): + access_rights: Optional[AccessRights] = None, + **kwargs): self.connection = connection self.topic_name = topic_name self.subscription_name = subscription_name diff --git a/azure/functions/decorators/timer.py b/azure/functions/decorators/timer.py index 1a9d6d31..2d1967b2 100644 --- a/azure/functions/decorators/timer.py +++ b/azure/functions/decorators/timer.py @@ -16,7 +16,8 @@ def __init__(self, schedule: str, run_on_startup: Optional[bool] = None, use_monitor: Optional[bool] = None, - data_type: Optional[DataType] = None) -> None: + data_type: Optional[DataType] = None, + **kwargs) -> None: self.schedule = schedule self.run_on_startup = run_on_startup self.use_monitor = use_monitor diff --git a/azure/functions/decorators/utils.py b/azure/functions/decorators/utils.py index b875d0ed..4d7bcbdf 100644 --- a/azure/functions/decorators/utils.py +++ b/azure/functions/decorators/utils.py @@ -47,16 +47,23 @@ def wrapper(*args, **kw): @staticmethod def add_to_dict(func: Callable): - def wrapper(*args, **kw): + def wrapper(*args, **kwargs): if args is None or len(args) == 0: raise ValueError( f'{func.__name__} has no args. Please ensure func is an ' f'object method.') - func(*args, **kw) + func(*args, **kwargs) + + self = args[0] + + init_params = set(inspect.signature(func).parameters.keys()) + init_params.update(kwargs.keys()) + for key in kwargs.keys(): + if not hasattr(self, key): + setattr(self, key, kwargs[key]) - setattr(args[0], 'init_params', - list(inspect.signature(func).parameters.keys())) + setattr(self, 'init_params', init_params) return wrapper @@ -90,7 +97,7 @@ def parse_singular_param_to_enum(param: Optional[Union[T, str]], return None if isinstance(param, str): try: - return class_name[param] + return class_name[param.upper()] except KeyError: raise KeyError( f"Can not parse str '{param}' to {class_name.__name__}. " @@ -106,8 +113,8 @@ def parse_iterable_param_to_enums( return None try: - return [class_name[value] if isinstance(value, str) else value for - value in param_values] + return [class_name[value.upper()] if isinstance(value, str) else value + for value in param_values] except KeyError: raise KeyError( f"Can not parse '{param_values}' to " diff --git a/docs/ProgModelSpec.pyi b/docs/ProgModelSpec.pyi index 54b6db74..9918f673 100644 --- a/docs/ProgModelSpec.pyi +++ b/docs/ProgModelSpec.pyi @@ -61,7 +61,10 @@ class FunctionApp: binding_arg_name: str = '$return', methods: Optional[ Union[Iterable[str], Iterable[HttpMethod]]] = None, - auth_level: Optional[Union[AuthLevel, str]] = None) -> Callable: + auth_level: Optional[Union[AuthLevel, str]] = None, + trigger_extra_fields: typing.Dict = {}, + binding_extra_fields: typing.Dict = {} + ) -> Callable: """The route decorator adds :class:`HttpTrigger` and :class:`HttpOutput` binding to the :class:`FunctionBuilder` object for building :class:`Function` object used in worker function @@ -88,6 +91,10 @@ class FunctionApp: :param auth_level: Determines what keys, if any, need to be present on the request in order to invoke the function. :return: Decorator function. + :param binding_extra_fields: Keyword arguments for specifying + additional binding fields to include in the binding json. + :param trigger_extra_fields: Keyword arguments for specifying + additional binding fields to include in the trigger json. """ pass @@ -96,7 +103,8 @@ class FunctionApp: schedule: str, run_on_startup: Optional[bool] = None, use_monitor: Optional[bool] = None, - data_type: Optional[Union[DataType, str]] = None) -> Callable: + data_type: Optional[Union[DataType, str]] = None, + **kwargs) -> Callable: """The schedule decorator adds :class:`TimerTrigger` to the :class:`FunctionBuilder` object for building :class:`Function` object used in worker function @@ -118,6 +126,9 @@ class FunctionApp: schedule should be monitored. :param data_type: Defines how Functions runtime should treat the parameter value. + :param kwargs: Keyword arguments for specifying additional binding + fields to include in the binding json. + :return: Decorator function. """ pass @@ -130,7 +141,8 @@ class FunctionApp: data_type: Optional[Union[DataType, str]] = None, access_rights: Optional[Union[AccessRights, str]] = None, is_sessions_enabled: Optional[bool] = None, - cardinality: Optional[Union[Cardinality, str]] = None) -> Callable: + cardinality: Optional[Union[Cardinality, str]] = None, + **kwargs) -> Callable: """The service_bus_queue_trigger decorator adds :class:`ServiceBusQueueTrigger` to the :class:`FunctionBuilder` object for building :class:`Function` object used in worker function @@ -153,6 +165,9 @@ class FunctionApp: :param is_sessions_enabled: True if connecting to a session-aware queue or subscription. :param cardinality: Set to many in order to enable batching. + :param kwargs: Keyword arguments for specifying additional binding + fields to include in the binding json. + :return: Decorator function. """ pass @@ -164,7 +179,8 @@ class FunctionApp: data_type: Optional[ Union[DataType, str]] = None, access_rights: Optional[Union[ - AccessRights, str]] = None) -> Callable: + AccessRights, str]] = None, + **kwargs) -> Callable: """The write_service_bus_queue decorator adds :class:`ServiceBusQueueOutput` to the :class:`FunctionBuilder` object for building :class:`Function` object used in worker function @@ -184,7 +200,11 @@ class FunctionApp: :param data_type: Defines how Functions runtime should treat the parameter value. :param access_rights: Access rights for the connection string. + :param kwargs: Keyword arguments for specifying additional binding + fields to include in the binding json. + :return: Decorator function. + """ pass @@ -197,7 +217,8 @@ class FunctionApp: data_type: Optional[Union[DataType, str]] = None, access_rights: Optional[Union[AccessRights, str]] = None, is_sessions_enabled: Optional[bool] = None, - cardinality: Optional[Union[Cardinality, str]] = None) -> Callable: + cardinality: Optional[Union[Cardinality, str]] = None, + **kwargs) -> Callable: """The service_bus_topic_trigger decorator adds :class:`ServiceBusTopicTrigger` to the :class:`FunctionBuilder` object for building :class:`Function` object used in worker function @@ -221,7 +242,11 @@ class FunctionApp: :param is_sessions_enabled: True if connecting to a session-aware queue or subscription. :param cardinality: Set to many in order to enable batching. + :param kwargs: Keyword arguments for specifying additional binding + fields to include in the binding json. + :return: Decorator function. + """ pass @@ -233,7 +258,8 @@ class FunctionApp: data_type: Optional[ Union[DataType, str]] = None, access_rights: Optional[Union[ - AccessRights, str]] = None) -> Callable: + AccessRights, str]] = None, + **kwargs) -> Callable: """The write_service_bus_topic decorator adds :class:`ServiceBusTopicOutput` to the :class:`FunctionBuilder` object for building :class:`Function` object used in worker function @@ -254,7 +280,11 @@ class FunctionApp: :param data_type: Defines how Functions runtime should treat the parameter value, defaults to DataType.UNDEFINED. :param access_rights: Access rights for the connection string. + :param kwargs: Keyword arguments for specifying additional binding + fields to include in the binding json. + :return: Decorator function. + """ pass @@ -262,8 +292,9 @@ class FunctionApp: arg_name: str, queue_name: str, connection: str, - data_type: Optional[DataType] = None) -> Callable: - """The queue_trigger decorator adds :class:`QueueTrigger` to the + data_type: Optional[DataType] = None, + **kwargs) -> Callable: + """The on_queue_change decorator adds :class:`QueueTrigger` to the :class:`FunctionBuilder` object for building :class:`Function` object used in worker function indexing model. This is equivalent to defining QueueTrigger @@ -281,7 +312,11 @@ class FunctionApp: that specifies how to connect to Azure Queues. :param data_type: Defines how Functions runtime should treat the parameter value. + :param kwargs: Keyword arguments for specifying additional binding + fields to include in the binding json. + :return: Decorator function. + """ pass @@ -289,7 +324,8 @@ class FunctionApp: arg_name: str, queue_name: str, connection: str, - data_type: Optional[DataType] = None) -> Callable: + data_type: Optional[DataType] = None, + **kwargs) -> Callable: """The write_queue decorator adds :class:`QueueOutput` to the :class:`FunctionBuilder` object for building :class:`Function` object used in worker function @@ -307,7 +343,11 @@ class FunctionApp: :param connection: The name of an app setting or setting collection that specifies how to connect to Azure Queues. :param data_type: Set to many in order to enable batching. + :param kwargs: Keyword arguments for specifying additional binding + fields to include in the binding json. + :return: Decorator function. + """ pass @@ -318,8 +358,9 @@ class FunctionApp: data_type: Optional[Union[DataType, str]] = None, cardinality: Optional[ Union[Cardinality, str]] = None, - consumer_group: Optional[str] = None) -> Callable: - """The event_hub_message_trigger decorator adds :class:`EventHubTrigger` + consumer_group: Optional[str] = None, + **kwargs) -> Callable: + """The on_event_hub_message decorator adds :class:`EventHubTrigger` to the :class:`FunctionBuilder` object for building :class:`Function` object used in worker function indexing model. This is equivalent to defining EventHubTrigger @@ -340,7 +381,11 @@ class FunctionApp: :param cardinality: Set to many in order to enable batching. :param consumer_group: An optional property that sets the consumer group used to subscribe to events in the hub. + :param kwargs: Keyword arguments for specifying additional binding + fields to include in the binding json. + :return: Decorator function. + """ pass @@ -349,7 +394,8 @@ class FunctionApp: connection: str, event_hub_name: str, data_type: Optional[ - Union[DataType, str]] = None) -> Callable: + Union[DataType, str]] = None, + **kwargs) -> Callable: """The write_event_hub_message decorator adds :class:`EventHubOutput` to the :class:`FunctionBuilder` object for building :class:`Function` object used in worker function @@ -368,7 +414,11 @@ class FunctionApp: :param event_hub_name: The name of the event hub. :param data_type: Defines how Functions runtime should treat the parameter value. + :param kwargs: Keyword arguments for specifying additional binding + fields to include in the binding json. + :return: Decorator function. + """ pass @@ -395,7 +445,8 @@ class FunctionApp: start_from_beginning: Optional[bool] = None, preferred_locations: Optional[str] = None, data_type: Optional[ - Union[DataType, str]] = None) -> \ + Union[DataType, str]] = None, + **kwargs) -> \ Callable: """The cosmos_db_trigger decorator adds :class:`CosmosDBTrigger` to the :class:`FunctionBuilder` object @@ -457,7 +508,11 @@ class FunctionApp: for geo-replicated database accounts in the Azure Cosmos DB service. :param data_type: Defines how Functions runtime should treat the parameter value. + :param kwargs: Keyword arguments for specifying additional binding + fields to include in the binding json. + :return: Decorator function. + """ pass @@ -473,7 +528,8 @@ class FunctionApp: bool] = None, preferred_locations: Optional[str] = None, data_type: Optional[ - Union[DataType, str]] = None) \ + Union[DataType, str]] = None, + **kwargs) \ -> Callable: """The write_cosmos_db_documents decorator adds :class:`CosmosDBOutput` to the :class:`FunctionBuilder` object @@ -506,7 +562,11 @@ class FunctionApp: for geo-replicated database accounts in the Azure Cosmos DB service. :param data_type: Defines how Functions runtime should treat the parameter value. + :param kwargs: Keyword arguments for specifying additional binding + fields to include in the binding json. + :return: Decorator function. + """ pass @@ -519,7 +579,8 @@ class FunctionApp: sql_query: Optional[str] = None, partition_key: Optional[str] = None, data_type: Optional[ - Union[DataType, str]] = None) \ + Union[DataType, str]] = None, + **kwargs) \ -> Callable: """The read_cosmos_db_documents decorator adds :class:`CosmosDBInput` to the :class:`FunctionBuilder` object @@ -545,7 +606,11 @@ class FunctionApp: lookup. :param data_type: Defines how Functions runtime should treat the parameter value. + :param kwargs: Keyword arguments for specifying additional binding + fields to include in the binding json. + :return: Decorator function. + """ pass @@ -553,7 +618,8 @@ class FunctionApp: arg_name: str, path: str, connection: str, - data_type: Optional[DataType] = None) -> Callable: + data_type: Optional[DataType] = None, + **kwargs) -> Callable: """ The blob_change_trigger decorator adds :class:`BlobTrigger` to the :class:`FunctionBuilder` object @@ -573,7 +639,11 @@ class FunctionApp: that specifies how to connect to Azure Blobs. :param data_type: Defines how Functions runtime should treat the parameter value. + :param kwargs: Keyword arguments for specifying additional binding + fields to include in the binding json. + :return: Decorator function. + """ pass @@ -581,7 +651,8 @@ class FunctionApp: arg_name: str, path: str, connection: str, - data_type: Optional[DataType] = None) -> Callable: + data_type: Optional[DataType] = None, + **kwargs) -> Callable: """ The read_blob decorator adds :class:`BlobInput` to the :class:`FunctionBuilder` object @@ -601,7 +672,11 @@ class FunctionApp: that specifies how to connect to Azure Blobs. :param data_type: Defines how Functions runtime should treat the parameter value. + :param kwargs: Keyword arguments for specifying additional binding + fields to include in the binding json. + :return: Decorator function. + """ pass @@ -609,7 +684,8 @@ class FunctionApp: arg_name: str, path: str, connection: str, - data_type: Optional[DataType] = None) -> Callable: + data_type: Optional[DataType] = None, + **kwargs) -> Callable: """ The write_blob decorator adds :class:`BlobOutput` to the :class:`FunctionBuilder` object @@ -629,7 +705,98 @@ class FunctionApp: that specifies how to connect to Azure Blobs. :param data_type: Defines how Functions runtime should treat the parameter value. + :param kwargs: Keyword arguments for specifying additional binding + fields to include in the binding json. + :return: Decorator function. + """ pass + + def custom_input_binding(self, + arg_name: str, + type: str, + data_type: Optional[Union[DataType, str]] = None, + **kwargs + ) -> Callable: + """ + The custom_input_binding decorator adds :class:`CustomInputBinding` + to the :class:`FunctionBuilder` object for building :class:`Function` + object used in worker function indexing model. + This is equivalent to defining a custom input binding in the + function.json which enables function to read data from a + custom defined input source. + All optional fields will be given default value by function host when + they are parsed by function host. + + Ref: https://aka.ms/azure-function-binding-custom + + :param arg_name: The name of input parameter in the function code. + :param type: The type of binding. + :param data_type: Defines how Functions runtime should treat the + parameter value. + :param kwargs: Keyword arguments for specifying additional binding + fields to include in the binding json. + + :return: Decorator function. + """ + pass + + def custom_output_binding(self, + arg_name: str, + type: str, + data_type: Optional[Union[DataType, str]] = None, + **kwargs + ) -> Callable: + """ + The custom_output_binding decorator adds :class:`CustomOutputBinding` + to the :class:`FunctionBuilder` object for building :class:`Function` + object used in worker function indexing model. + This is equivalent to defining a custom output binding in the + function.json which enables function to write data from a + custom defined output source. + All optional fields will be given default value by function host when + they are parsed by function host. + + Ref: https://aka.ms/azure-function-binding-custom + + :param arg_name: The name of output parameter in the function code. + :param type: The type of binding. + :param data_type: Defines how Functions runtime should treat the + parameter value. + :param kwargs: Keyword arguments for specifying additional binding + fields to include in the binding json. + + :return: Decorator function. + """ + pass + + def custom_trigger(self, + arg_name: str, + type: str, + data_type: Optional[Union[DataType, str]] = None, + **kwargs + ) -> Callable: + """ + The custom_trigger decorator adds :class:`CustomTrigger` + to the :class:`FunctionBuilder` object for building :class:`Function` + object used in worker function indexing model. + This is equivalent to defining a custom trigger in the + function.json which triggers function to execute when custom trigger + events are received by host. + All optional fields will be given default value by function host when + they are parsed by function host. + + Ref: https://aka.ms/azure-function-binding-custom + + :param arg_name: The name of trigger parameter in the function code. + :param type: The type of binding. + :param data_type: Defines how Functions runtime should treat the + parameter value. + :param kwargs: Keyword arguments for specifying additional binding + fields to include in the binding json. + + :return: Decorator function. + """ + pass diff --git a/tests/decorators/test_blob.py b/tests/decorators/test_blob.py index 2682936e..9d6154e6 100644 --- a/tests/decorators/test_blob.py +++ b/tests/decorators/test_blob.py @@ -11,12 +11,14 @@ def test_blob_trigger_valid_creation(self): trigger = BlobTrigger(name="req", path="dummy_path", connection="dummy_connection", - data_type=DataType.UNDEFINED) + data_type=DataType.UNDEFINED, + dummy_field="dummy") self.assertEqual(trigger.get_binding_name(), "blobTrigger") self.assertEqual(trigger.get_dict_repr(), { "type": "blobTrigger", "direction": BindingDirection.IN, + 'dummyField': 'dummy', "name": "req", "dataType": DataType.UNDEFINED, "path": "dummy_path", @@ -27,12 +29,14 @@ def test_blob_input_valid_creation(self): blob_input = BlobInput(name="res", path="dummy_path", connection="dummy_connection", - data_type=DataType.UNDEFINED) + data_type=DataType.UNDEFINED, + dummy_field="dummy") self.assertEqual(blob_input.get_binding_name(), "blob") self.assertEqual(blob_input.get_dict_repr(), { "type": "blob", "direction": BindingDirection.IN, + 'dummyField': 'dummy', "name": "res", "dataType": DataType.UNDEFINED, "path": "dummy_path", @@ -43,12 +47,14 @@ def test_blob_output_valid_creation(self): blob_output = BlobOutput(name="res", path="dummy_path", connection="dummy_connection", - data_type=DataType.UNDEFINED) + data_type=DataType.UNDEFINED, + dummy_field="dummy") self.assertEqual(blob_output.get_binding_name(), "blob") self.assertEqual(blob_output.get_dict_repr(), { "type": "blob", "direction": BindingDirection.OUT, + 'dummyField': 'dummy', "name": "res", "dataType": DataType.UNDEFINED, "path": "dummy_path", diff --git a/tests/decorators/test_core.py b/tests/decorators/test_core.py index d2475a3b..76eedfde 100644 --- a/tests/decorators/test_core.py +++ b/tests/decorators/test_core.py @@ -14,7 +14,8 @@ def get_binding_name() -> str: def __init__(self, name: str, - data_type: DataType = DataType.UNDEFINED): + data_type: DataType = DataType.UNDEFINED, + **kwargs): super().__init__(name=name, data_type=data_type) @@ -25,7 +26,8 @@ def get_binding_name() -> str: def __init__(self, name: str, - data_type: DataType = DataType.UNDEFINED): + data_type: DataType = DataType.UNDEFINED, + **kwargs): super().__init__(name=name, data_type=data_type) @@ -36,17 +38,25 @@ def get_binding_name() -> str: def __init__(self, name: str, - data_type: DataType = DataType.UNDEFINED): + data_type: DataType = DataType.UNDEFINED, + **kwargs): super().__init__(name=name, data_type=data_type) class TestBindings(unittest.TestCase): def test_trigger_creation(self): - """Testing if the trigger creation sets the correct values by default - """ test_trigger = DummyTrigger(name="dummy", data_type=DataType.UNDEFINED) - self.assertTrue(test_trigger.is_trigger) + expected_dict = {'dataType': DataType.UNDEFINED, + 'direction': BindingDirection.IN, + 'name': 'dummy', + 'type': 'Dummy'} + self.assertEqual(test_trigger.get_binding_name(), "Dummy") + self.assertEqual(test_trigger.get_dict_repr(), expected_dict) + + def test_param_direction_unset(self): + test_trigger = DummyTrigger(name="dummy", data_type=DataType.UNDEFINED, + direction="dummy", type="hello") expected_dict = {'dataType': DataType.UNDEFINED, 'direction': BindingDirection.IN, @@ -56,8 +66,6 @@ def test_trigger_creation(self): self.assertEqual(test_trigger.get_dict_repr(), expected_dict) def test_input_creation(self): - """Testing if the input creation sets the correct values by default - """ test_input = DummyInputBinding(name="dummy", data_type=DataType.UNDEFINED) @@ -67,12 +75,9 @@ def test_input_creation(self): 'type': 'DummyInputBinding'} self.assertEqual(test_input.get_binding_name(), "DummyInputBinding") - self.assertFalse(test_input.is_trigger) self.assertEqual(test_input.get_dict_repr(), expected_dict) def test_output_creation(self): - """Testing if the output creation sets the correct values by default - """ test_output = DummyOutputBinding(name="dummy", data_type=DataType.UNDEFINED) @@ -82,5 +87,4 @@ def test_output_creation(self): 'type': 'DummyOutputBinding'} self.assertEqual(test_output.get_binding_name(), "DummyOutputBinding") - self.assertFalse(test_output.is_trigger) self.assertEqual(test_output.get_dict_repr(), expected_dict) diff --git a/tests/decorators/test_cosmosdb.py b/tests/decorators/test_cosmosdb.py index b45fab30..2cd524a0 100644 --- a/tests/decorators/test_cosmosdb.py +++ b/tests/decorators/test_cosmosdb.py @@ -28,22 +28,20 @@ def test_cosmos_db_trigger_valid_creation(self): start_from_beginning=False, create_lease_collection_if_not_exists=False, preferred_locations="dummy_loc", - data_type=DataType.UNDEFINED) + data_type=DataType.UNDEFINED, + dummy_field="dummy") self.assertEqual(trigger.get_binding_name(), "cosmosDBTrigger") self.assertEqual(trigger.get_dict_repr(), {"checkpointDocumentCount": 3, "checkpointInterval": 2, - "collectionName": - "dummy_collection", - "connectionStringSetting": - "dummy_str", - "createLeaseCollection" - "IfNotExists": - False, + "collectionName": "dummy_collection", + "connectionStringSetting": "dummy_str", + "createLeaseCollectionIfNotExists": False, "dataType": DataType.UNDEFINED, "databaseName": "dummy_db", "direction": BindingDirection.IN, + 'dummyField': 'dummy', "feedPollDelay": 4, "leaseAcquireInterval": 6, "leaseCollectionName": 'coll_name', @@ -52,12 +50,10 @@ def test_cosmos_db_trigger_valid_creation(self): "leaseDatabaseName": 'db', "leaseExpirationInterval": 7, "leaseRenewInterval": 5, - "leasesCollectionThroughput": - 1, + "leasesCollectionThroughput": 1, "maxItemsPerInvocation": 8, "name": "req", - "preferredLocations": - "dummy_loc", + "preferredLocations": "dummy_loc", "startFromBeginning": False, "type": COSMOS_DB_TRIGGER}) @@ -71,7 +67,8 @@ def test_cosmos_db_output_valid_creation(self): use_multiple_write_locations=False, data_type=DataType.UNDEFINED, partition_key='key', - preferred_locations='locs') + preferred_locations='locs', + dummy_field="dummy") self.assertEqual(output.get_binding_name(), "cosmosDB") self.assertEqual(output.get_dict_repr(), @@ -82,6 +79,7 @@ def test_cosmos_db_output_valid_creation(self): 'dataType': DataType.UNDEFINED, 'databaseName': 'dummy_db', 'direction': BindingDirection.OUT, + 'dummyField': 'dummy', 'name': 'req', 'partitionKey': 'key', 'preferredLocations': 'locs', @@ -95,7 +93,8 @@ def test_cosmos_db_input_valid_creation(self): id="dummy_id", sql_query="dummy_query", partition_key="dummy_partitions", - data_type=DataType.UNDEFINED) + data_type=DataType.UNDEFINED, + dummy_field="dummy") self.assertEqual(cosmosdb_input.get_binding_name(), "cosmosDB") self.assertEqual(cosmosdb_input.get_dict_repr(), {'collectionName': 'dummy_collection', @@ -103,6 +102,7 @@ def test_cosmos_db_input_valid_creation(self): 'dataType': DataType.UNDEFINED, 'databaseName': 'dummy_db', 'direction': BindingDirection.IN, + 'dummyField': 'dummy', 'id': 'dummy_id', 'name': 'req', 'partitionKey': 'dummy_partitions', diff --git a/tests/decorators/test_custom.py b/tests/decorators/test_custom.py new file mode 100644 index 00000000..249b10f5 --- /dev/null +++ b/tests/decorators/test_custom.py @@ -0,0 +1,69 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import unittest + +from azure.functions.decorators.constants import HTTP_TRIGGER, COSMOS_DB, BLOB +from azure.functions.decorators.core import BindingDirection, AuthLevel, \ + DataType +from azure.functions.decorators.custom import CustomInputBinding, \ + CustomTrigger, CustomOutputBinding + + +class TestCustom(unittest.TestCase): + def test_custom_trigger_valid_creation(self): + trigger = CustomTrigger(name="req", + type=HTTP_TRIGGER, + data_type=DataType.UNDEFINED, + auth_level=AuthLevel.ANONYMOUS, + methods=["GET", "POST"], + route="dummy") + + self.assertEqual(trigger.get_binding_name(), None) + self.assertEqual(trigger.type, HTTP_TRIGGER) + self.assertEqual(trigger.get_dict_repr(), { + "authLevel": AuthLevel.ANONYMOUS, + "type": HTTP_TRIGGER, + "direction": BindingDirection.IN, + "name": 'req', + "dataType": DataType.UNDEFINED, + "route": 'dummy', + "methods": ["GET", "POST"] + }) + + def test_custom_input_valid_creation(self): + cosmosdb_input = CustomInputBinding( + name="inDocs", + type=COSMOS_DB, + database_name="dummy_db", + collection_name="dummy_collection", + connection_string_setting="dummy_str", + id='dummy_id', + partitionKey='dummy_partitions', + sqlQuery='dummy_query') + self.assertEqual(cosmosdb_input.get_binding_name(), None) + self.assertEqual(cosmosdb_input.get_dict_repr(), + {'collectionName': 'dummy_collection', + 'connectionStringSetting': 'dummy_str', + 'databaseName': 'dummy_db', + 'direction': BindingDirection.IN, + 'id': 'dummy_id', + 'name': 'inDocs', + 'partitionKey': 'dummy_partitions', + 'sqlQuery': 'dummy_query', + 'type': COSMOS_DB}) + + def test_custom_output_valid_creation(self): + blob_output = CustomOutputBinding(name="res", type=BLOB, + data_type=DataType.UNDEFINED, + path="dummy_path", + connection="dummy_connection") + + self.assertEqual(blob_output.get_binding_name(), None) + self.assertEqual(blob_output.get_dict_repr(), { + "type": BLOB, + "direction": BindingDirection.OUT, + "name": "res", + "dataType": DataType.UNDEFINED, + "path": "dummy_path", + "connection": "dummy_connection" + }) diff --git a/tests/decorators/test_decorators.py b/tests/decorators/test_decorators.py index d349c973..e7121b64 100644 --- a/tests/decorators/test_decorators.py +++ b/tests/decorators/test_decorators.py @@ -91,7 +91,7 @@ def test_timer_trigger_full_args(self): @app.schedule(arg_name="req", schedule="dummy_schedule", run_on_startup=False, use_monitor=False, - data_type=DataType.STRING) + data_type=DataType.STRING, dummy_field='dummy') def dummy(): pass @@ -104,6 +104,7 @@ def dummy(): "type": TIMER_TRIGGER, "dataType": DataType.STRING, "direction": BindingDirection.IN, + 'dummyField': 'dummy', "schedule": "dummy_schedule", "runOnStartup": False, "useMonitor": False @@ -142,7 +143,9 @@ def test_route_with_all_args(self): @app.route(trigger_arg_name='trigger_name', binding_arg_name='out', methods=(HttpMethod.GET, HttpMethod.PATCH), - auth_level=AuthLevel.FUNCTION, route='dummy_route') + auth_level=AuthLevel.FUNCTION, route='dummy_route', + trigger_extra_fields={"dummy_field": "dummy"}, + binding_extra_fields={"dummy_field": "dummy"}) def dummy(): pass @@ -152,6 +155,7 @@ def dummy(): "bindings": [ { "direction": BindingDirection.IN, + 'dummyField': 'dummy', "type": HTTP_TRIGGER, "name": "trigger_name", "authLevel": AuthLevel.FUNCTION, @@ -162,6 +166,7 @@ def dummy(): }, { "direction": BindingDirection.OUT, + 'dummyField': 'dummy', "type": HTTP_OUTPUT, "name": "out", } @@ -202,10 +207,10 @@ def test_queue_full_args(self): @app.queue_trigger(arg_name="req", queue_name="dummy_queue", connection="dummy_conn", - data_type=DataType.STRING) + data_type=DataType.STRING, dummy_field="dummy") @app.write_queue(arg_name="out", queue_name="dummy_out_queue", connection="dummy_out_conn", - data_type=DataType.STRING) + data_type=DataType.STRING, dummy_field="dummy") def dummy(): pass @@ -215,6 +220,7 @@ def dummy(): "bindings": [ { "direction": BindingDirection.OUT, + 'dummyField': 'dummy', "dataType": DataType.STRING, "type": QUEUE, "name": "out", @@ -223,6 +229,7 @@ def dummy(): }, { "direction": BindingDirection.IN, + 'dummyField': 'dummy', "dataType": DataType.STRING, "type": QUEUE_TRIGGER, "name": "req", @@ -272,12 +279,14 @@ def test_service_bus_queue_full_args(self): data_type=DataType.STREAM, access_rights=AccessRights.MANAGE, is_sessions_enabled=True, - cardinality=Cardinality.MANY) + cardinality=Cardinality.MANY, + dummy_field="dummy") @app.write_service_bus_queue(arg_name='res', connection='dummy_out_conn', queue_name='dummy_out_queue', data_type=DataType.STREAM, - access_rights=AccessRights.MANAGE) + access_rights=AccessRights.MANAGE, + dummy_field="dummy") def dummy(): pass @@ -287,6 +296,7 @@ def dummy(): "bindings": [ { "direction": BindingDirection.OUT, + 'dummyField': 'dummy', "dataType": DataType.STREAM, "type": SERVICE_BUS, "name": "res", @@ -296,6 +306,7 @@ def dummy(): }, { "direction": BindingDirection.IN, + 'dummyField': 'dummy', "dataType": DataType.STREAM, "type": SERVICE_BUS_TRIGGER, "name": "req", @@ -354,12 +365,14 @@ def test_service_bus_topic_full_args(self): data_type=DataType.STRING, access_rights=AccessRights.LISTEN, is_sessions_enabled=False, - cardinality=Cardinality.MANY) + cardinality=Cardinality.MANY, + dummy_field="dummy") @app.write_service_bus_topic(arg_name='res', connection='dummy_conn', topic_name='dummy_topic', subscription_name='dummy_sub', data_type=DataType.STRING, - access_rights=AccessRights.LISTEN) + access_rights=AccessRights.LISTEN, + dummy_field="dummy") def dummy(): pass @@ -370,6 +383,7 @@ def dummy(): { "type": SERVICE_BUS, "direction": BindingDirection.OUT, + 'dummyField': 'dummy', "name": "res", "connection": "dummy_conn", "topicName": "dummy_topic", @@ -380,6 +394,7 @@ def dummy(): { "type": SERVICE_BUS_TRIGGER, "direction": BindingDirection.IN, + 'dummyField': 'dummy', "name": "req", "connection": "dummy_conn", "topicName": "dummy_topic", @@ -433,11 +448,13 @@ def test_event_hub_full_args(self): event_hub_name="dummy_event_hub", cardinality=Cardinality.ONE, consumer_group="dummy_group", - data_type=DataType.UNDEFINED) + data_type=DataType.UNDEFINED, + dummy_field="dummy") @app.write_event_hub_message(arg_name="res", event_hub_name="dummy_event_hub", connection="dummy_connection", - data_type=DataType.UNDEFINED) + data_type=DataType.UNDEFINED, + dummy_field="dummy") def dummy(): pass @@ -448,6 +465,7 @@ def dummy(): "bindings": [ { "direction": BindingDirection.OUT, + 'dummyField': 'dummy', "dataType": DataType.UNDEFINED, "type": EVENT_HUB, "name": "res", @@ -456,6 +474,7 @@ def dummy(): }, { "direction": BindingDirection.IN, + 'dummyField': 'dummy', "dataType": DataType.UNDEFINED, "type": EVENT_HUB_TRIGGER, "name": "req", @@ -490,7 +509,8 @@ def test_cosmosdb_full_args(self): start_from_beginning=False, create_lease_collection_if_not_exists=False, preferred_locations="dummy_loc", - data_type=DataType.STRING) + data_type=DataType.STRING, + dummy_field="dummy") @app.read_cosmos_db_documents(arg_name="in", database_name="dummy_in_db", collection_name="dummy_in_collection", @@ -498,7 +518,8 @@ def test_cosmosdb_full_args(self): id="dummy_id", sql_query="dummy_query", partition_key="dummy_partitions", - data_type=DataType.STRING) + data_type=DataType.STRING, + dummy_field="dummy") @app.write_cosmos_db_documents(arg_name="out", database_name="dummy_out_db", collection_name="dummy_out_collection", @@ -508,7 +529,8 @@ def test_cosmosdb_full_args(self): collection_throughput=1, use_multiple_write_locations=False, preferred_locations="dummy_location", - data_type=DataType.STRING) + data_type=DataType.STRING, + dummy_field="dummy") def dummy(): pass @@ -518,6 +540,7 @@ def dummy(): "bindings": [ { "direction": BindingDirection.OUT, + 'dummyField': 'dummy', "dataType": DataType.STRING, "type": COSMOS_DB, "name": "out", @@ -535,6 +558,7 @@ def dummy(): }, { "direction": BindingDirection.IN, + 'dummyField': 'dummy', "dataType": DataType.STRING, "type": COSMOS_DB, "name": "in", @@ -549,6 +573,7 @@ def dummy(): }, { "direction": BindingDirection.IN, + 'dummyField': 'dummy', "dataType": DataType.STRING, "type": COSMOS_DB_TRIGGER, "name": "trigger", @@ -857,6 +882,8 @@ def dummy(): func = self._get_func(app) + bindings = func.get_bindings() + self.assertEqual(len(bindings), 1) trigger = func.get_bindings()[0] self.assertEqual(trigger.get_dict_repr(), { @@ -882,9 +909,22 @@ def dummy(): func = self._get_func(app) - trigger = func.get_bindings()[0] + bindings = func.get_bindings() + self.assertEqual(len(bindings), 2) + + input_binding = bindings[0] + trigger = bindings[1] self.assertEqual(trigger.get_dict_repr(), { + "direction": BindingDirection.IN, + "dataType": DataType.STRING, + "type": BLOB_TRIGGER, + "name": "req", + "path": "dummy_path", + "connection": "dummy_conn" + }) + + self.assertEqual(input_binding.get_dict_repr(), { "direction": BindingDirection.IN, "dataType": DataType.STRING, "type": BLOB, @@ -907,9 +947,193 @@ def dummy(): func = self._get_func(app) + bindings = func.get_bindings() + self.assertEqual(len(bindings), 2) + + output_binding = bindings[0] + trigger = bindings[1] + + self.assertEqual(trigger.get_dict_repr(), { + "direction": BindingDirection.IN, + "dataType": DataType.STRING, + "type": BLOB_TRIGGER, + "name": "req", + "path": "dummy_path", + "connection": "dummy_conn" + }) + + self.assertEqual(output_binding.get_dict_repr(), { + "direction": BindingDirection.OUT, + "dataType": DataType.STRING, + "type": BLOB, + "name": "out", + "path": "dummy_out_path", + "connection": "dummy_out_conn" + }) + + # mix, asgi/wsgi + + def test_custom_trigger(self): + app = self.func_app + + @app.custom_trigger(arg_name="req", type=BLOB_TRIGGER, + data_type=DataType.BINARY, connection="dummy_conn", + path="dummy_path") + def dummy(): + pass + + func = self._get_func(app) + + bindings = func.get_bindings() + self.assertEqual(len(bindings), 1) + + trigger = bindings[0] + + self.assertEqual(trigger.get_dict_repr(), { + "direction": BindingDirection.IN, + "dataType": DataType.BINARY, + "type": BLOB_TRIGGER, + "name": "req", + "path": "dummy_path", + "connection": "dummy_conn" + }) + + def test_custom_input_binding(self): + app = self.func_app + + @app.custom_trigger(arg_name="req", type=TIMER_TRIGGER, + data_type=DataType.BINARY, + schedule="dummy_schedule") + @app.custom_input_binding(arg_name="file", type=BLOB, + path="dummy_in_path", + connection="dummy_in_conn", + data_type=DataType.STRING) + def dummy(): + pass + + func = self._get_func(app) + + bindings = func.get_bindings() + self.assertEqual(len(bindings), 2) + + input_binding = bindings[0] + trigger = bindings[1] + + self.assertEqual(trigger.get_dict_repr(), { + "direction": BindingDirection.IN, + "dataType": DataType.BINARY, + "type": TIMER_TRIGGER, + "name": "req", + "schedule": "dummy_schedule" + }) + + self.assertEqual(input_binding.get_dict_repr(), { + "direction": BindingDirection.IN, + "dataType": DataType.STRING, + "type": BLOB, + "name": "file", + "path": "dummy_in_path", + "connection": "dummy_in_conn" + }) + + def test_custom_output_binding(self): + app = self.func_app + + @app.custom_trigger(arg_name="req", type=QUEUE_TRIGGER, + queue_name="dummy_queue", + connection="dummy_conn") + @app.custom_output_binding(arg_name="out", type=BLOB, + path="dummy_out_path", + connection="dummy_out_conn", + data_type=DataType.STRING) + def dummy(): + pass + + func = self._get_func(app) + + output_binding = func.get_bindings()[0] + trigger = func.get_bindings()[1] + + self.assertEqual(trigger.get_dict_repr(), { + "direction": BindingDirection.IN, + "type": QUEUE_TRIGGER, + "name": "req", + "queueName": "dummy_queue", + "connection": "dummy_conn" + }) + + self.assertEqual(output_binding.get_dict_repr(), { + "direction": BindingDirection.OUT, + "dataType": DataType.STRING, + "type": BLOB, + "name": "out", + "path": "dummy_out_path", + "connection": "dummy_out_conn" + }) + + def test_custom_http_trigger(self): + app = self.func_app + + @app.custom_trigger(arg_name="req", type=HTTP_TRIGGER) + def dummy(): + pass + + func = self._get_func(app) + trigger = func.get_bindings()[0] self.assertEqual(trigger.get_dict_repr(), { + "direction": BindingDirection.IN, + "type": HTTP_TRIGGER, + "name": "req", + "route": "dummy", + "authLevel": AuthLevel.FUNCTION + }) + + def test_custom_binding_with_excluded_params(self): + app = self.func_app + + @app.custom_trigger(arg_name="req", type=QUEUE_TRIGGER, + direction=BindingDirection.INOUT) + def dummy(): + pass + + func = self._get_func(app) + + trigger = func.get_bindings()[0] + + self.assertEqual(trigger.get_dict_repr(), { + "direction": BindingDirection.IN, + "type": QUEUE_TRIGGER, + "name": "req" + }) + + def test_mixed_custom_and_supported_binding(self): + app = self.func_app + + @app.queue_trigger(arg_name="req", queue_name="dummy_queue", + connection="dummy_conn") + @app.custom_output_binding(arg_name="out", type=BLOB, + path="dummy_out_path", + connection="dummy_out_conn", + data_type=DataType.STRING) + def dummy(): + pass + + func = self._get_func(app) + + output_binding = func.get_bindings()[0] + trigger = func.get_bindings()[1] + + self.assertEqual(trigger.get_dict_repr(), { + "direction": BindingDirection.IN, + "type": QUEUE_TRIGGER, + "name": "req", + "queueName": "dummy_queue", + "connection": "dummy_conn" + }) + + self.assertEqual(output_binding.get_dict_repr(), { "direction": BindingDirection.OUT, "dataType": DataType.STRING, "type": BLOB, diff --git a/tests/decorators/test_eventhub.py b/tests/decorators/test_eventhub.py index 1ab796d7..33e9066d 100644 --- a/tests/decorators/test_eventhub.py +++ b/tests/decorators/test_eventhub.py @@ -15,7 +15,8 @@ def test_event_hub_trigger_valid_creation(self): event_hub_name="dummy_event_hub", cardinality=Cardinality.ONE, consumer_group="dummy_group", - data_type=DataType.UNDEFINED) + data_type=DataType.UNDEFINED, + dummy_field="dummy") self.assertEqual(trigger.get_binding_name(), "eventHubTrigger") self.assertEqual(trigger.get_dict_repr(), @@ -24,6 +25,7 @@ def test_event_hub_trigger_valid_creation(self): "consumerGroup": "dummy_group", "dataType": DataType.UNDEFINED, "direction": BindingDirection.IN, + 'dummyField': 'dummy', "eventHubName": "dummy_event_hub", "name": "req", "type": EVENT_HUB_TRIGGER}) @@ -32,13 +34,15 @@ def test_event_hub_output_valid_creation(self): output = EventHubOutput(name="res", event_hub_name="dummy_event_hub", connection="dummy_connection", - data_type=DataType.UNDEFINED) + data_type=DataType.UNDEFINED, + dummy_field="dummy") self.assertEqual(output.get_binding_name(), "eventHub") self.assertEqual(output.get_dict_repr(), {'connection': 'dummy_connection', 'dataType': DataType.UNDEFINED, 'direction': BindingDirection.OUT, + 'dummyField': 'dummy', 'eventHubName': 'dummy_event_hub', 'name': 'res', 'type': EVENT_HUB}) diff --git a/tests/decorators/test_function_app.py b/tests/decorators/test_function_app.py index ec62509a..103c7268 100644 --- a/tests/decorators/test_function_app.py +++ b/tests/decorators/test_function_app.py @@ -1,5 +1,6 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +import json import unittest from unittest import mock @@ -92,12 +93,23 @@ def test_function_creation_with_binding_and_trigger(self): } ] }) - self.assertEqual(self.func.get_raw_bindings(), [ - '{"direction": "OUT", "dataType": "UNDEFINED", "type": "http", ' - '"name": "out"}', - '{"direction": "IN", "dataType": "UNDEFINED", "type": ' - '"httpTrigger", "name": "req", "methods": ["GET", "POST"], ' - '"authLevel": "ANONYMOUS", "route": "dummy"}']) + + raw_bindings = self.func.get_raw_bindings() + self.assertEqual(len(raw_bindings), 2) + raw_output_binding = raw_bindings[0] + raw_trigger = raw_bindings[1] + + self.assertEqual(json.loads(raw_output_binding), + json.loads( + '{"direction": "OUT", "dataType": "UNDEFINED", ' + '"type": "http", "name": "out"}')) + + self.assertEqual(json.loads(raw_trigger), + json.loads( + '{"direction": "IN", "dataType": "UNDEFINED", ' + '"type": "httpTrigger", "authLevel": ' + '"ANONYMOUS", "route": "dummy", "methods": [' + '"GET", "POST"], "name": "req"}')) class TestFunctionBuilder(unittest.TestCase): @@ -262,15 +274,23 @@ def test_add_http_app(self): func = funcs[0] self.assertEqual(func.get_function_name(), "http_app_func") - self.assertEqual(func.get_raw_bindings(), [ - '{"direction": "IN", "type": ' - '"httpTrigger", "name": ' - '"req", "methods": ["GET", "POST", "DELETE", "HEAD", "PATCH", ' - '"PUT", "OPTIONS"], "authLevel": "FUNCTION", "route": ' - '"/{*route}"}', - '{"direction": "OUT", "type": "http", ' - '"name": ' - '"$return"}']) + + raw_bindings = func.get_raw_bindings() + raw_trigger = raw_bindings[0] + raw_output_binding = raw_bindings[0] + + self.assertEqual(json.loads(raw_trigger), + json.loads( + '{"direction": "IN", "type": "httpTrigger", ' + '"authLevel": "FUNCTION", "route": "/{*route}", ' + '"methods": ["GET", "POST", "DELETE", "HEAD", ' + '"PATCH", "PUT", "OPTIONS"], "name": "req"}')) + self.assertEqual(json.loads(raw_output_binding), json.loads( + '{"direction": "IN", "type": "httpTrigger", "authLevel": ' + '"FUNCTION", "methods": ["GET", "POST", "DELETE", "HEAD", ' + '"PATCH", "PUT", "OPTIONS"], "name": "req", "route": "/{' + '*route}"}')) + self.assertEqual(func.get_bindings_dict(), { "bindings": [ { diff --git a/tests/decorators/test_http.py b/tests/decorators/test_http.py index 9fc62f42..c1edb443 100644 --- a/tests/decorators/test_http.py +++ b/tests/decorators/test_http.py @@ -21,13 +21,15 @@ def test_http_trigger_valid_creation_with_methods(self): methods=[HttpMethod.GET, HttpMethod.POST], data_type=DataType.UNDEFINED, auth_level=AuthLevel.ANONYMOUS, - route='dummy') + route='dummy', + dummy_field="dummy") self.assertEqual(http_trigger.get_binding_name(), HTTP_TRIGGER) self.assertEqual(http_trigger.get_dict_repr(), { "authLevel": AuthLevel.ANONYMOUS, "type": HTTP_TRIGGER, "direction": BindingDirection.IN, + 'dummyField': 'dummy', "name": 'req', "dataType": DataType.UNDEFINED, "route": 'dummy', @@ -35,12 +37,14 @@ def test_http_trigger_valid_creation_with_methods(self): }) def test_http_output_valid_creation(self): - http_output = HttpOutput(name='req', data_type=DataType.UNDEFINED) + http_output = HttpOutput(name='req', data_type=DataType.UNDEFINED, + dummy_field="dummy") self.assertEqual(http_output.get_binding_name(), HTTP_OUTPUT) self.assertEqual(http_output.get_dict_repr(), { "type": HTTP_OUTPUT, "direction": BindingDirection.OUT, + 'dummyField': 'dummy', "name": "req", "dataType": DataType.UNDEFINED, }) diff --git a/tests/decorators/test_queue.py b/tests/decorators/test_queue.py index 1bf15eed..f6a3011d 100644 --- a/tests/decorators/test_queue.py +++ b/tests/decorators/test_queue.py @@ -12,12 +12,14 @@ def test_queue_trigger_valid_creation(self): trigger = QueueTrigger(name="req", queue_name="dummy_queue", connection="dummy_connection", - data_type=DataType.UNDEFINED) + data_type=DataType.UNDEFINED, + dummy_field="dummy") self.assertEqual(trigger.get_binding_name(), "queueTrigger") self.assertEqual(trigger.get_dict_repr(), { "type": QUEUE_TRIGGER, "direction": BindingDirection.IN, + 'dummyField': 'dummy', "name": "req", "dataType": DataType.UNDEFINED, "queueName": "dummy_queue", @@ -28,12 +30,14 @@ def test_queue_output_valid_creation(self): output = QueueOutput(name="res", queue_name="dummy_queue_out", connection="dummy_connection", - data_type=DataType.UNDEFINED) + data_type=DataType.UNDEFINED, + dummy_field="dummy") self.assertEqual(output.get_binding_name(), "queue") self.assertEqual(output.get_dict_repr(), { "type": QUEUE, "direction": BindingDirection.OUT, + 'dummyField': 'dummy', "name": "res", "dataType": DataType.UNDEFINED, "queueName": "dummy_queue_out", diff --git a/tests/decorators/test_servicebus.py b/tests/decorators/test_servicebus.py index 7e7c5ea1..1f088f92 100644 --- a/tests/decorators/test_servicebus.py +++ b/tests/decorators/test_servicebus.py @@ -18,12 +18,14 @@ def test_service_bus_queue_trigger_valid_creation(self): data_type=DataType.UNDEFINED, access_rights=AccessRights.MANAGE, is_sessions_enabled=True, - cardinality=Cardinality.ONE) + cardinality=Cardinality.ONE, + dummy_field="dummy") self.assertEqual(trigger.get_binding_name(), "serviceBusTrigger") self.assertEqual(trigger.get_dict_repr(), { "type": SERVICE_BUS_TRIGGER, "direction": BindingDirection.IN, + 'dummyField': 'dummy', "name": "req", "connection": "dummy_conn", "queueName": "dummy_queue", @@ -39,13 +41,15 @@ def test_service_bus_queue_output_valid_creation(self): connection="dummy_conn", queue_name="dummy_queue", data_type=DataType.UNDEFINED, - access_rights=AccessRights.MANAGE) + access_rights=AccessRights.MANAGE, + dummy_field="dummy") self.assertEqual(service_bus_queue_output.get_binding_name(), "serviceBus") self.assertEqual(service_bus_queue_output.get_dict_repr(), { "type": SERVICE_BUS, "direction": BindingDirection.OUT, + 'dummyField': 'dummy', "name": "res", "dataType": DataType.UNDEFINED, "connection": "dummy_conn", @@ -60,12 +64,14 @@ def test_service_bus_topic_trigger_valid_creation(self): data_type=DataType.UNDEFINED, access_rights=AccessRights.MANAGE, is_sessions_enabled=True, - cardinality=Cardinality.ONE) + cardinality=Cardinality.ONE, + dummy_field="dummy") self.assertEqual(trigger.get_binding_name(), "serviceBusTrigger") self.assertEqual(trigger.get_dict_repr(), { "type": SERVICE_BUS_TRIGGER, "direction": BindingDirection.IN, + 'dummyField': 'dummy', "name": "req", "connection": "dummy_conn", "topicName": "dummy_topic", @@ -81,12 +87,14 @@ def test_service_bus_topic_output_valid_creation(self): topic_name="dummy_topic", subscription_name="dummy_sub", data_type=DataType.UNDEFINED, - access_rights=AccessRights.MANAGE) + access_rights=AccessRights.MANAGE, + dummy_field="dummy") self.assertEqual(output.get_binding_name(), "serviceBus") self.assertEqual(output.get_dict_repr(), { "type": SERVICE_BUS, "direction": BindingDirection.OUT, + 'dummyField': 'dummy', "name": "res", "dataType": DataType.UNDEFINED, "connection": "dummy_conn", diff --git a/tests/decorators/test_timer.py b/tests/decorators/test_timer.py index 7a8332ad..c8923477 100644 --- a/tests/decorators/test_timer.py +++ b/tests/decorators/test_timer.py @@ -13,12 +13,14 @@ def test_timer_trigger_valid_creation(self): schedule="dummy_schedule", data_type=DataType.UNDEFINED, run_on_startup=False, - use_monitor=False) + use_monitor=False, + dummy_field="dummy") self.assertEqual(trigger.get_binding_name(), "timerTrigger") self.assertEqual(trigger.get_dict_repr(), { "type": TIMER_TRIGGER, "direction": BindingDirection.IN, + 'dummyField': 'dummy', "name": "req", "dataType": DataType.UNDEFINED, "schedule": "dummy_schedule", diff --git a/tests/decorators/test_utils.py b/tests/decorators/test_utils.py index 791fe7bf..90acdb39 100644 --- a/tests/decorators/test_utils.py +++ b/tests/decorators/test_utils.py @@ -4,7 +4,10 @@ from azure.functions import HttpMethod from azure.functions.decorators import utils -from azure.functions.decorators.core import DataType +from azure.functions.decorators.constants import HTTP_TRIGGER +from azure.functions.decorators.core import DataType, is_supported_trigger_type +from azure.functions.decorators.custom import CustomTrigger +from azure.functions.decorators.http import HttpTrigger from azure.functions.decorators.utils import to_camel_case, BuildDictMeta, \ is_snake_case, is_word @@ -19,6 +22,11 @@ def test_parse_singular_str_to_enum_str(self): utils.parse_singular_param_to_enum('STRING', DataType), DataType.STRING) + def test_parse_singular_lowercase_str_to_enum_str(self): + self.assertEqual( + utils.parse_singular_param_to_enum('string', DataType), + DataType.STRING) + def test_parse_singular_enum_to_enum(self): self.assertEqual( utils.parse_singular_param_to_enum(DataType.STRING, DataType), @@ -43,6 +51,11 @@ def test_parse_iterable_str_to_enums(self): utils.parse_iterable_param_to_enums(['GET', 'POST'], HttpMethod), [HttpMethod.GET, HttpMethod.POST]) + def test_parse_iterable_lowercase_str_to_enums(self): + self.assertEqual( + utils.parse_iterable_param_to_enums(['get', 'post'], HttpMethod), + [HttpMethod.GET, HttpMethod.POST]) + def test_parse_iterable_enums_to_enums(self): self.assertEqual( utils.parse_iterable_param_to_enums( @@ -142,7 +155,7 @@ def test_clean_nones_nested(self): { "hello2": ["dummy1", "dummy2", ["dummy3"], {}], "hello4": {"dummy5": "pass1"} - } # NoQA + } # NoQA ) def test_add_to_dict_no_args(self): @@ -160,14 +173,19 @@ def dummy(): def test_add_to_dict_valid(self): class TestDict: @BuildDictMeta.add_to_dict - def dummy(self, arg1, arg2): - pass + def __init__(self, arg1, arg2, **kwargs): + self.arg1 = arg1 + self.arg2 = arg2 - test_obj = TestDict() - test_obj.dummy('val1', 'val2') + test_obj = TestDict('val1', 'val2', dummy1="dummy1", dummy2="dummy2") - self.assertEqual(getattr(test_obj, 'init_params'), - ['self', 'arg1', 'arg2']) + self.assertCountEqual(getattr(test_obj, 'init_params'), + {'self', 'arg1', 'arg2', 'kwargs', 'dummy1', + 'dummy2'}) + self.assertEqual(getattr(test_obj, "arg1", None), "val1") + self.assertEqual(getattr(test_obj, "arg2", None), "val2") + self.assertEqual(getattr(test_obj, "dummy1", None), "dummy1") + self.assertEqual(getattr(test_obj, "dummy2", None), "dummy2") def test_build_dict_meta(self): class TestBuildDict(metaclass=BuildDictMeta): @@ -182,6 +200,20 @@ def get_dict_repr(self): test_obj = TestBuildDict('val1', 'val2') - self.assertEqual(getattr(test_obj, 'init_params'), - ['self', 'arg1', 'arg2']) + self.assertCountEqual(getattr(test_obj, 'init_params'), + {'self', 'arg1', 'arg2'}) self.assertEqual(test_obj.get_dict_repr(), {"world": ["dummy"]}) + + def test_is_supported_trigger_binding_name(self): + self.assertTrue( + is_supported_trigger_type( + CustomTrigger(name='req', type=HTTP_TRIGGER), HttpTrigger)) + + def test_is_supported_trigger_instance(self): + self.assertTrue( + is_supported_trigger_type(HttpTrigger(name='req'), HttpTrigger)) + + def test_is_not_supported_trigger_type(self): + self.assertFalse( + is_supported_trigger_type(CustomTrigger(name='req', type="dummy"), + HttpTrigger)) From 3468a43564e40c70153175883a7dd57743ea791f Mon Sep 17 00:00:00 2001 From: peterstone2017 Date: Wed, 20 Apr 2022 18:18:59 -0500 Subject: [PATCH 2/3] address pr comments --- azure/functions/decorators/core.py | 17 +- azure/functions/decorators/function_app.py | 66 ++-- .../decorators/{custom.py => generic.py} | 7 +- docs/ProgModelSpec.pyi | 353 ++++++++++-------- tests/decorators/test_core.py | 12 + tests/decorators/test_decorators.py | 51 ++- .../{test_custom.py => test_generic.py} | 34 +- tests/decorators/test_utils.py | 16 +- 8 files changed, 308 insertions(+), 248 deletions(-) rename azure/functions/decorators/{custom.py => generic.py} (88%) rename tests/decorators/{test_custom.py => test_generic.py} (67%) diff --git a/azure/functions/decorators/core.py b/azure/functions/decorators/core.py index 526138a2..a520065b 100644 --- a/azure/functions/decorators/core.py +++ b/azure/functions/decorators/core.py @@ -84,6 +84,9 @@ def __init__(self, name: str, direction: BindingDirection, data_type: Optional[DataType] = None, type: Optional[str] = None): # NoQa + # For natively supported bindings, get_binding_name is always + # implemented, and for generic bindings, type is a required argument + # in decorator functions. self.type = self.get_binding_name() \ if self.get_binding_name() is not None else type self.name = name @@ -124,6 +127,13 @@ class Trigger(Binding, ABC, metaclass=ABCBuildDictMeta): Ref: https://aka.ms/functions-triggers-bindings-overview """ + @staticmethod + def is_supported_trigger_type(trigger_instance: 'Trigger', + trigger_type: Type['Trigger']): + return isinstance(trigger_instance, + trigger_type) or trigger_instance.type == \ + trigger_type.get_binding_name() + def __init__(self, name: str, data_type: Optional[DataType] = None, type: Optional[str] = None) -> None: super().__init__(direction=BindingDirection.IN, @@ -150,10 +160,3 @@ 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) - - -def is_supported_trigger_type(trigger_instance: Trigger, - trigger_type: Type[Trigger]): - return isinstance(trigger_instance, - trigger_type) or \ - trigger_instance.type == trigger_type.get_binding_name() diff --git a/azure/functions/decorators/function_app.py b/azure/functions/decorators/function_app.py index cf755e42..a10119d5 100644 --- a/azure/functions/decorators/function_app.py +++ b/azure/functions/decorators/function_app.py @@ -5,8 +5,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, \ - is_supported_trigger_type + AuthLevel, SCRIPT_FILE_NAME, Cardinality, AccessRights from azure.functions.decorators.cosmosdb import CosmosDBTrigger, \ CosmosDBOutput, CosmosDBInput from azure.functions.decorators.eventhub import EventHubTrigger, EventHubOutput @@ -21,7 +20,7 @@ parse_iterable_param_to_enums, StringifyEnumJsonEncoder from azure.functions.http import HttpRequest from .constants import HTTP_TRIGGER -from .custom import CustomInputBinding, CustomTrigger, CustomOutputBinding +from .generic import GenericInputBinding, GenericTrigger, GenericOutputBinding from .._http_asgi import AsgiMiddleware from .._http_wsgi import WsgiMiddleware, Context @@ -179,9 +178,9 @@ def _validate_function(self) -> None: f" in bindings {bindings}") # Set route to function name if unspecified in the http trigger - if is_supported_trigger_type(trigger, HttpTrigger) \ + if Trigger.is_supported_trigger_type(trigger, HttpTrigger) \ and getattr(trigger, 'route', None) is None: - setattr(trigger, 'route', self._function.get_function_name()) + setattr(trigger, 'route', function_name) def build(self) -> Function: self._validate_function() @@ -1236,17 +1235,17 @@ def decorator(): return wrap - def custom_input_binding(self, - arg_name: str, - type: str, - data_type: Optional[Union[DataType, str]] = None, - **kwargs - ) -> Callable: + def generic_input_binding(self, + arg_name: str, + type: str, + data_type: Optional[Union[DataType, str]] = None, + **kwargs + ) -> Callable: """ - The custom_input_binding decorator adds :class:`CustomInputBinding` + The generic_input_binding decorator adds :class:`GenericInputBinding` to the :class:`FunctionBuilder` object for building :class:`Function` object used in worker function indexing model. - This is equivalent to defining a custom input binding in the + This is equivalent to defining a generic input binding in the function.json which enables function to read data from a custom defined input source. All optional fields will be given default value by function host when @@ -1268,7 +1267,7 @@ def custom_input_binding(self, def wrap(fb): def decorator(): fb.add_binding( - binding=CustomInputBinding( + binding=GenericInputBinding( name=arg_name, type=type, data_type=parse_singular_param_to_enum(data_type, @@ -1280,17 +1279,18 @@ def decorator(): return wrap - def custom_output_binding(self, - arg_name: str, - type: str, - data_type: Optional[Union[DataType, str]] = None, - **kwargs - ) -> Callable: + def generic_output_binding(self, + arg_name: str, + type: str, + data_type: Optional[ + Union[DataType, str]] = None, + **kwargs + ) -> Callable: """ - The custom_output_binding decorator adds :class:`CustomOutputBinding` + The generic_output_binding decorator adds :class:`GenericOutputBinding` to the :class:`FunctionBuilder` object for building :class:`Function` object used in worker function indexing model. - This is equivalent to defining a custom output binding in the + This is equivalent to defining a generic output binding in the function.json which enables function to write data from a custom defined output source. All optional fields will be given default value by function host when @@ -1312,7 +1312,7 @@ def custom_output_binding(self, def wrap(fb): def decorator(): fb.add_binding( - binding=CustomOutputBinding( + binding=GenericOutputBinding( name=arg_name, type=type, data_type=parse_singular_param_to_enum(data_type, @@ -1324,18 +1324,18 @@ def decorator(): return wrap - def custom_trigger(self, - arg_name: str, - type: str, - data_type: Optional[Union[DataType, str]] = None, - **kwargs - ) -> Callable: + def generic_trigger(self, + arg_name: str, + type: str, + data_type: Optional[Union[DataType, str]] = None, + **kwargs + ) -> Callable: """ - The custom_trigger decorator adds :class:`CustomTrigger` + The generic_trigger decorator adds :class:`GenericTrigger` to the :class:`FunctionBuilder` object for building :class:`Function` object used in worker function indexing model. - This is equivalent to defining a custom trigger in the - function.json which triggers function to execute when custom trigger + This is equivalent to defining a generic trigger in the + function.json which triggers function to execute when generic trigger events are received by host. All optional fields will be given default value by function host when they are parsed by function host. @@ -1362,7 +1362,7 @@ def decorator(): if 'route' not in kwargs: kwargs['route'] = None fb.add_trigger( - trigger=CustomTrigger( + trigger=GenericTrigger( name=arg_name, type=type, data_type=parse_singular_param_to_enum(data_type, diff --git a/azure/functions/decorators/custom.py b/azure/functions/decorators/generic.py similarity index 88% rename from azure/functions/decorators/custom.py rename to azure/functions/decorators/generic.py index 0e7ee0b8..baa78806 100644 --- a/azure/functions/decorators/custom.py +++ b/azure/functions/decorators/generic.py @@ -6,7 +6,7 @@ InputBinding, OutputBinding, DataType -class CustomInputBinding(InputBinding): +class GenericInputBinding(InputBinding): @staticmethod def get_binding_name() -> str: @@ -20,8 +20,7 @@ def __init__(self, super().__init__(name=name, data_type=data_type, type=type) -class CustomOutputBinding(OutputBinding): - # binding_name: str = "" +class GenericOutputBinding(OutputBinding): @staticmethod def get_binding_name() -> str: @@ -35,7 +34,7 @@ def __init__(self, super().__init__(name=name, data_type=data_type, type=type) -class CustomTrigger(Trigger): +class GenericTrigger(Trigger): @staticmethod def get_binding_name() -> str: diff --git a/docs/ProgModelSpec.pyi b/docs/ProgModelSpec.pyi index 9918f673..1412b0f4 100644 --- a/docs/ProgModelSpec.pyi +++ b/docs/ProgModelSpec.pyi @@ -3,7 +3,9 @@ import typing from typing import Callable, Optional, Union, Iterable -from azure.functions import AsgiMiddleware, WsgiMiddleware +from azure.functions import AsgiMiddleware, WsgiMiddleware, Function, \ + HttpRequest, Context +from azure.functions.decorators.function_app import FunctionBuilder from azure.functions.decorators.http import HttpMethod from azure.functions.decorators.core import DataType, \ AuthLevel, Cardinality, AccessRights @@ -11,10 +13,10 @@ from azure.functions.decorators.core import DataType, \ class FunctionApp: """FunctionApp object used by worker function indexing model captures - user defined functions and metadata. + user defined functions and metadata. - Ref: https://aka.ms/azure-function-ref - """ + Ref: https://aka.ms/azure-function-ref + """ def __init__(self, http_auth_level: Union[AuthLevel, str] = AuthLevel.FUNCTION, @@ -33,27 +35,101 @@ class FunctionApp: """ pass + @property + def app_script_file(self) -> str: + """Name of function app script file in which all the functions + are defined. \n + Script file defined here is for placeholder purpose, please refer to + worker defined script file path as the single point of truth. + + :return: Script file name. + """ + return self._app_script_file + + @property + def auth_level(self) -> AuthLevel: + """Authorization level of the function app. Will be applied to the http + trigger functions which does not have authorization level specified. + + :return: Authorization level of the function app. + """ + + return self._auth_level + + def get_functions(self) -> typing.List[Function]: + """Get the function objects in the function app. + + :return: List of functions in the function app. + """ + return [function_builder.build() for function_builder + in self._function_builders] + + def _validate_type(self, func: Union[Callable, FunctionBuilder]) \ + -> FunctionBuilder: + """Validate the type of the function object and return the created + :class:`FunctionBuilder` object. + + + :param func: Function object passed to + :meth:`_configure_function_builder` + :raises ValueError: Raise error when func param is neither + :class:`Callable` nor :class:`FunctionBuilder`. + :return: :class:`FunctionBuilder` object. + """ + if isinstance(func, FunctionBuilder): + fb = self._function_builders.pop() + elif callable(func): + fb = FunctionBuilder(func, self._app_script_file) + else: + raise ValueError( + "Unsupported type for function app decorator found.") + return fb + + def _configure_function_builder(self, wrap) -> Callable: + """Decorator function on user defined function to create and return + :class:`FunctionBuilder` object from :class:`Callable` func. + """ + + def decorator(func): + fb = self._validate_type(func) + self._function_builders.append(fb) + return wrap(fb) + + return decorator + def function_name(self, name: str) -> Callable: """Set name of the :class:`Function` object. :param name: Name of the function. :return: Decorator function. """ - pass + + @self._configure_function_builder + def wrap(fb): + def decorator(): + fb.configure_function_name(name) + return fb + + return decorator() + + return wrap def _add_http_app(self, - http_middleware: Union[AsgiMiddleware, WsgiMiddleware], - app_kwargs: typing.Dict) -> None: + http_middleware: Union[ + AsgiMiddleware, WsgiMiddleware]) -> None: """Add a Wsgi or Asgi app integrated http function. :param http_middleware: :class:`AsgiMiddleware` or :class:`WsgiMiddleware` instance. - :param app_kwargs: dict of :meth:`route` param names and values for - custom configuration of wsgi/asgi app. :return: None """ - pass + + @self.route(methods=(method for method in HttpMethod), + auth_level=self.auth_level, + route="/{*route}") + def http_app_func(req: HttpRequest, context: Context): + return http_middleware.handle(req, context) def route(self, route: Optional[str] = None, @@ -82,20 +158,19 @@ class FunctionApp: defaults to 'req'. :param binding_arg_name: Argument name for :class:`HttpResponse`, defaults to '$return'. - :param trigger_arg_data_type: Defines how Functions runtime should - treat the trigger_arg_name value. - :param output_arg_data_type: Defines how Functions runtime should - treat the binding_arg_name value. :param methods: A tuple of the HTTP methods to which the function responds. :param auth_level: Determines what keys, if any, need to be present on the request in order to invoke the function. :return: Decorator function. - :param binding_extra_fields: Keyword arguments for specifying - additional binding fields to include in the binding json. - :param trigger_extra_fields: Keyword arguments for specifying - additional binding fields to include in the trigger json. + :param trigger_extra_fields: Additional fields to include in trigger + json. For example, + >>> data_type='STRING' # 'dataType': 'STRING' in trigger json + :param binding_extra_fields: Additional fields to include in binding + json. For example, + >>> data_type='STRING' # 'dataType': 'STRING' in binding json """ + pass def schedule(self, @@ -126,11 +201,9 @@ class FunctionApp: schedule should be monitored. :param data_type: Defines how Functions runtime should treat the parameter value. - :param kwargs: Keyword arguments for specifying additional binding - fields to include in the binding json. - :return: Decorator function. """ + pass def service_bus_queue_trigger( @@ -143,7 +216,7 @@ class FunctionApp: is_sessions_enabled: Optional[bool] = None, cardinality: Optional[Union[Cardinality, str]] = None, **kwargs) -> Callable: - """The service_bus_queue_trigger decorator adds + """The on_service_bus_queue_change decorator adds :class:`ServiceBusQueueTrigger` to the :class:`FunctionBuilder` object for building :class:`Function` object used in worker function indexing model. This is equivalent to defining ServiceBusQueueTrigger @@ -165,11 +238,9 @@ class FunctionApp: :param is_sessions_enabled: True if connecting to a session-aware queue or subscription. :param cardinality: Set to many in order to enable batching. - :param kwargs: Keyword arguments for specifying additional binding - fields to include in the binding json. - :return: Decorator function. """ + pass def write_service_bus_queue(self, @@ -180,7 +251,8 @@ class FunctionApp: Union[DataType, str]] = None, access_rights: Optional[Union[ AccessRights, str]] = None, - **kwargs) -> Callable: + **kwargs) -> \ + Callable: """The write_service_bus_queue decorator adds :class:`ServiceBusQueueOutput` to the :class:`FunctionBuilder` object for building :class:`Function` object used in worker function @@ -200,12 +272,9 @@ class FunctionApp: :param data_type: Defines how Functions runtime should treat the parameter value. :param access_rights: Access rights for the connection string. - :param kwargs: Keyword arguments for specifying additional binding - fields to include in the binding json. - :return: Decorator function. - """ + pass def service_bus_topic_trigger( @@ -219,7 +288,7 @@ class FunctionApp: is_sessions_enabled: Optional[bool] = None, cardinality: Optional[Union[Cardinality, str]] = None, **kwargs) -> Callable: - """The service_bus_topic_trigger decorator adds + """The on_service_bus_topic_change decorator adds :class:`ServiceBusTopicTrigger` to the :class:`FunctionBuilder` object for building :class:`Function` object used in worker function indexing model. This is equivalent to defining ServiceBusTopicTrigger @@ -242,12 +311,9 @@ class FunctionApp: :param is_sessions_enabled: True if connecting to a session-aware queue or subscription. :param cardinality: Set to many in order to enable batching. - :param kwargs: Keyword arguments for specifying additional binding - fields to include in the binding json. - :return: Decorator function. - """ + pass def write_service_bus_topic(self, @@ -259,7 +325,8 @@ class FunctionApp: Union[DataType, str]] = None, access_rights: Optional[Union[ AccessRights, str]] = None, - **kwargs) -> Callable: + **kwargs) -> \ + Callable: """The write_service_bus_topic decorator adds :class:`ServiceBusTopicOutput` to the :class:`FunctionBuilder` object for building :class:`Function` object used in worker function @@ -280,21 +347,18 @@ class FunctionApp: :param data_type: Defines how Functions runtime should treat the parameter value, defaults to DataType.UNDEFINED. :param access_rights: Access rights for the connection string. - :param kwargs: Keyword arguments for specifying additional binding - fields to include in the binding json. - :return: Decorator function. - """ + pass - def on_queue_change(self, - arg_name: str, - queue_name: str, - connection: str, - data_type: Optional[DataType] = None, - **kwargs) -> Callable: - """The on_queue_change decorator adds :class:`QueueTrigger` to the + def queue_trigger(self, + arg_name: str, + queue_name: str, + connection: str, + data_type: Optional[DataType] = None, + **kwargs) -> Callable: + """The queue_trigger decorator adds :class:`QueueTrigger` to the :class:`FunctionBuilder` object for building :class:`Function` object used in worker function indexing model. This is equivalent to defining QueueTrigger @@ -312,12 +376,9 @@ class FunctionApp: that specifies how to connect to Azure Queues. :param data_type: Defines how Functions runtime should treat the parameter value. - :param kwargs: Keyword arguments for specifying additional binding - fields to include in the binding json. - :return: Decorator function. - """ + pass def write_queue(self, @@ -342,25 +403,26 @@ class FunctionApp: :param queue_name: The name of the queue to poll. :param connection: The name of an app setting or setting collection that specifies how to connect to Azure Queues. - :param data_type: Set to many in order to enable batching. - :param kwargs: Keyword arguments for specifying additional binding - fields to include in the binding json. - + :param data_type: Defines how Functions runtime should treat the + parameter value. :return: Decorator function. - """ + pass def event_hub_message_trigger(self, - arg_name: str, - connection: str, - event_hub_name: str, - data_type: Optional[Union[DataType, str]] = None, - cardinality: Optional[ - Union[Cardinality, str]] = None, - consumer_group: Optional[str] = None, - **kwargs) -> Callable: - """The on_event_hub_message decorator adds :class:`EventHubTrigger` + arg_name: str, + connection: str, + event_hub_name: str, + data_type: Optional[ + Union[DataType, str]] = None, + cardinality: Optional[ + Union[Cardinality, str]] = None, + consumer_group: Optional[ + str] = None, + **kwargs) -> Callable: + """The event_hub_message_trigger decorator adds + :class:`EventHubTrigger` to the :class:`FunctionBuilder` object for building :class:`Function` object used in worker function indexing model. This is equivalent to defining EventHubTrigger @@ -381,12 +443,9 @@ class FunctionApp: :param cardinality: Set to many in order to enable batching. :param consumer_group: An optional property that sets the consumer group used to subscribe to events in the hub. - :param kwargs: Keyword arguments for specifying additional binding - fields to include in the binding json. - :return: Decorator function. - """ + pass def write_event_hub_message(self, @@ -395,7 +454,8 @@ class FunctionApp: event_hub_name: str, data_type: Optional[ Union[DataType, str]] = None, - **kwargs) -> Callable: + **kwargs) -> \ + Callable: """The write_event_hub_message decorator adds :class:`EventHubOutput` to the :class:`FunctionBuilder` object for building :class:`Function` object used in worker function @@ -414,39 +474,36 @@ class FunctionApp: :param event_hub_name: The name of the event hub. :param data_type: Defines how Functions runtime should treat the parameter value. - :param kwargs: Keyword arguments for specifying additional binding - fields to include in the binding json. - :return: Decorator function. - """ + pass def cosmos_db_trigger(self, - arg_name: str, - database_name: str, - collection_name: str, - connection_string_setting: str, - lease_collection_name: Optional[str] = None, - lease_connection_string_setting: Optional[ - str] = None, - lease_database_name: Optional[str] = None, - create_lease_collection_if_not_exists: Optional[ - bool] = None, - leases_collection_throughput: Optional[int] = None, - lease_collection_prefix: Optional[str] = None, - checkpoint_interval: Optional[int] = None, - checkpoint_document_count: Optional[int] = None, - feed_poll_delay: Optional[int] = None, - lease_renew_interval: Optional[int] = None, - lease_acquire_interval: Optional[int] = None, - lease_expiration_interval: Optional[int] = None, - max_items_per_invocation: Optional[int] = None, - start_from_beginning: Optional[bool] = None, - preferred_locations: Optional[str] = None, - data_type: Optional[ - Union[DataType, str]] = None, - **kwargs) -> \ + arg_name: str, + database_name: str, + collection_name: str, + connection_string_setting: str, + lease_collection_name: Optional[str] = None, + lease_connection_string_setting: Optional[ + str] = None, + lease_database_name: Optional[str] = None, + create_lease_collection_if_not_exists: Optional[ + bool] = None, + leases_collection_throughput: Optional[int] = None, + lease_collection_prefix: Optional[str] = None, + checkpoint_interval: Optional[int] = None, + checkpoint_document_count: Optional[int] = None, + feed_poll_delay: Optional[int] = None, + lease_renew_interval: Optional[int] = None, + lease_acquire_interval: Optional[int] = None, + lease_expiration_interval: Optional[int] = None, + max_items_per_invocation: Optional[int] = None, + start_from_beginning: Optional[bool] = None, + preferred_locations: Optional[str] = None, + data_type: Optional[ + Union[DataType, str]] = None, + **kwargs) -> \ Callable: """The cosmos_db_trigger decorator adds :class:`CosmosDBTrigger` to the :class:`FunctionBuilder` object @@ -508,12 +565,9 @@ class FunctionApp: for geo-replicated database accounts in the Azure Cosmos DB service. :param data_type: Defines how Functions runtime should treat the parameter value. - :param kwargs: Keyword arguments for specifying additional binding - fields to include in the binding json. - :return: Decorator function. - """ + pass def write_cosmos_db_documents(self, @@ -562,12 +616,9 @@ class FunctionApp: for geo-replicated database accounts in the Azure Cosmos DB service. :param data_type: Defines how Functions runtime should treat the parameter value. - :param kwargs: Keyword arguments for specifying additional binding - fields to include in the binding json. - :return: Decorator function. - """ + pass def read_cosmos_db_documents(self, @@ -606,20 +657,17 @@ class FunctionApp: lookup. :param data_type: Defines how Functions runtime should treat the parameter value. - :param kwargs: Keyword arguments for specifying additional binding - fields to include in the binding json. - :return: Decorator function. - """ + pass - def blob_change_trigger(self, - arg_name: str, - path: str, - connection: str, - data_type: Optional[DataType] = None, - **kwargs) -> Callable: + def blob_trigger(self, + arg_name: str, + path: str, + connection: str, + data_type: Optional[DataType] = None, + **kwargs) -> Callable: """ The blob_change_trigger decorator adds :class:`BlobTrigger` to the :class:`FunctionBuilder` object @@ -639,12 +687,9 @@ class FunctionApp: that specifies how to connect to Azure Blobs. :param data_type: Defines how Functions runtime should treat the parameter value. - :param kwargs: Keyword arguments for specifying additional binding - fields to include in the binding json. - :return: Decorator function. - """ + pass def read_blob(self, @@ -653,6 +698,7 @@ class FunctionApp: connection: str, data_type: Optional[DataType] = None, **kwargs) -> Callable: + """ The read_blob decorator adds :class:`BlobInput` to the :class:`FunctionBuilder` object @@ -672,12 +718,9 @@ class FunctionApp: that specifies how to connect to Azure Blobs. :param data_type: Defines how Functions runtime should treat the parameter value. - :param kwargs: Keyword arguments for specifying additional binding - fields to include in the binding json. - :return: Decorator function. - """ + pass def write_blob(self, @@ -686,6 +729,7 @@ class FunctionApp: connection: str, data_type: Optional[DataType] = None, **kwargs) -> Callable: + """ The write_blob decorator adds :class:`BlobOutput` to the :class:`FunctionBuilder` object @@ -705,26 +749,22 @@ class FunctionApp: that specifies how to connect to Azure Blobs. :param data_type: Defines how Functions runtime should treat the parameter value. - :param kwargs: Keyword arguments for specifying additional binding - fields to include in the binding json. - :return: Decorator function. - """ - pass + pass - def custom_input_binding(self, - arg_name: str, - type: str, - data_type: Optional[Union[DataType, str]] = None, - **kwargs - ) -> Callable: + def generic_input_binding(self, + arg_name: str, + type: str, + data_type: Optional[Union[DataType, str]] = None, + **kwargs + ) -> Callable: """ - The custom_input_binding decorator adds :class:`CustomInputBinding` + The generic_input_binding decorator adds :class:`GenericInputBinding` to the :class:`FunctionBuilder` object for building :class:`Function` object used in worker function indexing model. - This is equivalent to defining a custom input binding in the + This is equivalent to defining a generic input binding in the function.json which enables function to read data from a custom defined input source. All optional fields will be given default value by function host when @@ -741,19 +781,21 @@ class FunctionApp: :return: Decorator function. """ + pass - def custom_output_binding(self, - arg_name: str, - type: str, - data_type: Optional[Union[DataType, str]] = None, - **kwargs - ) -> Callable: + def generic_output_binding(self, + arg_name: str, + type: str, + data_type: Optional[ + Union[DataType, str]] = None, + **kwargs + ) -> Callable: """ - The custom_output_binding decorator adds :class:`CustomOutputBinding` + The generic_output_binding decorator adds :class:`GenericOutputBinding` to the :class:`FunctionBuilder` object for building :class:`Function` object used in worker function indexing model. - This is equivalent to defining a custom output binding in the + This is equivalent to defining a generic output binding in the function.json which enables function to write data from a custom defined output source. All optional fields will be given default value by function host when @@ -770,20 +812,21 @@ class FunctionApp: :return: Decorator function. """ + pass - def custom_trigger(self, - arg_name: str, - type: str, - data_type: Optional[Union[DataType, str]] = None, - **kwargs - ) -> Callable: + def generic_trigger(self, + arg_name: str, + type: str, + data_type: Optional[Union[DataType, str]] = None, + **kwargs + ) -> Callable: """ - The custom_trigger decorator adds :class:`CustomTrigger` + The generic_trigger decorator adds :class:`GenericTrigger` to the :class:`FunctionBuilder` object for building :class:`Function` object used in worker function indexing model. - This is equivalent to defining a custom trigger in the - function.json which triggers function to execute when custom trigger + This is equivalent to defining a generic trigger in the + function.json which triggers function to execute when generic trigger events are received by host. All optional fields will be given default value by function host when they are parsed by function host. @@ -799,4 +842,6 @@ class FunctionApp: :return: Decorator function. """ + pass + diff --git a/tests/decorators/test_core.py b/tests/decorators/test_core.py index 76eedfde..390d6b9e 100644 --- a/tests/decorators/test_core.py +++ b/tests/decorators/test_core.py @@ -88,3 +88,15 @@ def test_output_creation(self): self.assertEqual(test_output.get_binding_name(), "DummyOutputBinding") self.assertEqual(test_output.get_dict_repr(), expected_dict) + + def test_supported_trigger_types_populated(self): + for supported_trigger in Trigger.__subclasses__(): + trigger_name = supported_trigger.__name__ + if trigger_name != "GenericTrigger": + trigger_type_name = supported_trigger.get_binding_name() + self.assertTrue(trigger_type_name is not None, + f"binding_type {trigger_name} can not be " + f"None!") + self.assertTrue(len(trigger_type_name) > 0, + f"binding_type {trigger_name} can not be " + f"empty str!") diff --git a/tests/decorators/test_decorators.py b/tests/decorators/test_decorators.py index e7121b64..f3350b46 100644 --- a/tests/decorators/test_decorators.py +++ b/tests/decorators/test_decorators.py @@ -971,14 +971,13 @@ def dummy(): "connection": "dummy_out_conn" }) - # mix, asgi/wsgi - def test_custom_trigger(self): app = self.func_app - @app.custom_trigger(arg_name="req", type=BLOB_TRIGGER, - data_type=DataType.BINARY, connection="dummy_conn", - path="dummy_path") + @app.generic_trigger(arg_name="req", type=BLOB_TRIGGER, + data_type=DataType.BINARY, + connection="dummy_conn", + path="dummy_path") def dummy(): pass @@ -1001,13 +1000,13 @@ def dummy(): def test_custom_input_binding(self): app = self.func_app - @app.custom_trigger(arg_name="req", type=TIMER_TRIGGER, - data_type=DataType.BINARY, - schedule="dummy_schedule") - @app.custom_input_binding(arg_name="file", type=BLOB, - path="dummy_in_path", - connection="dummy_in_conn", - data_type=DataType.STRING) + @app.generic_trigger(arg_name="req", type=TIMER_TRIGGER, + data_type=DataType.BINARY, + schedule="dummy_schedule") + @app.generic_input_binding(arg_name="file", type=BLOB, + path="dummy_in_path", + connection="dummy_in_conn", + data_type=DataType.STRING) def dummy(): pass @@ -1039,13 +1038,13 @@ def dummy(): def test_custom_output_binding(self): app = self.func_app - @app.custom_trigger(arg_name="req", type=QUEUE_TRIGGER, - queue_name="dummy_queue", - connection="dummy_conn") - @app.custom_output_binding(arg_name="out", type=BLOB, - path="dummy_out_path", - connection="dummy_out_conn", - data_type=DataType.STRING) + @app.generic_trigger(arg_name="req", type=QUEUE_TRIGGER, + queue_name="dummy_queue", + connection="dummy_conn") + @app.generic_output_binding(arg_name="out", type=BLOB, + path="dummy_out_path", + connection="dummy_out_conn", + data_type=DataType.STRING) def dummy(): pass @@ -1074,7 +1073,7 @@ def dummy(): def test_custom_http_trigger(self): app = self.func_app - @app.custom_trigger(arg_name="req", type=HTTP_TRIGGER) + @app.generic_trigger(arg_name="req", type=HTTP_TRIGGER) def dummy(): pass @@ -1093,8 +1092,8 @@ def dummy(): def test_custom_binding_with_excluded_params(self): app = self.func_app - @app.custom_trigger(arg_name="req", type=QUEUE_TRIGGER, - direction=BindingDirection.INOUT) + @app.generic_trigger(arg_name="req", type=QUEUE_TRIGGER, + direction=BindingDirection.INOUT) def dummy(): pass @@ -1113,10 +1112,10 @@ def test_mixed_custom_and_supported_binding(self): @app.queue_trigger(arg_name="req", queue_name="dummy_queue", connection="dummy_conn") - @app.custom_output_binding(arg_name="out", type=BLOB, - path="dummy_out_path", - connection="dummy_out_conn", - data_type=DataType.STRING) + @app.generic_output_binding(arg_name="out", type=BLOB, + path="dummy_out_path", + connection="dummy_out_conn", + data_type=DataType.STRING) def dummy(): pass diff --git a/tests/decorators/test_custom.py b/tests/decorators/test_generic.py similarity index 67% rename from tests/decorators/test_custom.py rename to tests/decorators/test_generic.py index 249b10f5..597c04db 100644 --- a/tests/decorators/test_custom.py +++ b/tests/decorators/test_generic.py @@ -5,18 +5,18 @@ from azure.functions.decorators.constants import HTTP_TRIGGER, COSMOS_DB, BLOB from azure.functions.decorators.core import BindingDirection, AuthLevel, \ DataType -from azure.functions.decorators.custom import CustomInputBinding, \ - CustomTrigger, CustomOutputBinding +from azure.functions.decorators.generic import GenericInputBinding, \ + GenericTrigger, GenericOutputBinding -class TestCustom(unittest.TestCase): - def test_custom_trigger_valid_creation(self): - trigger = CustomTrigger(name="req", - type=HTTP_TRIGGER, - data_type=DataType.UNDEFINED, - auth_level=AuthLevel.ANONYMOUS, - methods=["GET", "POST"], - route="dummy") +class TestGeneric(unittest.TestCase): + def test_generic_trigger_valid_creation(self): + trigger = GenericTrigger(name="req", + type=HTTP_TRIGGER, + data_type=DataType.UNDEFINED, + auth_level=AuthLevel.ANONYMOUS, + methods=["GET", "POST"], + route="dummy") self.assertEqual(trigger.get_binding_name(), None) self.assertEqual(trigger.type, HTTP_TRIGGER) @@ -30,8 +30,8 @@ def test_custom_trigger_valid_creation(self): "methods": ["GET", "POST"] }) - def test_custom_input_valid_creation(self): - cosmosdb_input = CustomInputBinding( + def test_generic_input_valid_creation(self): + cosmosdb_input = GenericInputBinding( name="inDocs", type=COSMOS_DB, database_name="dummy_db", @@ -52,11 +52,11 @@ def test_custom_input_valid_creation(self): 'sqlQuery': 'dummy_query', 'type': COSMOS_DB}) - def test_custom_output_valid_creation(self): - blob_output = CustomOutputBinding(name="res", type=BLOB, - data_type=DataType.UNDEFINED, - path="dummy_path", - connection="dummy_connection") + def test_generic_output_valid_creation(self): + blob_output = GenericOutputBinding(name="res", type=BLOB, + data_type=DataType.UNDEFINED, + path="dummy_path", + connection="dummy_connection") self.assertEqual(blob_output.get_binding_name(), None) self.assertEqual(blob_output.get_dict_repr(), { diff --git a/tests/decorators/test_utils.py b/tests/decorators/test_utils.py index 90acdb39..4dd87e06 100644 --- a/tests/decorators/test_utils.py +++ b/tests/decorators/test_utils.py @@ -5,8 +5,8 @@ from azure.functions import HttpMethod from azure.functions.decorators import utils from azure.functions.decorators.constants import HTTP_TRIGGER -from azure.functions.decorators.core import DataType, is_supported_trigger_type -from azure.functions.decorators.custom import CustomTrigger +from azure.functions.decorators.core import DataType, Trigger +from azure.functions.decorators.generic import GenericTrigger from azure.functions.decorators.http import HttpTrigger from azure.functions.decorators.utils import to_camel_case, BuildDictMeta, \ is_snake_case, is_word @@ -206,14 +206,16 @@ def get_dict_repr(self): def test_is_supported_trigger_binding_name(self): self.assertTrue( - is_supported_trigger_type( - CustomTrigger(name='req', type=HTTP_TRIGGER), HttpTrigger)) + Trigger.is_supported_trigger_type( + GenericTrigger(name='req', type=HTTP_TRIGGER), HttpTrigger)) def test_is_supported_trigger_instance(self): self.assertTrue( - is_supported_trigger_type(HttpTrigger(name='req'), HttpTrigger)) + Trigger.is_supported_trigger_type(HttpTrigger(name='req'), + HttpTrigger)) def test_is_not_supported_trigger_type(self): self.assertFalse( - is_supported_trigger_type(CustomTrigger(name='req', type="dummy"), - HttpTrigger)) + Trigger.is_supported_trigger_type( + GenericTrigger(name='req', type="dummy"), + HttpTrigger)) From 443e02f64a978cfedcce3d8831ef27da65e40fda Mon Sep 17 00:00:00 2001 From: peterstone2017 Date: Wed, 20 Apr 2022 18:29:34 -0500 Subject: [PATCH 3/3] update prog spec --- docs/ProgModelSpec.pyi | 49 +++++++++++------------------------------- 1 file changed, 13 insertions(+), 36 deletions(-) diff --git a/docs/ProgModelSpec.pyi b/docs/ProgModelSpec.pyi index 1412b0f4..ac52ce04 100644 --- a/docs/ProgModelSpec.pyi +++ b/docs/ProgModelSpec.pyi @@ -3,12 +3,11 @@ import typing from typing import Callable, Optional, Union, Iterable -from azure.functions import AsgiMiddleware, WsgiMiddleware, Function, \ - HttpRequest, Context -from azure.functions.decorators.function_app import FunctionBuilder -from azure.functions.decorators.http import HttpMethod +from azure.functions import AsgiMiddleware, WsgiMiddleware, Function from azure.functions.decorators.core import DataType, \ AuthLevel, Cardinality, AccessRights +from azure.functions.decorators.function_app import FunctionBuilder +from azure.functions.decorators.http import HttpMethod class FunctionApp: @@ -33,6 +32,7 @@ class FunctionApp: AuthLevel. :param kwargs: Extra arguments passed to :func:`__init__`. """ + pass @property @@ -44,7 +44,8 @@ class FunctionApp: :return: Script file name. """ - return self._app_script_file + + pass @property def auth_level(self) -> AuthLevel: @@ -54,15 +55,14 @@ class FunctionApp: :return: Authorization level of the function app. """ - return self._auth_level + pass def get_functions(self) -> typing.List[Function]: """Get the function objects in the function app. :return: List of functions in the function app. """ - return [function_builder.build() for function_builder - in self._function_builders] + pass def _validate_type(self, func: Union[Callable, FunctionBuilder]) \ -> FunctionBuilder: @@ -76,26 +76,15 @@ class FunctionApp: :class:`Callable` nor :class:`FunctionBuilder`. :return: :class:`FunctionBuilder` object. """ - if isinstance(func, FunctionBuilder): - fb = self._function_builders.pop() - elif callable(func): - fb = FunctionBuilder(func, self._app_script_file) - else: - raise ValueError( - "Unsupported type for function app decorator found.") - return fb + + pass def _configure_function_builder(self, wrap) -> Callable: """Decorator function on user defined function to create and return :class:`FunctionBuilder` object from :class:`Callable` func. """ - def decorator(func): - fb = self._validate_type(func) - self._function_builders.append(fb) - return wrap(fb) - - return decorator + pass def function_name(self, name: str) -> Callable: """Set name of the :class:`Function` object. @@ -104,15 +93,7 @@ class FunctionApp: :return: Decorator function. """ - @self._configure_function_builder - def wrap(fb): - def decorator(): - fb.configure_function_name(name) - return fb - - return decorator() - - return wrap + pass def _add_http_app(self, http_middleware: Union[ @@ -125,11 +106,7 @@ class FunctionApp: :return: None """ - @self.route(methods=(method for method in HttpMethod), - auth_level=self.auth_level, - route="/{*route}") - def http_app_func(req: HttpRequest, context: Context): - return http_middleware.handle(req, context) + pass def route(self, route: Optional[str] = None,