Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .flake8
Original file line number Diff line number Diff line change
Expand Up @@ -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*
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ celerybeat-schedule
venv/
env/
py3env/
pyvenv*/

# Spyder project settings
.spyderproject
Expand Down
38 changes: 33 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,52 @@
<img src="https://raw.githubusercontent.com/Azure/azure-functions-python-worker/dev/docs/Azure.Functions.svg" width = "180" alt="Functions Header Image">
# <img src="https://raw.githubusercontent.com/Azure/azure-functions-python-worker/dev/docs/Azure.Functions.svg" width = "30" alt="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
Expand Down
32 changes: 28 additions & 4 deletions azure/functions/blob.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,25 @@
# 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


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]:
Expand All @@ -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)

Expand All @@ -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))
Expand Down Expand Up @@ -99,16 +109,30 @@ 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(
trigger_metadata, 'BlobTrigger', python_type=str),
length=length,
uri=cls._decode_trigger_metadata_field(
trigger_metadata, 'Uri', python_type=str),
blob_properties=blob_properties,
metadata=metadata
)
4 changes: 4 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
96 changes: 89 additions & 7 deletions tests/test_blob.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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(
Expand Down Expand Up @@ -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')
Expand Down