Source code for helpscout.base_model

# -*- 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__, ), )