diff --git a/.flake8 b/.flake8
index ac4a7662..b2c20a27 100644
--- a/.flake8
+++ b/.flake8
@@ -2,4 +2,4 @@
ignore = W503,E402,E731
exclude =
.git, __pycache__, build, dist, .eggs, .github, .local,
- Samples, azure/functions/_thirdparty, docs/, .venv*/, .env*/, .vscode/, venv
+ Samples, azure/functions/_thirdparty, docs/, .venv*/, .env*/, .vscode/, venv*, pyvenv*
diff --git a/.gitignore b/.gitignore
index 09277d5d..b2d97d7f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -85,6 +85,7 @@ celerybeat-schedule
venv/
env/
py3env/
+pyvenv*/
# Spyder project settings
.spyderproject
diff --git a/README.md b/README.md
index 28fb4643..76ef8e58 100644
--- a/README.md
+++ b/README.md
@@ -1,24 +1,52 @@
-
+#
Azure Functions Python Library
|Branch|Status|Coverage|CodeCov|
|---|---|---|---|
|master|[](https://azfunc.visualstudio.com/Azure%20Functions%20Python/_build/latest?definitionId=19&branchName=master)||[](https://codecov.io/gh/Azure/azure-functions-python-library)
|dev|[](https://azfunc.visualstudio.com/Azure%20Functions%20Python/_build/latest?definitionId=19&branchName=dev)||[](https://codecov.io/gh/Azure/azure-functions-python-library)
-# Overview
+## Overview
-Python support for Azure Functions is based on Python 3.6/3.7/3.8, serverless hosting on Linux and the Functions 2.0 runtime.
+Python support for Azure Functions is based on Python 3.6/3.7/3.8 and 3.9 (coming soon), serverless hosting on Linux and the Functions 2.0 runtime.
Here is the current status of Python in Azure Functions:
-What's available?
+_What are the supported Python versions?_
+|Azure Functions Runtime|Python 3.6|Python 3.7|Python 3.8|Python 3.9|
+|---|---|---|---|---|
+|Azure Functions 2.0|✔|✔|-|-|
+|Azure Functions 3.0|✔|✔|✔|(preview)|
+
+_What's available?_
- Build, test, debug and publish using Azure Functions Core Tools (CLI) or Visual Studio Code
- Triggers / Bindings : HTTP, Blob, Queue, Timer, Cosmos DB, Event Grid, Event Hubs and Service Bus
- Create a Python Function on Linux using a custom docker image
- Triggers / Bindings : Custom binding support
-# Contributing
+#### Get Started
+
+- [Create your first Python function](https://docs.microsoft.com/en-us/azure/azure-functions/functions-create-first-function-python)
+- [Developer guide](https://docs.microsoft.com/en-us/azure/azure-functions/functions-reference-python)
+- [Binding API reference](https://docs.microsoft.com/en-us/python/api/azure-functions/azure.functions?view=azure-python)
+- [Develop using VS Code](https://docs.microsoft.com/en-us/azure/azure-functions/functions-create-first-function-vs-code)
+- [Create a Python Function on Linux using a custom docker image](https://docs.microsoft.com/en-us/azure/azure-functions/functions-create-function-linux-custom-image)
+
+#### Give Feedback
+
+Issues and feature requests are tracked in a variety of places. To report this feedback, please file an issue to the relevant repository below:
+
+|Item|Description|Link|
+|----|-----|-----|
+| Python Worker | Programming Model, Triggers & Bindings |[File an Issue](https://github.com/Azure/azure-functions-python-worker/issues)|
+| Linux | Base Docker Images |[File an Issue](https://github.com/Azure/azure-functions-docker/issues)|
+| Runtime | Script Host & Language Extensibility |[File an Issue](https://github.com/Azure/azure-functions-host/issues)|
+| VSCode | VSCode Extension for Azure Functions |[File an Issue](https://github.com/microsoft/vscode-azurefunctions/issues)
+| Core Tools | Command Line Interface for Local Development |[File an Issue](https://github.com/Azure/azure-functions-core-tools/issues)|
+| Portal | User Interface or Experience Issue |[File an Issue](https://github.com/azure/azure-functions-ux/issues)|
+| Templates | Code Issues with Creation Template |[File an Issue](https://github.com/Azure/azure-functions-templates/issues)|
+
+## Contributing
This project welcomes contributions and suggestions. Most contributions require you to agree to a
Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
diff --git a/azure/functions/blob.py b/azure/functions/blob.py
index ce47ee98..ad5484a9 100644
--- a/azure/functions/blob.py
+++ b/azure/functions/blob.py
@@ -2,10 +2,9 @@
# Licensed under the MIT License.
import io
-from typing import Optional, Union, Any
+from typing import Any, Optional, Union
from azure.functions import _abc as azf_abc
-
from . import meta
@@ -13,11 +12,15 @@ class InputStream(azf_abc.InputStream):
def __init__(self, *, data: Union[bytes, meta.Datum],
name: Optional[str] = None,
uri: Optional[str] = None,
- length: Optional[int] = None) -> None:
+ length: Optional[int] = None,
+ blob_properties: Optional[dict] = None,
+ metadata: Optional[dict] = None) -> None:
self._io = io.BytesIO(data) # type: ignore
self._name = name
self._length = length
self._uri = uri
+ self._blob_properties = blob_properties
+ self._metadata = metadata
@property
def name(self) -> Optional[str]:
@@ -31,6 +34,14 @@ def length(self) -> Optional[int]:
def uri(self) -> Optional[str]:
return self._uri
+ @property
+ def blob_properties(self):
+ return self._blob_properties
+
+ @property
+ def metadata(self):
+ return self._metadata
+
def read(self, size=-1) -> bytes:
return self._io.read(size)
@@ -48,7 +59,6 @@ class BlobConverter(meta.InConverter,
meta.OutConverter,
binding='blob',
trigger='blobTrigger'):
-
@classmethod
def check_input_type_annotation(cls, pytype: type) -> bool:
return issubclass(pytype, (azf_abc.InputStream, bytes, str))
@@ -99,11 +109,23 @@ def decode(cls, data: meta.Datum, *, trigger_metadata) -> Any:
properties = cls._decode_trigger_metadata_field(
trigger_metadata, 'Properties', python_type=dict)
if properties:
+ blob_properties = properties
length = properties.get('Length')
length = int(length) if length else None
else:
+ blob_properties = None
length = None
+ metadata = None
+ try:
+ metadata = cls._decode_trigger_metadata_field(trigger_metadata,
+ 'Metadata',
+ python_type=dict)
+ except (KeyError, ValueError):
+ # avoiding any exceptions when fetching Metadata as the
+ # metadata type is unclear.
+ pass
+
return InputStream(
data=data,
name=cls._decode_trigger_metadata_field(
@@ -111,4 +133,6 @@ def decode(cls, data: meta.Datum, *, trigger_metadata) -> Any:
length=length,
uri=cls._decode_trigger_metadata_field(
trigger_metadata, 'Uri', python_type=str),
+ blob_properties=blob_properties,
+ metadata=metadata
)
diff --git a/setup.py b/setup.py
index 07c97711..0592ebd0 100644
--- a/setup.py
+++ b/setup.py
@@ -19,6 +19,10 @@
'License :: OSI Approved :: MIT License',
'Intended Audience :: Developers',
'Programming Language :: Python :: 3',
+ 'Programming Language :: Python :: 3.6',
+ 'Programming Language :: Python :: 3.7',
+ 'Programming Language :: Python :: 3.8',
+ 'Programming Language :: Python :: 3.9',
'Operating System :: Microsoft :: Windows',
'Operating System :: POSIX',
'Operating System :: MacOS :: MacOS X',
diff --git a/tests/test_blob.py b/tests/test_blob.py
index 150951d2..fd7f08a5 100644
--- a/tests/test_blob.py
+++ b/tests/test_blob.py
@@ -1,13 +1,13 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
-
-from typing import Dict, Any
+import json
import unittest
+from typing import Any, Dict
import azure.functions as func
import azure.functions.blob as afb
-from azure.functions.meta import Datum
from azure.functions.blob import InputStream
+from azure.functions.meta import Datum
class TestBlob(unittest.TestCase):
@@ -23,6 +23,11 @@ def test_blob_input_none(self):
data=None, trigger_metadata=None)
self.assertIsNone(result)
+ def test_blob_input_incorrect_type(self):
+ datum: Datum = Datum(value=b'string_content', type='bytearray')
+ with self.assertRaises(ValueError):
+ afb.BlobConverter.decode(data=datum, trigger_metadata=None)
+
def test_blob_input_string_no_metadata(self):
datum: Datum = Datum(value='string_content', type='string')
result: InputStream = afb.BlobConverter.decode(
@@ -61,21 +66,98 @@ def test_blob_input_bytes_no_metadata(self):
content: bytes = result.read()
self.assertEqual(content, b'bytes_content')
- def test_blob_input_with_metadata(self):
+ def test_blob_input_with_metadata_no_blob_properties(self):
+ datum: Datum = Datum(value=b'blob_content', type='bytes')
+ trigger_metadata: Dict[str, Any] = {
+ 'BlobTrigger': Datum('blob_trigger_name', 'string'),
+ 'Uri': Datum('https://test.io/blob_trigger', 'string')
+ }
+ result: InputStream = afb.\
+ BlobConverter.decode(data=datum, trigger_metadata=trigger_metadata)
+
+ # Verify result metadata
+ self.assertIsInstance(result, InputStream)
+ self.assertEqual(result.name, 'blob_trigger_name')
+ self.assertEqual(result.length, None)
+ self.assertEqual(result.uri, 'https://test.io/blob_trigger')
+ self.assertEqual(result.blob_properties, None)
+ self.assertEqual(result.metadata, None)
+
+ def test_blob_input_with_metadata_no_trigger_metadata(self):
+ sample_blob_properties = '{"Length": "12"}'
+ datum: Datum = Datum(value=b'blob_content', type='bytes')
+ trigger_metadata: Dict[str, Any] = {
+ 'Properties': Datum(sample_blob_properties, 'json'),
+ 'BlobTrigger': Datum('blob_trigger_name', 'string'),
+ 'Uri': Datum('https://test.io/blob_trigger', 'string')
+ }
+ result: InputStream = afb.\
+ BlobConverter.decode(data=datum, trigger_metadata=trigger_metadata)
+
+ # Verify result metadata
+ self.assertIsInstance(result, InputStream)
+ self.assertEqual(result.name, 'blob_trigger_name')
+ self.assertEqual(result.length, len(b'blob_content'))
+ self.assertEqual(result.uri, 'https://test.io/blob_trigger')
+ self.assertEqual(result.blob_properties,
+ json.loads(sample_blob_properties))
+ self.assertEqual(result.metadata, None)
+
+ def test_blob_input_with_metadata_with_trigger_metadata(self):
+ sample_metadata = '{"Hello": "World"}'
+ sample_blob_properties = '''{
+ "ContentMD5": "B54d+wzLC8IlnxyyZxwPsw==",
+ "ContentType": "application/octet-stream",
+ "ETag": "0x8D8989BC453467D",
+ "Created": "2020-12-03T08:07:26+00:00",
+ "LastModified": "2020-12-04T21:30:05+00:00",
+ "BlobType": 2,
+ "LeaseStatus": 2,
+ "LeaseState": 1,
+ "LeaseDuration": 0,
+ "Length": "12"
+}'''
datum: Datum = Datum(value=b'blob_content', type='bytes')
- metadata: Dict[str, Any] = {
- 'Properties': Datum('{"Length": "12"}', 'json'),
+ trigger_metadata: Dict[str, Any] = {
+ 'Metadata': Datum(sample_metadata, 'json'),
+ 'Properties': Datum(sample_blob_properties, 'json'),
'BlobTrigger': Datum('blob_trigger_name', 'string'),
'Uri': Datum('https://test.io/blob_trigger', 'string')
}
result: InputStream = afb.BlobConverter.decode(
- data=datum, trigger_metadata=metadata)
+ data=datum, trigger_metadata=trigger_metadata)
+
+ # Verify result metadata
+ self.assertIsInstance(result, InputStream)
+ self.assertEqual(result.name, 'blob_trigger_name')
+ self.assertEqual(result.length, len(b'blob_content'))
+ self.assertEqual(result.uri, 'https://test.io/blob_trigger')
+ self.assertEqual(result.blob_properties,
+ json.loads(sample_blob_properties))
+ self.assertEqual(result.metadata,
+ json.loads(sample_metadata))
+
+ def test_blob_input_with_metadata_with_incorrect_trigger_metadata(self):
+ sample_metadata = 'Hello World'
+ sample_blob_properties = '''{"Length": "12"}'''
+ datum: Datum = Datum(value=b'blob_content', type='bytes')
+ trigger_metadata: Dict[str, Any] = {
+ 'Metadata': Datum(sample_metadata, 'string'),
+ 'Properties': Datum(sample_blob_properties, 'json'),
+ 'BlobTrigger': Datum('blob_trigger_name', 'string'),
+ 'Uri': Datum('https://test.io/blob_trigger', 'string')
+ }
+ result: InputStream = afb.\
+ BlobConverter.decode(data=datum, trigger_metadata=trigger_metadata)
# Verify result metadata
self.assertIsInstance(result, InputStream)
self.assertEqual(result.name, 'blob_trigger_name')
self.assertEqual(result.length, len(b'blob_content'))
self.assertEqual(result.uri, 'https://test.io/blob_trigger')
+ self.assertEqual(result.blob_properties,
+ json.loads(sample_blob_properties))
+ self.assertEqual(result.metadata, None)
def test_blob_incomplete_read(self):
datum: Datum = Datum(value=b'blob_content', type='bytes')