# -*- coding: utf-8 -*-
# Copyright 2017-TODAY LasLabs Inc.
# License MIT (https://opensource.org/licenses/MIT).
import logging
import properties
import re
from .exceptions import HelpScoutValidationException
logger = logging.getLogger(__name__)
# Identify lowerCamelCase strings.
# https://stackoverflow.com/a/1176023/861399
REGEX_CAMEL_FIRST = re.compile(r'(.)([A-Z][a-z]+)')
REGEX_CAMEL_SECOND = re.compile(r'([a-z0-9])([A-Z])')
[docs]class BaseModel(properties.HasProperties):
"""This is the model that all other models inherit from.
It provides some convenience functions, and the standard ``id`` property.
"""
id = properties.Integer(
'Unique identifier',
)
[docs] @classmethod
def from_api(cls, **kwargs):
"""Create a new instance from API arguments.
This will switch camelCase keys into snake_case for instantiation.
It will also identify any ``Instance`` or ``List`` properties, and
instantiate the proper objects using the values. The end result being
a fully Objectified and Pythonified API response.
Returns:
BaseModel: Instantiated model using the API values.
"""
vals = cls.get_non_empty_vals({
cls._to_snake_case(k): v for k, v in kwargs.items()
})
remove = []
for attr, val in vals.items():
try:
vals[attr] = cls._parse_property(attr, val)
except HelpScoutValidationException:
remove.append(attr)
logger.info(
'Unexpected property received in API response',
exc_info=True,
)
for attr in remove:
del vals[attr]
return cls(**cls.get_non_empty_vals(vals))
[docs] def get(self, key, default=None):
"""Return the field indicated by the key, if present."""
try:
return self.__getitem__(key)
except KeyError:
return default
[docs] def to_api(self):
"""Return a dictionary to send to the API.
Returns:
dict: Mapping representing this object that can be sent to the
API.
"""
vals = {}
for attribute, attribute_type in self._props.items():
prop = getattr(self, attribute)
vals[self._to_camel_case(attribute)] = self._to_api_value(
attribute_type, prop,
)
return vals
def _to_api_value(self, attribute_type, value):
"""Return a parsed value for the API."""
if not value:
return None
if isinstance(attribute_type, properties.Instance):
return value.to_api()
if isinstance(attribute_type, properties.List):
return self._parse_api_value_list(value)
return attribute_type.serialize(value)
def _parse_api_value_list(self, values):
"""Return a list field compatible with the API."""
try:
return [v.to_api() for v in values]
# Not models
except AttributeError:
return list(values)
[docs] @staticmethod
def get_non_empty_vals(mapping):
"""Return the mapping without any ``None`` values."""
return {
k: v for k, v in mapping.items() if v is not None
}
@classmethod
def _parse_property(cls, name, value):
"""Parse a property received from the API into an internal object.
Args:
name (str): Name of the property on the object.
value (mixed): The unparsed API value.
Raises:
HelpScoutValidationException: In the event that the property name
is not found.
Returns:
mixed: A value compatible with the internal models.
"""
prop = cls._props.get(name)
return_value = value
if not prop:
logger.debug(
'"%s" with value "%s" is not a valid property for "%s".' % (
name, value, cls,
),
)
return_value = None
elif isinstance(prop, properties.Instance):
return_value = prop.instance_class.from_api(**value)
elif isinstance(prop, properties.List):
return_value = cls._parse_property_list(prop, value)
elif isinstance(prop, properties.Color):
return_value = cls._parse_property_color(value)
return return_value
@staticmethod
def _parse_property_color(value):
"""Parse a color property and return a valid value."""
if value == 'none':
return 'lightgrey'
return value
@staticmethod
def _parse_property_list(prop, value):
"""Parse a list property and return a list of the results."""
attributes = []
for v in value:
try:
attributes.append(
prop.prop.instance_class.from_api(**v),
)
except AttributeError:
attributes.append(v)
return attributes
@staticmethod
def _to_snake_case(string):
"""Return a snake cased version of the input string.
Args:
string (str): A camel cased string.
Returns:
str: A snake cased string.
"""
sub_string = r'\1_\2'
string = REGEX_CAMEL_FIRST.sub(sub_string, string)
return REGEX_CAMEL_SECOND.sub(sub_string, string).lower()
@staticmethod
def _to_camel_case(string):
"""Return a camel cased version of the input string.
Args:
string (str): A snake cased string.
Returns:
str: A camel cased string.
"""
components = string.split('_')
return '%s%s' % (
components[0],
''.join(c.title() for c in components[1:]),
)
def __getitem__(self, item):
"""Return the field indicated by the key, if present.
This is better than using ``getattr`` because it will not expose any
properties that are not meant to be fields for the object.
Raises:
KeyError: In the event that the field doesn't exist.
"""
self.__check_field(item)
return getattr(self, item)
def __setitem__(self, key, value):
"""Return the field indicated by the key, if present.
This is better than using ``getattr`` because it will not expose any
properties that are not meant to be fields for the object.
Raises:
KeyError: In the event that the field doesn't exist.
"""
self.__check_field(key)
return setattr(self, key, value)
def __check_field(self, key):
"""Raises a KeyError if the field doesn't exist."""
if not self._props.get(key):
raise KeyError(
'The field "%s" does not exist on "%s"' % (
key, self.__class__.__name__,
),
)