from __future__ import absolute_import, division, print_function import datetime import json from copy import deepcopy import six import telnyx from telnyx import api_requestor, util def _compute_diff(current, previous): if isinstance(current, dict): previous = previous or {} diff = current.copy() for key in set(previous.keys()) - set(diff.keys()): diff[key] = previous[key] return diff return current if current is not None else "" def _serialize_list(array, previous): array = array or [] previous = previous or [] params = {} for i, v in enumerate(array): previous_item = previous[i] if len(previous) > i else None if hasattr(v, "serialize"): params[str(i)] = v.serialize(previous_item) else: params[str(i)] = _compute_diff(v, previous_item) return params class TelnyxObject(dict): class ReprJSONEncoder(json.JSONEncoder): def default(self, obj): # pylint: disable=method-hidden if isinstance(obj, datetime.datetime): return api_requestor._encode_datetime(obj) return super(TelnyxObject.ReprJSONEncoder, self).default(obj) def __init__(self, id=None, api_key=None, last_response=None, **params): super(TelnyxObject, self).__init__() self._unsaved_values = set() self._transient_values = set() self._last_response = last_response self._retrieve_params = params self._previous = None object.__setattr__(self, "api_key", api_key) if id: self["id"] = id @property def last_response(self): return self._last_response def update(self, update_dict): for k in update_dict: self._unsaved_values.add(k) return super(TelnyxObject, self).update(update_dict) def __setattr__(self, k, v): if k[0] == "_" or k in self.__dict__: return super(TelnyxObject, self).__setattr__(k, v) self[k] = v return None def __getattr__(self, k): if k[0] == "_": raise AttributeError(k) try: return self[k] except KeyError as err: raise AttributeError(*err.args) def __delattr__(self, k): if k[0] == "_" or k in self.__dict__: return super(TelnyxObject, self).__delattr__(k) else: del self[k] def __setitem__(self, k, v): if v == "": raise ValueError( "You cannot set %s to an empty string. " "We interpret empty strings as None in requests." "You may set %s.%s = None to delete the property" % (k, str(self), k) ) # Allows for unpickling in Python 3.x if not hasattr(self, "_unsaved_values"): self._unsaved_values = set() self._unsaved_values.add(k) super(TelnyxObject, self).__setitem__(k, v) def __getitem__(self, k): try: return super(TelnyxObject, self).__getitem__(k) except KeyError as err: if k in self._transient_values: raise KeyError( "%r. HINT: The %r attribute was set in the past." "It was then wiped when refreshing the object with " "the result returned by Telnyx's API, probably as a " "result of a save(). The attributes currently " "available on this object are: %s" % (k, k, ", ".join(list(self.keys()))) ) else: raise err def __delitem__(self, k): super(TelnyxObject, self).__delitem__(k) # Allows for unpickling in Python 3.x if hasattr(self, "_unsaved_values"): self._unsaved_values.remove(k) # Custom unpickling method that uses `update` to update the dictionary # without calling __setitem__, which would fail if any value is an empty # string def __setstate__(self, state): self.update(state) # Custom pickling method to ensure the instance is pickled as a custom # class and not as a dict, otherwise __setstate__ would not be called when # unpickling. def __reduce__(self): reduce_value = ( type(self), # callable (self.get("id", None), self.api_key), # args dict(self), # state ) return reduce_value @classmethod def construct_from(cls, values, key, last_response=None): instance = cls(values.get("id"), api_key=key, last_response=last_response) instance.refresh_from(values, api_key=key, last_response=last_response) return instance def refresh_from(self, values, api_key=None, partial=False, last_response=None): self.api_key = api_key or getattr(values, "api_key", None) self._last_response = last_response or getattr(values, "_last_response", None) # Wipe old state before setting new. if partial: self._unsaved_values = self._unsaved_values - set(values) else: removed = set(self.keys()) - set(values) self._transient_values = self._transient_values | removed self._unsaved_values = set() self.clear() self._transient_values = self._transient_values - set(values) for k, v in six.iteritems(values): super(TelnyxObject, self).__setitem__( k, util.convert_to_telnyx_object(v, api_key) ) self._previous = values @classmethod def api_base(cls): return None def request(self, method, url, params=None, headers=None): if params is None: params = self._retrieve_params requestor = api_requestor.APIRequestor( key=self.api_key, api_base=self.api_base() ) response, api_key = requestor.request(method, url, params, headers) return util.convert_to_telnyx_object(response, api_key) def __repr__(self): ident_parts = [type(self).__name__] if isinstance(self.get("record_type"), six.string_types): ident_parts.append(self.get("record_type")) if isinstance(self.get("id"), six.string_types): ident_parts.append("id=%s" % (self.get("id"),)) unicode_repr = "<%s at %s> JSON: %s" % ( " ".join(ident_parts), hex(id(self)), str(self), ) if six.PY2: return unicode_repr.encode("utf-8") else: return unicode_repr def __str__(self): return json.dumps( self.to_dict_recursive(), sort_keys=True, indent=2, cls=self.ReprJSONEncoder ) def to_dict(self): return dict(self) def to_dict_recursive(self): d = dict(self) for k, v in six.iteritems(d): if isinstance(v, TelnyxObject): d[k] = v.to_dict_recursive() return d def serialize(self, previous): params = {} unsaved_keys = self._unsaved_values or set() previous = previous or self._previous or {} for k, v in six.iteritems(self): if k == "id" or (isinstance(k, str) and k.startswith("_")): continue elif isinstance(v, telnyx.api_resources.abstract.APIResource): continue elif hasattr(v, "serialize"): child = v.serialize(previous.get(k, None)) if child != {}: params[k] = child elif k in unsaved_keys: params[k] = _compute_diff(v, previous.get(k, None)) elif k == "additional_owners" and v is not None: params[k] = _serialize_list(v, previous.get(k, None)) params = util.rewrite_reserved_words(params) return params # This class overrides __setitem__ to throw exceptions on inputs that it # doesn't like. This can cause problems when we try to copy an object # wholesale because some data that's returned from the API may not be valid # if it was set to be set manually. Here we override the class' copy # arguments so that we can bypass these possible exceptions on __setitem__. def __copy__(self): copied = TelnyxObject(self.get("id"), self.api_key) copied._retrieve_params = self._retrieve_params for k, v in six.iteritems(self): # Call parent's __setitem__ to avoid checks that we've added in the # overridden version that can throw exceptions. super(TelnyxObject, copied).__setitem__(k, v) return copied # This class overrides __setitem__ to throw exceptions on inputs that it # doesn't like. This can cause problems when we try to copy an object # wholesale because some data that's returned from the API may not be valid # if it was set to be set manually. Here we override the class' copy # arguments so that we can bypass these possible exceptions on __setitem__. def __deepcopy__(self, memo): copied = self.__copy__() memo[id(self)] = copied for k, v in six.iteritems(self): # Call parent's __setitem__ to avoid checks that we've added in the # overridden version that can throw exceptions. super(TelnyxObject, copied).__setitem__(k, deepcopy(v, memo)) return copied