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..a520065b 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,13 @@ 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 + # 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 self._direction = direction self._data_type = data_type @@ -111,7 +116,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 +126,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: + + @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, - 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) 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/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..a10119d5 100644 --- a/azure/functions/decorators/function_app.py +++ b/azure/functions/decorators/function_app.py @@ -19,6 +19,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 .generic import GenericInputBinding, GenericTrigger, GenericOutputBinding from .._http_asgi import AsgiMiddleware from .._http_wsgi import WsgiMiddleware, Context @@ -175,8 +177,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 Trigger.is_supported_trigger_type(trigger, HttpTrigger) \ + and getattr(trigger, 'route', None) is None: + setattr(trigger, 'route', function_name) def build(self) -> Function: self._validate_function() @@ -321,7 +325,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 +351,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 +371,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 +385,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 +421,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 +437,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 +479,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 +494,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 +530,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 +547,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 +591,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 +607,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 +646,8 @@ def decorator(): DataType), access_rights=parse_singular_param_to_enum( access_rights, - AccessRights))) + AccessRights), + **kwargs)) return fb return decorator() @@ -632,7 +658,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 +690,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 +702,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 +733,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 +750,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 +789,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 +802,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 +835,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 +866,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 +952,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 +977,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 +1030,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 +1047,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 +1090,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 +1102,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 +1135,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 +1147,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 +1181,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 +1193,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 +1227,147 @@ 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 generic_input_binding(self, + arg_name: str, + type: str, + data_type: Optional[Union[DataType, str]] = None, + **kwargs + ) -> Callable: + """ + 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 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 + 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=GenericInputBinding( + name=arg_name, + type=type, + data_type=parse_singular_param_to_enum(data_type, + DataType), + **kwargs)) + return fb + + return decorator() + + return wrap + + def generic_output_binding(self, + arg_name: str, + type: str, + data_type: Optional[ + Union[DataType, str]] = None, + **kwargs + ) -> Callable: + """ + 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 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 + 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=GenericOutputBinding( + name=arg_name, + type=type, + data_type=parse_singular_param_to_enum(data_type, + DataType), + **kwargs)) + return fb + + return decorator() + + return wrap + + def generic_trigger(self, + arg_name: str, + type: str, + data_type: Optional[Union[DataType, str]] = None, + **kwargs + ) -> Callable: + """ + 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 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. + + 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=GenericTrigger( + 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/generic.py b/azure/functions/decorators/generic.py new file mode 100644 index 00000000..baa78806 --- /dev/null +++ b/azure/functions/decorators/generic.py @@ -0,0 +1,48 @@ +# 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 GenericInputBinding(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 GenericOutputBinding(OutputBinding): + + @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 GenericTrigger(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/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..ac52ce04 100644 --- a/docs/ProgModelSpec.pyi +++ b/docs/ProgModelSpec.pyi @@ -3,18 +3,19 @@ import typing from typing import Callable, Optional, Union, Iterable -from azure.functions import AsgiMiddleware, WsgiMiddleware -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: """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, @@ -31,6 +32,58 @@ class FunctionApp: AuthLevel. :param kwargs: Extra arguments passed to :func:`__init__`. """ + + 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. + """ + + pass + + @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. + """ + + pass + + def get_functions(self) -> typing.List[Function]: + """Get the function objects in the function app. + + :return: List of functions in the function app. + """ + pass + + 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. + """ + + 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. + """ + pass def function_name(self, name: str) -> Callable: @@ -39,20 +92,20 @@ class FunctionApp: :param name: Name of the function. :return: Decorator function. """ + pass 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 def route(self, @@ -61,7 +114,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 @@ -79,16 +135,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 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, @@ -96,7 +155,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 @@ -120,6 +180,7 @@ class FunctionApp: parameter value. :return: Decorator function. """ + pass def service_bus_queue_trigger( @@ -130,8 +191,9 @@ 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: - """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 @@ -155,6 +217,7 @@ class FunctionApp: :param cardinality: Set to many in order to enable batching. :return: Decorator function. """ + pass def write_service_bus_queue(self, @@ -164,7 +227,9 @@ 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 @@ -186,6 +251,7 @@ class FunctionApp: :param access_rights: Access rights for the connection string. :return: Decorator function. """ + pass def service_bus_topic_trigger( @@ -197,8 +263,9 @@ 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: - """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 @@ -223,6 +290,7 @@ class FunctionApp: :param cardinality: Set to many in order to enable batching. :return: Decorator function. """ + pass def write_service_bus_topic(self, @@ -233,7 +301,9 @@ 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 @@ -256,13 +326,15 @@ class FunctionApp: :param access_rights: Access rights for the connection string. :return: Decorator function. """ + pass - def on_queue_change(self, - arg_name: str, - queue_name: str, - connection: str, - data_type: Optional[DataType] = None) -> Callable: + 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 @@ -283,13 +355,15 @@ class FunctionApp: parameter value. :return: Decorator function. """ + pass 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 @@ -306,20 +380,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 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) -> Callable: - """The event_hub_message_trigger 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 @@ -342,6 +422,7 @@ class FunctionApp: group used to subscribe to events in the hub. :return: Decorator function. """ + pass def write_event_hub_message(self, @@ -349,7 +430,9 @@ 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 @@ -370,32 +453,34 @@ class FunctionApp: parameter value. :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) -> \ + 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 @@ -459,6 +544,7 @@ class FunctionApp: parameter value. :return: Decorator function. """ + pass def write_cosmos_db_documents(self, @@ -473,7 +559,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 @@ -508,6 +595,7 @@ class FunctionApp: parameter value. :return: Decorator function. """ + pass def read_cosmos_db_documents(self, @@ -519,7 +607,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 @@ -547,13 +636,15 @@ class FunctionApp: parameter value. :return: Decorator function. """ + pass - def blob_change_trigger(self, - arg_name: str, - path: str, - connection: str, - data_type: Optional[DataType] = None) -> 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 @@ -575,13 +666,16 @@ class FunctionApp: parameter value. :return: Decorator function. """ + pass 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 :class:`FunctionBuilder` object @@ -603,13 +697,16 @@ class FunctionApp: parameter value. :return: Decorator function. """ + pass 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 :class:`FunctionBuilder` object @@ -631,5 +728,97 @@ class FunctionApp: parameter value. :return: Decorator function. """ + + pass + + def generic_input_binding(self, + arg_name: str, + type: str, + data_type: Optional[Union[DataType, str]] = None, + **kwargs + ) -> Callable: + """ + 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 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 + 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 generic_output_binding(self, + arg_name: str, + type: str, + data_type: Optional[ + Union[DataType, str]] = None, + **kwargs + ) -> Callable: + """ + 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 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 + 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 generic_trigger(self, + arg_name: str, + type: str, + data_type: Optional[Union[DataType, str]] = None, + **kwargs + ) -> Callable: + """ + 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 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. + + 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..390d6b9e 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,16 @@ 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) + + 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_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_decorators.py b/tests/decorators/test_decorators.py index d349c973..f3350b46 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,192 @@ 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" + }) + + def test_custom_trigger(self): + app = self.func_app + + @app.generic_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.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 + + 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.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 + + 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.generic_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.generic_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.generic_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_generic.py b/tests/decorators/test_generic.py new file mode 100644 index 00000000..597c04db --- /dev/null +++ b/tests/decorators/test_generic.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.generic import GenericInputBinding, \ + GenericTrigger, GenericOutputBinding + + +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) + 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_generic_input_valid_creation(self): + cosmosdb_input = GenericInputBinding( + 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_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(), { + "type": BLOB, + "direction": BindingDirection.OUT, + "name": "res", + "dataType": DataType.UNDEFINED, + "path": "dummy_path", + "connection": "dummy_connection" + }) 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..4dd87e06 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, 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 @@ -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,22 @@ 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( + Trigger.is_supported_trigger_type( + GenericTrigger(name='req', type=HTTP_TRIGGER), HttpTrigger)) + + def test_is_supported_trigger_instance(self): + self.assertTrue( + Trigger.is_supported_trigger_type(HttpTrigger(name='req'), + HttpTrigger)) + + def test_is_not_supported_trigger_type(self): + self.assertFalse( + Trigger.is_supported_trigger_type( + GenericTrigger(name='req', type="dummy"), + HttpTrigger))