-
Notifications
You must be signed in to change notification settings - Fork 1
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
Changes from 1 commit
d13f4e9
6f4e607
4ac6dd8
17ace59
aa262a5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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') | ||
|
@@ -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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @grokcode Love that you used |
||
|
||
|
||
@python_2_unicode_compatible | ||
class Profile(models.Model): | ||
|
@@ -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): | ||
|
@@ -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): | ||
|
@@ -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): | ||
|
@@ -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 | ||
|
@@ -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): | ||
|
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 | ||
|
||
|
@@ -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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @grokcode Do we need to wrap this block in a There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
There was a problem hiding this comment.
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.