Skip to content

Adds Encode and Decode logic for "arbitrary" objects for Durable Python #57

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

Merged
merged 6 commits into from
Apr 24, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions azure/functions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
from ._servicebus import ServiceBusMessage # NoQA
from ._durable_functions import OrchestrationContext # NoQA
from .meta import get_binding_registry # NoQA

# Import binding implementations to register them
from . import blob # NoQA
from . import cosmosdb # NoQA
Expand Down Expand Up @@ -44,7 +43,7 @@
'TimerRequest',

# Middlewares
'WsgiMiddleware',
'WsgiMiddleware'
)

__version__ = '1.2.0'
79 changes: 79 additions & 0 deletions azure/functions/_durable_functions.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,84 @@
from typing import Union
from . import _abc
from importlib import import_module


# Utilities
def _serialize_custom_object(obj):
"""Serialize a user-defined object to JSON.

This function gets called when `json.dumps` cannot serialize
an object and returns a serializable dictionary containing enough
metadata to recontrust the original object.

Parameters
----------
obj: Object
The object to serialize

Returns
-------
dict_obj: A serializable dictionary with enough metadata to reconstruct
`obj`

Exceptions
----------
TypeError:
Raise if `obj` does not contain a `to_json` attribute
"""
# 'safety' guard: raise error if object does not
# support serialization
if not hasattr(obj, "to_json"):
raise TypeError(f"class {type(obj)} does not expose a `to_json` "
"function")
# Encode to json using the object's `to_json`
obj_type = type(obj)
dict_obj = {
"__class__": obj.__class__.__name__,
"__module__": obj.__module__,
"__data__": obj_type.to_json(obj)
}
return dict_obj


def _deserialize_custom_object(obj: dict) -> object:
"""Deserialize a user-defined object from JSON.

Deserializes a dictionary encoding a custom object,
if it contains class metadata suggesting that it should be
decoded further.

Parameters:
----------
obj: dict
Dictionary object that potentially encodes a custom class

Returns:
--------
object
Either the original `obj` dictionary or the custom object it encoded

Exceptions
----------
TypeError
If the decoded object does not contain a `from_json` function
"""
if ("__class__" in obj) and ("__module__" in obj) and ("__data__" in obj):
class_name = obj.pop("__class__")
module_name = obj.pop("__module__")
obj_data = obj.pop("__data__")

# Importing the clas
module = import_module(module_name)
class_ = getattr(module, class_name)

if not hasattr(class_, "from_json"):
raise TypeError(f"class {type(obj)} does not expose a `from_json` "
"function")

# Initialize the object using its `from_json` deserializer
obj = class_.from_json(obj_data)
return obj


class OrchestrationContext(_abc.OrchestrationContext):
Expand Down
7 changes: 4 additions & 3 deletions azure/functions/durable_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import json

from azure.functions import _durable_functions

from . import meta


Expand Down Expand Up @@ -62,7 +61,8 @@ def decode(cls,
# See durable functions library's call_activity_task docs
if data_type == 'string' or data_type == 'json':
try:
result = json.loads(data.value)
callback = _durable_functions._deserialize_custom_object
result = json.loads(data.value, object_hook=callback)
except json.JSONDecodeError:
# String failover if the content is not json serializable
result = data.value
Expand All @@ -80,7 +80,8 @@ def decode(cls,
def encode(cls, obj: typing.Any, *,
expected_type: typing.Optional[type]) -> meta.Datum:
try:
result = json.dumps(obj)
callback = _durable_functions._serialize_custom_object
result = json.dumps(obj, default=callback)
except TypeError:
raise ValueError(
f'activity trigger output must be json serializable ({obj})')
Expand Down