diff --git a/my/arbtt.py b/my/arbtt.py new file mode 100644 index 0000000..2ef82a4 --- /dev/null +++ b/my/arbtt.py @@ -0,0 +1,93 @@ +''' +[[https://github.com/nomeata/arbtt#arbtt-the-automatic-rule-based-time-tracker][Arbtt]] time tracking +''' + +REQUIRES = ['ijson', 'cffi'] + + +from pathlib import Path +from typing import Sequence, Iterable, List, Optional + + +def inputs() -> Sequence[Path]: + try: + from my.config import arbtt as user_config + except ImportError: + from .core.warnings import low + low("Couldn't find 'arbtt' config section, falling back to the default capture.log (usually in HOME dir). Add 'arbtt' section with logfiles = '' to suppress this warning.") + return [] + else: + from .core import get_files + return get_files(user_config.logfiles) + + +from .core import dataclass, Json, Res, PathIsh, datetime_aware +from .core.common import isoparse + + +@dataclass +class Entry: + ''' + For the format reference, see + https://github.com/nomeata/arbtt/blob/e120ad20b9b8e753fbeb02041720b7b5b271ab20/src/DumpFormat.hs#L39-L46 + ''' + + json: Json + # inactive time -- in ms + + @property + def dt(self) -> datetime_aware: + # contains utc already + ds = self.json['date'] + elen = 27 + lds = len(ds) + if lds < elen: + # ugh. sometimes contains less that 6 decimal points + ds = ds[:-1] + '0' * (elen - lds) + 'Z' + elif lds > elen: + # ahd sometimes more... + ds = ds[:elen - 1] + 'Z' + + return isoparse(ds) + + @property + def active(self) -> Optional[str]: + # NOTE: WIP, might change this in the future... + ait = (w for w in self.json['windows'] if w['active']) + a = next(ait, None) + if a is None: + return None + a2 = next(ait, None) + assert a2 is None, a2 # hopefully only one can be active in a time? + + p = a['program'] + t = a['title'] + # todo perhaps best to keep it structured, e.g. for influx + return f'{p}: {t}' + + +# todo multiple threads? not sure if would help much... (+ need to find offset somehow?) +def entries() -> Iterable[Res[Entry]]: + inps = list(inputs()) + + base: List[PathIsh] = ['arbtt-dump', '--format=json'] + + cmds: List[List[PathIsh]] + if len(inps) == 0: + cmds = [base] # rely on default + else: + # otherise, 'merge' them + cmds = [base + ['--logfile', f] for f in inps] + + import ijson.backends.yajl2_cffi as ijson # type: ignore + from subprocess import Popen, PIPE + for cmd in cmds: + with Popen(cmd, stdout=PIPE) as p: + out = p.stdout; assert out is not None + for json in ijson.items(out, 'item'): + yield Entry(json=json) + + +from .core import stat, Stats +def stats() -> Stats: + return stat(entries) diff --git a/my/config.py b/my/config.py index b6f91e6..fb502c3 100644 --- a/my/config.py +++ b/my/config.py @@ -69,3 +69,7 @@ class time: class orgmode: paths: Paths + + +class arbtt: + logfiles: Paths diff --git a/my/core/__init__.py b/my/core/__init__.py index 8567cd7..f680f37 100644 --- a/my/core/__init__.py +++ b/my/core/__init__.py @@ -4,6 +4,7 @@ from .common import get_files from .common import LazyLogger from .common import warn_if_empty from .common import stat, Stats +from .common import datetime_naive, datetime_aware from .cfg import make_config from .util import __NOT_HPI_MODULE__ diff --git a/my/core/common.py b/my/core/common.py index 0ec8448..b39a10a 100644 --- a/my/core/common.py +++ b/my/core/common.py @@ -552,3 +552,7 @@ def to_jsons(it) -> Iterable[Json]: yield error_to_json(r) else: yield asdict(r) + + +datetime_naive = datetime +datetime_aware = datetime diff --git a/my/hypothesis.py b/my/hypothesis.py index 40fbb6c..bd0aedc 100644 --- a/my/hypothesis.py +++ b/my/hypothesis.py @@ -67,9 +67,8 @@ def pages() -> List[Res[Page]]: return sort_res_by(_dal().pages(), key=key) -# todo not public api yet -def stats(): - from .core import stat +from .core import stat, Stats +def stats() -> Stats: return { **stat(highlights), **stat(pages), diff --git a/my/location/google.py b/my/location/google.py index 96bdf5e..c200b06 100644 --- a/my/location/google.py +++ b/my/location/google.py @@ -41,6 +41,7 @@ TsLatLon = Tuple[int, int, int] def _iter_via_ijson(fo) -> Iterable[TsLatLon]: # ijson version takes 25 seconds for 1M items (without processing) + # todo extract to common? try: # pip3 install ijson cffi import ijson.backends.yajl2_cffi as ijson # type: ignore diff --git a/tox.ini b/tox.ini index 270f2ed..9bd8464 100644 --- a/tox.ini +++ b/tox.ini @@ -76,6 +76,7 @@ commands = hpi module install my.reddit hpi module install my.stackexchange.stexport hpi module install my.pinboard + hpi module install my.arbtt # todo fuck. -p my.github isn't checking the subpackages?? wtf... # guess it wants .pyi file?? @@ -94,6 +95,7 @@ commands = -p my.location.google \ -p my.time.tz.via_location \ -p my.calendar.holidays \ + -p my.arbtt \ --txt-report .coverage.mypy-misc \ --html-report .coverage.mypy-misc \ {posargs}