Skip to content

Add generic binding support #409

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jul 1, 2019
Merged

Add generic binding support #409

merged 4 commits into from
Jul 1, 2019

Conversation

elprans
Copy link
Collaborator

@elprans elprans commented May 6, 2019

Add a generic binding implementation to allow unknown bindings with
dataType: string and dataType: binary.

Fixes: #66
Fixes: #408

Copy link
Member

@pragnagopa pragnagopa left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added few comments. Thanks!

@elprans elprans requested a review from pragnagopa May 10, 2019 15:21
@pragnagopa
Copy link
Member

Please open a separate issue to add support to dataclasses with the details

Copy link
Member

@pragnagopa pragnagopa left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added few more comments. Thanks!

logger = logging.getLogger(__name__)


def main(msg: str) -> str:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add another test without annotation. The purpose of explicity specifying dataType in function.json is to to binding to basic types without annotations

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this should no longer be applicable since we are removing the dependency on data type?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do need to have tests for both i.e dataType is specified and not specified.

@@ -60,6 +60,10 @@ def check_input_type_annotation(
cls, pytype: type, datatype: protos.BindingInfo.DataType) -> bool:
if datatype is protos.BindingInfo.undefined:
return issubclass(pytype, azf_abc.QueueMessage)
elif (datatype is protos.BindingInfo.binary):
return issubclass(pytype, bytes)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about other bindings? shouldn't they be updated to handle dataType binary and string? what about outputbindings?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Output bindings already support elementary types, where applicable.

what about other bindings?

Which bindings do you have in mind? Is there actually value in providing raw input for all bindings given that rich bindings already exist? I'm thinking an abundance of options here would create confusion without actual benefit.

Copy link
Member

@pragnagopa pragnagopa May 10, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Goal of this work item is to enable all bindings to work when datatype is specified in function.json
This means that, when issue #135 is addressed, users should be able to use functions without adding reference to azure.functions to bind to basic types.

This work-item is also about designing the worker to be a layer between host and any library types users to choose to request in their functions

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still seems to me like a step backwards. I understand why raw interface might be useful for when there is no canonical azure.functions binding yet, essentially as a workaround.

But our original direction with the design of the worker was to expose well-documented Pythonic interfaces as the API, not raw, untyped, data. Promoting the latter creates a strong binding to the internals, and that may restrict our ability to upgrade existing users to newer versions of the platform.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using azure.functions is still the recommended approach. All the documentation and the samples will be using types in azure.functions package.

Again, this work item is to make python worker an interface layer that converts to requested types.

@asavaritayal
Copy link
Contributor

@elprans can you add tests here for Azure Table Storage and SignalR bindings?

These are two of the scenarios we don't provide rich bindings for and want to make sure they're tracked and tested with each release.

@pragnagopa
Copy link
Member

pragnagopa commented May 13, 2019

With these changes, i think both table and signalr bindings should work. @elprans - Please test E2E.

@elprans
Copy link
Collaborator Author

elprans commented May 15, 2019

@asavaritayal @pragnagopa I'll open a separate issue for SignalR and Table Storage. This PR already has tests for arbitrary bindings.

@elprans elprans force-pushed the default-binding branch from b08bd77 to c7395a7 Compare May 15, 2019 16:10
@pragnagopa
Copy link
Member

I understand setup needed for testing signalR is difficult. Can you please add E2E test for table storage? It just needs storage account connection string which E2E tests are already using. I can signoff once table storage is tested.

@elprans
Copy link
Collaborator Author

elprans commented May 15, 2019

@pragnagopa How do I add Table API to an existing Cosmos DB account?

@pragnagopa
Copy link
Member

Here is the documentation for table storage bindings: https://docs.microsoft.com/en-us/azure/azure-functions/functions-bindings-storage-table for https://azure.microsoft.com/en-us/services/storage/tables/ . Please note these are not related to cosmosdb

@elprans
Copy link
Collaborator Author

elprans commented May 15, 2019

Please note these are not related to cosmosdb

So is this the wrong doc? https://docs.microsoft.com/en-us/azure/cosmos-db/tutorial-develop-table-dotnet

@pragnagopa
Copy link
Member

That's correct. The doc you referred to is for CosmosDB Table API. The test I am referring to is just for Azure Table Storage. Which is similar to Blob, Queue bindings we have already.

@elprans
Copy link
Collaborator Author

elprans commented May 15, 2019

@pragnagopa I get this failure:

fail: Function.table_input[0]
      Executed 'Functions.table_input' (Failed, Id=7ad33ddf-12de-4d7b-8e54-cdc444fdfc12)
Microsoft.Azure.WebJobs.Host.FunctionInvocationException: Exception while executing function: Functions.table_input ---> System.InvalidOperationException: Table entity types must provide a default constructor.
   at Microsoft.Azure.WebJobs.Host.Tables.TableClient.VerifyDefaultConstructor(Type entityType) in C:\projects\azure-webjobs-sdk-rqm4t\src\Microsoft.Azure.WebJobs.Extensions.Storage\Tables\TableClient.cs:line 75
   at Microsoft.Azure.WebJobs.Host.Tables.PocoEntityArgumentBindingProvider.TryCreate(ParameterInfo parameter) in C:\projects\azure-webjobs-sdk-rqm4t\src\Microsoft.Azure.WebJobs.Extensions.Storage\Tables\PocoEntityArgumentBindingProvider.cs:line 24
   at Microsoft.Azure.WebJobs.Host.Tables.CompositeEntityArgumentBindingProvider.TryCreate(ParameterInfo parameter) in C:\projects\azure-webjobs-sdk-rqm4t\src\Microsoft.Azure.WebJobs.Extensions.Storage\Tables\CompositeEntityArgumentBindingProvider.cs:line 23
   at Microsoft.Azure.WebJobs.Host.Tables.TableAttributeBindingProvider.TryCreate(BindingProviderContext context) in C:\projects\azure-webjobs-sdk-rqm4t\src\Microsoft.Azure.WebJobs.Extensions.Storage\Tables\TableAttributeBindingProvider.cs:line 69
   at Microsoft.Azure.WebJobs.Host.Tables.TableAttributeBindingProvider.<>c__DisplayClass5_0.<TryCreateAsync>b__0() in C:\projects\azure-webjobs-sdk-rqm4t\src\Microsoft.Azure.WebJobs.Extensions.Storage\Tables\TableAttributeBindingProvider.cs:line 84
   at System.Threading.Tasks.Task`1.InnerInvoke()
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
--- End of stack trace from previous location where exception was thrown ---
   at System.Threading.Tasks.Task.ExecuteWithThreadLocal(Task& currentTaskSlot)
--- End of stack trace from previous location where exception was thrown ---
   at Microsoft.Azure.WebJobs.Host.Bindings.GenericCompositeBindingProvider`1.TryCreateAsync(BindingProviderContext context) in C:\projects\azure-webjobs-sdk-rqm4t\src\Microsoft.Azure.WebJobs.Host\Bindings\BindingProviders\GenericCompositeBindingProvider.cs:line 61
   at Microsoft.Azure.WebJobs.Host.Bindings.CompositeBindingProvider.TryCreateAsync(BindingProviderContext context) in C:\projects\azure-webjobs-sdk-rqm4t\src\Microsoft.Azure.WebJobs.Host\Bindings\BindingProviders\CompositeBindingProvider.cs:line 25
   at Microsoft.Azure.WebJobs.Binder.BindAsync[TValue](Attribute[] attributes, CancellationToken cancellationToken) in C:\projects\azure-webjobs-sdk-rqm4t\src\Microsoft.Azure.WebJobs.Host\Bindings\Runtime\Binder.cs:line 97
   at Microsoft.Azure.WebJobs.Script.Binding.FunctionBinding.BindStringAsync(BindingContext context) in C:\azure-webjobs-sdk-script\src\WebJobs.Script\Binding\FunctionBinding.cs:line 220
   at Microsoft.Azure.WebJobs.Script.Binding.ExtensionBinding.BindAsync(BindingContext context) in C:\azure-webjobs-sdk-script\src\WebJobs.Script\Binding\ExtensionBinding.cs:line 75
   at Microsoft.Azure.WebJobs.Script.Description.WorkerLanguageInvoker.<>c__DisplayClass7_0.<<BindInputsAsync>b__1>d.MoveNext() in C:\azure-webjobs-sdk-script\src\WebJobs.Script\Description\Rpc\WorkerLanguageInvoker.cs:line 95
--- End of stack trace from previous location where exception was thrown ---
   at Microsoft.Azure.WebJobs.Script.Description.WorkerLanguageInvoker.BindInputsAsync(Binder binder) in C:\azure-webjobs-sdk-script\src\WebJobs.Script\Description\Rpc\WorkerLanguageInvoker.cs:line 98
   at Microsoft.Azure.WebJobs.Script.Description.WorkerLanguageInvoker.InvokeCore(Object[] parameters, FunctionInvocationContext context) in C:\azure-webjobs-sdk-script\src\WebJobs.Script\Description\Rpc\WorkerLanguageInvoker.cs:line 56
   at Microsoft.Azure.WebJobs.Script.Description.FunctionInvokerBase.Invoke(Object[] parameters) in C:\azure-webjobs-sdk-script\src\WebJobs.Script\Description\FunctionInvokerBase.cs:line 84
   at Microsoft.Azure.WebJobs.Script.Description.FunctionGenerator.Coerce[T](Task`1 src) in C:\azure-webjobs-sdk-script\src\WebJobs.Script\Description\FunctionGenerator.cs:line 225
   at Microsoft.Azure.WebJobs.Host.Executors.FunctionInvoker`2.InvokeAsync(Object instance, Object[] arguments) in C:\projects\azure-webjobs-sdk-rqm4t\src\Microsoft.Azure.WebJobs.Host\Executors\FunctionInvoker.cs:line 63
   at Microsoft.Azure.WebJobs.Host.Executors.FunctionExecutor.InvokeAsync(IFunctionInvoker invoker, ParameterHelper parameterHelper, CancellationTokenSource timeoutTokenSource, CancellationTokenSource functionCancellationTokenSource, Boolean throwOnTimeout, TimeSpan timerInterval, IFunctionInstance instance) in C:\projects\azure-webjobs-sdk-rqm4t\src\Microsoft.Azure.WebJobs.Host\Executors\FunctionExecutor.cs:line 556
   at Microsoft.Azure.WebJobs.Host.Executors.FunctionExecutor.ExecuteWithWatchersAsync(IFunctionInstance instance, ParameterHelper parameterHelper, ILogger logger, CancellationTokenSource functionCancellationTokenSource) in C:\projects\azure-webjobs-sdk-rqm4t\src\Microsoft.Azure.WebJobs.Host\Executors\FunctionExecutor.cs:line 503
   at Microsoft.Azure.WebJobs.Host.Executors.FunctionExecutor.ExecuteWithLoggingAsync(IFunctionInstance instance, ParameterHelper parameterHelper, IFunctionOutputDefinition outputDefinition, ILogger logger, CancellationTokenSource functionCancellationTokenSource) in C:\projects\azure-webjobs-sdk-rqm4t\src\Microsoft.Azure.WebJobs.Host\Executors\FunctionExecutor.cs:line 439
   at Microsoft.Azure.WebJobs.Host.Executors.FunctionExecutor.ExecuteWithLoggingAsync(IFunctionInstance instance, FunctionStartedMessage message, FunctionInstanceLogEntry instanceLogEntry, ParameterHelper parameterHelper, ILogger logger, CancellationToken cancellationToken) in C:\projects\azure-webjobs-sdk-rqm4t\src\Microsoft.Azure.WebJobs.Host\Executors\FunctionExecutor.cs:line 249
   --- End of inner exception stack trace ---
   at Microsoft.Azure.WebJobs.Host.Executors.FunctionExecutor.ExecuteWithLoggingAsync(IFunctionInstance instance, FunctionStartedMessage message, FunctionInstanceLogEntry instanceLogEntry, ParameterHelper parameterHelper, ILogger logger, CancellationToken cancellationToken) in C:\projects\azure-webjobs-sdk-rqm4t\src\Microsoft.Azure.WebJobs.Host\Executors\FunctionExecutor.cs:line 293
   at Microsoft.Azure.WebJobs.Host.Executors.FunctionExecutor.TryExecuteAsync(IFunctionInstance functionInstance, CancellationToken cancellationToken) in C:\projects\azure-webjobs-sdk-rqm4t\src\Microsoft.Azure.WebJobs.Host\Executors\FunctionExecutor.cs:line 89

function.json:

{
  "scriptFile": "__init__.py",
  "disabled": false,

  "bindings": [
    {
      "type": "httpTrigger",
      "direction": "in",
      "name": "req"
    },
    {
      "direction": "in",
      "type": "table",
      "name": "docs",
      "dataType": "string",
      "partitionKey": "test",
      "rowKey": "foo",
      "tableName": "test",
      "connection": "AzureWebJobsStorage"
    },
    {
      "type": "http",
      "direction": "out",
      "name": "$return"
    }
  ]
}

@pragnagopa
Copy link
Member

looks like specifying dataType is causing the problem. Can you try output binding?

@elprans
Copy link
Collaborator Author

elprans commented May 15, 2019

@pragnagopa Isn't the point here to test unstructured binding via 'dataType'?

@pragnagopa
Copy link
Member

Isn't the point here to test unstructured binding via 'dataType'?

Goal of E2E testing

  1. Use dataType and test a trigger/input using generic binding
  2. Test generic output binding i.e a binding that is not defined in azure.functions package

For 1, open an issue on the host. Parsing table input data as string is failing.
To test 2, try Table storage output binding

I think worker needs to handle any binding that is not defined in azure.functions package as a generic binding without enforcing the use of 'dataType` in function.json

@elprans
Copy link
Collaborator Author

elprans commented May 15, 2019

I think worker needs to handle any binding that is not defined in azure.functions package as a generic binding without enforcing the use of 'dataType` in function.json

I don't think this is a good idea. The Rich/Unstructured switch has to be explicit. Otherwise, if we choose to support a binding in a future release, user code will break.

@pragnagopa
Copy link
Member

I don't think this is a good idea. The Rich/Unstructured switch has to be explicit. Otherwise, if we choose to support a binding in a future release, user code will break.

Can you explain? Generic binding is only fall back if there are no rich types found in any of the referenced assemblies

@elprans
Copy link
Collaborator Author

elprans commented May 15, 2019

I cannot write a decent E2E test if only half of the host functionality is working. How do I test that the output binding is working correctly without being able to read the data back?

@pragnagopa
Copy link
Member

I cannot write a decent E2E test if only half of the host functionality is working. How do I test that the output binding is working correctly without being able to read the data back?

You do not have to read data to write data to an output binding. Let's take one step at a time. Does writing using generic output binding work?

@elprans
Copy link
Collaborator Author

elprans commented May 15, 2019

You do not have to read data to write data to an output binding. Let's take one step at a time. Does writing using generic output binding work?

The function doesn't crash, but I have no way to verify if the data was actually written.

@pragnagopa
Copy link
Member

pragnagopa commented Jun 3, 2019

can you give example python function for non-text blob?

@elprans
Copy link
Collaborator Author

elprans commented Jun 3, 2019

can you give example for non-text blob?

I upload a PNG image or a ZIP file using an HTTP trigger, and store that as a blob.

@pragnagopa
Copy link
Member

Not sure I understand the issue you are pointing. When using a generic binding, uploading a PNG image or a ZIP file should still work. User needs to set the data on the output binding. Here is the expanded sample of java script function that uploads a local file

const fs = require('fs');
const util = require('util');
const readFileAsync = util.promisify(fs.readFile);
module.exports = async function (context, myBlob) {
    context.log("JavaScript blob trigger function processed blob \n Name:", context.bindingData.name, "\n Blob Size:", myBlob.length, "Bytes");
    context.bindings.myOutputBlob = context.bindings.myInputBlob;
    context.bindings.myOutputBlobString = "stringout";
    var text = '{ "employees" : [' +
                '{ "firstName":"John" , "lastName":"Doe" },' +
                '{ "firstName":"Anna" , "lastName":"Smith" },' +
                '{ "firstName":"Peter" , "lastName":"Jones" } ]}';
    context.bindings.myOutputBlobJson = text;
    var data =  await readFileAsync('./BlobTrigger1/JavaVersion.png');
    context.log(data.length);
    context.bindings.myOutputBlobPng = data;       
};

corresponding function.json

{
  "bindings": [
    {
      "name": "myInputBlob",
      "type": "blobTrigger",
      "direction": "in",
      "path": "samples-workitems/{name}",
      "connection": "AzureWebJobsStorage"
    },
    {
      "type": "blob",
      "name": "myOutputBlobString",
      "path": "outcontainer/stringOut",
      "connection": "AzureWebJobsStorage",
      "direction": "out"
    },
    {
      "type": "blob",
      "name": "myOutputBlob",
      "path": "outcontainer/{rand-guid}",
      "connection": "AzureWebJobsStorage",
      "direction": "out"
    },
    {
      "type": "blob",
      "name": "myOutputBlobJson",
      "path": "outcontainer/jsonout",
      "connection": "AzureWebJobsStorage",
      "direction": "out"
    },
    {
      "type": "blob",
      "name": "myOutputBlobPng",
      "path": "outcontainer/{rand-guid}.png",
      "connection": "AzureWebJobsStorage",
      "direction": "out"
    }
  ]
}

It would be best if you can share python function code and function.json for cases you think might not work.

@elprans
Copy link
Collaborator Author

elprans commented Jun 3, 2019

OK, so in generic binding mode the function must return either a str instance or an object supporting the buffer protocol (bytes, bytearray etc). If the function returns anything other than this, we raise an error. Additionally, if the output type annotation does not match the returned value type, we also raise an error. Does this sound right?

@pragnagopa
Copy link
Member

so in generic binding mode the function must return either a str instance or an object supporting the buffer protocol (bytes, bytearray etc).

As long as return type matches supported types by the TypedData defined in protobuf, it should work

if the output type annotation does not match the returned value type, we also raise an error. Does this sound right?

Yes. If user specified annotation, we should raise error if return type does not match annotation type.

@pragnagopa
Copy link
Member

Expanding on the earlier question:

if the output type annotation does not match the returned value type, we also raise an error. Does this sound right?

Type checking only matters when using $return . A function can have multiple output bindings and no type restriction checks are needed when explicitly setting the value for an output binding.

@pragnagopa
Copy link
Member

Ping!

@pragnagopa
Copy link
Member

@elprans - Ping! Please give an update here or over email.

elprans added a commit to Azure/azure-functions-python-library that referenced this pull request Jun 17, 2019
Per Azure/azure-functions-python-worker#409, the rich binding
implementations are now in the library.
Copy link
Member

@pragnagopa pragnagopa left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for putting this together. Mostly looks good. Added minor comments. Please update the PR description to reflect the changes.


__all__ = (
'Out', 'Context',
'is_binding', 'is_trigger_binding',
'is_trigger_binding',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do you need 'is_trigger_binding'?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is used to distinguish regular input bindings from trigger bindings, because there are differences in handling these (metadata, etc.)

"name": "msg",
"queueName": "testqueue-untyped-return",
"connection": "AzureWebJobsStorage",
"dataType": "string"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove dataType: string

@pragnagopa pragnagopa changed the title Allow generic bindings with elementary dataType Add generic binding support Jun 19, 2019
Copy link
Member

@pragnagopa pragnagopa left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. Please address minor comments I had on the tests before merging.

@@ -54,6 +54,18 @@ def test_queue_message_object_return(self):
self.assertEqual(r.status_code, 200)
self.assertEqual(r.text, 'test-message-object-return')

def test_queue_untyped_return(self):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not fully understanding the flow of this test case. The put_queue_untyped_return function adds a message to the queue testqueue-untyped-return via an http post. After the sleep, the get_queue_untyped_blob_return reads from an input blob and returns an http response. The second function call does not use the queue message from the first function - is that intended? The naming and structure of the function suggests a relation between the two.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Each test is independent to avoid races. The flow in each trigger test is "put data" -> "wait for trigger" (trigger writes a blob) -> "check the blob to ensure trigger ran".

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not able to match up the code with flow that you've described. Here's what I see as the flow for each of these requests.

  1. put_queue_untyped_return: POST Http data -> return queue message with bytes type. This is meant to check that we are able to write queue message with bytes?
  2. get_queue_untyped_blob_return: GET request -> read blob data -> return data as string via HTTP. The function does not make use of queues so I'm not sure why queue is mentioned in the name. The intention here is to check that HTTP messages can be returned with str type?

I'm also not sure why both of these calls are in the same test function test_queue_untyped_return even though they are completely independent and are not using the same resources either.

Copy link
Collaborator Author

@elprans elprans Jun 25, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function does not make use of queues so I'm not sure why queue is mentioned in the name. The intention here is to check that HTTP messages can be returned with str type?

The intent is to verify that the queue trigger has fired. There is no way to do this other than make the trigger produce a verifiable side-effect. Writing blobs seems like the most reliable way. The name of the function contains queue because it belongs logically to the same test.

elprans added a commit to Azure/azure-functions-python-library that referenced this pull request Jun 24, 2019
Per Azure/azure-functions-python-worker#409, the rich binding
implementations are now in the library.
@elprans elprans force-pushed the default-binding branch 2 times, most recently from 99b03f3 to b774843 Compare June 24, 2019 20:20
elprans added 4 commits June 24, 2019 17:03
Add a generic binding implementation to allow unknown bindings with
`dataType: string` and `dataType: binary`.

Fixes: #66
Fixes: #408
Per discussion, the worker no longer includes support for rich bindings
by default.  The implementation have been moved to
azure-functions-python-library, and the runtime dependency on is has
been removed.
@maiqbal11
Copy link
Contributor

@elprans, should be good to check in?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Custom binding error Implement base object for handling triggers with basic data types
5 participants