diff --git a/README.md b/README.md index df57ecf..f8b33c9 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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) diff --git a/sqlalchemy_serializer/serializer.py b/sqlalchemy_serializer/serializer.py index 0ab1c97..ff13f98 100644 --- a/sqlalchemy_serializer/serializer.py +++ b/sqlalchemy_serializer/serializer.py @@ -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 = ( @@ -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) @@ -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) diff --git a/tests/test_custom_serializer.py b/tests/test_custom_model.py similarity index 100% rename from tests/test_custom_serializer.py rename to tests/test_custom_model.py diff --git a/tests/test_custom_serializer_class.py b/tests/test_custom_serializer_class.py new file mode 100644 index 0000000..8a4ada2 --- /dev/null +++ b/tests/test_custom_serializer_class.py @@ -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