From 6b5a402946c726de63b7121a46c0c5fcf3f9aa98 Mon Sep 17 00:00:00 2001 From: Lucy Zhang Date: Wed, 19 Jan 2022 16:18:06 -0800 Subject: [PATCH 1/4] add sql bindings --- azure/functions/__init__.py | 4 ++ azure/functions/_abc.py | 29 ++++++++++ azure/functions/_sql.py | 42 ++++++++++++++ azure/functions/sql.py | 76 +++++++++++++++++++++++++ tests/test_sql.py | 107 ++++++++++++++++++++++++++++++++++++ 5 files changed, 258 insertions(+) create mode 100644 azure/functions/_sql.py create mode 100644 azure/functions/sql.py create mode 100644 tests/test_sql.py diff --git a/azure/functions/__init__.py b/azure/functions/__init__.py index cc85b8a3..99ff7f9a 100644 --- a/azure/functions/__init__.py +++ b/azure/functions/__init__.py @@ -16,6 +16,7 @@ from .meta import get_binding_registry from .extension import (ExtensionMeta, FunctionExtensionException, FuncExtensionBase, AppExtensionBase) +from ._sql import SqlRow, SqlRowList # Import binding implementations to register them from . import blob # NoQA @@ -28,6 +29,7 @@ from . import servicebus # NoQA from . import timer # NoQA from . import durable_functions # NoQA +from . import sql # NoQA __all__ = ( @@ -54,6 +56,8 @@ 'EntityContext', 'QueueMessage', 'ServiceBusMessage', + 'SqlRow', + 'SqlRowList', 'TimerRequest', # Middlewares diff --git a/azure/functions/_abc.py b/azure/functions/_abc.py index 40329ae0..fbf954cc 100644 --- a/azure/functions/_abc.py +++ b/azure/functions/_abc.py @@ -421,3 +421,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 \ No newline at end of file diff --git a/azure/functions/_sql.py b/azure/functions/_sql.py new file mode 100644 index 00000000..29887903 --- /dev/null +++ b/azure/functions/_sql.py @@ -0,0 +1,42 @@ +# 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)) + + 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'' + ) + +class SqlRowList(_abc.SqlRowList, collections.UserList): + "A ''UserList'' subclass containing a list of :class:'~SqlRow' objects" + pass \ No newline at end of file diff --git a/azure/functions/sql.py b/azure/functions/sql.py new file mode 100644 index 00000000..a0908d0a --- /dev/null +++ b/azure/functions/sql.py @@ -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: + 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 + + return meta.Datum( + type='json', + value=json.dumps([dict(d) for d in data]) + ) \ No newline at end of file diff --git a/tests/test_sql.py b/tests/test_sql.py new file mode 100644 index 00000000..466d5c6e --- /dev/null +++ b/tests/test_sql.py @@ -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)) From ee015acb86aadd7fa4533f55b61dcc2816c7c2cc Mon Sep 17 00:00:00 2001 From: luczhan Date: Wed, 20 Apr 2022 11:41:34 -0700 Subject: [PATCH 2/4] fix spacing --- azure/functions/_abc.py | 2 +- azure/functions/_sql.py | 4 +++- azure/functions/sql.py | 5 +++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/azure/functions/_abc.py b/azure/functions/_abc.py index fbf954cc..4ba2aba5 100644 --- a/azure/functions/_abc.py +++ b/azure/functions/_abc.py @@ -449,4 +449,4 @@ def to_json(self) -> str: class SqlRowList(abc.ABC): - pass \ No newline at end of file + pass diff --git a/azure/functions/_sql.py b/azure/functions/_sql.py index 29887903..a673c320 100644 --- a/azure/functions/_sql.py +++ b/azure/functions/_sql.py @@ -6,6 +6,7 @@ from . import _abc + class SqlRow(_abc.SqlRow, collections.UserDict): """A SQL Row. @@ -37,6 +38,7 @@ def __repr__(self) -> str: f'' ) + class SqlRowList(_abc.SqlRowList, collections.UserList): "A ''UserList'' subclass containing a list of :class:'~SqlRow' objects" - pass \ No newline at end of file + pass diff --git a/azure/functions/sql.py b/azure/functions/sql.py index a0908d0a..3926d91a 100644 --- a/azure/functions/sql.py +++ b/azure/functions/sql.py @@ -11,7 +11,7 @@ class SqlConverter(meta.InConverter, meta.OutConverter, - binding='sql'): + binding='sql'): @classmethod def check_input_type_annotation(cls, pytype: type) -> bool: @@ -73,4 +73,5 @@ def encode(cls, obj: typing.Any, *, return meta.Datum( type='json', value=json.dumps([dict(d) for d in data]) - ) \ No newline at end of file + ) + \ No newline at end of file From 63b9679d86bf2004a74853b2ff7d4d269b42d4ca Mon Sep 17 00:00:00 2001 From: luczhan Date: Wed, 20 Apr 2022 11:44:52 -0700 Subject: [PATCH 3/4] fix spacing --- azure/functions/sql.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/azure/functions/sql.py b/azure/functions/sql.py index 3926d91a..0c3bb42a 100644 --- a/azure/functions/sql.py +++ b/azure/functions/sql.py @@ -11,7 +11,7 @@ class SqlConverter(meta.InConverter, meta.OutConverter, - binding='sql'): + binding='sql'): @classmethod def check_input_type_annotation(cls, pytype: type) -> bool: @@ -74,4 +74,3 @@ def encode(cls, obj: typing.Any, *, type='json', value=json.dumps([dict(d) for d in data]) ) - \ No newline at end of file From 91007640ee0484fd06365735236d1740e9637f2c Mon Sep 17 00:00:00 2001 From: luczhan Date: Wed, 20 Apr 2022 11:47:53 -0700 Subject: [PATCH 4/4] fix imports --- azure/functions/__init__.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/azure/functions/__init__.py b/azure/functions/__init__.py index 662bb550..57e1a8b0 100644 --- a/azure/functions/__init__.py +++ b/azure/functions/__init__.py @@ -17,10 +17,6 @@ from .meta import get_binding_registry from ._queue import QueueMessage from ._servicebus import ServiceBusMessage -from ._durable_functions import OrchestrationContext, EntityContext -from .meta import get_binding_registry -from .extension import (ExtensionMeta, FunctionExtensionException, - FuncExtensionBase, AppExtensionBase) from ._sql import SqlRow, SqlRowList # Import binding implementations to register them