116 lines
3.4 KiB
Python
116 lines
3.4 KiB
Python
'''
|
|
[[https://github.com/nomeata/arbtt#arbtt-the-automatic-rule-based-time-tracker][Arbtt]] time tracking
|
|
'''
|
|
|
|
from __future__ import annotations
|
|
|
|
REQUIRES = ['ijson', 'cffi']
|
|
# NOTE likely also needs libyajl2 from apt or elsewhere?
|
|
|
|
|
|
from collections.abc import Iterable, Sequence
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
|
|
|
|
def inputs() -> Sequence[Path]:
|
|
try:
|
|
from my.config import arbtt as user_config
|
|
except ImportError:
|
|
from my.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 my.core import Json, PathIsh, datetime_aware
|
|
from my.core.compat import fromisoformat
|
|
|
|
|
|
@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
|
|
# TODO after python>=3.11, could just use fromisoformat
|
|
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:
|
|
# and sometimes more...
|
|
ds = ds[:elen - 1] + 'Z'
|
|
|
|
return fromisoformat(ds)
|
|
|
|
@property
|
|
def active(self) -> str | None:
|
|
# 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[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:
|
|
# otherwise, 'merge' them
|
|
cmds = [[*base, '--logfile', f] for f in inps]
|
|
|
|
from subprocess import PIPE, Popen
|
|
|
|
import ijson.backends.yajl2_cffi as ijson # type: ignore
|
|
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)
|
|
|
|
|
|
def fill_influxdb() -> None:
|
|
from .core.freezer import Freezer
|
|
from .core.influxdb import magic_fill
|
|
freezer = Freezer(Entry)
|
|
fit = (freezer.freeze(e) for e in entries())
|
|
# TODO crap, influxdb doesn't like None https://github.com/influxdata/influxdb/issues/7722
|
|
# wonder if can check it statically/warn?
|
|
fit = (f for f in fit if f.active is not None)
|
|
|
|
# todo could tag with computer name or something...
|
|
# todo should probably also tag with 'program'?
|
|
magic_fill(fit, name=f'{entries.__module__}:{entries.__name__}')
|
|
|
|
|
|
from .core import Stats, stat
|
|
|
|
|
|
def stats() -> Stats:
|
|
return stat(entries)
|