Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Import historical data. #2

Merged
merged 5 commits into from
Dec 19, 2014
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions misfitapp/extras.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import re
import datetime

def cc_to_underscore(name):
""" Convert camelCase name to under_score """
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()


def cc_to_underscore_keys(dictionary):
""" Convert dictionary keys from camelCase to under_score """
return dict((cc_to_underscore(key), val) for key, val in dictionary.items())


def chunkify_dates(start, end, days_in_chunk):
"""
Return a list of tuples that chunks the date range into ranges
of length days_in_chunk.
"""
chunks = []
s = start
e = start + datetime.timedelta(days=days_in_chunk)
while e - datetime.timedelta(days=30) < end:
e = min(e, end)
chunks.append((s, e))
s = e + datetime.timedelta(days=1)
e = s + datetime.timedelta(days=days_in_chunk)
return chunks
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call on the chunkifying! I didn't realize it was necessary.

119 changes: 119 additions & 0 deletions misfitapp/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
from django.db import models
from django.utils.encoding import python_2_unicode_compatible
from math import pow
import datetime

from .extras import cc_to_underscore_keys, chunkify_dates

MAX_KEY_LEN = 24
UserModel = getattr(settings, 'AUTH_USER_MODEL', 'auth.User')
Expand Down Expand Up @@ -37,6 +40,27 @@ def __str__(self):
class Meta:
unique_together = ('user', 'date')

@classmethod
def create_from_misfit(cls, misfit, uid, start_date=datetime.date(2014,1,1), end_date=datetime.date.today()):
"""
Imports all Summary data from misfit for the specified date range, chunking API
calls if needed.
"""
# Keep track of the data we already have
exists = cls.objects.filter(user_id=uid,
date__gte=start_date,
date__lte=end_date).values_list('date', flat=True)
obj_list = []
date_chunks = chunkify_dates(start_date, end_date, 30)
for start, end in date_chunks:
summaries = misfit.summary(start_date=start, end_date=end, detail=True)
for summary in summaries:
if summary.date.date() not in exists:
data = cc_to_underscore_keys(summary.data)
data['user_id'] = uid
obj_list.append(cls(**data))
cls.objects.bulk_create(obj_list)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@grokcode Love that you used bulk_create on these.



@python_2_unicode_compatible
class Profile(models.Model):
Expand All @@ -51,6 +75,14 @@ class Profile(models.Model):
def __str__(self):
return self.email

@classmethod
def create_from_misfit(cls, misfit, uid):
if not cls.objects.filter(user_id=uid).exists():
profile = misfit.profile()
data = cc_to_underscore_keys(profile.data)
data['user_id'] = uid
cls(**data).save()


@python_2_unicode_compatible
class Device(models.Model):
Expand All @@ -66,6 +98,14 @@ class Device(models.Model):
def __str__(self):
return '%s: %s' % (self.device_type, self.serial_number)

@classmethod
def create_from_misfit(cls, misfit, uid):
if not cls.objects.filter(user_id=uid).exists():
device = misfit.device()
data = cc_to_underscore_keys(device.data)
data['user_id'] = uid
cls(**data).save()


@python_2_unicode_compatible
class Goal(models.Model):
Expand All @@ -80,6 +120,30 @@ def __str__(self):
return '%s %s %s of %s' % (self.id, self.date, self.points,
self.target_points)

class Meta:
unique_together = ('user', 'date')

@classmethod
def create_from_misfit(cls, misfit, uid, start_date=datetime.date(2014,1,1), end_date=datetime.date.today()):
"""
Imports all Goal data from misfit for the specified date range, chunking API
calls if needed.
"""
# Keep track of the data we already have
exists = cls.objects.filter(user_id=uid,
date__gte=start_date,
date__lte=end_date).values_list('date', flat=True)
obj_list = []
date_chunks = chunkify_dates(start_date, end_date, 30)
for start, end in date_chunks:
goals = misfit.goal(start_date=start, end_date=end)
for goal in goals:
if goal.date.date() not in exists:
data = cc_to_underscore_keys(goal.data)
data['user_id'] = uid
obj_list.append(cls(**data))
cls.objects.bulk_create(obj_list)


@python_2_unicode_compatible
class Session(models.Model):
Expand All @@ -103,6 +167,29 @@ class Session(models.Model):
def __str__(self):
return '%s %s %s' % (self.start_time, self.duration,
self.activity_type)
class Meta:
unique_together = ('user', 'start_time')

@classmethod
def create_from_misfit(cls, misfit, uid, start_date=datetime.date(2014,1,1), end_date=datetime.date.today()):
"""
Imports all Session data from misfit for the specified date range, chunking API
calls if needed.
"""
# Keep track of the data we already have
exists = cls.objects.filter(user_id=uid,
start_time__gte=start_date,
start_time__lte=end_date).values_list('start_time', flat=True)
obj_list = []
date_chunks = chunkify_dates(start_date, end_date, 30)
for start, end in date_chunks:
sessions = misfit.session(start_date=start, end_date=end)
for session in sessions:
if session.startTime not in exists:
data = cc_to_underscore_keys(session.data)
data['user_id'] = uid
obj_list.append(cls(**data))
cls.objects.bulk_create(obj_list)


@python_2_unicode_compatible
Expand All @@ -116,6 +203,38 @@ class Sleep(models.Model):
def __str__(self):
return '%s %s' % (self.start_time, self.duration)

class Meta:
unique_together = ('user', 'start_time')

@classmethod
def create_from_misfit(cls, misfit, uid, start_date=datetime.date(2014,1,1), end_date=datetime.date.today()):
"""
Imports all Sleep and Sleep Segment data from misfit for the specified date range,
chunking API calls if needed.
"""
# Keep track of the data we already have
exists = cls.objects.filter(user_id=uid,
start_time__gte=start_date,
start_time__lte=end_date).values_list('start_time', flat=True)
seg_list = []
date_chunks = chunkify_dates(start_date, end_date, 30)
for start, end in date_chunks:
sleeps = misfit.sleep(start_date=start, end_date=end)
for sleep in sleeps:
if sleep.startTime not in exists:
data = cc_to_underscore_keys(sleep.data)
data['user_id'] = uid
segments = data.pop('sleep_details')
s = cls(**data)
s.save()
for seg in segments:
seg_list.append(SleepSegment(sleep=s,
time=seg['datetime'],
sleep_type=seg['value']))


SleepSegment.objects.bulk_create(seg_list)


@python_2_unicode_compatible
class SleepSegment(models.Model):
Expand Down
19 changes: 11 additions & 8 deletions misfitapp/tasks.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import arrow
import logging
import re
import sys

from celery import shared_task
from celery.exceptions import Reject
from cryptography.exceptions import InvalidSignature
from django.core.cache import cache
from datetime import timedelta, date
from misfit.exceptions import MisfitRateLimitError
from misfit.notification import MisfitNotification

Expand All @@ -21,19 +21,22 @@
Summary,
Goal
)
from .extras import cc_to_underscore_keys

logger = logging.getLogger(__name__)


def cc_to_underscore(name):
""" Convert camelCase name to under_score """
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
@shared_task
def import_historical(misfit_user):
"""
Import a user's historical data from Misfit starting at start_date.
If there is existing data, it is not overwritten.
"""

misfit = utils.create_misfit(access_token=misfit_user.access_token)
for cls in (Profile, Device, Summary, Goal, Session, Sleep):
cls.create_from_misfit(misfit, misfit_user.user_id)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@grokcode Do we need to wrap this block in a try..except or do you think we're safe? In my experience, I've seen the celery queue crash completely when an uncaught exception occurs, then I have to restart the queue manually (if I even notice it has stopped)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@brad Yeah, and I think hitting a rate limit error there is pretty likely, so I will put some error handling and recovery there before we merge. Good catch.


def cc_to_underscore_keys(dictionary):
""" Convert dictionary keys from camelCase to under_score """
return dict((cc_to_underscore(key), val) for key, val in dictionary.items())


@shared_task
Expand Down
5 changes: 4 additions & 1 deletion misfitapp/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

from . import utils
from .models import MisfitUser
from .tasks import process_notification
from .tasks import process_notification, import_historical


@login_required
Expand Down Expand Up @@ -88,6 +88,9 @@ def complete(request):
# Add the Misfit user info to the session
request.session['misfit_profile'] = profile.data

# Import their data TODO: Fix failing tests, when below line is uncommented
# import_historical.delay(misfit_user)

next_url = request.session.pop('misfit_next', None) or utils.get_setting(
'MISFIT_LOGIN_REDIRECT')
return redirect(next_url)
Expand Down