import logging from .utils import extract, perform_request, CommentClientRequestError log = logging.getLogger(__name__) class Model(object): accessible_fields = ['id'] updatable_fields = ['id'] initializable_fields = ['id'] base_url = None default_retrieve_params = {} metric_tag_fields = [] DEFAULT_ACTIONS_WITH_ID = ['get', 'put', 'delete'] DEFAULT_ACTIONS_WITHOUT_ID = ['get_all', 'post'] DEFAULT_ACTIONS = DEFAULT_ACTIONS_WITH_ID + DEFAULT_ACTIONS_WITHOUT_ID def __init__(self, *args, **kwargs): self.attributes = extract(kwargs, self.accessible_fields) self.retrieved = False def __getattr__(self, name): if name == 'id': return self.attributes.get('id', None) try: return self.attributes[name] except KeyError: if self.retrieved or self.id is None: raise AttributeError("Field {0} does not exist".format(name)) self.retrieve() return self.__getattr__(name) def __setattr__(self, name, value): if name == 'attributes' or name not in self.accessible_fields + self.updatable_fields: super(Model, self).__setattr__(name, value) else: self.attributes[name] = value def __getitem__(self, key): if key not in self.accessible_fields: raise KeyError("Field {0} does not exist".format(key)) return self.attributes.get(key) def __setitem__(self, key, value): if key not in self.accessible_fields + self.updatable_fields: raise KeyError("Field {0} does not exist".format(key)) self.attributes.__setitem__(key, value) def items(self, *args, **kwargs): return self.attributes.items(*args, **kwargs) def get(self, *args, **kwargs): return self.attributes.get(*args, **kwargs) def to_dict(self): self.retrieve() return self.attributes def retrieve(self, *args, **kwargs): if not self.retrieved: self._retrieve(*args, **kwargs) self.retrieved = True return self def _retrieve(self, *args, **kwargs): url = self.url(action='get', params=self.attributes) response = perform_request( 'get', url, self.default_retrieve_params, metric_tags=self._metric_tags, metric_action='model.retrieve' ) self._update_from_response(response) @property def _metric_tags(self): """ Returns a list of tags to be used when recording metrics about this model. Each field named in ``self.metric_tag_fields`` is used as a tag value, under the key ``<class>.<metric_field>``. The tag model_class is used to record the class name of the model. """ tags = [ u'{}.{}:{}'.format(self.__class__.__name__, attr, self[attr]) for attr in self.metric_tag_fields if attr in self.attributes ] tags.append(u'model_class:{}'.format(self.__class__.__name__)) return tags @classmethod def find(cls, id): return cls(id=id) def _update_from_response(self, response_data): for k, v in response_data.items(): if k in self.accessible_fields: self.__setattr__(k, v) else: log.warning( "Unexpected field {field_name} in model {model_name}".format( field_name=k, model_name=self.__class__.__name__ ) ) def updatable_attributes(self): return extract(self.attributes, self.updatable_fields) def initializable_attributes(self): return extract(self.attributes, self.initializable_fields) @classmethod def before_save(cls, instance): pass @classmethod def after_save(cls, instance): pass def save(self, params=None): """ Invokes Forum's POST/PUT service to create/update thread """ self.before_save(self) if self.id: # if we have id already, treat this as an update request_params = self.updatable_attributes() if params: request_params.update(params) url = self.url(action='put', params=self.attributes) response = perform_request( 'put', url, request_params, metric_tags=self._metric_tags, metric_action='model.update' ) else: # otherwise, treat this as an insert url = self.url(action='post', params=self.attributes) response = perform_request( 'post', url, self.initializable_attributes(), metric_tags=self._metric_tags, metric_action='model.insert' ) self.retrieved = True self._update_from_response(response) self.after_save(self) def delete(self): url = self.url(action='delete', params=self.attributes) response = perform_request('delete', url, metric_tags=self._metric_tags, metric_action='model.delete') self.retrieved = True self._update_from_response(response) @classmethod def url_with_id(cls, params={}): return cls.base_url + '/' + str(params['id']) @classmethod def url_without_id(cls, params={}): return cls.base_url @classmethod def url(cls, action, params={}): if cls.base_url is None: raise CommentClientRequestError("Must provide base_url when using default url function") if action not in cls.DEFAULT_ACTIONS: raise ValueError("Invalid action {0}. The supported action must be in {1}".format(action, str(cls.DEFAULT_ACTIONS))) elif action in cls.DEFAULT_ACTIONS_WITH_ID: try: return cls.url_with_id(params) except KeyError: raise CommentClientRequestError("Cannot perform action {0} without id".format(action)) else: # action must be in DEFAULT_ACTIONS_WITHOUT_ID now return cls.url_without_id()