diff --git a/.gitignore b/.gitignore index e87e067..acd0473 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ build dist MANIFEST _build +venv +.tox diff --git a/.travis.yml b/.travis.yml index dd0bb3f..f7a3b43 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,17 +1,21 @@ language: python python: - - "2.6" - - "2.7" + - "3.8" + - "3.7" + - "3.6" +services: + - redis-server before_install: - export PIP_USE_MIRRORS=true - - sudo apt-get update - - sudo apt-get install redis-server +matrix: + fast_finish: true + include: + - python: "3.5" + env: DJANGO=2.2 install: - - pip install -e . - - pip install -r requirements/tests.txt Django==$DJANGO + - pip install tox tox-venv tox-travis script: - - django-admin.py test --settings=app_metrics.tests.settings app_metrics + - tox env: - - DJANGO=1.3.7 - - DJANGO=1.4.5 - - DJANGO=1.5 + - DJANGO=2.2 + - DJANGO=3.0 diff --git a/README.rst b/README.rst index 61ce13f..0017f2b 100644 --- a/README.rst +++ b/README.rst @@ -40,7 +40,15 @@ settings and it will behave as if Celery was not configured. .. _Celery: http://celeryproject.org/ .. _`django-celery`: http://ask.github.com/django-celery/ -Django 1.2 and above +Django 2.2 and above +Python 3.5 and above + +Support for older Django and Python +----------------------------------- + +Django App Metrics v0.8.0 supports older versions of Django and Python if you still need it. + +``$ pip install django-app-metrics==0.8.0`` Usage ===== diff --git a/app_metrics/management/commands/metrics_aggregate.py b/app_metrics/management/commands/metrics_aggregate.py index 7c51e2c..d98c165 100644 --- a/app_metrics/management/commands/metrics_aggregate.py +++ b/app_metrics/management/commands/metrics_aggregate.py @@ -1,23 +1,23 @@ import datetime -from django.core.management.base import NoArgsCommand +from django.core.management import BaseCommand from app_metrics.models import Metric, MetricItem, MetricDay, MetricWeek, MetricMonth, MetricYear from app_metrics.utils import week_for_date, month_for_date, year_for_date, get_backend -class Command(NoArgsCommand): +class Command(BaseCommand): help = "Aggregate Application Metrics" requires_model_validation = True - def handle_noargs(self, **options): + def handle(self, **options): """ Aggregate Application Metrics """ backend = get_backend() # If using Mixpanel this command is a NOOP if backend == 'app_metrics.backends.mixpanel': - print "Useless use of metrics_aggregate when using Mixpanel backend" + print("Useless use of metrics_aggregate when using Mixpanel backend") return # Aggregate Items diff --git a/app_metrics/management/commands/metrics_send_mail.py b/app_metrics/management/commands/metrics_send_mail.py index 9eae6c6..f5deae9 100644 --- a/app_metrics/management/commands/metrics_send_mail.py +++ b/app_metrics/management/commands/metrics_send_mail.py @@ -1,7 +1,7 @@ import datetime import string -from django.core.management.base import NoArgsCommand +from django.core.management import BaseCommand from django.conf import settings from django.db.models import Q from django.utils import translation @@ -11,12 +11,12 @@ from app_metrics.models import MetricSet, Metric from app_metrics.utils import get_backend -class Command(NoArgsCommand): +class Command(BaseCommand): help = "Send Report E-mails" requires_model_validation = True can_import_settings = True - def handle_noargs(self, **options): + def handle(self, **options): """ Send Report E-mails """ from django.conf import settings @@ -26,7 +26,7 @@ def handle_noargs(self, **options): # This command is a NOOP if using the Mixpanel backend if backend == 'app_metrics.backends.mixpanel': - print "Useless use of metrics_send_email when using Mixpanel backend." + print("Useless use of metrics_send_email when using Mixpanel backend.") return # Determine if we should also send any weekly or monthly reports diff --git a/app_metrics/management/commands/move_to_mixpanel.py b/app_metrics/management/commands/move_to_mixpanel.py index 524e4df..74483d4 100644 --- a/app_metrics/management/commands/move_to_mixpanel.py +++ b/app_metrics/management/commands/move_to_mixpanel.py @@ -1,29 +1,30 @@ -from django.core.management.base import NoArgsCommand +from django.core.management import BaseCommand from app_metrics.models import MetricItem from app_metrics.backends.mixpanel import metric -from app_metrics.utils import get_backend +from app_metrics.utils import get_backend, get_timestamp -class Command(NoArgsCommand): + +class Command(BaseCommand): help = "Move MetricItems from the db backend to MixPanel" requires_model_validation = True - def handle_noargs(self, **options): + def handle(self, **options): """ Move MetricItems from the db backend to MixPanel" """ backend = get_backend() # If not using Mixpanel this command is a NOOP if backend != 'app_metrics.backends.mixpanel': - print "You need to set the backend to MixPanel" + print("You need to set the backend to MixPanel") return items = MetricItem.objects.all() for i in items: properties = { - 'time': i.created.strftime('%s'), + 'time': int(get_timestamp(i.created)), } metric(i.metric.slug, num=i.num, properties=properties) diff --git a/app_metrics/management/commands/move_to_statsd.py b/app_metrics/management/commands/move_to_statsd.py index 54acb81..d2c7fbb 100644 --- a/app_metrics/management/commands/move_to_statsd.py +++ b/app_metrics/management/commands/move_to_statsd.py @@ -1,14 +1,14 @@ import sys -from django.core.management.base import NoArgsCommand +from django.core.management import BaseCommand from app_metrics.models import MetricItem from app_metrics.backends.statsd_backend import metric -class Command(NoArgsCommand): +class Command(BaseCommand): help = "Move MetricItems from the db backend to statsd" requires_model_validation = True - def handle_noargs(self, **options): + def handle(self, **options): """Move MetricItems from the db backend to statsd""" backend = get_backend() diff --git a/app_metrics/migrations/0001_initial.py b/app_metrics/migrations/0001_initial.py index e3ff022..881d99c 100644 --- a/app_metrics/migrations/0001_initial.py +++ b/app_metrics/migrations/0001_initial.py @@ -1,212 +1,128 @@ -# -*- coding: utf-8 -*- -import datetime -from south.db import db -from south.v2 import SchemaMigration -from django.db import models - -from app_metrics.compat import AUTH_USER_MODEL - - -class Migration(SchemaMigration): - - def forwards(self, orm): - # Adding model 'Metric' - db.create_table('app_metrics_metric', ( - ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('name', self.gf('django.db.models.fields.CharField')(max_length=50)), - ('slug', self.gf('django.db.models.fields.SlugField')(unique=True, max_length=60)), - )) - db.send_create_signal('app_metrics', ['Metric']) - - # Adding model 'MetricSet' - db.create_table('app_metrics_metricset', ( - ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('name', self.gf('django.db.models.fields.CharField')(max_length=50)), - ('no_email', self.gf('django.db.models.fields.BooleanField')(default=False)), - ('send_daily', self.gf('django.db.models.fields.BooleanField')(default=True)), - ('send_weekly', self.gf('django.db.models.fields.BooleanField')(default=False)), - ('send_monthly', self.gf('django.db.models.fields.BooleanField')(default=False)), - )) - db.send_create_signal('app_metrics', ['MetricSet']) - - # Adding M2M table for field metrics on 'MetricSet' - db.create_table('app_metrics_metricset_metrics', ( - ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), - ('metricset', models.ForeignKey(orm['app_metrics.metricset'], null=False)), - ('metric', models.ForeignKey(orm['app_metrics.metric'], null=False)) - )) - db.create_unique('app_metrics_metricset_metrics', ['metricset_id', 'metric_id']) - - # Adding M2M table for field email_recipients on 'MetricSet' - db.create_table('app_metrics_metricset_email_recipients', ( - ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), - ('metricset', models.ForeignKey(orm['app_metrics.metricset'], null=False)), - ('user', models.ForeignKey(orm[AUTH_USER_MODEL], null=False)) - )) - db.create_unique('app_metrics_metricset_email_recipients', ['metricset_id', 'user_id']) - - # Adding model 'MetricItem' - db.create_table('app_metrics_metricitem', ( - ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('metric', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['app_metrics.Metric'])), - ('num', self.gf('django.db.models.fields.IntegerField')(default=1)), - ('created', self.gf('django.db.models.fields.DateField')(default=datetime.date.today)), - )) - db.send_create_signal('app_metrics', ['MetricItem']) - - # Adding model 'MetricDay' - db.create_table('app_metrics_metricday', ( - ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('metric', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['app_metrics.Metric'])), - ('num', self.gf('django.db.models.fields.BigIntegerField')(default=0)), - ('created', self.gf('django.db.models.fields.DateField')(default=datetime.date.today)), - )) - db.send_create_signal('app_metrics', ['MetricDay']) - - # Adding model 'MetricWeek' - db.create_table('app_metrics_metricweek', ( - ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('metric', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['app_metrics.Metric'])), - ('num', self.gf('django.db.models.fields.BigIntegerField')(default=0)), - ('created', self.gf('django.db.models.fields.DateField')(default=datetime.date.today)), - )) - db.send_create_signal('app_metrics', ['MetricWeek']) - - # Adding model 'MetricMonth' - db.create_table('app_metrics_metricmonth', ( - ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('metric', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['app_metrics.Metric'])), - ('num', self.gf('django.db.models.fields.BigIntegerField')(default=0)), - ('created', self.gf('django.db.models.fields.DateField')(default=datetime.date.today)), - )) - db.send_create_signal('app_metrics', ['MetricMonth']) +# Generated by Django 2.2.13 on 2020-06-26 21:03 - # Adding model 'MetricYear' - db.create_table('app_metrics_metricyear', ( - ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('metric', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['app_metrics.Metric'])), - ('num', self.gf('django.db.models.fields.BigIntegerField')(default=0)), - ('created', self.gf('django.db.models.fields.DateField')(default=datetime.date.today)), - )) - db.send_create_signal('app_metrics', ['MetricYear']) - - def backwards(self, orm): - # Deleting model 'Metric' - db.delete_table('app_metrics_metric') - - # Deleting model 'MetricSet' - db.delete_table('app_metrics_metricset') - - # Removing M2M table for field metrics on 'MetricSet' - db.delete_table('app_metrics_metricset_metrics') - - # Removing M2M table for field email_recipients on 'MetricSet' - db.delete_table('app_metrics_metricset_email_recipients') - - # Deleting model 'MetricItem' - db.delete_table('app_metrics_metricitem') - - # Deleting model 'MetricDay' - db.delete_table('app_metrics_metricday') - - # Deleting model 'MetricWeek' - db.delete_table('app_metrics_metricweek') - - # Deleting model 'MetricMonth' - db.delete_table('app_metrics_metricmonth') - - # Deleting model 'MetricYear' - db.delete_table('app_metrics_metricyear') - - models = { - 'app_metrics.metric': { - 'Meta': {'object_name': 'Metric'}, - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}), - 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '60'}) - }, - 'app_metrics.metricday': { - 'Meta': {'object_name': 'MetricDay'}, - 'created': ('django.db.models.fields.DateField', [], {'default': 'datetime.date.today'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'metric': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['app_metrics.Metric']"}), - 'num': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}) - }, - 'app_metrics.metricitem': { - 'Meta': {'object_name': 'MetricItem'}, - 'created': ('django.db.models.fields.DateField', [], {'default': 'datetime.datetime.now'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'metric': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['app_metrics.Metric']"}), - 'num': ('django.db.models.fields.IntegerField', [], {'default': '1'}) - }, - 'app_metrics.metricmonth': { - 'Meta': {'object_name': 'MetricMonth'}, - 'created': ('django.db.models.fields.DateField', [], {'default': 'datetime.date.today'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'metric': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['app_metrics.Metric']"}), - 'num': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}) - }, - 'app_metrics.metricset': { - 'Meta': {'object_name': 'MetricSet'}, - 'email_recipients': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm[AUTH_USER_MODEL]", 'symmetrical': 'False'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'metrics': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['app_metrics.Metric']", 'symmetrical': 'False'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}), - 'no_email': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'send_daily': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), - 'send_monthly': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'send_weekly': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) - }, - 'app_metrics.metricweek': { - 'Meta': {'object_name': 'MetricWeek'}, - 'created': ('django.db.models.fields.DateField', [], {'default': 'datetime.date.today'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'metric': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['app_metrics.Metric']"}), - 'num': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}) - }, - 'app_metrics.metricyear': { - 'Meta': {'object_name': 'MetricYear'}, - 'created': ('django.db.models.fields.DateField', [], {'default': 'datetime.date.today'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'metric': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['app_metrics.Metric']"}), - 'num': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}) - }, - 'auth.group': { - 'Meta': {'object_name': 'Group'}, - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), - 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) - }, - 'auth.permission': { - 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, - 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) - }, - AUTH_USER_MODEL: { - 'Meta': {'object_name': AUTH_USER_MODEL.split('.')[-1]}, - 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), - 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), - 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), - 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), - 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), - 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), - 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) - }, - 'contenttypes.contenttype': { - 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, - 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) - } - } - - complete_apps = ['app_metrics'] +import datetime +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Gauge', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=50, verbose_name='name')), + ('slug', models.SlugField(max_length=60, unique=True, verbose_name='slug')), + ('current_value', models.DecimalField(decimal_places=6, default='0.00', max_digits=15, verbose_name='current value')), + ('created', models.DateTimeField(default=django.utils.timezone.now, verbose_name='created')), + ('updated', models.DateTimeField(default=django.utils.timezone.now, verbose_name='updated')), + ], + options={ + 'verbose_name': 'gauge', + 'verbose_name_plural': 'gauges', + }, + ), + migrations.CreateModel( + name='Metric', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=50, verbose_name='name')), + ('slug', models.SlugField(max_length=60, unique=True, verbose_name='slug')), + ], + options={ + 'verbose_name': 'metric', + 'verbose_name_plural': 'metrics', + }, + ), + migrations.CreateModel( + name='MetricYear', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('num', models.BigIntegerField(default=0, verbose_name='number')), + ('created', models.DateField(default=datetime.date.today, verbose_name='created')), + ('metric', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='app_metrics.Metric', verbose_name='metric')), + ], + options={ + 'verbose_name': 'year metric', + 'verbose_name_plural': 'year metrics', + }, + ), + migrations.CreateModel( + name='MetricWeek', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('num', models.BigIntegerField(default=0, verbose_name='number')), + ('created', models.DateField(default=datetime.date.today, verbose_name='created')), + ('metric', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='app_metrics.Metric', verbose_name='metric')), + ], + options={ + 'verbose_name': 'week metric', + 'verbose_name_plural': 'week metrics', + }, + ), + migrations.CreateModel( + name='MetricSet', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=50, verbose_name='name')), + ('no_email', models.BooleanField(default=False, verbose_name='no e-mail')), + ('send_daily', models.BooleanField(default=True, verbose_name='send daily')), + ('send_weekly', models.BooleanField(default=False, verbose_name='send weekly')), + ('send_monthly', models.BooleanField(default=False, verbose_name='send monthly')), + ('email_recipients', models.ManyToManyField(to=settings.AUTH_USER_MODEL, verbose_name='email recipients')), + ('metrics', models.ManyToManyField(to='app_metrics.Metric', verbose_name='metrics')), + ], + options={ + 'verbose_name': 'metric set', + 'verbose_name_plural': 'metric sets', + }, + ), + migrations.CreateModel( + name='MetricMonth', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('num', models.BigIntegerField(default=0, verbose_name='number')), + ('created', models.DateField(default=datetime.date.today, verbose_name='created')), + ('metric', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='app_metrics.Metric', verbose_name='metric')), + ], + options={ + 'verbose_name': 'month metric', + 'verbose_name_plural': 'month metrics', + }, + ), + migrations.CreateModel( + name='MetricItem', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('num', models.IntegerField(default=1, verbose_name='number')), + ('created', models.DateTimeField(default=django.utils.timezone.now, verbose_name='created')), + ('metric', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='app_metrics.Metric', verbose_name='metric')), + ], + options={ + 'verbose_name': 'metric item', + 'verbose_name_plural': 'metric items', + }, + ), + migrations.CreateModel( + name='MetricDay', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('num', models.BigIntegerField(default=0, verbose_name='number')), + ('created', models.DateField(default=datetime.date.today, verbose_name='created')), + ('metric', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='app_metrics.Metric', verbose_name='metric')), + ], + options={ + 'verbose_name': 'day metric', + 'verbose_name_plural': 'day metrics', + }, + ), + ] diff --git a/app_metrics/models.py b/app_metrics/models.py index 3dbde82..98928ca 100644 --- a/app_metrics/models.py +++ b/app_metrics/models.py @@ -1,17 +1,13 @@ import datetime +from django.conf import settings from django.db import models, IntegrityError +from django.db.transaction import atomic from django.template.defaultfilters import slugify from django.utils.translation import ugettext_lazy as _ +from django.utils import timezone -from app_metrics.compat import User - -try: - from django.db.transaction import atomic -except ImportError: - # Django < 1.6 use noop context manager - from contextlib import contextmanager - atomic = contextmanager(lambda:(yield)) +USER_MODEL = getattr(settings, 'AUTH_USER_MODEL', 'auth.User') class Metric(models.Model): @@ -45,7 +41,7 @@ class MetricSet(models.Model): """ A set of metrics that should be sent via email to certain users """ name = models.CharField(_('name'), max_length=50) metrics = models.ManyToManyField(Metric, verbose_name=_('metrics')) - email_recipients = models.ManyToManyField(User, verbose_name=_('email recipients')) + email_recipients = models.ManyToManyField(USER_MODEL, verbose_name=_('email recipients')) no_email = models.BooleanField(_('no e-mail'), default=False) send_daily = models.BooleanField(_('send daily'), default=True) send_weekly = models.BooleanField(_('send weekly'), default=False) @@ -61,9 +57,9 @@ def __unicode__(self): class MetricItem(models.Model): """ Individual metric items """ - metric = models.ForeignKey(Metric, verbose_name=_('metric')) + metric = models.ForeignKey(Metric, verbose_name=_('metric'), on_delete=models.PROTECT) num = models.IntegerField(_('number'), default=1) - created = models.DateTimeField(_('created'), default=datetime.datetime.now) + created = models.DateTimeField(_('created'), default=timezone.now) class Meta: verbose_name = _('metric item') @@ -79,7 +75,7 @@ def __unicode__(self): class MetricDay(models.Model): """ Aggregation of Metrics on a per day basis """ - metric = models.ForeignKey(Metric, verbose_name=_('metric')) + metric = models.ForeignKey(Metric, verbose_name=_('metric'), on_delete=models.PROTECT) num = models.BigIntegerField(_('number'), default=0) created = models.DateField(_('created'), default=datetime.date.today) @@ -96,7 +92,7 @@ def __unicode__(self): class MetricWeek(models.Model): """ Aggregation of Metrics on a weekly basis """ - metric = models.ForeignKey(Metric, verbose_name=_('metric')) + metric = models.ForeignKey(Metric, verbose_name=_('metric'), on_delete=models.PROTECT) num = models.BigIntegerField(_('number'), default=0) created = models.DateField(_('created'), default=datetime.date.today) @@ -114,7 +110,7 @@ def __unicode__(self): class MetricMonth(models.Model): """ Aggregation of Metrics on monthly basis """ - metric = models.ForeignKey(Metric, verbose_name=('metric')) + metric = models.ForeignKey(Metric, verbose_name=('metric'), on_delete=models.PROTECT) num = models.BigIntegerField(_('number'), default=0) created = models.DateField(_('created'), default=datetime.date.today) @@ -132,7 +128,7 @@ def __unicode__(self): class MetricYear(models.Model): """ Aggregation of Metrics on a yearly basis """ - metric = models.ForeignKey(Metric, verbose_name=_('metric')) + metric = models.ForeignKey(Metric, verbose_name=_('metric'), on_delete=models.PROTECT) num = models.BigIntegerField(_('number'), default=0) created = models.DateField(_('created'), default=datetime.date.today) @@ -154,8 +150,8 @@ class Gauge(models.Model): name = models.CharField(_('name'), max_length=50) slug = models.SlugField(_('slug'), unique=True, max_length=60) current_value = models.DecimalField(_('current value'), max_digits=15, decimal_places=6, default='0.00') - created = models.DateTimeField(_('created'), default=datetime.datetime.now) - updated = models.DateTimeField(_('updated'), default=datetime.datetime.now) + created = models.DateTimeField(_('created'), default=timezone.now) + updated = models.DateTimeField(_('updated'), default=timezone.now) class Meta: verbose_name = _('gauge') @@ -168,5 +164,5 @@ def save(self, *args, **kwargs): if not self.id and not self.slug: self.slug = slugify(self.name) - self.updated = datetime.datetime.now() + self.updated = timezone.datetime.now() return super(Gauge, self).save(*args, **kwargs) diff --git a/app_metrics/south_migrations/0001_initial.py b/app_metrics/south_migrations/0001_initial.py new file mode 100644 index 0000000..e3ff022 --- /dev/null +++ b/app_metrics/south_migrations/0001_initial.py @@ -0,0 +1,212 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +from app_metrics.compat import AUTH_USER_MODEL + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'Metric' + db.create_table('app_metrics_metric', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=50)), + ('slug', self.gf('django.db.models.fields.SlugField')(unique=True, max_length=60)), + )) + db.send_create_signal('app_metrics', ['Metric']) + + # Adding model 'MetricSet' + db.create_table('app_metrics_metricset', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=50)), + ('no_email', self.gf('django.db.models.fields.BooleanField')(default=False)), + ('send_daily', self.gf('django.db.models.fields.BooleanField')(default=True)), + ('send_weekly', self.gf('django.db.models.fields.BooleanField')(default=False)), + ('send_monthly', self.gf('django.db.models.fields.BooleanField')(default=False)), + )) + db.send_create_signal('app_metrics', ['MetricSet']) + + # Adding M2M table for field metrics on 'MetricSet' + db.create_table('app_metrics_metricset_metrics', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('metricset', models.ForeignKey(orm['app_metrics.metricset'], null=False)), + ('metric', models.ForeignKey(orm['app_metrics.metric'], null=False)) + )) + db.create_unique('app_metrics_metricset_metrics', ['metricset_id', 'metric_id']) + + # Adding M2M table for field email_recipients on 'MetricSet' + db.create_table('app_metrics_metricset_email_recipients', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('metricset', models.ForeignKey(orm['app_metrics.metricset'], null=False)), + ('user', models.ForeignKey(orm[AUTH_USER_MODEL], null=False)) + )) + db.create_unique('app_metrics_metricset_email_recipients', ['metricset_id', 'user_id']) + + # Adding model 'MetricItem' + db.create_table('app_metrics_metricitem', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('metric', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['app_metrics.Metric'])), + ('num', self.gf('django.db.models.fields.IntegerField')(default=1)), + ('created', self.gf('django.db.models.fields.DateField')(default=datetime.date.today)), + )) + db.send_create_signal('app_metrics', ['MetricItem']) + + # Adding model 'MetricDay' + db.create_table('app_metrics_metricday', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('metric', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['app_metrics.Metric'])), + ('num', self.gf('django.db.models.fields.BigIntegerField')(default=0)), + ('created', self.gf('django.db.models.fields.DateField')(default=datetime.date.today)), + )) + db.send_create_signal('app_metrics', ['MetricDay']) + + # Adding model 'MetricWeek' + db.create_table('app_metrics_metricweek', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('metric', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['app_metrics.Metric'])), + ('num', self.gf('django.db.models.fields.BigIntegerField')(default=0)), + ('created', self.gf('django.db.models.fields.DateField')(default=datetime.date.today)), + )) + db.send_create_signal('app_metrics', ['MetricWeek']) + + # Adding model 'MetricMonth' + db.create_table('app_metrics_metricmonth', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('metric', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['app_metrics.Metric'])), + ('num', self.gf('django.db.models.fields.BigIntegerField')(default=0)), + ('created', self.gf('django.db.models.fields.DateField')(default=datetime.date.today)), + )) + db.send_create_signal('app_metrics', ['MetricMonth']) + + # Adding model 'MetricYear' + db.create_table('app_metrics_metricyear', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('metric', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['app_metrics.Metric'])), + ('num', self.gf('django.db.models.fields.BigIntegerField')(default=0)), + ('created', self.gf('django.db.models.fields.DateField')(default=datetime.date.today)), + )) + db.send_create_signal('app_metrics', ['MetricYear']) + + def backwards(self, orm): + # Deleting model 'Metric' + db.delete_table('app_metrics_metric') + + # Deleting model 'MetricSet' + db.delete_table('app_metrics_metricset') + + # Removing M2M table for field metrics on 'MetricSet' + db.delete_table('app_metrics_metricset_metrics') + + # Removing M2M table for field email_recipients on 'MetricSet' + db.delete_table('app_metrics_metricset_email_recipients') + + # Deleting model 'MetricItem' + db.delete_table('app_metrics_metricitem') + + # Deleting model 'MetricDay' + db.delete_table('app_metrics_metricday') + + # Deleting model 'MetricWeek' + db.delete_table('app_metrics_metricweek') + + # Deleting model 'MetricMonth' + db.delete_table('app_metrics_metricmonth') + + # Deleting model 'MetricYear' + db.delete_table('app_metrics_metricyear') + + models = { + 'app_metrics.metric': { + 'Meta': {'object_name': 'Metric'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '60'}) + }, + 'app_metrics.metricday': { + 'Meta': {'object_name': 'MetricDay'}, + 'created': ('django.db.models.fields.DateField', [], {'default': 'datetime.date.today'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'metric': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['app_metrics.Metric']"}), + 'num': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}) + }, + 'app_metrics.metricitem': { + 'Meta': {'object_name': 'MetricItem'}, + 'created': ('django.db.models.fields.DateField', [], {'default': 'datetime.datetime.now'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'metric': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['app_metrics.Metric']"}), + 'num': ('django.db.models.fields.IntegerField', [], {'default': '1'}) + }, + 'app_metrics.metricmonth': { + 'Meta': {'object_name': 'MetricMonth'}, + 'created': ('django.db.models.fields.DateField', [], {'default': 'datetime.date.today'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'metric': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['app_metrics.Metric']"}), + 'num': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}) + }, + 'app_metrics.metricset': { + 'Meta': {'object_name': 'MetricSet'}, + 'email_recipients': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm[AUTH_USER_MODEL]", 'symmetrical': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'metrics': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['app_metrics.Metric']", 'symmetrical': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'no_email': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'send_daily': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'send_monthly': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'send_weekly': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) + }, + 'app_metrics.metricweek': { + 'Meta': {'object_name': 'MetricWeek'}, + 'created': ('django.db.models.fields.DateField', [], {'default': 'datetime.date.today'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'metric': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['app_metrics.Metric']"}), + 'num': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}) + }, + 'app_metrics.metricyear': { + 'Meta': {'object_name': 'MetricYear'}, + 'created': ('django.db.models.fields.DateField', [], {'default': 'datetime.date.today'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'metric': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['app_metrics.Metric']"}), + 'num': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}) + }, + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + AUTH_USER_MODEL: { + 'Meta': {'object_name': AUTH_USER_MODEL.split('.')[-1]}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + } + } + + complete_apps = ['app_metrics'] diff --git a/app_metrics/migrations/0002_alter_created_to_datetime.py b/app_metrics/south_migrations/0002_alter_created_to_datetime.py similarity index 100% rename from app_metrics/migrations/0002_alter_created_to_datetime.py rename to app_metrics/south_migrations/0002_alter_created_to_datetime.py diff --git a/app_metrics/migrations/0003_auto__add_gauge.py b/app_metrics/south_migrations/0003_auto__add_gauge.py similarity index 100% rename from app_metrics/migrations/0003_auto__add_gauge.py rename to app_metrics/south_migrations/0003_auto__add_gauge.py diff --git a/app_metrics/tests/librato_tests.py b/app_metrics/south_migrations/__init__.py similarity index 100% rename from app_metrics/tests/librato_tests.py rename to app_metrics/south_migrations/__init__.py diff --git a/app_metrics/tasks.py b/app_metrics/tasks.py index 7f5399d..2388638 100644 --- a/app_metrics/tasks.py +++ b/app_metrics/tasks.py @@ -1,8 +1,9 @@ import base64 import json -import urllib -import urllib2 +import urllib.request, urllib.parse, urllib.error +import urllib.request, urllib.error, urllib.parse import datetime +from decimal import Decimal try: from celery.task import task @@ -82,56 +83,58 @@ def mixpanel_metric_task(slug, num, properties=None, **kwargs): url = getattr(settings, 'APP_METRICS_MIXPANEL_API_URL', "http://api.mixpanel.com/track/") params = {"event": slug, "properties": properties} - b64_data = base64.b64encode(json.dumps(params)) + b64_data = base64.b64encode(json.dumps(params).encode('utf8')) - data = urllib.urlencode({"data": b64_data}) - req = urllib2.Request(url, data) + data = urllib.parse.urlencode({"data": b64_data}).encode('utf8') + req = urllib.request.Request(url, data) for i in range(num): - response = urllib2.urlopen(req) + response = urllib.request.urlopen(req) if response.read() == '0': - raise MixPanelTrackError(u'MixPanel returned 0') + raise MixPanelTrackError('MixPanel returned 0') # Statsd tasks -def get_statsd_conn(): +def get_statsd_client(): if statsd is None: raise ImproperlyConfigured("You must install 'python-statsd' in order to use this backend.") - conn = statsd.Connection( + client = statsd.StatsClient( host=getattr(settings, 'APP_METRICS_STATSD_HOST', 'localhost'), port=int(getattr(settings, 'APP_METRICS_STATSD_PORT', 8125)), - sample_rate=float(getattr(settings, 'APP_METRICS_STATSD_SAMPLE_RATE', 1)), ) - return conn + return client @task def statsd_metric_task(slug, num=1, **kwargs): - conn = get_statsd_conn() - counter = statsd.Counter(slug, connection=conn) - counter += num + client = get_statsd_client() + client.incr(slug, count=num, rate=float(getattr(settings, 'APP_METRICS_STATSD_SAMPLE_RATE', 1))) @task def statsd_timing_task(slug, seconds_taken=1.0, **kwargs): - conn = get_statsd_conn() + client = get_statsd_client() # You might be wondering "Why not use ``timer.start/.stop`` here?" # The problem is that this is a task, likely running out of process # & perhaps with network overhead. We'll measure the timing elsewhere, # in-process, to be as accurate as possible, then use the out-of-process # task for talking to the statsd backend. - timer = statsd.Timer(slug, connection=conn) - timer.send('total', seconds_taken) + + # Must convert to milliseconds + # https://statsd.readthedocs.io/en/v3.3/timing.html#calling-timing-manually + client.timing(slug, int(seconds_taken * 1000), rate=float(getattr(settings, 'APP_METRICS_STATSD_SAMPLE_RATE', 1))) @task def statsd_gauge_task(slug, current_value, **kwargs): - conn = get_statsd_conn() - gauge = statsd.Gauge(slug, connection=conn) - # We send nothing here, since we only have one name/slug to work with here. - gauge.send('', current_value) + client = get_statsd_client() + if isinstance(current_value, str): + current_value = Decimal(current_value) + + client.gauge(slug, current_value, rate=float(getattr(settings, 'APP_METRICS_STATSD_SAMPLE_RATE', 1))) + # Redis tasks diff --git a/app_metrics/tests/__init__.py b/app_metrics/tests/__init__.py index 6ac3338..62b3f4b 100644 --- a/app_metrics/tests/__init__.py +++ b/app_metrics/tests/__init__.py @@ -1,29 +1,5 @@ -from app_metrics.tests.base_tests import * -from app_metrics.tests.mixpanel_tests import * +from celery import current_app -try: - import statsd -except ImportError: - print "Skipping the statsd tests." - statsd = None -if statsd is not None: - from app_metrics.tests.statsd_tests import * - -try: - import redis -except ImportError: - print "Skipping redis tests." - redis = None - -if redis is not None: - from app_metrics.tests.redis_tests import * - -#try: -# import librato -#except ImportError: -# print "Skipping librato tests..." -# librato = None -# -#if librato is not None: -# from app_metrics.tests.librato_tests import * +current_app.conf.CELERY_ALWAYS_EAGER = True +current_app.conf.CELERY_EAGER_PROPAGATES_EXCEPTIONS = True diff --git a/app_metrics/tests/settings.py b/app_metrics/tests/settings.py index 52d33e8..fc06ae6 100644 --- a/app_metrics/tests/settings.py +++ b/app_metrics/tests/settings.py @@ -1,50 +1,49 @@ import os -import django - BASE_PATH = os.path.dirname(__file__) -if django.VERSION[:2] >= (1, 3): - DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': ':memory:', - } +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': ':memory:', } -else: - DATABASE_ENGINE = 'sqlite3' - DATABASE_NAME = ':memory:' +} + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ] + } + }, +] SITE_ID = 1 DEBUG = True -TEST_RUNNER = 'django_coverage.coverage_runner.CoverageRunner' - -COVERAGE_MODULE_EXCLUDES = [ - 'tests$', 'settings$', 'urls$', - 'common.views.test', '__init__', 'django', - 'migrations', 'djcelery' +MIDDLEWARE = [ + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', ] -COVERAGE_REPORT_HTML_OUTPUT_DIR = os.path.join(BASE_PATH, 'coverage') - INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.sites', + 'django.contrib.messages', 'app_metrics', - 'app_metrics.tests', - 'djcelery', - 'django_coverage' ] ROOT_URLCONF = 'app_metrics.tests.urls' -CELERY_ALWAYS_EAGER = True - APP_METRICS_BACKEND = 'app_metrics.backends.db' APP_METRICS_MIXPANEL_TOKEN = None APP_METRICS_DISABLED = False diff --git a/app_metrics/tests/statsd_tests.py b/app_metrics/tests/statsd_tests.py deleted file mode 100644 index 49ea8f4..0000000 --- a/app_metrics/tests/statsd_tests.py +++ /dev/null @@ -1,55 +0,0 @@ -from decimal import Decimal -import mock -import time -from django.test import TestCase -from django.conf import settings -from app_metrics.utils import metric, timing, gauge - - -class StatsdCreationTests(TestCase): - def setUp(self): - super(StatsdCreationTests, self).setUp() - self.old_backend = getattr(settings, 'APP_METRICS_BACKEND', None) - settings.APP_METRICS_BACKEND = 'app_metrics.backends.statsd' - - def test_metric(self): - with mock.patch('statsd.Client') as mock_client: - instance = mock_client.return_value - instance._send.return_value = 1 - - metric('testing') - mock_client._send.assert_called_with(mock.ANY, {'testing': '1|c'}) - - metric('testing', 2) - mock_client._send.assert_called_with(mock.ANY, {'testing': '2|c'}) - - metric('another', 4) - mock_client._send.assert_called_with(mock.ANY, {'another': '4|c'}) - - def test_timing(self): - with mock.patch('statsd.Client') as mock_client: - instance = mock_client.return_value - instance._send.return_value = 1 - - with timing('testing'): - time.sleep(0.025) - - mock_client._send.assert_called_with(mock.ANY, {'testing.total': mock.ANY}) - - def test_gauge(self): - with mock.patch('statsd.Client') as mock_client: - instance = mock_client.return_value - instance._send.return_value = 1 - - gauge('testing', 10.5) - mock_client._send.assert_called_with(mock.ANY, {'testing': '10.5|g'}) - - gauge('testing', Decimal('6.576')) - mock_client._send.assert_called_with(mock.ANY, {'testing': '6.576|g'}) - - gauge('another', 1) - mock_client._send.assert_called_with(mock.ANY, {'another': '1|g'}) - - def tearDown(self): - settings.APP_METRICS_BACKEND = self.old_backend - super(StatsdCreationTests, self).tearDown() diff --git a/app_metrics/tests/base_tests.py b/app_metrics/tests/test_base.py similarity index 87% rename from app_metrics/tests/base_tests.py rename to app_metrics/tests/test_base.py index d422aaf..581686e 100644 --- a/app_metrics/tests/base_tests.py +++ b/app_metrics/tests/test_base.py @@ -1,27 +1,35 @@ import datetime from decimal import Decimal -import mock +from unittest import mock -from django.test import TestCase +from django.test import TransactionTestCase, TestCase from django.core import management from django.conf import settings from django.core import mail -from django.contrib.auth.models import User +from django.db import transaction, IntegrityError +from django.contrib.auth import get_user_model from django.core.exceptions import ImproperlyConfigured +from django.utils import timezone from app_metrics.exceptions import TimerError from app_metrics.models import Metric, MetricItem, MetricDay, MetricWeek, MetricMonth, MetricYear, Gauge from app_metrics.utils import * from app_metrics.trending import _trending_for_current_day, _trending_for_yesterday, _trending_for_week, _trending_for_month, _trending_for_year -class MetricCreationTests(TestCase): + +class MetricCreationTests(TransactionTestCase): def test_auto_slug_creation(self): new_metric = Metric.objects.create(name='foo bar') self.assertEqual(new_metric.name, 'foo bar') self.assertEqual(new_metric.slug, 'foo-bar') - new_metric2 = Metric.objects.create(name='foo bar') + try: + with transaction.atomic(): + new_metric2 = Metric.objects.create(name='foo bar') + except IntegrityError: + pass + self.assertEqual(new_metric2.name, 'foo bar') self.assertEqual(new_metric2.slug, 'foo-bar_1') @@ -220,8 +228,8 @@ def test_missing_trending(self): class EmailTests(TestCase): """ Test that our emails send properly """ def setUp(self): - self.user1 = User.objects.create_user('user1', 'user1@example.com', 'user1pass') - self.user2 = User.objects.create_user('user2', 'user2@example.com', 'user2pass') + self.user1 = get_user_model().objects.create_user('user1', 'user1@example.com', 'user1pass') + self.user2 = get_user_model().objects.create_user('user2', 'user2@example.com', 'user2pass') self.metric1 = create_metric(name='Test Trending1', slug='test_trend1') self.metric2 = create_metric(name='Test Trending2', slug='test_trend2') self.set = create_metric_set(name="Fake Report", @@ -336,3 +344,32 @@ def test_mixpanel_op(self): def tearDown(self): settings.APP_METRICS_BACKEND = self.old_backend + +class MockTimezone(datetime.tzinfo): + def utcoffset(self, dt): + return datetime.timedelta(hours=-9) + +class TimestampTest(TestCase): + """ Test timestamp utilities """ + + def test_tz_timestamp(self): + dt = datetime.datetime(2016, 5, 1, 0, 0, 0, tzinfo=timezone.utc) + utc_ts = get_timestamp(dt) + dt_tz = datetime.datetime(2016, 5, 1, 0, 0, 0, tzinfo=MockTimezone()) + tz_ts = get_timestamp(dt_tz) + + # timestamp with a UTC offset of -9 hours should be ahead by 32400 seconds. + self.assertEqual(utc_ts, tz_ts - 32400) + + def test_naive_timestamp(self): + dt = datetime.datetime(2016, 5, 1, 0, 0, 0, tzinfo=None) + + # Naive datetime to timestamp will just use local timezone. Same + # behavior as using strftime('%s') on 'nix systems. + + # The above dt, even in a TZ of -12 hours would have to be more than + # or equal to 1462060800 (UTC) - 43200 seconds as a timestamp. + self.assertGreaterEqual(get_timestamp(dt), 1462060800 - 43200) + + # However, it should be less than or equal to the same +12 hours. + self.assertLessEqual(get_timestamp(dt), 1462060800 + 43200) diff --git a/app_metrics/tests/test_librato.py b/app_metrics/tests/test_librato.py new file mode 100644 index 0000000..e69de29 diff --git a/app_metrics/tests/mixpanel_tests.py b/app_metrics/tests/test_mixpanel.py similarity index 100% rename from app_metrics/tests/mixpanel_tests.py rename to app_metrics/tests/test_mixpanel.py diff --git a/app_metrics/tests/redis_tests.py b/app_metrics/tests/test_redis.py similarity index 86% rename from app_metrics/tests/redis_tests.py rename to app_metrics/tests/test_redis.py index afdabc2..1b53ec0 100644 --- a/app_metrics/tests/redis_tests.py +++ b/app_metrics/tests/test_redis.py @@ -1,8 +1,16 @@ -import mock +from unittest import mock from django.test import TestCase from django.conf import settings from app_metrics.utils import metric, gauge +from unittest import skipUnless +try: + import redis +except ImportError: + redis = None + + +@skipUnless(redis, "No redis module. Skipping.") class RedisTests(TestCase): def setUp(self): super(RedisTests, self).setUp() diff --git a/app_metrics/tests/test_statsd.py b/app_metrics/tests/test_statsd.py new file mode 100644 index 0000000..1d618a2 --- /dev/null +++ b/app_metrics/tests/test_statsd.py @@ -0,0 +1,60 @@ +from decimal import Decimal +from unittest import mock +import time +from django.test import TestCase +from django.conf import settings +from app_metrics.utils import metric, timing, gauge +from unittest import skipUnless + +try: + import statsd +except ImportError: + statsd = None + + +@skipUnless(statsd, "No statsd module. Skipping.") +class StatsdCreationTests(TestCase): + def setUp(self): + super(StatsdCreationTests, self).setUp() + self.old_backend = getattr(settings, 'APP_METRICS_BACKEND', None) + settings.APP_METRICS_BACKEND = 'app_metrics.backends.statsd' + + def test_metric(self): + with mock.patch('statsd.StatsClient._send') as mock_client_send: + mock_client_send.return_value = 1 + + metric('testing') + mock_client_send.assert_called_with('testing:1|c') + + metric('testing', 2) + mock_client_send.assert_called_with('testing:2|c') + + metric('another', 4) + mock_client_send.assert_called_with('another:4|c') + + def test_timing(self): + with mock.patch('statsd.StatsClient._send') as mock_client_send: + mock_client_send.return_value = 1 + + with timing('testing'): + time.sleep(0.025) + + args, kwargs = mock_client_send.call_args + self.assertRegex(args[0], r'testing:\d{2}\.0000|ms') + + def test_gauge(self): + with mock.patch('statsd.StatsClient._send') as mock_client_send: + mock_client_send.return_value = 1 + + gauge('testing', 10.5) + mock_client_send.assert_called_with('testing:10.5|g') + + gauge('testing', Decimal('6.576')) + mock_client_send.assert_called_with('testing:6.576|g') + + gauge('another', 1) + mock_client_send.assert_called_with('another:1|g') + + def tearDown(self): + settings.APP_METRICS_BACKEND = self.old_backend + super(StatsdCreationTests, self).tearDown() diff --git a/app_metrics/tests/urls.py b/app_metrics/tests/urls.py index 1656170..a4bbcdd 100644 --- a/app_metrics/tests/urls.py +++ b/app_metrics/tests/urls.py @@ -1,10 +1,9 @@ -from django.conf.urls import * -from django.conf import settings +from django.conf.urls import include, url from django.contrib import admin admin.autodiscover() -urlpatterns = patterns('', - (r'^admin/', include(admin.site.urls)), - (r'^admin/metrics/', include('app_metrics.urls')), -) +urlpatterns = [ + url(r'^admin/', admin.site.urls), + url(r'^admin/metrics/', include('app_metrics.urls')), +] diff --git a/app_metrics/urls.py b/app_metrics/urls.py index f5c5c73..073ab5b 100644 --- a/app_metrics/urls.py +++ b/app_metrics/urls.py @@ -1,11 +1,11 @@ -from django.conf.urls import * +from django.conf.urls import url -from app_metrics.views import * +from app_metrics.views import metric_report_view -urlpatterns = patterns('', - url( - regex = r'^reports/$', - view = metric_report_view, - name = 'app_metrics_reports', - ), - ) + +urlpatterns = [ + url(r'^reports/$', + metric_report_view, + name='app_metrics_reports', + ), +] diff --git a/app_metrics/utils.py b/app_metrics/utils.py index 8d6a727..4ba04dc 100644 --- a/app_metrics/utils.py +++ b/app_metrics/utils.py @@ -2,7 +2,8 @@ import datetime import time from django.conf import settings -from django.utils.importlib import import_module +from importlib import import_module +from django.utils import timezone from app_metrics.exceptions import InvalidMetricsBackend, TimerError from app_metrics.models import Metric, MetricSet @@ -99,7 +100,7 @@ def import_backend(): # Attempt to import the backend try: backend = import_module(backend_string) - except Exception, e: + except Exception as e: raise InvalidMetricsBackend("Could not load '%s' as a backend: %s" % (backend_string, e)) @@ -212,7 +213,7 @@ def month_for_date(month): return month - datetime.timedelta(days=month.day-1) def year_for_date(year): - return datetime.date(year.year, 01, 01) + return datetime.date(year.year, 1, 1) def get_previous_month(date): if date.month == 1: @@ -227,3 +228,17 @@ def get_previous_year(date): new = date return new.replace(year=new.year-1) +def timedelta_total_seconds(timedelta): + return timedelta.seconds + timedelta.days * 24 * 3600 + +def get_timestamp(dt): + if dt.tzinfo is None: + return time.mktime((dt.year, dt.month, dt.day, + dt.hour, dt.minute, dt.second, + -1, -1, -1)) + else: + timedelta = dt - datetime.datetime(1970, 1, 1, tzinfo=timezone.utc) + try: + return timedelta.total_seconds() + except AttributeError: + return timedelta_total_seconds(timedelta) diff --git a/manage.py b/manage.py new file mode 100644 index 0000000..1fb207c --- /dev/null +++ b/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'app_metrics.tests.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/setup.py b/setup.py index 23fa303..202f69c 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ author='Frank Wiles', author_email='frank@revsys.com', url='https://github.com/frankwiles/django-app-metrics', - packages=find_packages(), + packages=find_packages(exclude=['app_metrics/tests*']), package_data={ 'app_metrics': [ 'templates/app_metrics/*', @@ -26,7 +26,7 @@ 'celery', 'django-celery', ], - tests_require = ['mock', 'django-coverage', 'coverage'], + tests_require = ['pytest-django', 'pytest-cov'], classifiers=[ 'Development Status :: 4 - Beta', 'Environment :: Web Environment', diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..72a1014 --- /dev/null +++ b/tox.ini @@ -0,0 +1,42 @@ +[tox] +envlist = + {py35,py36,py37,py38}-django22, + {py36,py37,py38}-django30, + +[travis:env] +DJANGO = + 2.2: django22 + 3.0: django30 + +[testenv] +commands = pytest --cov --cov-report=html --cov-report=term {posargs} +envdir = {toxworkdir}/venvs/{envname} +setenv = + PYTHONDONTWRITEBYTECODE=1 + PYTHONWARNINGS=once +deps = + django22: Django>=2.2,<3.0 + django30: Django>=3.0,<3.1 + pytest + pytest-django + pytest-cov + redis + statsd + +[pytest] +DJANGO_SETTINGS_MODULE = app_metrics.tests.settings +python_files = test_*.py + +[coverage:run] +branch = True +source = app_metrics +omit = + app_metrics/migrations/* + app_metrics/south_migrations/* + app_metrics/management/* + app_metrics/admin.py + app_metrics/compat.py + *urls.py + +[coverage:html] +directory = coverage