From f5bf9c28b8d8543f181a0711cb67c060a84362c4 Mon Sep 17 00:00:00 2001 From: Lucy Zhang Date: Fri, 14 Jul 2023 08:26:24 -0700 Subject: [PATCH 1/8] add sql decorator --- azure/functions/decorators/constants.py | 2 + azure/functions/decorators/function_app.py | 159 +++++++++++++++++++++ azure/functions/decorators/sql.py | 57 ++++++++ 3 files changed, 218 insertions(+) create mode 100644 azure/functions/decorators/sql.py diff --git a/azure/functions/decorators/constants.py b/azure/functions/decorators/constants.py index fde60864..fd6fb7d4 100644 --- a/azure/functions/decorators/constants.py +++ b/azure/functions/decorators/constants.py @@ -19,3 +19,5 @@ EVENT_GRID_TRIGGER = "eventGridTrigger" EVENT_GRID = "eventGrid" TABLE = "table" +SQL = "sql" +SQL_TRIGGER = "sqlTrigger" diff --git a/azure/functions/decorators/function_app.py b/azure/functions/decorators/function_app.py index 3e19d7e3..1827defe 100644 --- a/azure/functions/decorators/function_app.py +++ b/azure/functions/decorators/function_app.py @@ -23,6 +23,7 @@ from azure.functions.decorators.servicebus import ServiceBusQueueTrigger, \ ServiceBusQueueOutput, ServiceBusTopicTrigger, \ ServiceBusTopicOutput +from azure.functions.decorators.sql import SqlTrigger, SqlInput, SqlOutput from azure.functions.decorators.table import TableInput, TableOutput from azure.functions.decorators.timer import TimerTrigger from azure.functions.decorators.utils import parse_singular_param_to_enum, \ @@ -1069,6 +1070,55 @@ def decorator(): return wrap + def sql_trigger(self, + arg_name: str, + tableName: str, + connectionStringSetting: str, + data_type: Optional[DataType] = None, + **kwargs) -> Callable[..., Any]: + """The sql_trigger decorator adds :class:`SqlTrigger` + 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 SqlTrigger in the function.json which + enables function to be triggered when there are changes in the Sql table. + All optional fields will be given default value by function host when + they are parsed by function host. + + Ref: https://aka.ms/sqlbindings + + :param arg_name: The name of the variable that represents a + :class:`SqlRowList` object in the function code + :param tableName: The name of the table monitored by the trigger + :param connectionStringSetting: 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_trigger( + trigger=SqlTrigger( + name=arg_name, + tableName=tableName, + connectionStringSetting=connectionStringSetting, + 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, @@ -1859,6 +1909,115 @@ def decorator(): return wrap + def sql_input(self, + arg_name: str, + commandText: str, + connectionStringSetting: str, + commandType: Optional[str] = 'Text', + parameters: Optional[str] = None, + data_type: Optional[DataType] = None, + **kwargs) -> Callable[..., Any]: + """The sql_input decorator adds + :class:`SqlInput` 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 SqlInput in the function.json which + enables the function to read from a Sql database. + All optional fields will be given default value by function host when + they are parsed by function host. + + Ref: https://aka.ms/sqlbindings + + :param arg_name: The name of the variable that represents a + :class:`SqlRowList` input object in function code + :param commandText: The Transact-SQL query command or name of the + stored procedure executed by the binding + :param connectionStringSetting: 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 commandType: 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=SqlInput( + name=arg_name, + commandText=commandText, + connectionStringSetting=connectionStringSetting, + commandType=commandType, + parameters=parameters, + data_type=parse_singular_param_to_enum(data_type, + DataType), + **kwargs)) + return fb + + return decorator() + + return wrap + + def sql_output(self, + arg_name: str, + commandText: str, + connectionStringSetting: str, + data_type: Optional[DataType] = None, + **kwargs) -> Callable[..., Any]: + """The sql_output decorator adds + :class:`SqlOutput` 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 SqlOutput in the function.json which + enables the function to write to a Sql database. + All optional fields will be given default value by function host when + they are parsed by function host. + + Ref: https://aka.ms/sqlbindings + + :param arg_name: The name of the variable that represents + Sql output object in function code + :param commandText: The Transact-SQL query command or name of the + stored procedure executed by the binding + :param connectionStringSetting: 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=SqlOutput( + name=arg_name, + commandText=commandText, + connectionStringSetting=connectionStringSetting, + data_type=parse_singular_param_to_enum(data_type, + DataType), + **kwargs)) + return fb + + return decorator() + + return wrap + def generic_input_binding(self, arg_name: str, type: str, diff --git a/azure/functions/decorators/sql.py b/azure/functions/decorators/sql.py new file mode 100644 index 00000000..e2a0c6db --- /dev/null +++ b/azure/functions/decorators/sql.py @@ -0,0 +1,57 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +from typing import Optional + +from azure.functions.decorators.constants import SQL, SQL_TRIGGER +from azure.functions.decorators.core import DataType, InputBinding, \ + OutputBinding, Trigger + +class SqlInput(InputBinding): + @staticmethod + def get_binding_name() -> str: + return SQL + + def __init__(self, + name: str, + commandText: str, + connectionStringSetting: str, + commandType: Optional[str] = 'Text', + parameters: Optional[str] = None, + data_type: Optional[DataType] = None, + **kwargs): + self.commandText = commandText + self.connectionStringSetting = connectionStringSetting + self.commandType = commandType + self.parameters = parameters + super().__init__(name=name, data_type=data_type) + + +class SqlOutput(OutputBinding): + @staticmethod + def get_binding_name() -> str: + return SQL + + def __init__(self, + name: str, + commandText: str, + connectionStringSetting: str, + data_type: Optional[DataType] = None, + **kwargs): + self.commandText = commandText + self.connectionStringSetting = connectionStringSetting + super().__init__(name=name, data_type=data_type) + +class SqlTrigger(Trigger): + @staticmethod + def get_binding_name() -> str: + return SQL_TRIGGER + + def __init__(self, + name: str, + tableName: str, + connectionStringSetting: str, + data_type: Optional[DataType] = None, + **kwargs): + self.tableName = tableName + self.connectionStringSetting = connectionStringSetting + super().__init__(name=name, data_type=data_type) \ No newline at end of file From 2b1f37c1f1720ec4691b93029d60e2aaf8326742 Mon Sep 17 00:00:00 2001 From: Lucy Zhang Date: Fri, 14 Jul 2023 10:01:43 -0700 Subject: [PATCH 2/8] add tests + update param names --- azure/functions/decorators/function_app.py | 65 ++++---- azure/functions/decorators/sql.py | 52 +++--- tests/decorators/test_decorators.py | 179 ++++++++++++++++++++- tests/decorators/test_sql.py | 59 +++++++ 4 files changed, 297 insertions(+), 58 deletions(-) create mode 100644 tests/decorators/test_sql.py diff --git a/azure/functions/decorators/function_app.py b/azure/functions/decorators/function_app.py index 1827defe..53bfc215 100644 --- a/azure/functions/decorators/function_app.py +++ b/azure/functions/decorators/function_app.py @@ -1072,8 +1072,8 @@ def decorator(): def sql_trigger(self, arg_name: str, - tableName: str, - connectionStringSetting: str, + table_name: str, + connection_string_setting: str, data_type: Optional[DataType] = None, **kwargs) -> Callable[..., Any]: """The sql_trigger decorator adds :class:`SqlTrigger` @@ -1082,7 +1082,8 @@ def sql_trigger(self, indexing model. This decorator will work only with extension bundle 4.x and above. This is equivalent to defining SqlTrigger in the function.json which - enables function to be triggered when there are changes in the Sql table. + enables function to be triggered when there are changes in the Sql + table. All optional fields will be given default value by function host when they are parsed by function host. @@ -1090,8 +1091,8 @@ def sql_trigger(self, :param arg_name: The name of the variable that represents a :class:`SqlRowList` object in the function code - :param tableName: The name of the table monitored by the trigger - :param connectionStringSetting: The name of an app setting that + :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 data_type: Defines how Functions runtime should treat the @@ -1108,10 +1109,10 @@ def decorator(): fb.add_trigger( trigger=SqlTrigger( name=arg_name, - tableName=tableName, - connectionStringSetting=connectionStringSetting, + table_name=table_name, + connection_string_setting=connection_string_setting, data_type=parse_singular_param_to_enum(data_type, - DataType), + DataType), **kwargs)) return fb @@ -1910,13 +1911,13 @@ def decorator(): return wrap def sql_input(self, - arg_name: str, - commandText: str, - connectionStringSetting: str, - commandType: Optional[str] = 'Text', - parameters: Optional[str] = None, - data_type: Optional[DataType] = None, - **kwargs) -> Callable[..., Any]: + 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 sql_input decorator adds :class:`SqlInput` to the :class:`FunctionBuilder` object for building :class:`Function` object used in worker function @@ -1931,12 +1932,12 @@ def sql_input(self, :param arg_name: The name of the variable that represents a :class:`SqlRowList` input object in function code - :param commandText: The Transact-SQL query command or name of the + :param command_text: The Transact-SQL query command or name of the stored procedure executed by the binding - :param connectionStringSetting: The name of an app setting that + :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 commandType: A CommandType value, which is Text for a query + :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 @@ -1955,12 +1956,12 @@ def decorator(): fb.add_binding( binding=SqlInput( name=arg_name, - commandText=commandText, - connectionStringSetting=connectionStringSetting, - commandType=commandType, + 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), + DataType), **kwargs)) return fb @@ -1969,11 +1970,11 @@ def decorator(): return wrap def sql_output(self, - arg_name: str, - commandText: str, - connectionStringSetting: str, - data_type: Optional[DataType] = None, - **kwargs) -> Callable[..., Any]: + arg_name: str, + command_text: str, + connection_string_setting: str, + data_type: Optional[DataType] = None, + **kwargs) -> Callable[..., Any]: """The sql_output decorator adds :class:`SqlOutput` to the :class:`FunctionBuilder` object for building :class:`Function` object used in worker function @@ -1988,9 +1989,9 @@ def sql_output(self, :param arg_name: The name of the variable that represents Sql output object in function code - :param commandText: The Transact-SQL query command or name of the + :param command_text: The Transact-SQL query command or name of the stored procedure executed by the binding - :param connectionStringSetting: The name of an app setting that + :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 @@ -2007,10 +2008,10 @@ def decorator(): fb.add_binding( binding=SqlOutput( name=arg_name, - commandText=commandText, - connectionStringSetting=connectionStringSetting, + command_text=command_text, + connection_string_setting=connection_string_setting, data_type=parse_singular_param_to_enum(data_type, - DataType), + DataType), **kwargs)) return fb diff --git a/azure/functions/decorators/sql.py b/azure/functions/decorators/sql.py index e2a0c6db..eba0ba35 100644 --- a/azure/functions/decorators/sql.py +++ b/azure/functions/decorators/sql.py @@ -6,22 +6,23 @@ from azure.functions.decorators.core import DataType, InputBinding, \ OutputBinding, Trigger + class SqlInput(InputBinding): @staticmethod def get_binding_name() -> str: return SQL def __init__(self, - name: str, - commandText: str, - connectionStringSetting: str, - commandType: Optional[str] = 'Text', - parameters: Optional[str] = None, - data_type: Optional[DataType] = None, - **kwargs): - self.commandText = commandText - self.connectionStringSetting = connectionStringSetting - self.commandType = commandType + 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) @@ -32,26 +33,27 @@ def get_binding_name() -> str: return SQL def __init__(self, - name: str, - commandText: str, - connectionStringSetting: str, - data_type: Optional[DataType] = None, - **kwargs): - self.commandText = commandText - self.connectionStringSetting = connectionStringSetting + 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 SqlTrigger(Trigger): @staticmethod def get_binding_name() -> str: return SQL_TRIGGER def __init__(self, - name: str, - tableName: str, - connectionStringSetting: str, - data_type: Optional[DataType] = None, - **kwargs): - self.tableName = tableName - self.connectionStringSetting = connectionStringSetting - super().__init__(name=name, data_type=data_type) \ No newline at end of file + name: str, + table_name: str, + connection_string_setting: str, + data_type: Optional[DataType] = None, + **kwargs): + self.table_name = table_name + self.connection_string_setting = connection_string_setting + super().__init__(name=name, data_type=data_type) diff --git a/tests/decorators/test_decorators.py b/tests/decorators/test_decorators.py index bb09fda4..ef6081b1 100644 --- a/tests/decorators/test_decorators.py +++ b/tests/decorators/test_decorators.py @@ -5,7 +5,8 @@ from azure.functions.decorators.constants import TIMER_TRIGGER, HTTP_TRIGGER, \ HTTP_OUTPUT, QUEUE, QUEUE_TRIGGER, SERVICE_BUS, SERVICE_BUS_TRIGGER, \ EVENT_HUB, EVENT_HUB_TRIGGER, COSMOS_DB, COSMOS_DB_TRIGGER, BLOB, \ - BLOB_TRIGGER, EVENT_GRID_TRIGGER, EVENT_GRID, TABLE, WARMUP_TRIGGER + BLOB_TRIGGER, EVENT_GRID_TRIGGER, EVENT_GRID, TABLE, WARMUP_TRIGGER, \ + SQL, SQL_TRIGGER from azure.functions.decorators.core import DataType, AuthLevel, \ BindingDirection, AccessRights, Cardinality from azure.functions.decorators.function_app import FunctionApp @@ -2054,6 +2055,182 @@ def dummy(): "connection": "dummy_out_conn" }) + def test_sql_default_args(self): + app = self.func_app + + @app.sql_trigger(arg_name="trigger", + table_name="dummy_table", + connection_string_setting="dummy_setting") + @app.sql_input(arg_name="in", + command_text="dummy_query", + connection_string_setting="dummy_setting") + @app.sql_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": SQL, + "name": "out", + "commandText": "dummy_table", + "connectionStringSetting": "dummy_setting" + }, + { + "direction": BindingDirection.IN, + "type": SQL, + "name": "in", + "commandText": "dummy_query", + "connectionStringSetting": "dummy_setting" + }, + { + "direction": BindingDirection.IN, + "type": SQL_TRIGGER, + "name": "trigger", + "tableName": "dummy_table", + "connectionStringSetting": "dummy_setting" + } + ] + }) + + def test_sql_full_args(self): + app = self.func_app + + @app.sql_trigger(arg_name="trigger", + table_name="dummy_table", + connection_string_setting="dummy_setting", + data_type=DataType.STRING, + dummy_field="dummy") + @app.sql_input(arg_name="in", + command_text="dummy_query", + connection_string_setting="dummy_setting", + command_type="dummy_type", + parameters="dummy_parameters", + data_type=DataType.STRING, + dummy_field="dummy") + @app.sql_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": SQL, + "name": "out", + "commandText": "dummy_table", + "connectionStringSetting": "dummy_setting" + }, + { + "direction": BindingDirection.IN, + 'dummyField': 'dummy', + "dataType": DataType.STRING, + "type": SQL, + "name": "in", + "commandText": "dummy_query", + "connectionStringSetting": "dummy_setting", + "commandType": "dummy_type", + "parameters": "dummy_parameters" + }, + { + "direction": BindingDirection.IN, + 'dummyField': 'dummy', + "dataType": DataType.STRING, + "type": SQL_TRIGGER, + "name": "trigger", + "tableName": "dummy_table", + "connectionStringSetting": "dummy_setting" + } + ] + }) + + def test_sql_trigger(self): + app = self.func_app + + @app.sql_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": SQL_TRIGGER, + "name": "trigger", + "tableName": "dummy_table", + "connectionStringSetting": "dummy_setting" + }) + + def test_sql_input_binding(self): + app = self.func_app + + @app.sql_trigger(arg_name="trigger", + table_name="dummy_table", + connection_string_setting="dummy_setting") + @app.sql_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": SQL, + "name": "in", + "commandText": "dummy_query", + "connectionStringSetting": "dummy_setting" + }) + + def test_sql_output_binding(self): + app = self.func_app + + @app.sql_trigger(arg_name="trigger", + table_name="dummy_table", + connection_string_setting="dummy_setting") + @app.sql_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": SQL, + "name": "out", + "commandText": "dummy_table", + "connectionStringSetting": "dummy_setting" + }) + def test_function_app_full_bindings_metadata_key_order(self): app = self.func_app diff --git a/tests/decorators/test_sql.py b/tests/decorators/test_sql.py new file mode 100644 index 00000000..1b5a8a77 --- /dev/null +++ b/tests/decorators/test_sql.py @@ -0,0 +1,59 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import unittest + +from azure.functions.decorators.constants import SQL_TRIGGER, SQL +from azure.functions.decorators.core import BindingDirection, DataType +from azure.functions.decorators.sql import SqlTrigger, \ + SqlInput, SqlOutput + + +class TestSql(unittest.TestCase): + def test_sql_trigger_valid_creation(self): + trigger = SqlTrigger(name="req", + table_name="dummy_table", + connection_string_setting="dummy_setting", + data_type=DataType.UNDEFINED, + dummy_field="dummy") + + self.assertEqual(trigger.get_binding_name(), "sqlTrigger") + self.assertEqual(trigger.get_dict_repr(), + {"connectionStringSetting": "dummy_setting", + "dataType": DataType.UNDEFINED, + "tableName": "dummy_table", + "direction": BindingDirection.IN, + "dummyField": "dummy", + "name": "req", + "type": SQL_TRIGGER}) + + def test_sql_output_valid_creation(self): + output = SqlOutput(name="req", + command_text="dummy_table", + connection_string_setting="dummy_setting", + data_type=DataType.UNDEFINED, + dummy_field="dummy") + self.assertEqual(output.get_binding_name(), "sql") + self.assertEqual(output.get_dict_repr(), + {"commandText": "dummy_table", + "connectionStringSetting": "dummy_setting", + "dataType": DataType.UNDEFINED, + "direction": BindingDirection.OUT, + "dummyField": "dummy", + "name": "req", + "type": SQL}) + + def test_sql_input_valid_creation(self): + input = SqlInput(name="req", + command_text="dummy_query", + connection_string_setting="dummy_setting", + data_type=DataType.UNDEFINED, + dummy_field="dummy") + self.assertEqual(input.get_binding_name(), "sql") + self.assertEqual(input.get_dict_repr(), + {"commandText": "dummy_query", + "connectionStringSetting": "dummy_setting", + "dataType": DataType.UNDEFINED, + "direction": BindingDirection.IN, + "dummyField": "dummy", + "name": "req", + "type": SQL}) From 922e0bd776f958a7d8de44df0aa25ace8f807cff Mon Sep 17 00:00:00 2001 From: Lucy Zhang Date: Fri, 14 Jul 2023 10:10:27 -0700 Subject: [PATCH 3/8] add commandType to tests --- tests/decorators/test_decorators.py | 114 +++++++++++++++------------- tests/decorators/test_sql.py | 16 ++-- 2 files changed, 69 insertions(+), 61 deletions(-) diff --git a/tests/decorators/test_decorators.py b/tests/decorators/test_decorators.py index ef6081b1..ea032e10 100644 --- a/tests/decorators/test_decorators.py +++ b/tests/decorators/test_decorators.py @@ -2075,28 +2075,30 @@ def dummy(): assert_json(self, func, { "scriptFile": "function_app.py", "bindings": [ - { - "direction": BindingDirection.OUT, - "type": SQL, - "name": "out", - "commandText": "dummy_table", - "connectionStringSetting": "dummy_setting" - }, - { - "direction": BindingDirection.IN, - "type": SQL, - "name": "in", - "commandText": "dummy_query", - "connectionStringSetting": "dummy_setting" - }, - { - "direction": BindingDirection.IN, - "type": SQL_TRIGGER, - "name": "trigger", - "tableName": "dummy_table", - "connectionStringSetting": "dummy_setting" - } - ] + { + "direction": BindingDirection.OUT, + "type": SQL, + "name": "out", + "commandText": "dummy_table", + "connectionStringSetting": "dummy_setting", + "commandType": "Text" + }, + { + "direction": BindingDirection.IN, + "type": SQL, + "name": "in", + "commandText": "dummy_query", + "connectionStringSetting": "dummy_setting", + "commandType": "Text" + }, + { + "direction": BindingDirection.IN, + "type": SQL_TRIGGER, + "name": "trigger", + "tableName": "dummy_table", + "connectionStringSetting": "dummy_setting" + } + ] }) def test_sql_full_args(self): @@ -2127,36 +2129,38 @@ def dummy(): assert_json(self, func, { "scriptFile": "function_app.py", "bindings": [ - { - "direction": BindingDirection.OUT, - 'dummyField': 'dummy', - "dataType": DataType.STRING, - "type": SQL, - "name": "out", - "commandText": "dummy_table", - "connectionStringSetting": "dummy_setting" - }, - { - "direction": BindingDirection.IN, - 'dummyField': 'dummy', - "dataType": DataType.STRING, - "type": SQL, - "name": "in", - "commandText": "dummy_query", - "connectionStringSetting": "dummy_setting", - "commandType": "dummy_type", - "parameters": "dummy_parameters" - }, - { - "direction": BindingDirection.IN, - 'dummyField': 'dummy', - "dataType": DataType.STRING, - "type": SQL_TRIGGER, - "name": "trigger", - "tableName": "dummy_table", - "connectionStringSetting": "dummy_setting" - } - ] + { + "direction": BindingDirection.OUT, + 'dummyField': 'dummy', + "dataType": DataType.STRING, + "type": SQL, + "name": "out", + "commandText": "dummy_table", + "connectionStringSetting": "dummy_setting", + "commandType": "Text" + }, + { + "direction": BindingDirection.IN, + 'dummyField': 'dummy', + "dataType": DataType.STRING, + "type": SQL, + "name": "in", + "commandText": "dummy_query", + "connectionStringSetting": "dummy_setting", + "commandType": "dummy_type", + "parameters": "dummy_parameters", + "commandType": "Text" + }, + { + "direction": BindingDirection.IN, + 'dummyField': 'dummy', + "dataType": DataType.STRING, + "type": SQL_TRIGGER, + "name": "trigger", + "tableName": "dummy_table", + "connectionStringSetting": "dummy_setting" + } + ] }) def test_sql_trigger(self): @@ -2203,7 +2207,8 @@ def dummy(): "type": SQL, "name": "in", "commandText": "dummy_query", - "connectionStringSetting": "dummy_setting" + "connectionStringSetting": "dummy_setting", + "commandType": "Text" }) def test_sql_output_binding(self): @@ -2228,7 +2233,8 @@ def dummy(): "type": SQL, "name": "out", "commandText": "dummy_table", - "connectionStringSetting": "dummy_setting" + "connectionStringSetting": "dummy_setting", + "commandType": "Text" }) def test_function_app_full_bindings_metadata_key_order(self): diff --git a/tests/decorators/test_sql.py b/tests/decorators/test_sql.py index 1b5a8a77..46132463 100644 --- a/tests/decorators/test_sql.py +++ b/tests/decorators/test_sql.py @@ -18,13 +18,13 @@ def test_sql_trigger_valid_creation(self): self.assertEqual(trigger.get_binding_name(), "sqlTrigger") self.assertEqual(trigger.get_dict_repr(), - {"connectionStringSetting": "dummy_setting", - "dataType": DataType.UNDEFINED, - "tableName": "dummy_table", - "direction": BindingDirection.IN, - "dummyField": "dummy", - "name": "req", - "type": SQL_TRIGGER}) + {"connectionStringSetting": "dummy_setting", + "dataType": DataType.UNDEFINED, + "tableName": "dummy_table", + "direction": BindingDirection.IN, + "dummyField": "dummy", + "name": "req", + "type": SQL_TRIGGER}) def test_sql_output_valid_creation(self): output = SqlOutput(name="req", @@ -36,6 +36,7 @@ def test_sql_output_valid_creation(self): self.assertEqual(output.get_dict_repr(), {"commandText": "dummy_table", "connectionStringSetting": "dummy_setting", + "commandType": "Text", "dataType": DataType.UNDEFINED, "direction": BindingDirection.OUT, "dummyField": "dummy", @@ -52,6 +53,7 @@ def test_sql_input_valid_creation(self): self.assertEqual(input.get_dict_repr(), {"commandText": "dummy_query", "connectionStringSetting": "dummy_setting", + "commandType": "Text", "dataType": DataType.UNDEFINED, "direction": BindingDirection.IN, "dummyField": "dummy", From 19bb93407908f8585acda5260dff39ed068ffb76 Mon Sep 17 00:00:00 2001 From: Lucy Zhang Date: Fri, 14 Jul 2023 14:18:49 -0700 Subject: [PATCH 4/8] fix indentation --- tests/decorators/test_decorators.py | 123 ++++++++++++++-------------- tests/decorators/test_sql.py | 12 +-- 2 files changed, 67 insertions(+), 68 deletions(-) diff --git a/tests/decorators/test_decorators.py b/tests/decorators/test_decorators.py index ea032e10..e6c36d15 100644 --- a/tests/decorators/test_decorators.py +++ b/tests/decorators/test_decorators.py @@ -2073,33 +2073,33 @@ def dummy(): func = self._get_user_function(app) assert_json(self, func, { - "scriptFile": "function_app.py", - "bindings": [ - { - "direction": BindingDirection.OUT, - "type": SQL, - "name": "out", - "commandText": "dummy_table", - "connectionStringSetting": "dummy_setting", - "commandType": "Text" - }, - { - "direction": BindingDirection.IN, - "type": SQL, - "name": "in", - "commandText": "dummy_query", - "connectionStringSetting": "dummy_setting", - "commandType": "Text" - }, - { - "direction": BindingDirection.IN, - "type": SQL_TRIGGER, - "name": "trigger", - "tableName": "dummy_table", - "connectionStringSetting": "dummy_setting" - } - ] - }) + "scriptFile": "function_app.py", + "bindings": [ + { + "direction": BindingDirection.OUT, + "type": SQL, + "name": "out", + "commandText": "dummy_table", + "connectionStringSetting": "dummy_setting", + "commandType": "Text" + }, + { + "direction": BindingDirection.IN, + "type": SQL, + "name": "in", + "commandText": "dummy_query", + "connectionStringSetting": "dummy_setting", + "commandType": "Text" + }, + { + "direction": BindingDirection.IN, + "type": SQL_TRIGGER, + "name": "trigger", + "tableName": "dummy_table", + "connectionStringSetting": "dummy_setting" + } + ] + }) def test_sql_full_args(self): app = self.func_app @@ -2127,41 +2127,40 @@ def dummy(): func = self._get_user_function(app) assert_json(self, func, { - "scriptFile": "function_app.py", - "bindings": [ - { - "direction": BindingDirection.OUT, - 'dummyField': 'dummy', - "dataType": DataType.STRING, - "type": SQL, - "name": "out", - "commandText": "dummy_table", - "connectionStringSetting": "dummy_setting", - "commandType": "Text" - }, - { - "direction": BindingDirection.IN, - 'dummyField': 'dummy', - "dataType": DataType.STRING, - "type": SQL, - "name": "in", - "commandText": "dummy_query", - "connectionStringSetting": "dummy_setting", - "commandType": "dummy_type", - "parameters": "dummy_parameters", - "commandType": "Text" - }, - { - "direction": BindingDirection.IN, - 'dummyField': 'dummy', - "dataType": DataType.STRING, - "type": SQL_TRIGGER, - "name": "trigger", - "tableName": "dummy_table", - "connectionStringSetting": "dummy_setting" - } - ] - }) + "scriptFile": "function_app.py", + "bindings": [ + { + "direction": BindingDirection.OUT, + 'dummyField': 'dummy', + "dataType": DataType.STRING, + "type": SQL, + "name": "out", + "commandText": "dummy_table", + "connectionStringSetting": "dummy_setting", + "commandType": "Text" + }, + { + "direction": BindingDirection.IN, + 'dummyField': 'dummy', + "dataType": DataType.STRING, + "type": SQL, + "name": "in", + "commandText": "dummy_query", + "connectionStringSetting": "dummy_setting", + "parameters": "dummy_parameters", + "commandType": "Text" + }, + { + "direction": BindingDirection.IN, + 'dummyField': 'dummy', + "dataType": DataType.STRING, + "type": SQL_TRIGGER, + "name": "trigger", + "tableName": "dummy_table", + "connectionStringSetting": "dummy_setting" + } + ] + }) def test_sql_trigger(self): app = self.func_app diff --git a/tests/decorators/test_sql.py b/tests/decorators/test_sql.py index 46132463..0b3d786c 100644 --- a/tests/decorators/test_sql.py +++ b/tests/decorators/test_sql.py @@ -19,12 +19,12 @@ def test_sql_trigger_valid_creation(self): self.assertEqual(trigger.get_binding_name(), "sqlTrigger") self.assertEqual(trigger.get_dict_repr(), {"connectionStringSetting": "dummy_setting", - "dataType": DataType.UNDEFINED, - "tableName": "dummy_table", - "direction": BindingDirection.IN, - "dummyField": "dummy", - "name": "req", - "type": SQL_TRIGGER}) + "dataType": DataType.UNDEFINED, + "tableName": "dummy_table", + "direction": BindingDirection.IN, + "dummyField": "dummy", + "name": "req", + "type": SQL_TRIGGER}) def test_sql_output_valid_creation(self): output = SqlOutput(name="req", From 510cb41f64d35385cd5f9361297549c84656f23e Mon Sep 17 00:00:00 2001 From: Lucy Zhang Date: Fri, 14 Jul 2023 14:22:53 -0700 Subject: [PATCH 5/8] fix output binding tests --- tests/decorators/test_decorators.py | 7 ++----- tests/decorators/test_sql.py | 1 - 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/decorators/test_decorators.py b/tests/decorators/test_decorators.py index e6c36d15..dd111246 100644 --- a/tests/decorators/test_decorators.py +++ b/tests/decorators/test_decorators.py @@ -2080,8 +2080,7 @@ def dummy(): "type": SQL, "name": "out", "commandText": "dummy_table", - "connectionStringSetting": "dummy_setting", - "commandType": "Text" + "connectionStringSetting": "dummy_setting" }, { "direction": BindingDirection.IN, @@ -2136,8 +2135,7 @@ def dummy(): "type": SQL, "name": "out", "commandText": "dummy_table", - "connectionStringSetting": "dummy_setting", - "commandType": "Text" + "connectionStringSetting": "dummy_setting" }, { "direction": BindingDirection.IN, @@ -2233,7 +2231,6 @@ def dummy(): "name": "out", "commandText": "dummy_table", "connectionStringSetting": "dummy_setting", - "commandType": "Text" }) def test_function_app_full_bindings_metadata_key_order(self): diff --git a/tests/decorators/test_sql.py b/tests/decorators/test_sql.py index 0b3d786c..7fbac8e4 100644 --- a/tests/decorators/test_sql.py +++ b/tests/decorators/test_sql.py @@ -36,7 +36,6 @@ def test_sql_output_valid_creation(self): self.assertEqual(output.get_dict_repr(), {"commandText": "dummy_table", "connectionStringSetting": "dummy_setting", - "commandType": "Text", "dataType": DataType.UNDEFINED, "direction": BindingDirection.OUT, "dummyField": "dummy", From 1e99ab5a0c733d9ab1661c2147b99858ab24c495 Mon Sep 17 00:00:00 2001 From: Lucy Zhang Date: Fri, 14 Jul 2023 14:25:27 -0700 Subject: [PATCH 6/8] fix input binding test --- tests/decorators/test_decorators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/decorators/test_decorators.py b/tests/decorators/test_decorators.py index dd111246..d06fd3ae 100644 --- a/tests/decorators/test_decorators.py +++ b/tests/decorators/test_decorators.py @@ -2111,7 +2111,7 @@ def test_sql_full_args(self): @app.sql_input(arg_name="in", command_text="dummy_query", connection_string_setting="dummy_setting", - command_type="dummy_type", + command_type="Text", parameters="dummy_parameters", data_type=DataType.STRING, dummy_field="dummy") From 4859b7fea97615e01b4931ecd5e8025d759acb36 Mon Sep 17 00:00:00 2001 From: Lucy Zhang Date: Fri, 21 Jul 2023 09:26:11 -0700 Subject: [PATCH 7/8] update ProgModelSpec.pyi --- docs/ProgModelSpec.pyi | 114 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 113 insertions(+), 1 deletion(-) diff --git a/docs/ProgModelSpec.pyi b/docs/ProgModelSpec.pyi index 857520c1..d6426e40 100644 --- a/docs/ProgModelSpec.pyi +++ b/docs/ProgModelSpec.pyi @@ -548,7 +548,42 @@ class TriggerApi(DecoratorApi, ABC): """ pass - + + def sql_trigger(self, + arg_name: str, + table_name: str, + connection_string_setting: str, + data_type: Optional[DataType] = None, + **kwargs) -> Callable[..., Any]: + """The sql_trigger decorator adds :class:`SqlTrigger` + 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 SqlTrigger in the function.json which + enables function to be triggered when there are changes in the Sql + table. + All optional fields will be given default value by function host when + they are parsed by function host. + + Ref: https://aka.ms/sqlbindings + + :param arg_name: The name of the variable that represents a + :class:`SqlRowList` 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 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, @@ -989,6 +1024,83 @@ class BindingApi(DecoratorApi, ABC): pass + def sql_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 sql_input decorator adds + :class:`SqlInput` 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 SqlInput in the function.json which + enables the function to read from a Sql database. + All optional fields will be given default value by function host when + they are parsed by function host. + + Ref: https://aka.ms/sqlbindings + + :param arg_name: The name of the variable that represents a + :class:`SqlRowList` 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 sql_output(self, + arg_name: str, + command_text: str, + connection_string_setting: str, + data_type: Optional[DataType] = None, + **kwargs) -> Callable[..., Any]: + """The sql_output decorator adds + :class:`SqlOutput` 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 SqlOutput in the function.json which + enables the function to write to a Sql database. + All optional fields will be given default value by function host when + they are parsed by function host. + + Ref: https://aka.ms/sqlbindings + + :param arg_name: The name of the variable that represents + Sql 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, From 286f6861d107290111295da293390d275958cbf6 Mon Sep 17 00:00:00 2001 From: Lucy Zhang Date: Tue, 1 Aug 2023 08:18:06 -0700 Subject: [PATCH 8/8] add leases_table_name to sqltrigger --- azure/functions/decorators/function_app.py | 5 +++++ azure/functions/decorators/sql.py | 2 ++ docs/ProgModelSpec.pyi | 4 ++++ 3 files changed, 11 insertions(+) diff --git a/azure/functions/decorators/function_app.py b/azure/functions/decorators/function_app.py index 77fe241e..3af406dc 100644 --- a/azure/functions/decorators/function_app.py +++ b/azure/functions/decorators/function_app.py @@ -1074,6 +1074,7 @@ def sql_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 sql_trigger decorator adds :class:`SqlTrigger` @@ -1095,6 +1096,9 @@ def sql_trigger(self, :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 @@ -1111,6 +1115,7 @@ def decorator(): 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)) diff --git a/azure/functions/decorators/sql.py b/azure/functions/decorators/sql.py index eba0ba35..be4b79da 100644 --- a/azure/functions/decorators/sql.py +++ b/azure/functions/decorators/sql.py @@ -52,8 +52,10 @@ 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 d6426e40..a01d8168 100644 --- a/docs/ProgModelSpec.pyi +++ b/docs/ProgModelSpec.pyi @@ -553,6 +553,7 @@ class TriggerApi(DecoratorApi, ABC): 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 sql_trigger decorator adds :class:`SqlTrigger` @@ -574,6 +575,9 @@ class TriggerApi(DecoratorApi, ABC): :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