Skip to content
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

Add SQL Bindings #118

Closed
wants to merge 12 commits into from
4 changes: 4 additions & 0 deletions azure/functions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from .meta import get_binding_registry
from ._queue import QueueMessage
from ._servicebus import ServiceBusMessage
from ._sql import SqlRow, SqlRowList

# Import binding implementations to register them
from . import blob # NoQA
Expand All @@ -29,6 +30,7 @@
from . import servicebus # NoQA
from . import timer # NoQA
from . import durable_functions # NoQA
from . import sql # NoQA


__all__ = (
Expand All @@ -55,6 +57,8 @@
'EntityContext',
'QueueMessage',
'ServiceBusMessage',
'SqlRow',
'SqlRowList',
'TimerRequest',

# Middlewares
Expand Down
29 changes: 29 additions & 0 deletions azure/functions/_abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -422,3 +422,32 @@ class OrchestrationContext(abc.ABC):
@abc.abstractmethod
def body(self) -> str:
pass


class SqlRow(abc.ABC):

@classmethod
@abc.abstractmethod
def from_json(cls, json_data: str) -> 'SqlRow':
pass

@classmethod
@abc.abstractmethod
def from_dict(cls, dct: dict) -> 'SqlRow':
pass

@abc.abstractmethod
def __getitem__(self, key):
pass

@abc.abstractmethod
def __setitem__(self, key, value):
pass

@abc.abstractmethod
def to_json(self) -> str:
pass


class SqlRowList(abc.ABC):
pass
44 changes: 44 additions & 0 deletions azure/functions/_sql.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

import collections
import json

from . import _abc


class SqlRow(_abc.SqlRow, collections.UserDict):
"""A SQL Row.

SqlRow objects are ''UserDict'' subclasses and behave like dicts.
"""

@classmethod
def from_json(cls, json_data: str) -> 'SqlRow':
"""Create a SqlRow from a JSON string."""
return cls.from_dict(json.loads(json_data))

@classmethod
def from_dict(cls, dct: dict) -> 'SqlRow':
"""Create a SqlRow from a dict object"""
return cls({k: v for k, v in dct.items()})

def to_json(self) -> str:
"""Return the JSON representation of the SqlRow"""
return json.dumps(dict(self))

Copy link
Member

Choose a reason for hiding this comment

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

can we add tests to address codecov warnings?

def __getitem__(self, key):
return collections.UserDict.__getitem__(self, key)

def __setitem__(self, key, value):
return collections.UserDict.__setitem__(self, key, value)

def __repr__(self) -> str:
return (
f'<SqlRow at 0x{id(self):0x}>'
)


class SqlRowList(_abc.SqlRowList, collections.UserList):
"A ''UserList'' subclass containing a list of :class:'~SqlRow' objects"
pass
76 changes: 76 additions & 0 deletions azure/functions/sql.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

import collections.abc
import json
import typing

from azure.functions import _sql as sql

from . import meta


class SqlConverter(meta.InConverter, meta.OutConverter,
binding='sql'):

@classmethod
def check_input_type_annotation(cls, pytype: type) -> bool:
return issubclass(pytype, sql.SqlRowList)

@classmethod
def check_output_type_annotation(cls, pytype: type) -> bool:
return issubclass(pytype, (sql.SqlRowList, sql.SqlRow))

@classmethod
def decode(cls,
data: meta.Datum,
*,
trigger_metadata) -> typing.Optional[sql.SqlRowList]:
if data is None or data.type is None:
return None

data_type = data.type

if data_type in ['string', 'json']:
body = data.value

elif data_type == 'bytes':
body = data.value.decode('utf-8')

else:
raise NotImplementedError(
f'unsupported queue payload type: {data_type}')

rows = json.loads(body)
if not isinstance(rows, list):
rows = [rows]

return sql.SqlRowList(
(None if row is None else sql.SqlRow.from_dict(row))
for row in rows)

@classmethod
def encode(cls, obj: typing.Any, *,
expected_type: typing.Optional[type]) -> meta.Datum:
YunchuWang marked this conversation as resolved.
Show resolved Hide resolved
if isinstance(obj, sql.SqlRow):
data = sql.SqlRowList([obj])

elif isinstance(obj, sql.SqlRowList):
data = obj

elif isinstance(obj, collections.abc.Iterable):
data = sql.SqlRowList()

for row in obj:
if not isinstance(row, sql.SqlRow):
raise NotImplementedError
else:
data.append(row)

else:
raise NotImplementedError
Copy link
Contributor

Choose a reason for hiding this comment

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

Add a message with the NotImplementedError


return meta.Datum(
type='json',
value=json.dumps([dict(d) for d in data])
)
107 changes: 107 additions & 0 deletions tests/test_sql.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

import unittest

import azure.functions as func
import azure.functions.sql as sql
from azure.functions.meta import Datum


class TestSql(unittest.TestCase):
def test_sql_convert_none(self):
result: func.SqlRowList = sql.SqlConverter.decode(
data=None, trigger_metadata=None)
self.assertIsNone(result)

def test_sql_convert_string(self):
datum: Datum = Datum("""
{
"id": "1",
"name": "test"
}
""", "string")
result: func.SqlRowList = sql.SqlConverter.decode(
data=datum, trigger_metadata=None)
self.assertIsNotNone(result)
self.assertEqual(len(result), 1)
self.assertEqual(result[0]['name'], 'test')

def test_sql_convert_bytes(self):
datum: Datum = Datum("""
{
"id": "1",
"name": "test"
}
""".encode(), "bytes")
result: func.SqlRowList = sql.SqlConverter.decode(
data=datum, trigger_metadata=None)
self.assertIsNotNone(result)
self.assertEqual(len(result), 1)
self.assertEqual(result[0]['name'], 'test')

def test_sql_convert_json(self):
datum: Datum = Datum("""
{
"id": "1",
"name": "test"
}
""", "json")
result: func.SqlRowList = sql.SqlConverter.decode(
data=datum, trigger_metadata=None)
self.assertIsNotNone(result)
self.assertEqual(len(result), 1)
self.assertEqual(result[0]['name'], 'test')

def test_sql_convert_json_name_is_null(self):
datum: Datum = Datum("""
{
"id": "1",
"name": null
}
""", "json")
result: func.SqlRowList = sql.SqlConverter.decode(
data=datum, trigger_metadata=None)
self.assertIsNotNone(result)
self.assertEqual(len(result), 1)
self.assertEqual(result[0]['name'], None)

def test_sql_convert_json_multiple_entries(self):
datum: Datum = Datum("""
[
{
"id": "1",
"name": "test1"
},
{
"id": "2",
"name": "test2"
}
]
""", "json")
result: func.SqlRowList = sql.SqlConverter.decode(
data=datum, trigger_metadata=None)
self.assertIsNotNone(result)
self.assertEqual(len(result), 2)
self.assertEqual(result[0]['name'], 'test1')
self.assertEqual(result[1]['name'], 'test2')

def test_sql_convert_json_multiple_nulls(self):
datum: Datum = Datum("[null]", "json")
result: func.SqlRowList = sql.SqlConverter.decode(
data=datum, trigger_metadata=None)
self.assertIsNotNone(result)
self.assertEqual(len(result), 1)
self.assertEqual(result[0], None)

def test_sql_input_type(self):
check_input_type = sql.SqlConverter.check_input_type_annotation
self.assertTrue(check_input_type(func.SqlRowList))
self.assertFalse(check_input_type(func.SqlRow))
self.assertFalse(check_input_type(str))

def test_sql_output_type(self):
check_output_type = sql.SqlConverter.check_output_type_annotation
self.assertTrue(check_output_type(func.SqlRowList))
self.assertTrue(check_output_type(func.SqlRow))
self.assertFalse(check_output_type(str))