Make portable objects saveable

The @anvil.server.portable_class decorator adds the machinery required to manage the serialization so an object can be passed between client and server.

Please make portable classes also saveable to a simple object column.

I imagine the serialization required for the transfer between server and client shouldn’t be too different from the serialization required to make an object compatible with simple object columns.

If my imagination is too far fetched, then please create a dedicated decorator @anvil.server.simpleobject

6 Likes

Thus far, the only difference between the two, that I know of, is that RPCs support temporal types (date, datetime) and SimpleObject does not.

3 Likes

I agree. I thought I would lump them together because they both need serialization/deserialization, even if they have different requirements at lower level.

It would be nice to have one decorator that takes care of both the tasks.
It would be nice to define the custom serialization only once.
It would be nice to be able to save objects with datetime values to a simple object column with just a decorator.

Today I was happy that adding the @anvil.server.portable_class decorator saved me the time to manage the serialization/deserialization of my classes, then, 30 minutes later when it was time to store to a simple object column, I was working on the serialization/deserialization anyway.

3 Likes

Agreed, 100%. Portable Classes are so close to that goal! One small step… one giant leap in usability.

2 Likes

I wrote a couple of functions to help convert values between SimpleObject and RPC formats.

I’m sure folks can improve on these…

"""Server Module simple_object"""

# std lib
from datetime import date, datetime

NoneType = type(None)


def to_simple_object(value):
    """Convert a serialized value, which is a scalar, dict, or list,
    (i.e., an anvil.server.call() parameter or return value)
    into something that can be stored in a Simple Object column.
    """
    t = type(value)
    if t in {NoneType, str, int, float, bool}:
        return value
    if t == list:
        return [to_simple_object(item) for item in value]
    if t == dict:
        return {str(k): to_simple_object(v) for k, v in value.items()}
    if t == date:
        return {"__serialized_date": value.isoformat()}
    if t == datetime:
        return {"__serialized_datetime": value.isoformat()}
    # if t == time:
    #  return {'__serialized_time': value.isoformat()}


def from_simple_object(value):
    """Convert a value retrieved from a Simple Object column
    into something that can be used as
    an anvil.server.call() parameter or return value."""
    t = type(value)
    if t in {NoneType, str, int, float, bool, date, datetime}:
        return value
    if t == list:
        return [from_simple_object(item) for item in value]
    if t == dict:
        if len(value) == 1:
            if "__serialized_date" in value:
                return date.fromisoformat(value["__serialized_date"])
            if "__serialized_datetime" in value:
                return datetime.fromisoformat(value["__serialized_datetime"])
            # if '__serialized_time' in value:
            #  return time.fromisoformat(value['__serialized_time'])
        return {k: from_simple_object(v) for k, v in value.items()}


# These functions are noramlly used only on the server side,
# immediately before saving (or after retrieving) a Simple Object value,
# so that the network transmission uses real datetime values.
# The advantage: the receiving end gets a value *with timezone*, resolving
# any ambiguity about which point in time it actually represents.

# It's possible to generalize these functions, however, to cover
# additional types, e.g., time, or user-defined types.  In that case,
# you'd need to do those conversions *before* transmittal over the wire.

# These are the situations that Portable Objects were *intended* to address.
# However, if the value is going directly into a Simple Object column, then
# Portable Objects add overhead.  Each Portable Object is re-constituted,
# on the receiving end (e.g., the server).  But then it would have to be
# converted *back* to wire format for saving in the column.  That's a waste
# of server time.

# If you do decide to generalize these functions, I have recommendations:
# 1. Make them table-driven.  This makes them open-ended about the types,
#    the type-name tags, and the translation functions.
# 2. Don't hack up the above functions.  Instead, give the extended versions
#    their own distinct names.  This lets the server-side continue to use
#    the old functions, where appropriate.

3 Likes