Skip to content

support custom Serializer class #56

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

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
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
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ this mixin definitely suits you.
- [Advanced usage](#Advanced-usage)
- [Custom formats](#Custom-formats)
- [Custom types](#Custom-types)
- [Custom Serializer](#Custom-serializer)
- [Timezones](#Timezones)
- [Troubleshooting](#Troubleshooting)
- [Tests](#Tests)
Expand Down Expand Up @@ -304,6 +305,27 @@ Unfortunately you can not access formats or tzinfo in that functions.
I'll implement this logic later if any of users needs it.


# Custom Serializer
To have full control over the Serializer, you can define your own subclass with custom logic, and then configure the mixin to use it.
```python
from sqlalchemy_serializer import Serializer, SerializerMixin


class CustomSerializer(Serializer):

def serialize_model(self, value) -> dict:
"""Custom override adding special case for a complex model."""
if isinstance(value, ComplexModel):
return complex_logic(value)
return super().serialize_model(value)


class CustomSerializerMixin(SerializerMixin):

serialize_class = CustomSerializer
```


# Timezones
To keep `datetimes` consistent its better to store it in the database normalized to **UTC**.
But when you return response, sometimes (mostly in web, mobile applications can do it themselves)
Expand Down
173 changes: 88 additions & 85 deletions sqlalchemy_serializer/serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,90 +19,20 @@
logger.setLevel(level="WARN")


class SerializerMixin:
"""
Mixin for retrieving public fields of sqlAlchemy-model in json-compatible format
with no pain
It can be inherited to redefine get_tzinfo callback, datetime formats or to add
some extra serialization logic
"""

# Default exclusive schema.
# If left blank, serializer becomes greedy and takes all SQLAlchemy-model's attributes
serialize_only: tuple = ()

# Additions to default schema. Can include negative rules
serialize_rules: tuple = ()

# Extra serialising functions
serialize_types: tuple = ()

# Custom list of fields to serialize in this model
serializable_keys: tuple = ()

date_format = "%Y-%m-%d"
datetime_format = "%Y-%m-%d %H:%M:%S"
time_format = "%H:%M"
decimal_format = "{}"

# Serialize fields of the model defined as @property automatically
auto_serialize_properties: bool = False

def get_tzinfo(self):
"""
Callback to make serializer aware of user's timezone. Should be redefined if needed
Example:
return pytz.timezone('Africa/Abidjan')

:return: datetime.tzinfo
"""
return None

def to_dict(
self,
only=(),
rules=(),
date_format=None,
datetime_format=None,
time_format=None,
tzinfo=None,
decimal_format=None,
serialize_types=None,
):
"""
Returns SQLAlchemy model's data in JSON compatible format

For details about datetime formats follow:
https://docs.python.org/3/library/datetime.html#strftime-and-strptime-behavior

:param only: exclusive schema to replace the default one
always have higher priority than rules
:param rules: schema to extend default one or schema defined in "only"
:param date_format: str
:param datetime_format: str
:param time_format: str
:param decimal_format: str
:param serialize_types:
:param tzinfo: datetime.tzinfo converts datetimes to local user timezone
:return: data: dict
"""
s = Serializer(
date_format=date_format or self.date_format,
datetime_format=datetime_format or self.datetime_format,
time_format=time_format or self.time_format,
decimal_format=decimal_format or self.decimal_format,
tzinfo=tzinfo or self.get_tzinfo(),
serialize_types=serialize_types or self.serialize_types,
)
return s(self, only=only, extend=rules)


Options = namedtuple(
"Options",
"date_format datetime_format time_format decimal_format tzinfo serialize_types",
)


class IsNotSerializable(Exception):
pass


def get_type(value) -> str:
return type(value).__name__


class Serializer:
# Types that do nod need any serialization logic
atomic_types = (
Expand Down Expand Up @@ -193,7 +123,7 @@ def fork(self, key: str) -> "Serializer":
Return new serializer for a key
:return: serializer
"""
serializer = Serializer(**self.opts._asdict())
serializer = self.__class__(**self.opts._asdict())
serializer.set_serialization_depth(self.serialization_depth + 1)
serializer.schema = self.schema.fork(key=key)

Expand Down Expand Up @@ -291,13 +221,86 @@ def serialize_model(self, value) -> dict:
return res


class IsNotSerializable(Exception):
pass
def serialize_collection(iterable: t.Iterable, *args, **kwargs) -> list:
return [item.to_dict(*args, **kwargs) for item in iterable]


def get_type(value) -> str:
return type(value).__name__
class SerializerMixin:
"""
Mixin for retrieving public fields of sqlAlchemy-model in json-compatible format
with no pain
It can be inherited to redefine get_tzinfo callback, datetime formats or to add
some extra serialization logic
"""

# Default Serializer class
serialize_class: Serializer = Serializer

def serialize_collection(iterable: t.Iterable, *args, **kwargs) -> list:
return [item.to_dict(*args, **kwargs) for item in iterable]
# Default exclusive schema.
# If left blank, serializer becomes greedy and takes all SQLAlchemy-model's attributes
serialize_only: tuple = ()

# Additions to default schema. Can include negative rules
serialize_rules: tuple = ()

# Extra serialising functions
serialize_types: tuple = ()

# Custom list of fields to serialize in this model
serializable_keys: tuple = ()

date_format = "%Y-%m-%d"
datetime_format = "%Y-%m-%d %H:%M:%S"
time_format = "%H:%M"
decimal_format = "{}"

# Serialize fields of the model defined as @property automatically
auto_serialize_properties: bool = False

def get_tzinfo(self):
"""
Callback to make serializer aware of user's timezone. Should be redefined if needed
Example:
return pytz.timezone('Africa/Abidjan')

:return: datetime.tzinfo
"""
return None

def to_dict(
self,
only=(),
rules=(),
date_format=None,
datetime_format=None,
time_format=None,
tzinfo=None,
decimal_format=None,
serialize_types=None,
):
"""
Returns SQLAlchemy model's data in JSON compatible format

For details about datetime formats follow:
https://docs.python.org/3/library/datetime.html#strftime-and-strptime-behavior

:param only: exclusive schema to replace the default one
always have higher priority than rules
:param rules: schema to extend default one or schema defined in "only"
:param date_format: str
:param datetime_format: str
:param time_format: str
:param decimal_format: str
:param serialize_types:
:param tzinfo: datetime.tzinfo converts datetimes to local user timezone
:return: data: dict
"""
s = self.serialize_class(
date_format=date_format or self.date_format,
datetime_format=datetime_format or self.datetime_format,
time_format=time_format or self.time_format,
decimal_format=decimal_format or self.decimal_format,
tzinfo=tzinfo or self.get_tzinfo(),
serialize_types=serialize_types or self.serialize_types,
)
return s(self, only=only, extend=rules)
File renamed without changes.
49 changes: 49 additions & 0 deletions tests/test_custom_serializer_class.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import sqlalchemy as sa
from sqlalchemy_serializer import SerializerMixin, Serializer

from .models import (
DATETIME,
Base,
)


CUSTOM_DICT_VALUE = {'CustomModelTwo': 'test value'}


class CustomSerializer(Serializer):
def serialize(self, value, **kwargs):
# special case for CustomModelTwo, returning string instead of real work
if isinstance(value, CustomModelTwo):
return CUSTOM_DICT_VALUE
return super().serialize(value, **kwargs)


class CustomSerializerMixin(SerializerMixin):
serializer_class = CustomSerializer


class CustomModelOne(Base, CustomSerializerMixin):
__tablename__ = "custom_model_one"
id = sa.Column(sa.Integer, primary_key=True)
datetime = sa.Column(sa.DateTime, default=DATETIME)


class CustomModelTwo(CustomModelOne):
__tablename__ = "custom_model_two"



def test_custom_serializer(get_instance):
"""
Very basic test to ensure custom serializer is used
"""
# Get instance for CustomModelOne, which should serialize normally
i = get_instance(CustomModelOne)
data = i.to_dict()
# Check model was processed correctly
assert "datetime" in data
assert data["datetime"] == DATETIME.strftime(i.datetime_format)
# Same for CustomModelTwo, which should instead return only a simple dict
i = get_instance(CustomModelTwo)
data = i.to_dict()
assert data == CUSTOM_DICT_VALUE