my.jawbone: minor cleanup & refactoring, proper error propagation

This commit is contained in:
Dima Gerasimov 2020-09-10 20:52:33 +01:00 committed by karlicoss
parent 99e50f0afe
commit 63b848087d
7 changed files with 108 additions and 86 deletions

View file

@ -53,11 +53,10 @@ def from_orgmode() -> Iterator[Result]:
) )
def dataframe(): def make_dataframe(data: Iterator[Result]):
import pandas as pd # type: ignore import pandas as pd # type: ignore
entries = from_orgmode()
def it(): def it():
for e in from_orgmode(): for e in data:
if isinstance(e, Exception): if isinstance(e, Exception):
dt = extract_error_datetime(e) dt = extract_error_datetime(e)
yield { yield {
@ -75,6 +74,11 @@ def dataframe():
df.index = pd.to_datetime(df.index, utc=True) df.index = pd.to_datetime(df.index, utc=True)
return df return df
def dataframe():
entries = from_orgmode()
return make_dataframe(entries)
# TODO move to a submodule? e.g. my.body.weight.orgmode? # TODO move to a submodule? e.g. my.body.weight.orgmode?
# so there could be more sources # so there could be more sources
# not sure about my.body thing though # not sure about my.body thing though

View file

@ -116,7 +116,7 @@ def extract_error_datetime(e: Exception) -> Optional[datetime]:
import re import re
# TODO FIXME meh. definitely need to preserve exception args types in cachew if possible.. # TODO FIXME meh. definitely need to preserve exception args types in cachew if possible..
for x in reversed(e.args): for x in reversed(e.args):
m = re.search(r'\d{4}.*T.*:..(\.\d{6})?(\+.....)?', x) m = re.search(r'\d{4}-\d\d-\d\d(T..:..:..)?(\.\d{6})?(\+.....)?', x)
if m is None: if m is None:
continue continue
ss = m.group(0) ss = m.group(0)
@ -141,3 +141,7 @@ def test_datetime_errors():
e3 = RuntimeError(str(['one', '2019-11-27T08:56:00', 'three'])) e3 = RuntimeError(str(['one', '2019-11-27T08:56:00', 'three']))
assert extract_error_datetime(e3) is not None assert extract_error_datetime(e3) is not None
# date only
e4 = RuntimeError(str(['one', '2019-11-27', 'three']))
assert extract_error_datetime(e4) is not None

View file

@ -11,7 +11,7 @@ from typing import Dict, List, Iterable
from ..core import get_files from ..core import get_files
from ..core.common import mcachew from ..core.common import mcachew
from ..core.cachew import cache_dir from ..core.cachew import cache_dir
from ..core.error import Res from ..core.error import Res, set_error_datetime, extract_error_datetime
from ..core.types import DataFrameT from ..core.types import DataFrameT
from my.config import emfit as config from my.config import emfit as config
@ -70,6 +70,7 @@ def pre_dataframe() -> Iterable[Res[Emfit]]:
yield r yield r
else: else:
err = RuntimeError(f'Multiple sleeps per night, not supported yet: {g}') err = RuntimeError(f'Multiple sleeps per night, not supported yet: {g}')
set_error_datetime(err, dt=g[0].date)
g.clear() g.clear()
yield err yield err
@ -90,8 +91,11 @@ def dataframe() -> DataFrameT:
last = None last = None
for s in pre_dataframe(): for s in pre_dataframe():
if isinstance(s, Exception): if isinstance(s, Exception):
# todo date would be nice too? edt = extract_error_datetime(s)
d = {'error': str(s)} d = {
'date' : edt,
'error': str(s),
}
else: else:
dd = s.date dd = s.date
pday = dd - timedelta(days=1) pday = dd - timedelta(days=1)

View file

@ -1,3 +1,4 @@
# TODO this should be integrated into dashboard
# def stats(): # def stats():
# for jj in iter_datas(): # for jj in iter_datas():
# # TODO fimezone?? # # TODO fimezone??

View file

@ -1,12 +1,16 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from typing import Dict, Any, List from typing import Dict, Any, List, Iterable
import json import json
from functools import lru_cache from functools import lru_cache
from datetime import datetime, date, time, timedelta from datetime import datetime, date, time, timedelta
from pathlib import Path from pathlib import Path
import logging
import pytz import pytz
from ..core.common import LazyLogger
logger = LazyLogger(__name__)
from my.config import jawbone as config from my.config import jawbone as config
@ -16,9 +20,6 @@ SLEEPS_FILE = BDIR / 'sleeps.json'
GRAPHS_DIR = BDIR / 'graphs' GRAPHS_DIR = BDIR / 'graphs'
def get_logger():
return logging.getLogger('jawbone-provider')
XID = str # TODO how to shared with backup thing? XID = str # TODO how to shared with backup thing?
@ -32,13 +33,13 @@ class SleepEntry:
def __init__(self, js) -> None: def __init__(self, js) -> None:
self.js = js self.js = js
# TODO @memoize decorator?
@property @property
def date_(self) -> date: def date_(self) -> date:
return self.sleep_end.date() return self.sleep_end.date()
def _fromts(self, ts: int) -> datetime: def _fromts(self, ts: int) -> datetime:
return pytz.utc.localize(datetime.utcfromtimestamp(ts)).astimezone(self._tz).astimezone(self._tz) return datetime.fromtimestamp(ts, tz=self._tz)
@property @property
def _tz(self): def _tz(self):
return pytz.timezone(self._details['tz']) return pytz.timezone(self._details['tz'])
@ -103,15 +104,63 @@ def load_sleeps() -> List[SleepEntry]:
return [SleepEntry(js) for js in sleeps] return [SleepEntry(js) for js in sleeps]
from ..core.error import Res, set_error_datetime, extract_error_datetime
def pre_dataframe() -> Iterable[Res[SleepEntry]]:
sleeps = load_sleeps()
# todo emit error if graph doesn't exist??
sleeps = [s for s in sleeps if s.graph.exists()] # TODO careful..
from ..common import group_by_key
for dd, group in group_by_key(sleeps, key=lambda s: s.date_).items():
if len(group) == 1:
yield group[0]
else:
err = RuntimeError(f'Multiple sleeps per night, not supported yet: {group}')
set_error_datetime(err, dt=dd)
logger.exception(err)
yield err
def dataframe():
dicts: List[Dict] = []
for s in pre_dataframe():
if isinstance(s, Exception):
dt = extract_error_datetime(s)
d = {
'date' : dt,
'error': str(s),
}
else:
d = {
# TODO make sure sleep start/end are consistent with emfit? add to test as well..
# I think it makes sense to be end date as 99% of time
# or maybe I shouldn't care about this at all?
'date' : s.date_,
'sleep_start': s.sleep_start,
'sleep_end' : s.sleep_end,
'bed_time' : s.bed_time,
}
dicts.append(d)
import pandas as pd # type: ignore
return pd.DataFrame(dicts)
# TODO tz is in sleeps json
# TODO add dataframe support to stat()
def stats():
from ..core import stat
return stat(pre_dataframe)
#### NOTE: most of the stuff below is deprecated and remnants of my old code!
#### sorry for it, feel free to remove if you don't need it
import numpy as np # type: ignore import numpy as np # type: ignore
import matplotlib.pyplot as plt # type: ignore import matplotlib.pyplot as plt # type: ignore
from matplotlib.figure import Figure # type: ignore from matplotlib.figure import Figure # type: ignore
from matplotlib.axes import Axes # type: ignore from matplotlib.axes import Axes # type: ignore
# pip install imageio
from imageio import imread # type: ignore
def hhmm(time: datetime): def hhmm(time: datetime):
return time.strftime("%H:%M") return time.strftime("%H:%M")
@ -128,6 +177,9 @@ def plot_one(sleep: SleepEntry, fig: Figure, axes: Axes, xlims=None, showtext=Tr
span = sleep.completed - sleep.created span = sleep.completed - sleep.created
print(f"{sleep.xid} span: {span}") print(f"{sleep.xid} span: {span}")
# pip install imageio
from imageio import imread # type: ignore
img = imread(sleep.graph) img = imread(sleep.graph)
# all of them are 300x300 images apparently # all of them are 300x300 images apparently
# span for image # span for image
@ -187,28 +239,6 @@ def plot_one(sleep: SleepEntry, fig: Figure, axes: Axes, xlims=None, showtext=Tr
axes.text(xlims[1] - timedelta(hours=1.5), 20, str(sleep),) axes.text(xlims[1] - timedelta(hours=1.5), 20, str(sleep),)
# plt.text(sleep.asleep(), 0, hhmm(sleep.asleep())) # plt.text(sleep.asleep(), 0, hhmm(sleep.asleep()))
from ..common import group_by_key
def sleeps_by_date() -> Dict[date, SleepEntry]:
logger = get_logger()
sleeps = load_sleeps()
sleeps = [s for s in sleeps if s.graph.exists()] # TODO careful..
res = {}
for dd, group in group_by_key(sleeps, key=lambda s: s.date_).items():
if len(group) == 1:
res[dd] = group[0]
else:
# TODO short ones I can ignore I guess. but won't bother now
logger.error('multiple sleeps on %s: %s', dd, group)
return res
# sleeps_count = 35 # len(sleeps) # apparently MPL fails at 298 with outofmemory or something
# start = 40
# 65 is arount 1 july
# sleeps = sleeps[start: start + sleeps_count]
# sleeps = sleeps[:sleeps_count]
# dt = {k: v for k, v in dt.items() if v is not None}
# TODO not really sure it belongs here... # TODO not really sure it belongs here...
# import melatonin # import melatonin
@ -225,6 +255,7 @@ def predicate(sleep: SleepEntry):
return False return False
# TODO move to dashboard
def plot(): def plot():
# TODO FIXME melatonin data # TODO FIXME melatonin data
melatonin_data = {} # type: ignore[var-annotated] melatonin_data = {} # type: ignore[var-annotated]
@ -264,48 +295,3 @@ def plot():
# plt.savefig('res.png', asp) # plt.savefig('res.png', asp)
plt.show() plt.show()
import pandas as pd # type: ignore
def get_dataframe():
sleeps = sleeps_by_date()
items = []
for dd, s in sleeps.items():
items.append({
'date' : dd, # TODO not sure... # TODO would also be great to sync column names...
'sleep_start': s.sleep_start,
'sleep_end' : s.sleep_end,
'bed_time' : s.bed_time,
})
# TODO tz is in sleeps json
res = pd.DataFrame(items)
return res
def test_tz():
sleeps = sleeps_by_date()
for s in sleeps.values():
assert s.sleep_start.tzinfo is not None
assert s.sleep_end.tzinfo is not None
for dd, exp in [
(date(year=2015, month=8 , day=28), time(hour=7, minute=20)),
(date(year=2015, month=9 , day=15), time(hour=6, minute=10)),
]:
sleep = sleeps[dd]
end = sleep.sleep_end
assert end.time() == exp
# TODO fuck. on 0909 I woke up at around 6 according to google timeline
# but according to jawbone, it was on 0910?? eh. I guess it's jus shitty tracking.
def main():
# TODO eh. vendorize klogging already?
from kython.klogging import setup_logzero
setup_logzero(get_logger())
test_tz()
# print(get_dataframe())
if __name__ == '__main__':
main()

View file

@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# TODO # TODO this should be in dashboard
from pathlib import Path from pathlib import Path
# from kython.plotting import * # from kython.plotting import *
from csv import DictReader from csv import DictReader

23
tests/jawbone.py Normal file
View file

@ -0,0 +1,23 @@
# TODO depends on my private data... move somewhere, and exclude from CI somehow?
from datetime import date, time
from my.jawbone import sleeps_by_date
# todo private test.. move away
def test_tz():
sleeps = sleeps_by_date()
for s in sleeps.values():
assert s.sleep_start.tzinfo is not None
assert s.sleep_end.tzinfo is not None
for dd, exp in [
(date(year=2015, month=8 , day=28), time(hour=7, minute=20)),
(date(year=2015, month=9 , day=15), time(hour=6, minute=10)),
]:
sleep = sleeps[dd]
end = sleep.sleep_end
assert end.time() == exp
# TODO fuck. on 0909 I woke up at around 6 according to google timeline
# but according to jawbone, it was on 0910?? eh. I guess it's just shitty tracking.