diff --git a/azure/functions/decorators/constants.py b/azure/functions/decorators/constants.py index 8d699b99..f1e45257 100644 --- a/azure/functions/decorators/constants.py +++ b/azure/functions/decorators/constants.py @@ -43,3 +43,5 @@ ASSISTANT_CREATE = "assistantCreate" ASSISTANT_POST = "assistantPost" SEMANTIC_SEARCH = "semanticSearch" +MYSQL = "mysql" +MYSQL_TRIGGER = "mysqlTrigger" diff --git a/azure/functions/decorators/function_app.py b/azure/functions/decorators/function_app.py index 9c0b99f6..f0feb470 100644 --- a/azure/functions/decorators/function_app.py +++ b/azure/functions/decorators/function_app.py @@ -47,6 +47,8 @@ from .warmup import WarmUpTrigger from .._http_asgi import AsgiMiddleware from .._http_wsgi import WsgiMiddleware, Context +from azure.functions.decorators.mysql import MySqlInput, MySqlOutput, \ + MySqlTrigger class Function(object): @@ -1443,6 +1445,58 @@ def decorator(): return wrap + def mysql_trigger(self, + arg_name: str, + table_name: str, + connection_string_setting: str, + leases_table_name: Optional[str] = None, + data_type: Optional[DataType] = None, + **kwargs) -> Callable[..., Any]: + """The mysql_trigger decorator adds :class:`MySqlTrigger` + to the :class:`FunctionBuilder` object + for building :class:`Function` object used in worker function + indexing model. This decorator will work only with extension bundle 4.x + and above. + This is equivalent to defining MySqlTrigger in the function.json which + enables function to be triggered when there are changes in the MySql + table. + All optional fields will be given default value by function host when + they are parsed by function host. + Ref: https://aka.ms/mysqlbindings + :param arg_name: The name of the variable that represents a + :class:`MySqlRowList` object in the function code + :param table_name: The name of the table monitored by the trigger + :param connection_string_setting: The name of an app setting that + contains the connection string for the database against which the + query or stored procedure is being executed + :param leases_table_name: The name of the table used to store + leases. If not specified, the leases table name will be + Leases_{FunctionId}_{TableId}. + :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_trigger( + trigger=MySqlTrigger( + name=arg_name, + table_name=table_name, + connection_string_setting=connection_string_setting, + leases_table_name=leases_table_name, + 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, @@ -3552,6 +3606,109 @@ def decorator(): return wrap + def mysql_input(self, + arg_name: str, + command_text: str, + connection_string_setting: str, + command_type: Optional[str] = 'Text', + parameters: Optional[str] = None, + data_type: Optional[DataType] = None, + **kwargs) -> Callable[..., Any]: + """The mysql_input decorator adds + :class:`MySqlInput` to the :class:`FunctionBuilder` object + for building :class:`Function` object used in worker function + indexing model. This decorator will work only with extension bundle 4.x + and above. + This is equivalent to defining MySqlInput in the function.json which + enables the function to read from a MySql database. + All optional fields will be given default value by function host when + they are parsed by function host. + Ref: https://aka.ms/mysqlbindings + :param arg_name: The name of the variable that represents a + :class:`MySqlRowList` input object in function code + :param command_text: The Transact-SQL query command or name of the + stored procedure executed by the binding + :param connection_string_setting: The name of an app setting that + contains the connection string for the database against which the + query or stored procedure is being executed + :param command_type: A CommandType value, which is Text for a query + and StoredProcedure for a stored procedure + :param parameters: Zero or more parameter values passed to the + command during execution as a single string. Must follow the format + @param1=param1,@param2=param2 + :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=MySqlInput( + name=arg_name, + command_text=command_text, + connection_string_setting=connection_string_setting, + command_type=command_type, + parameters=parameters, + data_type=parse_singular_param_to_enum(data_type, + DataType), + **kwargs)) + return fb + + return decorator() + + return wrap + + def mysql_output(self, + arg_name: str, + command_text: str, + connection_string_setting: str, + data_type: Optional[DataType] = None, + **kwargs) -> Callable[..., Any]: + """The mysql_output decorator adds + :class:`MySqlOutput` to the :class:`FunctionBuilder` object + for building :class:`Function` object used in worker function + indexing model. This decorator will work only with extension bundle 4.x + and above. + This is equivalent to defining MySqlOutput in the function.json which + enables the function to write to a MySql database. + All optional fields will be given default value by function host when + they are parsed by function host. + Ref: https://aka.ms/mysqlbindings + :param arg_name: The name of the variable that represents + MySql output object in function code + :param command_text: The Transact-SQL query command or name of the + stored procedure executed by the binding + :param connection_string_setting: The name of an app setting that + contains the connection string for the database against which the + query or stored procedure is being executed + :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=MySqlOutput( + name=arg_name, + command_text=command_text, + connection_string_setting=connection_string_setting, + data_type=parse_singular_param_to_enum(data_type, + DataType), + **kwargs)) + return fb + + return decorator() + + return wrap + class SettingsApi(DecoratorApi, ABC): """Interface to extend for using existing settings decorator in diff --git a/azure/functions/decorators/mysql.py b/azure/functions/decorators/mysql.py new file mode 100644 index 00000000..531e255f --- /dev/null +++ b/azure/functions/decorators/mysql.py @@ -0,0 +1,61 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +from typing import Optional + +from azure.functions.decorators.constants import MYSQL, MYSQL_TRIGGER +from azure.functions.decorators.core import DataType, InputBinding, \ + OutputBinding, Trigger + + +class MySqlInput(InputBinding): + @staticmethod + def get_binding_name() -> str: + return MYSQL + + def __init__(self, + name: str, + command_text: str, + connection_string_setting: str, + command_type: Optional[str] = 'Text', + parameters: Optional[str] = None, + data_type: Optional[DataType] = None, + **kwargs): + self.command_text = command_text + self.connection_string_setting = connection_string_setting + self.command_type = command_type + self.parameters = parameters + super().__init__(name=name, data_type=data_type) + + +class MySqlOutput(OutputBinding): + @staticmethod + def get_binding_name() -> str: + return MYSQL + + def __init__(self, + name: str, + command_text: str, + connection_string_setting: str, + data_type: Optional[DataType] = None, + **kwargs): + self.command_text = command_text + self.connection_string_setting = connection_string_setting + super().__init__(name=name, data_type=data_type) + + +class MySqlTrigger(Trigger): + @staticmethod + def get_binding_name() -> str: + return MYSQL_TRIGGER + + def __init__(self, + name: str, + table_name: str, + connection_string_setting: str, + leases_table_name: Optional[str] = None, + data_type: Optional[DataType] = None, + **kwargs): + self.table_name = table_name + self.connection_string_setting = connection_string_setting + self.leases_table_name = leases_table_name + super().__init__(name=name, data_type=data_type) diff --git a/docs/ProgModelSpec.pyi b/docs/ProgModelSpec.pyi index dd7a5e98..b9c1d60a 100644 --- a/docs/ProgModelSpec.pyi +++ b/docs/ProgModelSpec.pyi @@ -595,6 +595,45 @@ class TriggerApi(DecoratorApi, ABC): pass + def mysql_trigger(self, + arg_name: str, + table_name: str, + connection_string_setting: str, + leases_table_name: Optional[str] = None, + data_type: Optional[DataType] = None, + **kwargs) -> Callable[..., Any]: + """The mysql_trigger decorator adds :class:`MySqlTrigger` + to the :class:`FunctionBuilder` object + for building :class:`Function` object used in worker function + indexing model. This decorator will work only with extension bundle 4.x + and above. + This is equivalent to defining MySqlTrigger in the function.json which + enables function to be triggered when there are changes in the MySql + table. + All optional fields will be given default value by function host when + they are parsed by function host. + + Ref: https://aka.ms/mysqlbindings + + :param arg_name: The name of the variable that represents a + :class:`MySqlRowList` object in the function code + :param table_name: The name of the table monitored by the trigger + :param connection_string_setting: The name of an app setting that + contains the connection string for the database against which the + query or stored procedure is being executed + :param leases_table_name: The name of the table used to store + leases. If not specified, the leases table name will be + Leases_{FunctionId}_{TableId}. + :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, @@ -1112,6 +1151,77 @@ class BindingApi(DecoratorApi, ABC): pass + def mysql_input(self, + arg_name: str, + command_text: str, + connection_string_setting: str, + command_type: Optional[str] = 'Text', + parameters: Optional[str] = None, + data_type: Optional[DataType] = None, + **kwargs) -> Callable[..., Any]: + """The mysql_input decorator adds + :class:`MySqlInput` to the :class:`FunctionBuilder` object + for building :class:`Function` object used in worker function + indexing model. This decorator will work only with extension bundle 4.x + and above. + This is equivalent to defining MySqlInput in the function.json which + enables the function to read from a MySql database. + All optional fields will be given default value by function host when + they are parsed by function host. + Ref: https://aka.ms/mysqlbindings + :param arg_name: The name of the variable that represents a + :class:`MySqlRowList` input object in function code + :param command_text: The Transact-SQL query command or name of the + stored procedure executed by the binding + :param connection_string_setting: The name of an app setting that + contains the connection string for the database against which the + query or stored procedure is being executed + :param command_type: A CommandType value, which is Text for a query + and StoredProcedure for a stored procedure + :param parameters: Zero or more parameter values passed to the + command during execution as a single string. Must follow the format + @param1=param1,@param2=param2 + :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 mysql_output(self, + arg_name: str, + command_text: str, + connection_string_setting: str, + data_type: Optional[DataType] = None, + **kwargs) -> Callable[..., Any]: + """The mysql_output decorator adds + :class:`MySqlOutput` to the :class:`FunctionBuilder` object + for building :class:`Function` object used in worker function + indexing model. This decorator will work only with extension bundle 4.x + and above. + This is equivalent to defining MySqlOutput in the function.json which + enables the function to write to a MySql database. + All optional fields will be given default value by function host when + they are parsed by function host. + Ref: https://aka.ms/mysqlbindings + :param arg_name: The name of the variable that represents + MySql output object in function code + :param command_text: The Transact-SQL query command or name of the + stored procedure executed by the binding + :param connection_string_setting: The name of an app setting that + contains the connection string for the database against which the + query or stored procedure is being executed + :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_input_binding(self, arg_name: str, type: str, diff --git a/tests/decorators/test_decorators.py b/tests/decorators/test_decorators.py index acdd5ccd..e2926e25 100644 --- a/tests/decorators/test_decorators.py +++ b/tests/decorators/test_decorators.py @@ -7,7 +7,7 @@ EVENT_HUB, EVENT_HUB_TRIGGER, COSMOS_DB, COSMOS_DB_TRIGGER, BLOB, \ BLOB_TRIGGER, EVENT_GRID_TRIGGER, EVENT_GRID, TABLE, WARMUP_TRIGGER, \ SQL, SQL_TRIGGER, ORCHESTRATION_TRIGGER, ACTIVITY_TRIGGER, \ - ENTITY_TRIGGER, DURABLE_CLIENT + ENTITY_TRIGGER, DURABLE_CLIENT, MYSQL, MYSQL_TRIGGER from azure.functions.decorators.core import BlobSource, DataType, AuthLevel, \ BindingDirection, AccessRights, Cardinality from azure.functions.decorators.function_app import FunctionApp @@ -2376,3 +2376,181 @@ def test_function_app_retry_default_args(): 'max_retry_count': '2', 'delay_interval': '4' }) + + def test_mysql_default_args(self): + app = self.func_app + + @app.mysql_trigger(arg_name="trigger", + table_name="dummy_table", + connection_string_setting="dummy_setting") + @app.mysql_input(arg_name="in", + command_text="dummy_query", + connection_string_setting="dummy_setting") + @app.mysql_output(arg_name="out", + command_text="dummy_table", + connection_string_setting="dummy_setting") + def dummy(): + pass + + func = self._get_user_function(app) + + assert_json(self, func, { + "scriptFile": "function_app.py", + "bindings": [ + { + "direction": BindingDirection.OUT, + "type": MYSQL, + "name": "out", + "commandText": "dummy_table", + "connectionStringSetting": "dummy_setting" + }, + { + "direction": BindingDirection.IN, + "type": MYSQL, + "name": "in", + "commandText": "dummy_query", + "connectionStringSetting": "dummy_setting", + "commandType": "Text" + }, + { + "direction": BindingDirection.IN, + "type": MYSQL_TRIGGER, + "name": "trigger", + "tableName": "dummy_table", + "connectionStringSetting": "dummy_setting" + } + ] + }) + + def test_mysql_full_args(self): + app = self.func_app + + @app.mysql_trigger(arg_name="trigger", + table_name="dummy_table", + connection_string_setting="dummy_setting", + data_type=DataType.STRING, + dummy_field="dummy") + @app.mysql_input(arg_name="in", + command_text="dummy_query", + connection_string_setting="dummy_setting", + command_type="Text", + parameters="dummy_parameters", + data_type=DataType.STRING, + dummy_field="dummy") + @app.mysql_output(arg_name="out", + command_text="dummy_table", + connection_string_setting="dummy_setting", + data_type=DataType.STRING, + dummy_field="dummy") + def dummy(): + pass + + func = self._get_user_function(app) + + assert_json(self, func, { + "scriptFile": "function_app.py", + "bindings": [ + { + "direction": BindingDirection.OUT, + 'dummyField': 'dummy', + "dataType": DataType.STRING, + "type": MYSQL, + "name": "out", + "commandText": "dummy_table", + "connectionStringSetting": "dummy_setting" + }, + { + "direction": BindingDirection.IN, + 'dummyField': 'dummy', + "dataType": DataType.STRING, + "type": MYSQL, + "name": "in", + "commandText": "dummy_query", + "connectionStringSetting": "dummy_setting", + "parameters": "dummy_parameters", + "commandType": "Text" + }, + { + "direction": BindingDirection.IN, + 'dummyField': 'dummy', + "dataType": DataType.STRING, + "type": MYSQL_TRIGGER, + "name": "trigger", + "tableName": "dummy_table", + "connectionStringSetting": "dummy_setting" + } + ] + }) + + def test_mysql_trigger(self): + app = self.func_app + + @app.mysql_trigger(arg_name="trigger", + table_name="dummy_table", + connection_string_setting="dummy_setting") + def dummy(): + pass + + func = self._get_user_function(app) + + self.assertEqual(len(func.get_bindings()), 1) + + output = func.get_bindings()[0] + self.assertEqual(output.get_dict_repr(), { + "direction": BindingDirection.IN, + "type": MYSQL_TRIGGER, + "name": "trigger", + "tableName": "dummy_table", + "connectionStringSetting": "dummy_setting" + }) + + def test_mysql_input_binding(self): + app = self.func_app + + @app.mysql_trigger(arg_name="trigger", + table_name="dummy_table", + connection_string_setting="dummy_setting") + @app.mysql_input(arg_name="in", + command_text="dummy_query", + connection_string_setting="dummy_setting") + def dummy(): + pass + + func = self._get_user_function(app) + + self.assertEqual(len(func.get_bindings()), 2) + + output = func.get_bindings()[0] + self.assertEqual(output.get_dict_repr(), { + "direction": BindingDirection.IN, + "type": MYSQL, + "name": "in", + "commandText": "dummy_query", + "connectionStringSetting": "dummy_setting", + "commandType": "Text" + }) + + def test_mysql_output_binding(self): + app = self.func_app + + @app.mysql_trigger(arg_name="trigger", + table_name="dummy_table", + connection_string_setting="dummy_setting") + @app.mysql_output(arg_name="out", + command_text="dummy_table", + connection_string_setting="dummy_setting") + def dummy(): + pass + + func = self._get_user_function(app) + + self.assertEqual(len(func.get_bindings()), 2) + + output = func.get_bindings()[0] + self.assertEqual(output.get_dict_repr(), { + "direction": BindingDirection.OUT, + "type": MYSQL, + "name": "out", + "commandText": "dummy_table", + "connectionStringSetting": "dummy_setting", + }) diff --git a/tests/decorators/test_mysql.py b/tests/decorators/test_mysql.py new file mode 100644 index 00000000..588343ce --- /dev/null +++ b/tests/decorators/test_mysql.py @@ -0,0 +1,60 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import unittest + +from azure.functions.decorators.constants import MYSQL, MYSQL_TRIGGER +from azure.functions.decorators.core import BindingDirection, DataType +from azure.functions.decorators.mysql import MySqlInput, \ + MySqlOutput, MySqlTrigger + + +class TestMySql(unittest.TestCase): + def test_mysql_input_valid_creation(self): + input = MySqlInput(name="req", + command_text="dummy_query", + connection_string_setting="dummy_setting", + data_type=DataType.UNDEFINED, + dummy_field="dummy") + self.assertEqual(input.get_binding_name(), "mysql") + self.assertEqual(input.get_dict_repr(), + {"commandText": "dummy_query", + "connectionStringSetting": "dummy_setting", + "commandType": "Text", + "dataType": DataType.UNDEFINED, + "direction": BindingDirection.IN, + "dummyField": "dummy", + "name": "req", + "type": MYSQL}) + + def test_mysql_output_valid_creation(self): + output = MySqlOutput(name="req", + command_text="dummy_table", + connection_string_setting="dummy_setting", + data_type=DataType.UNDEFINED, + dummy_field="dummy") + self.assertEqual(output.get_binding_name(), "mysql") + self.assertEqual(output.get_dict_repr(), + {"commandText": "dummy_table", + "connectionStringSetting": "dummy_setting", + "dataType": DataType.UNDEFINED, + "direction": BindingDirection.OUT, + "dummyField": "dummy", + "name": "req", + "type": MYSQL}) + + def test_mysql_trigger_valid_creation(self): + trigger = MySqlTrigger(name="req", + table_name="dummy_table", + connection_string_setting="dummy_setting", + data_type=DataType.UNDEFINED, + dummy_field="dummy") + + self.assertEqual(trigger.get_binding_name(), "mysqlTrigger") + self.assertEqual(trigger.get_dict_repr(), + {"connectionStringSetting": "dummy_setting", + "dataType": DataType.UNDEFINED, + "tableName": "dummy_table", + "direction": BindingDirection.IN, + "dummyField": "dummy", + "name": "req", + "type": MYSQL_TRIGGER})