blob: 6ac9ff38144ccbf20b286177a7127a29c8269371 [file] [log] [blame]
"""
Used to create Django Rest Framework serializers for Apache Thrift Data Types
"""
import copy
import datetime
import logging
from rest_framework.serializers import (
BooleanField,
CharField,
DateTimeField,
DecimalField,
DictField,
Field,
IntegerField,
ListField,
ListSerializer,
Serializer,
SerializerMetaclass,
ValidationError
)
from thrift.Thrift import TType
logger = logging.getLogger(__name__)
# used to map apache thrift data types to django serializer fields
mapping = {
TType.STRING: CharField,
TType.I08: IntegerField,
TType.I16: IntegerField,
TType.I32: IntegerField,
TType.I64: IntegerField,
TType.DOUBLE: DecimalField,
TType.BOOL: BooleanField,
TType.MAP: DictField
}
class UTCPosixTimestampDateTimeField(DateTimeField):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.default = self.current_time_ms
self.initial = self.initial_value
self.required = False
def to_representation(self, obj):
# Create datetime instance from milliseconds that is aware of timezon
dt = datetime.datetime.fromtimestamp(obj / 1000, datetime.timezone.utc)
return super().to_representation(dt)
def to_internal_value(self, data):
dt = super().to_internal_value(data)
return int(dt.timestamp() * 1000)
def initial_value(self):
return self.to_representation(self.current_time_ms())
def current_time_ms(self):
return int(datetime.datetime.utcnow().timestamp() * 1000)
class ThriftEnumField(Field):
def __init__(self, enumClass, *args, **kwargs):
super().__init__(*args, **kwargs)
self.enumClass = enumClass
def to_representation(self, obj):
if obj is None:
return None
return self.enumClass._VALUES_TO_NAMES[obj]
def to_internal_value(self, data):
if self.allow_null and data is None:
return None
if data not in self.enumClass._NAMES_TO_VALUES:
raise ValidationError(
"Not an allowed name of enum {}".format(
self.enumClass.__name__))
return self.enumClass._NAMES_TO_VALUES.get(data, None)
def create_serializer(thrift_data_type, enable_date_time_conversion=False, **kwargs):
"""
Create django rest framework serializer based on the thrift data type
:param thrift_data_type: Thrift data type
:param kwargs: Other Django Framework Serializer initialization parameters
:param enable_date_time_conversion: enable conversion of field with name ending with time to
UTCPosixTimestampDateTimeField instead of IntegerField
:return: instance of custom serializer for the given thrift data type
"""
return create_serializer_class(thrift_data_type, enable_date_time_conversion)(**kwargs)
def create_serializer_class(thrift_data_type, enable_date_time_conversion=False):
class CustomSerializerMeta(SerializerMetaclass):
def __new__(cls, name, bases, attrs):
meta = attrs.get('Meta', None)
thrift_spec = thrift_data_type.thrift_spec
for field in thrift_spec:
# Don't replace existing attrs to allow subclasses to override
if field and field[2] not in attrs:
required = (field[2] in meta.required
if meta and hasattr(meta, 'required')
else False)
read_only = (field[2] in meta.read_only
if meta and hasattr(meta, 'read_only')
else False)
allow_null = not required
field_serializer = process_field(
field, enable_date_time_conversion, required=required, read_only=read_only,
allow_null=allow_null)
attrs[field[2]] = field_serializer
return super().__new__(cls, name, bases, attrs)
class CustomSerializer(Serializer, metaclass=CustomSerializerMeta):
"""
Custom Serializer which handle the list fields which holds custom class objects
"""
def process_nested_fields(self, validated_data):
fields = self.fields
params = copy.deepcopy(validated_data)
for field_name, serializer in fields.items():
if (isinstance(serializer, ListField) or
isinstance(serializer, ListSerializer)):
if (params.get(field_name, None) is not None or
not serializer.allow_null):
if isinstance(serializer.child, Serializer):
params[field_name] = [serializer.child.create(
item) for item in params[field_name]]
else:
params[field_name] = serializer.to_representation(
params[field_name])
elif isinstance(serializer, Serializer):
if field_name in params and params[field_name] is not None:
params[field_name] = serializer.create(
params[field_name])
return params
def create(self, validated_data):
params = self.process_nested_fields(validated_data)
return thrift_data_type(**params)
def update(self, instance, validated_data):
return self.create(validated_data)
return CustomSerializer
def process_field(field, enable_date_time_conversion, required=False, read_only=False, allow_null=False):
"""
Used to process a thrift data type field
:param field:
:param required:
:param read_only:
:param allow_null:
:return:
"""
if field[1] in mapping:
# handling scenarios when the thrift field type is present in the
# mapping
field_class = mapping[field[1]]
kwargs = dict(required=required, read_only=read_only)
# allow_null isn't allowed for BooleanField
if field_class not in (BooleanField,):
kwargs['allow_null'] = allow_null
# allow_null CharField are also allowed to be blank
if field_class == CharField:
kwargs['allow_blank'] = allow_null
thrift_model_class = mapping[field[1]]
if enable_date_time_conversion and thrift_model_class == IntegerField and field[2].lower().endswith("time"):
thrift_model_class = UTCPosixTimestampDateTimeField
return thrift_model_class(**kwargs)
elif field[1] == TType.LIST:
# handling scenario when the thrift field type is list
list_field_serializer = process_list_field(field)
return ListField(child=list_field_serializer,
required=required,
read_only=read_only,
allow_null=allow_null)
elif field[1] == TType.STRUCT:
# handling scenario when the thrift field type is struct
return create_serializer(field[3][0],
required=required,
read_only=read_only,
allow_null=allow_null)
def process_list_field(field):
"""
Used to process thrift list type field
:param field:
:return:
"""
list_details = field[3]
if list_details[0] in mapping:
# handling scenario when the data type hold by the list is in the
# mapping
return mapping[list_details[0]]()
elif list_details[0] == TType.STRUCT:
# handling scenario when the data type hold by the list is a struct
return create_serializer(list_details[1][0])