|
5 | 5 | import json |
6 | 6 |
|
7 | 7 | from azure.functions import _durable_functions |
| 8 | +from azure.functions.decorators.durable_functions import get_durable_package |
8 | 9 | from . import meta |
9 | 10 |
|
| 11 | +import logging |
| 12 | +_logger = logging.getLogger('azure.functions.AsgiMiddleware') |
10 | 13 |
|
11 | | -# Durable Function Orchestration Trigger |
12 | | -class OrchestrationTriggerConverter(meta.InConverter, |
| 14 | +# ---------------- Legacy Durable Functions Converters ---------------- # |
| 15 | +# Legacy Durable Function Orchestration Trigger |
| 16 | +class LegacyOrchestrationTriggerConverter(meta.InConverter, |
13 | 17 | meta.OutConverter, |
14 | | - binding='orchestrationTrigger', |
| 18 | + binding=None, |
15 | 19 | trigger=True): |
16 | 20 | @classmethod |
17 | 21 | def check_input_type_annotation(cls, pytype): |
@@ -39,9 +43,196 @@ def has_implicit_output(cls) -> bool: |
39 | 43 | return True |
40 | 44 |
|
41 | 45 |
|
| 46 | +# Legacy Durable Function Entity Trigger |
| 47 | +class LegacyEnitityTriggerConverter(meta.InConverter, |
| 48 | + meta.OutConverter, |
| 49 | + binding=None, |
| 50 | + trigger=True): |
| 51 | + @classmethod |
| 52 | + def check_input_type_annotation(cls, pytype): |
| 53 | + return issubclass(pytype, _durable_functions.EntityContext) |
| 54 | + |
| 55 | + @classmethod |
| 56 | + def check_output_type_annotation(cls, pytype): |
| 57 | + # Implicit output should accept any return type |
| 58 | + return True |
| 59 | + |
| 60 | + @classmethod |
| 61 | + def decode(cls, |
| 62 | + data: meta.Datum, *, |
| 63 | + trigger_metadata) -> _durable_functions.EntityContext: |
| 64 | + return _durable_functions.EntityContext(data.value) |
| 65 | + |
| 66 | + @classmethod |
| 67 | + def encode(cls, obj: typing.Any, *, |
| 68 | + expected_type: typing.Optional[type]) -> meta.Datum: |
| 69 | + # Durable function context should be a json |
| 70 | + return meta.Datum(type='json', value=obj) |
| 71 | + |
| 72 | + @classmethod |
| 73 | + def has_implicit_output(cls) -> bool: |
| 74 | + return True |
| 75 | + |
| 76 | + |
| 77 | +# Legacy Durable Function Activity Trigger |
| 78 | +class LegacyActivityTriggerConverter(meta.InConverter, |
| 79 | + meta.OutConverter, |
| 80 | + binding=None, |
| 81 | + trigger=True): |
| 82 | + @classmethod |
| 83 | + def check_input_type_annotation(cls, pytype): |
| 84 | + # Activity Trigger's arguments should accept any types |
| 85 | + return True |
| 86 | + |
| 87 | + @classmethod |
| 88 | + def check_output_type_annotation(cls, pytype): |
| 89 | + # The activity trigger should accept any JSON serializable types |
| 90 | + return True |
| 91 | + |
| 92 | + @classmethod |
| 93 | + def decode(cls, |
| 94 | + data: meta.Datum, *, |
| 95 | + trigger_metadata) -> typing.Any: |
| 96 | + data_type = data.type |
| 97 | + |
| 98 | + # Durable functions extension always returns a string of json |
| 99 | + # See durable functions library's call_activity_task docs |
| 100 | + if data_type in ['string', 'json']: |
| 101 | + try: |
| 102 | + callback = _durable_functions._deserialize_custom_object |
| 103 | + result = json.loads(data.value, object_hook=callback) |
| 104 | + except json.JSONDecodeError: |
| 105 | + # String failover if the content is not json serializable |
| 106 | + result = data.value |
| 107 | + except Exception as e: |
| 108 | + raise ValueError( |
| 109 | + 'activity trigger input must be a string or a ' |
| 110 | + f'valid json serializable ({data.value})') from e |
| 111 | + else: |
| 112 | + raise NotImplementedError( |
| 113 | + f'unsupported activity trigger payload type: {data_type}') |
| 114 | + |
| 115 | + return result |
| 116 | + |
| 117 | + @classmethod |
| 118 | + def encode(cls, obj: typing.Any, *, |
| 119 | + expected_type: typing.Optional[type]) -> meta.Datum: |
| 120 | + try: |
| 121 | + callback = _durable_functions._serialize_custom_object |
| 122 | + result = json.dumps(obj, default=callback) |
| 123 | + except TypeError as e: |
| 124 | + raise ValueError( |
| 125 | + f'activity trigger output must be json serializable ({obj})') from e |
| 126 | + |
| 127 | + return meta.Datum(type='json', value=result) |
| 128 | + |
| 129 | + @classmethod |
| 130 | + def has_implicit_output(cls) -> bool: |
| 131 | + return True |
| 132 | + |
| 133 | + |
| 134 | +# Legacy Durable Functions Durable Client Bindings |
| 135 | +class LegacyDurableClientConverter(meta.InConverter, |
| 136 | + meta.OutConverter, |
| 137 | + binding=None): |
| 138 | + @classmethod |
| 139 | + def has_implicit_output(cls) -> bool: |
| 140 | + return False |
| 141 | + |
| 142 | + @classmethod |
| 143 | + def has_trigger_support(cls) -> bool: |
| 144 | + return False |
| 145 | + |
| 146 | + @classmethod |
| 147 | + def check_input_type_annotation(cls, pytype: type) -> bool: |
| 148 | + return issubclass(pytype, (str, bytes)) |
| 149 | + |
| 150 | + @classmethod |
| 151 | + def check_output_type_annotation(cls, pytype: type) -> bool: |
| 152 | + return issubclass(pytype, (str, bytes, bytearray)) |
| 153 | + |
| 154 | + @classmethod |
| 155 | + def encode(cls, obj: typing.Any, *, |
| 156 | + expected_type: typing.Optional[type]) -> meta.Datum: |
| 157 | + if isinstance(obj, str): |
| 158 | + return meta.Datum(type='string', value=obj) |
| 159 | + |
| 160 | + elif isinstance(obj, (bytes, bytearray)): |
| 161 | + return meta.Datum(type='bytes', value=bytes(obj)) |
| 162 | + elif obj is None: |
| 163 | + return meta.Datum(type=None, value=obj) |
| 164 | + elif isinstance(obj, dict): |
| 165 | + return meta.Datum(type='dict', value=obj) |
| 166 | + elif isinstance(obj, list): |
| 167 | + return meta.Datum(type='list', value=obj) |
| 168 | + elif isinstance(obj, bool): |
| 169 | + return meta.Datum(type='bool', value=obj) |
| 170 | + elif isinstance(obj, int): |
| 171 | + return meta.Datum(type='int', value=obj) |
| 172 | + elif isinstance(obj, float): |
| 173 | + return meta.Datum(type='double', value=obj) |
| 174 | + else: |
| 175 | + raise NotImplementedError |
| 176 | + |
| 177 | + @classmethod |
| 178 | + def decode(cls, data: meta.Datum, *, trigger_metadata) -> typing.Any: |
| 179 | + if data is None: |
| 180 | + return None |
| 181 | + data_type = data.type |
| 182 | + |
| 183 | + if data_type == 'string': |
| 184 | + result = data.value |
| 185 | + elif data_type == 'bytes': |
| 186 | + result = data.value |
| 187 | + elif data_type == 'json': |
| 188 | + result = data.value |
| 189 | + elif data_type is None: |
| 190 | + result = None |
| 191 | + else: |
| 192 | + raise ValueError( |
| 193 | + 'unexpected type of data received for the "generic" binding ', |
| 194 | + repr(data_type) |
| 195 | + ) |
| 196 | + |
| 197 | + return result |
| 198 | + |
| 199 | + |
| 200 | +# ---------------- Durable Task Durable Functions Converters ---------------- # |
| 201 | +# Durable Function Orchestration Trigger |
| 202 | +class OrchestrationTriggerConverter(meta.InConverter, |
| 203 | + meta.OutConverter, |
| 204 | + binding=None, |
| 205 | + trigger=True): |
| 206 | + @classmethod |
| 207 | + def check_input_type_annotation(cls, pytype): |
| 208 | + return issubclass(pytype, _durable_functions.OrchestrationContext) |
| 209 | + |
| 210 | + @classmethod |
| 211 | + def check_output_type_annotation(cls, pytype): |
| 212 | + # Implicit output should accept any return type |
| 213 | + return True |
| 214 | + |
| 215 | + @classmethod |
| 216 | + def decode(cls, |
| 217 | + data: meta.Datum, *, |
| 218 | + trigger_metadata) -> _durable_functions.OrchestrationContext: |
| 219 | + return _durable_functions.OrchestrationContext(data.value) |
| 220 | + |
| 221 | + @classmethod |
| 222 | + def encode(cls, obj: typing.Any, *, |
| 223 | + expected_type: typing.Optional[type]) -> meta.Datum: |
| 224 | + # Durable function context should be a string |
| 225 | + return meta.Datum(type='string', value=obj) |
| 226 | + |
| 227 | + @classmethod |
| 228 | + def has_implicit_output(cls) -> bool: |
| 229 | + return True |
| 230 | + |
| 231 | + |
| 232 | +# Durable Function Entity Trigger |
42 | 233 | class EnitityTriggerConverter(meta.InConverter, |
43 | 234 | meta.OutConverter, |
44 | | - binding='entityTrigger', |
| 235 | + binding=None, |
45 | 236 | trigger=True): |
46 | 237 | @classmethod |
47 | 238 | def check_input_type_annotation(cls, pytype): |
@@ -72,7 +263,7 @@ def has_implicit_output(cls) -> bool: |
72 | 263 | # Durable Function Activity Trigger |
73 | 264 | class ActivityTriggerConverter(meta.InConverter, |
74 | 265 | meta.OutConverter, |
75 | | - binding='activityTrigger', |
| 266 | + binding=None, |
76 | 267 | trigger=True): |
77 | 268 | @classmethod |
78 | 269 | def check_input_type_annotation(cls, pytype): |
@@ -129,7 +320,7 @@ def has_implicit_output(cls) -> bool: |
129 | 320 | # Durable Functions Durable Client Bindings |
130 | 321 | class DurableClientConverter(meta.InConverter, |
131 | 322 | meta.OutConverter, |
132 | | - binding='durableClient'): |
| 323 | + binding=None): |
133 | 324 | @classmethod |
134 | 325 | def has_implicit_output(cls) -> bool: |
135 | 326 | return False |
@@ -190,3 +381,34 @@ def decode(cls, data: meta.Datum, *, trigger_metadata) -> typing.Any: |
190 | 381 | ) |
191 | 382 |
|
192 | 383 | return result |
| 384 | + |
| 385 | + |
| 386 | +def register_durable_converters(): |
| 387 | + _logger.info("Registering Durable Functions converters based on ") |
| 388 | + pkg = get_durable_package() |
| 389 | + if pkg is None: |
| 390 | + # Durable library not installed → do nothing |
| 391 | + return |
| 392 | + |
| 393 | + _logger.info("Durable Functions package loaded: %s", pkg.__name__) |
| 394 | + _logger.info("Current bindings before registration: %s", meta._ConverterMeta._bindings) |
| 395 | + # Clear existing bindings if they exist |
| 396 | + meta._ConverterMeta._bindings.pop("orchestrationTrigger", None) |
| 397 | + meta._ConverterMeta._bindings.pop("entityTrigger", None) |
| 398 | + meta._ConverterMeta._bindings.pop("activityTrigger", None) |
| 399 | + meta._ConverterMeta._bindings.pop("durableClient", None) |
| 400 | + |
| 401 | + if pkg.__name__ == "azure.durable_functions": |
| 402 | + _logger.info("Registering Legacy Durable Functions converters.") |
| 403 | + meta._ConverterMeta._bindings["orchestrationTrigger"] = LegacyOrchestrationTriggerConverter |
| 404 | + meta._ConverterMeta._bindings["entityTrigger"] = LegacyEnitityTriggerConverter |
| 405 | + meta._ConverterMeta._bindings["activityTrigger"] = LegacyActivityTriggerConverter |
| 406 | + meta._ConverterMeta._bindings["durableClient"] = LegacyDurableClientConverter |
| 407 | + else: |
| 408 | + _logger.info("Registering Durable Task Durable Functions converters.") |
| 409 | + meta._ConverterMeta._bindings["orchestrationTrigger"] = OrchestrationTriggerConverter |
| 410 | + meta._ConverterMeta._bindings["entityTrigger"] = EnitityTriggerConverter |
| 411 | + meta._ConverterMeta._bindings["activityTrigger"] = ActivityTriggerConverter |
| 412 | + meta._ConverterMeta._bindings["durableClient"] = DurableClientConverter |
| 413 | + _logger.info("Durable Functions converters registered.") |
| 414 | + _logger.info("Current bindings after registration: %s", meta._ConverterMeta._bindings) |
0 commit comments