Building a Fitness Tracking Dashboard with Python. Pt 2 Data Update and SQL Alchemy

Now that we have developed classes to help us interact for both the Fitbit and Strava API, we need a way to store that data where it is easily accessible. In order to do so we will roll out a postgres database on docker and use sqlalchemy to simplify our interactions with the db.

Project Organization

Since eventually I want to setup a flask website to display the information, I will start setting up the project as a flask application factory and separating into modules. There are much better tutorials out there for how to set it up and I recommend searching for them.
At this point my project structure looks as follows.

\app
  \static
    \examples
    \js
  \stats
    __init__.py
    models.py
  \templates
  __init__.py
stats_con.py
tasks.py

Test DB setup

Since we are still in the development stages we choose to roll out a docker based db  (in this case Postgres).

We can spin up the container with the following command

docker run --name sqlalchemy-orm-psql -e
POSTGRES_PASSWORD=pass -e POSTGRES_USER=usr -e
POSTGRES_DB=sqlalchemy -p 5432:5432 -d postgres

This will initialize postgres database at localhost:5432 a username named usr and set its password as pass, finally it will also create a database called sqlalchemy.

Creating the sqlalchemy classes

SQLAlchemy is a great tool for setting up our database interactions and tables (it also has a very useful flask plugin for down the line).

The first step is setting up classes to allow us to interact with the database and to function as representations of our tables.

Since this isn't a particularly complex projects I only setup one folder to contain my classes under the stats folder, making sure to create the __init__.py to treat it as a module.

Upper Level Initialization

Since I will eventually move everything to flask, we want to start enough to it. Under the app subfolder we want to create an __init__.py file to initialize properly and allow for imports into the sqlalchemny classes.

All we are doing in this file is creating the database connection and setting its parameters.

We import sqlalchemy and create the engine using the previously created postgres db URI.

from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

engine = create_engine('postgresql://usr:pass@localhost:5432/sqlalchemy')

We use this engine to create session which allows us to run queries against the database and functions as our main connection to it.

Session = sessionmaker(bind=engine)

Finally we create a Base, from the declarative_base() function. This creates the base for our classes. This makes sure that the correct Table objects are created and properly maps our objects to the database.

Base=declarative_base()

This more than likely will be changed when using flask since flask-alchemy has additional functionality tied to using db.Model as opposed to Base=declarative_base()

Fitbit Classes

Now that we have initialize the connection to our database and created the base factory we can create the actual classes for our data.

First we import our Base object and the necessary sqlalchemy.

from .. import Base
from sqlalchemy import BigInteger, Column, String, Integer, ForeignKey, Float, DateTime, TIMESTAMP, func, Time

class Fitbit_Weight(Base):
    __tablename__='fitbit_weight'
    id=Column(BigInteger(), primary_key=True)
    weight=Column(Float(), nullable=False)
    bmi=Column(Float())
    fat=Column(Float())
    record_date=Column(DateTime())
    record_time=Column(Time())
    last_time = Column(TIMESTAMP, server_default=func.now(), onupdate=func.current_timestamp())

    def __repr__(self):
        return "<FITBIT WEIGHT '%s', weight='%s', bmi='%s', date='%s'>"%(self.id, self.weight, self.bmi, self.record_date)

class Fitbit_Calories(Base):
    __tablename__='fitbit_calories'
    id=Column(BigInteger(), primary_key=True)
    calories=Column(Float())
    record_date=Column(DateTime())
    last_time = Column(TIMESTAMP, server_default=func.now(), onupdate=func.current_timestamp())

    def __repr__(self):
        return "<FITBIT Calories '%s', calories='%s', date='%s'>"%(self.id, self.calories, self.record_date)

The process is pretty self explanatory. The the things to pay attention to is to remember to create a primary_key on each table.

w also used the func function to make sure that I store the time at which the records are updated.

Finally to assist with debugging and logging to a certain degree, we create a __repr__ method to allow for a more informative message for each of objects created with these classes.

Strava Class

The same process is used for the Strava information

class Strava_Activity(Base):
    __tablename__='strava_activity'
    #index=Column(Integer(), primary_key=True)
    id=Column(BigInteger(), primary_key=True)
    owner=Column(Integer())#Probs Foregin keyring
    activity_type=Column(String(50))
    distance=Column(Float())
    elapsed_time=Column(Float())
    average_speed=Column(Float())
    average_cadence=Column(Float())
    average_heartrate=Column(Float())
    name=Column(String(50))
    utc_offset=Column(Float())
    max_speed=Column(Float())
    max_heartrate=Column(Float())
    total_elevation_gain=Column(Float())
    upload_id=Column(BigInteger())
    moving_time=Column(Float())
    start_date=Column(DateTime())
    start_date_local=Column(DateTime())
    last_time = Column(TIMESTAMP, server_default=func.now(), onupdate=func.current_timestamp())

    def __repr__(self):
        return "<STRAVA ACTIVITY '%s', distance='%s', type='%s', date='%s'>"%(self.id, self.distance, self.activity_type, self.start_date_local)

Storing the data

Now that we have a well defined way to access our database we can get to the process of actually recording the data obtained from our API calls into the db.

The first step is to create all the tables we defined in our sqlalchemy classes if they dont exist.
Base.metadata.create_all()

We also initialize our db session with session=Session()

Importing the stats_con.py classes Strava and Fitbit we can use the previously defined functions to obtain our information.

def Update_Strava_Activities():
    # Initialize Strava connection and get the data
    stv=Strava()
    data=stv.get_activities().json()

    # Get the required columns from our Strava Class
    strava_params=[c for c in inspect(Strava_Activity).columns.keys()]

    # Remove the last time parameter as that is autogenerated
    strava_params.remove('last_time')

    #We will first create all our model class instances for Strava_Activity
    acts=[]
    for dic in data:
        #Initialize an empty default dict so we dont get triped up with key missing issues
        d = defaultdict(lambda: None, dic)
        #Rename some columns from the API json so they match our class
        d['owner']=d['athlete']['id']
        d['activity_type']=d['type']

        #Search for values needed in our class in the API json
        update={}
        for val in strava_params:
            update[val]=d[val]

        log.info(update)

        #Initialize our model class from the dictionary
        act=Strava_Activity(**update)
        acts.append(act)

    # Merge our results into the database (I will rewrite all of them for the last 30 items regardless of what it says), at the current moment I don't need to check the API for deleted activities but might in the future.
    for act in acts:
        try:
            with session.begin_nested():
                session.merge(act)
            log.info("Updated: %s"%str(act))
        except:
            log.info("Skipped %s"%str(act))
    session.commit()
    session.flush()

First we initialize our Strava connection using our previously developed class.
We obtain our data using stv.get_activities().json(). This will return a json with our information.

Since we need to define the values of the  Since we are lazy we actually get the names of the sqlalchemy columns from the sqlalchemy Strava Class. We can do that using inspect and pushing into an array.

strava_params=[c for c in inspec(Strava_Activity).columns.keys()]

In the next code block we iterate thru the array and push the values into a default dictionary. The reason we use a default dictionary is to prepolulate with nulls since some activities will not have all the infomrmation. We also use this to rename some of the obtained json values into the correct column name.

We use**update to pass a dictionary to initialize our sqlaclhemy object from a dictionary instead of typing it out.

Then we use our session object with begin_nested and try to merge our recods and commit them, this is better explained at sqlalchemy documentation.

We will also do the same for our Fitbit Weight

def Update_Fitbit_Weight():
    fbt=Fitbit()
    wdata=fbt.get_weight().json()

    fweight_params=[c for c in inspect(Fitbit_Weight).columns.keys()]
    fweight_params.remove('last_time')

    acts=[]

    for dic in wdata['weight']:
        d = defaultdict(lambda: None, dic)
        d['id']=d['logId']
        d['record_date']=d['date']
        d['record_time']=d['time']

        update={}
        for val in fweight_params:
            update[val]=d[val]

        act=Fitbit_Weight(**update)
        acts.append(act)

    for act in acts:
        try:
            with session.begin_nested():
                session.merge(act)
            log.info("Updated: %s"%str(act))
        except:
            log.info("Skipped %s"%str(act))
    session.commit()
    session.flush()

and the Fitbit Calories

def Update_Fitbit_Calories():
    fbt=Fitbit()
    #The calories dont have an ID so create one out of the date
    cdata=fbt.get_calories().json()
    acts=[]
    for dic in cdata['foods-log-caloriesIn']:
        d = defaultdict(lambda: None, dic)
        update={}
        update['id']=int(datetime.datetime.strptime(d['dateTime'], '%Y-%m-%d').timestamp())
        update['record_date']=d['dateTime']
        update['calories']=d['value']

        act=Fitbit_Calories(**update)
        acts.append(act)

    for act in acts:
        try:
            with session.begin_nested():
                session.merge(act)
            log.info("Updated: %s"%str(act))
        except:
            log.info("Skipped %s"%str(act))
    session.commit()
    session.flush()

So now we have a method of storing our data into our database.

Scheduling our Updates

Finally we probably want to set this to update on a schedule. There are multiple ways of doing this. The most popular being chrontab or something with a celery scheduled worker. I have also been looking at using airflow for stuff like this. However at this moment we just want to keep this simple and for that Prefect works pretty well.

All we have to do is create tasks and define a Flow and a Schedule, we can even set up dependent tasks that way.

So for example we update the Update_Strava_Activities function to be a Prefect tasks.

@task(max_retries=2, retry_delay=timedelta(seconds=2))
def Update_Strava_Activities():

Which sets it to retry 2 times with a 2 second delay between retries.
We then define theses tasks inside a flow, tied to a schedule.

from datetime import timedelta
import prefect
from prefect import Flow, Parameter, task, unmapped
from prefect.schedules import IntervalSchedule

...

schedule = IntervalSchedule(interval=timedelta(minutes=60))

with Flow("Data Updater", schedule) as flow:
    Update_Strava_Activities()
    Update_Fitbit_Weight()
    Update_Fitbit_Calories()

This will run both of those tasks independently (so if one fails it wont prevent the other from running) and they will fire off every 60 seconds. All we have to do after is run the flow with flow.run() and the script will fire off and update our data every 60 minutes.

In the final part we will modify this basic project and develop a quick flask dashboard to present a calendar view of our weekly stats.

Get the github repo here.