From 7c3c2e0e3f61db1d02e60562d0068a6d18ba3c71 Mon Sep 17 00:00:00 2001 From: David Justo Date: Mon, 24 Aug 2020 23:54:59 -0700 Subject: [PATCH 1/5] added durable entities trigger --- azure/functions/_durable_functions.py | 27 ++++++++++++++++++++++++++ azure/functions/durable_functions.py | 28 +++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/azure/functions/_durable_functions.py b/azure/functions/_durable_functions.py index 7a14e32b..82eae15c 100644 --- a/azure/functions/_durable_functions.py +++ b/azure/functions/_durable_functions.py @@ -109,3 +109,30 @@ def __repr__(self): def __str__(self): return self.__body + +class EntityContext(_abc.OrchestrationContext): + """A durable function entity context. + + :param str body: + The body of orchestration context json. + """ + + def __init__(self, + body: Union[str, bytes]) -> None: + if isinstance(body, str): + self.__body = body + if isinstance(body, bytes): + self.__body = body.decode('utf-8') + + @property + def body(self) -> str: + return self.__body + + def __repr__(self): + return ( + f'' + ) + + def __str__(self): + return self.__body diff --git a/azure/functions/durable_functions.py b/azure/functions/durable_functions.py index 7b230135..1a3f14bf 100644 --- a/azure/functions/durable_functions.py +++ b/azure/functions/durable_functions.py @@ -38,6 +38,34 @@ def encode(cls, obj: typing.Any, *, def has_implicit_output(cls) -> bool: return True +class EnitityTriggerConverter(meta.InConverter, + meta.OutConverter, + binding='entityTrigger', + trigger=True): + @classmethod + def check_input_type_annotation(cls, pytype): + return issubclass(pytype, _durable_functions.EntityContext) + + @classmethod + def check_output_type_annotation(cls, pytype): + # Implicit output should accept any return type + return True + + @classmethod + def decode(cls, + data: meta.Datum, *, + trigger_metadata) -> _durable_functions.EntityContext: + return _durable_functions.EntityContext(data.value) + + @classmethod + def encode(cls, obj: typing.Any, *, + expected_type: typing.Optional[type]) -> meta.Datum: + # Durable function context should be a json + return meta.Datum(type='json', value=obj) + + @classmethod + def has_implicit_output(cls) -> bool: + return True # Durable Function Activity Trigger class ActivityTriggerConverter(meta.InConverter, From 12d1ff03b4e954f0f2d1c6fb3653828808c002c8 Mon Sep 17 00:00:00 2001 From: David Justo Date: Wed, 26 Aug 2020 18:14:48 -0700 Subject: [PATCH 2/5] fixed style error --- azure/functions/_durable_functions.py | 1 + azure/functions/durable_functions.py | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/azure/functions/_durable_functions.py b/azure/functions/_durable_functions.py index 82eae15c..aa533679 100644 --- a/azure/functions/_durable_functions.py +++ b/azure/functions/_durable_functions.py @@ -110,6 +110,7 @@ def __repr__(self): def __str__(self): return self.__body + class EntityContext(_abc.OrchestrationContext): """A durable function entity context. diff --git a/azure/functions/durable_functions.py b/azure/functions/durable_functions.py index 1a3f14bf..a6b0221f 100644 --- a/azure/functions/durable_functions.py +++ b/azure/functions/durable_functions.py @@ -39,9 +39,9 @@ def has_implicit_output(cls) -> bool: return True class EnitityTriggerConverter(meta.InConverter, - meta.OutConverter, - binding='entityTrigger', - trigger=True): + meta.OutConverter, + binding='entityTrigger', + trigger=True): @classmethod def check_input_type_annotation(cls, pytype): return issubclass(pytype, _durable_functions.EntityContext) @@ -67,6 +67,7 @@ def encode(cls, obj: typing.Any, *, def has_implicit_output(cls) -> bool: return True + # Durable Function Activity Trigger class ActivityTriggerConverter(meta.InConverter, meta.OutConverter, From 68acf7d69f7371930a7ae81b9d579aa913530e78 Mon Sep 17 00:00:00 2001 From: David Justo Date: Wed, 26 Aug 2020 18:17:53 -0700 Subject: [PATCH 3/5] more style issues --- azure/functions/durable_functions.py | 1 + 1 file changed, 1 insertion(+) diff --git a/azure/functions/durable_functions.py b/azure/functions/durable_functions.py index a6b0221f..16c4fc4c 100644 --- a/azure/functions/durable_functions.py +++ b/azure/functions/durable_functions.py @@ -38,6 +38,7 @@ def encode(cls, obj: typing.Any, *, def has_implicit_output(cls) -> bool: return True + class EnitityTriggerConverter(meta.InConverter, meta.OutConverter, binding='entityTrigger', From b434815c0ac3ae518be28acc949e0776a215bb75 Mon Sep 17 00:00:00 2001 From: David Justo Date: Thu, 10 Sep 2020 10:17:54 -0700 Subject: [PATCH 4/5] exported entitycontext, generalized tests --- azure/functions/__init__.py | 3 +- tests/test_durable_functions.py | 121 +++++++++++++++++--------------- 2 files changed, 68 insertions(+), 56 deletions(-) diff --git a/azure/functions/__init__.py b/azure/functions/__init__.py index eb1eb489..0ccf4046 100644 --- a/azure/functions/__init__.py +++ b/azure/functions/__init__.py @@ -11,7 +11,7 @@ from .kafka import KafkaEvent, KafkaConverter, KafkaTriggerConverter # NoQA from ._queue import QueueMessage # NoQA from ._servicebus import ServiceBusMessage # NoQA -from ._durable_functions import OrchestrationContext # NoQA +from ._durable_functions import OrchestrationContext, EntityContext # NoQA from .meta import get_binding_registry # NoQA # Import binding implementations to register them @@ -47,6 +47,7 @@ 'KafkaConverter', 'KafkaTriggerConverter', 'OrchestrationContext', + 'EntityContext', 'QueueMessage', 'ServiceBusMessage', 'TimerRequest', diff --git a/tests/test_durable_functions.py b/tests/test_durable_functions.py index 461439c1..dda27701 100644 --- a/tests/test_durable_functions.py +++ b/tests/test_durable_functions.py @@ -5,78 +5,89 @@ import json from azure.functions.durable_functions import ( - OrchestrationTriggerConverter, + OrchestrationTriggerConverter, + EnitityTriggerConverter, ActivityTriggerConverter ) -from azure.functions._durable_functions import OrchestrationContext +from azure.functions._durable_functions import OrchestrationContext, EntityContext from azure.functions.meta import Datum +CONTEXT_CLASSES = [OrchestrationContext, EntityContext] +CONVERTERS = [OrchestrationTriggerConverter, EnitityTriggerConverter] class TestDurableFunctions(unittest.TestCase): - def test_orchestration_context_string_body(self): - raw_string = '{ "name": "great function" }' - context = OrchestrationContext(raw_string) - self.assertIsNotNone(getattr(context, 'body', None)) - - content = json.loads(context.body) - self.assertEqual(content.get('name'), 'great function') - - def test_orchestration_context_string_cast(self): - raw_string = '{ "name": "great function" }' - context = OrchestrationContext(raw_string) - self.assertEqual(str(context), raw_string) - - content = json.loads(str(context)) - self.assertEqual(content.get('name'), 'great function') - - def test_orchestration_context_bytes_body(self): - raw_bytes = '{ "name": "great function" }'.encode('utf-8') - context = OrchestrationContext(raw_bytes) - self.assertIsNotNone(getattr(context, 'body', None)) - - content = json.loads(context.body) - self.assertEqual(content.get('name'), 'great function') - - def test_orchestration_context_bytes_cast(self): - raw_bytes = '{ "name": "great function" }'.encode('utf-8') - context = OrchestrationContext(raw_bytes) - self.assertIsNotNone(getattr(context, 'body', None)) - - content = json.loads(context.body) - self.assertEqual(content.get('name'), 'great function') - - def test_orchestration_trigger_converter(self): + def test_context_string_body(self): + body = '{ "name": "great function" }' + for ctx in CONTEXT_CLASSES: + context = ctx(body) + self.assertIsNotNone(getattr(context, 'body', None)) + + content = json.loads(context.body) + self.assertEqual(content.get('name'), 'great function') + + def test_context_string_cast(self): + body = '{ "name": "great function" }' + for ctx in CONTEXT_CLASSES: + context = ctx(body) + self.assertEqual(str(context), body) + + content = json.loads(str(context)) + self.assertEqual(content.get('name'), 'great function') + + def test_context_bytes_body(self): + body = '{ "name": "great function" }'.encode('utf-8') + for ctx in CONTEXT_CLASSES: + context = ctx(body) + self.assertIsNotNone(getattr(context, 'body', None)) + + content = json.loads(context.body) + self.assertEqual(content.get('name'), 'great function') + + def test_context_bytes_cast(self): + # TODO: this is just like the test above (test_orchestration_context_bytes_body) + body = '{ "name": "great function" }'.encode('utf-8') + for ctx in CONTEXT_CLASSES: + context = ctx(body) + self.assertIsNotNone(getattr(context, 'body', None)) + + content = json.loads(context.body) + self.assertEqual(content.get('name'), 'great function') + + def test_trigger_converter(self): datum = Datum(value='{ "name": "great function" }', type=str) - otc = OrchestrationTriggerConverter.decode(datum, - trigger_metadata=None) - content = json.loads(otc.body) - self.assertEqual(content.get('name'), 'great function') + for converter in CONVERTERS: + otc = converter.decode(datum, trigger_metadata=None) + content = json.loads(otc.body) + self.assertEqual(content.get('name'), 'great function') - def test_orchestration_trigger_converter_type(self): + def test_trigger_converter_type(self): datum = Datum(value='{ "name": "great function" }'.encode('utf-8'), type=bytes) - otc = OrchestrationTriggerConverter.decode(datum, - trigger_metadata=None) - content = json.loads(otc.body) - self.assertEqual(content.get('name'), 'great function') + for converter in CONVERTERS: + otc = converter.decode(datum, trigger_metadata=None) + content = json.loads(otc.body) + self.assertEqual(content.get('name'), 'great function') + + def test_trigger_check_good_annotation(self): - def test_orchestration_trigger_check_good_annotation(self): - for dt in (OrchestrationContext,): + for converter, ctx in zip(CONVERTERS, CONTEXT_CLASSES): self.assertTrue( - OrchestrationTriggerConverter.check_input_type_annotation(dt) + converter.check_input_type_annotation(ctx) ) - def test_orchestration_trigger_check_bad_annotation(self): + def test_trigger_check_bad_annotation(self): for dt in (str, bytes, int): - self.assertFalse( - OrchestrationTriggerConverter.check_input_type_annotation(dt) - ) + for converter in CONVERTERS: + self.assertFalse( + converter.check_input_type_annotation(dt) + ) - def test_orchestration_trigger_has_implicit_return(self): - self.assertTrue( - OrchestrationTriggerConverter.has_implicit_output() - ) + def test_trigger_has_implicit_return(self): + for converter in CONVERTERS: + self.assertTrue( + converter.has_implicit_output() + ) def test_activity_trigger_inputs(self): # Activity Trigger only accept string type from durable extensions From 6ebc042601f561260fa5da9963be39a2d3e8a274 Mon Sep 17 00:00:00 2001 From: David Justo Date: Thu, 10 Sep 2020 10:26:06 -0700 Subject: [PATCH 5/5] flake8 errors now fixed --- tests/test_durable_functions.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/test_durable_functions.py b/tests/test_durable_functions.py index dda27701..1739cdcd 100644 --- a/tests/test_durable_functions.py +++ b/tests/test_durable_functions.py @@ -5,16 +5,20 @@ import json from azure.functions.durable_functions import ( - OrchestrationTriggerConverter, + OrchestrationTriggerConverter, EnitityTriggerConverter, ActivityTriggerConverter ) -from azure.functions._durable_functions import OrchestrationContext, EntityContext +from azure.functions._durable_functions import ( + OrchestrationContext, + EntityContext +) from azure.functions.meta import Datum CONTEXT_CLASSES = [OrchestrationContext, EntityContext] CONVERTERS = [OrchestrationTriggerConverter, EnitityTriggerConverter] + class TestDurableFunctions(unittest.TestCase): def test_context_string_body(self): body = '{ "name": "great function" }' @@ -44,7 +48,8 @@ def test_context_bytes_body(self): self.assertEqual(content.get('name'), 'great function') def test_context_bytes_cast(self): - # TODO: this is just like the test above (test_orchestration_context_bytes_body) + # TODO: this is just like the test above + # (test_orchestration_context_bytes_body) body = '{ "name": "great function" }'.encode('utf-8') for ctx in CONTEXT_CLASSES: context = ctx(body)