import struct from typing import Optional, Type, Union import structlog from google.protobuf.pyext._message import Message from delphi_api.core.constants import UTF8 from delphi_api.errors import Codes from delphi_api.errors.exceptions import ProtoAttributeError from delphi_api.v3.constants import BIGTABLE_INT_BYTE_FORMAT, BIGTABLE_LONG_BYTE_FORMAT LOG = structlog.get_logger(__name__) class BigTableAttribute: """An abstract base class representing an attribute of a BigTable model""" def __init__(self, column_family: str, attr_name: str, deserialize_class: Type[Message] = None): """ Args: column_family: BigTable column family ID (name) attr_name: BigTable column ID (name) deserialize_class: (optional) Specific class used for :class:`ProtoAttribute` attrs """ self.column_family = column_family self.attr_name = attr_name self.deserialize_class = deserialize_class @property def attr_name(self) -> str: return self._attr_name @attr_name.setter def attr_name(self, value): self._attr_name = value def __get__(self, instance, owner): if not instance.attribute_values.get(self.attr_name): return None return self.deserialize(instance.attribute_values[self.attr_name]) def __set__(self, instance, value): instance.attribute_values[self.attr_name] = self.serialize(value) def serialize(self, value) -> bytes: """This method should be overridden in subclasses and return a BigTable compatible value. """ return value def deserialize(self, value): """This method should be overridden in subclasses and return a deserialized representation of the value as a python object. """ return value class TextAttribute(BigTableAttribute): def serialize(self, value) -> bytes: """ Returns: bytes: a unicode str as bytes """ if not value: return b'' elif isinstance(value, str): return bytes(value, UTF8) else: return value def deserialize(self, value) -> str: """ Returns: str: bytes cast to str """ if isinstance(value, bytes): return value.decode(UTF8) return value class ProtoAttribute(BigTableAttribute): """This class represents a model attribute that is stored in a protobuf binary string format""" def serialize(self, value: Union[Message, bytes]) -> bytes: """ Returns: bytes: a protobuf encoded binary string """ if hasattr(value, 'SerializeToString'): return value.SerializeToString() return value def deserialize(self, value) -> Type[Message]: """ Returns: :class:`Message`: a decoded binary string as a protobuf object """ if not value: return value try: return self.deserialize_class.FromString(value) except AttributeError as e: # pragma: no cover raise ProtoAttributeError({ 'code': Codes.deserialization_error.value, 'description': 'Failed to deserialize BigTable binary protobuf attribute', 'detail': str(e), }) class IntegerAttribute(BigTableAttribute): def serialize(self, value) -> bytes: """ Returns: bytes: an integer as bytes """ if isinstance(value, str): value = int(value) if isinstance(value, int): return struct.pack(BIGTABLE_INT_BYTE_FORMAT, value) else: return value def deserialize(self, value) -> Optional[int]: """ Returns: int: bytes unpacked to int (or ``value`` if ``value`` is not bytes) """ if not value: return None if isinstance(value, bytes): try: return struct.unpack(BIGTABLE_INT_BYTE_FORMAT, value)[0] except Exception as e: LOG.exception( 'Integer Encoding %s' % str(e), error_details={'deserialize_value': value}) # set field to None instead of erroring endpoint return None return None class LongIntAttribute(BigTableAttribute): def serialize(self, value) -> bytes: """ Returns: bytes: a long integer as bytes """ if isinstance(value, str): value = int(value) if isinstance(value, int): return struct.pack(BIGTABLE_LONG_BYTE_FORMAT, value) else: return value def deserialize(self, value) -> Optional[int]: """ Returns: int: bytes unpacked to int (or ``value`` if ``value`` is not bytes) """ if not value: return None if isinstance(value, bytes): try: return struct.unpack(BIGTABLE_LONG_BYTE_FORMAT, value)[0] except Exception as e: LOG.exception( 'Long Integer Encoding %s' % str(e), error_details={'deserialize_value': value}) # set field to None instead of erroring endpoint return None return None