Skip to content

Commit 7a31be1

Browse files
davidmrdavidvrdmr
andauthored
Added support for Entity Trigger (#68)
* added durable entities trigger * fixed style error * more style issues * exported entitycontext, generalized tests * flake8 errors now fixed Co-authored-by: Varad Meru <vrdmr@users.noreply.github.com>
1 parent 18368cb commit 7a31be1

File tree

4 files changed

+130
-55
lines changed

4 files changed

+130
-55
lines changed

azure/functions/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from .kafka import KafkaEvent, KafkaConverter, KafkaTriggerConverter # NoQA
1212
from ._queue import QueueMessage # NoQA
1313
from ._servicebus import ServiceBusMessage # NoQA
14-
from ._durable_functions import OrchestrationContext # NoQA
14+
from ._durable_functions import OrchestrationContext, EntityContext # NoQA
1515
from .meta import get_binding_registry # NoQA
1616

1717
# Import binding implementations to register them
@@ -47,6 +47,7 @@
4747
'KafkaConverter',
4848
'KafkaTriggerConverter',
4949
'OrchestrationContext',
50+
'EntityContext',
5051
'QueueMessage',
5152
'ServiceBusMessage',
5253
'TimerRequest',

azure/functions/_durable_functions.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,3 +109,31 @@ def __repr__(self):
109109

110110
def __str__(self):
111111
return self.__body
112+
113+
114+
class EntityContext(_abc.OrchestrationContext):
115+
"""A durable function entity context.
116+
117+
:param str body:
118+
The body of orchestration context json.
119+
"""
120+
121+
def __init__(self,
122+
body: Union[str, bytes]) -> None:
123+
if isinstance(body, str):
124+
self.__body = body
125+
if isinstance(body, bytes):
126+
self.__body = body.decode('utf-8')
127+
128+
@property
129+
def body(self) -> str:
130+
return self.__body
131+
132+
def __repr__(self):
133+
return (
134+
f'<azure.EntityContext '
135+
f'body={self.body}>'
136+
)
137+
138+
def __str__(self):
139+
return self.__body

azure/functions/durable_functions.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,36 @@ def has_implicit_output(cls) -> bool:
3939
return True
4040

4141

42+
class EnitityTriggerConverter(meta.InConverter,
43+
meta.OutConverter,
44+
binding='entityTrigger',
45+
trigger=True):
46+
@classmethod
47+
def check_input_type_annotation(cls, pytype):
48+
return issubclass(pytype, _durable_functions.EntityContext)
49+
50+
@classmethod
51+
def check_output_type_annotation(cls, pytype):
52+
# Implicit output should accept any return type
53+
return True
54+
55+
@classmethod
56+
def decode(cls,
57+
data: meta.Datum, *,
58+
trigger_metadata) -> _durable_functions.EntityContext:
59+
return _durable_functions.EntityContext(data.value)
60+
61+
@classmethod
62+
def encode(cls, obj: typing.Any, *,
63+
expected_type: typing.Optional[type]) -> meta.Datum:
64+
# Durable function context should be a json
65+
return meta.Datum(type='json', value=obj)
66+
67+
@classmethod
68+
def has_implicit_output(cls) -> bool:
69+
return True
70+
71+
4272
# Durable Function Activity Trigger
4373
class ActivityTriggerConverter(meta.InConverter,
4474
meta.OutConverter,

tests/test_durable_functions.py

Lines changed: 70 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -6,77 +6,93 @@
66

77
from azure.functions.durable_functions import (
88
OrchestrationTriggerConverter,
9+
EnitityTriggerConverter,
910
ActivityTriggerConverter
1011
)
11-
from azure.functions._durable_functions import OrchestrationContext
12+
from azure.functions._durable_functions import (
13+
OrchestrationContext,
14+
EntityContext
15+
)
1216
from azure.functions.meta import Datum
1317

18+
CONTEXT_CLASSES = [OrchestrationContext, EntityContext]
19+
CONVERTERS = [OrchestrationTriggerConverter, EnitityTriggerConverter]
1420

15-
class TestDurableFunctions(unittest.TestCase):
16-
def test_orchestration_context_string_body(self):
17-
raw_string = '{ "name": "great function" }'
18-
context = OrchestrationContext(raw_string)
19-
self.assertIsNotNone(getattr(context, 'body', None))
20-
21-
content = json.loads(context.body)
22-
self.assertEqual(content.get('name'), 'great function')
23-
24-
def test_orchestration_context_string_cast(self):
25-
raw_string = '{ "name": "great function" }'
26-
context = OrchestrationContext(raw_string)
27-
self.assertEqual(str(context), raw_string)
28-
29-
content = json.loads(str(context))
30-
self.assertEqual(content.get('name'), 'great function')
31-
32-
def test_orchestration_context_bytes_body(self):
33-
raw_bytes = '{ "name": "great function" }'.encode('utf-8')
34-
context = OrchestrationContext(raw_bytes)
35-
self.assertIsNotNone(getattr(context, 'body', None))
36-
37-
content = json.loads(context.body)
38-
self.assertEqual(content.get('name'), 'great function')
39-
40-
def test_orchestration_context_bytes_cast(self):
41-
raw_bytes = '{ "name": "great function" }'.encode('utf-8')
42-
context = OrchestrationContext(raw_bytes)
43-
self.assertIsNotNone(getattr(context, 'body', None))
4421

45-
content = json.loads(context.body)
46-
self.assertEqual(content.get('name'), 'great function')
47-
48-
def test_orchestration_trigger_converter(self):
22+
class TestDurableFunctions(unittest.TestCase):
23+
def test_context_string_body(self):
24+
body = '{ "name": "great function" }'
25+
for ctx in CONTEXT_CLASSES:
26+
context = ctx(body)
27+
self.assertIsNotNone(getattr(context, 'body', None))
28+
29+
content = json.loads(context.body)
30+
self.assertEqual(content.get('name'), 'great function')
31+
32+
def test_context_string_cast(self):
33+
body = '{ "name": "great function" }'
34+
for ctx in CONTEXT_CLASSES:
35+
context = ctx(body)
36+
self.assertEqual(str(context), body)
37+
38+
content = json.loads(str(context))
39+
self.assertEqual(content.get('name'), 'great function')
40+
41+
def test_context_bytes_body(self):
42+
body = '{ "name": "great function" }'.encode('utf-8')
43+
for ctx in CONTEXT_CLASSES:
44+
context = ctx(body)
45+
self.assertIsNotNone(getattr(context, 'body', None))
46+
47+
content = json.loads(context.body)
48+
self.assertEqual(content.get('name'), 'great function')
49+
50+
def test_context_bytes_cast(self):
51+
# TODO: this is just like the test above
52+
# (test_orchestration_context_bytes_body)
53+
body = '{ "name": "great function" }'.encode('utf-8')
54+
for ctx in CONTEXT_CLASSES:
55+
context = ctx(body)
56+
self.assertIsNotNone(getattr(context, 'body', None))
57+
58+
content = json.loads(context.body)
59+
self.assertEqual(content.get('name'), 'great function')
60+
61+
def test_trigger_converter(self):
4962
datum = Datum(value='{ "name": "great function" }',
5063
type=str)
51-
otc = OrchestrationTriggerConverter.decode(datum,
52-
trigger_metadata=None)
53-
content = json.loads(otc.body)
54-
self.assertEqual(content.get('name'), 'great function')
64+
for converter in CONVERTERS:
65+
otc = converter.decode(datum, trigger_metadata=None)
66+
content = json.loads(otc.body)
67+
self.assertEqual(content.get('name'), 'great function')
5568

56-
def test_orchestration_trigger_converter_type(self):
69+
def test_trigger_converter_type(self):
5770
datum = Datum(value='{ "name": "great function" }'.encode('utf-8'),
5871
type=bytes)
59-
otc = OrchestrationTriggerConverter.decode(datum,
60-
trigger_metadata=None)
61-
content = json.loads(otc.body)
62-
self.assertEqual(content.get('name'), 'great function')
72+
for converter in CONVERTERS:
73+
otc = converter.decode(datum, trigger_metadata=None)
74+
content = json.loads(otc.body)
75+
self.assertEqual(content.get('name'), 'great function')
6376

64-
def test_orchestration_trigger_check_good_annotation(self):
65-
for dt in (OrchestrationContext,):
77+
def test_trigger_check_good_annotation(self):
78+
79+
for converter, ctx in zip(CONVERTERS, CONTEXT_CLASSES):
6680
self.assertTrue(
67-
OrchestrationTriggerConverter.check_input_type_annotation(dt)
81+
converter.check_input_type_annotation(ctx)
6882
)
6983

70-
def test_orchestration_trigger_check_bad_annotation(self):
84+
def test_trigger_check_bad_annotation(self):
7185
for dt in (str, bytes, int):
72-
self.assertFalse(
73-
OrchestrationTriggerConverter.check_input_type_annotation(dt)
74-
)
86+
for converter in CONVERTERS:
87+
self.assertFalse(
88+
converter.check_input_type_annotation(dt)
89+
)
7590

76-
def test_orchestration_trigger_has_implicit_return(self):
77-
self.assertTrue(
78-
OrchestrationTriggerConverter.has_implicit_output()
79-
)
91+
def test_trigger_has_implicit_return(self):
92+
for converter in CONVERTERS:
93+
self.assertTrue(
94+
converter.has_implicit_output()
95+
)
8096

8197
def test_activity_trigger_inputs(self):
8298
# Activity Trigger only accept string type from durable extensions

0 commit comments

Comments
 (0)