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 @@ -Functions Header Image +# Functions Header Image - Lightning Logo Azure Functions Python Library |Branch|Status|Coverage|CodeCov| |---|---|---|---| |master|[![Build Status](https://azfunc.visualstudio.com/Azure%20Functions%20Python/_apis/build/status/Azure%20Functions%20Python-CI?branchName=master)](https://azfunc.visualstudio.com/Azure%20Functions%20Python/_build/latest?definitionId=19&branchName=master)|![Azure DevOps coverage](https://img.shields.io/azure-devops/coverage/azfunc/Azure%20Functions%20Python/19/master)|[![codecov](https://codecov.io/gh/Azure/azure-functions-python-library/branch/master/graph/badge.svg)](https://codecov.io/gh/Azure/azure-functions-python-library) |dev|[![Build Status](https://azfunc.visualstudio.com/Azure%20Functions%20Python/_apis/build/status/Azure%20Functions%20Python-CI?branchName=dev)](https://azfunc.visualstudio.com/Azure%20Functions%20Python/_build/latest?definitionId=19&branchName=dev)|![Azure DevOps coverage](https://img.shields.io/azure-devops/coverage/azfunc/Azure%20Functions%20Python/19/dev)|[![codecov](https://codecov.io/gh/Azure/azure-functions-python-library/branch/dev/graph/badge.svg)](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')