Skip to content

Commit

Permalink
Revert "Revert "Merge pull request #135 from cheejung/main""
Browse files Browse the repository at this point in the history
This reverts commit f3c69a2.
  • Loading branch information
TrafficCop committed Jan 16, 2024
1 parent f3c69a2 commit 7517886
Show file tree
Hide file tree
Showing 11 changed files with 2,395 additions and 1 deletion.
1 change: 1 addition & 0 deletions CONTRIBUTORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@
- Jack Hung (jjhung66@stanford.edu)
- Saarth Shah (saarth@berkeley.edu)
- Suvan Kumar (kumarsuvan0@gmail.com)
- Hee Jung Choi (cheejung@stanford.edu)
3 changes: 3 additions & 0 deletions gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.DS_Store
.project
*.pyc
1,618 changes: 1,617 additions & 1 deletion poetry.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ fbm = "^0.3.0"
beautifulsoup4 = "^4.12.2"
myfitnesspal = "^2.0.1"
polyline = "^2.0.0"
jupyter = "^1.0.0"

[tool.poetry.group.dev.dependencies]
bandit = "^1.7.1"
Expand Down
91 changes: 91 additions & 0 deletions tests/devices/biostrap/test_evo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
from datetime import datetime, timedelta

import pytest

import wearipedia

data_formats = {
"bpm": (int, float),
"brpm": (int, float),
"hrv": (int, float),
"spo2": (int, float),
"rest_cals": (int, float),
"work_cals": (int, float),
"active_cals": (int, float),
"step_cals": (int, float),
"total_cals": (int, float),
"steps": (int, float),
"distance": (int, float),
}


@pytest.mark.parametrize("real", [True, False])
def test_evo(real):
start_synthetic = datetime(2023, 6, 5)
end_synthetic = datetime(2023, 6, 20, 23, 59, 59)

device = wearipedia.get_device(
"biostrap/evo",
start_date=datetime.strftime(start_synthetic, "%Y-%m-%d"),
end_date=datetime.strftime(end_synthetic, "%Y-%m-%d"),
)
if real:
wearipedia._authenticate_device("evo", device)

for data_type, data_format in data_formats.items():
data = device.get_data(data_type)

# Checks specific to date-keyed data
if data_type in [
"rest_cals",
"work_cals",
"active_cals",
"step_cals",
"total_cals",
]:
dates = list(data.keys())

# Check dates are consecutive and within the range
expected_date = start_synthetic
for date_str in dates:
date = datetime.strptime(date_str, "%Y-%m-%d")
assert (
date == expected_date
), f"Expected date {expected_date}, but got {date}"
expected_date += timedelta(days=1)

# Checks specific to datetime-keyed data
elif data_type in ["steps", "distance"]:

datetimes = [
datetime.strptime(dt_str, "%Y-%m-%d %H:%M:%S") for dt_str in data.keys()
]

# Check that datetimes are within the specified range.
for dt in datetimes:
assert (
start_synthetic <= dt <= end_synthetic
), f"Datetime {dt} out of range"

# Check that the datetimes are sequential and increase by a minute.
datetimes.sort()
for i in range(1, len(datetimes)):
expected_dt = datetimes[i - 1] + timedelta(minutes=1)
assert (
datetimes[i] == expected_dt
), f"Expected datetime {expected_dt}, but got {datetimes[i]}"
else:
datetimes = [key[0] for key in data.keys()]

# Check datetimes are within the range
for datetime_str in datetimes:
dt = datetime.strptime(datetime_str, "%Y-%m-%d %H:%M:%S")
assert (
start_synthetic <= dt <= end_synthetic
), f"Datetime {dt} out of range"

# Check data values are of expected type
for value in data.values():
assert isinstance(
value, data_format
), f"{data_type} data {value} is not a {data_format.__name__}"
1 change: 1 addition & 0 deletions wearipedia/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ def get_all_device_names():

return [
"apple/healthkit",
"biostrap/evo",
"cronometer/cronometer",
"whoop/whoop_4",
"withings/scanwatch",
Expand Down
1 change: 1 addition & 0 deletions wearipedia/devices/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from .apple import *
from .biostrap import *
from .cronometer import *
from .dexcom import *
from .dreem import *
Expand Down
1 change: 1 addition & 0 deletions wearipedia/devices/biostrap/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .evo import *
169 changes: 169 additions & 0 deletions wearipedia/devices/biostrap/evo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import os
import pickle
import webbrowser
from datetime import datetime
from urllib.parse import urlencode

import requests

from ...utils import seed_everything
from ..device import BaseDevice
from .evo_fetch import *
from .evo_gen import *

class_name = "EVO"


class EVO(BaseDevice):
def __init__(self, seed=0, start_date="2023-06-05", end_date="2023-06-20"):
params = {
"seed": seed,
"start_date": str(start_date),
"end_date": str(end_date),
}

self._initialize_device_params(
[
"activities",
"bpm",
"brpm",
"hrv",
"spo2",
"rest_cals",
"work_cals",
"active_cals",
"step_cals",
"total_cals",
"sleep_session",
"sleep_detail",
"steps",
"distance",
],
params,
{
"seed": 0,
"synthetic_start_date": "2023-06-05",
"synthetic_end_date": "2023-06-20",
},
)

def _default_params(self):
return {
"start_date": self.init_params["synthetic_start_date"],
"end_date": self.init_params["synthetic_end_date"],
}

def _get_real(self, data_type, params):
return fetch_real_data(
self.access_token, params["start_date"], params["end_date"], data_type
)

def _filter_synthetic(self, data, data_type, params):
start_date = params["start_date"]
end_date = params["end_date"]

start_datetime = f"{start_date} 00:00:00"
end_datetime = f"{end_date} 23:59:59"

# For data types that are stored with a date string as a key
if data_type in [
"rest_cals",
"work_cals",
"active_cals",
"step_cals",
"total_cals",
]:
return {
date: value
for date, value in data.items()
if start_date <= date <= end_date
}

# For data types that are stored with a datetime string as a key in a tuple
elif data_type in [
"bpm",
"brpm",
"spo2",
"hrv",
]:
return {
key: value
for key, value in data.items()
if start_datetime <= key[0] <= end_datetime
}

# For steps and distance that use datetime strings as keys (assuming distance also has datetime as per steps)
elif data_type in ["steps", "distance"]:
return {
datetime: value
for datetime, value in data.items()
if start_datetime <= datetime <= end_datetime
}

else:
return data

def _gen_synthetic(self):
# generate random data according to seed
seed_everything(self.init_params["seed"])
# and based on start and end dates
(
self.activities,
self.bpm,
self.brpm,
self.hrv,
self.spo2,
self.rest_cals,
self.work_cals,
self.active_cals,
self.step_cals,
self.total_cals,
self.sleep_session,
self.sleep_detail,
self.steps,
self.distance,
) = create_syn_data(
self.init_params["synthetic_start_date"],
self.init_params["synthetic_end_date"],
)

# We get the access token to make requests to the Biostrap API
def _authenticate(self, auth_creds):
self.client_id = auth_creds["client_id"]
self.client_secret = auth_creds["client_secret"]
redirect_uri = "https://127.0.0.1:8080"
token_url = "https://auth.biostrap.com/token"

# Generate the authorization URL and open it in the default web browser
params = {
"client_id": self.client_id,
"response_type": "code",
"redirect_uri": redirect_uri,
}
authorization_url = f"https://auth.biostrap.com/authorize?{urlencode(params)}"

# Instruct the user to open the URL in their browser ()
print(
f"Please go to the following URL and authorize access: {authorization_url}"
)
print(
"If a page cannot be displayed, please make sure you are logged into the Biostrap account in your browser."
)

# Get the authorization response URL from the command line (the method Jack and I talked about didn't work)
authorization_response = input("Enter the full callback URL: ")

code = authorization_response.split("code=")[1].split("&")[0]

# Now we can request the access token
data = {
"grant_type": "authorization_code",
"code": code,
"redirect_uri": redirect_uri,
}
auth = (self.client_id, self.client_secret)
response = requests.post(token_url, auth=auth, data=data)

# Get the access token from the response
token_json = response.json()
self.access_token = token_json["access_token"]
Loading

0 comments on commit 7517886

Please sign in to comment.