This captures real-time metrics for all of the comment client actions, segregated by course_id, as well as other small-cardinality fields. The goal is to be able to detect changes in forum usage, with the goal of alerting on potential error conditions.
168 lines
5.6 KiB
Python
168 lines
5.6 KiB
Python
from .utils import extract, perform_request, CommentClientRequestError
|
|
|
|
|
|
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:
|
|
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:
|
|
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_attributes(**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_attributes(self, *args, **kwargs):
|
|
for k, v in kwargs.items():
|
|
if k in self.accessible_fields:
|
|
self.__setattr__(k, v)
|
|
else:
|
|
raise AttributeError("Field {0} does not exist".format(k))
|
|
|
|
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):
|
|
self.before_save(self)
|
|
if self.id: # if we have id already, treat this as an update
|
|
url = self.url(action='put', params=self.attributes)
|
|
response = perform_request(
|
|
'put',
|
|
url,
|
|
self.updatable_attributes(),
|
|
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_attributes(**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_attributes(**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()
|